docs(zeroconf): clearer literal-IP error and a Security Considerations note

Building on the strict literal-IP validator from the previous commit,
make the runtime error self-explanatory so anyone tripping on a
hostname URL can fix it in one shot:

* Errors now lead with the offending zeroconf URL and the rejected
  host, so wrapping by GetInfo / PushCredentials / pushSimplifiedToken
  doesn't bury the actual bad value.
* The "host must be a literal IP" error suggests two concrete one-liner
  resolutions (`getent hosts <name>` and `dig +short <name>`) so the
  user has a copy-paste fix.
* The "host is not on a local network" error names the accepted ranges
  (loopback / RFC1918 private / link-local v4+v6) so the user knows
  what they're allowed to pass.

docs/guides/SOUNDTOUCH-SERVICE.md gains a bullet under Security
Considerations explaining the constraint and the rationale (LAN-resident
SSRF surface), so the strict behaviour is documented rather than a
surprise.

The 17 TestValidateZcBaseURL cases still pass — only the message bodies
changed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tobias Gesellchen
2026-05-10 14:30:55 +02:00
co-authored by Claude Opus 4.7
parent fbde4e136f
commit dc1f811a81
2 changed files with 13 additions and 5 deletions
+1
View File
@@ -836,6 +836,7 @@ fi
- **SSH Access**: Migration requires SSH access to devices. Ensure your network security policies allow this.
- **Proxy Logging**: Disable `REDACT_PROXY_LOGS` only in development environments.
- **Data Protection**: The data directory contains device configurations and usage patterns. Secure appropriately.
- **Spotify / Amazon Music credential push (zeroconf)**: outbound credential-push requests are restricted to literal IP hosts on local-network ranges (loopback, RFC1918 private, IPv4/IPv6 link-local). Hostname-style URLs (DNS, mDNS `*.local`) are rejected at runtime; if you have a hostname, resolve it first (`getent hosts <name>` or `dig +short <name>`) and pass the resolved IP. This guards against a malicious LAN-resident speaker pointing the credential push at a non-speaker host (server-side request forgery).
## Performance Tuning
+12 -5
View File
@@ -192,25 +192,32 @@ func DecryptBlob(encKey, macKey, blob []byte) ([]byte, error) {
func validateZcBaseURL(zcBaseURL string) (*url.URL, error) {
u, err := url.Parse(zcBaseURL)
if err != nil {
return nil, fmt.Errorf("parse: %w", err)
return nil, fmt.Errorf("zeroconf URL %q: parse: %w", zcBaseURL, err)
}
if u.Scheme != "http" && u.Scheme != "https" {
return nil, fmt.Errorf("scheme %q not allowed (expected http or https)", u.Scheme)
return nil, fmt.Errorf("zeroconf URL %q: scheme %q not allowed — must be http or https", zcBaseURL, u.Scheme)
}
host := u.Hostname()
if host == "" {
return nil, fmt.Errorf("missing host")
return nil, fmt.Errorf("zeroconf URL %q: missing host", zcBaseURL)
}
ip := net.ParseIP(host)
if ip == nil {
return nil, fmt.Errorf("host %q must be a literal IP (DNS/mDNS hostnames are not supported on this path; resolve to a private IP before calling)", host)
return nil, fmt.Errorf(
"zeroconf URL %q: host %q must be a literal IP — resolve the hostname to a private-network IP first "+
"(e.g. `getent hosts %s` or `dig +short %s`) and retry with the resolved address",
zcBaseURL, host, host, host)
}
if !ip.IsLoopback() && !ip.IsPrivate() && !ip.IsLinkLocalUnicast() {
return nil, fmt.Errorf("host %q is not on a local network", host)
return nil, fmt.Errorf(
"zeroconf URL %q: host %q is not on a local network — only loopback (127.0.0.0/8, ::1), "+
"RFC1918 private (10/8, 172.16/12, 192.168/16) and link-local (169.254/16, fe80::/10) "+
"addresses are accepted",
zcBaseURL, host)
}
// Build a fresh URL from validated components only — the IP literal,