Multicast and IGMP: How One UDP Stream Reaches Many Receivers Without a Single Extra Copy
Join two receivers to group 239.1.1.1 with IGMP, send one UDP stream, and watch both receive it in full — then find the IGMP reports and the 01:00:5e multicast MAC in a packet capture.
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.
Every lab so far has moved packets from one host to one host — unicast. Lab #29 breaks that pattern with IP multicast: one sender puts a single UDP stream on the wire addressed to a group (239.1.1.1), and both receivers — which have joined that group via IGMP — receive it. The sender never sends a per-receiver copy.
Reading guide: rfc-notes/multicast-igmp.md
Prerequisite: Lab 24: ARP — IPv4 Neighbor Resolution
Expected time: 40–55 minutes.
The Goal
- Two receivers join group
239.1.1.1(iperf -s -u -B 239.1.1.1), each emitting an IGMP membership report. - The sender sends one UDP stream to
239.1.1.1(iperf -c 239.1.1.1 -u). - Both receivers report
0/513 (0%)— each got the whole stream from a single send. - A packet capture shows the IGMP reports and the multicast destination MAC
01:00:5e:01:01:01.
By the end, you should be able to explain this table:
| destination | who receives | copies on the segment | |
|---|---|---|---|
| unicast | one host | that host | one per receiver (N sends for N) |
| multicast | a group | hosts that joined | one (shared by all joiners) |
What You Will Learn
- What an IP multicast group (class D,
224.0.0.0/4) is, and how239.0.0.0/8differs from link-local control groups. - How a host joins a group and how IGMP (membership reports) signals that to the network.
- How an IPv4 multicast address maps to the
01:00:5eEthernet MAC. - Why one send reaches many receivers with a single copy on a shared segment.
- What IGMP snooping does (and why this lab leaves it off so the bridge floods).
This lab does not cover:
- Multicast routing between segments (PIM, mroute) — this is a single L2 segment.
- Source-specific multicast (SSM) filtering in depth.
- IPv6 multicast / MLD (touched on in the NDP lab).
Where to Read in the RFCs
| Reference | What to focus on |
|---|---|
| RFC 1112 | The multicast host model, class D addresses, and the IP-to-MAC mapping |
| RFC 2236 | IGMPv2 membership reports and queries |
| RFC 3376 | IGMPv3 (what Linux sends by default) |
| RFC 2365 | Where 239.0.0.0/8 (administratively scoped) fits in |
| RFC 5737 | Confirming the 10.0.0.0/24 range used here is local/closed (supplementary) |
The Big Picture
Three hosts — sender, rx1, and rx2 — plus sw, a Linux bridge that ties them into one shared segment.
sender (10.0.0.1)
|
[ sw ] (Linux bridge: one shared segment, snooping off = flood)
/ \
rx1 (10.0.0.2) rx2 (10.0.0.3)
join 239.1.1.1 join 239.1.1.1
The sender transmits just one UDP stream to 239.1.1.1. The bridge floods it to every port on the segment, and rx1 / rx2 — having joined the group — receive it.
flowchart TD
S["sender<br/>iperf -c 239.1.1.1 -u"] -->|one UDP stream<br/>dst 239.1.1.1| SW["sw (bridge)<br/>floods the segment"]
SW -->|same single copy| R1["rx1<br/>joined 239.1.1.1"]
SW -->|same single copy| R2["rx2<br/>joined 239.1.1.1"]
R1 -.->|IGMP report<br/>dst 224.0.0.22| SW
R2 -.->|IGMP report<br/>dst 224.0.0.22| SW
10.0.0.0/24 is a local, closed range, and 239.1.1.1 is an administratively scoped multicast group.
Note: Everything here uses local/closed address space, so nothing in this lab touches the real internet.
Why multicast is pinned to eth1
Each container has a management interface eth0 (containerlab's management bridge) and the lab interface eth1 (the link to sw). Linux's default multicast route tends to pick eth0, in which case the group traffic would travel over the management network and bypass the very switch we want to observe. Each node therefore adds ip route add 239.0.0.0/8 dev eth1 so that multicast always exits toward sw (eth1). The topology file (mcast-29.clab.yml) already includes this.
What You Need
Recommended environment:
- Linux / WSL2 / a Linux VM
- Docker
- containerlab
Images used:
nicolaka/netshoot:latest— bundlesiperf,tcpdump, andip.
No additional images are required.
Running the Lab
The quick path, which deploys, verifies, and tears down for you:
./scripts/labctl.sh run mcast-29
Or step through it manually:
1. Move into the working directory
cd protocol-lab/examples/mcast-29
2. Deploy
sudo containerlab deploy -t mcast-29.clab.yml
sw bundles its three ports into a single bridge (br0) with mcast_snooping 0 (flood). sender, rx1, and rx2 each carry the route that points 239.0.0.0/8 at eth1.
3. Join the group on the receivers (IGMP membership)
docker exec -d clab-mcast-29-rx1 sh -c "iperf -s -u -B 239.1.1.1 -i1 >/tmp/rx1.log 2>&1"
docker exec -d clab-mcast-29-rx2 sh -c "iperf -s -u -B 239.1.1.1 -i1 >/tmp/rx2.log 2>&1"
docker exec clab-mcast-29-rx1 ip maddr show eth1 # 239.1.1.1 appears
iperf -s -u -B 239.1.1.1 binds to the group address and joins it — the kernel sends an IGMP membership report on its behalf.
4. Capture IGMP and multicast traffic on a receiver
docker exec -d clab-mcast-29-rx1 tcpdump -i eth1 -n -e -w /tmp/mc.pcap "igmp or (udp and dst 239.1.1.1)"
5. Send a single stream to the group
docker exec clab-mcast-29-sender iperf -c 239.1.1.1 -u -T 5 -t 3 -b 2m
-T 5 sets the multicast TTL. The sender transmits exactly one stream to 239.1.1.1.
6. Look at the results
docker exec clab-mcast-29-rx1 sh -c 'grep Bytes /tmp/rx1.log | tail -1' # 0/513 (0%)
docker exec clab-mcast-29-rx2 sh -c 'grep Bytes /tmp/rx2.log | tail -1' # 0/513 (0%)
docker exec clab-mcast-29-rx1 pkill -INT tcpdump
docker exec clab-mcast-29-rx1 tcpdump -n -e -r /tmp/mc.pcap | grep -E "igmp|01:00:5e"
Both receivers got the same single stream, and the capture shows the IGMP reports and the multicast MAC.
Expected Output
ip maddr show eth1lists239.1.1.1(both receivers have joined).- Both rx1's and rx2's iperf logs show
0/513 (0%)— each fully received the one stream. - The capture contains
10.0.0.x > 224.0.0.22: igmp v3 report(membership). - The capture contains
01:00:5e:01:01:01(the multicast data MAC for239.1.1.1).
Why It Works
Unicast means "to one host" — to reach N receivers, the sender must send N times. Multicast means "to one group": the sender transmits once to the group address, and every host that has joined that group receives it. The sender knows neither how many receivers there are nor who they are.
- Group addresses (class D).
224.0.0.0/4identifies a group, not a host.239.0.0.0/8is administratively scoped — local to an organization. - Joining and IGMP. When a host wants a group, it sends an IGMP membership report (destination
224.0.0.22for IGMPv3). That tells routers and switches "this group is wanted here." IGMP is signalling — it never carries the data itself. - IP to MAC. IPv4 multicast maps onto MAC addresses starting with
01:00:5e(the low 23 bits of the IP are copied in). That's why239.1.1.1becomes01:00:5e:01:01:01. Receiver NICs are programmed to pick up frames for that MAC. - One copy. On a shared segment, the sender's single frame reaches both receivers as-is. Like broadcast, there's one copy on the segment — but multicast only concerns the set of hosts that joined.
- Switch behavior. A plain L2 bridge floods multicast to every port. Enabling IGMP snooping makes the switch peek at the reports and forward only to ports with listeners. This lab keeps snooping off so you can observe membership signalling and flood delivery in their plainest form.
The key insight: a single send reaches every joined receiver without per-receiver copies — and IGMP is the mechanism that tells the network about the "join."
Common Pitfalls
- Confusing multicast with "unicast N times." There's one copy on the segment; the sender sends once.
- Thinking IGMP carries the data. IGMP is join signalling. The data flows separately, as UDP or similar.
- Leaking onto the management interface. The default multicast route tends to pick
eth0(the management network). Pin it to the switch side withip route add 239.0.0.0/8 dev eth1(already configured in this topology). Remove it, and iperf still receives — but theeth1capture comes up empty. - Assuming the switch forwards intelligently. Without snooping, L2 floods. Smart forwarding requires IGMP snooping.
- TTL. Even a small
-Tgets across a single L2 hop, but224.0.0.0/24(link-local control) never leaves the link — TTL 1. - Treating a group address like a host. Class D names a group. Pinging it won't get you a reply.
Cleanup
sudo containerlab destroy -t mcast-29.clab.yml --cleanup
If you used labctl.sh run mcast-29, the script runs destroy for you at the end.
Check Your Understanding
- What's the difference between unicast and multicast? To reach N receivers, how many times does the sender send in each?
- What is a multicast group address (class D)? What kind of range is
239.0.0.0/8? - How does a host join a group? What does IGMP carry — and what does it not carry?
- Which Ethernet MAC does
239.1.1.1map to, and why? - How does an L2 switch's multicast forwarding change with IGMP snooping on versus off?
- Why does this lab install a route pinning multicast to
eth1?
References
- RFC 1112: Host Extensions for IP Multicasting
- RFC 2236: Internet Group Management Protocol, Version 2
- RFC 3376: Internet Group Management Protocol, Version 3
- RFC 2365: Administratively Scoped IP Multicast
- RFC 5737: IPv4 Address Blocks Reserved for Documentation
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)
- Docker 29.1.3
- containerlab 0.77.0
- sender / rx1 / rx2 / sw:
nicolaka/netshoot:latest(iperf,tcpdump,ip)
Running PATH="/tmp/pl-shim:$PATH" ./scripts/labctl.sh run mcast-29 performed deploy → verify → destroy, and verification.json returned "status": "verified".
One send reaches both receivers
The sender transmitted a single UDP stream to 239.1.1.1 (514 datagrams). rx1 and rx2, both joined via IGMP, each received the entire stream.
rx1: [ 1] 0.00-3.01 sec 736 KBytes 2.00 Mbits/sec 0.002 ms 0/513 (0%)
rx2: [ 1] 0.00-3.01 sec 736 KBytes 2.00 Mbits/sec 0.002 ms 0/513 (0%)
sender: [ 1] Sent 514 datagrams
No per-receiver copies were sent — the sender transmitted once, and both receivers report 0/513 (0%) (zero loss).
The capture: IGMP membership and the multicast MAC
On rx1's eth1, capturing "igmp or (udp and dst 239.1.1.1)" collected 523 packets in total.
12:25:57.041374 aa:c1:ab:40:2d:42 > 01:00:5e:00:00:16, IPv4, length 54: 10.0.0.2 > 224.0.0.22: igmp v3 report, 1 group record(s)
12:25:57.061375 aa:c1:ab:60:17:a6 > 01:00:5e:00:00:16, IPv4, length 54: 10.0.0.3 > 224.0.0.22: igmp v3 report, 1 group record(s)
12:25:59.117790 aa:c1:ab:35:ff:36 > 01:00:5e:01:01:01, IPv4, length 1512: 10.0.0.1.34590 > 239.1.1.1.5001: UDP, length 1470
- rx1 (
10.0.0.2) and rx2 (10.0.0.3) each send an IGMPv3 membership report to224.0.0.22(MAC01:00:5e:00:00:16) — the join signalling. - The sender's (
10.0.0.1) multicast data travels with destination MAC01:00:5e:01:01:01— the mapping of239.1.1.1. You can see the IP's low 23 bits copied into the low bits of the MAC. ip maddr show eth1showed239.1.1.1on both receivers (group membership).
A snag worth recording
The first verification run failed. The receivers' iperf was receiving the multicast, yet tcpdump on eth1 captured zero packets. The cause: Linux's default multicast route had picked the management eth0 (containerlab's management bridge), so the group traffic was arriving via the management network instead of through the switch under observation. After adding ip route add 239.0.0.0/8 dev eth1 on sender/rx1/rx2 to pin multicast to the sw side (eth1), the capture showed the IGMP reports and the 01:00:5e multicast frames, and verification went green. That route is now baked into the topology (mcast-29.clab.yml).
Cleanup
containerlab destroy -t mcast-29.clab.yml --cleanup
That's multicast in a nutshell: one send, one copy on the wire, and every joined receiver gets the whole stream — with IGMP quietly telling the network who wants in.
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: anycast — the opposite trick, where the same IP address lives on many servers and BGP quietly picks which one answers.