diff --git a/core/config_compat_test.go b/core/config_compat_test.go new file mode 100644 index 0000000..bcdf13c --- /dev/null +++ b/core/config_compat_test.go @@ -0,0 +1,125 @@ +package core + +import ( + "bytes" + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "math/big" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/alireza0/s-ui/logger" + + "github.com/op/go-logging" + "github.com/sagernet/sing-box/experimental/deprecated" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/service" +) + +// collectManager records deprecation notes instead of writing them to stderr, +// so tests can assert on which ones a config triggers. +type collectManager struct{ notes []deprecated.Note } + +func (c *collectManager) ReportDeprecated(feature deprecated.Note) { + c.notes = append(c.notes, feature) +} + +// writeTestCert generates a throwaway self-signed cert for the TLS-bearing +// inbounds in testdata, returning the certificate and key paths. +func writeTestCert(t *testing.T) (string, string) { + t.Helper() + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + t.Fatal(err) + } + tmpl := x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{CommonName: "t.example.com"}, + DNSNames: []string{"t.example.com"}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().Add(24 * time.Hour), + } + der, err := x509.CreateCertificate(rand.Reader, &tmpl, &tmpl, &key.PublicKey, key) + if err != nil { + t.Fatal(err) + } + keyDER, err := x509.MarshalECPrivateKey(key) + if err != nil { + t.Fatal(err) + } + dir := t.TempDir() + certPath := filepath.Join(dir, "cert.pem") + keyPath := filepath.Join(dir, "key.pem") + writePEM(t, certPath, "CERTIFICATE", der) + writePEM(t, keyPath, "EC PRIVATE KEY", keyDER) + return certPath, keyPath +} + +func writePEM(t *testing.T, path, blockType string, der []byte) { + t.Helper() + var buf bytes.Buffer + if err := pem.Encode(&buf, &pem.Block{Type: blockType, Bytes: der}); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, buf.Bytes(), 0o600); err != nil { + t.Fatal(err) + } +} + +// TestConfigCompat starts a box for each config in testdata/configs and reports +// the deprecation notes it triggers. It guards the sing-box upgrade: configs +// s-ui's UI can produce must keep starting, and any new deprecation shows up in +// the test log so it can be migrated before release. +func TestConfigCompat(t *testing.T) { + logger.InitLogger(logging.ERROR) + certPath, keyPath := writeTestCert(t) + + files, err := filepath.Glob(filepath.Join("testdata", "configs", "*.json")) + if err != nil { + t.Fatal(err) + } + if len(files) == 0 { + t.Fatal("no test configs found") + } + + for _, file := range files { + t.Run(filepath.Base(file), func(t *testing.T) { + raw, err := os.ReadFile(file) + if err != nil { + t.Fatal(err) + } + data := strings.NewReplacer("__CERT__", certPath, "__KEY__", keyPath).Replace(string(raw)) + + ctx := Context(context.Background(), InboundRegistry(), OutboundRegistry(), + EndpointRegistry(), DNSTransportRegistry(), ServiceRegistry(), CertificateProviderRegistry()) + notes := &collectManager{} + ctx = service.ContextWith[deprecated.Manager](ctx, notes) + + var opts option.Options + if err = opts.UnmarshalJSONContext(ctx, []byte(data)); err != nil { + t.Fatalf("parse: %v", err) + } + + instance, err := NewBox(Options{Context: ctx, Options: opts}) + if err != nil { + t.Fatalf("create box: %v", err) + } + defer instance.Close() + if err = instance.Start(); err != nil { + t.Fatalf("start: %v", err) + } + + for _, note := range notes.notes { + t.Logf("deprecated: %s -- %s", note.Name, note.Description) + } + }) + } +} diff --git a/core/testdata/configs/02-dns-independent-cache.json b/core/testdata/configs/02-dns-independent-cache.json new file mode 100644 index 0000000..1c41587 --- /dev/null +++ b/core/testdata/configs/02-dns-independent-cache.json @@ -0,0 +1,8 @@ +{ + "dns": { + "servers": [{"type": "udp", "tag": "google", "server": "8.8.8.8"}], + "rules": [], + "independent_cache": true + }, + "outbounds": [{"type": "direct", "tag": "direct"}] +} diff --git a/core/testdata/configs/03-dns-rule-strategy.json b/core/testdata/configs/03-dns-rule-strategy.json new file mode 100644 index 0000000..49e50ae --- /dev/null +++ b/core/testdata/configs/03-dns-rule-strategy.json @@ -0,0 +1,7 @@ +{ + "dns": { + "servers": [{"type": "udp", "tag": "google", "server": "8.8.8.8"}], + "rules": [{"domain": ["example.com"], "action": "route", "server": "google", "strategy": "ipv4_only"}] + }, + "outbounds": [{"type": "direct", "tag": "direct"}] +} diff --git a/core/testdata/configs/06-store-rdrc.json b/core/testdata/configs/06-store-rdrc.json new file mode 100644 index 0000000..8ae25cd --- /dev/null +++ b/core/testdata/configs/06-store-rdrc.json @@ -0,0 +1,4 @@ +{ + "experimental": {"cache_file": {"enabled": true, "store_rdrc": true}}, + "outbounds": [{"type": "direct", "tag": "direct"}] +} diff --git a/core/testdata/configs/07-sui-default.json b/core/testdata/configs/07-sui-default.json new file mode 100644 index 0000000..a5315a5 --- /dev/null +++ b/core/testdata/configs/07-sui-default.json @@ -0,0 +1,6 @@ +{ + "log": {"level": "info"}, + "dns": {"servers": [], "rules": []}, + "route": {"rules": [{"action": "sniff"}, {"protocol": ["dns"], "action": "hijack-dns"}]}, + "experimental": {} +} diff --git a/core/testdata/configs/08-full-protocols.json b/core/testdata/configs/08-full-protocols.json new file mode 100644 index 0000000..3e9fbd8 --- /dev/null +++ b/core/testdata/configs/08-full-protocols.json @@ -0,0 +1,26 @@ +{ + "log": {"level": "error"}, + "inbounds": [ + {"type": "shadowsocks", "tag": "ss-in", "listen": "127.0.0.1", "listen_port": 42001, + "method": "2022-blake3-aes-128-gcm", "password": "8JCsPssfgS8tiRwiMlhARg=="}, + {"type": "vmess", "tag": "vmess-in", "listen": "127.0.0.1", "listen_port": 42002, + "users": [{"uuid": "b831381d-6324-4d53-ad4f-8cda48b30811", "alterId": 0}]}, + {"type": "vless", "tag": "vless-in", "listen": "127.0.0.1", "listen_port": 42003, + "users": [{"uuid": "b831381d-6324-4d53-ad4f-8cda48b30811"}]}, + {"type": "trojan", "tag": "trojan-in", "listen": "127.0.0.1", "listen_port": 42004, + "users": [{"password": "trojanpass"}]}, + {"type": "anytls", "tag": "anytls-in", "listen": "127.0.0.1", "listen_port": 42005, + "users": [{"password": "anytlspass"}], + "tls": {"enabled": true, "server_name": "t.example.com", "key_path": "__KEY__", "certificate_path": "__CERT__"}}, + {"type": "hysteria2", "tag": "hy2-in", "listen": "127.0.0.1", "listen_port": 42006, + "users": [{"password": "hy2pass"}], + "tls": {"enabled": true, "server_name": "t.example.com", "alpn": ["h3"], "key_path": "__KEY__", "certificate_path": "__CERT__"}}, + {"type": "tuic", "tag": "tuic-in", "listen": "127.0.0.1", "listen_port": 42007, + "users": [{"uuid": "b831381d-6324-4d53-ad4f-8cda48b30811", "password": "tuicpass"}], + "tls": {"enabled": true, "server_name": "t.example.com", "alpn": ["h3"], "key_path": "__KEY__", "certificate_path": "__CERT__"}}, + {"type": "hysteria", "tag": "hy1-in", "listen": "127.0.0.1", "listen_port": 42008, + "up_mbps": 100, "down_mbps": 100, "users": [{"auth_str": "hy1pass"}], + "tls": {"enabled": true, "server_name": "t.example.com", "alpn": ["h3"], "key_path": "__KEY__", "certificate_path": "__CERT__"}} + ], + "outbounds": [{"type": "direct", "tag": "direct"}] +} diff --git a/core/testdata/configs/10-dns-ipcidr.json b/core/testdata/configs/10-dns-ipcidr.json new file mode 100644 index 0000000..3d77dda --- /dev/null +++ b/core/testdata/configs/10-dns-ipcidr.json @@ -0,0 +1,5 @@ +{ + "dns": {"servers": [{"type": "udp", "tag": "google", "server": "8.8.8.8"}], + "rules": [{"ip_cidr": ["10.0.0.0/8"], "action": "route", "server": "google"}]}, + "outbounds": [{"type": "direct", "tag": "direct"}] +}