Back to Technology

Complete Protocols Master Part 3: Network Layer & IP

January 31, 2026 Wasil Zafar 38 min read

Master the Network Layer where packets find their path across networks. Learn IPv4 and IPv6 addressing, subnetting, CIDR notation, ICMP, NAT, and the routing protocols that make the global internet possible.

Table of Contents

  1. Introduction
  2. IPv4 Addressing
  3. Subnetting & CIDR
  4. IPv6 Addressing
  5. IP Packet Structure
  6. ICMP Protocol
  7. Routing Fundamentals
  8. NAT & PAT
  9. Hands-On Exercises
  10. Summary & Next Steps

Introduction: The Network Layer

In Part 2, we explored how data moves within local networks at Layers 1 and 2. Now we ascend to Layer 3—the Network Layer—where the magic of global connectivity happens. This is where:

  • Logical addresses (IP) replace physical addresses (MAC)
  • Routers make forwarding decisions across networks
  • Packets find paths across the globe
  • The Internet becomes possible
Series Context: This is Part 3 of 20 in the Complete Protocols Master series. We're building up from physical layers to application protocols.
Layer 3 Network Layer

Network Layer Responsibilities

  • Logical Addressing: IP addresses identify devices globally
  • Routing: Determining the best path for packets
  • Packet Forwarding: Moving packets hop-by-hop toward destination
  • Fragmentation: Breaking large packets to fit link MTU
  • Error Reporting: ICMP for network diagnostics

Key Protocols: IP (IPv4, IPv6), ICMP, IGMP, IPsec

Key Devices: Routers, Layer 3 switches

MAC vs IP: MAC addresses work on local networks (Layer 2). IP addresses work globally (Layer 3). When a packet crosses a router, the MAC addresses change at each hop, but the IP addresses stay the same end-to-end.

IPv4 Addressing

Internet Protocol version 4 (IPv4) has been the backbone of the internet since 1983. Despite IPv6's availability, IPv4 still carries the majority of internet traffic.

IPv4

IPv4 Address Structure

An IPv4 address is a 32-bit number, typically written in dotted decimal notation:

IPv4 Address: 192.168.1.100

Binary representation:
192      .168      .1        .100
11000000 .10101000 .00000001 .01100100

Each octet: 8 bits (0-255)
Total: 32 bits = 4 bytes
Maximum addresses: 2^32 = 4,294,967,296 (~4.3 billion)
# Understanding IPv4 addresses in Python
import ipaddress

# Create an IPv4 address object
ip = ipaddress.IPv4Address('192.168.1.100')

print(f"Address: {ip}")
print(f"Integer value: {int(ip)}")
print(f"Binary: {bin(int(ip))}")
print(f"Packed bytes: {ip.packed}")
print(f"Is private: {ip.is_private}")
print(f"Is global: {ip.is_global}")
print(f"Is loopback: {ip.is_loopback}")
print(f"Is multicast: {ip.is_multicast}")

# Convert between formats
ip_from_int = ipaddress.IPv4Address(3232235876)
print(f"\nFrom integer 3232235876: {ip_from_int}")

# Binary to decimal conversion for each octet
def ip_to_binary(ip_str):
    """Convert IP address to binary representation"""
    octets = ip_str.split('.')
    binary = [format(int(octet), '08b') for octet in octets]
    return '.'.join(binary)

print(f"\n192.168.1.100 in binary: {ip_to_binary('192.168.1.100')}")

IPv4 Address Classes (Historical)

Originally, IPv4 addresses were divided into classes. While classful addressing is obsolete, understanding it helps comprehend legacy systems and network fundamentals:

Historical Reference

Classful Address Ranges

Class First Octet Range Default Mask Networks Hosts/Network
A 0xxxxxxx 1.0.0.0 - 126.255.255.255 /8 (255.0.0.0) 126 16,777,214
B 10xxxxxx 128.0.0.0 - 191.255.255.255 /16 (255.255.0.0) 16,384 65,534
C 110xxxxx 192.0.0.0 - 223.255.255.255 /24 (255.255.255.0) 2,097,152 254
D 1110xxxx 224.0.0.0 - 239.255.255.255 N/A Multicast
E 1111xxxx 240.0.0.0 - 255.255.255.255 N/A Reserved/Experimental
Note: Classful addressing wasted IP addresses massively. A Class A network gave you 16 million hosts even if you needed 1,000. Modern networks use CIDR (Classless Inter-Domain Routing) for flexible address allocation.

Private and Special Addresses

RFC 1918

Private Address Ranges

These addresses are not routable on the public internet and can be reused by any organization:

Range CIDR Addresses Typical Use
10.0.0.0 - 10.255.255.255 10.0.0.0/8 16,777,216 Large enterprises, cloud VPCs
172.16.0.0 - 172.31.255.255 172.16.0.0/12 1,048,576 Medium organizations
192.168.0.0 - 192.168.255.255 192.168.0.0/16 65,536 Home networks, small offices
Special Addresses

