linux - 如何绑定(bind)到只有一个网络接口(interface)(Linux)的所有地址?

标签 linux networking ipv6 tcp sockets

我想要实现的是将 IPv6 套接字绑定(bind)到任何地址只是一个特定设备,而不是系统范围的。我的直觉是,我可以使用 SO_BINDTODEVICEsetsockopt(),然后绑定(bind)到 ::。它主要做我期望它做的事。 v4 中的行为相同。

使用 SO_BINDTODEVICE 绑定(bind)到接口(interface)的套接字将只接受与该接口(interface)上的地址建立的连接。这是意料之中的事。

但是,如果我尝试绑定(bind)到接口(interface) B 上的源端口,而接口(interface) A 上有一个使用相同端口的套​​接字,我会遇到错误号“地址已在使用中” .

例如:

  • 网卡 A 有 IPv6 fd00:aaaa::a/64
  • 网卡 B 有 IPv6 fd00:bbbb::b/64
  • 他们不共享网络。

简而言之(伪代码):

  • 进程 1 调用 socket(...) 并绑定(bind) bind(fd00:aaaa::a/64, 9000)
  • 进程 2 调用 socket(...)setsockopt(SO_BINDTODEVICE, "B")
  • 进程 2(续)调用 bind(::, 9000) 并获取 EADDRINUSE。为什么?

SO_BINDTODEVICE 是如何工作的?保守地说,“使用中的地址”的确定是否忽略了接口(interface)套接字所绑定(bind)的?是网络堆栈分层问题吗?

跟踪示例:

  1. 我在特定地址上启动了一个监听套接字(服务器):nc -l fd00:aaaa::a 9000。其踪迹如下:
