-
Notifications
You must be signed in to change notification settings - Fork 5.2k
Description
Background and motivation
In security-conscious or policy-driven environments, it's often necessary to inspect the resolved IP address of an outbound HTTP request before initiating the connection. This enables applications to enforce custom traffic governance rules such as blocking requests to internal subnets, applying geolocation constraints, or logging outbound targets for audit and compliance.
In my project, the service connects to external websites at the initiation of an untrusted external user. To uphold our anti-abuse policy, we must prevent traffic to internal IPs or restricted networks. However, HttpClient does not expose the resolved IP prior to connection, making it impossible to enforce these policies without bypassing the class entirely.
For one-shot requests, we're currently bypassing HttpClient
entirely using:
- Dns.GetHostAddressesAsync() for resolution
- TcpClient for socket connection
- SslStream for TLS
- Manual HTTP request construction
This approach works, but it's fragile and circumvents the benefits of HttpClient
including redirect handling, decompression, proxy support, and connection pooling.
API Proposal
API Proposal
Introduce a pluggable IP resolution mechanism in SocketsHttpHandler
to allow the caller to introduce their own DNS resolution before connection. The default behavior would remain unchanged unless explicitly configured.
public class SocketsHttpHandler : HttpMessageHandler
{
/// <summary>
/// Optional delegate for custom IP resolution. If set, this function is invoked
/// with the target hostname before establishing a connection.
/// </summary>
public Func<string, Task<IPAddress[]>>? ResolveIPAddressesAsync { get; set; }
}
Behavior Notes:
- If
ResolveIPAddressesAsync
isnull
, the default DNS resolution is used.- Alternatively, the default delegate could be
Dns.GetHostAddressesAsync
; settingnull
would then result in aNullReferenceException
.
- Alternatively, the default delegate could be
- If set, the delegate is invoked before connection, allowing inspection, filtering, or substitution of resolved IP addresses.
- The delegate may return multiple IPs. The handler selects one using the same logic as when the system DNS resolver returns multiple IPs.
- All other
HttpClient
features—such as redirects, proxy support, decompression, and connection pooling—remain fully functional. - The delegate should be thread-safe.
SocketsHttpHandler
does not enforce thread safety. - If the delegate throws an exception, it propagates out of the
HttpClient
call, leaving the handler in a consistent state for reuse. - If the delegate hangs beyond the configured timeout,
HttpClient
should throw an exception indicating that IP resolution timed out. - The delegate may implement its own caching logic.
SocketsHttpHandler
does not cache the delegate’s response.
API Usage
API Usage
var handler = new SocketsHttpHandler
{
ResolveIPAddressesAsync = async (host) =>
{
var addresses = await Dns.GetHostAddressesAsync(host);
foreach (var ip in addresses)
{
if (IsBlocked(ip))
throw new SecurityException($"Outbound IP {ip} blocked by policy.");
}
return addresses;
}
};
var client = new HttpClient(handler);
await client.GetAsync("https://example.com");
Alternative Designs
Alternative Designs
-
A simpler model could allow the caller to perform DNS resolution independently and pass a single
IPAddress
toHttpClient
. This would shift responsibility for resolution and selection entirely to the caller.
(In this situation, theHost
header would continue to be the host string from the URL as normal, not the supplied IP address.) -
Instead of returning an array, the delegate could return a single
IPAddress
. This would simplify the API but reduce flexibility in multi-IP scenarios (e.g., round-robin DNS, CDN edge nodes). Returning an array allows the caller to inspect all candidates and apply stricter policies if needed, while still enabling single-IP enforcement by returning a one-element array. -
An
OnIPAddressResolved
event could pass the selected IP to the configured delegate, which would either allow it or throw an exception. The caller code could not provide their own IP but would be empowered to prevent connections to IPs it prefers to avoid.
These alternatives offer varying degrees of control and complexity, but the proposed delegate-based model strikes a balance between flexibility, safety, and compatibility with existing HttpClient
behavior.
Risks
Risks
-
Increased API surface complexity: Introducing a delegate-based IP resolution hook adds another layer of configuration to
SocketsHttpHandler
, which may complicate usage for developers unfamiliar with DNS or traffic governance. -
Misuse or misunderstanding: Developers might use the delegate for purposes better served by other mechanisms (e.g., hostname validation, proxy routing), leading to fragile or inconsistent behavior.
-
Thread-safety concerns: If the delegate is not implemented with thread safety in mind, it could introduce concurrency issues in high-throughput scenarios. This risk is mitigated by clearly documenting that thread safety is the caller’s responsibility.
-
Timeout handling: If the delegate hangs or performs slow resolution, it could delay outbound requests. This risk is mitigated by enforcing timeouts and surfacing clear exceptions when resolution exceeds configured thresholds.
-
Caching inconsistencies: Custom resolvers may implement their own caching logic, which could diverge from system-level DNS caching behavior. This could lead to unexpected results unless clearly documented.
-
Security implications: Allowing custom resolution logic introduces a new surface for policy enforcement—but also for policy bypass if misconfigured. Careful documentation and default behavior safeguards are essential.