Skip to content

wowjeeez/rsnet

Repository files navigation

rsnet

rsnet

Rust bindings for Tailscale's libtailscale C library. Embed a Tailscale node directly into your Rust process — get an IP on your tailnet entirely from userspace, no system daemon required.

Fully async (tokio). Streams implement AsyncRead + AsyncWrite + Unpin.

Prerequisites

  • Go (to compile libtailscale from the git submodule)
  • Rust stable (edition 2021+)
  • A Tailscale auth key
git clone --recurse-submodules https://github.com/wowjeeez/rsnet.git

Quick start

let mut server = RawTsTcpServer::new("my-node")?;
server.set_auth_key("tskey-auth-...")?;
server.set_dir("/var/lib/my-node")?;
server.up()?;

let listener = server.listen("tcp", ":80")?;
loop {
    let stream = listener.accept().await?;
    println!("peer: {:?}, port: {:?}", stream.peer_addr(), stream.local_port());
    tokio::spawn(handle_connection(stream));
}

TLS

Go handles TLS + ACME certs natively — no rustls needed:

let listener = server.listen_native_tls("tcp", ":443")?;
let stream = listener.accept().await?; // already decrypted

Services

Multi-port Tailscale Services with a builder pattern:

let mut svc = server.service("svc:my-api")
    .https(443)
    .http(80)
    .tcp(9000)
    .bind()?;

println!("fqdn: {}", svc.fqdn);

loop {
    let (port, stream) = svc.accept().await?;
    tokio::spawn(async move {
        match port {
            443 => handle_https(stream).await,
            80 => handle_http(stream).await,
            9000 => handle_tcp(stream).await,
            _ => {}
        }
    });
}

Note: services require a tagged auth key (create in admin console with e.g. tag:service).

LocalAPI

Typed access to Tailscale's node-local HTTP API:

let client = server.local_client()?;

let me = client.whoami().await?;
let domain = client.fqdn().await?;
let status = client.status().await?;
let who = client.whois("100.x.y.z:443").await?;
let prefs = client.prefs().await?;

let (cert, key) = client.cert_pair("my-node.tailnet.ts.net").await?;

client.advertise_exit_node().await?;
client.advertise_routes(&["10.0.0.0/8"]).await?;
client.set_tags(&["tag:server"]).await?;

let (code, body) = client.get("/localapi/v0/status").await?;

Stream API

TailscaleStream mirrors tokio::TcpStream:

let stream = listener.accept().await?;

stream.peer_addr()              // Option<&str>
stream.local_port()             // Option<u16>
stream.readable().await?;       // wait for readable
stream.writable().await?;       // wait for writable
stream.try_read(&mut buf)?;     // non-blocking
stream.try_write(b"hello")?;    // non-blocking

// async read/write via AsyncReadExt/AsyncWriteExt
stream.read_exact(&mut buf).await?;
stream.write_all(b"hello").await?;

// split for bidirectional
let (reader, writer) = tokio::io::split(stream);

Examples

# plain HTTP on port 80
cargo run --example hello -- <auth-key> <hostname>

# HTTPS with native TLS on port 443
cargo run --example hello_tls -- <auth-key> <hostname>

# multi-port service (requires tagged auth key)
cargo run --example service -- <auth-key> <hostname> svc:my-api

Logging

Go-side logs are piped through tracing at debug level with target libtailscale:

RUST_LOG=libtailscale=debug cargo run --example hello -- ...

State persistence

Set a state directory to avoid re-authentication on every restart:

server.set_dir("/var/lib/my-node")?;

Without this, libtailscale uses a default path keyed by binary name. Combined with set_ephemeral(true), the node is deleted from your tailnet when the process exits and needs re-auth next run.

About

Async wrapper for the Tailscale Local & libtailscaled C API (and go later).

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors