|
@@ -0,0 +1,655 @@
|
|
|
|
|
+package cli
|
|
|
|
|
+
|
|
|
|
|
+import (
|
|
|
|
|
+ "fmt"
|
|
|
|
|
+ "os/exec"
|
|
|
|
|
+ "strconv"
|
|
|
|
|
+ "strings"
|
|
|
|
|
+
|
|
|
|
|
+ "proxmox-router/internal/auth"
|
|
|
|
|
+ "proxmox-router/internal/dhcp"
|
|
|
|
|
+ "proxmox-router/internal/dns"
|
|
|
|
|
+ "proxmox-router/internal/firewall"
|
|
|
|
|
+ "proxmox-router/internal/network"
|
|
|
|
|
+ "proxmox-router/internal/routing"
|
|
|
|
|
+ "proxmox-router/internal/system"
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
|
|
+type Executor struct {
|
|
|
|
|
+ authMgr *auth.Manager
|
|
|
|
|
+ netMgr *network.Manager
|
|
|
|
|
+ routingMgr *routing.RoutingManager
|
|
|
|
|
+ fwMgr *firewall.FirewallManager
|
|
|
|
|
+ dhcpMgr *dhcp.Manager
|
|
|
|
|
+ dhcpServMgr *dhcp.ServerManager
|
|
|
|
|
+ dnsMgr *dns.Manager
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func NewExecutor(
|
|
|
|
|
+ authMgr *auth.Manager,
|
|
|
|
|
+ netMgr *network.Manager,
|
|
|
|
|
+ routingMgr *routing.RoutingManager,
|
|
|
|
|
+ fwMgr *firewall.FirewallManager,
|
|
|
|
|
+ dhcpMgr *dhcp.Manager,
|
|
|
|
|
+ dhcpServMgr *dhcp.ServerManager,
|
|
|
|
|
+ dnsMgr *dns.Manager,
|
|
|
|
|
+) *Executor {
|
|
|
|
|
+ return &Executor{
|
|
|
|
|
+ authMgr: authMgr,
|
|
|
|
|
+ netMgr: netMgr,
|
|
|
|
|
+ routingMgr: routingMgr,
|
|
|
|
|
+ fwMgr: fwMgr,
|
|
|
|
|
+ dhcpMgr: dhcpMgr,
|
|
|
|
|
+ dhcpServMgr: dhcpServMgr,
|
|
|
|
|
+ dnsMgr: dnsMgr,
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func (e *Executor) Execute(rawCmd string, user *auth.PublicUser) (string, bool) {
|
|
|
|
|
+ cmd := strings.TrimSpace(rawCmd)
|
|
|
|
|
+ if cmd == "" {
|
|
|
|
|
+ return "", false
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Remove leading slash if present (RouterOS compatibility e.g. /ip route print)
|
|
|
|
|
+ cmd = strings.TrimPrefix(cmd, "/")
|
|
|
|
|
+ parts := strings.Fields(cmd)
|
|
|
|
|
+ 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 "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 "user":
|
|
|
|
|
+ return e.handleUser(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 (e *Executor) handleInterface(parts []string, user *auth.PublicUser) string {
|
|
|
|
|
+ if len(parts) == 0 || parts[0] == "print" || parts[0] == "pr" || parts[0] == "show" || parts[0] == "list" {
|
|
|
|
|
+ ifaces, err := e.netMgr.ListInterfaces()
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return ColorRed + "Error listing interfaces: " + err.Error() + ColorReset + "\n"
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ tbl := NewTable("#", "NAME", "TYPE", "STATE", "MTU", "MAC", "IP ADDRESSES")
|
|
|
|
|
+ for idx, iface := range ifaces {
|
|
|
|
|
+ stateColor := ColorGreen
|
|
|
|
|
+ if iface.State != "UP" {
|
|
|
|
|
+ stateColor = ColorRed
|
|
|
|
|
+ }
|
|
|
|
|
+ stateStr := fmt.Sprintf("%s[%s]%s", stateColor, iface.State, ColorReset)
|
|
|
|
|
+
|
|
|
|
|
+ var ips []string
|
|
|
|
|
+ for _, a := range iface.IPv4 {
|
|
|
|
|
+ ips = append(ips, a)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ tbl.AddRow(
|
|
|
|
|
+ fmt.Sprintf("%d", idx),
|
|
|
|
|
+ ColorBold+iface.Name+ColorReset,
|
|
|
|
|
+ iface.Type,
|
|
|
|
|
+ stateStr,
|
|
|
|
|
+ fmt.Sprintf("%d", iface.MTU),
|
|
|
|
|
+ iface.MAC,
|
|
|
|
|
+ strings.Join(ips, ", "),
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+ return tbl.Render()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if parts[0] == "set" {
|
|
|
|
|
+ if user.Role == auth.RoleRead {
|
|
|
|
|
+ return ColorRed + "Permission denied: read-only user cannot modify interface state\n" + ColorReset
|
|
|
|
|
+ }
|
|
|
|
|
+ kv := parseKeyValues(parts[1:])
|
|
|
|
|
+ name := ""
|
|
|
|
|
+ if len(parts) > 1 && !strings.Contains(parts[1], "=") {
|
|
|
|
|
+ name = parts[1]
|
|
|
|
|
+ }
|
|
|
|
|
+ if n, ok := kv["name"]; ok {
|
|
|
|
|
+ name = n
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if name == "" {
|
|
|
|
|
+ return ColorRed + "Usage: interface set <name> disabled=yes|no\n" + ColorReset
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if dis, ok := kv["disabled"]; ok {
|
|
|
|
|
+ up := strings.ToLower(dis) != "yes"
|
|
|
|
|
+ if err := e.netMgr.SetInterfaceState(name, up); err != nil {
|
|
|
|
|
+ return ColorRed + "Error: " + err.Error() + ColorReset + "\n"
|
|
|
|
|
+ }
|
|
|
|
|
+ return ColorGreen + fmt.Sprintf("Interface %s state updated\n", name) + ColorReset
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return ColorYellow + "Usage: interface print | interface set <name> disabled=yes|no\n" + ColorReset
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func (e *Executor) handleIP(parts []string, user *auth.PublicUser) string {
|
|
|
|
|
+ if len(parts) == 0 {
|
|
|
|
|
+ return ColorYellow + "Usage: ip [address|route|dhcp-client|dhcp-server|firewall|dns] print\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.handleDHCPClient(parts[1:], user)
|
|
|
|
|
+ case "dhcp-server":
|
|
|
|
|
+ return e.handleDHCPServer(parts[1:], user)
|
|
|
|
|
+ case "firewall":
|
|
|
|
|
+ return e.handleFirewall(parts[1:], user)
|
|
|
|
|
+ case "dns":
|
|
|
|
|
+ return e.handleDNS(parts[1:], user)
|
|
|
|
|
+ default:
|
|
|
|
|
+ return ColorRed + fmt.Sprintf("Unknown ip subsystem: %q. Try 'ip address', 'ip route', 'ip firewall', etc.\n", sub) + ColorReset
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func (e *Executor) handleIPAddress(parts []string, user *auth.PublicUser) string {
|
|
|
|
|
+ if len(parts) == 0 || parts[0] == "print" || parts[0] == "pr" || parts[0] == "show" {
|
|
|
|
|
+ ifaces, err := e.netMgr.ListInterfaces()
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return ColorRed + "Error: " + err.Error() + ColorReset + "\n"
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ tbl := NewTable("#", "INTERFACE", "ADDRESS", "TYPE", "STATUS")
|
|
|
|
|
+ idx := 0
|
|
|
|
|
+ for _, iface := range ifaces {
|
|
|
|
|
+ for _, ip := range iface.IPv4 {
|
|
|
|
|
+ tbl.AddRow(
|
|
|
|
|
+ fmt.Sprintf("%d", idx),
|
|
|
|
|
+ ColorBold+iface.Name+ColorReset,
|
|
|
|
|
+ ColorGreen+ip+ColorReset,
|
|
|
|
|
+ iface.Type,
|
|
|
|
|
+ iface.State,
|
|
|
|
|
+ )
|
|
|
|
|
+ idx++
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return tbl.Render()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if parts[0] == "add" {
|
|
|
|
|
+ if user.Role == auth.RoleRead {
|
|
|
|
|
+ return ColorRed + "Permission denied: read-only user cannot add IP address\n" + ColorReset
|
|
|
|
|
+ }
|
|
|
|
|
+ kv := parseKeyValues(parts[1:])
|
|
|
|
|
+ addr := kv["address"]
|
|
|
|
|
+ iface := kv["interface"]
|
|
|
|
|
+ if addr == "" || iface == "" {
|
|
|
|
|
+ return ColorYellow + "Usage: ip address add address=X.X.X.X/YY interface=<iface>\n" + ColorReset
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if err := e.netMgr.AddInterfaceIP(iface, addr); err != nil {
|
|
|
|
|
+ return ColorRed + "Failed to add address: " + err.Error() + ColorReset + "\n"
|
|
|
|
|
+ }
|
|
|
|
|
+ return ColorGreen + fmt.Sprintf("Assigned %s to %s successfully\n", addr, iface) + ColorReset
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if parts[0] == "remove" || parts[0] == "del" {
|
|
|
|
|
+ if user.Role == auth.RoleRead {
|
|
|
|
|
+ return ColorRed + "Permission denied: read-only user cannot remove IP address\n" + ColorReset
|
|
|
|
|
+ }
|
|
|
|
|
+ kv := parseKeyValues(parts[1:])
|
|
|
|
|
+ addr := kv["address"]
|
|
|
|
|
+ iface := kv["interface"]
|
|
|
|
|
+ if addr == "" || iface == "" {
|
|
|
|
|
+ return ColorYellow + "Usage: ip address remove address=X.X.X.X/YY interface=<iface>\n" + ColorReset
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if err := e.netMgr.DeleteInterfaceIP(iface, addr); err != nil {
|
|
|
|
|
+ return ColorRed + "Failed to remove address: " + err.Error() + ColorReset + "\n"
|
|
|
|
|
+ }
|
|
|
|
|
+ return ColorGreen + fmt.Sprintf("Removed %s from %s successfully\n", addr, iface) + ColorReset
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return ColorYellow + "Usage: ip address print | ip address add address=... interface=... | ip address remove address=... interface=...\n" + ColorReset
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func (e *Executor) handleIPRoute(parts []string, user *auth.PublicUser) string {
|
|
|
|
|
+ if len(parts) == 0 || parts[0] == "print" || parts[0] == "pr" || parts[0] == "show" {
|
|
|
|
|
+ routes := e.routingMgr.ListRoutes()
|
|
|
|
|
+
|
|
|
|
|
+ tbl := NewTable("#", "DST-ADDRESS", "GATEWAY", "INTERFACE", "METRIC", "TABLE", "FLAGS")
|
|
|
|
|
+ for idx, r := range routes {
|
|
|
|
|
+ flags := "DAC" // Dynamic Active Connected
|
|
|
|
|
+ if r.Protocol == "static" {
|
|
|
|
|
+ flags = "DAS" // Dynamic Active Static
|
|
|
|
|
+ } else if r.Protocol == "dhcp" {
|
|
|
|
|
+ flags = "DAD" // Dynamic Active DHCP
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ tbl.AddRow(
|
|
|
|
|
+ fmt.Sprintf("%d", idx),
|
|
|
|
|
+ ColorBold+r.DstAddress+ColorReset,
|
|
|
|
|
+ r.Gateway,
|
|
|
|
|
+ r.Interface,
|
|
|
|
|
+ fmt.Sprintf("%d", r.Distance),
|
|
|
|
|
+ r.RoutingTable,
|
|
|
|
|
+ ColorGreen+flags+ColorReset,
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+ return tbl.Render()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if parts[0] == "add" {
|
|
|
|
|
+ if user.Role == auth.RoleRead {
|
|
|
|
|
+ return ColorRed + "Permission denied: read-only user cannot add routes\n" + ColorReset
|
|
|
|
|
+ }
|
|
|
|
|
+ kv := parseKeyValues(parts[1:])
|
|
|
|
|
+ dst := kv["dst-address"]
|
|
|
|
|
+ if dst == "" {
|
|
|
|
|
+ dst = kv["dst"]
|
|
|
|
|
+ }
|
|
|
|
|
+ gw := kv["gateway"]
|
|
|
|
|
+ if gw == "" {
|
|
|
|
|
+ gw = kv["gw"]
|
|
|
|
|
+ }
|
|
|
|
|
+ dist := 1
|
|
|
|
|
+ if dStr, ok := kv["distance"]; ok {
|
|
|
|
|
+ if parsed, err := strconv.Atoi(dStr); err == nil && parsed > 0 {
|
|
|
|
|
+ dist = parsed
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if dst == "" || gw == "" {
|
|
|
|
|
+ return ColorYellow + "Usage: ip route add dst-address=X.X.X.X/YY gateway=Z.Z.Z.Z [distance=N]\n" + ColorReset
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ newRoute := routing.RouteItem{
|
|
|
|
|
+ DstAddress: dst,
|
|
|
|
|
+ Gateway: gw,
|
|
|
|
|
+ Distance: dist,
|
|
|
|
|
+ RoutingTable: "main",
|
|
|
|
|
+ Active: true,
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if err := e.routingMgr.SaveRoute(newRoute); err != nil {
|
|
|
|
|
+ return ColorRed + "Failed to add route: " + err.Error() + ColorReset + "\n"
|
|
|
|
|
+ }
|
|
|
|
|
+ return ColorGreen + fmt.Sprintf("Added route to %s via %s (distance %d)\n", dst, gw, dist) + ColorReset
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return ColorYellow + "Usage: ip route print | ip route add dst-address=... gateway=... [distance=...]\n" + ColorReset
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func (e *Executor) handleDHCPClient(parts []string, user *auth.PublicUser) string {
|
|
|
|
|
+ clients := e.dhcpMgr.ListClients()
|
|
|
|
|
+ tbl := NewTable("#", "INTERFACE", "STATUS", "IP ADDRESS", "GATEWAY", "DHCP-SERVER", "EXPIRES")
|
|
|
|
|
+ for idx, c := range clients {
|
|
|
|
|
+ stColor := ColorYellow
|
|
|
|
|
+ if c.Status == "bound" {
|
|
|
|
|
+ stColor = ColorGreen
|
|
|
|
|
+ } else if c.Status == "disabled" || c.Status == "stopped" {
|
|
|
|
|
+ stColor = ColorRed
|
|
|
|
|
+ }
|
|
|
|
|
+ tbl.AddRow(
|
|
|
|
|
+ fmt.Sprintf("%d", idx),
|
|
|
|
|
+ ColorBold+c.Interface+ColorReset,
|
|
|
|
|
+ stColor+c.Status+ColorReset,
|
|
|
|
|
+ c.IPAddress,
|
|
|
|
|
+ c.Gateway,
|
|
|
|
|
+ c.DHCPServer,
|
|
|
|
|
+ c.ExpiresIn,
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+ return tbl.Render()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func (e *Executor) handleDHCPServer(parts []string, user *auth.PublicUser) string {
|
|
|
|
|
+ if len(parts) > 0 && parts[0] == "lease" {
|
|
|
|
|
+ leases := e.dhcpServMgr.ListLeases()
|
|
|
|
|
+ tbl := NewTable("#", "ADDRESS", "MAC-ADDRESS", "HOSTNAME", "EXPIRES-IN", "SERVER")
|
|
|
|
|
+ for idx, l := range leases {
|
|
|
|
|
+ tbl.AddRow(
|
|
|
|
|
+ fmt.Sprintf("%d", idx),
|
|
|
|
|
+ ColorGreen+l.Address+ColorReset,
|
|
|
|
|
+ l.MACAddress,
|
|
|
|
|
+ l.HostName,
|
|
|
|
|
+ l.ExpiresIn,
|
|
|
|
|
+ l.Server,
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+ return tbl.Render()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ servers := e.dhcpServMgr.ListServers()
|
|
|
|
|
+ tbl := NewTable("#", "INTERFACE", "SUBNET", "IP-POOL", "LEASE-TIME", "STATUS")
|
|
|
|
|
+ for idx, s := range servers {
|
|
|
|
|
+ st := ColorGreen + "running" + ColorReset
|
|
|
|
|
+ if s.Disabled {
|
|
|
|
|
+ st = ColorRed + "disabled" + ColorReset
|
|
|
|
|
+ }
|
|
|
|
|
+ tbl.AddRow(
|
|
|
|
|
+ fmt.Sprintf("%d", idx),
|
|
|
|
|
+ ColorBold+s.Interface+ColorReset,
|
|
|
|
|
+ s.Network,
|
|
|
|
|
+ s.AddressPool,
|
|
|
|
|
+ s.LeaseTime,
|
|
|
|
|
+ st,
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+ return tbl.Render()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func (e *Executor) handleFirewall(parts []string, user *auth.PublicUser) string {
|
|
|
|
|
+ if len(parts) == 0 {
|
|
|
|
|
+ return ColorYellow + "Usage: ip firewall [filter|nat|mangle] print\n" + ColorReset
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ sub := strings.ToLower(parts[0])
|
|
|
|
|
+ switch sub {
|
|
|
|
|
+ case "filter":
|
|
|
|
|
+ rules := e.fwMgr.ListFilterRules()
|
|
|
|
|
+ tbl := NewTable("#", "CHAIN", "ACTION", "PROTOCOL", "SRC-ADDRESS", "DST-ADDRESS", "IN-IFACE", "OUT-IFACE", "COMMENT")
|
|
|
|
|
+ for idx, r := range rules {
|
|
|
|
|
+ tbl.AddRow(
|
|
|
|
|
+ fmt.Sprintf("%d", idx),
|
|
|
|
|
+ r.Chain,
|
|
|
|
|
+ ColorBold+r.Action+ColorReset,
|
|
|
|
|
+ r.Protocol,
|
|
|
|
|
+ r.SrcAddress,
|
|
|
|
|
+ r.DstAddress,
|
|
|
|
|
+ r.InInterface,
|
|
|
|
|
+ r.OutInterface,
|
|
|
|
|
+ ColorDim+r.Comment+ColorReset,
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+ return tbl.Render()
|
|
|
|
|
+
|
|
|
|
|
+ case "nat":
|
|
|
|
|
+ rules := e.fwMgr.ListNATRules()
|
|
|
|
|
+ tbl := NewTable("#", "CHAIN", "ACTION", "OUT-INTERFACE", "TO-ADDRESS", "COMMENT")
|
|
|
|
|
+ for idx, r := range rules {
|
|
|
|
|
+ out := r.OutInterface
|
|
|
|
|
+ if r.OutInterfaceList != "" {
|
|
|
|
|
+ out = fmt.Sprintf("list:%s", r.OutInterfaceList)
|
|
|
|
|
+ }
|
|
|
|
|
+ tbl.AddRow(
|
|
|
|
|
+ fmt.Sprintf("%d", idx),
|
|
|
|
|
+ r.Chain,
|
|
|
|
|
+ ColorGreen+r.Action+ColorReset,
|
|
|
|
|
+ out,
|
|
|
|
|
+ r.ToAddresses,
|
|
|
|
|
+ ColorDim+r.Comment+ColorReset,
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+ return tbl.Render()
|
|
|
|
|
+
|
|
|
|
|
+ default:
|
|
|
|
|
+ return ColorYellow + "Usage: ip firewall filter print | ip firewall nat print\n" + ColorReset
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func (e *Executor) handleDNS(parts []string, user *auth.PublicUser) string {
|
|
|
|
|
+ if len(parts) > 0 && parts[0] == "cache" {
|
|
|
|
|
+ if len(parts) > 1 && parts[1] == "flush" {
|
|
|
|
|
+ if user.Role == auth.RoleRead {
|
|
|
|
|
+ return ColorRed + "Permission denied: read-only user cannot flush DNS cache\n" + ColorReset
|
|
|
|
|
+ }
|
|
|
|
|
+ if err := e.dnsMgr.FlushCache(); err != nil {
|
|
|
|
|
+ return ColorRed + "Error flushing DNS cache: " + err.Error() + ColorReset + "\n"
|
|
|
|
|
+ }
|
|
|
|
|
+ return ColorGreen + "DNS Cache flushed successfully\n" + ColorReset
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ settings := e.dnsMgr.GetSettings()
|
|
|
|
|
+ var sb strings.Builder
|
|
|
|
|
+ sb.WriteString(ColorCyan + ColorBold + "DNS Settings:\n" + ColorReset)
|
|
|
|
|
+ sb.WriteString(fmt.Sprintf(" Upstream Servers : %s\n", strings.Join(settings.Servers, ", ")))
|
|
|
|
|
+ sb.WriteString(fmt.Sprintf(" Dynamic Servers : %s\n", strings.Join(settings.DynamicServers, ", ")))
|
|
|
|
|
+ sb.WriteString(fmt.Sprintf(" Domain : %s\n", settings.Domain))
|
|
|
|
|
+ sb.WriteString(fmt.Sprintf(" Cache Size : %d entries\n", settings.CacheSize))
|
|
|
|
|
+ sb.WriteString(fmt.Sprintf(" Allow Remote Reqs : %t\n", settings.AllowRemoteRequests))
|
|
|
|
|
+ return sb.String()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func (e *Executor) handleUser(parts []string, user *auth.PublicUser) string {
|
|
|
|
|
+ if len(parts) == 0 || parts[0] == "print" || parts[0] == "pr" {
|
|
|
|
|
+ if user.Role != auth.RoleFull {
|
|
|
|
|
+ return ColorRed + "Permission denied: administrative privileges required\n" + ColorReset
|
|
|
|
|
+ }
|
|
|
|
|
+ users := e.authMgr.ListUsers()
|
|
|
|
|
+ tbl := NewTable("#", "USERNAME", "ROLE", "DISABLED", "COMMENT", "LAST LOGIN")
|
|
|
|
|
+ for idx, u := range users {
|
|
|
|
|
+ disStr := ColorGreen + "no" + ColorReset
|
|
|
|
|
+ if u.Disabled {
|
|
|
|
|
+ disStr = ColorRed + "yes" + ColorReset
|
|
|
|
|
+ }
|
|
|
|
|
+ last := ColorDim + "never" + ColorReset
|
|
|
|
|
+ if !u.LastLoginAt.IsZero() {
|
|
|
|
|
+ last = u.LastLoginAt.Format("2006-01-02 15:04")
|
|
|
|
|
+ }
|
|
|
|
|
+ tbl.AddRow(
|
|
|
|
|
+ fmt.Sprintf("%d", idx),
|
|
|
|
|
+ ColorBold+u.Username+ColorReset,
|
|
|
|
|
+ ColorYellow+strings.ToUpper(string(u.Role))+ColorReset,
|
|
|
|
|
+ disStr,
|
|
|
|
|
+ u.Comment,
|
|
|
|
|
+ last,
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+ return tbl.Render()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if parts[0] == "add" {
|
|
|
|
|
+ if user.Role != auth.RoleFull {
|
|
|
|
|
+ return ColorRed + "Permission denied: administrative privileges required\n" + ColorReset
|
|
|
|
|
+ }
|
|
|
|
|
+ kv := parseKeyValues(parts[1:])
|
|
|
|
|
+ name := kv["name"]
|
|
|
|
|
+ pass := kv["password"]
|
|
|
|
|
+ role := auth.Role(kv["role"])
|
|
|
|
|
+ if role == "" {
|
|
|
|
|
+ role = auth.RoleRead
|
|
|
|
|
+ }
|
|
|
|
|
+ comment := kv["comment"]
|
|
|
|
|
+
|
|
|
|
|
+ if name == "" || pass == "" {
|
|
|
|
|
+ return ColorYellow + "Usage: user add name=<username> password=<password> [role=full|write|read] [comment=\"...\"]\n" + ColorReset
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ req := auth.CreateUserRequest{
|
|
|
|
|
+ Username: name,
|
|
|
|
|
+ Password: pass,
|
|
|
|
|
+ Role: role,
|
|
|
|
|
+ Comment: comment,
|
|
|
|
|
+ }
|
|
|
|
|
+ if err := e.authMgr.CreateUser(req); err != nil {
|
|
|
|
|
+ return ColorRed + "Failed to create user: " + err.Error() + ColorReset + "\n"
|
|
|
|
|
+ }
|
|
|
|
|
+ return ColorGreen + fmt.Sprintf("User %q (%s) created successfully\n", name, role) + ColorReset
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if parts[0] == "remove" || parts[0] == "del" {
|
|
|
|
|
+ if user.Role != auth.RoleFull {
|
|
|
|
|
+ return ColorRed + "Permission denied: administrative privileges required\n" + ColorReset
|
|
|
|
|
+ }
|
|
|
|
|
+ if len(parts) < 2 {
|
|
|
|
|
+ return ColorYellow + "Usage: user remove <username>\n" + ColorReset
|
|
|
|
|
+ }
|
|
|
|
|
+ target := parts[1]
|
|
|
|
|
+ if err := e.authMgr.DeleteUser(target, user.Username); err != nil {
|
|
|
|
|
+ return ColorRed + "Failed to remove user: " + err.Error() + ColorReset + "\n"
|
|
|
|
|
+ }
|
|
|
|
|
+ return ColorGreen + fmt.Sprintf("User %q removed successfully\n", target) + ColorReset
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return ColorYellow + "Usage: user print | user add name=... password=... [role=...] | user remove <username>\n" + ColorReset
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func (e *Executor) handleSystem(parts []string, user *auth.PublicUser) string {
|
|
|
|
|
+ if len(parts) == 0 {
|
|
|
|
|
+ return ColorYellow + "Usage: system [resource|identity|reboot]\n" + ColorReset
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ sub := strings.ToLower(parts[0])
|
|
|
|
|
+ switch sub {
|
|
|
|
|
+ case "resource":
|
|
|
|
|
+ st, err := system.GetStatus()
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return ColorRed + "Error getting system status: " + err.Error() + ColorReset + "\n"
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var sb strings.Builder
|
|
|
|
|
+ sb.WriteString(ColorCyan + ColorBold + "System Resources:\n" + ColorReset)
|
|
|
|
|
+ sb.WriteString(fmt.Sprintf(" Appliance Hostname : %s\n", st.Hostname))
|
|
|
|
|
+ sb.WriteString(fmt.Sprintf(" Operating System : %s\n", st.OS))
|
|
|
|
|
+ sb.WriteString(fmt.Sprintf(" Kernel Version : %s\n", st.Kernel))
|
|
|
|
|
+ sb.WriteString(fmt.Sprintf(" Uptime : %s\n", st.UptimeHuman))
|
|
|
|
|
+ sb.WriteString(fmt.Sprintf(" CPU Cores : %d cores\n", st.CPUCores))
|
|
|
|
|
+ sb.WriteString(fmt.Sprintf(" CPU Utilization : %.1f%%\n", st.CPUPct))
|
|
|
|
|
+ sb.WriteString(fmt.Sprintf(" Load Average : %s\n", strings.Join(st.CPULoad, ", ")))
|
|
|
|
|
+ sb.WriteString(fmt.Sprintf(" Total Memory : %d MB\n", st.MemTotalMB))
|
|
|
|
|
+ sb.WriteString(fmt.Sprintf(" Used Memory : %d MB (%.1f%%)\n", st.MemUsedMB, st.MemUsagePct))
|
|
|
|
|
+ sb.WriteString(fmt.Sprintf(" Default Gateway : %s\n", st.DefaultGateway))
|
|
|
|
|
+ return sb.String()
|
|
|
|
|
+
|
|
|
|
|
+ case "identity":
|
|
|
|
|
+ st, _ := system.GetStatus()
|
|
|
|
|
+ if len(parts) > 1 && parts[1] == "set" {
|
|
|
|
|
+ if user.Role == auth.RoleRead {
|
|
|
|
|
+ return ColorRed + "Permission denied: read-only user cannot change identity\n" + ColorReset
|
|
|
|
|
+ }
|
|
|
|
|
+ kv := parseKeyValues(parts[2:])
|
|
|
|
|
+ newName := kv["name"]
|
|
|
|
|
+ if newName == "" {
|
|
|
|
|
+ return ColorYellow + "Usage: system identity set name=<new-name>\n" + ColorReset
|
|
|
|
|
+ }
|
|
|
|
|
+ _ = exec.Command("hostnamectl", "set-hostname", newName).Run()
|
|
|
|
|
+ return ColorGreen + fmt.Sprintf("Identity set to %q\n", newName) + ColorReset
|
|
|
|
|
+ }
|
|
|
|
|
+ return fmt.Sprintf(ColorCyan+"name: %s%s\n", st.Hostname, ColorReset)
|
|
|
|
|
+
|
|
|
|
|
+ case "reboot":
|
|
|
|
|
+ if user.Role != auth.RoleFull {
|
|
|
|
|
+ return ColorRed + "Permission denied: full administrative privileges required to reboot\n" + ColorReset
|
|
|
|
|
+ }
|
|
|
|
|
+ _ = exec.Command("/sbin/reboot").Start()
|
|
|
|
|
+ return ColorRed + ColorBold + "System is going down for reboot NOW...\n" + ColorReset
|
|
|
|
|
+
|
|
|
|
|
+ default:
|
|
|
|
|
+ return ColorYellow + "Usage: system resource print | system identity print | system reboot\n" + ColorReset
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func (e *Executor) handlePing(args []string) string {
|
|
|
|
|
+ if len(args) == 0 {
|
|
|
|
|
+ return ColorYellow + "Usage: ping <host> [count=N]\n" + ColorReset
|
|
|
|
|
+ }
|
|
|
|
|
+ host := args[0]
|
|
|
|
|
+ count := "4"
|
|
|
|
|
+ for _, a := range args[1:] {
|
|
|
|
|
+ if strings.HasPrefix(a, "count=") {
|
|
|
|
|
+ count = strings.TrimPrefix(a, "count=")
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ cmd := exec.Command("ping", "-c", count, "-W", "2", host)
|
|
|
|
|
+ out, err := cmd.CombinedOutput()
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ return fmt.Sprintf("%s%s%s\n", ColorRed, string(out), ColorReset)
|
|
|
|
|
+ }
|
|
|
|
|
+ return string(out)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func (e *Executor) handleTraceroute(args []string) string {
|
|
|
|
|
+ if len(args) == 0 {
|
|
|
|
|
+ return ColorYellow + "Usage: traceroute <host>\n" + ColorReset
|
|
|
|
|
+ }
|
|
|
|
|
+ host := args[0]
|
|
|
|
|
+ cmd := exec.Command("traceroute", "-m", "15", "-w", "1", host)
|
|
|
|
|
+ out, err := cmd.CombinedOutput()
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ // Fallback to ping/tracepath if traceroute not found
|
|
|
|
|
+ return fmt.Sprintf("%s%s%s\n", ColorRed, string(out), ColorReset)
|
|
|
|
|
+ }
|
|
|
|
|
+ return string(out)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func (e *Executor) showHelp() string {
|
|
|
|
|
+ var sb strings.Builder
|
|
|
|
|
+ sb.WriteString(ColorCyan + ColorBold + "\n--- DebTik RouterOS CLI Commands Reference ---\n" + ColorReset)
|
|
|
|
|
+ sb.WriteString(ColorBold + "Interfaces:\n" + ColorReset)
|
|
|
|
|
+ sb.WriteString(" interface print List all interfaces, MACs and IPs\n")
|
|
|
|
|
+ sb.WriteString(" interface set <name> disabled=yes|no Enable or disable interface\n")
|
|
|
|
|
+ sb.WriteString(ColorBold + "IP & Routing:\n" + ColorReset)
|
|
|
|
|
+ sb.WriteString(" ip address print List configured IP addresses\n")
|
|
|
|
|
+ sb.WriteString(" ip address add address=X/Y interface=DEV Add IP address to interface\n")
|
|
|
|
|
+ sb.WriteString(" ip address remove address=X/Y interface=DEV Remove IP address\n")
|
|
|
|
|
+ sb.WriteString(" ip route print Display routing table\n")
|
|
|
|
|
+ sb.WriteString(" ip route add dst-address=X/Y gateway=Z Add static route\n")
|
|
|
|
|
+ sb.WriteString(ColorBold + "DHCP & Services:\n" + ColorReset)
|
|
|
|
|
+ sb.WriteString(" ip dhcp-client print Show DHCP client leases\n")
|
|
|
|
|
+ sb.WriteString(" ip dhcp-server print Show DHCP server instances\n")
|
|
|
|
|
+ sb.WriteString(" ip dhcp-server lease print Show active client leases\n")
|
|
|
|
|
+ sb.WriteString(" ip dns print Show DNS cache and forwarders\n")
|
|
|
|
|
+ sb.WriteString(" ip dns cache flush Flush DNS cache\n")
|
|
|
|
|
+ sb.WriteString(ColorBold + "Firewall:\n" + ColorReset)
|
|
|
|
|
+ sb.WriteString(" ip firewall filter print List Filter rules\n")
|
|
|
|
|
+ sb.WriteString(" ip firewall nat print List NAT rules\n")
|
|
|
|
|
+ sb.WriteString(ColorBold + "Users & Management:\n" + ColorReset)
|
|
|
|
|
+ sb.WriteString(" user print List system users (Admin)\n")
|
|
|
|
|
+ sb.WriteString(" user add name=X password=Y role=full|write Add local user\n")
|
|
|
|
|
+ sb.WriteString(" user remove <name> Delete local user\n")
|
|
|
|
|
+ sb.WriteString(" password Change current password\n")
|
|
|
|
|
+ sb.WriteString(ColorBold + "System & Utilities:\n" + ColorReset)
|
|
|
|
|
+ sb.WriteString(" system resource print CPU, RAM, Uptime and load\n")
|
|
|
|
|
+ sb.WriteString(" system identity print Show router hostname\n")
|
|
|
|
|
+ sb.WriteString(" system identity set name=<name> Set router hostname\n")
|
|
|
|
|
+ sb.WriteString(" system reboot Reboot system\n")
|
|
|
|
|
+ sb.WriteString(" ping <host> [count=N] Send ICMP echo requests\n")
|
|
|
|
|
+ sb.WriteString(" traceroute <host> Trace route to target\n")
|
|
|
|
|
+ sb.WriteString(" clear Clear screen\n")
|
|
|
|
|
+ sb.WriteString(" exit / quit Disconnect from session\n")
|
|
|
|
|
+ sb.WriteString("\n")
|
|
|
|
|
+ return sb.String()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func parseKeyValues(parts []string) map[string]string {
|
|
|
|
|
+ res := make(map[string]string)
|
|
|
|
|
+ for _, p := range parts {
|
|
|
|
|
+ if eq := strings.Index(p, "="); eq != -1 {
|
|
|
|
|
+ k := strings.ToLower(strings.TrimSpace(p[:eq]))
|
|
|
|
|
+ v := strings.TrimSpace(p[eq+1:])
|
|
|
|
|
+ v = strings.Trim(v, "\"")
|
|
|
|
|
+ res[k] = v
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return res
|
|
|
|
|
+}
|