vaskevich/orbstack-sonos-relay

Enables Sonos for Home Assistant under OrbStack on macOS

★ 1Forks 0PythonGitHub ↗Compare

README

orbstack-sonos-relay

orbstack-sonos-relay is a small compatibility shim for the Sonos integration in Home Assistant Container when it runs under OrbStack on macOS. It relays the two paths Sonos needs: SSDP discovery and UPnP event callbacks.

It is deliberately narrow. It is not a Layer-2 bridge, an mDNS/Bonjour relay, a generic UPnP proxy, a generic TCP forwarder, a Beacon replacement, or a Sonos cloud integration. It does not claim to fix every OrbStack multicast or networking issue.

Why this is needed

A container using network_mode: host shares OrbStack Linux's network namespace—not the Mac's physical en0 identity. In the tested topology, Home Assistant and the speakers therefore live on opposite sides of a macOS/OrbStack boundary:

Physical Wi-Fi LAN (192.168.86.0/24)

  Sonos speakers                 Mac
  192.168.86.x  <---------->  en0: 192.168.86.240
                                     |
                           macOS / OrbStack boundary
                                     |
                              bridge100: 192.168.139.3
                                     |
                           OrbStack Linux / HA host net
                              HA: 192.168.139.2

Home Assistant can initiate ordinary HTTP/SOAP connections to Sonos, but two independent reverse/discovery paths do not naturally cross this boundary in the form Sonos expects.

Packet flows

SSDP discovery

Home Assistant sends ST:ssdp:all M-SEARCH packets from its OrbStack-side address. The tested installation emits both a multicast and a limited-broadcast copy:

192.168.139.2:ephemeral -> 239.255.255.250:1900
192.168.139.2:ephemeral -> 255.255.255.255:1900

The relay captures those packets from bridge100 through macOS tcpdump/BPF, ignores searches originating from the Mac's own bridge address, and deduplicates identical multicast/broadcast copies for a short period. It then sends the original M-SEARCH bytes as a LAN broadcast sourced from the Mac's physical LAN IP.

HA / OrbStack                  relay on Mac                    physical LAN

192.168.139.2:49152  --M-SEARCH-->  capture on bridge100
                                      bind 192.168.86.240:any
                                      broadcast raw M-SEARCH  ----->  :1900

192.168.139.2:49152  <--raw 200 OK--  filter Sonos replies    <-----  Sonos
                         via bridge100  (RINCON_, ZonePlayer,
                                         or Sonos/ marker)

The raw Sonos SSDP 200 OK responses are sent back to the source IP and UDP port of the original HA search. Other UPnP responses are not injected. No speaker IP addresses are configured or hardcoded.

With --ha-ip auto, the relay can learn the OrbStack-side HA address from three signals:

  1. When an early Sonos callback arrives before HA is known, existing IPv4 neighbors from the configured OrbStack bridge are tested on that same callback port. Only entries already present in macOS's ARP table are considered; the daemon does not scan the subnet. A backend is pinned only when exactly one candidate accepts the connection.
  2. Observed outbound HA TCP traffic to the Sonos UPnP endpoint on port 1400 identifies its source as HA. This traffic normally occurs during Sonos setup or polling.
  3. An observed HA SSDP M-SEARCH can identify its source as HA, as before.

The neighbor lookup handles the common relay-restart case where Home Assistant is already running and may not immediately emit new setup, polling, or SSDP traffic before an existing Sonos callback arrives. The bridge's own address, unusable ARP entries, and non-unicast addresses are excluded, and candidates in the detected bridge subnet are preferred. The first learned address is pinned until the process restarts so it cannot oscillate between clients.

UPnP event callbacks

Home Assistant's SoCo event listener runs inside OrbStack, commonly on 192.168.139.2:1400. The Sonos integration is configured to advertise the Mac's LAN address, so a speaker correctly sends NOTIFY requests to 192.168.86.240:1400. The remaining missing hop is from the Mac to HA.

Sonos speaker                Mac relay                    HA / SoCo

NOTIFY --------------> 192.168.86.240:P  ----------> 192.168.139.2:P
                           TCP listener       same-port TCP connection

By default, the daemon listens on TCP ports 1400 through 1499 on the selected LAN address. Each port P proxies only to the configured or learned HA address on the same port P. The range allows SoCo to choose another listener port when 1400 is occupied. Unavailable ports are reported and skipped; startup fails if none can be bound.

The bridge capture is confirmed ready before these LAN listeners are opened. If an early callback finds no unique accepting bridge neighbor, its handler waits up to 0.75 seconds for concurrent HA/Sonos packet traffic to identify the backend before rejecting the connection. Neighbor probes use the callback's same TCP port and successful probe sockets are closed immediately before the real forwarding connection is opened.

Requirements

  • macOS with OrbStack
  • Home Assistant Container using OrbStack host networking
  • Python 3.10 or later, with no third-party packages
  • /usr/sbin/tcpdump
  • /usr/sbin/arp
  • Home Assistant's Sonos integration and Sonos devices on the physical LAN

The LaunchDaemon runs as root because macOS BPF capture normally requires elevated privileges.

Installation

Clone the repository, review the scripts and daemon, then run:

sudo ./install.sh

The installer copies the daemon to /usr/local/libexec/orbstack-sonos-relay/, writes /Library/LaunchDaemons/io.github.vaskevich.orbstack-sonos-relay.plist, and starts it with modern launchctl bootstrap. RunAtLoad and KeepAlive are enabled. If OrbStack has not created bridge100 yet, the daemon waits rather than exiting in a restart loop.

Defaults can be overridden for one installation:

sudo env \
  LAN_IFACE=en0 \
  ORB_IFACE=bridge100 \
  HA_IP=auto \
  EVENT_PORTS=1400-1499 \
  PYTHON_BIN=/usr/bin/python3 \
  ./install.sh

For deterministic operation, use HA's actual OrbStack-side IPv4 address instead of auto:

sudo env HA_IP=192.168.139.2 ./install.sh

This OrbStack-side address is internal to OrbStack. It is not assigned by the physical LAN's DHCP server, and it does not need a reservation in the user's router. Explicit HA_IP remains useful as a deterministic override or when automatic traffic classification is ambiguous.

Re-running install.sh replaces the installed daemon and plist, then restarts the service. It does not modify Home Assistant or OrbStack configuration.

Home Assistant configuration

Configure the Sonos integration to advertise the Mac's physical LAN address—not the HA/OrbStack address:

sonos:
  media_player:
    advertise_addr: 192.168.86.240

Restart Home Assistant after changing this setting. The address that should remain stable is the Mac's physical LAN address used by advertise_addr—192.168.86.240 on en0 in this example. Reserve that address for the physical Mac interface/MAC in the LAN router's DHCP settings (or assign it statically) so it does not become stale after a lease change. Do not create a LAN DHCP reservation for HA's internal OrbStack-side address.

Choosing interfaces

en0 and bridge100 are tested defaults, not universal names. Find the interface used by the default route and inspect its IPv4 address:

route get default
ifconfig en0

List bridge interfaces and inspect likely OrbStack bridges while OrbStack is running:

ifconfig -l
ifconfig bridge100

Choose the physical interface that shares the Sonos LAN for LAN_IFACE. Choose the bridge on which HA's 239.255.255.250:1900 or 255.255.255.255:1900 M-SEARCH packets are visible for ORB_IFACE. If uncertain, verify traffic directly:

sudo /usr/sbin/tcpdump -ni bridge100 \
  '(udp and dst port 1900) or (tcp and dst port 1400)'

If the Mac has two active physical interfaces on the same LAN/subnet, such as Ethernet and Wi-Fi, each can receive a separate DHCP lease while advertising the same hostname. Select the interface actually used to reach Sonos as LAN_IFACE and reserve that interface's address/MAC. The other interface does not necessarily need to be disabled, although temporarily disabling it can simplify route and firewall troubleshooting.

Interface names and addresses can change when network services or OrbStack configuration change; reinstall with updated overrides when necessary.

Logs and troubleshooting

Show launchd state and follow both logs:

