diff --git a/README.md b/README.md index f03a2d5..d5cb2dc 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,12 @@ interval = "1hr" iptables_cmd = "sudo iptables-legacy" # Ports to forward ports = [ 1234 ] + +# Leave out this section to disable SSH agent forwarding +[ssh_agent] +# Default to the path below, can be omitted if unchanged +# Set `SSH_AUTH_SOCK` to the path you specified. +ssh_auth_sock = "/tmp/.wsld/ssh_auth_sock" ``` then run `wsld` and set `DISPLAY=:0`. diff --git a/client/src/config.rs b/client/src/config.rs index 6d44d35..6cc7831 100644 --- a/client/src/config.rs +++ b/client/src/config.rs @@ -18,6 +18,9 @@ pub struct Config { #[serde(default)] pub tcp_forward: Option, + + #[serde(default)] + pub ssh_agent: Option, } impl Default for Config { @@ -27,6 +30,7 @@ impl Default for Config { time: None, x11: None, tcp_forward: None, + ssh_agent: None, } } } @@ -85,3 +89,13 @@ pub struct TcpForwardConfig { pub ports: Vec, } + +fn default_ssh_auth_sock() -> String { + "/tmp/.wsld/ssh_auth_sock".to_owned() +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct SshAgentConfig { + #[serde(default = "default_ssh_auth_sock")] + pub ssh_auth_sock: String, +} diff --git a/client/src/main.rs b/client/src/main.rs index 3b6483d..52f6046 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -1,4 +1,5 @@ mod config; +mod ssh_agent; mod tcp; mod time; mod util; @@ -101,6 +102,14 @@ async fn main() { })); } + if let Some(config) = &CONFIG.ssh_agent { + tasks.push(tokio::task::spawn(async move { + if let Err(err) = ssh_agent::ssh_agent_forward(config).await { + eprintln!("SSH-Agent forwarder error: {}", err); + } + })); + } + for task in tasks { let _ = task.await; } diff --git a/client/src/ssh_agent.rs b/client/src/ssh_agent.rs new file mode 100644 index 0000000..2795693 --- /dev/null +++ b/client/src/ssh_agent.rs @@ -0,0 +1,40 @@ +use super::config::SshAgentConfig; +use super::util::{connect_stream, either}; +use super::vmsocket::VmSocket; +use super::CONFIG; + +use std::fs::Permissions; +use std::os::unix::fs::PermissionsExt; +use std::path::Path; +use tokio::io::AsyncWriteExt; +use tokio::net::{UnixListener, UnixStream}; + +async fn handle_stream(mut stream: UnixStream) -> std::io::Result<()> { + let mut server = VmSocket::connect(CONFIG.service_port).await?; + server.write_all(b"ssha").await?; + + let (client_r, client_w) = stream.split(); + let (server_r, server_w) = server.split(); + let a = connect_stream(client_r, server_w); + let b = connect_stream(server_r, client_w); + either(a, b).await +} + +pub async fn ssh_agent_forward(config: &'static SshAgentConfig) -> std::io::Result<()> { + // Remove existing socket + let _ = std::fs::create_dir_all(Path::new(&config.ssh_auth_sock).parent().unwrap()); + let _ = std::fs::remove_file(&config.ssh_auth_sock); + + let listener = UnixListener::bind(&config.ssh_auth_sock)?; + let _ = std::fs::set_permissions(&config.ssh_auth_sock, Permissions::from_mode(0o600)); + + loop { + let stream = listener.accept().await?.0; + + tokio::task::spawn(async move { + if let Err(err) = handle_stream(stream).await { + eprintln!("Failed to transfer: {}", err); + } + }); + } +} diff --git a/docs/impl.md b/docs/impl.md index 5297b95..6be0a70 100644 --- a/docs/impl.md +++ b/docs/impl.md @@ -34,3 +34,7 @@ Windows could access service listening to localhost in WSL2, but WSL2 couldn't d You might think we can just listen on a TCP port in WSL2 and forward it to Windows through Vsock, just like how we forward X11. However, this is not true. If you listen to a port in WSL2, Windows-to-WSL2 localhost forwarding will kick in, forwarding the `wsldhost` to Windows connection back into `wsld` inside WSL2. This creates a loop and soon both daemons will run out of file descriptors or memory. We creatively use iptables redirection to achieve this forwarding. `wsld` will only listen on a service port, which is not any of the ports being forwarded. WSL uses `/proc/net/tcp` to determine if a port is being listened on and whether forwarding from Windows to WSL2 should kick in. Because none of the forwarded port is being listened on, we avoid the loop issue. To allow `wsld` to intercept requests sent to forwarded ports, we employ iptables's nat table's OUTPUT chain. `wsld` will add one `REDIRECT` rule for each port being forwarded. + +# SSH Agent Forwarding + +`wsld` will listen on `/tmp/.wsld/ssh_auth_sock` (or another path configured) and forward the connection to `wsldhost`, which will in turn forward the connection to the named pipe `\\.\pipe\openssh-ssh-agent` which OpenSSH on Windows listens on. diff --git a/server/src/main.rs b/server/src/main.rs index 1f4def4..2de6ec9 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -2,6 +2,7 @@ #![windows_subsystem = "windows"] mod config; +mod ssh_agent; mod tcp; mod time; mod util; @@ -33,6 +34,7 @@ async fn handle_stream(mut stream: TcpStream) -> std::io::Result<()> { b"x11\0" => x11::handle_x11(stream).await, b"time" => time::handle_time(stream).await, b"tcp\0" => tcp::handle_tcp(stream).await, + b"ssha" => ssh_agent::handle_ssh_agent(stream).await, b"noop" => Ok(()), _ => Err(Error::new( ErrorKind::InvalidData, diff --git a/server/src/ssh_agent.rs b/server/src/ssh_agent.rs new file mode 100644 index 0000000..297b868 --- /dev/null +++ b/server/src/ssh_agent.rs @@ -0,0 +1,14 @@ +use super::util::{connect_stream, either}; + +use tokio::net::windows::named_pipe; +use tokio::net::TcpStream; + +pub async fn handle_ssh_agent(mut stream: TcpStream) -> std::io::Result<()> { + let (client_r, client_w) = stream.split(); + + let server = named_pipe::ClientOptions::new().open(r"\\.\pipe\openssh-ssh-agent")?; + let (server_r, server_w) = tokio::io::split(server); + let a = connect_stream(client_r, server_w); + let b = connect_stream(server_r, client_w); + either(a, b).await +}