Skip to content

Commit 621d619

Browse files
committed
feat: QX 支持 AnyTLS
1 parent 7e70ffd commit 621d619

7 files changed

Lines changed: 203 additions & 9 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ Core functionalities:
4242
- [x] Clash Proxies YAML
4343
- [x] Clash Proxy JSON/JSON5/YAML(single line)
4444
> [NaiveProxy](https://t.me/zhetengsha/4308)
45-
- [x] QX (SS, SSR, VMess, Trojan, HTTP, SOCKS5, VLESS)
45+
- [x] QX (SS, SSR, VMess, Trojan, HTTP, SOCKS5, VLESS, AnyTLS)
4646
- [x] Loon (SS, SSR, VMess, Trojan, HTTP, SOCKS5, SOCKS5-TLS, WireGuard, VLESS, Hysteria 2, AnyTLS)
4747
- [x] Surge (Direct, SS, VMess, Trojan, HTTP, SOCKS5, SOCKS5-TLS, AnyTLS, TrustTunnel, TUIC, Snell, Hysteria 2, SSH(Password authentication only), External Proxy Program(only for macOS), WireGuard(Surge to Surge))
4848
- [x] mihomo(Clash.Meta) Compatible (Direct, SS, SSR, VMess, Trojan, HTTP, SOCKS5, Snell, VLESS, WireGuard, Hysteria, Hysteria 2, TUIC, SSH, mieru, sudoku, AnyTLS, MASQUE, Tailscale)

backend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "sub-store",
3-
"version": "2.21.95",
3+
"version": "2.21.96",
44
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and Shadowrocket.",
55
"main": "src/main.js",
66
"scripts": {

backend/src/core/proxy-utils/parsers/index.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1612,6 +1612,15 @@ function QX_VLESS() {
16121612
return { name, test, parse };
16131613
}
16141614

1615+
function QX_AnyTLS() {
1616+
const name = 'QX AnyTLS Parser';
1617+
const test = (line) => {
1618+
return /^anytls\s*=/.test(line.split(',')[0].trim());
1619+
};
1620+
const parse = (line) => getQXParser().parse(line);
1621+
return { name, test, parse };
1622+
}
1623+
16151624
function QX_Trojan() {
16161625
const name = 'QX Trojan Parser';
16171626
const test = (line) => {
@@ -2093,6 +2102,7 @@ export default [
20932102
QX_SSR(),
20942103
QX_VMess(),
20952104
QX_VLESS(),
2105+
QX_AnyTLS(),
20962106
QX_Trojan(),
20972107
QX_Http(),
20982108
QX_Socks5(),

backend/src/core/proxy-utils/parsers/peggy/qx.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ const grammars = String.raw`
5353
}
5454
}
5555
56-
start = (trojan/shadowsocks/vmess/vless/http/socks5) {
56+
start = (trojan/shadowsocks/vmess/vless/anytls/http/socks5) {
5757
return proxy
5858
}
5959
@@ -119,6 +119,12 @@ vless = "vless" equals address
119119
handleObfs();
120120
}
121121
122+
anytls = "anytls" equals address
123+
(password/over_tls/tls_host/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/tag/udp_relay/fast_open/server_check_url/reality_base64_pubkey/reality_hex_shortid/others)* {
124+
proxy.type = "anytls";
125+
proxy.tls = true;
126+
}
127+
122128
http = "http" equals address
123129
(username/password/over_tls/tls_host/tls_pubkey_sha256/tls_alpn/tls_no_session_ticket/tls_no_session_reuse/tls_fingerprint/tls_verification/tag/fast_open/udp_relay/udp_over_tcp/server_check_url/reality_base64_pubkey/reality_hex_shortid/others)*{
124130
proxy.type = "http";

backend/src/core/proxy-utils/producers/qx.js

Lines changed: 59 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ export default function QX_Producer() {
2828
return socks5(proxy);
2929
case 'vless':
3030
return vless(proxy);
31+
case 'anytls':
32+
return anytls(proxy);
3133
}
3234
throw new Error(
3335
`Platform ${targetPlatform} does not support proxy type: ${proxy.type}`,
@@ -57,12 +59,9 @@ export default function QX_Producer() {
5759
function getQxHttpObfs(proxy) {
5860
// QX accepts multiple http-obfs spellings for ss/vmess/vless. Preserve
5961
// the original token for round-trip output, including "vemss-http".
60-
return [
61-
'http',
62-
'vmess-http',
63-
'vemss-http',
64-
'shadowsocks-http',
65-
].includes(proxy._qx_obfs_http)
62+
return ['http', 'vmess-http', 'vemss-http', 'shadowsocks-http'].includes(
63+
proxy._qx_obfs_http,
64+
)
6665
? proxy._qx_obfs_http
6766
: 'http';
6867
}
@@ -533,6 +532,60 @@ function vless(proxy) {
533532
return result.toString();
534533
}
535534

535+
function anytls(proxy) {
536+
const network = proxy.network?.trim().toLowerCase();
537+
if (network && network !== 'tcp') {
538+
throw new Error(
539+
`Platform ${targetPlatform} does not support AnyTLS with transport ${proxy.network}`,
540+
);
541+
}
542+
543+
const result = new Result(proxy);
544+
const append = result.append.bind(result);
545+
const appendIfPresent = result.appendIfPresent.bind(result);
546+
547+
append(`anytls=${proxy.server}:${proxy.port}`);
548+
append(`,password=${proxy.password}`);
549+
550+
proxy.tls = true;
551+
append(`,over-tls=true`);
552+
553+
appendIfPresent(
554+
`,tls-pubkey-sha256=${proxy['tls-pubkey-sha256']}`,
555+
'tls-pubkey-sha256',
556+
);
557+
appendIfPresent(`,tls-alpn=${proxy['tls-alpn']}`, 'tls-alpn');
558+
appendIfPresent(
559+
`,tls-no-session-ticket=${proxy['tls-no-session-ticket']}`,
560+
'tls-no-session-ticket',
561+
);
562+
appendIfPresent(
563+
`,tls-no-session-reuse=${proxy['tls-no-session-reuse']}`,
564+
'tls-no-session-reuse',
565+
);
566+
appendIfPresent(
567+
`,tls-cert-sha256=${proxy['tls-fingerprint']}`,
568+
'tls-fingerprint',
569+
);
570+
appendIfPresent(
571+
`,tls-verification=${!proxy['skip-cert-verify']}`,
572+
'skip-cert-verify',
573+
);
574+
appendIfPresent(`,tls-host=${proxy.sni}`, 'sni');
575+
576+
appendIfPresent(`,fast-open=${proxy.tfo}`, 'tfo');
577+
appendIfPresent(`,udp-relay=${proxy.udp}`, 'udp');
578+
579+
result.appendIfPresent(
580+
`,server_check_url=${proxy['test-url']}`,
581+
'test-url',
582+
);
583+
584+
append(`,tag=${proxy.name}`);
585+
586+
return result.toString();
587+
}
588+
536589
function http(proxy) {
537590
const result = new Result(proxy);
538591
const append = result.append.bind(result);

backend/src/test/proxy-parsers/v2ray-and-platforms.spec.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1742,6 +1742,38 @@ describe('Platform raw-format parser coverage', function () {
17421742
flow: 'xtls-rprx-vision',
17431743
},
17441744
},
1745+
{
1746+
title: 'parses anytls standard tls lines',
1747+
input: 'anytls=example.com:443,password=pwd,over-tls=true,tls-host=apple.com,udp-relay=true,tag=anytls-standard-tls-01',
1748+
expected: {
1749+
type: 'anytls',
1750+
name: 'anytls-standard-tls-01',
1751+
server: 'example.com',
1752+
port: 443,
1753+
password: 'pwd',
1754+
tls: true,
1755+
sni: 'apple.com',
1756+
udp: true,
1757+
},
1758+
},
1759+
{
1760+
title: 'parses anytls reality tls lines',
1761+
input: 'anytls=example.com:443,password=pwd,over-tls=true,tls-host=apple.com,reality-base64-pubkey=k4Uxez0sjl8bKaZH2Vgi8-WDFshML51QkxKFLWFIONk,reality-hex-shortid=0123456789abcdef,tag=anytls-reality-tls-01',
1762+
expected: {
1763+
type: 'anytls',
1764+
name: 'anytls-reality-tls-01',
1765+
server: 'example.com',
1766+
port: 443,
1767+
password: 'pwd',
1768+
tls: true,
1769+
sni: 'apple.com',
1770+
'reality-opts': {
1771+
'public-key':
1772+
'k4Uxez0sjl8bKaZH2Vgi8-WDFshML51QkxKFLWFIONk',
1773+
'short-id': '0123456789abcdef',
1774+
},
1775+
},
1776+
},
17451777
{
17461778
title: 'parses trojan websocket tls lines',
17471779
input: 'trojan=qx-trojan.example.com:443,password=secret,obfs=wss,obfs-host=cdn.example.com,obfs-uri=/trojan,tls-verification=false,tls-host=sni.example.com,tls-cert-sha256=fingerprint,tag=QX Trojan',

backend/src/test/proxy-producers/text.spec.js

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Base64 } from 'js-base64';
33
import { describe, it } from 'mocha';
44

55
import { ProxyUtils } from '@/core/proxy-utils';
6+
import QX_Producer from '@/core/proxy-utils/producers/qx';
67
import { produceExternal, UUID } from './helpers';
78

89
describe('Proxy text producers', function () {
@@ -287,6 +288,98 @@ describe('Proxy text producers', function () {
287288
);
288289
});
289290

291+
it('produces Quantumult X AnyTLS standard tls lines', function () {
292+
const output = produceExternal('QX', {
293+
type: 'anytls',
294+
name: 'anytls-standard-tls-01',
295+
server: 'example.com',
296+
port: 443,
297+
password: 'pwd',
298+
tls: true,
299+
sni: 'apple.com',
300+
udp: true,
301+
});
302+
303+
expect(output).to.equal(
304+
'anytls=example.com:443,password=pwd,over-tls=true,tls-host=apple.com,udp-relay=true,tag=anytls-standard-tls-01',
305+
);
306+
});
307+
308+
it('produces Quantumult X AnyTLS reality tls lines', function () {
309+
const output = produceExternal('QX', {
310+
type: 'anytls',
311+
name: 'anytls-reality-tls-01',
312+
server: 'example.com',
313+
port: 443,
314+
password: 'pwd',
315+
tls: true,
316+
sni: 'apple.com',
317+
udp: true,
318+
'reality-opts': {
319+
'public-key':
320+
'k4Uxez0sjl8bKaZH2Vgi8-WDFshML51QkxKFLWFIONk',
321+
'short-id': '0123456789abcdef',
322+
},
323+
});
324+
325+
expect(output).to.equal(
326+
'anytls=example.com:443,password=pwd,over-tls=true,tls-host=apple.com,udp-relay=true,tag=anytls-reality-tls-01,reality-base64-pubkey=k4Uxez0sjl8bKaZH2Vgi8-WDFshML51QkxKFLWFIONk,reality-hex-shortid=0123456789abcdef',
327+
);
328+
});
329+
330+
it('produces Quantumult X AnyTLS lines and keeps only supported fields', function () {
331+
const output = produceExternal('QX', {
332+
type: 'anytls',
333+
name: 'QX AnyTLS Supported Fields',
334+
server: 'anytls.example.com',
335+
port: 443,
336+
password: 'secret',
337+
tls: true,
338+
sni: 'sni.example.com',
339+
udp: true,
340+
tfo: true,
341+
'skip-cert-verify': true,
342+
'test-url': 'https://check.example.com',
343+
'idle-session-timeout': 30,
344+
'max-stream-count': 16,
345+
});
346+
347+
expect(output).to.equal(
348+
'anytls=anytls.example.com:443,password=secret,over-tls=true,tls-verification=false,tls-host=sni.example.com,fast-open=true,udp-relay=true,server_check_url=https://check.example.com,tag=QX AnyTLS Supported Fields',
349+
);
350+
});
351+
352+
it('rejects Quantumult X AnyTLS transport shapes that QX cannot represent', function () {
353+
const producer = QX_Producer();
354+
355+
expect(() =>
356+
producer.produce({
357+
type: 'anytls',
358+
name: 'QX AnyTLS WS',
359+
server: 'anytls.example.com',
360+
port: 443,
361+
password: 'secret',
362+
tls: true,
363+
network: 'ws',
364+
'ws-opts': {
365+
path: '/anytls',
366+
headers: {
367+
Host: 'cdn.example.com',
368+
},
369+
},
370+
}),
371+
).to.throw('Platform QX does not support AnyTLS with transport ws');
372+
});
373+
374+
it('round-trips Quantumult X AnyTLS reality lines without changing supported semantics', function () {
375+
const raw =
376+
'anytls=example.com:443,password=pwd,over-tls=true,tls-host=apple.com,udp-relay=true,tag=anytls-reality-tls-01,reality-base64-pubkey=k4Uxez0sjl8bKaZH2Vgi8-WDFshML51QkxKFLWFIONk,reality-hex-shortid=0123456789abcdef';
377+
const [proxy] = ProxyUtils.parse(raw);
378+
const output = ProxyUtils.produce([proxy], 'QX', 'external');
379+
380+
expect(output).to.equal(raw);
381+
});
382+
290383
it('produces Loon VLESS reality websocket lines', function () {
291384
const output = produceExternal('Loon', {
292385
type: 'vless',

0 commit comments

Comments
 (0)