Skip to content

Commit 13b7dab

Browse files
author
bneradt
committed
Fix H3 quiche traffic handling
# Overview This patch extends the HTTP/3 autest coverage, using curl, golang, and Proxy Verifier HTTP/3 clients to generate their implementations of h3 traffic. It also adds request and response bodies of various sizes, including "large" 300k bodies to exercise multiple packet, buffer, and flow control ATS HTTP/3 implementations. It also exercises some interesting requests, such as HEAD and 204 responses. This patch also includes the various production fixes needed for these tests. # Issues Found and their Fixes ## UDP batches could stall large H3 transfers Large request and response bodies exposed a UDP receive starvation bug in the UDP read path. On systems using `recvmmsg()` with edge-triggered readiness, ATS could read one full batch of datagrams and then leave the rest queued in the kernel without another readable event to wake the QUIC stack. This changes `UDPNetProcessorInternal::read_multiple_messages_from_net()` in `src/iocore/net/UnixUDPNet.cc` to return whether the kernel supplied a full batch. `udp_read_from_net()` now loops while full batches are returned, draining the socket before handing packets to QUIC and avoiding large-transfer stalls caused by unread UDP bursts. ## QUIC stream writes consumed data before quiche accepted it The stream write path consumed the `QUICStreamVCAdapter` write reader inside `_read()`, before `QUICStream::send_data()` knew whether `quiche_conn_stream_send()` had accepted the bytes. When quiche accepted only a partial write or returned a flow-control error, ATS could lose stream data and report write progress too early. This makes `QUICStream::send_data()` keep a pending `IOBufferBlock`/FIN pair until quiche reports successful consumption, and only then calls the new `QUICStreamAdapter::consume()` hook. The concrete reader accounting lives in `QUICStreamVCAdapter::_consume()`, while `QUICStream::has_data_to_send()`, `QUICStream::on_write()`, and `QUICNetVConnection::on_stream_updated()` make newly writable stream data schedule packet writes again. This also treats completed finite writes with only FIN left as writable stream state, so empty bodies and fully consumed bodies still close the H3 stream cleanly. ## H3 transaction cleanup raced with stream closure The timeout and stream lifetime tests exposed cases where an `HQTransaction` could be deleted while an event handler was still active, or while the QUIC stream adapter still had read/write cleanup to finish. That left later stream-close and timeout paths touching state that had already been torn down. This adds explicit transaction lifetime state in `HQTransaction`: `_closed`, `_stream_closed`, `_event_handler_active`, and `_is_write_buffer_flushed()`. `Http3App::on_stream_close()` now calls `HQTransaction::stream_closed()`, and `HQTransaction::_delete_if_possible()` waits until the transaction is done, the stream is closed or no longer readable, and pending writes have flushed before deleting the transaction. ## H3 read completion could run before headers and DATA were settled The H3 request read path could signal completion before asynchronous QPACK header decode and buffered DATA delivery had finished updating the sink VIO. That showed up around HEAD, 204, and stream-close timing because the HTTP state machine needed a stable view of whether headers were decoded and whether a request body existed. This updates `Http3HeaderVIOAdaptor::_on_qpack_decode_complete()` to add the printed header length to the sink VIO and notify `Http3Transaction::on_header_decode_complete()`, which schedules the appropriate read event. `Http3StreamDataVIOAdaptor::finalize()` now uses a persistent reader, writes buffered DATA into the sink VIO exactly once, and updates `ndone`/`nbytes` consistently before the transaction is signaled. ## The QPACK static table had drifted from the standard table The HEAD, 204, and quic-go coverage exposed that ATS's static QPACK table was not the table used by external HTTP/3 implementations. The extra zstd entry and modified `accept-encoding` value in `src/proxy/http3/QPACK.cc` shifted later static indexes, so an externally encoded `:status 204` could decode as a different status. This restores the standard static table entries by using `accept-encoding: gzip, deflate, br` and removing the non-standard `content-encoding: zstd` entry. The new 204 cases in `tests/gold_tests/h3/replays/h3_proxy_verifier.replay.yaml` and `tests/gold_tests/h3/replays/h3_server_for_go_client.replay.yaml` cover this interoperability point with Proxy Verifier and quic-go.
1 parent fb1f03d commit 13b7dab

46 files changed

Lines changed: 2085 additions & 103 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

ci/rat-exclude.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ blib/**
2424
**/*.default.in
2525
**/*.config
2626
**/*.gold
27+
**/go.mod
28+
**/go.sum
2729
**/*.hrw4u
2830
**/.gitignore
2931
**/.gitmodules

include/iocore/net/quic/Mock.h

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
#include "iocore/net/quic/QUICStreamAdapter.h"
2929
#include "iocore/net/quic/QUICStream.h"
3030

