xray_config_inject_test.go 42 KB

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