PathVector Blog

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.

  • multicast
  • igmp
  • network
  • containerlab
  • udp

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 how 239.0.0.0/8 differs 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:5e Ethernet 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 — bundles iperf, tcpdump, and ip.

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 eth1 lists 239.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 for 239.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/4 identifies a group, not a host. 239.0.0.0/8 is 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.22 for 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 why 239.1.1.1 becomes 01: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 with ip route add 239.0.0.0/8 dev eth1 (already configured in this topology). Remove it, and iperf still receives — but the eth1 capture comes up empty.
  • Assuming the switch forwards intelligently. Without snooping, L2 floods. Smart forwarding requires IGMP snooping.
  • TTL. Even a small -T gets across a single L2 hop, but 224.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

  1. What's the difference between unicast and multicast? To reach N receivers, how many times does the sender send in each?
  2. What is a multicast group address (class D)? What kind of range is 239.0.0.0/8?
  3. How does a host join a group? What does IGMP carry — and what does it not carry?
  4. Which Ethernet MAC does 239.1.1.1 map to, and why?
  5. How does an L2 switch's multicast forwarding change with IGMP snooping on versus off?
  6. Why does this lab install a route pinning multicast to eth1?

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)
  • 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 to 224.0.0.22 (MAC 01:00:5e:00:00:16) — the join signalling.
  • The sender's (10.0.0.1) multicast data travels with destination MAC 01:00:5e:01:01:01 — the mapping of 239.1.1.1. You can see the IP's low 23 bits copied into the low bits of the MAC.
  • ip maddr show eth1 showed 239.1.1.1 on 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.