package bridge import ( "bufio" "encoding/json" "errors" "fmt" "log" "net" "os" "os/exec" "path/filepath" "sort" "strconv" "strings" "sync" "syscall" "time" "github.com/vishvananda/netlink" ) const BridgeConfigFile = "/etc/proxmox-router/bridge.json" type BridgeConfigStore struct { Bridges []*BridgeInstance `json:"bridges"` Ports []*BridgePort `json:"ports"` VLANs []*BridgeVLAN `json:"vlans"` } type BridgeManager struct { mu sync.RWMutex storePath string bridges map[string]*BridgeInstance ports map[string]*BridgePort vlans map[string]*BridgeVLAN } func NewBridgeManager() *BridgeManager { bm := &BridgeManager{ storePath: BridgeConfigFile, bridges: make(map[string]*BridgeInstance), ports: make(map[string]*BridgePort), vlans: make(map[string]*BridgeVLAN), } bm.load() bm.SyncAll() return bm } func (m *BridgeManager) load() { m.mu.Lock() defer m.mu.Unlock() data, err := os.ReadFile(m.storePath) if err != nil { return } var store BridgeConfigStore if err := json.Unmarshal(data, &store); err != nil { return } for _, b := range store.Bridges { m.bridges[b.ID] = b } for _, p := range store.Ports { m.ports[p.ID] = p } for _, v := range store.VLANs { m.vlans[v.ID] = v } } func (m *BridgeManager) save() error { _ = os.MkdirAll(filepath.Dir(m.storePath), 0755) store := BridgeConfigStore{ Bridges: make([]*BridgeInstance, 0, len(m.bridges)), Ports: make([]*BridgePort, 0, len(m.ports)), VLANs: make([]*BridgeVLAN, 0, len(m.vlans)), } for _, b := range m.bridges { store.Bridges = append(store.Bridges, b) } for _, p := range m.ports { store.Ports = append(store.Ports, p) } for _, v := range m.vlans { store.VLANs = append(store.VLANs, v) } data, err := json.MarshalIndent(store, "", " ") if err != nil { return err } return os.WriteFile(m.storePath, data, 0644) } // ==================== Bridge Interfaces ==================== func (m *BridgeManager) ListBridges() []*BridgeInstance { m.mu.Lock() defer m.mu.Unlock() list := make([]*BridgeInstance, 0, len(m.bridges)) for _, b := range m.bridges { copy := *b copy.IPAddresses = append([]string(nil), b.IPAddresses...) m.updateBridgeLiveStats(©) list = append(list, ©) } sort.Slice(list, func(i, j int) bool { return list[i].Name < list[j].Name }) return list } func (m *BridgeManager) GetBridge(idOrName string) (*BridgeInstance, bool) { m.mu.Lock() defer m.mu.Unlock() if b, ok := m.bridges[idOrName]; ok { copy := *b copy.IPAddresses = append([]string(nil), b.IPAddresses...) m.updateBridgeLiveStats(©) return ©, true } for _, b := range m.bridges { if b.Name == idOrName { copy := *b copy.IPAddresses = append([]string(nil), b.IPAddresses...) m.updateBridgeLiveStats(©) return ©, true } } return nil, false } func (m *BridgeManager) SaveBridge(b BridgeInstance) error { if b.STP == "rstp" { if _, err := exec.LookPath("mstpctl"); err != nil { return fmt.Errorf("RSTP requires mstpd/mstpctl; select STP") } } m.mu.Lock() defer m.mu.Unlock() if b.Name == "" { return fmt.Errorf("bridge name is required") } if b.MTU <= 0 { b.MTU = 1500 } if b.PVID <= 0 { b.PVID = 1 } if b.STP == "" { b.STP = "stp" } if b.Priority <= 0 { b.Priority = 32768 } if b.ID == "" { b.ID = fmt.Sprintf("br-%d", time.Now().UnixNano()) } // Update existing or add new old := m.bridges[b.ID] if err := m.syncBridge(&b); err != nil { return err } m.bridges[b.ID] = &b if err := m.save(); err != nil { if old == nil { delete(m.bridges, b.ID) } else { m.bridges[b.ID] = old _ = m.syncBridge(old) } return err } return nil } func (m *BridgeManager) DeleteBridge(id string) error { m.mu.Lock() defer m.mu.Unlock() b, ok := m.bridges[id] if !ok { return fmt.Errorf("bridge not found") } // Detach any ports belonging to this bridge for _, p := range m.ports { if p.Bridge == b.Name { if out, err := exec.Command("ip", "link", "set", p.Interface, "nomaster").CombinedOutput(); err != nil { return fmt.Errorf("bridge command failed: %s: %w", out, err) } delete(m.ports, p.ID) } } // Delete any VLAN entries for this bridge for vid, v := range m.vlans { if v.Bridge == b.Name { delete(m.vlans, vid) } } // Delete bridge link in kernel if out, err := exec.Command("ip", "link", "del", b.Name).CombinedOutput(); err != nil { return fmt.Errorf("bridge command failed: %s: %w", out, err) } delete(m.bridges, id) return m.save() } func (m *BridgeManager) SetBridgeState(id string, disabled bool) error { m.mu.Lock() defer m.mu.Unlock() b, ok := m.bridges[id] if !ok { return fmt.Errorf("bridge not found") } b.Disabled = disabled if err := m.save(); err != nil { return err } if disabled { if out, err := exec.Command("ip", "link", "set", b.Name, "down").CombinedOutput(); err != nil { return fmt.Errorf("bridge command failed: %s: %w", out, err) } } else { if out, err := exec.Command("ip", "link", "set", b.Name, "up").CombinedOutput(); err != nil { return fmt.Errorf("bridge command failed: %s: %w", out, err) } } return nil } func (m *BridgeManager) AddBridgeIP(idOrName string, ipCIDR string) error { m.mu.Lock() defer m.mu.Unlock() b, ok := m.bridges[idOrName] if !ok { for _, br := range m.bridges { if br.Name == idOrName { b = br ok = true break } } } if !ok { return fmt.Errorf("bridge not found") } ipCIDR = strings.TrimSpace(ipCIDR) if _, _, err := net.ParseCIDR(ipCIDR); err != nil { return fmt.Errorf("invalid IP/CIDR: %w", err) } // Check if already in list for _, ip := range b.IPAddresses { if ip == ipCIDR { return nil } } b.IPAddresses = append(b.IPAddresses, ipCIDR) if err := m.save(); err != nil { return err } // Add to kernel if out, err := exec.Command("ip", "addr", "add", ipCIDR, "dev", b.Name).CombinedOutput(); err != nil { return fmt.Errorf("bridge command failed: %s: %w", out, err) } return nil } func (m *BridgeManager) DeleteBridgeIP(idOrName string, ipCIDR string) error { m.mu.Lock() defer m.mu.Unlock() b, ok := m.bridges[idOrName] if !ok { for _, br := range m.bridges { if br.Name == idOrName { b = br ok = true break } } } if !ok { return fmt.Errorf("bridge not found") } ipCIDR = strings.TrimSpace(ipCIDR) var updated []string for _, ip := range b.IPAddresses { if ip != ipCIDR { updated = append(updated, ip) } } b.IPAddresses = updated if err := m.save(); err != nil { return err } if out, err := exec.Command("ip", "addr", "del", ipCIDR, "dev", b.Name).CombinedOutput(); err != nil { return fmt.Errorf("bridge command failed: %s: %w", out, err) } return nil } func (m *BridgeManager) syncBridge(b *BridgeInstance) error { if b.STP == "rstp" { if _, err := exec.LookPath("mstpctl"); err != nil { log.Printf("Bridge %s: legacy RSTP configuration uses kernel STP because mstpd is unavailable", b.Name) b.STP = "stp" } } link, err := netlink.LinkByName(b.Name) if err != nil { // Create bridge args := []string{"link", "add", b.Name, "type", "bridge"} if b.VlanFiltering { args = append(args, "vlan_filtering", "1") } else { args = append(args, "vlan_filtering", "0") } if b.PVID > 0 { args = append(args, "vlan_default_pvid", strconv.Itoa(b.PVID)) } if out, err := exec.Command("ip", args...).CombinedOutput(); err != nil { return fmt.Errorf("failed to create bridge %s: %s: %w", b.Name, string(out), err) } link, _ = netlink.LinkByName(b.Name) } if link != nil { // Set MTU if b.MTU > 0 { if err := netlink.LinkSetMTU(link, b.MTU); err != nil { return err } } // Set Admin MAC if specified if b.MACAddress != "" { if hw, err := net.ParseMAC(b.MACAddress); err == nil { if err := netlink.LinkSetHardwareAddr(link, hw); err != nil { return err } } } // Set VLAN filtering on existing bridge vfVal := "0" if b.VlanFiltering { vfVal = "1" } pvidVal := strconv.Itoa(b.PVID) if b.PVID <= 0 { pvidVal = "1" } if out, err := exec.Command("ip", "link", "set", b.Name, "type", "bridge", "vlan_filtering", vfVal, "vlan_default_pvid", pvidVal).CombinedOutput(); err != nil { return fmt.Errorf("bridge command failed: %s: %w", out, err) } // Set STP stpVal := "0" if b.STP == "rstp" || b.STP == "stp" { stpVal = "1" } if err := os.WriteFile(fmt.Sprintf("/sys/class/net/%s/bridge/stp_state", b.Name), []byte(stpVal), 0644); err != nil { return err } if b.STP == "rstp" { if out, err := exec.Command("mstpctl", "addbridge", b.Name).CombinedOutput(); err != nil { return fmt.Errorf("RSTP daemon: %s: %w", out, err) } if out, err := exec.Command("mstpctl", "setforcevers", b.Name, "rstp").CombinedOutput(); err != nil { return fmt.Errorf("RSTP configuration: %s: %w", out, err) } } // Set link up or down if b.Disabled { if err := netlink.LinkSetDown(link); err != nil { return err } } else { if err := netlink.LinkSetUp(link); err != nil { return err } } // Sync IP addresses for _, ip := range b.IPAddresses { addr, err := netlink.ParseAddr(ip) if err != nil { return err } if err = netlink.AddrAdd(link, addr); err != nil && !errors.Is(err, syscall.EEXIST) { return err } } } return nil } func (m *BridgeManager) updateBridgeLiveStats(b *BridgeInstance) { // Count ports belonging to this bridge portsCount := 0 for _, p := range m.ports { if p.Bridge == b.Name && !p.Disabled { portsCount++ } } b.PortsCount = portsCount link, err := netlink.LinkByName(b.Name) if err != nil { b.Running = false return } attrs := link.Attrs() b.Running = (attrs.Flags&net.FlagUp != 0) && (attrs.OperState == netlink.OperUp || attrs.OperState == netlink.OperUnknown) if b.MACAddress == "" { b.MACAddress = attrs.HardwareAddr.String() } if attrs.Statistics != nil { b.RxPackets = attrs.Statistics.RxPackets b.TxPackets = attrs.Statistics.TxPackets b.RxBytes = attrs.Statistics.RxBytes b.TxBytes = attrs.Statistics.TxBytes b.RxHuman = formatBytes(b.RxBytes) b.TxHuman = formatBytes(b.TxBytes) } // Read IP addresses from kernel if addrs, err := netlink.AddrList(link, netlink.FAMILY_V4); err == nil { var ips []string for _, a := range addrs { ips = append(ips, a.IPNet.String()) } b.IPAddresses = ips } } // ==================== Bridge Ports ==================== func (m *BridgeManager) ListPorts() []*BridgePort { m.mu.Lock() defer m.mu.Unlock() list := make([]*BridgePort, 0, len(m.ports)) for _, p := range m.ports { m.updatePortLiveStatus(p) list = append(list, p) } sort.Slice(list, func(i, j int) bool { if list[i].Bridge != list[j].Bridge { return list[i].Bridge < list[j].Bridge } return list[i].Interface < list[j].Interface }) return list } func (m *BridgeManager) SavePort(p BridgePort) error { m.mu.Lock() defer m.mu.Unlock() if p.Bridge == "" || p.Interface == "" { return fmt.Errorf("bridge and interface are required") } if p.PVID <= 0 { p.PVID = 1 } if p.Priority <= 0 { p.Priority = 128 } if p.PathCost <= 0 { p.PathCost = 10 } if p.FrameTypes == "" { p.FrameTypes = "admit-all" } if p.ID == "" { p.ID = fmt.Sprintf("port-%d", time.Now().UnixNano()) } old := m.ports[p.ID] if err := m.syncPort(&p); err != nil { if old != nil { _ = m.syncPort(old) } return err } m.ports[p.ID] = &p if err := m.save(); err != nil { if old != nil { m.ports[p.ID] = old _ = m.syncPort(old) } else { delete(m.ports, p.ID) } return err } return nil } func (m *BridgeManager) DeletePort(id string) error { m.mu.Lock() defer m.mu.Unlock() p, ok := m.ports[id] if !ok { return fmt.Errorf("port not found") } // Remove from bridge in kernel if out, err := exec.Command("ip", "link", "set", p.Interface, "nomaster").CombinedOutput(); err != nil { return fmt.Errorf("bridge command failed: %s: %w", out, err) } delete(m.ports, id) return m.save() } func (m *BridgeManager) SetPortState(id string, disabled bool) error { m.mu.Lock() defer m.mu.Unlock() p, ok := m.ports[id] if !ok { return fmt.Errorf("port not found") } p.Disabled = disabled if err := m.save(); err != nil { return err } if disabled { if out, err := exec.Command("ip", "link", "set", p.Interface, "nomaster").CombinedOutput(); err != nil { return fmt.Errorf("bridge command failed: %s: %w", out, err) } } else { if err := m.syncPort(p); err != nil { return err } } return nil } func (m *BridgeManager) syncPort(p *BridgePort) error { if p.Disabled { if out, err := exec.Command("ip", "link", "set", p.Interface, "nomaster").CombinedOutput(); err != nil { return fmt.Errorf("bridge command failed: %s: %w", out, err) } return nil } // Check if interface exists if _, err := netlink.LinkByName(p.Interface); err != nil { return fmt.Errorf("interface %s does not exist", p.Interface) } // Attach interface to bridge if out, err := exec.Command("ip", "link", "set", p.Interface, "master", p.Bridge).CombinedOutput(); err != nil { return fmt.Errorf("failed to attach %s to bridge %s: %s: %w", p.Interface, p.Bridge, string(out), err) } // Bring interface UP if out, err := exec.Command("ip", "link", "set", p.Interface, "up").CombinedOutput(); err != nil { return fmt.Errorf("bridge command failed: %s: %w", out, err) } // Port isolation / Split Horizon isolated := "off" if p.Horizon > 0 { isolated = "on" } if out, err := exec.Command("bridge", "link", "set", "dev", p.Interface, "isolated", isolated).CombinedOutput(); err != nil { return fmt.Errorf("bridge command failed: %s: %w", out, err) } // Learning learning := "on" if !p.Learn { learning = "off" } if out, err := exec.Command("bridge", "link", "set", "dev", p.Interface, "learning", learning).CombinedOutput(); err != nil { return fmt.Errorf("bridge command failed: %s: %w", out, err) } // Linux VLAN admission is controlled by the bridge's VLAN filtering state. if data, err := os.ReadFile(fmt.Sprintf("/sys/class/net/%s/bridge/vlan_filtering", p.Bridge)); err == nil { p.IngressFiltering = strings.TrimSpace(string(data)) == "1" } // Priority & Path Cost prioCostArgs := []string{"link", "set", "dev", p.Interface} if p.Priority > 0 { prioCostArgs = append(prioCostArgs, "priority", strconv.Itoa(p.Priority/4)) } if p.PathCost > 0 { prioCostArgs = append(prioCostArgs, "cost", strconv.Itoa(p.PathCost)) } if len(prioCostArgs) > 4 { if out, err := exec.Command("bridge", prioCostArgs...).CombinedOutput(); err != nil { return fmt.Errorf("bridge command failed: %s: %w", out, err) } } // VLAN filtering configuration if p.PVID > 0 { if out, err := exec.Command("bridge", "vlan", "add", "dev", p.Interface, "vid", strconv.Itoa(p.PVID), "pvid", "untagged").CombinedOutput(); err != nil { return fmt.Errorf("bridge command failed: %s: %w", out, err) } } return nil } func (m *BridgeManager) updatePortLiveStatus(p *BridgePort) { if data, err := os.ReadFile(fmt.Sprintf("/sys/class/net/%s/bridge/vlan_filtering", p.Bridge)); err == nil { p.IngressFiltering = strings.TrimSpace(string(data)) == "1" } if p.Disabled { p.Status = "disabled" return } link, err := netlink.LinkByName(p.Interface) if err != nil { p.Status = "inactive" return } attrs := link.Attrs() if attrs.OperState == netlink.OperDown { p.Status = "disabled" } else if attrs.MasterIndex > 0 { p.Status = "forwarding" } else { p.Status = "unattached" } } // ==================== Bridge VLANs ==================== func (m *BridgeManager) ListVLANs() []*BridgeVLAN { m.mu.Lock() defer m.mu.Unlock() list := make([]*BridgeVLAN, 0, len(m.vlans)) for _, v := range m.vlans { v.CurrentTagged = v.Tagged v.CurrentUntagged = v.Untagged list = append(list, v) } sort.Slice(list, func(i, j int) bool { if list[i].Bridge != list[j].Bridge { return list[i].Bridge < list[j].Bridge } return list[i].VlanIDs < list[j].VlanIDs }) return list } func (m *BridgeManager) SaveVLAN(v BridgeVLAN) error { m.mu.Lock() defer m.mu.Unlock() if v.Bridge == "" || len(parseVLANRange(v.VlanIDs)) == 0 { return fmt.Errorf("valid bridge and VLAN IDs are required") } for _, vid := range parseVLANRange(v.VlanIDs) { if vid < 1 || vid > 4094 { return fmt.Errorf("VLAN ID must be 1-4094") } } if v.ID == "" { v.ID = fmt.Sprintf("bvlan-%d", time.Now().UnixNano()) } old := m.vlans[v.ID] if err := syncMembers(old, &v); err != nil { return err } m.vlans[v.ID] = &v if err := m.save(); err != nil { _ = syncMembers(&v, old) if old == nil { delete(m.vlans, v.ID) } else { m.vlans[v.ID] = old } return err } return nil } func (m *BridgeManager) DeleteVLAN(id string) error { m.mu.Lock() defer m.mu.Unlock() old, ok := m.vlans[id] if !ok { return fmt.Errorf("VLAN not found") } if err := syncMembers(old, nil); err != nil { return err } delete(m.vlans, id) if err := m.save(); err != nil { m.vlans[id] = old _ = syncMembers(nil, old) return err } return nil } func (m *BridgeManager) SetVLANState(id string, disabled bool) error { m.mu.Lock() defer m.mu.Unlock() old, ok := m.vlans[id] if !ok { return fmt.Errorf("VLAN not found") } next := *old next.Disabled = disabled if err := syncMembers(old, &next); err != nil { return err } m.vlans[id] = &next if err := m.save(); err != nil { m.vlans[id] = old _ = syncMembers(&next, old) return err } return nil } func (m *BridgeManager) syncVLAN(v *BridgeVLAN) error { return syncMembers(nil, v) } // ==================== Bridge Hosts (FDB / MAC Table) ==================== func (m *BridgeManager) ListHosts(bridgeFilter string) ([]BridgeHost, error) { args := []string{"fdb", "show"} if bridgeFilter != "" { args = append(args, "br", bridgeFilter) } out, err := exec.Command("bridge", args...).Output() if err != nil { return nil, fmt.Errorf("failed to run bridge fdb show: %w", err) } var hosts []BridgeHost scanner := bufio.NewScanner(strings.NewReader(string(out))) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if line == "" { continue } fields := strings.Fields(line) if len(fields) < 3 { continue } mac := fields[0] dev := "" vlan := 1 master := "" age := 0.0 isLocal := false isStatic := false for i := 1; i < len(fields); i++ { switch fields[i] { case "dev": if i+1 < len(fields) { dev = fields[i+1] i++ } case "vlan": if i+1 < len(fields) { vlan, _ = strconv.Atoi(fields[i+1]) i++ } case "master": if i+1 < len(fields) { master = fields[i+1] i++ } case "self": isLocal = true case "permanent": isLocal = true isStatic = true case "static": isStatic = true default: if val, err := strconv.ParseFloat(fields[i], 64); err == nil { age = val } } } if master == "" && strings.HasPrefix(dev, "br") { master = dev } var flags []string if isLocal { flags = append(flags, "L") } else if isStatic { flags = append(flags, "S") } else { flags = append(flags, "D") } hosts = append(hosts, BridgeHost{ MAC: mac, Interface: dev, Bridge: master, VlanID: vlan, Age: age, Local: isLocal, Static: isStatic, Flags: strings.Join(flags, ","), }) } return hosts, nil } func (m *BridgeManager) FlushHosts(bridge string) error { if bridge == "" { return fmt.Errorf("bridge name required for flush") } if out, err := exec.Command("bridge", "fdb", "flush", "dev", bridge, "dynamic").CombinedOutput(); err != nil { return fmt.Errorf("bridge command failed: %s: %w", out, err) } return nil } // ==================== Summary & SyncAll ==================== func (m *BridgeManager) GetSummary() BridgeSummary { m.mu.RLock() bCount := len(m.bridges) pCount := len(m.ports) vCount := len(m.vlans) m.mu.RUnlock() hCount := 0 if hosts, err := m.ListHosts(""); err == nil { hCount = len(hosts) } return BridgeSummary{ BridgesCount: bCount, PortsCount: pCount, VLANsCount: vCount, HostsCount: hCount, } } func (m *BridgeManager) SyncAll() { m.mu.RLock() defer m.mu.RUnlock() // 1. Sync all bridges for _, b := range m.bridges { _ = m.syncBridge(b) } // 2. Sync all ports for _, p := range m.ports { _ = m.syncPort(p) } // 3. Sync all VLAN entries for _, v := range m.vlans { _ = m.syncVLAN(v) } } // RenameInterface updates bridge, port, and VLAN configurations when an interface is renamed. func (m *BridgeManager) RenameInterface(oldName, newName string) { m.mu.Lock() defer m.mu.Unlock() changed := false for _, b := range m.bridges { if b.Name == oldName { b.Name = newName changed = true } } for _, p := range m.ports { if p.Interface == oldName { p.Interface = newName changed = true } } for _, v := range m.vlans { if v.Bridge == oldName { v.Bridge = newName changed = true } for i, tagged := range v.Tagged { if tagged == oldName { v.Tagged[i] = newName changed = true } } for i, untagged := range v.Untagged { if untagged == oldName { v.Untagged[i] = newName changed = true } } } if changed { _ = m.save() } } // Helper: parse "10" or "10,20,30" or "10-20" into int slice func parseVLANRange(s string) []int { var result []int parts := strings.Split(s, ",") for _, p := range parts { p = strings.TrimSpace(p) if strings.Contains(p, "-") { rangeParts := strings.Split(p, "-") if len(rangeParts) == 2 { start, err1 := strconv.Atoi(strings.TrimSpace(rangeParts[0])) end, err2 := strconv.Atoi(strings.TrimSpace(rangeParts[1])) if err1 == nil && err2 == nil && start <= end { for i := start; i <= end; i++ { result = append(result, i) } continue } } } if val, err := strconv.Atoi(p); err == nil { result = append(result, val) } } return result } func formatBytes(b uint64) string { const unit = 1024 if b < unit { return fmt.Sprintf("%d B", b) } div, exp := uint64(unit), 0 for n := b / unit; n >= unit; n /= unit { div *= unit exp++ } return fmt.Sprintf("%.1f %cB", float64(b)/float64(div), "KMGTPE"[exp]) }