manager.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666
  1. package dhcp
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "net"
  6. "os"
  7. "os/exec"
  8. "regexp"
  9. "sort"
  10. "strconv"
  11. "strings"
  12. "sync"
  13. "syscall"
  14. "time"
  15. "github.com/vishvananda/netlink"
  16. )
  17. const (
  18. DHCPConfigFile = "/etc/proxmox-router/dhcp_clients.json"
  19. DHCPRunDir = "/run"
  20. DHCPLeaseDir = "/var/lib/dhcp"
  21. DHCPConfDir = "/etc/dhcp"
  22. )
  23. var (
  24. leaseBlockRegex = regexp.MustCompile(`(?s)lease\s+\{([^}]+)\}`)
  25. fixedAddrRegex = regexp.MustCompile(`fixed-address\s+([0-9.]+);`)
  26. subnetMaskRegex = regexp.MustCompile(`option subnet-mask\s+([0-9.]+);`)
  27. routersRegex = regexp.MustCompile(`option routers\s+([0-9.]+);`)
  28. dhcpServerRegex = regexp.MustCompile(`option dhcp-server-identifier\s+([0-9.]+);`)
  29. dnsServersRegex = regexp.MustCompile(`option domain-name-servers\s+([^;]+);`)
  30. domainNameRegex = regexp.MustCompile(`option domain-name\s+"([^"]+)";`)
  31. leaseTimeRegex = regexp.MustCompile(`option dhcp-lease-time\s+(\d+);`)
  32. expireTimeRegex = regexp.MustCompile(`expire\s+\d+\s+([0-9/]+ [0-9:]+);`)
  33. renewTimeRegex = regexp.MustCompile(`renew\s+\d+\s+([0-9/]+ [0-9:]+);`)
  34. validInterfaceRegex = regexp.MustCompile(`^[a-zA-Z0-9.:_-]+$`)
  35. )
  36. // DHCPConfigStore wraps the persisted configuration.
  37. type DHCPConfigStore struct {
  38. Clients []*DHCPClient `json:"clients"`
  39. }
  40. // Manager orchestrates all DHCP client instances.
  41. type Manager struct {
  42. mu sync.RWMutex
  43. storePath string
  44. clients map[string]*DHCPClient
  45. }
  46. // NewManager initializes the DHCP Manager and restores saved clients.
  47. func NewManager() *Manager {
  48. _ = os.MkdirAll("/etc/proxmox-router", 0755)
  49. _ = os.MkdirAll(DHCPLeaseDir, 0755)
  50. _ = os.MkdirAll(DHCPConfDir, 0755)
  51. m := &Manager{
  52. storePath: DHCPConfigFile,
  53. clients: make(map[string]*DHCPClient),
  54. }
  55. _ = m.Load()
  56. return m
  57. }
  58. // Load loads saved clients from disk and ensures enabled clients are started.
  59. func (m *Manager) Load() error {
  60. m.mu.Lock()
  61. defer m.mu.Unlock()
  62. data, err := os.ReadFile(m.storePath)
  63. if err != nil {
  64. if os.IsNotExist(err) {
  65. return nil
  66. }
  67. return err
  68. }
  69. var store DHCPConfigStore
  70. if err := json.Unmarshal(data, &store); err != nil {
  71. return err
  72. }
  73. for _, c := range store.Clients {
  74. if c.ID == "" {
  75. c.ID = "dhcp-" + c.Interface
  76. }
  77. m.clients[c.ID] = c
  78. }
  79. // Startup active clients and run periodic background sync
  80. go func() {
  81. time.Sleep(1 * time.Second)
  82. m.SyncAll()
  83. ticker := time.NewTicker(5 * time.Second)
  84. defer ticker.Stop()
  85. for range ticker.C {
  86. m.SyncAll()
  87. }
  88. }()
  89. return nil
  90. }
  91. // Save persists the current DHCP clients to disk.
  92. func (m *Manager) Save() error {
  93. store := DHCPConfigStore{
  94. Clients: make([]*DHCPClient, 0, len(m.clients)),
  95. }
  96. for _, c := range m.clients {
  97. store.Clients = append(store.Clients, c)
  98. }
  99. data, err := json.MarshalIndent(store, "", " ")
  100. if err != nil {
  101. return err
  102. }
  103. tmpFile := m.storePath + ".tmp"
  104. if err := os.WriteFile(tmpFile, data, 0644); err != nil {
  105. return err
  106. }
  107. return os.Rename(tmpFile, m.storePath)
  108. }
  109. // SyncAll refreshes status and launches any enabled clients that aren't running.
  110. func (m *Manager) SyncAll() {
  111. m.mu.Lock()
  112. defer m.mu.Unlock()
  113. for _, c := range m.clients {
  114. m.updateStatus(c)
  115. if c.Enabled && c.Status != "bound" && c.Status != "searching" {
  116. _ = m.startClient(c)
  117. m.updateStatus(c)
  118. }
  119. }
  120. }
  121. // ListClients returns all DHCP clients with real-time lease info.
  122. func (m *Manager) ListClients() []*DHCPClient {
  123. m.mu.Lock()
  124. defer m.mu.Unlock()
  125. res := make([]*DHCPClient, 0, len(m.clients))
  126. for _, c := range m.clients {
  127. m.updateStatus(c)
  128. res = append(res, c)
  129. }
  130. sort.Slice(res, func(i, j int) bool {
  131. return res[i].Interface < res[j].Interface
  132. })
  133. return res
  134. }
  135. // GetSummary returns summary KPIs.
  136. func (m *Manager) GetSummary() DHCPSummary {
  137. clients := m.ListClients()
  138. sum := DHCPSummary{
  139. TotalClients: len(clients),
  140. }
  141. for _, c := range clients {
  142. switch c.Status {
  143. case "bound":
  144. sum.BoundClients++
  145. case "searching":
  146. sum.Searching++
  147. case "disabled":
  148. sum.Disabled++
  149. }
  150. }
  151. return sum
  152. }
  153. // GetClient retrieves a single client by ID.
  154. func (m *Manager) GetClient(id string) (*DHCPClient, error) {
  155. m.mu.Lock()
  156. defer m.mu.Unlock()
  157. c, ok := m.clients[id]
  158. if !ok {
  159. return nil, fmt.Errorf("dhcp client %s not found", id)
  160. }
  161. m.updateStatus(c)
  162. return c, nil
  163. }
  164. // AddClient creates and optionally starts a DHCP client on an interface.
  165. func (m *Manager) AddClient(req CreateDHCPClientRequest) (*DHCPClient, error) {
  166. iface := strings.TrimSpace(req.Interface)
  167. if iface == "" || !validInterfaceRegex.MatchString(iface) {
  168. return nil, fmt.Errorf("invalid interface name")
  169. }
  170. // Verify interface exists on system
  171. if _, err := net.InterfaceByName(iface); err != nil {
  172. return nil, fmt.Errorf("interface %s does not exist on host", iface)
  173. }
  174. m.mu.Lock()
  175. defer m.mu.Unlock()
  176. id := "dhcp-" + iface
  177. if _, exists := m.clients[id]; exists {
  178. return nil, fmt.Errorf("dhcp client already exists for interface %s", iface)
  179. }
  180. dist := req.DefaultRouteDistance
  181. if dist <= 0 {
  182. dist = 1
  183. }
  184. c := &DHCPClient{
  185. ID: id,
  186. Interface: iface,
  187. Enabled: req.Enabled,
  188. AddDefaultRoute: req.AddDefaultRoute,
  189. DefaultRouteDistance: dist,
  190. UsePeerDNS: req.UsePeerDNS,
  191. UsePeerNTP: req.UsePeerNTP,
  192. Comment: req.Comment,
  193. Status: "stopped",
  194. DNSServers: []string{},
  195. }
  196. if c.Enabled {
  197. if err := m.startClient(c); err != nil {
  198. c.Status = "error"
  199. }
  200. } else {
  201. c.Status = "disabled"
  202. }
  203. m.updateStatus(c)
  204. m.clients[id] = c
  205. _ = m.Save()
  206. return c, nil
  207. }
  208. // UpdateClient modifies client settings and restarts if needed.
  209. func (m *Manager) UpdateClient(id string, req UpdateDHCPClientRequest) (*DHCPClient, error) {
  210. m.mu.Lock()
  211. defer m.mu.Unlock()
  212. c, ok := m.clients[id]
  213. if !ok {
  214. return nil, fmt.Errorf("dhcp client %s not found", id)
  215. }
  216. needsRestart := false
  217. if req.AddDefaultRoute != nil && *req.AddDefaultRoute != c.AddDefaultRoute {
  218. c.AddDefaultRoute = *req.AddDefaultRoute
  219. needsRestart = true
  220. }
  221. if req.DefaultRouteDistance != nil && *req.DefaultRouteDistance != c.DefaultRouteDistance {
  222. c.DefaultRouteDistance = *req.DefaultRouteDistance
  223. needsRestart = true
  224. }
  225. if req.UsePeerDNS != nil && *req.UsePeerDNS != c.UsePeerDNS {
  226. c.UsePeerDNS = *req.UsePeerDNS
  227. needsRestart = true
  228. }
  229. if req.UsePeerNTP != nil && *req.UsePeerNTP != c.UsePeerNTP {
  230. c.UsePeerNTP = *req.UsePeerNTP
  231. needsRestart = true
  232. }
  233. if req.Comment != nil {
  234. c.Comment = *req.Comment
  235. }
  236. if req.Enabled != nil {
  237. if *req.Enabled != c.Enabled {
  238. c.Enabled = *req.Enabled
  239. if c.Enabled {
  240. _ = m.startClient(c)
  241. } else {
  242. _ = m.stopClient(c)
  243. c.Status = "disabled"
  244. }
  245. needsRestart = false
  246. }
  247. }
  248. if needsRestart && c.Enabled {
  249. _ = m.stopClient(c)
  250. _ = m.startClient(c)
  251. }
  252. m.updateStatus(c)
  253. _ = m.Save()
  254. return c, nil
  255. }
  256. // ToggleState enables or disables a DHCP client.
  257. func (m *Manager) ToggleState(id string, enabled bool) error {
  258. m.mu.Lock()
  259. defer m.mu.Unlock()
  260. c, ok := m.clients[id]
  261. if !ok {
  262. return fmt.Errorf("dhcp client %s not found", id)
  263. }
  264. c.Enabled = enabled
  265. if enabled {
  266. if err := m.startClient(c); err != nil {
  267. c.Status = "error"
  268. }
  269. } else {
  270. _ = m.stopClient(c)
  271. c.Status = "disabled"
  272. }
  273. m.updateStatus(c)
  274. return m.Save()
  275. }
  276. // RenewLease forces dhclient to renew its lease.
  277. func (m *Manager) RenewLease(id string) error {
  278. m.mu.Lock()
  279. defer m.mu.Unlock()
  280. c, ok := m.clients[id]
  281. if !ok {
  282. return fmt.Errorf("dhcp client %s not found", id)
  283. }
  284. if !c.Enabled {
  285. return fmt.Errorf("cannot renew disabled dhcp client")
  286. }
  287. _ = m.stopClient(c)
  288. time.Sleep(200 * time.Millisecond)
  289. err := m.startClient(c)
  290. m.updateStatus(c)
  291. return err
  292. }
  293. // ReleaseLease sends DHCPRELEASE and removes the IP address.
  294. func (m *Manager) ReleaseLease(id string) error {
  295. m.mu.Lock()
  296. defer m.mu.Unlock()
  297. c, ok := m.clients[id]
  298. if !ok {
  299. return fmt.Errorf("dhcp client %s not found", id)
  300. }
  301. pidFile := fmt.Sprintf("%s/dhclient.%s.pid", DHCPRunDir, c.Interface)
  302. leaseFile := fmt.Sprintf("%s/dhclient.%s.leases", DHCPLeaseDir, c.Interface)
  303. cmd := exec.Command("dhclient", "-4", "-r", "-pf", pidFile, "-lf", leaseFile, c.Interface)
  304. _ = cmd.Run()
  305. _ = m.stopClient(c)
  306. c.Status = "stopped"
  307. c.IPAddress = ""
  308. c.Gateway = ""
  309. c.ExpiresIn = ""
  310. return m.Save()
  311. }
  312. // DeleteClient stops the client and removes it from configuration.
  313. func (m *Manager) DeleteClient(id string) error {
  314. m.mu.Lock()
  315. defer m.mu.Unlock()
  316. c, ok := m.clients[id]
  317. if !ok {
  318. return fmt.Errorf("dhcp client %s not found", id)
  319. }
  320. _ = m.stopClient(c)
  321. // Clean up conf & lease files
  322. _ = os.Remove(fmt.Sprintf("%s/dhclient.%s.pid", DHCPRunDir, c.Interface))
  323. _ = os.Remove(fmt.Sprintf("%s/dhclient-%s.conf", DHCPConfDir, c.Interface))
  324. delete(m.clients, id)
  325. return m.Save()
  326. }
  327. // RenameInterface updates DHCP client interface bindings when an interface is renamed.
  328. func (m *Manager) RenameInterface(oldName, newName string) {
  329. m.mu.Lock()
  330. defer m.mu.Unlock()
  331. for _, c := range m.clients {
  332. if c.Interface == oldName {
  333. wasRunning := c.Enabled
  334. if wasRunning {
  335. _ = m.stopClient(c)
  336. }
  337. c.Interface = newName
  338. if wasRunning {
  339. _ = m.startClient(c)
  340. }
  341. _ = m.Save()
  342. break
  343. }
  344. }
  345. }
  346. func (m *Manager) startClient(c *DHCPClient) error {
  347. pidFile := fmt.Sprintf("%s/dhclient.%s.pid", DHCPRunDir, c.Interface)
  348. leaseFile := fmt.Sprintf("%s/dhclient.%s.leases", DHCPLeaseDir, c.Interface)
  349. confFile := fmt.Sprintf("%s/dhclient-%s.conf", DHCPConfDir, c.Interface)
  350. // Generate custom dhclient config
  351. var sb strings.Builder
  352. sb.WriteString("# RouterOS-style dhclient configuration\n")
  353. sb.WriteString("send host-name = gethostname();\n")
  354. sb.WriteString("request subnet-mask, broadcast-address, time-offset")
  355. if c.AddDefaultRoute {
  356. sb.WriteString(", routers")
  357. }
  358. if c.UsePeerDNS {
  359. sb.WriteString(", domain-name, domain-name-servers, domain-search, host-name")
  360. }
  361. if c.UsePeerNTP {
  362. sb.WriteString(", ntp-servers")
  363. }
  364. sb.WriteString(";\n")
  365. _ = os.WriteFile(confFile, []byte(sb.String()), 0644)
  366. // Ensure enter & exit hooks prevent dhclient from overriding static routes
  367. _ = os.MkdirAll("/etc/dhcp/dhclient-enter-hooks.d", 0755)
  368. _ = os.MkdirAll("/etc/dhcp/dhclient-exit-hooks.d", 0755)
  369. if !c.AddDefaultRoute {
  370. enterHook := fmt.Sprintf("#!/bin/sh\ncase \"$interface\" in\n %s)\n unset new_routers\n ;;\nesac\n", c.Interface)
  371. _ = os.WriteFile("/etc/dhcp/dhclient-enter-hooks.d/no-default-route", []byte(enterHook), 0755)
  372. exitHook := fmt.Sprintf("#!/bin/sh\ncase \"$interface\" in\n %s)\n ip -4 route del default dev %s metric 0 >/dev/null 2>&1 || true\n ;;\nesac\n", c.Interface, c.Interface)
  373. _ = os.WriteFile("/etc/dhcp/dhclient-exit-hooks.d/no-default-route", []byte(exitHook), 0755)
  374. }
  375. // Dynamic host route hook for DHCP server reachability (Distance 0, Dynamic)
  376. serverRouteHook := fmt.Sprintf(`#!/bin/sh
  377. case "$reason" in
  378. BOUND|RENEW|REBIND|REBOOT)
  379. if [ -n "$new_dhcp_server_identifier" ] && [ -n "$new_routers" ]; then
  380. gw="${new_routers%%%% *}"
  381. ip -4 route replace "${new_dhcp_server_identifier}/32" via "$gw" dev "$interface" proto dhcp metric 0 onlink >/dev/null 2>&1 || true
  382. fi
  383. ;;
  384. EXPIRE|FAIL|RELEASE|STOP)
  385. if [ -n "$old_dhcp_server_identifier" ]; then
  386. ip -4 route del "${old_dhcp_server_identifier}/32" dev "$interface" proto dhcp >/dev/null 2>&1 || true
  387. fi
  388. ;;
  389. esac
  390. `)
  391. _ = os.WriteFile("/etc/dhcp/dhclient-exit-hooks.d/dhcp-server-route", []byte(serverRouteHook), 0755)
  392. // If already running, kill first
  393. _ = m.stopClient(c)
  394. // Launch dhclient in background
  395. cmd := exec.Command("dhclient", "-4", "-nw", "-pf", pidFile, "-lf", leaseFile, "-cf", confFile, c.Interface)
  396. if err := cmd.Run(); err != nil {
  397. return fmt.Errorf("failed to start dhclient: %w", err)
  398. }
  399. c.Status = "searching"
  400. return nil
  401. }
  402. func (m *Manager) stopClient(c *DHCPClient) error {
  403. m.removeDHCPServerRoute(c)
  404. pidFile := fmt.Sprintf("%s/dhclient.%s.pid", DHCPRunDir, c.Interface)
  405. // Check PID file
  406. if pidBytes, err := os.ReadFile(pidFile); err == nil {
  407. if pid, err := strconv.Atoi(strings.TrimSpace(string(pidBytes))); err == nil && pid > 0 {
  408. _ = syscall.Kill(pid, syscall.SIGTERM)
  409. time.Sleep(100 * time.Millisecond)
  410. if err := syscall.Kill(pid, 0); err == nil {
  411. _ = syscall.Kill(pid, syscall.SIGKILL)
  412. }
  413. }
  414. }
  415. _ = os.Remove(pidFile)
  416. c.PID = 0
  417. return nil
  418. }
  419. func (m *Manager) syncDHCPServerRoute(c *DHCPClient) {
  420. if c == nil || c.Interface == "" {
  421. return
  422. }
  423. // Only maintain dynamic server route when client is bound
  424. if c.Status != "bound" || c.DHCPServer == "" || c.Gateway == "" {
  425. return
  426. }
  427. serverIP := net.ParseIP(c.DHCPServer)
  428. if serverIP == nil {
  429. return
  430. }
  431. // If DHCP server is already part of the directly assigned subnet, no explicit host route is required
  432. if c.IPAddress != "" {
  433. if _, ipNet, err := net.ParseCIDR(c.IPAddress); err == nil && ipNet.Contains(serverIP) {
  434. return
  435. }
  436. }
  437. // Install dynamic host route (Distance 0, Dynamic proto dhcp, onlink)
  438. cmd := exec.Command("ip", "route", "replace", c.DHCPServer+"/32", "via", c.Gateway, "dev", c.Interface, "proto", "dhcp", "metric", "0", "onlink")
  439. _ = cmd.Run()
  440. }
  441. func (m *Manager) removeDHCPServerRoute(c *DHCPClient) {
  442. if c == nil || c.DHCPServer == "" || c.Interface == "" {
  443. return
  444. }
  445. cmd := exec.Command("ip", "route", "del", c.DHCPServer+"/32", "dev", c.Interface, "proto", "dhcp")
  446. _ = cmd.Run()
  447. }
  448. func (m *Manager) updateStatus(c *DHCPClient) {
  449. pidFile := fmt.Sprintf("%s/dhclient.%s.pid", DHCPRunDir, c.Interface)
  450. leaseFile := fmt.Sprintf("%s/dhclient.%s.leases", DHCPLeaseDir, c.Interface)
  451. // Check if process is alive
  452. isRunning := false
  453. if pidBytes, err := os.ReadFile(pidFile); err == nil {
  454. if pid, err := strconv.Atoi(strings.TrimSpace(string(pidBytes))); err == nil && pid > 0 {
  455. if err := syscall.Kill(pid, 0); err == nil {
  456. isRunning = true
  457. c.PID = pid
  458. }
  459. }
  460. }
  461. if !isRunning {
  462. c.PID = 0
  463. if !c.Enabled {
  464. c.Status = "disabled"
  465. } else {
  466. c.Status = "stopped"
  467. }
  468. } else {
  469. c.Status = "searching"
  470. }
  471. // Read interface IPv4 addresses via netlink
  472. if link, err := netlink.LinkByName(c.Interface); err == nil {
  473. if addrs, err := netlink.AddrList(link, netlink.FAMILY_V4); err == nil && len(addrs) > 0 {
  474. for _, a := range addrs {
  475. ip := a.IPNet.IP.String()
  476. if !strings.HasPrefix(ip, "127.") && !strings.HasPrefix(ip, "169.254.") {
  477. c.IPAddress = a.IPNet.String()
  478. if isRunning {
  479. c.Status = "bound"
  480. }
  481. break
  482. }
  483. }
  484. }
  485. }
  486. // Parse lease file for rich metadata (gateway, dns, lease times)
  487. m.parseLeaseFile(leaseFile, c)
  488. // Dynamically maintain DHCP Server reachability route (Distance 0, Dynamic)
  489. m.syncDHCPServerRoute(c)
  490. }
  491. func (m *Manager) parseLeaseFile(leaseFile string, c *DHCPClient) {
  492. data, err := os.ReadFile(leaseFile)
  493. if err != nil {
  494. return
  495. }
  496. // Find the last lease block in the file
  497. matches := leaseBlockRegex.FindAllStringSubmatch(string(data), -1)
  498. if len(matches) == 0 {
  499. return
  500. }
  501. lastLease := matches[len(matches)-1][1]
  502. if m := fixedAddrRegex.FindStringSubmatch(lastLease); len(m) > 1 {
  503. ip := m[1]
  504. mask := "24"
  505. if sm := subnetMaskRegex.FindStringSubmatch(lastLease); len(sm) > 1 {
  506. mask = subnetToCIDR(sm[1])
  507. }
  508. if c.IPAddress == "" {
  509. c.IPAddress = fmt.Sprintf("%s/%s", ip, mask)
  510. }
  511. }
  512. if m := routersRegex.FindStringSubmatch(lastLease); len(m) > 1 {
  513. c.Gateway = m[1]
  514. }
  515. if m := dhcpServerRegex.FindStringSubmatch(lastLease); len(m) > 1 {
  516. c.DHCPServer = m[1]
  517. }
  518. if m := dnsServersRegex.FindStringSubmatch(lastLease); len(m) > 1 {
  519. raw := strings.Split(m[1], ",")
  520. dns := make([]string, 0, len(raw))
  521. for _, s := range raw {
  522. trimmed := strings.TrimSpace(s)
  523. if trimmed != "" {
  524. dns = append(dns, trimmed)
  525. }
  526. }
  527. c.DNSServers = dns
  528. }
  529. if m := domainNameRegex.FindStringSubmatch(lastLease); len(m) > 1 {
  530. c.Domain = m[1]
  531. }
  532. if m := leaseTimeRegex.FindStringSubmatch(lastLease); len(m) > 1 {
  533. c.LeaseTime, _ = strconv.Atoi(m[1])
  534. }
  535. if m := expireTimeRegex.FindStringSubmatch(lastLease); len(m) > 1 {
  536. // e.g. "2026/10/26 18:30:00"
  537. if t, err := time.Parse("2006/01/02 15:04:05", m[1]); err == nil {
  538. c.ExpiresAt = t
  539. rem := time.Until(t)
  540. if rem > 0 {
  541. hours := int(rem.Hours())
  542. mins := int(rem.Minutes()) % 60
  543. if hours > 24 {
  544. c.ExpiresIn = fmt.Sprintf("%dd %dh", hours/24, hours%24)
  545. } else if hours > 0 {
  546. c.ExpiresIn = fmt.Sprintf("%dh %dm", hours, mins)
  547. } else {
  548. c.ExpiresIn = fmt.Sprintf("%dm %ds", mins, int(rem.Seconds())%60)
  549. }
  550. } else {
  551. c.ExpiresIn = "Expired"
  552. }
  553. }
  554. }
  555. }
  556. func subnetToCIDR(mask string) string {
  557. parts := strings.Split(mask, ".")
  558. if len(parts) != 4 {
  559. return "24"
  560. }
  561. var count int
  562. for _, p := range parts {
  563. n, _ := strconv.Atoi(p)
  564. for n > 0 {
  565. count += n & 1
  566. n >>= 1
  567. }
  568. }
  569. return strconv.Itoa(count)
  570. }