Skip to content

DotNettyTransport.ResolveNameAsync selects link-local (169.254.x.x) APIPA addresses from DNS results, preventing cluster formation on multi-NIC Windows hosts #8178

@Arkatufus

Description

@Arkatufus

Version Information

Version of Akka.NET: 1.5.x (behavior confirmed on current dev branch, unchanged since introduction)
Which Akka.NET Modules: Akka.Remote (DotNetty transport)

Describe the bug

On Windows hosts with multiple network adapters — common in environments with VPN clients, Hyper-V/WSL virtual switches, or any configuration where an adapter can fall back to APIPA (169.254.0.0/16) when DHCP fails — Dns.GetHostEntryAsync can return multiple IPAddress entries for a single hostname, and one or more of those entries may be a link-local/APIPA address.

DotNettyTransport.ResolveNameAsync unconditionally selects the last address in the filtered list:

https://github.com/akkadotnet/akka.net/blob/dev/src/core/Akka.Remote/Transport/DotNetty/DotNettyTransport.cs

private async Task<IPEndPoint> ResolveNameAsync(DnsEndPoint address, AddressFamily addressFamily)
{
    var resolved = await Dns.GetHostEntryAsync(address.Host).ConfigureAwait(false);
    var found = resolved.AddressList.LastOrDefault(a => a.AddressFamily == addressFamily);
    if (found == null)
    {
        throw new KeyNotFoundException(\$\"Couldn't resolve IP endpoint from provided DNS name '{address}' with address family of '{addressFamily}'\");
    }
    return new IPEndPoint(found, address.Port);
}

When the last matching entry is a link-local address (169.254.x.x), the transport binds to or attempts to connect to an unreachable address. Cluster formation fails with no automatic fallback to other valid addresses in the AddressList.

The LastOrDefault selection appears to be a legacy choice from the original Helios transport — the earlier non-filtering overload contains a comment:

//NOTE: for some reason while Helios takes first element from resolved address list
// on the DotNetty side we need to take the last one in order to be compatible

This is not a deliberate technical selection — it's a backward-compatibility decision from a predecessor transport, and it has no defense against link-local addresses appearing at the end of the DNS result list.

To Reproduce

  1. Configure a Windows host with at least two active network adapters where one adapter has an APIPA (169.254.x.x) address assigned — easy to reproduce by having a VPN adapter not yet connected or a DHCP-failed secondary NIC
  2. Ensure Windows dynamic DNS registration is enabled on all adapters (default behavior for domain-joined machines), so the APIPA address is registered into the DNS zone
  3. Configure Akka.Remote with hostname / public-hostname set to the machine's FQDN (required for VPN/split-horizon/ZTNA scenarios where IP literals cannot be used)
  4. Start the actor system → bind or outbound seed connection attempts use the APIPA address → cluster formation fails

Expected behavior

ResolveNameAsync should filter out addresses that are fundamentally unreachable from remote cluster members:

  • IPv4 link-local (169.254.0.0/16) — RFC 3927, not routable off-link
  • Loopback (127.0.0.0/8 / ::1) — never reachable from another host
  • IPv6 link-local (fe80::/10)
  • Multicast / unspecified (0.0.0.0)

When multiple valid addresses remain, ideally attempt connection to each in order rather than committing to a single selection. At minimum, log a warning when link-local addresses are filtered so operators can diagnose the underlying network configuration.

Actual behavior

The last matching address is selected unconditionally, including link-local addresses, and no retry is attempted against other valid entries in AddressList.

Proposed fix

private async Task<IPEndPoint> ResolveNameAsync(DnsEndPoint address, AddressFamily addressFamily)
{
    var resolved = await Dns.GetHostEntryAsync(address.Host).ConfigureAwait(false);

    var candidates = resolved.AddressList
        .Where(a => a.AddressFamily == addressFamily)
        .Where(a => !IPAddress.IsLoopback(a))
        .Where(a => !IsIPv4LinkLocal(a))
        .Where(a => a.AddressFamily != AddressFamily.InterNetworkV6 || !a.IsIPv6LinkLocal)
        .ToArray();

    var found = candidates.LastOrDefault()
        ?? resolved.AddressList.LastOrDefault(a => a.AddressFamily == addressFamily);  // fallback preserves prior behavior

    if (found == null)
    {
        throw new KeyNotFoundException(\$\"Couldn't resolve IP endpoint from provided DNS name '{address}' with address family of '{addressFamily}'\");
    }
    return new IPEndPoint(found, address.Port);
}

private static bool IsIPv4LinkLocal(IPAddress ip)
{
    if (ip.AddressFamily != AddressFamily.InterNetwork) return false;
    var bytes = ip.GetAddressBytes();
    return bytes[0] == 169 && bytes[1] == 254;
}

A warning should be logged when link-local addresses are filtered, to help operators identify misbehaving adapters or dynamic DNS registration issues on multi-NIC hosts.

Environment

  • OS: Windows Server (any supported version); issue occurs wherever multi-NIC + dynamic DNS registration can produce APIPA entries in DNS results
  • .NET version: Any supported Akka.Remote target framework
  • Akka.NET version: 1.5.x (behavior unchanged on dev)

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions