@@ -999,10 +999,10 @@ impl DaemonState {
999999 let listener_cancel = CancellationToken :: new ( ) ;
10001000 let lease = DialListenerLease :: new ( self . active_dial_listeners . clone ( ) ) ;
10011001
1002- // Built-in exec remains a one-shot raw framed stream. Built-in shell
1003- // rides the resumable tunnel path so its PTY outlives transient iroh
1004- // attaches; local reconnect notices are encoded as shell status frames .
1005- let listener_task = if alpn == exec:: EXEC_ALPN {
1002+ // Built-in exec and legacy shell/0 remain one-shot raw framed streams.
1003+ // Resumable shell/1 negotiates its own tunnel path and falls back to
1004+ // shell/0 when the peer does not advertise the new ALPN .
1005+ let listener_task = if alpn == exec:: EXEC_ALPN || alpn == shell :: SHELL_ALPN {
10061006 tokio:: spawn ( run_raw_dial_socket (
10071007 listener,
10081008 self . endpoint_rx ( ) ,
@@ -1014,8 +1014,21 @@ impl DaemonState {
10141014 self . dial_slots . clone ( ) ,
10151015 lease,
10161016 ) )
1017+ } else if alpn == shell:: RESUMABLE_SHELL_ALPN {
1018+ tokio:: spawn ( run_shell_dial_socket (
1019+ listener,
1020+ self . endpoint_rx ( ) ,
1021+ self . home . clone ( ) ,
1022+ peer. to_string ( ) ,
1023+ peer_addr. clone ( ) ,
1024+ listener_cancel. clone ( ) ,
1025+ self . cancel . clone ( ) ,
1026+ self . tunnel_drop_rx ( ) ,
1027+ self . dial_failures . clone ( ) ,
1028+ self . dial_slots . clone ( ) ,
1029+ lease,
1030+ ) )
10171031 } else {
1018- let notices = ( alpn == shell:: SHELL_ALPN ) . then ( shell_client_notices) ;
10191032 tokio:: spawn ( run_dial_socket (
10201033 listener,
10211034 self . endpoint_rx ( ) ,
@@ -1028,7 +1041,7 @@ impl DaemonState {
10281041 self . dial_failures . clone ( ) ,
10291042 self . dial_slots . clone ( ) ,
10301043 lease,
1031- notices ,
1044+ None ,
10321045 ) )
10331046 } ;
10341047 sockets. insert (
@@ -2437,7 +2450,7 @@ async fn process_control_request(
24372450 . dial_alpn (
24382451 & peer,
24392452 shell:: SHELL_PROTOCOL ,
2440- shell:: SHELL_ALPN . to_vec ( ) ,
2453+ shell:: RESUMABLE_SHELL_ALPN . to_vec ( ) ,
24412454 false ,
24422455 )
24432456 . await ?;
@@ -2584,8 +2597,14 @@ async fn process_incoming_iroh(incoming: Incoming, state: Arc<DaemonState>) -> R
25842597 }
25852598 if alpn == shell:: SHELL_ALPN {
25862599 let connection = accepting. await ?;
2587- log_connection_paths ( "builtin_shell_accept" , & connection) ;
2588- handle_builtin_shell ( connection, state) . await ?;
2600+ log_connection_paths ( "builtin_legacy_shell_accept" , & connection) ;
2601+ handle_builtin_legacy_shell ( connection, state) . await ?;
2602+ return Ok ( ( ) ) ;
2603+ }
2604+ if alpn == shell:: RESUMABLE_SHELL_ALPN {
2605+ let connection = accepting. await ?;
2606+ log_connection_paths ( "builtin_resumable_shell_accept" , & connection) ;
2607+ handle_builtin_resumable_shell ( connection, state) . await ?;
25892608 return Ok ( ( ) ) ;
25902609 }
25912610 if alpn == exec:: EXEC_ALPN {
@@ -2639,7 +2658,26 @@ async fn handle_builtin_echo(connection: Connection, state: Arc<DaemonState>) ->
26392658 Ok ( ( ) )
26402659}
26412660
2642- async fn handle_builtin_shell ( connection : Connection , state : Arc < DaemonState > ) -> Result < ( ) > {
2661+ async fn handle_builtin_legacy_shell (
2662+ connection : Connection ,
2663+ state : Arc < DaemonState > ,
2664+ ) -> Result < ( ) > {
2665+ let peer = connection. remote_id ( ) . to_string ( ) ;
2666+ let ( mut send, mut recv) = connection. accept_bi ( ) . await ?;
2667+ if state. allow_shell {
2668+ shell:: serve_shell_session ( & mut recv, & mut send, & peer) . await ?;
2669+ } else {
2670+ shell:: serve_shell_disabled ( & mut send) . await ?;
2671+ }
2672+ send. finish ( ) ?;
2673+ connection. closed ( ) . await ;
2674+ Ok ( ( ) )
2675+ }
2676+
2677+ async fn handle_builtin_resumable_shell (
2678+ connection : Connection ,
2679+ state : Arc < DaemonState > ,
2680+ ) -> Result < ( ) > {
26432681 let peer_id = connection. remote_id ( ) ;
26442682 let ( send, recv) = connection. accept_bi ( ) . await ?;
26452683 tunnel:: serve_connection (
@@ -2680,10 +2718,12 @@ async fn handle_sync(connection: Connection, state: Arc<DaemonState>) -> Result<
26802718 let ( send, recv) = connection. accept_bi ( ) . await ?;
26812719 let stream = tokio:: io:: join ( recv, send) ;
26822720 let resolver_engine = engine. clone ( ) ;
2683- let outcome = sync:: wire:: run_server ( stream, move |name| {
2721+ let outcome = sync:: wire:: run_server ( stream, move |name, remote_manifest | {
26842722 let engine = resolver_engine. clone ( ) ;
26852723 async move {
2686- let prepared = engine. prepare_inbound ( & name) . await ?;
2724+ let prepared = engine
2725+ . prepare_inbound_for_manifest ( & name, & remote_manifest)
2726+ . await ?;
26872727 Ok ( prepared. map ( |prepared| ( prepared. node ( ) , prepared) ) )
26882728 }
26892729 } )
@@ -2706,9 +2746,10 @@ async fn handle_sync(connection: Connection, state: Arc<DaemonState>) -> Result<
27062746}
27072747
27082748fn accepted_alpns ( exposures : & HashMap < Vec < u8 > , Exposure > ) -> Vec < Vec < u8 > > {
2709- let mut alpns = Vec :: with_capacity ( exposures. len ( ) + 4 ) ;
2749+ let mut alpns = Vec :: with_capacity ( exposures. len ( ) + 5 ) ;
27102750 alpns. push ( BUILTIN_ECHO_ALPN . to_vec ( ) ) ;
27112751 alpns. push ( shell:: SHELL_ALPN . to_vec ( ) ) ;
2752+ alpns. push ( shell:: RESUMABLE_SHELL_ALPN . to_vec ( ) ) ;
27122753 alpns. push ( exec:: EXEC_ALPN . to_vec ( ) ) ;
27132754 alpns. push ( SYNC_ALPN . to_vec ( ) ) ;
27142755 alpns. extend ( exposures. keys ( ) . cloned ( ) ) ;
@@ -2718,6 +2759,7 @@ fn accepted_alpns(exposures: &HashMap<Vec<u8>, Exposure>) -> Vec<Vec<u8>> {
27182759fn matches_reserved_alpn ( alpn : & [ u8 ] ) -> bool {
27192760 alpn == BUILTIN_ECHO_ALPN
27202761 || alpn == shell:: SHELL_ALPN
2762+ || alpn == shell:: RESUMABLE_SHELL_ALPN
27212763 || alpn == exec:: EXEC_ALPN
27222764 || alpn == SYNC_ALPN
27232765}
@@ -3133,6 +3175,140 @@ async fn run_dial_socket(
31333175 }
31343176}
31353177
3178+ async fn run_shell_dial_socket (
3179+ listener : UnixListener ,
3180+ endpoint_rx : watch:: Receiver < CurrentEndpoint > ,
3181+ home : FabricHome ,
3182+ peer : String ,
3183+ peer_addr : EndpointAddr ,
3184+ listener_cancel : CancellationToken ,
3185+ daemon_cancel : CancellationToken ,
3186+ drop_rx : watch:: Receiver < u64 > ,
3187+ dial_failures : Arc < FailureBackoff > ,
3188+ dial_slots : Arc < Semaphore > ,
3189+ _lease : DialListenerLease ,
3190+ ) {
3191+ loop {
3192+ tokio:: select! {
3193+ biased;
3194+ _ = listener_cancel. cancelled( ) => break ,
3195+ _ = daemon_cancel. cancelled( ) => break ,
3196+ accepted = listener. accept( ) => {
3197+ let Ok ( ( local, _) ) = accepted else {
3198+ break ;
3199+ } ;
3200+ let permit = tokio:: select! {
3201+ biased;
3202+ _ = listener_cancel. cancelled( ) => break ,
3203+ _ = daemon_cancel. cancelled( ) => break ,
3204+ permit = dial_slots. clone( ) . acquire_owned( ) => {
3205+ let Ok ( permit) = permit else {
3206+ break ;
3207+ } ;
3208+ permit
3209+ }
3210+ } ;
3211+ let endpoint_rx = endpoint_rx. clone( ) ;
3212+ let home = home. clone( ) ;
3213+ let peer = peer. clone( ) ;
3214+ let peer_addr = peer_addr. clone( ) ;
3215+ let cancel = daemon_cancel. clone( ) ;
3216+ let drop_rx = drop_rx. clone( ) ;
3217+ let dial_failures = dial_failures. clone( ) ;
3218+ tokio:: spawn( async move {
3219+ let _permit = permit;
3220+ if !dial_failures. wait( & cancel) . await {
3221+ return ;
3222+ }
3223+ match handle_shell_dial_socket_connection(
3224+ local,
3225+ endpoint_rx,
3226+ home,
3227+ peer,
3228+ peer_addr,
3229+ cancel,
3230+ drop_rx,
3231+ )
3232+ . await
3233+ {
3234+ Ok ( ( ) ) => dial_failures. record_success( ) . await ,
3235+ Err ( error) => {
3236+ dial_failures
3237+ . record_failure( "shell dial socket connection failed" , & error)
3238+ . await ;
3239+ }
3240+ }
3241+ } ) ;
3242+ }
3243+ }
3244+ }
3245+ }
3246+
3247+ async fn handle_shell_dial_socket_connection (
3248+ local : UnixStream ,
3249+ endpoint_rx : watch:: Receiver < CurrentEndpoint > ,
3250+ home : FabricHome ,
3251+ peer : String ,
3252+ peer_addr : EndpointAddr ,
3253+ cancel : CancellationToken ,
3254+ drop_rx : watch:: Receiver < u64 > ,
3255+ ) -> Result < ( ) > {
3256+ let endpoint = endpoint_rx. borrow ( ) . endpoint . clone ( ) ;
3257+ match endpoint
3258+ . connect ( peer_addr. clone ( ) , shell:: RESUMABLE_SHELL_ALPN )
3259+ . await
3260+ {
3261+ Ok ( connection) => {
3262+ tunnel:: run_client_connection_with_initial (
3263+ local,
3264+ endpoint_rx,
3265+ home,
3266+ peer,
3267+ shell:: RESUMABLE_SHELL_ALPN . to_vec ( ) ,
3268+ cancel,
3269+ drop_rx,
3270+ Some ( shell_client_notices ( ) ) ,
3271+ connection,
3272+ )
3273+ . await
3274+ }
3275+ Err ( error) => {
3276+ let error = anyhow:: Error :: new ( error) ;
3277+ if !shell_resumable_alpn_unsupported ( & error) {
3278+ // A transient initial failure should retain the resumable
3279+ // attach loop and its backoff. Only a definitive ALPN mismatch
3280+ // is allowed to downgrade the session to legacy shell/0.
3281+ return tunnel:: run_client_connection (
3282+ local,
3283+ endpoint_rx,
3284+ home,
3285+ peer,
3286+ shell:: RESUMABLE_SHELL_ALPN . to_vec ( ) ,
3287+ cancel,
3288+ drop_rx,
3289+ Some ( shell_client_notices ( ) ) ,
3290+ )
3291+ . await ;
3292+ }
3293+ handle_raw_dial_socket_connection (
3294+ local,
3295+ endpoint,
3296+ peer_addr,
3297+ shell:: SHELL_ALPN . to_vec ( ) ,
3298+ )
3299+ . await
3300+ }
3301+ }
3302+ }
3303+
3304+ fn shell_resumable_alpn_unsupported ( error : & anyhow:: Error ) -> bool {
3305+ error. chain ( ) . any ( |cause| {
3306+ let message = cause. to_string ( ) . to_ascii_lowercase ( ) ;
3307+ message. contains ( "peer doesn't support any known protocol" )
3308+ || message. contains ( "no application protocol" )
3309+ } )
3310+ }
3311+
31363312fn shell_client_notices ( ) -> tunnel:: ClientConnectionNotices {
31373313 tunnel:: ClientConnectionNotices :: new ( |event| {
31383314 let encoded = match event {
@@ -3757,9 +3933,9 @@ mod tests {
37573933 let state = node. state ( ) ;
37583934 let cases = [
37593935 ( "peer-a" , exec:: EXEC_PROTOCOL , exec:: EXEC_ALPN ) ,
3760- ( "peer-a" , shell:: SHELL_PROTOCOL , shell:: SHELL_ALPN ) ,
3936+ ( "peer-a" , shell:: SHELL_PROTOCOL , shell:: RESUMABLE_SHELL_ALPN ) ,
37613937 ( "peer-b" , exec:: EXEC_PROTOCOL , exec:: EXEC_ALPN ) ,
3762- ( "peer-b" , shell:: SHELL_PROTOCOL , shell:: SHELL_ALPN ) ,
3938+ ( "peer-b" , shell:: SHELL_PROTOCOL , shell:: RESUMABLE_SHELL_ALPN ) ,
37633939 ] ;
37643940
37653941 // More replacements than the production macOS soft FD limit. The old
0 commit comments