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?

  1. Add this line to /etc/hosts:
    • fe80::1%lo0 localhost
  2. Try to use anyio.create_tcp_listener(host="localhost", port=0).
  3. Which results in:
    • OSError: [Errno 49] Can't assign requested address

Or even simpler:

  1. 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)

Most upvoted comments

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 like sock.bind(("fe80::1%1", 0)) would be valid, but it’s really not (gives Invalid 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.