From b834ef1c434d9d3b85e79996a6728e0d0de150b6 Mon Sep 17 00:00:00 2001 From: Jan-Erik Rediger Date: Sun, 12 Mar 2023 14:59:30 +0100 Subject: [PATCH] Build it out --- .gitignore | 2 +- Cargo.lock | 65 ++++++++++ Cargo.toml | 2 + rust/Cargo.toml | 1 + rust/examples/echo_client.rs | 23 ++++ rust/examples/echo_server.rs | 44 ++++--- rust/src/lib.rs | 235 +++++++++++++++++++++++------------ 7 files changed, 279 insertions(+), 93 deletions(-) create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 rust/examples/echo_client.rs diff --git a/.gitignore b/.gitignore index 468017d..74f6c05 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,4 @@ libtailscale.h /ruby/ext/libtailscale/go.mod /ruby/ext/libtailscale/go.sum /ruby/LICENSE -/rust/target +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..33fcd27 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,65 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "libtailscale" +version = "0.1.0" +dependencies = [ + "thiserror", +] + +[[package]] +name = "proc-macro2" +version = "1.0.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5ab016db510546d856297882807df8da66a16fb8c4101cb8b30054b0d5b2d9c" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5420d42e90af0c38c3290abcca25b9b3bdf379fc9f55c528f53a269d9c9a267e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..790843a --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,2 @@ +[workspace] +members = ["rust"] diff --git a/rust/Cargo.toml b/rust/Cargo.toml index ab56365..a8a2cc8 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -4,3 +4,4 @@ version = "0.1.0" edition = "2021" [dependencies] +thiserror = "1.0.39" diff --git a/rust/examples/echo_client.rs b/rust/examples/echo_client.rs new file mode 100644 index 0000000..fcced88 --- /dev/null +++ b/rust/examples/echo_client.rs @@ -0,0 +1,23 @@ +use std::{env, io::Write}; + +use libtailscale::{ServerBuilder, Network}; + +fn main() { + let target = env::args() + .skip(1) + .next() + .expect("usage: echoclient host:port"); + let srv = ServerBuilder::new() + .hostname("libtailscale-rs-echoclient") + .ephemeral() + .authkey(env::var("TS_AUTHKEY").expect("set TS_AUTHKEY in environment")) + .build() + .unwrap(); + + let mut conn = srv.connect(Network::Tcp, &target).unwrap(); + write!( + conn, + "This is a test of the Tailscale connection service.\n" + ) + .unwrap(); +} diff --git a/rust/examples/echo_server.rs b/rust/examples/echo_server.rs index a74c4fe..7ba2e57 100644 --- a/rust/examples/echo_server.rs +++ b/rust/examples/echo_server.rs @@ -1,24 +1,38 @@ -use std::io::{Read, self, Write}; +use std::{ + io::{self, Read, Write}, + net::TcpStream, + thread, +}; -use libtailscale::*; +use libtailscale::{ServerBuilder, Network}; fn main() { - let ts = Tailscale::new(); - ts.set_ephermal(true).unwrap(); - ts.up().unwrap(); + let ts = ServerBuilder::new().ephemeral().build().unwrap(); let ln = ts.listen(Network::Tcp, ":1999").unwrap(); - loop { - let mut conn = ln.accept().unwrap(); - let mut buf = [0; 2048]; - loop { - let nread = conn.read(&mut buf).unwrap(); - if nread > 0 { - io::stdout().write_all(&buf[0..nread]).unwrap(); - } else { - break; + for conn in ln { + match conn { + Ok(conn) => { + thread::spawn(move || { + handle_client(conn); + }); } + Err(err) => panic!("{err}"), + } + } +} + +fn handle_client(mut stream: TcpStream) { + let mut buf = [0; 2048]; + loop { + match stream.read(&mut buf) { + Ok(n) if n == 0 => { + break; + } + Ok(n) => { + io::stdout().write_all(&buf[0..n]).unwrap(); + } + Err(err) => panic!("{err}"), } } - } diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 9de66ca..39b6256 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -3,14 +3,33 @@ mod sys; use std::{ ffi::{c_int, CStr, CString}, fmt::Display, - fs::File, - io::{Read, Write}, - mem::ManuallyDrop, + net::TcpStream, os::fd::FromRawFd, + path::PathBuf, }; -#[derive(Debug)] -pub struct Error(String); +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("can't convert this from an OsString to a String, invalid unicode?")] + CantConvertToString, + + #[error("error fetching error from tsnet")] + FetchingErrorFromTSNet, + + #[error("[unexpected] string from tsnet has invalid UTF-8: {0}")] + TSNetSentBadUTF8(#[from] std::string::FromUtf8Error), + + #[error("tsnet: {0}")] + TSNet(String), + + #[error("io error: {0}")] + IO(#[from] std::io::Error), + + #[error("your string has NULL in it: {0}")] + NullInString(#[from] std::ffi::NulError), +} + +pub type Result = std::result::Result; #[derive(Debug)] pub enum Network { @@ -27,7 +46,7 @@ impl Display for Network { } } -pub struct Tailscale { +pub struct Server { /// a handle onto a Tailscale serve handle: sys::tailscale, } @@ -43,59 +62,41 @@ fn err(handle: c_int, code: c_int) -> Result<(), Error> { let slice = CStr::from_ptr(&errmsg as *const _); let errmsg = String::from_utf8_lossy(slice.to_bytes()).to_string(); - Err(Error(errmsg)) + Err(Error::TSNet(errmsg)) } } else { Ok(()) } } -impl Tailscale { - /// Creates a tailscale server object. - /// - /// No network connection is initialized until [`start`] is called. - pub fn new() -> Self { - unsafe { - Tailscale { - handle: sys::tailscale_new(), - } - } - } +impl Server { + pub fn connect(&self, network: Network, addr: &str) -> Result { + let mut conn: sys::tailscale_conn = 0; + let network = CString::new(format!("{}", network)).unwrap(); + let addr = CString::new(addr)?; - /// Connects the server to the tailnet. - /// - /// Calling this function is optional as it will be called by the first use - /// of `listen` or `dial` on a server. - /// - /// See also: `up`. - pub fn start(&self) -> Result<(), Error> { - unsafe { err(self.handle, sys::tailscale_start(self.handle)) } - } - - pub fn set_ephermal(&self, ephemeral: bool) -> Result<(), Error> { unsafe { err( self.handle, - sys::tailscale_set_ephemeral(self.handle, ephemeral as c_int), - ) + sys::tailscale_dial(self.handle, network.as_ptr(), addr.as_ptr(), &mut conn), + )? } - } - pub fn up(&self) -> Result<(), Error> { - unsafe { err(self.handle, sys::tailscale_up(self.handle)) } + let conn = conn as c_int; + Ok(unsafe { TcpStream::from_raw_fd(conn) }) } pub fn listen(&self, network: Network, address: &str) -> Result { unsafe { - let c_network = CString::new(format!("{}", network)).unwrap(); - let c_addr = CString::new(address).unwrap(); + let network = CString::new(format!("{}", network)).unwrap(); + let addr = CString::new(address).unwrap(); let mut out = 0; let res = err( self.handle, sys::tailscale_listen( self.handle, - c_network.as_ptr(), - c_addr.as_ptr(), + network.as_ptr(), + addr.as_ptr(), &mut out as *mut _, ), ); @@ -108,7 +109,7 @@ impl Tailscale { } } -impl Drop for Tailscale { +impl Drop for Server { fn drop(&mut self) { unsafe { sys::tailscale_close(self.handle); @@ -116,20 +117,131 @@ impl Drop for Tailscale { } } +#[derive(Default)] +pub struct ServerBuilder { + dir: Option, + hostname: Option, + authkey: Option, + control_url: Option, + ephemeral: bool, +} + +impl ServerBuilder { + /// Creates a server builder. + /// + /// Call [`ServerBuilder::build`] to start the server. + pub fn new() -> ServerBuilder { + ServerBuilder::default() + } + + pub fn dir(mut self, dir: PathBuf) -> Self { + self.dir = Some(dir); + self + } + + pub fn hostname(mut self, hostname: &str) -> Self { + self.hostname = Some(hostname.to_owned()); + self + } + + pub fn authkey(mut self, authkey: String) -> Self { + self.authkey = Some(authkey); + self + } + + pub fn control_url(mut self, control_url: String) -> Self { + self.control_url = Some(control_url); + self + } + + pub fn ephemeral(mut self) -> Self { + self.ephemeral = true; + self + } + + pub fn build(self) -> Result { + let result = unsafe { + Server { + handle: sys::tailscale_new(), + } + }; + + if let Some(dir) = self.dir { + let dir = dir.into_os_string(); + let dir = dir.into_string().map_err(|_| Error::CantConvertToString)?; + let dir = CString::new(dir)?; + unsafe { + err( + result.handle, + sys::tailscale_set_dir(result.handle, dir.as_ptr()), + )? + } + } + + if let Some(hostname) = self.hostname { + let hostname = CString::new(hostname)?; + unsafe { + err( + result.handle, + sys::tailscale_set_hostname(result.handle, hostname.as_ptr()), + )? + } + } + + if let Some(authkey) = self.authkey { + let authkey = CString::new(authkey)?; + unsafe { + err( + result.handle, + sys::tailscale_set_authkey(result.handle, authkey.as_ptr()), + )? + } + } + + if let Some(control_url) = self.control_url { + let control_url = CString::new(control_url)?; + unsafe { + err( + result.handle, + sys::tailscale_set_control_url(result.handle, control_url.as_ptr()), + )? + } + } + + unsafe { + err( + result.handle, + sys::tailscale_set_ephemeral(result.handle, if self.ephemeral { 1 } else { 0 }), + )? + } + + unsafe { err(result.handle, sys::tailscale_start(result.handle))? } + + Ok(result) + } +} + pub struct Listener { ts: sys::tailscale, handle: sys::tailscale_listener, } impl Listener { - pub fn accept(&self) -> Result { + pub fn accept(&self) -> Result { unsafe { let mut conn = 0; - let res = err(self.ts, sys::tailscale_accept(self.handle, &mut conn as *mut _)); - res.map(|_| Stream { handle: conn }) + let res = err( + self.ts, + sys::tailscale_accept(self.handle, &mut conn as *mut _), + ); + res.map(|_| TcpStream::from_raw_fd(conn)) } } + + pub fn incoming(&mut self) -> &Self { + self + } } impl Drop for Listener { @@ -140,40 +252,9 @@ impl Drop for Listener { } } -#[derive(Debug)] -pub struct Stream { - handle: sys::tailscale_conn, -} - -impl Drop for Stream { - fn drop(&mut self) { - unsafe { - drop(File::from_raw_fd(self.handle)); - } - } -} - -impl Write for Stream { - fn write(&mut self, buf: &[u8]) -> std::io::Result { - unsafe { - let mut fd = ManuallyDrop::new(File::from_raw_fd(self.handle)); - fd.write(buf) - } - } - - fn flush(&mut self) -> std::io::Result<()> { - unsafe { - let mut fd = ManuallyDrop::new(File::from_raw_fd(self.handle)); - fd.flush() - } - } -} - -impl Read for Stream { - fn read(&mut self, buf: &mut [u8]) -> std::io::Result { - unsafe { - let mut fd = ManuallyDrop::new(File::from_raw_fd(self.handle)); - fd.read(buf) - } +impl Iterator for Listener { + type Item = Result; + fn next(&mut self) -> Option> { + Some(self.accept()) } }