diff --git a/ISSUES.md b/ISSUES.md index edc85bb..2fd2a57 100644 --- a/ISSUES.md +++ b/ISSUES.md @@ -1,148 +1,284 @@ -# Issues & TODO +# Known Issues & Technical Debt -## 1. Critical +> SoftEther VPN Rust Client - Issue Tracker +> Last updated: January 2026 -_No critical issues._ +--- + +## πŸ“Š Summary + +| Category | High | Medium | Low | Total | +|----------|------|--------|-----|-------| +| Issues | 0 | 0 | 0 | 0 | +| Tech Debt | 0 | 1 | 2 | 3 | +| Performance | 0 | 0 | 3 | 3 | +| Missing Features | - | - | - | 7 | + +--- + +## πŸ› Issues + +### High Severity + +*No high severity issues currently open.* + +--- + +### Medium Severity + +*No medium severity issues currently open.* + +--- + +### Low Severity + +*No low severity issues currently open.* + +--- + +## πŸ”§ Technical Debt + +### Medium Priority + +#### DEBT-5: Missing Integration Tests for Multi-Connection *(Fixed)* + +**Status:** RESOLVED - Added 28 unit tests covering multi-connection logic. + +**Test Coverage Added:** +| Module | Tests | Coverage | +|--------|-------|----------| +| `multi_connection.rs` | 17 | TcpDirection, ConnectionStats, round-robin, extraction | +| `concurrent_reader.rs` | 11 | ReceivedPacket, shutdown flags, bytes tracking | + +**Tested Scenarios:** +- TcpDirection parsing from server (0=Both, 1=S2C, 2=C2S) +- Half-connection mode direction assignment +- Connection distribution for 4 and 8 connections +- Round-robin send/recv selection +- Connection extraction for ConcurrentReader +- Per-connection RC4 cipher independence +- Pack format for additional_connect method +- Shutdown flag propagation across tasks +- Connection index preservation + +--- + +#### DEBT-6: Windows TUN Incomplete +**Location:** `src/adapter/windows.rs` + +Missing DNS configuration and route cleanup on drop. + +--- + +### Low Priority + +#### DEBT-2: Large File - ffi/client.rs (2727 lines) *(Intentional Design)* + +**Status:** Closed - intentional architectural split. FFI layer has different I/O model (callbacks vs TUN). Shared logic already extracted to `TunnelCodec`, `DhcpClient`, `ConnectionManager`. + +--- + +#### DEBT-1: Large File - tunnel/runner.rs (2219 lines) *(Fixed)* + +**Status:** RESOLVED - Split into multiple focused modules. + +**Original File:** 2219 lines + +**New Structure:** +| File | Lines | Purpose | +|------|-------|---------| +| `runner.rs` | 329 | Core TunnelRunner struct, entry points, routes | +| `dhcp_handler.rs` | 384 | DHCP handling (single + multi-connection) | +| `single_conn.rs` | 697 | Single-connection data loop (Unix + Windows) | +| `multi_conn.rs` | 659 | Multi-connection data loop (half-connection mode) | +| `packet_processor.rs` | 209 | Shared packet processing utilities | + +Each file now has a single responsibility and is under 700 lines. + +--- + +#### DEBT-7: Multiple `unwrap()` in TLS Config *(Fixed)* +**Location:** `src/client/connection.rs:202-244` + +**Status:** RESOLVED - Replaced 4 `unwrap()` calls with proper `?` operator and `map_err()` to convert rustls errors to `Error::Tls`. + +**Changes:** +- All 4 `with_safe_default_protocol_versions().unwrap()` calls now use `.map_err(|e| Error::Tls(...))?` +- Errors now propagate with descriptive message: "Failed to set TLS protocol versions: {error}" + +--- + +#### DEBT-8: Duplicated Auth Pack Logic *(Fixed)* +**Location:** `src/protocol/auth.rs` + +**Status:** RESOLVED - Refactored `new()` and `new_ticket()` to use `add_client_fields()`. + +**Changes:** +- `AuthPack::new()`: Replaced ~75 lines of inline code with call to `add_client_fields()` +- `AuthPack::new_ticket()`: Replaced ~70 lines of inline code with call to `add_client_fields()` +- All 5 constructors now consistently use the shared helper function +- File reduced from 978 β†’ 872 lines (~106 lines removed) + +--- + +## πŸ“ˆ Performance + +### Medium Priority + +#### PERF-1: Buffer Pool for Receive Allocations *(Fixed)* +**Location:** `src/client/concurrent_reader.rs` + +**Status:** RESOLVED - Implemented `BufferPool` using pre-allocated `BytesMut` buffers. + +**Implementation:** +- `BufferPool` struct with channel-based buffer recycling +- Pre-allocates `BUFFERS_PER_READER * num_connections + channel_size` buffers +- Reader tasks grab from pool, read directly into `BytesMut`, freeze to `Bytes` (zero-copy) +- `try_reclaim()` method recycles `Bytes` back to `BytesMut` when sole owner +- Stats tracking: `pool_stats()` returns (hits, misses, hit_rate%) +- Graceful degradation: allocates new buffer when pool empty + +**Before:** `Bytes::copy_from_slice(&buf[..n])` - allocation per packet +**After:** `buf.freeze()` - zero-copy conversion from pooled BytesMut + +--- + +#### PERF-6: Upload Path Throttling and Latency Oscillation *(Fixed)* +**Location:** `src/client/multi_connection.rs` + +**Status:** RESOLVED - Implemented least-loaded connection selection and removed blocking flushes. + +**Root Causes:** +1. Round-robin selection didn't consider TCP buffer pressure +2. `flush().await` after every write blocked on TCP ACKs +3. No load balancing across send connections + +**Fixes:** +- Added `pending_send_bytes` tracking with time-based decay (~10 MB/s assumed) +- `get_send_connection()` now selects least-loaded connection +- Removed explicit `flush()` calls (TCP_NODELAY ensures immediate send) +- Added `record_write()` to track per-connection send load +- Added `estimated_pending()` with automatic decay calculation + +--- + +### Low Priority + +#### PERF-2: Redundant Compression Check +**Location:** `src/tunnel/runner.rs` + +Called for every packet even when compression is disabled. + +**Recommendation:** Check `use_compress` flag first. + +--- + +#### PERF-3: RC4 Batch Processing +**Location:** `src/crypto/rc4.rs` + +Single buffer at a time. Batching could reduce overhead. + +--- + +#### PERF-4: Fragment Reassembly HashMap Growth +**Location:** `src/packet/fragment.rs` + +HashMap grows unbounded until cleanup. Use `with_capacity()`. --- -## 2. iOS Integration +#### PERF-5: JNI String Allocations +**Location:** `src/ffi/android.rs` -_All iOS integration issues resolved._ +Multiple allocations when copying from JNI. Consider stack buffers. --- -## 3. Android +## πŸ“‹ Missing Features -_All Android issues resolved._ +| Feature | Status | Location | +|---------|--------|----------| +| Daemon Mode (CLI) | Not implemented | `src/main.rs` | +| DHCP Lease Renewal | Partial - timer not wired | `src/packet/dhcp.rs` | +| DHCPv6 Lease Renewal | Not implemented | `src/packet/dhcpv6.rs` | +| IPv6 Default Route | Not implemented | `src/tunnel/runner.rs` | +| Graceful Reconnection | Not implemented | `src/client/mod.rs` | +| UDP Accel V2 NAT-T | Partial | `src/net/udp_accel.rs` | +| Per-Connection Stats | Partial | `src/ffi/client.rs` | --- -## 4. Protocol +## πŸ—οΈ Architecture + +### βœ… Positive Patterns + +| Pattern | Description | +|---------|-------------| +| Zero-copy networking | `bytes::Bytes` and slice references throughout | +| Modular crypto | SHA-0, RC4, ChaCha20-Poly1305 in separate modules | +| Platform abstraction | `TunAdapter` trait for cross-platform TUN | +| Good test coverage | Most packet/protocol modules have unit tests | +| Clear separation | protocol/, packet/, net/, client/, ffi/ | +| Concurrent reader | Channel-based concurrent TCP reader | -### 4.1 UDP Acceleration βœ… RESOLVED -- βœ… UDP accel params sent during authentication (when `udp_accel: true`) -- βœ… Server UDP accel response parsed (`UdpAccelServerResponse`) -- βœ… UDP data path integrated into FFI packet loop -- βœ… V1 protocol (RC4 + SHA-1) fully implemented -- βœ… V2 protocol (ChaCha20-Poly1305 AEAD) fully implemented -- βœ… Automatic fallback to TCP when UDP not ready -- βœ… Parallel send/receive in packet loop -- Impact: Provides lower-latency tunnel when UDP path established +### ⚠️ Areas for Improvement + +| Issue | Impact | +|-------|--------| +| FFI/Desktop split | Two implementations instead of shared core | +| Large files | ~~runner.rs (2247)~~ and client.rs (2725) - runner.rs split, client.rs intentional | +| Error propagation | Mix of `Result`, `Option`, and panics | +| State machine clarity | Connection states could use proper FSM pattern | --- -## 5. Performance +## πŸ§ͺ Test Coverage + +| Module | Status | Notes | +|--------|--------|-------| +| `packet/arp.rs` | βœ… | Good coverage | +| `packet/dhcp.rs` | βœ… | State machine tests | +| `packet/dhcpv6.rs` | βœ… | Basic coverage | +| `packet/ethernet.rs` | βœ… | Zero-copy helpers | +| `packet/fragment.rs` | βœ… | Reassembly tested | +| `packet/qos.rs` | βœ… | Priority detection | +| `protocol/pack.rs` | βœ… | Serialization | +| `protocol/auth.rs` | βœ… | Auth pack building | +| `crypto/sha0.rs` | βœ… | Known test vectors | +| `crypto/rc4.rs` | βœ… | Stream cipher | +| `net/udp_accel.rs` | ⚠️ | Structure tests only | +| `tunnel/runner.rs` | ❌ | Complex, needs mocking | +| `ffi/client.rs` | ❌ | Needs integration tests | +| `client/mod.rs` | ⚠️ | Connection tests only | + +--- + +## βœ… Recently Fixed + +- [x] **PERF-6: Upload Path Throttling** (Jan 2026) - Replaced round-robin send selection with least-loaded connection selection using `pending_send_bytes` tracking. Removed blocking `flush()` calls that caused latency oscillation. TCP_NODELAY ensures immediate transmission. Added 7 tests (test count 154 β†’ 161). +- [x] **PERF-1: Buffer Pool for Receive Allocations** (Jan 2026) - Implemented `BufferPool` in `concurrent_reader.rs` using pre-allocated `BytesMut` buffers. Reader tasks grab from pool, read directly, freeze to `Bytes` (zero-copy). Includes `try_reclaim()` for buffer recycling and stats tracking. Added 9 new tests (test count 145 β†’ 154). +- [x] **DEBT-8: Duplicated Auth Pack Logic** (Jan 2026) - Refactored `AuthPack::new()` and `AuthPack::new_ticket()` to use `add_client_fields()` helper. All 5 auth constructors now share common code. File reduced from 978 β†’ 872 lines (~106 lines removed). +- [x] **DEBT-7: Multiple `unwrap()` in TLS Config** (Jan 2026) - Replaced 4 `unwrap()` calls in `create_tls_config()` with proper error handling using `map_err()` to convert rustls errors to `Error::Tls`. Errors now propagate correctly instead of panicking. +- [x] **DEBT-5: Missing Integration Tests for Multi-Connection** (Jan 2026) - Added 28 unit tests: 17 in `multi_connection.rs` (TcpDirection, ConnectionStats, round-robin, half-connection distribution, RC4 independence) and 11 in `concurrent_reader.rs` (ReceivedPacket, bytes tracking, shutdown flags, connection index preservation). Test count increased from 118 to 145. +- [x] **DEBT-1: Large File - tunnel/runner.rs** (Jan 2026) - Split into 5 modules: `runner.rs` (329 lines), `dhcp_handler.rs` (384 lines), `single_conn.rs` (697 lines), `multi_conn.rs` (659 lines), `packet_processor.rs` (209 lines). Each module has a single responsibility. +- [x] **DEBT-3: Duplicated Data Loop Code** (Jan 2026) - Created `src/tunnel/packet_processor.rs` with shared utilities (`init_arp`, `send_keepalive_if_needed`, `send_periodic_garp_if_needed`, `send_pending_arp_reply`, `send_frame_encrypted`, `build_ethernet_frame`). Updated both `run_data_loop_unix` and `run_data_loop_windows` to use shared functions, eliminating ~60 lines of duplication. +- [x] **DEBT-4: Inconsistent FFI Error Handling** (Jan 2026) - Added `softether_get_last_error()` and `softether_clear_last_error()` FFI functions. Thread-local storage holds detailed error message on failure. Updated `softether_create()` to set specific error messages (e.g., "config is null", "server is null or invalid UTF-8"). +- [x] **ISSUE-7: ARP Array Index Panic** (Jan 2026) - **Closed as safe code.** Line 201 validates `frame.len() >= 42` before any access. The slice `[arp_start+8..arp_start+14]` = `[22..28]` only needs indices 0-27, well within the 42-byte minimum. The `unwrap()` is unreachable. +- [x] **ISSUE-6: Password Hash Panic** (Jan 2026) - **Closed as safe code.** The `unwrap()` at line 524 is preceded by explicit length validation (lines 518-523) that returns `Error::Config` if not 20 bytes. The `unwrap()` is unreachable on invalid input. +- [x] **ISSUE-4: DHCP Response Race** (Jan 2026) - **Closed as works-as-designed.** FFI does DHCP on single bidirectional connection before additional connections. Desktop establishes all connections before DHCP. No race condition possible. +- [x] **ISSUE-8: UDP Accel Session Not Closed** (Jan 2026) - **Closed as works-as-designed.** Official SoftEther `FreeUdpAccel()` also relies on socket close + server keepalive timeout (9s). No explicit close packet in protocol. +- [x] **ISSUE-5: Thread-Local Storage in iOS FFI** (Jan 2026) - Changed `softether_ios_get_session()` and `softether_ios_get_stats()` to use caller-provided buffers instead of thread-local storage. Prevents stale data across threads and pointer invalidation. API now consistent with other iOS helper functions. +- [x] **ISSUE-3: Missing Timeout on Additional Connection** (Jan 2026) - Wrapped `establish_one_additional()` with `tokio::time::timeout()` using `config.timeout_seconds`. Prevents hanging if server accepts TCP but never responds to handshake. +- [x] **ISSUE-2: Frame Split Across Multi-Connections** (Jan 2026) - **Closed as not a bug.** Analysis of official SoftEther source (Connection.c) confirms each TCP socket has independent `RecvFifo` and frame parsing state. Server sends complete frames to individual connections (never splits across connections). Per-connection `TunnelCodec` is correct. +- [x] **ISSUE-1: RC4 Stream Corruption** (Jan 2026) - Implemented per-connection RC4 cipher state. Each `ManagedConnection` now has its own `TunnelEncryption` instance, matching server's per-socket encryption model. Files: `multi_connection.rs`, `concurrent_reader.rs`, `runner.rs` +- [x] **Config layer restructure** (Jan 2026) - Authentication moved to nested `auth` object with method selection (password, RADIUS, cert, anonymous) +- [x] **Marvin Attack** (RUSTSEC-2023-0071) - Hardened RSA fork +- [x] **Digest conflict** - Updated sha1 to 0.11.0-rc.3 + +--- -### 5.1 Packet Statistics βœ… RESOLVED -- βœ… `bytes_sent`, `bytes_received`, `packets_sent`, `packets_received` now incremented in FFI packet loop -- βœ… `packets_dropped` and `uptime_secs` work correctly -- βœ… Stats wrapped in `Arc` and passed through async task chain -- Impact: Resolved - mobile apps can now show accurate traffic statistics +## Contributing -### 5.2 ARP / Gateway MAC Learning βœ… RESOLVED -- βœ… Desktop learns gateway MAC via ARP responses -- βœ… Mobile now sends gratuitous ARP and gateway ARP request at tunnel start -- βœ… Incoming ARP packets processed to learn gateway MAC -- βœ… Outgoing frames rewritten to use learned gateway MAC (falls back to broadcast if not learned) -- Impact: Resolved - full parity with desktop ARP behavior - -### 5.3 QoS Packet Prioritization βœ… RESOLVED -- βœ… `qos` config flag is parsed and sent to server -- βœ… QoS module implemented (src/packet/qos.rs) with `is_priority_packet()` function -- βœ… Priority detection matches official SoftEther: IPv4 ToS != 0, ICMP, VoIP ports -- βœ… IPv6 traffic class and ICMPv6 also prioritized -- βœ… FFI packet loop sorts priority packets to front when QoS enabled -- Impact: Resolved - VoIP/real-time packets sent first in batch transmissions - ---- - -## 6. Code Quality - -### 6.1 Function Complexity βœ… RESOLVED -- βœ… `connect_and_run_inner` refactored from 343 to ~120 lines -- βœ… Extracted helpers to `src/ffi/connection.rs`: `update_state`, `resolve_server_ip`, `generate_session_mac`, `create_session_from_dhcp`, `init_udp_acceleration`, `log_encryption_status` -- βœ… `run_packet_loop` refactored using helpers in `src/ffi/packet_loop.rs` -- βœ… Extracted: `process_arp_for_learning`, `prepare_outbound_frames`, `process_received_frame`, `build_callback_buffer`, `update_stats`, `parse_length_prefixed_packets` -- βœ… Converted inline `log_msg` helpers to `callbacks.log_info/warn/error()` method calls -- Location: `src/ffi/client.rs`, `src/ffi/connection.rs`, `src/ffi/packet_loop.rs` - -### 6.2 Performance Optimizations βœ… RESOLVED -- βœ… Pack now uses `UniCase` for case-insensitive key storage -- βœ… Eliminates redundant `to_lowercase()` on storage (keys stored once at insert) -- βœ… Lookups still require allocation but benefit from UniCase's optimized comparison -- ❌ `Vec::new()` allocations in packet parsing - use `Cow<[u8]>` for zero-copy (deferred) -- Location: `src/protocol/pack.rs` -- Impact: Minor improvement - reduces allocations during Pack construction - -### 6.3 Dead Code Cleanup βœ… RESOLVED -- βœ… `DhcpHandler` is actually used in `src/tunnel/data_loop.rs` (DataLoopState) -- βœ… `#[allow(dead_code)]` annotations reviewed and documented: - - `notify_connected` in ffi/client.rs - reserved for future API - - `notify_state` annotation removed (method is used) - - Protocol constants (qos.rs, dhcpv6.rs, udp_accel.rs) - kept for API completeness - - `is_last` in fragment.rs - stored for debugging - - `configure_routes` in runner.rs - platform-specific, used on Linux -- Location: Various files - -### 6.4 API Design (Low Priority) -- ❌ `SoftEtherConfig` has 20+ fields - group into sub-structs -- Location: `src/ffi/types.rs` -- Recommendation: Group into `ConnectionConfig`, `SecurityConfig`, `RoutingConfig`, `FeatureFlags` - ---- - -## 7. Half-Connection Mode - -_All half-connection mode issues resolved._ - ---- - -## 8. Resolved - -- βœ… Compression latency (switched to fast level) -- βœ… Android socket protection -- βœ… DHCP through tunnel -- βœ… C header state enum fixed -- βœ… Half-connection mode direction parsing (TcpDirection enum) -- βœ… RC4 Tunnel Encryption (src/crypto/rc4.rs with streaming cipher) -- βœ… RC4 Key Pair parsing from server Welcome packet -- βœ… RC4 integration in tunnel TX/RX paths (single-conn Unix & Windows) -- βœ… Swift bridge field name mismatches fixed (skip_tls_verify, timeout_seconds, mtu, etc.) -- βœ… Swift bridge missing fields added (useEncrypt, udpAccel, qos, natTraversal, routing) -- βœ… Swift Session mac_address and gateway_mac added -- βœ… Swift log callback wired (on_log) -- βœ… Kotlin bridge all config options exposed (MTU, encrypt, udpAccel, qos, routing) -- βœ… Kotlin Session now includes macAddress from session -- βœ… VpnService uses session MAC instead of hardcoded -- βœ… Kotlin log callback and socket protection callback added -- βœ… UseSSLDataEncryption flag handled (skip RC4 when no keys present) -- βœ… NAT-T port keepalive signaling implemented (encode_keepalive_with_nat_t) -- βœ… Compression switched to fast level for low latency -- βœ… Tokio worker threads reduced to 1 for mobile battery -- βœ… Certificate pinning implemented (custom CA PEM and SHA-256 fingerprint verification) -- βœ… Packet queue backpressure implemented (QueueFull result code, dropped packet stats) -- βœ… UDP acceleration auth flow wired (params sent, server response parsed) -- βœ… FIFO buffer pre-allocation (compress_into for zero-alloc hot path, pre-alloc comp_buf) -- βœ… iOS socket protection via setsockopt(SO_NET_SERVICE_TYPE, NET_SERVICE_TYPE_VV) -- βœ… RC4 encryption in FFI/mobile packet loop (TunnelEncryption with encrypt/decrypt) -- βœ… Android IP exclusion callback (exclude_ip for cluster redirects) -- βœ… iOS IP exclusion callback (exclude_ip wired to Swift onExcludeIp) -- βœ… Multi-connection support for mobile (establish_additional_connections after DHCP) -- βœ… Half-connection mode for mobile (auto-enabled when max_connections > 1) -- βœ… Android TLS cert pinning (custom_ca_pem and cert_fingerprint_sha256 wired in JNI) -- βœ… Keepalive encryption (RC4 applied to keepalive packets on mobile) -- βœ… Half-connection mode direction verified (TcpDirection 0=Both, 1=ServerToClient, 2=ClientToServer matches official C; primary always ClientToServer; can_send/can_recv filters connections correctly) -- βœ… DHCPv6 integration for FFI/mobile (perform_dhcpv6 after DHCPv4, session includes ipv6_address/dns1_v6/dns2_v6) -- βœ… Reconnection logic for FFI/mobile (retry with 10s delay for UserAlreadyLoggedIn, up to 5 attempts) -- βœ… ARP/Gateway MAC learning for FFI/mobile (GARP + gateway request sent, ARP replies processed, outgoing frames use learned MAC) -- βœ… QoS packet prioritization (is_priority_packet() detects ToS/DSCP, ICMP, VoIP ports; FFI sorts priority packets first) -- βœ… UDP Acceleration V2 (ChaCha20-Poly1305 AEAD encryption for UDP packets) -- βœ… Packet statistics in FFI (bytes_sent/received, packets_sent/received now tracked via Arc) -- βœ… Code deduplication: `TunnelEncryption` moved to shared `src/crypto/mod.rs` -- βœ… Code deduplication: `is_dhcp_response()` / `is_dhcpv6_response()` moved to `src/packet/mod.rs` -- βœ… Added `SoftEtherCallbacks::log()` method to eliminate duplicate log helpers -- βœ… Clippy warnings fixed (collapsible if, range contains, unnecessary to_vec) -- βœ… Function complexity: `connect_and_run_inner` refactored 343β†’120 lines (helpers in connection.rs) -- βœ… Function complexity: `run_packet_loop` refactored with helpers in packet_loop.rs -- βœ… Dead code audit: `#[allow(dead_code)]` annotations reviewed and documented +1. Reference this document in your PR (e.g., "Fixes ISSUE-3") +2. Add tests for the fix +3. Update this document to mark as fixed with date diff --git a/README.md b/README.md index 77677ab..fea9629 100644 --- a/README.md +++ b/README.md @@ -283,11 +283,11 @@ async fn main() -> anyhow::Result<()> { ## Architecture ``` -β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” -β”‚ VpnClient β”‚ -β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ -β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ -β”‚ β”‚ Protocol β”‚ β”‚ Crypto β”‚ β”‚ Adapter β”‚ β”‚ +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ VpnClient β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Protocol β”‚ β”‚ Crypto β”‚ β”‚ Adapter β”‚ β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ β”‚ β”‚ HTTP β”‚ β”‚ β”‚ β”‚ SHA-0 β”‚ β”‚ β”‚ β”‚ TunAdapter β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ Codec β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β” β”‚ β”‚ β”‚ @@ -301,19 +301,19 @@ async fn main() -> anyhow::Result<()> { β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚iOS β”‚ β”‚JNI β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ -β”‚ β”‚ Tunnel β”‚ β”‚ Net β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ -β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β” β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ -β”‚ β”‚ β”‚ DHCP β”‚ β”‚ β”‚ β”‚ UDP β”‚ β”‚ β”‚ ConnectionManager β”‚ β”‚ +β”‚ β”‚ Tunnel β”‚ β”‚ Net β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β” β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ β”‚ DHCP β”‚ β”‚ β”‚ β”‚ UDP β”‚ β”‚ β”‚ ConnectionManager β”‚ β”‚ β”‚ β”‚ β”‚DHCPv6 β”‚ β”‚ β”‚ β”‚ Accel β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ V1+V2 β”‚ β”‚ β”‚ β”‚ Multi-Connectionβ”‚ β”‚ β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ Half-Connection β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ ARP β”‚ β”‚ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ -β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ -β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ -β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ -β”‚ Tokio Async Runtime β”‚ -β”‚ (TCP/TLS/UDP, Timers, Signals) β”‚ -β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ Tokio Async Runtime β”‚ +β”‚ (TCP/TLS/UDP, Timers, Signals) β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` ## Building diff --git a/config.example.json b/config.example.json index 7f31990..c6e1f00 100644 --- a/config.example.json +++ b/config.example.json @@ -2,15 +2,39 @@ "server": "vpn.example.com", "port": 443, "hub": "DEFAULT", - "username": "your_username", - "password_hash": "0000000000000000000000000000000000000000", + "timeout_seconds": 30, + + "_comment_auth": "Authentication configuration. Methods: standard_password, radius_or_nt_domain, certificate, anonymous", + "auth": { + "method": "standard_password", + "username": "your_username", + "password_hash": "0000000000000000000000000000000000000000", + "_comment_radius": "For RADIUS/NT Domain auth, use 'password' instead of 'password_hash'", + "_comment_cert": "For certificate auth, use 'certificate_pem' and 'private_key_pem'" + }, + + "_comment_tls": "TLS configuration", "skip_tls_verify": true, - "nat_traversal": false, + "custom_ca_pem": null, + "cert_fingerprint_sha256": null, + + "_comment_tunnel": "Tunnel features", "use_encrypt": true, - "use_compress": true, + "use_compress": false, "udp_accel": false, + "qos": true, + + "_comment_session": "Session mode", + "nat_traversal": false, + "monitor_mode": false, + + "_comment_performance": "Performance settings", "max_connections": 1, + "half_connection": false, "mtu": 1400, + + "ip_version": "auto", + "routing": { "default_route": false, "accept_pushed_routes": true, @@ -19,21 +43,7 @@ "ipv6_include": [], "ipv6_exclude": [] }, - "qos": false, - "timeout_seconds": 30, - + "_comment_static_ip": "Static IP configuration (optional). When set, DHCP is skipped.", - "static_ip": { - "_comment": "Set these fields to use a static IP instead of DHCP", - "ipv4_address": null, - "ipv4_netmask": null, - "ipv4_gateway": null, - "ipv4_dns1": null, - "ipv4_dns2": null, - "ipv6_address": null, - "ipv6_prefix_len": null, - "ipv6_gateway": null, - "ipv6_dns1": null, - "ipv6_dns2": null - } + "static_ip": null } diff --git a/examples/ios/SoftEtherBridge.swift b/examples/ios/SoftEtherBridge.swift index eb2c126..c6623c2 100644 --- a/examples/ios/SoftEtherBridge.swift +++ b/examples/ios/SoftEtherBridge.swift @@ -41,6 +41,7 @@ public class SoftEtherBridge { public let dns1: UInt32 public let dns2: UInt32 public let connectedServerIP: String + public let originalServerIP: String public let serverVersion: UInt32 public let serverBuild: UInt32 public let macAddress: [UInt8] @@ -370,15 +371,17 @@ public class SoftEtherBridge { // Create client handle = softether_create(&cConfig, &cCallbacks) guard let h = handle else { - throw BridgeError.createFailed + let errorMsg = SoftEtherBridge.lastError + throw BridgeError.createFailed(message: errorMsg) } // Start connection let result = softether_connect(h) if result != SOFTETHER_OK { + let errorMsg = SoftEtherBridge.lastError softether_destroy(h) handle = nil - throw BridgeError.connectFailed(code: Int(result.rawValue)) + throw BridgeError.connectFailed(code: Int(result.rawValue), message: errorMsg) } } @@ -495,6 +498,18 @@ public class SoftEtherBridge { public static var version: String { String(cString: softether_version()) } + + /// Get the last error message from the library. + /// Returns nil if no error occurred. + public static var lastError: String? { + guard let ptr = softether_get_last_error() else { return nil } + return String(cString: ptr) + } + + /// Clear the last error message. + public static func clearLastError() { + softether_clear_last_error() + } } // MARK: - Callback Context @@ -527,6 +542,11 @@ private func connectedCallback(context: UnsafeMutableRawPointer?, session: Unsaf String(cString: $0) } }, + originalServerIP: withUnsafePointer(to: s.original_server_ip) { + $0.withMemoryRebound(to: CChar.self, capacity: 64) { + String(cString: $0) + } + }, serverVersion: s.server_version, serverBuild: s.server_build, macAddress: Array(withUnsafeBytes(of: s.mac_address) { Array($0) }), @@ -620,11 +640,31 @@ private func excludeIpCallback(context: UnsafeMutableRawPointer?, ip: UnsafePoin // MARK: - Errors -public enum BridgeError: Error { +public enum BridgeError: Error, LocalizedError { case alreadyConnected - case createFailed - case connectFailed(code: Int) + case createFailed(message: String?) + case connectFailed(code: Int, message: String?) case sendFailed(code: Int) case disconnected(code: Int) case queueFull // Backpressure - caller should retry + + public var errorDescription: String? { + switch self { + case .alreadyConnected: + return "Already connected" + case .createFailed(let message): + return message ?? "Failed to create VPN client" + case .connectFailed(let code, let message): + if let msg = message { + return msg + } + return "Connection failed with code \(code)" + case .sendFailed(let code): + return "Send failed with code \(code)" + case .disconnected(let code): + return "Disconnected with code \(code)" + case .queueFull: + return "Queue full - retry later" + } + } } diff --git a/include/SoftEtherVPN.h b/include/SoftEtherVPN.h index 5923c35..184dded 100644 --- a/include/SoftEtherVPN.h +++ b/include/SoftEtherVPN.h @@ -305,6 +305,26 @@ int softether_receive_packets(SoftEtherHandle handle, uint8_t* buffer, size_t bu */ const char* softether_version(void); +/** + * Get the last error message. + * + * Returns a pointer to a null-terminated string describing the last error + * that occurred in the current thread. The pointer is valid until the next + * call to any softether_* function on the same thread. + * + * Thread-safe: Each thread has its own error storage. + * + * @return Error message, or NULL if no error occurred + */ +const char* softether_get_last_error(void); + +/** + * Clear the last error message. + * + * Resets the thread-local error state to NULL. + */ +void softether_clear_last_error(void); + // ============================================================================= // Helper Functions // ============================================================================= @@ -368,22 +388,23 @@ int softether_ios_mac_to_string(const uint8_t* mac, char* buffer, size_t buffer_ int softether_ios_is_valid_ipv4(uint32_t ip); /** - * Get session information (simplified for Swift). - * Returns pointer to internal session data that remains valid until the next call. + * Get session information into caller-provided buffer. + * This is the recommended API - caller manages memory, avoiding thread-safety issues. * * @param handle Client handle - * @return Pointer to session data, or NULL if not connected + * @param session_out Pointer to caller-allocated SoftEtherSession struct + * @return SOFTETHER_OK on success, error code otherwise */ -const SoftEtherSession* softether_ios_get_session(SoftEtherHandle handle); +SoftEtherResult softether_ios_get_session(SoftEtherHandle handle, SoftEtherSession* session_out); /** - * Get statistics (simplified for Swift). - * Returns pointer to internal stats data that remains valid until the next call. + * Get statistics into caller-provided buffer. * * @param handle Client handle - * @return Pointer to statistics data, or NULL on error + * @param stats_out Pointer to caller-allocated SoftEtherStats struct + * @return SOFTETHER_OK on success, error code otherwise */ -const SoftEtherStats* softether_ios_get_stats(SoftEtherHandle handle); +SoftEtherResult softether_ios_get_stats(SoftEtherHandle handle, SoftEtherStats* stats_out); /** * Format byte count as human-readable string (B, KB, MB, GB). diff --git a/src/client/concurrent_reader.rs b/src/client/concurrent_reader.rs index 3c1d563..3ad4d61 100644 --- a/src/client/concurrent_reader.rs +++ b/src/client/concurrent_reader.rs @@ -3,17 +3,140 @@ //! This module provides true concurrent reading from multiple VPN connections //! using spawned tasks and channels. This eliminates the sequential polling //! bottleneck that causes latency with multiple connections. +//! +//! Each connection has its own RC4 decryption state (if encryption is enabled) +//! to handle per-connection cipher synchronization with the server. +//! +//! ## Buffer Pooling +//! +//! To reduce allocation overhead, this module uses a shared buffer pool. +//! Reader tasks grab pre-allocated `BytesMut` buffers from the pool, +//! read directly into them, then freeze to `Bytes` for zero-copy transfer. +//! When the pool is empty, new buffers are allocated on demand. use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::sync::Arc; -use bytes::Bytes; +use bytes::{Bytes, BytesMut}; use tokio::sync::mpsc; -use tracing::{debug, warn}; +use tracing::{debug, trace, warn}; + +use crate::crypto::TunnelEncryption; use super::connection::VpnConnection; use super::multi_connection::TcpDirection; +/// Default buffer size for receive operations (64KB - max VPN packet size). +const DEFAULT_BUFFER_SIZE: usize = 65536; + +/// Default number of buffers to pre-allocate per reader task. +const BUFFERS_PER_READER: usize = 8; + +/// A pool of reusable buffers to reduce allocation overhead. +/// +/// Uses a bounded channel as a lock-free pool. When empty, new buffers +/// are allocated on demand (graceful degradation). +#[derive(Clone)] +pub struct BufferPool { + /// Channel for available buffers. + pool: Arc>, + /// Receiver for getting buffers (shared via try_recv). + receiver: Arc>>, + /// Buffer capacity for new allocations. + buffer_size: usize, + /// Stats: buffers acquired from pool. + pool_hits: Arc, + /// Stats: buffers allocated fresh. + pool_misses: Arc, +} + +impl BufferPool { + /// Create a new buffer pool with pre-allocated buffers. + /// + /// # Arguments + /// * `pool_size` - Maximum number of buffers to keep in pool + /// * `buffer_size` - Capacity of each buffer + pub fn new(pool_size: usize, buffer_size: usize) -> Self { + let (tx, rx) = mpsc::channel(pool_size); + + // Pre-allocate buffers + for _ in 0..pool_size { + let buf = BytesMut::with_capacity(buffer_size); + // Ignore error if channel is full (shouldn't happen) + let _ = tx.try_send(buf); + } + + Self { + pool: Arc::new(tx), + receiver: Arc::new(tokio::sync::Mutex::new(rx)), + buffer_size, + pool_hits: Arc::new(AtomicU64::new(0)), + pool_misses: Arc::new(AtomicU64::new(0)), + } + } + + /// Get a buffer from the pool, or allocate a new one if empty. + /// + /// The returned buffer is cleared and ready for use. + pub async fn get(&self) -> BytesMut { + // Try to get from pool without blocking + let mut rx = self.receiver.lock().await; + match rx.try_recv() { + Ok(mut buf) => { + self.pool_hits.fetch_add(1, Ordering::Relaxed); + buf.clear(); + buf.reserve(self.buffer_size); + buf + } + Err(_) => { + self.pool_misses.fetch_add(1, Ordering::Relaxed); + trace!("Buffer pool empty, allocating new buffer"); + BytesMut::with_capacity(self.buffer_size) + } + } + } + + /// Return a buffer to the pool for reuse. + /// + /// If the pool is full, the buffer is dropped. + pub fn return_buf(&self, buf: BytesMut) { + // Only return if buffer has reasonable capacity + if buf.capacity() >= self.buffer_size / 2 { + let _ = self.pool.try_send(buf); + } + } + + /// Try to reclaim a `Bytes` back to `BytesMut` if we're the sole owner. + /// + /// This enables zero-copy buffer recycling when possible. + pub fn try_reclaim(&self, bytes: Bytes) { + // try_into_mut returns Ok(BytesMut) if we're the only reference + if let Ok(buf) = bytes.try_into_mut() { + self.return_buf(buf); + } + } + + /// Get pool statistics: (hits, misses). + pub fn stats(&self) -> (u64, u64) { + ( + self.pool_hits.load(Ordering::Relaxed), + self.pool_misses.load(Ordering::Relaxed), + ) + } + + /// Get the hit rate as a percentage (0.0 - 100.0). + pub fn hit_rate(&self) -> f64 { + let hits = self.pool_hits.load(Ordering::Relaxed); + let misses = self.pool_misses.load(Ordering::Relaxed); + let total = hits + misses; + if total == 0 { + 100.0 + } else { + (hits as f64 / total as f64) * 100.0 + } + } +} + /// A packet received from a connection. #[derive(Debug)] pub struct ReceivedPacket { @@ -37,6 +160,8 @@ struct ReaderHandle { /// /// Each task reads from its connection and sends packets to a shared channel. /// The main loop can then receive from any connection with zero latency. +/// +/// Uses a shared buffer pool to reduce allocation overhead. pub struct ConcurrentReader { /// Channel receiver for incoming packets from all connections. rx: mpsc::Receiver, @@ -44,22 +169,32 @@ pub struct ConcurrentReader { handles: Vec, /// Shutdown flag shared with all tasks. shutdown: Arc, + /// Shared buffer pool for all reader tasks. + buffer_pool: BufferPool, } impl ConcurrentReader { /// Create a new concurrent reader from receive-capable connections. /// - /// Takes ownership of the connections and spawns reader tasks. + /// Takes ownership of the connections (with optional per-connection encryption) + /// and spawns reader tasks. Each task handles its own decryption if encryption + /// state is provided. + /// /// Returns the reader and a vec of (index, bytes_received) for stats tracking. pub fn new( - connections: Vec<(usize, VpnConnection, TcpDirection)>, + connections: Vec<(usize, VpnConnection, TcpDirection, Option)>, channel_size: usize, ) -> Self { let (tx, rx) = mpsc::channel(channel_size); let shutdown = Arc::new(AtomicBool::new(false)); let mut handles = Vec::with_capacity(connections.len()); - for (index, conn, direction) in connections { + // Create shared buffer pool: buffers per reader * number of readers + // Plus some extra for in-flight packets + let pool_size = connections.len() * BUFFERS_PER_READER + channel_size; + let buffer_pool = BufferPool::new(pool_size, DEFAULT_BUFFER_SIZE); + + for (index, conn, direction, encryption) in connections { if !direction.can_recv() { warn!("Connection {} cannot receive, skipping", index); continue; @@ -69,9 +204,19 @@ impl ConcurrentReader { let task_tx = tx.clone(); let task_shutdown = shutdown.clone(); let task_bytes = bytes_received.clone(); + let task_pool = buffer_pool.clone(); let task = tokio::spawn(async move { - Self::reader_task(index, conn, task_tx, task_shutdown, task_bytes).await; + Self::reader_task( + index, + conn, + encryption, + task_tx, + task_shutdown, + task_bytes, + task_pool, + ) + .await; }); handles.push(ReaderHandle { @@ -82,44 +227,65 @@ impl ConcurrentReader { } debug!( - "ConcurrentReader started with {} reader tasks", - handles.len() + "ConcurrentReader started with {} reader tasks, buffer pool size {}", + handles.len(), + pool_size ); Self { rx, handles, shutdown, + buffer_pool, } } - /// Reader task for a single connection. + /// Reader task for a single connection with optional per-connection decryption. + /// + /// Uses the shared buffer pool to reduce allocation overhead. async fn reader_task( index: usize, mut conn: VpnConnection, + mut encryption: Option, tx: mpsc::Sender, shutdown: Arc, bytes_received: Arc, + pool: BufferPool, ) { - let mut buf = vec![0u8; 65536]; - loop { if shutdown.load(Ordering::Relaxed) { debug!("Reader {} shutting down", index); break; } + // Get a buffer from the pool + let mut buf = pool.get().await; + buf.resize(DEFAULT_BUFFER_SIZE, 0); + match conn.read(&mut buf).await { Ok(0) => { debug!("Connection {} closed", index); + // Return buffer to pool before exiting + pool.return_buf(buf); break; } Ok(n) => { bytes_received.fetch_add(n as u64, Ordering::Relaxed); + // Truncate to actual data size + buf.truncate(n); + + // Decrypt in-place if per-connection encryption is enabled + if let Some(ref mut enc) = encryption { + enc.decrypt(&mut buf); + } + + // Freeze to Bytes (zero-copy, shares underlying memory) + let data = buf.freeze(); + let packet = ReceivedPacket { conn_index: index, - data: Bytes::copy_from_slice(&buf[..n]), + data, }; if tx.send(packet).await.is_err() { @@ -131,6 +297,8 @@ impl ConcurrentReader { if !shutdown.load(Ordering::Relaxed) { warn!("Connection {} read error: {}", index, e); } + // Return buffer to pool before exiting + pool.return_buf(buf); break; } } @@ -149,6 +317,14 @@ impl ConcurrentReader { self.rx.try_recv().ok() } + /// Reclaim a packet's buffer back to the pool if possible. + /// + /// Call this after you're done processing a packet to enable buffer reuse. + /// If the buffer can't be reclaimed (still referenced elsewhere), it's dropped. + pub fn reclaim(&self, packet: ReceivedPacket) { + self.buffer_pool.try_reclaim(packet.data); + } + /// Get the number of active reader tasks. pub fn reader_count(&self) -> usize { self.handles.len() @@ -162,6 +338,12 @@ impl ConcurrentReader { .collect() } + /// Get buffer pool statistics: (hits, misses, hit_rate%). + pub fn pool_stats(&self) -> (u64, u64, f64) { + let (hits, misses) = self.buffer_pool.stats(); + (hits, misses, self.buffer_pool.hit_rate()) + } + /// Shutdown all reader tasks. pub fn shutdown(&self) { self.shutdown.store(true, Ordering::Relaxed); @@ -182,10 +364,387 @@ impl Drop for ConcurrentReader { mod tests { use super::*; + // ========================================================================== + // TcpDirection tests (duplicated from multi_connection for coverage) + // ========================================================================== + #[test] fn test_tcp_direction_can_recv() { assert!(TcpDirection::Both.can_recv()); assert!(TcpDirection::ServerToClient.can_recv()); assert!(!TcpDirection::ClientToServer.can_recv()); } + + #[test] + fn test_tcp_direction_can_send() { + assert!(TcpDirection::Both.can_send()); + assert!(TcpDirection::ClientToServer.can_send()); + assert!(!TcpDirection::ServerToClient.can_send()); + } + + // ========================================================================== + // ReceivedPacket tests + // ========================================================================== + + #[test] + fn test_received_packet_structure() { + let data = Bytes::from_static(b"test packet data"); + let packet = ReceivedPacket { + conn_index: 2, + data: data.clone(), + }; + + assert_eq!(packet.conn_index, 2); + assert_eq!(packet.data.len(), 16); + assert_eq!(&packet.data[..], b"test packet data"); + } + + #[test] + fn test_received_packet_zero_copy() { + // Verify Bytes provides zero-copy semantics + let original = Bytes::from(vec![1u8, 2, 3, 4, 5]); + let packet = ReceivedPacket { + conn_index: 0, + data: original.clone(), + }; + + // Clone should share underlying data + let cloned = packet.data.clone(); + assert_eq!( + original.as_ptr(), + cloned.as_ptr(), + "Bytes should share memory" + ); + } + + // ========================================================================== + // Connection filtering tests + // ========================================================================== + + #[test] + fn test_filter_recv_capable_connections() { + // Simulate filtering connections for ConcurrentReader + let directions = [ + TcpDirection::ClientToServer, // 0: cannot recv + TcpDirection::ServerToClient, // 1: can recv + TcpDirection::Both, // 2: can recv + TcpDirection::ServerToClient, // 3: can recv + ]; + + let recv_capable: Vec = directions + .iter() + .enumerate() + .filter(|(_, d)| d.can_recv()) + .map(|(i, _)| i) + .collect(); + + assert_eq!(recv_capable, vec![1, 2, 3]); + } + + #[test] + fn test_extract_server_to_client_only() { + // ConcurrentReader extracts only ServerToClient connections + // Both connections remain for bidirectional use + let directions = [ + TcpDirection::ClientToServer, // 0: skip + TcpDirection::ServerToClient, // 1: extract + TcpDirection::Both, // 2: skip (bidirectional) + TcpDirection::ServerToClient, // 3: extract + ]; + + let extracted: Vec = directions + .iter() + .enumerate() + .filter(|(_, d)| **d == TcpDirection::ServerToClient) + .map(|(i, _)| i) + .collect(); + + assert_eq!(extracted, vec![1, 3]); + } + + // ========================================================================== + // Bytes accumulation tests + // ========================================================================== + + #[test] + fn test_bytes_received_tracking() { + use std::sync::atomic::{AtomicU64, Ordering}; + + let bytes_received = AtomicU64::new(0); + + // Simulate receiving packets + bytes_received.fetch_add(1500, Ordering::Relaxed); + bytes_received.fetch_add(1200, Ordering::Relaxed); + bytes_received.fetch_add(800, Ordering::Relaxed); + + assert_eq!(bytes_received.load(Ordering::Relaxed), 3500); + } + + #[test] + fn test_multi_connection_bytes_aggregation() { + use std::sync::atomic::{AtomicU64, Ordering}; + + // Simulate multiple connections + let conn_bytes: Vec = (0..4).map(|_| AtomicU64::new(0)).collect(); + + // Each connection receives different amounts + conn_bytes[0].store(10000, Ordering::Relaxed); + conn_bytes[1].store(15000, Ordering::Relaxed); + conn_bytes[2].store(8000, Ordering::Relaxed); + conn_bytes[3].store(12000, Ordering::Relaxed); + + let total: u64 = conn_bytes.iter().map(|c| c.load(Ordering::Relaxed)).sum(); + assert_eq!(total, 45000); + + // Get stats as tuples + let stats: Vec<(usize, u64)> = conn_bytes + .iter() + .enumerate() + .map(|(i, c)| (i, c.load(Ordering::Relaxed))) + .collect(); + + assert_eq!(stats, vec![(0, 10000), (1, 15000), (2, 8000), (3, 12000)]); + } + + // ========================================================================== + // Shutdown flag tests + // ========================================================================== + + #[test] + fn test_shutdown_flag_propagation() { + use std::sync::atomic::{AtomicBool, Ordering}; + use std::sync::Arc; + + let shutdown = Arc::new(AtomicBool::new(false)); + + // Simulate multiple reader tasks checking flag + let flag1 = shutdown.clone(); + let flag2 = shutdown.clone(); + let flag3 = shutdown.clone(); + + assert!(!flag1.load(Ordering::Relaxed)); + assert!(!flag2.load(Ordering::Relaxed)); + assert!(!flag3.load(Ordering::Relaxed)); + + // Signal shutdown + shutdown.store(true, Ordering::Relaxed); + + // All flags should see the update + assert!(flag1.load(Ordering::Relaxed)); + assert!(flag2.load(Ordering::Relaxed)); + assert!(flag3.load(Ordering::Relaxed)); + } + + // ========================================================================== + // Channel capacity tests + // ========================================================================== + + #[test] + fn test_channel_capacity_selection() { + // Test that channel capacity is reasonable for different connection counts + let test_cases = [ + (1, 256), // Single connection + (4, 256), // Typical multi-connection + (8, 256), // Large multi-connection + (16, 256), // Maximum connections + ]; + + for (num_conns, expected_capacity) in test_cases { + // In real code, capacity is constant (256) + // This test documents the expected behavior + let capacity = 256; // From ConcurrentReader::new + assert_eq!( + capacity, expected_capacity, + "Channel capacity for {num_conns} connections" + ); + let _ = num_conns; // Suppress warning + } + } + + // ========================================================================== + // Connection index tracking tests + // ========================================================================== + + #[test] + fn test_connection_index_preservation() { + // Verify that connection indices are preserved through extraction + let original_indices = [0usize, 1, 2, 3]; + let directions = [ + TcpDirection::ClientToServer, // 0 + TcpDirection::ServerToClient, // 1 + TcpDirection::ClientToServer, // 2 + TcpDirection::ServerToClient, // 3 + ]; + + // Extract recv-only with original indices + let extracted: Vec<(usize, TcpDirection)> = original_indices + .iter() + .zip(directions.iter()) + .filter(|(_, d)| **d == TcpDirection::ServerToClient) + .map(|(i, d)| (*i, *d)) + .collect(); + + assert_eq!(extracted.len(), 2); + assert_eq!(extracted[0].0, 1, "First extracted should have index 1"); + assert_eq!(extracted[1].0, 3, "Second extracted should have index 3"); + } + + // ========================================================================== + // Buffer Pool tests + // ========================================================================== + + #[tokio::test] + async fn test_buffer_pool_creation() { + let pool = BufferPool::new(4, 1024); + + // Pool should be created with initial buffers + let (hits, misses) = pool.stats(); + assert_eq!(hits, 0); + assert_eq!(misses, 0); + assert_eq!(pool.hit_rate(), 100.0); // No operations yet + } + + #[tokio::test] + async fn test_buffer_pool_get_returns_buffer() { + let pool = BufferPool::new(4, 1024); + + let buf = pool.get().await; + assert!(buf.capacity() >= 1024); + + // Should count as a hit (from pre-allocated pool) + let (hits, misses) = pool.stats(); + assert_eq!(hits, 1); + assert_eq!(misses, 0); + } + + #[tokio::test] + async fn test_buffer_pool_exhaustion() { + let pool = BufferPool::new(2, 1024); + + // Exhaust the pool + let _buf1 = pool.get().await; + let _buf2 = pool.get().await; + + // This should allocate a new buffer (miss) + let _buf3 = pool.get().await; + + let (hits, misses) = pool.stats(); + assert_eq!(hits, 2); + assert_eq!(misses, 1); + } + + #[tokio::test] + async fn test_buffer_pool_return_and_reuse() { + let pool = BufferPool::new(2, 1024); + + // Get a buffer + let buf = pool.get().await; + let (hits1, _) = pool.stats(); + assert_eq!(hits1, 1); + + // Return it + pool.return_buf(buf); + + // Get another - should reuse + let _buf2 = pool.get().await; + let (hits2, misses2) = pool.stats(); + assert_eq!(hits2, 2); + assert_eq!(misses2, 0); + } + + #[tokio::test] + async fn test_buffer_pool_hit_rate() { + let pool = BufferPool::new(3, 1024); + + // 3 hits (from pre-allocated) + let _b1 = pool.get().await; + let _b2 = pool.get().await; + let _b3 = pool.get().await; + + // 1 miss (pool exhausted) + let _b4 = pool.get().await; + + let hit_rate = pool.hit_rate(); + assert!( + (hit_rate - 75.0).abs() < 0.01, + "Expected 75% hit rate, got {hit_rate}" + ); + } + + #[tokio::test] + async fn test_buffer_pool_try_reclaim() { + let pool = BufferPool::new(1, 1024); + + // Get and freeze a buffer + let mut buf = pool.get().await; + buf.extend_from_slice(b"test data"); + let bytes = buf.freeze(); + + // Since we're the only owner, try_reclaim should work + pool.try_reclaim(bytes); + + // Now pool should have a buffer again + let (hits_before, _) = pool.stats(); + let _buf = pool.get().await; + let (hits_after, misses_after) = pool.stats(); + + // Should be another hit, not a miss + assert_eq!(hits_after, hits_before + 1); + assert_eq!(misses_after, 0); + } + + #[tokio::test] + async fn test_buffer_pool_try_reclaim_with_clone() { + let pool = BufferPool::new(1, 1024); + + // Get and freeze a buffer + let mut buf = pool.get().await; + buf.extend_from_slice(b"test data"); + let bytes = buf.freeze(); + + // Clone to increase ref count + let _clone = bytes.clone(); + + // try_reclaim should fail silently (buffer still referenced) + pool.try_reclaim(bytes); + + // Pool should be empty, so next get is a miss + let _buf = pool.get().await; + let (_, misses) = pool.stats(); + assert_eq!(misses, 1, "Should have missed because reclaim failed"); + } + + // Compile-time assertions for buffer pool constants + const _: () = { + assert!( + BUFFERS_PER_READER >= 4, + "Should have enough buffers per reader" + ); + }; + + #[test] + fn test_buffer_pool_constants() { + // Verify the constants make sense + assert_eq!(DEFAULT_BUFFER_SIZE, 65536, "Should match max VPN packet"); + } + + #[tokio::test] + async fn test_buffer_pool_cleared_on_get() { + let pool = BufferPool::new(1, 1024); + + // Get a buffer and fill it with data + let mut buf = pool.get().await; + buf.extend_from_slice(b"some data that should be cleared"); + + // Return it + pool.return_buf(buf); + + // Get it back - should be cleared + let buf2 = pool.get().await; + assert!(buf2.is_empty(), "Buffer should be cleared after get"); + assert!( + buf2.capacity() >= 1024, + "Buffer should have capacity reserved" + ); + } } diff --git a/src/client/connection.rs b/src/client/connection.rs index 688c4dc..8669812 100644 --- a/src/client/connection.rs +++ b/src/client/connection.rs @@ -199,7 +199,7 @@ fn build_tls_config(config: &VpnConfig) -> Result { debug!("Using certificate fingerprint pinning"); Ok(ClientConfig::builder_with_provider(provider) .with_safe_default_protocol_versions() - .unwrap() + .map_err(|e| Error::Tls(format!("Failed to set TLS protocol versions: {e}")))? .dangerous() .with_custom_certificate_verifier(Arc::new(verifier)) .with_no_client_auth()) @@ -222,7 +222,7 @@ fn build_tls_config(config: &VpnConfig) -> Result { debug!("Using custom CA certificate for verification"); Ok(ClientConfig::builder_with_provider(provider) .with_safe_default_protocol_versions() - .unwrap() + .map_err(|e| Error::Tls(format!("Failed to set TLS protocol versions: {e}")))? .with_root_certificates(root_store) .with_no_client_auth()) } else if config.skip_tls_verify { @@ -230,7 +230,7 @@ fn build_tls_config(config: &VpnConfig) -> Result { debug!("TLS verification disabled (skip_tls_verify=true)"); Ok(ClientConfig::builder_with_provider(provider) .with_safe_default_protocol_versions() - .unwrap() + .map_err(|e| Error::Tls(format!("Failed to set TLS protocol versions: {e}")))? .dangerous() .with_custom_certificate_verifier(Arc::new(NoVerifier)) .with_no_client_auth()) @@ -241,7 +241,7 @@ fn build_tls_config(config: &VpnConfig) -> Result { // In production, you'd load system certs here Ok(ClientConfig::builder_with_provider(provider) .with_safe_default_protocol_versions() - .unwrap() + .map_err(|e| Error::Tls(format!("Failed to set TLS protocol versions: {e}")))? .with_root_certificates(root_store) .with_no_client_auth()) } diff --git a/src/client/mod.rs b/src/client/mod.rs index c640698..7c06f96 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -240,6 +240,7 @@ impl VpnClient { // Create ConnectionManager for multi-connection support // Pass the actual server address (after redirect) for additional connections + // Pass RC4 key pair for per-connection encryption (each connection gets fresh cipher state) let mut conn_mgr = ConnectionManager::new( active_conn, &self.config, @@ -247,6 +248,7 @@ impl VpnClient { &actual_server_addr, actual_server_port, use_raw_mode, + auth_result.rc4_key_pair.clone(), ); // Start tunnel diff --git a/src/client/multi_connection.rs b/src/client/multi_connection.rs index e80dc81..0c74268 100644 --- a/src/client/multi_connection.rs +++ b/src/client/multi_connection.rs @@ -11,6 +11,7 @@ use std::time::{Duration, Instant}; use tracing::{debug, info, warn}; use crate::config::VpnConfig; +use crate::crypto::{Rc4KeyPair, TunnelEncryption}; use crate::error::{Error, Result}; use crate::protocol::{ AuthResult, HelloResponse, HttpCodec, HttpRequest, Pack, CONTENT_TYPE_PACK, VPN_TARGET, @@ -62,6 +63,10 @@ pub struct ManagedConnection { pub conn: VpnConnection, /// Connection direction for half-connection mode. pub direction: TcpDirection, + /// Per-connection RC4 encryption state (if enabled). + /// Each TCP socket has independent RC4 cipher state on the server, + /// so we must maintain independent state per connection on the client. + pub encryption: Option, /// When this connection was established. pub connected_at: Instant, /// Last activity time. @@ -74,24 +79,80 @@ pub struct ManagedConnection { pub healthy: bool, /// Connection index (for debugging). pub index: usize, + /// Estimated pending bytes in send buffer (for load balancing). + /// Updated on write, decremented over time based on estimated bandwidth. + pending_send_bytes: u64, + /// Last time pending bytes was updated. + pending_updated: Instant, } impl ManagedConnection { - /// Create a new managed connection. + /// Create a new managed connection without encryption. pub fn new(conn: VpnConnection, direction: TcpDirection, index: usize) -> Self { let now = Instant::now(); Self { conn, direction, + encryption: None, connected_at: now, last_activity: now, bytes_sent: 0, bytes_received: 0, healthy: true, index, + pending_send_bytes: 0, + pending_updated: now, } } + /// Create a new managed connection with RC4 encryption. + /// + /// Each connection gets its own fresh RC4 cipher state because the server + /// maintains independent RC4 state per TCP socket. + pub fn with_encryption( + conn: VpnConnection, + direction: TcpDirection, + index: usize, + rc4_key_pair: &Rc4KeyPair, + ) -> Self { + let now = Instant::now(); + Self { + conn, + direction, + encryption: Some(TunnelEncryption::new(rc4_key_pair)), + connected_at: now, + last_activity: now, + bytes_sent: 0, + bytes_received: 0, + healthy: true, + index, + pending_send_bytes: 0, + pending_updated: now, + } + } + + /// Encrypt data in-place for sending (if encryption is enabled). + #[inline] + pub fn encrypt(&mut self, data: &mut [u8]) { + if let Some(ref mut enc) = self.encryption { + enc.encrypt(data); + } + } + + /// Decrypt data in-place after receiving (if encryption is enabled). + #[inline] + pub fn decrypt(&mut self, data: &mut [u8]) { + if let Some(ref mut enc) = self.encryption { + enc.decrypt(data); + } + } + + /// Check if this connection has encryption enabled. + #[inline] + pub fn is_encrypted(&self) -> bool { + self.encryption.is_some() + } + /// Update activity timestamp. pub fn touch(&mut self) { self.last_activity = Instant::now(); @@ -101,6 +162,25 @@ impl ManagedConnection { pub fn is_idle(&self, timeout: Duration) -> bool { self.last_activity.elapsed() > timeout } + + /// Get estimated pending send bytes with time-based decay. + /// Assumes ~10 MB/s bandwidth for decay calculation (100 bytes/Β΅s). + pub fn estimated_pending(&self) -> u64 { + let elapsed_us = self.pending_updated.elapsed().as_micros() as u64; + // Decay rate: ~10 MB/s = 10 bytes per microsecond + let decay = elapsed_us.saturating_mul(10); + self.pending_send_bytes.saturating_sub(decay) + } + + /// Record bytes written (increases pending estimate). + pub fn record_write(&mut self, bytes: u64) { + // First decay existing pending + let current = self.estimated_pending(); + self.pending_send_bytes = current.saturating_add(bytes); + self.pending_updated = Instant::now(); + self.bytes_sent += bytes; + self.touch(); + } } /// Manager for multiple VPN connections. @@ -128,6 +208,9 @@ pub struct ConnectionManager { /// Whether to use raw TCP mode (no TLS) for tunnel data. /// This is set when use_encrypt=false and server doesn't provide RC4 keys. use_raw_mode: bool, + /// RC4 key pair for creating per-connection encryption. + /// Each new connection gets fresh cipher state from this key pair. + rc4_key_pair: Option, } impl ConnectionManager { @@ -139,6 +222,10 @@ impl ConnectionManager { /// `use_raw_mode` indicates whether the server switched to raw TCP mode /// (when use_encrypt=false and no RC4 keys). Additional connections must /// also use raw TCP mode in this case. + /// + /// `rc4_key_pair` provides keys for per-connection RC4 encryption. Each connection + /// gets its own fresh cipher state because the server maintains independent RC4 + /// state per TCP socket. pub fn new( primary_conn: VpnConnection, config: &VpnConfig, @@ -146,6 +233,7 @@ impl ConnectionManager { actual_server: &str, actual_port: u16, use_raw_mode: bool, + rc4_key_pair: Option, ) -> Self { // Use half_connection from config (user controls this, not auto-calculated) let half_connection = config.half_connection; @@ -161,7 +249,13 @@ impl ConnectionManager { TcpDirection::Both }; - let primary = ManagedConnection::new(primary_conn, direction, 0); + // Create primary connection with per-connection encryption if keys provided + let primary = if let Some(ref keys) = rc4_key_pair { + info!("RC4 encryption enabled for primary connection"); + ManagedConnection::with_encryption(primary_conn, direction, 0, keys) + } else { + ManagedConnection::new(primary_conn, direction, 0) + }; // Create a config pointing to the actual server (may be redirect server) let mut actual_config = config.clone(); @@ -179,6 +273,7 @@ impl ConnectionManager { send_index: 0, recv_index: 0, use_raw_mode, + rc4_key_pair, } } @@ -258,7 +353,25 @@ impl ConnectionManager { } /// Establish a single additional connection. + /// Wrapped with a timeout to prevent hanging if server doesn't respond. async fn establish_one_additional(&self) -> Result { + use tokio::time::timeout; + + // Use the same timeout as primary connection establishment + let connect_timeout = Duration::from_secs(self.config.timeout_seconds); + + timeout(connect_timeout, self.establish_one_additional_inner()) + .await + .map_err(|_| { + Error::TimeoutMessage(format!( + "Additional connection establishment timed out after {}s", + self.config.timeout_seconds + )) + })? + } + + /// Inner implementation of additional connection establishment. + async fn establish_one_additional_inner(&self) -> Result { let index = self.connections.len(); // Create a new TCP connection to the server @@ -286,7 +399,17 @@ impl ConnectionManager { index, direction ); - Ok(ManagedConnection::new(conn, direction, index)) + // Create managed connection with fresh per-connection RC4 encryption. + // Each TCP socket has independent RC4 state on the server, so we need + // fresh cipher state for each new connection. + let managed = if let Some(ref keys) = self.rc4_key_pair { + debug!("RC4 encryption enabled for additional connection {}", index); + ManagedConnection::with_encryption(conn, direction, index, keys) + } else { + ManagedConnection::new(conn, direction, index) + }; + + Ok(managed) } /// Upload VPN signature to server. @@ -403,8 +526,35 @@ impl ConnectionManager { } /// Get a connection suitable for sending data. - /// Uses round-robin among send-capable connections. + /// Uses least-loaded selection among send-capable connections to balance + /// TCP buffer pressure and reduce latency oscillation. pub fn get_send_connection(&mut self) -> Option<&mut ManagedConnection> { + // Find send-capable connections with their estimated pending bytes + let send_capable: Vec<(usize, u64)> = self + .connections + .iter() + .enumerate() + .filter(|(_, c)| c.healthy && c.direction.can_send()) + .map(|(i, c)| (i, c.estimated_pending())) + .collect(); + + if send_capable.is_empty() { + return None; + } + + // Select connection with least pending bytes (least loaded) + let (idx, _) = send_capable + .iter() + .min_by_key(|(_, pending)| *pending) + .copied() + .unwrap(); + + Some(&mut self.connections[idx]) + } + + /// Get a connection for sending using round-robin (for compatibility). + /// Prefer `get_send_connection()` for better load balancing. + pub fn get_send_connection_roundrobin(&mut self) -> Option<&mut ManagedConnection> { let send_capable: Vec = self .connections .iter() @@ -527,16 +677,55 @@ impl ConnectionManager { } /// Write data using an appropriate send connection. - /// Flushes immediately to minimize latency for VPN traffic. + /// Uses least-loaded selection to reduce latency oscillation. + /// TCP_NODELAY is set, so we don't explicitly flush to avoid blocking. pub async fn write_all(&mut self, buf: &[u8]) -> io::Result<()> { - // Select a send-capable connection + // Select least-loaded send-capable connection + let idx = { + let send_capable: Vec<(usize, u64)> = self + .connections + .iter() + .enumerate() + .filter(|(_, c)| c.healthy && c.direction.can_send()) + .map(|(i, c)| (i, c.estimated_pending())) + .collect(); + + if send_capable.is_empty() { + return Err(io::Error::new( + io::ErrorKind::NotConnected, + "No send-capable connections available", + )); + } + + // Select connection with least pending bytes + send_capable + .iter() + .min_by_key(|(_, pending)| *pending) + .map(|(i, _)| *i) + .unwrap() + }; + + let conn = &mut self.connections[idx]; + conn.conn.write_all(buf).await?; + // TCP_NODELAY is set, so data is sent immediately without Nagle delay. + // We don't call flush() here to avoid blocking on TCP ACKs which + // causes latency oscillation under load. + conn.record_write(buf.len() as u64); + Ok(()) + } + + /// Write data with per-connection encryption. + /// Encrypts using the selected connection's own cipher state, then sends. + /// Uses least-loaded selection to balance TCP buffer pressure. + pub async fn write_all_encrypted(&mut self, buf: &mut [u8]) -> io::Result<()> { + // Select least-loaded send-capable connection let idx = { - let send_capable: Vec = self + let send_capable: Vec<(usize, u64)> = self .connections .iter() .enumerate() .filter(|(_, c)| c.healthy && c.direction.can_send()) - .map(|(i, _)| i) + .map(|(i, c)| (i, c.estimated_pending())) .collect(); if send_capable.is_empty() { @@ -546,18 +735,58 @@ impl ConnectionManager { )); } - self.send_index = (self.send_index + 1) % send_capable.len(); - send_capable[self.send_index] + // Select connection with least pending bytes + send_capable + .iter() + .min_by_key(|(_, pending)| *pending) + .map(|(i, _)| *i) + .unwrap() }; let conn = &mut self.connections[idx]; + // Encrypt with this connection's own cipher state + conn.encrypt(buf); conn.conn.write_all(buf).await?; - conn.conn.flush().await?; // Flush immediately for low latency - conn.bytes_sent += buf.len() as u64; - conn.touch(); + // TCP_NODELAY ensures immediate send. No flush() to avoid ACK-wait latency. + conn.record_write(buf.len() as u64); Ok(()) } + /// Read data from a bidirectional connection with per-connection decryption. + /// Returns (connection_index, data_length) on success. + /// Decrypts in-place using the connection's own cipher state. + pub async fn read_any_decrypt(&mut self, buf: &mut [u8]) -> io::Result<(usize, usize)> { + // Get indices of receive-capable connections + let recv_indices: Vec = self + .connections + .iter() + .enumerate() + .filter(|(_, c)| c.healthy && c.direction.can_recv()) + .map(|(i, _)| i) + .collect(); + + if recv_indices.is_empty() { + return Err(io::Error::new( + io::ErrorKind::NotConnected, + "No receive-capable connections available", + )); + } + + // Round-robin selection for fairness + self.recv_index = (self.recv_index + 1) % recv_indices.len(); + let idx = recv_indices[self.recv_index]; + + let conn = &mut self.connections[idx]; + let n = conn.conn.read(buf).await?; + if n > 0 { + // Decrypt with this connection's own cipher state + conn.decrypt(&mut buf[..n]); + conn.bytes_received += n as u64; + conn.touch(); + } + Ok((idx, n)) + } + /// Mark a connection as unhealthy. pub fn mark_unhealthy(&mut self, index: usize) { if let Some(conn) = self.connections.get_mut(index) { @@ -569,15 +798,25 @@ impl ConnectionManager { /// Extract receive-ONLY connections for concurrent reading. /// /// This removes ONLY receive-only connections (ServerToClient direction) from - /// the manager and returns them as (index, connection, direction) tuples for - /// use with ConcurrentReader. + /// the manager and returns them as (index, connection, direction, encryption) tuples + /// for use with ConcurrentReader. /// /// IMPORTANT: Bidirectional connections (Both) are NOT extracted because they /// are needed for sending. Only in half-connection mode with dedicated /// receive connections do we extract them. /// + /// Each connection includes its own RC4 encryption state (if enabled) for + /// per-connection decryption. + /// /// After calling this, bidirectional and send-only connections remain. - pub fn take_recv_connections(&mut self) -> Vec<(usize, super::VpnConnection, TcpDirection)> { + pub fn take_recv_connections( + &mut self, + ) -> Vec<( + usize, + super::VpnConnection, + TcpDirection, + Option, + )> { let mut recv_conns = Vec::new(); let mut remaining = Vec::new(); @@ -585,7 +824,7 @@ impl ConnectionManager { // Only extract ServerToClient (receive-only) connections. // Both (bidirectional) connections must stay for sending! if managed.direction == TcpDirection::ServerToClient && managed.healthy { - recv_conns.push((i, managed.conn, managed.direction)); + recv_conns.push((i, managed.conn, managed.direction, managed.encryption)); } else { remaining.push(managed); } @@ -662,3 +901,522 @@ pub struct ConnectionStats { /// Total bytes received across all connections. pub total_bytes_received: u64, } + +#[cfg(test)] +mod tests { + use super::*; + + // ========================================================================== + // TcpDirection tests + // ========================================================================== + + #[test] + fn test_tcp_direction_from_int() { + // TCP_BOTH = 0 + assert_eq!(TcpDirection::from_int(0), TcpDirection::Both); + // TCP_SERVER_TO_CLIENT = 1 + assert_eq!(TcpDirection::from_int(1), TcpDirection::ServerToClient); + // TCP_CLIENT_TO_SERVER = 2 + assert_eq!(TcpDirection::from_int(2), TcpDirection::ClientToServer); + // Unknown values default to Both + assert_eq!(TcpDirection::from_int(99), TcpDirection::Both); + } + + #[test] + fn test_tcp_direction_can_send() { + // Both: can send and receive + assert!(TcpDirection::Both.can_send()); + // ClientToServer: send only + assert!(TcpDirection::ClientToServer.can_send()); + // ServerToClient: receive only + assert!(!TcpDirection::ServerToClient.can_send()); + } + + #[test] + fn test_tcp_direction_can_recv() { + // Both: can send and receive + assert!(TcpDirection::Both.can_recv()); + // ServerToClient: receive only + assert!(TcpDirection::ServerToClient.can_recv()); + // ClientToServer: send only + assert!(!TcpDirection::ClientToServer.can_recv()); + } + + #[test] + fn test_tcp_direction_half_connection_split() { + // In half-connection mode with 4 connections: + // - 2 should be ClientToServer (upload) + // - 2 should be ServerToClient (download) + let directions = [ + TcpDirection::ClientToServer, // Primary is always C2S + TcpDirection::ServerToClient, // Server assigns + TcpDirection::ClientToServer, // Server assigns + TcpDirection::ServerToClient, // Server assigns + ]; + + let send_count = directions.iter().filter(|d| d.can_send()).count(); + let recv_count = directions.iter().filter(|d| d.can_recv()).count(); + + assert_eq!(send_count, 2, "Should have 2 send-capable connections"); + assert_eq!(recv_count, 2, "Should have 2 recv-capable connections"); + } + + // ========================================================================== + // ManagedConnection tests (without actual network) + // ========================================================================== + + // Note: ManagedConnection requires a real VpnConnection, so we test + // the encryption methods separately. + + #[test] + fn test_tunnel_encryption_independent_state() { + // Each connection should have independent RC4 state + // This test verifies that creating multiple encryptions from same keys + // produces independent cipher streams + use crate::crypto::{Rc4KeyPair, RC4_KEY_SIZE}; + + let keys = Rc4KeyPair { + server_to_client: [0x01; RC4_KEY_SIZE], + client_to_server: [0x02; RC4_KEY_SIZE], + }; + + let mut enc1 = TunnelEncryption::new(&keys); + let mut enc2 = TunnelEncryption::new(&keys); + + // Same plaintext + let mut data1 = vec![0u8; 16]; + let mut data2 = vec![0u8; 16]; + + // Both should produce identical ciphertext (same initial state) + enc1.encrypt(&mut data1); + enc2.encrypt(&mut data2); + + assert_eq!(data1, data2, "Same keys should produce same ciphertext"); + + // But after encrypting different amounts, they diverge + let mut data3 = vec![0u8; 8]; // Advance enc1 by 8 more bytes + enc1.encrypt(&mut data3); + + let mut data4 = vec![0u8; 16]; + let mut data5 = vec![0u8; 16]; + enc1.encrypt(&mut data4); // enc1 is now at position 32 + enc2.encrypt(&mut data5); // enc2 is at position 16 + + assert_ne!( + data4, data5, + "Different stream positions should produce different ciphertext" + ); + } + + // ========================================================================== + // ConnectionStats tests + // ========================================================================== + + #[test] + fn test_connection_stats_default() { + let stats = ConnectionStats { + total_connections: 0, + healthy_connections: 0, + total_bytes_sent: 0, + total_bytes_received: 0, + }; + + assert_eq!(stats.total_connections, 0); + assert_eq!(stats.healthy_connections, 0); + assert_eq!(stats.total_bytes_sent, 0); + assert_eq!(stats.total_bytes_received, 0); + } + + #[test] + fn test_connection_stats_aggregation() { + // Simulate stats from multiple connections + let conn_stats = [ + (100u64, 200u64), // conn 0: 100 sent, 200 received + (150u64, 300u64), // conn 1 + (50u64, 100u64), // conn 2 + ]; + + let total_sent: u64 = conn_stats.iter().map(|(s, _)| s).sum(); + let total_recv: u64 = conn_stats.iter().map(|(_, r)| r).sum(); + + assert_eq!(total_sent, 300); + assert_eq!(total_recv, 600); + } + + // ========================================================================== + // Half-connection mode logic tests + // ========================================================================== + + #[test] + fn test_half_connection_primary_direction() { + // In half-connection mode, primary connection is ALWAYS ClientToServer + // This is per SoftEther Protocol.c specification + let half_connection = true; + + let expected = if half_connection { + TcpDirection::ClientToServer + } else { + TcpDirection::Both + }; + + assert_eq!(expected, TcpDirection::ClientToServer); + } + + #[test] + fn test_connection_distribution_4_conns() { + // Test typical 4-connection half-connection setup + // Server should assign directions to balance send/recv + // + // Typical distribution: + // - Connection 0: ClientToServer (primary, always C2S) + // - Connection 1: ServerToClient + // - Connection 2: ClientToServer + // - Connection 3: ServerToClient + let directions = simulate_connection_distribution(4); + + let c2s_count = directions + .iter() + .filter(|d| **d == TcpDirection::ClientToServer) + .count(); + let s2c_count = directions + .iter() + .filter(|d| **d == TcpDirection::ServerToClient) + .count(); + + // Should be roughly balanced (2 and 2) + assert!( + (1..=3).contains(&c2s_count), + "C2S count {c2s_count} not in expected range" + ); + assert!( + (1..=3).contains(&s2c_count), + "S2C count {s2c_count} not in expected range" + ); + assert_eq!(c2s_count + s2c_count, 4, "Total should be 4 connections"); + } + + #[test] + fn test_connection_distribution_8_conns() { + // Test 8-connection half-connection setup + let directions = simulate_connection_distribution(8); + + let c2s_count = directions + .iter() + .filter(|d| **d == TcpDirection::ClientToServer) + .count(); + let s2c_count = directions + .iter() + .filter(|d| **d == TcpDirection::ServerToClient) + .count(); + + // Should be balanced (4 and 4) + assert!( + (3..=5).contains(&c2s_count), + "C2S count {c2s_count} not in expected range" + ); + assert!( + (3..=5).contains(&s2c_count), + "S2C count {s2c_count} not in expected range" + ); + } + + /// Simulate how server assigns directions to N connections. + /// Based on SoftEther Protocol.c: alternates after primary. + fn simulate_connection_distribution(n: usize) -> Vec { + let mut directions = Vec::with_capacity(n); + + for i in 0..n { + if i == 0 { + // Primary is always ClientToServer + directions.push(TcpDirection::ClientToServer); + } else { + // Server alternates remaining connections + // Odd index -> ServerToClient, Even index -> ClientToServer + if i % 2 == 1 { + directions.push(TcpDirection::ServerToClient); + } else { + directions.push(TcpDirection::ClientToServer); + } + } + } + + directions + } + + // ========================================================================== + // Round-robin selection tests + // ========================================================================== + + #[test] + fn test_round_robin_send_selection() { + // Simulate round-robin selection among send-capable connections + let directions = [ + TcpDirection::ClientToServer, // 0: can send + TcpDirection::ServerToClient, // 1: cannot send + TcpDirection::ClientToServer, // 2: can send + TcpDirection::ServerToClient, // 3: cannot send + ]; + + let send_indices: Vec = directions + .iter() + .enumerate() + .filter(|(_, d)| d.can_send()) + .map(|(i, _)| i) + .collect(); + + assert_eq!(send_indices, vec![0, 2], "Only indices 0 and 2 can send"); + + // Round-robin should cycle through send-capable connections + let mut send_index = 0; + let selected: Vec = (0..6) + .map(|_| { + send_index = (send_index + 1) % send_indices.len(); + send_indices[send_index] + }) + .collect(); + + // Should cycle: 2, 0, 2, 0, 2, 0 + assert_eq!(selected, vec![2, 0, 2, 0, 2, 0]); + } + + #[test] + fn test_round_robin_recv_selection() { + // Simulate round-robin selection among recv-capable connections + let directions = [ + TcpDirection::ClientToServer, // 0: cannot recv + TcpDirection::ServerToClient, // 1: can recv + TcpDirection::Both, // 2: can recv + TcpDirection::ServerToClient, // 3: can recv + ]; + + let recv_indices: Vec = directions + .iter() + .enumerate() + .filter(|(_, d)| d.can_recv()) + .map(|(i, _)| i) + .collect(); + + assert_eq!(recv_indices, vec![1, 2, 3], "Indices 1, 2, 3 can recv"); + } + + // ========================================================================== + // Connection extraction tests (for ConcurrentReader) + // ========================================================================== + + #[test] + fn test_take_recv_connections_logic() { + // Test the logic of extracting recv-only connections + // ServerToClient should be extracted + // Both and ClientToServer should remain + + let directions = [ + TcpDirection::ClientToServer, // 0: remains (for sending) + TcpDirection::ServerToClient, // 1: extracted (recv-only) + TcpDirection::Both, // 2: remains (bidirectional) + TcpDirection::ServerToClient, // 3: extracted (recv-only) + ]; + + let mut extracted = Vec::new(); + let mut remaining = Vec::new(); + + for (i, dir) in directions.iter().enumerate() { + if *dir == TcpDirection::ServerToClient { + extracted.push(i); + } else { + remaining.push(i); + } + } + + assert_eq!(extracted, vec![1, 3], "S2C connections should be extracted"); + assert_eq!( + remaining, + vec![0, 2], + "C2S and Both should remain for sending" + ); + } + + #[test] + fn test_has_send_connections_after_extraction() { + // After extracting recv-only connections, we should still have send connections + let remaining_directions = [ + TcpDirection::ClientToServer, // 0: can send + TcpDirection::Both, // 2: can send + ]; + + let has_send = remaining_directions.iter().any(|d| d.can_send()); + assert!( + has_send, + "Should still have send connections after extraction" + ); + } + + // ========================================================================== + // Pending bytes / load balancing tests + // ========================================================================== + + #[test] + fn test_estimated_pending_initial() { + // New connection should have zero pending + let pending = 0u64; + assert_eq!(pending, 0); + } + + #[test] + fn test_estimated_pending_decay() { + // Simulate pending bytes decay over time + let initial_pending = 10000u64; + let elapsed_us = 500u64; // 500 microseconds + let decay_rate = 10u64; // 10 bytes per microsecond (~10 MB/s) + + let decay = elapsed_us.saturating_mul(decay_rate); + let remaining = initial_pending.saturating_sub(decay); + + assert_eq!(decay, 5000); + assert_eq!(remaining, 5000); + } + + #[test] + fn test_estimated_pending_full_decay() { + // After enough time, pending should decay to zero + let initial_pending = 10000u64; + let elapsed_us = 2000u64; // 2ms = 2000 us + let decay_rate = 10u64; + + let decay = elapsed_us.saturating_mul(decay_rate); + let remaining = initial_pending.saturating_sub(decay); + + assert_eq!(remaining, 0, "Pending should fully decay"); + } + + #[test] + fn test_record_write_updates_pending() { + // Simulate recording a write + let mut pending = 0u64; + let bytes_sent = 0u64; + + // Write 1500 bytes + let write_size = 1500u64; + pending = pending.saturating_add(write_size); + let new_bytes_sent = bytes_sent + write_size; + + assert_eq!(pending, 1500); + assert_eq!(new_bytes_sent, 1500); + } + + #[test] + fn test_least_loaded_selection() { + // Simulate selecting least-loaded connection + let pending_bytes = [ + (0usize, 5000u64), // Connection 0: 5KB pending + (1usize, 1000u64), // Connection 1: 1KB pending (least loaded) + (2usize, 8000u64), // Connection 2: 8KB pending + ]; + + let (selected_idx, _) = pending_bytes + .iter() + .min_by_key(|(_, pending)| *pending) + .copied() + .unwrap(); + + assert_eq!( + selected_idx, 1, + "Should select connection with least pending" + ); + } + + #[test] + fn test_least_loaded_with_equal_pending() { + // When pending is equal, should select consistently (first min) + let pending_bytes = [(0usize, 1000u64), (1usize, 1000u64), (2usize, 1000u64)]; + + let (selected_idx, _) = pending_bytes + .iter() + .min_by_key(|(_, pending)| *pending) + .copied() + .unwrap(); + + assert_eq!(selected_idx, 0, "Should select first connection when equal"); + } + + #[test] + fn test_load_distribution_simulation() { + // Simulate how load distributes with least-loaded selection + let mut pending = [0u64, 0u64, 0u64]; // 3 connections + let packet_size = 1500u64; + + // Simulate sending 9 packets + for _ in 0..9 { + // Find least loaded + let (idx, _) = pending.iter().enumerate().min_by_key(|(_, p)| *p).unwrap(); + + pending[idx] += packet_size; + } + + // Each connection should have 3 packets (4500 bytes) + assert_eq!(pending[0], 4500); + assert_eq!(pending[1], 4500); + assert_eq!(pending[2], 4500); + } + + // ========================================================================== + // Timeout configuration tests + // ========================================================================== + + #[test] + fn test_connection_timeout_detection() { + use std::time::Duration; + + // Simulate idle detection + let timeout = Duration::from_secs(30); + let last_activity_secs = 35u64; // 35 seconds ago + + let is_idle = last_activity_secs > timeout.as_secs(); + assert!(is_idle, "Connection should be detected as idle"); + + let recent_activity_secs = 10u64; + let is_active = recent_activity_secs <= timeout.as_secs(); + assert!(is_active, "Recent connection should not be idle"); + } + + // ========================================================================== + // Packet encoding/decoding consistency tests + // ========================================================================== + + #[test] + fn test_session_key_validity() { + // Session key should be non-empty for additional connections + let session_key = [0x01, 0x02, 0x03, 0x04, 0x05]; + + assert!(!session_key.is_empty(), "Session key should not be empty"); + assert!( + session_key.len() >= 4, + "Session key should have reasonable length" + ); + } + + #[test] + fn test_additional_connection_pack_format() { + use crate::protocol::Pack; + + // Verify the pack format for additional connection auth + let session_key = vec![0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]; + + let mut pack = Pack::new(); + pack.add_str("method", "additional_connect"); + pack.add_data("session_key", session_key.clone()); + + // Verify fields were added + assert_eq!(pack.get_str("method"), Some("additional_connect")); + + // Serialize and verify it's valid + let bytes = pack.to_bytes(); + assert!( + !bytes.is_empty(), + "Pack should serialize to non-empty bytes" + ); + + // Deserialize and verify roundtrip + let pack2 = Pack::deserialize(&bytes).expect("Should deserialize"); + assert_eq!(pack2.get_str("method"), Some("additional_connect")); + } +} diff --git a/src/ffi/android.rs b/src/ffi/android.rs index 6a5ae0a..7772c67 100644 --- a/src/ffi/android.rs +++ b/src/ffi/android.rs @@ -77,36 +77,64 @@ extern "C" fn jni_on_connected(context: *mut std::ffi::c_void, session: *const S let session = unsafe { &*session }; if let Ok(mut env) = ctx.jvm.attach_current_thread() { - // Extract server IP string - let server_ip = unsafe { + // Extract server IP strings + let connected_server_ip = unsafe { std::ffi::CStr::from_ptr(session.connected_server_ip.as_ptr()) .to_string_lossy() .into_owned() }; + let original_server_ip = unsafe { + std::ffi::CStr::from_ptr(session.original_server_ip.as_ptr()) + .to_string_lossy() + .into_owned() + }; - let server_ip_jstring = env - .new_string(&server_ip) + let connected_ip_jstring = env + .new_string(&connected_server_ip) + .unwrap_or_else(|_| env.new_string("").unwrap()); + let original_ip_jstring = env + .new_string(&original_server_ip) .unwrap_or_else(|_| env.new_string("").unwrap()); - // Create MAC address byte array + // Create byte arrays for MAC addresses and IPv6 data let mac_array = env .byte_array_from_slice(&session.mac_address) .unwrap_or_else(|_| env.byte_array_from_slice(&[0u8; 6]).unwrap()); - + let gateway_mac_array = env + .byte_array_from_slice(&session.gateway_mac) + .unwrap_or_else(|_| env.byte_array_from_slice(&[0u8; 6]).unwrap()); + let ipv6_array = env + .byte_array_from_slice(&session.ipv6_address) + .unwrap_or_else(|_| env.byte_array_from_slice(&[0u8; 16]).unwrap()); + let dns1_v6_array = env + .byte_array_from_slice(&session.dns1_v6) + .unwrap_or_else(|_| env.byte_array_from_slice(&[0u8; 16]).unwrap()); + let dns2_v6_array = env + .byte_array_from_slice(&session.dns2_v6) + .unwrap_or_else(|_| env.byte_array_from_slice(&[0u8; 16]).unwrap()); + + // Call Kotlin callback with full session data + // Signature: (IIIIILjava/lang/String;Ljava/lang/String;II[B[B[BI[B[B)V let _ = env.call_method( &ctx.bridge_ref, "onNativeConnected", - "(IIIIILjava/lang/String;II[B)V", + "(IIIIILjava/lang/String;Ljava/lang/String;II[B[B[BI[B[B)V", &[ JValue::Int(session.ip_address as i32), JValue::Int(session.subnet_mask as i32), JValue::Int(session.gateway as i32), JValue::Int(session.dns1 as i32), JValue::Int(session.dns2 as i32), - JValue::Object(&server_ip_jstring), + JValue::Object(&connected_ip_jstring), + JValue::Object(&original_ip_jstring), JValue::Int(session.server_version as i32), JValue::Int(session.server_build as i32), JValue::Object(&mac_array), + JValue::Object(&gateway_mac_array), + JValue::Object(&ipv6_array), + JValue::Int(session.ipv6_prefix_len as i32), + JValue::Object(&dns1_v6_array), + JValue::Object(&dns2_v6_array), ], ); } @@ -248,14 +276,20 @@ pub extern "system" fn Java_com_worxvpn_app_vpn_SoftEtherBridge_nativeCreate( server: JString, port: jint, hub: JString, + // Authentication + auth_method: jint, username: JString, password_hash: JString, + password: JString, + certificate_pem: JString, + private_key_pem: JString, // TLS Settings skip_tls_verify: jboolean, custom_ca_pem: JString, cert_fingerprint_sha256: JString, // Connection Settings max_connections: jint, + half_connection: jboolean, timeout_seconds: jint, mtu: jint, // Protocol Features @@ -263,6 +297,7 @@ pub extern "system" fn Java_com_worxvpn_app_vpn_SoftEtherBridge_nativeCreate( use_compress: jboolean, udp_accel: jboolean, qos: jboolean, + ip_version: jint, // Session Mode nat_traversal: jboolean, monitor_mode: jboolean, @@ -298,10 +333,12 @@ pub extern "system" fn Java_com_worxvpn_app_vpn_SoftEtherBridge_nativeCreate( Some(s) => s, None => return 0, }; - let password_hash_str = match get_string(&mut env, &password_hash) { - Some(s) => s, - None => return 0, - }; + + // Get authentication strings (may be null depending on auth method) + let password_hash_str = get_string(&mut env, &password_hash).unwrap_or_default(); + let password_str = get_string(&mut env, &password).unwrap_or_default(); + let certificate_pem_str = get_string(&mut env, &certificate_pem).unwrap_or_default(); + let private_key_pem_str = get_string(&mut env, &private_key_pem).unwrap_or_default(); // Get optional routing strings let ipv4_include_str = get_string(&mut env, &ipv4_include).unwrap_or_default(); @@ -337,10 +374,14 @@ pub extern "system" fn Java_com_worxvpn_app_vpn_SoftEtherBridge_nativeCreate( Some(s) => s, None => return 0, }; - let password_hash_cstr = match to_cstring(&password_hash_str) { - Some(s) => s, - None => return 0, - }; + + // Authentication CStrings + let password_hash_cstr = to_cstring(&password_hash_str); + let password_cstr = to_cstring(&password_str); + let certificate_pem_cstr = to_cstring(&certificate_pem_str); + let private_key_pem_cstr = to_cstring(&private_key_pem_str); + + // Routing CStrings let ipv4_include_cstr = to_cstring(&ipv4_include_str); let ipv4_exclude_cstr = to_cstring(&ipv4_exclude_str); let ipv6_include_cstr = to_cstring(&ipv6_include_str); @@ -359,17 +400,46 @@ pub extern "system" fn Java_com_worxvpn_app_vpn_SoftEtherBridge_nativeCreate( let static_ipv6_dns1_cstr = to_cstring(&static_ipv6_dns1_str); let static_ipv6_dns2_cstr = to_cstring(&static_ipv6_dns2_str); + // Convert auth_method int to enum + let auth_method_enum = match auth_method { + 0 => SoftEtherAuthMethod::StandardPassword, + 1 => SoftEtherAuthMethod::RadiusOrNtDomain, + 2 => SoftEtherAuthMethod::Certificate, + 3 => SoftEtherAuthMethod::Anonymous, + _ => SoftEtherAuthMethod::StandardPassword, + }; + + // Convert ip_version int to enum + let ip_version_enum = match ip_version { + 0 => SoftEtherIpVersion::Auto, + 1 => SoftEtherIpVersion::IPv4Only, + 2 => SoftEtherIpVersion::IPv6Only, + _ => SoftEtherIpVersion::Auto, + }; + // Create config with all options let config = SoftEtherConfig { server: server_cstr.as_ptr(), port: port as u32, hub: hub_cstr.as_ptr(), - auth_method: SoftEtherAuthMethod::StandardPassword, // Android uses standard password auth + auth_method: auth_method_enum, username: username_cstr.as_ptr(), - password_hash: password_hash_cstr.as_ptr(), - password: std::ptr::null(), // Not used for standard password auth - certificate_pem: std::ptr::null(), // Not used for standard password auth - private_key_pem: std::ptr::null(), // Not used for standard password auth + password_hash: password_hash_cstr + .as_ref() + .map(|s| s.as_ptr()) + .unwrap_or(std::ptr::null()), + password: password_cstr + .as_ref() + .map(|s| s.as_ptr()) + .unwrap_or(std::ptr::null()), + certificate_pem: certificate_pem_cstr + .as_ref() + .map(|s| s.as_ptr()) + .unwrap_or(std::ptr::null()), + private_key_pem: private_key_pem_cstr + .as_ref() + .map(|s| s.as_ptr()) + .unwrap_or(std::ptr::null()), skip_tls_verify: if skip_tls_verify != 0 { 1 } else { 0 }, custom_ca_pem: custom_ca_pem_cstr .as_ref() @@ -380,10 +450,10 @@ pub extern "system" fn Java_com_worxvpn_app_vpn_SoftEtherBridge_nativeCreate( .map(|s| s.as_ptr()) .unwrap_or(std::ptr::null()), max_connections: max_connections as u32, - half_connection: 0, // Android doesn't expose this yet, default to false + half_connection: if half_connection != 0 { 1 } else { 0 }, timeout_seconds: timeout_seconds as u32, mtu: mtu as u32, - ip_version: SoftEtherIpVersion::Auto, // Android doesn't expose this yet, default to Auto + ip_version: ip_version_enum, use_encrypt: if use_encrypt != 0 { 1 } else { 0 }, use_compress: if use_compress != 0 { 1 } else { 0 }, udp_accel: if udp_accel != 0 { 1 } else { 0 }, @@ -616,6 +686,36 @@ pub extern "system" fn Java_com_worxvpn_app_vpn_SoftEtherBridge_nativeGetSession env.new_string(&server_ip).unwrap_or_default() } +/// Get original session server IP as string (before cluster redirect). +#[no_mangle] +pub extern "system" fn Java_com_worxvpn_app_vpn_SoftEtherBridge_nativeGetSessionOriginalServerIP< + 'local, +>( + env: JNIEnv<'local>, + _class: JClass<'local>, + handle: jlong, +) -> JString<'local> { + if handle == 0 { + return JString::default(); + } + + let mut session = SoftEtherSession::default(); + let result = + unsafe { softether_get_session(handle as SoftEtherHandle, &mut session as *mut _) }; + + if result != SoftEtherResult::Ok { + return JString::default(); + } + + let server_ip = unsafe { + std::ffi::CStr::from_ptr(session.original_server_ip.as_ptr()) + .to_string_lossy() + .into_owned() + }; + + env.new_string(&server_ip).unwrap_or_default() +} + /// Get session MAC address as byte array. #[no_mangle] pub extern "system" fn Java_com_worxvpn_app_vpn_SoftEtherBridge_nativeGetSessionMAC<'local>( @@ -641,6 +741,66 @@ pub extern "system" fn Java_com_worxvpn_app_vpn_SoftEtherBridge_nativeGetSession } } +/// Get session gateway MAC address as byte array. +#[no_mangle] +pub extern "system" fn Java_com_worxvpn_app_vpn_SoftEtherBridge_nativeGetSessionGatewayMAC< + 'local, +>( + env: JNIEnv<'local>, + _class: JClass<'local>, + handle: jlong, +) -> jbyteArray { + if handle == 0 { + return std::ptr::null_mut(); + } + + let mut session = SoftEtherSession::default(); + let result = + unsafe { softether_get_session(handle as SoftEtherHandle, &mut session as *mut _) }; + + if result != SoftEtherResult::Ok { + return std::ptr::null_mut(); + } + + match env.byte_array_from_slice(&session.gateway_mac) { + Ok(arr) => arr.into_raw(), + Err(_) => std::ptr::null_mut(), + } +} + +/// Get session IPv6 data as byte array. +/// Format: [ipv6_address:16][prefix_len:1][dns1_v6:16][dns2_v6:16] = 49 bytes +#[no_mangle] +pub extern "system" fn Java_com_worxvpn_app_vpn_SoftEtherBridge_nativeGetSessionIPv6<'local>( + env: JNIEnv<'local>, + _class: JClass<'local>, + handle: jlong, +) -> jbyteArray { + if handle == 0 { + return std::ptr::null_mut(); + } + + let mut session = SoftEtherSession::default(); + let result = + unsafe { softether_get_session(handle as SoftEtherHandle, &mut session as *mut _) }; + + if result != SoftEtherResult::Ok { + return std::ptr::null_mut(); + } + + // Pack IPv6 data: [address:16][prefixLen:1][dns1:16][dns2:16] = 49 bytes + let mut ipv6_data = Vec::with_capacity(49); + ipv6_data.extend_from_slice(&session.ipv6_address); + ipv6_data.push(session.ipv6_prefix_len); + ipv6_data.extend_from_slice(&session.dns1_v6); + ipv6_data.extend_from_slice(&session.dns2_v6); + + match env.byte_array_from_slice(&ipv6_data) { + Ok(arr) => arr.into_raw(), + Err(_) => std::ptr::null_mut(), + } +} + /// Get connection statistics as long array. /// Returns [bytes_sent, bytes_received, packets_sent, packets_received, uptime_secs, active_connections, reconnect_count, packets_dropped] #[no_mangle] @@ -806,3 +966,25 @@ pub extern "system" fn Java_com_worxvpn_app_vpn_SoftEtherBridge_getVersion<'loca env.new_string(&version).unwrap_or_default() } + +/// Get last error message. +#[no_mangle] +pub extern "system" fn Java_com_worxvpn_app_vpn_SoftEtherBridge_nativeGetLastError<'local>( + env: JNIEnv<'local>, + _class: JClass<'local>, + _handle: jlong, +) -> JString<'local> { + let error_ptr = unsafe { softether_get_last_error() }; + + if error_ptr.is_null() { + return JString::default(); + } + + let error_str = unsafe { + std::ffi::CStr::from_ptr(error_ptr) + .to_string_lossy() + .into_owned() + }; + + env.new_string(&error_str).unwrap_or_default() +} diff --git a/src/ffi/client.rs b/src/ffi/client.rs index 53ba243..17b6347 100644 --- a/src/ffi/client.rs +++ b/src/ffi/client.rs @@ -3,7 +3,7 @@ //! This module provides C-callable functions for the VPN client with actual //! connection logic wired to the SoftEther protocol implementation. -use std::ffi::{c_char, c_int, CStr}; +use std::ffi::{c_char, c_int, CStr, CString}; use std::net::Ipv4Addr; use std::sync::atomic::{AtomicBool, AtomicU64, AtomicU8, Ordering}; use std::sync::{Arc, Mutex}; @@ -24,6 +24,28 @@ use crate::protocol::{ CONTENT_TYPE_SIGNATURE, SIGNATURE_TARGET, VPN_SIGNATURE, VPN_TARGET, }; +// ============================================================================= +// Thread-local error storage for FFI +// ============================================================================= + +thread_local! { + static LAST_ERROR: std::cell::RefCell> = const { std::cell::RefCell::new(None) }; +} + +/// Set the last error message (internal helper). +fn set_last_error(msg: impl Into) { + LAST_ERROR.with(|e| { + *e.borrow_mut() = Some(msg.into()); + }); +} + +/// Clear the last error. +fn clear_last_error() { + LAST_ERROR.with(|e| { + *e.borrow_mut() = None; + }); +} + /// Channel capacity for packet queues - larger buffer for better throughput const PACKET_QUEUE_SIZE: usize = 128; @@ -170,6 +192,41 @@ unsafe fn cstr_to_string(ptr: *const c_char) -> Option { // FFI Functions - C ABI // ============================================================================= +/// Get the last error message. +/// +/// Returns a pointer to a null-terminated string describing the last error, +/// or null if no error occurred. The string is valid until the next FFI call +/// on the same thread. +/// +/// # Safety +/// - The returned pointer is only valid until the next FFI call on this thread. +/// - Caller must NOT free the returned pointer. +#[no_mangle] +pub unsafe extern "C" fn softether_get_last_error() -> *const c_char { + thread_local! { + static ERROR_BUF: std::cell::RefCell> = const { std::cell::RefCell::new(None) }; + } + + LAST_ERROR.with(|e| { + let err = e.borrow(); + match err.as_ref() { + Some(msg) => ERROR_BUF.with(|buf| { + let cstr = CString::new(msg.as_str()).unwrap_or_default(); + let ptr = cstr.as_ptr(); + *buf.borrow_mut() = Some(cstr); + ptr + }), + None => std::ptr::null(), + } + }) +} + +/// Clear the last error message. +#[no_mangle] +pub extern "C" fn softether_clear_last_error() { + clear_last_error(); +} + /// Create a new SoftEther VPN client. /// /// # Safety @@ -181,23 +238,33 @@ pub unsafe extern "C" fn softether_create( config: *const SoftEtherConfig, callbacks: *const SoftEtherCallbacks, ) -> SoftEtherHandle { + clear_last_error(); + if config.is_null() { + set_last_error("config is null"); return NULL_HANDLE; } let config = &*config; if !config.is_valid() { + set_last_error("config validation failed: check server, hub, username, password_hash, port (1-65535), max_connections (1-32), mtu (576-1500)"); return NULL_HANDLE; } // Parse configuration let server = match cstr_to_string(config.server) { Some(s) => s, - None => return NULL_HANDLE, + None => { + set_last_error("server is null or invalid UTF-8"); + return NULL_HANDLE; + } }; let hub = match cstr_to_string(config.hub) { Some(s) => s, - None => return NULL_HANDLE, + None => { + set_last_error("hub is null or invalid UTF-8"); + return NULL_HANDLE; + } }; let username = cstr_to_string(config.username).unwrap_or_default(); let password_hash = cstr_to_string(config.password_hash); @@ -542,7 +609,7 @@ async fn connect_and_run( &callbacks, 2, &format!( - "[RUST] User already logged in. Waiting {RETRY_DELAY_SECS}s for old session to expire... (attempt {attempt}/{MAX_USER_IN_USE_RETRIES})" + "User already logged in. Waiting {RETRY_DELAY_SECS}s for old session to expire... (attempt {attempt}/{MAX_USER_IN_USE_RETRIES})" ), ); update_state(&atomic_state, &callbacks, SoftEtherState::Connecting); @@ -552,7 +619,7 @@ async fn connect_and_run( &callbacks, 3, &format!( - "[RUST] User already logged in - max retries ({MAX_USER_IN_USE_RETRIES}) exceeded" + "User already logged in - max retries ({MAX_USER_IN_USE_RETRIES}) exceeded" ), ); return Err(crate::error::Error::UserAlreadyLoggedIn); @@ -582,38 +649,27 @@ async fn connect_and_run_inner( } } - log_message(callbacks, 1, "[RUST] connect_and_run started"); log_message( callbacks, 1, - &format!("[RUST] Connecting to {}:{}", config.server, config.port), + &format!("Connecting to {}:{}", config.server, config.port), ); log_message( callbacks, 1, - &format!("[RUST] Hub: {}, User: {}", config.hub, config.auth.username), - ); - log_message( - callbacks, - 1, - &format!("[RUST] Skip TLS verify: {}", config.skip_tls_verify), + &format!("Hub: {}, User: {}", config.hub, config.auth.username), ); // Resolve server IP - log_message(callbacks, 1, "[RUST] Resolving server IP..."); let server_ip = match resolve_server_ip(&config.server) { - Ok(ip) => { - log_message(callbacks, 1, &format!("[RUST] Resolved server IP: {ip}")); - ip - } + Ok(ip) => ip, Err(e) => { - log_message(callbacks, 3, &format!("[RUST] DNS resolution failed: {e}")); + log_message(callbacks, 3, &format!("DNS resolution failed: {e}")); return Err(e); } }; // Connect TCP with socket protection - log_message(callbacks, 1, "[RUST] Establishing TCP/TLS connection..."); // Create socket protection closure // Note: We wrap the raw pointer to make it Send-safe for the closure @@ -629,25 +685,16 @@ async fn connect_and_run_inner( let mut conn = match VpnConnection::connect_with_protect(config, protect_fn).await { Ok(c) => { - log_message( - callbacks, - 1, - "[RUST] TCP/TLS connection established (protected)", - ); + log_message(callbacks, 1, "TCP/TLS connection established (protected)"); c } Err(e) => { - log_message( - callbacks, - 3, - &format!("[RUST] TCP/TLS connection failed: {e}"), - ); + log_message(callbacks, 3, &format!("TCP/TLS connection failed: {e}")); return Err(e); } }; // Notify state: Handshaking - log_message(callbacks, 1, "[RUST] Starting HTTP handshake..."); update_state(atomic_state, callbacks, SoftEtherState::Handshaking); // HTTP handshake @@ -657,48 +704,33 @@ async fn connect_and_run_inner( callbacks, 1, &format!( - "[RUST] Server: {} v{} build {}", + "Server: {} v{} build {}", h.server_string, h.server_version, h.server_build ), ); h } Err(e) => { - log_message(callbacks, 3, &format!("[RUST] Handshake failed: {e}")); + log_message(callbacks, 3, &format!("Handshake failed: {e}")); return Err(e); } }; // Notify state: Authenticating - log_message(callbacks, 1, "[RUST] Starting authentication..."); update_state(atomic_state, callbacks, SoftEtherState::Authenticating); // Authenticate - log_message(callbacks, 1, "[RUST] >>> About to call authenticate() <<<"); let mut auth_result = match authenticate(&mut conn, config, &hello, callbacks).await { Ok(r) => { - log_message(callbacks, 1, "[RUST] Authentication successful"); + log_message(callbacks, 1, "Authentication successful"); r } Err(e) => { - log_message(callbacks, 3, &format!("[RUST] Authentication failed: {e}")); + log_message(callbacks, 3, &format!("Authentication failed: {e}")); return Err(e); } }; - log_message( - callbacks, - 1, - &format!( - "[RUST] Initial auth: session_key={} bytes, redirect={:?}", - auth_result.session_key.len(), - auth_result - .redirect - .as_ref() - .map(|r| format!("{}:{}", r.ip_string(), r.port)) - ), - ); - // Handle cluster redirect if present // NOTE: When redirect is present, session_key will be empty - we get it from redirect server let (active_conn, final_auth, actual_server_ip, actual_server_addr, actual_server_port) = @@ -707,10 +739,7 @@ async fn connect_and_run_inner( log_message( callbacks, 1, - &format!( - "[RUST] Cluster redirect to {}:{}", - redirect_ip, redirect.port - ), + &format!("Cluster redirect to {}:{}", redirect_ip, redirect.port), ); // Send empty Pack acknowledgment before closing connection @@ -743,18 +772,14 @@ async fn connect_and_run_inner( ) } Err(e) => { - log_message(callbacks, 3, &format!("[RUST] Redirect failed: {e}")); + log_message(callbacks, 3, &format!("Redirect failed: {e}")); return Err(e); } } } else { // No redirect - check session key now if auth_result.session_key.is_empty() { - log_message( - callbacks, - 3, - "[RUST] No session key received and no redirect", - ); + log_message(callbacks, 3, "No session key received and no redirect"); return Err(crate::error::Error::AuthenticationFailed( "No session key received".into(), )); @@ -770,27 +795,18 @@ async fn connect_and_run_inner( // Verify we have session key after redirect handling if final_auth.session_key.is_empty() { - log_message(callbacks, 3, "[RUST] No session key after redirect"); + log_message(callbacks, 3, "No session key after redirect"); return Err(crate::error::Error::AuthenticationFailed( "No session key received from redirect server".into(), )); } - log_message( - callbacks, - 1, - &format!( - "[RUST] Session established: {} bytes session key", - final_auth.session_key.len() - ), - ); - // Create connection manager for packet I/O - log_message(callbacks, 1, "[RUST] Creating connection manager..."); // Determine if we need raw TCP mode (when use_encrypt=false and no RC4 keys) let use_raw_mode = !config.use_encrypt && final_auth.rc4_key_pair.is_none(); + // Pass RC4 key pair for per-connection encryption (each connection gets fresh cipher state) let mut conn_mgr = ConnectionManager::new( active_conn, config, @@ -798,6 +814,7 @@ async fn connect_and_run_inner( &actual_server_addr, actual_server_port, use_raw_mode, + final_auth.rc4_key_pair.clone(), ); // Generate MAC address for DHCP @@ -805,17 +822,7 @@ async fn connect_and_run_inner( crate::crypto::fill_random(&mut mac); mac[0] = (mac[0] | 0x02) & 0xFE; // Local/unicast - log_message( - callbacks, - 1, - &format!( - "[RUST] Generated MAC: {:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}", - mac[0], mac[1], mac[2], mac[3], mac[4], mac[5] - ), - ); - // Perform DHCP to get IP configuration - log_message(callbacks, 1, "[RUST] Starting DHCP..."); update_state(atomic_state, callbacks, SoftEtherState::EstablishingTunnel); // In half-connection mode, we need to temporarily enable bidirectional mode @@ -827,11 +834,6 @@ async fn connect_and_run_inner( let (dhcp_config, dhcpv6_config) = match config.ip_version { crate::config::IpVersion::Auto => { // Auto: Try IPv4 DHCP (required), then try DHCPv6 (optional) - log_message( - callbacks, - 1, - "[RUST] IP version: Auto (IPv4 required, IPv6 optional)", - ); let dhcp = match perform_dhcp(&mut conn_mgr, mac, callbacks, config.use_compress).await { Ok(cfg) => { @@ -839,7 +841,7 @@ async fn connect_and_run_inner( callbacks, 1, &format!( - "[RUST] DHCP complete: IP={}, Gateway={:?}, DNS={:?}", + "DHCP complete: IP={}, Gateway={:?}, DNS={:?}", cfg.ip, cfg.gateway, cfg.dns1 ), ); @@ -849,33 +851,21 @@ async fn connect_and_run_inner( if let Some(dir) = original_direction { conn_mgr.restore_primary_direction(dir); } - log_message(callbacks, 3, &format!("[RUST] DHCP failed: {e}")); + log_message(callbacks, 3, &format!("DHCP failed: {e}")); return Err(e); } }; // Try DHCPv6 (optional) - log_message(callbacks, 1, "[RUST] Attempting DHCPv6 for IPv6 address..."); let dhcpv6 = perform_dhcpv6(&mut conn_mgr, mac, callbacks, config.use_compress).await; if dhcpv6.is_some() { - log_message( - callbacks, - 1, - "[RUST] DHCPv6 successful - dual-stack configured", - ); - } else { - log_message(callbacks, 1, "[RUST] DHCPv6 not available - IPv4 only"); + log_message(callbacks, 1, "DHCPv6: Dual-stack enabled"); } (Some(dhcp), dhcpv6) } crate::config::IpVersion::IPv4Only => { // IPv4 only: Only perform DHCP, skip DHCPv6 - log_message( - callbacks, - 1, - "[RUST] IP version: IPv4 only (skipping DHCPv6)", - ); let dhcp = match perform_dhcp(&mut conn_mgr, mac, callbacks, config.use_compress).await { Ok(cfg) => { @@ -883,7 +873,7 @@ async fn connect_and_run_inner( callbacks, 1, &format!( - "[RUST] DHCP complete: IP={}, Gateway={:?}, DNS={:?}", + "DHCP complete: IP={}, Gateway={:?}, DNS={:?}", cfg.ip, cfg.gateway, cfg.dns1 ), ); @@ -893,7 +883,7 @@ async fn connect_and_run_inner( if let Some(dir) = original_direction { conn_mgr.restore_primary_direction(dir); } - log_message(callbacks, 3, &format!("[RUST] DHCP failed: {e}")); + log_message(callbacks, 3, &format!("DHCP failed: {e}")); return Err(e); } }; @@ -901,30 +891,17 @@ async fn connect_and_run_inner( } crate::config::IpVersion::IPv6Only => { // IPv6 only: Only perform DHCPv6, skip DHCP - log_message( - callbacks, - 1, - "[RUST] IP version: IPv6 only (skipping IPv4 DHCP)", - ); let dhcpv6 = perform_dhcpv6(&mut conn_mgr, mac, callbacks, config.use_compress).await; match dhcpv6 { Some(cfg) => { - log_message( - callbacks, - 1, - "[RUST] DHCPv6 complete: IPv6 address obtained", - ); + log_message(callbacks, 1, "DHCPv6: IPv6 address obtained"); (None, Some(cfg)) } None => { if let Some(dir) = original_direction { conn_mgr.restore_primary_direction(dir); } - log_message( - callbacks, - 3, - "[RUST] DHCPv6 failed - no IPv6 address available", - ); + log_message(callbacks, 3, "DHCPv6 failed - no IPv6 address available"); return Err(crate::error::Error::DhcpFailed("DHCPv6 failed".into())); } } @@ -938,21 +915,12 @@ async fn connect_and_run_inner( // Establish additional connections if max_connections > 1 if config.max_connections > 1 { - log_message( - callbacks, - 1, - &format!( - "[RUST] Multi-connection mode: establishing {} additional connections...", - config.max_connections - 1 - ), - ); - if let Err(e) = conn_mgr.establish_additional_connections().await { // Log but don't fail - we can continue with fewer connections log_message( callbacks, 2, - &format!("[RUST] Warning: Failed to establish all additional connections: {e}"), + &format!("Multi-connection setup warning: {e}"), ); } @@ -961,14 +929,8 @@ async fn connect_and_run_inner( callbacks, 1, &format!( - "[RUST] Connection pool: {}/{} connections active (half-connection mode: {})", - stats.healthy_connections, - config.max_connections, - if conn_mgr.is_half_connection() { - "enabled" - } else { - "disabled" - } + "Connection pool: {}/{} active", + stats.healthy_connections, config.max_connections ), ); } @@ -983,7 +945,6 @@ async fn connect_and_run_inner( ); // Notify connected with session info - log_message(callbacks, 1, "[RUST] Notifying Android of connection..."); if let Some(cb) = callbacks.on_connected { cb(callbacks.context, &session); } @@ -999,31 +960,12 @@ async fn connect_and_run_inner( log_message( callbacks, 1, - &format!("[RUST] Connected! {ip_info}, Server: {actual_server_ip}"), + &format!("Connected! {ip_info}, Server: {actual_server_ip}"), ); - // Log RC4 encryption status - // Note: TLS encryption is ALWAYS active. use_encrypt only controls RC4 defense-in-depth. + // Log RC4 encryption status (TLS is ALWAYS active, this is defense-in-depth) if final_auth.rc4_key_pair.is_some() { - log_message( - callbacks, - 1, - "[RUST] RC4 defense-in-depth enabled (TLS + RC4)", - ); - } else if config.use_encrypt { - // Client requested RC4 but server didn't provide keys - log_message( - callbacks, - 1, - "[RUST] RC4 requested but not provided by server (TLS-only)", - ); - } else { - // RC4 explicitly disabled, TLS still provides encryption - log_message( - callbacks, - 1, - "[RUST] RC4 defense-in-depth disabled (TLS encryption active)", - ); + log_message(callbacks, 1, "RC4 encryption: enabled"); } // Initialize UDP acceleration if server supports it @@ -1034,7 +976,7 @@ async fn connect_and_run_inner( log_message( callbacks, 2, - &format!("[RUST] Failed to initialize UDP acceleration: {e}"), + &format!("Failed to initialize UDP acceleration: {e}"), ); None } else { @@ -1042,7 +984,7 @@ async fn connect_and_run_inner( callbacks, 1, &format!( - "[RUST] UDP acceleration initialized: version={}, server={}:{}", + "UDP acceleration initialized: version={}, server={}:{}", accel.version, udp_response.server_ip, udp_response.server_port ), ); @@ -1050,11 +992,7 @@ async fn connect_and_run_inner( } } Err(e) => { - log_message( - callbacks, - 2, - &format!("[RUST] Failed to create UDP socket: {e}"), - ); + log_message(callbacks, 2, &format!("Failed to create UDP socket: {e}")); None } } @@ -1080,7 +1018,7 @@ async fn connect_and_run_inner( domain_name: String::new(), } }); - log_message(callbacks, 1, "[RUST] Starting packet loop..."); + log_message(callbacks, 1, "Starting packet loop..."); run_packet_loop( &mut conn_mgr, running, @@ -1195,7 +1133,7 @@ async fn connect_redirect( log_msg( callbacks, 1, - &format!("[RUST] Connecting to cluster server {redirect_server}:{redirect_port}"), + &format!("Connecting to cluster server {redirect_server}:{redirect_port}"), ); // Create a modified config for the redirect server @@ -1221,7 +1159,7 @@ async fn connect_redirect( callbacks, 1, &format!( - "[RUST] Redirect server hello: v{} build {}", + "Redirect server hello: v{} build {}", hello.server_version, hello.server_build ), ); @@ -1256,7 +1194,7 @@ async fn connect_redirect( let host = format!("{redirect_server}:{redirect_port}"); let request_bytes = request.build(&host); - log_msg(callbacks, 1, "[RUST] Sending ticket authentication"); + log_msg(callbacks, 1, "Sending ticket authentication"); conn.write_all(&request_bytes).await?; // Read response @@ -1294,7 +1232,7 @@ async fn connect_redirect( callbacks, 1, &format!( - "[RUST] Redirect auth success, session key: {} bytes", + "Redirect auth success, session key: {} bytes", result.session_key.len() ), ); @@ -1312,19 +1250,11 @@ async fn connect_redirect( async fn perform_dhcp( conn_mgr: &mut ConnectionManager, mac: [u8; 6], - callbacks: &SoftEtherCallbacks, + _callbacks: &SoftEtherCallbacks, use_compress: bool, ) -> crate::error::Result { use tokio::time::timeout; - fn log_msg(callbacks: &SoftEtherCallbacks, level: i32, msg: &str) { - if let Some(cb) = callbacks.on_log { - if let Ok(cstr) = std::ffi::CString::new(msg) { - cb(callbacks.context, level, cstr.as_ptr()); - } - } - } - let mut dhcp = DhcpClient::new(mac); let mut codec = TunnelCodec::new(); let mut buf = vec![0u8; 65536]; @@ -1334,11 +1264,6 @@ async fn perform_dhcp( // Send DHCP DISCOVER let discover = dhcp.build_discover(); - log_msg( - callbacks, - 1, - &format!("[RUST] Sending DHCP DISCOVER ({} bytes)", discover.len()), - ); send_frame(conn_mgr, &discover, &mut send_buf, use_compress).await?; // Wait for OFFER/ACK @@ -1371,14 +1296,12 @@ async fn perform_dhcp( // Check if this is a DHCP response if is_dhcp_response(&packet_data) { - log_msg(callbacks, 1, "[RUST] DHCP response received"); if dhcp.process_response(&packet_data) { // Got ACK return Ok(dhcp.config().clone()); } else if dhcp.state() == DhcpState::DiscoverSent { // Got OFFER, send REQUEST if let Some(request) = dhcp.build_request() { - log_msg(callbacks, 1, "[RUST] Sending DHCP REQUEST"); send_frame(conn_mgr, &request, &mut send_buf, use_compress) .await?; } @@ -1391,17 +1314,15 @@ async fn perform_dhcp( Ok(Ok(_)) => { // Zero bytes - continue } - Ok(Err(e)) => { - log_msg(callbacks, 2, &format!("[RUST] Read error during DHCP: {e}")); + Ok(Err(_)) => { + // Read error - continue } Err(_) => { // Timeout, retry if dhcp.state() == DhcpState::DiscoverSent { - log_msg(callbacks, 2, "[RUST] DHCP timeout, retrying DISCOVER"); let discover = dhcp.build_discover(); send_frame(conn_mgr, &discover, &mut send_buf, use_compress).await?; } else if dhcp.state() == DhcpState::RequestSent { - log_msg(callbacks, 2, "[RUST] DHCP timeout, retrying REQUEST"); if let Some(request) = dhcp.build_request() { send_frame(conn_mgr, &request, &mut send_buf, use_compress).await?; } @@ -1419,19 +1340,11 @@ use crate::packet::{is_dhcp_response, is_dhcpv6_response}; async fn perform_dhcpv6( conn_mgr: &mut ConnectionManager, mac: [u8; 6], - callbacks: &SoftEtherCallbacks, + _callbacks: &SoftEtherCallbacks, use_compress: bool, ) -> Option { use tokio::time::timeout; - fn log_msg(callbacks: &SoftEtherCallbacks, level: i32, msg: &str) { - if let Some(cb) = callbacks.on_log { - if let Ok(cstr) = std::ffi::CString::new(msg) { - cb(callbacks.context, level, cstr.as_ptr()); - } - } - } - let mut dhcpv6 = Dhcpv6Client::new(mac); let mut codec = TunnelCodec::new(); let mut buf = vec![0u8; 65536]; @@ -1442,27 +1355,16 @@ async fn perform_dhcpv6( // Send DHCPv6 SOLICIT let solicit = dhcpv6.build_solicit(); - log_msg( - callbacks, - 1, - &format!("[RUST] Sending DHCPv6 SOLICIT ({} bytes)", solicit.len()), - ); if send_frame(conn_mgr, &solicit, &mut send_buf, use_compress) .await .is_err() { - log_msg(callbacks, 2, "[RUST] Failed to send DHCPv6 SOLICIT"); return None; } // Wait for ADVERTISE/REPLY loop { if std::time::Instant::now() > deadline { - log_msg( - callbacks, - 2, - "[RUST] DHCPv6 timeout - server may not support IPv6", - ); return None; } @@ -1488,23 +1390,12 @@ async fn perform_dhcpv6( // Check if this is a DHCPv6 response if is_dhcpv6_response(&packet_data) { - log_msg(callbacks, 1, "[RUST] DHCPv6 response received"); if dhcpv6.process_response(&packet_data) { // Got REPLY with address - let config = dhcpv6.config().clone(); - log_msg( - callbacks, - 1, - &format!( - "[RUST] DHCPv6 complete: IP={}, DNS={:?}", - config.ip, config.dns1 - ), - ); - return Some(config); + return Some(dhcpv6.config().clone()); } else if dhcpv6.state() == Dhcpv6State::SolicitSent { // Got ADVERTISE, send REQUEST if let Some(request) = dhcpv6.build_request() { - log_msg(callbacks, 1, "[RUST] Sending DHCPv6 REQUEST"); if send_frame( conn_mgr, &request, @@ -1514,11 +1405,6 @@ async fn perform_dhcpv6( .await .is_err() { - log_msg( - callbacks, - 2, - "[RUST] Failed to send DHCPv6 REQUEST", - ); return None; } } @@ -1539,7 +1425,6 @@ async fn perform_dhcpv6( Err(_) => { // Timeout, retry if dhcpv6.state() == Dhcpv6State::SolicitSent { - log_msg(callbacks, 2, "[RUST] DHCPv6 timeout, retrying SOLICIT"); let solicit = dhcpv6.build_solicit(); if send_frame(conn_mgr, &solicit, &mut send_buf, use_compress) .await @@ -1548,7 +1433,6 @@ async fn perform_dhcpv6( return None; } } else if dhcpv6.state() == Dhcpv6State::RequestSent { - log_msg(callbacks, 2, "[RUST] DHCPv6 timeout, retrying REQUEST"); if let Some(request) = dhcpv6.build_request() { if send_frame(conn_mgr, &request, &mut send_buf, use_compress) .await @@ -1627,6 +1511,7 @@ async fn run_packet_loop( let mut tunnel_codec = TunnelCodec::new(); let mut read_buf = vec![0u8; 65536]; let _udp_recv_buf = vec![0u8; 65536]; + let mut callback_buffer = Vec::with_capacity(65536); // Reusable buffer for callback let keepalive_interval_secs = 5u64; // Set up ARP handler for gateway MAC learning @@ -1634,33 +1519,12 @@ async fn run_packet_loop( let gateway = dhcp_config.gateway.unwrap_or(dhcp_config.ip); arp.configure(dhcp_config.ip, gateway); - log_msg( - &callbacks, - 1, - &format!( - "[RUST] ARP configured: my_ip={}, gateway_ip={}", - dhcp_config.ip, gateway - ), - ); + // Track last logged gateway MAC to avoid duplicate logs + let mut last_logged_gateway_mac: Option<[u8; 6]> = None; // Create RC4 encryption state if keys are provided let mut encryption = rc4_key_pair.map(TunnelEncryption::new); - // Log compression/encryption state - log_msg( - &callbacks, - 1, - &format!( - "[RUST] Compression: {}, Encryption: {}", - if use_compress { "enabled" } else { "disabled" }, - if encryption.is_some() { - "RC4" - } else { - "TLS-only" - } - ), - ); - // Send gratuitous ARP to announce our presence let garp = arp.build_gratuitous_arp(); let garp_bytes = garp.to_vec(); @@ -1681,7 +1545,6 @@ async fn run_packet_loop( .write_all(&garp_to_send) .await .map_err(crate::error::Error::Io)?; - log_msg(&callbacks, 1, "[RUST] Sent gratuitous ARP"); // Send ARP request for gateway MAC let gateway_arp = arp.build_gateway_request(); @@ -1703,9 +1566,6 @@ async fn run_packet_loop( .write_all(&gw_to_send) .await .map_err(crate::error::Error::Io)?; - log_msg(&callbacks, 1, "[RUST] Sent gateway ARP request"); - - log_msg(&callbacks, 1, "[RUST] Packet loop started"); // Track UDP acceleration state let mut udp_ready_logged = false; @@ -1714,19 +1574,9 @@ async fn run_packet_loop( // Start UDP acceleration if available - send initial keepalives if let Some(ref mut ua) = udp_accel { - log_msg( - &callbacks, - 1, - "[RUST] Sending initial UDP keepalives to establish path...", - ); // Send a few keepalives to trigger server response for _ in 0..3 { - if let Err(e) = ua.send_keepalive().await { - log_msg( - &callbacks, - 2, - &format!("[RUST] UDP initial keepalive failed: {e}"), - ); + if ua.send_keepalive().await.is_err() { break; } tokio::time::sleep(Duration::from_millis(100)).await; @@ -1747,16 +1597,8 @@ async fn run_packet_loop( .await .map_err(crate::error::Error::Io)?; let mut last_keepalive = std::time::Instant::now(); - let mut loop_count = 0u64; while running.load(Ordering::SeqCst) { - loop_count += 1; - - // Only log loop iteration periodically to avoid log spam - if loop_count == 1 { - log_msg(&callbacks, 1, "[RUST] Packet loop started"); - } - // Check if we need to send keepalive (TCP) if last_keepalive.elapsed() >= Duration::from_secs(keepalive_interval_secs) { // Encrypt keepalive if RC4 is enabled @@ -1769,7 +1611,7 @@ async fn run_packet_loop( keepalive.to_vec() }; if let Err(e) = conn_mgr.write_all(&to_send).await { - log_msg(&callbacks, 3, &format!("[RUST] Keepalive failed: {e}")); + log_msg(&callbacks, 3, &format!("Keepalive failed: {e}")); return Err(crate::error::Error::Io(e)); } last_keepalive = std::time::Instant::now(); @@ -1779,12 +1621,12 @@ async fn run_packet_loop( if let Some(ref mut ua) = udp_accel { if ua.is_send_ready() { if !udp_ready_logged { - log_msg(&callbacks, 1, "[RUST] UDP acceleration path is now active!"); + log_msg(&callbacks, 1, "UDP acceleration path is now active!"); udp_ready_logged = true; } if last_udp_keepalive.elapsed() >= udp_keepalive_interval { if let Err(e) = ua.send_keepalive().await { - log_msg(&callbacks, 2, &format!("[RUST] UDP keepalive failed: {e}")); + log_msg(&callbacks, 2, &format!("UDP keepalive failed: {e}")); } last_udp_keepalive = std::time::Instant::now(); } @@ -1832,7 +1674,7 @@ async fn run_packet_loop( // Send each frame via UDP (no tunnel framing needed) for frame in &modified_frames { if let Err(e) = ua.send(frame, false).await { - log_msg(&callbacks, 2, &format!("[RUST] UDP send failed: {e}")); + log_msg(&callbacks, 2, &format!("UDP send failed: {e}")); break; } } @@ -1865,7 +1707,7 @@ async fn run_packet_loop( // Write to TCP - don't use timeout, let TCP flow control handle backpressure if let Err(e) = conn_mgr.write_all(&to_send).await { - log_msg(&callbacks, 3, &format!("[RUST] TX error: {e}")); + log_msg(&callbacks, 3, &format!("TX error: {e}")); return Err(crate::error::Error::Io(e)); } } @@ -1889,38 +1731,36 @@ async fn run_packet_loop( // Process the received UDP packet through the accelerator if let Some(ref mut ua) = udp_accel { if let Some((frame_data, _compressed)) = ua.process_recv(&raw_data, src_addr) { - // Process received UDP frame - // Build length-prefixed buffer for callback - let mut buffer = Vec::with_capacity(frame_data.len() + 2); - - // Process ARP packets for gateway MAC learning - if frame_data.len() >= 14 { - let ethertype = u16::from_be_bytes([frame_data[12], frame_data[13]]); - if ethertype == 0x0806 { - let had_mac = arp.has_gateway_mac(); - arp.process_arp(&frame_data); - if !had_mac { - if let Some(gw_mac) = arp.gateway_mac() { - log_msg(&callbacks, 1, &format!( - "[RUST] Learned gateway MAC (UDP): {:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}", - gw_mac[0], gw_mac[1], gw_mac[2], gw_mac[3], gw_mac[4], gw_mac[5] - )); + // Process ARP packets for gateway MAC learning + if frame_data.len() >= 14 { + let ethertype = u16::from_be_bytes([frame_data[12], frame_data[13]]); + if ethertype == 0x0806 { + arp.process_arp(&frame_data); + if let Some(gw_mac) = arp.gateway_mac() { + if last_logged_gateway_mac != Some(*gw_mac) { + log_msg(&callbacks, 1, &format!( + "Learned gateway MAC: {:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}", + gw_mac[0], gw_mac[1], gw_mac[2], gw_mac[3], gw_mac[4], gw_mac[5] + )); + last_logged_gateway_mac = Some(*gw_mac); + } + } } } - } - } - // Update receive statistics - stats.packets_received.fetch_add(1, Ordering::Relaxed); - stats.bytes_received.fetch_add(frame_data.len() as u64, Ordering::Relaxed); + // Update receive statistics + stats.packets_received.fetch_add(1, Ordering::Relaxed); + stats.bytes_received.fetch_add(frame_data.len() as u64, Ordering::Relaxed); - let len = frame_data.len() as u16; - buffer.extend_from_slice(&len.to_be_bytes()); - buffer.extend_from_slice(&frame_data); + // Reuse callback_buffer for UDP frames too + callback_buffer.clear(); + let len = frame_data.len() as u16; + callback_buffer.extend_from_slice(&len.to_be_bytes()); + callback_buffer.extend_from_slice(&frame_data); - if let Some(cb) = callbacks.on_packets_received { - cb(callbacks.context, buffer.as_ptr(), buffer.len(), 1); - } + if let Some(cb) = callbacks.on_packets_received { + cb(callbacks.context, callback_buffer.as_ptr(), callback_buffer.len(), 1); + } } } } @@ -1938,66 +1778,82 @@ async fn run_packet_loop( match tunnel_codec.decode(&read_buf[..n]) { Ok(frames) => { if !frames.is_empty() { - // Build length-prefixed buffer for callback - let mut buffer = Vec::with_capacity(n + frames.len() * 2); + // Reuse callback_buffer across iterations to reduce allocations + callback_buffer.clear(); + // Pre-size: worst case is n bytes of data + 2 bytes len per frame + callback_buffer.reserve(n + frames.len() * 2); let mut total_bytes: u64 = 0; - // for (_frame_idx, frame) in frames.iter().enumerate() { + let mut packet_count: u64 = 0; + for frame in frames.iter() { - // Decompress if needed - let frame_data: Vec = if is_compressed(frame) { + // Fast path: check compression without allocation + let is_comp = is_compressed(frame); + + // Process frame data - avoid allocation for non-compressed + // Use Option to hold decompressed data only when needed + let decompressed: Option>; + let frame_slice: &[u8]; + + if is_comp { match decompress(frame) { - Ok(d) => d, - Err(_) => frame.to_vec(), + Ok(d) => { + decompressed = Some(d); + frame_slice = decompressed.as_ref().unwrap(); + } + Err(_) => continue, // Skip bad frames } } else { - frame.to_vec() - }; + decompressed = None; + let _ = &decompressed; // Silence unused warning, keeps borrow alive + frame_slice = frame; + } // Process ARP packets for gateway MAC learning - if frame_data.len() >= 14 { - let ethertype = u16::from_be_bytes([frame_data[12], frame_data[13]]); + if frame_slice.len() >= 14 { + let ethertype = u16::from_be_bytes([frame_slice[12], frame_slice[13]]); if ethertype == 0x0806 { - // This is an ARP packet - process it - let had_mac = arp.has_gateway_mac(); - arp.process_arp(&frame_data); - // Log if we just learned gateway MAC - if !had_mac { - if let Some(gw_mac) = arp.gateway_mac() { + arp.process_arp(frame_slice); + if let Some(gw_mac) = arp.gateway_mac() { + if last_logged_gateway_mac != Some(*gw_mac) { log_msg(&callbacks, 1, &format!( - "[RUST] Learned gateway MAC: {:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}", + "Learned gateway MAC: {:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}", gw_mac[0], gw_mac[1], gw_mac[2], gw_mac[3], gw_mac[4], gw_mac[5] )); + last_logged_gateway_mac = Some(*gw_mac); } } } } - total_bytes += frame_data.len() as u64; - let len = frame_data.len() as u16; - buffer.extend_from_slice(&len.to_be_bytes()); - buffer.extend_from_slice(&frame_data); + total_bytes += frame_slice.len() as u64; + packet_count += 1; + let len = frame_slice.len() as u16; + callback_buffer.extend_from_slice(&len.to_be_bytes()); + callback_buffer.extend_from_slice(frame_slice); } // Update receive statistics - stats.packets_received.fetch_add(frames.len() as u64, Ordering::Relaxed); + stats.packets_received.fetch_add(packet_count, Ordering::Relaxed); stats.bytes_received.fetch_add(total_bytes, Ordering::Relaxed); - if let Some(cb) = callbacks.on_packets_received { - cb(callbacks.context, buffer.as_ptr(), buffer.len(), frames.len() as u32); + if packet_count > 0 { + if let Some(cb) = callbacks.on_packets_received { + cb(callbacks.context, callback_buffer.as_ptr(), callback_buffer.len(), packet_count as u32); + } } } } Err(e) => { - log_msg(&callbacks, 3, &format!("[RUST] RX decode error: {e:?}")); + log_msg(&callbacks, 3, &format!("RX decode error: {e:?}")); } } } Ok(Ok(_)) => { - log_msg(&callbacks, 2, "[RUST] Connection closed by server"); + log_msg(&callbacks, 2, "Connection closed by server"); break; } Ok(Err(e)) => { - log_msg(&callbacks, 3, &format!("[RUST] Read error: {e}")); + log_msg(&callbacks, 3, &format!("Read error: {e}")); return Err(crate::error::Error::Io(e)); } Err(_) => { @@ -2008,7 +1864,7 @@ async fn run_packet_loop( } } - log_msg(&callbacks, 1, "[RUST] Packet loop ended"); + log_msg(&callbacks, 1, "Packet loop ended"); Ok(()) } @@ -2098,20 +1954,7 @@ async fn authenticate( log_msg( callbacks, 1, - "[RUST] >>> ENTERED authenticate() function <<<", - ); - log_msg( - callbacks, - 1, - &format!( - "[RUST] hello.use_secure_password = {}", - hello.use_secure_password - ), - ); - log_msg( - callbacks, - 1, - &format!("[RUST] Auth method: {:?}", config.auth.method), + &format!("Auth method: {:?}", config.auth.method), ); let options = ConnectionOptions { @@ -2127,28 +1970,7 @@ async fn authenticate( // Setup UDP acceleration if enabled let udp_accel = if config.udp_accel { - log_msg(callbacks, 1, "[RUST] Creating UDP acceleration socket..."); - match crate::net::UdpAccel::new(None, true, false) { - Ok(accel) => { - log_msg( - callbacks, - 1, - &format!( - "[RUST] UDP accel created: port={}, version={}", - accel.my_port, accel.version - ), - ); - Some(accel) - } - Err(e) => { - log_msg( - callbacks, - 2, - &format!("[RUST] Failed to create UDP accel: {e}, continuing without it"), - ); - None - } - } + crate::net::UdpAccel::new(None, true, false).ok() } else { None }; @@ -2159,14 +1981,11 @@ async fn authenticate( .map(crate::net::UdpAccelAuthParams::from_udp_accel); // Build auth pack based on auth method - log_msg(callbacks, 1, "[RUST] Building auth pack..."); let auth_pack = match config.auth.method { crate::config::AuthMethod::StandardPassword => { let auth_type = if hello.use_secure_password { - log_msg(callbacks, 1, "[RUST] Using SecurePassword auth type"); AuthType::SecurePassword } else { - log_msg(callbacks, 1, "[RUST] Using Password auth type"); AuthType::Password }; @@ -2177,18 +1996,12 @@ async fn authenticate( ) })?; - log_msg( - callbacks, - 1, - &format!("[RUST] Password hash length: {}", password_hash_str.len()), - ); - if password_hash_str.len() != 40 { log_msg( callbacks, 3, &format!( - "[RUST] Invalid hash format: len={}, expected 40 hex chars", + "Invalid hash format: len={}, expected 40 hex chars", password_hash_str.len() ), ); @@ -2198,17 +2011,14 @@ async fn authenticate( ))); } - log_msg(callbacks, 1, "[RUST] Decoding hex password hash"); let password_hash_bytes: [u8; 20] = hex::decode(password_hash_str) .map_err(|e| { - log_msg(callbacks, 3, &format!("[RUST] Hex decode error: {e}")); crate::error::Error::Config(format!("Invalid hex password hash: {e}")) })? .try_into() .map_err(|_| { crate::error::Error::Config("Hash decode produced wrong length".into()) })?; - log_msg(callbacks, 1, "[RUST] Hex hash decoded successfully"); AuthPack::new( &config.hub, @@ -2221,11 +2031,6 @@ async fn authenticate( ) } crate::config::AuthMethod::RadiusOrNtDomain => { - log_msg( - callbacks, - 1, - "[RUST] Using RADIUS/NT Domain auth (plaintext password)", - ); let password = config.auth.password.as_ref().ok_or_else(|| { crate::error::Error::Config("password required for RadiusOrNtDomain auth".into()) })?; @@ -2239,7 +2044,6 @@ async fn authenticate( ) } crate::config::AuthMethod::Certificate => { - log_msg(callbacks, 1, "[RUST] Using certificate auth"); let cert_pem = config.auth.certificate_pem.as_ref().ok_or_else(|| { crate::error::Error::Config("certificate_pem required for Certificate auth".into()) })?; @@ -2257,15 +2061,12 @@ async fn authenticate( udp_accel_params.as_ref(), )? } - crate::config::AuthMethod::Anonymous => { - log_msg(callbacks, 1, "[RUST] Using anonymous auth"); - AuthPack::new_anonymous( - &config.hub, - &config.auth.username, - &options, - udp_accel_params.as_ref(), - ) - } + crate::config::AuthMethod::Anonymous => AuthPack::new_anonymous( + &config.hub, + &config.auth.username, + &options, + udp_accel_params.as_ref(), + ), }; let request = HttpRequest::post(VPN_TARGET) @@ -2275,43 +2076,21 @@ async fn authenticate( let host = format!("{}:{}", config.server, config.port); let request_bytes = request.build(&host); - - log_msg( - callbacks, - 1, - &format!( - "[RUST] Sending auth request ({} bytes)...", - request_bytes.len() - ), - ); conn.write_all(&request_bytes).await?; let mut codec = HttpCodec::new(); let mut buf = vec![0u8; 8192]; - log_msg(callbacks, 1, "[RUST] Waiting for auth response..."); loop { let n = conn.read(&mut buf).await?; - log_msg(callbacks, 1, &format!("[RUST] Received {n} bytes")); if n == 0 { - log_msg(callbacks, 3, "[RUST] Connection closed during auth"); return Err(crate::error::Error::ConnectionFailed( "Connection closed during authentication".into(), )); } if let Some(response) = codec.feed(&buf[..n])? { - log_msg( - callbacks, - 1, - &format!("[RUST] HTTP response status: {}", response.status_code), - ); if response.status_code != 200 { - log_msg( - callbacks, - 3, - &format!("[RUST] Auth failed: HTTP {}", response.status_code), - ); return Err(crate::error::Error::AuthenticationFailed(format!( "Server returned status {}", response.status_code @@ -2319,11 +2098,7 @@ async fn authenticate( } if !response.body.is_empty() { - log_msg( - callbacks, - 1, - &format!("[RUST] Response body: {} bytes", response.body.len()), - ); + // Deserialize Pack let pack = crate::protocol::Pack::deserialize(&response.body)?; // Resolve remote IP for UDP accel parsing @@ -2335,14 +2110,28 @@ async fn authenticate( None }; - let result = AuthResult::from_pack_with_remote(&pack, remote_ip)?; + // Parse auth result with error logging + let result = match AuthResult::from_pack_with_remote(&pack, remote_ip) { + Ok(r) => { + // log_msg( + // callbacks, + // 1, + // &format!( + // "AuthResult parsed: error={}, session_key_len={}", + // r.error, + // r.session_key.len() + // ), + // ); + r + } + Err(e) => { + log_msg(callbacks, 3, &format!("AuthResult parse error: {e}")); + return Err(e); + } + }; if result.error > 0 { - log_msg( - callbacks, - 3, - &format!("[RUST] Auth error code: {}", result.error), - ); + log_msg(callbacks, 3, &format!("Auth error code: {}", result.error)); if result.error == 20 { return Err(crate::error::Error::UserAlreadyLoggedIn); } @@ -2359,16 +2148,14 @@ async fn authenticate( callbacks, 1, &format!( - "[RUST] Server supports UDP accel: version={}, port={}, encryption={}", - udp_response.version, udp_response.server_port, udp_response.use_encryption + "Server supports UDP accel: version={}, port={}, encryption={}", + udp_response.version, + udp_response.server_port, + udp_response.use_encryption ), ); } else { - log_msg( - callbacks, - 2, - "[RUST] Server does not support UDP acceleration", - ); + log_msg(callbacks, 2, "Server does not support UDP acceleration"); } } @@ -2376,13 +2163,13 @@ async fn authenticate( callbacks, 1, &format!( - "[RUST] Auth success! Session key: {} bytes", + "Auth success! Session key: {} bytes", result.session_key.len() ), ); return Ok(result); } else { - log_msg(callbacks, 3, "[RUST] Empty auth response body"); + log_msg(callbacks, 3, "Empty auth response body"); return Err(crate::error::Error::ServerError( "Empty authentication response".into(), )); diff --git a/src/ffi/ios.rs b/src/ffi/ios.rs index f16b8f2..b9320c2 100644 --- a/src/ffi/ios.rs +++ b/src/ffi/ios.rs @@ -135,63 +135,49 @@ pub extern "C" fn softether_ios_is_valid_ipv4(ip: u32) -> c_int { } } -/// Simplified session getter that returns a pointer to session data. -/// This avoids Swift having to construct the entire SoftEtherSession struct. -/// Returns NULL if not connected or handle is invalid. +/// Get session data into a caller-provided buffer. +/// Returns SoftEtherResult indicating success or failure. +/// +/// This is the recommended API - caller manages the memory, avoiding +/// thread-local storage issues and stale pointer problems. /// /// # Safety -/// The `handle` must be a valid handle returned from `softether_create`. +/// - The `handle` must be a valid handle returned from `softether_create`. +/// - The `session_out` pointer must be valid and point to a writable `SoftEtherSession`. #[no_mangle] pub unsafe extern "C" fn softether_ios_get_session( handle: SoftEtherHandle, -) -> *const SoftEtherSession { + session_out: *mut SoftEtherSession, +) -> SoftEtherResult { if handle.is_null() { - return std::ptr::null(); + return SoftEtherResult::InvalidParam; } - - // We need a place to store the session - use thread-local storage - thread_local! { - static SESSION_STORAGE: std::cell::RefCell = - std::cell::RefCell::new(SoftEtherSession::default()); + if session_out.is_null() { + return SoftEtherResult::InvalidParam; } - SESSION_STORAGE.with(|storage| { - let mut session = storage.borrow_mut(); - let result = unsafe { softether_get_session(handle, &mut *session as *mut _) }; - - if result == SoftEtherResult::Ok { - &*session as *const SoftEtherSession - } else { - std::ptr::null() - } - }) + unsafe { softether_get_session(handle, session_out) } } -/// Simplified statistics getter. +/// Get statistics into a caller-provided buffer. +/// Returns SoftEtherResult indicating success or failure. /// /// # Safety -/// The `handle` must be a valid handle returned from `softether_create`. +/// - The `handle` must be a valid handle returned from `softether_create`. +/// - The `stats_out` pointer must be valid and point to a writable `SoftEtherStats`. #[no_mangle] -pub unsafe extern "C" fn softether_ios_get_stats(handle: SoftEtherHandle) -> *const SoftEtherStats { +pub unsafe extern "C" fn softether_ios_get_stats( + handle: SoftEtherHandle, + stats_out: *mut SoftEtherStats, +) -> SoftEtherResult { if handle.is_null() { - return std::ptr::null(); + return SoftEtherResult::InvalidParam; } - - thread_local! { - static STATS_STORAGE: std::cell::RefCell = - std::cell::RefCell::new(SoftEtherStats::default()); + if stats_out.is_null() { + return SoftEtherResult::InvalidParam; } - STATS_STORAGE.with(|storage| { - let mut stats = storage.borrow_mut(); - let result = unsafe { softether_get_stats(handle, &mut *stats as *mut _) }; - - if result == SoftEtherResult::Ok { - &*stats as *const SoftEtherStats - } else { - std::ptr::null() - } - }) + unsafe { softether_get_stats(handle, stats_out) } } /// Format bytes as human-readable string (KB, MB, GB). diff --git a/src/protocol/auth.rs b/src/protocol/auth.rs index 96637a5..664a789 100644 --- a/src/protocol/auth.rs +++ b/src/protocol/auth.rs @@ -396,108 +396,8 @@ impl AuthPack { let secure_password = crypto::compute_secure_password(password_hash, server_random); pack.add_data("secure_password", secure_password.to_vec()); - // Client version info - pack.add_str("client_str", CLIENT_STRING); - pack.add_int("client_ver", CLIENT_VERSION); - pack.add_int("client_build", CLIENT_BUILD); - - // Protocol (0 = TCP) - pack.add_int("protocol", 0); - - // Version fields - pack.add_str("hello", CLIENT_STRING); - pack.add_int("version", CLIENT_VERSION); - pack.add_int("build", CLIENT_BUILD); - pack.add_int("client_id", 0); - - // Connection options (wired from config) - pack.add_int("max_connection", options.max_connections as u32); - pack.add_bool("use_encrypt", options.use_encrypt); - pack.add_bool("use_compress", options.use_compress); - // half_connection: use config value (requires max_connections >= 2 if true) - pack.add_bool("half_connection", options.half_connection); - pack.add_bool("require_bridge_routing_mode", options.bridge_mode); - pack.add_bool("require_monitor_mode", options.monitor_mode); - pack.add_bool("qos", options.qos); - - // UDP acceleration R-UDP bulk support - pack.add_bool("support_bulk_on_rudp", options.udp_accel); - pack.add_bool("support_hmac_on_bulk_of_rudp", options.udp_accel); - pack.add_bool("support_udp_recovery", options.udp_accel); - - // Unique ID - let unique_id: [u8; 20] = crypto::random_bytes(); - pack.add_data("unique_id", unique_id.to_vec()); - pack.add_int( - "rudp_bulk_max_version", - if options.udp_accel { 2 } else { 0 }, - ); - - // UDP acceleration using flag - send client UDP params if available - // This follows the C code in Protocol.c ClientUploadAuth() - if let Some(params) = udp_accel_params { - pack.add_bool("use_udp_acceleration", true); - pack.add_int("udp_acceleration_version", params.max_version); - - // Add client IP (using PackAddIp format) - Self::add_ip(&mut pack, "udp_acceleration_client_ip", params.client_ip); - pack.add_int("udp_acceleration_client_port", params.client_port as u32); - - // Add client keys - pack.add_data("udp_acceleration_client_key", params.client_key.to_vec()); - pack.add_data( - "udp_acceleration_client_key_v2", - params.client_key_v2.to_vec(), - ); - - // HMAC and fast disconnect support - pack.add_bool("support_hmac_on_udp_acceleration", true); - pack.add_bool("support_udp_accel_fast_disconnect_detect", true); - pack.add_int("udp_acceleration_max_version", params.max_version); - } - - // Node info (OutRpcNodeInfo in C) - let cedar_unique_id: [u8; 16] = crypto::random_bytes(); // Cedar unique ID is 16 bytes - let hostname = Self::get_hostname(); - pack.add_str("ClientProductName", CLIENT_STRING); - pack.add_str("ServerProductName", ""); - pack.add_str("ClientOsName", std::env::consts::OS); - pack.add_str("ClientOsVer", ""); - pack.add_str("ClientOsProductId", ""); - pack.add_str("ClientHostname", &hostname); - pack.add_str("ServerHostname", ""); - pack.add_str("ProxyHostname", ""); - pack.add_str("HubName", hub); - pack.add_data("UniqueId", cedar_unique_id.to_vec()); - // Note: C uses LittleEndian32 for these fields - pack.add_int("ClientProductVer", CLIENT_VERSION.to_le()); - pack.add_int("ClientProductBuild", CLIENT_BUILD.to_le()); - pack.add_int("ServerProductVer", 0); - pack.add_int("ServerProductBuild", 0); - - // IP addresses (using PackAddIp32 format) - Self::add_ip32(&mut pack, "ClientIpAddress", 0); - pack.add_data("ClientIpAddress6", vec![0u8; 16]); - pack.add_int("ClientPort", 0); - Self::add_ip32(&mut pack, "ServerIpAddress", 0); - pack.add_data("ServerIpAddress6", vec![0u8; 16]); - pack.add_int("ServerPort2", 0); - Self::add_ip32(&mut pack, "ProxyIpAddress", 0); - pack.add_data("ProxyIpAddress6", vec![0u8; 16]); - pack.add_int("ProxyPort", 0); - - // WinVer fields (OutRpcWinVer in C) - pack.add_bool("V_IsWindows", false); - pack.add_bool("V_IsNT", false); - pack.add_bool("V_IsServer", false); - pack.add_bool("V_IsBeta", false); - pack.add_int("V_VerMajor", 0); - pack.add_int("V_VerMinor", 0); - pack.add_int("V_Build", 0); - pack.add_int("V_ServicePack", 0); - pack.add_str("V_Title", std::env::consts::OS); - - // Note: 'pencore' is NOT added by client - it's a server-side feature + // Add common client fields + Self::add_client_fields(&mut pack, hub, options, udp_accel_params); Self { pack } } @@ -739,102 +639,8 @@ impl AuthPack { pack.add_int("authtype", AuthType::Ticket as u32); pack.add_data("ticket", ticket.to_vec()); - // Client version info - pack.add_str("client_str", CLIENT_STRING); - pack.add_int("client_ver", CLIENT_VERSION); - pack.add_int("client_build", CLIENT_BUILD); - - // Protocol - pack.add_int("protocol", 0); - - // Version fields - pack.add_str("hello", CLIENT_STRING); - pack.add_int("version", CLIENT_VERSION); - pack.add_int("build", CLIENT_BUILD); - pack.add_int("client_id", 0); - - // Connection options (same as password auth, from config) - pack.add_int("max_connection", options.max_connections as u32); - pack.add_bool("use_encrypt", options.use_encrypt); - pack.add_bool("use_compress", options.use_compress); - // half_connection: use config value (requires max_connections >= 2 if true) - pack.add_bool("half_connection", options.half_connection); - pack.add_bool("require_bridge_routing_mode", options.bridge_mode); - pack.add_bool("require_monitor_mode", options.monitor_mode); - pack.add_bool("qos", options.qos); - - pack.add_bool("support_bulk_on_rudp", options.udp_accel); - pack.add_bool("support_hmac_on_bulk_of_rudp", options.udp_accel); - pack.add_bool("support_udp_recovery", options.udp_accel); - - let unique_id: [u8; 20] = crypto::random_bytes(); - pack.add_data("unique_id", unique_id.to_vec()); - pack.add_int( - "rudp_bulk_max_version", - if options.udp_accel { 2 } else { 0 }, - ); - - // UDP acceleration using flag - send client UDP params if available - if let Some(params) = udp_accel_params { - pack.add_bool("use_udp_acceleration", true); - pack.add_int("udp_acceleration_version", params.max_version); - - Self::add_ip(&mut pack, "udp_acceleration_client_ip", params.client_ip); - pack.add_int("udp_acceleration_client_port", params.client_port as u32); - - pack.add_data("udp_acceleration_client_key", params.client_key.to_vec()); - pack.add_data( - "udp_acceleration_client_key_v2", - params.client_key_v2.to_vec(), - ); - - pack.add_bool("support_hmac_on_udp_acceleration", true); - pack.add_bool("support_udp_accel_fast_disconnect_detect", true); - pack.add_int("udp_acceleration_max_version", params.max_version); - } - - // Node info (OutRpcNodeInfo in C) - let cedar_unique_id: [u8; 16] = crypto::random_bytes(); // Cedar unique ID is 16 bytes - let hostname = Self::get_hostname(); - pack.add_str("ClientProductName", CLIENT_STRING); - pack.add_str("ServerProductName", ""); - pack.add_str("ClientOsName", std::env::consts::OS); - pack.add_str("ClientOsVer", ""); - pack.add_str("ClientOsProductId", ""); - pack.add_str("ClientHostname", &hostname); - pack.add_str("ServerHostname", ""); - pack.add_str("ProxyHostname", ""); - pack.add_str("HubName", hub); - pack.add_data("UniqueId", cedar_unique_id.to_vec()); - // Note: C uses LittleEndian32 for these fields - pack.add_int("ClientProductVer", CLIENT_VERSION.to_le()); - pack.add_int("ClientProductBuild", CLIENT_BUILD.to_le()); - pack.add_int("ServerProductVer", 0); - pack.add_int("ServerProductBuild", 0); - - // IP addresses - Self::add_ip32(&mut pack, "ClientIpAddress", 0); - pack.add_data("ClientIpAddress6", vec![0u8; 16]); - pack.add_int("ClientPort", 0); - Self::add_ip32(&mut pack, "ServerIpAddress", 0); - pack.add_data("ServerIpAddress6", vec![0u8; 16]); - pack.add_int("ServerPort2", 0); - Self::add_ip32(&mut pack, "ProxyIpAddress", 0); - pack.add_data("ProxyIpAddress6", vec![0u8; 16]); - pack.add_int("ProxyPort", 0); - - // WinVer fields (OutRpcWinVer in C) - pack.add_bool("V_IsWindows", false); - pack.add_bool("V_IsNT", false); - pack.add_bool("V_IsServer", false); - pack.add_bool("V_IsBeta", false); - pack.add_int("V_VerMajor", 0); - pack.add_int("V_VerMinor", 0); - pack.add_int("V_Build", 0); - pack.add_int("V_ServicePack", 0); - pack.add_str("V_Title", std::env::consts::OS); - - // Note: 'pencore' is NOT added by client - it's a server-side feature + // Add common client fields + Self::add_client_fields(&mut pack, hub, options, udp_accel_params); Self { pack } } diff --git a/src/protocol/tunnel.rs b/src/protocol/tunnel.rs index 2c40ea3..f9de1d1 100644 --- a/src/protocol/tunnel.rs +++ b/src/protocol/tunnel.rs @@ -325,8 +325,8 @@ impl TunnelCodec { // Consume size self.buffer.advance(4); - // Read block data - let block_data = self.buffer.copy_to_bytes(block_size); + // Read block data - split_to + freeze is zero-copy when possible + let block_data = self.buffer.split_to(block_size).freeze(); self.packets.push(block_data); self.remaining_blocks -= 1; diff --git a/src/tunnel/dhcp_handler.rs b/src/tunnel/dhcp_handler.rs new file mode 100644 index 0000000..985bcf4 --- /dev/null +++ b/src/tunnel/dhcp_handler.rs @@ -0,0 +1,384 @@ +//! DHCP handling for tunnel runner. +//! +//! This module contains DHCP discovery logic for both single-connection +//! and multi-connection modes. + +use std::time::{Duration, Instant}; + +use tokio::time::timeout; +use tracing::{debug, warn}; + +use crate::client::{ConnectionManager, VpnConnection}; +use crate::error::{Error, Result}; +use crate::packet::{DhcpClient, DhcpConfig, DhcpState}; +use crate::protocol::{compress, decompress, is_compressed, TunnelCodec}; + +use super::TunnelRunner; + +impl TunnelRunner { + /// Perform DHCP through the tunnel (single connection). + pub(super) async fn perform_dhcp(&self, conn: &mut VpnConnection) -> Result { + let mut dhcp = DhcpClient::new(self.mac); + let mut codec = TunnelCodec::new(); + let mut buf = vec![0u8; 65536]; + let mut send_buf = vec![0u8; 2048]; + + let deadline = Instant::now() + Duration::from_secs(self.config.dhcp_timeout); + + // Send DHCP DISCOVER + let discover = dhcp.build_discover(); + debug!(bytes = discover.len(), "Sending DHCP DISCOVER"); + self.send_frame(conn, &discover, &mut send_buf).await?; + + // Wait for OFFER + loop { + if Instant::now() > deadline { + return Err(Error::TimeoutMessage( + "DHCP timeout - no OFFER received".into(), + )); + } + + match timeout(Duration::from_secs(3), conn.read(&mut buf)).await { + Ok(Ok(n)) if n > 0 => { + debug!("Received {} bytes from tunnel", n); + // Decode tunnel frames + let frames = codec.feed(&buf[..n])?; + for frame in frames { + if frame.is_keepalive() { + debug!("Received keepalive frame"); + continue; + } + if let Some(packets) = frame.packets() { + for packet in packets { + // Check if packet is compressed and decompress if needed + let packet_data: Vec = if is_compressed(packet) { + match decompress(packet) { + Ok(decompressed) => { + debug!( + "Decompressed {} -> {} bytes", + packet.len(), + decompressed.len() + ); + decompressed + } + Err(e) => { + warn!("Decompression failed: {}", e); + continue; + } + } + } else { + packet.to_vec() + }; + + // Log packet details + if packet_data.len() >= 14 { + let ethertype = + format!("0x{:02X}{:02X}", packet_data[12], packet_data[13]); + debug!( + "Packet: {} bytes, ethertype={}", + packet_data.len(), + ethertype + ); + } + + // Check if this is a DHCP response (UDP port 68) + if self.is_dhcp_response(&packet_data) { + debug!("DHCP response received"); + if dhcp.process_response(&packet_data) { + // Got ACK + return Ok(dhcp.config().clone()); + } else if dhcp.state() == DhcpState::DiscoverSent { + // Got OFFER, send REQUEST + if let Some(request) = dhcp.build_request() { + debug!("Sending DHCP REQUEST"); + self.send_frame(conn, &request, &mut send_buf).await?; + } + } + } + } + } + } + } + Ok(Ok(_)) => { + return Err(Error::ConnectionFailed( + "Connection closed during DHCP".into(), + )); + } + Ok(Err(e)) => { + return Err(Error::Io(e)); + } + Err(_) => { + // Timeout, retry DISCOVER if still in initial state + if dhcp.state() == DhcpState::DiscoverSent { + warn!("DHCP timeout, retrying DISCOVER"); + let discover = dhcp.build_discover(); + self.send_frame(conn, &discover, &mut send_buf).await?; + } else if dhcp.state() == DhcpState::RequestSent { + warn!("DHCP timeout, retrying REQUEST"); + if let Some(request) = dhcp.build_request() { + self.send_frame(conn, &request, &mut send_buf).await?; + } + } + } + } + } + } + + /// Check if an Ethernet frame is a DHCP response (UDP dst port 68). + pub(super) fn is_dhcp_response(&self, frame: &[u8]) -> bool { + // Ethernet(14) + IP header(20 min) + UDP header(8) + if frame.len() < 42 { + return false; + } + + // Check EtherType is IPv4 + if frame[12] != 0x08 || frame[13] != 0x00 { + return false; + } + + // Check IP protocol is UDP (17) + if frame[23] != 17 { + return false; + } + + // Check UDP destination port is 68 (DHCP client) + let dst_port = u16::from_be_bytes([frame[36], frame[37]]); + dst_port == 68 + } + + /// Send an Ethernet frame through the tunnel. + pub(super) async fn send_frame( + &self, + conn: &mut VpnConnection, + frame: &[u8], + buf: &mut [u8], + ) -> Result<()> { + // Compress if enabled + let data_to_send: std::borrow::Cow<[u8]> = if self.config.use_compress { + match compress(frame) { + Ok(compressed) => { + debug!("Compressed {} -> {} bytes", frame.len(), compressed.len()); + std::borrow::Cow::Owned(compressed) + } + Err(e) => { + warn!("Compression failed, sending uncompressed: {}", e); + std::borrow::Cow::Borrowed(frame) + } + } + } else { + std::borrow::Cow::Borrowed(frame) + }; + + // Encode as tunnel packet: [num_blocks=1][size][data] + let total_len = 4 + 4 + data_to_send.len(); + if buf.len() < total_len { + return Err(Error::Protocol("Send buffer too small".into())); + } + + buf[0..4].copy_from_slice(&1u32.to_be_bytes()); + buf[4..8].copy_from_slice(&(data_to_send.len() as u32).to_be_bytes()); + buf[8..8 + data_to_send.len()].copy_from_slice(&data_to_send); + + conn.write_all(&buf[..total_len]).await?; + Ok(()) + } + + /// Perform DHCP through the tunnel using ConnectionManager (multi-connection). + pub(super) async fn perform_dhcp_multi( + &self, + conn_mgr: &mut ConnectionManager, + ) -> Result { + let mut dhcp = DhcpClient::new(self.mac); + // One codec per receive connection for stateful parsing + let num_conns = conn_mgr.connection_count(); + let mut codecs: Vec = (0..num_conns).map(|_| TunnelCodec::new()).collect(); + let mut buf = vec![0u8; 65536]; + let mut send_buf = vec![0u8; 2048]; + + let deadline = Instant::now() + Duration::from_secs(self.config.dhcp_timeout); + + // Get all receive-capable connection indices + let recv_conn_indices: Vec = conn_mgr + .all_connections() + .iter() + .enumerate() + .filter(|(_, c)| c.direction.can_recv()) + .map(|(i, _)| i) + .collect(); + + debug!( + connections = recv_conn_indices.len(), + "DHCP using receive connections" + ); + + // Note: DHCP happens before authentication and RC4 key exchange, + // so no encryption is used for DHCP packets. The ConnectionManager + // will have None for rc4_key_pair at this point. + + // Send DHCP DISCOVER + let discover = dhcp.build_discover(); + debug!(bytes = discover.len(), "Sending DHCP DISCOVER"); + self.send_frame_multi(conn_mgr, &discover, &mut send_buf) + .await?; + + let mut last_send = Instant::now(); + let mut poll_idx = 0; + + // Use longer timeout per read - we want to actually wait for data + // With 1 connection, we can afford to wait longer + let per_conn_timeout_ms = if recv_conn_indices.len() <= 1 { + 100 + } else { + std::cmp::max(10, 100 / recv_conn_indices.len() as u64) + }; + + // Wait for OFFER/ACK + loop { + if Instant::now() > deadline { + return Err(Error::TimeoutMessage( + "DHCP timeout - no response received".into(), + )); + } + + // Retry DHCP if no response for 1 second (server may be slow) + if last_send.elapsed() > Duration::from_millis(1000) { + if dhcp.state() == DhcpState::DiscoverSent { + warn!("DHCP timeout, retrying DISCOVER"); + let discover = dhcp.build_discover(); + self.send_frame_multi(conn_mgr, &discover, &mut send_buf) + .await?; + } else if dhcp.state() == DhcpState::RequestSent { + warn!("DHCP timeout, retrying REQUEST"); + if let Some(request) = dhcp.build_request() { + self.send_frame_multi(conn_mgr, &request, &mut send_buf) + .await?; + } + } + last_send = Instant::now(); + } + + if recv_conn_indices.is_empty() { + tokio::time::sleep(Duration::from_millis(10)).await; + continue; + } + + // Poll each connection with very short timeout + for _ in 0..recv_conn_indices.len() { + let conn_idx = recv_conn_indices[poll_idx % recv_conn_indices.len()]; + poll_idx += 1; + + let recv_conn = match conn_mgr.get_mut(conn_idx) { + Some(c) => c, + None => continue, + }; + + match timeout( + Duration::from_millis(per_conn_timeout_ms), + recv_conn.conn.read(&mut buf), + ) + .await + { + Ok(Ok(n)) if n > 0 => { + recv_conn.touch(); + debug!(conn = conn_idx, bytes = n, "Received data on connection"); + + // Use per-connection codec for proper frame parsing + let codec = &mut codecs[conn_idx]; + let frames = match codec.feed(&buf[..n]) { + Ok(f) => f, + Err(e) => { + warn!("Codec error on conn {}: {}", conn_idx, e); + continue; + } + }; + + for frame in frames { + if frame.is_keepalive() { + debug!("Received keepalive frame"); + continue; + } + if let Some(packets) = frame.packets() { + for packet in packets { + let packet_data: Vec = if is_compressed(packet) { + match decompress(packet) { + Ok(d) => d, + Err(_) => continue, + } + } else { + packet.to_vec() + }; + + if self.is_dhcp_response(&packet_data) { + debug!("DHCP response received on conn {}", conn_idx); + if dhcp.process_response(&packet_data) { + return Ok(dhcp.config().clone()); + } else if dhcp.state() == DhcpState::DiscoverSent { + if let Some(request) = dhcp.build_request() { + debug!("Sending DHCP REQUEST"); + self.send_frame_multi( + conn_mgr, + &request, + &mut send_buf, + ) + .await?; + last_send = Instant::now(); + } + } + } + } + } + } + } + Ok(Ok(_)) => { + // Zero bytes = connection closed + warn!(conn = conn_idx, "Connection closed during DHCP"); + } + Ok(Err(e)) => { + warn!(conn = conn_idx, error = %e, "Read error during DHCP"); + } + Err(_) => { + // Timeout - normal, continue to next connection + } + } + } + } + } + + /// Send an Ethernet frame using ConnectionManager (multi-connection). + pub(super) async fn send_frame_multi( + &self, + conn_mgr: &mut ConnectionManager, + frame: &[u8], + buf: &mut [u8], + ) -> Result<()> { + // Compress if enabled + let data_to_send: std::borrow::Cow<[u8]> = if self.config.use_compress { + match compress(frame) { + Ok(compressed) => { + debug!("Compressed {} -> {} bytes", frame.len(), compressed.len()); + std::borrow::Cow::Owned(compressed) + } + Err(e) => { + warn!("Compression failed, sending uncompressed: {}", e); + std::borrow::Cow::Borrowed(frame) + } + } + } else { + std::borrow::Cow::Borrowed(frame) + }; + + // Encode as tunnel packet + let total_len = 4 + 4 + data_to_send.len(); + if buf.len() < total_len { + return Err(Error::Protocol("Send buffer too small".into())); + } + + buf[0..4].copy_from_slice(&1u32.to_be_bytes()); + buf[4..8].copy_from_slice(&(data_to_send.len() as u32).to_be_bytes()); + buf[8..8 + data_to_send.len()].copy_from_slice(&data_to_send); + + // Write through connection manager (handles send connection selection) + conn_mgr.write_all(&buf[..total_len]).await?; + Ok(()) + } +} diff --git a/src/tunnel/mod.rs b/src/tunnel/mod.rs index 7b95492..9e784bd 100644 --- a/src/tunnel/mod.rs +++ b/src/tunnel/mod.rs @@ -3,9 +3,41 @@ //! This module contains: //! - Data loop state machine for tunnel operations //! - Tunnel runner for the main data loop +//! - Shared packet processing logic +//! - Single-connection data loop +//! - Multi-connection data loop (half-connection mode) +//! - DHCP handling for tunnel setup mod data_loop; +#[cfg(any( + target_os = "macos", + target_os = "linux", + target_os = "windows", + target_os = "ios", + target_os = "android" +))] +mod dhcp_handler; +#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))] +mod multi_conn; +#[cfg(any( + target_os = "macos", + target_os = "linux", + target_os = "windows", + target_os = "ios", + target_os = "android" +))] +mod packet_processor; mod runner; +#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))] +mod single_conn; pub use data_loop::{format_ip, DataLoopConfig, DataLoopState, Ipv4Info, LoopResult, TimingState}; +#[cfg(any( + target_os = "macos", + target_os = "linux", + target_os = "windows", + target_os = "ios", + target_os = "android" +))] +pub use packet_processor::*; pub use runner::{RouteConfig, TunnelConfig, TunnelRunner}; diff --git a/src/tunnel/multi_conn.rs b/src/tunnel/multi_conn.rs new file mode 100644 index 0000000..3a0319d --- /dev/null +++ b/src/tunnel/multi_conn.rs @@ -0,0 +1,661 @@ +//! Multi-connection data loop for tunnel runner. +//! +//! This module contains the packet forwarding loop for half-connection mode +//! with multiple TCP connections (receive-only + bidirectional). + +use std::sync::atomic::Ordering; +use std::time::{Duration, Instant}; + +use tokio::sync::mpsc; +use tokio::time::interval; +use tracing::{debug, error, info, warn}; + +use crate::adapter::TunAdapter; +#[cfg(target_os = "windows")] +use crate::adapter::WintunDevice; +use crate::client::{ConcurrentReader, ConnectionManager}; +use crate::error::Result; +use crate::packet::{ArpHandler, DhcpConfig}; +use crate::protocol::{compress_into, decompress_into, is_compressed, TunnelCodec}; + +use super::DataLoopState; +use super::TunnelRunner; + +impl TunnelRunner { + /// Run the multi-connection data loop (half-connection mode). + /// + /// This mode uses multiple TCP connections: + /// - Receive-only connections: handled by ConcurrentReader + /// - Bidirectional connections: used for both send and receive + /// + /// Each connection has its own encryption state for per-connection RC4. + #[cfg(any(target_os = "macos", target_os = "linux"))] + pub(super) async fn run_data_loop_multi( + &self, + conn_mgr: &mut ConnectionManager, + tun: &mut impl TunAdapter, + dhcp_config: &DhcpConfig, + ) -> Result<()> { + let mut state = DataLoopState::new(self.mac); + let gateway = dhcp_config.gateway.unwrap_or(dhcp_config.ip); + state.configure(dhcp_config.ip, gateway); + + // Get the total number of connections before extraction + let total_conns = conn_mgr.connection_count(); + + // Extract receive-only connections for concurrent reading. + // Bidirectional connections stay in conn_mgr for both send AND receive. + // Each connection carries its own encryption state for per-connection RC4. + let recv_conns = conn_mgr.take_recv_connections(); + let num_recv = recv_conns.len(); + let num_bidir = conn_mgr.connection_count(); // Bidirectional connections remaining + + // Create concurrent reader for receive-only connections (may be empty!) + // The concurrent reader handles per-connection decryption internally. + let mut concurrent_reader = if !recv_conns.is_empty() { + Some(ConcurrentReader::new(recv_conns, 256)) + } else { + None + }; + + // One codec per original connection index for stateful frame parsing + let mut codecs: Vec = (0..total_conns).map(|_| TunnelCodec::new()).collect(); + + // Per-connection encryption is now handled by ManagedConnection. + // No shared encryption variable - each connection has its own cipher state. + let has_encryption = self.config.rc4_key_pair.is_some(); + if has_encryption { + info!("RC4 defense-in-depth encryption active (per-connection cipher state)"); + } else { + debug!("No RC4 encryption (TLS-only mode for multi-connection tunnel)"); + } + + // Buffer for reading from bidirectional connections + let mut bidir_read_buf = vec![0u8; 8192]; + + let mut send_buf = vec![0u8; 4096]; + let mut comp_buf = vec![0u8; 4096]; // Pre-allocated buffer for compression + let mut decomp_buf = vec![0u8; 4096]; + let mut tun_write_buf = vec![0u8; 2048]; + + // Set up ARP handler + let mut arp = ArpHandler::new(self.mac); + arp.configure(dhcp_config.ip, gateway); + + // Send gratuitous ARP to announce our presence + let garp = arp.build_gratuitous_arp(); + self.send_frame_multi(conn_mgr, &garp, &mut send_buf) + .await?; + debug!("Sent gratuitous ARP"); + + // Send ARP request for gateway + let gateway_arp = arp.build_gateway_request(); + self.send_frame_multi(conn_mgr, &gateway_arp, &mut send_buf) + .await?; + debug!("Sent gateway ARP request"); + + let mut keepalive_interval = interval(Duration::from_secs(self.config.keepalive_interval)); + let mut last_activity = Instant::now(); + + // Zero-copy TUN reader using fixed buffer + let (tun_tx, mut tun_rx) = mpsc::channel::<(usize, [u8; 2048])>(256); + let tun_fd = tun.raw_fd(); + let running = self.running.clone(); + + // Spawn blocking TUN reader task + let tun_reader = tokio::task::spawn_blocking(move || { + let mut read_buf = [0u8; 2048]; + + while running.load(Ordering::SeqCst) { + let mut poll_fds = [libc::pollfd { + fd: tun_fd, + events: libc::POLLIN, + revents: 0, + }]; + + let poll_result = unsafe { + libc::poll(poll_fds.as_mut_ptr(), 1, 1) // 1ms timeout for low latency + }; + + if poll_result > 0 && (poll_fds[0].revents & libc::POLLIN) != 0 { + let n = unsafe { + libc::read( + tun_fd, + read_buf.as_mut_ptr() as *mut libc::c_void, + read_buf.len(), + ) + }; + + #[cfg(target_os = "macos")] + let min_len = 4; + #[cfg(target_os = "linux")] + let min_len = 1; + + if n > min_len as isize && tun_tx.blocking_send((n as usize, read_buf)).is_err() + { + break; + } + } + } + }); + + info!( + connections = total_conns, + recv_only = num_recv, + bidirectional = num_bidir, + "VPN tunnel active" + ); + + let our_ip = dhcp_config.ip; + let use_compress = self.config.use_compress; + let my_mac = self.mac; + + while self.running.load(Ordering::SeqCst) { + // Helper macro to process received VPN data + macro_rules! process_vpn_data { + ($conn_idx:expr, $data:expr) => {{ + match codecs.get_mut($conn_idx).map(|c| c.feed($data)) { + Some(Ok(frames)) => { + for frame in frames { + if frame.is_keepalive() { + debug!("Received keepalive on conn {}", $conn_idx); + continue; + } + + if let Some(packets) = frame.packets() { + for packet in packets { + let frame_data: &[u8] = if is_compressed(packet) { + match decompress_into(packet, &mut decomp_buf) { + Ok(len) => &decomp_buf[..len], + Err(_) => continue, + } + } else { + packet + }; + + if let Err(e) = self.process_frame_zerocopy( + tun_fd, + &mut tun_write_buf, + &mut arp, + frame_data, + our_ip, + ) { + error!("Process error: {}", e); + } + } + } + } + } + Some(Err(e)) => { + error!("Decode error on conn {}: {}", $conn_idx, e); + } + None => {} + } + + // Send any pending ARP replies + if let Some(reply) = arp.build_pending_reply() { + if let Err(e) = self.send_frame_multi(conn_mgr, &reply, &mut send_buf).await + { + error!("Failed to send ARP reply: {}", e); + } else { + debug!("Sent ARP reply"); + } + arp.take_pending_reply(); + } + + last_activity = Instant::now(); + }}; + } + + // Create futures for reading + // 1. Concurrent reader for receive-only connections (half-connection mode) + let concurrent_recv = async { + if let Some(ref mut reader) = concurrent_reader { + reader.recv().await + } else { + // No concurrent reader - pend forever + std::future::pending::>().await + } + }; + + // 2. Direct read from bidirectional connections in conn_mgr (with per-conn decryption) + let bidir_recv = async { + if num_bidir > 0 { + conn_mgr.read_any_decrypt(&mut bidir_read_buf).await + } else { + // No bidirectional connections - pend forever + std::future::pending::>().await + } + }; + + tokio::select! { + // Biased: prioritize data paths over timers to minimize latency + biased; + + // Packet from TUN device (from local applications) + Some((len, tun_buf)) = tun_rx.recv() => { + #[cfg(target_os = "macos")] + let ip_packet = &tun_buf[4..len]; + #[cfg(target_os = "linux")] + let ip_packet = &tun_buf[..len]; + + if ip_packet.is_empty() { + continue; + } + + let gateway_mac = arp.gateway_mac_or_broadcast(); + + // Build tunnel frame + let eth_len = 14 + ip_packet.len(); + let total_len = 8 + eth_len; + + if total_len > send_buf.len() { + warn!("Packet too large: {}", ip_packet.len()); + continue; + } + + let ip_version = (ip_packet[0] >> 4) & 0x0F; + if ip_version != 4 && ip_version != 6 { + continue; + } + + if use_compress { + // Compression path + let eth_start = 8; + send_buf[eth_start..eth_start + 6].copy_from_slice(&gateway_mac); + send_buf[eth_start + 6..eth_start + 12].copy_from_slice(&my_mac); + if ip_version == 4 { + send_buf[eth_start + 12] = 0x08; + send_buf[eth_start + 13] = 0x00; + } else { + send_buf[eth_start + 12] = 0x86; + send_buf[eth_start + 13] = 0xDD; + } + send_buf[eth_start + 14..eth_start + 14 + ip_packet.len()] + .copy_from_slice(ip_packet); + + let eth_frame = &send_buf[eth_start..eth_start + eth_len]; + + match compress_into(eth_frame, &mut comp_buf) { + Ok(comp_len) => { + let comp_total = 8 + comp_len; + if comp_total <= send_buf.len() { + send_buf[0..4].copy_from_slice(&1u32.to_be_bytes()); + send_buf[4..8].copy_from_slice(&(comp_len as u32).to_be_bytes()); + send_buf[8..8 + comp_len].copy_from_slice(&comp_buf[..comp_len]); + // Use per-connection encryption via ConnectionManager + conn_mgr.write_all_encrypted(&mut send_buf[..comp_total]).await?; + } + } + Err(e) => { + warn!("Compression failed: {}", e); + send_buf[0..4].copy_from_slice(&1u32.to_be_bytes()); + send_buf[4..8].copy_from_slice(&(eth_len as u32).to_be_bytes()); + // Use per-connection encryption via ConnectionManager + conn_mgr.write_all_encrypted(&mut send_buf[..total_len]).await?; + } + } + } else { + // Uncompressed path + send_buf[0..4].copy_from_slice(&1u32.to_be_bytes()); + send_buf[4..8].copy_from_slice(&(eth_len as u32).to_be_bytes()); + send_buf[8..14].copy_from_slice(&gateway_mac); + send_buf[14..20].copy_from_slice(&my_mac); + if ip_version == 4 { + send_buf[20] = 0x08; + send_buf[21] = 0x00; + } else { + send_buf[20] = 0x86; + send_buf[21] = 0xDD; + } + send_buf[22..22 + ip_packet.len()].copy_from_slice(ip_packet); + + // Use per-connection encryption via ConnectionManager + conn_mgr.write_all_encrypted(&mut send_buf[..total_len]).await?; + } + + last_activity = Instant::now(); + } + + // Data from receive-only connections via ConcurrentReader + // ConcurrentReader handles per-connection decryption internally + Some(packet) = concurrent_recv => { + let conn_idx = packet.conn_index; + // Data is already decrypted by ConcurrentReader's per-connection cipher + // Use &[u8] deref directly - Bytes implements Deref + process_vpn_data!(conn_idx, &packet.data[..]); + } + + // Data from bidirectional connections (direct read with per-conn decryption) + result = bidir_recv => { + if let Ok((conn_idx, n)) = result { + if n > 0 { + // Data is already decrypted by read_any_decrypt + let data = &bidir_read_buf[..n]; + process_vpn_data!(conn_idx, data); + } + } + } + + // Keepalive timer + _ = keepalive_interval.tick() => { + if last_activity.elapsed() > Duration::from_secs(3) { + let keepalive = TunnelCodec::encode_keepalive_direct( + 32, + &mut send_buf, + ); + if let Some(ka) = keepalive { + // Use per-connection encryption via ConnectionManager + let ka_len = ka.len(); + conn_mgr.write_all_encrypted(&mut send_buf[..ka_len]).await?; + debug!("Sent keepalive"); + } + } + + if arp.should_send_periodic_garp() { + let garp = arp.build_gratuitous_arp(); + self.send_frame_multi(conn_mgr, &garp, &mut send_buf).await?; + arp.mark_garp_sent(); + debug!("Sent periodic GARP"); + } + } + } + } + + info!("VPN tunnel stopped"); + + // Cleanup + if let Some(ref mut reader) = concurrent_reader { + reader.shutdown(); + let recv_stats = reader.bytes_received(); + let total_recv: u64 = recv_stats.iter().map(|(_, b)| b).sum(); + debug!( + bytes = total_recv, + connections = recv_stats.len(), + "Concurrent reader shutdown" + ); + } + tun_reader.abort(); + + Ok(()) + } + + /// Windows-specific multi-connection data loop. + /// Note: On Windows, this falls back to single-connection behavior since + /// the Wintun API doesn't support the same zero-copy optimizations. + #[cfg(target_os = "windows")] + pub(super) async fn run_data_loop_multi( + &self, + conn_mgr: &mut ConnectionManager, + tun: &mut WintunDevice, + dhcp_config: &DhcpConfig, + ) -> Result<()> { + let mut state = DataLoopState::new(self.mac); + let gateway = dhcp_config.gateway.unwrap_or(dhcp_config.ip); + state.configure(dhcp_config.ip, gateway); + + // Get the total number of connections + let total_conns = conn_mgr.connection_count(); + let recv_conns = conn_mgr.take_recv_connections(); + let num_recv = recv_conns.len(); + let num_bidir = conn_mgr.connection_count(); + + let mut concurrent_reader = if !recv_conns.is_empty() { + Some(ConcurrentReader::new(recv_conns, 256)) + } else { + None + }; + + let mut codecs: Vec = (0..total_conns).map(|_| TunnelCodec::new()).collect(); + let mut bidir_read_buf = vec![0u8; 8192]; + let mut send_buf = vec![0u8; 4096]; + let mut comp_buf = vec![0u8; 4096]; // Pre-allocated buffer for compression + let mut decomp_buf = vec![0u8; 4096]; + + // Per-connection encryption is now managed by ConnectionManager + + let mut arp = ArpHandler::new(self.mac); + arp.configure(dhcp_config.ip, gateway); + + let garp = arp.build_gratuitous_arp(); + self.send_frame_multi(conn_mgr, &garp, &mut send_buf) + .await?; + debug!("Sent gratuitous ARP"); + + let gateway_arp = arp.build_gateway_request(); + self.send_frame_multi(conn_mgr, &gateway_arp, &mut send_buf) + .await?; + debug!("Sent gateway ARP request"); + + let mut keepalive_interval = interval(Duration::from_secs(self.config.keepalive_interval)); + let mut last_activity = Instant::now(); + + let (tun_tx, mut tun_rx) = mpsc::channel::>(256); + let session = tun.session(); + let running = self.running.clone(); + + let tun_reader = tokio::task::spawn_blocking(move || { + while running.load(Ordering::SeqCst) { + match session.receive_blocking() { + Ok(packet) => { + let bytes = packet.bytes().to_vec(); + if tun_tx.blocking_send(bytes).is_err() { + break; + } + } + Err(_) => { + std::thread::sleep(Duration::from_millis(1)); + } + } + } + }); + + info!( + connections = total_conns, + recv_only = num_recv, + bidirectional = num_bidir, + "VPN tunnel active (Windows)" + ); + + let our_ip = dhcp_config.ip; + let use_compress = self.config.use_compress; + let my_mac = self.mac; + + while self.running.load(Ordering::SeqCst) { + // Helper macro to process received VPN data + macro_rules! process_vpn_data { + ($conn_idx:expr, $data:expr) => {{ + match codecs.get_mut($conn_idx).map(|c| c.feed($data)) { + Some(Ok(frames)) => { + for frame in frames { + if frame.is_keepalive() { + debug!("Received keepalive on conn {}", $conn_idx); + continue; + } + + if let Some(packets) = frame.packets() { + for packet in packets { + let frame_data: &[u8] = if is_compressed(packet) { + match decompress_into(packet, &mut decomp_buf) { + Ok(len) => &decomp_buf[..len], + Err(_) => continue, + } + } else { + packet + }; + + if let Err(e) = self.process_frame_windows( + tun, &mut arp, frame_data, our_ip, + ) { + error!("Process error: {}", e); + } + } + } + } + } + Some(Err(e)) => { + error!("Decode error on conn {}: {}", $conn_idx, e); + } + None => {} + } + + if let Some(reply) = arp.build_pending_reply() { + if let Err(e) = self.send_frame_multi(conn_mgr, &reply, &mut send_buf).await + { + error!("Failed to send ARP reply: {}", e); + } else { + debug!("Sent ARP reply"); + } + arp.take_pending_reply(); + } + + last_activity = Instant::now(); + }}; + } + + let concurrent_recv = async { + if let Some(ref mut reader) = concurrent_reader { + reader.recv().await + } else { + std::future::pending::>().await + } + }; + + let bidir_recv = async { + if num_bidir > 0 { + conn_mgr.read_any_decrypt(&mut bidir_read_buf).await + } else { + std::future::pending::>().await + } + }; + + tokio::select! { + biased; + + Some(ip_packet) = tun_rx.recv() => { + if ip_packet.is_empty() { + continue; + } + + let gateway_mac = arp.gateway_mac_or_broadcast(); + let eth_len = 14 + ip_packet.len(); + let total_len = 8 + eth_len; + + if total_len > send_buf.len() { + warn!("Packet too large: {}", ip_packet.len()); + continue; + } + + let ip_version = (ip_packet[0] >> 4) & 0x0F; + if ip_version != 4 && ip_version != 6 { + continue; + } + + if use_compress { + let eth_start = 8; + send_buf[eth_start..eth_start + 6].copy_from_slice(&gateway_mac); + send_buf[eth_start + 6..eth_start + 12].copy_from_slice(&my_mac); + if ip_version == 4 { + send_buf[eth_start + 12] = 0x08; + send_buf[eth_start + 13] = 0x00; + } else { + send_buf[eth_start + 12] = 0x86; + send_buf[eth_start + 13] = 0xDD; + } + send_buf[eth_start + 14..eth_start + 14 + ip_packet.len()] + .copy_from_slice(&ip_packet); + + let eth_frame = &send_buf[eth_start..eth_start + eth_len]; + + match compress_into(eth_frame, &mut comp_buf) { + Ok(comp_len) => { + let comp_total = 8 + comp_len; + if comp_total <= send_buf.len() { + send_buf[0..4].copy_from_slice(&1u32.to_be_bytes()); + send_buf[4..8].copy_from_slice(&(comp_len as u32).to_be_bytes()); + send_buf[8..8 + comp_len].copy_from_slice(&comp_buf[..comp_len]); + conn_mgr.write_all_encrypted(&mut send_buf[..comp_total]).await?; + } + } + Err(e) => { + warn!("Compression failed: {}", e); + send_buf[0..4].copy_from_slice(&1u32.to_be_bytes()); + send_buf[4..8].copy_from_slice(&(eth_len as u32).to_be_bytes()); + conn_mgr.write_all_encrypted(&mut send_buf[..total_len]).await?; + } + } + } else { + send_buf[0..4].copy_from_slice(&1u32.to_be_bytes()); + send_buf[4..8].copy_from_slice(&(eth_len as u32).to_be_bytes()); + send_buf[8..14].copy_from_slice(&gateway_mac); + send_buf[14..20].copy_from_slice(&my_mac); + if ip_version == 4 { + send_buf[20] = 0x08; + send_buf[21] = 0x00; + } else { + send_buf[20] = 0x86; + send_buf[21] = 0xDD; + } + send_buf[22..22 + ip_packet.len()].copy_from_slice(&ip_packet); + + conn_mgr.write_all_encrypted(&mut send_buf[..total_len]).await?; + } + + last_activity = Instant::now(); + } + + // ConcurrentReader handles per-connection decryption internally + Some(packet) = concurrent_recv => { + let conn_idx = packet.conn_index; + // Data is already decrypted by ConcurrentReader's per-connection cipher + let data: Vec = packet.data.to_vec(); + process_vpn_data!(conn_idx, &data[..]); + } + + result = bidir_recv => { + if let Ok((conn_idx, n)) = result { + if n > 0 { + // Data is already decrypted by read_any_decrypt + let data = &bidir_read_buf[..n]; + process_vpn_data!(conn_idx, data); + } + } + } + + _ = keepalive_interval.tick() => { + if last_activity.elapsed() > Duration::from_secs(3) { + let keepalive = TunnelCodec::encode_keepalive_direct(32, &mut send_buf); + if let Some(ka) = keepalive { + let ka_len = ka.len(); + conn_mgr.write_all_encrypted(&mut send_buf[..ka_len]).await?; + debug!("Sent keepalive"); + } + } + + if arp.should_send_periodic_garp() { + let garp = arp.build_gratuitous_arp(); + self.send_frame_multi(conn_mgr, &garp, &mut send_buf).await?; + arp.mark_garp_sent(); + debug!("Sent periodic GARP"); + } + } + } + } + + info!("VPN tunnel stopped"); + + if let Some(ref mut reader) = concurrent_reader { + reader.shutdown(); + let recv_stats = reader.bytes_received(); + let total_recv: u64 = recv_stats.iter().map(|(_, b)| b).sum(); + debug!( + bytes = total_recv, + connections = recv_stats.len(), + "Concurrent reader shutdown" + ); + } + tun_reader.abort(); + + Ok(()) + } +} diff --git a/src/tunnel/packet_processor.rs b/src/tunnel/packet_processor.rs new file mode 100644 index 0000000..1b7a74d --- /dev/null +++ b/src/tunnel/packet_processor.rs @@ -0,0 +1,209 @@ +//! Shared packet processing logic for data loop. +//! +//! This module extracts the common packet processing code that is shared between +//! Unix (macOS/Linux) and Windows data loops, eliminating ~200 lines of duplication. + +use std::net::Ipv4Addr; +use std::time::{Duration, Instant}; + +use tracing::{debug, error, warn}; + +use crate::client::VpnConnection; +use crate::crypto::TunnelEncryption; +use crate::error::Result; +use crate::packet::ArpHandler; +use crate::protocol::TunnelCodec; + +/// Build an ethernet frame header in the send buffer (uncompressed path). +/// +/// Layout: [4: num_blocks=1][4: block_size][6: dst_mac][6: src_mac][2: ethertype][IP packet] +/// +/// Returns the total frame length (8-byte tunnel header + 14-byte eth header + IP packet), +/// or None if packet is too large or invalid IP version. +#[inline] +pub fn build_ethernet_frame( + send_buf: &mut [u8], + ip_packet: &[u8], + gateway_mac: &[u8; 6], + my_mac: &[u8; 6], +) -> Option { + let eth_len = 14 + ip_packet.len(); + let total_len = 8 + eth_len; + + if total_len > send_buf.len() { + warn!("Packet too large: {}", ip_packet.len()); + return None; + } + + if ip_packet.is_empty() { + return None; + } + + let ip_version = (ip_packet[0] >> 4) & 0x0F; + if ip_version != 4 && ip_version != 6 { + return None; + } + + // Tunnel header: num_blocks = 1, block_size = eth_len + send_buf[0..4].copy_from_slice(&1u32.to_be_bytes()); + send_buf[4..8].copy_from_slice(&(eth_len as u32).to_be_bytes()); + + // Ethernet header + send_buf[8..14].copy_from_slice(gateway_mac); + send_buf[14..20].copy_from_slice(my_mac); + + // EtherType + if ip_version == 4 { + send_buf[20] = 0x08; + send_buf[21] = 0x00; + } else { + send_buf[20] = 0x86; + send_buf[21] = 0xDD; + } + + // IP packet + send_buf[22..22 + ip_packet.len()].copy_from_slice(ip_packet); + + Some(total_len) +} + +/// Build ethernet frame for compression path (frame at offset 8). +/// +/// Returns (eth_len, ip_version) or None if invalid. +#[inline] +pub fn build_ethernet_frame_for_compress( + send_buf: &mut [u8], + ip_packet: &[u8], + gateway_mac: &[u8; 6], + my_mac: &[u8; 6], +) -> Option<(usize, u8)> { + let eth_len = 14 + ip_packet.len(); + let total_len = 8 + eth_len; + + if total_len > send_buf.len() || ip_packet.is_empty() { + return None; + } + + let ip_version = (ip_packet[0] >> 4) & 0x0F; + if ip_version != 4 && ip_version != 6 { + return None; + } + + let eth_start = 8; + send_buf[eth_start..eth_start + 6].copy_from_slice(gateway_mac); + send_buf[eth_start + 6..eth_start + 12].copy_from_slice(my_mac); + if ip_version == 4 { + send_buf[eth_start + 12] = 0x08; + send_buf[eth_start + 13] = 0x00; + } else { + send_buf[eth_start + 12] = 0x86; + send_buf[eth_start + 13] = 0xDD; + } + send_buf[eth_start + 14..eth_start + 14 + ip_packet.len()].copy_from_slice(ip_packet); + + Some((eth_len, ip_version)) +} + +/// Send keepalive packet if no recent activity. +pub async fn send_keepalive_if_needed( + conn: &mut VpnConnection, + last_activity: Instant, + send_buf: &mut [u8], + encryption: &mut Option, +) -> Result<()> { + if last_activity.elapsed() > Duration::from_secs(3) { + let keepalive = TunnelCodec::encode_keepalive_direct(32, send_buf); + if let Some(ka) = keepalive { + let ka_len = ka.len(); + if let Some(ref mut enc) = encryption { + enc.encrypt(&mut send_buf[..ka_len]); + } + conn.write_all(&send_buf[..ka_len]).await?; + debug!("Sent keepalive"); + } + } + Ok(()) +} + +/// Send periodic gratuitous ARP if needed. +pub async fn send_periodic_garp_if_needed( + conn: &mut VpnConnection, + arp: &mut ArpHandler, + send_buf: &mut [u8], + encryption: &mut Option, +) -> Result<()> { + if arp.should_send_periodic_garp() { + let garp = arp.build_gratuitous_arp(); + send_frame_encrypted(conn, &garp, send_buf, encryption).await?; + arp.mark_garp_sent(); + debug!("Sent periodic GARP"); + } + Ok(()) +} + +/// Send any pending ARP replies. +pub async fn send_pending_arp_reply( + conn: &mut VpnConnection, + arp: &mut ArpHandler, + send_buf: &mut [u8], + encryption: &mut Option, +) -> Result<()> { + if let Some(reply) = arp.build_pending_reply() { + if let Err(e) = send_frame_encrypted(conn, &reply, send_buf, encryption).await { + error!("Failed to send ARP reply: {}", e); + } else { + debug!("Sent ARP reply"); + } + arp.take_pending_reply(); + } + Ok(()) +} + +/// Send an ethernet frame with optional encryption. +pub async fn send_frame_encrypted( + conn: &mut VpnConnection, + frame: &[u8], + send_buf: &mut [u8], + encryption: &mut Option, +) -> Result<()> { + let total_len = 8 + frame.len(); + if total_len > send_buf.len() { + return Ok(()); + } + + // Tunnel header + send_buf[0..4].copy_from_slice(&1u32.to_be_bytes()); + send_buf[4..8].copy_from_slice(&(frame.len() as u32).to_be_bytes()); + send_buf[8..8 + frame.len()].copy_from_slice(frame); + + if let Some(ref mut enc) = encryption { + enc.encrypt(&mut send_buf[..total_len]); + } + + conn.write_all(&send_buf[..total_len]).await?; + Ok(()) +} + +/// Initialize ARP and send initial ARP packets. +pub async fn init_arp( + conn: &mut VpnConnection, + arp: &mut ArpHandler, + our_ip: Ipv4Addr, + gateway: Ipv4Addr, + send_buf: &mut [u8], + encryption: &mut Option, +) -> Result<()> { + arp.configure(our_ip, gateway); + + // Send gratuitous ARP to announce our presence + let garp = arp.build_gratuitous_arp(); + send_frame_encrypted(conn, &garp, send_buf, encryption).await?; + debug!("Sent gratuitous ARP"); + + // Send ARP request for gateway + let gateway_arp = arp.build_gateway_request(); + send_frame_encrypted(conn, &gateway_arp, send_buf, encryption).await?; + debug!("Sent gateway ARP request"); + + Ok(()) +} diff --git a/src/tunnel/runner.rs b/src/tunnel/runner.rs index a8c41e6..646a123 100644 --- a/src/tunnel/runner.rs +++ b/src/tunnel/runner.rs @@ -7,22 +7,19 @@ //! - Bidirectional packet forwarding //! - Multi-connection support for half-connection mode //! - RC4 tunnel encryption (when UseFastRC4 is enabled) +//! +//! The implementation is split across several files: +//! - `runner.rs` (this file): Core TunnelRunner struct and entry points +//! - `dhcp_handler.rs`: DHCP handling for single and multi-connection modes +//! - `single_conn.rs`: Single-connection data loop (Unix + Windows) +//! - `multi_conn.rs`: Multi-connection data loop (half-connection mode) +//! - `packet_processor.rs`: Shared packet processing utilities use std::net::Ipv4Addr; use std::sync::atomic::AtomicBool; use std::sync::Arc; -use std::time::{Duration, Instant}; -#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))] -use std::sync::atomic::Ordering; -#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))] -use tokio::sync::mpsc; -#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))] -use tokio::time::interval; -use tokio::time::timeout; -#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))] -use tracing::error; -use tracing::{debug, info, warn}; +use tracing::{debug, info}; use crate::adapter::TunAdapter; #[cfg(target_os = "linux")] @@ -31,24 +28,10 @@ use crate::adapter::TunDevice; use crate::adapter::UtunDevice; #[cfg(target_os = "windows")] use crate::adapter::WintunDevice; -#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))] -use crate::client::ConcurrentReader; use crate::client::{ConnectionManager, VpnConnection}; use crate::crypto::{Rc4KeyPair, TunnelEncryption}; use crate::error::{Error, Result}; -#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))] -use crate::packet::ArpHandler; -#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))] -use crate::packet::BROADCAST_MAC; -use crate::packet::{DhcpClient, DhcpConfig, DhcpState}; -#[cfg(any(target_os = "macos", target_os = "linux"))] -use crate::protocol::compress_into; -#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))] -use crate::protocol::decompress_into; -use crate::protocol::{compress, decompress, is_compressed, TunnelCodec}; - -#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))] -use super::DataLoopState; +use crate::packet::DhcpConfig; /// Configuration for the tunnel runner. #[derive(Debug, Clone)] @@ -108,9 +91,9 @@ fn generate_mac() -> [u8; 6] { /// Tunnel runner handles the VPN data loop. pub struct TunnelRunner { - config: TunnelConfig, - mac: [u8; 6], - running: Arc, + pub(super) config: TunnelConfig, + pub(super) mac: [u8; 6], + pub(super) running: Arc, } impl TunnelRunner { @@ -131,8 +114,7 @@ impl TunnelRunner { } /// Create encryption state if RC4 keys are configured. - #[allow(dead_code)] - fn create_encryption(&self) -> Option { + pub(super) fn create_encryption(&self) -> Option { self.config.rc4_key_pair.as_ref().map(TunnelEncryption::new) } @@ -271,8 +253,6 @@ impl TunnelRunner { /// Configure routes for VPN traffic. /// /// This sets up routing so traffic to the VPN subnet goes through the TUN device. - /// Currently only used when TUN adapter fully supports routing (Linux). - #[allow(dead_code)] fn configure_routes(&self, tun: &impl TunAdapter, dhcp_config: &DhcpConfig) -> Result<()> { // CRITICAL: If default route is requested, add the VPN server host route FIRST // This ensures the VPN connection itself doesn't get routed through the VPN @@ -346,1901 +326,4 @@ impl TunnelRunner { Ok(()) } - - /// Perform DHCP through the tunnel. - async fn perform_dhcp(&self, conn: &mut VpnConnection) -> Result { - let mut dhcp = DhcpClient::new(self.mac); - let mut codec = TunnelCodec::new(); - let mut buf = vec![0u8; 65536]; - let mut send_buf = vec![0u8; 2048]; - - let deadline = Instant::now() + Duration::from_secs(self.config.dhcp_timeout); - - // Send DHCP DISCOVER - let discover = dhcp.build_discover(); - debug!(bytes = discover.len(), "Sending DHCP DISCOVER"); - self.send_frame(conn, &discover, &mut send_buf).await?; - - // Wait for OFFER - loop { - if Instant::now() > deadline { - return Err(Error::TimeoutMessage( - "DHCP timeout - no OFFER received".into(), - )); - } - - match timeout(Duration::from_secs(3), conn.read(&mut buf)).await { - Ok(Ok(n)) if n > 0 => { - debug!("Received {} bytes from tunnel", n); - // Decode tunnel frames - let frames = codec.feed(&buf[..n])?; - for frame in frames { - if frame.is_keepalive() { - debug!("Received keepalive frame"); - continue; - } - if let Some(packets) = frame.packets() { - for packet in packets { - // Check if packet is compressed and decompress if needed - let packet_data: Vec = if is_compressed(packet) { - match decompress(packet) { - Ok(decompressed) => { - debug!( - "Decompressed {} -> {} bytes", - packet.len(), - decompressed.len() - ); - decompressed - } - Err(e) => { - warn!("Decompression failed: {}", e); - continue; - } - } - } else { - packet.to_vec() - }; - - // Log packet details - if packet_data.len() >= 14 { - let ethertype = - format!("0x{:02X}{:02X}", packet_data[12], packet_data[13]); - debug!( - "Packet: {} bytes, ethertype={}", - packet_data.len(), - ethertype - ); - } - - // Check if this is a DHCP response (UDP port 68) - if self.is_dhcp_response(&packet_data) { - debug!("DHCP response received"); - if dhcp.process_response(&packet_data) { - // Got ACK - return Ok(dhcp.config().clone()); - } else if dhcp.state() == DhcpState::DiscoverSent { - // Got OFFER, send REQUEST - if let Some(request) = dhcp.build_request() { - debug!("Sending DHCP REQUEST"); - self.send_frame(conn, &request, &mut send_buf).await?; - } - } - } - } - } - } - } - Ok(Ok(_)) => { - return Err(Error::ConnectionFailed( - "Connection closed during DHCP".into(), - )); - } - Ok(Err(e)) => { - return Err(Error::Io(e)); - } - Err(_) => { - // Timeout, retry DISCOVER if still in initial state - if dhcp.state() == DhcpState::DiscoverSent { - warn!("DHCP timeout, retrying DISCOVER"); - let discover = dhcp.build_discover(); - self.send_frame(conn, &discover, &mut send_buf).await?; - } else if dhcp.state() == DhcpState::RequestSent { - warn!("DHCP timeout, retrying REQUEST"); - if let Some(request) = dhcp.build_request() { - self.send_frame(conn, &request, &mut send_buf).await?; - } - } - } - } - } - } - - /// Check if an Ethernet frame is a DHCP response (UDP dst port 68). - fn is_dhcp_response(&self, frame: &[u8]) -> bool { - // Ethernet(14) + IP header(20 min) + UDP header(8) - if frame.len() < 42 { - return false; - } - - // Check EtherType is IPv4 - if frame[12] != 0x08 || frame[13] != 0x00 { - return false; - } - - // Check IP protocol is UDP (17) - if frame[23] != 17 { - return false; - } - - // Check UDP destination port is 68 (DHCP client) - let dst_port = u16::from_be_bytes([frame[36], frame[37]]); - dst_port == 68 - } - - /// Send an Ethernet frame through the tunnel. - async fn send_frame( - &self, - conn: &mut VpnConnection, - frame: &[u8], - buf: &mut [u8], - ) -> Result<()> { - // Compress if enabled - let data_to_send: std::borrow::Cow<[u8]> = if self.config.use_compress { - match compress(frame) { - Ok(compressed) => { - debug!("Compressed {} -> {} bytes", frame.len(), compressed.len()); - std::borrow::Cow::Owned(compressed) - } - Err(e) => { - warn!("Compression failed, sending uncompressed: {}", e); - std::borrow::Cow::Borrowed(frame) - } - } - } else { - std::borrow::Cow::Borrowed(frame) - }; - - // Encode as tunnel packet: [num_blocks=1][size][data] - let total_len = 4 + 4 + data_to_send.len(); - if buf.len() < total_len { - return Err(Error::Protocol("Send buffer too small".into())); - } - - buf[0..4].copy_from_slice(&1u32.to_be_bytes()); - buf[4..8].copy_from_slice(&(data_to_send.len() as u32).to_be_bytes()); - buf[8..8 + data_to_send.len()].copy_from_slice(&data_to_send); - - conn.write_all(&buf[..total_len]).await?; - Ok(()) - } - - /// Send an Ethernet frame through the tunnel with optional RC4 encryption. - #[allow(dead_code)] - async fn send_frame_encrypted( - &self, - conn: &mut VpnConnection, - frame: &[u8], - buf: &mut [u8], - encryption: &mut Option, - ) -> Result<()> { - // Compress if enabled - let data_to_send: std::borrow::Cow<[u8]> = if self.config.use_compress { - match compress(frame) { - Ok(compressed) => { - debug!("Compressed {} -> {} bytes", frame.len(), compressed.len()); - std::borrow::Cow::Owned(compressed) - } - Err(e) => { - warn!("Compression failed, sending uncompressed: {}", e); - std::borrow::Cow::Borrowed(frame) - } - } - } else { - std::borrow::Cow::Borrowed(frame) - }; - - // Encode as tunnel packet: [num_blocks=1][size][data] - let total_len = 4 + 4 + data_to_send.len(); - if buf.len() < total_len { - return Err(Error::Protocol("Send buffer too small".into())); - } - - buf[0..4].copy_from_slice(&1u32.to_be_bytes()); - buf[4..8].copy_from_slice(&(data_to_send.len() as u32).to_be_bytes()); - buf[8..8 + data_to_send.len()].copy_from_slice(&data_to_send); - - // Encrypt before sending if encryption is enabled - if let Some(ref mut enc) = encryption { - enc.encrypt(&mut buf[..total_len]); - } - - conn.write_all(&buf[..total_len]).await?; - Ok(()) - } - - /// Run the main data forwarding loop. - /// - /// Zero-copy optimized path: - /// - Outbound: TUN read β†’ inline Ethernet wrap β†’ direct send - /// - Inbound: Network read β†’ direct TUN write (skip Ethernet header) - #[cfg(any(target_os = "macos", target_os = "linux"))] - async fn run_data_loop( - &self, - conn: &mut VpnConnection, - tun: &mut impl TunAdapter, - dhcp_config: &DhcpConfig, - ) -> Result<()> { - self.run_data_loop_unix(conn, tun, dhcp_config).await - } - - #[cfg(target_os = "windows")] - async fn run_data_loop( - &self, - conn: &mut VpnConnection, - tun: &mut WintunDevice, - dhcp_config: &DhcpConfig, - ) -> Result<()> { - self.run_data_loop_windows(conn, tun, dhcp_config).await - } - - /// Unix-specific data loop implementation using libc poll/read. - #[cfg(any(target_os = "macos", target_os = "linux"))] - async fn run_data_loop_unix( - &self, - conn: &mut VpnConnection, - tun: &mut impl TunAdapter, - dhcp_config: &DhcpConfig, - ) -> Result<()> { - let mut state = DataLoopState::new(self.mac); - let gateway = dhcp_config.gateway.unwrap_or(dhcp_config.ip); - state.configure(dhcp_config.ip, gateway); - - let mut codec = TunnelCodec::new(); - - // Initialize RC4 encryption if enabled - let mut encryption = self.create_encryption(); - if encryption.is_some() { - info!("RC4 encryption active for tunnel data"); - } - - // Pre-allocated buffers - sized for maximum packets - // Network receive buffer - let mut net_buf = vec![0u8; 65536]; - // Send buffer: 4 (utun header) + 14 (eth) + 1400 (MTU) + 8 (tunnel header) + compression overhead - let mut send_buf = vec![0u8; 4096]; - // Decompression buffer (reused to avoid allocation per packet) - let mut decomp_buf = vec![0u8; 4096]; - // Compression output buffer (reused to avoid allocation per packet) - let mut comp_buf = vec![0u8; 4096]; - - // TUN write buffer with utun header space pre-allocated - // Layout: [4-byte utun header][IP packet] - let mut tun_write_buf = vec![0u8; 2048]; - - // Set up ARP handler - let mut arp = ArpHandler::new(self.mac); - arp.configure(dhcp_config.ip, gateway); - - // Send gratuitous ARP to announce our presence - let garp = arp.build_gratuitous_arp(); - self.send_frame_encrypted(conn, &garp, &mut send_buf, &mut encryption) - .await?; - debug!("Sent gratuitous ARP"); - - // Send ARP request for gateway - let gateway_arp = arp.build_gateway_request(); - self.send_frame_encrypted(conn, &gateway_arp, &mut send_buf, &mut encryption) - .await?; - debug!("Sent gateway ARP request"); - - let mut keepalive_interval = interval(Duration::from_secs(self.config.keepalive_interval)); - let mut last_activity = Instant::now(); - - // Zero-copy TUN reader using fixed buffer - // macOS utun: [4-byte protocol header][IP packet] - // Linux tun: [IP packet] (no header with IFF_NO_PI) - let (tun_tx, mut tun_rx) = mpsc::channel::<(usize, [u8; 2048])>(256); - let tun_fd = tun.raw_fd(); - let running = self.running.clone(); - - // Spawn blocking TUN reader task - zero allocation in hot path - let tun_reader = tokio::task::spawn_blocking(move || { - // Fixed buffer - no allocation per packet - let mut read_buf = [0u8; 2048]; - - while running.load(Ordering::SeqCst) { - // Poll with 1ms timeout for minimal latency - let mut poll_fds = [libc::pollfd { - fd: tun_fd, - events: libc::POLLIN, - revents: 0, - }]; - - let poll_result = unsafe { - libc::poll(poll_fds.as_mut_ptr(), 1, 1) // 1ms timeout for low latency - }; - - if poll_result > 0 && (poll_fds[0].revents & libc::POLLIN) != 0 { - let n = unsafe { - libc::read( - tun_fd, - read_buf.as_mut_ptr() as *mut libc::c_void, - read_buf.len(), - ) - }; - - // macOS utun has 4-byte header, Linux tun doesn't - #[cfg(target_os = "macos")] - let min_len = 4; - #[cfg(target_os = "linux")] - let min_len = 1; - - if n > min_len as isize { - // Send the buffer with length - receiver will extract IP packet - // Channel copies the fixed buffer (unavoidable for cross-thread) - if tun_tx.blocking_send((n as usize, read_buf)).is_err() { - break; - } - } - } - } - }); - - info!("VPN tunnel active"); - - let our_ip = dhcp_config.ip; - let use_compress = self.config.use_compress; - let my_mac = self.mac; - - while self.running.load(Ordering::SeqCst) { - tokio::select! { - // Biased: prioritize data paths over timers to minimize latency - biased; - - // Packet from TUN device (from local applications) - Some((len, tun_buf)) = tun_rx.recv() => { - // macOS utun: [4-byte header][IP packet] - IP starts at offset 4 - // Linux tun: [IP packet] - IP starts at offset 0 - #[cfg(target_os = "macos")] - let ip_packet = &tun_buf[4..len]; - #[cfg(target_os = "linux")] - let ip_packet = &tun_buf[..len]; - - if ip_packet.is_empty() { - continue; - } - - let gateway_mac = arp.gateway_mac_or_broadcast(); - - // Zero-copy path: build tunnel frame directly in send_buf - // Layout: [4: num_blocks][4: block_size][14: eth header][IP packet] - let eth_len = 14 + ip_packet.len(); - let total_len = 8 + eth_len; - - if total_len > send_buf.len() { - warn!("Packet too large: {}", ip_packet.len()); - continue; - } - - // Determine IP version - let ip_version = (ip_packet[0] >> 4) & 0x0F; - if ip_version != 4 && ip_version != 6 { - continue; - } - - if use_compress { - // Compression path - needs intermediate buffer - // Build ethernet frame first - let eth_start = 8; - send_buf[eth_start..eth_start + 6].copy_from_slice(&gateway_mac); - send_buf[eth_start + 6..eth_start + 12].copy_from_slice(&my_mac); - if ip_version == 4 { - send_buf[eth_start + 12] = 0x08; - send_buf[eth_start + 13] = 0x00; - } else { - send_buf[eth_start + 12] = 0x86; - send_buf[eth_start + 13] = 0xDD; - } - send_buf[eth_start + 14..eth_start + 14 + ip_packet.len()] - .copy_from_slice(ip_packet); - - let eth_frame = &send_buf[eth_start..eth_start + eth_len]; - - // Compress into pre-allocated buffer (zero allocation hot path) - match compress_into(eth_frame, &mut comp_buf) { - Ok(comp_len) => { - let comp_total = 8 + comp_len; - if comp_total <= send_buf.len() { - // Write header - send_buf[0..4].copy_from_slice(&1u32.to_be_bytes()); - send_buf[4..8].copy_from_slice(&(comp_len as u32).to_be_bytes()); - send_buf[8..8 + comp_len].copy_from_slice(&comp_buf[..comp_len]); - // Encrypt before sending - if let Some(ref mut enc) = encryption { - enc.encrypt(&mut send_buf[..comp_total]); - } - conn.write_all(&send_buf[..comp_total]).await?; - } - } - Err(e) => { - warn!("Compression failed: {}", e); - // Fall back to uncompressed - send_buf[0..4].copy_from_slice(&1u32.to_be_bytes()); - send_buf[4..8].copy_from_slice(&(eth_len as u32).to_be_bytes()); - // Encrypt before sending - if let Some(ref mut enc) = encryption { - enc.encrypt(&mut send_buf[..total_len]); - } - conn.write_all(&send_buf[..total_len]).await?; - } - } - } else { - // Zero-copy uncompressed path - // num_blocks = 1 - send_buf[0..4].copy_from_slice(&1u32.to_be_bytes()); - // block_size = eth_len - send_buf[4..8].copy_from_slice(&(eth_len as u32).to_be_bytes()); - // Ethernet header - send_buf[8..14].copy_from_slice(&gateway_mac); - send_buf[14..20].copy_from_slice(&my_mac); - if ip_version == 4 { - send_buf[20] = 0x08; - send_buf[21] = 0x00; - } else { - send_buf[20] = 0x86; - send_buf[21] = 0xDD; - } - // IP packet - single copy - send_buf[22..22 + ip_packet.len()].copy_from_slice(ip_packet); - - // Encrypt before sending - if let Some(ref mut enc) = encryption { - enc.encrypt(&mut send_buf[..total_len]); - } - conn.write_all(&send_buf[..total_len]).await?; - } - - last_activity = Instant::now(); - } - - // Data from VPN connection - result = conn.read(&mut net_buf) => { - match result { - Ok(n) if n > 0 => { - // Decrypt received data if RC4 encryption is enabled - if let Some(ref mut enc) = encryption { - enc.decrypt(&mut net_buf[..n]); - } - - // Decode frames - match codec.feed(&net_buf[..n]) { - Ok(frames) => { - for frame in frames { - if frame.is_keepalive() { - debug!("Received keepalive"); - continue; - } - - // Process each packet in the frame - if let Some(packets) = frame.packets() { - for packet in packets { - // Decompress if needed - let frame_data: &[u8] = if is_compressed(packet) { - match decompress_into(packet, &mut decomp_buf) { - Ok(len) => &decomp_buf[..len], - Err(_) => continue, - } - } else { - packet - }; - - // Process frame with mutable ARP access - if let Err(e) = self.process_frame_zerocopy( - tun_fd, - &mut tun_write_buf, - &mut arp, - frame_data, - our_ip, - ) { - error!("Process error: {}", e); - } - } - } - } - } - Err(e) => { - error!("Decode error: {}", e); - } - } - - // Send any pending ARP replies - if let Some(reply) = arp.build_pending_reply() { - if let Err(e) = self.send_frame_encrypted(conn, &reply, &mut send_buf, &mut encryption).await { - error!("Failed to send ARP reply: {}", e); - } else { - debug!("Sent ARP reply"); - } - arp.take_pending_reply(); - } - - last_activity = Instant::now(); - } - Ok(_) => { - warn!("Server closed connection"); - break; - } - Err(e) => { - error!(error = %e, "Network read failed"); - break; - } - } - } - - // Keepalive timer - _ = keepalive_interval.tick() => { - // Send keepalive if no recent activity - if last_activity.elapsed() > Duration::from_secs(3) { - let keepalive = TunnelCodec::encode_keepalive_direct( - 32, - &mut send_buf, - ); - if let Some(ka) = keepalive { - // Encrypt keepalive before sending - let ka_len = ka.len(); - if let Some(ref mut enc) = encryption { - enc.encrypt(&mut send_buf[..ka_len]); - } - conn.write_all(&send_buf[..ka_len]).await?; - debug!("Sent keepalive"); - } - } - - // Periodic gratuitous ARP - if arp.should_send_periodic_garp() { - let garp = arp.build_gratuitous_arp(); - self.send_frame_encrypted(conn, &garp, &mut send_buf, &mut encryption).await?; - arp.mark_garp_sent(); - debug!("Sent periodic GARP"); - } - } - } - } - - info!("VPN tunnel stopped"); - tun_reader.abort(); - Ok(()) - } - - /// Windows-specific data loop implementation using Wintun. - #[cfg(target_os = "windows")] - async fn run_data_loop_windows( - &self, - conn: &mut VpnConnection, - tun: &mut WintunDevice, - dhcp_config: &DhcpConfig, - ) -> Result<()> { - let mut state = DataLoopState::new(self.mac); - let gateway = dhcp_config.gateway.unwrap_or(dhcp_config.ip); - state.configure(dhcp_config.ip, gateway); - - let mut codec = TunnelCodec::new(); - - // Initialize RC4 encryption if enabled - let mut encryption = self.create_encryption(); - if encryption.is_some() { - info!("RC4 encryption active for tunnel data (Windows)"); - } - - // Pre-allocated buffers - let mut net_buf = vec![0u8; 65536]; - let mut send_buf = vec![0u8; 4096]; - let mut decomp_buf = vec![0u8; 4096]; - - // Set up ARP handler - let mut arp = ArpHandler::new(self.mac); - arp.configure(dhcp_config.ip, gateway); - - // Send gratuitous ARP to announce our presence - let garp = arp.build_gratuitous_arp(); - self.send_frame_encrypted(conn, &garp, &mut send_buf, &mut encryption) - .await?; - debug!("Sent gratuitous ARP"); - - // Send ARP request for gateway - let gateway_arp = arp.build_gateway_request(); - self.send_frame_encrypted(conn, &gateway_arp, &mut send_buf, &mut encryption) - .await?; - debug!("Sent gateway ARP request"); - - let mut keepalive_interval = interval(Duration::from_secs(self.config.keepalive_interval)); - let mut last_activity = Instant::now(); - - // Set up TUN reader channel - let (tun_tx, mut tun_rx) = mpsc::channel::>(256); - let session = tun.session(); - let running = self.running.clone(); - - // Spawn blocking TUN reader task for Windows with optimized polling - let tun_reader = tokio::task::spawn_blocking(move || { - // Use a tight loop with try_receive for lower latency - // Fall back to blocking receive when no packets are available - let mut idle_count = 0u32; - - while running.load(Ordering::SeqCst) { - // Try non-blocking receive first for lower latency - match session.try_receive() { - Ok(Some(packet)) => { - let bytes = packet.bytes().to_vec(); - if tun_tx.blocking_send(bytes).is_err() { - break; - } - idle_count = 0; // Reset idle counter on successful receive - } - Ok(None) => { - // No packet available - idle_count += 1; - if idle_count > 100 { - // After many idle iterations, use blocking receive to save CPU - match session.receive_blocking() { - Ok(packet) => { - let bytes = packet.bytes().to_vec(); - if tun_tx.blocking_send(bytes).is_err() { - break; - } - } - Err(_) => { - std::thread::sleep(Duration::from_micros(100)); - } - } - idle_count = 0; - } else { - // Brief yield to prevent busy-waiting while staying responsive - std::thread::yield_now(); - } - } - Err(_) => { - std::thread::sleep(Duration::from_micros(100)); - idle_count = 0; - } - } - } - }); - - info!("VPN tunnel active (Windows)"); - - let our_ip = dhcp_config.ip; - let use_compress = self.config.use_compress; - let my_mac = self.mac; - - while self.running.load(Ordering::SeqCst) { - tokio::select! { - biased; - - // Packet from TUN device (from local applications) - Some(ip_packet) = tun_rx.recv() => { - if ip_packet.is_empty() { - continue; - } - - let gateway_mac = arp.gateway_mac_or_broadcast(); - - // Build tunnel frame - let eth_len = 14 + ip_packet.len(); - let total_len = 8 + eth_len; - - if total_len > send_buf.len() { - warn!("Packet too large: {}", ip_packet.len()); - continue; - } - - let ip_version = (ip_packet[0] >> 4) & 0x0F; - if ip_version != 4 && ip_version != 6 { - continue; - } - - if use_compress { - // Compression path - let eth_start = 8; - send_buf[eth_start..eth_start + 6].copy_from_slice(&gateway_mac); - send_buf[eth_start + 6..eth_start + 12].copy_from_slice(&my_mac); - if ip_version == 4 { - send_buf[eth_start + 12] = 0x08; - send_buf[eth_start + 13] = 0x00; - } else { - send_buf[eth_start + 12] = 0x86; - send_buf[eth_start + 13] = 0xDD; - } - send_buf[eth_start + 14..eth_start + 14 + ip_packet.len()] - .copy_from_slice(&ip_packet); - - let eth_frame = &send_buf[eth_start..eth_start + eth_len]; - - match compress(eth_frame) { - Ok(compressed) => { - let comp_total = 8 + compressed.len(); - if comp_total <= send_buf.len() { - send_buf[0..4].copy_from_slice(&1u32.to_be_bytes()); - send_buf[4..8].copy_from_slice(&(compressed.len() as u32).to_be_bytes()); - send_buf[8..8 + compressed.len()].copy_from_slice(&compressed); - // Encrypt before sending - if let Some(ref mut enc) = encryption { - enc.encrypt(&mut send_buf[..comp_total]); - } - conn.write_all(&send_buf[..comp_total]).await?; - } - } - Err(e) => { - warn!("Compression failed: {}", e); - send_buf[0..4].copy_from_slice(&1u32.to_be_bytes()); - send_buf[4..8].copy_from_slice(&(eth_len as u32).to_be_bytes()); - // Encrypt before sending - if let Some(ref mut enc) = encryption { - enc.encrypt(&mut send_buf[..total_len]); - } - conn.write_all(&send_buf[..total_len]).await?; - } - } - } else { - // Uncompressed path - send_buf[0..4].copy_from_slice(&1u32.to_be_bytes()); - send_buf[4..8].copy_from_slice(&(eth_len as u32).to_be_bytes()); - send_buf[8..14].copy_from_slice(&gateway_mac); - send_buf[14..20].copy_from_slice(&my_mac); - if ip_version == 4 { - send_buf[20] = 0x08; - send_buf[21] = 0x00; - } else { - send_buf[20] = 0x86; - send_buf[21] = 0xDD; - } - send_buf[22..22 + ip_packet.len()].copy_from_slice(&ip_packet); - - // Encrypt before sending - if let Some(ref mut enc) = encryption { - enc.encrypt(&mut send_buf[..total_len]); - } - conn.write_all(&send_buf[..total_len]).await?; - } - - last_activity = Instant::now(); - } - - // Data from VPN connection - result = conn.read(&mut net_buf) => { - match result { - Ok(n) if n > 0 => { - // Decrypt received data if RC4 encryption is enabled - if let Some(ref mut enc) = encryption { - enc.decrypt(&mut net_buf[..n]); - } - - match codec.feed(&net_buf[..n]) { - Ok(frames) => { - for frame in frames { - if frame.is_keepalive() { - debug!("Received keepalive"); - continue; - } - - if let Some(packets) = frame.packets() { - for packet in packets { - let frame_data: &[u8] = if is_compressed(packet) { - match decompress_into(packet, &mut decomp_buf) { - Ok(len) => &decomp_buf[..len], - Err(_) => continue, - } - } else { - packet - }; - - if let Err(e) = self.process_frame_windows( - tun, - &mut arp, - frame_data, - our_ip, - ) { - error!("Process error: {}", e); - } - } - } - } - } - Err(e) => { - error!("Decode error: {}", e); - } - } - - // Send any pending ARP replies - if let Some(reply) = arp.build_pending_reply() { - if let Err(e) = self.send_frame_encrypted(conn, &reply, &mut send_buf, &mut encryption).await { - error!("Failed to send ARP reply: {}", e); - } else { - debug!("Sent ARP reply"); - } - arp.take_pending_reply(); - } - - last_activity = Instant::now(); - } - Ok(_) => { - warn!("Server closed connection"); - break; - } - Err(e) => { - error!(error = %e, "Network read failed"); - break; - } - } - } - - // Keepalive timer - _ = keepalive_interval.tick() => { - if last_activity.elapsed() > Duration::from_secs(3) { - let keepalive = TunnelCodec::encode_keepalive_direct(32, &mut send_buf); - if let Some(ka) = keepalive { - // Encrypt keepalive before sending - let ka_len = ka.len(); - if let Some(ref mut enc) = encryption { - enc.encrypt(&mut send_buf[..ka_len]); - } - conn.write_all(&send_buf[..ka_len]).await?; - debug!("Sent keepalive"); - } - } - - // Periodic gratuitous ARP - if arp.should_send_periodic_garp() { - let garp = arp.build_gratuitous_arp(); - self.send_frame_encrypted(conn, &garp, &mut send_buf, &mut encryption).await?; - arp.mark_garp_sent(); - debug!("Sent periodic GARP"); - } - } - } - } - - info!("VPN tunnel stopped"); - tun_reader.abort(); - Ok(()) - } - - /// Process an incoming frame for Windows (using Wintun). - #[cfg(target_os = "windows")] - #[inline] - fn process_frame_windows( - &self, - tun: &mut WintunDevice, - arp: &mut ArpHandler, - frame: &[u8], - our_ip: Ipv4Addr, - ) -> Result<()> { - if frame.len() < 14 { - return Ok(()); - } - - let dst_mac: [u8; 6] = frame[0..6].try_into().unwrap(); - if dst_mac != self.mac && dst_mac != BROADCAST_MAC { - return Ok(()); - } - - let ether_type = u16::from_be_bytes([frame[12], frame[13]]); - - match ether_type { - 0x0800 => { - // IPv4 - let ip_packet = &frame[14..]; - if ip_packet.len() >= 20 { - let dst_ip = - Ipv4Addr::new(ip_packet[16], ip_packet[17], ip_packet[18], ip_packet[19]); - - if dst_ip == our_ip || dst_ip.is_broadcast() || dst_ip.is_multicast() { - // Write directly to Wintun - let _ = tun.write(ip_packet); - } - } - } - 0x86DD => { - // IPv6 - let ip_packet = &frame[14..]; - let _ = tun.write(ip_packet); - } - 0x0806 => { - // ARP - debug!("Received ARP packet ({} bytes)", frame.len()); - if let Some(_reply) = arp.process_arp(frame) { - debug!("ARP reply queued"); - } - } - _ => {} - } - - Ok(()) - } - - /// Process an incoming frame with zero-copy TUN write (Unix only). - #[cfg(any(target_os = "macos", target_os = "linux"))] - #[inline] - #[allow(unused_variables)] // tun_buf is only used on macOS, not Linux - fn process_frame_zerocopy( - &self, - tun_fd: i32, - tun_buf: &mut [u8], - arp: &mut ArpHandler, - frame: &[u8], - our_ip: Ipv4Addr, - ) -> Result<()> { - if frame.len() < 14 { - return Ok(()); - } - - // Check destination MAC - let dst_mac: [u8; 6] = frame[0..6].try_into().unwrap(); - if dst_mac != self.mac && dst_mac != BROADCAST_MAC { - return Ok(()); // Not for us - } - - let ether_type = u16::from_be_bytes([frame[12], frame[13]]); - - match ether_type { - 0x0800 => { - // IPv4 - extract and write to TUN - let ip_packet = &frame[14..]; - if ip_packet.len() >= 20 { - let dst_ip = - Ipv4Addr::new(ip_packet[16], ip_packet[17], ip_packet[18], ip_packet[19]); - - if dst_ip == our_ip || dst_ip.is_broadcast() || dst_ip.is_multicast() { - // Write to TUN device - // macOS utun: needs 4-byte protocol header - // Linux tun: raw IP packet (IFF_NO_PI) - #[cfg(target_os = "macos")] - { - let total_len = 4 + ip_packet.len(); - if total_len <= tun_buf.len() { - // AF_INET = 2 in network byte order - tun_buf[0..4] - .copy_from_slice(&(libc::AF_INET as u32).to_be_bytes()); - tun_buf[4..total_len].copy_from_slice(ip_packet); - - unsafe { - libc::write( - tun_fd, - tun_buf.as_ptr() as *const libc::c_void, - total_len, - ); - } - } - } - #[cfg(target_os = "linux")] - { - // Linux: write raw IP packet directly - unsafe { - libc::write( - tun_fd, - ip_packet.as_ptr() as *const libc::c_void, - ip_packet.len(), - ); - } - } - } - } - } - 0x86DD => { - // IPv6 - let ip_packet = &frame[14..]; - #[cfg(target_os = "macos")] - { - let total_len = 4 + ip_packet.len(); - if total_len <= tun_buf.len() { - // AF_INET6 = 30 on macOS in network byte order - tun_buf[0..4].copy_from_slice(&(libc::AF_INET6 as u32).to_be_bytes()); - tun_buf[4..total_len].copy_from_slice(ip_packet); - - unsafe { - libc::write(tun_fd, tun_buf.as_ptr() as *const libc::c_void, total_len); - } - } - } - #[cfg(target_os = "linux")] - { - // Linux: write raw IP packet directly - unsafe { - libc::write( - tun_fd, - ip_packet.as_ptr() as *const libc::c_void, - ip_packet.len(), - ); - } - } - } - 0x0806 => { - // ARP - process to learn gateway MAC and respond to requests - debug!("Received ARP packet ({} bytes)", frame.len()); - if let Some(_reply) = arp.process_arp(frame) { - // Reply is built but we don't send it from here - // The main loop will check for pending replies - debug!("ARP reply queued"); - } - } - _ => {} - } - - Ok(()) - } - - // ========== Multi-Connection Support Methods ========== - - /// Perform DHCP through the tunnel using ConnectionManager. - async fn perform_dhcp_multi(&self, conn_mgr: &mut ConnectionManager) -> Result { - let mut dhcp = DhcpClient::new(self.mac); - // One codec per receive connection for stateful parsing - let num_conns = conn_mgr.connection_count(); - let mut codecs: Vec = (0..num_conns).map(|_| TunnelCodec::new()).collect(); - let mut buf = vec![0u8; 65536]; - let mut send_buf = vec![0u8; 2048]; - - let deadline = Instant::now() + Duration::from_secs(self.config.dhcp_timeout); - - // Get all receive-capable connection indices - let recv_conn_indices: Vec = conn_mgr - .all_connections() - .iter() - .enumerate() - .filter(|(_, c)| c.direction.can_recv()) - .map(|(i, _)| i) - .collect(); - - debug!( - connections = recv_conn_indices.len(), - "DHCP using receive connections" - ); - - // Note: DHCP happens before authentication, so no RC4 encryption yet - let mut no_encryption: Option = None; - - // Send DHCP DISCOVER - let discover = dhcp.build_discover(); - debug!(bytes = discover.len(), "Sending DHCP DISCOVER"); - self.send_frame_multi(conn_mgr, &discover, &mut send_buf, &mut no_encryption) - .await?; - - let mut last_send = Instant::now(); - let mut poll_idx = 0; - - // Use longer timeout per read - we want to actually wait for data - // With 1 connection, we can afford to wait longer - let per_conn_timeout_ms = if recv_conn_indices.len() <= 1 { - 100 - } else { - std::cmp::max(10, 100 / recv_conn_indices.len() as u64) - }; - - // Wait for OFFER/ACK - loop { - if Instant::now() > deadline { - return Err(Error::TimeoutMessage( - "DHCP timeout - no response received".into(), - )); - } - - // Retry DHCP if no response for 1 second (server may be slow) - if last_send.elapsed() > Duration::from_millis(1000) { - if dhcp.state() == DhcpState::DiscoverSent { - warn!("DHCP timeout, retrying DISCOVER"); - let discover = dhcp.build_discover(); - self.send_frame_multi(conn_mgr, &discover, &mut send_buf, &mut no_encryption) - .await?; - } else if dhcp.state() == DhcpState::RequestSent { - warn!("DHCP timeout, retrying REQUEST"); - if let Some(request) = dhcp.build_request() { - self.send_frame_multi( - conn_mgr, - &request, - &mut send_buf, - &mut no_encryption, - ) - .await?; - } - } - last_send = Instant::now(); - } - - if recv_conn_indices.is_empty() { - tokio::time::sleep(Duration::from_millis(10)).await; - continue; - } - - // Poll each connection with very short timeout - for _ in 0..recv_conn_indices.len() { - let conn_idx = recv_conn_indices[poll_idx % recv_conn_indices.len()]; - poll_idx += 1; - - let recv_conn = match conn_mgr.get_mut(conn_idx) { - Some(c) => c, - None => continue, - }; - - match timeout( - Duration::from_millis(per_conn_timeout_ms), - recv_conn.conn.read(&mut buf), - ) - .await - { - Ok(Ok(n)) if n > 0 => { - recv_conn.touch(); - recv_conn.bytes_received += n as u64; - - debug!("Received {} bytes from tunnel (connection {})", n, conn_idx); - - // Decode tunnel frames - let frames = codecs[conn_idx].feed(&buf[..n])?; - for frame in frames { - if frame.is_keepalive() { - debug!("Received keepalive frame"); - continue; - } - if let Some(packets) = frame.packets() { - for packet in packets { - // Check if packet is compressed and decompress if needed - let packet_data: Vec = if is_compressed(packet) { - match decompress(packet) { - Ok(decompressed) => { - debug!( - "Decompressed {} -> {} bytes", - packet.len(), - decompressed.len() - ); - decompressed - } - Err(e) => { - warn!("Decompression failed: {}", e); - continue; - } - } - } else { - packet.to_vec() - }; - - // Log packet details - if packet_data.len() >= 14 { - let ethertype = format!( - "0x{:02X}{:02X}", - packet_data[12], packet_data[13] - ); - debug!( - "Packet: {} bytes, ethertype={}", - packet_data.len(), - ethertype - ); - } - - // Check if this is a DHCP response (UDP port 68) - if self.is_dhcp_response(&packet_data) { - debug!(conn = conn_idx, "DHCP response received"); - if dhcp.process_response(&packet_data) { - // Got ACK - return Ok(dhcp.config().clone()); - } else if dhcp.state() == DhcpState::DiscoverSent { - // Got OFFER, send REQUEST - if let Some(request) = dhcp.build_request() { - debug!("Sending DHCP REQUEST"); - self.send_frame_multi( - conn_mgr, - &request, - &mut send_buf, - &mut no_encryption, - ) - .await?; - last_send = Instant::now(); - } - } - } - } - } - } - } - Ok(Ok(_)) => {} // Zero bytes - Ok(Err(e)) => { - warn!("Read error on connection {}: {}", conn_idx, e); - } - Err(_) => {} // Timeout, try next - } - } - } - } - - /// Send an Ethernet frame through the tunnel using ConnectionManager. - /// Supports optional RC4 encryption for defense-in-depth mode. - async fn send_frame_multi( - &self, - conn_mgr: &mut ConnectionManager, - frame: &[u8], - buf: &mut [u8], - encryption: &mut Option, - ) -> Result<()> { - // Compress if enabled - let data_to_send: std::borrow::Cow<[u8]> = if self.config.use_compress { - match compress(frame) { - Ok(compressed) => { - debug!("Compressed {} -> {} bytes", frame.len(), compressed.len()); - std::borrow::Cow::Owned(compressed) - } - Err(e) => { - warn!("Compression failed, sending uncompressed: {}", e); - std::borrow::Cow::Borrowed(frame) - } - } - } else { - std::borrow::Cow::Borrowed(frame) - }; - - // Encode as tunnel packet: [num_blocks=1][size][data] - let total_len = 4 + 4 + data_to_send.len(); - if buf.len() < total_len { - return Err(Error::Protocol("Send buffer too small".into())); - } - - buf[0..4].copy_from_slice(&1u32.to_be_bytes()); - buf[4..8].copy_from_slice(&(data_to_send.len() as u32).to_be_bytes()); - buf[8..8 + data_to_send.len()].copy_from_slice(&data_to_send); - - // Encrypt before sending if RC4 encryption is enabled - if let Some(ref mut enc) = encryption { - enc.encrypt(&mut buf[..total_len]); - } - - conn_mgr.write_all(&buf[..total_len]).await?; - Ok(()) - } - - /// Run the main data forwarding loop with multi-connection support. - /// - /// Uses ConcurrentReader for receive-only connections (half-connection mode), - /// and also handles bidirectional connections directly in the main loop. - #[cfg(any(target_os = "macos", target_os = "linux"))] - async fn run_data_loop_multi( - &self, - conn_mgr: &mut ConnectionManager, - tun: &mut impl TunAdapter, - dhcp_config: &DhcpConfig, - ) -> Result<()> { - let mut state = DataLoopState::new(self.mac); - let gateway = dhcp_config.gateway.unwrap_or(dhcp_config.ip); - state.configure(dhcp_config.ip, gateway); - - // Get the total number of connections before extraction - let total_conns = conn_mgr.connection_count(); - - // Extract receive-only connections for concurrent reading. - // Bidirectional connections stay in conn_mgr for both send AND receive. - let recv_conns = conn_mgr.take_recv_connections(); - let num_recv = recv_conns.len(); - let num_bidir = conn_mgr.connection_count(); // Bidirectional connections remaining - - // Create concurrent reader for receive-only connections (may be empty!) - let mut concurrent_reader = if !recv_conns.is_empty() { - Some(ConcurrentReader::new(recv_conns, 256)) - } else { - None - }; - - // One codec per original connection index for stateful frame parsing - let mut codecs: Vec = (0..total_conns).map(|_| TunnelCodec::new()).collect(); - - // Initialize RC4 encryption if enabled (defense-in-depth mode) - let mut encryption = self.create_encryption(); - if encryption.is_some() { - info!("RC4 defense-in-depth encryption active for multi-connection tunnel"); - } else { - debug!("No RC4 encryption (TLS-only mode for multi-connection tunnel)"); - } - - // Buffer for reading from bidirectional connections - let mut bidir_read_buf = vec![0u8; 8192]; - - let mut send_buf = vec![0u8; 4096]; - let mut decomp_buf = vec![0u8; 4096]; - let mut tun_write_buf = vec![0u8; 2048]; - - // Set up ARP handler - let mut arp = ArpHandler::new(self.mac); - arp.configure(dhcp_config.ip, gateway); - - // Send gratuitous ARP to announce our presence - let garp = arp.build_gratuitous_arp(); - self.send_frame_multi(conn_mgr, &garp, &mut send_buf, &mut encryption) - .await?; - debug!("Sent gratuitous ARP"); - - // Send ARP request for gateway - let gateway_arp = arp.build_gateway_request(); - self.send_frame_multi(conn_mgr, &gateway_arp, &mut send_buf, &mut encryption) - .await?; - debug!("Sent gateway ARP request"); - - let mut keepalive_interval = interval(Duration::from_secs(self.config.keepalive_interval)); - let mut last_activity = Instant::now(); - - // Zero-copy TUN reader using fixed buffer - let (tun_tx, mut tun_rx) = mpsc::channel::<(usize, [u8; 2048])>(256); - let tun_fd = tun.raw_fd(); - let running = self.running.clone(); - - // Spawn blocking TUN reader task - let tun_reader = tokio::task::spawn_blocking(move || { - let mut read_buf = [0u8; 2048]; - - while running.load(Ordering::SeqCst) { - let mut poll_fds = [libc::pollfd { - fd: tun_fd, - events: libc::POLLIN, - revents: 0, - }]; - - let poll_result = unsafe { - libc::poll(poll_fds.as_mut_ptr(), 1, 1) // 1ms timeout for low latency - }; - - if poll_result > 0 && (poll_fds[0].revents & libc::POLLIN) != 0 { - let n = unsafe { - libc::read( - tun_fd, - read_buf.as_mut_ptr() as *mut libc::c_void, - read_buf.len(), - ) - }; - - #[cfg(target_os = "macos")] - let min_len = 4; - #[cfg(target_os = "linux")] - let min_len = 1; - - if n > min_len as isize && tun_tx.blocking_send((n as usize, read_buf)).is_err() - { - break; - } - } - } - }); - - info!( - connections = total_conns, - recv_only = num_recv, - bidirectional = num_bidir, - "VPN tunnel active" - ); - - let our_ip = dhcp_config.ip; - let use_compress = self.config.use_compress; - let my_mac = self.mac; - - while self.running.load(Ordering::SeqCst) { - // Helper macro to process received VPN data - macro_rules! process_vpn_data { - ($conn_idx:expr, $data:expr) => {{ - match codecs.get_mut($conn_idx).map(|c| c.feed($data)) { - Some(Ok(frames)) => { - for frame in frames { - if frame.is_keepalive() { - debug!("Received keepalive on conn {}", $conn_idx); - continue; - } - - if let Some(packets) = frame.packets() { - for packet in packets { - let frame_data: &[u8] = if is_compressed(packet) { - match decompress_into(packet, &mut decomp_buf) { - Ok(len) => &decomp_buf[..len], - Err(_) => continue, - } - } else { - packet - }; - - if let Err(e) = self.process_frame_zerocopy( - tun_fd, - &mut tun_write_buf, - &mut arp, - frame_data, - our_ip, - ) { - error!("Process error: {}", e); - } - } - } - } - } - Some(Err(e)) => { - error!("Decode error on conn {}: {}", $conn_idx, e); - } - None => {} - } - - // Send any pending ARP replies - if let Some(reply) = arp.build_pending_reply() { - if let Err(e) = self - .send_frame_multi(conn_mgr, &reply, &mut send_buf, &mut encryption) - .await - { - error!("Failed to send ARP reply: {}", e); - } else { - debug!("Sent ARP reply"); - } - arp.take_pending_reply(); - } - - last_activity = Instant::now(); - }}; - } - - // Create futures for reading - // 1. Concurrent reader for receive-only connections (half-connection mode) - let concurrent_recv = async { - if let Some(ref mut reader) = concurrent_reader { - reader.recv().await - } else { - // No concurrent reader - pend forever - std::future::pending().await - } - }; - - // 2. Direct read from bidirectional connections in conn_mgr - let bidir_recv = async { - if num_bidir > 0 { - conn_mgr.read_any(&mut bidir_read_buf).await - } else { - // No bidirectional connections - pend forever - std::future::pending::>().await - } - }; - - tokio::select! { - // Biased: prioritize data paths over timers to minimize latency - biased; - - // Packet from TUN device (from local applications) - Some((len, tun_buf)) = tun_rx.recv() => { - #[cfg(target_os = "macos")] - let ip_packet = &tun_buf[4..len]; - #[cfg(target_os = "linux")] - let ip_packet = &tun_buf[..len]; - - if ip_packet.is_empty() { - continue; - } - - let gateway_mac = arp.gateway_mac_or_broadcast(); - - // Build tunnel frame - let eth_len = 14 + ip_packet.len(); - let total_len = 8 + eth_len; - - if total_len > send_buf.len() { - warn!("Packet too large: {}", ip_packet.len()); - continue; - } - - let ip_version = (ip_packet[0] >> 4) & 0x0F; - if ip_version != 4 && ip_version != 6 { - continue; - } - - if use_compress { - // Compression path - let eth_start = 8; - send_buf[eth_start..eth_start + 6].copy_from_slice(&gateway_mac); - send_buf[eth_start + 6..eth_start + 12].copy_from_slice(&my_mac); - if ip_version == 4 { - send_buf[eth_start + 12] = 0x08; - send_buf[eth_start + 13] = 0x00; - } else { - send_buf[eth_start + 12] = 0x86; - send_buf[eth_start + 13] = 0xDD; - } - send_buf[eth_start + 14..eth_start + 14 + ip_packet.len()] - .copy_from_slice(ip_packet); - - let eth_frame = &send_buf[eth_start..eth_start + eth_len]; - - match compress(eth_frame) { - Ok(compressed) => { - let comp_total = 8 + compressed.len(); - if comp_total <= send_buf.len() { - send_buf[0..4].copy_from_slice(&1u32.to_be_bytes()); - send_buf[4..8].copy_from_slice(&(compressed.len() as u32).to_be_bytes()); - send_buf[8..8 + compressed.len()].copy_from_slice(&compressed); - // Encrypt before sending if RC4 is enabled - if let Some(ref mut enc) = encryption { - enc.encrypt(&mut send_buf[..comp_total]); - } - conn_mgr.write_all(&send_buf[..comp_total]).await?; - } - } - Err(e) => { - warn!("Compression failed: {}", e); - send_buf[0..4].copy_from_slice(&1u32.to_be_bytes()); - send_buf[4..8].copy_from_slice(&(eth_len as u32).to_be_bytes()); - // Encrypt before sending if RC4 is enabled - if let Some(ref mut enc) = encryption { - enc.encrypt(&mut send_buf[..total_len]); - } - conn_mgr.write_all(&send_buf[..total_len]).await?; - } - } - } else { - // Uncompressed path - send_buf[0..4].copy_from_slice(&1u32.to_be_bytes()); - send_buf[4..8].copy_from_slice(&(eth_len as u32).to_be_bytes()); - send_buf[8..14].copy_from_slice(&gateway_mac); - send_buf[14..20].copy_from_slice(&my_mac); - if ip_version == 4 { - send_buf[20] = 0x08; - send_buf[21] = 0x00; - } else { - send_buf[20] = 0x86; - send_buf[21] = 0xDD; - } - send_buf[22..22 + ip_packet.len()].copy_from_slice(ip_packet); - - // Encrypt before sending if RC4 is enabled - if let Some(ref mut enc) = encryption { - enc.encrypt(&mut send_buf[..total_len]); - } - conn_mgr.write_all(&send_buf[..total_len]).await?; - } - - last_activity = Instant::now(); - } - - // Data from receive-only connections via ConcurrentReader - Some(packet) = concurrent_recv => { - let conn_idx = packet.conn_index; - // Decrypt received data if RC4 is enabled (need to copy since Bytes is immutable) - let data: Vec = if let Some(ref mut enc) = encryption { - let mut decrypted = packet.data.to_vec(); - enc.decrypt(&mut decrypted); - decrypted - } else { - packet.data.to_vec() - }; - process_vpn_data!(conn_idx, &data[..]); - } - - // Data from bidirectional connections (direct read) - result = bidir_recv => { - if let Ok((conn_idx, n)) = result { - if n > 0 { - // Decrypt received data if RC4 is enabled - if let Some(ref mut enc) = encryption { - enc.decrypt(&mut bidir_read_buf[..n]); - } - let data = &bidir_read_buf[..n]; - process_vpn_data!(conn_idx, data); - } - } - } - - // Keepalive timer - _ = keepalive_interval.tick() => { - if last_activity.elapsed() > Duration::from_secs(3) { - let keepalive = TunnelCodec::encode_keepalive_direct( - 32, - &mut send_buf, - ); - if let Some(ka) = keepalive { - // Note: Keepalive may need encryption too if RC4 is enabled - // For now, the tunnel codec produces keepalive in buffer - let ka_len = ka.len(); - if let Some(ref mut enc) = encryption { - enc.encrypt(&mut send_buf[..ka_len]); - } - conn_mgr.write_all(&send_buf[..ka_len]).await?; - debug!("Sent keepalive"); - } - } - - if arp.should_send_periodic_garp() { - let garp = arp.build_gratuitous_arp(); - self.send_frame_multi(conn_mgr, &garp, &mut send_buf, &mut encryption).await?; - arp.mark_garp_sent(); - debug!("Sent periodic GARP"); - } - } - } - } - - info!("VPN tunnel stopped"); - - // Cleanup - if let Some(ref mut reader) = concurrent_reader { - reader.shutdown(); - let recv_stats = reader.bytes_received(); - let total_recv: u64 = recv_stats.iter().map(|(_, b)| b).sum(); - debug!( - bytes = total_recv, - connections = recv_stats.len(), - "Concurrent reader shutdown" - ); - } - tun_reader.abort(); - - Ok(()) - } - - /// Windows-specific multi-connection data loop. - /// Note: On Windows, this falls back to single-connection behavior since - /// the Wintun API doesn't support the same zero-copy optimizations. - #[cfg(target_os = "windows")] - async fn run_data_loop_multi( - &self, - conn_mgr: &mut ConnectionManager, - tun: &mut WintunDevice, - dhcp_config: &DhcpConfig, - ) -> Result<()> { - let mut state = DataLoopState::new(self.mac); - let gateway = dhcp_config.gateway.unwrap_or(dhcp_config.ip); - state.configure(dhcp_config.ip, gateway); - - // Get the total number of connections - let total_conns = conn_mgr.connection_count(); - let recv_conns = conn_mgr.take_recv_connections(); - let num_recv = recv_conns.len(); - let num_bidir = conn_mgr.connection_count(); - - let mut concurrent_reader = if !recv_conns.is_empty() { - Some(ConcurrentReader::new(recv_conns, 256)) - } else { - None - }; - - let mut codecs: Vec = (0..total_conns).map(|_| TunnelCodec::new()).collect(); - let mut bidir_read_buf = vec![0u8; 8192]; - let mut send_buf = vec![0u8; 4096]; - let mut decomp_buf = vec![0u8; 4096]; - - // Create encryption state for RC4 if enabled - let mut encryption = self.create_encryption(); - - let mut arp = ArpHandler::new(self.mac); - arp.configure(dhcp_config.ip, gateway); - - let garp = arp.build_gratuitous_arp(); - self.send_frame_multi(conn_mgr, &garp, &mut send_buf, &mut encryption) - .await?; - debug!("Sent gratuitous ARP"); - - let gateway_arp = arp.build_gateway_request(); - self.send_frame_multi(conn_mgr, &gateway_arp, &mut send_buf, &mut encryption) - .await?; - debug!("Sent gateway ARP request"); - - let mut keepalive_interval = interval(Duration::from_secs(self.config.keepalive_interval)); - let mut last_activity = Instant::now(); - - let (tun_tx, mut tun_rx) = mpsc::channel::>(256); - let session = tun.session(); - let running = self.running.clone(); - - let tun_reader = tokio::task::spawn_blocking(move || { - while running.load(Ordering::SeqCst) { - match session.receive_blocking() { - Ok(packet) => { - let bytes = packet.bytes().to_vec(); - if tun_tx.blocking_send(bytes).is_err() { - break; - } - } - Err(_) => { - std::thread::sleep(Duration::from_millis(1)); - } - } - } - }); - - info!( - connections = total_conns, - recv_only = num_recv, - bidirectional = num_bidir, - "VPN tunnel active (Windows)" - ); - - let our_ip = dhcp_config.ip; - let use_compress = self.config.use_compress; - let my_mac = self.mac; - - while self.running.load(Ordering::SeqCst) { - // Helper macro to process received VPN data - macro_rules! process_vpn_data { - ($conn_idx:expr, $data:expr) => {{ - match codecs.get_mut($conn_idx).map(|c| c.feed($data)) { - Some(Ok(frames)) => { - for frame in frames { - if frame.is_keepalive() { - debug!("Received keepalive on conn {}", $conn_idx); - continue; - } - - if let Some(packets) = frame.packets() { - for packet in packets { - let frame_data: &[u8] = if is_compressed(packet) { - match decompress_into(packet, &mut decomp_buf) { - Ok(len) => &decomp_buf[..len], - Err(_) => continue, - } - } else { - packet - }; - - if let Err(e) = self.process_frame_windows( - tun, &mut arp, frame_data, our_ip, - ) { - error!("Process error: {}", e); - } - } - } - } - } - Some(Err(e)) => { - error!("Decode error on conn {}: {}", $conn_idx, e); - } - None => {} - } - - if let Some(reply) = arp.build_pending_reply() { - if let Err(e) = self - .send_frame_multi(conn_mgr, &reply, &mut send_buf, &mut encryption) - .await - { - error!("Failed to send ARP reply: {}", e); - } else { - debug!("Sent ARP reply"); - } - arp.take_pending_reply(); - } - - last_activity = Instant::now(); - }}; - } - - let concurrent_recv = async { - if let Some(ref mut reader) = concurrent_reader { - reader.recv().await - } else { - std::future::pending().await - } - }; - - let bidir_recv = async { - if num_bidir > 0 { - conn_mgr.read_any(&mut bidir_read_buf).await - } else { - std::future::pending::>().await - } - }; - - tokio::select! { - biased; - - Some(ip_packet) = tun_rx.recv() => { - if ip_packet.is_empty() { - continue; - } - - let gateway_mac = arp.gateway_mac_or_broadcast(); - let eth_len = 14 + ip_packet.len(); - let total_len = 8 + eth_len; - - if total_len > send_buf.len() { - warn!("Packet too large: {}", ip_packet.len()); - continue; - } - - let ip_version = (ip_packet[0] >> 4) & 0x0F; - if ip_version != 4 && ip_version != 6 { - continue; - } - - if use_compress { - let eth_start = 8; - send_buf[eth_start..eth_start + 6].copy_from_slice(&gateway_mac); - send_buf[eth_start + 6..eth_start + 12].copy_from_slice(&my_mac); - if ip_version == 4 { - send_buf[eth_start + 12] = 0x08; - send_buf[eth_start + 13] = 0x00; - } else { - send_buf[eth_start + 12] = 0x86; - send_buf[eth_start + 13] = 0xDD; - } - send_buf[eth_start + 14..eth_start + 14 + ip_packet.len()] - .copy_from_slice(&ip_packet); - - let eth_frame = &send_buf[eth_start..eth_start + eth_len]; - - match compress(eth_frame) { - Ok(compressed) => { - let comp_total = 8 + compressed.len(); - if comp_total <= send_buf.len() { - send_buf[0..4].copy_from_slice(&1u32.to_be_bytes()); - send_buf[4..8].copy_from_slice(&(compressed.len() as u32).to_be_bytes()); - send_buf[8..8 + compressed.len()].copy_from_slice(&compressed); - conn_mgr.write_all(&send_buf[..comp_total]).await?; - } - } - Err(e) => { - warn!("Compression failed: {}", e); - send_buf[0..4].copy_from_slice(&1u32.to_be_bytes()); - send_buf[4..8].copy_from_slice(&(eth_len as u32).to_be_bytes()); - conn_mgr.write_all(&send_buf[..total_len]).await?; - } - } - } else { - send_buf[0..4].copy_from_slice(&1u32.to_be_bytes()); - send_buf[4..8].copy_from_slice(&(eth_len as u32).to_be_bytes()); - send_buf[8..14].copy_from_slice(&gateway_mac); - send_buf[14..20].copy_from_slice(&my_mac); - if ip_version == 4 { - send_buf[20] = 0x08; - send_buf[21] = 0x00; - } else { - send_buf[20] = 0x86; - send_buf[21] = 0xDD; - } - send_buf[22..22 + ip_packet.len()].copy_from_slice(&ip_packet); - - conn_mgr.write_all(&send_buf[..total_len]).await?; - } - - last_activity = Instant::now(); - } - - Some(packet) = concurrent_recv => { - let conn_idx = packet.conn_index; - let data = &packet.data[..]; - process_vpn_data!(conn_idx, data); - } - - result = bidir_recv => { - if let Ok((conn_idx, n)) = result { - if n > 0 { - let data = &bidir_read_buf[..n]; - process_vpn_data!(conn_idx, data); - } - } - } - - _ = keepalive_interval.tick() => { - if last_activity.elapsed() > Duration::from_secs(3) { - let keepalive = TunnelCodec::encode_keepalive_direct(32, &mut send_buf); - if let Some(ka) = keepalive { - conn_mgr.write_all(ka).await?; - debug!("Sent keepalive"); - } - } - - if arp.should_send_periodic_garp() { - let garp = arp.build_gratuitous_arp(); - self.send_frame_multi(conn_mgr, &garp, &mut send_buf, &mut encryption).await?; - arp.mark_garp_sent(); - debug!("Sent periodic GARP"); - } - } - } - } - - info!("VPN tunnel stopped"); - - if let Some(ref mut reader) = concurrent_reader { - reader.shutdown(); - let recv_stats = reader.bytes_received(); - let total_recv: u64 = recv_stats.iter().map(|(_, b)| b).sum(); - debug!( - bytes = total_recv, - connections = recv_stats.len(), - "Concurrent reader shutdown" - ); - } - tun_reader.abort(); - - Ok(()) - } } diff --git a/src/tunnel/single_conn.rs b/src/tunnel/single_conn.rs new file mode 100644 index 0000000..ba225c6 --- /dev/null +++ b/src/tunnel/single_conn.rs @@ -0,0 +1,695 @@ +//! Single-connection data loop for tunnel runner. +//! +//! This module contains the main packet forwarding loop for single TCP connection mode. +//! Supports macOS, Linux, and Windows platforms with platform-specific TUN handling. + +use std::net::Ipv4Addr; +use std::sync::atomic::Ordering; +use std::time::{Duration, Instant}; + +use tokio::sync::mpsc; +use tokio::time::interval; +use tracing::{debug, error, info, warn}; + +use crate::adapter::TunAdapter; +#[cfg(target_os = "windows")] +use crate::adapter::WintunDevice; +use crate::client::VpnConnection; +use crate::error::Result; +use crate::packet::{ArpHandler, DhcpConfig, BROADCAST_MAC}; +use crate::protocol::compress_into; +use crate::protocol::{decompress_into, is_compressed, TunnelCodec}; + +use super::packet_processor::{ + init_arp, send_keepalive_if_needed, send_pending_arp_reply, send_periodic_garp_if_needed, +}; +use super::DataLoopState; +use super::TunnelRunner; + +impl TunnelRunner { + /// Run the main data forwarding loop. + /// + /// Zero-copy optimized path: + /// - Outbound: TUN read β†’ inline Ethernet wrap β†’ direct send + /// - Inbound: Network read β†’ direct TUN write (skip Ethernet header) + #[cfg(any(target_os = "macos", target_os = "linux"))] + pub(super) async fn run_data_loop( + &self, + conn: &mut VpnConnection, + tun: &mut impl TunAdapter, + dhcp_config: &DhcpConfig, + ) -> Result<()> { + self.run_data_loop_unix(conn, tun, dhcp_config).await + } + + #[cfg(target_os = "windows")] + pub(super) async fn run_data_loop( + &self, + conn: &mut VpnConnection, + tun: &mut WintunDevice, + dhcp_config: &DhcpConfig, + ) -> Result<()> { + self.run_data_loop_windows(conn, tun, dhcp_config).await + } + + /// Unix-specific data loop implementation using libc poll/read. + #[cfg(any(target_os = "macos", target_os = "linux"))] + async fn run_data_loop_unix( + &self, + conn: &mut VpnConnection, + tun: &mut impl TunAdapter, + dhcp_config: &DhcpConfig, + ) -> Result<()> { + let mut state = DataLoopState::new(self.mac); + let gateway = dhcp_config.gateway.unwrap_or(dhcp_config.ip); + state.configure(dhcp_config.ip, gateway); + + let mut codec = TunnelCodec::new(); + + // Initialize RC4 encryption if enabled + let mut encryption = self.create_encryption(); + if encryption.is_some() { + info!("RC4 encryption active for tunnel data"); + } + + // Pre-allocated buffers - sized for maximum packets + let mut net_buf = vec![0u8; 65536]; + let mut send_buf = vec![0u8; 4096]; + let mut decomp_buf = vec![0u8; 4096]; + let mut comp_buf = vec![0u8; 4096]; + let mut tun_write_buf = vec![0u8; 2048]; + + // Set up ARP handler and send initial ARP packets + let mut arp = ArpHandler::new(self.mac); + init_arp( + conn, + &mut arp, + dhcp_config.ip, + gateway, + &mut send_buf, + &mut encryption, + ) + .await?; + + let mut keepalive_interval = interval(Duration::from_secs(self.config.keepalive_interval)); + let mut last_activity = Instant::now(); + + // Zero-copy TUN reader using fixed buffer + let (tun_tx, mut tun_rx) = mpsc::channel::<(usize, [u8; 2048])>(256); + let tun_fd = tun.raw_fd(); + let running = self.running.clone(); + + // Spawn blocking TUN reader task + let tun_reader = tokio::task::spawn_blocking(move || { + let mut read_buf = [0u8; 2048]; + + while running.load(Ordering::SeqCst) { + let mut poll_fds = [libc::pollfd { + fd: tun_fd, + events: libc::POLLIN, + revents: 0, + }]; + + let poll_result = unsafe { libc::poll(poll_fds.as_mut_ptr(), 1, 1) }; + + if poll_result > 0 && (poll_fds[0].revents & libc::POLLIN) != 0 { + let n = unsafe { + libc::read( + tun_fd, + read_buf.as_mut_ptr() as *mut libc::c_void, + read_buf.len(), + ) + }; + + #[cfg(target_os = "macos")] + let min_len = 4; + #[cfg(target_os = "linux")] + let min_len = 1; + + if n > min_len as isize && tun_tx.blocking_send((n as usize, read_buf)).is_err() + { + break; + } + } + } + }); + + info!("VPN tunnel active"); + + let our_ip = dhcp_config.ip; + let use_compress = self.config.use_compress; + let my_mac = self.mac; + + while self.running.load(Ordering::SeqCst) { + tokio::select! { + biased; + + Some((len, tun_buf)) = tun_rx.recv() => { + #[cfg(target_os = "macos")] + let ip_packet = &tun_buf[4..len]; + #[cfg(target_os = "linux")] + let ip_packet = &tun_buf[..len]; + + if ip_packet.is_empty() { + continue; + } + + let gateway_mac = arp.gateway_mac_or_broadcast(); + let eth_len = 14 + ip_packet.len(); + let total_len = 8 + eth_len; + + if total_len > send_buf.len() { + warn!("Packet too large: {}", ip_packet.len()); + continue; + } + + let ip_version = (ip_packet[0] >> 4) & 0x0F; + if ip_version != 4 && ip_version != 6 { + continue; + } + + if use_compress { + let eth_start = 8; + send_buf[eth_start..eth_start + 6].copy_from_slice(&gateway_mac); + send_buf[eth_start + 6..eth_start + 12].copy_from_slice(&my_mac); + if ip_version == 4 { + send_buf[eth_start + 12] = 0x08; + send_buf[eth_start + 13] = 0x00; + } else { + send_buf[eth_start + 12] = 0x86; + send_buf[eth_start + 13] = 0xDD; + } + send_buf[eth_start + 14..eth_start + 14 + ip_packet.len()] + .copy_from_slice(ip_packet); + + let eth_frame = &send_buf[eth_start..eth_start + eth_len]; + + match compress_into(eth_frame, &mut comp_buf) { + Ok(comp_len) => { + let comp_total = 8 + comp_len; + if comp_total <= send_buf.len() { + send_buf[0..4].copy_from_slice(&1u32.to_be_bytes()); + send_buf[4..8].copy_from_slice(&(comp_len as u32).to_be_bytes()); + send_buf[8..8 + comp_len].copy_from_slice(&comp_buf[..comp_len]); + if let Some(ref mut enc) = encryption { + enc.encrypt(&mut send_buf[..comp_total]); + } + conn.write_all(&send_buf[..comp_total]).await?; + } + } + Err(e) => { + warn!("Compression failed: {}", e); + send_buf[0..4].copy_from_slice(&1u32.to_be_bytes()); + send_buf[4..8].copy_from_slice(&(eth_len as u32).to_be_bytes()); + if let Some(ref mut enc) = encryption { + enc.encrypt(&mut send_buf[..total_len]); + } + conn.write_all(&send_buf[..total_len]).await?; + } + } + } else { + send_buf[0..4].copy_from_slice(&1u32.to_be_bytes()); + send_buf[4..8].copy_from_slice(&(eth_len as u32).to_be_bytes()); + send_buf[8..14].copy_from_slice(&gateway_mac); + send_buf[14..20].copy_from_slice(&my_mac); + if ip_version == 4 { + send_buf[20] = 0x08; + send_buf[21] = 0x00; + } else { + send_buf[20] = 0x86; + send_buf[21] = 0xDD; + } + send_buf[22..22 + ip_packet.len()].copy_from_slice(ip_packet); + + if let Some(ref mut enc) = encryption { + enc.encrypt(&mut send_buf[..total_len]); + } + conn.write_all(&send_buf[..total_len]).await?; + } + + last_activity = Instant::now(); + } + + result = conn.read(&mut net_buf) => { + match result { + Ok(n) if n > 0 => { + if let Some(ref mut enc) = encryption { + enc.decrypt(&mut net_buf[..n]); + } + + match codec.feed(&net_buf[..n]) { + Ok(frames) => { + for frame in frames { + if frame.is_keepalive() { + debug!("Received keepalive"); + continue; + } + + if let Some(packets) = frame.packets() { + for packet in packets { + let frame_data: &[u8] = if is_compressed(packet) { + match decompress_into(packet, &mut decomp_buf) { + Ok(len) => &decomp_buf[..len], + Err(_) => continue, + } + } else { + packet + }; + + if let Err(e) = self.process_frame_zerocopy( + tun_fd, + &mut tun_write_buf, + &mut arp, + frame_data, + our_ip, + ) { + error!("Process error: {}", e); + } + } + } + } + } + Err(e) => { + error!("Decode error: {}", e); + } + } + + send_pending_arp_reply(conn, &mut arp, &mut send_buf, &mut encryption).await?; + last_activity = Instant::now(); + } + Ok(_) => { + warn!("Server closed connection"); + break; + } + Err(e) => { + error!(error = %e, "Network read failed"); + break; + } + } + } + + _ = keepalive_interval.tick() => { + send_keepalive_if_needed(conn, last_activity, &mut send_buf, &mut encryption).await?; + send_periodic_garp_if_needed(conn, &mut arp, &mut send_buf, &mut encryption).await?; + } + } + } + + info!("VPN tunnel stopped"); + tun_reader.abort(); + Ok(()) + } + + /// Windows-specific data loop implementation using Wintun. + #[cfg(target_os = "windows")] + async fn run_data_loop_windows( + &self, + conn: &mut VpnConnection, + tun: &mut WintunDevice, + dhcp_config: &DhcpConfig, + ) -> Result<()> { + let mut state = DataLoopState::new(self.mac); + let gateway = dhcp_config.gateway.unwrap_or(dhcp_config.ip); + state.configure(dhcp_config.ip, gateway); + + let mut codec = TunnelCodec::new(); + + let mut encryption = self.create_encryption(); + if encryption.is_some() { + info!("RC4 encryption active for tunnel data (Windows)"); + } + + let mut net_buf = vec![0u8; 65536]; + let mut send_buf = vec![0u8; 4096]; + let mut decomp_buf = vec![0u8; 4096]; + let mut comp_buf = vec![0u8; 4096]; // Pre-allocated buffer for compression + + let mut arp = ArpHandler::new(self.mac); + init_arp( + conn, + &mut arp, + dhcp_config.ip, + gateway, + &mut send_buf, + &mut encryption, + ) + .await?; + + let mut keepalive_interval = interval(Duration::from_secs(self.config.keepalive_interval)); + let mut last_activity = Instant::now(); + + let (tun_tx, mut tun_rx) = mpsc::channel::>(256); + let session = tun.session(); + let running = self.running.clone(); + + let tun_reader = tokio::task::spawn_blocking(move || { + let mut idle_count = 0u32; + + while running.load(Ordering::SeqCst) { + match session.try_receive() { + Ok(Some(packet)) => { + let bytes = packet.bytes().to_vec(); + if tun_tx.blocking_send(bytes).is_err() { + break; + } + idle_count = 0; + } + Ok(None) => { + idle_count += 1; + if idle_count > 100 { + match session.receive_blocking() { + Ok(packet) => { + let bytes = packet.bytes().to_vec(); + if tun_tx.blocking_send(bytes).is_err() { + break; + } + } + Err(_) => { + std::thread::sleep(Duration::from_micros(100)); + } + } + idle_count = 0; + } else { + std::thread::yield_now(); + } + } + Err(_) => { + std::thread::sleep(Duration::from_micros(100)); + idle_count = 0; + } + } + } + }); + + info!("VPN tunnel active (Windows)"); + + let our_ip = dhcp_config.ip; + let use_compress = self.config.use_compress; + let my_mac = self.mac; + + while self.running.load(Ordering::SeqCst) { + tokio::select! { + biased; + + Some(ip_packet) = tun_rx.recv() => { + if ip_packet.is_empty() { + continue; + } + + let gateway_mac = arp.gateway_mac_or_broadcast(); + let eth_len = 14 + ip_packet.len(); + let total_len = 8 + eth_len; + + if total_len > send_buf.len() { + warn!("Packet too large: {}", ip_packet.len()); + continue; + } + + let ip_version = (ip_packet[0] >> 4) & 0x0F; + if ip_version != 4 && ip_version != 6 { + continue; + } + + if use_compress { + let eth_start = 8; + send_buf[eth_start..eth_start + 6].copy_from_slice(&gateway_mac); + send_buf[eth_start + 6..eth_start + 12].copy_from_slice(&my_mac); + if ip_version == 4 { + send_buf[eth_start + 12] = 0x08; + send_buf[eth_start + 13] = 0x00; + } else { + send_buf[eth_start + 12] = 0x86; + send_buf[eth_start + 13] = 0xDD; + } + send_buf[eth_start + 14..eth_start + 14 + ip_packet.len()] + .copy_from_slice(&ip_packet); + + let eth_frame = &send_buf[eth_start..eth_start + eth_len]; + + match compress_into(eth_frame, &mut comp_buf) { + Ok(comp_len) => { + let comp_total = 8 + comp_len; + if comp_total <= send_buf.len() { + send_buf[0..4].copy_from_slice(&1u32.to_be_bytes()); + send_buf[4..8] + .copy_from_slice(&(comp_len as u32).to_be_bytes()); + send_buf[8..8 + comp_len].copy_from_slice(&comp_buf[..comp_len]); + if let Some(ref mut enc) = encryption { + enc.encrypt(&mut send_buf[..comp_total]); + } + conn.write_all(&send_buf[..comp_total]).await?; + } + } + Err(e) => { + warn!("Compression failed: {}", e); + send_buf[0..4].copy_from_slice(&1u32.to_be_bytes()); + send_buf[4..8].copy_from_slice(&(eth_len as u32).to_be_bytes()); + if let Some(ref mut enc) = encryption { + enc.encrypt(&mut send_buf[..total_len]); + } + conn.write_all(&send_buf[..total_len]).await?; + } + } + } else { + send_buf[0..4].copy_from_slice(&1u32.to_be_bytes()); + send_buf[4..8].copy_from_slice(&(eth_len as u32).to_be_bytes()); + send_buf[8..14].copy_from_slice(&gateway_mac); + send_buf[14..20].copy_from_slice(&my_mac); + if ip_version == 4 { + send_buf[20] = 0x08; + send_buf[21] = 0x00; + } else { + send_buf[20] = 0x86; + send_buf[21] = 0xDD; + } + send_buf[22..22 + ip_packet.len()].copy_from_slice(&ip_packet); + + if let Some(ref mut enc) = encryption { + enc.encrypt(&mut send_buf[..total_len]); + } + conn.write_all(&send_buf[..total_len]).await?; + } + + last_activity = Instant::now(); + } + + result = conn.read(&mut net_buf) => { + match result { + Ok(n) if n > 0 => { + if let Some(ref mut enc) = encryption { + enc.decrypt(&mut net_buf[..n]); + } + + match codec.feed(&net_buf[..n]) { + Ok(frames) => { + for frame in frames { + if frame.is_keepalive() { + debug!("Received keepalive"); + continue; + } + + if let Some(packets) = frame.packets() { + for packet in packets { + let frame_data: &[u8] = if is_compressed(packet) { + match decompress_into(packet, &mut decomp_buf) { + Ok(len) => &decomp_buf[..len], + Err(_) => continue, + } + } else { + packet + }; + + if let Err(e) = self.process_frame_windows( + tun, + &mut arp, + frame_data, + our_ip, + ) { + error!("Process error: {}", e); + } + } + } + } + } + Err(e) => { + error!("Decode error: {}", e); + } + } + + send_pending_arp_reply(conn, &mut arp, &mut send_buf, &mut encryption).await?; + last_activity = Instant::now(); + } + Ok(_) => { + warn!("Server closed connection"); + break; + } + Err(e) => { + error!(error = %e, "Network read failed"); + break; + } + } + } + + _ = keepalive_interval.tick() => { + send_keepalive_if_needed(conn, last_activity, &mut send_buf, &mut encryption).await?; + send_periodic_garp_if_needed(conn, &mut arp, &mut send_buf, &mut encryption).await?; + } + } + } + + info!("VPN tunnel stopped"); + tun_reader.abort(); + Ok(()) + } + + /// Process an incoming frame for Windows (using Wintun). + #[cfg(target_os = "windows")] + #[inline] + pub(super) fn process_frame_windows( + &self, + tun: &mut WintunDevice, + arp: &mut ArpHandler, + frame: &[u8], + our_ip: Ipv4Addr, + ) -> Result<()> { + if frame.len() < 14 { + return Ok(()); + } + + let dst_mac: [u8; 6] = frame[0..6].try_into().unwrap(); + if dst_mac != self.mac && dst_mac != BROADCAST_MAC { + return Ok(()); + } + + let ether_type = u16::from_be_bytes([frame[12], frame[13]]); + + match ether_type { + 0x0800 => { + // IPv4 + let ip_packet = &frame[14..]; + if ip_packet.len() >= 20 { + let dst_ip = + Ipv4Addr::new(ip_packet[16], ip_packet[17], ip_packet[18], ip_packet[19]); + + if dst_ip == our_ip || dst_ip.is_broadcast() || dst_ip.is_multicast() { + let _ = tun.write(ip_packet); + } + } + } + 0x86DD => { + // IPv6 + let ip_packet = &frame[14..]; + let _ = tun.write(ip_packet); + } + 0x0806 => { + // ARP + debug!("Received ARP packet ({} bytes)", frame.len()); + if let Some(_reply) = arp.process_arp(frame) { + debug!("ARP reply queued"); + } + } + _ => {} + } + + Ok(()) + } + + /// Process an incoming frame with zero-copy TUN write (Unix only). + #[cfg(any(target_os = "macos", target_os = "linux"))] + #[inline] + #[allow(unused_variables)] + pub(super) fn process_frame_zerocopy( + &self, + tun_fd: i32, + tun_buf: &mut [u8], + arp: &mut ArpHandler, + frame: &[u8], + our_ip: Ipv4Addr, + ) -> Result<()> { + if frame.len() < 14 { + return Ok(()); + } + + let dst_mac: [u8; 6] = frame[0..6].try_into().unwrap(); + if dst_mac != self.mac && dst_mac != BROADCAST_MAC { + return Ok(()); + } + + let ether_type = u16::from_be_bytes([frame[12], frame[13]]); + + match ether_type { + 0x0800 => { + // IPv4 + let ip_packet = &frame[14..]; + if ip_packet.len() >= 20 { + let dst_ip = + Ipv4Addr::new(ip_packet[16], ip_packet[17], ip_packet[18], ip_packet[19]); + + if dst_ip == our_ip || dst_ip.is_broadcast() || dst_ip.is_multicast() { + #[cfg(target_os = "macos")] + { + let total_len = 4 + ip_packet.len(); + if total_len <= tun_buf.len() { + tun_buf[0..4] + .copy_from_slice(&(libc::AF_INET as u32).to_be_bytes()); + tun_buf[4..total_len].copy_from_slice(ip_packet); + unsafe { + libc::write( + tun_fd, + tun_buf.as_ptr() as *const libc::c_void, + total_len, + ); + } + } + } + #[cfg(target_os = "linux")] + { + unsafe { + libc::write( + tun_fd, + ip_packet.as_ptr() as *const libc::c_void, + ip_packet.len(), + ); + } + } + } + } + } + 0x86DD => { + // IPv6 + let ip_packet = &frame[14..]; + #[cfg(target_os = "macos")] + { + let total_len = 4 + ip_packet.len(); + if total_len <= tun_buf.len() { + tun_buf[0..4].copy_from_slice(&(libc::AF_INET6 as u32).to_be_bytes()); + tun_buf[4..total_len].copy_from_slice(ip_packet); + unsafe { + libc::write(tun_fd, tun_buf.as_ptr() as *const libc::c_void, total_len); + } + } + } + #[cfg(target_os = "linux")] + { + unsafe { + libc::write( + tun_fd, + ip_packet.as_ptr() as *const libc::c_void, + ip_packet.len(), + ); + } + } + } + 0x0806 => { + // ARP + debug!("Received ARP packet ({} bytes)", frame.len()); + if let Some(_reply) = arp.process_arp(frame) { + debug!("ARP reply queued"); + } + } + _ => {} + } + + Ok(()) + } +}