xray_config_inject_test.go 40 KB

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