using System.Diagnostics; using Microsoft.Extensions.Options; using ServerMonitorManager.Core; namespace ServerMonitorManager.Control; public interface ILinkPolicyApplier { Task> ListRulesAsync(CancellationToken cancellationToken); Task ApplyConnectAsync(LinkPolicy link, CancellationToken cancellationToken); Task ApplyDisconnectAsync(LinkPolicy link, CancellationToken cancellationToken); Task ApplyDisconnectAsync(LinkRule rule, CancellationToken cancellationToken); Task IsConnectedAsync(LinkPolicy link, CancellationToken cancellationToken); Task GetReconciliationRequestAsync(CancellationToken cancellationToken) => Task.FromResult(null); Task CompleteReconciliationAsync(string generation, CancellationToken cancellationToken) => Task.CompletedTask; } public sealed record LinkRule(string SourceNodeId, string TargetNodeId, string Protocol, int Port); public sealed class MeshFirewallUnavailableException : InvalidOperationException { public MeshFirewallUnavailableException(string message) : base(message) { } } public sealed class MeshNodeNotActivatedException : InvalidOperationException { public MeshNodeNotActivatedException(string message) : base(message) { } } public sealed class LinkPolicyApplier(IOptions options) : ILinkPolicyApplier { public async Task> ListRulesAsync(CancellationToken cancellationToken) { var output = await RunAsync(["link-list"], cancellationToken); if (output.Length == 0) { return []; } var rules = new List(); foreach (var line in output.Split('\n', StringSplitOptions.RemoveEmptyEntries)) { var fields = line.Trim().Split('\t'); if (fields.Length != 4 || !int.TryParse(fields[3], System.Globalization.NumberStyles.None, System.Globalization.CultureInfo.InvariantCulture, out var port) || port is < 1 or > 65535) { throw new InvalidOperationException("Hub policy helper returned an invalid link-list record."); } rules.Add(new LinkRule(fields[0], fields[1], fields[2], port)); } return rules; } public Task ApplyConnectAsync(LinkPolicy link, CancellationToken cancellationToken) => RunAsync( [ "link-connect", link.SourceNodeId, link.TargetNodeId, link.Protocol, link.Port.ToString(System.Globalization.CultureInfo.InvariantCulture), link.TtlMinutes.ToString(System.Globalization.CultureInfo.InvariantCulture) ], cancellationToken); public Task ApplyDisconnectAsync(LinkPolicy link, CancellationToken cancellationToken) => ApplyDisconnectAsync( new LinkRule(link.SourceNodeId, link.TargetNodeId, link.Protocol, link.Port), cancellationToken); public Task ApplyDisconnectAsync(LinkRule rule, CancellationToken cancellationToken) => RunAsync( [ "link-disconnect", rule.SourceNodeId, rule.TargetNodeId, rule.Protocol, rule.Port.ToString(System.Globalization.CultureInfo.InvariantCulture) ], cancellationToken); public async Task IsConnectedAsync(LinkPolicy link, CancellationToken cancellationToken) { var output = await RunAsync( [ "link-status", link.SourceNodeId, link.TargetNodeId, link.Protocol, link.Port.ToString(System.Globalization.CultureInfo.InvariantCulture) ], cancellationToken); return output switch { "active" => true, "disabled" => false, _ => throw new InvalidOperationException("Hub policy helper returned an invalid link status.") }; } public async Task GetReconciliationRequestAsync(CancellationToken cancellationToken) => await RunAsync(["reconcile-status"], cancellationToken) switch { "complete" => null, var status when status.StartsWith("requested:", StringComparison.Ordinal) && Guid.TryParseExact(status[10..], "D", out _) => status[10..], _ => throw new InvalidOperationException("Hub policy helper returned an invalid reconciliation marker status.") }; public async Task CompleteReconciliationAsync(string generation, CancellationToken cancellationToken) { if (!Guid.TryParseExact(generation, "D", out _)) { throw new ArgumentException("Invalid reconciliation generation.", nameof(generation)); } _ = await RunAsync(["reconcile-complete", generation], cancellationToken); } private async Task RunAsync(IReadOnlyList arguments, CancellationToken cancellationToken) { var startInfo = new ProcessStartInfo { FileName = options.Value.PrivilegeEscalationPath, RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true }; startInfo.ArgumentList.Add("-n"); startInfo.ArgumentList.Add(options.Value.HubHelperPath); foreach (var argument in arguments) { startInfo.ArgumentList.Add(argument); } using var process = Process.Start(startInfo) ?? throw new InvalidOperationException("Could not start the Hub policy helper."); var output = process.StandardOutput.ReadToEndAsync(cancellationToken); var error = process.StandardError.ReadToEndAsync(cancellationToken); await process.WaitForExitAsync(cancellationToken); if (process.ExitCode != 0) { var message = (await error).Trim(); if (process.ExitCode == 79 && string.Equals(message, LinkService.FirewallUnavailableCode, StringComparison.Ordinal)) { throw new MeshFirewallUnavailableException(LinkService.FirewallUnavailableCode); } if (process.ExitCode == 80 && string.Equals(message, LinkService.NodeNotActivatedCode, StringComparison.Ordinal)) { throw new MeshNodeNotActivatedException(LinkService.NodeNotActivatedCode); } throw new InvalidOperationException(string.IsNullOrWhiteSpace(message) ? $"Hub policy helper exited with code {process.ExitCode}." : message); } return (await output).Trim(); } }