Skip to main content
Networking · lesson 1 of 8
Networking beginner Lesson 1 of 8

IP Addresses and CIDR: Which Hosts Are on My Subnet?

Read CIDR with Python's ipaddress: network, broadcast and host counts, the /25 membership bug a string check hides, and a Docker network that enforces it.

By SkillByExample · Updated

networking ip-address cidr subnet python docker

Every networking question starts with an address and a question: is that host next to me, or behind a router? A subnet answers it, and CIDR notation like 10.0.0.0/25 is how a subnet is written down. This lesson reads CIDR with Python’s standard ipaddress module, shows the membership mistake that looks right, and then lets Docker enforce the same rule on a real network. Tested with Docker Engine 29.8.2, Python 3.12.15 (python:3.12-slim) and Alpine 3.22.6.

Where this fits, as one table rather than a diagram:

LayerWhat it addressesLesson
Network (IP)hosts, by IP address and subnetthis one
Transport (TCP/UDP)processes on a host, by portPorts and sockets
Applicationnames and requestsDNS, HTTP

Read a CIDR block

# subnet.py
import ipaddress

net = ipaddress.ip_network("10.0.0.0/25")
print("network:  ", net.network_address)
print("netmask:  ", net.netmask)
print("broadcast:", net.broadcast_address)
print("addresses:", net.num_addresses)
hosts = list(net.hosts())
print("usable:   ", len(hosts), "from", hosts[0], "to", hosts[-1])
$ python subnet.py
network:   10.0.0.0
netmask:   255.255.255.128
broadcast: 10.0.0.127
addresses: 128
usable:    126 from 10.0.0.1 to 10.0.0.126

/25 means the first 25 bits name the network and the last 7 number the hosts, so the block holds 2⁷ = 128 addresses. The first (.0) is the network address and the last (.127) is the broadcast address. Neither can be given to a machine, which leaves 126. The netmask 255.255.255.128 is the same prefix written as 25 one-bits followed by 7 zero-bits.

The bug: “it starts with 10.0.0., so it’s local”

A service at 10.0.0.130 is unreachable from a host on 10.0.0.0/25, and the address looks like it belongs there. Here’s the check people write by eye, next to the real one:

# same_subnet.py
import ipaddress

server = "10.0.0.130"
subnet = "10.0.0.0/25"

# The bug: "it starts with 10.0.0., so it's on my subnet"
prefix = subnet.split("/")[0].rsplit(".", 1)[0] + "."
print("string check:", server.startswith(prefix))

# The real check compares the address against the mask
net = ipaddress.ip_network(subnet)
print("ipaddress check:", ipaddress.ip_address(server) in net)
print("first 25 bits of .130:", format(int(ipaddress.ip_address(server)), "032b")[:25])
print("first 25 bits of net: ", format(int(net.network_address), "032b")[:25])
$ python same_subnet.py
string check: True
ipaddress check: False
first 25 bits of .130: 0000101000000000000000001
first 25 bits of net:  0000101000000000000000000

The string check assumes subnet boundaries fall on the dots. A /25 boundary falls inside the last octet: 130 is 10000010 in binary, and its leading 1 is bit 25, the last network bit. That one bit puts .130 in the next block, 10.0.0.128/25. A host on 10.0.0.0/25 sends traffic for .130 to its default gateway instead of straight to it, and if no router connects the two halves, the traffic never arrives. Compare addresses with ipaddress, never with string prefixes.

Which network does a host belong to?

Given a host and its prefix, you often want its network. The obvious call refuses:

# host_bits.py
import ipaddress

print(ipaddress.ip_interface("10.0.0.130/25").network)
print(ipaddress.ip_network("10.0.0.130/25"))
$ python host_bits.py
10.0.0.128/25
Traceback (most recent call last):
  File "/work/host_bits.py", line 5, in <module>
    print(ipaddress.ip_network("10.0.0.130/25"))
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/ipaddress.py", line 74, in ip_network
    return IPv4Network(address, strict)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/ipaddress.py", line 1544, in __init__
    raise ValueError('%s has host bits set' % self)
ValueError: 10.0.0.130/25 has host bits set

ip_interface models “this host, with this prefix”, which is exactly what ip addr shows on a Linux box. Its .network answers the question: 10.0.0.130 lives in 10.0.0.128/25. ip_network models a network, and a network address must have every host bit set to zero. The strictness is useful: a firewall rule or route written as 10.0.0.130/25 almost always means someone misread the boundary.

Split a block into subnets

Cloud networks are carved this way: one large block for the VPC, smaller ones per subnet.

# split.py
import ipaddress

