如何获取与TCP套接字关联的接口名称/索引?

14
我正在编写一个TCP服务器,需要知道每个连接所使用的接口。由于可能存在具有相同地址/子网的接口,因此我不能使用地址/子网来推断使用的接口。它是基于Linux的,代码无需可移植性。
我找到的所有函数都是用于获取所有接口或通过索引获取单个接口,但我找不到任何方法来获取与已接受的TCP套接字关联的接口。
有什么想法吗?我错过了什么吗?
编辑:再次强调,在我的情况下IP地址不是唯一的。目的地地址(服务器本身)和源地址(客户端)均如此。是的,这是非常极端的IP方案。

这里唯一正确的答案,考虑到你正在寻找要添加到服务器的代码,是来自User1的答案。我已经测试过它,它可以正常工作 - 你应该接受它。另请参阅http://stackoverflow.com/questions/43659634/find-the-interface-used-by-a-connected-socket/43663713#43663713 - EML
@EML 目前没有 User1。 - Aman Deep Gautam
@AmanDeepGautam:不太确定,但我认为应该是SKi。我给他点了赞,代码看起来很熟悉。 - EML
9个回答

13

使用getsockname()来获取TCP连接本地端的IP地址,然后使用getifaddrs()找到对应的接口。

struct sockaddr_in addr;
struct ifaddrs* ifaddr;
struct ifaddrs* ifa;
socklen_t addr_len;

addr_len = sizeof (addr);
getsockname(sock_fd, (struct sockaddr*)&addr, &addr_len);
getifaddrs(&ifaddr);

// look which interface contains the wanted IP.
// When found, ifa->ifa_name contains the name of the interface (eth0, eth1, ppp0...)
for (ifa = ifaddr; ifa != NULL; ifa = ifa->ifa_next)
{
    if (ifa->ifa_addr)
    {
        if (AF_INET == ifa->ifa_addr->sa_family)
        {
            struct sockaddr_in* inaddr = (struct sockaddr_in*)ifa->ifa_addr;

            if (inaddr->sin_addr.s_addr == addr.sin_addr.s_addr)
            {
                if (ifa->ifa_name)
                {
                    // Found it
                }
            }
        }
    }
}
freeifaddrs(ifaddr);

以上只是一个简单的示例,需要进行一些修改:

  1. 添加缺失的错误检查
  2. 支持IPv6

请注意,这个 if (inaddr->sin_addr.s_addr == addr.sin_addr.s_addr) 可能永远不会匹配。我建议您应用子网掩码来稍微提高一点机会:if (inaddr->sin_addr.s_addr == (addr.sin_addr.s_addr & ((struct sockaddr_in*)ifa->ifa_netmask)->sin_addr.s_addr)) - Alexis Wilke
@AlexisWilke:我们在生产软件中多年来一直使用这种if语句而没有出现问题。我不明白为什么它不会与列表中的任何项匹配,因为'inaddr'包含本地(而不是对等或子网)IP地址。 - SKi
在我的情况下,我想找到与任何地址对应的接口,因此我可以拥有192.168.3.111,它不会匹配192.168.3.0,除非我先应用接口的掩码。 - Alexis Wilke
多个接口可以属于同一IP网络,甚至多个接口可以拥有相同的IP地址。哪个接口用于发送流量只能通过查看路由表来确定。 - Mecki

5

以下是一段用于查找套接字接口名称的C++11代码:

std::string to_string(sockaddr_in const& addr)
{
    char buf[INET_ADDRSTRLEN];
    if (inet_ntop(AF_INET, &addr.sin_addr, buf, sizeof(buf)) == nullptr)
    {
        std::clog << "inet_ntop: " << strerror(errno) << '\n';
        return {};
    }
    return buf;
}

std::string to_string(sockaddr_in6 const& addr)
{
    char buf[INET6_ADDRSTRLEN];
    if (inet_ntop(AF_INET6, &addr.sin6_addr, buf, sizeof(buf)) == nullptr)
    {
        std::clog << "inet_ntop: " << strerror(errno) << '\n';
        return {};
    }
    return buf;
}

std::string to_string(sockaddr_storage const& addr, socklen_t len)
{
    switch (addr.ss_family)
    {
    case AF_INET:
    {
        auto& a = reinterpret_cast<sockaddr_in const&>(addr);
        if (len < sizeof(a))
        {
            std::clog << "Invalid sockaddr length: " << len << '\n';
            return {};
        }
        return to_string(a);
    }
    case AF_INET6:
    {
        auto& a = reinterpret_cast<sockaddr_in6 const&>(addr);
        if (len < sizeof(a))
        {
            std::clog << "Invalid sockaddr length: " << len << '\n';
            return {};
        }
        return to_string(a);
    }
    default:
    {
        std::clog << "Invalid sockaddr family: " << addr.ss_family << '\n';
        return {};
    }
    }
}

