Home

Multicast

Written on 2024-06-08

This post has two purposes:

What is multicast?

Multicast is a method of sending data to a group of hosts on a network, most often using UDP as the transport protocol. It is useful to send data to an unknown amount of hosts with low overhead, at the cost of being unreliable. Since UPD is unreliable, it is common that a lightweight protocol is added on top of it: common features include versioning, having sequence numbers and being able to split a payload across multiple datagrams (and stitch them back together on the receiving side). When incremental data is being served, gaps are often handled by getting snapshots, either from a side channel or on demand.

Adjacent technologies to consider include broadcast, anycast and messaging systems such as Kafka/Redpanda, RabbitMQ or Redis/Dragonfly.

Contrary to TCP or UDP unicast, there is more to UDP multicast than just binding or connecting a socket to a given address and receiving/sending data over it. With unicast, IP addresses and network addresses are somewhat interchangeable as they have a one-to-one mapping (very roughly speaking, omitting e.g. NAT and proxying), whereas with multicast, a sender does not send data to a specific remote network interface but to a group of hosts instead. More importantly, a receiver will receive data on a specific network interface, which has an IP address associated with it, but it will listen for datagrams addressed to a different IP address, that of the multicast group that the senders send data to. This group has an IP address associated with it, but no specific network interface. This has important implications on how traffic is routed on the network. With unicast, the members of the network “just” have to route datagrams based on the destination IP address and their routing table. With multicast however, a listener advertises that it cares about a given group via IGMP, and any traffic for that group gets directed to the listener (provided that all the equipment involved supports multicast).

IP addresses

Since IP addresses are not per-host, they cannot be assigned via a protocol like DHCP. Not only must multicast groups be assigned manually, it is important to not let them overlap. This is somewhat of an issue with unicast as well – you cannot bind multiple services to the same port – but it is a lot easier to keep track of one-to-one communication, and a lot less likely that some program will accidentally connect to a service with unicast. To help bring order to all this, the IANA maintains a registry that standardizes which IP ranges should be used for various purposes. Wikipedia also has a user-friendly explanation of what the purpose of various IP ranges are. In short, pick an address between 224.0.0.0 and 224.0.0.255 for local testing, but mind special addresses, as well as those that might already be used by services on your network. For a production address within your organization, check with your networks team if there are any rules or guidelines in place, otherwise pick one between 239.0.0.0 and 239.255.255.255.

Tools

Before trying any of the following tools and code snippets, make sure that your firewall allows traffic on the relevant interfaces and groups!

socat is a Swiss Army knife for networking with… challenging documentation. To save you/myself some pain, here are a couple of useful snippets:

socat STDIO UDP4-DATAGRAM:224.0.0.123:7777

will take data from stdin and send it out to the group with address 224.0.0.123, on port 7777. The network interface on which the data will be sent out will be determined by the kernel.

socat -v UDP4-RECVFROM:7777,ip-add-membership=224.0.0.123:192.168.1.245,fork EXEC:hostname

will listen for data sent to the same group, on the same port. It will bind to the network interface with address 192.168.1.245, which you should change to the address of the network card that you expect to receive traffic on (or 0.0.0.0 to bind to all interfaces). The -v flag will enable verbose logging, which will print the payloads to stdout.

Together, those two snippets can help troubleshoot routing/firewall issues, ensure that a service properly sends multicast data out, or test a service that listens to a specific group.

In case you have elevated permissions on your machine, you can also use tcpdump instead:

Code snippets

The following snippets can be useful as testing tools, or as starting points for more complex programs.

Python

sender.py:

import socket

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
sock.sendto(b"Hello, World!", ("224.0.0.123", 7777))

receiver.py:

import socket

MCAST_GRP = "224.0.0.123"
MCAST_PORT = 7777

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
# Optional: allows binding even if the address is already in use.
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind((MCAST_GRP, MCAST_PORT))
sock.setsockopt(
    socket.IPPROTO_IP,
    socket.IP_ADD_MEMBERSHIP,
    # Group + interface to listen on.
    socket.inet_aton(MCAST_GRP) + socket.inet_aton("0.0.0.0"),
)

while True:
    print(sock.recv(1024))
    # Alternatively, to print an asterisk for every CHUNK_SIZE bytes received:
    # CHUNK_SIZE = 10
    # bytes_received += len(sock.recv(1024))
    # while bytes_received > CHUNK_SIZE:
    #     print("*", end="", flush=True)
    #     bytes_received -= CHUNK_SIZE

Rust

sender.rs:

use std::{net::UdpSocket, time::Duration};

fn main() {
    // Send on any interface, from port 1234.
    let socket = UdpSocket::bind("0.0.0.0:1234").unwrap();
    // Send to group 224.0.0.123 on port 7777.
    socket.connect("224.0.0.123:7777").unwrap();
    loop {
        socket.send(b"Hello, World!").unwrap();
        std::thread::sleep(Duration::from_secs(1));
    }
}

receiver.rs:

use std::net::{Ipv4Addr, UdpSocket};

fn main() {
    // Bind to address 224.0.0.123, on port 7777.
    let socket = UdpSocket::bind("224.0.0.123:7777").unwrap();
    socket
        .join_multicast_v4(
            // Join group 224.0.0.123.
            &Ipv4Addr::from([224, 0, 0, 123]),
            // Let the kernel pick the interface to listen on.
            &Ipv4Addr::from([0, 0, 0, 0]),
        )
        .unwrap();

    loop {
        let mut buf = [0; 2048];
        let (read, src) = socket.recv_from(&mut buf).unwrap();
        println!(
            "Got {read} bytes from {src:?}: {}",
            std::str::from_utf8(&buf).unwrap()
        );
    }
}