Explorar o código

feat(ospf): support redistribute-default and originate-default in OSPF instance (v1.8.7)

Proxmox Router hai 2 días
pai
achega
7a4e83f49c

+ 1 - 1
VERSION

@@ -1 +1 @@
-1.8.6
+1.8.7

BIN=BIN
bin/debtik


BIN=BIN
bin/debtik-arm64


BIN=BIN
bin/debtik-cli


BIN=BIN
bin/debtik-cli-arm64


+ 38 - 1
internal/cli/executor.go

@@ -2306,6 +2306,13 @@ func (e *Executor) handleOSPFInstance(parts []string, user *auth.PublicUser) str
 			if inst.RedistributeBGP {
 				redist = append(redist, "bgp")
 			}
+			if inst.RedistributeDefault || (inst.OriginateDefault != "" && inst.OriginateDefault != "never") {
+				mode := inst.OriginateDefault
+				if mode == "" || mode == "never" {
+					mode = "if-installed"
+				}
+				redist = append(redist, fmt.Sprintf("default(%s)", mode))
+			}
 			redistStr := "-"
 			if len(redist) > 0 {
 				redistStr = strings.Join(redist, ",")
@@ -2341,6 +2348,17 @@ func (e *Executor) handleOSPFInstance(parts []string, user *auth.PublicUser) str
 		redistConn := parseCLIEnableDisable(params["redistribute-connected"], false)
 		redistStat := parseCLIEnableDisable(params["redistribute-static"], false)
 		redistBGP := parseCLIEnableDisable(params["redistribute-bgp"], false)
+		redistDef := parseCLIEnableDisable(params["redistribute-default"], false)
+		origDef := strings.ToLower(strings.TrimSpace(params["originate-default"]))
+		if origDef == "" {
+			if redistDef {
+				origDef = "if-installed"
+			} else {
+				origDef = "never"
+			}
+		} else if origDef == "if-installed" || origDef == "always" {
+			redistDef = true
+		}
 		metric := 0
 		if mStr, ok := params["default-metric"]; ok && mStr != "" {
 			metric, _ = strconv.Atoi(mStr)
@@ -2366,6 +2384,8 @@ func (e *Executor) handleOSPFInstance(parts []string, user *auth.PublicUser) str
 			RedistributeConnected: redistConn,
 			RedistributeStatic:    redistStat,
 			RedistributeBGP:       redistBGP,
+			RedistributeDefault:   redistDef,
+			OriginateDefault:      origDef,
 			DefaultMetric:         metric,
 			Comment:               params["comment"],
 		}
@@ -2377,7 +2397,7 @@ func (e *Executor) handleOSPFInstance(parts []string, user *auth.PublicUser) str
 	case "set":
 		pos, params := parseArgs(parts[1:])
 		if len(pos) == 0 && params["numbers"] == "" {
-			return ColorRed + "Usage: /routing ospf instance set <number|name> [router-id=...] [redistribute-connected=yes/no] ...\n" + ColorReset
+			return ColorRed + "Usage: /routing ospf instance set <number|name> [router-id=...] [redistribute-connected=yes/no] [redistribute-static=yes/no] [redistribute-bgp=yes/no] [redistribute-default=yes/no] [originate-default=never|if-installed|always] ...\n" + ColorReset
 		}
 		target := ""
 		if len(pos) > 0 {
@@ -2417,6 +2437,23 @@ func (e *Executor) handleOSPFInstance(parts []string, user *auth.PublicUser) str
 		if v, ok := params["redistribute-bgp"]; ok {
 			targetInst.RedistributeBGP = parseCLIEnableDisable(v, false)
 		}
+		if v, ok := params["redistribute-default"]; ok {
+			targetInst.RedistributeDefault = parseCLIEnableDisable(v, false)
+			if targetInst.RedistributeDefault && (targetInst.OriginateDefault == "" || targetInst.OriginateDefault == "never") {
+				targetInst.OriginateDefault = "if-installed"
+			} else if !targetInst.RedistributeDefault {
+				targetInst.OriginateDefault = "never"
+			}
+		}
+		if v, ok := params["originate-default"]; ok {
+			vLower := strings.ToLower(strings.TrimSpace(v))
+			targetInst.OriginateDefault = vLower
+			if vLower == "if-installed" || vLower == "always" {
+				targetInst.RedistributeDefault = true
+			} else if vLower == "never" {
+				targetInst.RedistributeDefault = false
+			}
+		}
 		if v, ok := params["default-metric"]; ok {
 			targetInst.DefaultMetric, _ = strconv.Atoi(v)
 		}

+ 54 - 4
internal/dynamicrouting/manager.go

@@ -84,6 +84,8 @@ func (m *Manager) initDefaults() {
 			RedistributeConnected: false,
 			RedistributeStatic:    true,
 			RedistributeBGP:       false,
+			RedistributeDefault:   false,
+			OriginateDefault:      "never",
 			DefaultMetric:         10,
 		},
 		OSPFAreas: []OSPFArea{
@@ -267,6 +269,7 @@ func (m *Manager) generateBirdConfig() string {
 	sb.WriteString("    metric 0;\n")
 	sb.WriteString("    ipv4 {\n")
 	sb.WriteString("        export filter {\n")
+	sb.WriteString("            if proto ~ \"ospf_def_*\" then reject;\n")
 	sb.WriteString("            if source = RTS_DEVICE then reject;\n")
 	sb.WriteString("            if source = RTS_BGP then {\n")
 	sb.WriteString("                if bgp_path.first = bgp_path.last && bgp_path.len = 0 then\n")
@@ -555,6 +558,8 @@ func (m *Manager) generateBirdConfig() string {
 				RedistributeConnected: legacy.RedistributeConnected,
 				RedistributeStatic:    legacy.RedistributeStatic,
 				RedistributeBGP:       legacy.RedistributeBGP,
+				RedistributeDefault:   legacy.RedistributeDefault,
+				OriginateDefault:      legacy.OriginateDefault,
 				DefaultMetric:         legacy.DefaultMetric,
 			},
 		}
@@ -571,6 +576,30 @@ func (m *Manager) generateBirdConfig() string {
 		}
 
 		instProtoName := sanitizeIdentifier("ospf", inst.Name)
+
+		// Determine Originate Default mode
+		origDefault := strings.ToLower(strings.TrimSpace(inst.OriginateDefault))
+		if inst.RedistributeDefault && (origDefault == "" || origDefault == "never") {
+			origDefault = "if-installed"
+		}
+		if origDefault == "" {
+			origDefault = "never"
+		}
+
+		defaultMetric := inst.DefaultMetric
+		if defaultMetric <= 0 {
+			defaultMetric = 10
+		}
+
+		defProtoName := ""
+		if origDefault == "always" {
+			defProtoName = fmt.Sprintf("ospf_def_%s", instProtoName)
+			sb.WriteString(fmt.Sprintf("protocol static %s {\n", defProtoName))
+			sb.WriteString("    ipv4;\n")
+			sb.WriteString("    route 0.0.0.0/0 unreachable;\n")
+			sb.WriteString("}\n\n")
+		}
+
 		sb.WriteString(fmt.Sprintf("protocol ospf v%d %s {\n", inst.Version, instProtoName))
 		if inst.VRF != "" && inst.VRF != "main" && inst.VRF != "default" {
 			sb.WriteString(fmt.Sprintf("    vrf %s;\n", inst.VRF))
@@ -593,12 +622,33 @@ func (m *Manager) generateBirdConfig() string {
 			ospfExports = append(ospfExports, "RTS_BGP")
 		}
 
-		if len(ospfExports) > 0 {
-			if inst.DefaultMetric > 0 {
-				sb.WriteString(fmt.Sprintf("        export filter {\n            if source ~ [ %s ] then {\n                ospf_metric1 = %d;\n                accept;\n            }\n            reject;\n        };\n", strings.Join(ospfExports, ", "), inst.DefaultMetric))
+		hasDefaultOrig := origDefault == "always" || origDefault == "if-installed"
+		if len(ospfExports) > 0 || hasDefaultOrig {
+			sb.WriteString("        export filter {\n")
+			if origDefault == "always" {
+				sb.WriteString(fmt.Sprintf("            if proto = \"%s\" || (net = 0.0.0.0/0 && source != RTS_OSPF) then {\n", defProtoName))
+				sb.WriteString(fmt.Sprintf("                ospf_metric1 = %d;\n", defaultMetric))
+				sb.WriteString("                accept;\n")
+				sb.WriteString("            }\n")
+			} else if origDefault == "if-installed" {
+				sb.WriteString("            if net = 0.0.0.0/0 && source != RTS_OSPF then {\n")
+				sb.WriteString(fmt.Sprintf("                ospf_metric1 = %d;\n", defaultMetric))
+				sb.WriteString("                accept;\n")
+				sb.WriteString("            }\n")
 			} else {
-				sb.WriteString(fmt.Sprintf("        export where source ~ [ %s ];\n", strings.Join(ospfExports, ", ")))
+				sb.WriteString("            if net = 0.0.0.0/0 then reject;\n")
+			}
+
+			if len(ospfExports) > 0 {
+				sb.WriteString(fmt.Sprintf("            if source ~ [ %s ] then {\n", strings.Join(ospfExports, ", ")))
+				if inst.DefaultMetric > 0 {
+					sb.WriteString(fmt.Sprintf("                ospf_metric1 = %d;\n", inst.DefaultMetric))
+				}
+				sb.WriteString("                accept;\n")
+				sb.WriteString("            }\n")
 			}
+			sb.WriteString("            reject;\n")
+			sb.WriteString("        };\n")
 		} else {
 			sb.WriteString("        export none;\n")
 		}

+ 45 - 0
internal/dynamicrouting/ospf_test.go

@@ -206,3 +206,48 @@ func TestOSPFStaticNeighborManagement(t *testing.T) {
 		t.Fatalf("expected 0 static neighbors after delete, got: %+v", list3)
 	}
 }
+
+func TestOSPFOriginateDefault(t *testing.T) {
+	// 1. Test "never" (default)
+	m := &Manager{}
+	m.config.OSPFInstances = []OSPFInstance{
+		{
+			ID:                  "inst-def",
+			Name:                "test_inst",
+			Version:             2,
+			Enabled:             true,
+			RedistributeStatic:  true,
+			RedistributeDefault: false,
+			OriginateDefault:    "never",
+		},
+	}
+	conf := m.generateBirdConfig()
+	if !strings.Contains(conf, "if net = 0.0.0.0/0 then reject;") {
+		t.Fatalf("expected 0.0.0.0/0 reject in OSPF export filter when originate-default is never:\n%s", conf)
+	}
+
+	// 2. Test "if-installed"
+	m.config.OSPFInstances[0].RedistributeDefault = true
+	m.config.OSPFInstances[0].OriginateDefault = "if-installed"
+	m.config.OSPFInstances[0].DefaultMetric = 20
+	conf = m.generateBirdConfig()
+	if !strings.Contains(conf, "if net = 0.0.0.0/0 && source != RTS_OSPF then {") {
+		t.Fatalf("expected if-installed check in OSPF export filter:\n%s", conf)
+	}
+	if !strings.Contains(conf, "ospf_metric1 = 20;") {
+		t.Fatalf("expected metric 20 in OSPF export filter:\n%s", conf)
+	}
+
+	// 3. Test "always"
+	m.config.OSPFInstances[0].OriginateDefault = "always"
+	conf = m.generateBirdConfig()
+	if !strings.Contains(conf, "protocol static ospf_def_ospf_test_inst {") {
+		t.Fatalf("expected protocol static ospf_def_ospf_test_inst for always:\n%s", conf)
+	}
+	if !strings.Contains(conf, "if proto ~ \"ospf_def_*\" then reject;") {
+		t.Fatalf("expected kernel export filter to reject ospf_def_*:\n%s", conf)
+	}
+	if !strings.Contains(conf, `if proto = "ospf_def_ospf_test_inst" || (net = 0.0.0.0/0 && source != RTS_OSPF) then {`) {
+		t.Fatalf("expected always check in OSPF export filter:\n%s", conf)
+	}
+}

+ 2 - 0
internal/dynamicrouting/types.go

@@ -122,6 +122,8 @@ type OSPFInstance struct {
 	RedistributeConnected bool   `json:"redistribute_connected"`
 	RedistributeStatic    bool   `json:"redistribute_static"`
 	RedistributeBGP       bool   `json:"redistribute_bgp"`
+	RedistributeDefault   bool   `json:"redistribute_default"`        // MikroTik redistribute-default
+	OriginateDefault      string `json:"originate_default,omitempty"` // "never", "if-installed", "always"
 	DefaultMetric         int    `json:"default_metric"`
 	Running               bool   `json:"running"`
 }

+ 42 - 1
internal/web/static/app.js

@@ -10156,6 +10156,20 @@ function renderOspfInstances() {
     const selClass = isSel ? 'bg-[#1b2f48] text-white border-l-2 border-sky-400' : 'hover:bg-[#1a2636] text-slate-300';
     const flagD = inst.disabled ? '<span class="text-rose-400 font-bold" title="Disabled">X</span>' : '';
 
+    const redist = [];
+    if (inst.redistribute_connected) redist.push('conn');
+    if (inst.redistribute_static) redist.push('static');
+    if (inst.redistribute_bgp) redist.push('bgp');
+    if (inst.redistribute_default || (inst.originate_default && inst.originate_default !== 'never')) {
+      redist.push('default');
+    }
+    const origDef = inst.originate_default || (inst.redistribute_default ? 'if-installed' : 'never');
+    const origBadge = origDef === 'always'
+      ? '<span class="px-1.5 py-0.5 rounded text-[10px] bg-amber-500/20 text-amber-300 font-bold border border-amber-500/30">always</span>'
+      : (origDef === 'if-installed'
+        ? '<span class="px-1.5 py-0.5 rounded text-[10px] bg-sky-500/20 text-sky-300 font-bold border border-sky-500/30">if-installed</span>'
+        : '<span class="text-slate-500 text-[10px]">never</span>');
+
     return `
       <tr class="${selClass} cursor-pointer transition select-none" onclick="selectOspfRow('instances', '${inst.id}')" ondblclick="openEditOspfInstanceModal('${inst.id}')">
         <td class="py-1.5 px-3 w-8 text-center text-slate-500">${idx}</td>
@@ -10167,6 +10181,8 @@ function renderOspfInstances() {
         <td class="py-1.5 px-3">${inst.version || 2}</td>
         <td class="py-1.5 px-3 font-mono text-slate-300">${escapeHtml(inst.vrf || 'main')}</td>
         <td class="py-1.5 px-3 font-mono font-medium text-sky-300">${escapeHtml(inst.router_id || '192.168.131.56')}</td>
+        <td class="py-1.5 px-3 font-mono text-slate-300">${redist.join(', ') || '-'}</td>
+        <td class="py-1.5 px-3">${origBadge}</td>
       </tr>
     `;
   }).join('');
@@ -10892,6 +10908,11 @@ function openOspfInstanceModal(id = null) {
   document.getElementById('modal-ospf-inst-redist-conn').checked = !!inst.redistribute_connected;
   document.getElementById('modal-ospf-inst-redist-static').checked = inst.redistribute_static !== false;
   document.getElementById('modal-ospf-inst-redist-bgp').checked = !!inst.redistribute_bgp;
+  const isDefaultRedist = !!inst.redistribute_default || (inst.originate_default && inst.originate_default !== 'never');
+  const origMode = inst.originate_default || (inst.redistribute_default ? 'if-installed' : 'never');
+  document.getElementById('modal-ospf-inst-redist-default').checked = isDefaultRedist;
+  document.getElementById('modal-ospf-inst-originate-default').value = origMode;
+  document.getElementById('modal-ospf-inst-default-metric').value = inst.default_metric || '';
 
   const btnRem = document.getElementById('btn-ospf-inst-modal-remove');
   if (btnRem) btnRem.style.display = isEdit ? 'block' : 'none';
@@ -10899,6 +10920,23 @@ function openOspfInstanceModal(id = null) {
   document.getElementById('ospf-instance-modal').classList.remove('hidden');
 }
 
+function toggleOspfDefaultModeUI() {
+  const isChecked = document.getElementById('modal-ospf-inst-redist-default')?.checked;
+  const select = document.getElementById('modal-ospf-inst-originate-default');
+  if (!select) return;
+  if (isChecked) {
+    if (select.value === 'never') select.value = 'if-installed';
+  } else {
+    select.value = 'never';
+  }
+}
+
+function onOspfOriginateDefaultChange() {
+  const val = document.getElementById('modal-ospf-inst-originate-default')?.value;
+  const chk = document.getElementById('modal-ospf-inst-redist-default');
+  if (chk) chk.checked = val !== 'never';
+}
+
 function openEditOspfInstanceModal(id) {
   openOspfInstanceModal(id);
 }
@@ -10921,7 +10959,10 @@ async function submitOspfInstance(e) {
     comment: document.getElementById('modal-ospf-inst-comment').value.trim(),
     redistribute_connected: document.getElementById('modal-ospf-inst-redist-conn').checked,
     redistribute_static: document.getElementById('modal-ospf-inst-redist-static').checked,
-    redistribute_bgp: document.getElementById('modal-ospf-inst-redist-bgp').checked
+    redistribute_bgp: document.getElementById('modal-ospf-inst-redist-bgp').checked,
+    redistribute_default: document.getElementById('modal-ospf-inst-redist-default').checked,
+    originate_default: document.getElementById('modal-ospf-inst-originate-default').value,
+    default_metric: parseInt(document.getElementById('modal-ospf-inst-default-metric').value, 10) || 0
   };
 
   try {

+ 24 - 0
internal/web/static/index.html

@@ -3155,6 +3155,8 @@
                       <th class="py-2 px-3 cursor-pointer" onclick="sortOspfTable('instances', 'version')">Version</th>
                       <th class="py-2 px-3 cursor-pointer" onclick="sortOspfTable('instances', 'vrf')">VRF</th>
                       <th class="py-2 px-3 cursor-pointer" onclick="sortOspfTable('instances', 'router_id')">Router ID</th>
+                      <th class="py-2 px-3 cursor-pointer" onclick="sortOspfTable('instances', 'redistribute')">Redistribute</th>
+                      <th class="py-2 px-3 cursor-pointer" onclick="sortOspfTable('instances', 'originate_default')">Originate Default</th>
                     </tr>
                   </thead>
                   <tbody id="ospf-instances-table-body" class="divide-y divide-[#1e2c3d]"></tbody>
@@ -10224,6 +10226,28 @@
                   <input type="checkbox" id="modal-ospf-inst-redist-bgp" class="rounded text-sky-500 bg-slate-800 border-slate-700">
                   <span>bgp</span>
                 </label>
+                <label class="flex items-center gap-2 cursor-pointer text-slate-300">
+                  <input type="checkbox" id="modal-ospf-inst-redist-default" onchange="toggleOspfDefaultModeUI()" class="rounded text-sky-500 bg-slate-800 border-slate-700">
+                  <span>default (0.0.0.0/0)</span>
+                </label>
+              </div>
+            </div>
+
+            <div id="row-ospf-inst-originate-default" class="grid grid-cols-3 items-center gap-3">
+              <label class="text-slate-400 font-medium">Originate Default</label>
+              <div class="col-span-2">
+                <select id="modal-ospf-inst-originate-default" onchange="onOspfOriginateDefaultChange()" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-1.5 text-white">
+                  <option value="never">never (Disabled)</option>
+                  <option value="if-installed">if-installed (Only if default route present)</option>
+                  <option value="always">always (Always advertise 0.0.0.0/0)</option>
+                </select>
+              </div>
+            </div>
+
+            <div class="grid grid-cols-3 items-center gap-3">
+              <label class="text-slate-400 font-medium">Default Metric</label>
+              <div class="col-span-2">
+                <input type="number" id="modal-ospf-inst-default-metric" placeholder="10" min="0" 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>