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()); + } + } +}