Where this fits, and which tools

The theory behind every command on this page — addresses, routes, the Address Resolution Protocol (ARP), ports, the Domain Name System (DNS) — is in the Networking topic, and the method for finding a fault is in Troubleshooting Tools and Method. This page is the Linux side: which command answers which question, how to read its output, and how to make a change that survives a reboot. Most commands only look and work as an ordinary user; anything that changes the network needs sudo.

Many tutorials still use the old net-tools commands. They are deprecated, no longer installed by default on most distributions, and do not show everything modern Linux networking can do. Their replacements come from the iproute2 package:

Question Old (net-tools) Current (iproute2) Theory
Addresses and interfaces ifconfig ip addr, ip link IPv4 Addressing
Routing table route -n ip route Routing Basics
Neighbor (ARP) cache arp -n ip neigh ARP and ICMP
Open ports and connections netstat -tulpn ss -tulpn TCP, UDP and Ports

Interfaces, addresses and routes: ip

The ip command (from iproute2) shows and changes the kernel's network configuration: interfaces (link), addresses (addr), routes (route) and the neighbor cache (neigh). Objects may be abbreviated — ip a, ip r, ip n — and -br (brief) gives one line per interface, which is often all you need:

$ ip -br link
$ ip -br addr
$ ip route
lo               UNKNOWN        00:00:00:00:00:00 <LOOPBACK,UP,LOWER_UP>
enp1s0           UP             00:00:5e:00:53:21 <BROADCAST,MULTICAST,UP,LOWER_UP>
enp2s0           DOWN           00:00:5e:00:53:22 <NO-CARRIER,BROADCAST,MULTICAST,UP>
lo               UNKNOWN        127.0.0.1/8 ::1/128
enp1s0           UP             192.168.1.50/24 2001:db8:1::50/64 fe80::200:5eff:fe00:5321/64
enp2s0           DOWN
default via 192.168.1.1 dev enp1s0 proto dhcp src 192.168.1.50 metric 100
192.168.1.0/24 dev enp1s0 proto kernel scope link src 192.168.1.50 metric 100

Interface names such as enp1s0 are "predictable names" based on where the network card sits (Peripheral Component Interconnect bus 1, slot 0); older systems and many virtual machines use eth0. enp2s0 is enabled (UP in the flags) but has NO-CARRIER: no cable, or nothing at the other end. How to read the full ip addr, ip route and ip neigh output is shown step by step in Addresses, routes and neighbors.

One more route command is worth knowing: ip route get asks the kernel which route, interface and source address it would use for one destination — no guessing from the table.

$ ip route get 198.51.100.10
198.51.100.10 via 192.168.1.1 dev enp1s0 src 192.168.1.50 uid 1000
    cache

Using ip output in scripts

ip -o (one line) prints each address on a single line, which is made for grep and awk. (For programs, ip -j prints JSON, JavaScript Object Notation.) Here a saved ip -o addr output is turned into a short report:

cat > ip-o.txt <<'EOF'
1: lo    inet 127.0.0.1/8 scope host lo\       valid_lft forever preferred_lft forever
1: lo    inet6 ::1/128 scope host noprefixroute \       valid_lft forever preferred_lft forever
2: enp1s0    inet 192.168.1.50/24 brd 192.168.1.255 scope global dynamic noprefixroute enp1s0\       valid_lft 82345sec preferred_lft 82345sec
2: enp1s0    inet6 2001:db8:1::50/64 scope global dynamic noprefixroute \       valid_lft 86124sec preferred_lft 14124sec
2: enp1s0    inet6 fe80::200:5eff:fe00:5321/64 scope link noprefixroute \       valid_lft forever preferred_lft forever
3: docker0    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0\       valid_lft forever preferred_lft forever
EOF

