Networking basics

Addresses, routes, ports and listeners.

Beginner14 min · lesson 23 of 29

When something cannot connect, four questions find the cause: which addresses the machine has, which way its traffic leaves, what is listening and on which address, and what the error message says about where the connection failed. This lesson answers each of them with ip and ss, the two commands from the iproute2 package, and with ping, curl and nc for testing. The outputs come from a default Ubuntu Server 26.04 install and from a second, simulated server that the lab sets up so that failures can be produced safely.

Interfaces, addresses and routes

A network interface is a connection point to a network, usually a network card. ip address lists every interface and the addresses on it:

deploy@web01 · Ubuntu 26.04 LTS (default install)
$ ip address
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet 127.0.0.1/8 scope host lo valid_lft forever preferred_lft forever inet6 ::1/128 scope host noprefixroute valid_lft forever preferred_lft forever 2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000 link/ether 52:55:55:e7:32:25 brd ff:ff:ff:ff:ff:ff altname enx525555e73225 inet 192.168.5.15/24 metric 200 brd 192.168.5.255 scope global dynamic eth0 valid_lft 3288sec preferred_lft 3288sec inet6 fe80::5055:55ff:fee7:3225/64 scope link proto kernel_ll valid_lft forever preferred_lft forever

lo is the loopback interface, which the machine uses to talk to itself; its address 127.0.0.1 never leaves the machine. eth0 is the network card. UP,LOWER_UP means it is switched on and has a link, and link/ether is its hardware (MAC) address. inet 192.168.5.15/24 is its IPv4 address. The /24 is the prefix length: the first 24 bits, 192.168.5, name the local network, so every host on it, 192.168.5.1 to 192.168.5.254, can be reached directly, without a router (.0 names the network itself and .255 is its broadcast address). dynamic and valid_lft show the address was leased from a DHCP server and for how long. The inet6 fe80:: line is an IPv6 link-local address, which every IPv6 interface has and which works only on its own network segment.

The name eth0 comes from the lab's virtual machine, whose network configuration renames the card. On a physical or cloud server you will usually see a predictable name built from where the card sits, such as enp0s1 or ens3. The altname line is another predictable name systemd derived from the MAC address. ip -br address prints the same facts one line per interface:

deploy@web01 · Ubuntu 26.04 LTS (default install)
$ ip -br address
lo UNKNOWN 127.0.0.1/8 ::1/128 eth0 UP 192.168.5.15/24 metric 200 fe80::5055:55ff:fee7:3225/64

A route tells the kernel where to send traffic for a range of addresses. ip route prints the routing table:

deploy@web01 · Ubuntu 26.04 LTS (default install)
$ ip route
default via 192.168.5.2 dev eth0 proto dhcp src 192.168.5.15 metric 200 192.168.5.0/24 dev eth0 proto kernel scope link src 192.168.5.15 metric 200 192.168.5.2 dev eth0 proto dhcp scope link src 192.168.5.15 metric 200
$ ip route get 9.9.9.9
9.9.9.9 via 192.168.5.2 dev eth0 src 192.168.5.15 uid 1001 cache

The default line matters most: anything not covered by a more specific route goes to the gateway 192.168.5.2, the router of this network, through eth0. proto dhcp says the route came from the DHCP lease, proto kernel that the kernel added it for the local network, and metric breaks ties when two routes match. ip route get asks which route the kernel would pick for one address, which settles questions such as "is this traffic going through the VPN interface or not?".

Where this configuration comes from differs by distribution. Ubuntu Server describes the network in YAML files under /etc/netplan/, and netplan hands them to systemd-networkd, which configures the interfaces:

deploy@web01 · Ubuntu 26.04 LTS (default install)
$ networkctl
IDX LINK TYPE OPERATIONAL SETUP 1 lo loopback carrier unmanaged 2 eth0 ether routable configured 2 links listed.
$ sudo cat /etc/netplan/50-cloud-init.yaml
network: version: 2 ethernets: eth0: match: macaddress: "52:55:55:e7:32:25" nameservers: addresses: - 192.168.5.2 dhcp-identifier: "mac" dhcp4: true dhcp4-overrides: route-metric: 200 set-name: "eth0"

