Operating Sonos Speakers in a Multi-VLAN Network
In a throwback to the problems I dealt with using AirPlay across VLANs, I recently jumped through similar hoops for Sonos speakers. There are many forum and blog posts out there that describe (or attempt to describe) how to make this work, however all of the ones I read suffered from one or both of these problems:
- Their instructions had errors (eg, reversing the upstream and downstream interfaces when talking about multicast).
- They don't have a diagram of traffic flow! Every network engineer knows that a diagram is a must when trying to understand how two systems are talking to each other.
This post will dive deep on what's happening on the wire when a Sonos controller (eg, your mobile phone running the Sonos app) tries to talk with the players (the speakers) on the network. The focus will be how to make this process work when those two devices are in different VLANs.
What you read below works successfully with Sonos Beam, Sonos Sub, and Sonos Move using the Sonos S1 app.
TL;DR⌗
In a hurry? Don't want to take the time to learn what's really going on? If you can grok this diagram, then you don't need to read any further. If the diagram alone isn't enough to help you, then keep reading.
Update Jan 9, 2023: There was a typo in the original version of the drawing above
and in the instructions below: I incorrectly wrote port 1433
instead of 1443
. I've
corrected this now. Thanks to Daniel Durrans for pointing this out.
Why doesn't this just work?!⌗
Well, why do you have multiple VLANs in your home network?!
What's that?
Oh, well, yes, I too have multiple VLANs in my home network. Moving on...
Did it ever occur to you how the Sonos app knows which players you have even though you didn't have to enter their IP addresses or names into the app? Ever notice how no matter which device you open the Sonos app on, they all see the same set of players?
That's all happening because of a protocol that the app (also known as the controller) and the players use called Simple Service Discovery Protocol (SSDP). The controller sends out messages asking, "HEY! Ya'll out there??" And the players respond with, "Hi, please don't yell."
This works great when everything is on the same VLAN or broadcast domain but
breaks across VLANs because SSDP messages are sent via
multicast and those packets are sent with a Time To Live
(TTL) of 1
. This creates two problems:
- Home routers and firewalls aren't configured to route multicast by default.
- And even if they were, the TTL of
1
would prevent the router/firewall from forwarding the packet because the TTL would hit zero as it did so.
When you move the Sonos players into a different VLAN from the controllers, the
players no longer see the SSDP discovery messages from the controller and
therefore don't answer back. The controller is essentially shouting into an
empty room VLAN.
Make it work!⌗
The root of the solution is pretty simple: get the multicast messages with a TTL of 1
from one VLAN into the other. As per The Google(tm), the way to do this is to proxy the
multicast messages from the VLAN where the controllers sit to the VLAN where the players
sit.
Update Nov 5, 2022: Since writing this post, I've come to know of an alternative to the igmpproxy software (which is described below). The mcast-proxy software is written by an OpenBSD developer specifically for OpenBSD. The software follows some of OpenBSD's best practices for daemons which includes droping root privileges and chrooting itself. It's available in ports and as a package on your friendly neighbood mirror.
Configuration is easy: indicate which interface(s) are upstream and which are downstream
in /etc/mcast-proxy.conf
:
interface vlan200 {
upstream
}
interface vlan100 {
downstream
}
If you're running OpenBSD, I recommend you use mcast-proxy instead of igmpproxy. /End Update
Aside from mcast-proxy, the generally accepted way to proxy IGMP is using a piece of software called igmpproxy. This software works on Linux and the *BSDs. Although not documented in the project's README, the software is also part of the Ubiquity Security Gateway and Edge Router.
At the time of this writing, you must be running igmpproxy newer than v0.3.
Specifically, one that contains
this commit.
On OpenBSD, you can build the
net/igmproxy
port from HEAD
or use the igmpproxy package from OpenBSD 7.0 or later,
whichever is most appropriate when you're reading this. Versions prior to this
will not pass SSDP multicast messages and so will not work.
There's one key concept to understand when configuring igmpproxy: When you're thinking about multicast, traffic flows from a source to the receivers. So we would say that upstream in the flow is towards the source and downstream is towards the receivers.
Imagine the multicast flow is like a river. If you were standing in the river looking upstream, first of all your feet would be wet, and secondly you'd be looking towards where the stream comes from; its source. Downstream would be where the river goes.
Modify igmpproxy.conf
and set the upstream
interface as the interface
connected to the VLAN with the controllers and the downstream
interface as
the interface connected to the VLAN with the players. A config that matches the
network topology in the diagram above would look like this:
phyint vlan200 upstream ratelimit 0 threshold 1
phyint vlan100 downstream ratelimit 0 threshold 1
It still doesn't work!⌗
Well, yeah. If the device you're running igmpproxy on is also doing packet filtering, I wouldn't expect it to work yet. All we've done so far is tell the firewall to proxy the multicast traffic. Let's now review how to pass the necessary traffic through the firewall.
- Allow protocol IGMP in and outbound on the players' interface to and from the firewall. IGMP is how the players signal to the firewall that they want to receive traffic for the SSDP multicast group and how the firewall maintains the list of group members by sending IGMP query messages.
- Allow protocol UDP from the controller subnet to the destination address
239.255.255.250
and destination port1900
inbound on the firewall's controller interface. This is for the SSDP discovery messages. - Allow protocol UDP with a destination address of
239.255.255.250
, destination port1900
, and source IP address of the controller's subnet outbound on the players' interface. This allows igmpproxy to properly proxy the multicast traffic from the controller to the players. The key here is that the source address of the controller's multicast traffic is not modified as it's proxied; the packets retain the source IP address of the controller that originated them. - Allow protocol TCP with a destination address of the controller subnet,
destination ports
3400
,3401
, and3500
, and source IP address of the players' subnet. These are the ports that the players use when communicating back to a controller. - Allow protocol TCP with a destination address of the players' subnet and
destination ports
1400
,1443
, and4444
and a source of the controller subnet. These are ports the controller uses to talk to the players once the controller has discovered them using SSDP. Port4444
is used for updating the software on the players.
One final step you may need to do is enable multicast routing on the firewall.
The command(s) to do this are highly platform dependent. On OpenBSD it's
sysctl net.inet.ip.mforwarding=1
(and uncomment the matching mforwarding
line in /etc/sysctl.conf
to ensure the setting persists across reboots).
Ok, the Sonos app works but AirPlay doesn't!?⌗
Yep. You'll have to deal with AirPlay separately. I recommend you read my blog post on that.
The ports you'll need to allow through the firewall are:
- From players to controllers:
- UDP, destination ports
319
and320
- UDP, destination ports
- From controllers to players:
- TCP, destination port
7000
- TCP, destination ports
> 30,000
(Or maybe all ephemeral ports? My observation is that > 30,000 works just fine) - UDP, destination ports
319
and320
- UDP, destination ports
> 30,000
(Same thoughts as above)
- TCP, destination port
Example firewall ruleset⌗
Here's an example ruleset for OpenBSD's pf(4)
player_if = "vlan100"
player_net = "192.168.100.0/24"
controller_if = "vlan200"
controller_net = "192.168.200.0/24"
mcast_ssdp = "239.255.255.250 port 1900"
airplay_to_controllers_udp = "319:320"
airplay_to_players_tcp = "{ 7000, > 30000 }"
airplay_to_players_udp = "{ 319:320, > 30000 }"
sonos_controller_tcp_ports = "{ 3400 3401 3500 }"
sonos_player_tcp_ports = "{ 1400 1443 4444 }"
# AirPlay from players to controllers
pass in on $player_if inet proto udp \
from $player_net \
to $controller_net port $airplay_to_controllers_udp \
tag CNTRL_OUT
# Sonos on player network
pass in on $player_if inet proto igmp from $player_net allow-opts
pass in on $player_if inet proto tcp \
from $player_net \
to $controller_net port $sonos_controller_tcp_ports \
tag CNTRL_OUT
# AirPlay from controller network
pass in on $controller_if inet proto tcp \
from $controller_net \
to $player_net port $airplay_to_players_tcp \
tag PLAYER_OUT
pass in on $controller_if inet proto udp \
from $controller_net \
to $player_net port $airplay_to_players_udp \
tag PLAYER_OUT
# Sonos on controller network
pass in on $controller_if inet proto udp \
from $controller_net \
to $mcast_ssdp
pass in on $controller_if inet proto tcp \
from $controller_net \
to $player_net port $sonos_player_tcp_ports \
tag PLAYER_OUT
pass out on $controller_if tagged CNTRL_OUT
pass out on $player_if tagged PLAYER_OUT
pass out on $player_if inet proto igmp from $player_if allow-opts
pass out on $player_if inet proto udp from $controller_net to $mcast_ssdp
If you're using pf(4) on *BSD, you'll need allow-opts
on the pass
rules for
IGMP because IGMP Report packets are sent with the Router Alert option set in
the IP header.