| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237 |
- package service
- import (
- "encoding/json"
- "strconv"
- "strings"
- "testing"
- "github.com/mhsanaei/3x-ui/v3/internal/amneziawgnet"
- "github.com/mhsanaei/3x-ui/v3/internal/database"
- "github.com/mhsanaei/3x-ui/v3/internal/database/model"
- "github.com/mhsanaei/3x-ui/v3/internal/xray"
- )
- // configFromInbounds builds the config the way the panel does, from raw JSON:
- // the probe is only worth anything if it parses what is really written.
- func configFromInbounds(t *testing.T, inbounds string) *xray.Config {
- t.Helper()
- var cfg xray.Config
- if err := json.Unmarshal([]byte(`{"inbounds":[`+inbounds+`]}`), &cfg); err != nil {
- t.Fatalf("build config: %v", err)
- }
- return &cfg
- }
- func TestBindConflicts(t *testing.T) {
- const (
- relay = `{"listen":"127.0.0.1","port":65101,"protocol":"socks","tag":"relay","settings":{"auth":"password","udp":true,"accounts":[]}}`
- user = `{"listen":"0.0.0.0","port":65101,"protocol":"vless","tag":"user","streamSettings":{"network":"tcp"}}`
- )
- cases := []struct {
- name string
- inbounds string
- running string
- want int
- }{
- {
- "same listen, port and tcp",
- `{"listen":"0.0.0.0","port":443,"protocol":"vless","tag":"a"},
- {"listen":"0.0.0.0","port":443,"protocol":"vmess","tag":"b"}`,
- ``, 1,
- },
- {
- "tcp and udp on one port are legal",
- `{"listen":"0.0.0.0","port":443,"protocol":"vless","tag":"a"},
- {"listen":"0.0.0.0","port":443,"protocol":"hysteria","tag":"b"}`,
- ``, 0,
- },
- {
- "kcp moves vless to udp and frees the port",
- `{"listen":"0.0.0.0","port":443,"protocol":"vless","tag":"a","streamSettings":{"network":"kcp"}},
- {"listen":"0.0.0.0","port":443,"protocol":"vless","tag":"b","streamSettings":{"network":"tcp"}}`,
- ``, 0,
- },
- {
- "wildcard listen overlaps a loopback one",
- `{"listen":"0.0.0.0","port":8443,"protocol":"vless","tag":"a"},
- {"listen":"127.0.0.1","port":8443,"protocol":"trojan","tag":"b"}`,
- ``, 1,
- },
- {
- "absent listen means wildcard",
- `{"port":8443,"protocol":"vless","tag":"a"},
- {"listen":"127.0.0.1","port":8443,"protocol":"trojan","tag":"b"}`,
- ``, 1,
- },
- {
- "distinct loopback addresses do not overlap",
- `{"listen":"127.0.0.1","port":8443,"protocol":"vless","tag":"a"},
- {"listen":"127.0.0.2","port":8443,"protocol":"vless","tag":"b"}`,
- ``, 0,
- },
- {
- "port zero is not a bind",
- `{"listen":"0.0.0.0","port":0,"protocol":"tunnel","tag":"a"},
- {"listen":"0.0.0.0","port":0,"protocol":"vless","tag":"b"}`,
- ``, 0,
- },
- {
- "clean config",
- `{"listen":"0.0.0.0","port":443,"protocol":"vless","tag":"a"},
- {"listen":"127.0.0.1","port":62789,"protocol":"tunnel","tag":"api"},
- ` + relay,
- ``, 0,
- },
- {
- // The relay the AmneziaWG family lives on: loopback tcp+udp, on the
- // same port as a user inbound's tcp.
- "amneziawg relay against a tcp inbound",
- relay + "," + user, ``, 1,
- },
- {
- "amneziawg relay against a udp inbound",
- relay + `,{"listen":"0.0.0.0","port":65101,"protocol":"hysteria","tag":"user"}`,
- ``, 1,
- },
- {
- // Proves the relay's transports come from settings.udp and not from a
- // blanket "loopback owns everything" rule.
- "socks bridge without udp coexists with a udp inbound",
- `{"listen":"127.0.0.1","port":65101,"protocol":"socks","tag":"bridge","settings":{"auth":"noauth"}},
- {"listen":"0.0.0.0","port":65101,"protocol":"hysteria","tag":"user"}`,
- ``, 0,
- },
- {
- "reserved api inbound against a user inbound",
- `{"listen":"127.0.0.1","port":62789,"protocol":"tunnel","tag":"api","settings":{"rewriteAddress":"127.0.0.1"}},
- {"listen":"0.0.0.0","port":62789,"protocol":"vless","tag":"user"}`,
- ``, 1,
- },
- {
- // The core is running this pair right now, so whatever a static read
- // says about it, it binds: an established setup is never refused.
- "collision the running config already serves",
- relay + "," + user, relay + "," + user, 0,
- },
- {
- "collision the running config does not have",
- relay + "," + user, user, 1,
- },
- {
- // `::` and `0.0.0.0` on one port is what bindv6only=1 makes legal, so the
- // running config excuses it -- but moving one onto the other is not it.
- "excused pair whose listen changed into a real collision",
- `{"listen":"0.0.0.0","port":443,"protocol":"vless","tag":"a"},
- {"listen":"0.0.0.0","port":443,"protocol":"vless","tag":"b"}`,
- `{"listen":"::","port":443,"protocol":"vless","tag":"a"},
- {"listen":"0.0.0.0","port":443,"protocol":"vless","tag":"b"}`,
- 1,
- },
- {
- // The same pair of sockets is the same evidence, whichever order the
- // generator happened to emit them in.
- "excused pair with its listens swapped stays excused",
- `{"listen":"0.0.0.0","port":443,"protocol":"vless","tag":"a"},
- {"listen":"::","port":443,"protocol":"vless","tag":"b"}`,
- `{"listen":"::","port":443,"protocol":"vless","tag":"a"},
- {"listen":"0.0.0.0","port":443,"protocol":"vless","tag":"b"}`,
- 0,
- },
- {
- "same pair on another port is still new",
- `{"listen":"127.0.0.1","port":65102,"protocol":"socks","tag":"relay","settings":{"auth":"password","udp":true,"accounts":[]}},
- {"listen":"0.0.0.0","port":65102,"protocol":"vless","tag":"user","streamSettings":{"network":"tcp"}}`,
- relay + "," + user, 1,
- },
- }
- for _, tc := range cases {
- t.Run(tc.name, func(t *testing.T) {
- var running *xray.Config
- if tc.running != "" {
- running = configFromInbounds(t, tc.running)
- }
- got := bindConflicts(configFromInbounds(t, tc.inbounds), running)
- if len(got) != tc.want {
- t.Fatalf("bindConflicts = %v, want %d conflict(s)", got, tc.want)
- }
- for _, c := range got {
- if c.tagA == "" || c.tagB == "" || c.tagA == c.tagB {
- t.Fatalf("a conflict must name both tags, got %+v", c)
- }
- }
- })
- }
- }
- func TestBindConflicts_MessageNamesBothSides(t *testing.T) {
- conflicts := bindConflicts(configFromInbounds(t, `
- {"listen":"127.0.0.1","port":65101,"protocol":"socks","tag":"relay","settings":{"auth":"password","udp":true,"accounts":[]}},
- {"listen":"0.0.0.0","port":65101,"protocol":"vless","tag":"user","streamSettings":{"network":"tcp"}}`), nil)
- if len(conflicts) != 1 {
- t.Fatalf("want exactly one conflict, got %v", conflicts)
- }
- msg := conflicts[0].String()
- for _, want := range []string{`"relay"`, `"user"`, "127.0.0.1", "65101"} {
- if !strings.Contains(msg, want) {
- t.Fatalf("conflict message %q must contain %q", msg, want)
- }
- }
- }
- // The probe must read what the panel really emits: the AmneziaWG relay is a
- // loopback "socks" inbound whose udp flag lives in settings, not streamSettings.
- func TestBindConflicts_GeneratedConfig(t *testing.T) {
- cases := []struct {
- name string
- portOff int
- running bool
- want int
- }{
- {"user inbound on the relay port", 0, false, 1},
- {"already running config", 0, true, 0},
- {"user inbound on a free port", 1, false, 0},
- }
- for _, tc := range cases {
- t.Run(tc.name, func(t *testing.T) {
- setupSettingTestDB(t)
- seedInboundConflict(t, "awg-1", "0.0.0.0", 51820, model.AmneziaWG, ``, amneziawgRoutedSettings)
- var awg model.Inbound
- if err := database.GetDB().Where("tag = ?", "awg-1").First(&awg).Error; err != nil {
- t.Fatalf("read seeded row: %v", err)
- }
- relayPort := amneziawgnet.SOCKSPortForInbound(awg.Id)
- seedInboundConflict(t, "user", "0.0.0.0", relayPort+tc.portOff, model.VLESS, `{"network":"tcp"}`, `{}`)
- svc := &XrayService{}
- cfg, err := svc.GetXrayConfig()
- if err != nil {
- t.Fatalf("GetXrayConfig: %v", err)
- }
- running := (*xray.Config)(nil)
- if tc.running {
- if running, err = svc.GetXrayConfig(); err != nil {
- t.Fatalf("second GetXrayConfig: %v", err)
- }
- }
- got := bindConflicts(cfg, running)
- if len(got) != tc.want {
- t.Fatalf("bindConflicts = %v, want %d", got, tc.want)
- }
- if tc.want == 0 {
- return
- }
- msg := got[0].String()
- for _, want := range []string{`"awg-1"`, `"user"`, strconv.Itoa(relayPort)} {
- if !strings.Contains(msg, want) {
- t.Fatalf("conflict message %q must contain %q", msg, want)
- }
- }
- })
- }
- }
|