IP Addresses & Subnetting IPv4 Addressing, Network Masks, CIDR Notation

 

IP Addresses & Subnetting
Chapter 58 โ€” Part 4: IPv4 Addressing, Network Masks, CIDR Notation
๐Ÿ“š Theory
๐Ÿ’ป Code Examples
โ“ Interview Q&A

Every device on a TCP/IP network must have a unique IP address. But an IP address is not just a flat number โ€” it is split into two parts: a network ID (which network is the device on?) and a host ID (which device on that network?). Understanding this split is the foundation of subnetting, routing, and socket programming with bind() and connect().

Key Concepts
IPv4 Address Dotted-Decimal Notation Network ID Host ID Network Mask Subnet Mask CIDR Notation inet_pton inet_ntop htonl / ntohl Network Byte Order

๐Ÿญ Structure of an IPv4 Address

An IPv4 address is 32 bits (4 bytes) long. It is made up of two logical parts:

  • Network ID โ€” identifies which network the host is on (like a street name)
  • Host ID โ€” identifies the specific host on that network (like a house number)

IPv4 Address: 204.152.189.116

Byte 1
Byte 2
Byte 3
Byte 4
204
152
189
116
Network ID (24 bits = /24)
Host ID (8 bits)

255
255
255
0
Network Mask = 255.255.255.0 (or /24 in CIDR notation)

In human-readable form, IPv4 addresses are written in dotted-decimal notation: each of the 4 bytes is written as a decimal number (0โ€“255), separated by dots. Example: 204.152.189.116.

๐Ÿณ๏ธ Network Mask โ€” Separating Network ID from Host ID

When an organization gets assigned a block of IPv4 addresses, it receives two things:

  1. A network address (e.g., 204.152.189.0)
  2. A network mask (e.g., 255.255.255.0)

The network mask in binary has a sequence of 1s on the left (indicating the network ID portion) followed by a sequence of 0s on the right (indicating the host ID portion available for the organization to assign).

Item
Byte 1
Byte 2
Byte 3
Byte 4
IP Address
11001100
10011000
10111101
01110100
Decimal
204
152
189
116
Mask (binary)
11111111
11111111
11111111
00000000
Mask (decimal)
255
255
255
0
Role
Network ID (24 bits โ€” assigned by ISP)
Host ID (8 bits โ€” assigned by org)

To find the Network ID from an IP address, you AND the address with the mask:

/* Computing network address using bitwise AND */
uint32_t ip   = 0xCC9CBD74; /* 204.152.189.116 */
uint32_t mask = 0xFFFFFF00; /* 255.255.255.0   */

uint32_t network = ip & mask;
/* Result: 0xCC9CBD00 = 204.152.189.0 โ€” the network address */

/* The host part: */
uint32_t host = ip & (~mask);
/* Result: 0x00000074 = 116 โ€” the host number */

๐Ÿ“ CIDR Notation โ€” The /24 Shorthand

Writing out a full mask like 255.255.255.0 every time is verbose. The CIDR (Classless Inter-Domain Routing) notation solves this by appending a slash and the number of 1 bits in the mask:

204.152.189.0/24
Network address = 204.152.189.0 ย |ย  Network mask = 255.255.255.0 ย |ย  Host bits = 8
Valid host addresses: 204.152.189.1 through 204.152.189.254 ย (256 โˆ’ 2 = 254 usable hosts)

The /24 tells you that 24 of the 32 bits are the network ID, leaving 8 bits for host IDs.

CIDR
Subnet Mask
Network bits
Host bits
Usable hosts
Typical use
/8
255.0.0.0
8
24
16,777,214
Large ISPs, Class A
/16
255.255.0.0
16
16
65,534
Universities, Class B
/24
255.255.255.0
24
8
254
Small orgs, Class C
/30
255.255.255.252
30
2
2
Point-to-point links
/32
255.255.255.255
32
0
1
Single host route
๐Ÿ’ก Note on host count formula: Usable hosts = 2^(host bits) โˆ’ 2. We subtract 2 because the network address (all host bits = 0) and the broadcast address (all host bits = 1) are reserved and cannot be assigned to individual hosts.

