Skip to content

refactor(moq-native)!: name the endpoint roles connect and listen - #2750

Open
kixelated wants to merge 1 commit into
devfrom
claude/github-issue-2696-fbda00
Open

refactor(moq-native)!: name the endpoint roles connect and listen#2750
kixelated wants to merge 1 commit into
devfrom
claude/github-issue-2696-fbda00

Conversation

@kixelated

@kixelated kixelated commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Closes #2696.

A QUIC endpoint dials and accepts, and the config surface spelled every transport knob twice: --client-* and --server-*, backed by duplicated quic::Client/quic::Server and tls::Client/tls::Server type definitions. This keeps the two roles apart, because most knobs really do belong to one of them, and fixes the naming instead: role modules with short names, and the genuinely shared tuning spelled once.

The shape

moq_native::connect::Config   --connect-*    the dial side
moq_native::listen::Config    --listen-*     the accept side
moq_native::quic::Config      --quic-*       transport tuning, shared by both

An endpoint that does both flattens all three and hands the tuning to each role: connect.init(quic) / listen.init(quic).

flag note
connect.url --connect kept short; it's the flag everyone types
connect.bind --connect-bind the dial socket
connect.timeout --connect-timeout
connect.race --connect-race was failover_delay
connect.once --connect-once reconnecting is the default; the flag names the exception
connect.tls --connect-tls-* roots, fingerprints, insecure, and the mTLS identity as plain cert/key
connect.websocket --connect-websocket-* the fallback's enable and head-start knobs
listen.bind --listen
listen.tls --listen-tls-* served cert/key/generate, plus root for mTLS CAs
listen.tcp, listen.unix --listen-tcp-*, --listen-unix-* the qmux stream listeners and the peer-credential allowlist
listen.preferred_v4/v6, listen.lb_id/lb_nonce --listen-* accept-only, so they moved off quic
quic.* --quic-* one definition, one spelling

A flag is its dotted field path, and --connect (for connect.url) is the one exception, because it's the flag everyone types. That rule is what pulled in the last few stragglers: the stream listeners still said --server-tcp-bind / --server-unix-*, the served certificate was bare --tls-cert next to a prefixed --listen-tls-root, and the WebSocket fallback was --websocket-* while its env var had already moved to MOQ_CONNECT_WEBSOCKET_*. Every one of those released spellings still parses, and still reads its original env var.

backend and version stay per-role: a quiche acceptor with a quinn dialer is a real configuration (and --client-backend/--server-backend already allowed it), and a per-role version is what lets a relay pin its mesh wire version without narrowing what it accepts from external publishers.

Why tls splits and quic doesn't

tls is two different things wearing one name: cert/key/generate are the served identity, while root/fingerprint/insecure/host_name are how you verify a peer you dial. Splitting them means the mTLS identity drops its client_ prefix (the module supplies the role), and the two roots stop being one field: connect.tls.root verifies servers you dial, listen.tls.root authenticates mTLS clients.

quic is the opposite: max_streams means the same thing whichever way the connection was opened. Sharing it is also forced, not just preferred, since clap can't flatten one struct twice in a parser, so per-role tuning would mean maintaining duplicate definitions that drift.

Compatibility

Every released flag and every released environment variable still configures the same thing. That is checked rather than asserted: rs/moq-relay/tests/released_cli.rs freezes the published moq-relay surface (98 flag/env pairs, taken from the parser itself at the last release) and fails if a spelling stops parsing or an env var stops being read.

Writing that test is what turned up the hole. The first pass renamed flags with clap alias, which carries the flag name and not the env var, so 20 released variables parsed as before on the command line while being silently ignored in the environment: every MOQ_CLIENT_TLS_*, MOQ_CLIENT_WEBSOCKET_*, MOQ_SERVER_TLS_*, and the MOQ_SERVER_BIND/BACKEND/VERSION/PREFERRED_V4/V6/QUIC_LB_* set. This repo's own deployments were in that blast radius: nix/modules/moq-relay.nix sets MOQ_SERVER_TLS_CERT/KEY/GENERATE and MOQ_SERVER_BIND, and fly.toml sets MOQ_SERVER_BIND, so a relay would have come up with no certificate and on the wrong port. Each released spelling now lives on a hidden arg carrying its original env var, folded into the canonical field by resolved() with a one-line deprecation warning, which is what --client-* and --client-quic-* already did. (--client-tls-disable-verify had also been aliased to a name that never shipped, --client-tls-insecure; both parse now.)

