# Module 04: DHCP Relay — Crossing Broadcast Boundaries

**Nav:** [← Module 3](03-dhcp-options.md) | [Course Home](../README.md) | [Module 5 →](05-advanced-wireshark.md)

| | |
|---|---|
| **Duration** | ~2 hours |
| **Lab Required** | CML (or EVE-NG/GNS3) with 2+ VLANs, 1 router, 1 DHCP server |
| **Wireshark Profile** | `dhcp-relay` (build during lab) |
| **Prerequisites** | Modules 01–03: DORA flow, options fundamentals |

---

## 4.1 — Why DHCP Relay Exists

### The Broadcast Domain Problem

DHCP's initial discovery relies on a Layer 2 broadcast — the client sends a `DHCPDISCOVER` to `255.255.255.255` because it has no IP address and no idea where a server lives. Broadcasts, by definition, do not cross router boundaries. Every router interface is a **broadcast domain boundary**.

In a flat, single-subnet lab this is invisible. In production, it's the central problem:

```
VLAN 10 (Users)          Router (L3)            VLAN 99 (Servers)
┌─────────────┐         ┌────────┐             ┌──────────────┐
│ PC (no IP)  │───Gi0/1─┤        ├─Gi0/2──────│ DHCP Server  │
│ DISCOVER →  │ ══════╳ │ drops  │             │ 10.99.0.10   │
│ broadcast   │  bcast  │ bcast  │             │              │
└─────────────┘         └────────┘             └──────────────┘
```

Without intervention, the Discover never reaches the server. You have three choices:

| Approach | Scalability | Security | Operational Cost |
|---|---|---|---|
| Place a DHCP server on every subnet | Terrible | Poor (attack surface) | Very high |
| Use DHCP relay on the gateway | Excellent | Good | Low |
| Extend VLANs everywhere (flat L2) | Dangerous | Terrible | Medium |

**DHCP relay** is the industry-standard answer. The router intercepts the broadcast Discover, rewrites critical header fields, and **unicasts** it to a centralized DHCP server on a different subnet. The server replies, the router relays the response back, and the client is none the wiser.

> **Key insight for packet analysts:** The DHCP frames you capture on the *client-facing* segment look different from the frames on the *server-facing* segment. Understanding *exactly* what changes — and why — is the core skill this module teaches.

---

## 4.2 — How `ip helper-address` Works on Cisco IOS

### Basic Configuration

On a Cisco IOS or IOS-XE router, relay is enabled per-interface with a single command:

```cisco
interface GigabitEthernet0/1
 description ** User VLAN 10 Gateway **
 ip address 10.10.0.1 255.255.255.0
 ip helper-address 10.99.0.10
```

When the router receives a **UDP broadcast** on Gi0/1 destined for certain well-known ports, it converts that broadcast into a **unicast** aimed at `10.99.0.10`. DHCP (UDP 67/68) is one of those ports, but `ip helper-address` forwards **eight UDP services** by default:

| Port | Service |
|---:|---|
| 37 | Time |
| 49 | TACACS |
| 53 | DNS |
| 67 | BOOTP/DHCP Server |
| 68 | BOOTP/DHCP Client |
| 69 | TFTP |
| 137 | NetBIOS Name Service |
| 138 | NetBIOS Datagram Service |

To restrict forwarding to DHCP only (recommended in production):

```cisco
! Stop forwarding all eight services
no ip forward-protocol udp 37
no ip forward-protocol udp 49
no ip forward-protocol udp 53
no ip forward-protocol udp 69
no ip forward-protocol udp 137
no ip forward-protocol udp 138

! Only UDP 67 and 68 remain active
```

### Multiple Helper Addresses (Redundancy)

You can configure **multiple** helper addresses on a single interface. The router sends a **copy** of every relayed Discover to each address:

```cisco
interface GigabitEthernet0/1
 ip helper-address 10.99.0.10
 ip helper-address 10.99.0.11
```

This is used for DHCP server redundancy. Both servers receive the Discover; whichever Offers first typically wins. Be sure the servers' scopes don't overlap, or use DHCP failover/split-scope to coordinate.

---

## 4.3 — What the Relay Agent Actually Does to the Packet

When the router's relay function processes an inbound DHCP broadcast, it modifies the packet in **five specific, observable ways** before forwarding. Every one of these changes is visible in Wireshark.

### 4.3.1 — Sets the `giaddr` (Gateway IP Address) Field

This is the most critical modification. The relay agent writes its own IP address — **the address of the interface where it received the broadcast** — into the `giaddr` field (offset 24 in the BOOTP header).

```
Before relay:   giaddr = 0.0.0.0
After relay:    giaddr = 10.10.0.1    ← router's Gi0/1 address
```

**Why this matters:** The DHCP server uses `giaddr` to determine which scope (IP pool) to allocate from. If `giaddr` is `10.10.0.1`, the server looks for a scope that includes `10.10.0.0/24`. This is how a single centralized server hands out addresses for dozens of different subnets.

### 4.3.2 — Increments the `hops` Field

The relay agent increments the `hops` counter (offset 3 in the BOOTP header) by 1. This prevents infinite relay loops if multiple relays are chained.

```
Before relay:   hops = 0
After relay:    hops = 1
```

Most DHCP servers reject packets with `hops > 16` (configurable). In a correctly designed network, you should never see hops greater than 2.

### 4.3.3 — Converts Broadcast → Unicast

The original Discover arrives as:

```
Ethernet dst:  ff:ff:ff:ff:ff:ff    (broadcast)
IP dst:        255.255.255.255      (broadcast)
```

The relay agent transmits it as:

```
Ethernet dst:  <MAC of next-hop to server>   (unicast)
IP dst:        10.99.0.10                     (unicast to server)
IP src:        10.99.0.1                      (router's server-facing interface)
```

### 4.3.4 — Rewrites IP Source Address

The source IP changes from `0.0.0.0` (the client has no address) to the **router's outbound interface IP** — the interface facing the server, not necessarily the interface that received the client broadcast. This is a normal IP routing behavior; the relay agent creates a new IP datagram.

### 4.3.5 — (Optional) Inserts Option 82

If configured, the relay agent appends **Option 82 (Relay Agent Information)** to the DHCP options payload. Covered in detail in Section 4.5.

### Summary of All Modifications

| Field | Client Original | After Relay |
|---|---|---|
| Ethernet Destination | `ff:ff:ff:ff:ff:ff` | Server's resolved MAC |
| IP Source | `0.0.0.0` | Router's outbound interface IP |
| IP Destination | `255.255.255.255` | Helper address (e.g., `10.99.0.10`) |
| UDP Ports | `68 → 67` | `67 → 67` |
| `giaddr` | `0.0.0.0` | Router's client-facing interface IP |
| `hops` | `0` | `1` |
| Options payload | Unchanged | Option 82 appended (if configured) |

> **Wireshark checkpoint:** The easiest way to confirm relay is active is to look for a non-zero `giaddr` in any DHCP packet. If `giaddr` is populated, a relay agent touched that packet.

---

## 4.4 — Relay Behavior in Wireshark: Client Side vs. Server Side

When you capture traffic on **both sides** of the relay agent simultaneously, you see two distinctly different conversations:

### Client-Side Capture (VLAN 10, Gi0/1)

```
No.  Time     Source          Destination       Info
1    0.000    0.0.0.0         255.255.255.255   DHCP Discover
2    0.045    10.10.0.1       255.255.255.255   DHCP Offer
3    0.048    0.0.0.0         255.255.255.255   DHCP Request
4    0.091    10.10.0.1       255.255.255.255   DHCP ACK
```

The client sees **broadcasts from the gateway IP** — the router converts the server's unicast replies back into broadcasts (or unicasts to the client's assigned IP, depending on the `broadcast` flag).

### Server-Side Capture (VLAN 99, Gi0/2)

```
No.  Time     Source          Destination       Info
1    0.003    10.99.0.1       10.99.0.10        DHCP Discover  [giaddr=10.10.0.1]
2    0.042    10.99.0.10      10.99.0.1         DHCP Offer     [giaddr=10.10.0.1]
3    0.051    10.99.0.1       10.99.0.10        DHCP Request   [giaddr=10.10.0.1]
4    0.088    10.99.0.10      10.99.0.1         DHCP ACK       [giaddr=10.10.0.1]
```

All four messages are **unicast** and `giaddr` is populated in every packet. The server sends its replies **back to the relay agent** (`10.99.0.1`), not to the client — because the server knows it can't route directly to a client that has no IP address on a remote subnet.

### Key Wireshark Filters for Relay Traffic

```wireshark
# Show only packets with a non-zero giaddr (relay was involved)
bootp.ip.relay != 0.0.0.0

# Show only packets relayed through a SPECIFIC gateway
bootp.ip.relay == 10.10.0.1

# Show Option 82 (Relay Agent Information)
dhcp.option.agent_information_option

# Show the Circuit ID sub-option of Option 82
dhcp.option.agent_information_option.agent_circuit_id

# Show the Remote ID sub-option of Option 82
dhcp.option.agent_information_option.agent_remote_id

# Combine: relay traffic for VLAN 10 with Option 82 present
bootp.ip.relay == 10.10.0.1 && dhcp.option.agent_information_option
```

> **Pro tip:** Create a custom Wireshark column for `giaddr` — right-click a giaddr field → **Apply as Column**. This makes relay identification instant when scrolling through large captures.

---

## 4.5 — Option 82: Relay Agent Information

### What It Is

Option 82 (RFC 3046) is a **DHCP option inserted by the relay agent** — not by the client. It tells the DHCP server the physical location of the client: which switch, which port, which VLAN. The server can use this to make policy decisions: assign IPs from a specific range, deny service, or log the location for audit.

### Sub-Options

Option 82 contains two standard sub-options:

| Sub-option | ID | Contains | Example Value |
|---|---|---|---|
| **Circuit ID** | 1 | Switch port / VLAN the client is connected to | `Gi1/0/5, VLAN 10` |
| **Remote ID** | 2 | Switch MAC address or hostname | `aabb.cc00.0100` |

### Cisco IOS Configuration

```cisco
interface GigabitEthernet0/1
 ip dhcp relay information option
```

Or globally (applies to all relay interfaces):

```cisco
ip dhcp relay information option
```

### What Option 82 Looks Like in Wireshark

Expand the DHCP options tree in Wireshark, and you'll see:

```
Option: (82) Relay Agent Information
  Length: 22
  Option 82 Suboption: (1) Agent Circuit ID
    Length: 6
    Agent Circuit ID: 00:04:00:0a:00:01     ← VLAN 10, port Gi0/1
  Option 82 Suboption: (2) Agent Remote ID
    Length: 8
    Agent Remote ID: aa:bb:cc:00:01:00:00:00  ← relay switch MAC
```

### Server-Side Behavior

When the DHCP server sees Option 82, it can:

1. **Log it** — record which switch port issued the request (invaluable for security teams tracking rogue devices)
2. **Use it for scope selection** — assign a specific IP range based on the circuit (e.g., all ports on floor 3 get `10.10.3.x`)
3. **Enforce policy** — reject requests from unauthorized ports

> **Warning:** Some older DHCP servers drop packets containing Option 82 if they're not configured to expect it. If clients suddenly stop getting addresses after you enable Option 82, check the server's relay information handling policy. On Windows DHCP Server, this works out of the box. On ISC DHCP, you may need `stash-agent-options` and `agent-store` directives.

---

## 4.6 — Multi-VLAN Relay Scenarios

### Architecture: One Router, Many VLANs, One Server

In a real campus network, you'll configure relay on every SVI (VLAN interface):

```cisco
ip dhcp relay information option

interface Vlan10
 description ** Users - Floor 1 **
 ip address 10.10.0.1 255.255.255.0
 ip helper-address 10.99.0.10

interface Vlan20
 description ** Users - Floor 2 **
 ip address 10.20.0.1 255.255.255.0
 ip helper-address 10.99.0.10

interface Vlan30
 description ** IoT Devices **
 ip address 10.30.0.1 255.255.255.0
 ip helper-address 10.99.0.10

interface Vlan99
 description ** Server VLAN **
 ip address 10.99.0.1 255.255.255.0
```

### Server Scope Mapping

The DHCP server needs a **scope for every subnet** that sends relayed traffic. It matches on `giaddr`:

| `giaddr` Received | Server Scope | Range |
|---|---|---|
| `10.10.0.1` | VLAN10-Users-F1 | `10.10.0.100` – `10.10.0.254` |
| `10.20.0.1` | VLAN20-Users-F2 | `10.20.0.100` – `10.20.0.254` |
| `10.30.0.1` | VLAN30-IoT | `10.30.0.100` – `10.30.0.254` |

**If the server has no scope matching the `giaddr`, the Discover is silently dropped.** This is the #1 cause of relay failures and produces zero error messages on the server in many implementations — only the absence of an Offer in your Wireshark capture reveals the problem.

### Wireshark Analysis: Identifying Which VLAN a Packet Belongs To

In a multi-VLAN environment captured at the server, every Discover looks similar. The differentiator is `giaddr`:

```wireshark
# See only VLAN 10 relay traffic at the server
bootp.ip.relay == 10.10.0.1

# See only VLAN 30 (IoT) traffic
bootp.ip.relay == 10.30.0.1
```

---

## 4.7 — CML Lab: DHCP Relay Across VLANs

### Topology

```
┌───────────┐      Gi0/1 (.1)  ┌──────────┐  Gi0/2 (.1)     ┌─────────────┐
│  Client   ├──── VLAN 10 ─────┤  Router  ├──── VLAN 99 ────┤ DHCP Server │
│ (no IP)   │   10.10.0.0/24   │  (relay) │   10.99.0.0/24  │  10.99.0.10 │
└───────────┘                  └──────────┘                  └─────────────┘
```

### Step 1 — Router Configuration

```cisco
hostname RELAY-RTR
!
interface GigabitEthernet0/1
 ip address 10.10.0.1 255.255.255.0
 ip helper-address 10.99.0.10
 no shutdown
!
interface GigabitEthernet0/2
 ip address 10.99.0.1 255.255.255.0
 no shutdown
!
ip dhcp relay information option
```

### Step 2 — DHCP Server Configuration (ISC dhcpd Example)

```conf
# /etc/dhcp/dhcpd.conf

# Authoritative for our network
authoritative;

# Shared network — both subnets the server needs to know about
shared-network CAMPUS {

  # Server's own subnet (required even if no allocations here)
  subnet 10.99.0.0 netmask 255.255.255.0 {
    option routers 10.99.0.1;
  }

  # Remote subnet served via relay
  subnet 10.10.0.0 netmask 255.255.255.0 {
    range 10.10.0.100 10.10.0.200;
    option routers 10.10.0.1;
    option domain-name-servers 10.99.0.5;
    option domain-name "lab.local";
    default-lease-time 3600;
    max-lease-time 7200;
  }
}
```

Restart the service: `sudo systemctl restart isc-dhcp-server`

### Step 3 — Start Wireshark Captures

You need **two simultaneous captures** to see the full relay transformation:

| Capture Point | Interface | Filter |
|---|---|---|
| **Client-side** | Router Gi0/1 (or client's NIC) | `udp port 67 or udp port 68` |
| **Server-side** | Router Gi0/2 (or server's NIC) | `udp port 67 or udp port 68` |

In CML, right-click each link → **Capture** → open in Wireshark.

### Step 4 — Trigger DHCP from the Client

```bash
# Linux client
sudo dhclient -r eth0 && sudo dhclient -v eth0

# Windows client
ipconfig /release && ipconfig /renew
```

### Step 5 — Analyze the Captures

**Client-side Discover (packet #1):**
- Ethernet dst: `ff:ff:ff:ff:ff:ff`
- IP src: `0.0.0.0`, IP dst: `255.255.255.255`
- `giaddr`: `0.0.0.0`
- `hops`: `0`
- Option 82: **absent**

**Server-side Discover (packet #1):**
- Ethernet dst: server's MAC (unicast)
- IP src: `10.99.0.1`, IP dst: `10.99.0.10`
- `giaddr`: **`10.10.0.1`**
- `hops`: **`1`**
- Option 82: **present** (Circuit ID + Remote ID)

> **Lab deliverable:** Screenshot both Discover packets side-by-side and annotate every field that changed. This is the single most important exercise in this module.

---

## 4.8 — Troubleshooting DHCP Relay Failures

### Systematic Debugging Workflow

```
Client gets no IP
        │
        ▼
  ┌─ Capture on client side ──── Do you see a Discover? ── No → Client issue
  │                                        │ Yes
  │                                        ▼
  ├─ Capture on server side ──── Does Discover arrive? ──── No → Relay issue
  │                                        │ Yes
  │                                        ▼
  │                              Does server send Offer? ── No → Server/scope issue
  │                                        │ Yes
  │                                        ▼
  └─ Capture on client side ──── Does Offer reach client? ─ No → Return path issue
```

### Common Misconfigurations and Fixes

| # | Symptom | Root Cause | Fix |
|---|---|---|---|
| 1 | Server receives Discover but sends no Offer | No scope matching `giaddr` | Create a scope/subnet declaration for the relayed subnet |
| 2 | Discover never reaches server | `ip helper-address` on wrong interface | Configure helper on the **client-facing** interface (SVI or physical) |
| 3 | Discover never reaches server | ACL blocking UDP 67/68 | Add `permit udp any any eq 67` and `eq 68` to the interface ACL |
| 4 | Discover never reaches server | No route from router to server | Verify `show ip route 10.99.0.10` shows a valid path |
| 5 | Server receives Discover, sends Offer, but client never gets it | Firewall on server blocking return traffic | Ensure server allows UDP 67 outbound to the relay agent |
| 6 | Intermittent failures | Duplicate IP / scope exhaustion | Check `show ip dhcp binding` on server; expand the pool |
| 7 | Option 82 rejection | Server drops packets with Option 82 | Configure server to accept relay agent info, or disable Option 82 on the relay |

### Essential IOS Debug and Show Commands

```cisco
! Verify helper is configured correctly
show ip interface GigabitEthernet0/1 | include helper

! Watch relay operations in real time (use sparingly in production)
debug ip dhcp server packet
debug ip dhcp relay

! Check if the router is forwarding UDP broadcasts
show ip forward-protocol

! Verify routing to the DHCP server
show ip route 10.99.0.10
```

### The `debug ip dhcp relay` Output Decoded

```
DHCPD-RELAY: forwarding BOOTREQUEST for 0050.7966.6800 to 10.99.0.10
DHCPD-RELAY: setting giaddr to 10.10.0.1
DHCPD-RELAY: forwarding BOOTREPLY to client 0050.7966.6800 via 10.10.0.1
```

If you see the first line but **not** the second, the relay agent is failing to set `giaddr` — which typically indicates the interface has no IP address assigned. If you see both forward lines but the client still fails, the return path (server → relay → client) is the problem — capture at the server to confirm.

---

## 4.9 — Module Summary

### What You Should Now Be Able To Do

- [x] Explain why broadcasts don't cross routers and why DHCP relay exists
- [x] Configure `ip helper-address` on Cisco IOS for single and multi-VLAN environments
- [x] Identify all five packet modifications a relay agent performs
- [x] Use Wireshark to compare client-side and server-side captures of the same DORA transaction
- [x] Read and interpret Option 82 (Relay Agent Information) in Wireshark
- [x] Systematically troubleshoot relay failures using captures and IOS debug output

### Key Wireshark Filters — Quick Reference

| Filter | Purpose |
|---|---|
| `bootp.ip.relay != 0.0.0.0` | All relayed DHCP traffic |
| `bootp.ip.relay == 10.10.0.1` | Traffic relayed via a specific gateway |
| `dhcp.option.agent_information_option` | Packets containing Option 82 |
| `bootp.hops > 0` | Packets touched by at least one relay |
| `udp.port == 67 && ip.dst == 10.99.0.10` | Relay-to-server unicasts |

### What's Next

In **Module 5**, we take your Wireshark skills to the next level: custom dissectors, advanced coloring rules for DHCP anomalies, Tshark automation for bulk PCAP analysis, and building a DHCP monitoring dashboard from capture data.

---

**Nav:** [← Module 3](03-dhcp-options.md) | [Course Home](../README.md) | [Module 5 →](05-advanced-wireshark.md)

---

*Module 04 — DHCP Relay · Packet Inspector: DHCP Deep Dive with Wireshark · v1.0*
