Monday, January 02, 2017

Messing around in UPnP with socat 

For the last year I've been working on a UPnP server framework in Ruby.  Mostly I've only been able to do this in short bursts punctuated by long periods of nothing - as the github statistics will show.  But over the quiet bit between Christmas and New Year I've been able to make some progress and the server will now respond to SSDP search requests.

To test it I've been using gupnp-universal-cp which is a gui that can act as a client for any UPnP server.  But it's not (or not easily) scriptable so I can't write any automated tests.  Thanks to Javier Lopez I've found a scriptable way, the socat tool which basically pings network packets at an address and optionally stores the response.

My first attempt was the command

socat -T5 -t5 STDIO UDP4-DATAGRAM:239.255.255.250:1900,ttl=4,sourceport=54321 < socat.in > search.out

which sends the contents of the socat.in file to the UPnP multicast address, the idea being any servers out there would send a Discovery response which gets put into search.out.  You need to pick a random, free port (I chose 54321) for the server to respond to. 

socat.in contained a standard M-SEARCH request (everything between the two lines below)
_____________________________________________________________
M-SEARCH * HTTP/1.1
HOST: 239.255.255.250:1900
MAN: "ssdp:discover"
MX: 1
ST: ssdp:all

 

_____________________________________________________________


However I found that only some of the servers I was expecting to respond were responding.  I ran the socat command again wrapped in strace and saw that the responses were being generated and picked up by socat, but weren't being dumped in the output file.

Looking at the socat documentation and the strace output in detail the reason for this is that some UPnP servers will respond to an M-SEARCH from port 1900 and some from a random, ephemeral port.  The UPnP standard doesn't say which is correct, but the socat command will only process responses from source port 1900 even if they are sent to the correct destination port (54321 in this case).

I'm not sure if this behaviour can be overridden in socat, to work around it I tried the following

socat -T5 -t5 STDIO UDP4-SENDTO:239.255.255.250:1900,ttl=4,sourceport=54322,reuseaddr < socat.in & socat -T5 -t5 STDIO UDP4-RECV:54322,reuseaddr > socat.out2

This runs socat twice, once to send the M-SEARCH requests and then immediately starts a second instance to receive them from any source port.  This worked much better with every UPnP server on my network sending a response that was recorded in socat.out2