In crates/rustnet-core/src/network/dpi/ssh.rs, analyze_ssh scans the payload for an SSH packet signature to set the connection state:
for i in 0..payload.len().saturating_sub(6) {
if payload.len() >= i + 6 {
if is_valid_ssh_packet_at_offset(payload, i) { ... }
}
}
A signature occupies 6 bytes (4-byte length, 1-byte padding length, 1-byte message type), so the last valid start offset is len - 6. Because the range is exclusive, it must end at len - 5 to include that offset. saturating_sub(6) ends the range at len - 6 exclusive, i.e. the loop stops at len - 7 and never inspects the final 6-byte window.
Effect: when a valid SSH packet's signature lands in the last 6 bytes of the payload (including a payload that is exactly 6 bytes long), it is never validated, so found_packet_state stays false and the connection state falls back to Banner even though a KEXINIT/NEWKEYS/USERAUTH/etc. message is present. The code comment says it scans "throughout the payload", which it does not.
Fix: use saturating_sub(5); the inner payload.len() >= i + 6 guard still bounds the access.
In
crates/rustnet-core/src/network/dpi/ssh.rs,analyze_sshscans the payload for an SSH packet signature to set the connection state:A signature occupies 6 bytes (4-byte length, 1-byte padding length, 1-byte message type), so the last valid start offset is
len - 6. Because the range is exclusive, it must end atlen - 5to include that offset.saturating_sub(6)ends the range atlen - 6exclusive, i.e. the loop stops atlen - 7and never inspects the final 6-byte window.Effect: when a valid SSH packet's signature lands in the last 6 bytes of the payload (including a payload that is exactly 6 bytes long), it is never validated, so
found_packet_statestays false and the connection state falls back toBannereven though a KEXINIT/NEWKEYS/USERAUTH/etc. message is present. The code comment says it scans "throughout the payload", which it does not.Fix: use
saturating_sub(5); the innerpayload.len() >= i + 6guard still bounds the access.