DevToolBoxGRATIS
Blog

IP Subnet Calculator: CIDR Notation & Subnetting β€” Complete Guide

13 min readby DevToolBox

TL;DR

A subnet divides an IP network into smaller segments. CIDR notation like 192.168.1.0/24 means 24 bits for the network, 8 bits for hosts = 254 usable addresses. Calculate usable hosts with 2^(32 - prefix) - 2. Use VLSM to allocate different-sized subnets for different needs. AWS reserves 5 IPs per subnet (not 2). Docker uses 172.17.0.0/16, Kubernetes pods use 10.244.0.0/16. Try our online IP subnet calculator to compute any CIDR block instantly.

CIDR Notation Basics β€” IP Classes, Subnet Masks, and Address Structure

CIDR (Classless Inter-Domain Routing) is the modern standard for IP address allocation and routing. Before CIDR, IP addresses were divided into fixed classes β€” Class A, B, and C β€” which caused massive waste of address space. CIDR allows any prefix length from /0 to /32, enabling precise allocation.

The Original IP Classes (Pre-CIDR)

ClassFirst OctetCIDR Equiv.Default MaskMax Hosts
A1–126/8255.0.0.016,777,214
B128–191/16255.255.0.065,534
C192–223/24255.255.255.0254
D224–239β€”β€”Multicast only
E240–255β€”β€”Reserved/Experimental

How CIDR Notation Works β€” Bit-Level Breakdown

A subnet mask is a 32-bit number where network bits are 1 and host bits are 0. The prefix length in CIDR notation is simply the count of consecutive 1 bits from the left:

# 192.168.1.0/24 β€” breakdown:
IP Address:  192.168.1.0   = 11000000.10101000.00000001.00000000
Subnet Mask: 255.255.255.0 = 11111111.11111111.11111111.00000000
                              |<--- 24 network bits --->|<8 host->|

Network Address  = IP AND Mask = 192.168.1.0   (all host bits = 0)
Broadcast Address              = 192.168.1.255  (all host bits = 1)
First Usable Host              = 192.168.1.1
Last Usable Host               = 192.168.1.254
Usable Hosts                   = 2^8 - 2 = 254

# 10.0.0.0/8 β€” breakdown:
IP Address:  10.0.0.0      = 00001010.00000000.00000000.00000000
Subnet Mask: 255.0.0.0     = 11111111.00000000.00000000.00000000
                              |< 8 >|  |<------- 24 host bits ------->|
Usable Hosts = 2^24 - 2 = 16,777,214

# Converting prefix to mask in Python:
import ipaddress
net = ipaddress.IPv4Network('192.168.1.0/24')
print(net.netmask)         # 255.255.255.0
print(net.hostmask)        # 0.0.0.255
print(net.num_addresses)   # 256
print(net.network_address) # 192.168.1.0
print(net.broadcast_address) # 192.168.1.255

JavaScript IP Subnet Calculator β€” Pure JS Implementation

Here is a complete, dependency-free JavaScript function that parses any CIDR block and returns all subnet details: network address, broadcast address, first/last usable host, host count, subnet mask, and wildcard mask.

/**
 * Parse a CIDR string and compute subnet details.
 * @param {string} cidr - e.g. "192.168.1.0/24" or "10.0.0.5/16"
 * @returns {object} Subnet calculation results
 */
function calculateSubnet(cidr) {
  const [ipStr, prefixStr] = cidr.split('/');
  const prefix = parseInt(prefixStr, 10);

  if (prefix < 0 || prefix > 32) {
    throw new Error('Prefix length must be between 0 and 32');
  }

  // Convert IP string to a 32-bit integer
  const ipToInt = (ip) =>
    ip.split('.').reduce((acc, octet) => (acc << 8) | parseInt(octet, 10), 0) >>> 0;

  // Convert 32-bit integer back to dotted-decimal string
  const intToIp = (n) =>
    [(n >>> 24) & 255, (n >>> 16) & 255, (n >>> 8) & 255, n & 255].join('.');

  const ipInt = ipToInt(ipStr);

  // Build mask: prefix 1-bits followed by (32-prefix) 0-bits
  const maskInt = prefix === 0 ? 0 : (0xFFFFFFFF << (32 - prefix)) >>> 0;
  const wildcardInt = (~maskInt) >>> 0;

  const networkInt = (ipInt & maskInt) >>> 0;
  const broadcastInt = (networkInt | wildcardInt) >>> 0;

  const totalAddresses = Math.pow(2, 32 - prefix);
  const usableHosts = prefix >= 31 ? totalAddresses : totalAddresses - 2;

  return {
    cidr,
    ipAddress:        ipStr,
    prefix,
    subnetMask:       intToIp(maskInt),
    wildcardMask:     intToIp(wildcardInt),
    networkAddress:   intToIp(networkInt),
    broadcastAddress: intToIp(broadcastInt),
    firstUsableHost:  prefix >= 31 ? intToIp(networkInt) : intToIp(networkInt + 1),
    lastUsableHost:   prefix >= 31 ? intToIp(broadcastInt) : intToIp(broadcastInt - 1),
    totalAddresses,
    usableHosts,
    ipClass: (() => {
      const firstOctet = (networkInt >>> 24) & 255;
      if (firstOctet < 128) return 'A';
      if (firstOctet < 192) return 'B';
      if (firstOctet < 224) return 'C';
      if (firstOctet < 240) return 'D (Multicast)';
      return 'E (Reserved)';
    })(),
    isPrivate: (() => {
      const a = (networkInt >>> 24) & 255;
      const b = (networkInt >>> 16) & 255;
      return (a === 10) ||
             (a === 172 && b >= 16 && b <= 31) ||
             (a === 192 && b === 168) ||
             (a === 127);
    })(),
  };
}

