manager.go 21 KB

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