package network import ( "fmt" "net" "strings" "sync" "time" "github.com/vishvananda/netlink" ) type prevStat struct { rxBytes uint64 txBytes uint64 time time.Time } // Manager handles all Linux network interactions via Netlink. type Manager struct { mu sync.Mutex metadata *MetadataManager prevStats map[string]prevStat } func NewManager(mm *MetadataManager) *Manager { return &Manager{ metadata: mm, prevStats: make(map[string]prevStat), } } // ListInterfaces returns all network links on the system. func (m *Manager) ListInterfaces() ([]InterfaceInfo, error) { links, err := netlink.LinkList() if err != nil { return nil, fmt.Errorf("failed to list links: %w", err) } linkMap := make(map[int]netlink.Link) for _, l := range links { linkMap[l.Attrs().Index] = l } m.mu.Lock() defer m.mu.Unlock() now := time.Now() result := make([]InterfaceInfo, 0) for _, link := range links { attrs := link.Attrs() info := InterfaceInfo{ Index: attrs.Index, Name: attrs.Name, Type: link.Type(), MTU: attrs.MTU, MAC: attrs.HardwareAddr.String(), OperState: strings.ToUpper(attrs.OperState.String()), IPv4: make([]string, 0), IPv6: make([]string, 0), Flags: make([]string, 0), } // Flags if attrs.Flags&net.FlagUp != 0 { info.State = "UP" } else { info.State = "DOWN" } for _, f := range strings.Split(attrs.Flags.String(), "|") { if f != "" { info.Flags = append(info.Flags, f) } } // Check if interface is a VLAN if vlan, ok := link.(*netlink.Vlan); ok { info.IsVLAN = true info.VlanID = vlan.VlanId info.ParentIndex = vlan.ParentIndex if parent, exists := linkMap[vlan.ParentIndex]; exists { info.ParentName = parent.Attrs().Name } } else if strings.Contains(attrs.Name, ".") { // Sub-interface naming fallback (e.g. eth0.10) parts := strings.Split(attrs.Name, ".") if len(parts) == 2 { var id int if n, _ := fmt.Sscanf(parts[1], "%d", &id); n == 1 { info.IsVLAN = true info.VlanID = id info.ParentName = parts[0] } } } // Addresses addrs, err := netlink.AddrList(link, netlink.FAMILY_ALL) if err == nil { for _, a := range addrs { if a.IP.To4() != nil { info.IPv4 = append(info.IPv4, a.IPNet.String()) } else { info.IPv6 = append(info.IPv6, a.IPNet.String()) } } } // Metadata meta := m.metadata.Get(attrs.Name) info.Role = meta.Role info.Description = meta.Description if info.Role == "" { if attrs.Name == "lo" { info.Role = "LOOPBACK" } else if info.IsVLAN { info.Role = "VLAN" } else if strings.HasPrefix(attrs.Name, "eth0") || strings.HasPrefix(attrs.Name, "ens") { info.Role = "WAN/LAN" } } // Statistics & throughput calculation if attrs.Statistics != nil { info.Stats = NetStats{ RxBytes: attrs.Statistics.RxBytes, TxBytes: attrs.Statistics.TxBytes, RxPackets: attrs.Statistics.RxPackets, TxPackets: attrs.Statistics.TxPackets, RxErrors: attrs.Statistics.RxErrors, TxErrors: attrs.Statistics.TxErrors, RxDropped: attrs.Statistics.RxDropped, TxDropped: attrs.Statistics.TxDropped, } if prev, ok := m.prevStats[attrs.Name]; ok { dt := now.Sub(prev.time).Seconds() if dt > 0.1 && attrs.Statistics.RxBytes >= prev.rxBytes && attrs.Statistics.TxBytes >= prev.txBytes { rxRate := float64(attrs.Statistics.RxBytes-prev.rxBytes) / dt txRate := float64(attrs.Statistics.TxBytes-prev.txBytes) / dt info.Rate = TrafficRate{ RxBytesPerSec: rxRate, TxBytesPerSec: txRate, RxHuman: formatBytes(rxRate) + "/s", TxHuman: formatBytes(txRate) + "/s", } } } m.prevStats[attrs.Name] = prevStat{ rxBytes: attrs.Statistics.RxBytes, txBytes: attrs.Statistics.TxBytes, time: now, } } result = append(result, info) } return result, nil } // GetInterface returns information for a specific interface. func (m *Manager) GetInterface(name string) (*InterfaceInfo, error) { interfaces, err := m.ListInterfaces() if err != nil { return nil, err } for _, iface := range interfaces { if iface.Name == name { return &iface, nil } } return nil, fmt.Errorf("interface %s not found", name) } // SetInterfaceState sets the administrative status (UP or DOWN). func (m *Manager) SetInterfaceState(name string, up bool) error { link, err := netlink.LinkByName(name) if err != nil { return fmt.Errorf("link not found: %w", err) } if up { return netlink.LinkSetUp(link) } return netlink.LinkSetDown(link) } // SetInterfaceMTU updates the MTU for the specified interface. func (m *Manager) SetInterfaceMTU(name string, mtu int) error { if mtu < 68 || mtu > 65535 { return fmt.Errorf("invalid MTU size: %d", mtu) } link, err := netlink.LinkByName(name) if err != nil { return fmt.Errorf("link not found: %w", err) } return netlink.LinkSetMTU(link, mtu) } // AddInterfaceIP adds an IP address with subnet CIDR to an interface. func (m *Manager) AddInterfaceIP(name string, cidr string) error { link, err := netlink.LinkByName(name) if err != nil { return fmt.Errorf("link not found: %w", err) } addr, err := netlink.ParseAddr(cidr) if err != nil { return fmt.Errorf("invalid IP/CIDR %q: %w", cidr, err) } return netlink.AddrAdd(link, addr) } // DeleteInterfaceIP removes an IP address from an interface. func (m *Manager) DeleteInterfaceIP(name string, cidr string) error { link, err := netlink.LinkByName(name) if err != nil { return fmt.Errorf("link not found: %w", err) } addr, err := netlink.ParseAddr(cidr) if err != nil { return fmt.Errorf("invalid IP/CIDR %q: %w", cidr, err) } return netlink.AddrDel(link, addr) } // CreateVLAN creates a new 802.1Q VLAN sub-interface. func (m *Manager) CreateVLAN(req CreateVLANRequest) (*InterfaceInfo, error) { if req.VlanID < 1 || req.VlanID > 4094 { return nil, fmt.Errorf("VLAN ID must be between 1 and 4094, got %d", req.VlanID) } parent, err := netlink.LinkByName(req.ParentName) if err != nil { return nil, fmt.Errorf("parent interface %q not found: %w", req.ParentName, err) } vlanName := req.Name if vlanName == "" { vlanName = fmt.Sprintf("%s.%d", req.ParentName, req.VlanID) } // Check if already exists if _, err := netlink.LinkByName(vlanName); err == nil { return nil, fmt.Errorf("interface %q already exists", vlanName) } mtu := req.MTU if mtu <= 0 { mtu = parent.Attrs().MTU if mtu == 0 { mtu = 1500 } } vlanLink := &netlink.Vlan{ LinkAttrs: netlink.LinkAttrs{ Name: vlanName, ParentIndex: parent.Attrs().Index, MTU: mtu, }, VlanId: req.VlanID, } if err := netlink.LinkAdd(vlanLink); err != nil { return nil, fmt.Errorf("failed to create VLAN link: %w", err) } // Bring UP if req.AutoUp { if err := netlink.LinkSetUp(vlanLink); err != nil { _ = netlink.LinkDel(vlanLink) return nil, fmt.Errorf("failed to set VLAN link UP: %w", err) } } // Assign IP if provided if strings.TrimSpace(req.IPAddress) != "" { addr, err := netlink.ParseAddr(strings.TrimSpace(req.IPAddress)) if err != nil { _ = netlink.LinkDel(vlanLink) return nil, fmt.Errorf("invalid IP address %q: %w", req.IPAddress, err) } if err := netlink.AddrAdd(vlanLink, addr); err != nil { _ = netlink.LinkDel(vlanLink) return nil, fmt.Errorf("failed to assign IP to VLAN: %w", err) } } // Store metadata role := req.Role if role == "" { role = "VLAN" } _ = m.metadata.Set(vlanName, role, req.Description) return m.GetInterface(vlanName) } // DeleteVLAN removes a VLAN interface. func (m *Manager) DeleteVLAN(name string) error { link, err := netlink.LinkByName(name) if err != nil { return fmt.Errorf("interface %s not found: %w", name, err) } if _, ok := link.(*netlink.Vlan); !ok && !strings.Contains(name, ".") { return fmt.Errorf("interface %s is not recognized as a VLAN interface", name) } if err := netlink.LinkDel(link); err != nil { return fmt.Errorf("failed to delete link: %w", err) } _ = m.metadata.Delete(name) return nil } // ListVLANs returns only the VLAN interfaces. func (m *Manager) ListVLANs() ([]InterfaceInfo, error) { all, err := m.ListInterfaces() if err != nil { return nil, err } vlans := make([]InterfaceInfo, 0) for _, iface := range all { if iface.IsVLAN { vlans = append(vlans, iface) } } return vlans, nil } func formatBytes(b float64) string { const unit = 1024 if b < unit { return fmt.Sprintf("%.0f B", b) } div, exp := int64(unit), 0 for n := b / unit; n >= unit; n /= unit { div *= unit exp++ } return fmt.Sprintf("%.1f %cB", b/float64(div), "KMGTPE"[exp]) }