#[must_use]
pub fn is_private_ipv4(ip: [u8; 4]) -> bool {
if ip[0] == 10 {
return true;
}
if ip[0] == 172 && ip[1] >= 16 && ip[1] <= 31 {
return true;
}
if ip[0] == 192 && ip[1] == 168 {
return true;
}
false
}
#[must_use]
pub fn is_loopback_ipv4(ip: [u8; 4]) -> bool {
ip[0] == 127
}
#[must_use]
pub fn is_link_local_ipv4(ip: [u8; 4]) -> bool {
ip[0] == 169 && ip[1] == 254
}
#[must_use]
pub fn is_multicast_ipv4(ip: [u8; 4]) -> bool {
ip[0] >= 224 && ip[0] <= 239
}
#[must_use]
pub fn is_public_ipv4(ip: [u8; 4]) -> bool {
!is_private_ipv4(ip)
&& !is_loopback_ipv4(ip)
&& !is_link_local_ipv4(ip)
&& !is_multicast_ipv4(ip)
}
#[must_use]
pub fn is_private_ipv6(ip: [u8; 16]) -> bool {
ip[0] & 0xFE == 0xFC
}
#[must_use]
pub fn is_ipv4_mapped_v6(ip: [u8; 16]) -> bool {
ip[..10] == [0u8; 10] && ip[10] == 0xFF && ip[11] == 0xFF
}
#[must_use]
pub fn is_loopback_ipv6(ip: [u8; 16]) -> bool {
ip[..15] == [0u8; 15] && ip[15] == 1
}
pub const DGA_MIN_LENGTH: usize = 12;
pub const DGA_MAX_VOWEL_RATIO_PPT: u32 = 200;
pub const DGA_MIN_CONSONANT_RUN: u8 = 4;
#[must_use]
pub fn vowel_ratio_ppt(s: &str) -> u32 {
let mut alpha: u32 = 0;
let mut vowels: u32 = 0;
for c in s.chars() {
if c.is_ascii_alphabetic() {
alpha += 1;
if matches!(c.to_ascii_lowercase(), 'a' | 'e' | 'i' | 'o' | 'u') {
vowels += 1;
}
}
}
if alpha == 0 {
return 0;
}
vowels * 1000 / alpha
}
#[must_use]
pub fn consonant_run_max(s: &str) -> u8 {
let mut max_run: u8 = 0;
let mut cur_run: u8 = 0;
for c in s.chars() {
if c.is_ascii_alphabetic() {
if !matches!(c.to_ascii_lowercase(), 'a' | 'e' | 'i' | 'o' | 'u') {
cur_run = cur_run.saturating_add(1);
if cur_run > max_run {
max_run = cur_run;
}
} else {
cur_run = 0;
}
} else {
cur_run = 0;
}
}
max_run
}
#[must_use]
pub fn is_likely_dga(subdomain: &str) -> bool {
subdomain.len() >= DGA_MIN_LENGTH
&& vowel_ratio_ppt(subdomain) < DGA_MAX_VOWEL_RATIO_PPT
&& consonant_run_max(subdomain) >= DGA_MIN_CONSONANT_RUN
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn private_10_x_x_x_is_private() {
assert!(is_private_ipv4([10, 0, 0, 1]));
assert!(is_private_ipv4([10, 255, 255, 255]));
}
#[test]
fn private_172_16_x_x_is_private() {
assert!(is_private_ipv4([172, 16, 0, 1]));
}
#[test]
fn private_172_31_x_x_is_private() {
assert!(is_private_ipv4([172, 31, 255, 254]));
}
#[test]
fn private_172_15_x_x_is_not_private() {
assert!(!is_private_ipv4([172, 15, 0, 1]));
}
#[test]
fn private_192_168_x_x_is_private() {
assert!(is_private_ipv4([192, 168, 1, 1]));
assert!(is_private_ipv4([192, 168, 255, 255]));
}
#[test]
fn public_8_8_8_8_is_not_private() {
assert!(!is_private_ipv4([8, 8, 8, 8]));
}
#[test]
fn loopback_127_0_0_1() {
assert!(is_loopback_ipv4([127, 0, 0, 1]));
}
#[test]
fn loopback_127_1_2_3() {
assert!(is_loopback_ipv4([127, 1, 2, 3]));
}
#[test]
fn non_loopback_126_x_x_x() {
assert!(!is_loopback_ipv4([126, 0, 0, 1]));
}
#[test]
fn link_local_169_254_x_x() {
assert!(is_link_local_ipv4([169, 254, 1, 1]));
assert!(is_link_local_ipv4([169, 254, 0, 0]));
}
#[test]
fn multicast_224_x_x_x() {
assert!(is_multicast_ipv4([224, 0, 0, 1]));
}
#[test]
fn multicast_239_x_x_x() {
assert!(is_multicast_ipv4([239, 255, 255, 255]));
}
#[test]
fn non_multicast_240_x_x_x() {
assert!(!is_multicast_ipv4([240, 0, 0, 1]));
}
#[test]
fn public_1_1_1_1_is_public() {
assert!(is_public_ipv4([1, 1, 1, 1]));
}
#[test]
fn private_address_is_not_public() {
assert!(!is_public_ipv4([10, 0, 0, 1]));
assert!(!is_public_ipv4([192, 168, 0, 1]));
}
#[test]
fn loopback_is_not_public() {
assert!(!is_public_ipv4([127, 0, 0, 1]));
}
#[test]
fn fc_prefix_is_private_ipv6() {
let mut ip = [0u8; 16];
ip[0] = 0xFC;
assert!(is_private_ipv6(ip));
}
#[test]
fn fd_prefix_is_private_ipv6() {
let mut ip = [0u8; 16];
ip[0] = 0xFD;
assert!(is_private_ipv6(ip));
}
#[test]
fn _2001_db8_is_not_private_ipv6() {
let mut ip = [0u8; 16];
ip[0] = 0x20;
ip[1] = 0x01;
ip[2] = 0x0D;
ip[3] = 0xB8;
assert!(!is_private_ipv6(ip));
}
#[test]
fn ipv4_mapped_ffff_prefix() {
let ip: [u8; 16] = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xFF, 0xFF, 93, 184, 216, 34];
assert!(is_ipv4_mapped_v6(ip));
}
#[test]
fn loopback_v6_is_loopback() {
let mut ip = [0u8; 16];
ip[15] = 1;
assert!(is_loopback_ipv6(ip));
}
#[test]
fn vowel_ratio_all_vowels_is_1000ppt() {
assert_eq!(vowel_ratio_ppt("aeiou"), 1000);
}
#[test]
fn vowel_ratio_no_vowels_is_0() {
assert_eq!(vowel_ratio_ppt("bcdfgh"), 0);
}
#[test]
fn vowel_ratio_mixed() {
assert_eq!(vowel_ratio_ppt("abcd"), 250);
}
#[test]
fn consonant_run_typical() {
assert_eq!(consonant_run_max("xkcd"), 4);
}
#[test]
fn consonant_run_empty_is_zero() {
assert_eq!(consonant_run_max(""), 0);
}
#[test]
fn dga_short_domain_not_flagged() {
assert!(!is_likely_dga("xkcdwplqrst")); }
#[test]
fn dga_high_vowel_ratio_not_flagged() {
assert!(!is_likely_dga("aeioumicrosofting")); }
#[test]
fn dga_typical_legitimate_domain_not_flagged() {
assert!(!is_likely_dga("microsoft"));
}
#[test]
fn dga_suspicious_long_consonant_heavy() {
assert!(is_likely_dga("xkcdwplqrstvmnbf"));
}
#[test]
fn dga_constants_correct() {
assert_eq!(DGA_MIN_LENGTH, 12);
assert_eq!(DGA_MAX_VOWEL_RATIO_PPT, 200);
assert_eq!(DGA_MIN_CONSONANT_RUN, 4);
}
}