GRE Explained: The Layer-3 Tunnel That Wraps Everything and Encrypts Nothing

This post is part of Protocol Lab, a free, hands-on series for learning networking protocols by building and breaking them in a container lab. All the lab material — topologies, configs, and scripts — lives in the repo: github.com/pathvector-studio/protocol-lab.

This lab completes a small trilogy of tunnels. GRE (Generic Routing Encapsulation) wraps an IP packet in a GRE header inside an outer IP packet — a Layer-3 tunnel — and, like VXLAN, does not encrypt. Lining the three up shows that encapsulation, encryption, and which layer you carry are independent choices:

Lab Carries Encrypted? On the wire
16 WireGuard IP (L3) yes UDP 51820, ciphertext
18 VXLAN Ethernet (L2) no UDP 4789, inner frame visible
21 GRE IP (L3) no IP proto 47, inner IP visible

You will build a point-to-point GRE tunnel, ping across the overlay, and capture the underlay to see GRE (47) with the inner ICMP in the clear.

Reading guide: rfc-notes/gre-tunnel.md

Prerequisites: Lab 16: WireGuard, Lab 18: VXLAN

Expected time: 40–55 minutes.

What You Will Learn

  • What GRE is: a generic Layer-3 tunnel, carried directly in IP as protocol 47 (not UDP/TCP).
  • That GRE encapsulates but does not encrypt (like VXLAN, unlike WireGuard).
  • The overlay vs underlay distinction again, now for an L3 tunnel.
  • How GRE differs from VXLAN (L3 vs L2) and from WireGuard (plaintext vs encrypted).
  • Why GRE is often paired with IPsec when confidentiality is needed.

This lab does not cover:

  • GRE keys, sequence numbers, or checksums (optional GRE header fields).
  • Routing protocols over GRE (a common real use — e.g. GRE + OSPF).
  • IPsec-protected GRE (GRE over IPsec).

Where to Read in the RFCs

RFC Section What to focus on
RFC 2784 2 GRE packet structure (delivery header + GRE header + payload)
RFC 2784 2.3 Protocol Type, and IP protocol number 47
RFC 1701 1 GRE's design philosophy (generic encapsulation)
RFC 5737 3 Confirming the addresses used here are documentation-only

The Big Picture

Two nodes. A GRE tunnel (gre1, 10.100.0.0/24) runs on top of the underlay (eth1, 10.0.0.0/24).

             overlay:  10.100.0.1  <== gre1 (IP proto 47) ==>  10.100.0.2
                            |                                       |
node-a --------------- eth1 (underlay 10.0.0.0/24) --------------- node-b
       10.0.0.1                                              10.0.0.2

When you ping the overlay address 10.100.0.2, GRE wraps the inner IP packet and sends it to the underlay peer as IP protocol 47. Since GRE does not encrypt, peeking at the underlay shows both the GRE header and the inner IP packet.

flowchart LR
  subgraph node-a
    p1["ping 10.100.0.2<br/>(inner IP/ICMP)"] --> g1["gre1<br/>encapsulate"]
  end
  subgraph node-b
    g2["gre1<br/>decapsulate"] --> p2["ICMP delivered"]
  end
  g1 -- "IP proto 47 (GRE)<br/>(inner IP in the clear)" --> g2
  note["underlay で GRE ヘッダも<br/>内側 IP も読める"]

Both the 10.0.0.0/24 underlay and the 10.100.0.0/24 overlay are local, closed ranges.

Note: Everything here uses local/documentation address space (RFC 1918 / RFC 5737), so nothing in this lab touches the real internet.

What You Need

Recommended environment:

  • Linux / WSL2 / a Linux VM — the host needs the ip_gre kernel module. It is standard on recent Linux and loads automatically when you create the tunnel.
  • Docker
  • containerlab

Images used:

  • nicolaka/netshoot:latest — bundles ip and tcpdump.

No additional images are required. The GRE data path runs in the host kernel.

Running the Lab

The quick path, which deploys, verifies, and tears down for you:

./scripts/labctl.sh run gre-21

Or step through it manually:

1. Move into the working directory

cd protocol-lab/examples/gre-21

2. Deploy

sudo containerlab deploy -t gre-21.clab.yml

3. Bring up the GRE tunnel

# use the name gre1 (to avoid clashing with the kernel's default gre0)
docker exec clab-gre-21-node-a sh -c \
  "ip link add gre1 type gre local 10.0.0.1 remote 10.0.0.2; \
   ip addr add 10.100.0.1/24 dev gre1; ip link set gre1 up"
docker exec clab-gre-21-node-b sh -c \
  "ip link add gre1 type gre local 10.0.0.2 remote 10.0.0.1; \
   ip addr add 10.100.0.2/24 dev gre1; ip link set gre1 up"

4. Ping across the overlay

docker exec clab-gre-21-node-a ping -c3 10.100.0.2
docker exec clab-gre-21-node-a ip -d link show gre1

5. Capture the underlay and confirm the payload is visible

docker exec -d clab-gre-21-node-a tcpdump -i eth1 -n -w /tmp/gre.pcap "proto 47"
docker exec clab-gre-21-node-a ping -c3 10.100.0.2
docker exec clab-gre-21-node-a pkill -INT tcpdump
docker exec clab-gre-21-node-a tcpdump -n -vv -r /tmp/gre.pcap

