CIDR Subnet Report
Problem Statement
Before a VPC exists you have to size its subnets, and a /24 that "looks big enough" is how a training cluster runs out of addresses mid-scale-up. Given a CIDR block, derive its network address, broadcast address, netmask and usable host count, and say which of a list of IPs fall inside it.
Background
An IPv4 address is 32 bits. A CIDR block A.B.C.D/p fixes the leading p bits (the network part) and leaves 32 - p host bits free. The netmask is p ones followed by 32 - p zeros.
- Network address: the address with all host bits 0 — ip & mask. Note the address you are given need not be the network address: 10.0.5.37/22 still describes the block 10.0.4.0/22.
- Broadcast address: all host bits 1.
- Membership: ip is inside the block exactly when ip & mask == network.
- Usable hosts: 2 ** (32 - p) - 2 for p <= 30, since the network and broadcast addresses are not assignable. Two special cases: a /31 has 2 usable addresses (RFC 3021 point-to-point links) and a /32 has 1. Blindly subtracting 2 gives 0 and -1 there.
Solve this with integer bit arithmetic — do not use the ipaddress module.
Your Task
Implement:
def subnet_report(cidr, ips):
Return a dict with keys, in this order: "network", "broadcast", "netmask" (all dotted-quad strings), "usable_hosts" (int), and "in_subnet" (list of bools, one per entry of ips, in order).
Input Format
- cidr: string like "10.0.5.37/22".
- ips: list of dotted-quad strings to test.
Output Format
- The dict described above.
Sample
print(subnet_report("10.0.5.37/22", ["10.0.4.9", "10.0.8.1", "10.0.7.255"]))
Output:
{'network': '10.0.4.0', 'broadcast': '10.0.7.255', 'netmask': '255.255.252.0', 'usable_hosts': 1022, 'in_subnet': [True, False, True]}
A /22 borrows 2 bits from the third octet, so the block spans 10.0.4.0-10.0.7.255: 1024 addresses, 1022 assignable.
Example:
print(subnet_report("10.0.5.37/22", ["10.0.4.9", "10.0.8.1", "10.0.7.255"])){'network': '10.0.4.0', 'broadcast': '10.0.7.255', 'netmask': '255.255.252.0', 'usable_hosts': 1022, 'in_subnet': [True, False, True]}A /22 mask is 255.255.252.0. ANDing 10.0.5.37 with it clears the low 10 host bits and gives the network 10.0.4.0; setting all host bits gives the broadcast 10.0.7.255. The block holds 2**10 = 1024 addresses, minus network and broadcast, so 1022 are assignable. 10.0.8.1 masks to 10.0.8.0, a different network, so it is outside; the broadcast address itself is still inside the block.
Constraints:
cidris a valid IPv4 block, prefix length 0 to 32- The given address may have host bits set; normalise it to the network address
- Usable hosts:
2 ** (32 - p) - 2for p <= 30,2for a /31,1for a /32 - 0 <= len(ips) <= 100
- Use integer bit arithmetic, not the
ipaddressmodule
1. Background Knowledge
IPv4 addresses are 32-bit integers, typically represented in dotted-decimal notation (e.g., 192.168.1.1). A CIDR (Classless Inter-Domain Routing) block is defined by an IP address and a prefix length p (e.g., /24). The prefix length indicates how many leading bits constitute the network portion, while the remaining 32−p bits constitute the host portion.
The netmask is a 32-bit integer with the first p bits set to 1 and the remaining bits set to 0. It acts as a filter to isolate the network portion of any IP address within that block. The network address is the lowest address in the block, obtained by performing a bitwise AND between any IP in the block and the netmask. The broadcast address is the highest address, obtained by setting all host bits to 1.
Membership in a subnet is determined by checking if the network portion of a candidate IP matches the subnet's network address. Specifically, an IP x belongs to the subnet if (x & mask) == network_address. Usable host counts depend on the prefix length: for p≤30, it is 232−p−2 (excluding network and broadcast addresses). Special cases exist for /31 (2 usable) and /32 (1 usable) per RFC standards.
2. Algorithm Approach
The core approach relies on integer bit manipulation. Since Python handles arbitrarily large integers, we can treat IPv4 addresses as standard 32-bit integers. The algorithm involves three main phases:
- Parsing: Convert the CIDR string and list of IPs into their integer representations.
- Mask Calculation: Compute the netmask integer based on the prefix length p.
- Derivation: Use bitwise operations (&, |, ~) to derive the network address, broadcast address, and check membership for each IP.
This approach avoids string parsing complexities during calculation and leverages efficient bitwise operators.
3. Step-by-Step Strategy
- Helper Function: Create a helper to convert dotted-quad strings (e.g., "10.0.0.1") to integers. Split by ., convert each octet to int, and combine using bitwise shifts: a << 24 | b << 16 | c << 8 | d.
- Parse CIDR: Split the input cidr string by / to get the base IP and prefix length p. Convert the base IP to an integer.
- Compute Netmask:
- If p==0, mask is 0.
- Otherwise, mask is (0xFFFFFFFF << (32 - p)) & 0xFFFFFFFF. This creates p ones followed by zeros.
- Derive Network Address: Perform network = ip_int & mask.
- Derive Broadcast Address: The host bits are the inverse of the mask. broadcast = network | (~mask & 0xFFFFFFFF). Alternatively, broadcast = network | (mask ^ 0xFFFFFFFF).
- Calculate Usable Hosts:
- If p==32, return 1.
- If p==31, return 2.
- Otherwise, return 232−p−2.
- Check Membership: For each IP in ips:
- Convert IP to integer.
- Check if (ip_int & mask) == network.
- Store the boolean result.
- Format Output: Convert the integer network, broadcast, and mask back to dotted-quad strings. Construct the final dictionary.
4. Common Pitfalls
- Sign Extension: In Python, integers are signed. When shifting or inverting masks, ensure you mask with 0xFFFFFFFF to keep values within 32-bit unsigned range. For example, ~mask will produce a negative number; ~mask & 0xFFFFFFFF corrects this.
- Prefix Edge Cases: Do not blindly apply 232−p−2. You must explicitly handle /31 and /32 prefixes as specified in the problem description.
- Non-Network Input IPs: The input CIDR IP (e.g., 10.0.5.37/22) is not necessarily the network address. Always compute the network address via ip & mask rather than assuming the input IP is the start of the block.
- String Conversion: When converting integers back to dotted-quad strings, ensure you extract octets correctly using shifts and masks (e.g., (ip >> 24) & 0xFF for the first octet).
5. Time & Space Complexity
- Time Complexity: O(N), where N is the number of IPs in the ips list. Parsing the CIDR and computing the mask is O(1). Each IP check involves constant-time bitwise operations.
- Space Complexity: O(N) to store the in_subnet list of booleans. The auxiliary space for integers and strings is O(1).