Documentation
Reference for the new-arp-scan CLI and library behavior (from the project README and repository).
Overview
new-arp-scan is an ARP scanning tool written in Rust. On Linux, the scan command performs address resolution protocol discovery across the selected interface’s IPv4 subnet using raw AF_PACKET / SOCK_RAW sockets; on macOS it does the same over a Berkeley Packet Filter (/dev/bpf*) device, with identical flags, output, and exit codes.
Contributor guides
Markdown guides in the repository complement this page:
- Contributor onboarding — toolchain, build, lint, tests, conventions summary
- Architecture overview — module map, unsafe boundaries, packet flow, testing strategy
- Linux platform support — AF_PACKET, capabilities, test environments, namespaces
- macOS platform support — Berkeley Packet Filter, root requirements, interface naming, tcpdump validation
Platform support
- Linux — full support for interfaces and scan via AF_PACKET raw sockets.
- macOS — full support for interfaces and scan via a Berkeley Packet Filter device, with the same flags, output, and exit codes as Linux.
- Other operating systems — scan and interfaces return an unsupported-platform error without calling platform-only APIs.
Permissions
On Linux, creating the raw packet socket requires capability
CAP_NET_RAW (often available to
the superuser); if opening the socket fails with permission denied, the tool surfaces an explicit
CAP_NET_RAW hint. On macOS, opening a Berkeley Packet Filter device
(/dev/bpf*) typically requires root
(run with sudo) unless your system grants BPF access to your user; permission
denied is surfaced with a "run with sudo" hint. See Linux
packet(7) / capabilities(7) and macOS
bpf(4).
Commands
new-arp-scan interfaces
new-arp-scan monitor [--interface <NAME>] [--timeout-ms <MILLISECONDS>]
new-arp-scan scan [--interface <NAME>] [--host <IPv4>] [--timeout-ms <MILLISECONDS>] [--pacing-ms <MILLISECONDS>] [--attempts <COUNT>] [--bandwidth <BITS_PER_SECOND>] [--interval-ms <MILLISECONDS>] [--backoff <FACTOR>] [--mac-vendor-file <PATH>] [--vlan <VID>] [--pcp <PRIORITY>] [--dei] [--svlan <VID>] [--spcp <PRIORITY>] [--sdei] [--padding <HEX>] [--arpspa <IPv4|dest>] [--llc] [--destaddr <MAC>] [--srcaddr <MAC>] [--arpsha <MAC>] [--arptha <MAC>] [--arphrd <UINT>] [--arppro <UINT>] [--arphln <UINT>] [--arppln <UINT>] [--arpop <UINT>]
Built-in help: new-arp-scan --help, new-arp-scan interfaces --help, new-arp-scan monitor --help, new-arp-scan scan --help. Running new-arp-scan with no arguments prints the root help and exits successfully.
interfaces
On Linux and macOS, lists interfaces usable for ARP scanning: Ethernet hardware type, administratively up, not loopback, not NOARP, with an IPv4 address, netmask, and a non-zero hardware address. Output is a plain aligned table sorted by interface index, then name, with NAME, INDEX, IPV4, NETMASK, and MAC columns. If none qualify, the tool prints no usable interfaces found and exits successfully.
monitor
Listen-only diagnostic on Linux and macOS. It uses the same interface selection and privileges as scan and transmits no frames. --timeout-ms defaults to 30000 and must be at least 1. The listen matches ARP against every IPv4 address configured on the selected interface. A request or reply is a local conflict when its sender protocol address is local and its sender hardware address is not the interface MAC. Frames that match both the local MAC and a local IPv4 address are suppressed. Other well-formed ARP is an observation. A nonzero, nonlocal sender protocol address claimed by two or more hardware addresses is labeled duplicate-ip, separately from a local conflict. This is not RFC 5227 address conflict detection: there are no announcements, defense, or address changes. ARP is unauthenticated, so a reported conflict can be spoofed. Scan-only flags, vendor lookup, timestamps, and JSON are not part of this command. On Linux the socket uses ETH_P_ALL; the kernel still strips the outermost VLAN tag, and the command does not request PACKET_AUXDATA.
scan
On Linux and macOS, reads the interface IPv4 address, netmask, and Ethernet hardware address (via ioctl on Linux, getifaddrs on macOS), opens a raw link-layer endpoint scoped to ARP (an ETH_P_ARP packet socket on Linux, or ETH_P_ALL when --vlan, --svlan, or --llc is set so tagged and IEEE 802.3 SNAP replies are not dropped; a filtered Berkeley Packet Filter device on macOS), then runs --attempts full rounds (default 1). With --host <IPv4>, each round sends one broadcast ARP request for that address only; the address must be strictly interior on the subnet (not network or broadcast, and not off-subnet). Otherwise each round sends one broadcast ARP request per target address in the subnet (excluding network and broadcast, but always including the interface’s own IPv4 when it falls outside that open range). In single-host mode, only replies whose sender IPv4 equals --host are recorded. Between rounds it sleeps --pacing-ms after each round except the last (default 0), then collects replies until --timeout-ms elapses after the last round (default 3000). Millisecond values that exceed what Linux poll(2) accepts are clamped internally. --bandwidth and --interval-ms are an optional outbound rate limit (not congestion control) and are mutually exclusive. Without them, each round still bursts every target. With either flag, sends keep a strict minimum gap from the previous send completion, with no catch-up burst, and each round receives for timeout * backoff^(round-1) before unanswered targets are retried.
If scan is run without --interface / --iface, the tool selects an interface automatically only when exactly one usable interface exists; otherwise it exits with an error describing the ambiguity or that no usable interface was found.
Examples
These examples match the CLI help footer and current operator workflows:
# List interfaces usable for ARP scanning (Linux and macOS)
new-arp-scan interfaces
# Scan a specific local IPv4 subnet
new-arp-scan scan --interface eth0
# Probe one strictly interior host on that subnet
new-arp-scan scan --interface eth0 --host 192.168.1.50
# Let the tool select the interface when exactly one usable interface exists
new-arp-scan scan
# Use a custom receive window, pacing between scan rounds, and multiple attempts
new-arp-scan scan --interface eth0 --timeout-ms 5000 --pacing-ms 10 --attempts 3
# Limit outbound frames to 256 kbit/s (about 2 ms between minimum-size frames)
new-arp-scan scan --interface eth0 --bandwidth 256K --attempts 3
# Annotate MAC addresses from an IEEE MA-L / MA-M / MA-S / IAB mapping file
new-arp-scan scan --interface eth0 --mac-vendor-file ieee-oui.txt
# Send IEEE 802.1Q tagged ARP requests on VLAN 10
new-arp-scan scan --interface eth0 --vlan 10
# IEEE 802.1Q priority tagging (PCP 5, DEI set)
new-arp-scan scan --interface eth0 --vlan 10 --pcp 5 --dei
# IEEE 802.1ad QinQ: service VLAN 100 (PCP 5) wrapping customer VLAN 10
new-arp-scan scan --interface eth0 --vlan 10 --svlan 100 --spcp 5
# Append custom payload padding after the ARP PDU
new-arp-scan scan --interface eth0 --padding deadbeef
# RFC 5227 ARP Probe (sender protocol address 0.0.0.0)
new-arp-scan scan --interface eth0 --arpspa 0.0.0.0
# RFC 5227 ARP Announcement (sender protocol address equals each target)
new-arp-scan scan --interface eth0 --arpspa dest
# RFC 1042 LLC/SNAP framing instead of Ethernet II
new-arp-scan scan --interface eth0 --llc
# Unicast ARP to a known Ethernet destination
new-arp-scan scan --interface eth0 --destaddr 00:11:22:33:44:55
Scan behavior
- With --host, the scanner sends only for that IPv4 and accepts replies only when the sender IPv4 matches. Invalid targets (for example the subnet network address) return an error before opening the socket.
- Target addresses come from the selected interface’s IPv4 address and netmask. The scanner excludes the network and broadcast addresses, but includes the interface’s own IPv4 address when that address falls outside the open host range.
- Requests are broadcast RFC 826 ARP frames (Ethernet II, 60-octet IEEE 802.3 minimum without FCS) sent through a raw link-layer endpoint scoped to ARP (an ETH_P_ARP packet socket on Linux, or ETH_P_ALL when --vlan or --llc is set; a filtered Berkeley Packet Filter device on macOS that also accepts a single IEEE 802.1Q tag, an IEEE 802.1ad service tag wrapping one customer tag, and RFC 1042 LLC/SNAP). With --vlan <VID> (0..=4095) each request carries a single IEEE 802.1Q tag (TPID 0x8100; PCP and DEI default to zero). --pcp and --dei require --vlan and fill the rest of the TCI. --svlan <VID> requires --vlan and wraps that customer tag in an IEEE 802.1ad service tag (TPID 0x88A8); --spcp and --sdei require --svlan and fill the service TCI. Missing prerequisites are usage errors (exit code 2). With --llc, requests use IEEE 802.3 length plus RFC 1042 LLC/SNAP; the length field is LLC+SNAP+ARP (36 for IPv4 ARP) plus any --padding. --padding <HEX> appends hex-encoded octets after the ARP PDU (no 0x prefix); the frame is still zero-padded to 60 octets when shorter. --arpspa 0.0.0.0 is an RFC 5227 ARP Probe; --arpspa dest is an RFC 5227 ARP Announcement. --destaddr / --srcaddr override Ethernet addresses; --arpsha is independent of the Ethernet source. Remaining RFC 826 fields follow original arp-scan names (--arphrd, --arppro, --arphln, --arppln, --arpop, --arptha). Replies may arrive Ethernet II, 802.1Q-tagged, 802.1ad service-tagged (one 0x88A8 tag wrapping one 0x8100 tag), or RFC 1042 LLC/SNAP under any of those; PCP, DEI, and VID are decoded for both tags. On Linux the kernel strips the outermost VLAN tag from received frames before the scanner sees them, so tagged replies are recorded from their remaining inline framing; on macOS both tags arrive inline. Every other tag arrangement is rejected: a lone service tag, a tag stacked inside a customer tag, three or more tags, and the unofficial TPIDs 0x9100 / 0x9200 / 0x9300. Well-formed ARP that is not a reply is ignored without a warning; RFC 5494 reserved opcodes still warn as malformed. --host is a single-target scan of one interior IPv4 address, not itself an RFC 5227 ARP Probe.
- --attempts is the total number of scan rounds (minimum 1). Each round sends one request per target; the default is 1.
- --pacing-ms sleeps after each full round of target sends except the last round. The default is 0, so the default scan sends all targets back-to-back within each round.
- --timeout-ms is the receive window after the last round on the default burst path. The default is 3000, and values larger than poll(2) accepts are clamped internally. On the rate-limited path the same timeout is the first round's receive window, and later unanswered rounds use timeout * backoff^(round-1). Receive stays between rounds, so replies can queue during a paced round.
- --bandwidth and --interval-ms opt into strict inter-target spacing. They are mutually exclusive, and neither is congestion control. --bandwidth is a positive integer bit rate with an optional K (1,000) or M (1,000,000) suffix. The gap is the ceiling of max(encoded frame + 4-octet FCS, 64) * 8 / bits per second, so 256 kbit/s on a minimum frame is 2 ms. --interval-ms sets that gap in whole milliseconds. The first send is immediate; later sends wait until the previous send completed plus the interval, with no catch-up if a send runs late. --pacing-ms is still extra delay only between rounds, added after both the inter-send deadline and the receive window. --backoff requires a rate flag, defaults to 1.5 when omitted, and must be a finite factor of at least 1. Answered targets are not retried.
- Non-fatal send or frame-parse problems are collected as warnings, while fatal interface, socket, receive, or poll failures return an error.
Output
- Discovered hosts: <IPv4> <MAC> on standard output, sorted by ascending IPv4 address. MAC addresses are colon-separated lowercase hex. With --mac-vendor-file, a present ieee-oui.txt, or a binary built with --features bundled-mac-vendors, the line is <IPv4> <MAC> <vendor> (or (Unknown) when no IEEE MA-L / MA-M / MA-S / IAB prefix matches). Search order is explicit path, then the current-directory file, then the compile-time bundle. scan never downloads IEEE listings. Operators refresh with make update-mac-vendors (MA-L, MA-M, MA-S, and IAB via system curl; generated ieee-oui.txt is not committed).
- Non-fatal issues (e.g. failed send, malformed frame, conflicting duplicate address resolution reply for the same IPv4): warning: ... on standard error.
- After the scan’s standard output lines, the binary prints one timing summary line on standard error with the stable template scan complete: interface <NAME>, <N> host(s), <R> round(s), <MS> ms (singular host / round when the count is one).
- No responses: prints no hosts found on standard output, still prints the timing summary on standard error, and exits successfully.
- A monitor prints conflict, observed, and duplicate-ip lines on standard output. Each packet line includes the opcode, sender and target hardware and protocol addresses, and a repeat count. With no local conflict it prints no conflicts observed. Warnings and one monitor complete: interface <NAME>, <N> conflict(s), <N> observation(s), <N> duplicate-ip claim(s), <MS> ms line go to standard error. At most 4,096 distinct packet records are kept. Packets past that limit do not create or extend duplicate-ip claims.
Exit codes
- 0 — successful command, including no hosts found, no conflicts observed, no usable interfaces found, printing help with no arguments, and successful --help invocations.
- 1 — any operational failure from the library (AppError), including unsupported platform, invalid interface or target, and raw socket errors (including missing CAP_NET_RAW when reported as permission denied).
- 2 — command-line usage or parse errors (typically unknown flags or invalid flag values).
Library API
The crate exposes the same application surface used by the binary. Call run(ApplicationCommand::Scan { ... }), run(ApplicationCommand::Monitor { ... }), or run(ApplicationCommand::UsableInterfacesList) and handle ApplicationOutcome or AppError. On Linux and macOS, set target_ipv4_address on ApplicationCommand::Scan for single-host probing; on Linux, perform_arp_probe is also re-exported for direct calls.
- Scan commands take explicit Duration values for timeout and pacing, and std::num::NonZeroU64 for attempts. Use DEFAULT_SCAN_TIMEOUT, DEFAULT_SCAN_PACING, and DEFAULT_SCAN_ATTEMPTS to match CLI defaults. rate_limit is Option<RateLimitedScanTiming>; None keeps the burst path. This field is source-breaking in crate 0.3.0. ApplicationCommand and ApplicationOutcome gained a monitor variant in crate 0.4.0 and are #[non_exhaustive]. A monitor timeout of zero is AppError::MonitorTimeoutRejected. DEFAULT_MONITOR_TIMEOUT is 30 seconds. MonitorOutcome carries the buffered report; vendor registries do not change its lines.
- ScanOutcome::discovered_hosts contains DiscoveredHost values sorted by IPv4 address. Each host stores the MAC in DiscoveredHost::media_access_control_address as a typed MacAddress. On Linux and macOS, run sets ScanOutcome::timing_summary after a successful scan; direct calls to perform_arp_probe leave it unset unless callers attach one.
- UsableInterfacesListOutcome exposes the same interface rows printed by the interfaces command.
- Pure IPv4 helpers Ipv4Cidr and Ipv4HostAddressIterator are public for CIDR parsing and target expansion.
Verification
To verify frames on the wire, run tcpdump or Wireshark on the same interface (for example tcpdump -ni eth0 arp) while scanning. This is optional manual validation and is not part of automated tests. A privileged monitor listen is the same class of manual check and must not transmit.
For a full acceptance check on hardware you control, build the binary and run a privileged scan such as sudo ./target/debug/new-arp-scan scan --interface eth0. Compare default runs with custom --timeout-ms, --pacing-ms, --attempts, and --bandwidth values to confirm timing behavior for your network.
Requirements
- Rust toolchain with Cargo (rustc, cargo fmt, cargo clippy)
- GNU Make (optional; recommended for Makefile targets)
Local development
| Command | Description |
|---|---|
| make build | cargo clean then cargo build --release |
| make test | cargo test then cargo test --tests |
| make lint | cargo fmt --all then cargo clippy --all-targets -- -D warnings |
| make coverage | cargo llvm-cov --all-targets --summary-only (install once with cargo install cargo-llvm-cov; may need rustup component add llvm-tools-preview) |
| make clean | cargo clean |
Run the same commands manually if you prefer not to use Make. Contributing guidelines: CONTRIBUTING.md. Extra notes may live under docs/ in the repository.
License
Copyright © Peter Aleksander Bizjak. Licensed under the GNU Affero General Public License v3.0 only.