fix(transport): harden QUIC admission boundaries
This commit is contained in:
@@ -46,6 +46,7 @@ type admission struct {
|
||||
lastTranscript []byte
|
||||
reusable bool
|
||||
failWorkOnce bool
|
||||
releaseFails bool
|
||||
authority protocol.SessionAuthority
|
||||
work protocol.ProviderSessionWork
|
||||
public ed25519.PublicKey
|
||||
@@ -67,11 +68,15 @@ func (a *admission) ProviderWork(context.Context, protocol.SessionAuthority) (pr
|
||||
if a.failWorkOnce { a.failWorkOnce = false; return protocol.ProviderSessionWork{}, context.DeadlineExceeded }
|
||||
return a.work, nil
|
||||
}
|
||||
func (a *admission) Release(context.Context, protocol.SessionAuthority) error { return nil }
|
||||
func (a *admission) Release(context.Context, protocol.SessionAuthority) error {
|
||||
if a.releaseFails { return errors.New("release failed") }
|
||||
return nil
|
||||
}
|
||||
|
||||
type provider struct{}
|
||||
type provider struct { failStart bool }
|
||||
type session struct { state protocol.ProviderState; video chan gateway.ProviderMedia; audio chan gateway.ProviderMedia; events chan gateway.ProviderEvent }
|
||||
func (provider) Start(_ context.Context, request gateway.LaunchRequest) (gateway.ProviderSession, error) {
|
||||
func (p provider) Start(_ context.Context, request gateway.LaunchRequest) (gateway.ProviderSession, error) {
|
||||
if p.failStart { return nil, context.DeadlineExceeded }
|
||||
return &session{state: protocol.ProviderState{Version:"1", SessionID:request.SessionID, State:gateway.ProviderStateReady, Channels:[]string{"video","audio","input","feedback"}}, video:make(chan gateway.ProviderMedia), audio:make(chan gateway.ProviderMedia), events:make(chan gateway.ProviderEvent)}, nil
|
||||
}
|
||||
func (s *session) Ready(context.Context) error { return nil }
|
||||
@@ -125,9 +130,9 @@ func main() {
|
||||
_, firstReplayErr := replayGuard.Admit(context.Background(),replayRequest)
|
||||
_, secondReplayErr := replayGuard.Admit(context.Background(),replayRequest)
|
||||
replayRejected := firstReplayErr == nil && errors.Is(secondReplayErr,gateway.ErrAdmissionRejected)
|
||||
admissionService := &admission{authority:authority,work:work,public:admissionKey.Public().(ed25519.PublicKey),reusable:mode == "reusable",failWorkOnce:mode == "post-retryable"}
|
||||
admissionService := &admission{authority:authority,work:work,public:admissionKey.Public().(ed25519.PublicKey),reusable:mode == "reusable",failWorkOnce:mode == "post-retryable",releaseFails:mode == "cleanup-release-failure"}
|
||||
newService := func() *gateway.Server {
|
||||
service, err := gateway.NewServer(gateway.ServerConfig{ListenAddress:"127.0.0.1:0",TLSConfig:server,GatewayID:authority.GatewayID,Admission:admissionService,Provider:provider{}}); if err != nil { panic(err) }
|
||||
service, err := gateway.NewServer(gateway.ServerConfig{ListenAddress:"127.0.0.1:0",TLSConfig:server,GatewayID:authority.GatewayID,Admission:admissionService,Provider:provider{failStart:mode == "provider-start-lost-response" || mode == "cleanup-release-failure"}}); if err != nil { panic(err) }
|
||||
return service
|
||||
}
|
||||
service := newService()
|
||||
@@ -139,7 +144,7 @@ func main() {
|
||||
services = []*gateway.Server{draining, service}
|
||||
addresses = []string{draining.Addr().String(), service.Addr().String()}
|
||||
}
|
||||
if mode == "post-retryable" {
|
||||
if mode == "post-retryable" || mode == "provider-start-lost-response" || mode == "cleanup-release-failure" {
|
||||
second := newService()
|
||||
services = []*gateway.Server{service, second}
|
||||
addresses = []string{service.Addr().String(), second.Addr().String()}
|
||||
@@ -303,7 +308,7 @@ fn callback_ed25519_signer_completes_tls13_quic_admission_without_private_key_in
|
||||
|
||||
assert_eq!(session.authority().session_id(), "session-1");
|
||||
assert_eq!(admission_calls.load(Ordering::SeqCst), 1);
|
||||
assert!(tls_calls.load(Ordering::SeqCst) >= 1);
|
||||
assert_eq!(tls_calls.load(Ordering::SeqCst), 1);
|
||||
assert!(admission_inputs
|
||||
.lock()
|
||||
.expect("admission transcript")
|
||||
@@ -598,6 +603,41 @@ fn overall_deadline_includes_synchronous_admission_signing() {
|
||||
assert!(started.elapsed() < Duration::from_secs(2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tls_signer_finite_deadline_overrun_is_cancelled_not_tls() {
|
||||
let oracle = Oracle::start("");
|
||||
let manifest = ConnectionManifest::decode(oracle.ready.manifest.as_bytes()).expect("manifest");
|
||||
let credential =
|
||||
NativeTunnelCredential::decode(oracle.ready.credential.as_bytes()).expect("credential");
|
||||
let admission_key = test_key(&oracle.ready.admission_key);
|
||||
let tls_key = test_key(&oracle.ready.client_key);
|
||||
let cancellation = Cancellation::new();
|
||||
let trigger = cancellation.clone();
|
||||
let canceller = thread::spawn(move || {
|
||||
thread::sleep(Duration::from_millis(25));
|
||||
trigger.cancel();
|
||||
});
|
||||
let result =
|
||||
tokio::runtime::Runtime::new()
|
||||
.expect("runtime")
|
||||
.block_on(connect_with_cancellation(
|
||||
&manifest,
|
||||
&credential,
|
||||
Signers::new(
|
||||
AdmissionSigner::new(signer(admission_key)),
|
||||
TlsEd25519Signer::new(move |message| {
|
||||
thread::sleep(Duration::from_millis(100));
|
||||
signer(Arc::clone(&tls_key))(message)
|
||||
}),
|
||||
),
|
||||
"2026-08-12T00:00:00Z",
|
||||
Duration::from_secs(5),
|
||||
&cancellation,
|
||||
));
|
||||
canceller.join().expect("canceller");
|
||||
assert_eq!(result.err(), Some(CoreError::Cancelled));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_cancellation_interrupts_network_wait() {
|
||||
let oracle = Oracle::start("");
|
||||
@@ -733,29 +773,6 @@ fn invalid_first_manifest_address_does_not_block_live_second_address() {
|
||||
fn retryable_stable_error_advances_to_live_manifest_address() {
|
||||
let oracle = Oracle::start("retryable");
|
||||
let manifest = ConnectionManifest::decode(oracle.ready.manifest.as_bytes()).expect("manifest");
|
||||
let credential =
|
||||
NativeTunnelCredential::decode(oracle.ready.credential.as_bytes()).expect("credential");
|
||||
let runtime = tokio::runtime::Runtime::new().expect("runtime");
|
||||
let session = runtime
|
||||
.block_on(connect(
|
||||
&manifest,
|
||||
&credential,
|
||||
recording_signers(
|
||||
test_key(&oracle.ready.admission_key),
|
||||
test_key(&oracle.ready.client_key),
|
||||
Arc::new(Mutex::new(Vec::new())),
|
||||
),
|
||||
"2026-08-12T00:00:00Z",
|
||||
Duration::from_secs(5),
|
||||
))
|
||||
.expect("retryable draining response advanced to live address");
|
||||
runtime.block_on(session.close());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn post_admission_retry_uses_a_fresh_signed_nonce() {
|
||||
let oracle = Oracle::start("post-retryable");
|
||||
let manifest = ConnectionManifest::decode(oracle.ready.manifest.as_bytes()).expect("manifest");
|
||||
let credential =
|
||||
NativeTunnelCredential::decode(oracle.ready.credential.as_bytes()).expect("credential");
|
||||
let transcripts = Arc::new(Mutex::new(Vec::new()));
|
||||
@@ -772,13 +789,48 @@ fn post_admission_retry_uses_a_fresh_signed_nonce() {
|
||||
"2026-08-12T00:00:00Z",
|
||||
Duration::from_secs(5),
|
||||
))
|
||||
.expect("post-admission retry used a fresh request");
|
||||
.expect("retryable draining response advanced to live address");
|
||||
runtime.block_on(session.close());
|
||||
let transcripts = transcripts.lock().expect("transcripts");
|
||||
assert_eq!(transcripts.len(), 2);
|
||||
assert_ne!(transcripts[0], transcripts[1]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn post_admission_failure_is_terminal_and_does_not_mutate_twice() {
|
||||
for mode in [
|
||||
"post-retryable",
|
||||
"provider-start-lost-response",
|
||||
"cleanup-release-failure",
|
||||
] {
|
||||
let oracle = Oracle::start(mode);
|
||||
let manifest =
|
||||
ConnectionManifest::decode(oracle.ready.manifest.as_bytes()).expect("manifest");
|
||||
let credential =
|
||||
NativeTunnelCredential::decode(oracle.ready.credential.as_bytes()).expect("credential");
|
||||
let transcripts = Arc::new(Mutex::new(Vec::new()));
|
||||
let runtime = tokio::runtime::Runtime::new().expect("runtime");
|
||||
let result = runtime.block_on(connect(
|
||||
&manifest,
|
||||
&credential,
|
||||
recording_signers(
|
||||
test_key(&oracle.ready.admission_key),
|
||||
test_key(&oracle.ready.client_key),
|
||||
Arc::clone(&transcripts),
|
||||
),
|
||||
"2026-08-12T00:00:00Z",
|
||||
Duration::from_secs(5),
|
||||
));
|
||||
assert!(result.is_err(), "{mode} was retried to success");
|
||||
let transcripts = transcripts.lock().expect("transcripts");
|
||||
assert_eq!(
|
||||
transcripts.len(),
|
||||
1,
|
||||
"{mode} attempted admission mutation twice"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repeated_production_gateway_admissions_remain_bounded() {
|
||||
let runtime = tokio::runtime::Runtime::new().expect("runtime");
|
||||
|
||||
@@ -145,6 +145,14 @@ fn manifest_public_identity_requires_dns_sni_not_ip_or_uuid() {
|
||||
"non-DNS SNI accepted: {invalid_identity}"
|
||||
);
|
||||
}
|
||||
|
||||
let identity_equals_dns_shaped_gateway_id = String::from_utf8(valid_manifest().to_vec())
|
||||
.expect("fixture is UTF-8")
|
||||
.replace("\"id\":\"gateway\"", "\"id\":\"gateway.test\"");
|
||||
assert!(
|
||||
ConnectionManifest::decode(identity_equals_dns_shaped_gateway_id.as_bytes()).is_err(),
|
||||
"public SNI identity matched the logical gateway id"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user