javamacosmulticastmultihomed

IPv4 link-local multicast not transmitted over multiple network interfaces


Many users have a computer with a Wifi connection to the internet and a wired connection to a local network. I'm trying to use IPv4 link-local multicast to discover devices on the wired network but this does not work, apparently because the automatically generated routing tables only transmit the multicast frames via the Wifi interface.

My Java code tries to specify both interfaces:

sock.setNetworkInterface(ifc);
sock.send(new DatagramPacket(...));

but this appears to have no effect.

I can use

sudo route -n change -host 224.0.0.189 -interface en4

to change the routing table, and this works. But it's obviously is not practical for an app.

Any idea how to solve this?

(Ideally, I'd like the multicasts to be transmitted from all interfaces.)

It all works using plain old BSD Sockets code:

static const uint8_t discoveryEchoReq[] = { 0, 0, 0, 2, 0, 0, 0, 0xff, 0, 0, 0, 0, 0, 0, 0, 0 };

int main(int argc, char **argv) {
    
    //setup socket addr
    struct sockaddr_in mcastSockAddr;
    memset((char *)&mcastSockAddr, 0, sizeof(mcastSockAddr));
    mcastSockAddr.sin_family = AF_INET;
    mcastSockAddr.sin_addr.s_addr = inet_addr("224.0.0.189");
    mcastSockAddr.sin_port = htons(48556);

    //look for interfaces
    struct ifaddrs * interfaces = NULL;
    struct ifaddrs * ifc = NULL;
    if (getifaddrs(&interfaces) < 0) exit(-1);
    ifc = interfaces;
    while(ifc != NULL) {
        if ( (ifc->ifa_addr->sa_family == AF_INET) && ((ifc->ifa_flags & IFF_MULTICAST) != 0) ) {
            int sock = socket(AF_INET, SOCK_DGRAM, 0); //create socket
            if (sock < 0) exit(-2);
            if (setsockopt(sock, IPPROTO_IP, IP_MULTICAST_IF, &((struct sockaddr_in *)ifc->ifa_addr)->sin_addr, sizeof(struct in_addr)) < 0) exit(-3);
            if (sendto(sock, discoveryEchoReq, sizeof(discoveryEchoReq), 0, (struct sockaddr*)&mcastSockAddr, sizeof(mcastSockAddr)) < 0) exit(-5);
            if (close(sock) < 0) exit(-6);
        }
        ifc = ifc->ifa_next; //follow linked list
    }
    freeifaddrs(interfaces); //free memory
    
    exit(0);
}

but similar Java code does not work:

for (Enumeration ifcs = NetworkInterface.getNetworkInterfaces() ; ifcs.hasMoreElements() ; ) {
    NetworkInterface ifc = (NetworkInterface)ifcs.nextElement();
    if (ifc.supportsMulticast()) {
        for (Enumeration addrs = ifc.getInetAddresses() ; addrs.hasMoreElements() ; ) {
            InetAddress addr = (InetAddress)addrs.nextElement();
            if (addr instanceof Inet4Address) {
                MulticastSocket sock = new MulticastSocket();
                sock.setNetworkInterface(ifc);
                sock.send(new DatagramPacket(Discoverer.ECHO_REQUEST, Discoverer.ECHO_REQUEST.length, Daemon.SKT_MCAST));
            }
        }
    }
}

i.e. in the BSD case, the multicast packets are emitted from both network interfaces, but in the Java case they are emitted from only one interface.


Solution

  • Using NIO fixes the problem, so my guess is that there's maybe a bug in the Java runtime?

    i.e. this works:

    for (Enumeration ifcs = NetworkInterface.getNetworkInterfaces() ; ifcs.hasMoreElements() ; ) {
        NetworkInterface ifc = (NetworkInterface)ifcs.nextElement();
            if (ifc.supportsMulticast()) {
            for (Enumeration addrs = ifc.getInetAddresses() ; addrs.hasMoreElements() ; ) {
                InetAddress addr = (InetAddress)addrs.nextElement();
                if (addr instanceof Inet4Address) {
                    DatagramChannel dc = DatagramChannel.open(StandardProtocolFamily.INET).setOption(StandardSocketOptions.IP_MULTICAST_IF, ifc);
                    DatagramSocket sock = dc.socket();
                    sock.send(new DatagramPacket(Discoverer.ECHO_REQUEST, Discoverer.ECHO_REQUEST.length, Daemon.SKT_MCAST));
                }
            }
        }
    }