dotvitals

How to test whether a port is open

Updated ·17 min read

Port testing is one of those tasks that looks trivial and is quietly full of traps. People reach for it for two very different reasons: either they want something to work — a game server, a VPN, a self-hosted application behind a home router — and need to know whether the outside world can reach it, or they want to know the opposite, that nothing is reachable that should not be. The same test answers both questions, but only if it is run from the right place and read correctly.

Most port testing goes wrong before the tool is even chosen. Testing from a machine inside the same network as the service almost always reports success, because the traffic never leaves the building and never meets the thing that would have blocked it. That is the single most common reason somebody spends an afternoon convinced a port forward is working when it is not.

This guide covers what an open port actually is, what a closed result and a filtered result each tell you, how the NAT and port-forwarding chain fits together and where it usually breaks, how to test properly with netcat, nmap, a browser and our own scan, which ports normally matter, and the security framing we use ourselves — including the ports we deliberately do not deduct points for.

Check yours now

What open actually means

A TCP port is open, in the only sense a test can establish, when a connection attempt to it completes the three-way handshake: your machine sends SYN, something at the other end answers SYN-ACK, and your machine replies ACK. At that point a connection exists. Nothing has been sent, nothing has been authenticated, and no protocol has been spoken.

That is a narrower claim than people usually take from it. A completed handshake proves an endpoint accepted the connection. It does not prove that the software normally associated with that port number is what accepted it — a reverse proxy, a load balancer, a security appliance or a honeypot all produce exactly the same observation. This is why our own port findings are reported at high confidence and never at confirmed: we never authenticate, so we never learn more than that something answered.

Some services volunteer a greeting the moment the connection opens, before the client says anything. SSH, SMTP, FTP and MySQL all do. Where a banner appears in a port result, that is what it is: text the server sent first, not the answer to a probe. It is a strong hint about what is listening and it is still a hint.

Closed, filtered, and why the difference matters

There are three outcomes worth distinguishing, and consumer port checkers routinely collapse them into two.

  • Open: the handshake completed. Something is listening and accepting.
  • Closed: the host answered, but with a TCP RST (reset) or, on some paths, an ICMP port-unreachable message. The host is alive and reachable; nothing is listening on that port. You get this back quickly, usually in the same round trip.
  • Filtered: nothing came back at all. The connection attempt timed out. A firewall somewhere between you and the host dropped the packet silently rather than refusing it. You cannot tell from the outside whether a service is running behind that firewall or not.

The practical difference is diagnostic. A closed result means your packet reached the host and the host had an opinion: the routing works, the address is right, and the problem is that the service is not running or is bound to the wrong interface. A filtered result means your packet did not reach anything that would answer, so the problem is upstream — a cloud security group, a host firewall with a default DROP policy, an ISP blocking the port, or a port forward that was never created.

Timing is the quick tell even without a tool that labels the states. A refusal comes back in milliseconds. A filtered port makes you wait for the timeout. If a check takes three seconds to fail, it was dropped, not refused.

The three outcomes, as netcat reports them
# Open: handshake completed
$ nc -vz -w 3 example.com 443
Connection to example.com port 443 [tcp/https] succeeded!

# Closed: the host answered with a reset, immediately
$ nc -vz -w 3 example.com 4444
nc: connect to example.com port 4444 (tcp) failed: Connection refused

# Filtered: nothing came back; note that it waited the full 3 seconds
$ nc -vz -w 3 198.51.100.10 3306
nc: connect to 198.51.100.10 port 3306 (tcp) timed out

Why testing from inside your own network lies to you

If you run a port test from a laptop on the same LAN as the server, you are not testing what you think you are testing. The connection goes from your laptop to the server over the local switch. It does not pass through the router's NAT, it does not pass through the port forward, it does not pass through the ISP, and on many setups it does not pass through the cloud security group either. Every one of those is a place the connection could have been stopped, and you skipped all of them.

The same applies to a test run on the server itself. Commands like ss, netstat or lsof tell you what the machine is listening on and on which address, which is genuinely useful — but a service can be listening perfectly and be unreachable from anywhere outside. These are complementary facts, not the same fact.

There is a second, subtler version of this trap on home networks: NAT loopback, sometimes called NAT hairpinning. Some routers let a device inside the network connect to the router's own public address and be folded back to the forwarded internal host. Many routers do not. So a test from inside against your public IP can succeed on one router and fail on an identically configured one, and neither result tells you anything about what the internet sees.