vpc = ipaddress.ip_network("10.0.0.0/24")
for sub in vpc.subnets(new_prefix=26):
    print(f"{str(sub):<15} {sub.num_addresses - 2:>3} usable hosts")
$ python split.py
10.0.0.0/26      62 usable hosts
10.0.0.64/26     62 usable hosts
10.0.0.128/26    62 usable hosts
10.0.0.192/26    62 usable hosts

Each extra prefix bit halves the block, so going from /24 to /26 gives four quarters. Every subnet loses two addresses to its own network and broadcast address, so four /26 blocks hold 248 usable hosts, not 254. Cloud providers reserve a few more per subnet on top of that, which the AWS VPC lesson shows.

# classify.py
import ipaddress

for text in ["10.20.30.40", "172.16.5.1", "172.32.0.1", "192.168.1.10",
             "127.0.0.1", "169.254.169.254", "8.8.8.8"]:
    ip = ipaddress.ip_address(text)
    kind = ("loopback" if ip.is_loopback else
            "link-local" if ip.is_link_local else
            "private" if ip.is_private else
            "global" if ip.is_global else "other")
    print(f"{text:<16} {kind}")
$ python classify.py
10.20.30.40      private
172.16.5.1       private
172.32.0.1       global
192.168.1.10     private
127.0.0.1        loopback
169.254.169.254  link-local
8.8.8.8          global

The three private ranges are 10.0.0.0/8, 172.16.0.0/12 and 192.168.0.0/16. The middle one catches people out: /12 ends at 172.31.255.255, so 172.32.0.1 is a public address. 169.254.169.254 is link-local, the address cloud instances use for their metadata service. Loopback and link-local are checked first because Python also counts them as is_private. Nothing here sends a packet. These are properties of the number alone.

Docker enforces the same arithmetic

A user-defined Docker network has a subnet, and Docker checks every address against it. Create one with the /25 from above. --internal keeps it off your real network, which matters because 10.0.0.0 is a range home LANs and VPNs often use:

docker network create --internal --subnet 10.0.0.0/25 subnet-lab
$ docker run --rm --network subnet-lab alpine:3.22 ip -4 -o addr show eth0
2: eth0    inet 10.0.0.2/25 brd 10.0.0.127 scope global eth0\       valid_lft forever preferred_lft forever
$ docker run --rm --network subnet-lab alpine:3.22 ip route
10.0.0.0/25 dev eth0 scope link  src 10.0.0.2
$ docker run --rm --network subnet-lab --ip 10.0.0.130 alpine:3.22 true
docker: Error response from daemon: invalid config for network subnet-lab: invalid endpoint settings:
no configured subnet contains IP address 10.0.0.130

Run 'docker run --help' for more information
$ docker run --rm --network subnet-lab --ip 10.0.0.100 alpine:3.22 ip -4 -o addr show eth0
2: eth0    inet 10.0.0.100/25 brd 10.0.0.127 scope global eth0\       valid_lft forever preferred_lft forever

ip addr prints the container’s address as an interface, 10.0.0.2/25, with the broadcast address ipaddress computed. Docker gave the first usable host, .1, to the network’s gateway, which is why the container got .2. The route table shows the decision from the bug section as the kernel makes it: addresses in 10.0.0.0/25 go straight out of eth0. There’s no default via line, because an internal network has no gateway route off the subnet, so every other address is unreachable. Drop --internal and a default via 10.0.0.1 line appears for everything else. Docker rejects .130 for the reason ipaddress printed False, and accepts .100. Remove the network with docker network rm subnet-lab. Routes and interfaces are covered in more depth in Linux networking basics.

Practice

  1. Size the blocks. Print the address count and the number of usable hosts for /24, /26, /28, /30, /31 and /32. Two of them break the “minus two” rule. Which ones?
Solution
# sizes.py
import ipaddress

for prefix in (24, 26, 28, 30, 31, 32):
    net = ipaddress.ip_network(f"10.0.0.0/{prefix}")
    print(f"/{prefix:<3} {net.num_addresses:>4} addresses {len(list(net.hosts())):>4} hosts")
$ python sizes.py
/24   256 addresses  254 hosts
/26    64 addresses   62 hosts
/28    16 addresses   14 hosts
/30     4 addresses    2 hosts
/31     2 addresses    2 hosts
/32     1 addresses    1 hosts

/31 and /32. A /31 is a point-to-point link (RFC 3021) with no room for a network or broadcast address, so both addresses are hosts. A /32 is one host, which is how a single address is written in a firewall rule or route.

  1. Local or via the gateway? An office uses 192.168.10.0/26. For 192.168.10.5, .63, .64 and 192.168.11.5, print whether a host in the office reaches it directly or through the gateway.
