Writing a Network Scanner in PowerShell
projects networking powershell
I run a /23 network at home, which means roughly 500 usable addresses spread across two /24 ranges. Every time I added a new device or container that needed a static IP, I found myself doing the same thing: guessing at an address, pinging it, and hoping nothing else was already there.
So I wrote a scanner. The goal was simple — tell me what’s used, what’s free, and where I should put the next static.
The requirements shaped the design
A few constraints drove most of the decisions:
It had to work by pasting into a console. Not everything I touch has a script directory I want to manage. That ruled out exit (closes the window) and Read-Host (eats the rest of the pasted text as input). Small details, but they’re the difference between something that works and something that’s annoying.
It had to be fast. Sequentially pinging 500 addresses at 500ms timeout each is over four minutes. Unacceptable. The scanner batches 128 concurrent async pings at a time and waits on the whole batch, which brings a /23 sweep down to a few seconds.
It couldn’t need admin rights. Everything uses standard .NET classes and built-in cmdlets — System.Net.NetworkInformation.Ping, Get-NetNeighbor, Get-NetIPConfiguration.
The ARP timing trick
The most useful thing I learned building this: read the ARP cache after the ICMP sweep, not before.
Plenty of devices drop ICMP entirely — printers, some IoT gear, hosts with strict firewall rules. They’ll never answer a ping. But the act of pinging them still triggers an ARP request at layer 2, and most of those devices will answer that.
So the sweep populates the ARP table as a side effect. Reading it afterward catches hosts that would otherwise look like free addresses. Those show up flagged as ARP instead of Ping in the output, so you know the difference.
Picking the right interface
On a machine with multiple adapters — VPN clients, virtual switches, WiFi and Ethernet both up — “which network am I on” isn’t obvious.
The scanner reads both the route metric and the interface metric, adds them, and picks the lowest. That’s the same calculation Windows itself uses to decide which path traffic takes, so the scanner ends up scanning the network you’re actually using rather than whichever adapter happened to enumerate first.
If it finds multiple distinct subnets, it prints the commands to scan the others.
MAC address hints
The output includes a vendor column, but full OUI lookup tables are large and go stale. Instead there’s a small hardcoded map covering hypervisors and common hardware — VMware, Hyper-V, VirtualBox, QEMU/KVM, Xen, Raspberry Pi.
For anything not in that map, it falls back to pure bit math on the first octet. Bit 0 set means multicast. Bit 1 set means locally administered, which in practice means a randomized MAC — phones and laptops with privacy features enabled. That’s always correct regardless of how old the table is.
Suggesting where to put statics
Finding free IPs is easy. Finding good free IPs is the useful part.
The scanner consolidates free addresses into contiguous ranges rather than listing 400 individual IPs. It reports the largest available block, and can find a run of N consecutive addresses if you need to allocate a group.
For single static suggestions it works top-down from the highest free address. DHCP pools almost always start low and grow upward, so the high end of a subnet is the safer place to park something you don’t want reassigned out from under you.
Guard rails
A /16 is 65,000 addresses. Scanning that by accident with a 500ms timeout would take a while and probably make the network unhappy.
There’s a MaxHosts ceiling that stops the scan and prints the exact command to override it, with a suggested lower timeout. Fail loudly, explain the fix, let the operator decide.
The whole thing is a single file with no dependencies. Paste it in, run Invoke-LabScan, get a clean picture of the segment. It’s not doing anything clever — just doing the obvious thing carefully.