有時需要知道本機連向遠端伺服器實際走哪張網卡、哪個來源 IP(類似 `pathping` 第一跳)。透過 `Socket.IOControl(IOControlCode.RoutingInterfaceQuery, ...)` 可取得路由介面資訊。本文提供完整可用代碼,並深度吐槽 .NET 2.0 `SocketAddress`/`IPEndPoint` API 設計缺陷。
using System;
using System.Net;
using System.Net.Sockets;
public static class RoutingHelper
{
///
/// 取得連往 remoteEP 所使用的本機介面 IP
///
public static IPEndPoint GetRoutingInterface(Socket socket, IPEndPoint remoteEP)
{
// 1. 將遠端端點序列化為 SocketAddress (含 sockaddr 結構)
SocketAddress sa = remoteEP.Serialize();
// 2. 複製到 byte[] 供 IOControl 使用 (API 設計缺陷:不直接接受 SocketAddress)
byte[] inBytes = new byte[sa.Size];
for (int i = 0; i < sa.Size; i++) inBytes[i] = sa[i];
byte[] outBytes = new byte[inBytes.Length];
// 3. 呼叫 WSAIoctl (SIO_ROUTING_INTERFACE_QUERY = 0x98000010)
socket.IOControl(IOControlCode.RoutingInterfaceQuery, inBytes, outBytes);
// 4. 將結果寫回 SocketAddress
for (int i = 0; i < sa.Size; i++) sa[i] = outBytes[i];
// 5. 反序列化為 IPEndPoint (API 設計缺陷:需實例化 IPEndPoint 才能呼叫 Create)
return CreateIPEndPointFromSocketAddress(sa);
}
///
/// 修正版:靜態方法直接由 SocketAddress 建立 IPEndPoint
///
public static IPEndPoint CreateIPEndPointFromSocketAddress(SocketAddress sa)
{
if (sa.Size < 8) throw new ArgumentException("SocketAddress too small");
// IPv4: sa.Family == AddressFamily.InterNetwork (2)
if (sa.Family == AddressFamily.InterNetwork)
{
// sa[2..3] = Port (big-endian)
int port = (sa[2] << 8) | sa[3];
// sa[4..7] = IP Address (big-endian)
long addr = ((long)sa[4] << 24) | ((long)sa[5] << 16) | ((long)sa[6] << 8) | sa[7];
return new IPEndPoint(addr, port);
}
// IPv6: sa.Family == AddressFamily.InterNetworkV6 (23)
else if (sa.Family == AddressFamily.InterNetworkV6)
{
int port = (sa[2] << 8) | sa[3];
uint scopeId = (uint)((sa[24] << 24) | (sa[25] << 16) | (sa[26] << 8) | sa[27]);
byte[] ipBytes = new byte[16];
for (int i = 0; i < 16; i++) ipBytes[i] = sa[8 + i];
return new IPEndPoint(new IPAddress(ipBytes, scopeId), port);
}
throw new NotSupportedException($"AddressFamily {sa.Family} not supported");
}
}
public static async Task Main()
{
using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
socket.Bind(new IPEndPoint(IPAddress.Any, 10001)); // 綁定本地端口
var remote = new IPEndPoint(
(await Dns.GetHostAddressesAsync("www.google.com"))[0],
80);
var localInterface = RoutingHelper.GetRoutingInterface(socket, remote);
Console.WriteLine($"目標: {remote}");
Console.WriteLine($"路由介面 (本機源 IP): {localInterface}");
}
[DllImport("iphlpapi.dll", SetLastError = true)]
static extern int GetBestInterface(UInt32 destAddr, out UInt32 bestIfIndex);
[DllImport("iphlpapi.dll", SetLastError = true)]
static extern int GetBestRoute(UInt32 destAddr, UInt32 sourceAddr, out MIB_IPFORWARD_ROW bestRoute);
// 使用範例
uint dest = BitConverter.ToUInt32(IPAddress.Parse("8.8.8.8").GetAddressBytes(), 0);
GetBestInterface(dest, out uint ifIndex);
Console.WriteLine($"最佳介面索引: {ifIndex}");
此 API 更直接、無需建立 Socket、支援 IPv4/IPv6、可取得介面索引進而查詢 `NetworkInterface.GetAllNetworkInterfaces()` 詳細資訊。
`Socket.IOControl(RoutingInterfaceQuery)` 可查詢路由介面,但 .NET 2.0 API 設計**極不友善**:`byte[]` 強制複製、`SocketAddress` 缺乏轉換方法、`IPEndPoint.Create` 實例方法怪癖。現代 .NET 部分改善,但**建議優先使用 IP Helper API (`GetBestInterface`/`GetBestRoute`)** 或封裝良好的第三方庫。理解這些歷史包袱,有助於在維護舊代碼時避坑,並在設計新 API 時吸取教訓:**強型別、不可變、靜態工廠、語意清晰**。