Skip to content

Commit

Permalink
Merge pull request #145 from osociety/arp-table-improved
Browse files Browse the repository at this point in the history
More devices on scan using ARP (Lin/Mac/Win)
  • Loading branch information
git-elliot authored Sep 18, 2023
2 parents 02f0b76 + a290b66 commit c59593d
Show file tree
Hide file tree
Showing 13 changed files with 247 additions and 102 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
12 changes: 9 additions & 3 deletions example/host_scan.dart
Original file line number Diff line number Diff line change
@@ -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(
Expand All @@ -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.
Expand Down
4 changes: 3 additions & 1 deletion lib/network_tools.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
100 changes: 100 additions & 0 deletions lib/src/device_info/arp_table.dart
Original file line number Diff line number Diff line change
@@ -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<List<String>?> entries() async {
final list = (await (await _db()).query(tableName, columns: [columnName]))
.map((e) => ARPData.fromJson(e).iPAddress.toString())
.toList();
return list;
}

static Future<ARPData?> 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<Database> _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<void> 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'(?<host>[\w.?]*)\s\((?<ip>.*)\)\sat\s(?<mac>.*)\son\s(?<intf>\w+)\sifscope\s*(\w*)\s*\[(?<typ>.*)\]',
);
} else if (Platform.isLinux) {
pattern = RegExp(
r'(?<host>[\w.?]*)\s\((?<ip>.*)\)\sat\s(?<mac>.*)\s\[(?<typ>.*)\]\son\s(?<intf>\w+)',
);
} else {
pattern = RegExp(r'(?<ip>.*)\s(?<mac>.*)\s(?<typ>.*)');
}

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,
);
}
}
}
}
}
37 changes: 37 additions & 0 deletions lib/src/device_info/net_interface.dart
Original file line number Diff line number Diff line change
@@ -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<NetInterface?> 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;
}
}
2 changes: 1 addition & 1 deletion lib/src/device_info/vendor_table.dart
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ class VendorTable {
}

static Future<void> _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<List<dynamic>> fields = await input
.transform(utf8.decoder)
Expand Down
32 changes: 23 additions & 9 deletions lib/src/host_scanner.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -31,6 +32,7 @@ class HostScanner {
ProgressCallback? progressCallback,
bool resultsInAddressAscendingOrder = true,
}) async* {
await ARPTable.buildTable();
final stream = getAllSendablePingableDevices(
subnet,
firstHostId: firstHostId,
Expand Down Expand Up @@ -70,7 +72,6 @@ class HostScanner {
_getHostFromPing(
activeHostsController: activeHostsController,
host: '$subnet.$i',
i: i,
timeoutInSeconds: timeoutInSeconds,
),
);
Expand All @@ -97,24 +98,35 @@ class HostScanner {

static Future<SendableActiveHost?> _getHostFromPing({
required String host,
required int i,
required StreamController<SendableActiveHost> 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(
Expand Down Expand Up @@ -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);
Expand All @@ -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();
Expand Down
7 changes: 2 additions & 5 deletions lib/src/models/active_host.dart
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -29,7 +27,6 @@ class ActiveHost extends Comparable<ActiveHost> {
} else {
hostId = '-1';
}

pingData ??= getPingData(tempAddress);
_pingData = pingData;

Expand All @@ -46,6 +43,7 @@ class ActiveHost extends Comparable<ActiveHost> {
}

deviceName = setDeviceName();

// fetch entry from in memory arp table
arpData = setARPData();

Expand Down Expand Up @@ -73,7 +71,6 @@ class ActiveHost extends Comparable<ActiveHost> {

factory ActiveHost.fromSendableActiveHost({
required SendableActiveHost sendableActiveHost,
List<OpenPort> openPorts = const [],
MdnsInfo? mdnsInfo,
}) {
final InternetAddress? internetAddressTemp =
Expand All @@ -83,7 +80,7 @@ class ActiveHost extends Comparable<ActiveHost> {
}
return ActiveHost(
internetAddress: internetAddressTemp,
openPorts: openPorts,
openPorts: [],
pingData: sendableActiveHost.pingData,
mdnsInfoVar: mdnsInfo,
);
Expand Down
8 changes: 8 additions & 0 deletions lib/src/models/arp_data.dart
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -8,13 +12,17 @@ class ARPData {
required this.interfaceName,
required this.interfaceType,
});
factory ARPData.fromJson(Map<String, dynamic> json) =>
_$ARPDataFromJson(json);

final String? host;
final String? iPAddress;
final String? macAddress;
final String? interfaceName;
final String? interfaceType;

Map<String, dynamic> toJson() => _$ARPDataToJson(this);

@override
String toString() {
if (Platform.isMacOS) {
Expand Down
23 changes: 23 additions & 0 deletions lib/src/models/arp_data.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit c59593d

Please sign in to comment.