// Example usage:
const result = calculateSubnet('192.168.1.0/24');
console.log(result);
/* Output:
{
  cidr: '192.168.1.0/24',
  ipAddress: '192.168.1.0',
  prefix: 24,
  subnetMask: '255.255.255.0',
  wildcardMask: '0.0.0.255',
  networkAddress: '192.168.1.0',
  broadcastAddress: '192.168.1.255',
  firstUsableHost: '192.168.1.1',
  lastUsableHost: '192.168.1.254',
  totalAddresses: 256,
  usableHosts: 254,
  ipClass: 'C',
  isPrivate: true
}
*/

// Check if an IP is within a subnet
function isIpInSubnet(ip, cidr) {
  const { networkAddress, broadcastAddress } = calculateSubnet(cidr);
  const ipToInt = (s) => s.split('.').reduce((a, o) => (a << 8) | +o, 0) >>> 0;
  const ipInt = ipToInt(ip);
  return ipInt >= ipToInt(networkAddress) && ipInt <= ipToInt(broadcastAddress);
}

console.log(isIpInSubnet('192.168.1.100', '192.168.1.0/24')); // true
console.log(isIpInSubnet('192.168.2.1',   '192.168.1.0/24')); // false

// Split a subnet into smaller subnets
function splitSubnet(cidr, newPrefix) {
  const { networkAddress, prefix } = calculateSubnet(cidr);
  if (newPrefix <= prefix) throw new Error('New prefix must be larger than original');
  const count = Math.pow(2, newPrefix - prefix);
  const blockSize = Math.pow(2, 32 - newPrefix);

  const ipToInt = (s) => s.split('.').reduce((a, o) => (a << 8) | +o, 0) >>> 0;
  const intToIp = (n) => [(n>>>24)&255,(n>>>16)&255,(n>>>8)&255,n&255].join('.');

  const baseInt = ipToInt(networkAddress);
  return Array.from({ length: count }, (_, i) =>
    `${intToIp((baseInt + i * blockSize) >>> 0)}/${newPrefix}`
  );
}

console.log(splitSubnet('192.168.1.0/24', 26));
// ['192.168.1.0/26', '192.168.1.64/26', '192.168.1.128/26', '192.168.1.192/26']

Python ipaddress Module β€” Complete Subnet Calculation

Python's built-in ipaddress module (Python 3.3+) provides comprehensive IP networking capabilities with no external dependencies. It handles both IPv4 and IPv6 natively.

import ipaddress

# ── IPv4 Network ──────────────────────────────────────────────────────────────
# strict=False allows host bits to be set (e.g., 192.168.1.5/24 β†’ 192.168.1.0/24)
net = ipaddress.IPv4Network('192.168.1.0/24', strict=True)

print(net.network_address)   # 192.168.1.0
print(net.broadcast_address) # 192.168.1.255
print(net.netmask)           # 255.255.255.0
print(net.hostmask)          # 0.0.0.255
print(net.prefixlen)         # 24
print(net.num_addresses)     # 256
print(net.max_prefixlen)     # 32

# Usable hosts (excludes network + broadcast)
hosts = list(net.hosts())
print(f"First host: {hosts[0]}")   # 192.168.1.1
print(f"Last host:  {hosts[-1]}")  # 192.168.1.254
print(f"Host count: {len(hosts)}") # 254

# Network properties
print(net.is_private)   # True  (RFC 1918)
print(net.is_global)    # False
print(net.is_loopback)  # False
print(net.is_multicast) # False

# ── IPv4 Interface (IP + network context) ────────────────────────────────────
iface = ipaddress.IPv4Interface('192.168.1.5/24')
print(iface.ip)          # 192.168.1.5  (the specific host IP)
print(iface.network)     # 192.168.1.0/24 (the network it belongs to)
print(iface.netmask)     # 255.255.255.0

# ── Subnets β€” splitting into smaller blocks ───────────────────────────────────
net = ipaddress.IPv4Network('10.0.0.0/16')

# Split /16 into /18 subnets (4 subnets)
subnets = list(net.subnets(new_prefix=18))
for s in subnets:
    print(s)
# 10.0.0.0/18
# 10.0.64.0/18
# 10.0.128.0/18
# 10.0.192.0/18

# Split by specifying number of additional bits (prefixlen_diff)
# Split /24 into /26 (2 additional bits = 4 subnets)
net24 = ipaddress.IPv4Network('192.168.1.0/24')
for sub in net24.subnets(prefixlen_diff=2):
    hosts = net24.num_addresses // 4 - 2
    print(f"{sub}  β†’  {list(sub.hosts())[0]} – {list(sub.hosts())[-1]}  ({hosts} hosts)")
# 192.168.1.0/26  β†’  192.168.1.1 – 192.168.1.62   (62 hosts)
# 192.168.1.64/26 β†’  192.168.1.65 – 192.168.1.126  (62 hosts)
# 192.168.1.128/26β†’  192.168.1.129 – 192.168.1.190 (62 hosts)
# 192.168.1.192/26β†’  192.168.1.193 – 192.168.1.254 (62 hosts)

# ── Supernet β€” aggregating into a larger block ────────────────────────────────
small = ipaddress.IPv4Network('192.168.1.0/25')
print(small.supernet())   # 192.168.1.0/24
print(small.supernet(new_prefix=22))  # 192.168.0.0/22