echo "Addresses (loopback left out):"
awk '$2 != "lo" {
  for (i = 4; i <= NF; i++) if ($i == "scope") scope = $(i + 1)
  printf "  %-8s %-6s %-28s %s\n", $2, $3, $4, scope
}' ip-o.txt

echo "IPv4 address of enp1s0, without prefix length:"
awk '$2 == "enp1s0" && $3 == "inet" { split($4, part, "/"); print "  " part[1] }' ip-o.txt
Addresses (loopback left out):
  enp1s0   inet   192.168.1.50/24              global
  enp1s0   inet6  2001:db8:1::50/64            global
  enp1s0   inet6  fe80::200:5eff:fe00:5321/64  link
  docker0  inet   172.17.0.1/16                global
IPv4 address of enp1s0, without prefix length:
  192.168.1.50

Field 2 is the interface, field 3 the family (inet for IPv4, inet6 for IPv6) and field 4 the address with its prefix length. The scope is found by looking for the word scope instead of a fixed position, because IPv4 lines have an extra brd (broadcast) pair. split() cuts 192.168.1.50/24 at the slash.

Temporary changes with ip

$ sudo ip link set enp2s0 up
$ sudo ip addr add 192.0.2.10/24 dev enp2s0
$ sudo ip route add 203.0.113.0/24 via 192.0.2.1
$ sudo ip addr del 192.0.2.10/24 dev enp2s0

These act immediately — and disappear at the next reboot, or as soon as NetworkManager or systemd-networkd reapplies its own configuration. That makes them good for a quick test and wrong for a permanent setting; for that, see Configuring the network.

Listening ports and connections: ss

The ss command (socket statistics) lists the machine's sockets: the ports it listens on and the connections it has open. It replaced netstat. The classic combination is ss -tulpn:

