summaryrefslogtreecommitdiff
path: root/src/Agent.Windows/Network/TaskNetworkPolicy.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.Windows/Network/TaskNetworkPolicy.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.Windows/Network/TaskNetworkPolicy.cs')
-rw-r--r--src/Agent.Windows/Network/TaskNetworkPolicy.cs83
1 files changed, 41 insertions, 42 deletions
diff --git a/src/Agent.Windows/Network/TaskNetworkPolicy.cs b/src/Agent.Windows/Network/TaskNetworkPolicy.cs
index 9f78b3d..4bcd8b2 100644
--- a/src/Agent.Windows/Network/TaskNetworkPolicy.cs
+++ b/src/Agent.Windows/Network/TaskNetworkPolicy.cs
@@ -1,79 +1,78 @@
+using System.Net;
using System.Security.Principal;
+using Agent.Admin.Client;
using Agent.Windows.Recovery;
using Agent.Windows.Security;
namespace Agent.Windows.Network;
-public sealed class TaskNetworkPolicy : IDisposable
+public sealed class TaskNetworkPolicy : IAsyncDisposable
{
- private readonly WfpDynamicSession _session;
+ private readonly IElevatedNetworkSession _session;
private readonly TaskResourceJournal _journal;
private readonly ISecurityAuditSink _audit;
- private readonly Guid _entryId;
private readonly SecurityIdentifier _appContainerSid;
private readonly Guid? _loopbackEntryId;
- private readonly bool _loopbackAdded;
private readonly Guid? _sessionId;
private bool _disposed;
- private TaskNetworkPolicy(WfpDynamicSession session, TaskResourceJournal journal, ISecurityAuditSink audit, Guid entryId,
- SecurityIdentifier appContainerSid, Guid? loopbackEntryId, bool loopbackAdded, Guid? sessionId)
+ private TaskNetworkPolicy(IElevatedNetworkSession session, TaskResourceJournal journal, ISecurityAuditSink audit,
+ SecurityIdentifier appContainerSid, Guid? loopbackEntryId, Guid? sessionId)
{
- _session = session; _journal = journal; _audit = audit; _entryId = entryId;
- _appContainerSid = appContainerSid; _loopbackEntryId = loopbackEntryId; _loopbackAdded = loopbackAdded;
+ _session = session;
+ _journal = journal;
+ _audit = audit;
+ _appContainerSid = appContainerSid;
+ _loopbackEntryId = loopbackEntryId;
_sessionId = sessionId;
}
- public static TaskNetworkPolicy Apply(TaskResourceJournal journal, SecurityIdentifier appContainerSid, IReadOnlyList<NetworkEndpoint> allowances,
- ISecurityAuditSink audit, Guid? sessionId = null, bool enableLoopback = true)
+ public static async Task<TaskNetworkPolicy> ApplyAsync(IElevatedNetworkSession session, TaskResourceJournal journal,
+ SecurityIdentifier appContainerSid, string networkAccess, IReadOnlyList<NetworkEndpoint> allowances,
+ ISecurityAuditSink audit, Guid? sessionId, CancellationToken cancellationToken)
{
- bool originallyExempt = enableLoopback && LoopbackExemption.Contains(appContainerSid);
- Guid? loopbackEntryId = enableLoopback ? journal.Prepare("network.loopback_exemption", appContainerSid.Value,
+ bool needsLoopback = allowances.Any(static item => IPAddress.IsLoopback(item.Address));
+ bool originallyExempt = needsLoopback && await session.InspectLoopbackAsync(appContainerSid.Value, cancellationToken).ConfigureAwait(false);
+ Guid? loopbackEntryId = needsLoopback ? journal.Prepare("network.loopback_exemption", appContainerSid.Value,
new Dictionary<string, string> { ["originally_present"] = originallyExempt.ToString() }) : null;
- bool loopbackAdded = false;
- Guid entryId = journal.Prepare("wfp.dynamic", appContainerSid.Value, new Dictionary<string, string>
- {
- ["session"] = "dynamic", ["allowances"] = string.Join(',', allowances.Select(static item => $"{item.Address}:{item.Port}"))
- });
- WfpDynamicSession session = WfpDynamicSession.Open();
try
{
- if (enableLoopback)
- {
- loopbackAdded = LoopbackExemption.Ensure(appContainerSid);
- journal.MarkApplied(loopbackEntryId!.Value, new Dictionary<string, string> { ["added"] = loopbackAdded.ToString() });
- }
- IReadOnlyList<ulong> ids = session.ApplyDefaultBlock(appContainerSid, allowances);
- journal.MarkApplied(entryId, new Dictionary<string, string> { ["filter_ids"] = string.Join(',', ids) });
+ AdminNetworkEndpoint[] endpoints = allowances.Select(static item => new AdminNetworkEndpoint(item.Address.ToString(), item.Port)).ToArray();
+ await session.ApplyAsync(appContainerSid.Value, networkAccess, endpoints, originallyExempt, cancellationToken).ConfigureAwait(false);
+ if (loopbackEntryId is not null) journal.MarkApplied(loopbackEntryId.Value);
audit.Write(new(DateTimeOffset.UtcNow, journal.TaskId, "network.rule.apply", "applied", new Dictionary<string, string>
{
- ["ipv4_ipv6_default_block"] = "true", ["filter_count"] = ids.Count.ToString(), ["session"] = "dynamic"
+ ["ipv4_ipv6_default_block"] = "true", ["endpoint_count"] = endpoints.Length.ToString(), ["session"] = "dynamic"
}, SessionId: sessionId));
- if (enableLoopback) audit.Write(new(DateTimeOffset.UtcNow, journal.TaskId, "network.loopback.apply", loopbackAdded ? "added" : "preserved",
- new Dictionary<string, string> { ["app_container_sid"] = appContainerSid.Value }, SessionId: sessionId));
- return new(session, journal, audit, entryId, appContainerSid, loopbackEntryId, loopbackAdded, sessionId);
+ if (loopbackEntryId is not null)
+ audit.Write(new(DateTimeOffset.UtcNow, journal.TaskId, "network.loopback.apply", originallyExempt ? "preserved" : "added",
+ new Dictionary<string, string> { ["app_container_sid"] = appContainerSid.Value }, SessionId: sessionId));
+ return new(session, journal, audit, appContainerSid, loopbackEntryId, sessionId);
}
catch
{
- session.Dispose();
- journal.MarkCleaned(entryId);
- if (loopbackAdded) LoopbackExemption.Remove(appContainerSid);
- if (loopbackEntryId is not null) journal.MarkCleaned(loopbackEntryId.Value);
+ bool cleaned = false;
+ try { await session.CleanupAsync(CancellationToken.None).ConfigureAwait(false); cleaned = true; } catch { }
+ if (cleaned && loopbackEntryId is not null) journal.MarkCleaned(loopbackEntryId.Value);
+ try { await session.DisposeAsync().ConfigureAwait(false); } catch { }
throw;
}
}
- public void Dispose()
+ public async ValueTask DisposeAsync()
{
if (_disposed) return;
- _session.RemovePolicy();
- _session.Dispose();
- _journal.MarkCleaned(_entryId);
- if (_loopbackAdded) LoopbackExemption.Remove(_appContainerSid);
- if (_loopbackEntryId is not null) _journal.MarkCleaned(_loopbackEntryId.Value);
- _audit.Write(new(DateTimeOffset.UtcNow, _journal.TaskId, "network.rule.remove", "removed", new Dictionary<string, string> { ["residual_filters"] = "0" }, SessionId: _sessionId));
- if (_loopbackEntryId is not null) _audit.Write(new(DateTimeOffset.UtcNow, _journal.TaskId, "network.loopback.remove", "removed",
- new Dictionary<string, string> { ["residual_exemptions"] = LoopbackExemption.Contains(_appContainerSid) ? "preexisting" : "0" }, SessionId: _sessionId));
+ try
+ {
+ await _session.CleanupAsync(CancellationToken.None).ConfigureAwait(false);
+ if (_loopbackEntryId is not null) _journal.MarkCleaned(_loopbackEntryId.Value);
+ }
+ finally { await _session.DisposeAsync().ConfigureAwait(false); }
+ _audit.Write(new(DateTimeOffset.UtcNow, _journal.TaskId, "network.rule.remove", "removed",
+ new Dictionary<string, string> { ["residual_filters"] = "0" }, SessionId: _sessionId));
+ if (_loopbackEntryId is not null)
+ _audit.Write(new(DateTimeOffset.UtcNow, _journal.TaskId, "network.loopback.remove", "restored",
+ new Dictionary<string, string> { ["app_container_sid"] = _appContainerSid.Value }, SessionId: _sessionId));
_disposed = true;
}
}