Skip to content

Commit

Permalink
Make the maximum datagram size configurable
Browse files Browse the repository at this point in the history
Also set the default maximum datagram size to 1200 bytes, in line with
RFC 9000.

Co-authored-by: Jeremy Lainé <jeremy.laine@m4x.org>
  • Loading branch information
ihlar and jlaine committed Nov 6, 2023
1 parent c88f38c commit 2ae1ad4
Show file tree
Hide file tree
Showing 10 changed files with 149 additions and 83 deletions.
10 changes: 9 additions & 1 deletion examples/http3_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,12 @@ async def main(
default=0,
help="local port to bind for connections",
)
parser.add_argument(
"--max-datagram-size",
type=int,
default=defaults.max_datagram_size,
help="maximum datagram size to send, excluding UDP or IP overhead",
)
parser.add_argument(
"--zero-rtt", action="store_true", help="try to send requests using 0-RTT"
)
Expand All @@ -519,7 +525,9 @@ async def main(

# prepare configuration
configuration = QuicConfiguration(
is_client=True, alpn_protocols=H0_ALPN if args.legacy_http else H3_ALPN
is_client=True,
alpn_protocols=H0_ALPN if args.legacy_http else H3_ALPN,
max_datagram_size=args.max_datagram_size,
)
if args.ca_certs:
configuration.load_verify_locations(args.ca_certs)
Expand Down
9 changes: 9 additions & 0 deletions examples/http3_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,8 @@ async def main(


if __name__ == "__main__":
defaults = QuicConfiguration(is_client=False)

parser = argparse.ArgumentParser(description="QUIC server")
parser.add_argument(
"app",
Expand Down Expand Up @@ -534,6 +536,12 @@ async def main(
type=str,
help="log secrets to a file, for use with Wireshark",
)
parser.add_argument(
"--max-datagram-size",
type=int,
default=defaults.max_datagram_size,
help="maximum datagram size to send, excluding UDP or IP overhead",
)
parser.add_argument(
"-q",
"--quic-log",
Expand Down Expand Up @@ -576,6 +584,7 @@ async def main(
alpn_protocols=H3_ALPN + H0_ALPN + ["siduck"],
is_client=False,
max_datagram_frame_size=65536,
max_datagram_size=args.max_datagram_size,
quic_logger=quic_logger,
secrets_log_file=secrets_log_file,
)
Expand Down
4 changes: 2 additions & 2 deletions src/aioquic/asyncio/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from typing import Callable, Dict, Optional, Text, Union, cast

from ..buffer import Buffer
from ..quic.configuration import QuicConfiguration
from ..quic.configuration import SMALLEST_MAX_DATAGRAM_SIZE, QuicConfiguration
from ..quic.connection import NetworkAddress, QuicConnection
from ..quic.packet import (
PACKET_TYPE_INITIAL,
Expand Down Expand Up @@ -85,7 +85,7 @@ def datagram_received(self, data: Union[bytes, Text], addr: NetworkAddress) -> N
retry_source_connection_id: Optional[bytes] = None
if (
protocol is None
and len(data) >= 1200
and len(data) >= SMALLEST_MAX_DATAGRAM_SIZE
and header.packet_type == PACKET_TYPE_INITIAL
):
# retry
Expand Down
7 changes: 7 additions & 0 deletions src/aioquic/quic/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
from .logger import QuicLogger
from .packet import QuicProtocolVersion

SMALLEST_MAX_DATAGRAM_SIZE = 1200


@dataclass
class QuicConfiguration:
Expand Down Expand Up @@ -46,6 +48,11 @@ class QuicConfiguration:
Connection-wide flow control limit.
"""

max_datagram_size: int = SMALLEST_MAX_DATAGRAM_SIZE
"""
The maximum QUIC payload size in bytes to send, excluding UDP or IP overhead.
"""

max_stream_data: int = 1048576
"""
Per-stream flow control limit.
Expand Down
31 changes: 21 additions & 10 deletions src/aioquic/quic/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
size_uint_var,
)
from . import events
from .configuration import QuicConfiguration
from .configuration import SMALLEST_MAX_DATAGRAM_SIZE, QuicConfiguration
from .crypto import CryptoError, CryptoPair, KeyUnavailableError
from .logger import QuicLoggerTrace
from .packet import (
Expand Down Expand Up @@ -46,7 +46,6 @@
push_quic_transport_parameters,
)
from .packet_builder import (
PACKET_MAX_SIZE,
QuicDeliveryState,
QuicPacketBuilder,
QuicPacketBuilderStop,
Expand Down Expand Up @@ -241,6 +240,10 @@ def __init__(
session_ticket_fetcher: Optional[tls.SessionTicketFetcher] = None,
session_ticket_handler: Optional[tls.SessionTicketHandler] = None,
) -> None:
assert configuration.max_datagram_size >= SMALLEST_MAX_DATAGRAM_SIZE, (
"The smallest allowed maximum datagram size is "
f"{SMALLEST_MAX_DATAGRAM_SIZE} bytes"
)
if configuration.is_client:
assert (
original_destination_connection_id is None
Expand Down Expand Up @@ -306,6 +309,7 @@ def __init__(
self._local_next_stream_id_bidi = 0 if self._is_client else 1
self._local_next_stream_id_uni = 2 if self._is_client else 3
self._loss_at: Optional[float] = None
self._max_datagram_size = configuration.max_datagram_size
self._network_paths: List[QuicNetworkPath] = []
self._pacing_at: Optional[float] = None
self._packet_number = 0
Expand Down Expand Up @@ -362,6 +366,7 @@ def __init__(
# loss recovery
self._loss = QuicPacketRecovery(
initial_rtt=configuration.initial_rtt,
max_datagram_size=self._max_datagram_size,
peer_completed_address_validation=not self._is_client,
quic_logger=self._quic_logger,
send_probe=self._send_probe,
Expand Down Expand Up @@ -503,6 +508,7 @@ def datagrams_to_send(self, now: float) -> List[Tuple[bytes, NetworkAddress]]:
builder = QuicPacketBuilder(
host_cid=self.host_cid,
is_client=self._is_client,
max_datagram_size=self._max_datagram_size,
packet_number=self._packet_number,
peer_cid=self._peer_cid.cid,
peer_token=self._peer_token,
Expand Down Expand Up @@ -541,8 +547,11 @@ def datagrams_to_send(self, now: float) -> List[Tuple[bytes, NetworkAddress]]:
builder.max_flight_bytes = (
self._loss.congestion_window - self._loss.bytes_in_flight
)
if self._probe_pending and builder.max_flight_bytes < PACKET_MAX_SIZE:
builder.max_flight_bytes = PACKET_MAX_SIZE
if (
self._probe_pending
and builder.max_flight_bytes < self._max_datagram_size
):
builder.max_flight_bytes = self._max_datagram_size

# limit data on un-validated network paths
if not network_path.is_validated:
Expand Down Expand Up @@ -2468,14 +2477,16 @@ def _parse_transport_parameters(
frame_type=QuicFrameType.CRYPTO,
reason_phrase="max_ack_delay must be < 2^14",
)
if (
quic_transport_parameters.max_udp_payload_size is not None
and quic_transport_parameters.max_udp_payload_size < 1200
if quic_transport_parameters.max_udp_payload_size is not None and (
quic_transport_parameters.max_udp_payload_size
< SMALLEST_MAX_DATAGRAM_SIZE
):
raise QuicConnectionError(
error_code=QuicErrorCode.TRANSPORT_PARAMETER_ERROR,
frame_type=QuicFrameType.CRYPTO,
reason_phrase="max_udp_payload_size must be >= 1200",
reason_phrase=(
f"max_udp_payload_size must be >= {SMALLEST_MAX_DATAGRAM_SIZE}"
),
)

# store remote parameters
Expand Down Expand Up @@ -2532,7 +2543,7 @@ def _serialize_transport_parameters(self) -> bytes:
initial_source_connection_id=self._local_initial_source_connection_id,
max_ack_delay=25,
max_datagram_frame_size=self._configuration.max_datagram_frame_size,
quantum_readiness=b"Q" * 1200
quantum_readiness=b"Q" * SMALLEST_MAX_DATAGRAM_SIZE
if self._configuration.quantum_readiness_test
else None,
stateless_reset_token=self._host_cids[0].stateless_reset_token,
Expand All @@ -2555,7 +2566,7 @@ def _serialize_transport_parameters(self) -> bytes:
),
)

buf = Buffer(capacity=3 * PACKET_MAX_SIZE)
buf = Buffer(capacity=3 * self._max_datagram_size)
push_quic_transport_parameters(buf, quic_transport_parameters)
return buf.data

Expand Down
8 changes: 4 additions & 4 deletions src/aioquic/quic/packet_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
is_long_header,
)

PACKET_MAX_SIZE = 1280
PACKET_LENGTH_SEND_SIZE = 2
PACKET_NUMBER_SEND_SIZE = 2

Expand Down Expand Up @@ -64,6 +63,7 @@ def __init__(
peer_cid: bytes,
version: int,
is_client: bool,
max_datagram_size: int,
packet_number: int = 0,
peer_token: bytes = b"",
quic_logger: Optional[QuicLoggerTrace] = None,
Expand Down Expand Up @@ -98,9 +98,9 @@ def __init__(
self._packet_start = 0
self._packet_type = 0

self._buffer = Buffer(PACKET_MAX_SIZE)
self._buffer_capacity = PACKET_MAX_SIZE
self._flight_capacity = PACKET_MAX_SIZE
self._buffer = Buffer(max_datagram_size)
self._buffer_capacity = max_datagram_size
self._flight_capacity = max_datagram_size

@property
def packet_is_empty(self) -> bool:
Expand Down
29 changes: 16 additions & 13 deletions src/aioquic/quic/recovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,8 @@
K_SECOND = 1.0

# congestion control
K_MAX_DATAGRAM_SIZE = 1280
K_INITIAL_WINDOW = 10 * K_MAX_DATAGRAM_SIZE
K_MINIMUM_WINDOW = 2 * K_MAX_DATAGRAM_SIZE
K_INITIAL_WINDOW = 10
K_MINIMUM_WINDOW = 2
K_LOSS_REDUCTION_FACTOR = 0.5


Expand All @@ -37,7 +36,8 @@ def __init__(self) -> None:


class QuicPacketPacer:
def __init__(self) -> None:
def __init__(self, *, max_datagram_size: int) -> None:
self._max_datagram_size = max_datagram_size
self.bucket_max: float = 0.0
self.bucket_time: float = 0.0
self.evaluation_time: float = 0.0
Expand Down Expand Up @@ -68,13 +68,13 @@ def update_bucket(self, now: float) -> None:
def update_rate(self, congestion_window: int, smoothed_rtt: float) -> None:
pacing_rate = congestion_window / max(smoothed_rtt, K_MICRO_SECOND)
self.packet_time = max(
K_MICRO_SECOND, min(K_MAX_DATAGRAM_SIZE / pacing_rate, K_SECOND)
K_MICRO_SECOND, min(self._max_datagram_size / pacing_rate, K_SECOND)
)

self.bucket_max = (
max(
2 * K_MAX_DATAGRAM_SIZE,
min(congestion_window // 4, 16 * K_MAX_DATAGRAM_SIZE),
2 * self._max_datagram_size,
min(congestion_window // 4, 16 * self._max_datagram_size),
)
/ pacing_rate
)
Expand All @@ -87,9 +87,10 @@ class QuicCongestionControl:
New Reno congestion control.
"""

def __init__(self) -> None:
def __init__(self, *, max_datagram_size: int) -> None:
self._max_datagram_size = max_datagram_size
self.bytes_in_flight = 0
self.congestion_window = K_INITIAL_WINDOW
self.congestion_window = K_INITIAL_WINDOW * self._max_datagram_size
self._congestion_recovery_start_time = 0.0
self._congestion_stash = 0
self._rtt_monitor = QuicRttMonitor()
Expand All @@ -111,7 +112,7 @@ def on_packet_acked(self, packet: QuicSentPacket) -> None:
count = self._congestion_stash // self.congestion_window
if count:
self._congestion_stash -= count * self.congestion_window
self.congestion_window += count * K_MAX_DATAGRAM_SIZE
self.congestion_window += count * self._max_datagram_size

def on_packet_sent(self, packet: QuicSentPacket) -> None:
self.bytes_in_flight += packet.sent_bytes
Expand All @@ -131,7 +132,8 @@ def on_packets_lost(self, packets: Iterable[QuicSentPacket], now: float) -> None
if lost_largest_time > self._congestion_recovery_start_time:
self._congestion_recovery_start_time = now
self.congestion_window = max(
int(self.congestion_window * K_LOSS_REDUCTION_FACTOR), K_MINIMUM_WINDOW
int(self.congestion_window * K_LOSS_REDUCTION_FACTOR),
K_MINIMUM_WINDOW * self._max_datagram_size,
)
self.ssthresh = self.congestion_window

Expand All @@ -153,6 +155,7 @@ class QuicPacketRecovery:
def __init__(
self,
initial_rtt: float,
max_datagram_size: int,
peer_completed_address_validation: bool,
send_probe: Callable[[], None],
logger: Optional[logging.LoggerAdapter] = None,
Expand All @@ -178,8 +181,8 @@ def __init__(
self._time_of_last_sent_ack_eliciting_packet = 0.0

# congestion control
self._cc = QuicCongestionControl()
self._pacer = QuicPacketPacer()
self._cc = QuicCongestionControl(max_datagram_size=max_datagram_size)
self._pacer = QuicPacketPacer(max_datagram_size=max_datagram_size)

@property
def bytes_in_flight(self) -> int:
Expand Down
Loading

0 comments on commit 2ae1ad4

Please sign in to comment.