outbound: finalmask

This commit is contained in:
MHSanaei 2026-02-01 01:56:23 +01:00
parent 6b3da4fe5e
commit c59f54bb0e
No known key found for this signature in database
GPG key ID: 7E4060F2FBE5AB7A
3 changed files with 115 additions and 39 deletions

View file

@ -318,7 +318,7 @@ TcpStreamSettings.TcpResponse = class extends XrayCommonClass {
class KcpStreamSettings extends XrayCommonClass { class KcpStreamSettings extends XrayCommonClass {
constructor( constructor(
mtu = 1250, mtu = 1350,
tti = 50, tti = 50,
uplinkCapacity = 5, uplinkCapacity = 5,
downlinkCapacity = 20, downlinkCapacity = 20,
@ -2509,7 +2509,7 @@ Inbound.HttpSettings.HttpAccount = class extends XrayCommonClass {
Inbound.WireguardSettings = class extends XrayCommonClass { Inbound.WireguardSettings = class extends XrayCommonClass {
constructor( constructor(
protocol, protocol,
mtu = 1250, mtu = 1420,
secretKey = Wireguard.generateKeypair().privateKey, secretKey = Wireguard.generateKeypair().privateKey,
peers = [new Inbound.WireguardSettings.Peer()], peers = [new Inbound.WireguardSettings.Peer()],
noKernelTun = false noKernelTun = false

View file

@ -165,7 +165,7 @@ class TcpStreamSettings extends CommonClass {
class KcpStreamSettings extends CommonClass { class KcpStreamSettings extends CommonClass {
constructor( constructor(
mtu = 1250, mtu = 1350,
tti = 50, tti = 50,
uplinkCapacity = 5, uplinkCapacity = 5,
downlinkCapacity = 20, downlinkCapacity = 20,
@ -558,27 +558,49 @@ class SockoptStreamSettings extends CommonClass {
} }
} }
class UdpMask extends CommonClass { class FinalMask extends CommonClass {
constructor(type = 'salamander', password = '') { constructor(type = 'salamander', settings = {}) {
super(); super();
this.type = type; this.type = type;
this.password = password; this.settings = this._getDefaultSettings(type, settings);
}
_getDefaultSettings(type, settings = {}) {
switch (type) {
case 'salamander':
case 'mkcp-aes128gcm':
return { password: settings.password || '' };
case 'header-dns':
case 'xdns':
return { domain: settings.domain || '' };
case 'mkcp-original':
case 'header-dtls':
case 'header-srtp':
case 'header-utp':
case 'header-wechat':
case 'header-wireguard':
return {}; // No settings needed
default:
return settings;
}
} }
static fromJson(json = {}) { static fromJson(json = {}) {
return new UdpMask( return new FinalMask(
json.type, json.type || 'salamander',
json.settings?.password || '' json.settings || {}
); );
} }
toJson() { toJson() {
return { const result = {
type: this.type, type: this.type
settings: {
password: this.password
}
}; };
// Only include settings if they exist and are not empty
if (this.settings && Object.keys(this.settings).length > 0) {
result.settings = this.settings;
}
return result;
} }
} }
@ -595,7 +617,7 @@ class StreamSettings extends CommonClass {
httpupgradeSettings = new HttpUpgradeStreamSettings(), httpupgradeSettings = new HttpUpgradeStreamSettings(),
xhttpSettings = new xHTTPStreamSettings(), xhttpSettings = new xHTTPStreamSettings(),
hysteriaSettings = new HysteriaStreamSettings(), hysteriaSettings = new HysteriaStreamSettings(),
udpmasks = [], finalmask = { udp: [] },
sockopt = undefined, sockopt = undefined,
) { ) {
super(); super();
@ -610,16 +632,21 @@ class StreamSettings extends CommonClass {
this.httpupgrade = httpupgradeSettings; this.httpupgrade = httpupgradeSettings;
this.xhttp = xhttpSettings; this.xhttp = xhttpSettings;
this.hysteria = hysteriaSettings; this.hysteria = hysteriaSettings;
this.udpmasks = udpmasks; this.finalmask = finalmask;
this.sockopt = sockopt; this.sockopt = sockopt;
} }
addUdpMask() { addUdpMask(type = 'salamander') {
this.udpmasks.push(new UdpMask()); if (!this.finalmask.udp) {
this.finalmask.udp = [];
}
this.finalmask.udp.push(new FinalMask(type));
} }
delUdpMask(index) { delUdpMask(index) {
this.udpmasks.splice(index, 1); if (this.finalmask.udp) {
this.finalmask.udp.splice(index, 1);
}
} }
get isTls() { get isTls() {
@ -639,7 +666,16 @@ class StreamSettings extends CommonClass {
} }
static fromJson(json = {}) { static fromJson(json = {}) {
const udpmasks = json.udpmasks ? json.udpmasks.map(mask => UdpMask.fromJson(mask)) : []; let finalmask = { udp: [] };
if (json.finalmask) {
if (Array.isArray(json.finalmask)) {
// Legacy format: direct array (backward compatibility)
finalmask.udp = json.finalmask.map(mask => FinalMask.fromJson(mask));
} else if (json.finalmask.udp) {
// New format: object with udp array
finalmask.udp = json.finalmask.udp.map(mask => FinalMask.fromJson(mask));
}
}
return new StreamSettings( return new StreamSettings(
json.network, json.network,
json.security, json.security,
@ -652,7 +688,7 @@ class StreamSettings extends CommonClass {
HttpUpgradeStreamSettings.fromJson(json.httpupgradeSettings), HttpUpgradeStreamSettings.fromJson(json.httpupgradeSettings),
xHTTPStreamSettings.fromJson(json.xhttpSettings), xHTTPStreamSettings.fromJson(json.xhttpSettings),
HysteriaStreamSettings.fromJson(json.hysteriaSettings), HysteriaStreamSettings.fromJson(json.hysteriaSettings),
udpmasks, finalmask,
SockoptStreamSettings.fromJson(json.sockopt), SockoptStreamSettings.fromJson(json.sockopt),
); );
} }
@ -671,7 +707,9 @@ class StreamSettings extends CommonClass {
httpupgradeSettings: network === 'httpupgrade' ? this.httpupgrade.toJson() : undefined, httpupgradeSettings: network === 'httpupgrade' ? this.httpupgrade.toJson() : undefined,
xhttpSettings: network === 'xhttp' ? this.xhttp.toJson() : undefined, xhttpSettings: network === 'xhttp' ? this.xhttp.toJson() : undefined,
hysteriaSettings: network === 'hysteria' ? this.hysteria.toJson() : undefined, hysteriaSettings: network === 'hysteria' ? this.hysteria.toJson() : undefined,
udpmasks: this.udpmasks.length > 0 ? this.udpmasks.map(mask => mask.toJson()) : undefined, finalmask: (this.finalmask.udp && this.finalmask.udp.length > 0) ? {
udp: this.finalmask.udp.map(mask => mask.toJson())
} : undefined,
sockopt: this.sockopt != undefined ? this.sockopt.toJson() : undefined, sockopt: this.sockopt != undefined ? this.sockopt.toJson() : undefined,
}; };
} }
@ -1285,12 +1323,15 @@ Outbound.VLESSSettings = class extends CommonClass {
flow: this.flow, flow: this.flow,
encryption: this.encryption, encryption: this.encryption,
}; };
// Only include Vision settings when flow is set
if (this.flow && this.flow !== '') {
if (this.testpre > 0) { if (this.testpre > 0) {
result.testpre = this.testpre; result.testpre = this.testpre;
} }
if (this.testseed && this.testseed.length >= 4) { if (this.testseed && this.testseed.length >= 4) {
result.testseed = this.testseed; result.testseed = this.testseed;
} }
}
return result; return result;
} }
}; };
@ -1422,7 +1463,7 @@ Outbound.HttpSettings = class extends CommonClass {
Outbound.WireguardSettings = class extends CommonClass { Outbound.WireguardSettings = class extends CommonClass {
constructor( constructor(
mtu = 1250, mtu = 1420,
secretKey = '', secretKey = '',
address = [''], address = [''],
workers = 2, workers = 2,

View file

@ -546,8 +546,9 @@
<a-input v-model.trim="outbound.stream.hysteria.auth"></a-input> <a-input v-model.trim="outbound.stream.hysteria.auth"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='Congestion'> <a-form-item label='Congestion'>
<a-select v-model="outbound.stream.hysteria.congestion" :dropdown-class-name="themeSwitcher.currentTheme"> <a-select v-model="outbound.stream.hysteria.congestion"
<a-select-option value="">BBR (Auto)</a-select-option> :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-option value="brutal">Brutal</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
@ -602,25 +603,59 @@
</template> </template>
</template> </template>
<!-- udpmasks settings --> <!-- finalmask settings -->
<template v-if="outbound.canEnableStream()"> <template v-if="outbound.canEnableStream()">
<a-form-item label="UDP Masks"> <a-form-item label="UDP Masks">
<a-button icon="plus" type="primary" size="small" @click="outbound.stream.addUdpMask()"></a-button> <a-button icon="plus" type="primary" size="small"
@click="outbound.stream.addUdpMask(outbound.protocol === Protocols.Hysteria ? 'salamander' : 'mkcp-aes128gcm')"></a-button>
</a-form-item> </a-form-item>
<template v-if="outbound.stream.udpmasks.length > 0"> <template
<a-form v-for="(mask, index) in outbound.stream.udpmasks" :key="index" :colon="false" v-if="outbound.stream.finalmask.udp && outbound.stream.finalmask.udp.length > 0">
<a-form v-for="(mask, index) in outbound.stream.finalmask.udp"
:key="index" :colon="false"
:label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }"> :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-divider :style="{ margin: '0' }"> UDP Mask [[ index + 1 ]] <a-divider :style="{ margin: '0' }"> UDP Mask [[ index + 1 ]]
<a-icon type="delete" @click="() => outbound.stream.delUdpMask(index)" <a-icon type="delete"
@click="() => outbound.stream.delUdpMask(index)"
:style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"></a-icon> :style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"></a-icon>
</a-divider> </a-divider>
<a-form-item label='Type'> <a-form-item label='Type'>
<a-select v-model="mask.type" :dropdown-class-name="themeSwitcher.currentTheme"> <a-select v-model="mask.type"
<a-select-option value="salamander">Salamander</a-select-option> @change="(type) => mask.settings = mask._getDefaultSettings(type, {})"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-if="outbound.protocol === Protocols.Hysteria" value="salamander">
Salamander (Hysteria2)</a-select-option>
<a-select-option value="mkcp-aes128gcm">
mKCP AES-128-GCM</a-select-option>
<a-select-option value="header-dns">
Header DNS</a-select-option>
<a-select-option value="header-dtls">
Header DTLS 1.2</a-select-option>
<a-select-option value="header-srtp">
Header SRTP</a-select-option>
<a-select-option value="header-utp">
Header uTP</a-select-option>
<a-select-option value="header-wechat">
Header WeChat Video</a-select-option>
<a-select-option value="header-wireguard">
Header WireGuard</a-select-option>
<a-select-option value="mkcp-original">
mKCP Original</a-select-option>
<a-select-option value="xdns">
xDNS (Experimental)</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label='Password'> <!-- Settings for password-based masks -->
<a-input v-model.trim="mask.password" placeholder="Obfuscation password"></a-input> <a-form-item label='Password'
v-if="['salamander', 'mkcp-aes128gcm'].includes(mask.type)">
<a-input v-model.trim="mask.settings.password"
placeholder="Obfuscation password"></a-input>
</a-form-item>
<!-- Settings for domain-based masks -->
<a-form-item label='Domain'
v-if="['header-dns', 'xdns'].includes(mask.type)">
<a-input v-model.trim="mask.settings.domain"
placeholder="e.g., www.example.com"></a-input>
</a-form-item> </a-form-item>
</a-form> </a-form>
</template> </template>