Other Reserved Addresses

Range Purpose
127.0.0.0/8 Loopback - localhost (usually 127.0.0.1)
169.254.0.0/16 Link-Local - Auto-assigned when DHCP fails (APIPA)
0.0.0.0/8 This Network - Used during boot before IP assigned
255.255.255.255 Limited Broadcast - Local network broadcast
100.64.0.0/10 Carrier-Grade NAT - ISP shared address space
192.0.2.0/24 Documentation - TEST-NET-1 for examples
203.0.113.0/24 Documentation - TEST-NET-3 for examples

Subnetting & CIDR

Subnetting divides a network into smaller, manageable pieces. CIDR (Classless Inter-Domain Routing) provides flexible allocation by specifying exactly how many bits identify the network.

Subnet Mask

Understanding Subnet Masks

A subnet mask separates the network portion from the host portion of an IP address:

IP Address:   192.168.1.100
Subnet Mask:  255.255.255.0

Binary:
IP:           11000000.10101000.00000001.01100100
Mask:         11111111.11111111.11111111.00000000
              |______Network______|___Host__|

Network bits (1s): Identify the network
Host bits (0s):    Identify devices within the network

Network Address:  192.168.1.0   (all host bits = 0)
Broadcast:        192.168.1.255 (all host bits = 1)
Usable Hosts:     192.168.1.1 - 192.168.1.254 (254 hosts)

CIDR Notation

CIDR notation uses a slash followed by the number of network bits:

CIDR Reference

Common CIDR Blocks

CIDR Subnet Mask Addresses Usable Hosts
/8 255.0.0.0 16,777,216 16,777,214
/16 255.255.0.0 65,536 65,534
/20 255.255.240.0 4,096 4,094
/24 255.255.255.0 256 254
/25 255.255.255.128 128 126
/26 255.255.255.192 64 62
/27 255.255.255.224 32 30
/28 255.255.255.240 16 14
/29 255.255.255.248 8 6
/30 255.255.255.252 4 2
/31 255.255.255.254 2 2*
/32 255.255.255.255 1 1 (host route)

* /31 is special - used for point-to-point links (RFC 3021), no network/broadcast addresses needed

Subnet Calculations

# Comprehensive subnet calculator in Python
import ipaddress

def analyze_network(network_str):
    """
    Analyze an IPv4 network and display all relevant information
    """
    network = ipaddress.IPv4Network(network_str, strict=False)
    
    print(f"Network Analysis: {network_str}")
    print("=" * 50)
    
    # Basic info
    print(f"Network Address:    {network.network_address}")
    print(f"Broadcast Address:  {network.broadcast_address}")
    print(f"Subnet Mask:        {network.netmask}")
    print(f"Wildcard Mask:      {network.hostmask}")
    print(f"CIDR Notation:      /{network.prefixlen}")
    
    # Address counts
    print(f"\nTotal Addresses:    {network.num_addresses}")
    print(f"Usable Hosts:       {network.num_addresses - 2 if network.num_addresses > 2 else network.num_addresses}")
    
    # Host range
    hosts = list(network.hosts())
    if hosts:
        print(f"First Usable Host:  {hosts[0]}")
        print(f"Last Usable Host:   {hosts[-1]}")
    
    # Binary representation
    print(f"\nBinary Network:     {bin(int(network.network_address))[2:].zfill(32)}")
    print(f"Binary Mask:        {bin(int(network.netmask))[2:].zfill(32)}")
    
    return network

# Example analysis
print("EXAMPLE 1: Standard /24 Network")
print("-" * 50)
analyze_network("192.168.1.0/24")

print("\n\nEXAMPLE 2: /27 Subnet")
print("-" * 50)
analyze_network("10.0.0.0/27")

print("\n\nEXAMPLE 3: Check if IP is in network")
print("-" * 50)
network = ipaddress.IPv4Network("192.168.1.0/24")
test_ips = ["192.168.1.50", "192.168.2.50", "192.168.1.255"]

for ip_str in test_ips:
    ip = ipaddress.IPv4Address(ip_str)
    in_network = ip in network
    print(f"{ip_str} in 192.168.1.0/24: {in_network}")
# Subnet division example
import ipaddress

def divide_network(network_str, new_prefix):
    """
    Divide a network into smaller subnets
    """
    network = ipaddress.IPv4Network(network_str)
    
    if new_prefix <= network.prefixlen:
        raise ValueError(f"New prefix must be larger than {network.prefixlen}")
    
    subnets = list(network.subnets(new_prefix=new_prefix))
    
    print(f"Dividing {network_str} into /{new_prefix} subnets:")
    print("=" * 60)
    
    for i, subnet in enumerate(subnets):
        hosts = list(subnet.hosts())
        first_host = hosts[0] if hosts else "N/A"
        last_host = hosts[-1] if hosts else "N/A"
        
        print(f"Subnet {i+1}: {subnet}")
        print(f"  Network:   {subnet.network_address}")
        print(f"  Broadcast: {subnet.broadcast_address}")
        print(f"  Range:     {first_host} - {last_host}")
        print(f"  Hosts:     {len(hosts)}")
        print()
    
    return subnets