Solution
# who_is_local.py
import ipaddress

office = ipaddress.ip_network("192.168.10.0/26")
for text in ["192.168.10.5", "192.168.10.63", "192.168.10.64", "192.168.11.5"]:
    where = "local" if ipaddress.ip_address(text) in office else "via gateway"
    print(f"{text:<15} {where}")
$ python who_is_local.py
192.168.10.5    local
192.168.10.63   local
192.168.10.64   via gateway
192.168.11.5    via gateway

A /26 holds 64 addresses, so the block ends at .63. .63 is technically the broadcast address, but it’s still “on the subnet”. .64 starts the next block.

  1. Tidy a rule list. A firewall has rules for 10.0.0.0/25, 10.0.0.128/25, 10.0.1.0/25 and 10.0.2.0/24. Use ipaddress.collapse_addresses to find the shortest equivalent list.
Solution
# collapse.py
import ipaddress

nets = [ipaddress.ip_network(n) for n in
        ["10.0.0.0/25", "10.0.0.128/25", "10.0.1.0/25", "10.0.2.0/24"]]
for n in ipaddress.collapse_addresses(nets):
    print(n)
$ python collapse.py
10.0.0.0/24
10.0.1.0/25
10.0.2.0/24

The two halves of 10.0.0.0/24 merge into one. 10.0.1.0/25 can’t merge with anything, because its other half, 10.0.1.128/25, isn’t in the list, and 10.0.1.0/24 would allow addresses the original rules didn’t.

  1. Ask Docker for the edges. Create a network with --subnet 10.0.0.0/28 and try --ip values of 10.0.0.14, 10.0.0.15 and 10.0.0.16. Predict which ones work before you run them.
Solution
$ docker network create --subnet 10.0.0.0/28 tiny-lab
$ docker run --rm --network tiny-lab --ip 10.0.0.14 alpine:3.22 ip -4 -o addr show eth0
2: eth0    inet 10.0.0.14/28 brd 10.0.0.15 scope global eth0\       valid_lft forever preferred_lft forever
$ docker run --rm --network tiny-lab --ip 10.0.0.15 alpine:3.22 ip -4 -o addr show eth0
docker: Error response from daemon: failed to set up container networking: Address already in use

Run 'docker run --help' for more information
$ docker run --rm --network tiny-lab --ip 10.0.0.16 alpine:3.22 ip -4 -o addr show eth0
docker: Error response from daemon: invalid config for network tiny-lab: invalid endpoint settings:
no configured subnet contains IP address 10.0.0.16

Run 'docker run --help' for more information

.14 is the last usable host. .15 is inside the subnet but is its broadcast address, which Docker reserves, so it’s reported as already in use. .16 is outside the /28 altogether, so the request fails the same way .130 did. Clean up with docker network rm tiny-lab.

Frequently Asked Questions

What does the /25 in 10.0.0.0/25 mean?
It is the prefix length: the first 25 of the address's 32 bits identify the network, and the remaining 7 bits number hosts inside it. So 10.0.0.0/25 holds 2 to the power of 7, or 128 addresses, from 10.0.0.0 to 10.0.0.127. The same prefix written as a netmask is 255.255.255.128.
Why does a /24 have 254 usable hosts and not 256?
In a normal IPv4 subnet the first address is the network address and the last is the broadcast address, and neither can be given to a host. A /24 has 256 addresses, so 254 are usable. The exceptions are /31, used for point-to-point links where both addresses are usable, and /32, which describes a single host.
How do I check whether an IP address is in a subnet in Python?
Use the standard library ipaddress module: ipaddress.ip_address('10.0.0.130') in ipaddress.ip_network('10.0.0.0/25') returns False. Never compare address strings by prefix, because a subnet boundary can fall in the middle of an octet, so two addresses that share the text 10.0.0. can still be on different subnets.
What are the private IPv4 address ranges?
RFC 1918 reserves 10.0.0.0/8, 172.16.0.0/12 and 192.168.0.0/16 for private networks, which are not routed on the public internet. The 172 range ends at 172.31.255.255, so 172.32.0.1 is a public address. In Python, ipaddress.ip_address(text).is_private reports whether an address is in a private or other reserved range.
What is the ValueError 'has host bits set' from ipaddress.ip_network?
ip_network expects a network address, so every host bit after the prefix must be zero. 10.0.0.130/25 has host bits set, because the network it belongs to starts at 10.0.0.128. Use ip_interface('10.0.0.130/25').network to get the network a host address belongs to, or pass strict=False to ip_network.