31+
#include <algorithm>
32+
3133
class MockQUICContext;
3234

3335
using namespace std::literals;
@@ -191,6 +193,11 @@ class MockQUICConnectionInfoProvider : public QUICConnectionInfoProvider
191193
{
192194
return negotiated_application_name_sv;
193195
}
196+
197+
void
198+
on_stream_updated() override
199+
{
200+
}
194201
};
195202

196203
class MockQUICStreamManager : public QUICStreamManager
@@ -431,6 +438,11 @@ class MockQUICConnection : public QUICConnection
431438
return negotiated_application_name_sv;
432439
}
433440

441+
void
442+
on_stream_updated() override
443+
{
444+
}
445+
434446
int _transmit_count = 0;
435447
int _retransmit_count = 0;
436448
Ptr<ProxyMutex> _mutex;
@@ -519,13 +531,19 @@ class MockQUICStreamAdapter : public QUICStreamAdapter
519531
Ptr<IOBufferBlock>
520532
_read(size_t len) override
521533
{
522-
this->_sending_data_len -= len;
523-
Ptr<IOBufferBlock> block = make_ptr<IOBufferBlock>(new_IOBufferBlock());
534+
len = std::min(len, this->_sending_data_len);
535+
Ptr<IOBufferBlock> block = make_ptr<IOBufferBlock>(new_IOBufferBlock());
524536
block->alloc(iobuffer_size_to_index(len, BUFFER_SIZE_INDEX_32K));
525537
block->fill(len);
526538
return block;
527539
}
528540

541+
void
542+
_consume(size_t len) override
543+
{
544+
this->_sending_data_len -= std::min(len, this->_sending_data_len);
545+
}
546+
529547
private:
530548
size_t _sending_data_len = 0;
531549
size_t _total_sending_data_len = 0;

include/iocore/net/quic/QUICConnection.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ class QUICConnectionInfoProvider
6060
virtual bool is_handshake_completed() const = 0;
6161
virtual QUICVersion negotiated_version() const = 0;
6262
virtual std::string_view negotiated_application_name() const = 0;
63+
virtual void on_stream_updated() = 0;
6364
};
6465

6566
class QUICConnection : public QUICConnectionInfoProvider

include/iocore/net/quic/QUICStream.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
#include "tscore/List.h"
2727

2828
#include "iocore/eventsystem/Event.h"
29+
#include "iocore/eventsystem/IOBuffer.h"
2930

3031
#include "iocore/net/quic/QUICConnection.h"
3132
#include "iocore/net/quic/QUICDebugNames.h"
@@ -53,6 +54,7 @@ class QUICStream
5354
QUICStreamDirection direction() const;
5455
bool is_bidirectional() const;
5556
bool has_no_more_data() const;
57+
bool has_data_to_send();
5658

5759
QUICOffset final_offset() const;
5860

@@ -66,6 +68,7 @@ class QUICStream
6668
* QUICApplication need to call one of these functions when it process VC_EVENT_*
6769
*/
6870
void on_read();
71+
void on_write();
6972
void on_eos();
7073