๐Ÿณ๏ธ Special IPv4 Addresses Worth Knowing
127.0.0.1
Loopback โ€” always refers to the local machine. A socket connecting to 127.0.0.1 never leaves the kernel. Used for local testing.
0.0.0.0
Wildcard / Any โ€” when used with bind(), means “listen on all interfaces.” When used as a source address, means “any local address.”
255.255.255.255
Limited Broadcast โ€” sends to all hosts on the local network. Routers do not forward this.
192.168.x.x
10.x.x.x
172.16-31.x.x
Private / RFC 1918 โ€” not routable on the public internet. Used in home and corporate networks. NAT translates these to public IPs.

๐Ÿ’ป Code: IP Address Manipulation in C (inet_pton, inet_ntop, byte order)

When you write socket code, you constantly convert between human-readable IP strings and binary network-order integers. The key functions are inet_pton() (text to binary) and inet_ntop() (binary to text). You also need byte-order conversion.

/* ip_address_demo.c
 * Demonstrates IP address conversion and subnet calculations
 * in C socket programming.
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>   /* inet_pton, inet_ntop, htonl, ntohl */
#include <sys/socket.h>
#include <netinet/in.h>

/* ---- Helper: print an IPv4 address in dotted decimal ---- */
void print_ip(const char *label, uint32_t addr_host_order)
{
    /* inet_ntop works with network byte order, so we convert */
    uint32_t net = htonl(addr_host_order);
    char str[INET_ADDRSTRLEN];
    inet_ntop(AF_INET, &net, str, sizeof(str));
    printf("%-20s: %s (0x%08X)\n", label, str, addr_host_order);
}

int main(void)
{
    /* ===== 1. Convert human-readable string to binary ===== */
    const char *ip_str = "204.152.189.116";
    struct in_addr bin;

    /*
     * inet_pton: presentation (text) to network (binary)
     * Stores result in NETWORK byte order (big-endian)
     * Returns 1 on success, 0 if invalid format, -1 on error
     */
    if (inet_pton(AF_INET, ip_str, &bin) != 1) {
        fprintf(stderr, "Invalid IPv4 address: %s\n", ip_str);
        exit(EXIT_FAILURE);
    }
    printf("String: %s\n", ip_str);
    printf("Binary (network order, hex): 0x%08X\n\n", ntohl(bin.s_addr));

    /* ===== 2. Convert binary back to string ===== */
    char out_str[INET_ADDRSTRLEN];
    inet_ntop(AF_INET, &bin, out_str, sizeof(out_str));
    printf("Back to string: %s\n\n", out_str);

    /* ===== 3. Byte order: network (big-endian) vs host order ===== */
    /*
     * Network byte order is always big-endian.
     * x86/ARM Linux is little-endian (host order).
     * Always convert when filling sockaddr structures:
     *   htons()  = host to network short (16-bit, for port numbers)
     *   htonl()  = host to network long  (32-bit, for IP addresses)
     *   ntohs()  = network to host short
     *   ntohl()  = network to host long
     */
    uint16_t port = 8080;
    printf("Port in host order:   %u (0x%04X)\n", port, port);
    printf("Port in network order:%u (0x%04X)\n\n",
           htons(port), htons(port));

    /* ===== 4. Subnet calculation using network mask ===== */
    uint32_t ip_addr   = ntohl(bin.s_addr);   /* convert to host order for arithmetic */
    uint8_t  cidr      = 24;                   /* /24 means 24 bits of network ID */
    uint32_t mask      = (cidr == 0) ? 0 : (~0u << (32 - cidr));

    uint32_t network   = ip_addr & mask;       /* AND gives network address */
    uint32_t broadcast = network | (~mask);     /* OR inverted mask = broadcast */
    uint32_t first_host= network + 1;
    uint32_t last_host = broadcast - 1;
    uint32_t num_hosts = (~mask) - 1;          /* 2^host_bits - 2 */

    printf("=== Subnet Calculation for %s/%u ===\n", ip_str, cidr);
    print_ip("Network address", network);
    print_ip("Subnet mask", mask);
    print_ip("Broadcast address", broadcast);
    print_ip("First host", first_host);
    print_ip("Last host", last_host);
    printf("%-20s: %u\n", "Usable hosts", num_hosts);

    /* ===== 5. Check if an IP is in the same subnet ===== */
    const char *test_ip_str = "204.152.189.200";
    struct in_addr test_bin;
    inet_pton(AF_INET, test_ip_str, &test_bin);
    uint32_t test_addr = ntohl(test_bin.s_addr);

    printf("\nIs %s in the same /24 subnet? %s\n",
           test_ip_str,
           (test_addr & mask) == network ? "YES" : "NO");

    return 0;
}
/* Compile and run:
   gcc -o ip_demo ip_address_demo.c
   ./ip_demo

   Expected output:
   String: 204.152.189.116
   Binary (network order, hex): 0xCC9CBD74

   Back to string: 204.152.189.116

   Port in host order:   8080 (0x1F90)
   Port in network order:36895 (0x901F)   โ† bytes are swapped!

   === Subnet Calculation for 204.152.189.116/24 ===
   Network address     : 204.152.189.0 (0xCC9CBD00)
   Subnet mask         : 255.255.255.0 (0xFFFFFF00)
   Broadcast address   : 204.152.189.255 (0xCC9CBDFF)
   First host          : 204.152.189.1 (0xCC9CBD01)
   Last host           : 204.152.189.254 (0xCC9CBDFE)
   Usable hosts        : 254

   Is 204.152.189.200 in the same /24 subnet? YES
*/

