Files
Adam Chester 63d4596b3f initial commit
2026-05-05 23:20:17 +01:00

117 lines
4.2 KiB
Diff

Subject: [PATCH] client: add Handle::channel_open_forwarded_tcpip
Adds the ability for an SSH *client* to initiate a `forwarded-tcpip`
channel. The existing upstream API only lets clients open `session`,
`x11`, `direct-tcpip`, and `direct-streamlocal` channels — sufficient
for RFC 4254 conformance, where `forwarded-tcpip` is a server-initiated
channel sent in response to a prior `tcpip-forward` request.
Microsoft's Dev Tunnels protocol inverts that convention: the tunnel
*client* opens `forwarded-tcpip` directly to reach a known remote port
on the tunnel host. This patch makes that possible without touching
any private state — it mirrors exactly the shape of the existing
`channel_open_direct_tcpip` plumbing.
Applies against microsoft/vscode-russh `main` (commit fd4f608), which is
byte-identical to Eugeny/russh v0.37.1 in the files this patch touches.
---
russh/src/client/mod.rs | 37 +++++++++++++++++++++++++++++++++++++
russh/src/client/session.rs | 15 +++++++++++++++
2 files changed, 52 insertions(+)
diff --git a/russh/src/client/mod.rs b/russh/src/client/mod.rs
--- a/russh/src/client/mod.rs
+++ b/russh/src/client/mod.rs
@@ -178,6 +178,13 @@ pub enum Msg {
originator_port: u32,
sender: UnboundedSender<ChannelMsg>,
},
+ ChannelOpenForwardedTcpIp {
+ connected_address: String,
+ connected_port: u32,
+ originator_address: String,
+ originator_port: u32,
+ sender: UnboundedSender<ChannelMsg>,
+ },
ChannelOpenDirectStreamLocal {
socket_path: String,
sender: UnboundedSender<ChannelMsg>,
@@ -511,6 +518,28 @@ impl<H: Handler> Handle<H> {
self.wait_channel_confirmation(receiver).await
}
+ /// Open a TCP/IP forwarding channel for a remotely forwarded port.
+ pub async fn channel_open_forwarded_tcpip<A: Into<String>, B: Into<String>>(
+ &self,
+ connected_address: A,
+ connected_port: u32,
+ originator_address: B,
+ originator_port: u32,
+ ) -> Result<Channel<Msg>, crate::Error> {
+ let (sender, receiver) = unbounded_channel();
+ self.sender
+ .send(Msg::ChannelOpenForwardedTcpIp {
+ connected_address: connected_address.into(),
+ connected_port,
+ originator_address: originator_address.into(),
+ originator_port,
+ sender,
+ })
+ .await
+ .map_err(|_| crate::Error::SendError)?;
+ self.wait_channel_confirmation(receiver).await
+ }
+
pub async fn channel_open_direct_streamlocal<S: Into<String>>(
&self,
socket_path: S,
@@ -890,6 +919,21 @@ impl Session {
)?;
self.channels.insert(id, sender);
}
+ Msg::ChannelOpenForwardedTcpIp {
+ connected_address,
+ connected_port,
+ originator_address,
+ originator_port,
+ sender,
+ } => {
+ let id = self.channel_open_forwarded_tcpip(
+ &connected_address,
+ connected_port,
+ &originator_address,
+ originator_port,
+ )?;
+ self.channels.insert(id, sender);
+ }
Msg::ChannelOpenDirectStreamLocal {
socket_path,
sender,
diff --git a/russh/src/client/session.rs b/russh/src/client/session.rs
--- a/russh/src/client/session.rs
+++ b/russh/src/client/session.rs
@@ -79,6 +79,21 @@ impl Session {
})
}
+ pub fn channel_open_forwarded_tcpip(
+ &mut self,
+ connected_address: &str,
+ connected_port: u32,
+ originator_address: &str,
+ originator_port: u32,
+ ) -> Result<ChannelId, crate::Error> {
+ self.channel_open_generic(b"forwarded-tcpip", |write| {
+ write.extend_ssh_string(connected_address.as_bytes());
+ write.push_u32_be(connected_port); // sender channel id.
+ write.extend_ssh_string(originator_address.as_bytes());
+ write.push_u32_be(originator_port); // sender channel id.
+ })
+ }
+
pub fn channel_open_direct_streamlocal(
&mut self,
socket_path: &str