core: add config compatibility test for the 1.14 upgrade

Starts a real box for each config in testdata/configs and logs the
deprecation notes it triggers. Covers the eight re-copied protocol inbounds
plus the option shapes s-ui's UI can emit that 1.14 deprecates.

Confirms no config s-ui produces fails to start under 1.14; the deprecated
ones warn and keep working.
This commit is contained in:
Alireza Ahmadi 2026-08-29 12:36:30 +02:00
parent afe16b4d2d
commit 6f046508db
7 changed files with 181 additions and 0 deletions

125
core/config_compat_test.go Normal file
View file

@ -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)
}
})
}
}

View file

@ -0,0 +1,8 @@
{
"dns": {
"servers": [{"type": "udp", "tag": "google", "server": "8.8.8.8"}],
"rules": [],
"independent_cache": true
},
"outbounds": [{"type": "direct", "tag": "direct"}]
}

View file

@ -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"}]
}

View file

@ -0,0 +1,4 @@
{
"experimental": {"cache_file": {"enabled": true, "store_rdrc": true}},
"outbounds": [{"type": "direct", "tag": "direct"}]
}

View file

@ -0,0 +1,6 @@
{
"log": {"level": "info"},
"dns": {"servers": [], "rules": []},
"route": {"rules": [{"action": "sniff"}, {"protocol": ["dns"], "action": "hijack-dns"}]},
"experimental": {}
}

View file

@ -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"}]
}

View file

@ -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"}]
}