├── .github └── workflows │ └── CI.yml ├── .gitignore ├── .gitmodules ├── Cargo.toml ├── LICENSE ├── README.md ├── examples ├── echo_client.rs ├── echo_server.rs └── http_echo_server.rs ├── libtailscale-sys ├── .gitignore ├── Cargo.toml ├── build.rs └── src │ └── lib.rs └── src └── lib.rs /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | workflow_dispatch: 9 | 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.event.pull_request.number || github.sha }} 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | test: 16 | name: Test Suite 17 | runs-on: ${{ matrix.os }} 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | os: [ubuntu-latest, macos-latest] 22 | steps: 23 | - uses: actions/checkout@v4 24 | with: 25 | submodules: 'recursive' 26 | - uses: dtolnay/rust-toolchain@stable 27 | - uses: actions/setup-go@v3 28 | with: 29 | go-version: '>=1.20.0' 30 | - run: cargo test --all 31 | 32 | fmt: 33 | name: Rustfmt 34 | runs-on: ubuntu-latest 35 | steps: 36 | - uses: actions/checkout@v4 37 | - uses: dtolnay/rust-toolchain@stable 38 | with: 39 | components: rustfmt 40 | - run: cargo fmt --all --check 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "libtailscale-sys/libtailscale"] 2 | path = libtailscale-sys/libtailscale 3 | url = https://github.com/tailscale/libtailscale.git 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "libtailscale" 3 | version.workspace = true 4 | edition = "2021" 5 | description = "Rust binding to libtailscale" 6 | keywords = ["tailscale"] 7 | license = "MIT" 8 | readme = "README.md" 9 | repository = "https://github.com/messense/libtailscale-rs" 10 | 11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 12 | 13 | [workspace] 14 | members = [".", "libtailscale-sys"] 15 | 16 | [workspace.package] 17 | version = "0.2.0" 18 | 19 | [dependencies] 20 | libc = "0.2.140" 21 | libtailscale-sys = { path = "libtailscale-sys", version = "0.2.0" } 22 | 23 | [dev-dependencies] 24 | bytes = "1.4.0" 25 | http-body-util = "0.1.0" 26 | hyper = { version = "1.0.0", features = ["http1", "server"] } 27 | hyper-util = { version = "0.1.11", features = ["tokio"] } 28 | tokio = { version = "1.26.0", features = ["full"] } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023-present messense 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # libtailscale-rs 2 | 3 | [![CI](https://github.com/messense/libtailscale-rs/actions/workflows/CI.yml/badge.svg)](https://github.com/messense/libtailscale-rs/actions/workflows/CI.yml) 4 | [![Crates.io](https://img.shields.io/crates/v/libtailscale.svg)](https://crates.io/crates/libtailscale) 5 | [![docs.rs](https://docs.rs/libtailscale/badge.svg)](https://docs.rs/libtailscale/) 6 | 7 | Rust binding to [libtailscale](https://github.com/tailscale/libtailscale). 8 | 9 | ## Installation 10 | 11 | Add it to your ``Cargo.toml``: 12 | 13 | ```bash 14 | cargo add libtailscale 15 | ``` 16 | 17 | ### Build Requirements 18 | 19 | * Rust 20 | * Go 1.20+ 21 | 22 | ## Examples 23 | 24 | * [echo server](./examples/echo_server.rs) 25 | * [echo client](./examples/echo_client.rs) 26 | * [http echo server](./examples/http_echo_server.rs) 27 | 28 | ## License 29 | 30 | This work is released under the MIT license. A copy of the license is provided in the [LICENSE](./LICENSE) file. 31 | 32 | -------------------------------------------------------------------------------- /examples/echo_client.rs: -------------------------------------------------------------------------------- 1 | //! To build and run it: 2 | //! 3 | //! ```bash 4 | //! TS_AUTHKEY= cargo run --example echo_client 5 | //! ``` 6 | use std::env; 7 | use std::io::{Read, Write}; 8 | 9 | use libtailscale::Tailscale; 10 | 11 | fn main() { 12 | let mut args = env::args().skip(1); 13 | let Some(addr) = args.next() else { 14 | eprintln!("USAGE: echo_client "); 15 | std::process::exit(1); 16 | }; 17 | 18 | let mut ts = Tailscale::new(); 19 | ts.set_ephemeral(true).unwrap(); 20 | ts.set_logfd(-1).unwrap(); 21 | ts.up().unwrap(); 22 | let mut stream = ts.dial("tcp", &addr).unwrap(); 23 | 24 | println!("Connected to {}", addr); 25 | 26 | loop { 27 | let mut buf = String::new(); 28 | let size = std::io::stdin().read_line(&mut buf).unwrap(); 29 | stream.write_all(&buf.as_bytes()[..size]).unwrap(); 30 | 31 | let mut data = vec![0; buf.len()]; 32 | match stream.read_exact(&mut data) { 33 | Ok(_) => { 34 | print!("{}", String::from_utf8(data).unwrap()); 35 | } 36 | Err(e) => { 37 | println!("Failed to receive data: {}", e); 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /examples/echo_server.rs: -------------------------------------------------------------------------------- 1 | //! To build and run it: 2 | //! 3 | //! ```bash 4 | //! TS_AUTHKEY= cargo run --example echo_server 5 | //! ``` 6 | use std::io::{Read, Write}; 7 | use std::net::TcpStream; 8 | use std::thread; 9 | 10 | use libtailscale::Tailscale; 11 | 12 | fn handle_client(mut stream: TcpStream) { 13 | let mut buf = [0; 2048]; 14 | while match stream.read(&mut buf) { 15 | Ok(0) => false, // connection closed 16 | Ok(size) => { 17 | stream.write_all(&buf[..size]).unwrap(); 18 | true 19 | } 20 | Err(_) => { 21 | let _ret = stream.shutdown(std::net::Shutdown::Both); 22 | false 23 | } 24 | } {} 25 | } 26 | 27 | fn main() { 28 | let mut ts = Tailscale::new(); 29 | ts.set_ephemeral(true).unwrap(); 30 | ts.up().unwrap(); 31 | 32 | let listener = ts.listen("tcp", ":1999").unwrap(); 33 | for stream in listener.incoming() { 34 | match stream { 35 | Ok(stream) => { 36 | thread::spawn(move || { 37 | handle_client(stream); 38 | }); 39 | } 40 | Err(e) => { 41 | println!("Error: {}", e); 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /examples/http_echo_server.rs: -------------------------------------------------------------------------------- 1 | //! To build and run it: 2 | //! 3 | //! ```bash 4 | //! TS_AUTHKEY= cargo run --example http_echo_server 5 | //! ``` 6 | use bytes::Bytes; 7 | use http_body_util::{combinators::BoxBody, BodyExt, Empty, Full}; 8 | use hyper::server::conn::http1; 9 | use hyper::service::service_fn; 10 | use hyper::{Method, Request, Response, StatusCode}; 11 | use hyper_util::rt::TokioIo; 12 | use libtailscale::Tailscale; 13 | 14 | /// This is our service handler. It receives a Request, routes on its 15 | /// path, and returns a Future of a Response. 16 | async fn echo( 17 | req: Request, 18 | ) -> Result>, hyper::Error> { 19 | match (req.method(), req.uri().path()) { 20 | // Serve some instructions at / 21 | (&Method::GET, "/") => Ok(Response::new(full( 22 | "Try POSTing data to /echo such as: `curl echo-http-server:3000/echo -XPOST -d 'hello world'`", 23 | ))), 24 | 25 | // Simply echo the body back to the client. 26 | (&Method::POST, "/echo") => Ok(Response::new(req.into_body().boxed())), 27 | 28 | // Return the 404 Not Found for other routes. 29 | _ => { 30 | let mut not_found = Response::new(empty()); 31 | *not_found.status_mut() = StatusCode::NOT_FOUND; 32 | Ok(not_found) 33 | } 34 | } 35 | } 36 | 37 | fn empty() -> BoxBody { 38 | Empty::::new() 39 | .map_err(|never| match never {}) 40 | .boxed() 41 | } 42 | 43 | fn full>(chunk: T) -> BoxBody { 44 | Full::new(chunk.into()) 45 | .map_err(|never| match never {}) 46 | .boxed() 47 | } 48 | 49 | #[tokio::main] 50 | async fn main() -> Result<(), Box> { 51 | let mut ts = Tailscale::new(); 52 | ts.set_ephemeral(true).unwrap(); 53 | ts.set_logfd(-1).unwrap(); 54 | ts.up().unwrap(); 55 | 56 | let loopback = ts.loopback().unwrap(); 57 | println!( 58 | "Loopback API: {}, credential: {}", 59 | loopback.address, loopback.credential 60 | ); 61 | println!( 62 | "Proxy username: {}, credential: {}", 63 | loopback.proxy_username, loopback.proxy_credential 64 | ); 65 | println!(); 66 | 67 | let listener = ts.listen("tcp", ":3000").unwrap(); 68 | println!("Listening on http://echo-http-server:3000"); 69 | for stream in listener.incoming() { 70 | match stream { 71 | Ok(stream) => { 72 | stream.set_nonblocking(true)?; 73 | let stream = tokio::net::TcpStream::from_std(stream)?; 74 | tokio::task::spawn(async move { 75 | if let Err(err) = http1::Builder::new() 76 | .serve_connection(TokioIo::new(stream), service_fn(echo)) 77 | .await 78 | { 79 | println!("Error serving connection: {:?}", err); 80 | } 81 | }); 82 | } 83 | Err(e) => { 84 | println!("Error: {}", e); 85 | } 86 | } 87 | } 88 | Ok(()) 89 | } 90 | -------------------------------------------------------------------------------- /libtailscale-sys/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | -------------------------------------------------------------------------------- /libtailscale-sys/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "libtailscale-sys" 3 | version.workspace = true 4 | edition = "2021" 5 | description = "Rust FFI to libtailscale C API" 6 | keywords = ["tailscale"] 7 | license = "MIT" 8 | links = "tailscale" 9 | repository = "https://github.com/messense/libtailscale-rs" 10 | 11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 12 | 13 | [dependencies] 14 | 15 | [build-dependencies] 16 | cc = "1.0.79" 17 | -------------------------------------------------------------------------------- /libtailscale-sys/build.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::ffi::OsString; 3 | use std::path::PathBuf; 4 | use std::process::Command; 5 | 6 | fn format_compiler_command(tool: Command) -> OsString { 7 | let mut cmd = tool.get_program().to_os_string(); 8 | for arg in tool.get_args() { 9 | cmd.push(" "); 10 | cmd.push(arg); 11 | } 12 | cmd 13 | } 14 | 15 | fn main() { 16 | println!("cargo:rerun-if-changed=libtailscale/tailscale.h"); 17 | println!("cargo:rerun-if-changed=libtailscale/tailscale.c"); 18 | println!("cargo:rerun-if-changed=libtailscale/tailscale.go"); 19 | println!("cargo:rerun-if-changed=libtailscale/go.mod"); 20 | println!("cargo:rerun-if-changed=libtailscale/go.sum"); 21 | 22 | let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); 23 | let host = env::var("HOST").unwrap(); 24 | let target = env::var("TARGET").unwrap(); 25 | 26 | let mut cmd = Command::new("go"); 27 | 28 | if std::env::var("DOCS_RS").is_ok() { 29 | // Avoid permission deinied error on docs.rs 30 | cmd.env("GOMODCACHE", out_dir.join("gomodcache")); 31 | } 32 | 33 | if host != target { 34 | let os = if target.contains("android") { 35 | "android" 36 | } else if target.contains("darwin") { 37 | "darwin" 38 | } else if target.contains("dragonfly") { 39 | "dragonfly" 40 | } else if target.contains("freebsd") { 41 | "freebsd" 42 | } else if target.contains("dragonfly") { 43 | "dragonfly" 44 | } else if target.contains("illumos") { 45 | "illumos" 46 | } else if target.contains("ios") { 47 | "ios" 48 | } else if target.contains("linux") { 49 | "linux" 50 | } else if target.contains("netbsd") { 51 | "netbsd" 52 | } else if target.contains("openbsd") { 53 | "openbsd" 54 | } else if target.contains("solaris") { 55 | "solaris" 56 | } else if target.contains("windows") { 57 | "windows" 58 | } else { 59 | panic!("unsupported operating system") 60 | }; 61 | let arch = if target.contains("i386") || target.contains("i585") || target.contains("i686") 62 | { 63 | "386" 64 | } else if target.contains("x86_64") { 65 | "amd64" 66 | } else if target.contains("aarch64") { 67 | "arm64" 68 | } else if target.contains("armeb") { 69 | "armbe" 70 | } else if target.contains("arm") { 71 | "arm" 72 | } else if target.contains("mips64el-") { 73 | "mips64le" 74 | } else if target.contains("mips64-") { 75 | "mips64" 76 | } else if target.contains("mipsel") { 77 | "mipsle" 78 | } else if target.contains("mips-") { 79 | "mips" 80 | } else if target.contains("powerpc64le-") { 81 | "ppc64le" 82 | } else if target.contains("powerpc64-") { 83 | "ppc64" 84 | } else if target.contains("powerpc-") { 85 | "ppc" 86 | } else if target.contains("riscv64") { 87 | "riscv64" 88 | } else if target.contains("riscv32") { 89 | "riscv" 90 | } else if target.contains("s390x") { 91 | "s390x" 92 | } else if target.contains("sparc64") { 93 | "sparc64" 94 | } else if target.contains("sparc-") { 95 | "sparc" 96 | } else if target.contains("wasm") { 97 | "wasm" 98 | } else { 99 | panic!("unsupported cpu architecture") 100 | }; 101 | println!("GOOS={os} GOARCH={arch}"); 102 | cmd.env("CGO_ENABLED", "1") 103 | .env("GOOS", os) 104 | .env("GOARCH", arch); 105 | 106 | if target.contains("armv7") { 107 | cmd.env("GOARM", "7"); 108 | } else if target.contains("armv5") { 109 | cmd.env("GOARM", "5"); 110 | } else if target.contains("arm-") { 111 | cmd.env("GOARM", "6"); 112 | } else if target.contains("i386") || target.contains("i586") { 113 | cmd.env("GO386", "softfloat"); 114 | } 115 | 116 | let mut build = cc::Build::new(); 117 | build 118 | // Suppress cargo metadata for example env vars printing 119 | .cargo_metadata(false) 120 | // opt_level, host and target are required 121 | .opt_level(0) 122 | .host(&host) 123 | .target(&target); 124 | let c_compiler = build.get_compiler(); 125 | cmd.env("CC", format_compiler_command(c_compiler.to_command())); 126 | 127 | build.cpp(true); 128 | let cxx_compiler = build.get_compiler(); 129 | cmd.env("CXX", format_compiler_command(cxx_compiler.to_command())); 130 | } 131 | 132 | let status = cmd 133 | .args(["build", "-x", "-buildmode=c-archive", "-o"]) 134 | .arg(out_dir.join("libtailscale.a")) 135 | .current_dir("libtailscale") 136 | .status() 137 | .expect("go build command failed to start"); 138 | assert!(status.success()); 139 | 140 | println!("cargo:rustc-link-search=native={}", out_dir.display()); 141 | println!("cargo:rustc-link-lib=static=tailscale"); 142 | 143 | if target.contains("apple-") { 144 | println!("cargo:rustc-link-lib=framework=CoreFoundation"); 145 | println!("cargo:rustc-link-lib=framework=Security"); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /libtailscale-sys/src/lib.rs: -------------------------------------------------------------------------------- 1 | /* automatically generated by rust-bindgen 0.64.0 */ 2 | #![allow(non_camel_case_types)] 3 | 4 | pub type tailscale = ::std::os::raw::c_int; 5 | extern "C" { 6 | pub fn tailscale_new() -> tailscale; 7 | } 8 | extern "C" { 9 | pub fn tailscale_start(sd: tailscale) -> ::std::os::raw::c_int; 10 | } 11 | extern "C" { 12 | pub fn tailscale_up(sd: tailscale) -> ::std::os::raw::c_int; 13 | } 14 | extern "C" { 15 | pub fn tailscale_close(sd: tailscale) -> ::std::os::raw::c_int; 16 | } 17 | extern "C" { 18 | pub fn tailscale_set_dir( 19 | sd: tailscale, 20 | dir: *const ::std::os::raw::c_char, 21 | ) -> ::std::os::raw::c_int; 22 | } 23 | extern "C" { 24 | pub fn tailscale_set_hostname( 25 | sd: tailscale, 26 | hostname: *const ::std::os::raw::c_char, 27 | ) -> ::std::os::raw::c_int; 28 | } 29 | extern "C" { 30 | pub fn tailscale_set_authkey( 31 | sd: tailscale, 32 | authkey: *const ::std::os::raw::c_char, 33 | ) -> ::std::os::raw::c_int; 34 | } 35 | extern "C" { 36 | pub fn tailscale_set_control_url( 37 | sd: tailscale, 38 | control_url: *const ::std::os::raw::c_char, 39 | ) -> ::std::os::raw::c_int; 40 | } 41 | extern "C" { 42 | pub fn tailscale_set_ephemeral( 43 | sd: tailscale, 44 | ephemeral: ::std::os::raw::c_int, 45 | ) -> ::std::os::raw::c_int; 46 | } 47 | extern "C" { 48 | pub fn tailscale_set_logfd(sd: tailscale, fd: ::std::os::raw::c_int) -> ::std::os::raw::c_int; 49 | } 50 | pub type tailscale_conn = ::std::os::raw::c_int; 51 | extern "C" { 52 | pub fn tailscale_dial( 53 | sd: tailscale, 54 | network: *const ::std::os::raw::c_char, 55 | addr: *const ::std::os::raw::c_char, 56 | conn_out: *mut tailscale_conn, 57 | ) -> ::std::os::raw::c_int; 58 | } 59 | pub type tailscale_listener = ::std::os::raw::c_int; 60 | extern "C" { 61 | pub fn tailscale_listen( 62 | sd: tailscale, 63 | network: *const ::std::os::raw::c_char, 64 | addr: *const ::std::os::raw::c_char, 65 | listener_out: *mut tailscale_listener, 66 | ) -> ::std::os::raw::c_int; 67 | } 68 | extern "C" { 69 | pub fn tailscale_accept( 70 | listener: tailscale_listener, 71 | conn_out: *mut tailscale_conn, 72 | ) -> ::std::os::raw::c_int; 73 | } 74 | extern "C" { 75 | pub fn tailscale_loopback( 76 | sd: tailscale, 77 | addr_out: *mut ::std::os::raw::c_char, 78 | addrlen: usize, 79 | proxy_cred_out: *mut ::std::os::raw::c_char, 80 | local_api_cred_out: *mut ::std::os::raw::c_char, 81 | ) -> ::std::os::raw::c_int; 82 | } 83 | extern "C" { 84 | pub fn tailscale_enable_funnel_to_localhost_plaintext_http1( 85 | sd: tailscale, 86 | localhost_port: ::std::os::raw::c_int, 87 | ) -> ::std::os::raw::c_int; 88 | } 89 | extern "C" { 90 | pub fn tailscale_errmsg( 91 | sd: tailscale, 92 | buf: *mut ::std::os::raw::c_char, 93 | buflen: usize, 94 | ) -> ::std::os::raw::c_int; 95 | } 96 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::{CStr, CString}; 2 | use std::iter::FusedIterator; 3 | use std::net::TcpStream; 4 | use std::os::fd::FromRawFd; 5 | use std::os::raw::c_int; 6 | 7 | use libtailscale_sys::*; 8 | 9 | /// A handle onto a Tailscale server 10 | #[derive(Debug)] 11 | pub struct Tailscale { 12 | inner: tailscale, 13 | } 14 | 15 | /// A socket on the tailnet listening for connections. 16 | #[derive(Debug)] 17 | pub struct Listener<'a> { 18 | tailscale: &'a Tailscale, 19 | listener: tailscale_listener, 20 | } 21 | 22 | impl Tailscale { 23 | /// Create a tailscale server object 24 | /// 25 | /// No network connection is initialized until [`Tailscale::start`] is called. 26 | pub fn new() -> Self { 27 | let inner = unsafe { tailscale_new() }; 28 | Self { inner } 29 | } 30 | 31 | /// Connect the server to the tailnet 32 | /// 33 | /// Calling this function is optional as it will be called by the first use 34 | /// of [`Tailscale::listen`] or [`Tailscale::dial`] on a server 35 | pub fn start(&mut self) -> Result<(), String> { 36 | let ret = unsafe { tailscale_start(self.inner) }; 37 | if ret != 0 { 38 | Err(self.last_error()) 39 | } else { 40 | Ok(()) 41 | } 42 | } 43 | 44 | /// Connect the server to the tailnet and waits for it to be usable 45 | /// 46 | /// To cancel an in-progress call, use [`Tailscale::close`] 47 | pub fn up(&mut self) -> Result<(), String> { 48 | let ret = unsafe { tailscale_up(self.inner) }; 49 | if ret != 0 { 50 | Err(self.last_error()) 51 | } else { 52 | Ok(()) 53 | } 54 | } 55 | 56 | /// Shut down the server 57 | fn close(&mut self) -> Result<(), ()> { 58 | let ret = unsafe { tailscale_close(self.inner) }; 59 | if ret != 0 { 60 | Err(()) 61 | } else { 62 | Ok(()) 63 | } 64 | } 65 | 66 | /// Set the name of the directory to use for state. 67 | pub fn set_dir(&mut self, dir: &str) -> Result<(), String> { 68 | let dir = CString::new(dir).unwrap(); 69 | let ret = unsafe { tailscale_set_dir(self.inner, dir.as_ptr()) }; 70 | if ret != 0 { 71 | Err(self.last_error()) 72 | } else { 73 | Ok(()) 74 | } 75 | } 76 | 77 | /// Set the hostname to present to the control server 78 | pub fn set_hostname(&mut self, hostname: &str) -> Result<(), String> { 79 | let hostname = CString::new(hostname).unwrap(); 80 | let ret = unsafe { tailscale_set_hostname(self.inner, hostname.as_ptr()) }; 81 | if ret != 0 { 82 | Err(self.last_error()) 83 | } else { 84 | Ok(()) 85 | } 86 | } 87 | 88 | /// Set the auth key to create the node and will be preferred over the 89 | /// `TS_AUTHKEY` environment variable. 90 | pub fn set_authkey(&mut self, authkey: &str) -> Result<(), String> { 91 | let authkey = CString::new(authkey).unwrap(); 92 | let ret = unsafe { tailscale_set_authkey(self.inner, authkey.as_ptr()) }; 93 | if ret != 0 { 94 | Err(self.last_error()) 95 | } else { 96 | Ok(()) 97 | } 98 | } 99 | 100 | /// Set the URL of the coordination server to use. 101 | /// 102 | /// If empty or unset, the Tailscale default is used. 103 | pub fn set_control_url(&mut self, control_url: &str) -> Result<(), String> { 104 | let control_url = CString::new(control_url).unwrap(); 105 | let ret = unsafe { tailscale_set_control_url(self.inner, control_url.as_ptr()) }; 106 | if ret != 0 { 107 | Err(self.last_error()) 108 | } else { 109 | Ok(()) 110 | } 111 | } 112 | 113 | /// Specifies whether the node should be ephemeral. 114 | pub fn set_ephemeral(&mut self, ephemeral: bool) -> Result<(), String> { 115 | let ret = unsafe { tailscale_set_ephemeral(self.inner, ephemeral as _) }; 116 | if ret != 0 { 117 | Err(self.last_error()) 118 | } else { 119 | Ok(()) 120 | } 121 | } 122 | 123 | /// Instruct the tailscale instance to write logs to `logfd` 124 | /// 125 | /// An `logfd` value of `-1` means discard all logging. 126 | pub fn set_logfd(&mut self, logfd: c_int) -> Result<(), String> { 127 | let ret = unsafe { tailscale_set_logfd(self.inner, logfd) }; 128 | if ret != 0 { 129 | Err(self.last_error()) 130 | } else { 131 | Ok(()) 132 | } 133 | } 134 | 135 | /// Connect to the address on the tailnet. 136 | /// 137 | /// * `network` is a string of the form "tcp", "udp", etc. 138 | /// * `address` is a string of an IP address or domain name. 139 | /// 140 | /// It will start the server if it has not been started yet. 141 | pub fn dial(&self, network: &str, address: &str) -> Result { 142 | let c_network = CString::new(network).unwrap(); 143 | let c_address = CString::new(address).unwrap(); 144 | let mut conn = 0; 145 | let ret = unsafe { 146 | tailscale_dial( 147 | self.inner, 148 | c_network.as_ptr(), 149 | c_address.as_ptr(), 150 | &mut conn, 151 | ) 152 | }; 153 | if ret != 0 { 154 | Err(self.last_error()) 155 | } else { 156 | Ok(unsafe { TcpStream::from_raw_fd(conn) }) 157 | } 158 | } 159 | 160 | /// Listen for a connection on the tailnet. 161 | /// 162 | /// * `network` is a string of the form "tcp", "udp", etc. 163 | /// * `address` is a string of an IP address or domain name. 164 | /// 165 | /// It will start the server if it has not been started yet. 166 | pub fn listen(&self, network: &str, address: &str) -> Result { 167 | let c_network = CString::new(network).unwrap(); 168 | let c_address = CString::new(address).unwrap(); 169 | let mut listener = 0; 170 | let ret = unsafe { 171 | tailscale_listen( 172 | self.inner, 173 | c_network.as_ptr(), 174 | c_address.as_ptr(), 175 | &mut listener, 176 | ) 177 | }; 178 | if ret != 0 { 179 | Err(self.last_error()) 180 | } else { 181 | Ok(Listener { 182 | tailscale: self, 183 | listener, 184 | }) 185 | } 186 | } 187 | 188 | /// Start a LocalAPI listener on a loopback address, and returns the address 189 | /// and credentials for using it as LocalAPI or a proxy. 190 | pub fn loopback(&mut self) -> Result { 191 | let mut addr = [0; 1024]; 192 | let mut cred = [0; 33]; 193 | let mut proxy_cred = [0; 33]; 194 | let ret = unsafe { 195 | tailscale_loopback( 196 | self.inner, 197 | addr.as_mut_ptr(), 198 | addr.len(), 199 | proxy_cred.as_mut_ptr(), 200 | cred.as_mut_ptr(), 201 | ) 202 | }; 203 | if ret != 0 { 204 | Err(self.last_error()) 205 | } else { 206 | let addr = unsafe { CStr::from_ptr(addr.as_ptr()) }; 207 | let cred = unsafe { CStr::from_ptr(cred.as_ptr()) }; 208 | let proxy_cred = unsafe { CStr::from_ptr(proxy_cred.as_ptr()) }; 209 | Ok(Loopback { 210 | address: addr.to_str().unwrap().to_owned(), 211 | credential: cred.to_str().unwrap().to_owned(), 212 | proxy_username: "tsnet", 213 | proxy_credential: proxy_cred.to_str().unwrap().to_owned(), 214 | }) 215 | } 216 | } 217 | 218 | /// Configures it to have Tailscale Funnel enabled, routing requests from the public web 219 | /// (without any authentication) down to this Tailscale node, requesting new 220 | /// LetsEncrypt TLS certs as needed, terminating TLS, and proxying all incoming 221 | /// HTTPS requests to `http://127.0.0.1:localhost_port` without TLS. 222 | /// 223 | /// There should be a plaintext HTTP/1 server listening on `127.0.0.1:localhost_port` 224 | /// or tsnet will serve HTTP 502 errors. 225 | /// 226 | /// Expect junk traffic from the internet from bots watching the public CT logs. 227 | pub fn enable_funnel_to_localhost_plaintext_http1( 228 | &self, 229 | localhost_port: u16, 230 | ) -> Result<(), String> { 231 | let ret = unsafe { 232 | tailscale_enable_funnel_to_localhost_plaintext_http1(self.inner, localhost_port as _) 233 | }; 234 | if ret != 0 { 235 | Err(self.last_error()) 236 | } else { 237 | Ok(()) 238 | } 239 | } 240 | 241 | fn last_error(&self) -> String { 242 | let mut buffer = [0; 256]; 243 | let ret = unsafe { tailscale_errmsg(self.inner, buffer.as_mut_ptr(), buffer.len() as _) }; 244 | if ret != 0 { 245 | return "tailscale internal error: failed to get error message".to_string(); 246 | } 247 | let cstr = unsafe { CStr::from_ptr(buffer.as_ptr()) }; 248 | cstr.to_string_lossy().into_owned() 249 | } 250 | } 251 | 252 | impl Drop for Tailscale { 253 | fn drop(&mut self) { 254 | let _ret = self.close(); 255 | } 256 | } 257 | 258 | impl Default for Tailscale { 259 | fn default() -> Self { 260 | Self::new() 261 | } 262 | } 263 | 264 | /// Tailscale loopback API information. 265 | #[derive(Debug, Clone)] 266 | pub struct Loopback { 267 | /// `ip:port` address of the LocalAPI 268 | pub address: String, 269 | /// Basic auth password 270 | pub credential: String, 271 | /// Proxy username, it's always `tsnet` 272 | pub proxy_username: &'static str, 273 | /// Proxy auth password 274 | pub proxy_credential: String, 275 | } 276 | 277 | impl<'a> Listener<'a> { 278 | /// Accept a connection on a Tailscale [`Listener`]. 279 | pub fn accept(&self) -> Result { 280 | let mut conn = 0; 281 | let ret = unsafe { tailscale_accept(self.listener, &mut conn) }; 282 | if ret != 0 { 283 | Err(self.tailscale.last_error()) 284 | } else { 285 | Ok(unsafe { TcpStream::from_raw_fd(conn) }) 286 | } 287 | } 288 | 289 | /// Returns an iterator over the connections being received on this 290 | /// listener. 291 | /// 292 | /// The returned iterator will never return [`None`]. Iterating over it is equivalent to 293 | /// calling [`Listener::accept`] in a loop. 294 | pub fn incoming(&self) -> Incoming<'_> { 295 | Incoming { listener: self } 296 | } 297 | 298 | /// Close the listener. 299 | fn close(&mut self) -> Result<(), String> { 300 | let ret = unsafe { libc::close(self.listener) }; 301 | if ret != 0 { 302 | Err(self.tailscale.last_error()) 303 | } else { 304 | Ok(()) 305 | } 306 | } 307 | } 308 | 309 | impl<'a> Drop for Listener<'a> { 310 | fn drop(&mut self) { 311 | let _ret = self.close(); 312 | } 313 | } 314 | 315 | /// An iterator that infinitely accepts connections on a [`Listener`]. 316 | #[must_use = "iterators are lazy and do nothing unless consumed"] 317 | #[derive(Debug)] 318 | pub struct Incoming<'a> { 319 | listener: &'a Listener<'a>, 320 | } 321 | 322 | impl<'a> Iterator for Incoming<'a> { 323 | type Item = Result; 324 | fn next(&mut self) -> Option> { 325 | Some(self.listener.accept()) 326 | } 327 | } 328 | 329 | impl FusedIterator for Incoming<'_> {} 330 | --------------------------------------------------------------------------------