use std::net::IpAddr; use crate::config::{RoutingConfig, RoutingRule}; #[derive(Debug, Clone)] pub struct Session { pub inbound_tag: String, pub source_ip: Option, pub destination_ip: Option, pub destination_port: u16, pub protocol: String, // "tcp" or "udp" pub sni: Option, pub process_name: Option, } pub struct Router { config: RoutingConfig, } impl Router { pub fn new(config: RoutingConfig) -> Self { Self { config } } /// Evaluates the session against routing rules and returns the outbound tag pub fn route(&self, session: &Session) -> String { for rule in &self.config.rules { if self.match_rule(rule, session) { return rule.outbound.clone(); } } self.config.default_outbound.clone() } fn match_rule(&self, rule: &RoutingRule, session: &Session) -> bool { // All specified conditions in a rule must match (AND logic) let mut matched_any_condition = false; // 1. Inbound Tag match if let Some(inbounds) = &rule.inbound_tag { if !inbounds.iter().any(|tag| tag == &session.inbound_tag) { return false; } matched_any_condition = true; } // 2. Domain / SNI match if let Some(domains) = &rule.domain_suffix { let mut domain_match = false; if let Some(sni) = &session.sni { let sni_lower = sni.to_lowercase(); domain_match = domains.iter().any(|d| { let d_lower = d.to_lowercase(); sni_lower == d_lower || sni_lower.ends_with(&format!(".{}", d_lower)) }); } if !domain_match { return false; } matched_any_condition = true; } // 3. Process match if let Some(processes) = &rule.process_name { let mut proc_match = false; if let Some(proc) = &session.process_name { let proc_lower = proc.to_lowercase(); proc_match = processes.iter().any(|p| { let p_lower = p.to_lowercase(); proc_lower.contains(&p_lower) }); } if !proc_match { return false; } matched_any_condition = true; } // 4. IP CIDR match if let Some(cidrs) = &rule.ip_cidr { let mut ip_match = false; if let Some(dst_ip) = session.destination_ip { ip_match = cidrs.iter().any(|cidr| { match ipnet::IpNet::from_str(cidr) { Ok(net) => net.contains(&dst_ip), Err(_) => { // fallback to exact ip match if not a valid CIDR if let Ok(ip) = cidr.parse::() { ip == dst_ip } else { false } } } }); } if !ip_match { return false; } matched_any_condition = true; } // A rule must have at least one condition to match matched_any_condition } } use std::str::FromStr; #[cfg(test)] mod tests { use super::*; #[test] fn test_router() { let rules = vec![ RoutingRule { domain_suffix: Some(vec!["vk.com".to_string()]), ip_cidr: None, process_name: None, inbound_tag: None, outbound: "direct".to_string(), }, RoutingRule { domain_suffix: None, ip_cidr: None, process_name: Some(vec!["telegram.exe".to_string()]), inbound_tag: None, outbound: "proxy-group".to_string(), }, ]; let config = RoutingConfig { rules, default_outbound: "proxy-group".to_string(), }; let router = Router::new(config); let mut session = Session { inbound_tag: "tun-in".to_string(), source_ip: None, destination_ip: None, destination_port: 443, protocol: "tcp".to_string(), sni: Some("api.vk.com".to_string()), process_name: None, }; assert_eq!(router.route(&session), "direct"); session.sni = None; session.process_name = Some("C:\\App\\Telegram.exe".to_string()); assert_eq!(router.route(&session), "proxy-group"); session.process_name = Some("chrome.exe".to_string()); assert_eq!(router.route(&session), "proxy-group"); // fallback } }