Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/MHSanaei/3x-ui.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMHSanaei <ho3ein.sanaei@gmail.com>2026-01-18 19:38:05 +0300
committerMHSanaei <ho3ein.sanaei@gmail.com>2026-01-18 19:38:05 +0300
commit93b7ce199fb7434ea55e669cae43ace44943f112 (patch)
tree04200fbc2f5d92ed0dfc70a3161ea082508fc6a2
parent2a76cec804ca28e1f128f99ec9c3c5b48474053c (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.
-rw-r--r--web/assets/js/model/outbound.js63
-rw-r--r--web/html/form/outbound.html30
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&param2=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" }}'>