server_manager.go 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924
  1. package dhcp
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "log"
  6. "net"
  7. "os"
  8. "os/exec"
  9. "path/filepath"
  10. "sort"
  11. "strconv"
  12. "strings"
  13. "sync"
  14. "time"
  15. )
  16. const (
  17. DHCPServerStoreFile = "/etc/proxmox-router/dhcp_server.json"
  18. DnsmasqConfFile = "/etc/dnsmasq.d/debtik_dhcp.conf"
  19. DnsmasqLeasesFile = "/var/lib/misc/dnsmasq.leases"
  20. )
  21. type ServerManager struct {
  22. mu sync.RWMutex
  23. storePath string
  24. confPath string
  25. leasesPath string
  26. servers map[string]DHCPServerInstance
  27. networks map[string]DHCPNetwork
  28. staticLeases map[string]DHCPLease
  29. options map[string]DHCPOption
  30. }
  31. func NewServerManager() *ServerManager {
  32. sm := &ServerManager{
  33. storePath: DHCPServerStoreFile,
  34. confPath: DnsmasqConfFile,
  35. leasesPath: DnsmasqLeasesFile,
  36. servers: make(map[string]DHCPServerInstance),
  37. networks: make(map[string]DHCPNetwork),
  38. staticLeases: make(map[string]DHCPLease),
  39. options: make(map[string]DHCPOption),
  40. }
  41. _ = os.MkdirAll(filepath.Dir(sm.storePath), 0755)
  42. _ = os.MkdirAll(filepath.Dir(sm.confPath), 0755)
  43. if err := sm.load(); err != nil {
  44. log.Printf("[DHCP-Server] Error loading config: %v", err)
  45. }
  46. // Initial render and reload dnsmasq
  47. if err := sm.applyDnsmasqConfigLocked(); err != nil {
  48. log.Printf("[DHCP-Server] Error applying dnsmasq config: %v", err)
  49. }
  50. return sm
  51. }
  52. func (sm *ServerManager) load() error {
  53. sm.mu.Lock()
  54. defer sm.mu.Unlock()
  55. data, err := os.ReadFile(sm.storePath)
  56. if err != nil {
  57. if os.IsNotExist(err) {
  58. // Populate default option presets (MikroTik standard presets)
  59. sm.initDefaultOptions()
  60. return nil
  61. }
  62. return err
  63. }
  64. var store DHCPServerConfigStore
  65. if err := json.Unmarshal(data, &store); err != nil {
  66. return err
  67. }
  68. for _, s := range store.Servers {
  69. sm.servers[s.ID] = s
  70. }
  71. for _, n := range store.Networks {
  72. sm.networks[n.ID] = n
  73. }
  74. for _, l := range store.StaticLeases {
  75. sm.staticLeases[l.ID] = l
  76. }
  77. for _, o := range store.Options {
  78. sm.options[o.ID] = o
  79. }
  80. if len(sm.options) == 0 {
  81. sm.initDefaultOptions()
  82. }
  83. return nil
  84. }
  85. func (sm *ServerManager) initDefaultOptions() {
  86. presets := []DHCPOption{
  87. {ID: "opt-ntp", Name: "ntp-servers", Code: 42, Value: "192.168.88.1", Comment: "NTP Time Servers (Option 42)"},
  88. {ID: "opt-tftp-name", Name: "tftp-server-name", Code: 66, Value: "192.168.88.50", Comment: "TFTP Server Name / IP (Option 66)"},
  89. {ID: "opt-bootfile", Name: "bootfile-name", Code: 67, Value: "pxelinux.0", Comment: "PXE Boot File (Option 67)"},
  90. {ID: "opt-wins", Name: "wins-servers", Code: 44, Value: "192.168.88.50", Comment: "WINS NetBIOS Servers (Option 44)"},
  91. {ID: "opt-unifi", Name: "unifi-controller", Code: 43, Value: "01:04:c0:a8:58:0a", Comment: "UniFi Inform Controller (Option 43 Hex IP)"},
  92. {ID: "opt-voip", Name: "cisco-voip-tftp", Code: 150, Value: "192.168.88.50", Comment: "Cisco VoIP TFTP (Option 150)"},
  93. {ID: "opt-capwap", Name: "capwap-ac", Code: 138, Value: "192.168.88.10", Comment: "CAPWAP Access Controller (Option 138)"},
  94. {ID: "opt-wpad", Name: "wpad-url", Code: 252, Value: "\"http://wpad/wpad.dat\"", Comment: "Web Proxy Auto-Discovery (Option 252)"},
  95. }
  96. for _, p := range presets {
  97. sm.options[p.ID] = p
  98. }
  99. }
  100. func (sm *ServerManager) saveLocked() error {
  101. store := DHCPServerConfigStore{
  102. Servers: make([]DHCPServerInstance, 0, len(sm.servers)),
  103. Networks: make([]DHCPNetwork, 0, len(sm.networks)),
  104. StaticLeases: make([]DHCPLease, 0, len(sm.staticLeases)),
  105. Options: make([]DHCPOption, 0, len(sm.options)),
  106. }
  107. for _, s := range sm.servers {
  108. copyS := s
  109. copyS.Running = false
  110. copyS.ActiveLeases = 0
  111. store.Servers = append(store.Servers, copyS)
  112. }
  113. for _, n := range sm.networks {
  114. store.Networks = append(store.Networks, n)
  115. }
  116. for _, l := range sm.staticLeases {
  117. store.StaticLeases = append(store.StaticLeases, l)
  118. }
  119. for _, o := range sm.options {
  120. store.Options = append(store.Options, o)
  121. }
  122. data, err := json.MarshalIndent(store, "", " ")
  123. if err != nil {
  124. return err
  125. }
  126. return os.WriteFile(sm.storePath, data, 0644)
  127. }
  128. func (sm *ServerManager) applyDnsmasqConfigLocked() error {
  129. var sb strings.Builder
  130. sb.WriteString("# ==================================================================\n")
  131. sb.WriteString("# Enterprise DHCP Server Configuration\n")
  132. sb.WriteString("# Auto-generated by Network Appliance. DO NOT EDIT DIRECTLY.\n")
  133. sb.WriteString("# ==================================================================\n\n")
  134. sb.WriteString("dhcp-authoritative\n")
  135. sb.WriteString("dhcp-leasefile=/var/lib/misc/dnsmasq.leases\n")
  136. sb.WriteString("log-dhcp\n")
  137. sb.WriteString("bind-dynamic\n\n")
  138. activeServerCount := 0
  139. for _, srv := range sm.servers {
  140. if srv.Disabled {
  141. continue
  142. }
  143. activeServerCount++
  144. tag := sanitizeTag(srv.Name)
  145. sb.WriteString(fmt.Sprintf("# --- DHCP Server: %s on %s ---\n", srv.Name, srv.Interface))
  146. sb.WriteString(fmt.Sprintf("interface=%s\n", srv.Interface))
  147. leaseTime := srv.LeaseTime
  148. if leaseTime == "" {
  149. leaseTime = "12h"
  150. }
  151. // Find assigned network for server
  152. var assignedNet *DHCPNetwork
  153. if srv.Network != "" {
  154. for _, n := range sm.networks {
  155. if n.Address == srv.Network || n.ID == srv.Network {
  156. copyN := n
  157. assignedNet = &copyN
  158. break
  159. }
  160. }
  161. }
  162. // Parse Address Pool range (e.g. 192.168.88.10-192.168.88.254)
  163. startIP, endIP, netmask := parseAddressPool(srv.AddressPool)
  164. if netmask == "" && assignedNet != nil {
  165. if assignedNet.Netmask != "" {
  166. netmask = assignedNet.Netmask
  167. } else if _, ipNet, err := net.ParseCIDR(assignedNet.Address); err == nil && ipNet != nil {
  168. netmask = net.IP(ipNet.Mask).String()
  169. }
  170. }
  171. if startIP != "" && endIP != "" {
  172. if srv.Interface != "" {
  173. if netmask != "" {
  174. sb.WriteString(fmt.Sprintf("dhcp-range=tag:%s,set:%s,%s,%s,%s,%s\n", srv.Interface, tag, startIP, endIP, netmask, leaseTime))
  175. } else {
  176. sb.WriteString(fmt.Sprintf("dhcp-range=tag:%s,set:%s,%s,%s,%s\n", srv.Interface, tag, startIP, endIP, leaseTime))
  177. }
  178. } else {
  179. if netmask != "" {
  180. sb.WriteString(fmt.Sprintf("dhcp-range=set:%s,%s,%s,%s,%s\n", tag, startIP, endIP, netmask, leaseTime))
  181. } else {
  182. sb.WriteString(fmt.Sprintf("dhcp-range=set:%s,%s,%s,%s\n", tag, startIP, endIP, leaseTime))
  183. }
  184. }
  185. }
  186. // Emit assigned network options specifically for this DHCP server tag
  187. if assignedNet != nil {
  188. targetTag := fmt.Sprintf("tag:%s,", tag)
  189. if assignedNet.Gateway != "" {
  190. sb.WriteString(fmt.Sprintf("dhcp-option=%soption:router,%s\n", targetTag, assignedNet.Gateway))
  191. }
  192. if len(assignedNet.DNSServers) > 0 {
  193. sb.WriteString(fmt.Sprintf("dhcp-option=%soption:dns-server,%s\n", targetTag, strings.Join(assignedNet.DNSServers, ",")))
  194. }
  195. if assignedNet.Domain != "" {
  196. sb.WriteString(fmt.Sprintf("dhcp-option=%soption:domain-name,%s\n", targetTag, assignedNet.Domain))
  197. }
  198. if len(assignedNet.NTPServers) > 0 {
  199. sb.WriteString(fmt.Sprintf("dhcp-option=%soption:ntp-server,%s\n", targetTag, strings.Join(assignedNet.NTPServers, ",")))
  200. }
  201. if assignedNet.NextServer != "" {
  202. sb.WriteString(fmt.Sprintf("dhcp-option=%soption:tftp-server,%s\n", targetTag, assignedNet.NextServer))
  203. }
  204. if assignedNet.BootFileName != "" {
  205. sb.WriteString(fmt.Sprintf("dhcp-option=%soption:bootfile-name,%s\n", targetTag, assignedNet.BootFileName))
  206. if assignedNet.NextServer != "" {
  207. sb.WriteString(fmt.Sprintf("dhcp-boot=%s%s,,%s\n", targetTag, assignedNet.BootFileName, assignedNet.NextServer))
  208. } else {
  209. sb.WriteString(fmt.Sprintf("dhcp-boot=%s%s\n", targetTag, assignedNet.BootFileName))
  210. }
  211. }
  212. if len(assignedNet.WINSServers) > 0 {
  213. sb.WriteString(fmt.Sprintf("dhcp-option=%soption:netbios-ns,%s\n", targetTag, strings.Join(assignedNet.WINSServers, ",")))
  214. }
  215. for _, optID := range assignedNet.DHCPOptions {
  216. if opt, exists := sm.options[optID]; exists {
  217. sb.WriteString(fmt.Sprintf("dhcp-option=%s%d,%s # %s\n", targetTag, opt.Code, opt.Value, opt.Name))
  218. }
  219. }
  220. }
  221. sb.WriteString("\n")
  222. }
  223. // Subnet Networks & Options (Fallback and IP-matching)
  224. sb.WriteString("# --- Subnet Networks Fallbacks & IP matching ---\n")
  225. for _, netw := range sm.networks {
  226. sb.WriteString(fmt.Sprintf("# Subnet: %s\n", netw.Address))
  227. _, ipNet, err := net.ParseCIDR(netw.Address)
  228. isCoveredByServer := false
  229. if ipNet != nil {
  230. for _, srv := range sm.servers {
  231. if !srv.Disabled && (srv.Network == netw.ID || srv.Network == netw.Address) {
  232. isCoveredByServer = true
  233. break
  234. }
  235. }
  236. }
  237. networkTag := ""
  238. if err == nil && ipNet != nil {
  239. networkTag = fmt.Sprintf("net_%s", strings.ReplaceAll(ipNet.IP.String(), ".", "_"))
  240. if !isCoveredByServer {
  241. sb.WriteString(fmt.Sprintf("dhcp-range=set:%s,%s,static\n", networkTag, ipNet.IP.String()))
  242. }
  243. }
  244. // Determine target tag (either network tag or global/matched)
  245. targetTag := ""
  246. if networkTag != "" {
  247. targetTag = fmt.Sprintf("tag:%s,", networkTag)
  248. }
  249. // Option 3: Gateway (Router)
  250. if netw.Gateway != "" {
  251. sb.WriteString(fmt.Sprintf("dhcp-option=%soption:router,%s\n", targetTag, netw.Gateway))
  252. }
  253. // Option 6: DNS Servers
  254. if len(netw.DNSServers) > 0 {
  255. sb.WriteString(fmt.Sprintf("dhcp-option=%soption:dns-server,%s\n", targetTag, strings.Join(netw.DNSServers, ",")))
  256. }
  257. // Option 15: Domain Name
  258. if netw.Domain != "" {
  259. sb.WriteString(fmt.Sprintf("dhcp-option=%soption:domain-name,%s\n", targetTag, netw.Domain))
  260. }
  261. // Check which codes are provided via linked custom options
  262. hasLinkedOption := make(map[int]bool)
  263. for _, optID := range netw.DHCPOptions {
  264. if opt, exists := sm.options[optID]; exists {
  265. hasLinkedOption[opt.Code] = true
  266. }
  267. }
  268. // Option 42: NTP Servers (if not linked via custom option)
  269. if len(netw.NTPServers) > 0 && !hasLinkedOption[42] {
  270. sb.WriteString(fmt.Sprintf("dhcp-option=%soption:ntp-server,%s\n", targetTag, strings.Join(netw.NTPServers, ",")))
  271. }
  272. // Option 66: TFTP Server Name / IP (if not linked via custom option)
  273. if netw.NextServer != "" && !hasLinkedOption[66] {
  274. sb.WriteString(fmt.Sprintf("dhcp-option=%soption:tftp-server,%s\n", targetTag, netw.NextServer))
  275. }
  276. // Option 67: Boot File Name (if not linked via custom option)
  277. if netw.BootFileName != "" && !hasLinkedOption[67] {
  278. sb.WriteString(fmt.Sprintf("dhcp-option=%soption:bootfile-name,%s\n", targetTag, netw.BootFileName))
  279. if netw.NextServer != "" {
  280. sb.WriteString(fmt.Sprintf("dhcp-boot=%s%s,,%s\n", targetTag, netw.BootFileName, netw.NextServer))
  281. } else {
  282. sb.WriteString(fmt.Sprintf("dhcp-boot=%s%s\n", targetTag, netw.BootFileName))
  283. }
  284. }
  285. // Option 44: WINS Servers (if not linked via custom option)
  286. if len(netw.WINSServers) > 0 && !hasLinkedOption[44] {
  287. sb.WriteString(fmt.Sprintf("dhcp-option=%soption:netbios-ns,%s\n", targetTag, strings.Join(netw.WINSServers, ",")))
  288. }
  289. // Linked custom DHCP options
  290. for _, optID := range netw.DHCPOptions {
  291. if opt, exists := sm.options[optID]; exists {
  292. sb.WriteString(fmt.Sprintf("dhcp-option=%s%d,%s # %s\n", targetTag, opt.Code, opt.Value, opt.Name))
  293. }
  294. }
  295. sb.WriteString("\n")
  296. }
  297. // Static Leases & Reservations
  298. sb.WriteString("# --- Static Leases & Reservations ---\n")
  299. for _, l := range sm.staticLeases {
  300. if l.Disabled {
  301. continue
  302. }
  303. mac := strings.ToLower(strings.TrimSpace(l.MACAddress))
  304. ip := strings.TrimSpace(l.Address)
  305. hostname := strings.TrimSpace(l.HostName)
  306. if hostname == "" {
  307. hostname = "reserved"
  308. }
  309. if mac != "" && ip != "" {
  310. sb.WriteString(fmt.Sprintf("dhcp-host=%s,%s,%s,infinite\n", mac, ip, hostname))
  311. }
  312. }
  313. // Write config to disk
  314. if err := os.WriteFile(sm.confPath, []byte(sb.String()), 0644); err != nil {
  315. return fmt.Errorf("failed to write dnsmasq config: %w", err)
  316. }
  317. // Restart or reload dnsmasq
  318. if activeServerCount > 0 {
  319. out, err := exec.Command("systemctl", "restart", "dnsmasq").CombinedOutput()
  320. if err != nil {
  321. log.Printf("[DHCPServer] Failed to restart dnsmasq: %v, output: %s", err, string(out))
  322. } else {
  323. log.Printf("[DHCPServer] dnsmasq restarted successfully with %d active DHCP server(s)", activeServerCount)
  324. }
  325. } else {
  326. _ = exec.Command("systemctl", "reload", "dnsmasq").Run()
  327. }
  328. return nil
  329. }
  330. // RenameInterface updates DHCP server interface bindings when an interface is renamed.
  331. func (sm *ServerManager) RenameInterface(oldName, newName string) {
  332. sm.mu.Lock()
  333. defer sm.mu.Unlock()
  334. changed := false
  335. for _, srv := range sm.servers {
  336. if srv.Interface == oldName {
  337. srv.Interface = newName
  338. changed = true
  339. }
  340. }
  341. if changed {
  342. _ = sm.saveLocked()
  343. _ = sm.applyDnsmasqConfigLocked()
  344. log.Printf("[DHCPServer] Updated DHCP server interface binding: %s -> %s", oldName, newName)
  345. }
  346. }
  347. func (sm *ServerManager) ListServers() []DHCPServerInstance {
  348. sm.mu.RLock()
  349. defer sm.mu.RUnlock()
  350. activeLeases := sm.countActiveLeasesByServer()
  351. list := make([]DHCPServerInstance, 0, len(sm.servers))
  352. for _, s := range sm.servers {
  353. copyS := s
  354. copyS.Running = !s.Disabled && isInterfaceUp(s.Interface)
  355. copyS.ActiveLeases = activeLeases[s.Name]
  356. list = append(list, copyS)
  357. }
  358. sort.Slice(list, func(i, j int) bool {
  359. return list[i].Name < list[j].Name
  360. })
  361. return list
  362. }
  363. func (sm *ServerManager) GetServer(id string) (DHCPServerInstance, bool) {
  364. sm.mu.RLock()
  365. defer sm.mu.RUnlock()
  366. s, ok := sm.servers[id]
  367. if !ok {
  368. return DHCPServerInstance{}, false
  369. }
  370. s.Running = !s.Disabled && isInterfaceUp(s.Interface)
  371. return s, true
  372. }
  373. func (sm *ServerManager) SaveServer(inst DHCPServerInstance) (DHCPServerInstance, error) {
  374. sm.mu.Lock()
  375. defer sm.mu.Unlock()
  376. if inst.ID == "" {
  377. inst.ID = fmt.Sprintf("dhcp-%d", time.Now().UnixNano())
  378. }
  379. if inst.Name == "" {
  380. inst.Name = fmt.Sprintf("dhcp%d", len(sm.servers)+1)
  381. }
  382. if inst.LeaseTime == "" {
  383. inst.LeaseTime = "12h"
  384. }
  385. inst.Authoritative = true
  386. sm.servers[inst.ID] = inst
  387. if err := sm.saveLocked(); err != nil {
  388. return inst, err
  389. }
  390. _ = sm.applyDnsmasqConfigLocked()
  391. return inst, nil
  392. }
  393. func (sm *ServerManager) DeleteServer(id string) error {
  394. sm.mu.Lock()
  395. defer sm.mu.Unlock()
  396. delete(sm.servers, id)
  397. if err := sm.saveLocked(); err != nil {
  398. return err
  399. }
  400. return sm.applyDnsmasqConfigLocked()
  401. }
  402. func (sm *ServerManager) ToggleServerState(id string, disabled bool) error {
  403. sm.mu.Lock()
  404. defer sm.mu.Unlock()
  405. s, exists := sm.servers[id]
  406. if !exists {
  407. return fmt.Errorf("server not found: %s", id)
  408. }
  409. s.Disabled = disabled
  410. sm.servers[id] = s
  411. if err := sm.saveLocked(); err != nil {
  412. return err
  413. }
  414. return sm.applyDnsmasqConfigLocked()
  415. }
  416. // ======================== NETWORKS ========================
  417. func (sm *ServerManager) ListNetworks() []DHCPNetwork {
  418. sm.mu.RLock()
  419. defer sm.mu.RUnlock()
  420. list := make([]DHCPNetwork, 0, len(sm.networks))
  421. for _, n := range sm.networks {
  422. list = append(list, n)
  423. }
  424. sort.Slice(list, func(i, j int) bool {
  425. return list[i].Address < list[j].Address
  426. })
  427. return list
  428. }
  429. func (sm *ServerManager) SaveNetwork(netw DHCPNetwork) (DHCPNetwork, error) {
  430. sm.mu.Lock()
  431. defer sm.mu.Unlock()
  432. if netw.ID == "" {
  433. netw.ID = fmt.Sprintf("net-%d", time.Now().UnixNano())
  434. }
  435. if netw.Address == "" {
  436. return netw, fmt.Errorf("subnet address (CIDR) is required")
  437. }
  438. sm.networks[netw.ID] = netw
  439. if err := sm.saveLocked(); err != nil {
  440. return netw, err
  441. }
  442. _ = sm.applyDnsmasqConfigLocked()
  443. return netw, nil
  444. }
  445. func (sm *ServerManager) DeleteNetwork(id string) error {
  446. sm.mu.Lock()
  447. defer sm.mu.Unlock()
  448. delete(sm.networks, id)
  449. if err := sm.saveLocked(); err != nil {
  450. return err
  451. }
  452. return sm.applyDnsmasqConfigLocked()
  453. }
  454. // ======================== LEASES ========================
  455. func (sm *ServerManager) ListLeases() []DHCPLease {
  456. sm.mu.RLock()
  457. defer sm.mu.RUnlock()
  458. dynamicLeases := sm.readDnsmasqLeasesFile()
  459. staticMap := make(map[string]DHCPLease)
  460. for _, st := range sm.staticLeases {
  461. staticMap[strings.ToLower(st.MACAddress)] = st
  462. }
  463. result := make([]DHCPLease, 0)
  464. seenMACs := make(map[string]bool)
  465. // Combine dynamic leases
  466. for _, dl := range dynamicLeases {
  467. macLower := strings.ToLower(dl.MACAddress)
  468. seenMACs[macLower] = true
  469. if st, isStatic := staticMap[macLower]; isStatic {
  470. dl.Dynamic = false
  471. dl.Status = "static"
  472. dl.Comment = st.Comment
  473. dl.Disabled = st.Disabled
  474. dl.ExpiresIn = "never (static)"
  475. } else {
  476. dl.Dynamic = true
  477. dl.Status = "bound"
  478. }
  479. result = append(result, dl)
  480. }
  481. // Add static reservations that haven't acquired lease yet
  482. for macLower, st := range staticMap {
  483. if !seenMACs[macLower] {
  484. st.Dynamic = false
  485. st.Status = "waiting"
  486. st.ExpiresIn = "never (static)"
  487. result = append(result, st)
  488. }
  489. }
  490. sort.Slice(result, func(i, j int) bool {
  491. return compareIPs(net.ParseIP(result[i].Address), net.ParseIP(result[j].Address)) < 0
  492. })
  493. return result
  494. }
  495. func (sm *ServerManager) readDnsmasqLeasesFile() []DHCPLease {
  496. data, err := os.ReadFile(sm.leasesPath)
  497. if err != nil {
  498. return nil
  499. }
  500. lines := strings.Split(string(data), "\n")
  501. leases := make([]DHCPLease, 0, len(lines))
  502. now := time.Now()
  503. for _, line := range lines {
  504. line = strings.TrimSpace(line)
  505. if line == "" {
  506. continue
  507. }
  508. // Format: <timestamp> <mac> <ip> <hostname> <client-id>
  509. parts := strings.Fields(line)
  510. if len(parts) < 3 {
  511. continue
  512. }
  513. expTimestamp, _ := strconv.ParseInt(parts[0], 10, 64)
  514. mac := parts[1]
  515. ip := parts[2]
  516. hostname := "*"
  517. if len(parts) >= 4 && parts[3] != "*" {
  518. hostname = parts[3]
  519. }
  520. clientID := ""
  521. if len(parts) >= 5 && parts[4] != "*" {
  522. clientID = parts[4]
  523. }
  524. expiresAt := time.Unix(expTimestamp, 0)
  525. expiresIn := "expired"
  526. if expiresAt.After(now) {
  527. dur := expiresAt.Sub(now).Truncate(time.Minute)
  528. hours := int(dur.Hours())
  529. mins := int(dur.Minutes()) % 60
  530. expiresIn = fmt.Sprintf("%dh %dm", hours, mins)
  531. }
  532. // Determine server by matching IP against configured networks/pools
  533. serverName := sm.matchServerForIP(ip)
  534. leases = append(leases, DHCPLease{
  535. ID: fmt.Sprintf("lease-%s", strings.ReplaceAll(mac, ":", "")),
  536. Address: ip,
  537. MACAddress: mac,
  538. HostName: hostname,
  539. ClientID: clientID,
  540. Server: serverName,
  541. ExpiresAt: expiresAt,
  542. ExpiresIn: expiresIn,
  543. Dynamic: true,
  544. Status: "bound",
  545. })
  546. }
  547. return leases
  548. }
  549. func (sm *ServerManager) matchServerForIP(ipStr string) string {
  550. targetIP := net.ParseIP(ipStr)
  551. if targetIP == nil {
  552. return "dhcp1"
  553. }
  554. for _, srv := range sm.servers {
  555. startIP, endIP, _ := parseAddressPool(srv.AddressPool)
  556. if startIP != "" && endIP != "" {
  557. s := net.ParseIP(startIP)
  558. e := net.ParseIP(endIP)
  559. if s != nil && e != nil && compareIPs(targetIP, s) >= 0 && compareIPs(targetIP, e) <= 0 {
  560. return srv.Name
  561. }
  562. }
  563. }
  564. return "dhcp1"
  565. }
  566. func (sm *ServerManager) countActiveLeasesByServer() map[string]int {
  567. counts := make(map[string]int)
  568. leases := sm.readDnsmasqLeasesFile()
  569. for _, l := range leases {
  570. counts[l.Server]++
  571. }
  572. return counts
  573. }
  574. func (sm *ServerManager) SaveStaticLease(lease DHCPLease) (DHCPLease, error) {
  575. sm.mu.Lock()
  576. defer sm.mu.Unlock()
  577. if lease.ID == "" {
  578. lease.ID = fmt.Sprintf("static-%d", time.Now().UnixNano())
  579. }
  580. if lease.MACAddress == "" || lease.Address == "" {
  581. return lease, fmt.Errorf("both MAC address and IP address are required")
  582. }
  583. lease.MACAddress = strings.ToLower(lease.MACAddress)
  584. lease.Dynamic = false
  585. lease.Status = "static"
  586. sm.staticLeases[lease.ID] = lease
  587. if err := sm.saveLocked(); err != nil {
  588. return lease, err
  589. }
  590. _ = sm.applyDnsmasqConfigLocked()
  591. return lease, nil
  592. }
  593. func (sm *ServerManager) DeleteStaticLease(id string) error {
  594. sm.mu.Lock()
  595. defer sm.mu.Unlock()
  596. delete(sm.staticLeases, id)
  597. if err := sm.saveLocked(); err != nil {
  598. return err
  599. }
  600. return sm.applyDnsmasqConfigLocked()
  601. }
  602. func (sm *ServerManager) MakeStaticLease(mac, ip, hostname, comment string) (DHCPLease, error) {
  603. sm.mu.Lock()
  604. defer sm.mu.Unlock()
  605. macLower := strings.ToLower(strings.TrimSpace(mac))
  606. // Check if already in static leases
  607. for _, st := range sm.staticLeases {
  608. if strings.ToLower(st.MACAddress) == macLower {
  609. st.Address = ip
  610. if hostname != "" && hostname != "*" {
  611. st.HostName = hostname
  612. }
  613. if comment != "" {
  614. st.Comment = comment
  615. }
  616. sm.staticLeases[st.ID] = st
  617. _ = sm.saveLocked()
  618. _ = sm.applyDnsmasqConfigLocked()
  619. return st, nil
  620. }
  621. }
  622. id := fmt.Sprintf("static-%d", time.Now().UnixNano())
  623. st := DHCPLease{
  624. ID: id,
  625. MACAddress: macLower,
  626. Address: ip,
  627. HostName: hostname,
  628. Server: "all",
  629. Comment: comment,
  630. Dynamic: false,
  631. Status: "static",
  632. }
  633. sm.staticLeases[id] = st
  634. if err := sm.saveLocked(); err != nil {
  635. return st, err
  636. }
  637. _ = sm.applyDnsmasqConfigLocked()
  638. return st, nil
  639. }
  640. // ======================== OPTIONS ========================
  641. func (sm *ServerManager) ListOptions() []DHCPOption {
  642. sm.mu.RLock()
  643. defer sm.mu.RUnlock()
  644. list := make([]DHCPOption, 0, len(sm.options))
  645. for _, o := range sm.options {
  646. list = append(list, o)
  647. }
  648. sort.Slice(list, func(i, j int) bool {
  649. return list[i].Code < list[j].Code
  650. })
  651. return list
  652. }
  653. func (sm *ServerManager) SaveOption(opt DHCPOption) (DHCPOption, error) {
  654. sm.mu.Lock()
  655. defer sm.mu.Unlock()
  656. if opt.ID == "" {
  657. opt.ID = fmt.Sprintf("opt-%d", time.Now().UnixNano())
  658. }
  659. if opt.Name == "" || opt.Code <= 0 || opt.Code > 254 {
  660. return opt, fmt.Errorf("valid option name and code (1-254) are required")
  661. }
  662. sm.options[opt.ID] = opt
  663. if err := sm.saveLocked(); err != nil {
  664. return opt, err
  665. }
  666. _ = sm.applyDnsmasqConfigLocked()
  667. return opt, nil
  668. }
  669. func (sm *ServerManager) DeleteOption(id string) error {
  670. sm.mu.Lock()
  671. defer sm.mu.Unlock()
  672. delete(sm.options, id)
  673. if err := sm.saveLocked(); err != nil {
  674. return err
  675. }
  676. return sm.applyDnsmasqConfigLocked()
  677. }
  678. // ======================== SETUP WIZARD (MikroTik style) ========================
  679. func (sm *ServerManager) RunSetupWizard(req DHCPServerSetupRequest) error {
  680. sm.mu.Lock()
  681. defer sm.mu.Unlock()
  682. if req.Interface == "" {
  683. return fmt.Errorf("interface is required")
  684. }
  685. if req.AddressSpace == "" {
  686. req.AddressSpace = "192.168.88.0/24"
  687. }
  688. if req.Gateway == "" {
  689. req.Gateway = "192.168.88.1"
  690. }
  691. if req.AddressPool == "" {
  692. req.AddressPool = "192.168.88.10-192.168.88.254"
  693. }
  694. if len(req.DNSServers) == 0 {
  695. req.DNSServers = []string{req.Gateway, "1.1.1.1"}
  696. }
  697. if req.LeaseTime == "" {
  698. req.LeaseTime = "12h"
  699. }
  700. srvID := fmt.Sprintf("dhcp-%s", req.Interface)
  701. srv := DHCPServerInstance{
  702. ID: srvID,
  703. Name: fmt.Sprintf("dhcp-%s", req.Interface),
  704. Interface: req.Interface,
  705. Network: req.AddressSpace,
  706. AddressPool: req.AddressPool,
  707. LeaseTime: req.LeaseTime,
  708. Authoritative: true,
  709. Disabled: false,
  710. Comment: "Created by DHCP Setup Wizard",
  711. }
  712. sm.servers[srvID] = srv
  713. netID := fmt.Sprintf("net-%s", strings.ReplaceAll(req.AddressSpace, "/", "_"))
  714. netw := DHCPNetwork{
  715. ID: netID,
  716. Address: req.AddressSpace,
  717. Gateway: req.Gateway,
  718. DNSServers: req.DNSServers,
  719. Domain: req.Domain,
  720. NTPServers: req.NTPServers,
  721. NextServer: req.NextServer,
  722. BootFileName: req.BootFileName,
  723. Comment: fmt.Sprintf("Network for %s", req.Interface),
  724. }
  725. sm.networks[netID] = netw
  726. if err := sm.saveLocked(); err != nil {
  727. return err
  728. }
  729. return sm.applyDnsmasqConfigLocked()
  730. }
  731. // ======================== SUMMARY ========================
  732. func (sm *ServerManager) GetSummary() DHCPServerSummary {
  733. sm.mu.RLock()
  734. defer sm.mu.RUnlock()
  735. summary := DHCPServerSummary{
  736. TotalServers: len(sm.servers),
  737. TotalNetworks: len(sm.networks),
  738. StaticLeases: len(sm.staticLeases),
  739. TotalOptions: len(sm.options),
  740. }
  741. for _, s := range sm.servers {
  742. if !s.Disabled && isInterfaceUp(s.Interface) {
  743. summary.ActiveServers++
  744. }
  745. }
  746. leases := sm.readDnsmasqLeasesFile()
  747. summary.ActiveLeases = len(leases)
  748. return summary
  749. }
  750. // ======================== HELPERS ========================
  751. func sanitizeTag(name string) string {
  752. res := strings.Map(func(r rune) rune {
  753. if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_' {
  754. return r
  755. }
  756. return '_'
  757. }, name)
  758. return strings.ToLower(res)
  759. }
  760. func parseAddressPool(pool string) (startIP, endIP, netmask string) {
  761. pool = strings.TrimSpace(pool)
  762. parts := strings.Split(pool, "-")
  763. if len(parts) == 2 {
  764. return strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1]), ""
  765. }
  766. return "", "", ""
  767. }
  768. func isInterfaceUp(ifaceName string) bool {
  769. iface, err := net.InterfaceByName(ifaceName)
  770. if err != nil {
  771. return false
  772. }
  773. return (iface.Flags & net.FlagUp) != 0
  774. }
  775. func compareIPs(a, b net.IP) int {
  776. a4 := a.To4()
  777. b4 := b.To4()
  778. if a4 == nil || b4 == nil {
  779. return 0
  780. }
  781. for i := 0; i < 4; i++ {
  782. if a4[i] < b4[i] {
  783. return -1
  784. }
  785. if a4[i] > b4[i] {
  786. return 1
  787. }
  788. }
  789. return 0
  790. }