
Explain why a Linux TCP port may or may not be reachable
PortClue explains why a TCP port on a Linux machine may or may not be reachable.
It turns socket, process, firewall, and Docker state into a short evidence chain instead
of making you correlate ss, /proc, nftables, iptables, and docker inspect by hand.

Run it without a port to discover which local TCP endpoints deserve attention:
$ sudo portclue
LOCAL TCP LISTENERS
PORT SERVICE CONFIDENCE BIND OWNER SOURCE BIND SCOPE
22 OpenSSH server HIGH 0.0.0.0,:: ssh.service host ALL_INTERFACES
8080 NGINX web server HIGH 0.0.0.0,:: nginx host ALL_INTERFACES
8443 api service MEDIUM 192.0.2.10 demo-api docker SPECIFIC_INTERFACE
9000 Python HTTP server MEDIUM 127.0.0.1 python3 host LOOPBACK_ONLY
BIND SCOPE describes socket binding, not firewall reachability.
Run `portclue PORT` for the complete evidence chain and local exposure verdict.
ALL_INTERFACES, SPECIFIC_INTERFACE, and LOOPBACK_ONLY describe where a
socket accepts traffic. They deliberately do not claim that a firewall permits it.
Inspect a port for the full local firewall analysis:
$ sudo portclue 8080
POTENTIAL EXTERNAL EXPOSURE
TCP port 8080
0.0.0.0:8080/tcp [POTENTIAL]
Service NGINX web server
Category web
Confidence HIGH
Identity evidence executable basename matched "nginx"
-> LISTEN NETLINK_INET_DIAG reports socket inode 123456 bound to 0.0.0.0:8080/tcp
-> OWNED PID 4242 (nginx), systemd unit nginx.service
-> ALL_INTERFACES 0.0.0.0 accepts traffic addressed to any local interface
-> ACCEPT nftables: a direct rule matches TCP destination port 8080 and returns accept
Unknown outside this machine:
- router port forwarding
- cloud firewall or security group
- upstream NAT, including carrier-grade NAT
[!IMPORTANT] PortClue v0.1 is an early, conservative Linux prototype.
POTENTIALmeans the observed local path allows traffic; it does not claim that a port is reachable from the public internet. Unsupported firewall expressions produceUNKNOWN.
ss or lsofNETLINK_INET_DIAG, the same kernel
interface ss uses, and process details from /proc. Nothing is parsed out of
another tool's output, so a container with neither ss nor lsof still gets the
listener and its owner. Firewall analysis does run nft or iptables-save, and
reports UNKNOWN without them.ss -ltnp gives you the socket and the PID.
Whether that bind is loopback-only, whether a firewall rule allows it, and whether
the port is a Docker publish rather than a host listener is work you would otherwise
do by hand.UNKNOWN, never a
confident wrong answer.NETLINK_INET_DIAG, not by scraping ss output/procsystemctl shownft --json list rulesetiptables-save when nftables is unavailablePortClue identifies what a port belongs to before explaining exposure. Evidence is ranked in this order:
/etc/services port convention.An actual owner always overrides a conventional port name. If only the port convention
is known, the identity is explicitly marked LOW confidence. The embedded catalog is
stored in internal/identify/catalog.json and ships
inside the single binary; PortClue does not download identity data at runtime.
PortClue is read-only. It does not connect to the queried port, scan another host, change firewall rules, stop processes or containers, upload data, or run a daemon.
Runs on Linux (amd64 and arm64). The script downloads the matching GitHub
Release archive, verifies SHA256SUMS, and installs a single binary. It does
not modify shell config.
User install (default, no root). Installs to ~/.local/bin, owned by you:
curl -fsSL https://raw.githubusercontent.com/pbxqdown/portclue/v0.1.2/scripts/install.sh | sh
portclue
A user install runs without root and reports the evidence available to your
account, noting what is missing. This is enough to explore listeners and Docker
mappings. Because sudo does not search ~/.local/bin and this binary is
writable by your user, do not run this copy with sudo.
System install (root-owned, for sudo portclue). PortClue reads the most
complete evidence (restricted /proc, full firewall state) as root. For that,
install a root-owned binary into /usr/local/bin (the script uses sudo only
for the final install step, not for downloading or extracting):
curl -fsSL https://raw.githubusercontent.com/pbxqdown/portclue/v0.1.2/scripts/install.sh | sh -s -- --system
sudo portclue
Optional overrides: PORTCLUE_VERSION=0.1.2 (no leading v) and
PORTCLUE_INSTALL_DIR for either mode.
Uninstall:
# user install
rm ~/.local/bin/portclue
# system install
sudo rm /usr/local/bin/portclue
# or, matching how you installed:
curl -fsSL https://raw.githubusercontent.com/pbxqdown/portclue/v0.1.2/scripts/install.sh | sh -s -- --uninstall
curl -fsSL https://raw.githubusercontent.com/pbxqdown/portclue/v0.1.2/scripts/install.sh | sh -s -- --system --uninstall
Download the matching portclue-VERSION-linux-ARCH.tar.gz and SHA256SUMS from
GitHub Releases, verify the
checksum, then install:
sha256sum -c SHA256SUMS --ignore-missing
tar -xzf portclue-0.1.2-linux-amd64.tar.gz # or linux-arm64
sudo install -m 0755 portclue-0.1.2-linux-amd64/portclue /usr/local/bin/portclue
portclue --version
Architecture mapping:
uname -m | Archive |
|---|---|
x86_64 | linux-amd64 |
aarch64, arm64 | linux-arm64 |
Each archive includes the binary, README, Apache-2.0 license, and third-party notices.
Requires Linux and Go 1.25+:
go install github.com/pbxqdown/portclue/cmd/[email protected]
This puts the binary in $(go env GOPATH)/bin. Prefer the checksum-verified
release archive or install script when you want reproducible install artifacts.
Requirements: Linux and Go 1.25 or newer.
go build -o portclue ./cmd/portclue
./portclue
./portclue --json
./portclue 8080
./portclue --json 8080
Overview mode accepts optional filters (ignored fields stay unconstrained):