TOML keeps working through serde aliases: [client]/[server] tables, connect for url, failover_delay for race, disable_verify for insecure, and reconnect for the inverted once.

Deliberate breaks, both loud rather than silent:

  • [server.quic] / [client.quic] move to a top-level [quic]. A move across tables is the one thing serde aliases can't express, so an old config fails at startup with unknown field quic instead of quietly ignoring the section.
  • QUIC tuning can no longer differ per role. Previously you could set server.quic.congestion_control = "delay" and client.quic.congestion_control = "loss"; now there is one value. backend and version remain per-role.

Bugs found on the way

  • The fold didn't reach the accept path at all. listen::Config::init never called quic.resolved(), so --server-quic-max-streams was parsed and dropped. Client::new and Server::new now resolve their own inputs (idempotent), so no call path can skip it.
  • The relay defaulted before folding. Relay::load filled in DEFAULT_MAX_STREAMS and then handed the config on; since the fold prefers the canonical field, a deployment still passing the released spelling was pinned to 10,000 streams. Now Config::resolve folds first and defaults after, with a regression test.
  • --websocket-enabled / --websocket-delay clobbered their own TOML. Both carried a clap default_value, which the post-file CLI re-parse materializes over whatever the file said, the exact pitfall rs/CLAUDE.md documents. They are Option now with the defaults resolved in code (resolved_enabled() / resolved_delay()), so delay = None means 200ms rather than "no head start"; the callers that wanted no head start say Some(ZERO).
  • --listen-quic-lb-nonce needs an id, which clap's requires can no longer express now that each knob has two spellings. Checked after the fold instead (Error::LbNonceWithoutId), so any mix of the four is judged correctly.

