-
Couldn't load subscription status.
- Fork 1k
Add: SignClient Vsock support #4002
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
2fd55c8
9690740
bece34e
e2ecab1
6480ed4
d573f0d
5f04b63
0409005
566236d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,22 +18,39 @@ public class Settings : PluginSettings | |
| public const string SectionName = "PluginConfiguration"; | ||
| private const string DefaultEndpoint = "http://127.0.0.1:9991"; | ||
|
|
||
| internal const string EndpointTcp = "tcp"; | ||
| internal const string EndpointVsock = "vsock"; | ||
|
|
||
| /// <summary> | ||
| /// The name of the sign client(i.e. Signer). | ||
| /// </summary> | ||
| public readonly string Name; | ||
|
|
||
| /// <summary> | ||
| /// The type of the endpoint. Default is "tcp", and "tcp" and "vsock" are supported now. | ||
| /// If the type is "vsock", the "Endpoint" should be "http://contextId:port" or "https://contextId:port". | ||
| /// </summary> | ||
| public readonly string EndpointType; | ||
|
|
||
| /// <summary> | ||
| /// The host of the sign client(i.e. Signer). | ||
| /// </summary> | ||
| public readonly string Endpoint; | ||
|
|
||
| /// <summary> | ||
| /// Create a new settings instance from the configuration section. | ||
| /// </summary> | ||
| /// <param name="section">The configuration section.</param> | ||
| /// <exception cref="FormatException">If the endpoint type or endpoint is invalid.</exception> | ||
| public Settings(IConfigurationSection section) : base(section) | ||
| { | ||
| Name = section.GetValue("Name", "SignClient"); | ||
| EndpointType = section.GetValue("EndpointType", EndpointTcp); | ||
| if (EndpointType != EndpointTcp && EndpointType != EndpointVsock) | ||
| throw new FormatException($"Invalid endpoint type: {EndpointType}"); | ||
|
|
||
| // 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 +68,36 @@ public static Settings Default | |
| return new Settings(section); | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Get the vsock address from the endpoint. | ||
| /// </summary> | ||
| /// <returns>The vsock address. If the endpoint type is not vsock, return null.</returns> | ||
| /// <exception cref="FormatException">If the endpoint is invalid.</exception> | ||
| internal VsockAddress? GetVsockAddress() | ||
| { | ||
| if (EndpointType != EndpointVsock) return null; | ||
|
|
||
| var endpoint = Endpoint; | ||
| if (Endpoint.StartsWith("http://")) | ||
|
||
| { | ||
| endpoint = endpoint.Substring("http://".Length); | ||
| } | ||
| else if (Endpoint.StartsWith("https://")) | ||
| { | ||
| endpoint = endpoint.Substring("https://".Length); | ||
| } | ||
|
|
||
| var parts = endpoint.Split(':', 2); | ||
|
||
| if (parts.Length != 2) throw new FormatException($"Invalid vsock endpoint: {Endpoint}"); | ||
| try | ||
| { | ||
| return new VsockAddress(int.Parse(parts[0]), int.Parse(parts[1])); | ||
| } | ||
| catch | ||
| { | ||
| throw new FormatException($"Invalid vsock endpoint: {Endpoint}"); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| { | ||
| "PluginConfiguration": { | ||
| "Name": "SignClient", | ||
| "Endpoint": "http://127.0.0.1:9991" | ||
| "EndpointType": "tcp", // "tcp" or "vsock" | ||
| "Endpoint": "http://127.0.0.1:9991" // tcp: "http://127.0.0.1:port", vsock: "http://contextId:port" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| { | ||
| /// <summary> | ||
| /// The address of the vsock address. | ||
| /// </summary> | ||
| public record VsockAddress(int ContextId, int Port); | ||
|
|
||
| /// <summary> | ||
| /// Grpc adapter for VSock. Only supported on Linux. | ||
| /// This is for the SignClient plugin to connect to the AWS Nitro Enclave. | ||
| /// </summary> | ||
| public class Vsock | ||
| { | ||
| private readonly VSockEndPoint _endpoint; | ||
|
|
||
| /// <summary> | ||
| /// Initializes a new instance of the <see cref="Vsock"/> class. | ||
| /// </summary> | ||
| /// <param name="address">The vsock address.</param> | ||
| 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<Stream> ConnectAsync(SocketsHttpConnectionContext context, CancellationToken cancellationToken) | ||
| { | ||
| 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), cancellationToken); | ||
| return new NetworkStream(socket, true); | ||
| } | ||
| catch | ||
| { | ||
| socket.Dispose(); | ||
| throw; | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Creates a Grpc channel for the vsock endpoint. | ||
| /// </summary> | ||
| /// <param name="address">The vsock address.</param> | ||
| /// <param name="serviceConfig">The Grpc service config.</param> | ||
| /// <returns>The Grpc channel.</returns> | ||
| 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, | ||
| }); | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -44,15 +44,23 @@ 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_TYPE and set SIGN_SERVICE_ENDPOINT | ||
| // For example: | ||
| // export SIGN_SERVICE_ENDPOINT=http://127.0.0.1:9991 | ||
| // export SIGN_SERVICE_ENDPOINT_TYPE=tcp | ||
| // or | ||
| // export SIGN_SERVICE_ENDPOINT=http://2345:9991 | ||
| // export SIGN_SERVICE_ENDPOINT_TYPE=vsock | ||
| var endpoint = Environment.GetEnvironmentVariable("SIGN_SERVICE_ENDPOINT"); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we make this environment variable have a prefix of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's trivial, I think. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Isnt this plugin related to |
||
| var endpointType = Environment.GetEnvironmentVariable("SIGN_SERVICE_ENDPOINT_TYPE"); | ||
| if (endpoint is not null) | ||
| { | ||
| var section = new ConfigurationBuilder() | ||
| .AddInMemoryCollection(new Dictionary<string, string?> | ||
| { | ||
| [Settings.SectionName + ":Name"] = "SignClient", | ||
| [Settings.SectionName + ":Endpoint"] = endpoint | ||
| [Settings.SectionName + ":Endpoint"] = endpoint, | ||
| [Settings.SectionName + ":EndpointType"] = endpointType ?? Settings.EndpointTcp | ||
| }) | ||
| .Build() | ||
| .GetSection(Settings.SectionName); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| // 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<string, string?> | ||
| { | ||
| ["PluginConfiguration:EndpointType"] = Settings.EndpointVsock, | ||
| ["PluginConfiguration:Endpoint"] = $"http://{address.ContextId}:{address.Port}" | ||
| }) | ||
| .Build() | ||
| .GetSection("PluginConfiguration"); | ||
|
|
||
| var settings = new Settings(section); | ||
| Assert.AreEqual(address, settings.GetVsockAddress()); | ||
|
|
||
| section = new ConfigurationBuilder() | ||
| .AddInMemoryCollection(new Dictionary<string, string?> | ||
| { | ||
| ["PluginConfiguration:Endpoint"] = "http://127.0.0.1:9991", | ||
| ["PluginConfiguration:EndpointType"] = Settings.EndpointTcp | ||
| }) | ||
| .Build() | ||
| .GetSection("PluginConfiguration"); | ||
| Assert.IsNull(new Settings(section).GetVsockAddress()); | ||
| } | ||
|
|
||
| [TestMethod] | ||
| public void TestInvalidEndpoint() | ||
| { | ||
| var section = new ConfigurationBuilder() | ||
| .AddInMemoryCollection(new Dictionary<string, string?> | ||
| { | ||
| ["PluginConfiguration:EndpointType"] = Settings.EndpointVsock, | ||
| ["PluginConfiguration:Endpoint"] = "http://127.0.0.1:9991" | ||
| }) | ||
| .Build() | ||
| .GetSection("PluginConfiguration"); | ||
| Assert.ThrowsExactly<FormatException>(() => _ = new Settings(section).GetVsockAddress()); | ||
|
|
||
| section = new ConfigurationBuilder() | ||
| .AddInMemoryCollection(new Dictionary<string, string?> | ||
| { | ||
| ["PluginConfiguration:EndpointType"] = "xyz", | ||
| ["PluginConfiguration:Endpoint"] = "http://127.0.0.1:9991" | ||
| }) | ||
| .Build() | ||
| .GetSection("PluginConfiguration"); | ||
| Assert.ThrowsExactly<FormatException>(() => _ = new Settings(section).GetVsockAddress()); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.