std::string get_iface_name(sockaddr_in const& addr)
{
    ifaddrs *ifa = nullptr;
    if (getifaddrs(&ifa) == -1)
    {
        std::clog << "getifaddrs: " << strerror(errno) << '\n';
        return {};
    }
    std::unique_ptr<ifaddrs, void(*)(ifaddrs*)>
        finally{ifa, freeifaddrs};

    for (; ifa; ifa = ifa->ifa_next)
    {
        if (!ifa->ifa_addr)
            continue;
        if (!ifa->ifa_name)
            continue;
        if (ifa->ifa_addr->sa_family != AF_INET)
            continue;
        auto& a = reinterpret_cast<sockaddr_in&>(*ifa->ifa_addr);
        if (a.sin_addr.s_addr == addr.sin_addr.s_addr)
            return ifa->ifa_name;
    }

    std::clog << "No interface found for IPv4 address " << to_string(addr) << '\n';
    return {};
}

std::string get_iface_name(sockaddr_in6 const& addr)
{
    ifaddrs *ifa = nullptr;
    if (getifaddrs(&ifa) == -1)
    {
        std::clog << "getifaddrs: " << strerror(errno) << '\n';
        return {};
    }
    std::unique_ptr<ifaddrs, void(*)(ifaddrs*)>
        finally{ifa, freeifaddrs};

    for (; ifa; ifa = ifa->ifa_next)
    {
        if (!ifa->ifa_addr)
            continue;
        if (!ifa->ifa_name)
            continue;
        if (ifa->ifa_addr->sa_family != AF_INET6)
            continue;
        auto& a = reinterpret_cast<sockaddr_in6&>(*ifa->ifa_addr);
        if (memcmp(a.sin6_addr.s6_addr,
                   addr.sin6_addr.s6_addr,
                   sizeof(a.sin6_addr.s6_addr)) == 0)
            return ifa->ifa_name;
    }

    std::clog << "No interface found for IPv6 address " << to_string(addr) << '\n';
    return {};
}

std::string get_iface_name(sockaddr_storage const& addr, socklen_t len)
{
    switch (addr.ss_family)
    {
    case AF_INET:
    {
        auto& a = reinterpret_cast<sockaddr_in const&>(addr);
        if (len < sizeof(a))
        {
            std::clog << "Invalid sockaddr length: " << len << '\n';
            return {};
        }
        return get_iface_name(a);
    }
    case AF_INET6:
    {
        auto& a = reinterpret_cast<sockaddr_in6 const&>(addr);
        if (len < sizeof(a))
        {
            std::clog << "Invalid sockaddr length: " << len << '\n';
            return {};
        }
        return get_iface_name(a);
    }
    default:
    {
        std::clog << "Invalid sockaddr family: " << addr.ss_family << '\n';
        return {};
    }
    }
}

std::string get_iface_name(int sockfd)
{
    sockaddr_storage addr;
    socklen_t len = sizeof(addr);
    if (getsockname(sockfd, (sockaddr*)&addr, &len) == -1)
    {
        std::clog << "getsockname: " << strerror(errno) << '\n';
        return {};
    }
    std::clog << "getsockname '" << to_string(addr, len) << '\'' << '\n';
    return get_iface_name(addr, len);
}

如果我将路径设置为指向不同的接口,则失败。决定流量将采取哪个接口的过程称为路由,路由表是相关数据源。 - Mecki

4
一般来说,您不需要知道数据包将要发送/接收的接口;这是内核路由表的工作。对于套接字而言,很难找到与之直接关联的接口,因为数据包的路由信息可能会在套接字生命周期内发生变化。
对于数据报(UDP)套接字,您可以尝试使用getsockopt(s, IPPROTO_IP, IP_PKTINFO, ...);请参见getsockopt(2)ip(7)
对于流(TCP)套接字,一种方法可能是为系统上的每个接口打开多个侦听套接字,并使用setsockopt(s, SOL_SOCKET, SO_BINDTODEVICE, ...)将每个套接字绑定到一个接口上;请参见setsockopt(2)socket(7)

是的,为每个接口设置单独的套接字可能会起作用,但考虑到有数十个接口并且它们会动态上下线,我想避免这种情况。 - Leeor

2
内核路由表决定将数据包发送到哪个接口,因此可以绑定设备。浏览“Linux Socket Programming, Warren W. Gay”时,指定接口是不好的,由于内核的动态性(防火墙、转发),这更加复杂。
建议更改IP方案,使IP信息通过查找方式告诉您接口,就像ifconfig一样,否则从设计角度来看,您会自寻烦恼。
1)从TCP会话中获取IP信息 2)查找这些信息可能适用的接口
尽管如此,我仍将在内核API中继续查找。您不需要知道这些,抽象存在有许多充分的理由。
额外的想法: 思考一下,如果两个接口使用相同的IP,则必须存在客户端地址范围路由差异(否则两个接口都将被使用)。您的服务器可以根据客户端IP检查路由表。

