dotnet / dotnet/runtime

[API Proposal]: Allow a custom IP lookup function for `HttpClient`.

Open
#120,168 13 comments 0 reactions 0 assignees View on GitHub
api-suggestion area-System.Net.Http
Dominant language
C#
Stars
18.3k
Forks
5.6k
PR merge metrics
PR metrics pending

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.

```csharp
public class SocketsHttpHandler : HttpMessageHandler
{
///
/// Optional delegate for custom IP resolution. If set, this function is invoked
/// with the target hostname before establishing a connection.
///
public Func>? ResolveIPAddressesAsync { get; set; }
}
```

**Behavior Notes:**

- If `ResolveIPAddressesAsync` is `null`, the default DNS resolution is used.
- Alternatively, the default delegate could be `Dns.GetHostAddressesAsync`; setting `null` would then result in a `NullReferenceException`.
- 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**

```csharp
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` to `HttpClient`. This would shift responsibility for resolution and selection entirely to the caller.
*(In this situation, the `Host` 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.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.