OF-2773 QUIC support#3251
Conversation
Add foundational support for XMPP-over-QUIC client listeners in the existing connection management architecture, while keeping runtime transport implementation intentionally incremental.\n\nCore model changes:\n- Added a new connection type, QUIC_C2S, with SOCKET_C2S fallback semantics for client-oriented behavior and configuration inheritance where applicable.\n- Added a default QUIC client port constant (5224) to ConnectionManager.\n- Added QUIC-specific client settings in ConnectionSettings.Client (enabled flag, port, thread/read-buffer properties, auth policy property, idle timeout property and max-streams system property).\n\nListener lifecycle integration:\n- ConnectionManagerImpl now constructs and manages a dedicated QUIC client listener.\n- QUIC listeners are included in getListeners(), getListener(), and getListeners(type) routing logic.\n- ConnectionListener now stores ConnectionAcceptor (interface type) instead of NettyConnectionAcceptor (implementation type), and instantiates acceptors per connection type.\n- Added QuicConnectionAcceptor as a lifecycle placeholder that integrates with listener start/stop/reconfigure behavior and logs explicit runtime-not-yet-implemented warnings when enabled.\n\nRate limiting alignment:\n- NewConnectionLimiterRegistry now classifies QUIC_C2S as part of the shared C2S limiter bucket, consistent with existing TCP/BOSH C2S grouping.\n\nTests:\n- Extended ConnectionTypeTest to verify QUIC_C2S is client-oriented.\n- Extended NewConnectionLimiterRegistryTest to verify QUIC shares the C2S limiter and receives dynamic C2S limiter updates.\n\nValidation run:\n- ./mvnw -pl xmppserver -Dtest=ConnectionTypeTest,NewConnectionLimiterRegistryTest test\n- Result: BUILD SUCCESS (12 tests, 0 failures).
Expose the new QUIC_C2S listener as a first-class admin-managed connection type, with UI and localization updates that align with existing house style for connection configuration pages.\n\nNew admin page:\n- Added connection-settings-quic-c2s.jsp for QUIC listener management.\n- Supports enabling/disabling the listener, setting QUIC port, configuring QUIC idle timeout, and configuring max streams per session.\n- Includes CSRF protection, validation, logging, success/error messaging, and a link to the existing advanced settings page for QUIC_C2S in direct TLS mode.\n\nNavigation and visibility:\n- Added a dedicated sidebar entry for QUIC client connections in admin-sidebar.xml.\n- Added QUIC certificate-store navigation entries (identity/trust) under TLS certificate management.\n- Updated index.jsp server ports overview to display QUIC listener type/description and link to the new QUIC settings page.\n\nExisting admin page compatibility:\n- Extended connection-settings-advanced.jsp to understand QUIC_C2S in connection-type translation, page title/pageID selection, and success messaging.\n- Extended certificate management pages to include QUIC_C2S labels (security-certificate-store-management.jsp and security-certificate-details.jsp).\n\nLocalization:\n- Added English i18n keys for:\n - sidebar labels/descriptions for QUIC settings\n - QUIC ports table labels/descriptions\n - QUIC connection-settings page text and validation errors\n - QUIC connection-type label for advanced settings\n - QUIC certificate-store/truststore labels\n - QUIC-related system property descriptions\n\nValidation run:\n- ./mvnw -pl xmppserver -Dtest=ConnectionTypeTest,NewConnectionLimiterRegistryTest test\n- Result: BUILD SUCCESS (12 tests, 0 failures).
Replace the QUIC acceptor placeholder with a Netty QUIC server bootstrap that binds a UDP listener, checks native QUIC availability, and initializes a single bidirectional stream pipeline for XMPP stanza traffic. The stream pipeline now mirrors the existing socket C2S stack: traffic shaping, idle handling, keepalive behavior, XMPP decoding, UTF-8 encoding, write-timeout protection, deferred auto-read startup, and business-logic execution on a blocking executor pool. Introduce a dedicated QuicClientConnectionHandler and wire it through NettyConnectionHandlerFactory so QUIC uses the standard client stanza processing path while marking connections encrypted from creation time. Update NettyConnection address resolution to support non-Inet stream channels by falling back to the parent channel remote address. This is required for QUIC stream channels, where peer socket addressing is exposed on the parent channel. Add Netty QUIC dependencies to xmppserver/pom.xml (classes artifact plus linux-x86_64 native runtime) so QUIC server bootstrap classes and native runtime are available at build/runtime.
Introduce NettyConnectionTest cases that validate peer-address resolution behavior for both standard socket channels and QUIC-style stream channels. The new tests verify three scenarios: direct InetSocketAddress resolution, fallback to the parent channel when the stream channel has no InetSocketAddress, and UnknownHostException when neither channel exposes a usable InetSocketAddress. These tests cover the address-resolution logic that QUIC depends on and guard against regressions that would break host/address lookups on stream-backed connections.
Resolve QUIC handshake failures caused by ALPN mismatch by replacing the hardcoded server ALPN value with configurable settings that default to the XMPP C2S token . Add () as a dynamic comma-separated list property, allowing operators to include additional ALPN identifiers for interoperability testing or non-standard clients. Update QUIC SSL context construction to read the configured ALPN list and fail fast on an empty list, producing a clear startup error instead of opaque handshake failures. Document the new system property in i18n system-property descriptions for admin visibility.
Fix NettyConnection peer-address resolution for QUIC by reading when traversing the channel hierarchy. Previously, QUIC stream and connection channels exposed connection/stream IDs as , which caused host resolution to fail and client session admission checks to reject the connection before session creation. The updated resolution logic now walks the channel ancestry and prioritizes the QUIC socket-level peer address when available, then falls back to InetSocketAddress remoteAddress values on each channel. Extend NettyConnectionTest with a QUIC-specific case that simulates stream/connection address types and validates successful resolution via QUIC remote socket address.
Increase the default QUIC stream budget to twenty-five streams and add a dedicated outbound stream cap (default twenty) for server-initiated send streams. Introduce QUIC session stream routing that is shared per QUIC connection. The router tracks inbound streams, opens outbound streams when needed, and chooses a deterministic outbound stream per stanza based on the bare from JID. Enforce stable routing rules for outbound stanzas: traffic from the server bare JID and from the authenticated account bare JID stays on the first stream, while other bare-JID keys are pinned to consistently selected non-primary streams. Add a QUIC-specific client stanza handler that allows additional inbound streams to attach to the existing LocalClientSession instead of creating a new session. This enables clients to send stanzas over multiple streams in one QUIC connection. Update NettyConnection close and delivery behavior for QUIC: closing non-primary streams no longer forces full session closure, and outbound packet delivery is delegated to the QUIC stream router when available. Document the new outbound stream property in system-property descriptions.
|
Commit messages use a funky literal |
| private static final Logger Log = LoggerFactory.getLogger(QuicConnectionAcceptor.class); | ||
|
|
||
| public static final SystemProperty<Duration> WRITE_TIMEOUT_SECONDS = SystemProperty.Builder.ofType(Duration.class) | ||
| .setKey("xmpp.quic.client.write-timeout-seconds") |
There was a problem hiding this comment.
This needs a system_property.xmpp.quic.client.write-timeout-seconds i18n entry to describe the property. Such descriptions are shown in the admin console's System Property page.
| import static org.junit.jupiter.api.Assertions.assertEquals; | ||
| import static org.junit.jupiter.api.Assertions.assertThrows; | ||
|
|
||
| public class NettyConnectionTest |
There was a problem hiding this comment.
I'd like some minimal javadoc that describes what each test is supposed to test / why.
|
|
||
| final QuicServerCodecBuilder serverCodecBuilder = new QuicServerCodecBuilder() | ||
| .sslContext(sslContext) | ||
| .tokenHandler(InsecureQuicTokenHandler.INSTANCE) |
There was a problem hiding this comment.
Javadoc of InsecureQuicTokenHandler reads:
This shouldn't be used in production.
Maybe we shouldn't use this in production.
| .initialMaxStreamDataBidirectionalLocal(10 * 1024 * 1024) | ||
| .initialMaxStreamDataBidirectionalRemote(10 * 1024 * 1024) | ||
| .initialMaxStreamsBidirectional(ConnectionSettings.Client.QUIC_MAX_STREAMS.getValue()) | ||
| .handler(new NewConnectionRateLimitHandler(ConnectionType.QUIC_C2S)) |
There was a problem hiding this comment.
With QUIC, the amount of transport-layers doesn't correspond to the amount of sessions, if I understand things correctly (which is a big 'if'). Is the rate limiter that's applied here on the transport, correct? Is this roughly equivalent to 'generate a new session' here, or do we now rate limit a transport on which many, many different sessions are likely to be created (should this rate limiter be moved to inside the initChannel thing?)
There was a problem hiding this comment.
This is a fascinating question wherein I lack the knowledge to answer. My guess is that it's "connection", ie QUIC session, not "stream".
| } | ||
|
|
||
| @Override | ||
| public synchronized void reconfigure(final ConnectionConfiguration configuration) |
There was a problem hiding this comment.
The Netty / TCP variant of reconfigure tries to modify state of all existing channels. This implementation does not. Is that OK?
There was a problem hiding this comment.
Probably not. I'll see about fixing.
| <groupId>io.netty</groupId> | ||
| <artifactId>netty-codec-native-quic</artifactId> | ||
| <version>${netty.version}</version> | ||
| <classifier>linux-x86_64</classifier> |
There was a problem hiding this comment.
This only works on (certain flavors of) Linux? Can this be made working on Windows? Alternatively, do things crash and burn when loaded on Windows? Should we have some kind of detection mechanism that gracefully informs an administrator if and why QUIC support is (un)available?
There was a problem hiding this comment.
Oh, apparently... The underlying library (Quiche) should work fine on Windows etc though. I'll look into getting it built on as many platforms as possible.
| .sslContext(sslContext) | ||
| .tokenHandler(InsecureQuicTokenHandler.INSTANCE) | ||
| .maxIdleTimeout(ConnectionSettings.Client.QUIC_IDLE_TIMEOUT_PROPERTY.getValue().toMillis(), TimeUnit.MILLISECONDS) | ||
| .initialMaxData(10 * 1024 * 1024) |
There was a problem hiding this comment.
Should these three hardcoded 10 * 1024 * 1024 values be made configurable?
There was a problem hiding this comment.
I don't know what MaxData is, actually. The other two are maxed out, then overridden by the bidirectional property.
Absolutely. |
Ew. Yes, I don't know why it's done that. |
…, async stream open, factory guard Bug 1: openOutboundStream() previously only added StringEncoder and WriteTimeoutHandler to server-initiated QUIC streams, leaving them with no inbound pipeline. Any data sent by the client on those streams was silently discarded and NettyConnection.reinit() would NPE. Fix: openOutboundStreamAsync() now builds the same full pipeline as the client-initiated stream initializer in QuicConnectionAcceptor (traffic shaping, idle/keepalive, NettyXMPPDecoder, StringEncoder, write-timeout, autoRead gate, QuicClientConnectionHandler). QuicSessionStreamRouter now receives ConnectionConfiguration and EventExecutorGroup from QuicConnectionAcceptor so it can construct the handler correctly. Bug 2: openOutboundStream() called awaitUninterruptibly(250ms) on the stream-open future, which can deadlock when called from a Netty I/O thread (deliver() -> writeStanza() path). Fix: replaced with a fully async approach. openOutboundStreamAsync() attaches a listener to the future; stanzas that arrive while the open is in flight are queued in pendingStanzas and flushed onto the new stream (or the primary stream as fallback) once the future completes. A streamOpenInFlight flag prevents concurrent open attempts. Bug 3: NettyConnectionHandlerFactory silently produced a QuicClientConnectionHandler without a QuicSessionStreamRouter for QUIC_C2S, which would fall back to plain ClientStanzaHandler and break multi-stream session sharing. Fix: throw UnsupportedOperationException with a clear message directing callers to QuicConnectionAcceptor. Tests: NettyConnectionHandlerFactoryTest covers the UnsupportedOperationException for QUIC_C2S and verifies correct handler types for SOCKET_C2S and SOCKET_S2S. Co-authored-by: Junie <junie@jetbrains.com>
…t, ALPN/token-handler docs - QuicSessionStreamRouter: replace CopyOnWriteArrayList with plain ArrayList for inboundConnections and outboundChannels. All access is already guarded by the instance monitor (synchronized methods), so COWL was redundant and misleading. - ConnectionSettings.Client.QUIC_MAX_OUTBOUND_STREAMS: change default from 20 to 0. The previous default of 20 could cause the server to attempt opening more server-initiated streams than the client permits (QUIC_MAX_STREAMS defaults to 25 inbound, leaving no headroom). Defaulting to 0 disables server-initiated streams until an operator explicitly enables them. Added a Javadoc comment explaining the relationship between the two properties. - ConnectionSettings.Client.QUIC_ALPN: add Javadoc noting that 'xmpp-client' is a placeholder pending finalisation of the IETF XMPP-over-QUIC draft. - QuicConnectionAcceptor: add a TODO comment on InsecureQuicTokenHandler warning that it must be replaced with a real address-validation token handler before production deployment to prevent UDP amplification attacks. Co-authored-by: Junie <junie@jetbrains.com>
On aux QUIC streams the stream-open exchange is pure boilerplate — the session is already authenticated and bound on the primary stream. This change makes the server proactively initialise each aux stream (both client-initiated and server-initiated) without waiting for the client to send <stream:stream>: - Add QuicClientStanzaHandler.initAsAuxStream(LocalClientSession): sets session + sessionCreated=true, wires the NettyConnection via reinit(), and sends the server stream-open response immediately. - Override QuicClientConnectionHandler.handlerAdded(): after the parent creates the NettyConnection and stanza handler, if a session already exists on the stream router (i.e. this is an aux stream), call initAsAuxStream() so the stream is ready for stanzas as soon as the channel becomes active. - Netty buffers the stream-open write until channelActive fires, so issuing it from handlerAdded() is safe. - Clients that still send <stream:stream> on an aux stream are handled gracefully: createSession() detects sessionCreated=true and simply sends another stream-open response without re-wiring the session. - Server-initiated (outbound) aux streams benefit automatically: the existing openOutboundStreamAsync() pipeline already uses QuicClientConnectionHandler with the stream router, so handlerAdded triggers initAsAuxStream() there too. Co-authored-by: Junie <junie@jetbrains.com>
Addresses client-agent diagnostic request for stream-credit investigation. Answers to client questions: 1. QUIC stack: Netty/quiche (netty-codec-classes-quic 4.2.10.Final) ALPN: configurable via xmpp.quic.client.alpn, default 'xmpp-client' 2. Transport parameters advertised to client (logged at INFO on startup): - initial_max_streams_bidi : 25 (xmpp.quic.client.max-streams, default 25) - initial_max_streams_uni : 0 (uni streams not used) - initial_max_data : 10 MB - initial_max_stream_data_bidi_local : 10 MB - initial_max_stream_data_bidi_remote : 10 MB - initial_max_stream_data_uni : 0 - max_idle_timeout : 360000 ms (xmpp.quic.client.idle, default 6 min) 3. MAX_STREAMS frames: handled automatically by quiche as streams close. No application-layer intervention required. 4/5. Server accepts all client-initiated bidi streams via streamHandler; server-initiated streams controlled by xmpp.quic.client.max-outbound-streams (default 0 = disabled). 6. Idle timeout: server advertises 6 min; connection teardown at 14-20 s suggests the *client* has a shorter idle timeout or keepalive issue. Changes: - Log all transport parameters at INFO level when listener starts so they can be verified from server logs without a packet capture. - Add xmpp.quic.client.qlog-dir property: when set to a writable directory, enables per-connection qlog files (one .qlog file per QUIC connection). Use qvis (https://qvis.quictools.info/) to inspect transport_parameters_set and max_streams frames. Disabled by default (empty string). - Add explicit .initialMaxStreamsUnidirectional(0) to document intent. - Add QLogConfiguration/QuicChannelOption imports. Co-authored-by: Junie <junie@jetbrains.com>
Adds INFO-level logging in two places to make inbound QUIC stream handling observable from the server log alone (no qlog capture needed): - QuicConnectionAcceptor stream handler: logs streamId, type, parent QUIC channel and remote address for every new inbound stream, so we can verify that a client open_bi() request actually reached the server. - QuicClientConnectionHandler.handlerAdded: logs whether the stream was classified as PRIMARY (no session yet on the QUIC connection, awaiting <stream:stream>) or AUX (session already bound, server-side auto-init). This directly addresses debugging of the reported symptom where client open_bi() for aux stream slot 0 never resolves: if the server log shows 'QUIC inbound stream initialised streamId=...', the request reached us and was accepted by the codec; if it does not, the issue is transport-layer (credits, flow control, idle timeout) rather than application-layer. Co-authored-by: Junie <junie@jetbrains.com>
…ents Add a per-connection handler on the QuicChannel pipeline that logs: - At connection establishment: peerAllowedBidiStreams and peerAllowedUniStreams (how many server-initiated streams the client is granting us) - On QuicStreamLimitChangedEvent: updated peer stream credit counts This is diagnostic infrastructure to help determine whether client-initiated aux stream opens are failing at the transport layer (STREAM frames not arriving) or the application layer (frames arrive but server doesn't accept them). If 'QUIC inbound stream initialised' never appears in the log after bind, but 'QUIC connection established' does, the client's open_bi() frames are not reaching the server's UDP socket — the issue is transport-layer (NAT, firewall, or client-side stream-credit misreading), not server application code. Co-authored-by: Junie <junie@jetbrains.com>
Aux streams now carry bare stanzas immediately with no <stream:stream> handshake. The server no longer sends a stream-open response when initialising an aux stream (initAsAuxStream), and if the client unexpectedly sends a stream-open on an aux stream the server wires up the session but does not reply with a stream-open of its own. Removed now-unused StringUtils import. Co-authored-by: Junie <junie@jetbrains.com>
Per XEP-0467 §3.3, XEP-0198 MUST NOT be advertised or negotiated over QUIC. Suppress the <sm xmlns='urn:xmpp:sm:2'/> and <sm xmlns='urn:xmpp:sm:3'/> stream features for QUIC connections, and reject any client <enable/> attempt arriving on a QUIC connection. Co-authored-by: Junie <junie@jetbrains.com>
Per XEP-0467 §3.2, top-level non-stanza elements (stream errors, CSI toggles, framing) MUST only be sent on the initial QUIC stream (id 0). NettyConnection.deliverRawText now routes such writes via the QuicSessionStreamRouter's primary stream when a router is present, even if the NettyConnection itself sits on an aux stream. Co-authored-by: Junie <junie@jetbrains.com>
…ime#8) XEP-0467 SHOULDs a QUIC max_idle_timeout of at most 600 seconds. Lower the default from 6 minutes to exactly 600 seconds to match. Co-authored-by: Junie <junie@jetbrains.com>
…anel The QUIC client listener settings page now exposes the xmpp.quic.client.max-outbound-streams, xmpp.quic.client.alpn and xmpp.quic.client.qlog-dir properties (alongside the existing port, idle timeout and max-streams fields), with matching i18n strings and form validation. Co-authored-by: Junie <junie@jetbrains.com>
…s on aux streams Bug 1 (XEP-0467 §3.2 violation): NettyConnection.close() was writing the stream:error + </stream:stream> directly via channelHandlerContext.writeAndFlush, bypassing the deliverRawText primary-stream routing. Fixed by routing the close payload through the primary QuicStreamChannel when a QuicSessionStreamRouter is present and the current channel is not the primary stream. Bug 2 (spurious keepalive timeouts): QuicConnectionAcceptor adds IdleStateHandler and NettyIdleStateKeepAliveHandler to every QUIC stream pipeline. On aux streams these fire independently of the QUIC connection's own liveness, causing connection-timeout stream:errors to be sent even when the QUIC connection is active. Fixed by removing 'idleStateHandler' and 'keepAliveHandler' from the pipeline in QuicClientConnectionHandler.handlerAdded() when the stream is classified as AUX (session already bound). The QUIC connection-level idle timeout (max_idle_timeout transport parameter) handles liveness for the whole connection. Co-authored-by: Junie <junie@jetbrains.com>
…keepalives The connection-timeout stream:error was being generated by the application-level IdleStateHandler + NettyIdleStateKeepAliveHandler pipeline, which used the TCP client idle timeout (xmpp.client.idle, default 6 min) independently of QUIC transport liveness. This caused spurious disconnections when the QUIC connection was healthy but no XMPP stanzas had been exchanged. Fix: QuicClientConnectionHandler.getMaxIdleTime() now returns Duration.ofMillis(-1), signalling that no application-level idle check is desired. QuicConnectionAcceptor skips adding idleStateHandler and keepAliveHandler to the pipeline when this value is negative. The QUIC max_idle_timeout transport parameter (default 600s, per XEP-0467 §2) handles connection liveness at the transport layer without generating any additional XMPP traffic. Also removed the now-redundant aux-stream idle-handler removal code from QuicClientConnectionHandler.handlerAdded(), since those handlers are never added to any QUIC stream pipeline. Co-authored-by: Junie <junie@jetbrains.com>
The WriteTimeoutHandler was inherited from the TCP pipeline. On a QUIC stream a write may legitimately sit in quiche's send queue for tens of seconds while the QUIC layer paces, retransmits and honours flow / congestion control on a slow or lossy link. Treating that as a stalled session caused healthy clients to be disconnected with a stream:error on high-latency / low-bandwidth paths (observed at 50 kbit / 120 ms sending vCard payloads, with srtt ballooning to ~34 s before the timer fired). Liveness on QUIC is correctly handled by the transport's max_idle_timeout (XEP-0467 §2 igniterealtime#8, 600 s default) and quiche's loss-detection / PTO machinery; an application-level write timeout adds no value on top of that. Removed from both QuicConnectionAcceptor's per-stream pipeline (client- initiated streams) and QuicSessionStreamRouter.openOutboundStreamAsync (server-initiated streams) for symmetry. Unused imports cleaned up. WRITE_TIMEOUT_SECONDS SystemProperty kept in place for backward compatibility with operator configuration. Co-authored-by: Junie <junie@jetbrains.com>
…> 16) Per XEP-0467 §3.2, traffic not to/from the user's own bare JID or the server's own JID SHOULD be carried on aux streams to avoid head-of-line blocking on the control stream id 0. The routing logic in QuicSessionStreamRouter.shouldUsePrimaryStream already implements this correctly: it returns true only for empty 'from', the account's own bare JID, or the server's bare JID, and falls through to aux-stream selection for everything else. However, QUIC_MAX_OUTBOUND_STREAMS defaulted to 0 (disabled) as a safe initial value when the multi-stream code first landed, with the result that openOutboundStreamAsync always returned false and every stanza fell back to the primary stream. On a slow/lossy link this caused a single large stanza (e.g. a vCard with an embedded photo from a remote peer) to head-of-line-block the control stream, queuing presence, keepalives and other small writes behind it for tens of seconds and contributing to disconnects. Raising the default to 16 keeps the total within the QUIC_MAX_STREAMS budget (25 by default), leaving headroom for the primary stream and any client-initiated aux streams, while still honouring the peerAllowedStreams check at runtime. Operators can set the value to 0 to disable aux streams entirely if needed. Co-authored-by: Junie <junie@jetbrains.com>
…ing new server streams When the server needs a non-primary QUIC stream for a stanza from a new sender bare JID, prefer any client-initiated (inbound) stream that has no current from-bare-JID assignment over opening a new server-initiated (outbound) stream. Falls back to outbound-stream open only when every existing non-primary stream is already allocated, and falls back to round-robin reuse only when both fail. Client-opened streams cost nothing extra to use and consume client stream credit that has already been granted; opening a new server stream costs a server stream credit and a round-trip. Reusing client streams first reflects this asymmetry and avoids opening unnecessary streams. Co-authored-by: Junie <junie@jetbrains.com>
… panel Previously the QUIC client listener had its own sidebar entry and JSP (connection-settings-quic-c2s.jsp). It is now embedded as a fourth contentBox within connection-settings-socket-c2s.jsp, alongside plaintext, Direct TLS, idle policy and rate-limit panels. The form handles plaintext/DirectTLS/QUIC/idle/rate-limit settings atomically. The standalone connection-settings-quic-c2s.jsp is kept as a redirect to connection-settings-socket-c2s.jsp for any bookmarks or external links pointing at it. The sidebar item is removed; the certificate-store sub-sidebar for QUIC_C2S remains so identity/trust stores are still reachable separately. Co-authored-by: Junie <junie@jetbrains.com>
The session-details admin page previously labelled QUIC sessions as
'TCP' (the catch-all for any NettyConnection) and surfaced no QUIC
specific information. This change:
* Detects QUIC_C2S sessions via ConnectionConfiguration.getType() and
labels them 'QUIC' in the Connection Type row.
* Adds a 'QUIC Connection' details panel for QUIC sessions only,
showing:
- Stream counts (client-opened / server-opened, primary stream id,
number of remote bare-JIDs mapped to non-primary streams) — pulls
directly from QuicSessionStreamRouter.
- Peer-allowed bidi stream credits (how many streams the server
may still open toward the client).
- RTT (smoothed), congestion window, PMTU and quiche delivery-rate
estimate from QuicConnectionPathStats (path 0).
- Packets sent/received/lost/retransmitted and bytes sent/received/
lost from QuicConnectionStats.
* Adds public accessors to QuicSessionStreamRouter for the counts,
primary stream id and underlying QuicChannel; widens
NettyConnection.getChannel() from package-private to public so the
JSP can locate the router via QuicSessionStreamRouter.find().
* Stats are fetched synchronously with a short (250 ms) timeout — the
admin page is rendered on demand so the small wait is acceptable and
it keeps the implementation simple without a periodic-poll cache.
Co-authored-by: Junie <junie@jetbrains.com>

This supports QUIC, rather minimalist, for C2S only.
Rather alarmingly, it actually runs.