소스 검색

feat(babel): add In-Filter and Out-Filter policy routing support to Babel instances (v1.8.25)

- Add InFilter and OutFilter fields to BabelInstance supporting MikroTik-style Policy Filters
- Generate BIRD 2 import filter and export filter with procedural rule compilation and redistribution chaining
- Add In-Filter and Out-Filter controls in Babel Web UI modal with live datalist autocompletion
- Add in-filter and out-filter options to CLI /routing babel instance [add|set|print]
- Add comprehensive BIRD 2 validation unit test for Babel policy filter compilation
- Bump version to v1.8.25 and build AMD64/ARM64 releases
Proxmox Router 1 일 전
부모
커밋
258d5aa024

+ 1 - 1
VERSION

@@ -1 +1 @@
-1.8.24
+1.8.25

BIN
bin/debtik


BIN
bin/debtik-arm64


BIN
bin/debtik-cli


BIN
bin/debtik-cli-arm64


+ 75 - 3
internal/cli/executor.go

@@ -3483,7 +3483,7 @@ func (e *Executor) handleRoutingBabel(parts []string, user *auth.PublicUser) str
 func (e *Executor) handleBabelInstance(parts []string, user *auth.PublicUser) string {
 	if len(parts) == 0 || parts[0] == "print" || parts[0] == "pr" {
 		instances := e.dynRoutingMgr.ListBabelInstances()
-		tbl := NewTable("#", "FLAGS", "NAME", "ROUTER-ID", "REDISTRIBUTE", "METRIC", "STATUS")
+		tbl := NewTable("#", "FLAGS", "NAME", "ROUTER-ID", "IN-FILTER", "OUT-FILTER", "REDISTRIBUTE", "METRIC", "STATUS")
 		for idx, inst := range instances {
 			st := "enabled"
 			flags := "I"
@@ -3518,7 +3518,15 @@ func (e *Executor) handleBabelInstance(parts []string, user *auth.PublicUser) st
 			if rID == "" {
 				rID = "auto"
 			}
-			tbl.AddRow(fmt.Sprintf("%d", idx), flags, inst.Name, rID, redistStr, fmt.Sprintf("%d", inst.DefaultMetric), st)
+			inF := inst.InFilter
+			if inF == "" {
+				inF = "-"
+			}
+			outF := inst.OutFilter
+			if outF == "" {
+				outF = "-"
+			}
+			tbl.AddRow(fmt.Sprintf("%d", idx), flags, inst.Name, rID, inF, outF, redistStr, fmt.Sprintf("%d", inst.DefaultMetric), st)
 		}
 		return tbl.String()
 	}
@@ -3538,6 +3546,8 @@ func (e *Executor) handleBabelInstance(parts []string, user *auth.PublicUser) st
 		inst := dynamicrouting.BabelInstance{
 			Name:                  name,
 			RouterID:              params["router-id"],
+			InFilter:              params["in-filter"],
+			OutFilter:             params["out-filter"],
 			Enabled:               true,
 			Disabled:              false,
 			RedistributeConnected: parseCLIEnableDisable(params["redistribute-connected"], false),
@@ -3551,6 +3561,68 @@ func (e *Executor) handleBabelInstance(parts []string, user *auth.PublicUser) st
 		}
 		return ColorGreen + fmt.Sprintf("Babel instance %s added.\n", name) + ColorReset
 
+	case "set":
+		pos, params := parseArgs(parts[1:])
+		target := ""
+		if len(pos) > 0 {
+			target = pos[0]
+		} else if v, ok := params["numbers"]; ok {
+			target = v
+		} else if v, ok := params["name"]; ok {
+			target = v
+		}
+		if target == "" {
+			return ColorRed + "Usage: /routing babel instance set <#|name|id> [in-filter=...] [out-filter=...] [router-id=...] [default-metric=...]\n" + ColorReset
+		}
+		instances := e.dynRoutingMgr.ListBabelInstances()
+		var targetInst *dynamicrouting.BabelInstance
+		if idx, err := strconv.Atoi(target); err == nil && idx >= 0 && idx < len(instances) {
+			targetInst = &instances[idx]
+		} else {
+			for i := range instances {
+				if strings.EqualFold(instances[i].Name, target) || instances[i].ID == target {
+					targetInst = &instances[i]
+					break
+				}
+			}
+		}
+		if targetInst == nil {
+			return ColorRed + "Babel instance not found: " + target + "\n" + ColorReset
+		}
+		if v, ok := params["in-filter"]; ok {
+			targetInst.InFilter = strings.TrimSpace(v)
+		}
+		if v, ok := params["out-filter"]; ok {
+			targetInst.OutFilter = strings.TrimSpace(v)
+		}
+		if v, ok := params["router-id"]; ok {
+			targetInst.RouterID = strings.TrimSpace(v)
+		}
+		if v, ok := params["default-metric"]; ok {
+			if m, err := strconv.Atoi(v); err == nil && m > 0 {
+				targetInst.DefaultMetric = m
+			}
+		}
+		if v, ok := params["redistribute-connected"]; ok {
+			targetInst.RedistributeConnected = parseCLIEnableDisable(v, targetInst.RedistributeConnected)
+		}
+		if v, ok := params["redistribute-static"]; ok {
+			targetInst.RedistributeStatic = parseCLIEnableDisable(v, targetInst.RedistributeStatic)
+		}
+		if v, ok := params["redistribute-bgp"]; ok {
+			targetInst.RedistributeBGP = parseCLIEnableDisable(v, targetInst.RedistributeBGP)
+		}
+		if v, ok := params["redistribute-ospf"]; ok {
+			targetInst.RedistributeOSPF = parseCLIEnableDisable(v, targetInst.RedistributeOSPF)
+		}
+		if v, ok := params["redistribute-default"]; ok {
+			targetInst.RedistributeDefault = parseCLIEnableDisable(v, targetInst.RedistributeDefault)
+		}
+		if _, err := e.dynRoutingMgr.SaveBabelInstance(*targetInst); err != nil {
+			return ColorRed + "Error: " + err.Error() + "\n" + ColorReset
+		}
+		return ColorGreen + fmt.Sprintf("Babel instance %s updated.\n", targetInst.Name) + ColorReset
+
 	case "remove", "rem", "delete":
 		pos, _ := parseArgs(parts[1:])
 		if len(pos) == 0 {
@@ -3600,7 +3672,7 @@ func (e *Executor) handleBabelInstance(parts []string, user *auth.PublicUser) st
 		return ColorGreen + fmt.Sprintf("Babel instance %s disabled.\n", target) + ColorReset
 
 	default:
-		return ColorRed + "Usage: /routing babel instance [print|add|remove|enable|disable]\n" + ColorReset
+		return ColorRed + "Usage: /routing babel instance [print|add|set|remove|enable|disable]\n" + ColorReset
 	}
 }
 

