package api import ( "context" "encoding/json" "fmt" "net/http" "strings" "sync" "time" "proxmox-router/internal/auth" "proxmox-router/internal/bridge" "proxmox-router/internal/bwtest" "proxmox-router/internal/certs" "proxmox-router/internal/dhcp" "proxmox-router/internal/dns" "proxmox-router/internal/dynamicrouting" "proxmox-router/internal/firewall" "proxmox-router/internal/network" "proxmox-router/internal/routing" "proxmox-router/internal/services" "proxmox-router/internal/system" "proxmox-router/internal/tools" "proxmox-router/internal/vpn" "proxmox-router/internal/vrrp" ) type Server struct { aeroDraftMu sync.Mutex aeroDrafts map[string]aeroIdentityDraft netMgr *network.Manager persistMgr *network.PersistenceManager metaMgr *network.MetadataManager vpnMgr *vpn.Manager fwMgr *firewall.FirewallManager routingMgr *routing.RoutingManager dynRoutingMgr *dynamicrouting.Manager bridgeMgr *bridge.BridgeManager toolsMgr *tools.Manager dhcpMgr *dhcp.Manager dhcpServerMgr *dhcp.ServerManager vrrpMgr *vrrp.Manager bwtestMgr *bwtest.Manager dnsMgr *dns.Manager authMgr *auth.Manager servicesMgr *services.Manager certMgr *certs.CertManager 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, dynRoutingMgr *dynamicrouting.Manager, bridgeMgr *bridge.BridgeManager, toolsMgr *tools.Manager, dhcpMgr *dhcp.Manager, dhcpServerMgr *dhcp.ServerManager, vrrpMgr *vrrp.Manager, bwtestMgr *bwtest.Manager, dnsMgr *dns.Manager, authMgr *auth.Manager, servicesMgr *services.Manager, certMgr *certs.CertManager, fsHandler http.Handler) *Server { s := &Server{ netMgr: netMgr, persistMgr: persistMgr, metaMgr: metaMgr, vpnMgr: vpnMgr, fwMgr: fwMgr, routingMgr: routingMgr, dynRoutingMgr: dynRoutingMgr, bridgeMgr: bridgeMgr, toolsMgr: toolsMgr, dhcpMgr: dhcpMgr, dhcpServerMgr: dhcpServerMgr, vrrpMgr: vrrpMgr, bwtestMgr: bwtestMgr, dnsMgr: dnsMgr, authMgr: authMgr, servicesMgr: servicesMgr, certMgr: certMgr, mux: http.NewServeMux(), fsHandler: fsHandler, } s.registerRoutes() return s } func (s *Server) registerRoutes() { s.mux.HandleFunc("GET /api/update/status", s.handleUpdateStatus) s.mux.HandleFunc("POST /api/update/config", s.handleUpdateConfig) s.mux.HandleFunc("POST /api/update/check", s.handleUpdateCheck) s.mux.HandleFunc("POST /api/update/{action}", s.handleUpdateAction) // 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/capabilities", s.handleFirewallCapabilities) s.mux.HandleFunc("POST /api/firewall/{kind}/{id}/reset-counters", s.handleResetRuleCounters) 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) s.mux.HandleFunc("POST /api/firewall/address-lists/{id}/state", s.handleSetAddressListEntryState) // Geo-Location s.mux.HandleFunc("GET /api/firewall/geoip/status", s.handleGeoIPStatus) s.mux.HandleFunc("POST /api/firewall/geoip/config", s.handleGeoIPConfig) s.mux.HandleFunc("GET /api/firewall/geoip/countries", s.handleListGeoCountries) s.mux.HandleFunc("POST /api/firewall/geoip/countries/{code}/toggle", s.handleToggleGeoCountry) s.mux.HandleFunc("POST /api/firewall/geoip/countries/{code}/update", s.handleUpdateGeoCountry) s.mux.HandleFunc("POST /api/firewall/geoip/update-now", s.handleGeoIPUpdateNow) s.mux.HandleFunc("POST /api/firewall/geoip/batch-toggle", s.handleGeoIPBatchToggle) s.mux.HandleFunc("GET /api/firewall/geoip/countries/{code}/subnets", s.handleGeoCountrySubnets) // 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/traffic/stream", s.handleInterfacesTrafficStream) s.mux.HandleFunc("GET /api/interfaces/traffic/rates", s.handleInterfacesTrafficRates) s.mux.HandleFunc("GET /api/interfaces/rates", s.handleInterfacesTrafficRates) s.mux.HandleFunc("GET /api/roles", s.handleListRoleTags) s.mux.HandleFunc("POST /api/roles", s.handleSaveRoleTag) s.mux.HandleFunc("PUT /api/roles/{id}", s.handleSaveRoleTag) s.mux.HandleFunc("DELETE /api/roles/{id}", s.handleDeleteRoleTag) s.mux.HandleFunc("GET /api/interfaces/roles", s.handleListRoleTags) 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("POST /api/interfaces/{name}/rename", s.handleRenameInterface) 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) // Bridge Management (/interface bridge) s.mux.HandleFunc("GET /api/bridge/summary", s.handleBridgeSummary) s.mux.HandleFunc("GET /api/bridge/bridges", s.handleListBridges) s.mux.HandleFunc("POST /api/bridge/bridges", s.handleSaveBridge) s.mux.HandleFunc("DELETE /api/bridge/bridges/{id}", s.handleDeleteBridge) s.mux.HandleFunc("POST /api/bridge/bridges/{id}/state", s.handleSetBridgeState) s.mux.HandleFunc("POST /api/bridge/bridges/{id}/ip", s.handleAddBridgeIP) s.mux.HandleFunc("DELETE /api/bridge/bridges/{id}/ip", s.handleDeleteBridgeIP) s.mux.HandleFunc("GET /api/bridge/ports", s.handleListBridgePorts) s.mux.HandleFunc("POST /api/bridge/ports", s.handleSaveBridgePort) s.mux.HandleFunc("DELETE /api/bridge/ports/{id}", s.handleDeleteBridgePort) s.mux.HandleFunc("POST /api/bridge/ports/{id}/state", s.handleSetBridgePortState) s.mux.HandleFunc("GET /api/bridge/vlans", s.handleListBridgeVLANs) s.mux.HandleFunc("POST /api/bridge/vlans", s.handleSaveBridgeVLAN) s.mux.HandleFunc("DELETE /api/bridge/vlans/{id}", s.handleDeleteBridgeVLAN) s.mux.HandleFunc("POST /api/bridge/vlans/{id}/state", s.handleSetBridgeVLANState) s.mux.HandleFunc("GET /api/bridge/hosts", s.handleListBridgeHosts) s.mux.HandleFunc("POST /api/bridge/hosts/flush", s.handleFlushBridgeHosts) 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("POST /api/vpn/wireguard/peers/state", s.handleSetWireGuardPeerState) s.mux.HandleFunc("POST /api/vpn/wireguard/peers/delete", s.handleDeleteWireGuardPeer) s.mux.HandleFunc("DELETE /api/vpn/wireguard/peers", s.handleDeleteWireGuardPeer) s.mux.HandleFunc("DELETE /api/vpn/wireguard/peers/{iface}/{pubkey...}", s.handleDeleteWireGuardPeer) s.mux.HandleFunc("POST /api/vpn/wireguard/genkeys", s.handleWireGuardGenKeys) s.mux.HandleFunc("POST /api/vpn/wireguard/calc-pubkey", s.handleWireGuardCalcPubKey) // 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) // EoIP (Ethernet over IP / GRETAP) s.mux.HandleFunc("GET /api/vpn/eoip", s.handleListEoIP) s.mux.HandleFunc("POST /api/vpn/eoip", s.handleSaveEoIP) s.mux.HandleFunc("DELETE /api/vpn/eoip/{id}", s.handleDeleteEoIP) s.mux.HandleFunc("POST /api/vpn/eoip/{id}/state", s.handleSetEoIPState) // AeroTUN (High-speed ChaCha20-Poly1305 + LZ4 Encrypted UDP Tunnel) s.mux.HandleFunc("GET /api/vpn/aerotun/tunnels", s.handleListAeroTUNTunnels) s.mux.HandleFunc("POST /api/vpn/aerotun/tunnels", s.handleSaveAeroTUNTunnel) s.mux.HandleFunc("DELETE /api/vpn/aerotun/tunnels/{id}", s.handleDeleteAeroTUNTunnel) s.mux.HandleFunc("POST /api/vpn/aerotun/tunnels/{id}/state", s.handleSetAeroTUNTunnelState) s.mux.HandleFunc("POST /api/vpn/aerotun/tunnels/{id}/start", s.handleStartAeroTUNTunnel) s.mux.HandleFunc("POST /api/vpn/aerotun/tunnels/{id}/stop", s.handleStopAeroTUNTunnel) s.mux.HandleFunc("GET /api/vpn/aerotun/genkey", s.handleGenerateAeroTUNKey) s.mux.HandleFunc("POST /api/vpn/aerotun/identity-drafts", s.handleCreateAeroIdentityDraft) s.mux.HandleFunc("DELETE /api/vpn/aerotun/identity-drafts/{id}", s.handleDeleteAeroIdentityDraft) s.mux.HandleFunc("GET /api/vpn/aerotun/stats", s.handleGetAeroTUNStats) // PPP Profiles (/ppp profile) s.mux.HandleFunc("GET /api/vpn/ppp/profiles", s.handleListPPPProfiles) s.mux.HandleFunc("POST /api/vpn/ppp/profiles", s.handleSavePPPProfile) s.mux.HandleFunc("DELETE /api/vpn/ppp/profiles/{name}", s.handleDeletePPPProfile) // PPP Secrets (/ppp secret) s.mux.HandleFunc("GET /api/vpn/ppp/secrets", s.handleListPPPSecrets) s.mux.HandleFunc("POST /api/vpn/ppp/secrets", s.handleSavePPPSecret) s.mux.HandleFunc("DELETE /api/vpn/ppp/secrets/{id}", s.handleDeletePPPSecret) s.mux.HandleFunc("POST /api/vpn/ppp/secrets/{id}/state", s.handleSetPPPSecretState) // Active PPP Sessions (/ppp active) s.mux.HandleFunc("GET /api/vpn/ppp/active", s.handleListPPPSessions) s.mux.HandleFunc("DELETE /api/vpn/ppp/active/{iface}", s.handleDisconnectPPPSession) // VPN Server Settings s.mux.HandleFunc("GET /api/vpn/servers/pppoe", s.handleGetPPPoEServer) s.mux.HandleFunc("POST /api/vpn/servers/pppoe", s.handleSetPPPoEServer) s.mux.HandleFunc("GET /api/vpn/servers/pptp", s.handleGetPPTPServer) s.mux.HandleFunc("POST /api/vpn/servers/pptp", s.handleSetPPTPServer) s.mux.HandleFunc("GET /api/vpn/servers/l2tp", s.handleGetL2TPServer) s.mux.HandleFunc("POST /api/vpn/servers/l2tp", s.handleSetL2TPServer) s.mux.HandleFunc("GET /api/vpn/servers/ipsec", s.handleGetIPSecServer) s.mux.HandleFunc("POST /api/vpn/servers/ipsec", s.handleSetIPSecServer) // WireGuard Client Config Generator s.mux.HandleFunc("POST /api/vpn/wireguard/client-config", s.handleGenerateWGClientConfig) // Dynamic Routing (BGP & OSPF) s.mux.HandleFunc("GET /api/routing/dynamic/summary", s.handleDynamicRoutingSummary) // BGP s.mux.HandleFunc("GET /api/routing/bgp/instance", s.handleGetBGPInstance) s.mux.HandleFunc("POST /api/routing/bgp/instance", s.handleUpdateBGPInstance) s.mux.HandleFunc("GET /api/routing/bgp/peers", s.handleListBGPPeers) s.mux.HandleFunc("POST /api/routing/bgp/peers", s.handleSaveBGPPeer) s.mux.HandleFunc("DELETE /api/routing/bgp/peers/{id}", s.handleDeleteBGPPeer) s.mux.HandleFunc("POST /api/routing/bgp/peers/{id}/state", s.handleSetBGPPeerState) s.mux.HandleFunc("GET /api/routing/bgp/networks", s.handleListBGPNetworks) s.mux.HandleFunc("POST /api/routing/bgp/networks", s.handleSaveBGPNetwork) s.mux.HandleFunc("DELETE /api/routing/bgp/networks/{id}", s.handleDeleteBGPNetwork) s.mux.HandleFunc("GET /api/routing/bgp/routes", s.handleGetBGPRoutes) // OSPF (RouterOS v7 Multi-Instance OSPF) s.mux.HandleFunc("GET /api/routing/ospf/instances", s.handleListOSPFInstances) s.mux.HandleFunc("POST /api/routing/ospf/instances", s.handleSaveOSPFInstance) s.mux.HandleFunc("DELETE /api/routing/ospf/instances/{id}", s.handleDeleteOSPFInstance) s.mux.HandleFunc("POST /api/routing/ospf/instances/{id}/state", s.handleSetOSPFInstanceState) s.mux.HandleFunc("GET /api/routing/ospf/instance", s.handleGetOSPFInstance) s.mux.HandleFunc("POST /api/routing/ospf/instance", s.handleUpdateOSPFInstance) s.mux.HandleFunc("GET /api/routing/ospf/areas", s.handleListOSPFAreas) s.mux.HandleFunc("POST /api/routing/ospf/areas", s.handleSaveOSPFArea) s.mux.HandleFunc("DELETE /api/routing/ospf/areas/{id}", s.handleDeleteOSPFArea) s.mux.HandleFunc("POST /api/routing/ospf/areas/{id}/state", s.handleSetOSPFAreaState) s.mux.HandleFunc("GET /api/routing/ospf/area-ranges", s.handleListOSPFAreaRanges) s.mux.HandleFunc("POST /api/routing/ospf/area-ranges", s.handleSaveOSPFAreaRange) s.mux.HandleFunc("DELETE /api/routing/ospf/area-ranges/{id}", s.handleDeleteOSPFAreaRange) s.mux.HandleFunc("GET /api/routing/ospf/templates", s.handleListOSPFInterfaces) s.mux.HandleFunc("POST /api/routing/ospf/templates", s.handleSaveOSPFInterface) s.mux.HandleFunc("DELETE /api/routing/ospf/templates/{id}", s.handleDeleteOSPFInterface) s.mux.HandleFunc("POST /api/routing/ospf/templates/{id}/state", s.handleSetOSPFInterfaceState) s.mux.HandleFunc("GET /api/routing/ospf/interfaces", s.handleListOSPFInterfaces) s.mux.HandleFunc("POST /api/routing/ospf/interfaces", s.handleSaveOSPFInterface) s.mux.HandleFunc("DELETE /api/routing/ospf/interfaces/{id}", s.handleDeleteOSPFInterface) s.mux.HandleFunc("POST /api/routing/ospf/interfaces/{id}/state", s.handleSetOSPFInterfaceState) s.mux.HandleFunc("GET /api/routing/ospf/interfaces-live", s.handleGetOSPFInterfacesLive) s.mux.HandleFunc("GET /api/routing/ospf/static-neighbors", s.handleListOSPFStaticNeighbors) s.mux.HandleFunc("POST /api/routing/ospf/static-neighbors", s.handleSaveOSPFStaticNeighbor) s.mux.HandleFunc("DELETE /api/routing/ospf/static-neighbors/{id}", s.handleDeleteOSPFStaticNeighbor) s.mux.HandleFunc("GET /api/routing/ospf/neighbors", s.handleGetOSPFNeighbors) s.mux.HandleFunc("GET /api/routing/ospf/lsa", s.handleGetOSPFLSA) s.mux.HandleFunc("GET /api/routing/ospf/routes", s.handleGetOSPFRoutes) // Routing Filters (/routing filter) s.mux.HandleFunc("GET /api/routing/filters", s.handleListRoutingFilters) s.mux.HandleFunc("POST /api/routing/filters", s.handleSaveRoutingFilter) s.mux.HandleFunc("DELETE /api/routing/filters/{id}", s.handleDeleteRoutingFilter) s.mux.HandleFunc("POST /api/routing/filters/{id}/state", s.handleSetRoutingFilterState) s.mux.HandleFunc("GET /api/routing/filters/rules", s.handleListRoutingFilters) s.mux.HandleFunc("POST /api/routing/filters/rules", s.handleSaveRoutingFilter) s.mux.HandleFunc("DELETE /api/routing/filters/rules/{id}", s.handleDeleteRoutingFilter) s.mux.HandleFunc("POST /api/routing/filters/rules/{id}/state", s.handleSetRoutingFilterState) s.mux.HandleFunc("GET /api/routing/filters/select-rules", s.handleListSelectRules) s.mux.HandleFunc("POST /api/routing/filters/select-rules", s.handleSaveSelectRule) s.mux.HandleFunc("DELETE /api/routing/filters/select-rules/{id}", s.handleDeleteSelectRule) s.mux.HandleFunc("POST /api/routing/filters/select-rules/{id}/state", s.handleSetSelectRuleState) s.mux.HandleFunc("GET /api/routing/filters/community-lists", s.handleListCommunityLists) s.mux.HandleFunc("POST /api/routing/filters/community-lists", s.handleSaveCommunityList) s.mux.HandleFunc("DELETE /api/routing/filters/community-lists/{id}", s.handleDeleteCommunityList) s.mux.HandleFunc("POST /api/routing/filters/community-lists/{id}/state", s.handleSetCommunityListState) s.mux.HandleFunc("GET /api/routing/filters/as-lists", s.handleListASLists) s.mux.HandleFunc("POST /api/routing/filters/as-lists", s.handleSaveASList) s.mux.HandleFunc("DELETE /api/routing/filters/as-lists/{id}", s.handleDeleteASList) s.mux.HandleFunc("POST /api/routing/filters/as-lists/{id}/state", s.handleSetASListState) // Troubleshooting Tools (/tool ping, traceroute, mtr, dns, port-probe) s.mux.HandleFunc("POST /api/tools/ping", s.handleToolsPing) s.mux.HandleFunc("POST /api/tools/ping/stream", s.handleToolsPingStream) s.mux.HandleFunc("GET /api/tools/ping/stream", s.handleToolsPingStream) s.mux.HandleFunc("POST /api/tools/traceroute", s.handleToolsTraceroute) s.mux.HandleFunc("POST /api/tools/traceroute/stream", s.handleToolsTracerouteStream) s.mux.HandleFunc("GET /api/tools/traceroute/stream", s.handleToolsTracerouteStream) s.mux.HandleFunc("POST /api/tools/dns", s.handleToolsDNS) s.mux.HandleFunc("POST /api/tools/port-probe", s.handleToolsPortProbe) s.mux.HandleFunc("POST /api/tools/mtr", s.handleToolsMTR) // Bandwidth Test Tool (/tool bandwidth-test / BwTest) s.mux.HandleFunc("GET /api/tools/bwtest/server", s.handleGetBwTestServer) s.mux.HandleFunc("POST /api/tools/bwtest/server", s.handleUpdateBwTestServer) s.mux.HandleFunc("POST /api/tools/bwtest/start", s.handleStartBwTest) s.mux.HandleFunc("POST /api/tools/bwtest/stop", s.handleStopBwTest) s.mux.HandleFunc("GET /api/tools/bwtest/status", s.handleGetBwTestStatus) s.mux.HandleFunc("GET /api/tools/bwtest/stream", s.handleStreamBwTest) s.mux.HandleFunc("GET /api/tools/bwtest/history", s.handleGetBwTestHistory) // DHCP Client (/ip dhcp-client) s.mux.HandleFunc("GET /api/dhcp/clients", s.handleListDHCPClients) s.mux.HandleFunc("GET /api/dhcp/clients/summary", s.handleGetDHCPSummary) s.mux.HandleFunc("POST /api/dhcp/clients", s.handleCreateDHCPClient) s.mux.HandleFunc("PUT /api/dhcp/clients/{id}", s.handleUpdateDHCPClient) s.mux.HandleFunc("DELETE /api/dhcp/clients/{id}", s.handleDeleteDHCPClient) s.mux.HandleFunc("POST /api/dhcp/clients/{id}/state", s.handleToggleDHCPClientState) s.mux.HandleFunc("POST /api/dhcp/clients/{id}/renew", s.handleRenewDHCPClient) s.mux.HandleFunc("POST /api/dhcp/clients/{id}/release", s.handleReleaseDHCPClient) // DHCP Server (/ip dhcp-server) s.mux.HandleFunc("GET /api/dhcp-server/summary", s.handleGetDHCPServerSummary) s.mux.HandleFunc("GET /api/dhcp-server/servers", s.handleListDHCPServers) s.mux.HandleFunc("POST /api/dhcp-server/servers", s.handleSaveDHCPServer) s.mux.HandleFunc("DELETE /api/dhcp-server/servers/{id}", s.handleDeleteDHCPServer) s.mux.HandleFunc("POST /api/dhcp-server/servers/{id}/state", s.handleSetDHCPServerState) s.mux.HandleFunc("GET /api/dhcp-server/networks", s.handleListDHCPNetworks) s.mux.HandleFunc("POST /api/dhcp-server/networks", s.handleSaveDHCPNetwork) s.mux.HandleFunc("DELETE /api/dhcp-server/networks/{id}", s.handleDeleteDHCPNetwork) s.mux.HandleFunc("GET /api/dhcp-server/leases", s.handleListDHCPLeases) s.mux.HandleFunc("POST /api/dhcp-server/leases/static", s.handleSaveDHCPStaticLease) s.mux.HandleFunc("DELETE /api/dhcp-server/leases/{id}", s.handleDeleteDHCPStaticLease) s.mux.HandleFunc("POST /api/dhcp-server/leases/make-static", s.handleMakeDHCPStaticLease) s.mux.HandleFunc("GET /api/dhcp-server/options", s.handleListDHCPOptions) s.mux.HandleFunc("POST /api/dhcp-server/options", s.handleSaveDHCPOption) s.mux.HandleFunc("DELETE /api/dhcp-server/options/{id}", s.handleDeleteDHCPOption) s.mux.HandleFunc("POST /api/dhcp-server/setup", s.handleRunDHCPSetup) // VRRP Redundancy & High Availability s.mux.HandleFunc("GET /api/vrrp/summary", s.handleGetVRRPSummary) s.mux.HandleFunc("GET /api/vrrp/instances", s.handleListVRRPInstances) s.mux.HandleFunc("POST /api/vrrp/instances", s.handleSaveVRRPInstance) s.mux.HandleFunc("DELETE /api/vrrp/instances/{id}", s.handleDeleteVRRPInstance) s.mux.HandleFunc("POST /api/vrrp/instances/{id}/state", s.handleToggleVRRPState) // Router DNS & Settings (/ip dns) s.mux.HandleFunc("GET /api/dns/settings", s.handleGetDNSSettings) s.mux.HandleFunc("POST /api/dns/settings", s.handleSaveDNSSettings) s.mux.HandleFunc("GET /api/dns/static", s.handleListDNSStatic) s.mux.HandleFunc("POST /api/dns/static", s.handleSaveDNSStatic) s.mux.HandleFunc("DELETE /api/dns/static/{id}", s.handleDeleteDNSStatic) s.mux.HandleFunc("POST /api/dns/static/{id}/state", s.handleSetDNSStaticState) s.mux.HandleFunc("POST /api/dns/flush", s.handleFlushDNSCache) s.mux.HandleFunc("GET /api/dns/status", s.handleGetDNSStatus) // System Settings (/system identity & clock) s.mux.HandleFunc("GET /api/system/settings", s.handleGetSystemSettings) s.mux.HandleFunc("POST /api/system/settings", s.handleSaveSystemSettings) // Management Services (/ip service) s.mux.HandleFunc("GET /api/system/services", s.handleListServices) s.mux.HandleFunc("POST /api/system/services/{name}", s.handleUpdateService) s.mux.HandleFunc("PUT /api/system/services/{name}", s.handleUpdateService) s.mux.HandleFunc("POST /api/system/services/{name}/state", s.handleSetServiceState) // Certificate Management (/certificate) s.mux.HandleFunc("GET /api/certificates", s.handleListCertificates) s.mux.HandleFunc("POST /api/certificates/ca", s.handleCreateCA) s.mux.HandleFunc("POST /api/certificates/generate", s.handleCreateCertificate) s.mux.HandleFunc("POST /api/certificates/import", s.handleImportCertificate) s.mux.HandleFunc("POST /api/certificates/letsencrypt", s.handleIssueLetsEncrypt) s.mux.HandleFunc("GET /api/certificates/{name}", s.handleGetCertificate) s.mux.HandleFunc("GET /api/certificates/{name}/export", s.handleExportCertificate) s.mux.HandleFunc("DELETE /api/certificates/{name}", s.handleDeleteCertificate) // ACME HTTP-01 Challenge responder for Let's Encrypt s.mux.HandleFunc("GET /.well-known/acme-challenge/{token}", s.handleACMEChallenge) // Logging (/log) s.mux.HandleFunc("GET /api/system/logs", s.handleListLogs) // Authentication & User Management s.mux.HandleFunc("POST /api/auth/login", s.handleLogin) s.mux.HandleFunc("POST /api/auth/logout", s.handleLogout) s.mux.HandleFunc("GET /api/auth/me", s.handleGetMe) s.mux.HandleFunc("POST /api/auth/change-password", s.handleChangePassword) s.mux.HandleFunc("GET /api/users", s.handleListUsers) s.mux.HandleFunc("POST /api/users", s.handleCreateUser) s.mux.HandleFunc("PUT /api/users/{username}", s.handleUpdateUser) s.mux.HandleFunc("DELETE /api/users/{username}", s.handleDeleteUser) s.mux.HandleFunc("POST /api/users/{username}/password", s.handleAdminResetPassword) // Web UI Static Files if s.fsHandler != nil { s.mux.Handle("/", s.fsHandler) } } func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { // HTTP and HTTPS have independent management service settings. if s.servicesMgr != nil { service := "www" if r.TLS != nil { service = "www-ssl" } if www, ok := s.servicesMgr.GetService(service); ok && www.Disabled { w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.WriteHeader(http.StatusServiceUnavailable) _, _ = fmt.Fprintf(w, "503 Service Unavailable: %s management service is disabled\n", service) return } } // 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 } // Authentication protection for API routes path := r.URL.Path if strings.HasPrefix(path, "/api/") { // Public endpoint: Login if path != "/api/auth/login" { token := extractToken(r) sess, err := s.authMgr.ValidateSession(token) if err != nil { writeJSON(w, http.StatusUnauthorized, map[string]any{ "error": true, "message": "Authentication required", "login_required": true, }) return } // Admin privileges required for /api/users management if strings.HasPrefix(path, "/api/users") && sess.Role != auth.RoleFull { writeJSON(w, http.StatusForbidden, map[string]any{ "error": true, "message": "Administrative privileges required", }) return } // Read-only role protection: forbid mutating operations (except logout & self password change) if sess.Role == auth.RoleRead && r.Method != http.MethodGet && r.Method != http.MethodHead { if path != "/api/auth/logout" && path != "/api/auth/change-password" { writeJSON(w, http.StatusForbidden, map[string]any{ "error": true, "message": "Read-only account cannot perform configuration modifications", }) return } } // Inject session into request context ctx := context.WithValue(r.Context(), sessionContextKey, sess) r = r.WithContext(ctx) } } 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) handleInterfacesTrafficStream(w http.ResponseWriter, r *http.Request) { flusher, ok := w.(http.Flusher) if !ok { http.Error(w, "Streaming unsupported", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "text/event-stream") w.Header().Set("Cache-Control", "no-cache") w.Header().Set("Connection", "keep-alive") w.Header().Set("Access-Control-Allow-Origin", "*") ch := s.netMgr.SubscribeTraffic() defer s.netMgr.UnsubscribeTraffic(ch) // Send immediate initial snapshot initSnap := s.netMgr.GetLatestTrafficSnapshot() if initSnap != nil { data, _ := json.Marshal(initSnap) fmt.Fprintf(w, "data: %s\n\n", data) flusher.Flush() } notify := r.Context().Done() for { select { case <-notify: return case snap, ok := <-ch: if !ok { return } data, _ := json.Marshal(snap) fmt.Fprintf(w, "data: %s\n\n", data) flusher.Flush() } } } func (s *Server) handleInterfacesTrafficRates(w http.ResponseWriter, r *http.Request) { snap := s.netMgr.GetLatestTrafficSnapshot() if snap == nil { snap = &network.TrafficSnapshot{ Timestamp: time.Now().Unix(), Interfaces: make(map[string]network.InterfaceLiveRate), } } writeJSON(w, http.StatusOK, snap) } 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 } // If this interface is a bridge, also sync to BridgeManager persistent config if s.bridgeMgr != nil { _ = s.bridgeMgr.AddBridgeIP(name, req.IP) } // Persist to Debian network configuration if s.persistMgr != nil { if _, err := s.persistMgr.SaveDebianConfig(); err != nil { writeError(w, 500, "Live change applied but persistence failed: "+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 } // If this interface is a bridge, also delete from BridgeManager persistent config if s.bridgeMgr != nil { _ = s.bridgeMgr.DeleteBridgeIP(name, req.IP) } // Persist to Debian network configuration if s.persistMgr != nil { if _, err := s.persistMgr.SaveDebianConfig(); err != nil { writeError(w, 500, "Live change applied but persistence failed: "+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 } previous := s.metaMgr.Get(name) if err := s.metaMgr.Set(name, req.Role, req.Description); err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } if s.fwMgr != nil { if err := s.fwMgr.Reapply(); err != nil { _ = s.metaMgr.Set(name, previous.Role, previous.Description) _ = s.fwMgr.Reapply() writeError(w, 500, "Interface role update rolled back: "+err.Error()) return } } writeJSON(w, http.StatusOK, map[string]any{"success": true, "name": name, "role": req.Role, "description": req.Description}) } func (s *Server) handleListRoleTags(w http.ResponseWriter, r *http.Request) { roles := s.metaMgr.GetRolesWithUsage() writeJSON(w, http.StatusOK, roles) } func (s *Server) handleSaveRoleTag(w http.ResponseWriter, r *http.Request) { var req network.RoleTag if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid request body") return } id := r.PathValue("id") if id != "" { req.ID = id } saved, err := s.metaMgr.SaveRole(req) if err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } writeJSON(w, http.StatusOK, saved) } func (s *Server) handleDeleteRoleTag(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") if id == "" { writeError(w, http.StatusBadRequest, "missing role ID") return } if err := s.metaMgr.DeleteRole(id); err != nil { writeError(w, http.StatusBadRequest, err.Error()) return } writeJSON(w, http.StatusOK, map[string]string{"status": "deleted", "id": id}) } func (s *Server) handleRenameInterface(w http.ResponseWriter, r *http.Request) { name := r.PathValue("name") var req struct { Name string `json:"name"` NewName string `json:"new_name"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid request body") return } targetName := req.NewName if targetName == "" { targetName = req.Name } targetName = strings.TrimSpace(targetName) if targetName == "" { writeError(w, http.StatusBadRequest, "new interface name is required") return } info, err := s.netMgr.RenameInterface(name, targetName) if err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } // Propagate rename to all referencing subsystems if s.bridgeMgr != nil { s.bridgeMgr.RenameInterface(name, targetName) } if s.dhcpMgr != nil { s.dhcpMgr.RenameInterface(name, targetName) } if s.dhcpServerMgr != nil { s.dhcpServerMgr.RenameInterface(name, targetName) } if s.vrrpMgr != nil { s.vrrpMgr.RenameInterface(name, targetName) } if s.routingMgr != nil { s.routingMgr.RenameInterface(name, targetName) } if s.fwMgr != nil { s.fwMgr.RenameInterface(name, targetName) } if s.vpnMgr != nil { s.vpnMgr.RenameInterface(name, targetName) } if s.persistMgr != nil { if _, err := s.persistMgr.SaveDebianConfig(); err != nil { writeError(w, 500, "Live change applied but persistence failed: "+err.Error()) return } } writeJSON(w, http.StatusOK, map[string]any{ "success": true, "old_name": name, "new_name": targetName, "interface": info, }) } 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, }) }