feat: Windows stack (daemon, installer, GUI)

Components:
- ostp-daemon: Windows Service with Named Pipe IPC
- ostp-installer: Setup wizard with admin privileges
- ostp-gui: Tauri dark theme UI (450x600)

Features:
- Background service management (OspabGuard)
- IPC commands: CONNECT/DISCONNECT/STATUS
- Firewall rules auto-configuration
- Wintun driver placeholder (download from wintun.net)
- Real-time stats display (upload/download/ping)

Note: Requires wintun.dll download for full functionality
This commit is contained in:
2026-01-02 02:17:15 +03:00
parent 7ed4217987
commit 85a2b01074
40 changed files with 1460 additions and 7376 deletions

19
ostp-gui/Cargo.toml Normal file
View File

@@ -0,0 +1,19 @@
[package]
name = "ostp-gui"
version.workspace = true
edition.workspace = true
description = "OSTP Windows GUI Client"
[build-dependencies]
tauri-build = { version = "2", features = [] }
[dependencies]
tauri = { version = "2", features = ["protocol-asset"] }
tauri-plugin-shell = "2"
serde.workspace = true
serde_json.workspace = true
tokio.workspace = true
anyhow.workspace = true
[target.'cfg(windows)'.dependencies]
winapi = { version = "0.3", features = ["winbase", "namedpipeapi", "fileapi"] }

3
ostp-gui/build.rs Normal file
View File

@@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}

39
ostp-gui/src/ipc.rs Normal file
View File

@@ -0,0 +1,39 @@
//! IPC communication with ostp-daemon via Named Pipe
use anyhow::{Context, Result};
use std::io::{Read, Write};
const PIPE_NAME: &str = r"\\.\pipe\ostp-daemon";
pub async fn send_command(command: &str) -> Result<String> {
#[cfg(windows)]
{
use std::fs::OpenOptions;
use std::os::windows::fs::OpenOptionsExt;
use winapi::um::winbase::FILE_FLAG_OVERLAPPED;
// Connect to Named Pipe
let mut pipe = OpenOptions::new()
.read(true)
.write(true)
.custom_flags(FILE_FLAG_OVERLAPPED)
.open(PIPE_NAME)
.context("Failed to connect to ostp-daemon. Is the service running?")?;
// Send command
pipe.write_all(command.as_bytes())?;
pipe.write_all(b"\n")?;
pipe.flush()?;
// Read response
let mut response = String::new();
pipe.read_to_string(&mut response)?;
Ok(response.trim().to_string())
}
#[cfg(not(windows))]
{
anyhow::bail!("Named pipes are only supported on Windows");
}
}

56
ostp-gui/src/main.rs Normal file
View File

