21
FTP's control connection — the port that carries the commands and the numbered replies, while the files themselves move over a second connection on a port nobody told your firewall about.
Run the line for your platform. Each one names the process holding the port and the address it bound to, which is usually the whole answer.
sudo lsof -nP -iTCP:21 -sTCP:LISTENOne row per listening socket. COMMAND is the program, PID is the number you would act on, USER is the account it runs as, and NAME holds the address it bound. `127.0.0.1:21` is loopback only, so a client on another machine never connects no matter what the firewall allows, while `*:21` is every interface. `-n` stops lsof resolving addresses to host names and `-P` stops it printing `ftp` where you want to read `21`, which is what keeps the NAME column usable. `-sTCP:LISTEN` hides established sessions; drop it to see who is transferring right now. No output at all, with lsof exiting 1, means nothing holds the port. Without `sudo` you see only your own processes, so a daemon running as root simply does not appear.
sudo ss -tlnp 'sport = :21'`-t` selects TCP, `-l` only listening sockets, `-n` keeps the port numeric and `-p` adds the owning process; `sport = :21` is ss's filter predicate for the source port. Read the Local Address:Port column first — `0.0.0.0:21` is every IPv4 interface, `127.0.0.1:21` is loopback only and explains a remote client that never arrives, and `[::]:21` is the IPv6 wildcard, which on most Linux hosts accepts IPv4 as well. The owner is the last column, `users:(("vsftpd",pid=1102,fd=3))`: the program name in quotes, then the PID. On a machine without `ss`, the net-tools spelling is `sudo netstat -tlnp | grep ':21 '`, where `-p` prints a slash-separated pid/program pair — keep that trailing space inside the pattern, or it also matches `:2121` and `:21000`. Either way, without superuser privileges the process column is blank while the socket still lists, which looks like "nothing is there" and is not.
netstat -ano | findstr ":21"`-a` displays all connections and the ports the computer is listening on, `-n` expresses addresses and ports numerically, and `-o` appends the owning process ID as the last column. The State column has to read LISTENING for the row to be the listener rather than a client session, and Local Address is the bind: `127.0.0.1:21` is loopback only, `0.0.0.0:21` is every interface. Turn the PID into a program with `tasklist /FI "PID eq 1102"`, whose PID filter accepts eq, ne, gt, lt, ge and le. Mind the substring: `findstr` matches anywhere on the line, so `":21"` also hits `:2121`, `:21000` and any ephemeral source port containing 21 — read the number in full before you kill anything.
Get-NetTCPConnection -LocalPort 21 | Select-Object LocalAddress,LocalPort,State,OwningProcessThe PowerShell equivalent matches the port as a number rather than as a substring, which is the reason to type it next to `netstat`. A `State` of `Listen` with a `LocalAddress` of `127.0.0.1` is exactly the configuration that answers on the machine itself and refuses everyone else. Pipe `OwningProcess` into `Get-Process -Id` for the executable name; on Windows the listener is usually a service that starts at boot rather than anything you launched in a shell. An empty result while a bind still fails means the port is reserved rather than occupied, and `netsh interface ipv4 show excludedportrange protocol=tcp` lists the blocks Hyper-V, WSL2 and Docker Desktop claim at startup.
`nmap -Pn -p 21 files.example.com` asks the only question that matters from outside the host, and `-Pn` skips host discovery so a machine that ignores pings still gets scanned. Nmap's own definitions decide what the answer means: `open` is an application actively accepting connections on that port; `closed` is a reachable port that answered, with no application listening on it; `filtered` means packet filtering stopped the probe from reaching the port, so nmap cannot tell which of the other two it would have been. Add `--reason` to see what decided it — a reset from a closed port against silence from a filtered one — and `-sV` to have nmap read the `220` greeting, which usually names the daemon and its version. One caveat specific to this port: the scan says nothing about the data connection, because a passive transfer uses a high port this scan never touches. `open` on 21 and a download that dies at zero bytes are perfectly consistent findings. Scan only hosts you are responsible for.
| What you see | What it means |
|---|---|
ftp: connect: Connection refused | The packet reached the host and its kernel answered with a reset, because no process holds port 21. The daemon is stopped, it failed during startup, or it is bound to an address that does not include the one you dialled. Run the probe for your platform on the server, not on your laptop, and read the bind address rather than only confirming that a row exists. |
The client sits silent for tens of seconds, then reports Connection timed out | Nothing answered at all. A packet filter is dropping the SYN without replying — a cloud security group, a host firewall, or a network ACL between you and the server. A refusal comes back instantly and a drop makes you wait, so the delay itself is the diagnosis. Confirm from outside with `nmap --reason`: `filtered` is the drop, `closed` is the refusal. |
425 Can't open data connection | Port 21 is fine — you are logged in and the server accepted the command, which is why you got a numbered reply at all. What failed is the second connection. In passive mode the server told you a high port and your firewall would not let you out to it, or the server's own passive range is not open inbound. In active mode the server tried to dial back to your client and something in between blocked it. Switch modes and see which one works: most clients toggle with `passive`, and `curl` uses passive unless you pass `-P -`. |
500 Illegal PORT command | The server is refusing active mode outright, which many now do because the callback is a well-known abuse vector. Use passive mode. The same shape appears when a NAT device rewrites the address inside the `PORT` command and the server sees an address that does not match the control connection it is talking to, which servers reject on purpose. |
227 Entering Passive Mode reports an address you cannot route to | A server behind NAT that has not been told its public address advertises its private one, so the client dutifully tries to open a data connection to something like 10.0.0.5 and hangs. The fix is on the server: give it the external address to advertise, or use the extended `EPSV` reply, which returns only a port and reuses the control connection's address. |
530 Login incorrect | Nothing here is a port problem. The control connection was established and the server read your credentials before rejecting them. Either the account or password is wrong, or the account exists but is denied FTP specifically — a shell of `/usr/sbin/nologin`, a name listed in the server's deny file, or a configuration that only permits chrooted virtual users. |
500 OOPS: could not bind listening IPv4 socket / bind: Address already in use | `EADDRINUSE`: something already holds the socket, and the daemon exits during startup rather than later. Usually a second FTP server from a different package, or a container publishing 21 on the host. Name the owner with the probe above before killing anything, and if the port frees itself a minute or two later the previous process left sockets in `TIME_WAIT` and nothing needed killing. |
bind: Permission denied while starting the server on 21 | This one really is the privileged-port rule: on Unix-like systems ports below 1024 need root, and 21 is one of them. Start the daemon through the init system as root, grant the binary `CAP_NET_BIND_SERVICE` on Linux, or listen high and redirect. On SELinux or AppArmor hosts the same message can also come from the policy refusing the bind for that particular binary, so check the audit log before assuming it is the 1024 rule. |
IANA assigns 21 to `ftp`, the File Transfer Protocol control connection defined by RFC 959, and registers the neighbouring 20 as `ftp-data`. Both TCP and UDP appear in the registry; only TCP is ever used. The split between the two connections is the single most useful thing to know about this port. A session opens on 21 and everything you type travels there in readable ASCII — `USER`, `PASS`, `LIST`, `RETR` — along with the server's numeric replies, but not one byte of file content does. Each transfer opens a second TCP connection, and which side dials it decides whether your firewall lets it through. In active mode the client sends `PORT`, or `EPRT` from RFC 2428, and the server connects back to the client, which nearly every NAT and client-side firewall now blocks. In passive mode the client sends `PASV` and the server answers `227 Entering Passive Mode (h1,h2,h3,h4,p1,p2)`, where the last two numbers are a port written as `p1 * 256 + p2`, or answers `229 Entering Extended Passive Mode (|||port|)`; the client then dials that. Passive is what everything defaults to today, and it is why an FTP server needs a configured range of data ports opened alongside 21 rather than 21 on its own. Then there is the naming trap that brings most people here: FTPS and SFTP are not two spellings of the same thing. FTPS is FTP with TLS added by RFC 4217 — the client sends `AUTH TLS` on this same port 21 and the session continues encrypted — and IANA separately assigned 990 to `ftps` for the older implicit variant that is encrypted from the first byte with no `AUTH` command at all. SFTP is not FTP in any form: it is a subsystem of SSH on port 22, it shares no commands, no reply codes and no second data connection with FTP, and it needs nothing opened but 22.
Do not put plaintext FTP on the internet. The password crosses the wire in the clear on port 21, exactly as telnet's does, and a public listener is found by internet-wide scanners within hours and then tried against anonymous access and vendor defaults. Two habits make FTP servers leak more than most: anonymous read access that was switched on for one file and never switched off, and a document root that was never chrooted per user, so an authenticated account can walk out of its own directory. If all you need is to hand files to someone, HTTPS on 443 does that with far less surface. If you need an interactive file session, SFTP over SSH on 22 needs one port, verifies the server's host key and encrypts commands and content together. When FTP genuinely has to stay — an appliance, a lab instrument, a partner who will not change — require explicit FTPS with `AUTH TLS`, and understand that securing the control connection does not secure the transfer: the data connection is encrypted only when the client also requests `PROT P`, so a session can authenticate under TLS and still ship the file in clear text. Pin the passive port range in the server configuration so the firewall rule is a narrow window instead of the whole high range, restrict the service by source address, and check what your router actually forwards rather than trusting NAT to hide it.
All of these run in your browser — nothing is uploaded.
Ports people usually end up checking in the same session as 21.
Almost always permissions. lsof and ss list the socket but hide the owning process when it belongs to another user, so a server started by a system account, by a container runtime or by launchd shows up as an anonymous listener until you re-run the command with sudo. On Windows the opposite happens: netstat shows nothing at all, yet a bind still fails, which means the port sits inside a range that Hyper-V, WSL2 or Docker Desktop reserved at boot.
Look at what it is first. An orphaned dev server is safe to kill; a database with a write in flight is not, and neither is a system service that something else depends on. If the owner is a container, stop the container instead — killing the process the host sees only makes the runtime restart it. When the process is something you need, moving your own service to another port is the faster fix.
The listener bound to 127.0.0.1 rather than 0.0.0.0. The probes on every page print the bound address next to the process for exactly this reason: a loopback bind is reachable from the machine itself and from nowhere else, no matter how the firewall is configured. Many frameworks made that the default deliberately, so it is usually a flag rather than a bug. If the address is already 0.0.0.0 and it still fails, the next suspect is a host firewall or a cloud security group.
Barely. Internet-wide scanners sweep the whole port range continuously, so an unusual number buys hours, not safety, and it costs you every colleague who now has to remember it. It does cut the noise in your logs from opportunistic scans of the default port, which is a real if modest benefit. Authentication, a firewall that restricts source addresses, and not exposing the service at all are the measures that actually change the outcome.
Still stuck on port 21? browse every port in the reference — or go back to the failure message you actually got, above.