diff --git a/PageantHandler.cs b/PageantHandler.cs index f437639..0556cf9 100644 --- a/PageantHandler.cs +++ b/PageantHandler.cs @@ -81,6 +81,7 @@ struct COPYDATASTRUCT [DllImport("kernel32.dll", SetLastError = true)] static extern bool CloseHandle(IntPtr hHandle); + internal static ArraySegment AGENT_EMPTY_RESPONSE = new ArraySegment(new byte[] { 0x00, 0x00, 0x00, 0x05, 0x0c, 0x00, 0x00, 0x00, 0x00 }); internal const uint AGENT_MAX_MSGLEN = 8192; static readonly IntPtr AGENT_COPYDATA_ID = new IntPtr(0x804e50ba); diff --git a/Program.cs b/Program.cs index 3415cbf..fae6f8f 100644 --- a/Program.cs +++ b/Program.cs @@ -1,66 +1,54 @@ -using System; -using System.IO; -using System.Net.Sockets; -using System.Threading; +using Microsoft.Extensions.CommandLineUtils; +using System.Collections.Generic; using System.Threading.Tasks; namespace WslSSHPageant { class Program { - static Mutex mutex; - - static async Task Main(string[] args) + static void Main(string[] args) { - var socketPath = @".\ssh-agent.sock"; + CommandLineApplication commandLineApplication = new CommandLineApplication(throwOnUnexpectedArg: false); - if (args.Length == 1) - { - socketPath = args[0]; - } - else if (args.Length != 0) - { - Console.WriteLine(@"wsl-ssh-agent.exe "); - return; - } + CommandOption wslSocketPath = commandLineApplication.Option( + "--wsl ", + "Which path to listen on with the AF_UNIX socket for WSL", + CommandOptionType.SingleValue); - socketPath = Path.GetFullPath(socketPath); + CommandOption winsshPipeName = commandLineApplication.Option( + "--winssh ", + "Which pipe to listen on for Windows 10 OpenSSH Client", + CommandOptionType.SingleValue); - var mutexName = socketPath + "-{642b3e23-f0f5-4cc1-8a41-bf95e9a438ad}"; - mutexName = mutexName.Replace(Path.DirectorySeparatorChar, '_'); - mutex = new Mutex(true, mutexName); + commandLineApplication.HelpOption("-? | -h | --help"); - if (!mutex.WaitOne(TimeSpan.Zero, true)) - { - Console.Error.WriteLine("Already running on that AF_UNIX path"); - Console.In.ReadLine(); - return; - } + List runningServers = new List(); - try + commandLineApplication.OnExecute(() => { - File.Delete(socketPath); - var server = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.IP); - server.Bind(new UnixEndPoint(socketPath)); - server.Listen(5); - - Console.WriteLine(@"Listening on {0}", socketPath); - - // Enter the listening loop. - while (true) + if (wslSocketPath.HasValue()) + { + WSLSocket wslSocket = new WSLSocket(wslSocketPath.Value()); + runningServers.Add(wslSocket.Listen()); + } + if (winsshPipeName.HasValue()) { - WSLClient client = new WSLClient(await server.AcceptAsync()); + WinSSHSocket winsshSocket = new WinSSHSocket(winsshPipeName.Value()); + runningServers.Add(winsshSocket.Listen()); + } - // Don't await this, we want to service other sockets -#pragma warning disable CS4014 - client.WorkSocket(); -#pragma warning restore CS4014 + if (runningServers.Count < 1) + { + commandLineApplication.ShowHelp(); + return 1; } - } - finally - { - mutex.ReleaseMutex(); - } + + Task.WaitAny(runningServers.ToArray()); + + return 0; + }); + + commandLineApplication.Execute(args); } } } \ No newline at end of file diff --git a/Readme.md b/Readme.md index e8b9d96..8306734 100644 --- a/Readme.md +++ b/Readme.md @@ -1,12 +1,10 @@ # wsl-ssh-pageant -**Now uses freshly baked AF_UNIX support in Windows 10 insider** - -## How to use +## How to use with WSL 1. On the Windows side run Pageant (or compatible agent such as gpg4win). -2. Run wsl-ssh-pageant.exe on windows in a short path (max ~100 characters total!) +2. Run `wsl-ssh-pageant.exe --wsl C:\wsl-ssh-pageant` (or any other path) on windows in a short path (max ~100 characters total!) 3. In WSL run the following @@ -20,6 +18,25 @@ $ export SSH_AUTH_SOCK=/mnt/c/wsl-ssh-pageant/ssh-agent.sock 4. The SSH keys from Pageant should now be usable by `ssh`! +## How to use with Windows 10 native OpenSSH client + +1. On the Windows side run Pageant (or compatible agent such as gpg4win). + +2. Run `wsl-ssh-pageant.exe --winssh ssh-pageant` (or any other name) on windows in a short path (max ~100 characters total!) + +3. In cmd.exe run the following (or define it in your Environment Variables on windows) + +``` +$ set SSH_AUTH_SOCK=\\.\pipe\ssh-pageant +``` +(or whichever name you gave the pipe) + +4. The SSH keys from Pageant should now be usable by `ssh`! + +## Note + +You can use both `--winssh` and `--wsl` parameters at the same time with the same process to proxy for both + ## Credit Thanks to [John Starks](https://github.com/jstarks/) for [npiperelay](https://github.com/jstarks/npiperelay/), showing a more secure way to create a stream between WSL and Linux. diff --git a/SSHAgentClient.cs b/SSHAgentClient.cs new file mode 100644 index 0000000..5daa499 --- /dev/null +++ b/SSHAgentClient.cs @@ -0,0 +1,138 @@ +using System; +using System.Net.Sockets; +using System.Threading.Tasks; + +namespace WslSSHPageant +{ + internal abstract class SSHAgentClientPartialRead : SSHAgentClient + { + internal SSHAgentClientPartialRead() + { + } + + protected override async Task ReceiveArraySegment(ArraySegment buf) + { + int i; + while ((i = await ReceivePartialArraySegment(buf)) != 0) + { + buf = buf.Slice(i); + if (buf.Count <= 0) + { + return true; + } + } + return false; + } + + protected abstract Task ReceivePartialArraySegment(ArraySegment buf); + } + + internal abstract class SSHAgentClient + { + internal SSHAgentClient() + { + } + + protected virtual void Initialize() + { + return; + } + + protected abstract bool IsConnected(); + + protected abstract void Close(); + + protected abstract Task ReceiveArraySegment(ArraySegment buf); + + protected abstract Task SendArraySegment(ArraySegment buf); + + internal async Task WorkSocket() + { + Initialize(); + + bool clientWasSuccess = false; + + try + { + clientWasSuccess = await ServiceSocket(); + } + catch (TimeoutException) + { + // Ignore timeouts, those should not explode our stuff + Console.Error.WriteLine("Socket timeout"); + } + // These two just mean the remote end closed the socket, we don't care, same for TaskCanceledException + catch (ObjectDisposedException) { } + catch (InvalidOperationException) { } + catch (TaskCanceledException) { } + catch (SocketException e) + { + // Other socket errors can happen and shouldn't kill the app + Console.Error.WriteLine(e); + } + catch (PageantException e) + { + // Pageant errors can happen, too + Console.Error.WriteLine(e); + } + catch (Exception e) + { + Console.Error.WriteLine(e); + throw e; + } + finally + { + if (IsConnected() && !clientWasSuccess) + { + try + { + await SendArraySegment(PageantHandler.AGENT_EMPTY_RESPONSE); + } + catch { } + } + + Close(); + } + } + + private async Task ServiceSocket() + { + var bytes = new byte[PageantHandler.AGENT_MAX_MSGLEN]; + + bool lastWasSuccess = true; + + while (IsConnected()) + { + // Read length as uint32 (4 bytes) + if (!await ReceiveArraySegment(new ArraySegment(bytes, 0, 4))) + { + break; + } + + lastWasSuccess = false; + + var len = (bytes[0] << 24) | + (bytes[1] << 16) | + (bytes[2] << 8) | + (bytes[3]); + + if (len + 4 > PageantHandler.AGENT_MAX_MSGLEN) + { + break; + } + + // Read actual data in the part after len + if (!await ReceiveArraySegment(new ArraySegment(bytes, 4, len))) + { + break; + } + + var msg = PageantHandler.Query(new ArraySegment(bytes, 0, len + 4)); + await SendArraySegment(new ArraySegment(msg, 0, msg.Length)); + lastWasSuccess = true; + } + + return lastWasSuccess; + } + } +} diff --git a/WSLClient.cs b/WSLClient.cs index ab4e22e..802b4d6 100644 --- a/WSLClient.cs +++ b/WSLClient.cs @@ -1,114 +1,42 @@ using System; -using System.Collections.Generic; using System.Net.Sockets; -using System.Text; using System.Threading.Tasks; namespace WslSSHPageant { - class WSLClient + class WSLClient : SSHAgentClientPartialRead { - static ArraySegment emptyResponse = new ArraySegment(new byte[] { 0x00, 0x00, 0x00, 0x05, 0x0c, 0x00, 0x00, 0x00, 0x00 }); + readonly Socket client; - Socket client; internal WSLClient(Socket client) { this.client = client; } - internal async Task WorkSocket() + protected override void Initialize() { client.ReceiveTimeout = 1000; client.SendTimeout = 1000; - - bool clientWasSuccess = false; - - try - { - clientWasSuccess = await ServiceSocket(client); - } - catch (TimeoutException) - { - // Ignore timeouts, those should not explode our stuff - Console.Error.WriteLine("Socket timeout"); - } - catch (SocketException e) - { - // Other socket errors can happen and shouldn't kill the app - Console.Error.WriteLine(e); - } - catch (PageantException e) - { - // Pageant errors can happen, too - Console.Error.WriteLine(e); - } - finally - { - if (client.Connected && !clientWasSuccess) - { - try - { - await client.SendAsync(emptyResponse, SocketFlags.None); - } - catch { } - } - - client.Dispose(); - } } - private async Task ReadUntil(Socket client, ArraySegment buf) + protected override void Close() { - int i; - while ((i = await client.ReceiveAsync(buf, SocketFlags.None)) != 0) - { - buf = buf.Slice(i); - if (buf.Count <= 0) - { - return true; - } - } - return false; + client.Close(); } - private async Task ServiceSocket(Socket client) + protected override async Task ReceivePartialArraySegment(ArraySegment buf) { - var bytes = new byte[PageantHandler.AGENT_MAX_MSGLEN]; - - bool lastWasSuccess = true; - - while (true) - { - // Read length as uint32 (4 bytes) - if (!await ReadUntil(client, new ArraySegment(bytes, 0, 4))) - { - break; - } - - lastWasSuccess = false; - - var len = (bytes[0] << 24) | - (bytes[1] << 16) | - (bytes[2] << 8) | - (bytes[3]); - - if (len + 4 > PageantHandler.AGENT_MAX_MSGLEN) - { - break; - } - - // Read actual data in the part after len - if (!await ReadUntil(client, new ArraySegment(bytes, 4, len))) - { - break; - } + return await client.ReceiveAsync(buf, SocketFlags.None); + } - var msg = PageantHandler.Query(new ArraySegment(bytes, 0, len + 4)); - await client.SendAsync(new ArraySegment(msg, 0, msg.Length), SocketFlags.None); - lastWasSuccess = true; - } + protected override bool IsConnected() + { + return client.Connected; + } - return lastWasSuccess; + protected override async Task SendArraySegment(ArraySegment buf) + { + return buf.Count == await client.SendAsync(buf, SocketFlags.None); } } } diff --git a/WSLSocket.cs b/WSLSocket.cs new file mode 100644 index 0000000..c56493d --- /dev/null +++ b/WSLSocket.cs @@ -0,0 +1,68 @@ +using System; +using System.IO; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; + +namespace WslSSHPageant +{ + class WSLSocket : IDisposable + { + readonly Mutex mutex; + readonly string path; + + internal WSLSocket(string path) + { + this.path = Path.GetFullPath(path); + + var mutexName = this.path + "-{642b3e23-f0f5-4cc1-8a41-bf95e9a438ad}"; + + mutexName = mutexName.Replace(Path.DirectorySeparatorChar, '_'); + mutex = new Mutex(true, mutexName); + + if (!mutex.WaitOne(TimeSpan.Zero, true)) + { + throw new ArgumentException("Already running on that AF_UNIX path"); + } + } + + internal async Task Listen() + { + try + { + File.Delete(path); + var server = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.IP); + server.Bind(new UnixEndPoint(path)); + server.Listen(5); + + Console.Out.WriteLine("WSL AF_UNIX socket listening on " + path); + + // Enter the listening loop. + while (true) + { + WSLClient client = new WSLClient(await server.AcceptAsync()); + + // Don't await this, we want to service other sockets +#pragma warning disable CS4014 + client.WorkSocket(); +#pragma warning restore CS4014 + } + } + finally + { + Dispose(); + } + } + + public void Dispose() + { + try + { + mutex.ReleaseMutex(); + mutex.Dispose(); + } + catch(ApplicationException) { } + catch(ObjectDisposedException) { } + } + } +} diff --git a/WinSSHClient.cs b/WinSSHClient.cs new file mode 100644 index 0000000..f19899a --- /dev/null +++ b/WinSSHClient.cs @@ -0,0 +1,51 @@ +using System; +using System.IO; +using System.IO.Pipes; +using System.Threading; +using System.Threading.Tasks; + +namespace WslSSHPageant +{ + class WinSSHClient : SSHAgentClientPartialRead + { + readonly NamedPipeServerStream pipeServer; + + internal WinSSHClient(NamedPipeServerStream pipeServer) + { + this.pipeServer = pipeServer; + } + + protected override void Close() + { + try + { + pipeServer.Disconnect(); + } + // Those two just mean it is closed already and thus we don't care + catch (ObjectDisposedException) { } + catch (InvalidOperationException) { } + + pipeServer.Dispose(); + } + + protected override bool IsConnected() + { + return pipeServer.IsConnected; + } + + protected override async Task ReceivePartialArraySegment(ArraySegment buf) + { + var cancellationTokenSource = new CancellationTokenSource(); + cancellationTokenSource.CancelAfter(1000); + return await pipeServer.ReadAsync(buf.Array, buf.Offset, buf.Count, cancellationTokenSource.Token); + } + + protected override async Task SendArraySegment(ArraySegment buf) + { + var cancellationTokenSource = new CancellationTokenSource(); + cancellationTokenSource.CancelAfter(1000); + await pipeServer.WriteAsync(buf.Array, buf.Offset, buf.Count, cancellationTokenSource.Token); + return true; + } + } +} diff --git a/WinSSHSocket.cs b/WinSSHSocket.cs new file mode 100644 index 0000000..5099d31 --- /dev/null +++ b/WinSSHSocket.cs @@ -0,0 +1,36 @@ +using System; +using System.IO; +using System.IO.Pipes; +using System.Threading; +using System.Threading.Tasks; + +namespace WslSSHPageant +{ + class WinSSHSocket + { + readonly string pipeName; + + internal WinSSHSocket(string name) + { + pipeName = name; + } + + internal async Task Listen() + { + Console.Out.WriteLine("Listening for Win10 OpenSSH connections on \\\\.\\pipe\\" + pipeName); + + while (true) + { + var pipeServer = new NamedPipeServerStream(pipeName, PipeDirection.InOut, NamedPipeServerStream.MaxAllowedServerInstances, PipeTransmissionMode.Byte, PipeOptions.Asynchronous); + await pipeServer.WaitForConnectionAsync(); + + WinSSHClient client = new WinSSHClient(pipeServer); + + // Don't await this, we want to service other sockets +#pragma warning disable CS4014 + client.WorkSocket(); +#pragma warning restore CS4014 + } + } + } +} diff --git a/wsl-ssh-pageant.csproj b/wsl-ssh-pageant.csproj index fc5c8a6..90878ca 100644 --- a/wsl-ssh-pageant.csproj +++ b/wsl-ssh-pageant.csproj @@ -23,4 +23,8 @@ false + + + +