181 lines
7.2 KiB
Go
181 lines
7.2 KiB
Go
package config
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestValidateRejectsUnsupportedSchemaVersion(t *testing.T) {
|
|
cfg := DefaultConfig()
|
|
cfg.SchemaVersion = CurrentSchemaVersion + 1
|
|
if err := cfg.Validate(); err == nil {
|
|
t.Fatalf("newer schema version should be rejected")
|
|
}
|
|
cfg.SchemaVersion = CurrentSchemaVersion - 1
|
|
if err := cfg.Validate(); err == nil {
|
|
t.Fatalf("older schema version should be rejected")
|
|
}
|
|
}
|
|
|
|
func TestValidateChecksProfileProjectionSchema(t *testing.T) {
|
|
cfg := DefaultConfig()
|
|
cfg.Profile.Nodes = []json.RawMessage{
|
|
json.RawMessage(`{"id":"node-1","protocol":"vless","server":"example.com","port":443,"stale":true,"source":{"type":"MANUAL"}}`),
|
|
}
|
|
|
|
if err := cfg.Validate(); err == nil {
|
|
t.Fatalf("config validation should reject invalid profile projection schema")
|
|
}
|
|
}
|
|
|
|
func TestValidateRequiresClashAPISecretWhenEnabled(t *testing.T) {
|
|
cfg := DefaultConfig()
|
|
cfg.Proxy.APIPort = 9090
|
|
if err := cfg.Validate(); err == nil {
|
|
t.Fatalf("api_port without api_secret should be rejected")
|
|
}
|
|
cfg.Proxy.APISecret = "secret"
|
|
if err := cfg.Validate(); err != nil {
|
|
t.Fatalf("api_port with api_secret should validate: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestWriteFileAtomicReplacesFileWithoutLeavingTemp(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "config.json")
|
|
if err := os.WriteFile(path, []byte("old"), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if err := writeFileAtomic(path, []byte("new\n"), 0600, "config"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(data) != "new\n" {
|
|
t.Fatalf("atomic replacement wrote %q", string(data))
|
|
}
|
|
if _, err := os.Stat(path + ".tmp"); !os.IsNotExist(err) {
|
|
t.Fatalf("temporary file should be gone after atomic write, stat err=%v", err)
|
|
}
|
|
info, err := os.Stat(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if info.Mode().Perm() != 0600 {
|
|
t.Fatalf("atomic replacement mode = %o, want 0600", info.Mode().Perm())
|
|
}
|
|
}
|
|
|
|
func TestLoadRejectsRemovedTopLevelNodeAndTransport(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "config.json")
|
|
raw := []byte(`{
|
|
"schema_version": 5,
|
|
"proxy": {"mode":"tproxy","tproxy_port":10853,"dns_port":10856,"gid":23333,"mark":8227,"api_port":0},
|
|
"transport": {"protocol":"reality","tls_server":"","fingerprint":"chrome"},
|
|
"node": [],
|
|
"routing": {"mode":"whitelist","bypass_lan":true,"bypass_china":false,"bypass_russia":false,"block_ads":false,"custom_direct":[],"custom_proxy":[],"custom_block":[]},
|
|
"apps": {"mode":"whitelist","list":[],"app_groups":{}},
|
|
"dns": {"hijack_per_uid":true,"proxy_dns":"https://1.1.1.1/dns-query","direct_dns":"https://dns.google/dns-query","bootstrap_ip":"1.1.1.1","fake_ip":false},
|
|
"ipv6": {"mode":"mirror"},
|
|
"sharing": {"enabled":false},
|
|
"health": {"enabled":true,"interval_sec":30,"threshold":3,"check_url":"https://www.gstatic.com/generate_204","timeout_sec":5,"dns_is_hard_readiness":false},
|
|
"autostart": false
|
|
}`)
|
|
if err := os.WriteFile(path, raw, 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if _, err := Load(path); err == nil || !strings.Contains(err.Error(), "transport") {
|
|
t.Fatalf("removed top-level transport/node fields must be rejected, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestLoadRejectsRemovedBlockQuicDNSField(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "config.json")
|
|
raw := []byte(`{
|
|
"schema_version": 5,
|
|
"proxy": {"mode":"tproxy","tproxy_port":10853,"dns_port":10856,"gid":23333,"mark":8227,"api_port":0},
|
|
"routing": {"mode":"whitelist","bypass_lan":true,"bypass_china":false,"bypass_russia":false,"block_ads":false,"custom_direct":[],"custom_proxy":[],"custom_block":[]},
|
|
"apps": {"mode":"whitelist","list":[],"app_groups":{}},
|
|
"dns": {"hijack_per_uid":true,"proxy_dns":"https://1.1.1.1/dns-query","direct_dns":"https://dns.google/dns-query","bootstrap_ip":"1.1.1.1","block_quic_dns":true,"fake_ip":false},
|
|
"ipv6": {"mode":"mirror"},
|
|
"sharing": {"enabled":false},
|
|
"health": {"enabled":true,"interval_sec":30,"threshold":3,"check_url":"https://www.gstatic.com/generate_204","timeout_sec":5,"dns_is_hard_readiness":false},
|
|
"autostart": false
|
|
}`)
|
|
if err := os.WriteFile(path, raw, 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if _, err := Load(path); err == nil || !strings.Contains(err.Error(), "block_quic_dns") {
|
|
t.Fatalf("removed block_quic_dns field should be rejected, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestLoadRejectsRemovedProfileProjectionPayloads(t *testing.T) {
|
|
for _, field := range []string{"tun", "extra", "subscriptions"} {
|
|
t.Run(field, func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "config.json")
|
|
raw := []byte(`{
|
|
"schema_version": 5,
|
|
"proxy": {"mode":"tproxy","tproxy_port":10853,"dns_port":10856,"gid":23333,"mark":8227,"api_port":0},
|
|
"profile": {"id":"default","name":"Default","` + field + `":{}},
|
|
"routing": {"mode":"whitelist","bypass_lan":true,"bypass_china":false,"bypass_russia":false,"block_ads":false,"custom_direct":[],"custom_proxy":[],"custom_block":[]},
|
|
"apps": {"mode":"whitelist","list":[],"app_groups":{}},
|
|
"dns": {"hijack_per_uid":true,"proxy_dns":"https://1.1.1.1/dns-query","direct_dns":"https://dns.google/dns-query","bootstrap_ip":"1.1.1.1","fake_ip":false},
|
|
"ipv6": {"mode":"mirror"},
|
|
"sharing": {"enabled":false},
|
|
"health": {"enabled":true,"interval_sec":30,"threshold":3,"check_url":"https://www.gstatic.com/generate_204","timeout_sec":5,"dns_is_hard_readiness":false},
|
|
"autostart": false
|
|
}`)
|
|
if err := os.WriteFile(path, raw, 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if _, err := Load(path); err == nil || !strings.Contains(err.Error(), field) {
|
|
t.Fatalf("removed profile.%s field should be rejected, got %v", field, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLoadRejectsApkOnlyProfileNodeMetadata(t *testing.T) {
|
|
for _, field := range []string{"source", "createdAt", "latencyMs", "responseMs", "throughputBps", "testStatus"} {
|
|
t.Run(field, func(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "config.json")
|
|
raw := []byte(`{
|
|
"schema_version": 5,
|
|
"proxy": {"mode":"tproxy","tproxy_port":10853,"dns_port":10856,"gid":23333,"mark":8227,"api_port":0},
|
|
"profile": {
|
|
"id":"default",
|
|
"name":"Default",
|
|
"nodes":[{"id":"node-1","name":"Node","protocol":"vless","server":"example.com","port":443,"outbound":{}, "` + field + `": "removed"}]
|
|
},
|
|
"routing": {"mode":"whitelist","bypass_lan":true,"bypass_china":false,"bypass_russia":false,"block_ads":false,"custom_direct":[],"custom_proxy":[],"custom_block":[]},
|
|
"apps": {"mode":"whitelist","list":[],"app_groups":{}},
|
|
"dns": {"hijack_per_uid":true,"proxy_dns":"https://1.1.1.1/dns-query","direct_dns":"https://dns.google/dns-query","bootstrap_ip":"1.1.1.1","fake_ip":false},
|
|
"ipv6": {"mode":"mirror"},
|
|
"sharing": {"enabled":false},
|
|
"health": {"enabled":true,"interval_sec":30,"threshold":3,"check_url":"https://www.gstatic.com/generate_204","timeout_sec":5,"dns_is_hard_readiness":false},
|
|
"autostart": false
|
|
}`)
|
|
if err := os.WriteFile(path, raw, 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if _, err := Load(path); err == nil || !strings.Contains(err.Error(), field) {
|
|
t.Fatalf("APK-only profile node field %s should be rejected, got %v", field, err)
|
|
}
|
|
})
|
|
}
|
|
}
|