mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-24 06:45:01 +03:00
c3394fefed
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.
163 lines
5.2 KiB
Go
163 lines
5.2 KiB
Go
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())
|
|
}
|
|
})
|
|
}
|
|
}
|