131 lines
4.0 KiB
Go
131 lines
4.0 KiB
Go
// ABOUTME: Stable client_id resolution (override -> config value -> MAC -> generated UUID)
|
|
// ABOUTME: Persistence of new values is delegated to a caller-supplied callback
|
|
package sendspin
|
|
|
|
import (
|
|
"bytes"
|
|
"log"
|
|
"net"
|
|
"sort"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
type clientIDSource string
|
|
|
|
const (
|
|
sourceOverride clientIDSource = "override"
|
|
sourceConfig clientIDSource = "config"
|
|
sourceMAC clientIDSource = "mac"
|
|
sourceGenerated clientIDSource = "generated"
|
|
)
|
|
|
|
// PersistFn is called by ResolveClientID whenever a new client_id needs to be
|
|
// written back to the config file. Implementations typically call
|
|
// WriteStringKey(path, "client_id", id). Returning an error is logged but not
|
|
// fatal — the player will still boot with the resolved id; the id just won't
|
|
// survive a restart.
|
|
type PersistFn func(id string) error
|
|
|
|
// ResolveClientID returns a stable client_id, following this precedence:
|
|
// 1. override — used as-is and persisted via persist (so a one-time seed sticks)
|
|
// 2. fromConfig — used as-is, no persistence (value came from the file we'd write to)
|
|
// 3. primary network interface MAC (xx:xx:xx:xx:xx:xx) — not persisted; MAC is inherently stable
|
|
// 4. freshly generated UUID — persisted so the next launch takes path 2
|
|
//
|
|
// Persisted value beating MAC is deliberate: once Music Assistant has
|
|
// registered a player under one id, switching to another creates a duplicate
|
|
// entry. Deleting client_id from the config file is the explicit "re-derive".
|
|
//
|
|
// persist may be nil if the caller cannot write back (e.g. no config path
|
|
// resolvable). In that case generated/override ids still work for the session
|
|
// but won't survive a restart.
|
|
func ResolveClientID(override, fromConfig string, persist PersistFn) (string, error) {
|
|
if override != "" {
|
|
if override != fromConfig {
|
|
tryPersist(persist, override, "override")
|
|
}
|
|
logClientID(override, sourceOverride, persist != nil && override != fromConfig)
|
|
return override, nil
|
|
}
|
|
|
|
if fromConfig != "" {
|
|
logClientID(fromConfig, sourceConfig, false)
|
|
return fromConfig, nil
|
|
}
|
|
|
|
if mac, ok := pickStableMAC(allInterfaces()); ok {
|
|
logClientID(mac, sourceMAC, false)
|
|
return mac, nil
|
|
}
|
|
|
|
generated := uuid.New().String()
|
|
tryPersist(persist, generated, "generated id")
|
|
logClientID(generated, sourceGenerated, persist != nil)
|
|
return generated, nil
|
|
}
|
|
|
|
func tryPersist(persist PersistFn, id, what string) {
|
|
if persist == nil {
|
|
return
|
|
}
|
|
if err := persist(id); err != nil {
|
|
log.Printf("client_id: could not persist %s: %v", what, err)
|
|
}
|
|
}
|
|
|
|
func logClientID(id string, source clientIDSource, persisted bool) {
|
|
if persisted {
|
|
log.Printf("client_id: %s (source: %s, persisted)", id, source)
|
|
return
|
|
}
|
|
log.Printf("client_id: %s (source: %s)", id, source)
|
|
}
|
|
|
|
// allInterfaces is a package-level var so tests can stub it.
|
|
var allInterfaces = func() []net.Interface {
|
|
ifaces, err := net.Interfaces()
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
return ifaces
|
|
}
|
|
|
|
// pickStableMAC returns the MAC of the alphabetically-first eligible interface.
|
|
// Eligible means: up, not loopback, 6-byte hardware address, not zero, not broadcast.
|
|
// Alphabetical ordering gives deterministic selection across reboots and OSes
|
|
// (eth0 < wlan0, Ethernet < Wi-Fi).
|
|
func pickStableMAC(ifaces []net.Interface) (string, bool) {
|
|
eligible := make([]net.Interface, 0, len(ifaces))
|
|
for _, iface := range ifaces {
|
|
if iface.Flags&net.FlagUp == 0 {
|
|
continue
|
|
}
|
|
if iface.Flags&net.FlagLoopback != 0 {
|
|
continue
|
|
}
|
|
if len(iface.HardwareAddr) != 6 {
|
|
continue
|
|
}
|
|
if isZeroMAC(iface.HardwareAddr) || isBroadcastMAC(iface.HardwareAddr) {
|
|
continue
|
|
}
|
|
eligible = append(eligible, iface)
|
|
}
|
|
if len(eligible) == 0 {
|
|
return "", false
|
|
}
|
|
sort.Slice(eligible, func(i, j int) bool {
|
|
return eligible[i].Name < eligible[j].Name
|
|
})
|
|
return eligible[0].HardwareAddr.String(), true
|
|
}
|
|
|
|
func isZeroMAC(mac net.HardwareAddr) bool {
|
|
return bytes.Equal(mac, []byte{0, 0, 0, 0, 0, 0})
|
|
}
|
|
|
|
func isBroadcastMAC(mac net.HardwareAddr) bool {
|
|
return bytes.Equal(mac, []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff})
|
|
}
|