manager.go 17 KB

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