Skip to content

servers: what is listening, and who can reach it

The problem

Dev servers pile up: a vite --host from last week, a python -m http.server started to pass one file across, a preview an agent left running. Some of them serve their folder to the whole Wi-Fi or every device on the VPN, because the command bound to 0.0.0.0 instead of 127.0.0.1.

lsof -iTCP -sTCP:LISTEN lists them but does not say what you need to know: can another machine reach this, and what is it? The name column is cut to 9 characters, each port shows twice (IPv4 and IPv6), and nothing says which project it belongs to.

The method

lsof -nP +c 0 -iTCP -sTCP:LISTEN -F pcn   # one field per line, full names
  • Reach comes from the address, read with Python's ipaddress: *, 0.0.0.0, [::] reach every interface; 127.0.0.0/8, ::1 and ::ffff:127.0.0.1 are local; any other address (192.168.1.5, a VPN address) is reachable on that network and counts as exposed. A plain "is it *?" test calls those local, which is exactly wrong.
  • One row per process; if any of its ports is reachable, the row is.
  • Two batched lookups: ps -o pid= -o etime= -o command= -p 1,2,3 and lsof -a -p 1,2,3 -d cwd -Fpn. The folder a server started in usually tells you which project it is.
  • Careful stop: TERM, poll until the port is free, KILL only if the same pid still holds the same port, so a reused pid is never hit.
  • Rebind recipes for http.server, Vite, Next, Astro, uvicorn, Flask, Hugo, Jekyll, Rails, serve and php -S: the old address flag and its value are dropped (--host 0.0.0.0, --host=…, a bare --host), the loopback one added. --run restarts in the same folder and reports success only once the port listens on loopback alone.
servers                    # table, exposed first
servers --exposed          # exit 1 if anything not kept is reachable
servers kill 5173          # TERM, wait, KILL
servers local 5173         # print the command bound to 127.0.0.1
servers local 5173 --run   # and restart it
SERVERS_KEEP=postgres servers kill --exposed

Limits

  • Needs lsof. Without root it sees only your own processes; sudo servers shows system services too.
  • TCP only.
  • "Reachable" is what the program asked for; a firewall may still block it.
  • local --run keeps the arguments and folder, not environment variables.
  • Recipes match command text; a server behind a wrapper script may have none, and then local prints the original command for you to change.

Code

methods/servers/ in the methods repository: one Python file (standard library), a README and a 66-check test against real lsof and real loopback-only servers.