Skip to content

Commit dd09559

Browse files
committed
feat: Surge TLS 协议支持 alpn
1 parent 1c7ab29 commit dd09559

5 files changed

Lines changed: 171 additions & 17 deletions

File tree

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.31.4",
3+
"version": "2.31.5",
44
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and Shadowrocket.",
55
"main": "src/main.js",
66
"packageManager": "pnpm@11.0.9",

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

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,12 @@ const grammars = String.raw`
345345
if (!supported.includes(normalized)) return "auto";
346346
return normalized === "chacha20-ietf-poly1305" ? "chacha20-poly1305" : normalized;
347347
}
348+
function parseAlpn(value) {
349+
return stripQuotes(value)
350+
.split(",")
351+
.map((item) => item.trim())
352+
.filter((item) => item !== "");
353+
}
348354
}
349355
350356
start = (anytls/shadowsocks/vmess/trojan/h2_connect/https/http/snell/socks5/socks5_tls/tuic/tuic_v5/wireguard/hysteria2/ssh/trust_tunnel/direct) {
@@ -362,7 +368,7 @@ shadowsocks = tag equals "ss" address (method/passwordk/obfs/obfs_host/obfs_uri/
362368
}
363369
handleShadowTLS();
364370
}
365-
vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/vmess_method/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls/sni/tls_fingerprint/tls_verification/client_cert/fast_open/tfo/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
371+
vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/vmess_method/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls/sni/alpn/tls_fingerprint/tls_verification/client_cert/fast_open/tfo/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
366372
proxy.type = "vmess";
367373
proxy.cipher = proxy.cipher || "auto";
368374
// Surfboard 与 Surge 默认不一致, 不管 Surfboard https://getsurfboard.com/docs/profile-format/proxy/external-proxy/vmess
@@ -374,17 +380,17 @@ vmess = tag equals "vmess" address (vmess_uuid/vmess_aead/ws/ws_path/ws_headers/
374380
handleWebsocket();
375381
handleShadowTLS();
376382
}
377-
trojan = tag equals "trojan" address (passwordk/ws/ws_path/ws_headers/tls/sni/tls_fingerprint/tls_verification/client_cert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
383+
trojan = tag equals "trojan" address (passwordk/ws/ws_path/ws_headers/tls/sni/alpn/tls_fingerprint/tls_verification/client_cert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/udp_relay/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
378384
proxy.type = "trojan";
379385
handleWebsocket();
380386
handleShadowTLS();
381387
}
382-
https = tag equals "https" address (username password)? (usernamek passwordk)? (headers/sni/tls_fingerprint/tls_verification/client_cert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
388+
https = tag equals "https" address (username password)? (usernamek passwordk)? (headers/sni/alpn/tls_fingerprint/tls_verification/client_cert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
383389
proxy.type = "http";
384390
proxy.tls = true;
385391
handleShadowTLS();
386392
}
387-
h2_connect = tag equals "h2-connect" address (username password)? (usernamek passwordk)? (headers/max_streams/sni/tls_fingerprint/tls_verification/client_cert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
393+
h2_connect = tag equals "h2-connect" address (username password)? (usernamek passwordk)? (headers/max_streams/sni/alpn/tls_fingerprint/tls_verification/client_cert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/fast_open/tfo/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
388394
proxy.type = "h2-connect";
389395
proxy.tls = true;
390396
handleShadowTLS();
@@ -420,24 +426,24 @@ wireguard = tag equals "wireguard" (section_name/no_error_alert/ip_version/under
420426
proxy.type = "wireguard-surge";
421427
handleShadowTLS();
422428
}
423-
hysteria2 = tag equals "hysteria2" address (no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/sni/tls_verification/client_cert/passwordk/tls_fingerprint/download_bandwidth/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/port_hopping_interval/salamander_password/gecko_password/others)* {
429+
hysteria2 = tag equals "hysteria2" address (no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/sni/alpn/tls_verification/client_cert/passwordk/tls_fingerprint/download_bandwidth/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/port_hopping_interval/salamander_password/gecko_password/others)* {
424430
proxy.type = "hysteria2";
425431
handleShadowTLS();
426432
}
427433
socks5 = tag equals "socks5" address (username password)? (usernamek passwordk)? (udp_relay/no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/fast_open/tfo/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
428434
proxy.type = "socks5";
429435
handleShadowTLS();
430436
}
431-
socks5_tls = tag equals "socks5-tls" address (username password)? (usernamek passwordk)? (udp_relay/no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/sni/tls_fingerprint/tls_verification/client_cert/fast_open/tfo/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
437+
socks5_tls = tag equals "socks5-tls" address (username password)? (usernamek passwordk)? (udp_relay/no_error_alert/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/sni/alpn/tls_fingerprint/tls_verification/client_cert/fast_open/tfo/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
432438
proxy.type = "socks5";
433439
proxy.tls = true;
434440
handleShadowTLS();
435441
}
436-
anytls = tag equals "anytls" address (passwordk/reuse/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls_fingerprint/tls_verification/client_cert/sni/fast_open/tfo/block_quic/others)* {
442+
anytls = tag equals "anytls" address (passwordk/reuse/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls_fingerprint/tls_verification/client_cert/sni/alpn/fast_open/tfo/block_quic/others)* {
437443
proxy.type = "anytls";
438444
proxy.tls = true;
439445
}
440-
trust_tunnel = tag equals "trust-tunnel" address (usernamek/passwordk/headers/max_streams/reuse/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls_fingerprint/tls_verification/client_cert/sni/fast_open/tfo/block_quic/others)* {
446+
trust_tunnel = tag equals "trust-tunnel" address (usernamek/passwordk/headers/max_streams/reuse/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls_fingerprint/tls_verification/client_cert/sni/alpn/fast_open/tfo/block_quic/others)* {
441447
proxy.type = "trusttunnel";
442448
proxy.tls = true;
443449
}
@@ -592,7 +598,11 @@ shadow_tls_version = comma "shadow-tls-version" equals match:$[0-9]+ { proxy["sh
592598
shadow_tls_sni = comma "shadow-tls-sni" equals match:[^,]+ { proxy["shadow-tls-sni"] = match.join(""); }
593599
shadow_tls_password = comma "shadow-tls-password" equals match:[^,]+ { proxy["shadow-tls-password"] = match.join("").replace(/^"(.*?)"$/, '$1').replace(/^'(.*?)'$/, '$1'); }
594600
token = comma "token" equals match:[^,]+ { proxy.token = match.join(""); }
595-
alpn = comma "alpn" equals match:[^,]+ { proxy.alpn = match.join(""); }
601+
alpn = comma "alpn" equals match:quoted_value {
602+
const values = parseAlpn(match);
603+
if (values.length > 0) proxy.alpn = values;
604+
}
605+
quoted_value = '"' match:$[^"]* '"' { return match; } / "'" match:$[^']* "'" { return match; } / match:$[^,]+ { return match; }
596606
uuidk = comma "uuid" equals match:[^,]+ { proxy.uuid = match.join(""); }
597607
salamander_password = comma "salamander-password" equals match:[^,]+ { proxy['obfs-password'] = match.join("").replace(/^"(.*?)"$/, '$1').replace(/^'(.*?)'$/, '$1'); proxy.obfs = 'salamander'; }
598608
gecko_password = comma "gecko-password" equals match:[^,]+ { proxy['obfs-password'] = match.join("").replace(/^"(.*?)"$/, '$1').replace(/^'(.*?)'$/, '$1'); proxy.obfs = 'gecko'; }

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

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,23 @@ function hasNonBlankValue(value) {
4848
return value != null && `${value}`.trim().length > 0;
4949
}
5050

51+
function formatSurgeAlpn(alpn) {
52+
const values = Array.isArray(alpn)
53+
? alpn
54+
: stripSurgeQuotes(`${alpn || ''}`).split(',');
55+
56+
return values
57+
.filter((item) => item != null)
58+
.map((item) => String(stripSurgeQuotes(item)).trim())
59+
.filter((item) => item !== '')
60+
.join(',');
61+
}
62+
63+
function appendAlpn(result, proxy) {
64+
const alpn = formatSurgeAlpn(proxy.alpn);
65+
if (alpn) result.append(`,alpn="${alpn}"`);
66+
}
67+
5168
function appendTlsProxyParams(result, proxy, enabled = true) {
5269
if (!enabled) {
5370
return;
@@ -58,6 +75,7 @@ function appendTlsProxyParams(result, proxy, enabled = true) {
5875
'tls-fingerprint',
5976
);
6077
result.appendIfPresent(`,sni="${proxy.sni}"`, 'sni');
78+
appendAlpn(result, proxy);
6179
result.appendIfPresent(
6280
`,skip-cert-verify=${proxy['skip-cert-verify']}`,
6381
'skip-cert-verify',
@@ -1039,11 +1057,6 @@ function tuic(proxy) {
10391057
result.appendIfPresent(`,password="${proxy.password}"`, 'password');
10401058
result.appendIfPresent(`,token=${proxy.token}`, 'token');
10411059

1042-
result.appendIfPresent(
1043-
`,alpn=${Array.isArray(proxy.alpn) ? proxy.alpn[0] : proxy.alpn}`,
1044-
'alpn',
1045-
);
1046-
10471060
if (hasNonBlankValue(proxy.ports)) {
10481061
result.append(
10491062
`,port-hopping="${String(proxy.ports).replace(/,/g, ';')}"`,

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3741,5 +3741,24 @@ describe('Platform raw-format parser coverage', function () {
37413741
},
37423742
},
37433743
]);
3744+
3745+
it('parses quoted alpn lists for Surge TLS protocol lines', function () {
3746+
const alpn = ['http/1.1', 'h2', 'h3'];
3747+
const cases = [
3748+
`Surge VMess ALPN = vmess,vmess-alpn.example.com,443,username=${UUID},tls=true,vmess-aead=true,alpn="http/1.1,h2,h3"`,
3749+
`Surge Trojan ALPN = trojan,trojan-alpn.example.com,443,password=secret,alpn='http/1.1,h2,h3'`,
3750+
`Surge HTTPS ALPN = https,https-alpn.example.com,443,alpn="http/1.1,h2,h3"`,
3751+
`Surge H2 ALPN = h2-connect,h2-alpn.example.com,443,alpn='http/1.1,h2,h3'`,
3752+
`Surge SOCKS5 ALPN = socks5-tls,socks-alpn.example.com,1080,alpn="http/1.1,h2,h3"`,
3753+
`Surge AnyTLS ALPN = anytls,anytls-alpn.example.com,443,password=secret,alpn='http/1.1,h2,h3'`,
3754+
`Surge TrustTunnel ALPN = trust-tunnel,trust-alpn.example.com,443,alpn="http/1.1,h2,h3"`,
3755+
`Surge TUIC ALPN = tuic-v5,tuic-alpn.example.com,443,uuid=${UUID},password=secret,alpn='http/1.1,h2,h3'`,
3756+
`Surge Hysteria2 ALPN = hysteria2,hy2-alpn.example.com,443,password=secret,alpn="http/1.1,h2,h3"`,
3757+
];
3758+
3759+
for (const input of cases) {
3760+
expectSubset(parseOne(input), { alpn });
3761+
}
3762+
});
37443763
});
37453764
});

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

Lines changed: 114 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -831,6 +831,118 @@ describe('Proxy text producers', function () {
831831
);
832832
});
833833

834+
it('emits quoted Surge alpn for TLS protocol outputs', function () {
835+
const alpn = ['http/1.1', 'h2', 'h3'];
836+
const output = produceExternal('Surge', [
837+
{
838+
type: 'vmess',
839+
name: 'Surge VMess ALPN',
840+
server: 'vmess-alpn.example.com',
841+
port: 443,
842+
cipher: 'auto',
843+
uuid: UUID,
844+
alterId: 0,
845+
tls: true,
846+
alpn,
847+
},
848+
{
849+
type: 'trojan',
850+
name: 'Surge Trojan ALPN',
851+
server: 'trojan-alpn.example.com',
852+
port: 443,
853+
password: 'secret',
854+
alpn,
855+
},
856+
{
857+
type: 'http',
858+
name: 'Surge HTTPS ALPN',
859+
server: 'https-alpn.example.com',
860+
port: 443,
861+
tls: true,
862+
alpn,
863+
},
864+
{
865+
type: 'h2-connect',
866+
name: 'Surge H2 ALPN',
867+
server: 'h2-alpn.example.com',
868+
port: 443,
869+
alpn,
870+
},
871+
{
872+
type: 'socks5',
873+
name: 'Surge SOCKS5 ALPN',
874+
server: 'socks-alpn.example.com',
875+
port: 1080,
876+
tls: true,
877+
alpn,
878+
},
879+
{
880+
type: 'anytls',
881+
name: 'Surge AnyTLS ALPN',
882+
server: 'anytls-alpn.example.com',
883+
port: 443,
884+
password: 'secret',
885+
alpn,
886+
},
887+
{
888+
type: 'trusttunnel',
889+
name: 'Surge TrustTunnel ALPN',
890+
server: 'trust-alpn.example.com',
891+
port: 443,
892+
alpn,
893+
},
894+
{
895+
type: 'tuic',
896+
name: 'Surge TUIC ALPN',
897+
server: 'tuic-alpn.example.com',
898+
port: 443,
899+
uuid: UUID,
900+
password: 'secret',
901+
alpn,
902+
},
903+
{
904+
type: 'hysteria2',
905+
name: 'Surge Hysteria2 ALPN',
906+
server: 'hy2-alpn.example.com',
907+
port: 443,
908+
password: 'secret',
909+
alpn,
910+
},
911+
]);
912+
913+
expect(output.match(/alpn="http\/1\.1,h2,h3"/g)).to.have.length(9);
914+
});
915+
916+
it('omits Surge alpn for non-TLS outputs', function () {
917+
const output = produceExternal('Surge', [
918+
{
919+
type: 'vmess',
920+
name: 'Surge VMess Plain ALPN',
921+
server: 'vmess-plain-alpn.example.com',
922+
port: 80,
923+
uuid: UUID,
924+
alterId: 0,
925+
alpn: ['http/1.1', 'h2'],
926+
},
927+
{
928+
type: 'http',
929+
name: 'Surge HTTP Plain ALPN',
930+
server: 'http-plain-alpn.example.com',
931+
port: 80,
932+
alpn: ['http/1.1', 'h2'],
933+
},
934+
{
935+
type: 'socks5',
936+
name: 'Surge SOCKS5 Plain ALPN',
937+
server: 'socks-plain-alpn.example.com',
938+
port: 1080,
939+
alpn: ['http/1.1', 'h2'],
940+
},
941+
]);
942+
943+
expect(output).to.not.include('alpn=');
944+
});
945+
834946
it('produces Surge TUIC v5 lines with port hopping', function () {
835947
const output = produceExternal('Surge', {
836948
type: 'tuic',
@@ -847,7 +959,7 @@ describe('Proxy text producers', function () {
847959
});
848960

849961
expect(output).to.equal(
850-
`Surge TUIC=tuic-v5,tuic.example.com,443,uuid=${UUID},password="secret",alpn=h3,port-hopping="9000;9002-9004",sni="sni.example.com",skip-cert-verify=true,ecn=true`,
962+
`Surge TUIC=tuic-v5,tuic.example.com,443,uuid=${UUID},password="secret",port-hopping="9000;9002-9004",sni="sni.example.com",alpn="h3",skip-cert-verify=true,ecn=true`,
851963
);
852964
});
853965

@@ -880,7 +992,7 @@ describe('Proxy text producers', function () {
880992
]);
881993

882994
expect(output).to.equal(
883-
`Surge TUIC Plain=tuic-v5,tuic.example.com,443,uuid=${UUID},password="secret",alpn=h3,sni="sni.example.com",skip-cert-verify=true\nSurge Hysteria2 Plain=hysteria2,hy2.example.com,443,password="secret",sni="peer.example.com",skip-cert-verify=true`,
995+
`Surge TUIC Plain=tuic-v5,tuic.example.com,443,uuid=${UUID},password="secret",sni="sni.example.com",alpn="h3",skip-cert-verify=true\nSurge Hysteria2 Plain=hysteria2,hy2.example.com,443,password="secret",sni="peer.example.com",skip-cert-verify=true`,
884996
);
885997
});
886998

0 commit comments

Comments
 (0)