Option Meaning
-t, -u Transmission Control Protocol (TCP) and User Datagram Protocol (UDP) sockets
-l Only listening sockets (servers waiting for clients)
-p The process that owns the socket (other users' processes only with sudo)
-n Numbers instead of names: 22, not ssh
$ sudo ss -tulpn
Netid State  Recv-Q Send-Q Local Address:Port  Peer Address:Port Process
udp   UNCONN 0      0         127.0.0.54:53         0.0.0.0:*     users:(("systemd-resolve",pid=611,fd=16))
udp   UNCONN 0      0      127.0.0.53%lo:53         0.0.0.0:*     users:(("systemd-resolve",pid=611,fd=14))
tcp   LISTEN 0      4096   127.0.0.53%lo:53         0.0.0.0:*     users:(("systemd-resolve",pid=611,fd=15))
tcp   LISTEN 0      511          0.0.0.0:80         0.0.0.0:*     users:(("nginx",pid=1026,fd=6))
tcp   LISTEN 0      200        127.0.0.1:5432       0.0.0.0:*     users:(("postgres",pid=990,fd=6))
tcp   LISTEN 0      4096         0.0.0.0:22         0.0.0.0:*     users:(("sshd",pid=812,fd=3))
tcp   LISTEN 0      511             [::]:80            [::]:*     users:(("nginx",pid=1026,fd=7))
tcp   LISTEN 0      4096            [::]:22            [::]:*     users:(("sshd",pid=812,fd=4))

The column to read first is Local Address:Port. It tells you not only the port but who can reach it:

  • 0.0.0.0:80 and [::]:80: listening on every IPv4 and IPv6 address of the machine — reachable from the network (unless a firewall blocks it).
  • 127.0.0.1:5432: only on the loopback address, so only programs on this machine can connect. That is the safe default for a database used by a local application.
  • 127.0.0.53%lo:53: the local DNS stub resolver of systemd-resolved (see name resolution).

A quick exposure report from the same (saved) output:

cat > listening.txt <<'EOF'
Netid State  Recv-Q Send-Q Local Address:Port  Peer Address:Port Process
udp   UNCONN 0      0         127.0.0.54:53         0.0.0.0:*     users:(("systemd-resolve",pid=611,fd=16))
udp   UNCONN 0      0      127.0.0.53%lo:53         0.0.0.0:*     users:(("systemd-resolve",pid=611,fd=14))
tcp   LISTEN 0      4096   127.0.0.53%lo:53         0.0.0.0:*     users:(("systemd-resolve",pid=611,fd=15))
tcp   LISTEN 0      511          0.0.0.0:80         0.0.0.0:*     users:(("nginx",pid=1026,fd=6))
tcp   LISTEN 0      200        127.0.0.1:5432       0.0.0.0:*     users:(("postgres",pid=990,fd=6))
tcp   LISTEN 0      4096         0.0.0.0:22         0.0.0.0:*     users:(("sshd",pid=812,fd=3))
tcp   LISTEN 0      511             [::]:80            [::]:*     users:(("nginx",pid=1026,fd=7))
tcp   LISTEN 0      4096            [::]:22            [::]:*     users:(("sshd",pid=812,fd=4))
EOF

awk 'NR > 1 {
  addr = $5
  port = addr; sub(/.*:/, "", port)
  prog = $7;   sub(/^users:\(\("/, "", prog); sub(/".*/, "", prog)
  if (addr ~ /^(0\.0\.0\.0|\[::\]|\*):/) where = "all interfaces"
  else where = "this host only"
  print $1 "/" port, prog, "-", where
}' listening.txt | sort -u
tcp/22 sshd - all interfaces
tcp/53 systemd-resolve - this host only
tcp/5432 postgres - this host only
tcp/80 nginx - all interfaces
udp/53 systemd-resolve - this host only

sub(/.*:/, "", port) removes everything up to the last colon, which also works for IPv6 addresses; the IPv4 and IPv6 lines of the same service give the same result, and sort -u keeps one of each. Every "all interfaces" line is a door into this server: each one should be a service you meant to offer, and the host firewall should allow only those.

Without -l, ss shows connections instead: ss -tn lists established TCP connections with the remote address, and ss -tn state established '( dport = :443 )' filters them, for example to see who this server talks to over HTTPS (Hypertext Transfer Protocol Secure).

Reachability: ping, traceroute and tracepath

ping tests whether a host answers and how long the answer takes; traceroute shows the routers on the way. Both are explained, with output, in Ping and Traceroute. On Linux, remember:

  • ping runs until you press Ctrl+C; ping -c 4 sends four packets and stops. ping -6 or ping6 forces IPv6.
  • traceroute is often not installed. tracepath does the same job without root rights and also reports the path MTU (maximum transmission unit); mtr combines ping and traceroute in one live view.
  • No answer to a ping does not prove a host is down: many firewalls drop ICMP (Internet Control Message Protocol). Test the actual service port as well (see curl).
$ tracepath -n 203.0.113.80
 1?: [LOCALHOST]                      pmtu 1500
 1:  192.168.1.1                                           0.612ms
 1:  192.168.1.1                                           0.498ms
 2:  198.51.100.1                                          8.917ms
 3:  no reply
 4:  198.51.100.45                                        11.203ms
 5:  203.0.113.80                                         12.040ms reached
     Resume: pmtu 1500 hops 5 back 5

Name resolution: dig, resolvectl and the files

dig (domain information groper) sends a DNS query and prints the complete answer, which makes it the tool of choice for testing DNS itself. How to read its full output is covered in DNS and Troubleshooting; in daily work, these forms are the most useful:

$ dig +short www.example.com
$ dig +short www.example.com AAAA
$ dig +short example.com MX
$ dig @192.0.2.53 +short www.example.com
$ dig +short -x 203.0.113.80
203.0.113.80
2001:db8:80::80
10 mail.example.com.
203.0.113.80
www.example.com.

+short prints only the answer. A record type after the name asks for that type (AAAA for IPv6, MX for mail servers); @server asks one particular DNS server, which is how you compare the company's server with a public one; -x does a reverse lookup. If dig is missing, it is in the dnsutils (Debian, Ubuntu) or bind-utils (Red Hat Enterprise Linux (RHEL), Fedora) package.

Which DNS servers the system uses

On Ubuntu and Fedora (and on many other systemd systems), the systemd-resolved service handles name lookups for programs. /etc/resolv.conf then points to its local stub resolver at 127.0.0.53, and the real DNS servers (from the Dynamic Host Configuration Protocol (DHCP), NetworkManager or netplan) are only visible through resolvectl:

$ cat /etc/resolv.conf
$ resolvectl status enp1s0
# This is /run/systemd/resolve/stub-resolv.conf managed by man:systemd-resolved(8).
# Do not edit.
…
nameserver 127.0.0.53
options edns0 trust-ad
search corp.example.com
Link 2 (enp1s0)
    Current Scopes: DNS
         Protocols: +DefaultRoute -LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported
Current DNS Server: 192.168.1.1
       DNS Servers: 192.168.1.1
        DNS Domain: corp.example.com

resolvectl query www.example.com resolves a name the way programs on this machine do, and sudo resolvectl flush-caches empties the local cache after a DNS change. On RHEL 9 and Debian, systemd-resolved is usually not active and /etc/resolv.conf lists the real servers directly. On RHEL 9, NetworkManager generates it; change DNS with nmcli. On a Debian server with ifupdown and no resolvconf, it is an ordinary file that you edit directly. With systemd-resolved, as above, don't edit it by hand: it is generated, and your change is overwritten; change the DNS servers in the network configuration instead. Check the comment at its top before editing.

/etc/hosts and getent

Before asking DNS, the system looks in /etc/hosts, a local list of names and addresses (the order is set in /etc/nsswitch.conf, normally files dns). It is handy for testing a new server under its future name, and a classic cause of "it works on my machine" when an old entry is forgotten (see the hosts file).

127.0.0.1       localhost
127.0.1.1       web01.corp.example.com web01
192.0.2.40      shop-new.example.com     # test the new server before the DNS switch

::1             localhost ip6-localhost ip6-loopback

Testing web services: curl and wget

curl is a command-line client for URLs (Uniform Resource Locators): it sends a request over the Hypertext Transfer Protocol (HTTP), HTTPS and many other protocols and prints the response. That makes it the way to test a web service or an API (application programming interface) from a server without a browser.

$ curl -I https://www.example.com/
HTTP/2 200
content-type: text/html; charset=UTF-8
last-modified: Mon, 28 Sep 2026 14:02:11 GMT
cache-control: max-age=3600
content-length: 1256
date: Wed, 30 Sep 2026 10:12:44 GMT
Option Meaning
-I Only the response headers (a HEAD request): status code, server, caching
-v Verbose: the address it connects to, the TLS (Transport Layer Security) handshake and certificate, the request and the response headers — the most useful option for troubleshooting
-o file, -O Save the body to a file (-O: use the remote file name)
-L Follow redirects (a 301 or 302 to another address)
-sS, -f Silent but show errors; fail with an exit code on HTTP errors (for scripts)
--resolve name:443:addr Connect to this address for the name, without touching DNS or /etc/hosts
$ curl -v http://127.0.0.1:8080/health
*   Trying 127.0.0.1:8080...
* connect to 127.0.0.1 port 8080 from 127.0.0.1 port 45918 failed: Connection refused
* Failed to connect to 127.0.0.1 port 8080 after 0 ms: Couldn't connect to server
* Closing connection
curl: (7) Failed to connect to 127.0.0.1 port 8080 after 0 ms: Couldn't connect to server

"Connection refused" means the machine answered, but nothing listens on that port — check the service and ss -tlnp. A timeout instead usually means a firewall silently drops the packets, or the host is unreachable. (A firewall that rejects instead of dropping gives "No route to host" — firewalld's default — or even "Connection refused".) An HTTP status such as 502 or 503 means the connection works and the problem is in the application behind it.

wget is the simpler downloader: wget URL saves the file under its own name, wget -c resumes an interrupted download, and it can download recursively. For testing and APIs, use curl; for fetching a file, either works.

Configuring the network

The host name: hostnamectl

$ hostnamectl
$ sudo hostnamectl set-hostname web02
 Static hostname: web01
       Icon name: computer-vm
         Chassis: vm
      Machine ID: 3f1c…
         Boot ID: 9a02…
  Virtualization: kvm
Operating System: Ubuntu 24.04.3 LTS
          Kernel: Linux 6.8.0-85-generic
    Architecture: x86-64

set-hostname writes /etc/hostname and takes effect immediately (the prompt changes at your next login). Also update the host's own line in /etc/hosts; on Debian and Ubuntu that is the 127.0.1.1 line, and a mismatch makes sudo print "unable to resolve host".

NetworkManager and nmcli

NetworkManager is the service that manages network connections on RHEL, Fedora, and Ubuntu and Debian desktops: it brings interfaces up, runs DHCP, applies static settings and DNS servers, and stores each configuration as a named connection profile. nmcli is its command-line client (nmtui is a text-menu version of the same).

$ nmcli device status
$ nmcli connection show
DEVICE  TYPE      STATE                   CONNECTION
enp1s0  ethernet  connected               enp1s0
lo      loopback  connected (externally)  lo
NAME    UUID                                  TYPE      DEVICE
enp1s0  6f1b2c1e-4d6a-4b8e-9c61-2b5f0a7e3d12  ethernet  enp1s0
lo      0d7b4e0a-5a1f-4f6e-8e2d-7c3b9a1f2e44  loopback  lo

A device is the interface; a connection is a saved set of settings that can be applied to it. To give a server a static address instead of DHCP, change the connection and reactivate it:

$ sudo nmcli connection modify enp1s0 \
    ipv4.method manual \
    ipv4.addresses 192.168.1.50/24 \
    ipv4.gateway 192.168.1.1 \
    ipv4.dns "192.168.1.1" \
    ipv4.dns-search corp.example.com
$ sudo nmcli connection up enp1s0

modify only saves the profile (on RHEL 9 in /etc/NetworkManager/system-connections/enp1s0.nmconnection); connection up applies it. Setting ipv4.method auto returns to DHCP. If you are connected over SSH through this interface and the new address differs, your session drops: have a console ready.

netplan on Ubuntu servers

Ubuntu Server describes its network in YAML (YAML Ain't Markup Language) files in /etc/netplan/. netplan turns them into configuration for systemd-networkd (the default on servers) or NetworkManager. A static configuration:

# /etc/netplan/01-static.yaml  (permissions 600)
network:
  version: 2
  ethernets:
    enp1s0:
      dhcp4: false
      addresses:
        - 192.168.1.50/24
      routes:
        - to: default
          via: 192.168.1.1
      nameservers:
        addresses: [192.168.1.1]
        search: [corp.example.com]
$ sudo netplan try
$ sudo netplan apply

YAML is strict about indentation: spaces only, and each level consistently indented. netplan try applies the new configuration and rolls it back automatically after 120 seconds unless you press Enter to confirm — exactly what you want when a mistake could cut your SSH session. netplan apply applies it without that safety net. Debian servers without NetworkManager use yet another format, /etc/network/interfaces, applied with ifup and ifdown.

Host firewalls: ufw and firewalld

A host firewall is a firewall that runs on the server itself and filters the traffic to and from that one machine, in addition to any network firewall in front of it (see Firewalls). On Linux the filtering is done by the kernel (nftables, earlier iptables); ufw and firewalld are friendlier front ends. The sound policy is the same everywhere: deny incoming traffic by default, and allow only the services this server offers.

Task ufw (Ubuntu) firewalld (RHEL, Fedora)
Default state Installed, inactive Installed and active, zone public on RHEL (Fedora: FedoraServer/FedoraWorkstation)
Show the rules sudo ufw status verbose sudo firewall-cmd --list-all
Allow SSH sudo ufw allow OpenSSH sudo firewall-cmd --permanent --add-service=ssh (allowed by default)
Allow a port sudo ufw allow 443/tcp sudo firewall-cmd --permanent --add-port=8443/tcp
Allow from one network only sudo ufw allow from 192.168.1.0/24 to any port 5432 proto tcp sudo firewall-cmd --permanent --zone=internal --add-source=192.168.1.0/24 plus the service in that zone
Remove a rule sudo ufw delete 2 (number from status numbered) sudo firewall-cmd --permanent --remove-service=http
Make it active sudo ufw enable sudo firewall-cmd --reload after --permanent changes

ufw

$ sudo ufw allow OpenSSH
$ sudo ufw allow 443/tcp
$ sudo ufw enable
$ sudo ufw status numbered
Rules updated
Rules updated (v6)
Rules updated
Rules updated (v6)
Command may disrupt existing ssh connections. Proceed with operation (y|n)? y
Firewall is active and enabled on system startup
Status: active

     To                         Action      From
     --                         ------      ----
[ 1] OpenSSH                    ALLOW IN    Anywhere
[ 2] 443/tcp                    ALLOW IN    Anywhere
[ 3] OpenSSH (v6)               ALLOW IN    Anywhere (v6)
[ 4] 443/tcp (v6)               ALLOW IN    Anywhere (v6)

OpenSSH is an application profile (a named set of ports, listed by sudo ufw app list). ufw's defaults deny incoming and allow outgoing traffic, so after enable only SSH and HTTPS get in. Exception: ports published by Docker (-p 8080:80) go around ufw's rules. Bind them to 127.0.0.1 or filter them in the DOCKER-USER chain.

firewalld

firewalld groups rules into zones: each interface or source network belongs to one zone, and the zone decides what is allowed. On a typical server everything is in the zone public.

$ sudo firewall-cmd --permanent --add-service=https
$ sudo firewall-cmd --reload
$ sudo firewall-cmd --list-all
success
success
public (active)
  target: default
  icmp-block-inversion: no
  interfaces: enp1s0
  sources:
  services: cockpit dhcpv6-client https ssh
  ports:
  protocols:
  forward: yes
  masquerade: no
  forward-ports:
  source-ports:
  icmp-blocks:
  rich rules:

firewalld has two configurations: the runtime one that is active now, and the permanent one that is loaded at boot and at --reload. A command without --permanent changes only the runtime configuration and is lost at the next reload or reboot; a command with --permanent changes only the saved one and does nothing until you reload. --permanent followed by --reload is the pattern to remember.

A quick troubleshooting order

When "the server has no network" or "the site is unreachable", work from the bottom up and stop at the first step that fails:

  1. Link: ip -br link — is the interface UP, without NO-CARRIER?
  2. Address: ip -br addr — the expected address and prefix, not a 169.254.x.x address or none at all?
  3. Route: ip route — is there a default route, via the right gateway?
  4. Gateway: ping -c 3 192.168.1.1 — can the host reach its own router?
  5. Beyond: ping -c 3 198.51.100.10 or tracepath — does traffic get out, and where does it stop?
  6. Names: resolvectl status, dig +short name and getent hosts name — does the name resolve, to the right address?
  7. Service: curl -v to the port from a client, ss -tlnp on the server — does anything listen, and on which address?
  8. Firewall: sudo ufw status or sudo firewall-cmd --list-all — is the port allowed? Then the service's own log (journalctl -u name).

The same order in more depth, with the general method, is in Troubleshooting Tools and Method, and Linux-specific cases in Troubleshooting Linux.