package api import ( "encoding/json" "net/http" "strings" "proxmox-router/internal/firewall" "proxmox-router/internal/network" "proxmox-router/internal/routing" "proxmox-router/internal/system" "proxmox-router/internal/vpn" ) type Server struct { netMgr *network.Manager persistMgr *network.PersistenceManager metaMgr *network.MetadataManager vpnMgr *vpn.Manager fwMgr *firewall.FirewallManager routingMgr *routing.RoutingManager mux *http.ServeMux fsHandler http.Handler } func NewServer(netMgr *network.Manager, persistMgr *network.PersistenceManager, metaMgr *network.MetadataManager, vpnMgr *vpn.Manager, fwMgr *firewall.FirewallManager, routingMgr *routing.RoutingManager, fsHandler http.Handler) *Server { s := &Server{ netMgr: netMgr, persistMgr: persistMgr, metaMgr: metaMgr, vpnMgr: vpnMgr, fwMgr: fwMgr, routingMgr: routingMgr, mux: http.NewServeMux(), fsHandler: fsHandler, } s.registerRoutes() return s } func (s *Server) registerRoutes() { // API Endpoints s.mux.HandleFunc("GET /api/system/status", s.handleSystemStatus) // Routing & Policy Rules (/ip route & /ip route rule) s.mux.HandleFunc("GET /api/routes", s.handleListRoutes) s.mux.HandleFunc("POST /api/routes", s.handleSaveRoute) s.mux.HandleFunc("DELETE /api/routes/{id}", s.handleDeleteRoute) s.mux.HandleFunc("POST /api/routes/{id}/state", s.handleSetRouteState) s.mux.HandleFunc("GET /api/routing/rules", s.handleListPolicyRules) s.mux.HandleFunc("POST /api/routing/rules", s.handleSavePolicyRule) s.mux.HandleFunc("DELETE /api/routing/rules/{id}", s.handleDeletePolicyRule) s.mux.HandleFunc("GET /api/routing/tables", s.handleListRoutingTables) s.mux.HandleFunc("POST /api/routing/tables", s.handleSaveRoutingTable) // Firewall Endpoints (/ip firewall) s.mux.HandleFunc("GET /api/firewall/summary", s.handleFirewallSummary) s.mux.HandleFunc("POST /api/firewall/reset-counters", s.handleResetFirewallCounters) // Filter Rules s.mux.HandleFunc("GET /api/firewall/filter", s.handleListFilterRules) s.mux.HandleFunc("POST /api/firewall/filter", s.handleSaveFilterRule) s.mux.HandleFunc("DELETE /api/firewall/filter/{id}", s.handleDeleteFilterRule) s.mux.HandleFunc("POST /api/firewall/filter/{id}/move", s.handleMoveFilterRule) s.mux.HandleFunc("POST /api/firewall/filter/{id}/state", s.handleSetFilterRuleState) // NAT Rules s.mux.HandleFunc("GET /api/firewall/nat", s.handleListNATRules) s.mux.HandleFunc("POST /api/firewall/nat", s.handleSaveNATRule) s.mux.HandleFunc("DELETE /api/firewall/nat/{id}", s.handleDeleteNATRule) s.mux.HandleFunc("POST /api/firewall/nat/{id}/move", s.handleMoveNATRule) s.mux.HandleFunc("POST /api/firewall/nat/{id}/state", s.handleSetNATRuleState) // Mangle Rules s.mux.HandleFunc("GET /api/firewall/mangle", s.handleListMangleRules) s.mux.HandleFunc("POST /api/firewall/mangle", s.handleSaveMangleRule) s.mux.HandleFunc("DELETE /api/firewall/mangle/{id}", s.handleDeleteMangleRule) s.mux.HandleFunc("POST /api/firewall/mangle/{id}/move", s.handleMoveMangleRule) s.mux.HandleFunc("POST /api/firewall/mangle/{id}/state", s.handleSetMangleRuleState) // Address Lists s.mux.HandleFunc("GET /api/firewall/address-lists", s.handleListAddressLists) s.mux.HandleFunc("POST /api/firewall/address-lists", s.handleSaveAddressListEntry) s.mux.HandleFunc("DELETE /api/firewall/address-lists/{id}", s.handleDeleteAddressListEntry) // Active Connections s.mux.HandleFunc("GET /api/firewall/connections", s.handleListConnections) s.mux.HandleFunc("POST /api/firewall/connections/kill", s.handleKillConnection) s.mux.HandleFunc("GET /api/interfaces", s.handleListInterfaces) s.mux.HandleFunc("GET /api/interfaces/{name}", s.handleGetInterface) s.mux.HandleFunc("POST /api/interfaces/{name}/state", s.handleSetInterfaceState) s.mux.HandleFunc("POST /api/interfaces/{name}/mtu", s.handleSetInterfaceMTU) s.mux.HandleFunc("POST /api/interfaces/{name}/ip", s.handleAddInterfaceIP) s.mux.HandleFunc("DELETE /api/interfaces/{name}/ip", s.handleDeleteInterfaceIP) s.mux.HandleFunc("POST /api/interfaces/{name}/metadata", s.handleSetInterfaceMetadata) s.mux.HandleFunc("GET /api/vlans", s.handleListVLANs) s.mux.HandleFunc("POST /api/vlans", s.handleCreateVLAN) s.mux.HandleFunc("DELETE /api/vlans/{name}", s.handleDeleteVLAN) s.mux.HandleFunc("GET /api/debian/config", s.handleGetDebianConfig) s.mux.HandleFunc("POST /api/debian/apply", s.handleSaveDebianConfig) // VPN & WAN Endpoints s.mux.HandleFunc("GET /api/vpn/status", s.handleOverallVPNStatus) // PPPoE Multi-Instance s.mux.HandleFunc("GET /api/vpn/pppoe/clients", s.handleListPPPoE) s.mux.HandleFunc("POST /api/vpn/pppoe/clients", s.handleSavePPPoE) s.mux.HandleFunc("DELETE /api/vpn/pppoe/clients/{name}", s.handleDeletePPPoE) s.mux.HandleFunc("POST /api/vpn/pppoe/clients/{name}/state", s.handleSetPPPoEState) s.mux.HandleFunc("POST /api/vpn/pppoe/clients/{name}/connect", s.handleConnectPPPoE) s.mux.HandleFunc("POST /api/vpn/pppoe/clients/{name}/disconnect", s.handleDisconnectPPPoE) // WireGuard Multi-Interface & Multi-Peer s.mux.HandleFunc("GET /api/vpn/wireguard/interfaces", s.handleListWireGuardInterfaces) s.mux.HandleFunc("POST /api/vpn/wireguard/interfaces", s.handleSaveWireGuardInterface) s.mux.HandleFunc("DELETE /api/vpn/wireguard/interfaces/{name}", s.handleDeleteWireGuardInterface) s.mux.HandleFunc("POST /api/vpn/wireguard/interfaces/{name}/state", s.handleSetWireGuardInterfaceState) s.mux.HandleFunc("POST /api/vpn/wireguard/interfaces/{name}/start", s.handleStartWireGuardInterface) s.mux.HandleFunc("POST /api/vpn/wireguard/interfaces/{name}/stop", s.handleStopWireGuardInterface) s.mux.HandleFunc("GET /api/vpn/wireguard/peers", s.handleListWireGuardPeers) s.mux.HandleFunc("POST /api/vpn/wireguard/peers", s.handleSaveWireGuardPeer) s.mux.HandleFunc("DELETE /api/vpn/wireguard/peers/{iface}/{pubkey}", s.handleDeleteWireGuardPeer) s.mux.HandleFunc("POST /api/vpn/wireguard/genkeys", s.handleWireGuardGenKeys) // PPTP Multi-Instance s.mux.HandleFunc("GET /api/vpn/pptp/clients", s.handleListPPTP) s.mux.HandleFunc("POST /api/vpn/pptp/clients", s.handleSavePPTP) s.mux.HandleFunc("DELETE /api/vpn/pptp/clients/{name}", s.handleDeletePPTP) s.mux.HandleFunc("POST /api/vpn/pptp/clients/{name}/state", s.handleSetPPTPState) s.mux.HandleFunc("POST /api/vpn/pptp/clients/{name}/connect", s.handleConnectPPTP) s.mux.HandleFunc("POST /api/vpn/pptp/clients/{name}/disconnect", s.handleDisconnectPPTP) // L2TP Multi-Instance s.mux.HandleFunc("GET /api/vpn/l2tp/clients", s.handleListL2TP) s.mux.HandleFunc("POST /api/vpn/l2tp/clients", s.handleSaveL2TP) s.mux.HandleFunc("DELETE /api/vpn/l2tp/clients/{name}", s.handleDeleteL2TP) s.mux.HandleFunc("POST /api/vpn/l2tp/clients/{name}/state", s.handleSetL2TPState) s.mux.HandleFunc("POST /api/vpn/l2tp/clients/{name}/connect", s.handleConnectL2TP) s.mux.HandleFunc("POST /api/vpn/l2tp/clients/{name}/disconnect", s.handleDisconnectL2TP) // IPSec Multi-Tunnel s.mux.HandleFunc("GET /api/vpn/ipsec/tunnels", s.handleListIPSec) s.mux.HandleFunc("POST /api/vpn/ipsec/tunnels", s.handleSaveIPSecTunnel) s.mux.HandleFunc("DELETE /api/vpn/ipsec/tunnels/{name}", s.handleDeleteIPSecTunnel) s.mux.HandleFunc("POST /api/vpn/ipsec/tunnels/{name}/state", s.handleSetIPSecState) s.mux.HandleFunc("POST /api/vpn/ipsec/tunnels/{name}/start", s.handleInitiateIPSecTunnel) s.mux.HandleFunc("POST /api/vpn/ipsec/tunnels/{name}/stop", s.handleTerminateIPSecTunnel) // Web UI Static Files if s.fsHandler != nil { s.mux.Handle("/", s.fsHandler) } } func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Enable CORS for local development/testing w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS, PUT") w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") if r.Method == http.MethodOptions { w.WriteHeader(http.StatusOK) return } s.mux.ServeHTTP(w, r) } func (s *Server) handleSystemStatus(w http.ResponseWriter, r *http.Request) { status, err := system.GetStatus() if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } ifaces, err := s.netMgr.ListInterfaces() if err == nil { status.TotalIfaces = len(ifaces) for _, iface := range ifaces { if iface.State == "UP" { status.UpIfaces++ } else { status.DownIfaces++ } if iface.IsVLAN { status.VlanIfaces++ } } } writeJSON(w, http.StatusOK, status) } func (s *Server) handleRoutes(w http.ResponseWriter, r *http.Request) { routes, err := system.GetRoutingTable() if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, routes) } func (s *Server) handleListInterfaces(w http.ResponseWriter, r *http.Request) { ifaces, err := s.netMgr.ListInterfaces() if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, ifaces) } func (s *Server) handleGetInterface(w http.ResponseWriter, r *http.Request) { name := r.PathValue("name") iface, err := s.netMgr.GetInterface(name) if err != nil { writeError(w, http.StatusNotFound, err.Error()) return } writeJSON(w, http.StatusOK, iface) } func (s *Server) handleSetInterfaceState(w http.ResponseWriter, r *http.Request) { name := r.PathValue("name") var req struct { State string `json:"state"` // "up" or "down" } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid request body") return } up := strings.ToLower(req.State) == "up" if err := s.netMgr.SetInterfaceState(name, up); err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, map[string]any{"success": true, "name": name, "state": req.State}) } func (s *Server) handleSetInterfaceMTU(w http.ResponseWriter, r *http.Request) { name := r.PathValue("name") var req struct { MTU int `json:"mtu"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid request body") return } if err := s.netMgr.SetInterfaceMTU(name, req.MTU); err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, map[string]any{"success": true, "name": name, "mtu": req.MTU}) } func (s *Server) handleAddInterfaceIP(w http.ResponseWriter, r *http.Request) { name := r.PathValue("name") var req network.AddIPRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.IP == "" { writeError(w, http.StatusBadRequest, "missing or invalid 'ip' parameter") return } if err := s.netMgr.AddInterfaceIP(name, req.IP); err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, map[string]any{"success": true, "name": name, "ip": req.IP}) } func (s *Server) handleDeleteInterfaceIP(w http.ResponseWriter, r *http.Request) { name := r.PathValue("name") var req network.AddIPRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.IP == "" { writeError(w, http.StatusBadRequest, "missing or invalid 'ip' parameter") return } if err := s.netMgr.DeleteInterfaceIP(name, req.IP); err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, map[string]any{"success": true, "name": name, "ip": req.IP}) } func (s *Server) handleSetInterfaceMetadata(w http.ResponseWriter, r *http.Request) { name := r.PathValue("name") var req struct { Role string `json:"role"` Description string `json:"description"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid request body") return } if err := s.metaMgr.Set(name, req.Role, req.Description); err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, map[string]any{"success": true, "name": name, "role": req.Role, "description": req.Description}) } func (s *Server) handleListVLANs(w http.ResponseWriter, r *http.Request) { vlans, err := s.netMgr.ListVLANs() if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, vlans) } func (s *Server) handleCreateVLAN(w http.ResponseWriter, r *http.Request) { var req network.CreateVLANRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid request body") return } if req.ParentName == "" || req.VlanID <= 0 { writeError(w, http.StatusBadRequest, "parent_name and valid vlan_id (1-4094) are required") return } iface, err := s.netMgr.CreateVLAN(req) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusCreated, iface) } func (s *Server) handleDeleteVLAN(w http.ResponseWriter, r *http.Request) { name := r.PathValue("name") if err := s.netMgr.DeleteVLAN(name); err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, map[string]any{"success": true, "name": name}) } func (s *Server) handleGetDebianConfig(w http.ResponseWriter, r *http.Request) { cfg, err := s.persistMgr.GenerateDebianConfig() if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, map[string]string{ "path": network.DebianVlansCfgPath, "content": cfg, }) } func (s *Server) handleSaveDebianConfig(w http.ResponseWriter, r *http.Request) { cfg, err := s.persistMgr.SaveDebianConfig() if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, map[string]any{ "success": true, "path": network.DebianVlansCfgPath, "content": cfg, "message": "Configuration saved to " + network.DebianVlansCfgPath, }) } func writeJSON(w http.ResponseWriter, status int, data any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(data) } func writeError(w http.ResponseWriter, status int, message string) { writeJSON(w, status, map[string]any{ "error": true, "message": message, }) }