# Divide a /24 into four /26 subnets
print("Scenario: Divide 192.168.1.0/24 into 4 departments")
print("-" * 60)
divide_network("192.168.1.0/24", 26)
Quick Math:
  • Hosts per subnet = 2(32 - prefix) - 2
  • /24 = 28 - 2 = 254 hosts
  • /26 = 26 - 2 = 62 hosts
  • /28 = 24 - 2 = 14 hosts
  • Subnets when dividing = 2(new_prefix - old_prefix)

IPv6 Addressing

IPv6 was designed to solve IPv4 address exhaustion. With 128-bit addresses, it provides 340 undecillion addresses—enough for every atom on Earth's surface!

Why IPv6?

IPv4 vs IPv6 Comparison

Feature IPv4 IPv6
Address Size 32 bits (4 bytes) 128 bits (16 bytes)
Address Space ~4.3 billion ~340 undecillion (3.4 × 1038)
Notation Dotted decimal (192.168.1.1) Hexadecimal (2001:db8::1)
Header Size 20-60 bytes (variable) 40 bytes (fixed)
Fragmentation Routers and hosts Source only (no router fragmentation)
Broadcast Yes No (uses multicast instead)
IPsec Optional Mandatory support
Auto-configuration DHCP required SLAAC built-in

IPv6 Address Format

Format Rules

IPv6 Address Structure

Full IPv6 Address:
2001:0db8:85a3:0000:0000:8a2e:0370:7334

Structure:
- 8 groups of 4 hexadecimal digits
- Separated by colons
- Each group = 16 bits
- Total = 128 bits

Compression Rules:

1. Leading zeros in a group can be omitted:
   2001:0db8:0001:0000:0000:0000:0000:0001
   → 2001:db8:1:0:0:0:0:1

2. One sequence of consecutive all-zero groups can be replaced with ::
   2001:db8:1:0:0:0:0:1
   → 2001:db8:1::1

3. :: can only be used ONCE in an address
   (otherwise ambiguous how many zeros)

Examples:
   ::1                     = Loopback (like 127.0.0.1)
   ::                      = All zeros (unspecified)
   2001:db8::              = 2001:db8:0:0:0:0:0:0
   fe80::1                 = Link-local address
   2001:db8:85a3::8a2e:370:7334 = Typical compressed form
# Working with IPv6 in Python
import ipaddress

# Create IPv6 address objects
full_addr = ipaddress.IPv6Address('2001:0db8:85a3:0000:0000:8a2e:0370:7334')
compressed = ipaddress.IPv6Address('2001:db8:85a3::8a2e:370:7334')
loopback = ipaddress.IPv6Address('::1')

print("IPv6 Address Analysis")
print("=" * 60)

for addr in [full_addr, compressed, loopback]:
    print(f"\nAddress: {addr}")
    print(f"  Compressed:  {addr.compressed}")
    print(f"  Exploded:    {addr.exploded}")
    print(f"  Is loopback: {addr.is_loopback}")
    print(f"  Is link-local: {addr.is_link_local}")
    print(f"  Is global:   {addr.is_global}")
    print(f"  Is private:  {addr.is_private}")

# IPv6 network analysis
print("\n\nIPv6 Network Analysis")
print("=" * 60)
network = ipaddress.IPv6Network('2001:db8::/32')
print(f"Network:     {network}")
print(f"Prefix:      /{network.prefixlen}")
print(f"Num addresses: {network.num_addresses}")
print(f"First addr:  {network.network_address}")
print(f"Last addr:   {network.broadcast_address}")

# Common IPv6 prefix sizes
print("\n\nCommon IPv6 Allocations")
print("=" * 60)
allocations = [
    ("/32", "Regional Internet Registry (RIR) to ISP"),
    ("/48", "ISP to Customer site"),
    ("/56", "ISP to Residential customer"),
    ("/64", "Single subnet (standard)"),
    ("/128", "Single host (loopback, etc.)")
]
for prefix, description in allocations:
    print(f"{prefix:8} - {description}")

IPv6 Address Types

Address Types

IPv6 Address Categories

Type Prefix Description
Global Unicast 2000::/3 Routable on the internet (like public IPv4)
Link-Local fe80::/10 Auto-configured, not routed (local network only)
Unique Local (ULA) fc00::/7 (fd00::/8 used) Private addresses (like RFC 1918 in IPv4)
Multicast ff00::/8 One-to-many communication
Loopback ::1/128 Localhost (like 127.0.0.1)
Unspecified ::/128 No address assigned yet
Documentation 2001:db8::/32 For examples and documentation
IPv6 in Practice: Every IPv6 interface typically has multiple addresses:
  • Link-local (fe80::) - Always present, auto-configured
  • Global unicast (2xxx::) - For internet communication
  • Privacy address - Temporary, rotates for privacy (SLAAC)

