mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-01 10:45:00 +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:
@@ -242,6 +242,9 @@
|
|||||||
"subClashPath": {
|
"subClashPath": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"subClashRemoteDNS": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"subClashRules": {
|
"subClashRules": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@@ -662,6 +665,7 @@
|
|||||||
"subClashEnable",
|
"subClashEnable",
|
||||||
"subClashEnableRouting",
|
"subClashEnableRouting",
|
||||||
"subClashPath",
|
"subClashPath",
|
||||||
|
"subClashRemoteDNS",
|
||||||
"subClashRules",
|
"subClashRules",
|
||||||
"subClashURI",
|
"subClashURI",
|
||||||
"subClashUserAgentRegex",
|
"subClashUserAgentRegex",
|
||||||
@@ -1018,6 +1022,9 @@
|
|||||||
"subClashPath": {
|
"subClashPath": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"subClashRemoteDNS": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"subClashRules": {
|
"subClashRules": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@@ -1446,6 +1453,7 @@
|
|||||||
"subClashEnable",
|
"subClashEnable",
|
||||||
"subClashEnableRouting",
|
"subClashEnableRouting",
|
||||||
"subClashPath",
|
"subClashPath",
|
||||||
|
"subClashRemoteDNS",
|
||||||
"subClashRules",
|
"subClashRules",
|
||||||
"subClashURI",
|
"subClashURI",
|
||||||
"subClashUserAgentRegex",
|
"subClashUserAgentRegex",
|
||||||
|
|||||||
@@ -242,6 +242,9 @@
|
|||||||
"subClashPath": {
|
"subClashPath": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"subClashRemoteDNS": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"subClashRules": {
|
"subClashRules": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@@ -662,6 +665,7 @@
|
|||||||
"subClashEnable",
|
"subClashEnable",
|
||||||
"subClashEnableRouting",
|
"subClashEnableRouting",
|
||||||
"subClashPath",
|
"subClashPath",
|
||||||
|
"subClashRemoteDNS",
|
||||||
"subClashRules",
|
"subClashRules",
|
||||||
"subClashURI",
|
"subClashURI",
|
||||||
"subClashUserAgentRegex",
|
"subClashUserAgentRegex",
|
||||||
@@ -1018,6 +1022,9 @@
|
|||||||
"subClashPath": {
|
"subClashPath": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"subClashRemoteDNS": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"subClashRules": {
|
"subClashRules": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@@ -1446,6 +1453,7 @@
|
|||||||
"subClashEnable",
|
"subClashEnable",
|
||||||
"subClashEnableRouting",
|
"subClashEnableRouting",
|
||||||
"subClashPath",
|
"subClashPath",
|
||||||
|
"subClashRemoteDNS",
|
||||||
"subClashRules",
|
"subClashRules",
|
||||||
"subClashURI",
|
"subClashURI",
|
||||||
"subClashUserAgentRegex",
|
"subClashUserAgentRegex",
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ export const EXAMPLES: Record<string, unknown> = {
|
|||||||
"subClashEnable": false,
|
"subClashEnable": false,
|
||||||
"subClashEnableRouting": false,
|
"subClashEnableRouting": false,
|
||||||
"subClashPath": "",
|
"subClashPath": "",
|
||||||
|
"subClashRemoteDNS": false,
|
||||||
"subClashRules": "",
|
"subClashRules": "",
|
||||||
"subClashURI": "",
|
"subClashURI": "",
|
||||||
"subClashUserAgentRegex": "",
|
"subClashUserAgentRegex": "",
|
||||||
@@ -252,6 +253,7 @@ export const EXAMPLES: Record<string, unknown> = {
|
|||||||
"subClashEnable": false,
|
"subClashEnable": false,
|
||||||
"subClashEnableRouting": false,
|
"subClashEnableRouting": false,
|
||||||
"subClashPath": "",
|
"subClashPath": "",
|
||||||
|
"subClashRemoteDNS": false,
|
||||||
"subClashRules": "",
|
"subClashRules": "",
|
||||||
"subClashURI": "",
|
"subClashURI": "",
|
||||||
"subClashUserAgentRegex": "",
|
"subClashUserAgentRegex": "",
|
||||||
|
|||||||
@@ -216,6 +216,9 @@ export const SCHEMAS: Record<string, unknown> = {
|
|||||||
"subClashPath": {
|
"subClashPath": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"subClashRemoteDNS": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"subClashRules": {
|
"subClashRules": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@@ -636,6 +639,7 @@ export const SCHEMAS: Record<string, unknown> = {
|
|||||||
"subClashEnable",
|
"subClashEnable",
|
||||||
"subClashEnableRouting",
|
"subClashEnableRouting",
|
||||||
"subClashPath",
|
"subClashPath",
|
||||||
|
"subClashRemoteDNS",
|
||||||
"subClashRules",
|
"subClashRules",
|
||||||
"subClashURI",
|
"subClashURI",
|
||||||
"subClashUserAgentRegex",
|
"subClashUserAgentRegex",
|
||||||
@@ -992,6 +996,9 @@ export const SCHEMAS: Record<string, unknown> = {
|
|||||||
"subClashPath": {
|
"subClashPath": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"subClashRemoteDNS": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"subClashRules": {
|
"subClashRules": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@@ -1420,6 +1427,7 @@ export const SCHEMAS: Record<string, unknown> = {
|
|||||||
"subClashEnable",
|
"subClashEnable",
|
||||||
"subClashEnableRouting",
|
"subClashEnableRouting",
|
||||||
"subClashPath",
|
"subClashPath",
|
||||||
|
"subClashRemoteDNS",
|
||||||
"subClashRules",
|
"subClashRules",
|
||||||
"subClashURI",
|
"subClashURI",
|
||||||
"subClashUserAgentRegex",
|
"subClashUserAgentRegex",
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ export interface AllSetting {
|
|||||||
subClashEnable: boolean;
|
subClashEnable: boolean;
|
||||||
subClashEnableRouting: boolean;
|
subClashEnableRouting: boolean;
|
||||||
subClashPath: string;
|
subClashPath: string;
|
||||||
|
subClashRemoteDNS: boolean;
|
||||||
subClashRules: string;
|
subClashRules: string;
|
||||||
subClashURI: string;
|
subClashURI: string;
|
||||||
subClashUserAgentRegex: string;
|
subClashUserAgentRegex: string;
|
||||||
@@ -261,6 +262,7 @@ export interface AllSettingView {
|
|||||||
subClashEnable: boolean;
|
subClashEnable: boolean;
|
||||||
subClashEnableRouting: boolean;
|
subClashEnableRouting: boolean;
|
||||||
subClashPath: string;
|
subClashPath: string;
|
||||||
|
subClashRemoteDNS: boolean;
|
||||||
subClashRules: string;
|
subClashRules: string;
|
||||||
subClashURI: string;
|
subClashURI: string;
|
||||||
subClashUserAgentRegex: string;
|
subClashUserAgentRegex: string;
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ export const AllSettingSchema = z.object({
|
|||||||
subClashEnable: z.boolean(),
|
subClashEnable: z.boolean(),
|
||||||
subClashEnableRouting: z.boolean(),
|
subClashEnableRouting: z.boolean(),
|
||||||
subClashPath: z.string(),
|
subClashPath: z.string(),
|
||||||
|
subClashRemoteDNS: z.boolean(),
|
||||||
subClashRules: z.string(),
|
subClashRules: z.string(),
|
||||||
subClashURI: z.string(),
|
subClashURI: z.string(),
|
||||||
subClashUserAgentRegex: z.string(),
|
subClashUserAgentRegex: z.string(),
|
||||||
@@ -278,6 +279,7 @@ export const AllSettingViewSchema = z.object({
|
|||||||
subClashEnable: z.boolean(),
|
subClashEnable: z.boolean(),
|
||||||
subClashEnableRouting: z.boolean(),
|
subClashEnableRouting: z.boolean(),
|
||||||
subClashPath: z.string(),
|
subClashPath: z.string(),
|
||||||
|
subClashRemoteDNS: z.boolean(),
|
||||||
subClashRules: z.string(),
|
subClashRules: z.string(),
|
||||||
subClashURI: z.string(),
|
subClashURI: z.string(),
|
||||||
subClashUserAgentRegex: z.string(),
|
subClashUserAgentRegex: z.string(),
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ export class AllSetting {
|
|||||||
subClashURI = '';
|
subClashURI = '';
|
||||||
subClashEnableRouting = false;
|
subClashEnableRouting = false;
|
||||||
subClashRules = '';
|
subClashRules = '';
|
||||||
|
subClashRemoteDNS = false;
|
||||||
subJsonMux = '';
|
subJsonMux = '';
|
||||||
subJsonRules = '';
|
subJsonRules = '';
|
||||||
subJsonRoutingRules = '';
|
subJsonRoutingRules = '';
|
||||||
|
|||||||
@@ -459,6 +459,16 @@ export default function SubscriptionGeneralTab({
|
|||||||
onChange={(e) => updateSetting({ subClashRules: e.target.value })}
|
onChange={(e) => updateSetting({ subClashRules: e.target.value })}
|
||||||
/>
|
/>
|
||||||
</SettingListItem>
|
</SettingListItem>
|
||||||
|
<SettingListItem
|
||||||
|
paddings="small"
|
||||||
|
title={t('pages.settings.subClashRemoteDNS')}
|
||||||
|
description={t('pages.settings.subClashRemoteDNSDesc')}
|
||||||
|
>
|
||||||
|
<Switch
|
||||||
|
checked={allSetting.subClashRemoteDNS}
|
||||||
|
onChange={(v) => updateSetting({ subClashRemoteDNS: v })}
|
||||||
|
/>
|
||||||
|
</SettingListItem>
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ export const AllSettingSchema = z
|
|||||||
subClashURI: z.string().optional(),
|
subClashURI: z.string().optional(),
|
||||||
subClashEnableRouting: z.boolean().optional(),
|
subClashEnableRouting: z.boolean().optional(),
|
||||||
subClashRules: z.string().optional(),
|
subClashRules: z.string().optional(),
|
||||||
|
subClashRemoteDNS: z.boolean().optional(),
|
||||||
subJsonMux: z.string().optional(),
|
subJsonMux: z.string().optional(),
|
||||||
subJsonRules: z.string().optional(),
|
subJsonRules: z.string().optional(),
|
||||||
subJsonRoutingRules: z.string().optional(),
|
subJsonRoutingRules: z.string().optional(),
|
||||||
|
|||||||
@@ -95,6 +95,12 @@ afterEach(async () => {
|
|||||||
for (let i = 0; i < 3; i += 1) {
|
for (let i = 0; i < 3; i += 1) {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
|
* AntD Form debounces clearing its error list by 10ms (useDebounce via
|
||||||
|
* rc-util useDelayState), and that timer survives unmount. Wait past it so
|
||||||
|
* it fires while `window` still exists.
|
||||||
|
*/
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 20));
|
||||||
});
|
});
|
||||||
|
|
||||||
import { HttpUtil, Msg } from '@/utils';
|
import { HttpUtil, Msg } from '@/utils';
|
||||||
|
|||||||
@@ -167,6 +167,29 @@ describe('SubscriptionGeneralTab', () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
it.each([false, true])(
|
||||||
|
'updates the Clash remote DNS opt-in, not the routing switch, when stored as %s',
|
||||||
|
(enabled) => {
|
||||||
|
const updateSetting = vi.fn();
|
||||||
|
|
||||||
|
renderWithProviders(
|
||||||
|
<MemoryRouter initialEntries={['/settings#subscription']}>
|
||||||
|
<SubscriptionGeneralTab
|
||||||
|
allSetting={new AllSetting({ subClashEnableRouting: true, subClashRemoteDNS: enabled })}
|
||||||
|
updateSetting={updateSetting}
|
||||||
|
/>
|
||||||
|
</MemoryRouter>,
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole('tab', { name: /Clash/ }));
|
||||||
|
const remoteDNSSwitch = screen.getByRole('switch', { name: 'Use DNS from remote rules' });
|
||||||
|
expect(remoteDNSSwitch.getAttribute('aria-checked')).toBe(String(enabled));
|
||||||
|
fireEvent.click(remoteDNSSwitch);
|
||||||
|
|
||||||
|
expect(updateSetting).toHaveBeenCalledExactlyOnceWith({ subClashRemoteDNS: !enabled });
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
it('shows the Happ link gate from the QR settings deep link without enabling generation', () => {
|
it('shows the Happ link gate from the QR settings deep link without enabling generation', () => {
|
||||||
const updateSetting = vi.fn();
|
const updateSetting = vi.fn();
|
||||||
|
|
||||||
|
|||||||
@@ -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"}
|
client := model.Client{Password: "clientpw", Email: "user"}
|
||||||
|
|
||||||
for _, tc := range tests {
|
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"
|
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) {
|
func TestClashExternalVlessEncryption(t *testing.T) {
|
||||||
svc := NewSubClashService(false, "", &SubService{})
|
svc := NewSubClashService(false, "", false, &SubService{})
|
||||||
for _, tc := range []struct {
|
for _, tc := range []struct {
|
||||||
name string
|
name string
|
||||||
query string
|
query string
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ func TestSubClash_InfoNode_Active(t *testing.T) {
|
|||||||
|
|
||||||
sub := NewSubService("{{EMAIL}}|📊{{TRAFFIC_LEFT}}")
|
sub := NewSubService("{{EMAIL}}|📊{{TRAFFIC_LEFT}}")
|
||||||
sub.subInfoNodeEnable = true
|
sub.subInfoNodeEnable = true
|
||||||
clash := NewSubClashService(false, "", sub)
|
clash := NewSubClashService(false, "", false, sub)
|
||||||
|
|
||||||
out, _, err := clash.GetClash("sub-clash", "sub.example.com")
|
out, _, err := clash.GetClash("sub-clash", "sub.example.com")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -115,7 +115,7 @@ func TestSubClash_InfoNode_Expired(t *testing.T) {
|
|||||||
sub := NewSubService("{{INBOUND}}")
|
sub := NewSubService("{{INBOUND}}")
|
||||||
sub.subInfoNodeEnable = true
|
sub.subInfoNodeEnable = true
|
||||||
sub.subExpiredTemplate = service.DefaultSubExpiredTemplate
|
sub.subExpiredTemplate = service.DefaultSubExpiredTemplate
|
||||||
clash := NewSubClashService(false, "", sub)
|
clash := NewSubClashService(false, "", false, sub)
|
||||||
|
|
||||||
out, _, err := clash.GetClash("sub-clash-exp", "sub.example.com")
|
out, _, err := clash.GetClash("sub-clash-exp", "sub.example.com")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -178,7 +178,7 @@ func TestSubClash_InfoNode_Depleted(t *testing.T) {
|
|||||||
sub := NewSubService("{{INBOUND}}")
|
sub := NewSubService("{{INBOUND}}")
|
||||||
sub.subInfoNodeEnable = true
|
sub.subInfoNodeEnable = true
|
||||||
sub.subTrafficDepletedTemplate = service.DefaultSubTrafficDepletedTemplate
|
sub.subTrafficDepletedTemplate = service.DefaultSubTrafficDepletedTemplate
|
||||||
clash := NewSubClashService(false, "", sub)
|
clash := NewSubClashService(false, "", false, sub)
|
||||||
|
|
||||||
out, _, err := clash.GetClash("sub-clash-dep", "sub.example.com")
|
out, _, err := clash.GetClash("sub-clash-dep", "sub.example.com")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -225,7 +225,7 @@ func TestSubClash_InfoNode_ProxyGroupOrder_DoesNotDefaultToDummy(t *testing.T) {
|
|||||||
|
|
||||||
sub := NewSubService("{{EMAIL}}|📊{{TRAFFIC_LEFT}}|{{STATUS}}")
|
sub := NewSubService("{{EMAIL}}|📊{{TRAFFIC_LEFT}}|{{STATUS}}")
|
||||||
sub.subInfoNodeEnable = true
|
sub.subInfoNodeEnable = true
|
||||||
clash := NewSubClashService(false, "", sub)
|
clash := NewSubClashService(false, "", false, sub)
|
||||||
|
|
||||||
out, _, err := clash.GetClash("sub-clash", "sub.example.com")
|
out, _, err := clash.GetClash("sub-clash", "sub.example.com")
|
||||||
if err != nil {
|
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 {
|
type SubClashService struct {
|
||||||
enableRouting bool
|
enableRouting bool
|
||||||
clashRules string
|
clashRules string
|
||||||
SubService *SubService
|
clashRemoteDNS bool
|
||||||
|
SubService *SubService
|
||||||
}
|
}
|
||||||
|
|
||||||
var errNoLegacyClashProxies = errors.New("no Clash for Windows-compatible proxies found; use the Mihomo subscription for modern proxy types")
|
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 {
|
func NewSubClashService(enableRouting bool, clashRules string, clashRemoteDNS bool, subService *SubService) *SubClashService {
|
||||||
return &SubClashService{enableRouting: enableRouting, clashRules: clashRules, SubService: subService}
|
return &SubClashService{enableRouting: enableRouting, clashRules: clashRules, clashRemoteDNS: clashRemoteDNS, SubService: subService}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SubClashService) GetClash(subId string, host string) (string, string, error) {
|
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)
|
resolved, remoteDocument, remote, resolveErr := resolveClashRoutingSource(s.clashRules)
|
||||||
if resolveErr == nil && strings.TrimSpace(resolved) != "" {
|
if resolveErr == nil && strings.TrimSpace(resolved) != "" {
|
||||||
if remote {
|
if remote {
|
||||||
if err := mergeRemoteClashRules(config, remoteDocument); err != nil {
|
if err := mergeRemoteClashRules(config, remoteDocument, s.clashRemoteDNS); err != nil {
|
||||||
return "", "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
} else if err := mergeClashRulesYAML(config, resolved); err != nil {
|
} else if err := mergeClashRulesYAML(config, resolved); err != nil {
|
||||||
@@ -1296,17 +1297,51 @@ func mergeClashRulesYAML(base map[string]any, raw string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// mergeRemoteClashRules lets remote update only the route graph (see
|
type remoteClashKeyClass int
|
||||||
// remoteClashAllowedKey) and never mutates remote: cached documents are shared.
|
|
||||||
func mergeRemoteClashRules(base map[string]any, remote map[string]any) error {
|
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 {
|
if len(remote) == 0 {
|
||||||
return fmt.Errorf("remote Clash routing source must be a YAML map")
|
return fmt.Errorf("remote Clash routing source must be a YAML map")
|
||||||
}
|
}
|
||||||
|
|
||||||
for key, value := range remote {
|
for key, value := range remote {
|
||||||
if !remoteClashAllowedKey(key) {
|
if !remoteClashKeyAllowed(key, allowClientConfig) {
|
||||||
continue
|
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 {
|
if err := validateRemoteClashValue(key, value); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -1382,15 +1417,6 @@ func validateRemoteClashValue(key string, value any) error {
|
|||||||
return nil
|
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 {
|
func validateClashRouteGraph(config map[string]any) error {
|
||||||
known := map[string]struct{}{
|
known := map[string]struct{}{
|
||||||
"DIRECT": {}, "REJECT": {}, "REJECT-DROP": {}, "REJECT-TINYGIF": {}, "PASS": {}, "GLOBAL": {},
|
"DIRECT": {}, "REJECT": {}, "REJECT-DROP": {}, "REJECT-TINYGIF": {}, "PASS": {}, "GLOBAL": {},
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ func TestBuildProxy_VLESSRealityFieldsForClash(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestClashRealityMLKEMAcrossSources(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 _, security := range []string{"reality", "tls", "none"} {
|
||||||
for _, fingerprint := range []string{"", "chrome", "firefox"} {
|
for _, fingerprint := range []string{"", "chrome", "firefox"} {
|
||||||
t.Run(security+"/"+fingerprint, func(t *testing.T) {
|
t.Run(security+"/"+fingerprint, func(t *testing.T) {
|
||||||
|
|||||||
@@ -115,6 +115,7 @@ type subControllerConfig struct {
|
|||||||
subJsonObservatory string
|
subJsonObservatory string
|
||||||
subClashEnableRouting bool
|
subClashEnableRouting bool
|
||||||
subClashRules string
|
subClashRules string
|
||||||
|
subClashRemoteDNS bool
|
||||||
|
|
||||||
subTitle string
|
subTitle string
|
||||||
subSupportURL string
|
subSupportURL string
|
||||||
@@ -217,6 +218,10 @@ func WithSUBClashRules(value string) SUBControllerOption {
|
|||||||
return func(config *subControllerConfig) { config.subClashRules = value }
|
return func(config *subControllerConfig) { config.subClashRules = value }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func WithSUBClashRemoteDNS(value bool) SUBControllerOption {
|
||||||
|
return func(config *subControllerConfig) { config.subClashRemoteDNS = value }
|
||||||
|
}
|
||||||
|
|
||||||
func WithSUBTitle(value string) SUBControllerOption {
|
func WithSUBTitle(value string) SUBControllerOption {
|
||||||
return func(config *subControllerConfig) { config.subTitle = value }
|
return func(config *subControllerConfig) { config.subTitle = value }
|
||||||
}
|
}
|
||||||
@@ -319,7 +324,7 @@ func NewSUBController(g *gin.RouterGroup, options ...SUBControllerOption) *SUBCo
|
|||||||
|
|
||||||
subService: sub,
|
subService: sub,
|
||||||
subJsonService: subJsonSvc,
|
subJsonService: subJsonSvc,
|
||||||
subClashService: NewSubClashService(config.subClashEnableRouting, config.subClashRules, sub),
|
subClashService: NewSubClashService(config.subClashEnableRouting, config.subClashRules, config.subClashRemoteDNS, sub),
|
||||||
|
|
||||||
subTemplateCache: map[string]*cachedSubTemplate{},
|
subTemplateCache: map[string]*cachedSubTemplate{},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ func TestLinkDisplayName(t *testing.T) {
|
|||||||
|
|
||||||
func TestClashProxyFromExternalTrojanReality(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"
|
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")
|
proxy := svc.clashProxyFromExternal(link, "DE-Provider")
|
||||||
if proxy == nil {
|
if proxy == nil {
|
||||||
t.Fatal("expected a clash proxy, got 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))
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("GetClash err = %v", err)
|
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"))
|
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")
|
yaml, _, err := clash.GetClash("s1", "req.example.com")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("GetClash: %v", err)
|
t.Fatalf("GetClash: %v", err)
|
||||||
@@ -281,7 +281,7 @@ func TestSub_HostHeaderReachesClashAndJson(t *testing.T) {
|
|||||||
HostHeader: "cdn.example.com", Path: "/ws-path",
|
HostHeader: "cdn.example.com", Path: "/ws-path",
|
||||||
})
|
})
|
||||||
|
|
||||||
clash := NewSubClashService(false, "", NewSubService(""))
|
clash := NewSubClashService(false, "", false, NewSubService(""))
|
||||||
yaml, _, err := clash.GetClash("s1", "req.example.com")
|
yaml, _, err := clash.GetClash("s1", "req.example.com")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("GetClash: %v", err)
|
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)
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("GetClash: %v", err)
|
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")
|
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")
|
yaml, _, err := clash.GetClash("s1", "req.example.com")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("GetClash: %v", err)
|
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)
|
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")
|
yaml, _, err := clash.GetClash("s1", "req.example.com")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("GetClash: %v", err)
|
t.Fatalf("GetClash: %v", err)
|
||||||
|
|||||||
@@ -450,7 +450,7 @@ func normalizeClashRouting(body []byte) (string, map[string]any, error) {
|
|||||||
}
|
}
|
||||||
hasSupportedKey := false
|
hasSupportedKey := false
|
||||||
for key := range document {
|
for key := range document {
|
||||||
if remoteClashAllowedKey(key) {
|
if remoteClashKeyClasses[key] == remoteClashRouteGraph {
|
||||||
hasSupportedKey = true
|
hasSupportedKey = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -465,7 +465,7 @@ func normalizeClashRouting(body []byte) (string, map[string]any, error) {
|
|||||||
}},
|
}},
|
||||||
"rules": []string{"MATCH,PROXY"},
|
"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 "", nil, fmt.Errorf("invalid remote Clash routing schema: %w", err)
|
||||||
}
|
}
|
||||||
return text, document, nil
|
return text, document, nil
|
||||||
|
|||||||
@@ -20,12 +20,12 @@ import (
|
|||||||
"github.com/mhsanaei/3x-ui/v3/internal/database/dbtest"
|
"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
|
var remote map[string]any
|
||||||
if err := yaml.Unmarshal([]byte(strings.TrimSpace(raw)), &remote); err != nil {
|
if err := yaml.Unmarshal([]byte(strings.TrimSpace(raw)), &remote); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return mergeRemoteClashRules(base, remote)
|
return mergeRemoteClashRules(base, remote, allowClientConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
type remoteRoutingRoundTripper func(*http.Request) (*http.Response, error)
|
type remoteRoutingRoundTripper func(*http.Request) (*http.Response, error)
|
||||||
@@ -475,7 +475,7 @@ rules:
|
|||||||
- RULE-SET,roscom,PROXY
|
- RULE-SET,roscom,PROXY
|
||||||
- MATCH,PROXY
|
- MATCH,PROXY
|
||||||
`
|
`
|
||||||
if err := mergeRemoteClashRulesYAML(base, remote); err != nil {
|
if err := mergeRemoteClashRulesYAML(base, remote, false); err != nil {
|
||||||
t.Fatalf("merge: %v", err)
|
t.Fatalf("merge: %v", err)
|
||||||
}
|
}
|
||||||
proxies, ok := base["proxies"].([]map[string]any)
|
proxies, ok := base["proxies"].([]map[string]any)
|
||||||
@@ -520,7 +520,7 @@ func TestMergeRemoteClashRulesKeepsBaseProxyGroupWhenRemoteOmitsIt(t *testing.T)
|
|||||||
proxies: [PROXY]
|
proxies: [PROXY]
|
||||||
rules:
|
rules:
|
||||||
- MATCH,PROXY
|
- MATCH,PROXY
|
||||||
`); err != nil {
|
`, false); err != nil {
|
||||||
t.Fatalf("merge: %v", err)
|
t.Fatalf("merge: %v", err)
|
||||||
}
|
}
|
||||||
groups, ok := asAnySlice(base["proxy-groups"])
|
groups, ok := asAnySlice(base["proxy-groups"])
|
||||||
@@ -692,7 +692,7 @@ func TestRemoteClashRouteGraphValidation(t *testing.T) {
|
|||||||
}},
|
}},
|
||||||
"rules": []string{"MATCH,PROXY"},
|
"rules": []string{"MATCH,PROXY"},
|
||||||
}
|
}
|
||||||
err := mergeRemoteClashRulesYAML(base, tt.remote)
|
err := mergeRemoteClashRulesYAML(base, tt.remote, false)
|
||||||
if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
|
if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
|
||||||
t.Fatalf("err=%v, want %q", err, tt.wantErr)
|
t.Fatalf("err=%v, want %q", err, tt.wantErr)
|
||||||
}
|
}
|
||||||
@@ -741,7 +741,7 @@ rules:
|
|||||||
}},
|
}},
|
||||||
"rules": []string{"MATCH,PROXY"},
|
"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)
|
t.Fatalf("merge cached document: %v", err)
|
||||||
}
|
}
|
||||||
if requests.Load() != 1 {
|
if requests.Load() != 1 {
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ func TestExcludedInboundHidesLinksButKeepsUsage(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("clash", func(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 {
|
if err != nil {
|
||||||
t.Fatalf("GetClash: %v", err)
|
t.Fatalf("GetClash: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -180,6 +180,11 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
|||||||
SubClashRules = ""
|
SubClashRules = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SubClashRemoteDNS, err := s.settingService.GetSubClashRemoteDNS()
|
||||||
|
if err != nil {
|
||||||
|
SubClashRemoteDNS = false
|
||||||
|
}
|
||||||
|
|
||||||
SubTitle, err := s.settingService.GetSubTitle()
|
SubTitle, err := s.settingService.GetSubTitle()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
SubTitle = ""
|
SubTitle = ""
|
||||||
@@ -361,6 +366,7 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
|||||||
WithSUBJsonObservatory(SubJsonObservatory),
|
WithSUBJsonObservatory(SubJsonObservatory),
|
||||||
WithSUBClashEnableRouting(SubClashEnableRouting),
|
WithSUBClashEnableRouting(SubClashEnableRouting),
|
||||||
WithSUBClashRules(SubClashRules),
|
WithSUBClashRules(SubClashRules),
|
||||||
|
WithSUBClashRemoteDNS(SubClashRemoteDNS),
|
||||||
WithSUBTitle(SubTitle),
|
WithSUBTitle(SubTitle),
|
||||||
WithSUBSupportURL(SubSupportUrl),
|
WithSUBSupportURL(SubSupportUrl),
|
||||||
WithSUBProfileURL(SubProfileUrl),
|
WithSUBProfileURL(SubProfileUrl),
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ func TestGetClashEmitsPinnedCertSha256(t *testing.T) {
|
|||||||
stream := `{"network":"tcp","security":"tls","tlsSettings":{"serverName":"pin.sni","settings":{"pinnedPeerCertSha256":["` + pin + `"]}}}`
|
stream := `{"network":"tcp","security":"tls","tlsSettings":{"serverName":"pin.sni","settings":{"pinnedPeerCertSha256":["` + pin + `"]}}}`
|
||||||
seedSubInbound(t, "pin1", "pin", 46300, 1, stream)
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("GetClash: %v", err)
|
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")
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("GetClash: %v", err)
|
t.Fatalf("GetClash: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ func TestSub_HostVlessRoute_Clash(t *testing.T) {
|
|||||||
ib := seedSubInbound(t, "s1", "vrc", 4502, 1, wsTLSStream)
|
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"})
|
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")
|
yaml, _, err := clash.GetClash("s1", "req.example.com")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("GetClash: %v", err)
|
t.Fatalf("GetClash: %v", err)
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ type AllSetting struct {
|
|||||||
SubClashURI string `json:"subClashURI" form:"subClashURI"`
|
SubClashURI string `json:"subClashURI" form:"subClashURI"`
|
||||||
SubClashEnableRouting bool `json:"subClashEnableRouting" form:"subClashEnableRouting"`
|
SubClashEnableRouting bool `json:"subClashEnableRouting" form:"subClashEnableRouting"`
|
||||||
SubClashRules string `json:"subClashRules" form:"subClashRules"`
|
SubClashRules string `json:"subClashRules" form:"subClashRules"`
|
||||||
|
SubClashRemoteDNS bool `json:"subClashRemoteDNS" form:"subClashRemoteDNS"`
|
||||||
SubJsonMux string `json:"subJsonMux" form:"subJsonMux"`
|
SubJsonMux string `json:"subJsonMux" form:"subJsonMux"`
|
||||||
SubJsonRules string `json:"subJsonRules" form:"subJsonRules"`
|
SubJsonRules string `json:"subJsonRules" form:"subJsonRules"`
|
||||||
SubJsonRoutingRules string `json:"subJsonRoutingRules" form:"subJsonRoutingRules"`
|
SubJsonRoutingRules string `json:"subJsonRoutingRules" form:"subJsonRoutingRules"`
|
||||||
|
|||||||
@@ -186,6 +186,7 @@ var defaultValueMap = map[string]string{
|
|||||||
"subClashURI": "",
|
"subClashURI": "",
|
||||||
"subClashEnableRouting": "false",
|
"subClashEnableRouting": "false",
|
||||||
"subClashRules": "",
|
"subClashRules": "",
|
||||||
|
"subClashRemoteDNS": "false",
|
||||||
"subJsonMux": "",
|
"subJsonMux": "",
|
||||||
"subJsonRules": "",
|
"subJsonRules": "",
|
||||||
"subJsonRoutingRules": "",
|
"subJsonRoutingRules": "",
|
||||||
@@ -1241,6 +1242,10 @@ func (s *SettingService) GetSubClashRules() (string, error) {
|
|||||||
return s.getString("subClashRules")
|
return s.getString("subClashRules")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) GetSubClashRemoteDNS() (bool, error) {
|
||||||
|
return s.getBool("subClashRemoteDNS")
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SettingService) GetSubJsonMux() (string, error) {
|
func (s *SettingService) GetSubJsonMux() (string, error) {
|
||||||
return s.getString("subJsonMux")
|
return s.getString("subJsonMux")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -114,6 +114,9 @@ func TestUpdateAllSettingPersistsClashSubscriptionSettings(t *testing.T) {
|
|||||||
if settings.SubJsonUserAgentRegex != "" {
|
if settings.SubJsonUserAgentRegex != "" {
|
||||||
t.Fatalf("subJsonUserAgentRegex = %q, want empty inherited value", settings.SubJsonUserAgentRegex)
|
t.Fatalf("subJsonUserAgentRegex = %q, want empty inherited value", settings.SubJsonUserAgentRegex)
|
||||||
}
|
}
|
||||||
|
if settings.SubClashRemoteDNS {
|
||||||
|
t.Fatal("subClashRemoteDNS default = true, want false")
|
||||||
|
}
|
||||||
if settings.SubClashUserAgentRegex != "" {
|
if settings.SubClashUserAgentRegex != "" {
|
||||||
t.Fatalf("subClashUserAgentRegex = %q, want empty inherited value", settings.SubClashUserAgentRegex)
|
t.Fatalf("subClashUserAgentRegex = %q, want empty inherited value", settings.SubClashUserAgentRegex)
|
||||||
}
|
}
|
||||||
@@ -130,6 +133,7 @@ func TestUpdateAllSettingPersistsClashSubscriptionSettings(t *testing.T) {
|
|||||||
settings.SubClashURI = "https://subscriptions.example.com/clash-custom/"
|
settings.SubClashURI = "https://subscriptions.example.com/clash-custom/"
|
||||||
settings.SubClashEnableRouting = true
|
settings.SubClashEnableRouting = true
|
||||||
settings.SubClashRules = "GEOIP,private,DIRECT"
|
settings.SubClashRules = "GEOIP,private,DIRECT"
|
||||||
|
settings.SubClashRemoteDNS = true
|
||||||
|
|
||||||
if err := s.UpdateAllSetting(settings, SecretClears{}); err != nil {
|
if err := s.UpdateAllSetting(settings, SecretClears{}); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -178,6 +182,9 @@ func TestUpdateAllSettingPersistsClashSubscriptionSettings(t *testing.T) {
|
|||||||
if got.SubClashRules != "GEOIP,private,DIRECT" {
|
if got.SubClashRules != "GEOIP,private,DIRECT" {
|
||||||
t.Fatalf("subClashRules = %q, want %q", got.SubClashRules, "GEOIP,private,DIRECT")
|
t.Fatalf("subClashRules = %q, want %q", got.SubClashRules, "GEOIP,private,DIRECT")
|
||||||
}
|
}
|
||||||
|
if !got.SubClashRemoteDNS {
|
||||||
|
t.Fatal("subClashRemoteDNS = false, want true")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUpdateAllSettingRejectsInvalidClashUserAgentRegex(t *testing.T) {
|
func TestUpdateAllSettingRejectsInvalidClashUserAgentRegex(t *testing.T) {
|
||||||
|
|||||||
@@ -1306,6 +1306,8 @@
|
|||||||
"subClashEnableRoutingDesc": "تضمين قواعد توجيه Clash/Mihomo العامة في اشتراكات YAML المُنشأة.",
|
"subClashEnableRoutingDesc": "تضمين قواعد توجيه Clash/Mihomo العامة في اشتراكات YAML المُنشأة.",
|
||||||
"subClashRoutingRules": "قواعد التوجيه العامة",
|
"subClashRoutingRules": "قواعد التوجيه العامة",
|
||||||
"subClashRoutingRulesDesc": "ألصق قواعد/YAML أو عنوان HTTPS دائمًا. تحدّثه اللوحة في الخلفية، وتستورد المجموعات وموفري القواعد والقواعد فقط، وتحافظ على عقد VPN المُنشأة وآخر قيمة صالحة.",
|
"subClashRoutingRulesDesc": "ألصق قواعد/YAML أو عنوان HTTPS دائمًا. تحدّثه اللوحة في الخلفية، وتستورد المجموعات وموفري القواعد والقواعد فقط، وتحافظ على عقد VPN المُنشأة وآخر قيمة صالحة.",
|
||||||
|
"subClashRemoteDNS": "Use DNS from remote rules",
|
||||||
|
"subClashRemoteDNSDesc": "When the global routing rules field is a URL, also take its dns and sniffer sections. Enable only for a template you trust.",
|
||||||
"subListen": "IP الاستماع",
|
"subListen": "IP الاستماع",
|
||||||
"subListenDesc": "عنوان IP لخدمة الاشتراك. (سيبه فاضي عشان يستمع على كل الـ IPs)",
|
"subListenDesc": "عنوان IP لخدمة الاشتراك. (سيبه فاضي عشان يستمع على كل الـ IPs)",
|
||||||
"subPort": "بورت الاستماع",
|
"subPort": "بورت الاستماع",
|
||||||
|
|||||||
@@ -1428,6 +1428,8 @@
|
|||||||
"subClashEnableRoutingDesc": "Include global Clash/Mihomo routing rules in generated YAML subscriptions.",
|
"subClashEnableRoutingDesc": "Include global Clash/Mihomo routing rules in generated YAML subscriptions.",
|
||||||
"subClashRoutingRules": "Global routing rules",
|
"subClashRoutingRules": "Global routing rules",
|
||||||
"subClashRoutingRulesDesc": "Paste inline rules/YAML or one permanent HTTPS URL. The panel refreshes it in the background, imports only groups/providers/rules, preserves generated VPN nodes and keeps the last valid value.",
|
"subClashRoutingRulesDesc": "Paste inline rules/YAML or one permanent HTTPS URL. The panel refreshes it in the background, imports only groups/providers/rules, preserves generated VPN nodes and keeps the last valid value.",
|
||||||
|
"subClashRemoteDNS": "Use DNS from remote rules",
|
||||||
|
"subClashRemoteDNSDesc": "When the global routing rules field is a URL, also take its dns and sniffer sections. Enable only for a template you trust.",
|
||||||
"subListen": "Listen IP",
|
"subListen": "Listen IP",
|
||||||
"subListenDesc": "The IP address for the subscription service. (leave blank to listen on all IPs)",
|
"subListenDesc": "The IP address for the subscription service. (leave blank to listen on all IPs)",
|
||||||
"subPort": "Listen Port",
|
"subPort": "Listen Port",
|
||||||
|
|||||||
@@ -1306,6 +1306,8 @@
|
|||||||
"subClashEnableRoutingDesc": "Incluir reglas globales de enrutamiento Clash/Mihomo en las suscripciones YAML generadas.",
|
"subClashEnableRoutingDesc": "Incluir reglas globales de enrutamiento Clash/Mihomo en las suscripciones YAML generadas.",
|
||||||
"subClashRoutingRules": "Reglas globales de enrutamiento",
|
"subClashRoutingRules": "Reglas globales de enrutamiento",
|
||||||
"subClashRoutingRulesDesc": "Pegue reglas/YAML o una URL HTTPS permanente. El panel la actualiza en segundo plano, importa solo grupos, proveedores de reglas y reglas, y conserva los nodos VPN generados y el último valor válido.",
|
"subClashRoutingRulesDesc": "Pegue reglas/YAML o una URL HTTPS permanente. El panel la actualiza en segundo plano, importa solo grupos, proveedores de reglas y reglas, y conserva los nodos VPN generados y el último valor válido.",
|
||||||
|
"subClashRemoteDNS": "Use DNS from remote rules",
|
||||||
|
"subClashRemoteDNSDesc": "When the global routing rules field is a URL, also take its dns and sniffer sections. Enable only for a template you trust.",
|
||||||
"subListen": "Listening IP",
|
"subListen": "Listening IP",
|
||||||
"subListenDesc": "Dejar en blanco por defecto para monitorear todas las IPs.",
|
"subListenDesc": "Dejar en blanco por defecto para monitorear todas las IPs.",
|
||||||
"subPort": "Puerto de Suscripción",
|
"subPort": "Puerto de Suscripción",
|
||||||
|
|||||||
@@ -1310,6 +1310,8 @@
|
|||||||
"subClashEnableRoutingDesc": "قوانین مسیریابی سراسری Clash/Mihomo را در اشتراکهای YAML تولیدشده وارد کن.",
|
"subClashEnableRoutingDesc": "قوانین مسیریابی سراسری Clash/Mihomo را در اشتراکهای YAML تولیدشده وارد کن.",
|
||||||
"subClashRoutingRules": "قوانین مسیریابی سراسری",
|
"subClashRoutingRules": "قوانین مسیریابی سراسری",
|
||||||
"subClashRoutingRulesDesc": "قوانین/YAML یا یک نشانی دائمی HTTPS وارد کنید. پنل آن را در پسزمینه بهروزرسانی میکند، فقط گروهها، ارائهدهندگان قانون و قوانین را وارد میکند و گرههای VPN ساختهشده و آخرین مقدار معتبر را حفظ میکند.",
|
"subClashRoutingRulesDesc": "قوانین/YAML یا یک نشانی دائمی HTTPS وارد کنید. پنل آن را در پسزمینه بهروزرسانی میکند، فقط گروهها، ارائهدهندگان قانون و قوانین را وارد میکند و گرههای VPN ساختهشده و آخرین مقدار معتبر را حفظ میکند.",
|
||||||
|
"subClashRemoteDNS": "Use DNS from remote rules",
|
||||||
|
"subClashRemoteDNSDesc": "When the global routing rules field is a URL, also take its dns and sniffer sections. Enable only for a template you trust.",
|
||||||
"subListen": "آدرس آیپی",
|
"subListen": "آدرس آیپی",
|
||||||
"subListenDesc": "آدرس آیپی برای سرویس سابسکریپشن. برای گوش دادن بهتمام آیپیها خالیبگذارید",
|
"subListenDesc": "آدرس آیپی برای سرویس سابسکریپشن. برای گوش دادن بهتمام آیپیها خالیبگذارید",
|
||||||
"subPort": "پورت",
|
"subPort": "پورت",
|
||||||
|
|||||||
@@ -1306,6 +1306,8 @@
|
|||||||
"subClashEnableRoutingDesc": "Sertakan aturan routing global Clash/Mihomo dalam langganan YAML yang dibuat.",
|
"subClashEnableRoutingDesc": "Sertakan aturan routing global Clash/Mihomo dalam langganan YAML yang dibuat.",
|
||||||
"subClashRoutingRules": "Aturan routing global",
|
"subClashRoutingRules": "Aturan routing global",
|
||||||
"subClashRoutingRulesDesc": "Tempel aturan/YAML atau satu URL HTTPS permanen. Panel memperbaruinya di latar belakang, hanya mengimpor grup, penyedia aturan, dan aturan, serta mempertahankan node VPN buatan panel dan nilai valid terakhir.",
|
"subClashRoutingRulesDesc": "Tempel aturan/YAML atau satu URL HTTPS permanen. Panel memperbaruinya di latar belakang, hanya mengimpor grup, penyedia aturan, dan aturan, serta mempertahankan node VPN buatan panel dan nilai valid terakhir.",
|
||||||
|
"subClashRemoteDNS": "Use DNS from remote rules",
|
||||||
|
"subClashRemoteDNSDesc": "When the global routing rules field is a URL, also take its dns and sniffer sections. Enable only for a template you trust.",
|
||||||
"subListen": "IP Pendengar",
|
"subListen": "IP Pendengar",
|
||||||
"subListenDesc": "Alamat IP untuk layanan langganan. (biarkan kosong untuk mendengarkan semua IP)",
|
"subListenDesc": "Alamat IP untuk layanan langganan. (biarkan kosong untuk mendengarkan semua IP)",
|
||||||
"subPort": "Port Pendengar",
|
"subPort": "Port Pendengar",
|
||||||
|
|||||||
@@ -1306,6 +1306,8 @@
|
|||||||
"subClashEnableRoutingDesc": "生成されたYAMLサブスクリプションにClash/Mihomoのグローバルルーティングルールを含めます。",
|
"subClashEnableRoutingDesc": "生成されたYAMLサブスクリプションにClash/Mihomoのグローバルルーティングルールを含めます。",
|
||||||
"subClashRoutingRules": "グローバルルーティングルール",
|
"subClashRoutingRules": "グローバルルーティングルール",
|
||||||
"subClashRoutingRulesDesc": "ルール/YAML、または永続的な HTTPS URL を入力します。パネルはバックグラウンドで更新し、グループ・ルールプロバイダー・ルールのみを取り込み、生成済み VPN ノードと最後の有効値を保持します。",
|
"subClashRoutingRulesDesc": "ルール/YAML、または永続的な HTTPS URL を入力します。パネルはバックグラウンドで更新し、グループ・ルールプロバイダー・ルールのみを取り込み、生成済み VPN ノードと最後の有効値を保持します。",
|
||||||
|
"subClashRemoteDNS": "Use DNS from remote rules",
|
||||||
|
"subClashRemoteDNSDesc": "When the global routing rules field is a URL, also take its dns and sniffer sections. Enable only for a template you trust.",
|
||||||
"subListen": "監視IP",
|
"subListen": "監視IP",
|
||||||
"subListenDesc": "サブスクリプションサービスが監視するIPアドレス(空白にするとすべてのIPを監視)",
|
"subListenDesc": "サブスクリプションサービスが監視するIPアドレス(空白にするとすべてのIPを監視)",
|
||||||
"subPort": "監視ポート",
|
"subPort": "監視ポート",
|
||||||
|
|||||||
@@ -1306,6 +1306,8 @@
|
|||||||
"subClashEnableRoutingDesc": "Incluir regras globais de roteamento Clash/Mihomo nas assinaturas YAML geradas.",
|
"subClashEnableRoutingDesc": "Incluir regras globais de roteamento Clash/Mihomo nas assinaturas YAML geradas.",
|
||||||
"subClashRoutingRules": "Regras globais de roteamento",
|
"subClashRoutingRules": "Regras globais de roteamento",
|
||||||
"subClashRoutingRulesDesc": "Cole regras/YAML ou uma URL HTTPS permanente. O painel a atualiza em segundo plano, importa apenas grupos, provedores de regras e regras, e preserva os nós VPN gerados e o último valor válido.",
|
"subClashRoutingRulesDesc": "Cole regras/YAML ou uma URL HTTPS permanente. O painel a atualiza em segundo plano, importa apenas grupos, provedores de regras e regras, e preserva os nós VPN gerados e o último valor válido.",
|
||||||
|
"subClashRemoteDNS": "Use DNS from remote rules",
|
||||||
|
"subClashRemoteDNSDesc": "When the global routing rules field is a URL, also take its dns and sniffer sections. Enable only for a template you trust.",
|
||||||
"subListen": "IP de Escuta",
|
"subListen": "IP de Escuta",
|
||||||
"subListenDesc": "O endereço IP para o serviço de assinatura. (deixe em branco para escutar em todos os IPs)",
|
"subListenDesc": "O endereço IP para o serviço de assinatura. (deixe em branco para escutar em todos os IPs)",
|
||||||
"subPort": "Porta de Escuta",
|
"subPort": "Porta de Escuta",
|
||||||
|
|||||||
@@ -1306,6 +1306,8 @@
|
|||||||
"subClashEnableRoutingDesc": "Добавлять глобальные правила маршрутизации Clash/Mihomo в сгенерированные YAML-подписки.",
|
"subClashEnableRoutingDesc": "Добавлять глобальные правила маршрутизации Clash/Mihomo в сгенерированные YAML-подписки.",
|
||||||
"subClashRoutingRules": "Глобальные правила маршрутизации",
|
"subClashRoutingRules": "Глобальные правила маршрутизации",
|
||||||
"subClashRoutingRulesDesc": "Вставьте правила/YAML либо одну постоянную HTTPS-ссылку. Панель обновляет её в фоне, импортирует только группы, провайдеры правил и правила, сохраняет созданные VPN-узлы и последнее рабочее значение.",
|
"subClashRoutingRulesDesc": "Вставьте правила/YAML либо одну постоянную HTTPS-ссылку. Панель обновляет её в фоне, импортирует только группы, провайдеры правил и правила, сохраняет созданные VPN-узлы и последнее рабочее значение.",
|
||||||
|
"subClashRemoteDNS": "DNS из удалённых правил",
|
||||||
|
"subClashRemoteDNSDesc": "Если в глобальных правилах маршрутизации указана ссылка, брать из неё также разделы dns и sniffer. Включайте только для шаблона, которому доверяете.",
|
||||||
"subListen": "Прослушивание IP",
|
"subListen": "Прослушивание IP",
|
||||||
"subListenDesc": "Оставьте пустым по умолчанию, чтобы отслеживать все IP-адреса",
|
"subListenDesc": "Оставьте пустым по умолчанию, чтобы отслеживать все IP-адреса",
|
||||||
"subPort": "Порт подписки",
|
"subPort": "Порт подписки",
|
||||||
|
|||||||
@@ -1306,6 +1306,8 @@
|
|||||||
"subClashEnableRoutingDesc": "Oluşturulan YAML aboneliklerine genel Clash/Mihomo yönlendirme kurallarını ekler.",
|
"subClashEnableRoutingDesc": "Oluşturulan YAML aboneliklerine genel Clash/Mihomo yönlendirme kurallarını ekler.",
|
||||||
"subClashRoutingRules": "Genel Yönlendirme Kuralları",
|
"subClashRoutingRules": "Genel Yönlendirme Kuralları",
|
||||||
"subClashRoutingRulesDesc": "Kurallar/YAML veya kalıcı bir HTTPS URL'si yapıştırın. Panel bunu arka planda yeniler, yalnızca grupları, kural sağlayıcılarını ve kuralları içe aktarır; oluşturulan VPN düğümlerini ve son geçerli değeri korur.",
|
"subClashRoutingRulesDesc": "Kurallar/YAML veya kalıcı bir HTTPS URL'si yapıştırın. Panel bunu arka planda yeniler, yalnızca grupları, kural sağlayıcılarını ve kuralları içe aktarır; oluşturulan VPN düğümlerini ve son geçerli değeri korur.",
|
||||||
|
"subClashRemoteDNS": "Use DNS from remote rules",
|
||||||
|
"subClashRemoteDNSDesc": "When the global routing rules field is a URL, also take its dns and sniffer sections. Enable only for a template you trust.",
|
||||||
"subListen": "Dinleme IP",
|
"subListen": "Dinleme IP",
|
||||||
"subListenDesc": "Abonelik hizmeti için IP adresi. (tüm IP'leri dinlemek için boş bırakın)",
|
"subListenDesc": "Abonelik hizmeti için IP adresi. (tüm IP'leri dinlemek için boş bırakın)",
|
||||||
"subPort": "Dinleme Portu",
|
"subPort": "Dinleme Portu",
|
||||||
|
|||||||
@@ -1306,6 +1306,8 @@
|
|||||||
"subClashEnableRoutingDesc": "Додавати глобальні правила маршрутизації Clash/Mihomo до згенерованих YAML-підписок.",
|
"subClashEnableRoutingDesc": "Додавати глобальні правила маршрутизації Clash/Mihomo до згенерованих YAML-підписок.",
|
||||||
"subClashRoutingRules": "Глобальні правила маршрутизації",
|
"subClashRoutingRules": "Глобальні правила маршрутизації",
|
||||||
"subClashRoutingRulesDesc": "Вставте правила/YAML або одну постійну HTTPS-адресу. Панель оновлює її у фоні, імпортує лише групи, постачальників правил і правила, зберігаючи створені VPN-вузли та останнє коректне значення.",
|
"subClashRoutingRulesDesc": "Вставте правила/YAML або одну постійну HTTPS-адресу. Панель оновлює її у фоні, імпортує лише групи, постачальників правил і правила, зберігаючи створені VPN-вузли та останнє коректне значення.",
|
||||||
|
"subClashRemoteDNS": "Use DNS from remote rules",
|
||||||
|
"subClashRemoteDNSDesc": "When the global routing rules field is a URL, also take its dns and sniffer sections. Enable only for a template you trust.",
|
||||||
"subListen": "Слухати IP",
|
"subListen": "Слухати IP",
|
||||||
"subListenDesc": "IP-адреса для служби підписки. (залиште порожнім, щоб слухати всі IP-адреси)",
|
"subListenDesc": "IP-адреса для служби підписки. (залиште порожнім, щоб слухати всі IP-адреси)",
|
||||||
"subPort": "Слухати порт",
|
"subPort": "Слухати порт",
|
||||||
|
|||||||
@@ -1306,6 +1306,8 @@
|
|||||||
"subClashEnableRoutingDesc": "Bao gồm quy tắc định tuyến Clash/Mihomo toàn cầu trong các đăng ký YAML được tạo.",
|
"subClashEnableRoutingDesc": "Bao gồm quy tắc định tuyến Clash/Mihomo toàn cầu trong các đăng ký YAML được tạo.",
|
||||||
"subClashRoutingRules": "Quy tắc định tuyến toàn cầu",
|
"subClashRoutingRules": "Quy tắc định tuyến toàn cầu",
|
||||||
"subClashRoutingRulesDesc": "Dán quy tắc/YAML hoặc một URL HTTPS cố định. Bảng điều khiển cập nhật trong nền, chỉ nhập nhóm, nhà cung cấp quy tắc và quy tắc, đồng thời giữ các nút VPN đã tạo và giá trị hợp lệ gần nhất.",
|
"subClashRoutingRulesDesc": "Dán quy tắc/YAML hoặc một URL HTTPS cố định. Bảng điều khiển cập nhật trong nền, chỉ nhập nhóm, nhà cung cấp quy tắc và quy tắc, đồng thời giữ các nút VPN đã tạo và giá trị hợp lệ gần nhất.",
|
||||||
|
"subClashRemoteDNS": "Use DNS from remote rules",
|
||||||
|
"subClashRemoteDNSDesc": "When the global routing rules field is a URL, also take its dns and sniffer sections. Enable only for a template you trust.",
|
||||||
"subListen": "Listening IP",
|
"subListen": "Listening IP",
|
||||||
"subListenDesc": "Mặc định để trống để nghe tất cả các IP",
|
"subListenDesc": "Mặc định để trống để nghe tất cả các IP",
|
||||||
"subPort": "Cổng gói đăng ký",
|
"subPort": "Cổng gói đăng ký",
|
||||||
|
|||||||
@@ -1306,6 +1306,8 @@
|
|||||||
"subClashEnableRoutingDesc": "在生成的 YAML 订阅中包含 Clash/Mihomo 全局路由规则。",
|
"subClashEnableRoutingDesc": "在生成的 YAML 订阅中包含 Clash/Mihomo 全局路由规则。",
|
||||||
"subClashRoutingRules": "全局路由规则",
|
"subClashRoutingRules": "全局路由规则",
|
||||||
"subClashRoutingRulesDesc": "粘贴规则/YAML 或一个固定 HTTPS URL。面板会在后台更新,仅导入代理组、规则提供者和规则,并保留面板生成的 VPN 节点及最后一个有效值。",
|
"subClashRoutingRulesDesc": "粘贴规则/YAML 或一个固定 HTTPS URL。面板会在后台更新,仅导入代理组、规则提供者和规则,并保留面板生成的 VPN 节点及最后一个有效值。",
|
||||||
|
"subClashRemoteDNS": "Use DNS from remote rules",
|
||||||
|
"subClashRemoteDNSDesc": "When the global routing rules field is a URL, also take its dns and sniffer sections. Enable only for a template you trust.",
|
||||||
"subListen": "监听 IP",
|
"subListen": "监听 IP",
|
||||||
"subListenDesc": "订阅服务监听的 IP 地址(留空表示监听所有 IP)",
|
"subListenDesc": "订阅服务监听的 IP 地址(留空表示监听所有 IP)",
|
||||||
"subPort": "监听端口",
|
"subPort": "监听端口",
|
||||||
|
|||||||
@@ -1306,6 +1306,8 @@
|
|||||||
"subClashEnableRoutingDesc": "在產生的 YAML 訂閱中包含 Clash/Mihomo 全域路由規則。",
|
"subClashEnableRoutingDesc": "在產生的 YAML 訂閱中包含 Clash/Mihomo 全域路由規則。",
|
||||||
"subClashRoutingRules": "全域路由規則",
|
"subClashRoutingRules": "全域路由規則",
|
||||||
"subClashRoutingRulesDesc": "貼上規則/YAML 或一個固定 HTTPS URL。面板會在背景更新,只匯入代理群組、規則提供者與規則,並保留面板產生的 VPN 節點及最後一個有效值。",
|
"subClashRoutingRulesDesc": "貼上規則/YAML 或一個固定 HTTPS URL。面板會在背景更新,只匯入代理群組、規則提供者與規則,並保留面板產生的 VPN 節點及最後一個有效值。",
|
||||||
|
"subClashRemoteDNS": "Use DNS from remote rules",
|
||||||
|
"subClashRemoteDNSDesc": "When the global routing rules field is a URL, also take its dns and sniffer sections. Enable only for a template you trust.",
|
||||||
"subListen": "監聽 IP",
|
"subListen": "監聽 IP",
|
||||||
"subListenDesc": "訂閱服務監聽的 IP 地址(留空表示監聽所有 IP)",
|
"subListenDesc": "訂閱服務監聽的 IP 地址(留空表示監聽所有 IP)",
|
||||||
"subPort": "監聽埠",
|
"subPort": "監聽埠",
|
||||||
|
|||||||
Reference in New Issue
Block a user