Notes

  • demo_configs_parse reads the real demo/relay/*.toml files, since a rename that lands in code but not in the configs people copy is invisible until a relay refuses to boot. It caught one such break while being written.
  • The --cluster-version knob an earlier revision of this PR added is gone: per-role version subsumes it.
  • --client-resolution-delay (feat(native): start dialing before the AAAA answer lands #2749) landed on main after this branch's base and hasn't reached dev, so it's the one released spelling not in the frozen list. Whoever merges main into dev should fold it into connect::Config alongside --client-failover-delay.
  • Every doc, demo justfile, smoke script, and README that showed a renamed flag was reconciled against the binaries' real --help; nothing published teaches a dead spelling. Three error messages did too (moq-bench, moq-boy, and moq transcode all said "--client-connect is required").
  • Verified: just fix, just check, and just test.

(Written by Opus 5)

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for security reviews. Please try again later.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 35ae5a018c

ℹ️ 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread rs/moq-bench/src/main.rs Outdated
.expect("failed to install default crypto provider");

let config = Config::load()?;
let endpoint = config.client.resolved();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Read the connect URL from the resolved config

When moq-bench is launched through the retained --client-connect flag or MOQ_CLIENT_CONNECT, resolved() places the URL only in this temporary endpoint; the original config.client.connect remains None, so the unchanged unwrap at line 49 panics. The clock example has the same raw-field read at rs/moq-native/examples/clock.rs:54 and reports that --connect is missing. Use the resolved config for all subsequent URL reads so the advertised compatibility path continues to work.

AGENTS.md reference: AGENTS.md:L91-L98

Useful? React with 👍 / 👎.

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.

Fixed. parse_and_merge now folds the legacy spellings once via resolved(), so every later field read (the startup log line and the dial in connection.rs) sees the canonical values; the clock example resolves before reading connect too. A --client-connect-launched run no longer panics.

(Written by Fable 5)

Comment thread rs/moq-native/src/lib.rs Outdated
Comment on lines +49 to +50
pub use client::Client;
pub use config::Config;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Update documentation for the removed config types

Removing the ClientConfig and ServerConfig exports leaves published examples uncompilable. For example, doc/lib/rs/index.md:278 and doc/lib/rs/env/native.md:21-34 still construct ClientConfig, while rs/moq-rtmp/README.md:87-95 still constructs tls::Server. Update every remaining reference to use Config::client, Config::server, or tls::Config::server_config as appropriate.

AGENTS.md reference: AGENTS.md:L91-L96

Useful? React with 👍 / 👎.

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.

Fixed. doc/lib/rs/index.md and doc/lib/rs/env/native.md now use moq_native::Config::default().client() (docs.rs links repointed at Config), and rs/moq-rtmp/README.md builds its RTMPS config from moq_native::tls::Config.

(Written by Fable 5)

Comment thread rs/moq-native/src/config.rs Outdated
Comment on lines +35 to +37
#[serde(skip_serializing_if = "Option::is_none")]
#[arg(id = "connect", long = "connect", env = "MOQ_CONNECT")]
pub connect: Option<Url>,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Replace deprecated flags in every usage guide

The canonical flag rename is incomplete across the repository: rs/moq-cli/README.md:22-52 still teaches --client-connect and --server-bind, and rs/moq-bench/README.md:51-78 still teaches --client-connect and other role-prefixed flags. These aliases are intentionally hidden and emit deprecation warnings, so the user-facing guides should use --connect, --bind, and the new unprefixed TLS/QUIC names.

AGENTS.md reference: AGENTS.md:L182-L182

Useful? React with 👍 / 👎.

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.

Fixed. rs/moq-cli/README.md and rs/moq-bench/README.md now teach --connect / --bind / --tls-*, and a repo-wide grep also updated doc/bin/cli.md plus stale flag mentions in comment prose (clock example header, smoke-test comments, moq-boy).

(Written by Fable 5)

@kixelated
kixelated force-pushed the claude/github-issue-2696-fbda00 branch from 35ae5a0 to f16ba49 Compare August 12, 2026 05:06
@kixelated

Copy link
Copy Markdown
Collaborator Author

Took over this PR: ran a Codex adversarial review, rebased onto current dev (37 commits), fixed CI, and addressed the review + inline findings. Details in the description's "Updates (takeover)" section; triage of the adversarial review's two findings:

[critical] "Outbound TLS roots become unrestricted inbound relay credentials" — partially agree.

  • Agreed and fixed: the legacy TOML fold was buggy. tls::Config::or kept the first non-empty list, so a config with distinct client.tls.root and server.tls.root silently dropped the server's mTLS CAs (breaking clients the old config authenticated) while still folding the dial roots into the accept side. List fields now concatenate, matching the CLI fold and the documented behavior, with regression tests covering distinct roots in both tables.
  • Disagreed (kept as designed): the single root serving both directions is the PR's deliberate, documented tradeoff (the description's "Merged trust roots" section, with tls.system_roots as the escape hatch). Worth being aware of the sharp edge though: a root added only to verify outbound dials now also arms inbound mTLS, and a validated client cert grants full access within the path root. The PR already keeps client_cert/client_key separate for the same class of reason, so splitting root into dial/accept halves has precedent if we ever want it; that's a design call for review rather than something I changed under a takeover.

[high] "This breaking dev stack must not target main" — artifact of the review setup: Codex diffed against origin/main, but the PR targets dev, which is exactly where a semver break goes per CONTRIBUTING. The valid kernel (the branch trailing dev by 37 commits) is addressed by the rebase.

One pre-existing issue surfaced while validating: goaway_cluster::cluster_diamond_goaway_seamless_failover fails deterministically on clean origin/dev (a duplicate group after the drained leg, 22 vs 21). Unrelated to this PR; tracking it separately.

(Written by Fable 5)

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f16ba4953e

ℹ️ 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread rs/moq-native/src/config.rs Outdated
Comment on lines +385 to +388
// The delay always parses to a value, so `or` would never fall
// back; keep the primary side wholesale.
#[cfg(feature = "websocket")]
websocket: a.websocket,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve fallback WebSocket settings during config merge

When the relay loads a legacy [client.websocket] table, a is normally the default endpoint config, but this branch always keeps a.websocket because its delay is pre-populated with 200 ms. Consequently, fallback values such as enabled = false or a custom delay are discarded by merge_legacy_tables, despite Config::or promising to fill every unset knob. Keep these fields optional until after merging, then apply their defaults so legacy TOML remains effective. rs/CLAUDE.mdL116-L116

Useful? React with 👍 / 👎.

@kixelated

Copy link
Copy Markdown
Collaborator Author

CI status: the Check failure on the latest push is not this PR. It fails in js/net's integration: lite applies initial and updated group bounds, which is broken on clean dev by the af7b572 merge conflict resolution (a break on endGroup present in neither parent). #2758 already root-causes and fixes it; this PR contains no JS changes (git diff dev -- js/ is empty), so a re-run after #2758 lands should clear it. Smoke and Swift are green, and the Rust side passes locally (just check, the previously-failing cluster_unknown suite, and the rustdoc -D warnings gate).

(Written by Fable 5)

@kixelated
kixelated force-pushed the claude/github-issue-2696-fbda00 branch from f16ba49 to 6d8308c Compare August 12, 2026 15:38

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6d8308c61b

ℹ️ 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread rs/moq-native/src/tls.rs Outdated
cert: concat(self.cert, fallback.cert),
key: concat(self.key, fallback.key),
generate: concat(self.generate, fallback.generate),
root: concat(self.root, fallback.root),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Separate inbound mTLS roots from dial verification

When a legacy relay config has [server].tls.root for inbound mTLS, concatenating it into the unified root also makes it the outbound server-verification store. A relay that previously dialed public certificates using system roots will now reject them, while valid combinations such as a server mTLS root plus [client].tls.disable_verify or fingerprint fail startup in Config::verification as mutually exclusive. Preserve the server root as accept-only trust instead of folding it into the dial policy. (Written by GPT-5.6 Sol)

AGENTS.md reference: AGENTS.md:L91-L95

Useful? React with 👍 / 👎.

Comment thread rs/moq-relay/src/config.rs Outdated
}
client.tls.cert.clear();
client.tls.key.clear();
self.endpoint = std::mem::take(&mut self.endpoint).or(client);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep legacy client version pins dial-only

For a retained relay TOML config with [client] version = [...], this merge puts the pin in endpoint.version and leaves cluster.version empty. Relay::load therefore applies the old outbound-only pin to both the cluster client and the public server, causing inbound peers that use any other otherwise-supported version to fail negotiation. Move the legacy client version into cluster.version before merging so existing role-specific configs retain their behavior. (Written by GPT-5.6 Sol)

AGENTS.md reference: AGENTS.md:L91-L95

Useful? React with 👍 / 👎.

@kixelated
kixelated force-pushed the claude/github-issue-2696-fbda00 branch from 6d8308c to d138db9 Compare August 12, 2026 16:00
@kixelated

Copy link
Copy Markdown
Collaborator Author

Rebased onto the new dev tip (0bde6b1), which includes #2766, so the unrelated @moq/net integration failure from the last run should be gone.

Two --all-features compile errors from the previous CI run, now fixed:

  • Mine: transcode.rs still read .connect where the field is now .connect.url. It's behind the transcode feature, which my default-feature clippy run doesn't compile; verified with cargo check -p moq-cli --features transcode.
  • Not mine, and it blocks every PR against dev: moq play sets decode.latency_max, but feat(moq-net)!: add a Latency type and standardize on latency_max #2688 renamed that field to latency and retyped it as Latency, while feat(cli): add moq play #2697 added the call site against the old shape. Verified pre-existing by compiling moq-cli --features play on a clean origin/dev worktree (fails identically there). It slipped through because Check only runs on pull_request, so nothing built --all-features on the merge itself. Fixed in a separate commit (ef2eaa1) since it isn't part of this refactor. Happy to split it into its own PR against dev if you'd rather unblock everyone else ahead of this one.

(Written by Fable 5)

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ef2eaa1cb1

ℹ️ 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread rs/moq-bench/src/config.rs Outdated
#[command(flatten)]
#[serde(default)]
pub client: moq_native::ClientConfig,
pub client: moq_native::Config,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve client-only benchmark TOML mappings

When an existing moq-bench TOML contains [client] bind = "..." or [client.tls] cert/key, deserializing it as the unified Config places those values in the accept-side bind and served-certificate fields. The later resolved() call does not remap TOML fields, while Client reads connect.bind and tls.client_cert/client_key, so the source bind is silently ignored and mTLS authentication fails. Fresh evidence beyond the earlier URL issue is that moq-relay::merge_legacy_tables performs these exact client-only remappings, but this loader does not; apply the same migration before resolving. (Written by GPT-5.6 Sol)

AGENTS.md reference: AGENTS.md:L91-L95

Useful? React with 👍 / 👎.

Comment thread demo/boy/justfile

# Start a Game Boy emulator publisher.
start rom url='http://localhost:4443':
cargo run --bin moq-boy -- --client-connect "{{ url }}" --rom "{{ rom }}" --location localhost

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.

good change

Comment thread demo/relay/leaf0.toml Outdated
# nodes' server certs are signed by `ca.pem`, so no custom trust
# store is required beyond this one file.
tls.root = ["ca.pem"]
tls.client_cert = "leaf0.crt"

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.

not a fan.

I think the existing server/client structs made sense here.

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.

Done in the re-cut: the merged [endpoint] table is gone. The relay's tables are [connect] and [listen] now (with client/server serde aliases so existing configs keep parsing), and the types are role modules, moq_native::connect::Config / moq_native::listen::Config, per your suggestion.

(Written by Fable 5)

Comment thread doc/bin/relay/config.md
# only; a non-loopback bind logs a warning. Requires the `tcp` build feature.
[server.tcp]
[endpoint.tcp]
bind = "127.0.0.1:4444"

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.

TCP can't use a single bind for both clients and servers (technically yes, but not if you want to dial multiple connections to the same server).

I think server. made more sense

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.

Confirmed by the spike, and it's now the documented reason: quiche binds a fresh socket per connection attempt, so a shared or pinned dial port costs it address racing entirely. The dial socket is --connect-bind (its own knob on the dial side), the listener is --listen, and they stay separate.

(Written by Fable 5)

Comment thread doc/bin/relay/config.md
timeout = "30s"

# Disable TLS verification (development only!)
tls.disable_verify = true

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.

While we're here, maybe rename it to insecure or something?

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.

Renamed: it's --connect-tls-insecure / connect.tls.insecure now, with disable_verify kept as a serde alias. The name should say what it does.

(Written by Fable 5)

// Automatically reconnects if the connection drops.
async fn run_session(origin: moq_net::origin::Producer) -> anyhow::Result<()> {
// Optional: Use moq_native to make a QUIC client.
let client = moq_native::ClientConfig::default().init()?;

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.

if we do go back to Client/Server structs, I tihnk a client/server module might be nicer? moq_native::client::Config

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.

Done in the re-cut: the merged [endpoint] table is gone. The relay's tables are [connect] and [listen] now (with client/server serde aliases so existing configs keep parsing), and the types are role modules, moq_native::connect::Config / moq_native::listen::Config, per your suggestion.

(Written by Fable 5)

Comment thread rs/moq-native/src/config.rs Outdated
/// How [`crate::Client::connect`] reacts to a peer's GOAWAY (`--goaway-*`).
#[command(flatten)]
#[serde(default)]
pub goaway: crate::GoawayConfig,

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.

This should go in connect

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.

Applied: backoff and goaway are in connect now, tcp/unix are on listen, and backend is per-role (connect.backend / listen.backend) rather than in the shared quic section, which turned out to matter: a quiche acceptor with a quinn dialer is a real setup that the merged config had quietly removed.

(Written by Fable 5)

Comment thread rs/moq-native/src/config.rs Outdated
/// Retry pacing for [`crate::Client::connect`] (`--backoff-*`).
#[command(flatten)]
#[serde(default)]
pub backoff: crate::Backoff,

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.

This should go in connect

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.

Applied: backoff and goaway are in connect now, tcp/unix are on listen, and backend is per-role (connect.backend / listen.backend) rather than in the shared quic section, which turned out to matter: a quiche acceptor with a quinn dialer is a real setup that the merged config had quietly removed.

(Written by Fable 5)

Comment thread rs/moq-native/src/config.rs Outdated
/// Auto-detected from compiled features if not specified.
#[serde(skip_serializing_if = "Option::is_none")]
#[arg(id = "backend", long = "backend", env = "MOQ_BACKEND")]
pub backend: Option<QuicBackend>,

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.

This should go in quic

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.

Applied: backoff and goaway are in connect now, tcp/unix are on listen, and backend is per-role (connect.backend / listen.backend) rather than in the shared quic section, which turned out to matter: a quiche acceptor with a quinn dialer is a real setup that the merged config had quietly removed.

(Written by Fable 5)

Comment thread rs/moq-native/src/config.rs Outdated
#[cfg(all(feature = "uds", unix))]
#[command(flatten)]
#[serde(default)]
pub unix: crate::unix::Config,

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.

Maybe in server?

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.

Applied: backoff and goaway are in connect now, tcp/unix are on listen, and backend is per-role (connect.backend / listen.backend) rather than in the shared quic section, which turned out to matter: a quiche acceptor with a quinn dialer is a real setup that the merged config had quietly removed.

(Written by Fable 5)

Comment thread rs/moq-native/src/config.rs Outdated
#[cfg(feature = "tcp")]
#[command(flatten)]
#[serde(default)]
pub tcp: crate::tcp::Config,

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.

maybe in server?

@kixelated
kixelated force-pushed the claude/github-issue-2696-fbda00 branch from ef2eaa1 to 0052ebe Compare August 13, 2026 00:07
@kixelated kixelated changed the title refactor(moq-native)!: merge the client/server split into one endpoint Config refactor(moq-native)!: name the endpoint roles connect and listen Aug 13, 2026
@kixelated

Copy link
Copy Markdown
Collaborator Author

Re-cut per your review: this keeps the client/server split instead of merging the two configs, so the diff is now a rename rather than a restructure (−5642/+2649 against the previous revision's shape).

What changed from the merged design:

  • connect:: / listen:: role modules, per your moq_native::client::Config suggestion, with --connect and --listen as the two bare flags.
  • backoff and goaway moved into connect, and the accept-only preferred-address / QUIC-LB knobs moved onto listen, as you asked.
  • backend and version are per-role, so quiche-accepting-with-quinn-dialing works again (the merge had quietly removed it). That also made the --cluster-version knob from the previous revision redundant, so it's gone.
  • disable_verify is insecure.

Two things worth your attention, both explained in the description:

  • Sharing quic is forced, not chosen. clap cannot flatten one struct twice in a parser, which is exactly why dev carries duplicated quic::Client/quic::Server definitions. So quic::Config is a sibling of the two role configs, handed to each at init. The cost is that QUIC tuning can no longer differ per role, and [server.quic]/[client.quic] become a top-level [quic] (a move across tables, which serde aliases can't cover, so it fails loudly at startup).
  • The merged trust root that Codex flagged as critical is simply gone. connect.tls.root and listen.tls.root are separate fields again, so there is no shared-root tradeoff to weigh.

Also added demo_configs_parse, which reads the real demo/relay/*.toml files. It caught a config-breaking rename while I was writing it, which is exactly the class of bug that otherwise surfaces as someone's relay refusing to boot.

Verified: cargo fmt, workspace clippy -D warnings, rustdoc -D warnings --all-features, and 2942/2942 tests.

(Written by Fable 5)

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0052ebe54b

ℹ️ 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread rs/moq-native/src/listen.rs Outdated
/// `tcp`/`unix` listener is configured to run a stream-only server with no
/// QUIC.
#[serde(alias = "listen")]
#[arg(id = "listen", long = "listen", alias = "server-bind", env = "MOQ_LISTEN")]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve the released environment variable names

Existing deployments using MOQ_SERVER_BIND now leave bind unset because the repository no longer registers that environment variable anywhere; the server-bind option alias does not create an environment-variable alias, so the relay falls back to [::]:443 rather than its configured address. The same regression affects the renamed backend/version/TLS and client TLS/WebSocket environment variables, including MOQ_SERVER_TLS_CERT and MOQ_CLIENT_TLS_ROOT; retain hidden env-backed fields and fold them into the canonical values. (Written by GPT-5.6 Sol)

AGENTS.md reference: AGENTS.md:L91-L95

Useful? React with 👍 / 👎.

Comment on lines +18 to +19
#[serde(alias = "server")]
pub listen: moq_native::listen::Config,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Migrate nested legacy QUIC TOML tables

When an existing relay file contains [server.quic] or [client.quic], aliasing only the outer table deserializes it directly into listen::Config or connect::Config, both of which deny the now-unknown quic field. The new top-level quic field therefore does not preserve these configurations, and the relay refuses to start instead of folding their settings into it; migrate the nested legacy tables before deserialization into the canonical config. (Written by GPT-5.6 Sol)

AGENTS.md reference: AGENTS.md:L91-L95

Useful? React with 👍 / 👎.

Comment thread rs/moq-cli/src/main.rs
Comment on lines +213 to 214
if moq.client.url.is_some()
&& let Some(reconnect) = net.client(moq.client.clone())?.consume(origin.clone())

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Resolve legacy URLs before deciding whether to dial

When moq is invoked with the retained --client-connect flag or MOQ_CLIENT_CONNECT, MoqSide::validate succeeds because it inspects resolved().url, but this raw field remains None, so neither the consume connection nor the analogous publish connection at line 271 is ever started. Fresh evidence beyond the earlier bench/clock report is that the final moq-cli flow still gates client construction on the unresolved field; resolve once before these checks. (Written by GPT-5.6 Sol)

AGENTS.md reference: AGENTS.md:L91-L95

Useful? React with 👍 / 👎.

Comment on lines +18 to 20
pub fn init(self, quic: crate::quic::Config) -> crate::Result<Server> {
Server::new(self, quic)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Fold legacy QUIC flags on the server path

For a server initialized from retained --server-quic-* flags or their old environment variables, the values exist only in quic.legacy, but this path passes the unresolved config to Server::new; consequently the listener silently uses defaults, while a client initialized from the same config does call quic.resolved() and may incorrectly receive the server-only legacy value. Resolve the QUIC config here just as connect::Config::init does. (Written by GPT-5.6 Sol)

AGENTS.md reference: AGENTS.md:L91-L95

Useful? React with 👍 / 👎.

Comment thread doc/lib/rs/env/native.md

```rust
let client = moq_native::ClientConfig::default().init()?;
let client = moq_native::connect::Config::default().init()?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Pass the QUIC config in the Rust quick start

The final connect::Config::init signature requires a quic::Config, so this newly updated example still does not compile with .init()?; the surrounding text also continues linking the removed ClientConfig type. Fresh evidence after the earlier resolution is the current two-argument API together with this one-argument invocation, which is also repeated in doc/lib/rs/index.md. (Written by GPT-5.6 Sol)

AGENTS.md reference: AGENTS.md:L91-L96

Useful? React with 👍 / 👎.

@kixelated
kixelated force-pushed the claude/github-issue-2696-fbda00 branch from 0052ebe to 500adbb Compare August 13, 2026 06:24

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 500adbbc5f

ℹ️ 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread rs/moq-cli/src/cluster.rs

let client = config
.init()?
.init(Default::default())?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Pass the shared QUIC config to LAN peer dials

When --cluster-lan is combined with any --quic-* setting, each discovered peer is initialized with a fresh default config here, so outbound mesh sessions ignore the configured stream limit, idle timeout, MTU discovery, congestion control, qlog, and GSO settings even though the listener and ordinary relay dial receive Net::quic. Store or pass the shared QUIC config into Lan and use it for these peer clients.

Useful? React with 👍 / 👎.

Comment thread doc/bin/relay/config.md
# Listen address for HTTP (TCP)
# Defaults to disabled if not specified
listen = "0.0.0.0:4443"
bind = "0.0.0.0:4443"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep listen in the web HTTP TOML example

A user copying this [web.http] example gets an unknown field bind startup error because HttpConfig still defines listen at rs/moq-relay/src/web.rs:61 and denies unknown fields. This section is unrelated to the endpoint role rename, so the example must remain listen = "0.0.0.0:4443".

AGENTS.md reference: AGENTS.md:L179-L179

Useful? React with 👍 / 👎.

The config surface spelled every transport knob twice, `--client-*` and
`--server-*`, backed by duplicated quic::Client/quic::Server and
tls::Client/tls::Server type definitions. Keep the two roles apart, since
most knobs really do belong to one of them, and fix the naming instead.

moq_native::connect::Config (--connect-*) is the dial side and
moq_native::listen::Config (--listen-*) the accept side, each owning short
unprefixed names. The tuning that means the same thing whichever way a
connection was opened moves to one shared quic::Config (--quic-*), handed
to each role at init; clap cannot flatten one struct twice, so per-role
tuning would mean duplicate definitions that drift. backend and version
stay per-role: a quiche acceptor with a quinn dialer is a real setup, and
a per-role version lets a relay pin its mesh wire version without
narrowing what it accepts.

Splitting tls is what separates the two trust stores again:
connect.tls.root verifies servers you dial, listen.tls.root authenticates
mTLS clients, and the dialing identity drops its client_ prefix since the
module now supplies the role. The accept-only QUIC knobs (preferred
address, QUIC-LB) move onto listen.

Reconnecting is the default, so the flag names the exception: connect.once
(--connect-once) replaces the released `reconnect` pair of flag and TOML
key, which keep working inverted.

Every released flag and env var still configures the same thing, which
tests/released_cli.rs freezes: the published moq-relay surface, checked
against the parser this build has. Writing it found the hole. A clap alias
carries the flag name and not the env var, so 20 released variables parsed
as before while being silently ignored in the environment, including the
MOQ_SERVER_BIND and MOQ_SERVER_TLS_* that this repo's own nix module and
fly.toml set. Each released spelling now lives on a hidden arg carrying its
original env var, folded by resolved() with a deprecation warning.

Three more bugs came out of the same audit. listen::Config::init never
resolved the quic section, dropping --server-quic-max-streams entirely, so
Client::new and Server::new now resolve their own inputs. Relay::load
filled in DEFAULT_MAX_STREAMS before the fold ran, and the fold prefers the
canonical field, so a relay still using the released spelling was pinned to
10,000 streams. And --websocket-enabled/--websocket-delay carried a clap
default_value, which the post-file CLI re-parse materializes over whatever
a config file said.

Closes #2696

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@kixelated
kixelated force-pushed the claude/github-issue-2696-fbda00 branch from 500adbb to 9de5aa9 Compare August 13, 2026 21:16

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9de5aa90d1

ℹ️ 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".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread rs/moq-boy/src/main.rs
let (cmd_tx, cmd_rx) = tokio::sync::mpsc::channel::<input::Command>(64);
let url = config.client.connect.clone().context("--client-connect is required")?;
let client = config.client.clone().init()?;
let url = config.client.url.clone().context("--connect is required")?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Resolve the legacy URL before reading it

When moq-boy is launched with the retained --client-connect flag or MOQ_CLIENT_CONNECT, the URL exists only in client.legacy.url; init() would fold it, but this raw read happens first and returns the misleading --connect is required error. The test masks the regression by calling resolved() explicitly, so resolve the config before extracting the URL. (Written by GPT-5.6 Sol)

AGENTS.md reference: AGENTS.md:L102-L104

Useful? React with 👍 / 👎.

Comment thread rs/moq-cli/src/args.rs
Comment on lines +130 to +131
pub fn server_config(&self) -> moq_native::listen::Config {
let mut config = self.server.resolved();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Use the resolved listener for downstream decisions

When the retained --server-bind flag or MOQ_SERVER_BIND is used, this helper correctly builds the server from the resolved bind, but spawn_server later consults the unresolved moq.server.bind at lines 110 and 121. With --cluster-lan, that makes cluster::serve treat the explicitly configured listener as private and reject every non-mesh request; without the mesh, it skips the /certificate.sha256 endpoint, so http:// clients cannot bootstrap the generated certificate. Carry the resolved listener through all of these decisions. (Written by GPT-5.6 Sol)

AGENTS.md reference: AGENTS.md:L102-L104

Useful? React with 👍 / 👎.

Comment on lines +346 to +349
default_value = "[::]:0",
env = "MOQ_CONNECT_BIND"
)]
pub bind: net::SocketAddr,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve an explicit default-valued connect bind

When an old MOQ_CLIENT_BIND or --client-bind remains present during migration, an explicit canonical --connect-bind [::]:0 cannot override it: resolved() treats equality with default_bind() as proof that the canonical option was absent and selects the legacy address. Keep this TOML-loadable clap field optional through merging and apply [::]:0 afterward, so explicit canonical values remain distinguishable from defaults. (Written by GPT-5.6 Sol) rs/CLAUDE.mdL116-L117

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant