executor.go 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655
  1. package cli
  2. import (
  3. "fmt"
  4. "os/exec"
  5. "strconv"
  6. "strings"
  7. "proxmox-router/internal/auth"
  8. "proxmox-router/internal/dhcp"
  9. "proxmox-router/internal/dns"
  10. "proxmox-router/internal/firewall"
  11. "proxmox-router/internal/network"
  12. "proxmox-router/internal/routing"
  13. "proxmox-router/internal/system"
  14. )
  15. type Executor struct {
  16. authMgr *auth.Manager
  17. netMgr *network.Manager
  18. routingMgr *routing.RoutingManager
  19. fwMgr *firewall.FirewallManager
  20. dhcpMgr *dhcp.Manager
  21. dhcpServMgr *dhcp.ServerManager
  22. dnsMgr *dns.Manager
  23. }
  24. func NewExecutor(
  25. authMgr *auth.Manager,
  26. netMgr *network.Manager,
  27. routingMgr *routing.RoutingManager,
  28. fwMgr *firewall.FirewallManager,
  29. dhcpMgr *dhcp.Manager,
  30. dhcpServMgr *dhcp.ServerManager,
  31. dnsMgr *dns.Manager,
  32. ) *Executor {
  33. return &Executor{
  34. authMgr: authMgr,
  35. netMgr: netMgr,
  36. routingMgr: routingMgr,
  37. fwMgr: fwMgr,
  38. dhcpMgr: dhcpMgr,
  39. dhcpServMgr: dhcpServMgr,
  40. dnsMgr: dnsMgr,
  41. }
  42. }
  43. func (e *Executor) Execute(rawCmd string, user *auth.PublicUser) (string, bool) {
  44. cmd := strings.TrimSpace(rawCmd)
  45. if cmd == "" {
  46. return "", false
  47. }
  48. // Remove leading slash if present (RouterOS compatibility e.g. /ip route print)
  49. cmd = strings.TrimPrefix(cmd, "/")
  50. parts := strings.Fields(cmd)
  51. if len(parts) == 0 {
  52. return "", false
  53. }
  54. mainCmd := strings.ToLower(parts[0])
  55. switch mainCmd {
  56. case "quit", "exit":
  57. return ColorYellow + "Connection closed. Goodbye!\n" + ColorReset, true
  58. case "help", "?":
  59. return e.showHelp(), false
  60. case "clear":
  61. return "\033[2J\033[H", false
  62. case "ping":
  63. return e.handlePing(parts[1:]), false
  64. case "traceroute":
  65. return e.handleTraceroute(parts[1:]), false
  66. case "system":
  67. return e.handleSystem(parts[1:], user), false
  68. case "interface":
  69. return e.handleInterface(parts[1:], user), false
  70. case "ip":
  71. return e.handleIP(parts[1:], user), false
  72. case "user":
  73. return e.handleUser(parts[1:], user), false
  74. default:
  75. return fmt.Sprintf(ColorRed+"Unknown command or syntax error: %q. Type 'help' or '?' for command list.\n"+ColorReset, cmd), false
  76. }
  77. }
  78. func (e *Executor) handleInterface(parts []string, user *auth.PublicUser) string {
  79. if len(parts) == 0 || parts[0] == "print" || parts[0] == "pr" || parts[0] == "show" || parts[0] == "list" {
  80. ifaces, err := e.netMgr.ListInterfaces()
  81. if err != nil {
  82. return ColorRed + "Error listing interfaces: " + err.Error() + ColorReset + "\n"
  83. }
  84. tbl := NewTable("#", "NAME", "TYPE", "STATE", "MTU", "MAC", "IP ADDRESSES")
  85. for idx, iface := range ifaces {
  86. stateColor := ColorGreen
  87. if iface.State != "UP" {
  88. stateColor = ColorRed
  89. }
  90. stateStr := fmt.Sprintf("%s[%s]%s", stateColor, iface.State, ColorReset)
  91. var ips []string
  92. for _, a := range iface.IPv4 {
  93. ips = append(ips, a)
  94. }
  95. tbl.AddRow(
  96. fmt.Sprintf("%d", idx),
  97. ColorBold+iface.Name+ColorReset,
  98. iface.Type,
  99. stateStr,
  100. fmt.Sprintf("%d", iface.MTU),
  101. iface.MAC,
  102. strings.Join(ips, ", "),
  103. )
  104. }
  105. return tbl.Render()
  106. }
  107. if parts[0] == "set" {
  108. if user.Role == auth.RoleRead {
  109. return ColorRed + "Permission denied: read-only user cannot modify interface state\n" + ColorReset
  110. }
  111. kv := parseKeyValues(parts[1:])
  112. name := ""
  113. if len(parts) > 1 && !strings.Contains(parts[1], "=") {
  114. name = parts[1]
  115. }
  116. if n, ok := kv["name"]; ok {
  117. name = n
  118. }
  119. if name == "" {
  120. return ColorRed + "Usage: interface set <name> disabled=yes|no\n" + ColorReset
  121. }
  122. if dis, ok := kv["disabled"]; ok {
  123. up := strings.ToLower(dis) != "yes"
  124. if err := e.netMgr.SetInterfaceState(name, up); err != nil {
  125. return ColorRed + "Error: " + err.Error() + ColorReset + "\n"
  126. }
  127. return ColorGreen + fmt.Sprintf("Interface %s state updated\n", name) + ColorReset
  128. }
  129. }
  130. return ColorYellow + "Usage: interface print | interface set <name> disabled=yes|no\n" + ColorReset
  131. }
  132. func (e *Executor) handleIP(parts []string, user *auth.PublicUser) string {
  133. if len(parts) == 0 {
  134. return ColorYellow + "Usage: ip [address|route|dhcp-client|dhcp-server|firewall|dns] print\n" + ColorReset
  135. }
  136. sub := strings.ToLower(parts[0])
  137. switch sub {
  138. case "address", "addr":
  139. return e.handleIPAddress(parts[1:], user)
  140. case "route":
  141. return e.handleIPRoute(parts[1:], user)
  142. case "dhcp-client":
  143. return e.handleDHCPClient(parts[1:], user)
  144. case "dhcp-server":
  145. return e.handleDHCPServer(parts[1:], user)
  146. case "firewall":
  147. return e.handleFirewall(parts[1:], user)
  148. case "dns":
  149. return e.handleDNS(parts[1:], user)
  150. default:
  151. return ColorRed + fmt.Sprintf("Unknown ip subsystem: %q. Try 'ip address', 'ip route', 'ip firewall', etc.\n", sub) + ColorReset
  152. }
  153. }
  154. func (e *Executor) handleIPAddress(parts []string, user *auth.PublicUser) string {
  155. if len(parts) == 0 || parts[0] == "print" || parts[0] == "pr" || parts[0] == "show" {
  156. ifaces, err := e.netMgr.ListInterfaces()
  157. if err != nil {
  158. return ColorRed + "Error: " + err.Error() + ColorReset + "\n"
  159. }
  160. tbl := NewTable("#", "INTERFACE", "ADDRESS", "TYPE", "STATUS")
  161. idx := 0
  162. for _, iface := range ifaces {
  163. for _, ip := range iface.IPv4 {
  164. tbl.AddRow(
  165. fmt.Sprintf("%d", idx),
  166. ColorBold+iface.Name+ColorReset,
  167. ColorGreen+ip+ColorReset,
  168. iface.Type,
  169. iface.State,
  170. )
  171. idx++
  172. }
  173. }
  174. return tbl.Render()
  175. }
  176. if parts[0] == "add" {
  177. if user.Role == auth.RoleRead {
  178. return ColorRed + "Permission denied: read-only user cannot add IP address\n" + ColorReset
  179. }
  180. kv := parseKeyValues(parts[1:])
  181. addr := kv["address"]
  182. iface := kv["interface"]
  183. if addr == "" || iface == "" {
  184. return ColorYellow + "Usage: ip address add address=X.X.X.X/YY interface=<iface>\n" + ColorReset
  185. }
  186. if err := e.netMgr.AddInterfaceIP(iface, addr); err != nil {
  187. return ColorRed + "Failed to add address: " + err.Error() + ColorReset + "\n"
  188. }
  189. return ColorGreen + fmt.Sprintf("Assigned %s to %s successfully\n", addr, iface) + ColorReset
  190. }
  191. if parts[0] == "remove" || parts[0] == "del" {
  192. if user.Role == auth.RoleRead {
  193. return ColorRed + "Permission denied: read-only user cannot remove IP address\n" + ColorReset
  194. }
  195. kv := parseKeyValues(parts[1:])
  196. addr := kv["address"]
  197. iface := kv["interface"]
  198. if addr == "" || iface == "" {
  199. return ColorYellow + "Usage: ip address remove address=X.X.X.X/YY interface=<iface>\n" + ColorReset
  200. }
  201. if err := e.netMgr.DeleteInterfaceIP(iface, addr); err != nil {
  202. return ColorRed + "Failed to remove address: " + err.Error() + ColorReset + "\n"
  203. }
  204. return ColorGreen + fmt.Sprintf("Removed %s from %s successfully\n", addr, iface) + ColorReset
  205. }
  206. return ColorYellow + "Usage: ip address print | ip address add address=... interface=... | ip address remove address=... interface=...\n" + ColorReset
  207. }
  208. func (e *Executor) handleIPRoute(parts []string, user *auth.PublicUser) string {
  209. if len(parts) == 0 || parts[0] == "print" || parts[0] == "pr" || parts[0] == "show" {
  210. routes := e.routingMgr.ListRoutes()
  211. tbl := NewTable("#", "DST-ADDRESS", "GATEWAY", "INTERFACE", "METRIC", "TABLE", "FLAGS")
  212. for idx, r := range routes {
  213. flags := "DAC" // Dynamic Active Connected
  214. if r.Protocol == "static" {
  215. flags = "DAS" // Dynamic Active Static
  216. } else if r.Protocol == "dhcp" {
  217. flags = "DAD" // Dynamic Active DHCP
  218. }
  219. tbl.AddRow(
  220. fmt.Sprintf("%d", idx),
  221. ColorBold+r.DstAddress+ColorReset,
  222. r.Gateway,
  223. r.Interface,
  224. fmt.Sprintf("%d", r.Distance),
  225. r.RoutingTable,
  226. ColorGreen+flags+ColorReset,
  227. )
  228. }
  229. return tbl.Render()
  230. }
  231. if parts[0] == "add" {
  232. if user.Role == auth.RoleRead {
  233. return ColorRed + "Permission denied: read-only user cannot add routes\n" + ColorReset
  234. }
  235. kv := parseKeyValues(parts[1:])
  236. dst := kv["dst-address"]
  237. if dst == "" {
  238. dst = kv["dst"]
  239. }
  240. gw := kv["gateway"]
  241. if gw == "" {
  242. gw = kv["gw"]
  243. }
  244. dist := 1
  245. if dStr, ok := kv["distance"]; ok {
  246. if parsed, err := strconv.Atoi(dStr); err == nil && parsed > 0 {
  247. dist = parsed
  248. }
  249. }
  250. if dst == "" || gw == "" {
  251. return ColorYellow + "Usage: ip route add dst-address=X.X.X.X/YY gateway=Z.Z.Z.Z [distance=N]\n" + ColorReset
  252. }
  253. newRoute := routing.RouteItem{
  254. DstAddress: dst,
  255. Gateway: gw,
  256. Distance: dist,
  257. RoutingTable: "main",
  258. Active: true,
  259. }
  260. if err := e.routingMgr.SaveRoute(newRoute); err != nil {
  261. return ColorRed + "Failed to add route: " + err.Error() + ColorReset + "\n"
  262. }
  263. return ColorGreen + fmt.Sprintf("Added route to %s via %s (distance %d)\n", dst, gw, dist) + ColorReset
  264. }
  265. return ColorYellow + "Usage: ip route print | ip route add dst-address=... gateway=... [distance=...]\n" + ColorReset
  266. }
  267. func (e *Executor) handleDHCPClient(parts []string, user *auth.PublicUser) string {
  268. clients := e.dhcpMgr.ListClients()
  269. tbl := NewTable("#", "INTERFACE", "STATUS", "IP ADDRESS", "GATEWAY", "DHCP-SERVER", "EXPIRES")
  270. for idx, c := range clients {
  271. stColor := ColorYellow
  272. if c.Status == "bound" {
  273. stColor = ColorGreen
  274. } else if c.Status == "disabled" || c.Status == "stopped" {
  275. stColor = ColorRed
  276. }
  277. tbl.AddRow(
  278. fmt.Sprintf("%d", idx),
  279. ColorBold+c.Interface+ColorReset,
  280. stColor+c.Status+ColorReset,
  281. c.IPAddress,
  282. c.Gateway,
  283. c.DHCPServer,
  284. c.ExpiresIn,
  285. )
  286. }
  287. return tbl.Render()
  288. }
  289. func (e *Executor) handleDHCPServer(parts []string, user *auth.PublicUser) string {
  290. if len(parts) > 0 && parts[0] == "lease" {
  291. leases := e.dhcpServMgr.ListLeases()
  292. tbl := NewTable("#", "ADDRESS", "MAC-ADDRESS", "HOSTNAME", "EXPIRES-IN", "SERVER")
  293. for idx, l := range leases {
  294. tbl.AddRow(
  295. fmt.Sprintf("%d", idx),
  296. ColorGreen+l.Address+ColorReset,
  297. l.MACAddress,
  298. l.HostName,
  299. l.ExpiresIn,
  300. l.Server,
  301. )
  302. }
  303. return tbl.Render()
  304. }
  305. servers := e.dhcpServMgr.ListServers()
  306. tbl := NewTable("#", "INTERFACE", "SUBNET", "IP-POOL", "LEASE-TIME", "STATUS")
  307. for idx, s := range servers {
  308. st := ColorGreen + "running" + ColorReset
  309. if s.Disabled {
  310. st = ColorRed + "disabled" + ColorReset
  311. }
  312. tbl.AddRow(
  313. fmt.Sprintf("%d", idx),
  314. ColorBold+s.Interface+ColorReset,
  315. s.Network,
  316. s.AddressPool,
  317. s.LeaseTime,
  318. st,
  319. )
  320. }
  321. return tbl.Render()
  322. }
  323. func (e *Executor) handleFirewall(parts []string, user *auth.PublicUser) string {
  324. if len(parts) == 0 {
  325. return ColorYellow + "Usage: ip firewall [filter|nat|mangle] print\n" + ColorReset
  326. }
  327. sub := strings.ToLower(parts[0])
  328. switch sub {
  329. case "filter":
  330. rules := e.fwMgr.ListFilterRules()
  331. tbl := NewTable("#", "CHAIN", "ACTION", "PROTOCOL", "SRC-ADDRESS", "DST-ADDRESS", "IN-IFACE", "OUT-IFACE", "COMMENT")
  332. for idx, r := range rules {
  333. tbl.AddRow(
  334. fmt.Sprintf("%d", idx),
  335. r.Chain,
  336. ColorBold+r.Action+ColorReset,
  337. r.Protocol,
  338. r.SrcAddress,
  339. r.DstAddress,
  340. r.InInterface,
  341. r.OutInterface,
  342. ColorDim+r.Comment+ColorReset,
  343. )
  344. }
  345. return tbl.Render()
  346. case "nat":
  347. rules := e.fwMgr.ListNATRules()
  348. tbl := NewTable("#", "CHAIN", "ACTION", "OUT-INTERFACE", "TO-ADDRESS", "COMMENT")
  349. for idx, r := range rules {
  350. out := r.OutInterface
  351. if r.OutInterfaceList != "" {
  352. out = fmt.Sprintf("list:%s", r.OutInterfaceList)
  353. }
  354. tbl.AddRow(
  355. fmt.Sprintf("%d", idx),
  356. r.Chain,
  357. ColorGreen+r.Action+ColorReset,
  358. out,
  359. r.ToAddresses,
  360. ColorDim+r.Comment+ColorReset,
  361. )
  362. }
  363. return tbl.Render()
  364. default:
  365. return ColorYellow + "Usage: ip firewall filter print | ip firewall nat print\n" + ColorReset
  366. }
  367. }
  368. func (e *Executor) handleDNS(parts []string, user *auth.PublicUser) string {
  369. if len(parts) > 0 && parts[0] == "cache" {
  370. if len(parts) > 1 && parts[1] == "flush" {
  371. if user.Role == auth.RoleRead {
  372. return ColorRed + "Permission denied: read-only user cannot flush DNS cache\n" + ColorReset
  373. }
  374. if err := e.dnsMgr.FlushCache(); err != nil {
  375. return ColorRed + "Error flushing DNS cache: " + err.Error() + ColorReset + "\n"
  376. }
  377. return ColorGreen + "DNS Cache flushed successfully\n" + ColorReset
  378. }
  379. }
  380. settings := e.dnsMgr.GetSettings()
  381. var sb strings.Builder
  382. sb.WriteString(ColorCyan + ColorBold + "DNS Settings:\n" + ColorReset)
  383. sb.WriteString(fmt.Sprintf(" Upstream Servers : %s\n", strings.Join(settings.Servers, ", ")))
  384. sb.WriteString(fmt.Sprintf(" Dynamic Servers : %s\n", strings.Join(settings.DynamicServers, ", ")))
  385. sb.WriteString(fmt.Sprintf(" Domain : %s\n", settings.Domain))
  386. sb.WriteString(fmt.Sprintf(" Cache Size : %d entries\n", settings.CacheSize))
  387. sb.WriteString(fmt.Sprintf(" Allow Remote Reqs : %t\n", settings.AllowRemoteRequests))
  388. return sb.String()
  389. }
  390. func (e *Executor) handleUser(parts []string, user *auth.PublicUser) string {
  391. if len(parts) == 0 || parts[0] == "print" || parts[0] == "pr" {
  392. if user.Role != auth.RoleFull {
  393. return ColorRed + "Permission denied: administrative privileges required\n" + ColorReset
  394. }
  395. users := e.authMgr.ListUsers()
  396. tbl := NewTable("#", "USERNAME", "ROLE", "DISABLED", "COMMENT", "LAST LOGIN")
  397. for idx, u := range users {
  398. disStr := ColorGreen + "no" + ColorReset
  399. if u.Disabled {
  400. disStr = ColorRed + "yes" + ColorReset
  401. }
  402. last := ColorDim + "never" + ColorReset
  403. if !u.LastLoginAt.IsZero() {
  404. last = u.LastLoginAt.Format("2006-01-02 15:04")
  405. }
  406. tbl.AddRow(
  407. fmt.Sprintf("%d", idx),
  408. ColorBold+u.Username+ColorReset,
  409. ColorYellow+strings.ToUpper(string(u.Role))+ColorReset,
  410. disStr,
  411. u.Comment,
  412. last,
  413. )
  414. }
  415. return tbl.Render()
  416. }
  417. if parts[0] == "add" {
  418. if user.Role != auth.RoleFull {
  419. return ColorRed + "Permission denied: administrative privileges required\n" + ColorReset
  420. }
  421. kv := parseKeyValues(parts[1:])
  422. name := kv["name"]
  423. pass := kv["password"]
  424. role := auth.Role(kv["role"])
  425. if role == "" {
  426. role = auth.RoleRead
  427. }
  428. comment := kv["comment"]
  429. if name == "" || pass == "" {
  430. return ColorYellow + "Usage: user add name=<username> password=<password> [role=full|write|read] [comment=\"...\"]\n" + ColorReset
  431. }
  432. req := auth.CreateUserRequest{
  433. Username: name,
  434. Password: pass,
  435. Role: role,
  436. Comment: comment,
  437. }
  438. if err := e.authMgr.CreateUser(req); err != nil {
  439. return ColorRed + "Failed to create user: " + err.Error() + ColorReset + "\n"
  440. }
  441. return ColorGreen + fmt.Sprintf("User %q (%s) created successfully\n", name, role) + ColorReset
  442. }
  443. if parts[0] == "remove" || parts[0] == "del" {
  444. if user.Role != auth.RoleFull {
  445. return ColorRed + "Permission denied: administrative privileges required\n" + ColorReset
  446. }
  447. if len(parts) < 2 {
  448. return ColorYellow + "Usage: user remove <username>\n" + ColorReset
  449. }
  450. target := parts[1]
  451. if err := e.authMgr.DeleteUser(target, user.Username); err != nil {
  452. return ColorRed + "Failed to remove user: " + err.Error() + ColorReset + "\n"
  453. }
  454. return ColorGreen + fmt.Sprintf("User %q removed successfully\n", target) + ColorReset
  455. }
  456. return ColorYellow + "Usage: user print | user add name=... password=... [role=...] | user remove <username>\n" + ColorReset
  457. }
  458. func (e *Executor) handleSystem(parts []string, user *auth.PublicUser) string {
  459. if len(parts) == 0 {
  460. return ColorYellow + "Usage: system [resource|identity|reboot]\n" + ColorReset
  461. }
  462. sub := strings.ToLower(parts[0])
  463. switch sub {
  464. case "resource":
  465. st, err := system.GetStatus()
  466. if err != nil {
  467. return ColorRed + "Error getting system status: " + err.Error() + ColorReset + "\n"
  468. }
  469. var sb strings.Builder
  470. sb.WriteString(ColorCyan + ColorBold + "System Resources:\n" + ColorReset)
  471. sb.WriteString(fmt.Sprintf(" Appliance Hostname : %s\n", st.Hostname))
  472. sb.WriteString(fmt.Sprintf(" Operating System : %s\n", st.OS))
  473. sb.WriteString(fmt.Sprintf(" Kernel Version : %s\n", st.Kernel))
  474. sb.WriteString(fmt.Sprintf(" Uptime : %s\n", st.UptimeHuman))
  475. sb.WriteString(fmt.Sprintf(" CPU Cores : %d cores\n", st.CPUCores))
  476. sb.WriteString(fmt.Sprintf(" CPU Utilization : %.1f%%\n", st.CPUPct))
  477. sb.WriteString(fmt.Sprintf(" Load Average : %s\n", strings.Join(st.CPULoad, ", ")))
  478. sb.WriteString(fmt.Sprintf(" Total Memory : %d MB\n", st.MemTotalMB))
  479. sb.WriteString(fmt.Sprintf(" Used Memory : %d MB (%.1f%%)\n", st.MemUsedMB, st.MemUsagePct))
  480. sb.WriteString(fmt.Sprintf(" Default Gateway : %s\n", st.DefaultGateway))
  481. return sb.String()
  482. case "identity":
  483. st, _ := system.GetStatus()
  484. if len(parts) > 1 && parts[1] == "set" {
  485. if user.Role == auth.RoleRead {
  486. return ColorRed + "Permission denied: read-only user cannot change identity\n" + ColorReset
  487. }
  488. kv := parseKeyValues(parts[2:])
  489. newName := kv["name"]
  490. if newName == "" {
  491. return ColorYellow + "Usage: system identity set name=<new-name>\n" + ColorReset
  492. }
  493. _ = exec.Command("hostnamectl", "set-hostname", newName).Run()
  494. return ColorGreen + fmt.Sprintf("Identity set to %q\n", newName) + ColorReset
  495. }
  496. return fmt.Sprintf(ColorCyan+"name: %s%s\n", st.Hostname, ColorReset)
  497. case "reboot":
  498. if user.Role != auth.RoleFull {
  499. return ColorRed + "Permission denied: full administrative privileges required to reboot\n" + ColorReset
  500. }
  501. _ = exec.Command("/sbin/reboot").Start()
  502. return ColorRed + ColorBold + "System is going down for reboot NOW...\n" + ColorReset
  503. default:
  504. return ColorYellow + "Usage: system resource print | system identity print | system reboot\n" + ColorReset
  505. }
  506. }
  507. func (e *Executor) handlePing(args []string) string {
  508. if len(args) == 0 {
  509. return ColorYellow + "Usage: ping <host> [count=N]\n" + ColorReset
  510. }
  511. host := args[0]
  512. count := "4"
  513. for _, a := range args[1:] {
  514. if strings.HasPrefix(a, "count=") {
  515. count = strings.TrimPrefix(a, "count=")
  516. }
  517. }
  518. cmd := exec.Command("ping", "-c", count, "-W", "2", host)
  519. out, err := cmd.CombinedOutput()
  520. if err != nil {
  521. return fmt.Sprintf("%s%s%s\n", ColorRed, string(out), ColorReset)
  522. }
  523. return string(out)
  524. }
  525. func (e *Executor) handleTraceroute(args []string) string {
  526. if len(args) == 0 {
  527. return ColorYellow + "Usage: traceroute <host>\n" + ColorReset
  528. }
  529. host := args[0]
  530. cmd := exec.Command("traceroute", "-m", "15", "-w", "1", host)
  531. out, err := cmd.CombinedOutput()
  532. if err != nil {
  533. // Fallback to ping/tracepath if traceroute not found
  534. return fmt.Sprintf("%s%s%s\n", ColorRed, string(out), ColorReset)
  535. }
  536. return string(out)
  537. }
  538. func (e *Executor) showHelp() string {
  539. var sb strings.Builder
  540. sb.WriteString(ColorCyan + ColorBold + "\n--- DebTik RouterOS CLI Commands Reference ---\n" + ColorReset)
  541. sb.WriteString(ColorBold + "Interfaces:\n" + ColorReset)
  542. sb.WriteString(" interface print List all interfaces, MACs and IPs\n")
  543. sb.WriteString(" interface set <name> disabled=yes|no Enable or disable interface\n")
  544. sb.WriteString(ColorBold + "IP & Routing:\n" + ColorReset)
  545. sb.WriteString(" ip address print List configured IP addresses\n")
  546. sb.WriteString(" ip address add address=X/Y interface=DEV Add IP address to interface\n")
  547. sb.WriteString(" ip address remove address=X/Y interface=DEV Remove IP address\n")
  548. sb.WriteString(" ip route print Display routing table\n")
  549. sb.WriteString(" ip route add dst-address=X/Y gateway=Z Add static route\n")
  550. sb.WriteString(ColorBold + "DHCP & Services:\n" + ColorReset)
  551. sb.WriteString(" ip dhcp-client print Show DHCP client leases\n")
  552. sb.WriteString(" ip dhcp-server print Show DHCP server instances\n")
  553. sb.WriteString(" ip dhcp-server lease print Show active client leases\n")
  554. sb.WriteString(" ip dns print Show DNS cache and forwarders\n")
  555. sb.WriteString(" ip dns cache flush Flush DNS cache\n")
  556. sb.WriteString(ColorBold + "Firewall:\n" + ColorReset)
  557. sb.WriteString(" ip firewall filter print List Filter rules\n")
  558. sb.WriteString(" ip firewall nat print List NAT rules\n")
  559. sb.WriteString(ColorBold + "Users & Management:\n" + ColorReset)
  560. sb.WriteString(" user print List system users (Admin)\n")
  561. sb.WriteString(" user add name=X password=Y role=full|write Add local user\n")
  562. sb.WriteString(" user remove <name> Delete local user\n")
  563. sb.WriteString(" password Change current password\n")
  564. sb.WriteString(ColorBold + "System & Utilities:\n" + ColorReset)
  565. sb.WriteString(" system resource print CPU, RAM, Uptime and load\n")
  566. sb.WriteString(" system identity print Show router hostname\n")
  567. sb.WriteString(" system identity set name=<name> Set router hostname\n")
  568. sb.WriteString(" system reboot Reboot system\n")
  569. sb.WriteString(" ping <host> [count=N] Send ICMP echo requests\n")
  570. sb.WriteString(" traceroute <host> Trace route to target\n")
  571. sb.WriteString(" clear Clear screen\n")
  572. sb.WriteString(" exit / quit Disconnect from session\n")
  573. sb.WriteString("\n")
  574. return sb.String()
  575. }
  576. func parseKeyValues(parts []string) map[string]string {
  577. res := make(map[string]string)
  578. for _, p := range parts {
  579. if eq := strings.Index(p, "="); eq != -1 {
  580. k := strings.ToLower(strings.TrimSpace(p[:eq]))
  581. v := strings.TrimSpace(p[eq+1:])
  582. v = strings.Trim(v, "\"")
  583. res[k] = v
  584. }
  585. }
  586. return res
  587. }