Skip to content

Add macOS IP_BOUND_IF/IPV6_BOUND_IF socket binding for interface-specific UDP listeners#2040

Open
IngmarStein wants to merge 1 commit into
TechnitiumSoftware:developfrom
IngmarStein:feature/macos-ip-bound-if
Open

Add macOS IP_BOUND_IF/IPV6_BOUND_IF socket binding for interface-specific UDP listeners#2040
IngmarStein wants to merge 1 commit into
TechnitiumSoftware:developfrom
IngmarStein:feature/macos-ip-bound-if

Conversation

@IngmarStein

Copy link
Copy Markdown
Contributor

On macOS, the equivalent of Linux's SO_BINDTODEVICE for binding UDP
sockets to a specific network interface is the IP_BOUND_IF (IPv4) and
IPV6_BOUND_IF (IPv6) socket options. These take an interface index
instead of an interface name.

Changes

  • Add macOS socket option constants: IPPROTO_IP=0, IP_BOUND_IF=25,
    IPPROTO_IPV6=41, IPV6_BOUND_IF=125
  • Add P/Invoke for if_nametoindex(): resolves interface names (e.g. en0)
    to system interface indices on macOS
  • Extend UDP and UDP-proxy listener binding: the existing per-interface
    binding logic now supports macOS via
    RuntimeInformation.IsOSPlatform(OSPlatform.OSX), using the
    appropriate protocol level (IPPROTO_IP / IPPROTO_IPV6) based on
    AddressFamily

Ref: #2023

@IngmarStein IngmarStein force-pushed the feature/macos-ip-bound-if branch from d2731be to d2867c6 Compare July 11, 2026 18:09
@ShreyasZare

Copy link
Copy Markdown
Member

Thanks for the PR. Can you check if the network index value can be read using NetworkInterface.GetAllNetworkInterfaces() using GetIPProperties().GetIPv4Properties().Index call on macOS? It would be better to use the dotnet API instead of native calls.

@IngmarStein

Copy link
Copy Markdown
Contributor Author

I looked into using NetworkInterface.GetAllNetworkInterfaces() with GetIPProperties().GetIPv4Properties().Index instead of the if_nametoindex P/Invoke. Turns out the .NET runtime's own native shim on macOS calls the exact same if_nametoindex() under the hood — the .NET API doesn't provide an alternative mechanism, just a managed wrapper around the same POSIX call.

There are also a couple of practical issues with the .NET approach:

  • GetIPv4Properties() returns null for interfaces that don't have an IPv4 address configured. We'd need a fallback to GetIPv6Properties().Index for IPv6-only interfaces, which adds branching without benefit.
  • GetAllNetworkInterfaces() requires enumerating every interface just to find one by name, which is unnecessary when the POSIX function does a direct lookup.

The P/Invoke is safe on non-macOS platforms — .NET resolves DllImport lazily at call time, and it's only invoked inside the RuntimeInformation.IsOSPlatform(OSPlatform.OSX) guard. It will never trigger a library load on Windows or Linux.

Given that both paths end up at the same if_nametoindex call, I kept the simpler one. Happy to switch if you'd prefer the managed API though.

@ShreyasZare

Copy link
Copy Markdown
Member

Thanks for looking this up.

I looked into using NetworkInterface.GetAllNetworkInterfaces() with GetIPProperties().GetIPv4Properties().Index instead of the if_nametoindex P/Invoke. Turns out the .NET runtime's own native shim on macOS calls the exact same if_nametoindex() under the hood — the .NET API doesn't provide an alternative mechanism, just a managed wrapper around the same POSIX call.

I tend to avoid P/Invoke if there is equivalent option available with dotnet. It helps in long term, like if something changes, the runtime will have it updated and the app keeps working normally without additional maintenance.

There are also a couple of practical issues with the .NET approach:

  • GetIPv4Properties() returns null for interfaces that don't have an IPv4 address configured. We'd need a fallback to GetIPv6Properties().Index for IPv6-only interfaces, which adds branching without benefit.

This wont come up in this case since the interface will have at least one IP address as the code is attempting to bind on one of them.

  • GetAllNetworkInterfaces() requires enumerating every interface just to find one by name, which is unnecessary when the POSIX function does a direct lookup.

This is fine with me since the loop is not on the hot path and will happen only when the DNS server starts. The GetAllNetworkInterfaces() is quite fast to return too.

The P/Invoke is safe on non-macOS platforms — .NET resolves DllImport lazily at call time, and it's only invoked inside the RuntimeInformation.IsOSPlatform(OSPlatform.OSX) guard. It will never trigger a library load on Windows or Linux.

I try to avoid P/Invoke for all platforms unless there is no other way to achieve it. Best to let the runtime do it so there is nothing to worry about it even a decade later.

Given that both paths end up at the same if_nametoindex call, I kept the simpler one. Happy to switch if you'd prefer the managed API though.

I would really prefer to have it done with managed code.

On macOS, the equivalent of Linux's SO_BINDTODEVICE for binding UDP
sockets to a specific network interface is the IP_BOUND_IF (IPv4) and
IPV6_BOUND_IF (IPv6) socket options. These take an interface index
instead of an interface name.

Changes:
- Add macOS socket option constants: IPPROTO_IP, IP_BOUND_IF,
  IPPROTO_IPV6, IPV6_BOUND_IF
- Add P/Invoke for if_nametoindex() to resolve interface names to
  system interface indices on macOS
- Extend UDP and UDP-proxy listener binding to support macOS via
  RuntimeInformation.IsOSPlatform(OSPlatform.OSX)

Ref: TechnitiumSoftware#2023
@IngmarStein IngmarStein force-pushed the feature/macos-ip-bound-if branch from d2867c6 to c472e05 Compare July 13, 2026 10:35
@IngmarStein

Copy link
Copy Markdown
Contributor Author

I would really prefer to have it done with managed code.

No problem at all - done.

@ShreyasZare

Copy link
Copy Markdown
Member

Thanks for the quick update.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants