Sfoglia il codice sorgente

feat(bgp): implement RouterOS v7 BGP Connection window with 4 tabs and dynamic output networks with + and -

Proxmox Router 1 settimana fa
parent
commit
775b6a39d1

+ 1 - 1
internal/api/dynamic_routing_handlers.go

@@ -53,7 +53,7 @@ func (s *Server) handleSaveBGPPeer(w http.ResponseWriter, r *http.Request) {
 		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "remote_address and remote_as are required"})
 		return
 	}
-	if err := s.dynRoutingMgr.SaveBGPPeer(peer); err != nil {
+	if err := s.dynRoutingMgr.SaveBGPPeer(&peer); err != nil {
 		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
 		return
 	}

+ 58 - 13
internal/dynamicrouting/manager.go

@@ -273,6 +273,30 @@ func (m *Manager) applyBirdConfig() error {
 			}
 
 			protoName := sanitizeIdentifier("bgp", fmt.Sprintf("%s_%d", peer.ID, i))
+
+			// Per-peer Output Networks (RouterOS v7 output.network)
+			var peerNets []string
+			for _, on := range peer.OutputNetworks {
+				on = strings.TrimSpace(on)
+				if on != "" {
+					peerNets = append(peerNets, on)
+				}
+			}
+			if peer.DefaultOriginate {
+				peerNets = append(peerNets, "0.0.0.0/0")
+			}
+
+			peerNetsProto := ""
+			if len(peerNets) > 0 {
+				peerNetsProto = fmt.Sprintf("bgp_nets_%s", protoName)
+				sb.WriteString(fmt.Sprintf("protocol static %s {\n", peerNetsProto))
+				sb.WriteString("    ipv4;\n")
+				for _, pn := range peerNets {
+					sb.WriteString(fmt.Sprintf("    route %s blackhole;\n", pn))
+				}
+				sb.WriteString("}\n\n")
+			}
+
 			sb.WriteString(fmt.Sprintf("protocol bgp %s {\n", protoName))
 			if peer.Name != "" {
 				sb.WriteString(fmt.Sprintf("    description \"%s\";\n", peer.Name))
@@ -282,16 +306,29 @@ func (m *Manager) applyBirdConfig() error {
 			}
 
 			if peer.LocalAddress != "" {
-				sb.WriteString(fmt.Sprintf("    local %s as %d;\n", peer.LocalAddress, localAS))
+				if peer.LocalPort > 0 {
+					sb.WriteString(fmt.Sprintf("    local %s port %d as %d;\n", peer.LocalAddress, peer.LocalPort, localAS))
+				} else {
+					sb.WriteString(fmt.Sprintf("    local %s as %d;\n", peer.LocalAddress, localAS))
+				}
+			} else {
+				if peer.LocalPort > 0 {
+					sb.WriteString(fmt.Sprintf("    local port %d as %d;\n", peer.LocalPort, localAS))
+				} else {
+					sb.WriteString(fmt.Sprintf("    local as %d;\n", localAS))
+				}
+			}
+
+			if peer.RemotePort > 0 {
+				sb.WriteString(fmt.Sprintf("    neighbor %s port %d as %d;\n", peer.RemoteAddress, peer.RemotePort, peer.RemoteAS))
 			} else {
-				sb.WriteString(fmt.Sprintf("    local as %d;\n", localAS))
+				sb.WriteString(fmt.Sprintf("    neighbor %s as %d;\n", peer.RemoteAddress, peer.RemoteAS))
 			}
-			sb.WriteString(fmt.Sprintf("    neighbor %s as %d;\n", peer.RemoteAddress, peer.RemoteAS))
 
 			if peer.Multihop {
 				sb.WriteString("    multihop;\n")
 			}
-			if peer.Passive {
+			if peer.Passive || (!peer.Connect && peer.Listen) {
 				sb.WriteString("    passive;\n")
 			}
 			if peer.Password != "" {
@@ -299,14 +336,19 @@ func (m *Manager) applyBirdConfig() error {
 			}
 
 			holdTime := 180
-			if m.config.BGPInstance.HoldTime > 0 {
+			if peer.HoldTime > 0 {
+				holdTime = peer.HoldTime
+			} else if m.config.BGPInstance.HoldTime > 0 {
 				holdTime = m.config.BGPInstance.HoldTime
 			}
-			sb.WriteString(fmt.Sprintf("    hold time %d;\n\n", holdTime))
+			sb.WriteString(fmt.Sprintf("    hold time %d;\n", holdTime))
+			if peer.KeepaliveTime > 0 {
+				sb.WriteString(fmt.Sprintf("    keepalive time %d;\n", peer.KeepaliveTime))
+			}
 
 			// IPv4 Channel
 			sb.WriteString("    ipv4 {\n")
-			if peer.NexthopSelf {
+			if peer.NexthopSelf || peer.NexthopChoice == "force-self" {
 				sb.WriteString("        next hop self;\n")
 			}
 
@@ -322,17 +364,20 @@ func (m *Manager) applyBirdConfig() error {
 				sb.WriteString(fmt.Sprintf("        export filter %s;\n", sanitizeIdentifier("filter", peer.OutFilter)))
 			} else {
 				var exportSources []string
+				if peerNetsProto != "" {
+					exportSources = append(exportSources, fmt.Sprintf(`proto = "%s"`, peerNetsProto))
+				}
 				if len(activeNets) > 0 {
 					exportSources = append(exportSources, `proto = "bgp_static_nets"`)
 				}
 				exportSources = append(exportSources, `source = RTS_BGP`)
-				if m.config.BGPInstance.RedistributeConnected {
+				if peer.RedistConnected || m.config.BGPInstance.RedistributeConnected {
 					exportSources = append(exportSources, `source = RTS_DEVICE`)
 				}
-				if m.config.BGPInstance.RedistributeStatic {
+				if peer.RedistStatic || m.config.BGPInstance.RedistributeStatic {
 					exportSources = append(exportSources, `source = RTS_STATIC`)
 				}
-				if m.config.BGPInstance.RedistributeOSPF {
+				if peer.RedistOSPF || m.config.BGPInstance.RedistributeOSPF {
 					exportSources = append(exportSources, `source = RTS_OSPF`)
 				}
 
@@ -609,7 +654,7 @@ func (m *Manager) ListBGPPeers() []BGPPeer {
 	return peers
 }
 
-func (m *Manager) SaveBGPPeer(peer BGPPeer) error {
+func (m *Manager) SaveBGPPeer(peer *BGPPeer) error {
 	m.mu.Lock()
 	defer m.mu.Unlock()
 
@@ -620,13 +665,13 @@ func (m *Manager) SaveBGPPeer(peer BGPPeer) error {
 	found := false
 	for i, p := range m.config.BGPPeers {
 		if p.ID == peer.ID {
-			m.config.BGPPeers[i] = peer
+			m.config.BGPPeers[i] = *peer
 			found = true
 			break
 		}
 	}
 	if !found {
-		m.config.BGPPeers = append(m.config.BGPPeers, peer)
+		m.config.BGPPeers = append(m.config.BGPPeers, *peer)
 	}
 
 	if err := m.save(); err != nil {

+ 35 - 14
internal/dynamicrouting/types.go

@@ -15,20 +15,41 @@ type BGPInstance struct {
 
 // BGPPeer represents a BGP peer connection (/routing bgp connection / peer)
 type BGPPeer struct {
-	ID             string `json:"id"`
-	Name           string `json:"name"`
-	RemoteAddress  string `json:"remote_address"`
-	RemoteAS       uint32 `json:"remote_as"`
-	LocalAddress   string `json:"local_address,omitempty"`
-	LocalAS        uint32 `json:"local_as,omitempty"` // Multi-Instance Local AS (if 0, uses global AS)
-	Multihop       bool   `json:"multihop"`
-	NexthopSelf    bool   `json:"nexthop_self"`
-	Passive        bool   `json:"passive"`
-	Password       string `json:"password,omitempty"`
-	Disabled       bool   `json:"disabled"`
-	Comment        string `json:"comment,omitempty"`
-	InFilter       string `json:"in_filter,omitempty"`  // Routing Filter chain for incoming routes
-	OutFilter      string `json:"out_filter,omitempty"` // Routing Filter chain for outgoing routes
+	ID               string   `json:"id"`
+	Name             string   `json:"name"`
+	Enabled          bool     `json:"enabled"`
+	Comment          string   `json:"comment,omitempty"`
+	LocalAS          uint32   `json:"local_as,omitempty"` // Multi-Instance Local AS (if 0, uses global AS)
+	RouterID         string   `json:"router_id,omitempty"`
+	AFI              string   `json:"afi,omitempty"` // "ip", "ipv6"
+	RemoteAddress    string   `json:"remote_address"`
+	RemotePort       int      `json:"remote_port,omitempty"`
+	RemoteAS         uint32   `json:"remote_as"`
+	LocalAddress     string   `json:"local_address,omitempty"`
+	LocalPort        int      `json:"local_port,omitempty"`
+	LocalRole        string   `json:"local_role,omitempty"` // ebgp, ibgp
+	Password         string   `json:"password,omitempty"`   // TCP MD5 Key
+	Multihop         bool     `json:"multihop"`
+	Connect          bool     `json:"connect"`
+	Listen           bool     `json:"listen"`
+	HoldTime         int      `json:"hold_time,omitempty"`
+	KeepaliveTime    int      `json:"keepalive_time,omitempty"`
+	RoutingTable     string   `json:"routing_table,omitempty"`
+	RedistConnected  bool     `json:"redist_connected"`
+	RedistStatic     bool     `json:"redist_static"`
+	RedistOSPF       bool     `json:"redist_ospf"`
+	RedistBGP        bool     `json:"redist_bgp"`
+	DefaultOriginate bool     `json:"default_originate"`
+	NexthopChoice    string   `json:"nexthop_choice,omitempty"` // default, force-self
+	NexthopSelf      bool     `json:"nexthop_self"`
+	DefaultPrepend   int      `json:"default_prepend,omitempty"`
+	AllowASIn        int      `json:"allow_as_in,omitempty"`
+	RemovePrivateAS  bool     `json:"remove_private_as"`
+	InFilter         string   `json:"in_filter,omitempty"`        // Routing Filter chain for incoming routes
+	OutFilter        string   `json:"out_filter,omitempty"`       // Routing Filter chain for outgoing routes
+	OutputNetworks   []string `json:"output_networks,omitempty"` // Dynamic [+] / [-] output networks
+	Disabled         bool     `json:"disabled"`
+	Passive          bool     `json:"passive"`
 	// Runtime telemetry
 	State          string `json:"state,omitempty"` // Established, Active, Connect, Idle
 	Uptime         string `json:"uptime,omitempty"`

+ 234 - 29
internal/web/static/app.js

@@ -7329,66 +7329,243 @@ async function submitBgpInstance(e) {
   }
 }
 
+// ----------------- BGP Peer Connection Modal (RouterOS v7) -----------------
+function switchBgpPeerModalTab(tab) {
+  const tabs = ['general', 'extra', 'attributes', 'filter'];
+  tabs.forEach(t => {
+    const pane = document.getElementById(`bgp-peer-tab-${t}`);
+    const btn = document.getElementById(`bgp-peer-tab-btn-${t}`);
+    if (t === tab) {
+      if (pane) pane.classList.remove('hidden');
+      if (btn) btn.className = 'px-4 py-2 text-xs font-bold border-b-2 border-cyan-500 text-white transition';
+    } else {
+      if (pane) pane.classList.add('hidden');
+      if (btn) btn.className = 'px-4 py-2 text-xs font-bold border-b-2 border-transparent text-slate-400 hover:text-slate-200 transition';
+    }
+  });
+}
+
+function addOutputNetworkRow(val = '') {
+  const container = document.getElementById('modal-bgp-output-networks-container');
+  if (!container) return;
+  const row = document.createElement('div');
+  row.className = 'flex items-center gap-2 output-net-row';
+  row.innerHTML = `
+    <input type="text" value="${escapeHtml(val)}" placeholder="e.g. 10.2.51.0/24 or BGP-AWMN" class="flex-1 bg-slate-900 border border-slate-700 rounded-lg px-3 py-1.5 text-white font-mono text-xs focus:outline-none focus:border-cyan-500">
+    <button type="button" onclick="this.closest('.output-net-row').remove()" class="w-7 h-7 flex items-center justify-center bg-slate-800 hover:bg-rose-950 text-slate-400 hover:text-rose-400 border border-slate-700 rounded-lg text-base font-bold transition" title="Remove network">-</button>
+  `;
+  container.appendChild(row);
+}
+
+function parseBgpTime(val) {
+  if (!val) return 0;
+  val = String(val).trim();
+  if (val.includes(':')) {
+    const parts = val.split(':').map(x => parseInt(x, 10) || 0);
+    if (parts.length === 3) return parts[0] * 3600 + parts[1] * 60 + parts[2];
+    if (parts.length === 2) return parts[0] * 60 + parts[1];
+  }
+  return parseInt(val, 10) || 0;
+}
+
+function formatBgpTime(secs) {
+  if (!secs || secs <= 0) return '00:00:00';
+  const h = Math.floor(secs / 3600);
+  const m = Math.floor((secs % 3600) / 60);
+  const s = secs % 60;
+  return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`;
+}
+
+function populateBgpRoutingTables() {
+  const sel = document.getElementById('modal-bgp-peer-routing-table');
+  if (!sel) return;
+  const cur = sel.value || 'main';
+  sel.innerHTML = '<option value="main">main</option>';
+  (state.routingTables || []).forEach(t => {
+    if (t.name !== 'main') {
+      const opt = document.createElement('option');
+      opt.value = t.name;
+      opt.innerText = t.name;
+      sel.appendChild(opt);
+    }
+  });
+  sel.value = cur;
+}
+
 function openAddBgpPeerModal() {
   document.getElementById('bgp-peer-form').reset();
   document.getElementById('modal-bgp-peer-id').value = '';
-  document.getElementById('bgp-peer-modal-title').innerText = 'Add BGP Peer Connection';
-  document.getElementById('modal-bgp-peer-nexthop-self').checked = true;
-  const localAsEl = document.getElementById('modal-bgp-peer-local-as');
-  if (localAsEl) localAsEl.value = '';
-  const inFilterEl = document.getElementById('modal-bgp-peer-in-filter');
-  if (inFilterEl) inFilterEl.value = '';
-  const outFilterEl = document.getElementById('modal-bgp-peer-out-filter');
-  if (outFilterEl) outFilterEl.value = '';
+  document.getElementById('bgp-peer-modal-title').innerText = 'New...';
+  
+  document.getElementById('modal-bgp-peer-enabled').checked = true;
+  document.getElementById('modal-bgp-peer-afi-ip').checked = true;
+  document.getElementById('modal-bgp-peer-afi-ipv6').checked = false;
+  document.getElementById('modal-bgp-peer-connect').checked = true;
+  document.getElementById('modal-bgp-peer-listen').checked = true;
+  document.getElementById('modal-bgp-peer-holdtime').value = '00:03:00';
+  document.getElementById('modal-bgp-peer-keepalive').value = '00:01:00';
+  document.getElementById('modal-bgp-peer-nexthop-choice').value = 'force-self';
+  document.getElementById('modal-bgp-peer-redist-conn').checked = false;
+  document.getElementById('modal-bgp-peer-redist-static').checked = true;
+  document.getElementById('modal-bgp-peer-redist-ospf').checked = false;
+  document.getElementById('modal-bgp-peer-redist-bgp').checked = false;
+  document.getElementById('modal-bgp-peer-default-orig').checked = false;
+  
+  populateBgpRoutingTables();
+
+  const container = document.getElementById('modal-bgp-output-networks-container');
+  if (container) {
+    container.innerHTML = '';
+    addOutputNetworkRow('');
+  }
+
+  const removeBtn = document.getElementById('modal-btn-remove-peer');
+  if (removeBtn) removeBtn.classList.add('hidden');
+
+  switchBgpPeerModalTab('general');
   document.getElementById('bgp-peer-modal').classList.remove('hidden');
 }
 
 function openEditBgpPeerModal(id) {
   const p = (state.bgpPeers || []).find(item => item.id === id);
   if (!p) return;
+
+  populateBgpRoutingTables();
+
   document.getElementById('modal-bgp-peer-id').value = p.id;
+  document.getElementById('bgp-peer-modal-title').innerText = p.name || p.id;
+  
+  // General Tab
+  document.getElementById('modal-bgp-peer-enabled').checked = p.enabled !== false && !p.disabled;
+  document.getElementById('modal-bgp-peer-comment').value = p.comment || '';
   document.getElementById('modal-bgp-peer-name').value = p.name || '';
+  document.getElementById('modal-bgp-peer-local-as').value = p.local_as || '';
+  document.getElementById('modal-bgp-peer-router-id').value = p.router_id || '';
+  document.getElementById('modal-bgp-peer-afi-ip').checked = true;
+  document.getElementById('modal-bgp-peer-afi-ipv6').checked = p.afi === 'ipv6';
   document.getElementById('modal-bgp-peer-remote-addr').value = p.remote_address || '';
+  document.getElementById('modal-bgp-peer-remote-port').value = p.remote_port || '';
   document.getElementById('modal-bgp-peer-remote-as').value = p.remote_as || '';
-  const localAsEl = document.getElementById('modal-bgp-peer-local-as');
-  if (localAsEl) localAsEl.value = p.local_as || '';
   document.getElementById('modal-bgp-peer-local-addr').value = p.local_address || '';
+  document.getElementById('modal-bgp-peer-local-port').value = p.local_port || '';
+  document.getElementById('modal-bgp-peer-local-role').value = p.local_role || 'ebgp';
   document.getElementById('modal-bgp-peer-password').value = p.password || '';
   document.getElementById('modal-bgp-peer-multihop').checked = !!p.multihop;
-  document.getElementById('modal-bgp-peer-nexthop-self').checked = !!p.nexthop_self;
-  document.getElementById('modal-bgp-peer-passive').checked = !!p.passive;
-  document.getElementById('modal-bgp-peer-disabled').checked = !!p.disabled;
-  document.getElementById('modal-bgp-peer-comment').value = p.comment || '';
-  const inFilterEl = document.getElementById('modal-bgp-peer-in-filter');
-  if (inFilterEl) inFilterEl.value = p.in_filter || '';
-  const outFilterEl = document.getElementById('modal-bgp-peer-out-filter');
-  if (outFilterEl) outFilterEl.value = p.out_filter || '';
-  document.getElementById('bgp-peer-modal-title').innerText = 'Edit BGP Peer Connection';
+  document.getElementById('modal-bgp-peer-connect').checked = p.connect !== false;
+  document.getElementById('modal-bgp-peer-listen').checked = p.listen !== false;
+
+  // Extra Tab
+  document.getElementById('modal-bgp-peer-holdtime').value = p.hold_time ? formatBgpTime(p.hold_time) : '00:03:00';
+  document.getElementById('modal-bgp-peer-keepalive').value = p.keepalive_time ? formatBgpTime(p.keepalive_time) : '00:01:00';
+  document.getElementById('modal-bgp-peer-routing-table').value = p.routing_table || 'main';
+  document.getElementById('modal-bgp-peer-redist-conn').checked = !!p.redist_connected;
+  document.getElementById('modal-bgp-peer-redist-static').checked = p.redist_static !== false;
+  document.getElementById('modal-bgp-peer-redist-ospf').checked = !!p.redist_ospf;
+  document.getElementById('modal-bgp-peer-redist-bgp').checked = !!p.redist_bgp;
+  document.getElementById('modal-bgp-peer-default-orig').checked = !!p.default_originate;
+
+  // Attributes Tab
+  document.getElementById('modal-bgp-peer-nexthop-choice').value = p.nexthop_choice || (p.nexthop_self ? 'force-self' : 'default');
+  document.getElementById('modal-bgp-peer-default-prepend').value = p.default_prepend || 0;
+  document.getElementById('modal-bgp-peer-allow-as-in').value = p.allow_as_in || 0;
+  document.getElementById('modal-bgp-peer-remove-priv-as').checked = !!p.remove_private_as;
+  document.getElementById('modal-bgp-peer-as-override').checked = !!p.as_override;
+
+  // Filter Tab
+  document.getElementById('modal-bgp-peer-in-filter').value = p.in_filter || '';
+  document.getElementById('modal-bgp-peer-out-filter').value = p.out_filter || '';
+
+  // Output Networks
+  const container = document.getElementById('modal-bgp-output-networks-container');
+  if (container) {
+    container.innerHTML = '';
+    if (p.output_networks && p.output_networks.length > 0) {
+      p.output_networks.forEach(net => addOutputNetworkRow(net));
+    } else {
+      addOutputNetworkRow('');
+    }
+  }
+
+  const removeBtn = document.getElementById('modal-btn-remove-peer');
+  if (removeBtn) removeBtn.classList.remove('hidden');
+
+  switchBgpPeerModalTab('general');
   document.getElementById('bgp-peer-modal').classList.remove('hidden');
 }
 
+function copyCurrentBgpPeer() {
+  document.getElementById('modal-bgp-peer-id').value = '';
+  const nameEl = document.getElementById('modal-bgp-peer-name');
+  if (nameEl) nameEl.value = (nameEl.value || 'peer') + '-copy';
+  document.getElementById('bgp-peer-modal-title').innerText = 'New...';
+  const removeBtn = document.getElementById('modal-btn-remove-peer');
+  if (removeBtn) removeBtn.classList.add('hidden');
+  showToast('Cloned', 'BGP connection copied to new draft', 'info');
+}
+
+function removeCurrentBgpPeer() {
+  const id = document.getElementById('modal-bgp-peer-id')?.value;
+  if (!id) return;
+  deleteBgpPeer(id);
+  closeBgpPeerModal();
+}
+
 function closeBgpPeerModal() {
   document.getElementById('bgp-peer-modal').classList.add('hidden');
 }
 
-async function submitBgpPeer(e) {
-  e.preventDefault();
-  const payload = {
+function getBgpPeerFormData() {
+  const netInputs = document.querySelectorAll('#modal-bgp-output-networks-container .output-net-row input');
+  const outputNetworks = [];
+  netInputs.forEach(inp => {
+    const v = inp.value.trim();
+    if (v) outputNetworks.push(v);
+  });
+
+  const enabled = document.getElementById('modal-bgp-peer-enabled').checked;
+
+  return {
     id: document.getElementById('modal-bgp-peer-id').value,
     name: document.getElementById('modal-bgp-peer-name').value.trim(),
-    remote_address: document.getElementById('modal-bgp-peer-remote-addr').value.trim(),
-    remote_as: parseInt(document.getElementById('modal-bgp-peer-remote-as').value, 10),
+    enabled: enabled,
+    disabled: !enabled,
+    comment: document.getElementById('modal-bgp-peer-comment').value.trim(),
     local_as: parseInt(document.getElementById('modal-bgp-peer-local-as')?.value, 10) || 0,
+    router_id: document.getElementById('modal-bgp-peer-router-id').value.trim(),
+    afi: document.getElementById('modal-bgp-peer-afi-ipv6').checked ? 'ipv6' : 'ip',
+    remote_address: document.getElementById('modal-bgp-peer-remote-addr').value.trim(),
+    remote_port: parseInt(document.getElementById('modal-bgp-peer-remote-port').value, 10) || 0,
+    remote_as: parseInt(document.getElementById('modal-bgp-peer-remote-as').value, 10) || 0,
     local_address: document.getElementById('modal-bgp-peer-local-addr').value.trim(),
+    local_port: parseInt(document.getElementById('modal-bgp-peer-local-port').value, 10) || 0,
+    local_role: document.getElementById('modal-bgp-peer-local-role').value,
     password: document.getElementById('modal-bgp-peer-password').value,
     multihop: document.getElementById('modal-bgp-peer-multihop').checked,
-    nexthop_self: document.getElementById('modal-bgp-peer-nexthop-self').checked,
-    passive: document.getElementById('modal-bgp-peer-passive').checked,
-    disabled: document.getElementById('modal-bgp-peer-disabled').checked,
+    connect: document.getElementById('modal-bgp-peer-connect').checked,
+    listen: document.getElementById('modal-bgp-peer-listen').checked,
+    hold_time: parseBgpTime(document.getElementById('modal-bgp-peer-holdtime').value),
+    keepalive_time: parseBgpTime(document.getElementById('modal-bgp-peer-keepalive').value),
+    routing_table: document.getElementById('modal-bgp-peer-routing-table').value,
+    redist_connected: document.getElementById('modal-bgp-peer-redist-conn').checked,
+    redist_static: document.getElementById('modal-bgp-peer-redist-static').checked,
+    redist_ospf: document.getElementById('modal-bgp-peer-redist-ospf').checked,
+    redist_bgp: document.getElementById('modal-bgp-peer-redist-bgp').checked,
+    default_originate: document.getElementById('modal-bgp-peer-default-orig').checked,
+    nexthop_choice: document.getElementById('modal-bgp-peer-nexthop-choice').value,
+    nexthop_self: document.getElementById('modal-bgp-peer-nexthop-choice').value === 'force-self',
+    default_prepend: parseInt(document.getElementById('modal-bgp-peer-default-prepend').value, 10) || 0,
+    allow_as_in: parseInt(document.getElementById('modal-bgp-peer-allow-as-in').value, 10) || 0,
+    remove_private_as: document.getElementById('modal-bgp-peer-remove-priv-as').checked,
+    as_override: document.getElementById('modal-bgp-peer-as-override').checked,
     in_filter: (document.getElementById('modal-bgp-peer-in-filter')?.value || '').trim(),
     out_filter: (document.getElementById('modal-bgp-peer-out-filter')?.value || '').trim(),
-    comment: document.getElementById('modal-bgp-peer-comment').value.trim()
+    output_networks: outputNetworks
   };
+}
+
+async function submitBgpPeer(e) {
+  if (e) e.preventDefault();
+  const payload = getBgpPeerFormData();
 
   try {
     const res = await fetch(`${API_BASE}/routing/bgp/peers`, {
@@ -7398,7 +7575,7 @@ async function submitBgpPeer(e) {
     });
     if (res.ok) {
       closeBgpPeerModal();
-      showToast('Success', 'BGP Peer saved', 'success');
+      showToast('Success', 'BGP Connection saved', 'success');
       loadDynamicRouting();
     } else {
       const err = await res.json();
@@ -7409,6 +7586,34 @@ async function submitBgpPeer(e) {
   }
 }
 
+async function applyBgpPeerModal(e) {
+  if (e) e.preventDefault();
+  const payload = getBgpPeerFormData();
+
+  try {
+    const res = await fetch(`${API_BASE}/routing/bgp/peers`, {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify(payload)
+    });
+    if (res.ok) {
+      const data = await res.json();
+      if (data.peer && data.peer.id) {
+        document.getElementById('modal-bgp-peer-id').value = data.peer.id;
+        document.getElementById('bgp-peer-modal-title').innerText = data.peer.name || data.peer.id;
+        document.getElementById('modal-btn-remove-peer')?.classList.remove('hidden');
+      }
+      showToast('Applied', 'BGP Connection applied', 'success');
+      loadDynamicRouting();
+    } else {
+      const err = await res.json();
+      showToast('Error', err.error || 'Failed to apply peer', 'error');
+    }
+  } catch (err) {
+    showToast('Error', err.message, 'error');
+  }
+}
+
 async function deleteBgpPeer(id) {
   if (!confirm('Are you sure you want to delete this BGP peer connection?')) return;
   try {

+ 324 - 73
internal/web/static/index.html

@@ -7001,89 +7001,340 @@
     </div>
   </div>
 
-  <!-- ================= MODAL: BGP PEER ================= -->
+  <!-- ================= MODAL: BGP CONNECTION (RouterOS v7 Style) ================= -->
   <div id="bgp-peer-modal" class="fixed inset-0 bg-slate-950/80 backdrop-blur-sm z-50 hidden flex items-center justify-center p-4">
-    <div class="bg-slate-900 border border-slate-800 rounded-2xl w-full max-w-lg shadow-2xl overflow-hidden animate-in">
-      <div class="px-6 py-4 border-b border-slate-800 flex items-center justify-between bg-slate-950/40">
-        <div>
-          <h3 id="bgp-peer-modal-title" class="text-sm font-bold text-white">Add BGP Peer Connection</h3>
-          <p class="text-[11px] text-slate-400">Establish eBGP / iBGP session with remote router</p>
-        </div>
-        <button onclick="closeBgpPeerModal()" class="text-slate-400 hover:text-white p-1 rounded-lg">✕</button>
+    <div class="bg-slate-900 border border-slate-800 rounded-2xl w-full max-w-4xl shadow-2xl overflow-hidden animate-in flex flex-col max-h-[90vh]">
+      <!-- Header -->
+      <div class="px-6 py-3.5 border-b border-slate-800 flex items-center justify-between bg-slate-950/60">
+        <div class="flex items-center gap-2.5">
+          <span class="text-cyan-400 text-sm">🔀</span>
+          <span class="text-xs font-semibold text-slate-400">BGP Connection &gt;</span>
+          <span id="bgp-peer-modal-title" class="text-xs font-bold text-cyan-300 font-mono">New...</span>
+        </div>
+        <button type="button" onclick="closeBgpPeerModal()" class="text-slate-400 hover:text-white p-1 rounded-lg">✕</button>
+      </div>
+
+      <!-- Tab Navigation -->
+      <div class="flex border-b border-slate-800 bg-slate-950/30 px-6 gap-2">
+        <button type="button" id="bgp-peer-tab-btn-general" onclick="switchBgpPeerModalTab('general')" class="px-4 py-2 text-xs font-bold border-b-2 border-cyan-500 text-white transition">General</button>
+        <button type="button" id="bgp-peer-tab-btn-extra" onclick="switchBgpPeerModalTab('extra')" class="px-4 py-2 text-xs font-bold border-b-2 border-transparent text-slate-400 hover:text-slate-200 transition">Extra</button>
+        <button type="button" id="bgp-peer-tab-btn-attributes" onclick="switchBgpPeerModalTab('attributes')" class="px-4 py-2 text-xs font-bold border-b-2 border-transparent text-slate-400 hover:text-slate-200 transition">Attributes</button>
+        <button type="button" id="bgp-peer-tab-btn-filter" onclick="switchBgpPeerModalTab('filter')" class="px-4 py-2 text-xs font-bold border-b-2 border-transparent text-slate-400 hover:text-slate-200 transition">Filter</button>
       </div>
-      <form id="bgp-peer-form" onsubmit="submitBgpPeer(event)" class="p-6 space-y-4 text-xs">
+
+      <!-- Body Form -->
+      <form id="bgp-peer-form" onsubmit="submitBgpPeer(event)" class="flex-1 overflow-hidden flex flex-col">
         <input type="hidden" id="modal-bgp-peer-id">
-        <div>
-          <label class="block font-semibold text-slate-300 mb-1.5">Connection Name *</label>
-          <input type="text" id="modal-bgp-peer-name" required placeholder="upstream-carrier-a" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono">
-        </div>
+        
+        <div class="flex-1 overflow-y-auto p-6 flex gap-6">
+          <!-- Main Tabs Area -->
+          <div class="flex-1 space-y-4 text-xs">
+            
+            <!-- TAB 1: GENERAL -->
+            <div id="bgp-peer-tab-general" class="space-y-3.5">
+              <div class="grid grid-cols-3 items-center gap-3">
+                <label class="text-slate-400 font-medium">Enabled</label>
+                <div class="col-span-2">
+                  <input type="checkbox" id="modal-bgp-peer-enabled" checked class="w-4 h-4 rounded text-cyan-500 bg-slate-800 border-slate-700">
+                </div>
+              </div>
 
-        <div class="grid grid-cols-2 gap-4">
-          <div>
-            <label class="block font-semibold text-slate-300 mb-1.5">Remote Address (IP) *</label>
-            <input type="text" id="modal-bgp-peer-remote-addr" required placeholder="192.168.131.1" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono">
-          </div>
-          <div>
-            <label class="block font-semibold text-slate-300 mb-1.5">Remote AS *</label>
-            <input type="number" id="modal-bgp-peer-remote-as" required min="1" max="4294967295" placeholder="65000" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono">
-          </div>
-          <div>
-            <label class="block font-semibold text-slate-300 mb-1.5">Local AS (Multi-Instance)</label>
-            <input type="number" id="modal-bgp-peer-local-as" min="1" max="4294967295" placeholder="Optional (uses global AS if blank)" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono">
-          </div>
-        </div>
+              <div class="grid grid-cols-3 items-center gap-3">
+                <label class="text-slate-400 font-medium">Comment</label>
+                <div class="col-span-2">
+                  <input type="text" id="modal-bgp-peer-comment" placeholder="Optional notes" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-1.5 text-white">
+                </div>
+              </div>
 
-        <div class="grid grid-cols-2 gap-4">
-          <div>
-            <label class="block font-semibold text-slate-300 mb-1.5">Local Address (Update Source)</label>
-            <input type="text" id="modal-bgp-peer-local-addr" placeholder="Optional source IP/iface" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono">
-          </div>
-          <div>
-            <label class="block font-semibold text-slate-300 mb-1.5">MD5 Password (BGP Auth)</label>
-            <input type="password" id="modal-bgp-peer-password" placeholder="Optional secret" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono">
-          </div>
-        </div>
+              <div class="grid grid-cols-3 items-center gap-3">
+                <label class="text-slate-400 font-medium">Name *</label>
+                <div class="col-span-2">
+                  <input type="text" id="modal-bgp-peer-name" required placeholder="ebgp-JB172-2" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-1.5 text-white font-mono">
+                </div>
+              </div>
 
-        <div class="grid grid-cols-2 gap-4">
-          <div>
-            <label class="block font-semibold text-slate-300 mb-1.5">In Filter (Routing Filter Chain)</label>
-            <input type="text" id="modal-bgp-peer-in-filter" placeholder="e.g. bgp-in" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono">
-            <p class="text-[10px] text-slate-500 mt-0.5">Filter chain for inbound routes from peer</p>
-          </div>
-          <div>
-            <label class="block font-semibold text-slate-300 mb-1.5">Out Filter (Routing Filter Chain)</label>
-            <input type="text" id="modal-bgp-peer-out-filter" placeholder="e.g. bgp-out" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono">
-            <p class="text-[10px] text-slate-500 mt-0.5">Filter chain for outbound routes to peer</p>
-          </div>
-        </div>
+              <div class="grid grid-cols-3 items-center gap-3">
+                <label class="text-slate-400 font-medium">AS (Local AS)</label>
+                <div class="col-span-2">
+                  <input type="number" id="modal-bgp-peer-local-as" min="1" max="4294967295" placeholder="e.g. 6610 (Multi-Instance Local AS)" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-1.5 text-white font-mono">
+                  <p class="text-[10px] text-slate-500 mt-0.5">Custom Local AS for this BGP session (Multi-Instance). Leave blank to use Global Router AS.</p>
+                </div>
+              </div>
 
-        <div class="grid grid-cols-2 gap-3 p-3 rounded-xl bg-slate-800/40 border border-slate-700/50">
-          <label class="flex items-center space-x-2 text-slate-300 cursor-pointer">
-            <input type="checkbox" id="modal-bgp-peer-multihop" class="rounded text-purple-600 bg-slate-800 border-slate-700">
-            <span>eBGP Multi-hop</span>
-          </label>
-          <label class="flex items-center space-x-2 text-slate-300 cursor-pointer">
-            <input type="checkbox" id="modal-bgp-peer-nexthop-self" checked class="rounded text-purple-600 bg-slate-800 border-slate-700">
-            <span>Next-hop Self</span>
-          </label>
-          <label class="flex items-center space-x-2 text-slate-300 cursor-pointer">
-            <input type="checkbox" id="modal-bgp-peer-passive" class="rounded text-purple-600 bg-slate-800 border-slate-700">
-            <span>Passive Mode</span>
-          </label>
-          <label class="flex items-center space-x-2 text-slate-300 cursor-pointer">
-            <input type="checkbox" id="modal-bgp-peer-disabled" class="rounded text-purple-600 bg-slate-800 border-slate-700">
-            <span>Disabled (X)</span>
-          </label>
-        </div>
+              <div class="grid grid-cols-3 items-center gap-3">
+                <label class="text-slate-400 font-medium">AFI</label>
+                <div class="col-span-2 flex items-center gap-4 text-slate-300">
+                  <label class="flex items-center gap-1.5 cursor-pointer">
+                    <input type="checkbox" id="modal-bgp-peer-afi-ip" checked class="rounded text-cyan-500 bg-slate-800 border-slate-700">
+                    <span>ip (IPv4)</span>
+                  </label>
+                  <label class="flex items-center gap-1.5 cursor-pointer">
+                    <input type="checkbox" id="modal-bgp-peer-afi-ipv6" class="rounded text-cyan-500 bg-slate-800 border-slate-700">
+                    <span>ipv6</span>
+                  </label>
+                </div>
+              </div>
 
-        <div>
-          <label class="block font-semibold text-slate-300 mb-1.5">Comment</label>
-          <input type="text" id="modal-bgp-peer-comment" placeholder="Core transit uplink" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white">
+              <div class="grid grid-cols-3 items-center gap-3">
+                <label class="text-slate-400 font-medium">Router ID</label>
+                <div class="col-span-2">
+                  <input type="text" id="modal-bgp-peer-router-id" placeholder="e.g. 10.42.62.254 (Optional override)" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-1.5 text-white font-mono">
+                </div>
+              </div>
+
+              <hr class="border-slate-800/80 my-2">
+
+              <div class="grid grid-cols-3 items-center gap-3">
+                <label class="text-slate-400 font-medium">Remote Address *</label>
+                <div class="col-span-2">
+                  <input type="text" id="modal-bgp-peer-remote-addr" required placeholder="10.2.51.249" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-1.5 text-white font-mono">
+                </div>
+              </div>
+
+              <div class="grid grid-cols-3 items-center gap-3">
+                <label class="text-slate-400 font-medium">Remote Port</label>
+                <div class="col-span-2">
+                  <input type="number" id="modal-bgp-peer-remote-port" placeholder="179 (default)" min="1" max="65535" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-1.5 text-white font-mono">
+                </div>
+              </div>
+
+              <div class="grid grid-cols-3 items-center gap-3">
+                <label class="text-slate-400 font-medium">Remote AS *</label>
+                <div class="col-span-2">
+                  <input type="number" id="modal-bgp-peer-remote-as" required min="1" max="4294967295" placeholder="17244" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-1.5 text-white font-mono">
+                </div>
+              </div>
+
+              <hr class="border-slate-800/80 my-2">
+
+              <div class="grid grid-cols-3 items-center gap-3">
+                <label class="text-slate-400 font-medium">Local Address</label>
+                <div class="col-span-2">
+                  <input type="text" id="modal-bgp-peer-local-addr" placeholder="e.g. 10.2.51.250" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-1.5 text-white font-mono">
+                </div>
+              </div>
+
+              <div class="grid grid-cols-3 items-center gap-3">
+                <label class="text-slate-400 font-medium">Local Port</label>
+                <div class="col-span-2">
+                  <input type="number" id="modal-bgp-peer-local-port" placeholder="Optional bind port" min="1" max="65535" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-1.5 text-white font-mono">
+                </div>
+              </div>
+
+              <div class="grid grid-cols-3 items-center gap-3">
+                <label class="text-slate-400 font-medium">Local Role</label>
+                <div class="col-span-2">
+                  <select id="modal-bgp-peer-local-role" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-1.5 text-white font-mono">
+                    <option value="ebgp">ebgp</option>
+                    <option value="ibgp">ibgp</option>
+                  </select>
+                </div>
+              </div>
+
+              <div class="grid grid-cols-3 items-center gap-3">
+                <label class="text-slate-400 font-medium">TCP MD5 Key</label>
+                <div class="col-span-2">
+                  <input type="password" id="modal-bgp-peer-password" placeholder="BGP authentication secret" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-1.5 text-white font-mono">
+                </div>
+              </div>
+
+              <hr class="border-slate-800/80 my-2">
+
+              <div class="grid grid-cols-3 items-center gap-3">
+                <label class="text-slate-400 font-medium">Multi-hop</label>
+                <div class="col-span-2">
+                  <label class="flex items-center gap-2 cursor-pointer text-slate-300">
+                    <input type="checkbox" id="modal-bgp-peer-multihop" class="rounded text-cyan-500 bg-slate-800 border-slate-700">
+                    <span>Enable eBGP Multi-hop</span>
+                  </label>
+                </div>
+              </div>
+
+              <div class="grid grid-cols-3 items-center gap-3">
+                <label class="text-slate-400 font-medium">Session Mode</label>
+                <div class="col-span-2 flex items-center gap-6 text-slate-300">
+                  <label class="flex items-center gap-2 cursor-pointer">
+                    <input type="checkbox" id="modal-bgp-peer-connect" checked class="rounded text-cyan-500 bg-slate-800 border-slate-700">
+                    <span>Connect</span>
+                  </label>
+                  <label class="flex items-center gap-2 cursor-pointer">
+                    <input type="checkbox" id="modal-bgp-peer-listen" checked class="rounded text-cyan-500 bg-slate-800 border-slate-700">
+                    <span>Listen</span>
+                  </label>
+                </div>
+              </div>
+            </div>
+
+            <!-- TAB 2: EXTRA -->
+            <div id="bgp-peer-tab-extra" class="space-y-3.5 hidden">
+              <div class="grid grid-cols-3 items-center gap-3">
+                <label class="text-slate-400 font-medium">Hold Time</label>
+                <div class="col-span-2">
+                  <input type="text" id="modal-bgp-peer-holdtime" placeholder="00:03:00 (180s)" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-1.5 text-white font-mono">
+                </div>
+              </div>
+
+              <div class="grid grid-cols-3 items-center gap-3">
+                <label class="text-slate-400 font-medium">Keepalive Time</label>
+                <div class="col-span-2">
+                  <input type="text" id="modal-bgp-peer-keepalive" placeholder="00:01:00 (60s)" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-1.5 text-white font-mono">
+                </div>
+              </div>
+
+              <div class="grid grid-cols-3 items-center gap-3">
+                <label class="text-slate-400 font-medium">Routing Table</label>
+                <div class="col-span-2">
+                  <select id="modal-bgp-peer-routing-table" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-1.5 text-white font-mono">
+                    <option value="main">main</option>
+                  </select>
+                </div>
+              </div>
+
+              <hr class="border-slate-800/80 my-2">
+
+              <div class="grid grid-cols-3 items-start gap-3">
+                <label class="text-slate-400 font-medium pt-1">Output Redistribute</label>
+                <div class="col-span-2 grid grid-cols-2 gap-2 text-slate-300">
+                  <label class="flex items-center gap-2 cursor-pointer">
+                    <input type="checkbox" id="modal-bgp-peer-redist-conn" class="rounded text-cyan-500 bg-slate-800 border-slate-700">
+                    <span>connected</span>
+                  </label>
+                  <label class="flex items-center gap-2 cursor-pointer">
+                    <input type="checkbox" id="modal-bgp-peer-redist-static" checked class="rounded text-cyan-500 bg-slate-800 border-slate-700">
+                    <span>static</span>
+                  </label>
+                  <label class="flex items-center gap-2 cursor-pointer">
+                    <input type="checkbox" id="modal-bgp-peer-redist-ospf" class="rounded text-cyan-500 bg-slate-800 border-slate-700">
+                    <span>ospf</span>
+                  </label>
+                  <label class="flex items-center gap-2 cursor-pointer">
+                    <input type="checkbox" id="modal-bgp-peer-redist-bgp" class="rounded text-cyan-500 bg-slate-800 border-slate-700">
+                    <span>bgp</span>
+                  </label>
+                </div>
+              </div>
+
+              <hr class="border-slate-800/80 my-2">
+
+              <div class="grid grid-cols-3 items-center gap-3">
+                <label class="text-slate-400 font-medium">Default Originate</label>
+                <div class="col-span-2">
+                  <label class="flex items-center gap-2 cursor-pointer text-slate-300">
+                    <input type="checkbox" id="modal-bgp-peer-default-orig" class="rounded text-cyan-500 bg-slate-800 border-slate-700">
+                    <span>Announce Default Route (0.0.0.0/0)</span>
+                  </label>
+                </div>
+              </div>
+            </div>
+
+            <!-- TAB 3: ATTRIBUTES -->
+            <div id="bgp-peer-tab-attributes" class="space-y-3.5 hidden">
+              <div class="grid grid-cols-3 items-center gap-3">
+                <label class="text-slate-400 font-medium">Nexthop Choice</label>
+                <div class="col-span-2">
+                  <select id="modal-bgp-peer-nexthop-choice" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-1.5 text-white font-mono">
+                    <option value="force-self">force-self (Next-hop Self)</option>
+                    <option value="default">default</option>
+                  </select>
+                </div>
+              </div>
+
+              <div class="grid grid-cols-3 items-center gap-3">
+                <label class="text-slate-400 font-medium">Default Prepend</label>
+                <div class="col-span-2">
+                  <input type="number" id="modal-bgp-peer-default-prepend" min="0" max="16" placeholder="0 (none)" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-1.5 text-white font-mono">
+                  <p class="text-[10px] text-slate-500 mt-0.5">Repeat local ASN in AS-PATH to make path less preferred</p>
+                </div>
+              </div>
+
+              <div class="grid grid-cols-3 items-center gap-3">
+                <label class="text-slate-400 font-medium">Allow AS In</label>
+                <div class="col-span-2">
+                  <input type="number" id="modal-bgp-peer-allow-as-in" min="0" max="10" placeholder="0" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-1.5 text-white font-mono">
+                </div>
+              </div>
+
+              <div class="grid grid-cols-3 items-center gap-3">
+                <label class="text-slate-400 font-medium">Remove Private AS</label>
+                <div class="col-span-2">
+                  <label class="flex items-center gap-2 cursor-pointer text-slate-300">
+                    <input type="checkbox" id="modal-bgp-peer-remove-priv-as" class="rounded text-cyan-500 bg-slate-800 border-slate-700">
+                    <span>Strip private AS numbers (64512-65534) from AS-PATH</span>
+                  </label>
+                </div>
+              </div>
+
+              <div class="grid grid-cols-3 items-center gap-3">
+                <label class="text-slate-400 font-medium">AS Override</label>
+                <div class="col-span-2">
+                  <label class="flex items-center gap-2 cursor-pointer text-slate-300">
+                    <input type="checkbox" id="modal-bgp-peer-as-override" class="rounded text-cyan-500 bg-slate-800 border-slate-700">
+                    <span>Replace peer AS in AS-PATH</span>
+                  </label>
+                </div>
+              </div>
+            </div>
+
+            <!-- TAB 4: FILTER -->
+            <div id="bgp-peer-tab-filter" class="space-y-4 hidden">
+              <div class="grid grid-cols-3 items-center gap-3">
+                <label class="text-slate-400 font-medium">Input Filter</label>
+                <div class="col-span-2">
+                  <input type="text" id="modal-bgp-peer-in-filter" placeholder="e.g. awmn or bgp-in" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-1.5 text-white font-mono">
+                  <p class="text-[10px] text-slate-500 mt-0.5">Routing Filter chain applied to incoming routes</p>
+                </div>
+              </div>
+
+              <div class="grid grid-cols-3 items-center gap-3">
+                <label class="text-slate-400 font-medium">Output Filter</label>
+                <div class="col-span-2">
+                  <input type="text" id="modal-bgp-peer-out-filter" placeholder="e.g. awmn or bgp-out" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-1.5 text-white font-mono">
+                  <p class="text-[10px] text-slate-500 mt-0.5">Routing Filter chain applied to outgoing routes</p>
+                </div>
+              </div>
+
+              <hr class="border-slate-800/80 my-3">
+
+              <!-- Output Networks section with [+] and [-] buttons as requested! -->
+              <div>
+                <div class="flex items-center justify-between mb-2">
+                  <div>
+                    <label class="text-slate-200 font-bold block">Output Networks</label>
+                    <p class="text-[10px] text-slate-400">Prefixes/subnets announced to this peer connection</p>
+                  </div>
+                  <button type="button" onclick="addOutputNetworkRow('')" class="text-xs px-2.5 py-1 bg-cyan-600/30 hover:bg-cyan-600 text-cyan-300 hover:text-white rounded border border-cyan-500/40 font-semibold transition flex items-center gap-1">
+                    <span>+</span>
+                    <span>Add Network</span>
+                  </button>
+                </div>
+
+                <div id="modal-bgp-output-networks-container" class="space-y-2 p-3 bg-slate-950/40 border border-slate-800 rounded-xl min-h-[80px]">
+                  <!-- Dynamic rows go here -->
+                </div>
+              </div>
+            </div>
+
+          </div>
+
+          <!-- Right Sidebar (RouterOS Actions) -->
+          <div class="w-36 border-l border-slate-800 pl-4 space-y-2 font-sans">
+            <div class="text-[10px] uppercase font-bold text-slate-500 tracking-wider mb-2">Actions</div>
+            <button type="button" onclick="copyCurrentBgpPeer()" class="w-full text-left text-xs px-2.5 py-1.5 rounded-lg text-slate-300 hover:text-white hover:bg-slate-800 transition flex items-center gap-2">
+              <span>📋</span>
+              <span>Copy</span>
+            </button>
+            <button type="button" id="modal-btn-remove-peer" onclick="removeCurrentBgpPeer()" class="w-full text-left text-xs px-2.5 py-1.5 rounded-lg text-rose-400 hover:text-rose-300 hover:bg-rose-950/40 transition flex items-center gap-2">
+              <span>✕</span>
+              <span>Remove</span>
+            </button>
+          </div>
         </div>
 
-        <div class="pt-4 border-t border-slate-800 flex justify-end space-x-3">
-          <button type="button" onclick="closeBgpPeerModal()" class="px-4 py-2 rounded-lg text-slate-400 hover:bg-slate-800">Cancel</button>
-          <button type="submit" class="px-5 py-2 rounded-lg font-semibold bg-purple-600 hover:bg-purple-500 text-white shadow-lg shadow-purple-500/20">Save Peer</button>
+        <!-- Footer Buttons -->
+        <div class="px-6 py-3.5 border-t border-slate-800 bg-slate-950/60 flex items-center justify-end space-x-2.5">
+          <button type="button" onclick="closeBgpPeerModal()" class="px-4 py-1.5 rounded-lg text-xs font-semibold text-slate-400 hover:bg-slate-800 hover:text-white transition">Cancel</button>
+          <button type="button" onclick="applyBgpPeerModal(event)" class="px-4 py-1.5 rounded-lg text-xs font-semibold bg-slate-800 hover:bg-slate-700 text-cyan-300 border border-slate-700 transition">Apply</button>
+          <button type="submit" class="px-5 py-1.5 rounded-lg text-xs font-semibold bg-cyan-600 hover:bg-cyan-500 text-white shadow-lg shadow-cyan-500/20 transition">OK</button>
         </div>
       </form>
     </div>