DHCP Relay and NAT

Updated

September 4, 2026

DHCP Relay and NAT

Edges do more than forward packets. They hand out addresses (or relay DHCP) and often translate private space toward a provider. This chapter keeps both topics vendor-agnostic and lab-first: Linux services + FRR routing in Containerlab (mid-2026 free path).

Learning goals

By the end of this chapter you can:

  • Explain DHCP discover/offer/request/ack across a relay
  • Configure a simple DHCP server and a relay path in a lab
  • Contrast source NAT (masquerade), static NAT, and “why not NAT everything forever”
  • Implement Linux nftables/iptables MASQUERADE on an edge
  • Predict breakage when relays, helpers, or return translations fail

Concepts — DHCP

Why not static everything?

Labs love static host IPs. Real access LANs use DHCP for:

Benefit Trade-off
Central policy (lease, DNS, gateway) Dependency on server/relay
Fast readdressing Lease timers and “sticky” clients
Audit of who got what Need logging and hygiene

Four-way dance (v4)

Client          Relay (optional)         Server
  |-- Discover ------(unicast to server)-->|
  |<-- Offer -------------------------------|
  |-- Request ----------------------------->|
  |<-- Ack ---------------------------------|

On a flat LAN, broadcast Discover reaches the server. Across subnets, a relay agent (ip helper / DHCP relay) listens on the client VLAN and unicasts to the server, inserting giaddr (gateway IP of the client interface) so the server knows which pool to use.

Critical fields

Field / idea Why it matters
Client MAC / Client-ID Lease binding
giaddr Selects pool / subnet
Options: router, DNS, domain Host becomes usable
Lease time How long address is “owned”

Relay failure modes

  • Wrong giaddr → wrong pool or no offer
  • ACL blocking UDP 67/68
  • Server unreachable from relay
  • Multiple relays without design → duplicate offers chaos

Concepts — NAT family

Kind Meaning
SNAT / MASQUERADE Rewrite source as packets leave (common “PAT”)
DNAT Rewrite destination (port forward / publish service)
1:1 static NAT Stable mapping for a host
NAT64 / CLAT v6/v4 transition tools (awareness only here)

NAT is state + rewrite. It is not a security model by itself. Stateful firewalls may sit with NAT; keep roles clear.

When NAT is appropriate in this book

  • Lab edge simulating consumer/ISP CPE
  • Hiding RFC1918 behind a single lab “public”
  • Temporary access during dual-stack migration

Prefer real global or ULA design for multi-site fabrics later—NAT at every hop becomes undebugable.

Addressing plan (combined edge lab)

  h-client (DHCP)          dhcp-srv
       |                      |
      lan ---- edge-rtr ---- pe (public-ish)
                 |
              NAT outside
Net Use
10.20.30.0/24 Client LAN (DHCP pool)
10.20.30.1 Edge LAN IP + VRRP VIP optional
172.31.0.0/30 Edge–server link for DHCP server placement
198.51.100.0/30 Edge–PE “public” lab space (TEST-NET)
Pool 10.20.30.100–200, GW 10.20.30.1, DNS 10.20.30.1 or public lab DNS

Topology YAML

name: dhcp-nat-edge

topology:
  nodes:
    edge:
      kind: linux
      image: alpine:3.20
      # or FRR image if you co-locate routing; Alpine for pure Linux NAT/relay
      exec:
        - apk add --no-cache iproute2 iptables iputils dhcrelay dnsmasq
    pe:
      kind: linux
      image: alpine:3.20
      exec:
        - apk add --no-cache iproute2 iputils
        - ip addr add 198.51.100.2/30 dev eth1
        - ip link set eth1 up
        - ip addr add 203.0.113.1/32 dev lo
        - ip link set lo up
    dhcpsrv:
      kind: linux
      image: alpine:3.20
      exec:
        - apk add --no-cache iproute2 dnsmasq iputils
        - ip addr add 172.31.0.2/30 dev eth1
        - ip link set eth1 up
    client:
      kind: linux
      image: alpine:3.20
      exec:
        - apk add --no-cache iproute2 dhclient iputils
    lan:
      kind: linux
      image: alpine:3.20
      exec:
        - apk add --no-cache iproute2
        - ip link add br0 type bridge && ip link set br0 up

  links:
    - endpoints: ["client:eth1", "lan:eth1"]
    - endpoints: ["edge:eth1", "lan:eth2"]
    - endpoints: ["edge:eth2", "dhcpsrv:eth1"]
    - endpoints: ["edge:eth3", "pe:eth1"]

Bridge enslavement (run after deploy or in richer exec scripts):

# on lan node — enslaves data ports into br0
for i in eth1 eth2; do ip link set $i master br0; ip link set $i up; done

Edge addressing and forward

# edge LAN toward clients
docker exec clab-dhcp-nat-edge-edge ip addr add 10.20.30.1/24 dev eth1
docker exec clab-dhcp-nat-edge-edge ip link set eth1 up
# toward DHCP server
docker exec clab-dhcp-nat-edge-edge ip addr add 172.31.0.1/30 dev eth2
docker exec clab-dhcp-nat-edge-edge ip link set eth2 up
# toward PE
docker exec clab-dhcp-nat-edge-edge ip addr add 198.51.100.1/30 dev eth3
docker exec clab-dhcp-nat-edge-edge ip link set eth3 up
docker exec clab-dhcp-nat-edge-edge sysctl -w net.ipv4.ip_forward=1
docker exec clab-dhcp-nat-edge-edge ip route add default via 198.51.100.2

