mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-24 14:55:02 +03:00
feat(sub): opt-in dns and sniffer from remote Clash rules (#6868)
Public Mihomo templates ship rules, rule-providers and proxy-groups together with a dns block those rules depend on (fake-ip ranges, nameserver policy). Pasting a template inline keeps its dns, but pointing the global routing rules at its URL dropped it, so URL users had to give up auto-refresh to keep it. A new switch, subClashRemoteDNS (off by default), also takes the remote dns and sniffer maps. It is opt-in because a remote dns block decides where clients send every lookup. A malformed dns or sniffer value is skipped, not the whole template. The component-test teardown now outlasts AntD Form's 10ms error debounce, which otherwise fired after jsdom was gone.
This commit is contained in:
@@ -39,7 +39,7 @@ func TestClashExternalShadowsocksMatchesInboundPath(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
svc := NewSubClashService(false, "", &SubService{})
|
||||
svc := NewSubClashService(false, "", false, &SubService{})
|
||||
client := model.Client{Password: "clientpw", Email: "user"}
|
||||
|
||||
for _, tc := range tests {
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
const clashExternalVlessLink = "vless://22222222-2222-4222-8222-222222222222@198.51.100.9:443?type=tcp&security=reality&sni=example.com&pbk=test-public-key&sid=ab12&fp=chrome&flow=xtls-rprx-vision"
|
||||
|
||||
func TestClashExternalVlessEncryption(t *testing.T) {
|
||||
svc := NewSubClashService(false, "", &SubService{})
|
||||
svc := NewSubClashService(false, "", false, &SubService{})
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
query string
|
||||
|
||||
@@ -56,7 +56,7 @@ func TestSubClash_InfoNode_Active(t *testing.T) {
|
||||
|
||||
sub := NewSubService("{{EMAIL}}|📊{{TRAFFIC_LEFT}}")
|
||||
sub.subInfoNodeEnable = true
|
||||
clash := NewSubClashService(false, "", sub)
|
||||
clash := NewSubClashService(false, "", false, sub)
|
||||
|
||||
out, _, err := clash.GetClash("sub-clash", "sub.example.com")
|
||||
if err != nil {
|
||||
@@ -115,7 +115,7 @@ func TestSubClash_InfoNode_Expired(t *testing.T) {
|
||||
sub := NewSubService("{{INBOUND}}")
|
||||
sub.subInfoNodeEnable = true
|
||||
sub.subExpiredTemplate = service.DefaultSubExpiredTemplate
|
||||
clash := NewSubClashService(false, "", sub)
|
||||
clash := NewSubClashService(false, "", false, sub)
|
||||
|
||||
out, _, err := clash.GetClash("sub-clash-exp", "sub.example.com")
|
||||
if err != nil {
|
||||
@@ -178,7 +178,7 @@ func TestSubClash_InfoNode_Depleted(t *testing.T) {
|
||||
sub := NewSubService("{{INBOUND}}")
|
||||
sub.subInfoNodeEnable = true
|
||||
sub.subTrafficDepletedTemplate = service.DefaultSubTrafficDepletedTemplate
|
||||
clash := NewSubClashService(false, "", sub)
|
||||
clash := NewSubClashService(false, "", false, sub)
|
||||
|
||||
out, _, err := clash.GetClash("sub-clash-dep", "sub.example.com")
|
||||
if err != nil {
|
||||
@@ -225,7 +225,7 @@ func TestSubClash_InfoNode_ProxyGroupOrder_DoesNotDefaultToDummy(t *testing.T) {
|
||||
|
||||
sub := NewSubService("{{EMAIL}}|📊{{TRAFFIC_LEFT}}|{{STATUS}}")
|
||||
sub.subInfoNodeEnable = true
|
||||
clash := NewSubClashService(false, "", sub)
|
||||
clash := NewSubClashService(false, "", false, sub)
|
||||
|
||||
out, _, err := clash.GetClash("sub-clash", "sub.example.com")
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
package sub
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
yaml "github.com/goccy/go-yaml"
|
||||
)
|
||||
|
||||
const remoteClashTemplateWithDNS = `
|
||||
dns:
|
||||
enable: true
|
||||
nameserver:
|
||||
- https://8.8.8.8/dns-query
|
||||
sniffer:
|
||||
enable: true
|
||||
rules:
|
||||
- DOMAIN,remote.example,DIRECT
|
||||
- MATCH,PROXY
|
||||
`
|
||||
|
||||
var remoteClashTemplateDNS = map[string]any{
|
||||
"enable": true,
|
||||
"nameserver": []any{"https://8.8.8.8/dns-query"},
|
||||
}
|
||||
|
||||
func remoteClashTestBase() map[string]any {
|
||||
return map[string]any{
|
||||
"proxies": []map[string]any{{"name": "vpn-node", "type": "vless"}},
|
||||
"proxy-groups": []map[string]any{{
|
||||
"name": "PROXY", "type": "select", "proxies": []string{"vpn-node", "DIRECT"},
|
||||
}},
|
||||
"rules": []string{"MATCH,PROXY"},
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeRemoteClashRulesClientConfigIsOptIn(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
allowClientConfig bool
|
||||
wantDNS any
|
||||
wantSniffer any
|
||||
}{
|
||||
{name: "off drops dns and sniffer", allowClientConfig: false},
|
||||
{
|
||||
name: "on copies dns and sniffer",
|
||||
allowClientConfig: true,
|
||||
wantDNS: remoteClashTemplateDNS,
|
||||
wantSniffer: map[string]any{"enable": true},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
base := remoteClashTestBase()
|
||||
if err := mergeRemoteClashRulesYAML(base, remoteClashTemplateWithDNS, tt.allowClientConfig); err != nil {
|
||||
t.Fatalf("merge: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(base["dns"], tt.wantDNS) {
|
||||
t.Fatalf("dns = %#v, want %#v", base["dns"], tt.wantDNS)
|
||||
}
|
||||
if rules, _ := asAnySlice(base["rules"]); len(rules) == 0 || rules[0] != "DOMAIN,remote.example,DIRECT" {
|
||||
t.Fatalf("rules = %#v, want remote rules applied", base["rules"])
|
||||
}
|
||||
if !reflect.DeepEqual(base["sniffer"], tt.wantSniffer) {
|
||||
t.Fatalf("sniffer = %#v, want %#v", base["sniffer"], tt.wantSniffer)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoteClashMalformedClientConfigIsSkipped(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
remote string
|
||||
}{
|
||||
{name: "scalar dns", remote: "dns: 8.8.8.8\nrules:\n - DOMAIN,remote.example,DIRECT\n - MATCH,PROXY\n"},
|
||||
{name: "list dns", remote: "dns:\n - 8.8.8.8\nrules:\n - DOMAIN,remote.example,DIRECT\n - MATCH,PROXY\n"},
|
||||
{name: "null dns", remote: "dns:\nrules:\n - DOMAIN,remote.example,DIRECT\n - MATCH,PROXY\n"},
|
||||
{name: "scalar sniffer", remote: "sniffer: true\nrules:\n - DOMAIN,remote.example,DIRECT\n - MATCH,PROXY\n"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if _, _, err := normalizeClashRouting([]byte(tt.remote)); err != nil {
|
||||
t.Fatalf("fetch-time err=%v, want nil", err)
|
||||
}
|
||||
base := remoteClashTestBase()
|
||||
if err := mergeRemoteClashRulesYAML(base, tt.remote, true); err != nil {
|
||||
t.Fatalf("merge err=%v, want nil", err)
|
||||
}
|
||||
if _, ok := base["dns"]; ok {
|
||||
t.Fatalf("dns = %#v, want absent", base["dns"])
|
||||
}
|
||||
if _, ok := base["sniffer"]; ok {
|
||||
t.Fatalf("sniffer = %#v, want absent", base["sniffer"])
|
||||
}
|
||||
if rules, _ := asAnySlice(base["rules"]); len(rules) == 0 || rules[0] != "DOMAIN,remote.example,DIRECT" {
|
||||
t.Fatalf("rules = %#v, want remote rules applied", base["rules"])
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeClashRoutingRequiresRouteGraphKey(t *testing.T) {
|
||||
_, _, err := normalizeClashRouting([]byte("dns:\n enable: true\nsniffer:\n enable: true\n"))
|
||||
if err == nil || err.Error() != "Clash routing response has no supported routing keys" {
|
||||
t.Fatalf("err=%v, want no supported routing keys", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClashSubscriptionRemoteDNSFollowsSetting(t *testing.T) {
|
||||
seedSubDB(t)
|
||||
seedSubInbound(t, "s1", "rdns", 4601, 1, wsTLSStream)
|
||||
gin.SetMode(gin.TestMode)
|
||||
|
||||
oldResolver := routingSourceResolver
|
||||
t.Cleanup(func() { routingSourceResolver = oldResolver })
|
||||
routingSourceResolver = newRemoteRoutingResolver(remoteRoutingTestClient(func(*http.Request) (*http.Response, error) {
|
||||
return remoteRoutingResponse(http.StatusOK, remoteClashTemplateWithDNS), nil
|
||||
}), false)
|
||||
const source = "https://example.com/mihomo/default.yaml"
|
||||
primeRemoteRouting(t, routingSourceResolver, remoteRoutingClash, source)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
remoteDNS bool
|
||||
wantDNS any
|
||||
}{
|
||||
{name: "off keeps client dns", remoteDNS: false},
|
||||
{name: "on takes remote dns", remoteDNS: true, wantDNS: remoteClashTemplateDNS},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
router := gin.New()
|
||||
NewSUBController(
|
||||
router.Group("/"),
|
||||
WithSUBClashEnabled(true),
|
||||
WithSUBClashEnableRouting(true),
|
||||
WithSUBClashRules(source),
|
||||
WithSUBClashRemoteDNS(tt.remoteDNS),
|
||||
)
|
||||
resp := httptest.NewRecorder()
|
||||
router.ServeHTTP(resp, httptest.NewRequest(http.MethodGet, "http://sub.example.com/clash/s1", nil))
|
||||
if resp.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want 200; body=%s", resp.Code, resp.Body.String())
|
||||
}
|
||||
var config map[string]any
|
||||
if err := yaml.Unmarshal(resp.Body.Bytes(), &config); err != nil {
|
||||
t.Fatalf("decode clash yaml: %v\n%s", err, resp.Body.String())
|
||||
}
|
||||
if !reflect.DeepEqual(config["dns"], tt.wantDNS) {
|
||||
t.Fatalf("dns = %#v, want %#v\n%s", config["dns"], tt.wantDNS, resp.Body.String())
|
||||
}
|
||||
if !strings.Contains(resp.Body.String(), "DOMAIN,remote.example,DIRECT") {
|
||||
t.Fatalf("remote rules were not applied:\n%s", resp.Body.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -20,15 +20,16 @@ import (
|
||||
)
|
||||
|
||||
type SubClashService struct {
|
||||
enableRouting bool
|
||||
clashRules string
|
||||
SubService *SubService
|
||||
enableRouting bool
|
||||
clashRules string
|
||||
clashRemoteDNS bool
|
||||
SubService *SubService
|
||||
}
|
||||
|
||||
var errNoLegacyClashProxies = errors.New("no Clash for Windows-compatible proxies found; use the Mihomo subscription for modern proxy types")
|
||||
|
||||
func NewSubClashService(enableRouting bool, clashRules string, subService *SubService) *SubClashService {
|
||||
return &SubClashService{enableRouting: enableRouting, clashRules: clashRules, SubService: subService}
|
||||
func NewSubClashService(enableRouting bool, clashRules string, clashRemoteDNS bool, subService *SubService) *SubClashService {
|
||||
return &SubClashService{enableRouting: enableRouting, clashRules: clashRules, clashRemoteDNS: clashRemoteDNS, SubService: subService}
|
||||
}
|
||||
|
||||
func (s *SubClashService) GetClash(subId string, host string) (string, string, error) {
|
||||
@@ -170,7 +171,7 @@ func (s *SubClashService) getClash(subId string, host string, legacy bool) (stri
|
||||
resolved, remoteDocument, remote, resolveErr := resolveClashRoutingSource(s.clashRules)
|
||||
if resolveErr == nil && strings.TrimSpace(resolved) != "" {
|
||||
if remote {
|
||||
if err := mergeRemoteClashRules(config, remoteDocument); err != nil {
|
||||
if err := mergeRemoteClashRules(config, remoteDocument, s.clashRemoteDNS); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
} else if err := mergeClashRulesYAML(config, resolved); err != nil {
|
||||
@@ -1296,17 +1297,51 @@ func mergeClashRulesYAML(base map[string]any, raw string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// mergeRemoteClashRules lets remote update only the route graph (see
|
||||
// remoteClashAllowedKey) and never mutates remote: cached documents are shared.
|
||||
func mergeRemoteClashRules(base map[string]any, remote map[string]any) error {
|
||||
type remoteClashKeyClass int
|
||||
|
||||
const (
|
||||
remoteClashRouteGraph remoteClashKeyClass = iota + 1
|
||||
remoteClashClientConfig
|
||||
)
|
||||
|
||||
// Client config keys steer the client's own resolver, so a remote URL may set
|
||||
// them only when the admin opts in (subClashRemoteDNS).
|
||||
var remoteClashKeyClasses = map[string]remoteClashKeyClass{
|
||||
"proxy-groups": remoteClashRouteGraph,
|
||||
"rule-providers": remoteClashRouteGraph,
|
||||
"rules": remoteClashRouteGraph,
|
||||
"dns": remoteClashClientConfig,
|
||||
"sniffer": remoteClashClientConfig,
|
||||
}
|
||||
|
||||
func remoteClashKeyAllowed(key string, allowClientConfig bool) bool {
|
||||
switch remoteClashKeyClasses[key] {
|
||||
case remoteClashRouteGraph:
|
||||
return true
|
||||
case remoteClashClientConfig:
|
||||
return allowClientConfig
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// mergeRemoteClashRules copies only keys listed in remoteClashKeyClasses and
|
||||
// never mutates remote: cached documents are shared.
|
||||
func mergeRemoteClashRules(base map[string]any, remote map[string]any, allowClientConfig bool) error {
|
||||
if len(remote) == 0 {
|
||||
return fmt.Errorf("remote Clash routing source must be a YAML map")
|
||||
}
|
||||
|
||||
for key, value := range remote {
|
||||
if !remoteClashAllowedKey(key) {
|
||||
if !remoteClashKeyAllowed(key, allowClientConfig) {
|
||||
continue
|
||||
}
|
||||
if remoteClashKeyClasses[key] == remoteClashClientConfig {
|
||||
// A malformed dns or sniffer must not cost the client its routing.
|
||||
if _, ok := value.(map[string]any); !ok {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if err := validateRemoteClashValue(key, value); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1382,15 +1417,6 @@ func validateRemoteClashValue(key string, value any) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func remoteClashAllowedKey(key string) bool {
|
||||
switch key {
|
||||
case "proxy-groups", "rule-providers", "rules":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func validateClashRouteGraph(config map[string]any) error {
|
||||
known := map[string]struct{}{
|
||||
"DIRECT": {}, "REJECT": {}, "REJECT-DROP": {}, "REJECT-TINYGIF": {}, "PASS": {}, "GLOBAL": {},
|
||||
|
||||
@@ -167,7 +167,7 @@ func TestBuildProxy_VLESSRealityFieldsForClash(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestClashRealityMLKEMAcrossSources(t *testing.T) {
|
||||
svc := NewSubClashService(false, "", &SubService{})
|
||||
svc := NewSubClashService(false, "", false, &SubService{})
|
||||
for _, security := range []string{"reality", "tls", "none"} {
|
||||
for _, fingerprint := range []string{"", "chrome", "firefox"} {
|
||||
t.Run(security+"/"+fingerprint, func(t *testing.T) {
|
||||
|
||||
@@ -115,6 +115,7 @@ type subControllerConfig struct {
|
||||
subJsonObservatory string
|
||||
subClashEnableRouting bool
|
||||
subClashRules string
|
||||
subClashRemoteDNS bool
|
||||
|
||||
subTitle string
|
||||
subSupportURL string
|
||||
@@ -217,6 +218,10 @@ func WithSUBClashRules(value string) SUBControllerOption {
|
||||
return func(config *subControllerConfig) { config.subClashRules = value }
|
||||
}
|
||||
|
||||
func WithSUBClashRemoteDNS(value bool) SUBControllerOption {
|
||||
return func(config *subControllerConfig) { config.subClashRemoteDNS = value }
|
||||
}
|
||||
|
||||
func WithSUBTitle(value string) SUBControllerOption {
|
||||
return func(config *subControllerConfig) { config.subTitle = value }
|
||||
}
|
||||
@@ -319,7 +324,7 @@ func NewSUBController(g *gin.RouterGroup, options ...SUBControllerOption) *SUBCo
|
||||
|
||||
subService: sub,
|
||||
subJsonService: subJsonSvc,
|
||||
subClashService: NewSubClashService(config.subClashEnableRouting, config.subClashRules, sub),
|
||||
subClashService: NewSubClashService(config.subClashEnableRouting, config.subClashRules, config.subClashRemoteDNS, sub),
|
||||
|
||||
subTemplateCache: map[string]*cachedSubTemplate{},
|
||||
}
|
||||
|
||||
@@ -155,7 +155,7 @@ func TestLinkDisplayName(t *testing.T) {
|
||||
|
||||
func TestClashProxyFromExternalTrojanReality(t *testing.T) {
|
||||
link := "trojan://provider-pass@37.27.201.56:8443?type=tcp&security=reality&sni=aws.amazon.com&pbk=PBK&sid=298b44&fp=chrome#srv"
|
||||
svc := NewSubClashService(false, "", NewSubService(""))
|
||||
svc := NewSubClashService(false, "", false, NewSubService(""))
|
||||
proxy := svc.clashProxyFromExternal(link, "DE-Provider")
|
||||
if proxy == nil {
|
||||
t.Fatal("expected a clash proxy, got nil")
|
||||
|
||||
@@ -56,7 +56,7 @@ func TestJsonAndClashServeExternalLinkOnlySub(t *testing.T) {
|
||||
t.Fatalf("standards-compliant GetJson profile count = %d, want 1", len(configs))
|
||||
}
|
||||
|
||||
clashOut, _, err := NewSubClashService(false, "", base).GetClash("ext-only", "sub.example.com")
|
||||
clashOut, _, err := NewSubClashService(false, "", false, base).GetClash("ext-only", "sub.example.com")
|
||||
if err != nil {
|
||||
t.Fatalf("GetClash err = %v", err)
|
||||
}
|
||||
|
||||
@@ -260,7 +260,7 @@ func TestSub_HostAllowInsecure(t *testing.T) {
|
||||
t.Fatalf("raw link should carry allowInsecure=1: %s", strings.Join(links, "\n"))
|
||||
}
|
||||
|
||||
clash := NewSubClashService(false, "", NewSubService(""))
|
||||
clash := NewSubClashService(false, "", false, NewSubService(""))
|
||||
yaml, _, err := clash.GetClash("s1", "req.example.com")
|
||||
if err != nil {
|
||||
t.Fatalf("GetClash: %v", err)
|
||||
@@ -281,7 +281,7 @@ func TestSub_HostHeaderReachesClashAndJson(t *testing.T) {
|
||||
HostHeader: "cdn.example.com", Path: "/ws-path",
|
||||
})
|
||||
|
||||
clash := NewSubClashService(false, "", NewSubService(""))
|
||||
clash := NewSubClashService(false, "", false, NewSubService(""))
|
||||
yaml, _, err := clash.GetClash("s1", "req.example.com")
|
||||
if err != nil {
|
||||
t.Fatalf("GetClash: %v", err)
|
||||
@@ -507,7 +507,7 @@ func TestSub_HostRealitySniJSONAndClash(t *testing.T) {
|
||||
t.Fatalf("server-side serverNames %v leaked into the json reality client:\n%s", names, out)
|
||||
}
|
||||
|
||||
yaml, _, err := NewSubClashService(false, "", NewSubService("")).GetClash("s1", "req.example.com")
|
||||
yaml, _, err := NewSubClashService(false, "", false, NewSubService("")).GetClash("s1", "req.example.com")
|
||||
if err != nil {
|
||||
t.Fatalf("GetClash: %v", err)
|
||||
}
|
||||
@@ -551,7 +551,7 @@ func TestSub_ExcludeFromSubTypes(t *testing.T) {
|
||||
t.Fatalf("host not excluded from raw should appear in GetSubs")
|
||||
}
|
||||
|
||||
clash := NewSubClashService(false, "", NewSubService(""))
|
||||
clash := NewSubClashService(false, "", false, NewSubService(""))
|
||||
yaml, _, err := clash.GetClash("s1", "req.example.com")
|
||||
if err != nil {
|
||||
t.Fatalf("GetClash: %v", err)
|
||||
|
||||
@@ -51,7 +51,7 @@ func TestSub_JSONStripsFlowOnUnsupportedTransport(t *testing.T) {
|
||||
t.Fatalf("raw link must not carry a flow on ws+tls: %s", joined)
|
||||
}
|
||||
|
||||
clash := NewSubClashService(false, "", NewSubService(""))
|
||||
clash := NewSubClashService(false, "", false, NewSubService(""))
|
||||
yaml, _, err := clash.GetClash("s1", "req.example.com")
|
||||
if err != nil {
|
||||
t.Fatalf("GetClash: %v", err)
|
||||
|
||||
@@ -450,7 +450,7 @@ func normalizeClashRouting(body []byte) (string, map[string]any, error) {
|
||||
}
|
||||
hasSupportedKey := false
|
||||
for key := range document {
|
||||
if remoteClashAllowedKey(key) {
|
||||
if remoteClashKeyClasses[key] == remoteClashRouteGraph {
|
||||
hasSupportedKey = true
|
||||
break
|
||||
}
|
||||
@@ -465,7 +465,7 @@ func normalizeClashRouting(body []byte) (string, map[string]any, error) {
|
||||
}},
|
||||
"rules": []string{"MATCH,PROXY"},
|
||||
}
|
||||
if err := mergeRemoteClashRules(base, document); err != nil {
|
||||
if err := mergeRemoteClashRules(base, document, false); err != nil {
|
||||
return "", nil, fmt.Errorf("invalid remote Clash routing schema: %w", err)
|
||||
}
|
||||
return text, document, nil
|
||||
|
||||
@@ -20,12 +20,12 @@ import (
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/database/dbtest"
|
||||
)
|
||||
|
||||
func mergeRemoteClashRulesYAML(base map[string]any, raw string) error {
|
||||
func mergeRemoteClashRulesYAML(base map[string]any, raw string, allowClientConfig bool) error {
|
||||
var remote map[string]any
|
||||
if err := yaml.Unmarshal([]byte(strings.TrimSpace(raw)), &remote); err != nil {
|
||||
return err
|
||||
}
|
||||
return mergeRemoteClashRules(base, remote)
|
||||
return mergeRemoteClashRules(base, remote, allowClientConfig)
|
||||
}
|
||||
|
||||
type remoteRoutingRoundTripper func(*http.Request) (*http.Response, error)
|
||||
@@ -475,7 +475,7 @@ rules:
|
||||
- RULE-SET,roscom,PROXY
|
||||
- MATCH,PROXY
|
||||
`
|
||||
if err := mergeRemoteClashRulesYAML(base, remote); err != nil {
|
||||
if err := mergeRemoteClashRulesYAML(base, remote, false); err != nil {
|
||||
t.Fatalf("merge: %v", err)
|
||||
}
|
||||
proxies, ok := base["proxies"].([]map[string]any)
|
||||
@@ -520,7 +520,7 @@ func TestMergeRemoteClashRulesKeepsBaseProxyGroupWhenRemoteOmitsIt(t *testing.T)
|
||||
proxies: [PROXY]
|
||||
rules:
|
||||
- MATCH,PROXY
|
||||
`); err != nil {
|
||||
`, false); err != nil {
|
||||
t.Fatalf("merge: %v", err)
|
||||
}
|
||||
groups, ok := asAnySlice(base["proxy-groups"])
|
||||
@@ -692,7 +692,7 @@ func TestRemoteClashRouteGraphValidation(t *testing.T) {
|
||||
}},
|
||||
"rules": []string{"MATCH,PROXY"},
|
||||
}
|
||||
err := mergeRemoteClashRulesYAML(base, tt.remote)
|
||||
err := mergeRemoteClashRulesYAML(base, tt.remote, false)
|
||||
if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
|
||||
t.Fatalf("err=%v, want %q", err, tt.wantErr)
|
||||
}
|
||||
@@ -741,7 +741,7 @@ rules:
|
||||
}},
|
||||
"rules": []string{"MATCH,PROXY"},
|
||||
}
|
||||
if err := mergeRemoteClashRules(base, entry.Clash); err != nil {
|
||||
if err := mergeRemoteClashRules(base, entry.Clash, false); err != nil {
|
||||
t.Fatalf("merge cached document: %v", err)
|
||||
}
|
||||
if requests.Load() != 1 {
|
||||
|
||||
@@ -54,7 +54,7 @@ func TestExcludedInboundHidesLinksButKeepsUsage(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("clash", func(t *testing.T) {
|
||||
out, header, err := NewSubClashService(false, "", NewSubService("")).GetClash("sub-excl", "req.example.com")
|
||||
out, header, err := NewSubClashService(false, "", false, NewSubService("")).GetClash("sub-excl", "req.example.com")
|
||||
if err != nil {
|
||||
t.Fatalf("GetClash: %v", err)
|
||||
}
|
||||
|
||||
@@ -180,6 +180,11 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
||||
SubClashRules = ""
|
||||
}
|
||||
|
||||
SubClashRemoteDNS, err := s.settingService.GetSubClashRemoteDNS()
|
||||
if err != nil {
|
||||
SubClashRemoteDNS = false
|
||||
}
|
||||
|
||||
SubTitle, err := s.settingService.GetSubTitle()
|
||||
if err != nil {
|
||||
SubTitle = ""
|
||||
@@ -361,6 +366,7 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
||||
WithSUBJsonObservatory(SubJsonObservatory),
|
||||
WithSUBClashEnableRouting(SubClashEnableRouting),
|
||||
WithSUBClashRules(SubClashRules),
|
||||
WithSUBClashRemoteDNS(SubClashRemoteDNS),
|
||||
WithSUBTitle(SubTitle),
|
||||
WithSUBSupportURL(SubSupportUrl),
|
||||
WithSUBProfileURL(SubProfileUrl),
|
||||
|
||||
@@ -99,7 +99,7 @@ func TestGetClashEmitsPinnedCertSha256(t *testing.T) {
|
||||
stream := `{"network":"tcp","security":"tls","tlsSettings":{"serverName":"pin.sni","settings":{"pinnedPeerCertSha256":["` + pin + `"]}}}`
|
||||
seedSubInbound(t, "pin1", "pin", 46300, 1, stream)
|
||||
|
||||
out, _, err := NewSubClashService(false, "", NewSubService("")).GetClash("pin1", "sub.example.com")
|
||||
out, _, err := NewSubClashService(false, "", false, NewSubService("")).GetClash("pin1", "sub.example.com")
|
||||
if err != nil {
|
||||
t.Fatalf("GetClash: %v", err)
|
||||
}
|
||||
@@ -122,7 +122,7 @@ func TestJsonAndClashTolerateExternalProxyMissingPort(t *testing.T) {
|
||||
t.Fatal("GetJson returned empty for an externalProxy entry missing port")
|
||||
}
|
||||
|
||||
clashOut, _, err := NewSubClashService(false, "", NewSubService("")).GetClash("extp1", "sub.example.com")
|
||||
clashOut, _, err := NewSubClashService(false, "", false, NewSubService("")).GetClash("extp1", "sub.example.com")
|
||||
if err != nil {
|
||||
t.Fatalf("GetClash: %v", err)
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ func TestSub_HostVlessRoute_Clash(t *testing.T) {
|
||||
ib := seedSubInbound(t, "s1", "vrc", 4502, 1, wsTLSStream)
|
||||
seedHost(t, &model.Host{InboundId: ib.Id, SortOrder: 1, Remark: "C", Address: "c.cdn.com", Port: 8443, Security: "tls", VlessRoute: "443"})
|
||||
|
||||
clash := NewSubClashService(false, "", NewSubService(""))
|
||||
clash := NewSubClashService(false, "", false, NewSubService(""))
|
||||
yaml, _, err := clash.GetClash("s1", "req.example.com")
|
||||
if err != nil {
|
||||
t.Fatalf("GetClash: %v", err)
|
||||
|
||||
Reference in New Issue
Block a user