anyio: "OSError: [Errno 49] Can't assign requested address" for IPv6 address
Things to check first
-
I have searched the existing issues and didn’t find my bug already reported there
-
I have checked that my bug is still present in the latest release
AnyIO version
3.6.2
Python version
3.10.6
What happened?
anyio.create_tcp_listener
was not able to bind to fe80::1%lo0
.
Details
It looks like the create_tcp_listener
always resolves IPv6 addresses to a 2-element tuples:
Though 2-element tuples for IPv6 addresses are not always understandable by socket.bind()
.
From https://docs.python.org/3/library/socket.html#socket-families:
For AF_INET6 address family, a four-tuple (host, port, flowinfo, scope_id) is used, where flowinfo and scope_id represent the sin6_flowinfo and sin6_scope_id members in struct sockaddr_in6 in C. For socket module methods, flowinfo and scope_id can be omitted just for backward compatibility. Note, however, omission of scope_id can cause problems in manipulating scoped IPv6 addresses
anyio.create_tcp_listener
under the hood uses anyio.getaddrinfo
.
It looks like the way how anyio.getaddrinfo
works for IPv6 addresses is not fully compatible with the way how socket.bind
is expecting the IPv6 addresses format to be.
anyio.getaddrinfo
parses fe80::1%lo0
(which is a link-local address) as ('fe80::1%1', 0)
(where %1
is scope_id
), whereas socket.bind
is expecting scope_id
as a separate parameter.
This is what anyio.getaddrinfo
produces:
('fe80::1%1', 0)
For comparison - this is the result of using socket.getaddrinfo
:
>>> import socket
>>> socket.getaddrinfo("fe80::1%lo0", 0, proto=socket.IPPROTO_TCP)
[(<AddressFamily.AF_INET6: 30>, <SocketKind.SOCK_STREAM: 1>, 6, '', ('fe80::1', 0, 0, 1))]
So we have ('fe80::1%1', 0)
vs. ('fe80::1', 0, 0, 1)
.
I’m not able to bind to ('fe80::1%1', 0)
using:
import socket
raw_socket = socket.socket(socket.AddressFamily.AF_INET6)
raw_socket.bind(('fe80::1%1', 0))
This is what socket.bind
expects (both work):
('fe80::1%1', 0, 0, 1)
('fe80::1', 0, 0, 1)
I am able to bind:
import socket
raw_socket = socket.socket(socket.AddressFamily.AF_INET6)
raw_socket.bind(('fe80::1%1', 0, 0, 1))
# or
# raw_socket.bind(('fe80::1', 0, 0, 1))
How can we reproduce the bug?
- Add this line to
/etc/hosts
:fe80::1%lo0 localhost
- Try to use
anyio.create_tcp_listener(host="localhost", port=0)
. - Which results in:
OSError: [Errno 49] Can't assign requested address
Or even simpler:
anyio.create_tcp_listener(host="fe80::1%lo0", port=0)
About this issue
- Original URL
- State: closed
- Created a year ago
- Comments: 20 (16 by maintainers)
Side note: I discovered (and worked around) a bug in PyPy’s implementation of
socket.getaddrinfo()
. Once I reported it, it was promptly fixed within 2 hours. Kudos to the PyPy folks for the lightning speed fix!I’ve looked into this further and it seems that I indeed misunderstood how
socket.bind()
works with the scope ID. Sorry for dismissing this earlier! I thought that a call likesock.bind(("fe80::1%1", 0))
would be valid, but it’s really not (givesInvalid argument
). The reason for my confusion is that for anything else than link-local addresses,bind()
works just fine with a 2-tuple address, even for IPv6, and I thought that it would accept an IPv6 address with the proper scope ID in it that way, but seems that it doesn’t. For now, I will work around this in v3.7.0, and change the API for v4.0. Testing this in CI might be a problem though.