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); } }