Skip to content

Commit

Permalink
forwards the SSDP packets. Not done yet, still need to mangle the por…
Browse files Browse the repository at this point in the history
…t 3400 notify packets
  • Loading branch information
reinhrst committed Dec 30, 2013
1 parent b1cac89 commit c1652b7
Show file tree
Hide file tree
Showing 2 changed files with 165 additions and 0 deletions.
46 changes: 46 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,49 @@ sonos-forwarder
===============

Forwards the sonos SSDP packets across a nat device

This tools is useful in case you have a nat device able to run this program (such as a linux box), with a Sonos device behind it.
Problem is that in this case normally Sonos clients on the outside of the nat cannot find the Sonos box.

Practical problem
-----------------

In my case I had a Sonos device, however I am only able to connect wifi devices to the network I am on.
An obvious solution would be to connect a wifi bridge (one that doesn't do NAT), however the access point was protected, it wouldn't allow wifi bridges, it wouldn't connect to anything that had 4addr set to on.
In addition, I can imagine the network operators frowning on the idea of me connecting a Sonos to their network.

To solve this problem I set up a Raspberry Pi as a NAT device, NATting from wlan0 to eth0 (I used the instructions from [here](http://hackhappy.org/uncategorized/how-to-use-a-raspberry-pi-to-create-a-wireless-to-wired-network-bridge/) to configure the Raspberry Pi. Please note that there is a typo in the page, the last line of /etc/network/interfaces should read "up iptables-restore < /etc/iptables.ipv4.nat", with a < in stead on a >).
Problem is that all my controllers are on the other side of the NAT, so now the Sonos clients (on my mac, iPad, etc), can't connect to the Sonos system any more.


Terminology
-----------

Just to make the terms clear, my network consists of the following:

* Sonos box (box): the device that connects Sonos to the internet. In my case this is a Play:1, but this can be Sonos:bridge or any Sonos component. It's the thing you bought from Sonos, in which you plug your ethernet cable.
* NAT router: In my case a Raspberry Pi. It has a wlan0 interface, connecting to the wider network, and an eth0 interface connected to the Sonos. In my case this is a direct connection from the Pi to the Sonos, but putting a switch in between here, or daisy chaining other network devices behind your Sonos, are all not a problem. The network on the Sonos side is called the network inside the NAT, the network on the other side connecting to the Internet is called outside the NAT.
* Sonos controller/Sonos client (client): These are the devices you use to control your Sonos, in my case an iPad, iPhone, Mac.

Sonos Discovery protocol
------------------------

To understand why this is, we have to look at the protocol used to discover a Sonos device.
This leans on [SSDP](http://en.wikipedia.org/wiki/Simple_Service_Discovery_Protocol).
The Sonos box regularly sends SSDP packets to announce its presence, and when a client wants to connect to a box it sends out SSDP messages as well.
An SSDP message is a UDP packet to broadcast address 239.255.255.250 on port 1900.
A client trying to connect expects a message back from the Sonos box, a unicast UDP message on the UDP port it used to send the broadcast, in which the Sonos box makes itself known, and gives the client a HTTP address where to connect to it.
This HTTP address is on port 1400.

What we need to do to route this protocol is 4 steps (after we have the NAT set up).
* Do NAT port forwarding on port 1400 to the Sonos device.
* Transmit any SSDP packets from one side of the network to the other: listen for SSDP packets on both interfaces, and as soon as we find one, retransmit it on the other side.
* As soon as we transmit a UDP packet, listen on the tranmiting port for a reply. If we receive one, transmit it on the other side.
* Any message going from the inside of the NAT to the outside, should rewrite the IP addresses in the packet. For instance, if the box is on 192.168.2.100, and the client on 192.168.1.20, and the NAT router's outside IP address is 192.168.1.30, then a message from the box saying "Hey connect to me on 192.168.2.100" means nothing to the client because it doesn't know how to route packets to 192.168.2.x. So the router must rewrite the message to "Hey connect to me on 192.168.1.30". The NAT port forwarding will then take care of the rest.

Solution
--------
* Make sure NATting works
* Do port forwarding
* compile and run this little C program
* enjoy the music
119 changes: 119 additions & 0 deletions sonos-forwarder.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <string.h>
#include <time.h>

#define RECEIVEBUFFER_SIZE 1E3
#define SSDP_PORT 1900
#define SSDP_GROUP "239.255.255.250"
#define REPLY_TIMEOUT_USEC 5E5
#define SONOS_LOCATION_TEMPLATE "\r\nLOCATION: http://%s:1400/xml/device_description.xml\r\n"

inline char* sonos_location_string(char *ip) {
char* temp;
asprintf(&temp, SONOS_LOCATION_TEMPLATE, ip);
return temp;
}

void fill_socketaddr_in(struct sockaddr_in* sock_address, uint16_t port, const char* ip_address) {
sock_address->sin_family = AF_INET;
sock_address->sin_port = htons(port);

if (inet_pton(AF_INET, ip_address, &sock_address->sin_addr) != 1) {
fprintf(stderr, "Not a valid address: %s\n", ip_address);
exit(1);
}
}

int create_bounded_socket(struct sockaddr_in sock_address) {
int sock;

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

if (bind(sock, (struct sockaddr *)&sock_address, sizeof(sock_address)) < 0) {
perror("bind failed");
exit(1);
}

return sock;
}

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

int ssdp_socket, forward_reply_socket;
struct sockaddr_in ssdp_socket_address = {0}, forward_reply_socket_address = {0};
struct sockaddr remote_address;
socklen_t remote_address_length;
char* receive_buffer=malloc(RECEIVEBUFFER_SIZE * sizeof(char));
struct ip_mreq mreq;
struct timeval timeout = {0, REPLY_TIMEOUT_USEC};


if (argc != 4) {
fprintf(stderr, "Call with 3 arguments:\n%s external-interface-ip-address internal-interface-ip-address sonos-box-ip-address", argv[0]);
}
char* outside_nat_ip_address = argv[1];
char* inside_nat_ip_address = argv[2];
char* sonos_box_ip_address = argv[3];

fill_socketaddr_in(&ssdp_socket_address, SSDP_PORT, SSDP_GROUP);
fill_socketaddr_in(&forward_reply_socket_address, SSDP_PORT, outside_nat_ip_address);

ssdp_socket = create_bounded_socket(ssdp_socket_address);
forward_reply_socket = create_bounded_socket(forward_reply_socket_address);
inet_pton(AF_INET, argv[1], &mreq.imr_interface);
mreq.imr_multiaddr.s_addr = inet_addr(SSDP_GROUP);

if (setsockopt(ssdp_socket, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) < 0) {
perror("setsockopt mreq");
exit(1);
}

while (1) {
int nr_bytes_received;
printf("a\n");
nr_bytes_received = recvfrom(ssdp_socket, receive_buffer, RECEIVEBUFFER_SIZE, 0, &remote_address, &remote_address_length);
printf("received message from outside: \n%s\n\n", receive_buffer);

if(strstr(receive_buffer, "ZonePlayer") != NULL) {
printf("forwarding message\n");
struct sockaddr_in passthrough_socket_address = {0};
fill_socketaddr_in(&passthrough_socket_address, 0, inside_nat_ip_address);
int passthrough_socket = create_bounded_socket(passthrough_socket_address);
sendto(passthrough_socket, receive_buffer, nr_bytes_received, 0, (struct sockaddr*)&ssdp_socket_address, sizeof(ssdp_socket_address));
if (setsockopt(passthrough_socket, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)) < 0) {
perror("Error");
}
nr_bytes_received = recv(passthrough_socket, receive_buffer, RECEIVEBUFFER_SIZE, 0);
if (nr_bytes_received <= 0) {
printf("no reply\n");
} else {
printf("received reply (%d) from inside: \n%s\n\n", nr_bytes_received, receive_buffer);
char* mark;
char* to_replace = sonos_location_string(sonos_box_ip_address);
char* replace_with = sonos_location_string(outside_nat_ip_address);
if ((mark = strstr(receive_buffer, to_replace)) != NULL) {
int send_buffer_size = nr_bytes_received - strlen(to_replace) + strlen(replace_with);
char* send_buffer = malloc(send_buffer_size * sizeof(char));
strncpy(send_buffer, receive_buffer, mark - receive_buffer);
strcpy(send_buffer + (mark - receive_buffer), replace_with);
mark += strlen(to_replace) - 1; //remove the terminating \0
strcpy(send_buffer + (mark - receive_buffer) - strlen(to_replace) + strlen(replace_with), mark);

//sendbuffer contains the rewriten package now
printf("forwarding reply (%d) from inside: \n%s\n\n", nr_bytes_received, send_buffer);
sendto(forward_reply_socket, send_buffer, send_buffer_size, 0, (struct sockaddr*) &remote_address, remote_address_length);
} else {
printf("Message didn't match \"%s\"", to_replace);
}
}
}
}
}

0 comments on commit c1652b7

Please sign in to comment.