From 5e6a9cac3692f425f391a82540bb412c9c5a894e Mon Sep 17 00:00:00 2001 From: Mohammad Date: Mon, 20 Apr 2026 22:36:33 +0330 Subject: [PATCH 01/13] feat: Add a new finalmask params --- app/models/host.py | 182 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 176 insertions(+), 6 deletions(-) diff --git a/app/models/host.py b/app/models/host.py index e33143091..8c22bf90c 100644 --- a/app/models/host.py +++ b/app/models/host.py @@ -1,7 +1,8 @@ from enum import Enum from ipaddress import ip_network +from typing import Any -from pydantic import BaseModel, ConfigDict, Field, field_validator +from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator from app.db.models import ProxyHostALPN, ProxyHostFingerprint, ProxyHostSecurity, UserStatus @@ -34,9 +35,21 @@ class ECHQueryStrategy(str, Enum): class XrayFragmentSettings(BaseModel): + model_config = ConfigDict(extra="allow", populate_by_name=True) + packets: str = Field(pattern=r"^(:?tlshello|[\d-]{1,16})$") length: str = Field(pattern=r"^[\d-]{1,16}$") - interval: str = Field(pattern=r"^[\d-]{1,16}$") + interval: str = Field(pattern=r"^[\d-]{1,16}$", serialization_alias="delay") + max_split: str | None = Field(default=None, alias="maxSplit") + + @model_validator(mode="before") + @classmethod + def delay_to_interval(cls, value): + if isinstance(value, dict) and "delay" in value: + value = {**value} + delay = value.pop("delay") + value.setdefault("interval", delay) + return value class SingBoxFragmentSettings(BaseModel): @@ -51,17 +64,174 @@ class FragmentSettings(BaseModel): class XrayNoiseSettings(BaseModel): - type: str = Field(pattern=r"^(:?rand|str|base64|hex)$") - packet: str - delay: str = Field(pattern=r"^\d{1,16}(-\d{1,16})?$") + model_config = ConfigDict(extra="allow", populate_by_name=True) + + type: str = Field(pattern=r"^$|^(:?rand|array|str|base64|hex)$") + packet: str | list[int] | None = Field(default=None) + delay: str | int | None = Field(default=None) apply_to: str = Field(default="ip", pattern=r"ip|ipv4|ipv6") - rand_range: str | None = Field(default=None, pattern=r"^\d{1,16}(-\d{1,16})?$") + rand: int | str | None = Field(default=None) + rand_range: str | None = Field(default=None, alias="randRange", pattern=r"^\d{1,16}(-\d{1,16})?$") class NoiseSettings(BaseModel): xray: list[XrayNoiseSettings] | None = Field(default=None) +class FinalMaskBaseModel(BaseModel): + model_config = ConfigDict(extra="allow", populate_by_name=True, use_enum_values=True) + + +class FinalMaskTcpType(str, Enum): + header_custom = "header-custom" + fragment = "fragment" + sudoku = "sudoku" + + +class FinalMaskUdpType(str, Enum): + header_custom = "header-custom" + header_dns = "header-dns" + header_dtls = "header-dtls" + header_srtp = "header-srtp" + header_utp = "header-utp" + header_wechat = "header-wechat" + header_wireguard = "header-wireguard" + mkcp_original = "mkcp-original" + mkcp_aes128gcm = "mkcp-aes128gcm" + noise = "noise" + salamander = "salamander" + sudoku = "sudoku" + xdns = "xdns" + xicmp = "xicmp" + + +class FinalMaskQuicCongestion(str, Enum): + reno = "reno" + bbr = "bbr" + brutal = "brutal" + force_brutal = "force-brutal" + + +class FinalMaskTcpHeaderCustomSettings(FinalMaskBaseModel): + clients: list[list[XrayNoiseSettings]] | None = Field(default=None) + servers: list[list[XrayNoiseSettings]] | None = Field(default=None) + errors: list[list[XrayNoiseSettings]] | None = Field(default=None) + + +class FinalMaskUdpHeaderCustomSettings(FinalMaskBaseModel): + client: list[XrayNoiseSettings] | None = Field(default=None) + server: list[XrayNoiseSettings] | None = Field(default=None) + + +class FinalMaskPasswordSettings(FinalMaskBaseModel): + password: str | None = Field(default=None) + + +class FinalMaskSudokuSettings(FinalMaskPasswordSettings): + ascii: str | None = Field(default=None) + custom_table: str | None = Field(default=None, alias="customTable") + custom_tables: list[str] | None = Field(default=None, alias="customTables") + padding_min: int | None = Field(default=None, alias="paddingMin") + padding_max: int | None = Field(default=None, alias="paddingMax") + + +class FinalMaskDomainSettings(FinalMaskBaseModel): + domain: str | None = Field(default=None) + + +class FinalMaskXicmpSettings(FinalMaskBaseModel): + listen_ip: str | None = Field(default=None, alias="listenIp") + id: int | None = Field(default=None) + + +class FinalMaskNoiseSettings(FinalMaskBaseModel): + reset: int | None = Field(default=None) + noise: list[XrayNoiseSettings] | None = Field(default=None) + + +class FinalMaskUdpHop(FinalMaskBaseModel): + ports: str | None = Field(default=None) + interval: str | int | None = Field(default=None) + + +class FinalMaskQuicParams(FinalMaskBaseModel): + congestion: FinalMaskQuicCongestion | None = Field(default=None) + debug: bool | None = Field(default=None) + brutal_up: str | int | None = Field(default=None, alias="brutalUp") + brutal_down: str | int | None = Field(default=None, alias="brutalDown") + udp_hop: FinalMaskUdpHop | None = Field(default=None, alias="udpHop") + init_stream_receive_window: int | None = Field(default=None, alias="initStreamReceiveWindow") + max_stream_receive_window: int | None = Field(default=None, alias="maxStreamReceiveWindow") + init_connection_receive_window: int | None = Field(default=None, alias="initConnectionReceiveWindow") + max_connection_receive_window: int | None = Field(default=None, alias="maxConnectionReceiveWindow") + max_idle_timeout: int | None = Field(default=None, alias="maxIdleTimeout") + keep_alive_period: int | None = Field(default=None, alias="keepAlivePeriod") + disable_path_mtu_discovery: bool | None = Field(default=None, alias="disablePathMTUDiscovery") + max_incoming_streams: int | None = Field(default=None, alias="maxIncomingStreams") + + +FinalMaskTcpSettings = ( + FinalMaskTcpHeaderCustomSettings | XrayFragmentSettings | FinalMaskSudokuSettings | dict[str, Any] +) +FinalMaskUdpSettings = ( + FinalMaskUdpHeaderCustomSettings + | FinalMaskPasswordSettings + | FinalMaskSudokuSettings + | FinalMaskDomainSettings + | FinalMaskXicmpSettings + | FinalMaskNoiseSettings + | dict[str, Any] +) + + +class FinalMaskTcpLayer(FinalMaskBaseModel): + type: FinalMaskTcpType + settings: FinalMaskTcpSettings = Field(default_factory=dict) + + @model_validator(mode="after") + def parse_settings(self): + if not isinstance(self.settings, dict): + return self + + settings_model = { + FinalMaskTcpType.header_custom: FinalMaskTcpHeaderCustomSettings, + FinalMaskTcpType.fragment: XrayFragmentSettings, + FinalMaskTcpType.sudoku: FinalMaskSudokuSettings, + }.get(FinalMaskTcpType(self.type)) + self.settings = settings_model.model_validate(self.settings) + return self + + +class FinalMaskUdpLayer(FinalMaskBaseModel): + type: FinalMaskUdpType + settings: FinalMaskUdpSettings = Field(default_factory=dict) + + @model_validator(mode="after") + def parse_settings(self): + if not isinstance(self.settings, dict): + return self + + settings_model = { + FinalMaskUdpType.header_custom: FinalMaskUdpHeaderCustomSettings, + FinalMaskUdpType.header_dns: FinalMaskDomainSettings, + FinalMaskUdpType.mkcp_aes128gcm: FinalMaskPasswordSettings, + FinalMaskUdpType.noise: FinalMaskNoiseSettings, + FinalMaskUdpType.salamander: FinalMaskPasswordSettings, + FinalMaskUdpType.sudoku: FinalMaskSudokuSettings, + FinalMaskUdpType.xdns: FinalMaskDomainSettings, + FinalMaskUdpType.xicmp: FinalMaskXicmpSettings, + }.get(FinalMaskUdpType(self.type)) + if settings_model is not None: + self.settings = settings_model.model_validate(self.settings) + return self + + +class FinalMask(FinalMaskBaseModel): + tcp: list[FinalMaskTcpLayer] | None = Field(default=None) + udp: list[FinalMaskUdpLayer] | None = Field(default=None) + quic_params: FinalMaskQuicParams | None = Field(default=None, alias="quicParams") + + class XMuxSettings(BaseModel): max_concurrency: str | int | None = Field( None, pattern=r"^\d{1,16}(-\d{1,16})?$", serialization_alias="maxConcurrency" From 9b0838640c0c6b6deeaea73caa2e7a9a2ae6c340 Mon Sep 17 00:00:00 2001 From: Mohammad Date: Sat, 2 May 2026 13:51:34 +0330 Subject: [PATCH 02/13] Add final_mask_settings to hosts --- app/core/hosts.py | 4 +-- ..._add_final_mask_settings_to_hosts_table.py | 32 +++++++++++++++++++ app/db/models.py | 2 ++ app/models/host.py | 5 +-- 4 files changed, 39 insertions(+), 4 deletions(-) create mode 100644 app/db/migrations/versions/f976bfcf4738_add_final_mask_settings_to_hosts_table.py diff --git a/app/core/hosts.py b/app/core/hosts.py index 2a1bc7b7b..6ecbf9de5 100644 --- a/app/core/hosts.py +++ b/app/core/hosts.py @@ -172,7 +172,7 @@ async def _prepare_subscription_inbound_data( if inbound_flow == "none": inbound_flow = "" - finalmask = inbound_config.get("finalmask") + final_mask_settings = host.final_mask_settings if host.final_mask_settings else inbound_config.get("finalmask") # Network comes from inbound, NOT from checking which transport exists on host! # Host can have ALL transport configs, inbound determines which one is used @@ -372,7 +372,7 @@ async def _prepare_subscription_inbound_data( use_sni_as_host=host.use_sni_as_host, fragment_settings=host.fragment_settings.model_dump() if host.fragment_settings else None, noise_settings=host.noise_settings.model_dump() if host.noise_settings else None, - finalmask=finalmask, + finalmask=final_mask_settings, priority=host.priority, status=list(host.status) if host.status else None, subscription_templates=host.subscription_templates.model_dump(exclude_none=True) diff --git a/app/db/migrations/versions/f976bfcf4738_add_final_mask_settings_to_hosts_table.py b/app/db/migrations/versions/f976bfcf4738_add_final_mask_settings_to_hosts_table.py new file mode 100644 index 000000000..875f8ee1e --- /dev/null +++ b/app/db/migrations/versions/f976bfcf4738_add_final_mask_settings_to_hosts_table.py @@ -0,0 +1,32 @@ +"""add final_mask_settings to hosts table + +Revision ID: f976bfcf4738 +Revises: b7d9e1a2c3f4 +Create Date: 2026-05-02 13:46:21.008567 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'f976bfcf4738' +down_revision = 'b7d9e1a2c3f4' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('hosts', schema=None) as batch_op: + batch_op.add_column(sa.Column('final_mask_settings', sa.JSON(none_as_null=True), nullable=True)) + + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('hosts', schema=None) as batch_op: + batch_op.drop_column('final_mask_settings') + + # ### end Alembic commands ### diff --git a/app/db/models.py b/app/db/models.py index 65d2b2b6b..516b7cc63 100644 --- a/app/db/models.py +++ b/app/db/models.py @@ -1,5 +1,6 @@ import os from datetime import datetime as dt, timezone as tz +from email.policy import default from enum import Enum from typing import Any, Dict, List, Optional @@ -507,6 +508,7 @@ class ProxyHost(Base): ) wireguard_overrides: Mapped[Optional[Dict[str, Any]]] = mapped_column(JSON(none_as_null=True), default=None) subscription_templates: Mapped[Optional[Dict[str, Any]]] = mapped_column(JSON(none_as_null=True), default=None) + final_mask_settings: Mapped[Optional[Dict[str, Any]]] = mapped_column(JSON(none_as_null=True), default=None) class System(Base): diff --git a/app/models/host.py b/app/models/host.py index 8c22bf90c..973ccd80c 100644 --- a/app/models/host.py +++ b/app/models/host.py @@ -64,8 +64,6 @@ class FragmentSettings(BaseModel): class XrayNoiseSettings(BaseModel): - model_config = ConfigDict(extra="allow", populate_by_name=True) - type: str = Field(pattern=r"^$|^(:?rand|array|str|base64|hex)$") packet: str | list[int] | None = Field(default=None) delay: str | int | None = Field(default=None) @@ -73,6 +71,8 @@ class XrayNoiseSettings(BaseModel): rand: int | str | None = Field(default=None) rand_range: str | None = Field(default=None, alias="randRange", pattern=r"^\d{1,16}(-\d{1,16})?$") + model_config = ConfigDict(extra="allow", populate_by_name=True) + class NoiseSettings(BaseModel): xray: list[XrayNoiseSettings] | None = Field(default=None) @@ -463,6 +463,7 @@ class BaseHost(BaseModel): verify_peer_cert_by_name: set[str] | None = Field(default_factory=set) wireguard_overrides: WireGuardHostOverrides | None = None subscription_templates: SubscriptionTemplates | None = None + final_mask_settings: FinalMask | None = None model_config = ConfigDict(from_attributes=True) From f42615b67e0bb5d98eb993ce85e7cababbedafcf Mon Sep 17 00:00:00 2001 From: Mohammad Date: Sat, 2 May 2026 17:26:02 +0330 Subject: [PATCH 03/13] fix: type in finalmask object --- app/db/models.py | 1 - app/models/subscription.py | 3 ++- app/subscription/links.py | 6 +++++- app/subscription/xray.py | 9 ++++++--- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/app/db/models.py b/app/db/models.py index 516b7cc63..4233d7e58 100644 --- a/app/db/models.py +++ b/app/db/models.py @@ -1,6 +1,5 @@ import os from datetime import datetime as dt, timezone as tz -from email.policy import default from enum import Enum from typing import Any, Dict, List, Optional diff --git a/app/models/subscription.py b/app/models/subscription.py index 79775ac42..0e9ccaf43 100644 --- a/app/models/subscription.py +++ b/app/models/subscription.py @@ -8,6 +8,7 @@ from typing import Any from pydantic import BaseModel, Field, computed_field +from app.models.host import FinalMask class TLSConfig(BaseModel): @@ -258,7 +259,7 @@ class SubscriptionInboundData(BaseModel): # Fragment and noise settings fragment_settings: dict[str, Any] | None = Field(None) noise_settings: dict[str, Any] | None = Field(None) - finalmask: dict[str, Any] | None = Field(None) + finalmask: FinalMask | dict[str, Any] | None = Field(None) # Priority and status priority: int = Field(0) diff --git a/app/subscription/links.py b/app/subscription/links.py index 490f8be7e..998696eb6 100644 --- a/app/subscription/links.py +++ b/app/subscription/links.py @@ -167,7 +167,11 @@ def _transport_kcp(self, payload: dict, protocol: str, config: KCPTransportConfi def _apply_finalmask(self, payload: dict, protocol: str, inbound: SubscriptionInboundData): """Apply finalMask for vmess if needed""" if inbound.finalmask: - payload["fm"] = json.dumps(inbound.finalmask) + if isinstance(inbound.finalmask, FinalMask): + finalmask = inbound.finalmask.model_dump() + else: + finalmask = inbound.finalmask + payload["fm"] = json.dumps(finalmask) def _transport_tcp(self, payload: dict, protocol: str, config: TCPTransportConfig, path: str): """Handle tcp/raw/http transport - only gets TCP config""" diff --git a/app/subscription/xray.py b/app/subscription/xray.py index 9432780e3..7717a2580 100644 --- a/app/subscription/xray.py +++ b/app/subscription/xray.py @@ -10,6 +10,7 @@ TLSConfig, WebSocketTransportConfig, XHTTPTransportConfig, + FinalMask, ) from app.templates import render_template_string from app.utils.helpers import UUIDEncoder @@ -467,9 +468,11 @@ def _build_shadowsocks(self, address: str, inbound: SubscriptionInboundData, set } if inbound.finalmask is not None: - outbound["streamSettings"] = self._stream_setting_config( - network=inbound.network, finalmask=inbound.finalmask - ) + if isinstance(inbound.finalmask, FinalMask): + finalmask = inbound.finalmask.model_dump() + else: + finalmask = inbound.finalmask + outbound["streamSettings"] = self._stream_setting_config(network=inbound.network, finalmask=finalmask) return self._normalize_and_remove_none_values(outbound) From 9cb6db0921888ea6ebfb835eebdbd6e69c77e8d6 Mon Sep 17 00:00:00 2001 From: Mohammad Date: Sat, 2 May 2026 17:30:40 +0330 Subject: [PATCH 04/13] fix: migrations --- .../f976bfcf4738_add_final_mask_settings_to_hosts_table.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/db/migrations/versions/f976bfcf4738_add_final_mask_settings_to_hosts_table.py b/app/db/migrations/versions/f976bfcf4738_add_final_mask_settings_to_hosts_table.py index 875f8ee1e..04f2a5c25 100644 --- a/app/db/migrations/versions/f976bfcf4738_add_final_mask_settings_to_hosts_table.py +++ b/app/db/migrations/versions/f976bfcf4738_add_final_mask_settings_to_hosts_table.py @@ -11,7 +11,7 @@ # revision identifiers, used by Alembic. revision = 'f976bfcf4738' -down_revision = 'b7d9e1a2c3f4' +down_revision = 'af2d644dda44' branch_labels = None depends_on = None From 4dcd1d0f6ef958aaac6d08eb80524d38c23baab9 Mon Sep 17 00:00:00 2001 From: Mohammad Date: Sat, 9 May 2026 12:05:14 +0330 Subject: [PATCH 05/13] fix: migrations revises id --- .../f976bfcf4738_add_final_mask_settings_to_hosts_table.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/db/migrations/versions/f976bfcf4738_add_final_mask_settings_to_hosts_table.py b/app/db/migrations/versions/f976bfcf4738_add_final_mask_settings_to_hosts_table.py index 04f2a5c25..55c20cd57 100644 --- a/app/db/migrations/versions/f976bfcf4738_add_final_mask_settings_to_hosts_table.py +++ b/app/db/migrations/versions/f976bfcf4738_add_final_mask_settings_to_hosts_table.py @@ -1,7 +1,7 @@ """add final_mask_settings to hosts table Revision ID: f976bfcf4738 -Revises: b7d9e1a2c3f4 +Revises: af2d644dda44 Create Date: 2026-05-02 13:46:21.008567 """ From 9108354ca130d197deca619e4ad6fde3fb73bf82 Mon Sep 17 00:00:00 2001 From: Mohammad Date: Sat, 9 May 2026 12:12:16 +0330 Subject: [PATCH 06/13] fix: import missing type --- app/subscription/links.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/subscription/links.py b/app/subscription/links.py index 998696eb6..b02fa1814 100644 --- a/app/subscription/links.py +++ b/app/subscription/links.py @@ -13,6 +13,7 @@ TLSConfig, WebSocketTransportConfig, XHTTPTransportConfig, + FinalMask, ) from config import EXTERNAL_CONFIG From a5f0aac263d59e1df08a41840fd12739bee8c057 Mon Sep 17 00:00:00 2001 From: Mohammad Date: Sat, 9 May 2026 12:17:09 +0330 Subject: [PATCH 07/13] fix: update hosts to code rabit suggest --- app/models/host.py | 90 ++++++++++++++++++++++++++++++---------------- 1 file changed, 60 insertions(+), 30 deletions(-) diff --git a/app/models/host.py b/app/models/host.py index 973ccd80c..61211e7e1 100644 --- a/app/models/host.py +++ b/app/models/host.py @@ -184,46 +184,76 @@ class FinalMaskQuicParams(FinalMaskBaseModel): ) +FINAL_MASK_TCP_SETTINGS_MODELS = { + FinalMaskTcpType.header_custom: FinalMaskTcpHeaderCustomSettings, + FinalMaskTcpType.fragment: XrayFragmentSettings, + FinalMaskTcpType.sudoku: FinalMaskSudokuSettings, +} + +FINAL_MASK_UDP_SETTINGS_MODELS = { + FinalMaskUdpType.header_custom: FinalMaskUdpHeaderCustomSettings, + FinalMaskUdpType.header_dns: FinalMaskDomainSettings, + FinalMaskUdpType.header_dtls: FinalMaskPasswordSettings, + FinalMaskUdpType.header_srtp: FinalMaskPasswordSettings, + FinalMaskUdpType.header_utp: FinalMaskPasswordSettings, + FinalMaskUdpType.header_wechat: FinalMaskPasswordSettings, + FinalMaskUdpType.header_wireguard: FinalMaskPasswordSettings, + FinalMaskUdpType.mkcp_original: FinalMaskPasswordSettings, + FinalMaskUdpType.mkcp_aes128gcm: FinalMaskPasswordSettings, + FinalMaskUdpType.noise: FinalMaskNoiseSettings, + FinalMaskUdpType.salamander: FinalMaskPasswordSettings, + FinalMaskUdpType.sudoku: FinalMaskSudokuSettings, + FinalMaskUdpType.xdns: FinalMaskDomainSettings, + FinalMaskUdpType.xicmp: FinalMaskXicmpSettings, +} + + +def _dispatch_final_mask_settings( + value: Any, + type_enum: type[Enum], + settings_models: dict[Enum, type[BaseModel]], +): + if not isinstance(value, dict): + return value + + settings = value.get("settings") + if not isinstance(settings, dict): + return value + + layer_type = value.get("type") + if layer_type is None: + return value + + try: + layer_type = type_enum(layer_type) + except ValueError: + return value + + value = {**value} + settings_model = settings_models.get(layer_type) + if settings_model is not None: + value["settings"] = settings_model.model_validate(settings) + return value + + class FinalMaskTcpLayer(FinalMaskBaseModel): type: FinalMaskTcpType settings: FinalMaskTcpSettings = Field(default_factory=dict) - @model_validator(mode="after") - def parse_settings(self): - if not isinstance(self.settings, dict): - return self - - settings_model = { - FinalMaskTcpType.header_custom: FinalMaskTcpHeaderCustomSettings, - FinalMaskTcpType.fragment: XrayFragmentSettings, - FinalMaskTcpType.sudoku: FinalMaskSudokuSettings, - }.get(FinalMaskTcpType(self.type)) - self.settings = settings_model.model_validate(self.settings) - return self + @model_validator(mode="before") + @classmethod + def parse_settings(cls, value): + return _dispatch_final_mask_settings(value, FinalMaskTcpType, FINAL_MASK_TCP_SETTINGS_MODELS) class FinalMaskUdpLayer(FinalMaskBaseModel): type: FinalMaskUdpType settings: FinalMaskUdpSettings = Field(default_factory=dict) - @model_validator(mode="after") - def parse_settings(self): - if not isinstance(self.settings, dict): - return self - - settings_model = { - FinalMaskUdpType.header_custom: FinalMaskUdpHeaderCustomSettings, - FinalMaskUdpType.header_dns: FinalMaskDomainSettings, - FinalMaskUdpType.mkcp_aes128gcm: FinalMaskPasswordSettings, - FinalMaskUdpType.noise: FinalMaskNoiseSettings, - FinalMaskUdpType.salamander: FinalMaskPasswordSettings, - FinalMaskUdpType.sudoku: FinalMaskSudokuSettings, - FinalMaskUdpType.xdns: FinalMaskDomainSettings, - FinalMaskUdpType.xicmp: FinalMaskXicmpSettings, - }.get(FinalMaskUdpType(self.type)) - if settings_model is not None: - self.settings = settings_model.model_validate(self.settings) - return self + @model_validator(mode="before") + @classmethod + def parse_settings(cls, value): + return _dispatch_final_mask_settings(value, FinalMaskUdpType, FINAL_MASK_UDP_SETTINGS_MODELS) class FinalMask(FinalMaskBaseModel): From 089d6506b0c7ca3363cf72aadaa63070a732d98f Mon Sep 17 00:00:00 2001 From: Mohammad Date: Sat, 9 May 2026 12:29:01 +0330 Subject: [PATCH 08/13] Update f976bfcf4738_add_final_mask_settings_to_hosts_table.py --- .../f976bfcf4738_add_final_mask_settings_to_hosts_table.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/db/migrations/versions/f976bfcf4738_add_final_mask_settings_to_hosts_table.py b/app/db/migrations/versions/f976bfcf4738_add_final_mask_settings_to_hosts_table.py index 55c20cd57..f93771c98 100644 --- a/app/db/migrations/versions/f976bfcf4738_add_final_mask_settings_to_hosts_table.py +++ b/app/db/migrations/versions/f976bfcf4738_add_final_mask_settings_to_hosts_table.py @@ -1,7 +1,7 @@ """add final_mask_settings to hosts table Revision ID: f976bfcf4738 -Revises: af2d644dda44 +Revises: 73c78c6a9b24 Create Date: 2026-05-02 13:46:21.008567 """ @@ -11,7 +11,7 @@ # revision identifiers, used by Alembic. revision = 'f976bfcf4738' -down_revision = 'af2d644dda44' +down_revision = '73c78c6a9b24' branch_labels = None depends_on = None From c4d8d721c82dc79b2bbc5b54cadc72c4c272957a Mon Sep 17 00:00:00 2001 From: Mohammad Date: Mon, 29 Jun 2026 22:14:02 +0330 Subject: [PATCH 09/13] fix import --- app/core/hosts.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/core/hosts.py b/app/core/hosts.py index ca52f0a85..cff44108d 100644 --- a/app/core/hosts.py +++ b/app/core/hosts.py @@ -13,7 +13,7 @@ from app.db import GetDB from app.db.crud.host import get_host_by_id, get_hosts, upsert_inbounds from app.db.models import ProxyHostSecurity -from app.models.host import BaseHost, TransportSettings, WireGuardHostOverrides +from app.models.host import BaseHost, FinalMask, TransportSettings, WireGuardHostOverrides from app.models.subscription import ( GRPCTransportConfig, KCPTransportConfig, @@ -188,7 +188,11 @@ async def _prepare_subscription_inbound_data( inbound_flow = "" final_mask_settings = host.final_mask_settings if host.final_mask_settings else inbound_config.get("finalmask") - finalmask_link = json.dumps(final_mask_settings, separators=(",", ":")) if final_mask_settings else None + fms = final_mask_settings + if final_mask_settings: + if isinstance(final_mask_settings, FinalMask): + fms = final_mask_settings.model_dump() + finalmask_link = json.dumps(fms, separators=(",", ":")) # Network comes from inbound, NOT from checking which transport exists on host! # Host can have ALL transport configs, inbound determines which one is used From b9b8c676787e39138508e1a4ef60bd1f67c9e7a7 Mon Sep 17 00:00:00 2001 From: Mohammad Date: Mon, 29 Jun 2026 22:20:59 +0330 Subject: [PATCH 10/13] fix migrations --- .../f976bfcf4738_add_final_mask_settings_to_hosts_table.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/db/migrations/versions/f976bfcf4738_add_final_mask_settings_to_hosts_table.py b/app/db/migrations/versions/f976bfcf4738_add_final_mask_settings_to_hosts_table.py index f93771c98..44924338b 100644 --- a/app/db/migrations/versions/f976bfcf4738_add_final_mask_settings_to_hosts_table.py +++ b/app/db/migrations/versions/f976bfcf4738_add_final_mask_settings_to_hosts_table.py @@ -1,7 +1,7 @@ """add final_mask_settings to hosts table Revision ID: f976bfcf4738 -Revises: 73c78c6a9b24 +Revises: b6c9d0e1f2a3 Create Date: 2026-05-02 13:46:21.008567 """ @@ -11,7 +11,7 @@ # revision identifiers, used by Alembic. revision = 'f976bfcf4738' -down_revision = '73c78c6a9b24' +down_revision = 'b6c9d0e1f2a3' branch_labels = None depends_on = None From 0598ace732596656443078b86b94a5d54e8613ea Mon Sep 17 00:00:00 2001 From: Mohammad Date: Mon, 29 Jun 2026 22:32:28 +0330 Subject: [PATCH 11/13] fix --- app/core/hosts.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/core/hosts.py b/app/core/hosts.py index cff44108d..0892f2ae8 100644 --- a/app/core/hosts.py +++ b/app/core/hosts.py @@ -188,6 +188,7 @@ async def _prepare_subscription_inbound_data( inbound_flow = "" final_mask_settings = host.final_mask_settings if host.final_mask_settings else inbound_config.get("finalmask") + finalmask_link = None fms = final_mask_settings if final_mask_settings: if isinstance(final_mask_settings, FinalMask): From 870f71386335ab712a4a348bd7a70ccf93e2ecda Mon Sep 17 00:00:00 2001 From: Mohammad Date: Mon, 29 Jun 2026 23:34:22 +0330 Subject: [PATCH 12/13] fix: Dump FinalMask with aliases --- app/core/hosts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/core/hosts.py b/app/core/hosts.py index 0892f2ae8..f41505fba 100644 --- a/app/core/hosts.py +++ b/app/core/hosts.py @@ -192,7 +192,7 @@ async def _prepare_subscription_inbound_data( fms = final_mask_settings if final_mask_settings: if isinstance(final_mask_settings, FinalMask): - fms = final_mask_settings.model_dump() + fms = final_mask_settings.model_dump(by_alias=True) finalmask_link = json.dumps(fms, separators=(",", ":")) # Network comes from inbound, NOT from checking which transport exists on host! From f89c0284e027d9ce437a0ffcc0fb5c69dc87e3dc Mon Sep 17 00:00:00 2001 From: default Date: Sun, 5 Jul 2026 12:49:10 +0000 Subject: [PATCH 13/13] feat: Add final mask settings to hosts modals in dashboard --- dashboard/public/statics/locales/en.json | 13 + dashboard/public/statics/locales/fa.json | 13 + dashboard/public/statics/locales/ru.json | 13 + dashboard/public/statics/locales/zh.json | 13 + .../hosts/components/finalmask-settings.tsx | 1079 +++++++++++++++++ .../features/hosts/components/hosts-list.tsx | 2 + .../src/features/hosts/dialogs/host-modal.tsx | 40 + .../src/features/hosts/forms/host-form.ts | 3 + dashboard/src/service/api/index.ts | 587 ++++++--- 9 files changed, 1600 insertions(+), 163 deletions(-) create mode 100644 dashboard/src/features/hosts/components/finalmask-settings.tsx diff --git a/dashboard/public/statics/locales/en.json b/dashboard/public/statics/locales/en.json index 18c60a682..227a10eba 100644 --- a/dashboard/public/statics/locales/en.json +++ b/dashboard/public/statics/locales/en.json @@ -1391,6 +1391,19 @@ "randRange": "Rand Range", "randRangePlaceholder": "Enter rand range (e.g. 10-20)" }, + "finalmask": { + "title": "FinalMask Settings", + "enable": "Enable FinalMask", + "info": "Configure custom finalmask client configurations (TCP, UDP, and QUIC params).", + "placeholder": "Enter FinalMask JSON configuration", + "invalidJson": "Invalid JSON: {{error}}", + "tcpLayers": "TCP Layers", + "addTcpLayer": "Add TCP Layer", + "noTcpLayers": "No TCP layers configured", + "udpLayers": "UDP Layers", + "addUdpLayer": "Add UDP Layer", + "noUdpLayers": "No UDP layers configured" + }, "muxSettings": "Mux Settings", "enableMux": "Enable Mux", "xraySettings": "Xray Settings", diff --git a/dashboard/public/statics/locales/fa.json b/dashboard/public/statics/locales/fa.json index 136f58b88..5217e27f5 100644 --- a/dashboard/public/statics/locales/fa.json +++ b/dashboard/public/statics/locales/fa.json @@ -1243,6 +1243,19 @@ "randRange": "محدوده تصادفی", "randRangePlaceholder": "محدوده تصادفی را وارد کنید (مثلاً 10-20)" }, + "finalmask": { + "title": "تنظیمات FinalMask", + "enable": "فعال‌سازی FinalMask", + "info": "پیکربندی تنظیمات کلاینت سفارشی finalmask (پارامترهای TCP، UDP و QUIC).", + "placeholder": "پیکربندی JSON برای FinalMask را وارد کنید", + "invalidJson": "JSON نامعتبر است: {{error}}", + "tcpLayers": "لایه‌های TCP", + "addTcpLayer": "افزودن لایه TCP", + "noTcpLayers": "هیچ لایه TCP پیکربندی نشده است", + "udpLayers": "لایه‌های UDP", + "addUdpLayer": "افزودن لایه UDP", + "noUdpLayers": "هیچ لایه UDP پیکربندی نشده است" + }, "muxSettings": "تنظیمات Mux", "enableMux": "فعال‌سازی Mux", "xraySettings": "تنظیمات Xray", diff --git a/dashboard/public/statics/locales/ru.json b/dashboard/public/statics/locales/ru.json index 537e29c17..c656e5465 100644 --- a/dashboard/public/statics/locales/ru.json +++ b/dashboard/public/statics/locales/ru.json @@ -1602,6 +1602,19 @@ "randRange": "Случайный диапазон", "randRangePlaceholder": "Введите случайный диапазон (например 10-20)" }, + "finalmask": { + "title": "Настройки FinalMask", + "enable": "Включить FinalMask", + "info": "Настройка пользовательских конфигураций клиента finalmask (параметры TCP, UDP и QUIC).", + "placeholder": "Введите конфигурацию FinalMask в формате JSON", + "invalidJson": "Неверный JSON: {{error}}", + "tcpLayers": "Слои TCP", + "addTcpLayer": "Добавить слой TCP", + "noTcpLayers": "Слои TCP не настроены", + "udpLayers": "Слои UDP", + "addUdpLayer": "Добавить слой UDP", + "noUdpLayers": "Слои UDP не настроены" + }, "muxSettings": "Настройки Mux", "enableMux": "Включить Mux", "xraySettings": "Настройки Xray", diff --git a/dashboard/public/statics/locales/zh.json b/dashboard/public/statics/locales/zh.json index 856d09f6c..400a76062 100644 --- a/dashboard/public/statics/locales/zh.json +++ b/dashboard/public/statics/locales/zh.json @@ -1364,6 +1364,19 @@ "randRange": "随机范围", "randRangePlaceholder": "输入随机范围(例如 10-20)" }, + "finalmask": { + "title": "FinalMask 设置", + "enable": "启用 FinalMask", + "info": "配置自定义 finalmask 客户端配置 (TCP, UDP, 和 QUIC 参数)。", + "placeholder": "输入 FinalMask JSON 配置", + "invalidJson": "无效的 JSON: {{error}}", + "tcpLayers": "TCP 层", + "addTcpLayer": "添加 TCP 层", + "noTcpLayers": "未配置 TCP 层", + "udpLayers": "UDP 层", + "addUdpLayer": "添加 UDP 层", + "noUdpLayers": "未配置 UDP 层" + }, "muxSettings": "Mux 设置", "enableMux": "启用 Mux", "xraySettings": "Xray 设置", diff --git a/dashboard/src/features/hosts/components/finalmask-settings.tsx b/dashboard/src/features/hosts/components/finalmask-settings.tsx new file mode 100644 index 000000000..8404163c0 --- /dev/null +++ b/dashboard/src/features/hosts/components/finalmask-settings.tsx @@ -0,0 +1,1079 @@ +import { useFieldArray, UseFormReturn } from 'react-hook-form' +import { useTranslation } from 'react-i18next' +import { HostFormValues } from '../forms/host-form' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Switch } from '@/components/ui/switch' +import { FormField, FormItem, FormLabel, FormControl } from '@/components/ui/form' +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' +import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs' +import { Plus, Trash2, Copy } from 'lucide-react' +import { useState } from 'react' +import { CodeEditorPanel } from '@/components/common/code-editor-panel' +import { StringArrayPopoverInput } from '@/components/common/string-array-popover-input' +import useDirDetection from '@/hooks/use-dir-detection' + +interface FinalMaskSettingsProps { + form: UseFormReturn +} + +export function FinalMaskSettings({ form }: FinalMaskSettingsProps) { + const { t } = useTranslation() + const dir = useDirDetection() + + return ( + + + TCP + UDP + QUIC + + + + + + + + + + + + + + + ) +} + +// ========================================== +// TCP Layers component +// ========================================== +function TcpLayersForm({ form }: { form: UseFormReturn }) { + const { t } = useTranslation() + const { fields, append, remove } = useFieldArray({ + control: form.control, + name: 'final_mask_settings.tcp' as any, + }) + + const handleAddLayer = () => { + append({ + type: 'fragment', + settings: { + packets: '', + length: '', + interval: '', + }, + }) + } + + const handleTypeChange = (index: number, newType: string) => { + form.setValue(`final_mask_settings.tcp.${index}.type` as any, newType) + if (newType === 'fragment') { + form.setValue(`final_mask_settings.tcp.${index}.settings` as any, { + packets: '', + length: '', + interval: '', + }) + } else if (newType === 'sudoku') { + form.setValue(`final_mask_settings.tcp.${index}.settings` as any, { + password: '', + ascii: '', + customTable: '', + customTables: [], + paddingMin: undefined, + paddingMax: undefined, + }) + } else if (newType === 'header-custom') { + form.setValue(`final_mask_settings.tcp.${index}.settings` as any, { + clients: [], + servers: [], + errors: [], + }) + } + } + + return ( +
+
+

+ {t('hostsDialog.finalmask.tcpLayers', { defaultValue: 'TCP Layers' })} +

+ +
+ +
+ {fields.map((field: any, index) => { + const type = form.watch(`final_mask_settings.tcp.${index}.type`) + return ( +
+
+
+ #{index + 1} + ( + + + + )} + /> +
+ +
+ + {type === 'fragment' && ( +
+ ( + + Packets + + + + + )} + /> + ( + + Length + + + + + )} + /> + ( + + Interval + + + + + )} + /> +
+ )} + + {type === 'sudoku' && ( + + )} + + {type === 'header-custom' && ( +
+ { + const [text, setText] = useState(() => JSON.stringify(jsonField.value || [], null, 2)) + return ( + + Clients (JSON array of noise arrays) + + { + setText(val) + try { + jsonField.onChange(JSON.parse(val)) + } catch (e) {} + }} + embeddedContainerClassName="h-32" + /> + + + ) + }} + /> + { + const [text, setText] = useState(() => JSON.stringify(jsonField.value || [], null, 2)) + return ( + + Servers (JSON array of noise arrays) + + { + setText(val) + try { + jsonField.onChange(JSON.parse(val)) + } catch (e) {} + }} + embeddedContainerClassName="h-32" + /> + + + ) + }} + /> + { + const [text, setText] = useState(() => JSON.stringify(jsonField.value || [], null, 2)) + return ( + + Errors (JSON array of noise arrays) + + { + setText(val) + try { + jsonField.onChange(JSON.parse(val)) + } catch (e) {} + }} + embeddedContainerClassName="h-32" + /> + + + ) + }} + /> +
+ )} +
+ ) + })} + + {fields.length === 0 && ( +
+ {t('hostsDialog.finalmask.noTcpLayers', { defaultValue: 'No TCP layers configured' })} +
+ )} +
+
+ ) +} + +// ========================================== +// UDP Layers component +// ========================================== +function UdpLayersForm({ form }: { form: UseFormReturn }) { + const { t } = useTranslation() + const { fields, append, remove } = useFieldArray({ + control: form.control, + name: 'final_mask_settings.udp' as any, + }) + + const handleAddLayer = () => { + append({ + type: 'header-dns', + settings: { + domain: '', + }, + }) + } + + const handleTypeChange = (index: number, newType: string) => { + form.setValue(`final_mask_settings.udp.${index}.type` as any, newType) + if (newType === 'header-dns' || newType === 'xdns') { + form.setValue(`final_mask_settings.udp.${index}.settings` as any, { domain: '' }) + } else if ( + [ + 'header-dtls', + 'header-srtp', + 'header-utp', + 'header-wechat', + 'header-wireguard', + 'mkcp-original', + 'mkcp-aes128gcm', + 'salamander', + ].includes(newType) + ) { + form.setValue(`final_mask_settings.udp.${index}.settings` as any, { password: '' }) + } else if (newType === 'noise') { + form.setValue(`final_mask_settings.udp.${index}.settings` as any, { reset: undefined, noise: [] }) + } else if (newType === 'sudoku') { + form.setValue(`final_mask_settings.udp.${index}.settings` as any, { + password: '', + ascii: '', + customTable: '', + customTables: [], + paddingMin: undefined, + paddingMax: undefined, + }) + } else if (newType === 'xicmp') { + form.setValue(`final_mask_settings.udp.${index}.settings` as any, { listenIp: '', id: undefined }) + } else if (newType === 'header-custom') { + form.setValue(`final_mask_settings.udp.${index}.settings` as any, { client: [], server: [] }) + } + } + + return ( +
+
+

