3

I am trying to implement a very simple upnp controller on linux, so that I can control a device which otherwise requires proprietary software.

The docs say that I need to send a UDP multicast request of a specific form (see the "M-SEARCH" string in the code below) to a specific address and port, and that devices will respond by UDP unicast to the address and port I sent from.

I can't make this work. tcpdump shows the UDP multicast request going to the correct address and port, and the format appears correct, but I cannot see a reply.

I am sending from and listening on the loopback interface (the device is on the same machine).

Another upnp controller (i.e. not mine) works properly on the loopback interface.

Can someone suggest what am I doing wrong?

Here is the code:

 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
 #include <errno.h>
 #include <sys/types.h>
 #include <sys/socket.h>
 #include <netinet/in.h>
 #include <netinet/udp.h>
 #include <unistd.h>
 #include <fcntl.h>

 #define MAXBUFSIZE 65536

int main(int argc, char ** argv ) {

unsigned char loop;
loop = 0;
unsigned char ttl;
ttl = 4;
int bcast;
bcast = 1;

int sock;

sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0) {
    perror("socket");
    exit(EXIT_FAILURE);
}

struct sockaddr_in destadd;
memset(&destadd, 0, sizeof(destadd));
destadd.sin_family = AF_INET;
destadd.sin_port = htons((uint16_t)1900);
if (inet_pton(AF_INET, "239.255.255.250", &destadd.sin_addr) < 1) {
    perror("inet_pton dest");
    exit(EXIT_FAILURE);
}

struct sockaddr_in interface_addr;
memset(&interface_addr, 0, sizeof(interface_addr));
interface_addr.sin_family = AF_INET;
interface_addr.sin_port = htons(0);
if (inet_pton(AF_INET, "127.0.0.1", &interface_addr.sin_addr) < 1) {
    perror("inet_pton interface");
    exit(EXIT_FAILURE);
}

if (setsockopt(sock, IPPROTO_IP, IP_MULTICAST_LOOP, &loop, sizeof(loop)) < 0){
    perror("setsockopt loop");
    exit(EXIT_FAILURE);
}

if (setsockopt(sock, IPPROTO_IP, IP_MULTICAST_TTL, &ttl, sizeof(ttl)) < 0){
    perror("setsockopt ttl");
    exit(EXIT_FAILURE);
}

if (setsockopt(sock, IPPROTO_IP, IP_MULTICAST_IF,
               (struct in_addr *)&interface_addr.sin_addr,
               sizeof(interface_addr.sin_addr)) < 0) {
    perror("setsockopt if");
    exit(EXIT_FAILURE);
}

if (setsockopt(sock, SOL_SOCKET, SO_BROADCAST, &bcast, sizeof(bcast)) < 0) {
    perror("setsockopt bcast");
    exit(EXIT_FAILURE);
}

struct ip_mreqn imr;
memset(&imr, 0, sizeof(imr));
if (inet_pton(AF_INET, "239.255.255.250", &imr.imr_multiaddr.s_addr) < 1) {
    perror("inet_pton");
    exit(EXIT_FAILURE);
}
inet_pton(AF_INET, "127.0.0.1", (struct in_addr *)&imr.imr_address);
imr.imr_ifindex = 0;
if (setsockopt(sock, IPPROTO_IP, IP_ADD_MEMBERSHIP,
               (void *)&imr, sizeof(imr)) < 0) {
    perror("setsockopt addmem");
    exit(EXIT_FAILURE);
}

if (bind(sock, (struct sockaddr *)&interface_addr,
    sizeof(struct sockaddr_in)) < 0) {
    perror("bind");
    exit(EXIT_FAILURE);
}

char buffer[1024];

strcpy(buffer, "M-SEARCH * HTTP/1.1\r\n"
                   "Host: 239.255.255.250:1900\r\n"
                   "Man: \"ssdp:discover\"\r\n"
                   "ST: upnp:rootdevice\r\n"
                   "MX: 3\r\n"
                   "User-Agent: Test/1.0\r\n"
                   "\r\n");

if (sendto(sock, buffer, strlen(buffer), 0, (struct sockaddr*)&destadd,
       sizeof(destadd)) < 0) {
    perror("sendto");
    exit(EXIT_FAILURE);
}

