pub struct Device { /* private fields */ }Expand description
How a program connects to a tailnet and communicates with peers.
The Device connects to the control plane, registers itself with the tailnet, and communicates
with tailnet peers. Its tailnet identity is determined by the key state provided at
construction-time.
Implementations§
Source§impl Device
impl Device
Authorize an incoming Tailscale SSH connection from remote requesting local user
requested_user, against the control-pushed SSH policy.
Fail-closed. This is the Rust analogue of Go tailssh’s policy evaluation. It:
- resolves
remote’s IP to a known tailnet peer — an unknown source is denied; - fetches the current
SshPolicy— no policy means deny-all; - evaluates the policy (first-match-wins, default-deny) against the peer’s identity.
Returns the SshDecision. Callers MUST reject the connection on any
SshDecision::Deny. Any lookup error is surfaced as Err and must also be treated as a
rejection by the caller — the connection is never allowed on the error path.
NOTE: userLogin-principal matching requires the connecting peer’s owner login, which this
fork’s domain node model does not yet retain (it is reported as None); such principals
therefore never match here. Node-id / node-IP / any principals match normally.
Source§impl Device
impl Device
Sourcepub async fn serve_ssh<H>(
self: Arc<Self>,
config: Config,
listen_addr: SocketAddr,
) -> Result<(), Error>
pub async fn serve_ssh<H>( self: Arc<Self>, config: Config, listen_addr: SocketAddr, ) -> Result<(), Error>
Serve an ssh service on the given TCP address.
This is a minimal helper that just wires up the relevant pieces. All the
authentication and actual SSH server logic must be implemented by the caller in
the TailnetServer (H) and configured by config.
Sourcepub async fn listen_ssh(
self: Arc<Self>,
config: Config,
listen_addr: SocketAddr,
) -> Result<(), Error>
pub async fn listen_ssh( self: Arc<Self>, config: Config, listen_addr: SocketAddr, ) -> Result<(), Error>
Run a turnkey Tailscale SSH server on listen_addr (tailnet overlay) that grants authorized
connections an interactive login shell as their policy-mapped local user.
Authorization is the control-pushed SSH policy (see [Device::authorize_ssh]) — fail-closed:
unknown source, no policy, no matching rule, or any error rejects. The accepted connection’s
local_user is resolved against the local passwd database and the login shell is spawned in
a PTY after dropping privileges to that user’s uid/gid (the daemon must run as root to do
so; if it cannot, the session fails closed). Mirrors Go tailssh’s incubator shell path.
Only the interactive login-shell path is implemented: pty-req → <shell> -l,
window-change → TIOCSWINSZ, and an exit-status on shell exit. The exec form
(<shell> -c <cmd>) is not supported because ChannelEvent does not surface an SSH
exec request in this fork’s channel abstraction.
Sourcepub async fn serve_ssh_tui<App>(
self: Arc<Self>,
config: Config,
listen_addr: SocketAddr,
) -> Result<(), Error>
pub async fn serve_ssh_tui<App>( self: Arc<Self>, config: Config, listen_addr: SocketAddr, ) -> Result<(), Error>
Serve an SSH TUI service on the given TCP address.
Wrapper around serve_ssh to specifically use
ChannelServer around a RatatuiTerm using App.
Source§impl Device
impl Device
Sourcepub async fn new_with_secret(
config: &Config,
auth_key: Option<SecretString>,
) -> Result<Self, Error>
pub async fn new_with_secret( config: &Config, auth_key: Option<SecretString>, ) -> Result<Self, Error>
Create a device from the given Config and a SecretString auth key.
This is a back-compat-preserving convenience over new for callers that already
hold the registration auth key as a secrecy::SecretString (e.g. a daemon that keeps the
pre-auth key wrapped end-to-end). It lets the caller avoid materializing a plain String at
the engine boundary: the secret is exposed only on the last inch, immediately before being
handed to new.
§Honesty about the plaintext window
This closes the caller’s boundary, not the engine’s internal handling. The engine still
resolves the auth key to a plain String internally for registration (the plaintext String
window inside the engine is identical to calling new directly) — this method
does not make the engine itself secret-clean. If you call new you create that
String yourself; if you call this you do not, but the engine creates one either way.
Passing None is equivalent to new(config, None) (falls back to config.auth_key).
§Example
let dev = Device::new_with_secret(
&Config::default_with_key_file("tsrs_keys.json").await?,
Some(SecretString::from("MY_AUTH_KEY")),
).await?;Sourcepub async fn ipv4_addr(&self) -> Result<Ipv4Addr, Error>
pub async fn ipv4_addr(&self) -> Result<Ipv4Addr, Error>
Get this Device’s IPv4 tailnet address.
Sourcepub async fn ipv6_addr(&self) -> Result<Ipv6Addr, Error>
pub async fn ipv6_addr(&self) -> Result<Ipv6Addr, Error>
Get this Device’s IPv6 tailnet address.
Sourcepub async fn udp_bind(
&self,
socket_addr: SocketAddr,
) -> Result<UdpSocket, Error>
pub async fn udp_bind( &self, socket_addr: SocketAddr, ) -> Result<UdpSocket, Error>
Bind a UDP socket to the specified SocketAddr.
Returns an error in TUN transport mode (there is no application netstack to bind on).
Sourcepub async fn tcp_listen(
&self,
socket_addr: SocketAddr,
) -> Result<TcpListener, Error>
pub async fn tcp_listen( &self, socket_addr: SocketAddr, ) -> Result<TcpListener, Error>
Bind a TCP listener to the specified SocketAddr.
Returns an error in TUN transport mode (there is no application netstack to listen on).
Sourcepub fn register_fallback_tcp_handler<F>(
&self,
cb: F,
) -> Result<FallbackTcpHandle, Error>
pub fn register_fallback_tcp_handler<F>( &self, cb: F, ) -> Result<FallbackTcpHandle, Error>
Register a fallback TCP handler (like tsnet’s RegisterFallbackTCPHandler).
The callback is consulted for every inbound TCP flow that matches no explicit
Device::tcp_listen listener, with the flow’s (src, dst) addresses. It returns
(handler, intercept):
(_, false)— decline; the next registered callback is tried.(Some(h), true)— claim the flow;his handed the acceptednetstack::TcpStream.(None, true)— claim and reject the flow (the connection is closed).
Multiple handlers may be registered; they are consulted in registration order and the first
to intercept wins. The returned FallbackTcpHandle deregisters the handler when dropped.
Handlers serve flows over the overlay netstack only — never a host socket — and a flow no handler claims is closed (fail-closed), never direct-dialed.
Returns an error in TUN transport mode (there is no application netstack to attach to).
Sourcepub async fn resolve(&self, name: &str) -> Result<Option<Ipv4Addr>, Error>
pub async fn resolve(&self, name: &str) -> Result<Option<Ipv4Addr>, Error>
Resolve a tailnet peer (or this node) by MagicDNS name to its tailnet IPv4 address.
This is an in-process lookup against the netmap we already hold — like tsnet’s in-memory
dnsMap, it does not query any DNS server (there is no 100.100.100.100 resolver). The
name may be a bare hostname or a fully-qualified MagicDNS name, with or without a trailing
dot, in any case (matching is case-insensitive). Returns Ok(None) if no tailnet node has
that name.
Only MagicDNS names are resolved; names outside the tailnet are not looked up here, so the caller’s system resolver remains responsible for them. IPv6 is intentionally not resolved — this fork operates IPv4-only on the tailnet.
Sourcepub async fn connect_by_name(
&self,
name: &str,
port: u16,
) -> Result<TcpStream, Error>
pub async fn connect_by_name( &self, name: &str, port: u16, ) -> Result<TcpStream, Error>
Connect to a tailnet peer by MagicDNS name and port over TCP.
Resolves name via Device::resolve (an in-process netmap lookup, no DNS server), then
dials the resulting tailnet IPv4 address. Returns InternalErrorKind::BadRequest if the
name does not resolve to a tailnet node.
Sourcepub async fn tcp_connect(&self, remote: SocketAddr) -> Result<TcpStream, Error>
pub async fn tcp_connect(&self, remote: SocketAddr) -> Result<TcpStream, Error>
Connect to a TCP socket at the remote address.
Returns an error in TUN transport mode (there is no application netstack to dial from).
Sourcepub async fn loopback(
&self,
) -> Result<(SocketAddr, String, LoopbackHandle), Error>
pub async fn loopback( &self, ) -> Result<(SocketAddr, String, LoopbackHandle), Error>
Start a SOCKS5 proxy on a host loopback address that dials into the tailnet (Go
tsnet.Server.Loopback, SOCKS5 half).
Binds a TCP listener on 127.0.0.1:0 (host loopback only — never an external interface) and
serves SOCKS5 (RFC 1928) with required username/password auth (RFC 1929): username tsnet,
password = the returned proxy_cred. Each CONNECT is dialed INTO the overlay via
Device::connect_by_name / Device::tcp_connect and spliced to the accepted host socket, so
a non-Rust host process can reach tailnet peers through the proxy. Returns the bound address, the
proxy credential, and a LoopbackHandle whose drop stops the listener.
Anti-leak: the listener is loopback-only and every connection egresses over the overlay, never a
host socket — the host’s real origin IP is never used to reach the destination. Unlike Go, the
LocalAPI HTTP surface is not served (this fork exposes status/whois/id-token natively on
Device); only the SOCKS5 proxy is provided.
Returns an error in TUN transport mode (no application netstack to dial from).
Sourcepub async fn self_key_expiry_unix(&self) -> Result<Option<i64>, Error>
pub async fn self_key_expiry_unix(&self) -> Result<Option<i64>, Error>
This node’s key-expiry instant as Unix seconds (Node.KeyExpiry in Go), or Ok(None) if
the key never expires.
Like Go, this fork is reactive about key expiry — it reports it rather than rotating the
node key in the background. A caller can schedule re-authentication around this time; on
expiry, re-create the Device (which re-registers), supplying a fresh node key + the prior
old_node_key to rotate, or the same key to refresh.
Sourcepub async fn self_key_expired(&self) -> Result<bool, Error>
pub async fn self_key_expired(&self) -> Result<bool, Error>
Whether this node’s key has expired as of now (!KeyExpiry.IsZero() && KeyExpiry.Before(now)
in Go). A key with no expiry is never expired. See Device::self_key_expiry_unix for the
reactive-rotation note.
Sourcepub async fn ssh_policy(&self) -> Result<Option<SshPolicy>, Error>
pub async fn ssh_policy(&self) -> Result<Option<SshPolicy>, Error>
Fetch the current Tailscale SSH policy pushed by control, if any.
Returns Ok(None) when control has not sent an SSH policy. The SSH server treats an absent
or empty policy as deny-all (fail-closed). Used by the SSH auth path
(SshPolicy::evaluate) to authorize incoming
connections.
Sourcepub async fn peer_by_name(&self, name: &str) -> Result<Option<NodeInfo>, Error>
pub async fn peer_by_name(&self, name: &str) -> Result<Option<NodeInfo>, Error>
Look up a peer by name.
Sourcepub async fn peer_by_tailnet_ip(
&self,
ip: IpAddr,
) -> Result<Option<NodeInfo>, Error>
pub async fn peer_by_tailnet_ip( &self, ip: IpAddr, ) -> Result<Option<NodeInfo>, Error>
Look up a peer by ip.
Sourcepub async fn peers_with_route(&self, ip: IpAddr) -> Result<Vec<NodeInfo>, Error>
pub async fn peers_with_route(&self, ip: IpAddr) -> Result<Vec<NodeInfo>, Error>
Look up the peer(s) with the most-specific route matches for ip.
This reports which peers advertise a route covering ip, independent of this device’s
accept_routes setting — analogous to the Go client’s informational PrimaryRoutes. It is
not a reachability oracle: with accept_routes off, the dataplane will not actually route
to (or accept return traffic from) advertised subnet routes even if this returns a peer.
Sourcepub fn taildrop_waiting_files(&self) -> Result<Vec<WaitingFile>, Error>
pub fn taildrop_waiting_files(&self) -> Result<Vec<WaitingFile>, Error>
List the Taildrop files this device has fully received and not yet consumed (Go LocalAPI
WaitingFiles).
Returns the files waiting under the configured taildrop_dir, sorted by name. Returns an
empty list when Taildrop is disabled (Config::taildrop_dir unset) — fail-closed, never an
error for the disabled case. A filesystem error while listing surfaces as
InternalErrorKind::Actor.
Sourcepub fn taildrop_open_file(&self, name: &str) -> Result<(File, u64), Error>
pub fn taildrop_open_file(&self, name: &str) -> Result<(File, u64), Error>
Open a received Taildrop file by name for reading, returning the handle and its size (Go
LocalAPI OpenFile).
The name is validated (path-traversal-safe) inside the store before any path is built.
Returns InternalErrorKind::BadRequest when Taildrop is disabled or the name is invalid,
and InternalErrorKind::Actor for a filesystem error (e.g. the file does not exist).
Sourcepub fn taildrop_delete_file(&self, name: &str) -> Result<(), Error>
pub fn taildrop_delete_file(&self, name: &str) -> Result<(), Error>
Delete a received Taildrop file by name (Go LocalAPI DeleteFile).
The name is validated (path-traversal-safe) inside the store before any path is built.
Returns InternalErrorKind::BadRequest when Taildrop is disabled or the name is invalid,
and InternalErrorKind::Actor for a filesystem error (e.g. the file does not exist).
Sourcepub async fn send_file<R>(
&self,
peer: &NodeInfo,
name: &str,
content_length: u64,
reader: R,
) -> Result<(), Error>
pub async fn send_file<R>( &self, peer: &NodeInfo, name: &str, content_length: u64, reader: R, ) -> Result<(), Error>
Send a local file to a tailnet peer via Taildrop (Go PushFile / tailscale file cp).
Pushes content_length bytes from reader to the peer’s peerAPI as
PUT /v0/put/<name> over the overlay netstack — the sending counterpart to the receive store
surfaced by Device::taildrop_waiting_files. The transfer rides the encrypted WireGuard
overlay, never a host socket. The body is streamed from offset 0 (no resume).
The destination is derived solely from peer’s own node record
(NodeInfo::peerapi_addr): its advertised tailnet IPv4 and
peerapi4 port. The caller obtains peer from Device::peer_by_name /
Device::peer_by_tailnet_ip, so it is always a current netmap peer — a raw control-supplied
or attacker-chosen address can never be targeted. As defense in depth, the resolved address is
additionally asserted to be a Tailscale CGNAT IP before dialing.
Returns InternalErrorKind::BadRequest when the peer advertises no IPv4 peerAPI (so it
cannot receive files), when the name is invalid, or when the peer refuses the transfer
(403/409/unexpected status); Error::Timeout on a dial failure or timeout; and
InternalErrorKind::Io on a mid-transfer stream error.
Sourcepub async fn capture_pcap<W>(&self, writer: W) -> Result<(), Error>
pub async fn capture_pcap<W>(&self, writer: W) -> Result<(), Error>
Begin a debug packet capture, streaming a pcap of every packet crossing the dataplane to
writer (Go tsnet.Server.CapturePcap).
Installs a capture hook on the running dataplane: from now until Device::stop_capture is
called (or another capture replaces this one), a copy of every plaintext IP packet on the
datapath — outbound (pre-encrypt) and inbound (post-decrypt) — is framed and written to
writer. The 24-byte pcap global header is written immediately on success.
The format is byte-faithful classic pcap with Tailscale’s LINKTYPE_USER0 + 4-byte path
preamble per record (see ts_runtime::capture); a resulting file opens in Wireshark, and
with Tailscale’s ts-dissector.lua the direction/path of each packet decodes.
The hook runs inline on the single-threaded dataplane step, so writer must not block for
long — a slow writer back-pressures the datapath. Records are not flushed per packet (that
would be a syscall on every packet on the dataplane thread); buffered bytes are flushed when
the writer is dropped on Device::stop_capture. Wrap writer in a std::io::BufWriter if
you want buffering. A write error is swallowed per-packet (the capture silently drops that
record) rather than tearing down the datapath; call Device::stop_capture to end it. Returns
an error only if the dataplane actor is unreachable or the initial global-header write fails.
Sourcepub async fn stop_capture(&self) -> Result<(), Error>
pub async fn stop_capture(&self) -> Result<(), Error>
Stop a debug packet capture started by Device::capture_pcap (Go ClearCaptureSink).
Clears the dataplane capture hook; the writer is dropped (its remaining buffered bytes are
flushed by its own Drop). Idempotent — clearing when no capture is installed is a no-op.
Returns an error only if the dataplane actor is unreachable.
Sourcepub async fn status(&self) -> Result<Status, Error>
pub async fn status(&self) -> Result<Status, Error>
Snapshot of this device and its tailnet peers (like tailscale status).
Combines this node’s self info with the current peer set: each StatusNode reports the
stable id, display name, tailnet IPs, advertised routes, and exit-node flag. (Per-peer
online/user/capabilities are honestly None/empty in this fork — the domain node model
does not yet carry the wire-level liveness/login fields; see ts_runtime::status docs.)
Sourcepub async fn tka_status(&self) -> Result<Option<TkaStatus>, Error>
pub async fn tka_status(&self) -> Result<Option<TkaStatus>, Error>
Fetch the current Tailnet Lock (TKA) status pushed by control, if any.
Returns Ok(None) when control has sent no TKAInfo (tailnet lock not in use, or no change
observed yet). The returned TkaStatus carries the authority head
(a base32 AUMHash, decode with tka::AumHash::from_base32)
and the disablement signal. Signature verification of a peer’s node-key signature against the
authority is performed with the tka module’s tka::Authority.
Sourcepub async fn fetch_id_token(
&self,
audience: &str,
) -> Result<String, IdTokenError>
pub async fn fetch_id_token( &self, audience: &str, ) -> Result<String, IdTokenError>
Request an OIDC ID token from control for this node, scoped to audience (workload-
identity federation, like tailscale’s id-token LocalAPI).
Returns a signed JWT whose sub claim is this node’s MagicDNS name and whose aud claim is
audience, suitable for presenting to a third-party relying party (e.g. AWS/GCP
workload-identity federation). The node is the token subject, not the authenticator — this
is token issuance over the Noise transport (POST /machine/id-token), not a login path.
Requires the control plane to support capability version ≥ 30.
Sourcepub async fn logout(&self) -> Result<(), LogoutError>
pub async fn logout(&self) -> Result<(), LogoutError>
Log this node out of the tailnet — deregister it from the control plane (the equivalent of
Go tsnet’s LocalClient.Logout).
Re-POSTs /machine/register with this node’s current node key and a past expiry, which the
control plane honors by expiring the node now: it drops out of every peer’s netmap and
must re-register (re-authenticate) to rejoin.
This is primarily for non-ephemeral nodes. An ephemeral node is garbage-collected by
control shortly after it disconnects, but a persistent node lingers in the tailnet
(visible to peers, counting against the machine limit) for up to ~24h after the process exits
unless explicitly logged out. Call this before shutdown to deregister
immediately. Calling it on an ephemeral node simply brings the GC forward; it is idempotent,
so logging out an already-gone node is not an error.
This is a control-plane state change only: it does not tear down the local datapath (do
that via shutdown), and it does not delete or rotate the on-disk node key
— re-registering with the same key (a fresh Device::new) is the re-login path.
Sourcepub fn metrics(&self) -> String
pub fn metrics(&self) -> String
Snapshot this node’s client metrics in Prometheus text exposition format.
Mirrors Go Tailscale’s clientmetric registry: process-global counters/gauges incremented
on the datapath hot loops (e.g. magicsock_send_udp, magicsock_recv_data_bytes_udp),
rendered as # TYPE <name> <kind>\n<name> <value>\n per metric, sorted by name. (Go tsnet
exposes no metrics method of its own, so this is the fork’s clean public surface.) The
registry is process-global, so the output covers every Device in the process.
Sourcepub async fn whois(&self, addr: SocketAddr) -> Result<Option<WhoIs>, Error>
pub async fn whois(&self, addr: SocketAddr) -> Result<Option<WhoIs>, Error>
Map a tailnet source addr to the node that owns its IP (like tsnet’s WhoIs).
Only the IP of addr is used; the port is ignored. Returns Ok(None) if no tailnet node
owns that address.
Sourcepub async fn set_exit_node(
&self,
exit_node: Option<ExitNodeSelector>,
) -> Result<(), Error>
pub async fn set_exit_node( &self, exit_node: Option<ExitNodeSelector>, ) -> Result<(), Error>
Change the selected exit node at runtime, without recreating the Device — the equivalent
of Go tsnet’s LocalClient.EditPrefs(ExitNodeID/ExitNodeIP).
The peer may be named by stable node ID, tailnet IP, or MagicDNS name via
ExitNodeSelector (a bare IP or name parses with selector.parse()); this is the same
selector type as Config::exit_node, so the construction-time
and runtime paths are identical. Passing None clears the exit node — internet-bound traffic
is then dropped (fail-closed) unless this node egresses directly.
The change is applied immediately: the new selector is re-resolved against the live peer set and the outbound route + inbound source filter are recomputed at once. A selector for a peer not yet in the netmap simply takes effect once that peer appears.
Only NEW flows use the changed exit; in-flight connections are not torn down and continue egressing via the previously-selected exit until they close.
Sourcepub fn exit_node(&self) -> Option<ExitNodeSelector>
pub fn exit_node(&self) -> Option<ExitNodeSelector>
The currently-selected exit node, or None if none is selected.
Sourcepub async fn rebind(&self) -> Result<(), Error>
pub async fn rebind(&self) -> Result<(), Error>
Re-bind the underlay UDP socket after a network/link change — Wi-Fi switch, sleep/wake,
or any event that invalidates the device’s local address/NAT mapping. This is the Rust
analog of Go magicsock’s Conn.Rebind().
The embedder owns deciding when to call this (it watches the OS for link changes — there is
no built-in network monitor); rebind is the engine half that does the socket work:
- Re-binds the underlay UDP socket, preferring the same local port (so the advertised endpoint stays stable) and falling back to an ephemeral port. The IPv4-only-by-default invariant is preserved.
- Invalidates the now-stale local mapping: learned reflexive (STUN) addresses and every peer’s confirmed direct path are cleared, while candidate endpoints are kept — so peers are re-probed over the new socket and relay over DERP (never a direct host dial) until a path re-confirms. Endpoint discovery re-runs on its normal cadence.
- Leaves peers, control, the netmap, disco keys, and DERP connections untouched; existing WireGuard sessions survive (they ride whatever underlay carries them).
A no-op if the underlay socket failed to bind at startup (the device is DERP-only). Existing connectivity is preserved on a re-bind error (the old socket is kept; the error is returned).
Sourcepub fn active_exit_node(&self) -> Option<StableNodeId>
pub fn active_exit_node(&self) -> Option<StableNodeId>
The stable id of the exit node traffic is currently egressing through, or None if none
is engaged (the equivalent of Go tsnet’s Status.ExitNodeStatus.ID).
This differs from exit_node, which returns the configured selector:
the active exit node is the route updater’s resolved, fail-closed answer. It is None when
no exit node is configured, the configured selector matches no current peer, or the matched
peer no longer advertises a default route (egress is then dropped, fail-closed). Match the id
against Status::peers (via status) for details.
Sourcepub async fn watch_netmap(&self) -> Result<Receiver<Vec<StatusNode>>, Error>
pub async fn watch_netmap(&self) -> Result<Receiver<Vec<StatusNode>>, Error>
Watch for netmap changes: the returned receiver’s value is the current set of peer
StatusNodes and updates on every netmap change (like subscribing to ipn notifications).
Sourcepub fn device_state(&self) -> DeviceState
pub fn device_state(&self) -> DeviceState
The current device connection-DeviceState (Connecting / Running / NeedsLogin /
Expired / Failed).
Sourcepub fn watch_state(&self) -> Receiver<DeviceState>
pub fn watch_state(&self) -> Receiver<DeviceState>
Watch the device connection-DeviceState, reacting push-style to control connection
transitions instead of polling status.
Returns a tokio::sync::watch::Receiver; await its
changed to be woken on each transition. The
initial value is the current state.
Sourcepub async fn wait_until_running(
&self,
timeout: Option<Duration>,
) -> Result<(), RegistrationError>
pub async fn wait_until_running( &self, timeout: Option<Duration>, ) -> Result<(), RegistrationError>
Wait until the device finishes registering, returning a typed outcome — the clean
replacement for polling ipv4_addr in a loop.
Resolves Ok(()) once the device is DeviceState::Running. On a non-running outcome it
returns a typed RegistrationError:
AuthRejected— bad/expired/unknown auth key; permanent (re-pair).NeedsLogin— interactive authorization required; not permanent (the runtime keeps retrying and reachesRunningonce the user authorizes). Auth-key callers treat this as failure; interactive callers should ignore it and drive the flow viawatch_state.NetworkUnreachable— transient (retry).Timeout— no settled state withintimeout(Nonewaits indefinitely).
KeyExpired is not produced here (a key expires only after
the node is up); observe it via watch_state. Use
RegistrationError::is_permanent to branch “re-pair” vs. “retry / drive login”.
Sourcepub async fn ping(
&self,
dst: IpAddr,
timeout: Duration,
) -> Result<Duration, PingError>
pub async fn ping( &self, dst: IpAddr, timeout: Duration, ) -> Result<Duration, PingError>
Ping a tailnet peer over the overlay with an ICMPv4 echo, returning the round-trip time
(like tailscale ping).
The echo is sent from this device’s own tailnet IPv4 over the overlay netstack — never a
host socket. IPv6 destinations return PingError::Ipv6Unsupported (this fork is
IPv4-only on the tailnet). A peer answers from its own OS stack; this netstack does not
auto-reply to echo requests.
In TUN transport mode there is no application netstack to ping from; this surfaces as
PingError::Timeout (the same error this method already uses for an unavailable source
address — PingError carries no dedicated “unsupported” variant).
Sourcepub async fn get_certificate(
&self,
name: &str,
) -> Result<CertifiedKey, CertError>
pub async fn get_certificate( &self, name: &str, ) -> Result<CertifiedKey, CertError>
See the no-acme variant for the contract; with acme this issues a real cert via the
runtime’s ACME engine (Device → Runtime → ControlRunner → issue_certificate_via_setdns).
Sourcepub async fn listen_tls(
&self,
cfg: &ServeConfig,
) -> Result<TlsAcceptor, CertError>
pub async fn listen_tls( &self, cfg: &ServeConfig, ) -> Result<TlsAcceptor, CertError>
Build a TlsAcceptor terminating TLS for cfg.name on the overlay (like tsnet’s
ListenTLS).
Obtains the certificate via Device::get_certificate — so with the acme feature this
issues a real Let’s Encrypt cert (when the control plane answers set-dns), and without it
(or when issuance is unavailable) it surfaces the same fail-closed
ts_control::CertError rather than ever serving a self-signed cert or downgrading to
plaintext. Terminate accepted overlay streams with ts_control::accept_tls.
Sourcepub fn get_serve_config(&self) -> ServeState
pub fn get_serve_config(&self) -> ServeState
The currently-stored Serve config (like tsnet’s GetServeConfig).
Returns the config last passed to Device::set_serve_config, or an empty
ts_control::ServeState (no ports) if none was ever set. Pure read — does not touch the
network.
Sourcepub async fn set_serve_config(
&self,
state: ServeState,
) -> Result<ServeAcceptedReceiver, Error>
pub async fn set_serve_config( &self, state: ServeState, ) -> Result<ServeAcceptedReceiver, Error>
Replace this node’s Serve config and (re)bind its tailnet ports (like tsnet’s
SetServeConfig, REPLACE semantics).
state becomes the whole config (full-replace reconcile: every previously-bound serve
port’s accept loop is torn down and the new config’s ports are bound from scratch). For each
configured port the manager binds an overlay listener on this node’s tailnet IPv4 and
dispatches per ts_control::ServeTarget:
Accept— the TLS-terminated stream is handed back over the returnedServeAcceptedReceiver(the in-process stand-in forListenTLS’snet.Listener).Proxy— reverse-proxy the decrypted stream to a local host backend.Text— write a fixed body and close.TcpForward— forward the raw (non-TLS) stream to a local host backend.
Fail-closed. state.validate() runs first. Every TLS-terminating port’s acceptor is
obtained up-front via Device::listen_tls (the ACME-aware cert path); if any cert cannot be
issued the whole call fails with that ts_control::CertError and nothing is bound — a
TLS port never downgrades to plaintext.
Anti-leak. Listeners bind the overlay netstack only (never a host socket). The
Proxy/TcpForward backend dial is a local host socket to the embedder’s own backend (like
Go’s reverse-proxy to 127.0.0.1), intentionally NOT routed through the exit-egress
forwarder. A backend dial failure drops that connection; it never falls back.
Returns an error in TUN transport mode (there is no application netstack to bind on). The
previous config’s accept loops (and any earlier ServeAcceptedReceiver) stop when this
returns; the new receiver delivers every Accept-port connection.
Sourcepub async fn listen_funnel(
&self,
cfg: &ServeConfig,
opts: FunnelOptions,
) -> Result<FunnelAcceptedReceiver, FunnelError>
pub async fn listen_funnel( &self, cfg: &ServeConfig, opts: FunnelOptions, ) -> Result<FunnelAcceptedReceiver, FunnelError>
Expose a tailnet TLS service to the public internet via Tailscale Funnel (like tsnet’s
ListenFunnel), returning a FunnelAcceptedReceiver
that delivers each TLS-terminated public connection.
Two fail-closed gates, then the live ingress listener. First the node-attribute gate is
fully enforced from this node’s own capability map (mirroring Go ipn.NodeCanFunnel +
ipn.CheckFunnelPort): the tailnet admin must have enabled HTTPS and granted the funnel
node attribute, and cfg.port must be in the set the funnel-ports capability allows —
otherwise this returns ts_control::FunnelError::NotAllowed /
ts_control::FunnelError::PortNotAllowed before touching any cert or network. Then the
node’s *.ts.net certificate is obtained via the ACME-aware Device::get_certificate (the
Funnel hostname is the node’s MagicDNS name, so its DNS-01 cert matches); fail-closed on
ts_control::FunnelError::Cert — no self-signed or plaintext fallback.
On success a FunnelManager is registered: its ingress
sink is installed into the runtime’s peerAPI /v0/ingress slot (making that route live without
restarting the peerAPI server), and the HostInfo.IngressEnabled map-request signal is set so
control routes Funnel traffic to this node. Public Funnel bytes arrive as a relay POST to
/v0/ingress, are membership-gated + 101-hijacked into a raw stream, TLS-terminated by the
manager, and delivered over the returned receiver.
Where the relay comes from. The public ingress relay + DNS mapping that feed
/v0/ingress are Tailscale infrastructure (ts_control::MISSING_FUNNEL_RELAY), provisioned
automatically against real Tailscale SaaS with a Funnel-enabled ACL; against a self-hosted
control plane no relay exists, so the listener is correct but never fed.
Anti-leak: Funnel TLS terminates only on the overlay netstack (the hijacked ingress stream
arrives on the overlay peerAPI listener), never a host socket; there is no self-signed or
plaintext fallback. A new listen_funnel replaces the previous manager (its pump + sink tear
down); dropping the Device tears it down too.
Sourcepub async fn listen_service(
&self,
name: &str,
mode: ServiceMode,
) -> Result<TcpListener, ServiceError>
pub async fn listen_service( &self, name: &str, mode: ServiceMode, ) -> Result<TcpListener, ServiceError>
Host a Tailscale VIP service (svc:<label>) by binding an overlay listener on the
service’s control-assigned virtual IP (like tsnet’s ListenService).
Fail-closed. Mirrors Go tsnet.Server.ListenService’s preconditions, enforced from this
node’s own netmap state (ts_control::resolve_service_listen): the name must be a valid
svc:<dns-label>, this node must be tagged (Go ErrUntaggedServiceHost), and control
must have assigned the service a VIP address on this node (delivered via the service-host
node-capability — see ts_control::Node::service_addresses). Any unmet precondition
returns a typed ts_control::ServiceError before binding anything.
When all hold, this binds a tcp_listen on the service VIP and the
configured mode port over the overlay netstack (never a host socket) and returns the
listener. The netstack already accepts packets for control-assigned VIPs (they are injected
alongside the node’s own tailnet address), so the listener is reachable by tailnet peers.
The Tun/L3 service mode is unsupported (a TODO in upstream tsnet); only TCP/HTTP modes
(which bind the same VIP:port at the listen layer) are offered. Returns an error in TUN
transport mode (there is no application netstack to bind on).
Sourcepub async fn shutdown(self, timeout: Option<Duration>) -> bool
pub async fn shutdown(self, timeout: Option<Duration>) -> bool
Attempt to gracefully shut down this device’s runtime.
Reports whether the device was fully shut down before the timeout. It is still shut down if it timed out, just more violently and with potential resource leaks.
If timeout is None, then shutdown will never time-out.
Auto Trait Implementations§
impl !Freeze for Device
impl !RefUnwindSafe for Device
impl !UnwindSafe for Device
impl Send for Device
impl Sync for Device
impl Unpin for Device
impl UnsafeUnpin for Device
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Source§impl<T> Downcast for Twhere
T: Any,
impl<T> Downcast for Twhere
T: Any,
Source§fn into_any(self: Box<T>) -> Box<dyn Any>
fn into_any(self: Box<T>) -> Box<dyn Any>
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>
fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>
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)
fn as_any(&self) -> &(dyn Any + 'static)
&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)
fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)
&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
impl<T> DowncastSend for T
Source§impl<T> DowncastSync for T
impl<T> DowncastSync for T
Source§impl<A, T> DynMessage<A> for T
impl<A, T> DynMessage<A> for T
Source§fn handle_dyn<'a>(
self: Box<T>,
state: &'a mut A,
actor_ref: ActorRef<A>,
tx: Option<Sender<Result<Box<dyn Any + Send>, SendError<Box<dyn Any + Send>, Box<dyn Any + Send>>>>>,
stop: &'a mut bool,
) -> Pin<Box<dyn Future<Output = Result<(), Box<dyn ReplyError>>> + Send + 'a>>
fn handle_dyn<'a>( self: Box<T>, state: &'a mut A, actor_ref: ActorRef<A>, tx: Option<Sender<Result<Box<dyn Any + Send>, SendError<Box<dyn Any + Send>, Box<dyn Any + Send>>>>>, stop: &'a mut bool, ) -> Pin<Box<dyn Future<Output = Result<(), Box<dyn ReplyError>>> + Send + 'a>>
impl<T> ErasedDestructor for Twhere
T: 'static,
impl<A, B, T> HttpServerConnExec<A, B> for Twhere
B: Body,
Source§impl<T> Instrument for T
impl<T> Instrument for T
Source§fn instrument(self, span: Span) -> Instrumented<Self>
fn instrument(self, span: Span) -> Instrumented<Self>
Source§fn in_current_span(self) -> Instrumented<Self>
fn in_current_span(self) -> Instrumented<Self>
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self>
fn into_either(self, into_left: bool) -> Either<Self, Self>
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 moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
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