客户端IP也可以重叠。允许这些IP重叠的区别是VLAN ID。据我所知,那也是无法访问的。 - Leeor
你可能已经发现了一个情况,即无法使用间接证据来确定接口。我认为没有办法,因为接口在高于内核的应用程序中有点抽象。也许你可以修改内核通过/proc/tcp_sessions_to_devs提供一些数据,但那是非常绝望的做法。 - Aiden Bell
另一个想法。尝试查看Wireshark使用的接口嗅探库(libpcap),并查看iftop源代码如何枚举设备上的会话。可能很有趣。 - Aiden Bell
你只能通过查看路由表而非接口配置来确定接口。尽管多个接口可能连接到同一IP网络,多个接口甚至可以拥有相同的IP地址。无论流量通常经过哪个接口,我都可以添加路由并强制其通过不同的接口。决定数据包离开系统的接口是路由表,而不是其他任何东西。接口设置只是在路由表中创建条目,但只有路由表知道真相。 - Mecki

1

我认为在接受传入连接后使用getsockname()可能是你想要的。这两个函数getsockname()和getpeername()分别获取套接字绑定的本地和远程地址。对于完全连接的TCP套接字,两者都应该有效。

编辑:虽然根据OpenBSD的man页面似乎是正确的,但Linux的man页面差异很大,因此在Linux上接受连接后使用getsockname()几乎肯定没有用。教训是我应该检查一切而不是依靠记忆。 叹气


实际上,我已经尝试过这个解决方案,在Linux和Windows中都有效。 - thang
错误的“name” - 这会返回地址。 OP想要接口名称,即eth0 / etc。正如glibc文档所说,“用于处理套接字地址的函数和符号命名不一致,有时使用术语'name',有时使用'address'”。 - EML

0

显然,这不是我深入研究过或尝试过的东西,但这可能是一个“如此疯狂以至于可能奏效”的选择...

如果它只会在Linux上使用,您可以编写一个自定义netfilter模块来跟踪传入连接并记录它们进入的接口,并将该信息写入某个位置供服务器应用程序读取。


0

Kieron 提出编写 netfilter 模块可能是一种尝试的方式,但我不想为此解决方案编写我的第一个内核模块。

我想到了另一种选项,即使用源地址转换 (source NAT) 并将连接的源端口进行转换以与连接源相关联。我可以为每个网络分配端口范围并在服务器中检查它。唯一的问题是 iptables 中的源地址转换是在 POSTROUTING 链中完成的,而且我不确定它是否用于被该主机接受的连接,因此我可能需要使用另一台服务器。

这里没有简单的解决方案,太糟糕了,我无法从套接字获取接口名称/索引...


另一个netfilter选项是将流量转发到已设置具有不同IP地址的其他本地接口。 - Bell

0

看目标地址。

每个接口通常绑定到唯一的地址。如果多个接口被绑定在一起,使用哪一个可能并不重要。

唯一的例外是使用ipv6 anycast时,但即使如此,在同一主机上通常也不会有多个具有相同ip的接口。


在这种情况下,接口没有绑定到唯一的地址。它们也没有绑定。它们连接到不同的网络,尽管它们具有重叠/相同的IP地址。 - Leeor
如果您有一个TCP连接,那么您必须拥有一个IP地址。您不必将套接字绑定到特定的地址,但是您的接口需要一个。 - JimB
是的,连接有一个IP地址,但它不是唯一的。它不是该接口的唯一标识,也不是该网络的唯一标识。我真的需要一种从套接字本身获取接口的方法。 - Leeor
看起来你的情况比较特殊。我会保留我的回答,因为它对大多数人的情况都是合理的,而且@Aiden已经回答了我要补充的内容。 - JimB

0

我在查看Wireshark和iftop的源代码后,添加了另一个答案和潜在解决方案,它们似乎间接具有类似的功能。

在我看来,您可以使用libpcap来嗅探接口。假设您可以识别TCP/IP会话的某些唯一部分,那么您可以使用过滤器和会话跟踪将其追踪到接口。

没有内核模块(并且它与线程兼容)

http://www.ex-parrot.com/pdw/iftop/ 这里有一些简单的源代码供您参考 www.tcpdump.org/ 用于libpcap

我认为您也可以使用它来匹配VLAN。

此外,wireshark可能对调试有用。希望这可以帮助您!自从以来,这一直在我的脑海中。


是的,这听起来像另一个选项。感谢你的努力! - Leeor

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