diff options
Diffstat (limited to 'src/Agent.Admin/Agent.Admin.Host')
| -rw-r--r-- | src/Agent.Admin/Agent.Admin.Host/Agent.Admin.Host.csproj | 19 | ||||
| -rw-r--r-- | src/Agent.Admin/Agent.Admin.Host/LoopbackExemption.cs | 123 | ||||
| -rw-r--r-- | src/Agent.Admin/Agent.Admin.Host/NativeMethods.txt | 27 | ||||
| -rw-r--r-- | src/Agent.Admin/Agent.Admin.Host/Program.cs | 211 | ||||
| -rw-r--r-- | src/Agent.Admin/Agent.Admin.Host/WfpDynamicSession.cs | 189 | ||||
| -rw-r--r-- | src/Agent.Admin/Agent.Admin.Host/app.manifest | 10 |
6 files changed, 579 insertions, 0 deletions
diff --git a/src/Agent.Admin/Agent.Admin.Host/Agent.Admin.Host.csproj b/src/Agent.Admin/Agent.Admin.Host/Agent.Admin.Host.csproj new file mode 100644 index 0000000..d5ee08a --- /dev/null +++ b/src/Agent.Admin/Agent.Admin.Host/Agent.Admin.Host.csproj @@ -0,0 +1,19 @@ +<Project Sdk="Microsoft.NET.Sdk"> + <PropertyGroup> + <OutputType>Exe</OutputType> + <TargetFramework>net10.0-windows10.0.26100.0</TargetFramework> + <AssemblyName>agent-mcp-admin</AssemblyName> + <RootNamespace>Agent.Admin.Host</RootNamespace> + <AllowUnsafeBlocks>true</AllowUnsafeBlocks> + <ApplicationManifest>app.manifest</ApplicationManifest> + <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles> + <CompilerGeneratedFilesOutputPath>$(IntermediateOutputPath)Generated</CompilerGeneratedFilesOutputPath> + </PropertyGroup> + <ItemGroup> + <ProjectReference Include="..\Agent.Admin.Client\Agent.Admin.Client.csproj" /> + <PackageReference Include="Microsoft.Windows.CsWin32" PrivateAssets="all" /> + </ItemGroup> + <ItemGroup> + <InternalsVisibleTo Include="Agent.Windows.Tests" /> + </ItemGroup> +</Project> diff --git a/src/Agent.Admin/Agent.Admin.Host/LoopbackExemption.cs b/src/Agent.Admin/Agent.Admin.Host/LoopbackExemption.cs new file mode 100644 index 0000000..3449491 --- /dev/null +++ b/src/Agent.Admin/Agent.Admin.Host/LoopbackExemption.cs @@ -0,0 +1,123 @@ +using System.ComponentModel; +using System.Runtime.InteropServices; +using System.Security.Principal; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.Security; +using Windows.Win32.System.Memory; + +namespace Agent.Admin.Host; + +internal static unsafe class LoopbackExemption +{ + private const string MutexName = "Local\\AgentSandboxMcpLoopbackExemption"; + + public static bool Contains(SecurityIdentifier sid) + { + using MutexLease mutex = AcquireMutex(); + return Read().Any(item => item.Sid.Equals(sid)); + } + + public static bool Ensure(SecurityIdentifier sid) + { + using MutexLease mutex = AcquireMutex(); + List<Entry> entries = Read(); + if (entries.Any(item => item.Sid.Equals(sid))) return false; + entries.Add(new(sid, 0)); + Write(entries); + return true; + } + + public static void Remove(SecurityIdentifier sid) + { + using MutexLease mutex = AcquireMutex(); + List<Entry> entries = Read(); + if (entries.RemoveAll(item => item.Sid.Equals(sid)) == 0) return; + Write(entries); + } + + private static MutexLease AcquireMutex() + { + var mutex = new Mutex(false, MutexName); + try + { + try + { + if (!mutex.WaitOne(TimeSpan.FromSeconds(30))) throw new TimeoutException("Timed out waiting for the loopback exemption lock."); + } + catch (AbandonedMutexException) + { + // Ownership is granted to this thread when the prior owner died. + } + return new(mutex); + } + catch + { + mutex.Dispose(); + throw; + } + } + + private static List<Entry> Read() + { + uint result = PInvoke.NetworkIsolationGetAppContainerConfig(out uint count, out SID_AND_ATTRIBUTES* native); + ThrowIfError(result, "NetworkIsolationGetAppContainerConfig"); + try + { + var entries = new List<Entry>(checked((int)count)); + for (uint index = 0; index < count; index++) + { + SID_AND_ATTRIBUTES item = native[index]; + entries.Add(new(new SecurityIdentifier((nint)item.Sid.Value), item.Attributes)); + } + return entries; + } + finally + { + HANDLE heap = PInvoke.GetProcessHeap(); + for (uint index = 0; index < count; index++) + if (native[index].Sid.Value is not null) _ = PInvoke.HeapFree(heap, HEAP_FLAGS.HEAP_NONE, native[index].Sid.Value); + if (native is not null) _ = PInvoke.HeapFree(heap, HEAP_FLAGS.HEAP_NONE, native); + } + } + + private static void Write(IReadOnlyList<Entry> entries) + { + var native = new SID_AND_ATTRIBUTES[entries.Count]; + var allocations = new List<nint>(entries.Count); + try + { + for (int index = 0; index < entries.Count; index++) + { + byte[] bytes = new byte[entries[index].Sid.BinaryLength]; + entries[index].Sid.GetBinaryForm(bytes, 0); + void* allocation = NativeMemory.Alloc((nuint)bytes.Length); + Marshal.Copy(bytes, 0, (nint)allocation, bytes.Length); + allocations.Add((nint)allocation); + native[index].Sid = new PSID(allocation); + native[index].Attributes = entries[index].Attributes; + } + ThrowIfError(PInvoke.NetworkIsolationSetAppContainerConfig(native), "NetworkIsolationSetAppContainerConfig"); + } + finally + { + foreach (nint allocation in allocations) NativeMemory.Free((void*)allocation); + } + } + + private static void ThrowIfError(uint result, string operation) + { + if (result != 0) throw new Win32Exception(unchecked((int)result), $"{operation} failed with Win32 error 0x{result:X8}."); + } + + private sealed record Entry(SecurityIdentifier Sid, uint Attributes); + + private sealed class MutexLease(Mutex mutex) : IDisposable + { + public void Dispose() + { + mutex.ReleaseMutex(); + mutex.Dispose(); + } + } +} diff --git a/src/Agent.Admin/Agent.Admin.Host/NativeMethods.txt b/src/Agent.Admin/Agent.Admin.Host/NativeMethods.txt new file mode 100644 index 0000000..cb038a6 --- /dev/null +++ b/src/Agent.Admin/Agent.Admin.Host/NativeMethods.txt @@ -0,0 +1,27 @@ +Windows.Win32.NetworkManagement.WindowsFilteringPlatform.FwpmEngineOpen0 +Windows.Win32.NetworkManagement.WindowsFilteringPlatform.FwpmEngineClose0 +Windows.Win32.NetworkManagement.WindowsFilteringPlatform.FwpmTransactionBegin0 +Windows.Win32.NetworkManagement.WindowsFilteringPlatform.FwpmTransactionCommit0 +Windows.Win32.NetworkManagement.WindowsFilteringPlatform.FwpmTransactionAbort0 +Windows.Win32.NetworkManagement.WindowsFilteringPlatform.FwpmSubLayerAdd0 +Windows.Win32.NetworkManagement.WindowsFilteringPlatform.FwpmSubLayerDeleteByKey0 +Windows.Win32.NetworkManagement.WindowsFilteringPlatform.FwpmFilterAdd0 +Windows.Win32.NetworkManagement.WindowsFilteringPlatform.FwpmFilterDeleteById0 +FWPM_SESSION0 +FWPM_SESSION_FLAG_DYNAMIC +FWPM_SUBLAYER0 +FWPM_FILTER0 +FWPM_FILTER_CONDITION0 +FWPM_LAYER_ALE_AUTH_CONNECT_V4 +FWPM_LAYER_ALE_AUTH_CONNECT_V6 +FWPM_CONDITION_ALE_PACKAGE_ID +FWPM_CONDITION_IP_REMOTE_ADDRESS +FWPM_CONDITION_IP_REMOTE_PORT +FWP_BYTE_ARRAY16 +FWP_ACTION_TYPE +FWP_MATCH_TYPE +FWP_DATA_TYPE +NetworkIsolationGetAppContainerConfig +NetworkIsolationSetAppContainerConfig +GetProcessHeap +HeapFree diff --git a/src/Agent.Admin/Agent.Admin.Host/Program.cs b/src/Agent.Admin/Agent.Admin.Host/Program.cs new file mode 100644 index 0000000..635060e --- /dev/null +++ b/src/Agent.Admin/Agent.Admin.Host/Program.cs @@ -0,0 +1,211 @@ +using System.Diagnostics; +using System.IO.Pipes; +using System.Net; +using System.Runtime.InteropServices; +using System.Security.Principal; +using Agent.Admin.Client; +using Microsoft.Win32.SafeHandles; + +namespace Agent.Admin.Host; + +internal static partial class Program +{ + private const string FixedProfileName = "AgentSandboxMcp.V1"; + + public static async Task<int> Main(string[] args) + { + try + { + Arguments options = Arguments.Parse(args); + using WindowsIdentity identity = WindowsIdentity.GetCurrent(); + if (!new WindowsPrincipal(identity).IsInRole(WindowsBuiltInRole.Administrator)) + throw new InvalidOperationException("The elevated helper requires an administrator token."); + using Process parent = Process.GetProcessById(checked((int)options.ParentPid)); + if (parent.HasExited || !string.Equals(Path.GetFileName(parent.MainModule?.FileName), "agent-mcp.exe", StringComparison.OrdinalIgnoreCase)) + throw new InvalidOperationException("The parent Process is not the fixed agent-mcp executable."); + + using var pipe = new NamedPipeClientStream(".", options.PipeName, PipeDirection.InOut, + PipeOptions.Asynchronous | PipeOptions.WriteThrough, TokenImpersonationLevel.Identification); + await pipe.ConnectAsync(30_000).ConfigureAwait(false); + if (!Native.GetNamedPipeServerProcessId(pipe.SafePipeHandle, out uint serverPid) || serverPid != options.ParentPid) + throw new InvalidOperationException("The Named Pipe server Process does not match the expected parent."); + + AdminRequest hello = await AdminProtocol.ReadAsync<AdminRequest>(pipe, CancellationToken.None).ConfigureAwait(false); + ValidateEnvelope(hello, options, "hello"); + await AdminProtocol.WriteAsync(pipe, new AdminResponse(true), CancellationToken.None).ConfigureAwait(false); + + using var state = new NetworkState(); + while (true) + { + AdminRequest request = await AdminProtocol.ReadAsync<AdminRequest>(pipe, CancellationToken.None).ConfigureAwait(false); + if (request.Version != AdminProtocol.Version || request.TaskId != options.TaskId) + { + await AdminProtocol.WriteAsync(pipe, new AdminResponse(false, "sandbox.ipc_rejected", "Protocol or Task identity mismatch."), CancellationToken.None).ConfigureAwait(false); + return 3; + } + try + { + AdminResponse response = request.Operation switch + { + "inspect-loopback" => Inspect(request), + "apply-network" => Apply(request, state), + "cleanup-network" => Cleanup(state), + "recover-loopback" => Recover(request), + "shutdown" => new(true), + _ => new(false, "sandbox.ipc_rejected", "Unsupported administrative pipe operation.") + }; + await AdminProtocol.WriteAsync(pipe, response, CancellationToken.None).ConfigureAwait(false); + if (request.Operation == "shutdown") return 0; + if (!response.Success && response.ErrorCode == "sandbox.ipc_rejected") return 3; + } + catch (Exception ex) when (ex is not (OutOfMemoryException or StackOverflowException or AccessViolationException)) + { + await AdminProtocol.WriteAsync(pipe, new AdminResponse(false, "sandbox.network_apply_failed", ex.Message), CancellationToken.None).ConfigureAwait(false); + } + } + } + catch (Exception ex) when (ex is not (OutOfMemoryException or StackOverflowException or AccessViolationException)) + { + Console.Error.WriteLine($"agent-mcp-admin: {ex.Message}"); + return 2; + } + } + + private static AdminResponse Inspect(AdminRequest request) + { + SecurityIdentifier sid = ValidateSid(request.ApplicationSid); + return new(true, LoopbackExempt: LoopbackExemption.Contains(sid)); + } + + private static AdminResponse Apply(AdminRequest request, NetworkState state) + { + if (state.Applied) return new(false, "sandbox.ipc_rejected", "Network Task Resource is already applied."); + SecurityIdentifier sid = ValidateSid(request.ApplicationSid); + IReadOnlyList<NetworkEndpoint> endpoints = ValidateNetwork(request.NetworkAccess, request.Endpoints); + bool needsLoopback = endpoints.Any(static item => IPAddress.IsLoopback(item.Address)); + bool currentLoopback = LoopbackExemption.Contains(sid); + if (needsLoopback && currentLoopback != request.OriginallyLoopbackExempt) + return new(false, "sandbox.network_apply_failed", "Loopback state changed after Recovery Journal preparation."); + + state.Session = WfpDynamicSession.Open(); + try + { + state.Session.ApplyDefaultBlock(sid, endpoints); + if (needsLoopback && !currentLoopback) state.LoopbackAdded = LoopbackExemption.Ensure(sid); + state.Sid = sid; + state.Applied = true; + return new(true); + } + catch + { + if (state.LoopbackAdded) LoopbackExemption.Remove(sid); + state.Session.Dispose(); + state.Session = null; + state.LoopbackAdded = false; + throw; + } + } + + private static AdminResponse Cleanup(NetworkState state) + { + state.Cleanup(); + return new(true); + } + + private static AdminResponse Recover(AdminRequest request) + { + SecurityIdentifier sid = ValidateSid(request.ApplicationSid); + if (request.OriginallyLoopbackExempt == true) _ = LoopbackExemption.Ensure(sid); + else LoopbackExemption.Remove(sid); + return new(true); + } + + private static SecurityIdentifier ValidateSid(string? value) + { + if (string.IsNullOrWhiteSpace(value)) throw new InvalidDataException("Application SID is required."); + var requested = new SecurityIdentifier(value); + int result = Native.DeriveAppContainerSidFromAppContainerName(FixedProfileName, out nint pointer); + if (result < 0) Marshal.ThrowExceptionForHR(result); + try + { + var fixedSid = new SecurityIdentifier(pointer); + if (!fixedSid.Equals(requested)) throw new InvalidDataException("Only the fixed Application Profile SID is accepted."); + return requested; + } + finally { if (pointer != 0) _ = Native.FreeSid(pointer); } + } + + private static IReadOnlyList<NetworkEndpoint> ValidateNetwork(string? access, IReadOnlyList<AdminNetworkEndpoint>? input) + { + if (access is not ("none" or "local" or "restricted")) throw new InvalidDataException("Unsupported Network Access Mode."); + if (input is null || input.Count > 4096) throw new InvalidDataException("Network endpoint list is invalid."); + var endpoints = input.Select(item => + { + if (!IPAddress.TryParse(item.Address, out IPAddress? address) || item.Port is < 1 or > 65535) + throw new InvalidDataException("Network endpoint is invalid."); + return new NetworkEndpoint(address, (ushort)item.Port); + }).Distinct().ToArray(); + if (access == "none" && endpoints.Length != 0) throw new InvalidDataException("Network none must not contain endpoints."); + if (access == "local" && (endpoints.Length == 0 || endpoints.Any(static item => !IPAddress.IsLoopback(item.Address)))) + throw new InvalidDataException("Network local accepts only IPv4/IPv6 loopback endpoints."); + if (access == "restricted" && endpoints.Length == 0) throw new InvalidDataException("Network restricted requires endpoints."); + return endpoints; + } + + private static void ValidateEnvelope(AdminRequest request, Arguments options, string operation) + { + if (request.Version != options.Protocol || request.Version != AdminProtocol.Version || + request.TaskId != options.TaskId || request.Operation != operation) + throw new InvalidDataException("Administrative pipe handshake mismatch."); + } + + private sealed class NetworkState : IDisposable + { + public WfpDynamicSession? Session { get; set; } + public SecurityIdentifier? Sid { get; set; } + public bool LoopbackAdded { get; set; } + public bool Applied { get; set; } + public void Cleanup() + { + if (LoopbackAdded && Sid is not null) LoopbackExemption.Remove(Sid); + LoopbackAdded = false; + if (Session is not null) + { + try { Session.RemovePolicy(); } + finally { Session.Dispose(); Session = null; } + } + Applied = false; + } + public void Dispose() { try { Cleanup(); } catch { Session?.Dispose(); } } + } + + private sealed record Arguments(string PipeName, uint ParentPid, Guid TaskId, int Protocol) + { + public static Arguments Parse(string[] args) + { + var values = new Dictionary<string, string>(StringComparer.Ordinal); + for (int i = 0; i < args.Length; i += 2) + { + if (i + 1 >= args.Length || args[i] is not ("--pipe" or "--parent-pid" or "--task" or "--protocol")) + throw new ArgumentException("The elevated helper accepts only its fixed internal arguments."); + values.Add(args[i], args[i + 1]); + } + if (values.Count != 4 || !uint.TryParse(values["--parent-pid"], out uint parentPid) || + !Guid.TryParseExact(values["--task"], "N", out Guid taskId) || !int.TryParse(values["--protocol"], out int protocol) || + protocol != AdminProtocol.Version || string.IsNullOrWhiteSpace(values["--pipe"])) + throw new ArgumentException("The elevated helper arguments are invalid."); + return new(values["--pipe"], parentPid, taskId, protocol); + } + } + + private static partial class Native + { + [LibraryImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static partial bool GetNamedPipeServerProcessId(SafePipeHandle pipe, out uint serverProcessId); + [LibraryImport("userenv.dll", StringMarshalling = StringMarshalling.Utf16)] + internal static partial int DeriveAppContainerSidFromAppContainerName(string name, out nint sid); + [LibraryImport("advapi32.dll")] + internal static partial nint FreeSid(nint sid); + } +} diff --git a/src/Agent.Admin/Agent.Admin.Host/WfpDynamicSession.cs b/src/Agent.Admin/Agent.Admin.Host/WfpDynamicSession.cs new file mode 100644 index 0000000..4aa21da --- /dev/null +++ b/src/Agent.Admin/Agent.Admin.Host/WfpDynamicSession.cs @@ -0,0 +1,189 @@ +using System.ComponentModel; +using System.Buffers.Binary; +using System.Net; +using System.Runtime.InteropServices; +using System.Security.Principal; +using Windows.Win32; +using Windows.Win32.Foundation; +using Windows.Win32.NetworkManagement.WindowsFilteringPlatform; +using Windows.Win32.Security; + +namespace Agent.Admin.Host; + +public sealed record NetworkEndpoint(IPAddress Address, ushort Port); + +public sealed unsafe class WfpDynamicSession : IDisposable +{ + private const FWPM_FILTER_FLAGS FwpmFilterFlagClearActionRight = (FWPM_FILTER_FLAGS)0x00000008; + private readonly FwpmEngineClose0SafeHandle _engine; + private Guid? _subLayerKey; + private readonly List<ulong> _filterIds = []; + + private WfpDynamicSession(FwpmEngineClose0SafeHandle engine) => _engine = engine; + public static WfpDynamicSession Open(string displayName = "Agent Sandbox MCP dynamic task session") + { + FWPM_SESSION0 session = default; + session.flags = PInvoke.FWPM_SESSION_FLAG_DYNAMIC; + uint result = PInvoke.FwpmEngineOpen0(10, null, session, out FwpmEngineClose0SafeHandle engine); + ThrowIfError(result, "FwpmEngineOpen0"); + return new(engine); + } + + public void ProbeWriteTransaction() + { + uint result = PInvoke.FwpmTransactionBegin0(_engine, 0); + ThrowIfError(result, "FwpmTransactionBegin0"); + result = PInvoke.FwpmTransactionAbort0(_engine); + ThrowIfError(result, "FwpmTransactionAbort0"); + } + + public IReadOnlyList<ulong> ApplyDefaultBlock(SecurityIdentifier appContainerSid, IReadOnlyList<NetworkEndpoint>? allowedEndpoints = null) + { + if (_subLayerKey is not null) throw new InvalidOperationException("WFP policy is already applied in this session."); + byte[] sidBytes = new byte[appContainerSid.BinaryLength]; + appContainerSid.GetBinaryForm(sidBytes, 0); + uint result = PInvoke.FwpmTransactionBegin0(_engine, 0); + ThrowIfError(result, "FwpmTransactionBegin0"); + bool committed = false; + try + { + Guid subLayerKey = Guid.NewGuid(); + fixed (char* subLayerName = "Agent Sandbox MCP task sublayer") + { + FWPM_SUBLAYER0 subLayer = default; + subLayer.subLayerKey = subLayerKey; + subLayer.displayData.name = new PWSTR(subLayerName); + subLayer.weight = 0x7FFF; + ThrowIfError(PInvoke.FwpmSubLayerAdd0(_engine, in subLayer, default), "FwpmSubLayerAdd0"); + } + + fixed (byte* sidPointer = sidBytes) + { + foreach (NetworkEndpoint endpoint in allowedEndpoints ?? []) _filterIds.Add(AddPackagePermit(endpoint, subLayerKey, (SID*)sidPointer)); + _filterIds.Add(AddPackageBlock(PInvoke.FWPM_LAYER_ALE_AUTH_CONNECT_V4, subLayerKey, (SID*)sidPointer, "Agent Sandbox MCP IPv4 default block")); + _filterIds.Add(AddPackageBlock(PInvoke.FWPM_LAYER_ALE_AUTH_CONNECT_V6, subLayerKey, (SID*)sidPointer, "Agent Sandbox MCP IPv6 default block")); + } + ThrowIfError(PInvoke.FwpmTransactionCommit0(_engine), "FwpmTransactionCommit0"); + committed = true; + _subLayerKey = subLayerKey; + return _filterIds.ToArray(); + } + finally + { + if (!committed) + { + _filterIds.Clear(); + _ = PInvoke.FwpmTransactionAbort0(_engine); + } + } + } + + public void RemovePolicy() + { + if (_subLayerKey is null) return; + ThrowIfError(PInvoke.FwpmTransactionBegin0(_engine, 0), "FwpmTransactionBegin0(remove)"); + bool committed = false; + try + { + foreach (ulong filterId in _filterIds.AsEnumerable().Reverse()) ThrowIfError(PInvoke.FwpmFilterDeleteById0(_engine, filterId), "FwpmFilterDeleteById0"); + Guid subLayerKey = _subLayerKey.Value; + ThrowIfError(PInvoke.FwpmSubLayerDeleteByKey0(_engine, in subLayerKey), "FwpmSubLayerDeleteByKey0"); + ThrowIfError(PInvoke.FwpmTransactionCommit0(_engine), "FwpmTransactionCommit0(remove)"); + committed = true; + _filterIds.Clear(); + _subLayerKey = null; + } + finally + { + if (!committed) _ = PInvoke.FwpmTransactionAbort0(_engine); + } + } + + public void Dispose() + { + _engine.Dispose(); + } + + private ulong AddPackageBlock(Guid layerKey, Guid subLayerKey, SID* sid, string displayName) + { + FWPM_FILTER_CONDITION0 condition = default; + condition.fieldKey = PInvoke.FWPM_CONDITION_ALE_PACKAGE_ID; + condition.matchType = FWP_MATCH_TYPE.FWP_MATCH_EQUAL; + condition.conditionValue.type = FWP_DATA_TYPE.FWP_SID; + condition.conditionValue.sid = sid; + fixed (char* name = displayName) + { + FWPM_FILTER0 filter = default; + filter.filterKey = Guid.NewGuid(); + filter.displayData.name = new PWSTR(name); + filter.layerKey = layerKey; + filter.subLayerKey = subLayerKey; + filter.weight.type = FWP_DATA_TYPE.FWP_UINT8; + filter.weight.uint8 = 8; + filter.numFilterConditions = 1; + filter.filterCondition = &condition; + filter.action.type = FWP_ACTION_TYPE.FWP_ACTION_BLOCK; + ThrowIfError(PInvoke.FwpmFilterAdd0(_engine, in filter, default, out ulong id), "FwpmFilterAdd0"); + return id; + } + } + + private ulong AddPackagePermit(NetworkEndpoint endpoint, Guid subLayerKey, SID* sid) + { + FWPM_FILTER_CONDITION0* conditions = stackalloc FWPM_FILTER_CONDITION0[3]; + conditions[0].fieldKey = PInvoke.FWPM_CONDITION_ALE_PACKAGE_ID; + conditions[0].matchType = FWP_MATCH_TYPE.FWP_MATCH_EQUAL; + conditions[0].conditionValue.type = FWP_DATA_TYPE.FWP_SID; + conditions[0].conditionValue.sid = sid; + conditions[1].fieldKey = PInvoke.FWPM_CONDITION_IP_REMOTE_ADDRESS; + conditions[1].matchType = FWP_MATCH_TYPE.FWP_MATCH_EQUAL; + Guid layer; + FWP_BYTE_ARRAY16 ipv6 = default; + if (endpoint.Address.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork) + { + layer = PInvoke.FWPM_LAYER_ALE_AUTH_CONNECT_V4; + conditions[1].conditionValue.type = FWP_DATA_TYPE.FWP_UINT32; + // FWP_UINT32 uses host-order numeric values. For 127.0.0.1 the + // numeric value is 0x7F000001; parse the network-order octets into + // that value rather than reinterpreting their in-memory byte order. + conditions[1].conditionValue.uint32 = BinaryPrimitives.ReadUInt32BigEndian(endpoint.Address.GetAddressBytes()); + } + else + { + layer = PInvoke.FWPM_LAYER_ALE_AUTH_CONNECT_V6; + ipv6.byteArray16 = endpoint.Address.GetAddressBytes(); + conditions[1].conditionValue.type = FWP_DATA_TYPE.FWP_BYTE_ARRAY16_TYPE; + conditions[1].conditionValue.byteArray16 = &ipv6; + } + conditions[2].fieldKey = PInvoke.FWPM_CONDITION_IP_REMOTE_PORT; + conditions[2].matchType = FWP_MATCH_TYPE.FWP_MATCH_EQUAL; + conditions[2].conditionValue.type = FWP_DATA_TYPE.FWP_UINT16; + conditions[2].conditionValue.uint16 = endpoint.Port; + string displayName = $"Agent Sandbox MCP allow {endpoint.Address}:{endpoint.Port}"; + fixed (char* name = displayName) + { + FWPM_FILTER0 filter = default; + filter.filterKey = Guid.NewGuid(); + filter.displayData.name = new PWSTR(name); + filter.layerKey = layer; + filter.subLayerKey = subLayerKey; + filter.weight.type = FWP_DATA_TYPE.FWP_UINT8; + filter.weight.uint8 = 15; + filter.numFilterConditions = 3; + filter.filterCondition = conditions; + // A plain permit is a soft permit and can be overridden by a block + // from another sub-layer (for example Windows Firewall). The exact + // task allowance must be a hard permit; our higher-weight match in + // this sub-layer still prevents the task default-block from running. + filter.flags = FwpmFilterFlagClearActionRight; + filter.action.type = FWP_ACTION_TYPE.FWP_ACTION_PERMIT; + ThrowIfError(PInvoke.FwpmFilterAdd0(_engine, in filter, default, out ulong id), "FwpmFilterAdd0(permit)"); + return id; + } + } + + private static void ThrowIfError(uint result, string operation) + { + if (result != 0) throw new Win32Exception(unchecked((int)result), $"{operation} failed with WFP/Win32 error 0x{result:X8}."); + } +} diff --git a/src/Agent.Admin/Agent.Admin.Host/app.manifest b/src/Agent.Admin/Agent.Admin.Host/app.manifest new file mode 100644 index 0000000..9ee29da --- /dev/null +++ b/src/Agent.Admin/Agent.Admin.Host/app.manifest @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="utf-8"?> +<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1"> + <trustInfo xmlns="urn:schemas-microsoft-com:asm.v3"> + <security> + <requestedPrivileges> + <requestedExecutionLevel level="requireAdministrator" uiAccess="false" /> + </requestedPrivileges> + </security> + </trustInfo> +</assembly> |
