use crate::layers::{TcpOption, TcpSlice};
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct Ja4tParts {
pub window: u16,
pub option_kinds: Vec<u8>,
pub mss: u16,
pub window_scale: u8,
}
impl std::fmt::Display for Ja4tParts {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let opts = if self.option_kinds.is_empty() {
"00".to_string()
} else {
self.option_kinds
.iter()
.map(|k| k.to_string())
.collect::<Vec<_>>()
.join("-")
};
let ws = if self.window_scale == 0 {
"00".to_string()
} else {
self.window_scale.to_string()
};
write!(f, "{}_{}_{:02}_{}", self.window, opts, self.mss, ws)
}
}
pub fn ja4t_from_parts(window: u16, option_kinds: &[u8], mss: u16, window_scale: u8) -> Ja4tParts {
Ja4tParts {
window,
option_kinds: option_kinds.to_vec(),
mss,
window_scale,
}
}
fn option_kind(opt: &TcpOption<'_>) -> Option<u8> {
Some(match opt {
TcpOption::EndOfList => return None,
TcpOption::Nop => 1,
TcpOption::Mss(_) => 2,
TcpOption::WindowScale(_) => 3,
TcpOption::SackPermitted => 4,
TcpOption::Sack(_) => 5,
TcpOption::Timestamp { .. } => 8,
TcpOption::Unknown { kind, .. } => *kind,
})
}
pub fn ja4t_from_tcp(tcp: &TcpSlice<'_>) -> Ja4tParts {
let mut kinds = Vec::new();
let mut mss = 0u16;
let mut window_scale = 0u8;
for opt in tcp.options() {
match opt {
TcpOption::Mss(v) => mss = v,
TcpOption::WindowScale(v) => window_scale = v,
_ => {}
}
match option_kind(&opt) {
Some(k) => kinds.push(k),
None => break, }
}
Ja4tParts {
window: tcp.window(),
option_kinds: kinds,
mss,
window_scale,
}
}
pub fn ja4t(tcp: &TcpSlice<'_>) -> String {
ja4t_from_tcp(tcp).to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn golden_windows_syn() {
let p = ja4t_from_parts(64240, &[2, 1, 3, 1, 1, 4], 1460, 8);
assert_eq!(p.to_string(), "64240_2-1-3-1-1-4_1460_8");
}
#[test]
fn golden_linux_syn_with_timestamps() {
let p = ja4t_from_parts(65535, &[2, 1, 3, 1, 1, 8, 4], 1440, 6);
assert_eq!(p.to_string(), "65535_2-1-3-1-1-8-4_1440_6");
}
#[test]
fn golden_no_options() {
let p = ja4t_from_parts(65535, &[], 0, 0);
assert_eq!(p.to_string(), "65535_00_00_00");
}
#[test]
fn golden_synack_ja4ts() {
let p = ja4t_from_parts(64240, &[2, 1, 1, 4, 1, 3], 1460, 7);
assert_eq!(p.to_string(), "64240_2-1-1-4-1-3_1460_7");
let p2 = ja4t_from_parts(64704, &[2, 4, 8, 1, 3], 1360, 13);
assert_eq!(p2.to_string(), "64704_2-4-8-1-3_1360_13");
}
#[test]
fn option_kind_mapping() {
assert_eq!(option_kind(&TcpOption::Nop), Some(1));
assert_eq!(option_kind(&TcpOption::Mss(1460)), Some(2));
assert_eq!(option_kind(&TcpOption::WindowScale(8)), Some(3));
assert_eq!(option_kind(&TcpOption::SackPermitted), Some(4));
assert_eq!(option_kind(&TcpOption::Sack(&[])), Some(5));
assert_eq!(
option_kind(&TcpOption::Timestamp { tsval: 0, tsecr: 0 }),
Some(8)
);
assert_eq!(
option_kind(&TcpOption::Unknown {
kind: 34,
data: &[]
}),
Some(34)
);
assert_eq!(option_kind(&TcpOption::EndOfList), None); }
#[test]
fn from_tcp_parses_a_real_syn() {
let mut hdr = vec![
0x04, 0xd2, 0x00, 0x50, 0, 0, 0, 0, 0, 0, 0, 0, 0xa0, 0x02, 0xfa, 0xf0, 0, 0, 0, 0, ];
hdr.extend_from_slice(&[
2, 4, 0x05, 0xb4, 4, 2, 8, 10, 0, 0, 0, 0, 0, 0, 0, 0, 1, 3, 3, 7, ]);
let tcp = crate::layers::TcpSlice::new(&hdr, 40);
let p = ja4t_from_tcp(&tcp);
assert_eq!(p.window, 64240);
assert_eq!(p.mss, 1460);
assert_eq!(p.window_scale, 7);
assert_eq!(p.option_kinds, vec![2, 4, 8, 1, 3]);
assert_eq!(p.to_string(), "64240_2-4-8-1-3_1460_7");
}
}