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 pub fn name(msg_type: u8) -> &'static str {
68 match msg_type {
69 DISCONNECT => "DISCONNECT",
70 IGNORE => "IGNORE",
71 UNIMPLEMENTED => "UNIMPLEMENTED",
72 DEBUG => "DEBUG",
73 SERVICE_REQUEST => "SERVICE_REQUEST",
74 SERVICE_ACCEPT => "SERVICE_ACCEPT",
75 EXT_INFO => "EXT_INFO",
76 NEWCOMPRESS => "NEWCOMPRESS",
77 KEXINIT => "KEXINIT",
78 NEWKEYS => "NEWKEYS",
79 KEXDH_INIT => "KEXDH_INIT",
80 KEXDH_REPLY => "KEXDH_REPLY",
81 USERAUTH_REQUEST => "USERAUTH_REQUEST",
82 USERAUTH_FAILURE => "USERAUTH_FAILURE",
83 USERAUTH_SUCCESS => "USERAUTH_SUCCESS",
84 USERAUTH_BANNER => "USERAUTH_BANNER",
85 GLOBAL_REQUEST => "GLOBAL_REQUEST",
86 REQUEST_SUCCESS => "REQUEST_SUCCESS",
87 REQUEST_FAILURE => "REQUEST_FAILURE",
88 CHANNEL_OPEN => "CHANNEL_OPEN",
89 CHANNEL_OPEN_CONFIRMATION => "CHANNEL_OPEN_CONFIRMATION",
90 CHANNEL_OPEN_FAILURE => "CHANNEL_OPEN_FAILURE",
91 CHANNEL_WINDOW_ADJUST => "CHANNEL_WINDOW_ADJUST",
92 CHANNEL_DATA => "CHANNEL_DATA",
93 CHANNEL_EXTENDED_DATA => "CHANNEL_EXTENDED_DATA",
94 CHANNEL_EOF => "CHANNEL_EOF",
95 CHANNEL_CLOSE => "CHANNEL_CLOSE",
96 CHANNEL_REQUEST => "CHANNEL_REQUEST",
97 CHANNEL_SUCCESS => "CHANNEL_SUCCESS",
98 CHANNEL_FAILURE => "CHANNEL_FAILURE",
99 _ => "UNKNOWN",
100 }
101 }
102}
103
104#[derive(Debug, Clone)]
106pub struct SshLayer {
107 pub index: LayerIndex,
108}
109
110pub static SSH_FIELDS: &[&str] = &[
112 "packet_length",
113 "padding_length",
114 "message_type",
115 "version_string",
116];
117
118impl SshLayer {
119 pub fn is_version_exchange(&self, buf: &[u8]) -> bool {
121 let slice = self.index.slice(buf);
122 slice.len() >= 4 && &slice[..4] == b"SSH-"
123 }
124
125 pub fn version_string<'a>(&self, buf: &'a [u8]) -> Option<&'a str> {
129 let slice = self.index.slice(buf);
130 if slice.len() < 4 || &slice[..4] != b"SSH-" {
131 return None;
132 }
133 let end = slice
135 .windows(2)
136 .position(|w| w == b"\r\n")
137 .unwrap_or(slice.len());
138 std::str::from_utf8(&slice[..end]).ok()
139 }
140
141 pub fn packet_length(&self, buf: &[u8]) -> Result<u32, FieldError> {
143 let slice = self.index.slice(buf);
144 if self.is_version_exchange(buf) {
145 return Err(FieldError::BufferTooShort {
146 offset: self.index.start,
147 need: 4,
148 have: 0,
149 });
150 }
151 if slice.len() < 4 {
152 return Err(FieldError::BufferTooShort {
153 offset: self.index.start,
154 need: 4,
155 have: slice.len(),
156 });
157 }
158 Ok(u32::from_be_bytes([slice[0], slice[1], slice[2], slice[3]]))
159 }
160
161 pub fn padding_length(&self, buf: &[u8]) -> Result<u8, FieldError> {
163 let slice = self.index.slice(buf);
164 if self.is_version_exchange(buf) {
165 return Err(FieldError::BufferTooShort {
166 offset: self.index.start,
167 need: 5,
168 have: 0,
169 });
170 }
171 if slice.len() < 5 {
172 return Err(FieldError::BufferTooShort {
173 offset: self.index.start,
174 need: 5,
175 have: slice.len(),
176 });
177 }
178 Ok(slice[4])
179 }
180
181 pub fn message_type(&self, buf: &[u8]) -> Result<Option<u8>, FieldError> {
183 if self.is_version_exchange(buf) {
184 return Ok(None);
185 }
186 let slice = self.index.slice(buf);
187 if slice.len() < 6 {
188 return Ok(None);
189 }
190 Ok(Some(slice[5]))
191 }
192
193 pub fn payload_data<'a>(&self, buf: &'a [u8]) -> &'a [u8] {
195 let slice = self.index.slice(buf);
196 if self.is_version_exchange(buf) {
197 return slice;
198 }
199 if slice.len() < SSH_BINARY_HEADER_LEN {
200 return &[];
201 }
202 let padding_len = slice[4] as usize;
203 let pkt_len = u32::from_be_bytes([slice[0], slice[1], slice[2], slice[3]]) as usize;
204 let payload_len = pkt_len.saturating_sub(padding_len).saturating_sub(1);
206 let payload_end = (5 + payload_len).min(slice.len());
207 &slice[5..payload_end]
208 }
209
210 pub fn get_field(&self, buf: &[u8], name: &str) -> Option<Result<FieldValue, FieldError>> {
212 match name {
213 "packet_length" => {
214 if self.is_version_exchange(buf) {
215 return Some(Ok(FieldValue::U32(0)));
216 }
217 Some(self.packet_length(buf).map(FieldValue::U32))
218 },
219 "padding_length" => {
220 if self.is_version_exchange(buf) {
221 return Some(Ok(FieldValue::U8(0)));
222 }
223 Some(self.padding_length(buf).map(FieldValue::U8))
224 },
225 "message_type" => match self.message_type(buf) {
226 Ok(Some(t)) => Some(Ok(FieldValue::U8(t))),
227 Ok(None) => Some(Ok(FieldValue::U8(0))),
228 Err(e) => Some(Err(e)),
229 },
230 "version_string" => {
231 if let Some(vs) = self.version_string(buf) {
232 Some(Ok(FieldValue::Bytes(vs.as_bytes().to_vec())))
233 } else {
234 Some(Ok(FieldValue::Bytes(vec![])))
235 }
236 },
237 _ => None,
238 }
239 }
240
241 pub fn set_field(
243 &self,
244 _buf: &mut [u8],
245 _name: &str,
246 _value: FieldValue,
247 ) -> Option<Result<(), FieldError>> {
248 None
250 }
251
252 pub fn field_names(&self) -> &'static [&'static str] {
254 SSH_FIELDS
255 }
256}
257
258impl Layer for SshLayer {
259 fn kind(&self) -> LayerKind {
260 LayerKind::Ssh
261 }
262
263 fn summary(&self, data: &[u8]) -> String {
264 let buf = self.index.slice(data);
265 if self.is_version_exchange(data) {
266 if let Some(vs) = self.version_string(data) {
267 return format!("SSH Version Exchange {}", vs);
268 }
269 return "SSH Version Exchange".to_string();
270 }
271 if buf.len() >= 6 {
272 let msg_type = buf[5];
273 let name = msg_types::name(msg_type);
274 format!("SSH {}", name)
275 } else {
276 "SSH".to_string()
277 }
278 }
279
280 fn header_len(&self, data: &[u8]) -> usize {
281 if self.is_version_exchange(data) {
282 return self.index.len();
283 }
284 let slice = self.index.slice(data);
285 if slice.len() >= 4 {
286 let pkt_len = u32::from_be_bytes([slice[0], slice[1], slice[2], slice[3]]) as usize;
287 (4 + pkt_len).min(slice.len())
289 } else {
290 slice.len()
291 }
292 }
293
294 fn hashret(&self, _data: &[u8]) -> Vec<u8> {
295 vec![]
296 }
297
298 fn field_names(&self) -> &'static [&'static str] {
299 SSH_FIELDS
300 }
301}
302
303pub fn is_ssh_payload(data: &[u8]) -> bool {
308 if data.len() >= 4 && &data[..4] == b"SSH-" {
309 return true;
310 }
311 if data.len() >= SSH_BINARY_HEADER_LEN {
312 let pkt_len = u32::from_be_bytes([data[0], data[1], data[2], data[3]]) as usize;
313 let padding_len = data[4] as usize;
314 pkt_len >= 12 && pkt_len <= 35000 && padding_len < pkt_len && padding_len >= 4
316 } else {
317 false
318 }
319}
320
321#[cfg(test)]
322mod tests {
323 use super::*;
324
325 #[test]
326 fn test_version_exchange() {
327 let data = b"SSH-2.0-OpenSSH_9.2p1 Debian-2+deb12u1\r\n";
328 let layer = SshLayer {
329 index: LayerIndex::new(LayerKind::Ssh, 0, data.len()),
330 };
331 assert!(layer.is_version_exchange(data));
332 assert_eq!(
333 layer.version_string(data),
334 Some("SSH-2.0-OpenSSH_9.2p1 Debian-2+deb12u1")
335 );
336 assert!(layer.summary(data).contains("SSH-2.0-OpenSSH_9.2p1"));
337 }
338
339 #[test]
340 fn test_binary_packet_kexinit() {
341 let mut data = vec![0u8; 32];
343 data[0..4].copy_from_slice(&24u32.to_be_bytes());
345 data[4] = 8; data[5] = 20; let layer = SshLayer {
350 index: LayerIndex::new(LayerKind::Ssh, 0, data.len()),
351 };
352 assert!(!layer.is_version_exchange(&data));
353 assert_eq!(layer.packet_length(&data).unwrap(), 24);
354 assert_eq!(layer.padding_length(&data).unwrap(), 8);
355 assert_eq!(layer.message_type(&data).unwrap(), Some(20));
356 assert!(layer.summary(&data).contains("KEXINIT"));
357 }
358
359 #[test]
360 fn test_binary_packet_newkeys() {
361 let mut data = vec![0u8; 16];
362 data[0..4].copy_from_slice(&12u32.to_be_bytes());
363 data[4] = 10; data[5] = 21; let layer = SshLayer {
367 index: LayerIndex::new(LayerKind::Ssh, 0, data.len()),
368 };
369 assert_eq!(layer.message_type(&data).unwrap(), Some(21));
370 assert!(layer.summary(&data).contains("NEWKEYS"));
371 }
372
373 #[test]
374 fn test_is_ssh_payload() {
375 assert!(is_ssh_payload(b"SSH-2.0-OpenSSH\r\n"));
376 assert!(!is_ssh_payload(b"HTTP/1.1"));
377 assert!(!is_ssh_payload(b"SH"));
378 }
379}