package dynamicrouting import ( "encoding/json" "fmt" "log" "os" "os/exec" "path/filepath" "sort" "strings" "sync" "time" ) const ( defaultConfigPath = "/etc/proxmox-router/routing/dynamic.json" frrConfigPath = "/etc/frr/frr.conf" ) type Manager struct { mu sync.RWMutex configPath string config DynamicConfig } func NewManager(configPath string) *Manager { if configPath == "" { configPath = defaultConfigPath } m := &Manager{ configPath: configPath, } if err := m.load(); err != nil { log.Printf("[DynamicRouting] Config load warning: %v, initializing defaults", err) m.initDefaults() _ = m.save() } // Apply initial configuration to FRR if err := m.applyFRRConfig(); err != nil { log.Printf("[DynamicRouting] Initial FRR apply warning: %v", err) } return m } func (m *Manager) initDefaults() { m.config = DynamicConfig{ BGPInstance: BGPInstance{ Enabled: false, AS: 65001, RouterID: "192.168.131.56", HoldTime: 180, KeepaliveTime: 60, RedistributeConnected: false, RedistributeStatic: false, RedistributeOSPF: false, }, BGPPeers: []BGPPeer{}, BGPNetworks: []BGPNetwork{}, OSPFInstance: OSPFInstance{ Enabled: false, RouterID: "192.168.131.56", RedistributeConnected: false, RedistributeStatic: false, RedistributeBGP: false, DefaultMetric: 10, }, OSPFAreas: []OSPFArea{ { ID: "area-backbone", Name: "backbone", AreaID: "0.0.0.0", AreaType: "default", Comment: "Default OSPF Backbone Area", }, }, OSPFInterfaces: []OSPFInterface{}, RoutingFilters: []RoutingFilterRule{}, } } func (m *Manager) load() error { m.mu.Lock() defer m.mu.Unlock() data, err := os.ReadFile(m.configPath) if err != nil { return err } return json.Unmarshal(data, &m.config) } func (m *Manager) save() error { dir := filepath.Dir(m.configPath) if err := os.MkdirAll(dir, 0755); err != nil { return err } data, err := json.MarshalIndent(m.config, "", " ") if err != nil { return err } return os.WriteFile(m.configPath, data, 0644) } // applyFRRConfig generates /etc/frr/frr.conf and reloads FRR daemons func (m *Manager) applyFRRConfig() error { var sb strings.Builder sb.WriteString("frr version 10.3\n") sb.WriteString("frr defaults traditional\n") sb.WriteString("hostname WAN-Balancer\n") sb.WriteString("log syslog informational\n") sb.WriteString("service integrated-vtysh-config\n!\n") // 1. Interfaces section for OSPF (cost, timers, authentication) interfaceConfigs := make(map[string]bool) if m.config.OSPFInstance.Enabled { for _, iface := range m.config.OSPFInterfaces { if iface.Disabled || iface.Interface == "" || interfaceConfigs[iface.Interface] { continue } interfaceConfigs[iface.Interface] = true sb.WriteString(fmt.Sprintf("interface %s\n", iface.Interface)) if iface.Cost > 0 { sb.WriteString(fmt.Sprintf(" ip ospf cost %d\n", iface.Cost)) } if iface.Priority > 0 { sb.WriteString(fmt.Sprintf(" ip ospf priority %d\n", iface.Priority)) } if iface.HelloInterval > 0 { sb.WriteString(fmt.Sprintf(" ip ospf hello-interval %d\n", iface.HelloInterval)) } if iface.DeadInterval > 0 { sb.WriteString(fmt.Sprintf(" ip ospf dead-interval %d\n", iface.DeadInterval)) } if iface.AuthType == "md5" && iface.AuthKey != "" { sb.WriteString(" ip ospf authentication message-digest\n") sb.WriteString(fmt.Sprintf(" ip ospf message-digest-key 1 md5 %s\n", iface.AuthKey)) } else if iface.AuthType == "simple" && iface.AuthKey != "" { sb.WriteString(" ip ospf authentication\n") sb.WriteString(fmt.Sprintf(" ip ospf authentication-key %s\n", iface.AuthKey)) } sb.WriteString("!\n") } } // 2. BGP Router Section if m.config.BGPInstance.Enabled && m.config.BGPInstance.AS > 0 { inst := m.config.BGPInstance sb.WriteString(fmt.Sprintf("router bgp %d\n", inst.AS)) if inst.RouterID != "" { sb.WriteString(fmt.Sprintf(" bgp router-id %s\n", inst.RouterID)) } keepalive := 60 hold := 180 if inst.KeepaliveTime > 0 { keepalive = inst.KeepaliveTime } if inst.HoldTime > 0 { hold = inst.HoldTime } sb.WriteString(fmt.Sprintf(" timers bgp %d %d\n", keepalive, hold)) // Peers definition for _, peer := range m.config.BGPPeers { if peer.Disabled || peer.RemoteAddress == "" || peer.RemoteAS == 0 { continue } sb.WriteString(fmt.Sprintf(" neighbor %s remote-as %d\n", peer.RemoteAddress, peer.RemoteAS)) if peer.Name != "" { sb.WriteString(fmt.Sprintf(" neighbor %s description \"%s\"\n", peer.RemoteAddress, peer.Name)) } if peer.Multihop { sb.WriteString(fmt.Sprintf(" neighbor %s ebgp-multihop 255\n", peer.RemoteAddress)) } if peer.Passive { sb.WriteString(fmt.Sprintf(" neighbor %s passive\n", peer.RemoteAddress)) } if peer.Password != "" { sb.WriteString(fmt.Sprintf(" neighbor %s password %s\n", peer.RemoteAddress, peer.Password)) } if peer.LocalAddress != "" { sb.WriteString(fmt.Sprintf(" neighbor %s update-source %s\n", peer.RemoteAddress, peer.LocalAddress)) } } // Address Family IPv4 Unicast sb.WriteString(" address-family ipv4 unicast\n") // Advertised Networks for _, net := range m.config.BGPNetworks { if !net.Disabled && net.Network != "" { sb.WriteString(fmt.Sprintf(" network %s\n", net.Network)) } } // Activate peers in IPv4 unicast for _, peer := range m.config.BGPPeers { if peer.Disabled || peer.RemoteAddress == "" || peer.RemoteAS == 0 { continue } sb.WriteString(fmt.Sprintf(" neighbor %s activate\n", peer.RemoteAddress)) if peer.NexthopSelf { sb.WriteString(fmt.Sprintf(" neighbor %s next-hop-self\n", peer.RemoteAddress)) } if peer.InFilter != "" { sb.WriteString(fmt.Sprintf(" neighbor %s route-map %s in\n", peer.RemoteAddress, peer.InFilter)) } if peer.OutFilter != "" { sb.WriteString(fmt.Sprintf(" neighbor %s route-map %s out\n", peer.RemoteAddress, peer.OutFilter)) } } // Redistribute if inst.RedistributeConnected { sb.WriteString(" redistribute connected\n") } if inst.RedistributeStatic { sb.WriteString(" redistribute static\n") } if inst.RedistributeOSPF { sb.WriteString(" redistribute ospf\n") } sb.WriteString(" exit-address-family\n!\n") } // 3. OSPF Router Section if m.config.OSPFInstance.Enabled { inst := m.config.OSPFInstance sb.WriteString("router ospf\n") if inst.RouterID != "" { sb.WriteString(fmt.Sprintf(" ospf router-id %s\n", inst.RouterID)) } if inst.DefaultMetric > 0 { sb.WriteString(fmt.Sprintf(" default-metric %d\n", inst.DefaultMetric)) } if inst.RedistributeConnected { sb.WriteString(" redistribute connected\n") } if inst.RedistributeStatic { sb.WriteString(" redistribute static\n") } if inst.RedistributeBGP { sb.WriteString(" redistribute bgp\n") } for _, iface := range m.config.OSPFInterfaces { if iface.Disabled || iface.Network == "" { continue } area := iface.Area if area == "" { area = "0.0.0.0" } sb.WriteString(fmt.Sprintf(" network %s area %s\n", iface.Network, area)) if iface.Passive && iface.Interface != "" { sb.WriteString(fmt.Sprintf(" passive-interface %s\n", iface.Interface)) } } sb.WriteString("!\n") } // 4. Routing Filters (Prefix lists, AS-Path ACLs, Community lists, Route Maps) chainRules := make(map[string][]RoutingFilterRule) for _, rule := range m.config.RoutingFilters { if rule.Disabled || strings.TrimSpace(rule.Chain) == "" { continue } chain := strings.TrimSpace(rule.Chain) chainRules[chain] = append(chainRules[chain], rule) } for chain, rules := range chainRules { // Sort rules by RuleOrder sort.Slice(rules, func(i, j int) bool { return rules[i].RuleOrder < rules[j].RuleOrder }) safeChain := strings.ReplaceAll(chain, "-", "_") for _, rule := range rules { order := rule.RuleOrder if order <= 0 { order = 10 } act := "permit" if rule.Action == "discard" || rule.Action == "reject" { act = "deny" } // Generate Prefix List if prefix specified pfxName := fmt.Sprintf("pfx_%s_%d", safeChain, order) if rule.Prefix != "" { pfxClause := formatPrefixListClause(rule.Action, rule.Prefix, rule.PrefixLen) sb.WriteString(fmt.Sprintf("ip prefix-list %s seq 5 %s\n", pfxName, pfxClause)) } // Generate AS-Path ACL if specified asPathName := fmt.Sprintf("asp_%s_%d", safeChain, order) if rule.MatchASPath != "" { sb.WriteString(fmt.Sprintf("bgp as-path access-list %s permit %s\n", asPathName, rule.MatchASPath)) } // Generate Community List if specified commName := fmt.Sprintf("comm_%s_%d", safeChain, order) if rule.MatchCommunity != "" { sb.WriteString(fmt.Sprintf("bgp community-list standard %s permit %s\n", commName, rule.MatchCommunity)) } // Generate Route-Map sb.WriteString(fmt.Sprintf("route-map %s %s %d\n", chain, act, order)) if rule.Prefix != "" { sb.WriteString(fmt.Sprintf(" match ip address prefix-list %s\n", pfxName)) } if rule.MatchASPath != "" { sb.WriteString(fmt.Sprintf(" match as-path %s\n", asPathName)) } if rule.MatchCommunity != "" { sb.WriteString(fmt.Sprintf(" match community %s\n", commName)) } // Set modifications if action is permit/accept or passthrough if act == "permit" { if rule.SetLocalPref > 0 { sb.WriteString(fmt.Sprintf(" set local-preference %d\n", rule.SetLocalPref)) } if rule.SetMetric > 0 { sb.WriteString(fmt.Sprintf(" set metric %d\n", rule.SetMetric)) } if rule.SetASPrepend > 0 { sb.WriteString(fmt.Sprintf(" set as-path prepend last-as %d\n", rule.SetASPrepend)) } if rule.SetCommunity != "" { sb.WriteString(fmt.Sprintf(" set community %s\n", rule.SetCommunity)) } if rule.SetNextHop != "" { sb.WriteString(fmt.Sprintf(" set ip next-hop %s\n", rule.SetNextHop)) } } sb.WriteString("!\n") } } sb.WriteString("end\n") // Write frr.conf if err := os.WriteFile(frrConfigPath, []byte(sb.String()), 0640); err != nil { return fmt.Errorf("failed to write %s: %w", frrConfigPath, err) } // Change ownership to frr:frr _ = exec.Command("chown", "frr:frr", frrConfigPath).Run() // Reload FRR using vtysh or systemctl out, err := exec.Command("vtysh", "-f", frrConfigPath).CombinedOutput() if err != nil { log.Printf("[DynamicRouting] vtysh -f reload warning: %v, output: %s", err, string(out)) // fallback to systemctl reload frr _ = exec.Command("systemctl", "reload", "frr").Run() } return nil } func vtyshExec(cmd string) ([]byte, error) { return exec.Command("vtysh", "-c", cmd).Output() } // ------------------------------------------------------------- // BGP API & Operations // ------------------------------------------------------------- func (m *Manager) GetBGPInstance() BGPInstance { m.mu.RLock() defer m.mu.RUnlock() inst := m.config.BGPInstance // Check if BGP is currently running in FRR out, err := vtyshExec("show ip bgp summary json") if err == nil && len(out) > 5 && !strings.Contains(string(out), "warning") { inst.Running = true } return inst } func (m *Manager) UpdateBGPInstance(inst BGPInstance) error { m.mu.Lock() m.config.BGPInstance = inst _ = m.save() m.mu.Unlock() return m.applyFRRConfig() } func (m *Manager) ListBGPPeers() []BGPPeer { m.mu.RLock() defer m.mu.RUnlock() // Make a copy of configured peers peers := make([]BGPPeer, len(m.config.BGPPeers)) copy(peers, m.config.BGPPeers) // Fetch live peer telemetry from vtysh out, err := vtyshExec("show ip bgp summary json") if err == nil { var summary struct { IPv4Unicast struct { Peers map[string]struct { RemoteAs uint32 `json:"remoteAs"` State string `json:"state"` PeerUptime string `json:"peerUptime"` PfxRcd int `json:"pfxRcd"` PfxSnt int `json:"pfxSnt"` } `json:"peers"` } `json:"ipv4Unicast"` } if err := json.Unmarshal(out, &summary); err == nil && summary.IPv4Unicast.Peers != nil { for i := range peers { if live, ok := summary.IPv4Unicast.Peers[peers[i].RemoteAddress]; ok { peers[i].State = live.State peers[i].Uptime = live.PeerUptime peers[i].PrefixReceived = live.PfxRcd peers[i].PrefixSent = live.PfxSnt } else if peers[i].Disabled { peers[i].State = "Disabled" } else { peers[i].State = "Idle" } } } } return peers } func (m *Manager) SaveBGPPeer(peer BGPPeer) error { m.mu.Lock() defer m.mu.Unlock() if peer.ID == "" { peer.ID = fmt.Sprintf("bgp-peer-%d", time.Now().UnixNano()) } found := false for i, p := range m.config.BGPPeers { if p.ID == peer.ID { m.config.BGPPeers[i] = peer found = true break } } if !found { m.config.BGPPeers = append(m.config.BGPPeers, peer) } if err := m.save(); err != nil { return err } return m.applyFRRConfig() } func (m *Manager) DeleteBGPPeer(id string) error { m.mu.Lock() defer m.mu.Unlock() newPeers := make([]BGPPeer, 0, len(m.config.BGPPeers)) for _, p := range m.config.BGPPeers { if p.ID != id { newPeers = append(newPeers, p) } } m.config.BGPPeers = newPeers if err := m.save(); err != nil { return err } return m.applyFRRConfig() } func (m *Manager) SetBGPPeerState(id string, disabled bool) error { m.mu.Lock() defer m.mu.Unlock() for i := range m.config.BGPPeers { if m.config.BGPPeers[i].ID == id { m.config.BGPPeers[i].Disabled = disabled break } } if err := m.save(); err != nil { return err } return m.applyFRRConfig() } func (m *Manager) ListBGPNetworks() []BGPNetwork { m.mu.RLock() defer m.mu.RUnlock() return m.config.BGPNetworks } func (m *Manager) SaveBGPNetwork(net BGPNetwork) error { m.mu.Lock() defer m.mu.Unlock() if net.ID == "" { net.ID = fmt.Sprintf("bgp-net-%d", time.Now().UnixNano()) } found := false for i, n := range m.config.BGPNetworks { if n.ID == net.ID { m.config.BGPNetworks[i] = net found = true break } } if !found { m.config.BGPNetworks = append(m.config.BGPNetworks, net) } if err := m.save(); err != nil { return err } return m.applyFRRConfig() } func (m *Manager) DeleteBGPNetwork(id string) error { m.mu.Lock() defer m.mu.Unlock() newNets := make([]BGPNetwork, 0, len(m.config.BGPNetworks)) for _, n := range m.config.BGPNetworks { if n.ID != id { newNets = append(newNets, n) } } m.config.BGPNetworks = newNets if err := m.save(); err != nil { return err } return m.applyFRRConfig() } func (m *Manager) GetBGPRoutes() []BGPRoute { var routes []BGPRoute out, err := vtyshExec("show ip bgp json") if err != nil { return routes } var parsed struct { Routes map[string][]struct { Metric int `json:"metric"` Weight int `json:"weight"` Path string `json:"path"` Origin string `json:"origin"` Nexthops []struct { IP string `json:"ip"` Used bool `json:"used"` } `json:"nexthops"` } `json:"routes"` } if err := json.Unmarshal(out, &parsed); err == nil && parsed.Routes != nil { for prefix, items := range parsed.Routes { for _, item := range items { nh := "" if len(item.Nexthops) > 0 { nh = item.Nexthops[0].IP } routes = append(routes, BGPRoute{ Prefix: prefix, NextHop: nh, Metric: item.Metric, Weight: item.Weight, Path: item.Path, Origin: item.Origin, Best: true, }) } } } return routes } // ------------------------------------------------------------- // OSPF API & Operations // ------------------------------------------------------------- func (m *Manager) GetOSPFInstance() OSPFInstance { m.mu.RLock() defer m.mu.RUnlock() inst := m.config.OSPFInstance out, err := vtyshExec("show ip ospf json") if err == nil && len(out) > 5 && !strings.Contains(string(out), "warning") { inst.Running = true } return inst } func (m *Manager) UpdateOSPFInstance(inst OSPFInstance) error { m.mu.Lock() m.config.OSPFInstance = inst _ = m.save() m.mu.Unlock() return m.applyFRRConfig() } func (m *Manager) ListOSPFAreas() []OSPFArea { m.mu.RLock() defer m.mu.RUnlock() return m.config.OSPFAreas } func (m *Manager) SaveOSPFArea(area OSPFArea) error { m.mu.Lock() defer m.mu.Unlock() if area.ID == "" { area.ID = fmt.Sprintf("area-%d", time.Now().UnixNano()) } found := false for i, a := range m.config.OSPFAreas { if a.ID == area.ID { m.config.OSPFAreas[i] = area found = true break } } if !found { m.config.OSPFAreas = append(m.config.OSPFAreas, area) } if err := m.save(); err != nil { return err } return m.applyFRRConfig() } func (m *Manager) DeleteOSPFArea(id string) error { m.mu.Lock() defer m.mu.Unlock() newAreas := make([]OSPFArea, 0, len(m.config.OSPFAreas)) for _, a := range m.config.OSPFAreas { if a.ID != id { newAreas = append(newAreas, a) } } m.config.OSPFAreas = newAreas if err := m.save(); err != nil { return err } return m.applyFRRConfig() } func (m *Manager) ListOSPFInterfaces() []OSPFInterface { m.mu.RLock() defer m.mu.RUnlock() return m.config.OSPFInterfaces } func (m *Manager) SaveOSPFInterface(iface OSPFInterface) error { m.mu.Lock() defer m.mu.Unlock() if iface.ID == "" { iface.ID = fmt.Sprintf("ospf-iface-%d", time.Now().UnixNano()) } if iface.Cost <= 0 { iface.Cost = 10 } if iface.Priority <= 0 { iface.Priority = 1 } if iface.HelloInterval <= 0 { iface.HelloInterval = 10 } if iface.DeadInterval <= 0 { iface.DeadInterval = 40 } found := false for i, item := range m.config.OSPFInterfaces { if item.ID == iface.ID { m.config.OSPFInterfaces[i] = iface found = true break } } if !found { m.config.OSPFInterfaces = append(m.config.OSPFInterfaces, iface) } if err := m.save(); err != nil { return err } return m.applyFRRConfig() } func (m *Manager) DeleteOSPFInterface(id string) error { m.mu.Lock() defer m.mu.Unlock() newInterfaces := make([]OSPFInterface, 0, len(m.config.OSPFInterfaces)) for _, item := range m.config.OSPFInterfaces { if item.ID != id { newInterfaces = append(newInterfaces, item) } } m.config.OSPFInterfaces = newInterfaces if err := m.save(); err != nil { return err } return m.applyFRRConfig() } func (m *Manager) SetOSPFInterfaceState(id string, disabled bool) error { m.mu.Lock() defer m.mu.Unlock() for i := range m.config.OSPFInterfaces { if m.config.OSPFInterfaces[i].ID == id { m.config.OSPFInterfaces[i].Disabled = disabled break } } if err := m.save(); err != nil { return err } return m.applyFRRConfig() } func (m *Manager) GetOSPFNeighbors() []OSPFNeighbor { var neighbors []OSPFNeighbor out, err := vtyshExec("show ip ospf neighbor json") if err != nil { return neighbors } var parsed struct { Neighbors map[string][]struct { NbrState string `json:"nbrState"` Address string `json:"address"` IfaceName string `json:"ifaceName"` Area string `json:"area"` DeadTime string `json:"deadTime"` } `json:"neighbors"` } if err := json.Unmarshal(out, &parsed); err == nil && parsed.Neighbors != nil { for nbrID, entries := range parsed.Neighbors { for _, entry := range entries { neighbors = append(neighbors, OSPFNeighbor{ NeighborID: nbrID, Address: entry.Address, Interface: entry.IfaceName, Area: entry.Area, State: entry.NbrState, DeadTimer: entry.DeadTime, }) } } } return neighbors } func (m *Manager) GetOSPFRoutes() []OSPFRoute { var routes []OSPFRoute out, err := vtyshExec("show ip ospf route json") if err != nil { return routes } var parsed map[string]struct { RouteType string `json:"routeType"` Cost int `json:"cost"` Area string `json:"area"` Nexthops []struct { IP string `json:"ip"` DirectlyAttachedTo string `json:"directlyAttachedTo"` } `json:"nexthops"` } if err := json.Unmarshal(out, &parsed); err == nil { for prefix, r := range parsed { nh := "" dev := "" if len(r.Nexthops) > 0 { nh = strings.TrimSpace(r.Nexthops[0].IP) dev = r.Nexthops[0].DirectlyAttachedTo } routes = append(routes, OSPFRoute{ Prefix: prefix, RouteType: r.RouteType, Cost: r.Cost, Area: r.Area, NextHop: nh, Interface: dev, }) } } return routes } // ------------------------------------------------------------- // Summary Telemetry // ------------------------------------------------------------- func (m *Manager) GetSummary() DynamicRoutingSummary { m.mu.RLock() defer m.mu.RUnlock() summary := DynamicRoutingSummary{ BGPEnabled: m.config.BGPInstance.Enabled, OSPFEnabled: m.config.OSPFInstance.Enabled, } // Check BGP status if summary.BGPEnabled { out, err := vtyshExec("show ip bgp summary json") if err == nil && len(out) > 5 && !strings.Contains(string(out), "warning") { summary.BGPRunning = true var bgpParsed struct { IPv4Unicast struct { PeerCount int `json:"peerCount"` Peers map[string]struct { State string `json:"state"` } `json:"peers"` } `json:"ipv4Unicast"` } if err := json.Unmarshal(out, &bgpParsed); err == nil { summary.BGPPeerCount = len(m.config.BGPPeers) for _, p := range bgpParsed.IPv4Unicast.Peers { if p.State == "Established" { summary.BGPEstablishedCount++ } } } } } // Check OSPF status if summary.OSPFEnabled { out, err := vtyshExec("show ip ospf json") if err == nil && len(out) > 5 && !strings.Contains(string(out), "warning") { summary.OSPFRunning = true } nbrs := m.GetOSPFNeighbors() summary.OSPFNeighborCount = len(nbrs) for _, n := range nbrs { if strings.HasPrefix(n.State, "Full") { summary.OSPFAdjacentCount++ } } } summary.RoutingFilterCount = len(m.config.RoutingFilters) return summary } func formatPrefixListClause(action, prefix, prefixLen string) string { act := "permit" if action == "discard" || action == "reject" { act = "deny" } clause := fmt.Sprintf("%s %s", act, prefix) prefixLen = strings.TrimSpace(prefixLen) if prefixLen != "" { if strings.Contains(prefixLen, "-") { parts := strings.Split(prefixLen, "-") if len(parts) == 2 { clause += fmt.Sprintf(" ge %s le %s", strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1])) } } else if strings.HasPrefix(prefixLen, "ge ") || strings.HasPrefix(prefixLen, "le ") { clause += " " + prefixLen } else { clause += fmt.Sprintf(" ge %s le %s", prefixLen, prefixLen) } } return clause } // ------------------------------------------------------------- // Routing Filter API & Operations (/routing filter) // ------------------------------------------------------------- func (m *Manager) ListRoutingFilters() []RoutingFilterRule { m.mu.RLock() defer m.mu.RUnlock() filters := make([]RoutingFilterRule, len(m.config.RoutingFilters)) copy(filters, m.config.RoutingFilters) return filters } func (m *Manager) SaveRoutingFilter(rule *RoutingFilterRule) error { m.mu.Lock() if rule.ID == "" { rule.ID = fmt.Sprintf("rf-%d", time.Now().UnixNano()/1e6) } if rule.RuleOrder <= 0 { rule.RuleOrder = (len(m.config.RoutingFilters) + 1) * 10 } found := false for i, r := range m.config.RoutingFilters { if r.ID == rule.ID { m.config.RoutingFilters[i] = *rule found = true break } } if !found { m.config.RoutingFilters = append(m.config.RoutingFilters, *rule) } _ = m.save() m.mu.Unlock() return m.applyFRRConfig() } func (m *Manager) DeleteRoutingFilter(id string) error { m.mu.Lock() newRules := make([]RoutingFilterRule, 0, len(m.config.RoutingFilters)) for _, r := range m.config.RoutingFilters { if r.ID != id { newRules = append(newRules, r) } } m.config.RoutingFilters = newRules _ = m.save() m.mu.Unlock() return m.applyFRRConfig() } func (m *Manager) SetRoutingFilterState(id string, disabled bool) error { m.mu.Lock() for i := range m.config.RoutingFilters { if m.config.RoutingFilters[i].ID == id { m.config.RoutingFilters[i].Disabled = disabled break } } _ = m.save() m.mu.Unlock() return m.applyFRRConfig() }