diff --git a/lib/services/bandwidth_service.dart b/lib/services/bandwidth_service.dart new file mode 100644 index 0000000..436bdf2 --- /dev/null +++ b/lib/services/bandwidth_service.dart @@ -0,0 +1,124 @@ +import 'dart:io'; +import 'dart:convert'; + +class BandwidthService { + // In-memory collector state: collector_id -> peer_public_key -> {upload_last_seen, download_last_seen} + static final Map>> _collectorStates = {}; + + /// Gets bandwidth stats for a collector + /// If collector_id is provided, returns incremental usage since last call + /// If no collector_id, returns total cumulative usage from WireGuard + static Future>> getBandwidthStats(String? collectorId) async { + final currentStats = await _getCurrentWireGuardStats(); + + if (collectorId == null || collectorId.isEmpty) { + // No collector ID - return cumulative totals + return currentStats; + } + + // Collector ID provided - calculate increments + return _calculateIncrementalStats(collectorId, currentStats); + } + + /// Gets current WireGuard traffic statistics using 'wg show wg0 dump' + static Future>> _getCurrentWireGuardStats() async { + try { + final result = await Process.run('wg', ['show', 'wg0', 'dump']); + if (result.exitCode != 0) { + throw Exception('Failed to get WireGuard stats: ${result.stderr}'); + } + + final output = result.stdout.toString().trim(); + final lines = output.split('\n'); + + final stats = >{}; + + // Skip first line (server info) and process peer lines + for (int i = 1; i < lines.length; i++) { + final line = lines[i].trim(); + if (line.isEmpty) continue; + + final parts = line.split('\t'); + if (parts.length >= 7) { + final publicKey = parts[0]; + final uploadBytes = int.tryParse(parts[5]) ?? 0; + final downloadBytes = int.tryParse(parts[6]) ?? 0; + + stats[publicKey] = { + 'upload_bytes': uploadBytes, + 'download_bytes': downloadBytes, + }; + } + } + + return stats; + } catch (e) { + print('Error getting WireGuard stats: $e'); + return {}; + } + } + + /// Calculates incremental stats for a collector + static Map> _calculateIncrementalStats( + String collectorId, + Map> currentStats + ) { + // Initialize collector state if it doesn't exist + _collectorStates[collectorId] ??= {}; + + final collectorState = _collectorStates[collectorId]!; + final incrementalStats = >{}; + + for (final entry in currentStats.entries) { + final publicKey = entry.key; + final currentUpload = entry.value['upload_bytes'] ?? 0; + final currentDownload = entry.value['download_bytes'] ?? 0; + + // Get last seen values for this collector and peer + final lastSeen = collectorState[publicKey] ?? {'upload_bytes': 0, 'download_bytes': 0}; + final lastSeenUpload = lastSeen['upload_bytes'] ?? 0; + final lastSeenDownload = lastSeen['download_bytes'] ?? 0; + + // Calculate increments (handle potential WireGuard resets) + final uploadIncrement = _calculateSafeIncrement(currentUpload, lastSeenUpload); + final downloadIncrement = _calculateSafeIncrement(currentDownload, lastSeenDownload); + + // Only include peers with non-zero usage (optimization) + if (uploadIncrement > 0 || downloadIncrement > 0) { + incrementalStats[publicKey] = { + 'upload_bytes': uploadIncrement, + 'download_bytes': downloadIncrement, + }; + } + + // Update collector state with current values + collectorState[publicKey] = { + 'upload_bytes': currentUpload, + 'download_bytes': currentDownload, + }; + } + + return incrementalStats; + } + + /// Safely calculates increment, handling WireGuard resets + static int _calculateSafeIncrement(int current, int lastSeen) { + if (current >= lastSeen) { + return current - lastSeen; + } else { + // WireGuard likely reset (server restart) - use current value as increment + print('WireGuard reset detected: current=$current < lastSeen=$lastSeen'); + return current; + } + } + + /// Clears collector state (for debugging/maintenance) + static void clearCollectorState(String collectorId) { + _collectorStates.remove(collectorId); + } + + /// Gets current collector state (for debugging) + static Map>> getCollectorStates() { + return Map.from(_collectorStates); + } +} \ No newline at end of file diff --git a/lib/web/peer_routes.dart b/lib/web/peer_routes.dart index d9be837..44b71db 100644 --- a/lib/web/peer_routes.dart +++ b/lib/web/peer_routes.dart @@ -6,6 +6,7 @@ import 'package:waylume_server/wireguard/peers.dart'; import 'package:waylume_server/wireguard/utils.dart'; import 'package:waylume_server/core/utils.dart'; import 'package:waylume_server/services/vpn_session_service.dart'; +import 'package:waylume_server/services/bandwidth_service.dart'; class PeerRoutes { Router get router { @@ -17,6 +18,7 @@ class PeerRoutes { router.get('/peer//config', _getPeerConfig); router.patch('/peer//speed-limit', _setSpeedLimit); router.patch('/peer//data-cap', _setDataCap); + router.get('/bandwidth-stats', _getBandwidthStats); return router; } @@ -251,4 +253,28 @@ class PeerRoutes { ); } } + + Future _getBandwidthStats(Request request) async { + try { + final collectorId = request.url.queryParameters['collector_id']; + + final bandwidthData = await BandwidthService.getBandwidthStats(collectorId); + + return Response.ok( + jsonEncode({ + 'success': true, + 'data': bandwidthData, + }), + headers: {'Content-Type': 'application/json'}, + ); + } catch (e) { + return Response.internalServerError( + body: jsonEncode({ + 'success': false, + 'error': e.toString(), + }), + headers: {'Content-Type': 'application/json'}, + ); + } + } } \ No newline at end of file