Replace InstanceConfig with DNSEntry for dnsmasq config generation
InstanceConfig was a ~50-field struct designed for Wild Cloud k8s instances,
but only 3 fields were ever read by dnsmasq (the sole consumer). The
reconciliation bridge constructed fake InstanceConfig objects from registered
domains just to satisfy this interface.
Replace with DNSEntry{Domain, IP} — a 2-field struct purpose-built for
dnsmasq. All domains get local=/ directives to prevent AAAA queries from
leaking to upstream DNS (Happy Eyeballs / RFC 8305 latency on LAN).
Removed dead code: InstanceConfig, NodeConfig, LoadCloudConfig,
SaveCloudConfig, DeepMerge, LoadMergedInstanceConfig, EnsureInstanceConfig,
instance path helpers, instanceLoadBalancerIP, GenerateInstanceConfig,
and all modular per-instance dnsmasq methods.
This commit is contained in:
@@ -13,6 +13,17 @@ import (
|
||||
"github.com/wild-cloud/wild-central/internal/network"
|
||||
)
|
||||
|
||||
// DNSEntry represents a single domain-to-IP DNS mapping for dnsmasq.
|
||||
// Each entry produces both local=/ and address=/ directives.
|
||||
// local=/ makes dnsmasq authoritative for the domain, which prevents
|
||||
// AAAA queries from leaking to upstream DNS. Without it, upstream could
|
||||
// return public IPv6 records and browsers using Happy Eyeballs (RFC 8305)
|
||||
// would try the unreachable IPv6 path first, adding latency on LAN.
|
||||
type DNSEntry struct {
|
||||
Domain string // FQDN to resolve (e.g. "cloud.payne.io")
|
||||
IP string // target IPv4 address
|
||||
}
|
||||
|
||||
// ConfigGenerator handles dnsmasq configuration generation
|
||||
type ConfigGenerator struct {
|
||||
configPath string
|
||||
@@ -33,9 +44,9 @@ func (g *ConfigGenerator) GetConfigPath() string {
|
||||
return g.configPath
|
||||
}
|
||||
|
||||
// Generate creates a dnsmasq configuration from the app config
|
||||
// It auto-detects the Wild Central server's IP address
|
||||
func (g *ConfigGenerator) Generate(cfg *config.State, clouds []config.InstanceConfig) string {
|
||||
// Generate creates a dnsmasq configuration from registered domain entries.
|
||||
// It auto-detects the Wild Central server's IP address for the listen directive.
|
||||
func (g *ConfigGenerator) Generate(cfg *config.State, entries []DNSEntry) string {
|
||||
// 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
|
||||
// when dnsmasq is only listening on eth0.
|
||||
@@ -53,30 +64,16 @@ func (g *ConfigGenerator) Generate(cfg *config.State, clouds []config.InstanceCo
|
||||
}
|
||||
|
||||
resolution_section := ""
|
||||
for _, cloud := range clouds {
|
||||
// Point cloud domains to the cluster load balancer IP
|
||||
loadBalancerIP := instanceLoadBalancerIP(cloud)
|
||||
if loadBalancerIP == "" {
|
||||
slog.Info("no load balancer IP configured, adding commented DNS config", "component", "dnsmasq", "instance", cloud.Cluster.Name)
|
||||
// Add commented out entries for instances without load balancer
|
||||
resolution_section += fmt.Sprintf("# No load balancer IP configured for instance %s\n", cloud.Cluster.Name)
|
||||
if cloud.Cloud.InternalDomain != "" {
|
||||
resolution_section += fmt.Sprintf("# local=/%s/\n# address=/%s/<load-balancer-ip>\n", cloud.Cloud.InternalDomain, cloud.Cloud.InternalDomain)
|
||||
}
|
||||
if cloud.Cloud.Domain != "" {
|
||||
resolution_section += fmt.Sprintf("# address=/%s/<load-balancer-ip>\n", cloud.Cloud.Domain)
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if entry.Domain == "" || entry.IP == "" {
|
||||
continue
|
||||
}
|
||||
// Internal domain (.internal.cloud.example.tld) - local only, no external DNS
|
||||
if cloud.Cloud.InternalDomain != "" {
|
||||
resolution_section += fmt.Sprintf("local=/%s/\naddress=/%s/%s\n", cloud.Cloud.InternalDomain, cloud.Cloud.InternalDomain, loadBalancerIP)
|
||||
}
|
||||
|
||||
// External/primary domain - resolve to backend IP
|
||||
if cloud.Cloud.Domain != "" {
|
||||
resolution_section += fmt.Sprintf("address=/%s/%s\n", cloud.Cloud.Domain, loadBalancerIP)
|
||||
}
|
||||
// local=/ makes dnsmasq authoritative for the domain so AAAA queries
|
||||
// return empty instead of leaking to upstream DNS. Without this,
|
||||
// upstream could return public IPv6 records and browsers using Happy
|
||||
// Eyeballs (RFC 8305) would try the unreachable IPv6 path first,
|
||||
// adding 250ms-2s latency on LAN.
|
||||
resolution_section += fmt.Sprintf("local=/%s/\naddress=/%s/%s\n", entry.Domain, entry.Domain, entry.IP)
|
||||
}
|
||||
|
||||
template := `# Configuration file for dnsmasq.
|
||||
@@ -104,8 +101,8 @@ log-dhcp
|
||||
}
|
||||
|
||||
// WriteConfig writes the dnsmasq configuration to the specified path
|
||||
func (g *ConfigGenerator) WriteConfig(cfg *config.State, clouds []config.InstanceConfig, configPath string) error {
|
||||
configContent := g.Generate(cfg, clouds)
|
||||
func (g *ConfigGenerator) WriteConfig(cfg *config.State, entries []DNSEntry, configPath string) error {
|
||||
configContent := g.Generate(cfg, entries)
|
||||
|
||||
slog.Info("writing dnsmasq config", "component", "dnsmasq", "path", configPath)
|
||||
|
||||
@@ -134,7 +131,7 @@ type ServiceStatus struct {
|
||||
PID int `json:"pid"`
|
||||
IP string `json:"ip"`
|
||||
ConfigFile string `json:"config_file"`
|
||||
InstancesConfigured int `json:"instances_configured"`
|
||||
DomainsConfigured int `json:"domains_configured"`
|
||||
LastRestart time.Time `json:"last_restart"`
|
||||
}
|
||||
|
||||
@@ -190,13 +187,9 @@ func (g *ConfigGenerator) GetStatus() (*ServiceStatus, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// Count instances in config (both active and commented)
|
||||
// Count domains in config by counting local=/ directives
|
||||
if data, err := os.ReadFile(g.configPath); err == nil {
|
||||
// Count both "local=/" and "# local=/" occurrences
|
||||
activeCount := strings.Count(string(data), "\nlocal=/")
|
||||
commentedCount := strings.Count(string(data), "\n# local=/")
|
||||
// Each instance creates 1 "local=/" entry (internal domain)
|
||||
status.InstancesConfigured = activeCount + commentedCount
|
||||
status.DomainsConfigured = strings.Count(string(data), "\nlocal=/")
|
||||
}
|
||||
|
||||
return status, nil
|
||||
@@ -211,10 +204,10 @@ func (g *ConfigGenerator) ReadConfig() (string, error) {
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
// UpdateConfig regenerates and writes the dnsmasq configuration for all instances
|
||||
func (g *ConfigGenerator) UpdateConfig(cfg *config.State, instances []config.InstanceConfig, restart bool) error {
|
||||
// UpdateConfig regenerates and writes the dnsmasq configuration and optionally restarts.
|
||||
func (g *ConfigGenerator) UpdateConfig(cfg *config.State, entries []DNSEntry, restart bool) error {
|
||||
// Generate fresh config from scratch
|
||||
configContent := g.Generate(cfg, instances)
|
||||
configContent := g.Generate(cfg, entries)
|
||||
|
||||
// Write config
|
||||
slog.Info("writing dnsmasq config", "component", "dnsmasq", "path", g.configPath)
|
||||
|
||||
Reference in New Issue
Block a user