Networking Basics
Inspecting interfaces and routes with ip, checking listening ports with ss, and basic firewalling with ufw/iptables.
Why every backend engineer needs this
Essential Commands already showed lsof/ss for spotting what's bound to one port. That's one narrow slice of a bigger picture: knowing your machine's own network identity, seeing every connection and listening socket at once, and controlling which ports are actually reachable from outside at all. These are the first things you check when "the app can't reach the database" or "why can users hit a port I never intended to expose."
Checking your own network identity: ip
The modern, actively maintained tool for inspecting and configuring networking on Linux is ip, from the iproute2 package — it replaced the older ifconfig/route/arp commands, which still exist on many systems but are considered legacy.
$ ip addr show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN
inet 127.0.0.1/8 scope host lo
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP
inet 10.0.4.15/24 brd 10.0.4.255 scope global eth0
ip addr show (often shortened to ip a) lists every network interface and the IP address(es) assigned to it — lo is the loopback interface (127.0.0.1, always present, used for a machine to talk to itself), and eth0 here is the real network interface with its actual address, 10.0.4.15.
$ ip route show
default via 10.0.4.1 dev eth0
10.0.4.0/24 dev eth0 proto kernel scope link src 10.0.4.15
ip route show (ip r) shows the routing table — where traffic actually goes. The default via 10.0.4.1 line is the default gateway: anything not matching a more specific route (like the local 10.0.4.0/24 subnet) gets sent there, typically toward the internet.
Seeing every connection and listening socket: ss
ss ("socket statistics") is the modern replacement for the older netstat, and is dramatically faster on a machine with many connections because it reads kernel data structures directly instead of parsing /proc the way netstat historically did.
$ ss -tulpn
Netid State Local Address:Port Peer Address:Port Process
tcp LISTEN 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=812,fd=3))
tcp LISTEN 127.0.0.1:3306 0.0.0.0:* users:(("mysqld",pid=1204,fd=21))
tcp LISTEN 0.0.0.0:80 0.0.0.0:* users:(("nginx",pid=2043,fd=6))
Breaking down the flags, since they come up as a set constantly:
| Flag | Meaning |
|---|---|
-t |
TCP sockets |
-u |
UDP sockets |
-l |
Only listening sockets (not established connections) |
-p |
Show the process (name and PID) holding each socket — usually needs sudo for processes you don't own |
-n |
Show numeric addresses/ports instead of resolving names — faster, and avoids a hung DNS lookup |
Reading the output above: 0.0.0.0:80 means nginx is listening on port 80 on every network interface (reachable from outside), while 127.0.0.1:3306 means MySQL only accepts connections from the machine itself — an intentional and common setup, since a database usually has no business being reachable directly from the internet.
# Every established (active) connection, not just listening sockets
$ ss -tn state established
netstat still works nearly identically on most systems (netstat -tulpn looks almost the same as the ss example above) and you'll still see it in older scripts and documentation, but ss is the one to reach for by default today.
Testing reachability and DNS
# Is the host reachable at all? (ICMP echo)
$ ping -c 4 example.com
# Which hops does traffic take to get there?
$ traceroute example.com
# Can you actually open a TCP connection to a specific port?
$ nc -zv db.internal 5432
Connection to db.internal 5432 port [tcp/*] succeeded!
# What does DNS resolve a name to?
$ dig example.com +short
93.184.216.34
ping only confirms basic reachability at the network layer — a host can block ICMP entirely (many cloud security groups do, by default) while still happily accepting real TCP connections, so a failed ping doesn't necessarily mean the application port is unreachable. nc -zv (netcat, "zero I/O mode, verbose") is the more directly useful check when the real question is "can my app reach that specific port," since it tests the exact same kind of connection the application itself would make.
Basic firewalls: ufw and iptables
Every packet arriving at or leaving a Linux machine passes through the kernel's netfilter framework, which a firewall configures with rules like "allow," "deny," or "drop" based on port, protocol, and source address. Two tools are in wide use, at two different levels of abstraction.
iptables — the traditional, lower-level tool
iptables configures netfilter directly, rule by rule, and is powerful but unforgiving — rule order matters, and it's easy to lock yourself out of a remote server by getting the order wrong.
# Allow established/related connections (so responses to outbound
# traffic, and existing connections, aren't blocked)
$ sudo iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
# Allow SSH explicitly — do this BEFORE any default-deny rule,
# or you will lock yourself out of a remote machine
$ sudo iptables -A INPUT -p tcp --dport 22 -j ACCEPT
# Allow HTTP/HTTPS
$ sudo iptables -A INPUT -p tcp --dport 80 -j ACCEPT
$ sudo iptables -A INPUT -p tcp --dport 443 -j ACCEPT
# Default-deny everything else inbound
$ sudo iptables -P INPUT DROP
# List current rules with line numbers, useful before editing
$ sudo iptables -L INPUT -n --line-numbers
The conceptual model: traffic flows through chains (INPUT for traffic destined for this machine, OUTPUT for traffic leaving it, FORWARD for traffic passing through it as a router), each chain is an ordered list of rules, and the first matching rule wins — which is exactly why the SSH-allow rule above has to exist before the final default-deny policy takes effect.
ufw — "uncomplicated firewall," a friendlier front-end
ufw (default on Ubuntu, available elsewhere) wraps iptables with a much simpler command syntax for the common cases, generating the equivalent low-level rules for you:
$ sudo ufw allow 22/tcp # or: sudo ufw allow ssh
$ sudo ufw allow 80/tcp
$ sudo ufw allow 443/tcp
$ sudo ufw enable
Firewall is active and enabled on system startup
$ sudo ufw status verbose
Status: active
To Action From
-- ------ ----
22/tcp ALLOW IN Anywhere
80/tcp ALLOW IN Anywhere
443/tcp ALLOW IN Anywhere
ufw's default posture (ufw default deny incoming, which is also its factory default) is exactly the same "explicit allow list, deny everything else" model as the raw iptables example — ufw just spares you from hand-writing the chain/rule/priority details and from the very real risk of applying a default-deny before an SSH-allow rule exists.
iptables |
ufw |
|
|---|---|---|
| Abstraction level | Low — direct netfilter rules | High — simple allow/deny by port/service name |
| Risk of self-lockout | Real, if rule order is wrong | Lower — designed around the common safe defaults |
| Flexibility | Maximum — anything netfilter supports | Covers the common cases; falls back to raw iptables for anything exotic |
| Typical use | Complex routing/NAT setups, fine-grained control | A single server's inbound rule set, quickly and safely |
Common mistakes
- Applying a default-deny policy (
iptables -P INPUT DROP, or forgetting to allow SSH before enablingufw) without an explicit SSH-allow rule already in place — this locks you out of a remote server with no console access to fix it. - Confusing a failed
pingwith "the port is unreachable" — many hosts and cloud security groups block ICMP by default while still accepting real TCP traffic on application ports; test the actual port withnc -zvinstead. - Binding a database or internal service to
0.0.0.0(every interface) when it only ever needs to be reached fromlocalhostor an internal network — this needlessly exposes it to anything that can reach the host's public interface at all. - Reaching for old
ifconfig/netstathabits and being confused when they're missing entirely on a minimal container or modern distro image —ipandssare the tools actually guaranteed to be present going forward.