CoursesLinux essentialsNetworking basics

Networking basics

ip, ss, ports, and sockets.

Beginner14 min · lesson 23 of 25

A server spends its whole life talking to other machines over the network. That makes it a little like an apartment building on a busy street. The street outside is the network. The building has one address, a row of numbered doors, and behind some of those doors somebody is waiting to answer. Whether you are keeping the place running or keeping intruders out, you need to know three things: the building's address, which doors are open, and who is standing behind each one.

Two small commands answer nearly every question you will have about a Linux machine's network. Both come from a package called iproute2 (Internet Protocol routing utilities, version two), the standard toolkit on any modern system. ip reports the machine's addresses and how its traffic leaves. ss (short for socket statistics) reports which programs are listening and who they are talking to. Older guides use ifconfig (interface configuration) and netstat (network statistics); those are the retired tools that ip and ss replaced, and on a fresh Ubuntu or Debian box netstat may not even be installed.

The machine's address, and where its traffic goes

Start with the building's own address. ip addr lists every network interface (the physical or virtual network cards the machine has) and the IP (Internet Protocol) addresses attached to each one.

~/secopslog — bash
$ ip addr
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 valid_lft forever preferred_lft forever 2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000 link/ether 06:1f:2a:3b:4c:5d brd ff:ff:ff:ff:ff:ff inet 10.0.1.10/24 brd 10.0.1.255 scope global dynamic eth0 valid_lft 3241sec preferred_lft 3241sec inet6 fe80::41f:2aff:fe3b:4c5d/64 scope link valid_lft forever preferred_lft forever

There are two interfaces here. lo is the loopback interface, an internal intercom the machine uses to talk to itself and that never reaches the outside; it always carries the address 127.0.0.1. eth0 is the real network card, and it holds the address 10.0.1.10. The /24 after it is the network prefix, a shorthand for the subnet mask, and it says how big the local neighborhood is (here, addresses 10.0.1.0 through 10.0.1.255 count as next-door neighbors reachable directly). The inet6 lines are the IPv6 (Internet Protocol version 6) equivalents. scope global means the address is usable from other machines; scope host means it stays on this box.

Where does traffic go when it leaves? ip route prints the routing table, which is the building's set of rules for outgoing mail: hand local deliveries straight to the neighbor, and give everything else to the front gate.

~/secopslog — bash
$ ip route
default via 10.0.1.1 dev eth0 proto dhcp src 10.0.1.10 metric 100 10.0.1.0/24 dev eth0 proto kernel scope link src 10.0.1.10 metric 100

The default line is the one that matters most. It says anything not on the local network goes to 10.0.1.1, the gateway (the router that connects this segment to everywhere else). The second line says the 10.0.1.0/24 neighborhood is reachable directly on eth0, no router needed. For you, this table decides whether a failed connection is a local problem or something upstream. For an attacker who has landed on the box, it is a free map of the network's shape.

Ports and sockets, in plain terms

A port is a numbered door on the building. The IP address gets a packet to the right building; the port number gets it to the right door inside. There are 65,536 of them (0 through 65535). Most services answer on a well-known number by convention: web traffic over HTTP (HyperText Transfer Protocol) uses door 80, its encrypted form HTTPS uses 443, SSH (Secure Shell, for remote logins) uses 22, the Postgres database uses 5432. These numbers are habits, not laws; a service can listen on any port you tell it to.

A socket is one end of a conversation. Think of a phone. A socket in the LISTEN state is a phone plugged in and waiting to ring: a program has claimed a port and is ready for callers. A socket in the ESTABLISHED state is a call already in progress. The operating system's kernel (the core part that manages hardware and connections) keeps thousands of these calls straight by tagging each one with five facts: the protocol, your address, your port, the other end's address, and the other end's port. That five-part label is why two people can load the same website at once without their traffic getting crossed.

Reading who is listening, and the 0.0.0.0 trap

This is the command you will run more than any other. ss -tlnp lists listening sockets. The flags read as a sentence: -t for TCP (Transmission Control Protocol, the reliable connection type most services use), -l for listening only, -n for numeric (show raw port numbers instead of translating them, which is faster and more honest), and -p to show the program that owns each socket.

