0

I want to implement a "simple" SSDP discovering client. Means the client should send out a

M-SEARCH * HTTP/1.1
HOST: 239.255.255.250:1900
MAN: "ssdp:discover"
MX: 1
ST: ssdp:all

and afterwards listen to "the network"(?) to get the list of IP addresses.

To test the implementation I've written a unit test which creates a "fake" MulticastServer which simply hear to the SSDP IP&Port and, when receive something, send the same message back.

The problem is that this code works on my machine (macOS) most of the time but never on our CI Server (Linux). I (macOS) receive sometimes the same assertion failed error as on the CI. But as I said - only sometimes! Not always. And I don't know why.

This is the implementation on the client side:

interface GatewayDiscoverer {

    companion object {
        val instance: GatewayDiscoverer = DefaultGatewayDiscoverer()
    }

    suspend fun discoverGateways(timeoutMillis: Int = 1000): List<String>
}

internal class DefaultGatewayDiscoverer : GatewayDiscoverer {

    override suspend fun discoverGateways(timeoutMillis: Int): List<String> {
        require(timeoutMillis in 1000..5000) {
            "timeoutMillis should be between 1000 (inclusive) and 5000 (inclusive)!"
        }

        val socket = DatagramSocket()
        sendSsdpPacket(socket)

        val gateways = receiveSsdpPacket(socket, timeoutMillis)

        return gateways
    }

    private fun sendSsdpPacket(socket: DatagramSocket) {
        val packetToSend =
            "M-SEARCH * HTTP/1.1\r\nHOST: 239.255.255.250:1900\r\nMAN: \"ssdp:discover\"\r\nMX: 1\r\nST: ssdp:all\r\n\r\n"
        val packetToSendAsBytes = packetToSend.toByteArray()
        val packet = DatagramPacket(
            packetToSendAsBytes,
            packetToSendAsBytes.size,
            InetAddress.getByName("239.255.255.250"),
            1900
        )
        socket.send(packet)
    }

    private fun receiveSsdpPacket(socket: DatagramSocket, timeoutInMillis: Int): List<String> {
        val gatewayList = mutableListOf<String>()

        while (true) {
            val receivedData = ByteArray(12)
            val packetToReceive = DatagramPacket(receivedData, receivedData.size)
            socket.soTimeout = timeoutInMillis
            try {
                socket.receive(packetToReceive)
                packetToReceive.address?.hostName?.let { gatewayList.add(it) }
            } catch (socketTimeout: SocketTimeoutException) {
                return gatewayList
            }
        }
    }
}

And this the test (includes the MulticastServer):

class DefaultGatewayDiscovererTest {

    @Test
    fun `discover gateways should return a list of gateway IPs`() = with(MulticastServer()) {
        start()

        val list = runBlocking { GatewayDiscoverer.instance.discoverGateways(1000) }

        close()
        assertThat(list.size).isEqualTo(1)
        assertThat(list).contains(InetAddress.getLocalHost().hostAddress)
        Unit
    }
}

/**
 * A "MulticastServer" which will join the
 * 239.255.255.250:1900 group to listen on SSDP events.
 * They will report back with the same package
 * it received.
 */
class MulticastServer : Thread(), Closeable {

    private val group = InetAddress.getByName("239.255.255.250")
    private val socket: MulticastSocket = MulticastSocket(1900)

    init {
        // This force to use IPv4...
        var netinterface: NetworkInterface? = null
        // Otherwise it will (at least on macOS) use IPv6 which leads to issues
        // while joining the group...
        val networkInterfaces = NetworkInterface.getNetworkInterfaces()
        while (networkInterfaces.hasMoreElements()) {
            val networkInterface = networkInterfaces.nextElement()
            val addressesFromNetworkInterface = networkInterface.inetAddresses
            while (addressesFromNetworkInterface.hasMoreElements()) {
                val inetAddress = addressesFromNetworkInterface.nextElement()
                if (inetAddress.isSiteLocalAddress
                    && !inetAddress.isAnyLocalAddress
                    && !inetAddress.isLinkLocalAddress
                    && !inetAddress.isLoopbackAddress
                    && !inetAddress.isMulticastAddress
                ) {
                    netinterface = NetworkInterface.getByName(networkInterface.name)
                }
            }
        }

        socket.joinGroup(InetSocketAddress("239.255.255.250", 1900), netinterface!!)
    }

    override fun run() {
        while (true) {
            val buf = ByteArray(256)
            val packet = DatagramPacket(buf, buf.size)
            try {
                socket.receive(packet)
            } catch (socketEx: SocketException) {
                break
            }
            // Print for debugging
            val message = String(packet.data, 0, packet.length)
            println(message)
            socket.send(packet)
        }
    }

    override fun close() = with(socket) {
        leaveGroup(group)
        close()
    }
}

When the test fails it fails on that line:

assertThat(list.size).isEqualTo(1)

The list is empty.

After some debugging I found out that the MulticastServer don't receive the message. Therefore the client don't get the response and add the IP address to the list.

I would expect that the MulticastServer will always work without that "flakiness". Do I something wrong with the implementation?

talex
  • 17,973
  • 3
  • 29
  • 66
StefMa
  • 3,344
  • 4
  • 27
  • 48
  • You need to join the group via all relevant network interfaces, not just the last one you found that matches your criteria. The 'print for debugging' code should be inside the `try` block, otherwise it is meaningless. – user207421 Apr 30 '19 at 06:59
  • When I try to join all networks I got a `SocketException: Can't assign requested address`. This happens with the `utun0` network (whatever this is ‍♂️). – StefMa Apr 30 '19 at 07:31
  • I carefully did not say 'all networks'. Read what I wrote. – user207421 Apr 30 '19 at 08:20
  • Ups. Sry for that :). But what are "relevant network interfaces"? For testing I have put a simply try/catch around `joinGroup()` and join each network interface where it's possible. It seems that it work at least on my mac for now. But I have still the same issue on the CI. – StefMa Apr 30 '19 at 08:36
  • The 'relevant' network interfaces are the ones that pass your five-line test. This seems obvious to me. – user207421 Apr 30 '19 at 08:42

0 Answers0