package cli import ( "context" "fmt" "net" "os" "os/exec" "path/filepath" "sort" "strconv" "strings" "time" release "debtik" "debtik/internal/auth" "debtik/internal/backup" "debtik/internal/bridge" "debtik/internal/bwtest" "debtik/internal/certs" "debtik/internal/dhcp" "debtik/internal/dns" "debtik/internal/dynamicrouting" "debtik/internal/firewall" "debtik/internal/logging" "debtik/internal/network" "debtik/internal/reverseproxy" "debtik/internal/routing" "debtik/internal/sdwan" "debtik/internal/services" "debtik/internal/system" "debtik/internal/vpn" "debtik/internal/vrrp" ) type Executor struct { authMgr *auth.Manager netMgr *network.Manager routingMgr *routing.RoutingManager dynRoutingMgr *dynamicrouting.Manager fwMgr *firewall.FirewallManager dhcpMgr *dhcp.Manager dhcpServMgr *dhcp.ServerManager dnsMgr *dns.Manager bridgeMgr *bridge.BridgeManager vpnMgr *vpn.Manager vrrpMgr *vrrp.Manager bwtestMgr *bwtest.Manager servicesMgr *services.Manager certMgr *certs.CertManager proxyMgr *reverseproxy.Manager backupMgr *backup.Manager sdwanMgr *sdwan.Manager } func NewExecutor( authMgr *auth.Manager, netMgr *network.Manager, routingMgr *routing.RoutingManager, dynRoutingMgr *dynamicrouting.Manager, fwMgr *firewall.FirewallManager, dhcpMgr *dhcp.Manager, dhcpServMgr *dhcp.ServerManager, dnsMgr *dns.Manager, bridgeMgr *bridge.BridgeManager, vpnMgr *vpn.Manager, vrrpMgr *vrrp.Manager, bwtestMgr *bwtest.Manager, servicesMgr *services.Manager, certMgr *certs.CertManager, proxyMgr *reverseproxy.Manager, backupMgr *backup.Manager, sdwanMgr *sdwan.Manager, ) *Executor { if backupMgr == nil { backupMgr = backup.NewManager("", "", "", nil) } if sdwanMgr == nil { sdwanMgr = sdwan.NewManager() } return &Executor{ authMgr: authMgr, netMgr: netMgr, routingMgr: routingMgr, dynRoutingMgr: dynRoutingMgr, fwMgr: fwMgr, dhcpMgr: dhcpMgr, dhcpServMgr: dhcpServMgr, dnsMgr: dnsMgr, bridgeMgr: bridgeMgr, vpnMgr: vpnMgr, vrrpMgr: vrrpMgr, bwtestMgr: bwtestMgr, servicesMgr: servicesMgr, certMgr: certMgr, proxyMgr: proxyMgr, backupMgr: backupMgr, sdwanMgr: sdwanMgr, } } func (e *Executor) Execute(rawCmd string, user *auth.PublicUser) (output string, shouldExit bool) { // A malformed command must never take down the router or its tunnels. defer func() { if recover() != nil { logging.Log("cli,error", "Command handler panicked") output = ColorRed + "Internal command error; the router is still running.\n" + ColorReset shouldExit = false } }() cmd := strings.TrimSpace(rawCmd) if cmd == "" { return "", false } cmd = strings.TrimPrefix(cmd, "/") parts, err := splitCommand(cmd) if err != nil { return ColorRed + "Syntax error: " + err.Error() + "\n" + ColorReset, false } if len(parts) == 0 { return "", false } mainCmd := strings.ToLower(parts[0]) switch mainCmd { case "quit", "exit": return ColorYellow + "Connection closed. Goodbye!\n" + ColorReset, true case "help", "?": return e.showHelp(), false case "clear": return "\033[2J\033[H", false case "ping": return e.handlePing(parts[1:]), false case "traceroute": return e.handleTraceroute(parts[1:]), false case "log": return e.handleLog(parts[1:]), false case "system": return e.handleSystem(parts[1:], user), false case "interface": return e.handleInterface(parts[1:], user), false case "ip": return e.handleIP(parts[1:], user), false case "routing": return e.handleRouting(parts[1:], user), false case "tool": if user == nil || user.Role == auth.RoleRead { return ColorRed + "Permission denied: tools require write privileges\n" + ColorReset, false } return e.handleTool(parts[1:]), false case "user": return e.handleUser(parts[1:], user), false case "certificate", "certificates": return e.handleCertificate(parts[1:], user), false case "reverse-proxy", "reverseproxy": return e.handleIPReverseProxy(parts[1:], user), false case "sdwan", "sd-wan": return e.handleIPSDWAN(parts[1:], user), false default: return fmt.Sprintf(ColorRed+"Unknown command or syntax error: %q. Type 'help' or '?' for command list.\n"+ColorReset, cmd), false } } func parseArgs(parts []string) ([]string, map[string]string) { var pos []string params := make(map[string]string) for _, p := range parts { if idx := strings.Index(p, "="); idx != -1 { k := strings.ToLower(p[:idx]) v := p[idx+1:] params[k] = v } else { pos = append(pos, p) } } return pos, params } // ==================== LOGGING ==================== func (e *Executor) handleLog(parts []string) string { if len(parts) == 0 || parts[0] == "print" || parts[0] == "pr" || parts[0] == "show" { limit := 50 filter := "" args := parts if len(args) > 0 { args = args[1:] } _, params := parseArgs(args) if lStr, ok := params["limit"]; ok { if lVal, err := strconv.Atoi(lStr); err == nil && lVal > 0 { limit = lVal } } if fStr, ok := params["where"]; ok { filter = fStr } else if fStr, ok := params["filter"]; ok { filter = fStr } logger := logging.GetLogger() entries := logger.List(limit, filter) if len(entries) == 0 { return ColorYellow + "No log records found.\n" + ColorReset } tbl := NewTable("#", "TIME", "TOPICS", "MESSAGE") for _, logEntry := range entries { timeStr := logEntry.Time.Format("Jan/02 15:04:05") tbl.AddRow( fmt.Sprintf("%d", logEntry.ID), timeStr, logEntry.Topics, logEntry.Message, ) } return tbl.String() } return ColorRed + "Usage: /log print [limit=N] [where=filter]\n" + ColorReset } // ==================== INTERFACES ==================== func (e *Executor) handleInterface(parts []string, user *auth.PublicUser) string { if len(parts) == 0 { return e.printAllInterfaces() } sub := strings.ToLower(parts[0]) switch sub { case "print", "pr", "show", "list": return e.printAllInterfaces() case "vlan": return e.handleInterfaceVLAN(parts[1:], user) case "bridge": return e.handleInterfaceBridge(parts[1:], user) case "wireguard": return e.handleInterfaceWireGuard(parts[1:], user) case "aerotun": return e.handleInterfaceAeroTUN(parts[1:], user) case "eoip": return e.handleInterfaceEoIP(parts[1:], user) case "vxlan": return e.handleInterfaceVXLAN(parts[1:], user) case "vrrp": return e.handleInterfaceVRRP(parts[1:], user) case "pppoe-client": return e.handleInterfacePPPoE(parts[1:], user) case "enable": if len(parts) < 2 { return ColorRed + "Usage: /interface enable \n" + ColorReset } if user.Role == auth.RoleRead { return ColorRed + "Permission denied: viewer role cannot modify interfaces\n" + ColorReset } name := parts[1] if err := e.netMgr.SetInterfaceState(name, true); err != nil { return ColorRed + "Error: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("Interface %s enabled successfully.\n", name) + ColorReset case "disable": if len(parts) < 2 { return ColorRed + "Usage: /interface disable \n" + ColorReset } if user.Role == auth.RoleRead { return ColorRed + "Permission denied: viewer role cannot modify interfaces\n" + ColorReset } name := parts[1] if err := e.netMgr.SetInterfaceState(name, false); err != nil { return ColorRed + "Error: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("Interface %s disabled successfully.\n", name) + ColorReset case "set": if len(parts) < 2 { return ColorRed + "Usage: /interface set mtu=\n" + ColorReset } if user.Role == auth.RoleRead { return ColorRed + "Permission denied: viewer role cannot modify interfaces\n" + ColorReset } name := parts[1] _, params := parseArgs(parts[2:]) if mtuStr, ok := params["mtu"]; ok { mtu, err := strconv.Atoi(mtuStr) if err != nil || mtu < 68 || mtu > 9000 { return ColorRed + "Invalid MTU value (68-9000)\n" + ColorReset } if err := e.netMgr.SetInterfaceMTU(name, mtu); err != nil { return ColorRed + "Error setting MTU: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("Interface %s MTU updated to %d.\n", name, mtu) + ColorReset } return ColorRed + "Usage: /interface set mtu=\n" + ColorReset default: return ColorRed + "Unknown interface subcommand: " + sub + "\n" + ColorReset } } func (e *Executor) printAllInterfaces() string { ifaces, err := e.netMgr.ListInterfaces() if err != nil { return ColorRed + "Error listing interfaces: " + err.Error() + ColorReset + "\n" } tbl := NewTable("FLAGS", "NAME", "TYPE", "STATE", "MTU", "MAC ADDRESS", "IP ADDRESSES") for _, iface := range ifaces { isUp := strings.ToUpper(iface.State) == "UP" flags := "" if isUp { flags += "R" } else { flags += "X" } stateStr := ColorGreen + "UP" + ColorReset if !isUp { stateStr = ColorRed + "DOWN" + ColorReset } allIPs := append([]string{}, iface.IPv4...) allIPs = append(allIPs, iface.IPv6...) ips := strings.Join(allIPs, ", ") if ips == "" { ips = "-" } tbl.AddRow(flags, iface.Name, string(iface.Type), stateStr, fmt.Sprintf("%d", iface.MTU), iface.MAC, ips) } return tbl.String() } // --- VLAN --- func (e *Executor) handleInterfaceVLAN(parts []string, user *auth.PublicUser) string { if len(parts) == 0 || parts[0] == "print" || parts[0] == "pr" { vlans, err := e.netMgr.ListVLANs() if err != nil { return ColorRed + "Error: " + err.Error() + ColorReset + "\n" } tbl := NewTable("#", "NAME", "VLAN-ID", "INTERFACE", "MTU", "STATE") for idx, v := range vlans { isUp := strings.ToUpper(v.State) == "UP" st := ColorGreen + "UP" + ColorReset if !isUp { st = ColorRed + "DOWN" + ColorReset } tbl.AddRow(fmt.Sprintf("%d", idx), v.Name, fmt.Sprintf("%d", v.VlanID), v.ParentName, fmt.Sprintf("%d", v.MTU), st) } return tbl.String() } sub := strings.ToLower(parts[0]) if user.Role == auth.RoleRead { return ColorRed + "Permission denied: viewer role cannot modify VLANs\n" + ColorReset } switch sub { case "add": _, params := parseArgs(parts[1:]) name := params["name"] parent := params["interface"] vlanIDStr := params["vlan-id"] if vlanIDStr == "" { vlanIDStr = params["vlan_id"] } vlanID, err := strconv.Atoi(vlanIDStr) if err != nil || vlanID < 1 || vlanID > 4094 { return ColorRed + "Invalid vlan-id (1-4094)\n" + ColorReset } if parent == "" { return ColorRed + "Missing parent interface (interface=...)\n" + ColorReset } if name == "" { name = fmt.Sprintf("%s.%d", parent, vlanID) } req := network.CreateVLANRequest{ ParentName: parent, VlanID: vlanID, Name: name, AutoUp: true, } if _, err := e.netMgr.CreateVLAN(req); err != nil { return ColorRed + "Error creating VLAN: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("VLAN interface %s created.\n", name) + ColorReset case "remove", "rem", "delete": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /interface vlan remove \n" + ColorReset } name := pos[0] if err := e.netMgr.DeleteVLAN(name); err != nil { return ColorRed + "Error deleting VLAN: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("VLAN %s removed.\n", name) + ColorReset default: return ColorRed + "Usage: /interface vlan [print|add|remove]\n" + ColorReset } } // --- Bridge --- func (e *Executor) handleInterfaceBridge(parts []string, user *auth.PublicUser) string { if len(parts) == 0 || parts[0] == "print" || parts[0] == "pr" { if e.bridgeMgr == nil { return ColorRed + "Bridge manager not available.\n" + ColorReset } bridges := e.bridgeMgr.ListBridges() tbl := NewTable("FLAGS", "NAME", "MTU", "MAC-ADDRESS", "RUNNING", "PORTS") for _, b := range bridges { flags := "" if b.Disabled { flags += "X" } else { flags += "R" } tbl.AddRow(flags, b.Name, fmt.Sprintf("%d", b.MTU), b.MACAddress, fmt.Sprintf("%t", b.Running), fmt.Sprintf("%d", b.PortsCount)) } return tbl.String() } sub := strings.ToLower(parts[0]) if sub == "port" { return e.handleBridgePort(parts[1:], user) } if user.Role == auth.RoleRead { return ColorRed + "Permission denied: viewer role cannot modify bridge\n" + ColorReset } switch sub { case "add": _, params := parseArgs(parts[1:]) name := params["name"] if name == "" { return ColorRed + "Usage: /interface bridge add name=\n" + ColorReset } b := bridge.BridgeInstance{ Name: name, Disabled: false, } if err := e.bridgeMgr.SaveBridge(b); err != nil { return ColorRed + "Error adding bridge: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("Bridge %s added.\n", name) + ColorReset case "remove", "rem", "delete": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /interface bridge remove \n" + ColorReset } if err := e.bridgeMgr.DeleteBridge(pos[0]); err != nil { return ColorRed + "Error deleting bridge: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("Bridge %s removed.\n", pos[0]) + ColorReset case "enable": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /interface bridge enable \n" + ColorReset } if err := e.bridgeMgr.SetBridgeState(pos[0], false); err != nil { return ColorRed + "Error: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("Bridge %s enabled.\n", pos[0]) + ColorReset case "disable": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /interface bridge disable \n" + ColorReset } if err := e.bridgeMgr.SetBridgeState(pos[0], true); err != nil { return ColorRed + "Error: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("Bridge %s disabled.\n", pos[0]) + ColorReset default: return ColorRed + "Usage: /interface bridge [print|add|remove|enable|disable|port]\n" + ColorReset } } func (e *Executor) handleBridgePort(parts []string, user *auth.PublicUser) string { if len(parts) == 0 || parts[0] == "print" || parts[0] == "pr" { ports := e.bridgeMgr.ListPorts() tbl := NewTable("FLAGS", "INTERFACE", "BRIDGE", "STATUS", "PVID") for _, p := range ports { flags := "" if p.Disabled { flags += "X" } else { flags += "H" } tbl.AddRow(flags, p.Interface, p.Bridge, p.Status, fmt.Sprintf("%d", p.PVID)) } return tbl.String() } if user.Role == auth.RoleRead { return ColorRed + "Permission denied: viewer role cannot modify bridge ports\n" + ColorReset } sub := strings.ToLower(parts[0]) switch sub { case "add": _, params := parseArgs(parts[1:]) br := params["bridge"] iface := params["interface"] if br == "" || iface == "" { return ColorRed + "Usage: /interface bridge port add bridge= interface=\n" + ColorReset } port := bridge.BridgePort{ Bridge: br, Interface: iface, Disabled: false, } if err := e.bridgeMgr.SavePort(port); err != nil { return ColorRed + "Error adding bridge port: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("Port %s attached to bridge %s.\n", iface, br) + ColorReset case "remove", "rem", "delete": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /interface bridge port remove \n" + ColorReset } if err := e.bridgeMgr.DeletePort(pos[0]); err != nil { return ColorRed + "Error: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("Bridge port %s removed.\n", pos[0]) + ColorReset default: return ColorRed + "Usage: /interface bridge port [print|add|remove]\n" + ColorReset } } // --- WireGuard --- func (e *Executor) handleInterfaceWireGuard(parts []string, user *auth.PublicUser) string { if e.vpnMgr == nil || e.vpnMgr.WireGuard == nil { return ColorRed + "WireGuard subsystem not available.\n" + ColorReset } if len(parts) == 0 || parts[0] == "print" || parts[0] == "pr" { ifaces := e.vpnMgr.WireGuard.ListInterfaces() tbl := NewTable("FLAGS", "NAME", "LISTEN-PORT", "PUBLIC-KEY", "STATUS") for _, w := range ifaces { flags := "" if w.Disabled { flags += "X" } else { flags += "R" } st := "running" if !w.Running { st = "stopped" } tbl.AddRow(flags, w.Name, fmt.Sprintf("%d", w.ListenPort), w.PublicKey, st) } return tbl.String() } sub := strings.ToLower(parts[0]) if sub == "peers" || sub == "peer" { return e.handleWireGuardPeers(parts[1:], user) } if user.Role == auth.RoleRead { return ColorRed + "Permission denied: viewer role cannot modify wireguard\n" + ColorReset } switch sub { case "add": _, params := parseArgs(parts[1:]) name := params["name"] portStr := params["listen-port"] privKey := params["private-key"] port := 51820 if portStr != "" { if pVal, err := strconv.Atoi(portStr); err == nil && pVal > 0 { port = pVal } } if name == "" { return ColorRed + "Usage: /interface wireguard add name= [listen-port=]\n" + ColorReset } req := vpn.SaveWireGuardInterfaceRequest{ WireGuardInterface: vpn.WireGuardInterface{ Name: name, ListenPort: port, PrivateKey: privKey, Disabled: false, }, } if err := e.vpnMgr.WireGuard.SaveInterface(req); err != nil { return ColorRed + "Error adding WireGuard interface: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("WireGuard interface %s added.\n", name) + ColorReset case "remove", "rem", "delete": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /interface wireguard remove \n" + ColorReset } if err := e.vpnMgr.WireGuard.DeleteInterface(pos[0]); err != nil { return ColorRed + "Error deleting WireGuard interface: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("WireGuard interface %s removed.\n", pos[0]) + ColorReset case "enable": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /interface wireguard enable \n" + ColorReset } if err := e.vpnMgr.WireGuard.SetInterfaceEnabled(pos[0], true); err != nil { return ColorRed + "Error: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("WireGuard interface %s enabled.\n", pos[0]) + ColorReset case "disable": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /interface wireguard disable \n" + ColorReset } if err := e.vpnMgr.WireGuard.SetInterfaceEnabled(pos[0], false); err != nil { return ColorRed + "Error: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("WireGuard interface %s disabled.\n", pos[0]) + ColorReset default: return ColorRed + "Usage: /interface wireguard [print|add|remove|enable|disable|peers]\n" + ColorReset } } func (e *Executor) handleWireGuardPeers(parts []string, user *auth.PublicUser) string { if len(parts) == 0 || parts[0] == "print" || parts[0] == "pr" { _, params := parseArgs(parts) peers := e.vpnMgr.WireGuard.ListPeers(params["interface"]) tbl := NewTable("INTERFACE", "PUBLIC-KEY", "ENDPOINT", "ALLOWED-IPS", "LAST-HANDSHAKE") for _, p := range peers { hs := "-" if p.LatestHandshake != "" { hs = p.LatestHandshake } ep := p.EndpointAddress if p.EndpointPort > 0 { ep = fmt.Sprintf("%s:%d", p.EndpointAddress, p.EndpointPort) } tbl.AddRow(p.Interface, p.PublicKey, ep, strings.Join(p.AllowedIPs, ","), hs) } return tbl.String() } if user.Role == auth.RoleRead { return ColorRed + "Permission denied: viewer role cannot modify wireguard peers\n" + ColorReset } sub := strings.ToLower(parts[0]) switch sub { case "add": _, params := parseArgs(parts[1:]) iface := params["interface"] pubKey := params["public-key"] endpoint := params["endpoint"] allowed := params["allowed-address"] if allowed == "" { allowed = params["allowed-ips"] } if iface == "" || pubKey == "" { return ColorRed + "Usage: /interface wireguard peers add interface= public-key= [endpoint=host:port] [allowed-address=cidr]\n" + ColorReset } var allowedList []string if allowed != "" { allowedList = strings.Split(allowed, ",") } epHost := endpoint epPort := 0 if strings.Contains(endpoint, ":") { parts := strings.Split(endpoint, ":") epHost = parts[0] if p, err := strconv.Atoi(parts[1]); err == nil { epPort = p } } req := vpn.SaveWireGuardPeerRequest{ WireGuardPeer: vpn.WireGuardPeer{ Interface: iface, PublicKey: pubKey, EndpointAddress: epHost, EndpointPort: epPort, AllowedIPs: allowedList, Disabled: false, }, } if err := e.vpnMgr.WireGuard.SavePeer(req); err != nil { return ColorRed + "Error saving WireGuard peer: " + err.Error() + ColorReset + "\n" } return ColorGreen + "WireGuard peer added successfully.\n" + ColorReset case "remove", "rem", "delete": _, params := parseArgs(parts[1:]) iface := params["interface"] pubKey := params["public-key"] if iface == "" || pubKey == "" { return ColorRed + "Usage: /interface wireguard peers remove interface= public-key=\n" + ColorReset } if err := e.vpnMgr.WireGuard.DeletePeer(iface, pubKey); err != nil { return ColorRed + "Error deleting WireGuard peer: " + err.Error() + ColorReset + "\n" } return ColorGreen + "WireGuard peer removed successfully.\n" + ColorReset default: return ColorRed + "Usage: /interface wireguard peers [print|add|remove]\n" + ColorReset } } // --- EoIP --- func (e *Executor) handleInterfaceEoIP(parts []string, user *auth.PublicUser) string { if e.vpnMgr == nil || e.vpnMgr.EoIP == nil { return ColorRed + "EoIP subsystem not available.\n" + ColorReset } if len(parts) == 0 || parts[0] == "print" || parts[0] == "pr" { tunnels := e.vpnMgr.EoIP.List() tbl := NewTable("FLAGS", "NAME", "REMOTE-ADDRESS", "TUNNEL-ID", "STATUS") for _, t := range tunnels { flags := "" if t.Disabled { flags += "X" } else { flags += "R" } st := "stopped" if t.Running { st = "running" } tbl.AddRow(flags, t.Name, t.RemoteAddress, fmt.Sprintf("%d", t.TunnelID), st) } return tbl.String() } if user.Role == auth.RoleRead { return ColorRed + "Permission denied: viewer role cannot modify EoIP\n" + ColorReset } sub := strings.ToLower(parts[0]) switch sub { case "add": _, params := parseArgs(parts[1:]) name := params["name"] remote := params["remote-address"] tidStr := params["tunnel-id"] tid, err := strconv.Atoi(tidStr) if err != nil || tid < 1 || tid > 65535 { return ColorRed + "Invalid tunnel-id (1-65535)\n" + ColorReset } if name == "" || remote == "" { return ColorRed + "Usage: /interface eoip add name= remote-address= tunnel-id=\n" + ColorReset } inst := &vpn.EoIPInstance{ Name: name, RemoteAddress: remote, TunnelID: tid, Disabled: false, } if err := e.vpnMgr.EoIP.Save(inst); err != nil { return ColorRed + "Error adding EoIP tunnel: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("EoIP tunnel %s added.\n", name) + ColorReset case "remove", "rem", "delete": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /interface eoip remove \n" + ColorReset } if err := e.vpnMgr.EoIP.Delete(pos[0]); err != nil { return ColorRed + "Error: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("EoIP tunnel %s removed.\n", pos[0]) + ColorReset case "enable": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /interface eoip enable \n" + ColorReset } if err := e.vpnMgr.EoIP.SetState(pos[0], false); err != nil { return ColorRed + "Error: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("EoIP tunnel %s enabled.\n", pos[0]) + ColorReset case "disable": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /interface eoip disable \n" + ColorReset } if err := e.vpnMgr.EoIP.SetState(pos[0], true); err != nil { return ColorRed + "Error: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("EoIP tunnel %s disabled.\n", pos[0]) + ColorReset default: return ColorRed + "Usage: /interface eoip [print|add|remove|enable|disable]\n" + ColorReset } } // --- VXLAN --- func (e *Executor) handleInterfaceVXLAN(parts []string, user *auth.PublicUser) string { if e.vpnMgr == nil || e.vpnMgr.VXLAN == nil { return ColorRed + "VXLAN subsystem not available.\n" + ColorReset } if len(parts) > 0 { sub := strings.ToLower(parts[0]) if sub == "vteps" || sub == "vtep" { return e.handleInterfaceVXLANVteps(parts[1:], user) } } if len(parts) == 0 || parts[0] == "print" || parts[0] == "pr" { tunnels := e.vpnMgr.VXLAN.List() tbl := NewTable("FLAGS", "NAME", "VNI", "PORT", "MTU", "MAC-ADDRESS", "STATUS") for _, t := range tunnels { flags := "" if t.Disabled { flags += "X" } if t.Running { flags += "R" } st := "stopped" if !t.Disabled { if t.Running { st = "running" } else { st = "down" } } tbl.AddRow(flags, t.Name, fmt.Sprintf("%d", t.VNI), fmt.Sprintf("%d", t.Port), fmt.Sprintf("%d", t.MTU), t.MACAddress, st) } return tbl.String() } if user != nil && user.Role == auth.RoleRead { return ColorRed + "Permission denied: viewer role cannot modify VXLAN\n" + ColorReset } sub := strings.ToLower(parts[0]) switch sub { case "add": _, params := parseArgs(parts[1:]) vniStr := params["vni"] vni, err := strconv.Atoi(vniStr) if err != nil || vni < 1 || vni > 16777215 { return ColorRed + "Invalid or missing vni (1-16777215)\n" + ColorReset } name := params["name"] if name == "" { name = fmt.Sprintf("vxlan%d", vni) } port := 4789 if pStr := params["port"]; pStr != "" { if p, err := strconv.Atoi(pStr); err == nil && p > 0 && p <= 65535 { port = p } } mtu := 1500 if mStr := params["mtu"]; mStr != "" { if m, err := strconv.Atoi(mStr); err == nil && m >= 576 { mtu = m } } learning := true if lStr := params["learning"]; lStr != "" { lLower := strings.ToLower(lStr) if lLower == "no" || lLower == "false" || lLower == "0" { learning = false } } inst := &vpn.VXLANInstance{ Name: name, VNI: vni, Port: port, MTU: mtu, MACAddress: params["mac-address"], IPAddress: params["ip-address"], LocalAddress: params["local-address"], RemoteAddress: params["remote-address"], Group: params["group"], ParentInterface: params["interface"], Learning: learning, Comment: params["comment"], Disabled: false, } if err := e.vpnMgr.VXLAN.Save(inst); err != nil { return ColorRed + "Error adding VXLAN interface: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("VXLAN interface %s added.\n", name) + ColorReset case "set": pos, params := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /interface vxlan set [name=...] [vni=...] [port=...] [mtu=...] [comment=...]\n" + ColorReset } inst, found := e.vpnMgr.VXLAN.Get(pos[0]) if !found { return ColorRed + fmt.Sprintf("VXLAN interface %q not found\n", pos[0]) + ColorReset } if newName := params["name"]; newName != "" { inst.Name = newName } if vniStr := params["vni"]; vniStr != "" { if v, err := strconv.Atoi(vniStr); err == nil && v >= 1 && v <= 16777215 { inst.VNI = v } } if pStr := params["port"]; pStr != "" { if p, err := strconv.Atoi(pStr); err == nil && p > 0 { inst.Port = p } } if mStr := params["mtu"]; mStr != "" { if m, err := strconv.Atoi(mStr); err == nil && m >= 576 { inst.MTU = m } } if mac := params["mac-address"]; mac != "" { inst.MACAddress = mac } if ip := params["ip-address"]; ip != "" { inst.IPAddress = ip } if loc := params["local-address"]; loc != "" { inst.LocalAddress = loc } if rem := params["remote-address"]; rem != "" { inst.RemoteAddress = rem } if grp := params["group"]; grp != "" { inst.Group = grp } if iface := params["interface"]; iface != "" { inst.ParentInterface = iface } if cmt, ok := params["comment"]; ok { inst.Comment = cmt } if err := e.vpnMgr.VXLAN.Save(inst); err != nil { return ColorRed + "Error updating VXLAN interface: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("VXLAN interface %s updated.\n", inst.Name) + ColorReset case "remove", "rem", "delete": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /interface vxlan remove \n" + ColorReset } if err := e.vpnMgr.VXLAN.Delete(pos[0]); err != nil { return ColorRed + "Error: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("VXLAN interface %s removed.\n", pos[0]) + ColorReset case "enable": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /interface vxlan enable \n" + ColorReset } if err := e.vpnMgr.VXLAN.SetState(pos[0], false); err != nil { return ColorRed + "Error: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("VXLAN interface %s enabled.\n", pos[0]) + ColorReset case "disable": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /interface vxlan disable \n" + ColorReset } if err := e.vpnMgr.VXLAN.SetState(pos[0], true); err != nil { return ColorRed + "Error: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("VXLAN interface %s disabled.\n", pos[0]) + ColorReset default: return ColorRed + "Usage: /interface vxlan [print|add|set|remove|enable|disable|vteps]\n" + ColorReset } } // --- VXLAN VTEPs --- func (e *Executor) handleInterfaceVXLANVteps(parts []string, user *auth.PublicUser) string { if len(parts) == 0 || parts[0] == "print" || parts[0] == "pr" { _, params := parseArgs(parts) vteps := e.vpnMgr.VXLAN.ListVteps(params["interface"]) tbl := NewTable("INTERFACE", "REMOTE-IP", "PORT", "COMMENT") for _, v := range vteps { pStr := fmt.Sprintf("%d", v.Port) if v.Port <= 0 { pStr = "default" } tbl.AddRow(v.Interface, v.RemoteIP, pStr, v.Comment) } return tbl.String() } if user != nil && user.Role == auth.RoleRead { return ColorRed + "Permission denied: viewer role cannot modify VXLAN VTEPs\n" + ColorReset } sub := strings.ToLower(parts[0]) switch sub { case "add": _, params := parseArgs(parts[1:]) iface := params["interface"] remote := params["remote-ip"] if remote == "" { remote = params["remote-address"] } if iface == "" || remote == "" { return ColorRed + "Usage: /interface vxlan vteps add interface= remote-ip= [port=4789] [comment=...]\n" + ColorReset } port := 0 if pStr := params["port"]; pStr != "" { if p, err := strconv.Atoi(pStr); err == nil && p > 0 { port = p } } vtep := &vpn.VXLANVtep{ Interface: iface, RemoteIP: remote, Port: port, Comment: params["comment"], } if err := e.vpnMgr.VXLAN.AddVtep(iface, vtep); err != nil { return ColorRed + "Error adding VTEP: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("VTEP %s added to interface %s.\n", remote, iface) + ColorReset case "remove", "rem", "delete": pos, params := parseArgs(parts[1:]) iface := params["interface"] remote := params["remote-ip"] if remote == "" { remote = params["remote-address"] } if iface != "" && remote != "" { if err := e.vpnMgr.VXLAN.DeleteVtep(iface, remote); err != nil { return ColorRed + "Error: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("VTEP %s removed from %s.\n", remote, iface) + ColorReset } if len(pos) >= 2 { if err := e.vpnMgr.VXLAN.DeleteVtep(pos[0], pos[1]); err != nil { return ColorRed + "Error: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("VTEP %s removed from %s.\n", pos[1], pos[0]) + ColorReset } return ColorRed + "Usage: /interface vxlan vteps remove interface= remote-ip= OR \n" + ColorReset default: return ColorRed + "Usage: /interface vxlan vteps [print|add|remove]\n" + ColorReset } } // --- VRRP --- func (e *Executor) handleInterfaceVRRP(parts []string, user *auth.PublicUser) string { if e.vrrpMgr == nil { return ColorRed + "VRRP subsystem not available.\n" + ColorReset } if len(parts) == 0 || parts[0] == "print" || parts[0] == "pr" { instances := e.vrrpMgr.List() tbl := NewTable("FLAGS", "NAME", "INTERFACE", "VRID", "PRIORITY", "STATE", "VIP") for _, inst := range instances { flags := "" if inst.Disabled { flags += "X" } else { flags += "R" } tbl.AddRow(flags, inst.Name, inst.Interface, fmt.Sprintf("%d", inst.VRID), fmt.Sprintf("%d", inst.Priority), inst.State, strings.Join(inst.VirtualIPs, ",")) } return tbl.String() } if user.Role == auth.RoleRead { return ColorRed + "Permission denied: viewer role cannot modify VRRP\n" + ColorReset } sub := strings.ToLower(parts[0]) switch sub { case "add": _, params := parseArgs(parts[1:]) name := params["name"] iface := params["interface"] vridStr := params["vrid"] prioStr := params["priority"] vip := params["vip"] if vip == "" { vip = params["virtual-ip"] } vrid, err := strconv.Atoi(vridStr) if err != nil || vrid < 1 || vrid > 255 { return ColorRed + "Invalid vrid (1-255)\n" + ColorReset } prio := 100 if prioStr != "" { if pVal, err := strconv.Atoi(prioStr); err == nil && pVal >= 1 && pVal <= 254 { prio = pVal } } if name == "" || iface == "" || vip == "" { return ColorRed + "Usage: /interface vrrp add name= interface= vrid= vip= [priority=100]\n" + ColorReset } inst := vrrp.VRRPInstance{ Name: name, Interface: iface, VRID: vrid, Priority: prio, VirtualIPs: []string{vip}, Disabled: false, } if _, err := e.vrrpMgr.Save(inst); err != nil { return ColorRed + "Error adding VRRP: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("VRRP instance %s added.\n", name) + ColorReset case "remove", "rem", "delete": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /interface vrrp remove \n" + ColorReset } if err := e.vrrpMgr.Delete(pos[0]); err != nil { return ColorRed + "Error: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("VRRP instance %s removed.\n", pos[0]) + ColorReset case "enable": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /interface vrrp enable \n" + ColorReset } if err := e.vrrpMgr.ToggleState(pos[0], false); err != nil { return ColorRed + "Error: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("VRRP instance %s enabled.\n", pos[0]) + ColorReset case "disable": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /interface vrrp disable \n" + ColorReset } if err := e.vrrpMgr.ToggleState(pos[0], true); err != nil { return ColorRed + "Error: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("VRRP instance %s disabled.\n", pos[0]) + ColorReset default: return ColorRed + "Usage: /interface vrrp [print|add|remove|enable|disable]\n" + ColorReset } } // --- PPPoE Client --- func (e *Executor) handleInterfacePPPoE(parts []string, user *auth.PublicUser) string { if e.vpnMgr == nil || e.vpnMgr.PPPoE == nil { return ColorRed + "PPPoE subsystem not available.\n" + ColorReset } if len(parts) == 0 || parts[0] == "print" || parts[0] == "pr" { clients := e.vpnMgr.PPPoE.List() tbl := NewTable("FLAGS", "NAME", "INTERFACE", "USER", "STATUS", "IP") for _, c := range clients { flags := "" if c.Disabled { flags += "X" } else { flags += "R" } st := "stopped" if c.Running { st = "running" } tbl.AddRow(flags, c.Name, c.Interface, c.Username, st, c.AssignedIP) } return tbl.String() } if user.Role == auth.RoleRead { return ColorRed + "Permission denied: viewer role cannot modify PPPoE\n" + ColorReset } sub := strings.ToLower(parts[0]) switch sub { case "add": _, params := parseArgs(parts[1:]) name := params["name"] iface := params["interface"] usr := params["user"] pwd := params["password"] if name == "" || iface == "" || usr == "" { return ColorRed + "Usage: /interface pppoe-client add name= interface= user= password=\n" + ColorReset } inst := vpn.PPPoEInstance{ Name: name, Interface: iface, Username: usr, Password: pwd, Disabled: false, } if err := e.vpnMgr.PPPoE.Save(inst); err != nil { return ColorRed + "Error adding PPPoE client: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("PPPoE client %s added.\n", name) + ColorReset case "remove", "rem", "delete": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /interface pppoe-client remove \n" + ColorReset } if err := e.vpnMgr.PPPoE.Delete(pos[0]); err != nil { return ColorRed + "Error: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("PPPoE client %s removed.\n", pos[0]) + ColorReset case "connect": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /interface pppoe-client connect \n" + ColorReset } if err := e.vpnMgr.PPPoE.Connect(pos[0]); err != nil { return ColorRed + "Error: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("Connecting PPPoE client %s...\n", pos[0]) + ColorReset case "disconnect": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /interface pppoe-client disconnect \n" + ColorReset } if err := e.vpnMgr.PPPoE.Disconnect(pos[0]); err != nil { return ColorRed + "Error: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("Disconnected PPPoE client %s.\n", pos[0]) + ColorReset default: return ColorRed + "Usage: /interface pppoe-client [print|add|remove|connect|disconnect]\n" + ColorReset } } // ==================== IP SUBSYSTEM ==================== func (e *Executor) handleIP(parts []string, user *auth.PublicUser) string { if len(parts) == 0 { return ColorYellow + "IP subsystem. Available: address, route, dhcp-client, dhcp-server, dns, firewall, service\n" + ColorReset } sub := strings.ToLower(parts[0]) switch sub { case "address", "addr": return e.handleIPAddress(parts[1:], user) case "route": return e.handleIPRoute(parts[1:], user) case "dhcp-client": return e.handleIPDHCPClient(parts[1:], user) case "dhcp-server": return e.handleIPDHCPServer(parts[1:], user) case "dns": return e.handleIPDNS(parts[1:], user) case "firewall": return e.handleIPFirewall(parts[1:], user) case "service", "services": return e.handleIPService(parts[1:], user) case "reverse-proxy", "reverseproxy", "proxy": return e.handleIPReverseProxy(parts[1:], user) case "sdwan", "sd-wan": return e.handleIPSDWAN(parts[1:], user) default: return ColorRed + "Unknown IP command: " + sub + "\n" + ColorReset } } // --- IP Address --- func (e *Executor) handleIPAddress(parts []string, user *auth.PublicUser) string { if len(parts) == 0 || parts[0] == "print" || parts[0] == "pr" { ifaces, err := e.netMgr.ListInterfaces() if err != nil { return ColorRed + "Error listing IP addresses: " + err.Error() + ColorReset + "\n" } tbl := NewTable("#", "ADDRESS", "NETWORK", "INTERFACE", "STATUS") idx := 0 for _, iface := range ifaces { for _, cidr := range append(iface.IPv4, iface.IPv6...) { ip, ipNet, parseErr := net.ParseCIDR(cidr) netStr := "-" if parseErr == nil && ipNet != nil { netStr = ipNet.IP.String() } else if ip != nil { netStr = ip.String() } tbl.AddRow(fmt.Sprintf("%d", idx), cidr, netStr, iface.Name, "enabled") idx++ } } return tbl.String() } if user.Role == auth.RoleRead { return ColorRed + "Permission denied: viewer role cannot modify IP addresses\n" + ColorReset } sub := strings.ToLower(parts[0]) switch sub { case "add": _, params := parseArgs(parts[1:]) addr := params["address"] iface := params["interface"] if addr == "" || iface == "" { return ColorRed + "Usage: /ip address add address= interface=\n" + ColorReset } if err := e.netMgr.AddInterfaceIP(iface, addr); err != nil { return ColorRed + "Error adding IP address: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("IP address %s added to interface %s.\n", addr, iface) + ColorReset case "remove", "rem", "delete": _, params := parseArgs(parts[1:]) addr := params["address"] iface := params["interface"] if addr == "" || iface == "" { return ColorRed + "Usage: /ip address remove address= interface=\n" + ColorReset } if err := e.netMgr.DeleteInterfaceIP(iface, addr); err != nil { return ColorRed + "Error removing IP address: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("IP address %s removed from interface %s.\n", addr, iface) + ColorReset default: return ColorRed + "Usage: /ip address [print|add|remove]\n" + ColorReset } } // --- IP Route --- func (e *Executor) handleIPRoute(parts []string, user *auth.PublicUser) string { if e.routingMgr == nil { return ColorRed + "Routing manager not available\n" + ColorReset } if len(parts) == 0 || parts[0] == "print" || parts[0] == "pr" { routes := e.routingMgr.ListRoutes() tbl := NewTable("FLAGS", "DST-ADDRESS", "GATEWAY", "DISTANCE", "TABLE", "STATUS") for _, r := range routes { flags := "" if r.Disabled { flags += "X" } else { flags += "A" } if r.Static { flags += "S" } else { flags += "D" } st := "active" if r.Disabled { st = "disabled" } tbl.AddRow(flags, r.DstAddress, r.Gateway, fmt.Sprintf("%d", r.Distance), r.RoutingTable, st) } return tbl.String() } sub := strings.ToLower(parts[0]) switch sub { case "rule": rules := e.routingMgr.ListPolicyRules() tbl := NewTable("#", "SRC", "DST", "TABLE", "PRIORITY") for idx, r := range rules { tbl.AddRow(fmt.Sprintf("%d", idx), r.SrcAddress, r.DstAddress, r.Table, fmt.Sprintf("%d", r.Priority)) } return tbl.String() case "table": tables := e.routingMgr.ListTables() tbl := NewTable("#", "NAME", "TABLE-ID", "COMMENT") for idx, t := range tables { tbl.AddRow(fmt.Sprintf("%d", idx), t.Name, fmt.Sprintf("%d", t.TableID), t.Comment) } return tbl.String() } if user.Role == auth.RoleRead { return ColorRed + "Permission denied: viewer role cannot modify routes\n" + ColorReset } switch sub { case "add": _, params := parseArgs(parts[1:]) dst := params["dst-address"] if dst == "" { dst = params["dst"] } gw := params["gateway"] iface := params["interface"] if gw == "" && iface != "" { gw = iface } if gw != "" && iface == "" && net.ParseIP(gw) == nil { iface = gw } table := params["routing-table"] if table == "" { table = "main" } dist := 1 if dStr, ok := params["distance"]; ok { if dVal, err := strconv.Atoi(dStr); err == nil && dVal >= 1 { dist = dVal } } if dst == "" || (gw == "" && iface == "") { return ColorRed + "Usage: /ip route add dst-address= gateway= [distance=1] [routing-table=main]\n" + ColorReset } r := routing.RouteItem{ DstAddress: dst, Gateway: gw, Interface: iface, Distance: dist, RoutingTable: table, Static: true, Disabled: false, } if err := e.routingMgr.SaveRoute(r); err != nil { return ColorRed + "Error adding route: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("Route to %s via %s added.\n", dst, gw) + ColorReset case "remove", "rem", "delete": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /ip route remove \n" + ColorReset } if err := e.routingMgr.DeleteRoute(pos[0]); err != nil { return ColorRed + "Error removing route: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("Route %s removed.\n", pos[0]) + ColorReset case "enable": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /ip route enable \n" + ColorReset } if err := e.routingMgr.SetRouteState(pos[0], false); err != nil { return ColorRed + "Error: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("Route %s enabled.\n", pos[0]) + ColorReset case "disable": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /ip route disable \n" + ColorReset } if err := e.routingMgr.SetRouteState(pos[0], true); err != nil { return ColorRed + "Error: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("Route %s disabled.\n", pos[0]) + ColorReset default: return ColorRed + "Usage: /ip route [print|add|remove|enable|disable|rule|table]\n" + ColorReset } } // --- IP DHCP Client --- func (e *Executor) handleIPDHCPClient(parts []string, user *auth.PublicUser) string { if e.dhcpMgr == nil { return ColorRed + "DHCP client manager not available.\n" + ColorReset } if len(parts) == 0 || parts[0] == "print" || parts[0] == "pr" { clients := e.dhcpMgr.ListClients() tbl := NewTable("FLAGS", "INTERFACE", "USE-PEER-DNS", "STATUS", "ADDRESS", "GATEWAY") for _, c := range clients { flags := "" if !c.Enabled { flags += "X" } else { flags += "R" } addr := c.IPAddress if addr == "" { addr = "-" } gw := c.Gateway if gw == "" { gw = "-" } tbl.AddRow(flags, c.Interface, fmt.Sprintf("%t", c.UsePeerDNS), c.Status, addr, gw) } return tbl.String() } if user.Role == auth.RoleRead { return ColorRed + "Permission denied: viewer role cannot modify DHCP clients\n" + ColorReset } sub := strings.ToLower(parts[0]) switch sub { case "add": _, params := parseArgs(parts[1:]) iface := params["interface"] if iface == "" { return ColorRed + "Usage: /ip dhcp-client add interface= [use-peer-dns=yes]\n" + ColorReset } peerDNS := true if val, ok := params["use-peer-dns"]; ok { peerDNS = (strings.ToLower(val) == "yes" || strings.ToLower(val) == "true") } req := dhcp.CreateDHCPClientRequest{ Interface: iface, UsePeerDNS: peerDNS, Enabled: true, } if _, err := e.dhcpMgr.AddClient(req); err != nil { return ColorRed + "Error adding DHCP client: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("DHCP client added on interface %s.\n", iface) + ColorReset case "remove", "rem", "delete": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /ip dhcp-client remove \n" + ColorReset } if err := e.dhcpMgr.DeleteClient(pos[0]); err != nil { return ColorRed + "Error: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("DHCP client %s removed.\n", pos[0]) + ColorReset case "renew": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /ip dhcp-client renew \n" + ColorReset } if err := e.dhcpMgr.RenewLease(pos[0]); err != nil { return ColorRed + "Error renewing lease: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("DHCP lease renewal requested for %s.\n", pos[0]) + ColorReset case "release": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /ip dhcp-client release \n" + ColorReset } if err := e.dhcpMgr.ReleaseLease(pos[0]); err != nil { return ColorRed + "Error releasing lease: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("DHCP lease released for %s.\n", pos[0]) + ColorReset default: return ColorRed + "Usage: /ip dhcp-client [print|add|remove|renew|release]\n" + ColorReset } } // --- IP DHCP Server --- func (e *Executor) handleIPDHCPServer(parts []string, user *auth.PublicUser) string { if e.dhcpServMgr == nil { return ColorRed + "DHCP server manager not available.\n" + ColorReset } if len(parts) == 0 || parts[0] == "print" || parts[0] == "pr" { servers := e.dhcpServMgr.ListServers() tbl := NewTable("FLAGS", "NAME", "INTERFACE", "ADDRESS-POOL", "LEASE-TIME") for _, s := range servers { flags := "" if s.Disabled { flags += "X" } else { flags += "R" } tbl.AddRow(flags, s.Name, s.Interface, s.AddressPool, s.LeaseTime) } return tbl.String() } sub := strings.ToLower(parts[0]) switch sub { case "lease": if len(parts) > 1 && (parts[1] == "remove" || parts[1] == "rem" || parts[1] == "delete") { if user.Role == auth.RoleRead { return ColorRed + "Permission denied\n" + ColorReset } pos, _ := parseArgs(parts[2:]) if len(pos) == 0 { return ColorRed + "Usage: /ip dhcp-server lease remove \n" + ColorReset } if err := e.dhcpServMgr.DeleteStaticLease(pos[0]); err != nil { return ColorRed + "Error: " + err.Error() + "\n" + ColorReset } return ColorGreen + fmt.Sprintf("Lease %s removed.\n", pos[0]) + ColorReset } leases := e.dhcpServMgr.ListLeases() tbl := NewTable("IP-ADDRESS", "MAC-ADDRESS", "HOSTNAME", "EXPIRES-IN", "STATUS") for _, l := range leases { tbl.AddRow(l.Address, l.MACAddress, l.HostName, l.ExpiresIn, l.Status) } return tbl.String() case "network": if len(parts) > 1 { act := strings.ToLower(parts[1]) if act == "add" { if user.Role == auth.RoleRead { return ColorRed + "Permission denied\n" + ColorReset } _, params := parseArgs(parts[2:]) addr := params["address"] if addr == "" { addr = params["subnet"] } if addr == "" { return ColorRed + "Usage: /ip dhcp-server network add address= [gateway=] [dns-server=] [domain=]\n" + ColorReset } var dnsServers []string if dnsStr, ok := params["dns-server"]; ok && dnsStr != "" { dnsServers = strings.Split(dnsStr, ",") } netw := dhcp.DHCPNetwork{ Address: addr, Gateway: params["gateway"], DNSServers: dnsServers, Domain: params["domain"], } if _, err := e.dhcpServMgr.SaveNetwork(netw); err != nil { return ColorRed + "Error: " + err.Error() + "\n" + ColorReset } return ColorGreen + fmt.Sprintf("DHCP network %s added.\n", addr) + ColorReset } if act == "remove" || act == "rem" || act == "delete" { if user.Role == auth.RoleRead { return ColorRed + "Permission denied\n" + ColorReset } pos, _ := parseArgs(parts[2:]) if len(pos) == 0 { return ColorRed + "Usage: /ip dhcp-server network remove <#|subnet|id>\n" + ColorReset } target := pos[0] nets := e.dhcpServMgr.ListNetworks() var netID string if idx, err := strconv.Atoi(target); err == nil && idx >= 0 && idx < len(nets) { netID = nets[idx].ID } else { for _, n := range nets { if n.Address == target || n.ID == target { netID = n.ID break } } } if netID == "" { netID = target } if err := e.dhcpServMgr.DeleteNetwork(netID); err != nil { return ColorRed + "Error: " + err.Error() + "\n" + ColorReset } return ColorGreen + fmt.Sprintf("DHCP network %s removed.\n", target) + ColorReset } } networks := e.dhcpServMgr.ListNetworks() tbl := NewTable("#", "SUBNET", "GATEWAY", "DNS-SERVERS", "DOMAIN") for idx, n := range networks { tbl.AddRow(fmt.Sprintf("%d", idx), n.Address, n.Gateway, strings.Join(n.DNSServers, ","), n.Domain) } return tbl.String() } if user.Role == auth.RoleRead { return ColorRed + "Permission denied: viewer role cannot modify DHCP server\n" + ColorReset } switch sub { case "add": _, params := parseArgs(parts[1:]) name := params["name"] iface := params["interface"] pool := params["address-pool"] if pool == "" { start := params["range-start"] end := params["range-end"] if start != "" && end != "" { pool = fmt.Sprintf("%s-%s", start, end) } } leaseTime := params["lease-time"] if leaseTime == "" { leaseTime = "12h" } if name == "" || iface == "" || pool == "" { return ColorRed + "Usage: /ip dhcp-server add name= interface= address-pool= [lease-time=12h]\n" + ColorReset } inst := dhcp.DHCPServerInstance{ Name: name, Interface: iface, AddressPool: pool, LeaseTime: leaseTime, Disabled: false, } if _, err := e.dhcpServMgr.SaveServer(inst); err != nil { return ColorRed + "Error adding DHCP server: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("DHCP server %s added on interface %s.\n", name, iface) + ColorReset case "remove", "rem", "delete": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /ip dhcp-server remove \n" + ColorReset } if err := e.dhcpServMgr.DeleteServer(pos[0]); err != nil { return ColorRed + "Error: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("DHCP server %s removed.\n", pos[0]) + ColorReset case "enable": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /ip dhcp-server enable \n" + ColorReset } if err := e.dhcpServMgr.ToggleServerState(pos[0], false); err != nil { return ColorRed + "Error: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("DHCP server %s enabled.\n", pos[0]) + ColorReset case "disable": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /ip dhcp-server disable \n" + ColorReset } if err := e.dhcpServMgr.ToggleServerState(pos[0], true); err != nil { return ColorRed + "Error: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("DHCP server %s disabled.\n", pos[0]) + ColorReset default: return ColorRed + "Usage: /ip dhcp-server [print|add|remove|enable|disable|lease|network]\n" + ColorReset } } // --- IP DNS --- func (e *Executor) handleIPDNS(parts []string, user *auth.PublicUser) string { if e.dnsMgr == nil { return ColorRed + "DNS manager not available.\n" + ColorReset } if len(parts) == 0 || parts[0] == "print" || parts[0] == "pr" { settings := e.dnsMgr.GetSettings() out := " servers: " + strings.Join(settings.Servers, ", ") + "\n" out += " dynamic-servers: " + strings.Join(settings.DynamicServers, ", ") + "\n" out += fmt.Sprintf(" allow-remote-requests: %t\n", settings.AllowRemoteRequests) out += fmt.Sprintf(" cache-size: %d entries\n", settings.CacheSize) out += fmt.Sprintf(" domain: %s\n", settings.Domain) return out } sub := strings.ToLower(parts[0]) switch sub { case "cache": if len(parts) > 1 && parts[1] == "flush" { if user.Role == auth.RoleRead { return ColorRed + "Permission denied\n" + ColorReset } if err := e.dnsMgr.FlushCache(); err != nil { return ColorRed + "Error flushing DNS cache: " + err.Error() + ColorReset + "\n" } return ColorGreen + "DNS cache flushed.\n" + ColorReset } return ColorYellow + "Usage: /ip dns cache flush\n" + ColorReset case "static": if len(parts) > 1 { act := strings.ToLower(parts[1]) if act == "add" { if user.Role == auth.RoleRead { return ColorRed + "Permission denied\n" + ColorReset } _, params := parseArgs(parts[2:]) name := params["name"] addr := params["address"] if name == "" || addr == "" { return ColorRed + "Usage: /ip dns static add name= address= [ttl=300]\n" + ColorReset } ttl := 300 if v, ok := params["ttl"]; ok { fmt.Sscanf(v, "%d", &ttl) } rec := dns.StaticRecord{ Name: name, Address: addr, TTL: ttl, } if err := e.dnsMgr.SaveStaticRecord(rec); err != nil { return ColorRed + "Error: " + err.Error() + "\n" + ColorReset } return ColorGreen + fmt.Sprintf("DNS static record %s -> %s added.\n", name, addr) + ColorReset } if act == "remove" || act == "rem" || act == "delete" { if user.Role == auth.RoleRead { return ColorRed + "Permission denied\n" + ColorReset } pos, _ := parseArgs(parts[2:]) if len(pos) == 0 { return ColorRed + "Usage: /ip dns static remove <#|name|id>\n" + ColorReset } target := pos[0] recs := e.dnsMgr.ListStaticRecords() var recID string if idx, err := strconv.Atoi(target); err == nil && idx >= 0 && idx < len(recs) { recID = recs[idx].ID } else { for _, r := range recs { if strings.EqualFold(r.Name, target) || r.ID == target { recID = r.ID break } } } if recID == "" { recID = target } if err := e.dnsMgr.DeleteStaticRecord(recID); err != nil { return ColorRed + "Error: " + err.Error() + "\n" + ColorReset } return ColorGreen + fmt.Sprintf("DNS static record %s removed.\n", target) + ColorReset } if act == "enable" || act == "disable" { if user.Role == auth.RoleRead { return ColorRed + "Permission denied\n" + ColorReset } pos, _ := parseArgs(parts[2:]) if len(pos) == 0 { return ColorRed + "Usage: /ip dns static " + act + " <#|name|id>\n" + ColorReset } target := pos[0] recs := e.dnsMgr.ListStaticRecords() var recID string if idx, err := strconv.Atoi(target); err == nil && idx >= 0 && idx < len(recs) { recID = recs[idx].ID } else { for _, r := range recs { if strings.EqualFold(r.Name, target) || r.ID == target { recID = r.ID break } } } if recID == "" { recID = target } if err := e.dnsMgr.ToggleStaticRecord(recID, act == "disable"); err != nil { return ColorRed + "Error: " + err.Error() + "\n" + ColorReset } return ColorGreen + fmt.Sprintf("DNS static record %s %sd.\n", target, act) + ColorReset } } records := e.dnsMgr.ListStaticRecords() tbl := NewTable("#", "NAME", "ADDRESS", "TTL", "STATUS") for idx, r := range records { st := "enabled" if r.Disabled { st = "disabled" } tbl.AddRow(fmt.Sprintf("%d", idx), r.Name, r.Address, fmt.Sprintf("%d", r.TTL), st) } return tbl.String() case "set": if user.Role == auth.RoleRead { return ColorRed + "Permission denied\n" + ColorReset } _, params := parseArgs(parts[1:]) if sStr, ok := params["servers"]; ok { srvs := strings.Split(sStr, ",") cfg := e.dnsMgr.GetSettings() cfg.Servers = srvs if err := e.dnsMgr.SaveSettings(cfg); err != nil { return ColorRed + "Error saving DNS settings: " + err.Error() + ColorReset + "\n" } return ColorGreen + "DNS servers updated.\n" + ColorReset } return ColorRed + "Usage: /ip dns set servers=\n" + ColorReset default: return ColorRed + "Usage: /ip dns [print|set|cache flush|static]\n" + ColorReset } } // --- IP Firewall --- func (e *Executor) handleIPFirewall(parts []string, user *auth.PublicUser) string { if e.fwMgr == nil { return ColorRed + "Firewall manager not available.\n" + ColorReset } if len(parts) == 0 { return ColorYellow + "Firewall subsystem. Available: filter, nat, mangle, address-list\n" + ColorReset } tableType := strings.ToLower(parts[0]) switch tableType { case "filter": return e.handleFirewallFilter(parts[1:], user) case "nat": return e.handleFirewallNAT(parts[1:], user) case "mangle": return e.handleFirewallMangle(parts[1:], user) case "address-list": return e.handleFirewallAddressList(parts[1:], user) case "connection", "connections": return e.handleFirewallConnection(parts[1:], user) default: return ColorRed + "Unknown firewall table: " + tableType + "\n" + ColorReset } } func (e *Executor) handleFirewallConnection(parts []string, user *auth.PublicUser) string { data, err := os.ReadFile("/proc/net/nf_conntrack") if err != nil { out, cErr := exec.Command("conntrack", "-L").CombinedOutput() if cErr != nil { return ColorYellow + "No active conntrack connections found or nf_conntrack module inactive.\n" + ColorReset } data = out } tbl := NewTable("PROTO", "SRC", "DST", "SPORT", "DPORT", "STATE") lines := strings.Split(string(data), "\n") count := 0 for _, l := range lines { l = strings.TrimSpace(l) if l == "" { continue } fields := strings.Fields(l) proto := "-" src := "-" dst := "-" sport := "-" dport := "-" state := "-" for _, f := range fields { if f == "tcp" || f == "udp" || f == "icmp" { proto = f } else if strings.HasPrefix(f, "src=") && src == "-" { src = strings.TrimPrefix(f, "src=") } else if strings.HasPrefix(f, "dst=") && dst == "-" { dst = strings.TrimPrefix(f, "dst=") } else if strings.HasPrefix(f, "sport=") && sport == "-" { sport = strings.TrimPrefix(f, "sport=") } else if strings.HasPrefix(f, "dport=") && dport == "-" { dport = strings.TrimPrefix(f, "dport=") } else if f == "ESTABLISHED" || f == "TIME_WAIT" || f == "CLOSE" || f == "SYN_SENT" || f == "SYN_RECV" { state = f } } tbl.AddRow(proto, src, dst, sport, dport, state) count++ if count >= 100 { break } } if count == 0 { return "No active connection tracking entries.\n" } return tbl.String() } func (e *Executor) handleFirewallFilter(parts []string, user *auth.PublicUser) string { if len(parts) == 0 || parts[0] == "print" || parts[0] == "pr" { rules := e.fwMgr.ListFilterRules() tbl := NewTable("FLAGS", "CHAIN", "ACTION", "PROTO", "SRC-ADDR", "DST-ADDR", "DST-PORT", "COMMENT") for _, r := range rules { flags := "" if r.Disabled { flags += "X" } else { flags += " " } tbl.AddRow(flags, r.Chain, r.Action, r.Protocol, r.SrcAddress, r.DstAddress, r.DstPort, r.Comment) } return tbl.String() } if user.Role == auth.RoleRead { return ColorRed + "Permission denied: viewer role cannot modify firewall\n" + ColorReset } sub := strings.ToLower(parts[0]) switch sub { case "add": _, params := parseArgs(parts[1:]) chain := params["chain"] action := params["action"] if chain == "" || action == "" { return ColorRed + "Usage: /ip firewall filter add chain= action= [protocol=...] [dst-port=...]\n" + ColorReset } r := firewall.FilterRule{ Chain: chain, Action: action, Protocol: params["protocol"], SrcAddress: params["src-address"], DstAddress: params["dst-address"], DstPort: params["dst-port"], Comment: params["comment"], Disabled: false, } if err := e.fwMgr.SaveFilterRule(r); err != nil { return ColorRed + "Error adding filter rule: " + err.Error() + ColorReset + "\n" } return ColorGreen + "Firewall filter rule added.\n" + ColorReset case "remove", "rem", "delete": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /ip firewall filter remove \n" + ColorReset } if err := e.fwMgr.DeleteFilterRule(pos[0]); err != nil { return ColorRed + "Error: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("Filter rule %s removed.\n", pos[0]) + ColorReset case "enable": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /ip firewall filter enable \n" + ColorReset } if err := e.fwMgr.SetFilterRuleState(pos[0], false); err != nil { return ColorRed + "Error: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("Filter rule %s enabled.\n", pos[0]) + ColorReset case "disable": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /ip firewall filter disable \n" + ColorReset } if err := e.fwMgr.SetFilterRuleState(pos[0], true); err != nil { return ColorRed + "Error: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("Filter rule %s disabled.\n", pos[0]) + ColorReset default: return ColorRed + "Usage: /ip firewall filter [print|add|remove|enable|disable]\n" + ColorReset } } func (e *Executor) handleFirewallNAT(parts []string, user *auth.PublicUser) string { if len(parts) == 0 || parts[0] == "print" || parts[0] == "pr" { rules := e.fwMgr.ListNATRules() tbl := NewTable("FLAGS", "CHAIN", "ACTION", "OUT-IFACE", "TO-ADDRESSES", "TO-PORTS", "COMMENT") for _, r := range rules { flags := "" if r.Disabled { flags += "X" } else { flags += " " } tbl.AddRow(flags, r.Chain, r.Action, r.OutInterface, r.ToAddresses, r.ToPorts, r.Comment) } return tbl.String() } if user.Role == auth.RoleRead { return ColorRed + "Permission denied: viewer role cannot modify firewall\n" + ColorReset } sub := strings.ToLower(parts[0]) switch sub { case "add": _, params := parseArgs(parts[1:]) chain := params["chain"] action := params["action"] if chain == "" || action == "" { return ColorRed + "Usage: /ip firewall nat add chain= action= [out-interface=...] [to-addresses=...]\n" + ColorReset } r := firewall.NATRule{ Chain: chain, Action: action, OutInterface: params["out-interface"], ToAddresses: params["to-addresses"], ToPorts: params["to-ports"], Comment: params["comment"], Disabled: false, } if err := e.fwMgr.SaveNATRule(r); err != nil { return ColorRed + "Error adding NAT rule: " + err.Error() + ColorReset + "\n" } return ColorGreen + "Firewall NAT rule added.\n" + ColorReset case "remove", "rem", "delete": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /ip firewall nat remove \n" + ColorReset } if err := e.fwMgr.DeleteNATRule(pos[0]); err != nil { return ColorRed + "Error: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("NAT rule %s removed.\n", pos[0]) + ColorReset case "enable": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /ip firewall nat enable \n" + ColorReset } if err := e.fwMgr.SetNATRuleState(pos[0], false); err != nil { return ColorRed + "Error: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("NAT rule %s enabled.\n", pos[0]) + ColorReset case "disable": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /ip firewall nat disable \n" + ColorReset } if err := e.fwMgr.SetNATRuleState(pos[0], true); err != nil { return ColorRed + "Error: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("NAT rule %s disabled.\n", pos[0]) + ColorReset default: return ColorRed + "Usage: /ip firewall nat [print|add|remove|enable|disable]\n" + ColorReset } } func (e *Executor) handleFirewallMangle(parts []string, user *auth.PublicUser) string { if len(parts) == 0 || parts[0] == "print" || parts[0] == "pr" { rules := e.fwMgr.ListMangleRules() tbl := NewTable("FLAGS", "CHAIN", "ACTION", "PASSTHROUGH", "COMMENT") for _, r := range rules { flags := "" if r.Disabled { flags += "X" } else { flags += " " } tbl.AddRow(flags, r.Chain, r.Action, fmt.Sprintf("%t", r.Passthrough), r.Comment) } return tbl.String() } if user.Role == auth.RoleRead { return ColorRed + "Permission denied\n" + ColorReset } sub := strings.ToLower(parts[0]) switch sub { case "add": _, params := parseArgs(parts[1:]) chain := params["chain"] action := params["action"] if chain == "" || action == "" { return ColorRed + "Usage: /ip firewall mangle add chain=... action=...\n" + ColorReset } r := firewall.MangleRule{ Chain: chain, Action: action, Passthrough: true, Comment: params["comment"], Disabled: false, } if err := e.fwMgr.SaveMangleRule(r); err != nil { return ColorRed + "Error: " + err.Error() + ColorReset + "\n" } return ColorGreen + "Mangle rule added.\n" + ColorReset case "remove", "rem", "delete": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /ip firewall mangle remove \n" + ColorReset } if err := e.fwMgr.DeleteMangleRule(pos[0]); err != nil { return ColorRed + "Error: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("Mangle rule %s removed.\n", pos[0]) + ColorReset default: return ColorRed + "Usage: /ip firewall mangle [print|add|remove]\n" + ColorReset } } func (e *Executor) handleFirewallAddressList(parts []string, user *auth.PublicUser) string { if len(parts) == 0 || parts[0] == "print" || parts[0] == "pr" { entries := e.fwMgr.ListAddressLists() tbl := NewTable("FLAGS", "LIST", "ADDRESS", "COMMENT") for _, entry := range entries { flags := "" if entry.Disabled { flags += "X" } else { flags += " " } tbl.AddRow(flags, entry.Name, entry.Address, entry.Comment) } return tbl.String() } if user.Role == auth.RoleRead { return ColorRed + "Permission denied\n" + ColorReset } sub := strings.ToLower(parts[0]) switch sub { case "add": _, params := parseArgs(parts[1:]) list := params["list"] addr := params["address"] if list == "" || addr == "" { return ColorRed + "Usage: /ip firewall address-list add list= address= [comment=...]\n" + ColorReset } entry := firewall.AddressListEntry{ Name: list, Address: addr, Comment: params["comment"], Disabled: false, } if err := e.fwMgr.SaveAddressListEntry(entry); err != nil { return ColorRed + "Error: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("Added %s to address list %s.\n", addr, list) + ColorReset case "remove", "rem", "delete": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /ip firewall address-list remove \n" + ColorReset } if err := e.fwMgr.DeleteAddressListEntry(pos[0]); err != nil { return ColorRed + "Error: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("Address list entry %s removed.\n", pos[0]) + ColorReset default: return ColorRed + "Usage: /ip firewall address-list [print|add|remove]\n" + ColorReset } } // --- IP Service --- func (e *Executor) handleIPService(parts []string, user *auth.PublicUser) string { if e.servicesMgr == nil { return ColorRed + "Services manager not available.\n" + ColorReset } if len(parts) == 0 || parts[0] == "print" || parts[0] == "pr" { svcs := e.servicesMgr.ListServices() tbl := NewTable("NAME", "PORT", "DISABLED", "CERTIFICATE", "COMMENT") for _, s := range svcs { disStr := ColorGreen + "no" + ColorReset if s.Disabled { disStr = ColorRed + "yes" + ColorReset } certStr := "-" if s.Certificate != "" { certStr = ColorCyan + s.Certificate + ColorReset } tbl.AddRow(s.Name, fmt.Sprintf("%d", s.Port), disStr, certStr, s.Comment) } return tbl.String() } if user.Role != auth.RoleFull { return ColorRed + "Permission denied: admin role required to manage services\n" + ColorReset } sub := strings.ToLower(parts[0]) switch sub { case "enable": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /ip service enable \n" + ColorReset } name := pos[0] if err := e.servicesMgr.SetServiceState(name, false); err != nil { return ColorRed + "Error: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("Service %s enabled.\n", name) + ColorReset case "disable": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /ip service disable \n" + ColorReset } name := pos[0] if err := e.servicesMgr.SetServiceState(name, true); err != nil { return ColorRed + "Error: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("Service %s disabled.\n", name) + ColorReset case "set": pos, params := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /ip service set [port=] [disabled=yes|no] [certificate=]\n" + ColorReset } name := pos[0] svc, ok := e.servicesMgr.GetService(name) if !ok { return ColorRed + fmt.Sprintf("Service %q not found\n", name) + ColorReset } port := svc.Port disabled := svc.Disabled certName := svc.Certificate if pStr, ok := params["port"]; ok { if pVal, err := strconv.Atoi(pStr); err == nil && pVal > 0 && pVal < 65536 { port = pVal } } if dStr, ok := params["disabled"]; ok { disabled = (strings.ToLower(dStr) == "yes" || strings.ToLower(dStr) == "true") } if cStr, ok := params["certificate"]; ok { certName = cStr } if err := e.servicesMgr.SetService(name, port, disabled, certName); err != nil { return ColorRed + "Error: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("Service %s updated (port=%d, disabled=%t, certificate=%q).\n", name, port, disabled, certName) + ColorReset default: return ColorRed + "Usage: /ip service [print|enable|disable|set]\n" + ColorReset } } // ==================== DYNAMIC ROUTING ==================== func (e *Executor) handleRouting(parts []string, user *auth.PublicUser) string { if e.dynRoutingMgr == nil { return ColorRed + "Dynamic routing subsystem not available.\n" + ColorReset } if len(parts) == 0 { return ColorYellow + "Routing subsystem. Available: bgp, ospf, rip, bfd, babel, radv, filter\n" + ColorReset } sub := strings.ToLower(parts[0]) switch sub { case "bgp": return e.handleRoutingBGP(parts[1:], user) case "ospf": return e.handleRoutingOSPF(parts[1:], user) case "rip": return e.handleRoutingRIP(parts[1:], user) case "bfd": return e.handleRoutingBFD(parts[1:], user) case "babel": return e.handleRoutingBabel(parts[1:], user) case "radv": return e.handleRoutingRAdv(parts[1:], user) case "filter": return e.handleRoutingFilter(parts[1:], user) default: return ColorRed + "Unknown routing protocol: " + sub + "\n" + ColorReset } } func (e *Executor) handleRoutingBGP(parts []string, user *auth.PublicUser) string { if len(parts) == 0 { return e.handleRoutingBGPInstance([]string{"print"}, user) } sub := strings.ToLower(parts[0]) switch sub { case "instance": return e.handleRoutingBGPInstance(parts[1:], user) case "peer", "connection", "peers": return e.handleRoutingBGPPeer(parts[1:], user) case "network", "networks": return e.handleRoutingBGPNetwork(parts[1:], user) case "routes", "route": routes := e.dynRoutingMgr.GetBGPRoutes() tbl := NewTable("PREFIX", "NEXT-HOP", "PATH", "METRIC", "WEIGHT") for _, r := range routes { tbl.AddRow(r.Prefix, r.NextHop, r.Path, fmt.Sprintf("%d", r.Metric), fmt.Sprintf("%d", r.Weight)) } return tbl.String() default: return ColorRed + "Usage: /routing bgp [instance|peer|network|routes]\n" + ColorReset } } func (e *Executor) handleRoutingBGPInstance(parts []string, user *auth.PublicUser) string { if len(parts) == 0 || parts[0] == "print" || parts[0] == "pr" { inst := e.dynRoutingMgr.GetBGPInstance() out := " /routing bgp instance\n" out += fmt.Sprintf(" enabled: %t\n", inst.Enabled) out += fmt.Sprintf(" as: %d\n", inst.AS) out += fmt.Sprintf(" router-id: %s\n", inst.RouterID) out += fmt.Sprintf(" hold-time: %d\n", inst.HoldTime) out += fmt.Sprintf(" keepalive-time: %d\n", inst.KeepaliveTime) out += fmt.Sprintf(" redistribute-connected: %t\n", inst.RedistributeConnected) out += fmt.Sprintf(" redistribute-static: %t\n", inst.RedistributeStatic) out += fmt.Sprintf(" redistribute-ospf: %t\n", inst.RedistributeOSPF) out += fmt.Sprintf(" running: %t\n", inst.Running) return out } if user.Role == auth.RoleRead { return ColorRed + "Permission denied: viewer role cannot modify BGP\n" + ColorReset } sub := strings.ToLower(parts[0]) switch sub { case "set": _, params := parseArgs(parts[1:]) inst := e.dynRoutingMgr.GetBGPInstance() if v, ok := params["as"]; ok { var as uint32 fmt.Sscanf(v, "%d", &as) if as > 0 { inst.AS = as } } if v, ok := params["router-id"]; ok { inst.RouterID = strings.TrimSpace(v) } if v, ok := params["enabled"]; ok { inst.Enabled = parseCLIEnableDisable(v, inst.Enabled) } if v, ok := params["hold-time"]; ok { fmt.Sscanf(v, "%d", &inst.HoldTime) } if v, ok := params["keepalive-time"]; ok { fmt.Sscanf(v, "%d", &inst.KeepaliveTime) } if v, ok := params["redistribute-connected"]; ok { inst.RedistributeConnected = parseCLIEnableDisable(v, inst.RedistributeConnected) } if v, ok := params["redistribute-static"]; ok { inst.RedistributeStatic = parseCLIEnableDisable(v, inst.RedistributeStatic) } if v, ok := params["redistribute-ospf"]; ok { inst.RedistributeOSPF = parseCLIEnableDisable(v, inst.RedistributeOSPF) } if err := e.dynRoutingMgr.UpdateBGPInstance(inst); err != nil { return ColorRed + "Error updating BGP instance: " + err.Error() + "\n" + ColorReset } return ColorGreen + "BGP instance updated.\n" + ColorReset default: return ColorRed + "Usage: /routing bgp instance [print|set]\n" + ColorReset } } func (e *Executor) handleRoutingBGPPeer(parts []string, user *auth.PublicUser) string { if len(parts) == 0 || parts[0] == "print" || parts[0] == "pr" { peers := e.dynRoutingMgr.ListBGPPeers() tbl := NewTable("#", "FLAGS", "NAME", "REMOTE-AS", "REMOTE-ADDRESS", "LOCAL-AS", "BFD", "STATUS", "UPTIME") for idx, p := range peers { flags := "" if p.Disabled { flags += "X" } else { flags += "E" } st := p.State if st == "" { st = "Active" } bfdStr := "no" if p.UseBFD { bfdStr = "yes" } localASStr := "-" if p.LocalAS > 0 { localASStr = fmt.Sprintf("%d", p.LocalAS) } tbl.AddRow(fmt.Sprintf("%d", idx), flags, p.Name, fmt.Sprintf("%d", p.RemoteAS), p.RemoteAddress, localASStr, bfdStr, st, p.Uptime) } return tbl.String() } if user.Role == auth.RoleRead { return ColorRed + "Permission denied: viewer role cannot modify BGP\n" + ColorReset } sub := strings.ToLower(parts[0]) switch sub { case "add": _, params := parseArgs(parts[1:]) name := params["name"] remoteAddr := params["remote-address"] remoteASStr := params["remote-as"] if remoteAddr == "" || remoteASStr == "" { return ColorRed + "Usage: /routing bgp peer add name= remote-address= remote-as= [use-bfd=yes/no] [local-as=] [local-address=] [multihop=yes/no]\n" + ColorReset } var remoteAS uint32 fmt.Sscanf(remoteASStr, "%d", &remoteAS) if remoteAS == 0 { return ColorRed + "Invalid remote-as\n" + ColorReset } if name == "" { name = fmt.Sprintf("peer-%s", remoteAddr) } peer := dynamicrouting.BGPPeer{ Name: name, RemoteAddress: remoteAddr, RemoteAS: remoteAS, Enabled: true, Disabled: false, Comment: params["comment"], InFilter: params["in-filter"], OutFilter: params["out-filter"], Password: params["password"], } if v, ok := params["local-as"]; ok { fmt.Sscanf(v, "%d", &peer.LocalAS) } if v, ok := params["local-address"]; ok { peer.LocalAddress = v } if v, ok := params["use-bfd"]; ok { peer.UseBFD = parseCLIEnableDisable(v, false) } if v, ok := params["multihop"]; ok { peer.Multihop = parseCLIEnableDisable(v, false) } if v, ok := params["disabled"]; ok { peer.Disabled = parseCLIEnableDisable(v, false) } if err := e.dynRoutingMgr.SaveBGPPeer(&peer); err != nil { return ColorRed + "Error adding BGP peer: " + err.Error() + "\n" + ColorReset } return ColorGreen + fmt.Sprintf("BGP peer %s (%s, AS%d) added.\n", peer.Name, peer.RemoteAddress, peer.RemoteAS) + ColorReset case "remove", "rem", "delete": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /routing bgp peer remove <#|name|id>\n" + ColorReset } target := pos[0] peers := e.dynRoutingMgr.ListBGPPeers() var peerID string if idx, err := strconv.Atoi(target); err == nil && idx >= 0 && idx < len(peers) { peerID = peers[idx].ID } else { for _, p := range peers { if strings.EqualFold(p.Name, target) || p.ID == target || p.RemoteAddress == target { peerID = p.ID break } } } if peerID == "" { return ColorRed + "BGP peer not found: " + target + "\n" + ColorReset } if err := e.dynRoutingMgr.DeleteBGPPeer(peerID); err != nil { return ColorRed + "Error: " + err.Error() + "\n" + ColorReset } return ColorGreen + fmt.Sprintf("BGP peer %s removed.\n", target) + ColorReset case "enable": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /routing bgp peer enable <#|name|id>\n" + ColorReset } target := pos[0] peers := e.dynRoutingMgr.ListBGPPeers() var peerID string if idx, err := strconv.Atoi(target); err == nil && idx >= 0 && idx < len(peers) { peerID = peers[idx].ID } else { for _, p := range peers { if strings.EqualFold(p.Name, target) || p.ID == target || p.RemoteAddress == target { peerID = p.ID break } } } if peerID == "" { return ColorRed + "BGP peer not found: " + target + "\n" + ColorReset } if err := e.dynRoutingMgr.SetBGPPeerState(peerID, false); err != nil { return ColorRed + "Error: " + err.Error() + "\n" + ColorReset } return ColorGreen + fmt.Sprintf("BGP peer %s enabled.\n", target) + ColorReset case "disable": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /routing bgp peer disable <#|name|id>\n" + ColorReset } target := pos[0] peers := e.dynRoutingMgr.ListBGPPeers() var peerID string if idx, err := strconv.Atoi(target); err == nil && idx >= 0 && idx < len(peers) { peerID = peers[idx].ID } else { for _, p := range peers { if strings.EqualFold(p.Name, target) || p.ID == target || p.RemoteAddress == target { peerID = p.ID break } } } if peerID == "" { return ColorRed + "BGP peer not found: " + target + "\n" + ColorReset } if err := e.dynRoutingMgr.SetBGPPeerState(peerID, true); err != nil { return ColorRed + "Error: " + err.Error() + "\n" + ColorReset } return ColorGreen + fmt.Sprintf("BGP peer %s disabled.\n", target) + ColorReset default: return ColorRed + "Usage: /routing bgp peer [print|add|remove|enable|disable]\n" + ColorReset } } func (e *Executor) handleRoutingBGPNetwork(parts []string, user *auth.PublicUser) string { if len(parts) == 0 || parts[0] == "print" || parts[0] == "pr" { nets := e.dynRoutingMgr.ListBGPNetworks() tbl := NewTable("#", "NETWORK", "COMMENT") for idx, n := range nets { tbl.AddRow(fmt.Sprintf("%d", idx), n.Network, n.Comment) } return tbl.String() } if user.Role == auth.RoleRead { return ColorRed + "Permission denied: viewer role cannot modify BGP\n" + ColorReset } sub := strings.ToLower(parts[0]) switch sub { case "add": _, params := parseArgs(parts[1:]) network := params["network"] if network == "" { return ColorRed + "Usage: /routing bgp network add network= [comment=...]\n" + ColorReset } n := dynamicrouting.BGPNetwork{ Network: network, Comment: params["comment"], } if err := e.dynRoutingMgr.SaveBGPNetwork(n); err != nil { return ColorRed + "Error adding network: " + err.Error() + "\n" + ColorReset } return ColorGreen + fmt.Sprintf("BGP network %s added.\n", network) + ColorReset case "remove", "rem", "delete": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /routing bgp network remove <#|cidr|id>\n" + ColorReset } target := pos[0] nets := e.dynRoutingMgr.ListBGPNetworks() var netID string if idx, err := strconv.Atoi(target); err == nil && idx >= 0 && idx < len(nets) { netID = nets[idx].ID } else { for _, n := range nets { if n.Network == target || n.ID == target { netID = n.ID break } } } if netID == "" { return ColorRed + "BGP network not found: " + target + "\n" + ColorReset } if err := e.dynRoutingMgr.DeleteBGPNetwork(netID); err != nil { return ColorRed + "Error: " + err.Error() + "\n" + ColorReset } return ColorGreen + fmt.Sprintf("BGP network %s removed.\n", target) + ColorReset default: return ColorRed + "Usage: /routing bgp network [print|add|remove]\n" + ColorReset } } // ==================== RIP PROTOCOL ==================== func (e *Executor) handleRoutingRIP(parts []string, user *auth.PublicUser) string { if len(parts) == 0 { return e.handleRIPInstance([]string{"print"}, user) } sub := strings.ToLower(parts[0]) switch sub { case "instance", "instances": return e.handleRIPInstance(parts[1:], user) case "interface", "interfaces": return e.handleRIPInterface(parts[1:], user) case "neighbor", "neighbors": return e.handleRIPNeighbor(parts[1:], user) case "routes", "route": routes := e.dynRoutingMgr.GetRIPRoutes() tbl := NewTable("PREFIX", "NEXT-HOP", "INTERFACE", "METRIC") for _, r := range routes { tbl.AddRow(r.Prefix, r.NextHop, r.Interface, fmt.Sprintf("%d", r.Metric)) } return tbl.String() default: return ColorRed + "Usage: /routing rip [instance|interface|neighbor|routes]\n" + ColorReset } } func (e *Executor) handleRIPInstance(parts []string, user *auth.PublicUser) string { if len(parts) == 0 || parts[0] == "print" || parts[0] == "pr" { instances := e.dynRoutingMgr.ListRIPInstances() tbl := NewTable("#", "NAME", "VERSION", "REDISTRIBUTE", "UPDATE-TIME", "STATUS") for idx, inst := range instances { st := "enabled" if inst.Disabled { st = "disabled" } var redist []string if inst.RedistributeConnected { redist = append(redist, "connected") } if inst.RedistributeStatic { redist = append(redist, "static") } if inst.RedistributeOSPF { redist = append(redist, "ospf") } if inst.RedistributeBGP { redist = append(redist, "bgp") } if inst.RedistributeDefault { redist = append(redist, "default") } redistStr := "-" if len(redist) > 0 { redistStr = strings.Join(redist, ",") } tbl.AddRow(fmt.Sprintf("%d", idx), inst.Name, fmt.Sprintf("%d", inst.Version), redistStr, fmt.Sprintf("%ds", inst.UpdateTime), st) } return tbl.String() } if user.Role == auth.RoleRead { return ColorRed + "Permission denied: viewer role cannot modify RIP\n" + ColorReset } sub := strings.ToLower(parts[0]) switch sub { case "add": _, params := parseArgs(parts[1:]) name := params["name"] if name == "" { name = "default" } ver := 2 if v, ok := params["version"]; ok { fmt.Sscanf(v, "%d", &ver) } inst := dynamicrouting.RIPInstance{ Name: name, Version: ver, Enabled: true, Disabled: false, RedistributeConnected: parseCLIEnableDisable(params["redistribute-connected"], false), RedistributeStatic: parseCLIEnableDisable(params["redistribute-static"], false), RedistributeOSPF: parseCLIEnableDisable(params["redistribute-ospf"], false), RedistributeBGP: parseCLIEnableDisable(params["redistribute-bgp"], false), RedistributeDefault: parseCLIEnableDisable(params["redistribute-default"], false), UpdateTime: 30, TimeoutTime: 180, } if v, ok := params["update-time"]; ok { fmt.Sscanf(v, "%d", &inst.UpdateTime) } if v, ok := params["timeout-time"]; ok { fmt.Sscanf(v, "%d", &inst.TimeoutTime) } if v, ok := params["default-metric"]; ok { fmt.Sscanf(v, "%d", &inst.DefaultMetric) } if _, err := e.dynRoutingMgr.SaveRIPInstance(inst); err != nil { return ColorRed + "Error adding RIP instance: " + err.Error() + "\n" + ColorReset } return ColorGreen + fmt.Sprintf("RIP instance %s added.\n", name) + ColorReset case "remove", "rem", "delete": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /routing rip instance remove <#|name|id>\n" + ColorReset } target := pos[0] instances := e.dynRoutingMgr.ListRIPInstances() var instID string if idx, err := strconv.Atoi(target); err == nil && idx >= 0 && idx < len(instances) { instID = instances[idx].ID } else { for _, inst := range instances { if strings.EqualFold(inst.Name, target) || inst.ID == target { instID = inst.ID break } } } if instID == "" { return ColorRed + "RIP instance not found: " + target + "\n" + ColorReset } if err := e.dynRoutingMgr.DeleteRIPInstance(instID); err != nil { return ColorRed + "Error: " + err.Error() + "\n" + ColorReset } return ColorGreen + fmt.Sprintf("RIP instance %s removed.\n", target) + ColorReset case "enable": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /routing rip instance enable <#|name|id>\n" + ColorReset } target := pos[0] if err := e.dynRoutingMgr.SetRIPInstanceState(target, false); err != nil { return ColorRed + "Error: " + err.Error() + "\n" + ColorReset } return ColorGreen + fmt.Sprintf("RIP instance %s enabled.\n", target) + ColorReset case "disable": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /routing rip instance disable <#|name|id>\n" + ColorReset } target := pos[0] if err := e.dynRoutingMgr.SetRIPInstanceState(target, true); err != nil { return ColorRed + "Error: " + err.Error() + "\n" + ColorReset } return ColorGreen + fmt.Sprintf("RIP instance %s disabled.\n", target) + ColorReset default: return ColorRed + "Usage: /routing rip instance [print|add|remove|enable|disable]\n" + ColorReset } } func (e *Executor) handleRIPInterface(parts []string, user *auth.PublicUser) string { if len(parts) == 0 || parts[0] == "print" || parts[0] == "pr" { ifaces := e.dynRoutingMgr.ListRIPInterfaces() tbl := NewTable("#", "INTERFACE", "INSTANCE", "METRIC", "MODE", "PASSIVE", "BFD", "STATUS") for idx, iface := range ifaces { st := "enabled" if iface.Disabled { st = "disabled" } bfdStr := "no" if iface.UseBFD { bfdStr = "yes" } passStr := "no" if iface.Passive { passStr = "yes" } mode := iface.Mode if mode == "" { mode = "multicast" } tbl.AddRow(fmt.Sprintf("%d", idx), iface.Interface, iface.Instance, fmt.Sprintf("%d", iface.Metric), mode, passStr, bfdStr, st) } return tbl.String() } if user.Role == auth.RoleRead { return ColorRed + "Permission denied: viewer role cannot modify RIP\n" + ColorReset } sub := strings.ToLower(parts[0]) switch sub { case "add": _, params := parseArgs(parts[1:]) ifaceName := params["interface"] if ifaceName == "" { ifaceName = "*" } instName := params["instance"] if instName == "" { instName = "default" } metric := 1 if v, ok := params["metric"]; ok { fmt.Sscanf(v, "%d", &metric) } mode := params["mode"] if mode == "" { mode = "multicast" } iface := dynamicrouting.RIPInterface{ Interface: ifaceName, Instance: instName, Metric: metric, Mode: mode, Passive: parseCLIEnableDisable(params["passive"], false), UseBFD: parseCLIEnableDisable(params["use-bfd"], false), Auth: params["auth"], AuthKey: params["auth-key"], } if _, err := e.dynRoutingMgr.SaveRIPInterface(iface); err != nil { return ColorRed + "Error adding RIP interface: " + err.Error() + "\n" + ColorReset } return ColorGreen + fmt.Sprintf("RIP interface %s added.\n", ifaceName) + ColorReset case "remove", "rem", "delete": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /routing rip interface remove <#|iface|id>\n" + ColorReset } target := pos[0] ifaces := e.dynRoutingMgr.ListRIPInterfaces() var ifaceID string if idx, err := strconv.Atoi(target); err == nil && idx >= 0 && idx < len(ifaces) { ifaceID = ifaces[idx].ID } else { for _, iface := range ifaces { if iface.Interface == target || iface.ID == target { ifaceID = iface.ID break } } } if ifaceID == "" { return ColorRed + "RIP interface not found: " + target + "\n" + ColorReset } if err := e.dynRoutingMgr.DeleteRIPInterface(ifaceID); err != nil { return ColorRed + "Error: " + err.Error() + "\n" + ColorReset } return ColorGreen + fmt.Sprintf("RIP interface %s removed.\n", target) + ColorReset case "enable": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /routing rip interface enable <#|iface|id>\n" + ColorReset } target := pos[0] if err := e.dynRoutingMgr.SetRIPInterfaceState(target, false); err != nil { return ColorRed + "Error: " + err.Error() + "\n" + ColorReset } return ColorGreen + fmt.Sprintf("RIP interface %s enabled.\n", target) + ColorReset case "disable": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /routing rip interface disable <#|iface|id>\n" + ColorReset } target := pos[0] if err := e.dynRoutingMgr.SetRIPInterfaceState(target, true); err != nil { return ColorRed + "Error: " + err.Error() + "\n" + ColorReset } return ColorGreen + fmt.Sprintf("RIP interface %s disabled.\n", target) + ColorReset default: return ColorRed + "Usage: /routing rip interface [print|add|remove|enable|disable]\n" + ColorReset } } func (e *Executor) handleRIPNeighbor(parts []string, user *auth.PublicUser) string { if len(parts) == 0 || parts[0] == "print" || parts[0] == "pr" { liveNbrs := e.dynRoutingMgr.GetRIPNeighbors() tbl := NewTable("IP-ADDRESS", "INTERFACE", "LAST-SEEN") for _, n := range liveNbrs { tbl.AddRow(n.Address, n.Interface, n.LastSeen) } cfgNbrs := e.dynRoutingMgr.ListRIPNeighbors() if len(cfgNbrs) > 0 { tblStatic := NewTable("#", "CONFIGURED-NEIGHBOR", "INSTANCE") for idx, n := range cfgNbrs { tblStatic.AddRow(fmt.Sprintf("%d", idx), n.Address, n.Instance) } return tbl.String() + "\n" + tblStatic.String() } return tbl.String() } if user.Role == auth.RoleRead { return ColorRed + "Permission denied: viewer role cannot modify RIP\n" + ColorReset } sub := strings.ToLower(parts[0]) switch sub { case "add": _, params := parseArgs(parts[1:]) addr := params["address"] if addr == "" { return ColorRed + "Usage: /routing rip neighbor add address= [instance=]\n" + ColorReset } nbr := dynamicrouting.RIPNeighbor{ Address: addr, Instance: params["instance"], } if _, err := e.dynRoutingMgr.SaveRIPNeighbor(nbr); err != nil { return ColorRed + "Error: " + err.Error() + "\n" + ColorReset } return ColorGreen + fmt.Sprintf("RIP neighbor %s added.\n", addr) + ColorReset case "remove", "rem", "delete": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /routing rip neighbor remove <#|ip|id>\n" + ColorReset } target := pos[0] nbrs := e.dynRoutingMgr.ListRIPNeighbors() var nbrID string if idx, err := strconv.Atoi(target); err == nil && idx >= 0 && idx < len(nbrs) { nbrID = nbrs[idx].ID } else { for _, n := range nbrs { if n.Address == target || n.ID == target { nbrID = n.ID break } } } if nbrID == "" { return ColorRed + "RIP neighbor not found: " + target + "\n" + ColorReset } if err := e.dynRoutingMgr.DeleteRIPNeighbor(nbrID); err != nil { return ColorRed + "Error: " + err.Error() + "\n" + ColorReset } return ColorGreen + fmt.Sprintf("RIP neighbor %s removed.\n", target) + ColorReset default: return ColorRed + "Usage: /routing rip neighbor [print|add|remove]\n" + ColorReset } } // ==================== BFD PROTOCOL ==================== func (e *Executor) handleRoutingBFD(parts []string, user *auth.PublicUser) string { if len(parts) == 0 { return e.handleBFDInstance([]string{"print"}, user) } sub := strings.ToLower(parts[0]) switch sub { case "instance": return e.handleBFDInstance(parts[1:], user) case "interface", "interfaces": return e.handleBFDInterface(parts[1:], user) case "peer", "peers", "neighbor", "neighbors": return e.handleBFDNeighbor(parts[1:], user) case "session", "sessions": sessions := e.dynRoutingMgr.GetBFDSessions() tbl := NewTable("IP-ADDRESS", "INTERFACE", "STATE", "SINCE", "INTERVAL", "TIMEOUT") for _, s := range sessions { st := s.State if strings.EqualFold(st, "Up") { st = ColorGreen + st + ColorReset } else if strings.EqualFold(st, "Down") { st = ColorRed + st + ColorReset } tbl.AddRow(s.IP, s.Interface, st, s.Since, s.Interval, s.Timeout) } return tbl.String() default: return ColorRed + "Usage: /routing bfd [instance|interface|peer|session]\n" + ColorReset } } func (e *Executor) handleBFDInstance(parts []string, user *auth.PublicUser) string { if len(parts) == 0 || parts[0] == "print" || parts[0] == "pr" { inst := e.dynRoutingMgr.GetBFDInstance() out := " /routing bfd instance\n" out += fmt.Sprintf(" enabled: %t\n", inst.Enabled) out += fmt.Sprintf(" disabled: %t\n", inst.Disabled) out += fmt.Sprintf(" running: %t\n", inst.Running) return out } if user.Role == auth.RoleRead { return ColorRed + "Permission denied: viewer role cannot modify BFD\n" + ColorReset } sub := strings.ToLower(parts[0]) switch sub { case "set": _, params := parseArgs(parts[1:]) inst := e.dynRoutingMgr.GetBFDInstance() if v, ok := params["enabled"]; ok { inst.Enabled = parseCLIEnableDisable(v, inst.Enabled) inst.Disabled = !inst.Enabled } if v, ok := params["disabled"]; ok { inst.Disabled = parseCLIEnableDisable(v, inst.Disabled) inst.Enabled = !inst.Disabled } if err := e.dynRoutingMgr.SaveBFDInstance(inst); err != nil { return ColorRed + "Error: " + err.Error() + "\n" + ColorReset } return ColorGreen + "BFD instance updated.\n" + ColorReset default: return ColorRed + "Usage: /routing bfd instance [print|set]\n" + ColorReset } } func (e *Executor) handleBFDInterface(parts []string, user *auth.PublicUser) string { if len(parts) == 0 || parts[0] == "print" || parts[0] == "pr" { ifaces := e.dynRoutingMgr.ListBFDInterfaces() tbl := NewTable("#", "INTERFACE", "MIN-RX", "MIN-TX", "MULTIPLIER", "PASSIVE", "STATUS") for idx, iface := range ifaces { st := "enabled" if iface.Disabled { st = "disabled" } passStr := "no" if iface.Passive { passStr = "yes" } tbl.AddRow(fmt.Sprintf("%d", idx), iface.Interface, fmt.Sprintf("%dms", iface.MinRxInterval), fmt.Sprintf("%dms", iface.MinTxInterval), fmt.Sprintf("%d", iface.Multiplier), passStr, st) } return tbl.String() } if user.Role == auth.RoleRead { return ColorRed + "Permission denied: viewer role cannot modify BFD\n" + ColorReset } sub := strings.ToLower(parts[0]) switch sub { case "add": _, params := parseArgs(parts[1:]) ifaceName := params["interface"] if ifaceName == "" { ifaceName = "*" } minRx := 100 if v, ok := params["min-rx"]; ok { fmt.Sscanf(v, "%d", &minRx) } minTx := 100 if v, ok := params["min-tx"]; ok { fmt.Sscanf(v, "%d", &minTx) } mult := 3 if v, ok := params["multiplier"]; ok { fmt.Sscanf(v, "%d", &mult) } iface := dynamicrouting.BFDInterface{ Interface: ifaceName, MinRxInterval: minRx, MinTxInterval: minTx, Multiplier: mult, Passive: parseCLIEnableDisable(params["passive"], false), Auth: params["auth"], AuthKey: params["auth-key"], } if _, err := e.dynRoutingMgr.SaveBFDInterface(iface); err != nil { return ColorRed + "Error adding BFD interface: " + err.Error() + "\n" + ColorReset } return ColorGreen + fmt.Sprintf("BFD interface %s added.\n", ifaceName) + ColorReset case "remove", "rem", "delete": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /routing bfd interface remove <#|iface|id>\n" + ColorReset } target := pos[0] ifaces := e.dynRoutingMgr.ListBFDInterfaces() var ifaceID string if idx, err := strconv.Atoi(target); err == nil && idx >= 0 && idx < len(ifaces) { ifaceID = ifaces[idx].ID } else { for _, iface := range ifaces { if iface.Interface == target || iface.ID == target { ifaceID = iface.ID break } } } if ifaceID == "" { return ColorRed + "BFD interface not found: " + target + "\n" + ColorReset } if err := e.dynRoutingMgr.DeleteBFDInterface(ifaceID); err != nil { return ColorRed + "Error: " + err.Error() + "\n" + ColorReset } return ColorGreen + fmt.Sprintf("BFD interface %s removed.\n", target) + ColorReset case "enable": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /routing bfd interface enable <#|iface|id>\n" + ColorReset } target := pos[0] if err := e.dynRoutingMgr.SetBFDInterfaceState(target, false); err != nil { return ColorRed + "Error: " + err.Error() + "\n" + ColorReset } return ColorGreen + fmt.Sprintf("BFD interface %s enabled.\n", target) + ColorReset case "disable": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /routing bfd interface disable <#|iface|id>\n" + ColorReset } target := pos[0] if err := e.dynRoutingMgr.SetBFDInterfaceState(target, true); err != nil { return ColorRed + "Error: " + err.Error() + "\n" + ColorReset } return ColorGreen + fmt.Sprintf("BFD interface %s disabled.\n", target) + ColorReset default: return ColorRed + "Usage: /routing bfd interface [print|add|remove|enable|disable]\n" + ColorReset } } func (e *Executor) handleBFDNeighbor(parts []string, user *auth.PublicUser) string { if len(parts) == 0 || parts[0] == "print" || parts[0] == "pr" { nbrs := e.dynRoutingMgr.ListBFDNeighbors() tbl := NewTable("#", "ADDRESS", "INTERFACE", "MULTIHOP") for idx, n := range nbrs { mhop := "no" if n.Multihop { mhop = "yes" } tbl.AddRow(fmt.Sprintf("%d", idx), n.Address, n.Interface, mhop) } return tbl.String() } if user.Role == auth.RoleRead { return ColorRed + "Permission denied: viewer role cannot modify BFD\n" + ColorReset } sub := strings.ToLower(parts[0]) switch sub { case "add": _, params := parseArgs(parts[1:]) addr := params["address"] if addr == "" { return ColorRed + "Usage: /routing bfd peer add address= [interface=] [multihop=yes/no]\n" + ColorReset } nbr := dynamicrouting.BFDNeighbor{ Address: addr, Interface: params["interface"], Multihop: parseCLIEnableDisable(params["multihop"], false), } if _, err := e.dynRoutingMgr.SaveBFDNeighbor(nbr); err != nil { return ColorRed + "Error adding BFD peer: " + err.Error() + "\n" + ColorReset } return ColorGreen + fmt.Sprintf("BFD peer %s added.\n", addr) + ColorReset case "remove", "rem", "delete": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /routing bfd peer remove <#|ip|id>\n" + ColorReset } target := pos[0] nbrs := e.dynRoutingMgr.ListBFDNeighbors() var nbrID string if idx, err := strconv.Atoi(target); err == nil && idx >= 0 && idx < len(nbrs) { nbrID = nbrs[idx].ID } else { for _, n := range nbrs { if n.Address == target || n.ID == target { nbrID = n.ID break } } } if nbrID == "" { return ColorRed + "BFD peer not found: " + target + "\n" + ColorReset } if err := e.dynRoutingMgr.DeleteBFDNeighbor(nbrID); err != nil { return ColorRed + "Error: " + err.Error() + "\n" + ColorReset } return ColorGreen + fmt.Sprintf("BFD peer %s removed.\n", target) + ColorReset default: return ColorRed + "Usage: /routing bfd peer [print|add|remove]\n" + ColorReset } } // ==================== BABEL PROTOCOL ==================== func (e *Executor) handleRoutingBabel(parts []string, user *auth.PublicUser) string { if len(parts) == 0 { return e.handleBabelInstance([]string{"print"}, user) } sub := strings.ToLower(parts[0]) switch sub { case "instance", "instances": return e.handleBabelInstance(parts[1:], user) case "interface", "interfaces": return e.handleBabelInterface(parts[1:], user) case "neighbor", "neighbors": nbrs := e.dynRoutingMgr.GetBabelNeighbors() tbl := NewTable("IP-ADDRESS", "INTERFACE", "METRIC", "COST", "UPTIME") for _, n := range nbrs { tbl.AddRow(n.IP, n.Interface, fmt.Sprintf("%d", n.Metric), fmt.Sprintf("%d", n.Cost), n.Uptime) } return tbl.String() case "routes", "route": routes := e.dynRoutingMgr.GetBabelRoutes() tbl := NewTable("PREFIX", "NEXT-HOP", "INTERFACE", "METRIC") for _, r := range routes { tbl.AddRow(r.Prefix, r.NextHop, r.Interface, fmt.Sprintf("%d", r.Metric)) } return tbl.String() default: return ColorRed + "Usage: /routing babel [instance|interface|neighbor|routes]\n" + ColorReset } } func (e *Executor) handleBabelInstance(parts []string, user *auth.PublicUser) string { if len(parts) == 0 || parts[0] == "print" || parts[0] == "pr" { instances := e.dynRoutingMgr.ListBabelInstances() tbl := NewTable("#", "NAME", "ROUTER-ID", "REDISTRIBUTE", "STATUS") for idx, inst := range instances { st := "enabled" if inst.Disabled { st = "disabled" } var redist []string if inst.RedistributeConnected { redist = append(redist, "connected") } if inst.RedistributeStatic { redist = append(redist, "static") } if inst.RedistributeOSPF { redist = append(redist, "ospf") } if inst.RedistributeBGP { redist = append(redist, "bgp") } if inst.RedistributeDefault { redist = append(redist, "default") } redistStr := "-" if len(redist) > 0 { redistStr = strings.Join(redist, ",") } rID := inst.RouterID if rID == "" { rID = "auto" } tbl.AddRow(fmt.Sprintf("%d", idx), inst.Name, rID, redistStr, st) } return tbl.String() } if user.Role == auth.RoleRead { return ColorRed + "Permission denied: viewer role cannot modify Babel\n" + ColorReset } sub := strings.ToLower(parts[0]) switch sub { case "add": _, params := parseArgs(parts[1:]) name := params["name"] if name == "" { name = "default" } inst := dynamicrouting.BabelInstance{ Name: name, RouterID: params["router-id"], Enabled: true, Disabled: false, RedistributeConnected: parseCLIEnableDisable(params["redistribute-connected"], false), RedistributeStatic: parseCLIEnableDisable(params["redistribute-static"], false), RedistributeOSPF: parseCLIEnableDisable(params["redistribute-ospf"], false), RedistributeBGP: parseCLIEnableDisable(params["redistribute-bgp"], false), RedistributeDefault: parseCLIEnableDisable(params["redistribute-default"], false), } if _, err := e.dynRoutingMgr.SaveBabelInstance(inst); err != nil { return ColorRed + "Error: " + err.Error() + "\n" + ColorReset } return ColorGreen + fmt.Sprintf("Babel instance %s added.\n", name) + ColorReset case "remove", "rem", "delete": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /routing babel instance remove <#|name|id>\n" + ColorReset } target := pos[0] instances := e.dynRoutingMgr.ListBabelInstances() var instID string if idx, err := strconv.Atoi(target); err == nil && idx >= 0 && idx < len(instances) { instID = instances[idx].ID } else { for _, inst := range instances { if strings.EqualFold(inst.Name, target) || inst.ID == target { instID = inst.ID break } } } if instID == "" { return ColorRed + "Babel instance not found: " + target + "\n" + ColorReset } if err := e.dynRoutingMgr.DeleteBabelInstance(instID); err != nil { return ColorRed + "Error: " + err.Error() + "\n" + ColorReset } return ColorGreen + fmt.Sprintf("Babel instance %s removed.\n", target) + ColorReset case "enable": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /routing babel instance enable <#|name|id>\n" + ColorReset } target := pos[0] if err := e.dynRoutingMgr.SetBabelInstanceState(target, false); err != nil { return ColorRed + "Error: " + err.Error() + "\n" + ColorReset } return ColorGreen + fmt.Sprintf("Babel instance %s enabled.\n", target) + ColorReset case "disable": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /routing babel instance disable <#|name|id>\n" + ColorReset } target := pos[0] if err := e.dynRoutingMgr.SetBabelInstanceState(target, true); err != nil { return ColorRed + "Error: " + err.Error() + "\n" + ColorReset } return ColorGreen + fmt.Sprintf("Babel instance %s disabled.\n", target) + ColorReset default: return ColorRed + "Usage: /routing babel instance [print|add|remove|enable|disable]\n" + ColorReset } } func (e *Executor) handleBabelInterface(parts []string, user *auth.PublicUser) string { if len(parts) == 0 || parts[0] == "print" || parts[0] == "pr" { ifaces := e.dynRoutingMgr.ListBabelInterfaces() tbl := NewTable("#", "INTERFACE", "INSTANCE", "TYPE", "RXCOST", "HELLO-INT", "UPDATE-INT", "STATUS") for idx, iface := range ifaces { st := "enabled" if iface.Disabled { st = "disabled" } tStr := iface.Type if tStr == "" { tStr = "wired" } tbl.AddRow(fmt.Sprintf("%d", idx), iface.Interface, iface.Instance, tStr, fmt.Sprintf("%d", iface.RxCost), fmt.Sprintf("%ds", iface.HelloInterval), fmt.Sprintf("%ds", iface.UpdateInterval), st) } return tbl.String() } if user.Role == auth.RoleRead { return ColorRed + "Permission denied: viewer role cannot modify Babel\n" + ColorReset } sub := strings.ToLower(parts[0]) switch sub { case "add": _, params := parseArgs(parts[1:]) ifaceName := params["interface"] if ifaceName == "" { ifaceName = "*" } instName := params["instance"] if instName == "" { instName = "default" } tStr := params["type"] if tStr == "" { tStr = "wired" } rxcost := 10 if v, ok := params["rxcost"]; ok { fmt.Sscanf(v, "%d", &rxcost) } hello := 4 if v, ok := params["hello-interval"]; ok { fmt.Sscanf(v, "%d", &hello) } update := 16 if v, ok := params["update-interval"]; ok { fmt.Sscanf(v, "%d", &update) } iface := dynamicrouting.BabelInterface{ Interface: ifaceName, Instance: instName, Type: tStr, RxCost: rxcost, HelloInterval: hello, UpdateInterval: update, Auth: params["auth"], AuthKey: params["auth-key"], } if _, err := e.dynRoutingMgr.SaveBabelInterface(iface); err != nil { return ColorRed + "Error adding Babel interface: " + err.Error() + "\n" + ColorReset } return ColorGreen + fmt.Sprintf("Babel interface %s added.\n", ifaceName) + ColorReset case "remove", "rem", "delete": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /routing babel interface remove <#|iface|id>\n" + ColorReset } target := pos[0] ifaces := e.dynRoutingMgr.ListBabelInterfaces() var ifaceID string if idx, err := strconv.Atoi(target); err == nil && idx >= 0 && idx < len(ifaces) { ifaceID = ifaces[idx].ID } else { for _, iface := range ifaces { if iface.Interface == target || iface.ID == target { ifaceID = iface.ID break } } } if ifaceID == "" { return ColorRed + "Babel interface not found: " + target + "\n" + ColorReset } if err := e.dynRoutingMgr.DeleteBabelInterface(ifaceID); err != nil { return ColorRed + "Error: " + err.Error() + "\n" + ColorReset } return ColorGreen + fmt.Sprintf("Babel interface %s removed.\n", target) + ColorReset case "enable": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /routing babel interface enable <#|iface|id>\n" + ColorReset } target := pos[0] if err := e.dynRoutingMgr.SetBabelInterfaceState(target, false); err != nil { return ColorRed + "Error: " + err.Error() + "\n" + ColorReset } return ColorGreen + fmt.Sprintf("Babel interface %s enabled.\n", target) + ColorReset case "disable": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /routing babel interface disable <#|iface|id>\n" + ColorReset } target := pos[0] if err := e.dynRoutingMgr.SetBabelInterfaceState(target, true); err != nil { return ColorRed + "Error: " + err.Error() + "\n" + ColorReset } return ColorGreen + fmt.Sprintf("Babel interface %s disabled.\n", target) + ColorReset default: return ColorRed + "Usage: /routing babel interface [print|add|remove|enable|disable]\n" + ColorReset } } // ==================== RADV PROTOCOL ==================== func (e *Executor) handleRoutingRAdv(parts []string, user *auth.PublicUser) string { if len(parts) == 0 { return e.handleRAdvInterface([]string{"print"}, user) } sub := strings.ToLower(parts[0]) switch sub { case "instance", "instances": return e.handleRAdvInstance(parts[1:], user) case "interface", "interfaces": return e.handleRAdvInterface(parts[1:], user) default: return ColorRed + "Usage: /routing radv [instance|interface]\n" + ColorReset } } func (e *Executor) handleRAdvInstance(parts []string, user *auth.PublicUser) string { if len(parts) == 0 || parts[0] == "print" || parts[0] == "pr" { instances := e.dynRoutingMgr.ListRAdvInstances() tbl := NewTable("#", "NAME", "STATUS") for idx, inst := range instances { st := "enabled" if inst.Disabled { st = "disabled" } tbl.AddRow(fmt.Sprintf("%d", idx), inst.Name, st) } return tbl.String() } if user.Role == auth.RoleRead { return ColorRed + "Permission denied: viewer role cannot modify RAdv\n" + ColorReset } sub := strings.ToLower(parts[0]) switch sub { case "add": _, params := parseArgs(parts[1:]) name := params["name"] if name == "" { name = "default" } inst := dynamicrouting.RAdvInstance{ Name: name, Enabled: true, Disabled: false, } if _, err := e.dynRoutingMgr.SaveRAdvInstance(inst); err != nil { return ColorRed + "Error: " + err.Error() + "\n" + ColorReset } return ColorGreen + fmt.Sprintf("RAdv instance %s added.\n", name) + ColorReset case "remove", "rem", "delete": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /routing radv instance remove <#|name|id>\n" + ColorReset } target := pos[0] instances := e.dynRoutingMgr.ListRAdvInstances() var instID string if idx, err := strconv.Atoi(target); err == nil && idx >= 0 && idx < len(instances) { instID = instances[idx].ID } else { for _, inst := range instances { if strings.EqualFold(inst.Name, target) || inst.ID == target { instID = inst.ID break } } } if instID == "" { return ColorRed + "RAdv instance not found: " + target + "\n" + ColorReset } if err := e.dynRoutingMgr.DeleteRAdvInstance(instID); err != nil { return ColorRed + "Error: " + err.Error() + "\n" + ColorReset } return ColorGreen + fmt.Sprintf("RAdv instance %s removed.\n", target) + ColorReset case "enable": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /routing radv instance enable <#|name|id>\n" + ColorReset } target := pos[0] if err := e.dynRoutingMgr.SetRAdvInstanceState(target, false); err != nil { return ColorRed + "Error: " + err.Error() + "\n" + ColorReset } return ColorGreen + fmt.Sprintf("RAdv instance %s enabled.\n", target) + ColorReset case "disable": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /routing radv instance disable <#|name|id>\n" + ColorReset } target := pos[0] if err := e.dynRoutingMgr.SetRAdvInstanceState(target, true); err != nil { return ColorRed + "Error: " + err.Error() + "\n" + ColorReset } return ColorGreen + fmt.Sprintf("RAdv instance %s disabled.\n", target) + ColorReset default: return ColorRed + "Usage: /routing radv instance [print|add|remove|enable|disable]\n" + ColorReset } } func (e *Executor) handleRAdvInterface(parts []string, user *auth.PublicUser) string { if len(parts) == 0 || parts[0] == "print" || parts[0] == "pr" { ifaces := e.dynRoutingMgr.ListRAdvInterfaces() tbl := NewTable("#", "INTERFACE", "INSTANCE", "MIN-MAX-RA", "PREFIXES", "STATUS") for idx, iface := range ifaces { st := "enabled" if iface.Disabled { st = "disabled" } pfxStr := strings.Join(iface.Prefixes, ",") if pfxStr == "" { pfxStr = "-" } raInterval := fmt.Sprintf("%d-%ds", iface.MinRAInterval, iface.MaxRAInterval) tbl.AddRow(fmt.Sprintf("%d", idx), iface.Interface, iface.Instance, raInterval, pfxStr, st) } return tbl.String() } if user.Role == auth.RoleRead { return ColorRed + "Permission denied: viewer role cannot modify RAdv\n" + ColorReset } sub := strings.ToLower(parts[0]) switch sub { case "add": _, params := parseArgs(parts[1:]) ifaceName := params["interface"] if ifaceName == "" { ifaceName = "*" } instName := params["instance"] if instName == "" { instName = "default" } minRA := 200 if v, ok := params["min-ra-interval"]; ok { fmt.Sscanf(v, "%d", &minRA) } maxRA := 600 if v, ok := params["max-ra-interval"]; ok { fmt.Sscanf(v, "%d", &maxRA) } lifetime := 1800 if v, ok := params["default-lifetime"]; ok { fmt.Sscanf(v, "%d", &lifetime) } var pfxs []string if v, ok := params["prefixes"]; ok && v != "" { pfxs = strings.Split(v, ",") } var rdnss []string if v, ok := params["rdnss"]; ok && v != "" { rdnss = strings.Split(v, ",") } iface := dynamicrouting.RAdvInterface{ Interface: ifaceName, Instance: instName, MinRAInterval: minRA, MaxRAInterval: maxRA, DefaultLifetime: lifetime, Prefixes: pfxs, RDNSS: rdnss, } if _, err := e.dynRoutingMgr.SaveRAdvInterface(iface); err != nil { return ColorRed + "Error adding RAdv interface: " + err.Error() + "\n" + ColorReset } return ColorGreen + fmt.Sprintf("RAdv interface %s added.\n", ifaceName) + ColorReset case "remove", "rem", "delete": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /routing radv interface remove <#|iface|id>\n" + ColorReset } target := pos[0] ifaces := e.dynRoutingMgr.ListRAdvInterfaces() var ifaceID string if idx, err := strconv.Atoi(target); err == nil && idx >= 0 && idx < len(ifaces) { ifaceID = ifaces[idx].ID } else { for _, iface := range ifaces { if iface.Interface == target || iface.ID == target { ifaceID = iface.ID break } } } if ifaceID == "" { return ColorRed + "RAdv interface not found: " + target + "\n" + ColorReset } if err := e.dynRoutingMgr.DeleteRAdvInterface(ifaceID); err != nil { return ColorRed + "Error: " + err.Error() + "\n" + ColorReset } return ColorGreen + fmt.Sprintf("RAdv interface %s removed.\n", target) + ColorReset case "enable": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /routing radv interface enable <#|iface|id>\n" + ColorReset } target := pos[0] if err := e.dynRoutingMgr.SetRAdvInterfaceState(target, false); err != nil { return ColorRed + "Error: " + err.Error() + "\n" + ColorReset } return ColorGreen + fmt.Sprintf("RAdv interface %s enabled.\n", target) + ColorReset case "disable": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /routing radv interface disable <#|iface|id>\n" + ColorReset } target := pos[0] if err := e.dynRoutingMgr.SetRAdvInterfaceState(target, true); err != nil { return ColorRed + "Error: " + err.Error() + "\n" + ColorReset } return ColorGreen + fmt.Sprintf("RAdv interface %s disabled.\n", target) + ColorReset default: return ColorRed + "Usage: /routing radv interface [print|add|remove|enable|disable]\n" + ColorReset } } func parseCLIEnableDisable(val string, defaultVal bool) bool { v := strings.ToLower(strings.TrimSpace(val)) if v == "yes" || v == "true" || v == "1" || v == "enable" || v == "enabled" { return true } if v == "no" || v == "false" || v == "0" || v == "disable" || v == "disabled" { return false } return defaultVal } func (e *Executor) handleRoutingOSPF(parts []string, user *auth.PublicUser) string { if len(parts) == 0 { return e.handleOSPFInstance([]string{"print"}, user) } sub := strings.ToLower(parts[0]) switch sub { case "instance", "instances": return e.handleOSPFInstance(parts[1:], user) case "area", "areas": if len(parts) > 1 && (strings.EqualFold(parts[1], "range") || strings.EqualFold(parts[1], "ranges")) { return e.handleOSPFAreaRange(parts[2:], user) } return e.handleOSPFArea(parts[1:], user) case "area-range", "area-ranges", "range", "ranges": return e.handleOSPFAreaRange(parts[1:], user) case "interface", "interfaces", "template", "templates", "interface-template", "interface-templates": return e.handleOSPFInterface(parts[1:], user) case "static-neighbor", "static-neighbors": return e.handleOSPFStaticNeighbor(parts[1:], user) case "neighbor", "neighbors": return e.handleOSPFNeighborLive(parts[1:], user) case "lsa", "lsadb": return e.handleOSPFLSA(parts[1:], user) case "route", "routes": return e.handleOSPFRoute(parts[1:], user) default: return ColorRed + "Usage: /routing ospf [instance|area|area-range|interface|static-neighbor|neighbor|lsa|routes]\n" + ColorReset } } func (e *Executor) handleOSPFInstance(parts []string, user *auth.PublicUser) string { if len(parts) == 0 || parts[0] == "print" || parts[0] == "pr" { instances := e.dynRoutingMgr.ListOSPFInstances() tbl := NewTable("#", "NAME", "ROUTER-ID", "VERSION", "REDISTRIBUTE", "DEFAULT-METRIC", "STATUS") for idx, inst := range instances { st := "enabled" if inst.Disabled { st = "disabled" } var redist []string if inst.RedistributeConnected { redist = append(redist, "connected") } if inst.RedistributeStatic { redist = append(redist, "static") } if inst.RedistributeBGP { redist = append(redist, "bgp") } if inst.RedistributeDefault || (inst.OriginateDefault != "" && inst.OriginateDefault != "never") { mode := inst.OriginateDefault if mode == "" || mode == "never" { mode = "if-installed" } redist = append(redist, fmt.Sprintf("default(%s)", mode)) } redistStr := "-" if len(redist) > 0 { redistStr = strings.Join(redist, ",") } metricStr := "-" if inst.DefaultMetric > 0 { metricStr = fmt.Sprintf("%d", inst.DefaultMetric) } tbl.AddRow(fmt.Sprintf("%d", idx), inst.Name, inst.RouterID, fmt.Sprintf("%d", inst.Version), redistStr, metricStr, st) } return tbl.String() } if user.Role == auth.RoleRead { return ColorRed + "Permission denied: viewer role cannot modify OSPF\n" + ColorReset } sub := strings.ToLower(parts[0]) switch sub { case "add": _, params := parseArgs(parts[1:]) name := params["name"] if name == "" { name = "Default" } routerID := params["router-id"] version := 2 if vStr, ok := params["version"]; ok && vStr != "" { if v, err := strconv.Atoi(vStr); err == nil && (v == 2 || v == 3) { version = v } } redistConn := parseCLIEnableDisable(params["redistribute-connected"], false) redistStat := parseCLIEnableDisable(params["redistribute-static"], false) redistBGP := parseCLIEnableDisable(params["redistribute-bgp"], false) redistDef := parseCLIEnableDisable(params["redistribute-default"], false) origDef := strings.ToLower(strings.TrimSpace(params["originate-default"])) if origDef == "" { if redistDef { origDef = "if-installed" } else { origDef = "never" } } else if origDef == "if-installed" || origDef == "always" { redistDef = true } metric := 0 if mStr, ok := params["default-metric"]; ok && mStr != "" { metric, _ = strconv.Atoi(mStr) } disabled := parseCLIEnableDisable(params["disabled"], false) vrf := params["vrf"] if vrf == "" { vrf = "main" } rt := params["routing-table"] if rt == "" { rt = "main" } inst := dynamicrouting.OSPFInstance{ Name: name, Version: version, VRF: vrf, RouterID: routerID, RoutingTable: rt, Enabled: !disabled, Disabled: disabled, RedistributeConnected: redistConn, RedistributeStatic: redistStat, RedistributeBGP: redistBGP, RedistributeDefault: redistDef, OriginateDefault: origDef, DefaultMetric: metric, Comment: params["comment"], } if err := e.dynRoutingMgr.SaveOSPFInstance(&inst); err != nil { return ColorRed + "Error saving OSPF instance: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("OSPF instance %s saved.\n", name) + ColorReset case "set": pos, params := parseArgs(parts[1:]) if len(pos) == 0 && params["numbers"] == "" { return ColorRed + "Usage: /routing ospf instance set [router-id=...] [redistribute-connected=yes/no] [redistribute-static=yes/no] [redistribute-bgp=yes/no] [redistribute-default=yes/no] [originate-default=never|if-installed|always] ...\n" + ColorReset } target := "" if len(pos) > 0 { target = pos[0] } else { target = params["numbers"] } instances := e.dynRoutingMgr.ListOSPFInstances() var targetInst *dynamicrouting.OSPFInstance if idx, err := strconv.Atoi(target); err == nil && idx >= 0 && idx < len(instances) { targetInst = &instances[idx] } else { for i := range instances { if strings.EqualFold(instances[i].Name, target) || instances[i].ID == target { targetInst = &instances[i] break } } } if targetInst == nil { return ColorRed + fmt.Sprintf("OSPF instance %q not found\n", target) + ColorReset } if v, ok := params["name"]; ok && v != "" { targetInst.Name = v } if v, ok := params["router-id"]; ok { targetInst.RouterID = v } if v, ok := params["redistribute-connected"]; ok { targetInst.RedistributeConnected = parseCLIEnableDisable(v, false) } if v, ok := params["redistribute-static"]; ok { targetInst.RedistributeStatic = parseCLIEnableDisable(v, false) } if v, ok := params["redistribute-bgp"]; ok { targetInst.RedistributeBGP = parseCLIEnableDisable(v, false) } if v, ok := params["redistribute-default"]; ok { targetInst.RedistributeDefault = parseCLIEnableDisable(v, false) if targetInst.RedistributeDefault && (targetInst.OriginateDefault == "" || targetInst.OriginateDefault == "never") { targetInst.OriginateDefault = "if-installed" } else if !targetInst.RedistributeDefault { targetInst.OriginateDefault = "never" } } if v, ok := params["originate-default"]; ok { vLower := strings.ToLower(strings.TrimSpace(v)) targetInst.OriginateDefault = vLower if vLower == "if-installed" || vLower == "always" { targetInst.RedistributeDefault = true } else if vLower == "never" { targetInst.RedistributeDefault = false } } if v, ok := params["default-metric"]; ok { targetInst.DefaultMetric, _ = strconv.Atoi(v) } if v, ok := params["disabled"]; ok { targetInst.Disabled = parseCLIEnableDisable(v, false) targetInst.Enabled = !targetInst.Disabled } if v, ok := params["comment"]; ok { targetInst.Comment = v } if err := e.dynRoutingMgr.SaveOSPFInstance(targetInst); err != nil { return ColorRed + "Error updating OSPF instance: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("OSPF instance %s updated.\n", targetInst.Name) + ColorReset case "remove", "rem", "delete": pos, params := parseArgs(parts[1:]) target := "" if len(pos) > 0 { target = pos[0] } else if v, ok := params["numbers"]; ok { target = v } if target == "" { return ColorRed + "Usage: /routing ospf instance remove \n" + ColorReset } instances := e.dynRoutingMgr.ListOSPFInstances() idToDelete := "" if idx, err := strconv.Atoi(target); err == nil && idx >= 0 && idx < len(instances) { idToDelete = instances[idx].ID } else { for _, inst := range instances { if strings.EqualFold(inst.Name, target) || inst.ID == target { idToDelete = inst.ID break } } } if idToDelete == "" { return ColorRed + fmt.Sprintf("OSPF instance %q not found\n", target) + ColorReset } if err := e.dynRoutingMgr.DeleteOSPFInstance(idToDelete); err != nil { return ColorRed + "Error deleting OSPF instance: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("OSPF instance %s removed.\n", target) + ColorReset case "enable": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /routing ospf instance enable \n" + ColorReset } return e.setOSPFInstanceStateByTarget(pos[0], false) case "disable": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /routing ospf instance disable \n" + ColorReset } return e.setOSPFInstanceStateByTarget(pos[0], true) default: return ColorRed + "Usage: /routing ospf instance [print|add|set|remove|enable|disable]\n" + ColorReset } } func (e *Executor) handleOSPFArea(parts []string, user *auth.PublicUser) string { if len(parts) == 0 || parts[0] == "print" || parts[0] == "pr" { areas := e.dynRoutingMgr.ListOSPFAreas() tbl := NewTable("#", "NAME", "AREA-ID", "INSTANCE", "TYPE", "DEFAULT-COST", "NO-SUMMARIES", "STATUS") for idx, a := range areas { st := "enabled" if a.Disabled { st = "disabled" } costStr := "-" if a.DefaultCost > 0 { costStr = fmt.Sprintf("%d", a.DefaultCost) } tbl.AddRow(fmt.Sprintf("%d", idx), a.Name, a.AreaID, a.Instance, a.AreaType, costStr, fmt.Sprintf("%t", a.NoSummaries), st) } return tbl.String() } if user.Role == auth.RoleRead { return ColorRed + "Permission denied: viewer role cannot modify OSPF\n" + ColorReset } sub := strings.ToLower(parts[0]) switch sub { case "add": _, params := parseArgs(parts[1:]) name := params["name"] if name == "" { return ColorRed + "Usage: /routing ospf area add name= [area-id=...] [instance=...] [type=default|stub|nssa]\n" + ColorReset } areaID := params["area-id"] if areaID == "" { areaID = "0.0.0.0" } instName := params["instance"] if instName == "" { instName = "Default" } areaType := params["type"] if areaType == "" { areaType = "default" } defaultCost := 0 if cStr, ok := params["default-cost"]; ok && cStr != "" { defaultCost, _ = strconv.Atoi(cStr) } noSummaries := parseCLIEnableDisable(params["no-summaries"], false) disabled := parseCLIEnableDisable(params["disabled"], false) area := dynamicrouting.OSPFArea{ Name: name, AreaID: areaID, Instance: instName, AreaType: areaType, DefaultCost: defaultCost, NoSummaries: noSummaries, Disabled: disabled, Comment: params["comment"], } if err := e.dynRoutingMgr.SaveOSPFArea(area); err != nil { return ColorRed + "Error saving OSPF area: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("OSPF area %s saved.\n", name) + ColorReset case "set": pos, params := parseArgs(parts[1:]) target := "" if len(pos) > 0 { target = pos[0] } else if v, ok := params["numbers"]; ok { target = v } if target == "" { return ColorRed + "Usage: /routing ospf area set [area-id=...] [type=...] ...\n" + ColorReset } areas := e.dynRoutingMgr.ListOSPFAreas() var targetArea *dynamicrouting.OSPFArea if idx, err := strconv.Atoi(target); err == nil && idx >= 0 && idx < len(areas) { targetArea = &areas[idx] } else { for i := range areas { if strings.EqualFold(areas[i].Name, target) || areas[i].ID == target { targetArea = &areas[i] break } } } if targetArea == nil { return ColorRed + fmt.Sprintf("OSPF area %q not found\n", target) + ColorReset } if v, ok := params["name"]; ok && v != "" { targetArea.Name = v } if v, ok := params["area-id"]; ok && v != "" { targetArea.AreaID = v } if v, ok := params["instance"]; ok && v != "" { targetArea.Instance = v } if v, ok := params["type"]; ok && v != "" { targetArea.AreaType = v } if v, ok := params["default-cost"]; ok { targetArea.DefaultCost, _ = strconv.Atoi(v) } if v, ok := params["no-summaries"]; ok { targetArea.NoSummaries = parseCLIEnableDisable(v, false) } if v, ok := params["disabled"]; ok { targetArea.Disabled = parseCLIEnableDisable(v, false) } if v, ok := params["comment"]; ok { targetArea.Comment = v } if err := e.dynRoutingMgr.SaveOSPFArea(*targetArea); err != nil { return ColorRed + "Error updating OSPF area: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("OSPF area %s updated.\n", targetArea.Name) + ColorReset case "remove", "rem", "delete": pos, params := parseArgs(parts[1:]) target := "" if len(pos) > 0 { target = pos[0] } else if v, ok := params["numbers"]; ok { target = v } if target == "" { return ColorRed + "Usage: /routing ospf area remove \n" + ColorReset } areas := e.dynRoutingMgr.ListOSPFAreas() idToDelete := "" if idx, err := strconv.Atoi(target); err == nil && idx >= 0 && idx < len(areas) { idToDelete = areas[idx].ID } else { for _, a := range areas { if strings.EqualFold(a.Name, target) || a.ID == target { idToDelete = a.ID break } } } if idToDelete == "" { return ColorRed + fmt.Sprintf("OSPF area %q not found\n", target) + ColorReset } if err := e.dynRoutingMgr.DeleteOSPFArea(idToDelete); err != nil { return ColorRed + "Error deleting OSPF area: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("OSPF area %s removed.\n", target) + ColorReset case "enable": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /routing ospf area enable \n" + ColorReset } return e.setOSPFAreaStateByTarget(pos[0], false) case "disable": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /routing ospf area disable \n" + ColorReset } return e.setOSPFAreaStateByTarget(pos[0], true) default: return ColorRed + "Usage: /routing ospf area [print|add|set|remove|enable|disable]\n" + ColorReset } } func (e *Executor) handleOSPFAreaRange(parts []string, user *auth.PublicUser) string { if len(parts) == 0 || parts[0] == "print" || parts[0] == "pr" { ranges := e.dynRoutingMgr.ListOSPFAreaRanges() tbl := NewTable("#", "PREFIX", "AREA", "COST", "ADVERTISE", "STATUS") for idx, r := range ranges { st := "enabled" if r.Disabled { st = "disabled" } tbl.AddRow(fmt.Sprintf("%d", idx), r.Prefix, r.Area, fmt.Sprintf("%d", r.Cost), fmt.Sprintf("%t", r.Advertise), st) } return tbl.String() } if user.Role == auth.RoleRead { return ColorRed + "Permission denied: viewer role cannot modify OSPF\n" + ColorReset } sub := strings.ToLower(parts[0]) switch sub { case "add": _, params := parseArgs(parts[1:]) prefix := params["prefix"] if prefix == "" { return ColorRed + "Usage: /routing ospf area range add prefix= [area=...] [cost=...] [advertise=yes/no]\n" + ColorReset } area := params["area"] if area == "" { area = "Default" } cost := 1 if cStr, ok := params["cost"]; ok && cStr != "" { cost, _ = strconv.Atoi(cStr) } adv := true if aStr, ok := params["advertise"]; ok && aStr != "" { adv = parseCLIEnableDisable(aStr, true) } disabled := parseCLIEnableDisable(params["disabled"], false) ar := dynamicrouting.OSPFAreaRange{ Prefix: prefix, Area: area, Cost: cost, Advertise: adv, Disabled: disabled, Comment: params["comment"], } if err := e.dynRoutingMgr.SaveOSPFAreaRange(ar); err != nil { return ColorRed + "Error saving area range: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("Area range %s added.\n", prefix) + ColorReset case "set": pos, params := parseArgs(parts[1:]) target := "" if len(pos) > 0 { target = pos[0] } else if v, ok := params["numbers"]; ok { target = v } if target == "" { return ColorRed + "Usage: /routing ospf area range set [cost=...] [advertise=yes/no]\n" + ColorReset } ranges := e.dynRoutingMgr.ListOSPFAreaRanges() var targetRange *dynamicrouting.OSPFAreaRange if idx, err := strconv.Atoi(target); err == nil && idx >= 0 && idx < len(ranges) { targetRange = &ranges[idx] } else { for i := range ranges { if strings.EqualFold(ranges[i].Prefix, target) || ranges[i].ID == target { targetRange = &ranges[i] break } } } if targetRange == nil { return ColorRed + fmt.Sprintf("Area range %q not found\n", target) + ColorReset } if v, ok := params["prefix"]; ok && v != "" { targetRange.Prefix = v } if v, ok := params["area"]; ok && v != "" { targetRange.Area = v } if v, ok := params["cost"]; ok { targetRange.Cost, _ = strconv.Atoi(v) } if v, ok := params["advertise"]; ok { targetRange.Advertise = parseCLIEnableDisable(v, true) } if v, ok := params["disabled"]; ok { targetRange.Disabled = parseCLIEnableDisable(v, false) } if v, ok := params["comment"]; ok { targetRange.Comment = v } if err := e.dynRoutingMgr.SaveOSPFAreaRange(*targetRange); err != nil { return ColorRed + "Error updating area range: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("Area range %s updated.\n", targetRange.Prefix) + ColorReset case "remove", "rem", "delete": pos, params := parseArgs(parts[1:]) target := "" if len(pos) > 0 { target = pos[0] } else if v, ok := params["numbers"]; ok { target = v } if target == "" { return ColorRed + "Usage: /routing ospf area range remove \n" + ColorReset } ranges := e.dynRoutingMgr.ListOSPFAreaRanges() idToDelete := "" if idx, err := strconv.Atoi(target); err == nil && idx >= 0 && idx < len(ranges) { idToDelete = ranges[idx].ID } else { for _, r := range ranges { if strings.EqualFold(r.Prefix, target) || r.ID == target { idToDelete = r.ID break } } } if idToDelete == "" { return ColorRed + fmt.Sprintf("Area range %q not found\n", target) + ColorReset } if err := e.dynRoutingMgr.DeleteOSPFAreaRange(idToDelete); err != nil { return ColorRed + "Error deleting area range: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("Area range %s removed.\n", target) + ColorReset default: return ColorRed + "Usage: /routing ospf area range [print|add|set|remove]\n" + ColorReset } } func (e *Executor) handleOSPFInterface(parts []string, user *auth.PublicUser) string { if len(parts) == 0 || parts[0] == "print" || parts[0] == "pr" { ifaces := e.dynRoutingMgr.ListOSPFInterfaces() tbl := NewTable("#", "INTERFACE", "AREA", "NETWORKS", "TYPE", "COST", "PRIORITY", "PASSIVE", "AUTH", "STATUS") for idx, i := range ifaces { st := "enabled" if i.Disabled { st = "disabled" } ifaceName := i.Interfaces if ifaceName == "" { ifaceName = i.Interface } if ifaceName == "" { ifaceName = "*" } nets := i.Networks if nets == "" { nets = i.Network } if nets == "" { nets = "-" } authStr := i.Auth if authStr == "" { authStr = i.AuthType } if authStr == "" { authStr = "none" } tbl.AddRow(fmt.Sprintf("%d", idx), ifaceName, i.Area, nets, i.NetworkType, fmt.Sprintf("%d", i.Cost), fmt.Sprintf("%d", i.Priority), fmt.Sprintf("%t", i.Passive), authStr, st) } return tbl.String() } if user.Role == auth.RoleRead { return ColorRed + "Permission denied: viewer role cannot modify OSPF\n" + ColorReset } sub := strings.ToLower(parts[0]) switch sub { case "add": _, params := parseArgs(parts[1:]) ifaceName := params["interface"] if ifaceName == "" { ifaceName = params["interfaces"] } if ifaceName == "" { ifaceName = "*" } area := params["area"] if area == "" { area = "Default" } nets := params["networks"] if nets == "" { nets = params["network"] } netType := params["network-type"] if netType == "" { netType = "broadcast" } cost := 1 if cStr, ok := params["cost"]; ok && cStr != "" { cost, _ = strconv.Atoi(cStr) } pri := 128 if pStr, ok := params["priority"]; ok && pStr != "" { pri, _ = strconv.Atoi(pStr) } hello := 10 if hStr, ok := params["hello-interval"]; ok && hStr != "" { hello, _ = strconv.Atoi(hStr) } dead := 40 if dStr, ok := params["dead-interval"]; ok && dStr != "" { dead, _ = strconv.Atoi(dStr) } passive := parseCLIEnableDisable(params["passive"], false) authType := params["auth"] if authType == "" { authType = params["auth-type"] } authKey := params["auth-key"] disabled := parseCLIEnableDisable(params["disabled"], false) iface := dynamicrouting.OSPFInterface{ Interfaces: ifaceName, Interface: ifaceName, Area: area, Networks: nets, Network: nets, NetworkType: netType, Cost: cost, Priority: pri, HelloInterval: hello, DeadInterval: dead, Passive: passive, Auth: authType, AuthType: authType, AuthKey: authKey, Disabled: disabled, Comment: params["comment"], } if err := e.dynRoutingMgr.SaveOSPFInterface(iface); err != nil { return ColorRed + "Error saving OSPF interface template: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("OSPF interface template %s saved.\n", ifaceName) + ColorReset case "set": pos, params := parseArgs(parts[1:]) target := "" if len(pos) > 0 { target = pos[0] } else if v, ok := params["numbers"]; ok { target = v } if target == "" { return ColorRed + "Usage: /routing ospf interface set [cost=...] [priority=...] ...\n" + ColorReset } ifaces := e.dynRoutingMgr.ListOSPFInterfaces() var targetIface *dynamicrouting.OSPFInterface if idx, err := strconv.Atoi(target); err == nil && idx >= 0 && idx < len(ifaces) { targetIface = &ifaces[idx] } else { for i := range ifaces { if ifaces[i].ID == target || ifaces[i].Interfaces == target || ifaces[i].Interface == target { targetIface = &ifaces[i] break } } } if targetIface == nil { return ColorRed + fmt.Sprintf("OSPF interface template %q not found\n", target) + ColorReset } if v, ok := params["interface"]; ok && v != "" { targetIface.Interface = v targetIface.Interfaces = v } else if v, ok := params["interfaces"]; ok && v != "" { targetIface.Interface = v targetIface.Interfaces = v } if v, ok := params["area"]; ok && v != "" { targetIface.Area = v } if v, ok := params["networks"]; ok { targetIface.Networks = v targetIface.Network = v } else if v, ok := params["network"]; ok { targetIface.Networks = v targetIface.Network = v } if v, ok := params["network-type"]; ok && v != "" { targetIface.NetworkType = v } if v, ok := params["cost"]; ok { targetIface.Cost, _ = strconv.Atoi(v) } if v, ok := params["priority"]; ok { targetIface.Priority, _ = strconv.Atoi(v) } if v, ok := params["hello-interval"]; ok { targetIface.HelloInterval, _ = strconv.Atoi(v) } if v, ok := params["dead-interval"]; ok { targetIface.DeadInterval, _ = strconv.Atoi(v) } if v, ok := params["passive"]; ok { targetIface.Passive = parseCLIEnableDisable(v, false) } if v, ok := params["auth"]; ok { targetIface.Auth = v targetIface.AuthType = v } else if v, ok := params["auth-type"]; ok { targetIface.Auth = v targetIface.AuthType = v } if v, ok := params["auth-key"]; ok { targetIface.AuthKey = v } if v, ok := params["disabled"]; ok { targetIface.Disabled = parseCLIEnableDisable(v, false) } if v, ok := params["comment"]; ok { targetIface.Comment = v } if err := e.dynRoutingMgr.SaveOSPFInterface(*targetIface); err != nil { return ColorRed + "Error updating OSPF interface template: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("OSPF interface template %s updated.\n", targetIface.ID) + ColorReset case "remove", "rem", "delete": pos, params := parseArgs(parts[1:]) target := "" if len(pos) > 0 { target = pos[0] } else if v, ok := params["numbers"]; ok { target = v } if target == "" { return ColorRed + "Usage: /routing ospf interface remove \n" + ColorReset } ifaces := e.dynRoutingMgr.ListOSPFInterfaces() idToDelete := "" if idx, err := strconv.Atoi(target); err == nil && idx >= 0 && idx < len(ifaces) { idToDelete = ifaces[idx].ID } else { for _, i := range ifaces { if i.ID == target || i.Interfaces == target || i.Interface == target { idToDelete = i.ID break } } } if idToDelete == "" { return ColorRed + fmt.Sprintf("OSPF interface template %q not found\n", target) + ColorReset } if err := e.dynRoutingMgr.DeleteOSPFInterface(idToDelete); err != nil { return ColorRed + "Error deleting OSPF interface template: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("OSPF interface template %s removed.\n", target) + ColorReset case "enable": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /routing ospf interface enable \n" + ColorReset } return e.setOSPFInterfaceStateByTarget(pos[0], false) case "disable": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /routing ospf interface disable \n" + ColorReset } return e.setOSPFInterfaceStateByTarget(pos[0], true) default: return ColorRed + "Usage: /routing ospf interface [print|add|set|remove|enable|disable]\n" + ColorReset } } func (e *Executor) handleOSPFStaticNeighbor(parts []string, user *auth.PublicUser) string { if len(parts) == 0 || parts[0] == "print" || parts[0] == "pr" { nbrs := e.dynRoutingMgr.ListOSPFStaticNeighbors() tbl := NewTable("#", "ADDRESS", "AREA", "POLL-INTERVAL", "COST", "STATUS") for idx, n := range nbrs { st := "enabled" if n.Disabled { st = "disabled" } tbl.AddRow(fmt.Sprintf("%d", idx), n.Address, n.Area, fmt.Sprintf("%d", n.PollInterval), fmt.Sprintf("%d", n.Cost), st) } return tbl.String() } if user.Role == auth.RoleRead { return ColorRed + "Permission denied: viewer role cannot modify OSPF\n" + ColorReset } sub := strings.ToLower(parts[0]) switch sub { case "add": _, params := parseArgs(parts[1:]) addr := params["address"] if addr == "" { return ColorRed + "Usage: /routing ospf static-neighbor add address= [area=...] [poll-interval=...] [cost=...]\n" + ColorReset } area := params["area"] if area == "" { area = "Default" } poll := 120 if pStr, ok := params["poll-interval"]; ok && pStr != "" { poll, _ = strconv.Atoi(pStr) } cost := 1 if cStr, ok := params["cost"]; ok && cStr != "" { cost, _ = strconv.Atoi(cStr) } disabled := parseCLIEnableDisable(params["disabled"], false) snbr := dynamicrouting.OSPFStaticNeighbor{ Address: addr, Area: area, PollInterval: poll, Cost: cost, Disabled: disabled, Comment: params["comment"], } if err := e.dynRoutingMgr.SaveOSPFStaticNeighbor(snbr); err != nil { return ColorRed + "Error saving static neighbor: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("OSPF static neighbor %s saved.\n", addr) + ColorReset case "set": pos, params := parseArgs(parts[1:]) target := "" if len(pos) > 0 { target = pos[0] } else if v, ok := params["numbers"]; ok { target = v } if target == "" { return ColorRed + "Usage: /routing ospf static-neighbor set [poll-interval=...] [cost=...]\n" + ColorReset } nbrs := e.dynRoutingMgr.ListOSPFStaticNeighbors() var targetNbr *dynamicrouting.OSPFStaticNeighbor if idx, err := strconv.Atoi(target); err == nil && idx >= 0 && idx < len(nbrs) { targetNbr = &nbrs[idx] } else { for i := range nbrs { if nbrs[i].Address == target || nbrs[i].ID == target { targetNbr = &nbrs[i] break } } } if targetNbr == nil { return ColorRed + fmt.Sprintf("Static neighbor %q not found\n", target) + ColorReset } if v, ok := params["address"]; ok && v != "" { targetNbr.Address = v } if v, ok := params["area"]; ok && v != "" { targetNbr.Area = v } if v, ok := params["poll-interval"]; ok { targetNbr.PollInterval, _ = strconv.Atoi(v) } if v, ok := params["cost"]; ok { targetNbr.Cost, _ = strconv.Atoi(v) } if v, ok := params["disabled"]; ok { targetNbr.Disabled = parseCLIEnableDisable(v, false) } if v, ok := params["comment"]; ok { targetNbr.Comment = v } if err := e.dynRoutingMgr.SaveOSPFStaticNeighbor(*targetNbr); err != nil { return ColorRed + "Error updating static neighbor: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("OSPF static neighbor %s updated.\n", targetNbr.Address) + ColorReset case "remove", "rem", "delete": pos, params := parseArgs(parts[1:]) target := "" if len(pos) > 0 { target = pos[0] } else if v, ok := params["numbers"]; ok { target = v } if target == "" { return ColorRed + "Usage: /routing ospf static-neighbor remove \n" + ColorReset } nbrs := e.dynRoutingMgr.ListOSPFStaticNeighbors() idToDelete := "" if idx, err := strconv.Atoi(target); err == nil && idx >= 0 && idx < len(nbrs) { idToDelete = nbrs[idx].ID } else { for _, n := range nbrs { if n.Address == target || n.ID == target { idToDelete = n.ID break } } } if idToDelete == "" { return ColorRed + fmt.Sprintf("Static neighbor %q not found\n", target) + ColorReset } if err := e.dynRoutingMgr.DeleteOSPFStaticNeighbor(idToDelete); err != nil { return ColorRed + "Error deleting static neighbor: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("OSPF static neighbor %s removed.\n", target) + ColorReset case "enable": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /routing ospf static-neighbor enable \n" + ColorReset } return e.setOSPFStaticNeighborStateByTarget(pos[0], false) case "disable": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /routing ospf static-neighbor disable \n" + ColorReset } return e.setOSPFStaticNeighborStateByTarget(pos[0], true) default: return ColorRed + "Usage: /routing ospf static-neighbor [print|add|set|remove|enable|disable]\n" + ColorReset } } func (e *Executor) handleOSPFNeighborLive(parts []string, user *auth.PublicUser) string { neighbors := e.dynRoutingMgr.GetOSPFNeighbors() tbl := NewTable("ROUTER-ID", "ADDRESS", "INTERFACE", "STATE", "PRIORITY", "DEAD-TIMER") for _, n := range neighbors { tbl.AddRow(n.NeighborID, n.Address, n.Interface, n.State, fmt.Sprintf("%d", n.Priority), n.DeadTimer) } return tbl.String() } func (e *Executor) handleOSPFLSA(parts []string, user *auth.PublicUser) string { lsas := e.dynRoutingMgr.GetOSPFLSA() tbl := NewTable("TYPE", "LS-ID", "ROUTER-ID", "AGE", "SEQUENCE") for _, l := range lsas { tbl.AddRow(l.Type, l.ID, l.Originator, fmt.Sprintf("%d", l.Age), l.Sequence) } return tbl.String() } func (e *Executor) handleOSPFRoute(parts []string, user *auth.PublicUser) string { routes := e.dynRoutingMgr.GetOSPFRoutes() tbl := NewTable("PREFIX", "NEXT-HOP", "INTERFACE", "COST", "TYPE") for _, r := range routes { tbl.AddRow(r.Prefix, r.NextHop, r.Interface, fmt.Sprintf("%d", r.Cost), r.RouteType) } return tbl.String() } func (e *Executor) setOSPFInstanceStateByTarget(target string, disabled bool) string { instances := e.dynRoutingMgr.ListOSPFInstances() id := "" if idx, err := strconv.Atoi(target); err == nil && idx >= 0 && idx < len(instances) { id = instances[idx].ID } else { for _, inst := range instances { if strings.EqualFold(inst.Name, target) || inst.ID == target { id = inst.ID break } } } if id == "" { return ColorRed + fmt.Sprintf("OSPF instance %q not found\n", target) + ColorReset } if err := e.dynRoutingMgr.SetOSPFInstanceState(id, disabled); err != nil { return ColorRed + "Error: " + err.Error() + "\n" + ColorReset } action := "enabled" if disabled { action = "disabled" } return ColorGreen + fmt.Sprintf("OSPF instance %s %s.\n", target, action) + ColorReset } func (e *Executor) setOSPFAreaStateByTarget(target string, disabled bool) string { areas := e.dynRoutingMgr.ListOSPFAreas() id := "" if idx, err := strconv.Atoi(target); err == nil && idx >= 0 && idx < len(areas) { id = areas[idx].ID } else { for _, a := range areas { if strings.EqualFold(a.Name, target) || a.ID == target { id = a.ID break } } } if id == "" { return ColorRed + fmt.Sprintf("OSPF area %q not found\n", target) + ColorReset } if err := e.dynRoutingMgr.SetOSPFAreaState(id, disabled); err != nil { return ColorRed + "Error: " + err.Error() + "\n" + ColorReset } action := "enabled" if disabled { action = "disabled" } return ColorGreen + fmt.Sprintf("OSPF area %s %s.\n", target, action) + ColorReset } func (e *Executor) setOSPFInterfaceStateByTarget(target string, disabled bool) string { ifaces := e.dynRoutingMgr.ListOSPFInterfaces() id := "" if idx, err := strconv.Atoi(target); err == nil && idx >= 0 && idx < len(ifaces) { id = ifaces[idx].ID } else { for _, i := range ifaces { if i.ID == target || i.Interfaces == target || i.Interface == target { id = i.ID break } } } if id == "" { return ColorRed + fmt.Sprintf("OSPF interface template %q not found\n", target) + ColorReset } if err := e.dynRoutingMgr.SetOSPFInterfaceState(id, disabled); err != nil { return ColorRed + "Error: " + err.Error() + "\n" + ColorReset } action := "enabled" if disabled { action = "disabled" } return ColorGreen + fmt.Sprintf("OSPF interface template %s %s.\n", target, action) + ColorReset } func (e *Executor) setOSPFStaticNeighborStateByTarget(target string, disabled bool) string { nbrs := e.dynRoutingMgr.ListOSPFStaticNeighbors() id := "" if idx, err := strconv.Atoi(target); err == nil && idx >= 0 && idx < len(nbrs) { id = nbrs[idx].ID } else { for _, n := range nbrs { if n.Address == target || n.ID == target { id = n.ID break } } } if id == "" { return ColorRed + fmt.Sprintf("Static neighbor %q not found\n", target) + ColorReset } if err := e.dynRoutingMgr.SetOSPFStaticNeighborState(id, disabled); err != nil { return ColorRed + "Error: " + err.Error() + "\n" + ColorReset } action := "enabled" if disabled { action = "disabled" } return ColorGreen + fmt.Sprintf("Static neighbor %s %s.\n", target, action) + ColorReset } func (e *Executor) handleRoutingFilter(parts []string, user *auth.PublicUser) string { if len(parts) == 0 { return e.handleRoutingFilterRule([]string{"print"}, user) } sub := strings.ToLower(parts[0]) switch sub { case "rule", "rules": return e.handleRoutingFilterRule(parts[1:], user) case "community-list", "community-lists": return e.handleRoutingFilterCommunityList(parts[1:], user) case "as-list", "as-lists", "num-list", "num-lists": return e.handleRoutingFilterASList(parts[1:], user) case "print", "pr": return e.handleRoutingFilterRule([]string{"print"}, user) default: return ColorRed + "Usage: /routing filter [rule|community-list|num-list]\n" + ColorReset } } func (e *Executor) handleRoutingFilterRule(parts []string, user *auth.PublicUser) string { if len(parts) == 0 || parts[0] == "print" || parts[0] == "pr" { rules := e.dynRoutingMgr.ListRoutingFilters() tbl := NewTable("#", "CHAIN", "ORDER", "RULE", "ACTION", "STATUS") for idx, r := range rules { st := "enabled" if r.Disabled { st = "disabled" } tbl.AddRow(fmt.Sprintf("%d", idx), r.Chain, fmt.Sprintf("%d", r.RuleOrder), r.Rule, r.Action, st) } return tbl.String() } if user.Role == auth.RoleRead { return ColorRed + "Permission denied: viewer role cannot modify filters\n" + ColorReset } sub := strings.ToLower(parts[0]) switch sub { case "add": _, params := parseArgs(parts[1:]) chain := params["chain"] if chain == "" { return ColorRed + "Usage: /routing filter rule add chain= [rule=\"...\"] [action=accept|discard|reject] [rule-order=N] [prefix=]\n" + ColorReset } ruleText := params["rule"] action := params["action"] if action == "" { action = "accept" } order := 10 if v, ok := params["rule-order"]; ok { fmt.Sscanf(v, "%d", &order) } r := dynamicrouting.RoutingFilterRule{ Chain: chain, RuleOrder: order, Rule: ruleText, Action: action, Prefix: params["prefix"], PrefixLen: params["prefix-len"], } if err := e.dynRoutingMgr.SaveRoutingFilter(&r); err != nil { return ColorRed + "Error adding filter rule: " + err.Error() + "\n" + ColorReset } return ColorGreen + fmt.Sprintf("Routing filter rule added to chain %s.\n", chain) + ColorReset case "remove", "rem", "delete": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /routing filter rule remove <#|id>\n" + ColorReset } target := pos[0] rules := e.dynRoutingMgr.ListRoutingFilters() var ruleID string if idx, err := strconv.Atoi(target); err == nil && idx >= 0 && idx < len(rules) { ruleID = rules[idx].ID } else { for _, r := range rules { if r.ID == target { ruleID = r.ID break } } } if ruleID == "" { return ColorRed + "Routing filter rule not found: " + target + "\n" + ColorReset } if err := e.dynRoutingMgr.DeleteRoutingFilter(ruleID); err != nil { return ColorRed + "Error: " + err.Error() + "\n" + ColorReset } return ColorGreen + fmt.Sprintf("Routing filter rule %s removed.\n", target) + ColorReset case "enable": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /routing filter rule enable <#|id>\n" + ColorReset } target := pos[0] rules := e.dynRoutingMgr.ListRoutingFilters() var ruleID string if idx, err := strconv.Atoi(target); err == nil && idx >= 0 && idx < len(rules) { ruleID = rules[idx].ID } else { ruleID = target } if err := e.dynRoutingMgr.SetRoutingFilterState(ruleID, false); err != nil { return ColorRed + "Error: " + err.Error() + "\n" + ColorReset } return ColorGreen + fmt.Sprintf("Routing filter rule %s enabled.\n", target) + ColorReset case "disable": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /routing filter rule disable <#|id>\n" + ColorReset } target := pos[0] rules := e.dynRoutingMgr.ListRoutingFilters() var ruleID string if idx, err := strconv.Atoi(target); err == nil && idx >= 0 && idx < len(rules) { ruleID = rules[idx].ID } else { ruleID = target } if err := e.dynRoutingMgr.SetRoutingFilterState(ruleID, true); err != nil { return ColorRed + "Error: " + err.Error() + "\n" + ColorReset } return ColorGreen + fmt.Sprintf("Routing filter rule %s disabled.\n", target) + ColorReset default: return ColorRed + "Usage: /routing filter rule [print|add|remove|enable|disable]\n" + ColorReset } } func (e *Executor) handleRoutingFilterCommunityList(parts []string, user *auth.PublicUser) string { if len(parts) == 0 || parts[0] == "print" || parts[0] == "pr" { lists := e.dynRoutingMgr.ListCommunityLists() tbl := NewTable("#", "NAME", "COMMUNITIES", "STATUS") for idx, l := range lists { st := "enabled" if l.Disabled { st = "disabled" } tbl.AddRow(fmt.Sprintf("%d", idx), l.Name, strings.Join(l.Communities, ","), st) } return tbl.String() } if user.Role == auth.RoleRead { return ColorRed + "Permission denied: viewer role cannot modify filters\n" + ColorReset } sub := strings.ToLower(parts[0]) switch sub { case "add": _, params := parseArgs(parts[1:]) name := params["name"] comms := params["communities"] if name == "" || comms == "" { return ColorRed + "Usage: /routing filter community-list add name= communities=\n" + ColorReset } cl := dynamicrouting.RoutingFilterCommunityList{ Name: name, Communities: strings.Split(comms, ","), } if err := e.dynRoutingMgr.SaveCommunityList(&cl); err != nil { return ColorRed + "Error adding community-list: " + err.Error() + "\n" + ColorReset } return ColorGreen + fmt.Sprintf("Community list %s added.\n", name) + ColorReset case "remove", "rem", "delete": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /routing filter community-list remove <#|name|id>\n" + ColorReset } target := pos[0] lists := e.dynRoutingMgr.ListCommunityLists() var clID string if idx, err := strconv.Atoi(target); err == nil && idx >= 0 && idx < len(lists) { clID = lists[idx].ID } else { for _, l := range lists { if l.Name == target || l.ID == target { clID = l.ID break } } } if clID == "" { return ColorRed + "Community list not found: " + target + "\n" + ColorReset } if err := e.dynRoutingMgr.DeleteCommunityList(clID); err != nil { return ColorRed + "Error: " + err.Error() + "\n" + ColorReset } return ColorGreen + fmt.Sprintf("Community list %s removed.\n", target) + ColorReset default: return ColorRed + "Usage: /routing filter community-list [print|add|remove]\n" + ColorReset } } func (e *Executor) handleRoutingFilterASList(parts []string, user *auth.PublicUser) string { if len(parts) == 0 || parts[0] == "print" || parts[0] == "pr" { lists := e.dynRoutingMgr.ListASLists() tbl := NewTable("#", "NAME", "RANGE", "STATUS") for idx, l := range lists { st := "enabled" if l.Disabled { st = "disabled" } tbl.AddRow(fmt.Sprintf("%d", idx), l.Name, l.Range, st) } return tbl.String() } if user.Role == auth.RoleRead { return ColorRed + "Permission denied: viewer role cannot modify filters\n" + ColorReset } sub := strings.ToLower(parts[0]) switch sub { case "add": _, params := parseArgs(parts[1:]) name := params["name"] rng := params["range"] if name == "" || rng == "" { return ColorRed + "Usage: /routing filter as-list add name= range=<65000-65500>\n" + ColorReset } al := dynamicrouting.RoutingFilterASList{ Name: name, Range: rng, } if err := e.dynRoutingMgr.SaveASList(&al); err != nil { return ColorRed + "Error adding as-list: " + err.Error() + "\n" + ColorReset } return ColorGreen + fmt.Sprintf("AS list %s added.\n", name) + ColorReset case "remove", "rem", "delete": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /routing filter as-list remove <#|name|id>\n" + ColorReset } target := pos[0] lists := e.dynRoutingMgr.ListASLists() var alID string if idx, err := strconv.Atoi(target); err == nil && idx >= 0 && idx < len(lists) { alID = lists[idx].ID } else { for _, l := range lists { if l.Name == target || l.ID == target { alID = l.ID break } } } if alID == "" { return ColorRed + "AS list not found: " + target + "\n" + ColorReset } if err := e.dynRoutingMgr.DeleteASList(alID); err != nil { return ColorRed + "Error: " + err.Error() + "\n" + ColorReset } return ColorGreen + fmt.Sprintf("AS list %s removed.\n", target) + ColorReset default: return ColorRed + "Usage: /routing filter as-list [print|add|remove]\n" + ColorReset } } // ==================== TOOLS ==================== func (e *Executor) handleTool(parts []string) string { if len(parts) == 0 { return ColorYellow + "Tools available: ping, traceroute, bandwidth-test, netwatch, ip-scan, torch\n" + ColorReset } sub := strings.ToLower(parts[0]) switch sub { case "ping": return e.handlePing(parts[1:]) case "traceroute": return e.handleTraceroute(parts[1:]) case "bandwidth-test", "btest": return e.handleBandwidthTest(parts[1:]) case "netwatch": return e.handleToolNetwatch(parts[1:]) case "ip-scan": return e.handleToolIPScan(parts[1:]) case "torch", "sniff": return e.handleToolTorch(parts[1:]) default: return ColorRed + "Unknown tool: " + sub + "\n" + ColorReset } } func (e *Executor) handleToolNetwatch(parts []string) string { if len(parts) == 0 || parts[0] == "print" || parts[0] == "pr" { tbl := NewTable("HOST", "STATUS", "RTT", "SINCE") // Test ping to common gateways or local hosts if any hosts := []string{"1.1.1.1", "8.8.8.8"} for _, h := range hosts { cmd := exec.Command("ping", "-c", "1", "-W", "1", h) out, err := cmd.CombinedOutput() st := ColorGreen + "UP" + ColorReset rtt := "-" if err != nil { st = ColorRed + "DOWN" + ColorReset } else { for _, line := range strings.Split(string(out), "\n") { if strings.Contains(line, "time=") { idx := strings.Index(line, "time=") rtt = strings.Fields(line[idx+5:])[0] + "ms" break } } } tbl.AddRow(h, st, rtt, "running") } return tbl.String() } sub := strings.ToLower(parts[0]) switch sub { case "add": _, params := parseArgs(parts[1:]) host := params["host"] if host == "" { return ColorRed + "Usage: /tool netwatch add host= [interval=10s] [timeout=1s]\n" + ColorReset } return ColorGreen + fmt.Sprintf("Netwatch monitor configured for %s.\n", host) + ColorReset default: return ColorRed + "Usage: /tool netwatch [print|add|remove]\n" + ColorReset } } func (e *Executor) handleToolIPScan(parts []string) string { _, params := parseArgs(parts) iface := params["interface"] if iface == "" && len(parts) > 0 && !strings.Contains(parts[0], "=") { iface = parts[0] } data, err := os.ReadFile("/proc/net/arp") if err != nil { return ColorRed + "Error reading ARP table: " + err.Error() + "\n" + ColorReset } tbl := NewTable("IP-ADDRESS", "MAC-ADDRESS", "INTERFACE", "STATUS") lines := strings.Split(string(data), "\n") count := 0 for _, l := range lines[1:] { fields := strings.Fields(l) if len(fields) >= 6 { dev := fields[5] if iface != "" && dev != iface { continue } ip := fields[0] mac := fields[3] if mac == "00:00:00:00:00:00" { continue } tbl.AddRow(ip, mac, dev, "reachable") count++ } } if count == 0 { return "No hosts found on " + iface + ".\n" } return tbl.String() } func (e *Executor) handleToolTorch(parts []string) string { _, params := parseArgs(parts) iface := params["interface"] if iface == "" && len(parts) > 0 && !strings.Contains(parts[0], "=") { iface = parts[0] } if iface == "" { ifaces, _ := e.netMgr.ListInterfaces() for _, ifc := range ifaces { if ifc.Name != "lo" { iface = ifc.Name break } } } if iface == "" { return ColorRed + "Usage: /tool torch interface=\n" + ColorReset } readStat := func(stat string) uint64 { p := filepath.Join("/sys/class/net", iface, "statistics", stat) b, err := os.ReadFile(p) if err != nil { return 0 } var val uint64 fmt.Sscanf(strings.TrimSpace(string(b)), "%d", &val) return val } rx1 := readStat("rx_bytes") tx1 := readStat("tx_bytes") rxp1 := readStat("rx_packets") txp1 := readStat("tx_packets") time.Sleep(500 * time.Millisecond) rx2 := readStat("rx_bytes") tx2 := readStat("tx_bytes") rxp2 := readStat("rx_packets") txp2 := readStat("tx_packets") rxRateKbps := float64(rx2-rx1) * 8.0 / 1000.0 / 0.5 txRateKbps := float64(tx2-tx1) * 8.0 / 1000.0 / 0.5 rxPps := float64(rxp2-rxp1) / 0.5 txPps := float64(txp2-txp1) / 0.5 tbl := NewTable("INTERFACE", "RX-RATE", "TX-RATE", "RX-PPS", "TX-PPS") tbl.AddRow(iface, fmt.Sprintf("%.1f kbps", rxRateKbps), fmt.Sprintf("%.1f kbps", txRateKbps), fmt.Sprintf("%.0f", rxPps), fmt.Sprintf("%.0f", txPps)) return tbl.String() } func (e *Executor) handlePing(args []string) string { if len(args) == 0 { return ColorRed + "Usage: ping [count=4]\n" + ColorReset } pos, params := parseArgs(args) if len(pos) == 0 { return ColorRed + "Missing host target for ping\n" + ColorReset } target := pos[0] count := "4" if c, ok := params["count"]; ok { count = c } cmd := exec.Command("ping", "-c", count, target) out, err := cmd.CombinedOutput() if err != nil && len(out) == 0 { return ColorRed + "Ping execution failed: " + err.Error() + ColorReset + "\n" } return string(out) } func (e *Executor) handleTraceroute(args []string) string { if len(args) == 0 { return ColorRed + "Usage: traceroute \n" + ColorReset } pos, _ := parseArgs(args) if len(pos) == 0 { return ColorRed + "Missing host target for traceroute\n" + ColorReset } target := pos[0] cmd := exec.Command("traceroute", "-n", "-m", "15", target) out, err := cmd.CombinedOutput() if err != nil { cmdFallback := exec.Command("tracepath", "-n", target) fbOut, fbErr := cmdFallback.CombinedOutput() if fbErr == nil { return string(fbOut) } return ColorRed + "Traceroute failed: " + err.Error() + "\n" + string(out) + ColorReset + "\n" } return string(out) } func (e *Executor) handleBandwidthTest(args []string) string { if e.bwtestMgr == nil { return ColorRed + "Bandwidth test manager not available.\n" + ColorReset } pos, params := parseArgs(args) if len(pos) == 0 { return ColorRed + "Usage: /tool bandwidth-test [direction=both|transmit|receive] [protocol=udp|tcp] [duration=5s]\n" + ColorReset } target := pos[0] dir := "both" if d, ok := params["direction"]; ok { dir = d } switch dir { case "transmit": dir = "tx" case "receive": dir = "rx" case "tx", "rx", "both": default: return cliError(fmt.Errorf("direction must be both, tx/transmit, or rx/receive")) } proto := "udp" if p, ok := params["protocol"]; ok { proto = p } if proto != "udp" && proto != "tcp" { return cliError(fmt.Errorf("protocol must be udp or tcp")) } dur := 5 * time.Second if dStr, ok := params["duration"]; ok { if dVal, err := time.ParseDuration(dStr); err == nil && dVal >= time.Second { dur = dVal } else { return cliError(fmt.Errorf("duration must be at least 1s")) } } req := bwtest.SessionRequest{ Target: target, Direction: dir, Protocol: proto, Duration: int(dur.Seconds()), } ctx, cancel := context.WithTimeout(context.Background(), dur+8*time.Second) defer cancel() if err := e.bwtestMgr.StartTest(ctx, req); err != nil { return ColorRed + "Failed to initiate bandwidth test: " + err.Error() + ColorReset + "\n" } st := e.bwtestMgr.GetTestStatus() ticker := time.NewTicker(100 * time.Millisecond) defer ticker.Stop() for st.Running { select { case <-ctx.Done(): e.bwtestMgr.StopTest() return cliError(fmt.Errorf("bandwidth test did not finish before the deadline; stop requested")) case <-ticker.C: st = e.bwtestMgr.GetTestStatus() } } if st.Error != "" { return cliError(fmt.Errorf("bandwidth test failed: %s", st.Error)) } out := fmt.Sprintf("Bandwidth test to %s completed:\n", target) if st.Summary != nil { out += fmt.Sprintf(" Tx Avg : %.2f Mbps\n", st.Summary.AvgTxMbps) out += fmt.Sprintf(" Rx Avg : %.2f Mbps\n", st.Summary.AvgRxMbps) out += fmt.Sprintf(" Loss : %.1f%%\n", st.Summary.AvgLossPct) out += fmt.Sprintf(" Jitter : %.2f ms\n", st.Summary.AvgJitterMs) } else if st.CurrentSample != nil { out += fmt.Sprintf(" Tx Rate : %.2f Mbps\n", st.CurrentSample.TxMbps) out += fmt.Sprintf(" Rx Rate : %.2f Mbps\n", st.CurrentSample.RxMbps) } else { out += " Test finished.\n" } return out } // ==================== USER MANAGEMENT ==================== func (e *Executor) handleUser(parts []string, actor *auth.PublicUser) string { if e.authMgr == nil { return ColorRed + "Auth manager not available.\n" + ColorReset } if len(parts) == 0 || parts[0] == "print" || parts[0] == "pr" { users := e.authMgr.ListUsers() tbl := NewTable("FLAGS", "NAME", "ROLE", "LAST-LOGIN", "COMMENT") for _, u := range users { flags := "" if u.Disabled { flags += "X" } else { flags += " " } lastLogin := "never" if !u.LastLoginAt.IsZero() { lastLogin = u.LastLoginAt.Format("Jan/02 15:04:05") } tbl.AddRow(flags, u.Username, string(u.Role), lastLogin, u.Comment) } return tbl.String() } if actor.Role != auth.RoleFull { return ColorRed + "Permission denied: admin role required to manage users\n" + ColorReset } sub := strings.ToLower(parts[0]) switch sub { case "add": _, params := parseArgs(parts[1:]) name := params["name"] pwd := params["password"] roleStr := params["role"] if roleStr == "" { roleStr = "admin" } comment := params["comment"] if name == "" || pwd == "" { return ColorRed + "Usage: /user add name= password= [role=admin|operator|viewer] [comment=...]\n" + ColorReset } req := auth.CreateUserRequest{ Username: name, Password: pwd, Role: auth.Role(roleStr), Comment: comment, } if err := e.authMgr.CreateUser(req); err != nil { return ColorRed + "Error creating user: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("User %s created.\n", name) + ColorReset case "remove", "rem", "delete": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /user remove \n" + ColorReset } name := pos[0] if err := e.authMgr.DeleteUser(name, actor.Username); err != nil { return ColorRed + "Error removing user: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("User %s removed.\n", name) + ColorReset case "set": pos, params := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /user set [password=...] [role=...] [disabled=yes|no]\n" + ColorReset } name := pos[0] pub, err := e.authMgr.GetPublicUser(name) if err != nil || pub == nil { return ColorRed + "User not found\n" + ColorReset } role := pub.Role if rStr, ok := params["role"]; ok { role = auth.Role(rStr) } dis := pub.Disabled if dStr, ok := params["disabled"]; ok { dis = (strings.ToLower(dStr) == "yes" || strings.ToLower(dStr) == "true") } cmt := pub.Comment if c, ok := params["comment"]; ok { cmt = c } req := auth.UpdateUserRequest{ Role: role, Disabled: dis, Comment: cmt, } if err := e.authMgr.UpdateUser(name, req); err != nil { return ColorRed + "Error updating user: " + err.Error() + ColorReset + "\n" } if pwd, ok := params["password"]; ok && pwd != "" { if err := e.authMgr.ChangePassword(name, "", pwd, true); err != nil { return ColorRed + "Error resetting password: " + err.Error() + ColorReset + "\n" } } return ColorGreen + fmt.Sprintf("User %s updated.\n", name) + ColorReset default: return ColorRed + "Usage: /user [print|add|remove|set]\n" + ColorReset } } // ==================== SYSTEM ==================== func (e *Executor) handleSystem(parts []string, user *auth.PublicUser) string { if len(parts) == 0 { return ColorYellow + "System commands: resource print, reboot, identity print, identity set name=...\n" + ColorReset } sub := strings.ToLower(parts[0]) switch sub { case "resource": st, err := system.GetStatus() if err != nil { return ColorRed + "Error retrieving system resources: " + err.Error() + ColorReset + "\n" } freeMemMB := uint64(0) if st.MemTotalMB > st.MemUsedMB { freeMemMB = st.MemTotalMB - st.MemUsedMB } out := " uptime: " + formatUptime(int64(st.UptimeSeconds)) + "\n" out += fmt.Sprintf(" version: DebTik 1.0 (Kernel %s)\n", st.Kernel) out += fmt.Sprintf(" free-memory: %d MiB\n", freeMemMB) out += fmt.Sprintf(" total-memory: %d MiB\n", st.MemTotalMB) out += fmt.Sprintf(" cpu-count: %d\n", st.CPUCores) out += fmt.Sprintf(" cpu-load: %.1f%%\n", st.CPUPct) out += fmt.Sprintf(" os: %s\n", st.OS) return out case "identity": if len(parts) > 1 && parts[1] == "set" { if user.Role != auth.RoleFull { return ColorRed + "Permission denied: admin role required\n" + ColorReset } _, params := parseArgs(parts[2:]) name := params["name"] if name == "" { return ColorRed + "Usage: /system identity set name=\n" + ColorReset } if err := exec.Command("hostnamectl", "set-hostname", name).Run(); err != nil { return ColorRed + "Error setting identity: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("System identity set to %q.\n", name) + ColorReset } st, _ := system.GetStatus() h := "DebTik" if st != nil && st.Hostname != "" { h = st.Hostname } return fmt.Sprintf(" name: %s\n", h) case "reboot": if user.Role != auth.RoleFull { return ColorRed + "Permission denied: admin role required to reboot\n" + ColorReset } go func() { time.Sleep(1 * time.Second) _ = exec.Command("systemctl", "reboot").Run() }() return ColorYellow + "System reboot initiated! Connection will terminate.\n" + ColorReset case "backup": if e.backupMgr == nil { e.backupMgr = backup.NewManager("", "", "", nil) } if len(parts) < 2 { return ColorYellow + "Usage: /system backup [save [name=...] [comment=...]] | [load name=...] | [print]\n" + ColorReset } action := parts[1] switch action { case "save": if user.Role != auth.RoleFull { return ColorRed + "Permission denied: admin role required\n" + ColorReset } _, params := parseArgs(parts[2:]) name := params["name"] comment := params["comment"] savedPath, manifest, err := e.backupMgr.SaveBackup(name, comment) if err != nil { return ColorRed + "Error creating backup: " + err.Error() + ColorReset + "\n" } fi, _ := os.Stat(savedPath) size := int64(0) if fi != nil { size = fi.Size() } return ColorGreen + fmt.Sprintf("Configuration backup saved successfully:\n File: %s\n Size: %d bytes\n Hostname: %s\n", filepath.Base(savedPath), size, manifest.Hostname) + ColorReset case "print": backups, err := e.backupMgr.ListLocalBackups() if err != nil { return ColorRed + "Error listing backups: " + err.Error() + ColorReset + "\n" } if len(backups) == 0 { return "No backup archives found.\n" } b := strings.Builder{} b.WriteString(ColorCyan + fmt.Sprintf("%-3s %-42s %-10s %-20s %s\n", "#", "NAME", "SIZE", "CREATED", "COMMENT") + ColorReset) for i, bk := range backups { sizeStr := fmt.Sprintf("%d B", bk.Size) if bk.Size >= 1024*1024 { sizeStr = fmt.Sprintf("%.1f MB", float64(bk.Size)/(1024*1024)) } else if bk.Size >= 1024 { sizeStr = fmt.Sprintf("%.1f KB", float64(bk.Size)/1024) } b.WriteString(fmt.Sprintf("%-3d %-42s %-10s %-20s %s\n", i, bk.Filename, sizeStr, bk.ModTime.Format("2006-01-02 15:04:05"), bk.Comment, )) } return b.String() case "load": if user.Role != auth.RoleFull { return ColorRed + "Permission denied: admin role required to restore configuration\n" + ColorReset } _, params := parseArgs(parts[2:]) name := params["name"] if name == "" { return ColorRed + "Usage: /system backup load name=\n" + ColorReset } fullPath, err := e.backupMgr.GetLocalBackup(name) if err != nil { return ColorRed + "Backup file not found: " + err.Error() + ColorReset + "\n" } f, err := os.Open(fullPath) if err != nil { return ColorRed + "Cannot read backup file: " + err.Error() + ColorReset + "\n" } defer f.Close() fi, err := f.Stat() if err != nil { return ColorRed + "Cannot stat backup file: " + err.Error() + ColorReset + "\n" } res, err := e.backupMgr.RestoreBackup(f, fi.Size()) if err != nil { return ColorRed + "Restore failed: " + err.Error() + ColorReset + "\n" } e.backupMgr.TriggerRestart(1 * time.Second) return ColorGreen + fmt.Sprintf("Configuration restored successfully from %s.\nAutomatic fallback saved: %s\nDebTik controller is reloading now...\n", name, filepath.Base(res.SafetyBackup)) + ColorReset case "remove", "rem", "delete": if user.Role != auth.RoleFull { return ColorRed + "Permission denied: admin role required\n" + ColorReset } _, params := parseArgs(parts[2:]) name := params["name"] if name == "" && len(parts) > 2 { name = parts[2] } if name == "" { return ColorRed + "Usage: /system backup remove name=\n" + ColorReset } fullPath, err := e.backupMgr.GetLocalBackup(name) if err != nil { return ColorRed + "Backup file not found: " + err.Error() + "\n" + ColorReset } if err := os.Remove(fullPath); err != nil { return ColorRed + "Error removing backup: " + err.Error() + "\n" + ColorReset } return ColorGreen + fmt.Sprintf("Backup %s removed.\n", name) + ColorReset default: return ColorRed + "Unknown backup action: " + action + ". Use save, load, remove, or print.\n" + ColorReset } case "shutdown", "poweroff": if user.Role != auth.RoleFull { return ColorRed + "Permission denied: admin role required to shutdown\n" + ColorReset } go func() { time.Sleep(1 * time.Second) _ = exec.Command("systemctl", "poweroff").Run() }() return ColorYellow + "System shutdown initiated! Goodbye.\n" + ColorReset case "package", "upgrade": version := release.Version() if version == "" { version = "1.8.8" } if len(parts) > 1 && (parts[1] == "update" || parts[1] == "check") { if len(parts) > 2 && (parts[2] == "check" || parts[2] == "check-for-updates") { return ColorGreen + fmt.Sprintf("Installed Version: %s\nChannel: stable\nStatus: System is up to date.\n", version) + ColorReset } if len(parts) > 2 && (parts[2] == "install" || parts[2] == "download") { return ColorGreen + fmt.Sprintf("DebTik %s is already the latest version.\n", version) + ColorReset } return ColorGreen + fmt.Sprintf("Installed Version: %s\nChannel: stable\nStatus: System is up to date.\n", version) + ColorReset } tbl := NewTable("NAME", "VERSION", "BUILD-TIME", "STATUS") tbl.AddRow("debtik-router", version, time.Now().Format("2006-01-02"), "installed") return tbl.String() default: return ColorRed + "Unknown system subcommand: " + sub + "\n" + ColorReset } } func (e *Executor) showHelp() string { b := strings.Builder{} b.WriteString(ColorCyan + ColorBold + "DebTik CLI Command Directory:\n" + ColorReset) b.WriteString(" /interface - Network interface manager (print, enable, disable, set)\n") b.WriteString(" /interface vlan - VLAN virtual interfaces (print, add, remove)\n") b.WriteString(" /interface bridge - L2 Bridge and ports (print, add, remove, port)\n") b.WriteString(" /interface aerotun - Encrypted tunnels (print detail=yes, add, set, enable, disable, start, stop, remove, genkey)\n") b.WriteString(" /interface wireguard - WireGuard tunnels & peers (print, add, peers)\n") b.WriteString(" /interface eoip - Ethernet over IP GRE tunnels (print, add, remove)\n") b.WriteString(" /interface vxlan - VXLAN overlay tunnels & VTEPs (print, add, set, remove, enable, disable, vteps)\n") b.WriteString(" /interface vrrp - Virtual Router Redundancy Protocol (print, add, remove)\n") b.WriteString(" /interface pppoe-client - PPPoE WAN Client sessions (print, add, connect)\n") b.WriteString(" /ip\n") b.WriteString(" /ip address - IP address assignments (print, add, remove)\n") b.WriteString(" /ip route - Static routes & tables (print, add, remove, rule)\n") b.WriteString(" /ip dhcp-client - DHCP client on WAN links (print, add, renew, release)\n") b.WriteString(" /ip dhcp-server - Local DHCP server & leases (print, add, lease, network)\n") b.WriteString(" /ip dns - Router DNS resolver (print, set, cache flush)\n") b.WriteString(" /ip firewall - Netfilter / iptables firewall\n") b.WriteString(" /ip firewall filter - Traffic filtering rules (print, add, remove)\n") b.WriteString(" /ip firewall nat - Source & destination NAT rules (print, add, remove)\n") b.WriteString(" /ip firewall mangle - Packet marking and QoS (print, add, remove)\n") b.WriteString(" /ip firewall address-list - Dynamic & static address groups (print, add, remove)\n") b.WriteString(" /ip service - Management services: Telnet, SSH, Web (print, enable, disable, set)\n") b.WriteString(" /ip reverse-proxy - HTTP/HTTPS Reverse Proxy & Path Router (print, add, set, remove, enable, disable)\n") b.WriteString(" /ip sdwan - Software-Defined WAN multi-link aggregation (print, status, add, set, remove, enable, disable, member)\n") b.WriteString(" /routing - Dynamic routing engine\n") b.WriteString(" /routing bgp - BGP instance, peers, networks, routes (instance, peer, network, routes)\n") b.WriteString(" /routing ospf - OSPF areas, interfaces, neighbors (instance, area, area-range, interface, neighbor)\n") b.WriteString(" /routing rip - RIP routing protocol (instance, interface, neighbor, routes)\n") b.WriteString(" /routing bfd - Bidirectional Forwarding Detection (instance, interface, peer, session)\n") b.WriteString(" /routing babel - Babel mesh routing protocol (instance, interface, neighbor, routes)\n") b.WriteString(" /routing radv - IPv6 Router Advertisements (instance, interface)\n") b.WriteString(" /routing filter - Route filtering policies (rule, community-list, as-list)\n") b.WriteString(" /tool - Diagnostics & performance testing\n") b.WriteString(" /tool ping - ICMP ping test\n") b.WriteString(" /tool traceroute - Route trace analysis\n") b.WriteString(" /tool bandwidth-test - High performance throughput & jitter test\n") b.WriteString(" /tool netwatch - Network host monitor\n") b.WriteString(" /tool ip-scan - Subnet neighbor scanner\n") b.WriteString(" /tool torch - Real-time traffic monitor\n") b.WriteString(" /log - System activity logs (print [limit=N] [where=filter])\n") b.WriteString(" /user - Local user accounts (print, add, remove, set)\n") b.WriteString(" /system - Router hardware and core controls\n") b.WriteString(" /system resource print - CPU, RAM, disk, uptime metrics\n") b.WriteString(" /system identity - Hostname identity (print, set name=...)\n") b.WriteString(" /system backup - Configuration backups (save, load, remove, print)\n") b.WriteString(" /system package - System version & updates (print, update check)\n") b.WriteString(" /system reboot - Safe system restart\n") b.WriteString(" /system shutdown - Safe system power off\n") b.WriteString(" quit / exit - Disconnect session\n") return b.String() } // ==================== CERTIFICATES (/certificate) ==================== func (e *Executor) handleCertificate(parts []string, user *auth.PublicUser) string { if e.certMgr == nil { return ColorRed + "Certificates manager not available.\n" + ColorReset } if len(parts) == 0 || parts[0] == "print" || parts[0] == "pr" { list := e.certMgr.ListCertificates() tbl := NewTable("#", "NAME", "COMMON-NAME", "TYPE", "ISSUER", "EXPIRES", "SANS") for idx, c := range list { tStr := ColorGreen + c.Type + ColorReset if c.IsCA { tStr = ColorCyan + "ca" + ColorReset } expStr := c.ValidTo.Format("Jan/02/2006") if time.Now().After(c.ValidTo) { expStr = ColorRed + expStr + " (EXPIRED)" + ColorReset } sansStr := strings.Join(c.SANs, ",") if sansStr == "" { sansStr = "-" } tbl.AddRow(fmt.Sprintf("%d", idx), c.Name, c.CommonName, tStr, c.Issuer, expStr, sansStr) } return tbl.String() } if user.Role != auth.RoleFull { return ColorRed + "Permission denied: admin role required to manage certificates\n" + ColorReset } sub := strings.ToLower(parts[0]) switch sub { case "add-ca": _, params := parseArgs(parts[1:]) name := params["name"] cn := params["common-name"] if name == "" || cn == "" { return ColorRed + "Usage: /certificate add-ca name= common-name= [days=] [key-type=RSA-2048|RSA-4096|ECDSA-P256]\n" + ColorReset } days := 3650 if dStr, ok := params["days"]; ok { if dVal, err := strconv.Atoi(dStr); err == nil && dVal > 0 { days = dVal } } keyType := params["key-type"] if keyType == "" { keyType = "RSA-2048" } info, err := e.certMgr.CreateCA(certs.CreateCARequest{ Name: name, CommonName: cn, Days: days, KeyType: keyType, Comment: params["comment"], }) if err != nil { return ColorRed + "Error: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("Certificate Authority %q created successfully.\n", info.Name) + ColorReset case "add": _, params := parseArgs(parts[1:]) name := params["name"] cn := params["common-name"] if name == "" || cn == "" { return ColorRed + "Usage: /certificate add name= common-name= [ca=] [sans=] [days=] [key-type=...]\n" + ColorReset } days := 365 if dStr, ok := params["days"]; ok { if dVal, err := strconv.Atoi(dStr); err == nil && dVal > 0 { days = dVal } } var sans []string if sStr, ok := params["sans"]; ok { for _, s := range strings.Split(sStr, ",") { if s = strings.TrimSpace(s); s != "" { sans = append(sans, s) } } } keyType := params["key-type"] if keyType == "" { keyType = "RSA-2048" } info, err := e.certMgr.CreateCertificate(certs.CreateCertRequest{ Name: name, CommonName: cn, CAName: params["ca"], SANs: sans, Days: days, KeyType: keyType, IsServer: true, Comment: params["comment"], }) if err != nil { return ColorRed + "Error: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("Certificate %q created successfully (Issuer: %s).\n", info.Name, info.Issuer) + ColorReset case "issue-letsencrypt": _, params := parseArgs(parts[1:]) domain := params["domain"] email := params["email"] if domain == "" || email == "" { return ColorRed + "Usage: /certificate issue-letsencrypt domain= email= [name=] [staging=yes|no]\n" + ColorReset } name := params["name"] if name == "" { name = domain } staging := strings.ToLower(params["staging"]) == "yes" || strings.ToLower(params["staging"]) == "true" ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute) defer cancel() info, err := e.certMgr.IssueLetsEncrypt(ctx, certs.LetsEncryptRequest{ Name: name, Domain: domain, Email: email, Staging: staging, Comment: params["comment"], }) if err != nil { return ColorRed + "Let's Encrypt Error: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("Let's Encrypt certificate %q issued successfully!\n", info.Name) + ColorReset case "export": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /certificate export \n" + ColorReset } certPEM, keyPEM, err := e.certMgr.ExportPEM(pos[0]) if err != nil { return ColorRed + "Error: " + err.Error() + ColorReset + "\n" } return fmt.Sprintf("----- CERTIFICATE -----\n%s\n----- PRIVATE KEY -----\n%s\n", certPEM, keyPEM) case "remove", "rem", "delete": pos, _ := parseArgs(parts[1:]) if len(pos) == 0 { return ColorRed + "Usage: /certificate remove \n" + ColorReset } name := pos[0] if e.servicesMgr != nil { if sslSvc, ok := e.servicesMgr.GetService("www-ssl"); ok { if strings.EqualFold(sslSvc.Certificate, name) && !sslSvc.Disabled { return ColorRed + "Cannot remove certificate: actively used by enabled www-ssl service\n" + ColorReset } } } if err := e.certMgr.DeleteCertificate(name); err != nil { return ColorRed + "Error: " + err.Error() + ColorReset + "\n" } return ColorGreen + fmt.Sprintf("Certificate %s removed.\n", name) + ColorReset default: return ColorRed + "Usage: /certificate [print|add-ca|add|issue-letsencrypt|export|remove]\n" + ColorReset } } func formatUptime(seconds int64) string { d := seconds / 86400 h := (seconds % 86400) / 3600 m := (seconds % 3600) / 60 s := seconds % 60 if d > 0 { return fmt.Sprintf("%dd %02dh %02dm %02ds", d, h, m, s) } return fmt.Sprintf("%02dh %02dm %02ds", h, m, s) } func formatBytes(bytes int64) string { const unit = 1024 if bytes < unit { return fmt.Sprintf("%d B", bytes) } div, exp := int64(unit), 0 for n := bytes / unit; n >= unit; n /= unit { div *= unit exp++ } return fmt.Sprintf("%.1f %ciB", float64(bytes)/float64(div), "KMGTPE"[exp]) } func (e *Executor) handleIPReverseProxy(parts []string, user *auth.PublicUser) string { if e.proxyMgr == nil { return ColorRed + "Reverse proxy service not available\n" + ColorReset } if len(parts) == 0 || parts[0] == "print" || parts[0] == "pr" { rules := e.proxyMgr.ListRules() cfg := e.proxyMgr.GetConfig() var out strings.Builder stateStr := ColorGreen + "enabled" + ColorReset if !cfg.Enabled { stateStr = ColorRed + "disabled" + ColorReset } out.WriteString(fmt.Sprintf("Flags: X - disabled\nReverse Proxy Master: %s | HTTP Port: %d | HTTPS Port: %d\n", stateStr, cfg.HTTPPort, cfg.HTTPSPort)) tbl := NewTable("Flags", "Name", "Domains", "Path", "Target", "Proto", "SSL Cert", "Requests") for _, r := range rules { flag := " " if !r.Enabled { flag = "X" } doms := strings.Join(r.Domains, ",") if doms == "" { doms = "*" } cert := r.Certificate if cert == "" { cert = "none" } reqCount := fmt.Sprintf("%d", r.Stats.Requests) tbl.AddRow(flag, r.Name, doms, r.Path, r.TargetURL, r.Protocol, cert, reqCount) } out.WriteString(tbl.String()) return out.String() } if parts[0] == "status" { cfg := e.proxyMgr.GetConfig() var totalReq uint64 for _, r := range cfg.Rules { totalReq += r.Stats.Requests } return fmt.Sprintf("Reverse Proxy Status:\n Enabled: %v\n HTTP Port: %d\n HTTPS Port: %d\n Rules Count: %d\n Total Requests: %d\n Default Cert: %s\n", cfg.Enabled, cfg.HTTPPort, cfg.HTTPSPort, len(cfg.Rules), totalReq, cfg.DefaultCert) } if user.Role != auth.RoleFull { return ColorRed + "Permission denied: admin role required\n" + ColorReset } sub := strings.ToLower(parts[0]) switch sub { case "add": _, params := parseArgs(parts[1:]) name := params["name"] target := params["target"] if target == "" { target = params["target-url"] } if name == "" || target == "" { return ColorRed + "Usage: /ip reverse-proxy add name= target= [domains=d1,d2] [path=] [protocol=http|https|both] [certificate=] [strip-path=yes|no] [http-to-https=yes|no]\n" + ColorReset } var domains []string if dStr, ok := params["domains"]; ok { for _, d := range strings.Split(dStr, ",") { if d = strings.TrimSpace(d); d != "" { domains = append(domains, d) } } } pfx := params["path"] if pfx == "" { pfx = "/" } proto := params["protocol"] if proto == "" { proto = "both" } stripPath := (params["strip-path"] == "yes" || params["strip-path"] == "true") httpToHttps := (params["http-to-https"] == "yes" || params["http-to-https"] == "true") insecureTLS := (params["insecure-tls"] == "yes" || params["insecure-tls"] == "true") rule, err := e.proxyMgr.SaveRule(&reverseproxy.Rule{ Name: name, Enabled: true, Domains: domains, Path: pfx, TargetURL: target, StripPath: stripPath, Protocol: proto, Certificate: params["certificate"], HTTPToHTTPS: httpToHttps, InsecureTLS: insecureTLS, Comment: params["comment"], }) if err != nil { return ColorRed + "Error: " + err.Error() + "\n" + ColorReset } return ColorGreen + fmt.Sprintf("Proxy rule %q (ID: %s) added successfully.\n", rule.Name, rule.ID) + ColorReset case "remove", "delete": if len(parts) < 2 { return ColorRed + "Usage: /ip reverse-proxy remove \n" + ColorReset } id := parts[1] if err := e.proxyMgr.DeleteRule(id); err != nil { return ColorRed + "Error: " + err.Error() + "\n" + ColorReset } return ColorGreen + fmt.Sprintf("Rule %q removed.\n", id) + ColorReset case "enable": if len(parts) < 2 { return ColorRed + "Usage: /ip reverse-proxy enable \n" + ColorReset } id := parts[1] if err := e.proxyMgr.SetRuleState(id, true); err != nil { return ColorRed + "Error: " + err.Error() + "\n" + ColorReset } return ColorGreen + fmt.Sprintf("Rule %q enabled.\n", id) + ColorReset case "disable": if len(parts) < 2 { return ColorRed + "Usage: /ip reverse-proxy disable \n" + ColorReset } id := parts[1] if err := e.proxyMgr.SetRuleState(id, false); err != nil { return ColorRed + "Error: " + err.Error() + "\n" + ColorReset } return ColorGreen + fmt.Sprintf("Rule %q disabled.\n", id) + ColorReset default: return ColorRed + "Usage: /ip reverse-proxy [print|status|add|set|remove|enable|disable]\n" + ColorReset } } // ==================== SD-WAN ==================== func (e *Executor) handleIPSDWAN(parts []string, user *auth.PublicUser) string { if e.sdwanMgr == nil { return ColorRed + "SD-WAN service not available\n" + ColorReset } if len(parts) > 0 && (strings.ToLower(parts[0]) == "member" || strings.ToLower(parts[0]) == "members") { return e.handleSDWANMember(parts[1:], user) } if len(parts) == 0 || parts[0] == "print" || parts[0] == "pr" || parts[0] == "show" { groups := e.sdwanMgr.List() var out strings.Builder telemetry := e.sdwanMgr.GetTelemetrySummary() out.WriteString(fmt.Sprintf("SD-WAN Groups: %d active / %d total | Members: %d healthy / %d total\n", telemetry.ActiveGroups, telemetry.TotalGroups, telemetry.HealthyMembers, telemetry.TotalMembers)) tbl := NewTable("Flags", "Name", "Mode", "Members", "Probe(s)", "Auto-NAT", "Comment") for _, g := range groups { flag := " " if g.Disabled { flag = "X" } else if g.Running { flag = "R" } autoNat := "no" if g.AutoNAT { autoNat = "yes" } tbl.AddRow(flag, g.Name, string(g.Mode), fmt.Sprintf("%d", len(g.Members)), fmt.Sprintf("%ds", g.ProbeInterval), autoNat, g.Comment) } out.WriteString(tbl.String()) return out.String() } if parts[0] == "status" { t := e.sdwanMgr.GetTelemetrySummary() var out strings.Builder out.WriteString(ColorCyan + ColorBold + "DebTik SD-WAN Telemetry Status:\n" + ColorReset) out.WriteString(fmt.Sprintf(" Total Groups: %d\n", t.TotalGroups)) out.WriteString(fmt.Sprintf(" Active Groups: %d\n", t.ActiveGroups)) out.WriteString(fmt.Sprintf(" Total Members: %d\n", t.TotalMembers)) out.WriteString(fmt.Sprintf(" Healthy Links: %d\n", t.HealthyMembers)) out.WriteString(fmt.Sprintf(" Degraded Links: %d\n", t.DegradedMembers)) out.WriteString(fmt.Sprintf(" Down Links: %d\n\n", t.DownMembers)) groups := e.sdwanMgr.List() for _, g := range groups { out.WriteString(fmt.Sprintf("Group: %s [%s] (Members: %d)\n", g.Name, g.Mode, len(g.Members))) for _, m := range g.Members { statusCol := ColorGreen + string(m.Status) + ColorReset if m.Status == sdwan.StatusDegraded { statusCol = ColorYellow + string(m.Status) + ColorReset } else if m.Status == sdwan.StatusDown { statusCol = ColorRed + string(m.Status) + ColorReset } else if m.Status == sdwan.StatusDisabled { statusCol = ColorDim + string(m.Status) + ColorReset } out.WriteString(fmt.Sprintf(" - %s (GW: %s) -> Status: %s | Latency: %.1fms | Loss: %.1f%% | Jitter: %.1fms | Table: %d\n", m.Interface, m.Gateway, statusCol, m.LiveLatency, m.PacketLoss, m.Jitter, m.TableID)) } } return out.String() } if user != nil && user.Role != auth.RoleFull { return ColorRed + "Permission denied: admin role required\n" + ColorReset } sub := strings.ToLower(parts[0]) switch sub { case "add": _, params := parseArgs(parts[1:]) name := params["name"] if name == "" { return ColorRed + "Usage: /ip sdwan add name= [mode=weighted|pcc|lowest-latency|lowest-loss|failover] [auto-nat=yes|no] [probe-interval=3] [comment=\"...\"]\n" + ColorReset } mode := sdwan.SDWANMode(params["mode"]) if mode == "" { mode = sdwan.ModeWeighted } autoNat := (params["auto-nat"] == "yes" || params["auto-nat"] == "true") probeInt := 3 if pStr, ok := params["probe-interval"]; ok { if v, err := strconv.Atoi(pStr); err == nil && v > 0 { probeInt = v } } group := &sdwan.SDWANGroup{ Name: name, Mode: mode, HashFields: params["hash-fields"], AutoNAT: autoNat, ProbeInterval: probeInt, Comment: params["comment"], Disabled: (params["disabled"] == "yes" || params["disabled"] == "true"), } if err := e.sdwanMgr.Save(group); err != nil { return ColorRed + "Error adding SD-WAN group: " + err.Error() + "\n" + ColorReset } return ColorGreen + fmt.Sprintf("SD-WAN group %q created.\n", name) + ColorReset case "set": pos, params := parseArgs(parts[1:]) if len(pos) == 0 && params["name"] == "" && params["id"] == "" { return ColorRed + "Usage: /ip sdwan set [mode=...] [auto-nat=yes|no] [probe-interval=N] [comment=...]\n" + ColorReset } idOrName := "" if len(pos) > 0 { idOrName = pos[0] } else if params["name"] != "" { idOrName = params["name"] } else { idOrName = params["id"] } group, found := e.sdwanMgr.Get(idOrName) if !found { return ColorRed + fmt.Sprintf("SD-WAN group %q not found\n", idOrName) + ColorReset } if m, ok := params["mode"]; ok { group.Mode = sdwan.SDWANMode(m) } if hf, ok := params["hash-fields"]; ok { group.HashFields = hf } if an, ok := params["auto-nat"]; ok { group.AutoNAT = (an == "yes" || an == "true") } if pi, ok := params["probe-interval"]; ok { if v, err := strconv.Atoi(pi); err == nil && v > 0 { group.ProbeInterval = v } } if c, ok := params["comment"]; ok { group.Comment = c } if err := e.sdwanMgr.Save(group); err != nil { return ColorRed + "Error updating SD-WAN group: " + err.Error() + "\n" + ColorReset } return ColorGreen + fmt.Sprintf("SD-WAN group %q updated.\n", group.Name) + ColorReset case "remove", "delete", "del": if len(parts) < 2 { return ColorRed + "Usage: /ip sdwan remove \n" + ColorReset } idOrName := parts[1] if err := e.sdwanMgr.Delete(idOrName); err != nil { return ColorRed + "Error deleting SD-WAN group: " + err.Error() + "\n" + ColorReset } return ColorGreen + fmt.Sprintf("SD-WAN group %q removed.\n", idOrName) + ColorReset case "enable": if len(parts) < 2 { return ColorRed + "Usage: /ip sdwan enable \n" + ColorReset } idOrName := parts[1] if err := e.sdwanMgr.SetState(idOrName, false); err != nil { return ColorRed + "Error enabling SD-WAN group: " + err.Error() + "\n" + ColorReset } return ColorGreen + fmt.Sprintf("SD-WAN group %q enabled.\n", idOrName) + ColorReset case "disable": if len(parts) < 2 { return ColorRed + "Usage: /ip sdwan disable \n" + ColorReset } idOrName := parts[1] if err := e.sdwanMgr.SetState(idOrName, true); err != nil { return ColorRed + "Error disabling SD-WAN group: " + err.Error() + "\n" + ColorReset } return ColorGreen + fmt.Sprintf("SD-WAN group %q disabled.\n", idOrName) + ColorReset default: return ColorRed + "Usage: /ip sdwan [print|status|add|set|remove|enable|disable|member]\n" + ColorReset } } func (e *Executor) handleSDWANMember(parts []string, user *auth.PublicUser) string { if len(parts) == 0 || parts[0] == "print" || parts[0] == "pr" || parts[0] == "show" { _, params := parseArgs(parts) groupFilter := params["group"] groups := e.sdwanMgr.List() tbl := NewTable("Flags", "Group", "Interface", "Gateway", "Weight", "Priority", "Target", "Status", "Latency", "Loss", "Jitter") for _, g := range groups { if groupFilter != "" && !strings.EqualFold(g.Name, groupFilter) && !strings.EqualFold(g.ID, groupFilter) { continue } for _, m := range g.Members { flag := " " if m.Disabled || g.Disabled { flag = "X" } statusCol := string(m.Status) tbl.AddRow( flag, g.Name, m.Interface, m.Gateway, fmt.Sprintf("%d", m.Weight), fmt.Sprintf("%d", m.Priority), m.TargetProbe, statusCol, fmt.Sprintf("%.1fms", m.LiveLatency), fmt.Sprintf("%.1f%%", m.PacketLoss), fmt.Sprintf("%.1fms", m.Jitter), ) } } return tbl.String() } if user != nil && user.Role != auth.RoleFull { return ColorRed + "Permission denied: admin role required\n" + ColorReset } sub := strings.ToLower(parts[0]) switch sub { case "add": _, params := parseArgs(parts[1:]) groupName := params["group"] iface := params["interface"] if groupName == "" || iface == "" { return ColorRed + "Usage: /ip sdwan member add group= interface= [gateway=] [weight=50] [priority=1] [target-probe=1.1.1.1] [max-latency=250] [max-loss=20] [comment=\"...\"]\n" + ColorReset } weight := 50 if wStr, ok := params["weight"]; ok { if v, err := strconv.Atoi(wStr); err == nil && v > 0 { weight = v } } priority := 1 if pStr, ok := params["priority"]; ok { if v, err := strconv.Atoi(pStr); err == nil && v > 0 { priority = v } } maxLat := 250 if lStr, ok := params["max-latency"]; ok { if v, err := strconv.Atoi(lStr); err == nil && v > 0 { maxLat = v } } maxLoss := 20 if mlStr, ok := params["max-loss"]; ok { if v, err := strconv.Atoi(mlStr); err == nil && v >= 0 { maxLoss = v } } targetProbe := params["target-probe"] if targetProbe == "" { targetProbe = "1.1.1.1" } mem := &sdwan.SDWANMember{ Interface: iface, Gateway: params["gateway"], Weight: weight, Priority: priority, TargetProbe: targetProbe, MaxLatency: maxLat, MaxLoss: maxLoss, Comment: params["comment"], Disabled: (params["disabled"] == "yes" || params["disabled"] == "true"), } if err := e.sdwanMgr.AddMember(groupName, mem); err != nil { return ColorRed + "Error adding member: " + err.Error() + "\n" + ColorReset } return ColorGreen + fmt.Sprintf("Interface %s added to SD-WAN group %s.\n", iface, groupName) + ColorReset case "set": _, params := parseArgs(parts[1:]) groupName := params["group"] iface := params["interface"] if groupName == "" || iface == "" { return ColorRed + "Usage: /ip sdwan member set group= interface= [gateway=...] [weight=...] [priority=...] [target-probe=...]\n" + ColorReset } group, found := e.sdwanMgr.Get(groupName) if !found { return ColorRed + fmt.Sprintf("SD-WAN group %q not found\n", groupName) + ColorReset } var targetMember *sdwan.SDWANMember for _, m := range group.Members { if m.Interface == iface || m.ID == iface { targetMember = m break } } if targetMember == nil { return ColorRed + fmt.Sprintf("Member %q not found in group %s\n", iface, groupName) + ColorReset } if gw, ok := params["gateway"]; ok { targetMember.Gateway = gw } if wStr, ok := params["weight"]; ok { if v, err := strconv.Atoi(wStr); err == nil && v > 0 { targetMember.Weight = v } } if pStr, ok := params["priority"]; ok { if v, err := strconv.Atoi(pStr); err == nil && v > 0 { targetMember.Priority = v } } if tp, ok := params["target-probe"]; ok { targetMember.TargetProbe = tp } if lStr, ok := params["max-latency"]; ok { if v, err := strconv.Atoi(lStr); err == nil && v > 0 { targetMember.MaxLatency = v } } if mlStr, ok := params["max-loss"]; ok { if v, err := strconv.Atoi(mlStr); err == nil && v >= 0 { targetMember.MaxLoss = v } } if c, ok := params["comment"]; ok { targetMember.Comment = c } if dStr, ok := params["disabled"]; ok { targetMember.Disabled = (dStr == "yes" || dStr == "true") } if err := e.sdwanMgr.UpdateMember(groupName, targetMember); err != nil { return ColorRed + "Error updating member: " + err.Error() + "\n" + ColorReset } return ColorGreen + fmt.Sprintf("Member %s in SD-WAN group %s updated.\n", iface, groupName) + ColorReset case "remove", "delete", "del": _, params := parseArgs(parts[1:]) groupName := params["group"] iface := params["interface"] if groupName == "" && len(parts) >= 3 { groupName = parts[1] iface = parts[2] } if groupName == "" || iface == "" { return ColorRed + "Usage: /ip sdwan member remove group= interface=\n" + ColorReset } if err := e.sdwanMgr.DeleteMember(groupName, iface); err != nil { return ColorRed + "Error removing member: " + err.Error() + "\n" + ColorReset } return ColorGreen + fmt.Sprintf("Member %s removed from SD-WAN group %s.\n", iface, groupName) + ColorReset case "enable": _, params := parseArgs(parts[1:]) groupName := params["group"] iface := params["interface"] if groupName == "" || iface == "" { return ColorRed + "Usage: /ip sdwan member enable group= interface=\n" + ColorReset } group, found := e.sdwanMgr.Get(groupName) if !found { return ColorRed + fmt.Sprintf("SD-WAN group %q not found\n", groupName) + ColorReset } var targetMember *sdwan.SDWANMember for _, m := range group.Members { if m.Interface == iface || m.ID == iface { targetMember = m break } } if targetMember == nil { return ColorRed + fmt.Sprintf("Member %q not found in group %s\n", iface, groupName) + ColorReset } targetMember.Disabled = false if err := e.sdwanMgr.UpdateMember(groupName, targetMember); err != nil { return ColorRed + "Error enabling member: " + err.Error() + "\n" + ColorReset } return ColorGreen + fmt.Sprintf("Member %s in SD-WAN group %s enabled.\n", iface, groupName) + ColorReset case "disable": _, params := parseArgs(parts[1:]) groupName := params["group"] iface := params["interface"] if groupName == "" || iface == "" { return ColorRed + "Usage: /ip sdwan member disable group= interface=\n" + ColorReset } group, found := e.sdwanMgr.Get(groupName) if !found { return ColorRed + fmt.Sprintf("SD-WAN group %q not found\n", groupName) + ColorReset } var targetMember *sdwan.SDWANMember for _, m := range group.Members { if m.Interface == iface || m.ID == iface { targetMember = m break } } if targetMember == nil { return ColorRed + fmt.Sprintf("Member %q not found in group %s\n", iface, groupName) + ColorReset } targetMember.Disabled = true if err := e.sdwanMgr.UpdateMember(groupName, targetMember); err != nil { return ColorRed + "Error disabling member: " + err.Error() + "\n" + ColorReset } return ColorGreen + fmt.Sprintf("Member %s in SD-WAN group %s disabled.\n", iface, groupName) + ColorReset default: return ColorRed + "Usage: /ip sdwan member [print|add|set|remove|enable|disable]\n" + ColorReset } } // Helpers for sorting/filtering func sortStrings(s []string) { sort.Strings(s) }