Skip to content

Commit

Permalink
cleanup, and it actually works (sort of) now :)
Browse files Browse the repository at this point in the history
  • Loading branch information
reinhrst committed Dec 31, 2013
1 parent 5bc58e3 commit e077056
Show file tree
Hide file tree
Showing 5 changed files with 209 additions and 50 deletions.
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
all:
gcc sonos-forwarder.c str_replace.c -o sonos-forwarder
gcc sonos-init-forwarder.c -o sonos-init-forwarder
gcc notify-forwarder.c str_replace.c -o notify-forwarder
31 changes: 23 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ 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 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. In my examples this is 192.168.2.101.
* 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. In my examples this device has the IP addresses 192.168.1.82 on the outside and 192.168.2.1 on the inside.
* Sonos controller/Sonos client (client): These are the devices you use to control your Sonos, in my case an iPad, iPhone, Mac. These are in 192.168.1.0/24 network.

Sonos Discovery protocol
------------------------
Expand All @@ -36,15 +36,30 @@ An SSDP message is a UDP packet to broadcast address 239.255.255.250 on port 190
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.

In addition there is a problem that the sonos box will send NOTIFY http calls to the client on port 340x (possibly even 34xx, actually I only ever saw it use 3400 for desktop clients and 3401 for mobile clients. For safety the instructions below reserver the whole 3400-3500 range).
This NOTIFY call contains the ip address of the sonos box, which, of course, is not reachable from the network of the client.

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.
* Redirect all traffic from the sonos box to the network outside of the NAT router (but not the internet) for the ports 3400-3500 to the router itself on port 3400.
* Listen on the NAT device on the external interface for SSDP packets. As soon as we find one, send this through to the internal network.
* As soon as we retransmit an SSDP packet, listen on the transmitting port for a reply. If we receive one, retransmit it on the other side, to the correct host. These reply packets should have their content rewritten, so that the ip address of the sonos box gets replaced by the ip address of the
* 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
* Make sure NATting works, including the port forwarding. Consult the Internet to do this. You can check that it's set up right by connecting to http://(router external ip):1400/xml/devicedescription.xml. This should show you a bunch of information in XML about your Sonos system.
* Redirect all traffic from the sonos box to the network outside of the NAT router (but not the internet) for the ports 3400-3500 to the router itself on port 3400:

-A PREROUTING -i eth0 -p tcp -s 192.168.2.101 -d 192.168.1.0/24 -m multiport --dports 3400:3499 -j DNAT --to-destination 192.168.2.1:3400

* compile this program

make all

* run in 2 windows the 2 executables sonos-forwarder and notify-forwarder. Note: you only need to run the sonos-init-forwarder if you want to pair a new device. Obviously you can run the programs headless with nohup, just watch out for your disk filling up.
* enjoy the music

Caveat
------
For now (testing time about 25 minutes :) ) everything seems to be working fine.
103 changes: 79 additions & 24 deletions notify-forwarder.c
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@
#include <time.h>
#include <unistd.h>
#include "str_replace.h"
#include <errno.h>

#define NOTIFY_PORT 3400
#define RECEIVE_BUFFER_SIZE 1E5
#define REPLY_TIMEOUT_SEC 5
#define MAX_HEADERS_SIZE 10000
#define REPLY_TIMEOUT_USEC 5E5
#define LISTEN_BACKLOG 5
#define HOST_LINE "\r\nHOST: "
#define CONTENT_LENGTH_LINE "\r\nCONTENT-LENGTH:"

