w4c-workflows-api/Services/Security/EgressGuard.cs
2026-09-12 01:02:46 +03:00

53 lines
1.7 KiB
C#

using System.Net;
using System.Net.Sockets;
namespace w4c_workflows.Services.Security;
/// <summary>
/// Authorises an outbound <see cref="Uri"/> before a workflow node sends it.
/// Names are resolved up front and every returned address is checked, so a host
/// that maps to both a public and a private address is rejected rather than
/// raced at connect time. Literal IPs skip DNS entirely.
/// </summary>
public sealed class EgressGuard
{
private readonly EgressPolicy _policy;
private readonly IHostAddressResolver _resolver;
public EgressGuard(EgressPolicy policy, IHostAddressResolver resolver)
{
_policy = policy;
_resolver = resolver;
}
/// <summary>Ceiling the HTTP node applies to its own redirect setting.</summary>
public int MaxRedirects => _policy.MaxRedirects;
public async Task<EgressDecision> AuthorizeAsync(Uri uri, CancellationToken ct)
{
var hostDecision = _policy.CheckHost(uri);
if (hostDecision != null)
return hostDecision;
var host = uri.DnsSafeHost;
if (IPAddress.TryParse(host, out var literal))
return _policy.CheckAddresses(host, new[] { literal });
IReadOnlyList<IPAddress> addresses;
try
{
addresses = await _resolver.ResolveAsync(host, ct);
}
catch (SocketException ex)
{
return EgressDecision.Block("dns_failure", $"could not resolve host '{host}': {ex.Message}");
}
catch (ArgumentException ex)
{
return EgressDecision.Block("dns_failure", $"could not resolve host '{host}': {ex.Message}");
}
return _policy.CheckAddresses(host, addresses);
}
}