Networking basics
Addresses, routes, ports and listeners.
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:
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:
A route tells the kernel where to send traffic for a range of addresses. ip route prints the routing table:
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:
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:
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:
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:
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:
ss -ulnp does the same for UDP:
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.
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:
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:
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.
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:
--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:
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.
Firewalls sit in several places. On Ubuntu the host firewall tool is ufw, installed but inactive on a default install:
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:
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:
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.