# ── Containment check ────────────────────────────────────────────────────────
net = ipaddress.IPv4Network('10.0.0.0/8')
ip = ipaddress.IPv4Address('10.5.6.7')
print(ip in net)  # True

# ── Summarize a range into CIDR blocks (reverse lookup) ─────────────────────
first = ipaddress.IPv4Address('192.168.1.0')
last  = ipaddress.IPv4Address('192.168.1.255')
summary = list(ipaddress.summarize_address_range(first, last))
print(summary)  # [IPv4Network('192.168.1.0/24')]

# Collapse overlapping networks
networks = [
    ipaddress.IPv4Network('10.0.0.0/24'),
    ipaddress.IPv4Network('10.0.1.0/24'),
    ipaddress.IPv4Network('10.0.0.0/23'),  # overlaps with both above
]
collapsed = list(ipaddress.collapse_addresses(networks))
print(collapsed)  # [IPv4Network('10.0.0.0/23')]

Subnet Reference Table β€” /8 to /32 with Host Counts and Masks

The table below covers every practically significant prefix length, from large backbone blocks to single-host routes. Bookmark this as a quick reference when designing network architectures.

PrefixSubnet MaskTotal IPsUsable HostsCommon Use
/8255.0.0.016,777,21616,777,214Large ISP / Class A private (10.0.0.0/8)
/12255.240.0.01,048,5761,048,574RFC 1918 172.16.0.0/12 private range
/16255.255.0.065,53665,534Large org LAN / AWS VPC typical size
/20255.255.240.04,0964,094Medium data center segment
/21255.255.248.02,0482,046Campus network block
/22255.255.252.01,0241,022Large office LAN
/23255.255.254.0512510Medium office / Kubernetes node block
/24255.255.255.0256254Standard LAN / AWS subnet typical
/25255.255.255.128128126Half of /24, split LAN
/26255.255.255.1926462Small department LAN
/27255.255.255.2243230Small office segment
/28255.255.255.2401614AWS minimum subnet size
/29255.255.255.24886Small cluster / management network
/30255.255.255.25242Point-to-point link (router↔router)
/31255.255.255.25422P2P link, no network/broadcast (RFC 3021)
/32255.255.255.25511Single host route / loopback

IPv6 Subnetting β€” /48, /64, /128, and Prefix Length

IPv6 uses 128-bit addresses written as eight groups of four hexadecimal digits separated by colons, such as 2001:0db8:85a3:0000:0000:8a2e:0370:7334. Consecutive groups of zeros can be replaced with :: once per address. The standard allocation hierarchy is:

  • /32 β€” ISP allocation from Regional Internet Registry (RIR)
  • /48 β€” Typical allocation to a single organization or site
  • /56 β€” Sometimes given to home users by ISPs
  • /64 β€” A single LAN segment; required for SLAAC (Stateless Address Autoconfiguration)
  • /128 β€” A single host (like /32 in IPv4)
import ipaddress

# ── IPv6 Network ──────────────────────────────────────────────────────────────
net6 = ipaddress.IPv6Network('2001:db8::/32')
print(net6.network_address)   # 2001:db8::
print(net6.prefixlen)         # 32
print(net6.num_addresses)     # 79228162514264337593543950336 (2^96)

# A /48 β€” typical organization block
org = ipaddress.IPv6Network('2001:db8:abcd::/48')
print(f"Subnets available (/64): {2**(64-48):,}")  # 65,536 LANs

# Split /48 into /64 subnets (each LAN segment)
subnets_64 = list(org.subnets(new_prefix=64))
print(f"First LAN: {subnets_64[0]}")   # 2001:db8:abcd::/64
print(f"Second LAN: {subnets_64[1]}")  # 2001:db8:abcd:1::/64
print(f"Total /64 subnets: {len(subnets_64)}")  # 65536

# A /64 LAN segment β€” how many addresses?
lan = ipaddress.IPv6Network('2001:db8:abcd:1::/64')
print(f"Addresses in /64: {lan.num_addresses:,}")
# 18,446,744,073,709,551,616 (2^64)

# Special IPv6 addresses
loopback  = ipaddress.IPv6Address('::1')
link_local = ipaddress.IPv6Network('fe80::/10')
ula        = ipaddress.IPv6Network('fc00::/7')  # Unique Local (private)

print(loopback.is_loopback)               # True
print(ipaddress.IPv6Address('fe80::1').is_link_local)  # True
print(ipaddress.IPv6Address('fd00::1') in ula)         # True

# IPv6 address types
addr = ipaddress.IPv6Address('2001:db8::1')
print(addr.is_global)       # True (public routable)
print(addr.is_private)      # False
print(addr.is_multicast)    # False
print(addr.is_unspecified)  # False (:: is unspecified)

# EUI-64: IPv6 auto-configuration from MAC address
# Given MAC: 00:1A:2B:3C:4D:5E
# 1. Split at middle: 00:1A:2B | 3C:4D:5E
# 2. Insert FF:FE: 00:1A:2B:FF:FE:3C:4D:5E
# 3. Flip 7th bit of first byte: 02:1A:2B:FF:FE:3C:4D:5E
# 4. Combine with prefix fe80::/64:
# Result: fe80::021a:2bff:fe3c:4d5e

def mac_to_eui64(mac, prefix='fe80::'):
    parts = mac.lower().replace('-', ':').split(':')
    parts[0] = format(int(parts[0], 16) ^ 0x02, '02x')  # flip bit 7
    eui64 = parts[:3] + ['ff', 'fe'] + parts[3:]
    groups = [''.join(eui64[i:i+2]) for i in range(0, 8, 2)]
    return f"{prefix}{':'.join(groups)}"