if (recvfrom(sock, &buffer, sizeof(buffer)-1, 0, NULL, NULL) < 0) {
    perror("recvfrom");
    exit(EXIT_FAILURE);
}


if (close(sock) < 0) {
    perror("close");
    exit(EXIT_FAILURE);
}

}
Tony
  • 1,645
  • 1
  • 10
  • 13
  • 1
    The device is on the localhost? How? Try removing the `bind()` step, and the `IP_MULTICAST_IF` step. – user207421 Oct 09 '14 at 11:42
  • To avoid firewall issues I am starting with a DLNA device which is in software (minidlna) and runs on the local system. Once I get that working I will try to use the real device on the LAN. minidlna responds from localhost to other DLNA controllers. Thank you for the suggested remedies. Unfortunately, if I remove the IP_MULTICAST_IF step, traffic from my program goes out on eth0. Removing bind alone, or both bind and IP_MULTICAST_IF, does not seem to solve the problem. – Tony Oct 10 '14 at 23:24
  • 1
    what is the output of route -n on your machine? – wick Oct 13 '14 at 15:30
  • Hi @wick. The output (with apologies for crummy formatting) is: `Destination Gateway Genmask Flags Metric Ref Use Iface` `0.0.0.0 192.168.1.1 0.0.0.0 UG 0 0 0 eth0` `192.168.1.0 0.0.0.0 255.255.255.0 U 1 0 0 eth0 ` – Tony Oct 14 '14 at 08:56
  • @Tony: according to this, a packet sent to 239.x.x.x will go via eth0, not loopback, right? It's just from my practice that 90% of troubleshooting multicast lie in routing tables... This is not an answer, but I would have a look in that direction, and add a static route for 238.0.0.0/8 – wick Oct 14 '14 at 13:48
  • @EJP: After re-reading your comment and doing some research, I'm still not quite sure of things but I have working code and I think I now understand a bit more what is happening. If I bind to INADDR_ANY the communication goes out on eth0. Then the reply comes back from the minidlna client (which is on the same host) and this comes via the loopback. I need to enable IP_MULTICAST_LOOP to get this to work (IP_MULTICAST_LOOP is not required for external clients). I will post working code in an answer, but if you want to post an answer I will be happy to give one of you or wick the credit. – Tony Oct 21 '14 at 09:13
  • @wick: see comment above. – Tony Oct 21 '14 at 09:29

2 Answers2

2

The code that worked for me in the end follows.

Summary of method:

  • Create a UDP socket
  • Enable IF_MULTICAST_LOOP (to allow communication with clients on the same host)
  • Set IF_MULTICAST_TTL
  • Bind to INADDR_ANY and port 8201 (port no. arbitrarily chosen)
  • Send multicast message to 239.255.255.250:1900
  • Receive reply using same socket

Here we go:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/udp.h>
#include <unistd.h>
#include <fcntl.h>

int main(int argc, char ** argv ) {

unsigned char loop;
loop = 1; // Needs to be on to get replies from clients on the same host
unsigned char ttl;
ttl = 4;
int bcast;
bcast = 1;

int sock;
sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0) {
    perror("socket");
    exit(EXIT_FAILURE);
}

// Multicast message will be sent to 239.255.255.250:1900
struct sockaddr_in destadd;
memset(&destadd, 0, sizeof(destadd));
destadd.sin_family = AF_INET;
destadd.sin_port = htons((uint16_t)1900);
if (inet_pton(AF_INET, "239.255.255.250", &destadd.sin_addr) < 1) {
    perror("inet_pton dest");
    exit(EXIT_FAILURE);
}

// Listen on all interfaces on port 8201 
struct sockaddr_in interface_addr;
memset(&interface_addr, 0, sizeof(interface_addr));
interface_addr.sin_family = AF_INET;
interface_addr.sin_port = htons(8201);
interface_addr.sin_addr.s_addr = htonl(INADDR_ANY);

// Got to have this to get replies from clients on same machine
if (setsockopt(sock, IPPROTO_IP, IP_MULTICAST_LOOP, &loop, sizeof(loop)) < 0){
    perror("setsockopt loop");
    exit(EXIT_FAILURE);
}

