manager.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486
  1. package dns
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "log"
  6. "net"
  7. "os"
  8. "os/exec"
  9. "path/filepath"
  10. "strings"
  11. "sync"
  12. "time"
  13. )
  14. const (
  15. SettingsStorePath = "/etc/proxmox-router/dns/settings.json"
  16. DnsmasqDNSConf = "/etc/dnsmasq.d/debtik_dns.conf"
  17. SystemResolvConf = "/etc/resolv.conf"
  18. )
  19. // Manager coordinates Router DNS forwarder, upstream resolvers, and static DNS mapping.
  20. type Manager struct {
  21. mu sync.RWMutex
  22. settings DNSSettings
  23. dynamicServers []string
  24. }
  25. func NewManager() *Manager {
  26. m := &Manager{
  27. dynamicServers: make([]string, 0),
  28. settings: DNSSettings{
  29. Servers: []string{"1.1.1.1", "8.8.8.8"},
  30. DynamicServers: make([]string, 0),
  31. AllowRemoteRequests: true,
  32. CacheSize: 2048,
  33. Domain: "",
  34. StaticRecords: make([]StaticRecord, 0),
  35. },
  36. }
  37. m.load()
  38. if err := m.applyLocked(); err != nil {
  39. log.Printf("[DNS] Error applying initial DNS configuration: %v", err)
  40. }
  41. return m
  42. }
  43. func (m *Manager) load() {
  44. m.mu.Lock()
  45. defer m.mu.Unlock()
  46. data, err := os.ReadFile(SettingsStorePath)
  47. if err != nil {
  48. // If no settings file, read existing resolv.conf nameservers if any
  49. if curNameservers := readSystemNameservers(); len(curNameservers) > 0 {
  50. m.settings.Servers = curNameservers
  51. }
  52. return
  53. }
  54. var s DNSSettings
  55. if err := json.Unmarshal(data, &s); err == nil {
  56. if len(s.Servers) > 0 {
  57. m.settings.Servers = s.Servers
  58. }
  59. m.settings.AllowRemoteRequests = s.AllowRemoteRequests
  60. if s.CacheSize > 0 {
  61. m.settings.CacheSize = s.CacheSize
  62. }
  63. m.settings.Domain = s.Domain
  64. m.settings.DoHServer = s.DoHServer
  65. m.settings.VerifyDoHCert = s.VerifyDoHCert
  66. if s.StaticRecords != nil {
  67. m.settings.StaticRecords = s.StaticRecords
  68. }
  69. }
  70. }
  71. func (m *Manager) saveLocked() error {
  72. dir := filepath.Dir(SettingsStorePath)
  73. if err := os.MkdirAll(dir, 0755); err != nil {
  74. return err
  75. }
  76. data, err := json.MarshalIndent(m.settings, "", " ")
  77. if err != nil {
  78. return err
  79. }
  80. return os.WriteFile(SettingsStorePath, data, 0644)
  81. }
  82. // applyLocked renders the dnsmasq DNS configuration and updates /etc/resolv.conf.
  83. func (m *Manager) applyLocked() (result error) {
  84. oldConfig, configErr := os.ReadFile(DnsmasqDNSConf)
  85. oldResolver, resolverErr := os.ReadFile(SystemResolvConf)
  86. wasRunning := exec.Command("systemctl", "is-active", "--quiet", "dnsmasq").Run() == nil
  87. defer func() {
  88. if result != nil {
  89. if configErr == nil {
  90. _ = os.WriteFile(DnsmasqDNSConf, oldConfig, 0644)
  91. } else if os.IsNotExist(configErr) {
  92. _ = os.Remove(DnsmasqDNSConf)
  93. }
  94. if resolverErr == nil {
  95. _ = os.WriteFile(SystemResolvConf, oldResolver, 0644)
  96. }
  97. if wasRunning {
  98. if err := exec.Command("systemctl", "restart", "dnsmasq").Run(); err != nil {
  99. result = fmt.Errorf("DNS apply failed: %v; previous config restart failed: %w", result, err)
  100. }
  101. }
  102. }
  103. }()
  104. var sb strings.Builder
  105. sb.WriteString("# ==================================================================\n")
  106. sb.WriteString("# Enterprise Router DNS Forwarder & Cache Configuration\n")
  107. sb.WriteString("# Auto-generated by Network Appliance Controller. DO NOT EDIT DIRECTLY.\n")
  108. sb.WriteString("# ==================================================================\n\n")
  109. cacheSize := m.settings.CacheSize
  110. if cacheSize <= 0 {
  111. cacheSize = 2048
  112. }
  113. sb.WriteString(fmt.Sprintf("cache-size=%d\n", cacheSize))
  114. // Do not read /etc/resolv.conf in dnsmasq (prevents loop with nameserver 127.0.0.1)
  115. sb.WriteString("no-resolv\n")
  116. // Upstream DNS Servers (Configured + Dynamic)
  117. allUpstreams := make([]string, 0)
  118. seen := make(map[string]bool)
  119. for _, s := range m.settings.Servers {
  120. trimmed := strings.TrimSpace(s)
  121. if trimmed != "" && !seen[trimmed] {
  122. allUpstreams = append(allUpstreams, trimmed)
  123. seen[trimmed] = true
  124. }
  125. }
  126. for _, s := range m.dynamicServers {
  127. trimmed := strings.TrimSpace(s)
  128. if trimmed != "" && !seen[trimmed] {
  129. allUpstreams = append(allUpstreams, trimmed)
  130. seen[trimmed] = true
  131. }
  132. }
  133. if len(allUpstreams) == 0 {
  134. allUpstreams = append(allUpstreams, "1.1.1.1", "8.8.8.8")
  135. }
  136. sb.WriteString("\n# Upstream DNS Forwarders\n")
  137. for _, s := range allUpstreams {
  138. sb.WriteString(fmt.Sprintf("server=%s\n", s))
  139. }
  140. // Domain
  141. domain := strings.TrimSpace(m.settings.Domain)
  142. if domain != "" {
  143. sb.WriteString(fmt.Sprintf("\ndomain=%s\n", domain))
  144. sb.WriteString(fmt.Sprintf("local=/%s/\n", domain))
  145. }
  146. // Remote requests restriction
  147. if !m.settings.AllowRemoteRequests {
  148. sb.WriteString("\n# Remote requests disabled: only listen on loopback for host\n")
  149. sb.WriteString("listen-address=127.0.0.1\n")
  150. }
  151. // Static DNS Records
  152. sb.WriteString("\n# Local Static DNS Records (/ip dns static)\n")
  153. for _, rec := range m.settings.StaticRecords {
  154. if rec.Disabled {
  155. continue
  156. }
  157. name := strings.TrimSpace(rec.Name)
  158. addr := strings.TrimSpace(rec.Address)
  159. if name == "" || addr == "" {
  160. continue
  161. }
  162. recType := strings.ToUpper(strings.TrimSpace(rec.Type))
  163. switch recType {
  164. case "CNAME":
  165. sb.WriteString(fmt.Sprintf("cname=%s,%s\n", name, addr))
  166. case "TXT":
  167. sb.WriteString(fmt.Sprintf("txt-record=%s,%q\n", name, addr))
  168. default: // "A" or "AAAA"
  169. sb.WriteString(fmt.Sprintf("address=/%s/%s\n", name, addr))
  170. }
  171. }
  172. // Write dnsmasq config
  173. _ = os.MkdirAll(filepath.Dir(DnsmasqDNSConf), 0755)
  174. if err := os.WriteFile(DnsmasqDNSConf, []byte(sb.String()), 0644); err != nil {
  175. return fmt.Errorf("failed to write %s: %w", DnsmasqDNSConf, err)
  176. }
  177. // Test dnsmasq syntax
  178. testOut, testErr := exec.Command("dnsmasq", "--test").CombinedOutput()
  179. if testErr != nil {
  180. log.Printf("[DNS] dnsmasq syntax check failed: %v, output: %s", testErr, string(testOut))
  181. return fmt.Errorf("dnsmasq configuration test failed: %s", string(testOut))
  182. }
  183. // Update host /etc/resolv.conf
  184. var rsb strings.Builder
  185. rsb.WriteString("# ==================================================================\n")
  186. rsb.WriteString("# Network Appliance System Resolver\n")
  187. rsb.WriteString("# Managed by DebTik DNS Controller\n")
  188. rsb.WriteString("# ==================================================================\n\n")
  189. rsb.WriteString("nameserver 127.0.0.1\n")
  190. for _, s := range allUpstreams {
  191. rsb.WriteString(fmt.Sprintf("nameserver %s\n", s))
  192. }
  193. if domain != "" {
  194. rsb.WriteString(fmt.Sprintf("search %s\n", domain))
  195. }
  196. if err := os.WriteFile(SystemResolvConf, []byte(rsb.String()), 0644); err != nil {
  197. return err
  198. }
  199. // Restart dnsmasq to re-read all configuration files
  200. if err := exec.Command("systemctl", "restart", "dnsmasq").Run(); err != nil {
  201. return fmt.Errorf("failed to restart dnsmasq: %w", err)
  202. }
  203. return nil
  204. }
  205. // GetSettings returns a snapshot of current DNS settings with dynamic servers populated.
  206. func (m *Manager) GetSettings() DNSSettings {
  207. m.mu.RLock()
  208. defer m.mu.RUnlock()
  209. s := m.settings
  210. s.DynamicServers = make([]string, len(m.dynamicServers))
  211. copy(s.DynamicServers, m.dynamicServers)
  212. return s
  213. }
  214. // SaveSettings updates the DNS configuration and applies it immediately.
  215. func (m *Manager) SaveSettings(newSettings DNSSettings) error {
  216. m.mu.Lock()
  217. defer m.mu.Unlock()
  218. cleanedServers := make([]string, 0)
  219. for _, s := range newSettings.Servers {
  220. s = strings.TrimSpace(s)
  221. if s != "" {
  222. if ip := net.ParseIP(s); ip == nil {
  223. return fmt.Errorf("invalid upstream DNS IP address %q", s)
  224. }
  225. cleanedServers = append(cleanedServers, s)
  226. }
  227. }
  228. if len(cleanedServers) == 0 {
  229. return fmt.Errorf("at least one valid upstream DNS server is required")
  230. }
  231. m.settings.Servers = cleanedServers
  232. m.settings.AllowRemoteRequests = newSettings.AllowRemoteRequests
  233. if newSettings.CacheSize > 0 {
  234. if newSettings.CacheSize > 50000 {
  235. m.settings.CacheSize = 50000
  236. } else {
  237. m.settings.CacheSize = newSettings.CacheSize
  238. }
  239. }
  240. m.settings.Domain = strings.TrimSpace(newSettings.Domain)
  241. m.settings.DoHServer = strings.TrimSpace(newSettings.DoHServer)
  242. m.settings.VerifyDoHCert = newSettings.VerifyDoHCert
  243. if err := m.saveLocked(); err != nil {
  244. return err
  245. }
  246. return m.applyLocked()
  247. }
  248. // SetDynamicServers sets peer DNS servers received dynamically from DHCP or PPP.
  249. func (m *Manager) SetDynamicServers(servers []string) {
  250. m.mu.Lock()
  251. defer m.mu.Unlock()
  252. changed := false
  253. if len(m.dynamicServers) != len(servers) {
  254. changed = true
  255. } else {
  256. for i := range servers {
  257. if m.dynamicServers[i] != servers[i] {
  258. changed = true
  259. break
  260. }
  261. }
  262. }
  263. if changed {
  264. m.dynamicServers = make([]string, 0, len(servers))
  265. for _, s := range servers {
  266. trimmed := strings.TrimSpace(s)
  267. if trimmed != "" {
  268. m.dynamicServers = append(m.dynamicServers, trimmed)
  269. }
  270. }
  271. _ = m.applyLocked()
  272. }
  273. }
  274. // ListStaticRecords returns all user-defined static DNS host mappings.
  275. func (m *Manager) ListStaticRecords() []StaticRecord {
  276. m.mu.RLock()
  277. defer m.mu.RUnlock()
  278. res := make([]StaticRecord, len(m.settings.StaticRecords))
  279. copy(res, m.settings.StaticRecords)
  280. return res
  281. }
  282. // SaveStaticRecord adds or updates a static DNS record.
  283. func (m *Manager) SaveStaticRecord(rec StaticRecord) error {
  284. m.mu.Lock()
  285. defer m.mu.Unlock()
  286. rec.Name = strings.TrimSpace(rec.Name)
  287. rec.Address = strings.TrimSpace(rec.Address)
  288. if rec.Name == "" {
  289. return fmt.Errorf("DNS name (hostname) is required")
  290. }
  291. if rec.Address == "" {
  292. return fmt.Errorf("target address is required")
  293. }
  294. rec.Type = strings.ToUpper(strings.TrimSpace(rec.Type))
  295. if rec.Type == "" {
  296. rec.Type = "A"
  297. }
  298. if rec.Type != "A" && rec.Type != "AAAA" && rec.Type != "CNAME" && rec.Type != "TXT" {
  299. return fmt.Errorf("unsupported record type %q: must be A, AAAA, CNAME, or TXT", rec.Type)
  300. }
  301. if rec.Type == "A" || rec.Type == "AAAA" {
  302. if ip := net.ParseIP(rec.Address); ip == nil {
  303. return fmt.Errorf("invalid IP address %q for record type %s", rec.Address, rec.Type)
  304. }
  305. }
  306. if rec.ID == "" {
  307. rec.ID = fmt.Sprintf("dns-rec-%d", time.Now().UnixNano())
  308. m.settings.StaticRecords = append(m.settings.StaticRecords, rec)
  309. } else {
  310. found := false
  311. for i, existing := range m.settings.StaticRecords {
  312. if existing.ID == rec.ID {
  313. m.settings.StaticRecords[i] = rec
  314. found = true
  315. break
  316. }
  317. }
  318. if !found {
  319. m.settings.StaticRecords = append(m.settings.StaticRecords, rec)
  320. }
  321. }
  322. if err := m.saveLocked(); err != nil {
  323. return err
  324. }
  325. return m.applyLocked()
  326. }
  327. // DeleteStaticRecord removes a static DNS record by ID.
  328. func (m *Manager) DeleteStaticRecord(id string) error {
  329. m.mu.Lock()
  330. defer m.mu.Unlock()
  331. newRecords := make([]StaticRecord, 0, len(m.settings.StaticRecords))
  332. found := false
  333. for _, rec := range m.settings.StaticRecords {
  334. if rec.ID == id {
  335. found = true
  336. continue
  337. }
  338. newRecords = append(newRecords, rec)
  339. }
  340. if !found {
  341. return fmt.Errorf("static DNS record %q not found", id)
  342. }
  343. m.settings.StaticRecords = newRecords
  344. if err := m.saveLocked(); err != nil {
  345. return err
  346. }
  347. return m.applyLocked()
  348. }
  349. // ToggleStaticRecord enables or disables a static DNS record.
  350. func (m *Manager) ToggleStaticRecord(id string, disabled bool) error {
  351. m.mu.Lock()
  352. defer m.mu.Unlock()
  353. found := false
  354. for i := range m.settings.StaticRecords {
  355. if m.settings.StaticRecords[i].ID == id {
  356. m.settings.StaticRecords[i].Disabled = disabled
  357. found = true
  358. break
  359. }
  360. }
  361. if !found {
  362. return fmt.Errorf("static DNS record %q not found", id)
  363. }
  364. if err := m.saveLocked(); err != nil {
  365. return err
  366. }
  367. return m.applyLocked()
  368. }
  369. // FlushCache clears dnsmasq DNS cache by sending SIGHUP.
  370. func (m *Manager) FlushCache() error {
  371. cmd := exec.Command("pkill", "-HUP", "dnsmasq")
  372. if out, err := cmd.CombinedOutput(); err != nil {
  373. // Fallback to reload
  374. _ = exec.Command("systemctl", "reload", "dnsmasq").Run()
  375. log.Printf("[DNS] pkill -HUP result: %v (%s), fallback reload triggered", err, string(out))
  376. }
  377. return nil
  378. }
  379. // GetStatus returns the operational status of the DNS subsystem.
  380. func (m *Manager) GetStatus() DNSStatus {
  381. m.mu.RLock()
  382. defer m.mu.RUnlock()
  383. resolvBytes, _ := os.ReadFile(SystemResolvConf)
  384. // Check if dnsmasq is running
  385. isRunning := false
  386. if err := exec.Command("systemctl", "is-active", "--quiet", "dnsmasq").Run(); err == nil {
  387. isRunning = true
  388. }
  389. activeServers := make([]string, 0, len(m.settings.Servers)+len(m.dynamicServers))
  390. activeServers = append(activeServers, m.settings.Servers...)
  391. activeServers = append(activeServers, m.dynamicServers...)
  392. return DNSStatus{
  393. ActiveServers: activeServers,
  394. AllowRemoteRequests: m.settings.AllowRemoteRequests,
  395. CacheSize: m.settings.CacheSize,
  396. StaticCount: len(m.settings.StaticRecords),
  397. Domain: m.settings.Domain,
  398. ResolvConfContent: string(resolvBytes),
  399. ServiceRunning: isRunning,
  400. }
  401. }
  402. // Helper: read existing nameservers from /etc/resolv.conf
  403. func readSystemNameservers() []string {
  404. data, err := os.ReadFile(SystemResolvConf)
  405. if err != nil {
  406. return nil
  407. }
  408. var servers []string
  409. lines := strings.Split(string(data), "\n")
  410. for _, l := range lines {
  411. l = strings.TrimSpace(l)
  412. if strings.HasPrefix(l, "nameserver") {
  413. parts := strings.Fields(l)
  414. if len(parts) >= 2 && parts[1] != "127.0.0.1" && parts[1] != "::1" {
  415. servers = append(servers, parts[1])
  416. }
  417. }
  418. }
  419. return servers
  420. }