Networking for Developers

IP Addresses, Subnets, NAT and Routing

IPv4 and IPv6, CIDR notation worked by hand, private ranges, NAT, routing tables and how a packet finds its way.

Lesson 0114 min read#networking#ip#cidr#nat

On this page

  1. Words used in this lesson
  2. IPv4 is just a 32-bit number
  3. Network part and host part: what the slash means
  4. Private addresses and the special ranges
  5. Same subnet or not? The first decision a host makes
  6. The routing table and longest prefix match
  7. How a packet crosses the world
  8. NAT: many machines, one public address
  9. IPv6: more addresses, fewer tricks
  10. Putting it to use: sizing a small network by hand
  11. Try it
  12. Interview questions
  13. Key takeaways
  14. What's next

In one line: an IP address has two parts, the network and the host, and the slash number (/24, /26) says where the cut is. Every machine keeps a routing table that picks the next hop by the longest matching prefix. NAT lets a whole private network hide behind one public address by rewriting ports. If you understand these three ideas, VPCs, Docker networks, firewall rules and "why can't I reach that host" become simple maths.

Many developers copy a CIDR block like 10.0.0.0/16 from a tutorial and never think about it again. Then one day two VPCs cannot be connected because they overlap, or a VPN breaks the office network, or Docker steals the address range that the company uses. All of these are the same small piece of binary maths, and you can learn it in an hour.

This is lesson 1 of 9. In lesson 0 we saw that every packet has an IP address, and that a machine picks where to send it by looking at a table. Here we open that table. The next lesson, TCP and UDP, puts connections on top of this.

What I ran / checked. The commands ran on Linux with Python 3.13 (the standard ipaddress module), Node.js v22.22.0, ip from iproute2 6.1.0, ping and iptables. I pasted real outputs. The routing table and the Docker NAT rule are from the sandbox machine itself. Sources I read: the MDN pages on the TCP glossary and on the evolution of HTTP (markdown from mdn/content on GitHub), only for background. Blocked / not read: the IP RFCs (791, 1918, 4632 and 8200) and the cloud documentation could not be fetched from the sandbox, so the facts about private ranges, CIDR rules and IPv6 are things I know and then tested with Python rather than quotes from a spec page. Where a number comes from my memory and not from a test (for example how many addresses a cloud provider reserves in every subnet), I say so. Limits of this machine: it has no IPv6 at all (no /proc/net/if_inet6, and creating an IPv6 socket fails), so the IPv6 section is Python maths and not live traffic. I also cannot show a real home-router NAT table; the NAT demo is a toy I wrote, plus the one real NAT rule that Docker installed on this machine.

Words used in this lesson

  • IPv4 is the older address format: 32 bits, written as four numbers like 192.168.10.77.
  • IPv6 is the newer format: 128 bits, written in hex like 2001:db8::1.
  • A bit is a 0 or a 1. An octet is 8 bits, one number from 0 to 255.
  • A subnet is a block of addresses that belong together, for example one office network or one cloud zone.
  • CIDR (say "cider") is the way of writing a block as address/prefix, like 192.168.10.0/26.
  • A prefix length is the number after the slash. It says how many bits, counting from the left, are the network part.
  • A netmask is the same thing written as an address: /26 is 255.255.255.192.
  • A network address is the first address in a block (all host bits 0). A broadcast address is the last (all host bits 1).
  • A host is any machine on a network.
  • A private address is one from a range reserved for internal use. It is not routed on the public internet.
  • A routing table is a list of rules: "for destinations in this block, send to that next hop".
  • A next hop is the next router on the path.
  • A default route is the rule used when nothing else matches. It is written 0.0.0.0/0.
  • Longest prefix match is the rule that picks the most specific route when several match.
  • NAT (network address translation) rewrites addresses in packets as they pass through a router.
  • A gateway is the machine on your network that leads to other networks.
  • Port forwarding is a NAT rule that sends traffic from a public port to a chosen inside machine.
  • CGNAT is carrier-grade NAT: your internet provider puts you behind NAT as well.

IPv4 is just a 32-bit number

