Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

More devices on scan using ARP (Lin/Mac/Win) #145

Merged
merged 5 commits into from
Sep 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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();

Check warning on line 17 in lib/src/device_info/arp_table.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/device_info/arp_table.dart#L14-L17

Added lines #L14 - L17 were not covered by tests
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