summaryrefslogtreecommitdiff
path: root/src/Agent.Admin/Agent.Admin.Host/WfpDynamicSession.cs
diff options
context:
space:
mode:
authortanaka takashi <isazaka@gmail.com>2026-08-13 22:48:24 +0900
committertanaka takashi <isazaka@gmail.com>2026-08-13 22:48:24 +0900
commit9337ac3e5edd42bb17f5e2ade6c2de02c7ef576d (patch)
tree9a48fefc88cc046f4ce1e2bc34fa0a47351e4f83 /src/Agent.Admin/Agent.Admin.Host/WfpDynamicSession.cs
parent36ae1a4cba791ec633a81b38b1d8345af8487276 (diff)
feat!: isolate privileged sandbox network operations per task
Run agent-mcp with an explicit asInvoker manifest and move WFP and loopback mutations into a fixed requireAdministrator helper under the release admin directory. Add the Agent.Admin client and host projects with task-scoped runas launch, a logon-SID and Administrators-only local named pipe, remote-client rejection, mutual PID validation, strict versioned JSON framing, fixed network-only operations, and bounded messages. Delay ACL and network Task Resource application until the first process_run. Verify executable identity, managed entry points, DNS snapshots, and normal-token WRITE_DAC access before elevation; reuse the helper only within the Task and keep elevation failures terminal without policy replanning. Keep LPAC creation, executable approval, Job confinement, and ACL leases in the normal process. Make startup platform checks read-only, create the fixed Application Profile during apply, preserve dynamic WFP lifetime in the helper, and route persistent loopback recovery through the same elevated boundary. Update recovery journaling, localized elevation and IPC errors, audit events, release publishing, manifests, README guidance, Specifications, Design, implementation records, and requirement traceability for the on-demand elevation model. Add administrative-boundary, file-only no-elevation, elevation-terminal, lazy-resource, crash-recovery, manifest, IPC schema, and operational verification coverage. Verified Release build with zero warnings, Core 105/105, Planner 10/10, administrative boundary 4/4, elevated Windows suite 79/79, and published stdio/HTTP MCP E2E.
Diffstat (limited to 'src/Agent.Admin/Agent.Admin.Host/WfpDynamicSession.cs')
-rw-r--r--src/Agent.Admin/Agent.Admin.Host/WfpDynamicSession.cs189
1 files changed, 189 insertions, 0 deletions
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}.");
+ }
+}