This post has two purposes:
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).
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
.
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:
-n
to prevent addresses from being converted to names-p multicast
to only capture multicast traffic-i <interface-name>
to only capture traffic on a specific network interface (you can get their name with ip adress
, or ip a
for short)-X
to dump the payloads as hexThe following snippets can be useful as testing tools, or as starting points for more complex programs.
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
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()
);
}
}