+ {t('hostsDialog.finalmask.udpLayers', { defaultValue: 'UDP Layers' })} +

+ +
+ +
+ {fields.map((field: any, index) => { + const type = form.watch(`final_mask_settings.udp.${index}.type`) + return ( +
+
+
+ #{index + 1} + ( + + + + )} + /> +
+ +
+ + {(type === 'header-dns' || type === 'xdns') && ( +
+ ( + + Domain + + + + + )} + /> +
+ )} + + {[ + 'header-dtls', + 'header-srtp', + 'header-utp', + 'header-wechat', + 'header-wireguard', + 'mkcp-original', + 'mkcp-aes128gcm', + 'salamander', + ].includes(type) && ( +
+ ( + + Password + + + + + )} + /> +
+ )} + + {type === 'sudoku' && ( + + )} + + {type === 'xicmp' && ( +
+ ( + + Listen IP + + + + + )} + /> + ( + + ID + + inputField.onChange(e.target.value === '' ? undefined : Number(e.target.value))} + className="h-8 text-xs" + /> + + + )} + /> +
+ )} + + {type === 'noise' && ( +
+ ( + + Reset count + + inputField.onChange(e.target.value === '' ? undefined : Number(e.target.value))} + className="h-8 text-xs" + /> + + + )} + /> + +
+ )} + + {type === 'header-custom' && ( +
+ + +
+ )} +
+ ) + })} + + {fields.length === 0 && ( +
+ {t('hostsDialog.finalmask.noUdpLayers', { defaultValue: 'No UDP layers configured' })} +
+ )} +
+
+ ) +} + +// ========================================== +// QUIC Params component +// ========================================== +function QuicParamsForm({ form }: { form: UseFormReturn }) { + return ( +
+
+ ( + + Congestion + + + )} + /> + + ( + + Debug + + + + + )} + /> + + ( + + Brutal Up (Mbps) + + + + + )} + /> + + ( + + Brutal Down (Mbps) + + + + + )} + /> +
+ +
+
UDP Hop
+
+ ( + + Ports + + + + + )} + /> + + ( + + Interval (ms or duration) + + + + + )} + /> +
+
+ +
+
Windows & Stream settings
+
+ ( + + Init Stream RX Window + + field.onChange(e.target.value === '' ? undefined : Number(e.target.value))} + className="h-8 text-xs" + /> + + + )} + /> + + ( + + Max Stream RX Window + + field.onChange(e.target.value === '' ? undefined : Number(e.target.value))} + className="h-8 text-xs" + /> + + + )} + /> + + ( + + Init Connection RX Window + + field.onChange(e.target.value === '' ? undefined : Number(e.target.value))} + className="h-8 text-xs" + /> + + + )} + /> + + ( + + Max Connection RX Window + + field.onChange(e.target.value === '' ? undefined : Number(e.target.value))} + className="h-8 text-xs" + /> + + + )} + /> + + ( + + Max Incoming Streams + + field.onChange(e.target.value === '' ? undefined : Number(e.target.value))} + className="h-8 text-xs" + /> + + + )} + /> + + ( + + Max Idle Timeout (ms) + + field.onChange(e.target.value === '' ? undefined : Number(e.target.value))} + className="h-8 text-xs" + /> + + + )} + /> + + ( + + Keep Alive Period (ms) + + field.onChange(e.target.value === '' ? undefined : Number(e.target.value))} + className="h-8 text-xs" + /> + + + )} + /> + + ( + + Disable PMTU Discovery + + + + + )} + /> +
+
+
+ ) +} + +// ========================================== +// Sudoku Settings Form +// ========================================== +function SudokuSettingsForm({ prefix, form }: { prefix: string; form: UseFormReturn }) { + const { t } = useTranslation() + return ( +
+ ( + + Password + + + + + )} + /> + ( + + ASCII + + + + + )} + /> + ( + + Custom Table + + + + + )} + /> + ( + + Custom Tables + + inputField.onChange(next)} + placeholder="Add Table" + addPlaceholder={t('arrayInput.addPlaceholder')} + addButtonLabel={t('arrayInput.addButton')} + itemsLabel={t('arrayInput.items')} + emptyMessage={t('arrayInput.noItems')} + duplicateErrorMessage={t('arrayInput.duplicateError')} + clickToEditTitle={t('arrayInput.clickToEdit')} + editItemTitle={t('arrayInput.editItem')} + removeItemTitle={t('arrayInput.removeItem')} + saveEditTitle={t('arrayInput.saveEdit')} + cancelEditTitle={t('arrayInput.cancelEdit')} + /> + + + )} + /> + ( + + Padding Min + + inputField.onChange(e.target.value === '' ? undefined : Number(e.target.value))} + className="h-8 text-xs" + /> + + + )} + /> + ( + + Padding Max + + inputField.onChange(e.target.value === '' ? undefined : Number(e.target.value))} + className="h-8 text-xs" + /> + + + )} + /> +
+ ) +} + +// ========================================== +// Noise Settings array editor helper +// ========================================== +interface XrayNoiseSettingsListProps { + form: UseFormReturn + name: string + label: string +} + +function XrayNoiseSettingsList({ form, name, label }: XrayNoiseSettingsListProps) { + const { t } = useTranslation() + const { fields, append, remove, insert } = useFieldArray({ + control: form.control, + name: name as any, + }) + + const handleDuplicate = (index: number) => { + const item = form.getValues(`${name}.${index}` as any) + if (item) { + insert(index + 1, { ...item }) + } + } + + return ( +
+
+ {label} + +
+ +
+ {fields.map((field, index) => ( +
+
+ {index + 1} + ( + + + + )} + /> + ( + + + + )} + /> +
+ + +
+
+ +
+ ( + + + + + + )} + /> + ( + + + + + + )} + /> + ( + + + + + + )} + /> + ( + + + + + + )} + /> +
+
+ ))} + {fields.length === 0 && ( +
+ {t('hostsDialog.noise.noNoiseSettings', { defaultValue: 'No noise items' })} +
+ )} +
+
+ ) +} diff --git a/dashboard/src/features/hosts/components/hosts-list.tsx b/dashboard/src/features/hosts/components/hosts-list.tsx index aca4c571d..d0e0c45dd 100644 --- a/dashboard/src/features/hosts/components/hosts-list.tsx +++ b/dashboard/src/features/hosts/components/hosts-list.tsx @@ -206,6 +206,7 @@ export default function HostsList({ xray: host.subscription_templates.xray ?? undefined, } : undefined, + final_mask_settings: host.final_mask_settings ?? undefined, fragment_settings: host.fragment_settings ? { xray: host.fragment_settings.xray ?? undefined, @@ -394,6 +395,7 @@ export default function HostsList({ http_headers: host.http_headers || {}, wireguard_overrides: host.wireguard_overrides ?? undefined, subscription_templates: host.subscription_templates ?? undefined, + final_mask_settings: host.final_mask_settings ?? undefined, } await createHost(newHost) diff --git a/dashboard/src/features/hosts/dialogs/host-modal.tsx b/dashboard/src/features/hosts/dialogs/host-modal.tsx index 833c94684..15f17cb2f 100644 --- a/dashboard/src/features/hosts/dialogs/host-modal.tsx +++ b/dashboard/src/features/hosts/dialogs/host-modal.tsx @@ -2,6 +2,7 @@ import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/ import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { StringArrayPopoverInput } from '@/components/common/string-array-popover-input' +import { CodeEditorPanel } from '@/components/common/code-editor-panel' import { Checkbox } from '@/components/ui/checkbox' import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog' import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form' @@ -23,6 +24,7 @@ import { UseFormReturn } from 'react-hook-form' import { useTranslation } from 'react-i18next' import { hostFormDefaultValues, type HostFormValues } from '@/features/hosts/forms/host-form' import { LoaderButton } from '@/components/ui/loader-button' +import { FinalMaskSettings } from '../components/finalmask-settings' interface HostModalProps { isDialogOpen: boolean @@ -372,6 +374,44 @@ const HostModal: React.FC = ({ isDialogOpen, onOpenChange, onSub {(selectedNoiseSettings || []).length === 0 &&
{t('hostsDialog.noise.noNoiseSettings')}
} + + {/* FinalMask Settings */} +
+
+
+

{t('hostsDialog.finalmask.title', { defaultValue: 'FinalMask Settings' })}

+ + + + + +
+

{t('hostsDialog.finalmask.info', { defaultValue: 'Configure custom finalmask client configurations (TCP, UDP, and QUIC params).' })}

+
+
+
+
+ { + if (checked) { + form.setValue('final_mask_settings', { + tcp: [], + udp: [], + quicParams: {} + }, { shouldDirty: true, shouldTouch: true }) + } else { + form.setValue('final_mask_settings', undefined, { shouldDirty: true, shouldTouch: true }) + } + }} + /> +
+ {form.watch('final_mask_settings') !== undefined && form.watch('final_mask_settings') !== null && ( + + )} +
diff --git a/dashboard/src/features/hosts/forms/host-form.ts b/dashboard/src/features/hosts/forms/host-form.ts index 5ab970db4..c91826231 100644 --- a/dashboard/src/features/hosts/forms/host-form.ts +++ b/dashboard/src/features/hosts/forms/host-form.ts @@ -161,6 +161,7 @@ export interface HostFormValues { heartbeatPeriod?: number } } + final_mask_settings?: any } const transportSettingsSchema = z @@ -449,6 +450,7 @@ export const HostFormSchema = z.object({ xray: z.number().int().positive().optional(), }) .optional(), + final_mask_settings: z.any().optional(), }) export const hostFormDefaultValues: HostFormValues = { @@ -476,4 +478,5 @@ export const hostFormDefaultValues: HostFormValues = { verify_peer_cert_by_name: [], fragment_settings: undefined, subscription_templates: undefined, + final_mask_settings: undefined, } diff --git a/dashboard/src/service/api/index.ts b/dashboard/src/service/api/index.ts index 705537313..3d89400ac 100644 --- a/dashboard/src/service/api/index.ts +++ b/dashboard/src/service/api/index.ts @@ -384,15 +384,22 @@ export type Health200 = { [key: string]: unknown } export type XrayNoiseSettingsRandRange = string | null +export type XrayNoiseSettingsRand = number | string | null + +export type XrayNoiseSettingsDelay = string | number | null + +export type XrayNoiseSettingsPacket = string | number[] | null + export interface XrayNoiseSettings { - /** @pattern ^(:?rand|str|base64|hex)$ */ + /** @pattern ^$|^(:?rand|array|str|base64|hex)$ */ type: string - packet: string - /** @pattern ^\d{1,16}(-\d{1,16})?$ */ - delay: string + packet?: XrayNoiseSettingsPacket + delay?: XrayNoiseSettingsDelay /** @pattern ip|ipv4|ipv6 */ apply_to?: string - rand_range?: XrayNoiseSettingsRandRange + rand?: XrayNoiseSettingsRand + randRange?: XrayNoiseSettingsRandRange + [key: string]: unknown } export type XrayMuxSettingsOutputXudpConcurrency = number | null @@ -410,13 +417,30 @@ export type XrayMuxSettingsInputXudpConcurrency = number | null export type XrayMuxSettingsInputConcurrency = number | null -export interface XrayFragmentSettings { +export type XrayFragmentSettingsOutputMaxSplit = string | null + +export interface XrayFragmentSettingsOutput { + /** @pattern ^(:?tlshello|[\d-]{1,16})$ */ + packets: string + /** @pattern ^[\d-]{1,16}$ */ + length: string + /** @pattern ^[\d-]{1,16}$ */ + delay: string + maxSplit?: XrayFragmentSettingsOutputMaxSplit + [key: string]: unknown +} + +export type XrayFragmentSettingsInputMaxSplit = string | null + +export interface XrayFragmentSettingsInput { /** @pattern ^(:?tlshello|[\d-]{1,16})$ */ packets: string /** @pattern ^[\d-]{1,16}$ */ length: string /** @pattern ^[\d-]{1,16}$ */ interval: string + maxSplit?: XrayFragmentSettingsInputMaxSplit + [key: string]: unknown } export type Xudp = (typeof Xudp)[keyof typeof Xudp] @@ -738,10 +762,6 @@ export type UsersPermissionsRevokeSubAnyOf = { [key: string]: PermissionScope | export type UsersPermissionsRevokeSub = boolean | UsersPermissionsRevokeSubAnyOf | null -export type UsersPermissionsResetUsageAnyOf = { [key: string]: PermissionScope | number } - -export type UsersPermissionsResetUsage = boolean | UsersPermissionsResetUsageAnyOf | null - export interface UsersPermissions { create?: UsersPermissionsCreate read?: UsersPermissionsRead @@ -754,6 +774,10 @@ export interface UsersPermissions { activate_next_plan?: UsersPermissionsActivateNextPlan } +export type UsersPermissionsResetUsageAnyOf = { [key: string]: PermissionScope | number } + +export type UsersPermissionsResetUsage = boolean | UsersPermissionsResetUsageAnyOf | null + export type UsersPermissionsDeleteAnyOf = { [key: string]: PermissionScope | number } export type UsersPermissionsDelete = boolean | UsersPermissionsDeleteAnyOf | null @@ -784,13 +808,6 @@ export const UsernameGenerationStrategy = { export type UserUsageStatsListPeriod = Period | null -export interface UserUsageStat { - total_traffic: number - period_start: string -} - -export type UserUsageStatsListStats = { [key: string]: UserUsageStat[] } - export interface UserUsageStatsList { period?: UserUsageStatsListPeriod start: string @@ -798,6 +815,13 @@ export interface UserUsageStatsList { stats: UserUsageStatsListStats } +export interface UserUsageStat { + total_traffic: number + period_start: string +} + +export type UserUsageStatsListStats = { [key: string]: UserUsageStat[] } + export type UserTemplateSimpleName = string | null /** @@ -1130,13 +1154,6 @@ export interface UserModify { status?: UserModifyStatus } -/** - * User IP lists for all nodes - */ -export interface UserIPListAll { - nodes: UserIPListAllNodes -} - export type UserIPListIps = { [key: string]: number } /** @@ -1148,6 +1165,13 @@ export interface UserIPList { export type UserIPListAllNodes = { [key: string]: UserIPList | null } +/** + * User IP lists for all nodes + */ +export interface UserIPListAll { + nodes: UserIPListAllNodes +} + export type UserHWIDResponseDeviceModel = string | null export type UserHWIDResponseOsVersion = string | null @@ -1215,15 +1239,24 @@ export interface UserCreate { status?: UserCreateStatus } +export type UserCountMetricStatsListStats = { [key: string]: UserCountMetricStat[] } + export type UserCountMetricStatsListPeriod = Period | null +export interface UserCountMetricStatsList { + period?: UserCountMetricStatsListPeriod + start: string + end: string + metric: UserCountMetric + count_during_period?: number + stats: UserCountMetricStatsListStats +} + export interface UserCountMetricStat { count: number period_start: string } -export type UserCountMetricStatsListStats = { [key: string]: UserCountMetricStat[] } - export type UserCountMetric = (typeof UserCountMetric)[keyof typeof UserCountMetric] // eslint-disable-next-line @typescript-eslint/no-redeclare @@ -1233,15 +1266,6 @@ export const UserCountMetric = { limited: 'limited', } as const -export interface UserCountMetricStatsList { - period?: UserCountMetricStatsListPeriod - start: string - end: string - metric: UserCountMetric - count_during_period?: number - stats: UserCountMetricStatsListStats -} - export type UsageTable = (typeof UsageTable)[keyof typeof UsageTable] // eslint-disable-next-line @typescript-eslint/no-redeclare @@ -1568,12 +1592,6 @@ export interface SettingsSchema { general?: SettingsSchemaGeneral } -export interface SettingsPermissions { - read?: SettingsPermissionsRead - read_general?: SettingsPermissionsReadGeneral - update?: SettingsPermissionsUpdate -} - export type SettingsPermissionsUpdateAnyOf = { [key: string]: PermissionScope | number } export type SettingsPermissionsUpdate = boolean | SettingsPermissionsUpdateAnyOf | null @@ -1586,6 +1604,12 @@ export type SettingsPermissionsReadAnyOf = { [key: string]: PermissionScope | nu export type SettingsPermissionsRead = boolean | SettingsPermissionsReadAnyOf | null +export interface SettingsPermissions { + read?: SettingsPermissionsRead + read_general?: SettingsPermissionsReadGeneral + update?: SettingsPermissionsUpdate +} + export type RunMethod = (typeof RunMethod)[keyof typeof RunMethod] // eslint-disable-next-line @typescript-eslint/no-redeclare @@ -1877,6 +1901,19 @@ export type NotificationSettingsTelegramChatId = number | null export type NotificationSettingsTelegramApiToken = string | null +export interface NotificationSettings { + notify_telegram?: boolean + notify_discord?: boolean + telegram_api_token?: NotificationSettingsTelegramApiToken + telegram_chat_id?: NotificationSettingsTelegramChatId + telegram_topic_id?: NotificationSettingsTelegramTopicId + discord_webhook_url?: NotificationSettingsDiscordWebhookUrl + channels?: NotificationChannels + proxy_url?: NotificationSettingsProxyUrl + /** */ + max_retries: number +} + export interface NotificationEnable { admin?: AdminNotificationEnable admin_role?: BaseNotificationEnable @@ -1906,19 +1943,6 @@ export interface NotificationChannels { api_key?: NotificationChannel } -export interface NotificationSettings { - notify_telegram?: boolean - notify_discord?: boolean - telegram_api_token?: NotificationSettingsTelegramApiToken - telegram_chat_id?: NotificationSettingsTelegramChatId - telegram_topic_id?: NotificationSettingsTelegramTopicId - discord_webhook_url?: NotificationSettingsDiscordWebhookUrl - channels?: NotificationChannels - proxy_url?: NotificationSettingsProxyUrl - /** */ - max_retries: number -} - export type NotificationChannelDiscordWebhookUrl = string | null export type NotificationChannelTelegramTopicId = number | null @@ -1997,18 +2021,6 @@ export type NodesPermissionsReadSimpleAnyOf = { [key: string]: PermissionScope | export type NodesPermissionsReadSimple = boolean | NodesPermissionsReadSimpleAnyOf | null -export interface NodesPermissions { - create?: NodesPermissionsCreate - read?: NodesPermissionsRead - read_simple?: NodesPermissionsReadSimple - update?: NodesPermissionsUpdate - delete?: NodesPermissionsDelete - reconnect?: NodesPermissionsReconnect - update_core?: NodesPermissionsUpdateCore - logs?: NodesPermissionsLogs - stats?: NodesPermissionsStats -} - export type NodesPermissionsReadAnyOf = { [key: string]: PermissionScope | number } export type NodesPermissionsRead = boolean | NodesPermissionsReadAnyOf | null @@ -2019,6 +2031,13 @@ export type NodesPermissionsCreate = boolean | NodesPermissionsCreateAnyOf | nul export type NodeUsageStatsListPeriod = Period | null +export interface NodeUsageStatsList { + period?: NodeUsageStatsListPeriod + start: string + end: string + stats: NodeUsageStatsListStats +} + export interface NodeUsageStat { uplink: number downlink: number @@ -2027,13 +2046,6 @@ export interface NodeUsageStat { export type NodeUsageStatsListStats = { [key: string]: NodeUsageStat[] } -export interface NodeUsageStatsList { - period?: NodeUsageStatsListPeriod - start: string - end: string - stats: NodeUsageStatsListStats -} - export type NodeStatus = (typeof NodeStatus)[keyof typeof NodeStatus] // eslint-disable-next-line @typescript-eslint/no-redeclare @@ -2224,19 +2236,6 @@ export interface NodeGeoFilesUpdate { export type NodeCreateProxyUrl = string | null -export interface NodeCoreUpdate { - /** @pattern ^(latest|v?\d+\.\d+\.\d+)$ */ - core_version?: string -} - -export type NodeConnectionType = (typeof NodeConnectionType)[keyof typeof NodeConnectionType] - -// eslint-disable-next-line @typescript-eslint/no-redeclare -export const NodeConnectionType = { - grpc: 'grpc', - rest: 'rest', -} as const - export interface NodeCreate { name: string address: string @@ -2265,6 +2264,19 @@ export interface NodeCreate { proxy_url?: NodeCreateProxyUrl } +export interface NodeCoreUpdate { + /** @pattern ^(latest|v?\d+\.\d+\.\d+)$ */ + core_version?: string +} + +export type NodeConnectionType = (typeof NodeConnectionType)[keyof typeof NodeConnectionType] + +// eslint-disable-next-line @typescript-eslint/no-redeclare +export const NodeConnectionType = { + grpc: 'grpc', + rest: 'rest', +} as const + export type NextPlanModelExpire = number | null export type NextPlanModelDataLimit = number | null @@ -2369,25 +2381,19 @@ export type HwidsPermissionsDeleteAnyOf = { [key: string]: PermissionScope | num export type HwidsPermissionsDelete = boolean | HwidsPermissionsDeleteAnyOf | null +export type HwidsPermissionsReadAnyOf = { [key: string]: PermissionScope | number } + +export type HwidsPermissionsRead = boolean | HwidsPermissionsReadAnyOf | null + export interface HwidsPermissions { read?: HwidsPermissionsRead delete?: HwidsPermissionsDelete } -export type HwidsPermissionsReadAnyOf = { [key: string]: PermissionScope | number } - -export type HwidsPermissionsRead = boolean | HwidsPermissionsReadAnyOf | null - export type HostsPermissionsUpdateAnyOf = { [key: string]: PermissionScope | number } export type HostsPermissionsUpdate = boolean | HostsPermissionsUpdateAnyOf | null -export interface HostsPermissions { - create?: HostsPermissionsCreate - read?: HostsPermissionsRead - update?: HostsPermissionsUpdate -} - export type HostsPermissionsReadAnyOf = { [key: string]: PermissionScope | number } export type HostsPermissionsRead = boolean | HostsPermissionsReadAnyOf | null @@ -2396,6 +2402,12 @@ export type HostsPermissionsCreateAnyOf = { [key: string]: PermissionScope | num export type HostsPermissionsCreate = boolean | HostsPermissionsCreateAnyOf | null +export interface HostsPermissions { + create?: HostsPermissionsCreate + read?: HostsPermissionsRead + update?: HostsPermissionsUpdate +} + export interface HostNotificationEnable { create?: boolean modify?: boolean @@ -2461,11 +2473,6 @@ export interface HTTPException { detail: string } -export interface GroupsResponse { - groups: GroupResponse[] - total: number -} - /** * Lightweight group model with only id and name for performance. */ @@ -2496,6 +2503,11 @@ export interface GroupResponse { total_users?: number } +export interface GroupsResponse { + groups: GroupResponse[] + total: number +} + export type GroupModifyInboundTags = string[] | null export interface GroupModify { @@ -2548,19 +2560,262 @@ export interface GRPCSettings { initial_windows_size?: GRPCSettingsInitialWindowsSize } -export type FragmentSettingsSingBox = SingBoxFragmentSettings | null +export type FragmentSettingsOutputSingBox = SingBoxFragmentSettings | null + +export type FragmentSettingsOutputXray = XrayFragmentSettingsOutput | null + +export interface FragmentSettingsOutput { + xray?: FragmentSettingsOutputXray + sing_box?: FragmentSettingsOutputSingBox +} + +export type FragmentSettingsInputSingBox = SingBoxFragmentSettings | null -export type FragmentSettingsXray = XrayFragmentSettings | null +export type FragmentSettingsInputXray = XrayFragmentSettingsInput | null -export interface FragmentSettings { - xray?: FragmentSettingsXray - sing_box?: FragmentSettingsSingBox +export interface FragmentSettingsInput { + xray?: FragmentSettingsInputXray + sing_box?: FragmentSettingsInputSingBox } export interface Forbidden { detail?: string } +export type FinalMaskXicmpSettingsId = number | null + +export type FinalMaskXicmpSettingsListenIp = string | null + +export interface FinalMaskXicmpSettings { + listenIp?: FinalMaskXicmpSettingsListenIp + id?: FinalMaskXicmpSettingsId + [key: string]: unknown +} + +export type FinalMaskUdpType = (typeof FinalMaskUdpType)[keyof typeof FinalMaskUdpType] + +// eslint-disable-next-line @typescript-eslint/no-redeclare +export const FinalMaskUdpType = { + 'header-custom': 'header-custom', + 'header-dns': 'header-dns', + 'header-dtls': 'header-dtls', + 'header-srtp': 'header-srtp', + 'header-utp': 'header-utp', + 'header-wechat': 'header-wechat', + 'header-wireguard': 'header-wireguard', + 'mkcp-original': 'mkcp-original', + 'mkcp-aes128gcm': 'mkcp-aes128gcm', + noise: 'noise', + salamander: 'salamander', + sudoku: 'sudoku', + xdns: 'xdns', + xicmp: 'xicmp', +} as const + +export type FinalMaskUdpLayerSettingsAnyOf = { [key: string]: unknown } + +export type FinalMaskUdpLayerSettings = + | FinalMaskUdpHeaderCustomSettings + | FinalMaskPasswordSettings + | FinalMaskSudokuSettings + | FinalMaskDomainSettings + | FinalMaskXicmpSettings + | FinalMaskNoiseSettings + | FinalMaskUdpLayerSettingsAnyOf + +export interface FinalMaskUdpLayer { + type: FinalMaskUdpType + settings?: FinalMaskUdpLayerSettings + [key: string]: unknown +} + +export type FinalMaskUdpHopInterval = string | number | null + +export type FinalMaskUdpHopPorts = string | null + +export interface FinalMaskUdpHop { + ports?: FinalMaskUdpHopPorts + interval?: FinalMaskUdpHopInterval + [key: string]: unknown +} + +export type FinalMaskUdpHeaderCustomSettingsServer = XrayNoiseSettings[] | null + +export type FinalMaskUdpHeaderCustomSettingsClient = XrayNoiseSettings[] | null + +export interface FinalMaskUdpHeaderCustomSettings { + client?: FinalMaskUdpHeaderCustomSettingsClient + server?: FinalMaskUdpHeaderCustomSettingsServer + [key: string]: unknown +} + +export type FinalMaskTcpType = (typeof FinalMaskTcpType)[keyof typeof FinalMaskTcpType] + +// eslint-disable-next-line @typescript-eslint/no-redeclare +export const FinalMaskTcpType = { + 'header-custom': 'header-custom', + fragment: 'fragment', + sudoku: 'sudoku', +} as const + +export type FinalMaskTcpLayerOutputSettingsAnyOf = { [key: string]: unknown } + +export type FinalMaskTcpLayerOutputSettings = FinalMaskTcpHeaderCustomSettings | XrayFragmentSettingsOutput | FinalMaskSudokuSettings | FinalMaskTcpLayerOutputSettingsAnyOf + +export interface FinalMaskTcpLayerOutput { + type: FinalMaskTcpType + settings?: FinalMaskTcpLayerOutputSettings + [key: string]: unknown +} + +export type FinalMaskTcpLayerInputSettingsAnyOf = { [key: string]: unknown } + +export type FinalMaskTcpLayerInputSettings = FinalMaskTcpHeaderCustomSettings | XrayFragmentSettingsInput | FinalMaskSudokuSettings | FinalMaskTcpLayerInputSettingsAnyOf + +export interface FinalMaskTcpLayerInput { + type: FinalMaskTcpType + settings?: FinalMaskTcpLayerInputSettings + [key: string]: unknown +} + +export type FinalMaskTcpHeaderCustomSettingsErrors = XrayNoiseSettings[][] | null + +export type FinalMaskTcpHeaderCustomSettingsServers = XrayNoiseSettings[][] | null + +export type FinalMaskTcpHeaderCustomSettingsClients = XrayNoiseSettings[][] | null + +export interface FinalMaskTcpHeaderCustomSettings { + clients?: FinalMaskTcpHeaderCustomSettingsClients + servers?: FinalMaskTcpHeaderCustomSettingsServers + errors?: FinalMaskTcpHeaderCustomSettingsErrors + [key: string]: unknown +} + +export type FinalMaskSudokuSettingsPaddingMax = number | null + +export type FinalMaskSudokuSettingsPaddingMin = number | null + +export type FinalMaskSudokuSettingsCustomTables = string[] | null + +export type FinalMaskSudokuSettingsCustomTable = string | null + +export type FinalMaskSudokuSettingsAscii = string | null + +export type FinalMaskSudokuSettingsPassword = string | null + +export interface FinalMaskSudokuSettings { + password?: FinalMaskSudokuSettingsPassword + ascii?: FinalMaskSudokuSettingsAscii + customTable?: FinalMaskSudokuSettingsCustomTable + customTables?: FinalMaskSudokuSettingsCustomTables + paddingMin?: FinalMaskSudokuSettingsPaddingMin + paddingMax?: FinalMaskSudokuSettingsPaddingMax + [key: string]: unknown +} + +export type FinalMaskQuicParamsMaxIncomingStreams = number | null + +export type FinalMaskQuicParamsDisablePathMTUDiscovery = boolean | null + +export type FinalMaskQuicParamsKeepAlivePeriod = number | null + +export type FinalMaskQuicParamsMaxIdleTimeout = number | null + +export type FinalMaskQuicParamsMaxConnectionReceiveWindow = number | null + +export type FinalMaskQuicParamsInitConnectionReceiveWindow = number | null + +export type FinalMaskQuicParamsMaxStreamReceiveWindow = number | null + +export type FinalMaskQuicParamsInitStreamReceiveWindow = number | null + +export type FinalMaskQuicParamsUdpHop = FinalMaskUdpHop | null + +export type FinalMaskQuicParamsBrutalDown = string | number | null + +export type FinalMaskQuicParamsBrutalUp = string | number | null + +export type FinalMaskQuicParamsDebug = boolean | null + +export interface FinalMaskQuicParams { + congestion?: FinalMaskQuicParamsCongestion + debug?: FinalMaskQuicParamsDebug + brutalUp?: FinalMaskQuicParamsBrutalUp + brutalDown?: FinalMaskQuicParamsBrutalDown + udpHop?: FinalMaskQuicParamsUdpHop + initStreamReceiveWindow?: FinalMaskQuicParamsInitStreamReceiveWindow + maxStreamReceiveWindow?: FinalMaskQuicParamsMaxStreamReceiveWindow + initConnectionReceiveWindow?: FinalMaskQuicParamsInitConnectionReceiveWindow + maxConnectionReceiveWindow?: FinalMaskQuicParamsMaxConnectionReceiveWindow + maxIdleTimeout?: FinalMaskQuicParamsMaxIdleTimeout + keepAlivePeriod?: FinalMaskQuicParamsKeepAlivePeriod + disablePathMTUDiscovery?: FinalMaskQuicParamsDisablePathMTUDiscovery + maxIncomingStreams?: FinalMaskQuicParamsMaxIncomingStreams + [key: string]: unknown +} + +export type FinalMaskQuicCongestion = (typeof FinalMaskQuicCongestion)[keyof typeof FinalMaskQuicCongestion] + +// eslint-disable-next-line @typescript-eslint/no-redeclare +export const FinalMaskQuicCongestion = { + reno: 'reno', + bbr: 'bbr', + brutal: 'brutal', + 'force-brutal': 'force-brutal', +} as const + +export type FinalMaskQuicParamsCongestion = FinalMaskQuicCongestion | null + +export type FinalMaskPasswordSettingsPassword = string | null + +export interface FinalMaskPasswordSettings { + password?: FinalMaskPasswordSettingsPassword + [key: string]: unknown +} + +export type FinalMaskNoiseSettingsNoise = XrayNoiseSettings[] | null + +export type FinalMaskNoiseSettingsReset = number | null + +export interface FinalMaskNoiseSettings { + reset?: FinalMaskNoiseSettingsReset + noise?: FinalMaskNoiseSettingsNoise + [key: string]: unknown +} + +export type FinalMaskDomainSettingsDomain = string | null + +export interface FinalMaskDomainSettings { + domain?: FinalMaskDomainSettingsDomain + [key: string]: unknown +} + +export type FinalMaskOutputQuicParams = FinalMaskQuicParams | null + +export type FinalMaskOutputUdp = FinalMaskUdpLayer[] | null + +export type FinalMaskOutputTcp = FinalMaskTcpLayerOutput[] | null + +export interface FinalMaskOutput { + tcp?: FinalMaskOutputTcp + udp?: FinalMaskOutputUdp + quicParams?: FinalMaskOutputQuicParams + [key: string]: unknown +} + +export type FinalMaskInputQuicParams = FinalMaskQuicParams | null + +export type FinalMaskInputUdp = FinalMaskUdpLayer[] | null + +export type FinalMaskInputTcp = FinalMaskTcpLayerInput[] | null + +export interface FinalMaskInput { + tcp?: FinalMaskInputTcp + udp?: FinalMaskInputUdp + quicParams?: FinalMaskInputQuicParams + [key: string]: unknown +} + export type ExtraSettingsMethod = ShadowsocksMethods | null export interface ExtraSettings { @@ -2609,6 +2864,8 @@ export interface CreateUserFromTemplate { username: string } +export type CreateHostFinalMaskSettings = FinalMaskInput | null + export type CreateHostSubscriptionTemplates = SubscriptionTemplates | null export type CreateHostWireguardOverrides = WireGuardHostOverrides | null @@ -2627,32 +2884,12 @@ export type CreateHostVlessRoute = string | null export type CreateHostNoiseSettings = NoiseSettings | null -export type CreateHostFragmentSettings = FragmentSettings | null +export type CreateHostFragmentSettings = FragmentSettingsInput | null export type CreateHostMuxSettings = MuxSettingsInput | null export type CreateHostTransportSettings = TransportSettingsInput | null -export type CreateHostHttpHeadersAnyOf = { [key: string]: string } - -export type CreateHostHttpHeaders = CreateHostHttpHeadersAnyOf | null - -export type CreateHostAllowinsecure = boolean | null - -export type CreateHostAlpn = ProxyHostALPN[] | null - -export type CreateHostPath = string | null - -export type CreateHostHost = string[] | null - -export type CreateHostSni = string[] | null - -export type CreateHostPort = number | null - -export type CreateHostInboundTag = string | null - -export type CreateHostId = number | null - export interface CreateHost { id?: CreateHostId remark: string @@ -2683,6 +2920,35 @@ export interface CreateHost { verify_peer_cert_by_name?: CreateHostVerifyPeerCertByName wireguard_overrides?: CreateHostWireguardOverrides subscription_templates?: CreateHostSubscriptionTemplates + final_mask_settings?: CreateHostFinalMaskSettings +} + +export type CreateHostHttpHeadersAnyOf = { [key: string]: string } + +export type CreateHostHttpHeaders = CreateHostHttpHeadersAnyOf | null + +export type CreateHostAllowinsecure = boolean | null + +export type CreateHostAlpn = ProxyHostALPN[] | null + +export type CreateHostPath = string | null + +export type CreateHostHost = string[] | null + +export type CreateHostSni = string[] | null + +export type CreateHostPort = number | null + +export type CreateHostInboundTag = string | null + +export type CreateHostId = number | null + +/** + * Response model for lightweight core list. + */ +export interface CoresSimpleResponse { + cores: CoreSimple[] + total: number } export type CoreType = (typeof CoreType)[keyof typeof CoreType] @@ -2706,14 +2972,6 @@ export interface CoreSimple { type?: CoreSimpleType } -/** - * Response model for lightweight core list. - */ -export interface CoresSimpleResponse { - cores: CoreSimple[] - total: number -} - export interface CoreResponseList { count: number cores?: CoreResponse[] @@ -2770,6 +3028,17 @@ export const ConfigFormat = { block: 'block', } as const +export type ClientTemplateType = (typeof ClientTemplateType)[keyof typeof ClientTemplateType] + +// eslint-disable-next-line @typescript-eslint/no-redeclare +export const ClientTemplateType = { + clash_subscription: 'clash_subscription', + xray_subscription: 'xray_subscription', + singbox_subscription: 'singbox_subscription', + user_agent: 'user_agent', + grpc_user_agent: 'grpc_user_agent', +} as const + export interface ClientTemplateSimple { id: number name: string @@ -2782,17 +3051,6 @@ export interface ClientTemplatesSimpleResponse { total: number } -export type ClientTemplateType = (typeof ClientTemplateType)[keyof typeof ClientTemplateType] - -// eslint-disable-next-line @typescript-eslint/no-redeclare -export const ClientTemplateType = { - clash_subscription: 'clash_subscription', - xray_subscription: 'xray_subscription', - singbox_subscription: 'singbox_subscription', - user_agent: 'user_agent', - grpc_user_agent: 'grpc_user_agent', -} as const - export interface ClientTemplateResponse { id: number name: string @@ -2859,14 +3117,6 @@ export type CRUDPermissionsReadSimpleAnyOf = { [key: string]: PermissionScope | export type CRUDPermissionsReadSimple = boolean | CRUDPermissionsReadSimpleAnyOf | null -export type CRUDPermissionsReadAnyOf = { [key: string]: PermissionScope | number } - -export type CRUDPermissionsRead = boolean | CRUDPermissionsReadAnyOf | null - -export type CRUDPermissionsCreateAnyOf = { [key: string]: PermissionScope | number } - -export type CRUDPermissionsCreate = boolean | CRUDPermissionsCreateAnyOf | null - /** * Standard create/read/read_simple/update/delete permissions. Used directly by: groups, templates, client_templates, cores, admin_roles. @@ -2880,6 +3130,14 @@ export interface CRUDPermissions { delete?: CRUDPermissionsDelete } +export type CRUDPermissionsReadAnyOf = { [key: string]: PermissionScope | number } + +export type CRUDPermissionsRead = boolean | CRUDPermissionsReadAnyOf | null + +export type CRUDPermissionsCreateAnyOf = { [key: string]: PermissionScope | number } + +export type CRUDPermissionsCreate = boolean | CRUDPermissionsCreateAnyOf | null + export type BulkWireGuardPeerIPsExpireBefore = string | null export type BulkWireGuardPeerIPsExpireAfter = string | null @@ -3114,6 +3372,8 @@ export interface BaseNotificationEnable { delete?: boolean } +export type BaseHostFinalMaskSettings = FinalMaskOutput | null + export type BaseHostSubscriptionTemplates = SubscriptionTemplates | null export type BaseHostWireguardOverrides = WireGuardHostOverrides | null @@ -3132,7 +3392,7 @@ export type BaseHostVlessRoute = string | null export type BaseHostNoiseSettings = NoiseSettings | null -export type BaseHostFragmentSettings = FragmentSettings | null +export type BaseHostFragmentSettings = FragmentSettingsOutput | null export type BaseHostMuxSettings = MuxSettingsOutput | null @@ -3188,6 +3448,7 @@ export interface BaseHost { verify_peer_cert_by_name?: BaseHostVerifyPeerCertByName wireguard_overrides?: BaseHostWireguardOverrides subscription_templates?: BaseHostSubscriptionTemplates + final_mask_settings?: BaseHostFinalMaskSettings } export type ApplicationDescription = { [key: string]: string } @@ -3229,14 +3490,6 @@ export type AdminsPermissionsResetUsageAnyOf = { [key: string]: PermissionScope export type AdminsPermissionsResetUsage = boolean | AdminsPermissionsResetUsageAnyOf | null -export type AdminsPermissionsDeleteAnyOf = { [key: string]: PermissionScope | number } - -export type AdminsPermissionsDelete = boolean | AdminsPermissionsDeleteAnyOf | null - -export type AdminsPermissionsUpdateAnyOf = { [key: string]: PermissionScope | number } - -export type AdminsPermissionsUpdate = boolean | AdminsPermissionsUpdateAnyOf | null - export interface AdminsPermissions { create?: AdminsPermissionsCreate read?: AdminsPermissionsRead @@ -3246,6 +3499,14 @@ export interface AdminsPermissions { reset_usage?: AdminsPermissionsResetUsage } +export type AdminsPermissionsDeleteAnyOf = { [key: string]: PermissionScope | number } + +export type AdminsPermissionsDelete = boolean | AdminsPermissionsDeleteAnyOf | null + +export type AdminsPermissionsUpdateAnyOf = { [key: string]: PermissionScope | number } + +export type AdminsPermissionsUpdate = boolean | AdminsPermissionsUpdateAnyOf | null + export type AdminsPermissionsReadSimpleAnyOf = { [key: string]: PermissionScope | number } export type AdminsPermissionsReadSimple = boolean | AdminsPermissionsReadSimpleAnyOf | null