void fill_socketaddr_in(struct sockaddr_in* sock_address, uint16_t port, const char* ip_address) {
sock_address->sin_family = AF_INET;
Expand Down Expand Up @@ -42,27 +44,73 @@ int create_bounded_tcp_socket(struct sockaddr_in sock_address) {
}

char* get_host(const char* buffer) {
char* host_start = strstr(buffer, HOST_LINE) + strlen(HOST_LINE);
char* host_end = strstr(host_start, ":");
char* host = malloc((host_end - host_start + 1));
strncpy(host, host_start, host_end - host_start);
host[host_end - host_start] = '\0';
return host;
char* host_start = strstr(buffer, HOST_LINE) + strlen(HOST_LINE);
char* host_end = strstr(host_start, ":");
char* host = malloc((host_end - host_start + 1));
strncpy(host, host_start, host_end - host_start);
host[host_end - host_start] = '\0';
return host;
}

unsigned short get_content_length(const char* buffer) {
char* cl_start = strstr(buffer, CONTENT_LENGTH_LINE);
if (!cl_start) {
return 0;
}
return (unsigned short) strtol(cl_start + strlen(CONTENT_LENGTH_LINE), NULL, 10);
}

unsigned short get_port(const char* buffer) {
char* host_start = strstr(buffer, HOST_LINE) + strlen(HOST_LINE);
char* host_end = strstr(host_start, ":");
return (unsigned short) strtol(host_end + 1, NULL, 10);
char* host_start = strstr(buffer, HOST_LINE) + strlen(HOST_LINE);
char* host_end = strstr(host_start, ":");
return (unsigned short) strtol(host_end + 1, NULL, 10);
}


char* receive_http(int conn) {
int nr_bytes_received=0;
char* headers = malloc(MAX_HEADERS_SIZE * sizeof(char));
while(1) {
if (nr_bytes_received == MAX_HEADERS_SIZE) {
headers[MAX_HEADERS_SIZE - 1] = '\0';
fprintf(stderr, "No header: %s\n", headers);
exit(1);
}

int nr_bytes = recvfrom(conn, headers + nr_bytes_received, MAX_HEADERS_SIZE - nr_bytes_received - 1, 0, NULL, NULL);
if (nr_bytes < 0) {
free(headers);
return NULL;
}
nr_bytes_received += nr_bytes;
headers[nr_bytes_received] = '\0';
if (strstr(headers, "\r\n\r\n")) {
break;
}
usleep(5E4);
}
int header_length = strstr(headers, "\r\n\r\n") - headers + 4;
int body_length = get_content_length(headers);
int alloc_size = header_length + body_length + 1;
char* data = realloc(headers, alloc_size * sizeof(char));
data[alloc_size-1] = '\0';
while (nr_bytes_received != header_length + body_length) {
int nr_bytes = recvfrom(conn, data+nr_bytes_received, alloc_size-nr_bytes_received, 0, NULL, NULL);
usleep(5E4);
if (nr_bytes < 0) {
free(headers);
return NULL;
}
nr_bytes_received += nr_bytes;
}
return data;
}

int main(int argc, char** argv) {
int notify_socket, forward_socket, sonos_connection;
struct sockaddr_in notify_socket_address = {0}, forward_destination_address = {0}, sonos_box_address;
socklen_t sonos_box_address_length;
char* receive_buffer=malloc(RECEIVE_BUFFER_SIZE * sizeof(char));
struct timeval timeout = {REPLY_TIMEOUT_SEC, 0};
socklen_t sonos_box_address_length=sizeof(sonos_box_address);
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]);
Expand All @@ -77,13 +125,15 @@ int main(int argc, char** argv) {

listen(notify_socket, LISTEN_BACKLOG);

printf("waiting for connection\n");
while (1) {
int nr_bytes_receieved;
printf("waiting for connection\n");
sonos_connection = accept(notify_socket, (struct sockaddr *)&sonos_box_address, &sonos_box_address_length);
usleep(5E5); //sleep for the complete request to be available
nr_bytes_receieved = recv(sonos_connection, receive_buffer, RECEIVE_BUFFER_SIZE, 0);
char* send_buffer = str_replace(receive_buffer, sonos_box_ip_address, outside_nat_ip_address);
char* request = receive_http(sonos_connection);
if (!request) {
printf("strange, no request.... %s\n", strerror(errno));
continue;
}
char* send_buffer = str_replace(request, sonos_box_ip_address, outside_nat_ip_address);
char* host = get_host(send_buffer);
unsigned short port = get_port(send_buffer);
fill_socketaddr_in(&forward_destination_address, port, host);
Expand All @@ -92,15 +142,20 @@ int main(int argc, char** argv) {
perror("Error");
}
connect(forward_socket, (struct sockaddr*) &forward_destination_address, sizeof(forward_destination_address));
printf("forwarding %s\n", send_buffer);
printf("connection %s --> %s(%d): forwarding %ld bytes...", inet_ntoa(sonos_box_address.sin_addr), host, port, strlen(send_buffer));
send(forward_socket, send_buffer, strlen(send_buffer), 0);
usleep(5E5);
nr_bytes_receieved = recv(forward_socket, receive_buffer, RECEIVE_BUFFER_SIZE, 0);
printf("replying %s\n", receive_buffer);
send(sonos_connection, receive_buffer, strlen(receive_buffer), 0);
char* response = receive_http(forward_socket);
if (!response) {
printf("no response (%s)\n", strerror(errno));
continue;
}
printf("replying %ld bytes\n", strlen(response));
send(sonos_connection, response, strlen(response), 0);
close(forward_socket);
close(sonos_connection);
free(host);
free(send_buffer);
free(request);
free(response);
}
}
29 changes: 11 additions & 18 deletions sonos-forwarder.c
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ 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;
struct sockaddr_in remote_address;
socklen_t remote_address_length = sizeof(remote_address);
char* receive_buffer=malloc(RECEIVEBUFFER_SIZE * sizeof(char));
struct ip_mreq mreq;
struct timeval timeout = {0, REPLY_TIMEOUT_USEC};
Expand Down Expand Up @@ -83,12 +83,9 @@ int main(int argc, char** argv) {

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);

