diff --git a/.editorconfig b/.editorconfig index 758eb9d9..ea604296 100644 --- a/.editorconfig +++ b/.editorconfig @@ -147,5 +147,5 @@ csharp_preserve_single_line_statements = true csharp_preserve_single_line_blocks = true # starting to convert to 4 spaces -[*WindowsOpenSshPipe*.cs] +[*{WindowsOpenSshPipe,WslSocket,UnixDomainSocketEndPoint}*.cs] indent_size = 4 diff --git a/CHANGELOG.md b/CHANGELOG.md index b7ffa653..5a0e4a5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,8 @@ ## Added - Added this changelog. -- Added PuTTY private key v3 support +- Added PuTTY private key v3 support. +- Added Window UNIX socket for WSL `ssh-agent` support. ## Fixed - Fixed using incorrect unmanaged memory free function in `PagentClent.SendMessage()`. diff --git a/SshAgentLib/Microsoft/UnixDomainSocketEndPoint.Windows.cs b/SshAgentLib/Microsoft/UnixDomainSocketEndPoint.Windows.cs new file mode 100644 index 00000000..0274c1f5 --- /dev/null +++ b/SshAgentLib/Microsoft/UnixDomainSocketEndPoint.Windows.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Net.Sockets +{ + /// Represents a Unix Domain Socket endpoint as a path. + public sealed partial class UnixDomainSocketEndPoint : EndPoint + { +#pragma warning disable CA1802 // on Unix these need to be static readonly rather than const, so we do the same on Windows for consistency + private static readonly int s_nativePathOffset = 2; // sizeof(sun_family) + private static readonly int s_nativePathLength = 108; // sizeof(sun_path) + private static readonly int s_nativeAddressSize = s_nativePathOffset + s_nativePathLength; // sizeof(sockaddr_un) +#pragma warning restore CA1802 + + private SocketAddress CreateSocketAddressForSerialize() => + new SocketAddress(AddressFamily.Unix, s_nativeAddressSize); + + // from afunix.h: + //#define UNIX_PATH_MAX 108 + //typedef struct sockaddr_un + //{ + // ADDRESS_FAMILY sun_family; /* AF_UNIX */ + // char sun_path[UNIX_PATH_MAX]; /* pathname */ + //} + //SOCKADDR_UN, *PSOCKADDR_UN; + } +} diff --git a/SshAgentLib/Microsoft/UnixDomainSocketEndPoint.cs b/SshAgentLib/Microsoft/UnixDomainSocketEndPoint.cs new file mode 100644 index 00000000..f2fe1716 --- /dev/null +++ b/SshAgentLib/Microsoft/UnixDomainSocketEndPoint.cs @@ -0,0 +1,161 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Text; +using System.IO; + +namespace System.Net.Sockets +{ + /// Represents a Unix Domain Socket endpoint as a path. + public sealed partial class UnixDomainSocketEndPoint : EndPoint + { + private const AddressFamily EndPointAddressFamily = AddressFamily.Unix; + + private readonly string _path; + private readonly byte[] _encodedPath; + + // Tracks the file Socket should delete on Dispose. + internal string BoundFileName { get; } + + public UnixDomainSocketEndPoint(string path) + : this(path, null) + { } + + private UnixDomainSocketEndPoint(string path, string boundFileName) + { + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + BoundFileName = boundFileName; + + // Pathname socket addresses should be null-terminated. + // Linux abstract socket addresses start with a zero byte, they must not be null-terminated. + bool isAbstract = IsAbstract(path); + int bufferLength = Encoding.UTF8.GetByteCount(path); + if (!isAbstract) + { + // for null terminator + bufferLength++; + } + + if (path.Length == 0 || bufferLength > s_nativePathLength) + { + const string ArgumentOutOfRange_PathLengthInvalid = + "The path '{0}' is of an invalid length for use with domain sockets on this platform. The length must be between 1 and {1} characters, inclusive."; + + throw new ArgumentOutOfRangeException( + nameof(path), path, + string.Format(ArgumentOutOfRange_PathLengthInvalid, path, s_nativePathLength)); + //SR.Format(SR.ArgumentOutOfRange_PathLengthInvalid, path, s_nativePathLength)); + } + + _path = path; + _encodedPath = new byte[bufferLength]; + int bytesEncoded = Encoding.UTF8.GetBytes(path, 0, path.Length, _encodedPath, 0); + Debug.Assert(bufferLength - (isAbstract ? 0 : 1) == bytesEncoded); + + // FIXME: see https://github.com/dotnet/runtime/blob/f85ea976f81945ea18cd5dc71959cccecdc93cd2/src/libraries/Common/src/System/Net/SocketProtocolSupportPal.Windows.cs#L14 + //if (!Socket.OSSupportsUnixDomainSockets) + //{ + // throw new PlatformNotSupportedException(); + //} + } + + internal static int MaxAddressSize => s_nativeAddressSize; + + internal UnixDomainSocketEndPoint(SocketAddress socketAddress) + { + if (socketAddress == null) + { + throw new ArgumentNullException(nameof(socketAddress)); + } + + if (socketAddress.Family != EndPointAddressFamily || + socketAddress.Size > s_nativeAddressSize) + { + throw new ArgumentOutOfRangeException(nameof(socketAddress)); + } + + if (socketAddress.Size > s_nativePathOffset) + { + _encodedPath = new byte[socketAddress.Size - s_nativePathOffset]; + for (int i = 0; i < _encodedPath.Length; i++) + { + _encodedPath[i] = socketAddress[s_nativePathOffset + i]; + } + + // Strip trailing null of pathname socket addresses. + int length = _encodedPath.Length; + if (!IsAbstract(_encodedPath)) + { + // Since this isn't an abstract path, we're sure our first byte isn't 0. + while (_encodedPath[length - 1] == 0) + { + length--; + } + } + _path = Encoding.UTF8.GetString(_encodedPath, 0, length); + } + else + { + _encodedPath = Array.Empty(); + _path = string.Empty; + } + } + + public override SocketAddress Serialize() + { + SocketAddress result = CreateSocketAddressForSerialize(); + + for (int index = 0; index < _encodedPath.Length; index++) + { + result[s_nativePathOffset + index] = _encodedPath[index]; + } + + return result; + } + + public override EndPoint Create(SocketAddress socketAddress) => new UnixDomainSocketEndPoint(socketAddress); + + public override AddressFamily AddressFamily => EndPointAddressFamily; + + public override string ToString() + { + bool isAbstract = IsAbstract(_path); + if (isAbstract) + { + // return string.Concat("@", _path.AsSpan(1)); + return "@" + _path.Substring(1); + } + else + { + return _path; + } + } + + internal UnixDomainSocketEndPoint CreateBoundEndPoint() + { + if (IsAbstract(_path)) + { + return this; + } + return new UnixDomainSocketEndPoint(_path, Path.GetFullPath(_path)); + } + + internal UnixDomainSocketEndPoint CreateUnboundEndPoint() + { + if (IsAbstract(_path) || BoundFileName is null) + { + return this; + } + return new UnixDomainSocketEndPoint(_path, null); + } + + private static bool IsAbstract(string path) => path.Length > 0 && path[0] == '\0'; + + private static bool IsAbstract(byte[] encodedPath) => encodedPath.Length > 0 && encodedPath[0] == 0; + } +} diff --git a/SshAgentLib/PageantAgent.cs b/SshAgentLib/PageantAgent.cs index 872813e9..05244fa4 100644 --- a/SshAgentLib/PageantAgent.cs +++ b/SshAgentLib/PageantAgent.cs @@ -1,4 +1,4 @@ -// +// // PageantAgent.cs // // Author(s): David Lechner @@ -53,6 +53,7 @@ public class PageantAgent : Agent const int ERROR_CLASS_ALREADY_EXISTS = 1410; const int WM_COPYDATA = 0x004A; const int WSAECONNABORTED = 10053; + const int WSAECONNRESET = 10054; /* From PuTTY source code */ @@ -70,6 +71,7 @@ public class PageantAgent : Agent object lockObject = new object(); CygwinSocket cygwinSocket; MsysSocket msysSocket; + WslSocket wslSocket; WindowsOpenSshPipe opensshPipe; Thread winThread; @@ -287,6 +289,36 @@ public void StopMsysSocket() msysSocket = null; } + /// + /// Starts a wsl style socket that can be used by the ssh program + /// that comes with wsl. + /// + /// The path to the socket file that will be created. + public void StartWslSocket(string path) + { + if (disposed) { + throw new ObjectDisposedException("PagentAgent"); + } + if (wslSocket != null) { + return; + } + // only overwrite a file if it looks like a WslSocket file. + if (File.Exists(path) && WslSocket.TestFile(path)) { + File.Delete(path); + } + wslSocket = new WslSocket(path, connectionHandler); + } + + public void StopWslSocket() + { + if (disposed) + throw new ObjectDisposedException("PagentAgent"); + if (wslSocket == null) + return; + wslSocket.Dispose(); + wslSocket = null; + } + public void StartWindowsOpenSshPipe() { if (disposed) { @@ -353,6 +385,7 @@ private void RunWindowInNewAppcontext() // make sure socket files are cleaned up when we stop. StopCygwinSocket(); StopMsysSocket(); + StopWslSocket(); StopWindowsOpenSshPipe(); if (hwnd != IntPtr.Zero) { @@ -455,7 +488,8 @@ void connectionHandler(Stream stream, Process process) } } catch (IOException ex) { var socketException = ex.InnerException as SocketException; - if (socketException != null && socketException.ErrorCode == WSAECONNABORTED) { + if (socketException != null && ( + socketException.ErrorCode == WSAECONNABORTED || socketException.ErrorCode == WSAECONNRESET)) { // expected error return; } diff --git a/SshAgentLib/SshAgentLib.csproj b/SshAgentLib/SshAgentLib.csproj index d56f83f1..a3523e30 100644 --- a/SshAgentLib/SshAgentLib.csproj +++ b/SshAgentLib/SshAgentLib.csproj @@ -95,6 +95,9 @@ + + + @@ -159,6 +162,9 @@ 1.8.1.3 + + 4.3.0 +