获取默认网关

28

我正在编写一个程序,用于显示用户的IP地址、子网掩码和默认网关。我可以获取前两者,但对于最后一项,这是我找到的:

GatewayIPAddressInformationCollection gwc = 
    System.Net.NetworkInformation.NetworkInterface.GetAllNetworkInterfaces()[0].GetIPProperties().GatewayAddresses;
当然,这会返回一个GatewayIPAddressInformation的集合。那么,如果计算机有多个网关,如何确定哪个是默认网关呢?
实际上,我只看到过这个集合包含一个条目,但由于它被实现为集合,理论上有些计算机包含多个网关,没有一个标记为“默认”。那么有没有办法确定默认网关,还是全凭猜测?

你的意思是如何确定默认适配器? - SLaks
2
我的假设是它们按重要性顺序排列...我会通过一些静态IP设置来验证,并进行彻底的测试。 - mckeejm
你可以在这里查看我的回答:https://dev59.com/2mYr5IYBdhLWcg3wfaMA#36149660 - aliep
9个回答

49

它很可能是第一个有效且启用的网卡接口的第一个启用网络接口地址:

public static IPAddress GetDefaultGateway()
{
    return NetworkInterface
        .GetAllNetworkInterfaces()
        .Where(n => n.OperationalStatus == OperationalStatus.Up)
        .Where(n => n.NetworkInterfaceType != NetworkInterfaceType.Loopback)
        .SelectMany(n => n.GetIPProperties()?.GatewayAddresses)
        .Select(g => g?.Address)
        .Where(a => a != null)
         // .Where(a => a.AddressFamily == AddressFamily.InterNetwork)
         // .Where(a => Array.FindIndex(a.GetAddressBytes(), b => b != 0) >= 0)
        .FirstOrDefault();
}

我还添加了一些注释性检查,这些检查被其他人指出非常有用。你可以检查AddressFamily来区分IPv4和IPv6。后者可用于过滤掉0.0.0.0地址。

上述解决方案将为您提供有效/已连接的接口,并且在99%的情况下足够好。也就是说,如果您有多个有效接口可以路由流量,并且您需要100%准确,则使用GetBestInterface来寻找路由到特定IP地址的接口是实现此目的的方式。这还处理了可能通过其他适配器(例如,10.*.*.*通过VPN,其他所有内容都通过路由器)路由特定地址范围的情况。

[DllImport("iphlpapi.dll", CharSet = CharSet.Auto)]
private static extern int GetBestInterface(UInt32 destAddr, out UInt32 bestIfIndex);

public static IPAddress GetGatewayForDestination(IPAddress destinationAddress)
{
    UInt32 destaddr = BitConverter.ToUInt32(destinationAddress.GetAddressBytes(), 0);

    uint interfaceIndex;
    int result = GetBestInterface(destaddr, out interfaceIndex);
    if (result != 0)
        throw new Win32Exception(result);

    foreach (var ni in NetworkInterface.GetAllNetworkInterfaces())
    {
        var niprops = ni.GetIPProperties();
        if (niprops == null)
            continue;

        var gateway = niprops.GatewayAddresses?.FirstOrDefault()?.Address;
        if (gateway == null)
            continue;

        if (ni.Supports(NetworkInterfaceComponent.IPv4))
        {
            var v4props = niprops.GetIPv4Properties();
            if (v4props == null)
                continue;

            if (v4props.Index == interfaceIndex)
                return gateway;
        }

        if (ni.Supports(NetworkInterfaceComponent.IPv6))
        {
            var v6props = niprops.GetIPv6Properties();
            if (v6props == null)
                continue;

            if (v6props.Index == interfaceIndex)
                return gateway;
        }
    }

    return null;
}

这两个示例可以被封装成一个帮助类,并在合适的情况下使用:您已经有目的地地址,或者还没有。


6
我可能不是第一个。在我的测试中,第一个已被禁用,因此第二个是正确的。因此在LINQ语句中添加一个条件:Where(o=>o.OperationalStatus==OperationalStatus.Up)。 - Wolf5
1
我更愿意确保您拥有具有网关的接口。var gateway = NetworkInterface.GetAllNetworkInterfaces().Where(e => e.OperationalStatus == OperationalStatus.Up).SelectMany(e => e.GetIPProperties().GatewayAddresses).FirstOrDefault(); - midspace
不能保证它一定是第一个。 - Alborz
这个不起作用。它会返回 0.0.0.0。你需要将其过滤掉。 - user541686
@Mehrdad:我无法在我的系统上重现这个0.0.0.0地址。你能描述一下发生这种情况的情形以及你是如何解决的吗?我会更新我的答案。 - caesay
显示剩余2条评论

