439 lines
14 KiB
Dart
439 lines
14 KiB
Dart
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<String> _processedConnections = {};
|
|
static Set<String> _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<void> _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<void> _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');
|
|
// Test if we can see ANY traffic from peers
|
|
if (_scanCount == 100) {
|
|
await _testPeerConnectivity();
|
|
}
|
|
}
|
|
} catch (e) {
|
|
print('❌ Error monitoring peer traffic: $e');
|
|
}
|
|
}
|
|
|
|
static Future<void> _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 = <String>[];
|
|
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<void> _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<void> _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 = <String>[];
|
|
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 Future<void> _testPeerConnectivity() async {
|
|
print('🧪 Testing peer connectivity on wg0 interface...');
|
|
for (final peerIP in _activePeerIPs) {
|
|
try {
|
|
// Check if wg0 interface exists and peer can be seen
|
|
final wgResult = await Process.run('ip', ['addr', 'show', 'wg0']);
|
|
if (wgResult.exitCode == 0) {
|
|
print('✅ wg0 interface exists');
|
|
} else {
|
|
print('❌ wg0 interface not found');
|
|
return;
|
|
}
|
|
|
|
// Test very basic traffic capture from peer
|
|
final process = await Process.start('timeout', [
|
|
'3',
|
|
'tcpdump',
|
|
'-i', 'wg0',
|
|
'-c', '1',
|
|
'host $peerIP',
|
|
]);
|
|
|
|
final output = <String>[];
|
|
await for (final data in process.stdout) {
|
|
output.add(String.fromCharCodes(data));
|
|
}
|
|
|
|
final exitCode = await process.exitCode;
|
|
process.kill();
|
|
|
|
if (exitCode == 0 && output.isNotEmpty) {
|
|
print('✅ Can see traffic from peer $peerIP on wg0');
|
|
print(' Sample: ${output.join().trim()}');
|
|
} else {
|
|
print('❌ No traffic detected from peer $peerIP on wg0 in 3 seconds');
|
|
print(' This suggests the peer is inactive or traffic routing issue');
|
|
}
|
|
} catch (e) {
|
|
print('❌ Error testing peer $peerIP connectivity: $e');
|
|
}
|
|
}
|
|
}
|
|
|
|
static List<Connection> _parseConnections(String output, String protocol) {
|
|
final connections = <Connection>[];
|
|
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<void> _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<void> _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 = <String>[];
|
|
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<String> _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 = <String>[];
|
|
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<String> 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');
|
|
}
|
|
} |