IP Packet Structure

Understanding the IP header helps you debug network issues, analyze packet captures, and understand how routing decisions are made.

IPv4 Header

IPv4 Header Format (20-60 bytes)

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|Version|  IHL  |    DSCP   |ECN|         Total Length          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|         Identification        |Flags|     Fragment Offset     |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|  Time to Live |    Protocol   |        Header Checksum        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                       Source IP Address                       |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                    Destination IP Address                     |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                    Options (if IHL > 5)                       |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Key Fields:
- Version (4 bits): Always 4 for IPv4
- IHL (4 bits): Header length in 32-bit words (min 5 = 20 bytes)
- TTL (8 bits): Decremented by each router, prevents infinite loops
- Protocol (8 bits): 1=ICMP, 6=TCP, 17=UDP, 47=GRE, 50=ESP
- Source/Dest IP: 32-bit addresses
# Parse IPv4 header in Python
import struct

def parse_ipv4_header(data):
    """Parse IPv4 packet header"""
    # First 20 bytes are the minimum IPv4 header
    # Unpack first byte separately to extract version and IHL
    version_ihl = data[0]
    version = version_ihl >> 4
    ihl = version_ihl & 0x0F
    header_length = ihl * 4  # In bytes
    
    # Unpack rest of the header
    dscp_ecn, total_length = struct.unpack('!BH', data[1:4])
    identification, flags_frag = struct.unpack('!HH', data[4:8])
    
    flags = flags_frag >> 13
    fragment_offset = flags_frag & 0x1FFF
    
    ttl, protocol, checksum = struct.unpack('!BBH', data[8:12])
    src_ip = '.'.join(str(b) for b in data[12:16])
    dst_ip = '.'.join(str(b) for b in data[16:20])
    
    protocols = {1: 'ICMP', 6: 'TCP', 17: 'UDP', 47: 'GRE', 50: 'ESP'}
    
    return {
        'version': version,
        'header_length': header_length,
        'dscp': dscp_ecn >> 2,
        'ecn': dscp_ecn & 0x03,
        'total_length': total_length,
        'identification': identification,
        'flags': {
            'reserved': (flags >> 2) & 1,
            'dont_fragment': (flags >> 1) & 1,
            'more_fragments': flags & 1
        },
        'fragment_offset': fragment_offset,
        'ttl': ttl,
        'protocol': protocols.get(protocol, f'Unknown ({protocol})'),
        'protocol_num': protocol,
        'checksum': hex(checksum),
        'source': src_ip,
        'destination': dst_ip
    }

# Example: Parse a sample IPv4 packet header
sample_header = bytes([
    0x45,                   # Version (4) + IHL (5)
    0x00,                   # DSCP + ECN
    0x00, 0x3c,             # Total length (60)
    0x1c, 0x46,             # Identification
    0x40, 0x00,             # Flags (DF) + Fragment offset
    0x40,                   # TTL (64)
    0x06,                   # Protocol (TCP)
    0xb1, 0xe6,             # Checksum
    0xc0, 0xa8, 0x01, 0x64, # Source: 192.168.1.100
    0x8e, 0xfa, 0xc4, 0xe4  # Dest: 142.250.196.228 (Google)
])

parsed = parse_ipv4_header(sample_header)
print("IPv4 Header Analysis")
print("=" * 50)
for key, value in parsed.items():
    if isinstance(value, dict):
        print(f"{key}:")
        for k, v in value.items():
            print(f"  {k}: {v}")
    else:
        print(f"{key}: {value}")

IPv6 Header

IPv6 Header

IPv6 Header Format (Fixed 40 bytes)

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|Version| Traffic Class |           Flow Label                  |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|         Payload Length        |  Next Header  |   Hop Limit   |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                                                               |
+                                                               +
|                                                               |
+                         Source Address                        +
|                                                               |
+                          (128 bits)                           +
|                                                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                                                               |
+                                                               +
|                                                               |
+                      Destination Address                      +
|                                                               |
+                          (128 bits)                           +
|                                                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Key Differences from IPv4:
- Fixed 40-byte header (simpler for routers)
- No checksum (handled by upper layers)
- No fragmentation fields (source handles it)
- Flow Label for QoS
- Extension headers for optional features

IP Fragmentation

MTU & Fragmentation

Maximum Transmission Unit

MTU is the largest packet size a network link can carry. When a packet exceeds the MTU, it must be fragmented:

  • Ethernet MTU: 1500 bytes (standard)
  • Jumbo frames: 9000 bytes (data centers)
  • Internet minimum: 576 bytes (IPv4), 1280 bytes (IPv6)

IPv4: Any router can fragment packets

IPv6: Only the source can fragment (Path MTU Discovery)

# Check MTU on your system

# Linux
ip link show | grep mtu
# or
cat /sys/class/net/eth0/mtu

# Windows
netsh interface ipv4 show subinterfaces

# macOS
networksetup -getMTU en0

# Test path MTU to a destination (Linux)
tracepath google.com

# Ping with specific size (don't fragment)
# Linux
ping -M do -s 1472 google.com   # 1472 + 28 (headers) = 1500

# Windows  
ping -f -l 1472 google.com

# macOS
ping -D -s 1472 google.com
Fragmentation Problems: Fragmentation hurts performance and can cause issues:
  • All fragments must arrive for reassembly
  • Loss of one fragment loses entire packet
  • Some firewalls block fragments (security risk)
  • TCP uses Path MTU Discovery to avoid fragmentation

ICMP: Internet Control Message Protocol

ICMP provides error reporting and diagnostic functions for IP. It's the protocol behind ping and traceroute.

ICMP Messages

Common ICMP Message Types

Type Name Description
0 Echo Reply Response to ping (Type 8)
3 Destination Unreachable Packet couldn't be delivered
4 Source Quench Congestion control (deprecated)
5 Redirect Better route available
8 Echo Request Ping request
11 Time Exceeded TTL expired (used by traceroute)
12 Parameter Problem Bad IP header

Ping and Traceroute

# Ping - Test connectivity using ICMP Echo Request/Reply

# Basic ping
ping google.com

# Ping with count
ping -c 4 google.com           # Linux/macOS
ping -n 4 google.com           # Windows

# Ping with specific TTL
ping -t 10 google.com          # Linux
ping -i 10 google.com          # macOS

# Continuous ping with timestamp
ping -D google.com             # Linux (show timestamp)


# Traceroute - Discover the path packets take

# Linux
traceroute google.com

# Windows
tracert google.com

# Using ICMP instead of UDP (Linux)
sudo traceroute -I google.com

# Show AS numbers
traceroute -A google.com

# MTR - Combined ping + traceroute (live updating)
mtr google.com
# Implement ping in Python (requires root/admin)
import socket
import struct
import time
import select

def calculate_checksum(data):
    """Calculate ICMP checksum"""
    if len(data) % 2:
        data += b'\x00'
    
    checksum = 0
    for i in range(0, len(data), 2):
        word = (data[i] << 8) + data[i + 1]
        checksum += word
    
    checksum = (checksum >> 16) + (checksum & 0xFFFF)
    checksum = ~checksum & 0xFFFF
    return checksum

def create_icmp_packet(icmp_id, sequence):
    """Create an ICMP Echo Request packet"""
    # Type 8 = Echo Request, Code 0
    icmp_type = 8
    icmp_code = 0
    checksum = 0
    
    # Header without checksum
    header = struct.pack('!BBHHH', icmp_type, icmp_code, checksum, icmp_id, sequence)
    
    # Data payload (timestamp)
    data = struct.pack('!d', time.time())
    
    # Calculate checksum
    checksum = calculate_checksum(header + data)
    
    # Rebuild header with checksum
    header = struct.pack('!BBHHH', icmp_type, icmp_code, checksum, icmp_id, sequence)
    
    return header + data

def ping(host, timeout=2):
    """
    Send ICMP Echo Request and measure RTT
    Note: Requires root/administrator privileges
    """
    try:
        # Resolve hostname
        dest_ip = socket.gethostbyname(host)
        
        # Create raw socket for ICMP
        icmp_socket = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_ICMP)
        icmp_socket.settimeout(timeout)
        
        # Create and send packet
        icmp_id = 1
        sequence = 1
        packet = create_icmp_packet(icmp_id, sequence)
        
        send_time = time.time()
        icmp_socket.sendto(packet, (dest_ip, 0))
        
        # Wait for reply
        ready = select.select([icmp_socket], [], [], timeout)
        
        if ready[0]:
            recv_packet, addr = icmp_socket.recvfrom(1024)
            recv_time = time.time()
            
            # Parse reply (skip IP header - 20 bytes)
            icmp_header = recv_packet[20:28]
            icmp_type, code, checksum, p_id, seq = struct.unpack('!BBHHH', icmp_header)
            
            if icmp_type == 0:  # Echo Reply
                rtt = (recv_time - send_time) * 1000
                return {
                    'success': True,
                    'host': host,
                    'ip': dest_ip,
                    'rtt_ms': round(rtt, 2),
                    'ttl': recv_packet[8]  # TTL from IP header
                }
        
        return {'success': False, 'host': host, 'error': 'Timeout'}
        
    except PermissionError:
        return {'success': False, 'error': 'Root/Admin privileges required'}
    except socket.gaierror:
        return {'success': False, 'error': f'Cannot resolve {host}'}
    except Exception as e:
        return {'success': False, 'error': str(e)}