An IPv4 address looks like four numbers, but it is really one number made of 32 bits. The dots only make it easier for humans. Each of the four numbers is 8 bits (one octet), from 0 to 255. Python can show both views:

Python
import ipaddress as ipa = ip.ip_address("192.168.10.77")print(int(a), format(int(a), "032b"))
TEXT
3232238157 11000000101010000000101001001101

Cut the 32 bits into four groups of eight: 11000000 . 10101000 . 00001010 . 01001101. In decimal that is 192 . 168 . 10 . 77. If you have never converted binary, remember the weights of the 8 bits from left to right: 128, 64, 32, 16, 8, 4, 2, 1. The bits 01001101 are 64 + 8 + 4 + 1 = 77.

There are 2^32 addresses, about 4.3 billion. That sounds like a lot. The world has more devices than that, which is the reason for both NAT (this lesson) and IPv6 (the end of this lesson).

Network part and host part: what the slash means

Every address is split into two parts. The first bits say which network. The rest say which machine on that network. The prefix length tells you where the split is.

Take 192.168.10.0/26. The /26 means "the first 26 bits are the network part". So 32 - 26 = 6 bits are left for hosts. With 6 bits you can make 2^6 = 64 different values. So this block holds 64 addresses.

Here is the same block as a picture. The n bits are fixed for the whole block. The h bits change from one host to the next:

TEXT
 192      . 168      . 10       . 0 to 63 11000000 . 10101000 . 00001010 . 00 hhhhhh nnnnnnnn   nnnnnnnn   nnnnnnnn   nn \_____________ 26 bits network ______/\ 6 host bits

Now let Python do all the numbers and compare to what we expect:

Python
import ipaddress as ipn = ip.ip_network("192.168.10.0/26")print("network", n.network_address, "mask", n.netmask, "wildcard", n.hostmask)print("broadcast", n.broadcast_address, "total", n.num_addresses, "usable", n.num_addresses - 2)h = list(n.hosts()); print("first", h[0], "last", h[-1])print("binary mask", bin(int(n.netmask))[2:].zfill(32))
TEXT
network 192.168.10.0 mask 255.255.255.192 wildcard 0.0.0.63broadcast 192.168.10.63 total 64 usable 62first 192.168.10.1 last 192.168.10.62binary mask 11111111111111111111111111000000

Look at the mask in binary: twenty-six 1s then six 0s. That is exactly what /26 means. 255.255.255.192 is the same bits written in decimal: the last octet 11000000 is 192.

The two special addresses explain the "minus 2":

  • The first address (all host bits 0), 192.168.10.0, names the network itself.
  • The last address (all host bits 1), 192.168.10.63, is the broadcast address, "everyone on this network".

So a /26 has 64 addresses but 62 usable hosts. Cloud providers often reserve a few more in every subnet for their own use. I remember it being five on AWS, but I could not open the docs from my sandbox, so check your provider's page before you plan a tiny subnet.

The shortcut: block size

You do not need to convert to binary every time. Use the block size. Find the octet where the cut happens (for /26 that is the last one, since 26 is between 24 and 32). The block size in that octet is 256 minus the mask value (256 - 192 = 64). Blocks of that size start at 0, 64, 128, 192:

TEXT
192.168.10.0/26    →  .0   to .63192.168.10.64/26   →  .64  to .127192.168.10.128/26  →  .128 to .191192.168.10.192/26  →  .192 to .255

Which block is 192.168.10.77 in? It is between 64 and 127, so the second block. Let us check with code:

Python
a = ip.ip_address("192.168.10.77")print(a, "in 192.168.10.0/26 ?", a in ip.ip_network("192.168.10.0/26"))print(a, "in 192.168.10.64/26 ?", a in ip.ip_network("192.168.10.64/26"))print(list(ip.ip_network("10.0.0.0/24").subnets(new_prefix=26)))
TEXT
192.168.10.77 in 192.168.10.0/26 ? False192.168.10.77 in 192.168.10.64/26 ? True[IPv4Network('10.0.0.0/26'), IPv4Network('10.0.0.64/26'), IPv4Network('10.0.0.128/26'), IPv4Network('10.0.0.192/26')]