routable and configured mean systemd-networkd manages eth0 and it has a working address. The file was written by cloud-init, the program that configures cloud images at first boot: it matches the card by MAC address, names it eth0 and asks DHCP for an address. After editing a netplan file on a remote server, apply it with sudo netplan try, which rolls the change back unless you confirm it within a time limit. RHEL 10 uses NetworkManager instead, driven with nmcli:

deploy@rocky10 · Rocky Linux 10.2
$ nmcli device status
DEVICE TYPE STATE CONNECTION eth0 ethernet connected cloud-init eth0 lo loopback connected (externally) lo

Older guides use ifconfig and netstat from the legacy net-tools package; ip and ss replaced them, and neither legacy command is installed on Ubuntu 26.04 or RHEL 10 by default:

deploy@web01 · Ubuntu 26.04 LTS (default install)
$ command -v ifconfig netstat ip ss
/usr/sbin/ip /usr/bin/ss
# only ip and ss are found: the legacy ifconfig and netstat are not installed

Ports and listening sockets

An address gets traffic to the right machine; a port number (0 to 65535) gets it to the right program on that machine. Services use well-known ports by convention: 22 for SSH, 53 for DNS, 80 and 443 for web servers. A socket is a program's end of a network conversation. A listening socket is a program waiting for connections on an address and port; an established socket is one connection in progress. TCP is the protocol most services use, with a connection set up before any data flows; UDP sends single messages with no connection, as DNS lookups mostly do.

ss -tlnp lists listening TCP sockets: -t for TCP, -l for listening, -n for numbers instead of service names, -p for the process that owns each socket. Run it without and then with sudo:

deploy@web01 · Ubuntu 26.04 LTS (default install)
$ ss -tlnp
State Recv-Q Send-Q Local Address:Port Peer Address:PortProcess LISTEN 0 4096 127.0.0.53%lo:53 0.0.0.0:* LISTEN 0 4096 0.0.0.0:22 0.0.0.0:* LISTEN 0 4096 127.0.0.54:53 0.0.0.0:* LISTEN 0 4096 [::]:22 [::]:*
$ sudo ss -tlnp
State Recv-Q Send-Q Local Address:Port Peer Address:PortProcess LISTEN 0 4096 127.0.0.53%lo:53 0.0.0.0:* users:(("systemd-resolve",pid=471,fd=20)) LISTEN 0 4096 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=882,fd=3),("systemd",pid=1,fd=260)) LISTEN 0 4096 127.0.0.54:53 0.0.0.0:* users:(("systemd-resolve",pid=471,fd=22)) LISTEN 0 4096 [::]:22 [::]:* users:(("sshd",pid=882,fd=4),("systemd",pid=1,fd=261))

Without sudo the Process column stays empty for sockets that belong to other users, so always add it. Read the Local Address:Port column first. 0.0.0.0:22 and [::]:22 mean SSH listens on every IPv4 and every IPv6 address the machine has, so other machines can reach it. 127.0.0.53%lo:53 and 127.0.0.54:53 are local addresses: systemd-resolved's DNS service answers only programs on this machine ("DNS and name resolution" explains it).

Port 22 shows two processes, systemd as PID 1 and sshd. This is socket activation, from "Services with systemd": ssh.socket makes systemd create the listening socket at boot, and when the first connection arrives systemd starts ssh.service and hands it the socket, so both hold it. Before anyone has connected, only systemd appears. systemctl list-sockets shows which unit a socket starts:

deploy@web01 · Ubuntu 26.04 LTS (default install)
$ systemctl list-sockets ssh.socket
LISTEN UNIT ACTIVATES 0.0.0.0:22 ssh.socket ssh.service [::]:22 ssh.socket ssh.service 2 sockets listed. Pass --all to see loaded but inactive sockets, too.

ss -ulnp does the same for UDP:

