Programming Multicast in Python
Table of Content
- Introduction
- IPv4 Multicast Address
- UDP Datagram Multicast
- Example Programs and Experiment Environment
- Exercise and Exploration
Introduction
There are a few terms describing communication patterns. These terms all ends with “-cast” and includes unicast, broadcast, multicast, anycast, and geocast. Here we are particularly interested in multicast, a technique that passes copies of a single packet to a selected subset of all possible destinations. TCP/IP supports multicast. Our aim here is to write a rudimentary Python program to illustrate the concept of multicast in the TCP/IP protocol stack.
IPv4 Multicast Address
IP addresses have a few categories. Multicast addresses are a category. Any IPv4 address in range 224.0.0.0/4 is a multicast address that defines a group, i.e., the selected subset of all possible hosts, to which we deliver the the packets. Note that the IP address range like this “224.0.0.0/4” is in the so-called CIDR notation. In this particular case, it basically means that any IPv4 addresses whose highest 4 bits (or the 4-bit prefix) are identical to those of 224.0.0.0 are in this range. (For the mathematically Inclined?, 224.0.0.0/4 defines the set of IPv4 addresses of where a is an IPv4 address, a 4-byte integer.)
UDP Datagram Multicast
UDP in the TCP/IP protocol stack realizes a datagram multicast service.
Example Programs and Experiment Environment
The UDP datagram multicast example here consists of two Python programs, mcastsend.py, the sender program and mcastrecv.py, the receiver program. We run the programs to demonstrate the concept of multicast using 4 Linux virtual machines.
The Sender Program (mcastsend.py)
import socket
import sys
def help_and_exit(prog):
print('Usage: ' + prog + ' host_ip mcast_group_ip mcast_port_num message',
file=sys.stderr)
sys.exit(1)
def mc_send(hostip, mcgrpip, mcport, msgbuf):
# This creates a UDP socket
sender = socket.socket(family=socket.AF_INET, type=socket.SOCK_DGRAM, \
proto=socket.IPPROTO_UDP, fileno=None)
# This defines a multicast end point, that is a pair
# (multicast group ip address, send-to port nubmer)
mcgrp = (mcgrpip, mcport)
# This defines how many hops a multicast datagram can travel.
# The IP_MULTICAST_TTL's default value is 1 unless we set it otherwise.
sender.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 1)
# This defines to which network interface (NIC) is responsible for
# transmitting the multicast datagram; otherwise, the socket
# uses the default interface (ifindex = 1 if loopback is 0)
# If we wish to transmit the datagram to multiple NICs, we
# ought to create a socket for each NIC.
sender.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_IF, \
socket.inet_aton(hostip))
# Transmit the datagram in the buffer
sender.sendto(msgbuf, mcgrp)
# release the socket resources
sender.close()
def main(argv):
if len(argv) < 5:
help_and_exit(argv[0])
hostipaddr = argv[1]
mcgrpipaddr = argv[2]
mcport = int(argv[3])
msg = argv[4]
mc_send(hostipaddr, mcgrpipaddr, mcport, msg.encode())
if __name__=='__main__':
main(sys.argv)
The Receiver Program (mcastrecv.py)
import sys
import socket
import struct
def help_and_exit(prog):
print('Usage: ' + prog + ' from_nic_by_host_ip mcast_group_ip mcast_port')
sys.exit(1)
def mc_recv(fromnicip, mcgrpip, mcport):
bufsize = 1024
# This creates a UDP socket
receiver = socket.socket(family=socket.AF_INET, type=socket.SOCK_DGRAM, \
proto=socket.IPPROTO_UDP, fileno=None)
# This configure the socket to receive datagrams sent to this multicast
# end point, i.e., the pair of
# (multicast group ip address, mulcast port number)
# that must match that of the sender
bindaddr = (mcgrpip, mcport)
receiver.bind(bindaddr)
# This joins the socket to the intended multicast group. The implications
# are two. It specifies the intended multicast group identified by the
# multicast IP address. This also specifies from which network interface
# (NIC) the socket receives the datagrams for the intended multicast group.
# It is important to note that socket.INADDR_ANY means the default network
# interface in the system (ifindex = 1 if loopback interface present). To
# receive multicast datagrams from multiple NICs, we ought to create a
# socket for each NIC. Also note that we identify a NIC by its assigned IP
# address.
if fromnicip == '0.0.0.0':
mreq = struct.pack("=4sl", socket.inet_aton(mcgrpip), socket.INADDR_ANY)
else:
mreq = struct.pack("=4s4s", \
socket.inet_aton(mcgrpip), socket.inet_aton(fromnicip))
receiver.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
# Receive the mssage
buf, senderaddr = receiver.recvfrom(1024)
msg = buf.decode()
# Release resources
receiver.close()
return msg
def main(argv):
if len(argv) < 4:
help_and_exit(argv[0])
fromnicip = argv[1]
mcgrpip = argv[2]
mcport = int(argv[3])
msg = mc_recv(fromnicip, mcgrpip, mcport)
print(msg)
if __name__=='__main__':
main(sys.argv)
Demonstration
Assumptions
For convience, let’s name the 4 Linux virtual machines and assume they have IPv4 addresses as given in the following table,
Host Name | IPv4 Address |
---|---|
eastny | 192.168.56.1 |
midwood | 192.168.56.3 |
flatbush | 192.168.56.4 |
bushwick | 192.168.56.5 |
Assume we have two multicast groups,
IPv4 Multicast Group 1 | IPv4 Multicast Group 2 |
---|---|
224.1.1.5 | 234.3.2.1 |
The sender decides to use UDP port 50001 to identify receiving end points, and deploy the two programs to the 4 hosts as follows,
Host | Program |
---|---|
midwood | mccastsend.py |
eastny | mccastrecv.py |
flatbush | mccastrecv.py |
bushwick | mccastrecv.py |
Running the Demonstration
We run the programs as follows.
-
First, we run mcastrecv.py on eastny and flatbush as follows,
brooklyn@flatbush:~$ python mcastrecv.py 192.168.56.104 224.1.1.5 50001
brooklyn@eastny:~$ python mcastrecv.py 192.168.56.101 224.1.1.5 50001
-
We run both mcastrecv.py and scapy3 on bushwick in two terminals,
brooklyn@bushwick:~$ python mcastrecv.py 192.168.56.105 234.3.2.1 50001
brooklyn@bushwick:~$ sudo scapy3 >>> packets = sniff(prn=lambda p: p.summary(), filter='udp port 50001')
-
Finally, we run mcastsend.py on midwood.
brooklyn@midwood:~$ python mcastsend.py 192.168.56.103 224.1.1.5 50001 "Hello, World!"
Output
The output are like the following,
On flatbush, we observe,
brooklyn@flatbush:~$ python mcastrecv.py 192.168.56.104 224.1.1.5 50001
Hello, World!
brooklyn@flatbush:~$
On eastny, we also observe,
brooklyn@eastny:~$ python mcastrecv.py 192.168.56.101 224.1.1.5 50001
Hello, World!
brooklyn@eastny:~$
However, on bushwick, mcastrecv.py is still waiting for data, which means that it hasn’t received a datagram intended for multicast group 234.3.2.1 from the network interface identified by the IPv4 address 192.168.56.101. (well, we should have taken the note that in this demo, we never send a datagram to that group.)
brooklyn@bushwick:~$ python mcastrecv.py 192.168.56.105 234.3.2.1 50001
However, scapy3 does captured the packet sent by mcastsend.py on midwood.
>>> packets = sniff(prn=lambda p: p.summary(), filter='udp port 50001')
Ether / IP / UDP 192.168.56.103:52582 > 224.1.1.5:50001 / Raw / Padding
^C>>> hexdump(packets[0])
0000 01005E010105080027A133B408004500 ..^.....'.3...E.
0010 00290DC74000011191E7C0A83867E001 .)..@.......8g..
0020 0105CD66C351001553C948656C6C6F2C ...f.Q..S.Hello,
0030 20576F726C64210000000000 World!.....
>>>
Exercise and Exploration
Let’s consider the following scenario.
We want to receive multicast datagrams from any network interfaces.
Motivated by the observation that socket.INADDR_ANY
represents any
addresses. We hypothesize that a simple revision in mcastrecv.py let
us archieve this objective. The revision is that we replace
if fromnicip == '0.0.0.0':
mreq = struct.pack("=4sl", socket.inet_aton(mcgrpip), socket.INADDR_ANY)
else:
mreq = struct.pack("=4s4s", \
socket.inet_aton(mcgrpip), socket.inet_aton(fromnicip))
receiver.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
with
mreq = struct.pack("=4sl", socket.inet_aton(mcgrpip), socket.INADDR_ANY)
When we run the revised mcastrecv.py on eastny or flatbush like the following,
brooklyn@flatbush:~$ python mcastrecv.py 192.168.56.104 224.1.1.5 50001
and we run mcastsend.py on midwood.
brooklyn@midwood:~$ python mcastsend.py 192.168.56.103 224.1.1.5 50001 "Hello, World!"
we observe that mcastrecv.py is still waiting for data while mcastsend.py completes. Design an experiment where you use scapy3 to capture packets and use the captured packets to explain the phenomenon.