The last line shows a /24 cut into four /26s. Each time you add one to the prefix, you cut the block in two. Each time you take one away, you double it. Memorise this small table, because it appears in every cloud setup:

PrefixAddressesUsable hosts (classic)Typical use
/816,777,21616,777,214A huge private range like 10.0.0.0/8
/1665,53665,534A whole VPC or a big site
/24256254One subnet, one office floor
/266462A small subnet
/281614A tiny subnet
/3042A link between two routers
/3211One single host (used in routes and firewall rules)

Two edge cases to know. A /32 is exactly one address (10.0.0.5/32 means "just this host"). And a /31 has two addresses and both are usable (modern routers use it for point-to-point links). Python agrees:

Python
print(list(ip.ip_network("10.0.0.0/30").hosts()), list(ip.ip_network("10.0.0.0/31").hosts()))
TEXT
[IPv4Address('10.0.0.1'), IPv4Address('10.0.0.2')] [IPv4Address('10.0.0.0'), IPv4Address('10.0.0.1')]

And at the other end, /0 is the whole internet: 0.0.0.0/0 contains 4,294,967,296 addresses. You will see this one soon, as the default route and in firewall rules ("allow from 0.0.0.0/0" means "from anywhere").

Private addresses and the special ranges

Some ranges are set aside for private use. Anyone can use them inside their own network. They are not routed on the public internet, so a thousand different companies can all have a 10.0.0.5.

BlockRangeSize
10.0.0.0/810.0.0.0 to 10.255.255.25516.7 million
172.16.0.0/12172.16.0.0 to 172.31.255.2551 million
192.168.0.0/16192.168.0.0 to 192.168.255.25565 thousand

Other special blocks you will meet: 127.0.0.0/8 (loopback, "this machine"), 169.254.0.0/16 (link-local, a machine gives itself one when DHCP fails), 100.64.0.0/10 (shared space used by internet providers for CGNAT), and the documentation blocks 192.0.2.0/24, 198.51.100.0/24, 203.0.113.0/24 that are meant to appear in examples. I tested a list with Python's ipaddress:

TEXT
10.1.2.3     private=True  global=False loopback=False linklocal=False172.16.5.4   private=True  global=False loopback=False linklocal=False172.32.0.1   private=False global=True  loopback=False linklocal=False192.168.1.1  private=True  global=False loopback=False linklocal=False100.64.0.5   private=False global=False loopback=False linklocal=False169.254.1.1  private=True  global=False loopback=False linklocal=True127.0.0.1    private=True  global=False loopback=True  linklocal=False8.8.8.8      private=False global=True  loopback=False linklocal=False192.0.2.2    private=True  global=False loopback=False linklocal=False

Three surprises, all real:

  1. 172.32.0.1 is public. The private block is only 172.16.0.0 to 172.31.255.255. Many people think all of 172.x is private. It is not.
  2. 100.64.0.5 is neither private nor global. It is in the shared CGNAT range, a third category.
  3. 192.0.2.2, the address of this very sandbox machine, is "private" in Python 3.13. It belongs to a documentation block. Python's is_private answers True for the special-purpose blocks too. So is_private does not mean "inside RFC 1918". If you use it for a security check (for example to block requests to internal addresses, which we study in the security series), read its documentation carefully and test your edge cases.

Pick a CIDR for a VPC or a Docker network with care. Two networks that you may ever join (VPC peering, a VPN, a company office) must not overlap. Many teams use 10.0.0.0/16 as a default, so everything overlaps with everything. Also remember that Docker uses 172.17.0.0/16 by default for its bridge (this machine has exactly that: docker0 172.17.0.1/16), and some home routers use 192.168.1.0/24. A VPN into an office that also uses 192.168.1.0/24 is a classic source of "it works at the office but not from home".

You can test overlap in code before you spend money on it:

Python
print(ip.ip_network("10.0.0.0/16").overlaps(ip.ip_network("10.0.5.0/24")))print(list(ip.ip_network("10.0.0.0/16").address_exclude(ip.ip_network("10.0.5.0/24")))[:3])
TEXT
True[IPv4Network('10.0.128.0/17'), IPv4Network('10.0.64.0/18'), IPv4Network('10.0.32.0/19')]