deploy@web01 · Ubuntu 26.04 LTS (default install)
$ sudo ss -ulnp
State Recv-Q Send-Q Local Address:Port Peer Address:PortProcess UNCONN 0 0 0.0.0.0:5353 0.0.0.0:* users:(("systemd-resolve",pid=471,fd=13)) UNCONN 0 0 127.0.0.54:53 0.0.0.0:* users:(("systemd-resolve",pid=471,fd=21)) UNCONN 0 0 127.0.0.53%lo:53 0.0.0.0:* users:(("systemd-resolve",pid=471,fd=19)) UNCONN 0 0 192.168.5.15%eth0:68 0.0.0.0:* users:(("systemd-network",pid=775,fd=36)) UNCONN 0 0 127.0.0.1:323 0.0.0.0:* users:(("chronyd",pid=930,fd=4)) UNCONN 0 0 [::]:5353 [::]:* users:(("systemd-resolve",pid=471,fd=14)) UNCONN 0 0 [::1]:323 [::]:* users:(("chronyd",pid=930,fd=5))

Port 68 is the DHCP client in systemd-networkd, bound to eth0, and port 323 is chronyd, the time service, taking commands from this machine only. Port 5353 (multicast DNS) is switched on by the lab's virtualisation tool; Ubuntu ships it off. So a default Ubuntu Server accepts connections from other machines only on SSH, plus replies to its own DHCP requests. On any server you are handed, run sudo ss -tlnp and sudo ss -ulnp first and make sure you can name every line.

0.0.0.0 or 127.0.0.1: who can reach a service

