diff --git a/README.md b/README.md index 8cfee64..d51c94c 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ It runs a `dosh-server` on the remote machine and a `dosh` client locally. The first setup can use SSH. After that, Dosh can attach over encrypted UDP with cached credentials, keep terminal sessions alive, reconnect after network changes, and forward TCP ports. +It also includes native encrypted file copy over the Dosh stream layer. ## Support @@ -15,8 +16,8 @@ changes, and forward TCP ports. - Windows: client only - UDP port: one configured server port, default `50000` -Dosh is not an SCP/SFTP client and does not implement X11 forwarding. Windows -is client-only. +Dosh does not implement SFTP, X11 forwarding, or a Windows server. Windows is +client-only. ## Install @@ -44,6 +45,12 @@ irm https://git.palav.dev/Palav/dosh/raw/branch/main/install.ps1 | iex dosh setup HOST # import SSH config and trust the Dosh host key dosh HOST # connect dosh HOST COMMAND # connect and run a command +dosh exec HOST COMMAND # run a non-interactive remote command +dosh cp SRC DST # copy files; use host:path for a remote side +dosh ls host:path # list a remote path +dosh cat host:path # print a remote file +dosh mkdir host:path # create a remote directory +dosh rm [-r] host:path # remove a remote file or directory dosh update # update Dosh dosh status HOST # show remote tmux sessions and Dosh service status dosh restart HOST # restart dosh-server and show service status @@ -56,12 +63,74 @@ Examples: ```sh dosh server dosh server tm +dosh exec server 'uname -a' +dosh cp file.txt server:tmp/file.txt +dosh cp -r server:Projects/app ./app +dosh cp -r server:Projects/app backup:app +dosh ls server:Projects +dosh cat server:tmp/file.txt dosh forward server -L 8080:127.0.0.1:80 dosh forward server -D 1080 dosh forward server -R 2222:127.0.0.1:22 ``` Agent forwarding is opt-in with `-A` and must be enabled on the server. +File copy is enabled by `allow_file_transfer = true` on the server. + +## Library + +Dosh can also be used as a Rust transport for application protocols that need +encrypted streams, roaming, reconnect behavior, keepalives, retransmission, +flow control, and in-order delivery. + +Use `dosh::client::DoshClient` and `dosh::server::DoshServer` for the complete +native-auth path. Use `dosh::transport::DoshTransport` or `StreamMux` only when +you already have your own session/auth layer. + +Client-side SDK: + +```rust +let client = dosh::client::DoshClient::load()?; +let mut dosh = client + .connect("server") + .service("myapp") + .connect() + .await? + .into_transport(); + +let stream = dosh.open_service("myapp").await?; +dosh.send(stream, b"hello").await?; +``` + +Server-side SDK: + +```rust +let config = dosh::server::DoshServerConfig::default() + .service("myapp")?; +let mut server = dosh::server::DoshServer::bind(config).await?; + +loop { + match server.recv().await? { + dosh::server::DoshServerEvent::Accepted(client) => { + eprintln!("accepted {:?}", client.conn_id); + } + dosh::server::DoshServerEvent::Session { conn_id, event } => { + if let dosh::transport::SessionEvent::Stream( + dosh::transport::TransportEvent::Open(open) + ) = event { + server.accept_stream(conn_id, open.stream_id).await?; + } + } + dosh::server::DoshServerEvent::Ignored => {} + } +} +``` + +The server runtime owns UDP reads and demuxes packets by connection id, so +multiple clients can share one Dosh port without per-session readers racing. + +Runnable SDK examples live in `examples/sdk_echo_server.rs` and +`examples/sdk_echo_client.rs`. ## Config diff --git a/examples/sdk_echo_client.rs b/examples/sdk_echo_client.rs new file mode 100644 index 0000000..a89f1da --- /dev/null +++ b/examples/sdk_echo_client.rs @@ -0,0 +1,50 @@ +use anyhow::{Result, bail}; +use dosh::client::DoshClient; +use dosh::transport::{SessionEvent, TransportEvent}; + +#[tokio::main] +async fn main() -> Result<()> { + let mut args = std::env::args().skip(1); + let host = args.next().unwrap_or_else(|| "server".to_string()); + let message = args.collect::>().join(" "); + let message = if message.is_empty() { + "hello from dosh".to_string() + } else { + message + }; + + let client = DoshClient::load()?; + let mut transport = client + .connect(host) + .service("echo") + .connect() + .await? + .into_transport(); + let stream = transport.open_service("echo").await?; + + loop { + match transport.recv().await? { + SessionEvent::Stream(TransportEvent::OpenOk { stream_id, .. }) + if stream_id == stream => + { + transport.send(stream, message.as_bytes().to_vec()).await?; + } + SessionEvent::Stream(TransportEvent::Data(data)) if data.stream_id == stream => { + for chunk in data.chunks { + print!("{}", String::from_utf8_lossy(&chunk)); + } + println!(); + transport.close(stream).await?; + return Ok(()); + } + SessionEvent::Stream(TransportEvent::OpenReject { reason, .. }) => { + bail!("server rejected echo stream: {reason}"); + } + SessionEvent::Stream(_) + | SessionEvent::Ping + | SessionEvent::Pong + | SessionEvent::Ignored => {} + } + transport.maintenance().await?; + } +} diff --git a/examples/sdk_echo_server.rs b/examples/sdk_echo_server.rs new file mode 100644 index 0000000..11857b6 --- /dev/null +++ b/examples/sdk_echo_server.rs @@ -0,0 +1,37 @@ +use anyhow::Result; +use dosh::server::{DoshServer, DoshServerConfig, DoshServerEvent}; +use dosh::transport::{SessionEvent, TransportEvent}; + +#[tokio::main] +async fn main() -> Result<()> { + let config = DoshServerConfig::default().service("echo")?; + let mut server = DoshServer::bind(config).await?; + eprintln!("listening on {}", server.local_addr()?); + + loop { + match server.recv().await? { + DoshServerEvent::Accepted(client) => { + eprintln!( + "accepted user={} session={} conn={:?}", + client.user, client.session, client.conn_id + ); + } + DoshServerEvent::Session { + conn_id, + event: SessionEvent::Stream(TransportEvent::Open(open)), + } => { + server.accept_stream(conn_id, open.stream_id).await?; + } + DoshServerEvent::Session { + conn_id, + event: SessionEvent::Stream(TransportEvent::Data(data)), + } => { + for chunk in data.chunks { + server.send(conn_id, data.stream_id, chunk).await?; + } + } + DoshServerEvent::Session { .. } | DoshServerEvent::Ignored => {} + } + server.maintenance().await?; + } +}