Table of Content

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.

  1. 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
    
  2. 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')
    
  3. 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.