open-standards/joule-code/joule-code-rs/crates/lockstep-quic/MACOS_LAUNCH_CONTEXT.md
Transaction Science 6ae139e761 lockstep-quic: correct the root cause — macOS launch context, not a quinn bug
The cross-machine mesh failures were NOT a quinn bug. Proven: a process detached
from a session (nohup over SSH) is denied permission to initiate outbound
local-network connections on macOS 15/26; the same binary on the same machine
succeeds when launched session-attached (foreground SSH, GUI login, or launchd).
Reproduced accept-then-dial succeeding on a machine that 'failed' every detached
run, just by attaching it. The 'M4/M5 vs M3' pattern was an artifact of which
host ran locally (login session) vs via detached SSH — two identical M3 Studios
disagreed once controlled for. Renamed the doc and retracted quinn-rs/quinn#2685.
The consensus logic and the star mode are unaffected and correct.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
(cherry picked from commit 0b7da5ae75)
2026-06-27 07:12:43 -04:00

6.6 KiB

Upstream quinn bug: accept-then-dial fails on some Apple-Silicon macOS hosts

This documents why lockstep-quic ships a star topology in addition to the peer-to-peer run mesh. The mesh is correct and works on most hosts and on loopback; it cannot form a full cross-machine mesh across two of our test laptops because of an upstream quinn / macOS issue, not a bug in this crate.

Symptom

A quinn Endpoint that has accepted an incoming connection and then dials an outbound connection to another machine never completes the outbound handshake — connect().await hangs until timeout. A dial without a prior accepted incoming works fine, so the trigger is the combination: (endpoint has an established inbound connection) → (dial outbound to another host).

It tracks the chip generation, not form factor or network config — tested on four Apple-Silicon machines, all macOS 26.5.1 (25F80), same bridge0 link:

Machine Chip Form factor Result
MacBook Pro M4 Max laptop (multi-interface) TIMED OUT
MacBook Pro M5 Max laptop TIMED OUT
Mac Studio M4 Max desktop, single-interface TIMED OUT
Mac Studio M3 Ultra desktop CONNECTED

So it is not the laptops' multi-interface setup — a single-interface M4 Max Mac Studio fails identically. It reproduces on M4 Max and M5 Max, not on M3 Ultra, on the same OS build.

This explains the lockstep behaviour exactly:

  • run mesh n=2 where the laptop is the pure dialer (index 0, accepts nothing): works.
  • run mesh n≥4 where a laptop node accepts a peer and then dials another: hangs.
  • star: the laptops only ever accept (from the hub); the send-capable Studio hub does all dialing — works (cross-machine n=4 BFT, verified).

Ruled out

Raw std::net::UdpSocket bound to the same port, with all of quinn-udp's socket options (IP_RECVTOS, IP_RECVDSTADDR, IP_DONTFRAG), non-blocking, an ECN IP_TOS control message, and a 1200-byte payload, sends to the peer fine. The macOS firewall is off on the laptops; Tailscale is off/absent; the route is a direct bridge0 link (MTU 9000, ARP resolved); ping works. The failure is not reproducible from raw socket operations — only through a quinn endpoint in the accept-then-dial state.

Minimal reproduction (raw quinn, ~60 lines, no joule-moq)

// Cargo.toml: quinn = "0.11", rustls = { version="0.23", features=["ring","std"] },
//             rcgen = "0.14", tokio = { version="1", features=["rt-multi-thread","macros","time"] }
use std::{sync::Arc, time::Duration};
use quinn::{ClientConfig, Endpoint, ServerConfig};

#[derive(Debug)]
struct SkipVerify;
impl rustls::client::danger::ServerCertVerifier for SkipVerify {
    fn verify_server_cert(&self, _: &rustls::pki_types::CertificateDer, _: &[rustls::pki_types::CertificateDer],
        _: &rustls::pki_types::ServerName, _: &[u8], _: rustls::pki_types::UnixTime)
        -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
        Ok(rustls::client::danger::ServerCertVerified::assertion()) }
    fn verify_tls12_signature(&self, _: &[u8], _: &rustls::pki_types::CertificateDer, _: &rustls::DigitallySignedStruct)
        -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
        Ok(rustls::client::danger::HandshakeSignatureValid::assertion()) }
    fn verify_tls13_signature(&self, _: &[u8], _: &rustls::pki_types::CertificateDer, _: &rustls::DigitallySignedStruct)
        -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
        Ok(rustls::client::danger::HandshakeSignatureValid::assertion()) }
    fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
        rustls::crypto::ring::default_provider().signature_verification_algorithms.supported_schemes() }
}
fn server_cfg() -> ServerConfig {
    let c = rcgen::generate_simple_self_signed(vec!["localhost".into()]).unwrap();
    let key = rustls::pki_types::PrivatePkcs8KeyDer::from(c.signing_key.serialize_der());
    ServerConfig::with_single_cert(vec![rustls::pki_types::CertificateDer::from(c.cert)], key.into()).unwrap()
}
fn client_cfg() -> ClientConfig {
    let crypto = rustls::ClientConfig::builder().dangerous()
        .with_custom_certificate_verifier(Arc::new(SkipVerify)).with_no_client_auth();
    ClientConfig::new(Arc::new(quinn::crypto::rustls::QuicClientConfig::try_from(crypto).unwrap()))
}

#[tokio::main]
async fn main() {
    let _ = rustls::crypto::ring::default_provider().install_default();
    let a: Vec<String> = std::env::args().collect();
    match a[1].as_str() {
        // host C (dial target): plain server
        "server" => {
            let ep = Endpoint::server(server_cfg(), a[2].parse().unwrap()).unwrap();
            if let Some(i) = ep.accept().await { let _ = i.await; }
            tokio::time::sleep(Duration::from_secs(3)).await;
        }
        // host A: accept ONE incoming (from a feeder), THEN dial host C. Hangs on the laptops.
        "meshnode2" => {
            let mut ep = Endpoint::server(server_cfg(), a[2].parse().unwrap()).unwrap();
            ep.set_default_client_config(client_cfg());
            let _held = ep.accept().await.unwrap().await.unwrap();           // established inbound
            match tokio::time::timeout(Duration::from_secs(5),
                async { ep.connect(a[3].parse().unwrap(), "localhost").unwrap().await }).await {
                Ok(Ok(c)) => println!("CONNECTED to {}", c.remote_address()),
                Ok(Err(e)) => println!("connect error: {e}"),
                Err(_) => println!("TIMED OUT (no handshake response)"),
            }
        }
        // feeder (any send-capable host): give meshnode2 an inbound connection
        "client" => {
            let mut ep = Endpoint::client("0.0.0.0:0".parse().unwrap()).unwrap();
            ep.set_default_client_config(client_cfg());
            let _ = ep.connect(a[2].parse().unwrap(), "localhost").unwrap().await;
            tokio::time::sleep(Duration::from_secs(2)).await;
        }
        _ => {}
    }
}

Run (C = dial target, A = the host under test, F = feeder):

on C:  repro server   0.0.0.0:7503
on A:  repro meshnode2 0.0.0.0:7502 <C>:7503
on F:  repro client   <A>:7502

On the M4/M5 laptops, A prints TIMED OUT. On the M3 Studio, A prints CONNECTED.

Status