diff options
Diffstat (limited to 'src/Agent.Windows/Network')
| -rw-r--r-- | src/Agent.Windows/Network/LoopbackExemption.cs | 123 | ||||
| -rw-r--r-- | src/Agent.Windows/Network/NetworkAllowanceSnapshot.cs | 27 | ||||
| -rw-r--r-- | src/Agent.Windows/Network/TaskNetworkPolicy.cs | 83 | ||||
| -rw-r--r-- | src/Agent.Windows/Network/WfpDynamicSession.cs | 208 |
4 files changed, 68 insertions, 373 deletions
diff --git a/src/Agent.Windows/Network/LoopbackExemption.cs b/src/Agent.Windows/Network/LoopbackExemption.cs deleted file mode 100644 index a3c68ba..0000000 --- a/src/Agent.Windows/Network/LoopbackExemption.cs +++ /dev/null @@ -1,123 +0,0 @@ -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.Windows.Network; - -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.Windows/Network/NetworkAllowanceSnapshot.cs b/src/Agent.Windows/Network/NetworkAllowanceSnapshot.cs new file mode 100644 index 0000000..5feb457 --- /dev/null +++ b/src/Agent.Windows/Network/NetworkAllowanceSnapshot.cs @@ -0,0 +1,27 @@ +using System.Net; + +namespace Agent.Windows.Network; + +public sealed record NetworkEndpoint(IPAddress Address, ushort Port); + +public static class NetworkAllowanceSnapshot +{ + public static async Task<IReadOnlyList<NetworkEndpoint>> ResolveAsync(IEnumerable<(string Host, IReadOnlyList<int> Ports)> allowances, + CancellationToken cancellationToken) + { + var endpoints = new HashSet<NetworkEndpoint>(); + foreach ((string host, IReadOnlyList<int> ports) in allowances) + { + IPAddress[] addresses = await Dns.GetHostAddressesAsync(host, cancellationToken).ConfigureAwait(false); + foreach (int port in ports) + { + if (port is < 1 or > 65535) throw new ArgumentOutOfRangeException(nameof(allowances), $"Invalid network port {port}."); + foreach (IPAddress address in addresses.Where(static item => item.AddressFamily is + System.Net.Sockets.AddressFamily.InterNetwork or System.Net.Sockets.AddressFamily.InterNetworkV6)) + endpoints.Add(new(address, (ushort)port)); + } + } + return endpoints.OrderBy(static item => item.Address.ToString(), StringComparer.Ordinal) + .ThenBy(static item => item.Port).ToArray(); + } +} 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; } } diff --git a/src/Agent.Windows/Network/WfpDynamicSession.cs b/src/Agent.Windows/Network/WfpDynamicSession.cs deleted file mode 100644 index ef8ba8e..0000000 --- a/src/Agent.Windows/Network/WfpDynamicSession.cs +++ /dev/null @@ -1,208 +0,0 @@ -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.Windows.Network; - -public sealed record NetworkEndpoint(IPAddress Address, ushort Port); - -public static class NetworkAllowanceSnapshot -{ - public static async Task<IReadOnlyList<NetworkEndpoint>> ResolveAsync(IEnumerable<(string Host, IReadOnlyList<int> Ports)> allowances, CancellationToken cancellationToken) - { - var endpoints = new HashSet<NetworkEndpoint>(); - foreach ((string host, IReadOnlyList<int> ports) in allowances) - { - IPAddress[] addresses = await Dns.GetHostAddressesAsync(host, cancellationToken).ConfigureAwait(false); - foreach (int port in ports) - { - if (port is < 1 or > 65535) throw new ArgumentOutOfRangeException(nameof(allowances), $"Invalid network port {port}."); - foreach (IPAddress address in addresses.Where(static item => item.AddressFamily is System.Net.Sockets.AddressFamily.InterNetwork or System.Net.Sockets.AddressFamily.InterNetworkV6)) - endpoints.Add(new(address, (ushort)port)); - } - } - return endpoints.OrderBy(static item => item.Address.ToString(), StringComparer.Ordinal).ThenBy(static item => item.Port).ToArray(); - } -} - -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}."); - } -} |
