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:13:34 +0300
committerMHSanaei <ho3ein.sanaei@gmail.com>2026-01-18 19:13:34 +0300
commit2a76cec804ca28e1f128f99ec9c3c5b48474053c (patch)
tree3a1b5bd941e42aa283f181e92c857869bc9e2014
parent88eab032be7197dd87d0b4c9864af4b6b4fd820b (diff)
Add Hysteria2 outbound protocol support
Introduces support for the Hysteria2 protocol in outbound settings, including model, parsing, and form UI integration. Adds Hysteria2-specific stream and protocol settings, updates protocol selection, and enables configuration of Hysteria2 parameters in the outbound form.
-rw-r--r--web/assets/js/model/outbound.js178
-rw-r--r--web/html/form/outbound.html366
2 files changed, 441 insertions, 103 deletions
diff --git a/web/assets/js/model/outbound.js b/web/assets/js/model/outbound.js
index 6fe34982..01f054ac 100644
--- a/web/assets/js/model/outbound.js
+++ b/web/assets/js/model/outbound.js
@@ -8,7 +8,8 @@ const Protocols = {
Shadowsocks: "shadowsocks",
Socks: "socks",
HTTP: "http",
- Wireguard: "wireguard"
+ Wireguard: "wireguard",
+ Hysteria: "hysteria"
};
const SSMethods = {
@@ -424,6 +425,86 @@ class RealityStreamSettings extends CommonClass {
};
}
};
+
+class HysteriaStreamSettings extends CommonClass {
+ constructor(
+ version = 2,
+ auth = '',
+ up = '0',
+ down = '0',
+ udphopPort = '',
+ udphopInterval = 30,
+ initStreamReceiveWindow = 8388608,
+ maxStreamReceiveWindow = 8388608,
+ initConnectionReceiveWindow = 20971520,
+ maxConnectionReceiveWindow = 20971520,
+ maxIdleTimeout = 30,
+ keepAlivePeriod = 0,
+ disablePathMTUDiscovery = false
+ ) {
+ super();
+ this.version = version;
+ this.auth = auth;
+ this.up = up;
+ this.down = down;
+ this.udphopPort = udphopPort;
+ this.udphopInterval = udphopInterval;
+ this.initStreamReceiveWindow = initStreamReceiveWindow;
+ this.maxStreamReceiveWindow = maxStreamReceiveWindow;
+ this.initConnectionReceiveWindow = initConnectionReceiveWindow;
+ this.maxConnectionReceiveWindow = maxConnectionReceiveWindow;
+ this.maxIdleTimeout = maxIdleTimeout;
+ this.keepAlivePeriod = keepAlivePeriod;
+ this.disablePathMTUDiscovery = disablePathMTUDiscovery;
+ }
+
+ static fromJson(json = {}) {
+ let udphopPort = '';
+ let udphopInterval = 30;
+ if (json.udphop) {
+ udphopPort = json.udphop.port || '';
+ udphopInterval = json.udphop.interval || 30;
+ }
+ return new HysteriaStreamSettings(
+ json.version,
+ json.auth,
+ json.up,
+ json.down,
+ udphopPort,
+ udphopInterval,
+ json.initStreamReceiveWindow,
+ json.maxStreamReceiveWindow,
+ json.initConnectionReceiveWindow,
+ json.maxConnectionReceiveWindow,
+ json.maxIdleTimeout,
+ json.keepAlivePeriod,
+ json.disablePathMTUDiscovery
+ );
+ }
+
+ toJson() {
+ const result = {
+ version: this.version,
+ auth: this.auth,
+ up: this.up,
+ down: this.down,
+ initStreamReceiveWindow: this.initStreamReceiveWindow,
+ maxStreamReceiveWindow: this.maxStreamReceiveWindow,
+ initConnectionReceiveWindow: this.initConnectionReceiveWindow,
+ maxConnectionReceiveWindow: this.maxConnectionReceiveWindow,
+ maxIdleTimeout: this.maxIdleTimeout,
+ keepAlivePeriod: this.keepAlivePeriod,
+ disablePathMTUDiscovery: this.disablePathMTUDiscovery
+ };
+ if (this.udphopPort) {
+ result.udphop = {
+ port: this.udphopPort,
+ interval: this.udphopInterval
+ };
+ }
+ return result;
+ }
+};
class SockoptStreamSettings extends CommonClass {
constructor(
dialerProxy = "",
@@ -485,6 +566,7 @@ class StreamSettings extends CommonClass {
grpcSettings = new GrpcStreamSettings(),
httpupgradeSettings = new HttpUpgradeStreamSettings(),
xhttpSettings = new xHTTPStreamSettings(),
+ hysteriaSettings = new HysteriaStreamSettings(),
sockopt = undefined,
) {
super();
@@ -498,6 +580,7 @@ class StreamSettings extends CommonClass {
this.grpc = grpcSettings;
this.httpupgrade = httpupgradeSettings;
this.xhttp = xhttpSettings;
+ this.hysteria = hysteriaSettings;
this.sockopt = sockopt;
}
@@ -529,6 +612,7 @@ class StreamSettings extends CommonClass {
GrpcStreamSettings.fromJson(json.grpcSettings),
HttpUpgradeStreamSettings.fromJson(json.httpupgradeSettings),
xHTTPStreamSettings.fromJson(json.xhttpSettings),
+ HysteriaStreamSettings.fromJson(json.hysteriaSettings),
SockoptStreamSettings.fromJson(json.sockopt),
);
}
@@ -546,6 +630,7 @@ class StreamSettings extends CommonClass {
grpcSettings: network === 'grpc' ? this.grpc.toJson() : undefined,
httpupgradeSettings: network === 'httpupgrade' ? this.httpupgrade.toJson() : undefined,
xhttpSettings: network === 'xhttp' ? this.xhttp.toJson() : undefined,
+ hysteriaSettings: network === 'hysteria' ? this.hysteria.toJson() : undefined,
sockopt: this.sockopt != undefined ? this.sockopt.toJson() : undefined,
};
}
@@ -634,7 +719,7 @@ class Outbound extends CommonClass {
}
canEnableStream() {
- return [Protocols.VMess, Protocols.VLESS, Protocols.Trojan, Protocols.Shadowsocks].includes(this.protocol);
+ return [Protocols.VMess, Protocols.VLESS, Protocols.Trojan, Protocols.Shadowsocks, Protocols.Hysteria].includes(this.protocol);
}
canEnableMux() {
@@ -673,7 +758,8 @@ class Outbound extends CommonClass {
Protocols.Trojan,
Protocols.Shadowsocks,
Protocols.Socks,
- Protocols.HTTP
+ Protocols.HTTP,
+ Protocols.Hysteria
].includes(this.protocol);
}
@@ -722,6 +808,9 @@ class Outbound extends CommonClass {
case Protocols.Trojan:
case 'ss':
return this.fromParamLink(link);
+ case 'hysteria2':
+ case Protocols.Hysteria:
+ return this.fromHysteriaLink(link);
default:
return null;
}
@@ -842,6 +931,61 @@ class Outbound extends CommonClass {
remark = remark.length > 0 ? remark.substring(1) : 'out-' + protocol + '-' + port;
return new Outbound(remark, protocol, settings, stream);
}
+
+ static fromHysteriaLink(link) {
+ // 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.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'));
+ }
+ if (urlParams.has('maxStreamReceiveWindow')) {
+ stream.hysteria.maxStreamReceiveWindow = parseInt(urlParams.get('maxStreamReceiveWindow'));
+ }
+ if (urlParams.has('initConnectionReceiveWindow')) {
+ stream.hysteria.initConnectionReceiveWindow = parseInt(urlParams.get('initConnectionReceiveWindow'));
+ }
+ if (urlParams.has('maxConnectionReceiveWindow')) {
+ stream.hysteria.maxConnectionReceiveWindow = parseInt(urlParams.get('maxConnectionReceiveWindow'));
+ }
+ if (urlParams.has('maxIdleTimeout')) {
+ stream.hysteria.maxIdleTimeout = parseInt(urlParams.get('maxIdleTimeout'));
+ }
+ if (urlParams.has('keepAlivePeriod')) {
+ stream.hysteria.keepAlivePeriod = parseInt(urlParams.get('keepAlivePeriod'));
+ }
+ 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);
+ }
}
Outbound.Settings = class extends CommonClass {
@@ -862,6 +1006,7 @@ Outbound.Settings = class extends CommonClass {
case Protocols.Socks: return new Outbound.SocksSettings();
case Protocols.HTTP: return new Outbound.HttpSettings();
case Protocols.Wireguard: return new Outbound.WireguardSettings();
+ case Protocols.Hysteria: return new Outbound.HysteriaSettings();
default: return null;
}
}
@@ -878,6 +1023,7 @@ Outbound.Settings = class extends CommonClass {
case Protocols.Socks: return Outbound.SocksSettings.fromJson(json);
case Protocols.HTTP: return Outbound.HttpSettings.fromJson(json);
case Protocols.Wireguard: return Outbound.WireguardSettings.fromJson(json);
+ case Protocols.Hysteria: return Outbound.HysteriaSettings.fromJson(json);
default: return null;
}
}
@@ -1324,4 +1470,30 @@ Outbound.WireguardSettings.Peer = class extends CommonClass {
keepAlive: this.keepAlive ?? undefined,
};
}
+};
+
+Outbound.HysteriaSettings = class extends CommonClass {
+ constructor(address = '', port = 443, version = 2) {
+ super();
+ this.address = address;
+ this.port = port;
+ this.version = version;
+ }
+
+ static fromJson(json = {}) {
+ if (Object.keys(json).length === 0) return new Outbound.HysteriaSettings();
+ return new Outbound.HysteriaSettings(
+ json.address,
+ json.port,
+ json.version
+ );
+ }
+
+ toJson() {
+ return {
+ address: this.address,
+ port: this.port,
+ version: this.version
+ };
+ }
}; \ No newline at end of file
diff --git a/web/html/form/outbound.html b/web/html/form/outbound.html
index 511caefe..2396052a 100644
--- a/web/html/form/outbound.html
+++ b/web/html/form/outbound.html
@@ -1,12 +1,16 @@
{{define "form/outbound"}}
<!-- base -->
-<a-tabs :active-key="outModal.activeKey" :style="{ padding: '0', backgroundColor: 'transparent' }"
+<a-tabs :active-key="outModal.activeKey"
+ :style="{ padding: '0', backgroundColor: 'transparent' }"
@change="(activeKey) => {outModal.toggleJson(activeKey == '2'); }">
<a-tab-pane key="1" tab="Form">
- <a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
+ <a-form :colon="false" :label-col="{ md: {span:8} }"
+ :wrapper-col="{ md: {span:14} }">
<a-form-item label='{{ i18n "protocol" }}'>
- <a-select v-model="outbound.protocol" :dropdown-class-name="themeSwitcher.currentTheme">
- <a-select-option v-for="x,y in Protocols" :value="x">[[ y ]]</a-select-option>
+ <a-select v-model="outbound.protocol"
+ :dropdown-class-name="themeSwitcher.currentTheme">
+ <a-select-option v-for="x,y in Protocols" :value="x">[[ y
+ ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label='{{ i18n "pages.xray.outbound.tag" }}' has-feedback
@@ -21,8 +25,10 @@
<!-- freedom settings-->
<template v-if="outbound.protocol === Protocols.Freedom">
<a-form-item label='Strategy'>
- <a-select v-model="outbound.settings.domainStrategy" :dropdown-class-name="themeSwitcher.currentTheme">
- <a-select-option v-for="s in OutboundDomainStrategies" :value="s">[[ s ]]</a-select-option>
+ <a-select v-model="outbound.settings.domainStrategy"
+ :dropdown-class-name="themeSwitcher.currentTheme">
+ <a-select-option v-for="s in OutboundDomainStrategies" :value="s">[[
+ s ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label='Redirect'>
@@ -35,18 +41,22 @@
</a-form-item>
<template v-if="Object.keys(outbound.settings.fragment).length >0">
<a-form-item label='Packets'>
- <a-select v-model="outbound.settings.fragment.packets" :dropdown-class-name="themeSwitcher.currentTheme">
- <a-select-option v-for="s in ['1-3','tlshello']" :value="s">[[ s ]]</a-select-option>
+ <a-select v-model="outbound.settings.fragment.packets"
+ :dropdown-class-name="themeSwitcher.currentTheme">
+ <a-select-option v-for="s in ['1-3','tlshello']" :value="s">[[ s
+ ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label='Length'>
<a-input v-model.trim="outbound.settings.fragment.length"></a-input>
</a-form-item>
<a-form-item label='Interval'>
- <a-input v-model.trim="outbound.settings.fragment.interval"></a-input>
+ <a-input
+ v-model.trim="outbound.settings.fragment.interval"></a-input>
</a-form-item>
<a-form-item label='Max Split'>
- <a-input v-model.trim="outbound.settings.fragment.maxSplit"></a-input>
+ <a-input
+ v-model.trim="outbound.settings.fragment.maxSplit"></a-input>
</a-form-item>
</template>
@@ -60,11 +70,13 @@
<!-- Add Noise Button -->
<template v-if="outbound.settings.noises.length > 0">
<a-form-item label="Noises">
- <a-button icon="plus" type="primary" size="small" @click="outbound.settings.addNoise()"></a-button>
+ <a-button icon="plus" type="primary" size="small"
+ @click="outbound.settings.addNoise()"></a-button>
</a-form-item>
<!-- Noise Configurations -->
- <a-form v-for="(noise, index) in outbound.settings.noises" :key="index" :colon="false"
+ <a-form v-for="(noise, index) in outbound.settings.noises"
+ :key="index" :colon="false"
:label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-divider :style="{ margin: '0' }"> Noise [[ index + 1 ]]
<a-icon v-if="outbound.settings.noises.length > 1" type="delete"
@@ -72,8 +84,10 @@
:style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"></a-icon>
</a-divider>
<a-form-item label='Type'>
- <a-select v-model="noise.type" :dropdown-class-name="themeSwitcher.currentTheme">
- <a-select-option v-for="s in ['rand','base64','str', 'hex']" :value="s">[[ s ]]</a-select-option>
+ <a-select v-model="noise.type"
+ :dropdown-class-name="themeSwitcher.currentTheme">
+ <a-select-option v-for="s in ['rand','base64','str', 'hex']"
+ :value="s">[[ s ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label='Packet'>
@@ -83,8 +97,10 @@
<a-input v-model.trim="noise.delay"></a-input>
</a-form-item>
<a-form-item label='Apply To'>
- <a-select v-model="noise.applyTo" :dropdown-class-name="themeSwitcher.currentTheme">
- <a-select-option v-for="s in ['ip','ipv4','ipv6']" :value="s">[[ s ]]</a-select-option>
+ <a-select v-model="noise.applyTo"
+ :dropdown-class-name="themeSwitcher.currentTheme">
+ <a-select-option v-for="s in ['ip','ipv4','ipv6']" :value="s">[[
+ s ]]</a-select-option>
</a-select>
</a-form-item>
</a-form>
@@ -94,8 +110,10 @@
<!-- blackhole settings -->
<template v-if="outbound.protocol === Protocols.Blackhole">
<a-form-item label='Response Type'>
- <a-select v-model="outbound.settings.type" :dropdown-class-name="themeSwitcher.currentTheme">
- <a-select-option v-for="s in ['', 'none','http']" :value="s">[[ s ]]</a-select-option>
+ <a-select v-model="outbound.settings.type"
+ :dropdown-class-name="themeSwitcher.currentTheme">
+ <a-select-option v-for="s in ['', 'none','http']" :value="s">[[ s
+ ]]</a-select-option>
</a-select>
</a-form-item>
</template>
@@ -103,16 +121,21 @@
<!-- dns settings -->
<template v-if="outbound.protocol === Protocols.DNS">
<a-form-item label='{{ i18n "pages.inbounds.network" }}'>
- <a-select v-model="outbound.settings.network" :dropdown-class-name="themeSwitcher.currentTheme">
- <a-select-option v-for="s in ['udp','tcp']" :value="s">[[ s ]]</a-select-option>
+ <a-select v-model="outbound.settings.network"
+ :dropdown-class-name="themeSwitcher.currentTheme">
+ <a-select-option v-for="s in ['udp','tcp']" :value="s">[[ s
+ ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label='non-IP queries'>
- <a-select v-model="outbound.settings.nonIPQuery" :dropdown-class-name="themeSwitcher.currentTheme">
- <a-select-option v-for="s in ['reject','drop','skip']" :value="s">[[ s ]]</a-select-option>
+ <a-select v-model="outbound.settings.nonIPQuery"
+ :dropdown-class-name="themeSwitcher.currentTheme">
+ <a-select-option v-for="s in ['reject','drop','skip']" :value="s">[[
+ s ]]</a-select-option>
</a-select>
</a-form-item>
- <a-form-item v-if="outbound.settings.nonIPQuery === 'skip'" label='Block Types'>
+ <a-form-item v-if="outbound.settings.nonIPQuery === 'skip'"
+ label='Block Types'>
<a-input v-model.number="outbound.settings.blockTypes"></a-input>
</a-form-item>
</template>
@@ -149,15 +172,19 @@
<a-input disabled v-model="outbound.settings.pubKey"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.xray.wireguard.domainStrategy" }}'>
- <a-select v-model="outbound.settings.domainStrategy" :dropdown-class-name="themeSwitcher.currentTheme">
- <a-select-option v-for="wds in ['', ...WireguardDomainStrategy]" :value="wds">[[ wds ]]</a-select-option>
+ <a-select v-model="outbound.settings.domainStrategy"
+ :dropdown-class-name="themeSwitcher.currentTheme">
+ <a-select-option v-for="wds in ['', ...WireguardDomainStrategy]"
+ :value="wds">[[ wds ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label='MTU'>
- <a-input-number v-model.number="outbound.settings.mtu" min="0"></a-input-number>
+ <a-input-number v-model.number="outbound.settings.mtu"
+ min="0"></a-input-number>
</a-form-item>
<a-form-item label='Workers'>
- <a-input-number v-model.number="outbound.settings.workers" min="0"></a-input-number>
+ <a-input-number v-model.number="outbound.settings.workers"
+ min="0"></a-input-number>
</a-form-item>
<a-form-item label='No Kernel Tun'>
<a-switch v-model="outbound.settings.noKernelTun"></a-switch>
@@ -173,11 +200,14 @@
<a-input v-model="outbound.settings.reserved"></a-input>
</a-form-item>
<a-form-item label="Peers">
- <a-button icon="plus" type="primary" size="small" @click="outbound.settings.addPeer()"></a-button>
+ <a-button icon="plus" type="primary" size="small"
+ @click="outbound.settings.addPeer()"></a-button>
</a-form-item>
- <a-form v-for="(peer, index) in outbound.settings.peers" :colon="false" :label-col="{ md: {span:8} }"
+ <a-form v-for="(peer, index) in outbound.settings.peers" :colon="false"
+ :label-col="{ md: {span:8} }"
:wrapper-col="{ md: {span:14} }">
- <a-divider :style="{ margin: '0' }"> Peer [[ index + 1 ]] <a-icon v-if="outbound.settings.peers.length>1"
+ <a-divider :style="{ margin: '0' }"> Peer [[ index + 1 ]] <a-icon
+ v-if="outbound.settings.peers.length>1"
type="delete" @click="() => outbound.settings.delPeer(index)"
:style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"></a-icon>
</a-divider>
@@ -193,17 +223,21 @@
<a-form-item>
<template slot="label">
{{ i18n "pages.xray.wireguard.allowedIPs" }}
- <a-button icon="plus" type="primary" size="small" @click="peer.allowedIPs.push('')"></a-button>
+ <a-button icon="plus" type="primary" size="small"
+ @click="peer.allowedIPs.push('')"></a-button>
</template>
- <template v-for="(aip, index) in peer.allowedIPs" :style="{ marginBottom: '10px' }">
+ <template v-for="(aip, index) in peer.allowedIPs"
+ :style="{ marginBottom: '10px' }">
<a-input v-model.trim="peer.allowedIPs[index]">
- <a-button icon="minus" v-if="peer.allowedIPs.length>1" slot="addonAfter" size="small"
+ <a-button icon="minus" v-if="peer.allowedIPs.length>1"
+ slot="addonAfter" size="small"
@click="peer.allowedIPs.splice(index, 1)"></a-button>
</a-input>
</template>
</a-form-item>
<a-form-item label='Keep Alive'>
- <a-input-number v-model.number="peer.keepAlive" :min="0"></a-input-number>
+ <a-input-number v-model.number="peer.keepAlive"
+ :min="0"></a-input-number>
</a-form-item>
</a-form>
</template>
@@ -214,12 +248,14 @@
<a-input v-model.trim="outbound.settings.address"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.port" }}'>
- <a-input-number v-model.number="outbound.settings.port" :min="1" :max="65532"></a-input-number>
+ <a-input-number v-model.number="outbound.settings.port" :min="1"
+ :max="65532"></a-input-number>
</a-form-item>
</template>
<!-- VLESS/VMess user settings -->
- <template v-if="[Protocols.VMess, Protocols.VLESS].includes(outbound.protocol)">
+ <template
+ v-if="[Protocols.VMess, Protocols.VLESS].includes(outbound.protocol)">
<a-form-item label='ID'>
<a-input v-model.trim="outbound.settings.id"></a-input>
</a-form-item>
@@ -227,8 +263,10 @@
<!-- vmess settings -->
<template v-if="outbound.protocol === Protocols.VMess">
<a-form-item label='Security'>
- <a-select v-model="outbound.settings.security" :dropdown-class-name="themeSwitcher.currentTheme">
- <a-select-option v-for="key in USERS_SECURITY" :value="key">[[ key ]]</a-select-option>
+ <a-select v-model="outbound.settings.security"
+ :dropdown-class-name="themeSwitcher.currentTheme">
+ <a-select-option v-for="key in USERS_SECURITY" :value="key">[[ key
+ ]]</a-select-option>
</a-select>
</a-form-item>
</template>
@@ -241,35 +279,47 @@
</template>
<template v-if="outbound.canEnableTlsFlow()">
<a-form-item label='Flow'>
- <a-select v-model="outbound.settings.flow" :dropdown-class-name="themeSwitcher.currentTheme">
- <a-select-option value="" selected>{{ i18n "none" }}</a-select-option>
- <a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
+ <a-select v-model="outbound.settings.flow"
+ :dropdown-class-name="themeSwitcher.currentTheme">
+ <a-select-option value selected>{{ i18n "none"
+ }}</a-select-option>
+ <a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[
+ key ]]</a-select-option>
</a-select>
</a-form-item>
</template>
<!-- XTLS Vision Advanced Settings -->
<template v-if="outbound.canEnableVisionSeed()">
<a-form-item label="Vision Pre-Connect">
- <a-input-number v-model.number="outbound.settings.testpre" :min="0" :max="10" :style="{ width: '100%' }"
+ <a-input-number v-model.number="outbound.settings.testpre" :min="0"
+ :max="10" :style="{ width: '100%' }"
placeholder="0"></a-input-number>
</a-form-item>
<a-form-item label="Vision Seed">
<a-row :gutter="8">
<a-col :span="6">
- <a-input-number v-model.number="outbound.settings.testseed[0]" :min="0" :max="9999"
- :style="{ width: '100%' }" placeholder="900" addon-before="[0]"></a-input-number>
+ <a-input-number v-model.number="outbound.settings.testseed[0]"
+ :min="0" :max="9999"
+ :style="{ width: '100%' }" placeholder="900"
+ addon-before="[0]"></a-input-number>
</a-col>
<a-col :span="6">
- <a-input-number v-model.number="outbound.settings.testseed[1]" :min="0" :max="9999"
- :style="{ width: '100%' }" placeholder="500" addon-before="[1]"></a-input-number>
+ <a-input-number v-model.number="outbound.settings.testseed[1]"
+ :min="0" :max="9999"
+ :style="{ width: '100%' }" placeholder="500"
+ addon-before="[1]"></a-input-number>
</a-col>
<a-col :span="6">
- <a-input-number v-model.number="outbound.settings.testseed[2]" :min="0" :max="9999"
- :style="{ width: '100%' }" placeholder="900" addon-before="[2]"></a-input-number>
+ <a-input-number v-model.number="outbound.settings.testseed[2]"
+ :min="0" :max="9999"
+ :style="{ width: '100%' }" placeholder="900"
+ addon-before="[2]"></a-input-number>
</a-col>
<a-col :span="6">
- <a-input-number v-model.number="outbound.settings.testseed[3]" :min="0" :max="9999"
- :style="{ width: '100%' }" placeholder="256" addon-before="[3]"></a-input-number>
+ <a-input-number v-model.number="outbound.settings.testseed[3]"
+ :min="0" :max="9999"
+ :style="{ width: '100%' }" placeholder="256"
+ addon-before="[3]"></a-input-number>
</a-col>
</a-row>
</a-form-item>
@@ -289,7 +339,8 @@
</template>
<!-- trojan/shadowsocks -->
- <template v-if="[Protocols.Trojan, Protocols.Shadowsocks].includes(outbound.protocol)">
+ <template
+ v-if="[Protocols.Trojan, Protocols.Shadowsocks].includes(outbound.protocol)">
<a-form-item label='{{ i18n "password" }}'>
<a-input v-model.trim="outbound.settings.password"></a-input>
</a-form-item>
@@ -298,8 +349,10 @@
<!-- shadowsocks -->
<template v-if="outbound.protocol === Protocols.Shadowsocks">
<a-form-item label='{{ i18n "encryption" }}'>
- <a-select v-model="outbound.settings.method" :dropdown-class-name="themeSwitcher.currentTheme">
- <a-select-option v-for="(method, method_name) in SSMethods" :value="method">[[ method_name
+ <a-select v-model="outbound.settings.method"
+ :dropdown-class-name="themeSwitcher.currentTheme">
+ <a-select-option v-for="(method, method_name) in SSMethods"
+ :value="method">[[ method_name
]]</a-select-option>
</a-select>
</a-form-item>
@@ -307,15 +360,25 @@
<a-switch v-model="outbound.settings.uot"></a-switch>
</a-form-item>
<a-form-item label='UoTVersion'>
- <a-input-number v-model.number="outbound.settings.UoTVersion" :min="1" :max="2"></a-input-number>
+ <a-input-number v-model.number="outbound.settings.UoTVersion"
+ :min="1" :max="2"></a-input-number>
</a-form-item>
</template>
</template>
+ <!-- hysteria settings -->
+ <template v-if="outbound.protocol === Protocols.Hysteria">
+ <a-form-item label='Version'>
+ <a-input-number v-model.number="outbound.settings.version" :min="2"
+ :max="2" disabled></a-input-number>
+ </a-form-item>
+ </template>
+
<!-- stream settings -->
<template v-if="outbound.canEnableStream()">
<a-form-item label='{{ i18n "transmission" }}'>
- <a-select v-model="outbound.stream.network" @change="streamNetworkChange"
+ <a-select v-model="outbound.stream.network"
+ @change="streamNetworkChange"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="tcp">TCP (RAW)</a-select-option>
<a-select-option value="kcp">mKCP</a-select-option>
@@ -323,6 +386,8 @@
<a-select-option value="grpc">gRPC</a-select-option>
<a-select-option value="httpupgrade">HTTPUpgrade</a-select-option>
<a-select-option value="xhttp">XHTTP</a-select-option>
+ <a-select-option v-if="outbound.protocol === Protocols.Hysteria"
+ value="hysteria">Hysteria2</a-select-option>
</a-select>
</a-form-item>
<template v-if="outbound.stream.network === 'tcp'">
@@ -343,7 +408,8 @@
<!-- kcp -->
<template v-if="outbound.stream.network === 'kcp'">
<a-form-item label='{{ i18n "camouflage" }}'>
- <a-select v-model="outbound.stream.kcp.type" :dropdown-class-name="themeSwitcher.currentTheme">
+ <a-select v-model="outbound.stream.kcp.type"
+ :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="none">None</a-select-option>
<a-select-option value="srtp">SRTP</a-select-option>
<a-select-option value="utp">uTP</a-select-option>
@@ -357,25 +423,31 @@
<a-input v-model="outbound.stream.kcp.seed"></a-input>
</a-form-item>
<a-form-item label='MTU'>
- <a-input-number v-model.number="outbound.stream.kcp.mtu" min="0"></a-input-number>
+ <a-input-number v-model.number="outbound.stream.kcp.mtu"
+ min="0"></a-input-number>
</a-form-item>
<a-form-item label='TTI (ms)'>
- <a-input-number v-model.number="outbound.stream.kcp.tti" min="0"></a-input-number>
+ <a-input-number v-model.number="outbound.stream.kcp.tti"
+ min="0"></a-input-number>
</a-form-item>
<a-form-item label='Uplink (MB/s)'>
- <a-input-number v-model.number="outbound.stream.kcp.upCap" min="0"></a-input-number>
+ <a-input-number v-model.number="outbound.stream.kcp.upCap"
+ min="0"></a-input-number>
</a-form-item>
<a-form-item label='Downlink (MB/s)'>
- <a-input-number v-model.number="outbound.stream.kcp.downCap" min="0"></a-input-number>
+ <a-input-number v-model.number="outbound.stream.kcp.downCap"
+ min="0"></a-input-number>
</a-form-item>
<a-form-item label='Congestion'>
<a-switch v-model="outbound.stream.kcp.congestion"></a-switch>
</a-form-item>
<a-form-item label='Read Buffer (MB)'>
- <a-input-number v-model.number="outbound.stream.kcp.readBuffer" min="0"></a-input-number>
+ <a-input-number v-model.number="outbound.stream.kcp.readBuffer"
+ min="0"></a-input-number>
</a-form-item>
<a-form-item label='Write Buffer (MB)'>
- <a-input-number v-model.number="outbound.stream.kcp.writeBuffer" min="0"></a-input-number>
+ <a-input-number v-model.number="outbound.stream.kcp.writeBuffer"
+ min="0"></a-input-number>
</a-form-item>
</template>
@@ -388,7 +460,8 @@
<a-input v-model.trim="outbound.stream.ws.path"></a-input>
</a-form-item>
<a-form-item label='Heartbeat Period'>
- <a-input-number v-model.number="outbound.stream.ws.heartbeatPeriod" :min="0"></a-input-number>
+ <a-input-number v-model.number="outbound.stream.ws.heartbeatPeriod"
+ :min="0"></a-input-number>
</a-form-item>
</template>
@@ -424,34 +497,101 @@
<a-input v-model.trim="outbound.stream.xhttp.path"></a-input>
</a-form-item>
<a-form-item label='Mode'>
- <a-select v-model="outbound.stream.xhttp.mode" :dropdown-class-name="themeSwitcher.currentTheme">
- <a-select-option v-for="key in MODE_OPTION" :value="key">[[ key ]]</a-select-option>
+ <a-select v-model="outbound.stream.xhttp.mode"
+ :dropdown-class-name="themeSwitcher.currentTheme">
+ <a-select-option v-for="key in MODE_OPTION" :value="key">[[ key
+ ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="No gRPC Header"
v-if="outbound.stream.xhttp.mode === 'stream-up' || outbound.stream.xhttp.mode === 'stream-one'">
<a-switch v-model="outbound.stream.xhttp.noGRPCHeader"></a-switch>
</a-form-item>
- <a-form-item label="Min Upload Interval (Ms)" v-if="outbound.stream.xhttp.mode === 'packet-up'">
- <a-input v-model.trim="outbound.stream.xhttp.scMinPostsIntervalMs"></a-input>
+ <a-form-item label="Min Upload Interval (Ms)"
+ v-if="outbound.stream.xhttp.mode === 'packet-up'">
+ <a-input
+ v-model.trim="outbound.stream.xhttp.scMinPostsIntervalMs"></a-input>
</a-form-item>
- <a-form-item label="Max Concurrency" v-if="!outbound.stream.xhttp.xmux.maxConnections">
- <a-input v-model="outbound.stream.xhttp.xmux.maxConcurrency"></a-input>
+ <a-form-item label="Max Concurrency"
+ v-if="!outbound.stream.xhttp.xmux.maxConnections">
+ <a-input
+ v-model="outbound.stream.xhttp.xmux.maxConcurrency"></a-input>
</a-form-item>
- <a-form-item label="Max Connections" v-if="!outbound.stream.xhttp.xmux.maxConcurrency">
- <a-input v-model="outbound.stream.xhttp.xmux.maxConnections"></a-input>
+ <a-form-item label="Max Connections"
+ v-if="!outbound.stream.xhttp.xmux.maxConcurrency">
+ <a-input
+ v-model="outbound.stream.xhttp.xmux.maxConnections"></a-input>
</a-form-item>
<a-form-item label="Max Reuse Times">
- <a-input v-model="outbound.stream.xhttp.xmux.cMaxReuseTimes"></a-input>
+ <a-input
+ v-model="outbound.stream.xhttp.xmux.cMaxReuseTimes"></a-input>
</a-form-item>
<a-form-item label="Max Request Times">
- <a-input v-model="outbound.stream.xhttp.xmux.hMaxRequestTimes"></a-input>
+ <a-input
+ v-model="outbound.stream.xhttp.xmux.hMaxRequestTimes"></a-input>
</a-form-item>
<a-form-item label="Max Reusable Secs">
- <a-input v-model="outbound.stream.xhttp.xmux.hMaxReusableSecs"></a-input>
+ <a-input
+ v-model="outbound.stream.xhttp.xmux.hMaxReusableSecs"></a-input>
</a-form-item>
<a-form-item label='Keep Alive Period'>
- <a-input-number v-model.number="outbound.stream.xhttp.xmux.hKeepAlivePeriod"></a-input-number>
+ <a-input-number
+ v-model.number="outbound.stream.xhttp.xmux.hKeepAlivePeriod"></a-input-number>
+ </a-form-item>
+ </template>
+
+ <!-- hysteria -->
+ <template v-if="outbound.stream.network === 'hysteria'">
+ <a-form-item label='Auth Password'>
+ <a-input v-model.trim="outbound.stream.hysteria.auth"></a-input>
+ </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>
+ </a-form-item>
+ <a-form-item label='Download Speed'>
+ <a-input v-model.trim="outbound.stream.hysteria.down"
+ placeholder="0 (BBR mode), e.g., 100 mbps"></a-input>
+ </a-form-item>
+ <a-form-item label='UDP Hop Port'>
+ <a-input v-model.trim="outbound.stream.hysteria.udphopPort"
+ placeholder="e.g., 1145-1919 or 11,13,15-17"></a-input>
+ </a-form-item>
+ <a-form-item label='UDP Hop Interval (s)'
+ v-if="outbound.stream.hysteria.udphopPort">
+ <a-input-number
+ v-model.number="outbound.stream.hysteria.udphopInterval"
+ :min="5"></a-input-number>
+ </a-form-item>
+ <a-form-item label='Init Stream Receive'>
+ <a-input-number
+ v-model.number="outbound.stream.