Creating a `ClientConfig` can on some platforms take an excessive amount
of time, likely due to rustls scanning the systems cert store.
This slows down all functions which make use of `quinn::Endpoint::builder()`,
even if the client config is known upfront and shared between mutliple connections.
In some tests I noticed an extra connection establishment time of 1s for
creating a client.
This changes will create the `ClientConfig` only if the Endpoint is built
when the `ClientConfig` hasn't been set by the application.
Server side code still suffers from the issue, since `ClientConfig`s there are not
known. However it might be less impacting, since a second of startup time for
a server matters less than for a client.
Specifically `TransportConfig::max_concurrent_bidi_streams` and `TransportConfig::max_concurrent_uni_streams` return `quinn_proto::ConfigError`.
This prevents consumers of `quinn` having to bring in `quinn-proto` just to use `ConfigError`, for example in their custom error type.
A stream's size is only known after we've issued at least that much
flow control credit, so the peer necessarily won't need any more. Our
logic to schedule credit issuing already uses similar judgement, so
this brings the last-minute sanity check into alignment.
Rusts default HashMap and HashSet use siphash as a hash method to
protect again malicious input. The downside of siphash is that it is slightly
more expensive than a more primitive mechanism. The siphash operations
currently show up in quinn flamegraphs - even if they only act on 8byte
input data like Stream IDs.
The security properties of siphash are not really required in the context
of quic, where the hashmaps are used with 2 key types:
- Stream IDs: The valid range for those is predetermined by the maximum
concurrent stream setting. The peer can't pick from a bigger range in
order to get more hashes into the same bucket. Besides this, the risk of
resource exhaustion via purely creating more requests/streams is already
much higher than exhaustion of the pure maps.
- Connection IDs: Connection IDs which are inserted into tracking data
structurues are created locally. The peer has no control over them.
Users found it rather confusing that binding to the default address
(::0) doesn't allow incoming connections via IPv4 on windows.
This change will allow for this, by using a IPv6 socket and
disabling IPv6-only on all platforms. This lets the windows and unix
implementations behave equally.
The executable names conflicted with the interop server:
> The bin target `server` in package `perf v0.1.0 (C:\Users\matth\Code\rust\quinn\perf)` has the same output filename as the bin target `server` in package `interop v0.1.0 (C:\Users\matth\Code\rust\quinn\interop)`.
> Colliding filename is: C:\Users\matth\Code\rust\quinn\target\debug\server.pdb
> The targets should have unique names.
This changes poll_transmit to accept the maximum number of datagrams
which are stored inside a single transmit.
The number wil be derived from the platforms capabilities. This will
automatically activate GSO on platforms which support it.
Quoth the spec: a server MUST expand the payload of all UDP datagrams
carrying ack-eliciting Initial packets to at least the smallest
allowed maximum datagram size of 1200 bytes.
Use zero-copy APIs when sending data in perf application.
Performance difference:
1. First line is before
2. Second line is after (but with only the download write change)
```
│ Duration │ FBL | Upload Throughput | Download Throughput
──────┼───────────┼───────────┼───────────────────┼────────────────────
AVG │ 5.00ms │ 1.00ms │ 332.82 MiB/s │ 334.20 MiB/s
AVG │ 5.00ms │ 0.00ns │ 332.64 MiB/s │ 335.96 MiB/s
```
=> Makes less than 1%. Still neat to have
This extends the public quinn API to offer support for writing owned buffers.
Besides the universal `write_chunks` API which supports a variable amount
of buffers the API also offers convenience methods for writing either a
single buffer completely or an arbitrary amount of buffers completely.
When a frame contained data for an offset of 0, the transmit logic did not
fully fill a packet. The reason for this is that the logic reserved 1 byte for
writing an offset. However storing offset 0 doesn't require 1 byte,
because it will be encoded with a special flag in the message header.
Without GSO, this isn't a huge issue. It mostly means we are wasting
1 byte per datagram.
However with GSO, this actually leads to data corruption:
When using GSO, and packet isn't fully filled, it later gets padded
to MTU size. Padding a packet ending with a STREAM frame that
doesn't contain a length information is however invalid, and will let
the receiver assume the padding is part of part of the data. This means
the receiver will receive an extra `0` at the end of a stream - and
depending on the duplicate detection at the receiver either the 0
or the correct byte in the next packet will win.