The only vantage point that answers the question is outside your network entirely. Practical options: a phone with Wi-Fi turned off, using mobile data; a shell on a machine somewhere else, such as a cheap virtual server or a friend's box; or a scan run from our infrastructure, which is external by construction.

Inside the host, ask a different question: what is bound, and to which address
# Linux: what is listening, on which address, and which process owns it
sudo ss -tlnp

# Read the Local Address column carefully:
#   0.0.0.0:3306   listening on every IPv4 interface  -> reachable if the firewall allows
#   127.0.0.1:3306 listening on loopback only         -> not reachable from anywhere else
#   [::]:443       listening on every IPv6 interface

# macOS
sudo lsof -nP -iTCP -sTCP:LISTEN

The port-forwarding chain, and where it breaks

On a home or small-office connection, a packet from the internet to your service crosses several independent gates. Each one can stop it, and each one fails in a way that looks identical from the outside — a timeout. Walking the chain in order is faster than guessing.

  • The ISP. Some providers block inbound ports outright — 25 almost universally on residential lines, often 80 and 443, sometimes 445 and 139. Some place you behind carrier-grade NAT (CGNAT), in which case you have no public address of your own and no port forward can work at all. The tell for CGNAT is that the WAN address shown in your router is in 100.64.0.0/10, or is an RFC 1918 address, rather than matching the address a what-is-my-IP service reports.
  • The router's WAN address. A dynamic address that changed since you set things up will make a previously working forward appear broken. Check what your router currently holds, not what you wrote down.
  • The port-forward rule itself. Confirm the external port, the internal port (they need not match), the protocol (TCP, UDP or both — a rule created for the wrong one is a common and invisible mistake), and the internal address it points at.
  • The internal address. This is the classic failure: the server was assigned a new address by DHCP and the forward still points at the old one. Give the host a DHCP reservation or a static address.
  • The host firewall. ufw, firewalld, nftables or Windows Defender Firewall on the machine itself. A service can be running, forwarded correctly, and still dropped on arrival.
  • The service bind address. If it is bound to 127.0.0.1 it will never answer a forwarded connection, no matter how correct everything upstream is.

In a cloud environment the chain is shorter but the same shape: the provider's security group or network ACL, then the host firewall, then the bind address. A security group and a host firewall are two separate controls and both must allow the traffic; fixing one and re-testing while the other still blocks is the usual reason a cloud port forward seems to take three attempts.

Tools that give a straight answer

netcat is the smallest useful tool. nc -vz -w 3 host port attempts one TCP connection and reports the result: -v makes it say what happened, -z means connect and disconnect without sending data, and -w 3 caps the wait at three seconds so a filtered port does not hang. It is on almost every Unix machine, and on a host you do not otherwise want to install anything on, bash can do it alone with its /dev/tcp pseudo-device.

nmap is the right tool when you want several ports at once, or when you want the open/closed/filtered distinction labelled for you rather than inferred from timing. nmap -Pn -p 22,80,443 example.com is a reasonable starting shape: -Pn skips the host-discovery ping, which many hosts drop anyway. Scan only hosts you control or have written permission to test — see the next section, which is not a formality.

A browser tests exactly one thing well: whether a web service on a given port answers HTTP. Visiting https://example.com:8443/ is a real test of that port, and a failure is ambiguous between a closed port, a filtered port, a TLS problem and a server error. Useful for confirming a success; poor for diagnosing a failure.

Public web-based port checkers test from their own vantage point, which is genuinely external and therefore useful. Read the small print on what they do with the result: some publish a searchable history of what they found on your address.

Our own port check runs as part of the full Domain Health Check. Enter a domain, run the scan, and the network section reports every port on our list with its state. It needs our external probe host — a Cloudflare Worker cannot open the raw TCP connections this check requires — and when that host is not configured or not reachable, the report says the check did not run and scores nothing for it. It never renders an untested host as clean.

Four ways to ask, from a machine outside the target network
# netcat: one port, one answer
nc -vz -w 3 example.com 443

# bash alone, where netcat is not installed
timeout 3 bash -c '</dev/tcp/example.com/443' && echo open || echo "closed or filtered"

# nmap: several ports, with the states named (only on hosts you control)
nmap -Pn -p 21,22,80,443,3306,3389 example.com

