xray_config_inject_test.go 40 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015
  1. package service
  2. import (
  3. "encoding/json"
  4. "os"
  5. "strings"
  6. "testing"
  7. "github.com/mhsanaei/3x-ui/v3/internal/amneziawg"
  8. "github.com/mhsanaei/3x-ui/v3/internal/amneziawgnet"
  9. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  10. xuilogger "github.com/mhsanaei/3x-ui/v3/internal/logger"
  11. "github.com/mhsanaei/3x-ui/v3/internal/util/json_util"
  12. "github.com/mhsanaei/3x-ui/v3/internal/xray"
  13. "github.com/op/go-logging"
  14. )
  15. func TestMain(m *testing.M) {
  16. // A test binary re-executed with MTG_FAKE_CHILD=1 poses as an mtg child
  17. // process (see mtproto_fake_test.go) and never reaches the test runner.
  18. if os.Getenv("MTG_FAKE_CHILD") == "1" {
  19. fakeMtgChildMain()
  20. }
  21. // injectPanelEgress logs when it skips injection; the package logger must
  22. // exist before any test exercises a skipped path.
  23. xuilogger.InitLogger(logging.ERROR)
  24. os.Exit(m.Run())
  25. }
  26. func TestEnsureAPIServices(t *testing.T) {
  27. // legacy template without RoutingService gets it injected
  28. out := ensureAPIServices(json_util.RawMessage(`{"services":["HandlerService","LoggerService","StatsService"],"tag":"api"}`))
  29. var parsed struct {
  30. Services []string `json:"services"`
  31. Tag string `json:"tag"`
  32. }
  33. if err := json.Unmarshal(out, &parsed); err != nil {
  34. t.Fatal(err)
  35. }
  36. want := map[string]bool{"HandlerService": true, "StatsService": true, "RoutingService": true, "LoggerService": true}
  37. if len(parsed.Services) != 4 {
  38. t.Fatalf("expected 4 services, got %v", parsed.Services)
  39. }
  40. for _, svc := range parsed.Services {
  41. if !want[svc] {
  42. t.Fatalf("unexpected service %q", svc)
  43. }
  44. }
  45. if parsed.Tag != "api" {
  46. t.Fatalf("tag must be preserved, got %q", parsed.Tag)
  47. }
  48. // complete api block is returned unchanged (no marshal churn)
  49. full := json_util.RawMessage(`{"services":["HandlerService","StatsService","RoutingService"],"tag":"api"}`)
  50. if got := ensureAPIServices(full); string(got) != string(full) {
  51. t.Fatalf("complete api block must pass through untouched, got %s", got)
  52. }
  53. // absent api block stays absent
  54. if got := ensureAPIServices(nil); got != nil {
  55. t.Fatalf("nil api block must stay nil, got %s", got)
  56. }
  57. }
  58. func TestEnsureStatsPolicy(t *testing.T) {
  59. // default-template shape: level "0" exists with traffic flags — the online
  60. // flag is added and the siblings survive untouched
  61. out := ensureStatsPolicy(json_util.RawMessage(`{"levels":{"0":{"handshake":4,"statsUserUplink":true,"statsUserDownlink":true}},"system":{"statsInboundDownlink":true}}`))
  62. var parsed struct {
  63. Levels map[string]map[string]any `json:"levels"`
  64. System map[string]any `json:"system"`
  65. }
  66. if err := json.Unmarshal(out, &parsed); err != nil {
  67. t.Fatal(err)
  68. }
  69. level0 := parsed.Levels["0"]
  70. if level0["statsUserOnline"] != true {
  71. t.Fatalf("statsUserOnline must be injected into level 0, got %v", level0)
  72. }
  73. if level0["statsUserUplink"] != true || level0["statsUserDownlink"] != true || level0["handshake"] != float64(4) {
  74. t.Fatalf("sibling keys must be preserved, got %v", level0)
  75. }
  76. if parsed.System["statsInboundDownlink"] != true {
  77. t.Fatalf("system block must be preserved, got %v", parsed.System)
  78. }
  79. // missing levels block: level "0" is created with the flag
  80. out = ensureStatsPolicy(json_util.RawMessage(`{"system":{}}`))
  81. if err := json.Unmarshal(out, &parsed); err != nil {
  82. t.Fatal(err)
  83. }
  84. if parsed.Levels["0"]["statsUserOnline"] != true {
  85. t.Fatalf("level 0 must be created with statsUserOnline, got %s", out)
  86. }
  87. // every level gets the flag, an explicit false included — the flag is
  88. // panel infrastructure, like the api services
  89. out = ensureStatsPolicy(json_util.RawMessage(`{"levels":{"0":{"statsUserOnline":false},"1":{"connIdle":300}}}`))
  90. if err := json.Unmarshal(out, &parsed); err != nil {
  91. t.Fatal(err)
  92. }
  93. for _, key := range []string{"0", "1"} {
  94. if parsed.Levels[key]["statsUserOnline"] != true {
  95. t.Fatalf("level %s must have statsUserOnline forced on, got %s", key, out)
  96. }
  97. }
  98. if parsed.Levels["1"]["connIdle"] != float64(300) {
  99. t.Fatalf("level 1 siblings must be preserved, got %s", out)
  100. }
  101. // already-enabled input passes through byte-identical (no marshal churn,
  102. // no spurious restart)
  103. full := json_util.RawMessage(`{"levels":{"0":{"statsUserOnline":true}}}`)
  104. if got := ensureStatsPolicy(full); string(got) != string(full) {
  105. t.Fatalf("already-enabled policy must pass through untouched, got %s", got)
  106. }
  107. // absent policy block stays absent
  108. if got := ensureStatsPolicy(nil); got != nil {
  109. t.Fatalf("nil policy must stay nil, got %s", got)
  110. }
  111. // unparsable policy is left untouched
  112. bad := json_util.RawMessage(`{not json`)
  113. if got := ensureStatsPolicy(bad); string(got) != string(bad) {
  114. t.Fatalf("unparsable policy must be left untouched, got %s", got)
  115. }
  116. }
  117. func egressTestConfig() *xray.Config {
  118. return &xray.Config{
  119. RouterConfig: json_util.RawMessage(`{"domainStrategy":"AsIs","rules":[{"type":"field","inboundTag":["api"],"outboundTag":"api"}]}`),
  120. OutboundConfigs: json_util.RawMessage(`[{"protocol":"freedom","tag":"direct"},{"protocol":"socks","tag":"warp"}]`),
  121. InboundConfigs: []xray.InboundConfig{
  122. {Port: 62789, Protocol: "tunnel", Tag: "api", Listen: json_util.RawMessage(`"127.0.0.1"`)},
  123. },
  124. }
  125. }
  126. type egressRouting struct {
  127. DomainStrategy string `json:"domainStrategy"`
  128. Rules []struct {
  129. InboundTag []string `json:"inboundTag"`
  130. OutboundTag string `json:"outboundTag"`
  131. Type string `json:"type"`
  132. } `json:"rules"`
  133. }
  134. func TestInjectPanelEgress(t *testing.T) {
  135. cfg := egressTestConfig()
  136. injectPanelEgress(cfg, "warp")
  137. if len(cfg.InboundConfigs) != 2 {
  138. t.Fatalf("expected the egress inbound to be appended, got %d inbounds", len(cfg.InboundConfigs))
  139. }
  140. ib := cfg.InboundConfigs[1]
  141. if ib.Tag != PanelEgressInboundTag || ib.Protocol != "socks" || ib.Port != panelEgressBasePort {
  142. t.Fatalf("unexpected egress inbound: %+v", ib)
  143. }
  144. if string(ib.Listen) != `"127.0.0.1"` {
  145. t.Fatalf("egress inbound must listen on loopback, got %s", ib.Listen)
  146. }
  147. var routing egressRouting
  148. if err := json.Unmarshal(cfg.RouterConfig, &routing); err != nil {
  149. t.Fatal(err)
  150. }
  151. if routing.DomainStrategy != "AsIs" {
  152. t.Fatalf("routing keys outside rules must be preserved, got %+v", routing)
  153. }
  154. if len(routing.Rules) != 2 {
  155. t.Fatalf("expected egress rule + existing rule, got %+v", routing.Rules)
  156. }
  157. first := routing.Rules[0]
  158. if first.Type != "field" || first.OutboundTag != "warp" ||
  159. len(first.InboundTag) != 1 || first.InboundTag[0] != PanelEgressInboundTag {
  160. t.Fatalf("egress rule must be prepended, got %+v", first)
  161. }
  162. }
  163. func TestInjectPanelEgress_BalancerTag(t *testing.T) {
  164. cfg := egressTestConfig()
  165. cfg.RouterConfig = json_util.RawMessage(`{"domainStrategy":"AsIs","rules":[],"balancers":[{"tag":"lb","selector":["warp"]}]}`)
  166. // A tag that names a balancer must be targeted via balancerTag so the
  167. // router resolves it; an outbound tag coexisting with balancers still uses
  168. // outboundTag.
  169. injectPanelEgress(cfg, "lb")
  170. var routing struct {
  171. Rules []struct {
  172. InboundTag []string `json:"inboundTag"`
  173. OutboundTag string `json:"outboundTag"`
  174. BalancerTag string `json:"balancerTag"`
  175. Type string `json:"type"`
  176. } `json:"rules"`
  177. }
  178. if err := json.Unmarshal(cfg.RouterConfig, &routing); err != nil {
  179. t.Fatal(err)
  180. }
  181. if len(routing.Rules) != 1 {
  182. t.Fatalf("expected the egress rule, got %+v", routing.Rules)
  183. }
  184. first := routing.Rules[0]
  185. if first.BalancerTag != "lb" || first.OutboundTag != "" {
  186. t.Fatalf("a balancer tag must target balancerTag, not outboundTag, got %+v", first)
  187. }
  188. if len(first.InboundTag) != 1 || first.InboundTag[0] != PanelEgressInboundTag {
  189. t.Fatalf("egress rule must bind the egress inbound, got %+v", first)
  190. }
  191. // A non-balancer tag alongside balancers keeps the plain outbound path.
  192. cfg2 := egressTestConfig()
  193. cfg2.RouterConfig = json_util.RawMessage(`{"rules":[],"balancers":[{"tag":"lb","selector":["warp"]}]}`)
  194. injectPanelEgress(cfg2, "warp")
  195. var routing2 struct {
  196. Rules []struct {
  197. OutboundTag string `json:"outboundTag"`
  198. BalancerTag string `json:"balancerTag"`
  199. } `json:"rules"`
  200. }
  201. if err := json.Unmarshal(cfg2.RouterConfig, &routing2); err != nil {
  202. t.Fatal(err)
  203. }
  204. if routing2.Rules[0].OutboundTag != "warp" || routing2.Rules[0].BalancerTag != "" {
  205. t.Fatalf("a concrete outbound must target outboundTag, got %+v", routing2.Rules[0])
  206. }
  207. }
  208. func TestInjectPanelEgress_PortCollision(t *testing.T) {
  209. cfg := egressTestConfig()
  210. cfg.InboundConfigs = append(cfg.InboundConfigs,
  211. xray.InboundConfig{Port: panelEgressBasePort, Protocol: "vless", Tag: "in-1"},
  212. xray.InboundConfig{Port: panelEgressBasePort + 1, Protocol: "vless", Tag: "in-2"},
  213. )
  214. injectPanelEgress(cfg, "direct")
  215. got := cfg.InboundConfigs[len(cfg.InboundConfigs)-1]
  216. if got.Tag != PanelEgressInboundTag || got.Port != panelEgressBasePort+2 {
  217. t.Fatalf("egress inbound must skip taken ports, got %+v", got)
  218. }
  219. }
  220. func TestInjectPanelEgress_TagCollisionSkips(t *testing.T) {
  221. cfg := egressTestConfig()
  222. cfg.InboundConfigs = append(cfg.InboundConfigs,
  223. xray.InboundConfig{Port: 1234, Protocol: "socks", Tag: PanelEgressInboundTag},
  224. )
  225. before := string(cfg.RouterConfig)
  226. injectPanelEgress(cfg, "direct")
  227. if len(cfg.InboundConfigs) != 2 || string(cfg.RouterConfig) != before {
  228. t.Fatal("a user inbound owning the egress tag must make injection a no-op")
  229. }
  230. }
  231. func TestInjectPanelEgress_NoRoutingSection(t *testing.T) {
  232. cfg := egressTestConfig()
  233. cfg.RouterConfig = nil
  234. injectPanelEgress(cfg, "direct")
  235. var routing egressRouting
  236. if err := json.Unmarshal(cfg.RouterConfig, &routing); err != nil {
  237. t.Fatal(err)
  238. }
  239. if len(routing.Rules) != 1 || routing.Rules[0].OutboundTag != "direct" {
  240. t.Fatalf("a routing section must be created with the egress rule, got %+v", routing)
  241. }
  242. if len(cfg.InboundConfigs) != 2 {
  243. t.Fatal("egress inbound must still be appended")
  244. }
  245. }
  246. func TestInjectPanelEgress_BadRoutingSkips(t *testing.T) {
  247. cfg := egressTestConfig()
  248. cfg.RouterConfig = json_util.RawMessage(`{not json`)
  249. injectPanelEgress(cfg, "direct")
  250. if len(cfg.InboundConfigs) != 1 {
  251. t.Fatal("unparsable routing must skip the whole injection, inbound included")
  252. }
  253. if string(cfg.RouterConfig) != `{not json` {
  254. t.Fatal("unparsable routing must be left untouched")
  255. }
  256. }
  257. func TestInjectPanelEgress_MissingTargetSkips(t *testing.T) {
  258. cfg := egressTestConfig()
  259. before := string(cfg.RouterConfig)
  260. injectPanelEgress(cfg, "removed-subscription-outbound")
  261. if len(cfg.InboundConfigs) != 1 {
  262. t.Fatalf("a missing target must not expose the panel bridge, got %+v", cfg.InboundConfigs)
  263. }
  264. if string(cfg.RouterConfig) != before {
  265. t.Fatalf("a missing target must leave routing untouched, got %s", cfg.RouterConfig)
  266. }
  267. }
  268. func TestInjectPanelEgress_BadOutboundsSkips(t *testing.T) {
  269. cfg := egressTestConfig()
  270. cfg.OutboundConfigs = json_util.RawMessage(`{not json`)
  271. before := string(cfg.RouterConfig)
  272. injectPanelEgress(cfg, "direct")
  273. if len(cfg.InboundConfigs) != 1 {
  274. t.Fatalf("unparsable outbounds must not expose the panel bridge, got %+v", cfg.InboundConfigs)
  275. }
  276. if string(cfg.RouterConfig) != before {
  277. t.Fatalf("unparsable outbounds must leave routing untouched, got %s", cfg.RouterConfig)
  278. }
  279. }
  280. func TestInjectNodeEgresses_MissingTargetSkips(t *testing.T) {
  281. cfg := egressTestConfig()
  282. injectNodeEgresses(cfg, []*model.Node{
  283. {Id: 1, Enable: true, OutboundTag: "removed-subscription-outbound"},
  284. {Id: 2, Enable: true, OutboundTag: "warp"},
  285. })
  286. if len(cfg.InboundConfigs) != 2 {
  287. t.Fatalf("only the node with a valid target should get a bridge, got %+v", cfg.InboundConfigs)
  288. }
  289. bridge := cfg.InboundConfigs[1]
  290. if bridge.Tag != NodeEgressInboundTag(2) || bridge.Port != nodeEgressBasePort+2 {
  291. t.Fatalf("unexpected node egress bridge: %+v", bridge)
  292. }
  293. var routing egressRouting
  294. if err := json.Unmarshal(cfg.RouterConfig, &routing); err != nil {
  295. t.Fatal(err)
  296. }
  297. if len(routing.Rules) != 2 || routing.Rules[0].OutboundTag != "warp" ||
  298. len(routing.Rules[0].InboundTag) != 1 || routing.Rules[0].InboundTag[0] != NodeEgressInboundTag(2) {
  299. t.Fatalf("only the valid node egress rule should be prepended, got %+v", routing.Rules)
  300. }
  301. }
  302. func TestInjectNodeEgresses_BadOutboundsSkips(t *testing.T) {
  303. cfg := egressTestConfig()
  304. cfg.OutboundConfigs = json_util.RawMessage(`{not json`)
  305. before := string(cfg.RouterConfig)
  306. injectNodeEgresses(cfg, []*model.Node{{Id: 1, Enable: true, OutboundTag: "direct"}})
  307. if len(cfg.InboundConfigs) != 1 {
  308. t.Fatalf("unparsable outbounds must not expose a node bridge, got %+v", cfg.InboundConfigs)
  309. }
  310. if string(cfg.RouterConfig) != before {
  311. t.Fatalf("unparsable outbounds must leave routing untouched, got %s", cfg.RouterConfig)
  312. }
  313. }
  314. func TestInjectNodeEgresses_BalancerTarget(t *testing.T) {
  315. cfg := egressTestConfig()
  316. cfg.RouterConfig = json_util.RawMessage(`{"rules":[],"balancers":[{"tag":"lb","selector":["warp"]}]}`)
  317. injectNodeEgresses(cfg, []*model.Node{{Id: 1, Enable: true, OutboundTag: "lb"}})
  318. var routing struct {
  319. Rules []struct {
  320. OutboundTag string `json:"outboundTag"`
  321. BalancerTag string `json:"balancerTag"`
  322. } `json:"rules"`
  323. }
  324. if err := json.Unmarshal(cfg.RouterConfig, &routing); err != nil {
  325. t.Fatal(err)
  326. }
  327. if len(cfg.InboundConfigs) != 2 || len(routing.Rules) != 1 ||
  328. routing.Rules[0].BalancerTag != "lb" || routing.Rules[0].OutboundTag != "" {
  329. t.Fatalf("a valid balancer target must create the node bridge and rule, got %+v", routing.Rules)
  330. }
  331. }
  332. func TestInjectNodeEgresses_TagCollisionSkips(t *testing.T) {
  333. cfg := egressTestConfig()
  334. cfg.InboundConfigs = append(cfg.InboundConfigs,
  335. xray.InboundConfig{Port: 1234, Protocol: "socks", Tag: NodeEgressInboundTag(1)},
  336. )
  337. before := string(cfg.RouterConfig)
  338. injectNodeEgresses(cfg, []*model.Node{{Id: 1, Enable: true, OutboundTag: "direct"}})
  339. if len(cfg.InboundConfigs) != 2 || string(cfg.RouterConfig) != before {
  340. t.Fatal("an existing node egress tag must make that node injection a no-op")
  341. }
  342. }
  343. func TestInjectNodeEgresses_PortCollision(t *testing.T) {
  344. cfg := egressTestConfig()
  345. cfg.InboundConfigs = append(cfg.InboundConfigs,
  346. xray.InboundConfig{Port: nodeEgressBasePort + 1, Protocol: "vless", Tag: "in-1"},
  347. xray.InboundConfig{Port: nodeEgressBasePort + 2, Protocol: "vless", Tag: "in-2"},
  348. )
  349. injectNodeEgresses(cfg, []*model.Node{{Id: 1, Enable: true, OutboundTag: "direct"}})
  350. bridge := cfg.InboundConfigs[len(cfg.InboundConfigs)-1]
  351. if bridge.Tag != NodeEgressInboundTag(1) || bridge.Port != nodeEgressBasePort+3 {
  352. t.Fatalf("node egress must skip taken ports, got %+v", bridge)
  353. }
  354. }
  355. func TestInjectNodeEgresses_NoRoutingSection(t *testing.T) {
  356. cfg := egressTestConfig()
  357. cfg.RouterConfig = nil
  358. injectNodeEgresses(cfg, []*model.Node{{Id: 1, Enable: true, OutboundTag: "direct"}})
  359. var routing egressRouting
  360. if err := json.Unmarshal(cfg.RouterConfig, &routing); err != nil {
  361. t.Fatal(err)
  362. }
  363. if len(cfg.InboundConfigs) != 2 || len(routing.Rules) != 1 ||
  364. routing.Rules[0].OutboundTag != "direct" ||
  365. len(routing.Rules[0].InboundTag) != 1 || routing.Rules[0].InboundTag[0] != NodeEgressInboundTag(1) {
  366. t.Fatalf("a routing section must be created with the node egress rule, got %+v", routing.Rules)
  367. }
  368. }
  369. func TestInjectNodeEgresses_BadRoutingSkips(t *testing.T) {
  370. cfg := egressTestConfig()
  371. cfg.RouterConfig = json_util.RawMessage(`{not json`)
  372. injectNodeEgresses(cfg, []*model.Node{{Id: 1, Enable: true, OutboundTag: "direct"}})
  373. if len(cfg.InboundConfigs) != 1 {
  374. t.Fatalf("unparsable routing must not expose a node bridge, got %+v", cfg.InboundConfigs)
  375. }
  376. if string(cfg.RouterConfig) != `{not json` {
  377. t.Fatalf("unparsable routing must be left untouched, got %s", cfg.RouterConfig)
  378. }
  379. }
  380. func mtprotoInbound(tag string, settings string) *model.Inbound {
  381. return &model.Inbound{Tag: tag, Protocol: model.MTProto, Enable: true, Settings: settings}
  382. }
  383. func TestInjectMtprotoEgress_WithOutbound(t *testing.T) {
  384. cfg := egressTestConfig()
  385. injectMtprotoEgress(cfg, mtprotoInbound("inbound-443",
  386. `{"routeThroughXray":true,"routeXrayPort":50000,"outboundTag":"warp"}`))
  387. if len(cfg.InboundConfigs) != 2 {
  388. t.Fatalf("expected the bridge inbound to be appended, got %d", len(cfg.InboundConfigs))
  389. }
  390. ib := cfg.InboundConfigs[1]
  391. if ib.Tag != "inbound-443" || ib.Protocol != "socks" || ib.Port != 50000 {
  392. t.Fatalf("unexpected bridge inbound: %+v", ib)
  393. }
  394. if string(ib.Listen) != `"127.0.0.1"` {
  395. t.Fatalf("bridge must listen on loopback, got %s", ib.Listen)
  396. }
  397. var routing egressRouting
  398. if err := json.Unmarshal(cfg.RouterConfig, &routing); err != nil {
  399. t.Fatal(err)
  400. }
  401. if len(routing.Rules) != 2 {
  402. t.Fatalf("expected the egress rule prepended to the existing rule, got %+v", routing.Rules)
  403. }
  404. first := routing.Rules[0]
  405. if first.Type != "field" || first.OutboundTag != "warp" ||
  406. len(first.InboundTag) != 1 || first.InboundTag[0] != "inbound-443" {
  407. t.Fatalf("egress rule must bind the inbound tag to the outbound, got %+v", first)
  408. }
  409. }
  410. func TestInjectMtprotoEgress_NoOutboundLeavesRouting(t *testing.T) {
  411. cfg := egressTestConfig()
  412. before := string(cfg.RouterConfig)
  413. injectMtprotoEgress(cfg, mtprotoInbound("inbound-443",
  414. `{"routeThroughXray":true,"routeXrayPort":50001}`))
  415. if len(cfg.InboundConfigs) != 2 || cfg.InboundConfigs[1].Port != 50001 {
  416. t.Fatalf("bridge must still be appended without an outbound, got %+v", cfg.InboundConfigs)
  417. }
  418. if string(cfg.RouterConfig) != before {
  419. t.Fatalf("no outbound means no rule change, got %s", cfg.RouterConfig)
  420. }
  421. }
  422. func TestInjectMtprotoEgress_BalancerTag(t *testing.T) {
  423. cfg := egressTestConfig()
  424. cfg.RouterConfig = json_util.RawMessage(`{"rules":[],"balancers":[{"tag":"lb","selector":["warp"]}]}`)
  425. injectMtprotoEgress(cfg, mtprotoInbound("inbound-443",
  426. `{"routeThroughXray":true,"routeXrayPort":50002,"outboundTag":"lb"}`))
  427. var routing struct {
  428. Rules []struct {
  429. OutboundTag string `json:"outboundTag"`
  430. BalancerTag string `json:"balancerTag"`
  431. } `json:"rules"`
  432. }
  433. if err := json.Unmarshal(cfg.RouterConfig, &routing); err != nil {
  434. t.Fatal(err)
  435. }
  436. if len(routing.Rules) != 1 || routing.Rules[0].BalancerTag != "lb" || routing.Rules[0].OutboundTag != "" {
  437. t.Fatalf("a balancer tag must target balancerTag, got %+v", routing.Rules)
  438. }
  439. }
  440. func TestInjectMtprotoEgress_Disabled(t *testing.T) {
  441. // Not routed, and routed-but-portless, are both no-ops.
  442. for _, settings := range []string{
  443. `{"routeThroughXray":false,"routeXrayPort":50000}`,
  444. `{"routeThroughXray":true}`,
  445. `{"routeThroughXray":true,"routeXrayPort":0}`,
  446. } {
  447. cfg := egressTestConfig()
  448. before := string(cfg.RouterConfig)
  449. injectMtprotoEgress(cfg, mtprotoInbound("inbound-443", settings))
  450. if len(cfg.InboundConfigs) != 1 || string(cfg.RouterConfig) != before {
  451. t.Fatalf("settings %s must be a no-op, got %d inbounds", settings, len(cfg.InboundConfigs))
  452. }
  453. }
  454. }
  455. func TestInjectMtprotoEgress_TagCollisionSkips(t *testing.T) {
  456. cfg := egressTestConfig()
  457. cfg.InboundConfigs = append(cfg.InboundConfigs,
  458. xray.InboundConfig{Port: 443, Protocol: "vless", Tag: "inbound-443"})
  459. before := string(cfg.RouterConfig)
  460. injectMtprotoEgress(cfg, mtprotoInbound("inbound-443",
  461. `{"routeThroughXray":true,"routeXrayPort":50003,"outboundTag":"warp"}`))
  462. if len(cfg.InboundConfigs) != 2 || string(cfg.RouterConfig) != before {
  463. t.Fatal("a real inbound already owning the tag must make the bridge a no-op")
  464. }
  465. }
  466. func TestInjectMtprotoEgress_MissingTargetSkips(t *testing.T) {
  467. cfg := egressTestConfig()
  468. before := string(cfg.RouterConfig)
  469. injectMtprotoEgress(cfg, mtprotoInbound("inbound-443",
  470. `{"routeThroughXray":true,"routeXrayPort":50004,"outboundTag":"removed-subscription-outbound"}`))
  471. if len(cfg.InboundConfigs) != 1 {
  472. t.Fatalf("a missing target must not expose the mtproto bridge, got %+v", cfg.InboundConfigs)
  473. }
  474. if string(cfg.RouterConfig) != before {
  475. t.Fatalf("a missing target must leave routing untouched, got %s", cfg.RouterConfig)
  476. }
  477. }
  478. func TestInjectMtprotoEgress_BadOutboundsSkips(t *testing.T) {
  479. cfg := egressTestConfig()
  480. cfg.OutboundConfigs = json_util.RawMessage(`{not json`)
  481. before := string(cfg.RouterConfig)
  482. injectMtprotoEgress(cfg, mtprotoInbound("inbound-443",
  483. `{"routeThroughXray":true,"routeXrayPort":50005,"outboundTag":"direct"}`))
  484. if len(cfg.InboundConfigs) != 1 {
  485. t.Fatalf("unparsable outbounds must not expose the mtproto bridge, got %+v", cfg.InboundConfigs)
  486. }
  487. if string(cfg.RouterConfig) != before {
  488. t.Fatalf("unparsable outbounds must leave routing untouched, got %s", cfg.RouterConfig)
  489. }
  490. }
  491. func TestInjectMtprotoEgress_BadRoutingSkips(t *testing.T) {
  492. cfg := egressTestConfig()
  493. cfg.RouterConfig = json_util.RawMessage(`{not json`)
  494. injectMtprotoEgress(cfg, mtprotoInbound("inbound-443",
  495. `{"routeThroughXray":true,"routeXrayPort":50006,"outboundTag":"direct"}`))
  496. if len(cfg.InboundConfigs) != 1 {
  497. t.Fatalf("unparsable routing must not expose the mtproto bridge, got %+v", cfg.InboundConfigs)
  498. }
  499. if string(cfg.RouterConfig) != `{not json` {
  500. t.Fatalf("unparsable routing must be left untouched, got %s", cfg.RouterConfig)
  501. }
  502. }
  503. func amneziawgInbound(id int, tag string, clients []model.Client) *model.Inbound {
  504. server := amneziawg.ServerSettings{SubnetIP: "10.8.1.0", SubnetCIDR: 24}
  505. settings, _ := json.Marshal(amneziawg.InboundSettings{Server: &server, Clients: clients})
  506. return &model.Inbound{Id: id, Tag: tag, Protocol: model.AmneziaWG, Enable: true, Settings: string(settings)}
  507. }
  508. func TestInjectAmneziawgnetSocks_CreatesRelayTaggedWithInboundsOwnTag(t *testing.T) {
  509. cfg := egressTestConfig()
  510. before := string(cfg.RouterConfig)
  511. inbound := amneziawgInbound(7, "awg-7", []model.Client{
  512. {Email: "a@x", Enable: true, PublicKey: "pub-a", AllowedIPs: []string{"10.8.1.2/32"}},
  513. })
  514. injectAmneziawgnetSocks(cfg, []*model.Inbound{inbound})
  515. if len(cfg.InboundConfigs) != 2 {
  516. t.Fatalf("expected the relay inbound to be appended, got %d inbounds", len(cfg.InboundConfigs))
  517. }
  518. ib := cfg.InboundConfigs[1]
  519. if ib.Tag != "awg-7" || ib.Protocol != "socks" || ib.Port != amneziawgnet.SOCKSPortForInbound(7) {
  520. t.Fatalf("relay inbound must reuse the inbound's own tag (so per-inbound stats totals keep matching, and it's already selectable in the stock Routing page) and this instance's own derived port, got %+v", ib)
  521. }
  522. if string(ib.Listen) != `"127.0.0.1"` {
  523. t.Fatalf("relay inbound must listen on loopback, got %s", ib.Listen)
  524. }
  525. if !strings.Contains(string(ib.Settings), `"auth":"password"`) || !strings.Contains(string(ib.Settings), `"udp":true`) {
  526. t.Fatalf("relay inbound must require password auth and allow UDP ASSOCIATE, got %s", ib.Settings)
  527. }
  528. if !strings.Contains(string(ib.Settings), `"a@x"`) {
  529. t.Fatalf("relay inbound must have an account for the peer's email, got %s", ib.Settings)
  530. }
  531. if !strings.Contains(string(ib.Sniffing), `"enabled":true`) {
  532. t.Fatalf("relay inbound must enable sniffing -- a peer's own DNS resolution means the decapsulated traffic never carries a domain at the network layer, so domain-based Routing rules can only ever match via sniffing the payload, got %s", ib.Sniffing)
  533. }
  534. // No auto-generated routing rule: it's entirely up to the admin's own
  535. // Routing-page rules, same as any other protocol's inbound tag.
  536. if string(cfg.RouterConfig) != before {
  537. t.Fatalf("injectAmneziawgnetSocks must never touch the routing section, got %s", cfg.RouterConfig)
  538. }
  539. }
  540. func TestInjectAmneziawgnetSocks_MultipleInboundsEachGetOwnRelay(t *testing.T) {
  541. cfg := egressTestConfig()
  542. inbound1 := amneziawgInbound(1, "awg-1", []model.Client{
  543. {Email: "a@x", Enable: true, PublicKey: "pub-a", AllowedIPs: []string{"10.8.1.2/32"}},
  544. })
  545. inbound2 := amneziawgInbound(2, "awg-2", []model.Client{
  546. {Email: "b@x", Enable: true, PublicKey: "pub-b", AllowedIPs: []string{"10.9.1.2/32"}},
  547. })
  548. injectAmneziawgnetSocks(cfg, []*model.Inbound{inbound1, inbound2})
  549. if len(cfg.InboundConfigs) != 3 {
  550. t.Fatalf("expected one relay inbound per inbound (plus the pre-existing one), got %d inbounds: %+v", len(cfg.InboundConfigs), cfg.InboundConfigs)
  551. }
  552. byTag := map[string]int{}
  553. for _, ib := range cfg.InboundConfigs[1:] {
  554. byTag[ib.Tag] = ib.Port
  555. }
  556. if byTag["awg-1"] != amneziawgnet.SOCKSPortForInbound(1) || byTag["awg-2"] != amneziawgnet.SOCKSPortForInbound(2) {
  557. t.Fatalf("each inbound must get its own tag and its own derived port, got %+v", byTag)
  558. }
  559. }
  560. func TestInjectAmneziawgnetSocks_NoQualifyingPeerSkipsRelay(t *testing.T) {
  561. cases := []struct {
  562. name string
  563. client model.Client
  564. enable bool
  565. }{
  566. {"client disabled", model.Client{Email: "a@x", Enable: false, PublicKey: "pub-a", AllowedIPs: []string{"10.8.1.2/32"}}, true},
  567. {"no PublicKey", model.Client{Email: "a@x", Enable: true, AllowedIPs: []string{"10.8.1.2/32"}}, true},
  568. {"no AllowedIPs", model.Client{Email: "a@x", Enable: true, PublicKey: "pub-a"}, true},
  569. {"inbound disabled", model.Client{Email: "a@x", Enable: true, PublicKey: "pub-a", AllowedIPs: []string{"10.8.1.2/32"}}, false},
  570. {"no Email", model.Client{Enable: true, PublicKey: "pub-a", AllowedIPs: []string{"10.8.1.2/32"}}, true},
  571. }
  572. for _, c := range cases {
  573. t.Run(c.name, func(t *testing.T) {
  574. cfg := egressTestConfig()
  575. inbound := amneziawgInbound(1, "awg-1", []model.Client{c.client})
  576. inbound.Enable = c.enable
  577. injectAmneziawgnetSocks(cfg, []*model.Inbound{inbound})
  578. if len(cfg.InboundConfigs) != 1 {
  579. t.Fatalf("%s must be a no-op, got %d inbounds", c.name, len(cfg.InboundConfigs))
  580. }
  581. })
  582. }
  583. }
  584. func TestInjectAmneziawgnetSocks_AlwaysOnRegardlessOfLegacyRouteThroughXrayField(t *testing.T) {
  585. // Unlike the retired kernel-module bridge, the embedded relay has no
  586. // opt-in gate: there is no alternative datapath once traffic is
  587. // decapsulated in gVisor. A stale RouteThroughXray=false left over from
  588. // a pre-cutover install must not suppress the relay inbound.
  589. cfg := egressTestConfig()
  590. server := amneziawg.ServerSettings{SubnetIP: "10.8.1.0", SubnetCIDR: 24, RouteThroughXray: false}
  591. settings, _ := json.Marshal(amneziawg.InboundSettings{
  592. Server: &server,
  593. Clients: []model.Client{
  594. {Email: "a@x", Enable: true, PublicKey: "pub-a", AllowedIPs: []string{"10.8.1.2/32"}},
  595. },
  596. })
  597. inbound := &model.Inbound{Id: 1, Tag: "awg-1", Protocol: model.AmneziaWG, Enable: true, Settings: string(settings)}
  598. injectAmneziawgnetSocks(cfg, []*model.Inbound{inbound})
  599. if len(cfg.InboundConfigs) != 2 {
  600. t.Fatalf("the relay inbound must always be created regardless of RouteThroughXray, got %+v", cfg.InboundConfigs)
  601. }
  602. }
  603. func TestInjectAmneziawgnetSocks_WrongProtocolOrNodeSkipped(t *testing.T) {
  604. cfg := egressTestConfig()
  605. vless := &model.Inbound{Id: 1, Tag: "in-1", Protocol: model.VLESS, Enable: true}
  606. nodeID := 5
  607. nodeHosted := amneziawgInbound(2, "awg-2", []model.Client{
  608. {Email: "a@x", Enable: true, PublicKey: "pub-a", AllowedIPs: []string{"10.8.1.2/32"}},
  609. })
  610. nodeHosted.NodeID = &nodeID
  611. injectAmneziawgnetSocks(cfg, []*model.Inbound{vless, nodeHosted})
  612. if len(cfg.InboundConfigs) != 1 {
  613. t.Fatalf("a non-AmneziaWG or node-hosted inbound must never get a relay inbound, got %+v", cfg.InboundConfigs)
  614. }
  615. }
  616. func TestInjectAmneziawgnetSocks_TagCollisionSkipsThatInboundOnly(t *testing.T) {
  617. cfg := egressTestConfig()
  618. cfg.InboundConfigs = append(cfg.InboundConfigs,
  619. xray.InboundConfig{Port: 1234, Protocol: "vless", Tag: "awg-1"})
  620. inbound1 := amneziawgInbound(1, "awg-1", []model.Client{
  621. {Email: "a@x", Enable: true, PublicKey: "pub-a", AllowedIPs: []string{"10.8.1.2/32"}},
  622. })
  623. inbound2 := amneziawgInbound(2, "awg-2", []model.Client{
  624. {Email: "b@x", Enable: true, PublicKey: "pub-b", AllowedIPs: []string{"10.9.1.2/32"}},
  625. })
  626. injectAmneziawgnetSocks(cfg, []*model.Inbound{inbound1, inbound2})
  627. // Started with 2 (api + the colliding vless entry); only awg-2's relay
  628. // inbound should have been added, awg-1's skipped since its tag is taken.
  629. if len(cfg.InboundConfigs) != 3 {
  630. t.Fatalf("expected only the non-colliding inbound's relay inbound to be added, got %+v", cfg.InboundConfigs)
  631. }
  632. found := false
  633. for _, ib := range cfg.InboundConfigs {
  634. if ib.Tag == "awg-2" && ib.Protocol == "socks" {
  635. found = true
  636. }
  637. }
  638. if !found {
  639. t.Fatal("awg-2's relay inbound must still be created despite awg-1's tag collision")
  640. }
  641. }
  642. // amneziawgV6Inbound builds an AmneziaWG inbound with IPv6 enabled and a
  643. // given external interface -- amneziawgInbound's own ServerSettings never
  644. // sets these, so injectAmneziawgV6Egress's tests need their own variant.
  645. func amneziawgV6Inbound(id int, tag string, ext6 string, clients []model.Client) *model.Inbound {
  646. server := amneziawg.ServerSettings{
  647. SubnetIP: "10.8.1.0", SubnetCIDR: 24,
  648. IPv6Enabled: true, IPv6ExternalInterface: ext6,
  649. }
  650. settings, _ := json.Marshal(amneziawg.InboundSettings{Server: &server, Clients: clients})
  651. return &model.Inbound{Id: id, Tag: tag, Protocol: model.AmneziaWG, Enable: true, Settings: string(settings)}
  652. }
  653. // amneziawgV6InboundNotActive builds an inbound that fails V6AliasesActive
  654. // (either toggle can do it), unlike amneziawgV6Inbound which always passes it.
  655. func amneziawgV6InboundNotActive(id int, tag string, ipv6Enabled bool, ext6 string, clients []model.Client) *model.Inbound {
  656. server := amneziawg.ServerSettings{
  657. SubnetIP: "10.8.1.0", SubnetCIDR: 24,
  658. IPv6Enabled: ipv6Enabled, IPv6ExternalInterface: ext6,
  659. }
  660. settings, _ := json.Marshal(amneziawg.InboundSettings{Server: &server, Clients: clients})
  661. return &model.Inbound{Id: id, Tag: tag, Protocol: model.AmneziaWG, Enable: true, Settings: string(settings)}
  662. }
  663. // injectAmneziawgV6Egress runs after injectAmneziawgnetSocks in the real
  664. // GetXrayConfig() pipeline and depends on its relay inbound already
  665. // existing (see the "live" tag check) -- every test below calls both, in
  666. // that order, to match production.
  667. func injectAmneziawgSocksThenV6(cfg *xray.Config, inbounds []*model.Inbound) {
  668. injectAmneziawgnetSocks(cfg, inbounds)
  669. injectAmneziawgV6Egress(cfg, inbounds)
  670. }
  671. type v6EgressRouting struct {
  672. Rules []struct {
  673. InboundTag []string `json:"inboundTag"`
  674. User []string `json:"user"`
  675. OutboundTag string `json:"outboundTag"`
  676. Type string `json:"type"`
  677. } `json:"rules"`
  678. }
  679. type v6EgressOutbound struct {
  680. Tag string `json:"tag"`
  681. Protocol string `json:"protocol"`
  682. SendThrough string `json:"sendThrough"`
  683. }
  684. func TestInjectAmneziawgV6Egress_CreatesOutboundAndRuleForV6Peer(t *testing.T) {
  685. cfg := egressTestConfig()
  686. inbound := amneziawgV6Inbound(7, "awg-7", "eth0", []model.Client{
  687. {Email: "a@x", Enable: true, PublicKey: "pub-a", AllowedIPs: []string{"10.8.1.2/32", "fd86:ea04:1115::2/128"}},
  688. })
  689. injectAmneziawgSocksThenV6(cfg, []*model.Inbound{inbound})
  690. var outbounds []v6EgressOutbound
  691. if err := json.Unmarshal(cfg.OutboundConfigs, &outbounds); err != nil {
  692. t.Fatal(err)
  693. }
  694. wantTag := amneziawgV6EgressTag(7, "a@x")
  695. var got *v6EgressOutbound
  696. for i := range outbounds {
  697. if outbounds[i].Tag == wantTag {
  698. got = &outbounds[i]
  699. }
  700. }
  701. if got == nil {
  702. t.Fatalf("expected an outbound tagged %q, got %+v", wantTag, outbounds)
  703. }
  704. if got.Protocol != "freedom" || got.SendThrough != "fd86:ea04:1115::2" {
  705. t.Fatalf("outbound must be a freedom outbound bound to the peer's own v6 address, got %+v", got)
  706. }
  707. // Pre-existing outbounds (direct, warp) must survive untouched.
  708. if len(outbounds) != 3 {
  709. t.Fatalf("expected the 2 pre-existing outbounds plus 1 new one, got %+v", outbounds)
  710. }
  711. var routing v6EgressRouting
  712. if err := json.Unmarshal(cfg.RouterConfig, &routing); err != nil {
  713. t.Fatal(err)
  714. }
  715. ruleIdx := -1
  716. for i := range routing.Rules {
  717. if routing.Rules[i].OutboundTag == wantTag {
  718. ruleIdx = i
  719. }
  720. }
  721. if ruleIdx == -1 {
  722. t.Fatalf("expected a routing rule targeting %q, got %+v", wantTag, routing.Rules)
  723. }
  724. rule := routing.Rules[ruleIdx]
  725. if rule.Type != "field" || len(rule.User) != 1 || rule.User[0] != "a@x" ||
  726. len(rule.InboundTag) != 1 || rule.InboundTag[0] != "awg-7" {
  727. t.Fatalf("rule must match this peer's email and inbound tag, got %+v", rule)
  728. }
  729. }
  730. func TestInjectAmneziawgV6Egress_SkipsPeerWithoutV6Address(t *testing.T) {
  731. cfg := egressTestConfig()
  732. before := string(cfg.OutboundConfigs)
  733. inbound := amneziawgV6Inbound(1, "awg-1", "eth0", []model.Client{
  734. {Email: "a@x", Enable: true, PublicKey: "pub-a", AllowedIPs: []string{"10.8.1.2/32"}}, // v4 only
  735. })
  736. injectAmneziawgSocksThenV6(cfg, []*model.Inbound{inbound})
  737. if string(cfg.OutboundConfigs) != before {
  738. t.Fatalf("a peer with no v6 AllowedIPs entry must not get an outbound, got %s", cfg.OutboundConfigs)
  739. }
  740. }
  741. // The documented "leave the interface blank to auto-detect" happy path must
  742. // not silently emit a sendThrough for an address the host was never told to
  743. // own -- there is no auto-detect, so that would fail every connection.
  744. func TestInjectAmneziawgV6Egress_SkipsWhenIPv6EnabledButInterfaceBlank(t *testing.T) {
  745. cfg := egressTestConfig()
  746. before := string(cfg.OutboundConfigs)
  747. inbound := amneziawgV6InboundNotActive(1, "awg-1", true, "", []model.Client{
  748. {Email: "a@x", Enable: true, PublicKey: "pub-a", AllowedIPs: []string{"10.8.1.2/32", "fd86::2/128"}},
  749. })
  750. injectAmneziawgSocksThenV6(cfg, []*model.Inbound{inbound})
  751. if string(cfg.OutboundConfigs) != before {
  752. t.Fatalf("IPv6Enabled with a blank interface must not get an outbound (no auto-detect exists), got %s", cfg.OutboundConfigs)
  753. }
  754. }
  755. // The inverse of amneziawgV6Inbound's own always-true IPv6Enabled: a filled
  756. // IPv6ExternalInterface alone (e.g. left over from a previous enable) must
  757. // not activate egress on its own.
  758. func TestInjectAmneziawgV6Egress_SkipsWhenIPv6DisabledEvenWithInterfaceSet(t *testing.T) {
  759. cfg := egressTestConfig()
  760. before := string(cfg.OutboundConfigs)
  761. inbound := amneziawgV6InboundNotActive(1, "awg-1", false, "eth0", []model.Client{
  762. {Email: "a@x", Enable: true, PublicKey: "pub-a", AllowedIPs: []string{"10.8.1.2/32", "fd86::2/128"}},
  763. })
  764. injectAmneziawgSocksThenV6(cfg, []*model.Inbound{inbound})
  765. if string(cfg.OutboundConfigs) != before {
  766. t.Fatalf("IPv6Enabled false must not get an outbound even with a leftover interface set, got %s", cfg.OutboundConfigs)
  767. }
  768. }
  769. func TestInjectAmneziawgV6Egress_MultiplePeersEachGetOwnOutboundAndRule(t *testing.T) {
  770. cfg := egressTestConfig()
  771. inbound := amneziawgV6Inbound(1, "awg-1", "eth0", []model.Client{
  772. {Email: "a@x", Enable: true, PublicKey: "pub-a", AllowedIPs: []string{"fd86:ea04:1115::2/128"}},
  773. {Email: "b@x", Enable: true, PublicKey: "pub-b", AllowedIPs: []string{"fd86:ea04:1115::3/128"}},
  774. })
  775. injectAmneziawgSocksThenV6(cfg, []*model.Inbound{inbound})
  776. var outbounds []v6EgressOutbound
  777. if err := json.Unmarshal(cfg.OutboundConfigs, &outbounds); err != nil {
  778. t.Fatal(err)
  779. }
  780. tagA, tagB := amneziawgV6EgressTag(1, "a@x"), amneziawgV6EgressTag(1, "b@x")
  781. seen := map[string]string{}
  782. for _, o := range outbounds {
  783. seen[o.Tag] = o.SendThrough
  784. }
  785. if seen[tagA] != "fd86:ea04:1115::2" || seen[tagB] != "fd86:ea04:1115::3" {
  786. t.Fatalf("each peer must get its own outbound bound to its own address, got %+v", seen)
  787. }
  788. }
  789. func TestInjectAmneziawgV6Egress_StableTagAcrossRegenerations(t *testing.T) {
  790. // Same instance data, two independent injections -- hot_diff.go relies on
  791. // the tag being a pure function of (inboundID, email) so it recognizes
  792. // "unchanged" rather than remove+recreate on every poll.
  793. inbound := amneziawgV6Inbound(1, "awg-1", "eth0", []model.Client{
  794. {Email: "a@x", Enable: true, PublicKey: "pub-a", AllowedIPs: []string{"fd86:ea04:1115::2/128"}},
  795. })
  796. cfg1 := egressTestConfig()
  797. injectAmneziawgSocksThenV6(cfg1, []*model.Inbound{inbound})
  798. cfg2 := egressTestConfig()
  799. injectAmneziawgSocksThenV6(cfg2, []*model.Inbound{inbound})
  800. var out1, out2 []v6EgressOutbound
  801. json.Unmarshal(cfg1.OutboundConfigs, &out1)
  802. json.Unmarshal(cfg2.OutboundConfigs, &out2)
  803. if len(out1) != len(out2) || out1[len(out1)-1].Tag != out2[len(out2)-1].Tag {
  804. t.Fatalf("tag must be stable across independent regenerations, got %+v vs %+v", out1, out2)
  805. }
  806. }
  807. func TestInjectAmneziawgV6Egress_SkipsWrongProtocolOrNodeHostedOrDisabled(t *testing.T) {
  808. cfg := egressTestConfig()
  809. before := string(cfg.OutboundConfigs)
  810. vless := &model.Inbound{Id: 1, Tag: "in-1", Protocol: model.VLESS, Enable: true}
  811. nodeID := 5
  812. nodeHosted := amneziawgV6Inbound(2, "awg-2", "eth0", []model.Client{
  813. {Email: "a@x", Enable: true, PublicKey: "pub-a", AllowedIPs: []string{"fd86:ea04:1115::2/128"}},
  814. })
  815. nodeHosted.NodeID = &nodeID
  816. disabled := amneziawgV6Inbound(3, "awg-3", "eth0", []model.Client{
  817. {Email: "b@x", Enable: true, PublicKey: "pub-b", AllowedIPs: []string{"fd86:ea04:1115::3/128"}},
  818. })
  819. disabled.Enable = false
  820. injectAmneziawgSocksThenV6(cfg, []*model.Inbound{vless, nodeHosted, disabled})
  821. if string(cfg.OutboundConfigs) != before {
  822. t.Fatalf("wrong-protocol, node-hosted, and disabled inbounds must never get a v6 outbound, got %s", cfg.OutboundConfigs)
  823. }
  824. }
  825. func TestInjectAmneziawgV6Egress_SkipsWhenRelayInboundNotCreated(t *testing.T) {
  826. cfg := egressTestConfig()
  827. // A pre-existing inbound already holds this AmneziaWG inbound's tag, so
  828. // injectAmneziawgnetSocks (called first, matching production order)
  829. // skips creating its relay SOCKS5 inbound entirely.
  830. cfg.InboundConfigs = append(cfg.InboundConfigs,
  831. xray.InboundConfig{Port: 1234, Protocol: "vless", Tag: "awg-1"})
  832. before := string(cfg.OutboundConfigs)
  833. inbound := amneziawgV6Inbound(1, "awg-1", "eth0", []model.Client{
  834. {Email: "a@x", Enable: true, PublicKey: "pub-a", AllowedIPs: []string{"fd86:ea04:1115::2/128"}},
  835. })
  836. injectAmneziawgSocksThenV6(cfg, []*model.Inbound{inbound})
  837. if string(cfg.OutboundConfigs) != before {
  838. t.Fatalf("no v6 outbound should be created when the relay inbound itself never got created, got %s", cfg.OutboundConfigs)
  839. }
  840. }
  841. func TestInjectAmneziawgV6Egress_OutboundTagCollisionSkipsThatPeerOnly(t *testing.T) {
  842. cfg := egressTestConfig()
  843. inbound := amneziawgV6Inbound(1, "awg-1", "eth0", []model.Client{
  844. {Email: "a@x", Enable: true, PublicKey: "pub-a", AllowedIPs: []string{"fd86:ea04:1115::2/128"}},
  845. {Email: "b@x", Enable: true, PublicKey: "pub-b", AllowedIPs: []string{"fd86:ea04:1115::3/128"}},
  846. })
  847. // Pre-seed a colliding outbound tag for a@x specifically.
  848. collidingTag := amneziawgV6EgressTag(1, "a@x")
  849. existing, _ := json.Marshal([]any{map[string]any{"tag": collidingTag, "protocol": "freedom"}})
  850. cfg.OutboundConfigs = json_util.RawMessage(existing)
  851. injectAmneziawgSocksThenV6(cfg, []*model.Inbound{inbound})
  852. var outbounds []v6EgressOutbound
  853. if err := json.Unmarshal(cfg.OutboundConfigs, &outbounds); err != nil {
  854. t.Fatal(err)
  855. }
  856. tagB := amneziawgV6EgressTag(1, "b@x")
  857. foundB := false
  858. countA := 0
  859. for _, o := range outbounds {
  860. if o.Tag == collidingTag {
  861. countA++
  862. }
  863. if o.Tag == tagB {
  864. foundB = true
  865. }
  866. }
  867. if countA != 1 {
  868. t.Fatalf("a@x's pre-existing outbound must not be duplicated, got %d copies", countA)
  869. }
  870. if !foundB {
  871. t.Fatal("b@x must still get its own outbound despite a@x's tag collision")
  872. }
  873. }
  874. func TestInjectAmneziawgV6Egress_BadOutboundsOrRoutingSkips(t *testing.T) {
  875. inbound := amneziawgV6Inbound(1, "awg-1", "eth0", []model.Client{
  876. {Email: "a@x", Enable: true, PublicKey: "pub-a", AllowedIPs: []string{"fd86:ea04:1115::2/128"}},
  877. })
  878. cfg := egressTestConfig()
  879. cfg.OutboundConfigs = json_util.RawMessage(`{not json`)
  880. injectAmneziawgSocksThenV6(cfg, []*model.Inbound{inbound})
  881. if string(cfg.OutboundConfigs) != `{not json` {
  882. t.Fatalf("unparsable outbounds must be left untouched, got %s", cfg.OutboundConfigs)
  883. }
  884. cfg2 := egressTestConfig()
  885. cfg2.RouterConfig = json_util.RawMessage(`{not json`)
  886. injectAmneziawgSocksThenV6(cfg2, []*model.Inbound{inbound})
  887. if string(cfg2.RouterConfig) != `{not json` {
  888. t.Fatalf("unparsable routing must be left untouched, got %s", cfg2.RouterConfig)
  889. }
  890. }
  891. func TestInjectAmneziawgV6Egress_NoQualifyingPeerLeavesConfigUntouched(t *testing.T) {
  892. cfg := egressTestConfig()
  893. beforeOut, beforeRoute := string(cfg.OutboundConfigs), string(cfg.RouterConfig)
  894. inbound := amneziawgV6Inbound(1, "awg-1", "eth0", nil) // no clients at all
  895. injectAmneziawgV6Egress(cfg, []*model.Inbound{inbound})
  896. if string(cfg.OutboundConfigs) != beforeOut || string(cfg.RouterConfig) != beforeRoute {
  897. t.Fatalf("an inbound with no qualifying peer must leave the config byte-identical")
  898. }
  899. }
  900. func TestInjectAmneziawgV6Egress_RulesPrependedBeforeExistingRules(t *testing.T) {
  901. cfg := egressTestConfig() // already has one rule, targeting "api"
  902. inbound := amneziawgV6Inbound(1, "awg-1", "eth0", []model.Client{
  903. {Email: "a@x", Enable: true, PublicKey: "pub-a", AllowedIPs: []string{"fd86:ea04:1115::2/128"}},
  904. })
  905. injectAmneziawgSocksThenV6(cfg, []*model.Inbound{inbound})
  906. var routing v6EgressRouting
  907. if err := json.Unmarshal(cfg.RouterConfig, &routing); err != nil {
  908. t.Fatal(err)
  909. }
  910. if len(routing.Rules) != 2 {
  911. t.Fatalf("expected the new rule plus the pre-existing one, got %+v", routing.Rules)
  912. }
  913. if routing.Rules[0].OutboundTag != amneziawgV6EgressTag(1, "a@x") {
  914. t.Fatalf("the new infra rule must be prepended ahead of the pre-existing rule, got %+v", routing.Rules[0])
  915. }
  916. if routing.Rules[1].OutboundTag != "api" {
  917. t.Fatalf("the pre-existing rule must survive, got %+v", routing.Rules[1])
  918. }
  919. }