繁体   English   中英

Linux:将 UDP 侦听套接字绑定到特定接口(或找出数据报来自的接口)?

[英]Linux: Bind UDP listening socket to specific interface (or find out the interface a datagram came in from)?

我有一个正在处理的守护进程,它侦听 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 与 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 完全相同(即除了原始套接字,我基本上必须自己实现 UDP)? 例如,我可以将AF_PACKETSOCK_DGRAM AF_PACKET使用吗? 我不明白所有的选项。

谁能帮我解决这个问题? 谢谢!

更新:

绑定到特定 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,尽管我认为这仅适用于出站数据包。

我发现有效的解决方案如下。 首先,我们要更改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到该地址(使用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;
}

(也许每次查找 NIC 的地址似乎是一种浪费,但是当地址更改时需要更多的代码来获得通知,并且在不依靠电池运行的系统上,这些事务每隔几秒钟才会发生一次。)

如果您的平台支持,您可以使用recvmsg()通过IP_RECVDSTADDR选项获取发件人使用的目标地址。 它相当复杂,在Unix Network Programming, volume I, 3rd edition, #22.2 和手册页中有所描述

重新编辑,您遇到了所谓的 TCP/IP 的“弱端系统模型”。 基本上,一旦数据包到达,系统就可以选择通过任何适当的接口侦听正确的端口来传送它。 它在 TCP/IP RFC 中的某处进行了讨论。

您将非法值传递给setsockopt

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

手册页说SO_BIND_TO_DEVICE

传递的选项是一个可变长度的空终止接口名称字符串,最大大小为 IFNAMIZ

strlen不包括终止空值。 你可以试试:

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

dnsmasq可以正常工作,并使用

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

我相信您可能从错误的角度处理问题。 在一般情况下,接口可以同时拥有多个 IP 地址,因此知道您连接到哪个接口不会为您提供 IP 地址(在一般情况下)

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

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

您可能偶尔需要检查新 IP 地址,但如果 DHCP 为您提供新地址,则会在套接字上出现错误。

我知道这是一个旧线程,但我没有找到我在这里寻找的答案。

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

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

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM