Simple C++ UDP Listener for Debugging

I asked ChatGPT to write a simple C++ socket program to listen and and it didn’t work at first but after a few pointed questions and some scolding :wink: I was able to receive the Heartbeat in the c++ program.

#include <iostream>
#include <cstring>
#include <iomanip>
#include <algorithm>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <net/if.h>
#include <unistd.h>

void printBufferAsHex(const char* buffer, size_t bufferSize) {
    size_t counter = 0;
    size_t const width = 32;
    size_t const width_2 = width / 2;
    for (size_t i = 0; i < bufferSize; ++i) {
        bool spacer = (counter > 0) and ((counter % width_2) == 0);
        bool newline = (counter > 0) and ((counter % width) == 0);
        printf("%s%s %02hhx", (spacer ? " " : ""), (newline ? "\r\n" : ""), buffer[i]);
        counter++;
    }

    // If the last line didn't reach width bytes, add a newline
    if (counter != 0) {
        printf("\r\n");
    }
}


int main(int argc, char* argv[]) {
    if (argc < 4) {
        std::cerr << "Usage: " << argv[0] << " <interface_address> <multicast_address> <port>" << std::endl;
        return 1;
    }

    const char* interfaceAddress = argv[1];
    const char* multicastAddress = argv[2];
    const int port = std::stoi(argv[3]);

    // Create a socket
    int sock = socket(AF_INET, SOCK_DGRAM, 0);
    if (sock < 0) {
        std::cerr << "Failed to create socket." << std::endl;
        return 1;
    }

    // Set SO_REUSEPORT option
    int reuseAddr = 1;
    if (setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &reuseAddr, sizeof(reuseAddr)) == -1) {
        std::cerr << "Failed to set SO_REUSEADDR option." << std::endl;
        close(sock);
        return 1;
    }

    // Set SO_REUSEPORT option
    int reusePort = 1;
    if (setsockopt(sock, SOL_SOCKET, SO_REUSEPORT, &reusePort, sizeof(reusePort)) == -1) {
        std::cerr << "Failed to set SO_REUSEPORT option." << std::endl;
        close(sock);
        return 1;
    }

    // Bind the socket to the specified interface address and port
    struct sockaddr_in interfaceAddr;
    std::memset(&interfaceAddr, 0, sizeof(interfaceAddr));
    interfaceAddr.sin_family = AF_INET;
    interfaceAddr.sin_addr.s_addr = inet_addr(interfaceAddress);
    interfaceAddr.sin_port = htons(port);

    // Set IP_MULTICAST_IF option
    if (inet_pton(AF_INET, interfaceAddress, &interfaceAddr) != 1) {
        std::cerr << "Failed to convert interface address." << std::endl;
        close(sock);
        return 1;
    }

    // Set socket options for multicast
    int ttl = 1;
    if (setsockopt(sock, IPPROTO_IP, IP_MULTICAST_TTL, &ttl, sizeof(ttl)) < 0) {
        std::cerr << "Failed to set TTL." << std::endl;
        close(sock);
        return 1;
    }

    // Set up the multicast group address and port
    struct sockaddr_in multicastAddr;
    std::memset(&multicastAddr, 0, sizeof(multicastAddr));
    multicastAddr.sin_family = AF_INET;
    multicastAddr.sin_addr.s_addr = inet_addr(multicastAddress);
    multicastAddr.sin_port = htons(port);

    if (bind(sock, (struct sockaddr*)&multicastAddr, sizeof(multicastAddr)) < 0) {
        std::cerr << "Failed to bind socket to multicast address." << std::endl;
        close(sock);
        return 1;
    }

    // Join the multicast group
    struct ip_mreq multicastRequest;
    multicastRequest.imr_multiaddr.s_addr = inet_addr(multicastAddress);
    multicastRequest.imr_interface.s_addr = inet_addr(interfaceAddress);

    if (setsockopt(sock, IPPROTO_IP, IP_ADD_MEMBERSHIP, (void*)&multicastRequest, sizeof(multicastRequest)) < 0) {
        std::cerr << "Failed to join multicast group." << std::endl;
        close(sock);
        return 1;
    }

    std::cout << "Socket successfully opened on multicast address " << multicastAddress << " and port " << port << " with interface address " << interfaceAddress << "." << std::endl;

    char buffer[1500];
    struct sockaddr_in senderAddr;
    socklen_t senderAddrLen = sizeof(senderAddr);

    while (true) {
        // Receive a UDP datagram
        ssize_t numBytes = recvfrom(sock, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&senderAddr, &senderAddrLen);
        if (numBytes < 0) {
            std::cerr << "Failed to receive data." << std::endl;
            close(sock);
            return 1;
        }

        buffer[numBytes] = '\0';

        // Dump the contents of the received message
        std::cout << "Received " << numBytes << " bytes from " << inet_ntoa(senderAddr.sin_addr) << ":" << ntohs(senderAddr.sin_port) << std::endl;
        std::cout << "Message: " << std::endl;
        size_t len = (numBytes < sizeof(buffer)) ? numBytes : sizeof(buffer);
        printBufferAsHex(buffer, len);
        std::cout << "==========================================" << std::endl;

        // Continue listening for more datagrams
    }

    // Cleanup and close the socket
    close(sock);

    return 0;
}