The /24 sits inside the /16, so they overlap (True). A block is always either nested inside another, equal to it, or completely apart. CIDR blocks never overlap partway. That makes checking easy: if two blocks overlap, one contains the other.

Same subnet or not? The first decision a host makes

When your machine wants to send a packet to 192.0.2.50, it does a quick check using its own address and mask. My machine is 192.0.2.2/24. The destination 192.0.2.50 is in 192.0.2.0/24, so it is on the same link. The machine finds the destination's MAC address (ARP) and sends the frame straight there. If the destination were outside the block, say 8.8.8.8, the machine does not try to reach it directly. It hands the packet to the gateway, and the gateway worries about the rest.

This is easy to see with ip route get, which asks the kernel "how would you send to this address?":

TEXT
$ ip route get 192.0.2.50192.0.2.50 dev eth0 src 192.0.2.2$ ip route get 8.8.8.88.8.8.8 via 192.0.2.1 dev eth0 src 192.0.2.2$ ip route get 172.17.0.5172.17.0.5 dev docker0 src 172.17.0.1$ ip route get 127.0.0.1local 127.0.0.1 dev lo src 127.0.0.1

Four destinations, four answers. The first has no via: it is directly reachable on eth0. The second has via 192.0.2.1: send it to the gateway. The third goes out through the Docker bridge. The fourth is local. The src part tells you which of my own addresses will be written as the source in the packet.

The routing table and longest prefix match

The kernel makes these decisions using a routing table. This is the real one from the sandbox:

TEXT
$ ip routedefault via 192.0.2.1 dev eth0172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1 linkdown172.18.0.0/16 dev br-a180b19a367b proto kernel scope link src 172.18.0.1 linkdown192.0.2.0/24 dev eth0 proto kernel scope link src 192.0.2.2

Read each line as "for destinations in this block, send like this".

  • 192.0.2.0/24 dev eth0 means my own subnet is directly attached on eth0.
  • 172.17.0.0/16 dev docker0 and 172.18.0.0/16 ... are the Docker networks (the linkdown word means no container is attached right now).
  • default via 192.0.2.1 dev eth0 is the default route, which is 0.0.0.0/0. Everything that matches nothing else goes to the gateway at 192.0.2.1.

The same table lives in /proc/net/route, in a form that shows how raw it is. The gateway column for the default route reads 010200C0:

TEXT
Iface	Destination	Gateway 	Flags	...	Masketh0	00000000	010200C0	0003	...	00000000

That is the address 192.0.2.1 stored as a little-endian hex number (the bytes are backwards: C0 00 02 01 is 192.0.2.1). I decoded it with socket.inet_ntoa(struct.pack("<L", 0x010200C0)) and got 192.0.2.1. A destination of 0 and a mask of 0 means "match everything".

When two routes match

What if several routes contain the destination? The rule is longest prefix match: the most specific rule (the biggest prefix number) wins. Here is a tiny router in Python, using four routes:

Python
import ipaddress as iptable = [("10.0.0.0/8", "gw A"), ("10.1.0.0/16", "gw B"), ("10.1.2.0/24", "gw C"), ("0.0.0.0/0", "default gw")]def lookup(d):    d = ip.ip_address(d)    m = [(ip.ip_network(n), g) for n, g in table if d in ip.ip_network(n)]    return max(m, key=lambda x: x[0].prefixlen)for d in ["10.1.2.9", "10.1.9.9", "10.9.9.9", "8.8.8.8"]:    print(d, "->", *lookup(d))
TEXT
10.1.2.9 -> 10.1.2.0/24 gw C10.1.9.9 -> 10.1.0.0/16 gw B10.9.9.9 -> 10.0.0.0/8 gw A8.8.8.8 -> 0.0.0.0/0 default gw

10.1.2.9 matches three routes (the /8, the /16 and the /24), and the /24 wins because it is the most specific. 8.8.8.8 matches only the default. Real routers do this with fast data structures, not a loop, but the rule is identical. This rule is why you can add a very specific route (a single /32 for one machine) to override a general one. It is also how a VPN works: it adds a route for the office block that beats your default route.