nr_bytes_received = recvfrom(ssdp_socket, receive_buffer, RECEIVEBUFFER_SIZE, 0, (struct sockaddr*)&remote_address, &remote_address_length);
if(strstr(receive_buffer, "ZonePlayer") != NULL) {
printf("forwarding message\n");
printf("Message %s --> internal\n", inet_ntoa(remote_address.sin_addr));
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);
Expand All @@ -100,18 +97,14 @@ int main(int argc, char** argv) {
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;
if ((mark = strstr(receive_buffer, to_replace)) != NULL) {
int send_buffer_size = nr_bytes_received - strlen(to_replace) + strlen(replace_with) + 1;
char* send_buffer = str_replace(receive_buffer, to_replace, replace_with);
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);
free(send_buffer);
} else {
printf("Message didn't match \"%s\"", to_replace);
}
int send_buffer_size = nr_bytes_received - strlen(to_replace) + strlen(replace_with) + 1;
char* send_buffer = str_replace(receive_buffer, to_replace, replace_with);
sendto(forward_reply_socket, send_buffer, send_buffer_size, 0, (struct sockaddr*) &remote_address, remote_address_length);
free(send_buffer);
printf("Message %s <-- internal\n", inet_ntoa(remote_address.sin_addr));
}
} else {
printf("Message %s not routed\n", inet_ntoa(remote_address.sin_addr));
}
}
}
95 changes: 95 additions & 0 deletions sonos-init-forwarder.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
#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>
#include <unistd.h>
#include "str_replace.h"

#define RECEIVEBUFFER_SIZE 1E3
#define INIT_PORT 6969
#define INIT_GROUP "0.0.0.0"

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 init_socket;
struct sockaddr_in init_socket_address = {0}, forward_reply_socket_address = {0}, destination_socket_address = {0};

struct sockaddr_in remote_address;
socklen_t remote_address_length = sizeof(remote_address);
char* receive_buffer=malloc(RECEIVEBUFFER_SIZE * sizeof(char));


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(&init_socket_address, INIT_PORT, INIT_GROUP);

init_socket = create_bounded_socket(init_socket_address);

printf("waiting for connection\n");
while (1) {
int nr_bytes_received;
nr_bytes_received = recvfrom(init_socket, receive_buffer, RECEIVEBUFFER_SIZE, 0, (struct sockaddr*)&remote_address, &remote_address_length);
if (remote_address.sin_addr.s_addr == htonl(inet_network(outside_nat_ip_address)) || remote_address.sin_addr.s_addr == htonl(inet_network(inside_nat_ip_address))) {
continue; //loopback
}

long r_addr = ntohl(remote_address.sin_addr.s_addr);
fill_socketaddr_in(&forward_reply_socket_address, 0, inside_nat_ip_address);
if ((r_addr & 0xFFFFFF00) == (ntohl(forward_reply_socket_address.sin_addr.s_addr) & 0xFFFFFF00)) {
fill_socketaddr_in(&forward_reply_socket_address, 0, outside_nat_ip_address);
fill_socketaddr_in(&destination_socket_address, INIT_PORT, outside_nat_ip_address);
} else {
fill_socketaddr_in(&destination_socket_address, INIT_PORT, inside_nat_ip_address);
}
destination_socket_address.sin_addr.s_addr = htonl(ntohl(destination_socket_address.sin_addr.s_addr) & 0xFFFFFF00 | 0xFF);
printf("Message %s --> ", inet_ntoa(remote_address.sin_addr));
printf("%s", inet_ntoa(forward_reply_socket_address.sin_addr));
printf("(%s)\n", inet_ntoa(destination_socket_address.sin_addr));

int passthrough_socket = create_bounded_socket(forward_reply_socket_address);
int broadcastEnable=1;
if (setsockopt(passthrough_socket, SOL_SOCKET, SO_BROADCAST, &broadcastEnable, sizeof(broadcastEnable)) < 0) {
perror("setopt failed");
}

if (sendto(passthrough_socket, receive_buffer, nr_bytes_received, 0, (struct sockaddr*)&destination_socket_address, sizeof(destination_socket_address)) <= 0) {
perror("send failed");
}
close(passthrough_socket);
}
}

0 comments on commit e077056

Please sign in to comment.