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
+