# curl: is it specifically HTTP, and does TLS work
curl -sS -o /dev/null -w 'http=%{http_code} connect=%{time_connect}s\n' https://example.com:8443/

Scanning hosts you do not control

Port scanning a host you neither own nor have permission to test may be unlawful where you are, and it is against the acceptable-use policy of most hosting providers and ISPs on both ends — the network you scan from as well as the network you scan. Jurisdictions differ in whether an unauthorised connection attempt alone crosses the line, and we are not in a position to give you legal advice about yours. The practical rule is simple and costs you nothing: scan hosts you control, or hosts whose owner has given you permission in writing, and nothing else.

A single connection to a port on a public service is not the same act as sweeping 65,535 ports across a range of addresses, and the difference is visible in the logs at the other end. Volume, breadth and repetition are what turn a test into something an abuse desk acts on.

This shapes our own product. Our scan checks twenty-one well-known ports and no more, and the list is fixed. A wider scan is noisy for the target, is prohibited by some providers' terms, and would make us the source of traffic we would not accept if it were aimed at us. A deeper scan of a domain is gated behind proof that you control it.

One consequence of a fixed list deserves stating plainly, because a report that implied otherwise would be lying by omission: a port not on our list is not reported closed. It is not reported at all. Twenty-one ports out of 65,535 is a sample, and the clean result our report gives you is a clean result for that sample.

The common ports, and whether the public internet is the right audience

These are the twenty-one ports our scan checks, with what normally listens and who should be able to reach it. The middle column is the part that matters: an open port is only a problem relative to who can reach it.

  • 21 FTP. File transfer in a protocol standardised before encryption. Credentials and file contents travel in clear text. The public internet is not the right audience; SFTP over port 22 usually replaces it with no new service to run.
  • 22 SSH. How Unix machines are administered. Encrypted from the first exchange with no plaintext fallback. Public exposure is normal and expected; it guarantees continuous automated login attempts, which is why key-only authentication matters.
  • 25 SMTP. Server-to-server mail delivery. Open is correct on a host that receives mail and wrong on one that does not. Most residential and many cloud providers block it outbound by default.
  • 53 DNS. A nameserver answering queries. Open is how a nameserver works. The real questions — open resolver, zone transfer — are not answerable by a connect test.
  • 80 HTTP. The web, usually just to redirect to 443. Open is the site working.
  • 110 POP3 and 143 IMAP. Mail retrieval in their legacy, upgrade-on-request forms. Open is expected on a host serving mail clients; whether STARTTLS is required is the question that matters and a connect test cannot see it.
  • 443 HTTPS. The web. Open is the site working. A tool that shows this in red is miscalibrated.
  • 465 and 587. Mail submission by authenticated clients: 465 is TLS from the first byte, 587 upgrades with STARTTLS. Expected on a host that accepts mail from users.
  • 993 IMAPS and 995 POP3S. The encrypted forms of 143 and 110, with no plaintext window at all. Preferable to their legacy counterparts and expected on a mail host.
  • 1433 Microsoft SQL Server, 3306 MySQL or MariaDB, 5432 PostgreSQL, 6379 Redis, 27017 MongoDB. Databases. Application servers talk to them; the public internet never should. Redis ships with no authentication by default and MongoDB deployments assumed to be private are behind several of the largest publicly catalogued leaks.
  • 3389 Remote Desktop and 5900 VNC. Full interactive sessions on the machine. Not for the public internet under any ordinary configuration. VNC additionally uses a weak legacy authentication scheme and is unencrypted unless tunnelled.
  • 8080 and 8443. Alternate HTTP and HTTPS. Frequently deliberate — an application server behind a proxy — and frequently a management console that was only ever meant to be internal. A handshake cannot tell you which, so this is a look-at-it, not a verdict.

Telnet on port 23 is not on our list, so our report says nothing about it either way. If you run one, it sends passwords in clear text and should not exist on a public address.

How dotvitals scores ports, and what it deliberately does not