@@ -0,0 +1,56 @@
//! OSTP GUI - Windows Client Interface
//!
//! Communicates with ostp-daemon via Named Pipe
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
mod ipc;
mod state;
use tauri::Manager;
use state::AppState;
#[tauri::command]
async fn connect_vpn(state: tauri::State<'_, AppState>) -> Result<String, String> {
ipc::send_command("CONNECT")
.await
.map_err(|e| e.to_string())
}
#[tauri::command]
async fn disconnect_vpn(state: tauri::State<'_, AppState>) -> Result<String, String> {
ipc::send_command("DISCONNECT")
.await
.map_err(|e| e.to_string())
}
#[tauri::command]
async fn get_status(state: tauri::State<'_, AppState>) -> Result<String, String> {
ipc::send_command("STATUS")
.await
.map_err(|e| e.to_string())
}
#[tauri::command]
async fn fetch_servers() -> Result<Vec<String>, String> {
// TODO: Fetch from Master Node API
Ok(vec![
"RU - Moscow".to_string(),
"US - New York".to_string(),
"DE - Frankfurt".to_string(),
])
}
fn main() {
tauri::Builder::default()
.plugin(tauri_plugin_shell::init())
.manage(AppState::new())
.invoke_handler(tauri::generate_handler![
connect_vpn,
disconnect_vpn,
get_status,
fetch_servers
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

37
ostp-gui/src/state.rs Normal file
View File

@@ -0,0 +1,37 @@
//! Application state management
use std::sync::{Arc, Mutex};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConnectionStatus {
pub connected: bool,
pub server: Option<String>,
pub upload_speed: u64,
pub download_speed: u64,
pub ping: u32,
}
impl Default for ConnectionStatus {
fn default() -> Self {
Self {
connected: false,
server: None,
upload_speed: 0,
download_speed: 0,
ping: 0,
}
}
}
pub struct AppState {
pub status: Arc<Mutex<ConnectionStatus>>,
}
impl AppState {
pub fn new() -> Self {
Self {
status: Arc::new(Mutex::new(ConnectionStatus::default())),
}
}
}

43
ostp-gui/tauri.conf.json Normal file
View File

@@ -0,0 +1,43 @@
{
"$schema": "https://schema.tauri.app/config/2.0",
"productName": "OSTP VPN",
"version": "0.1.0",
"identifier": "network.ospab.ostp",
"build": {
"beforeDevCommand": "",
"devUrl": "../ui/index.html",
"beforeBuildCommand": "",
"frontendDist": "../ui"
},
"app": {
"windows": [
{
"title": "OSTP VPN",
"width": 450,
"height": 600,
"resizable": false,
"fullscreen": false,
"decorations": true,
"transparent": false,
"alwaysOnTop": false
}
],
"security": {
"csp": "default-src 'self'; style-src 'self' 'unsafe-inline'"
}
},
"bundle": {
"active": true,
"targets": ["msi"],
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/icon.ico"
],
"windows": {
"certificateThumbprint": null,
"digestAlgorithm": "sha256",
"timestampUrl": ""
}
}
}

117
ostp-gui/ui/app.js Normal file
View File

@@ -0,0 +1,117 @@
const { invoke } = window.__TAURI__.core;
let isConnected = false;
// Initialize
document.addEventListener('DOMContentLoaded', async () => {
await loadServers();
await updateStatus();
// Update stats every 2 seconds when connected
setInterval(async () => {
if (isConnected) {
await updateStatus();
}
}, 2000);
});
async function loadServers() {
try {
const servers = await invoke('fetch_servers');
const select = document.getElementById('serverSelect');
select.innerHTML = '';
servers.forEach(server => {
const option = document.createElement('option');
option.value = server;
option.textContent = server;
select.appendChild(option);
});
} catch (error) {
console.error('Failed to load servers:', error);
showError('Failed to load server list');
}
}
async function toggleConnection() {
const button = document.getElementById('connectButton');
const buttonText = document.getElementById('buttonText');
button.disabled = true;
buttonText.textContent = 'Connecting...';
try {
if (!isConnected) {
const response = await invoke('connect_vpn');
console.log('Connect response:', response);
isConnected = true;
updateUI(true);
} else {
const response = await invoke('disconnect_vpn');
console.log('Disconnect response:', response);
isConnected = false;
updateUI(false);
}
} catch (error) {
console.error('Connection error:', error);
showError(error);
} finally {
button.disabled = false;
}
}
function updateUI(connected) {
const button = document.getElementById('connectButton');
const buttonText = document.getElementById('buttonText');
const statusIndicator = document.getElementById('statusIndicator');
const statsGrid = document.getElementById('statsGrid');
if (connected) {
button.classList.add('connected');
buttonText.textContent = 'Disconnect';
statusIndicator.textContent = 'Connected';
statusIndicator.classList.add('connected');
statsGrid.style.display = 'grid';
} else {
button.classList.remove('connected');
buttonText.textContent = 'Connect';
statusIndicator.textContent = 'Disconnected';
statusIndicator.classList.remove('connected');
statsGrid.style.display = 'none';
}
}
async function updateStatus() {
if (!isConnected) return;
try {
const status = await invoke('get_status');
const data = JSON.parse(status);
// Update stats
document.getElementById('uploadSpeed').textContent = formatSpeed(data.upload_speed);
document.getElementById('downloadSpeed').textContent = formatSpeed(data.download_speed);
document.getElementById('ping').textContent = `${data.ping} ms`;
} catch (error) {
console.error('Failed to update status:', error);
}
}
function formatSpeed(bytesPerSecond) {
if (bytesPerSecond < 1024) {
return `${bytesPerSecond} B/s`;
} else if (bytesPerSecond < 1024 * 1024) {
return `${(bytesPerSecond / 1024).toFixed(1)} KB/s`;
} else {
return `${(bytesPerSecond / (1024 * 1024)).toFixed(2)} MB/s`;
}
}
function showSettings() {
alert('Settings panel coming soon!');
}
function showError(message) {
// TODO: Better error UI
alert(`Error: ${message}`);
}

55
ostp-gui/ui/index.html Normal file
View File

@@ -0,0 +1,55 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OSTP VPN</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="container">
<div class="header">
<h1>OSTP VPN</h1>
<div class="status-indicator" id="statusIndicator">Disconnected</div>
</div>
<div class="main-content">
<!-- Connection Toggle -->
<button class="connect-button" id="connectButton" onclick="toggleConnection()">
<span id="buttonText">Connect</span>
</button>
<!-- Server Selection -->
<div class="server-section">
<label for="serverSelect">Server:</label>
<select id="serverSelect">
<option value="">Loading...</option>
</select>
</div>
<!-- Stats Display -->
<div class="stats-grid" id="statsGrid" style="display: none;">
<div class="stat">
<div class="stat-label">Upload</div>
<div class="stat-value" id="uploadSpeed">0 KB/s</div>
</div>
<div class="stat">
<div class="stat-label">Download</div>
<div class="stat-value" id="downloadSpeed">0 KB/s</div>
</div>
<div class="stat">
<div class="stat-label">Ping</div>
<div class="stat-value" id="ping">0 ms</div>
</div>
</div>
<!-- Settings -->
<div class="settings-section">
<button class="settings-button" onclick="showSettings()">⚙ Settings</button>
</div>
</div>
</div>
<script src="app.js"></script>
</body>
</html>

162
ostp-gui/ui/styles.css Normal file
View File

@@ -0,0 +1,162 @@
/* Dark Stealth Theme */
:root {
--bg-dark: #0a0a0a;
--bg-darker: #050505;
--accent: #00ff88;
--accent-dim: #00aa55;
--text: #ffffff;
--text-dim: #888888;
--border: #222222;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: var(--bg-dark);
color: var(--text);
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
}
.container {
width: 100%;
height: 100%;
background: var(--bg-darker);
display: flex;
flex-direction: column;
}
.header {
padding: 20px;
border-bottom: 1px solid var(--border);
text-align: center;
}
.header h1 {
font-size: 24px;
font-weight: 600;
margin-bottom: 10px;
}
.status-indicator {
display: inline-block;
padding: 5px 15px;
border-radius: 20px;
font-size: 12px;
background: var(--border);
color: var(--text-dim);
}
.status-indicator.connected {
background: var(--accent);
color: var(--bg-dark);
font-weight: 600;
}
.main-content {
flex: 1;
padding: 30px;
display: flex;
flex-direction: column;
gap: 25px;
}
.connect-button {
width: 100%;
height: 80px;
border: 3px solid var(--accent-dim);
background: transparent;
color: var(--accent);
font-size: 20px;
font-weight: 600;
border-radius: 10px;
cursor: pointer;
transition: all 0.3s ease;
}
.connect-button:hover {
background: var(--accent-dim);
color: var(--bg-dark);
}
.connect-button.connected {
background: var(--accent);
color: var(--bg-dark);
border-color: var(--accent);
}
.server-section {
display: flex;
flex-direction: column;
gap: 10px;
}
.server-section label {
font-size: 14px;
color: var(--text-dim);
}
.server-section select {
padding: 12px;
background: var(--bg-dark);
color: var(--text);
border: 1px solid var(--border);
border-radius: 5px;
font-size: 14px;
cursor: pointer;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 15px;
}
.stat {
background: var(--bg-dark);
padding: 15px;
border-radius: 8px;
text-align: center;
}
.stat-label {
font-size: 11px;
color: var(--text-dim);
margin-bottom: 5px;
text-transform: uppercase;
}
.stat-value {
font-size: 18px;
font-weight: 600;
color: var(--accent);
}
.settings-section {
margin-top: auto;
}
.settings-button {
width: 100%;
padding: 12px;
background: transparent;
color: var(--text-dim);
border: 1px solid var(--border);
border-radius: 5px;
font-size: 14px;
cursor: pointer;
transition: all 0.2s ease;
}
.settings-button:hover {
border-color: var(--accent-dim);
color: var(--accent);
}