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,