Networking from the Command Line
Checking and configuring the network on Linux: ip, ss, ping, dig and curl, NetworkManager, and host firewalls with ufw and firewalld.
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:80and[::]: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:
-
pingruns until you press Ctrl+C;ping -c 4sends four packets and stops.ping -6orping6forces IPv6. -
tracerouteis often not installed.tracepathdoes the same job without root rights and also reports the path MTU (maximum transmission unit);mtrcombines 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:
-
Link:
ip -br link— is the interfaceUP, withoutNO-CARRIER? -
Address:
ip -br addr— the expected address and prefix, not a 169.254.x.x address or none at all? -
Route:
ip route— is there a default route, via the right gateway? -
Gateway:
ping -c 3 192.168.1.1— can the host reach its own router? -
Beyond:
ping -c 3 198.51.100.10ortracepath— does traffic get out, and where does it stop? -
Names:
resolvectl status,dig +short nameandgetent hosts name— does the name resolve, to the right address? -
Service:
curl -vto the port from a client,ss -tlnpon the server — does anything listen, and on which address? -
Firewall:
sudo ufw statusorsudo 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.