๐Ÿ’ป Code: Using IP Addresses in Socket Programming

In practice, IP addresses appear in socket programming whenever you call bind() (server) or connect() (client). Here is the pattern you will use constantly:

/* socket_bind_connect.c
 * Shows how IP addresses are used in bind() and connect()
 */
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

void server_example(void)
{
    int server_fd = socket(AF_INET, SOCK_STREAM, 0);

    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(addr));

    addr.sin_family = AF_INET;
    addr.sin_port   = htons(8080);      /* always use htons for port */

    /*
     * Three choices for sin_addr:
     *
     * 1. INADDR_ANY (0.0.0.0) โ€” bind to ALL network interfaces
     *    This is the most common choice for a server.
     */
    addr.sin_addr.s_addr = htonl(INADDR_ANY);

    /*
     * 2. Specific IP โ€” bind only to one interface
     *    inet_pton(AF_INET, "192.168.1.10", &addr.sin_addr);
     *
     * 3. Loopback โ€” only accept local connections
     *    addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); // 127.0.0.1
     */

    if (bind(server_fd, (struct sockaddr *)&addr, sizeof(addr)) == -1) {
        perror("bind");
    } else {
        printf("Server bound to 0.0.0.0:8080 (all interfaces)\n");
    }

    close(server_fd);
}

void client_example(const char *server_ip, uint16_t port)
{
    int client_fd = socket(AF_INET, SOCK_STREAM, 0);

    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port   = htons(port);

    /* inet_pton converts "192.168.1.10" string to 32-bit binary in network order */
    if (inet_pton(AF_INET, server_ip, &server_addr.sin_addr) != 1) {
        fprintf(stderr, "Invalid server IP: %s\n", server_ip);
        close(client_fd);
        return;
    }

    printf("Connecting to %s:%u ...\n", server_ip, port);
    if (connect(client_fd, (struct sockaddr *)&server_addr,
                sizeof(server_addr)) == -1) {
        perror("connect");
    } else {
        printf("Connected!\n");

        /* After connect, retrieve the local address the kernel assigned */
        struct sockaddr_in local;
        socklen_t len = sizeof(local);
        getsockname(client_fd, (struct sockaddr *)&local, &len);
        char local_ip[INET_ADDRSTRLEN];
        inet_ntop(AF_INET, &local.sin_addr, local_ip, sizeof(local_ip));
        printf("Local endpoint: %s:%u\n", local_ip, ntohs(local.sin_port));
    }

    close(client_fd);
}

int main(void)
{
    server_example();
    client_example("127.0.0.1", 8080);
    return 0;
}

๐Ÿ”„ IPv6 Addresses โ€” A Brief Overview

IPv6 addresses are 128 bits, written as 8 groups of 4 hexadecimal digits separated by colons:

2001:0db8:85a3:0000:0000:8a2e:0370:7334

Two shortening rules apply:

  • Leading zeros within a group can be omitted: 0db8 โ†’ db8
  • One run of consecutive all-zero groups can be replaced by ::
2001:db8:85a3::8a2e:370:7334 ย ย  (shortened form of the above)

Important IPv6 special addresses:

  • ::1 โ€” loopback (equivalent to 127.0.0.1 in IPv4)
  • :: โ€” any address (equivalent to 0.0.0.0 in IPv4)
  • fe80::/10 โ€” link-local addresses (not routed beyond local link)
/* IPv6 socket example using AF_INET6 */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>

