From b6e40b7b54c28a6b3445fe6c01b535c1b527d22a Mon Sep 17 00:00:00 2001 From: Wiktor Kwapisiewicz Date: Sat, 23 Mar 2024 16:18:58 +0100 Subject: [PATCH] Add support for Windows Named Pipes Signed-off-by: Wiktor Kwapisiewicz --- .github/workflows/ci.yml | 24 +++++++++++++++++--- Cargo.lock | 4 ++-- Cargo.toml | 2 +- README.md | 20 ++++++++++++++++- src/agent.rs | 47 ++++++++++++++++++++++++++++++++++++---- 5 files changed, 86 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1db68de..3c33522 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,7 +22,13 @@ jobs: formatting: name: Check formatting - runs-on: ubuntu-latest + strategy: + matrix: + include: + - os: ubuntu-latest + - os: macos-latest + - os: windows-latest + runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - name: Check formatting @@ -30,7 +36,13 @@ jobs: tests: name: Unit tests - runs-on: ubuntu-latest + strategy: + matrix: + include: + - os: ubuntu-latest + - os: macos-latest + - os: windows-latest + runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - name: Build and test @@ -38,7 +50,13 @@ jobs: lints: name: Clippy lints - runs-on: ubuntu-latest + strategy: + matrix: + include: + - os: ubuntu-latest + - os: macos-latest + - os: windows-latest + runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - name: Check for lints diff --git a/Cargo.lock b/Cargo.lock index a3e50be..fa8f36f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -484,9 +484,9 @@ dependencies = [ [[package]] name = "service-binding" -version = "1.1.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f8418a60a094aaaf2bd38ed3ee4fea0b188bf5903a9655c144085f1464c221b" +checksum = "2ef733c6f7034fe1a9c3d812cdef93f2bec735e22b0467f84063de142be42428" [[package]] name = "slab" diff --git a/Cargo.toml b/Cargo.toml index 96963d2..71a7974 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,7 @@ futures = { version = "0.3.30", optional = true } log = { version = "0.4.6", optional = true } tokio = { version = "1", optional = true, features = ["rt", "net"] } tokio-util = { version = "0.7.1", optional = true, features = ["codec"] } -service-binding = "1.1.0" +service-binding = { version = "^2" } [features] default = ["agent"] diff --git a/README.md b/README.md index 765af91..463c8fb 100644 --- a/README.md +++ b/README.md @@ -9,10 +9,14 @@ This makes it possible to utilize remote keys not supported by the default OpenS ## Example -This example starts listening on a Unix socket `ssh-agent.sock` and processes requests. +The following example starts listening on a socket and processing requests. +On Unix it uses `ssh-agent.sock` Unix domain socket while on Windows it uses a named pipe `\\.\pipe\agent`. ```rust,no_run +#[cfg(not(windows))] use tokio::net::UnixListener; +#[cfg(windows)] +use ssh_agent_lib::agent::NamedPipeListener; use ssh_agent_lib::agent::{Session, Agent}; use ssh_agent_lib::proto::message::Message; @@ -35,6 +39,7 @@ impl Session for MyAgent { } #[tokio::main] +#[cfg(not(windows))] async fn main() -> Result<(), Box> { let socket = "ssh-agent.sock"; let _ = std::fs::remove_file(socket); // remove the socket if exists @@ -42,6 +47,13 @@ async fn main() -> Result<(), Box> { MyAgent.listen(UnixListener::bind(socket)?).await?; Ok(()) } + +#[tokio::main] +#[cfg(windows)] +async fn main() -> Result<(), Box> { + MyAgent.listen(NamedPipeListener::new(r"\\.\pipe\agent")?).await?; + Ok(()) +} ``` Now, point your OpenSSH client to this socket using `SSH_AUTH_SOCK` environment variable and it will transparently use the agent: @@ -50,6 +62,12 @@ Now, point your OpenSSH client to this socket using `SSH_AUTH_SOCK` environment SSH_AUTH_SOCK=ssh-agent.sock ssh user@example.com ``` +On Windows the path of the pipe has to be used: + +```sh +SSH_AUTH_SOCK=\\.\pipe\agent ssh user@example.com +``` + For more elaborate example see the `examples` directory or [crates using `ssh-agent-lib`](https://crates.io/crates/ssh-agent-lib/reverse_dependencies). ## Note diff --git a/src/agent.rs b/src/agent.rs index cb79ec9..15ff966 100644 --- a/src/agent.rs +++ b/src/agent.rs @@ -4,6 +4,8 @@ use bytes::{Buf, BufMut, BytesMut}; use futures::{SinkExt, TryStreamExt}; use log::{error, info}; use tokio::io::{AsyncRead, AsyncWrite}; +#[cfg(windows)] +use tokio::net::windows::named_pipe::{NamedPipeServer, ServerOptions}; use tokio::net::{TcpListener, TcpStream}; #[cfg(unix)] use tokio::net::{UnixListener, UnixStream}; @@ -58,14 +60,14 @@ impl Encoder for MessageCodec { pub trait ListeningSocket { type Stream: fmt::Debug + AsyncRead + AsyncWrite + Send + Unpin + 'static; - async fn accept(&self) -> io::Result; + async fn accept(&mut self) -> io::Result; } #[cfg(unix)] #[async_trait] impl ListeningSocket for UnixListener { type Stream = UnixStream; - async fn accept(&self) -> io::Result { + async fn accept(&mut self) -> io::Result { UnixListener::accept(self).await.map(|(s, _addr)| s) } } @@ -73,11 +75,40 @@ impl ListeningSocket for UnixListener { #[async_trait] impl ListeningSocket for TcpListener { type Stream = TcpStream; - async fn accept(&self) -> io::Result { + async fn accept(&mut self) -> io::Result { TcpListener::accept(self).await.map(|(s, _addr)| s) } } +#[cfg(windows)] +#[derive(Debug)] +pub struct NamedPipeListener(NamedPipeServer, std::ffi::OsString); + +#[cfg(windows)] +impl NamedPipeListener { + pub fn new(pipe: std::ffi::OsString) -> std::io::Result { + Ok(NamedPipeListener( + ServerOptions::new() + .first_pipe_instance(true) + .create(&pipe)?, + pipe, + )) + } +} + +#[cfg(windows)] +#[async_trait] +impl ListeningSocket for NamedPipeListener { + type Stream = NamedPipeServer; + async fn accept(&mut self) -> io::Result { + self.0.connect().await?; + Ok(std::mem::replace( + &mut self.0, + ServerOptions::new().create(&self.1)?, + )) + } +} + #[async_trait] pub trait Session: 'static + Sync + Send + Sized { async fn handle(&mut self, message: Message) -> Result>; @@ -112,7 +143,7 @@ pub trait Session: 'static + Sync + Send + Sized { #[async_trait] pub trait Agent: 'static + Sync + Send + Sized { fn new_session(&mut self) -> impl Session; - async fn listen(mut self, socket: S) -> Result<(), AgentError> + async fn listen(mut self, mut socket: S) -> Result<(), AgentError> where S: ListeningSocket + fmt::Debug + Send, { @@ -144,6 +175,14 @@ pub trait Agent: 'static + Sync + Send + Sized { service_binding::Listener::Tcp(listener) => { self.listen(TcpListener::from_std(listener)?).await } + #[cfg(windows)] + service_binding::Listener::NamedPipe(pipe) => { + self.listen(NamedPipeListener::new(pipe)?).await + } + #[cfg(not(windows))] + service_binding::Listener::NamedPipe(_) => Err(AgentError::IO(std::io::Error::other( + "Named pipes supported on Windows only", + ))), } } }