1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
//! Threat intelligence — suspicious IP detection.
//!
//! Detects connections to known bad IP ranges (bogons in WAN context,
//! known malware C2 ranges, known Tor exit nodes, scanners, and proxies).
//! Uses embedded ranges for zero-dependency, high-performance lookups.
use std::collections::HashSet;
use std::net::{IpAddr, Ipv4Addr};
use crate::types::{Connection, ThreatInfo, TcpState};
/// Threat detection engine with embedded intelligence.
pub struct ThreatDetector {
/// IPs already flagged this session (avoid repeat alerts).
flagged: HashSet<IpAddr>,
}
impl ThreatDetector {
pub fn new() -> Self {
Self {
flagged: HashSet::new(),
}
}
/// Scan connections for threats. Returns newly detected threats
/// (already-flagged IPs are skipped).
pub fn scan(&mut self, connections: &[Connection]) -> Vec<ThreatInfo> {
let mut threats = Vec::new();
for conn in connections {
if matches!(conn.state.as_ref(), Some(TcpState::Listen)) {
continue;
}
let Some(remote) = conn.remote_addr else { continue };
if remote.is_loopback() || remote.is_unspecified() {
continue;
}
if self.flagged.contains(&remote) {
continue;
}
if let Some(threat) = classify_ip(remote) {
self.flagged.insert(remote);
threats.push(threat);
}
}
threats
}
/// Check a single IP.
pub fn check_ip(&self, ip: IpAddr) -> Option<ThreatInfo> {
classify_ip(ip)
}
}
/// Classify an IP address against threat intelligence databases.
fn classify_ip(ip: IpAddr) -> Option<ThreatInfo> {
match ip {
IpAddr::V4(v4) => classify_ipv4(v4),
IpAddr::V6(_) => None, // IPv6 threat detection is limited
}
}
fn classify_ipv4(ip: Ipv4Addr) -> Option<ThreatInfo> {
let octets = ip.octets();
let ip_addr = IpAddr::V4(ip);
// ── Bogon ranges (should never appear as WAN destinations) ──
// RFC 1918 private ranges appearing as remote addresses in WAN context
// is suspicious (possible NAT leak or misconfiguration)
// Note: we don't flag these for local connections
if is_bogon_remote(octets) {
return Some(ThreatInfo {
ip: ip_addr,
reason: "Bogon/reserved IP used as remote endpoint".to_string(),
});
}
// ── Known scanner/attacker ranges ──
if is_known_scanner(octets) {
return Some(ThreatInfo {
ip: ip_addr,
reason: "Known scanner/attacker IP range".to_string(),
});
}
// ── Known malware C2 ranges (curated common ranges) ──
if is_known_malware_range(octets) {
return Some(ThreatInfo {
ip: ip_addr,
reason: "Known malware C2 IP range".to_string(),
});
}
None
}
/// Check if the IP is a bogon (reserved/unroutable) that shouldn't be a remote WAN address.
fn is_bogon_remote(octets: [u8; 4]) -> bool {
match octets[0] {
// Not flagged on purpose (issue #11): 100.64.0.0/10 is carrier-grade
// NAT and the range Tailscale and other mesh VPNs hand out, and
// 169.254.0.0/16 is link-local. Both are normal peers, not threats.
0 => true, // 0.0.0.0/8 — "This" network
127 => true, // 127.0.0.0/8 — Loopback
192 if octets[1] == 0 && octets[2] == 0 => true, // 192.0.0.0/24 — IETF Protocol
192 if octets[1] == 0 && octets[2] == 2 => true, // 192.0.2.0/24 — TEST-NET-1
198 if octets[1] == 51 && octets[2] == 100 => true, // 198.51.100.0/24 — TEST-NET-2
203 if octets[1] == 0 && octets[2] == 113 => true, // 203.0.113.0/24 — TEST-NET-3
198 if octets[1] >= 18 && octets[1] <= 19 => true, // 198.18.0.0/15 — Benchmarking
240..=255 => true, // 240.0.0.0/4 — Reserved & Broadcast
_ => false,
}
}
/// Known scanner networks (research scanners, known attack sources).
fn is_known_scanner(octets: [u8; 4]) -> bool {
// Censys' published scanner /24s. The old list had 142.163.0.0/16 (a
// byte swapped 162.142, really a Canadian ISP) and two whole /16s of
// European hosting, so ordinary SSH sessions to VPSes there were raised
// as critical (issue #11).
let prefix24 = [octets[0], octets[1], octets[2]];
matches!(prefix24,
[162, 142, 125] |
[167, 94, 138] | [167, 94, 145] | [167, 94, 146] |
[167, 248, 133] |
[199, 45, 154] | [199, 45, 155] |
[206, 168, 34]
)
}
/// Known malware C2 / botnet ranges.
fn is_known_malware_range(octets: [u8; 4]) -> bool {
// These are illustrative high-confidence ranges commonly associated
// with malware infrastructure. In production, integrate a threat feed.
let _prefix16 = (octets[0] as u16) << 8 | octets[1] as u16;
// Check for suspicious port + IP combinations common in C2
// Rather than static IPs (which change), we focus on behavioral patterns
false
}
#[cfg(test)]
mod tests {
use super::*;
fn flagged(s: &str) -> bool {
classify_ip(s.parse().unwrap()).is_some()
}
/// Issue #11: SSH sessions showed as critical alerts. These are all
/// ordinary peers that the old ranges flagged.
#[test]
fn ordinary_peers_are_not_threats() {
assert!(!flagged("100.101.12.34"), "Tailscale / CGNAT peer");
assert!(!flagged("169.254.10.20"), "link-local peer");
assert!(!flagged("142.163.50.60"), "Canadian ISP, not Censys");
assert!(!flagged("185.24.10.10"), "European hosting /16");
assert!(!flagged("185.232.20.20"), "European hosting /16");
assert!(!flagged("192.168.1.10"));
}
#[test]
fn real_scanners_and_bogons_still_flag() {
assert!(flagged("162.142.125.7"));
assert!(flagged("167.94.138.120"));
assert!(flagged("0.1.2.3"));
assert!(flagged("198.18.0.5"));
assert!(flagged("250.0.0.1"));
}
}