4

traceroute 命令返回的第一个 IP 地址通常就是网关,可以利用这个事实来获取网关。一个很好的 tracerout 实现可以在这里找到:TraceRoute and Ping in C#


3
根据 @midspace 对 @caesay 回答的评论,这是一个更好的答案:
public static IPAddress GetDefaultGateway()
{
    var gateway_address = NetworkInterface.GetAllNetworkInterfaces()
        .Where(e => e.OperationalStatus == OperationalStatus.Up)
        .SelectMany(e => e.GetIPProperties().GatewayAddresses)
        .FirstOrDefault();
    if (gateway_address == null) return null;
    return gateway_address.Address;
}

我警告这不是一个完整的解决方案,如果你正在寻找负责互联网接入的主要界面,你应该结合其他方法,比如使用win32 API中的GetBestInterface来查找连接到目标地址的最佳接口。

你可以在这里找到使用示例:http://www.pinvoke.net/default.aspx/iphlpapi.getbestinterface


2
我知道这是一个稍微老一点的问题,但是我刚刚需要检索本地网关的IPV4地址。 接受的答案在我的系统上不太适用,因此我进行了修改,并且我相信其他人也能使用这个解决方案。
由于我还没有足够的声望来发表评论,所以我被迫将其添加为“问题”。
public static IPAddress GetDefaultGateway()
    {
        IPAddress result = null;
        var cards = NetworkInterface.GetAllNetworkInterfaces().ToList();
        if (cards.Any())
        {
            foreach (var card in cards)
            {
                var props = card.GetIPProperties();
                if (props == null)
                    continue;

                var gateways = props.GatewayAddresses;
                if (!gateways.Any())
                    continue;

                var gateway =
                    gateways.FirstOrDefault(g => g.Address.AddressFamily.ToString() == "InterNetwork");
                if (gateway == null)
                    continue;

                result = gateway.Address;
                break;
            };
        }

        return result;
    }

我也遇到了同样的问题,但这个运行得很好,谢谢! - Jos Lemmerling
4
一个快速提示--查找第一个特定为IPv4地址的网关的代码,'g.Address.AddressFamily.ToString() =="InterNetwork"',最好写成枚举值比较而不是字符串值比较:g.Address.AddressFamily == AddressFamily.InterNetwork. - Jonathan Gilbert

2

我刚了解到这个,并将使用以下代码 - 基本上它查找不是环回的接口,已经启动并且具有网关和单播地址。然后从接口中获取非瞬态IPV4地址。

public static class NetworkInterfaces
{
    public static NetworkInterface GetDefaultInterface()
    {
        var interfaces = System.Net.NetworkInformation.NetworkInterface.GetAllNetworkInterfaces();
        foreach (var intf in interfaces)
        {
            if (intf.OperationalStatus != OperationalStatus.Up)
            {
                continue;
            }
            if (intf.NetworkInterfaceType == NetworkInterfaceType.Loopback)
            {
                continue;
            }

            var properties = intf.GetIPProperties();
            if (properties == null)
            {
                continue;
            }
            var gateways = properties.GatewayAddresses;
            if ((gateways == null) || (gateways.Count == 0))
            {
                continue;
            }
            var addresses = properties.UnicastAddresses;
            if ((addresses == null) || (addresses.Count == 0))
            {
                continue;
            }
            return intf;
        }
        return null;
    }

    public static IPAddress GetDefaultIPV4Address(NetworkInterface intf)
    {
        if (intf == null)
        {
            return null;
        }
        foreach (var address in intf.GetIPProperties().UnicastAddresses)
        {
            if (address.Address.AddressFamily != AddressFamily.InterNetwork)
            {
                continue;
            }
            if (address.IsTransient)
            {
                continue;
            }
            return address.Address;
        }
        return null;
    }
}

2

一直在寻找一种奇怪的技巧来确定本地IP地址吗?然而,与之前的方法相比,它非常稳定?

[请查看最后对@caesay的回复]

这可能就是你要找的。

using (var s = new System.Net.Sockets.Socket(AddressFamily.InterNetwork, SocketType.Dgram,ProtocolType.Udp))
{
    s.Bind( new System.Net.IPEndPoint(System.Net.IPAddress.Any,0));
    s.Connect("google.com",0);

    var ipaddr = s.LocalEndPoint as System.Net.IPEndPoint;
    var addr = ipaddr.Address.ToString();
    Console.WriteLine(addr);
}

