IP addresses and ports
What an IP address and subnet mask are, public and private addresses, NAT (SNAT/DNAT), what ports are for, and why 80 and 443 are special.
Why this matters to a DevOps engineer
Half of all networking tickets sound the same: "the service is running, but I can't reach it." To find the cause, you need to read an address with its mask, understand whether a packet goes straight to a neighbor or through a gateway, and see which address and port the process is actually listening on. That's what we'll cover here.
IPv4 in a nutshell
An IPv4 address takes up 32 bits. It's written as four octets separated by
dots: 192.168.1.42. Each octet takes a value from 0 to 255.
There are 2^32 such addresses in total, about 4.3 billion, and the free ones ran out long ago. That's why most home and office devices get private addresses (more on this below) and reach the internet through NAT.
Subnet masks
The 32 bits of an address split into two parts: the network address (the high bits, shared by everyone in the subnet) and the host address (the low bits, different for each machine). The mask shows where the boundary runs.
After the address you'll often see /24, /16, /8. That's a mask in
CIDR notation: the number of high bits given to the network. The same mask
in the old notation looks like 255.255.255.0 for /24 or
255.255.252.0 for /22.
/24 means "24 high bits for the network, the remaining 8 for hosts." 8
bits give 2^8 = 256 addresses. The smaller the number after the slash,
the bigger the subnet:
| Prefix | Host bits | Addresses | Example |
|---|---|---|---|
/8 | 24 | 16,777,216 | 10.0.0.0/8, the whole 10.x.x.x block |
/16 | 16 | 65,536 | 192.168.0.0/16, the whole 192.168.x.x block |
/24 | 8 | 256 | 192.168.1.0/24, everything inside 192.168.1.x |
/32 | 0 | 1 | 8.8.8.8/32, one specific IP |
The boundary doesn't have to land on an octet break. In /22, 10 bits are
left for hosts, that's 1024 addresses, and the subnet 172.16.4.0/22
stretches from 172.16.4.0 to 172.16.7.255.
Two addresses in every subnet are reserved and never handed to machines:
- the network address: all host bits are zero (
192.168.1.0for/24); - the broadcast: all host bits are one (
192.168.1.255), a packet to it reaches everyone in the subnet.
So in a /24, only 254 of the 256 addresses are left for actual machines.
There are exceptions: /32 describes a single address, and in a /31 for
a point-to-point link both addresses are usable
(RFC 3021).
Why it matters to the machine
Before sending a packet, a host decides whether the recipient is a neighbor. It applies its own mask to both its own address and the recipient's address (bitwise AND) and compares the results. It doesn't know the recipient's mask, and it doesn't need to.
my network: 192.168.1.42 & /24 -> 192.168.1.0
destination: 192.168.1.99 & /24 -> 192.168.1.0 matches, neighbor
destination: 8.8.8.8 & /24 -> 8.8.8.0 no match
If the networks match, this is a neighbor, and the packet goes to it directly through the switch. If they don't, the packet goes to the gateway (the router), which figures out the rest.
On Linux, this decision is visible in the routing table. A route with no
via means "this network is directly connected"; a default via route
gives the gateway for everything else. The ip route get command shows
which route the kernel will pick for a specific address. Inside a
container on Docker's default network, it looks like this:
ip route
default via 172.17.0.1 dev eth0
172.17.0.0/16 dev eth0 scope link src 172.17.0.3
ip route get 8.8.8.8
8.8.8.8 via 172.17.0.1 dev eth0 src 172.17.0.3
For an address in its own subnet, say 172.17.5.5, the reply won't have
via: the packet goes directly.
Public and private IPs
A public address is visible from the internet and unique across the whole planet. People outside can reach you through it. Such addresses are issued by your provider, they cost money, and there aren't many left.
A private address only works inside a local network; such addresses aren't routed on the internet. Three blocks are reserved for them (RFC 1918):
10.0.0.0/8(all of10.x.x.x);172.16.0.0/12(from172.16.x.xto172.31.x.x);192.168.0.0/16(all of192.168.x.x).
At home, a laptop has a private address, say 192.168.0.42. All home
devices reach the outside world through the router's address thanks to NAT
(next section). Often the router itself doesn't get a public address from the
provider either, only an address from the 100.64.0.0/10 block. That's
carrier-grade NAT (RFC 6598): the
provider runs another NAT on their end, and you can't be reached from the
outside even with port mapping set up on the router.
In the cloud, machines in a VPC also sit on private addresses like 10.x
or 172.16.x, and a public address is attached separately when outside
access is needed. That's a normal setup.
NAT: SNAT, DNAT, MASQUERADE
NAT (Network Address Translation) swaps addresses in a packet on the fly. It's what lets many private addresses live behind one public one.
SNAT (source NAT) swaps the sender's address. Your packet leaves
with 192.168.0.42, the router rewrites the source to its own public
address and remembers the connection so it can route the reply back.
That's how outgoing traffic from a local network to the internet works.
MASQUERADE does the same thing as SNAT, but doesn't require writing the address into the rule: it takes whatever address the outgoing interface currently has. This is handy when the public address is dynamic and the provider hands it out over DHCP.
PAT (overload) is SNAT with port translation. The term comes from Cisco; Linux has no separate mode for it: SNAT and MASQUERADE change the source port themselves if it's already taken by another connection. That's how hundreds of devices sit behind one public address, and the router tells their connections apart by port.
DNAT (destination NAT) swaps the recipient's address. A packet
arrives at the router's public address and port, and the router rewrites
the destination to the private address of an internal machine. This is
port mapping: from the outside you knock on public_ip:443, and
inside you land on 192.168.0.10:443.
Easy to remember: SNAT and MASQUERADE let traffic out, DNAT lets it in.
On Linux, NAT rules live in the nat table (iptables or nftables). SNAT
and MASQUERADE go in the POSTROUTING chain: the packet is already
leaving. DNAT goes in PREROUTING, where the packet has just arrived, and
for packets from the machine itself, in OUTPUT
(iptables-extensions).
docker run -p 127.0.0.1:8080:80 on Linux creates exactly this kind of
DNAT rule.
Special addresses
127.0.0.1is loopback, "myself." Connections to it never go out onto the network, the local stack handles them. The whole127.0.0.0/8block is loopback.0.0.0.0in a service's configuration means "all interfaces." A service listening on0.0.0.0:8080accepts connections on any IPv4 address of the machine.169.254.0.0/16is link-local. The system assigns itself an address like this when DHCP doesn't answer. On Windows this is the familiar169.254.x.xand the "no internet access" icon. In the cloud, the virtual machine's metadata service answers at169.254.169.254(AWS).
IPv6
IPv6 is replacing IPv4: 128 bits, about 3.4×10³⁸ addresses. The address is
written as eight hexadecimal groups separated by ::
2001:0db8:85a3:0000:0000:8a2e:0370:7334
Zeros get compressed: 2001:db8:85a3::8a2e:370:7334. A double colon
replaces any run of zero groups, but only once per address, and leading
zeros in a group can be dropped
(RFC 5952).
Worth knowing by heart:
::1is IPv6 loopback, the equivalent of127.0.0.1;::in a service's configuration means "all IPv6 interfaces," the equivalent of0.0.0.0;fe80::/10is link-local in IPv6;fc00::/7is unique local addresses (ULA), the equivalent of private blocks (RFC 4193).
On servers, IPv6 often runs alongside IPv4. The name localhost can
resolve to ::1, and then the client connects over IPv6 while the service
only listens on 127.0.0.1. If "localhost doesn't respond but 127.0.0.1
works," check exactly this.
Ports
An IP address gets a packet to a machine, a port gets it to a
process inside that machine. A port takes up 16 bits, values from 0
to 65535. TCP and UDP number ports independently: 53/tcp and 53/udp
are different ports.
IANA splits the range into three parts (RFC 6335):
| Range | Name | Who uses it |
|---|---|---|
| 0-1023 | system (well-known) | system services: SSH 22, DNS 53, HTTP 80, HTTPS 443 |
| 1024-49151 | user (registered) | reserved for applications: PostgreSQL 5432, Redis 6379 |
| 49152-65535 | dynamic (ephemeral) | temporary, the OS hands them to outgoing connections |
On Linux, only root or a process with the CAP_NET_BIND_SERVICE
capability can bind a port below 1024. The boundary is set by the
net.ipv4.ip_unprivileged_port_start parameter, 1024 by default. Docker
sets it to 0 inside containers, so nginx running as a regular user in a
container can happily listen on port 80.
80 and 443 are special because the browser fills them in on its own:
http://site means port 80, https://site means port 443. Any other port
has to be written into the address explicitly: http://127.0.0.1:8080.
Linux takes ephemeral ports not from the IANA range but from
32768-60999. The current value lives in
/proc/sys/net/ipv4/ip_local_port_range. macOS and Windows follow IANA
and use 49152-65535. The difference matters sometimes: a service
listening on port 40000 on Linux can run into an outgoing connection that
the kernel handed the same number.
When a browser opens https://example.com, its request has a destination
port of 443, and the source port comes from the ephemeral range, say
54321. The server replies the other way: from 443 to 54321.
What "listening on 0.0.0.0:8080" means
0.0.0.0 means "all local IPv4 addresses." The service accepts
connections from the machine itself, from the local network, and from the
internet, if the firewall lets them through.
The alternative: 127.0.0.1:8080. Then you can only connect from the
machine itself. That's how you run things that shouldn't be exposed:
internal APIs, debug panels, a local proxy to a database.
ss -tlnp (TCP) or ss -tulnp (TCP and UDP) shows who's listening on
what. The -n flag prints addresses as numbers, -p adds the process;
for other users' processes you need sudo.
The old netstat -tulnp from the net-tools package is often not installed
on fresh distributions, so plan on ss. Minimal images like Alpine have a
stripped-down netstat from BusyBox, it understands the -tln flags.
Here's what nginx looks like in the nginx:1.27.3-alpine container:
Proto Recv-Q Send-Q Local Address Foreign Address State
tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN
tcp 0 0 :::80 :::* LISTEN
First line: port 80 on all IPv4 addresses. Second: the same for IPv6
(::).
The 127.0.0.1-in-a-container trap
Newcomers often get caught like this: a service inside a container listens
on 127.0.0.1:8080, and it's unreachable from another container or from
the host.
The reason is that a container has its own network stack. 127.0.0.1
inside a container means the container itself, not the host and not a
neighboring container. A connection from a neighbor arrives at the
container's address on the Docker network, and a service bound to
loopback doesn't accept it. For a service to be reachable from outside its
container, it has to listen on 0.0.0.0 (or a specific address of the
container).
Don't confuse this with publishing a port. In
docker run -p 127.0.0.1:8080:80, the address 127.0.0.1 refers to the
host: the port will only open for the machine itself. The process inside
the container still has to listen on 0.0.0.0:80.
Common mistakes
- Assuming every subnet is
/24. The mask can be anything, read it fromip addr, don't guess from the last octet. - Forgetting about the network and broadcast addresses when counting free addresses.
- Confusing
0.0.0.0in a service's configuration (all interfaces) with0.0.0.0as a destination address: you never connect to0.0.0.0, connect to127.0.0.1or to the machine's real address. - Binding a service inside a container to
127.0.0.1and then being surprised that a neighboring service in Compose can't connect to it. - Expecting port mapping on a home router to open access from outside
when the provider has you behind carrier-grade NAT (
100.64.x.xon the router's external interface).
What you'll need for the tasks
ip -4 addr show eth0: the interface's address, mask, and broadcast;ip routeandip route get <address>: directly or through the gateway;- counting addresses in a subnet:
2^(32 - prefix), minus the two reserved ones; docker compose -f <file> exec <service> <command>: run a command inside a lab environment container;netstat -tlninside a container: which address and port a process is listening on;docker compose -f <file> logs <service>andrestart <service>.
In the next lesson we'll cover DNS: how a name turns into an IP.
Interview-style questions
Interview questions on this topic: 1
Included with practice access, $49 one-time. Get access
Answer out loud or in writing, then reveal the answer and honestly mark whether you knew it.
Done reading?
Mark the lesson as read to move your track progress forward.
Lesson marked as done.
Go through it with an AI agent
The agent explains anything unclear in plain words, gives you 2 labs for this lesson and drills you on interview questions. $49 one-time, lifetime access.
Tell the agent: use MCP izi, give me lesson networking-basics-02-ip-and-ports
Progress saved in this browser
Clear your history or open this on your phone, and the marks will be gone. Leave your email and they'll move to your account. No password needed.
Done. moved to your account. You can see your progress on any device. My progress