diff --git a/CHANGELOG.md b/CHANGELOG.md index cec919c..2ef69ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ 1. Removed example folder and added pubignore. 2. Added vendor details but database looks old. +3. More results are added using ARP on Lin/Mac/Win. ## 3.2.6 diff --git a/example/host_scan.dart b/example/host_scan.dart index ad42569..fed436e 100644 --- a/example/host_scan.dart +++ b/example/host_scan.dart @@ -1,7 +1,7 @@ import 'package:logging/logging.dart'; import '../lib/network_tools.dart'; -void main() { +void main() async { Logger.root.level = Level.FINE; Logger.root.onRecord.listen((record) { print( @@ -10,10 +10,16 @@ void main() { }); final log = Logger("host_scan_example"); - const String address = '192.168.1.1'; + String subnet = '192.168.0'; //Default network id for home networks + + final interface = await NetInterface.localInterface(); + final netId = interface?.networkId; + if (netId != null) { + subnet = netId; + } + // or You can also get address using network_info_plus package // final String? address = await (NetworkInfo().getWifiIP()); - final String subnet = address.substring(0, address.lastIndexOf('.')); log.fine("Starting scan on subnet $subnet"); // You can set [firstHostId] and scan will start from this host in the network. diff --git a/lib/network_tools.dart b/lib/network_tools.dart index 5f0d9f8..728501c 100644 --- a/lib/network_tools.dart +++ b/lib/network_tools.dart @@ -1,11 +1,13 @@ /// Network tools base library library network_tools; -//TODO: add dartdocs +export 'src/device_info/arp_table.dart'; +export 'src/device_info/net_interface.dart'; export 'src/device_info/vendor_table.dart'; export 'src/host_scanner.dart'; export 'src/mdns_scanner/mdns_scanner.dart'; export 'src/models/active_host.dart'; +export 'src/models/arp_data.dart'; export 'src/models/callbacks.dart'; export 'src/models/mdns_info.dart'; export 'src/models/open_port.dart'; diff --git a/lib/src/device_info/arp_table.dart b/lib/src/device_info/arp_table.dart new file mode 100644 index 0000000..6a86543 --- /dev/null +++ b/lib/src/device_info/arp_table.dart @@ -0,0 +1,100 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:logging/logging.dart'; +import 'package:network_tools/src/models/arp_data.dart'; +import 'package:path/path.dart'; +import 'package:sqflite_common_ffi/sqflite_ffi.dart'; + +class ARPTable { + static final arpLogger = Logger("arp-table-logger"); + static const String tableName = 'ARPPackets'; + static const String columnName = 'iPAddress'; + + static Future?> entries() async { + final list = (await (await _db()).query(tableName, columns: [columnName])) + .map((e) => ARPData.fromJson(e).iPAddress.toString()) + .toList(); + return list; + } + + static Future entryFor(String address) async { + arpLogger.fine('Trying to fetch arp table entry for $address'); + final entries = (await (await _db()) + .query(tableName, where: '$columnName = ?', whereArgs: [address])) + .map((e) => ARPData.fromJson(e)) + .toList(); + if (entries.isNotEmpty) { + return entries.first; + } + return null; + } + + static Future _db() async { + sqfliteFfiInit(); + final databaseFactory = databaseFactoryFfi; + final databasesPath = await databaseFactory.getDatabasesPath(); + final path = join(databasesPath, 'localarp.db'); + return databaseFactory.openDatabase( + path, + options: OpenDatabaseOptions( + version: 4, + singleInstance: false, + onCreate: (db, version) async { + await db.execute(''' + CREATE TABLE IF NOT EXISTS $tableName ( + host TEXT, + iPAddress TEXT PRIMARY KEY UNIQUE, + macAddress TEXT, + interfaceName TEXT, + interfaceType TEXT) + '''); + }, + ), + ); + } + + static Future buildTable() async { + final database = await _db(); + final result = await Process.run('arp', ['-a']); + final entries = const LineSplitter().convert(result.stdout.toString()); + RegExp? pattern; + if (Platform.isMacOS) { + pattern = RegExp( + r'(?[\w.?]*)\s\((?.*)\)\sat\s(?.*)\son\s(?\w+)\sifscope\s*(\w*)\s*\[(?.*)\]', + ); + } else if (Platform.isLinux) { + pattern = RegExp( + r'(?[\w.?]*)\s\((?.*)\)\sat\s(?.*)\s\[(?.*)\]\son\s(?\w+)', + ); + } else { + pattern = RegExp(r'(?.*)\s(?.*)\s(?.*)'); + } + + for (final entry in entries) { + final match = pattern.firstMatch(entry); + if (match != null) { + final arpData = ARPData( + host: match.groupNames.contains('host') + ? match.namedGroup("host") + : null, + iPAddress: match.namedGroup("ip"), + macAddress: match.namedGroup("mac"), + interfaceName: match.groupNames.contains('intf') + ? match.namedGroup("intf") + : null, + interfaceType: match.namedGroup("typ"), + ); + final key = arpData.iPAddress; + if (key != null && arpData.macAddress != '(incomplete)') { + arpLogger.fine("Adding entry to table -> $arpData"); + await database.insert( + tableName, + arpData.toJson(), + conflictAlgorithm: ConflictAlgorithm.replace, + ); + } + } + } + } +} diff --git a/lib/src/device_info/net_interface.dart b/lib/src/device_info/net_interface.dart new file mode 100644 index 0000000..f1f9ecb --- /dev/null +++ b/lib/src/device_info/net_interface.dart @@ -0,0 +1,37 @@ +import 'dart:io'; + +class NetInterface { + NetInterface({ + required this.networkId, + required this.hostId, + required this.ipAddress, + }); + + final String networkId; + final int hostId; + final String ipAddress; + + static Future localInterface() async { + final interfaceList = + await NetworkInterface.list(); //will give interface list + if (interfaceList.isNotEmpty) { + final localInterface = + interfaceList.first; //fetching first interface like en0/eth0 + if (localInterface.addresses.isNotEmpty) { + final address = localInterface.addresses + .elementAt(0) + .address; //gives IP address of GHA local machine. + final networkId = address.substring(0, address.lastIndexOf('.')); + final hostId = int.parse( + address.substring(address.lastIndexOf('.') + 1, address.length), + ); + return NetInterface( + networkId: networkId, + hostId: hostId, + ipAddress: address, + ); + } + } + return null; + } +} diff --git a/lib/src/device_info/vendor_table.dart b/lib/src/device_info/vendor_table.dart index ba39921..01e46da 100644 --- a/lib/src/device_info/vendor_table.dart +++ b/lib/src/device_info/vendor_table.dart @@ -33,7 +33,7 @@ class VendorTable { } static Future _fetchVendorTable(SendPort sendPort) async { - final input = File('lib/assets/mac-vendors-export.csv').openRead(); + final input = File('./lib/assets/mac-vendors-export.csv').openRead(); List> fields = await input .transform(utf8.decoder) diff --git a/lib/src/host_scanner.dart b/lib/src/host_scanner.dart index 426dc5d..a3244c6 100644 --- a/lib/src/host_scanner.dart +++ b/lib/src/host_scanner.dart @@ -3,6 +3,7 @@ import 'dart:isolate'; import 'dart:math'; import 'package:dart_ping/dart_ping.dart'; +import 'package:network_tools/src/device_info/arp_table.dart'; import 'package:network_tools/src/models/active_host.dart'; import 'package:network_tools/src/models/callbacks.dart'; import 'package:network_tools/src/models/sendable_active_host.dart'; @@ -31,6 +32,7 @@ class HostScanner { ProgressCallback? progressCallback, bool resultsInAddressAscendingOrder = true, }) async* { + await ARPTable.buildTable(); final stream = getAllSendablePingableDevices( subnet, firstHostId: firstHostId, @@ -70,7 +72,6 @@ class HostScanner { _getHostFromPing( activeHostsController: activeHostsController, host: '$subnet.$i', - i: i, timeoutInSeconds: timeoutInSeconds, ), ); @@ -97,24 +98,35 @@ class HostScanner { static Future _getHostFromPing({ required String host, - required int i, required StreamController activeHostsController, int timeoutInSeconds = 1, }) async { + SendableActiveHost? tempSendableActivateHost; await for (final PingData pingData in Ping(host, count: 1, timeout: timeoutInSeconds).stream) { final PingResponse? response = pingData.response; final PingError? pingError = pingData.error; - if (response != null && pingError == null) { - final Duration? time = response.time; - if (time != null) { - final tempSendableActivateHost = SendableActiveHost(host, pingData); - activeHostsController.add(tempSendableActivateHost); - return tempSendableActivateHost; + if (response != null) { + // Check if ping succeeded + if (pingError == null) { + final Duration? time = response.time; + if (time != null) { + tempSendableActivateHost = SendableActiveHost(host, pingData); + } } } + if (tempSendableActivateHost == null) { + // Check if it's there in arp table + final data = await ARPTable.entryFor(host); + if (data != null) { + tempSendableActivateHost = SendableActiveHost(host, pingData); + } + } + if (tempSendableActivateHost != null) { + activeHostsController.add(tempSendableActivateHost); + } } - return null; + return tempSendableActivateHost; } static int validateAndGetLastValidSubnet( @@ -143,6 +155,7 @@ class HostScanner { ProgressCallback? progressCallback, bool resultsInAddressAscendingOrder = true, }) async* { + await ARPTable.buildTable(); const int scanRangeForIsolate = 51; final int lastValidSubnet = validateAndGetLastValidSubnet(subnet, firstHostId, lastHostId); @@ -168,6 +181,7 @@ class HostScanner { } else if (message is SendableActiveHost) { progressCallback ?.call((i - firstHostId) * 100 / (lastValidSubnet - firstHostId)); + // print('Address ${message.address}'); final activeHostFound = ActiveHost.fromSendableActiveHost(sendableActiveHost: message); await activeHostFound.resolveInfo(); diff --git a/lib/src/models/active_host.dart b/lib/src/models/active_host.dart index 31d43a1..8621e58 100644 --- a/lib/src/models/active_host.dart +++ b/lib/src/models/active_host.dart @@ -1,7 +1,5 @@ import 'package:dart_ping/dart_ping.dart'; import 'package:network_tools/network_tools.dart'; -import 'package:network_tools/src/models/arp_data.dart'; -import 'package:network_tools/src/models/arp_table.dart'; import 'package:network_tools/src/network_tools_utils.dart'; import 'package:universal_io/io.dart'; @@ -29,7 +27,6 @@ class ActiveHost extends Comparable { } else { hostId = '-1'; } - pingData ??= getPingData(tempAddress); _pingData = pingData; @@ -46,6 +43,7 @@ class ActiveHost extends Comparable { } deviceName = setDeviceName(); + // fetch entry from in memory arp table arpData = setARPData(); @@ -73,7 +71,6 @@ class ActiveHost extends Comparable { factory ActiveHost.fromSendableActiveHost({ required SendableActiveHost sendableActiveHost, - List openPorts = const [], MdnsInfo? mdnsInfo, }) { final InternetAddress? internetAddressTemp = @@ -83,7 +80,7 @@ class ActiveHost extends Comparable { } return ActiveHost( internetAddress: internetAddressTemp, - openPorts: openPorts, + openPorts: [], pingData: sendableActiveHost.pingData, mdnsInfoVar: mdnsInfo, ); diff --git a/lib/src/models/arp_data.dart b/lib/src/models/arp_data.dart index 85f300d..d21cdfd 100644 --- a/lib/src/models/arp_data.dart +++ b/lib/src/models/arp_data.dart @@ -1,5 +1,9 @@ import 'dart:io'; +import 'package:json_annotation/json_annotation.dart'; +part 'arp_data.g.dart'; + +@JsonSerializable() class ARPData { ARPData({ required this.host, @@ -8,6 +12,8 @@ class ARPData { required this.interfaceName, required this.interfaceType, }); + factory ARPData.fromJson(Map json) => + _$ARPDataFromJson(json); final String? host; final String? iPAddress; @@ -15,6 +21,8 @@ class ARPData { final String? interfaceName; final String? interfaceType; + Map toJson() => _$ARPDataToJson(this); + @override String toString() { if (Platform.isMacOS) { diff --git a/lib/src/models/arp_data.g.dart b/lib/src/models/arp_data.g.dart new file mode 100644 index 0000000..126c34c --- /dev/null +++ b/lib/src/models/arp_data.g.dart @@ -0,0 +1,23 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'arp_data.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +ARPData _$ARPDataFromJson(Map json) => ARPData( + host: json['host'] as String?, + iPAddress: json['iPAddress'] as String?, + macAddress: json['macAddress'] as String?, + interfaceName: json['interfaceName'] as String?, + interfaceType: json['interfaceType'] as String?, + ); + +Map _$ARPDataToJson(ARPData instance) => { + 'host': instance.host, + 'iPAddress': instance.iPAddress, + 'macAddress': instance.macAddress, + 'interfaceName': instance.interfaceName, + 'interfaceType': instance.interfaceType, + }; diff --git a/lib/src/models/arp_table.dart b/lib/src/models/arp_table.dart deleted file mode 100644 index 6f4ae1a..0000000 --- a/lib/src/models/arp_table.dart +++ /dev/null @@ -1,53 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; - -import 'package:logging/logging.dart'; -import 'package:network_tools/src/models/arp_data.dart'; - -class ARPTable { - static final arpLogger = Logger("arp-table-logger"); - static final arpTable = {}; - static bool resolved = false; - - static Future entryFor(String address) async { - if (resolved) return arpTable[address]; - final result = await Process.run('arp', ['-a']); - final entries = const LineSplitter().convert(result.stdout.toString()); - RegExp? pattern; - if (Platform.isMacOS) { - pattern = RegExp( - r'(?[\w.?]*)\s\((?.*)\)\sat\s(?.*)\son\s(?\w+)\sifscope\s*(\w*)\s*\[(?.*)\]', - ); - } else if (Platform.isLinux) { - pattern = RegExp( - r'(?[\w.?]*)\s\((?.*)\)\sat\s(?.*)\s\[(?.*)\]\son\s(?\w+)', - ); - } else { - pattern = RegExp(r'(?.*)\s(?.*)\s(?.*)'); - } - - for (final entry in entries) { - final match = pattern.firstMatch(entry); - if (match != null) { - final arpData = ARPData( - host: match.groupNames.contains('host') - ? match.namedGroup("host") - : null, - iPAddress: match.namedGroup("ip"), - macAddress: match.namedGroup("mac"), - interfaceName: match.groupNames.contains('intf') - ? match.namedGroup("intf") - : null, - interfaceType: match.namedGroup("typ"), - ); - final key = arpData.iPAddress; - if (key != null) { - arpLogger.fine("Adding entry to table -> $arpData"); - arpTable[key] = arpData; - } - } - } - resolved = true; - return arpTable[address]; - } -} diff --git a/pubspec.yaml b/pubspec.yaml index f88835a..f870e15 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -23,16 +23,27 @@ dependencies: csv: ^5.0.2 # Multi-platform network ping utility. dart_ping: ^9.0.0 + # Defines the annotations used by json_serializable + json_annotation: ^4.8.1 # Debugging and error logging. logging: ^1.2.0 # Performing mDNS queries (e.g. Bonjour, Avahi). multicast_dns: ^0.3.2+2 + # A comprehensive, cross-platform path manipulation library for Dart. + path: ^1.8.3 # Process run helpers process_run: ^0.13.1 + # sqflite based ffi implementation + sqflite_common_ffi: ^2.3.0+2 # Cross-platform 'dart:io' that works in all platforms. universal_io: ^2.0.4 + dev_dependencies: + # Standalone generator and watcher for Dart + build_runner: ^2.4.6 + # Provides Dart Build System builders for handling JSON. + json_serializable: ^6.7.1 # Set of lint rules for Dart. lint: ^2.1.2 # Writing and running Dart tests. diff --git a/test/network_tools_test.dart b/test/network_tools_test.dart index 3045751..a462192 100644 --- a/test/network_tools_test.dart +++ b/test/network_tools_test.dart @@ -31,37 +31,28 @@ void main() { await ServerSocket.bind(InternetAddress.anyIPv4, port, shared: true); port = server.port; log.fine("Opened port in this machine at $port"); - final interfaceList = - await NetworkInterface.list(); //will give interface list - if (interfaceList.isNotEmpty) { - final localInterface = - interfaceList.elementAt(0); //fetching first interface like en0/eth0 - if (localInterface.addresses.isNotEmpty) { - final address = localInterface.addresses - .elementAt(0) - .address; //gives IP address of GHA local machine. - myOwnHost = address; - interfaceIp = address.substring(0, address.lastIndexOf('.')); - final hostId = int.parse( - address.substring(address.lastIndexOf('.') + 1, address.length), - ); - // Better to restrict to scan from hostId - 1 to hostId + 1 to prevent GHA timeouts - firstHostId = hostId <= 1 ? hostId : hostId - 1; - lastHostId = hostId >= 254 ? hostId : hostId + 1; - await for (final host - in HostScanner.scanDevicesForSinglePort(interfaceIp, port)) { - hostsWithOpenPort.add(host); - for (final tempOpenPort in host.openPorts) { - if (tempOpenPort.port == port) { - openPort = tempOpenPort; - break; - } + + final interface = await NetInterface.localInterface(); + if (interface != null) { + final hostId = interface.hostId; + interfaceIp = interface.networkId; + myOwnHost = interface.ipAddress; + // Better to restrict to scan from hostId - 1 to hostId + 1 to prevent GHA timeouts + firstHostId = hostId <= 1 ? hostId : hostId - 1; + lastHostId = hostId >= 254 ? hostId : hostId + 1; + await for (final host + in HostScanner.scanDevicesForSinglePort(interfaceIp, port)) { + hostsWithOpenPort.add(host); + for (final tempOpenPort in host.openPorts) { + if (tempOpenPort.port == port) { + openPort = tempOpenPort; + break; } } - log.fine( - 'Fetched own host as $myOwnHost and interface address as $interfaceIp', - ); } + log.fine( + 'Fetched own host as $myOwnHost and interface address as $interfaceIp', + ); } }); @@ -102,7 +93,11 @@ void main() { firstHostId: firstHostId, lastHostId: lastHostId, ), - emitsThrough(ActiveHost(internetAddress: InternetAddress(myOwnHost))), + emitsThrough( + ActiveHost( + internetAddress: InternetAddress(myOwnHost), + ), + ), ); }, ); @@ -128,7 +123,11 @@ void main() { firstHostId: firstHostId, lastHostId: lastHostId, ), - emitsThrough(ActiveHost(internetAddress: InternetAddress(myOwnHost))), + emitsThrough( + ActiveHost( + internetAddress: InternetAddress(myOwnHost), + ), + ), ); }, );