+ 22 - 2
internal/dynamicrouting/manager.go

@@ -1123,7 +1123,11 @@ func (m *Manager) generateBirdConfig() string {
 		if inst.RoutingTable != "" && inst.RoutingTable != "main" {
 			sb.WriteString(fmt.Sprintf("        table %s;\n", inst.RoutingTable))
 		}
-		sb.WriteString("        import filter { krt_metric = 130; accept; };\n")
+		if inst.InFilter != "" && len(chainRules[inst.InFilter]) > 0 {
+			sb.WriteString(fmt.Sprintf("        import filter %s;\n", sanitizeIdentifier("filter", inst.InFilter)))
+		} else {
+			sb.WriteString("        import filter { krt_metric = 130; accept; };\n")
+		}
 
 		var babelExports []string
 		if inst.RedistributeStatic {
@@ -1139,7 +1143,23 @@ func (m *Manager) generateBirdConfig() string {
 			babelExports = append(babelExports, "RTS_BGP")
 		}
 
-		if len(babelExports) > 0 || inst.RedistributeDefault {
+		if inst.OutFilter != "" && len(chainRules[inst.OutFilter]) > 0 {
+			fnName := fmt.Sprintf("fn_%s", sanitizeIdentifier("filter", inst.OutFilter))
+			sb.WriteString("        export filter {\n")
+			if len(babelExports) > 0 || inst.RedistributeDefault {
+				var conds []string
+				if inst.RedistributeDefault {
+					conds = append(conds, "net = 0.0.0.0/0")
+				}
+				if len(babelExports) > 0 {
+					conds = append(conds, fmt.Sprintf("source ~ [ %s ]", strings.Join(babelExports, ", ")))
+				}
+				sb.WriteString(fmt.Sprintf("            if !( %s ) then reject;\n", strings.Join(conds, " || ")))
+			}
+			sb.WriteString(fmt.Sprintf("            %s();\n", fnName))
+			sb.WriteString("            reject;\n")
+			sb.WriteString("        };\n")
+		} else if len(babelExports) > 0 || inst.RedistributeDefault {
 			sb.WriteString("        export filter {\n")
 			if inst.RedistributeDefault {
 				sb.WriteString("            if net = 0.0.0.0/0 then accept;\n")

+ 56 - 0
internal/dynamicrouting/protocols_test.go

@@ -245,6 +245,62 @@ func TestBabelFallbackInterfaceAndInstanceManagement(t *testing.T) {
 	}
 }
 
+func TestBabelFiltersAndValidation(t *testing.T) {
+	tmpDir, err := os.MkdirTemp("", "debtik_babel_filter_test_*")
+	if err != nil {
+		t.Fatalf("Failed to create temp dir: %v", err)
+	}
+	defer os.RemoveAll(tmpDir)
+
+	mgr := &Manager{
+		configPath: tmpDir + "/dynamic.json",
+		config: DynamicConfig{
+			RoutingFilters: []RoutingFilterRule{
+				{Chain: "babel-in", RuleOrder: 10, Rule: "if (dst in 192.168.100.0/24) { accept; }"},
+				{Chain: "babel-out", RuleOrder: 10, Rule: "if (dst in 10.0.0.0/8) { accept; }"},
+			},
+			BabelInstances: []BabelInstance{
+				{
+					Name:               "test_babel",
+					RouterID:           "1.1.1.1",
+					InFilter:           "babel-in",
+					OutFilter:          "babel-out",
+					RedistributeStatic: true,
+				},
+			},
+			BabelInterfaces: []BabelInterface{
+				{Interface: "eth0", Instance: "test_babel"},
+			},
+		},
+	}
+
+	cfg := mgr.generateBirdConfig()
+	if !strings.Contains(cfg, "import filter filter_babel_in;") {
+		t.Fatalf("expected import filter filter_babel_in in config:\n%s", cfg)
+	}
+	if !strings.Contains(cfg, "fn_filter_babel_out();") {
+		t.Fatalf("expected fn_filter_babel_out() in export filter:\n%s", cfg)
+	}
+
+	if _, err := exec.LookPath("bird"); err == nil {
+		tmpFile, err := os.CreateTemp("", "bird_babel_filter_*.conf")
+		if err != nil {
+			t.Fatalf("Failed to create temp file: %v", err)
+		}
+		defer os.Remove(tmpFile.Name())
+		if _, err := tmpFile.WriteString(cfg); err != nil {
+			t.Fatalf("Failed to write temp file: %v", err)
+		}
+		tmpFile.Close()
+
+		cmd := exec.Command("bird", "-p", "-c", tmpFile.Name())
+		out, err := cmd.CombinedOutput()
+		if err != nil {
+			t.Fatalf("bird -p validation failed on babel filter config: %s\nConfig:\n%s", string(out), cfg)
+		}
+	}
+}
+
 func TestBFDAndRIPAndRAdvManagement(t *testing.T) {
 	tmpDir, err := os.MkdirTemp("", "debtik_protocols_test_*")
 	if err != nil {

+ 8 - 0
internal/dynamicrouting/types.go

@@ -125,6 +125,8 @@ type OSPFInstance struct {
 	Enabled               bool   `json:"enabled"`
 	Disabled              bool   `json:"disabled"`
 	Comment               string `json:"comment,omitempty"`
+	InFilter              string `json:"in_filter,omitempty"`
+	OutFilter             string `json:"out_filter,omitempty"`
 	RedistributeConnected bool   `json:"redistribute_connected"`
 	RedistributeStatic    bool   `json:"redistribute_static"`
 	RedistributeBGP       bool   `json:"redistribute_bgp"`
@@ -338,6 +340,8 @@ type RIPInstance struct {
 	Enabled               bool   `json:"enabled"`
 	Disabled              bool   `json:"disabled"`
 	Comment               string `json:"comment,omitempty"`
+	InFilter              string `json:"in_filter,omitempty"`
+	OutFilter             string `json:"out_filter,omitempty"`
 	RedistributeConnected bool   `json:"redistribute_connected"`
 	RedistributeStatic    bool   `json:"redistribute_static"`
 	RedistributeOSPF      bool   `json:"redistribute_ospf"`
@@ -479,6 +483,8 @@ type BabelInstance struct {
 	Enabled               bool   `json:"enabled"`
 	Disabled              bool   `json:"disabled"`
 	Comment               string `json:"comment,omitempty"`
+	InFilter              string `json:"in_filter,omitempty"`
+	OutFilter             string `json:"out_filter,omitempty"`
 	RedistributeConnected bool   `json:"redistribute_connected"`
 	RedistributeStatic    bool   `json:"redistribute_static"`
 	RedistributeOSPF      bool   `json:"redistribute_ospf"`
@@ -577,6 +583,8 @@ type EIGRPInstance struct {
 	Disabled              bool   `json:"disabled"`
 	Running               bool   `json:"running"`
 	Comment               string `json:"comment,omitempty"`
+	InFilter              string `json:"in_filter,omitempty"`
+	OutFilter             string `json:"out_filter,omitempty"`
 	RedistributeConnected bool   `json:"redistribute_connected"`
 	RedistributeStatic    bool   `json:"redistribute_static"`
 	RedistributeOSPF      bool   `json:"redistribute_ospf"`

+ 10 - 0
internal/web/static/app.js

@@ -20488,6 +20488,9 @@ function renderBabelInstances() {
     if (inst.redistribute_bgp) redist.push('bgp');
     if (inst.redistribute_ospf) redist.push('ospf');
     if (inst.redistribute_default) redist.push('default');
+    const filterBadges = [];
+    if (inst.in_filter) filterBadges.push(`<span class="px-1.5 py-0.5 rounded text-[10px] bg-blue-950 text-blue-300 border border-blue-800/50" title="In Filter">IN: ${escapeHtml(inst.in_filter)}</span>`);
+    if (inst.out_filter) filterBadges.push(`<span class="px-1.5 py-0.5 rounded text-[10px] bg-purple-950 text-purple-300 border border-purple-800/50" title="Out Filter">OUT: ${escapeHtml(inst.out_filter)}</span>`);
     return `
       <tr class="${selClass} cursor-pointer transition select-none" onclick="selectBabelRow('instances', '${inst.id}')" ondblclick="openEditBabelInstanceModal('${inst.id}')">
         <td class="py-1.5 px-3 w-8 text-center text-slate-500">${idx}</td>
@@ -20495,6 +20498,7 @@ function renderBabelInstances() {
         <td class="py-1.5 px-3 text-slate-400 italic">${escapeHtml(inst.comment || '')}</td>
         <td class="py-1.5 px-3 font-semibold text-white flex items-center gap-1.5">
           <span class="text-teal-400">↳</span> <span>${escapeHtml(inst.name || 'default')}</span>
+          ${filterBadges.join(' ')}
         </td>
         <td class="py-1.5 px-3 font-mono text-cyan-300">${escapeHtml(inst.router_id || 'auto')}</td>
         <td class="py-1.5 px-3 text-emerald-400 font-bold">IPv4 + IPv6</td>
@@ -20611,6 +20615,8 @@ function openBabelInstanceModal() {
   document.getElementById('modal-babel-instance-routerid').value = '';
   document.getElementById('modal-babel-instance-metric').value = '10';
   document.getElementById('modal-babel-instance-comment').value = '';
+  document.getElementById('modal-babel-instance-in-filter').value = '';
+  document.getElementById('modal-babel-instance-out-filter').value = '';
   document.getElementById('modal-babel-instance-enabled').checked = true;
   document.getElementById('modal-babel-instance-redist-conn').checked = false;
   document.getElementById('modal-babel-instance-redist-static').checked = false;
@@ -20629,6 +20635,8 @@ function openEditBabelInstanceModal(id) {
   document.getElementById('modal-babel-instance-routerid').value = inst.router_id || '';
   document.getElementById('modal-babel-instance-metric').value = inst.default_metric || 10;
   document.getElementById('modal-babel-instance-comment').value = inst.comment || '';
+  document.getElementById('modal-babel-instance-in-filter').value = inst.in_filter || '';
+  document.getElementById('modal-babel-instance-out-filter').value = inst.out_filter || '';
   document.getElementById('modal-babel-instance-enabled').checked = !inst.disabled;
   document.getElementById('modal-babel-instance-redist-conn').checked = !!inst.redistribute_connected;
   document.getElementById('modal-babel-instance-redist-static').checked = !!inst.redistribute_static;
@@ -20652,6 +20660,8 @@ async function submitBabelInstance(e) {
     router_id: document.getElementById('modal-babel-instance-routerid').value.trim(),
     default_metric: parseInt(document.getElementById('modal-babel-instance-metric').value, 10) || 10,
     comment: document.getElementById('modal-babel-instance-comment').value.trim(),
+    in_filter: (document.getElementById('modal-babel-instance-in-filter')?.value || '').trim(),
+    out_filter: (document.getElementById('modal-babel-instance-out-filter')?.value || '').trim(),
     enabled: isEnabled,
     disabled: !isEnabled,
     redistribute_connected: document.getElementById('modal-babel-instance-redist-conn').checked,

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

@@ -12454,6 +12454,18 @@
             <input type="text" id="modal-babel-instance-comment" placeholder="Optional notes" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white">
           </div>
         </div>
+        <div class="grid grid-cols-2 gap-4">
+          <div>
+            <label class="block font-semibold text-slate-300 mb-1">In Filter</label>
+            <input type="text" list="filter-chains-list" id="modal-babel-instance-in-filter" placeholder="e.g. Internal, babel-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 routes received from neighbors</p>
+          </div>
+          <div>
+            <label class="block font-semibold text-slate-300 mb-1">Out Filter</label>
+            <input type="text" list="filter-chains-list" id="modal-babel-instance-out-filter" placeholder="e.g. Internal, babel-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 routes advertised to neighbors</p>
+          </div>
+        </div>
         <div class="border-t border-slate-800 pt-3">
           <label class="block font-semibold text-slate-300 mb-2">Route Redistribution</label>
           <div class="grid grid-cols-3 gap-2 text-slate-300">