aiortc / aioquic

QUIC and HTTP/3 implementation in Python
BSD 3-Clause "New" or "Revised" License
1.6k stars 229 forks source link

0-RTT early data not included in initial datagram #492

Open MWedl opened 2 months ago

MWedl commented 2 months ago

First of all, thanks for developing and maintaining this amazing library.

Description

I am trying use the full potential of HTTP/3 and QUIC by utilizing 0-RTT handshakes and inculde early data (HTTP/3 requests) in the 0-RTT handshakes. aioquic sends the Initial packet and 0-RTT packet as two separate UDP datagrams. This means that early data requests are not included in the transmitted datagram.

The problem seems to be how aioquic pads initial packets. aioquic adds PADDING frames to the initial packet. This fills the UDP datagram leaving no space for the 0-RTT packet anymore, which results in the 0-RTT packet being sent in the next datagram.

Wireshark Screenshot

In order to solve this issue, aioquic should add padding to UDP datagrams instead of QUIC packets. UDP datagrams can be padded by adding zero bytes after all QUIC packets inside a UDP datagram. This UDP datagram padding works for datagrams containing only packets with long headers, because the long header features a length field. This UDP padding is used by other QUIC clients such as Firefox.

Reproducable example

import asyncio
from pathlib import Path
import ssl
from urllib.parse import urlparse

from aioquic.asyncio.client import connect
from aioquic.h3.connection import H3_ALPN
from aioquic.quic.configuration import QuicConfiguration

from example_aioquic_http3_client import HttpClient  # examples/http3_client.py

class HttpClient0Rtt(HttpClient):
    def connect(self, addr) -> None:
        self._quic.connect(addr, now=self._loop.time())
        # Do not transmit handshake packets yet. We might want to add a 0-RTT request first.
        # self.transmit()

    async def wait_connected(self) -> None:
        """
        Wait for the TLS handshake to complete.
        """
        assert self._connected_waiter is None, "already awaiting connected"
        if not self._connected:
            self._connected_waiter = self._loop.create_future()

            # Transmit handshake packets
            self.transmit()

            await asyncio.shield(self._connected_waiter)

async def get_session_ticket(url):    
    # Connect to server and store session ticket for 0-RTT
    session_tickets = []
    async with connect(
            host=url.hostname,
            port=url.port or 443,
            configuration=QuicConfiguration(
                is_client=True,
                alpn_protocols=H3_ALPN,
                verify_mode=ssl.CERT_NONE,
                secrets_log_file=open(Path(__file__).parent.parent / 'quic_secrets.log', 'a'),
                server_name=url.hostname,
            ),
            create_protocol=HttpClient0Rtt,
            session_ticket_handler=lambda t: session_tickets.append(t),
            wait_connected=True
    ) as client:
        await client.get(url.geturl())

    return session_tickets[-1]

async def main() -> None:
    url = urlparse('https://127.0.0.1:4431/')
    session_ticket = await get_session_ticket(url)

    # Try to send a HTTP/3 request in 0-RTT early data
    async with connect(
            host=url.hostname,
            port=url.port or 443,
            configuration=QuicConfiguration(
                is_client=True,
                alpn_protocols=H3_ALPN,
                verify_mode=ssl.CERT_NONE,
                server_name=url.hostname,
                session_ticket=session_ticket,
            ),
            create_protocol=HttpClient0Rtt,
            wait_connected=False
    ) as client:
        await client.get(url.geturl())

if __name__ == "__main__":
    asyncio.run(main())
rthalley commented 2 months ago

We are planning work on the padding system that should address this and some other issues. No timeline at the moment though.

jlaine commented 5 days ago

@rthalley The more I think about it, the more I find the "pad the datagram, not the packet" approach is the easiest to implement..

rthalley commented 4 days ago

What you say about padding the datagram makes sense to me!