mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-18 16:46:17 +00:00
Fallback to the system's dns resolver by default
This commit is contained in:
+35
-35
@@ -14,7 +14,7 @@ import (
|
||||
// DNSDiscovery handles DNS queries and records discovered hosts.
|
||||
type DNSDiscovery struct {
|
||||
// Configuration
|
||||
upstreamDNS string
|
||||
upstreamDNS []string
|
||||
serviceIP string
|
||||
|
||||
// State
|
||||
@@ -48,7 +48,7 @@ type DiscoveredHost struct {
|
||||
}
|
||||
|
||||
// NewDNSDiscovery creates a new DNSDiscovery instance.
|
||||
func NewDNSDiscovery(upstreamDNS, serviceIP string) *DNSDiscovery {
|
||||
func NewDNSDiscovery(upstreamDNS []string, serviceIP string) *DNSDiscovery {
|
||||
return &DNSDiscovery{
|
||||
upstreamDNS: upstreamDNS,
|
||||
serviceIP: serviceIP,
|
||||
@@ -83,7 +83,7 @@ func (d *DNSDiscovery) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
|
||||
d.throttledLog(fmt.Sprintf("[DNS] Intercepting %s (type %d) -> %s", hostname, q.Qtype, d.serviceIP))
|
||||
} else {
|
||||
// Forward to real DNS
|
||||
if d.upstreamDNS == "" {
|
||||
if len(d.upstreamDNS) == 0 {
|
||||
d.throttledLog("[DNS ERROR] No upstream DNS configured, cannot forward")
|
||||
|
||||
m := new(dns.Msg)
|
||||
@@ -94,7 +94,7 @@ func (d *DNSDiscovery) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
|
||||
return
|
||||
}
|
||||
|
||||
d.throttledLog(fmt.Sprintf("[DNS] Forwarding %s (type %d) to %s", hostname, q.Qtype, d.upstreamDNS))
|
||||
d.throttledLog(fmt.Sprintf("[DNS] Forwarding %s (type %d) to %v", hostname, q.Qtype, d.upstreamDNS))
|
||||
d.forward(w, r)
|
||||
}
|
||||
}
|
||||
@@ -234,44 +234,44 @@ func (d *DNSDiscovery) forward(w dns.ResponseWriter, r *dns.Msg) {
|
||||
return
|
||||
}
|
||||
|
||||
// Add port 53 if not present
|
||||
upstream := d.upstreamDNS
|
||||
if !strings.Contains(upstream, ":") {
|
||||
upstream += ":53"
|
||||
}
|
||||
|
||||
// Loop prevention: don't forward to ourselves
|
||||
if upstream == d.bindAddr || (strings.HasPrefix(upstream, "127.0.0.1:") && strings.HasSuffix(d.bindAddr, upstream[9:])) {
|
||||
d.throttledLog(fmt.Sprintf("[DNS ERROR] Refusing to forward %s to ourselves (%s)", q.Name, upstream))
|
||||
|
||||
m := new(dns.Msg)
|
||||
m.SetReply(r)
|
||||
m.Rcode = dns.RcodeServerFailure
|
||||
_ = w.WriteMsg(m)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
c := new(dns.Client)
|
||||
c.Timeout = 2 * time.Second
|
||||
|
||||
in, _, err := c.Exchange(r, upstream)
|
||||
if err != nil {
|
||||
d.throttledLog(fmt.Sprintf("[DNS ERROR] Forward failed for %s (type %d): %v", q.Name, q.Qtype, err))
|
||||
// Return a failure response instead of just dropping
|
||||
m := new(dns.Msg)
|
||||
m.SetReply(r)
|
||||
|
||||
m.Rcode = dns.RcodeServerFailure
|
||||
if err := w.WriteMsg(m); err != nil {
|
||||
log.Printf("[DNS ERROR] Failed to write failure response: %v", err)
|
||||
for _, upstream := range d.upstreamDNS {
|
||||
// Add port 53 if not present
|
||||
if !strings.Contains(upstream, ":") {
|
||||
upstream += ":53"
|
||||
}
|
||||
|
||||
return
|
||||
// Loop prevention: don't forward to ourselves
|
||||
if upstream == d.bindAddr || (strings.HasPrefix(upstream, "127.0.0.1:") && strings.HasSuffix(d.bindAddr, upstream[9:])) {
|
||||
d.throttledLog(fmt.Sprintf("[DNS ERROR] Refusing to forward %s to ourselves (%s)", q.Name, upstream))
|
||||
continue
|
||||
}
|
||||
|
||||
in, _, err := c.Exchange(r, upstream)
|
||||
if err == nil {
|
||||
if in.Rcode == dns.RcodeSuccess {
|
||||
if writeErr := w.WriteMsg(in); writeErr != nil {
|
||||
log.Printf("[DNS ERROR] Failed to write forwarded response from %s: %v", upstream, writeErr)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
d.throttledLog(fmt.Sprintf("[DNS] Upstream %s returned %s for %s, trying next", upstream, dns.RcodeToString[in.Rcode], q.Name))
|
||||
} else {
|
||||
d.throttledLog(fmt.Sprintf("[DNS ERROR] Forward failed for %s (type %d) via %s: %v", q.Name, q.Qtype, upstream, err))
|
||||
}
|
||||
}
|
||||
|
||||
if err := w.WriteMsg(in); err != nil {
|
||||
log.Printf("[DNS ERROR] Failed to write forwarded response: %v", err)
|
||||
// If we reach here, all upstreams failed
|
||||
m := new(dns.Msg)
|
||||
m.SetReply(r)
|
||||
m.Rcode = dns.RcodeServerFailure
|
||||
|
||||
if err := w.WriteMsg(m); err != nil {
|
||||
log.Printf("[DNS ERROR] Failed to write failure response: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
|
||||
func TestDNSDiscovery_Interception(t *testing.T) {
|
||||
serviceIP := "192.168.1.100"
|
||||
upstreamDNS := "8.8.8.8"
|
||||
upstreamDNS := []string{"8.8.8.8"}
|
||||
d := NewDNSDiscovery(upstreamDNS, serviceIP)
|
||||
|
||||
// Test intercepting Bose service
|
||||
@@ -79,7 +79,7 @@ func TestDNSDiscovery_Forwarding(t *testing.T) {
|
||||
// This test is harder because it needs a real upstream or a mock.
|
||||
// For now, let's just test that it calls forward and record.
|
||||
serviceIP := "192.168.1.100"
|
||||
upstreamDNS := "127.0.0.1:5353" // Use a port that is likely closed or we can mock
|
||||
upstreamDNS := []string{"127.0.0.1:5353"} // Use a port that is likely closed or we can mock
|
||||
d := NewDNSDiscovery(upstreamDNS, serviceIP)
|
||||
|
||||
m := new(dns.Msg)
|
||||
@@ -120,7 +120,7 @@ func TestDNSDiscovery_Forwarding(t *testing.T) {
|
||||
|
||||
func TestDNSDiscovery_StartTCP(t *testing.T) {
|
||||
serviceIP := "192.168.1.100"
|
||||
upstreamDNS := "8.8.8.8"
|
||||
upstreamDNS := []string{"8.8.8.8"}
|
||||
d := NewDNSDiscovery(upstreamDNS, serviceIP)
|
||||
|
||||
addr := "127.0.0.1:5354"
|
||||
@@ -169,7 +169,7 @@ func TestDNSDiscovery_StartTCP(t *testing.T) {
|
||||
|
||||
func TestDNSDiscovery_IsRunning(t *testing.T) {
|
||||
serviceIP := "192.168.1.100"
|
||||
upstreamDNS := "8.8.8.8"
|
||||
upstreamDNS := []string{"8.8.8.8"}
|
||||
d := NewDNSDiscovery(upstreamDNS, serviceIP)
|
||||
|
||||
addr := "127.0.0.1:5355"
|
||||
@@ -214,7 +214,7 @@ func (m *mockResponseWriter) TsigTimersOnly(bool) {}
|
||||
func (m *mockResponseWriter) Hijack() {}
|
||||
|
||||
func TestDNSDiscovery_LogThrottling(t *testing.T) {
|
||||
d := NewDNSDiscovery("8.8.8.8", "192.168.1.100")
|
||||
d := NewDNSDiscovery([]string{"8.8.8.8"}, "192.168.1.100")
|
||||
|
||||
// Capture log output
|
||||
var logBuf strings.Builder
|
||||
@@ -247,7 +247,7 @@ func TestDNSDiscovery_LogThrottling(t *testing.T) {
|
||||
func TestDNSDiscovery_LoopPrevention(t *testing.T) {
|
||||
serviceIP := "192.168.1.100"
|
||||
bindAddr := "127.0.0.1:53"
|
||||
upstreamDNS := "127.0.0.1:53"
|
||||
upstreamDNS := []string{"127.0.0.1:53"}
|
||||
d := NewDNSDiscovery(upstreamDNS, serviceIP)
|
||||
d.bindAddr = bindAddr
|
||||
|
||||
@@ -274,7 +274,7 @@ func TestDNSDiscovery_LoopPrevention(t *testing.T) {
|
||||
|
||||
func TestDNSDiscovery_EmptyUpstream(t *testing.T) {
|
||||
serviceIP := "192.168.1.100"
|
||||
upstreamDNS := "" // Empty upstream
|
||||
var upstreamDNS []string // Empty upstream
|
||||
d := NewDNSDiscovery(upstreamDNS, serviceIP)
|
||||
d.bindAddr = ":53"
|
||||
|
||||
@@ -298,7 +298,7 @@ func TestDNSDiscovery_EmptyUpstream(t *testing.T) {
|
||||
func TestDNSDiscovery_ForwardTimeout(t *testing.T) {
|
||||
serviceIP := "192.168.1.100"
|
||||
// Use an IP that is unroutable or doesn't exist on the network to ensure timeout
|
||||
upstreamDNS := "192.0.2.1:53" // TEST-NET-1, usually non-routable
|
||||
upstreamDNS := []string{"192.0.2.1:53"} // TEST-NET-1, usually non-routable
|
||||
d := NewDNSDiscovery(upstreamDNS, serviceIP)
|
||||
|
||||
m := new(dns.Msg)
|
||||
@@ -318,3 +318,58 @@ func TestDNSDiscovery_ForwardTimeout(t *testing.T) {
|
||||
t.Errorf("Expected RcodeServerFailure after timeout")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDNSDiscovery_MultipleUpstreams(t *testing.T) {
|
||||
serviceIP := "192.168.1.100"
|
||||
|
||||
// Mock server 1: returns NXDOMAIN
|
||||
mux1 := dns.NewServeMux()
|
||||
mux1.HandleFunc("test.com.", func(w dns.ResponseWriter, r *dns.Msg) {
|
||||
m := new(dns.Msg)
|
||||
m.SetReply(r)
|
||||
m.Rcode = dns.RcodeNameError
|
||||
_ = w.WriteMsg(m)
|
||||
})
|
||||
ts1 := &dns.Server{Addr: "127.0.0.1:5356", Net: "udp", Handler: mux1}
|
||||
go func() { _ = ts1.ListenAndServe() }()
|
||||
defer func() { _ = ts1.Shutdown() }()
|
||||
|
||||
// Mock server 2: succeeds
|
||||
mux2 := dns.NewServeMux()
|
||||
mux2.HandleFunc("test.com.", func(w dns.ResponseWriter, r *dns.Msg) {
|
||||
m := new(dns.Msg)
|
||||
m.SetReply(r)
|
||||
m.Answer = append(m.Answer, &dns.A{
|
||||
Hdr: dns.RR_Header{Name: r.Question[0].Name, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 300},
|
||||
A: net.ParseIP("1.2.3.4"),
|
||||
})
|
||||
_ = w.WriteMsg(m)
|
||||
})
|
||||
ts2 := &dns.Server{Addr: "127.0.0.1:5357", Net: "udp", Handler: mux2}
|
||||
go func() { _ = ts2.ListenAndServe() }()
|
||||
defer func() { _ = ts2.Shutdown() }()
|
||||
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
upstreamDNS := []string{"127.0.0.1:5356", "127.0.0.1:5357"}
|
||||
d := NewDNSDiscovery(upstreamDNS, serviceIP)
|
||||
|
||||
m := new(dns.Msg)
|
||||
m.SetQuestion("test.com.", dns.TypeA)
|
||||
rw := &mockResponseWriter{}
|
||||
|
||||
d.forward(rw, m)
|
||||
|
||||
if rw.msg == nil {
|
||||
t.Fatal("Expected a response message")
|
||||
}
|
||||
|
||||
// It should succeed because it falls back to the second upstream
|
||||
if rw.msg.Rcode != dns.RcodeSuccess {
|
||||
t.Errorf("Expected RcodeSuccess (0), got %d. Fallback failed.", rw.msg.Rcode)
|
||||
}
|
||||
|
||||
if len(rw.msg.Answer) == 0 {
|
||||
t.Fatal("Expected an answer from the second upstream")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -709,7 +709,7 @@ type Settings struct {
|
||||
DiscoveryEnabled bool `json:"discovery_enabled"`
|
||||
EnableSoundcorkProxy bool `json:"enable_soundcork_proxy"`
|
||||
DNSEnabled bool `json:"dns_enabled"`
|
||||
DNSUpstream string `json:"dns_upstream,omitempty"`
|
||||
DNSUpstream []string `json:"dns_upstream,omitempty"`
|
||||
DNSBindAddr string `json:"dns_bind_addr,omitempty"`
|
||||
Shortcuts map[string]int `json:"shortcuts,omitempty"`
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"fmt"
|
||||
@@ -170,7 +171,7 @@ func (s *Server) HandleGetSettings(w http.ResponseWriter, _ *http.Request) {
|
||||
"dns_enabled": dnsEnabled,
|
||||
"dns_running": dnsRunning,
|
||||
"dns_actual_bind": actualBind,
|
||||
"dns_upstream": dnsUpstream,
|
||||
"dns_upstream": strings.Join(dnsUpstream, ","),
|
||||
"dns_bind_addr": dnsBindAddr,
|
||||
"enable_soundcork_proxy": enableSoundcorkProxy,
|
||||
"redact_logs": redact,
|
||||
@@ -223,7 +224,20 @@ func (s *Server) HandleUpdateSettings(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
s.discoveryEnabled = settings.DiscoveryEnabled
|
||||
s.dnsEnabled = settings.DNSEnabled
|
||||
s.dnsUpstream = settings.DNSUpstream
|
||||
|
||||
// Handle comma-separated upstream DNS servers
|
||||
var upstreamList []string
|
||||
|
||||
if settings.DNSUpstream != "" {
|
||||
for _, u := range strings.Split(settings.DNSUpstream, ",") {
|
||||
u = strings.TrimSpace(u)
|
||||
if u != "" {
|
||||
upstreamList = append(upstreamList, u)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
s.dnsUpstream = upstreamList
|
||||
s.dnsBindAddr = settings.DNSBindAddr
|
||||
|
||||
s.enableSoundcorkProxy = settings.EnableSoundcorkProxy
|
||||
@@ -260,12 +274,12 @@ func (s *Server) HandleUpdateSettings(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
|
||||
dnsEnabled := s.dnsEnabled
|
||||
dnsUpstream := s.dnsUpstream
|
||||
dnsUpstreamStr := strings.Join(s.dnsUpstream, ",")
|
||||
dnsBindAddr := s.dnsBindAddr
|
||||
|
||||
s.mu.Unlock()
|
||||
|
||||
s.SetDNSSettings(dnsEnabled, dnsUpstream, dnsBindAddr)
|
||||
s.SetDNSSettings(dnsEnabled, dnsUpstreamStr, dnsBindAddr)
|
||||
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to save settings: "+err.Error(), http.StatusInternalServerError)
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -17,6 +18,7 @@ import (
|
||||
"github.com/gesellix/bose-soundtouch/pkg/service/proxy"
|
||||
"github.com/gesellix/bose-soundtouch/pkg/service/setup"
|
||||
"github.com/gesellix/bose-soundtouch/pkg/service/spotify"
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// Server handles HTTP requests for the SoundTouch service.
|
||||
@@ -34,7 +36,7 @@ type Server struct {
|
||||
discoveryInterval time.Duration
|
||||
discoveryEnabled bool
|
||||
dnsEnabled bool
|
||||
dnsUpstream string
|
||||
dnsUpstream []string
|
||||
dnsBindAddr string
|
||||
enableSoundcorkProxy bool
|
||||
shortcuts map[string]int
|
||||
@@ -88,6 +90,47 @@ func (s *Server) SetDiscoverySettings(interval time.Duration, enabled bool) {
|
||||
s.discoveryEnabled = enabled
|
||||
}
|
||||
|
||||
// parseUpstreamDNS splits a comma-separated string of DNS servers.
|
||||
func parseUpstreamDNS(upstream string) []string {
|
||||
var upstreamList []string
|
||||
|
||||
if upstream != "" {
|
||||
for _, u := range strings.Split(upstream, ",") {
|
||||
u = strings.TrimSpace(u)
|
||||
if u != "" {
|
||||
upstreamList = append(upstreamList, u)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return upstreamList
|
||||
}
|
||||
|
||||
// getSystemDNS returns the DNS servers from /etc/resolv.conf.
|
||||
func getSystemDNS() []string {
|
||||
config, _ := dns.ClientConfigFromFile("/etc/resolv.conf")
|
||||
if config != nil && len(config.Servers) > 0 {
|
||||
return config.Servers
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// areUpstreamsEqual compares two slices of DNS server addresses.
|
||||
func areUpstreamsEqual(a, b []string) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
|
||||
for i := range a {
|
||||
if a[i] != b[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// SetDNSSettings sets the DNS discovery settings for the server.
|
||||
func (s *Server) SetDNSSettings(enabled bool, upstream, bind string) {
|
||||
s.mu.Lock()
|
||||
@@ -97,11 +140,23 @@ func (s *Server) SetDNSSettings(enabled bool, upstream, bind string) {
|
||||
oldUpstream := s.dnsUpstream
|
||||
|
||||
s.dnsEnabled = enabled
|
||||
s.dnsUpstream = upstream
|
||||
s.dnsBindAddr = bind
|
||||
|
||||
upstreamList := parseUpstreamDNS(upstream)
|
||||
|
||||
// Try to get system DNS if none provided
|
||||
if enabled && len(upstreamList) == 0 {
|
||||
upstreamList = getSystemDNS()
|
||||
if len(upstreamList) > 0 {
|
||||
log.Printf("[DNS] Using system DNS servers from /etc/resolv.conf: %v", upstreamList)
|
||||
}
|
||||
}
|
||||
|
||||
s.dnsUpstream = upstreamList
|
||||
upstreamChanged := !areUpstreamsEqual(upstreamList, oldUpstream)
|
||||
|
||||
if s.dnsDiscovery != nil {
|
||||
if !enabled || bind != oldBind || upstream != oldUpstream {
|
||||
if !enabled || bind != oldBind || upstreamChanged {
|
||||
log.Printf("[DNS] Settings changed, stopping DNS discovery server")
|
||||
|
||||
_ = s.dnsDiscovery.Shutdown()
|
||||
@@ -109,8 +164,8 @@ func (s *Server) SetDNSSettings(enabled bool, upstream, bind string) {
|
||||
}
|
||||
}
|
||||
|
||||
if enabled && upstream == "" {
|
||||
log.Printf("[DNS] Cannot start DNS discovery server: upstream DNS is empty")
|
||||
if enabled && len(upstreamList) == 0 {
|
||||
log.Printf("[DNS] Cannot start DNS discovery server: upstream DNS is empty and no system DNS found")
|
||||
|
||||
s.dnsEnabled = false
|
||||
|
||||
@@ -118,28 +173,32 @@ func (s *Server) SetDNSSettings(enabled bool, upstream, bind string) {
|
||||
}
|
||||
|
||||
if enabled && s.dnsDiscovery == nil {
|
||||
log.Printf("[DNS] Starting DNS discovery server on %s", bind)
|
||||
|
||||
u, _ := url.Parse(s.serverURL)
|
||||
|
||||
serviceIP := u.Hostname()
|
||||
if serviceIP == "localhost" || serviceIP == "" {
|
||||
serviceIP = "127.0.0.1"
|
||||
}
|
||||
|
||||
if s.sm != nil {
|
||||
serviceIP = s.sm.GetResolvedIP(serviceIP)
|
||||
}
|
||||
|
||||
s.dnsDiscovery = discovery.NewDNSDiscovery(upstream, serviceIP)
|
||||
go func(d *discovery.DNSDiscovery, addr string) {
|
||||
if err := d.Start(addr); err != nil {
|
||||
log.Printf("Warning: DNS discovery server error: %v", err)
|
||||
}
|
||||
}(s.dnsDiscovery, bind)
|
||||
s.startDNSDiscovery(bind, upstreamList)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) startDNSDiscovery(bind string, upstreamList []string) {
|
||||
log.Printf("[DNS] Starting DNS discovery server on %s", bind)
|
||||
|
||||
u, _ := url.Parse(s.serverURL)
|
||||
|
||||
serviceIP := u.Hostname()
|
||||
if serviceIP == "localhost" || serviceIP == "" {
|
||||
serviceIP = "127.0.0.1"
|
||||
}
|
||||
|
||||
if s.sm != nil {
|
||||
serviceIP = s.sm.GetResolvedIP(serviceIP)
|
||||
}
|
||||
|
||||
s.dnsDiscovery = discovery.NewDNSDiscovery(upstreamList, serviceIP)
|
||||
go func(d *discovery.DNSDiscovery, addr string) {
|
||||
if err := d.Start(addr); err != nil {
|
||||
log.Printf("Warning: DNS discovery server error: %v", err)
|
||||
}
|
||||
}(s.dnsDiscovery, bind)
|
||||
}
|
||||
|
||||
// GetDNSRunning returns whether DNS discovery is active and its bind address.
|
||||
func (s *Server) GetDNSRunning() (bool, string) {
|
||||
s.mu.RLock()
|
||||
|
||||
@@ -103,6 +103,43 @@ pre { background-color: #eee; padding: 10px; overflow-x: auto; font-size: 12px;
|
||||
.status-success { background-color: #e8f5e9; color: #2e7d32; }
|
||||
.status-error { background-color: #ffebee; color: #c62828; }
|
||||
|
||||
.info-toggle {
|
||||
display: inline-block;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
line-height: 18px;
|
||||
text-align: center;
|
||||
background-color: #607D8B;
|
||||
color: white;
|
||||
border-radius: 50%;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
margin-left: 5px;
|
||||
font-style: normal;
|
||||
user-select: none;
|
||||
}
|
||||
.info-toggle:hover {
|
||||
background-color: #455A64;
|
||||
}
|
||||
.info-details {
|
||||
display: none;
|
||||
background-color: #f0f7ff;
|
||||
border: 1px solid #d0e0f0;
|
||||
padding: 10px;
|
||||
margin-top: 5px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.85em;
|
||||
color: #333;
|
||||
line-height: 1.4;
|
||||
max-width: 400px;
|
||||
}
|
||||
.info-details code {
|
||||
background-color: #e3f2fd;
|
||||
padding: 2px 4px;
|
||||
border-radius: 3px;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.badge {
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
|
||||
@@ -108,8 +108,12 @@
|
||||
</label>
|
||||
<div style="margin-left: 20px; margin-bottom: 5px;">
|
||||
<label for="dns-upstream">Upstream DNS:</label>
|
||||
<input type="text" id="dns-upstream" placeholder="8.8.8.8" style="width: 150px;">
|
||||
<span style="font-size: 0.8em; color: #666; margin-left: 5px;">(For non-intercepted queries)</span>
|
||||
<input type="text" id="dns-upstream" placeholder="8.8.8.8" style="width: 200px;">
|
||||
<span class="info-toggle" onclick="toggleInfo('dns-upstream-info')">ⓘ</span>
|
||||
<div id="dns-upstream-info" class="info-details">
|
||||
Optional: comma-separated list of DNS servers (e.g., <code>1.1.1.1, 8.8.8.8</code>).
|
||||
If empty, AfterTouch defaults to the system nameservers (from <code>/etc/resolv.conf</code>).
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-left: 20px;">
|
||||
<label for="dns-bind">DNS Bind Address:</label>
|
||||
|
||||
@@ -42,6 +42,13 @@ async function fetchSpotifyStatus() {
|
||||
}
|
||||
}
|
||||
|
||||
function toggleInfo(id) {
|
||||
const el = document.getElementById(id);
|
||||
if (el) {
|
||||
el.style.display = el.style.display === 'block' ? 'none' : 'block';
|
||||
}
|
||||
}
|
||||
|
||||
async function linkSpotify() {
|
||||
try {
|
||||
const response = await fetch('/mgmt/spotify/init', { method: 'POST' });
|
||||
|
||||
Reference in New Issue
Block a user