Sign in
← Networking: basics
16 min · easy · Lesson 2 of 13

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:

PrefixHost bitsAddressesExample
/82416,777,21610.0.0.0/8, the whole 10.x.x.x block
/161665,536192.168.0.0/16, the whole 192.168.x.x block
/248256192.168.1.0/24, everything inside 192.168.1.x
/32018.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:

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):

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

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:

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):

RangeNameWho uses it
0-1023system (well-known)system services: SSH 22, DNS 53, HTTP 80, HTTPS 443
1024-49151user (registered)reserved for applications: PostgreSQL 5432, Redis 6379
49152-65535dynamic (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

What you'll need for the tasks

In the next lesson we'll cover DNS: how a name turns into an IP.

Done reading?

Mark the lesson as read to move your track progress forward.

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.

Get access for $49

Tell the agent: use MCP izi, give me lesson networking-basics-02-ip-and-ports