print(mac_to_eui64('00:1A:2B:3C:4D:5E'))
# fe80::021a:2bff:fe3c:4d5e

VLSM β€” Split 192.168.1.0/24 into Multiple Subnets of Different Sizes

Variable Length Subnet Masking (VLSM) lets you carve a single IP block into subnets of different sizes to avoid wasting addresses. The key rule: always allocate the largest subnet first, work down in size.

Scenario: You have 192.168.1.0/24 (254 usable hosts) and need to create:

  • Department A: 100 hosts needed
  • Department B: 50 hosts needed
  • Department C: 25 hosts needed
  • WAN Link 1: 2 hosts (router point-to-point)
  • WAN Link 2: 2 hosts (router point-to-point)
# VLSM Allocation for 192.168.1.0/24
# Rule: sort requirements largest-first, allocate sequentially

# Step 1 β€” Dept A needs 100 hosts β†’ smallest subnet that fits β‰₯ 102 total
# 2^7 = 128 addresses β†’ /25 (126 usable hosts) βœ“
Dept A: 192.168.1.0/25
  Network:    192.168.1.0
  Broadcast:  192.168.1.127
  Usable:     192.168.1.1 – 192.168.1.126  (126 hosts)
  Remaining:  192.168.1.128/25

# Step 2 β€” Dept B needs 50 hosts β†’ /26 (62 usable hosts) βœ“
Dept B: 192.168.1.128/26
  Network:    192.168.1.128
  Broadcast:  192.168.1.191
  Usable:     192.168.1.129 – 192.168.1.190  (62 hosts)
  Remaining:  192.168.1.192/26

# Step 3 β€” Dept C needs 25 hosts β†’ /27 (30 usable hosts) βœ“
Dept C: 192.168.1.192/27
  Network:    192.168.1.192
  Broadcast:  192.168.1.223
  Usable:     192.168.1.193 – 192.168.1.222  (30 hosts)
  Remaining:  192.168.1.224/27

# Step 4 β€” WAN Link 1 needs 2 hosts β†’ /30 (2 usable hosts) βœ“
WAN1: 192.168.1.224/30
  Network:    192.168.1.224
  Broadcast:  192.168.1.227
  Usable:     192.168.1.225 – 192.168.1.226  (2 hosts)
  Remaining:  192.168.1.228/30 + 192.168.1.232/29 + ...

# Step 5 β€” WAN Link 2 needs 2 hosts β†’ /30
WAN2: 192.168.1.228/30
  Network:    192.168.1.228
  Broadcast:  192.168.1.231
  Usable:     192.168.1.229 – 192.168.1.230  (2 hosts)
  Remaining:  192.168.1.232/29 (unused, available for future)

# ── Python VLSM allocator ──────────────────────────────────────────────────
import ipaddress

def vlsm_allocate(base_cidr, requirements):
    """
    Allocate subnets using VLSM.
    requirements: list of (name, min_hosts) sorted largest-first
    """
    pool = ipaddress.IPv4Network(base_cidr, strict=False)
    available = [pool]
    allocations = []

    for name, min_hosts in sorted(requirements, key=lambda x: x[1], reverse=True):
        # Find the smallest prefix that provides enough hosts
        needed_prefix = 32
        for prefix in range(30, -1, -1):
            if (2 ** (32 - prefix) - 2) >= min_hosts:
                needed_prefix = prefix
            else:
                break

        # Find first available block that can fit this prefix
        for i, block in enumerate(available):
            if block.prefixlen <= needed_prefix:
                # Carve the subnet from this block
                subnet = list(block.subnets(new_prefix=needed_prefix))[0]
                allocations.append((name, subnet, min_hosts))
                available.pop(i)
                # Return remaining space to pool
                remaining = list(block.address_exclude(subnet))
                available = remaining + available[i:]
                break

    return allocations

reqs = [
    ('Dept A', 100),
    ('Dept B', 50),
    ('Dept C', 25),
    ('WAN 1', 2),
    ('WAN 2', 2),
]

results = vlsm_allocate('192.168.1.0/24', reqs)
for name, subnet, needed in results:
    hosts = list(subnet.hosts())
    print(f"{name:8} β†’ {str(subnet):20} usable: {len(hosts):3}  needed: {needed}")

# Dept A   β†’ 192.168.1.0/25        usable: 126  needed: 100
# Dept B   β†’ 192.168.1.128/26      usable:  62  needed:  50
# Dept C   β†’ 192.168.1.192/27      usable:  30  needed:  25
# WAN 1    β†’ 192.168.1.224/30      usable:   2  needed:   2
# WAN 2    β†’ 192.168.1.228/30      usable:   2  needed:   2

Node.js ip-cidr Library β€” npm Install and Usage

The ip-cidr npm package provides a clean API for working with CIDR blocks in Node.js applications. It supports both IPv4 and IPv6 and is useful for backend validation, firewall rule engines, and IP address management tools.

# Install the package
npm install ip-cidr

# Also useful: the 'cidr-tools' package for set operations
npm install cidr-tools

# ── Basic ip-cidr usage ───────────────────────────────────────────────────────
const IPCIDR = require('ip-cidr');

const cidr = new IPCIDR('192.168.1.0/24');

// Validation
console.log(IPCIDR.isValidCIDR('192.168.1.0/24')); // true
console.log(IPCIDR.isValidCIDR('256.0.0.0/24'));   // false

// Network info
console.log(cidr.start());    // '192.168.1.0'  (network address)
console.log(cidr.end());      // '192.168.1.255' (broadcast)
console.log(cidr.toString()); // '192.168.1.0/24'