sudo launchctl print system/io.github.vaskevich.orbstack-sonos-relay
sudo tail -f /var/log/orbstack-sonos-relay.log \
  /var/log/orbstack-sonos-relay.err.log

Useful checks:

  • Waiting for interface bridge100 means OrbStack has not created the configured bridge or it has no IPv4 address yet.
  • Confirm the startup diagnostics show the expected LAN IP, bridge IP, HA mode/address, and callback ports.
  • In auto mode, look for a learned-address message ending in from bridge neighbor accepting TCP :P, from outbound Sonos TCP, or from SSDP M-SEARCH. An ambiguity message means multiple known bridge neighbors accepted the callback port, so none was selected. Trigger a Sonos action/poll or discovery, or set HA_IP explicitly.
  • Port warnings identify callback listeners already occupied by another process. A partial range is usable; no available ports is fatal.
  • Use sudo lsof -nP -iTCP:1400 -sTCP:LISTEN (changing the port as needed) to identify a conflict.
  • If searches appear on the bridge but no Sonos replies are forwarded, verify the Mac and speakers share the selected LAN, macOS firewall policy permits the traffic, and LAN client isolation is disabled.
  • Reinstall with the right interface names or a deterministic HA_IP after correcting configuration.

For foreground diagnostics, stop the LaunchDaemon and invoke the installed program with --verbose; do not run two copies because their callback ports will conflict:

sudo launchctl bootout system/io.github.vaskevich.orbstack-sonos-relay
sudo /usr/bin/python3 \
  /usr/local/libexec/orbstack-sonos-relay/orbstack-sonos-relay.py \
  --lan en0 --orb bridge100 --ha-ip auto --verbose

Re-run install.sh to restore and start the LaunchDaemon afterward.

Uninstall

sudo ./uninstall.sh

This boots out the LaunchDaemon and removes its plist and installed program. Log files are deliberately preserved in /var/log; remove them manually if desired.

Security

Read and understand the daemon before installing it:

  • It runs as root to capture packets with tcpdump/BPF.
  • It binds TCP 1400-1499 on the selected physical LAN address by default.
  • Those listeners proxy only to the configured/learned HA backend on the same TCP port; they are not arbitrary forwarding endpoints.
  • SSDP responses are injected into HA only when they are HTTP 200 responses containing a Sonos-looking RINCON_, ZonePlayer, or Sonos/ marker. This is a useful scope filter, not cryptographic authentication.
  • A device on the LAN can connect to the callback listeners or forge UDP content. Treat the physical LAN and the configured HA backend as part of the trust boundary. Do not deploy this unchanged on an untrusted LAN.

Reducing EVENT_PORTS to the ports your installation actually uses narrows the exposed listener range, but ensure the range still covers any port SoCo may select.

Limitations

  • This handles only the tested Home Assistant Sonos SSDP and event-callback mismatch across OrbStack/macOS.
  • It supports Ethernet-framed IPv4 UDP and TCP capture, with at most one VLAN tag. IPv6 and fragmented IPv4 packets are not used for detection or relaying.
  • Auto HA discovery uses existing bridge neighbors plus observed outbound TCP destination-port 1400 traffic and suitable M-SEARCH packets. Neighbor probing is limited to existing ARP entries and the callback's port, but it still creates brief real TCP connections. Ambiguous accepting neighbors are not guessed. Use HA_IP when more than one OrbStack client may expose the same callback port or deterministic startup is important.
  • The SSDP classifier uses recognizable Sonos response markers; it is not device authentication.
  • Changes to macOS, OrbStack, Home Assistant, SoCo, interface naming, firewall behavior, or Sonos firmware may require new validation.
  • This does not relay multicast generally and does not make HA literally share the Mac's physical network identity.
  • mDNS/Bonjour is explicitly out of scope. Beacon or another mDNS solution can be used independently for HomeKit, Bonjour, and similar services.

Development

All tests use synthetic packets and the Python standard library; root, tcpdump, OrbStack, Home Assistant, and Sonos hardware are not needed:

python3 -m unittest discover -s tests -v
python3 -m py_compile orbstack-sonos-relay.py tests/test_relay.py
sh -n install.sh uninstall.sh

License

MIT