To test connections the lab adds a second machine: a network namespace (an isolated copy of the kernel's network stack; the Linux internals course covers them) with the address 192.0.2.20, joined to web01 by a virtual cable. Commands that start with sudo ip netns exec ess-net-srv run on that server, as they would after logging in to it.

deploy@web01 · Ubuntu 26.04 LTS
$ ip -br address
lo UNKNOWN 127.0.0.1/8 ::1/128 eth0 UP 192.168.5.15/24 metric 200 fe80::5055:55ff:feb5:b4a4/64 ess-net0@if2 UP 192.0.2.1/24 fe80::d41b:79ff:fe00:107f/64
$ ip route get 192.0.2.20
192.0.2.20 dev ess-net0 src 192.0.2.1 uid 1001 cache
$ ping -c 2 192.0.2.20
PING 192.0.2.20 (192.0.2.20) 56(84) bytes of data. 64 bytes from 192.0.2.20: icmp_seq=1 ttl=64 time=0.432 ms 64 bytes from 192.0.2.20: icmp_seq=2 ttl=64 time=0.225 ms --- 192.0.2.20 ping statistics --- 2 packets transmitted, 2 received, 0% packet loss, time 1021ms rtt min/avg/max/mdev = 0.225/0.328/0.432/0.103 ms

ess-net0 is web01's end of the virtual cable. ping sends ICMP echo requests and prints each reply with its round-trip time, so the server is up and reachable. On the server, two copies of Python's built-in web server are listening:

deploy@web01 · Ubuntu 26.04 LTS
$ sudo ip netns exec ess-net-srv ss -tlnp
State Recv-Q Send-Q Local Address:Port Peer Address:PortProcess LISTEN 0 5 0.0.0.0:8080 0.0.0.0:* users:(("python3",pid=1270718,fd=3)) LISTEN 0 5 127.0.0.1:8081 0.0.0.0:* users:(("python3",pid=1270719,fd=3))

One listens on 0.0.0.0:8080, every address, and the other on 127.0.0.1:8081, the server's loopback address only. curl fetches a URL and prints the reply; -sS hides the progress bar but keeps error messages:

deploy@web01 · Ubuntu 26.04 LTS
$ curl -sS http://192.0.2.20:8080/
hello from the app server
$ curl -sS http://192.0.2.20:8081/
curl: (7) Failed to connect to 192.0.2.20 port 8081 after 0 ms: Could not connect to server
$ sudo ip netns exec ess-net-srv curl -sS http://127.0.0.1:8081/
admin page

Port 8080 answers from web01. Port 8081 refuses: the connection reached 192.0.2.20, but nothing listens on that address and port, because the service is bound to the server's loopback address. On the server itself, the same service answers on 127.0.0.1. The bind address decides who can reach a service, before any firewall is involved.

Where a service listens decides who can reach it
127.0.0.1 or ::1
Reachable from
this machine only
Use for
databases, caches, admin endpoints
0.0.0.0 or [::]
Reachable from
every network the machine is on
Use for
services meant for other machines
One address, e.g. 192.0.2.20
Reachable from
networks that can route to it
Use for
a service for one network only
Read the Local Address column of sudo ss -tlnp for every listener.

A database, cache or admin page listening on 0.0.0.0 when it only needs local clients is one of the most common serious exposures on Linux servers, and on a cloud server every address can include a public one. Most services set this with a bind or listen setting in their configuration.

Refused, timed out or no route

A failed connection says where it failed, if you read the message. On the simulated server a firewall rule drops everything sent to TCP port 8443, where nothing listens anyway:

deploy@web01 · Ubuntu 26.04 LTS
$ sudo ip netns exec ess-net-srv nft list ruleset
table inet ess_net { chain input { type filter hook input priority filter; policy accept; tcp dport 8443 drop } }
$ curl -sS --connect-timeout 5 http://192.0.2.20:8443/
curl: (28) Connection timed out after 5011 milliseconds

--connect-timeout 5 limits the wait; without it curl would wait much longer. nc -zv (netcat) only tries to connect and reports the result, which makes it a quick test for any TCP port, and -w 5 sets its timeout. Here are the three outcomes side by side, and a fourth for an address no machine holds:

deploy@web01 · Ubuntu 26.04 LTS
$ nc -zv -w 5 192.0.2.20 8080 nc -zv -w 5 192.0.2.20 8081 nc -zv -w 5 192.0.2.20 8443
Connection to 192.0.2.20 8080 port [tcp/http-alt] succeeded! nc: connect to 192.0.2.20 port 8081 (tcp) failed: Connection refused nc: connect to 192.0.2.20 port 8443 (tcp) timed out: Operation now in progress
$ nc -zv -w 5 192.0.2.99 8080
nc: connect to 192.0.2.99 port 8080 (tcp) failed: No route to host
$ ping -c 2 192.0.2.99
PING 192.0.2.99 (192.0.2.99) 56(84) bytes of data. From 192.0.2.1 icmp_seq=1 Destination Host Unreachable From 192.0.2.1 icmp_seq=2 Destination Host Unreachable --- 192.0.2.99 ping statistics --- 2 packets transmitted, 0 received, +2 errors, 100% packet loss, time 1038ms pipe 2

Connection refused comes back at once: the server's kernel answered that nothing listens on that port. curl's version of it is "Could not connect to server", and the time in the message is a few milliseconds, where a timeout takes seconds. A firewall set to reject rather than drop can also answer this way. A timeout means no answer came back: a firewall dropped the packets, or the machine is down or cut off somewhere on the way. ping succeeded a moment ago, so this machine is up and something is filtering that one port. ping tests the host, never the port, and some networks and firewalls block ping altogether, so a failed ping does not prove a host is down. No route to host, for an address on the local network, means no machine answered web01's request for the owner of that address (ARP, the protocol that finds the hardware address behind an IP address); a router or firewall can also send it back deliberately.

What a failed connection tells you
A connection to a host and port fails
read the exact error before changing anything
Connection refused
The host answered: nothing listens there
on the server: sudo ss -tlnp, check the bind address
Timed out
No answer: packets are dropped
host firewall, cloud security group, or host down
No route to host
The host cannot be found on the path
address, network prefix, gateway
Could not resolve host
The name failed, not the network
see "DNS and name resolution"

Firewalls sit in several places. On Ubuntu the host firewall tool is ufw, installed but inactive on a default install:

deploy@web01 · Ubuntu 26.04 LTS (default install)
$ sudo ufw status
Status: inactive

RHEL servers run firewalld instead, and clouds add their own filters, such as security groups, outside the machine. The hardening course sets up both host firewalls.

Established connections

Listening sockets are what other machines can try to reach; established sockets are the conversations in progress. In a second terminal, nc 192.0.2.20 8080 opens a connection to the web server and holds it open while it waits for input. List the established TCP connections:

deploy@web01 · Ubuntu 26.04 LTS
$ ss -tnp state established
Recv-Q Send-Q Local Address:Port Peer Address:PortProcess 0 0 192.0.2.1:41664 192.0.2.20:8080 users:(("nc",pid=1271162,fd=3))

Each connection is identified by both ends: web01's address and a temporary port the kernel picked, and the server's address and port 8080. The process is visible without sudo because nc runs as deploy. The server sees the same connection from its side:

deploy@web01 · Ubuntu 26.04 LTS
$ sudo ip netns exec ess-net-srv ss -tnp state established
Recv-Q Send-Q Local Address:Port Peer Address:Port Process 0 0 192.0.2.20:8080 192.0.2.1:41664 users:(("python3",pid=1270718,fd=4))

On a real server your own SSH login appears in this list as an established connection to port 22, owned by an sshd-session process, the part of OpenSSH that serves one login. A connection you cannot explain, especially an outgoing one from a program that has no reason to talk to the internet, deserves investigation.

Try this

In a second terminal on your lab machine, start a small web server bound to the loopback address: python3 -m http.server 8090 --bind 127.0.0.1. Find it with ss -tlnp sport = :8090, where sport = :8090 is a filter that keeps only sockets on local port 8090 (no sudo needed, since it is your own process), and fetch it with curl -sS http://127.0.0.1:8090/. Now use the machine's network address instead: curl -sS http://$(hostname -I | cut -d' ' -f1):8090/ (hostname -I prints the machine's addresses and cut keeps the first) reports "Could not connect to server", a refusal, although the request never left the machine. Stop the server with Ctrl-C, start it again with --bind 0.0.0.0, and check that ss now shows 0.0.0.0:8090 and the same curl now gets a reply (a listing of the directory you started the server in). If another machine can reach your lab machine (a second VM, or a VM on a bridged or host-only network; many local VMs sit behind NAT and cannot be reached from the workstation), try the address from there too. If it times out there, something between the two machines is dropping the packets.

Takeaway

Read the error before you change anything: refused means nothing listens on that address and port, a timeout means something is dropping the packets. On the server, sudo ss -tlnp shows which address a service is bound to, and that is the first thing to check.

Quick check
01An application server gets "Connection refused" when it connects to a new database server on port 5432. On the database server, sudo ss -tlnp shows LISTEN 127.0.0.1:5432 for postgres. What is wrong?
Incorrect — Dropped packets produce a timeout, not an immediate refusal. A refusal means the connection reached the host.
Incorrect — ss lists sockets that exist now, with the process that holds them. PostgreSQL is running.
Correct — 127.0.0.1 accepts connections from the machine itself. Connections to the server's network address find no listener and are refused.
Incorrect — One port serves every client that can reach its listening address. The listening address is the problem, not the port.
02curl to a web service waits and finally prints "Connection timed out", while ping to the same host gets replies. What does that tell you?
Correct — The host is up (it answers ping), but nothing comes back from that port, which is what a dropping firewall rule looks like.
Incorrect — A missing listener produces an immediate "Connection refused", because the host answers.
Incorrect — ping replies come from the host itself. There is no cache that answers pings on its behalf.
Incorrect — A failed name lookup gives "Could not resolve host" at once, before any connection attempt.
03sudo ss -tlnp on an Ubuntu 26.04 server shows users:(("sshd",pid=882,fd=3),("systemd",pid=1,fd=260)) on 0.0.0.0:22. What does the systemd entry mean?
Incorrect — There is one listening socket held by two processes, not two servers. Stopping either would not remove a duplicate.
Incorrect — systemd only accepts the listening socket. sshd itself serves each connection once it has started.
Incorrect — sshd appears in the same line, alive, sharing the same socket.
Correct — With socket activation, PID 1 creates the listening socket and hands it to the service, so both hold it.

Related