Skip to content

Commit c058943

Browse files
committed
Record --interface as ROADMAP item 25, gated on a sendspin-cpp bind address
The flag cannot be built against the pinned library: the listener is constructed with a literal "0.0.0.0" and SendspinClientConfig carries no bind-address field. Item 25 settles the design and the evidence so neither has to be re-derived when upstream moves. It corrects two parity claims commonly made about the Python CLI, both checked against the pinned aiosendspin 6.0.1: its --interface is address-valued rather than an interface name, and it does not scope the mDNS advertisement to that address -- what it scopes locally is the browse. The flag is settled as --interface <name-or-address>, validated in the two stages item 24 established: shape at parse time, resolution at startup, since an interface name depends on live host state.
1 parent 7f33e4d commit c058943

1 file changed

Lines changed: 107 additions & 0 deletions

File tree

‎docs/ROADMAP.md‎

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2204,3 +2204,110 @@ exit on failure.
22042204

22052205
Verified against a real `aiosendspin` server: with `pcm:44100:16:2` pinned on a device
22062206
whose ranked head is FLAC 48 kHz, the server encoded and streamed PCM at 44100 Hz.
2207+
2208+
2209+
### 25. `--interface` — *open*
2210+
2211+
Bind the inbound listener — and scope the advertisement — to one interface, instead of
2212+
taking every address the host holds. **Nothing here is buildable against the pinned
2213+
library**, so this item exists to settle the design and the evidence rather than to leave
2214+
them to be re-derived.
2215+
2216+
**The gap, in the real code.** The listener is constructed with a literal at
2217+
sendspin-cpp's `src/host/ws_server.cpp:53` — `ix::WebSocketServer(server_port_,
2218+
"0.0.0.0", ...)` — and its `SendspinClientConfig` (`include/sendspin/config.h`) carries
2219+
`server_port` and `server_max_connections` and nothing else about the socket. This repo's
2220+
advertisement registers on `kDNSServiceInterfaceIndexAny` at `src/mdns_dnssd.cpp:200`.
2221+
Those two are *consistent* today: both unrestricted. The flaw is not a mismatch between
2222+
them — it is that neither can be narrowed, and only the listener would matter if it could.
2223+
2224+
**The one knob that looks like it and is not.** `SendspinClientConfig` does carry a
2225+
`mac_address` override, documented upstream as "recommended on multi-homed hosts where
2226+
detection may pick the wrong interface" — the same scenario, so it is worth saying that it
2227+
is not the same lever. That value goes into `client/hello`'s `device_info`: it is identity,
2228+
and it moves no socket.
2229+
2230+
**Why this is more than "it listens broadly".** Pinned v0.7.2 has **no inbound
2231+
authentication of any kind**: no PSK, no pairing gate on the inbound path, and — per item
2232+
17, whose text still cites v0.7.0, re-checked here at v0.7.2 — no `activities` /
2233+
`server/activate` either, since it still runs the older `connection_reason` handoff.
2234+
`server_max_connections` is the only inbound limit, and it counts sockets rather than
2235+
judging them. Reachability is therefore authorization: whatever reaches the listen port
2236+
completes the handshake and drives the player, and a bind address would be the only such
2237+
control the player itself offers.
2238+
2239+
**That is a statement about the pinned library, not about Sendspin.** The spec
2240+
authenticates in the handshake — pairing and a PSK, as item 6 records — and v0.7.2 does
2241+
not implement that half yet. This item's premise expires when it does.
2242+
2243+
**In proportion.** Most installs sit behind NAT, where no interface holds a routable
2244+
address and the exposure is the LAN the player is meant to serve anyway. The real cases
2245+
are multi-homed hosts, VPSes, tunnel interfaces and untrusted VLANs. **A host firewall is
2246+
the working mitigation today**, and it is the honest answer for an operator who needs one
2247+
now.
2248+
2249+
**Why the mDNS half is not shippable on its own.** An advertisement scoped to one
2250+
interface while the socket still listens on `0.0.0.0` hides the player without protecting
2251+
it: anything that already knows the address and port still connects. It does buy something
2252+
real — an unscoped register publishes address records on every interface, and a server can
2253+
take one that is unreachable from where it sits — but that is a *discovery-correctness*
2254+
fix, not access control. Shipping it under this flag's name would sell it as the latter.
2255+
Both halves land together, or the mDNS half lands under its own name.
2256+
2257+
**The parity record, corrected.** The Python CLI is the usual reference for a flag like
2258+
this, and two things commonly assumed about it are wrong — the flag checked in
2259+
`sendspin-python-cli`, the behaviour in `aiosendspin` `6.0.1`, the version that CLI pins:
2260+
2261+
- **Its `--interface` takes an IP address, not an interface name.** `sendspin/cli.py:475`
2262+
documents it as "IP address of the network interface to bind to", and
2263+
`sendspin/daemon/daemon.py:212` passes it through unexamined as
2264+
`host=self._args.interface if ... else "0.0.0.0"`. There is no `if_nametoindex()`
2265+
anywhere in it.
2266+
- **It does not scope the advertisement to that address.** The bind is honoured —
2267+
`ClientListener` does `web.TCPSite(self._runner, self._host, self._port)` — but
2268+
`_start_mdns()` builds a bare `AsyncZeroconf()` and takes the address it publishes from
2269+
`get_local_ip()` in `aiosendspin/util.py`, a UDP route lookup to `8.8.8.8`.
2270+
`self._host` never reaches it. Unchanged at `9.1.0`.
2271+
2272+
What Python *does* scope locally is the **browse**: `ServiceDiscovery(interfaces=[...])`
2273+
hands the value to `AsyncZeroconf(interfaces=...)` in `sendspin/discovery.py:150`. That
2274+
is the outbound discovery path, not the inbound listener.
2275+
2276+
**The flag, settled.** `--interface <name-or-address>` (config key `interface`), long-only
2277+
like the other non-squeezelite flags. **One value has to yield both halves** — the bind
2278+
needs an *address*, `DNSServiceRegister` needs an *index* — and a name is what an operator
2279+
on a Pi actually knows, while the address is the thing a DHCP lease changes underneath
2280+
them. That makes this a deliberate superset of Python's address-only flag rather than a
2281+
divergence from it.
2282+
2283+
**Both directions have to be resolved, and neither is a lookup.** A name yields an index
2284+
from `if_nametoindex()` but must go through `getifaddrs()` for an address — and an
2285+
interface holds several: a v4, a link-local v6, often a global v6, sometimes aliases.
2286+
`ix::WebSocketServer` takes one host string, so **which one is a decision to settle when
2287+
the flag is built**, not a detail: prefer the first global v4, fall back to a global v6,
2288+
and refuse a link-local v6 rather than guess at a `%scope` suffix the transport may not
2289+
accept. In reverse, a literal address is what the bind wants directly but yields an index
2290+
only by scanning `getifaddrs()` for the interface holding it — which can match none, and
2291+
with aliases can match more than one. Both misses are refusals, not fallbacks.
2292+
2293+
**Validated in two stages, the split item 24 established.** The shape is checked when the
2294+
flag is read — a name within `IFNAMSIZ`, or an address `inet_pton()` accepts — so a config
2295+
file is validated without touching the host, and a typo still hard-fails at parse time with
2296+
the single `error:` line of item 1. **Resolution happens at startup**, where the interfaces
2297+
are real, because it depends on live host state: a Pi booting with `interface = eth0` in
2298+
its config before DHCP has brought the link up must fail where a device failure is reported,
2299+
not from inside the parser. That is the same seam item 24 drew between `parse_format_spec()`
2300+
and the advertisement check, for the same reason.
2301+
2302+
**Scope when it lands.** The inbound listener, which is the whole point, and the
2303+
advertisement's interface index. The outbound `-s` dial stays out: it is kernel-routed,
2304+
as it is in Python. Scoping the browse at `src/mdns_dnssd.cpp:217` is the one thing Python
2305+
does locally and can come along as an extension — but it is discovery convenience, not
2306+
security, and is not what the flag exists for.
2307+
2308+
**The upstream gate.** A bind address on `SendspinClientConfig`, threaded through
2309+
`ConnectionManager` into `SendspinWsServer` to replace the literal. Host-only and small,
2310+
but `SendspinWsServer` exposes only connection callbacks — there is no transport seam the
2311+
CLI can reach around it — so the flag cannot be expressed by any other route. Gated on a
2312+
`SENDSPIN_GIT_TAG` bump the same way item 17 is, and likely to arrive with one rather than
2313+
on its own. **Not expected to move soon.**

0 commit comments

Comments
 (0)