# Example (run with sudo/admin)
print("Note: Running ping requires root/administrator privileges")
print("Example usage: sudo python script.py")
print()
print("Expected output format:")
print({
    'success': True,
    'host': 'google.com', 
    'ip': '142.250.185.46',
    'rtt_ms': 15.23,
    'ttl': 117
})
Traceroute Mechanism: Traceroute works by sending packets with increasing TTL values (1, 2, 3...). Each router decrements TTL and sends back ICMP "Time Exceeded" when TTL reaches 0, revealing its IP address. This maps the entire path to the destination.

Routing Fundamentals

Routing is the process of selecting paths for traffic in a network. Routers use routing tables to decide where to forward packets.

Routing Basics

How Routing Works

  1. Router receives a packet
  2. Examines destination IP address
  3. Looks up longest matching prefix in routing table
  4. Forwards packet to next hop or directly to destination
  5. Decrements TTL (drops if TTL = 0)
# View routing table on different systems

# Linux
ip route show
# or
route -n
# or
netstat -rn

# Windows
route print
# or
netstat -rn

# macOS
netstat -rn

# Example Linux output:
# default via 192.168.1.1 dev eth0 proto dhcp metric 100
# 192.168.1.0/24 dev eth0 proto kernel scope link src 192.168.1.100

# Explanation:
# default         - Default route (0.0.0.0/0)
# via 192.168.1.1 - Next hop (gateway)
# dev eth0        - Outgoing interface
# metric 100      - Route preference (lower = preferred)
# Simulate a simple routing table lookup
class Router:
    """Simple router simulation with longest prefix matching"""
    
    def __init__(self, name):
        self.name = name
        self.routes = []  # List of (network, next_hop, interface)
    
    def add_route(self, network, next_hop, interface):
        """Add a route to the routing table"""
        import ipaddress
        net = ipaddress.IPv4Network(network)
        self.routes.append({
            'network': net,
            'prefix': network,
            'next_hop': next_hop,
            'interface': interface
        })
        # Sort by prefix length (longest first) for proper matching
        self.routes.sort(key=lambda r: r['network'].prefixlen, reverse=True)
    
    def lookup(self, destination):
        """Find the best route for a destination (longest prefix match)"""
        import ipaddress
        dest_ip = ipaddress.IPv4Address(destination)
        
        for route in self.routes:
            if dest_ip in route['network']:
                return {
                    'destination': destination,
                    'matched_route': route['prefix'],
                    'next_hop': route['next_hop'],
                    'interface': route['interface']
                }
        
        return {'destination': destination, 'result': 'No route found'}
    
    def show_routes(self):
        """Display routing table"""
        print(f"\nRouting Table for {self.name}")
        print("=" * 60)
        print(f"{'Network':<20} {'Next Hop':<16} {'Interface':<12}")
        print("-" * 60)
        for route in self.routes:
            print(f"{route['prefix']:<20} {route['next_hop']:<16} {route['interface']:<12}")

# Create a router with typical routes
router = Router("R1")

# Add routes (specific to general)
router.add_route("192.168.1.0/24", "directly_connected", "eth0")
router.add_route("192.168.2.0/24", "192.168.1.254", "eth0")
router.add_route("10.0.0.0/8", "192.168.1.253", "eth0")
router.add_route("0.0.0.0/0", "192.168.1.1", "eth0")  # Default route

router.show_routes()

# Test lookups
print("\nRoute Lookups:")
print("-" * 60)
test_destinations = [
    "192.168.1.50",   # Local network
    "192.168.2.100",  # Adjacent network
    "10.50.100.200",  # Remote private network
    "8.8.8.8",        # Internet (Google DNS)
]

for dest in test_destinations:
    result = router.lookup(dest)
    print(f"\n{dest}:")
    for key, value in result.items():
        print(f"  {key}: {value}")

Routing Protocols

Protocol Types

Interior vs Exterior Gateway Protocols

IGP (Interior Gateway Protocol): Used within an organization (Autonomous System)

  • RIP (Routing Information Protocol): Simple, distance-vector, max 15 hops. Legacy.
  • OSPF (Open Shortest Path First): Link-state, uses Dijkstra's algorithm. Most common IGP.
  • EIGRP (Enhanced Interior Gateway Routing Protocol): Cisco proprietary, advanced distance-vector.
  • IS-IS (Intermediate System to Intermediate System): Link-state, popular with ISPs.

EGP (Exterior Gateway Protocol): Used between organizations

  • BGP (Border Gateway Protocol): The protocol that routes the entire internet!

BGP: The Internet's Routing Protocol

BGP

Border Gateway Protocol

