-
-
Notifications
You must be signed in to change notification settings - Fork 30k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
asyncio DatagramTransport.sendto does not send empty UDP packets #113812
Comments
@joudinet This is not enough information for us to help you. There could be a bug in your program. I don't know much about UDP multicast, and there doesn't seem to be any special provision for it in asyncio. (Does it work by using a specific IP address? Maybe the socket needs to have some flag applied to enable multicast.) Can you narrow the problem down to a program that's small enough to paste into a comment here and simple enough for someone to try running locally? |
Here is a small enough program to replicate the issue: #! /usr/bin/env python
import socket
import asyncio
import os
import sys
MULTICAST_GROUP="ff12::4242:2e5e:7:c011:f16"
PORT = 4242
ENABLE_LOOPBACK_TESTING = 0
if os.name == "nt" and not hasattr(socket, "IPPROTO_IPV6"):
socket.IPPROTO_IPV6 = 41
class MyProtocol(asyncio.DatagramProtocol):
def __init__(self, ifindex, on_connection_lost):
self.ifindex = ifindex
self.on_connection_lost = on_connection_lost
self.transport = None
def connection_made(self, transport):
self.transport = transport
sock = transport.get_extra_info('socket')
try:
sock.setsockopt(socket.IPPROTO_IPV6,
socket.IPV6_MULTICAST_IF,
self.ifindex)
sock.setsockopt(socket.IPPROTO_IPV6,
socket.IPV6_MULTICAST_LOOP,
ENABLE_LOOPBACK_TESTING)
self.sock = sock
except (OSError, socket.error):
# Expected exceptions, simply close the socket.
self.transport.close()
def send_packet(self, content):
if bytes is not str and isinstance(content, str):
content = content.encode()
# FIXME: the following transport.sendto call does not work:
# self.transport.sendto(content,
# (MULTICAST_GROUP, PORT, 0, self.ifindex))
self.sock.sendto(content,
(MULTICAST_GROUP, PORT, 0, self.ifindex))
def send_probe(self):
self.send_packet("")
def writable(self):
"""Not interested in write events"""
return False
def datagram_received(self, data, addr):
"""Called when some datagram is received.
data is a bytes object containing the incoming data.
addr is the address of the peer sending the data.
"""
# Skip the received part for this example.
pass
def error_received(self, exc):
print("Error received:", exc)
def connection_lost(self, exc):
self.on_connection_lost.set_result(True)
async def main():
loop = asyncio.get_running_loop()
connections = set()
# Bruteforce the interface index, because there is no reliable way
# to get them
for ifindex in range(2,1000):
on_connection_lost = loop.create_future()
transport, protocol = await loop.create_datagram_endpoint(
lambda: MyProtocol(ifindex, on_connection_lost),
family=socket.AF_INET6)
if not on_connection_lost.done():
connections.add(
(ifindex, on_connection_lost, transport, protocol))
if not connections:
print("No network interfaces found")
sys.exit(1)
first_probe = True
while connections:
await asyncio.sleep(1)
if first_probe:
print("* Sending probes every second on interface indexes:",
",".join(str(c[0]) for c in connections))
first_probe = False
done_connections = set()
for ifindex, on_conn_lost, transport, protocol in connections:
if on_conn_lost.done():
done_connections.add(
(ifindex, on_conn_lost, transport, protocol))
else:
try:
protocol.send_probe()
except:
# Continue to send probes to faulty interfaces. So
# this program won't fail if the system reports an
# error while the device is restarting.
pass
for conn in done_connections:
connections.discard(conn)
if __name__=='__main__':
asyncio.run(main()) This program sends UDP packets to the MULTICAST_GROUP IPv6 address on port 4242 on all network interfaces every second. Now, if you replace |
I tried running your sample program on macOS and ran the tcpdump command you suggested, but I have no idea what any of these mean, and the tcpdump program didn't give any output. I don't have access to Linux. Maybe someone else with more networking and Linux experience can jump in? |
Hi @joudinet the issue is because If you pass a non-empty string to Side note:
You can use Short exampleimport socket
import asyncio
import sys
import time
class Protocol(asyncio.DatagramProtocol):
def __init__(self, multicast_group, multicast_port, if_index):
# addr, port, flowinfo, scope
self._addr = (multicast_group, multicast_port, 0, if_index)
self.transport = None
self.closed = False
def connection_made(self, transport):
self.transport = transport
def sendto(self, content):
return self.transport.sendto(content, addr=self._addr)
def connection_lost(self, _):
self.closed = True
async def main(multicast_group, multicast_port, interface="en0"):
def protocol_factory():
return Protocol(
multicast_group=multicast_group,
multicast_port=multicast_port,
if_index=socket.if_nametoindex(interface),
)
loop = asyncio.get_running_loop()
transport, protocol = await loop.create_datagram_endpoint(
protocol_factory, family=socket.AF_INET6
)
if protocol.closed:
print("Failed to connect")
sys.exit(1)
try:
print(f"{time.time()}: sending empty")
protocol.sendto(b"")
time.sleep(1)
print(f"{time.time()}: sending 'hello'")
protocol.sendto(b"hello")
except Exception as exc:
print(f"Failed to send: {exc}")
finally:
transport.close()
if __name__ == "__main__":
asyncio.run(
main(multicast_group="ff12::1234", multicast_port=4242, interface="en0")
) tcpdump
Notice there is only one datagram with a payload of length=5 ("hello") |
For what it's worth, I don't believe the |
Hi @ordinary-jamie , |
Yup agreed; Microsoft on Win32 API, Winsock.h recvfrom
However this is probably only the case for unconnected sockets, and may require additional documentation to make clear the differing behaviour. Since for connected sockets, I believe the
I am unsure about unix-domain sockets; a quick browse and it appears a zero-length return is ambiguous. @gvanrossum, what do you think about removing this pattern for just the datagram transports ( |
Thanks @ordinary-jamie for that diagnosis! I have changed the title to reflect this. Now we need to decide whether this is a bug or a feature. Do we even care, now that we know that the OP's problem can be solved by adding some data to the test packets? I suspect that ignoring So, apart from the fact that it's different from the underlying |
Perhaps this can be rephrased as -- should
Agreed. However, if instead we can show that a zero-length There is added complexity for when a caller passes their own socket to |
We're reasonably sure the However, without any standard internet protocols that actually use All of which adds up to: |
I let you decide as I don't know enough about |
Okay, @joudinet's mention of the Time Protocol has convinced me that there's a valid use case for sending zero-length packets, and after reading through the code I think it is as simple as removing the two lines that check for empty data. Now, there are two implementations (one for Proactor and one for Selector), and both have these lines. There are no docs mentioning a special case for empty packets. I have a feeling @ordinary-jamie might want to look into submitting a PR, but anyone can give it a try (just leave a comment here if you intend to work on this). I would like the PR to add some tests (for both implementations) and for good measure add a line to the docs (and to the docstring for sendto in transports.py) explicitly allowing the sending of empty packets, and the .rst docs should add a versionchanged note explaining that this was added in 3.13. I don't think we can treat this as a backportable bug (it'll be too confusing to explain exactly which micro versions have the fix). I will next edit the issue subject line to remove "muticast", since this bug applies to all uses of sendto. |
Once this is fixed someone should file a uvloop bug letting them know this is changed in Python 3.13. |
🤭 Thanks Guido, I'll work on this if there are no takers! |
Also include the UDP packet header sizes (8 bytes per packet) in the buffer size reported to the flow control subsystem.
Fixed by #115199. Can't backport because this feels like a (tiny) new feature. |
…ython#115199) Also include the UDP packet header sizes (8 bytes per packet) in the buffer size reported to the flow control subsystem.
…ython#115199) Also include the UDP packet header sizes (8 bytes per packet) in the buffer size reported to the flow control subsystem.
Bug report
Bug description:
Trying to send multicast UDP packets with DatagramTransport.sendto, I notice no error on the python program but no packet is sent (I've checked with tcpdump). Modifying the call to use the underlying socket fixes the issue but I get a warning that I should not use this socket directly.
Is this a known issue?
How should I use asyncio to properly send UDP multicast packets?
CPython versions tested on:
3.10
Operating systems tested on:
Linux
Linked PRs
The text was updated successfully, but these errors were encountered: