Skip to content

feat(relay): one TCP port for HTTP, RTMP, RTMPS, and tls:// - #7362

Merged
moq-bot[bot] merged 8 commits into
mainfrom
quest/m2/one-port/tcp-demux
Nov 1, 2026
Merged

moq-bot[bot] merged 8 commits into
mainfrom
quest/m2/one-port/tcp-demux

Conversation

@kixelated

@kixelated kixelated commented Oct 30, 2026 •

Copy link
Copy Markdown
Collaborator

Finishes quest/m2/one-port/tcp-demux.md (one-port epic). The relay's HTTPS port now carries TLS-terminated HTTP, RTMP, RTMPS, and tls:// qmux, and the quest is deleted.

Summary

  • listener::classify (rs/moq-relay/src/listener/demux.rs) runs inside the web acceptor, which axum_server already spawns on each connection's own task. The accept loop never peeks, and a silent peer only holds up its own task, under the existing idle deadline (listen.timeout, no new setting).
    • Plaintext first byte 0x03 means RTMP, on either web listener.
    • On HTTPS, any other first byte goes to rustls through LazyConfigAcceptor, which reads the ClientHello first:
      • If the ClientHello offers a moq ALPN (qmux-01.<protocol>), the handshake finishes with the listen TLS identity, and the connection is tls:// qmux.
      • Otherwise it finishes with web.https.cert (or ACME), as before. A negotiated ALPN means HTTP. With no ALPN, the first decrypted byte decides: 0x03 is RTMPS, anything else is HTTP.
  • tls:// on HTTPS runs as a relay connection on its own task, as a WebSocket session does. Its admission carries into the session. Its request reports the SNI (server_name and authority), the negotiated ALPN, and a client certificate the listen verifier accepted (peer_identity), as the listen.tcp tls:// listener does since moq-dev/moq#7358.
  • moq-tokio gains tcp::TlsAcceptor, from Server::tls_acceptor(). It builds the same moq-ALPN config as the listen.tcp TLS listener (TcpTls::new), including its client-certificate verifier, and serves the same stream versions. TlsAcceptor::accept takes the tokio_rustls::server::TlsStream whose handshake finished with that config, runs the qmux handshake and SETUP, and returns a Request. qmux gains tls::Server::accept_established, which tls::Server::accept now calls, and tcp::adopt is now public, so both handshake halves are shared with the existing tls:// listener. The TCP loop and TlsAcceptor build the request's TLS facts with one helper (Incoming::tcp).
  • RTMP goes to an embedder through Relay::rtmp() / Web::rtmp() → web::rtmp::Incoming. If no consumer is taken, the relay closes RTMP connections with a debug log. RTMP connections leave the drain count when they are handed off, because the embedder owns their lifetime.
  • WebAcceptor is no longer generic. The mTLS peer, SNI, h2, and ACME checks read the stream's rustls::ServerConnection directly.

Impact

  • Public API, additive:
    • moq-relay: web::rtmp::{Incoming, Stream}, Web::rtmp, Relay::rtmp, and Web::with_tls_acceptor.
    • moq-tokio: tcp::TlsAcceptor and Server::tls_acceptor. tcp now depends on tokio-rustls (no default features), which qmux's tls already pulled in.
    • qmux: tls::Server::accept_established, and tcp::adopt made public.
  • Wire: a TLS ClientHello on the HTTPS port that offers a moq ALPN now gets the listen certificate and a MoQ session; before, it failed ALPN negotiation. A plaintext 0x03 on either web port, or a TLS connection with no ALPN whose first byte is 0x03, is classified as RTMP. HTTP and HTTPS clients see no change.
  • Config: none. listen.tcp stays a separate optional listener. A relay with no listen TLS identity (stream-only, without listen.tcp.tls) serves no moq ALPN on HTTPS.
  • Auth: under listen.tls.root or tls.peers, tls:// on HTTPS asks for a client certificate as the listen.tcp tls:// listener does: optional under CA roots, required under pinned peers. HTTPS and RTMPS on the port never see that config.
  • Docs: updated doc/bin/relay/{config,auth,cluster}.md and added an embed bullet in doc/bin/relay/index.md.
  • Quests: deleted tcp-demux.md and its links. The one-port epic's TCP section is dropped, since that work has landed. rate-websocket.md keeps its pointer to listener::Stream::tcp.

Decisions

  1. How does the HTTPS port start carrying tls:// qmux, and what happens to the separate listen.tcp TLS bind? (maintainer, 2026-10-30)
    • ✅ (a) The HTTPS listener always accepts moq ALPNs when the relay has a listen TLS identity. listen.tcp stays a separate optional listener. No new setting.
    • (b) Serve it on HTTPS only when listen.tcp.tls is set, and stop binding listen.tcp separately.
    • (c) Add a new opt-in setting, such as web.https.qmux.
  2. RTMP API shape (maintainer, 2026-10-30)
    • ✅ Relay::rtmp() / Web::rtmp() return web::rtmp::Incoming; Incoming::accept() returns a Stream that has tcp().
    • Also expose peer() and a TLS flag.

Choices made without asking

  • Classification happens inside the acceptor, not in a channel-fed listener. axum_server already runs the acceptor on its own task per connection, so this meets the Plan's goal (the accept loop never peeks) with less machinery. One idle deadline covers peek, TLS, and the first request.
  • The HTTPS port still refuses plaintext HTTP. Serving cleartext HTTP, including WebSocket tokens, on 443 would be a security change.
  • A ClientHello that offers any moq ALPN goes to tls://, even if it also offers h2 or http/1.1. Only a MoQ client offers qmux-01.*.
  • The TLS handshake keeps axum_server's 10s bound (TLS_HANDSHAKE), because the lazy acceptor replaces RustlsAcceptor, which applied it.
  • tls:// on HTTPS verifies client certificates as listen.tcp.tls does. moq-dev/moq#7358 landed first, and the quest recorded (2026-10-08, 2026-10-30) that whichever landed second keeps tls:// client-certificate verification working, optional under CA roots and required under pinned peers. Resolving the merge, accept_established and TlsAcceptor::accept take the rustls TlsStream rather than a generic stream plus ALPN, so they read the chain, SNI, and ALPN from the handshake itself and a caller cannot drop them.
  • The RTMP refusal log is at debug, so peers sending 0x03 cannot flood the logs.

Checks

  • just ci origin/main: passed locally on b201d42 (quest gate), 5634 tests.
  • quest check --base origin/main --branch quest/m2/one-port/tcp-demux: ok.
  • New test https_listener_serves_tls_qmux:
    • a tls:// dial to the HTTPS port that trusts only the https.cert CA is refused, because the moq arm serves the listen identity;
    • an insecure tls:// dial lands a MoQ session;
    • GET /health over http/1.1 on the same port still returns 200 with https.cert.
  • New test https_listener_verifies_tls_qmux_client_certificates: under pinned tls.peers, a tls:// dial to the HTTPS port without a certificate is refused, while HTTPS on the port still answers without one.
  • web_listeners_hand_rtmp_to_its_consumer covers RTMP, RTMPS, and HTTP on each listener; server_name_is_the_sni_never_the_host covers HTTP and WebSocket.

Automated review

  • CodeQL "cleartext logging of sensitive information" at rs/moq-relay/src/web.rs (the two assert! messages in web_listeners_hand_rtmp_to_its_consumer): declined. Both are test assertion messages printing a loopback socket address and the offered ALPN list; no certificate or key material reaches them.