A wrong route is silent. If a more specific route sends your packets somewhere unexpected (a leftover VPN route, a Docker network that took your office's range), nothing prints an error. The packets just go into a hole. When "I cannot reach X" is confusing, run ip route get X first. It shows you the exact route the kernel will use.

How a packet crosses the world

Put it together. Your laptop sends to a far server:

  1. Laptop checks its table. The server is not local, so it uses the default route: next hop is the home router. It writes the router's MAC address on the frame (lesson 0).
  2. The home router receives the frame, takes off the Ethernet header, and looks at the destination IP. It checks its own routing table. Almost always the answer is "default, send to the provider's router". It lowers the TTL by one, builds a new Ethernet frame with the next router's MAC, and sends it.
  3. This repeats 8 to 20 times. Routers in the middle of the internet carry large tables (they learn routes from each other using protocols such as BGP; that is outside our scope) and still use longest prefix match.
  4. The last router sees that the destination is on a network it is directly attached to, and delivers the frame to the server's MAC.

No router knows the whole path. Each only knows the next hop for each block. The path is the sum of many small local decisions. That is why one broken router in the middle can make the whole path fail, and why traceroute (one reply per hop, using the TTL trick from lesson 0) is the tool to see where it stops.

Packets can also take different paths. Two packets of the same TCP connection may take different routes and arrive in a different order. IP does not mind. TCP puts the order right again (lesson 2).

You also get a useful tool from this: ICMP, the control messages of IP. ping uses ICMP echo. traceroute uses ICMP "time exceeded". Routers use ICMP "destination unreachable" and "fragmentation needed". A firewall that blocks all ICMP "to be safe" breaks path MTU discovery (lesson 0) and makes debugging harder. Block ICMP types with care, not all of them.

NAT: many machines, one public address

IPv4 addresses ran short. The fix for most homes and offices is NAT. Inside, every device has a private address (192.168.1.x). The router has one public address. When an inside device talks to the internet, the router rewrites the source address and port to its own public ones, and remembers the mapping. When the reply comes back, it looks up the mapping and rewrites the destination back to the inside device.

Let us write that logic as a tiny program, so there is no magic left. This is a toy: a real router does it in the kernel, with timeouts and many more rules.

Python
PUBLIC = "203.0.113.7"table, rev, nxt = {}, {}, 40000def outbound(src_ip, src_port, dst):    global nxt    key = (src_ip, src_port, dst)    if key not in table:        table[key] = nxt; rev[nxt] = key; nxt += 1    return PUBLIC, table[key]def inbound(dst_port):    return rev.get(dst_port)   # None = no mapping, packet droppedprint(outbound("192.168.1.10", 51000, "93.184.216.34:443"))print(outbound("192.168.1.11", 51000, "93.184.216.34:443"))print(outbound("192.168.1.10", 51000, "93.184.216.34:443"))print("reply to 40001 ->", inbound(40001))print("unsolicited to 40009 ->", inbound(40009))
TEXT
('203.0.113.7', 40000)('203.0.113.7', 40001)('203.0.113.7', 40000)reply to 40001 -> ('192.168.1.11', 51000, '93.184.216.34:443')unsolicited to 40009 -> None

Look at what happened. Two inside machines both used source port 51000. The router gave them different public ports (40000 and 40001), so the server can tell them apart and the router knows where each reply belongs. The third call is the same connection again and gets the same public port. A reply to 40001 goes back to the second machine. A packet to port 40009, which nobody asked for, has no mapping, so it is dropped.

That last line is an important side effect. NAT drops unsolicited inbound traffic, because there is no entry. It acts like a basic firewall, by accident. But it is not a real security feature: you should still have a real firewall. It has bad effects as well:

  • You cannot run a server behind NAT without help. Nobody outside can start a connection to 192.168.1.10. The fix is port forwarding: a static rule "public port 8080 goes to 192.168.1.10:3000".
  • Peer-to-peer is hard. Two clients behind two NATs cannot connect directly. Video calls and games use helper servers (STUN and TURN) and tricks to punch holes. This is a reason WebRTC is complex.
  • The router keeps state. If the table is full or a mapping times out, long idle connections die silently. This is why many apps send keep-alive pings. Load balancers and cloud NAT gateways have idle timeouts too (often a few minutes; check your provider's value).
  • CGNAT. Many internet providers now put whole neighbourhoods behind one public address (the 100.64.0.0/10 range). Then even port forwarding at home is impossible.

A real NAT rule on this machine

Docker uses NAT to give containers internet access. I asked the kernel for its NAT table with iptables -t nat -S:

TEXT
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE-A POSTROUTING -s 172.18.0.0/16 ! -o br-a180b19a367b -j MASQUERADE

In plain words: "for packets from the Docker network 172.17.0.0/16 that are leaving through any interface other than docker0 itself, replace the source address with the address of the outgoing interface." MASQUERADE is the name for "NAT with the router's own address". This is the same idea as the toy above. It is why a container with address 172.17.0.5 can reach the internet, while the internet cannot start a connection to it unless you publish a port (-p 8080:80 adds a forwarding rule).

Your server sees the NAT address, not the client's. Behind a load balancer or NAT, req.socket.remoteAddress in Node may be the proxy's address. To get the real client IP you must trust a header like X-Forwarded-For that the proxy sets, and only from the proxy you control. If you trust that header from anyone, an attacker can write any IP they like and defeat your rate limiting. We come back to this in lesson 6.

IPv6: more addresses, fewer tricks

IPv6 solves the shortage by using 128 bits. That gives 2^128 addresses, a number with 39 digits. Addresses are written in hex, in groups of 16 bits, separated by colons. Leading zeros in a group can be dropped, and one run of all-zero groups can be replaced by :::

Python
print(ip.ip_address("2001:0db8:0000:0000:0000:ff00:0042:8329").compressed)print(ip.ip_address("2001:db8::1").exploded, ip.ip_address("::1").is_loopback)
TEXT
2001:db8::ff00:42:83292001:0db8:0000:0000:0000:0000:0000:0001 True

::1 is the loopback address (like 127.0.0.1). fe80::/10 is link-local, always present on an interface. 2001:db8::/32 is the documentation range, like 192.0.2.0/24 for IPv4.

CIDR works the same way, just with bigger numbers. The usual rule is that a normal subnet is a /64, which leaves 64 bits for hosts: 18,446,744,073,709,551,616 addresses, one subnet. A site normally receives a /48 or /56 from its provider and cuts it into /64s:

Python
print(ip.ip_network("2001:db8:abcd:12::/64").num_addresses)print(len(list(ip.ip_network("2001:db8:abcd::/48").subnets(new_prefix=64))))
TEXT
1844674407370955161665536

One /48 gives 65,536 subnets of /64. Because there is so much room, you do not need NAT for address shortage. Each device can have its own globally unique address, and a firewall (not NAT) decides what is allowed.

You must still care about IPv6 as a developer, for three reasons:

  1. Dual stack. Most real networks run IPv4 and IPv6 together. A hostname can have both an A record (IPv4) and an AAAA record (IPv6), which we meet in lesson 3. Your client picks one. Node 22 has "happy eyeballs" on by default: net.getDefaultAutoSelectFamily() returned true on my machine, with a per-attempt timeout of 250 ms. It tries the addresses in turn, and if the first is silent for 250 ms it tries the next. This hides broken IPv6 from users but can add a visible delay.
  2. Your server may listen on only one family. listen(3000, "0.0.0.0") is IPv4 only. listen(3000, "::") usually accepts both families on Linux. Mixed up configs cause "works on my laptop, not on the phone".
  3. Address validation and storage. If your code stores an IP in a VARCHAR(15) column, an IPv6 client breaks it. Use 45 characters, or a proper inet type. Regexes that match only dotted-quad addresses will fail on ::1. IPv4 can also appear inside IPv6 as a "mapped" address: ::ffff:192.0.2.1. Python's .ipv4_mapped gives 192.0.2.1. Security code that checks "is this IP internal?" must unwrap this form first.

I could not show IPv6 traffic. This sandbox has no IPv6 stack, and my attempt to open an IPv6 socket in a Python QUIC library failed with Address family not supported by protocol. So treat the IPv6 section as notation and maths verified in Python, not as a live test.

Putting it to use: sizing a small network by hand

Here is a short exercise that uses everything so far. You need subnets for a small cloud project inside 10.20.0.0/16: a public subnet with about 60 machines, a private subnet with about 200 machines, and a database subnet with about 10.

  1. Public, 60 hosts: you need at least 62 usable, so a /26 (64 addresses) just fits. If the cloud reserves five, only 59 are usable, so use a /25 (128 addresses) to be safe.
  2. Private, 200 hosts: a /24 has 254 usable. Good.
  3. Database, 10 hosts: a /28 has 14 usable. If you want growth, use a /27 (30 usable).

A clean layout: 10.20.0.0/25 public, 10.20.1.0/24 private, 10.20.2.0/27 database. Check for overlap with Python before you type it into a console. Leave gaps, because you may need new subnets later and you cannot easily resize a subnet.

Try it

  1. Write 172.16.40.130/20 as network address, broadcast address and number of usable hosts. Check with Python.
  2. Is 172.31.255.1 private? Is 172.32.0.1?
  3. Your machine is 10.0.4.200/22. Which of these are on the same subnet: 10.0.5.9, 10.0.8.1, 10.0.7.250?
  4. A route table has 0.0.0.0/0 via A, 10.0.0.0/8 via B, 10.4.0.0/16 via C. Where does a packet to 10.4.9.9 go? To 10.5.1.1? To 11.0.0.1?
  5. Run ip route get 1.1.1.1 and ip route get 127.0.0.1 on your machine. What is different?
  6. A teammate says "our two VPCs are 10.0.0.0/16 and 10.0.128.0/17, we will peer them." Will it work?

Answers

  1. /20 means 12 host bits, so the block size in the third octet is 16 (256 - 240). 40 falls in the block starting at 32. The network is 172.16.32.0, the broadcast is 172.16.47.255, and usable hosts are 4096 - 2 = 4094. In Python: ip.ip_network("172.16.40.130/20", strict=False).
  2. 172.31.255.1 is private (the private block ends at 172.31.255.255). 172.32.0.1 is public, as the Python test above showed.
  3. /22 has block size 4 in the third octet. The block that holds 4 is 10.0.4.0 to 10.0.7.255. So 10.0.5.9 and 10.0.7.250 are on the same subnet, and 10.0.8.1 is not.
  4. 10.4.9.9 goes via C (the /16 is the longest match). 10.5.1.1 goes via B (the /8). 11.0.0.1 goes via A (default).
  5. The first one has via <gateway>. The second says local ... dev lo, which means delivered inside your machine.
  6. No. 10.0.128.0/17 lies inside 10.0.0.0/16, so they overlap. Peering needs non-overlapping blocks, because a router cannot choose between two networks that claim the same addresses.

Interview questions

What does /24 mean in 192.168.1.0/24? (Must know)

It says the first 24 bits are the network part and the last 8 bits identify the host. So the block has 2^8 = 256 addresses, of which 254 are usable as hosts, because the first is the network address and the last is the broadcast. The mask in dotted form is 255.255.255.0.

How many usable hosts are in a /26, and how do you work it out quickly? (Must know)

A /26 leaves 32 - 26 = 6 host bits, so 2^6 = 64 addresses and 62 usable. The quick way is the block size: 256 minus the mask in the cut octet (192) is 64. The blocks start at 0, 64, 128, 192. In cloud networks a few extra addresses per subnet may be reserved, so I check the provider's rule before I choose a small subnet.

Which address ranges are private, and which common mistake do people make? (Must know)

10.0.0.0/8, 172.16.0.0/12 and 192.168.0.0/16. The common mistake is to think all of 172.x.x.x is private. Only 172.16.0.0 to 172.31.255.255 is. I tested 172.32.0.1 and it is a public address.

How does a machine decide where to send a packet? (Must know)

It looks up the destination in the routing table and takes the route with the longest matching prefix. If the destination is in a directly attached subnet, it delivers on that link (finding the MAC with ARP). Otherwise it sends the packet to the next hop, often the default gateway. ip route get <addr> shows the chosen route.

What is the default route? (Must know)

It is the route 0.0.0.0/0, which matches every address, with the shortest possible prefix. So it is used only when nothing more specific matches. On a home or cloud machine it points at the gateway. If it is missing, the machine can reach its own subnet but nothing else.

Explain NAT and what it breaks. (Must know)

NAT lets many machines with private addresses share one public address. The router rewrites the source address and port of outgoing packets, remembers the mapping, and reverses it for replies. It breaks anything that needs inbound connections to an inside machine, so servers need port forwarding, and peer-to-peer apps need STUN or TURN helpers. It also keeps state, so idle connections may be dropped after a timeout. Docker's MASQUERADE rule is the same mechanism.

Two VPCs have overlapping CIDR blocks. Can you peer them? (Senior signal)

No, not in the normal way. A router cannot tell which network an address belongs to when both claim it. The fix is to renumber one of them, which is painful once resources exist. So I plan CIDR blocks for all environments, and for the office and VPN, before creating the first VPC, and I check overlap with a small script.

Why does IPv6 not need NAT? (Senior signal)

IPv6 has 2^128 addresses, so every device can have a globally unique address. A normal subnet is a /64, which alone holds more than 18 quintillion addresses. The "shortage" reason for NAT disappears. Security is then done by a stateful firewall, not by the side effect of NAT. NAT66 exists but is rarely wanted.

What is longest prefix match and why does it matter in practice? (Senior signal)

When several routes match, the one with the most specific prefix wins. It is how a /32 host route overrides a /24, and how a VPN's route for the office network beats the default route. It matters in debugging: a leftover specific route can silently send traffic to the wrong place. I use ip route get to see the winner.

A client behind a load balancer shows the load balancer's IP in your logs. How do you get the real address, and what is the risk? (Trap)

The proxy adds the client address in a header such as X-Forwarded-For. I read it, but only trust it when the request comes from my own proxy, and I take the right entry from the list (the one added by my first trusted proxy, not the first value that a client could have written). If I trust the header from any client, an attacker can fake any IP and bypass rate limits or IP allow lists. Some platforms use the PROXY protocol instead.

Is Python's ipaddress.isprivate safe to use to block requests to internal addresses? (Trap)

Not on its own. In my test it returned True for the documentation block 192.0.2.2 and False for the shared CGNAT address 100.64.0.5, so its idea of "private" is not the same as "internal to my network". It also does not unwrap IPv4-mapped IPv6 addresses by itself. For security checks, I use an explicit allow list, resolve the name first and check the address I will actually connect to, and test the odd cases.

Key takeaways

  • An IPv4 address is a 32-bit number. /N means the first N bits are the network. Block size = 256 minus the mask in the cut octet. Usable hosts = 2^(32-N) minus 2.
  • Private ranges are 10/8, 172.16/12 (only 172.16 to 172.31) and 192.168/16. 100.64/10 is CGNAT space. 127/8 is loopback.
  • CIDR blocks either nest or are fully apart, never partly overlap. Check overlap in code before you create networks you may need to join.
  • A routing table maps blocks to next hops. The most specific (longest prefix) match wins. The default route 0.0.0.0/0 catches the rest. ip route get tells you the real decision.
  • Routers only know the next hop, never the whole path. Each hop lowers the TTL and builds a new Ethernet frame.
  • NAT rewrites source addresses and ports and keeps a table. It blocks unsolicited inbound traffic, breaks peer-to-peer and servers behind it, and times out idle mappings.
  • IPv6 is 128 bits, normal subnets are /64, and it needs no NAT. Real networks are dual stack, so make sure your code and storage handle both.

What's next

Next: TCP and UDP: Connections, Reliability and Congestion, Lesson 2. We will capture a real three-way handshake, watch packets get lost and sent again, see a receiver tell a sender to stop (a window of zero), and compare TCP with UDP side by side. Practise now: run ip route on your machine, and for each line say which block it covers and where it sends the packets.

Published by Jainik · Last updated · How these lessons are made

Keep learning

Related lessons

Part of the study guide

Related series