Linux: 如何将UDP监听套接字绑定到特定的接口(或查找数据报来自哪个接口)?

10

我正在开发一个守护进程,监听UDP广播包并通过UDP响应。当有数据包到达时,我想知道该数据包所到达的IP地址(或NIC),以便我可以使用该IP地址作为源地址进行回复。(由于涉及很多痛苦的原因,我们系统的一些用户想要将同一台机器上的两个NIC连接到同一个子网上。虽然我们告诉他们不要这样做,但他们仍然坚持。我不需要被提醒这是多么丑陋。)

似乎没有办法直接检查数据报并找出其目标地址或进入的接口。根据大量搜索,我发现找出数据报的目标唯一方法是为每个接口创建一个监听套接字,并将套接字绑定到各自的接口。

首先,我是这样创建我的监听套接字的:

s=socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)

为了绑定套接字,我尝试的第一件事是使用以下代码(其中nic是指接口名称的char*):

// Bind to a single interface
rc=setsockopt(s, SOL_SOCKET, SO_BINDTODEVICE, nic, strlen(nic));
if (rc != 0) { ... }

这没有任何效果并且会默默地失败。用ASCII名称(例如eth0)作为这个调用的名称类型是正确的吗?为什么会默默地失败?根据man 7 socket,“请注意,这仅适用于某些套接字类型,特别是AF_INET套接字。对于数据包套接字,它不受支持(在那里使用普通的bind(8))。 "我不确定它所说的“数据包套接字”是什么意思,但这是一个AF_INET套接字。

所以我尝试了下面这个(基于bind vs SO_BINDTODEVICE socket):

struct sockaddr_ll sock_address;
memset(&sock_address, 0, sizeof(sock_address));
sock_address.sll_family = PF_PACKET;
sock_address.sll_protocol = htons(ETH_P_ALL);
sock_address.sll_ifindex = if_nametoindex(nic);
rc=bind(s, (struct sockaddr*) &sock_address, sizeof(sock_address));
if (rc < 0) { ... }

这次出现了错误Cannot assign requested address。我还尝试将协议族更改为AF_INET,但仍然出现相同的错误。

还有一种选择是将套接字绑定到特定的IP地址。我可以查找接口地址并将其绑定到那些地址。不幸的是,这是一个糟糕的选择,因为由于DHCP和热插拔以太网电缆,地址可能会动态变化。

当涉及广播和多播时,这个选项也可能不好。我担心绑定到特定地址意味着我无法接收广播(发送到与我绑定地址不同的地址)。今晚我实际上会测试这个问题并更新这个问题。

问题:

  • 是否可能将UDP监听套接字专门绑定到一个接口?
  • 或者,是否有一种机制可以通知我的程序,在发生该更改的那一刻,某个接口的地址已更改(而不是轮询)?
  • 是否有另一种类型的监听套接字(我具有root权限)可以创建,可以绑定到特定接口,除了原本的UDP之外,它表现得完全相同(例如,我可以使用AF_PACKETSOCK_DGRAM吗? 我不理解所有选项)。

有人能帮我解决这个问题吗?谢谢!

更新:

将其绑定到特定的IP地址并不能正常工作。具体来说,我无法接收广播包,这正是我想要接收的。

更新:

我尝试使用IP_PKTINFOrecvmsg来获取有关接收到的数据包的更多信息。我可以获得接收接口、接收接口地址、发送方的目标地址以及发送方的地址。以下是我在接收一份广播数据包时收到的报告示例:

Got message from eth0
Peer address 192.168.115.11
Received from interface eth0
Receiving interface address 10.1.2.47
Desination address 10.1.2.47

很奇怪的是,eth0的地址是10.1.2.9,而ech1的地址是10.1.2.47。那么为什么eth0会收到应该由eth1接收的数据包呢?这绝对是个问题。

请注意,我启用了net.ipv4.conf.all.arp_filter,尽管我认为它只适用于出站数据包。


1
我发现这个很有用:https://dev59.com/7HRB5IYBdhLWcg3wl4EQ - Timothy Miller
如果你需要一个例子,dnsmasq 已经实现了这个功能(通过 bind-interfaces 配置选项进行控制)。 - Ben Voigt
5个回答

7

我找到的解决方案如下。首先,我们需要更改ARP和RP设置。在/etc/sysctl.conf中添加以下内容,并重新启动(也有一条命令可以动态设置):

net.ipv4.conf.default.arp_filter = 1
net.ipv4.conf.default.rp_filter = 2
net.ipv4.conf.all.arp_filter = 1
net.ipv4.conf.all.rp_filter = 2

ARP过滤器是为了允许来自eth0的响应路由到WAN。RP过滤器选项是为了严格将传入的数据包与它们所在的NIC关联起来(而不是与匹配子网的任何NIC相关联的弱模型)。EJP的评论引导我迈出了这个至关重要的步骤。

之后,SO_BINDTODEVICE开始工作。每个套接字都绑定到自己的NIC,因此我可以根据来自哪个套接字的消息来确定它来自哪个NIC。

s=socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
rc=setsockopt(s, SOL_SOCKET, SO_BINDTODEVICE, nic, IF_NAMESIZE);
memset((char *) &si_me, 0, sizeof(si_me));
si_me.sin_family = AF_INET;
si_me.sin_port = htons(LISTEN_PORT);
si_me.sin_addr.s_addr = htonl(INADDR_ANY);
rc=bind(s, (struct sockaddr *)&si_me, sizeof(si_me))

接下来,我想用源地址为原始请求的NIC的地址来响应传入的数据报。解决方法就是查找该NIC的地址,并将出站套接字绑定到该地址上(使用bind)。
s=socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)
get_nic_addr(nics, (struct sockaddr *)&sa)
sa.sin_port = 0;
rc = bind(s, (struct sockaddr *)&sa, sizeof(struct sockaddr));
sendto(s, ...);

