|
|
@@ -3,7 +3,9 @@ package bridge
|
|
|
import (
|
|
|
"bufio"
|
|
|
"encoding/json"
|
|
|
+ "errors"
|
|
|
"fmt"
|
|
|
+ "log"
|
|
|
"net"
|
|
|
"os"
|
|
|
"os/exec"
|
|
|
@@ -12,6 +14,7 @@ import (
|
|
|
"strconv"
|
|
|
"strings"
|
|
|
"sync"
|
|
|
+ "syscall"
|
|
|
"time"
|
|
|
|
|
|
"github.com/vishvananda/netlink"
|
|
|
@@ -103,8 +106,10 @@ func (m *BridgeManager) ListBridges() []*BridgeInstance {
|
|
|
|
|
|
list := make([]*BridgeInstance, 0, len(m.bridges))
|
|
|
for _, b := range m.bridges {
|
|
|
- m.updateBridgeLiveStats(b)
|
|
|
- list = append(list, b)
|
|
|
+ copy := *b
|
|
|
+ copy.IPAddresses = append([]string(nil), b.IPAddresses...)
|
|
|
+ m.updateBridgeLiveStats(©)
|
|
|
+ list = append(list, ©)
|
|
|
}
|
|
|
|
|
|
sort.Slice(list, func(i, j int) bool {
|
|
|
@@ -119,19 +124,28 @@ func (m *BridgeManager) GetBridge(idOrName string) (*BridgeInstance, bool) {
|
|
|
defer m.mu.Unlock()
|
|
|
|
|
|
if b, ok := m.bridges[idOrName]; ok {
|
|
|
- m.updateBridgeLiveStats(b)
|
|
|
- return b, true
|
|
|
+ copy := *b
|
|
|
+ copy.IPAddresses = append([]string(nil), b.IPAddresses...)
|
|
|
+ m.updateBridgeLiveStats(©)
|
|
|
+ return ©, true
|
|
|
}
|
|
|
for _, b := range m.bridges {
|
|
|
if b.Name == idOrName {
|
|
|
- m.updateBridgeLiveStats(b)
|
|
|
- return b, true
|
|
|
+ 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()
|
|
|
|
|
|
@@ -145,7 +159,7 @@ func (m *BridgeManager) SaveBridge(b BridgeInstance) error {
|
|
|
b.PVID = 1
|
|
|
}
|
|
|
if b.STP == "" {
|
|
|
- b.STP = "rstp"
|
|
|
+ b.STP = "stp"
|
|
|
}
|
|
|
if b.Priority <= 0 {
|
|
|
b.Priority = 32768
|
|
|
@@ -156,11 +170,21 @@ func (m *BridgeManager) SaveBridge(b BridgeInstance) error {
|
|
|
}
|
|
|
|
|
|
// Update existing or add new
|
|
|
+ old := m.bridges[b.ID]
|
|
|
+ if err := m.syncBridge(&b); err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
m.bridges[b.ID] = &b
|
|
|
- _ = m.save()
|
|
|
-
|
|
|
- // Apply in Linux kernel
|
|
|
- return m.syncBridge(&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 {
|
|
|
@@ -175,7 +199,9 @@ func (m *BridgeManager) DeleteBridge(id string) error {
|
|
|
// Detach any ports belonging to this bridge
|
|
|
for _, p := range m.ports {
|
|
|
if p.Bridge == b.Name {
|
|
|
- _ = exec.Command("ip", "link", "set", p.Interface, "nomaster").Run()
|
|
|
+ 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)
|
|
|
}
|
|
|
}
|
|
|
@@ -188,7 +214,9 @@ func (m *BridgeManager) DeleteBridge(id string) error {
|
|
|
}
|
|
|
|
|
|
// Delete bridge link in kernel
|
|
|
- _ = exec.Command("ip", "link", "del", b.Name).Run()
|
|
|
+ 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()
|
|
|
@@ -204,12 +232,18 @@ func (m *BridgeManager) SetBridgeState(id string, disabled bool) error {
|
|
|
}
|
|
|
|
|
|
b.Disabled = disabled
|
|
|
- _ = m.save()
|
|
|
+ if err := m.save(); err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
|
|
|
if disabled {
|
|
|
- _ = exec.Command("ip", "link", "set", b.Name, "down").Run()
|
|
|
+ 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 {
|
|
|
- _ = exec.Command("ip", "link", "set", b.Name, "up").Run()
|
|
|
+ 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
|
|
|
}
|
|
|
@@ -245,10 +279,14 @@ func (m *BridgeManager) AddBridgeIP(idOrName string, ipCIDR string) error {
|
|
|
}
|
|
|
|
|
|
b.IPAddresses = append(b.IPAddresses, ipCIDR)
|
|
|
- _ = m.save()
|
|
|
+ if err := m.save(); err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
|
|
|
// Add to kernel
|
|
|
- _ = exec.Command("ip", "addr", "add", ipCIDR, "dev", b.Name).Run()
|
|
|
+ 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
|
|
|
}
|
|
|
|
|
|
@@ -278,13 +316,23 @@ func (m *BridgeManager) DeleteBridgeIP(idOrName string, ipCIDR string) error {
|
|
|
}
|
|
|
}
|
|
|
b.IPAddresses = updated
|
|
|
- _ = m.save()
|
|
|
+ if err := m.save(); err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
|
|
|
- _ = exec.Command("ip", "addr", "del", ipCIDR, "dev", b.Name).Run()
|
|
|
+ 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
|
|
|
@@ -306,13 +354,17 @@ func (m *BridgeManager) syncBridge(b *BridgeInstance) error {
|
|
|
if link != nil {
|
|
|
// Set MTU
|
|
|
if b.MTU > 0 {
|
|
|
- _ = netlink.LinkSetMTU(link, b.MTU)
|
|
|
+ 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 {
|
|
|
- _ = netlink.LinkSetHardwareAddr(link, hw)
|
|
|
+ if err := netlink.LinkSetHardwareAddr(link, hw); err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -325,25 +377,47 @@ func (m *BridgeManager) syncBridge(b *BridgeInstance) error {
|
|
|
if b.PVID <= 0 {
|
|
|
pvidVal = "1"
|
|
|
}
|
|
|
- _ = exec.Command("ip", "link", "set", b.Name, "type", "bridge", "vlan_filtering", vfVal, "vlan_default_pvid", pvidVal).Run()
|
|
|
+ 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"
|
|
|
}
|
|
|
- _ = os.WriteFile(fmt.Sprintf("/sys/class/net/%s/bridge/stp_state", b.Name), []byte(stpVal), 0644)
|
|
|
+ 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 {
|
|
|
- _ = netlink.LinkSetDown(link)
|
|
|
+ if err := netlink.LinkSetDown(link); err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
} else {
|
|
|
- _ = netlink.LinkSetUp(link)
|
|
|
+ if err := netlink.LinkSetUp(link); err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
// Sync IP addresses
|
|
|
for _, ip := range b.IPAddresses {
|
|
|
- _ = exec.Command("ip", "addr", "add", ip, "dev", b.Name).Run()
|
|
|
+ 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
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -437,10 +511,24 @@ func (m *BridgeManager) SavePort(p BridgePort) error {
|
|
|
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
|
|
|
- _ = m.save()
|
|
|
-
|
|
|
- return m.syncPort(&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 {
|
|
|
@@ -453,7 +541,9 @@ func (m *BridgeManager) DeletePort(id string) error {
|
|
|
}
|
|
|
|
|
|
// Remove from bridge in kernel
|
|
|
- _ = exec.Command("ip", "link", "set", p.Interface, "nomaster").Run()
|
|
|
+ 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()
|
|
|
@@ -469,19 +559,27 @@ func (m *BridgeManager) SetPortState(id string, disabled bool) error {
|
|
|
}
|
|
|
|
|
|
p.Disabled = disabled
|
|
|
- _ = m.save()
|
|
|
+ if err := m.save(); err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
|
|
|
if disabled {
|
|
|
- _ = exec.Command("ip", "link", "set", p.Interface, "nomaster").Run()
|
|
|
+ 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 {
|
|
|
- _ = m.syncPort(p)
|
|
|
+ if err := m.syncPort(p); err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
}
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
func (m *BridgeManager) syncPort(p *BridgePort) error {
|
|
|
if p.Disabled {
|
|
|
- _ = exec.Command("ip", "link", "set", p.Interface, "nomaster").Run()
|
|
|
+ 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
|
|
|
}
|
|
|
|
|
|
@@ -496,50 +594,61 @@ func (m *BridgeManager) syncPort(p *BridgePort) error {
|
|
|
}
|
|
|
|
|
|
// Bring interface UP
|
|
|
- _ = exec.Command("ip", "link", "set", p.Interface, "up").Run()
|
|
|
+ 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"
|
|
|
}
|
|
|
- _ = exec.Command("bridge", "link", "set", "dev", p.Interface, "isolated", isolated).Run()
|
|
|
+ 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"
|
|
|
}
|
|
|
- _ = exec.Command("bridge", "link", "set", "dev", p.Interface, "learning", learning).Run()
|
|
|
+ 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)
|
|
|
+ }
|
|
|
|
|
|
- // Ingress filtering
|
|
|
- ingress := "off"
|
|
|
- if p.IngressFiltering {
|
|
|
- ingress = "on"
|
|
|
+ // 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"
|
|
|
}
|
|
|
- _ = exec.Command("bridge", "link", "set", "dev", p.Interface, "ingress_filtering", ingress).Run()
|
|
|
|
|
|
// Priority & Path Cost
|
|
|
prioCostArgs := []string{"link", "set", "dev", p.Interface}
|
|
|
if p.Priority > 0 {
|
|
|
- prioCostArgs = append(prioCostArgs, "priority", strconv.Itoa(p.Priority))
|
|
|
+ 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 {
|
|
|
- _ = exec.Command("bridge", prioCostArgs...).Run()
|
|
|
+ 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 {
|
|
|
- _ = exec.Command("bridge", "vlan", "add", "dev", p.Interface, "vid", strconv.Itoa(p.PVID), "pvid", "untagged").Run()
|
|
|
+ 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
|
|
|
@@ -587,86 +696,72 @@ func (m *BridgeManager) ListVLANs() []*BridgeVLAN {
|
|
|
func (m *BridgeManager) SaveVLAN(v BridgeVLAN) error {
|
|
|
m.mu.Lock()
|
|
|
defer m.mu.Unlock()
|
|
|
-
|
|
|
- if v.Bridge == "" || v.VlanIDs == "" {
|
|
|
- return fmt.Errorf("bridge and vlan_ids are required")
|
|
|
+ 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
|
|
|
- _ = m.save()
|
|
|
-
|
|
|
- return m.syncVLAN(&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()
|
|
|
-
|
|
|
- v, ok := m.vlans[id]
|
|
|
+ old, ok := m.vlans[id]
|
|
|
if !ok {
|
|
|
- return fmt.Errorf("vlan entry not found")
|
|
|
+ return fmt.Errorf("VLAN not found")
|
|
|
}
|
|
|
-
|
|
|
- // Remove VLANs from kernel
|
|
|
- vids := parseVLANRange(v.VlanIDs)
|
|
|
- for _, vid := range vids {
|
|
|
- vidStr := strconv.Itoa(vid)
|
|
|
- for _, iface := range append(v.Tagged, v.Untagged...) {
|
|
|
- _ = exec.Command("bridge", "vlan", "del", "dev", iface, "vid", vidStr).Run()
|
|
|
- }
|
|
|
+ if err := syncMembers(old, nil); err != nil {
|
|
|
+ return err
|
|
|
}
|
|
|
-
|
|
|
delete(m.vlans, id)
|
|
|
- return m.save()
|
|
|
+ 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()
|
|
|
-
|
|
|
- v, ok := m.vlans[id]
|
|
|
+ old, ok := m.vlans[id]
|
|
|
if !ok {
|
|
|
- return fmt.Errorf("vlan entry not found")
|
|
|
+ return fmt.Errorf("VLAN not found")
|
|
|
}
|
|
|
-
|
|
|
- v.Disabled = disabled
|
|
|
- _ = m.save()
|
|
|
-
|
|
|
- if disabled {
|
|
|
- vids := parseVLANRange(v.VlanIDs)
|
|
|
- for _, vid := range vids {
|
|
|
- vidStr := strconv.Itoa(vid)
|
|
|
- for _, iface := range append(v.Tagged, v.Untagged...) {
|
|
|
- _ = exec.Command("bridge", "vlan", "del", "dev", iface, "vid", vidStr).Run()
|
|
|
- }
|
|
|
- }
|
|
|
- } else {
|
|
|
- _ = m.syncVLAN(v)
|
|
|
- }
|
|
|
- return nil
|
|
|
-}
|
|
|
-
|
|
|
-func (m *BridgeManager) syncVLAN(v *BridgeVLAN) error {
|
|
|
- if v.Disabled {
|
|
|
- return nil
|
|
|
+ next := *old
|
|
|
+ next.Disabled = disabled
|
|
|
+ if err := syncMembers(old, &next); err != nil {
|
|
|
+ return err
|
|
|
}
|
|
|
-
|
|
|
- vids := parseVLANRange(v.VlanIDs)
|
|
|
- for _, vid := range vids {
|
|
|
- vidStr := strconv.Itoa(vid)
|
|
|
- for _, iface := range v.Tagged {
|
|
|
- _ = exec.Command("bridge", "vlan", "add", "dev", iface, "vid", vidStr).Run()
|
|
|
- }
|
|
|
- for _, iface := range v.Untagged {
|
|
|
- _ = exec.Command("bridge", "vlan", "add", "dev", iface, "vid", vidStr, "untagged").Run()
|
|
|
- }
|
|
|
+ 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) ====================
|
|
|
|
|
|
@@ -765,7 +860,9 @@ func (m *BridgeManager) FlushHosts(bridge string) error {
|
|
|
if bridge == "" {
|
|
|
return fmt.Errorf("bridge name required for flush")
|
|
|
}
|
|
|
- _ = exec.Command("bridge", "fdb", "flush", "dev", bridge, "dynamic").Run()
|
|
|
+ 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
|
|
|
}
|
|
|
|