diff --git a/dhcp-clients-webapp-backend/pkg/uibackend/dnsmasq.go b/dhcp-clients-webapp-backend/pkg/uibackend/dnsmasq.go index cc1c6a8..9335457 100644 --- a/dhcp-clients-webapp-backend/pkg/uibackend/dnsmasq.go +++ b/dhcp-clients-webapp-backend/pkg/uibackend/dnsmasq.go @@ -70,10 +70,8 @@ func chaosTXTQueryInteger(server, query string, timeout time.Duration) (int, err return intVal, nil } -func getDnsStats() (DnsServerStats, error) { - - // this code is meant to be executed on the same machine/container where dnsmasq is running, so: - dnsServer := "localhost" +func getDnsStats(serverHost string, serverPort int) (DnsServerStats, error) { + dnsServer := fmt.Sprintf("%s:%d", serverHost, serverPort) // since the server is local, the max query duration is expected to be small dnsTimeout := time.Duration(500 * time.Millisecond) diff --git a/dhcp-clients-webapp-backend/pkg/uibackend/dnsmasq_test.go b/dhcp-clients-webapp-backend/pkg/uibackend/dnsmasq_test.go new file mode 100644 index 0000000..9b74fe6 --- /dev/null +++ b/dhcp-clients-webapp-backend/pkg/uibackend/dnsmasq_test.go @@ -0,0 +1,99 @@ +package uibackend + +import ( + "fmt" + "net" + "os" + "os/exec" + "testing" + "time" +) + +func TestGetDnsStats_NoUpstreamServers(t *testing.T) { + // Start a temporary dnsmasq instance + dnsmasqCmd := exec.Command("dnsmasq", "--port=12345", "--cache-size=100", "--no-daemon", "--no-resolv") // Adjust arguments as needed + if err := dnsmasqCmd.Start(); err != nil { + t.Fatalf("Failed to start dnsmasq: %v", err) + } + defer func() { + if err := dnsmasqCmd.Process.Kill(); err != nil { + t.Errorf("Failed to kill dnsmasq: %v", err) + } + }() + + // Wait for dnsmasq to start listening + for i := 0; i < 10; i++ { + if conn, err := net.DialTimeout("tcp", "localhost:12345", 1*time.Second); err == nil { + conn.Close() + break + } + time.Sleep(100 * time.Millisecond) + } + + stats, err := getDnsStats("localhost", 12345) + if err != nil { + t.Fatalf("getDnsStats failed: %v", err) + } + + // Assertions + if stats.CacheSize != 100 { + t.Errorf("Unexpected CacheSize: got %d, want %d", stats.CacheSize, 100) + } + + // Check for upstream servers. Since we started with --no-resolv, there shouldn't be any upstream servers initially. + if len(stats.UpstreamServers) != 0 { + t.Errorf("Unexpected Upstream Servers found: %v", stats.UpstreamServers) + } + +} + +func TestGetDnsStats_WithUpstreamServers(t *testing.T) { + // Test with resolv-file, simulating an upstream server + resolvFileContent := "nameserver 8.8.4.4" // Example upstream server + resolvFilePath := "/tmp/resolv.conf" // Choose a temporary file + err := writeTempFile(resolvFilePath, resolvFileContent) + if err != nil { + t.Fatalf("Failed to write temporary resolv file: %v", err) + } + + // Restart dnsmasq with the resolv file + dnsmasqCmd := exec.Command("dnsmasq", "--port=12346", "--cache-size=100", "--no-daemon", fmt.Sprintf("--resolv-file=%s", resolvFilePath)) + if err := dnsmasqCmd.Start(); err != nil { + t.Fatalf("Failed to restart dnsmasq with resolv-file: %v", err) + } + defer func() { + if err := dnsmasqCmd.Process.Kill(); err != nil { + t.Errorf("Failed to kill dnsmasq: %v", err) + } + }() + for i := 0; i < 10; i++ { + if conn, err := net.DialTimeout("tcp", "localhost:12346", 1*time.Second); err == nil { + conn.Close() + break + } + time.Sleep(100 * time.Millisecond) + } + + stats, err := getDnsStats("localhost", 12346) + if err != nil { + t.Fatalf("getDnsStats failed with resolv-file: %v", err) + } + if len(stats.UpstreamServers) != 1 { + t.Errorf("Expected Upstream Servers but found none") + } + if stats.UpstreamServers[0].ServerURL != "8.8.4.4#53" { + t.Errorf("Expected google upstream Servers but found something else") + } +} + +// Helper function to write to a temporary file +func writeTempFile(filePath, content string) error { // ... (implementation remains the same) + file, err := os.Create(filePath) + if err != nil { + return err + } + defer file.Close() + + _, err = file.WriteString(content) + return err +} diff --git a/dhcp-clients-webapp-backend/pkg/uibackend/types.go b/dhcp-clients-webapp-backend/pkg/uibackend/types.go index 5534693..213bf2d 100644 --- a/dhcp-clients-webapp-backend/pkg/uibackend/types.go +++ b/dhcp-clients-webapp-backend/pkg/uibackend/types.go @@ -60,6 +60,7 @@ type AddonConfig struct { // DNS dnsEnable bool dnsDomain string + dnsPort int } // UnmarshalJSON reads the configuration of this Home Assistant addon and converts it @@ -92,6 +93,7 @@ func (b *AddonConfig) UnmarshalJSON(data []byte) error { DnsServer struct { Enable bool `json:"enable"` DnsDomain string `json:"dns_domain"` + Port int `json:"port"` } `json:"dns_server"` WebUI struct { @@ -180,6 +182,7 @@ func (b *AddonConfig) UnmarshalJSON(data []byte) error { b.addressReservationLease = cfg.DhcpServer.AddressReservationLease b.dnsEnable = cfg.DnsServer.Enable b.dnsDomain = cfg.DnsServer.DnsDomain + b.dnsPort = cfg.DnsServer.Port return nil } diff --git a/dhcp-clients-webapp-backend/pkg/uibackend/uibackend.go b/dhcp-clients-webapp-backend/pkg/uibackend/uibackend.go index c33eeeb..ec0594c 100644 --- a/dhcp-clients-webapp-backend/pkg/uibackend/uibackend.go +++ b/dhcp-clients-webapp-backend/pkg/uibackend/uibackend.go @@ -224,7 +224,9 @@ func (b *UIBackend) generateWebSocketMessage() WebSocketMessage { return cmp.Compare(a.PastInfo.LastSeen.Unix(), b.PastInfo.LastSeen.Unix()) }) - dnsStats, err := getDnsStats() + // this code is meant to be executed on the same machine/container where dnsmasq is running, so + // that's why we pass "localhost" as DNS server host: + dnsStats, err := getDnsStats("localhost", b.cfg.dnsPort) if err != nil { b.logger.Warnf("failed to get updated DNS stats: %s", err.Error()) // keep going diff --git a/dhcp-clients-webapp-backend/templates/dnsmasq-dhcp.js b/dhcp-clients-webapp-backend/templates/dnsmasq-dhcp.js index d759e8d..a88935b 100644 --- a/dhcp-clients-webapp-backend/templates/dnsmasq-dhcp.js +++ b/dhcp-clients-webapp-backend/templates/dnsmasq-dhcp.js @@ -167,6 +167,87 @@ function initAll() { initTableDarkOrLightTheme() } +function processWebSocketDHCPCurrentClients(data) { + console.log("Websocket connection: received " + data.current_clients.length + " current DHCP clients from websocket"); + + // rerender the CURRENT table + tableData = []; + dhcp_addresses_used = 0; + dhcp_static_ip = 0; + data.current_clients.forEach(function (item, index) { + console.log(`CurrentItem ${index + 1}:`, item); + + if (item.is_inside_dhcp_pool) + dhcp_addresses_used += 1; + + static_ip_str = "NO"; + if (item.has_static_ip) { + static_ip_str = "YES"; + dhcp_static_ip += 1; + } + + external_link_symbol="🡕" + //external_link_symbol="⧉" + if (item.evaluated_link) { + link_str = "" + item.evaluated_link + " " + external_link_symbol + } else { + link_str = "N/A" + } + + // append new row + tableData.push([index + 1, + item.friendly_name, item.lease.hostname, link_str, + item.lease.ip_addr, item.lease.mac_addr, + formatTimeLeft(item.lease.expires), static_ip_str]); + }); + table_current.clear().rows.add(tableData).draw(false /* do not reset page position */); + + return dhcp_static_ip, dhcp_addresses_used +} + +function processWebSocketDHCPPastClients(data) { + console.log("Websocket connection: received " + data.past_clients.length + " past DHCP clients from websocket"); + + // rerender the PAST table + tableData = []; + data.past_clients.forEach(function (item, index) { + console.log(`PastItem ${index + 1}:`, item); + + static_ip_str = "NO"; + if (item.has_static_ip) { + static_ip_str = "YES"; + } + + // append new row + tableData.push([index + 1, + item.friendly_name, item.past_info.hostname, + item.past_info.mac_addr, static_ip_str, + formatTimeSince(item.past_info.last_seen), item.notes]); + }); + table_past.clear().rows.add(tableData).draw(false /* do not reset page position */); +} + +function updateDHCPStatus(data, dhcp_static_ip, dhcp_addresses_used, messageElem) { + // compute DHCP pool usage + var usagePerc = 0 + if (templated_dhcpPoolSize > 0) { + usagePerc = 100 * dhcp_addresses_used / templated_dhcpPoolSize + + // truncate to only 1 digit accuracy + usagePerc = Math.round(usagePerc * 10) / 10 + } + + // format server uptime + uptime_str = formatTimeSince(templated_dhcpServerStartTime) + + // update the message + messageElem.innerHTML = "" + data.current_clients.length + " DHCP current clients hold a DHCP lease.
" + + dhcp_static_ip + " have a static IP address configuration.
" + + dhcp_addresses_used + " are within the DHCP pool. DHCP pool usage is at " + usagePerc + "%.
" + + "" + data.past_clients.length + " DHCP past clients contacted the server some time ago but failed to do so since last DHCP server restart, " + + uptime_str + " hh:mm:ss ago.
"; +} + function processWebSocketEvent(event) { try { @@ -187,7 +268,8 @@ function processWebSocketEvent(event) { message.innerText = "No DHCP clients so far."; } else if (!("current_clients" in data) || - !("past_clients" in data)) { + !("past_clients" in data) || + !("dns_stats" in data)) { console.error("Websocket connection: expecting a JSON matching the golang WebSocketMessage type, received something else", data); // clear the table @@ -198,77 +280,14 @@ function processWebSocketEvent(event) { } else { // console.log("DEBUG:" + JSON.stringify(data)) - console.log("Websocket connection: received " + data.current_clients.length + " current clients from websocket"); - console.log("Websocket connection: received " + data.past_clients.length + " past clients from websocket"); - - // rerender the CURRENT table - tableData = []; - dhcp_addresses_used = 0; - dhcp_static_ip = 0; - data.current_clients.forEach(function (item, index) { - console.log(`CurrentItem ${index + 1}:`, item); - - if (item.is_inside_dhcp_pool) - dhcp_addresses_used += 1; - - static_ip_str = "NO"; - if (item.has_static_ip) { - static_ip_str = "YES"; - dhcp_static_ip += 1; - } - - external_link_symbol="🡕" - //external_link_symbol="⧉" - if (item.evaluated_link) { - link_str = "" + item.evaluated_link + " " + external_link_symbol - } else { - link_str = "N/A" - } - - // append new row - tableData.push([index + 1, - item.friendly_name, item.lease.hostname, link_str, - item.lease.ip_addr, item.lease.mac_addr, - formatTimeLeft(item.lease.expires), static_ip_str]); - }); - table_current.clear().rows.add(tableData).draw(false /* do not reset page position */); - - // rerender the PAST table - tableData = []; - data.past_clients.forEach(function (item, index) { - console.log(`PastItem ${index + 1}:`, item); - - static_ip_str = "NO"; - if (item.has_static_ip) { - static_ip_str = "YES"; - } - - // append new row - tableData.push([index + 1, - item.friendly_name, item.past_info.hostname, - item.past_info.mac_addr, static_ip_str, - formatTimeSince(item.past_info.last_seen), item.notes]); - }); - table_past.clear().rows.add(tableData).draw(false /* do not reset page position */); - - // compute DHCP pool usage - var usagePerc = 0 - if (templated_dhcpPoolSize > 0) { - usagePerc = 100 * dhcp_addresses_used / templated_dhcpPoolSize - - // truncate to only 1 digit accuracy - usagePerc = Math.round(usagePerc * 10) / 10 - } - // format server uptime - uptime_str = formatTimeSince(templated_dhcpServerStartTime) + // process DHCP + dhcp_static_ip, dhcp_addresses_used = processWebSocketDHCPCurrentClients(data) + processWebSocketDHCPPastClients(data) + updateDHCPStatus(data, dhcp_static_ip, dhcp_addresses_used, message) - // update the message - message.innerHTML = "" + data.current_clients.length + " DHCP current clients hold a DHCP lease.
" + - dhcp_static_ip + " have a static IP address configuration.
" + - dhcp_addresses_used + " are within the DHCP pool. DHCP pool usage is at " + usagePerc + "%.
" + - "" + data.past_clients.length + " DHCP past clients contacted the server some time ago but failed to do so since last DHCP server restart, " + - uptime_str + " hh:mm:ss ago.
"; + // process DNS + } } diff --git a/test-options.json b/test-options.json index a12b1c8..840bcb4 100644 --- a/test-options.json +++ b/test-options.json @@ -50,6 +50,7 @@ }, "dns_server": { "enable": true, + "port": 53, "dns_domain": "lan", "cache_size": 10000, "log_requests": false,