Review

  • Reviewer: Codex gpt-6-astra, medium effort (--deep), via quest review --base origin/main. Session started with --new after the hand-resolved merge of main (2a7c3b4545, round 1: 0 findings). Reviewed head: b201d42 (round 2, after moving the web tests to kio::clock::timeout for main's lint: 0 findings).
  • Earlier sessions: b6cbee2 (0 findings); 4e8db4b (round 1 fixed the RTMP consumer outliving serving; round 2 had 0 findings).

🤖 Generated with Claude Code

(Written by Claude Opus 5.5)

kixelated and others added 3 commits October 30, 2026 05:21
…bytes

Each public web connection is classified on its own task before HTTP: a
plaintext C0 (0x03) is RTMP, a TLS connection with no ALPN is sniffed again
on its first decrypted byte (RTMPS), and everything else reaches the router.
RTMP goes to an embedder through Relay::rtmp / Web::rtmp; untaken, it is
logged and closed. The tls:// arm stays in the quest pending its config gate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Connections still classifying held sender clones, so Incoming::accept never
returned None after the listeners stopped with no idle deadline.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated kixelated changed the title TCP acceptor feat(relay): split web connections into HTTP, RTMP, and RTMPS Oct 30, 2026
Comment thread rs/moq-relay/src/web.rs Dismissed
Comment thread rs/moq-relay/src/web.rs Dismissed
kixelated and others added 2 commits October 30, 2026 13:04
The HTTPS acceptor reads each ClientHello. One that offers a moq ALPN
finishes with the listen TLS identity and runs as a MoQ session, through
the new moq_tokio::tcp::TlsAcceptor (from Server::tls_acceptor). Every
other connection keeps web.https.cert. listen.tcp stays a separate
optional listener, and there is no new setting.

qmux gains tls::Server::accept_established and a public tcp::adopt, so
the HTTPS arm shares both handshake halves with the tls:// listener.

The quest is done and deleted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated kixelated changed the title feat(relay): split web connections into HTTP, RTMP, and RTMPS feat(relay): one TCP port for HTTP, RTMP, RTMPS, and tls:// Oct 30, 2026
@kixelated
kixelated marked this pull request as ready for review October 30, 2026 20:24
@coderabbitai

coderabbitai Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 9 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used all 4 included reviews currently available.

Learn how review limits work.

Review configuration:

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 1a5bf80b-f90d-4681-b966-6f01d9faa1d5
Commits

Reviewing files that changed from the base of the PR and between caef9b3 and d5b7591.

Files selected for processing: 17
  • doc/bin/relay/auth.md
  • doc/bin/relay/cluster.md
  • doc/bin/relay/config.md
  • doc/bin/relay/index.md
  • quest/m2/one-port/README.md
  • quest/m2/one-port/tcp-demux.md
  • quest/m2/rate-websocket.md
  • rs/moq-relay/src/listener.rs
  • rs/moq-relay/src/listener/demux.rs
  • rs/moq-relay/src/relay.rs
  • rs/moq-relay/src/web.rs
  • rs/moq-relay/src/web/rtmp.rs
  • rs/moq-tokio/Cargo.toml
  • rs/moq-tokio/src/server.rs
  • rs/moq-tokio/src/tcp.rs
  • rs/qmux/src/tcp.rs
  • rs/qmux/src/tls.rs
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-30T20:26:27.339854Z b6cbee2 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@kixelated kixelated left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review by review (OpenAI)

Reviewed commit: b6cbee2 (base 9387174).

No actionable bugs found in this review. The direction is sound: classifying inside the existing per-connection acceptor avoids a second listener/channel layer, while sharing the qmux handshake and SETUP path avoids parallel protocol implementations. I checked TLS identity/ALPN selection, decrypted-byte replay, HTTP metadata, RTMP handoff, and admission/deadline handling around the new HTTPS qmux path.

The existing CodeQL threads concern test assertion output; I found no additional sensitive-data logging issue in those changes.

Verification limits: static review of the diff and relevant surrounding code through GitHub only. I did not run builds, tests, or live TLS/RTMP interoperability checks; the added tests were inspected, not executed.

@kixelated

Copy link
Copy Markdown
Collaborator Author

Merge summary (/quest-merge, maintainer pick 2026-10-31):

  • Landing head b6cbee2 unchanged. Its local review is the Codex gpt-6-astra --deep round recorded under ## Review (0 findings). quest check --base origin/main passes, and the head merges cleanly with current main.
  • The failing CodeQL check is the two rust/cleartext-logging alerts (#67, #68) on assertion messages in the test web_listeners_hand_rtmp_to_its_consumer. They are false positives. CodeQL traces taint from read_certs through the Web object into the loopback addr, but no certificate or key material is printed. The check is optional, so I left the code as is and resolved the threads, which already carry that reasoning. This matches how earlier test-only alerts were triaged in fix(security): resolve CodeQL redos and workflow_run checkout alerts #6084. Follow-up: dismiss both alerts as "used in tests".
  • feat(tokio)!: verify client certificates on tls:// listeners #7358 (mTLS on tls:// listeners) has not landed, so there is nothing to reconcile here. Whichever of the two lands second resolves the overlap in its own branch.

(Written by Claude Opus 5.5)

@kixelated
kixelated enabled auto-merge (squash) October 31, 2026 05:43
@moq-bot
moq-bot Bot disabled auto-merge October 31, 2026 13:07
@moq-bot moq-bot Bot removed the quest:merge label Oct 31, 2026
@moq-bot

moq-bot Bot commented Oct 31, 2026

Copy link
Copy Markdown
Contributor

quest land removed this pull request from the queue: the batch check failed (exit 1).

js: selecting docs
..js: selecting reader
...
----------------------------------------------------------------------
Ran 24 tests in 0.332s

OK
python3 sh/js/audit.test.py
..
----------------------------------------------------------------------
Ran 2 tests in 0.064s

OK
python3 sh/rs/test-lints.test.py
.
----------------------------------------------------------------------
Ran 1 test in 2.344s

OK
python3 sh/gh/nightly.test.py
..............
----------------------------------------------------------------------
Ran 14 tests in 1.112s

OK
python3 sh/gh/pages-clean.test.py
....
----------------------------------------------------------------------
Ran 4 tests in 0.048s

OK
python3 sh/gh/bench-report.test.py
............
----------------------------------------------------------------------
Ran 12 tests in 0.407s

OK
bench-ffi: 2 measured entries
bench-rust: failure; no history point
bench-ffi: failure; no history point
bench-rust: failure; no history point
no completed benchmark reports; no history point
bench-rust: 1 measured entries
bench-rust: failure; no history point
no completed benchmark reports; no history point
Paired comparison of c2 against c1: 1 confirmed regressions, 1 targets not compared

- `rust/kio/lock/4`: +40.0% (rounds +30.0%, +40.0%, +50.0%)
- `rust/gst/moq-gst/catalog` not compared: base build failed
Paired comparison of c2 against c1: 0 confirmed regressions, 0 targets not compared

python3 sh/gh/ts-paths.test.py
.....
----------------------------------------------------------------------
Ran 5 tests in 0.000s

OK
sh/go/publish-wrapper.test.sh
publish-wrapper test: no rsync on PATH, skipping
bash .claude/hooks/direnv.test.sh
direnv hook: could not load the dev shell, see the direnv output above.
direnv hook: regression tests passed
python3 sh/rs/uring.test.py
jq: error (at <stdin>:36): null (null) cannot be matched, as it is not a string
rs: selecting moq-relay moq-uring 
jq: error (at <stdin>:36): null (null) cannot be matched, as it is not a string
rs: selecting moq-relay 
jq: error (at <stdin>:36): null (null) cannot be matched, as it is not a string
rs: selecting kio moq-relay moq-uring 
jq: error (at <stdin>:36): null (null) cannot be matched, as it is not a string
rs: selecting moq-unrelated 
rs: selecting the workspace
rs: selecting the workspace
rs: selecting the workspace
.
----------------------------------------------------------------------
Ran 1 test in 0.270s

OK
ci: check quest failed
ci: check rs failed
ci: check rs_shapes failed
ci: check js failed
ci: seconds per module
    24  check rs (failed)
     9  check rs_shapes (failed)
    14  check js (failed)
    19  check drafts
    11  check markdown
     6  check gh
    36  test gh
     0  check drill
     2  check quest (failed)
    13  check shell
     0  check large_files
     0  check toml
     0  check nix
     0  check justfile
    87  wall time
error: recipe `ci` failed on line 104 with exit code 1

.

@moq-bot
moq-bot Bot enabled auto-merge (squash) October 31, 2026 13:42
@moq-bot moq-bot Bot added the quest:merge label Oct 31, 2026
@moq-bot

moq-bot Bot commented Oct 31, 2026

Copy link
Copy Markdown
Contributor

codex / Codex land fixer: Requeued unchanged: Healthy in the failure log; batch failures are attributable to PRs &#35;7316 and &#35;7383.

(written by codex / Codex)

@moq-bot
moq-bot Bot disabled auto-merge October 31, 2026 19:01
@moq-bot moq-bot Bot removed the quest:merge label Oct 31, 2026
@moq-bot

moq-bot Bot commented Oct 31, 2026

Copy link
Copy Markdown
Contributor

quest land removed this pull request from the queue: the batch check failed (exit 1).

              candidate #3: `std::iter::Iterator`
help: there is a method `next_run` with a similar name
      |
19292 |         assert_eq!(fill.next_run(), Some(1));
      |                             ++++

error[E0599]: no method named `next` found for struct `model::track::fill::Fill` in the current scope
     --> rs/moq-net/src/model/track.rs:19396:35
      |
19396 |         while let Some(sequence) = fill.next() {
      |                                         ^^^^
      |
     ::: rs/moq-net/src/model/track/fill.rs:668:1
      |
  668 | pub struct Fill {
      | --------------- method `next` not found for this struct
      |
      = help: items from traits can only be used if the trait is implemented and in scope
      = note: the following traits define an item `next`, perhaps you need to implement one of them:
              candidate #1: `futures::StreamExt`
              candidate #2: `serde_json::de::Read`
              candidate #3: `std::iter::Iterator`
help: there is a method `next_run` with a similar name
      |
19396 |         while let Some(sequence) = fill.next_run() {
      |                                             ++++

error[E0599]: no method named `next` found for struct `model::track::fill::Fill` in the current scope
     --> rs/moq-net/src/model/track.rs:19456:19
      |
19456 |         assert_eq!(fill.next(), Some(1));
      |                         ^^^^
      |
     ::: rs/moq-net/src/model/track/fill.rs:668:1
      |
  668 | pub struct Fill {
      | --------------- method `next` not found for this struct
      |
      = help: items from traits can only be used if the trait is implemented and in scope
      = note: the following traits define an item `next`, perhaps you need to implement one of them:
              candidate #1: `futures::StreamExt`
              candidate #2: `serde_json::de::Read`
              candidate #3: `std::iter::Iterator`
help: there is a method `next_run` with a similar name
      |
19456 |         assert_eq!(fill.next_run(), Some(1));
      |                             ++++

error[E0599]: no method named `next` found for struct `model::track::fill::Fill` in the current scope
     --> rs/moq-net/src/model/track.rs:19485:37
      |
19485 |                 while let Some(sequence) = fill.next() {
      |                                                 ^^^^
      |
     ::: rs/moq-net/src/model/track/fill.rs:668:1
      |
  668 | pub struct Fill {
      | --------------- method `next` not found for this struct
      |
      = help: items from traits can only be used if the trait is implemented and in scope
      = note: the following traits define an item `next`, perhaps you need to implement one of them:
              candidate #1: `futures::StreamExt`
              candidate #2: `serde_json::de::Read`
              candidate #3: `std::iter::Iterator`
help: there is a method `next_run` with a similar name
      |
19485 |                 while let Some(sequence) = fill.next_run() {
      |                                                     ++++

For more information about this error, try `rustc --explain E0599`.
error: could not compile `moq-net` (lib test) due to 4 previous errors
error: process didn't exit successfully: `/nix/store/85lxjwg78jw6rf7pqkf3vcq6291nrlfh-cargo-1.98.1-x86_64-unknown-linux-gnu/bin/cargo clippy --locked --tests --all-features --manifest-path rs/moq-net/Cargo.toml -- -D warnings` (exit status: 101)
mbx[cache]: object cache: 0 hits, 8 misses; 0 B downloaded, 0 B uploaded, 13.3 MiB stored locally
mbx[cache]: 38.84s session; 0s estimated compiler time avoided (summed across compilations)
mbx[cache]: Cargo artifact reuse and CI cache archive transfers are not included above
mbx[savings]: 2.4 TiB of outputs exist once and appear everywhere. do not tell the checkouts.
error: recipe `feature-shapes` failed on line 251 with exit code 1
error: recipe `feature-shapes-changed` failed on line 75 with exit code 1
ci: check quest failed
ci: test rs failed
ci: check rs_shapes failed
ci: seconds per module
   132  check rs
    54  test rs (failed)
   674  check rs_shapes (failed)
   194  check js
    54  test js
    21  check drafts
    13  check markdown
     3  check gh
    71  test gh
     0  check drill
     1  check quest (failed)
    12  check shell
     1  check large_files
     0  check toml
     0  check nix
     0  check justfile
   861  wall time
error: recipe `ci` failed on line 106 with exit code 1

.

@moq-bot

moq-bot Bot commented Oct 31, 2026

Copy link
Copy Markdown
Contributor

codex / Codex land fixer: This maintainer pick already used its mechanical fi·x attempt. Run /quest-iterate to diagnose the repeated failure, then pick it again.

(written by codex / Codex)

@kixelated
kixelated enabled auto-merge (squash) October 31, 2026 19:52
@moq-bot
moq-bot Bot disabled auto-merge November 1, 2026 07:41
@moq-bot moq-bot Bot removed the quest:merge label Nov 1, 2026
@moq-bot

moq-bot Bot commented Nov 1, 2026

Copy link
Copy Markdown
Contributor

quest land removed this pull request from the queue: Conflicts with the base: doc/bin/relay/auth.md, doc/bin/relay/cluster.md, quest/m2/one-port/tcp-demux.md, quest/m2/tls-listener-mtls.md, rs/moq-tokio/src/server.rs, rs/qmux/src/tls.rs
.

@moq-bot

moq-bot Bot commented Nov 1, 2026

Copy link
Copy Markdown
Contributor

codex / Codex land fixer: Base adds TLS peer-certificate and SNI propagation in rs/qmux/src/tls.rs and rs/moq-tokio/src/server.rs, but the PR's generic accept_established/TlsAcceptor path cannot carry that metadata; resolving . Recommendation: Rebase with author review, carry validated peer identity and SNI through the shared HTTPS qmux accept path, preserve base TLS authentication policy, and test dedicated and shared TLS listeners.

@@ -146,6 +149,10 @@ impl Server {
 	/// [`handshake_timeout`](crate::Config::handshake_timeout)). Drive each
 	/// connection with `tokio::spawn` so a slow or non-cooperative peer can't
 	/// stall your `listener.accept()` loop.
+	///
+	/// The returned session reports the client's certificate chain, when the
+	/// `rustls::ServerConfig` asked for one and the client presented it, and the
+	/// SNI it sent (see [`Session::peer_certificates`] and [`Session::server_name`]).
 	pub async fn accept(&self, stream: TcpStream) -> Result<Session, Error> {
 		let socket_stats = crate::tcp::adopt(&stream)?;
 		let acceptor = TlsAcceptor::from(self.config.clone());
@@ -159,11 +166,53 @@ impl Server {
 		tracing::debug!(?version, ?protocol, "parsed ALPN");
 
 		let session_config = self.session.clone().with_negotiated(version, protocol);
+		let conn = tls_stream.get_ref().1;
+		let handshake = Handshake::new(conn.peer_certificates(), conn.server_name());
 		let mut transport = Stream::new(tls_stream);
 		if let Some(stats) = socket_stats {
 			transport = transport.with_socket_stats(stats);
 		}
 		// `accept` awaits the peer's transport parameters so `protocol()` is resolved.
-		Session::accept(transport, session_config).await
+		Ok(Session::accept(transport, session_config).await?.with_tls(handshake))
+	}
+}
+
+/// What a session's TLS handshake learned about the peer, captured before the
+/// stream is handed to the session.
+#[derive(Debug)]
+pub(crate) struct Handshake {
+	peer_certificates: Option<Vec<CertificateDer<'static>>>,
+	server_name: Option<String>,
+}
+
+impl Handshake {
+	fn new(peer_certificates: Option<&[CertificateDer<'static>]>, server_name: Option<&str>) -> Arc<Self> {
+		Arc::new(Self {
+			peer_certificates: peer_certificates.map(<[_]>::to_vec),
+			server_name: server_name.map(str::to_owned),
+		})
+	}
+}
+
+impl Session {
+	fn with_tls(mut self, handshake: Arc<Handshake>) -> Self {
+		self.tls = Some(handshake);
+		self
+	}
+
+	/// The certificate chain the peer presented in the TLS handshake, leaf first.
+	///
+	/// On a [`Client`] session this is the server's chain. On a [`Server`] session
+	/// it is the client's, and only when the `rustls::ServerConfig` asked for one
+	/// and the client sent it; its client-certificate verifier has already
+	/// accepted the chain. `None` on a session not carried by TLS.
+	pub fn peer_certificates(&self) -> Option<&[CertificateDer<'static>]> {
+		self.tls.as_ref()?.peer_certificates.as_deref()
+	}
+
+	/// The SNI the client sent, on a session a [`Server`] accepted. `None` when the
+	/// client sent none, on a [`Client`] session, or off TLS.
+	pub fn server_name(&self) -> Option<&str> {
+		self.tls.as_ref()?.server_name.as_deref()
 	}
 }

codex
The conflicts involve TLS authentication behavior. Base added client-certificate and SNI reporting, while PR 7362’s new shared-listener API has no way to carry that metadata through. Preserving both requires API integration and review, beyond a mechanical fix. I’ll hand it back and leave its branch unchanged.

exec
/bin/bash -lc "git merge --abort; python3 - <<'PY'
import json
from pathlib import Path
result = [{\"number\":7362,\"action\":\"handback\",\"diagnosis\":\"Base adds TLS peer-certificate and SNI propagation in rs/qmux/src/tls.rs and rs/moq-tokio/src/server.rs, but the PR's generic accept_established/TlsAcceptor path cannot carry that metadata; resolving the conflicts requires authentication API integration beyond a mechanical fix.\",\"recommendation\":\"Rebase with author review, carry validated peer identity and SNI through the shared HTTPS qmux accept path, preserve base TLS authentication policy, and test dedicated and shared TLS listeners.\"}]
Path('quest-land-result.json').write_text(json.dumps(result, indent=2) + '\\n')
PY
 git status --short; git rev-parse pr-7362; git diff --exit-code; cat quest-land-result.json" in /work
 succeeded in 349ms:
?? quest-land-result.json
b6cbee2c4edce9f34ec11b378a7dfa75e725896a
[
  {
    "number": 7362,
    "action": "handback",
    "diagnosis": "Base adds TLS peer-certificate and SNI propagation in rs/qmux/src/tls.rs and rs/moq-tokio/src/server.rs, but the PR's generic accept_established/TlsAcceptor path cannot carry that metadata; resolving the conflicts requires authentication API integration beyond a mechanical fix.",
    "recommendation": "Rebase with author review, carry validated peer identity and SNI through the shared HTTPS qmux accept path, preserve base TLS authentication policy, and test dedicated and shared TLS listeners."
  }
]

codex
PR 7362 handed back: resolving the TLS identity and SNI conflicts requires API integration and review beyond mechanical fixes.

Branch unchanged. Result saved uncommitted in [quest-land-result.json](/work/quest-land-result.json).
tokens used
29,443
PR 7362 handed back: resolving the TLS identity and SNI conflicts requires API integration and review beyond mechanical fixes.

Branch unchanged. Result saved uncommitted in [quest-land-result.json](/work/quest-land-result.json).
fixer result written

(written by codex / Codex)

kixelated and others added 2 commits November 1, 2026 01:26
Main verifies client certificates on tls:// listeners (#7358). The HTTPS
port's tls:// arm keeps that: it serves the same verifier config, and its
request reports the SNI and accepted client certificate. To read them,
qmux's Server::accept_established and moq-tokio's TlsAcceptor::accept now
take the rustls TlsStream itself, and share the tcp loop's Incoming::tcp.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main's lint disallows tokio::time::timeout workspace-wide.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated

Copy link
Copy Markdown
Collaborator Author

Merged main (b201d42) to clear the merge_conflict from quest land.

  • Main landed client-certificate verification on tls:// listeners (moq-dev/moq#7358). The quest had recorded that whichever landed second keeps it working, so the HTTPS port's tls:// arm now serves the same verifier config (optional under CA roots, required under pinned peers), and its request reports the SNI, ALPN, and accepted certificate.
  • To carry those facts, qmux::tls::Server::accept_established and moq_tokio::tcp::TlsAcceptor::accept now take the rustls TlsStream instead of a generic stream plus an ALPN string. The TCP loop and TlsAcceptor share one helper for the request's TLS facts.
  • Test added: https_listener_verifies_tls_qmux_client_certificates. Docs merged so both tls:// entry points describe the shared certificate policy. quest/m2/tls-listener-mtls.md stays deleted, as main had already deleted it.
  • Web tests moved to kio::clock::timeout for main's new lint.
  • just ci origin/main passed. Codex (gpt-6-astra, --deep) reviewed the merge and the follow-up commit: 0 findings in either round.

(Written by Claude Opus 5.5)

@kixelated
kixelated enabled auto-merge (squash) November 1, 2026 09:34

@kixelated kixelated left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review by review (OpenAI)

Reviewed commit: b201d42 (base 947c91d).

Compared with my review of b6cbee2, separating merged-main changes from the substantive HTTPS qmux/mTLS integration. No actionable bugs found in this update; the previous review had no open findings.

The direction is sound: taking the established rustls stream directly preserves negotiated ALPN, verified peer certificates and SNI, while sharing Incoming::tcp keeps authority/identity construction aligned with the dedicated listener. The added pinned-peer refusal/HTTPS isolation test addresses the changed policy boundary without adding another authentication path.

Verification limits: GitHub-only static review of the integration, surrounding TLS/request construction and tests. Builds, tests and live mTLS interoperability were not run.

@moq-bot
moq-bot Bot merged commit 090fec2 into main Nov 1, 2026
1 check passed
@moq-bot
moq-bot Bot deleted the quest/m2/one-port/tcp-demux branch November 1, 2026 09:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants