Skip to content

Commit b72646c

Browse files
authored
fix: accept full 256k channel packets (#666)
1 parent 43799a1 commit b72646c

2 files changed

Lines changed: 206 additions & 2 deletions

File tree

russh/src/cipher/mod.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -311,9 +311,17 @@ pub(crate) async fn read<R: AsyncRead + Unpin>(
311311
pub(crate) const PACKET_LENGTH_LEN: usize = 4;
312312

313313
const MINIMUM_PACKET_LEN: usize = 16;
314-
const MAXIMUM_PACKET_LEN: usize = 256 * 1024;
315-
314+
// Keep the transport limit aligned with the 256 KiB channel packet baseline.
315+
const MAXIMUM_PACKET_LEN_BASELINE: usize = 256 * 1024;
316+
const CHANNEL_DATA_PACKET_OVERHEAD: usize = 1 + 4 + 4;
317+
const CHANNEL_EXTENDED_DATA_PACKET_OVERHEAD: usize = CHANNEL_DATA_PACKET_OVERHEAD + 4;
316318
const PADDING_LENGTH_LEN: usize = 1;
319+
// SSH requires at least four bytes of padding; with 16-byte blocks, that means
320+
// a full-size channel packet can need up to 19 bytes of transport padding.
321+
const MAXIMUM_PADDING_LEN: usize = 19;
322+
const MAXIMUM_PACKET_LEN_HEADROOM: usize =
323+
PADDING_LENGTH_LEN + CHANNEL_EXTENDED_DATA_PACKET_OVERHEAD + MAXIMUM_PADDING_LEN;
324+
const MAXIMUM_PACKET_LEN: usize = MAXIMUM_PACKET_LEN_BASELINE + MAXIMUM_PACKET_LEN_HEADROOM;
317325

318326
#[cfg(feature = "_bench")]
319327
pub mod benchmark;
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
2+
3+
use std::borrow::Cow;
4+
use std::sync::Arc;
5+
6+
use russh::keys::PrivateKeyWithHashAlg;
7+
use russh::keys::ssh_key::rand_core::OsRng;
8+
use russh::*;
9+
use ssh_key::PrivateKey;
10+
use tokio::io::{AsyncWrite, AsyncWriteExt};
11+
12+
const MAX_CHANNEL_PACKET_SIZE: u32 = 256 * 1024;
13+
const TEST_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
14+
const STDERR_EXTENDED_DATA_TYPE: u32 = 1;
15+
16+
#[tokio::test]
17+
async fn test_aes256_gcm_allows_full_256k_channel_packet() {
18+
let _ = env_logger::try_init();
19+
20+
let client_key = PrivateKey::random(&mut OsRng, ssh_key::Algorithm::Ed25519).unwrap();
21+
22+
let mut server_config = server::Config::default();
23+
server_config.inactivity_timeout = None;
24+
server_config.auth_rejection_time = std::time::Duration::from_secs(3);
25+
server_config.maximum_packet_size = MAX_CHANNEL_PACKET_SIZE;
26+
server_config.window_size = MAX_CHANNEL_PACKET_SIZE * 4;
27+
server_config
28+
.keys
29+
.push(PrivateKey::random(&mut OsRng, ssh_key::Algorithm::Ed25519).unwrap());
30+
server_config.preferred = {
31+
let mut preferred = Preferred::default();
32+
preferred.cipher = Cow::Borrowed(&[cipher::AES_256_GCM]);
33+
preferred
34+
};
35+
36+
let server_config = Arc::new(server_config);
37+
let socket = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
38+
let addr = socket.local_addr().unwrap();
39+
40+
tokio::spawn(async move {
41+
let (socket, _) = socket.accept().await.unwrap();
42+
server::run_stream(server_config, socket, EchoServer {})
43+
.await
44+
.unwrap();
45+
});
46+
47+
let mut client_config = client::Config::default();
48+
client_config.maximum_packet_size = MAX_CHANNEL_PACKET_SIZE;
49+
client_config.window_size = MAX_CHANNEL_PACKET_SIZE * 4;
50+
client_config.preferred = {
51+
let mut preferred = Preferred::default();
52+
preferred.cipher = Cow::Borrowed(&[cipher::AES_256_GCM]);
53+
preferred
54+
};
55+
56+
let mut session = client::connect(Arc::new(client_config), addr, TestClient {})
57+
.await
58+
.unwrap();
59+
60+
let authenticated = session
61+
.authenticate_publickey(
62+
std::env::var("USER").unwrap_or("user".to_owned()),
63+
PrivateKeyWithHashAlg::new(Arc::new(client_key), None),
64+
)
65+
.await
66+
.unwrap()
67+
.success();
68+
assert!(authenticated);
69+
70+
let payload = vec![0x5a; MAX_CHANNEL_PACKET_SIZE as usize];
71+
let mut channel = session.channel_open_session().await.unwrap();
72+
write_and_expect_echo(
73+
channel.make_writer(),
74+
&mut channel,
75+
&payload,
76+
|msg| match msg {
77+
ChannelMsg::Data { data } => Some(data),
78+
other => panic!("Unexpected message while waiting for echoed payload: {other:?}"),
79+
},
80+
)
81+
.await;
82+
83+
write_and_expect_echo(
84+
channel.make_writer_ext(Some(STDERR_EXTENDED_DATA_TYPE)),
85+
&mut channel,
86+
&payload,
87+
|msg| match msg {
88+
ChannelMsg::ExtendedData { data, ext } if ext == STDERR_EXTENDED_DATA_TYPE => {
89+
Some(data)
90+
}
91+
other => {
92+
panic!("Unexpected message while waiting for echoed extended payload: {other:?}")
93+
}
94+
},
95+
)
96+
.await;
97+
98+
channel.eof().await.unwrap();
99+
session
100+
.disconnect(Disconnect::ByApplication, "", "")
101+
.await
102+
.unwrap();
103+
}
104+
105+
async fn write_and_expect_echo<W, F>(
106+
mut writer: W,
107+
channel: &mut Channel<client::Msg>,
108+
payload: &[u8],
109+
mut extract_data: F,
110+
) where
111+
W: AsyncWrite + Unpin,
112+
F: FnMut(ChannelMsg) -> Option<bytes::Bytes>,
113+
{
114+
writer.write_all(payload).await.unwrap();
115+
writer.flush().await.unwrap();
116+
117+
let echoed = tokio::time::timeout(TEST_TIMEOUT, async {
118+
let mut echoed = Vec::with_capacity(payload.len());
119+
while echoed.len() < payload.len() {
120+
match channel.wait().await {
121+
Some(ChannelMsg::WindowAdjusted { .. }) => {}
122+
Some(ChannelMsg::Eof) | Some(ChannelMsg::Close) => {
123+
panic!("channel closed before echoing a full 256 KiB packet")
124+
}
125+
Some(msg) => {
126+
if let Some(data) = extract_data(msg) {
127+
echoed.extend_from_slice(&data);
128+
}
129+
}
130+
None => panic!("channel closed before echoing a full 256 KiB packet"),
131+
}
132+
}
133+
echoed
134+
})
135+
.await
136+
.expect("timed out waiting for echoed payload");
137+
138+
assert_eq!(echoed, payload);
139+
}
140+
141+
#[derive(Clone)]
142+
struct EchoServer {}
143+
144+
impl server::Handler for EchoServer {
145+
type Error = russh::Error;
146+
147+
async fn auth_publickey(
148+
&mut self,
149+
_user: &str,
150+
_public_key: &ssh_key::PublicKey,
151+
) -> Result<server::Auth, Self::Error> {
152+
Ok(server::Auth::Accept)
153+
}
154+
155+
async fn channel_open_session(
156+
&mut self,
157+
_channel: Channel<server::Msg>,
158+
_session: &mut server::Session,
159+
) -> Result<bool, Self::Error> {
160+
Ok(true)
161+
}
162+
163+
async fn data(
164+
&mut self,
165+
channel: ChannelId,
166+
data: &[u8],
167+
session: &mut server::Session,
168+
) -> Result<(), Self::Error> {
169+
session.data(channel, bytes::Bytes::copy_from_slice(data))?;
170+
Ok(())
171+
}
172+
173+
async fn extended_data(
174+
&mut self,
175+
channel: ChannelId,
176+
ext: u32,
177+
data: &[u8],
178+
session: &mut server::Session,
179+
) -> Result<(), Self::Error> {
180+
session.extended_data(channel, ext, bytes::Bytes::copy_from_slice(data))?;
181+
Ok(())
182+
}
183+
}
184+
185+
struct TestClient {}
186+
187+
impl client::Handler for TestClient {
188+
type Error = russh::Error;
189+
190+
async fn check_server_key(
191+
&mut self,
192+
_server_public_key: &ssh_key::PublicKey,
193+
) -> Result<bool, Self::Error> {
194+
Ok(true)
195+
}
196+
}

0 commit comments

Comments
 (0)