浏览代码

fix(babel): fix instance running state, add live interfaces telemetry, template instance binding and BIRD 2 quoted interface syntax

Proxmox Router 2 天之前
父节点
当前提交
073636b116

+ 1 - 1
VERSION

@@ -1 +1 @@
-1.8.11
+1.8.12

二进制
bin/debtik


二进制
bin/debtik-arm64


二进制
bin/debtik-cli


二进制
bin/debtik-cli-arm64


+ 1 - 0
internal/api/api.go

@@ -436,6 +436,7 @@ func (s *Server) registerRoutes() {
 	s.mux.HandleFunc("POST /api/routing/babel/instances/{id}/state", s.handleSetBabelInstanceState)
 
 	s.mux.HandleFunc("GET /api/routing/babel/interfaces", s.handleListBabelInterfaces)
+	s.mux.HandleFunc("GET /api/routing/babel/interfaces-live", s.handleGetBabelInterfacesLive)
 	s.mux.HandleFunc("POST /api/routing/babel/interfaces", s.handleSaveBabelInterface)
 	s.mux.HandleFunc("DELETE /api/routing/babel/interfaces/{id}", s.handleDeleteBabelInterface)
 	s.mux.HandleFunc("POST /api/routing/babel/interfaces/{id}/state", s.handleSetBabelInterfaceState)

+ 8 - 0
internal/api/dynamic_routing_handlers.go

@@ -970,6 +970,14 @@ func (s *Server) handleListBabelInterfaces(w http.ResponseWriter, r *http.Reques
 	writeJSON(w, http.StatusOK, ifaces)
 }
 
+func (s *Server) handleGetBabelInterfacesLive(w http.ResponseWriter, r *http.Request) {
+	ifaces := s.dynRoutingMgr.GetBabelInterfacesLive()
+	if ifaces == nil {
+		ifaces = []dynamicrouting.BabelInterfaceLive{}
+	}
+	writeJSON(w, http.StatusOK, ifaces)
+}
+
 func (s *Server) handleSaveBabelInterface(w http.ResponseWriter, r *http.Request) {
 	var iface dynamicrouting.BabelInterface
 	if err := json.NewDecoder(r.Body).Decode(&iface); err != nil {

+ 6 - 1
internal/cli/completer.go

@@ -1133,7 +1133,7 @@ func (c *Completer) buildCommandTree() *Node {
 	babelInstAdd.AddChild(&Node{Name: "redistribute-ospf=", Dynamic: func() []string { return []string{"yes", "no"} }})
 	babelInstAdd.AddChild(&Node{Name: "redistribute-bgp=", Dynamic: func() []string { return []string{"yes", "no"} }})
 
-	babelIface := babelNode.AddChild(&Node{Name: "interface", Aliases: []string{"interfaces"}})
+	babelIface := babelNode.AddChild(&Node{Name: "interface", Aliases: []string{"interfaces", "interface-template", "interface-templates"}})
 	addPrint(babelIface)
 	addRemove(babelIface)
 	addEnableDisable(babelIface)
@@ -1144,6 +1144,11 @@ func (c *Completer) buildCommandTree() *Node {
 	babelIfaceAdd.AddChild(&Node{Name: "rxcost="})
 	babelIfaceAdd.AddChild(&Node{Name: "hello-interval="})
 	babelIfaceAdd.AddChild(&Node{Name: "update-interval="})
+	babelIfaceAdd.AddChild(&Node{Name: "auth=", Dynamic: func() []string { return []string{"none", "mac"} }})
+	babelIfaceAdd.AddChild(&Node{Name: "auth-key="})
+
+	babelIfaceLive := babelNode.AddChild(&Node{Name: "interface-live", Aliases: []string{"interfaces-live", "live"}})
+	addPrint(babelIfaceLive)
 
 	babelNbr := babelNode.AddChild(&Node{Name: "neighbor", Aliases: []string{"neighbors"}})
 	addPrint(babelNbr)

+ 57 - 11
internal/cli/executor.go

@@ -3362,38 +3362,53 @@ func (e *Executor) handleRoutingBabel(parts []string, user *auth.PublicUser) str
 	case "instance", "instances":
 		return e.handleBabelInstance(parts[1:], user)
 
+	case "interface-template", "interface-templates", "template", "templates":
+		return e.handleBabelInterfaceTemplate(parts[1:], user)
+
 	case "interface", "interfaces":
 		return e.handleBabelInterface(parts[1:], user)
 
+	case "interface-live", "interfaces-live", "live":
+		return e.handleBabelInterfacesLive(parts[1:], user)
+
 	case "neighbor", "neighbors":
 		nbrs := e.dynRoutingMgr.GetBabelNeighbors()
-		tbl := NewTable("IP-ADDRESS", "INTERFACE", "METRIC", "COST", "UPTIME")
+		tbl := NewTable("IP-ADDRESS", "INTERFACE", "METRIC", "COST", "ROUTES", "HELLOS", "EXPIRES", "AUTH", "RTT")
 		for _, n := range nbrs {
-			tbl.AddRow(n.IP, n.Interface, fmt.Sprintf("%d", n.Metric), fmt.Sprintf("%d", n.Cost), n.Uptime)
+			rttStr := ""
+			if n.RTT > 0 {
+				rttStr = fmt.Sprintf("%.2fms", n.RTT)
+			}
+			tbl.AddRow(n.IP, n.Interface, fmt.Sprintf("%d", n.Metric), fmt.Sprintf("%d", n.Cost), fmt.Sprintf("%d", n.Routes), fmt.Sprintf("%d", n.Hellos), n.Expires, n.Auth, rttStr)
 		}
 		return tbl.String()
 
 	case "routes", "route":
 		routes := e.dynRoutingMgr.GetBabelRoutes()
-		tbl := NewTable("PREFIX", "NEXT-HOP", "INTERFACE", "METRIC")
+		tbl := NewTable("PREFIX", "NEXT-HOP", "INTERFACE", "METRIC", "FLAG", "SEQNO", "EXPIRES")
 		for _, r := range routes {
-			tbl.AddRow(r.Prefix, r.NextHop, r.Interface, fmt.Sprintf("%d", r.Metric))
+			tbl.AddRow(r.Prefix, r.NextHop, r.Interface, fmt.Sprintf("%d", r.Metric), r.Flag, fmt.Sprintf("%d", r.Seqno), r.Expires)
 		}
 		return tbl.String()
 
 	default:
-		return ColorRed + "Usage: /routing babel [instance|interface|neighbor|routes]\n" + ColorReset
+		return ColorRed + "Usage: /routing babel [instance|interface|interface-template|interface-live|neighbor|routes]\n" + ColorReset
 	}
 }
 
 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("#", "NAME", "ROUTER-ID", "REDISTRIBUTE", "STATUS")
+		tbl := NewTable("#", "FLAGS", "NAME", "ROUTER-ID", "REDISTRIBUTE", "METRIC", "STATUS")
 		for idx, inst := range instances {
 			st := "enabled"
+			flags := ""
+			if inst.Running {
+				flags = "R"
+			}
 			if inst.Disabled {
 				st = "disabled"
+				flags = "X"
 			}
 			var redist []string
 			if inst.RedistributeConnected {
@@ -3419,7 +3434,7 @@ func (e *Executor) handleBabelInstance(parts []string, user *auth.PublicUser) st
 			if rID == "" {
 				rID = "auto"
 			}
-			tbl.AddRow(fmt.Sprintf("%d", idx), inst.Name, rID, redistStr, st)
+			tbl.AddRow(fmt.Sprintf("%d", idx), flags, inst.Name, rID, redistStr, fmt.Sprintf("%d", inst.DefaultMetric), st)
 		}
 		return tbl.String()
 	}
@@ -3505,7 +3520,38 @@ func (e *Executor) handleBabelInstance(parts []string, user *auth.PublicUser) st
 	}
 }
 
+func (e *Executor) handleBabelInterfacesLive(parts []string, user *auth.PublicUser) string {
+	ifaces := e.dynRoutingMgr.GetBabelInterfacesLive()
+	tbl := NewTable("#", "FLAGS", "INTERFACE", "INSTANCE", "STATE", "AUTH", "RXCOST", "NBRS", "TIMER", "NEXT-HOP-V4", "NEXT-HOP-V6")
+	for idx, iface := range ifaces {
+		tbl.AddRow(fmt.Sprintf("%d", idx), "D", iface.Interface, iface.Instance, iface.State, iface.Auth, fmt.Sprintf("%d", iface.RxCost), fmt.Sprintf("%d", iface.Neighbors), iface.Timer, iface.NextHopV4, iface.NextHopV6)
+	}
+	return tbl.String()
+}
+
 func (e *Executor) handleBabelInterface(parts []string, user *auth.PublicUser) string {
+	if len(parts) == 0 || parts[0] == "print" || parts[0] == "pr" {
+		isTemplate := false
+		for _, p := range parts {
+			if strings.EqualFold(p, "template") || strings.EqualFold(p, "templates") {
+				isTemplate = true
+				break
+			}
+		}
+		liveIfaces := e.dynRoutingMgr.GetBabelInterfacesLive()
+		if !isTemplate && len(liveIfaces) > 0 {
+			tbl := NewTable("#", "FLAGS", "INTERFACE", "INSTANCE", "STATE", "AUTH", "RXCOST", "NBRS", "TIMER", "NEXT-HOP-V4", "NEXT-HOP-V6")
+			for idx, iface := range liveIfaces {
+				tbl.AddRow(fmt.Sprintf("%d", idx), "D", iface.Interface, iface.Instance, iface.State, iface.Auth, fmt.Sprintf("%d", iface.RxCost), fmt.Sprintf("%d", iface.Neighbors), iface.Timer, iface.NextHopV4, iface.NextHopV6)
+			}
+			return tbl.String()
+		}
+		return e.handleBabelInterfaceTemplate([]string{"print"}, user)
+	}
+	return e.handleBabelInterfaceTemplate(parts, user)
+}
+
+func (e *Executor) handleBabelInterfaceTemplate(parts []string, user *auth.PublicUser) string {
 	if len(parts) == 0 || parts[0] == "print" || parts[0] == "pr" {
 		ifaces := e.dynRoutingMgr.ListBabelInterfaces()
 		tbl := NewTable("#", "INTERFACE", "INSTANCE", "TYPE", "RXCOST", "HELLO-INT", "UPDATE-INT", "STATUS")
@@ -3568,7 +3614,7 @@ func (e *Executor) handleBabelInterface(parts []string, user *auth.PublicUser) s
 		if _, err := e.dynRoutingMgr.SaveBabelInterface(iface); err != nil {
 			return ColorRed + "Error adding Babel interface: " + err.Error() + "\n" + ColorReset
 		}
-		return ColorGreen + fmt.Sprintf("Babel interface %s added.\n", ifaceName) + ColorReset
+		return ColorGreen + fmt.Sprintf("Babel interface template %s added.\n", ifaceName) + ColorReset
 
 	case "remove", "rem", "delete":
 		pos, _ := parseArgs(parts[1:])
@@ -3594,7 +3640,7 @@ func (e *Executor) handleBabelInterface(parts []string, user *auth.PublicUser) s
 		if err := e.dynRoutingMgr.DeleteBabelInterface(ifaceID); err != nil {
 			return ColorRed + "Error: " + err.Error() + "\n" + ColorReset
 		}
-		return ColorGreen + fmt.Sprintf("Babel interface %s removed.\n", target) + ColorReset
+		return ColorGreen + fmt.Sprintf("Babel interface template %s removed.\n", target) + ColorReset
 
 	case "enable":
 		pos, _ := parseArgs(parts[1:])
@@ -3605,7 +3651,7 @@ func (e *Executor) handleBabelInterface(parts []string, user *auth.PublicUser) s
 		if err := e.dynRoutingMgr.SetBabelInterfaceState(target, false); err != nil {
 			return ColorRed + "Error: " + err.Error() + "\n" + ColorReset
 		}
-		return ColorGreen + fmt.Sprintf("Babel interface %s enabled.\n", target) + ColorReset
+		return ColorGreen + fmt.Sprintf("Babel interface template %s enabled.\n", target) + ColorReset
 
 	case "disable":
 		pos, _ := parseArgs(parts[1:])
@@ -3616,7 +3662,7 @@ func (e *Executor) handleBabelInterface(parts []string, user *auth.PublicUser) s
 		if err := e.dynRoutingMgr.SetBabelInterfaceState(target, true); err != nil {
 			return ColorRed + "Error: " + err.Error() + "\n" + ColorReset
 		}
-		return ColorGreen + fmt.Sprintf("Babel interface %s disabled.\n", target) + ColorReset
+		return ColorGreen + fmt.Sprintf("Babel interface template %s disabled.\n", target) + ColorReset
 
 	default:
 		return ColorRed + "Usage: /routing babel interface [print|add|remove|enable|disable]\n" + ColorReset

+ 165 - 27
internal/dynamicrouting/manager.go

@@ -1126,13 +1126,15 @@ func (m *Manager) generateBirdConfig() string {
 		sb.WriteString("        export all;\n")
 		sb.WriteString("    };\n\n")
 
+		hasIface := false
 		for _, iface := range m.config.BabelInterfaces {
 			if iface.Disabled {
 				continue
 			}
-			if iface.Instance != "" && !strings.EqualFold(iface.Instance, inst.Name) && !strings.EqualFold(iface.Instance, "Default") {
+			if iface.Instance != "" && !strings.EqualFold(iface.Instance, inst.Name) && !strings.EqualFold(iface.Instance, "Default") && !strings.EqualFold(iface.Instance, "*") && !strings.EqualFold(iface.Instance, "all") {
 				continue
 			}
+			hasIface = true
 			ifName := strings.TrimSpace(iface.Interface)
 			if ifName == "" {
 				ifName = "*"
@@ -1158,6 +1160,11 @@ func (m *Manager) generateBirdConfig() string {
 			}
 			sb.WriteString("    };\n")
 		}
+		if !hasIface {
+			sb.WriteString("    interface \"*\" {\n")
+			sb.WriteString("        type wired;\n")
+			sb.WriteString("    };\n")
+		}
 		sb.WriteString("}\n\n")
 	}
 
@@ -3282,7 +3289,7 @@ func (m *Manager) ListBabelInstances() []BabelInstance {
 	res := make([]BabelInstance, len(m.config.BabelInstances))
 	copy(res, m.config.BabelInstances)
 	for i := range res {
-		res[i].Running = birdRunning && res[i].Enabled && !res[i].Disabled
+		res[i].Running = birdRunning && (res[i].Enabled || !res[i].Disabled) && !res[i].Disabled
 	}
 	return res
 }
@@ -3297,6 +3304,12 @@ func (m *Manager) SaveBabelInstance(inst BabelInstance) (*BabelInstance, error)
 	if inst.Name == "" {
 		inst.Name = "default"
 	}
+	if !inst.Disabled && !inst.Enabled {
+		inst.Enabled = true
+	}
+	if inst.Disabled {
+		inst.Enabled = false
+	}
 
 	found := false
 	for i, existing := range m.config.BabelInstances {
@@ -3373,6 +3386,13 @@ func (m *Manager) SaveBabelInterface(iface BabelInterface) (*BabelInterface, err
 	if iface.Interface == "" {
 		iface.Interface = "*"
 	}
+	if iface.Instance == "" {
+		if len(m.config.BabelInstances) > 0 {
+			iface.Instance = m.config.BabelInstances[0].Name
+		} else {
+			iface.Instance = "default"
+		}
+	}
 	if iface.Type == "" {
 		iface.Type = "wired"
 	}
@@ -3442,6 +3462,78 @@ func (m *Manager) SetBabelInterfaceState(id string, disabled bool) error {
 	return m.applyBirdConfig()
 }
 
+func (m *Manager) resolveBabelInstanceName(protoName string) string {
+	protoName = strings.TrimSuffix(protoName, ":")
+	for _, inst := range m.config.BabelInstances {
+		if strings.Contains(protoName, inst.Name) {
+			return inst.Name
+		}
+	}
+	name := strings.TrimPrefix(protoName, "babel_")
+	if idx := strings.LastIndex(name, "_"); idx > 0 {
+		name = name[:idx]
+	}
+	if name == "" {
+		name = "default"
+	}
+	return name
+}
+
+func (m *Manager) GetBabelInterfacesLive() []BabelInterfaceLive {
+	m.mu.RLock()
+	defer m.mu.RUnlock()
+
+	var list []BabelInterfaceLive
+	out, err := birdcExec("show babel interfaces")
+	if err != nil {
+		return list
+	}
+
+	lines := strings.Split(string(out), "\n")
+	currentProto := ""
+	for _, line := range lines {
+		trimmed := strings.TrimSpace(line)
+		if trimmed == "" || strings.HasPrefix(trimmed, "BIRD") || strings.HasPrefix(trimmed, "Interface ") || strings.Contains(trimmed, "There is no Babel") {
+			continue
+		}
+		if strings.HasSuffix(trimmed, ":") {
+			currentProto = strings.TrimSuffix(trimmed, ":")
+			continue
+		}
+		fields := strings.Fields(trimmed)
+		if len(fields) >= 6 {
+			rxCost := 0
+			fmt.Sscanf(fields[3], "%d", &rxCost)
+			nbrs := 0
+			fmt.Sscanf(fields[4], "%d", &nbrs)
+
+			nextHopV4 := ""
+			nextHopV6 := ""
+			if len(fields) >= 7 {
+				nextHopV4 = fields[6]
+			}
+			if len(fields) >= 8 {
+				nextHopV6 = fields[7]
+			}
+
+			instName := m.resolveBabelInstanceName(currentProto)
+
+			list = append(list, BabelInterfaceLive{
+				Instance:  instName,
+				Interface: fields[0],
+				State:     fields[1],
+				Auth:      fields[2],
+				RxCost:    rxCost,
+				Neighbors: nbrs,
+				Timer:     fields[5],
+				NextHopV4: nextHopV4,
+				NextHopV6: nextHopV6,
+			})
+		}
+	}
+	return list
+}
+
 func (m *Manager) GetBabelNeighbors() []BabelNeighbor {
 	var list []BabelNeighbor
 	out, err := birdcExec("show babel neighbors")
@@ -3451,7 +3543,7 @@ func (m *Manager) GetBabelNeighbors() []BabelNeighbor {
 	lines := strings.Split(string(out), "\n")
 	for _, line := range lines {
 		trimmed := strings.TrimSpace(line)
-		if trimmed == "" || strings.HasPrefix(trimmed, "BIRD") || strings.HasPrefix(trimmed, "IP address") || strings.Contains(trimmed, "There is no Babel") {
+		if trimmed == "" || strings.HasPrefix(trimmed, "BIRD") || strings.HasPrefix(trimmed, "IP address") || strings.Contains(trimmed, "There is no Babel") || strings.HasSuffix(trimmed, ":") {
 			continue
 		}
 		fields := strings.Fields(trimmed)
@@ -3464,10 +3556,19 @@ func (m *Manager) GetBabelNeighbors() []BabelNeighbor {
 				fmt.Sscanf(fields[2], "%d", &nbr.Metric)
 			}
 			if len(fields) >= 4 {
-				fmt.Sscanf(fields[3], "%d", &nbr.Cost)
+				fmt.Sscanf(fields[3], "%d", &nbr.Routes)
 			}
 			if len(fields) >= 5 {
-				nbr.Uptime = fields[4]
+				fmt.Sscanf(fields[4], "%d", &nbr.Hellos)
+			}
+			if len(fields) >= 6 {
+				nbr.Expires = fields[5]
+			}
+			if len(fields) >= 7 {
+				nbr.Auth = fields[6]
+			}
+			if len(fields) >= 8 {
+				fmt.Sscanf(fields[7], "%f", &nbr.RTT)
 			}
 			list = append(list, nbr)
 		}
@@ -3477,31 +3578,68 @@ func (m *Manager) GetBabelNeighbors() []BabelNeighbor {
 
 func (m *Manager) GetBabelRoutes() []BabelRoute {
 	var routes []BabelRoute
-	out, err := birdcExec("show route where source = RTS_BABEL all")
-	if err != nil {
-		return routes
-	}
-	lines := strings.Split(string(out), "\n")
-	for _, line := range lines {
-		trimmed := strings.TrimSpace(line)
-		if trimmed == "" || strings.HasPrefix(trimmed, "BIRD") || strings.HasPrefix(trimmed, "Table") {
-			continue
-		}
-		if !strings.HasPrefix(line, "\t") && !strings.HasPrefix(line, " ") {
+	out, err := birdcExec("show babel routes")
+	if err == nil {
+		lines := strings.Split(string(out), "\n")
+		for _, line := range lines {
+			trimmed := strings.TrimSpace(line)
+			if trimmed == "" || strings.HasPrefix(trimmed, "BIRD") || strings.HasPrefix(trimmed, "Prefix") || strings.Contains(trimmed, "There is no Babel") || strings.HasSuffix(trimmed, ":") {
+				continue
+			}
 			fields := strings.Fields(trimmed)
-			if len(fields) >= 2 {
-				rt := BabelRoute{
-					Prefix: fields[0],
+			if len(fields) >= 4 {
+				metric := 0
+				fmt.Sscanf(fields[3], "%d", &metric)
+				flag := ""
+				seqno := 0
+				expires := ""
+				if len(fields) >= 5 {
+					flag = fields[4]
 				}
-				for i := 1; i < len(fields); i++ {
-					if fields[i] == "via" && i+1 < len(fields) {
-						rt.NextHop = fields[i+1]
-					}
-					if fields[i] == "dev" && i+1 < len(fields) {
-						rt.Interface = fields[i+1]
+				if len(fields) >= 6 {
+					fmt.Sscanf(fields[5], "%d", &seqno)
+				}
+				if len(fields) >= 7 {
+					expires = fields[6]
+				}
+				routes = append(routes, BabelRoute{
+					Prefix:    fields[0],
+					NextHop:   fields[1],
+					Interface: fields[2],
+					Metric:    metric,
+					Flag:      flag,
+					Seqno:     seqno,
+					Expires:   expires,
+				})
+			}
+		}
+	}
+	if len(routes) == 0 {
+		outK, errK := birdcExec("show route where source = RTS_BABEL all")
+		if errK == nil {
+			lines := strings.Split(string(outK), "\n")
+			for _, line := range lines {
+				trimmed := strings.TrimSpace(line)
+				if trimmed == "" || strings.HasPrefix(trimmed, "BIRD") || strings.HasPrefix(trimmed, "Table") {
+					continue
+				}
+				if !strings.HasPrefix(line, "\t") && !strings.HasPrefix(line, " ") {
+					fields := strings.Fields(trimmed)
+					if len(fields) >= 2 {
+						rt := BabelRoute{
+							Prefix: fields[0],
+						}
+						for i := 1; i < len(fields); i++ {
+							if fields[i] == "via" && i+1 < len(fields) {
+								rt.NextHop = fields[i+1]
+							}
+							if fields[i] == "dev" && i+1 < len(fields) {
+								rt.Interface = fields[i+1]
+							}
+						}
+						routes = append(routes, rt)
 					}
 				}
-				routes = append(routes, rt)
 			}
 		}
 	}
@@ -3744,7 +3882,7 @@ func (m *Manager) GetSummary() DynamicRoutingSummary {
 	}
 
 	for _, inst := range m.config.BabelInstances {
-		if inst.Enabled && !inst.Disabled {
+		if (inst.Enabled || !inst.Disabled) && !inst.Disabled {
 			summary.BabelEnabled = true
 			break
 		}

+ 74 - 0
internal/dynamicrouting/protocols_test.go

@@ -169,3 +169,77 @@ func TestAllProtocolsBirdConfigGeneration(t *testing.T) {
 		}
 	}
 }
+
+func TestBabelFallbackInterfaceAndInstanceManagement(t *testing.T) {
+	tmpDir, err := os.MkdirTemp("", "debtik_babel_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{
+			BabelInstances:  []BabelInstance{},
+			BabelInterfaces: []BabelInterface{},
+		},
+	}
+
+	// 1. Save Babel instance without explicit enabled flag
+	inst, err := mgr.SaveBabelInstance(BabelInstance{
+		Name:     "internal-Maiden2",
+		RouterID: "192.168.132.1",
+	})
+	if err != nil {
+		t.Fatalf("SaveBabelInstance failed: %v", err)
+	}
+	if !inst.Enabled {
+		t.Errorf("Expected inst.Enabled to be true when Disabled is false, got false")
+	}
+
+	// 2. Generate BIRD config when no interfaces exist - should generate fallback interface "*"
+	cfg := mgr.generateBirdConfig()
+	if !strings.Contains(cfg, `protocol babel babel_internal_Maiden2_0`) {
+		t.Errorf("Expected protocol babel in config, got:\n%s", cfg)
+	}
+	if !strings.Contains(cfg, `interface "*"`) {
+		t.Errorf("Expected fallback interface \"*\" in config, got:\n%s", cfg)
+	}
+
+	// 3. Save Babel interface without explicit instance - should default to "internal-Maiden2"
+	iface, err := mgr.SaveBabelInterface(BabelInterface{
+		Interface: "eth0",
+		RxCost:    96,
+	})
+	if err != nil {
+		t.Fatalf("SaveBabelInterface failed: %v", err)
+	}
+	if iface.Instance != "internal-Maiden2" {
+		t.Errorf("Expected iface.Instance to default to internal-Maiden2, got %q", iface.Instance)
+	}
+
+	// 4. Regenerate config - now should contain eth0 template
+	cfg2 := mgr.generateBirdConfig()
+	if !strings.Contains(cfg2, `interface "eth0"`) {
+		t.Errorf("Expected interface \"eth0\" in config, got:\n%s", cfg2)
+	}
+
+	// 5. Validate with bird -p if bird is installed
+	if _, err := exec.LookPath("bird"); err == nil {
+		tmpFile, err := os.CreateTemp("", "bird_babel_val_*.conf")
+		if err != nil {
+			t.Fatalf("Failed to create temp file: %v", err)
+		}
+		defer os.Remove(tmpFile.Name())
+		if _, err := tmpFile.WriteString(cfg2); 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 config: %s\nConfig:\n%s", string(out), cfg2)
+		}
+	}
+}

+ 26 - 5
internal/dynamicrouting/types.go

@@ -432,11 +432,16 @@ type BabelInterface struct {
 
 // BabelNeighbor represents a live Babel neighbor (/routing babel neighbor)
 type BabelNeighbor struct {
-	IP        string `json:"ip"`
-	Interface string `json:"interface"`
-	Metric    int    `json:"metric"`
-	Cost      int    `json:"cost"`
-	Uptime    string `json:"uptime"`
+	IP        string  `json:"ip"`
+	Interface string  `json:"interface"`
+	Metric    int     `json:"metric"`
+	Cost      int     `json:"cost,omitempty"`
+	Routes    int     `json:"routes,omitempty"`
+	Hellos    int     `json:"hellos,omitempty"`
+	Uptime    string  `json:"uptime,omitempty"`
+	Expires   string  `json:"expires,omitempty"`
+	Auth      string  `json:"auth,omitempty"`
+	RTT       float64 `json:"rtt,omitempty"`
 }
 
 // BabelRoute represents an active Babel route (/routing babel route)
@@ -445,6 +450,22 @@ type BabelRoute struct {
 	NextHop   string `json:"next_hop"`
 	Metric    int    `json:"metric"`
 	Interface string `json:"interface"`
+	Flag      string `json:"flag,omitempty"`
+	Seqno     int    `json:"seqno,omitempty"`
+	Expires   string `json:"expires,omitempty"`
+}
+
+// BabelInterfaceLive represents a running Babel interface reported by BIRD (/routing babel interface)
+type BabelInterfaceLive struct {
+	Instance  string `json:"instance"`
+	Interface string `json:"interface"`
+	State     string `json:"state"`
+	Auth      string `json:"auth"`
+	RxCost    int    `json:"rxcost"`
+	Neighbors int    `json:"neighbors"`
+	Timer     string `json:"timer"`
+	NextHopV4 string `json:"next_hop_v4"`
+	NextHopV6 string `json:"next_hop_v6"`
 }
 
 // RAdvInstance represents an IPv6 Router Advertisement instance (/routing radv instance)

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

@@ -9834,7 +9834,7 @@ async function loadDynamicRouting(showToastMsg = false) {
       selectRules, commLists, asLists,
       ripInsts, ripIfaces, ripNbrsLive, ripRoutes,
       bfdInst, bfdIfaces, bfdPeers, bfdSessions,
-      babelInsts, babelIfaces, babelNbrs, babelRoutes,
+      babelInsts, babelIfaces, babelLiveIfaces, babelNbrs, babelRoutes,
       radvInsts, radvIfaces
     ] = await Promise.all([
       fetchSafeJson(`${API_BASE}/routing/dynamic/summary`),
@@ -9863,6 +9863,7 @@ async function loadDynamicRouting(showToastMsg = false) {
       fetchSafeJson(`${API_BASE}/routing/bfd/sessions`),
       fetchSafeJson(`${API_BASE}/routing/babel/instances`),
       fetchSafeJson(`${API_BASE}/routing/babel/interfaces`),
+      fetchSafeJson(`${API_BASE}/routing/babel/interfaces-live`),
       fetchSafeJson(`${API_BASE}/routing/babel/neighbors`),
       fetchSafeJson(`${API_BASE}/routing/babel/routes`),
       fetchSafeJson(`${API_BASE}/routing/radv/instances`),
@@ -9898,6 +9899,7 @@ async function loadDynamicRouting(showToastMsg = false) {
 
     if (babelInsts) state.babelInstances = babelInsts;
     if (babelIfaces) state.babelInterfaces = babelIfaces;
+    if (babelLiveIfaces) state.babelInterfacesLive = babelLiveIfaces;
     if (babelNbrs) state.babelNeighbors = babelNbrs;
     if (babelRoutes) state.babelRoutes = babelRoutes;
 
@@ -9930,6 +9932,7 @@ async function loadDynamicRouting(showToastMsg = false) {
 
     try { renderBabelInstances(); } catch(e) { console.error('renderBabelInstances error:', e); }
     try { renderBabelInterfaces(); } catch(e) { console.error('renderBabelInterfaces error:', e); }
+    try { renderBabelLiveInterfaces(); } catch(e) { console.error('renderBabelLiveInterfaces error:', e); }
     try { renderBabelNeighbors(); } catch(e) { console.error('renderBabelNeighbors error:', e); }
     try { renderBabelRoutes(); } catch(e) { console.error('renderBabelRoutes error:', e); }
 
@@ -20275,6 +20278,7 @@ async function removeSelectedBfdPeer() {
 // ==============================================================================
 state.babelInstances = [];
 state.babelInterfaces = [];
+state.babelInterfacesLive = [];
 state.babelNeighbors = [];
 state.babelRoutes = [];
 state.selectedBabelRow = {};
@@ -20282,7 +20286,7 @@ state.activeBabelTab = 'instances';
 
 function switchBabelTab(tab) {
   state.activeBabelTab = tab;
-  ['instances', 'interfaces', 'neighbors', 'routes'].forEach(t => {
+  ['instances', 'interfaces', 'interfaces-live', 'neighbors', 'routes'].forEach(t => {
     const pane = document.getElementById(`babel-pane-${t}`);
     const btn = document.getElementById(`babel-tab-btn-${t}`);
     if (pane) {
@@ -20335,7 +20339,11 @@ function renderBabelInstances() {
   tbody.innerHTML = list.map((inst, idx) => {
     const isSel = inst.id === selId;
     const selClass = isSel ? 'bg-[#1b2f48] text-white border-l-2 border-teal-400' : 'hover:bg-[#1a2636] text-slate-300';
-    const flagD = inst.disabled ? '<span class="text-rose-400 font-bold">X</span>' : '<span class="text-emerald-400">B</span>';
+    const flagD = inst.disabled
+      ? '<span class="text-rose-400 font-bold" title="Disabled">X</span>'
+      : (inst.running
+        ? '<span class="text-emerald-400 font-bold" title="Running">R</span>'
+        : '<span class="text-amber-400 font-bold" title="Inactive">I</span>');
     const redist = [];
     if (inst.redistribute_connected) redist.push('conn');
     if (inst.redistribute_static) redist.push('static');
@@ -20366,7 +20374,7 @@ function renderBabelInterfaces() {
   const filter = (document.getElementById('filter-babel-interfaces')?.value || '').toLowerCase();
   let list = state.babelInterfaces || [];
   if (filter) {
-    list = list.filter(i => (i.interface || '').toLowerCase().includes(filter) || (i.comment || '').toLowerCase().includes(filter));
+    list = list.filter(i => (i.interface || '').toLowerCase().includes(filter) || (i.instance || '').toLowerCase().includes(filter) || (i.comment || '').toLowerCase().includes(filter));
   }
   if (countEl) countEl.innerText = list.length;
   const selId = state.selectedBabelRow?.interfaces;
@@ -20379,6 +20387,7 @@ function renderBabelInterfaces() {
         <td class="py-1.5 px-3 w-8 text-center text-slate-500">${idx}</td>
         <td class="py-1.5 px-3 w-12 text-center">${flagD}</td>
         <td class="py-1.5 px-3 text-slate-400 italic">${escapeHtml(iface.comment || '')}</td>
+        <td class="py-1.5 px-3 font-semibold text-teal-300 font-mono">${escapeHtml(iface.instance || 'all')}</td>
         <td class="py-1.5 px-3 font-mono font-semibold text-white">${escapeHtml(iface.interface || '*')}</td>
         <td class="py-1.5 px-3 uppercase text-teal-300 font-bold">${escapeHtml(iface.type || 'wired')}</td>
         <td class="py-1.5 px-3 font-mono text-slate-300">${iface.rxcost || 10}</td>
@@ -20389,6 +20398,39 @@ function renderBabelInterfaces() {
   }).join('');
 }
 
+function renderBabelLiveInterfaces() {
+  const tbody = document.getElementById('babel-interfaces-live-table-body');
+  if (!tbody) return;
+  const countEl = document.getElementById('babel-interfaces-live-footer-count');
+  const filter = (document.getElementById('filter-babel-interfaces-live')?.value || '').toLowerCase();
+  let list = state.babelInterfacesLive || [];
+  if (filter) {
+    list = list.filter(i => (i.interface || '').toLowerCase().includes(filter) || (i.instance || '').toLowerCase().includes(filter));
+  }
+  if (countEl) countEl.innerText = list.length;
+  tbody.innerHTML = list.map((iface, idx) => {
+    const isUp = (iface.state || '').toLowerCase() === 'up';
+    const stateBadge = isUp
+      ? '<span class="px-1.5 py-0.5 rounded text-[10px] font-bold bg-emerald-500/20 text-emerald-400 border border-emerald-500/30">UP</span>'
+      : '<span class="px-1.5 py-0.5 rounded text-[10px] font-bold bg-rose-500/20 text-rose-400 border border-rose-500/30">DOWN</span>';
+    return `
+      <tr class="hover:bg-[#1a2636] text-slate-300 font-mono">
+        <td class="py-1.5 px-3 w-8 text-center text-slate-500 font-sans">${idx}</td>
+        <td class="py-1.5 px-3 w-12 text-center text-cyan-400 font-bold">D</td>
+        <td class="py-1.5 px-3 font-semibold text-white">${escapeHtml(iface.interface || '')}</td>
+        <td class="py-1.5 px-3 font-semibold text-teal-300">${escapeHtml(iface.instance || 'default')}</td>
+        <td class="py-1.5 px-3">${stateBadge}</td>
+        <td class="py-1.5 px-3 text-slate-400">${escapeHtml(iface.auth || 'No')}</td>
+        <td class="py-1.5 px-3 text-slate-300">${iface.rxcost || 96}</td>
+        <td class="py-1.5 px-3 text-slate-300">${iface.neighbors || 0}</td>
+        <td class="py-1.5 px-3 text-slate-400">${escapeHtml(iface.timer || '')}</td>
+        <td class="py-1.5 px-3 text-cyan-300">${escapeHtml(iface.next_hop_v4 || '-')}</td>
+        <td class="py-1.5 px-3 text-slate-400 text-[11px]">${escapeHtml(iface.next_hop_v6 || '-')}</td>
+      </tr>
+    `;
+  }).join('');
+}
+
 function renderBabelNeighbors() {
   const tbody = document.getElementById('babel-neighbors-table-body');
   if (!tbody) return;
@@ -20401,8 +20443,8 @@ function renderBabelNeighbors() {
       <td class="py-1.5 px-3 font-mono font-semibold text-teal-300">${escapeHtml(nbr.ip || '')}</td>
       <td class="py-1.5 px-3 font-mono text-slate-300">${escapeHtml(nbr.interface || '')}</td>
       <td class="py-1.5 px-3 font-mono text-slate-400">${nbr.metric || 0}</td>
-      <td class="py-1.5 px-3 font-mono text-slate-400">${nbr.cost || 0}</td>
-      <td class="py-1.5 px-3 text-slate-400">${escapeHtml(nbr.uptime || '')}</td>
+      <td class="py-1.5 px-3 font-mono text-slate-400">${nbr.cost || nbr.routes || 0}</td>
+      <td class="py-1.5 px-3 text-slate-400">${escapeHtml(nbr.uptime || nbr.expires || '')}</td>
     </tr>
   `).join('');
 }
@@ -20465,13 +20507,15 @@ function closeBabelInstanceModal() {
 async function submitBabelInstance(e) {
   e.preventDefault();
   const id = document.getElementById('modal-babel-instance-id').value;
+  const isEnabled = document.getElementById('modal-babel-instance-enabled').checked;
   const payload = {
     id: id || undefined,
     name: document.getElementById('modal-babel-instance-name').value.trim(),
     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(),
-    disabled: !document.getElementById('modal-babel-instance-enabled').checked,
+    enabled: isEnabled,
+    disabled: !isEnabled,
     redistribute_connected: document.getElementById('modal-babel-instance-redist-conn').checked,
     redistribute_static: document.getElementById('modal-babel-instance-redist-static').checked,
     redistribute_bgp: document.getElementById('modal-babel-instance-redist-bgp').checked,
@@ -20515,11 +20559,38 @@ async function removeSelectedBabelInstance() {
   await loadDynamicRouting(false);
 }
 
+function toggleBabelAuthKey() {
+  const authVal = document.getElementById('modal-babel-iface-auth')?.value;
+  const wrap = document.getElementById('modal-babel-iface-auth-key-wrap');
+  if (wrap) {
+    if (authVal === 'mac') wrap.classList.remove('hidden');
+    else wrap.classList.add('hidden');
+  }
+}
+
 function openBabelInterfaceModal() {
-  document.getElementById('babel-interface-modal-title').innerText = 'New Babel Interface';
+  document.getElementById('babel-interface-modal-title').innerText = 'New Babel Interface Template';
   document.getElementById('modal-babel-iface-id').value = '';
-  document.getElementById('modal-babel-iface-pattern').value = '';
+
+  const instSel = document.getElementById('modal-babel-iface-instance');
+  if (instSel) {
+    instSel.innerHTML = '<option value="all">all (Any instance)</option>' +
+      (state.babelInstances || []).map(i => `<option value="${escapeHtml(i.name)}">${escapeHtml(i.name)}</option>`).join('');
+    if (state.babelInstances && state.babelInstances.length > 0) {
+      instSel.value = state.babelInstances[0].name;
+    }
+  }
+
+  const dl = document.getElementById('babel-iface-datalist');
+  if (dl) {
+    dl.innerHTML = '<option value="*">' + (state.interfaces || []).map(i => `<option value="${escapeHtml(i.name)}">`).join('');
+  }
+
+  document.getElementById('modal-babel-iface-pattern').value = '*';
   document.getElementById('modal-babel-iface-type').value = 'wired';
+  document.getElementById('modal-babel-iface-auth').value = 'none';
+  document.getElementById('modal-babel-iface-auth-key').value = '';
+  toggleBabelAuthKey();
   document.getElementById('modal-babel-iface-rxcost').value = '10';
   document.getElementById('modal-babel-iface-hello').value = '4';
   document.getElementById('modal-babel-iface-update').value = '16';
@@ -20533,8 +20604,24 @@ function openEditBabelInterfaceModal(id) {
   if (!iface) return;
   document.getElementById('babel-interface-modal-title').innerText = `Edit Babel Interface: ${iface.interface}`;
   document.getElementById('modal-babel-iface-id').value = iface.id;
+
+  const instSel = document.getElementById('modal-babel-iface-instance');
+  if (instSel) {
+    instSel.innerHTML = '<option value="all">all (Any instance)</option>' +
+      (state.babelInstances || []).map(i => `<option value="${escapeHtml(i.name)}">${escapeHtml(i.name)}</option>`).join('');
+    instSel.value = iface.instance || 'all';
+  }
+
+  const dl = document.getElementById('babel-iface-datalist');
+  if (dl) {
+    dl.innerHTML = '<option value="*">' + (state.interfaces || []).map(i => `<option value="${escapeHtml(i.name)}">`).join('');
+  }
+
   document.getElementById('modal-babel-iface-pattern').value = iface.interface || '';
   document.getElementById('modal-babel-iface-type').value = iface.type || 'wired';
+  document.getElementById('modal-babel-iface-auth').value = iface.auth || 'none';
+  document.getElementById('modal-babel-iface-auth-key').value = iface.auth_key || '';
+  toggleBabelAuthKey();
   document.getElementById('modal-babel-iface-rxcost').value = iface.rxcost || 10;
   document.getElementById('modal-babel-iface-hello').value = iface.hello_interval || 4;
   document.getElementById('modal-babel-iface-update').value = iface.update_interval || 16;
@@ -20550,15 +20637,19 @@ function closeBabelInterfaceModal() {
 async function submitBabelInterface(e) {
   e.preventDefault();
   const id = document.getElementById('modal-babel-iface-id').value;
+  const isEnabled = document.getElementById('modal-babel-iface-enabled').checked;
   const payload = {
     id: id || undefined,
+    instance: document.getElementById('modal-babel-iface-instance').value.trim(),
     interface: document.getElementById('modal-babel-iface-pattern').value.trim(),
     type: document.getElementById('modal-babel-iface-type').value,
+    auth: document.getElementById('modal-babel-iface-auth').value,
+    auth_key: document.getElementById('modal-babel-iface-auth-key').value.trim(),
     rxcost: parseInt(document.getElementById('modal-babel-iface-rxcost').value, 10) || 10,
     hello_interval: parseInt(document.getElementById('modal-babel-iface-hello').value, 10) || 4,
     update_interval: parseInt(document.getElementById('modal-babel-iface-update').value, 10) || 16,
     comment: document.getElementById('modal-babel-iface-comment').value.trim(),
-    disabled: !document.getElementById('modal-babel-iface-enabled').checked
+    disabled: !isEnabled
   };
   try {
     const res = await fetch(`${API_BASE}/routing/babel/interfaces`, {

+ 63 - 3
internal/web/static/index.html

@@ -4274,7 +4274,10 @@
                   <span>Instances</span>
                 </button>
                 <button id="babel-tab-btn-interfaces" onclick="switchBabelTab('interfaces')" class="px-3 py-2 text-xs font-semibold border-b-2 border-transparent text-slate-400 hover:text-white transition flex items-center gap-1.5 whitespace-nowrap">
-                  <span>Interfaces</span>
+                  <span>Interfaces (Templates)</span>
+                </button>
+                <button id="babel-tab-btn-interfaces-live" onclick="switchBabelTab('interfaces-live')" class="px-3 py-2 text-xs font-semibold border-b-2 border-transparent text-slate-400 hover:text-white transition flex items-center gap-1.5 whitespace-nowrap">
+                  <span>Interfaces (Live)</span>
                 </button>
                 <button id="babel-tab-btn-neighbors" onclick="switchBabelTab('neighbors')" class="px-3 py-2 text-xs font-semibold border-b-2 border-transparent text-slate-400 hover:text-white transition flex items-center gap-1.5 whitespace-nowrap">
                   <span>Neighbors (Live)</span>
@@ -4332,7 +4335,7 @@
               </div>
             </div>
 
-            <!-- TAB 2: BABEL INTERFACES -->
+            <!-- TAB 2: BABEL INTERFACES (TEMPLATES) -->
             <div id="babel-pane-interfaces" class="hidden flex-1 flex flex-col">
               <div class="bg-[#101721] border-b border-[#243347] px-3 py-1.5 flex items-center justify-between text-xs select-none">
                 <div class="flex items-center space-x-2">
@@ -4360,6 +4363,7 @@
                       <th class="py-2 px-3 w-8 text-center">#</th>
                       <th class="py-2 px-3 w-12 text-center">Flags</th>
                       <th class="py-2 px-3">Comment</th>
+                      <th class="py-2 px-3">Instance</th>
                       <th class="py-2 px-3">Interface</th>
                       <th class="py-2 px-3">Type</th>
                       <th class="py-2 px-3">RX Cost</th>
@@ -4376,6 +4380,43 @@
               </div>
             </div>
 
+            <!-- TAB 3: BABEL INTERFACES (LIVE) -->
+            <div id="babel-pane-interfaces-live" class="hidden flex-1 flex flex-col">
+              <div class="bg-[#101721] border-b border-[#243347] px-3 py-1.5 flex items-center justify-between text-xs select-none">
+                <div class="flex items-center space-x-2">
+                  <span class="text-slate-400 font-sans">Live Babel Operational Interfaces</span>
+                </div>
+                <div class="flex items-center space-x-2">
+                  <input type="text" id="filter-babel-interfaces-live" oninput="renderBabelLiveInterfaces()" placeholder="Find..." class="bg-[#15202e] border border-[#26374d] rounded px-2 py-0.5 text-xs text-white placeholder-slate-500 focus:outline-none focus:border-teal-500 w-32">
+                  <button onclick="loadDynamicRouting(true)" class="px-2 py-1 bg-[#1a2737] hover:bg-slate-700 text-slate-300 rounded text-xs">🔄 Refresh</button>
+                </div>
+              </div>
+              <div class="flex-1 overflow-x-auto">
+                <table class="w-full text-left text-xs font-mono">
+                  <thead class="text-slate-400 bg-[#0e1622] border-b border-[#243347] text-[11px] font-sans select-none">
+                    <tr>
+                      <th class="py-2 px-3 w-8 text-center">#</th>
+                      <th class="py-2 px-3 w-12 text-center">Flags</th>
+                      <th class="py-2 px-3">Interface</th>
+                      <th class="py-2 px-3">Instance</th>
+                      <th class="py-2 px-3">State</th>
+                      <th class="py-2 px-3">Auth</th>
+                      <th class="py-2 px-3">RX Cost</th>
+                      <th class="py-2 px-3">Neighbors</th>
+                      <th class="py-2 px-3">Timer</th>
+                      <th class="py-2 px-3">Next Hop (IPv4)</th>
+                      <th class="py-2 px-3">Next Hop (IPv6)</th>
+                    </tr>
+                  </thead>
+                  <tbody id="babel-interfaces-live-table-body" class="divide-y divide-[#1e2c3d]"></tbody>
+                </table>
+              </div>
+              <div class="bg-[#101721] border-t border-[#243347] px-3 py-1.5 flex items-center justify-between text-[11px] font-mono text-slate-400 select-none">
+                <span id="babel-interfaces-live-footer-count">0</span>
+                <span class="flex items-center gap-1.5 text-emerald-400"><span class="w-2 h-2 rounded-full bg-emerald-400 animate-pulse"></span> ▶ Live</span>
+              </div>
+            </div>
+
             <!-- TAB 3: BABEL NEIGHBORS (LIVE) -->
             <div id="babel-pane-neighbors" class="hidden flex-1 flex flex-col">
               <div class="bg-[#101721] border-b border-[#243347] px-3 py-1.5 flex items-center justify-between text-xs select-none">
@@ -12115,10 +12156,18 @@
       <form onsubmit="submitBabelInterface(event)" class="p-6 space-y-4 text-xs">
         <input type="hidden" id="modal-babel-iface-id">
         <div class="grid grid-cols-2 gap-4">
+          <div>
+            <label class="block font-semibold text-slate-300 mb-1">Babel Instance *</label>
+            <select id="modal-babel-iface-instance" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono">
+            </select>
+          </div>
           <div>
             <label class="block font-semibold text-slate-300 mb-1">Interface Pattern *</label>
-            <input type="text" id="modal-babel-iface-pattern" required placeholder="e.g. eth0, wlan*, or *" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono">
+            <input type="text" id="modal-babel-iface-pattern" required placeholder="e.g. eth0, *, aerotun*" list="babel-iface-datalist" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono">
+            <datalist id="babel-iface-datalist"></datalist>
           </div>
+        </div>
+        <div class="grid grid-cols-2 gap-4">
           <div>
             <label class="block font-semibold text-slate-300 mb-1">Link Type</label>
             <select id="modal-babel-iface-type" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono">
@@ -12126,6 +12175,17 @@
               <option value="wireless">Wireless (Mesh)</option>
             </select>
           </div>
+          <div>
+            <label class="block font-semibold text-slate-300 mb-1">Authentication</label>
+            <select id="modal-babel-iface-auth" onchange="toggleBabelAuthKey()" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono">
+              <option value="none">None</option>
+              <option value="mac">MAC (HMAC-SHA256)</option>
+            </select>
+          </div>
+        </div>
+        <div id="modal-babel-iface-auth-key-wrap" class="hidden">
+          <label class="block font-semibold text-slate-300 mb-1">Auth Secret Key</label>
+          <input type="text" id="modal-babel-iface-auth-key" placeholder="Enter Babel shared authentication key" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono">
         </div>
         <div class="grid grid-cols-3 gap-3">
           <div>