manager.go 12 KB

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