Add Waylume Manifesto and update API routes for peer management

This commit is contained in:
ImBenji
2025-08-17 21:56:59 +01:00
parent 573744a22d
commit 8b63de7db2
6 changed files with 574 additions and 253 deletions

View File

@@ -1,26 +1,4 @@
import 'dart:isolate';
import 'dart:math';
import 'package:waylume_server/services/vpn_session_service.dart';
void initVpnSessionMonitor() {
// Run this on a separate thread
Isolate.spawn((_) async {
// To avoid server deadlock, add random delay
await Future.delayed(Duration(seconds: Random().nextInt(10)));
print('VPN Session Monitor started');
while (true) {
try {
await VpnSessionService.detectSessions();
} catch (e) {
print('Error in VPN session monitor: $e');
}
// Check for sessions every 60 seconds
await Future.delayed(Duration(seconds: 60));
}
}, null);
// VPN session monitoring disabled - peer management now handled via API endpoints
print('VPN Session Monitor: Monitoring disabled - using API endpoints for peer management');
}

View File

@@ -1,63 +1,98 @@
import 'dart:convert';
import 'dart:io';
import 'package:supabase/supabase.dart';
import 'package:waylume_server/config/supabase_config.dart';
class VpnSessionService {
/// Detects existing VPN sessions for this server and prints their IDs with keepalive info
static Future<void> detectSessions() async {
/// Gets all peers currently on the WireGuard interface
static Future<List<Map<String, dynamic>>> getAllLocalPeers() async {
try {
final serverId = fromEnivronment('SERVER_ID');
final peers = <Map<String, dynamic>>[];
if (serverId == null) {
print('ERROR: SERVER_ID environment variable not set');
return;
// Get all peer IPs and public keys
final allowedIpsResult = await Process.run('wg', ['show', 'wg0', 'allowed-ips']);
if (allowedIpsResult.exitCode != 0) {
return peers;
}
// Get all sessions for this server
final sessions = await SUPABASE_CLIENT
.from('vpn_sessions')
.select('id, peer_info')
.eq('server_id', serverId);
final lines = allowedIpsResult.stdout.toString().trim().split('\n');
if (sessions.isEmpty) {
print('No VPN sessions found for server: $serverId');
} else {
print('Found ${sessions.length} VPN sessions for server: $serverId');
for (final line in lines) {
if (line.trim().isEmpty) continue;
for (final session in sessions) {
final sessionId = session['id'];
final peerInfo = session['peer_info'];
final parts = line.trim().split('\t');
if (parts.length >= 2) {
final publicKey = parts[0];
final ipWithMask = parts[1];
final ipAddress = ipWithMask.split('/')[0];
if (peerInfo != null && peerInfo['peer'] != null && peerInfo['peer']['publicKey'] != null) {
final publicKey = peerInfo['peer']['publicKey'] as String;
final keepaliveInfo = await _getKeepaliveForPeer(publicKey);
print('Session ID: $sessionId - Peer: ${publicKey.substring(0, 8)}... - $keepaliveInfo');
} else {
print('Session ID: $sessionId - No peer public key available');
}
final keepaliveInfo = await _getKeepaliveForPeer(publicKey);
final peerData = _parseKeepaliveInfo(keepaliveInfo, publicKey, ipAddress);
peers.add(peerData);
}
}
return peers;
} catch (e) {
print('Error detecting sessions: $e');
print('Error getting local peers: $e');
return [];
}
}
/// Generates public key from private key
static Future<String> _getPublicKeyFromPrivateKey(String privateKey) async {
try {
final pubProcess = await Process.start('wg', ['pubkey']);
pubProcess.stdin.writeln(privateKey);
await pubProcess.stdin.close();
final publicKey = await pubProcess.stdout.transform(utf8.decoder).join();
return publicKey.trim();
} catch (e) {
return 'Error generating public key: $e';
}
/// Gets only inactive peers (no handshake in >150 seconds)
static Future<List<Map<String, dynamic>>> getInactivePeers() async {
final allPeers = await getAllLocalPeers();
return allPeers.where((peer) => peer['status'] == 'DEAD').toList();
}
/// Parses keepalive info string into structured data
static Map<String, dynamic> _parseKeepaliveInfo(String keepaliveInfo, String publicKey, String ipAddress) {
if (keepaliveInfo.startsWith('DEAD')) {
final regex = RegExp(r'DEAD - Last handshake: ([^(]+) \((\d+)m (\d+)s ago\)');
final match = regex.firstMatch(keepaliveInfo);
if (match != null) {
final lastHandshake = match.group(1)?.trim();
final minutes = int.tryParse(match.group(2) ?? '0') ?? 0;
final seconds = int.tryParse(match.group(3) ?? '0') ?? 0;
final totalMinutes = minutes + (seconds / 60).round();
return {
'public_key': publicKey,
'ip_address': ipAddress,
'last_handshake': lastHandshake,
'minutes_since_handshake': totalMinutes,
'status': 'DEAD'
};
}
} else if (keepaliveInfo.startsWith('ALIVE')) {
final regex = RegExp(r'ALIVE - Last handshake: ([^(]+) \((\d+)s ago\)');
final match = regex.firstMatch(keepaliveInfo);
if (match != null) {
final lastHandshake = match.group(1)?.trim();
final seconds = int.tryParse(match.group(2) ?? '0') ?? 0;
final minutes = (seconds / 60).round();
return {
'public_key': publicKey,
'ip_address': ipAddress,
'last_handshake': lastHandshake,
'minutes_since_handshake': minutes,
'status': 'ALIVE'
};
}
}
// Fallback for other statuses
return {
'public_key': publicKey,
'ip_address': ipAddress,
'last_handshake': null,
'minutes_since_handshake': null,
'status': keepaliveInfo
};
}
/// Gets keepalive info for a specific peer
static Future<String> _getKeepaliveForPeer(String publicKey) async {

View File

@@ -11,15 +11,52 @@ class PeerRoutes {
Router get router {
final router = Router();
router.post('/peers', _createPeer);
router.post('/peers/delete', _deletePeer);
router.post('/peers/config', _getPeerConfig);
router.post('/peers/speed-limit', _setSpeedLimit);
router.post('/peers/data-cap', _setDataCap);
router.get('/peers', _getPeers);
router.post('/peer', _createPeer);
router.delete('/peer/<publicKey>', _deletePeer);
router.get('/peer/<publicKey>/config', _getPeerConfig);
router.patch('/peer/<publicKey>/speed-limit', _setSpeedLimit);
router.patch('/peer/<publicKey>/data-cap', _setDataCap);
return router;
}
Future<Response> _getPeers(Request request) async {
try {
final statusParam = request.url.queryParameters['status']?.toUpperCase();
List<Map<String, dynamic>> peers;
if (statusParam == 'DEAD') {
peers = await VpnSessionService.getInactivePeers();
} else if (statusParam == 'ALIVE') {
final allPeers = await VpnSessionService.getAllLocalPeers();
peers = allPeers.where((peer) => peer['status'] == 'ALIVE').toList();
} else {
// No filter or invalid filter - return all peers
peers = await VpnSessionService.getAllLocalPeers();
}
return Response.ok(
jsonEncode({
'success': true,
'peers': peers,
'total_count': peers.length,
'filter': statusParam,
}),
headers: {'Content-Type': 'application/json'},
);
} catch (e) {
return Response.internalServerError(
body: jsonEncode({
'success': false,
'error': e.toString(),
}),
headers: {'Content-Type': 'application/json'},
);
}
}
Future<Response> _createPeer(Request request) async {
try {
final peer = await createPeer();
@@ -57,11 +94,9 @@ class PeerRoutes {
Future<Response> _deletePeer(Request request) async {
try {
final body = await request.readAsString();
final data = jsonDecode(body) as Map<String, dynamic>;
final publicKey = data['publicKey'] as String?;
final publicKey = request.params['publicKey'];
if (publicKey == null) {
if (publicKey == null || publicKey.isEmpty) {
return Response.badRequest(
body: jsonEncode({
'success': false,
@@ -93,11 +128,9 @@ class PeerRoutes {
Future<Response> _getPeerConfig(Request request) async {
try {
final body = await request.readAsString();
final data = jsonDecode(body) as Map<String, dynamic>;
final publicKey = data['publicKey'] as String?;
final publicKey = request.params['publicKey'];
if (publicKey == null) {
if (publicKey == null || publicKey.isEmpty) {
return Response.badRequest(
body: jsonEncode({
'success': false,
@@ -127,12 +160,12 @@ class PeerRoutes {
Future<Response> _setSpeedLimit(Request request) async {
try {
final publicKey = request.params['publicKey'];
final body = await request.readAsString();
final data = jsonDecode(body) as Map<String, dynamic>;
final publicKey = data['publicKey'] as String?;
final bytesPerSecond = data['bytesPerSecond'] as int?;
if (publicKey == null) {
if (publicKey == null || publicKey.isEmpty) {
return Response.badRequest(
body: jsonEncode({
'success': false,
@@ -174,12 +207,12 @@ class PeerRoutes {
Future<Response> _setDataCap(Request request) async {
try {
final publicKey = request.params['publicKey'];
final body = await request.readAsString();
final data = jsonDecode(body) as Map<String, dynamic>;
final publicKey = data['publicKey'] as String?;
final quotaBytes = data['quotaBytes'] as int?;
if (publicKey == null) {
if (publicKey == null || publicKey.isEmpty) {
return Response.badRequest(
body: jsonEncode({
'success': false,