pub struct Server {Show 14 fields
pub hostname: Option<String>,
pub auth_key: Option<String>,
pub control_url: Option<String>,
pub ephemeral: bool,
pub advertise_tags: Vec<String>,
pub port: Option<u16>,
pub run_web_client: bool,
pub client_id: Option<String>,
pub client_secret: Option<String>,
pub id_token: Option<String>,
pub audience: Option<String>,
pub dir: Option<PathBuf>,
pub store: Option<Arc<dyn StateStore>>,
pub tun: Option<TunSpec>,
/* private fields */
}Expand description
An embedded Tailscale node, shaped like Go’s tsnet.Server.
Set the public fields (they map onto Config; see the table in docs/TSNET_FACADE_DESIGN.md),
then call any method — the wrapped Device is constructed lazily on the first method call
(Go: “fields may be changed until the first method call”). In Rust this ordering is enforced by
the borrow checker: methods borrow &self, so you cannot mutate a field after the first call.
For fork capabilities beyond Go tsnet parity (accept-routes, exit nodes, residential-proxy exit
egress, …), reach the underlying Config with Server::configure, or drop to the full
engine surface with Server::device.
Fields§
§hostname: Option<String>Hostname to present to control (Go Hostname) → Config::requested_hostname. Empty ⇒
engine default.
auth_key: Option<String>Auth key to register with (Go AuthKey) → Config::auth_key and the auth_key arg of
Device::new. Falls back to TS_AUTH_KEY.
control_url: Option<String>Coordination server URL (Go ControlURL) → Config::control_server_url. None ⇒ the
fork’s default control server.
ephemeral: boolRegister as an ephemeral node (Go Ephemeral) → Config::ephemeral. Defaults to
false to match Go (note: bare Config::default defaults this to true).
ACL tags to advertise (Go AdvertiseTags) → Config::requested_tags.
port: Option<u16>WireGuard/peer-to-peer UDP port pin (Go Port) → Config::wireguard_listen_port.
run_web_client: boolRun the node web client (Go RunWebClient) → Config::run_web_client. Partial: the pref
is carried but no embedded web client runs (see docs/TSNET_PARITY.md).
client_id: Option<String>OAuth/WIF client id (Go ClientID) → Config::client_id.
client_secret: Option<String>OAuth client secret (Go ClientSecret) → Config::client_secret.
id_token: Option<String>IdP-issued OIDC token (Go IDToken) → Config::id_token.
audience: Option<String>Audience for a requested ID token (Go Audience) → Config::audience.
dir: Option<PathBuf>State directory (Go Dir). Persists node identity keys to dir/STATE_FILE via the
engine’s key-file format. Absent store, this is the standard on-disk identity.
store: Option<Arc<dyn StateStore>>A pluggable state store (Go Store). Takes precedence over Server::dir. None + no
dir ⇒ an ephemeral in-memory identity (fresh every run).
tun: Option<TunSpec>Run over a real kernel TUN interface instead of the userspace netstack (Go Tun) →
Config::use_tun.
Implementations§
Source§impl Server
impl Server
Sourcepub fn new() -> Self
pub fn new() -> Self
A new server with Go-default fields (not ephemeral, default control server, no persistence).
Sourcepub fn configure<F>(&mut self, f: F) -> &mut Self
pub fn configure<F>(&mut self, f: F) -> &mut Self
Register a hook to customize the derived Config just before the device is built — the
escape hatch to fork capabilities that have no Go tsnet field. Ignored if the server has
already started.
Sourcepub async fn start(&self) -> Result<(), Error>
pub async fn start(&self) -> Result<(), Error>
Connect to the tailnet (Go Start). Idempotent — subsequent calls are no-ops.
Sourcepub async fn up(&self, timeout: Option<Duration>) -> Result<Status, Error>
pub async fn up(&self, timeout: Option<Duration>) -> Result<Status, Error>
Connect and wait until the node is Running, returning its status (Go Up). timeout
None waits forever.
Sourcepub async fn device(&self) -> Result<&Device, Error>
pub async fn device(&self) -> Result<&Device, Error>
The wrapped Device (Go LocalClient-and-more), starting it if needed. The escape hatch to
the full engine surface (whois, ping, set_* prefs, taildrop, TKA, …).
Sourcepub async fn dial(&self, network: &str, addr: &str) -> Result<DialConn, Error>
pub async fn dial(&self, network: &str, addr: &str) -> Result<DialConn, Error>
Dial a tailnet address over TCP or UDP (Go Dial(ctx, network, address)), returning the
tsnet-shaped DialConn whose arm matches the transport.
network is one of "tcp", "tcp4", "tcp6", "udp", "udp4", "udp6"; addr is a
host:port string — a MagicDNS name or an IP literal (bracketed for IPv6,
[2001:db8::1]:443). The network string is parsed at the facade boundary, so an unsupported
network is a typed Error::UnsupportedNetwork reported before the device is started
(fail-fast, no network I/O). For the common case, Server::dial_tcp / Server::dial_udp
hand back the transport’s stream / socket directly.
Routing: the unsuffixed "tcp"/"udp" forward to the transport-specific typed accessors
Device::dial_tcp / Device::dial_udp
(for Family::Any these are Device::dial’s arms); the family-pinned
…4/…6 forms forward to Device::dial, which enforces the v4/v6 constraint that the
family-agnostic sub-calls do not.
Sourcepub async fn dial_tcp(&self, addr: &str) -> Result<TcpStream, Error>
pub async fn dial_tcp(&self, addr: &str) -> Result<TcpStream, Error>
Dial a tailnet TCP address, yielding the overlay stream directly — the common case of
Server::dial for "tcp". This is the building block for HTTP-over-tailnet: a hyper/
reqwest connector dials with dial_tcp(&format!("{host}:{port}")), mirroring Go
tsnet.Server.HTTPClient.
Sourcepub async fn dial_udp(&self, addr: &str) -> Result<ConnectedUdpSocket, Error>
pub async fn dial_udp(&self, addr: &str) -> Result<ConnectedUdpSocket, Error>
Dial a tailnet UDP address, yielding the connected overlay socket directly — the "udp"
sibling of Server::dial_tcp and the common case of Server::dial for "udp".
Returns a ConnectedUdpSocket (send/recv against a fixed
peer) — the connected-net.Conn shape Go’s Dial("udp", …) returns, as opposed to
Server::listen_packet’s unconnected packet socket.
Sourcepub async fn listen(
&self,
network: &str,
addr: &str,
) -> Result<TcpListener, Error>
pub async fn listen( &self, network: &str, addr: &str, ) -> Result<TcpListener, Error>
Listen for inbound TCP on the tailnet (Go Listen).
network is a stream network — "tcp", "tcp4", or "tcp6"; a "udp*" or unknown
value is Error::InvalidNetwork (Go’s net.Listen likewise rejects a packet network).
addr may be a bare ":80" — the wildcard host, 0.0.0.0 for tcp/tcp4 and [::] for
tcp6 — or a full "100.x.y.z:80". Returns the std::net-style overlay
netstack::TcpListener (backed by ts_netstack_smoltcp): .accept() it for inbound
streams, exactly as Go .Accept()s the returned net.Listener.
Sourcepub async fn listen_packet(
&self,
network: &str,
addr: &str,
) -> Result<UdpSocket, Error>
pub async fn listen_packet( &self, network: &str, addr: &str, ) -> Result<UdpSocket, Error>
Listen for inbound UDP on the tailnet (Go ListenPacket).
network is a packet network — "udp", "udp4", or "udp6"; a "tcp*" or unknown
value is Error::InvalidNetwork. addr is a host:port IP literal (a bare ":0" is
filled with the family’s wildcard host); like Go’s ListenPacket, a MagicDNS name is not
accepted — and neither does Server::listen accept one (both bind by IP literal; only
Server::dial resolves MagicDNS names). An unspecified host binds this node’s tailnet
address. Returns the std::net-style overlay netstack::UdpSocket (backed by
ts_netstack_smoltcp), a net.PacketConn analog (recv_from/send_to).
Sourcepub async fn listen_funnel(
&self,
cfg: &ServeConfig,
opts: FunnelOptions,
) -> Result<FunnelAcceptedReceiver, ListenFunnelError>
pub async fn listen_funnel( &self, cfg: &ServeConfig, opts: FunnelOptions, ) -> Result<FunnelAcceptedReceiver, ListenFunnelError>
Expose a tailnet TLS service to the public internet via Tailscale Funnel (Go ListenFunnel).
A thin wrapper over Device::listen_funnel: it lazily starts
the node, converts FunnelOptions into the engine’s ts_control::FunnelOptions serve
config, and hands cfg (the MagicDNS name + tailnet port) straight through. On success it
yields the engine’s FunnelAcceptedReceiver.
Errors are a ListenFunnelError, which keeps a lifecycle/start failure
(Start) distinct from the engine’s typed Funnel error
(Funnel): a node that never registered surfaces as a startup
failure carrying its real cause, never misdiagnosed as a Funnel access denial.
Sourcepub async fn listen_service(
&self,
name: &str,
mode: ServiceMode,
) -> Result<ServiceListener, ListenServiceError>
pub async fn listen_service( &self, name: &str, mode: ServiceMode, ) -> Result<ServiceListener, ListenServiceError>
Host a Tailscale VIP service (Go ListenService), returning a Go-shaped ServiceListener
(overlay listener + resolved FQDN).
A thin wrapper over Device::listen_service: it lazily
starts the node, passes name + the ServiceMode serve config straight through, and pairs
the returned overlay listener with the node’s resolved service FQDN.
Errors are a ListenServiceError, which keeps a lifecycle/start failure
(Start) distinct from the engine’s typed ServiceError
(Service, whose UntaggedHost == Go
ErrUntaggedServiceHost): a node that never registered surfaces as a startup failure
carrying its real cause, never misdiagnosed as a listener bind failure.
Sourcepub async fn loopback(&self) -> Result<Loopback, Error>
pub async fn loopback(&self) -> Result<Loopback, Error>
Start (once) the loopback surface and return its addresses + both credentials (Go
Loopback() (addr, proxyCred, localAPICred, err)).
Brings up two things, living for the Server’s lifetime (torn down by Server::close):
- a SOCKS5 proxy onto the tailnet (the engine’s
Device::loopback), authenticated withLoopback::proxy_cred; and - an in-process LocalAPI HTTP server authenticated with the separate
Loopback::local_api_cred(HTTP Basic-auth password), servingGET /localapi/v0/status.
Idempotent: repeated calls return the same addresses and credentials. See Loopback for
the one honest delta from Go (two 127.0.0.1 listeners rather than one muxed listener).
Sourcepub async fn local_client(&self) -> Result<LocalClient, Error>
pub async fn local_client(&self) -> Result<LocalClient, Error>
A LocalClient for this node’s in-process LocalAPI HTTP server (Go
tsnet.Server.LocalClient()), starting the loopback surface if needed.
The returned client authenticates with the loopback’s local_api_cred and speaks plain HTTP
to 127.0.0.1 (no hyper dependency). For typed status prefer the in-process Server::status;
the LocalClient is the Go-shaped path that actually round-trips through the LocalAPI server.
Sourcepub async fn http_client<B>(&self) -> Result<Client<TailnetConnector, B>, Error>
pub async fn http_client<B>(&self) -> Result<Client<TailnetConnector, B>, Error>
A hyper HTTP client whose every request egresses over the tailnet (Go
tsnet.Server.HTTPClient()), built over Device::http_connector.
The exact analog of Go’s &http.Client{Transport: &http.Transport{DialContext: s.Dial}}: a
pooled hyper_util client wired to the tailnet connector, with TLS/redirects/pooling left
to the client (the connector is plaintext — see TailnetConnector
for wrapping it in TLS). B is your request body type (e.g. String, or
http_body_util::Full<Bytes>).
Available only with the hyper crate feature (as in the engine).
Sourcepub async fn tailscale_ips(&self) -> Result<(Ipv4Addr, Option<Ipv6Addr>), Error>
pub async fn tailscale_ips(&self) -> Result<(Ipv4Addr, Option<Ipv6Addr>), Error>
This node’s tailnet addresses (Go TailscaleIPs). The tuple shape mirrors
Device::tailscale_ips verbatim (v6 is None on an
IPv4-only tailnet).
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 that terminates TLS for cfg.name on the tailnet
overlay using this node’s own certificate (Go tsnet.Server.ListenTLS’s cert path).
A thin delegation to Device::listen_tls. The serve config is
validated at the facade boundary first — a non-tailnet cfg.name or a zero cfg.port is a
typed ts_control::CertError returned before the lazy device start, so a misconfiguration
never touches the network (fail-fast, exactly as Server::dial/Server::listen reject a
bad network/address up front). The certificate is then acquired through
ts_control::tls via the node’s ACME-aware cert path.
acme feature. Issuance is fail-closed: with the acme feature this issues a real
Let’s Encrypt certificate (DNS-01, published via the node’s set-dns RPC — SaaS-only); without
it (the default) it surfaces ts_control::CertError::Unimplemented rather than ever serving a
self-signed cert or downgrading to plaintext. Either way the acceptor is ring-only
(ts_control::tls_acceptor pins the ring provider — no aws-lc/openssl).
This keeps the fork’s typed ts_control::CertError (matching Device::listen_tls), not the
unified lifecycle Error — the cert path is a specialized, typed surface (design doc §7).
Like Go’s ListenTLS, terminate accepted overlay streams (from a Server::listen listener)
with ts_control::accept_tls, reusing the one acceptor across connections.
Sourcepub async fn cert_pair(
&self,
name: &str,
min_validity: Option<Duration>,
) -> Result<(String, String), CertError>
pub async fn cert_pair( &self, name: &str, min_validity: Option<Duration>, ) -> Result<(String, String), CertError>
Issue a real Let’s Encrypt certificate for this node’s MagicDNS name and return the PEM
pair (cert_chain_pem, key_pem) — the analog of Go LocalClient().CertPair /
CertPairWithValidity, for writing an on-disk .crt + .key. A thin delegation to
Device::cert_pair; acme feature only (the wrapped engine
method is itself acme-gated).
The name is checked at the facade boundary first — a non-tailnet (*.ts.net) name is
ts_control::CertError::NotTailnetName before any device start (anti-leak: this fork never
mints certs for off-tailnet names). min_validity is accepted for Go signature parity but does
not change behavior: this fork keeps no cert cache and always issues fresh, so a freshly issued
(full-lifetime) cert satisfies any min_validity (see Device::cert_pair). The second tuple
element is secret key material — persist it to a 0600 file and never log it. Fail-closed
and ring-only, like Server::listen_tls.
Sourcepub async fn cert_domains(&self) -> Result<Vec<String>, Error>
pub async fn cert_domains(&self) -> Result<Vec<String>, Error>
The DNS names this node may obtain TLS certificates for (Go tsnet.Server.CertDomains).
A thin delegation to Device::cert_domains: the CertDomains
control pushed in the netmap DNS config — the names to request a cert for via
Server::listen_tls (or, with acme, cert_pair). Empty before the first netmap, or when
control granted none (Go returns a clone of nm.DNS.CertDomains). Unlike the cert-issuance
calls this only reads netmap state, so it returns the unified lifecycle Error.
Sourcepub async fn status(&self) -> Result<Status, Error>
pub async fn status(&self) -> Result<Status, Error>
Node + peer status (Go LocalClient().Status).
Sourcepub async fn close(self, timeout: Option<Duration>) -> bool
pub async fn close(self, timeout: Option<Duration>) -> bool
Stop the server (Go Close). Consumes self; returns whether shutdown completed within
timeout (None = wait forever). A never-started server closes cleanly.
Tears down the loopback surface first (aborting the SOCKS5 and LocalAPI accept loops), then
gracefully shuts the wrapped device down. The LocalAPI server holds only a Weak to the
device, so this reclaims the sole strong reference for the graceful shutdown.
Trait Implementations§
Auto Trait Implementations§
impl !Freeze for Server
impl !RefUnwindSafe for Server
impl !UnwindSafe for Server
impl Send for Server
impl Sync for Server
impl Unpin for Server
impl UnsafeUnpin for Server
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
impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
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