Add: dns leak detection
This commit is contained in:
116
crates/wtfnet-dnsleak/src/rules.rs
Normal file
116
crates/wtfnet-dnsleak/src/rules.rs
Normal file
@@ -0,0 +1,116 @@
|
||||
use crate::policy::LeakPolicy;
|
||||
use crate::report::{EnrichedEvent, LeakTransport, LeakType, Severity};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LeakDecision {
|
||||
pub leak_type: LeakType,
|
||||
pub severity: Severity,
|
||||
pub policy_rule_id: String,
|
||||
}
|
||||
|
||||
pub fn evaluate(event: &EnrichedEvent, policy: &LeakPolicy) -> Option<LeakDecision> {
|
||||
match event.transport {
|
||||
LeakTransport::Udp53 | LeakTransport::Tcp53 => {
|
||||
if is_proxy_required(event, policy) && !is_allowed(event, policy) {
|
||||
return Some(LeakDecision {
|
||||
leak_type: LeakType::B,
|
||||
severity: Severity::P1,
|
||||
policy_rule_id: "LEAK_B_PROXY_REQUIRED".to_string(),
|
||||
});
|
||||
}
|
||||
if !is_allowed(event, policy) {
|
||||
return Some(LeakDecision {
|
||||
leak_type: LeakType::A,
|
||||
severity: Severity::P0,
|
||||
policy_rule_id: "LEAK_A_PLAINTEXT".to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
LeakTransport::Dot | LeakTransport::Doh => {
|
||||
if !is_allowed(event, policy) {
|
||||
return Some(LeakDecision {
|
||||
leak_type: LeakType::C,
|
||||
severity: Severity::P1,
|
||||
policy_rule_id: "LEAK_C_ENCRYPTED".to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
LeakTransport::Unknown => {}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn is_allowed(event: &EnrichedEvent, policy: &LeakPolicy) -> bool {
|
||||
let has_rules = !policy.allowed_ifaces.is_empty()
|
||||
|| !policy.allowed_destinations.is_empty()
|
||||
|| !policy.allowed_ports.is_empty()
|
||||
|| !policy.allowed_processes.is_empty();
|
||||
if !has_rules {
|
||||
return false;
|
||||
}
|
||||
|
||||
if let Some(iface) = event.iface_name.as_ref() {
|
||||
if policy
|
||||
.allowed_ifaces
|
||||
.iter()
|
||||
.any(|allowed| allowed.eq_ignore_ascii_case(iface))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if policy
|
||||
.allowed_ports
|
||||
.iter()
|
||||
.any(|port| *port == event.dst_port)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if policy
|
||||
.allowed_destinations
|
||||
.iter()
|
||||
.any(|net| net.contains(&event.dst_ip))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if let Some(name) = event
|
||||
.owner
|
||||
.as_ref()
|
||||
.and_then(|owner| owner.process_name.as_ref())
|
||||
{
|
||||
if policy
|
||||
.allowed_processes
|
||||
.iter()
|
||||
.any(|value| value.eq_ignore_ascii_case(name))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
fn is_proxy_required(event: &EnrichedEvent, policy: &LeakPolicy) -> bool {
|
||||
let Some(qname) = event.qname.as_ref() else {
|
||||
return false;
|
||||
};
|
||||
let qname = qname.to_ascii_lowercase();
|
||||
if policy.proxy_required_domains.iter().any(|domain| {
|
||||
let domain = domain.to_ascii_lowercase();
|
||||
qname == domain || qname.ends_with(&format!(".{domain}"))
|
||||
}) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if !policy.allowlist_domains.is_empty() {
|
||||
let allowed = policy.allowlist_domains.iter().any(|domain| {
|
||||
let domain = domain.to_ascii_lowercase();
|
||||
qname == domain || qname.ends_with(&format!(".{domain}"))
|
||||
});
|
||||
return !allowed;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
Reference in New Issue
Block a user