Skip to main content

vcl_protocol/
tunnel.rs

1//! # VCL Tunnel
2//!
3//! [`VCLTunnel`] is a high-level facade that combines all VCL components
4//! into a single easy-to-use object for building VPN applications.
5//!
6//! Instead of manually wiring together `VCLConnection`, `VCLTun`,
7//! `KeepaliveManager`, `ReconnectManager`, `DnsFilter`, `Obfuscator`,
8//! and `MtuNegotiator` — just create a `VCLTunnel`.
9//!
10//! ## Example
11//!
12//! ```rust
13//! use vcl_protocol::tunnel::{VCLTunnel, TunnelConfig};
14//!
15//! #[tokio::main]
16//! async fn main() {
17//!     let config = TunnelConfig::mobile("10.0.0.1", "10.0.0.2");
18//!     println!("Tunnel config created: {:?}", config.obfuscation_mode);
19//! }
20//! ```
21
22use crate::error::VCLError;
23use crate::keepalive::{KeepaliveConfig, KeepaliveManager, KeepaliveAction, KeepalivePreset};
24use crate::reconnect::{ReconnectConfig, ReconnectManager};
25use crate::dns::{DnsConfig, DnsFilter, DnsAction, DnsQueryType};
26use crate::obfuscation::{ObfuscationConfig, ObfuscationMode, Obfuscator, recommended_mode};
27use crate::mtu::{MtuConfig, MtuNegotiator};
28use crate::metrics::VCLMetrics;
29use std::net::IpAddr;
30use std::time::{Duration, Instant};
31use tracing::{debug, info, warn};
32
33/// High-level tunnel configuration.
34#[derive(Debug, Clone)]
35pub struct TunnelConfig {
36    /// Local TUN interface IP address.
37    pub local_ip: String,
38    /// Remote peer IP address.
39    pub remote_ip: String,
40    /// MTU for the tunnel interface.
41    pub mtu: u16,
42    /// Obfuscation mode for DPI bypass.
43    pub obfuscation_mode: ObfuscationMode,
44    /// Keepalive preset.
45    pub keepalive: KeepalivePreset,
46    /// Whether to enable DNS leak protection.
47    pub dns_protection: bool,
48    /// DNS upstream servers.
49    pub dns_servers: Vec<String>,
50    /// Domains to block via DNS.
51    pub blocked_domains: Vec<String>,
52    /// Domains that bypass the tunnel (split DNS).
53    pub split_domains: Vec<String>,
54    /// Maximum reconnect attempts (None = infinite).
55    pub max_reconnect_attempts: Option<u32>,
56}
57
58impl TunnelConfig {
59    /// Configuration optimised for mobile networks (МТС, Beeline).
60    /// Full obfuscation, aggressive keepalive, mobile reconnect.
61    pub fn mobile(local_ip: &str, remote_ip: &str) -> Self {
62        TunnelConfig {
63            local_ip: local_ip.to_string(),
64            remote_ip: remote_ip.to_string(),
65            mtu: 1380,
66            obfuscation_mode: ObfuscationMode::Full,
67            keepalive: KeepalivePreset::Mobile,
68            dns_protection: true,
69            dns_servers: vec![
70                "1.1.1.1:53".to_string(),
71                "1.0.0.1:53".to_string(),
72            ],
73            blocked_domains: Vec::new(),
74            split_domains: Vec::new(),
75            max_reconnect_attempts: Option::None,
76        }
77    }
78
79    /// Configuration for home broadband.
80    pub fn home(local_ip: &str, remote_ip: &str) -> Self {
81        TunnelConfig {
82            local_ip: local_ip.to_string(),
83            remote_ip: remote_ip.to_string(),
84            mtu: 1420,
85            obfuscation_mode: ObfuscationMode::TlsMimicry,
86            keepalive: KeepalivePreset::Home,
87            dns_protection: true,
88            dns_servers: vec![
89                "1.1.1.1:53".to_string(),
90            ],
91            blocked_domains: Vec::new(),
92            split_domains: Vec::new(),
93            max_reconnect_attempts: Some(10),
94        }
95    }
96
97    /// Configuration for corporate/office networks.
98    pub fn corporate(local_ip: &str, remote_ip: &str) -> Self {
99        TunnelConfig {
100            local_ip: local_ip.to_string(),
101            remote_ip: remote_ip.to_string(),
102            mtu: 1400,
103            obfuscation_mode: ObfuscationMode::Http2Mimicry,
104            keepalive: KeepalivePreset::Corporate,
105            dns_protection: true,
106            dns_servers: vec![
107                "8.8.8.8:53".to_string(),
108            ],
109            blocked_domains: Vec::new(),
110            split_domains: Vec::new(),
111            max_reconnect_attempts: Some(5),
112        }
113    }
114
115    /// Auto-detect configuration from network hint string.
116    pub fn auto(local_ip: &str, remote_ip: &str, network_hint: &str) -> Self {
117        let mode = recommended_mode(network_hint);
118        let keepalive = match network_hint.to_lowercase().as_str() {
119            "mobile" | "mts" | "beeline" | "megafon" => KeepalivePreset::Mobile,
120            "corporate" | "office"                    => KeepalivePreset::Corporate,
121            _                                         => KeepalivePreset::Home,
122        };
123        TunnelConfig {
124            local_ip: local_ip.to_string(),
125            remote_ip: remote_ip.to_string(),
126            mtu: 1400,
127            obfuscation_mode: mode,
128            keepalive,
129            dns_protection: true,
130            dns_servers: vec!["1.1.1.1:53".to_string()],
131            blocked_domains: Vec::new(),
132            split_domains: Vec::new(),
133            max_reconnect_attempts: Option::None,
134        }
135    }
136
137    /// Add a domain to block via DNS.
138    pub fn block_domain(mut self, domain: &str) -> Self {
139        self.blocked_domains.push(domain.to_string());
140        self
141    }
142
143    /// Add a split DNS domain (bypasses tunnel).
144    pub fn split_domain(mut self, domain: &str) -> Self {
145        self.split_domains.push(domain.to_string());
146        self
147    }
148
149    /// Set custom DNS servers.
150    pub fn with_dns(mut self, servers: Vec<&str>) -> Self {
151        self.dns_servers = servers.iter().map(|s| s.to_string()).collect();
152        self
153    }
154}
155
156/// Current state of the tunnel.
157#[derive(Debug, Clone, PartialEq)]
158pub enum TunnelState {
159    /// Tunnel is stopped.
160    Stopped,
161    /// Tunnel is connecting.
162    Connecting,
163    /// Tunnel is active and passing traffic.
164    Connected,
165    /// Tunnel lost connection and is attempting to reconnect.
166    Reconnecting,
167    /// Tunnel has permanently failed.
168    Failed,
169}
170
171/// Statistics snapshot from the tunnel.
172#[derive(Debug, Clone)]
173pub struct TunnelStats {
174    /// Current tunnel state.
175    pub state: TunnelState,
176    /// Total bytes sent through the tunnel.
177    pub bytes_sent: u64,
178    /// Total bytes received through the tunnel.
179    pub bytes_received: u64,
180    /// Current packet loss rate (0.0–1.0).
181    pub loss_rate: f64,
182    /// Average RTT measured by keepalive pings.
183    pub keepalive_rtt: Option<Duration>,
184    /// Number of reconnections since tunnel started.
185    pub reconnect_count: u64,
186    /// Number of DNS queries intercepted.
187    pub dns_intercepted: u64,
188    /// Number of DNS queries blocked.
189    pub dns_blocked: u64,
190    /// Obfuscation overhead ratio.
191    pub obfuscation_overhead: f64,
192    /// How long the tunnel has been running.
193    pub uptime: Duration,
194    /// Current MTU in use.
195    pub mtu: u16,
196}
197
198/// High-level VPN tunnel facade.
199///
200/// Combines `KeepaliveManager`, `ReconnectManager`, `DnsFilter`,
201/// `Obfuscator`, `MtuNegotiator`, and `VCLMetrics` into one object.
202pub struct VCLTunnel {
203    config: TunnelConfig,
204    state: TunnelState,
205    keepalive: KeepaliveManager,
206    reconnect: ReconnectManager,
207    dns: DnsFilter,
208    obfuscator: Obfuscator,
209    mtu: MtuNegotiator,
210    metrics: VCLMetrics,
211    started_at: Option<Instant>,
212    reconnect_count: u64,
213}
214
215impl VCLTunnel {
216    /// Create a new tunnel with the given configuration.
217    pub fn new(config: TunnelConfig) -> Self {
218        let keepalive = KeepaliveManager::from_preset(config.keepalive.clone());
219
220        let reconnect_config = ReconnectConfig {
221            max_attempts: config.max_reconnect_attempts,
222            ..match config.keepalive {
223                KeepalivePreset::Mobile     => ReconnectConfig::mobile(),
224                KeepalivePreset::Corporate  => ReconnectConfig::stable(),
225                _                           => ReconnectConfig::default(),
226            }
227        };
228        let reconnect = ReconnectManager::new(reconnect_config);
229
230        let mut dns_config = DnsConfig {
231            upstream_servers: config.dns_servers.clone(),
232            split_dns_domains: config.split_domains.clone(),
233            blocked_domains: config.blocked_domains.clone(),
234            enable_cache: true,
235            cache_ttl: Duration::from_secs(300),
236            max_cache_size: 1024,
237        };
238        let dns = DnsFilter::new(dns_config);
239
240        let obf_config = match &config.obfuscation_mode {
241            ObfuscationMode::None             => ObfuscationConfig::none(),
242            ObfuscationMode::Padding          => ObfuscationConfig::padding(),
243            ObfuscationMode::SizeNormalization => ObfuscationConfig::size_normalization(),
244            ObfuscationMode::TlsMimicry       => ObfuscationConfig::tls_mimicry(),
245            ObfuscationMode::Http2Mimicry     => ObfuscationConfig::http2_mimicry(),
246            ObfuscationMode::Full             => ObfuscationConfig::full(),
247        };
248        let obfuscator = Obfuscator::new(obf_config);
249
250        let mtu_config = MtuConfig {
251            start_mtu: config.mtu as usize,
252            max_mtu: config.mtu as usize,
253            ..MtuConfig::default()
254        };
255        let mut mtu = MtuNegotiator::new(mtu_config);
256        mtu.set_mtu(config.mtu as usize);
257
258        info!(
259            local = %config.local_ip,
260            remote = %config.remote_ip,
261            mtu = config.mtu,
262            obfuscation = ?config.obfuscation_mode,
263            "VCLTunnel created"
264        );
265
266        VCLTunnel {
267            config,
268            state: TunnelState::Stopped,
269            keepalive,
270            reconnect,
271            dns,
272            obfuscator,
273            mtu,
274            metrics: VCLMetrics::new(),
275            started_at: Option::None,
276            reconnect_count: 0,
277        }
278    }
279
280    // ─── Lifecycle ────────────────────────────────────────────────────────────
281
282    /// Mark the tunnel as connecting.
283    pub fn on_connecting(&mut self) {
284        self.state = TunnelState::Connecting;
285        self.started_at = Some(Instant::now());
286        info!(remote = %self.config.remote_ip, "VCLTunnel connecting");
287    }
288
289    /// Mark the tunnel as connected. Resets reconnect backoff.
290    pub fn on_connected(&mut self) {
291        self.state = TunnelState::Connected;
292        self.reconnect.on_connect();
293        self.metrics.record_handshake();
294        info!(remote = %self.config.remote_ip, "VCLTunnel connected");
295    }
296
297    /// Mark the tunnel as disconnected. Starts reconnect backoff.
298    pub fn on_disconnected(&mut self) {
299        self.state = TunnelState::Reconnecting;
300        self.reconnect.on_disconnect();
301        warn!(remote = %self.config.remote_ip, "VCLTunnel disconnected");
302    }
303
304    /// Mark the tunnel as permanently failed.
305    pub fn on_failed(&mut self) {
306        self.state = TunnelState::Failed;
307        warn!("VCLTunnel permanently failed");
308    }
309
310    /// Stop the tunnel.
311    pub fn stop(&mut self) {
312        self.state = TunnelState::Stopped;
313        info!("VCLTunnel stopped");
314    }
315
316    // ─── Keepalive ────────────────────────────────────────────────────────────
317
318    /// Check keepalive — call every second in your main loop.
319    ///
320    /// Returns the action to take.
321    pub fn check_keepalive(&mut self) -> KeepaliveAction {
322        self.keepalive.check()
323    }
324
325    /// Record that a keepalive ping was sent.
326    pub fn keepalive_sent(&mut self) {
327        self.keepalive.record_keepalive_sent();
328    }
329
330    /// Record that a pong was received.
331    pub fn keepalive_pong_received(&mut self) {
332        self.keepalive.record_pong_received();
333        if let Some(rtt) = self.keepalive.srtt() {
334            debug!(rtt_ms = rtt.as_millis(), "Keepalive RTT updated");
335        }
336    }
337
338    /// Record any data activity — resets keepalive timer.
339    pub fn record_activity(&mut self) {
340        self.keepalive.record_activity();
341    }
342
343    // ─── Reconnect ────────────────────────────────────────────────────────────
344
345    /// Returns true if it's time to attempt reconnection.
346    pub fn should_reconnect(&mut self) -> bool {
347        self.reconnect.should_reconnect()
348    }
349
350    /// Call when a reconnect attempt starts.
351    pub fn reconnect_attempt_start(&mut self) {
352        self.reconnect.on_attempt_start();
353        self.reconnect_count += 1;
354        info!(attempt = self.reconnect.attempts(), "Reconnect attempt starting");
355    }
356
357    /// Call when a reconnect attempt fails.
358    pub fn reconnect_failed(&mut self) {
359        self.reconnect.on_failure();
360        if self.reconnect.is_giving_up() {
361            self.on_failed();
362        }
363    }
364
365    /// Returns true if the reconnect manager has given up.
366    pub fn is_giving_up(&self) -> bool {
367        self.reconnect.is_giving_up()
368    }
369
370    /// How long until next reconnect attempt.
371    pub fn time_until_reconnect(&self) -> Duration {
372        self.reconnect.time_until_reconnect()
373    }
374
375    // ─── Obfuscation ──────────────────────────────────────────────────────────
376
377    /// Obfuscate outgoing packet data.
378    pub fn obfuscate(&mut self, data: &[u8]) -> Vec<u8> {
379        let result = self.obfuscator.obfuscate(data);
380        self.metrics.record_sent(data.len());
381        result
382    }
383
384    /// Deobfuscate incoming packet data.
385    pub fn deobfuscate(&mut self, data: &[u8]) -> Result<Vec<u8>, VCLError> {
386        let result = self.obfuscator.deobfuscate(data)?;
387        self.metrics.record_received(result.len());
388        Ok(result)
389    }
390
391    /// Returns jitter delay in ms to apply before sending.
392    pub fn jitter_ms(&self) -> u64 {
393        self.obfuscator.jitter_ms()
394    }
395
396    // ─── DNS ──────────────────────────────────────────────────────────────────
397
398    /// Decide what to do with a DNS query for the given domain.
399    pub fn dns_decide(&mut self, domain: &str) -> DnsAction {
400        self.dns.decide(domain, &DnsQueryType::A)
401    }
402
403    /// Cache a DNS response.
404    pub fn dns_cache(&mut self, domain: &str, addr: IpAddr) {
405        self.dns.cache_response(domain, addr);
406    }
407
408    /// Returns true if a raw UDP payload looks like a DNS packet.
409    pub fn is_dns_packet(data: &[u8]) -> bool {
410        DnsFilter::is_dns_packet(data)
411    }
412
413    /// Block a domain at runtime.
414    pub fn block_domain(&mut self, domain: &str) {
415        self.dns.block_domain(domain);
416    }
417
418    /// Add a split DNS domain at runtime.
419    pub fn add_split_domain(&mut self, domain: &str) {
420        self.dns.add_split_domain(domain);
421    }
422
423    // ─── MTU ──────────────────────────────────────────────────────────────────
424
425    /// Returns the current recommended fragment size.
426    pub fn fragment_size(&self) -> usize {
427        self.mtu.recommended_fragment_size()
428    }
429
430    /// Returns the current path MTU.
431    pub fn current_mtu(&self) -> usize {
432        self.mtu.current_mtu()
433    }
434
435    /// Update MTU from external discovery.
436    pub fn set_mtu(&mut self, mtu: usize) {
437        self.mtu.set_mtu(mtu);
438        info!(mtu, "VCLTunnel MTU updated");
439    }
440
441    // ─── Metrics ──────────────────────────────────────────────────────────────
442
443    /// Record a retransmitted packet.
444    pub fn record_retransmit(&mut self) {
445        self.metrics.record_retransmit();
446    }
447
448    /// Returns a statistics snapshot.
449    pub fn stats(&self) -> TunnelStats {
450        TunnelStats {
451            state: self.state.clone(),
452            bytes_sent: self.metrics.bytes_sent,
453            bytes_received: self.metrics.bytes_received,
454            loss_rate: self.metrics.loss_rate(),
455            keepalive_rtt: self.keepalive.srtt(),
456            reconnect_count: self.reconnect_count,
457            dns_intercepted: self.dns.total_intercepted(),
458            dns_blocked: self.dns.total_blocked(),
459            obfuscation_overhead: self.obfuscator.overhead_ratio(),
460            uptime: self.started_at.map(|t| t.elapsed()).unwrap_or(Duration::ZERO),
461            mtu: self.mtu.current_mtu() as u16,
462        }
463    }
464
465    /// Returns the current tunnel state.
466    pub fn state(&self) -> &TunnelState {
467        &self.state
468    }
469
470    /// Returns true if the tunnel is currently connected.
471    pub fn is_connected(&self) -> bool {
472        self.state == TunnelState::Connected
473    }
474
475    /// Returns a reference to the config.
476    pub fn config(&self) -> &TunnelConfig {
477        &self.config
478    }
479
480    /// Returns a reference to the raw metrics.
481    pub fn metrics(&self) -> &VCLMetrics {
482        &self.metrics
483    }
484}
485
486#[cfg(test)]
487mod tests {
488    use super::*;
489
490    fn mobile_tunnel() -> VCLTunnel {
491        VCLTunnel::new(TunnelConfig::mobile("10.0.0.1", "10.0.0.2"))
492    }
493
494    fn home_tunnel() -> VCLTunnel {
495        VCLTunnel::new(TunnelConfig::home("10.0.0.1", "10.0.0.2"))
496    }
497
498    fn corporate_tunnel() -> VCLTunnel {
499        VCLTunnel::new(TunnelConfig::corporate("10.0.0.1", "10.0.0.2"))
500    }
501
502    // ─── Config tests ──────────────────────────────────────────────────────────
503
504    #[test]
505    fn test_mobile_config() {
506        let c = TunnelConfig::mobile("10.0.0.1", "10.0.0.2");
507        assert_eq!(c.local_ip, "10.0.0.1");
508        assert_eq!(c.remote_ip, "10.0.0.2");
509        assert_eq!(c.mtu, 1380);
510        assert_eq!(c.obfuscation_mode, ObfuscationMode::Full);
511        assert!(c.dns_protection);
512        assert!(c.max_reconnect_attempts.is_none());
513    }
514
515    #[test]
516    fn test_home_config() {
517        let c = TunnelConfig::home("10.0.0.1", "10.0.0.2");
518        assert_eq!(c.mtu, 1420);
519        assert_eq!(c.obfuscation_mode, ObfuscationMode::TlsMimicry);
520        assert_eq!(c.max_reconnect_attempts, Some(10));
521    }
522
523    #[test]
524    fn test_corporate_config() {
525        let c = TunnelConfig::corporate("10.0.0.1", "10.0.0.2");
526        assert_eq!(c.obfuscation_mode, ObfuscationMode::Http2Mimicry);
527        assert_eq!(c.max_reconnect_attempts, Some(5));
528    }
529
530    #[test]
531    fn test_auto_config_mobile() {
532        let c = TunnelConfig::auto("10.0.0.1", "10.0.0.2", "mts");
533        assert_eq!(c.obfuscation_mode, ObfuscationMode::Full);
534    }
535
536    #[test]
537    fn test_auto_config_home() {
538        let c = TunnelConfig::auto("10.0.0.1", "10.0.0.2", "home");
539        assert_eq!(c.obfuscation_mode, ObfuscationMode::TlsMimicry);
540    }
541
542    #[test]
543    fn test_config_block_domain() {
544        let c = TunnelConfig::mobile("10.0.0.1", "10.0.0.2")
545            .block_domain("ads.com")
546            .block_domain("tracking.io");
547        assert_eq!(c.blocked_domains.len(), 2);
548    }
549
550    #[test]
551    fn test_config_split_domain() {
552        let c = TunnelConfig::mobile("10.0.0.1", "10.0.0.2")
553            .split_domain("corp.internal");
554        assert_eq!(c.split_domains.len(), 1);
555    }
556
557    #[test]
558    fn test_config_with_dns() {
559        let c = TunnelConfig::mobile("10.0.0.1", "10.0.0.2")
560            .with_dns(vec!["8.8.8.8:53", "8.8.4.4:53"]);
561        assert_eq!(c.dns_servers.len(), 2);
562    }
563
564    // ─── Lifecycle tests ───────────────────────────────────────────────────────
565
566    #[test]
567    fn test_initial_state() {
568        let t = mobile_tunnel();
569        assert_eq!(t.state(), &TunnelState::Stopped);
570        assert!(!t.is_connected());
571    }
572
573    #[test]
574    fn test_on_connecting() {
575        let mut t = mobile_tunnel();
576        t.on_connecting();
577        assert_eq!(t.state(), &TunnelState::Connecting);
578    }
579
580    #[test]
581    fn test_on_connected() {
582        let mut t = mobile_tunnel();
583        t.on_connecting();
584        t.on_connected();
585        assert_eq!(t.state(), &TunnelState::Connected);
586        assert!(t.is_connected());
587    }
588
589    #[test]
590    fn test_on_disconnected() {
591        let mut t = mobile_tunnel();
592        t.on_connecting();
593        t.on_connected();
594        t.on_disconnected();
595        assert_eq!(t.state(), &TunnelState::Reconnecting);
596        assert!(!t.is_connected());
597    }
598
599    #[test]
600    fn test_stop() {
601        let mut t = mobile_tunnel();
602        t.on_connecting();
603        t.on_connected();
604        t.stop();
605        assert_eq!(t.state(), &TunnelState::Stopped);
606    }
607
608    #[test]
609    fn test_on_failed() {
610        let mut t = mobile_tunnel();
611        t.on_failed();
612        assert_eq!(t.state(), &TunnelState::Failed);
613    }
614
615    // ─── Obfuscation tests ─────────────────────────────────────────────────────
616
617    #[test]
618    fn test_obfuscate_deobfuscate() {
619        let mut t = mobile_tunnel();
620        let data = b"secret tunnel data";
621        let obfuscated = t.obfuscate(data);
622        let restored = t.deobfuscate(&obfuscated).unwrap();
623        assert_eq!(restored, data);
624    }
625
626    #[test]
627    fn test_obfuscate_records_metrics() {
628        let mut t = mobile_tunnel();
629        t.obfuscate(b"hello");
630        t.obfuscate(b"world");
631        assert_eq!(t.metrics().bytes_sent, 10);
632    }
633
634    #[test]
635    fn test_deobfuscate_records_metrics() {
636        let mut t = mobile_tunnel();
637        let obf = t.obfuscate(b"hello");
638        t.deobfuscate(&obf).unwrap();
639        assert_eq!(t.metrics().bytes_received, 5);
640    }
641
642    #[test]
643    fn test_jitter_ms() {
644        let t = mobile_tunnel();
645        assert!(t.jitter_ms() <= 15); // full mode max jitter
646    }
647
648    // ─── DNS tests ─────────────────────────────────────────────────────────────
649
650    #[test]
651    fn test_dns_forward() {
652        let mut t = home_tunnel();
653        let action = t.dns_decide("example.com");
654        assert_eq!(action, DnsAction::ForwardThroughTunnel);
655    }
656
657    #[test]
658    fn test_dns_block_runtime() {
659        let mut t = home_tunnel();
660        t.block_domain("evil.com");
661        let action = t.dns_decide("evil.com");
662        assert_eq!(action, DnsAction::Block);
663    }
664
665    #[test]
666    fn test_dns_block_from_config() {
667        let config = TunnelConfig::home("10.0.0.1", "10.0.0.2")
668            .block_domain("ads.com");
669        let mut t = VCLTunnel::new(config);
670        assert_eq!(t.dns_decide("ads.com"), DnsAction::Block);
671    }
672
673    #[test]
674    fn test_dns_split_from_config() {
675        let config = TunnelConfig::home("10.0.0.1", "10.0.0.2")
676            .split_domain("corp.internal");
677        let mut t = VCLTunnel::new(config);
678        assert_eq!(t.dns_decide("host.corp.internal"), DnsAction::AllowDirect);
679    }
680
681    #[test]
682    fn test_dns_cache() {
683        let mut t = home_tunnel();
684        let addr: IpAddr = "1.2.3.4".parse().unwrap();
685        t.dns_cache("cached.com", addr);
686        let action = t.dns_decide("cached.com");
687        assert_eq!(action, DnsAction::ReturnCached(addr));
688    }
689
690    #[test]
691    fn test_is_dns_packet() {
692        let pkt = vec![
693            0x00, 0x01, 0x01, 0x00, 0x00, 0x01,
694            0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
695        ];
696        assert!(VCLTunnel::is_dns_packet(&pkt));
697        assert!(!VCLTunnel::is_dns_packet(&[0u8; 4]));
698    }
699
700    // ─── MTU tests ─────────────────────────────────────────────────────────────
701
702    #[test]
703    fn test_mtu_initial() {
704        let t = mobile_tunnel();
705        assert_eq!(t.current_mtu(), 1380);
706        assert!(t.fragment_size() > 0);
707        assert!(t.fragment_size() < 1380);
708    }
709
710    #[test]
711    fn test_set_mtu() {
712        let mut t = home_tunnel();
713        t.set_mtu(1280);
714        assert_eq!(t.current_mtu(), 1280);
715    }
716
717    // ─── Stats tests ───────────────────────────────────────────────────────────
718
719    #[test]
720    fn test_stats_initial() {
721        let t = mobile_tunnel();
722        let s = t.stats();
723        assert_eq!(s.bytes_sent, 0);
724        assert_eq!(s.bytes_received, 0);
725        assert_eq!(s.reconnect_count, 0);
726        assert_eq!(s.dns_intercepted, 0);
727        assert_eq!(s.loss_rate, 0.0);
728        assert_eq!(s.mtu, 1380);
729    }
730
731    #[test]
732    fn test_stats_after_traffic() {
733        let mut t = mobile_tunnel();
734        t.on_connecting();
735        t.on_connected();
736        let obf = t.obfuscate(b"hello world");
737        t.deobfuscate(&obf).unwrap();
738        let s = t.stats();
739        assert_eq!(s.bytes_sent, 11);
740        assert_eq!(s.bytes_received, 11);
741        assert_eq!(s.state, TunnelState::Connected);
742        assert!(s.uptime > Duration::ZERO);
743    }
744
745    #[test]
746    fn test_stats_dns_counts() {
747        let mut t = home_tunnel();
748        t.block_domain("bad.com");
749        t.dns_decide("good.com");
750        t.dns_decide("bad.com");
751        let s = t.stats();
752        assert_eq!(s.dns_intercepted, 2);
753        assert_eq!(s.dns_blocked, 1);
754    }
755
756    #[test]
757    fn test_reconnect_count_increments() {
758        let mut t = mobile_tunnel();
759        t.on_connected();
760        t.on_disconnected();
761        t.reconnect_attempt_start();
762        t.reconnect_attempt_start();
763        assert_eq!(t.reconnect_count, 2);
764    }
765
766    // ─── Reconnect tests ───────────────────────────────────────────────────────
767
768    #[test]
769    fn test_reconnect_after_disconnect() {
770        let mut t = VCLTunnel::new(TunnelConfig {
771            max_reconnect_attempts: Some(3),
772            ..TunnelConfig::home("10.0.0.1", "10.0.0.2")
773        });
774        t.on_connected();
775        t.on_disconnected();
776        assert!(!t.is_giving_up());
777    }
778
779    #[test]
780    fn test_giving_up_after_max_attempts() {
781        let mut t = VCLTunnel::new(TunnelConfig {
782            max_reconnect_attempts: Some(1),
783            ..TunnelConfig::home("10.0.0.1", "10.0.0.2")
784        });
785        t.on_connected();
786        t.on_disconnected();
787        t.reconnect_failed();
788        assert!(t.is_giving_up());
789        assert_eq!(t.state(), &TunnelState::Failed);
790    }
791
792    #[test]
793    fn test_config_ref() {
794        let t = mobile_tunnel();
795        assert_eq!(t.config().local_ip, "10.0.0.1");
796        assert_eq!(t.config().remote_ip, "10.0.0.2");
797    }
798
799    #[test]
800    fn test_all_three_presets_create_ok() {
801        let _m = mobile_tunnel();
802        let _h = home_tunnel();
803        let _c = corporate_tunnel();
804    }
805}