Sonos speakers on a dedicated subnet/VLAN

There are many reasons why one might want to have a Sonos speaker on a different subnet than the controller. People have asked about this before, but the answers are either wrong (“it’s not possible”), buried in the thread or lack detail.

Here I’m providing more detail and will explain

Disclaimer: I have only tested with an already configured Sonos system. I have not tested yet which configuration is necessary to add a new speaker directly on the other subnet.

In the following examples, host 192.168.0.5 is running the Sonos controller software, host 192.168.1.10 is a speaker.

UPnP and SSDP

Sonos uses Universal Plug and Play (UPnP) to announce and find speakers. UPnP defines protocols for devices to communicate with each other without prior configuration. It groups the protocols into a series of steps. The steps interesting to us are:

Looking at the messages exchanged between controller and speakers related to these steps will let us know which ports need to be configured on our firewall.

Step 1: Discovery

The UPnP specification describes this step as

When a device is added to the network, the UPnP discovery protocol allows that device to advertise its services to control points on the network. Similarly, when a control point is added to the network, the UPnP discovery protocol allows that control point to search for devices of interest on the network.
[…]

This step is performed by the Simple Service Discovery Protocol (SSDP). For our purposes we only need to look at the packages sent by a control device.

The controller periodically sends the same search request to find speakers to two addresses:

$ tcpdump -n -# -c 6 -t -r sonos.pcapng "port 1900"  
    1  IP 192.168.0.5.1901 > 239.255.255.250.1900: UDP, length 275  
    2  IP 192.168.0.5.1901 > 255.255.255.255.1900: UDP, length 275  
    3  IP 192.168.0.5.1901 > 255.255.255.255.1900: UDP, length 248  
    4  IP 192.168.0.5.1901 > 239.255.255.250.1900: UDP, length 248  
    5  IP 192.168.0.5.1901 > 239.255.255.250.1900: UDP, length 275  
    6  IP 192.168.0.5.1901 > 255.255.255.255.1900: UDP, length 275  
    7  IP 192.168.1.10.54038 > 192.168.0.5.1901: UDP, length 496  
    ...  

255.255.255.250 is the local broadcast address, which is not routable, so that doesn’t help us. 239.255.255.250 is more interesting for us: It’s the IP multicast address reserved for SSDP, which can be routed. I will explain IP multicast in the next section.

For now we know that packets to address 239.255.255.250 (or to port 1900) and packets from speakers to controllers to port 1901 must be allowed to pass the Firewall.

SSDP messages use the same format as HTTP 1.1 header fields. When a controller app starts, it sends the following message (... represents removed potentially sensitive information):

M-SEARCH * HTTP/1.1  
HOST: 239.255.255.250:1900  
MAN: "ssdp:discover"  
MX: 1  
ST: urn:schemas-upnp-org:device:ZonePlayer:1  
USER-AGENT: Linux UPnP/1.0 Sonos/47.2-59120 (...)  
X-SONOS-DEVICEID: ...  
X-SONOS-SESSIONSECONDS: 0  
X-SONOS-MDPMODEL: 4  

This requests consists mostly of standard UPnP fields. The ST: urn:schemas-upnp-org:device:ZonePlayer:1 indicates that the controller is looking for ZonePlayer (Sonos) devices.

An example of a speaker’s response is

HTTP/1.1 200 OK  
CACHE-CONTROL: max-age = 1800  
EXT:  
LOCATION: http://192.168.1.10:1400/xml/device_description.xml  
SERVER: Linux UPnP/1.0 Sonos/47.2-59120 (ZPS9)  
ST: urn:schemas-upnp-org:device:ZonePlayer:1  
USN: uuid:RINCON_...::urn:schemas-upnp-org:device:ZonePlayer:1  
X-RINCON-HOUSEHOLD: ...  
X-RINCON-BOOTSEQ: 95  
X-RINCON-WIFIMODE: 0  
X-RINCON-VARIANT: 0  
HOUSEHOLD.SMARTSPEAKER.AUDIO: ...  

which brings us to the next step.

Step 2: Description

The UPnP specification describes this step as

After a control point has discovered a device, the control point still knows very little about the device. For the control point to learn more about the device and its capabilities, or to interact with the device, the control point shall retrieve the device’s description from the URL provided by the device in the discovery message.
[…]

This URL is the value of the LOCATION field in the response:

LOCATION: http://192.168.1.10:1400/xml/device_description.xml  

This means controllers must be able to access port 1400 on speakers to learn more about their capabilities.

Step 4: Eventing

The UPnP specification describes this step as:

A UPnP description for a service includes a list of actions the service responds to and a list of variables that model the state of the service at run time. The service publishes updates when these variables change, and a control point may subscribe to receive this information.
[…]

