@@ -578,7 +578,15 @@ mod linux {
578578 } ) ;
579579 }
580580 Err ( error) if error. kind ( ) == io:: ErrorKind :: Interrupted => { }
581- Err ( error) if error. kind ( ) == io:: ErrorKind :: PermissionDenied => { }
581+ // A refused or already-dead peer must not stop the listener.
582+ Err ( error)
583+ if matches ! (
584+ error. kind( ) ,
585+ io:: ErrorKind :: PermissionDenied
586+ | io:: ErrorKind :: NotConnected
587+ | io:: ErrorKind :: ConnectionReset
588+ | io:: ErrorKind :: ConnectionAborted
589+ ) => { }
582590 Err ( error) if error. kind ( ) == io:: ErrorKind :: WouldBlock => {
583591 std:: thread:: sleep ( Duration :: from_millis ( 10 ) ) ;
584592 }
@@ -3277,6 +3285,7 @@ mod linux {
32773285 Tcp {
32783286 listener : std:: net:: TcpListener ,
32793287 server_config : Arc < rustls:: ServerConfig > ,
3288+ reject_local_peers : bool ,
32803289 } ,
32813290 }
32823291
@@ -3305,12 +3314,13 @@ mod linux {
33053314 } )
33063315 }
33073316 BoundaryListenerConfig :: TlsTcp { address, tls } => {
3308- let listener = Self :: bind_tcp ( * address) ?;
3317+ let ( listener, reject_local_peers ) = Self :: bind_tcp ( * address) ?;
33093318 listener. set_nonblocking ( true ) ?;
33103319 let server_config = Arc :: new ( load_tls_server_config ( tls) ?) ;
33113320 Ok ( Self :: Tcp {
33123321 listener,
33133322 server_config,
3323+ reject_local_peers,
33143324 } )
33153325 }
33163326 }
@@ -3320,22 +3330,30 @@ mod linux {
33203330 /// before it listens so workload sockets cannot reach it through
33213331 /// loopback or the pod's own address. Configuration rejects loopback
33223332 /// addresses; tests bind them without the filter.
3323- fn bind_tcp ( address : std:: net:: SocketAddr ) -> io:: Result < std:: net:: TcpListener > {
3333+ ///
3334+ /// The filter is a standing guarantee where the kernel allows it, but
3335+ /// `accept` repeats the check for every non-loopback listener so a
3336+ /// sandbox behaves the same whatever kernel its node runs. Returns
3337+ /// whether `accept` must apply it.
3338+ fn bind_tcp ( address : std:: net:: SocketAddr ) -> io:: Result < ( std:: net:: TcpListener , bool ) > {
33243339 let socket = socket2:: Socket :: new (
33253340 socket2:: Domain :: for_address ( address) ,
33263341 socket2:: Type :: STREAM ,
33273342 Some ( socket2:: Protocol :: TCP ) ,
33283343 ) ?;
33293344 socket. set_cloexec ( true ) ?;
33303345 socket. set_reuse_address ( true ) ?;
3331- if !address. ip ( ) . is_loopback ( ) {
3332- openshell_isolation_interface:: linux:: socket_confinement:: reject_loopback_ingress (
3346+ let reject_local_peers = !address. ip ( ) . is_loopback ( ) ;
3347+ if reject_local_peers
3348+ && !openshell_isolation_interface:: linux:: socket_confinement:: try_reject_loopback_ingress (
33333349 & socket,
3334- ) ?;
3350+ ) ?
3351+ {
3352+ report_unavailable_ingress_filter ( ) ;
33353353 }
33363354 socket. bind ( & address. into ( ) ) ?;
33373355 socket. listen ( 128 ) ?;
3338- Ok ( socket. into ( ) )
3356+ Ok ( ( socket. into ( ) , reject_local_peers ) )
33393357 }
33403358
33413359 fn bind_vsock ( port : u32 ) -> io:: Result < OwnedFd > {
@@ -3428,8 +3446,12 @@ mod linux {
34283446 Self :: Tcp {
34293447 listener,
34303448 server_config,
3449+ reject_local_peers,
34313450 } => {
3432- let ( stream, _) = listener. accept ( ) ?;
3451+ let ( stream, peer) = listener. accept ( ) ?;
3452+ if * reject_local_peers {
3453+ reject_workload_tcp_peer ( & stream, peer) ?;
3454+ }
34333455 if let Err ( error) = stream. set_nodelay ( true ) {
34343456 tracing:: debug!( %error, "Failed to set boundary TCP_NODELAY" ) ;
34353457 }
@@ -3442,6 +3464,42 @@ mod linux {
34423464 }
34433465 }
34443466
3467+ // Traffic the workload can reach the listener with arrives over `lo`, so one
3468+ // end is loopback or the two ends share an address. A supervisor runs in
3469+ // another pod and presents a different address. The peer comes from `accept`
3470+ // because `getpeername` fails once that peer has reset.
3471+ fn reject_workload_tcp_peer (
3472+ stream : & std:: net:: TcpStream ,
3473+ peer : std:: net:: SocketAddr ,
3474+ ) -> io:: Result < ( ) > {
3475+ let denied = || io:: Error :: from_raw_os_error ( libc:: EACCES ) ;
3476+ let peer = peer. ip ( ) . to_canonical ( ) ;
3477+ let local = stream. local_addr ( ) . map_err ( |_| denied ( ) ) ?. ip ( ) . to_canonical ( ) ;
3478+ if peer. is_loopback ( ) || local. is_loopback ( ) || peer == local {
3479+ return Err ( denied ( ) ) ;
3480+ }
3481+ Ok ( ( ) )
3482+ }
3483+
3484+ fn report_unavailable_ingress_filter ( ) {
3485+ tracing:: warn!(
3486+ "Kernel denied SO_ATTACH_FILTER on the boundary control listener; rejecting \
3487+ same-host control peers on accept instead"
3488+ ) ;
3489+ openshell_ocsf:: ocsf_emit!(
3490+ openshell_ocsf:: ConfigStateChangeBuilder :: new( openshell_ocsf:: ctx:: ctx( ) )
3491+ . severity( openshell_ocsf:: SeverityId :: Low )
3492+ . status( openshell_ocsf:: StatusId :: Failure )
3493+ . state( openshell_ocsf:: StateId :: Disabled , "unavailable" )
3494+ . message(
3495+ "Boundary loopback-ingress filter unavailable (SO_ATTACH_FILTER denied \
3496+ with EPERM); control listener rejects same-host peers on accept"
3497+ . to_string( )
3498+ )
3499+ . build( )
3500+ ) ;
3501+ }
3502+
34453503 fn reject_workload_unix_peer ( stream : & std:: os:: unix:: net:: UnixStream ) -> io:: Result < ( ) > {
34463504 let mut credentials = libc:: ucred {
34473505 pid : 0 ,
@@ -5150,22 +5208,94 @@ mod linux {
51505208 . tcp_local_addr ( )
51515209 . expect ( "TLS listener address" )
51525210 . port ( ) ;
5153- // Loopback and the host's own address both arrive on `lo`; the
5154- // dropped SYN never completes a handshake .
5155- let result = std:: net:: TcpStream :: connect_timeout (
5211+ // The filter drops the SYN; without it the handshake completes and
5212+ // `accept` refuses the peer instead .
5213+ let _client = std:: net:: TcpStream :: connect_timeout (
51565214 & std:: net:: SocketAddr :: from ( ( [ 127 , 0 , 0 , 1 ] , port) ) ,
51575215 Duration :: from_millis ( 300 ) ,
51585216 ) ;
5159- assert ! (
5160- result. is_err( ) ,
5161- "loopback client reached the control listener"
5162- ) ;
51635217 assert ! ( matches!(
51645218 listener. accept( ) . map( |_| ( ) ) ,
5165- Err ( error) if error. kind( ) == io:: ErrorKind :: WouldBlock
5219+ Err ( error) if matches!(
5220+ error. kind( ) ,
5221+ io:: ErrorKind :: WouldBlock | io:: ErrorKind :: PermissionDenied
5222+ )
51665223 ) ) ;
51675224 }
51685225
5226+ /// A control listener in accept-time mode, bound to loopback so a test
5227+ /// client can actually reach it.
5228+ fn accept_time_control_listener (
5229+ directory : & Path ,
5230+ name : & str ,
5231+ ) -> ( ControlListener , std:: net:: SocketAddr ) {
5232+ let ( server_tls, _client_tls) = stage_test_tls ( directory, name) ;
5233+ let listener = std:: net:: TcpListener :: bind ( "127.0.0.1:0" ) . expect ( "bind listener" ) ;
5234+ listener. set_nonblocking ( true ) . expect ( "nonblocking listener" ) ;
5235+ let address = listener. local_addr ( ) . expect ( "listener address" ) ;
5236+ let server_config =
5237+ Arc :: new ( load_tls_server_config ( & server_tls) . expect ( "TLS server config" ) ) ;
5238+ (
5239+ ControlListener :: Tcp {
5240+ listener,
5241+ server_config,
5242+ reject_local_peers : true ,
5243+ } ,
5244+ address,
5245+ )
5246+ }
5247+
5248+ fn accept_pending ( listener : & ControlListener ) -> io:: Result < ( ) > {
5249+ let deadline = std:: time:: Instant :: now ( ) + Duration :: from_secs ( 2 ) ;
5250+ loop {
5251+ match listener. accept ( ) {
5252+ Err ( error)
5253+ if error. kind ( ) == io:: ErrorKind :: WouldBlock
5254+ && std:: time:: Instant :: now ( ) < deadline =>
5255+ {
5256+ std:: thread:: sleep ( Duration :: from_millis ( 5 ) ) ;
5257+ }
5258+ other => return other. map ( |_| ( ) ) ,
5259+ }
5260+ }
5261+ }
5262+
5263+ #[ test]
5264+ fn accept_refuses_a_same_host_control_peer_without_the_ingress_filter ( ) {
5265+ let directory = tempfile:: tempdir ( ) . expect ( "temporary directory" ) ;
5266+ let ( listener, address) = accept_time_control_listener ( directory. path ( ) , "fallback" ) ;
5267+ let _client = std:: net:: TcpStream :: connect ( address) . expect ( "connect loopback client" ) ;
5268+ let error = accept_pending ( & listener) . expect_err ( "loopback peer refused" ) ;
5269+ assert_eq ! ( error. kind( ) , io:: ErrorKind :: PermissionDenied ) ;
5270+ }
5271+
5272+ #[ test]
5273+ fn accept_survives_a_control_peer_that_resets_before_it_is_accepted ( ) {
5274+ let directory = tempfile:: tempdir ( ) . expect ( "temporary directory" ) ;
5275+ let ( listener, address) = accept_time_control_listener ( directory. path ( ) , "reset" ) ;
5276+ let client = socket2:: Socket :: new (
5277+ socket2:: Domain :: for_address ( address) ,
5278+ socket2:: Type :: STREAM ,
5279+ Some ( socket2:: Protocol :: TCP ) ,
5280+ )
5281+ . expect ( "client socket" ) ;
5282+ client. connect ( & address. into ( ) ) . expect ( "connect" ) ;
5283+ // Zero linger makes the close an RST while the connection is still
5284+ // queued, which is what makes `getpeername` fail with ENOTCONN.
5285+ client. set_linger ( Some ( Duration :: ZERO ) ) . expect ( "zero linger" ) ;
5286+ drop ( client) ;
5287+ let kind = accept_pending ( & listener)
5288+ . expect_err ( "reset peer refused" )
5289+ . kind ( ) ;
5290+ assert ! (
5291+ matches!(
5292+ kind,
5293+ io:: ErrorKind :: PermissionDenied | io:: ErrorKind :: WouldBlock
5294+ ) ,
5295+ "reset peer produced a listener-fatal error: {kind:?}"
5296+ ) ;
5297+ }
5298+
51695299 #[ test]
51705300 fn tls_listener_preserves_session_when_control_switches_to_async_streaming ( ) {
51715301 let directory = tempfile:: tempdir ( ) . expect ( "temporary directory" ) ;
0 commit comments