Skip to content

Commit 59fe16a

Browse files
committed
feat: Surge Hysteria2 与 TUIC 协议支持端口跳跃; Hysteria2 URI 的端口部分支持 端口跳跃 的「多端口地址格式」
1 parent 562d349 commit 59fe16a

File tree

7 files changed

+100
-21
lines changed

7 files changed

+100
-21
lines changed

README.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,18 @@
1010
Advanced Subscription Manager for QX, Loon, Surge, Stash and Shadowrocket.
1111
</p>
1212

13-
[![Build](https://github.com/sub-store-org/Sub-Store/actions/workflows/main.yml/badge.svg)](https://github.com/sub-store-org/Sub-Store/actions/workflows/main.yml) ![GitHub](https://img.shields.io/github/license/sub-store-org/Sub-Store) ![GitHub issues](https://img.shields.io/github/issues/sub-store-org/Sub-Store) ![GitHub closed pull requests](https://img.shields.io/github/issues-pr-closed-raw/Peng-Ym/Sub-Store) ![Lines of code](https://img.shields.io/tokei/lines/github/sub-store-org/Sub-Store) ![Size](https://img.shields.io/github/languages/code-size/sub-store-org/Sub-Store)
13+
[![Build](https://github.com/sub-store-org/Sub-Store/actions/workflows/main.yml/badge.svg)](https://github.com/sub-store-org/Sub-Store/actions/workflows/main.yml) ![GitHub](https://img.shields.io/github/license/sub-store-org/Sub-Store) ![GitHub issues](https://img.shields.io/github/issues/sub-store-org/Sub-Store) ![GitHub closed pull requests](https://img.shields.io/github/issues-pr-closed-raw/Peng-Ym/Sub-Store) ![Lines of code](https://img.shields.io/tokei/lines/github/sub-store-org/Sub-Store) ![Size](https://img.shields.io/github/languages/code-size/sub-store-org/Sub-Store)
1414
<a href="https://trendshift.io/repositories/4572" target="_blank"><img src="https://trendshift.io/api/badge/repositories/4572" alt="sub-store-org%2FSub-Store | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
1515
[!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/PengYM)
16-
16+
1717
Core functionalities:
1818

1919
1. Conversion among various formats.
2020
2. Subscription formatting.
2121
3. Collect multiple subscriptions in one URL.
2222

2323
> The following descriptions of features may not be updated in real-time. Please refer to the actual available features for accurate information.
24-
24+
2525
## 1. Subscription Conversion
2626

2727
### Supported Input Formats
@@ -98,7 +98,7 @@ or
9898
esbuild(experimental)
9999

100100
```
101-
pnpm run --parallel "/^dev:.*/"
101+
SUB_STORE_BACKEND_API_PORT=3000 pnpm run --parallel "/^dev:.*/"
102102
```
103103

104104
## LICENSE
@@ -111,7 +111,6 @@ This project is under the GPL V3 LICENSE.
111111

112112
[![Star History Chart](https://api.star-history.com/svg?repos=sub-store-org/sub-store&type=Date)](https://star-history.com/#sub-store-org/sub-store&Date)
113113

114-
115114
## Acknowledgements
116115

117116
- Special thanks to @KOP-XIAO for his awesome resource-parser. Please give a [star](https://github.com/KOP-XIAO/QuantumultX) for his great work!

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.14.368",
3+
"version": "2.14.370",
44
"description": "Advanced Subscription Manager for QX, Loon, Surge, Stash and ShadowRocket.",
55
"main": "src/main.js",
66
"scripts": {

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -424,8 +424,12 @@ function lastParse(proxy) {
424424
proxy[`${proxy.network}-opts`].path = [transportPath];
425425
}
426426
}
427-
if (['hysteria', 'hysteria2'].includes(proxy.type) && !proxy.ports) {
428-
delete proxy.ports;
427+
if (['hysteria', 'hysteria2'].includes(proxy.type)) {
428+
if (proxy.ports) {
429+
proxy.ports = proxy.ports.replace(/\//g, ',');
430+
} else {
431+
delete proxy.ports;
432+
}
429433
}
430434
if (
431435
['hysteria2'].includes(proxy.type) &&

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

Lines changed: 61 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
isPresent,
66
isNotBlank,
77
getIfPresent,
8+
getRandomPort,
89
} from '@/utils';
910
import getSurgeParser from './peggy/surge';
1011
import getLoonParser from './peggy/loon';
@@ -13,6 +14,19 @@ import getTrojanURIParser from './peggy/trojan-uri';
1314

1415
import { Base64 } from 'js-base64';
1516

17+
function surge_port_hopping(raw) {
18+
const [parts, port_hopping] =
19+
raw.match(
20+
/,\s*?port-hopping\s*?=\s*?["']?\s*?((\d+(-\d+)?)([,;]\d+(-\d+)?)*)\s*?["']?\s*?/,
21+
) || [];
22+
return {
23+
port_hopping: port_hopping
24+
? port_hopping.replace(/;/g, ',')
25+
: undefined,
26+
line: parts ? raw.replace(parts, '') : raw,
27+
};
28+
}
29+
1630
// Parse SS URI format (only supports new SIP002, legacy format is depreciated).
1731
// reference: https://github.com/shadowsocks/shadowsocks-org/wiki/SIP002-URI-Scheme
1832
function URI_SS() {
@@ -545,13 +559,42 @@ function URI_Hysteria2() {
545559
};
546560
const parse = (line) => {
547561
line = line.split(/(hysteria2|hy2):\/\//)[2];
548-
// eslint-disable-next-line no-unused-vars
549-
let [__, password, server, ___, port, ____, addons = '', name] =
550-
/^(.*?)@(.*?)(:(\d+))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(line);
551-
port = parseInt(`${port}`, 10);
552-
if (isNaN(port)) {
562+
// 端口跳跃有两种写法:
563+
// 1. 服务器的地址和可选端口。如果省略端口,则默认为 443。
564+
// 端口部分支持 端口跳跃 的「多端口地址格式」。
565+
// https://hysteria.network/zh/docs/advanced/Port-Hopping
566+
// 2. 参数 mport
567+
let ports;
568+
/* eslint-disable no-unused-vars */
569+
let [
570+
__,
571+
password,
572+
server,
573+
___,
574+
port,
575+
____,
576+
_____,
577+
______,
578+
_______,
579+
________,
580+
addons = '',
581+
name,
582+
] = /^(.*?)@(.*?)(:((\d+(-\d+)?)([,;]\d+(-\d+)?)*))?\/?(\?(.*?))?(?:#(.*?))?$/.exec(
583+
line,
584+
);
585+
/* eslint-enable no-unused-vars */
586+
if (/^\d+$/.test(port)) {
587+
port = parseInt(`${port}`, 10);
588+
if (isNaN(port)) {
589+
port = 443;
590+
}
591+
} else if (port) {
592+
ports = port;
593+
port = getRandomPort(ports);
594+
} else {
553595
port = 443;
554596
}
597+
555598
password = decodeURIComponent(password);
556599
if (name != null) {
557600
name = decodeURIComponent(name);
@@ -563,6 +606,7 @@ function URI_Hysteria2() {
563606
name,
564607
server,
565608
port,
609+
ports,
566610
password,
567611
};
568612

@@ -1295,7 +1339,12 @@ function Surge_Tuic() {
12951339
const test = (line) => {
12961340
return /^.*=\s*tuic(-v5)?/.test(line.split(',')[0]);
12971341
};
1298-
const parse = (line) => getSurgeParser().parse(line);
1342+
const parse = (raw) => {
1343+
const { port_hopping, line } = surge_port_hopping(raw);
1344+
const proxy = getSurgeParser().parse(line);
1345+
proxy['ports'] = port_hopping;
1346+
return proxy;
1347+
};
12991348
return { name, test, parse };
13001349
}
13011350
function Surge_WireGuard() {
@@ -1312,7 +1361,12 @@ function Surge_Hysteria2() {
13121361
const test = (line) => {
13131362
return /^.*=\s*hysteria2/.test(line.split(',')[0]);
13141363
};
1315-
const parse = (line) => getSurgeParser().parse(line);
1364+
const parse = (raw) => {
1365+
const { port_hopping, line } = surge_port_hopping(raw);
1366+
const proxy = getSurgeParser().parse(line);
1367+
proxy['ports'] = port_hopping;
1368+
return proxy;
1369+
};
13161370
return { name, test, parse };
13171371
}
13181372

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,11 +91,11 @@ snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_
9191
}
9292
handleShadowTLS();
9393
}
94-
tuic = tag equals "tuic" address (alpn/token/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls_fingerprint/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
94+
tuic = tag equals "tuic" address (alpn/token/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls_fingerprint/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/port_hopping_interval/others)* {
9595
proxy.type = "tuic";
9696
handleShadowTLS();
9797
}
98-
tuic_v5 = tag equals "tuic-v5" address (alpn/passwordk/uuidk/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls_fingerprint/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
98+
tuic_v5 = tag equals "tuic-v5" address (alpn/passwordk/uuidk/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls_fingerprint/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/port_hopping_interval/others)* {
9999
proxy.type = "tuic";
100100
proxy.version = 5;
101101
handleShadowTLS();
@@ -104,7 +104,7 @@ wireguard = tag equals "wireguard" (section_name/no_error_alert/ip_version/under
104104
proxy.type = "wireguard-surge";
105105
handleShadowTLS();
106106
}
107-
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/passwordk/tls_fingerprint/download_bandwidth/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
107+
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/passwordk/tls_fingerprint/download_bandwidth/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/port_hopping_interval/others)* {
108108
proxy.type = "hysteria2";
109109
handleShadowTLS();
110110
}
@@ -151,6 +151,8 @@ port = digits:[0-9]+ {
151151
}
152152
}
153153
154+
port_hopping_interval = comma "port-hopping-interval" equals match:$[0-9]+ { proxy["hop-interval"] = parseInt(match.trim()); }
155+
154156
username = & {
155157
let j = peg$currPos;
156158
let start, end;

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,11 +89,11 @@ snell = tag equals "snell" address (snell_version/snell_psk/obfs/obfs_host/obfs_
8989
}
9090
handleShadowTLS();
9191
}
92-
tuic = tag equals "tuic" address (alpn/token/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls_fingerprint/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
92+
tuic = tag equals "tuic" address (alpn/token/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls_fingerprint/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/port_hopping_interval/others)* {
9393
proxy.type = "tuic";
9494
handleShadowTLS();
9595
}
96-
tuic_v5 = tag equals "tuic-v5" address (alpn/passwordk/uuidk/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls_fingerprint/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
96+
tuic_v5 = tag equals "tuic-v5" address (alpn/passwordk/uuidk/ip_version/underlying_proxy/tos/allow_other_interface/interface/test_url/test_udp/test_timeout/hybrid/no_error_alert/tls_fingerprint/tls_verification/sni/fast_open/tfo/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/port_hopping_interval/others)* {
9797
proxy.type = "tuic";
9898
proxy.version = 5;
9999
handleShadowTLS();
@@ -102,7 +102,7 @@ wireguard = tag equals "wireguard" (section_name/no_error_alert/ip_version/under
102102
proxy.type = "wireguard-surge";
103103
handleShadowTLS();
104104
}
105-
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/passwordk/tls_fingerprint/download_bandwidth/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/others)* {
105+
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/passwordk/tls_fingerprint/download_bandwidth/ecn/shadow_tls_version/shadow_tls_sni/shadow_tls_password/block_quic/port_hopping_interval/others)* {
106106
proxy.type = "hysteria2";
107107
handleShadowTLS();
108108
}
@@ -149,6 +149,8 @@ port = digits:[0-9]+ {
149149
}
150150
}
151151

152+
port_hopping_interval = comma "port-hopping-interval" equals match:$[0-9]+ { proxy["hop-interval"] = parseInt(match.trim()); }
153+
152154
username = & {
153155
let j = peg$currPos;
154156
let start, end;

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -675,6 +675,15 @@ function tuic(proxy) {
675675
'alpn',
676676
);
677677

678+
if (isPresent(proxy, 'ports')) {
679+
result.append(`,port-hopping=${proxy.ports.replace(/,/g, ';')}`);
680+
}
681+
682+
result.appendIfPresent(
683+
`,port-hopping-interval=${proxy['hop-interval']}`,
684+
'hop-interval',
685+
);
686+
678687
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
679688
result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
680689

@@ -935,6 +944,15 @@ function hysteria2(proxy) {
935944

936945
result.appendIfPresent(`,password=${proxy.password}`, 'password');
937946

947+
if (isPresent(proxy, 'ports')) {
948+
result.append(`,port-hopping=${proxy.ports.replace(/,/g, ';')}`);
949+
}
950+
951+
result.appendIfPresent(
952+
`,port-hopping-interval=${proxy['hop-interval']}`,
953+
'hop-interval',
954+
);
955+
938956
const ip_version = ipVersions[proxy['ip-version']] || proxy['ip-version'];
939957
result.appendIfPresent(`,ip-version=${ip_version}`, 'ip-version');
940958

0 commit comments

Comments
 (0)