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

api

package
v0.6.0 Latest Latest
Warning

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

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

Documentation

Overview

Package api is tailkeeper's Tailscale API client for reads (spec §4, §20).

Responses are decoded into tailkeeper's own allowlisted structs: any field a struct does not declare is dropped, so a credential the API returns in a field nobody listed can never reach an output. Tailscale's Go client library is used only for workload identity federation.

Index

Constants

View Source
const DefaultBaseURL = "https://api.tailscale.com"

DefaultBaseURL is the only API host tailkeeper talks to by default.

Variables

This section is empty.

Functions

func ETagOf added in v0.6.0

func ETagOf(body []byte) string

ETagOf is the ETag the API gives a policy whose plain body is body: the SHA-256 of the bytes as lower-case hex, in quotes (probe A-13). A text that is not the body as the API sent it has another ETag.

func Path

func Path(format string, segments ...string) string

Path builds /api/v2/<format> with each %s replaced by a path-escaped segment.

Types

type Client

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

Client performs authenticated reads against one tailnet.

func New

func New(ctx context.Context, tailnet string, cred creds.Read, opt Options) (*Client, error)

New builds a Client for tailnet using cred. It makes no network call. ctx must outlive the Client: token requests made on its behalf stop when ctx ends, which is how a signal reaches a hung token exchange.

func (*Client) BeginValidate added in v0.6.0

func (c *Client) BeginValidate(ctx context.Context, posts int) (*ValidateCall, error)

BeginValidate starts a call that will send posts policies (1, or 2 for a call with invariants). It waits for a free slot (or for ctx), then takes posts tokens at once or none: when fewer are left it frees the slot and returns validate_rate_limited, whose retryAfterSeconds is the whole seconds, rounded up, until posts tokens exist. A call that ends having sent fewer gets the rest back.

func (*Client) BeginValidateCharged added in v0.6.0

func (c *Client) BeginValidateCharged(ctx context.Context, posts, tokens int) (*ValidateCall, error)

BeginValidateCharged is BeginValidate for a call that sends posts policies and is charged tokens in all, tokens being at least posts. The tokens beyond the posts pay for what the call asks of Tailscale besides sending: they are taken with the rest, at once or not at all, and End does not give them back. A call that asks for more tokens than the bucket holds can never be granted: it is refused at once, with an error that says so and not one that tells its caller to wait.

func (*Client) Get

func (c *Client) Get(ctx context.Context, path string, q url.Values, out any) error

Get performs a GET of path (already escaped, see TailnetPath and Path) and decodes the JSON body into out. Reads are retried on 429, 500, 502, 503, 504 and transport errors (client timeouts included) with bounded backoff; Retry-After is honoured when present (up to 30s). The caller's cancellation is never retried.

func (*Client) GetOnce added in v0.6.0

func (c *Client) GetOnce(ctx context.Context, path string, q url.Values) (RawResponse, error)

GetOnce performs a GET of path (already escaped, see TailnetPath) with the query q and returns the answer, whatever its status: one attempt, no retry on a 429 or a 5xx, no Accept header. Get and GetRaw retry; the probes must not, because a second attempt is a request they did not plan.

func (*Client) GetPolicy added in v0.6.0

func (c *Client) GetPolicy(ctx context.Context, g PolicyGet) (RawResponse, error)

GetPolicy performs GET .../acl as g says and returns the answer: the status, the ETag and the body unparsed. One attempt, like ValidatePolicy.

func (*Client) GetPolicyDetails added in v0.6.0

func (c *Client) GetPolicyDetails(ctx context.Context) (PolicyDetails, error)

GetPolicyDetails reads the live policy with its warnings and errors in one request: GET .../acl?details=true, retried like any read. The answer is read strictly: the acl field is required and standard base64, warnings and errors are null or lists of strings, and any other shape is an error, never a zero-value success. A decoded policy over guard.MaxPolicyBytes is refused with its size. Text is not parsed here: what it holds, an array say, is for acl.Parse to refuse.

func (*Client) GetRaw

func (c *Client) GetRaw(ctx context.Context, path, accept string) (Raw, error)