那将创建一个UDP套接字,但不会在其上发送数据。然后,您可以查看套接字绑定到的本地接口和地址。您必须提供一个IP地址或主机名,我使用了google.com,这将由操作系统用于确定套接字将绑定到哪个接口以及使用此方法将找到哪个IP地址。对于99%以上的IP地址或主机名,您使用的接口+地址将用于默认路由上的流量。
虽然这有点难懂,但我认为它比上面的方法更可靠,人们使用PInvoke调用并扫描结构以查找接口的方法。
在我的3倍双接口系统中,我肯定发现它比使用启发式来尝试找出默认接口地址的NetworkInterface.GetAllNetworkInterfaces()结果要更健壮。
[更新/回应@caesay 2/6/2019] @caesay在这里提出了一个很好的观点,因此我已经从使用UdpClient切换到Socket,并在Bind()之后显式调用了Socket.Connect()。我还审查了Connect()和LocalEndPoint的框架源代码,并且两者都立即调用其相应的Win32 OS系统调用,而不是推迟或懒惰地调用OS。 Win32 bind页面说“应用程序可以在调用bind后使用getsockname来了解已分配给套接字的地址和端口。如果Internet地址等于INADDR_ANY或in6addr_any,则getsockname不能保证在套接字连接之前提供地址”。现在明确解决了这个条件,并且在审查了框架源代码后,IP地址应始终在Connect()调用后可用。

如果没有对参考源进行广泛的审查和测试,我会非常谨慎地使用它。UdpClient的实现只是打开一个IPv4或IPv6套接字(取决于远程ep),然后调用socket.Bind(new IPEndPoint(IPAddress.Any, port))。我没有逐步查找框架代码以找到IPAddress.Any被转换为您期望的IP地址的位置,但这种情况是可以想象的,它并不总是按照这种方式工作。 - caesay
@caesay,是的,非常好的观点!我做了一些更改,并审查了 .Net Framework 以确认 Socket.Connect() 和 LocalEndPoint 轻松包装了 OS 系统调用:WSAConnect() 和 getsockname(),它们确实如此,使用这种方法应该没有太大风险。需要注意的是,Windows 和 Linux 在连接系统调用时将接口分配给套接字。 - Cameron

1
NetworkInterface[] allNICs = NetworkInterface.GetAllNetworkInterfaces();
foreach (var nic in allNICs)
{
    var ipProp = nic.GetIPProperties();
    var gwAddresses = ipProp.GatewayAddresses;
    if (gwAddresses.Count > 0 &&
        gwAddresses.FirstOrDefault(g => g.Address.AddressFamily == AddressFamily.InterNetwork) != null)
    {
        localIP = ipProp.UnicastAddresses.First(d => d.Address.AddressFamily == AddressFamily.InterNetwork).Address;
    }
}
Debug.Print(localIP.ToString());

这是针对IPv4的。如果您需要IPv6网关,请更改AddressFamily。 - Wayne

1

这里有一种不同的方法,通过使用类似于traceroute的方法来查找默认网关。使用TTL为1的地址在互联网(或其他您想要到达的网络)上进行ping,并让ping告诉您第一个跳跃的IP地址。那应该就是您要找的网关。

我想如果网关被配置为不回复ping,这可能会出现问题,但我认为我从未听说过设置网络的方式。

注意:即使您传递给它一个IPV6地址,这个例程也可以正常工作。潜在的问题是它似乎返回具有全局范围而不是链路本地范围的IPV6地址。我没有足够的使用IPV6的经验来知道这是好还是坏。但我知道Windows ipconfig命令显示接口网关的链路本地IPV6地址。

using System.Net
using System.Net.NetworkInformation

// ping internet address with ttl=1 and return address of host where ttl expired
public static IPAddress FindDefaultGateway(IPAddress netaddr = null)
{
    // user can provide an ip address that exists on the network they want to connect to, 
    // or this routine will default to 1.1.1.1 (IP of a popular internet dns provider)
    if (netaddr is null)
        netaddr = IPAddress.Parse("1.1.1.1"); 

    PingReply reply = default;  
    using var ping = new Ping();
    var options = new PingOptions(1, true); // ttl=1, dont fragment=true
    try { 
        // I arbitrarily used a 200ms timeout; tune as you see fit.
        reply = ping.Send(netaddr, 200, new byte[0], options); 
    }
    catch (PingException) {
        System.Diagnostics.Debug.WriteLine("Gateway not available");
        return default;
    }
    if (reply.Status != IPStatus.TtlExpired) {
        System.Diagnostics.Debug.WriteLine("Gateway not available");
        return default;
    }
    return reply.Address;
}

0

我认为你应该迭代这个集合并显示所有的网关,因为如果有多个网关连接到一个适配器,则它们都被视为该适配器的默认网关。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接