diff --git a/demo/relay/leaf0.toml b/demo/relay/leaf0.toml index 40872815d0..5aab70afd9 100644 --- a/demo/relay/leaf0.toml +++ b/demo/relay/leaf0.toml @@ -15,8 +15,9 @@ tls.key = ["leaf0.key"] # To authenticate peers by certificate, trust client certificates signed by # this CA and switch `[auth]` below to the auth server, which grants them -# through `--mtls-publish` / `--mtls-subscribe`. Public rules ignore -# certificates, so the relay refuses a client CA alongside them. +# as cluster peers through `--mtls-publish`, `--mtls-subscribe`, and +# `--mtls-peer`. Public rules ignore certificates, so the relay refuses a +# client CA alongside them. # tls.root = ["ca.pem"] [connect] @@ -44,7 +45,7 @@ connect = ["https://localhost:4443/"] public = "**" # To exercise JWT + public-pattern auth instead, drop `public` above, run -# `moq auth serve --key root.jwk --public-subscribe 'anon/**' --public-subscribe 'demo/**' --public-publish 'anon/**' --mtls-publish '**' --mtls-subscribe '**'`, +# `moq auth serve --key root.jwk --public-subscribe 'anon/**' --public-subscribe 'demo/**' --public-publish 'anon/**' --mtls-publish '**' --mtls-subscribe '**' --mtls-peer`, # point the relay at it, and uncomment `listen.tls.root` above: # url = "http://127.0.0.1:4440/" diff --git a/demo/relay/leaf1.toml b/demo/relay/leaf1.toml index c112e619ac..5e80c9b142 100644 --- a/demo/relay/leaf1.toml +++ b/demo/relay/leaf1.toml @@ -15,8 +15,9 @@ tls.key = ["leaf1.key"] # To authenticate peers by certificate, trust client certificates signed by # this CA and switch `[auth]` below to the auth server, which grants them -# through `--mtls-publish` / `--mtls-subscribe`. Public rules ignore -# certificates, so the relay refuses a client CA alongside them. +# as cluster peers through `--mtls-publish`, `--mtls-subscribe`, and +# `--mtls-peer`. Public rules ignore certificates, so the relay refuses a +# client CA alongside them. # tls.root = ["ca.pem"] [connect] @@ -44,7 +45,7 @@ connect = ["https://localhost:4443/", "https://localhost:4444/"] public = "**" # To exercise JWT + public-pattern auth instead, drop `public` above, run -# `moq auth serve --key root.jwk --public-subscribe 'anon/**' --public-subscribe 'demo/**' --public-publish 'anon/**' --mtls-publish '**' --mtls-subscribe '**'`, +# `moq auth serve --key root.jwk --public-subscribe 'anon/**' --public-subscribe 'demo/**' --public-publish 'anon/**' --mtls-publish '**' --mtls-subscribe '**' --mtls-peer`, # point the relay at it, and uncomment `listen.tls.root` above: # url = "http://127.0.0.1:4440/" diff --git a/demo/relay/prod.toml b/demo/relay/prod.toml index 287af9c74d..61896b22af 100644 --- a/demo/relay/prod.toml +++ b/demo/relay/prod.toml @@ -32,7 +32,7 @@ key = "key.pem" # moq auth serve --listen 127.0.0.1:4440 --key root.jwk \ # --public-subscribe 'anon/**' --public-subscribe 'demo/**' \ # --public-publish 'anon/**' --public-publish 'demo/viewer/**' \ -# --mtls-publish '**' --mtls-subscribe '**' +# --mtls-publish '**' --mtls-subscribe '**' --mtls-peer # # For multiple keys with rotation, give it `--key-dir /path/to/keys/` instead: # keys are named {kid}.jwk and resolved by the kid in the JWT header. diff --git a/demo/relay/root.toml b/demo/relay/root.toml index 0597547604..0043426f58 100644 --- a/demo/relay/root.toml +++ b/demo/relay/root.toml @@ -16,8 +16,9 @@ tls.key = ["root.key"] # To authenticate peers by certificate, trust client certificates signed by # this CA and switch `[auth]` below to the auth server, which grants them -# through `--mtls-publish` / `--mtls-subscribe`. Public rules ignore -# certificates, so the relay refuses a client CA alongside them. +# as cluster peers through `--mtls-publish`, `--mtls-subscribe`, and +# `--mtls-peer`. Public rules ignore certificates, so the relay refuses a +# client CA alongside them. # `just ca` generates the CA. # tls.root = ["ca.pem"] @@ -31,6 +32,6 @@ listen = "[::]:4443" public = "**" # To exercise JWT + public-pattern auth instead, drop `public` above, run -# `moq auth serve --key root.jwk --public-subscribe 'anon/**' --public-subscribe 'demo/**' --public-publish 'anon/**' --mtls-publish '**' --mtls-subscribe '**'`, +# `moq auth serve --key root.jwk --public-subscribe 'anon/**' --public-subscribe 'demo/**' --public-publish 'anon/**' --mtls-publish '**' --mtls-subscribe '**' --mtls-peer`, # point the relay at it, and uncomment `listen.tls.root` above: # url = "http://127.0.0.1:4440/" diff --git a/doc/bin/relay/auth.md b/doc/bin/relay/auth.md index e227663ec2..17df3535cb 100644 --- a/doc/bin/relay/auth.md +++ b/doc/bin/relay/auth.md @@ -248,8 +248,16 @@ verifies `listen.tls.root`, so a stream-only relay (no `listen.bind`) refuses to start with it too. Cluster peers are admitted the same way, so a mesh runs -`moq auth serve --mtls-publish '**' --mtls-subscribe '**'` (or a server -granting its cluster CA everything); see [Clustering](/bin/relay/cluster). +`moq auth serve --mtls-publish '**' --mtls-subscribe '**' --mtls-peer` (or a +server granting its cluster CA everything with `peer: true`); see +[Clustering](/bin/relay/cluster). `--mtls-peer` marks every certificate as +another relay, whose broadcasts entered the cluster elsewhere; leave it off +when certificates identify clients. `--mtls-upstream` also marks those relays +[upstream](/bin/relay/cluster#upstream-links) and needs `--mtls-peer`. +Adding or removing either flag on a running server ends each live mTLS session +at its next re-check, from the `--revalidate` cadence or a +[push](#the-contract) such as `moq auth revalidate`, so the mesh redials once; +until then, a session keeps its old marks. The LAN mesh credential on `/.cluster/` stays relay-internal: it is a secret the relay minted for itself, checked locally, and never a request to the server. @@ -272,7 +280,7 @@ hold, answered over the contract above. moq auth serve --listen 127.0.0.1:4440 \ --key-dir /etc/moq/keys \ --public-subscribe 'anon/**' --public-publish 'anon/**' \ - --mtls-publish '**' --mtls-subscribe '**' \ + --mtls-publish '**' --mtls-subscribe '**' --mtls-peer \ --tier edge --expires 1d --revalidate 1m --limit-remote 64 ``` @@ -300,7 +308,7 @@ certificate, not both. `--mtls-*` and `--public-*` are rooted at `/`, like a token with an empty root, and a session they reach nothing at is refused. Cluster peers are admitted by -certificate; a mesh needs `--mtls-publish '**' --mtls-subscribe '**'`. +certificate; a mesh needs `--mtls-publish '**' --mtls-subscribe '**' --mtls-peer`. Every grant carries `--tier`. As in 0.14, nothing is re-checked or closed by default: a session lives until its token's `exp` or its certificate's notAfter. @@ -338,7 +346,7 @@ anonymous rules move too. | `--auth-public-publish` / `--auth-public-subscribe` | `--public-publish` / `--public-subscribe`, as patterns | | `--auth-public-api URL` | your own server answering the contract | | `--auth-mtls-tier LABEL` | `--tier LABEL` (one tier per server) | -| `listen.tls.root` alone admitting a peer unscoped | `--mtls-publish '**' --mtls-subscribe '**'` | +| `listen.tls.root` alone admitting a peer unscoped | `--mtls-publish '**' --mtls-subscribe '**' --mtls-peer` | | `--auth-api` (token or proxy mode), `Cache-Control` | `--auth-url` pointed at any server answering the contract; `revalidate` and `expires` in the grant | | `--auth-domain` | your server reads `server_name` and decides | diff --git a/doc/bin/relay/cluster.md b/doc/bin/relay/cluster.md index 5aba2c0ff5..056b9e1287 100644 --- a/doc/bin/relay/cluster.md +++ b/doc/bin/relay/cluster.md @@ -106,8 +106,11 @@ The mark belongs to the link, whichever side dialed. A peer this relay dials is upstream when its `connect` or `connect_api` entry says so. A peer that dials in is upstream when its [grant](/bin/relay/auth#the-contract) sets `"upstream": true` beside `"peer": true`, so an edge whose auth server grants -that to core certificates treats a core that dials it as upstream too. A -relay that predates the mark treats every link as transit, so a cluster +that to core certificates treats a core that dials it as upstream too. +`moq auth serve --mtls-peer --mtls-upstream` grants it to every certificate, +so use it only where nothing but cores dial in with mTLS: on a hub that +leaves dial into, it marks every leaf upstream, and the hub stops forwarding +between them. A relay that predates the mark treats every link as transit, so a cluster migrates one region at a time. Which relay dials which is still the peer list's job: there are no roles and @@ -279,10 +282,12 @@ URL, `token` on a peer object, or a shared `cluster.token` file for every listed peer). The accepting relay admits a peer through the same lease as any client: its certificate is reported to the auth server, which grants it, so a mesh needs -`moq auth serve --mtls-publish '**' --mtls-subscribe '**'` (or a server of -your own that grants the cluster CA) behind `--auth-url`. A relay on -`--auth-public '**'` admits peers through that grant instead, as long as they -send no `cluster.token`: public rules refuse a token. LAN peers +`moq auth serve --mtls-publish '**' --mtls-subscribe '**' --mtls-peer` (or a +server of your own that grants the cluster CA with `peer: true`) behind +`--auth-url`. A relay on `--auth-public '**'` admits peers through that grant +instead, as long as they send no `cluster.token`: public rules refuse a token. +Such a peer is admitted as a client, so what it forwards counts as ingested +here. LAN peers authenticate with the mDNS credential on `/.cluster/`, a secret the relay minted for itself and checks locally, and never receive `cluster.token`. Dials retry forever with capped backoff, so a rejected peer @@ -291,9 +296,9 @@ is loud in the logs rather than fatal. See [Authentication](/bin/relay/auth#mtls A relay records whether each route entered here or came from a peer, which the hop list alone cannot say: a client and a peer each add one hop. Routes over a dial this relay made, and over an accepted LAN peer, count as a peer's. An -accepted peer counts only when its grant sets `peer: true`; otherwise it looks -like a client ingesting here. An embedder reads this as `Route::source()` and -filters with `origin::Consumer::local()`. +accepted peer counts only when its grant sets `peer: true`, as `--mtls-peer` +does; otherwise it looks like a client ingesting here. An embedder reads this +as `Route::source()` and filters with `origin::Consumer::local()`. The `/nodes` [internal endpoint](/bin/relay/http#get-nodes) lists the peers this relay dialed and holds a session with. diff --git a/doc/setup/upgrade.md b/doc/setup/upgrade.md index de3dc8c821..30243b4506 100644 --- a/doc/setup/upgrade.md +++ b/doc/setup/upgrade.md @@ -230,8 +230,8 @@ Other changes to a deployment: - **`moq auth serve` never re-checks or expires by default**, as 0.14 never did. `--revalidate` needs `--expires`, and `--limit-*` needs `--revalidate`. - **mTLS admits nothing on its own.** A verified client certificate is reported - to the auth server, which grants it. `moq auth serve --mtls-publish '**' --mtls-subscribe '**'` restores the old full access for every certificate - the relay's client CA verifies, so keep that CA to cluster peers. + to the auth server, which grants it. `moq auth serve --mtls-publish '**' --mtls-subscribe '**' --mtls-peer` restores the old full access for every certificate + the relay's client CA verifies, as a cluster peer, so keep that CA to cluster peers. - **`moq --listen` needs auth.** A CLI listener refuses to start without `--auth-url` or `--auth-public` instead of accepting everyone. - **Gossip discovery is removed.** A relay dials only the peers it lists or diff --git a/nix/modules/moq-relay.nix b/nix/modules/moq-relay.nix index 1a520b9874..de5745a080 100644 --- a/nix/modules/moq-relay.nix +++ b/nix/modules/moq-relay.nix @@ -215,7 +215,7 @@ in "${pkgs.moq-cli}/bin/moq auth serve" "--listen 127.0.0.1:${toString cfg.auth.port}" "--key ${authKey}" - "--mtls-publish '**' --mtls-subscribe '**'" + "--mtls-publish '**' --mtls-subscribe '**' --mtls-peer" ] ++ lib.optionals (cfg.auth.publicPath != null) [ "--public-publish '${publicPattern}' --public-subscribe '${publicPattern}'" diff --git a/rs/moq-auth/src/serve.rs b/rs/moq-auth/src/serve.rs index fb571a5539..9b2c379993 100644 --- a/rs/moq-auth/src/serve.rs +++ b/rs/moq-auth/src/serve.rs @@ -80,6 +80,12 @@ pub struct Policy { /// What a session presenting a verified certificate is granted, rooted at `/`; /// empty refuses it. pub mtls: Permissions, + /// Mark every session presenting a verified certificate as a cluster + /// [peer](Grant::peer), for a mesh whose relays dial each other with mTLS. + pub mtls_peer: bool, + /// Mark every certificate's peer as [upstream](Grant::upstream) too. Requires + /// [`mtls_peer`](Self::mtls_peer). + pub mtls_upstream: bool, /// The tier stamped on every grant. pub tier: Option, /// How often the relay re-checks each grant; `None` never re-checks. The contract @@ -135,7 +141,7 @@ impl Policy { if jwt.is_some() && request.tls.is_some() { return Err(Refusal::TokenAndCertificate); } - let (permissions, expires) = if let Some(jwt) = jwt { + let (permissions, expires, certificate) = if let Some(jwt) = jwt { let claims = self.verify(jwt).await?; let permissions = claims.authorize(&request.path).map_err(|err| match err { crate::Error::RootMismatch(path) => Refusal::RootMismatch { @@ -145,17 +151,17 @@ impl Policy { crate::Error::NoAccess(path) => Refusal::NoAccess { path }, other => Refusal::InvalidToken(other.to_string()), })?; - (permissions, claims.expires) + (permissions, claims.expires, false) } else if let Some(peer) = &request.tls { if self.mtls.is_empty() { return Err(Refusal::NoMtlsGrant); } - (authorize(&self.mtls, &request.path)?, peer.expires) + (authorize(&self.mtls, &request.path)?, peer.expires, true) } else { if self.public.is_empty() { return Err(Refusal::NoPublicGrant); } - (authorize(&self.public, &request.path)?, None) + (authorize(&self.public, &request.path)?, None, false) }; let mut grant = Grant::new(permissions.publish, permissions.subscribe); @@ -166,6 +172,8 @@ impl Policy { }; grant.revalidate = self.revalidate; grant.tier = self.tier.clone(); + grant.peer = certificate && self.mtls_peer; + grant.upstream = certificate && self.mtls_upstream; Ok(grant) } @@ -331,9 +339,12 @@ pub struct Server { impl Server { /// A server answering with `policy`, refusing one that asks for a re-check without - /// a bound, bounds grants past the clock's range, or caps sessions without the - /// re-check that ages out a dead relay's. + /// a bound, bounds grants past the clock's range, caps sessions without the + /// re-check that ages out a dead relay's, or marks an upstream that is not a peer. pub fn new(policy: Policy) -> crate::Result { + if policy.mtls_upstream && !policy.mtls_peer { + return Err(crate::Error::UpstreamWithoutPeer); + } if policy.revalidate.is_some() && policy.expires.is_none() { return Err(crate::Error::UnboundedRevalidate); } @@ -824,11 +835,52 @@ mod tests { // A certificate without a bound gets none by default, like 0.14. let grant = policy.decide(&with_peer(request("/"), None)).await.unwrap(); assert_eq!(grant.expires, None); + // A certificate is a client unless the policy says it is a relay. + assert!(!grant.peer); // The certificate does not stand in for a public grant. assert_eq!(policy.decide(&request("/")).await.unwrap_err(), Refusal::NoPublicGrant); } + /// `mtls_peer` and `mtls_upstream` mark a certificate's grant, and only a certificate's. + #[tokio::test] + async fn mtls_peer_marks_only_certificates() { + let (dir, key) = key_dir(); + let policy = Policy { + keys: Some(Keys::Dir(dir.path().into())), + public: rules(&["**"], &["**"]), + mtls: rules(&["**"], &["**"]), + mtls_peer: true, + ..Default::default() + }; + let grant = policy.decide(&with_peer(request("/"), None)).await.unwrap(); + assert!(grant.peer && !grant.upstream); + + let policy = Policy { + mtls_upstream: true, + ..policy + }; + let grant = policy.decide(&with_peer(request("/"), None)).await.unwrap(); + assert!(grant.peer && grant.upstream); + grant.validate().unwrap(); + let grant = policy.decide(&request("/")).await.unwrap(); + assert!(!grant.peer && !grant.upstream); + let jwt = sign(&key, "", &["**"], &[], None); + let grant = policy.decide(&with_token(request("/"), &jwt)).await.unwrap(); + assert!(!grant.peer && !grant.upstream); + } + + /// An upstream that is not a peer would answer grants every relay refuses. + #[test] + fn a_server_refuses_an_upstream_that_is_not_a_peer() { + let policy = Policy { + mtls: rules(&["**"], &["**"]), + mtls_upstream: true, + ..Default::default() + }; + assert!(matches!(Server::new(policy), Err(Error::UpstreamWithoutPeer))); + } + /// A certificate would override a JWT meant to narrow it, and a JWT would narrow or /// refuse a peer by accident, so presenting both is refused, even with a bad JWT. #[tokio::test] diff --git a/rs/moq-cli/src/auth.rs b/rs/moq-cli/src/auth.rs index 424314dd7f..477130dc1d 100644 --- a/rs/moq-cli/src/auth.rs +++ b/rs/moq-cli/src/auth.rs @@ -387,6 +387,14 @@ pub struct Serve { #[usage(long)] mtls_subscribe: Vec, + /// Mark sessions with a verified certificate as cluster peers: relays forwarding what entered elsewhere. + #[usage(long)] + mtls_peer: bool, + + /// Mark those peers upstream: never offered a route learned from another upstream. Needs `--mtls-peer`. + #[usage(long)] + mtls_upstream: bool, + /// The tier label stamped on every grant, handed to the relay's stats. #[usage(long)] tier: Option, @@ -426,6 +434,12 @@ impl Serve { "--limit-token and --limit-remote need --revalidate, which ages out the slots of dead relays" ); } + if self.mtls_peer && self.mtls_publish.is_empty() && self.mtls_subscribe.is_empty() { + anyhow::bail!("--mtls-peer needs --mtls-publish or --mtls-subscribe, or every certificate is refused"); + } + if self.mtls_upstream && !self.mtls_peer { + anyhow::bail!("--mtls-upstream needs --mtls-peer; only a cluster peer can be upstream"); + } // 0.14 read `anon` as the prefix `anon/`, and a pattern reads it as exactly the // broadcast `anon`, so either silent reading would mislead someone upgrading. let flags = [ @@ -460,6 +474,8 @@ impl Serve { }; policy.public = rules(&self.public_publish, &self.public_subscribe); policy.mtls = rules(&self.mtls_publish, &self.mtls_subscribe); + policy.mtls_peer = self.mtls_peer; + policy.mtls_upstream = self.mtls_upstream; policy.tier = self.tier.clone(); policy.revalidate = revalidate; policy.expires = expires; @@ -653,6 +669,8 @@ mod tests { "**", "--mtls-subscribe", "**", + "--mtls-peer", + "--mtls-upstream", "--tier", "internal", "--revalidate", @@ -673,6 +691,7 @@ mod tests { ); assert!(policy.public.publish.is_empty()); assert_eq!(policy.mtls.publish, ["**".parse().unwrap()].into_iter().collect()); + assert!(policy.mtls_peer && policy.mtls_upstream); assert_eq!(policy.tier.as_deref(), Some("internal")); assert_eq!(policy.revalidate, Some(std::time::Duration::from_secs(30))); assert_eq!(policy.expires, Some(std::time::Duration::from_secs(7200))); @@ -694,6 +713,7 @@ mod tests { let bare = serve(&["moq", "auth", "serve"]).policy().unwrap(); assert!(bare.keys.is_none()); assert!(bare.public.is_empty() && bare.mtls.is_empty()); + assert!(!bare.mtls_peer && !bare.mtls_upstream); assert_eq!(bare.revalidate, None); assert_eq!(bare.expires, None); @@ -705,6 +725,12 @@ mod tests { // Only a re-check keeps a slot alive. (&["--limit-token", "3"], "need --revalidate"), (&["--limit-remote", "3", "--expires", "1h"], "need --revalidate"), + // A peer marking with nothing granted to certificates marks nobody. + (&["--mtls-peer"], "--mtls-peer needs"), + ( + &["--mtls-subscribe", "**", "--mtls-upstream"], + "--mtls-upstream needs --mtls-peer", + ), ] { let argv: Vec<&str> = ["moq", "auth", "serve"].iter().chain(args).copied().collect(); let err = serve(&argv).policy().unwrap_err().to_string(); diff --git a/rs/moq-relay/tests/auth_lifetime.rs b/rs/moq-relay/tests/auth_lifetime.rs index b78bd2cd4f..d4e8a3c3bd 100644 --- a/rs/moq-relay/tests/auth_lifetime.rs +++ b/rs/moq-relay/tests/auth_lifetime.rs @@ -306,6 +306,16 @@ async fn assert_refused_with(client: moq_tokio::Client, url: &url::Url) { async fn spawn_quic_relay( auth: moq_relay::auth::Auth, root: Option, +) -> (std::net::SocketAddr, tokio::task::JoinHandle<()>) { + let cluster = cluster::Cluster::new(cluster::Options::default()).expect("cluster init"); + spawn_quic_relay_with(auth, root, cluster).await +} + +/// [`spawn_quic_relay`] serving `cluster`. +async fn spawn_quic_relay_with( + auth: moq_relay::auth::Auth, + root: Option, + cluster: cluster::Cluster, ) -> (std::net::SocketAddr, tokio::task::JoinHandle<()>) { let _ = rustls::crypto::aws_lc_rs::default_provider().install_default(); let mut config = moq_tokio::listen::Config::default(); @@ -315,7 +325,6 @@ async fn spawn_quic_relay( let server = config.init(Default::default()).expect("server init"); let addr = server.local_addr().expect("quic addr"); let mut server = server.listen().await.expect("listen"); - let cluster = cluster::Cluster::new(cluster::Options::default()).expect("cluster init"); let handle = tokio::spawn(async move { while let Some(request) = server.accept().await { let conn = Connection::new(request, cluster.clone(), auth.clone()); @@ -865,6 +874,74 @@ async fn a_certificate_admits_only_what_the_server_grants() { relay.abort(); } +/// A relay dialing in with a certificate, admitted by `moq auth serve --mtls-peer`, +/// is a cluster peer: what it announces entered the cluster elsewhere. +#[tokio::test] +async fn an_mtls_peer_is_a_cluster_peer() { + let source = mtls_route_source(true).await; + assert!( + matches!(source, moq_net::origin::Source::Peer(_)), + "an mTLS peer's route counted as {source:?}" + ); +} + +/// Without `--mtls-peer`, a certificate identifies a client ingesting here. +#[tokio::test] +async fn an_mtls_client_ingests_here() { + assert_eq!(mtls_route_source(false).await, moq_net::origin::Source::Local); +} + +/// The source the relay records for a broadcast announced over a certificate +/// session, admitted by `moq auth serve` with `mtls_peer` set as given. +async fn mtls_route_source(mtls_peer: bool) -> moq_net::origin::Source { + let _ = rustls::crypto::aws_lc_rs::default_provider().install_default(); + let dir = tempfile::tempdir().expect("tempdir"); + let (root, client_cert, client_key) = signed_client(dir.path()); + + let mut policy = moq_auth::serve::Policy::default(); + policy.mtls = moq_auth::Permissions::new(all(), all()); + policy.mtls_peer = mtls_peer; + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + let auth_url: url::Url = format!("http://{}/", listener.local_addr().unwrap()).parse().unwrap(); + let server = moq_auth::serve::Server::new(policy).unwrap(); + tokio::spawn(async move { server.serve(listener).await }); + + let cluster = cluster::Cluster::new(cluster::Options::default()).expect("cluster init"); + let (addr, relay) = spawn_quic_relay_with(build_auth(auth_url), Some(root), cluster.clone()).await; + + let mut config = moq_tokio::connect::Config::default(); + config.tls.insecure = Some(true); + config.bind = Some("127.0.0.1:0".parse().expect("parse bind")); + config.tls.cert = Some(client_cert); + config.tls.key = Some(client_key); + let peer = moq_tokio::origin::spawn(); + let _forwarded = peer.create_broadcast("forwarded").expect("create"); + _forwarded.announce(Default::default()).expect("announce"); + let url: url::Url = format!("moql://127.0.0.1:{}/", addr.port()).parse().unwrap(); + let _session = tokio::time::timeout( + TIMEOUT, + config + .init(Default::default()) + .expect("client init") + .with_publisher(peer.consume()) + .with_reconnect(false) + .connect(url) + .established(), + ) + .await + .expect("connect timeout") + .expect("the peer is admitted"); + + let mut announced = cluster.origin.consume().announced(); + let (update, _) = tokio::time::timeout(TIMEOUT, next_update(&mut announced)) + .await + .expect("timed out waiting for forwarded") + .expect("origin closed"); + assert_eq!(update.prefix.as_str(), "forwarded"); + relay.abort(); + update.route.source() +} + fn signed_client(dir: &std::path::Path) -> (std::path::PathBuf, std::path::PathBuf, std::path::PathBuf) { let ca_key = rcgen::KeyPair::generate().expect("ca keypair"); let mut ca_params = rcgen::CertificateParams::new(Vec::new()).expect("ca params");