diff options
| author | MHSanaei <ho3ein.sanaei@gmail.com> | 2026-01-18 19:38:05 +0300 |
|---|---|---|
| committer | MHSanaei <ho3ein.sanaei@gmail.com> | 2026-01-18 19:38:05 +0300 |
| commit | 93b7ce199fb7434ea55e669cae43ace44943f112 (patch) | |
| tree | 04200fbc2f5d92ed0dfc70a3161ea082508fc6a2 /web | |
| parent | 2a76cec804ca28e1f128f99ec9c3c5b48474053c (diff) | |
Add UDP mask support for Hysteria outbound
Introduces a 'congestion' option to Hysteria stream settings and updates the form to allow selection between BBR (Auto) and Brutal. Adds support for UDP masks, including model, serialization, and UI for adding/removing masks with type and password fields.
Diffstat (limited to 'web')
| -rw-r--r-- | web/assets/js/model/outbound.js | 63 | ||||
| -rw-r--r-- | web/html/form/outbound.html | 30 |
2 files changed, 83 insertions, 10 deletions
diff --git a/web/assets/js/model/outbound.js b/web/assets/js/model/outbound.js index 01f054ac..c6529560 100644 --- a/web/assets/js/model/outbound.js +++ b/web/assets/js/model/outbound.js @@ -430,6 +430,7 @@ class HysteriaStreamSettings extends CommonClass { constructor( version = 2, auth = '', + congestion = '', up = '0', down = '0', udphopPort = '', @@ -445,6 +446,7 @@ class HysteriaStreamSettings extends CommonClass { super(); this.version = version; this.auth = auth; + this.congestion = congestion; this.up = up; this.down = down; this.udphopPort = udphopPort; @@ -468,6 +470,7 @@ class HysteriaStreamSettings extends CommonClass { return new HysteriaStreamSettings( json.version, json.auth, + json.congestion, json.up, json.down, udphopPort, @@ -486,6 +489,7 @@ class HysteriaStreamSettings extends CommonClass { const result = { version: this.version, auth: this.auth, + congestion: this.congestion, up: this.up, down: this.down, initStreamReceiveWindow: this.initStreamReceiveWindow, @@ -554,6 +558,30 @@ class SockoptStreamSettings extends CommonClass { } } +class UdpMask extends CommonClass { + constructor(type = 'salamander', password = '') { + super(); + this.type = type; + this.password = password; + } + + static fromJson(json = {}) { + return new UdpMask( + json.type, + json.settings?.password || '' + ); + } + + toJson() { + return { + type: this.type, + settings: { + password: this.password + } + }; + } +} + class StreamSettings extends CommonClass { constructor( network = 'tcp', @@ -567,6 +595,7 @@ class StreamSettings extends CommonClass { httpupgradeSettings = new HttpUpgradeStreamSettings(), xhttpSettings = new xHTTPStreamSettings(), hysteriaSettings = new HysteriaStreamSettings(), + udpmasks = [], sockopt = undefined, ) { super(); @@ -581,9 +610,18 @@ class StreamSettings extends CommonClass { this.httpupgrade = httpupgradeSettings; this.xhttp = xhttpSettings; this.hysteria = hysteriaSettings; + this.udpmasks = udpmasks; this.sockopt = sockopt; } + addUdpMask() { + this.udpmasks.push(new UdpMask()); + } + + delUdpMask(index) { + this.udpmasks.splice(index, 1); + } + get isTls() { return this.security === 'tls'; } @@ -601,6 +639,7 @@ class StreamSettings extends CommonClass { } static fromJson(json = {}) { + const udpmasks = json.udpmasks ? json.udpmasks.map(mask => UdpMask.fromJson(mask)) : []; return new StreamSettings( json.network, json.security, @@ -613,6 +652,7 @@ class StreamSettings extends CommonClass { HttpUpgradeStreamSettings.fromJson(json.httpupgradeSettings), xHTTPStreamSettings.fromJson(json.xhttpSettings), HysteriaStreamSettings.fromJson(json.hysteriaSettings), + udpmasks, SockoptStreamSettings.fromJson(json.sockopt), ); } @@ -631,6 +671,7 @@ class StreamSettings extends CommonClass { httpupgradeSettings: network === 'httpupgrade' ? this.httpupgrade.toJson() : undefined, xhttpSettings: network === 'xhttp' ? this.xhttp.toJson() : undefined, hysteriaSettings: network === 'hysteria' ? this.hysteria.toJson() : undefined, + udpmasks: this.udpmasks.length > 0 ? this.udpmasks.map(mask => mask.toJson()) : undefined, sockopt: this.sockopt != undefined ? this.sockopt.toJson() : undefined, }; } @@ -694,7 +735,8 @@ class Outbound extends CommonClass { } canEnableTls() { - if (![Protocols.VMess, Protocols.VLESS, Protocols.Trojan, Protocols.Shadowsocks].includes(this.protocol)) return false; + if (![Protocols.VMess, Protocols.VLESS, Protocols.Trojan, Protocols.Shadowsocks, Protocols.Hysteria].includes(this.protocol)) return false; + if (this.protocol === Protocols.Hysteria) return this.stream.network === 'hysteria'; return ["tcp", "ws", "http", "grpc", "httpupgrade", "xhttp"].includes(this.stream.network); } @@ -936,25 +978,26 @@ class Outbound extends CommonClass { // Parse hysteria2://password@address:port[?param1=value1¶m2=value2...][#remarks] const regex = /^hysteria2?:\/\/([^@]+)@([^:?#]+):(\d+)([^#]*)(#.*)?$/; const match = link.match(regex); - + if (!match) return null; - + let [, password, address, port, params, hash] = match; port = parseInt(port); - + // Parse URL parameters if present let urlParams = new URLSearchParams(params); - + // Create stream settings with hysteria network let stream = new StreamSettings('hysteria', 'none'); - + // Set hysteria stream settings stream.hysteria.auth = password; + stream.hysteria.congestion = urlParams.get('congestion') ?? ''; stream.hysteria.up = urlParams.get('up') ?? '0'; stream.hysteria.down = urlParams.get('down') ?? '0'; stream.hysteria.udphopPort = urlParams.get('udphopPort') ?? ''; stream.hysteria.udphopInterval = parseInt(urlParams.get('udphopInterval') ?? '30'); - + // Optional QUIC parameters if (urlParams.has('initStreamReceiveWindow')) { stream.hysteria.initStreamReceiveWindow = parseInt(urlParams.get('initStreamReceiveWindow')); @@ -977,13 +1020,13 @@ class Outbound extends CommonClass { if (urlParams.has('disablePathMTUDiscovery')) { stream.hysteria.disablePathMTUDiscovery = urlParams.get('disablePathMTUDiscovery') === 'true'; } - + // Create settings let settings = new Outbound.HysteriaSettings(address, port, 2); - + // Extract remark from hash let remark = hash ? decodeURIComponent(hash.substring(1)) : `out-hysteria-${port}`; - + return new Outbound(remark, Protocols.Hysteria, settings, stream); } } diff --git a/web/html/form/outbound.html b/web/html/form/outbound.html index 2396052a..24bdc751 100644 --- a/web/html/form/outbound.html +++ b/web/html/form/outbound.html @@ -545,6 +545,12 @@ <a-form-item label='Auth Password'> <a-input v-model.trim="outbound.stream.hysteria.auth"></a-input> </a-form-item> + <a-form-item label='Congestion'> + <a-select v-model="outbound.stream.hysteria.congestion" :dropdown-class-name="themeSwitcher.currentTheme"> + <a-select-option value="">BBR (Auto)</a-select-option> + <a-select-option value="brutal">Brutal</a-select-option> + </a-select> + </a-form-item> <a-form-item label='Upload Speed'> <a-input v-model.trim="outbound.stream.hysteria.up" placeholder="0 (BBR mode), e.g., 100 mbps"></a-input> @@ -596,6 +602,30 @@ </template> </template> + <!-- udpmasks settings --> + <template v-if="outbound.canEnableStream()"> + <a-form-item label="UDP Masks"> + <a-button icon="plus" type="primary" size="small" @click="outbound.stream.addUdpMask()"></a-button> + </a-form-item> + <template v-if="outbound.stream.udpmasks.length > 0"> + <a-form v-for="(mask, index) in outbound.stream.udpmasks" :key="index" :colon="false" + :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }"> + <a-divider :style="{ margin: '0' }"> UDP Mask [[ index + 1 ]] + <a-icon type="delete" @click="() => outbound.stream.delUdpMask(index)" + :style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"></a-icon> + </a-divider> + <a-form-item label='Type'> + <a-select v-model="mask.type" :dropdown-class-name="themeSwitcher.currentTheme"> + <a-select-option value="salamander">Salamander</a-select-option> + </a-select> + </a-form-item> + <a-form-item label='Password'> + <a-input v-model.trim="mask.password" placeholder="Obfuscation password"></a-input> + </a-form-item> + </a-form> + </template> + </template> + <!-- tls settings --> <template v-if="outbound.canEnableTls()"> <a-form-item label='{{ i18n "security" }}'> |
