Computer Networks Module 7 – IPv4 addressing, subnetting and NAT
Subnetting step by step
Split an IPv4 network into equal subnets for a number of subnets or hosts, list them with the block-size method and find the subnet of any address.
What you will learn
- Split a network into equal subnets for a required number of subnets or hosts
- Use the block-size method to list subnet ranges quickly
- Decide which subnet an address belongs to and whether it is a usable host address
Before you start
What you’ll learn · Before you start
What you will learn
- Split a network into equal subnets for a required number of subnets or hosts
- Use the block-size method to list subnet ranges quickly
- Decide which subnet an address belongs to and whether it is a usable host address
Before you start
On this page
Subnetting splits one network into smaller networks of equal size by moving the boundary between the network part and the host part to the right. Every host bit you borrow for the network part doubles the number of subnets and halves the size of each one: borrowing s bits from a network with h host bits gives 2^s subnets of 2^(h − s) addresses. The method has five steps. First, work out how many bits to borrow from what you need, a number of subnets or a number of hosts per subnet. Second, write the new prefix length and mask. Third, find the block size: 256 minus the mask value in the octet where the boundary falls. Fourth, list the subnets, which start at multiples of the block size. Fifth, check each subnet’s network address, host range and broadcast address. This lesson runs every step on worked examples and in Python.
Why networks are split
RFC 950, the standard that introduced subnets, describes organisations that divided one network into several subnets instead of asking for more network numbers. The reasons still hold. Each subnet is its own broadcast domain, so a broadcast from one floor of a building does not reach every device on the campus. Each subnet can carry its own rules: the guest Wi-Fi can be kept away from the accounts office by filtering between two subnets. And a router can reach a whole building through one route instead of one route per device.
Equal subnets are the simplest plan: every subnet gets the same prefix length. The next lesson of this module, on VLSM, gives each subnet a size of its own. Both rest on the same arithmetic, which is what this lesson practises.
Step 1: decide how many bits to borrow
Start from what you need:
- A number of subnets N. Borrow the smallest s with 2^s ≥ N. Six subnets need s = 3, because 2³ = 8 is the first power of two that reaches 6, so a /24 becomes a /27.
- A number of hosts per subnet H. Keep the smallest number of host bits h with 2^h − 2 ≥ H; the new prefix is 32 − h. Fifty hosts need h = 6 (2⁶ − 2 = 62), so the subnets are /26.
- Both. The prefix must be at least the one the subnets need and at most the one the hosts need. When those two limits cross, no equal split works.
# Step 1 of subnetting: how many bits to borrow from 192.168.10.0/24, from a number of subnets, a number of
# hosts per subnet, or both.
import math
def bits_for_subnets(count):
return (count - 1).bit_length() # the smallest s with 2**s >= count
def bits_for_hosts(count):
return (count + 1).bit_length() # the smallest h with 2**h - 2 >= count
def describe(prefix):
return f"/{prefix}, {2 ** (prefix - BASE)} subnets of {2 ** (32 - prefix) - 2} hosts"
def need_text(subnets, hosts):
if subnets and hosts:
return f"{subnets} subnets of {hosts} hosts"
return f"{subnets} subnets" if subnets else f"{hosts} hosts each"
BASE = 24
for subnets, hosts in [(6, None), (None, 50), (6, 25), (6, 40)]:
shortest = BASE + bits_for_subnets(subnets) if subnets else BASE # enough subnets from here on
longest = 32 - bits_for_hosts(hosts) if hosts else 30 # enough hosts up to here
need = need_text(subnets, hosts)
if shortest > longest:
print(f"{need}: impossible")
print(f" (subnets need /{shortest} or longer, hosts /{longest} or shorter)")
else:
print(f"{need}: {describe(shortest if subnets else longest)}")
# Whole numbers avoid the rounding of a floating-point logarithm at an exact power of two.
print("ceil(log(2**29, 2)) =", math.ceil(math.log(2**29, 2)))
print("bits_for_subnets(2**29) =", bits_for_subnets(2**29)) Output
6 subnets: /27, 8 subnets of 30 hosts 50 hosts each: /26, 4 subnets of 62 hosts 6 subnets of 25 hosts: /27, 8 subnets of 30 hosts 6 subnets of 40 hosts: impossible (subnets need /27 or longer, hosts /26 or shorter) ceil(log(2**29, 2)) = 30 bits_for_subnets(2**29) = 29
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 borrow_bits.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
The fourth case is worth a second look. Six subnets need at least 3 borrowed bits, which leaves 5 host bits and 30 hosts, while 40 hosts need 6 host bits, which leaves room to borrow only 2. The answer is to ask for a larger network, or to give the subnets different sizes with VLSM.
In code, (n - 1).bit_length() is the smallest s with 2^s ≥ n, and (n + 1).bit_length() is the smallest h with
2^h − 2 ≥ n. Both stay in whole numbers. A formula built on math.ceil(math.log(n, 2)) can be one too high at an
exact power of two, as the last two lines show: math.log(2**29, 2) comes out a little above 29, so its ceiling is
30.
Steps 2 and 3: the new mask and the block size
The new prefix gives the new mask, and only one octet of the mask is interesting: the one where the boundary falls. Prefixes /1 to /8 put the boundary in the first octet, /9 to /16 in the second, /17 to /24 in the third and /25 to /32 in the fourth. Inside that octet the mask can take only these values:
| Network bits in the octet | Mask value | Block size |
|---|---|---|
| 1 | 128 | 128 |
| 2 | 192 | 64 |
| 3 | 224 | 32 |
| 4 | 240 | 16 |
| 5 | 248 | 8 |
| 6 | 252 | 4 |
| 7 | 254 | 2 |
| 8 | 255 | 1 |
The block size is 256 minus the mask value: how far apart two neighbouring subnets are in that octet. For a /27 the fourth octet of the mask is 224, so the subnets are 32 apart. For a /20 the third octet of the mask is 240, so the third octet counts up in steps of 16. When the prefix is a multiple of 8, as with /16 or /24, the mask value in the interesting octet is 255 and the block size is 1: each subnet is one value of that octet, such as 10.1.0.0/16, 10.2.0.0/16 and 10.3.0.0/16.
Subnet Mask Cheat Sheet Every prefix with its mask, wildcard mask and host count, to look up a mask value or a block size.Step 4: list the subnets
Subnets start at every multiple of the block size in the interesting octet. Within one subnet:
- the network address is the start of the block;
- the first host is the start plus 1;
- the last host is the start plus the block size minus 2;
- the broadcast address is the start plus the block size minus 1, one less than the next subnet’s network address.
The block-size method on a number line
Text description of the diagram
The diagram draws the fourth octet of 192.168.10.0/24, the values 0 to 255, as one bar cut into eight equal blocks of 32 addresses, one per /27 subnet.
- The blocks start at .0, .32, .64, .96, .128, .160, .192 and .224: every multiple of the block size, 32.
- An arrow under the bar points at .77, inside the third block, which is shaded.
- The text under the bar says that 192.168.10.77 lies in 192.168.10.64/27, whose network address is .64, whose hosts are .65 to .94 and whose broadcast address is .95.
To place any address, round its fourth octet down to a multiple of 32: 77 rounds down to 64. The broadcast address is one less than the start of the next block, 96 − 1 = 95.
For 192.168.10.0/24 cut into /27s the block size is 32, so the eight subnets start at .0, .32, .64, .96, .128, .160,
.192 and .224. The program lists them from the block size and compares the list with ipaddress.subnets():
# Steps 2 to 4: the /27 subnets of 192.168.10.0/24 from the block size, compared with ipaddress.subnets().
import ipaddress
base = ipaddress.ip_network("192.168.10.0/24")
new_prefix = 27
mask = ipaddress.ip_network(f"0.0.0.0/{new_prefix}").netmask
block = 256 - int(str(mask).split(".")[3]) # the boundary falls in the fourth octet
print(f"mask {mask}, block size {block}")
by_hand = []
for start in range(0, 256, block):
hosts = f".{start + 1}-.{start + block - 2}"
print(f"192.168.10.{start:<3} hosts {hosts:<10} broadcast .{start + block - 1}")
by_hand.append(f"192.168.10.{start}/{new_prefix}")
print("same as ipaddress:", by_hand == [str(n) for n in base.subnets(new_prefix=new_prefix)]) Output
mask 255.255.255.224, block size 32 192.168.10.0 hosts .1-.30 broadcast .31 192.168.10.32 hosts .33-.62 broadcast .63 192.168.10.64 hosts .65-.94 broadcast .95 192.168.10.96 hosts .97-.126 broadcast .127 192.168.10.128 hosts .129-.158 broadcast .159 192.168.10.160 hosts .161-.190 broadcast .191 192.168.10.192 hosts .193-.222 broadcast .223 192.168.10.224 hosts .225-.254 broadcast .255 same as ipaddress: True
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 block_size.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Step 5: find the subnet of an address
Going the other way, from an address to its subnet, takes three moves. Take the interesting octet of the address and round it down to a multiple of the block size: that is the network address in that octet. Add the block size minus 1 to get the broadcast address in that octet. Octets before the interesting one stay as they are; octets after it become 0 in the network address and 255 in the broadcast address.
Take 172.16.45.77/20. The boundary is in the third octet, where the mask is 240 and the block size 16. 45 rounds down to 32, so the network address is 172.16.32.0, and 32 + 15 = 47 gives the broadcast address 172.16.47.255. The address lies strictly between them, so it is a usable host.
# Step 5: which subnet holds an address, by the block-size method, and whether the address is a usable host.
import ipaddress
def locate(address, prefix):
octets = [int(x) for x in address.split(".")]
i = (prefix - 1) // 8 # the octet where the boundary falls (0 to 3)
bits_in_octet = prefix - 8 * i # network bits inside that octet (1 to 8)
block = 2 ** (8 - bits_in_octet)
start = octets[i] // block * block
network = octets[:i] + [start] + [0] * (3 - i)
broadcast = octets[:i] + [start + block - 1] + [255] * (3 - i)
return ".".join(map(str, network)), ".".join(map(str, broadcast)), i + 1, block
for address, prefix in [("172.16.45.77", 20), ("192.168.10.95", 27), ("192.168.10.96", 27),
("10.200.15.255", 21), ("10.200.12.255", 21), ("10.200.12.0", 21)]:
network, broadcast, octet, block = locate(address, prefix)
role = {network: "network address", broadcast: "broadcast address"}.get(address, "usable host")
print(f"{address}/{prefix}: octet {octet}, block {block}")
print(f" {network} - {broadcast}, {role}")
net = ipaddress.ip_network(f"{address}/{prefix}", strict=False)
assert (str(net.network_address), str(net.broadcast_address)) == (network, broadcast) Output
172.16.45.77/20: octet 3, block 16 172.16.32.0 - 172.16.47.255, usable host 192.168.10.95/27: octet 4, block 32 192.168.10.64 - 192.168.10.95, broadcast address 192.168.10.96/27: octet 4, block 32 192.168.10.96 - 192.168.10.127, network address 10.200.15.255/21: octet 3, block 8 10.200.8.0 - 10.200.15.255, broadcast address 10.200.12.255/21: octet 3, block 8 10.200.8.0 - 10.200.15.255, usable host 10.200.12.0/21: octet 3, block 8 10.200.8.0 - 10.200.15.255, usable host
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 which_subnet.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
An address that ends in .255 is not always a broadcast address, and one that ends in .0 is not always a network address. In 10.200.8.0/21 the broadcast address is 10.200.15.255, so 10.200.12.255 and 10.200.12.0 are ordinary hosts. Only the interesting octet and the octets after it decide.
Subnets that cross an octet boundary
The borrowed bits do not have to fit inside one octet. Suppose 172.16.0.0/16 must be cut into at least 500 subnets. That needs 9 borrowed bits (2⁹ = 512), so the new prefix is /25. The boundary sits in the fourth octet, where the block size is 128, but the borrowed bits run through the whole third octet as well. Two /25 subnets fit in each value of the third octet: 172.16.0.0 and 172.16.0.128, then 172.16.1.0 and 172.16.1.128, and so on.
Subnet number k (counting from 0) therefore starts k × 128 addresses after the base. Dividing k × 128 by 256 gives the third octet, and the remainder gives the fourth: subnet 299, the 300th subnet, is 172.16.149.128/25.
# Subnetting across an octet boundary: 172.16.0.0/16 cut into at least 500 equal subnets.
import ipaddress
base = ipaddress.ip_network("172.16.0.0/16")
borrowed = (500 - 1).bit_length() # 2**9 = 512 >= 500
prefix = base.prefixlen + borrowed
size = 2 ** (32 - prefix)
print(f"/{prefix}: {2 ** borrowed} subnets of {size} addresses ({size - 2} hosts)")
# Each subnet is the base address plus k times the subnet size: with 128 addresses per subnet,
# two subnets fit in each value of the third octet.
for k in [0, 1, 2, 3, 299, 511]:
third, fourth = divmod(k * size, 256)
print(f"subnet {k:<3} 172.16.{third}.{fourth}/{prefix}")
subnets = list(base.subnets(new_prefix=prefix))
print("ipaddress agrees:", str(subnets[299]) == "172.16.149.128/25", len(subnets)) Output
/25: 512 subnets of 128 addresses (126 hosts) subnet 0 172.16.0.0/25 subnet 1 172.16.0.128/25 subnet 2 172.16.1.0/25 subnet 3 172.16.1.128/25 subnet 299 172.16.149.128/25 subnet 511 172.16.255.128/25 ipaddress agrees: True 512
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 nth_subnet.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
The zero subnet and the all-ones subnet
RFC 950 asked networks not to assign the subnets whose subnet bits are all zeros or all ones, because those values also meant “this network” and “all hosts”. Ten years later RFC 1878 called excluding them obsolete and noted that modern software can use every subnet that can be defined. Today a /24 split into /27s gives eight usable subnets, the first and the last included.
Some exam questions and older textbooks still follow the old rule. When a question says the zero subnet and the all-ones subnet must not be used, count 2^s − 2 subnets instead of 2^s: a /24 split with 3 borrowed bits then gives 6 usable subnets rather than 8. Without such an instruction, use all of them.
GATE CS
GATE papers mix multiple choice questions (MCQ), multiple select questions (MSQ) and numerical answer type (NAT) questions, and the official question paper pattern says a wrong MSQ or NAT answer costs no marks; only MCQs carry negative marking. A count of hosts or subnets is a number you type in, so a numerical subnetting question rewards checking the arithmetic rather than guessing.
Checking an answer in seconds
A few checks catch almost every slip:
- The network address’s interesting octet is a multiple of the block size, and every later octet is 0.
- The broadcast address is one less than the next subnet’s network address.
- A subnet has 2^h − 2 usable hosts (a /31 point-to-point link is the exception, with 2).
- The number of subnets times the addresses in each equals the addresses of the network you started with: 8 × 32 = 256 for the /27s of a /24.
- Writing only the interesting octet in binary confirms an edge: the host bits of a network address are all 0 and those of a broadcast address all 1.
Interview questions
What is the block size of a /27, and what are the first three /27 subnets of 192.168.10.0/24?
A /27 mask is 255.255.255.224, so the boundary falls in the fourth octet and the block size is 256 − 224 = 32: each /27 holds 32 addresses, 30 of them usable. Subnets start at multiples of 32 in the fourth octet, so the first three are 192.168.10.0/27 (hosts .1 to .30, broadcast .31), 192.168.10.32/27 (hosts .33 to .62, broadcast .63) and 192.168.10.64/27 (hosts .65 to .94, broadcast .95). A /24 holds eight of them; the last is 192.168.10.224/27.
Key takeaways
- Borrowing s host bits makes 2^s subnets; keeping h host bits leaves 2^h − 2 usable hosts in each.
- Six subnets need 3 borrowed bits and 50 hosts need 6 host bits; when the two needs cross, no equal split works.
- The block size is 256 minus the mask value in the octet where the boundary falls, and subnets start at its multiples.
- An address’s subnet: round its interesting octet down to a multiple of the block size; the broadcast address is one less than the next subnet.
- Borrowed bits can span octets: subnet k of a split starts k × (subnet size) addresses after the base.
- Modern networks use the zero subnet and the all-ones subnet; drop them only when a question says so.
Exercise
Exercise · Medium · Python
Split a network into equal subnets
Write plan_equal_subnets(base, need_subnets=None, need_hosts=None), which splits the IPv4 network base (a string such as "192.168.10.0/24") into equal subnets and returns all of them, in address order, as a list of strings such as "192.168.10.32/27".
- With only need_subnets, use the shortest prefix that gives at least that many subnets, so that each subnet is as large as possible. need_subnets=1 returns the base network itself. - With only need_hosts, use the longest prefix whose subnets each hold at least that many usable hosts (2^h − 2), so that there are as many subnets as possible. - With both, use the shortest prefix that gives enough subnets, as long as its subnets still hold need_hosts hosts. - Raise ValueError when the request cannot be met inside base (too many subnets, too many hosts, or both together), and when neither number is given.
Every subnet must keep at least two usable hosts, so the longest prefix you may return is /30. Count every subnet, including the zero subnet and the all-ones subnet. The sample tests import your function from plan.py.
Starter code · plan.py
def plan_equal_subnets(base, need_subnets=None, need_hosts=None):
"""Split base into equal subnets for need_subnets subnets and/or need_hosts hosts per subnet."""
# Replace this line with your code.
return [] The sample tests · test_plan.py
from plan import plan_equal_subnets
def refused(base, **numbers):
"""True when plan_equal_subnets raises ValueError for this request."""
try:
plan_equal_subnets(base, **numbers)
except ValueError:
return True
return False
def test_by_number_of_subnets():
"""uses the shortest prefix that gives enough subnets"""
assert plan_equal_subnets("192.168.10.0/24", need_subnets=6) == [
"192.168.10.0/27", "192.168.10.32/27", "192.168.10.64/27", "192.168.10.96/27",
"192.168.10.128/27", "192.168.10.160/27", "192.168.10.192/27", "192.168.10.224/27",
]
def test_by_hosts_per_subnet():
"""uses the longest prefix that still holds the hosts"""
assert plan_equal_subnets("192.168.10.0/24", need_hosts=50) == [
"192.168.10.0/26", "192.168.10.64/26", "192.168.10.128/26", "192.168.10.192/26",
]
def test_both_numbers():
"""meets both numbers with the largest subnets that are possible"""
assert len(plan_equal_subnets("192.168.10.0/24", need_subnets=6, need_hosts=25)) == 8
assert plan_equal_subnets("10.20.0.0/22", need_subnets=3, need_hosts=100) == [
"10.20.0.0/24", "10.20.1.0/24", "10.20.2.0/24", "10.20.3.0/24",
]
def test_across_an_octet_boundary():
"""lets the subnets cross from one octet into the next"""
assert plan_equal_subnets("10.20.0.0/22", need_subnets=8) == [
"10.20.0.0/25", "10.20.0.128/25", "10.20.1.0/25", "10.20.1.128/25",
"10.20.2.0/25", "10.20.2.128/25", "10.20.3.0/25", "10.20.3.128/25",
]
def test_one_subnet_and_exact_powers():
"""returns the base for one subnet and borrows no extra bit for a power of two"""
assert plan_equal_subnets("172.16.0.0/16", need_subnets=1) == ["172.16.0.0/16"]
assert len(plan_equal_subnets("172.16.0.0/16", need_subnets=4)) == 4
assert plan_equal_subnets("192.168.10.0/24", need_hosts=62) == [
"192.168.10.0/26", "192.168.10.64/26", "192.168.10.128/26", "192.168.10.192/26",
]
assert plan_equal_subnets("192.168.10.0/24", need_hosts=63) == ["192.168.10.0/25", "192.168.10.128/25"]
def test_impossible_requests():
"""raises ValueError when the network cannot meet the request"""
assert refused("192.168.10.0/24", need_subnets=6, need_hosts=40)
assert refused("192.168.10.0/24", need_hosts=300)
assert refused("192.168.10.0/24", need_subnets=128)
assert refused("192.168.10.0/24") A hint
Work with prefix lengths first and build the list last. (need_subnets - 1).bit_length() is the number of bits to borrow for the subnets, and (need_hosts + 1).bit_length() the number of host bits that hold the hosts. That gives the shortest and the longest prefix you may use; when the shortest is longer than the longest, the request cannot be met. ipaddress.ip_network(base).subnets(new_prefix=...) lists the subnets once you know the prefix.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Check yourself
6 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
References
- RFC 950: Internet Standard Subnetting Procedure (RFC Editor (IETF))
- RFC 1878: Variable Length Subnet Table For IPv4 (RFC Editor (IETF))
- RFC 4632: Classless Inter-domain Routing (CIDR): The Internet Address Assignment and Aggregation Plan (RFC Editor (IETF))
- RFC 3021: Using 31-Bit Prefixes on IPv4 Point-to-Point Links (RFC Editor (IETF))
- ipaddress: IPv4/IPv6 manipulation library (subnets()) (Python Software Foundation)
- GATE 2027: Question Paper Pattern (IIT Madras)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress