Explorar el Código

feat: complete all BIRD dynamic routing protocols and full DebTik CLI parity (v1.8.8)

Proxmox Router hace 2 días
padre
commit
04fbaf9f5c

+ 1 - 1
VERSION

@@ -1 +1 @@
-1.8.7
+1.8.8

BIN
bin/debtik


BIN
bin/debtik-arm64


BIN
bin/debtik-cli


BIN
bin/debtik-cli-arm64


+ 56 - 0
internal/api/api.go

@@ -398,6 +398,62 @@ func (s *Server) registerRoutes() {
 	s.mux.HandleFunc("GET /api/routing/ospf/lsa", s.handleGetOSPFLSA)
 	s.mux.HandleFunc("GET /api/routing/ospf/routes", s.handleGetOSPFRoutes)
 
+	// RIP (/routing rip)
+	s.mux.HandleFunc("GET /api/routing/rip/instances", s.handleListRIPInstances)
+	s.mux.HandleFunc("POST /api/routing/rip/instances", s.handleSaveRIPInstance)
+	s.mux.HandleFunc("DELETE /api/routing/rip/instances/{id}", s.handleDeleteRIPInstance)
+	s.mux.HandleFunc("POST /api/routing/rip/instances/{id}/state", s.handleSetRIPInstanceState)
+
+	s.mux.HandleFunc("GET /api/routing/rip/interfaces", s.handleListRIPInterfaces)
+	s.mux.HandleFunc("POST /api/routing/rip/interfaces", s.handleSaveRIPInterface)
+	s.mux.HandleFunc("DELETE /api/routing/rip/interfaces/{id}", s.handleDeleteRIPInterface)
+	s.mux.HandleFunc("POST /api/routing/rip/interfaces/{id}/state", s.handleSetRIPInterfaceState)
+
+	s.mux.HandleFunc("GET /api/routing/rip/neighbors", s.handleListRIPNeighbors)
+	s.mux.HandleFunc("POST /api/routing/rip/neighbors", s.handleSaveRIPNeighbor)
+	s.mux.HandleFunc("DELETE /api/routing/rip/neighbors/{id}", s.handleDeleteRIPNeighbor)
+	s.mux.HandleFunc("GET /api/routing/rip/neighbors-live", s.handleGetRIPNeighborsLive)
+	s.mux.HandleFunc("GET /api/routing/rip/routes", s.handleGetRIPRoutes)
+
+	// BFD (/routing bfd)
+	s.mux.HandleFunc("GET /api/routing/bfd/instance", s.handleGetBFDInstance)
+	s.mux.HandleFunc("POST /api/routing/bfd/instance", s.handleSaveBFDInstance)
+
+	s.mux.HandleFunc("GET /api/routing/bfd/interfaces", s.handleListBFDInterfaces)
+	s.mux.HandleFunc("POST /api/routing/bfd/interfaces", s.handleSaveBFDInterface)
+	s.mux.HandleFunc("DELETE /api/routing/bfd/interfaces/{id}", s.handleDeleteBFDInterface)
+	s.mux.HandleFunc("POST /api/routing/bfd/interfaces/{id}/state", s.handleSetBFDInterfaceState)
+
+	s.mux.HandleFunc("GET /api/routing/bfd/peers", s.handleListBFDNeighbors)
+	s.mux.HandleFunc("POST /api/routing/bfd/peers", s.handleSaveBFDNeighbor)
+	s.mux.HandleFunc("DELETE /api/routing/bfd/peers/{id}", s.handleDeleteBFDNeighbor)
+	s.mux.HandleFunc("GET /api/routing/bfd/sessions", s.handleGetBFDSessions)
+
+	// Babel (/routing babel)
+	s.mux.HandleFunc("GET /api/routing/babel/instances", s.handleListBabelInstances)
+	s.mux.HandleFunc("POST /api/routing/babel/instances", s.handleSaveBabelInstance)
+	s.mux.HandleFunc("DELETE /api/routing/babel/instances/{id}", s.handleDeleteBabelInstance)
+	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("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)
+
+	s.mux.HandleFunc("GET /api/routing/babel/neighbors", s.handleGetBabelNeighbors)
+	s.mux.HandleFunc("GET /api/routing/babel/routes", s.handleGetBabelRoutes)
+
+	// RAdv (/routing radv)
+	s.mux.HandleFunc("GET /api/routing/radv/instances", s.handleListRAdvInstances)
+	s.mux.HandleFunc("POST /api/routing/radv/instances", s.handleSaveRAdvInstance)
+	s.mux.HandleFunc("DELETE /api/routing/radv/instances/{id}", s.handleDeleteRAdvInstance)
+	s.mux.HandleFunc("POST /api/routing/radv/instances/{id}/state", s.handleSetRAdvInstanceState)
+
+	s.mux.HandleFunc("GET /api/routing/radv/interfaces", s.handleListRAdvInterfaces)
+	s.mux.HandleFunc("POST /api/routing/radv/interfaces", s.handleSaveRAdvInterface)
+	s.mux.HandleFunc("DELETE /api/routing/radv/interfaces/{id}", s.handleDeleteRAdvInterface)
+	s.mux.HandleFunc("POST /api/routing/radv/interfaces/{id}/state", s.handleSetRAdvInterfaceState)
+
 	// Routing Filters (/routing filter)
 	s.mux.HandleFunc("GET /api/routing/filters", s.handleListRoutingFilters)
 	s.mux.HandleFunc("POST /api/routing/filters", s.handleSaveRoutingFilter)

+ 501 - 0
internal/api/dynamic_routing_handlers.go

@@ -633,3 +633,504 @@ func (s *Server) handleSetASListState(w http.ResponseWriter, r *http.Request) {
 	}
 	writeJSON(w, http.StatusOK, map[string]interface{}{"status": "success", "disabled": req.Disabled})
 }
+
+// -------------------------------------------------------------
+// RIP Handlers (/api/routing/rip/*)
+// -------------------------------------------------------------
+
+func (s *Server) handleListRIPInstances(w http.ResponseWriter, r *http.Request) {
+	insts := s.dynRoutingMgr.ListRIPInstances()
+	if insts == nil {
+		insts = []dynamicrouting.RIPInstance{}
+	}
+	writeJSON(w, http.StatusOK, insts)
+}
+
+func (s *Server) handleSaveRIPInstance(w http.ResponseWriter, r *http.Request) {
+	var inst dynamicrouting.RIPInstance
+	if err := json.NewDecoder(r.Body).Decode(&inst); err != nil {
+		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Invalid JSON: " + err.Error()})
+		return
+	}
+	res, err := s.dynRoutingMgr.SaveRIPInstance(inst)
+	if err != nil {
+		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
+		return
+	}
+	writeJSON(w, http.StatusOK, res)
+}
+
+func (s *Server) handleDeleteRIPInstance(w http.ResponseWriter, r *http.Request) {
+	id := r.PathValue("id")
+	if id == "" {
+		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Missing instance ID"})
+		return
+	}
+	if err := s.dynRoutingMgr.DeleteRIPInstance(id); err != nil {
+		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
+		return
+	}
+	writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"})
+}
+
+func (s *Server) handleSetRIPInstanceState(w http.ResponseWriter, r *http.Request) {
+	id := r.PathValue("id")
+	var req struct {
+		Disabled bool `json:"disabled"`
+	}
+	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Invalid JSON"})
+		return
+	}
+	if err := s.dynRoutingMgr.SetRIPInstanceState(id, req.Disabled); err != nil {
+		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
+		return
+	}
+	writeJSON(w, http.StatusOK, map[string]interface{}{"status": "success", "disabled": req.Disabled})
+}
+
+func (s *Server) handleListRIPInterfaces(w http.ResponseWriter, r *http.Request) {
+	ifaces := s.dynRoutingMgr.ListRIPInterfaces()
+	if ifaces == nil {
+		ifaces = []dynamicrouting.RIPInterface{}
+	}
+	writeJSON(w, http.StatusOK, ifaces)
+}
+
+func (s *Server) handleSaveRIPInterface(w http.ResponseWriter, r *http.Request) {
+	var iface dynamicrouting.RIPInterface
+	if err := json.NewDecoder(r.Body).Decode(&iface); err != nil {
+		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Invalid JSON: " + err.Error()})
+		return
+	}
+	res, err := s.dynRoutingMgr.SaveRIPInterface(iface)
+	if err != nil {
+		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
+		return
+	}
+	writeJSON(w, http.StatusOK, res)
+}
+
+func (s *Server) handleDeleteRIPInterface(w http.ResponseWriter, r *http.Request) {
+	id := r.PathValue("id")
+	if id == "" {
+		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Missing interface ID"})
+		return
+	}
+	if err := s.dynRoutingMgr.DeleteRIPInterface(id); err != nil {
+		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
+		return
+	}
+	writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"})
+}
+
+func (s *Server) handleSetRIPInterfaceState(w http.ResponseWriter, r *http.Request) {
+	id := r.PathValue("id")
+	var req struct {
+		Disabled bool `json:"disabled"`
+	}
+	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Invalid JSON"})
+		return
+	}
+	if err := s.dynRoutingMgr.SetRIPInterfaceState(id, req.Disabled); err != nil {
+		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
+		return
+	}
+	writeJSON(w, http.StatusOK, map[string]interface{}{"status": "success", "disabled": req.Disabled})
+}
+
+func (s *Server) handleListRIPNeighbors(w http.ResponseWriter, r *http.Request) {
+	nbrs := s.dynRoutingMgr.ListRIPNeighbors()
+	if nbrs == nil {
+		nbrs = []dynamicrouting.RIPNeighbor{}
+	}
+	writeJSON(w, http.StatusOK, nbrs)
+}
+
+func (s *Server) handleSaveRIPNeighbor(w http.ResponseWriter, r *http.Request) {
+	var nbr dynamicrouting.RIPNeighbor
+	if err := json.NewDecoder(r.Body).Decode(&nbr); err != nil {
+		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Invalid JSON: " + err.Error()})
+		return
+	}
+	res, err := s.dynRoutingMgr.SaveRIPNeighbor(nbr)
+	if err != nil {
+		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
+		return
+	}
+	writeJSON(w, http.StatusOK, res)
+}
+
+func (s *Server) handleDeleteRIPNeighbor(w http.ResponseWriter, r *http.Request) {
+	id := r.PathValue("id")
+	if id == "" {
+		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Missing neighbor ID"})
+		return
+	}
+	if err := s.dynRoutingMgr.DeleteRIPNeighbor(id); err != nil {
+		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
+		return
+	}
+	writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"})
+}
+
+func (s *Server) handleGetRIPNeighborsLive(w http.ResponseWriter, r *http.Request) {
+	nbrs := s.dynRoutingMgr.GetRIPNeighbors()
+	if nbrs == nil {
+		nbrs = []dynamicrouting.RIPNeighborLive{}
+	}
+	writeJSON(w, http.StatusOK, nbrs)
+}
+
+func (s *Server) handleGetRIPRoutes(w http.ResponseWriter, r *http.Request) {
+	rts := s.dynRoutingMgr.GetRIPRoutes()
+	if rts == nil {
+		rts = []dynamicrouting.RIPRoute{}
+	}
+	writeJSON(w, http.StatusOK, rts)
+}
+
+// -------------------------------------------------------------
+// BFD Handlers (/api/routing/bfd/*)
+// -------------------------------------------------------------
+
+func (s *Server) handleGetBFDInstance(w http.ResponseWriter, r *http.Request) {
+	inst := s.dynRoutingMgr.GetBFDInstance()
+	writeJSON(w, http.StatusOK, inst)
+}
+
+func (s *Server) handleSaveBFDInstance(w http.ResponseWriter, r *http.Request) {
+	var inst dynamicrouting.BFDInstance
+	if err := json.NewDecoder(r.Body).Decode(&inst); err != nil {
+		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Invalid JSON: " + err.Error()})
+		return
+	}
+	if err := s.dynRoutingMgr.SaveBFDInstance(inst); err != nil {
+		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
+		return
+	}
+	writeJSON(w, http.StatusOK, s.dynRoutingMgr.GetBFDInstance())
+}
+
+func (s *Server) handleListBFDInterfaces(w http.ResponseWriter, r *http.Request) {
+	ifaces := s.dynRoutingMgr.ListBFDInterfaces()
+	if ifaces == nil {
+		ifaces = []dynamicrouting.BFDInterface{}
+	}
+	writeJSON(w, http.StatusOK, ifaces)
+}
+
+func (s *Server) handleSaveBFDInterface(w http.ResponseWriter, r *http.Request) {
+	var iface dynamicrouting.BFDInterface
+	if err := json.NewDecoder(r.Body).Decode(&iface); err != nil {
+		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Invalid JSON: " + err.Error()})
+		return
+	}
+	res, err := s.dynRoutingMgr.SaveBFDInterface(iface)
+	if err != nil {
+		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
+		return
+	}
+	writeJSON(w, http.StatusOK, res)
+}
+
+func (s *Server) handleDeleteBFDInterface(w http.ResponseWriter, r *http.Request) {
+	id := r.PathValue("id")
+	if id == "" {
+		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Missing interface ID"})
+		return
+	}
+	if err := s.dynRoutingMgr.DeleteBFDInterface(id); err != nil {
+		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
+		return
+	}
+	writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"})
+}
+
+func (s *Server) handleSetBFDInterfaceState(w http.ResponseWriter, r *http.Request) {
+	id := r.PathValue("id")
+	var req struct {
+		Disabled bool `json:"disabled"`
+	}
+	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Invalid JSON"})
+		return
+	}
+	if err := s.dynRoutingMgr.SetBFDInterfaceState(id, req.Disabled); err != nil {
+		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
+		return
+	}
+	writeJSON(w, http.StatusOK, map[string]interface{}{"status": "success", "disabled": req.Disabled})
+}
+
+func (s *Server) handleListBFDNeighbors(w http.ResponseWriter, r *http.Request) {
+	nbrs := s.dynRoutingMgr.ListBFDNeighbors()
+	if nbrs == nil {
+		nbrs = []dynamicrouting.BFDNeighbor{}
+	}
+	writeJSON(w, http.StatusOK, nbrs)
+}
+
+func (s *Server) handleSaveBFDNeighbor(w http.ResponseWriter, r *http.Request) {
+	var nbr dynamicrouting.BFDNeighbor
+	if err := json.NewDecoder(r.Body).Decode(&nbr); err != nil {
+		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Invalid JSON: " + err.Error()})
+		return
+	}
+	res, err := s.dynRoutingMgr.SaveBFDNeighbor(nbr)
+	if err != nil {
+		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
+		return
+	}
+	writeJSON(w, http.StatusOK, res)
+}
+
+func (s *Server) handleDeleteBFDNeighbor(w http.ResponseWriter, r *http.Request) {
+	id := r.PathValue("id")
+	if id == "" {
+		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Missing neighbor ID"})
+		return
+	}
+	if err := s.dynRoutingMgr.DeleteBFDNeighbor(id); err != nil {
+		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
+		return
+	}
+	writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"})
+}
+
+func (s *Server) handleGetBFDSessions(w http.ResponseWriter, r *http.Request) {
+	sessions := s.dynRoutingMgr.GetBFDSessions()
+	if sessions == nil {
+		sessions = []dynamicrouting.BFDSession{}
+	}
+	writeJSON(w, http.StatusOK, sessions)
+}
+
+// -------------------------------------------------------------
+// Babel Handlers (/api/routing/babel/*)
+// -------------------------------------------------------------
+
+func (s *Server) handleListBabelInstances(w http.ResponseWriter, r *http.Request) {
+	insts := s.dynRoutingMgr.ListBabelInstances()
+	if insts == nil {
+		insts = []dynamicrouting.BabelInstance{}
+	}
+	writeJSON(w, http.StatusOK, insts)
+}
+
+func (s *Server) handleSaveBabelInstance(w http.ResponseWriter, r *http.Request) {
+	var inst dynamicrouting.BabelInstance
+	if err := json.NewDecoder(r.Body).Decode(&inst); err != nil {
+		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Invalid JSON: " + err.Error()})
+		return
+	}
+	res, err := s.dynRoutingMgr.SaveBabelInstance(inst)
+	if err != nil {
+		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
+		return
+	}
+	writeJSON(w, http.StatusOK, res)
+}
+
+func (s *Server) handleDeleteBabelInstance(w http.ResponseWriter, r *http.Request) {
+	id := r.PathValue("id")
+	if id == "" {
+		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Missing instance ID"})
+		return
+	}
+	if err := s.dynRoutingMgr.DeleteBabelInstance(id); err != nil {
+		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
+		return
+	}
+	writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"})
+}
+
+func (s *Server) handleSetBabelInstanceState(w http.ResponseWriter, r *http.Request) {
+	id := r.PathValue("id")
+	var req struct {
+		Disabled bool `json:"disabled"`
+	}
+	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Invalid JSON"})
+		return
+	}
+	if err := s.dynRoutingMgr.SetBabelInstanceState(id, req.Disabled); err != nil {
+		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
+		return
+	}
+	writeJSON(w, http.StatusOK, map[string]interface{}{"status": "success", "disabled": req.Disabled})
+}
+
+func (s *Server) handleListBabelInterfaces(w http.ResponseWriter, r *http.Request) {
+	ifaces := s.dynRoutingMgr.ListBabelInterfaces()
+	if ifaces == nil {
+		ifaces = []dynamicrouting.BabelInterface{}
+	}
+	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 {
+		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Invalid JSON: " + err.Error()})
+		return
+	}
+	res, err := s.dynRoutingMgr.SaveBabelInterface(iface)
+	if err != nil {
+		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
+		return
+	}
+	writeJSON(w, http.StatusOK, res)
+}
+
+func (s *Server) handleDeleteBabelInterface(w http.ResponseWriter, r *http.Request) {
+	id := r.PathValue("id")
+	if id == "" {
+		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Missing interface ID"})
+		return
+	}
+	if err := s.dynRoutingMgr.DeleteBabelInterface(id); err != nil {
+		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
+		return
+	}
+	writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"})
+}
+
+func (s *Server) handleSetBabelInterfaceState(w http.ResponseWriter, r *http.Request) {
+	id := r.PathValue("id")
+	var req struct {
+		Disabled bool `json:"disabled"`
+	}
+	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Invalid JSON"})
+		return
+	}
+	if err := s.dynRoutingMgr.SetBabelInterfaceState(id, req.Disabled); err != nil {
+		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
+		return
+	}
+	writeJSON(w, http.StatusOK, map[string]interface{}{"status": "success", "disabled": req.Disabled})
+}
+
+func (s *Server) handleGetBabelNeighbors(w http.ResponseWriter, r *http.Request) {
+	nbrs := s.dynRoutingMgr.GetBabelNeighbors()
+	if nbrs == nil {
+		nbrs = []dynamicrouting.BabelNeighbor{}
+	}
+	writeJSON(w, http.StatusOK, nbrs)
+}
+
+func (s *Server) handleGetBabelRoutes(w http.ResponseWriter, r *http.Request) {
+	rts := s.dynRoutingMgr.GetBabelRoutes()
+	if rts == nil {
+		rts = []dynamicrouting.BabelRoute{}
+	}
+	writeJSON(w, http.StatusOK, rts)
+}
+
+// -------------------------------------------------------------
+// RAdv Handlers (/api/routing/radv/*)
+// -------------------------------------------------------------
+
+func (s *Server) handleListRAdvInstances(w http.ResponseWriter, r *http.Request) {
+	insts := s.dynRoutingMgr.ListRAdvInstances()
+	if insts == nil {
+		insts = []dynamicrouting.RAdvInstance{}
+	}
+	writeJSON(w, http.StatusOK, insts)
+}
+
+func (s *Server) handleSaveRAdvInstance(w http.ResponseWriter, r *http.Request) {
+	var inst dynamicrouting.RAdvInstance
+	if err := json.NewDecoder(r.Body).Decode(&inst); err != nil {
+		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Invalid JSON: " + err.Error()})
+		return
+	}
+	res, err := s.dynRoutingMgr.SaveRAdvInstance(inst)
+	if err != nil {
+		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
+		return
+	}
+	writeJSON(w, http.StatusOK, res)
+}
+
+func (s *Server) handleDeleteRAdvInstance(w http.ResponseWriter, r *http.Request) {
+	id := r.PathValue("id")
+	if id == "" {
+		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Missing instance ID"})
+		return
+	}
+	if err := s.dynRoutingMgr.DeleteRAdvInstance(id); err != nil {
+		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
+		return
+	}
+	writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"})
+}
+
+func (s *Server) handleSetRAdvInstanceState(w http.ResponseWriter, r *http.Request) {
+	id := r.PathValue("id")
+	var req struct {
+		Disabled bool `json:"disabled"`
+	}
+	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Invalid JSON"})
+		return
+	}
+	if err := s.dynRoutingMgr.SetRAdvInstanceState(id, req.Disabled); err != nil {
+		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
+		return
+	}
+	writeJSON(w, http.StatusOK, map[string]interface{}{"status": "success", "disabled": req.Disabled})
+}
+
+func (s *Server) handleListRAdvInterfaces(w http.ResponseWriter, r *http.Request) {
+	ifaces := s.dynRoutingMgr.ListRAdvInterfaces()
+	if ifaces == nil {
+		ifaces = []dynamicrouting.RAdvInterface{}
+	}
+	writeJSON(w, http.StatusOK, ifaces)
+}
+
+func (s *Server) handleSaveRAdvInterface(w http.ResponseWriter, r *http.Request) {
+	var iface dynamicrouting.RAdvInterface
+	if err := json.NewDecoder(r.Body).Decode(&iface); err != nil {
+		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Invalid JSON: " + err.Error()})
+		return
+	}
+	res, err := s.dynRoutingMgr.SaveRAdvInterface(iface)
+	if err != nil {
+		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
+		return
+	}
+	writeJSON(w, http.StatusOK, res)
+}
+
+func (s *Server) handleDeleteRAdvInterface(w http.ResponseWriter, r *http.Request) {
+	id := r.PathValue("id")
+	if id == "" {
+		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Missing interface ID"})
+		return
+	}
+	if err := s.dynRoutingMgr.DeleteRAdvInterface(id); err != nil {
+		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
+		return
+	}
+	writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"})
+}
+
+func (s *Server) handleSetRAdvInterfaceState(w http.ResponseWriter, r *http.Request) {
+	id := r.PathValue("id")
+	var req struct {
+		Disabled bool `json:"disabled"`
+	}
+	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Invalid JSON"})
+		return
+	}
+	if err := s.dynRoutingMgr.SetRAdvInterfaceState(id, req.Disabled); err != nil {
+		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
+		return
+	}
+	writeJSON(w, http.StatusOK, map[string]interface{}{"status": "success", "disabled": req.Disabled})
+}

+ 162 - 0
internal/cli/completer.go

@@ -749,6 +749,10 @@ func (c *Completer) buildCommandTree() *Node {
 	fwAddrListAdd.AddChild(&Node{Name: "address="})
 	fwAddrListAdd.AddChild(&Node{Name: "comment="})
 
+	// /ip firewall connection
+	fwConn := fwNode.AddChild(&Node{Name: "connection", Aliases: []string{"connections"}})
+	addPrint(fwConn)
+
 	// /ip service
 	serviceNode := ipNode.AddChild(&Node{Name: "service", Aliases: []string{"services"}})
 	addPrint(serviceNode)
@@ -927,6 +931,142 @@ func (c *Completer) buildCommandTree() *Node {
 	rfRuleAdd.AddChild(&Node{Name: "action=", Dynamic: func() []string { return []string{"accept", "discard", "reject"} }})
 	rfRuleAdd.AddChild(&Node{Name: "prefix="})
 	rfRuleAdd.AddChild(&Node{Name: "prefix-len="})
+	rfRuleAdd.AddChild(&Node{Name: "rule-order="})
+
+	rfComm := routingFilter.AddChild(&Node{Name: "community-list", Aliases: []string{"community-lists"}})
+	addPrint(rfComm)
+	addRemove(rfComm)
+	rfCommAdd := rfComm.AddChild(&Node{Name: "add"})
+	rfCommAdd.AddChild(&Node{Name: "name="})
+	rfCommAdd.AddChild(&Node{Name: "communities="})
+
+	rfAS := routingFilter.AddChild(&Node{Name: "as-list", Aliases: []string{"as-lists", "num-list", "num-lists"}})
+	addPrint(rfAS)
+	addRemove(rfAS)
+	rfASAdd := rfAS.AddChild(&Node{Name: "add"})
+	rfASAdd.AddChild(&Node{Name: "name="})
+	rfASAdd.AddChild(&Node{Name: "range="})
+
+	// /routing rip
+	ripNode := routingNode.AddChild(&Node{Name: "rip"})
+	ripInst := ripNode.AddChild(&Node{Name: "instance", Aliases: []string{"instances"}})
+	addPrint(ripInst)
+	addRemove(ripInst)
+	addEnableDisable(ripInst)
+	ripInstAdd := ripInst.AddChild(&Node{Name: "add"})
+	ripInstAdd.AddChild(&Node{Name: "name="})
+	ripInstAdd.AddChild(&Node{Name: "version=", Dynamic: func() []string { return []string{"1", "2"} }})
+	ripInstAdd.AddChild(&Node{Name: "redistribute-connected=", Dynamic: func() []string { return []string{"yes", "no"} }})
+	ripInstAdd.AddChild(&Node{Name: "redistribute-static=", Dynamic: func() []string { return []string{"yes", "no"} }})
+	ripInstAdd.AddChild(&Node{Name: "redistribute-ospf=", Dynamic: func() []string { return []string{"yes", "no"} }})
+	ripInstAdd.AddChild(&Node{Name: "redistribute-bgp=", Dynamic: func() []string { return []string{"yes", "no"} }})
+	ripInstAdd.AddChild(&Node{Name: "redistribute-default=", Dynamic: func() []string { return []string{"yes", "no"} }})
+
+	ripIface := ripNode.AddChild(&Node{Name: "interface", Aliases: []string{"interfaces"}})
+	addPrint(ripIface)
+	addRemove(ripIface)
+	addEnableDisable(ripIface)
+	ripIfaceAdd := ripIface.AddChild(&Node{Name: "add"})
+	ripIfaceAdd.AddChild(&Node{Name: "interface=", Dynamic: dynInterfaces})
+	ripIfaceAdd.AddChild(&Node{Name: "instance="})
+	ripIfaceAdd.AddChild(&Node{Name: "metric="})
+	ripIfaceAdd.AddChild(&Node{Name: "mode=", Dynamic: func() []string { return []string{"multicast", "broadcast"} }})
+	ripIfaceAdd.AddChild(&Node{Name: "passive=", Dynamic: func() []string { return []string{"yes", "no"} }})
+	ripIfaceAdd.AddChild(&Node{Name: "use-bfd=", Dynamic: func() []string { return []string{"yes", "no"} }})
+
+	ripNbr := ripNode.AddChild(&Node{Name: "neighbor", Aliases: []string{"neighbors"}})
+	addPrint(ripNbr)
+	addRemove(ripNbr)
+	ripNbrAdd := ripNbr.AddChild(&Node{Name: "add"})
+	ripNbrAdd.AddChild(&Node{Name: "address="})
+	ripNbrAdd.AddChild(&Node{Name: "instance="})
+
+	ripRoutes := ripNode.AddChild(&Node{Name: "routes", Aliases: []string{"route"}})
+	addPrint(ripRoutes)
+
+	// /routing bfd
+	bfdNode := routingNode.AddChild(&Node{Name: "bfd"})
+	bfdInst := bfdNode.AddChild(&Node{Name: "instance"})
+	addPrint(bfdInst)
+	bfdInstSet := bfdInst.AddChild(&Node{Name: "set"})
+	bfdInstSet.AddChild(&Node{Name: "enabled=", Dynamic: func() []string { return []string{"yes", "no"} }})
+
+	bfdIface := bfdNode.AddChild(&Node{Name: "interface", Aliases: []string{"interfaces"}})
+	addPrint(bfdIface)
+	addRemove(bfdIface)
+	addEnableDisable(bfdIface)
+	bfdIfaceAdd := bfdIface.AddChild(&Node{Name: "add"})
+	bfdIfaceAdd.AddChild(&Node{Name: "interface=", Dynamic: dynInterfaces})
+	bfdIfaceAdd.AddChild(&Node{Name: "min-rx="})
+	bfdIfaceAdd.AddChild(&Node{Name: "min-tx="})
+	bfdIfaceAdd.AddChild(&Node{Name: "idle-tx="})
+	bfdIfaceAdd.AddChild(&Node{Name: "multiplier="})
+	bfdIfaceAdd.AddChild(&Node{Name: "passive=", Dynamic: func() []string { return []string{"yes", "no"} }})
+
+	bfdPeer := bfdNode.AddChild(&Node{Name: "peer", Aliases: []string{"peers", "neighbor", "neighbors"}})
+	addPrint(bfdPeer)
+	addRemove(bfdPeer)
+	bfdPeerAdd := bfdPeer.AddChild(&Node{Name: "add"})
+	bfdPeerAdd.AddChild(&Node{Name: "address="})
+	bfdPeerAdd.AddChild(&Node{Name: "interface=", Dynamic: dynInterfaces})
+	bfdPeerAdd.AddChild(&Node{Name: "multihop=", Dynamic: func() []string { return []string{"yes", "no"} }})
+
+	bfdSession := bfdNode.AddChild(&Node{Name: "session", Aliases: []string{"sessions"}})
+	addPrint(bfdSession)
+
+	// /routing babel
+	babelNode := routingNode.AddChild(&Node{Name: "babel"})
+	babelInst := babelNode.AddChild(&Node{Name: "instance", Aliases: []string{"instances"}})
+	addPrint(babelInst)
+	addRemove(babelInst)
+	addEnableDisable(babelInst)
+	babelInstAdd := babelInst.AddChild(&Node{Name: "add"})
+	babelInstAdd.AddChild(&Node{Name: "name="})
+	babelInstAdd.AddChild(&Node{Name: "router-id="})
+	babelInstAdd.AddChild(&Node{Name: "redistribute-connected=", Dynamic: func() []string { return []string{"yes", "no"} }})
+	babelInstAdd.AddChild(&Node{Name: "redistribute-static=", Dynamic: func() []string { return []string{"yes", "no"} }})
+	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"}})
+	addPrint(babelIface)
+	addRemove(babelIface)
+	addEnableDisable(babelIface)
+	babelIfaceAdd := babelIface.AddChild(&Node{Name: "add"})
+	babelIfaceAdd.AddChild(&Node{Name: "interface=", Dynamic: dynInterfaces})
+	babelIfaceAdd.AddChild(&Node{Name: "instance="})
+	babelIfaceAdd.AddChild(&Node{Name: "type=", Dynamic: func() []string { return []string{"wired", "wireless"} }})
+	babelIfaceAdd.AddChild(&Node{Name: "rxcost="})
+	babelIfaceAdd.AddChild(&Node{Name: "hello-interval="})
+	babelIfaceAdd.AddChild(&Node{Name: "update-interval="})
+
+	babelNbr := babelNode.AddChild(&Node{Name: "neighbor", Aliases: []string{"neighbors"}})
+	addPrint(babelNbr)
+
+	babelRoutes := babelNode.AddChild(&Node{Name: "routes", Aliases: []string{"route"}})
+	addPrint(babelRoutes)
+
+	// /routing radv
+	radvNode := routingNode.AddChild(&Node{Name: "radv"})
+	radvInst := radvNode.AddChild(&Node{Name: "instance", Aliases: []string{"instances"}})
+	addPrint(radvInst)
+	addRemove(radvInst)
+	addEnableDisable(radvInst)
+	radvInstAdd := radvInst.AddChild(&Node{Name: "add"})
+	radvInstAdd.AddChild(&Node{Name: "name="})
+
+	radvIface := radvNode.AddChild(&Node{Name: "interface", Aliases: []string{"interfaces"}})
+	addPrint(radvIface)
+	addRemove(radvIface)
+	addEnableDisable(radvIface)
+	radvIfaceAdd := radvIface.AddChild(&Node{Name: "add"})
+	radvIfaceAdd.AddChild(&Node{Name: "interface=", Dynamic: dynInterfaces})
+	radvIfaceAdd.AddChild(&Node{Name: "instance="})
+	radvIfaceAdd.AddChild(&Node{Name: "min-ra-interval="})
+	radvIfaceAdd.AddChild(&Node{Name: "max-ra-interval="})
+	radvIfaceAdd.AddChild(&Node{Name: "default-lifetime="})
+	radvIfaceAdd.AddChild(&Node{Name: "prefixes="})
+	radvIfaceAdd.AddChild(&Node{Name: "rdnss="})
 
 	// 4. /tool
 	toolNode := root.AddChild(&Node{Name: "tool"})
@@ -943,6 +1083,21 @@ func (c *Completer) buildCommandTree() *Node {
 	toolBtest.AddChild(&Node{Name: "duration="})
 	toolBtest.AddChild(&Node{Name: "direction=", Dynamic: func() []string { return []string{"receive", "send", "both"} }})
 
+	toolNetwatch := toolNode.AddChild(&Node{Name: "netwatch"})
+	addPrint(toolNetwatch)
+	addRemove(toolNetwatch)
+	toolNWAdd := toolNetwatch.AddChild(&Node{Name: "add"})
+	toolNWAdd.AddChild(&Node{Name: "host="})
+	toolNWAdd.AddChild(&Node{Name: "interval="})
+	toolNWAdd.AddChild(&Node{Name: "timeout="})
+
+	toolIPScan := toolNode.AddChild(&Node{Name: "ip-scan"})
+	toolIPScan.AddChild(&Node{Name: "interface=", Dynamic: dynInterfaces})
+	toolIPScan.AddChild(&Node{Name: "address-range="})
+
+	toolTorch := toolNode.AddChild(&Node{Name: "torch", Aliases: []string{"sniff"}})
+	toolTorch.AddChild(&Node{Name: "interface=", Dynamic: dynInterfaces})
+
 	// 5. /log
 	logNode := root.AddChild(&Node{Name: "log"})
 	logPrint := logNode.AddChild(&Node{Name: "print", Aliases: []string{"pr", "show"}})
@@ -976,7 +1131,14 @@ func (c *Completer) buildCommandTree() *Node {
 	sysIdentSet := sysIdent.AddChild(&Node{Name: "set"})
 	sysIdentSet.AddChild(&Node{Name: "name="})
 
+	sysPkg := sysNode.AddChild(&Node{Name: "package", Aliases: []string{"upgrade"}})
+	addPrint(sysPkg)
+	sysPkgUpdate := sysPkg.AddChild(&Node{Name: "update"})
+	sysPkgUpdate.AddChild(&Node{Name: "check"})
+	sysPkgUpdate.AddChild(&Node{Name: "install"})
+
 	sysNode.AddChild(&Node{Name: "reboot"})
+	sysNode.AddChild(&Node{Name: "shutdown", Aliases: []string{"poweroff"}})
 
 	// 8. /certificate
 	certNode := root.AddChild(&Node{Name: "certificate", Aliases: []string{"certificates", "cert"}})

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 1623 - 26
internal/cli/executor.go


+ 1334 - 28
internal/dynamicrouting/manager.go

@@ -100,6 +100,16 @@ func (m *Manager) initDefaults() {
 		OSPFAreaRanges:        []OSPFAreaRange{},
 		OSPFInterfaces:        []OSPFInterface{},
 		OSPFStaticNbrs:        []OSPFStaticNeighbor{},
+		RIPInstances:          []RIPInstance{},
+		RIPInterfaces:         []RIPInterface{},
+		RIPNeighbors:          []RIPNeighbor{},
+		BFDInstance:           BFDInstance{Enabled: false},
+		BFDInterfaces:         []BFDInterface{},
+		BFDNeighbors:          []BFDNeighbor{},
+		BabelInstances:        []BabelInstance{},
+		BabelInterfaces:       []BabelInterface{},
+		RAdvInstances:         []RAdvInstance{},
+		RAdvInterfaces:        []RAdvInterface{},
 		RoutingFilters:        []RoutingFilterRule{},
 		RoutingCommunityLists: []RoutingFilterCommunityList{},
 		RoutingASLists:        []RoutingFilterASList{},
@@ -124,6 +134,33 @@ func (m *Manager) load() error {
 	if m.config.RoutingASLists == nil {
 		m.config.RoutingASLists = []RoutingFilterASList{}
 	}
+	if m.config.RIPInstances == nil {
+		m.config.RIPInstances = []RIPInstance{}
+	}
+	if m.config.RIPInterfaces == nil {
+		m.config.RIPInterfaces = []RIPInterface{}
+	}
+	if m.config.RIPNeighbors == nil {
+		m.config.RIPNeighbors = []RIPNeighbor{}
+	}
+	if m.config.BFDInterfaces == nil {
+		m.config.BFDInterfaces = []BFDInterface{}
+	}
+	if m.config.BFDNeighbors == nil {
+		m.config.BFDNeighbors = []BFDNeighbor{}
+	}
+	if m.config.BabelInstances == nil {
+		m.config.BabelInstances = []BabelInstance{}
+	}
+	if m.config.BabelInterfaces == nil {
+		m.config.BabelInterfaces = []BabelInterface{}
+	}
+	if m.config.RAdvInstances == nil {
+		m.config.RAdvInstances = []RAdvInstance{}
+	}
+	if m.config.RAdvInterfaces == nil {
+		m.config.RAdvInterfaces = []RAdvInterface{}
+	}
 
 	if len(m.config.OSPFInstances) == 0 {
 		rID := m.config.OSPFInstance.RouterID
@@ -283,6 +320,9 @@ func (m *Manager) generateBirdConfig() string {
 	sb.WriteString("            if source = RTS_RIP then {\n")
 	sb.WriteString("                krt_metric = 120;\n")
 	sb.WriteString("            }\n")
+	sb.WriteString("            if source = RTS_BABEL then {\n")
+	sb.WriteString("                krt_metric = 130;\n")
+	sb.WriteString("            }\n")
 	sb.WriteString("            if source ~ [ RTS_STATIC, RTS_INHERIT ] then {\n")
 	sb.WriteString("                krt_metric = 255;\n")
 	sb.WriteString("            }\n")
@@ -291,6 +331,14 @@ func (m *Manager) generateBirdConfig() string {
 	sb.WriteString("    };\n")
 	sb.WriteString("    learn;\n")
 	sb.WriteString("}\n\n")
+	sb.WriteString("protocol kernel {\n")
+	sb.WriteString("    metric 0;\n")
+	sb.WriteString("    ipv6 {\n")
+	sb.WriteString("        import none;\n")
+	sb.WriteString("        export all;\n")
+	sb.WriteString("    };\n")
+	sb.WriteString("    learn;\n")
+	sb.WriteString("}\n\n")
 
 	// Static BGP Networks announcements
 	var activeNets []string
@@ -533,6 +581,9 @@ func (m *Manager) generateBirdConfig() string {
 			}
 
 			sb.WriteString("    };\n")
+			if peer.UseBFD {
+				sb.WriteString("    bfd yes;\n")
+			}
 			sb.WriteString("}\n\n")
 		}
 	}
@@ -790,6 +841,9 @@ func (m *Manager) generateBirdConfig() string {
 					sb.WriteString("            authentication simple;\n")
 					sb.WriteString(fmt.Sprintf("            password \"%s\";\n", iface.AuthKey))
 				}
+				if iface.UseBFD {
+					sb.WriteString("            bfd yes;\n")
+				}
 
 				// Find static neighbors matching this interface template
 				var templateNbrs []OSPFStaticNeighbor
@@ -864,6 +918,304 @@ func (m *Manager) generateBirdConfig() string {
 		sb.WriteString("}\n\n")
 	}
 
+	// BFD Protocol (/routing bfd)
+	if m.config.BFDInstance.Enabled || len(m.config.BFDInterfaces) > 0 || len(m.config.BFDNeighbors) > 0 {
+		bfdDisabled := m.config.BFDInstance.Disabled
+		sb.WriteString("protocol bfd bfd_main {\n")
+		if bfdDisabled {
+			sb.WriteString("    disabled;\n")
+		}
+		for _, bif := range m.config.BFDInterfaces {
+			if bif.Disabled {
+				continue
+			}
+			ifName := strings.TrimSpace(bif.Interface)
+			if ifName == "" {
+				ifName = "*"
+			}
+			sb.WriteString(fmt.Sprintf("    interface \"%s\" {\n", ifName))
+			if bif.MinRxInterval > 0 {
+				sb.WriteString(fmt.Sprintf("        min rx interval %d ms;\n", bif.MinRxInterval))
+			}
+			if bif.MinTxInterval > 0 {
+				sb.WriteString(fmt.Sprintf("        min tx interval %d ms;\n", bif.MinTxInterval))
+			}
+			if bif.IdleTxInterval > 0 {
+				sb.WriteString(fmt.Sprintf("        idle tx interval %d ms;\n", bif.IdleTxInterval))
+			}
+			if bif.Multiplier > 0 {
+				sb.WriteString(fmt.Sprintf("        multiplier %d;\n", bif.Multiplier))
+			}
+			if bif.Passive {
+				sb.WriteString("        passive yes;\n")
+			}
+			if bif.Auth != "" && bif.Auth != "none" && bif.AuthKey != "" {
+				sb.WriteString(fmt.Sprintf("        authentication %s;\n", bif.Auth))
+				sb.WriteString(fmt.Sprintf("        password \"%s\";\n", bif.AuthKey))
+			}
+			sb.WriteString("    };\n")
+		}
+		for _, bn := range m.config.BFDNeighbors {
+			if bn.Disabled || strings.TrimSpace(bn.Address) == "" {
+				continue
+			}
+			if bn.Interface != "" {
+				sb.WriteString(fmt.Sprintf("    neighbor %s dev \"%s\";\n", strings.TrimSpace(bn.Address), strings.TrimSpace(bn.Interface)))
+			} else if bn.Multihop {
+				sb.WriteString(fmt.Sprintf("    neighbor %s multihop;\n", strings.TrimSpace(bn.Address)))
+			} else {
+				sb.WriteString(fmt.Sprintf("    neighbor %s;\n", strings.TrimSpace(bn.Address)))
+			}
+		}
+		sb.WriteString("}\n\n")
+	}
+
+	// RIP Protocols (/routing rip)
+	for idx, inst := range m.config.RIPInstances {
+		if inst.Disabled {
+			continue
+		}
+		instProtoName := sanitizeIdentifier("rip", fmt.Sprintf("%s_%d", inst.Name, idx))
+
+		protoKeyword := "protocol rip"
+		channelKeyword := "ipv4"
+		if inst.Version == 1 {
+			protoKeyword = "protocol rip ng"
+			channelKeyword = "ipv6"
+		}
+
+		sb.WriteString(fmt.Sprintf("%s %s {\n", protoKeyword, instProtoName))
+		if inst.VRF != "" && inst.VRF != "main" && inst.VRF != "default" {
+			sb.WriteString(fmt.Sprintf("    vrf %s;\n", inst.VRF))
+		}
+		sb.WriteString(fmt.Sprintf("    %s {\n", channelKeyword))
+		if inst.RoutingTable != "" && inst.RoutingTable != "main" {
+			sb.WriteString(fmt.Sprintf("        table %s;\n", inst.RoutingTable))
+		}
+		sb.WriteString("        import filter { krt_metric = 120; accept; };\n")
+
+		var ripExports []string
+		if inst.RedistributeStatic {
+			ripExports = append(ripExports, "RTS_STATIC", "RTS_INHERIT")
+		}
+		if inst.RedistributeConnected {
+			ripExports = append(ripExports, "RTS_DEVICE")
+		}
+		if inst.RedistributeOSPF {
+			ripExports = append(ripExports, "RTS_OSPF")
+		}
+		if inst.RedistributeBGP {
+			ripExports = append(ripExports, "RTS_BGP")
+		}
+
+		if len(ripExports) > 0 || inst.RedistributeDefault {
+			sb.WriteString("        export filter {\n")
+			if inst.RedistributeDefault {
+				sb.WriteString("            if net = 0.0.0.0/0 || net = ::/0 then accept;\n")
+			}
+			if len(ripExports) > 0 {
+				sb.WriteString(fmt.Sprintf("            if source ~ [ %s ] then accept;\n", strings.Join(ripExports, ", ")))
+			}
+			sb.WriteString("            reject;\n")
+			sb.WriteString("        };\n")
+		} else {
+			sb.WriteString("        export none;\n")
+		}
+		sb.WriteString("    };\n\n")
+
+		// Interfaces for this RIP instance
+		for _, iface := range m.config.RIPInterfaces {
+			if iface.Disabled {
+				continue
+			}
+			if iface.Instance != "" && !strings.EqualFold(iface.Instance, inst.Name) && !strings.EqualFold(iface.Instance, "Default") {
+				continue
+			}
+			ifName := strings.TrimSpace(iface.Interface)
+			if ifName == "" {
+				ifName = "*"
+			}
+			sb.WriteString(fmt.Sprintf("    interface \"%s\" {\n", ifName))
+			if iface.Metric > 0 {
+				sb.WriteString(fmt.Sprintf("        metric %d;\n", iface.Metric))
+			}
+			if iface.Mode == "broadcast" {
+				sb.WriteString("        mode broadcast;\n")
+			} else {
+				sb.WriteString("        mode multicast;\n")
+			}
+			if inst.Version == 2 || inst.Version == 0 {
+				sb.WriteString("        version 2;\n")
+			}
+			if inst.UpdateTime > 0 {
+				sb.WriteString(fmt.Sprintf("        update time %d;\n", inst.UpdateTime))
+			}
+			if inst.TimeoutTime > 0 {
+				sb.WriteString(fmt.Sprintf("        timeout time %d;\n", inst.TimeoutTime))
+			}
+			if iface.Passive {
+				sb.WriteString("        passive yes;\n")
+			}
+			if iface.UseBFD {
+				sb.WriteString("        bfd yes;\n")
+			}
+			if iface.Auth != "" && iface.Auth != "none" && iface.AuthKey != "" {
+				if iface.Auth == "md5" || iface.Auth == "cryptographic" {
+					sb.WriteString("        authentication cryptographic;\n")
+				} else {
+					sb.WriteString("        authentication plaintext;\n")
+				}
+				sb.WriteString(fmt.Sprintf("        password \"%s\";\n", iface.AuthKey))
+			}
+			sb.WriteString("    };\n")
+		}
+
+		sb.WriteString("}\n\n")
+	}
+
+	// Babel Protocols (/routing babel)
+	for idx, inst := range m.config.BabelInstances {
+		if inst.Disabled {
+			continue
+		}
+		instProtoName := sanitizeIdentifier("babel", fmt.Sprintf("%s_%d", inst.Name, idx))
+		sb.WriteString(fmt.Sprintf("protocol babel %s {\n", instProtoName))
+		if inst.VRF != "" && inst.VRF != "main" && inst.VRF != "default" {
+			sb.WriteString(fmt.Sprintf("    vrf %s;\n", inst.VRF))
+		}
+		if inst.RouterID != "" {
+			sb.WriteString(fmt.Sprintf("    router id %s;\n", inst.RouterID))
+		}
+		sb.WriteString("    ipv4 {\n")
+		if inst.RoutingTable != "" && inst.RoutingTable != "main" {
+			sb.WriteString(fmt.Sprintf("        table %s;\n", inst.RoutingTable))
+		}
+		sb.WriteString("        import filter { krt_metric = 130; accept; };\n")
+
+		var babelExports []string
+		if inst.RedistributeStatic {
+			babelExports = append(babelExports, "RTS_STATIC", "RTS_INHERIT")
+		}
+		if inst.RedistributeConnected {
+			babelExports = append(babelExports, "RTS_DEVICE")
+		}
+		if inst.RedistributeOSPF {
+			babelExports = append(babelExports, "RTS_OSPF")
+		}
+		if inst.RedistributeBGP {
+			babelExports = append(babelExports, "RTS_BGP")
+		}
+
+		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")
+			}
+			if len(babelExports) > 0 {
+				sb.WriteString(fmt.Sprintf("            if source ~ [ %s ] then accept;\n", strings.Join(babelExports, ", ")))
+			}
+			sb.WriteString("            reject;\n")
+			sb.WriteString("        };\n")
+		} else {
+			sb.WriteString("        export none;\n")
+		}
+		sb.WriteString("    };\n\n")
+
+		sb.WriteString("    ipv6 {\n")
+		sb.WriteString("        import all;\n")
+		sb.WriteString("        export all;\n")
+		sb.WriteString("    };\n\n")
+
+		for _, iface := range m.config.BabelInterfaces {
+			if iface.Disabled {
+				continue
+			}
+			if iface.Instance != "" && !strings.EqualFold(iface.Instance, inst.Name) && !strings.EqualFold(iface.Instance, "Default") {
+				continue
+			}
+			ifName := strings.TrimSpace(iface.Interface)
+			if ifName == "" {
+				ifName = "*"
+			}
+			sb.WriteString(fmt.Sprintf("    interface \"%s\" {\n", ifName))
+			if iface.Type != "" {
+				sb.WriteString(fmt.Sprintf("        type %s;\n", iface.Type))
+			} else {
+				sb.WriteString("        type wired;\n")
+			}
+			if iface.RxCost > 0 {
+				sb.WriteString(fmt.Sprintf("        rxcost %d;\n", iface.RxCost))
+			}
+			if iface.HelloInterval > 0 {
+				sb.WriteString(fmt.Sprintf("        hello interval %d s;\n", iface.HelloInterval))
+			}
+			if iface.UpdateInterval > 0 {
+				sb.WriteString(fmt.Sprintf("        update interval %d s;\n", iface.UpdateInterval))
+			}
+			if iface.Auth != "" && iface.Auth != "none" && iface.AuthKey != "" {
+				sb.WriteString("        authentication mac;\n")
+				sb.WriteString(fmt.Sprintf("        password \"%s\";\n", iface.AuthKey))
+			}
+			sb.WriteString("    };\n")
+		}
+		sb.WriteString("}\n\n")
+	}
+
+	// RAdv Protocols (/routing radv)
+	for idx, inst := range m.config.RAdvInstances {
+		if inst.Disabled {
+			continue
+		}
+		instProtoName := sanitizeIdentifier("radv", fmt.Sprintf("%s_%d", inst.Name, idx))
+		sb.WriteString(fmt.Sprintf("protocol radv %s {\n", instProtoName))
+		for _, iface := range m.config.RAdvInterfaces {
+			if iface.Disabled {
+				continue
+			}
+			if iface.Instance != "" && !strings.EqualFold(iface.Instance, inst.Name) && !strings.EqualFold(iface.Instance, "Default") {
+				continue
+			}
+			ifName := strings.TrimSpace(iface.Interface)
+			if ifName == "" {
+				ifName = "*"
+			}
+			sb.WriteString(fmt.Sprintf("    interface \"%s\" {\n", ifName))
+			if iface.MinRAInterval > 0 {
+				sb.WriteString(fmt.Sprintf("        min ra interval %d;\n", iface.MinRAInterval))
+			}
+			if iface.MaxRAInterval > 0 {
+				sb.WriteString(fmt.Sprintf("        max ra interval %d;\n", iface.MaxRAInterval))
+			}
+			if iface.DefaultLifetime > 0 {
+				sb.WriteString(fmt.Sprintf("        default lifetime %d;\n", iface.DefaultLifetime))
+			}
+			if iface.HopLimit > 0 {
+				sb.WriteString(fmt.Sprintf("        hop limit %d;\n", iface.HopLimit))
+			}
+			for _, pfx := range iface.Prefixes {
+				pfx = strings.TrimSpace(pfx)
+				if pfx != "" {
+					sb.WriteString(fmt.Sprintf("        prefix %s {\n", pfx))
+					sb.WriteString("            autonomous 1;\n")
+					sb.WriteString("            onlink 1;\n")
+					sb.WriteString("        };\n")
+				}
+			}
+			if len(iface.RDNSS) > 0 {
+				sb.WriteString("        rdnss {\n")
+				for _, ns := range iface.RDNSS {
+					ns = strings.TrimSpace(ns)
+					if ns != "" {
+						sb.WriteString(fmt.Sprintf("            ns %s;\n", ns))
+					}
+				}
+				sb.WriteString("        };\n")
+			}
+			sb.WriteString("    };\n")
+		}
+		sb.WriteString("}\n\n")
+	}
+
 	return sb.String()
 }
 
@@ -2417,46 +2769,1000 @@ func (m *Manager) SetASListState(id string, disabled bool) error {
 }
 
 // -------------------------------------------------------------
-// Summary Telemetry
+// RIP Methods (/routing rip)
 // -------------------------------------------------------------
 
-func (m *Manager) GetSummary() DynamicRoutingSummary {
+func (m *Manager) ListRIPInstances() []RIPInstance {
 	m.mu.RLock()
 	defer m.mu.RUnlock()
 
-	summary := DynamicRoutingSummary{
-		BGPEnabled:         m.config.BGPInstance.Enabled,
-		OSPFEnabled:        m.config.OSPFInstance.Enabled,
-		BGPPeerCount:       len(m.config.BGPPeers),
-		RoutingFilterCount: len(m.config.RoutingFilters),
-	}
-
 	out, err := birdcExec("show status")
 	birdRunning := err == nil && strings.Contains(string(out), "Daemon is up and running")
 
-	if birdRunning && summary.BGPEnabled {
-		summary.BGPRunning = true
-		peers := m.ListBGPPeers()
-		est := 0
-		for _, p := range peers {
-			if strings.EqualFold(p.State, "Established") || strings.EqualFold(p.State, "Up") {
-				est++
-			}
+	res := make([]RIPInstance, len(m.config.RIPInstances))
+	copy(res, m.config.RIPInstances)
+	for i := range res {
+		res[i].Running = birdRunning && res[i].Enabled && !res[i].Disabled
+	}
+	return res
+}
+
+func (m *Manager) SaveRIPInstance(inst RIPInstance) (*RIPInstance, error) {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
+	if inst.ID == "" {
+		inst.ID = fmt.Sprintf("rip-inst-%d", time.Now().UnixNano())
+	}
+	if inst.Name == "" {
+		inst.Name = "default"
+	}
+	if inst.Version == 0 {
+		inst.Version = 2
+	}
+	if inst.UpdateTime == 0 {
+		inst.UpdateTime = 30
+	}
+	if inst.TimeoutTime == 0 {
+		inst.TimeoutTime = 180
+	}
+	if inst.GarbageTime == 0 {
+		inst.GarbageTime = 120
+	}
+
+	found := false
+	for i, existing := range m.config.RIPInstances {
+		if existing.ID == inst.ID || strings.EqualFold(existing.Name, inst.Name) {
+			m.config.RIPInstances[i] = inst
+			found = true
+			break
 		}
-		summary.BGPEstablishedCount = est
+	}
+	if !found {
+		m.config.RIPInstances = append(m.config.RIPInstances, inst)
 	}
 
-	if birdRunning && summary.OSPFEnabled {
-		summary.OSPFRunning = true
-		nbrs := m.GetOSPFNeighbors()
-		summary.OSPFNeighborCount = len(nbrs)
-		adj := 0
-		for _, n := range nbrs {
-			if strings.Contains(strings.ToLower(n.State), "full") {
-				adj++
-			}
+	if err := m.save(); err != nil {
+		return nil, err
+	}
+	if err := m.applyBirdConfig(); err != nil {
+		return nil, err
+	}
+	return &inst, nil
+}
+
+func (m *Manager) DeleteRIPInstance(id string) error {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
+	newInsts := make([]RIPInstance, 0, len(m.config.RIPInstances))
+	for _, inst := range m.config.RIPInstances {
+		if inst.ID != id && inst.Name != id {
+			newInsts = append(newInsts, inst)
 		}
-		summary.OSPFAdjacentCount = adj
+	}
+	m.config.RIPInstances = newInsts
+
+	if err := m.save(); err != nil {
+		return err
+	}
+	return m.applyBirdConfig()
+}
+
+func (m *Manager) SetRIPInstanceState(id string, disabled bool) error {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
+	for i := range m.config.RIPInstances {
+		if m.config.RIPInstances[i].ID == id || m.config.RIPInstances[i].Name == id {
+			m.config.RIPInstances[i].Disabled = disabled
+			m.config.RIPInstances[i].Enabled = !disabled
+			break
+		}
+	}
+
+	if err := m.save(); err != nil {
+		return err
+	}
+	return m.applyBirdConfig()
+}
+
+func (m *Manager) ListRIPInterfaces() []RIPInterface {
+	m.mu.RLock()
+	defer m.mu.RUnlock()
+	res := make([]RIPInterface, len(m.config.RIPInterfaces))
+	copy(res, m.config.RIPInterfaces)
+	return res
+}
+
+func (m *Manager) SaveRIPInterface(iface RIPInterface) (*RIPInterface, error) {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
+	if iface.ID == "" {
+		iface.ID = fmt.Sprintf("rip-iface-%d", time.Now().UnixNano())
+	}
+	if iface.Interface == "" {
+		iface.Interface = "*"
+	}
+	if iface.Metric == 0 {
+		iface.Metric = 1
+	}
+
+	found := false
+	for i, existing := range m.config.RIPInterfaces {
+		if existing.ID == iface.ID {
+			m.config.RIPInterfaces[i] = iface
+			found = true
+			break
+		}
+	}
+	if !found {
+		m.config.RIPInterfaces = append(m.config.RIPInterfaces, iface)
+	}
+
+	if err := m.save(); err != nil {
+		return nil, err
+	}
+	if err := m.applyBirdConfig(); err != nil {
+		return nil, err
+	}
+	return &iface, nil
+}
+
+func (m *Manager) DeleteRIPInterface(id string) error {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
+	newIfaces := make([]RIPInterface, 0, len(m.config.RIPInterfaces))
+	for _, iface := range m.config.RIPInterfaces {
+		if iface.ID != id && iface.Interface != id {
+			newIfaces = append(newIfaces, iface)
+		}
+	}
+	m.config.RIPInterfaces = newIfaces
+
+	if err := m.save(); err != nil {
+		return err
+	}
+	return m.applyBirdConfig()
+}
+
+func (m *Manager) SetRIPInterfaceState(id string, disabled bool) error {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
+	for i := range m.config.RIPInterfaces {
+		if m.config.RIPInterfaces[i].ID == id || m.config.RIPInterfaces[i].Interface == id {
+			m.config.RIPInterfaces[i].Disabled = disabled
+			break
+		}
+	}
+
+	if err := m.save(); err != nil {
+		return err
+	}
+	return m.applyBirdConfig()
+}
+
+func (m *Manager) ListRIPNeighbors() []RIPNeighbor {
+	m.mu.RLock()
+	defer m.mu.RUnlock()
+	res := make([]RIPNeighbor, len(m.config.RIPNeighbors))
+	copy(res, m.config.RIPNeighbors)
+	return res
+}
+
+func (m *Manager) SaveRIPNeighbor(nbr RIPNeighbor) (*RIPNeighbor, error) {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
+	if nbr.ID == "" {
+		nbr.ID = fmt.Sprintf("rip-nbr-%d", time.Now().UnixNano())
+	}
+
+	found := false
+	for i, existing := range m.config.RIPNeighbors {
+		if existing.ID == nbr.ID || existing.Address == nbr.Address {
+			m.config.RIPNeighbors[i] = nbr
+			found = true
+			break
+		}
+	}
+	if !found {
+		m.config.RIPNeighbors = append(m.config.RIPNeighbors, nbr)
+	}
+
+	if err := m.save(); err != nil {
+		return nil, err
+	}
+	if err := m.applyBirdConfig(); err != nil {
+		return nil, err
+	}
+	return &nbr, nil
+}
+
+func (m *Manager) DeleteRIPNeighbor(id string) error {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
+	newNbrs := make([]RIPNeighbor, 0, len(m.config.RIPNeighbors))
+	for _, nbr := range m.config.RIPNeighbors {
+		if nbr.ID != id && nbr.Address != id {
+			newNbrs = append(newNbrs, nbr)
+		}
+	}
+	m.config.RIPNeighbors = newNbrs
+
+	if err := m.save(); err != nil {
+		return err
+	}
+	return m.applyBirdConfig()
+}
+
+func (m *Manager) GetRIPNeighbors() []RIPNeighborLive {
+	var list []RIPNeighborLive
+	out, err := birdcExec("show rip neighbors")
+	if err != nil {
+		return list
+	}
+	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 RIP") {
+			continue
+		}
+		fields := strings.Fields(trimmed)
+		if len(fields) >= 2 {
+			nbr := RIPNeighborLive{
+				Address:   fields[0],
+				Interface: fields[1],
+			}
+			if len(fields) >= 3 {
+				nbr.LastSeen = fields[len(fields)-1]
+			}
+			list = append(list, nbr)
+		}
+	}
+	return list
+}
+
+func (m *Manager) GetRIPRoutes() []RIPRoute {
+	var routes []RIPRoute
+	out, err := birdcExec("show route where source = RTS_RIP all")
+	if err != nil {
+		return routes
+	}
+	lines := strings.Split(string(out), "\n")
+	var cur *RIPRoute
+	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 {
+				routes = append(routes, RIPRoute{
+					Prefix: fields[0],
+				})
+				cur = &routes[len(routes)-1]
+				for i := 1; i < len(fields); i++ {
+					if fields[i] == "via" && i+1 < len(fields) {
+						cur.NextHop = fields[i+1]
+					}
+					if fields[i] == "dev" && i+1 < len(fields) {
+						cur.Interface = fields[i+1]
+					}
+				}
+			}
+		} else if cur != nil {
+			if strings.Contains(trimmed, "rip_metric:") {
+				parts := strings.Split(trimmed, ":")
+				if len(parts) >= 2 {
+					var mVal int
+					fmt.Sscanf(strings.TrimSpace(parts[1]), "%d", &mVal)
+					cur.Metric = mVal
+				}
+			}
+		}
+	}
+	return routes
+}
+
+// -------------------------------------------------------------
+// BFD Methods (/routing bfd)
+// -------------------------------------------------------------
+
+func (m *Manager) GetBFDInstance() BFDInstance {
+	m.mu.RLock()
+	defer m.mu.RUnlock()
+
+	inst := m.config.BFDInstance
+	out, err := birdcExec("show status")
+	inst.Running = err == nil && strings.Contains(string(out), "Daemon is up and running") && inst.Enabled && !inst.Disabled
+	return inst
+}
+
+func (m *Manager) SaveBFDInstance(inst BFDInstance) error {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
+	m.config.BFDInstance = inst
+	if err := m.save(); err != nil {
+		return err
+	}
+	return m.applyBirdConfig()
+}
+
+func (m *Manager) ListBFDInterfaces() []BFDInterface {
+	m.mu.RLock()
+	defer m.mu.RUnlock()
+	res := make([]BFDInterface, len(m.config.BFDInterfaces))
+	copy(res, m.config.BFDInterfaces)
+	return res
+}
+
+func (m *Manager) SaveBFDInterface(iface BFDInterface) (*BFDInterface, error) {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
+	if iface.ID == "" {
+		iface.ID = fmt.Sprintf("bfd-iface-%d", time.Now().UnixNano())
+	}
+	if iface.Interface == "" {
+		iface.Interface = "*"
+	}
+	if iface.MinRxInterval == 0 {
+		iface.MinRxInterval = 100
+	}
+	if iface.MinTxInterval == 0 {
+		iface.MinTxInterval = 100
+	}
+	if iface.Multiplier == 0 {
+		iface.Multiplier = 3
+	}
+
+	found := false
+	for i, existing := range m.config.BFDInterfaces {
+		if existing.ID == iface.ID {
+			m.config.BFDInterfaces[i] = iface
+			found = true
+			break
+		}
+	}
+	if !found {
+		m.config.BFDInterfaces = append(m.config.BFDInterfaces, iface)
+	}
+
+	if err := m.save(); err != nil {
+		return nil, err
+	}
+	if err := m.applyBirdConfig(); err != nil {
+		return nil, err
+	}
+	return &iface, nil
+}
+
+func (m *Manager) DeleteBFDInterface(id string) error {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
+	newIfaces := make([]BFDInterface, 0, len(m.config.BFDInterfaces))
+	for _, iface := range m.config.BFDInterfaces {
+		if iface.ID != id && iface.Interface != id {
+			newIfaces = append(newIfaces, iface)
+		}
+	}
+	m.config.BFDInterfaces = newIfaces
+
+	if err := m.save(); err != nil {
+		return err
+	}
+	return m.applyBirdConfig()
+}
+
+func (m *Manager) SetBFDInterfaceState(id string, disabled bool) error {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
+	for i := range m.config.BFDInterfaces {
+		if m.config.BFDInterfaces[i].ID == id || m.config.BFDInterfaces[i].Interface == id {
+			m.config.BFDInterfaces[i].Disabled = disabled
+			break
+		}
+	}
+
+	if err := m.save(); err != nil {
+		return err
+	}
+	return m.applyBirdConfig()
+}
+
+func (m *Manager) ListBFDNeighbors() []BFDNeighbor {
+	m.mu.RLock()
+	defer m.mu.RUnlock()
+	res := make([]BFDNeighbor, len(m.config.BFDNeighbors))
+	copy(res, m.config.BFDNeighbors)
+	return res
+}
+
+func (m *Manager) SaveBFDNeighbor(nbr BFDNeighbor) (*BFDNeighbor, error) {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
+	if nbr.ID == "" {
+		nbr.ID = fmt.Sprintf("bfd-nbr-%d", time.Now().UnixNano())
+	}
+
+	found := false
+	for i, existing := range m.config.BFDNeighbors {
+		if existing.ID == nbr.ID || existing.Address == nbr.Address {
+			m.config.BFDNeighbors[i] = nbr
+			found = true
+			break
+		}
+	}
+	if !found {
+		m.config.BFDNeighbors = append(m.config.BFDNeighbors, nbr)
+	}
+
+	if err := m.save(); err != nil {
+		return nil, err
+	}
+	if err := m.applyBirdConfig(); err != nil {
+		return nil, err
+	}
+	return &nbr, nil
+}
+
+func (m *Manager) DeleteBFDNeighbor(id string) error {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
+	newNbrs := make([]BFDNeighbor, 0, len(m.config.BFDNeighbors))
+	for _, nbr := range m.config.BFDNeighbors {
+		if nbr.ID != id && nbr.Address != id {
+			newNbrs = append(newNbrs, nbr)
+		}
+	}
+	m.config.BFDNeighbors = newNbrs
+
+	if err := m.save(); err != nil {
+		return err
+	}
+	return m.applyBirdConfig()
+}
+
+func (m *Manager) GetBFDSessions() []BFDSession {
+	var list []BFDSession
+	out, err := birdcExec("show bfd sessions")
+	if err != nil {
+		return list
+	}
+	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 BFD") {
+			continue
+		}
+		fields := strings.Fields(trimmed)
+		if len(fields) >= 3 {
+			sess := BFDSession{
+				IP:        fields[0],
+				Interface: fields[1],
+				State:     fields[2],
+			}
+			if len(fields) >= 4 {
+				sess.Since = fields[3]
+			}
+			if len(fields) >= 5 {
+				sess.Interval = fields[4]
+			}
+			if len(fields) >= 6 {
+				sess.Timeout = fields[5]
+			}
+			list = append(list, sess)
+		}
+	}
+	return list
+}
+
+// -------------------------------------------------------------
+// Babel Methods (/routing babel)
+// -------------------------------------------------------------
+
+func (m *Manager) ListBabelInstances() []BabelInstance {
+	m.mu.RLock()
+	defer m.mu.RUnlock()
+
+	out, err := birdcExec("show status")
+	birdRunning := err == nil && strings.Contains(string(out), "Daemon is up and running")
+
+	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
+	}
+	return res
+}
+
+func (m *Manager) SaveBabelInstance(inst BabelInstance) (*BabelInstance, error) {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
+	if inst.ID == "" {
+		inst.ID = fmt.Sprintf("babel-inst-%d", time.Now().UnixNano())
+	}
+	if inst.Name == "" {
+		inst.Name = "default"
+	}
+
+	found := false
+	for i, existing := range m.config.BabelInstances {
+		if existing.ID == inst.ID || strings.EqualFold(existing.Name, inst.Name) {
+			m.config.BabelInstances[i] = inst
+			found = true
+			break
+		}
+	}
+	if !found {
+		m.config.BabelInstances = append(m.config.BabelInstances, inst)
+	}
+
+	if err := m.save(); err != nil {
+		return nil, err
+	}
+	if err := m.applyBirdConfig(); err != nil {
+		return nil, err
+	}
+	return &inst, nil
+}
+
+func (m *Manager) DeleteBabelInstance(id string) error {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
+	newInsts := make([]BabelInstance, 0, len(m.config.BabelInstances))
+	for _, inst := range m.config.BabelInstances {
+		if inst.ID != id && inst.Name != id {
+			newInsts = append(newInsts, inst)
+		}
+	}
+	m.config.BabelInstances = newInsts
+
+	if err := m.save(); err != nil {
+		return err
+	}
+	return m.applyBirdConfig()
+}
+
+func (m *Manager) SetBabelInstanceState(id string, disabled bool) error {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
+	for i := range m.config.BabelInstances {
+		if m.config.BabelInstances[i].ID == id || m.config.BabelInstances[i].Name == id {
+			m.config.BabelInstances[i].Disabled = disabled
+			m.config.BabelInstances[i].Enabled = !disabled
+			break
+		}
+	}
+
+	if err := m.save(); err != nil {
+		return err
+	}
+	return m.applyBirdConfig()
+}
+
+func (m *Manager) ListBabelInterfaces() []BabelInterface {
+	m.mu.RLock()
+	defer m.mu.RUnlock()
+	res := make([]BabelInterface, len(m.config.BabelInterfaces))
+	copy(res, m.config.BabelInterfaces)
+	return res
+}
+
+func (m *Manager) SaveBabelInterface(iface BabelInterface) (*BabelInterface, error) {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
+	if iface.ID == "" {
+		iface.ID = fmt.Sprintf("babel-iface-%d", time.Now().UnixNano())
+	}
+	if iface.Interface == "" {
+		iface.Interface = "*"
+	}
+	if iface.Type == "" {
+		iface.Type = "wired"
+	}
+	if iface.RxCost == 0 {
+		iface.RxCost = 10
+	}
+	if iface.HelloInterval == 0 {
+		iface.HelloInterval = 4
+	}
+	if iface.UpdateInterval == 0 {
+		iface.UpdateInterval = 16
+	}
+
+	found := false
+	for i, existing := range m.config.BabelInterfaces {
+		if existing.ID == iface.ID {
+			m.config.BabelInterfaces[i] = iface
+			found = true
+			break
+		}
+	}
+	if !found {
+		m.config.BabelInterfaces = append(m.config.BabelInterfaces, iface)
+	}
+
+	if err := m.save(); err != nil {
+		return nil, err
+	}
+	if err := m.applyBirdConfig(); err != nil {
+		return nil, err
+	}
+	return &iface, nil
+}
+
+func (m *Manager) DeleteBabelInterface(id string) error {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
+	newIfaces := make([]BabelInterface, 0, len(m.config.BabelInterfaces))
+	for _, iface := range m.config.BabelInterfaces {
+		if iface.ID != id && iface.Interface != id {
+			newIfaces = append(newIfaces, iface)
+		}
+	}
+	m.config.BabelInterfaces = newIfaces
+
+	if err := m.save(); err != nil {
+		return err
+	}
+	return m.applyBirdConfig()
+}
+
+func (m *Manager) SetBabelInterfaceState(id string, disabled bool) error {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
+	for i := range m.config.BabelInterfaces {
+		if m.config.BabelInterfaces[i].ID == id || m.config.BabelInterfaces[i].Interface == id {
+			m.config.BabelInterfaces[i].Disabled = disabled
+			break
+		}
+	}
+
+	if err := m.save(); err != nil {
+		return err
+	}
+	return m.applyBirdConfig()
+}
+
+func (m *Manager) GetBabelNeighbors() []BabelNeighbor {
+	var list []BabelNeighbor
+	out, err := birdcExec("show babel neighbors")
+	if err != nil {
+		return list
+	}
+	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") {
+			continue
+		}
+		fields := strings.Fields(trimmed)
+		if len(fields) >= 2 {
+			nbr := BabelNeighbor{
+				IP:        fields[0],
+				Interface: fields[1],
+			}
+			if len(fields) >= 3 {
+				fmt.Sscanf(fields[2], "%d", &nbr.Metric)
+			}
+			if len(fields) >= 4 {
+				fmt.Sscanf(fields[3], "%d", &nbr.Cost)
+			}
+			if len(fields) >= 5 {
+				nbr.Uptime = fields[4]
+			}
+			list = append(list, nbr)
+		}
+	}
+	return list
+}
+
+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, " ") {
+			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)
+			}
+		}
+	}
+	return routes
+}
+
+// -------------------------------------------------------------
+// RAdv Methods (/routing radv)
+// -------------------------------------------------------------
+
+func (m *Manager) ListRAdvInstances() []RAdvInstance {
+	m.mu.RLock()
+	defer m.mu.RUnlock()
+
+	out, err := birdcExec("show status")
+	birdRunning := err == nil && strings.Contains(string(out), "Daemon is up and running")
+
+	res := make([]RAdvInstance, len(m.config.RAdvInstances))
+	copy(res, m.config.RAdvInstances)
+	for i := range res {
+		res[i].Running = birdRunning && res[i].Enabled && !res[i].Disabled
+	}
+	return res
+}
+
+func (m *Manager) SaveRAdvInstance(inst RAdvInstance) (*RAdvInstance, error) {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
+	if inst.ID == "" {
+		inst.ID = fmt.Sprintf("radv-inst-%d", time.Now().UnixNano())
+	}
+	if inst.Name == "" {
+		inst.Name = "default"
+	}
+
+	found := false
+	for i, existing := range m.config.RAdvInstances {
+		if existing.ID == inst.ID || strings.EqualFold(existing.Name, inst.Name) {
+			m.config.RAdvInstances[i] = inst
+			found = true
+			break
+		}
+	}
+	if !found {
+		m.config.RAdvInstances = append(m.config.RAdvInstances, inst)
+	}
+
+	if err := m.save(); err != nil {
+		return nil, err
+	}
+	if err := m.applyBirdConfig(); err != nil {
+		return nil, err
+	}
+	return &inst, nil
+}
+
+func (m *Manager) DeleteRAdvInstance(id string) error {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
+	newInsts := make([]RAdvInstance, 0, len(m.config.RAdvInstances))
+	for _, inst := range m.config.RAdvInstances {
+		if inst.ID != id && inst.Name != id {
+			newInsts = append(newInsts, inst)
+		}
+	}
+	m.config.RAdvInstances = newInsts
+
+	if err := m.save(); err != nil {
+		return err
+	}
+	return m.applyBirdConfig()
+}
+
+func (m *Manager) SetRAdvInstanceState(id string, disabled bool) error {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
+	for i := range m.config.RAdvInstances {
+		if m.config.RAdvInstances[i].ID == id || m.config.RAdvInstances[i].Name == id {
+			m.config.RAdvInstances[i].Disabled = disabled
+			m.config.RAdvInstances[i].Enabled = !disabled
+			break
+		}
+	}
+
+	if err := m.save(); err != nil {
+		return err
+	}
+	return m.applyBirdConfig()
+}
+
+func (m *Manager) ListRAdvInterfaces() []RAdvInterface {
+	m.mu.RLock()
+	defer m.mu.RUnlock()
+	res := make([]RAdvInterface, len(m.config.RAdvInterfaces))
+	copy(res, m.config.RAdvInterfaces)
+	return res
+}
+
+func (m *Manager) SaveRAdvInterface(iface RAdvInterface) (*RAdvInterface, error) {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
+	if iface.ID == "" {
+		iface.ID = fmt.Sprintf("radv-iface-%d", time.Now().UnixNano())
+	}
+	if iface.Interface == "" {
+		iface.Interface = "*"
+	}
+	if iface.MinRAInterval == 0 {
+		iface.MinRAInterval = 200
+	}
+	if iface.MaxRAInterval == 0 {
+		iface.MaxRAInterval = 600
+	}
+	if iface.DefaultLifetime == 0 {
+		iface.DefaultLifetime = 1800
+	}
+
+	found := false
+	for i, existing := range m.config.RAdvInterfaces {
+		if existing.ID == iface.ID {
+			m.config.RAdvInterfaces[i] = iface
+			found = true
+			break
+		}
+	}
+	if !found {
+		m.config.RAdvInterfaces = append(m.config.RAdvInterfaces, iface)
+	}
+
+	if err := m.save(); err != nil {
+		return nil, err
+	}
+	if err := m.applyBirdConfig(); err != nil {
+		return nil, err
+	}
+	return &iface, nil
+}
+
+func (m *Manager) DeleteRAdvInterface(id string) error {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
+	newIfaces := make([]RAdvInterface, 0, len(m.config.RAdvInterfaces))
+	for _, iface := range m.config.RAdvInterfaces {
+		if iface.ID != id && iface.Interface != id {
+			newIfaces = append(newIfaces, iface)
+		}
+	}
+	m.config.RAdvInterfaces = newIfaces
+
+	if err := m.save(); err != nil {
+		return err
+	}
+	return m.applyBirdConfig()
+}
+
+func (m *Manager) SetRAdvInterfaceState(id string, disabled bool) error {
+	m.mu.Lock()
+	defer m.mu.Unlock()
+
+	for i := range m.config.RAdvInterfaces {
+		if m.config.RAdvInterfaces[i].ID == id || m.config.RAdvInterfaces[i].Interface == id {
+			m.config.RAdvInterfaces[i].Disabled = disabled
+			break
+		}
+	}
+
+	if err := m.save(); err != nil {
+		return err
+	}
+	return m.applyBirdConfig()
+}
+
+// -------------------------------------------------------------
+// Summary Telemetry
+// -------------------------------------------------------------
+
+func (m *Manager) GetSummary() DynamicRoutingSummary {
+	m.mu.RLock()
+	defer m.mu.RUnlock()
+
+	summary := DynamicRoutingSummary{
+		BGPEnabled:         m.config.BGPInstance.Enabled,
+		OSPFEnabled:        m.config.OSPFInstance.Enabled,
+		BGPPeerCount:       len(m.config.BGPPeers),
+		RoutingFilterCount: len(m.config.RoutingFilters),
+	}
+
+	out, err := birdcExec("show status")
+	birdRunning := err == nil && strings.Contains(string(out), "Daemon is up and running")
+
+	if birdRunning && summary.BGPEnabled {
+		summary.BGPRunning = true
+		peers := m.ListBGPPeers()
+		est := 0
+		for _, p := range peers {
+			if strings.EqualFold(p.State, "Established") || strings.EqualFold(p.State, "Up") {
+				est++
+			}
+		}
+		summary.BGPEstablishedCount = est
+	}
+
+	if birdRunning && summary.OSPFEnabled {
+		summary.OSPFRunning = true
+		nbrs := m.GetOSPFNeighbors()
+		summary.OSPFNeighborCount = len(nbrs)
+		adj := 0
+		for _, n := range nbrs {
+			if strings.Contains(strings.ToLower(n.State), "full") {
+				adj++
+			}
+		}
+		summary.OSPFAdjacentCount = adj
+	}
+
+	for _, inst := range m.config.RIPInstances {
+		if inst.Enabled && !inst.Disabled {
+			summary.RIPEnabled = true
+			break
+		}
+	}
+	if summary.RIPEnabled && birdRunning {
+		summary.RIPRunning = true
+		nbrs := m.GetRIPNeighbors()
+		summary.RIPNeighborCount = len(nbrs)
+	}
+
+	if m.config.BFDInstance.Enabled && !m.config.BFDInstance.Disabled {
+		summary.BFDEnabled = true
+	}
+	if summary.BFDEnabled && birdRunning {
+		summary.BFDRunning = true
+		sessions := m.GetBFDSessions()
+		summary.BFDSessionCount = len(sessions)
+	}
+
+	for _, inst := range m.config.BabelInstances {
+		if inst.Enabled && !inst.Disabled {
+			summary.BabelEnabled = true
+			break
+		}
+	}
+	if summary.BabelEnabled && birdRunning {
+		summary.BabelRunning = true
+		nbrs := m.GetBabelNeighbors()
+		summary.BabelNeighborCount = len(nbrs)
+	}
+
+	for _, inst := range m.config.RAdvInstances {
+		if inst.Enabled && !inst.Disabled {
+			summary.RAdvEnabled = true
+			break
+		}
+	}
+	if summary.RAdvEnabled && birdRunning {
+		summary.RAdvRunning = true
 	}
 
 	return summary

+ 171 - 0
internal/dynamicrouting/protocols_test.go

@@ -0,0 +1,171 @@
+package dynamicrouting
+
+import (
+	"os"
+	"os/exec"
+	"strings"
+	"testing"
+)
+
+func TestAllProtocolsBirdConfigGeneration(t *testing.T) {
+	mgr := &Manager{
+		config: DynamicConfig{
+			BGPInstance: BGPInstance{
+				Enabled:  true,
+				AS:       65001,
+				RouterID: "10.42.62.254",
+			},
+			BGPPeers: []BGPPeer{
+				{
+					ID:            "peer-1",
+					Name:          "Uplink",
+					RemoteAddress: "10.42.36.253",
+					RemoteAS:      65002,
+					UseBFD:        true,
+				},
+			},
+			OSPFInstances: []OSPFInstance{
+				{
+					ID:       "ospf-1",
+					Name:     "Default",
+					Version:  2,
+					RouterID: "10.42.62.254",
+					Enabled:  true,
+				},
+			},
+			OSPFAreas: []OSPFArea{
+				{
+					ID:       "area-0",
+					Name:     "Default",
+					AreaID:   "0.0.0.0",
+					AreaType: "default",
+				},
+			},
+			OSPFInterfaces: []OSPFInterface{
+				{
+					ID:         "ospf-if-1",
+					Interfaces: "eth0",
+					Area:       "Default",
+					Cost:       10,
+					UseBFD:     true,
+				},
+			},
+			BFDInstance: BFDInstance{
+				Enabled: true,
+			},
+			BFDInterfaces: []BFDInterface{
+				{
+					ID:            "bfd-if-1",
+					Interface:     "eth0",
+					MinRxInterval: 100,
+					MinTxInterval: 100,
+					Multiplier:    3,
+				},
+			},
+			BFDNeighbors: []BFDNeighbor{
+				{
+					ID:        "bfd-nbr-1",
+					Address:   "10.42.36.253",
+					Interface: "eth0",
+				},
+			},
+			RIPInstances: []RIPInstance{
+				{
+					ID:                    "rip-1",
+					Name:                  "default",
+					Version:               2,
+					Enabled:               true,
+					RedistributeConnected: true,
+					RedistributeStatic:    true,
+				},
+			},
+			RIPInterfaces: []RIPInterface{
+				{
+					ID:        "rip-if-1",
+					Instance:  "default",
+					Interface: "eth0",
+					Metric:    1,
+					Mode:      "multicast",
+					UseBFD:    true,
+				},
+			},
+			BabelInstances: []BabelInstance{
+				{
+					ID:                    "babel-1",
+					Name:                  "default",
+					RouterID:              "10.42.62.254",
+					Enabled:               true,
+					RedistributeConnected: true,
+				},
+			},
+			BabelInterfaces: []BabelInterface{
+				{
+					ID:             "babel-if-1",
+					Instance:       "default",
+					Interface:      "eth0",
+					Type:           "wired",
+					RxCost:         10,
+					HelloInterval:  4,
+					UpdateInterval: 16,
+				},
+			},
+			RAdvInstances: []RAdvInstance{
+				{
+					ID:      "radv-1",
+					Name:    "default",
+					Enabled: true,
+				},
+			},
+			RAdvInterfaces: []RAdvInterface{
+				{
+					ID:              "radv-if-1",
+					Instance:        "default",
+					Interface:       "eth0",
+					MinRAInterval:   200,
+					MaxRAInterval:   600,
+					DefaultLifetime: 1800,
+					Prefixes:        []string{"2001:db8:1::/64"},
+					RDNSS:           []string{"2001:4860:4860::8888"},
+				},
+			},
+		},
+	}
+
+	cfg := mgr.generateBirdConfig()
+
+	// Verify all protocol blocks are generated
+	if !strings.Contains(cfg, "protocol bfd bfd_main") {
+		t.Errorf("Expected protocol bfd bfd_main in config, got:\n%s", cfg)
+	}
+	if !strings.Contains(cfg, "protocol rip rip_default") {
+		t.Errorf("Expected protocol rip in config, got:\n%s", cfg)
+	}
+	if !strings.Contains(cfg, "protocol babel babel_default") {
+		t.Errorf("Expected protocol babel in config, got:\n%s", cfg)
+	}
+	if !strings.Contains(cfg, "protocol radv radv_default") {
+		t.Errorf("Expected protocol radv in config, got:\n%s", cfg)
+	}
+	if !strings.Contains(cfg, "bfd yes;") {
+		t.Errorf("Expected bfd yes; in peer and interface, got:\n%s", cfg)
+	}
+
+	// Validate generated config directly with BIRD 2 compiler if bird binary is available
+	if _, err := exec.LookPath("bird"); err == nil {
+		tmpFile, err := os.CreateTemp("", "bird_val_*.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: %s\nConfig:\n%s", string(out), cfg)
+		}
+	}
+}

+ 205 - 0
internal/dynamicrouting/types.go

@@ -166,6 +166,7 @@ type OSPFInterface struct {
 	Passive       bool   `json:"passive"`
 	Auth          string `json:"auth,omitempty"`       // none, simple, md5
 	AuthKey       string `json:"auth_key,omitempty"`
+	UseBFD        bool   `json:"use_bfd,omitempty"`
 	Disabled      bool   `json:"disabled"`
 	Comment       string `json:"comment,omitempty"`
 
@@ -288,6 +289,189 @@ type RoutingFilterASList struct {
 	Comment  string `json:"comment,omitempty"`
 }
 
+// RIPInstance represents a RIP routing instance (/routing rip instance)
+type RIPInstance struct {
+	ID                    string `json:"id"`
+	Name                  string `json:"name"`                   // e.g. "default"
+	Version               int    `json:"version"`                // 2 for IPv4, 1 for RIPng IPv6 (default 2)
+	VRF                   string `json:"vrf,omitempty"`          // default "main"
+	RoutingTable          string `json:"routing_table,omitempty"`// default "main"
+	Enabled               bool   `json:"enabled"`
+	Disabled              bool   `json:"disabled"`
+	Comment               string `json:"comment,omitempty"`
+	RedistributeConnected bool   `json:"redistribute_connected"`
+	RedistributeStatic    bool   `json:"redistribute_static"`
+	RedistributeOSPF      bool   `json:"redistribute_ospf"`
+	RedistributeBGP       bool   `json:"redistribute_bgp"`
+	RedistributeDefault   bool   `json:"redistribute_default"`
+	DefaultMetric         int    `json:"default_metric"`         // default 1
+	UpdateTime            int    `json:"update_time"`            // default 30
+	TimeoutTime           int    `json:"timeout_time"`           // default 180
+	GarbageTime           int    `json:"garbage_time"`           // default 120
+	Running               bool   `json:"running"`
+}
+
+// RIPInterface represents a RIP interface template (/routing rip interface)
+type RIPInterface struct {
+	ID        string `json:"id"`
+	Instance  string `json:"instance,omitempty"`
+	Interface string `json:"interface"`          // e.g. "eth0" or "*"
+	Metric    int    `json:"metric"`             // Metric/cost added (default 1)
+	Mode      string `json:"mode,omitempty"`     // "multicast", "broadcast" (default multicast)
+	Passive   bool   `json:"passive"`
+	UseBFD    bool   `json:"use_bfd,omitempty"`
+	Auth      string `json:"auth,omitempty"`     // "none", "simple", "md5"
+	AuthKey   string `json:"auth_key,omitempty"`
+	Disabled  bool   `json:"disabled"`
+	Comment   string `json:"comment,omitempty"`
+}
+
+// RIPNeighbor represents an explicit RIP neighbor (/routing rip neighbor)
+type RIPNeighbor struct {
+	ID        string `json:"id"`
+	Instance  string `json:"instance,omitempty"`
+	Address   string `json:"address"`            // Neighbor IP
+	Interface string `json:"interface,omitempty"`
+	Disabled  bool   `json:"disabled"`
+	Comment   string `json:"comment,omitempty"`
+}
+
+// RIPNeighborLive represents a live RIP neighbor detected by BIRD
+type RIPNeighborLive struct {
+	Address   string `json:"address"`
+	Interface string `json:"interface"`
+	Uptime    string `json:"uptime"`
+	LastSeen  string `json:"last_seen"`
+}
+
+// RIPRoute represents an active RIP route learned or advertised
+type RIPRoute struct {
+	Prefix    string `json:"prefix"`
+	NextHop   string `json:"next_hop"`
+	Metric    int    `json:"metric"`
+	Interface string `json:"interface"`
+	Uptime    string `json:"uptime"`
+}
+
+// BFDInstance represents global BFD settings (/routing bfd instance)
+type BFDInstance struct {
+	Enabled  bool   `json:"enabled"`
+	Disabled bool   `json:"disabled"`
+	Comment  string `json:"comment,omitempty"`
+	Running  bool   `json:"running"`
+}
+
+// BFDInterface represents a BFD interface template (/routing bfd interface)
+type BFDInterface struct {
+	ID             string `json:"id"`
+	Interface      string `json:"interface"`                 // e.g. "eth0" or "*"
+	MinRxInterval  int    `json:"min_rx_interval"`           // ms, default 100
+	MinTxInterval  int    `json:"min_tx_interval"`           // ms, default 100
+	IdleTxInterval int    `json:"idle_tx_interval,omitempty"`// ms, default 1000
+	Multiplier     int    `json:"multiplier"`                // default 3
+	Passive        bool   `json:"passive"`
+	Auth           string `json:"auth,omitempty"`            // "none", "simple", "meticulous keyed sha1"
+	AuthKey        string `json:"auth_key,omitempty"`
+	Disabled       bool   `json:"disabled"`
+	Comment        string `json:"comment,omitempty"`
+}
+
+// BFDNeighbor represents an explicit BFD neighbor / peer (/routing bfd peer)
+type BFDNeighbor struct {
+	ID        string `json:"id"`
+	Address   string `json:"address"`
+	Interface string `json:"interface,omitempty"`
+	Multihop  bool   `json:"multihop"`
+	Disabled  bool   `json:"disabled"`
+	Comment   string `json:"comment,omitempty"`
+}
+
+// BFDSession represents a live BFD session (/routing bfd session)
+type BFDSession struct {
+	IP        string `json:"ip"`
+	Interface string `json:"interface"`
+	State     string `json:"state"`     // "Up", "Down", "Init", "AdminDown"
+	Since     string `json:"since"`
+	Interval  string `json:"interval"`
+	Timeout   string `json:"timeout"`
+}
+
+// BabelInstance represents a Babel instance (/routing babel instance)
+type BabelInstance struct {
+	ID                    string `json:"id"`
+	Name                  string `json:"name"` // "default"
+	RouterID              string `json:"router_id,omitempty"`
+	VRF                   string `json:"vrf,omitempty"`
+	RoutingTable          string `json:"routing_table,omitempty"`
+	Enabled               bool   `json:"enabled"`
+	Disabled              bool   `json:"disabled"`
+	Comment               string `json:"comment,omitempty"`
+	RedistributeConnected bool   `json:"redistribute_connected"`
+	RedistributeStatic    bool   `json:"redistribute_static"`
+	RedistributeOSPF      bool   `json:"redistribute_ospf"`
+	RedistributeBGP       bool   `json:"redistribute_bgp"`
+	RedistributeDefault   bool   `json:"redistribute_default"`
+	DefaultMetric         int    `json:"default_metric"`
+	Running               bool   `json:"running"`
+}
+
+// BabelInterface represents a Babel interface template (/routing babel interface)
+type BabelInterface struct {
+	ID             string `json:"id"`
+	Instance       string `json:"instance,omitempty"`
+	Interface      string `json:"interface"`           // "eth0" or "*"
+	Type           string `json:"type,omitempty"`      // "wired", "wireless" (default "wired")
+	RxCost         int    `json:"rxcost,omitempty"`    // default 10 or 96
+	HelloInterval  int    `json:"hello_interval,omitempty"` // default 4 s
+	UpdateInterval int    `json:"update_interval,omitempty"` // default 16 s
+	Auth           string `json:"auth,omitempty"`      // "none", "mac"
+	AuthKey        string `json:"auth_key,omitempty"`
+	Disabled       bool   `json:"disabled"`
+	Comment        string `json:"comment,omitempty"`
+}
+
+// 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"`
+}
+
+// BabelRoute represents an active Babel route (/routing babel route)
+type BabelRoute struct {
+	Prefix    string `json:"prefix"`
+	NextHop   string `json:"next_hop"`
+	Metric    int    `json:"metric"`
+	Interface string `json:"interface"`
+}
+
+// RAdvInstance represents an IPv6 Router Advertisement instance (/routing radv instance)
+type RAdvInstance struct {
+	ID       string `json:"id"`
+	Name     string `json:"name"` // "default"
+	Enabled  bool   `json:"enabled"`
+	Disabled bool   `json:"disabled"`
+	Comment  string `json:"comment,omitempty"`
+	Running  bool   `json:"running"`
+}
+
+// RAdvInterface represents a RAdv interface (/routing radv interface)
+type RAdvInterface struct {
+	ID              string   `json:"id"`
+	Instance        string   `json:"instance,omitempty"`
+	Interface       string   `json:"interface"`
+	MinRAInterval   int      `json:"min_ra_interval,omitempty"`   // default 200 s
+	MaxRAInterval   int      `json:"max_ra_interval,omitempty"`   // default 600 s
+	DefaultLifetime int      `json:"default_lifetime,omitempty"` // default 1800 s
+	HopLimit        int      `json:"hop_limit,omitempty"`        // default 64
+	Prefixes        []string `json:"prefixes,omitempty"`         // IPv6 prefixes e.g. ["2001:db8::/64"]
+	RDNSS           []string `json:"rdnss,omitempty"`            // IPv6 DNS servers e.g. ["2001:4860:4860::8888"]
+	Disabled        bool     `json:"disabled"`
+	Comment         string   `json:"comment,omitempty"`
+}
+
 // DynamicRoutingSummary aggregates status for dashboard & badges
 type DynamicRoutingSummary struct {
 	BGPEnabled          bool `json:"bgp_enabled"`
@@ -298,6 +482,17 @@ type DynamicRoutingSummary struct {
 	OSPFRunning         bool `json:"ospf_running"`
 	OSPFNeighborCount   int  `json:"ospf_neighbor_count"`
 	OSPFAdjacentCount   int  `json:"ospf_adjacent_count"`
+	RIPEnabled          bool `json:"rip_enabled"`
+	RIPRunning          bool `json:"rip_running"`
+	RIPNeighborCount    int  `json:"rip_neighbor_count"`
+	BFDEnabled          bool `json:"bfd_enabled"`
+	BFDRunning          bool `json:"bfd_running"`
+	BFDSessionCount     int  `json:"bfd_session_count"`
+	BabelEnabled        bool `json:"babel_enabled"`
+	BabelRunning        bool `json:"babel_running"`
+	BabelNeighborCount  int  `json:"babel_neighbor_count"`
+	RAdvEnabled         bool `json:"radv_enabled"`
+	RAdvRunning         bool `json:"radv_running"`
 	RoutingFilterCount  int  `json:"routing_filter_count"`
 }
 
@@ -312,6 +507,16 @@ type DynamicConfig struct {
 	OSPFAreaRanges        []OSPFAreaRange              `json:"ospf_area_ranges"`
 	OSPFInterfaces        []OSPFInterface              `json:"ospf_interfaces"`
 	OSPFStaticNbrs        []OSPFStaticNeighbor         `json:"ospf_static_nbrs"`
+	RIPInstances          []RIPInstance                `json:"rip_instances"`
+	RIPInterfaces         []RIPInterface               `json:"rip_interfaces"`
+	RIPNeighbors          []RIPNeighbor                `json:"rip_neighbors"`
+	BFDInstance           BFDInstance                  `json:"bfd_instance"`
+	BFDInterfaces         []BFDInterface               `json:"bfd_interfaces"`
+	BFDNeighbors          []BFDNeighbor                `json:"bfd_neighbors"`
+	BabelInstances        []BabelInstance              `json:"babel_instances"`
+	BabelInterfaces       []BabelInterface             `json:"babel_interfaces"`
+	RAdvInstances         []RAdvInstance               `json:"radv_instances"`
+	RAdvInterfaces        []RAdvInterface              `json:"radv_interfaces"`
 	RoutingFilters        []RoutingFilterRule          `json:"routing_filters"`
 	RoutingSelectRules    []RoutingFilterSelectRule    `json:"routing_select_rules,omitempty"` // Deprecated: unused in BIRD 2
 	RoutingCommunityLists []RoutingFilterCommunityList `json:"routing_community_lists"`

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio