| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486 |
- package dns
- import (
- "encoding/json"
- "fmt"
- "log"
- "net"
- "os"
- "os/exec"
- "path/filepath"
- "strings"
- "sync"
- "time"
- )
- const (
- SettingsStorePath = "/etc/debtik/dns/settings.json"
- DnsmasqDNSConf = "/etc/dnsmasq.d/debtik_dns.conf"
- SystemResolvConf = "/etc/resolv.conf"
- )
- // Manager coordinates Router DNS forwarder, upstream resolvers, and static DNS mapping.
- type Manager struct {
- mu sync.RWMutex
- settings DNSSettings
- dynamicServers []string
- }
- func NewManager() *Manager {
- m := &Manager{
- dynamicServers: make([]string, 0),
- settings: DNSSettings{
- Servers: []string{"1.1.1.1", "8.8.8.8"},
- DynamicServers: make([]string, 0),
- AllowRemoteRequests: true,
- CacheSize: 2048,
- Domain: "",
- StaticRecords: make([]StaticRecord, 0),
- },
- }
- m.load()
- if err := m.applyLocked(); err != nil {
- log.Printf("[DNS] Error applying initial DNS configuration: %v", err)
- }
- return m
- }
- func (m *Manager) load() {
- m.mu.Lock()
- defer m.mu.Unlock()
- data, err := os.ReadFile(SettingsStorePath)
- if err != nil {
- // If no settings file, read existing resolv.conf nameservers if any
- if curNameservers := readSystemNameservers(); len(curNameservers) > 0 {
- m.settings.Servers = curNameservers
- }
- return
- }
- var s DNSSettings
- if err := json.Unmarshal(data, &s); err == nil {
- if len(s.Servers) > 0 {
- m.settings.Servers = s.Servers
- }
- m.settings.AllowRemoteRequests = s.AllowRemoteRequests
- if s.CacheSize > 0 {
- m.settings.CacheSize = s.CacheSize
- }
- m.settings.Domain = s.Domain
- m.settings.DoHServer = s.DoHServer
- m.settings.VerifyDoHCert = s.VerifyDoHCert
- if s.StaticRecords != nil {
- m.settings.StaticRecords = s.StaticRecords
- }
- }
- }
- func (m *Manager) saveLocked() error {
- dir := filepath.Dir(SettingsStorePath)
- if err := os.MkdirAll(dir, 0755); err != nil {
- return err
- }
- data, err := json.MarshalIndent(m.settings, "", " ")
- if err != nil {
- return err
- }
- return os.WriteFile(SettingsStorePath, data, 0644)
- }
- // applyLocked renders the dnsmasq DNS configuration and updates /etc/resolv.conf.
- func (m *Manager) applyLocked() (result error) {
- oldConfig, configErr := os.ReadFile(DnsmasqDNSConf)
- oldResolver, resolverErr := os.ReadFile(SystemResolvConf)
- wasRunning := exec.Command("systemctl", "is-active", "--quiet", "dnsmasq").Run() == nil
- defer func() {
- if result != nil {
- if configErr == nil {
- _ = os.WriteFile(DnsmasqDNSConf, oldConfig, 0644)
- } else if os.IsNotExist(configErr) {
- _ = os.Remove(DnsmasqDNSConf)
- }
- if resolverErr == nil {
- _ = os.WriteFile(SystemResolvConf, oldResolver, 0644)
- }
- if wasRunning {
- if err := exec.Command("systemctl", "restart", "dnsmasq").Run(); err != nil {
- result = fmt.Errorf("DNS apply failed: %v; previous config restart failed: %w", result, err)
- }
- }
- }
- }()
- var sb strings.Builder
- sb.WriteString("# ==================================================================\n")
- sb.WriteString("# Enterprise Router DNS Forwarder & Cache Configuration\n")
- sb.WriteString("# Auto-generated by Network Appliance Controller. DO NOT EDIT DIRECTLY.\n")
- sb.WriteString("# ==================================================================\n\n")
- cacheSize := m.settings.CacheSize
- if cacheSize <= 0 {
- cacheSize = 2048
- }
- sb.WriteString(fmt.Sprintf("cache-size=%d\n", cacheSize))
- // Do not read /etc/resolv.conf in dnsmasq (prevents loop with nameserver 127.0.0.1)
- sb.WriteString("no-resolv\n")
- // Upstream DNS Servers (Configured + Dynamic)
- allUpstreams := make([]string, 0)
- seen := make(map[string]bool)
- for _, s := range m.settings.Servers {
- trimmed := strings.TrimSpace(s)
- if trimmed != "" && !seen[trimmed] {
- allUpstreams = append(allUpstreams, trimmed)
- seen[trimmed] = true
- }
- }
- for _, s := range m.dynamicServers {
- trimmed := strings.TrimSpace(s)
- if trimmed != "" && !seen[trimmed] {
- allUpstreams = append(allUpstreams, trimmed)
- seen[trimmed] = true
- }
- }
- if len(allUpstreams) == 0 {
- allUpstreams = append(allUpstreams, "1.1.1.1", "8.8.8.8")
- }
- sb.WriteString("\n# Upstream DNS Forwarders\n")
- for _, s := range allUpstreams {
- sb.WriteString(fmt.Sprintf("server=%s\n", s))
- }
- // Domain
- domain := strings.TrimSpace(m.settings.Domain)
- if domain != "" {
- sb.WriteString(fmt.Sprintf("\ndomain=%s\n", domain))
- sb.WriteString(fmt.Sprintf("local=/%s/\n", domain))
- }
- // Remote requests restriction
- if !m.settings.AllowRemoteRequests {
- sb.WriteString("\n# Remote requests disabled: only listen on loopback for host\n")
- sb.WriteString("listen-address=127.0.0.1\n")
- }
- // Static DNS Records
- sb.WriteString("\n# Local Static DNS Records (/ip dns static)\n")
- for _, rec := range m.settings.StaticRecords {
- if rec.Disabled {
- continue
- }
- name := strings.TrimSpace(rec.Name)
- addr := strings.TrimSpace(rec.Address)
- if name == "" || addr == "" {
- continue
- }
- recType := strings.ToUpper(strings.TrimSpace(rec.Type))
- switch recType {
- case "CNAME":
- sb.WriteString(fmt.Sprintf("cname=%s,%s\n", name, addr))
- case "TXT":
- sb.WriteString(fmt.Sprintf("txt-record=%s,%q\n", name, addr))
- default: // "A" or "AAAA"
- sb.WriteString(fmt.Sprintf("address=/%s/%s\n", name, addr))
- }
- }
- // Write dnsmasq config
- _ = os.MkdirAll(filepath.Dir(DnsmasqDNSConf), 0755)
- if err := os.WriteFile(DnsmasqDNSConf, []byte(sb.String()), 0644); err != nil {
- return fmt.Errorf("failed to write %s: %w", DnsmasqDNSConf, err)
- }
- // Test dnsmasq syntax
- testOut, testErr := exec.Command("dnsmasq", "--test").CombinedOutput()
- if testErr != nil {
- log.Printf("[DNS] dnsmasq syntax check failed: %v, output: %s", testErr, string(testOut))
- return fmt.Errorf("dnsmasq configuration test failed: %s", string(testOut))
- }
- // Update host /etc/resolv.conf
- var rsb strings.Builder
- rsb.WriteString("# ==================================================================\n")
- rsb.WriteString("# Network Appliance System Resolver\n")
- rsb.WriteString("# Managed by DebTik DNS Controller\n")
- rsb.WriteString("# ==================================================================\n\n")
- rsb.WriteString("nameserver 127.0.0.1\n")
- for _, s := range allUpstreams {
- rsb.WriteString(fmt.Sprintf("nameserver %s\n", s))
- }
- if domain != "" {
- rsb.WriteString(fmt.Sprintf("search %s\n", domain))
- }
- if err := os.WriteFile(SystemResolvConf, []byte(rsb.String()), 0644); err != nil {
- return err
- }
- // Restart dnsmasq to re-read all configuration files
- if err := exec.Command("systemctl", "restart", "dnsmasq").Run(); err != nil {
- return fmt.Errorf("failed to restart dnsmasq: %w", err)
- }
- return nil
- }
- // GetSettings returns a snapshot of current DNS settings with dynamic servers populated.
- func (m *Manager) GetSettings() DNSSettings {
- m.mu.RLock()
- defer m.mu.RUnlock()
- s := m.settings
- s.DynamicServers = make([]string, len(m.dynamicServers))
- copy(s.DynamicServers, m.dynamicServers)
- return s
- }
- // SaveSettings updates the DNS configuration and applies it immediately.
- func (m *Manager) SaveSettings(newSettings DNSSettings) error {
- m.mu.Lock()
- defer m.mu.Unlock()
- cleanedServers := make([]string, 0)
- for _, s := range newSettings.Servers {
- s = strings.TrimSpace(s)
- if s != "" {
- if ip := net.ParseIP(s); ip == nil {
- return fmt.Errorf("invalid upstream DNS IP address %q", s)
- }
- cleanedServers = append(cleanedServers, s)
- }
- }
- if len(cleanedServers) == 0 {
- return fmt.Errorf("at least one valid upstream DNS server is required")
- }
- m.settings.Servers = cleanedServers
- m.settings.AllowRemoteRequests = newSettings.AllowRemoteRequests
- if newSettings.CacheSize > 0 {
- if newSettings.CacheSize > 50000 {
- m.settings.CacheSize = 50000
- } else {
- m.settings.CacheSize = newSettings.CacheSize
- }
- }
- m.settings.Domain = strings.TrimSpace(newSettings.Domain)
- m.settings.DoHServer = strings.TrimSpace(newSettings.DoHServer)
- m.settings.VerifyDoHCert = newSettings.VerifyDoHCert
- if err := m.saveLocked(); err != nil {
- return err
- }
- return m.applyLocked()
- }
- // SetDynamicServers sets peer DNS servers received dynamically from DHCP or PPP.
- func (m *Manager) SetDynamicServers(servers []string) {
- m.mu.Lock()
- defer m.mu.Unlock()
- changed := false
- if len(m.dynamicServers) != len(servers) {
- changed = true
- } else {
- for i := range servers {
- if m.dynamicServers[i] != servers[i] {
- changed = true
- break
- }
- }
- }
- if changed {
- m.dynamicServers = make([]string, 0, len(servers))
- for _, s := range servers {
- trimmed := strings.TrimSpace(s)
- if trimmed != "" {
- m.dynamicServers = append(m.dynamicServers, trimmed)
- }
- }
- _ = m.applyLocked()
- }
- }
- // ListStaticRecords returns all user-defined static DNS host mappings.
- func (m *Manager) ListStaticRecords() []StaticRecord {
- m.mu.RLock()
- defer m.mu.RUnlock()
- res := make([]StaticRecord, len(m.settings.StaticRecords))
- copy(res, m.settings.StaticRecords)
- return res
- }
- // SaveStaticRecord adds or updates a static DNS record.
- func (m *Manager) SaveStaticRecord(rec StaticRecord) error {
- m.mu.Lock()
- defer m.mu.Unlock()
- rec.Name = strings.TrimSpace(rec.Name)
- rec.Address = strings.TrimSpace(rec.Address)
- if rec.Name == "" {
- return fmt.Errorf("DNS name (hostname) is required")
- }
- if rec.Address == "" {
- return fmt.Errorf("target address is required")
- }
- rec.Type = strings.ToUpper(strings.TrimSpace(rec.Type))
- if rec.Type == "" {
- rec.Type = "A"
- }
- if rec.Type != "A" && rec.Type != "AAAA" && rec.Type != "CNAME" && rec.Type != "TXT" {
- return fmt.Errorf("unsupported record type %q: must be A, AAAA, CNAME, or TXT", rec.Type)
- }
- if rec.Type == "A" || rec.Type == "AAAA" {
- if ip := net.ParseIP(rec.Address); ip == nil {
- return fmt.Errorf("invalid IP address %q for record type %s", rec.Address, rec.Type)
- }
- }
- if rec.ID == "" {
- rec.ID = fmt.Sprintf("dns-rec-%d", time.Now().UnixNano())
- m.settings.StaticRecords = append(m.settings.StaticRecords, rec)
- } else {
- found := false
- for i, existing := range m.settings.StaticRecords {
- if existing.ID == rec.ID {
- m.settings.StaticRecords[i] = rec
- found = true
- break
- }
- }
- if !found {
- m.settings.StaticRecords = append(m.settings.StaticRecords, rec)
- }
- }
- if err := m.saveLocked(); err != nil {
- return err
- }
- return m.applyLocked()
- }
- // DeleteStaticRecord removes a static DNS record by ID.
- func (m *Manager) DeleteStaticRecord(id string) error {
- m.mu.Lock()
- defer m.mu.Unlock()
- newRecords := make([]StaticRecord, 0, len(m.settings.StaticRecords))
- found := false
- for _, rec := range m.settings.StaticRecords {
- if rec.ID == id {
- found = true
- continue
- }
- newRecords = append(newRecords, rec)
- }
- if !found {
- return fmt.Errorf("static DNS record %q not found", id)
- }
- m.settings.StaticRecords = newRecords
- if err := m.saveLocked(); err != nil {
- return err
- }
- return m.applyLocked()
- }
- // ToggleStaticRecord enables or disables a static DNS record.
- func (m *Manager) ToggleStaticRecord(id string, disabled bool) error {
- m.mu.Lock()
- defer m.mu.Unlock()
- found := false
- for i := range m.settings.StaticRecords {
- if m.settings.StaticRecords[i].ID == id {
- m.settings.StaticRecords[i].Disabled = disabled
- found = true
- break
- }
- }
- if !found {
- return fmt.Errorf("static DNS record %q not found", id)
- }
- if err := m.saveLocked(); err != nil {
- return err
- }
- return m.applyLocked()
- }
- // FlushCache clears dnsmasq DNS cache by sending SIGHUP.
- func (m *Manager) FlushCache() error {
- cmd := exec.Command("pkill", "-HUP", "dnsmasq")
- if out, err := cmd.CombinedOutput(); err != nil {
- // Fallback to reload
- _ = exec.Command("systemctl", "reload", "dnsmasq").Run()
- log.Printf("[DNS] pkill -HUP result: %v (%s), fallback reload triggered", err, string(out))
- }
- return nil
- }
- // GetStatus returns the operational status of the DNS subsystem.
- func (m *Manager) GetStatus() DNSStatus {
- m.mu.RLock()
- defer m.mu.RUnlock()
- resolvBytes, _ := os.ReadFile(SystemResolvConf)
- // Check if dnsmasq is running
- isRunning := false
- if err := exec.Command("systemctl", "is-active", "--quiet", "dnsmasq").Run(); err == nil {
- isRunning = true
- }
- activeServers := make([]string, 0, len(m.settings.Servers)+len(m.dynamicServers))
- activeServers = append(activeServers, m.settings.Servers...)
- activeServers = append(activeServers, m.dynamicServers...)
- return DNSStatus{
- ActiveServers: activeServers,
- AllowRemoteRequests: m.settings.AllowRemoteRequests,
- CacheSize: m.settings.CacheSize,
- StaticCount: len(m.settings.StaticRecords),
- Domain: m.settings.Domain,
- ResolvConfContent: string(resolvBytes),
- ServiceRunning: isRunning,
- }
- }
- // Helper: read existing nameservers from /etc/resolv.conf
- func readSystemNameservers() []string {
- data, err := os.ReadFile(SystemResolvConf)
- if err != nil {
- return nil
- }
- var servers []string
- lines := strings.Split(string(data), "\n")
- for _, l := range lines {
- l = strings.TrimSpace(l)
- if strings.HasPrefix(l, "nameserver") {
- parts := strings.Fields(l)
- if len(parts) >= 2 && parts[1] != "127.0.0.1" && parts[1] != "::1" {
- servers = append(servers, parts[1])
- }
- }
- }
- return servers
- }
|