stackforge_core/layer/ssh/
mod.rs1pub mod builder;
17
18pub use builder::SshBuilder;
19
20use crate::layer::field::{FieldError, FieldValue};
21use crate::layer::{Layer, LayerIndex, LayerKind};
22
23pub const SSH_BINARY_HEADER_LEN: usize = 5;
25
26pub const SSH_PORT: u16 = 22;
28
29pub mod msg_types {
31 pub const DISCONNECT: u8 = 1;
33 pub const IGNORE: u8 = 2;
34 pub const UNIMPLEMENTED: u8 = 3;
35 pub const DEBUG: u8 = 4;
36 pub const SERVICE_REQUEST: u8 = 5;
37 pub const SERVICE_ACCEPT: u8 = 6;
38 pub const EXT_INFO: u8 = 7; pub const NEWCOMPRESS: u8 = 8;
40 pub const KEXINIT: u8 = 20;
41 pub const NEWKEYS: u8 = 21;
42 pub const KEXDH_INIT: u8 = 30;
44 pub const KEXDH_REPLY: u8 = 31;
45 pub const USERAUTH_REQUEST: u8 = 50;
47 pub const USERAUTH_FAILURE: u8 = 51;
48 pub const USERAUTH_SUCCESS: u8 = 52;
49 pub const USERAUTH_BANNER: u8 = 53;
50 pub const CHANNEL_OPEN: u8 = 90;
52 pub const CHANNEL_OPEN_CONFIRMATION: u8 = 91;
53 pub const CHANNEL_OPEN_FAILURE: u8 = 92;
54 pub const CHANNEL_WINDOW_ADJUST: u8 = 93;
55 pub const CHANNEL_DATA: u8 = 94;
56 pub const CHANNEL_EXTENDED_DATA: u8 = 95;
57 pub const CHANNEL_EOF: u8 = 96;
58 pub const CHANNEL_CLOSE: u8 = 97;
59 pub const CHANNEL_REQUEST: u8 = 98;
60 pub const CHANNEL_SUCCESS: u8 = 99;
61 pub const CHANNEL_FAILURE: u8 = 100;
62 pub const GLOBAL_REQUEST: u8 = 80;
63 pub const REQUEST_SUCCESS: u8 = 81;
64 pub const REQUEST_FAILURE: u8 = 82;
65
66 #[must_use]
68 pub fn name(msg_type: u8) -> &'static str {
69 match msg_type {
70 DISCONNECT => "DISCONNECT",
71 IGNORE => "IGNORE",
72 UNIMPLEMENTED => "UNIMPLEMENTED",
73 DEBUG => "DEBUG",
74 SERVICE_REQUEST => "SERVICE_REQUEST",
75 SERVICE_ACCEPT => "SERVICE_ACCEPT",
76 EXT_INFO => "EXT_INFO",
77 NEWCOMPRESS => "NEWCOMPRESS",
78 KEXINIT => "KEXINIT",
79 NEWKEYS => "NEWKEYS",
80 KEXDH_INIT => "KEXDH_INIT",
81 KEXDH_REPLY => "KEXDH_REPLY",
82 USERAUTH_REQUEST => "USERAUTH_REQUEST",
83 USERAUTH_FAILURE => "USERAUTH_FAILURE",
84 USERAUTH_SUCCESS => "USERAUTH_SUCCESS",
85 USERAUTH_BANNER => "USERAUTH_BANNER",
86 GLOBAL_REQUEST => "GLOBAL_REQUEST",
87 REQUEST_SUCCESS => "REQUEST_SUCCESS",
88 REQUEST_FAILURE => "REQUEST_FAILURE",
89 CHANNEL_OPEN => "CHANNEL_OPEN",
90 CHANNEL_OPEN_CONFIRMATION => "CHANNEL_OPEN_CONFIRMATION",
91 CHANNEL_OPEN_FAILURE => "CHANNEL_OPEN_FAILURE",
92 CHANNEL_WINDOW_ADJUST => "CHANNEL_WINDOW_ADJUST",
93 CHANNEL_DATA => "CHANNEL_DATA",
94 CHANNEL_EXTENDED_DATA => "CHANNEL_EXTENDED_DATA",
95 CHANNEL_EOF => "CHANNEL_EOF",
96 CHANNEL_CLOSE => "CHANNEL_CLOSE",
97 CHANNEL_REQUEST => "CHANNEL_REQUEST",
98 CHANNEL_SUCCESS => "CHANNEL_SUCCESS",
99 CHANNEL_FAILURE => "CHANNEL_FAILURE",
100 _ => "UNKNOWN",
101 }
102 }
103}
104
105#[derive(Debug, Clone)]
107pub struct SshLayer {
108 pub index: LayerIndex,
109}
110
111pub static SSH_FIELDS: &[&str] = &[
113 "packet_length",
114 "padding_length",
115 "message_type",
116 "version_string",
117];
118
119impl SshLayer {
120 #[must_use]
122 pub fn is_version_exchange(&self, buf: &[u8]) -> bool {
123 let slice = self.index.slice(buf);
124 slice.len() >= 4 && &slice[..4] == b"SSH-"
125 }
126
127 #[must_use]
131 pub fn version_string<'a>(&self, buf: &'a [u8]) -> Option<&'a str> {
132 let slice = self.index.slice(buf);
133 if slice.len() < 4 || &slice[..4] != b"SSH-" {
134 return None;
135 }
136 let end = slice
138 .windows(2)
139 .position(|w| w == b"\r\n")
140 .unwrap_or(slice.len());
141 std::str::from_utf8(&slice[..end]).ok()
142 }
143
144 pub fn packet_length(&self, buf: &[u8]) -> Result<u32, FieldError> {
146 let slice = self.index.slice(buf);
147 if self.is_version_exchange(buf) {
148 return Err(FieldError::BufferTooShort {
149 offset: self.index.start,
150 need: 4,
151 have: 0,
152 });
153 }
154 if slice.len() < 4 {
155 return Err(FieldError::BufferTooShort {
156 offset: self.index.start,
157 need: 4,
158 have: slice.len(),
159 });
160 }
161 Ok(u32::from_be_bytes([slice[0], slice[1], slice[2], slice[3]]))
162 }
163
164 pub fn padding_length(&self, buf: &[u8]) -> Result<u8, FieldError> {
166 let slice = self.index.slice(buf);
167 if self.is_version_exchange(buf) {
168 return Err(FieldError::BufferTooShort {
169 offset: self.index.start,
170 need: 5,
171 have: 0,
172 });
173 }
174 if slice.len() < 5 {
175 return Err(FieldError::BufferTooShort {
176 offset: self.index.start,
177 need: 5,
178 have: slice.len(),
179 });
180 }
181 Ok(slice[4])
182 }
183
184 pub fn message_type(&self, buf: &[u8]) -> Result<Option<u8>, FieldError> {
186 if self.is_version_exchange(buf) {
187 return Ok(None);
188 }
189 let slice = self.index.slice(buf);
190 if slice.len() < 6 {
191 return Ok(None);
192 }
193 Ok(Some(slice[5]))
194 }
195
196 #[must_use]
198 pub fn payload_data<'a>(&self, buf: &'a [u8]) -> &'a [u8] {
199 let slice = self.index.slice(buf);
200 if self.is_version_exchange(buf) {
201 return slice;
202 }
203 if slice.len() < SSH_BINARY_HEADER_LEN {
204 return &[];
205 }
206 let padding_len = slice[4] as usize;
207 let pkt_len = u32::from_be_bytes([slice[0], slice[1], slice[2], slice[3]]) as usize;
208 let payload_len = pkt_len.saturating_sub(padding_len).saturating_sub(1);
210 let payload_end = (5 + payload_len).min(slice.len());
211 &slice[5..payload_end]
212 }
213
214 pub fn get_field(&self, buf: &[u8], name: &str) -> Option<Result<FieldValue, FieldError>> {
216 match name {
217 "packet_length" => {
218 if self.is_version_exchange(buf) {
219 return Some(Ok(FieldValue::U32(0)));
220 }
221 Some(self.packet_length(buf).map(FieldValue::U32))
222 },
223 "padding_length" => {
224 if self.is_version_exchange(buf) {
225 return Some(Ok(FieldValue::U8(0)));
226 }
227 Some(self.padding_length(buf).map(FieldValue::U8))
228 },
229 "message_type" => match self.message_type(buf) {
230 Ok(Some(t)) => Some(Ok(FieldValue::U8(t))),
231 Ok(None) => Some(Ok(FieldValue::U8(0))),
232 Err(e) => Some(Err(e)),
233 },
234 "version_string" => {
235 if let Some(vs) = self.version_string(buf) {
236 Some(Ok(FieldValue::Bytes(vs.as_bytes().to_vec())))
237 } else {
238 Some(Ok(FieldValue::Bytes(vec![])))
239 }
240 },
241 _ => None,
242 }
243 }
244
245 pub fn set_field(
247 &self,
248 _buf: &mut [u8],
249 _name: &str,
250 _value: FieldValue,
251 ) -> Option<Result<(), FieldError>> {
252 None
254 }
255
256 #[must_use]
258 pub fn field_names(&self) -> &'static [&'static str] {
259 SSH_FIELDS
260 }
261}
262
263impl Layer for SshLayer {
264 fn kind(&self) -> LayerKind {
265 LayerKind::Ssh
266 }
267
268 fn summary(&self, data: &[u8]) -> String {
269 let buf = self.index.slice(data);
270 if self.is_version_exchange(data) {
271 if let Some(vs) = self.version_string(data) {
272 return format!("SSH Version Exchange {vs}");
273 }
274 return "SSH Version Exchange".to_string();
275 }
276 if buf.len() >= 6 {
277 let msg_type = buf[5];
278 let name = msg_types::name(msg_type);
279 format!("SSH {name}")
280 } else {
281 "SSH".to_string()
282 }
283 }
284
285 fn header_len(&self, data: &[u8]) -> usize {
286 if self.is_version_exchange(data) {
287 return self.index.len();
288 }
289 let slice = self.index.slice(data);
290 if slice.len() >= 4 {
291 let pkt_len = u32::from_be_bytes([slice[0], slice[1], slice[2], slice[3]]) as usize;
292 (4 + pkt_len).min(slice.len())
294 } else {
295 slice.len()
296 }
297 }
298
299 fn hashret(&self, _data: &[u8]) -> Vec<u8> {
300 vec![]
301 }
302
303 fn field_names(&self) -> &'static [&'static str] {
304 SSH_FIELDS
305 }
306}
307
308#[must_use]
313pub fn is_ssh_payload(data: &[u8]) -> bool {
314 if data.len() >= 4 && &data[..4] == b"SSH-" {
315 return true;
316 }
317 if data.len() >= SSH_BINARY_HEADER_LEN {
318 let pkt_len = u32::from_be_bytes([data[0], data[1], data[2], data[3]]) as usize;
319 let padding_len = data[4] as usize;
320 (12..=35000).contains(&pkt_len) && padding_len < pkt_len && padding_len >= 4
322 } else {
323 false
324 }
325}
326
327#[cfg(test)]
328mod tests {
329 use super::*;
330
331 #[test]
332 fn test_version_exchange() {
333 let data = b"SSH-2.0-OpenSSH_9.2p1 Debian-2+deb12u1\r\n";
334 let layer = SshLayer {
335 index: LayerIndex::new(LayerKind::Ssh, 0, data.len()),
336 };
337 assert!(layer.is_version_exchange(data));
338 assert_eq!(
339 layer.version_string(data),
340 Some("SSH-2.0-OpenSSH_9.2p1 Debian-2+deb12u1")
341 );
342 assert!(layer.summary(data).contains("SSH-2.0-OpenSSH_9.2p1"));
343 }
344
345 #[test]
346 fn test_binary_packet_kexinit() {
347 let mut data = vec![0u8; 32];
349 data[0..4].copy_from_slice(&24u32.to_be_bytes());
351 data[4] = 8; data[5] = 20; let layer = SshLayer {
356 index: LayerIndex::new(LayerKind::Ssh, 0, data.len()),
357 };
358 assert!(!layer.is_version_exchange(&data));
359 assert_eq!(layer.packet_length(&data).unwrap(), 24);
360 assert_eq!(layer.padding_length(&data).unwrap(), 8);
361 assert_eq!(layer.message_type(&data).unwrap(), Some(20));
362 assert!(layer.summary(&data).contains("KEXINIT"));
363 }
364
365 #[test]
366 fn test_binary_packet_newkeys() {
367 let mut data = vec![0u8; 16];
368 data[0..4].copy_from_slice(&12u32.to_be_bytes());
369 data[4] = 10; data[5] = 21; let layer = SshLayer {
373 index: LayerIndex::new(LayerKind::Ssh, 0, data.len()),
374 };
375 assert_eq!(layer.message_type(&data).unwrap(), Some(21));
376 assert!(layer.summary(&data).contains("NEWKEYS"));
377 }
378
379 #[test]
380 fn test_is_ssh_payload() {
381 assert!(is_ssh_payload(b"SSH-2.0-OpenSSH\r\n"));
382 assert!(!is_ssh_payload(b"HTTP/1.1"));
383 assert!(!is_ssh_payload(b"SH"));
384 }
385}