package cli import ( "context" "fmt" "net" "os" "os/exec" "path/filepath" "sort" "strconv" "strings" "time" "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/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 } 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, ) *Executor { if backupMgr == nil { backupMgr = backup.NewManager("", "", "", nil) } 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, } } 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 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 "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 } } // --- 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) 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 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"] 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 == "" { return ColorRed + "Usage: /ip route add dst-address= gateway= [distance=1] [routing-table=main]\n" + ColorReset } r := routing.RouteItem{ DstAddress: dst, Gateway: gw, 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": 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": networks := e.dhcpServMgr.ListNetworks() tbl := NewTable("SUBNET", "GATEWAY", "DNS-SERVERS", "DOMAIN") for _, n := range networks { tbl.AddRow(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": 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) default: return ColorRed + "Unknown firewall table: " + tableType + "\n" + ColorReset } } 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, 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 "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 || parts[0] == "instance" { inst := e.dynRoutingMgr.GetBGPInstance() 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) return out } sub := strings.ToLower(parts[0]) switch sub { case "peer", "connection", "peers": peers := e.dynRoutingMgr.ListBGPPeers() tbl := NewTable("FLAGS", "NAME", "REMOTE-AS", "REMOTE-ADDRESS", "STATUS", "UPTIME") for _, p := range peers { flags := "" if p.Disabled { flags += "X" } else { flags += "E" } st := p.State if st == "" { st = "Active" } tbl.AddRow(flags, p.Name, fmt.Sprintf("%d", p.RemoteAS), p.RemoteAddress, st, p.Uptime) } return tbl.String() case "network", "networks": nets := e.dynRoutingMgr.ListBGPNetworks() tbl := NewTable("NETWORK", "COMMENT") for _, n := range nets { tbl.AddRow(n.Network, n.Comment) } return tbl.String() case "routes": 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) handleRoutingOSPF(parts []string, user *auth.PublicUser) string { if len(parts) == 0 || parts[0] == "instance" { instances := e.dynRoutingMgr.ListOSPFInstances() tbl := NewTable("NAME", "ROUTER-ID", "VERSION", "STATUS") for _, inst := range instances { st := "enabled" if inst.Disabled { st = "disabled" } tbl.AddRow(inst.Name, inst.RouterID, fmt.Sprintf("%d", inst.Version), st) } return tbl.String() } sub := strings.ToLower(parts[0]) switch sub { case "area", "areas": areas := e.dynRoutingMgr.ListOSPFAreas() tbl := NewTable("NAME", "AREA-ID", "INSTANCE", "TYPE") for _, a := range areas { tbl.AddRow(a.Name, a.AreaID, a.Instance, a.AreaType) } return tbl.String() case "interface", "interfaces": ifaces := e.dynRoutingMgr.ListOSPFInterfaces() tbl := NewTable("INTERFACE", "AREA", "COST", "PRIORITY", "NETWORK-TYPE") for _, i := range ifaces { tbl.AddRow(i.Interface, i.Area, fmt.Sprintf("%d", i.Cost), fmt.Sprintf("%d", i.Priority), i.NetworkType) } return tbl.String() case "neighbor", "neighbors": neighbors := e.dynRoutingMgr.GetOSPFNeighbors() tbl := NewTable("ROUTER-ID", "ADDRESS", "INTERFACE", "STATE", "PRIORITY") for _, n := range neighbors { tbl.AddRow(n.NeighborID, n.Address, n.Interface, n.State, fmt.Sprintf("%d", n.Priority)) } return tbl.String() default: return ColorRed + "Usage: /routing ospf [instance|area|interface|neighbor]\n" + ColorReset } } func (e *Executor) handleRoutingFilter(parts []string, user *auth.PublicUser) string { rules := e.dynRoutingMgr.ListRoutingFilters() tbl := NewTable("CHAIN", "RULE", "ACTION", "STATUS") for _, r := range rules { st := "enabled" if r.Disabled { st = "disabled" } tbl.AddRow(r.Chain, r.Rule, r.Action, st) } return tbl.String() } // ==================== TOOLS ==================== func (e *Executor) handleTool(parts []string) string { if len(parts) == 0 { return ColorYellow + "Tools available: ping, traceroute, bandwidth-test\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:]) default: return ColorRed + "Unknown tool: " + sub + "\n" + ColorReset } } 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 default: return ColorRed + "Unknown backup action: " + action + ". Use save, load, or print.\n" + ColorReset } 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 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(" /routing - Dynamic routing engine\n") b.WriteString(" /routing bgp - BGP instance, peers, routes (instance, peer, routes)\n") b.WriteString(" /routing ospf - OSPF areas, interfaces, neighbors (instance, area, neighbor)\n") b.WriteString(" /routing filter - Route filtering policies (print)\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(" /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, print)\n") b.WriteString(" /system reboot - Safe system restart\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 } } // Helpers for sorting/filtering func sortStrings(s []string) { sort.Strings(s) }