Felix Kling

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.