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.
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.
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.
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.
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:
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.