~/secopslog — bash
$ sudo ss -tlnp
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process LISTEN 0 128 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=701,fd=3)) LISTEN 0 4096 127.0.0.53%lo:53 0.0.0.0:* users:(("systemd-resolve",pid=412,fd=14)) LISTEN 0 244 127.0.0.1:5432 0.0.0.0:* users:(("postgres",pid=812,fd=6)) LISTEN 0 511 0.0.0.0:80 0.0.0.0:* users:(("nginx",pid=934,fd=6)) LISTEN 0 128 [::]:22 [::]:* users:(("sshd",pid=701,fd=4))

Read the Local Address:Port column, because that is where the security is. 127.0.0.1 is the loopback address: only this machine can reach that door. 0.0.0.0 is a wildcard meaning every interface, including the public street outside. So sshd on 0.0.0.0:22 is reachable from other machines (intended, since you log in remotely), nginx on 0.0.0.0:80 is a public web server (intended), and postgres on 127.0.0.1:5432 is local-only (correct, a database should not face the world). systemd-resolve on 127.0.0.53 is the local DNS (Domain Name System) helper, and [::] is the IPv6 way of writing 0.0.0.0.

The high-value habit is this: run ss -tlnp on every server you touch, and for each 0.0.0.0 line ask one question, should the whole world be able to reach this? A database, an internal admin dashboard, a metrics endpoint, or a message queue bound to 0.0.0.0 is the most common serious exposure on Linux hosts. On a cloud server, 0.0.0.0 can mean the actual public internet, where automated scanners find an open port within minutes.

Every 0.0.0.0 listener is a door to the street
The classic Linux breach is a service bound to 0.0.0.0 that should have been local. Redis (a fast in-memory data store) shipped this way for years, listening on all interfaces with no password, and internet-wide scans turned thousands of installs into instant takeovers. Account for every listener: bind internal services to 127.0.0.1, put a firewall in front of anything that must be public, and treat an unexpected open port as something to investigate, not ignore.

Locking a service down is usually one line in its config file. Modern Redis ships safe by default, and its config shows the pattern clearly.

/etc/redis/redis.conf
# Only accept connections from this machine.
bind 127.0.0.1 -::1
protected-mode yes
port 6379

bind lists the addresses the service will listen on. Change either entry to 0.0.0.0 and you have reopened the door to everyone. When you genuinely must expose a service, require a password and put firewall rules in front of it.

No sudo, no process names
Run ss -tlnp as a normal user and it still lists the ports, but the Process column comes back blank for every service you do not own. Reading which program owns another user's socket needs privilege, so if those owners show up empty, you forgot sudo. Add it. Otherwise it is easy to fool yourself into thinking a port has no owner.

When a connection won't cooperate

When something cannot connect, work from the street inward, the way you would trace a delivery that never arrived: does the building exist at that address, is the right door open, and does anyone actually answer. Three tools map to those three questions, and running them in order turns a vague it does not work into a specific failing step.

First, ping knocks on the building itself. It sends a small ICMP (Internet Control Message Protocol) message and waits for the echo, confirming the host is powered on and reachable.

~/secopslog — bash
$ ping -c3 10.0.1.10
PING 10.0.1.10 (10.0.1.10) 56(84) bytes of data. 64 bytes from 10.0.1.10: icmp_seq=1 ttl=64 time=0.412 ms 64 bytes from 10.0.1.10: icmp_seq=2 ttl=64 time=0.388 ms 64 bytes from 10.0.1.10: icmp_seq=3 ttl=64 time=0.401 ms --- 10.0.1.10 ping statistics --- 3 packets transmitted, 3 received, 0% packet loss, time 2043ms rtt min/avg/max/mdev = 0.388/0.400/0.412/0.010 ms

Next, nc (netcat, a tool for opening raw network connections) checks whether one specific door is open. The -z flag means scan without sending data, and -v means say what happened.

~/secopslog — bash
$ nc -zv 10.0.1.10 22
Connection to 10.0.1.10 22 port [tcp/ssh] succeeded!

Finally, curl (a command-line tool for making web requests) checks whether the service behind the door answers correctly, not only that the port is open.

