nmap: What Is Actually Listening on Your Network

ss tells you what a machine is listening on. nmap tells you what the network can actually reach, which is the question that matters.

Host discovery and port and service scans, run only against machines and networks you own. Scanning someone else's hosts can break your provider's terms or the law, and it looks exactly like the first step of an attack. Your own LAN, your own servers and a lab VM are fair game.

The examples were run with nmap 7.95 on Debian 13, inside a throwaway VM. To get a small network to scan, the VM has a private bridge, 10.77.0.0/24: the VM itself is 10.77.0.1 and runs ssh and nginx, 10.77.0.2 runs a Python web server on port 8080, and 10.77.0.3 runs nothing. Swap in your own subnet, usually something like 192.168.1.0/24.

install

Debian/Ubuntu
sudo apt install nmap
macOS
brew install nmap

Debian 13 packages 7.95. Homebrew is on a newer release (7.993 at the time of writing). Every flag on this page has been in nmap for years, but we only ran the Debian package.

find the hosts that are up

-sn means discovery only, no port scan. Give it a subnet and it lists every address that answers:

# Which hosts are up? -n skips reverse DNS, which is most of the wait
sudo nmap -sn -n 10.77.0.0/24
output
Nmap scan report for 10.77.0.2
Host is up (0.000021s latency).
MAC Address: E2:1B:BB:04:B7:CA (Unknown)
Nmap scan report for 10.77.0.3
Host is up (0.000033s latency).
MAC Address: E2:42:23:8D:CA:66 (Unknown)
Nmap scan report for 10.77.0.1
Host is up.
Nmap done: 256 IP addresses (3 hosts up) scanned in 2.01 seconds

Output trimmed: every run also starts with a Starting Nmap line. On a local subnet, root lets nmap ask with ARP, which is why MAC addresses show up (the scanning machine, 10.77.0.1, has none). ARP also finds hosts that drop ping: we blocked ICMP echo on 10.77.0.3 with a firewall rule and it still showed as up. Without sudo the same scan found the same three hosts, but with no MAC addresses.

The -n is worth the habit. Without it, nmap looks up a name for every address, and the same scan took 27.95 seconds instead of 2.01 in our lab.

scan for open ports

# The 1,000 most common TCP ports on one host
nmap -n 10.77.0.1
output
Nmap scan report for 10.77.0.1
Host is up (0.000033s latency).
Not shown: 998 closed tcp ports (conn-refused)
PORT   STATE SERVICE
22/tcp open  ssh
80/tcp open  http

The SERVICE column is a guess from the port number, not a check. The default scan only covers 1,000 ports, so scan all 65,535 before you call a host clean:

# Every TCP port
nmap -n -p- 10.77.0.1
output
Not shown: 65532 closed tcp ports (conn-refused)
PORT     STATE SERVICE
22/tcp   open  ssh
80/tcp   open  http
5355/tcp open  llmnr

Port 5355 is systemd-resolved answering LLMNR name lookups, on in this Debian image and missed by the default scan.

# A few ports across the whole subnet, open ones only
nmap -n -p 22,80,8080 --open 10.77.0.0/24
# The 100 most common ports, with faster timing
nmap -n -T4 -F 10.77.0.1

open, closed, filtered

open means something accepted the connection. closed means the host answered that nothing is there. filtered means no answer came back, usually because a firewall dropped the probe. --reason shows what nmap saw. Here 10.77.0.3 has a firewall rule that drops port 22:

# Show why each port got its state
sudo nmap -n -p 22,80 --reason 10.77.0.3
output
Host is up, received arp-response (0.000015s latency).

PORT   STATE    SERVICE REASON
22/tcp filtered ssh     no-response
80/tcp closed   http    reset ttl 64

As root, nmap sends half-open SYN probes (-sS). As a normal user it makes full connections (-sT). That's why closed ports show as reset in one output and conn-refused in the other. For finding what's listening, either works.

what is actually running

-sV talks to each open port and reads what comes back, so the SERVICE column becomes a real answer:

# Ask each open port what it is
sudo nmap -sV -p 22,80,8080,9000 localhost
output
PORT     STATE  SERVICE    VERSION
22/tcp   open   ssh        OpenSSH 10.0p2 Debian 7+deb13u4 (protocol 2.0)
80/tcp   open   http       nginx
8080/tcp closed http-proxy
9000/tcp open   http       SimpleHTTPServer 0.6 (Python 3.13.5)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

A plain scan had called port 9000 cslistener, the name in nmap's port list. -sV found the Python test server we had left running there.

# Run one script from nmap's library against a web port
nmap -n -p 80 --script http-title 10.77.0.1
output
PORT   STATE SERVICE
80/tcp open  http
|_http-title: Welcome to nginx!

UDP

# UDP needs root. Name the ports: a full UDP scan is very slow
sudo nmap -sU -p 53,123,5353 localhost
output
PORT     STATE         SERVICE
53/udp   closed        domain
123/udp  closed        ntp
5353/udp open|filtered zeroconf

UDP services often don't reply to an empty probe, so nmap can't tell open from filtered. open|filtered on 5353 is systemd-resolved's mDNS port, which is listening.

inside view vs outside view

On the machine itself, ss lists every listening socket, including ones bound to localhost only:

# Listening TCP sockets, IPv4, numeric
ss -tln4
output
State  Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0      4096   127.0.0.53%lo:53        0.0.0.0:*
LISTEN 0      4096         0.0.0.0:5355      0.0.0.0:*
LISTEN 0      5          127.0.0.1:9000      0.0.0.0:*
LISTEN 0      4096      127.0.0.54:53        0.0.0.0:*
LISTEN 0      511          0.0.0.0:80        0.0.0.0:*
LISTEN 0      128          0.0.0.0:22        0.0.0.0:*

Port 9000 is bound to 127.0.0.1, so nmap localhost finds it, but the scans of 10.77.0.1 above don't. Anything on 0.0.0.0 is on every interface, so another machine can reach it unless a firewall says no. Scan from a second machine to see what the network sees. In the lab, a scan from 10.77.0.3 found 22 and 80 on 10.77.0.1, and no 9000. The process one-liners show which process owns a port.

save the results

# Keep a readable copy and a grep-friendly one
nmap -n -T4 -F 10.77.0.1 -oN scan.txt -oG scan.gnmap
grep Ports: scan.gnmap
output
Host: 10.77.0.1 ()	Ports: 22/open/tcp//ssh///, 80/open/tcp//http///	Ignored State: closed (98)

Save a scan of your network when it looks right, and scan again after changes. A new line in the diff is a new thing listening.

pro tip

After you add a firewall rule, scan the port from another machine with --reason. A port you meant to close should come back filtered with no-response. If it still says open, the rule isn't doing what you think.

honest limits

Everything here ran with nmap 7.95 on Debian 13, against a private bridge inside one VM, not a real home network with Wi-Fi, a router and sleeping phones. Those take longer and miss hosts that are asleep. OS detection (sudo nmap -O) found no exact match for our lab hosts, so it isn't covered. We didn't test the Homebrew build or nmap on macOS. Results from a scan are a snapshot: a laptop that was off is not in it.