pub struct Config {Show 26 fields
pub server_url: Url,
pub hostname: Option<String>,
pub client_name: Option<String>,
pub tags: Vec<String>,
pub ephemeral: bool,
pub accept_routes: bool,
pub accept_dns: bool,
pub exit_node: Option<ExitNodeSelector>,
pub advertise_routes: Vec<IpNet>,
pub advertise_exit_node: bool,
pub forward_tcp_ports: Vec<u16>,
pub forward_udp_ports: Vec<u16>,
pub forward_all_ports: bool,
pub forward_exit_egress: bool,
pub block_incoming: bool,
pub exit_proxy: Option<ExitProxyConfig>,
pub peerapi_port: Option<u16>,
pub taildrop_dir: Option<PathBuf>,
pub tcp_buffer_size: Option<usize>,
pub enable_ipv6: bool,
pub persistent_keepalive_interval: Option<Duration>,
pub transport_mode: TransportMode,
pub wire_ingress: bool,
pub ingress_active: Arc<AtomicBool>,
pub advertise_services: Vec<String>,
pub allow_http_key_fetch: bool,
}Expand description
Configuration for the control server.
Fields§
§server_url: UrlThe URL of the control server to connect to.
hostname: Option<String>The hostname of the current node.
client_name: Option<String>A name for this type of client.
This will be reported to the control server in the HostInfo.App field.
Tags to request from the control server (--advertise-tags / AdvertiseTags in the Go
client).
Sent as HostInfo.RequestTags on registration and on every map request, so a
tag-keyed control ACL (e.g. a self-hosted control plane’s route auto-approver) can match this node. Each
entry is a full tag string including the tag: prefix (e.g. tag:exit). Defaults to
empty (claim no tags); an empty set omits the wire field entirely.
ephemeral: boolWhether this node registers as ephemeral (--ephemeral / Ephemeral in the Go client).
An ephemeral node is garbage-collected by the control server shortly after it
disconnects. That is the right default for short-lived clients, but a persistent exit node
or subnet router must set this to false or it will be GC’d out of the tailnet while
briefly offline. Defaults to true to match the historical behavior of this client.
accept_routes: boolWhether to accept subnet routes advertised by peers (--accept-routes / RouteAll in the
Go client).
When false (the default, matching the Go client on Linux/server platforms and our
fail-closed posture), only each peer’s own tailnet addresses are routed; larger advertised
subnet routes are ignored. When true, traffic destined for an accepted subnet egresses
via the advertising peer.
This is a client-side preference and is not read inside ts_control: control always sends
the full set of advertised routes, and the runtime trims them. It is carried here only to
be threaded through to the runtime’s route filter.
accept_dns: boolWhether to accept the tailnet’s DNS configuration (MagicDNS + the pushed resolvers/search
domains) — --accept-dns / the CorpDNS pref in the Go client. Defaults to true, matching
Go’s NewPrefs() (CorpDNS: true).
When true, the MagicDNS responder serves the control-pushed DnsConfig
(overlay-name answers + split-DNS routes + recursive forwarding). When false, the node
ignores the pushed DNS config and the responder serves nothing (every query is REFUSED),
mirroring Go applying an essentially-empty dns.Config when CorpDNS is off — so a node can
join the tailnet for connectivity without taking over its DNS.
Like accept_routes, this is a client-side preference not read inside
ts_control (control always pushes the full DNSConfig; the runtime decides whether to honor
it); it is carried here only to be threaded through to the runtime’s MagicDNS responder, and is
runtime-settable via Device::set_accept_dns (the analog of tailscale set --accept-dns).
exit_node: Option<ExitNodeSelector>Which peer (if any) to use as an exit node (--exit-node / ExitNodeID in the Go client).
The selector may name the peer by stable id, tailnet IP, or MagicDNS name (see
ExitNodeSelector); it is resolved against the live peer set on
every route rebuild, so an IP/name selection follows the peer across netmap changes. When
set and resolvable, the selected peer’s advertised default route (0.0.0.0/0 / ::/0) is
installed so internet-bound traffic egresses through it. When None (the default) or
unresolvable, no peer receives a default route and internet-bound traffic is dropped
(fail-closed).
Like accept_routes, this is a client-side preference not read
inside ts_control; it is carried here only to be threaded through to the runtime’s route
filter.
Full-tunnel exit vs. just reaching a peer’s port — leave this None unless you mean
full-tunnel. Set exit_node only to route all internet-bound traffic through a peer
that advertises a default route (advertise_exit_node). To merely reach a specific peer’s
service over the tailnet — e.g. Device::tcp_connect to its 100.x.y.z:1080 — you do
not set exit_node at all; direct peer dials need no exit node. Setting exit_node to a
peer that is only a selective CONNECT proxy (advertises no 0.0.0.0/0) leaves egress
fail-closed and logs a warning that internet-bound traffic is dropped — which looks like a
failure but is just “that peer isn’t a full-tunnel exit.” If you saw that warning while only
trying to dial a peer’s port, the fix is to unset exit_node.
advertise_routes: Vec<IpNet>Subnet routes to advertise to the control server (--advertise-routes / RoutableIPs in
the Go client).
Unlike accept_routes/exit_node, this field
is read inside ts_control: it populates HostInfo.RoutableIPs on every map request so
the control server can grant this node as a subnet router. Defaults to empty (advertise
nothing — fail-closed). Only IPv4 prefixes are advertised; IPv6 prefixes are dropped to
uphold the IPv6-off posture (advertising a route we won’t forward would be a black hole).
advertise_exit_node: boolWhether to advertise this node as an exit node (--advertise-exit-node in the Go client).
When true, the default route 0.0.0.0/0 is added to the advertised
routable_ips so the control server can grant this node as an
exit node, after which other peers may egress internet-bound traffic through our real IP.
Defaults to false (fail-closed): being an exit node means other peers’ traffic leaves
via our real origin IP, so it must be explicit opt-in. IPv6 (::/0) is never advertised,
per the IPv6-off posture.
forward_tcp_ports: Vec<u16>TCP ports the inbound forwarder accepts and splices to real OS sockets for every advertised
route (advertise_routes / advertise_exit_node).
smoltcp has no all-port accept mode (see the ts_forwarder crate docs), so the forwarder
forwards a configured set of ports rather than the full 1–65535 range. Defaults to empty: a
node that advertises routes but configures no forward ports accepts inbound flows into its
dedicated forwarder netstack but forwards none of them (fail-closed — nothing is dialed).
forward_udp_ports: Vec<u16>UDP ports the inbound forwarder accepts and splices to real OS sockets for every advertised
route. See forward_tcp_ports; defaults to empty.
forward_all_ports: boolForward all TCP/UDP ports (1–65535) on every advertised route, like a Go subnet router
(tailscale up --advertise-routes forwards all ports), instead of the explicit
forward_tcp_ports /
forward_udp_ports sets.
smoltcp cannot wildcard-port-accept, so all-port mode is implemented with an on-demand
per-port listener manager driven by a raw-socket port observer on the dedicated forwarder
netstack (see the ts_forwarder crate docs). When true, the explicit port sets are
ignored. Anti-leak is unchanged: every flow still routes through the same
RouteTable→dialer chokepoint, so forward_exit_egress still
governs exit-node egress. Defaults to false.
forward_exit_egress: boolWhether exit-node (0.0.0.0/0) inbound flows are actually egressed via this host’s real
origin IP.
This is the anti-leak opt-in, kept separate from
advertise_exit_node: advertising the default route only
makes control offer this node as an exit; it does not by itself egress a peer’s traffic.
When false (the default, fail-closed), the forwarder uses a dialer that structurally
refuses exit-node egress — a 0.0.0.0/0 flow is dropped at dial time, never leaked out our
real IP. Set to true only on a node whose real IP is the intended egress (e.g. a
residential exit), never on a node whose host IP must stay hidden (e.g. a cloud VPS). Subnet
routes are dialed identically regardless of this flag.
block_incoming: boolShields-up (Go ipn prefs ShieldsUp): when true, refuse all inbound connections from
peers that terminate on this node — the packet filter drops inbound packets aimed at this
node’s own addresses. Replies to connections this node itself initiated, and forwarded
subnet/exit transit, are unaffected (the deny is scoped to self-destined packets; see
ts_packetfilter::ShieldsUpFilter). Transport-only client preference — ts_control never
reads it; the runtime’s packet-filter updater consumes it. Defaults to false.
exit_proxy: Option<ExitProxyConfig>Optional upstream proxy that exit-node egress is routed through, so the node egresses via the proxy’s IP rather than its own origin IP.
Only consulted when forward_exit_egress is true. When
set, the runtime wires the forwarder with a proxy dialer (SOCKS5 / HTTP CONNECT) that
fails closed — any proxy connect or handshake failure drops the flow rather than falling
back to a direct host-IP dial, so the real origin IP never leaks. When None (the default)
and exit egress is enabled, egress uses this host’s real IP (HostExitDialer).
Like the other dataplane fields, this is a client-side preference not read inside
ts_control; it is carried here only to be threaded through to the runtime’s dialer
selection. This is a product capability (residential-proxy egress) beyond strict tsnet
parity — see the repo’s AGENTS.md/CLAUDE.md.
peerapi_port: Option<u16>The IPv4 peerAPI port this node binds to serve exit-node DoH (DNS-over-HTTPS) proxying for
peers that select it as their exit node (peerapi4 + peerapi-dns-proxy services).
When Some(port), the runtime binds a peerAPI DoH server on this host’s overlay IPv4
address at port, and registration / map requests advertise both the peerapi4 service
(at port) and the peerapi-dns-proxy service (Go quirk: its advertised port is always
1) so peers know they can delegate DNS to us. When None (the default, fail-closed), no
peerAPI is run and no services are advertised — this node never offers DNS proxying.
The DoH server always answers authoritative/overlay records (MagicDNS peer names,
ExtraRecords, PTR); recursive resolution to real upstream resolvers is gated separately
behind forward_exit_egress, so a cloud exit node can serve
overlay DNS without ever exposing its real origin IP via a recursive lookup.
taildrop_dir: Option<PathBuf>Filesystem directory that received Taildrop files land in, or None to disable Taildrop
(the default, fail-closed).
When Some(dir) and peerapi_port is also set, the runtime
serves the Taildrop peerAPI route PUT /v0/put/<name> on the shared peerAPI listener, and
incoming files are written under dir (created if absent). When None, no Taildrop server
is run — a peer’s PUT is refused. This is a pure on-disk destination: like the other
dataplane fields it is not read inside ts_control; it is carried here only to be threaded
into the runtime, which constructs the file store from it.
Independently of the network server, the embedder consumes received files via the
Device::taildrop_* methods (Go exposes these over LocalAPI; this fork exposes them on the
device). With no peerapi_port, the store still exists for those read APIs but no peer can
deliver to it.
tcp_buffer_size: Option<usize>Per-direction TCP send/receive buffer size (bytes) for the userspace netstack, or None to
use the netstack default (256 KiB per direction, ~512 KiB per socket).
smoltcp has no window auto-tuning, so this is the hard cap on a single flow’s
bandwidth-delay product; raising it helps large model-API responses on high-RTT links, at
the cost of more memory per concurrent socket (each socket allocates this size for both rx
and tx). Like the other dataplane fields, this is a client-side preference not read inside
ts_control; it is carried here only to be threaded into the runtime’s netstack
configuration.
enable_ipv6: boolWhether IPv6 is enabled on the tailnet overlay. Defaults to false (IPv4-only).
Like the other dataplane fields, this is a client-side preference not read inside
ts_control; it is carried here only to be threaded into the runtime’s underlay socket,
disco candidate filter, netstack address assignment, and MagicDNS AAAA handling. It governs
only the overlay and never the exit-node / forwarder egress path, which stays IPv4-only
regardless to uphold the real-origin-IP isolation invariant.
persistent_keepalive_interval: Option<Duration>WireGuard persistent-keepalive interval applied to every peer, or None to disable persistent
keepalives (PersistentKeepalive; Tailscale uses 25s).
When Some(interval), each peer emits an empty authenticated keepalive every interval of
outbound silence, holding the (typically DERP-relayed) path/NAT mapping warm so an idle
session doesn’t age past expiry and wedge the next dial — the failure this fork’s primary
userspace-netstack deployment hits, where the relay is the only path to a peer. Unlike the
reactive WireGuard §6.5 keepalive (armed only by inbound traffic), this re-arms unconditionally
and fires on a fully idle tunnel; the empty packet does not advance the session’s
rotation/expiry timers, so a genuinely dead peer is still detected. Defaults to Some(25s)
(DEFAULT_PERSISTENT_KEEPALIVE). Like the other dataplane fields it is not read inside
ts_control; it is carried here only to be threaded into the runtime’s dataplane actor.
transport_mode: TransportModeHow the application overlay data path is realized: userspace netstack (default) or a real
kernel TUN interface. See TransportMode.
Like the other dataplane fields, this is a client-side preference not read inside
ts_control; it is carried here only to be threaded into the runtime, which builds either a
netstack actor or a TUN transport from it. ts_control must not depend on ts_transport_tun.
wire_ingress: boolWhether to ask control to wire this node up server-side for Tailscale Funnel
(HostInfo.WireIngress, the capver-113 client→control Funnel signal), even when no Funnel
endpoint is currently active.
Unlike the dataplane fields above, this one is read inside ts_control: it sets
HostInfo.WireIngress on registration and the streaming map request, asking control to
provision the DNS / ingress records a Funnel node needs so a later serve/funnel session
works immediately. It mirrors Go tsnet’s “would like to be wired up for Funnel” signal.
This fork cannot yet terminate public Funnel ingress — crate::listen_funnel is
fail-closed (no client-side ACME engine, and a self-hosted control plane provides no public
ingress relay). So HostInfo.IngressEnabled (Funnel endpoints actually live) is never set;
only WireIngress is, and only when this flag is true. Defaults to false (fail-closed):
a node requests Funnel wiring only when explicitly opted in.
ingress_active: Arc<AtomicBool>Live signal that this node currently has an active Funnel ingress listener
(Device::listen_funnel was called and its listener is up), driving HostInfo.IngressEnabled
on the streaming map request.
Unlike wire_ingress (a static “please provision Funnel records” hint),
this is a dynamic flag: the runtime flips it true when a funnel listener starts serving and
back to false when it stops, so the next map request advertises IngressEnabled accordingly
(Go sets HostInfo.IngressEnabled only while Funnel endpoints are actually live, and
IngressEnabled implies WireIngress). Shared (Arc) with the runtime so the device can flip
it without rebuilding the config. Defaults to a fresh false (fail-closed: no live endpoint).
Not serialized — it is process-local runtime state, not persisted configuration.
advertise_services: Vec<String>VIP services this node advertises that it hosts (svc:<dns-label> names), the
advertise side of Tailscale VIP services (Go tsnet’s Hostinfo.ServicesHash +
c2n GET /vip-services).
Each entry is a full svc:-prefixed service name. This field is read inside ts_control:
the valid names (validate_service_name is applied
fail-closed; malformed names are dropped and logged) are hashed into HostInfo.ServicesHash
on every map request, and answered when control fetches the list via the c2n
/vip-services endpoint. Defaults to empty: with no entries the hash is "" and behavior is
byte-for-byte the historical non-advertising path. Hosting a service additionally requires
control to assign it a VIP and the node to be tagged (the consume side, unchanged here).
allow_http_key_fetch: boolAllow fetching the control server’s machine public key (GET /key) over plain http when
the server_url is itself http://.
By default (false) the /key fetch is always upgraded to https, even when the control
URL is http:// — matching Tailscale’s posture that the unauthenticated key bootstrap must
be TLS-protected. That upgrade makes registration fail against a control plane that only
serves plain http (e.g. a self-hosted Headscale exposed over a http://host:port LAN
endpoint / NodePort with no TLS), even though the rest of the control connection already
honors the http scheme. Set this to true for such a deployment to fetch /key over the
same http scheme as the control URL.
Security: only enable this when you control both ends and the control plane is reachable
over a trusted network path — an on-path attacker could otherwise substitute the control
key. It has no effect when server_url is https:// (the fetch stays https regardless).
Fail-closed default is false.
Implementations§
Source§impl Config
impl Config
Sourcepub fn format_client_name(&self) -> String
pub fn format_client_name(&self) -> String
Get the full client name as a string.
This takes the form tailscale-rs ({client_name}), where the parenthetical is only
provided if self.client_name is set.
Sourcepub fn advertised_routes(&self) -> Vec<IpNet>
pub fn advertised_routes(&self) -> Vec<IpNet>
Compute the set of IP prefixes to advertise in HostInfo.RoutableIPs, combining
advertise_routes with the exit-node default route when
advertise_exit_node is set.
IPv6 prefixes are filtered out (IPv6-off posture): we never forward IPv6, so advertising an
IPv6 route would create a black hole. The exit-node default route is therefore 0.0.0.0/0
only, never ::/0. The result is deduplicated and order-preserving; an empty result means
“advertise nothing”, and callers omit the wire field entirely.
Sourcepub fn advertised_services(&self) -> Vec<Service<'static>>
pub fn advertised_services(&self) -> Vec<Service<'static>>
The services to advertise in HostInfo.Services, derived from
peerapi_port.
When a peerAPI port is configured, we advertise the peerapi4 service at that port plus the
peerapi-dns-proxy service (whose advertised port is always 1, matching the Go client’s
quirk) so peers learn they can delegate exit-node DNS to us. When None, the result is empty
and callers omit the HostInfo.Services wire field entirely (advertise no services). IPv6
peerAPI (peerapi6) is never advertised, per the IPv6-off posture.
Sourcepub fn advertised_vip_services(&self) -> Vec<VipServiceOwned>
pub fn advertised_vip_services(&self) -> Vec<VipServiceOwned>
The validated set of VIP services this node advertises that it hosts, derived from
advertise_services.
Each configured name is validated with
validate_service_name (fail-closed: a name that is not a
well-formed svc:<dns-label> is dropped with a warning, never advertised). Each surviving
service is advertised on all ports (a single 0/0..=65535
ProtoPortRange, matching
Go’s default ServicePortRange() when no explicit ports are configured) and marked active.
The result is the canonical input to both services_hash and the c2n /vip-services
response. An empty config yields an empty Vec (advertise nothing — the hash is "").