diff --git a/src/Plugins/SignClient/Settings.cs b/src/Plugins/SignClient/Settings.cs
index 7660c76e87..4bb58e4382 100644
--- a/src/Plugins/SignClient/Settings.cs
+++ b/src/Plugins/SignClient/Settings.cs
@@ -25,15 +25,21 @@ public class Settings : PluginSettings
///
/// The host of the sign client(i.e. Signer).
+ /// The "Endpoint" should be "vsock://contextId:port" if use vsock.
+ /// The "Endpoint" should be "http://host:port" or "https://host:port" if use tcp.
///
public readonly string Endpoint;
+ ///
+ /// Create a new settings instance from the configuration section.
+ ///
+ /// The configuration section.
+ /// If the endpoint type or endpoint is invalid.
public Settings(IConfigurationSection section) : base(section)
{
Name = section.GetValue("Name", "SignClient");
-
- // Only support local host at present, so host always is "127.0.0.1" or "::1" now.
- Endpoint = section.GetValue("Endpoint", DefaultEndpoint);
+ Endpoint = section.GetValue("Endpoint", DefaultEndpoint); // Only support local host at present
+ _ = GetVsockAddress(); // for check the endpoint is valid
}
public static Settings Default
@@ -51,5 +57,24 @@ public static Settings Default
return new Settings(section);
}
}
+
+ ///
+ /// Get the vsock address from the endpoint.
+ ///
+ /// The vsock address. If the endpoint type is not vsock, return null.
+ /// If the endpoint is invalid.
+ internal VsockAddress? GetVsockAddress()
+ {
+ var uri = new Uri(Endpoint); // UriFormatException is a subclass of FormatException
+ if (uri.Scheme != "vsock") return null;
+ try
+ {
+ return new VsockAddress(int.Parse(uri.Host), uri.Port);
+ }
+ catch
+ {
+ throw new FormatException($"Invalid vsock endpoint: {Endpoint}");
+ }
+ }
}
}
diff --git a/src/Plugins/SignClient/SignClient.cs b/src/Plugins/SignClient/SignClient.cs
index 8667aaba5d..ad54ac6426 100644
--- a/src/Plugins/SignClient/SignClient.cs
+++ b/src/Plugins/SignClient/SignClient.cs
@@ -66,9 +66,8 @@ private void Reset(string name, SecureSign.SecureSignClient? client)
if (!string.IsNullOrEmpty(_name)) SignerManager.RegisterSigner(_name, this);
}
- private void Reset(Settings settings)
+ private ServiceConfig GetServiceConfig(Settings settings)
{
- // _settings = settings;
var methodConfig = new MethodConfig
{
Names = { MethodName.Default },
@@ -91,10 +90,24 @@ private void Reset(Settings settings)
}
};
- var channel = GrpcChannel.ForAddress(settings.Endpoint, new GrpcChannelOptions
+ return new ServiceConfig { MethodConfigs = { methodConfig } };
+ }
+
+ private void Reset(Settings settings)
+ {
+ // _settings = settings;
+ var serviceConfig = GetServiceConfig(settings);
+ var vsockAddress = settings.GetVsockAddress();
+
+ GrpcChannel channel;
+ if (vsockAddress is not null)
{
- ServiceConfig = new ServiceConfig { MethodConfigs = { methodConfig } }
- });
+ channel = Vsock.CreateChannel(vsockAddress, serviceConfig);
+ }
+ else
+ {
+ channel = GrpcChannel.ForAddress(settings.Endpoint, new() { ServiceConfig = serviceConfig });
+ }
_channel?.Dispose();
_channel = channel;
diff --git a/src/Plugins/SignClient/SignClient.csproj b/src/Plugins/SignClient/SignClient.csproj
index 9553edc27d..843d5fc869 100644
--- a/src/Plugins/SignClient/SignClient.csproj
+++ b/src/Plugins/SignClient/SignClient.csproj
@@ -25,6 +25,7 @@
+
diff --git a/src/Plugins/SignClient/SignClient.json b/src/Plugins/SignClient/SignClient.json
index dd9be7eb3e..7ff39caa8f 100644
--- a/src/Plugins/SignClient/SignClient.json
+++ b/src/Plugins/SignClient/SignClient.json
@@ -1,6 +1,6 @@
{
"PluginConfiguration": {
"Name": "SignClient",
- "Endpoint": "http://127.0.0.1:9991"
+ "Endpoint": "http://127.0.0.1:9991" // tcp: "http://host:port", vsock: "vsock://contextId:port"
}
-}
\ No newline at end of file
+}
diff --git a/src/Plugins/SignClient/Vsock.cs b/src/Plugins/SignClient/Vsock.cs
new file mode 100644
index 0000000000..15bba0abf6
--- /dev/null
+++ b/src/Plugins/SignClient/Vsock.cs
@@ -0,0 +1,83 @@
+// Copyright (C) 2015-2025 The Neo Project.
+//
+// Vsock.cs file belongs to the neo project and is free
+// software distributed under the MIT software license, see the
+// accompanying file LICENSE in the main directory of the
+// repository or http://www.opensource.org/licenses/mit-license.php
+// for more details.
+//
+// Redistribution and use in source and binary forms with or without
+// modifications are permitted.
+
+using Grpc.Net.Client;
+using Grpc.Net.Client.Configuration;
+using Ookii.VmSockets;
+using System.Net.Sockets;
+
+namespace Neo.Plugins.SignClient
+{
+ ///
+ /// The address of the vsock address.
+ ///
+ public record VsockAddress(int ContextId, int Port);
+
+ ///
+ /// Grpc adapter for VSock. Only supported on Linux.
+ /// This is for the SignClient plugin to connect to the AWS Nitro Enclave.
+ ///
+ public class Vsock
+ {
+ private readonly VSockEndPoint _endpoint;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The vsock address.
+ public Vsock(VsockAddress address)
+ {
+ if (!OperatingSystem.IsLinux()) throw new PlatformNotSupportedException("Vsock is only supported on Linux.");
+
+ _endpoint = new VSockEndPoint(address.ContextId, address.Port);
+ }
+
+ internal async ValueTask ConnectAsync(SocketsHttpConnectionContext context, CancellationToken cancellation)
+ {
+ if (!OperatingSystem.IsLinux()) throw new PlatformNotSupportedException("Vsock is only supported on Linux.");
+
+ var socket = VSock.Create(SocketType.Stream);
+ try
+ {
+ // Have to use `Task.Run` with `Connect` to avoid some compatibility issues(if use ConnectAsync).
+ await Task.Run(() => socket.Connect(_endpoint), cancellation);
+ return new NetworkStream(socket, true);
+ }
+ catch
+ {
+ socket.Dispose();
+ throw;
+ }
+ }
+
+ ///
+ /// Creates a Grpc channel for the vsock endpoint.
+ ///
+ /// The vsock address.
+ /// The Grpc service config.
+ /// The Grpc channel.
+ public static GrpcChannel CreateChannel(VsockAddress address, ServiceConfig serviceConfig)
+ {
+ var vsock = new Vsock(address);
+ var socketsHttpHandler = new SocketsHttpHandler
+ {
+ ConnectCallback = vsock.ConnectAsync,
+ };
+
+ var addressPlaceholder = $"http://127.0.0.1:{address.Port}"; // just a placeholder
+ return GrpcChannel.ForAddress(addressPlaceholder, new GrpcChannelOptions
+ {
+ HttpHandler = socketsHttpHandler,
+ ServiceConfig = serviceConfig,
+ });
+ }
+ }
+}
diff --git a/tests/Neo.Plugins.SignClient.Tests/UT_SignClient.cs b/tests/Neo.Plugins.SignClient.Tests/UT_SignClient.cs
index 044be50570..1709ea4e0f 100644
--- a/tests/Neo.Plugins.SignClient.Tests/UT_SignClient.cs
+++ b/tests/Neo.Plugins.SignClient.Tests/UT_SignClient.cs
@@ -44,7 +44,11 @@ public class UT_SignClient
private static SignClient NewClient(Block? block, ExtensiblePayload? payload)
{
- // for test sign service, set SIGN_SERVICE_ENDPOINT env
+ // When test sepcific endpoint, set SIGN_SERVICE_ENDPOINT
+ // For example:
+ // export SIGN_SERVICE_ENDPOINT=http://127.0.0.1:9991
+ // or
+ // export SIGN_SERVICE_ENDPOINT=vsock://2345:9991
var endpoint = Environment.GetEnvironmentVariable("SIGN_SERVICE_ENDPOINT");
if (endpoint is not null)
{
@@ -52,7 +56,7 @@ private static SignClient NewClient(Block? block, ExtensiblePayload? payload)
.AddInMemoryCollection(new Dictionary
{
[Settings.SectionName + ":Name"] = "SignClient",
- [Settings.SectionName + ":Endpoint"] = endpoint
+ [Settings.SectionName + ":Endpoint"] = endpoint,
})
.Build()
.GetSection(Settings.SectionName);
diff --git a/tests/Neo.Plugins.SignClient.Tests/UT_Vsock.cs b/tests/Neo.Plugins.SignClient.Tests/UT_Vsock.cs
new file mode 100644
index 0000000000..18828b69f0
--- /dev/null
+++ b/tests/Neo.Plugins.SignClient.Tests/UT_Vsock.cs
@@ -0,0 +1,67 @@
+// Copyright (C) 2015-2025 The Neo Project.
+//
+// UT_Vsock.cs file belongs to the neo project and is free
+// software distributed under the MIT software license, see the
+// accompanying file LICENSE in the main directory of the
+// repository or http://www.opensource.org/licenses/mit-license.php
+// for more details.
+//
+// Redistribution and use in source and binary forms with or without
+// modifications are permitted.
+
+using Microsoft.Extensions.Configuration;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Neo.Plugins.SignClient.Tests
+{
+ [TestClass]
+ public class UT_Vsock
+ {
+ [TestMethod]
+ public void TestGetVsockAddress()
+ {
+ var address = new VsockAddress(1, 9991);
+ var section = new ConfigurationBuilder()
+ .AddInMemoryCollection(new Dictionary
+ {
+ ["PluginConfiguration:Endpoint"] = $"vsock://{address.ContextId}:{address.Port}"
+ })
+ .Build()
+ .GetSection("PluginConfiguration");
+
+ var settings = new Settings(section);
+ Assert.AreEqual(address, settings.GetVsockAddress());
+
+ section = new ConfigurationBuilder()
+ .AddInMemoryCollection(new Dictionary
+ {
+ ["PluginConfiguration:Endpoint"] = "http://127.0.0.1:9991",
+ })
+ .Build()
+ .GetSection("PluginConfiguration");
+ Assert.IsNull(new Settings(section).GetVsockAddress());
+ }
+
+ [TestMethod]
+ public void TestInvalidEndpoint()
+ {
+ var section = new ConfigurationBuilder()
+ .AddInMemoryCollection(new Dictionary
+ {
+ ["PluginConfiguration:Endpoint"] = "vsock://127.0.0.1:9991"
+ })
+ .Build()
+ .GetSection("PluginConfiguration");
+ Assert.ThrowsExactly(() => _ = new Settings(section).GetVsockAddress());
+
+ section = new ConfigurationBuilder()
+ .AddInMemoryCollection(new Dictionary
+ {
+ ["PluginConfiguration:Endpoint"] = "vsock://127.0.0.1:xyz"
+ })
+ .Build()
+ .GetSection("PluginConfiguration");
+ Assert.ThrowsExactly(() => _ = new Settings(section).GetVsockAddress());
+ }
+ }
+}