| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351 |
- package service
- import (
- "crypto/ecdsa"
- "crypto/elliptic"
- "crypto/rand"
- "crypto/x509"
- "crypto/x509/pkix"
- "encoding/json"
- "encoding/pem"
- "math/big"
- "os"
- "path/filepath"
- "sort"
- "strings"
- "testing"
- "time"
- "github.com/mhsanaei/3x-ui/v3/internal/database/model"
- "github.com/xtls/xray-core/infra/conf"
- )
- // The frontend's golden fixtures are the panel's model of an xray config: the
- // Zod snapshots pin what the forms parse and emit. Parsing proves the panel
- // agrees with itself, not that xray-core would accept the result, so every
- // fixture is also built here through the very config builders the panel hands
- // its config to — conf.InboundDetourConfig for the full-config and AddInbound
- // paths, conf.RouterConfig for ApplyRoutingConfig, conf.DNSConfig for the dns
- // section. A fixture xray-core refuses is a config the panel would let an
- // admin save and then fail to start the core with, taking every inbound down.
- //
- // mtproto is excluded: it is served by the bundled mtg-multi sidecar, not by
- // xray, so xray-core has no config id for it.
- func goldenFixtureDir(t *testing.T, category string) string {
- t.Helper()
- dir, err := filepath.Abs(filepath.Join("..", "..", "..", "frontend", "src", "test", "golden", "fixtures", category))
- if err != nil {
- t.Fatalf("resolve fixture dir: %v", err)
- }
- return dir
- }
- // goldenFixtures returns every fixture in a category as name -> decoded object.
- func goldenFixtures(t *testing.T, category string) map[string]map[string]any {
- t.Helper()
- dir := goldenFixtureDir(t, category)
- entries, err := os.ReadDir(dir)
- if err != nil {
- t.Fatalf("read %s: %v", dir, err)
- }
- out := make(map[string]map[string]any)
- names := make([]string, 0, len(entries))
- for _, entry := range entries {
- if filepath.Ext(entry.Name()) != ".json" {
- continue
- }
- names = append(names, entry.Name())
- }
- sort.Strings(names)
- if len(names) == 0 {
- t.Fatalf("no fixtures under %s", dir)
- }
- for _, name := range names {
- raw, err := os.ReadFile(filepath.Join(dir, name))
- if err != nil {
- t.Fatalf("read %s: %v", name, err)
- }
- var obj map[string]any
- if err := json.Unmarshal(raw, &obj); err != nil {
- t.Fatalf("unmarshal %s: %v", name, err)
- }
- out[strings.TrimSuffix(name, ".json")] = obj
- }
- return out
- }
- // writeTestCertificate writes a self-signed certificate and key, returning both
- // paths. The TLS fixtures point certificateFile/keyFile at deployment paths
- // that do not exist here, and xray-core reads them while building.
- func writeTestCertificate(t *testing.T) (certPath, keyPath string) {
- t.Helper()
- key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
- if err != nil {
- t.Fatalf("generate key: %v", err)
- }
- template := x509.Certificate{
- SerialNumber: big.NewInt(1),
- Subject: pkix.Name{CommonName: "golden-fixture.test"},
- NotBefore: time.Now().Add(-time.Hour),
- NotAfter: time.Now().Add(24 * time.Hour),
- DNSNames: []string{"golden-fixture.test"},
- }
- der, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key)
- if err != nil {
- t.Fatalf("create certificate: %v", err)
- }
- keyDER, err := x509.MarshalECPrivateKey(key)
- if err != nil {
- t.Fatalf("marshal key: %v", err)
- }
- dir := t.TempDir()
- certPath = filepath.Join(dir, "fixture.crt")
- keyPath = filepath.Join(dir, "fixture.key")
- certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})
- keyPEM := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER})
- if err := os.WriteFile(certPath, certPEM, 0o600); err != nil {
- t.Fatalf("write certificate: %v", err)
- }
- if err := os.WriteFile(keyPath, keyPEM, 0o600); err != nil {
- t.Fatalf("write key: %v", err)
- }
- return certPath, keyPath
- }
- // repointCertificateFiles rewrites every certificateFile/keyFile reference to
- // the generated pair, so a fixture is judged on its shape rather than on paths
- // that only exist on a deployed server.
- func repointCertificateFiles(node any, certPath, keyPath string) {
- switch value := node.(type) {
- case map[string]any:
- for key, child := range value {
- switch key {
- case "certificateFile":
- if _, ok := child.(string); ok {
- value[key] = certPath
- continue
- }
- case "keyFile":
- if _, ok := child.(string); ok {
- value[key] = keyPath
- continue
- }
- }
- repointCertificateFiles(child, certPath, keyPath)
- }
- case []any:
- for _, child := range value {
- repointCertificateFiles(child, certPath, keyPath)
- }
- }
- }
- // assertXrayAccepts fails unless xray-core built the fixture. Fixtures naming
- // geoip:/geosite: need the dat files the panel ships next to the xray binary;
- // where they are absent the loader fails on the missing file rather than on the
- // fixture, so those are skipped instead of reported as broken.
- func assertXrayAccepts(t *testing.T, subject string, err error) {
- t.Helper()
- if err == nil {
- return
- }
- if isMissingGeoAssetErr(err) {
- t.Skipf("geo data files not available, cannot judge %s: %v", subject, err)
- }
- t.Fatalf("xray-core refuses %s: %v", subject, err)
- }
- func buildGoldenInbound(t *testing.T, inbound map[string]any) error {
- t.Helper()
- certPath, keyPath := writeTestCertificate(t)
- repointCertificateFiles(inbound, certPath, keyPath)
- raw, err := json.Marshal(inbound)
- if err != nil {
- t.Fatalf("marshal inbound: %v", err)
- }
- detour := new(conf.InboundDetourConfig)
- if err := json.Unmarshal(raw, detour); err != nil {
- return err
- }
- _, err = detour.Build()
- return err
- }
- // TestGoldenInboundFixturesBuildInXray wraps each protocol fixture in a minimal
- // inbound and builds it.
- func TestGoldenInboundFixturesBuildInXray(t *testing.T) {
- for name, fixture := range goldenFixtures(t, "inbound") {
- if protocol, _ := fixture["protocol"].(string); protocol == string(model.MTProto) {
- continue
- }
- t.Run(name, func(t *testing.T) {
- inbound := map[string]any{
- "tag": "golden-in",
- "listen": "127.0.0.1",
- "port": 8443,
- "protocol": fixture["protocol"],
- "settings": fixture["settings"],
- }
- assertXrayAccepts(t, "this fixture", buildGoldenInbound(t, inbound))
- })
- }
- }
- // TestGoldenInboundFullFixturesBuildInXray runs the complete panel Inbound
- // model through GenXrayInboundConfig, the conversion both the full-config and
- // the live AddInbound paths use, and builds the result.
- func TestGoldenInboundFullFixturesBuildInXray(t *testing.T) {
- for name, fixture := range goldenFixtures(t, "inbound-full") {
- protocol, _ := fixture["protocol"].(string)
- if protocol == string(model.MTProto) {
- continue
- }
- t.Run(name, func(t *testing.T) {
- section := func(key string) string {
- value, ok := fixture[key]
- if !ok || value == nil {
- return ""
- }
- raw, err := json.Marshal(value)
- if err != nil {
- t.Fatalf("marshal %s: %v", key, err)
- }
- return string(raw)
- }
- port := 0
- if p, ok := fixture["port"].(float64); ok {
- port = int(p)
- }
- listen, _ := fixture["listen"].(string)
- tag, _ := fixture["tag"].(string)
- ib := &model.Inbound{
- Protocol: model.Protocol(protocol),
- Port: port,
- Listen: listen,
- Tag: tag,
- Settings: section("settings"),
- StreamSettings: section("streamSettings"),
- Sniffing: section("sniffing"),
- }
- raw, err := json.Marshal(ib.GenXrayInboundConfig())
- if err != nil {
- t.Fatalf("marshal generated inbound: %v", err)
- }
- var generated map[string]any
- if err := json.Unmarshal(raw, &generated); err != nil {
- t.Fatalf("decode generated inbound: %v", err)
- }
- assertXrayAccepts(t, "the generated inbound", buildGoldenInbound(t, generated))
- })
- }
- }
- // TestGoldenStreamFixturesBuildInXray attaches the transport fragments — whole
- // stream sections, the security block, sockopt and finalmask — to an inbound.
- func TestGoldenStreamFixturesBuildInXray(t *testing.T) {
- for _, category := range []string{"stream", "security", "sockopt", "finalmask"} {
- for name, fixture := range goldenFixtures(t, category) {
- t.Run(category+"/"+name, func(t *testing.T) {
- stream := map[string]any{"network": "tcp"}
- switch category {
- case "stream":
- stream = fixture
- case "security":
- for key, value := range fixture {
- stream[key] = value
- }
- case "sockopt":
- stream["sockopt"] = fixture
- case "finalmask":
- stream["finalmask"] = fixture
- }
- inbound := map[string]any{
- "tag": "golden-in", "listen": "127.0.0.1", "port": 8443, "protocol": "vless",
- "settings": map[string]any{
- "clients": []any{map[string]any{"id": "b831381d-6324-4d53-ad4f-8cda48b30811", "email": "golden"}},
- "decryption": "none",
- },
- "streamSettings": stream,
- }
- assertXrayAccepts(t, "this fixture", buildGoldenInbound(t, inbound))
- })
- }
- }
- }
- func buildGoldenRouting(routing map[string]any) error {
- raw, err := json.Marshal(routing)
- if err != nil {
- return err
- }
- router := new(conf.RouterConfig)
- if err := json.Unmarshal(raw, router); err != nil {
- return err
- }
- _, err = router.Build()
- return err
- }
- // TestGoldenRoutingFixturesBuildInXray builds the rule and balancer fixtures
- // through the router config ApplyRoutingConfig hands to the running core.
- func TestGoldenRoutingFixturesBuildInXray(t *testing.T) {
- for name, rule := range goldenFixtures(t, "rule") {
- t.Run("rule/"+name, func(t *testing.T) {
- assertXrayAccepts(t, "this rule", buildGoldenRouting(map[string]any{
- "domainStrategy": "AsIs",
- "rules": []any{rule},
- "balancers": []any{map[string]any{"tag": "balancer-load", "selector": []any{"proxy-"}}},
- }))
- })
- }
- for name, balancer := range goldenFixtures(t, "balancer") {
- t.Run("balancer/"+name, func(t *testing.T) {
- assertXrayAccepts(t, "this balancer", buildGoldenRouting(map[string]any{
- "domainStrategy": "AsIs",
- "balancers": []any{balancer},
- "rules": []any{map[string]any{
- "type": "field", "port": "443", "balancerTag": balancer["tag"],
- }},
- }))
- })
- }
- }
- // TestGoldenDNSFixturesBuildInXray builds the dns section, and each dns-server
- // fixture inside one.
- func TestGoldenDNSFixturesBuildInXray(t *testing.T) {
- build := func(dns map[string]any) error {
- raw, err := json.Marshal(dns)
- if err != nil {
- return err
- }
- dnsConf := new(conf.DNSConfig)
- if err := json.Unmarshal(raw, dnsConf); err != nil {
- return err
- }
- _, err = dnsConf.Build()
- return err
- }
- for name, fixture := range goldenFixtures(t, "dns") {
- t.Run("dns/"+name, func(t *testing.T) {
- assertXrayAccepts(t, "this dns section", build(fixture))
- })
- }
- for name, server := range goldenFixtures(t, "dns-server") {
- t.Run("dns-server/"+name, func(t *testing.T) {
- assertXrayAccepts(t, "this dns server", build(map[string]any{"servers": []any{server}}))
- })
- }
- }
- func isMissingGeoAssetErr(err error) bool {
- msg := err.Error()
- return strings.Contains(msg, "geoip.dat") || strings.Contains(msg, "geosite.dat")
- }
|