发送 UDP 到本地服务器,但不通过环回接口

Posted

技术标签:

【中文标题】发送 UDP 到本地服务器,但不通过环回接口【英文标题】:Send UDP to local server, but not via loopback interface 【发布时间】:2019-04-22 08:36:51 【问题描述】:

我最终尝试测试一个 UDP 客户端,并希望确保它在通过环回接口发送数据时正常工作,以避免由此引入的任何细微问题,例如校验和验证 (Bad UDP checksum has no effect: why?)。

但是,即使将数据发送到socket.gethostbyname(socket.gethostname()) 的结果,而不是127.0.0.1,那么根据Wireshark,数据似乎是通过环回接口。

以下程序成功发送和接收b'somedata',并从Wireshark捕获以下内容。

import asyncio
import socket

async def server():
    with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
        sock.setblocking(False)
        sock.bind(('', 4567))
        data = await loop.sock_recv(sock, 512)
        print('Received', data)

async def main():
    local_ip = socket.gethostbyname(socket.gethostname())
    print('Local IP', local_ip)  # Outputs 192.168.0.34

    asyncio.ensure_future(server())
    await asyncio.sleep(0)

    with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
        sock.setblocking(False)
        sock.connect((local_ip, 4567))
        await loop.sock_sendall(sock, b'somedata')
        await asyncio.sleep(1)

loop = asyncio.get_event_loop()
loop.run_until_complete(main())
loop.close()

如何将数据从本地运行的客户端发送到本地运行的服务器,但避免环回接口并实际将数据发送到网络?

理想的答案将适用于 Linux 和 macOS。

【问题讨论】:

请问您要达到什么目的?您是在测试您的代码,还是在测试网络接口?如果你想测试你的代码如何处理坏包,那么也许你应该创建一个在套接字上工作的方法,这样你就可以在单元测试中模拟套接字对象。如果您正在创建生产测试/集成测试/系统测试,那么测试设置应尽可能接近真实(预期)设置。理想情况下,您将拥有一个彼此分离的真实服务器和客户端。也许您可以使用虚拟机或使用两个 USB 转以太网适配器来实现这一点? @wovano 我正在测试我的代码,以及它如何与系统的其余部分交互,避免了模拟引入的假设,所以它更像是一种集成式测试...“那么测试设置应该尽可能接近真实(预期)设置。”同意,看看在没有多台机器/适配器开销的情况下我能走多远。 我当然理解您希望避免这种开销。但我认为如果没有系统的其余部分,您无法测试您的应用程序如何与系统的其余部分交互;-) 要实际测试网络接口,您至少需要两个网络接口来保证数据包正在通过线路传输。即使这样,没有交换机的行为也可能与使用交换机的行为有很大不同(例如,由于数据包重新排序、IP 分段等)。例如,Linux 系统可能与 Windows 系统有细微差别。 可以做的是测试正常(预期)行为,以及您能想到的所有错误情况(例如错误的 UDP 校验和)。如果您的应用程序的接口边界选择正确,您可能能够测试大多数情况。您能描述一下您想要测试的什么,即哪些测试用例?这可能有助于提供更好的解决方案。您认为 mocking 会引入哪些假设? “你能描述一下你想测试什么吗”。例如,到达的数据包具有错误的 UDP 校验和如何影响应用程序。它被忽略了吗?照常处理?引发异常? “你认为模拟会引入什么假设”:较低级别的行为实际上就像模拟一样。了解不同的系统有细微的差异,这正是我希望测试使用系统的较低级别行为以确保对其进行较少假设的原因,并且测试通过意味着很有可能应用程序有效。 【参考方案1】:

要“说服”网络堆栈使用以太网(或 WiFi)卡而不是环回物理传输帧,请使用广播地址。

我已经在我的 Linux 系统上以这种方式成功地发送和接收了一个 UDP 数据包。我用tcpdump 验证了它。它在以太网接口上显示一个数据包,并且在环回上没有活动。

我使用了文字广播地址。 socket module 文档还提到字符串 '<broadcast>' 作为特例地址。我没试过。

with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
    sock.setblocking(False)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
    sock.connect(('192.168.0.255', 4567))
    await loop.sock_sendall(sock, b'somedata')
    await asyncio.sleep(1)

注意事项:

    同一网络上的其他主机也会​​收到 UDP 数据包。 确保防火墙/数据包过滤器(例如 Linux iptables/nftables)不会阻止数据包。 关于setsockopt:Python socket.error: [Errno 13] Permission denied

【讨论】:

有趣...从来没有考虑过这个。然而,就我的情况而言,虽然使测试更多像通过网络的真实案例一样,但它也更少像真实案例,因为不使用广播 我找到了一个相关主题:serverfault.com/q/483209 我在一个以太网卡上尝试了 2 个 IP 地址,并且能够将数据报从 addr1 传输到 addr2,但它没有从交换机收到。 【参考方案2】:

这可能是因为您的主机名指向环回地址,因此socket.gethostbyname(socket.gethostname()) 将产生127.0.0.1

你需要做的是取消从主机名指向环回地址:

在 Linux 中编辑 /etc/hosts 并注释掉 127.0.0.1 YOUR_HOSTNAME 行 在 Windows 中,您应该有 c:\windows\system32\drivers\etc\hosts,它看起来与 Linux 相似

在此之后,如果您调用 socket.gethostbyname(socket.gethostname()),它将产生您的 DHCP 分配的 IP。

尽管在这种情况下,从yourip 调用到yourip 可能会导致网络驱动程序通过环回接口路由包。另一种方法是在网络路由器之外使用公共 IP。您可以使用this answer

中所述的外部服务

【讨论】:

啊我不认为这就是这里发生的事情socket.gethostbyname(socket.gethostname()) 不会产生 127.0.0.1。我已经编辑了问题以使其更加明确。 Wireshark 转储最终表明数据包不是发往或发往 127.0.0.1。 @user207421 我想我可能第一次错过了那个细节。尽管如此,它仍然会像这样通过lo 接口。

以上是关于发送 UDP 到本地服务器,但不通过环回接口的主要内容,如果未能解决你的问题,请参考以下文章

C# UDP 客户端读取多播 IP(本地接口),并将单播 UDP 发送到 ***

Linux本地环回地址lo

有没有办法监控发送到本地 Apache 服务器的 UDP 数据包?

如果启用了环回,为啥发送方收不到它的多播 UDP 数据包?

使用 boost asio udp 套接字时如何设置本地端点

将udp数据包发送到本地局域网很奇怪