A port scanner that renders 443 in red teaches its readers to stop reading, and a reader who has stopped reading will miss the finding that mattered. So our six port rules split deliberately: two are informational and deduct nothing, three deduct, and one reports that the check did not run.

  • The inventory deducts nothing. It lists every port examined and its state, including the ones that are supposed to be open, with a sentence about what normally listens there.
  • SSH on 22 deducts nothing. It is how most servers are administered and it is not a defect. The finding exists to tell you that your machine's security now rests entirely on SSH's own authentication, which is worth one check: PasswordAuthentication no in sshd_config turns the continuous background of automated guessing into noise, because there is no password to guess.
  • A database port reachable from the internet is a critical failure, 40 points. 1433, 3306, 5432, 6379 or 27017 answering from anywhere on the internet has no ordinary justification.
  • A remote desktop service reachable from the internet is a high-severity failure, 30 points. 3389 and 5900. A database leaks data; an interactive session is the machine.
  • FTP on 21 is a medium warning, 12 points, for the plaintext credentials and the accidental anonymous-write configurations it is famous for.

Several ports are deliberately left to the module that already answers the real question about them, so one fault is not deducted for twice. 80 and 443 belong to the HTTP reachability and TLS checks. The mail ports belong to the MX and SMTP TLS checks, which can see whether transport security is actually required — something a connect test cannot. 53 belongs to the DNS health check, which owns open-resolver and zone-transfer exposure. 8080 and 8443 appear in the inventory with no deduction, because an alternate HTTP port is deliberate about as often as it is an accident and we cannot tell which from a handshake.

When the check could not run, and how to fix what it finds

Our report distinguishes a port check that found nothing open from a port check that did not happen, and keeps them visibly apart. Reporting an untested host as clean would be telling you a database is firewalled when nobody looked. When the external probe host is not configured, is unreachable or refuses the target, the network section carries one finding that says so and no exposure findings at all — not a pass, not a fail, and not an empty list presented as a result.

Where the check does find an exposure, the fix is two changes rather than one, and that is the part people skip. Bind the service to loopback or a private address, and deny the port at the firewall. A service bound only privately is re-exposed by one package upgrade that restores a default config; a firewall rule alone is undone by one flush on reboot. Doing both means a single mistake is not enough to put your database back on the internet.

In a cloud environment, apply it at both layers: the host firewall and the provider's security group or network ACL. A host firewall does not protect you against a permissive cloud rule, and a tight cloud rule does not protect you against a host that is also reachable over a private peering path.

If the service genuinely must be reached across the internet, put it behind a VPN, an SSH tunnel or the provider's private networking rather than an allow-list of addresses. An address allow-list is better than nothing and it is not an access control: it authenticates a network path, not a person.

One thing worth ruling out explicitly, because it comes up constantly: no Cloudflare setting closes a database port. The proxy carries HTTP and HTTPS only, so if the origin's address is publicly known the database port is reachable regardless of what the DNS record says. Cloudflare Tunnel or Access can replace a direct exposure, but only once the port itself is closed.

Close it, then verify from outside — not from the host
# 1. Find what is listening and on which address
sudo ss -tlnp | grep 3306

# 2. Bind privately (my.cnf), then restart
#    bind-address = 127.0.0.1

# 3. Deny at the firewall, then allow only what needs it
sudo ufw deny 3306/tcp
sudo ufw allow from 203.0.113.10 to any port 3306 proto tcp

# 4. Verify from a machine outside the network
nc -vz -w 3 example.com 3306    # expect refused, or a timeout
sudo ss -tlnp | grep 3306       # expect 127.0.0.1, never 0.0.0.0

What commonly goes wrong

  • Testing from inside the network the service lives on. The traffic never meets NAT, the port forward, the ISP or the cloud security group, so the test passes and the internet still cannot reach you. Every subsequent hour of debugging is spent on the wrong thing.
  • Reading a timeout as proof the port is closed. A timeout means something dropped the packet silently. The service may be running perfectly behind a firewall you have not found yet, and you will conclude it is not running and restart it repeatedly.
  • Forwarding TCP when the service needs UDP, or the reverse. The rule looks right in the router's list and nothing works. Game servers, VPNs and DNS are the usual victims.
  • A port forward pointing at an address DHCP has since reassigned. It worked for weeks, then stopped after a reboot, and nothing in the router's configuration changed to explain it.
  • Opening a database port to the internet as a temporary measure. Automated tooling connects to every address continuously; temporary in this context means hours, not weeks, and the credentials should be treated as known afterwards.
  • Moving SSH to a non-standard port and calling it hardening. It reduces log volume and nothing else — the scanners that matter scan every port. Disabling password authentication is the change that actually removes the risk.
  • Treating a clean port report as proof that nothing is exposed. Ours covers twenty-one ports; a service on 9000 is simply not in the sample, and a check that did not run is reported as not run rather than as a pass.

Check your domain with the domain health check