sccn / liblsl

C++ lsl library for multi-modal time-synched data transmission over the local network
Other
108 stars 63 forks source link

macOS fails to join IPv6 multicast groups #36

Open tstenner opened 4 years ago

tstenner commented 4 years ago

Output of ifconfig:

lo0: flags=8049<UP,LOOPBACK,RUNNING,MULTICAST> mtu 16384
        options=1203<RXCSUM,TXCSUM,TXSTATUS,SW_TIMESTAMP>
        inet 127.0.0.1 netmask 0xff000000 
        inet6 ::1 prefixlen 128 
        inet6 fe80::1%lo0 prefixlen 64 scopeid 0x1 
        nd6 options=201<PERFORMNUD,DAD>
gif0: flags=8010<POINTOPOINT,MULTICAST> mtu 1280
stf0: flags=0<> mtu 1280
EHC250: flags=0<> mtu 0
EHC253: flags=0<> mtu 0
en0: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500
        options=b<RXCSUM,TXCSUM,VLAN_HWTAGGING>
        ether c8:bc:c8:96:ec:2a 
        inet6 fe80::1c83:5086:dc41:c9ec%en0 prefixlen 64 secured scopeid 0x6 
        inet6 2001::1 prefixlen 72 
        inet XXX.X.XX.40 netmask 0xfffffc00 broadcast 172.22.15.255
        nd6 options=201<PERFORMNUD,DAD>
        media: autoselect (100baseTX <full-duplex,flow-control>)
        status: active

With the default setting of 0 for ipv6mr_interface in the IPV6_JOIN_GROUP request struct, joining the group(s) fails with error 49 (Can't assign requested address) and no IPv6 multicast packets are received.

After changing it to 6 (the interface index), the multicast join works without a hitch and the multicast packets are received.

Test code:

import asyncio
import socket
import struct

addrs = {socket.AF_INET: ['239.0.0.183'],
        socket.AF_INET6: ['FF02:113D:6FDD:2C17:A643:FFE2:1BD1:3CD2']
}
class MulticastListener(asyncio.DatagramProtocol):
    def __init__(self, name):
        self.transport = None
        self.name = name

    def datagram_received(self, data, addr):
        if data.startswith(b'LSL:shortinfo'):
            _, query, returnaddr = data.splitlines()
            returnaddr, queryid = returnaddr.split(b' ')
            print(f'{self.name} received {query} from {addr} -> {returnaddr}:')

    def error_received(self, exc):
        print('Error received:', exc)

    def connection_lost(self, exc):
        asyncio.get_event_loop().stop()

    @staticmethod
    def server_socket(family):
        mcastaddrs = addrs[family]
        # (_, _, _, _, sockaddr) = socket.getaddrinfo(mcastaddrs[0], MCastHelper.port, family, socket.SOCK_DGRAM)[0]

        s = socket.socket(family, socket.SOCK_DGRAM)
        s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        if hasattr(socket, 'SO_REUSEADDR'):
            s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        if family == socket.AF_INET:
            s.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 2)
        elif family == socket.AF_INET6:
            s.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_HOPS, 2)

        s.bind(('', 16571))
        for group in mcastaddrs:
            try:
                binaddr = socket.inet_pton(family, group)
                if family == socket.AF_INET:
                    s.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, binaddr + struct.pack('=I', socket.INADDR_ANY))
                elif family == socket.AF_INET6:
                    s.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, binaddr + struct.pack('@I', 0))
                print(f'Bound IP family {family} socket to {group}')
            except OSError as e:
                print(f'Error joining socket to group {group}: {e.errno}, {e.strerror}')
        return s

async def main():
    loop = asyncio.get_running_loop()
    tasks = []
    for name, family in [('IPv4', socket.AF_INET), ('IPv6', socket.AF_INET6)]:
        tasks.append(await loop.create_datagram_endpoint(lambda: MulticastListener(name=name),
                                                         sock=MulticastListener.server_socket(family)))
    try:
        await asyncio.sleep(60)
    finally:
        for transport, protocol in tasks:
            transport.close()

if __name__ == '__main__':
    asyncio.run(main())