package network import ( "fmt" "net" "regexp" "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), } } // IsKernelDummyInterface returns true if link is an internal kernel fallback/dummy device (e.g. gre0, gretap0, erspan0) func IsKernelDummyInterface(name string) bool { if strings.HasPrefix(name, "ep_") { return true } switch name { case "gre0", "gretap0", "erspan0", "sit0", "tunl0", "ip6tnl0", "ip6gre0", "ip6erspan0": return true default: return false } } // 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() if IsKernelDummyInterface(attrs.Name) { continue } 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) } var ifaceNameRegex = regexp.MustCompile(`^[a-zA-Z0-9_][a-zA-Z0-9_.-]*$`) // RenameInterface safely renames an existing network device using Netlink. func (m *Manager) RenameInterface(oldName, newName string) (*InterfaceInfo, error) { oldName = strings.TrimSpace(oldName) newName = strings.TrimSpace(newName) if oldName == "" || newName == "" { return nil, fmt.Errorf("interface name cannot be empty") } if oldName == newName { return m.GetInterface(newName) } if oldName == "lo" || newName == "lo" { return nil, fmt.Errorf("cannot rename the loopback interface") } if len(newName) > 15 { return nil, fmt.Errorf("interface name %q exceeds Linux limit of 15 characters", newName) } if !ifaceNameRegex.MatchString(newName) { return nil, fmt.Errorf("invalid interface name %q: must contain only letters, numbers, underscores, dots, or hyphens", newName) } link, err := netlink.LinkByName(oldName) if err != nil { return nil, fmt.Errorf("interface %q not found: %w", oldName, err) } if _, err := netlink.LinkByName(newName); err == nil { return nil, fmt.Errorf("interface %q already exists", newName) } wasUp := (link.Attrs().Flags & net.FlagUp) != 0 // Linux kernel requires interface to be DOWN to change dev name if wasUp { if err := netlink.LinkSetDown(link); err != nil { return nil, fmt.Errorf("failed to bring interface %q down for renaming: %w", oldName, err) } } if err := netlink.LinkSetName(link, newName); err != nil { if wasUp { _ = netlink.LinkSetUp(link) } return nil, fmt.Errorf("failed to rename interface %q to %q: %w", oldName, newName, err) } // Restore UP state if it was originally UP if wasUp { if newLink, err := netlink.LinkByName(newName); err == nil { _ = netlink.LinkSetUp(newLink) } } // Migrate metadata _ = m.metadata.Rename(oldName, newName) // Update traffic rate history m.mu.Lock() if prev, ok := m.prevStats[oldName]; ok { delete(m.prevStats, oldName) m.prevStats[newName] = prev } m.mu.Unlock() return m.GetInterface(newName) } // 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) } // Validate parent interface supports 802.1Q VLAN sub-interfaces parentType := strings.ToLower(parent.Type()) parentName := strings.ToLower(parent.Attrs().Name) if parentType == "wireguard" || strings.HasPrefix(parentName, "wg") || parentType == "loopback" || parentName == "lo" || parentType == "ppp" || strings.HasPrefix(parentName, "ppp") || parentType == "dummy" || parentType == "tun" || parentType == "tap" || parentType == "gre" || parent.Attrs().Flags&net.FlagLoopback != 0 || parent.Attrs().Flags&net.FlagPointToPoint != 0 { return nil, fmt.Errorf("interface %q does not support 802.1Q VLAN sub-interfaces", req.ParentName) } 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]) }