import 'dart:async'; import 'dart:io'; import 'package:waylume_server/services/vpn_session_service.dart'; class Connection { final String protocol; final String localIP; final int localPort; final String remoteIP; final int remotePort; Connection({ required this.protocol, required this.localIP, required this.localPort, required this.remoteIP, required this.remotePort, }); String get id => '$protocol:$localIP:$localPort->$remoteIP:$remotePort'; @override String toString() { return '$protocol $localIP:$localPort -> $remoteIP:$remotePort'; } } class ProtocolBlockingService { static Timer? _monitorTimer; static final Set _processedConnections = {}; static Set _activePeerIPs = {}; static void initialize() { print('Initializing Protocol Blocking Service...'); _startConnectionMonitoring(); } static void _startConnectionMonitoring() { print('📡 Starting connection monitoring timer (100ms intervals)...'); // Update peer IPs every 30 seconds Timer.periodic(Duration(seconds: 30), (_) async { await _updateActivePeerIPs(); }); // Initial peer IP update _updateActivePeerIPs(); _monitorTimer = Timer.periodic(Duration(milliseconds: 100), (_) async { await _scanForNewConnections(); }); } static Future _updateActivePeerIPs() async { try { final peers = await VpnSessionService.getAllLocalPeers(); final newPeerIPs = peers.map((peer) => peer['ip_address'] as String).toSet(); if (newPeerIPs.length != _activePeerIPs.length || !_activePeerIPs.containsAll(newPeerIPs)) { _activePeerIPs = newPeerIPs; print('🔄 Updated active peer IPs: $_activePeerIPs'); } } catch (e) { print('❌ Error updating peer IPs: $e'); } } static int _scanCount = 0; static Future _scanForNewConnections() async { _scanCount++; if (_activePeerIPs.isEmpty) { return; // No peers to monitor } try { // Monitor peer traffic directly using tcpdump on wg0 interface await _monitorPeerTraffic(); if (_scanCount % 100 == 0) { print('🔍 Monitoring ${_activePeerIPs.length} active peers: $_activePeerIPs'); } } catch (e) { print('❌ Error monitoring peer traffic: $e'); } } static Future _monitorPeerTraffic() async { for (final peerIP in _activePeerIPs) { try { // Capture any new outbound traffic from this peer final process = await Process.start('timeout', [ '0.1', // Very short timeout - just check for new packets 'tcpdump', '-i', 'wg0', '-c', '1', '-l', // Line buffered 'src $peerIP and (tcp[tcpflags] & tcp-syn != 0 or udp)', ]); final output = []; await for (final data in process.stdout) { output.add(String.fromCharCodes(data)); } final exitCode = await process.exitCode; process.kill(); if (exitCode == 0 && output.isNotEmpty) { final packetData = output.join(); await _analyzeNewPacket(packetData, peerIP); } } catch (e) { // Ignore timeout errors - normal when no new packets if (!e.toString().contains('timeout')) { print('❌ Error monitoring peer $peerIP: $e'); } } } } static Future _analyzeNewPacket(String packetData, String peerIP) async { if (_processedConnections.contains(packetData.hashCode.toString())) { return; // Already processed this packet pattern } _processedConnections.add(packetData.hashCode.toString()); print('🔍 New peer traffic detected from $peerIP'); print('📡 Capturing detailed handshake...'); // Now capture the full handshake with more detail await _captureDetailedHandshake(peerIP, packetData); } static Future _captureDetailedHandshake(String peerIP, String initialPacket) async { try { final process = await Process.start('timeout', [ '2', 'tcpdump', '-i', 'wg0', '-c', '3', // Capture a few packets to get handshake '-s', '200', '-x', 'src $peerIP', ]); final handshakeData = []; await for (final data in process.stdout) { handshakeData.add(String.fromCharCodes(data)); } final exitCode = await process.exitCode; process.kill(); if (exitCode == 0 && handshakeData.isNotEmpty) { final data = handshakeData.join(); print('✅ Handshake captured for peer $peerIP'); _analyzeHandshake(data, Connection( protocol: 'unknown', localIP: peerIP, localPort: 0, remoteIP: 'unknown', remotePort: 0, )); } else { print('⏱️ No additional handshake data captured for peer $peerIP'); } } catch (e) { print('❌ Error capturing detailed handshake: $e'); } } static List _parseConnections(String output, String protocol) { final connections = []; final lines = output.split('\n'); for (final line in lines) { try { final conn = _parseConnectionLine(line.trim(), protocol); if (conn != null) { connections.add(conn); } } catch (e) { // Skip malformed lines } } return connections; } static Connection? _parseConnectionLine(String line, String protocol) { if (line.isEmpty || line.startsWith('Netid') || line.startsWith('State')) { return null; } // Split by whitespace and filter empty strings final parts = line.split(RegExp(r'\s+')).where((s) => s.isNotEmpty).toList(); if (parts.length < 4) { return null; } try { // For TCP: State Recv-Q Send-Q Local_Address:Port Peer_Address:Port // For UDP: State Recv-Q Send-Q Local_Address:Port Peer_Address:Port final localAddr = parts[3]; final remoteAddr = parts.length > 4 ? parts[4] : ''; if (remoteAddr.isEmpty || remoteAddr == '*:*') { return null; // Skip listening sockets } final localParts = localAddr.split(':'); final remoteParts = remoteAddr.split(':'); if (localParts.length < 2 || remoteParts.length < 2) { return null; } final localIP = localParts.sublist(0, localParts.length - 1).join(':'); final localPort = int.parse(localParts.last); final remoteIP = remoteParts.sublist(0, remoteParts.length - 1).join(':'); final remotePort = int.parse(remoteParts.last); return Connection( protocol: protocol, localIP: localIP, localPort: localPort, remoteIP: remoteIP, remotePort: remotePort, ); } catch (e) { return null; } } static bool _isNewConnection(Connection conn) { final id = conn.id; if (_processedConnections.contains(id)) { return false; } _processedConnections.add(id); // Clean up old connections periodically (keep last 1000) if (_processedConnections.length > 1000) { final toRemove = _processedConnections.length - 800; final oldConnections = _processedConnections.take(toRemove).toList(); for (final old in oldConnections) { _processedConnections.remove(old); } } return true; } static bool _isPeerConnection(Connection conn) { // Check if the connection is FROM a VPN peer (local IP is peer IP) return _activePeerIPs.contains(conn.localIP); } static Future _handleNewConnection(Connection conn) async { print('🔍 New connection detected: $conn'); try { // Attempt to capture handshake data await _captureHandshake(conn); } catch (e) { print('⚠️ Error capturing handshake for $conn: $e'); } } static Future _captureHandshake(Connection conn) async { try { print('📡 Capturing handshake for: $conn'); // Use tcpdump to capture packets on WireGuard interface specifically final process = await Process.start('timeout', [ '2', // 2 second timeout 'tcpdump', '-i', 'wg0', // Monitor WireGuard interface specifically '-c', '1', // Capture only 1 packet '-s', '200', // Capture first 200 bytes only '-x', // Output in hex 'src ${conn.localIP} and dst ${conn.remoteIP}', ]); final handshakeData = []; await for (final data in process.stdout) { handshakeData.add(String.fromCharCodes(data)); } final exitCode = await process.exitCode; if (exitCode == 0 && handshakeData.isNotEmpty) { final data = handshakeData.join(); print('✅ Handshake captured for $conn:'); print(' Data (first 200 chars): ${data.length > 200 ? data.substring(0, 200) + "..." : data}'); // Analyze the handshake _analyzeHandshake(data, conn); } else if (exitCode == 124) { print('⏱️ Handshake capture timeout for $conn (no packets in 2s)'); } else { print('❌ Failed to capture handshake for $conn (exit code: $exitCode)'); } process.kill(); } catch (e) { print('💥 Error in handshake capture: $e'); } } static void _analyzeHandshake(String handshakeData, Connection conn) { print('════════════════ HANDSHAKE SIGNATURE ANALYSIS ════════════════'); print('📍 Connection: $conn'); // Extract raw bytes from tcpdump hex output final hexBytes = _extractHexBytes(handshakeData); final asciiData = _extractAsciiFromHex(hexBytes); print('📊 Raw Data Length: ${handshakeData.length} chars'); print('🔢 Hex Bytes (first 64): ${hexBytes.take(64).join(' ')}'); print('📝 ASCII Representation: ${asciiData.replaceAll('\n', '\\n').replaceAll('\r', '\\r')}'); print('🔍 First 32 bytes as string: ${String.fromCharCodes(hexBytes.take(32).map((h) => int.tryParse(h, radix: 16) ?? 0).where((b) => b >= 32 && b <= 126))}'); // Protocol detection with signature details final data = handshakeData.toLowerCase(); String? detectedProtocol; String signature = ''; if (data.contains('bittorrent protocol') || hexBytes.join('').contains('13426974546f7272656e742070726f746f636f6c')) { detectedProtocol = 'BitTorrent'; signature = 'BitTorrent handshake signature detected'; } else if (data.contains('ssh-2.0') || data.contains('ssh-1.')) { detectedProtocol = 'SSH'; signature = 'SSH protocol version string'; } else if (data.contains('get ') || data.contains('post ') || data.contains('http/')) { detectedProtocol = 'HTTP'; signature = 'HTTP request headers'; } else if (data.contains('220 ') && conn.remotePort == 25) { detectedProtocol = 'SMTP'; signature = 'SMTP welcome message'; } else if (data.contains('220 ') && conn.remotePort == 21) { detectedProtocol = 'FTP'; signature = 'FTP welcome message'; } else if (hexBytes.isNotEmpty && hexBytes.first == '16' && hexBytes.length > 5) { // TLS detection detectedProtocol = 'TLS/SSL'; signature = 'TLS ClientHello/ServerHello (0x16 record type)'; } if (detectedProtocol != null) { print('🎯 PROTOCOL IDENTIFIED: $detectedProtocol'); print('📋 Signature: $signature'); } else { print('❓ UNKNOWN PROTOCOL'); print('💡 Pattern not recognized - logging for analysis'); } print('══════════════════════════════════════════════════════════════'); } static List _extractHexBytes(String tcpdumpOutput) { final hexPattern = RegExp(r'0x[0-9a-f]+:\s*([0-9a-f\s]+)', caseSensitive: false); final matches = hexPattern.allMatches(tcpdumpOutput); final hexBytes = []; for (final match in matches) { final hexLine = match.group(1)?.replaceAll(' ', '') ?? ''; for (int i = 0; i < hexLine.length; i += 2) { if (i + 1 < hexLine.length) { hexBytes.add(hexLine.substring(i, i + 2)); } } } return hexBytes; } static String _extractAsciiFromHex(List hexBytes) { return hexBytes .map((hex) => int.tryParse(hex, radix: 16) ?? 0) .map((byte) => (byte >= 32 && byte <= 126) ? String.fromCharCode(byte) : '.') .join(''); } static void dispose() { _monitorTimer?.cancel(); _monitorTimer = null; _processedConnections.clear(); print('Protocol Blocking Service disposed'); } }