Bake these into startup scripts for real lab-as-code.

DHCP server (dnsmasq) on dhcpsrv

docker exec clab-dhcp-nat-edge-dhcpsrv sh -c 'cat >/etc/dnsmasq.conf <<EOF
interface=eth1
dhcp-range=10.20.30.100,10.20.30.200,12h
dhcp-option=option:router,10.20.30.1
dhcp-option=option:dns-server,10.20.30.1
log-dhcp
EOF'
docker exec clab-dhcp-nat-edge-dhcpsrv dnsmasq

Server is not on 10.20.30.0/24—the relay’s giaddr must make the pool selection work. For dnsmasq/simple labs, operators often put the server on the client subnet first (no relay). Teach both:

  1. Same subnet: client broadcast reaches server—no relay.
  2. Off-subnet: relay required; server must understand the client subnet (shared-network / giaddr-aware config).

Minimal same-subnet lab (sanity first)

Place dnsmasq on edge’s LAN IP or a server on the bridge:

dhcp-range=10.20.30.100,10.20.30.200,12h
dhcp-option=option:router,10.20.30.1

Relay path (concept + command)

# edge: relay client-facing interface toward server
docker exec clab-dhcp-nat-edge-edge dhcrelay -i eth1 172.31.0.2

Predict (DHCP)

  • Client sends Discover; server logs Offer for 10.20.30.x
  • Client installs default via 10.20.30.1
  • Lease appears in server logs

Observe

docker exec clab-dhcp-nat-edge-client ip link set eth1 up
docker exec clab-dhcp-nat-edge-client dhclient eth1 -v
docker exec clab-dhcp-nat-edge-client ip addr show eth1
docker exec clab-dhcp-nat-edge-client ip route
docker exec clab-dhcp-nat-edge-dhcpsrv cat /var/lib/misc/dnsmasq.leases 2>/dev/null || true

Fix

Symptom Checks
No offer bridge, interface up, firewall, wrong interface in dnsmasq
Offer but wrong GW dhcp-option router
Relay silent UDP 67/68, giaddr, server route

NAT — MASQUERADE on edge

Clients use private 10.20.30.0/24; PE only knows 198.51.100.0/30.

docker exec clab-dhcp-nat-edge-edge iptables -t nat -A POSTROUTING \
  -o eth3 -j MASQUERADE
# nftables equivalent pattern:
# nft add table ip nat
# nft add chain ip nat postrouting { type nat hook postrouting priority 100 \; }
# nft add rule ip nat postrouting oifname "eth3" masquerade

PE needs a way to answer (connected route to 198.51.100.1 is enough for return of MASQUERADE traffic).

Predict

  • client → 203.0.113.1 works
  • On pe, sources appear as 198.51.100.1, not 10.20.30.x
  • Without MASQUERADE, pe has no return path to 10.20.30.0/24

Observe

docker exec clab-dhcp-nat-edge-client ping -c 3 203.0.113.1
docker exec clab-dhcp-nat-edge-pe sh -c 'apk add --no-cache tcpdump; tcpdump -ni eth1 -c 5 icmp'
docker exec clab-dhcp-nat-edge-edge iptables -t nat -L -n -v

Fix

  • ip_forward=1
  • Correct -o egress interface
  • Default route on client and edge
  • Conntrack full / flush during experiments: conntrack -F if tool present

Drill — break DHCP then NAT

  1. Stop dnsmasq → new client gets nothing; journal “dependency.”
  2. Remove MASQUERADE → pings to PE fail; add static route on PE for 10.20.30.0/24 via edge as a teaching alternate (routing vs NAT).
  3. DNAT publish (optional): map 198.51.100.1:8080 → client:80 and curl from pe.
docker exec clab-dhcp-nat-edge-edge iptables -t nat -A PREROUTING \
  -i eth3 -p tcp --dport 8080 -j DNAT --to-destination 10.20.30.100:80

Only works if a client actually holds that lease/IP and listens.

FRR on the edge (optional co-location)

Many designs run FRR for BGP/OSPF and Linux for NAT/DHCP. Pattern:

  • FRR owns routing (vtysh)
  • Linux netfilter owns NAT
  • DHCP server may be centralized; FRR/host runs relay

Do not assume FRR “is” the NAT engine—check which process owns rewrite.

Verification checklist

Check Command / intent
Lease client has /24 + default
Options DNS/GW match design
Relay server log shows giaddr path
NAT capture shows public source
Forward sysctl ip_forward
Rules iptables/nft list nat

Common mistakes

Mistake Symptom
DHCP server wrong subnet logic No lease or wrong mask
Client static leftover IP “DHCP broken” false positive
NAT without hairpin plan Local services odd
Forgetting return routing when NAT removed One-way pings
Blocking bootp/dhcp in filter Silent failure

Summary

  • DHCP centralizes host addressing; relay extends it across L3
  • giaddr and pool design matter as much as the daemon
  • NAT rewrites and tracks state—use deliberately, verify with captures
  • Linux edge + Containerlab is enough to practice both without licenses
  • Prefer lab scripts that start dnsmasq, relay, and NAT rules idempotently

Next: name, time, and log hygiene—the quiet services that make edge failures diagnosable.