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:

  1. Their instructions had errors (eg, reversing the upstream and downstream interfaces when talking about multicast).
  2. 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:

  1. Home routers and firewalls aren't configured to route multicast by default.
  2. 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 port 1900 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 port 1900, 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, and 3500, 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, and 4444 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. Port 4444 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 and 320
  • 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 and 320
    • UDP, destination ports > 30,000 (Same thoughts as above)

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.

References