Skip to content
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

Avoid dual-stack #516

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 32 additions & 12 deletions src/aioquic/asyncio/client.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import asyncio
import socket
from contextlib import asynccontextmanager
from typing import AsyncGenerator, Callable, Optional, cast
from typing import AsyncGenerator, Callable, Optional, Tuple, Union, cast

from ..quic.configuration import QuicConfiguration
from ..quic.connection import QuicConnection, QuicTokenHandler
Expand All @@ -23,6 +23,7 @@ async def connect(
token_handler: Optional[QuicTokenHandler] = None,
wait_connected: bool = True,
local_port: int = 0,
dual_stack: bool = socket.has_dualstack_ipv6(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would we want to disable dual stack on a system that supports it?

Copy link
Author

@catap catap Jul 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To enforce binding to IPv6 only for example. Rigth now it binds to both protocol on Linux, and if you desier to bind only for IPv6 here no way.

) -> AsyncGenerator[QuicConnectionProtocol, None]:
"""
Connect to a QUIC server at the given `host` and `port`.
Expand Down Expand Up @@ -50,15 +51,10 @@ async def connect(
you can set it to `False` if you want to immediately start sending data using
0-RTT.
* ``local_port`` is the UDP port number that this client wants to bind.
* ``dual_stack`` is a flag which enabled or disabled using IPv4/IPv6 Dual-Stack.
The default value is platform specific and similar to socket.has_dualstack_ipv6()
"""
loop = asyncio.get_event_loop()
local_host = "::"

# lookup remote address
infos = await loop.getaddrinfo(host, port, type=socket.SOCK_DGRAM)
addr = infos[0][4]
if len(addr) == 2:
addr = ("::ffff:" + addr[0], addr[1], 0, 0)

# prepare QUIC connection
if configuration is None:
Expand All @@ -71,16 +67,40 @@ async def connect(
token_handler=token_handler,
)

# explicitly enable IPv4/IPv6 dual stack
sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
# Use AI_ADDRCONFIG on platforms which doesn't support dual-stack
flags = 0
if not dual_stack:
flags = socket.AI_ADDRCONFIG

# lookup remote address
infos = await loop.getaddrinfo(host, port, type=socket.SOCK_DGRAM, flags=flags)

local_tuple: Union[Tuple[str, int], Tuple[str, int, int, int]]
addr = infos[0][4]
# addr is 2-tuple for AF_INET and 4-tuple for AF_INET6
if dual_stack and len(addr) == 2:
addr = ("::ffff:" + addr[0], addr[1], 0, 0)
local_tuple = ("::", local_port, 0, 0)
sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
elif len(addr) == 2:
local_tuple = ("0.0.0.0", local_port)
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
elif len(addr) == 4:
local_tuple = ("::", local_port, 0, 0)
sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
else:
raise Exception("Unsupported response from getaddrinfo")

completed = False
try:
sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
sock.bind((local_host, local_port, 0, 0))
if dual_stack:
sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
sock.bind(local_tuple)
completed = True
finally:
if not completed:
sock.close()

# connect
transport, protocol = await loop.create_datagram_endpoint(
lambda: create_protocol(connection, stream_handler=stream_handler),
Expand Down
6 changes: 5 additions & 1 deletion tests/test_asyncio.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,11 @@ async def run_client(
return response

@contextlib.asynccontextmanager
async def run_server(self, configuration=None, host="::", **kwargs):
async def run_server(self, configuration=None, host=None, **kwargs):
if host is None and not socket.has_dualstack_ipv6():
host = "127.0.0.1"
elif host is None:
host = "::"
if configuration is None:
configuration = QuicConfiguration(is_client=False)
configuration.load_cert_chain(SERVER_CERTFILE, SERVER_KEYFILE)
Expand Down