if (setsockopt(sock, IPPROTO_IP, IP_MULTICAST_TTL, &ttl, sizeof(ttl)) < 0){
    perror("setsockopt ttl");
    exit(EXIT_FAILURE);
}

// Bind to port 8201 on all interfaces
if (bind(sock, (struct sockaddr *)&interface_addr,
    sizeof(struct sockaddr_in)) < 0) {
    perror("bind");
    exit(EXIT_FAILURE);
}

char buffer[1024];

strcpy(buffer, "M-SEARCH * HTTP/1.1\r\n"
               "Host: 239.255.255.250:1900\r\n"
               "Man: \"ssdp:discover\"\r\n"
               "ST: upnp:rootdevice\r\n"
               "MX: 3\r\n"
               "User-Agent: Test/1.0\r\n"
               "\r\n");

if (sendto(sock, buffer, strlen(buffer), 0, (struct sockaddr*)&destadd,
       sizeof(destadd)) < 0) {
    perror("sendto");
    exit(EXIT_FAILURE);
}

if (recvfrom(sock, &buffer, sizeof(buffer)-1, 0, NULL, NULL) < 0) {
    perror("recvfrom");
    exit(EXIT_FAILURE);
}


printf("%s\n", buffer);

if (close(sock) < 0) {
    perror("close");
    exit(EXIT_FAILURE);
}
}

To get replies from external clients, ensure port 8201 is not blocked.

Tony
  • 1,645
  • 1
  • 10
  • 13
  • So what exacly is the difference between that and your original code? – user207421 Oct 21 '14 at 09:43
  • @EJP. Edited to indicate the changes. – Tony Oct 21 '14 at 10:05
  • Messages go out via whatever interface is indicated by the IP routing tables. It doesn't have anything to do with 'whatever the lowest numbered interface happens to be'. – user207421 Oct 21 '14 at 11:06
  • @EJP. I got myself confused by [this reference](https://www.cs.cmu.edu/~srini/15-441/F01.full/www/assignments/P2/htmlsim_split/node18.html). I now see [at least one other](http://www.linux.com/learn/docs/ldp/Multicast-HOWTO#ss2.4) which refers to the routing table (in the context of subscribing to groups). I have edited the answer to say less, so hopefully that reduces the chance for error. – Tony Oct 22 '14 at 06:54
0

As found in comments, the output of route -n:

0.0.0.0 192.168.1.1 0.0.0.0 UG 0 0 0 eth0 192.168.1.0 0.0.0.0 255.255.255.0 U 1 0 0 eth0

suggests that a packet sent to 239.x.x.x will go via eth0, not loopback.

So it would make sense to either add a static route to force outgoing packets on loopback interface, or make sure you can receive via loopback by means of IF_MULTICAST_LOOP flag.

wick
  • 1,995
  • 2
  • 20
  • 31
  • Would a static route work? When I run ifconfig it is not clear that lo is multicast-capable. Using IF_MULTICAST_LOOP works more directly, as I understand it, by putting the packet directly into the lo queue, rather than needing lo to spot that the multicast packet is to be added to the queue. Apologies if I am missing the point -- out of my depth. – Tony Oct 22 '14 at 06:58
  • @Tony: unfortunately, I can't test it as you have pretty specific configuration, but it is easy to add a static route like this with route command and check. man oages for route explain how. – wick Oct 22 '14 at 13:43
  • I tried adding the static route using `sudo ip route add 239.0.0.0/8 via 127.0.0.1`. The route was added and `ip route get to 239.255.255.250` produced `multicast 239.255.255.250 dev lo src 192.168.1.xxx`. However, multicast stopped working, with or without IF_MULTICAST_LOOP. – Tony Oct 23 '14 at 07:39
  • not quite, don't use via, try adding with this: route add -net 239.0.0.0 netmask 255.0.0.0 dev lo0 – wick Oct 23 '14 at 21:42
  • Thanks. I tried it, but got the same result -- no joy. IF_MULTICAST_LOOP without the static route does the job though. – Tony Oct 24 '14 at 10:13