pythonssdp

Python sockets SSDP search working inconsistently on Windows


Goal

I am writing a python windows application that needs to search for available network devices using ssdp. The search will be used to get the IP addresses for specific device types to set up a connection.

Problem

We were getting very inconsistent ssdp search results. Sometimes it works, sometimes it fails. Using Wireshark and Device Sniffer (from Meshcommander UPnP tools), we discovered that when it fails, the ssdp M-SEARCH is getting sent to 127.0.0.1, so it never reaches the network. There appears to be no rhyme or reason for when it works or fails. I am confident this is specific to windows, as the same code works on Mac without issue.

Code that works intermittently:

import socket
import struct
import time

local_ip = '192.168.20.48'
ssdp_addr = "239.255.255.250"
ssdp_port = 1900
searchterm = 'ssdp:all'

m_msg = f"""\
M-SEARCH * HTTP/1.1\r\n\
Host: {ssdp_addr}:{ssdp_port}\r\n\
Man: "ssdp:discover"\r\n\
ST: {searchterm}\r\n\
MX: 2\r\n\r\n""".encode("utf-8")

if __name__ == "__main__":
    # Method 1, occassionally works. Most of the time sends to 127.0.0.1:port
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 5)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
    sock.settimeout(5)
    sock.sendto(m_msg, (ssdp_addr, ssdp_port)) # Sometimes works, sometimes doesn't...

    # If send worked, this will receive the response. But, if send is routed to 127.0.0.1,
    # nothing is received (which is expected given it is localhost)
    tLim = time.time() + 6
    while time.time() < tLim:
        try:
            data, addr = sock.recvfrom(4096)
            print(f"Received from {addr}: {data}")
        
        except socket.timeout:
            break

    sock.close()

This issue also occurs with the ssdpy library on PyPi, which is not surprising as the source of the library is similar to the above implementation.

Attempted Solution

I have experimented with every type of windows configuration I can think of (Firewall settings, services, permissions, switching networks...) to no avail. After searching online I found a method where the sent M-Search message no longer gets routed to 127.0.0.1, but I am unable to receive the response from the device! With wireshark, I can see the response is sent by the device, but the code wont read it. Although the implementation WILL find the random ssdp traffic that is occurring at the same time on the network.

import socket
import struct
import time

local_ip = '192.168.20.48'
ssdp_addr = "239.255.255.250"
ssdp_port = 1900
searchterm = 'ssdp:all'

m_msg = f"""\
M-SEARCH * HTTP/1.1\r\n\
Host: {ssdp_addr}:{ssdp_port}\r\n\
Man: "ssdp:discover"\r\n\
ST: {searchterm}\r\n\
MX: 2\r\n\r\n""".encode("utf-8")

if __name__ == "__main__":
    # Method 2
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    sock.bind((local_ip, ssdp_port))
    
    group = socket.inet_aton(ssdp_addr)
    mreq = struct.pack('4s4s', group, socket.inet_aton(local_ip))
    sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
    sock.settimeout(5)
    
    ret = sock.sendto(m_msg, (ssdp_addr, ssdp_port))

    # CODE to listen for SSDP responses. This finds unrelated network upnp notifies, but 
    # does not get the response from the device. I can see the response from the device is 
    # being sent with wireshark
    tLim = time.time() + 6
    while time.time() < tLim:
        try:
            data, addr = sock.recvfrom(4096)
            print(f"Received from {addr}: {data}")
        
        except socket.timeout:
            break
        
    sock.close()

With this method, Wireshark shows the M-SEARCH with Source/Destination = 192.168.20.48 / 239.255.255.250 and the response from the device Source/Destination = 192.168.30.82 / 192.168.20.48

Where 192.168.20.48 = PC local IP, and 192.168.30.82 = device local IP.

Help

I am wondering if there are changes I can make to either of these methods to make them work consistently. The first method works part of the time, some of the time. The second method half works all the time. I am open to using a modified version of either of these methods, or a totally new one if I am completely on the wrong track.


Solution

  • As you suspected, you are seeing an inconsistent device discovery on Windows because your SSDP M-SEARCH messages are intermittently being sent to 127.0.0.1 (localhost) instead of the intended multicast address 239.255.255.250.

    You need to follow these steps:

    1. Identify the Primary Local IP Address
    2. Configure the UDP Socket for Multicast
    3. Send the M-SEARCH Message
    4. Listen for Responses

    I tested this and saw my VirtualBox bridge as the found primary IP. Not shown below, I added a search for all local interfaces.

    import socket
    import struct
    import time
    
    def get_primary_ip(target_subnet=''):
        local_ips = socket.gethostbyname_ex(socket.gethostname())[2]
        for ip in local_ips:
            if ip.startswith(target_subnet):
                return ip
        return '127.0.0.1'
    
    def send_ssdp_discover(search_target="ssdp:all", mx=2, target_subnet=''):
        ssdp_addr = "239.255.255.250"
        ssdp_port = 1900
        m_search = f"""M-SEARCH * HTTP/1.1\r
    HOST: {ssdp_addr}:{ssdp_port}\r
    MAN: "ssdp:discover"\r
    ST: {search_target}\r
    MX: {mx}\r
    \r
    """.encode('utf-8')
        # Get the primary LAN IP within the target subnet
        primary_ip = get_primary_ip(target_subnet)
        if primary_ip == '127.0.0.1':
            print(f"No active interface found in the {target_subnet} subnet.")
            return
        print(f"Primary LAN IP Address: {primary_ip}")
        # Create a UDP socket for Multicast
        sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
        try:
            sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
            ttl = struct.pack('b', 2)
            sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl)
            local_interface = socket.inet_aton(primary_ip)
            sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_IF, local_interface)
            sock.bind((primary_ip, 0))
            print(f"Using primary IP: {primary_ip}")
            # Send the M-SEARCH message
            sock.sendto(m_search, (ssdp_addr, ssdp_port))
            print(f"Sent M-SEARCH from {primary_ip} to {ssdp_addr}:{ssdp_port}")
            # Listen for responses
            sock.settimeout(mx + 1)
            start_time = time.time()
            while True:
                try:
                    data, addr = sock.recvfrom(65507)
                    print(f"Received response from {addr}:\n{data.decode('utf-8')}\n")
                except socket.timeout:
                    print("No more responses.")
                    break
                if time.time() - start_time > mx + 1:
                    break
        except Exception as e:
            print(f"An error occurred: {e}")
        finally:
            sock.close()
    
    if __name__ == "__main__":
        send_ssdp_discover(target_subnet='192.168.30') #Example, this is my local subnet.  Call with yours.