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
sudo apt install nmap
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
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
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
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
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
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
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
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
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
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.