Standardize codebase consistency: naming, JSON tags, logging, error wrapping
- JSON tags: fix snake_case to camelCase in dnsmasq (configFile, domainsConfigured, lastRestart), crowdsec Machine (lastPush, lastHeartbeat), network (primaryIP, primaryInterface). cscli raw parsing structs keep snake_case to match CLI output. - Error wrapping: fix %v to %w in enableAuthelia for proper error chain preservation - Naming: rename dnsmasq.ConfigGenerator to dnsmasq.Manager (matches all other packages), rename ServiceStatus to Status in dnsmasq and haproxy (matches authelia, crowdsec, etc.) - Logging: standardize all slog calls to use "component" key instead of message prefixes. Affects reconcile, dnsfilter, ddns — now consistent with dnsmasq, haproxy, nftables, sse.
This commit is contained in:
@@ -42,7 +42,7 @@ type API struct {
|
|||||||
ctx gocontext.Context // parent context for restartable goroutines
|
ctx gocontext.Context // parent context for restartable goroutines
|
||||||
reconciler *reconcile.Reconciler
|
reconciler *reconcile.Reconciler
|
||||||
secrets *secrets.Manager
|
secrets *secrets.Manager
|
||||||
dnsmasq *dnsmasq.ConfigGenerator
|
dnsmasq *dnsmasq.Manager
|
||||||
haproxy *haproxy.Manager
|
haproxy *haproxy.Manager
|
||||||
nftables *nftables.Manager
|
nftables *nftables.Manager
|
||||||
ddns *ddns.Runner
|
ddns *ddns.Runner
|
||||||
@@ -72,7 +72,7 @@ func NewAPI(dataDir, version string, allowedOrigins []string) (*API, error) {
|
|||||||
version: version,
|
version: version,
|
||||||
allowedOrigins: allowedOrigins,
|
allowedOrigins: allowedOrigins,
|
||||||
secrets: secrets.NewManager(filepath.Join(dataDir, "secrets.yaml")),
|
secrets: secrets.NewManager(filepath.Join(dataDir, "secrets.yaml")),
|
||||||
dnsmasq: dnsmasq.NewConfigGenerator(dnsmasqConfigPath),
|
dnsmasq: dnsmasq.NewManager(dnsmasqConfigPath),
|
||||||
haproxy: haproxy.NewManager(haproxyConfigPath),
|
haproxy: haproxy.NewManager(haproxyConfigPath),
|
||||||
nftables: nftables.NewManager(nftablesRulesPath),
|
nftables: nftables.NewManager(nftablesRulesPath),
|
||||||
ddns: ddns.NewRunner(),
|
ddns: ddns.NewRunner(),
|
||||||
@@ -142,7 +142,7 @@ func (api *API) StartDDNS(ctx gocontext.Context) {
|
|||||||
func (api *API) StartDNSFilter(ctx gocontext.Context) {
|
func (api *API) StartDNSFilter(ctx gocontext.Context) {
|
||||||
api.dnsFilterRunner = dnsfilter.NewRunner(api.dnsFilter, func() {
|
api.dnsFilterRunner = dnsfilter.NewRunner(api.dnsFilter, func() {
|
||||||
if err := api.dnsmasq.ReloadService(); err != nil {
|
if err := api.dnsmasq.ReloadService(); err != nil {
|
||||||
slog.Warn("dns-filter: failed to reload dnsmasq after update", "error", err)
|
slog.Warn("failed to reload dnsmasq after update", "component", "dns-filter", "error", err)
|
||||||
}
|
}
|
||||||
api.broadcastDNSFilterEvent("dns-filter:updated", "DNS filter lists updated")
|
api.broadcastDNSFilterEvent("dns-filter:updated", "DNS filter lists updated")
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -150,26 +150,26 @@ func (api *API) enableAuthelia(state *config.State) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := api.authelia.EnsureDataDir(); err != nil {
|
if err := api.authelia.EnsureDataDir(); err != nil {
|
||||||
return fmt.Errorf("Failed to create data directory: %v", err)
|
return fmt.Errorf("failed to create data directory: %w", err)
|
||||||
}
|
}
|
||||||
if err := api.ensureAutheliaSecrets(); err != nil {
|
if err := api.ensureAutheliaSecrets(); err != nil {
|
||||||
return fmt.Errorf("Failed to generate secrets: %v", err)
|
return fmt.Errorf("failed to generate secrets: %w", err)
|
||||||
}
|
}
|
||||||
if err := api.authelia.EnsureJWKS(); err != nil {
|
if err := api.authelia.EnsureJWKS(); err != nil {
|
||||||
return fmt.Errorf("Failed to generate JWKS: %v", err)
|
return fmt.Errorf("failed to generate JWKS: %w", err)
|
||||||
}
|
}
|
||||||
if err := api.authelia.EnsureUsersDB(); err != nil {
|
if err := api.authelia.EnsureUsersDB(); err != nil {
|
||||||
return fmt.Errorf("Failed to initialize user database: %v", err)
|
return fmt.Errorf("failed to initialize user database: %w", err)
|
||||||
}
|
}
|
||||||
if err := api.generateAutheliaConfig(state); err != nil {
|
if err := api.generateAutheliaConfig(state); err != nil {
|
||||||
return fmt.Errorf("Failed to generate config: %v", err)
|
return fmt.Errorf("failed to generate config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
api.ensureAuthDomain(state.Cloud.Authelia.Domain)
|
api.ensureAuthDomain(state.Cloud.Authelia.Domain)
|
||||||
|
|
||||||
if api.authelia.UserCount() > 0 {
|
if api.authelia.UserCount() > 0 {
|
||||||
if err := api.authelia.RestartService(); err != nil {
|
if err := api.authelia.RestartService(); err != nil {
|
||||||
return fmt.Errorf("Configuration saved but Authelia failed to start: %v", err)
|
return fmt.Errorf("authelia failed to start: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -83,13 +83,13 @@ func (api *API) DNSFilterUpdateConfig(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
api.dnsFilterRunner.Start(api.ctx, interval)
|
api.dnsFilterRunner.Start(api.ctx, interval)
|
||||||
|
|
||||||
slog.Info("dns-filter: enabled", "interval", interval)
|
slog.Info("enabled", "component", "dns-filter", "interval", interval)
|
||||||
} else {
|
} else {
|
||||||
// Stop the runner and clear addn-hosts
|
// Stop the runner and clear addn-hosts
|
||||||
api.dnsFilterRunner.Stop()
|
api.dnsFilterRunner.Stop()
|
||||||
api.dnsmasq.SetFilterConfPath("")
|
api.dnsmasq.SetFilterConfPath("")
|
||||||
|
|
||||||
slog.Info("dns-filter: disabled")
|
slog.Info("disabled", "component", "dns-filter")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reconcile to add/remove addn-hosts from dnsmasq config
|
// Reconcile to add/remove addn-hosts from dnsmasq config
|
||||||
@@ -162,11 +162,11 @@ func (api *API) DNSFilterRemoveList(w http.ResponseWriter, r *http.Request) {
|
|||||||
// Recompile without the removed list
|
// Recompile without the removed list
|
||||||
go func() {
|
go func() {
|
||||||
if _, err := api.dnsFilter.Compile(); err != nil {
|
if _, err := api.dnsFilter.Compile(); err != nil {
|
||||||
slog.Warn("dns-filter: recompile after removal failed", "error", err)
|
slog.Warn("recompile after removal failed", "component", "dns-filter", "error", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := api.dnsmasq.ReloadService(); err != nil {
|
if err := api.dnsmasq.ReloadService(); err != nil {
|
||||||
slog.Warn("dns-filter: reload after removal failed", "error", err)
|
slog.Warn("reload after removal failed", "component", "dns-filter", "error", err)
|
||||||
}
|
}
|
||||||
api.broadcastDNSFilterEvent("dns-filter:updated", "DNS filter lists updated")
|
api.broadcastDNSFilterEvent("dns-filter:updated", "DNS filter lists updated")
|
||||||
}()
|
}()
|
||||||
@@ -194,11 +194,11 @@ func (api *API) DNSFilterToggleList(w http.ResponseWriter, r *http.Request) {
|
|||||||
// Recompile with updated list status
|
// Recompile with updated list status
|
||||||
go func() {
|
go func() {
|
||||||
if _, err := api.dnsFilter.Compile(); err != nil {
|
if _, err := api.dnsFilter.Compile(); err != nil {
|
||||||
slog.Warn("dns-filter: recompile after toggle failed", "error", err)
|
slog.Warn("recompile after toggle failed", "component", "dns-filter", "error", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := api.dnsmasq.ReloadService(); err != nil {
|
if err := api.dnsmasq.ReloadService(); err != nil {
|
||||||
slog.Warn("dns-filter: reload after toggle failed", "error", err)
|
slog.Warn("reload after toggle failed", "component", "dns-filter", "error", err)
|
||||||
}
|
}
|
||||||
api.broadcastDNSFilterEvent("dns-filter:updated", "DNS filter lists updated")
|
api.broadcastDNSFilterEvent("dns-filter:updated", "DNS filter lists updated")
|
||||||
}()
|
}()
|
||||||
@@ -240,11 +240,11 @@ func (api *API) DNSFilterSetCustomEntry(w http.ResponseWriter, r *http.Request)
|
|||||||
// Recompile with updated custom entries
|
// Recompile with updated custom entries
|
||||||
go func() {
|
go func() {
|
||||||
if _, err := api.dnsFilter.Compile(); err != nil {
|
if _, err := api.dnsFilter.Compile(); err != nil {
|
||||||
slog.Warn("dns-filter: recompile after custom entry failed", "error", err)
|
slog.Warn("recompile after custom entry failed", "component", "dns-filter", "error", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := api.dnsmasq.ReloadService(); err != nil {
|
if err := api.dnsmasq.ReloadService(); err != nil {
|
||||||
slog.Warn("dns-filter: reload after custom entry failed", "error", err)
|
slog.Warn("reload after custom entry failed", "component", "dns-filter", "error", err)
|
||||||
}
|
}
|
||||||
api.broadcastDNSFilterEvent("dns-filter:updated", "DNS filter custom entry updated")
|
api.broadcastDNSFilterEvent("dns-filter:updated", "DNS filter custom entry updated")
|
||||||
}()
|
}()
|
||||||
@@ -268,11 +268,11 @@ func (api *API) DNSFilterRemoveCustomEntry(w http.ResponseWriter, r *http.Reques
|
|||||||
// Recompile
|
// Recompile
|
||||||
go func() {
|
go func() {
|
||||||
if _, err := api.dnsFilter.Compile(); err != nil {
|
if _, err := api.dnsFilter.Compile(); err != nil {
|
||||||
slog.Warn("dns-filter: recompile after custom removal failed", "error", err)
|
slog.Warn("recompile after custom removal failed", "component", "dns-filter", "error", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := api.dnsmasq.ReloadService(); err != nil {
|
if err := api.dnsmasq.ReloadService(); err != nil {
|
||||||
slog.Warn("dns-filter: reload after custom removal failed", "error", err)
|
slog.Warn("reload after custom removal failed", "component", "dns-filter", "error", err)
|
||||||
}
|
}
|
||||||
api.broadcastDNSFilterEvent("dns-filter:updated", "DNS filter custom entry removed")
|
api.broadcastDNSFilterEvent("dns-filter:updated", "DNS filter custom entry removed")
|
||||||
}()
|
}()
|
||||||
@@ -328,11 +328,11 @@ func (api *API) DNSFilterUploadList(w http.ResponseWriter, r *http.Request) {
|
|||||||
// Recompile with new list
|
// Recompile with new list
|
||||||
go func() {
|
go func() {
|
||||||
if _, err := api.dnsFilter.Compile(); err != nil {
|
if _, err := api.dnsFilter.Compile(); err != nil {
|
||||||
slog.Warn("dns-filter: recompile after upload failed", "error", err)
|
slog.Warn("recompile after upload failed", "component", "dns-filter", "error", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := api.dnsmasq.ReloadService(); err != nil {
|
if err := api.dnsmasq.ReloadService(); err != nil {
|
||||||
slog.Warn("dns-filter: reload after upload failed", "error", err)
|
slog.Warn("reload after upload failed", "component", "dns-filter", "error", err)
|
||||||
}
|
}
|
||||||
api.broadcastDNSFilterEvent("dns-filter:updated", "DNS filter list uploaded")
|
api.broadcastDNSFilterEvent("dns-filter:updated", "DNS filter list uploaded")
|
||||||
}()
|
}()
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ import (
|
|||||||
type Machine struct {
|
type Machine struct {
|
||||||
MachineID string `json:"machineId"`
|
MachineID string `json:"machineId"`
|
||||||
IsValidated bool `json:"isValidated"`
|
IsValidated bool `json:"isValidated"`
|
||||||
LastPush string `json:"last_push,omitempty"`
|
LastPush string `json:"lastPush,omitempty"`
|
||||||
LastHeartbeat string `json:"last_heartbeat,omitempty"`
|
LastHeartbeat string `json:"lastHeartbeat,omitempty"`
|
||||||
IPAddress string `json:"ipAddress,omitempty"`
|
IPAddress string `json:"ipAddress,omitempty"`
|
||||||
Version string `json:"version,omitempty"`
|
Version string `json:"version,omitempty"`
|
||||||
}
|
}
|
||||||
@@ -104,10 +104,29 @@ func (m *Manager) GetMachines() ([]Machine, error) {
|
|||||||
if out == "" || out == "null" {
|
if out == "" || out == "null" {
|
||||||
return []Machine{}, nil
|
return []Machine{}, nil
|
||||||
}
|
}
|
||||||
var machines []Machine
|
// cscli outputs snake_case JSON — map to our camelCase API types
|
||||||
if err := json.Unmarshal([]byte(out), &machines); err != nil {
|
var raw []struct {
|
||||||
|
MachineID string `json:"machineId"`
|
||||||
|
IsValidated bool `json:"isValidated"`
|
||||||
|
LastPush string `json:"last_push"`
|
||||||
|
LastHeartbeat string `json:"last_heartbeat"`
|
||||||
|
IPAddress string `json:"ipAddress"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal([]byte(out), &raw); err != nil {
|
||||||
return nil, fmt.Errorf("parsing machines: %w", err)
|
return nil, fmt.Errorf("parsing machines: %w", err)
|
||||||
}
|
}
|
||||||
|
machines := make([]Machine, 0, len(raw))
|
||||||
|
for _, r := range raw {
|
||||||
|
machines = append(machines, Machine{
|
||||||
|
MachineID: r.MachineID,
|
||||||
|
IsValidated: r.IsValidated,
|
||||||
|
LastPush: r.LastPush,
|
||||||
|
LastHeartbeat: r.LastHeartbeat,
|
||||||
|
IPAddress: r.IPAddress,
|
||||||
|
Version: r.Version,
|
||||||
|
})
|
||||||
|
}
|
||||||
return machines, nil
|
return machines, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ func (rn *Runner) checkAndUpdate() {
|
|||||||
|
|
||||||
p := rn.paramsFn()
|
p := rn.paramsFn()
|
||||||
if p.APIToken == "" || len(p.Records) == 0 {
|
if p.APIToken == "" || len(p.Records) == 0 {
|
||||||
slog.Debug("DDNS: no token or records, skipping", "component", "ddns")
|
slog.Debug("no token or records, skipping", "component", "ddns")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,11 +164,11 @@ func (rn *Runner) checkAndUpdate() {
|
|||||||
rn.mu.Lock()
|
rn.mu.Lock()
|
||||||
rn.status.LastError = fmt.Sprintf("getting public IP: %v", err)
|
rn.status.LastError = fmt.Sprintf("getting public IP: %v", err)
|
||||||
rn.mu.Unlock()
|
rn.mu.Unlock()
|
||||||
slog.Error("DDNS: failed to get public IP", "component", "ddns", "error", err)
|
slog.Error("failed to get public IP", "component", "ddns", "error", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
slog.Debug("DDNS: syncing records", "component", "ddns", "ip", ip, "records", p.Records)
|
slog.Debug("syncing records", "component", "ddns", "ip", ip, "records", p.Records)
|
||||||
|
|
||||||
cf := &cfClient{apiToken: p.APIToken}
|
cf := &cfClient{apiToken: p.APIToken}
|
||||||
|
|
||||||
@@ -177,7 +177,7 @@ func (rn *Runner) checkAndUpdate() {
|
|||||||
for _, record := range p.Records {
|
for _, record := range p.Records {
|
||||||
if err := cf.updateRecord(record, ip); err != nil {
|
if err := cf.updateRecord(record, ip); err != nil {
|
||||||
lastErr = fmt.Sprintf("updating %s: %v", record, err)
|
lastErr = fmt.Sprintf("updating %s: %v", record, err)
|
||||||
slog.Error("DDNS: failed to update record", "component", "ddns", "record", record, "error", err)
|
slog.Error("failed to update record", "component", "ddns", "record", record, "error", err)
|
||||||
records = append(records, RecordStatus{Name: record, OK: false, Error: err.Error()})
|
records = append(records, RecordStatus{Name: record, OK: false, Error: err.Error()})
|
||||||
} else {
|
} else {
|
||||||
records = append(records, RecordStatus{Name: record, IP: ip, OK: true})
|
records = append(records, RecordStatus{Name: record, IP: ip, OK: true})
|
||||||
@@ -304,13 +304,13 @@ func (c *cfClient) updateRecord(recordName, ip string) error {
|
|||||||
|
|
||||||
// No A record. Remove any CNAME that would conflict.
|
// No A record. Remove any CNAME that would conflict.
|
||||||
if cnameID, _, cnameErr := c.getRecordID(zoneID, recordName, "CNAME"); cnameErr == nil {
|
if cnameID, _, cnameErr := c.getRecordID(zoneID, recordName, "CNAME"); cnameErr == nil {
|
||||||
slog.Info("DDNS: removing CNAME before creating A record", "component", "ddns", "record", recordName)
|
slog.Info("removing CNAME before creating A record", "component", "ddns", "record", recordName)
|
||||||
if err := c.deleteRecord(zoneID, cnameID); err != nil {
|
if err := c.deleteRecord(zoneID, cnameID); err != nil {
|
||||||
return fmt.Errorf("removing CNAME for %s: %w", recordName, err)
|
return fmt.Errorf("removing CNAME for %s: %w", recordName, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
slog.Info("DDNS: creating A record", "component", "ddns", "record", recordName)
|
slog.Info("creating A record", "component", "ddns", "record", recordName)
|
||||||
return c.createRecord(zoneID, recordName, ip)
|
return c.createRecord(zoneID, recordName, ip)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -315,7 +315,7 @@ func (m *Manager) UpdateLists(ctx context.Context) error {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
slog.Info("dns-filter: downloading list", "name", bl.Name, "url", bl.URL)
|
slog.Info("downloading list", "component", "dns-filter", "name", bl.Name, "url", bl.URL)
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, bl.URL, nil)
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, bl.URL, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -329,7 +329,7 @@ func (m *Manager) UpdateLists(ctx context.Context) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
bl.LastError = fmt.Sprintf("download failed: %v", err)
|
bl.LastError = fmt.Sprintf("download failed: %v", err)
|
||||||
changed = true
|
changed = true
|
||||||
slog.Warn("dns-filter: download failed", "name", bl.Name, "error", err)
|
slog.Warn("download failed", "component", "dns-filter", "name", bl.Name, "error", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -337,7 +337,7 @@ func (m *Manager) UpdateLists(ctx context.Context) error {
|
|||||||
resp.Body.Close()
|
resp.Body.Close()
|
||||||
bl.LastError = fmt.Sprintf("HTTP %d", resp.StatusCode)
|
bl.LastError = fmt.Sprintf("HTTP %d", resp.StatusCode)
|
||||||
changed = true
|
changed = true
|
||||||
slog.Warn("dns-filter: download failed", "name", bl.Name, "status", resp.StatusCode)
|
slog.Warn("download failed", "component", "dns-filter", "name", bl.Name, "status", resp.StatusCode)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -393,7 +393,7 @@ func (m *Manager) Compile() (int, error) {
|
|||||||
|
|
||||||
domains, err := ParseFile(cachePath)
|
domains, err := ParseFile(cachePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Warn("dns-filter: failed to parse list", "name", bl.Name, "error", err)
|
slog.Warn("failed to parse list", "component", "dns-filter", "name", bl.Name, "error", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -446,7 +446,7 @@ func (m *Manager) Compile() (int, error) {
|
|||||||
// Update entry counts and save
|
// Update entry counts and save
|
||||||
_ = m.SaveFilterData(fd)
|
_ = m.SaveFilterData(fd)
|
||||||
|
|
||||||
slog.Info("dns-filter: compiled blocklist", "domains", len(sorted))
|
slog.Info("compiled blocklist", "component", "dns-filter", "domains", len(sorted))
|
||||||
return len(sorted), nil
|
return len(sorted), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -496,7 +496,7 @@ func (m *Manager) GetQueryStats() *QueryStats {
|
|||||||
cmd := exec.Command("sudo", "journalctl", "-u", "dnsmasq", "--since", "24 hours ago", "--no-pager", "-o", "cat")
|
cmd := exec.Command("sudo", "journalctl", "-u", "dnsmasq", "--since", "24 hours ago", "--no-pager", "-o", "cat")
|
||||||
output, err := cmd.Output()
|
output, err := cmd.Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Debug("dns-filter: failed to read dnsmasq journal", "error", err)
|
slog.Debug("failed to read dnsmasq journal", "component", "dns-filter", "error", err)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ func (r *Runner) Trigger() {
|
|||||||
func (r *Runner) run(ctx context.Context, intervalHours int) {
|
func (r *Runner) run(ctx context.Context, intervalHours int) {
|
||||||
interval := time.Duration(intervalHours) * time.Hour
|
interval := time.Duration(intervalHours) * time.Hour
|
||||||
|
|
||||||
slog.Info("dns-filter: runner started", "interval", interval)
|
slog.Info("runner started", "component", "dns-filter", "interval", interval)
|
||||||
|
|
||||||
// Immediate update on start
|
// Immediate update on start
|
||||||
r.updateAndCompile(ctx)
|
r.updateAndCompile(ctx)
|
||||||
@@ -89,7 +89,7 @@ func (r *Runner) run(ctx context.Context, intervalHours int) {
|
|||||||
r.mu.Lock()
|
r.mu.Lock()
|
||||||
r.running = false
|
r.running = false
|
||||||
r.mu.Unlock()
|
r.mu.Unlock()
|
||||||
slog.Info("dns-filter: runner stopped")
|
slog.Info("runner stopped", "component", "dns-filter")
|
||||||
return
|
return
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
r.updateAndCompile(ctx)
|
r.updateAndCompile(ctx)
|
||||||
@@ -101,11 +101,11 @@ func (r *Runner) run(ctx context.Context, intervalHours int) {
|
|||||||
|
|
||||||
func (r *Runner) updateAndCompile(ctx context.Context) {
|
func (r *Runner) updateAndCompile(ctx context.Context) {
|
||||||
if err := r.manager.UpdateLists(ctx); err != nil {
|
if err := r.manager.UpdateLists(ctx); err != nil {
|
||||||
slog.Error("dns-filter: update failed", "error", err)
|
slog.Error("update failed", "component", "dns-filter", "error", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := r.manager.Compile(); err != nil {
|
if _, err := r.manager.Compile(); err != nil {
|
||||||
slog.Error("dns-filter: compile failed", "error", err)
|
slog.Error("compile failed", "component", "dns-filter", "error", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,36 +24,36 @@ type DNSEntry struct {
|
|||||||
Wildcard bool // true: address=/ (all subdomains); false: host-record= (exact match)
|
Wildcard bool // true: address=/ (all subdomains); false: host-record= (exact match)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConfigGenerator handles dnsmasq configuration generation
|
// Manager handles dnsmasq configuration generation
|
||||||
type ConfigGenerator struct {
|
type Manager struct {
|
||||||
configPath string
|
configPath string
|
||||||
filterConfPath string // path to addn-hosts file for DNS filtering
|
filterConfPath string // path to addn-hosts file for DNS filtering
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewConfigGenerator creates a new dnsmasq config generator
|
// NewManager creates a new dnsmasq config generator
|
||||||
func NewConfigGenerator(configPath string) *ConfigGenerator {
|
func NewManager(configPath string) *Manager {
|
||||||
if configPath == "" {
|
if configPath == "" {
|
||||||
configPath = "/etc/dnsmasq.d/wild-cloud.conf"
|
configPath = "/etc/dnsmasq.d/wild-cloud.conf"
|
||||||
}
|
}
|
||||||
return &ConfigGenerator{
|
return &Manager{
|
||||||
configPath: configPath,
|
configPath: configPath,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetConfigPath returns the dnsmasq config file path
|
// GetConfigPath returns the dnsmasq config file path
|
||||||
func (g *ConfigGenerator) GetConfigPath() string {
|
func (g *Manager) GetConfigPath() string {
|
||||||
return g.configPath
|
return g.configPath
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetFilterConfPath sets the path to an additional hosts file for DNS filtering.
|
// SetFilterConfPath sets the path to an additional hosts file for DNS filtering.
|
||||||
// When set, the generated config includes an addn-hosts= directive.
|
// When set, the generated config includes an addn-hosts= directive.
|
||||||
func (g *ConfigGenerator) SetFilterConfPath(path string) {
|
func (g *Manager) SetFilterConfPath(path string) {
|
||||||
g.filterConfPath = path
|
g.filterConfPath = path
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate creates a dnsmasq configuration from registered domain entries.
|
// Generate creates a dnsmasq configuration from registered domain entries.
|
||||||
// It auto-detects the Wild Central server's IP address for the listen directive.
|
// It auto-detects the Wild Central server's IP address for the listen directive.
|
||||||
func (g *ConfigGenerator) Generate(cfg *config.State, entries []DNSEntry) string {
|
func (g *Manager) Generate(cfg *config.State, entries []DNSEntry) string {
|
||||||
// Use configured dnsmasq IP (bound to eth0), falling back to auto-detect.
|
// Use configured dnsmasq IP (bound to eth0), falling back to auto-detect.
|
||||||
// Auto-detect can pick wlan0 if that's the default route, which is wrong
|
// Auto-detect can pick wlan0 if that's the default route, which is wrong
|
||||||
// when dnsmasq is only listening on eth0.
|
// when dnsmasq is only listening on eth0.
|
||||||
@@ -123,7 +123,7 @@ log-dhcp
|
|||||||
// A full restart is required because SIGHUP (reload) does not re-read
|
// A full restart is required because SIGHUP (reload) does not re-read
|
||||||
// host-record= directives — only address=/ and a few other directives
|
// host-record= directives — only address=/ and a few other directives
|
||||||
// are reloaded on SIGHUP.
|
// are reloaded on SIGHUP.
|
||||||
func (g *ConfigGenerator) RestartService() error {
|
func (g *Manager) RestartService() error {
|
||||||
cmd := exec.Command("systemctl", "restart", "dnsmasq.service")
|
cmd := exec.Command("systemctl", "restart", "dnsmasq.service")
|
||||||
output, err := cmd.CombinedOutput()
|
output, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -133,18 +133,18 @@ func (g *ConfigGenerator) RestartService() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServiceStatus represents the status of the dnsmasq service
|
// Status represents the status of the dnsmasq service
|
||||||
type ServiceStatus struct {
|
type Status struct {
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
PID int `json:"pid"`
|
PID int `json:"pid"`
|
||||||
IP string `json:"ip"`
|
IP string `json:"ip"`
|
||||||
ConfigFile string `json:"config_file"`
|
ConfigFile string `json:"configFile"`
|
||||||
DomainsConfigured int `json:"domains_configured"`
|
DomainsConfigured int `json:"domainsConfigured"`
|
||||||
LastRestart time.Time `json:"last_restart"`
|
LastRestart time.Time `json:"lastRestart"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetStatus checks the status of the dnsmasq service
|
// GetStatus checks the status of the dnsmasq service
|
||||||
func (g *ConfigGenerator) GetStatus() (*ServiceStatus, error) {
|
func (g *Manager) GetStatus() (*Status, error) {
|
||||||
// Get the Wild Central IP address
|
// Get the Wild Central IP address
|
||||||
dnsIP, err := network.GetWildCentralIP()
|
dnsIP, err := network.GetWildCentralIP()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -152,7 +152,7 @@ func (g *ConfigGenerator) GetStatus() (*ServiceStatus, error) {
|
|||||||
dnsIP = ""
|
dnsIP = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
status := &ServiceStatus{
|
status := &Status{
|
||||||
ConfigFile: g.configPath,
|
ConfigFile: g.configPath,
|
||||||
IP: dnsIP,
|
IP: dnsIP,
|
||||||
}
|
}
|
||||||
@@ -205,7 +205,7 @@ func (g *ConfigGenerator) GetStatus() (*ServiceStatus, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ReadConfig reads the current dnsmasq configuration
|
// ReadConfig reads the current dnsmasq configuration
|
||||||
func (g *ConfigGenerator) ReadConfig() (string, error) {
|
func (g *Manager) ReadConfig() (string, error) {
|
||||||
data, err := os.ReadFile(g.configPath)
|
data, err := os.ReadFile(g.configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("reading dnsmasq config: %w", err)
|
return "", fmt.Errorf("reading dnsmasq config: %w", err)
|
||||||
@@ -214,7 +214,7 @@ func (g *ConfigGenerator) ReadConfig() (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// UpdateConfig regenerates and writes the dnsmasq configuration and optionally restarts.
|
// UpdateConfig regenerates and writes the dnsmasq configuration and optionally restarts.
|
||||||
func (g *ConfigGenerator) UpdateConfig(cfg *config.State, entries []DNSEntry, restart bool) error {
|
func (g *Manager) UpdateConfig(cfg *config.State, entries []DNSEntry, restart bool) error {
|
||||||
// Generate fresh config from scratch
|
// Generate fresh config from scratch
|
||||||
configContent := g.Generate(cfg, entries)
|
configContent := g.Generate(cfg, entries)
|
||||||
|
|
||||||
@@ -235,7 +235,7 @@ func (g *ConfigGenerator) UpdateConfig(cfg *config.State, entries []DNSEntry, re
|
|||||||
// GenerateMainConfig creates the main dnsmasq configuration with global settings.
|
// GenerateMainConfig creates the main dnsmasq configuration with global settings.
|
||||||
// Used by the DnsmasqGenerate handler to preview/apply the base config independently
|
// Used by the DnsmasqGenerate handler to preview/apply the base config independently
|
||||||
// of domain-specific DNS entries (which are handled by Generate + UpdateConfig).
|
// of domain-specific DNS entries (which are handled by Generate + UpdateConfig).
|
||||||
func (g *ConfigGenerator) GenerateMainConfig(cfg *config.State) string {
|
func (g *Manager) GenerateMainConfig(cfg *config.State) string {
|
||||||
dnsIP, err := network.GetWildCentralIP()
|
dnsIP, err := network.GetWildCentralIP()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("failed to detect Wild Central IP", "component", "dnsmasq", "error", err)
|
slog.Error("failed to detect Wild Central IP", "component", "dnsmasq", "error", err)
|
||||||
@@ -300,7 +300,7 @@ log-dhcp
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ValidateConfig tests a dnsmasq configuration file for syntax errors
|
// ValidateConfig tests a dnsmasq configuration file for syntax errors
|
||||||
func (g *ConfigGenerator) ValidateConfig(configPath string) error {
|
func (g *Manager) ValidateConfig(configPath string) error {
|
||||||
cmd := exec.Command("dnsmasq", "--test", "-C", configPath)
|
cmd := exec.Command("dnsmasq", "--test", "-C", configPath)
|
||||||
output, err := cmd.CombinedOutput()
|
output, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -314,13 +314,13 @@ func (g *ConfigGenerator) ValidateConfig(configPath string) error {
|
|||||||
|
|
||||||
// ReloadService restarts dnsmasq to apply configuration changes.
|
// ReloadService restarts dnsmasq to apply configuration changes.
|
||||||
// Uses a full restart because SIGHUP does not re-read host-record= directives.
|
// Uses a full restart because SIGHUP does not re-read host-record= directives.
|
||||||
func (g *ConfigGenerator) ReloadService() error {
|
func (g *Manager) ReloadService() error {
|
||||||
return g.RestartService()
|
return g.RestartService()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConfigureSystemDNS configures systemd-resolved to use the local dnsmasq server
|
// ConfigureSystemDNS configures systemd-resolved to use the local dnsmasq server
|
||||||
// This should only be called on first start of dnsmasq
|
// This should only be called on first start of dnsmasq
|
||||||
func (g *ConfigGenerator) ConfigureSystemDNS() error {
|
func (g *Manager) ConfigureSystemDNS() error {
|
||||||
// Auto-detect network info to get the DNS IP
|
// Auto-detect network info to get the DNS IP
|
||||||
netInfo, err := network.DetectNetworkInfo()
|
netInfo, err := network.DetectNetworkInfo()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ func wildcardEntry(domain, ip string) DNSEntry {
|
|||||||
|
|
||||||
// Test: Generate with exact-match entries produces host-record= directives
|
// Test: Generate with exact-match entries produces host-record= directives
|
||||||
func TestGenerate_WithEntries(t *testing.T) {
|
func TestGenerate_WithEntries(t *testing.T) {
|
||||||
g := NewConfigGenerator("/tmp/test-dnsmasq.conf")
|
g := NewManager("/tmp/test-dnsmasq.conf")
|
||||||
entries := []DNSEntry{
|
entries := []DNSEntry{
|
||||||
entry("cloud.example.com", "192.168.1.10"),
|
entry("cloud.example.com", "192.168.1.10"),
|
||||||
entry("internal.example.com", "192.168.1.10"),
|
entry("internal.example.com", "192.168.1.10"),
|
||||||
@@ -39,7 +39,7 @@ func TestGenerate_WithEntries(t *testing.T) {
|
|||||||
|
|
||||||
// Test: Generate with no entries still produces valid config skeleton
|
// Test: Generate with no entries still produces valid config skeleton
|
||||||
func TestGenerate_NoEntries(t *testing.T) {
|
func TestGenerate_NoEntries(t *testing.T) {
|
||||||
g := NewConfigGenerator("/tmp/test-dnsmasq.conf")
|
g := NewManager("/tmp/test-dnsmasq.conf")
|
||||||
|
|
||||||
out := g.Generate(nil, []DNSEntry{})
|
out := g.Generate(nil, []DNSEntry{})
|
||||||
|
|
||||||
@@ -56,7 +56,7 @@ func TestGenerate_NoEntries(t *testing.T) {
|
|||||||
|
|
||||||
// Test: Generate skips entries with empty IP
|
// Test: Generate skips entries with empty IP
|
||||||
func TestGenerate_EntryWithoutIP(t *testing.T) {
|
func TestGenerate_EntryWithoutIP(t *testing.T) {
|
||||||
g := NewConfigGenerator("/tmp/test-dnsmasq.conf")
|
g := NewManager("/tmp/test-dnsmasq.conf")
|
||||||
|
|
||||||
out := g.Generate(nil, []DNSEntry{entry("cloud.example.com", "")})
|
out := g.Generate(nil, []DNSEntry{entry("cloud.example.com", "")})
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ func TestGenerate_EntryWithoutIP(t *testing.T) {
|
|||||||
|
|
||||||
// Test: GenerateMainConfig produces valid base config
|
// Test: GenerateMainConfig produces valid base config
|
||||||
func TestGenerateMainConfig(t *testing.T) {
|
func TestGenerateMainConfig(t *testing.T) {
|
||||||
g := NewConfigGenerator("/tmp/test-dnsmasq.conf")
|
g := NewManager("/tmp/test-dnsmasq.conf")
|
||||||
|
|
||||||
out := g.GenerateMainConfig(nil)
|
out := g.GenerateMainConfig(nil)
|
||||||
|
|
||||||
@@ -81,7 +81,7 @@ func TestGenerateMainConfig(t *testing.T) {
|
|||||||
|
|
||||||
// Test: GenerateMainConfig with DHCP disabled does not include dhcp-range
|
// Test: GenerateMainConfig with DHCP disabled does not include dhcp-range
|
||||||
func TestGenerateMainConfig_DHCPDisabled(t *testing.T) {
|
func TestGenerateMainConfig_DHCPDisabled(t *testing.T) {
|
||||||
g := NewConfigGenerator("/tmp/test-dnsmasq.conf")
|
g := NewManager("/tmp/test-dnsmasq.conf")
|
||||||
cfg := &config.State{}
|
cfg := &config.State{}
|
||||||
cfg.Cloud.Dnsmasq.DHCP.Enabled = false
|
cfg.Cloud.Dnsmasq.DHCP.Enabled = false
|
||||||
|
|
||||||
@@ -94,7 +94,7 @@ func TestGenerateMainConfig_DHCPDisabled(t *testing.T) {
|
|||||||
|
|
||||||
// Test: GenerateMainConfig with DHCP enabled includes dhcp-range
|
// Test: GenerateMainConfig with DHCP enabled includes dhcp-range
|
||||||
func TestGenerateMainConfig_DHCPEnabled(t *testing.T) {
|
func TestGenerateMainConfig_DHCPEnabled(t *testing.T) {
|
||||||
g := NewConfigGenerator("/tmp/test-dnsmasq.conf")
|
g := NewManager("/tmp/test-dnsmasq.conf")
|
||||||
cfg := &config.State{}
|
cfg := &config.State{}
|
||||||
cfg.Cloud.Dnsmasq.DHCP.Enabled = true
|
cfg.Cloud.Dnsmasq.DHCP.Enabled = true
|
||||||
cfg.Cloud.Dnsmasq.DHCP.RangeStart = "192.168.8.100"
|
cfg.Cloud.Dnsmasq.DHCP.RangeStart = "192.168.8.100"
|
||||||
@@ -114,7 +114,7 @@ func TestGenerateMainConfig_DHCPEnabled(t *testing.T) {
|
|||||||
|
|
||||||
// Test: DHCP without gateway omits dhcp-option=3
|
// Test: DHCP without gateway omits dhcp-option=3
|
||||||
func TestGenerateMainConfig_DHCPNoGateway(t *testing.T) {
|
func TestGenerateMainConfig_DHCPNoGateway(t *testing.T) {
|
||||||
g := NewConfigGenerator("/tmp/test-dnsmasq.conf")
|
g := NewManager("/tmp/test-dnsmasq.conf")
|
||||||
cfg := &config.State{}
|
cfg := &config.State{}
|
||||||
cfg.Cloud.Dnsmasq.DHCP.Enabled = true
|
cfg.Cloud.Dnsmasq.DHCP.Enabled = true
|
||||||
cfg.Cloud.Dnsmasq.DHCP.RangeStart = "192.168.8.100"
|
cfg.Cloud.Dnsmasq.DHCP.RangeStart = "192.168.8.100"
|
||||||
@@ -129,7 +129,7 @@ func TestGenerateMainConfig_DHCPNoGateway(t *testing.T) {
|
|||||||
|
|
||||||
// Test: DHCP explicit gateway is used
|
// Test: DHCP explicit gateway is used
|
||||||
func TestGenerateMainConfig_DHCPExplicitGateway(t *testing.T) {
|
func TestGenerateMainConfig_DHCPExplicitGateway(t *testing.T) {
|
||||||
g := NewConfigGenerator("/tmp/test-dnsmasq.conf")
|
g := NewManager("/tmp/test-dnsmasq.conf")
|
||||||
cfg := &config.State{}
|
cfg := &config.State{}
|
||||||
cfg.Cloud.Dnsmasq.DHCP.Enabled = true
|
cfg.Cloud.Dnsmasq.DHCP.Enabled = true
|
||||||
cfg.Cloud.Dnsmasq.DHCP.RangeStart = "192.168.8.100"
|
cfg.Cloud.Dnsmasq.DHCP.RangeStart = "192.168.8.100"
|
||||||
@@ -145,7 +145,7 @@ func TestGenerateMainConfig_DHCPExplicitGateway(t *testing.T) {
|
|||||||
|
|
||||||
// Test: DHCP static leases appear in output
|
// Test: DHCP static leases appear in output
|
||||||
func TestGenerateMainConfig_DHCPStaticLeases(t *testing.T) {
|
func TestGenerateMainConfig_DHCPStaticLeases(t *testing.T) {
|
||||||
g := NewConfigGenerator("/tmp/test-dnsmasq.conf")
|
g := NewManager("/tmp/test-dnsmasq.conf")
|
||||||
cfg := &config.State{}
|
cfg := &config.State{}
|
||||||
cfg.Cloud.Dnsmasq.DHCP.Enabled = true
|
cfg.Cloud.Dnsmasq.DHCP.Enabled = true
|
||||||
cfg.Cloud.Dnsmasq.DHCP.RangeStart = "192.168.8.100"
|
cfg.Cloud.Dnsmasq.DHCP.RangeStart = "192.168.8.100"
|
||||||
@@ -167,7 +167,7 @@ func TestGenerateMainConfig_DHCPStaticLeases(t *testing.T) {
|
|||||||
|
|
||||||
// Test: DHCP lease time defaults to 24h when not specified
|
// Test: DHCP lease time defaults to 24h when not specified
|
||||||
func TestGenerateMainConfig_DHCPLeaseTimeDefault(t *testing.T) {
|
func TestGenerateMainConfig_DHCPLeaseTimeDefault(t *testing.T) {
|
||||||
g := NewConfigGenerator("/tmp/test-dnsmasq.conf")
|
g := NewManager("/tmp/test-dnsmasq.conf")
|
||||||
cfg := &config.State{}
|
cfg := &config.State{}
|
||||||
cfg.Cloud.Dnsmasq.DHCP.Enabled = true
|
cfg.Cloud.Dnsmasq.DHCP.Enabled = true
|
||||||
cfg.Cloud.Dnsmasq.DHCP.RangeStart = "192.168.8.100"
|
cfg.Cloud.Dnsmasq.DHCP.RangeStart = "192.168.8.100"
|
||||||
@@ -180,17 +180,17 @@ func TestGenerateMainConfig_DHCPLeaseTimeDefault(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test: NewConfigGenerator uses provided path
|
// Test: NewManager uses provided path
|
||||||
func TestNewConfigGenerator_CustomPath(t *testing.T) {
|
func TestNewManager_CustomPath(t *testing.T) {
|
||||||
g := NewConfigGenerator("/custom/path/dnsmasq.conf")
|
g := NewManager("/custom/path/dnsmasq.conf")
|
||||||
if g.GetConfigPath() != "/custom/path/dnsmasq.conf" {
|
if g.GetConfigPath() != "/custom/path/dnsmasq.conf" {
|
||||||
t.Errorf("got %q, want /custom/path/dnsmasq.conf", g.GetConfigPath())
|
t.Errorf("got %q, want /custom/path/dnsmasq.conf", g.GetConfigPath())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test: NewConfigGenerator uses default path when empty
|
// Test: NewManager uses default path when empty
|
||||||
func TestNewConfigGenerator_DefaultPath(t *testing.T) {
|
func TestNewManager_DefaultPath(t *testing.T) {
|
||||||
g := NewConfigGenerator("")
|
g := NewManager("")
|
||||||
if g.GetConfigPath() != "/etc/dnsmasq.d/wild-cloud.conf" {
|
if g.GetConfigPath() != "/etc/dnsmasq.d/wild-cloud.conf" {
|
||||||
t.Errorf("got %q, want /etc/dnsmasq.d/wild-cloud.conf", g.GetConfigPath())
|
t.Errorf("got %q, want /etc/dnsmasq.d/wild-cloud.conf", g.GetConfigPath())
|
||||||
}
|
}
|
||||||
@@ -198,7 +198,7 @@ func TestNewConfigGenerator_DefaultPath(t *testing.T) {
|
|||||||
|
|
||||||
// Test: Exact-match domain produces host-record=
|
// Test: Exact-match domain produces host-record=
|
||||||
func TestGenerate_ExactMatchDomain(t *testing.T) {
|
func TestGenerate_ExactMatchDomain(t *testing.T) {
|
||||||
g := NewConfigGenerator("/tmp/test-dnsmasq.conf")
|
g := NewManager("/tmp/test-dnsmasq.conf")
|
||||||
globalCfg := &config.State{}
|
globalCfg := &config.State{}
|
||||||
|
|
||||||
out := g.Generate(globalCfg, []DNSEntry{entry("my-api.payne.io", "192.168.8.151")})
|
out := g.Generate(globalCfg, []DNSEntry{entry("my-api.payne.io", "192.168.8.151")})
|
||||||
@@ -216,7 +216,7 @@ func TestGenerate_ExactMatchDomain(t *testing.T) {
|
|||||||
|
|
||||||
// Test: Wildcard domain produces address=/ without local=/
|
// Test: Wildcard domain produces address=/ without local=/
|
||||||
func TestGenerate_WildcardDomain(t *testing.T) {
|
func TestGenerate_WildcardDomain(t *testing.T) {
|
||||||
g := NewConfigGenerator("/tmp/test-dnsmasq.conf")
|
g := NewManager("/tmp/test-dnsmasq.conf")
|
||||||
globalCfg := &config.State{}
|
globalCfg := &config.State{}
|
||||||
|
|
||||||
out := g.Generate(globalCfg, []DNSEntry{wildcardEntry("cloud.payne.io", "192.168.8.240")})
|
out := g.Generate(globalCfg, []DNSEntry{wildcardEntry("cloud.payne.io", "192.168.8.240")})
|
||||||
@@ -234,7 +234,7 @@ func TestGenerate_WildcardDomain(t *testing.T) {
|
|||||||
|
|
||||||
// Test: No local=/ directives are ever generated
|
// Test: No local=/ directives are ever generated
|
||||||
func TestGenerate_NoLocalDirectives(t *testing.T) {
|
func TestGenerate_NoLocalDirectives(t *testing.T) {
|
||||||
g := NewConfigGenerator("/tmp/test-dnsmasq.conf")
|
g := NewManager("/tmp/test-dnsmasq.conf")
|
||||||
globalCfg := &config.State{}
|
globalCfg := &config.State{}
|
||||||
|
|
||||||
entries := []DNSEntry{
|
entries := []DNSEntry{
|
||||||
@@ -251,7 +251,7 @@ func TestGenerate_NoLocalDirectives(t *testing.T) {
|
|||||||
|
|
||||||
// Test: filter-AAAA appears in generated config
|
// Test: filter-AAAA appears in generated config
|
||||||
func TestGenerate_FilterAAAA(t *testing.T) {
|
func TestGenerate_FilterAAAA(t *testing.T) {
|
||||||
g := NewConfigGenerator("/tmp/test-dnsmasq.conf")
|
g := NewManager("/tmp/test-dnsmasq.conf")
|
||||||
|
|
||||||
out := g.Generate(nil, nil)
|
out := g.Generate(nil, nil)
|
||||||
|
|
||||||
@@ -262,7 +262,7 @@ func TestGenerate_FilterAAAA(t *testing.T) {
|
|||||||
|
|
||||||
// Test: Mix of exact and wildcard domains
|
// Test: Mix of exact and wildcard domains
|
||||||
func TestGenerate_MixedDomains(t *testing.T) {
|
func TestGenerate_MixedDomains(t *testing.T) {
|
||||||
g := NewConfigGenerator("/tmp/test-dnsmasq.conf")
|
g := NewManager("/tmp/test-dnsmasq.conf")
|
||||||
globalCfg := &config.State{}
|
globalCfg := &config.State{}
|
||||||
|
|
||||||
entries := []DNSEntry{
|
entries := []DNSEntry{
|
||||||
@@ -289,7 +289,7 @@ func TestGenerate_MixedDomains(t *testing.T) {
|
|||||||
|
|
||||||
// Test: empty entries list produces base config with no DNS entries
|
// Test: empty entries list produces base config with no DNS entries
|
||||||
func TestGenerate_EmptyEntries(t *testing.T) {
|
func TestGenerate_EmptyEntries(t *testing.T) {
|
||||||
g := NewConfigGenerator("/tmp/test-dnsmasq.conf")
|
g := NewManager("/tmp/test-dnsmasq.conf")
|
||||||
globalCfg := &config.State{}
|
globalCfg := &config.State{}
|
||||||
|
|
||||||
out := g.Generate(globalCfg, []DNSEntry{})
|
out := g.Generate(globalCfg, []DNSEntry{})
|
||||||
@@ -310,7 +310,7 @@ func TestGenerate_EmptyEntries(t *testing.T) {
|
|||||||
|
|
||||||
// Test: multiple exact-match entries each get their own DNS records
|
// Test: multiple exact-match entries each get their own DNS records
|
||||||
func TestGenerate_MultipleEntries(t *testing.T) {
|
func TestGenerate_MultipleEntries(t *testing.T) {
|
||||||
g := NewConfigGenerator("/tmp/test-dnsmasq.conf")
|
g := NewManager("/tmp/test-dnsmasq.conf")
|
||||||
globalCfg := &config.State{}
|
globalCfg := &config.State{}
|
||||||
|
|
||||||
entries := []DNSEntry{
|
entries := []DNSEntry{
|
||||||
@@ -329,7 +329,7 @@ func TestGenerate_MultipleEntries(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestGenerate_ConfFile(t *testing.T) {
|
func TestGenerate_ConfFile(t *testing.T) {
|
||||||
g := NewConfigGenerator("/tmp/test-dnsmasq.conf")
|
g := NewManager("/tmp/test-dnsmasq.conf")
|
||||||
g.SetFilterConfPath("/var/lib/wild-central/dns-filter/hosts.blocked")
|
g.SetFilterConfPath("/var/lib/wild-central/dns-filter/hosts.blocked")
|
||||||
|
|
||||||
out := g.Generate(nil, nil)
|
out := g.Generate(nil, nil)
|
||||||
@@ -343,7 +343,7 @@ func TestGenerate_ConfFile(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestGenerate_NoConfFileWhenEmpty(t *testing.T) {
|
func TestGenerate_NoConfFileWhenEmpty(t *testing.T) {
|
||||||
g := NewConfigGenerator("/tmp/test-dnsmasq.conf")
|
g := NewManager("/tmp/test-dnsmasq.conf")
|
||||||
// addnHostsPath is empty by default
|
// addnHostsPath is empty by default
|
||||||
|
|
||||||
out := g.Generate(nil, nil)
|
out := g.Generate(nil, nil)
|
||||||
@@ -354,7 +354,7 @@ func TestGenerate_NoConfFileWhenEmpty(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestGenerateMainConfig_ConfFile(t *testing.T) {
|
func TestGenerateMainConfig_ConfFile(t *testing.T) {
|
||||||
g := NewConfigGenerator("/tmp/test-dnsmasq.conf")
|
g := NewManager("/tmp/test-dnsmasq.conf")
|
||||||
g.SetFilterConfPath("/tmp/hosts.blocked")
|
g.SetFilterConfPath("/tmp/hosts.blocked")
|
||||||
|
|
||||||
cfg := &config.State{}
|
cfg := &config.State{}
|
||||||
@@ -367,7 +367,7 @@ func TestGenerateMainConfig_ConfFile(t *testing.T) {
|
|||||||
|
|
||||||
// Test: nil config doesn't panic, uses auto-detect for IP
|
// Test: nil config doesn't panic, uses auto-detect for IP
|
||||||
func TestGenerate_NilConfig(t *testing.T) {
|
func TestGenerate_NilConfig(t *testing.T) {
|
||||||
g := NewConfigGenerator("/tmp/test-dnsmasq.conf")
|
g := NewManager("/tmp/test-dnsmasq.conf")
|
||||||
|
|
||||||
out := g.Generate(nil, []DNSEntry{})
|
out := g.Generate(nil, []DNSEntry{})
|
||||||
|
|
||||||
@@ -381,7 +381,7 @@ func TestGenerate_NilConfig(t *testing.T) {
|
|||||||
|
|
||||||
// Test: when cfg.Cloud.Dnsmasq.IP is set, uses that instead of auto-detect
|
// Test: when cfg.Cloud.Dnsmasq.IP is set, uses that instead of auto-detect
|
||||||
func TestGenerate_ConfiguredDnsmasqIP(t *testing.T) {
|
func TestGenerate_ConfiguredDnsmasqIP(t *testing.T) {
|
||||||
g := NewConfigGenerator("/tmp/test-dnsmasq.conf")
|
g := NewManager("/tmp/test-dnsmasq.conf")
|
||||||
cfg := &config.State{}
|
cfg := &config.State{}
|
||||||
cfg.Cloud.Dnsmasq.IP = "192.168.8.100"
|
cfg.Cloud.Dnsmasq.IP = "192.168.8.100"
|
||||||
|
|
||||||
@@ -394,7 +394,7 @@ func TestGenerate_ConfiguredDnsmasqIP(t *testing.T) {
|
|||||||
|
|
||||||
// Test: verify no leaked empty entries appear
|
// Test: verify no leaked empty entries appear
|
||||||
func TestGenerate_NoLeakedEmptyEntries(t *testing.T) {
|
func TestGenerate_NoLeakedEmptyEntries(t *testing.T) {
|
||||||
g := NewConfigGenerator("/tmp/test-dnsmasq.conf")
|
g := NewManager("/tmp/test-dnsmasq.conf")
|
||||||
cfg := &config.State{}
|
cfg := &config.State{}
|
||||||
|
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
|
|||||||
@@ -572,8 +572,8 @@ func (m *Manager) RestartService() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServiceStatus represents the status of the HAProxy service
|
// Status represents the status of the HAProxy service
|
||||||
type ServiceStatus struct {
|
type Status struct {
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
PID int `json:"pid"`
|
PID int `json:"pid"`
|
||||||
ConfigFile string `json:"configFile"`
|
ConfigFile string `json:"configFile"`
|
||||||
@@ -581,8 +581,8 @@ type ServiceStatus struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetStatus returns the current HAProxy service status
|
// GetStatus returns the current HAProxy service status
|
||||||
func (m *Manager) GetStatus() (*ServiceStatus, error) {
|
func (m *Manager) GetStatus() (*Status, error) {
|
||||||
status := &ServiceStatus{
|
status := &Status{
|
||||||
ConfigFile: m.configPath,
|
ConfigFile: m.configPath,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import (
|
|||||||
|
|
||||||
// NetworkInfo holds detected network configuration
|
// NetworkInfo holds detected network configuration
|
||||||
type NetworkInfo struct {
|
type NetworkInfo struct {
|
||||||
PrimaryIP string `json:"primary_ip"`
|
PrimaryIP string `json:"primaryIP"`
|
||||||
PrimaryInterface string `json:"primary_interface"`
|
PrimaryInterface string `json:"primaryInterface"`
|
||||||
Gateway string `json:"gateway"`
|
Gateway string `json:"gateway"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,11 +29,11 @@ type HAProxyManager interface {
|
|||||||
ReloadService() error
|
ReloadService() error
|
||||||
}
|
}
|
||||||
|
|
||||||
// DNSManager is the subset of dnsmasq.ConfigGenerator used by reconciliation.
|
// DNSManager is the subset of dnsmasq.Manager used by reconciliation.
|
||||||
type DNSManager interface {
|
type DNSManager interface {
|
||||||
UpdateConfig(cfg *config.State, entries []dnsmasq.DNSEntry, restart bool) error
|
UpdateConfig(cfg *config.State, entries []dnsmasq.DNSEntry, restart bool) error
|
||||||
SetFilterConfPath(path string)
|
SetFilterConfPath(path string)
|
||||||
GetStatus() (*dnsmasq.ServiceStatus, error)
|
GetStatus() (*dnsmasq.Status, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DomainManager is the subset of domains.Manager used by reconciliation.
|
// DomainManager is the subset of domains.Manager used by reconciliation.
|
||||||
@@ -85,13 +85,13 @@ type Reconciler struct {
|
|||||||
func (r *Reconciler) Reconcile() {
|
func (r *Reconciler) Reconcile() {
|
||||||
doms, err := r.Domains.List()
|
doms, err := r.Domains.List()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("reconcile: failed to list domains", "error", err)
|
slog.Error("failed to list domains", "component", "reconcile", "error", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
globalCfg, err := config.LoadState(r.StatePath)
|
globalCfg, err := config.LoadState(r.StatePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Warn("reconcile: failed to load state, using empty", "error", err)
|
slog.Warn("failed to load state, using empty", "component", "reconcile", "error", err)
|
||||||
globalCfg = &config.State{}
|
globalCfg = &config.State{}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,7 +105,7 @@ func (r *Reconciler) Reconcile() {
|
|||||||
if hasCertForDomain(route.Domain) {
|
if hasCertForDomain(route.Domain) {
|
||||||
activeHTTPRoutes = append(activeHTTPRoutes, route)
|
activeHTTPRoutes = append(activeHTTPRoutes, route)
|
||||||
} else {
|
} else {
|
||||||
slog.Warn("reconcile: skipping L7 route (no cert)", "domain", route.Domain)
|
slog.Warn("skipping L7 route (no cert)", "component", "reconcile", "domain", route.Domain)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,7 +132,7 @@ func (r *Reconciler) Reconcile() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := r.DNS.UpdateConfig(globalCfg, dnsEntries, true); err != nil {
|
if err := r.DNS.UpdateConfig(globalCfg, dnsEntries, true); err != nil {
|
||||||
slog.Error("reconcile: failed to update dnsmasq", "error", err)
|
slog.Error("failed to update dnsmasq", "component", "reconcile", "error", err)
|
||||||
} else {
|
} else {
|
||||||
r.broadcastDNSEvent("dnsmasq:config", "DNS config regenerated from registered domains")
|
r.broadcastDNSEvent("dnsmasq:config", "DNS config regenerated from registered domains")
|
||||||
}
|
}
|
||||||
@@ -143,7 +143,7 @@ func (r *Reconciler) Reconcile() {
|
|||||||
|
|
||||||
r.DDNS.Trigger()
|
r.DDNS.Trigger()
|
||||||
|
|
||||||
slog.Info("reconcile: networking updated",
|
slog.Info("networking updated", "component", "reconcile",
|
||||||
"domains", len(doms),
|
"domains", len(doms),
|
||||||
"l4Routes", len(l4Routes),
|
"l4Routes", len(l4Routes),
|
||||||
"l7Routes", len(httpRoutes),
|
"l7Routes", len(httpRoutes),
|
||||||
@@ -235,7 +235,7 @@ func (r *Reconciler) writeHAProxyConfig(l4Routes []haproxy.L4Route, httpRoutes [
|
|||||||
|
|
||||||
if r.GenerateAutheliaConfig != nil {
|
if r.GenerateAutheliaConfig != nil {
|
||||||
if err := r.GenerateAutheliaConfig(globalCfg); err != nil {
|
if err := r.GenerateAutheliaConfig(globalCfg); err != nil {
|
||||||
slog.Warn("reconcile: failed to regenerate authelia config", "error", err)
|
slog.Warn("failed to regenerate authelia config", "component", "reconcile", "error", err)
|
||||||
} else if r.Auth.UserCount() > 0 {
|
} else if r.Auth.UserCount() > 0 {
|
||||||
_ = r.Auth.RestartService()
|
_ = r.Auth.RestartService()
|
||||||
}
|
}
|
||||||
@@ -247,8 +247,8 @@ func (r *Reconciler) writeHAProxyConfig(l4Routes []haproxy.L4Route, httpRoutes [
|
|||||||
if err := r.HAProxy.WriteConfig(cfg); err != nil {
|
if err := r.HAProxy.WriteConfig(cfg); err != nil {
|
||||||
brokenDomains := haproxy.FindBrokenServices(cfg, haproxy.ParseValidationErrors(err.Error()))
|
brokenDomains := haproxy.FindBrokenServices(cfg, haproxy.ParseValidationErrors(err.Error()))
|
||||||
if len(brokenDomains) > 0 {
|
if len(brokenDomains) > 0 {
|
||||||
slog.Error("reconcile: excluding broken domains and retrying",
|
slog.Error("excluding broken domains and retrying",
|
||||||
"broken", brokenDomains, "error", err)
|
"component", "reconcile", "broken", brokenDomains, "error", err)
|
||||||
|
|
||||||
exclude := map[string]bool{}
|
exclude := map[string]bool{}
|
||||||
for _, d := range brokenDomains {
|
for _, d := range brokenDomains {
|
||||||
@@ -271,17 +271,17 @@ func (r *Reconciler) writeHAProxyConfig(l4Routes []haproxy.L4Route, httpRoutes [
|
|||||||
genOpts.HTTPRoutes = filteredHTTP
|
genOpts.HTTPRoutes = filteredHTTP
|
||||||
cfg = r.HAProxy.GenerateWithOpts(filteredL4, nil, genOpts)
|
cfg = r.HAProxy.GenerateWithOpts(filteredL4, nil, genOpts)
|
||||||
if err := r.HAProxy.WriteConfig(cfg); err != nil {
|
if err := r.HAProxy.WriteConfig(cfg); err != nil {
|
||||||
slog.Error("reconcile: retry also failed", "error", err)
|
slog.Error("retry also failed", "component", "reconcile", "error", err)
|
||||||
} else if err := r.HAProxy.ReloadService(); err != nil {
|
} else if err := r.HAProxy.ReloadService(); err != nil {
|
||||||
slog.Warn("reconcile: failed to reload HAProxy", "error", err)
|
slog.Warn("failed to reload HAProxy", "component", "reconcile", "error", err)
|
||||||
} else {
|
} else {
|
||||||
r.broadcastEvent("haproxy:config", "HAProxy config regenerated (excluded broken domains)")
|
r.broadcastEvent("haproxy:config", "HAProxy config regenerated (excluded broken domains)")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
slog.Error("reconcile: failed to write HAProxy config (no broken domains identified)", "error", err)
|
slog.Error("failed to write HAProxy config (no broken domains identified)", "component", "reconcile", "error", err)
|
||||||
}
|
}
|
||||||
} else if err := r.HAProxy.ReloadService(); err != nil {
|
} else if err := r.HAProxy.ReloadService(); err != nil {
|
||||||
slog.Warn("reconcile: failed to reload HAProxy", "error", err)
|
slog.Warn("failed to reload HAProxy", "component", "reconcile", "error", err)
|
||||||
} else {
|
} else {
|
||||||
r.broadcastEvent("haproxy:config", "HAProxy config regenerated from registered domains")
|
r.broadcastEvent("haproxy:config", "HAProxy config regenerated from registered domains")
|
||||||
}
|
}
|
||||||
@@ -296,7 +296,7 @@ func cleanZeroByteCerts(certsDir string) {
|
|||||||
for _, e := range entries {
|
for _, e := range entries {
|
||||||
if strings.HasSuffix(e.Name(), ".pem") {
|
if strings.HasSuffix(e.Name(), ".pem") {
|
||||||
if info, err := e.Info(); err == nil && info.Size() == 0 {
|
if info, err := e.Info(); err == nil && info.Size() == 0 {
|
||||||
slog.Warn("reconcile: removing 0-byte cert file", "file", e.Name())
|
slog.Warn("removing 0-byte cert file", "component", "reconcile", "file", e.Name())
|
||||||
_ = os.Remove(filepath.Join(certsDir, e.Name()))
|
_ = os.Remove(filepath.Join(certsDir, e.Name()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -355,7 +355,7 @@ func ensureTLSCerts(globalCfg *config.State, doms []domains.Domain) {
|
|||||||
if gatewayDomain != "" && strings.HasSuffix(dom.DomainName, "."+gatewayDomain) {
|
if gatewayDomain != "" && strings.HasSuffix(dom.DomainName, "."+gatewayDomain) {
|
||||||
wildcardCert := certbot.HAProxyCertPath(gatewayDomain)
|
wildcardCert := certbot.HAProxyCertPath(gatewayDomain)
|
||||||
if _, err := os.Stat(wildcardCert); err != nil {
|
if _, err := os.Stat(wildcardCert); err != nil {
|
||||||
slog.Warn("reconcile/tls: no wildcard cert for gateway domain — provision via Certificates page",
|
slog.Warn("no wildcard cert for gateway domain — provision via Certificates page", "component", "reconcile",
|
||||||
"domain", dom.DomainName, "needed", "*."+gatewayDomain)
|
"domain", dom.DomainName, "needed", "*."+gatewayDomain)
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
@@ -363,7 +363,7 @@ func ensureTLSCerts(globalCfg *config.State, doms []domains.Domain) {
|
|||||||
|
|
||||||
// Check individual cert
|
// Check individual cert
|
||||||
if _, err := os.Stat(certbot.HAProxyCertPath(dom.DomainName)); err != nil {
|
if _, err := os.Stat(certbot.HAProxyCertPath(dom.DomainName)); err != nil {
|
||||||
slog.Warn("reconcile/tls: no cert for domain — provision via Certificates page",
|
slog.Warn("no cert for domain — provision via Certificates page", "component", "reconcile",
|
||||||
"domain", dom.DomainName)
|
"domain", dom.DomainName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ func (s *stubDNS) UpdateConfig(_ *config.State, entries []dnsmasq.DNSEntry, _ bo
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
func (s *stubDNS) SetFilterConfPath(p string) { s.filterPath = p }
|
func (s *stubDNS) SetFilterConfPath(p string) { s.filterPath = p }
|
||||||
func (s *stubDNS) GetStatus() (*dnsmasq.ServiceStatus, error) { return nil, nil }
|
func (s *stubDNS) GetStatus() (*dnsmasq.Status, error) { return nil, nil }
|
||||||
|
|
||||||
type stubAuth struct {
|
type stubAuth struct {
|
||||||
userCount int
|
userCount int
|
||||||
|
|||||||
Reference in New Issue
Block a user