// Get specific addresses
const options = { type: 'addressObject' };
console.log(cidr.start(options)); // { address: '192.168.1.0', subnet: '/24' }

// Containment check
const cidr2 = new IPCIDR('10.0.0.0/8');
const isContained = new IPCIDR('10.5.0.0/16');
// Manual containment: compare integer ranges

// Iterate over IPs (be careful with large ranges!)
const smallCidr = new IPCIDR('192.168.1.248/29');
smallCidr.loop((ip) => {
  console.log(ip);
});
// 192.168.1.248
// 192.168.1.249
// ...
// 192.168.1.255

// Get array of all IPs in range
const ips = smallCidr.toArray();
console.log(ips); // ['192.168.1.248', ..., '192.168.1.255']

// Get start/end as integers for comparison
const startInt = smallCidr.start({ type: 'bigInteger' });
const endInt   = smallCidr.end({ type: 'bigInteger' });

// ── cidr-tools: set operations ────────────────────────────────────────────────
const { merge, exclude, contains, overlap, expand } = require('cidr-tools');

// Merge overlapping/adjacent CIDRs
const merged = merge(['10.0.0.0/24', '10.0.1.0/24']);
console.log(merged); // ['10.0.0.0/23']

// Exclude a subnet from a block
const remaining = exclude(['10.0.0.0/24'], ['10.0.0.0/26']);
console.log(remaining); // ['10.0.0.64/26', '10.0.0.128/25']

// Check containment
console.log(contains('10.0.0.0/8', '10.5.6.0/24')); // true
console.log(contains('10.0.0.0/8', '192.168.1.0/24')); // false

// Check overlap between two CIDR blocks
console.log(overlap('10.0.0.0/8', '10.5.0.0/16')); // true
console.log(overlap('10.0.0.0/8', '192.168.1.0/24')); // false

// ── Express.js IP allowlist middleware ─────────────────────────────────────────
const express = require('express');
const app = express();

const ALLOWED_CIDRS = ['10.0.0.0/8', '192.168.0.0/16', '172.16.0.0/12'];

function isPrivateIP(ip) {
  // Remove IPv6 prefix if present (e.g., ::ffff:192.168.1.1)
  const cleanIp = ip.replace(/^::ffff:/, '');
  return ALLOWED_CIDRS.some((cidrStr) => {
    const cidrObj = new IPCIDR(cidrStr);
    // Simplified containment via range check
    return cidrObj.contains ? cidrObj.contains(cleanIp) : false;
  });
}

app.use((req, res, next) => {
  const clientIp = req.ip || req.connection.remoteAddress;
  if (!isPrivateIP(clientIp)) {
    return res.status(403).json({ error: 'Access denied: IP not in allowlist' });
  }
  next();
});

Go net Package β€” ParseCIDR, Contains, and Subnet Ops

Go's standard library net package provides robust IP and CIDR handling with zero dependencies. It is the foundation for Go-based network tools like Kubernetes, Docker, and Terraform.

package main

import (
    "encoding/binary"
    "fmt"
    "math"
    "net"
)

