|
|
@@ -2,11 +2,23 @@ package service
|
|
|
|
|
|
import (
|
|
|
"encoding/json"
|
|
|
+ "os"
|
|
|
"testing"
|
|
|
|
|
|
+ xuilogger "github.com/mhsanaei/3x-ui/v3/internal/logger"
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/util/json_util"
|
|
|
+ "github.com/mhsanaei/3x-ui/v3/internal/xray"
|
|
|
+
|
|
|
+ "github.com/op/go-logging"
|
|
|
)
|
|
|
|
|
|
+func TestMain(m *testing.M) {
|
|
|
+ // injectPanelEgress logs when it skips injection; the package logger must
|
|
|
+ // exist before any test exercises a skipped path.
|
|
|
+ xuilogger.InitLogger(logging.ERROR)
|
|
|
+ os.Exit(m.Run())
|
|
|
+}
|
|
|
+
|
|
|
func TestEnsureAPIServices(t *testing.T) {
|
|
|
// legacy template without RoutingService gets it injected
|
|
|
out := ensureAPIServices(json_util.RawMessage(`{"services":["HandlerService","LoggerService","StatsService"],"tag":"api"}`))
|
|
|
@@ -41,3 +53,107 @@ func TestEnsureAPIServices(t *testing.T) {
|
|
|
t.Fatalf("nil api block must stay nil, got %s", got)
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+func egressTestConfig() *xray.Config {
|
|
|
+ return &xray.Config{
|
|
|
+ RouterConfig: json_util.RawMessage(`{"domainStrategy":"AsIs","rules":[{"type":"field","inboundTag":["api"],"outboundTag":"api"}]}`),
|
|
|
+ InboundConfigs: []xray.InboundConfig{
|
|
|
+ {Port: 62789, Protocol: "tunnel", Tag: "api", Listen: json_util.RawMessage(`"127.0.0.1"`)},
|
|
|
+ },
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+type egressRouting struct {
|
|
|
+ DomainStrategy string `json:"domainStrategy"`
|
|
|
+ Rules []struct {
|
|
|
+ InboundTag []string `json:"inboundTag"`
|
|
|
+ OutboundTag string `json:"outboundTag"`
|
|
|
+ Type string `json:"type"`
|
|
|
+ } `json:"rules"`
|
|
|
+}
|
|
|
+
|
|
|
+func TestInjectPanelEgress(t *testing.T) {
|
|
|
+ cfg := egressTestConfig()
|
|
|
+ injectPanelEgress(cfg, "warp")
|
|
|
+
|
|
|
+ if len(cfg.InboundConfigs) != 2 {
|
|
|
+ t.Fatalf("expected the egress inbound to be appended, got %d inbounds", len(cfg.InboundConfigs))
|
|
|
+ }
|
|
|
+ ib := cfg.InboundConfigs[1]
|
|
|
+ if ib.Tag != PanelEgressInboundTag || ib.Protocol != "socks" || ib.Port != panelEgressBasePort {
|
|
|
+ t.Fatalf("unexpected egress inbound: %+v", ib)
|
|
|
+ }
|
|
|
+ if string(ib.Listen) != `"127.0.0.1"` {
|
|
|
+ t.Fatalf("egress inbound must listen on loopback, got %s", ib.Listen)
|
|
|
+ }
|
|
|
+
|
|
|
+ var routing egressRouting
|
|
|
+ if err := json.Unmarshal(cfg.RouterConfig, &routing); err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ if routing.DomainStrategy != "AsIs" {
|
|
|
+ t.Fatalf("routing keys outside rules must be preserved, got %+v", routing)
|
|
|
+ }
|
|
|
+ if len(routing.Rules) != 2 {
|
|
|
+ t.Fatalf("expected egress rule + existing rule, got %+v", routing.Rules)
|
|
|
+ }
|
|
|
+ first := routing.Rules[0]
|
|
|
+ if first.Type != "field" || first.OutboundTag != "warp" ||
|
|
|
+ len(first.InboundTag) != 1 || first.InboundTag[0] != PanelEgressInboundTag {
|
|
|
+ t.Fatalf("egress rule must be prepended, got %+v", first)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func TestInjectPanelEgress_PortCollision(t *testing.T) {
|
|
|
+ cfg := egressTestConfig()
|
|
|
+ cfg.InboundConfigs = append(cfg.InboundConfigs,
|
|
|
+ xray.InboundConfig{Port: panelEgressBasePort, Protocol: "vless", Tag: "in-1"},
|
|
|
+ xray.InboundConfig{Port: panelEgressBasePort + 1, Protocol: "vless", Tag: "in-2"},
|
|
|
+ )
|
|
|
+ injectPanelEgress(cfg, "direct")
|
|
|
+ got := cfg.InboundConfigs[len(cfg.InboundConfigs)-1]
|
|
|
+ if got.Tag != PanelEgressInboundTag || got.Port != panelEgressBasePort+2 {
|
|
|
+ t.Fatalf("egress inbound must skip taken ports, got %+v", got)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func TestInjectPanelEgress_TagCollisionSkips(t *testing.T) {
|
|
|
+ cfg := egressTestConfig()
|
|
|
+ cfg.InboundConfigs = append(cfg.InboundConfigs,
|
|
|
+ xray.InboundConfig{Port: 1234, Protocol: "socks", Tag: PanelEgressInboundTag},
|
|
|
+ )
|
|
|
+ before := string(cfg.RouterConfig)
|
|
|
+ injectPanelEgress(cfg, "direct")
|
|
|
+ if len(cfg.InboundConfigs) != 2 || string(cfg.RouterConfig) != before {
|
|
|
+ t.Fatal("a user inbound owning the egress tag must make injection a no-op")
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func TestInjectPanelEgress_NoRoutingSection(t *testing.T) {
|
|
|
+ cfg := egressTestConfig()
|
|
|
+ cfg.RouterConfig = nil
|
|
|
+ injectPanelEgress(cfg, "direct")
|
|
|
+
|
|
|
+ var routing egressRouting
|
|
|
+ if err := json.Unmarshal(cfg.RouterConfig, &routing); err != nil {
|
|
|
+ t.Fatal(err)
|
|
|
+ }
|
|
|
+ if len(routing.Rules) != 1 || routing.Rules[0].OutboundTag != "direct" {
|
|
|
+ t.Fatalf("a routing section must be created with the egress rule, got %+v", routing)
|
|
|
+ }
|
|
|
+ if len(cfg.InboundConfigs) != 2 {
|
|
|
+ t.Fatal("egress inbound must still be appended")
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func TestInjectPanelEgress_BadRoutingSkips(t *testing.T) {
|
|
|
+ cfg := egressTestConfig()
|
|
|
+ cfg.RouterConfig = json_util.RawMessage(`{not json`)
|
|
|
+ injectPanelEgress(cfg, "direct")
|
|
|
+ if len(cfg.InboundConfigs) != 1 {
|
|
|
+ t.Fatal("unparsable routing must skip the whole injection, inbound included")
|
|
|
+ }
|
|
|
+ if string(cfg.RouterConfig) != `{not json` {
|
|
|
+ t.Fatal("unparsable routing must be left untouched")
|
|
|
+ }
|
|
|
+}
|