Skip to main content

HeadscaleClient

Struct HeadscaleClient 

Source
pub struct HeadscaleClient { /* private fields */ }
Expand description

HTTP client for a running Headscale coordinator.

The Headscale REST API lives at <server_url>/api/v1/…. Requests are authenticated with an API key in the Authorization: Bearer <key> header. The API key must be created on the coordinator with headscale apikeys create or the equivalent API call; store it in the vault as headscale-api-key.

Implementations§

Source§

impl HeadscaleClient

Source

pub fn new( base_url: impl Into<String>, api_key: impl Into<String>, ) -> Result<Self>

Construct a client from explicit credentials.

Source

pub fn from_vault_or_env() -> Result<Option<Self>>

Open a client from the vault (slot headscale-api-key + mesh-url) with env-var fallbacks (HEADSCALE_API_KEY + HEADSCALE_URL).

Returns Ok(None) when either credential is absent — callers can decide whether that’s fatal.

Source

pub async fn create_preauth_key( &self, user: &str, tags: &[String], ) -> Result<PreauthKey>

Generate a single-use pre-auth key for the given ACL tags.

The key expires in 1 hour and is non-reusable — suitable for one-shot machine onboarding via cloud-init. Each yah cloud machine provision call that uses Headscale mesh should request its own key.

Source

pub async fn create_preauth_key_with( &self, req: &PreauthKeyRequest, ) -> Result<PreauthKey>

Mint a pre-auth key with an explicit lifetime and reuse policy.

The general form behind create_preauth_key. A reconciled desired state wants a standing key (reusable, long TTL) rather than the one-shot hour-long key a single provision needs, and both shapes are the same POST /api/v1/preauthkey.

Source

pub async fn list_nodes(&self) -> Result<Vec<NodeInfo>>

List all nodes currently in the tailnet.

Uses Headscale’s GET /api/v1/node (the endpoint was renamed from the pre-v0.23 /api/v1/machine, with the response key machines → nodes, when Headscale retired “machine” for “node”). The per-node JSON shape is otherwise unchanged (id, name, ipAddresses, online).

Source

pub async fn list_users(&self) -> Result<Vec<String>>

List the Headscale users (namespaces) preauth keys can be minted against.

GET /api/v1/user. Headscale scopes every preauth key to a user, and a key minted against a user that does not exist fails at mint time.

Source

pub async fn resolve_user(&self, preferred: Option<&str>) -> Result<String>

Resolve the user to mint a preauth key against, asking the coordinator instead of assuming a name.

R608-B19: this used to be the literal "default" at the yah cloud machine provision call site, and against the live coordinator that is simply wrong — POST /api/v1/preauthkey answers 500 for a user that does not exist. The two halves of the system disagree on the name: crate::mesh’s camp-local yah mesh start path creates default (its --user default), while yah mesh bootstrap creates yah (mint_bootstrap_preauth_key in yubaba’s lib.rs runs headscale users create yah). The production coordinator was bootstrapped, so it has only yah — every provision against it would have failed at the preauth step with an opaque 500.

Rather than swap one hardcoded guess for the other:

  • preferred present and known to the coordinator → use it;
  • preferred absent and the coordinator has exactly one user → use it, which is every yah mesh in existence today;
  • otherwise → error naming the users that DO exist, so the operator can pick, instead of a 500 that names nothing.
Source

pub async fn create_user(&self, name: &str) -> Result<()>

Create a Headscale user (namespace). POST /api/v1/user.

NOT idempotent on headscale’s side — creating a name that already exists answers 500 with no useful discriminator — so every caller must list_users first. That list-then-create shape is what HeadscaleReconciler does, and it is why this method deliberately does not try to swallow a conflict itself: a 500 here means something other than “already there”.

Source

pub async fn list_preauth_keys( &self, user: &str, ) -> Result<Vec<PreauthKeyRecord>>

List the pre-auth keys minted against user. GET /api/v1/preauthkey?user=<user>.

Source

pub async fn get_policy(&self) -> Result<String>

Read the ACL policy the coordinator currently has loaded. GET /api/v1/policy → {"policy": "<HuJSON>", "updatedAt": …}.

Works in BOTH policy modes — measured against the live v0.23.0 coordinator on 2026-09-04, which was still on policy.mode: file and answered 200 with the file’s contents. That is what makes drift detectable on a not-yet-migrated coordinator even though set_policy cannot correct it there.

R861-T2 read the pinned v0.23.0 GetPolicy handler rather than inferring: in database mode it returns the stored row’s Data string verbatim, in file mode the file’s bytes as-is. Same field, same document, so a caller never has to know which mode answered.

One consequence worth knowing: a database-mode coordinator with no policy row yet answers with a gRPC error, not an empty string — the migration treats that as “nothing pushed yet”, not as a fault.

Source

pub async fn set_policy(&self, hujson: &str) -> Result<()>

Replace the ACL policy. PUT /api/v1/policy.

Only legal when the coordinator runs policy.mode: database — the mode POLICY_MODE now renders. In file mode the pinned v0.23.0 SetPolicy handler returns ErrPolicyUpdateIsDisabled before looking at the payload at all, because the file on disk is the source of truth and a write here would be silently overwritten at the next reload. Callers get that refusal as an error rather than a no-op success.