Compiling (i’m on Mac)

clang++ -std=c++14 -I/usr/include multicast.cpp -o multicast

Running:

./multicast 172.16.0.100 239.0.29.85 9382

I didn’t need to be sudo to receive.

image

With some changes posted above i can have two socket listeners (same program running twice) on the same multicast IP/port (each gets a copy). I’m still perplexed why pycyphal can’t receive data on it’s multicast sockets for this same data.

I asked ChatGPT nicely this time to convert the C++ to python and after some iteration with changing out the local interface address with the multicast address, I can see heart beart with this simple script too:

import argparse
import socket
import sys

def print_buffer_as_hex(buffer):
    counter = 0
    for byte in buffer:
        print(f'{byte:02X}', end=' ')
        counter += 1
        if counter == 16:
            print()
            counter = 0
    if counter != 0:
        print()

def main():
    # Argument parsing
    parser = argparse.ArgumentParser(description='UDP Multicast Receiver')
    parser.add_argument('interface_address', type=str, help='Interface address to bind the socket')
    parser.add_argument('multicast_address', type=str, help='Multicast group address')
    parser.add_argument('port', type=int, help='Port number')
    parser.add_argument('--ttl', type=int, default=1, help='Multicast TTL')
    args = parser.parse_args()

    interface_address = args.interface_address
    multicast_address = args.multicast_address
    port = args.port

    # Create socket
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

    # Set SO_REUSEADDR option
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

    # Set SO_REUSEPORT option if available (Linux-specific)
    if hasattr(socket, 'SO_REUSEPORT'):
        sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)

    # Bind to multicast address and port
    sock.bind((multicast_address, port))

    # Set IP_ADD_MEMBERSHIP option to join multicast group
    group = socket.inet_aton(multicast_address)
    sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, group + socket.inet_aton(interface_address))

    print(f'Joined multicast group {multicast_address}')

    # Set SO_MULTICAST_TTL option
    sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, args.ttl)

    # Set SO_MULTICAST_IF option
    interface_ip = socket.inet_aton(interface_address)
    sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_IF, interface_ip)

    print(f"Socket successfully opened on multicast address {multicast_address} and port {port} with interface address {interface_address}.")

    # Receive and print data
    buffer_size = 1500
    buffer = bytearray(buffer_size)

    while True:
        try:
            num_bytes, addr = sock.recvfrom_into(buffer)
            if num_bytes == 0:
                break
            print(f'Received {num_bytes} bytes from {addr[0]}:{addr[1]}')
            print_buffer_as_hex(buffer[:num_bytes])
        except KeyboardInterrupt:
            break

    sock.close()

if __name__ == '__main__':
    main()

So in the end here we can see that this at least proves in theory there’s no other configuration problem other than the local firewalls and packet filters getting in the way.

On Mac sudo pfctl -d -i 100000000000000000 :slight_smile: can’t disable for ever
On Linux sudo ufw disable

I’ll ask Chat how to construct rules for the packet filter to allow multicast through

1 Like

Added pfctl rules here: Mac OSX `pfctl` rule for Cyphal/UDP - #2 by erik.rainey

Not sure if this is related, but there’s something similar when developing pycyphal on macOS:

https://pycyphal.readthedocs.io/en/stable/pages/dev.html#setup

(See the green tip box)

I wireshark installed w/ the Cyphal/UDP plugin. I can see the remove devices on the network respond to the Service request but pycyphal doesn’t does not receive them. The simple programs above do see those service response when you run them on those other (239.1.0.100) multicast addresses.