command_test.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376
  1. package cli
  2. import (
  3. "path/filepath"
  4. "reflect"
  5. "strings"
  6. "testing"
  7. "debtik/internal/aerotun"
  8. "debtik/internal/auth"
  9. "debtik/internal/dynamicrouting"
  10. "debtik/internal/sdwan"
  11. "debtik/internal/vpn"
  12. )
  13. func TestQuotedCommandArguments(t *testing.T) {
  14. got, err := splitCommand(`/interface aerotun set test comment="office uplink" psk='literal $\\key' name=""`)
  15. want := []string{"/interface", "aerotun", "set", "test", "comment=office uplink", `psk=literal $\\key`, "name="}
  16. if err != nil || !reflect.DeepEqual(got, want) {
  17. t.Fatalf("%q %v", got, err)
  18. }
  19. if _, err := splitCommand(`comment="unfinished`); err == nil {
  20. t.Fatal("accepted unterminated quote")
  21. }
  22. }
  23. func TestBareLogAndHandlerRecovery(t *testing.T) {
  24. e := &Executor{}
  25. user := &auth.PublicUser{Role: auth.RoleFull}
  26. output, _ := e.Execute("/log", user)
  27. if commandExitStatus(output) != 0 {
  28. t.Fatal(output)
  29. }
  30. output, _ = e.Execute("/interface print", user) // absent manager simulates a handler failure
  31. if commandExitStatus(output) == 0 || !strings.Contains(output, "still running") {
  32. t.Fatal(output)
  33. }
  34. output, _ = e.Execute("nonsense", user)
  35. if commandExitStatus(output) == 0 {
  36. t.Fatal("unknown command returned success")
  37. }
  38. output, _ = e.Execute("/help", user)
  39. if commandExitStatus(output) != 0 {
  40. t.Fatal("help returned failure")
  41. }
  42. }
  43. func TestAeroTUNCLIConfigurationAndPermissions(t *testing.T) {
  44. m := aerotun.NewManagerWithPath(filepath.Join(t.TempDir(), "aerotun.json"))
  45. defer m.StopAll()
  46. e := &Executor{vpnMgr: &vpn.Manager{AeroTUN: m}}
  47. admin := &auth.PublicUser{Role: auth.RoleFull}
  48. psk := strings.Repeat("12", 32)
  49. command := `/interface aerotun add name=atest mode=server local-address=10.90.0.1/30 peer-address=10.90.0.2/32 allowed-ips=10.90.0.2/32 psk=` + psk + ` comment="office uplink"`
  50. output, _ := e.Execute(command, admin)
  51. if commandExitStatus(output) != 0 {
  52. t.Fatal(output)
  53. }
  54. inst := m.List()[0]
  55. if !inst.Disabled || inst.PublicKey == "" || inst.Comment != "office uplink" {
  56. t.Fatal("draft configuration not retained")
  57. }
  58. for _, role := range []auth.Role{auth.RoleRead, auth.RoleWrite} {
  59. output, _ = e.Execute("/interface aerotun remove atest", &auth.PublicUser{Role: role})
  60. if commandExitStatus(output) == 0 {
  61. t.Fatal("non-admin removed tunnel")
  62. }
  63. }
  64. output, _ = e.Execute("/interface aerotun print detail=yes", admin)
  65. if strings.Contains(output, psk) || strings.Contains(output, inst.PrivateKey) || !strings.Contains(output, inst.PublicKey) {
  66. t.Fatal("secret disclosure or missing public identity")
  67. }
  68. output, _ = e.Execute(`/interface aerotun set atest comment="updated uplink"`, admin)
  69. if commandExitStatus(output) != 0 {
  70. t.Fatal(output)
  71. }
  72. updated, _ := m.Get(inst.ID)
  73. if updated.PSK != psk || updated.PublicKey != inst.PublicKey {
  74. t.Fatal("edit changed identity")
  75. }
  76. output, _ = e.Execute("/interface aerotun remove atest", admin)
  77. if commandExitStatus(output) != 0 || len(m.List()) != 0 {
  78. t.Fatal(output)
  79. }
  80. }
  81. func TestVXLANCLIConfigurationAndPermissions(t *testing.T) {
  82. vxlanMgr := vpn.NewVXLANManager(filepath.Join(t.TempDir(), "vxlan.json"))
  83. e := &Executor{vpnMgr: &vpn.Manager{VXLAN: vxlanMgr}}
  84. admin := &auth.PublicUser{Role: auth.RoleFull}
  85. viewer := &auth.PublicUser{Role: auth.RoleRead}
  86. // 1. Add VXLAN
  87. output, _ := e.Execute(`/interface vxlan add name=vxlan99 vni=99 port=4789 mtu=1500 comment="test overlay"`, admin)
  88. if strings.Contains(output, "Error") {
  89. t.Fatalf("unexpected error adding vxlan: %s", output)
  90. }
  91. // 2. Print VXLAN
  92. output, _ = e.Execute("/interface vxlan print", viewer)
  93. if !strings.Contains(output, "vxlan99") || !strings.Contains(output, "99") {
  94. t.Fatalf("vxlan99 not found in print output: %s", output)
  95. }
  96. // 3. Add VTEP
  97. output, _ = e.Execute("/interface vxlan vteps add interface=vxlan99 remote-ip=192.168.1.50 comment=peera", admin)
  98. if strings.Contains(output, "Error") {
  99. t.Fatalf("unexpected error adding vtep: %s", output)
  100. }
  101. // 4. Print VTEP
  102. output, _ = e.Execute("/interface vxlan vteps print", viewer)
  103. if !strings.Contains(output, "192.168.1.50") {
  104. t.Fatalf("vtep not found in print output: %s", output)
  105. }
  106. // 5. Viewer cannot modify
  107. output, _ = e.Execute("/interface vxlan remove vxlan99", viewer)
  108. if !strings.Contains(output, "Permission denied") {
  109. t.Fatalf("expected permission denied for viewer, got: %s", output)
  110. }
  111. // 6. Set comment
  112. output, _ = e.Execute(`/interface vxlan set vxlan99 comment="updated vxlan"`, admin)
  113. if strings.Contains(output, "Error") {
  114. t.Fatalf("unexpected error setting vxlan: %s", output)
  115. }
  116. // 7. Remove VTEP
  117. output, _ = e.Execute("/interface vxlan vteps remove interface=vxlan99 remote-ip=192.168.1.50", admin)
  118. if strings.Contains(output, "Error") {
  119. t.Fatalf("unexpected error removing vtep: %s", output)
  120. }
  121. // 8. Remove VXLAN
  122. output, _ = e.Execute("/interface vxlan remove vxlan99", admin)
  123. if strings.Contains(output, "Error") {
  124. t.Fatalf("unexpected error removing vxlan: %s", output)
  125. }
  126. }
  127. func TestSDWANCLIConfigurationAndPermissions(t *testing.T) {
  128. sdwanMgr := sdwan.NewManager(filepath.Join(t.TempDir(), "sdwan.json"))
  129. defer sdwanMgr.Stop()
  130. e := &Executor{sdwanMgr: sdwanMgr}
  131. admin := &auth.PublicUser{Role: auth.RoleFull}
  132. viewer := &auth.PublicUser{Role: auth.RoleRead}
  133. // 1. Add SD-WAN group
  134. output, _ := e.Execute(`/ip sdwan add name=sdwan0 mode=weighted auto-nat=yes probe-interval=5 comment="Multi-WAN LB"`, admin)
  135. if strings.Contains(output, "Error") {
  136. t.Fatalf("unexpected error adding sdwan group: %s", output)
  137. }
  138. // 2. Print SD-WAN groups
  139. output, _ = e.Execute("/ip sdwan print", viewer)
  140. if !strings.Contains(output, "sdwan0") || !strings.Contains(output, "weighted") {
  141. t.Fatalf("sdwan0 not found in print output: %s", output)
  142. }
  143. // 3. Status
  144. output, _ = e.Execute("/ip sdwan status", viewer)
  145. if !strings.Contains(output, "SD-WAN Telemetry Status") {
  146. t.Fatalf("status output unexpected: %s", output)
  147. }
  148. // 4. Add Members
  149. output, _ = e.Execute("/ip sdwan member add group=sdwan0 interface=eth0 gateway=192.168.1.1 weight=70 target-probe=1.1.1.1", admin)
  150. if strings.Contains(output, "Error") {
  151. t.Fatalf("error adding member eth0: %s", output)
  152. }
  153. output, _ = e.Execute("/ip sdwan member add group=sdwan0 interface=eth1 gateway=192.168.2.1 weight=30 target-probe=8.8.8.8", admin)
  154. if strings.Contains(output, "Error") {
  155. t.Fatalf("error adding member eth1: %s", output)
  156. }
  157. // 5. Print Members
  158. output, _ = e.Execute("/ip sdwan member print", viewer)
  159. if !strings.Contains(output, "eth0") || !strings.Contains(output, "eth1") {
  160. t.Fatalf("members not found in member print output: %s", output)
  161. }
  162. // 6. Permission check for viewer
  163. output, _ = e.Execute("/ip sdwan remove sdwan0", viewer)
  164. if !strings.Contains(output, "Permission denied") {
  165. t.Fatalf("expected permission denied for viewer, got: %s", output)
  166. }
  167. // 7. Member set & disable
  168. output, _ = e.Execute("/ip sdwan member set group=sdwan0 interface=eth0 weight=80", admin)
  169. if strings.Contains(output, "Error") {
  170. t.Fatalf("error setting member weight: %s", output)
  171. }
  172. output, _ = e.Execute("/ip sdwan member disable group=sdwan0 interface=eth1", admin)
  173. if strings.Contains(output, "Error") {
  174. t.Fatalf("error disabling member eth1: %s", output)
  175. }
  176. // 8. Group disable & enable
  177. output, _ = e.Execute("/ip sdwan disable sdwan0", admin)
  178. if strings.Contains(output, "Error") {
  179. t.Fatalf("error disabling sdwan0: %s", output)
  180. }
  181. output, _ = e.Execute("/ip sdwan enable sdwan0", admin)
  182. if strings.Contains(output, "Error") {
  183. t.Fatalf("error enabling sdwan0: %s", output)
  184. }
  185. // 9. Remove member
  186. output, _ = e.Execute("/ip sdwan member remove group=sdwan0 interface=eth1", admin)
  187. if strings.Contains(output, "Error") {
  188. t.Fatalf("error removing member eth1: %s", output)
  189. }
  190. // 10. Remove group
  191. output, _ = e.Execute("/ip sdwan remove sdwan0", admin)
  192. if strings.Contains(output, "Error") {
  193. t.Fatalf("error removing group sdwan0: %s", output)
  194. }
  195. }
  196. func TestRoutingOSPFCLI(t *testing.T) {
  197. dynMgr := dynamicrouting.NewManager(filepath.Join(t.TempDir(), "dynamic.json"))
  198. e := &Executor{dynRoutingMgr: dynMgr}
  199. admin := &auth.PublicUser{Role: auth.RoleFull}
  200. viewer := &auth.PublicUser{Role: auth.RoleRead}
  201. // 1. Instance commands
  202. out, _ := e.Execute("/routing ospf instance print", admin)
  203. if !strings.Contains(out, "NAME") {
  204. t.Fatalf("expected NAME table header in instance print: %s", out)
  205. }
  206. out, _ = e.Execute("/routing ospf instance add name=ospf-test router-id=10.255.0.1 redistribute-connected=yes redistribute-static=no default-metric=30", admin)
  207. if !strings.Contains(out, "saved") {
  208. t.Fatalf("failed to add ospf instance: %s", out)
  209. }
  210. out, _ = e.Execute("/routing ospf instance set ospf-test default-metric=45", admin)
  211. if !strings.Contains(out, "updated") {
  212. t.Fatalf("failed to set ospf instance: %s", out)
  213. }
  214. out, _ = e.Execute("/routing ospf instance disable ospf-test", admin)
  215. if !strings.Contains(out, "disabled") {
  216. t.Fatalf("failed to disable ospf instance: %s", out)
  217. }
  218. out, _ = e.Execute("/routing ospf instance enable ospf-test", admin)
  219. if !strings.Contains(out, "enabled") {
  220. t.Fatalf("failed to enable ospf instance: %s", out)
  221. }
  222. // Viewer permission check
  223. out, _ = e.Execute("/routing ospf instance remove ospf-test", viewer)
  224. if !strings.Contains(out, "Permission denied") {
  225. t.Fatalf("expected permission denied for viewer: %s", out)
  226. }
  227. // 2. Area commands
  228. out, _ = e.Execute("/routing ospf area print", admin)
  229. if !strings.Contains(out, "AREA-ID") {
  230. t.Fatalf("expected AREA-ID table header in area print: %s", out)
  231. }
  232. out, _ = e.Execute("/routing ospf area add name=area1 area-id=0.0.0.1 type=stub default-cost=20 no-summaries=yes", admin)
  233. if !strings.Contains(out, "saved") {
  234. t.Fatalf("failed to add ospf area: %s", out)
  235. }
  236. out, _ = e.Execute("/routing ospf area set area1 default-cost=35", admin)
  237. if !strings.Contains(out, "updated") {
  238. t.Fatalf("failed to set ospf area: %s", out)
  239. }
  240. // 3. Area range commands
  241. out, _ = e.Execute("/routing ospf area range print", admin)
  242. if !strings.Contains(out, "PREFIX") {
  243. t.Fatalf("expected PREFIX in area range print: %s", out)
  244. }
  245. out, _ = e.Execute("/routing ospf area range add prefix=10.10.0.0/16 cost=10 advertise=no", admin)
  246. if !strings.Contains(out, "added") {
  247. t.Fatalf("failed to add area range: %s", out)
  248. }
  249. out, _ = e.Execute("/routing ospf area range set 10.10.0.0/16 cost=20 advertise=yes", admin)
  250. if !strings.Contains(out, "updated") {
  251. t.Fatalf("failed to set area range: %s", out)
  252. }
  253. out, _ = e.Execute("/routing ospf area range remove 10.10.0.0/16", admin)
  254. if !strings.Contains(out, "removed") {
  255. t.Fatalf("failed to remove area range: %s", out)
  256. }
  257. // 4. Interface template commands
  258. out, _ = e.Execute("/routing ospf interface print", admin)
  259. if !strings.Contains(out, "INTERFACE") {
  260. t.Fatalf("expected INTERFACE in interface print: %s", out)
  261. }
  262. out, _ = e.Execute("/routing ospf interface add interface=eth0,eth1 area=Default cost=15 network-type=nonbroadcast auth=md5 auth-key=secret123", admin)
  263. if !strings.Contains(out, "saved") {
  264. t.Fatalf("failed to add ospf interface template: %s", out)
  265. }
  266. out, _ = e.Execute("/routing ospf interface set eth0,eth1 cost=25", admin)
  267. if !strings.Contains(out, "updated") {
  268. t.Fatalf("failed to set ospf interface template: %s", out)
  269. }
  270. out, _ = e.Execute("/routing ospf interface remove eth0,eth1", admin)
  271. if !strings.Contains(out, "removed") {
  272. t.Fatalf("failed to remove ospf interface template: %s", out)
  273. }
  274. // 5. Static neighbor commands
  275. out, _ = e.Execute("/routing ospf static-neighbor print", admin)
  276. if !strings.Contains(out, "ADDRESS") {
  277. t.Fatalf("expected ADDRESS in static-neighbor print: %s", out)
  278. }
  279. out, _ = e.Execute("/routing ospf static-neighbor add address=192.168.100.2 poll-interval=30 cost=5", admin)
  280. if !strings.Contains(out, "saved") {
  281. t.Fatalf("failed to add static neighbor: %s", out)
  282. }
  283. out, _ = e.Execute("/routing ospf static-neighbor set 192.168.100.2 poll-interval=60", admin)
  284. if !strings.Contains(out, "updated") {
  285. t.Fatalf("failed to update static neighbor: %s", out)
  286. }
  287. out, _ = e.Execute("/routing ospf static-neighbor disable 192.168.100.2", admin)
  288. if !strings.Contains(out, "disabled") {
  289. t.Fatalf("failed to disable static neighbor: %s", out)
  290. }
  291. out, _ = e.Execute("/routing ospf static-neighbor enable 192.168.100.2", admin)
  292. if !strings.Contains(out, "enabled") {
  293. t.Fatalf("failed to enable static neighbor: %s", out)
  294. }
  295. out, _ = e.Execute("/routing ospf static-neighbor remove 192.168.100.2", admin)
  296. if !strings.Contains(out, "removed") {
  297. t.Fatalf("failed to remove static neighbor: %s", out)
  298. }
  299. // 6. Live status commands
  300. out, _ = e.Execute("/routing ospf neighbor print", admin)
  301. if !strings.Contains(out, "ROUTER-ID") {
  302. t.Fatalf("expected ROUTER-ID in neighbor print: %s", out)
  303. }
  304. out, _ = e.Execute("/routing ospf lsa print", admin)
  305. if !strings.Contains(out, "TYPE") {
  306. t.Fatalf("expected TYPE in lsa print: %s", out)
  307. }
  308. out, _ = e.Execute("/routing ospf route print", admin)
  309. if !strings.Contains(out, "PREFIX") {
  310. t.Fatalf("expected PREFIX in route print: %s", out)
  311. }
  312. // Cleanup
  313. out, _ = e.Execute("/routing ospf area remove area1", admin)
  314. if !strings.Contains(out, "removed") {
  315. t.Fatalf("failed to remove area1: %s", out)
  316. }
  317. out, _ = e.Execute("/routing ospf instance remove ospf-test", admin)
  318. if !strings.Contains(out, "removed") {
  319. t.Fatalf("failed to remove ospf-test: %s", out)
  320. }
  321. }