- Detect TCP SYN packets and protocol-specific handshakes - Print raw hex data in readable hex dump format with ASCII - Show actual packet contents for signature analysis
835 lines
29 KiB
Dart
835 lines
29 KiB
Dart
import 'dart:async';
|
||
import 'dart:convert';
|
||
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 ALL interfaces
|
||
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();
|
||
}
|
||
if (_scanCount == 100) {
|
||
await _debugNetworking();
|
||
}
|
||
}
|
||
} catch (e) {
|
||
print('❌ Error monitoring peer traffic: $e');
|
||
}
|
||
}
|
||
|
||
static Process? _continuousMonitor;
|
||
static StreamSubscription? _monitorSubscription;
|
||
|
||
static Future<void> _monitorPeerTraffic() async {
|
||
// Start continuous monitoring if not already running
|
||
if (_continuousMonitor == null) {
|
||
await _startContinuousMonitoring();
|
||
}
|
||
}
|
||
|
||
static Future<void> _startContinuousMonitoring() async {
|
||
try {
|
||
print('🔄 Starting continuous PAYLOAD monitoring...');
|
||
_continuousMonitor = await Process.start('tcpdump', [
|
||
'-i', 'any',
|
||
'-l', // Line buffered for real-time output
|
||
'-s', '0', // Capture full packet (unlimited snap length)
|
||
'-x', // Output in hex format for payload analysis
|
||
// Only capture forwarded packets from VPN peers to internet
|
||
'src net 10.0.0.0/24 and not dst net 10.0.0.0/24',
|
||
]);
|
||
|
||
_monitorSubscription = _continuousMonitor!.stdout
|
||
.transform(utf8.decoder)
|
||
.transform(LineSplitter())
|
||
.listen((line) async {
|
||
if (line.trim().isNotEmpty) {
|
||
await _processCapturedPacket(line.trim());
|
||
}
|
||
});
|
||
|
||
// Handle process errors
|
||
_continuousMonitor!.stderr
|
||
.transform(utf8.decoder)
|
||
.listen((error) {
|
||
if (!error.contains('listening on') && !error.contains('data link type') && !error.contains('verbose output suppressed')) {
|
||
print('❌ tcpdump error: $error');
|
||
} else {
|
||
print('ℹ️ tcpdump info: $error');
|
||
}
|
||
});
|
||
|
||
} catch (e) {
|
||
print('❌ Failed to start continuous monitoring: $e');
|
||
}
|
||
}
|
||
|
||
static String? _currentPeerIP;
|
||
static final List<String> _currentPacketHex = [];
|
||
|
||
static Future<void> _processCapturedPacket(String packetLine) async {
|
||
// Check if this is a new packet header (timestamp line)
|
||
if (packetLine.contains('IP ') && !packetLine.contains('0x')) {
|
||
// If we have accumulated hex data from previous packet, analyze it
|
||
if (_currentPeerIP != null && _currentPacketHex.isNotEmpty) {
|
||
await _analyzeAccumulatedHex(_currentPeerIP!, _currentPacketHex.join('\n'));
|
||
}
|
||
|
||
// Reset for new packet
|
||
_currentPacketHex.clear();
|
||
_currentPeerIP = null;
|
||
|
||
// Check if new packet is from our monitored peers
|
||
for (final ip in _activePeerIPs) {
|
||
if (packetLine.contains(ip)) {
|
||
_currentPeerIP = ip;
|
||
print('🎯 NEW PACKET FROM PEER $ip');
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
// If this is hex data and we're tracking a peer packet
|
||
else if (packetLine.contains('0x') && _currentPeerIP != null) {
|
||
_currentPacketHex.add(packetLine);
|
||
print('📦 Collecting hex data for ${_currentPeerIP}: ${_currentPacketHex.length} lines');
|
||
}
|
||
}
|
||
|
||
static Future<void> _analyzeAccumulatedHex(String peerIP, String hexData) async {
|
||
print('🔍 ANALYZING COMPLETE PACKET FROM $peerIP');
|
||
print('📊 Hex data lines: ${_currentPacketHex.length}');
|
||
await _analyzeFullPayload(hexData, peerIP);
|
||
}
|
||
|
||
static Future<void> _analyzeFullPayload(String hexPacketLine, String peerIP) async {
|
||
// Extract hex bytes from tcpdump output
|
||
final hexBytes = _extractHexBytes(hexPacketLine);
|
||
|
||
if (hexBytes.isEmpty) {
|
||
return;
|
||
}
|
||
|
||
// Check if this looks like a handshake (first packet of connection)
|
||
if (_isHandshakePacket(hexBytes)) {
|
||
print('🤝 HANDSHAKE DETECTED FROM $peerIP');
|
||
print('📦 RAW HEX DATA:');
|
||
|
||
// Print hex data in readable format
|
||
for (int i = 0; i < hexBytes.length; i += 16) {
|
||
final chunk = hexBytes.skip(i).take(16);
|
||
final hexLine = chunk.map((h) => h.padLeft(2, '0')).join(' ');
|
||
final asciiLine = chunk.map((h) {
|
||
final byte = int.parse(h, radix: 16);
|
||
return (byte >= 32 && byte <= 126) ? String.fromCharCode(byte) : '.';
|
||
}).join();
|
||
|
||
print('${i.toString().padLeft(4, '0')}: ${hexLine.padRight(48)} |$asciiLine|');
|
||
}
|
||
|
||
// Also show raw ASCII if readable
|
||
final asciiData = _extractAsciiFromHex(hexBytes);
|
||
final readableAscii = asciiData.replaceAll(RegExp(r'[^\x20-\x7E]'), '.');
|
||
if (readableAscii.contains(RegExp(r'[a-zA-Z]'))) {
|
||
print('📝 ASCII: $readableAscii');
|
||
}
|
||
|
||
print('═══════════════════════════════════════');
|
||
}
|
||
}
|
||
|
||
static Future<void> _fallbackPayloadAnalysis(List<String> hexBytes, String peerIP) async {
|
||
print('🔍 FALLBACK: Manual payload signature detection');
|
||
|
||
// Convert hex to ASCII for pattern matching
|
||
final asciiData = _extractAsciiFromHex(hexBytes);
|
||
final hexString = hexBytes.join('').toLowerCase();
|
||
|
||
final cleanAscii = asciiData.replaceAll(RegExp(r'[^\x20-\x7E]'), '.');
|
||
final sampleLength = cleanAscii.length < 100 ? cleanAscii.length : 100;
|
||
print('🔤 ASCII sample: ${cleanAscii.substring(0, sampleLength)}${cleanAscii.length > 100 ? '...' : ''}');
|
||
|
||
String? detectedProtocol;
|
||
|
||
// BitTorrent handshake signature: "BitTorrent protocol" (hex: 426974546f7272656e742070726f746f636f6c)
|
||
if (asciiData.contains('BitTorrent protocol') ||
|
||
hexString.contains('13426974546f7272656e742070726f746f636f6c')) {
|
||
detectedProtocol = 'BitTorrent';
|
||
print('🚨 BitTorrent handshake signature found in payload!');
|
||
}
|
||
// SSH handshake
|
||
else if (asciiData.contains('SSH-2.0') || asciiData.contains('SSH-1.')) {
|
||
detectedProtocol = 'SSH';
|
||
print('🔐 SSH handshake signature found in payload!');
|
||
}
|
||
// HTTP signatures
|
||
else if (asciiData.contains('GET ') || asciiData.contains('POST ') ||
|
||
asciiData.contains('HTTP/1.1') || asciiData.contains('HTTP/2')) {
|
||
detectedProtocol = 'HTTP';
|
||
print('🌐 HTTP signature found in payload!');
|
||
}
|
||
// TLS Client Hello (0x16 0x03 0x01/0x03)
|
||
else if (hexString.startsWith('16') && (hexString.startsWith('160301') || hexString.startsWith('160303'))) {
|
||
detectedProtocol = 'TLS';
|
||
print('🔒 TLS handshake signature found in payload!');
|
||
}
|
||
|
||
if (detectedProtocol != null) {
|
||
print('🎯 PAYLOAD SIGNATURE DETECTED: $detectedProtocol');
|
||
|
||
if (['BitTorrent'].contains(detectedProtocol)) {
|
||
print('🚫 BLOCKING PAYLOAD-DETECTED PROTOCOL: $detectedProtocol');
|
||
print('💡 This would be blocked regardless of port!');
|
||
} else {
|
||
print('✅ ALLOWING PAYLOAD-DETECTED PROTOCOL: $detectedProtocol');
|
||
}
|
||
} else {
|
||
print('❓ UNKNOWN PROTOCOL - No payload signatures matched');
|
||
print('🔍 First 32 hex bytes: ${hexBytes.take(32).join(' ')}');
|
||
}
|
||
}
|
||
|
||
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', 'any',
|
||
'-c', '3', // Capture a few packets to get handshake
|
||
'-s', '200',
|
||
'-x',
|
||
// Capture decrypted traffic from peer to internet for handshake analysis
|
||
'src $peerIP and not dst net 10.0.0.0/24',
|
||
]);
|
||
|
||
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;
|
||
}
|
||
|
||
print('🔍 Testing live traffic capture for peer $peerIP...');
|
||
|
||
// Test very basic traffic capture from peer - longer timeout
|
||
final process = await Process.start('timeout', [
|
||
'10', // Longer timeout to catch browsing traffic
|
||
'tcpdump',
|
||
'-i', 'wg0',
|
||
'-c', '5', // Capture multiple packets
|
||
'-v', // Verbose output
|
||
'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('✅ LIVE TRAFFIC DETECTED from peer $peerIP:');
|
||
print('${output.join().trim()}');
|
||
print('───────────────────────────────────────');
|
||
} else {
|
||
print('❌ No traffic detected from peer $peerIP in 10 seconds');
|
||
print(' Try browsing a website now...');
|
||
|
||
// Also test ANY traffic on wg0
|
||
print('🔍 Testing if wg0 interface has ANY traffic...');
|
||
final anyTrafficProcess = await Process.start('timeout', [
|
||
'5',
|
||
'tcpdump',
|
||
'-i', 'wg0',
|
||
'-c', '3',
|
||
]);
|
||
|
||
final anyOutput = <String>[];
|
||
await for (final data in anyTrafficProcess.stdout) {
|
||
anyOutput.add(String.fromCharCodes(data));
|
||
}
|
||
|
||
final anyExitCode = await anyTrafficProcess.exitCode;
|
||
anyTrafficProcess.kill();
|
||
|
||
if (anyExitCode == 0 && anyOutput.isNotEmpty) {
|
||
print('✅ wg0 has traffic, but not from expected peer IP:');
|
||
print('${anyOutput.join().trim()}');
|
||
} else {
|
||
print('❌ No traffic at all on wg0 interface');
|
||
}
|
||
}
|
||
} catch (e) {
|
||
print('❌ Error testing peer $peerIP connectivity: $e');
|
||
}
|
||
}
|
||
}
|
||
|
||
static Future<void> _debugNetworking() async {
|
||
print('🔧 NETWORK DEBUGGING...');
|
||
|
||
try {
|
||
// Check all network interfaces
|
||
print('📡 Available network interfaces:');
|
||
final ifaceResult = await Process.run('ip', ['link', 'show']);
|
||
if (ifaceResult.exitCode == 0) {
|
||
print(ifaceResult.stdout.toString().trim());
|
||
}
|
||
|
||
// Check specifically for wg0
|
||
print('\n🔍 WireGuard wg0 interface details:');
|
||
final wg0Result = await Process.run('ip', ['addr', 'show', 'wg0']);
|
||
if (wg0Result.exitCode == 0) {
|
||
print(wg0Result.stdout.toString().trim());
|
||
} else {
|
||
print('❌ wg0 interface not found: ${wg0Result.stderr}');
|
||
}
|
||
|
||
// Check WireGuard status
|
||
print('\n📋 WireGuard peer status:');
|
||
final wgResult = await Process.run('wg', ['show']);
|
||
if (wgResult.exitCode == 0) {
|
||
print(wgResult.stdout.toString().trim());
|
||
}
|
||
|
||
// Try tcpdump on different interfaces
|
||
print('\n🕵️ Testing packet capture capabilities:');
|
||
|
||
// Test Docker internal interface
|
||
final dockerTest = await Process.run('timeout', ['2', 'tcpdump', '-i', 'any', '-c', '1']);
|
||
if (dockerTest.exitCode == 0 && dockerTest.stdout.toString().isNotEmpty) {
|
||
print('✅ Can capture on "any" interface');
|
||
print(' Sample: ${dockerTest.stdout.toString().trim()}');
|
||
} else {
|
||
print('❌ Cannot capture on "any" interface');
|
||
print(' Error: ${dockerTest.stderr}');
|
||
}
|
||
|
||
// Test if we need different interface name
|
||
final ethTest = await Process.run('timeout', ['2', 'tcpdump', '-i', 'eth0', '-c', '1']);
|
||
if (ethTest.exitCode == 0 && ethTest.stdout.toString().isNotEmpty) {
|
||
print('✅ Traffic detected on eth0');
|
||
print(' Sample: ${ethTest.stdout.toString().trim()}');
|
||
}
|
||
|
||
} catch (e) {
|
||
print('❌ Network debugging error: $e');
|
||
}
|
||
|
||
print('🔧 Network debugging complete\n');
|
||
}
|
||
|
||
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 Future<void> _analyzeHandshake(String handshakeData, Connection conn) async {
|
||
print('════════════════ nDPI PROTOCOL ANALYSIS ════════════════');
|
||
print('📍 Connection: $conn');
|
||
|
||
// Extract hex bytes from tcpdump output
|
||
final hexBytes = _extractHexBytes(handshakeData);
|
||
|
||
if (hexBytes.isEmpty) {
|
||
print('❌ No hex data found in tcpdump output');
|
||
print('══════════════════════════════════════════════════════════════');
|
||
return;
|
||
}
|
||
|
||
// Convert hex bytes to single hex string for C analyzer
|
||
final hexString = hexBytes.join('');
|
||
print('🔢 Analyzing ${hexBytes.length} bytes of packet data');
|
||
|
||
try {
|
||
// First check if the protocol analyzer exists
|
||
final analyzerCheck = await Process.run('ls', ['-la', './protocol_analyzer']);
|
||
if (analyzerCheck.exitCode != 0) {
|
||
print('⚠️ nDPI protocol analyzer not available - using basic pattern matching');
|
||
await _basicProtocolAnalysis(hexBytes, conn);
|
||
return;
|
||
}
|
||
|
||
// Call our C nDPI analyzer
|
||
final result = await Process.run('./protocol_analyzer', [hexString]);
|
||
|
||
if (result.exitCode == 0) {
|
||
print('✅ nDPI Analysis Results:');
|
||
print(result.stdout.toString().trim());
|
||
|
||
// Parse JSON output to extract protocol info
|
||
try {
|
||
final jsonStr = result.stdout.toString().trim();
|
||
// Simple protocol extraction - look for protocol field
|
||
final protocolMatch = RegExp(r'"protocol":\s*"([^"]+)"').firstMatch(jsonStr);
|
||
final categoryMatch = RegExp(r'"category":\s*"([^"]+)"').firstMatch(jsonStr);
|
||
|
||
if (protocolMatch != null) {
|
||
final protocol = protocolMatch.group(1) ?? 'Unknown';
|
||
final category = categoryMatch?.group(1) ?? 'Unknown';
|
||
|
||
print('🎯 DETECTED: $protocol (Category: $category)');
|
||
|
||
// Check if this is a protocol we want to block
|
||
if (_shouldBlockProtocol(protocol, category)) {
|
||
print('🚫 BLOCKING PROTOCOL: $protocol');
|
||
// TODO: Implement blocking logic here
|
||
} else {
|
||
print('✅ ALLOWING PROTOCOL: $protocol');
|
||
}
|
||
}
|
||
} catch (e) {
|
||
print('⚠️ Error parsing nDPI results: $e');
|
||
}
|
||
|
||
} else {
|
||
print('❌ nDPI analyzer failed:');
|
||
print(' Exit code: ${result.exitCode}');
|
||
print(' Error: ${result.stderr}');
|
||
}
|
||
} catch (e) {
|
||
print('❌ Error running nDPI analyzer: $e');
|
||
}
|
||
|
||
print('══════════════════════════════════════════════════════════════');
|
||
}
|
||
|
||
static bool _shouldBlockProtocol(String protocol, String category) {
|
||
// Define protocols/categories to block
|
||
final blockedProtocols = {
|
||
'BitTorrent', 'uTorrent', 'Transmission', 'qBittorrent',
|
||
'eMule', 'KaZaA', 'Gnutella', 'DirectConnect',
|
||
'Skype_Call', // Block Skype calls but allow chat
|
||
};
|
||
|
||
final blockedCategories = {
|
||
'Download', 'P2P', 'FileSharing'
|
||
};
|
||
|
||
return blockedProtocols.contains(protocol) ||
|
||
blockedCategories.contains(category);
|
||
}
|
||
|
||
static Future<void> _basicProtocolAnalysis(List<String> hexBytes, Connection conn) async {
|
||
print('🔍 Using basic pattern matching fallback');
|
||
|
||
// Convert hex to ASCII for pattern matching
|
||
final asciiData = _extractAsciiFromHex(hexBytes);
|
||
final hexString = hexBytes.join('').toLowerCase();
|
||
|
||
String? detectedProtocol;
|
||
|
||
// BitTorrent detection
|
||
if (asciiData.contains('BitTorrent protocol') || hexString.contains('13426974546f7272656e742070726f746f636f6c')) {
|
||
detectedProtocol = 'BitTorrent';
|
||
}
|
||
// SSH detection
|
||
else if (asciiData.contains('SSH-2.0') || asciiData.contains('SSH-1.')) {
|
||
detectedProtocol = 'SSH';
|
||
}
|
||
// HTTP detection
|
||
else if (asciiData.toLowerCase().contains('get ') || asciiData.toLowerCase().contains('post ') || asciiData.toLowerCase().contains('http/')) {
|
||
detectedProtocol = 'HTTP';
|
||
}
|
||
// TLS/SSL detection (0x16 = handshake record type)
|
||
else if (hexBytes.isNotEmpty && hexBytes.first.toLowerCase() == '16') {
|
||
detectedProtocol = 'TLS/SSL';
|
||
}
|
||
// SMTP detection
|
||
else if (asciiData.contains('220 ') && conn.remotePort == 25) {
|
||
detectedProtocol = 'SMTP';
|
||
}
|
||
|
||
if (detectedProtocol != null) {
|
||
print('🎯 BASIC DETECTION: $detectedProtocol');
|
||
|
||
// Simple blocking logic for basic patterns
|
||
if (['BitTorrent', 'eMule'].contains(detectedProtocol)) {
|
||
print('🚫 BLOCKING PROTOCOL: $detectedProtocol');
|
||
} else {
|
||
print('✅ ALLOWING PROTOCOL: $detectedProtocol');
|
||
}
|
||
} else {
|
||
// Check for BitTorrent by traffic patterns
|
||
if (_analyzeTrafficPatterns(conn)) {
|
||
detectedProtocol = 'BitTorrent (Pattern Analysis)';
|
||
} else {
|
||
print('❓ UNKNOWN PROTOCOL (basic analysis)');
|
||
print('🔤 ASCII sample: ${asciiData.replaceAll(RegExp(r'[^\x20-\x7E]'), '.').substring(0, 50)}...');
|
||
}
|
||
}
|
||
|
||
if (detectedProtocol != null) {
|
||
print('🎯 BASIC DETECTION: $detectedProtocol');
|
||
|
||
// Simple blocking logic for basic patterns
|
||
if (['BitTorrent', 'BitTorrent (Pattern Analysis)', 'eMule'].contains(detectedProtocol)) {
|
||
print('🚫 BLOCKING PROTOCOL: $detectedProtocol');
|
||
} else {
|
||
print('✅ ALLOWING PROTOCOL: $detectedProtocol');
|
||
}
|
||
}
|
||
}
|
||
|
||
static final Map<String, List<DateTime>> _recentConnections = {};
|
||
|
||
static bool _analyzeTrafficPatterns(Connection conn) {
|
||
final now = DateTime.now();
|
||
final peerIP = conn.localIP;
|
||
|
||
// Track recent connections from this peer
|
||
if (!_recentConnections.containsKey(peerIP)) {
|
||
_recentConnections[peerIP] = [];
|
||
}
|
||
|
||
// Clean old connections (older than 2 minutes)
|
||
_recentConnections[peerIP]!.removeWhere((time) =>
|
||
now.difference(time).inMinutes > 2);
|
||
|
||
_recentConnections[peerIP]!.add(now);
|
||
|
||
final connectionCount = _recentConnections[peerIP]!.length;
|
||
|
||
// BitTorrent pattern: Multiple connections in short time
|
||
if (connectionCount >= 3) {
|
||
print('🔍 Pattern Analysis: $connectionCount connections from $peerIP in 2 minutes');
|
||
print('🚨 SUSPICIOUS: Multiple rapid connections typical of P2P protocols');
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
static bool _isHandshakePacket(List<String> hexBytes) {
|
||
if (hexBytes.length < 20) return false;
|
||
|
||
// Check for TCP SYN packet (handshake initiation)
|
||
if (hexBytes.length >= 34) {
|
||
// IP header is first 20 bytes, TCP header starts at byte 20
|
||
// TCP flags are at offset 33 (13th byte of TCP header)
|
||
try {
|
||
final tcpFlags = int.parse(hexBytes[33], radix: 16);
|
||
// Check for SYN flag (0x02) - indicates handshake
|
||
if (tcpFlags & 0x02 != 0) {
|
||
return true;
|
||
}
|
||
} catch (e) {
|
||
// Ignore parsing errors
|
||
}
|
||
}
|
||
|
||
// Check for common handshake patterns in payload
|
||
final asciiData = _extractAsciiFromHex(hexBytes);
|
||
|
||
// BitTorrent handshake starts with 19 + "BitTorrent protocol"
|
||
if (hexBytes.length >= 20 && hexBytes[0] == '13') {
|
||
return true;
|
||
}
|
||
|
||
// HTTP requests
|
||
if (asciiData.startsWith('GET ') || asciiData.startsWith('POST ') ||
|
||
asciiData.startsWith('HEAD ') || asciiData.startsWith('PUT ')) {
|
||
return true;
|
||
}
|
||
|
||
// TLS Client Hello
|
||
if (hexBytes.length >= 6 && hexBytes[0] == '16' && hexBytes[1] == '03') {
|
||
return true;
|
||
}
|
||
|
||
// SSH handshake
|
||
if (asciiData.startsWith('SSH-')) {
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
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');
|
||
}
|
||
} |