To subscribe to a service, a controller sends a SUBSCRIBE HTTP request to the URL found in the speaker’s device description. For example, if the device description contains

<service>  
  <serviceType>  
    urn:schemas-upnp-org:service:MusicServices:1  
  </serviceType>  
  <serviceId>  
    urn:upnp-org:serviceId:MusicServices  
  </serviceId>  
  <controlURL>  
    /MusicServices/Control  
  </controlURL>  
  <eventSubURL>  
    /MusicServices/Event  
  </eventSubURL>  
  <SCPDURL>  
    /xml/MusicServices1.xml  
  </SCPDURL>  
</service>  

the controller will sent a subscribe request that looks like

SUBSCRIBE /MusicServices/Event HTTP/1.1  
HOST: 192.168.1.10:1400  
USER-AGENT: Linux UPnP/1.0 Sonos/46.3-57250 ()  
CALLBACK: <http://192.168.0.5:3400/notify>  
NT: upnp:event  
TIMEOUT: Second-3600  

This tells the speaker that whenever a related event happens, it should send a NOTIFY request to http://192.168.0.5:3400/notify.

Thus, we need to allow speakers to access port 3400 on every controllers.

NOTE: The port number is different for different platforms. The iOS app listens on port 3401, the Android app listens on port 3500.

IP multicast

IP multicast is a way to send a single data stream to multiple destinations (1:n). This is opposed to unicast where the source host sends data to a single destination (1:1). It’s also different from broadcast, which is link-local only, i.e. not routable. Many IP multicast addresses are routable in internal networks, including the one used by SSDP (routable within “Organization-Local Scope”).

What’s nice about this is that the server doesn’t have to know the actual destinations for the data stream. Instead, the clients are telling a multicast-aware router that they are interested in data for a specific address. Clients that should be reachable by a specific address form a multicast group. When the router receives multicast data, it forwards (and duplicates, if necessary) the data to the clients it knows about.

How does a client tell a router which multicast group it is interested in? With
IGMP.

Internet Group Management Protocol (IGMP)

IGMP is the protocol for managing multicast group “subscriptions”. It runs on the same level as ICMP.

When the controller starts, it’s sends a membership report:

$ tcpdump -n -# -r sonos.pcapng "igmp"  
    ...  
    3  IP 192.168.0.5 > 239.255.255.250: igmp v2 report 239.255.255.250  
    ...  

When a multicast aware router on the network receives such a packet on a specific interface, it remembers that there is a client for this group on that interface and will forward all packets addressed to the multicast address to that interface.

IGMP snooping

You may have heard of “IGMP snooping”. This is a feature on switches to only
send multicast traffic to the ports that have a host of the corresponding group connected.

IGMP snooping generally only works if there is a multicast router running that periodically queries the networks for group membership, and thus the switch learns about which groups are at which ports.

Final router and firewall setup

I’m using mrouted as multicast router, because it is part of OpenBSD’s base install. Another one is igmpproxy. My configuration is

# /etc/mrouted.conf  

phyint em1  
phyint vlan20  
phyint vlan30  
phyint vlan40  
phyint vlan50  

phyint em0 disable  

which means the daemon is registering IGMP messages on every interface but em0, my WAN interface.

Just like with normal IP packets, multicast forwarding needs to be explicitly enabled in the kernel:

# OpenBSD  
sysctl net.inet.ip.mforwarding=1  

# Debian  
sysctl net.ipv4.conf.all.mc_forwarding=1  

The exact setting name will likely differ between OSes. A grep forward over all settings should help.

As indicated earlier, this alone is not enough. The router’s firewall also need to be configured to allow IGMP, UPnP and Sonos traffic. Sonos actually provides a list of ports that it uses, and all the ports we discovered are mentioned there as well.

These are my rules for pf, something similar should be doable with iptables:

# /etc/pf.conf  

# Allow IGMP traffic from speakers and controllers  
pass quick proto igmp from {<sonos_speaker> <sonos_controller>} allow-opts  

# Allow discovery requests from speakers and controllers  
pass quick from {<sonos_speaker> <sonos_controller>} to 239.255.255.250  

# Allow discovery responses from speakers to controllers. Technically the  
# reverse direction might also be useful, but I haven't found it necessary  
pass quick proto udp from <sonos_speaker> to <sonos_controller> port 1901  

# Allow device description requests and responses between speakers and  
# controllers (pf is stateful)  
# Port 4444 is somehow used to monitor the firmware update process  
pass quick proto tcp from <sonos_controller> to <sonos_speaker> port {1400 4444}  

# Allow event messages from speakers to controllers  
pass quick proto tcp from <sonos_speaker> to <sonos_controller> port {3400 3401}  

<sonos_speaker> and <sonos_controller> are pf tables, which are just lists of IP addresses. This traffic is only allowed from (and to) devices that are Sonos speakers and controllers.