diff --git a/src/builders/ClashConfigBuilder.js b/src/builders/ClashConfigBuilder.js index 992aa7a28..01bcbee6f 100644 --- a/src/builders/ClashConfigBuilder.js +++ b/src/builders/ClashConfigBuilder.js @@ -39,6 +39,13 @@ function supportsMrsFormat(userAgent) { return true; } +function getClashUdpValue(proxy, defaultEnabled = true) { + if (typeof proxy?.udp !== 'undefined') { + return proxy.udp; + } + return defaultEnabled; +} + export class ClashConfigBuilder extends BaseConfigBuilder { constructor(inputString, selectedRules, customRules, baseConfig, lang, userAgent, groupByCountry = false, enableClashUI = false, externalController, externalUiDownloadUrl, includeAutoSelect = true) { if (!baseConfig) { @@ -126,7 +133,7 @@ export class ClashConfigBuilder extends BaseConfigBuilder { port: proxy.server_port, cipher: proxy.method, password: proxy.password, - ...(typeof proxy.udp !== 'undefined' ? { udp: proxy.udp } : {}), + udp: getClashUdpValue(proxy), ...(proxy.plugin ? { plugin: proxy.plugin } : {}), ...(proxy.plugin_opts ? { 'plugin-opts': proxy.plugin_opts } : {}) }; @@ -171,7 +178,8 @@ export class ClashConfigBuilder extends BaseConfigBuilder { path: proxy.transport.path, host: proxy.transport.host } - : undefined + : undefined, + udp: getClashUdpValue(proxy) }; case 'vless': return { @@ -198,7 +206,7 @@ export class ClashConfigBuilder extends BaseConfigBuilder { } : undefined, tfo: proxy.tcp_fast_open, 'skip-cert-verify': !!proxy.tls?.insecure, - ...(typeof proxy.udp !== 'undefined' ? { udp: proxy.udp } : {}), + udp: getClashUdpValue(proxy), ...(proxy.alpn ? { alpn: proxy.alpn } : {}), ...(proxy.packet_encoding ? { 'packet-encoding': proxy.packet_encoding } : {}), 'flow': proxy.flow ?? undefined, @@ -250,6 +258,7 @@ export class ClashConfigBuilder extends BaseConfigBuilder { 'skip-cert-verify': !!proxy.tls?.insecure, ...(proxy.alpn ? { alpn: proxy.alpn } : {}), 'flow': proxy.flow ?? undefined, + udp: getClashUdpValue(proxy), }; case 'tuic': return { @@ -276,7 +285,7 @@ export class ClashConfigBuilder extends BaseConfigBuilder { server: proxy.server, port: proxy.server_port, password: proxy.password, - ...(proxy.udp !== undefined ? { udp: proxy.udp } : {}), + udp: getClashUdpValue(proxy), ...(proxy.tls?.utls?.fingerprint ? { 'client-fingerprint': proxy.tls.utls.fingerprint } : {}), ...(proxy.tls?.server_name ? { sni: proxy.tls.server_name } : {}), ...(proxy.tls?.insecure !== undefined ? { 'skip-cert-verify': !!proxy.tls.insecure } : {}), diff --git a/src/config/clashConfig.js b/src/config/clashConfig.js index cb37ccf27..08c3213aa 100644 --- a/src/config/clashConfig.js +++ b/src/config/clashConfig.js @@ -24,7 +24,7 @@ export const CLASH_CONFIG = { }, 'dns': { 'enable': true, - 'ipv6': true, + 'ipv6': false, 'respect-rules': true, 'enhanced-mode': 'fake-ip', 'nameserver': [ @@ -46,6 +46,18 @@ export const CLASH_CONFIG = { ] } }, + 'tun': { + 'enable': true, + 'stack': 'mixed', + 'dns-hijack': [ + 'any:53', + 'tcp://any:53' + ], + 'auto-route': true, + 'auto-redirect': true, + 'auto-detect-interface': true, + 'strict-route': true + }, 'proxies': [], 'proxy-groups': [] }; diff --git a/test/clash-builder.test.js b/test/clash-builder.test.js index 15a9530e7..700b1ed3f 100644 --- a/test/clash-builder.test.js +++ b/test/clash-builder.test.js @@ -116,4 +116,25 @@ ss://YWVzLTEyOC1nY206dGVzdA@example.com:444#US-Node-1 expect(fallbackGroup).toBeDefined(); expect(fallbackGroup.proxies[0]).not.toBe('DIRECT'); }); + + it('should enable local TUN-compatible defaults in Clash config', async () => { + const input = ` +ss://YWVzLTEyOC1nY206dGVzdA@example.com:443#HK-Node-1 + `; + + const builder = new ClashConfigBuilder(input, 'minimal', [], null, 'zh-CN', 'test-agent'); + const yamlText = await builder.build(); + const built = yaml.load(yamlText); + + expect(built.dns.ipv6).toBe(false); + expect(built.tun).toEqual({ + enable: true, + stack: 'mixed', + 'dns-hijack': ['any:53', 'tcp://any:53'], + 'auto-route': true, + 'auto-redirect': true, + 'auto-detect-interface': true, + 'strict-route': true, + }); + }); }); diff --git a/test/udp-handling.test.js b/test/udp-handling.test.js index 2dbf428fd..d1cb1184e 100644 --- a/test/udp-handling.test.js +++ b/test/udp-handling.test.js @@ -1,4 +1,5 @@ import { describe, it, expect } from 'vitest'; +import yaml from 'js-yaml'; import { SingboxConfigBuilder } from '../src/builders/SingboxConfigBuilder.js'; import { ClashConfigBuilder } from '../src/builders/ClashConfigBuilder.js'; import { parseVless } from '../src/parsers/protocols/vlessParser.js'; @@ -88,6 +89,33 @@ describe('UDP handling in proxy conversion', () => { expect(converted.name).toBe('TestProxy'); expect(converted.type).toBe('vless'); }); + + it('should enable udp by default for Clash proxies built from URI subscriptions', async () => { + const input = 'ss://YWVzLTEyOC1nY206dGVzdA@example.com:443#TestSS'; + const builder = new ClashConfigBuilder(input, 'minimal', [], null, 'zh-CN', null); + const built = yaml.load(await builder.build()); + + expect(built.proxies).toHaveLength(1); + expect(built.proxies[0].type).toBe('ss'); + expect(built.proxies[0].udp).toBe(true); + }); + + it('should keep explicit udp=false when generating Clash proxies', () => { + const proxyWithDisabledUdp = { + tag: 'TestProxy', + type: 'vmess', + server: 'example.com', + server_port: 443, + uuid: 'test-uuid', + udp: false, + tls: { enabled: true, server_name: 'example.com' } + }; + + const builder = new ClashConfigBuilder('', [], [], null, 'zh-CN', null); + const converted = builder.convertProxy(proxyWithDisabledUdp); + + expect(converted.udp).toBe(false); + }); }); describe('Clash YAML to sing-box conversion should strip udp', () => {