BGP is a path-vector protocol that exchanges routing information between Autonomous Systems (AS)—networks under single administrative control.

  • AS Numbers: Unique identifiers (e.g., Google is AS15169)
  • BGP Peers: Routers exchange routes via TCP port 179
  • Path Selection: Based on policies, AS path length, and other attributes
  • Full Internet Table: ~900,000+ routes (2024)
# Explore BGP and AS information

# Look up ASN for an IP
whois -h whois.radb.net 8.8.8.8

# View BGP looking glass (web tools)
# https://bgp.he.net/
# https://www.ripe.net/analyse/internet-measurements/routing-information-service-ris

# Check your own public IP's ASN
curl ipinfo.io

# Traceroute with AS numbers
traceroute -A google.com

# Example output shows AS path:
# 1  192.168.1.1 [AS0]      0.5ms
# 2  10.0.0.1 [AS12345]     5.2ms
# 3  72.14.238.1 [AS15169]  10.1ms  # Google's AS
BGP Security: BGP was designed without security! Notable incidents:
  • BGP Hijacking: Malicious AS announces routes it doesn't own
  • Route Leaks: Accidental propagation of internal routes
  • Solutions: RPKI (Resource Public Key Infrastructure), BGPsec

NAT: Network Address Translation

NAT allows multiple devices to share a single public IP address. It's what lets your entire home network access the internet through one IP from your ISP.

How NAT Works

NAT Translation Process

Outbound (Private → Public):
1. Device 192.168.1.100 sends packet to 8.8.8.8
2. Router receives packet
3. Router changes source IP: 192.168.1.100 → 203.0.113.50 (public IP)
4. Router records mapping in NAT table
5. Packet sent to internet with public source IP

Inbound (Public → Private):
1. Response arrives for 203.0.113.50
2. Router looks up NAT table
3. Router changes dest IP: 203.0.113.50 → 192.168.1.100
4. Packet forwarded to internal device

NAT Table Example:
┌─────────────────────┬──────────────────────┬───────────────┐
│ Internal            │ External             │ Destination   │
│ 192.168.1.100:54321 │ 203.0.113.50:12345   │ 8.8.8.8:53   │
│ 192.168.1.101:54322 │ 203.0.113.50:12346   │ 8.8.8.8:53   │
└─────────────────────┴──────────────────────┴───────────────┘

Types of NAT

NAT Variants

NAT Types Explained

Type Description Use Case
Static NAT 1:1 mapping of private to public IP Servers that need consistent public IP
Dynamic NAT Pool of public IPs assigned dynamically Organizations with multiple public IPs
PAT/NAPT (Overload) Many private IPs share one public IP using ports Home routers, most common type
Port Forwarding External port → internal IP:port Hosting servers behind NAT
# NAT configuration examples (Linux iptables)

# Enable IP forwarding
echo 1 > /proc/sys/net/ipv4/ip_forward

# Basic SNAT (Source NAT) - Masquerade
# All traffic from 192.168.1.0/24 gets NAT'd to eth0's IP
iptables -t nat -A POSTROUTING -s 192.168.1.0/24 -o eth0 -j MASQUERADE

# SNAT with specific public IP
iptables -t nat -A POSTROUTING -s 192.168.1.0/24 -o eth0 -j SNAT --to-source 203.0.113.50

# DNAT (Destination NAT) - Port forwarding
# Forward port 8080 on public IP to internal web server
iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 8080 -j DNAT --to-destination 192.168.1.100:80

# View NAT rules
iptables -t nat -L -n -v

# View active NAT connections
conntrack -L
NAT Drawbacks:
  • Breaks end-to-end connectivity: Devices behind NAT can't be reached directly
  • Complicates protocols: FTP, SIP, gaming require special NAT traversal
  • Adds latency: Translation takes time
  • IPv6 eliminates need: Enough addresses for every device to have public IP

Hands-On Exercises

Exercise 1: Network Exploration Tools

# Complete IP investigation toolkit

# 1. Find your IP configuration
# Linux
ip addr show
ip route show

# Windows
ipconfig /all

# macOS
ifconfig
netstat -rn

# 2. Test connectivity
ping -c 4 google.com
ping -c 4 8.8.8.8

# 3. Trace the path
traceroute google.com

# 4. DNS lookup
nslookup google.com
dig google.com

# 5. Check what's listening
netstat -tlnp    # Linux
netstat -an      # Windows

# 6. Check your public IP
curl ifconfig.me
curl ipinfo.io/json

Exercise 2: Python IP Tools

# Comprehensive IP utility library
import ipaddress
import socket