Right after the outer IP marked proto GRE (47), the inner 10.100.0.1 > 10.100.0.2: ICMP echo request is visible as-is.

Expected Output

  • ping 10.100.0.2 succeeds.
  • ip -d link show gre1 shows gre remote 10.0.0.2 local 10.0.0.1.
  • The underlay capture shows proto GRE (47) / GREv0 with the inner ICMP echo in plaintext.

Why It Works

GRE (Generic Routing Encapsulation) is exactly what the name says: generic encapsulation. It wraps an arbitrary L3 packet in a GRE header and carries it in an outer IP packet.

  • It rides directly on IP. Where VXLAN and WireGuard use UDP, GRE goes straight into the IP payload as IP protocol number 47 — no UDP or TCP in between. That's why the underlay capture shows it as "proto GRE" rather than a transport port.
  • It carries L3. What GRE wraps is the inner IP packet (in this lab, IP carrying ICMP). Contrast that with VXLAN, which carries Ethernet frames (L2). So GRE's overlay gets IP addresses, not an L2 broadcast domain.
  • It does not encrypt. GRE's job is encapsulation only. Capture the underlay and both the GRE header and the inner IP are readable in plaintext. When confidentiality is needed, the standard move is to pair it with IPsec (GRE over IPsec).
  • Its place in the trilogy. WireGuard (L3, encrypted), VXLAN (L2, plaintext), GRE (L3, plaintext). "Which layer you carry" and "whether you encrypt" are two independent axes, and the three tunnels sit at different points of that grid. GRE is the "unencrypted L3 tunnel."

The key insight: encapsulation, encryption, and the layer you carry are separate choices — and GRE's spot is "carry L3, unencrypted, wrapped as IP proto 47."

Common Pitfalls

  • Assuming GRE is an encrypted tunnel. GRE does not encrypt. The payload is readable on the underlay. Only combining it with IPsec makes it confidential.
  • Assuming it uses UDP. GRE is IP protocol 47, not UDP/TCP (the capture filter is proto 47).
  • Clashing with the default gre0. The kernel reserves gre0, so use a different name (such as gre1).
  • Confusing L2 and L3. GRE is L3 (it carries IP). VXLAN is L2 (it carries Ethernet).
  • Mixing up overlay and underlay addresses. Underlay is 10.0.0.0/24, overlay is 10.100.0.0/24. You ping the overlay.
  • MTU. The GRE header shaves the overlay's effective MTU (about 24 bytes).

Cleanup

sudo containerlab destroy -t gre-21.clab.yml --cleanup

If you used labctl.sh run gre-21, the script runs destroy for you at the end.

Check Your Understanding

  1. What does GRE wrap in what? Under which protocol number does it ride on IP?
  2. Does GRE encrypt? What do you combine it with when confidentiality is required?
  3. How do GRE (L3) and VXLAN (L2) differ in what they carry?
  4. How do GRE and WireGuard look different in an underlay capture, and why?
  5. Are encapsulation, encryption, and the carried layer independent or dependent choices? Where do the three tunnels sit?
  6. Why use a name other than gre0?

References

Verified Run Log (2026-07-07)

This lab has been confirmed reproducible on real hardware.

Environment:

  • Ubuntu 26.04 LTS (kernel 7.0.0-27-generic, x86_64). The ip_gre kernel module loaded automatically when the tunnel was created.
  • Docker 29.1.3
  • containerlab 0.77.0
  • node-a / node-b: nicolaka/netshoot:latest (bundles tcpdump)

Running PATH="/tmp/pl-shim:$PATH" ./scripts/labctl.sh run gre-21 performed deploy → verify → destroy, and verification.json returned "status": "verified".

Ping across the overlay

$ docker exec clab-gre-21-node-a ping -c3 10.100.0.2
3 packets transmitted, 3 received, 0% packet loss

Both the GRE header and the inner IP are visible on the underlay

$ docker exec clab-gre-21-node-a tcpdump -n -vv -r underlay.pcap
IP (tos 0x0, ttl 64, ... proto GRE (47), length 108)
    10.0.0.1 > 10.0.0.2: GREv0, Flags [none], length 88
    10.100.0.1 > 10.100.0.2: ICMP echo request, id ..., seq 1, length 64

The outer layer shows proto GRE (47) / GREv0 (the underlay), and inside it the ICMP echo request (the overlay) is visible in plaintext, as-is. GRE encapsulates but does not encrypt.

  • WireGuard (Lab 16) = L3, encrypted → the underlay shows only ciphertext
  • VXLAN (Lab 18) = L2, plaintext → the inner Ethernet frame is visible
  • GRE (this lab) = L3, plaintext → the inner IP packet is visible

Encapsulation, encryption, and the carried layer are independent choices, and the three tunnels each sit at a different point of that grid.

Cleanup

containerlab destroy -t gre-21.clab.yml --cleanup

That's GRE: wrap any L3 packet in IP protocol 47, carry it across the underlay, and leave everything readable — encapsulation without encryption, by design.

Explore the full Protocol Lab series here: github.com/pathvector-studio/protocol-lab. If these labs are useful to you, please ⭐ star the repo on GitHub — it genuinely helps others find the project.

Next up, we'll build on this tunnel trilogy — think routing protocols over tunnels and what it takes to make an overlay actually private.

Read more