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:
19
ostp-gui/Cargo.toml
Normal file
19
ostp-gui/Cargo.toml
Normal 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
3
ostp-gui/build.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
39
ostp-gui/src/ipc.rs
Normal file
39
ostp-gui/src/ipc.rs
Normal 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
56
ostp-gui/src/main.rs
Normal 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
37
ostp-gui/src/state.rs
Normal 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
43
ostp-gui/tauri.conf.json
Normal 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
117
ostp-gui/ui/app.js
Normal 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
55
ostp-gui/ui/index.html
Normal 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
162
ostp-gui/ui/styles.css
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user