18 KiB
AGENTS.md - VPN Tray Manager Development Guide
Project Overview
Xray-VPN-Manager is a Windows-only system tray application for managing multiple Xray-core VPN servers from a single subscription URL. Built in Rust with native Windows UI (no web/electron), it provides a lightweight way to enable/disable VPN servers with custom proxy ports.
Platform: Windows 10/11 only (uses Windows API extensively)
Language: Rust (edition 2024)
Architecture: Native Win32 GUI + Tokio async runtime for process management
Essential Commands
Build & Run
# Development build
cargo build
# Release build (optimized)
cargo build --release
# Run (shows console output - #![windows_subsystem = "windows"] commented out in main.rs:1)
cargo run
# Release run (production mode)
cargo run --release
Testing
No test suite currently exists. Manual testing required:
- Run app, check tray icon appears
- Open Settings, enter subscription URL and xray binary path
- Click Update to fetch servers
- Enable servers, set ports/proxy types
- Click Save and verify servers start
- Check tray menu shows running servers
Linting & Formatting
# Check code (standard Rust linting)
cargo check
# Format code
cargo fmt
# Run Clippy for additional lints
cargo clippy
Project Structure
VPN-Manager/
├── src/
│ ├── main.rs # Entry point, tray icon, Windows message loop
│ ├── config.rs # Config persistence (JSON in %APPDATA%)
│ ├── xray_manager.rs # Xray process lifecycle management
│ ├── vpn/
│ │ └── mod.rs # Subscription parsing, URI handling
│ └── ui/
│ ├── mod.rs # UI module exports
│ ├── tray.rs # Tray icon creation, menu rendering
│ └── settings_window.rs # Native Win32 settings window (1200+ LOC)
├── Cargo.toml # Dependencies, Windows features
├── build.rs # Embeds app.manifest via app.rc
├── app.manifest # Windows DPI awareness, compatibility
└── app.rc # Resource compiler input
Module Responsibilities
- main.rs: Global state (
TOKIO_RUNTIME,MENU_UPDATE_REQUESTED), server restart logic, Windows message pump - config.rs:
Configstruct, load/save to%APPDATA%\Xray-VPN-Manager\config.json - xray_manager.rs: Wraps
v2parser::xray_runner::XrayRunner, manages server processes inXRAY_PROCESSESHashMap - vpn/mod.rs: Fetches subscription URLs (base64-encoded), parses URIs (vless, vmess, trojan, ss, socks), assigns local ports
- ui/tray.rs: Creates tray icon (gold star), builds dynamic menu with running servers
- ui/settings_window.rs: Complex native Win32 window with custom scrolling, file dialogs, dynamic server list
Configuration & Data Flow
Config File
Location: %APPDATA%\Xray-VPN-Manager\config.json
{
"subscription_url": "https://example.com/sub",
"xray_binary_path": "C:\\path\\to\\xray.exe",
"server_settings": {
"VLESS://server1.com:443": {
"local_port": 1080,
"proxy_type": "SOCKS",
"enabled": true
}
}
}
Server Key Format: PROTOCOL://address:port (e.g., VLESS://server.com:443)
Data Flow
-
Startup:
- Load config from
%APPDATA% - Fetch subscription URIs
- Parse and assign ports (preserve saved settings)
- Store in
VPN_SERVERSglobal mutex - Start enabled servers via
xray_manager::start_server()
- Load config from
-
Settings Window:
- User enters/updates subscription URL
- Click "Update" → fetch and parse in background thread
- Store in
VPN_SERVERS, postWM_UPDATE_SERVERSmessage - Rebuild server list UI with checkboxes, port edits, proxy type combos
- Click "Save" → build
Configfrom UI state, save to JSON, callrestart_xray_servers()
-
Server Control:
restart_xray_servers()stops all, starts enabled ones- Updates tray menu via
request_menu_update()(sets atomic flag) - Main loop checks flag and calls
update_tray_menu()
Code Patterns & Conventions
Global State
Uses LazyLock (nightly-stabilized) for lazy static initialization:
pub static TOKIO_RUNTIME: LazyLock<tokio::runtime::Runtime> = LazyLock::new(|| {
tokio::runtime::Runtime::new().expect("Failed to create tokio runtime")
});
pub static VPN_SERVERS: Mutex<Option<Vec<VpnServer>>> = Mutex::new(None);
pub static XRAY_PROCESSES: LazyLock<Mutex<HashMap<String, XrayRunner>>> = ...;
pub static MENU_UPDATE_REQUESTED: AtomicBool = AtomicBool::new(false);
Threading Model
- Main thread: Windows message loop (synchronous, blocking
GetMessageW) - Background threads: Subscription fetching, server start/stop (spawn via
std::thread::spawn) - Async runtime: Tokio runtime for xray process management, accessed via
TOKIO_RUNTIME.block_on(async { ... })
Error Handling
- Functions return
Result<T, String>(error messages as strings) - Silent failures common in networking code (vpn/mod.rs):
Err(_) => {}without logging - Print statements (
println!,eprintln!) for debugging (no structured logging)
Naming Conventions
- Functions:
snake_case(Rust standard) - Types:
PascalCase(VpnServer,ServerSettings) - Constants:
UPPER_SNAKE_CASE(ID_URL_EDIT,MARGIN,WM_UPDATE_SERVERS) - Control IDs: Sequential ranges (checkboxes: 2000+, port edits: 3000+, combos: 4000+, labels: 5000+)
Windows API Patterns
Uses windows crate (v0.58) with extensive feature flags:
#[cfg(windows)]
use windows::{
Win32::{
Foundation::{HWND, LPARAM, WPARAM},
UI::WindowsAndMessaging::*,
},
};
- String encoding: UTF-16 conversion everywhere:
"text\0".encode_utf16().collect::<Vec<u16>>() - Resource management: No explicit cleanup (relies on OS to clean up on process exit)
- Unsafe: Almost all Win32 calls wrapped in
unsafe { }blocks - Error handling:
.ok(),.unwrap(),.expect()common (crashes on critical failures)
UI Control Layout
Constants define consistent spacing:
const MARGIN: i32 = 15;
const FONT_SIZE: i32 = 32;
const LABEL_HEIGHT: i32 = 45;
const CONTROL_HEIGHT: i32 = 45;
const ROW_HEIGHT: i32 = 55;
Dynamic widths calculated from window size in WM_SIZE handler.
Dependencies & External Tools
Key Rust Dependencies
- tray-icon (0.21): Cross-platform tray icon (uses Windows native API)
- windows (0.58): Direct Win32 API bindings (extensive feature list in Cargo.toml)
- tokio (1.x): Async runtime for process management (rt-multi-thread, sync, macros)
- serde/serde_json (1.x): Config serialization
- reqwest (0.12): HTTP client for subscription fetching (blocking feature)
- base64 (0.22): Decode subscription content
- image (0.25): Image handling (unused in current code?)
- v2parser (local path): Custom parser for VPN URIs (path:
../v2-uri-parser)
Critical External Dependency
v2parser is a local crate located at ../v2-uri-parser (relative to project root). Must exist for project to build.
Provides:
v2parser::parser::get_metadata(uri: &str) -> String(JSON string)v2parser::parser::create_json_config(uri, socks_port, http_port) -> Stringv2parser::xray_runner::XrayRunner(process management)
External Binaries
xray-core: User must download separately from https://github.com/XTLS/Xray-core/releases
Binary path configured in settings window and saved to config.
Architecture Details
Async Runtime Usage
Tokio runtime is created once at startup and used for:
- Starting xray processes:
TOKIO_RUNTIME.block_on(async { xray_manager::start_server(...).await }) - Stopping processes:
TOKIO_RUNTIME.block_on(async { xray_manager::stop_all_servers().await })
Important: Main thread blocks on async operations (no true concurrency for process management).
Process Management
Each enabled server spawns an xray process via v2parser::xray_runner::XrayRunner:
- Stored in global
XRAY_PROCESSES: HashMap<server_key, XrayRunner> - Server key format:
PROTOCOL://address:port - Processes cleaned up on
stop_server()orstop_all_servers() - All processes stopped on app exit (main.rs:154)
Tray Menu Updates
Two-step update mechanism:
- Call
request_menu_update()to setMENU_UPDATE_REQUESTEDatomic flag - Main loop checks flag and calls
update_tray_menu(tray_icon, settings_item, quit_item) - Creates new menu with
create_tray_menu_with_servers()and sets it viatray_icon.set_menu()
Why? tray-icon doesn't support callbacks, and menu must be updated from main thread.
Settings Window Lifecycle
- Created on "Settings" menu item click
- HWND stored in
Arc<Mutex<Option<HWND>>>to prevent duplicates - Brings to front if already open (main.rs:140-151)
- Custom window class "SettingsWindowClass" with white background
- Custom scroll container class "ScrollContainerClass" with manual scroll handling
Common Gotchas
1. Windows-Only Build
Project will not compile on Linux/macOS due to:
#[cfg(windows)]everywhere- Direct Win32 API calls
- Platform-specific dependencies
2. Edition 2024 Requirement
Cargo.toml specifies edition = "2024" (unreleased as of knowledge cutoff). May need Rust nightly or change to edition = "2021".
3. Local Dependency Path
v2parser = { path = "../v2-uri-parser" } must exist at that exact relative path. Not published on crates.io.
4. Windows Subsystem Flag
main.rs:1 has #![windows_subsystem = "windows"] commented out for debugging. In release:
- Uncomment to hide console window
- Comment to see stdout/stderr
5. Manifest Embedding
build.rs embeds app.manifest via embed-resource crate. Required for:
- DPI awareness (PerMonitorV2)
- Windows 10/11 compatibility flags
- Non-admin execution (asInvoker)
6. UTF-16 Null Termination
All Windows API strings must be null-terminated:
let text: Vec<u16> = "Hello\0".encode_utf16().collect(); // ✅ Correct
let text: Vec<u16> = "Hello".encode_utf16().collect(); // ❌ Crashes or garbage
7. Control IDs Must Be Unique
Each window control needs unique ID for GetDlgItem():
- Checkboxes:
ID_SERVER_CHECKBOX_BASE + index(2000+) - Port edits:
ID_SERVER_PORT_EDIT_BASE + index(3000+) - Combos:
ID_SERVER_PROXY_COMBO_BASE + index(4000+)
ID collision causes controls to be unreachable.
8. Custom Message Range
Custom Windows messages start at WM_USER + 1:
const WM_UPDATE_SERVERS: u32 = WM_USER + 1;
Don't use values below WM_USER (conflicts with system messages).
9. Background Thread UI Updates
Cannot update UI from background threads. Use PostMessageW() to marshal to main thread:
std::thread::spawn(move || {
let servers = fetch_and_process_vpn_list(&url);
unsafe {
PostMessageW(hwnd, WM_UPDATE_SERVERS, WPARAM(0), LPARAM(0));
}
});
10. Scroll Container Children
Server list controls are children of scroll container, not main window. Must use:
GetDlgItem(container, control_id) // ✅ Correct
GetDlgItem(hwnd, control_id) // ❌ Returns error
Testing Approach
Manual Testing Checklist
-
Tray Icon:
- Icon appears in system tray (gold star)
- Right-click shows menu (Settings, Exit)
- Exit cleanly stops all processes
-
Settings Window:
- Opens on Settings click
- Focuses if already open (no duplicates)
- URL and Xray path persist from config
- Browse button opens file dialog
- Update fetches servers and populates list
-
Server List:
- Checkboxes toggle enabled state
- Port edits accept numbers only
- Proxy type combo shows SOCKS/HTTP
- Scroll works with mouse wheel and scrollbar
- Window resize adjusts layout
-
Save Functionality:
- Save writes to
%APPDATA%\Xray-VPN-Manager\config.json - Servers restart on save
- Tray menu updates with running servers
- Settings persist after app restart
- Save writes to
-
Error Handling:
- Invalid URL shows no error (silent failure)
- Missing xray binary path causes start failures (check console)
- Invalid port numbers handled gracefully
No Automated Tests
Currently no unit tests, integration tests, or CI/CD pipeline. All validation is manual.
Adding New Features
Adding a New Menu Item
-
Create
MenuIteminmain.rs:let new_item = MenuItem::new("New Feature", true, None); -
Add to menu in
ui/tray.rs:tray_menu.append(&new_item).unwrap(); -
Handle event in main loop:
if event.id == new_item.id() { // Handle click }
Adding a Config Field
-
Add to
Configstruct inconfig.rs:pub struct Config { pub subscription_url: String, pub xray_binary_path: String, pub new_field: String, // Add this #[serde(default)] pub server_settings: HashMap<String, ServerSettings>, } -
Update
Defaultimpl:fn default() -> Self { Config { // ... new_field: String::new(), // ... } } -
Add UI control in
settings_window.rs:- Define control ID constant
- Create control in
create_controls() - Read value in Save button handler
Adding a New VPN Protocol
-
Update
parse_vpn_uri()invpn/mod.rs:let is_supported = uri.starts_with("vless://") || uri.starts_with("newprotocol://"); // Add this -
Ensure
v2parsercrate supports the protocol (external dependency).
Debugging Tips
Enable Console Output
Ensure #![windows_subsystem = "windows"] is commented in main.rs:1.
Common Debug Points
-
Subscription fetch fails:
- Check URL is valid and returns base64-encoded content
- Add
println!infetch_and_process_vpn_list()to see response
-
Servers don't start:
- Verify xray binary path exists
- Check
xray_manager::start_server()errors in console - Ensure
v2parsergenerates valid config
-
Menu doesn't update:
- Verify
request_menu_update()called - Check
MENU_UPDATE_REQUESTEDflag in main loop - Ensure
VPN_SERVERSmutex populated
- Verify
-
Settings window controls missing:
- Check control IDs are unique
- Verify
GetDlgItem()uses correct parent (container vs. hwnd) - Look for "Failed to create..." messages
-
DPI scaling issues:
- Verify
app.manifestembedded (checkbuild.rsruns) - Ensure
SetProcessDpiAwarenessContextcalled at startup
- Verify
Useful Print Statements
println!("Settings window created: {:?}", hwnd);
println!("URL entered: {}", url);
println!("Started server: {}", server.name);
eprintln!("Failed to start server {}: {}", server.name, e);
Already present in code for basic debugging.
Performance Considerations
UI Thread Blocking
Main thread blocks on:
GetMessageW()(Windows message pump)TOKIO_RUNTIME.block_on()(async operations)
Keep async operations fast to avoid UI freezes.
Scroll Performance
Settings window with many servers (100+) may lag:
- Each server creates 4-5 controls (checkbox, label, edit, combo)
- Scroll handler moves all children on every scroll event
- Consider virtualization if server count exceeds ~50
Subscription Fetch
Blocking HTTP call in background thread:
reqwest::blocking::get(url)
No timeout configured. May hang on slow/unresponsive servers.
Security & Safety
Unsafe Code
Extensive use of unsafe for Windows API:
- String pointer conversions (
PCWSTR::from_raw()) - Window handles (
HWNDcasting) - Message parameter packing (
WPARAM,LPARAM)
Assumption: Windows API contracts upheld (e.g., null-terminated strings).
Process Execution
Executes arbitrary xray binary from user-configured path. No validation of binary integrity.
Network Requests
Fetches subscription URLs without TLS verification control. Uses reqwest defaults (should verify certificates).
Config Storage
Plaintext JSON in %APPDATA%. No encryption. Contains:
- Subscription URLs (may include credentials in query params)
- Server addresses/ports
- Local proxy ports
Not suitable for highly sensitive credentials.
Future Improvements
Based on code analysis:
- Add tests: Unit tests for config, integration tests for subscription parsing
- Logging: Replace
println!with structured logging (e.g.,tracing,env_logger) - Error handling: Return structured errors instead of
String, display errors in UI - Timeouts: Add request timeouts to
reqwest::blocking::get() - Virtualized list: For 50+ servers, implement virtual scrolling
- Tray menu callbacks: Investigate better tray update mechanism (polling is suboptimal)
- CI/CD: Add GitHub Actions for Windows builds
- Installer: Package as MSI/NSIS installer instead of bare
.exe - Auto-update: Check for new versions on startup
- Connection testing: Ping/test servers before enabling
Build Troubleshooting
"edition 2024 not found"
Change Cargo.toml:
edition = "2021" # Change from 2024
"v2parser not found"
Ensure ../v2-uri-parser exists relative to project root. Clone or create it separately.
"embed-resource failed"
Ensure Windows SDK installed (required for rc.exe resource compiler).
Missing Windows features
If compile fails with missing types, add to Cargo.toml:
[target.'cfg(windows)'.dependencies]
windows = { version = "0.58", features = [
# Add missing feature here
] }
Summary for AI Agents
When working in this codebase:
- Platform: Windows-only, will not compile elsewhere
- External dep:
v2parserat../v2-uri-parserrequired - Testing: Manual only, no automated tests
- UI: Native Win32, complex custom controls in
settings_window.rs - Async: Tokio runtime used, but main thread blocks on operations
- State: Global mutexes (
VPN_SERVERS,XRAY_PROCESSES) shared across threads - Config:
%APPDATA%\Xray-VPN-Manager\config.json, plaintext JSON - Strings: Always UTF-16 with null terminator for Windows API
- IDs: Control IDs must be unique, use defined constants + index
- Updates: UI updates from background threads via
PostMessageW()
Most complex file: ui/settings_window.rs (1200+ lines, custom scrolling, dynamic layout)
Most critical function: restart_xray_servers() in main.rs (stops/starts all servers)
Most fragile part: Windows API unsafe code (crashes if assumptions violated)