GetRaw performs a GET of path with the given Accept type and returns the body unparsed, for formats that are not JSON (the policy file's HuJSON). It is retried and bounded like Get.

func (*Client) GrantedScopes

func (c *Client) GrantedScopes() (scopes []string, known bool)

GrantedScopes returns the scopes the token endpoint reported, once a token has been fetched. known is false for API keys and federated identities.

func (*Client) LiveStamp added in v0.6.0

func (c *Client) LiveStamp(ctx context.Context) (PolicyStamp, error)

LiveStamp reads the live policy once more, as the plain text, and returns its ETag and the SHA-256 of its bytes. It is the re-check after a validation: the API never answered 304 to If-None-Match (probe A-8), so nothing sends that header, and the whole body is read again. A policy over guard.MaxPolicyBytes is refused with its size.

func (*Client) Tailnet

func (c *Client) Tailnet() string

Tailnet returns the tailnet ID this client is bound to.

func (*Client) TailnetPath

func (c *Client) TailnetPath(suffix string) string

TailnetPath returns /api/v2/tailnet/<tailnet><suffix>.

func (*Client) ValidatePolicy added in v0.6.0

func (c *Client) ValidatePolicy(ctx context.Context, body []byte, contentType string) (RawResponse, error)

ValidatePolicy sends body to POST .../acl/validate with the given Content-Type and returns the answer. The endpoint chooses its mode from the body: a JSON array is a set of tests run against the live policy (send application/json), a JSON object is a policy to validate (send application/hujson, the policy's exact bytes). The call changes nothing on the tailnet. It makes one attempt: no retry on a 429 or a 5xx, and no check that the body suits the content type (the probes send mismatches on purpose). A transport failure, and a body over the client's limit, are errors.

func (*Client) ValidateTests

func (c *Client) ValidateTests(ctx context.Context, tests []PolicyTest) (Validation, error)

ValidateTests runs tests against the live policy without changing it: POST .../acl/validate with a JSON array of tests (tests mode), the one request other than GET this client sends. The endpoint answers HTTP 200 for pass, fail and warnings alike, so a pass is read only from a recognised shape: a blank body, {}, or data holding at least one entry. An empty list, a null list and an empty message are errors, never a pass. It is retried like a read: the endpoint changes nothing. The call is metered like every send of the validate POST: it takes one token of the bucket and a slot among the calls in flight, and ends at the call's deadline (bucket.go).

func (*Client) WithMaxBody

func (c *Client) WithMaxBody(n int64) *Client

WithMaxBody returns a copy of c that refuses response bodies over n bytes, or over c's own limit if that is lower. The copy shares c's credential, token cache and observed scopes.

type Options

type Options struct {
	BaseURL   string                                     // default DefaultBaseURL
	Transport http.RoundTripper                          // default http.DefaultTransport
	Timeout   time.Duration                              // per request; default 60s
	Retries   int                                        // read retries; default 3; negative disables
	MaxBody   int64                                      // default 64 MiB
	Sleep     func(context.Context, time.Duration) error // default: timer honouring ctx

	// The limits on validation requests (bucket.go). Zero values are the production defaults.
	ValidatePerMinute int              // tokens a minute, one for each validation request tailkeeper decides to send (a retry of a request takes none); the bucket holds max(this, 2); default config.DefaultValidatePerMinute (30)
	Now               func() time.Time // the bucket's clock; default time.Now
	ValidateDeadline  time.Duration    // one validate call, all its sends; default 60s
	ValidateInFlight  int              // validate calls at once; default 2
}

Options tune a Client. Zero values are production defaults.

type PolicyDetails added in v0.6.0

type PolicyDetails struct {
	Text     []byte   // the policy as HuJSON, decoded from the base64 acl field; at most guard.MaxPolicyBytes
	ETag     string   // the header verbatim, quotes included; "" when none
	Warnings []string // the server's own warnings for the stored policy: the baseline
	Errors   []string // the stored policy's errors: it no longer parses or resolves
}

PolicyDetails is the live policy as GET .../acl?details=true answers it.

type PolicyGet added in v0.6.0

type PolicyGet struct {
	Details     bool   // adds ?details=true: a JSON object with the policy, its warnings and errors
	Accept      string // "" sends no Accept header
	IfNoneMatch string // "" sends no If-None-Match header; otherwise the value, verbatim
}

PolicyGet selects how GetPolicy asks for the live policy.

type PolicyStamp added in v0.6.0

type PolicyStamp struct {
	ETag   string // the header verbatim, quotes included; "" when none
	SHA256 string // lower-case hex of the plain body's bytes
}

PolicyStamp identifies the live policy without carrying it.

type PolicyStatus added in v0.6.0

type PolicyStatus string

PolicyStatus is what an answer of the validate endpoint to a policy (policy mode) means. The reader trusts nothing it does not recognise: a pass is read only from a shape the documentation or a probe of the live API showed, and everything else is PolicyUnknown, which a caller must treat as "not validated".

const (
	PolicyPass     PolicyStatus = "pass"     // HTTP 200 and a recognised pass; warnings may ride along
	PolicyFail     PolicyStatus = "fail"     // HTTP 200, at least one entry with errors
	PolicyRejected PolicyStatus = "rejected" // a bare {message}: the server refused the document
	PolicyUnknown  PolicyStatus = "unknown"  // anything else, including every Assumed shape
)

The four things a policy-mode answer can mean.

type PolicyTest

type PolicyTest struct {
	Src             string         `json:"src"`
	Proto           string         `json:"proto,omitempty"`
	SrcPostureAttrs map[string]any `json:"srcPostureAttrs,omitempty"` // string, number or bool values, passed through as the caller gave them
	Accept          []string       `json:"accept,omitempty"`
	Deny            []string       `json:"deny,omitempty"`
}

PolicyTest is one test of the live policy, in the shape Tailscale's validate endpoint takes in tests mode.

type PolicyValidation added in v0.6.0

type PolicyValidation struct {
	Status  PolicyStatus
	HTTP    int
	Message string       // the server's message: untrusted text
	Results []TestResult // entries with their errors and warnings, in order; set for pass and fail only
	Why     string       // for PolicyUnknown: tailkeeper's words, quoting nothing but capped untrusted tokens
	Hint    string       // set by the client's send, never by the reader: the size hint of a rejection that speaks of size
}

PolicyValidation is a policy-mode answer as the strict reader understood it.

func ReadPolicyValidation added in v0.6.0

func ReadPolicyValidation(status int, body []byte) PolicyValidation

ReadPolicyValidation reads the answer of the validate endpoint to a policy:

  • 200 with an empty body or {} is a pass;
  • 200 {message, data[]}: any entry with errors is a fail; entries with warnings only are a pass whose Results carry them, if the message reports warnings or is absent; an entry with neither is unknown;
  • 200 or 400 with exactly the key message is a rejection (the live API rejects a well-formed document with a 200 and a bare message, and a body that is no policy at all with a 400), unless the message says "test(s) failed" or "warning(s) found": it then promises entries that are not there, which is unknown;
  • anything else is unknown: a 400 that carries data, any other status, a field the documentation does not name, a field given twice, a non-object, trailing bytes, a comment, a byte order mark.

body is at most what the client read (the policy call reads 1 MiB); the message is not cut here.

func RemoveCanary added in v0.6.0

func RemoveCanary(v PolicyValidation, src string, grammar func(line string) bool) (PolicyValidation, bool)

RemoveCanary drops the one result whose User is src, which must carry exactly one error and no warning, and sets the status from what is left: a fail while an entry has errors, else a pass, which carries no message. It reports false, and returns v unchanged, when v is not a fail, when no result or more than one has that User (compared exactly), or when the one has anything but a single error. grammar, when not nil, is the caller's check of that one error: a canary that comes back in another shape is not a pass. The check lives in grammar, not in a second copy here.

type Raw

type Raw struct {
	Body []byte
	ETag string
}

Raw is a response body as the API sent it, and its ETag (verbatim, quotes included; "" when the API sent none).

type RawResponse added in v0.6.0

type RawResponse struct {
	Status int
	ETag   string
	Body   []byte
}

RawResponse is one answer of the API, whatever its status: the HTTP status, the ETag header verbatim (quotes included; "" when the API sent none) and the body unparsed. The low-level calls below return a non-2xx answer as data, not as an error, because the probes exist to see what the API says to a request that fails. A later, strict reader decides what each status means.

type TestResult

type TestResult struct {
	User     string   `json:"user"`
	Errors   []string `json:"errors"`
	Warnings []string `json:"warnings"`
}

TestResult is the validate endpoint's answer for one source.

type ValidateCall added in v0.6.0

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

ValidateCall is one tool call's use of the validate endpoint: the tokens it reserved, its deadline and its slot among the calls in flight. One goroutine uses it. It must be ended.

func (*ValidateCall) Context added in v0.6.0

func (v *ValidateCall) Context() context.Context

Context is the context of the call: the caller's, with the call's deadline.

func (*ValidateCall) End added in v0.6.0

func (v *ValidateCall) End()

End frees the call's slot, stops its deadline and gives back the tokens it did not use. It may be called more than once.

func (*ValidateCall) Send added in v0.6.0

func (v *ValidateCall) Send(body []byte) (PolicyValidation, error)

Send validates a policy in policy mode. Each Send uses one of the tokens the call reserved; the retries inside it use none. A send past the call's deadline fails with upstream_unavailable.

type Validation

type Validation struct {
	Passed  bool         // no test failed
	Results []TestResult // sources with errors or warnings
}

Validation is how the live policy answered a set of tests.

Directories

Path Synopsis
Package apitest is a fake Tailscale API for tests.
Package apitest is a fake Tailscale API for tests.
Package openapi pins the operations of Tailscale's OpenAPI document (spec §20).
Package openapi pins the operations of Tailscale's OpenAPI document (spec §20).

Jump to

Keyboard shortcuts

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