7174
/**
@@ -85,6 +88,9 @@ class QUICStream
8588
uint64_t _received_bytes = 0;
8689
uint64_t _sent_bytes = 0;
8790
bool _has_no_more_data = false;
91+
Ptr<IOBufferBlock> _pending_send_block;
92+
bool _pending_send_fin = false;
93+
bool _sent_fin = false;
8894
};
8995

9096
class QUICStreamStateListener

include/iocore/net/quic/QUICStreamAdapter.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ class QUICStreamAdapter
3939

4040
virtual int64_t write(QUICOffset offset, const uint8_t *data, uint64_t data_length, bool fin) = 0;
4141
Ptr<IOBufferBlock> read(size_t len);
42+
void consume(size_t len);
4243
virtual bool is_eos() = 0;
4344
virtual uint64_t unread_len() = 0;
4445
virtual uint64_t read_len() = 0;
@@ -60,6 +61,7 @@ class QUICStreamAdapter
6061
virtual void notify_eos() = 0;
6162

6263
protected:
63-
virtual Ptr<IOBufferBlock> _read(size_t len) = 0;
64+
virtual Ptr<IOBufferBlock> _read(size_t len) = 0;
65+
virtual void _consume(size_t len) = 0;
6466
QUICStream &_stream;
6567
};

include/iocore/net/quic/QUICStreamVCAdapter.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ class QUICStreamVCAdapter : public VConnection, public QUICStreamAdapter
6565

6666
protected:
6767
Ptr<IOBufferBlock> _read(size_t len) override;
68+
void _consume(size_t len) override;
6869

6970
VIO _read_vio;
7071
VIO _write_vio;

include/iocore/net/quic/QUICTypes.h

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -455,9 +455,9 @@ class QUICFiveTuple
455455
int protocol() const;
456456

457457
private:
458-
IpEndpoint _source;
459-
IpEndpoint _destination;
460-
int _protocol;
458+
IpEndpoint _source{};
459+
IpEndpoint _destination{};
460+
int _protocol = 0;
461461
uint64_t _hash_code = 0;
462462
};
463463

include/proxy/http3/Http3StreamDataVIOAdaptor.h

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,9 @@ class Http3StreamDataVIOAdaptor : public Http3FrameHandler
4242
bool has_data();
4343

4444
private:
45-
VIO *_sink_vio = nullptr;
46-
int64_t _total_data_length = 0;
47-
MIOBuffer *_buffer;
45+
VIO *_sink_vio = nullptr;
46+
int64_t _total_data_length = 0;
47+
MIOBuffer *_buffer = nullptr;
48+
IOBufferReader *_reader = nullptr;
49+
bool _finalized = false;
4850
};

include/proxy/http3/Http3Transaction.h

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ class HQTransaction : public ProxyTransaction
5353
void transaction_done() override;
5454
void release() override;
5555
int get_transaction_id() const override;
56+
void stream_closed();
5657
void increment_transactions_stat() override;
5758
void decrement_transactions_stat() override;
5859

@@ -81,6 +82,7 @@ class HQTransaction : public ProxyTransaction
8182
void _schedule_read_complete_event();
8283
void _unschedule_read_complete_event();
8384
void _close_read_complete_event(Event *e);
85+
void _schedule_read_event();
8486
void _schedule_write_ready_event();
8587
void _unschedule_write_ready_event();
8688
void _close_write_ready_event(Event *e);
@@ -90,12 +92,14 @@ class HQTransaction : public ProxyTransaction
9092
void _signal_event(int event, Event *e);
9193
void _signal_read_event();
9294
void _signal_write_event();
95+
bool _is_write_buffer_flushed();
9396
void _delete_if_possible();
9497

9598
EThread *_thread = nullptr;
9699

97100
MIOBuffer _read_vio_buf{BUFFER_SIZE_INDEX_4K};
98101
QUICStreamVCAdapter::IOInfo &_info;
102+
QUICStreamId _stream_id = 0;
99103

100104
size_t _sent_bytes = 0;
101105

@@ -106,7 +110,10 @@ class HQTransaction : public ProxyTransaction
106110
Event *_write_ready_event = nullptr;
107111
Event *_write_complete_event = nullptr;
108112

109-
bool _transaction_done = false;
113+
bool _transaction_done = false;
114+
bool _event_handler_active = false;
115+
bool _closed = false;
116+
bool _stream_closed = false;
110117
};
111118

112119
class Http3Transaction : public HQTransaction
@@ -121,6 +128,7 @@ class Http3Transaction : public HQTransaction
121128
int state_stream_closed(int event, Event *data) override;
122129

123130
void do_io_close(int lerrno = -1) override;
131+
void on_header_decode_complete();
124132

125133
bool is_response_header_sent() const;
126134
bool is_response_body_sent() const;

include/proxy/logging/TransactionLogData.h

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
#include "proxy/hdrs/HTTP.h"
2828
#include "tscore/ink_inet.h"
2929

30+
#include <cstddef>
31+
#include <cstdint>
3032
#include <optional>
3133
#include <string_view>
3234

@@ -99,13 +101,16 @@ class TransactionLogData
99101
SquidHierarchyCode get_hier_code() const;
100102

101103
// ===== Byte counters =====
102-
int64_t get_client_request_body_bytes() const;
103-
int64_t get_client_response_hdr_bytes() const;
104-
int64_t get_client_response_body_bytes() const;
105-
int64_t get_server_request_body_bytes() const;
106-
int64_t get_server_response_body_bytes() const;
107-
int64_t get_cache_response_body_bytes() const;
108-
int64_t get_cache_response_hdr_bytes() const;
104+
int64_t get_client_request_body_bytes() const;
105+
int64_t get_client_response_hdr_bytes() const;
106+
int64_t get_client_response_body_bytes() const;
107+
int64_t get_server_request_body_bytes() const;
108+
int64_t get_server_response_body_bytes() const;
109+
int64_t get_cache_response_body_bytes() const;
110+
int64_t get_cache_response_hdr_bytes() const;
111+
uint64_t get_client_tls_handshake_bytes_rx() const;
112+
uint64_t get_client_tls_handshake_bytes_tx() const;
113+
size_t get_client_tls_early_data_len() const;
109114

110115
// ===== Transaction identifiers =====
111116
int64_t get_sm_id() const;

0 commit comments

Comments
 (0)