(
&mut self,
channel: russh::Channel<russh::server::Msg>,
host_to_connect: &str,
port_to_connect: u32,
_originator_address: &str,
_originator_port: u32,
| 289 | } |
| 290 | |
| 291 | async fn channel_open_direct_tcpip( |
| 292 | &mut self, |
| 293 | channel: russh::Channel<russh::server::Msg>, |
| 294 | host_to_connect: &str, |
| 295 | port_to_connect: u32, |
| 296 | _originator_address: &str, |
| 297 | _originator_port: u32, |
| 298 | _session: &mut Session, |
| 299 | ) -> Result<bool, Self::Error> { |
| 300 | // Validate port range before truncating u32 -> u16. The SSH protocol |
| 301 | // uses u32 for ports, but valid TCP ports are 0-65535. Without this |
| 302 | // check, port 65537 truncates to port 1 (privileged). |
| 303 | if port_to_connect > u32::from(u16::MAX) { |
| 304 | ocsf_emit!(SshActivityBuilder::new(openshell_ocsf::ctx::ctx()) |
| 305 | .activity(ActivityId::Refuse) |
| 306 | .action(ActionId::Denied) |
| 307 | .disposition(DispositionId::Blocked) |
| 308 | .severity(SeverityId::Medium) |
| 309 | .message(format!( |
| 310 | "direct-tcpip rejected: port {port_to_connect} exceeds valid TCP range for host {host_to_connect}" |
| 311 | )) |
| 312 | .build()); |
| 313 | return Ok(false); |
| 314 | } |
| 315 | |
| 316 | // Only allow forwarding to loopback destinations to prevent the |
| 317 | // sandbox SSH server from being used as a generic proxy. |
| 318 | if !is_loopback_host(host_to_connect) { |
| 319 | ocsf_emit!(SshActivityBuilder::new(openshell_ocsf::ctx::ctx()) |
| 320 | .activity(ActivityId::Refuse) |
| 321 | .action(ActionId::Denied) |
| 322 | .disposition(DispositionId::Blocked) |
| 323 | .severity(SeverityId::Medium) |
| 324 | .message(format!( |
| 325 | "direct-tcpip rejected: non-loopback destination {host_to_connect}:{port_to_connect}" |
| 326 | )) |
| 327 | .build()); |
| 328 | return Ok(false); |
| 329 | } |
| 330 | |
| 331 | let host = host_to_connect.to_string(); |
| 332 | // SSH protocol port is bounded by u32 but only u16 is meaningful; |
| 333 | // saturate as a guard for malformed clients. |
| 334 | let port = u16::try_from(port_to_connect).unwrap_or(u16::MAX); |
| 335 | let netns_fd = self.netns_fd; |
| 336 | |
| 337 | tokio::spawn(async move { |
| 338 | let addr = format!("{host}:{port}"); |
| 339 | let tcp = match connect_in_netns(&addr, netns_fd).await { |
| 340 | Ok(stream) => stream, |
| 341 | Err(err) => { |
| 342 | ocsf_emit!( |
| 343 | SshActivityBuilder::new(openshell_ocsf::ctx::ctx()) |
| 344 | .activity(ActivityId::Fail) |
| 345 | .severity(SeverityId::Low) |
| 346 | .status(StatusId::Failure) |
| 347 | .message(format!("direct-tcpip: failed to connect to {addr}: {err}")) |
| 348 | .build() |
nothing calls this directly
no test coverage detected