xray_bind_conflict_test.go 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. package service
  2. import (
  3. "encoding/json"
  4. "strconv"
  5. "strings"
  6. "testing"
  7. "github.com/mhsanaei/3x-ui/v3/internal/amneziawgnet"
  8. "github.com/mhsanaei/3x-ui/v3/internal/database"
  9. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  10. "github.com/mhsanaei/3x-ui/v3/internal/xray"
  11. )
  12. // configFromInbounds builds the config the way the panel does, from raw JSON:
  13. // the probe is only worth anything if it parses what is really written.
  14. func configFromInbounds(t *testing.T, inbounds string) *xray.Config {
  15. t.Helper()
  16. var cfg xray.Config
  17. if err := json.Unmarshal([]byte(`{"inbounds":[`+inbounds+`]}`), &cfg); err != nil {
  18. t.Fatalf("build config: %v", err)
  19. }
  20. return &cfg
  21. }
  22. func TestBindConflicts(t *testing.T) {
  23. const (
  24. relay = `{"listen":"127.0.0.1","port":65101,"protocol":"socks","tag":"relay","settings":{"auth":"password","udp":true,"accounts":[]}}`
  25. user = `{"listen":"0.0.0.0","port":65101,"protocol":"vless","tag":"user","streamSettings":{"network":"tcp"}}`
  26. )
  27. cases := []struct {
  28. name string
  29. inbounds string
  30. running string
  31. want int
  32. }{
  33. {
  34. "same listen, port and tcp",
  35. `{"listen":"0.0.0.0","port":443,"protocol":"vless","tag":"a"},
  36. {"listen":"0.0.0.0","port":443,"protocol":"vmess","tag":"b"}`,
  37. ``, 1,
  38. },
  39. {
  40. "tcp and udp on one port are legal",
  41. `{"listen":"0.0.0.0","port":443,"protocol":"vless","tag":"a"},
  42. {"listen":"0.0.0.0","port":443,"protocol":"hysteria","tag":"b"}`,
  43. ``, 0,
  44. },
  45. {
  46. "kcp moves vless to udp and frees the port",
  47. `{"listen":"0.0.0.0","port":443,"protocol":"vless","tag":"a","streamSettings":{"network":"kcp"}},
  48. {"listen":"0.0.0.0","port":443,"protocol":"vless","tag":"b","streamSettings":{"network":"tcp"}}`,
  49. ``, 0,
  50. },
  51. {
  52. "ipv4 and ipv6 wildcards bind one dual-stack socket",
  53. `{"listen":"::","port":443,"protocol":"vless","tag":"ipv6"},
  54. {"listen":"0.0.0.0","port":443,"protocol":"vless","tag":"ipv4"}`,
  55. ``, 1,
  56. },
  57. {
  58. "dual-stack ipv6 wildcard takes an ipv4 address",
  59. `{"listen":"::","port":443,"protocol":"vless","tag":"ipv6"},
  60. {"listen":"10.5.0.200","port":443,"protocol":"vless","tag":"ipv4"}`,
  61. ``, 1,
  62. },
  63. {
  64. "v6only ipv6 wildcard leaves an ipv4 address free",
  65. `{"listen":"::","port":443,"protocol":"vless","tag":"ipv6","streamSettings":{"network":"tcp","sockopt":{"v6only":true}}},
  66. {"listen":"10.5.0.200","port":443,"protocol":"vless","tag":"ipv4"}`,
  67. ``, 0,
  68. },
  69. {
  70. "v6only ipv6 wildcard still collides with the dual-stack 0.0.0.0",
  71. `{"listen":"::","port":443,"protocol":"vless","tag":"ipv6","streamSettings":{"network":"tcp","sockopt":{"v6only":true}}},
  72. {"listen":"0.0.0.0","port":443,"protocol":"vless","tag":"ipv4"}`,
  73. ``, 1,
  74. },
  75. {
  76. "wildcard listen overlaps a loopback one",
  77. `{"listen":"0.0.0.0","port":8443,"protocol":"vless","tag":"a"},
  78. {"listen":"127.0.0.1","port":8443,"protocol":"trojan","tag":"b"}`,
  79. ``, 1,
  80. },
  81. {
  82. "absent listen means wildcard",
  83. `{"port":8443,"protocol":"vless","tag":"a"},
  84. {"listen":"127.0.0.1","port":8443,"protocol":"trojan","tag":"b"}`,
  85. ``, 1,
  86. },
  87. {
  88. "distinct loopback addresses do not overlap",
  89. `{"listen":"127.0.0.1","port":8443,"protocol":"vless","tag":"a"},
  90. {"listen":"127.0.0.2","port":8443,"protocol":"vless","tag":"b"}`,
  91. ``, 0,
  92. },
  93. {
  94. "port zero is not a bind",
  95. `{"listen":"0.0.0.0","port":0,"protocol":"tunnel","tag":"a"},
  96. {"listen":"0.0.0.0","port":0,"protocol":"vless","tag":"b"}`,
  97. ``, 0,
  98. },
  99. {
  100. "clean config",
  101. `{"listen":"0.0.0.0","port":443,"protocol":"vless","tag":"a"},
  102. {"listen":"127.0.0.1","port":62789,"protocol":"tunnel","tag":"api"},
  103. ` + relay,
  104. ``, 0,
  105. },
  106. {
  107. // The relay the AmneziaWG family lives on: loopback tcp+udp, on the
  108. // same port as a user inbound's tcp.
  109. "amneziawg relay against a tcp inbound",
  110. relay + "," + user, ``, 1,
  111. },
  112. {
  113. "amneziawg relay against a udp inbound",
  114. relay + `,{"listen":"0.0.0.0","port":65101,"protocol":"hysteria","tag":"user"}`,
  115. ``, 1,
  116. },
  117. {
  118. // Proves the relay's transports come from settings.udp and not from a
  119. // blanket "loopback owns everything" rule.
  120. "socks bridge without udp coexists with a udp inbound",
  121. `{"listen":"127.0.0.1","port":65101,"protocol":"socks","tag":"bridge","settings":{"auth":"noauth"}},
  122. {"listen":"0.0.0.0","port":65101,"protocol":"hysteria","tag":"user"}`,
  123. ``, 0,
  124. },
  125. {
  126. "reserved api inbound against a user inbound",
  127. `{"listen":"127.0.0.1","port":62789,"protocol":"tunnel","tag":"api","settings":{"rewriteAddress":"127.0.0.1"}},
  128. {"listen":"0.0.0.0","port":62789,"protocol":"vless","tag":"user"}`,
  129. ``, 1,
  130. },
  131. {
  132. // The core is running this pair right now, so whatever a static read
  133. // says about it, it binds: an established setup is never refused.
  134. "collision the running config already serves",
  135. relay + "," + user, relay + "," + user, 0,
  136. },
  137. {
  138. "collision the running config does not have",
  139. relay + "," + user, user, 1,
  140. },
  141. {
  142. // `::` and `0.0.0.0` on one port is what bindv6only=1 makes legal, so the
  143. // running config excuses it -- but moving one onto the other is not it.
  144. "excused pair whose listen changed into a real collision",
  145. `{"listen":"0.0.0.0","port":443,"protocol":"vless","tag":"a"},
  146. {"listen":"0.0.0.0","port":443,"protocol":"vless","tag":"b"}`,
  147. `{"listen":"::","port":443,"protocol":"vless","tag":"a"},
  148. {"listen":"0.0.0.0","port":443,"protocol":"vless","tag":"b"}`,
  149. 1,
  150. },
  151. {
  152. // The same pair of sockets is the same evidence, whichever order the
  153. // generator happened to emit them in.
  154. "excused pair with its listens swapped stays excused",
  155. `{"listen":"0.0.0.0","port":443,"protocol":"vless","tag":"a"},
  156. {"listen":"::","port":443,"protocol":"vless","tag":"b"}`,
  157. `{"listen":"::","port":443,"protocol":"vless","tag":"a"},
  158. {"listen":"0.0.0.0","port":443,"protocol":"vless","tag":"b"}`,
  159. 0,
  160. },
  161. {
  162. "same pair on another port is still new",
  163. `{"listen":"127.0.0.1","port":65102,"protocol":"socks","tag":"relay","settings":{"auth":"password","udp":true,"accounts":[]}},
  164. {"listen":"0.0.0.0","port":65102,"protocol":"vless","tag":"user","streamSettings":{"network":"tcp"}}`,
  165. relay + "," + user, 1,
  166. },
  167. }
  168. for _, tc := range cases {
  169. t.Run(tc.name, func(t *testing.T) {
  170. var running *xray.Config
  171. if tc.running != "" {
  172. running = configFromInbounds(t, tc.running)
  173. }
  174. got := bindConflicts(configFromInbounds(t, tc.inbounds), running)
  175. if len(got) != tc.want {
  176. t.Fatalf("bindConflicts = %v, want %d conflict(s)", got, tc.want)
  177. }
  178. for _, c := range got {
  179. if c.tagA == "" || c.tagB == "" || c.tagA == c.tagB {
  180. t.Fatalf("a conflict must name both tags, got %+v", c)
  181. }
  182. }
  183. })
  184. }
  185. }
  186. func TestBindConflicts_MessageNamesBothSides(t *testing.T) {
  187. conflicts := bindConflicts(configFromInbounds(t, `
  188. {"listen":"127.0.0.1","port":65101,"protocol":"socks","tag":"relay","settings":{"auth":"password","udp":true,"accounts":[]}},
  189. {"listen":"0.0.0.0","port":65101,"protocol":"vless","tag":"user","streamSettings":{"network":"tcp"}}`), nil)
  190. if len(conflicts) != 1 {
  191. t.Fatalf("want exactly one conflict, got %v", conflicts)
  192. }
  193. msg := conflicts[0].String()
  194. for _, want := range []string{`"relay"`, `"user"`, "127.0.0.1", "65101"} {
  195. if !strings.Contains(msg, want) {
  196. t.Fatalf("conflict message %q must contain %q", msg, want)
  197. }
  198. }
  199. }
  200. // The probe must read what the panel really emits: the AmneziaWG relay is a
  201. // loopback "socks" inbound whose udp flag lives in settings, not streamSettings.
  202. func TestBindConflicts_GeneratedConfig(t *testing.T) {
  203. cases := []struct {
  204. name string
  205. portOff int
  206. running bool
  207. want int
  208. }{
  209. {"user inbound on the relay port", 0, false, 1},
  210. {"already running config", 0, true, 0},
  211. {"user inbound on a free port", 1, false, 0},
  212. }
  213. for _, tc := range cases {
  214. t.Run(tc.name, func(t *testing.T) {
  215. setupSettingTestDB(t)
  216. seedInboundConflict(t, "awg-1", "0.0.0.0", 51820, model.AmneziaWG, ``, amneziawgRoutedSettings)
  217. var awg model.Inbound
  218. if err := database.GetDB().Where("tag = ?", "awg-1").First(&awg).Error; err != nil {
  219. t.Fatalf("read seeded row: %v", err)
  220. }
  221. relayPort := amneziawgnet.SOCKSPortForInbound(awg.Id)
  222. seedInboundConflict(t, "user", "0.0.0.0", relayPort+tc.portOff, model.VLESS, `{"network":"tcp"}`, `{}`)
  223. svc := &XrayService{}
  224. cfg, err := svc.GetXrayConfig()
  225. if err != nil {
  226. t.Fatalf("GetXrayConfig: %v", err)
  227. }
  228. running := (*xray.Config)(nil)
  229. if tc.running {
  230. if running, err = svc.GetXrayConfig(); err != nil {
  231. t.Fatalf("second GetXrayConfig: %v", err)
  232. }
  233. }
  234. got := bindConflicts(cfg, running)
  235. if len(got) != tc.want {
  236. t.Fatalf("bindConflicts = %v, want %d", got, tc.want)
  237. }
  238. if tc.want == 0 {
  239. return
  240. }
  241. msg := got[0].String()
  242. for _, want := range []string{`"awg-1"`, `"user"`, strconv.Itoa(relayPort)} {
  243. if !strings.Contains(msg, want) {
  244. t.Fatalf("conflict message %q must contain %q", msg, want)
  245. }
  246. }
  247. })
  248. }
  249. }