Fix migration check

This commit is contained in:
Tobias Gesellchen
2026-02-22 18:58:20 +01:00
parent 6a65376784
commit f50ee1131e
3 changed files with 225 additions and 59 deletions
+100
View File
@@ -167,6 +167,106 @@ func TestDNSDiscovery_StartTCP(t *testing.T) {
}
}
func TestDNSDiscovery_SelfForwarding(t *testing.T) {
serviceIP := "soundtouch.local"
upstreamDNS := []string{"127.0.0.1:5357"}
d := NewDNSDiscovery(upstreamDNS, serviceIP)
// Mock upstream DNS server for soundtouch.local
mux := dns.NewServeMux()
mux.HandleFunc("soundtouch.local.", func(w dns.ResponseWriter, r *dns.Msg) {
m := new(dns.Msg)
m.SetReply(r)
rr, _ := dns.NewRR("soundtouch.local. 60 IN A 192.168.178.10")
m.Answer = append(m.Answer, rr)
_ = w.WriteMsg(m)
})
ts := &dns.Server{Addr: "127.0.0.1:5357", Net: "udp", Handler: mux, ReadTimeout: 100 * time.Millisecond, WriteTimeout: 100 * time.Millisecond}
go func() {
_ = ts.ListenAndServe()
}()
defer func() { _ = ts.Shutdown() }()
time.Sleep(100 * time.Millisecond)
m := new(dns.Msg)
m.SetQuestion("soundtouch.local.", dns.TypeA)
rw := &mockResponseWriter{}
d.ServeDNS(rw, m)
if rw.msg == nil {
t.Fatal("Expected a response for soundtouch.local")
}
if rw.msg.Rcode != dns.RcodeSuccess {
t.Errorf("Expected Success (0) for soundtouch.local being forwarded, got %d", rw.msg.Rcode)
}
if len(rw.msg.Answer) == 0 {
t.Fatal("Expected an answer in the response")
}
if a, ok := rw.msg.Answer[0].(*dns.A); ok {
if a.A.String() != "192.168.178.10" {
t.Errorf("Expected IP 192.168.178.10, got %s", a.A.String())
}
}
// Check if d.recordQuery logged it correctly.
d.mu.RLock()
host, exists := d.discovered["soundtouch.local"]
d.mu.RUnlock()
if !exists {
t.Error("Expected soundtouch.local to be recorded")
}
// It should NOT be intercepted anymore
if host != nil && host.IsIntercepted {
t.Error("Expected soundtouch.local NOT to be intercepted anymore, but forwarded")
}
}
func TestDNSDiscovery_ForwardLocal(t *testing.T) {
serviceIP := "192.168.1.100"
upstreamDNS := []string{"127.0.0.1:5356"}
d := NewDNSDiscovery(upstreamDNS, serviceIP)
m := new(dns.Msg)
m.SetQuestion("someone-else.local.", dns.TypeA)
rw := &mockResponseWriter{}
// Start a mock upstream DNS server that returns SUCCESS for .local
mux := dns.NewServeMux()
mux.HandleFunc("someone-else.local.", func(w dns.ResponseWriter, r *dns.Msg) {
m := new(dns.Msg)
m.SetReply(r)
rr, _ := dns.NewRR("someone-else.local. 60 IN A 192.168.1.50")
m.Answer = append(m.Answer, rr)
_ = w.WriteMsg(m)
})
ts := &dns.Server{Addr: "127.0.0.1:5356", Net: "udp", Handler: mux, ReadTimeout: 100 * time.Millisecond, WriteTimeout: 100 * time.Millisecond}
go func() {
_ = ts.ListenAndServe()
}()
defer func() { _ = ts.Shutdown() }()
time.Sleep(100 * time.Millisecond)
d.ServeDNS(rw, m)
if rw.msg == nil {
t.Fatal("Expected a response message")
}
if rw.msg.Rcode != dns.RcodeSuccess {
t.Errorf("Expected Success (0) for .local being forwarded, got %d", rw.msg.Rcode)
}
if len(rw.msg.Answer) == 0 {
t.Fatal("Expected an answer in the response")
}
}
func TestDNSDiscovery_IsRunning(t *testing.T) {
serviceIP := "192.168.1.100"
upstreamDNS := []string{"8.8.8.8"}
+73 -59
View File
@@ -287,73 +287,87 @@ func (m *Manager) checkIsMigrated(summary *MigrationSummary, deviceIP string) {
return
}
// Case 1: XML Migration
// Check if any URL in the current config points to our server (targetURL)
if summary.ParsedCurrentConfig != nil {
targetURL := m.ServerURL
// Strip protocol for comparison if needed, or just check for substring
parsedTarget, err := url.Parse(targetURL)
if err == nil {
targetHost := parsedTarget.Hostname()
if strings.Contains(summary.ParsedCurrentConfig.MargeServerUrl, targetHost) ||
strings.Contains(summary.ParsedCurrentConfig.StatsServerUrl, targetHost) ||
strings.Contains(summary.ParsedCurrentConfig.SwUpdateUrl, targetHost) ||
strings.Contains(summary.ParsedCurrentConfig.BmxRegistryUrl, targetHost) {
summary.IsMigrated = true
return
}
}
}
// Case 2: /etc/hosts + Trust CA Migration
// Check if /etc/hosts contains redirections for Bose domains
client := m.NewSSH(deviceIP)
if m.isXMLMigrated(summary) || m.isHostsMigrated(client, summary) || m.isResolvConfMigrated(client, summary) {
summary.IsMigrated = true
}
}
// isXMLMigrated checks whether current XML config already points to our server.
func (m *Manager) isXMLMigrated(summary *MigrationSummary) bool {
if summary.ParsedCurrentConfig == nil {
return false
}
parsedTarget, err := url.Parse(m.ServerURL)
if err != nil {
return false
}
targetHost := parsedTarget.Hostname()
return strings.Contains(summary.ParsedCurrentConfig.MargeServerUrl, targetHost) ||
strings.Contains(summary.ParsedCurrentConfig.StatsServerUrl, targetHost) ||
strings.Contains(summary.ParsedCurrentConfig.SwUpdateUrl, targetHost) ||
strings.Contains(summary.ParsedCurrentConfig.BmxRegistryUrl, targetHost)
}
// isHostsMigrated checks if /etc/hosts contains Bose domain redirections and CA is trusted.
func (m *Manager) isHostsMigrated(client SSHClient, summary *MigrationSummary) bool {
hostsContent, err := client.Run("cat /etc/hosts")
if err == nil {
boseDomains := []string{
"streaming.bose.com",
"updates.bose.com",
"stats.bose.com",
"bmx.bose.com",
}
for _, domain := range boseDomains {
if strings.Contains(hostsContent, domain) {
// If CA is also trusted, it's a strong indicator of migration
if summary.CACertTrusted {
summary.IsMigrated = true
return
}
}
if err != nil {
return false
}
boseDomains := []string{
"streaming.bose.com",
"updates.bose.com",
"stats.bose.com",
"bmx.bose.com",
}
for _, domain := range boseDomains {
if strings.Contains(hostsContent, domain) && summary.CACertTrusted {
return true
}
}
// Case 3: /etc/resolv.conf Migration (including Aftertouch hook)
// Check if /etc/resolv.conf contains our target nameserver OR if hook marker exists
if summary.SSHSuccess {
// Check for aftertouch.resolv.conf
if _, err := client.Run("[ -f /mnt/nv/aftertouch.resolv.conf ]"); err == nil {
if summary.CACertTrusted {
summary.IsMigrated = true
return
}
}
return false
}
if summary.CurrentResolvConf != "" {
targetURL := m.ServerURL
parsedTarget, err := url.Parse(targetURL)
if err == nil {
targetHost := parsedTarget.Hostname()
if strings.Contains(summary.CurrentResolvConf, targetHost) {
if summary.CACertTrusted {
summary.IsMigrated = true
return
}
}
}
}
// isResolvConfMigrated checks for Aftertouch DNS migration signals and CA trust.
func (m *Manager) isResolvConfMigrated(client SSHClient, summary *MigrationSummary) bool {
// Hook file present
if _, err := client.Run("[ -f /mnt/nv/aftertouch.resolv.conf ]"); err == nil {
return summary.CACertTrusted
}
if summary.CurrentResolvConf == "" {
return false
}
// Marker comment present
if strings.Contains(summary.CurrentResolvConf, "# Priority nameserver for Bose service redirection") && summary.CACertTrusted {
return true
}
// Match hostname or resolved IP
parsedTarget, err := url.Parse(m.ServerURL)
if err != nil {
return false
}
targetHost := parsedTarget.Hostname()
if strings.Contains(summary.CurrentResolvConf, targetHost) && summary.CACertTrusted {
return true
}
resolvedIP := m.resolveIP(targetHost, client)
if resolvedIP != "" && strings.Contains(summary.CurrentResolvConf, resolvedIP) && summary.CACertTrusted {
return true
}
return false
}
// populateDeviceInfo fills in device information from datastore and live info
+52
View File
@@ -1409,6 +1409,58 @@ func TestCheckIsMigrated(t *testing.T) {
}
})
t.Run("ResolvConf Migrated (Marker)", func(t *testing.T) {
m.NewSSH = func(host string) SSHClient {
return &mockSSH{
runFunc: func(command string) (string, error) {
if command == "cat /etc/hosts" {
return "127.0.0.1\tlocalhost", nil
}
if command == "[ -f /mnt/nv/aftertouch.resolv.conf ]" {
return "", fmt.Errorf("not found")
}
return "", nil
},
}
}
summary := &MigrationSummary{
SSHSuccess: true,
CACertTrusted: true,
CurrentResolvConf: "# Priority nameserver for Bose service redirection\nnameserver 192.168.1.1\n",
}
m.checkIsMigrated(summary, "127.0.0.1")
if !summary.IsMigrated {
t.Errorf("Expected IsMigrated to be true for resolv.conf migration with marker comment")
}
})
t.Run("ResolvConf Migrated (IP)", func(t *testing.T) {
m.NewSSH = func(host string) SSHClient {
return &mockSSH{
runFunc: func(command string) (string, error) {
if command == "cat /etc/hosts" {
return "127.0.0.1\tlocalhost", nil
}
if command == "[ -f /mnt/nv/aftertouch.resolv.conf ]" {
return "", fmt.Errorf("not found")
}
// Mock resolveIP by mocking its SSH commands if any, or just wait for it to return targetHost
return "", nil
},
}
}
// m.ServerURL is "http://aftertouch:8000" in this test (see top of TestCheckIsMigrated)
summary := &MigrationSummary{
SSHSuccess: true,
CACertTrusted: true,
CurrentResolvConf: "nameserver aftertouch\n",
}
m.checkIsMigrated(summary, "127.0.0.1")
if !summary.IsMigrated {
t.Errorf("Expected IsMigrated to be true for resolv.conf migration with matching hostname/IP")
}
})
t.Run("Not Migrated", func(t *testing.T) {
m.NewSSH = func(host string) SSHClient {
return &mockSSH{