Line data Source code
1 : import 'dart:convert';
2 :
3 : import 'package:logging/logging.dart';
4 : import 'package:network_tools/src/models/arp_data.dart';
5 : import 'package:universal_io/io.dart';
6 :
7 : /// Retreiving ARP packets is only supported for Desktop such as
8 : /// Linux, Windows, and macOS. Dart native doesn't provide a way or rejects
9 : /// call to arp command on mobile such as Android and iOS.
10 : /// Maybe in future dart native will support sending raw packets,
11 : /// so that time we can add implementation for mobile devices.
12 : /// Currenlty this is achieved by process package.
13 : /// Helper class for retrieving and parsing the ARP table on supported desktop platforms.
14 : class ARPTableHelper {
15 : /// Logger for ARP table operations.
16 9 : static final arpLogger = Logger("arp-table-logger");
17 :
18 : bool isMobilePlatform = Platform.isAndroid || Platform.isIOS;
19 : bool isMacOSPlatform = Platform.isMacOS;
20 : bool isLinuxPlatform = Platform.isLinux;
21 :
22 3 : List<String> executeARPCommand() {
23 : // ARP is not allowed to be run for mobile devices currenlty.
24 3 : if (isMobilePlatform) {
25 0 : arpLogger.warning("ARP command is not supported on mobile platforms.");
26 0 : return [];
27 : }
28 6 : final result = Process.runSync('arp', ['-a']);
29 6 : if (result.exitCode != 0) {
30 0 : arpLogger.severe("Failed to execute ARP command: ${result.stderr}");
31 0 : return [];
32 : }
33 9 : return const LineSplitter().convert(result.stdout.toString());
34 : }
35 :
36 3 : RegExp _getARPCmdRegExPattern() {
37 3 : if (isMacOSPlatform) {
38 1 : return RegExp(
39 : r'(?<host>[\w.?]*)\s\((?<ip>.*)\)\sat\s(?<mac>.*)\son\s(?<intf>\w+)\sifscope\s*(\w*)\s*\[(?<typ>.*)\]',
40 : );
41 3 : } else if (isLinuxPlatform) {
42 3 : return RegExp(
43 : r'(?<host>[\w.?]*)\s\((?<ip>.*)\)\sat\s(?<mac>.*)\s\[(?<typ>.*)\]\son\s(?<intf>\w+)',
44 : );
45 : } else {
46 : // Windows: non-greedy match and trim whitespace
47 1 : return RegExp(
48 : r'(?<ip>[^\s]+)\s+(?<mac>[^\s]+)\s+(?<typ>\w+)',
49 : caseSensitive: false,
50 : );
51 : }
52 : }
53 :
54 : /// Retrieves the ARP table by running the `arp -a` command on Linux, Windows, and macOS.
55 : ///
56 : /// Parses the output and returns a list of [ARPData] objects representing each ARP entry.
57 : /// Returns an empty list on unsupported platforms (e.g., Android, iOS).
58 3 : Future<List<ARPData>> buildTable() async {
59 3 : final Map<String, ARPData> arpEntries = {};
60 6 : final int startTime = DateTime.now().millisecondsSinceEpoch;
61 3 : final entries = executeARPCommand();
62 3 : if (entries.isEmpty) {
63 2 : arpLogger.warning("No ARP entries found.");
64 1 : return [];
65 : }
66 :
67 3 : final pattern = _getARPCmdRegExPattern();
68 :
69 6 : for (final entry in entries) {
70 : // Skip Windows header and interface lines
71 6 : if (entry.trim().isEmpty ||
72 3 : entry.startsWith('Interface:') ||
73 9 : entry.trim().toLowerCase().startsWith('internet address')) {
74 : continue;
75 : }
76 3 : final match = pattern.firstMatch(entry);
77 : if (match != null) {
78 3 : final arpData = ARPData.fromRegExpMatch(match);
79 6 : if (arpData.macAddress != '(incomplete)') {
80 9 : arpLogger.fine("Adding entry to table -> $arpData");
81 6 : arpEntries[arpData.iPAddress] = arpData;
82 : }
83 : }
84 : }
85 6 : arpLogger.fine(
86 15 : "ARP calculation took ${DateTime.now().millisecondsSinceEpoch - startTime} ms with ${arpEntries.length} entries",
87 : );
88 6 : return arpEntries.values.toList();
89 : }
90 : }
|