class IPTools:
    """Collection of IP utility functions"""
    
    @staticmethod
    def get_network_info(ip_cidr):
        """Get comprehensive network information"""
        network = ipaddress.IPv4Network(ip_cidr, strict=False)
        
        return {
            'network': str(network.network_address),
            'broadcast': str(network.broadcast_address),
            'netmask': str(network.netmask),
            'wildcard': str(network.hostmask),
            'prefix_length': network.prefixlen,
            'total_hosts': network.num_addresses,
            'usable_hosts': max(0, network.num_addresses - 2),
            'first_usable': str(list(network.hosts())[0]) if list(network.hosts()) else None,
            'last_usable': str(list(network.hosts())[-1]) if list(network.hosts()) else None,
            'is_private': network.is_private
        }
    
    @staticmethod
    def ip_in_range(ip, network):
        """Check if IP is in a network range"""
        addr = ipaddress.IPv4Address(ip)
        net = ipaddress.IPv4Network(network, strict=False)
        return addr in net
    
    @staticmethod
    def get_ip_class(ip):
        """Determine classful address class (historical)"""
        first_octet = int(ip.split('.')[0])
        
        if first_octet < 128:
            return 'A'
        elif first_octet < 192:
            return 'B'
        elif first_octet < 224:
            return 'C'
        elif first_octet < 240:
            return 'D (Multicast)'
        else:
            return 'E (Reserved)'
    
    @staticmethod
    def resolve_hostname(hostname):
        """Resolve hostname to IP address"""
        try:
            ip = socket.gethostbyname(hostname)
            return {'hostname': hostname, 'ip': ip, 'success': True}
        except socket.gaierror as e:
            return {'hostname': hostname, 'error': str(e), 'success': False}
    
    @staticmethod
    def reverse_lookup(ip):
        """Reverse DNS lookup"""
        try:
            hostname = socket.gethostbyaddr(ip)[0]
            return {'ip': ip, 'hostname': hostname, 'success': True}
        except socket.herror as e:
            return {'ip': ip, 'error': str(e), 'success': False}
    
    @staticmethod
    def summarize_networks(networks):
        """Summarize multiple networks into supernets"""
        nets = [ipaddress.IPv4Network(n) for n in networks]
        return list(ipaddress.collapse_addresses(nets))

# Examples
tools = IPTools()

# Network analysis
print("Network Analysis: 192.168.1.0/24")
print("=" * 50)
info = tools.get_network_info("192.168.1.0/24")
for key, value in info.items():
    print(f"  {key}: {value}")

# IP range check
print("\n\nIP Range Checks:")
print("=" * 50)
test_ips = ["192.168.1.50", "192.168.2.50", "10.0.0.1"]
for ip in test_ips:
    in_range = tools.ip_in_range(ip, "192.168.1.0/24")
    print(f"  {ip} in 192.168.1.0/24: {in_range}")

# Hostname resolution
print("\n\nHostname Resolution:")
print("=" * 50)
hosts = ["google.com", "github.com", "invalid.hostname.test"]
for host in hosts:
    result = tools.resolve_hostname(host)
    if result['success']:
        print(f"  {host} → {result['ip']}")
    else:
        print(f"  {host} → Error: {result['error']}")

# Network summarization (route aggregation)
print("\n\nNetwork Summarization:")
print("=" * 50)
networks_to_summarize = [
    "192.168.0.0/24",
    "192.168.1.0/24", 
    "192.168.2.0/24",
    "192.168.3.0/24"
]
print(f"  Original: {networks_to_summarize}")
summarized = tools.summarize_networks(networks_to_summarize)
print(f"  Summarized: {[str(n) for n in summarized]}")
Self-Assessment

Quiz: Test Your Knowledge

  1. How many bits in an IPv4 address? (Answer: 32 bits)
  2. What's the subnet mask for /27? (Answer: 255.255.255.224)
  3. How many usable hosts in a /28 network? (Answer: 14)
  4. What private range is 172.16.0.0 in? (Answer: 172.16.0.0/12)
  5. What ICMP type is Echo Reply? (Answer: Type 0)
  6. What protocol runs the internet's routing? (Answer: BGP)
  7. What's the IPv6 loopback address? (Answer: ::1)
  8. What does NAT stand for? (Answer: Network Address Translation)

Summary & Next Steps

Key Takeaways:
  • IPv4 uses 32-bit addresses; IPv6 uses 128-bit addresses
  • CIDR notation (/24, /16) specifies network size flexibly
  • Subnetting divides networks; calculate hosts with 2(32-prefix)-2
  • ICMP provides diagnostics (ping, traceroute)
  • Routers use longest prefix match to forward packets
  • BGP routes between autonomous systems (the internet)
  • NAT allows private IPs to share public IPs
Quick Reference

IP Addressing Cheat Sheet

  • Private Ranges: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16
  • /24: 256 IPs, 254 hosts (most common subnet)
  • /30: 4 IPs, 2 hosts (point-to-point links)
  • IPv6 Prefix: /64 is standard for subnets
  • TTL: Default 64 or 128, prevents loops

Next in the Series

In Part 4: Transport Layer, we'll explore TCP, UDP, and QUIC. You'll learn about the three-way handshake, flow control, congestion control, ports, sockets, and how reliable (and unreliable) data delivery works end-to-end.