mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-06 13:15:01 +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:
@@ -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": {},
|
||||
|
||||
Reference in New Issue
Block a user