func main() {
    // ── net.ParseCIDR ───────────────────────────────────────────────────────────
    // Returns the IP address (with host bits) AND the network
    ip, ipNet, err := net.ParseCIDR("192.168.1.42/24")
    if err != nil {
        panic(err)
    }

    fmt.Println(ip)              // 192.168.1.42  (the original IP)
    fmt.Println(ipNet.IP)        // 192.168.1.0   (network address)
    fmt.Println(ipNet.Mask)      // ffffff00       (hex mask)

    ones, bits := ipNet.Mask.Size()
    fmt.Printf("/%d of %d bits
", ones, bits) // /24 of 32 bits

    // Human-readable subnet mask
    mask := net.IP(ipNet.Mask)
    fmt.Println(mask.String()) // 255.255.255.0 (trick: treat mask as IPv4)

    // ── Containment: IPNet.Contains ───────────────────────────────────────────
    _, subnet, _ := net.ParseCIDR("10.0.0.0/8")

    testIPs := []string{"10.5.6.7", "192.168.1.1", "10.255.255.255"}
    for _, testIP := range testIPs {
        fmt.Printf("%s in %s: %v
", testIP, subnet, subnet.Contains(net.ParseIP(testIP)))
    }
    // 10.5.6.7       in 10.0.0.0/8: true
    // 192.168.1.1    in 10.0.0.0/8: false
    // 10.255.255.255 in 10.0.0.0/8: true

    // ── Manual subnet arithmetic ──────────────────────────────────────────────
    _, network, _ := net.ParseCIDR("192.168.1.0/24")

    netIP := network.IP.To4()
    netInt := binary.BigEndian.Uint32(netIP)

    ones2, _ := network.Mask.Size()
    hostBits := 32 - ones2
    totalAddrs := uint32(math.Pow(2, float64(hostBits)))

    networkAddr := netInt
    broadcastAddr := netInt | (totalAddrs - 1)
    firstHost := networkAddr + 1
    lastHost := broadcastAddr - 1
    usableHosts := totalAddrs - 2

    intToIP := func(n uint32) net.IP {
        ip := make(net.IP, 4)
        binary.BigEndian.PutUint32(ip, n)
        return ip
    }

    fmt.Printf("Network:   %s
", intToIP(networkAddr))   // 192.168.1.0
    fmt.Printf("Broadcast: %s
", intToIP(broadcastAddr)) // 192.168.1.255
    fmt.Printf("First:     %s
", intToIP(firstHost))     // 192.168.1.1
    fmt.Printf("Last:      %s
", intToIP(lastHost))      // 192.168.1.254
    fmt.Printf("Hosts:     %d
", usableHosts)            // 254

    // ── Split subnet into smaller blocks ──────────────────────────────────────
    _, base, _ := net.ParseCIDR("10.0.0.0/16")
    splitPrefix := 18 // split into /18 blocks

    baseMaskOnes, _ := base.Mask.Size()
    count := int(math.Pow(2, float64(splitPrefix-baseMaskOnes)))

    blockSize := uint32(math.Pow(2, float64(32-splitPrefix)))
    baseInt := binary.BigEndian.Uint32(base.IP.To4())

    fmt.Printf("
Splitting %s into /%d subnets:
", base, splitPrefix)
    for i := 0; i < count; i++ {
        startInt := baseInt + uint32(i)*blockSize
        subnet := &net.IPNet{
            IP:   intToIP(startInt),
            Mask: net.CIDRMask(splitPrefix, 32),
        }
        fmt.Printf("  %s
", subnet)
    }
    // 10.0.0.0/18
    // 10.0.64.0/18
    // 10.0.128.0/18
    // 10.0.192.0/18

    // ── Check if IP is private (RFC 1918) ────────────────────────────────────
    privateRanges := []*net.IPNet{}
    for _, cidr := range []string{"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "127.0.0.0/8"} {
        _, network, _ := net.ParseCIDR(cidr)
        privateRanges = append(privateRanges, network)
    }

    isPrivate := func(ip net.IP) bool {
        for _, network := range privateRanges {
            if network.Contains(ip) {
                return true
            }
        }
        return false
    }

    fmt.Println(isPrivate(net.ParseIP("192.168.1.1"))) // true
    fmt.Println(isPrivate(net.ParseIP("8.8.8.8")))     // false
}

AWS and Cloud Subnetting β€” VPC Design and Reserved Addresses

Cloud VPC networking follows the same CIDR principles but with platform-specific constraints. Understanding these is critical when designing production AWS, GCP, or Azure network architectures.

AWS VPC Subnetting Rules

# AWS VPC CIDR constraints:
# - VPC CIDR: /16 to /28 (16 to 65,536 addresses)
# - Subnet CIDR: /16 to /28
# - Subnet must be a subset of VPC CIDR
# - AWS ALWAYS reserves 5 IPs per subnet (not the standard 2)

# Example: VPC 10.0.0.0/16 β†’ subnet 10.0.1.0/24
Reserved addresses in 10.0.1.0/24:
  10.0.1.0   β†’ Network address (standard)
  10.0.1.1   β†’ AWS VPC router (gateway)
  10.0.1.2   β†’ AWS DNS server (always VPC_CIDR_BASE + 2)
  10.0.1.3   β†’ AWS future use (reserved)
  10.0.1.255 β†’ Broadcast address (standard)

Usable for EC2 instances: 10.0.1.4 – 10.0.1.254 = 251 addresses (not 254!)

# For /28 (minimum subnet size): 16 - 5 = 11 usable addresses
# For /24: 256 - 5 = 251 usable addresses

# ── Typical 3-tier AWS VPC architecture (10.0.0.0/16) ─────────────────────────
VPC: 10.0.0.0/16  (65,536 addresses)

# Public subnets (one per AZ) β€” for ALB, NAT Gateway, Bastion
10.0.1.0/24   (us-east-1a public)  β†’ 251 usable
10.0.2.0/24   (us-east-1b public)  β†’ 251 usable
10.0.3.0/24   (us-east-1c public)  β†’ 251 usable

# Private subnets (one per AZ) β€” for EC2, ECS, RDS
10.0.11.0/24  (us-east-1a private) β†’ 251 usable
10.0.12.0/24  (us-east-1b private) β†’ 251 usable
10.0.13.0/24  (us-east-1c private) β†’ 251 usable

# Database subnets (one per AZ) β€” for RDS Multi-AZ
10.0.21.0/24  (us-east-1a db)      β†’ 251 usable
10.0.22.0/24  (us-east-1b db)      β†’ 251 usable
10.0.23.0/24  (us-east-1c db)      β†’ 251 usable

# Reserved for future expansion
10.0.100.0/22 (future use)         β†’ 1,019 usable

# ── AWS CloudFormation CIDR helper function ────────────────────────────────────
# Fn::Cidr splits a CIDR block into smaller subnets
Resources:
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16

  PublicSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      # Fn::Cidr: [cidr, count, hostBits]
      # Split 10.0.0.0/16 into 256 subnets of /24 (8 host bits), take first
      CidrBlock: !Select [0, !Cidr [!GetAtt VPC.CidrBlock, 256, 8]]
      AvailabilityZone: !Select [0, !GetAZs '']

  PrivateSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: !Select [10, !Cidr [!GetAtt VPC.CidrBlock, 256, 8]]
      AvailabilityZone: !Select [0, !GetAZs '']

# ── Terraform AWS VPC subnetting ──────────────────────────────────────────────
# main.tf
variable "vpc_cidr" {
  default = "10.0.0.0/16"
}

variable "availability_zones" {
  default = ["us-east-1a", "us-east-1b", "us-east-1c"]
}

resource "aws_vpc" "main" {
  cidr_block           = var.vpc_cidr
  enable_dns_hostnames = true
  enable_dns_support   = true
}

resource "aws_subnet" "public" {
  count             = length(var.availability_zones)
  vpc_id            = aws_vpc.main.id
  # cidrsubnet(prefix, newbits, netnum)
  # cidrsubnet("10.0.0.0/16", 8, count.index) β†’ 10.0.0.0/24, 10.0.1.0/24, ...
  cidr_block        = cidrsubnet(var.vpc_cidr, 8, count.index)
  availability_zone = var.availability_zones[count.index]
  map_public_ip_on_launch = true
}

resource "aws_subnet" "private" {
  count             = length(var.availability_zones)
  vpc_id            = aws_vpc.main.id
  cidr_block        = cidrsubnet(var.vpc_cidr, 8, count.index + 10)
  availability_zone = var.availability_zones[count.index]
}

# ── GCP and Azure equivalents ─────────────────────────────────────────────────
# GCP VPC: /8 to /29 subnets, only 4 reserved IPs (not 5 like AWS)
#   .0 = network, .1 = gateway, .2 = DNS, .255 = broadcast

# Azure VNet: /8 to /29, 5 reserved IPs like AWS:
#   .0 = network, .1 = gateway, .2-.3 = Azure DNS, .255 = broadcast

Common Use Cases β€” Firewalls, Docker, Kubernetes, and More

Subnetting knowledge applies across every layer of modern infrastructure. Here are the most practical scenarios you'll encounter as a developer or DevOps engineer:

Firewall Rules β€” CIDR-Based Access Control

# ── iptables (Linux) ─────────────────────────────────────────────────────────
# Allow SSH only from your office IP range (203.0.113.0/24)
iptables -A INPUT -p tcp --dport 22 -s 203.0.113.0/24 -j ACCEPT
iptables -A INPUT -p tcp --dport 22 -j DROP

# Allow all traffic from private networks, block public
iptables -A INPUT -s 10.0.0.0/8     -j ACCEPT
iptables -A INPUT -s 172.16.0.0/12  -j ACCEPT
iptables -A INPUT -s 192.168.0.0/16 -j ACCEPT
iptables -A INPUT -j DROP

# Allow HTTP/HTTPS from anywhere, restrict admin port to VPN
iptables -A INPUT -p tcp --dport 80  -j ACCEPT
iptables -A INPUT -p tcp --dport 443 -j ACCEPT
iptables -A INPUT -p tcp --dport 8080 -s 10.8.0.0/24 -j ACCEPT  # VPN subnet
iptables -A INPUT -p tcp --dport 8080 -j DROP

# ── nginx β€” restrict admin endpoints by CIDR ─────────────────────────────────
server {
    listen 443 ssl;
    server_name app.example.com;

    location / {
        proxy_pass http://app:3000;
    }

    location /admin {
        # Only allow from office IP range and VPN
        allow 203.0.113.0/24;  # Office
        allow 10.8.0.0/16;     # VPN
        allow 127.0.0.1;       # Localhost
        deny all;
        proxy_pass http://app:3000;
    }
}

# ── AWS Security Group rules ──────────────────────────────────────────────────
# Terraform: allow MySQL only from app subnet
resource "aws_security_group_rule" "mysql_from_app" {
  type              = "ingress"
  from_port         = 3306
  to_port           = 3306
  protocol          = "tcp"
  cidr_blocks       = ["10.0.11.0/24", "10.0.12.0/24", "10.0.13.0/24"]
  security_group_id = aws_security_group.database.id
}

# ── UFW (Uncomplicated Firewall) ──────────────────────────────────────────────
ufw allow from 192.168.1.0/24 to any port 22   # SSH from LAN
ufw allow from 10.0.0.0/8 to any port 5432      # PostgreSQL from private
ufw deny 22   # Block SSH from everywhere else
ufw enable

Docker Networking β€” Bridge, Overlay, and Custom Networks

# ── Docker default networks ───────────────────────────────────────────────────
# bridge (docker0): 172.17.0.0/16  β€” default for single-host containers
# host: shares host network (no isolation)
# none: no networking

# Inspect the default bridge
docker network inspect bridge
# "Subnet": "172.17.0.0/16"
# "Gateway": "172.17.0.1"
# Each container gets an IP from 172.17.0.2 onwards

# ── Create a custom bridge network with specific CIDR ─────────────────────────
docker network create   --driver bridge   --subnet 172.20.0.0/16   --ip-range 172.20.240.0/20   --gateway 172.20.0.1   myapp-network

# Assign a static IP to a container
docker run -d   --name mysql   --network myapp-network   --ip 172.20.240.10   mysql:8.0

docker run -d   --name app   --network myapp-network   --ip 172.20.240.11   myapp:latest

# ── docker-compose.yml: custom subnets ────────────────────────────────────────
version: '3.8'

services:
  app:
    image: myapp:latest
    networks:
      frontend:
        ipv4_address: 172.28.1.10
      backend:
        ipv4_address: 172.29.1.10

  db:
    image: postgres:15
    networks:
      backend:
        ipv4_address: 172.29.1.20

  nginx:
    image: nginx:alpine
    networks:
      frontend:
        ipv4_address: 172.28.1.5

networks:
  frontend:
    driver: bridge
    ipam:
      config:
        - subnet: 172.28.0.0/16
          ip_range: 172.28.1.0/24
          gateway: 172.28.0.1

  backend:
    driver: bridge
    ipam:
      config:
        - subnet: 172.29.0.0/16
          ip_range: 172.29.1.0/24
          gateway: 172.29.0.1

# ── Docker Swarm overlay networks ─────────────────────────────────────────────
# Default overlay for Swarm: 10.0.0.0/24 (ingress)
# Ingress network: 10.0.0.0/24 (load balancing)

docker network create   --driver overlay   --subnet 10.20.0.0/16   --gateway 10.20.0.1   swarm-overlay

# ── Check container IP addresses ──────────────────────────────────────────────
docker inspect --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' container_name

# List all Docker networks and their subnets
docker network ls --format '{{.Name}}' | while read net; do
  echo -n "$net: "
  docker network inspect "$net" --format '{{range .IPAM.Config}}{{.Subnet}}{{end}}'
done

Kubernetes Pod CIDR and Service CIDR

# ── Kubernetes CIDR ranges ───────────────────────────────────────────────────
# Pod CIDR:       10.244.0.0/16  (Flannel default)
# Service CIDR:   10.96.0.0/12   (ClusterIP services)
# Node gets /24:  10.244.0.0/24, 10.244.1.0/24, ...

# kubeadm init with custom CIDRs
kubeadm init   --pod-network-cidr=10.244.0.0/16   --service-cidr=10.96.0.0/12   --apiserver-advertise-address=192.168.1.10

# ── CNI plugin CIDR defaults ──────────────────────────────────────────────────
# Flannel:  10.244.0.0/16  (each node gets /24 = 254 pods max)
# Calico:   192.168.0.0/16 (configurable, supports BGP)
# Weave:    10.32.0.0/12
# Cilium:   10.0.0.0/8     (highly configurable)

# View pod CIDRs per node
kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.name}{"	"}{.spec.podCIDR}{"
"}{end}'
# node-1    10.244.0.0/24
# node-2    10.244.1.0/24
# node-3    10.244.2.0/24

# ── Service types and IPs ─────────────────────────────────────────────────────
# ClusterIP: virtual IP from service CIDR (10.96.0.0/12)
#   - Only reachable within the cluster
#   - kube-proxy programs iptables/IPVS rules

# NodePort: exposes on all nodes at static port (30000-32767)
# LoadBalancer: cloud provider assigns external IP

# Get all services and their ClusterIPs
kubectl get svc --all-namespaces -o wide

# ── NetworkPolicy: restrict pod-to-pod traffic with CIDR ─────────────────────
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: restrict-db-access
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: postgres
  policyTypes:
    - Ingress
  ingress:
    # Only allow from app pods in the same namespace
    - from:
        - podSelector:
            matchLabels:
              app: backend
        - namespaceSelector:
            matchLabels:
              name: production
      ports:
        - port: 5432

    # Also allow from monitoring subnet
    - from:
        - ipBlock:
            cidr: 10.0.20.0/24    # monitoring subnet
            except:
              - 10.0.20.100/32    # exclude specific IP
      ports:
        - port: 9187  # postgres_exporter

# ── Kubernetes IPVS vs iptables ───────────────────────────────────────────────
# iptables mode: O(n) rules, degrades with many services
# IPVS mode: O(1) hash table, better for large clusters

# Enable IPVS in kube-proxy
# In kube-proxy configmap:
mode: "ipvs"
ipvs:
  scheduler: "rr"  # round-robin

# Check current mode
kubectl logs -n kube-system kube-proxy-xxx | grep "Using"
# I0101 Using ipvs Proxier.

Try Our IP Subnet Calculator

Compute network address, broadcast, usable host range, subnet mask, wildcard mask, and more for any CIDR block β€” instantly, with no installation required.

Open IP Subnet Calculator β†’

Key Takeaways

  • CIDR notation: IP/prefix where prefix = count of network bits. /24 = 256 total, 254 usable hosts.
  • Host count formula: 2^(32 - prefix) - 2. The -2 removes network address and broadcast address.
  • Network address: all host bits = 0. Broadcast address: all host bits = 1. Neither can be assigned to a host.
  • Private ranges (RFC 1918): 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16. Not routable on the public internet.
  • VLSM: allocate largest subnet first, work down. Avoids wasting addresses by assigning right-sized blocks to each segment.
  • AWS reserves 5 IPs per subnet (not 2): network, router, DNS, future, broadcast. A /28 gives only 11 usable addresses.
  • Docker default: 172.17.0.0/16 for bridge. Use custom subnets in production to avoid conflicts with your VPC.
  • Kubernetes pod CIDR: 10.244.0.0/16 (Flannel). Each node gets a /24 = max 254 pods per node.
  • IPv6 /64: required for SLAAC. One /64 contains 2^64 β‰ˆ 18 quintillion addresses β€” always use /64 for LAN segments.
  • Python ipaddress and Go net handle all subnet math natively β€” no external dependencies needed.
𝕏 Twitterin LinkedIn
Was dit nuttig?

Blijf op de hoogte

Ontvang wekelijkse dev-tips en nieuwe tools.

Geen spam. Altijd opzegbaar.

Try These Related Tools

🌐IP Subnet Calculator

Related Articles

PEM Decoder: Decode SSL Certificates and Keys Online β€” Complete Guide

Decode and inspect PEM files, SSL certificates, and private keys. Complete guide with OpenSSL commands, Node.js tls module, Python cryptography library, certificate chains, mTLS, and Let's Encrypt.

CORS Tester: Fix CORS Errors and Configure Cross-Origin Requests β€” Complete Guide

Fix CORS errors and configure cross-origin requests. Complete guide covering CORS headers, preflight requests, Express/Next.js/Nginx/FastAPI configuration, credentials, debugging, and security.

HTML Escape / Unescape: Encode Special Characters Online β€” Complete Guide

Escape and unescape HTML, URLs, JSON, SQL, and shell strings. Complete guide covering XSS prevention, HTML entities, URL encoding, JSON escaping, SQL injection prevention, and regex escaping.