manager.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689
  1. package network
  2. import (
  3. "fmt"
  4. "log"
  5. "net"
  6. "os"
  7. "regexp"
  8. "strconv"
  9. "strings"
  10. "sync"
  11. "time"
  12. "github.com/vishvananda/netlink"
  13. )
  14. type prevStat struct {
  15. rxBytes uint64
  16. txBytes uint64
  17. time time.Time
  18. }
  19. type rawNetDevStat struct {
  20. rxBytes uint64
  21. rxPackets uint64
  22. rxErrors uint64
  23. rxDropped uint64
  24. txBytes uint64
  25. txPackets uint64
  26. txErrors uint64
  27. txDropped uint64
  28. time time.Time
  29. }
  30. // Manager handles all Linux network interactions via Netlink.
  31. type Manager struct {
  32. mu sync.Mutex
  33. metadata *MetadataManager
  34. prevStats map[string]prevStat
  35. snapMu sync.RWMutex
  36. latestSnapshot *TrafficSnapshot
  37. subMu sync.Mutex
  38. subscribers map[chan *TrafficSnapshot]bool
  39. }
  40. func NewManager(mm *MetadataManager) *Manager {
  41. mgr := &Manager{
  42. metadata: mm,
  43. prevStats: make(map[string]prevStat),
  44. subscribers: make(map[chan *TrafficSnapshot]bool),
  45. }
  46. mgr.startIPSyncWorker()
  47. mgr.startTrafficSampler()
  48. return mgr
  49. }
  50. // syncPersistedIPs ensures that all IP addresses saved in metadata are applied to the kernel interfaces
  51. func (m *Manager) syncPersistedIPs() {
  52. if m.metadata == nil {
  53. return
  54. }
  55. configured := m.metadata.GetAllWithIPs()
  56. for name, ips := range configured {
  57. link, err := netlink.LinkByName(name)
  58. if err != nil {
  59. continue // Interface might not be attached/created yet by Proxmox host
  60. }
  61. // Ensure link is UP if it has configured IPs
  62. if link.Attrs().Flags&net.FlagUp == 0 {
  63. _ = netlink.LinkSetUp(link)
  64. }
  65. currentAddrs, err := netlink.AddrList(link, netlink.FAMILY_V4)
  66. if err != nil {
  67. continue
  68. }
  69. currentMap := make(map[string]bool)
  70. for _, a := range currentAddrs {
  71. currentMap[a.IPNet.String()] = true
  72. }
  73. for _, cidr := range ips {
  74. if !currentMap[cidr] {
  75. addr, err := netlink.ParseAddr(cidr)
  76. if err == nil {
  77. if err := netlink.AddrAdd(link, addr); err == nil {
  78. log.Printf("[NetSync] Restored IP %s on interface %s", cidr, name)
  79. }
  80. }
  81. }
  82. }
  83. }
  84. }
  85. func (m *Manager) startIPSyncWorker() {
  86. go func() {
  87. m.syncPersistedIPs()
  88. ticker := time.NewTicker(3 * time.Second)
  89. defer ticker.Stop()
  90. for range ticker.C {
  91. m.syncPersistedIPs()
  92. }
  93. }()
  94. }
  95. // IsKernelDummyInterface returns true if link is an internal kernel fallback/dummy device (e.g. gre0, gretap0, erspan0)
  96. func IsKernelDummyInterface(name string) bool {
  97. if strings.HasPrefix(name, "ep_") {
  98. return true
  99. }
  100. switch name {
  101. case "gre0", "gretap0", "erspan0", "sit0", "tunl0", "ip6tnl0", "ip6gre0", "ip6erspan0":
  102. return true
  103. default:
  104. return false
  105. }
  106. }
  107. // ListInterfaces returns all network links on the system.
  108. func (m *Manager) ListInterfaces() ([]InterfaceInfo, error) {
  109. links, err := netlink.LinkList()
  110. if err != nil {
  111. return nil, fmt.Errorf("failed to list links: %w", err)
  112. }
  113. linkMap := make(map[int]netlink.Link)
  114. for _, l := range links {
  115. linkMap[l.Attrs().Index] = l
  116. }
  117. latestSnap := m.GetLatestTrafficSnapshot()
  118. var latestRates map[string]InterfaceLiveRate
  119. if latestSnap != nil {
  120. latestRates = latestSnap.Interfaces
  121. }
  122. m.mu.Lock()
  123. defer m.mu.Unlock()
  124. result := make([]InterfaceInfo, 0)
  125. for _, link := range links {
  126. attrs := link.Attrs()
  127. if IsKernelDummyInterface(attrs.Name) {
  128. continue
  129. }
  130. info := InterfaceInfo{
  131. Index: attrs.Index,
  132. Name: attrs.Name,
  133. Type: link.Type(),
  134. MTU: attrs.MTU,
  135. MAC: attrs.HardwareAddr.String(),
  136. OperState: strings.ToUpper(attrs.OperState.String()),
  137. IPv4: make([]string, 0),
  138. IPv6: make([]string, 0),
  139. Flags: make([]string, 0),
  140. }
  141. // Flags
  142. if attrs.Flags&net.FlagUp != 0 {
  143. info.State = "UP"
  144. } else {
  145. info.State = "DOWN"
  146. }
  147. for _, f := range strings.Split(attrs.Flags.String(), "|") {
  148. if f != "" {
  149. info.Flags = append(info.Flags, f)
  150. }
  151. }
  152. // Check if interface is a VLAN
  153. if vlan, ok := link.(*netlink.Vlan); ok {
  154. info.IsVLAN = true
  155. info.VlanID = vlan.VlanId
  156. info.ParentIndex = vlan.ParentIndex
  157. if parent, exists := linkMap[vlan.ParentIndex]; exists {
  158. info.ParentName = parent.Attrs().Name
  159. }
  160. } else if strings.Contains(attrs.Name, ".") {
  161. // Sub-interface naming fallback (e.g. eth0.10)
  162. parts := strings.Split(attrs.Name, ".")
  163. if len(parts) == 2 {
  164. var id int
  165. if n, _ := fmt.Sscanf(parts[1], "%d", &id); n == 1 {
  166. info.IsVLAN = true
  167. info.VlanID = id
  168. info.ParentName = parts[0]
  169. }
  170. }
  171. }
  172. // Addresses
  173. addrs, err := netlink.AddrList(link, netlink.FAMILY_ALL)
  174. if err == nil {
  175. for _, a := range addrs {
  176. if a.IP.To4() != nil {
  177. info.IPv4 = append(info.IPv4, a.IPNet.String())
  178. } else {
  179. info.IPv6 = append(info.IPv6, a.IPNet.String())
  180. }
  181. }
  182. }
  183. // Metadata
  184. meta := m.metadata.Get(attrs.Name)
  185. info.Role = meta.Role
  186. info.Description = meta.Description
  187. if info.Role == "" {
  188. if attrs.Name == "lo" {
  189. info.Role = "LOOPBACK"
  190. } else if info.IsVLAN {
  191. info.Role = "VLAN"
  192. } else if strings.HasPrefix(attrs.Name, "eth0") || strings.HasPrefix(attrs.Name, "ens") {
  193. info.Role = "WAN/LAN"
  194. }
  195. }
  196. // Statistics & throughput calculation
  197. if attrs.Statistics != nil {
  198. info.Stats = NetStats{
  199. RxBytes: attrs.Statistics.RxBytes,
  200. TxBytes: attrs.Statistics.TxBytes,
  201. RxPackets: attrs.Statistics.RxPackets,
  202. TxPackets: attrs.Statistics.TxPackets,
  203. RxErrors: attrs.Statistics.RxErrors,
  204. TxErrors: attrs.Statistics.TxErrors,
  205. RxDropped: attrs.Statistics.RxDropped,
  206. TxDropped: attrs.Statistics.TxDropped,
  207. }
  208. }
  209. if lr, ok := latestRates[attrs.Name]; ok {
  210. info.Rate = TrafficRate{
  211. RxBytesPerSec: lr.RxBytesPerSec,
  212. TxBytesPerSec: lr.TxBytesPerSec,
  213. RxHuman: lr.RxHuman,
  214. TxHuman: lr.TxHuman,
  215. }
  216. if attrs.Statistics == nil || attrs.Statistics.RxBytes == 0 {
  217. info.Stats = NetStats{
  218. RxBytes: lr.RxBytes,
  219. TxBytes: lr.TxBytes,
  220. RxPackets: lr.RxPackets,
  221. TxPackets: lr.TxPackets,
  222. RxErrors: lr.RxErrors,
  223. TxErrors: lr.TxErrors,
  224. RxDropped: lr.RxDropped,
  225. TxDropped: lr.TxDropped,
  226. }
  227. }
  228. }
  229. result = append(result, info)
  230. }
  231. return result, nil
  232. }
  233. // GetInterface returns information for a specific interface.
  234. func (m *Manager) GetInterface(name string) (*InterfaceInfo, error) {
  235. interfaces, err := m.ListInterfaces()
  236. if err != nil {
  237. return nil, err
  238. }
  239. for _, iface := range interfaces {
  240. if iface.Name == name {
  241. return &iface, nil
  242. }
  243. }
  244. return nil, fmt.Errorf("interface %s not found", name)
  245. }
  246. // SetInterfaceState sets the administrative status (UP or DOWN).
  247. func (m *Manager) SetInterfaceState(name string, up bool) error {
  248. link, err := netlink.LinkByName(name)
  249. if err != nil {
  250. return fmt.Errorf("link not found: %w", err)
  251. }
  252. if up {
  253. return netlink.LinkSetUp(link)
  254. }
  255. return netlink.LinkSetDown(link)
  256. }
  257. // SetInterfaceMTU updates the MTU for the specified interface.
  258. func (m *Manager) SetInterfaceMTU(name string, mtu int) error {
  259. if mtu < 68 || mtu > 65535 {
  260. return fmt.Errorf("invalid MTU size: %d", mtu)
  261. }
  262. link, err := netlink.LinkByName(name)
  263. if err != nil {
  264. return fmt.Errorf("link not found: %w", err)
  265. }
  266. return netlink.LinkSetMTU(link, mtu)
  267. }
  268. var ifaceNameRegex = regexp.MustCompile(`^[a-zA-Z0-9_][a-zA-Z0-9_.-]*$`)
  269. // RenameInterface safely renames an existing network device using Netlink.
  270. func (m *Manager) RenameInterface(oldName, newName string) (*InterfaceInfo, error) {
  271. oldName = strings.TrimSpace(oldName)
  272. newName = strings.TrimSpace(newName)
  273. if oldName == "" || newName == "" {
  274. return nil, fmt.Errorf("interface name cannot be empty")
  275. }
  276. if oldName == newName {
  277. return m.GetInterface(newName)
  278. }
  279. if oldName == "lo" || newName == "lo" {
  280. return nil, fmt.Errorf("cannot rename the loopback interface")
  281. }
  282. if len(newName) > 15 {
  283. return nil, fmt.Errorf("interface name %q exceeds Linux limit of 15 characters", newName)
  284. }
  285. if !ifaceNameRegex.MatchString(newName) {
  286. return nil, fmt.Errorf("invalid interface name %q: must contain only letters, numbers, underscores, dots, or hyphens", newName)
  287. }
  288. link, err := netlink.LinkByName(oldName)
  289. if err != nil {
  290. return nil, fmt.Errorf("interface %q not found: %w", oldName, err)
  291. }
  292. if _, err := netlink.LinkByName(newName); err == nil {
  293. return nil, fmt.Errorf("interface %q already exists", newName)
  294. }
  295. wasUp := (link.Attrs().Flags & net.FlagUp) != 0
  296. // Linux kernel requires interface to be DOWN to change dev name
  297. if wasUp {
  298. if err := netlink.LinkSetDown(link); err != nil {
  299. return nil, fmt.Errorf("failed to bring interface %q down for renaming: %w", oldName, err)
  300. }
  301. }
  302. if err := netlink.LinkSetName(link, newName); err != nil {
  303. if wasUp {
  304. _ = netlink.LinkSetUp(link)
  305. }
  306. return nil, fmt.Errorf("failed to rename interface %q to %q: %w", oldName, newName, err)
  307. }
  308. // Restore UP state if it was originally UP
  309. if wasUp {
  310. if newLink, err := netlink.LinkByName(newName); err == nil {
  311. _ = netlink.LinkSetUp(newLink)
  312. }
  313. }
  314. // Migrate metadata
  315. _ = m.metadata.Rename(oldName, newName)
  316. // Update traffic rate history
  317. m.mu.Lock()
  318. if prev, ok := m.prevStats[oldName]; ok {
  319. delete(m.prevStats, oldName)
  320. m.prevStats[newName] = prev
  321. }
  322. m.mu.Unlock()
  323. return m.GetInterface(newName)
  324. }
  325. // AddInterfaceIP adds an IP address with subnet CIDR to an interface and persists it.
  326. func (m *Manager) AddInterfaceIP(name string, cidr string) error {
  327. link, err := netlink.LinkByName(name)
  328. if err != nil {
  329. return fmt.Errorf("link not found: %w", err)
  330. }
  331. addr, err := netlink.ParseAddr(cidr)
  332. if err != nil {
  333. return fmt.Errorf("invalid IP/CIDR %q: %w", cidr, err)
  334. }
  335. if err := netlink.AddrAdd(link, addr); err != nil && !strings.Contains(err.Error(), "file exists") {
  336. return err
  337. }
  338. if m.metadata != nil {
  339. _ = m.metadata.AddIP(name, cidr)
  340. }
  341. return nil
  342. }
  343. // DeleteInterfaceIP removes an IP address from an interface and updates persistence.
  344. func (m *Manager) DeleteInterfaceIP(name string, cidr string) error {
  345. link, err := netlink.LinkByName(name)
  346. if err != nil {
  347. return fmt.Errorf("link not found: %w", err)
  348. }
  349. addr, err := netlink.ParseAddr(cidr)
  350. if err != nil {
  351. return fmt.Errorf("invalid IP/CIDR %q: %w", cidr, err)
  352. }
  353. if err := netlink.AddrDel(link, addr); err != nil && !strings.Contains(err.Error(), "cannot assign requested address") {
  354. return err
  355. }
  356. if m.metadata != nil {
  357. _ = m.metadata.RemoveIP(name, cidr)
  358. }
  359. return nil
  360. }
  361. // CreateVLAN creates a new 802.1Q VLAN sub-interface.
  362. func (m *Manager) CreateVLAN(req CreateVLANRequest) (*InterfaceInfo, error) {
  363. if req.VlanID < 1 || req.VlanID > 4094 {
  364. return nil, fmt.Errorf("VLAN ID must be between 1 and 4094, got %d", req.VlanID)
  365. }
  366. parent, err := netlink.LinkByName(req.ParentName)
  367. if err != nil {
  368. return nil, fmt.Errorf("parent interface %q not found: %w", req.ParentName, err)
  369. }
  370. // Validate parent interface supports 802.1Q VLAN sub-interfaces
  371. parentType := strings.ToLower(parent.Type())
  372. parentName := strings.ToLower(parent.Attrs().Name)
  373. if parentType == "wireguard" || strings.HasPrefix(parentName, "wg") ||
  374. parentType == "loopback" || parentName == "lo" ||
  375. parentType == "ppp" || strings.HasPrefix(parentName, "ppp") ||
  376. parentType == "dummy" || parentType == "tun" || parentType == "tap" || parentType == "gre" ||
  377. parent.Attrs().Flags&net.FlagLoopback != 0 || parent.Attrs().Flags&net.FlagPointToPoint != 0 {
  378. return nil, fmt.Errorf("interface %q does not support 802.1Q VLAN sub-interfaces", req.ParentName)
  379. }
  380. vlanName := req.Name
  381. if vlanName == "" {
  382. vlanName = fmt.Sprintf("%s.%d", req.ParentName, req.VlanID)
  383. }
  384. // Check if already exists
  385. if _, err := netlink.LinkByName(vlanName); err == nil {
  386. return nil, fmt.Errorf("interface %q already exists", vlanName)
  387. }
  388. mtu := req.MTU
  389. if mtu <= 0 {
  390. mtu = parent.Attrs().MTU
  391. if mtu == 0 {
  392. mtu = 1500
  393. }
  394. }
  395. vlanLink := &netlink.Vlan{
  396. LinkAttrs: netlink.LinkAttrs{
  397. Name: vlanName,
  398. ParentIndex: parent.Attrs().Index,
  399. MTU: mtu,
  400. },
  401. VlanId: req.VlanID,
  402. }
  403. if err := netlink.LinkAdd(vlanLink); err != nil {
  404. return nil, fmt.Errorf("failed to create VLAN link: %w", err)
  405. }
  406. // Bring UP
  407. if req.AutoUp {
  408. if err := netlink.LinkSetUp(vlanLink); err != nil {
  409. _ = netlink.LinkDel(vlanLink)
  410. return nil, fmt.Errorf("failed to set VLAN link UP: %w", err)
  411. }
  412. }
  413. // Assign IP if provided
  414. if strings.TrimSpace(req.IPAddress) != "" {
  415. addr, err := netlink.ParseAddr(strings.TrimSpace(req.IPAddress))
  416. if err != nil {
  417. _ = netlink.LinkDel(vlanLink)
  418. return nil, fmt.Errorf("invalid IP address %q: %w", req.IPAddress, err)
  419. }
  420. if err := netlink.AddrAdd(vlanLink, addr); err != nil {
  421. _ = netlink.LinkDel(vlanLink)
  422. return nil, fmt.Errorf("failed to assign IP to VLAN: %w", err)
  423. }
  424. }
  425. // Store metadata
  426. role := req.Role
  427. if role == "" {
  428. role = "VLAN"
  429. }
  430. _ = m.metadata.Set(vlanName, role, req.Description)
  431. return m.GetInterface(vlanName)
  432. }
  433. // DeleteVLAN removes a VLAN interface.
  434. func (m *Manager) DeleteVLAN(name string) error {
  435. link, err := netlink.LinkByName(name)
  436. if err != nil {
  437. return fmt.Errorf("interface %s not found: %w", name, err)
  438. }
  439. if _, ok := link.(*netlink.Vlan); !ok && !strings.Contains(name, ".") {
  440. return fmt.Errorf("interface %s is not recognized as a VLAN interface", name)
  441. }
  442. if err := netlink.LinkDel(link); err != nil {
  443. return fmt.Errorf("failed to delete link: %w", err)
  444. }
  445. _ = m.metadata.Delete(name)
  446. return nil
  447. }
  448. // ListVLANs returns only the VLAN interfaces.
  449. func (m *Manager) ListVLANs() ([]InterfaceInfo, error) {
  450. all, err := m.ListInterfaces()
  451. if err != nil {
  452. return nil, err
  453. }
  454. vlans := make([]InterfaceInfo, 0)
  455. for _, iface := range all {
  456. if iface.IsVLAN {
  457. vlans = append(vlans, iface)
  458. }
  459. }
  460. return vlans, nil
  461. }
  462. func formatBytes(b float64) string {
  463. const unit = 1024
  464. if b < unit {
  465. return fmt.Sprintf("%.0f B", b)
  466. }
  467. div, exp := int64(unit), 0
  468. for n := b / unit; n >= unit; n /= unit {
  469. div *= unit
  470. exp++
  471. }
  472. return fmt.Sprintf("%.1f %cB", b/float64(div), "KMGTPE"[exp])
  473. }
  474. func readProcNetDev() (map[string]rawNetDevStat, error) {
  475. data, err := os.ReadFile("/proc/net/dev")
  476. if err != nil {
  477. return nil, err
  478. }
  479. lines := strings.Split(string(data), "\n")
  480. result := make(map[string]rawNetDevStat)
  481. now := time.Now()
  482. for _, line := range lines {
  483. colonIdx := strings.Index(line, ":")
  484. if colonIdx == -1 {
  485. continue
  486. }
  487. ifaceName := strings.TrimSpace(line[:colonIdx])
  488. fields := strings.Fields(line[colonIdx+1:])
  489. if len(fields) < 16 {
  490. continue
  491. }
  492. rxBytes, _ := strconv.ParseUint(fields[0], 10, 64)
  493. rxPackets, _ := strconv.ParseUint(fields[1], 10, 64)
  494. rxErrors, _ := strconv.ParseUint(fields[2], 10, 64)
  495. rxDropped, _ := strconv.ParseUint(fields[3], 10, 64)
  496. txBytes, _ := strconv.ParseUint(fields[8], 10, 64)
  497. txPackets, _ := strconv.ParseUint(fields[9], 10, 64)
  498. txErrors, _ := strconv.ParseUint(fields[10], 10, 64)
  499. txDropped, _ := strconv.ParseUint(fields[11], 10, 64)
  500. result[ifaceName] = rawNetDevStat{
  501. rxBytes: rxBytes,
  502. rxPackets: rxPackets,
  503. rxErrors: rxErrors,
  504. rxDropped: rxDropped,
  505. txBytes: txBytes,
  506. txPackets: txPackets,
  507. txErrors: txErrors,
  508. txDropped: txDropped,
  509. time: now,
  510. }
  511. }
  512. return result, nil
  513. }
  514. func (m *Manager) startTrafficSampler() {
  515. go func() {
  516. // Initial sample
  517. prev, _ := readProcNetDev()
  518. ticker := time.NewTicker(1 * time.Second)
  519. defer ticker.Stop()
  520. for range ticker.C {
  521. curr, err := readProcNetDev()
  522. if err != nil {
  523. continue
  524. }
  525. now := time.Now()
  526. snap := &TrafficSnapshot{
  527. Timestamp: now.Unix(),
  528. Interfaces: make(map[string]InterfaceLiveRate),
  529. }
  530. var totalRxRate, totalTxRate float64
  531. for name, c := range curr {
  532. p, exists := prev[name]
  533. var rxRate, txRate float64
  534. if exists {
  535. dt := c.time.Sub(p.time).Seconds()
  536. if dt > 0.05 && c.rxBytes >= p.rxBytes && c.txBytes >= p.txBytes {
  537. rxRate = float64(c.rxBytes-p.rxBytes) / dt
  538. txRate = float64(c.txBytes-p.txBytes) / dt
  539. }
  540. }
  541. lr := InterfaceLiveRate{
  542. Name: name,
  543. RxBytesPerSec: rxRate,
  544. TxBytesPerSec: txRate,
  545. RxHuman: formatBytes(rxRate) + "/s",
  546. TxHuman: formatBytes(txRate) + "/s",
  547. RxBytes: c.rxBytes,
  548. TxBytes: c.txBytes,
  549. RxPackets: c.rxPackets,
  550. TxPackets: c.txPackets,
  551. RxErrors: c.rxErrors,
  552. TxErrors: c.txErrors,
  553. RxDropped: c.rxDropped,
  554. TxDropped: c.txDropped,
  555. }
  556. snap.Interfaces[name] = lr
  557. // Exclude loopback from global WAN/LAN totals
  558. if name != "lo" && !strings.HasPrefix(name, "dummy") {
  559. totalRxRate += rxRate
  560. totalTxRate += txRate
  561. }
  562. }
  563. snap.TotalRxRate = totalRxRate
  564. snap.TotalTxRate = totalTxRate
  565. snap.TotalRxHuman = formatBytes(totalRxRate) + "/s"
  566. snap.TotalTxHuman = formatBytes(totalTxRate) + "/s"
  567. m.snapMu.Lock()
  568. m.latestSnapshot = snap
  569. m.snapMu.Unlock()
  570. prev = curr
  571. // Fan out to SSE subscribers
  572. m.subMu.Lock()
  573. for ch := range m.subscribers {
  574. select {
  575. case ch <- snap:
  576. default:
  577. // drop frame if client is slow
  578. }
  579. }
  580. m.subMu.Unlock()
  581. }
  582. }()
  583. }
  584. func (m *Manager) GetLatestTrafficSnapshot() *TrafficSnapshot {
  585. m.snapMu.RLock()
  586. defer m.snapMu.RUnlock()
  587. return m.latestSnapshot
  588. }
  589. func (m *Manager) SubscribeTraffic() chan *TrafficSnapshot {
  590. m.subMu.Lock()
  591. defer m.subMu.Unlock()
  592. ch := make(chan *TrafficSnapshot, 4)
  593. m.subscribers[ch] = true
  594. return ch
  595. }
  596. func (m *Manager) UnsubscribeTraffic(ch chan *TrafficSnapshot) {
  597. m.subMu.Lock()
  598. defer m.subMu.Unlock()
  599. delete(m.subscribers, ch)
  600. close(ch)
  601. }