socket(PF_INET6, SOCK_STREAM, IPPROTO_TCP) = 3
setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
bind(3, {
    sa_family=AF_INET6,
    sin6_port=htons(9000),
    inet_pton(AF_INET6, "fd00:aaaa::a", &sin6_addr),
    sin6_flowinfo=0, sin6_scope_id=0
}, 28) = 0
listen(3, 1)                            = 0
accept(3, ...
  1. 如果我绑定(bind)到另一个接口(interface)正在使用的端口,即使我已经绑定(bind)到另一个接口(interface),连接到它(客户端)也会失败:
socket(PF_INET6, SOCK_STREAM, IPPROTO_IP) = 3
setsockopt(3, SOL_SOCKET, SO_BINDTODEVICE, "nicB\0", 5) = 0
bind(3, {sa_family=AF_INET6,
         sin6_port=htons(9000),
         inet_pton(AF_INET6, "::", &sin6_addr), 
         sin6_flowinfo=0,
         sin6_scope_id=0
        }, 28) = -1 //EADDRINUSE (Address already in use)
  1. 但是,如果我不指定端口,那么在绑定(bind)到 :: 时一切正常(同时监听器仍在运行):
socket(PF_INET6, SOCK_STREAM, IPPROTO_IP) = 3
setsockopt(3, SOL_SOCKET, SO_BINDTODEVICE, "nicB\0", 5) = 0
bind(3, {
    sa_family=AF_INET6,
    sin6_port=htons(0),
    inet_pton(AF_INET6, "::", &sin6_addr),
    sin6_flowinfo=0, sin6_scope_id=0
}, 28) = 0
connect(3, {
    sa_family=AF_INET6,
    sin6_port=htons(9000),
    inet_pton(AF_INET6, "fd00:aaaa::a", &sin6_addr),
    sin6_flowinfo=0, sin6_scope_id=0
}, 28) = ...

注意:这是在 3.19.0-68-generic x86_64 上。 Ubuntu 14.04。如果它有所不同,对于我的测试,nicB 是桥接模式下的 macvlan,其父节点是 nicA。

最佳答案

我找到了这个问题的令人满意的解释。

观察是即使只有接口(interface)“A”有 IP fd00:aaaa::a/64 当程序启动时,监听套接字可以接受来自不同接口(interface)的连接,如果它们将来会收到该 IP。可以添加和删除 IP——当接口(interface)接收到新 IP 时,无需重新启动监听 :: 或(v4 中的 0.0.0.0)的服务器进程。

因此,在某种程度上,进程 1 的 bind("fd00:aaaa::a/64", 9000) 隐式绑定(bind)到 ALL 接口(interface)。即使进程 2 只需要使用接口(interface) B,进程 1 已经获得了优先权,因为它在两个接口(interface)上都使用了端口 9000,所以进程 2 被拒绝了。

如果我更改程序 1,使其也使用 SO_BINDTODEVICE(接口(interface)“A”),那么两个进程都可以bind(::, 9000) 而不会出现问题。

实验

我已经用一个小的 LD_PRELOAD goop 测试了这个,它在调用 bind() 之前使用 setsockopt(...SO_BINDTODEVICE...)。如果以下两个 TCP 监听器分别绑定(bind)到不同的接口(interface),则它们都可以同时绑定(bind)到端口 9000。

# LD_PRELOAD=./bind_hook.so _BINDTODEVICE=eth0 nc -l 0.0.0.0 9000

# LD_PRELOAD=./bind_hook.so _BINDTODEVICE=eth1 nc -l 0.0.0.0 9000

如果两者中只有一个使用 SO_BINDTODEVICE,则最后一个进程获得 EADDRINUSE。题中提出的是哪种情况。

我为我的工具包含了 C 代码 (GNU/Linux),以防​​有人需要类似的东西:

/**                                                                                                                                                              
 * bind_hook.c                                                                                                                                                   
 *                                                                                                                                                               
 * Calls setsockopt() with #SO_BINDTODEVICE before _any_ bind().                                                                                                       
 * The name of the interface to bind to is obtained from                                                                                                         
 * environment variable `_BINDTODEVICE`.
 *
 * Needs root perms. errors are not signalled out.
 *                                                                                                                                                               
 * Compile with:
 *   gcc -Wall -Werror -shared -fPIC -o bind_hook.so -D_GNU_SOURCE bind_hook.c -ldl
 * Example usage:
 *   LD_PRELOAD=./bind_hook.so _BINDTODEVICE=eth0 nc -l 0.0.0.0 9500
 *                                                                                                                                                               
 * @author: init-js
 **/
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <net/if.h>
#include <dlfcn.h>
#include <errno.h>


static char iface[IF_NAMESIZE];
static int (*bind_original)(int, const struct sockaddr*, socklen_t addrlen);

int bind(int sockfd, const struct sockaddr *addr,
         socklen_t addrlen);

__attribute__((constructor))
void ctor() {
        bind_original = dlsym(RTLD_NEXT, "bind");

        char *env_iface = getenv("_BINDTODEVICE");
        if (env_iface) {
                strncpy(iface, env_iface, IF_NAMESIZE - 1);
        }
}

/* modified bind() -- call setsockopt first */
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen) {
        int _errno;

        if (iface[0]) {
                /* preserve errno */
                _errno = errno;
                setsockopt(sockfd, SOL_SOCKET, SO_BINDTODEVICE,
                           (void*)iface, IF_NAMESIZE);
                errno = _errno;
        }
        return bind_original(sockfd, addr, addrlen);
}

关于linux - 如何绑定(bind)到只有一个网络接口(interface)(Linux)的所有地址?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/39536172/

相关文章:

c++ - 从 libexpect 获取输出

c - 如何在没有 libcurl 的情况下用 C 发出 HTTP get 请求?

ios - 无法从 IPv6 连接到 IPv4 错误域=NSURLErrorDomain 代码=-1003“找不到具有指定主机名的服务器

ios - Iphone App 拒绝 Ipv6 不兼容

python - 如何解决 Windows 上的 "NotImplementedError"urllib2/gevent 错误?

c# mono interop - 无法识别的配置部分 dllmap

python - 在共享虚拟主机服务器上使用 wsgi 和 virtualenv 部署 Django 项目,无需 root 访问权限

regex - Grep 模式匹配-下划线

c - 试图理解网络通信背后的逻辑

azure - 将 VNET 移至新的 RG,但 AKS 仍指向旧的 RG