mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-18 08:36:13 +00:00
Fix mDNS discovery IPv6 issues and improve timeout handling
- Force IPv4-only mDNS queries with DisableIPv6=true to avoid routing issues - Add automatic IPv4 interface selection for better compatibility - Filter mDNS results to only include SoundTouch devices - Clean up device names by unescaping mDNS characters - Fix timeout flag handling to respect DISCOVERY_TIMEOUT from .env file - Only override discovery timeout when --timeout flag is explicitly provided - Add file operations safety guidelines to docs/CLAUDE.md - Remove duplicate timeout flags from discover command, use global flags Fixes IPv6 'no route to host' errors that prevented mDNS discovery. Now discovers same devices as native dns-sd and dig tools.
This commit is contained in:
@@ -12,29 +12,42 @@ import (
|
||||
|
||||
// discoverDevices handles device discovery command
|
||||
func discoverDevices(c *cli.Context) error {
|
||||
timeout := c.Duration("timeout")
|
||||
httpTimeout := c.Duration("timeout")
|
||||
showAll := c.Bool("all")
|
||||
|
||||
fmt.Printf("Discovering SoundTouch devices...\n")
|
||||
|
||||
if showAll {
|
||||
fmt.Printf("Timeout: %v\n", timeout)
|
||||
fmt.Printf("Mode: Detailed information\n")
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
|
||||
// Load configuration
|
||||
cfg, err := config.LoadFromEnv()
|
||||
if err != nil {
|
||||
cfg = config.DefaultConfig()
|
||||
}
|
||||
|
||||
// Override discovery timeout if provided
|
||||
if timeout > 0 {
|
||||
cfg.DiscoveryTimeout = timeout
|
||||
// Only override discovery timeout if user explicitly provided --timeout flag
|
||||
// This respects DISCOVERY_TIMEOUT from .env file when no flag is provided
|
||||
if c.IsSet("timeout") {
|
||||
cfg.HTTPTimeout = httpTimeout
|
||||
// Set discovery timeout to be 2x HTTP timeout (min 5s, max 30s)
|
||||
discoveryTimeout := httpTimeout * 2
|
||||
if discoveryTimeout < 5*time.Second {
|
||||
discoveryTimeout = 5 * time.Second
|
||||
}
|
||||
|
||||
if discoveryTimeout > 30*time.Second {
|
||||
discoveryTimeout = 30 * time.Second
|
||||
}
|
||||
|
||||
cfg.DiscoveryTimeout = discoveryTimeout
|
||||
}
|
||||
|
||||
if showAll {
|
||||
fmt.Printf("HTTP Timeout: %v\n", cfg.HTTPTimeout)
|
||||
fmt.Printf("Discovery Timeout: %v\n", cfg.DiscoveryTimeout)
|
||||
fmt.Printf("Mode: Detailed information\n")
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
|
||||
// Create discovery service
|
||||
discoveryService := discovery.NewUnifiedDiscoveryService(cfg)
|
||||
|
||||
|
||||
@@ -68,7 +68,6 @@ func main() {
|
||||
{
|
||||
Name: "name",
|
||||
Usage: "Get or set device name",
|
||||
Flags: CommonFlags,
|
||||
Before: RequireHost,
|
||||
Subcommands: []*cli.Command{
|
||||
{
|
||||
|
||||
@@ -80,6 +80,14 @@ When creating test data for API endpoints, prefer real device responses over hyp
|
||||
- **Coverage**: Use multiple real devices to cover different response variations
|
||||
- **Non-responsive endpoints**: Some endpoints like `/trackInfo` may not respond or exist on all devices
|
||||
|
||||
### 9. File Operations Safety
|
||||
|
||||
- **Never delete files** - use move/rename instead when possible
|
||||
- **Ask before destructive operations** - especially for config files (.env, *.config, etc.)
|
||||
- **Prefer non-destructive operations** - copy, move, rename over delete
|
||||
- **Respect user data** - treat all user files as potentially containing sensitive data
|
||||
- **Configuration files are sacred** - .env, config files may contain secrets and personal settings
|
||||
|
||||
## Additional Notes
|
||||
|
||||
- **Language: English** for code, commits, labels, and text in code
|
||||
|
||||
+83
-8
@@ -47,18 +47,38 @@ func (m *MDNSDiscoveryService) DiscoverDevices(ctx context.Context) ([]*models.D
|
||||
log.Printf("mDNS: Starting discovery for service '%s.%s' with timeout %v",
|
||||
soundTouchServiceType, soundTouchDomain, m.timeout)
|
||||
|
||||
// Query for SoundTouch devices
|
||||
// Note: hashicorp/mdns expects service and domain separately
|
||||
// IPv4-only query to fix "no route to host" errors on IPv6
|
||||
// This addresses the issue where hashicorp/mdns fails with:
|
||||
// "write udp6 [::]:port->[ff02::fb]:5353: sendto: no route to host"
|
||||
// The trailing dot in service names is handled correctly by separating
|
||||
// service and domain parameters as expected by the library.
|
||||
err := mdns.Query(&mdns.QueryParam{
|
||||
Service: "_soundtouch._tcp",
|
||||
Domain: "local.",
|
||||
Timeout: m.timeout,
|
||||
Entries: entries,
|
||||
Service: "_soundtouch._tcp",
|
||||
Domain: "local.",
|
||||
Timeout: m.timeout,
|
||||
Entries: entries,
|
||||
DisableIPv6: true, // Force IPv4 only to avoid routing issues
|
||||
Interface: m.getIPv4Interface(), // Use specific interface if available
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("mDNS query completed with error: %v", err)
|
||||
log.Printf("mDNS IPv4 query failed: %v", err)
|
||||
|
||||
// Fallback to standard query (both IPv4 and IPv6)
|
||||
log.Printf("mDNS: Falling back to standard query...")
|
||||
|
||||
err = mdns.Query(&mdns.QueryParam{
|
||||
Service: "_soundtouch._tcp",
|
||||
Domain: "local.",
|
||||
Timeout: m.timeout,
|
||||
Entries: entries,
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("mDNS query completed with error: %v", err)
|
||||
} else {
|
||||
log.Printf("mDNS query completed successfully")
|
||||
}
|
||||
} else {
|
||||
log.Printf("mDNS query completed successfully")
|
||||
log.Printf("mDNS IPv4 query completed successfully")
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -78,6 +98,12 @@ func (m *MDNSDiscoveryService) DiscoverDevices(ctx context.Context) ([]*models.D
|
||||
log.Printf("mDNS: Received service entry: Name='%s', Host='%s', Port=%d, AddrV4=%v, AddrV6=%v",
|
||||
entry.Name, entry.Host, entry.Port, entry.AddrV4, entry.AddrV6)
|
||||
|
||||
// Only process SoundTouch devices
|
||||
if !strings.Contains(entry.Name, "_soundtouch._tcp") {
|
||||
log.Printf("mDNS: Skipping non-SoundTouch service: %s", entry.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
device := m.serviceEntryToDevice(entry)
|
||||
if device != nil {
|
||||
log.Printf("mDNS: Successfully converted to device: %s at %s:%d", device.Name, device.Host, device.Port)
|
||||
@@ -170,6 +196,11 @@ func (m *MDNSDiscoveryService) serviceEntryToDevice(entry *mdns.ServiceEntry) *m
|
||||
name = strings.TrimSuffix(name, "."+soundTouchServiceType+"."+soundTouchDomain)
|
||||
}
|
||||
|
||||
// Unescape any escaped characters in the name (common in mDNS)
|
||||
name = strings.ReplaceAll(name, `\ `, " ")
|
||||
name = strings.ReplaceAll(name, `\.`, ".")
|
||||
name = strings.ReplaceAll(name, `\\`, `\`)
|
||||
|
||||
device := &models.DiscoveredDevice{
|
||||
Host: host,
|
||||
Port: port,
|
||||
@@ -182,3 +213,47 @@ func (m *MDNSDiscoveryService) serviceEntryToDevice(entry *mdns.ServiceEntry) *m
|
||||
|
||||
return device
|
||||
}
|
||||
|
||||
// getIPv4Interface returns the first suitable IPv4 network interface
|
||||
func (m *MDNSDiscoveryService) getIPv4Interface() *net.Interface {
|
||||
interfaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
log.Printf("mDNS: Failed to get network interfaces: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, iface := range interfaces {
|
||||
// Skip loopback, down interfaces, and point-to-point interfaces
|
||||
if iface.Flags&net.FlagLoopback != 0 ||
|
||||
iface.Flags&net.FlagUp == 0 ||
|
||||
iface.Flags&net.FlagPointToPoint != 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if this interface has IPv4 addresses
|
||||
addrs, err := iface.Addrs()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
hasIPv4 := false
|
||||
|
||||
for _, addr := range addrs {
|
||||
if ipNet, ok := addr.(*net.IPNet); ok {
|
||||
if ipNet.IP.To4() != nil && !ipNet.IP.IsLoopback() {
|
||||
hasIPv4 = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if hasIPv4 {
|
||||
log.Printf("mDNS: Using IPv4 interface: %s", iface.Name)
|
||||
return &iface
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("mDNS: No suitable IPv4 interface found")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user