Skip to content

Commit fd18072

Browse files
committed
fix(ci): recover relay opens and isolate VM proxy ports
Signed-off-by: Drew Newberry <anewberry@nvidia.com>
1 parent 11f1fe5 commit fd18072

3 files changed

Lines changed: 73 additions & 13 deletions

File tree

‎architecture/gateway.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,8 @@ the serving gateway retries ownership lookup until the normal relay wait
405405
deadline. Each retry re-reads the owner record, so a supervisor reconnect or
406406
heartbeat can surface a new owner; if no fresh reachable owner appears before
407407
the deadline, the client operation fails rather than electing an owner itself.
408+
The owning gateway replays unclaimed relay opens when a supervisor reconnects,
409+
including when the previous session ended before the new one registered.
408410
Provider-readiness reports, endpoint-status reports, and provider-status reads
409411
also follow the durable owner record through unary peer RPCs. The owning replica
410412
validates the current supervisor session and keeps the in-memory evidence; a

‎crates/openshell-server/src/supervisor_session.rs‎

Lines changed: 53 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -933,7 +933,7 @@ impl SupervisorSessionRegistry {
933933
payload: Some(gateway_message::Payload::RelayOpen(relay_open)),
934934
};
935935
if tx.send(msg).await.is_err() {
936-
warn!(sandbox_id = %sandbox_id, channel_id = %channel_id, "supervisor session: failed to replay pending relay to superseding session");
936+
warn!(sandbox_id = %sandbox_id, channel_id = %channel_id, "supervisor session: failed to replay pending relay to new session");
937937
break;
938938
}
939939
}
@@ -1960,12 +1960,12 @@ async fn establish_supervisor_session(
19601960
}
19611961
state.telemetry.sandbox_session_connected(&sandbox_id);
19621962

1963-
if superseded {
1964-
state
1965-
.supervisor_sessions
1966-
.replay_pending_relays(&sandbox_id, &tx)
1967-
.await;
1968-
}
1963+
// The previous session may already have been removed before this one
1964+
// registers. Pending relay opens still need to reach the new session.
1965+
state
1966+
.supervisor_sessions
1967+
.replay_pending_relays(&sandbox_id, &tx)
1968+
.await;
19691969

19701970
// Step 4: Spawn the session loop that reads inbound messages.
19711971
let state_clone = Arc::clone(&state);
@@ -2971,6 +2971,52 @@ mod tests {
29712971
}
29722972
}
29732973

2974+
#[tokio::test]
2975+
async fn replay_pending_relays_reissues_open_after_disconnected_session() {
2976+
let registry = SupervisorSessionRegistry::new();
2977+
let (tx_old, mut rx_old) = mpsc::channel::<GatewayMessage>(4);
2978+
let (tx_new, mut rx_new) = mpsc::channel::<GatewayMessage>(4);
2979+
2980+
registry.register(
2981+
"sbx".to_string(),
2982+
"s-old".to_string(),
2983+
tx_old,
2984+
make_shutdown(),
2985+
);
2986+
let (channel_id, _relay_rx) = registry
2987+
.open_relay("sbx", Duration::from_secs(1))
2988+
.await
2989+
.expect("open_relay should succeed");
2990+
rx_old
2991+
.recv()
2992+
.await
2993+
.expect("old session should receive RelayOpen");
2994+
2995+
assert_eq!(registry.remove_if_current("sbx", "s-old"), Some(false));
2996+
let superseded = registry.register(
2997+
"sbx".to_string(),
2998+
"s-new".to_string(),
2999+
tx_new,
3000+
make_shutdown(),
3001+
);
3002+
assert!(!superseded, "old session was removed before reconnect");
3003+
3004+
registry
3005+
.replay_pending_relays("sbx", &registry.lookup_session("sbx").unwrap())
3006+
.await;
3007+
3008+
let replayed = rx_new
3009+
.recv()
3010+
.await
3011+
.expect("new session should receive RelayOpen");
3012+
match replayed.payload {
3013+
Some(gateway_message::Payload::RelayOpen(open)) => {
3014+
assert_eq!(open.channel_id, channel_id);
3015+
}
3016+
other => panic!("expected RelayOpen, got {other:?}"),
3017+
}
3018+
}
3019+
29743020
#[tokio::test]
29753021
async fn require_persisted_sandbox_rejects_missing_sandbox() {
29763022
let store = test_store().await;

‎e2e/rust/tests/vm_corporate_proxy.rs‎

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ const BYPASS_MARKER: &str = "vm-corp-proxy-e2e-bypass-upstream";
5454
const READY_MARKER: &str = "vm-corp-proxy-e2e-workload-done";
5555

5656
/// Ports the fixtures bind and the guest addresses them by.
57+
#[derive(Debug)]
5758
struct FixturePorts {
5859
proxy: u16,
5960
allowed: u16,
@@ -63,11 +64,21 @@ struct FixturePorts {
6364

6465
impl FixturePorts {
6566
fn pick() -> Self {
67+
// find_free_port releases its listener before returning, so repeated
68+
// calls can return the same port and accidentally authorize the
69+
// "denied" fixture through an allowed endpoint.
70+
let mut used = std::collections::HashSet::new();
71+
let mut next = || loop {
72+
let port = find_free_port();
73+
if used.insert(port) {
74+
break port;
75+
}
76+
};
6677
Self {
67-
proxy: find_free_port(),
68-
allowed: find_free_port(),
69-
denied: find_free_port(),
70-
bypass: find_free_port(),
78+
proxy: next(),
79+
allowed: next(),
80+
denied: next(),
81+
bypass: next(),
7182
}
7283
}
7384
}
@@ -424,7 +435,6 @@ filesystem_policy:
424435
read_only:
425436
- /usr
426437
- /lib
427-
- /proc
428438
- /dev/urandom
429439
- /app
430440
- /etc
@@ -433,6 +443,7 @@ filesystem_policy:
433443
- /sandbox
434444
- /tmp
435445
- /dev/null
446+
- /proc
436447
437448
landlock:
438449
compatibility: best_effort
@@ -822,7 +833,8 @@ async fn vm_corporate_proxy_trusts_ca_bundle_for_https_proxy() {
822833
);
823834
assert!(
824835
!proxy_logs.contains(&format!(":{}", ports.denied)),
825-
"policy-denied destination must never reach the https proxy:\n{proxy_logs}"
836+
"policy-denied destination must never reach the https proxy; ports={ports:?}, workload output:\n{}\nproxy logs:\n{proxy_logs}",
837+
sandbox.create_output,
826838
);
827839

828840
sandbox.cleanup().await;

0 commit comments

Comments
 (0)