Two properties R861-T2 established from that handler, both load-bearing for the migration:

  • It validates before storing: the document is parsed with the same LoadACLPolicyFromBytes a file-mode startup uses, then compiled against the live node list (CompileFilterRules, and CompileSSHPolicy when any node exists). A policy headscale would refuse to boot on is refused here too, which is why a failed push is safe.
  • It APPENDS: db.SetPolicy inserts a new policies row every call and GetPolicy reads ORDER BY id DESC LIMIT 1. Repeating a push is therefore idempotent in effect but not in storage — which is why the reconciler and the migration both compare before writing.
Source

pub async fn health(&self) -> HeadscaleHealth

Light health check — HEAD or GET the Headscale root, no auth required.

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<T> Downcast for T
where T: Any,

Source§

fn into_any(self: Box<T>) -> Box<dyn Any>

Convert Box<dyn Trait> (where Trait: Downcast) to Box<dyn Any>. Box<dyn Any> can then be further downcast into Box<ConcreteType> where ConcreteType implements Trait.
Source§

fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>

Convert Rc<Trait> (where Trait: Downcast) to Rc<Any>. Rc<Any> can then be further downcast into Rc<ConcreteType> where ConcreteType implements Trait.
Source§

fn as_any(&self) -> &(dyn Any + 'static)

Convert &Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot generate &Any’s vtable from &Trait’s.
Source§

fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)

Convert &mut Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot generate &mut Any’s vtable from &mut Trait’s.
Source§

impl<T> Downcast for T
where T: Any,

Source§

fn into_any(self: Box<T>) -> Box<dyn Any>

Converts Box<dyn Trait> (where Trait: Downcast) to Box<dyn Any>, which can then be downcast into Box<dyn ConcreteType> where ConcreteType implements Trait.
Source§

fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>

Converts Rc<Trait> (where Trait: Downcast) to Rc<Any>, which can then be further downcast into Rc<ConcreteType> where ConcreteType implements Trait.
Source§

fn as_any(&self) -> &(dyn Any + 'static)

Converts &Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot generate &Any’s vtable from &Trait’s.
Source§

fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)

Converts &mut Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot generate &mut Any’s vtable from &mut Trait’s.
Source§

impl<T> DowncastSend for T
where T: Any + Send,

Source§

fn into_any_send(self: Box<T>) -> Box<dyn Any + Send>

Converts Box<Trait> (where Trait: DowncastSend) to Box<dyn Any + Send>, which can then be downcast into Box<ConcreteType> where ConcreteType implements Trait.
Source§

impl<T> DowncastSync for T
where T: Any + Send + Sync,

Source§

fn into_any_arc(self: Arc<T>) -> Arc<dyn Any + Send + Sync> ⓘ

Convert Arc<Trait> (where Trait: Downcast) to Arc<Any>. Arc<Any> can then be further downcast into Arc<ConcreteType> where ConcreteType implements Trait.
Source§

impl<T> DowncastSync for T
where T: Any + Send + Sync,

Source§

fn into_any_sync(self: Box<T>) -> Box<dyn Any + Send + Sync>

Converts Box<Trait> (where Trait: DowncastSync) to Box<dyn Any + Send + Sync>, which can then be downcast into Box<ConcreteType> where ConcreteType implements Trait.
Source§

fn into_any_arc(self: Arc<T>) -> Arc<dyn Any + Send + Sync> ⓘ

Converts Arc<Trait> (where Trait: DowncastSync) to Arc<Any>, which can then be downcast into Arc<ConcreteType> where ConcreteType implements Trait.
Source§

impl<T> ErasedDestructor for T
where T: 'static,

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Fruit for T
where T: Send + Downcast,

Source§

impl<A, B, T> HttpServerConnExec<A, B> for T
where B: Body,

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self> ⓘ

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self> ⓘ

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ

Converts self into a Left variant of Either<Self, Self> if into_left is true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
where F: FnOnce(&Self) -> bool,

Converts self into a Left variant of Either<Self, Self> if into_left(&self) returns true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

impl<T> Pointable for T

Source§

const ALIGN: usize

The alignment of pointer.
Source§

type Init = T

The type for initializers.
Source§

unsafe fn init(init: <T as Pointable>::Init) -> usize

Initializes a with the given initializer. Read more
Source§

unsafe fn deref<'a>(ptr: usize) -> &'a T

Dereferences the given pointer. Read more
Source§

unsafe fn deref_mut<'a>(ptr: usize) -> &'a mut T

Mutably dereferences the given pointer. Read more
Source§

unsafe fn drop(ptr: usize)

Drops the object pointed to by the given pointer. Read more
Source§

impl<T> PolicyExt for T
where T: ?Sized,

Source§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow only if self and other return Action::Follow. Read more
Source§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow if either self or other returns Action::Follow. Read more
Source§

impl<T> Read<Exclusive, BecauseExclusive> for T
where T: ?Sized,

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

Source§

fn vzip(self) -> V

Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self> ⓘ
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self> ⓘ

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more