Skip to main content

Config

Struct Config 

Source
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: Url

The 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: Vec<String>

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: bool

Whether 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: bool

Whether 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: bool

Whether 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: bool

Whether 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: bool

Forward 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: bool

Whether 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: bool

Shields-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: bool

Whether 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: TransportMode

How 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: bool

Whether 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: bool

Allow 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

Source

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.

Source

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.

Source

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.

Source

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 "").

Trait Implementations§

Source§

impl Clone for Config

Source§

fn clone(&self) -> Config

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for Config

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Default for Config

Source§

fn default() -> Self

Returns the “default value” for a type. Read more
Source§

impl<'de> Deserialize<'de> for Config

Source§

fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>
where __D: Deserializer<'de>,

Deserialize this value from the given Serde deserializer. Read more
Source§

impl Serialize for Config

Source§

fn serialize<__S>(&self, __serializer: __S) -> Result<__S::Ok, __S::Error>
where __S: Serializer,

Serialize this value into the given Serde serializer. Read more

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<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> DeserializeOwned for T
where T: for<'de> Deserialize<'de>,

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> 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> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

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

Source§

type Error = Infallible

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

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

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<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