~/secopslog — bash
$ curl -sS http://10.0.1.10/healthz
ok

Each result narrows the problem. If ping works but nc fails, the host is up but the port is closed or blocked by a firewall. If nc succeeds but curl hangs or errors, the door is open but the program behind it is broken. There is a security echo here: these same probes are exactly what an attacker's port scan does. A firewall configured to silently drop traffic (rather than politely refuse it) makes nc hang instead of failing fast, which is one reason careful scans are slow.

Listening is your surface, established is your activity

Listening sockets are the doors an attacker gets to try, so they are your attack surface. Established sockets are the calls in progress, so they are your activity. ss shows the second kind too, and that is how you catch a machine phoning home to somewhere it should not.

~/secopslog — bash
$ sudo ss -tnp state established
Recv-Q Send-Q Local Address:Port Peer Address:Port Process 0 0 10.0.1.10:22 203.0.113.7:52344 users:(("sshd",pid=1422,fd=4)) 0 0 10.0.1.10:41022 10.0.1.20:5432 users:(("app",pid=1533,fd=9))

Two live connections here. One is an incoming SSH login from 203.0.113.7 (a face you should recognize, or start asking questions), and one is your own application reaching out to a Postgres database at 10.0.1.20. An established connection you cannot explain, especially an outbound one from a process that has no business talking to the internet, is one of the clearest signals of a compromised host: a reverse shell (a hidden connection that hands an attacker a command prompt on your machine) or malware beaconing back to whoever controls it. On a healthy server, you can name every line.

Where a service binds decides who can reach it
127.0.0.1 / ::1 (loopback)
Reachable from
this machine only
Good for
databases, caches, local admin
Network risk
none
0.0.0.0 / [::] (all interfaces)
Reachable from
every network the host sits on
Good for
public web, SSH you mean to expose
Network risk
the whole internet on a cloud host
10.0.1.10 (one interface)
Reachable from
that segment only
Good for
internal services on a private network
Network risk
anyone on that subnet
Run ss -tlnp and check the Local Address of every listener.
Quick check
01You run sudo ss -tlnp on a cloud server and see: LISTEN 0.0.0.0:6379 users:(("redis-server",pid=980)). What does this line tell you?
Correct — 0.0.0.0 means all interfaces; on a cloud host that can include the public IP, and open Redis is a classic breach.
Incorrect — Local-only would show 127.0.0.1. 0.0.0.0 is the opposite: every interface.
Incorrect — LISTEN is the normal, healthy state for a server waiting for callers.
Incorrect — ss shows it precisely because it is open and a process owns it.
02The lesson calls listening sockets your attack surface and established sockets your activity. What is the actual difference between a socket in the LISTEN state and one in the ESTABLISHED state?
Incorrect — socket state describes the connection itself, not any firewall decision.
Incorrect — LISTEN is a healthy waiting state, not a failure.
Correct — a LISTEN socket is a phone plugged in waiting to ring, and an ESTABLISHED socket is a call already underway.
Incorrect — either state can involve local or remote addresses; the state alone does not decide reachability.
03On 'sudo ss -tnp state established' you find an outbound connection from a process to an unfamiliar external IP on a high port, and that process has no business reaching the internet. Why is this a red flag?
Incorrect — established is a normal state; the concern is the unexplained peer, not the state itself.
Incorrect — server processes make outbound connections routinely, and ss reports them accurately.
Incorrect — the alarming part is the unexplained outbound peer, not any inference about the firewall.
Correct — on a healthy host you can name every established line, so one you cannot is a classic sign of compromise.

Make ss -tlnp the first thing you run on any server someone hands you. The list it prints is the exact set of doors that anyone on the network gets to try, and everything sitting on 0.0.0.0 is your homework: prove each one is meant to be public, or move it behind loopback and a firewall.

Try this

Work through “Listening is your surface, established is your activity” yourself on a sandbox you can throw away, following the commands above in order. Then break one step deliberately and re-run, so you have seen the failure before it finds you.

Takeaway

The trap worth remembering here: every 0.0.0.0 listener is a door to the street. Check that on your own systems before you need to, because it is cheaper to find on a quiet afternoon than during an incident.

Related