Skip to content
4 changes: 2 additions & 2 deletions app/core/hosts.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""add final_mask_settings to hosts table

Revision ID: f976bfcf4738
Revises: 73c78c6a9b24
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 = '73c78c6a9b24'
branch_labels = None
depends_on = None
Comment thread
ImMohammad20000 marked this conversation as resolved.


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 ###
1 change: 1 addition & 0 deletions app/db/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -508,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):
Expand Down
213 changes: 207 additions & 6 deletions app/models/host.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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):
Expand All @@ -51,17 +64,204 @@ 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})?$")
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})?$")

model_config = ConfigDict(extra="allow", populate_by_name=True)


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]
)
Comment thread
ImMohammad20000 marked this conversation as resolved.


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="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="before")
@classmethod
def parse_settings(cls, value):
return _dispatch_final_mask_settings(value, FinalMaskUdpType, FINAL_MASK_UDP_SETTINGS_MODELS)


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"
Expand Down Expand Up @@ -293,6 +493,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)

Expand Down
3 changes: 2 additions & 1 deletion app/models/subscription.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from typing import Any

from pydantic import BaseModel, Field, computed_field
from app.models.host import FinalMask


class TLSConfig(BaseModel):
Expand Down Expand Up @@ -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)
Expand Down
7 changes: 6 additions & 1 deletion app/subscription/links.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
TLSConfig,
WebSocketTransportConfig,
XHTTPTransportConfig,
FinalMask,
)
from config import subscription_env_settings

Expand Down Expand Up @@ -167,7 +168,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"""
Expand Down
9 changes: 6 additions & 3 deletions app/subscription/xray.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
TLSConfig,
WebSocketTransportConfig,
XHTTPTransportConfig,
FinalMask,
)
from app.templates import render_template_string
from app.utils.helpers import UUIDEncoder
Expand Down Expand Up @@ -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)

Expand Down
Loading