diff --git a/web/assets/js/model/inbound.js b/web/assets/js/model/inbound.js index 75b05fb9..a885a7a7 100644 --- a/web/assets/js/model/inbound.js +++ b/web/assets/js/model/inbound.js @@ -774,6 +774,7 @@ class HysteriaMasquerade extends XrayCommonClass { dir = '', url = '', rewriteHost = false, + xForwarded = false, insecure = false, content = '', headers = [], @@ -784,6 +785,7 @@ class HysteriaMasquerade extends XrayCommonClass { this.dir = dir; this.url = url; this.rewriteHost = rewriteHost; + this.xForwarded = xForwarded; this.insecure = insecure; this.content = content; this.headers = headers; @@ -804,6 +806,7 @@ class HysteriaMasquerade extends XrayCommonClass { json.dir, json.url, json.rewriteHost, + json.xForwarded, json.insecure, json.content, XrayCommonClass.toHeaders(json.headers), @@ -817,6 +820,7 @@ class HysteriaMasquerade extends XrayCommonClass { dir: XrayCommonClass.shrinkObject(this.dir), url: XrayCommonClass.shrinkObject(this.url), rewriteHost: this.rewriteHost ? true : undefined, + xForwarded: this.xForwarded ? true : undefined, insecure: this.insecure ? true : undefined, content: XrayCommonClass.shrinkObject(this.content), headers: XrayCommonClass.shrinkObject(XrayCommonClass.toV2Headers(this.headers, false)), @@ -1323,6 +1327,31 @@ class UdpMask extends XrayCommonClass { .filter(d => d && d.name !== undefined); } + // Xray-core takes either random bytes ("rand") or a fixed "packet" per noise + // item, never both, and an "array" packet must be a real JSON array. "exp" + // builds the packet from tags such as . + static noiseItemToJson(item = {}) { + const out = {}; + const packet = item.packet ?? ''; + if (item.type === 'exp') { + out.type = 'exp'; + out.packet = String(packet); + } else if (Array.isArray(packet) ? packet.length > 0 : String(packet) !== '') { + if (!item.type || item.type === 'array') { + out.packet = Array.isArray(packet) ? packet + : String(packet).split(/[\s,]+/).filter(Boolean).map(Number); + } else { + out.type = item.type; + out.packet = packet; + } + } else { + if (item.rand !== '' && item.rand != null) out.rand = item.rand; + if (item.randRange) out.randRange = item.randRange; + } + if (item.delay !== '' && item.delay != null && item.delay !== 0) out.delay = item.delay; + return out; + } + // Masks that open their own sockets, so Xray-core only accepts them first. get dialsItself() { return this.type === 'xicmp'; @@ -1337,6 +1366,9 @@ class UdpMask extends XrayCommonClass { toJson() { let settings = this.settings; + if (this.type === 'noise') { + settings = { reset: settings.reset || undefined, noise: (settings.noise || []).map(n => UdpMask.noiseItemToJson(n)) }; + } if (this.type === 'xicmp') { settings = { dgram: settings.dgram || undefined, ips: settings.ips?.length ? settings.ips : undefined }; } @@ -1415,6 +1447,10 @@ class QuicParams extends XrayCommonClass { maxIdleTimeout = 30, keepAlivePeriod = 0, disablePathMTUDiscovery = false, + brutalDisableLossCompensation = false, + disableChromeParrot = false, + disableGSO = false, + disableStatelessReset = false, maxIncomingStreams = 1024, } = {}) { super(); @@ -1431,6 +1467,10 @@ class QuicParams extends XrayCommonClass { this.maxIdleTimeout = maxIdleTimeout; this.keepAlivePeriod = keepAlivePeriod; this.disablePathMTUDiscovery = disablePathMTUDiscovery; + this.brutalDisableLossCompensation = brutalDisableLossCompensation; + this.disableChromeParrot = disableChromeParrot; + this.disableGSO = disableGSO; + this.disableStatelessReset = disableStatelessReset; this.maxIncomingStreams = maxIncomingStreams; } @@ -1440,7 +1480,7 @@ class QuicParams extends XrayCommonClass { 'initStreamReceiveWindow', 'maxStreamReceiveWindow', 'initConnectionReceiveWindow', 'maxConnectionReceiveWindow', 'maxIdleTimeout', 'keepAlivePeriod', 'disablePathMTUDiscovery', - 'maxIncomingStreams']; + 'maxIncomingStreams', 'brutalDisableLossCompensation', 'disableChromeParrot', 'disableGSO', 'disableStatelessReset']; return keys.some(k => json[k] !== undefined && json[k] !== '' && json[k] !== 0 && json[k] !== false); } @@ -1472,6 +1512,10 @@ class QuicParams extends XrayCommonClass { maxIdleTimeout: json.maxIdleTimeout || 0, keepAlivePeriod: json.keepAlivePeriod || 0, disablePathMTUDiscovery: !!json.disablePathMTUDiscovery, + brutalDisableLossCompensation: !!json.brutalDisableLossCompensation, + disableChromeParrot: !!json.disableChromeParrot, + disableGSO: !!json.disableGSO, + disableStatelessReset: !!json.disableStatelessReset, maxIncomingStreams: json.maxIncomingStreams || 0, }); } @@ -1493,6 +1537,10 @@ class QuicParams extends XrayCommonClass { if (this.maxIdleTimeout) result.maxIdleTimeout = this.maxIdleTimeout; if (this.keepAlivePeriod) result.keepAlivePeriod = this.keepAlivePeriod; if (this.disablePathMTUDiscovery) result.disablePathMTUDiscovery = this.disablePathMTUDiscovery; + if (this.brutalDisableLossCompensation) result.brutalDisableLossCompensation = true; + if (this.disableChromeParrot) result.disableChromeParrot = true; + if (this.disableGSO) result.disableGSO = true; + if (this.disableStatelessReset) result.disableStatelessReset = true; if (this.maxIncomingStreams) result.maxIncomingStreams = this.maxIncomingStreams; return Object.keys(result).length > 0 ? result : undefined; } diff --git a/web/assets/js/model/outbound.js b/web/assets/js/model/outbound.js index 448da13f..7d779a75 100644 --- a/web/assets/js/model/outbound.js +++ b/web/assets/js/model/outbound.js @@ -924,6 +924,31 @@ class UdpMask extends CommonClass { return { domains, resolvers, extraPoll: settings.extraPoll ?? 0 }; } + // Xray-core takes either random bytes ("rand") or a fixed "packet" per noise + // item, never both, and an "array" packet must be a real JSON array. "exp" + // builds the packet from tags such as . + static noiseItemToJson(item = {}) { + const out = {}; + const packet = item.packet ?? ''; + if (item.type === 'exp') { + out.type = 'exp'; + out.packet = String(packet); + } else if (Array.isArray(packet) ? packet.length > 0 : String(packet) !== '') { + if (!item.type || item.type === 'array') { + out.packet = Array.isArray(packet) ? packet + : String(packet).split(/[\s,]+/).filter(Boolean).map(Number); + } else { + out.type = item.type; + out.packet = packet; + } + } else { + if (item.rand !== '' && item.rand != null) out.rand = item.rand; + if (item.randRange) out.randRange = item.randRange; + } + if (item.delay !== '' && item.delay != null && item.delay !== 0) out.delay = item.delay; + return out; + } + // Masks that open their own sockets, so Xray-core only accepts them first. get dialsItself() { return ['udphop', 'xicmp'].includes(this.type); @@ -938,6 +963,10 @@ class UdpMask extends CommonClass { toJson() { let settings = this.settings; + if (this.type === 'noise') { + settings = { ...settings, noise: (settings.noise || []).map(n => UdpMask.noiseItemToJson(n)) }; + if (!settings.reset) delete settings.reset; + } if (['xdns', 'xicmp', 'udphop'].includes(this.type)) { settings = Object.fromEntries(Object.entries(settings).filter(([, v]) => !(v === '' || v === 0 || v === false || (Array.isArray(v) && v.length === 0)))); @@ -1015,6 +1044,10 @@ class QuicParams extends CommonClass { maxIdleTimeout = 0, keepAlivePeriod = 0, disablePathMTUDiscovery = false, + brutalDisableLossCompensation = false, + disableChromeParrot = false, + disableGSO = false, + disableStatelessReset = false, maxIncomingStreams = 0, } = {}) { super(); @@ -1029,6 +1062,10 @@ class QuicParams extends CommonClass { this.maxIdleTimeout = maxIdleTimeout; this.keepAlivePeriod = keepAlivePeriod; this.disablePathMTUDiscovery = disablePathMTUDiscovery; + this.brutalDisableLossCompensation = brutalDisableLossCompensation; + this.disableChromeParrot = disableChromeParrot; + this.disableGSO = disableGSO; + this.disableStatelessReset = disableStatelessReset; this.maxIncomingStreams = maxIncomingStreams; } @@ -1038,7 +1075,7 @@ class QuicParams extends CommonClass { 'initStreamReceiveWindow', 'maxStreamReceiveWindow', 'initConnectionReceiveWindow', 'maxConnectionReceiveWindow', 'maxIdleTimeout', 'keepAlivePeriod', 'disablePathMTUDiscovery', - 'maxIncomingStreams']; + 'maxIncomingStreams', 'brutalDisableLossCompensation', 'disableChromeParrot', 'disableGSO', 'disableStatelessReset']; return keys.some(k => json[k] !== undefined && json[k] !== '' && json[k] !== 0 && json[k] !== false); } @@ -1067,6 +1104,10 @@ class QuicParams extends CommonClass { maxIdleTimeout: json.maxIdleTimeout || 0, keepAlivePeriod: json.keepAlivePeriod || 0, disablePathMTUDiscovery: !!json.disablePathMTUDiscovery, + brutalDisableLossCompensation: !!json.brutalDisableLossCompensation, + disableChromeParrot: !!json.disableChromeParrot, + disableGSO: !!json.disableGSO, + disableStatelessReset: !!json.disableStatelessReset, maxIncomingStreams: json.maxIncomingStreams || 0, }); } @@ -1099,6 +1140,10 @@ class QuicParams extends CommonClass { if (this.maxIdleTimeout) result.maxIdleTimeout = this.maxIdleTimeout; if (this.keepAlivePeriod) result.keepAlivePeriod = this.keepAlivePeriod; if (this.disablePathMTUDiscovery) result.disablePathMTUDiscovery = this.disablePathMTUDiscovery; + if (this.brutalDisableLossCompensation) result.brutalDisableLossCompensation = true; + if (this.disableChromeParrot) result.disableChromeParrot = true; + if (this.disableGSO) result.disableGSO = true; + if (this.disableStatelessReset) result.disableStatelessReset = true; if (this.maxIncomingStreams) result.maxIncomingStreams = this.maxIncomingStreams; return Object.keys(result).length > 0 ? result : undefined; } diff --git a/web/html/xui/form/outbound.html b/web/html/xui/form/outbound.html index 7bfdf789..36c894d7 100644 --- a/web/html/xui/form/outbound.html +++ b/web/html/xui/form/outbound.html @@ -977,7 +977,7 @@ diff --git a/web/html/xui/form/stream/stream_finalmask.html b/web/html/xui/form/stream/stream_finalmask.html index b4dbc0be..35fd8e31 100644 --- a/web/html/xui/form/stream/stream_finalmask.html +++ b/web/html/xui/form/stream/stream_finalmask.html @@ -234,7 +234,7 @@ diff --git a/web/html/xui/form/stream/stream_finalmask_fields.html b/web/html/xui/form/stream/stream_finalmask_fields.html index dc9b6254..ea1424b5 100644 --- a/web/html/xui/form/stream/stream_finalmask_fields.html +++ b/web/html/xui/form/stream/stream_finalmask_fields.html @@ -32,3 +32,52 @@ :dropdown-class-name="themeSwitcher.currentTheme"> {{end}} + +{{define "form/noiseItem"}} + + + Array + String + Hex + Base64 + Exp + + + + + + + +{{end}} diff --git a/web/html/xui/form/stream/stream_hysteria.html b/web/html/xui/form/stream/stream_hysteria.html index f06a0311..28af1071 100644 --- a/web/html/xui/form/stream/stream_hysteria.html +++ b/web/html/xui/form/stream/stream_hysteria.html @@ -25,6 +25,9 @@ + + + diff --git a/web/translation/translate.en_US.toml b/web/translation/translate.en_US.toml index 25041da8..c4367e29 100644 --- a/web/translation/translate.en_US.toml +++ b/web/translation/translate.en_US.toml @@ -454,6 +454,12 @@ "remoteIPs" = "Remote IPs" "remotePorts" = "Remote Ports" "udpHopLinkDesc" = "Only written into share links and subscriptions. On the server, forward this port range to the inbound port with iptables" +"brutalDisableLossCompensation" = "Brutal: Disable Loss Compensation" +"disableChromeParrot" = "Disable Chrome Parrot" +"disableGSO" = "Disable GSO" +"disableStatelessReset" = "Disable Stateless Reset" +"noiseExpDesc" = "Builds a fresh packet on every send from tags: , , , , , , (bytes, random, random letters, random digits, timestamp, counter, nonce)" +"noisePacketDesc" = "A fixed packet replaces the random bytes; leave it empty to use Rand. Array: comma-separated byte values" [pages.inbounds.stream.hysteria] "authPassword" = "Auth Password" @@ -466,6 +472,7 @@ "insecure" = "Insecure" "content" = "Content" "statusCode" = "Status Code" +"xForwarded" = "X-Forwarded Headers" [pages.inbounds.protocols] "flow" = "Flow" diff --git a/web/translation/translate.fa_IR.toml b/web/translation/translate.fa_IR.toml index 5cd41e0a..3ace03ab 100644 --- a/web/translation/translate.fa_IR.toml +++ b/web/translation/translate.fa_IR.toml @@ -454,6 +454,12 @@ "remoteIPs" = "آی‌پی‌های مقصد" "remotePorts" = "پورت‌های مقصد" "udpHopLinkDesc" = "فقط در لینک‌های اشتراک و سابسکریپشن قرار می‌گیرد. روی سرور این بازه پورت را با iptables به پورت اینباند هدایت کنید" +"brutalDisableLossCompensation" = "Brutal: غیرفعال‌سازی جبران Loss" +"disableChromeParrot" = "غیرفعال‌سازی تقلید Chrome" +"disableGSO" = "غیرفعال‌سازی GSO" +"disableStatelessReset" = "غیرفعال‌سازی Stateless Reset" +"noiseExpDesc" = "در هر ارسال یک پکت تازه از این تگ‌ها می‌سازد: , , , , , , (بایت، تصادفی، حروف تصادفی، ارقام تصادفی، زمان، شمارنده، nonce)" +"noisePacketDesc" = "پکت ثابت جای بایت‌های تصادفی را می‌گیرد؛ برای استفاده از Rand خالی بگذارید. Array: مقادیر بایت جداشده با کاما" [pages.inbounds.stream.hysteria] "authPassword" = "رمز احراز هویت" @@ -466,6 +472,7 @@ "insecure" = "ناامن" "content" = "محتوا" "statusCode" = "Status Code" +"xForwarded" = "هدرهای X-Forwarded" [pages.inbounds.protocols] "flow" = "Flow" diff --git a/web/translation/translate.ru_RU.toml b/web/translation/translate.ru_RU.toml index 7dfeb06a..d51020e8 100644 --- a/web/translation/translate.ru_RU.toml +++ b/web/translation/translate.ru_RU.toml @@ -454,6 +454,12 @@ "remoteIPs" = "Удалённые IP" "remotePorts" = "Удалённые порты" "udpHopLinkDesc" = "Используется только в ссылках и подписках. На сервере перенаправьте этот диапазон портов на порт входящего через iptables" +"brutalDisableLossCompensation" = "Brutal: отключить компенсацию потерь" +"disableChromeParrot" = "Отключить имитацию Chrome" +"disableGSO" = "Отключить GSO" +"disableStatelessReset" = "Отключить Stateless Reset" +"noiseExpDesc" = "При каждой отправке собирает новый пакет из тегов: , , , , , , (байты, случайные, случайные буквы, случайные цифры, метка времени, счётчик, nonce)" +"noisePacketDesc" = "Фиксированный пакет заменяет случайные байты; оставьте пустым, чтобы использовать Rand. Array: байты через запятую" [pages.inbounds.stream.hysteria] "authPassword" = "Пароль аутентификации" @@ -466,6 +472,7 @@ "insecure" = "Небезопасный" "content" = "Контент" "statusCode" = "Код статуса" +"xForwarded" = "Заголовки X-Forwarded" [pages.inbounds.protocols] "flow" = "Flow" diff --git a/web/translation/translate.vi_VN.toml b/web/translation/translate.vi_VN.toml index 05f05b04..963cc927 100644 --- a/web/translation/translate.vi_VN.toml +++ b/web/translation/translate.vi_VN.toml @@ -454,6 +454,12 @@ "remoteIPs" = "IP từ xa" "remotePorts" = "Cổng từ xa" "udpHopLinkDesc" = "Chỉ dùng trong liên kết chia sẻ và gói đăng ký. Trên máy chủ, chuyển tiếp dải cổng này tới cổng inbound bằng iptables" +"brutalDisableLossCompensation" = "Brutal: Tắt bù mất gói" +"disableChromeParrot" = "Tắt giả lập Chrome" +"disableGSO" = "Tắt GSO" +"disableStatelessReset" = "Tắt Stateless Reset" +"noiseExpDesc" = "Tạo gói mới mỗi lần gửi từ các thẻ: , , , , , , (byte, ngẫu nhiên, chữ ngẫu nhiên, số ngẫu nhiên, dấu thời gian, bộ đếm, nonce)" +"noisePacketDesc" = "Gói cố định thay cho byte ngẫu nhiên; để trống để dùng Rand. Array: các giá trị byte phân tách bằng dấu phẩy" [pages.inbounds.stream.hysteria] "authPassword" = "Mật khẩu xác thực" @@ -466,6 +472,7 @@ "insecure" = "Không bảo mật" "content" = "Nội dung" "statusCode" = "Status Code" +"xForwarded" = "Header X-Forwarded" [pages.inbounds.protocols] "flow" = "Flow" diff --git a/web/translation/translate.zh_Hans.toml b/web/translation/translate.zh_Hans.toml index 9900b478..d7e0d864 100644 --- a/web/translation/translate.zh_Hans.toml +++ b/web/translation/translate.zh_Hans.toml @@ -454,6 +454,12 @@ "remoteIPs" = "远程 IP" "remotePorts" = "远程端口" "udpHopLinkDesc" = "仅写入分享链接和订阅。服务端需用 iptables 将该端口范围转发到入站端口" +"brutalDisableLossCompensation" = "Brutal:禁用丢包补偿" +"disableChromeParrot" = "禁用 Chrome 模拟" +"disableGSO" = "禁用 GSO" +"disableStatelessReset" = "禁用无状态重置" +"noiseExpDesc" = "每次发送时用以下标签生成新包:, , , , , , (字节、随机、随机字母、随机数字、时间戳、计数器、nonce)" +"noisePacketDesc" = "固定包会替代随机字节;留空则使用 Rand。Array:以逗号分隔的字节值" [pages.inbounds.stream.hysteria] "authPassword" = "认证密码" @@ -466,6 +472,7 @@ "insecure" = "不安全" "content" = "内容" "statusCode" = "状态码" +"xForwarded" = "X-Forwarded 头" [pages.inbounds.protocols] "flow" = "Flow"