DEV Community

Jeff Ronnie
Jeff Ronnie

Posted on

What Building a Port Scanner Taught Me About Trust in Networks

A few weeks into building my port scanner, I hit a moment that had nothing to do with syntax and everything to do with judgment.

The scanner worked. It walked a subnet, hit each host, checked a range of ports, and reported back what was open and what was likely running on it. On paper, that's the whole project. Point it at a network, get a list. Done. I remember the first clean run, every goroutine returning, no panics, no hung connections, and thinking the hard part was over.

It wasn't. The first time I ran it against a network with more than a handful of devices, I noticed something: it was fast, thorough, and completely indifferent to what it was actually doing. It didn't know the difference between a curious developer checking his own home network and something that, from the other side, looked like the opening move of an attack. Nothing in the code cared. It just fired connections as fast as the concurrency limit allowed, because that's what I told it to do, and I hadn't really thought about what "as fast as possible" was going to look like from the receiving end.

That's the moment the project stopped being a networking exercise and started being a design problem. And it's stayed a design problem ever since, because the code itself has been the easy part.

Fast and correct are not the same thing as ready

Concurrency was the first thing I got right, and for a while I treated that as the whole win. Go makes it almost too easy: spin up goroutines, fan out across a subnet, collect results on a channel, done. Scanning a /24 that used to take minutes with a naive sequential loop dropped to seconds. I was proud of that number.

Then I sat with what that number actually meant. A scan that completes in seconds against a few hundred hosts isn't just fast, it's loud. It's a burst of near-simultaneous connection attempts across a range of ports, which is exactly the signature that intrusion detection systems are built to catch, and exactly the pattern that makes a legitimate diagnostic tool indistinguishable from the reconnaissance phase of an actual attack. Nobody on the receiving end of that traffic knows or cares that I was trying to help a small business owner check whether their office printer had an exposed admin panel. They just see a spike.

So I had two versions of the same tool sitting in front of me. One was fast, aggressive, and technically correct: it found everything, quickly, and looked exactly like a threat while doing it. The other was slower, throttled, spaced out, gentler on the network, less likely to trip anything, and less impressive in a demo. Neither one was objectively "better." They were built for different assumptions about who's running the tool and what they're allowed to assume about how the target network will react.

That's not a performance problem you solve with better code. It's a question about who the tool is for, and I hadn't actually answered that question before I started optimizing.

The user I was actually building for

The honest answer, once I sat with it, is that I wasn't building this for someone like me. I was building it for the SME side of the project, a shop owner or office manager in Kenya who wants to know if something on their network is exposed, who has never run a security tool before, and who is not going to read a flag reference before running it.

That person is never going to tune a concurrency setting. They're never going to know that "fast" mode might get flagged by their ISP or their own router's basic protection. Whatever the default behavior is, that's the only behavior that exists for them. There is no "advanced user" fallback where responsibility quietly shifts onto someone who knows better, because there is no one else in the loop.

Once I framed it that way, the trade-off stopped being close. The tool has to default to being the boring, throttled, well-behaved option, and let genuine power users opt into aggression deliberately, with a flag they have to go looking for. Speed became something you ask for, not something you get by default. That felt like giving something up the first time I made the change (the demo got less flashy), but it was the first moment the project actually matched what it claimed to be for.

Version detection lies to you if you let it

The second place this same question showed up was less about network etiquette and more about honesty in the output itself.

Grabbing a banner off an open port and reporting it as "the service" is the easy version of version detection, and it's also frequently wrong. Services get reconfigured, banners get stripped or deliberately spoofed, proxies sit in front of the real service and answer on its behalf. A port being open tells you almost nothing about what's actually listening on it unless you go a layer deeper and actually probe for it, and even then, you're inferring, not confirming.

The tempting move, especially with a deadline in front of me, was to report whatever the banner said and let the reader assume it was accurate. It made the output look more complete. It also meant that any time the banner was wrong, spoofed, outdated, or misconfigured, the tool would confidently hand someone a false sense of security about their own network, which is close to the worst thing a security-adjacent tool can do.

I kept coming back to the same asymmetry: an honest "I couldn't confirm this" costs you almost nothing. A confident wrong answer costs the person reading it everything, because they have no independent way to check it. They're not going to cross-reference your output against anything; your output is the entire picture they get. So the harder, less satisfying engineering choice was building in explicit uncertainty, separating "confirmed" from "probable" results instead of presenting every finding with the same flat authority. It made the tool look less finished. It also made it something I'd actually trust a stranger to use.

What this actually taught me

None of this was really about port scanning, in the end. It was about what changes the moment a tool stops being "something I built to learn Go" and becomes "something a business is going to point at its own infrastructure and believe." The scope of the decisions gets bigger even when the codebase barely changes. A ten-line concurrency limiter and a boolean flag for "confirmed vs. probable" are small diffs. The reasoning behind them is not small at all.

Building for people who will never read documentation means the defaults are the product. Every choice you don't force the user to make is a choice you're quietly making on their behalf, whether you meant to or not. That's a different kind of responsibility than getting the concurrency pattern right or shaving milliseconds off a scan, and it's the part nobody mentions when they hand you "build a port scanner" as a good project for learning a language.

I'm still working through where exactly these lines sit as the project grows toward something closer to an NMap for small businesses. There's a long list of decisions like this still ahead of it. But the standard I'm building against now is simple to say and harder to hold to: I'd rather ship something a little slower and a little more honest than something fast and confidently wrong.

Where do you draw the line between a tool being thorough and a tool being trustworthy?

Top comments (0)