server-monitor-manager/tests/ServerMonitorManager.Control.Tests/ProvisioningHelperTests.cs
ochenstarik-ui ba14d29211
fix(security): close desktop and helper debts (#10)
Co-authored-by: Ochenstarik <ochenstarik@inbox.ru>
2026-07-31 13:22:00 +07:00

964 lines
39 KiB
C#

using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Net.Sockets;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.Json;
using ServerMonitorManager.Agent;
using ServerMonitorManager.Core;
using ServerMonitorManager.Provisioning.Helper;
using Xunit;
namespace ServerMonitorManager.Control.Tests;
public sealed class ProvisioningHelperTests
{
[Fact]
public async Task ConfiguredAgentUidMustMatchTheInstalledAgentUser()
{
var path = Path.Combine(Path.GetTempPath(), $"smm-passwd-{Guid.NewGuid():N}");
await File.WriteAllTextAsync(
path,
"root:x:0:0:root:/root:/bin/bash\n"
+ "ochenstarik-smm-agent:x:1234:1234::/var/lib/ochenstarik-server-monitor-manager:/usr/sbin/nologin\n",
TestContext.Current.CancellationToken);
try
{
Assert.True(ProvisioningAgentIdentity.MatchesConfiguredUid(path, 1234));
Assert.False(ProvisioningAgentIdentity.MatchesConfiguredUid(path, 1235));
}
finally
{
File.Delete(path);
}
}
[Fact]
public void HelperRejectsEveryActionOutsideFixedAllowlist()
{
using var document = JsonDocument.Parse("{}");
var response = ProvisioningHelperServer.Execute(new ProvisioningHelperRequest(
"1", new string('a', 32), "shell", 1,
ProvisioningActionCatalog.PreflightModuleHash, document.RootElement.Clone()));
Assert.False(response.Success);
Assert.Equal("action.denied", response.Code);
Assert.Null(response.Preflight);
}
[Fact]
public void HelperAcceptsOnlyEmptyPreflightSchemaOne()
{
if (!OperatingSystem.IsLinux())
{
return;
}
using var document = JsonDocument.Parse("{}");
var response = ProvisioningHelperServer.Execute(new ProvisioningHelperRequest(
"1", new string('b', 32), "preflight", 1,
ProvisioningActionCatalog.PreflightModuleHash, document.RootElement.Clone()));
Assert.True(response.Success);
Assert.Equal("preflight.completed", response.Code);
Assert.NotNull(response.Preflight);
Assert.NotEmpty(response.Preflight.OperatingSystem);
Assert.NotEmpty(response.Preflight.Architecture);
}
[Fact]
public void HelperContractRejectsUnknownJsonMembers()
{
const string json = """
{"protocolVersion":"1","jobId":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"actionType":"preflight","schemaVersion":1,
"moduleHash":"2dc48fb4528a291221954fc2dd3478d431b66fe34228f29684ce1648dbe2f32b",
"parameters":{},"command":"id"}
""";
Assert.Throws<JsonException>(() =>
JsonSerializer.Deserialize(json, SmmJsonContext.Default.ProvisioningHelperRequest));
}
[Fact]
public void HelperRejectsMissingJobIdWithoutThrowing()
{
using var document = JsonDocument.Parse("{}");
var response = ProvisioningHelperServer.Execute(new ProvisioningHelperRequest(
"1", null!, "preflight", 1,
ProvisioningActionCatalog.PreflightModuleHash, document.RootElement.Clone()));
Assert.False(response.Success);
Assert.Equal("request.invalid-job", response.Code);
}
[Fact]
public void BaseInstallSchemaRejectsCommandText()
{
const string json = """
{"timezone":"UTC","locale":"en_US.UTF-8","aptUpdate":true,"aptUpgrade":false,
"packageCatalogVersion":1,"packageGroupIds":["core"],"swapMode":"disabled",
"swapSizeMiB":null,"vmSwappiness":60,"enableUnattendedUpgrades":true,
"rebootPolicy":"never","command":"id"}
""";
Assert.Throws<JsonException>(() =>
JsonSerializer.Deserialize(json, SmmJsonContext.Default.SystemBaseInstallParameters));
}
[Fact]
public void HelperBuildsDeterministicBaseInstallPlanWithoutCommands()
{
var parameters = new SystemBaseInstallParameters(
"UTC", "en_US.UTF-8", true, false, 1,
["development", "core"], "disabled", null, 60, true, "never");
var json = JsonSerializer.SerializeToElement(
parameters, SmmJsonContext.Default.SystemBaseInstallParameters);
var response = ProvisioningHelperServer.Execute(new ProvisioningHelperRequest(
"1", new string('c', 32), "system.base-install", 1,
ProvisioningActionCatalog.SystemBaseInstallModuleHash, json));
Assert.True(response.Success);
Assert.Equal("system.base-install.plan-ready", response.Code);
Assert.Null(response.Preflight);
Assert.Equal(
["ca-certificates", "curl", "jq", "build-essential", "git"],
response.BaseInstallPlan!.Packages);
Assert.Equal("never", response.BaseInstallPlan.RebootPolicy);
}
[Fact]
public async Task ConfirmedTimezoneOnlyPlanUsesAllowlistedBinaryCreatesBackupAndVerifies()
{
using var authority = CreateAuthority(out var signingKey);
using (signingKey)
{
var plan = CreateTimezoneOnlyPlan("Europe/Berlin");
var grant = SignGrant(authority, signingKey, plan);
var events = new List<string>();
var files = new FakeFileSystem(events);
files.Files.Add("/usr/share/zoneinfo/Europe/Berlin");
var process = new FakeProcessRunner(events,
new(0, "UTC\n", ""),
new(0, "", ""),
new(0, "Europe/Berlin\n", ""));
var executor = new TimezoneProvisioningExecutor(
authority, "home", files, process, TimeProvider.System, "/var/lib/ochenstarik-server-monitor-manager/provisioning/rollback");
var result = await executor.ExecuteAsync(
CreateExecutionRequest(plan, grant), TestContext.Current.CancellationToken);
Assert.True(result.Success);
Assert.True(result.Changed);
Assert.True(result.Verified);
Assert.False(result.RollbackAttempted);
Assert.Equal("Europe/Berlin", result.ObservedTimezone);
Assert.Equal(2, files.Writes.Count);
Assert.True(events.IndexOf("write-backup") < events.IndexOf("process:set-timezone Europe/Berlin"));
Assert.All(process.Calls, call => Assert.Equal("/usr/bin/timedatectl", call.FileName));
Assert.Equal(
[
"show --property=Timezone --value",
"set-timezone Europe/Berlin",
"show --property=Timezone --value"
],
process.Calls.Select(call => string.Join(' ', call.Arguments)));
}
}
[Theory]
[InlineData("null")]
[InlineData("[]")]
public async Task ExecutionDispatchRejectsNonObjectParametersBeforeMutation(string parametersJson)
{
using var authority = CreateAuthority(out var signingKey);
using (signingKey)
using (var parameters = JsonDocument.Parse(parametersJson))
{
var plan = CreateTimezoneOnlyPlan("Europe/Berlin");
var events = new List<string>();
var files = new FakeFileSystem(events);
files.Files.Add("/usr/share/zoneinfo/Europe/Berlin");
var process = new FakeProcessRunner(events,
new(0, "UTC\n", ""),
new(0, "", ""),
new(0, "Europe/Berlin\n", ""));
var executor = new TimezoneProvisioningExecutor(
authority, "home", files, process, TimeProvider.System,
"/var/lib/ochenstarik-server-monitor-manager/provisioning/rollback");
var server = new ProvisioningHelperServer("unused", 0, executor);
var request = CreateExecutionRequest(plan, SignGrant(authority, signingKey, plan)) with
{
Parameters = parameters.RootElement.Clone()
};
var dispatch = typeof(ProvisioningHelperServer).GetMethod(
"ExecuteRequestAsync",
BindingFlags.Instance | BindingFlags.NonPublic)!;
var task = (Task<ProvisioningHelperResponse>)dispatch.Invoke(
server,
[request, TestContext.Current.CancellationToken])!;
var response = await task;
Assert.False(response.Success);
Assert.Equal("action.denied", response.Code);
Assert.Empty(files.Writes);
Assert.Empty(process.Calls);
}
}
[Fact]
public async Task ValidNoOpTimezoneCompletesOnlyAfterFactualVerification()
{
using var authority = CreateAuthority(out var signingKey);
using (signingKey)
{
var plan = CreateTimezoneOnlyPlan("UTC");
var process = new FakeProcessRunner(
[], new ProvisioningProcessResult(0, "UTC\n", ""));
var files = new FakeFileSystem([]);
files.Files.Add("/usr/share/zoneinfo/UTC");
var executor = new TimezoneProvisioningExecutor(
authority, "home", files, process, TimeProvider.System, "/var/lib/ochenstarik-server-monitor-manager/provisioning/rollback");
var result = await executor.ExecuteAsync(
CreateExecutionRequest(plan, SignGrant(authority, signingKey, plan)),
TestContext.Current.CancellationToken);
Assert.True(result.Success);
Assert.False(result.Changed);
Assert.True(result.Verified);
Assert.Single(files.Writes);
Assert.Single(process.Calls);
}
}
[Fact]
public async Task ConsumedGrantCannotBeReplayedEvenAfterVerifiedNoOp()
{
using var authority = CreateAuthority(out var signingKey);
using (signingKey)
{
var plan = CreateTimezoneOnlyPlan("UTC");
var grant = SignGrant(authority, signingKey, plan);
var files = new FakeFileSystem([]);
files.Files.Add("/usr/share/zoneinfo/UTC");
var firstProcess = new FakeProcessRunner(
[], new ProvisioningProcessResult(0, "UTC\n", ""));
var executor = new TimezoneProvisioningExecutor(
authority, "home", files, firstProcess, TimeProvider.System,
"/var/lib/ochenstarik-server-monitor-manager/provisioning/rollback");
var first = await executor.ExecuteAsync(
CreateExecutionRequest(plan, grant), TestContext.Current.CancellationToken);
var secondProcess = new FakeProcessRunner([]);
var replayExecutor = new TimezoneProvisioningExecutor(
authority, "home", files, secondProcess, TimeProvider.System,
"/var/lib/ochenstarik-server-monitor-manager/provisioning/rollback");
var replay = await replayExecutor.ExecuteAsync(
CreateExecutionRequest(plan, grant), TestContext.Current.CancellationToken);
Assert.True(first.Success);
Assert.False(replay.Success);
Assert.Equal("execution.grant-consumed", replay.Code);
Assert.Empty(secondProcess.Calls);
}
}
[Fact]
public async Task ForgedMismatchedAndExpiredGrantsCauseZeroMutation()
{
using var authority = CreateAuthority(out var signingKey);
using (signingKey)
{
var plan = CreateTimezoneOnlyPlan("Europe/Berlin");
var valid = SignGrant(authority, signingKey, plan);
var invalid = new[]
{
valid with { Signature = valid.Signature[..^1] + (valid.Signature[^1] == 'A' ? "B" : "A") },
valid with { JobId = new string('f', 32) },
valid with { NodeId = "other-node" },
valid with { PlanSha256 = new string('0', 64) },
SignGrant(authority, signingKey, plan, DateTimeOffset.UtcNow.AddMinutes(-10))
};
foreach (var grant in invalid)
{
var files = new FakeFileSystem([]);
files.Files.Add("/usr/share/zoneinfo/Europe/Berlin");
var process = new FakeProcessRunner([]);
var executor = new TimezoneProvisioningExecutor(
authority, "home", files, process, TimeProvider.System, "/var/lib/ochenstarik-server-monitor-manager/provisioning/rollback");
var result = await executor.ExecuteAsync(
CreateExecutionRequest(plan, grant), TestContext.Current.CancellationToken);
Assert.False(result.Success);
Assert.Equal("execution.authorization-denied", result.Code);
Assert.Empty(files.Writes);
Assert.Empty(process.Calls);
}
}
}
[Fact]
public async Task ValidGrantForAnotherNodeIsRejectedBeforeMutation()
{
using var authority = CreateAuthority(out var signingKey);
using (signingKey)
{
var plan = CreateTimezoneOnlyPlan("Europe/Berlin");
var otherNodeGrant = SignGrant(authority, signingKey, plan, nodeId: "other-node");
var localGrant = SignGrant(authority, signingKey, plan);
var requests = new[]
{
CreateExecutionRequest(plan, otherNodeGrant) with
{
Execution = new ProvisioningBaseInstallExecutionAuthorization(
"other-node", plan, otherNodeGrant)
},
CreateExecutionRequest(plan, localGrant) with
{
Execution = new ProvisioningBaseInstallExecutionAuthorization(
"other-node", plan, localGrant)
}
};
foreach (var request in requests)
{
var files = new FakeFileSystem([]);
files.Files.Add("/usr/share/zoneinfo/Europe/Berlin");
var process = new FakeProcessRunner([]);
var executor = new TimezoneProvisioningExecutor(
authority, "home", files, process, TimeProvider.System,
"/var/lib/ochenstarik-server-monitor-manager/provisioning/rollback");
var result = await executor.ExecuteAsync(
request, TestContext.Current.CancellationToken);
Assert.False(result.Success);
Assert.Equal("execution.authorization-denied", result.Code);
Assert.Empty(files.Writes);
Assert.Empty(process.Calls);
}
}
}
[Fact]
public async Task UnsupportedBaseInstallFieldsCauseZeroMutation()
{
using var authority = CreateAuthority(out var signingKey);
using (signingKey)
{
var baseline = CreateTimezoneOnlyPlan("Europe/Berlin");
var unsupportedPlans = new[]
{
baseline with { Locale = "en_US.UTF-8" },
baseline with { AptUpdate = true },
baseline with { AptUpgrade = true },
baseline with { Packages = ["curl"] },
baseline with { SwapMode = "automatic" },
baseline with { VmSwappiness = 10 },
baseline with { EnableUnattendedUpgrades = true },
baseline with { RebootPolicy = "always" }
};
foreach (var plan in unsupportedPlans)
{
var files = new FakeFileSystem([]);
files.Files.Add("/usr/share/zoneinfo/Europe/Berlin");
var process = new FakeProcessRunner([]);
var executor = new TimezoneProvisioningExecutor(
authority, "home", files, process, TimeProvider.System, "/var/lib/ochenstarik-server-monitor-manager/provisioning/rollback");
var result = await executor.ExecuteAsync(
CreateExecutionRequest(plan, SignGrant(authority, signingKey, plan)),
TestContext.Current.CancellationToken);
Assert.False(result.Success);
Assert.Equal("system.base-install.unsupported-fields", result.Code);
Assert.Empty(files.Writes);
Assert.Empty(process.Calls);
}
}
}
[Fact]
public void AgentExecutionRequestUsesTheControlAuthorizedPlanAndGrant()
{
using var authority = CreateAuthority(out var signingKey);
using (signingKey)
using (var parameters = JsonDocument.Parse("{}"))
{
var plan = CreateTimezoneOnlyPlan("Europe/Berlin");
var grant = SignGrant(authority, signingKey, plan);
var authorization = new ProvisioningBaseInstallExecutionAuthorization("home", plan, grant);
var job = new ProvisioningJob(
new string('d', 32), "home", "system.base-install", 1,
parameters.RootElement.Clone(), ProvisioningJobStates.Running, true,
"Change timezone", "operator", DateTimeOffset.UtcNow.AddMinutes(-1),
DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddMinutes(30),
DateTimeOffset.UtcNow, null, 3, 40, "execute", null);
var request = ProvisioningHelperClient.CreateBaseInstallExecutionRequest(job, authorization);
Assert.Same(authorization, request.Execution);
Assert.Equal(job.Id, request.JobId);
Assert.Equal(job.ActionType, request.ActionType);
Assert.Equal(ProvisioningActionCatalog.SystemBaseInstallModuleHash, request.ModuleHash);
Assert.Equal(JsonValueKind.Object, request.Parameters.ValueKind);
}
}
[Fact]
public void AgentRejectsInconsistentOrUnverifiedHelperSuccess()
{
using var authority = CreateAuthority(out var signingKey);
using (signingKey)
{
var plan = CreateTimezoneOnlyPlan("UTC");
var authorization = new ProvisioningBaseInstallExecutionAuthorization(
"home", plan, SignGrant(authority, signingKey, plan));
var unverified = new ProvisioningBaseInstallExecutionResult(
true, "bad", "bad", false, false, false, false, "UTC");
var contradictory = new ProvisioningHelperResponse(
false, "bad", "bad", null, null,
unverified with { Verified = true });
Assert.Throws<InvalidDataException>(() =>
ProvisioningHelperClient.ValidateBaseInstallExecutionResponse(
new ProvisioningHelperResponse(true, "bad", "bad", null, null, unverified),
authorization));
Assert.Throws<InvalidDataException>(() =>
ProvisioningHelperClient.ValidateBaseInstallExecutionResponse(
contradictory, authorization));
}
}
[Fact]
public async Task SymlinkedBackupPathIsRejectedBeforeTimezoneMutation()
{
using var authority = CreateAuthority(out var signingKey);
using (signingKey)
{
var plan = CreateTimezoneOnlyPlan("Europe/Berlin");
var files = new FakeFileSystem([]);
files.Files.Add("/usr/share/zoneinfo/Europe/Berlin");
files.SymbolicLinks.Add("/var/lib/ochenstarik-server-monitor-manager/provisioning");
var process = new FakeProcessRunner(
[], new ProvisioningProcessResult(0, "UTC\n", ""));
var executor = new TimezoneProvisioningExecutor(
authority, "home", files, process, TimeProvider.System, "/var/lib/ochenstarik-server-monitor-manager/provisioning/rollback");
var result = await executor.ExecuteAsync(
CreateExecutionRequest(plan, SignGrant(authority, signingKey, plan)),
TestContext.Current.CancellationToken);
Assert.False(result.Success);
Assert.Equal("backup.unsafe-path", result.Code);
Assert.Empty(files.Writes);
Assert.DoesNotContain(process.Calls,
call => call.Arguments.Count > 0 && call.Arguments[0] == "set-timezone");
}
}
[Fact]
public async Task VerificationFailureAttemptsAndFactuallyVerifiesRollback()
{
using var authority = CreateAuthority(out var signingKey);
using (signingKey)
{
var plan = CreateTimezoneOnlyPlan("Europe/Berlin");
var events = new List<string>();
var files = new FakeFileSystem(events);
files.Files.Add("/usr/share/zoneinfo/Europe/Berlin");
var process = new FakeProcessRunner(events,
new(0, "UTC\n", ""),
new(0, "", ""),
new(0, "Etc/Unknown\n", ""),
new(0, "", ""),
new(0, "UTC\n", ""));
var executor = new TimezoneProvisioningExecutor(
authority, "home", files, process, TimeProvider.System, "/var/lib/ochenstarik-server-monitor-manager/provisioning/rollback");
var result = await executor.ExecuteAsync(
CreateExecutionRequest(plan, SignGrant(authority, signingKey, plan)),
TestContext.Current.CancellationToken);
Assert.False(result.Success);
Assert.Equal("timezone.verification-failed", result.Code);
Assert.True(result.RollbackAttempted);
Assert.True(result.RollbackSucceeded);
Assert.Equal(
["set-timezone Europe/Berlin", "set-timezone UTC"],
process.Calls
.Where(call => call.Arguments.Count > 0 && call.Arguments[0] == "set-timezone")
.Select(call => string.Join(' ', call.Arguments)));
}
}
[Fact]
public async Task SilentClientDoesNotBlockASecondValidRequest()
{
if (!OperatingSystem.IsLinux())
{
return;
}
var socketPath = Path.Combine(Path.GetTempPath(), $"smm-{Guid.NewGuid():N}.sock");
using var shutdown = CancellationTokenSource.CreateLinkedTokenSource(
TestContext.Current.CancellationToken);
shutdown.CancelAfter(TimeSpan.FromSeconds(5));
var server = new ProvisioningHelperServer(
socketPath,
expectedPeerUserId: GetEffectiveUserId(),
connectionTimeout: TimeSpan.FromSeconds(1));
var serverTask = server.RunAsync(shutdown.Token);
using var silentClient = new Socket(
AddressFamily.Unix,
SocketType.Stream,
ProtocolType.Unspecified);
try
{
await WaitForSocketAsync(socketPath, shutdown.Token);
await silentClient.ConnectAsync(
new UnixDomainSocketEndPoint(socketPath),
shutdown.Token);
var response = await SendPreflightAsync(socketPath, shutdown.Token);
Assert.True(response.Success);
Assert.Equal("preflight.completed", response.Code);
}
finally
{
shutdown.Cancel();
silentClient.Dispose();
try
{
await serverTask;
}
catch (OperationCanceledException)
{
}
}
Assert.False(File.Exists(socketPath));
}
[Fact]
public async Task SynchronousExecutionCannotBlockAcceptLoopAndIsCancelledOnShutdown()
{
if (!OperatingSystem.IsLinux())
{
return;
}
using var authority = CreateAuthority(out var signingKey);
using (signingKey)
using (var shutdown = CancellationTokenSource.CreateLinkedTokenSource(
TestContext.Current.CancellationToken))
{
shutdown.CancelAfter(TimeSpan.FromSeconds(5));
var plan = CreateTimezoneOnlyPlan("Europe/Berlin");
var files = new FakeFileSystem([]);
files.Files.Add("/usr/share/zoneinfo/Europe/Berlin");
var process = new SynchronouslyBlockingProcessRunner();
var executor = new TimezoneProvisioningExecutor(
authority,
"home",
files,
process,
TimeProvider.System,
"/var/lib/ochenstarik-server-monitor-manager/provisioning/rollback");
var socketPath = Path.Combine(Path.GetTempPath(), $"smm-{Guid.NewGuid():N}.sock");
var server = new ProvisioningHelperServer(
socketPath,
GetEffectiveUserId(),
executor,
connectionTimeout: TimeSpan.FromSeconds(5));
var serverTask = server.RunAsync(shutdown.Token);
Task<string?>? executionTask = null;
try
{
await WaitForSocketAsync(socketPath, shutdown.Token);
executionTask = SendRequestPayloadAsync(
socketPath,
CreateExecutionRequest(plan, SignGrant(authority, signingKey, plan)),
shutdown.Token);
Assert.True(process.Started.Wait(
TimeSpan.FromSeconds(2),
TestContext.Current.CancellationToken));
using var secondRequest = CancellationTokenSource.CreateLinkedTokenSource(
TestContext.Current.CancellationToken);
secondRequest.CancelAfter(TimeSpan.FromSeconds(1));
var response = await SendPreflightAsync(socketPath, secondRequest.Token);
Assert.True(response.Success);
Assert.Equal("preflight.completed", response.Code);
}
finally
{
shutdown.Cancel();
await serverTask;
if (executionTask is not null)
{
try
{
await executionTask;
}
catch (OperationCanceledException) when (shutdown.IsCancellationRequested)
{
}
}
}
Assert.True(process.CancellationObserved);
Assert.True(process.RecoveryObserved);
Assert.False(File.Exists(socketPath));
}
}
[Fact]
public async Task UnexpectedPeerUserIdIsRejected()
{
if (!OperatingSystem.IsLinux())
{
return;
}
var socketPath = Path.Combine(Path.GetTempPath(), $"smm-{Guid.NewGuid():N}.sock");
using var shutdown = CancellationTokenSource.CreateLinkedTokenSource(
TestContext.Current.CancellationToken);
shutdown.CancelAfter(TimeSpan.FromSeconds(5));
var currentUserId = GetEffectiveUserId();
var expectedUserId = currentUserId == uint.MaxValue
? uint.MaxValue - 1
: currentUserId + 1;
var server = new ProvisioningHelperServer(socketPath, expectedUserId);
var serverTask = server.RunAsync(shutdown.Token);
try
{
await WaitForSocketAsync(socketPath, shutdown.Token);
Assert.Null(await SendPreflightPayloadAsync(socketPath, shutdown.Token));
}
finally
{
shutdown.Cancel();
await serverTask;
}
}
[Fact]
public async Task PartialConnectionIsClosedAfterTimeout()
{
if (!OperatingSystem.IsLinux())
{
return;
}
var socketPath = Path.Combine(Path.GetTempPath(), $"smm-{Guid.NewGuid():N}.sock");
using var shutdown = CancellationTokenSource.CreateLinkedTokenSource(
TestContext.Current.CancellationToken);
shutdown.CancelAfter(TimeSpan.FromSeconds(5));
var server = new ProvisioningHelperServer(
socketPath,
GetEffectiveUserId(),
connectionTimeout: TimeSpan.FromMilliseconds(100));
var serverTask = server.RunAsync(shutdown.Token);
try
{
await WaitForSocketAsync(socketPath, shutdown.Token);
using var client = new Socket(
AddressFamily.Unix,
SocketType.Stream,
ProtocolType.Unspecified);
await client.ConnectAsync(new UnixDomainSocketEndPoint(socketPath), shutdown.Token);
await client.SendAsync(new byte[] { (byte)'{' }, shutdown.Token);
var buffer = new byte[1];
var count = await client.ReceiveAsync(buffer, shutdown.Token);
Assert.Equal(0, count);
}
finally
{
shutdown.Cancel();
await serverTask;
}
}
[Fact]
public async Task RequestRateLimitRejectsExcessConnections()
{
if (!OperatingSystem.IsLinux())
{
return;
}
var socketPath = Path.Combine(Path.GetTempPath(), $"smm-{Guid.NewGuid():N}.sock");
using var shutdown = CancellationTokenSource.CreateLinkedTokenSource(
TestContext.Current.CancellationToken);
shutdown.CancelAfter(TimeSpan.FromSeconds(5));
var server = new ProvisioningHelperServer(
socketPath,
GetEffectiveUserId(),
requestsPerMinute: 1);
var serverTask = server.RunAsync(shutdown.Token);
try
{
await WaitForSocketAsync(socketPath, shutdown.Token);
Assert.NotNull(await SendPreflightPayloadAsync(socketPath, shutdown.Token));
Assert.Null(await SendPreflightPayloadAsync(socketPath, shutdown.Token));
}
finally
{
shutdown.Cancel();
await serverTask;
}
}
[Fact]
public async Task RequestFramingIsBoundedAndListenerSurvivesInvalidInput()
{
if (!OperatingSystem.IsLinux())
{
return;
}
var socketPath = Path.Combine(Path.GetTempPath(), $"smm-{Guid.NewGuid():N}.sock");
using var shutdown = CancellationTokenSource.CreateLinkedTokenSource(
TestContext.Current.CancellationToken);
shutdown.CancelAfter(TimeSpan.FromSeconds(5));
var server = new ProvisioningHelperServer(socketPath, GetEffectiveUserId());
var serverTask = server.RunAsync(shutdown.Token);
try
{
await WaitForSocketAsync(socketPath, shutdown.Token);
var exactPayload = await SendPayloadAsync(
socketPath,
new string(' ', 16 * 1024) + "\n",
shutdown.Token);
var oversizedPayload = await SendPayloadAsync(
socketPath,
new string('x', 16 * 1024 + 1),
shutdown.Token);
var validResponse = await SendPreflightAsync(socketPath, shutdown.Token);
var oversizedResponse = JsonSerializer.Deserialize(
oversizedPayload!,
SmmJsonContext.Default.ProvisioningHelperResponse)!;
Assert.DoesNotContain("too large", exactPayload, StringComparison.OrdinalIgnoreCase);
Assert.Equal("request.invalid-size", oversizedResponse.Code);
Assert.Equal("Invalid helper request size.", oversizedResponse.Message);
Assert.True(validResponse.Success);
}
finally
{
shutdown.Cancel();
await serverTask;
}
}
private static async Task WaitForSocketAsync(string path, CancellationToken cancellationToken)
{
while (!File.Exists(path))
{
await Task.Delay(10, cancellationToken);
}
}
private static async Task<ProvisioningHelperResponse> SendPreflightAsync(
string socketPath,
CancellationToken cancellationToken)
{
var responsePayload = await SendPreflightPayloadAsync(socketPath, cancellationToken);
return JsonSerializer.Deserialize(
responsePayload!,
SmmJsonContext.Default.ProvisioningHelperResponse)!;
}
private static async Task<string?> SendPreflightPayloadAsync(
string socketPath,
CancellationToken cancellationToken)
{
using var document = JsonDocument.Parse("{}");
var request = new ProvisioningHelperRequest(
"1", new string('e', 32), "preflight", 1,
ProvisioningActionCatalog.PreflightModuleHash,
document.RootElement.Clone());
return await SendRequestPayloadAsync(socketPath, request, cancellationToken);
}
private static Task<string?> SendRequestPayloadAsync(
string socketPath,
ProvisioningHelperRequest request,
CancellationToken cancellationToken)
{
var payload = JsonSerializer.Serialize(
request,
SmmJsonContext.Default.ProvisioningHelperRequest) + "\n";
return SendPayloadAsync(socketPath, payload, cancellationToken);
}
private static async Task<string?> SendPayloadAsync(
string socketPath,
string payload,
CancellationToken cancellationToken)
{
using var socket = new Socket(
AddressFamily.Unix,
SocketType.Stream,
ProtocolType.Unspecified);
await socket.ConnectAsync(new UnixDomainSocketEndPoint(socketPath), cancellationToken);
try
{
await using var stream = new NetworkStream(socket, ownsSocket: false);
await stream.WriteAsync(Encoding.UTF8.GetBytes(payload), cancellationToken);
using var reader = new StreamReader(stream, Encoding.UTF8);
return await reader.ReadLineAsync(cancellationToken);
}
catch (IOException)
{
return null;
}
catch (SocketException)
{
return null;
}
}
[DllImport("libc")]
private static extern uint geteuid();
private static uint GetEffectiveUserId() => geteuid();
private static ProvisioningHelperRequest CreateExecutionRequest(
SystemBaseInstallPlan plan,
ProvisioningExecutionGrant grant)
{
using var document = JsonDocument.Parse("{}");
return new ProvisioningHelperRequest(
"1", new string('d', 32), "system.base-install", 1,
ProvisioningActionCatalog.SystemBaseInstallModuleHash,
document.RootElement.Clone(),
new ProvisioningBaseInstallExecutionAuthorization("home", plan, grant));
}
private static SystemBaseInstallPlan CreateTimezoneOnlyPlan(string timezone)
=> new(timezone, "unchanged", false, false, [], "unchanged", null, 60, false, "never", []);
private static X509Certificate2 CreateAuthority(out ECDsa key)
{
key = ECDsa.Create(ECCurve.NamedCurves.nistP256);
var request = new CertificateRequest("CN=SMM Test CA", key, HashAlgorithmName.SHA256);
request.CertificateExtensions.Add(new X509BasicConstraintsExtension(true, false, 0, true));
return request.CreateSelfSigned(
DateTimeOffset.UtcNow.AddMinutes(-1), DateTimeOffset.UtcNow.AddDays(1));
}
private static ProvisioningExecutionGrant SignGrant(
X509Certificate2 authority,
ECDsa key,
SystemBaseInstallPlan plan,
DateTimeOffset? issuedAt = null,
string nodeId = "home")
{
_ = authority;
var now = issuedAt ?? DateTimeOffset.UtcNow;
var grant = new ProvisioningExecutionGrant(
"1", new string('d', 32), nodeId, "system.base-install", 1,
ProvisioningExecutionGrantCodec.ComputePlanSha256(plan),
now.ToUnixTimeSeconds(), now.AddMinutes(2).ToUnixTimeSeconds(),
new string('e', 32), ProvisioningExecutionGrantCodec.SignatureAlgorithm, string.Empty);
var signature = key.SignData(
ProvisioningExecutionGrantCodec.CreateSigningPayload(grant),
HashAlgorithmName.SHA256,
DSASignatureFormat.IeeeP1363FixedFieldConcatenation);
return grant with { Signature = ProvisioningExecutionGrantCodec.EncodeBase64Url(signature) };
}
private sealed class FakeFileSystem(List<string> events) : IProvisioningFileSystem
{
public HashSet<string> Files { get; } = new(StringComparer.Ordinal);
public HashSet<string> SymbolicLinks { get; } = new(StringComparer.Ordinal);
public List<(string Path, string Content)> Writes { get; } = [];
public bool FileExists(string path) => Files.Contains(path);
public bool IsSymbolicLink(string path) => SymbolicLinks.Contains(path);
public void CreateOwnerOnlyDirectory(string path)
=> events.Add($"mkdir:{path}");
public void WriteOwnerOnlyFile(string path, string content)
{
if (Writes.Any(write => string.Equals(write.Path, path, StringComparison.Ordinal)))
{
throw new IOException("Owner-only file already exists.");
}
Writes.Add((path, content));
events.Add("write-backup");
}
}
private sealed class FakeProcessRunner(
List<string> events,
params ProvisioningProcessResult[] results) : IProvisioningProcessRunner
{
private readonly Queue<ProvisioningProcessResult> _results = new(results);
public List<(string FileName, IReadOnlyList<string> Arguments)> Calls { get; } = [];
public Task<ProvisioningProcessResult> RunAsync(
string fileName,
IReadOnlyList<string> arguments,
CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
Calls.Add((fileName, arguments.ToArray()));
events.Add($"process:{string.Join(' ', arguments)}");
return Task.FromResult(_results.Count == 0
? throw new InvalidOperationException("Unexpected process call.")
: _results.Dequeue());
}
}
private sealed class SynchronouslyBlockingProcessRunner : IProvisioningProcessRunner
{
private int _calls;
public ManualResetEventSlim Started { get; } = new(false);
public bool CancellationObserved { get; private set; }
public bool RecoveryObserved { get; private set; }
public Task<ProvisioningProcessResult> RunAsync(
string fileName,
IReadOnlyList<string> arguments,
CancellationToken cancellationToken)
{
var call = Interlocked.Increment(ref _calls);
if (call == 1)
{
return Task.FromResult(new ProvisioningProcessResult(0, "UTC\n", ""));
}
if (call == 2)
{
Started.Set();
cancellationToken.WaitHandle.WaitOne();
CancellationObserved = cancellationToken.IsCancellationRequested;
cancellationToken.ThrowIfCancellationRequested();
throw new InvalidOperationException("Cancellation was not observed.");
}
RecoveryObserved = true;
return Task.FromResult(call switch
{
3 => new ProvisioningProcessResult(0, "Europe/Berlin\n", ""),
4 => new ProvisioningProcessResult(0, "", ""),
5 => new ProvisioningProcessResult(0, "UTC\n", ""),
_ => throw new InvalidOperationException("Unexpected recovery process call.")
});
}
}
}