int get_nic_addr(const char *nic, struct sockaddr *sa)
{
    struct ifreq ifr;
    int fd, r;
    fd = socket(AF_INET, SOCK_DGRAM, 0);
    if (fd < 0) return -1;
    ifr.ifr_addr.sa_family = AF_INET;
    strncpy(ifr.ifr_name, nic, IFNAMSIZ);
    r = ioctl(fd, SIOCGIFADDR, &ifr);
    if (r < 0) { ... }
    close(fd);
    *sa = *(struct sockaddr *)&ifr.ifr_addr;
    return 0;
}

(也许每次查找网卡地址都感觉很浪费,但是为了在地址更改时得到通知需要编写更多的代码,在不使用电池的系统上,这些事务每隔几秒钟才会发生。)

sysctl 可以通过 /sbin/sysct 进行动态更改。 读取 sysctl net.ipv4.conf.default.arp_filter。 写入则使用 sysctl -w net.ipv4.conf.default.arp_filter=x - init_js

3
如果您的平台支持,您可以通过使用recvmsg()IP_RECVDSTADDR选项获取发送方使用的目标地址。这相当复杂,在《Unix网络编程》第一卷第三版22.2中有描述,并在man页面中有说明。
关于您的编辑,您面临的是TCP/IP的“弱端系统模型”。基本上,一旦数据包到达,系统可以选择通过任何适当的接口传递它,监听正确的端口。这在TCP/IP的RFC文档中有讨论。

1
dnsmasq也有此方法的示例,可以参考(http://bazaar.launchpad.net/~vcs-imports/dnsmasq/master/view/head:/src/forward.c#L1174)。 - Ben Voigt
我进行了调查,并发现了rp_filter选项(net.ipv4.conf.all.rp_filter)。将其设置为1没有任何效果。但是我将其设置为2,突然之间,我开始在eth0和eth1上接收广播。这是进展!尽管如此,发送到10.1.2.47的特定数据包仍通过eth0到达,所以我无法理解这一点。 - Timothy Miller
@TimothyMiller:IP_RECVDSTADDR并不过滤数据包,它只是告诉你数据包从哪里接收到的。如果要过滤数据包,你需要使用SO_BINDTODEVICE。它对于dnsmasq有效,也许你应该查找一下代码,看看其他套接字选项被设置了什么。 - Ben Voigt
3
@EJP:你关于弱端系统模型的评论或许是最有帮助的。它间接地帮我找到了rp_filter(我把它设置为2),这导致Linux大部分将数据报与它们所在的NIC相关联。因此,SO_BINDTODEVICE开始起作用了(之前没有效果),并且IP_PKTINFO开始将数据报报告为来自eth1(之前只报告eth0)。除了点赞之外,我希望有一种方法可以向stackoverflow表明,“这个回答在找到答案方面至关重要”,而不意味着它就是整个答案。 - Timothy Miller
我快要哭了,因为在这个小世界上,又有一个人在咒骂内核文档如此贫乏,而信息量又如此巨大!这种行为真是疯狂!我会尝试并报告结果,但到目前为止,我的症状与你的几乎相同。 - Riccardo Manfrin

3
您正在向setsockopt传递非法值。
rc=setsockopt(s, SOL_SOCKET, SO_BINDTODEVICE, nic, strlen(nic));

关于 SO_BIND_TO_DEVICE,man手册上写道:

传递的选项是一个变长的以null结尾的接口名称字符串,最大大小为IFNAMSIZ。

strlen 不包括终止符。您可以尝试:

rc=setsockopt(s, SOL_SOCKET, SO_BINDTODEVICE, nic, 1 + strlen(nic));

dnsmasq已经正确地实现了这一功能,并使用了

setsockopt(fd, SOL_SOCKET, SO_BINDTODEVICE, intname, IF_NAMESIZE)

我已经尝试过strlen+1和IF_NAMESIZE,但都没有起作用。我可以将数据包发送到eth1的地址,但我尝试绑定到eth0的套接字却接收不到它。因此,要么我在这里仍然做错了什么,要么这种方法行不通。 - Timothy Miller
大多数声称能够工作的实现,当传入类似于“eth0”这样的内容时,只会传递长度为4。 - Timothy Miller

0

我认为你可能从错误的角度来解决问题。一般情况下,接口可以同时拥有多个IP地址,因此知道你连接到哪个接口并不能给你IP地址(在一般情况下)。

相反,不要过于关注使用的接口,而是关注使用的IP地址。首先使用getifaddrs()获取所有IP地址列表,并将每个地址绑定到一个套接字上。

可以使用select()等待所有套接字上的数据包。使用接收到数据包的套接字确定数据包的目标地址。此外,接收到数据包的套接字可用于发送回复,这将自动适当地设置源地址。

偶尔需要检查新的IP地址,但如果DHCP给您一个新地址,则套接字会出现错误。


原來綁定特定地址是錯誤的答案。當我這麼做時,我無法接收廣播封包。儘管我同意通常一個介面可能有多個地址,但這裡不是這種情況,因為這是一個特殊用途的系統。 - Timothy Miller

0

我知道这是一个旧的帖子,但我在这里没有找到我要找的答案。

通过使用我在这里找到的信息,将原始套接字绑定到一个接口,使得套接字不会看到来自另一个接口的任何数据包(包括广播、IGMP等),对我有用。

https://cs.wikipedia.org/wiki/Raw_socket

我需要的功能是BindRawSocketToInterface()函数。

希望这能帮助其他人。 干杯!


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