11use netdev:: Interface ;
2+ use netdev:: interface:: InterfaceType ;
3+
4+ /// Common patterns that indicate a VPN/tunnel adapter
5+ const VPN_NAME_PATTERNS : & [ & str ] = & [
6+ "tun" ,
7+ "tap" ,
8+ "wg" ,
9+ "tailscale" ,
10+ "zerotier" ,
11+ "zt" ,
12+ "openvpn" ,
13+ "ovpn" ,
14+ "ipsec" ,
15+ "utun" ,
16+ "vpn" ,
17+ "adapter" ,
18+ "wan miniport" ,
19+ "nord" ,
20+ "expressvpn" ,
21+ ] ;
222
323pub fn collect_all_interfaces ( ) -> Vec < Interface > {
424 netdev:: get_interfaces ( )
@@ -20,3 +40,97 @@ pub fn get_interface_by_name(name: &str) -> Option<Interface> {
2040 }
2141 None
2242}
43+
44+ #[ derive( Debug ) ]
45+ pub struct VpnHeuristic {
46+ pub is_vpn_like : bool ,
47+ #[ allow( dead_code) ]
48+ pub score : i32 ,
49+ #[ allow( dead_code) ]
50+ pub signals : Vec < String > ,
51+ }
52+
53+ /// Check if the given interface looks like a VPN interface using simple heuristics.
54+ pub fn detect_vpn_like ( default_if : & Interface ) -> VpnHeuristic {
55+ let mut score = 0 ;
56+ let mut sig = Vec :: new ( ) ;
57+
58+ // Check InterfaceType
59+ match default_if. if_type {
60+ InterfaceType :: Tunnel | InterfaceType :: Ppp | InterfaceType :: ProprietaryVirtual => {
61+ score += 4 ;
62+ sig. push ( format ! ( "type={:?}" , default_if. if_type) ) ;
63+ }
64+ _ => { }
65+ }
66+
67+ // Check name patterns
68+ let name = default_if. name . to_lowercase ( ) ;
69+ if VPN_NAME_PATTERNS . iter ( ) . any ( |p| name. contains ( p) ) {
70+ score += 3 ;
71+ sig. push ( format ! ( "name={}" , default_if. name) ) ;
72+ }
73+
74+ // Check friendly_name patterns
75+ if let Some ( fname) = & default_if. friendly_name {
76+ let fname_lower = fname. to_lowercase ( ) ;
77+ if VPN_NAME_PATTERNS . iter ( ) . any ( |p| fname_lower. contains ( p) ) {
78+ score += 3 ;
79+ sig. push ( format ! ( "friendly_name={}" , fname) ) ;
80+ }
81+ }
82+
83+ // Check MTU
84+ if let Some ( mtu) = default_if. mtu {
85+ if mtu < 1500 {
86+ // Likely VPN MTU
87+ score += if ( 1410 ..=1460 ) . contains ( & mtu) { 2 } else { 1 } ;
88+ sig. push ( format ! ( "mtu={}" , mtu) ) ;
89+ }
90+ }
91+
92+ // Check if IPv4 is 10/8 or 100.64/10
93+ let v4_inner_like = default_if. ipv4 . iter ( ) . any ( |n| {
94+ let ip = n. addr ( ) ;
95+ let oct = ip. octets ( ) ;
96+ oct[ 0 ] == 10 || ( oct[ 0 ] == 100 && ( oct[ 1 ] & 0b1100_0000 ) == 0b0100_0000 ) // 100.64.0.0/10
97+ } ) ;
98+ if v4_inner_like {
99+ score += 2 ;
100+ sig. push ( "ipv4=private(10/8 or 100.64/10)" . into ( ) ) ;
101+ }
102+
103+ // Check if DNS is 100.64/10
104+ let dns_any_100_64 = default_if. dns_servers . iter ( ) . any ( |ip| {
105+ if let std:: net:: IpAddr :: V4 ( v4) = ip {
106+ let o = v4. octets ( ) ;
107+ o[ 0 ] == 100 && ( o[ 1 ] & 0b1100_0000 ) == 0b0100_0000
108+ } else {
109+ false
110+ }
111+ } ) ;
112+ if dns_any_100_64 {
113+ score += 1 ;
114+ sig. push ( "dns=100.64.0.0/10" . into ( ) ) ;
115+ }
116+
117+ // Check if the type is clearly not physical
118+ match default_if. if_type {
119+ InterfaceType :: Ethernet
120+ | InterfaceType :: Wireless80211
121+ | InterfaceType :: GigabitEthernet
122+ | InterfaceType :: FastEthernetT
123+ | InterfaceType :: FastEthernetFx => { }
124+ _ => {
125+ score += 1 ;
126+ sig. push ( format ! ( "type-other={:?}" , default_if. if_type) ) ;
127+ }
128+ }
129+
130+ let is_vpn_like = score >= 5 ;
131+ VpnHeuristic {
132+ is_vpn_like,
133+ score,
134+ signals : sig,
135+ }
136+ }
0 commit comments