docs/design-notes.md

# Design Notes (Retrospective)

portr was a weekend project from early 2021. I archived it in mid
2022. These notes are what I would do differently if I were starting
it today, written as a letter to the version of me who was about to
open a new Go module and call it `portr`.

If you are looking for a live replacement, read
[docs/migration.md](/src/portr/docs-migration-md/).

## What portr was

A concurrent TCP port scanner with a tview TUI. About 400 lines of
Go. A `net.Dial` per port in a worker pool. Results streamed into
the TUI and sorted by port number. Useful for one-off "is this open"
checks on machines I owned.

It was perfectly fine for what it did. It just didn't need to
exist.

## What I would do differently

### 1. Not start

The honest answer. nmap, rustscan, naabu existed in 2021 and were
all better. The reason portr got written was that I had a weekend,
wanted to practice Go's concurrency primitives, and this was a
well-shaped problem for that. It was a learning exercise that I
accidentally shipped.

If I had asked myself "would I recommend this to someone else" on
day two, I would have pushed the repo to `private` and saved the
README. I didn't, and now the project's second life is as a warning
to myself.

### 2. Tests before TUI

I wrote the scanner, then the TUI, then the tests. The tests turned
out well (commit `30dd2a1`, "scanner: context cancellation, tests")
but retro-fitting them was painful because the scanner's API had
shaped itself around what the TUI wanted, not around what was easy
to test. The TUI concerns leaked into the scanner.

Lesson: if you know you want a TUI, write the core library with no
TUI in mind first, test it fully, then build the TUI as a consumer.
The [tests file](/src/portr/scanner-test-go/) is readable, but in
the shape it takes I can still see where I had to refactor to make
it testable at all.

### 3. Don't write a TUI

portr's TUI doesn't add value over a well-formatted stdout. `less`
and `grep` handle the UX needs. In 2022 I migrated away from TUI
scanners entirely; the progress-bar aesthetic was never worth it.

### 4. Bounded concurrency from day one

The first version spawned one goroutine per port. For /24 scans at
a thousand ports that was a million goroutines. Linux is patient
but my VPN wasn't. Fixing it introduced the `--concurrency` flag
(`5a02e8b`), which defaults to 128. I should have bounded from day
one by reading the ulimit, but it's easy to forget because Go
makes unbounded look safe.

### 5. Don't accept CIDR

CIDR support (`20ee6fa`, "cmd: accept CIDR ranges") felt like a
feature when I added it. In practice, every CIDR scan I ran was
scary - either I had permission and should have used nmap with
decent fingerprinting, or I didn't and shouldn't have been scanning
at all. A tool with a smaller default blast radius would have made
my life easier.

In the replacement I use (nmap), I usually pass explicit hosts. I
would do the same in portr v2 if there was a reason for a v2.

### 6. Don't sort in the TUI

`b3c1927` "tui: sort results by port" was a nice polish but made
the scanner's streaming pointless because you can't really see
progress when the TUI is waiting for all results before sorting. I
should have either left the TUI streaming and added a "sort on
enter" toggle, or not bothered sorting at all.

### 7. Ship a plain stdout mode first

`--no-tui` was a late addition. It should have been the default,
with `--tui` as the opt-in. Most people who wanted this tool wanted
to pipe it to something else.

### 8. Dial timeouts

The original scanner used a 3-second connect timeout per port.
Scanning 1024 ports at concurrency 128 with pessimistic timeouts
took a long time. I dropped to 500 ms (commit `5a02e8b` era) and
things became usable. Should have picked 500 ms from the start;
the internet has not gotten slower.

### 9. UDP

I never implemented UDP scanning. Good call. UDP scanning without
raw sockets is mostly lies (you can't distinguish "filtered" from
"no response" without OS-level tricks). The "small, correct, TCP-
only" scope was one of the few right decisions.

### 10. Worry about being a foot-gun

Port scanners are dual-use. The README and `--help` both mention
"point at things you own." A more serious treatment would have
included rate-limiting defaults and per-destination caution, so
that accidentally typing `portr 8.8.8.8` at default settings
didn't instantly look like an attack to someone's IDS.

## Things I liked about portr

Not everything was wrong.

- The test file (`scanner_test.go`) is a tight table-driven
  example. I still point students at it.
- Context cancellation worked correctly from day one.
- The concurrency shape - worker pool reading from a channel of
  `(ip, port)` tuples - is the right Go idiom for this kind of
  problem.
- The code was small enough that anyone can read the whole thing
  in half an hour.

## Why archive instead of delete

Deleting would break incoming links, the `go.sum` entries of any
tiny things that somehow depend on this, and the context of the
tests that I share. Archiving keeps it all, marks the intent, and
takes the repo out of the "is this maintained" conversation.

The `ARCHIVE_NOTICE` commit (`d71e004`) put a big banner on the
README and added a redirect policy in the issues tracker:

- security issues: email, reply within a week
- feature requests: closed as "archived"
- PRs: closed with a link to an alternative tool

## Closing thoughts

The part of portr that I'm proud of is the decision to archive it
cleanly. It's easy to leave a hobby project dangling with a last
commit in 2019 and no indication of state. That's worse than saying
"done" out loud. If you wrote a small tool that solved your problem
and now doesn't need to exist, mark it as such and move on.

This doc is the admission that portr solved its problem for a year
and then stopped being necessary. The tests and the TUI code are
preserved for anyone who wants to read Go; the binary shouldn't be
deployed anywhere new.

## Frequently asked questions

**Will you accept patches?**
For security issues, yes. Otherwise no; they'd be added to a tool
that nobody should be using.

**Can I fork it?**
The license is Apache 2.0. Go ahead.

**Which of the alternatives should I use?**
See [docs/migration.md](/src/portr/docs-migration-md/).

**Why Apache 2.0 and not MIT like your other repos?**
Early me liked the patent clause. Later me doesn't care; either is
fine for a tool at this scope. I didn't re-license on archive.

**Where did the name come from?**
"port + r" because "portscanner" was taken on crates.io and I
liked the shape of the word. Not profound.

## One thing that aged well

The `scanner_test.go` file survives as a reference for table-driven
tests with context cancellation. If there is a legacy of portr, it's
that file. The rest is just history.