@@ -2204,3 +2204,110 @@ exit on failure.
22042204
22052205Verified against a real ` aiosendspin ` server: with ` pcm:44100:16:2 ` pinned on a device
22062206whose 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