int main(void)
{
    /* AF_INET6 = IPv6, SOCK_STREAM = TCP */
    int sockfd = socket(AF_INET6, SOCK_STREAM, 0);
    if (sockfd == -1) { perror("socket"); return 1; }

    struct sockaddr_in6 addr6;
    memset(&addr6, 0, sizeof(addr6));
    addr6.sin6_family = AF_INET6;
    addr6.sin6_port   = htons(8080);

    /* Bind to :: (all IPv6 interfaces) */
    addr6.sin6_addr = in6addr_any;  /* equivalent of INADDR_ANY for IPv6 */

    if (bind(sockfd, (struct sockaddr *)&addr6, sizeof(addr6)) == 0)
        printf("Bound to [::]:8080 (all IPv6 interfaces)\n");

    /* Convert IPv6 string to binary */
    struct in6_addr target;
    const char *ipv6_str = "2001:db8::1";
    if (inet_pton(AF_INET6, ipv6_str, &target) == 1)
        printf("Parsed %s successfully\n", ipv6_str);

    /* Convert binary back to string โ€” needs INET6_ADDRSTRLEN (46 chars) */
    char buf[INET6_ADDRSTRLEN];
    inet_ntop(AF_INET6, &target, buf, sizeof(buf));
    printf("Back to string: %s\n", buf);

    close(sockfd);
    return 0;
}

โ“ Interview Questions & Answers
Q1: What are the two parts of an IPv4 address?
A: Network ID โ€” identifies which network the host is on. Host ID โ€” identifies the specific host within that network. The network mask determines how many bits belong to each part.
Q2: What is CIDR notation and what does /24 mean?
A: CIDR (Classless Inter-Domain Routing) notation appends a slash and the number of 1-bits in the network mask to an IP address. /24 means 24 bits are the network ID, leaving 8 bits for host IDs (254 usable hosts). It is equivalent to the subnet mask 255.255.255.0.
Q3: How do you find the network address from an IP address and mask?
A: Perform a bitwise AND of the IP address and the subnet mask. Example: 204.152.189.116 AND 255.255.255.0 = 204.152.189.0. This gives the network address. The broadcast address is obtained by OR-ing the network address with the inverted mask.
Q4: What does INADDR_ANY (0.0.0.0) mean in a server’s bind() call?
A: It tells the kernel to accept incoming connections on all available network interfaces. A server bound to INADDR_ANY:8080 will accept connections arriving on any of the machine’s IP addresses (WiFi, Ethernet, loopback, etc.). If you bind to a specific IP instead, the server only accepts connections on that interface.
Q5: Why must you use htons()/htonl() when setting port and IP in sockaddr?
A: Network protocols use big-endian (most significant byte first) byte order. x86 and ARM processors use little-endian. Without conversion, the bytes are in the wrong order in the packet. htons() (host to network short) converts 16-bit port numbers; htonl() converts 32-bit IP addresses. Forgetting this is a classic bug causing silent connection failures.
Q6: What is the difference between inet_aton(), inet_addr(), and inet_pton()?
A: inet_aton() and inet_addr() are older IPv4-only functions. inet_addr() returns INADDR_NONE (0xFFFFFFFF) on error, which clashes with the valid broadcast address 255.255.255.255. inet_pton() (presentation to network) is the modern replacement โ€” it supports both IPv4 and IPv6, and returns 1 on success, 0 on invalid format, -1 on error. Always prefer inet_pton().
Q7: Why are there reserved/private IP ranges (RFC 1918)?
A: Because of IPv4 address exhaustion, private IP ranges (10.x.x.x, 172.16โ€“31.x.x, 192.168.x.x) were defined for internal/private networks. These addresses are not routed on the public internet, so many private networks can reuse the same addresses without conflict. NAT (Network Address Translation) at the router translates private IPs to a single public IP when communicating with the internet.
Q8: How many usable hosts does a /28 subnet have?
A: /28 means 28 network bits and 4 host bits. Usable hosts = 2^4 โˆ’ 2 = 14. The mask is 255.255.255.240. Common formula: usable = 2^(32 โˆ’ prefix_length) โˆ’ 2.

Chapter 58 Series Complete โ€” You Now Know TCP/IP Fundamentals!

โ† Part 1: TCP/IP Layers โ† Part 3: IP Fragmentation Next Chapter โ†’

Leave a Reply

Your email address will not be published. Required fields are marked *