Skip to content

Getting started

The following instructions are for Rust! For other languages, see the language-specific README:

Add the crate with cargo add geiserx_tailscale --rename tailscale, which writes the current version into your Cargo.toml. The line it adds looks like this:

[dependencies]
# Published as `geiserx_tailscale`; imported as `tailscale`.
tailscale = { package = "geiserx_tailscale", version = "0.57.4" }

Or depend on the latest from git:

[dependencies]
tailscale = { package = "geiserx_tailscale", git = "https://github.com/GeiserX/tailscale-rs" }

Either way, you import it as tailscale (e.g. use tailscale::Device;) — the crate name on crates.io is geiserx_tailscale, but the library name is tailscale.

Examples of using the tailscale crate can be found in examples/.

For instructions on how to run tests, lints, etc., see CONTRIBUTING.md. For the high-level architecture and repository layout, see ARCHITECTURE.md.

Code sample

A simple UDP client that periodically sends messages to a tailnet peer at 100.64.0.1:5678:

use std::{
    time::Duration,
    net::Ipv4Addr,
    error::Error,
};
use tailscale::{Config, Device};

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
    // Open a new connection to tailscale
    let dev = Device::new(
        &Config::default_with_key_file("tsrs_keys.json").await?,
        Some("YOUR_AUTH_KEY_HERE".to_owned()),
    ).await?;

    // Bind a UDP socket on this node's tailnet IP, port 1234
    let sock = dev.udp_bind((dev.ipv4().await?, 1234).into()).await?;

    // Send a packet containing "ping" to 100.64.0.1:5678 once per second
    loop {
        sock.send_to((Ipv4Addr::new(100, 64, 0, 1), 5678).into(), b"ping").await?;
        tokio::time::sleep(Duration::from_secs(1)).await;
    }
}

tsnet facade (Go-idiomatic Server)

If you're coming from Go's tsnet, the optional tsnet feature adds a tsnet::Server facade with the shape you already know — settable fields, a lazy Start on the first method call, and Up/Listen/Dial/Loopback/ListenFunnel/ListenService/ Close. It's a thin ergonomics layer over the same Device/Config engine — no new crate, same typed returns — so you get Go's lifecycle shape without giving up Rust's typed values.

[dependencies]
tailscale = { package = "geiserx_tailscale", version = "0.57.4", features = ["tsnet"] }
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
use tailscale::tsnet::Server;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Set fields where Go writes struct fields...
    let mut srv = Server::new();
    srv.hostname = Some("web".into());
    srv.auth_key = Some("YOUR_AUTH_KEY_HERE".into());
    srv.dir = Some("web_state".into());

    // ...then call a method: the wrapped Device is built and started lazily here.
    let listener = srv.listen("tcp", ":80").await?;
    loop {
        let conn = listener.accept().await?;
        // ...serve conn... (see the tsnet_echo example for a full echo server)
        drop(conn);
    }
}

The equivalent Go:

srv := &tsnet.Server{Hostname: "web", AuthKey: key, Dir: "web_state"}
defer srv.Close()
ln, _ := srv.Listen("tcp", ":80")

See the runnable tsnet_echo example — the tcp_echo server rewritten against this facade — and the crate's tsnet module docs.

Go tsnet.Server → tsnet::Server mapping

Go tsnet.Server tsnet::Server Notes
struct fields (Hostname, AuthKey, ControlURL, Ephemeral, AdvertiseTags, Port, Dir, Store, Tun, …) public fields on Server Set before the first method call.
Start() / Up(ctx) start() / up(timeout) Lazy start; up waits until Running.
Dial(ctx, net, addr) dial(net, addr) Returns the typed DialConn (dial_tcp/dial_udp for the stream/socket directly).
Listen(net, addr) listen(net, addr) Returns the overlay netstack::TcpListener.
ListenPacket(net, addr) listen_packet(net, addr) Returns the overlay netstack::UdpSocket.
ListenFunnel(...) / ListenService(name, mode) listen_funnel(...) / listen_service(name, mode) Keep the fork-typed FunnelError / ServiceError.
Loopback() / LocalClient() loopback() / local_client() SOCKS5 proxy + in-process LocalAPI HTTP server.
HTTPClient() http_client() Requires the hyper feature.
TailscaleIPs() / CertDomains() tailscale_ips() / cert_domains()
Close() close(timeout) Consumes the server; reports whether it shut down cleanly in time.

For fork capabilities beyond Go tsnet parity (accept-routes, exit nodes, residential-proxy exit egress, …) reach the full Config via Server::configure, or drop to the whole engine surface with Server::device. The complete field/method mapping (with parity verdicts) and the design rationale live in docs/TSNET_FACADE_DESIGN.md.