Skip to content

Commit b9c23c5

Browse files
feat(hosts): Add pinnedPeerCertSha256 support
1 parent 7e1e54a commit b9c23c5

File tree

8 files changed

+82
-45
lines changed

8 files changed

+82
-45
lines changed

app/core/hosts.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ async def _prepare_subscription_inbound_data(
6969
if tls_value is None:
7070
tls_value = inbound_config.get("tls", "none")
7171

72+
pinnedPeerCertSha256 = host.pinnedPeerCertSha256
7273
alpn_list = [alpn.value for alpn in host.alpn] if host.alpn else inbound_config.get("alpn", [])
7374
fp = host.fingerprint.value if host.fingerprint.value != "none" else inbound_config.get("fp")
7475
fp = fp or ("chrome" if tls_value == "reality" else "")
@@ -81,6 +82,7 @@ async def _prepare_subscription_inbound_data(
8182
sni=sni_list,
8283
fingerprint=fp,
8384
allowinsecure=ais,
85+
pinnedPeerCertSha256=pinnedPeerCertSha256,
8486
alpn_list=alpn_list,
8587
ech_config_list=host.ech_config_list,
8688
reality_public_key=reality_pbk,
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
"""add pinnedPeerCertSha256 to hosts
2+
3+
Revision ID: 5213b80a795c
4+
Revises: 7b5e1623c19a
5+
Create Date: 2026-02-11 22:34:07.678777
6+
7+
"""
8+
from alembic import op
9+
import sqlalchemy as sa
10+
11+
12+
# revision identifiers, used by Alembic.
13+
revision = '5213b80a795c'
14+
down_revision = '7b5e1623c19a'
15+
branch_labels = None
16+
depends_on = None
17+
18+
19+
def upgrade() -> None:
20+
# ### commands auto generated by Alembic - please adjust! ###
21+
op.drop_index(op.f('ix_apscheduler_jobs_next_run_time'), table_name='apscheduler_jobs')
22+
op.drop_table('apscheduler_jobs')
23+
op.add_column('hosts', sa.Column('pinnedPeerCertSha256', sa.String(length=128), nullable=True))
24+
# ### end Alembic commands ###
25+
26+
27+
def downgrade() -> None:
28+
# ### commands auto generated by Alembic - please adjust! ###
29+
op.drop_column('hosts', 'pinnedPeerCertSha256')
30+
op.create_table('apscheduler_jobs',
31+
sa.Column('id', sa.VARCHAR(length=191), nullable=False),
32+
sa.Column('next_run_time', sa.FLOAT(), nullable=True),
33+
sa.Column('job_state', sa.BLOB(), nullable=False),
34+
sa.PrimaryKeyConstraint('id')
35+
)
36+
op.create_index(op.f('ix_apscheduler_jobs_next_run_time'), 'apscheduler_jobs', ['next_run_time'], unique=False)
37+
# ### end Alembic commands ###

app/db/models.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,7 @@ class ProxyHost(Base):
450450
)
451451
ech_config_list: Mapped[Optional[str]] = mapped_column(String(512), default=None)
452452
vless_route: Mapped[Optional[str]] = mapped_column(String(4), default=None)
453+
pinnedPeerCertSha256: Mapped[Optional[str]] = mapped_column(String(128), default=None)
453454

454455

455456
class System(Base):

app/models/host.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,7 @@ class BaseHost(BaseModel):
238238
priority: int
239239
status: set[UserStatus] | None = Field(default_factory=set)
240240
ech_config_list: str | None = Field(default=None)
241+
pinnedPeerCertSha256:str| None = Field(default=None)
241242

242243
model_config = ConfigDict(from_attributes=True)
243244

app/models/subscription.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ class TLSConfig(BaseModel):
1919
allowinsecure: bool = Field(False)
2020
alpn_list: list[str] = Field(default_factory=list)
2121
ech_config_list: str | None = Field(None)
22+
pinnedPeerCertSha256: str | None = Field(default=None)
2223

2324
# Reality specific
2425
reality_public_key: str = Field("")

app/subscription/links.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ def _apply_tls_settings(self, payload: dict, tls_config: TLSConfig, fragment_set
173173
sni = tls_config.sni if isinstance(tls_config.sni, str) else ""
174174
payload["sni"] = sni
175175
payload["fp"] = tls_config.fingerprint
176+
payload["pcs"] = tls_config.pinnedPeerCertSha256
176177

177178
# Use pre-formatted alpn for links (comma-separated string)
178179
if tls_config.alpn_links:

app/subscription/singbox.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -136,15 +136,13 @@ def _transport_ws(self, config: WebSocketTransportConfig, path: str) -> dict:
136136

137137
def _transport_grpc(self, config: GRPCTransportConfig, path: str) -> dict:
138138
"""Handle GRPC transport - only gets GRPC config"""
139-
return self._normalize_and_remove_none_values(
140-
{
141-
"type": "grpc",
142-
"service_name": path,
143-
"idle_timeout": f"{config.idle_timeout}s" if config.idle_timeout else "15s",
144-
"ping_timeout": f"{config.health_check_timeout}s" if config.health_check_timeout else "15s",
145-
"permit_without_stream": config.permit_without_stream,
146-
}
147-
)
139+
return self._normalize_and_remove_none_values({
140+
"type": "grpc",
141+
"service_name": path,
142+
"idle_timeout": f"{config.idle_timeout}s" if config.idle_timeout else "15s",
143+
"ping_timeout": f"{config.health_check_timeout}s" if config.health_check_timeout else "15s",
144+
"permit_without_stream": config.permit_without_stream,
145+
})
148146

149147
def _transport_httpupgrade(self, config: WebSocketTransportConfig, path: str) -> dict:
150148
"""Handle HTTPUpgrade transport - only gets WS config (similar to WS)"""
@@ -192,6 +190,9 @@ def _apply_tls(self, tls_config: TLSConfig, fragment_settings: dict | None = Non
192190
if isinstance(tls_config.sni, str)
193191
else (tls_config.sni[0] if tls_config.sni else None),
194192
"insecure": tls_config.allowinsecure,
193+
"certificate_public_key_sha256": [tls_config.pinnedPeerCertSha256]
194+
if tls_config.pinnedPeerCertSha256
195+
else [],
195196
"utls": {
196197
"enabled": bool(tls_config.fingerprint) or tls_config.tls == "reality",
197198
"fingerprint": tls_config.fingerprint,

app/subscription/xray.py

Lines changed: 29 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -261,27 +261,23 @@ def _transport_quic(self, config: QUICTransportConfig, path: str) -> dict:
261261
"""Handle QUIC transport - only gets QUIC config"""
262262
host = config.host if isinstance(config.host, str) else (config.host[0] if config.host else "")
263263

264-
return self._normalize_and_remove_none_values(
265-
{
266-
"security": host,
267-
"header": {"type": config.header_type},
268-
"key": path,
269-
}
270-
)
264+
return self._normalize_and_remove_none_values({
265+
"security": host,
266+
"header": {"type": config.header_type},
267+
"key": path,
268+
})
271269

272270
def _transport_kcp(self, config: KCPTransportConfig, path: str) -> dict:
273271
"""Handle KCP transport - only gets KCP config"""
274-
return self._normalize_and_remove_none_values(
275-
{
276-
"mtu": config.mtu if config.mtu is not None else 1350,
277-
"tti": config.tti if config.tti is not None else 50,
278-
"uplinkCapacity": config.uplink_capacity if config.uplink_capacity is not None else 5,
279-
"downlinkCapacity": config.downlink_capacity if config.downlink_capacity is not None else 20,
280-
"congestion": config.congestion,
281-
"readBufferSize": config.read_buffer_size if config.read_buffer_size is not None else 2,
282-
"writeBufferSize": config.write_buffer_size if config.write_buffer_size is not None else 2,
283-
}
284-
)
272+
return self._normalize_and_remove_none_values({
273+
"mtu": config.mtu if config.mtu is not None else 1350,
274+
"tti": config.tti if config.tti is not None else 50,
275+
"uplinkCapacity": config.uplink_capacity if config.uplink_capacity is not None else 5,
276+
"downlinkCapacity": config.downlink_capacity if config.downlink_capacity is not None else 20,
277+
"congestion": config.congestion,
278+
"readBufferSize": config.read_buffer_size if config.read_buffer_size is not None else 2,
279+
"writeBufferSize": config.write_buffer_size if config.write_buffer_size is not None else 2,
280+
})
285281

286282
def _apply_transport(self, network: str, inbound: SubscriptionInboundData, path: str) -> dict | None:
287283
"""Apply transport settings using registry pattern"""
@@ -297,24 +293,23 @@ def _apply_tls(self, tls_config: TLSConfig, security: str) -> dict:
297293
sni = tls_config.sni if isinstance(tls_config.sni, str) else (tls_config.sni[0] if tls_config.sni else None)
298294

299295
if security == "reality":
300-
return self._normalize_and_remove_none_values(
301-
{
302-
"serverName": sni,
303-
"fingerprint": tls_config.fingerprint,
304-
"show": False,
305-
"publicKey": tls_config.reality_public_key,
306-
"shortId": tls_config.reality_short_id,
307-
"spiderX": tls_config.reality_spx,
308-
"mldsa65Verify": tls_config.mldsa65_verify,
309-
}
310-
)
296+
return self._normalize_and_remove_none_values({
297+
"serverName": sni,
298+
"fingerprint": tls_config.fingerprint,
299+
"show": False,
300+
"publicKey": tls_config.reality_public_key,
301+
"shortId": tls_config.reality_short_id,
302+
"spiderX": tls_config.reality_spx,
303+
"mldsa65Verify": tls_config.mldsa65_verify,
304+
})
311305
else: # tls
312306
config = {
313307
"serverName": sni,
314308
"allowInsecure": tls_config.allowinsecure,
315309
"show": False,
316310
"fingerprint": tls_config.fingerprint,
317311
"echConfigList": tls_config.ech_config_list,
312+
"pinnedPeerCertSha256": tls_config.pinnedPeerCertSha256,
318313
}
319314
if tls_config.alpn_list:
320315
config["alpn"] = tls_config.alpn_list # Use list for xray
@@ -371,13 +366,11 @@ def _download_config(self, download_settings: SubscriptionInboundData, link_form
371366
sockopt=sockopt,
372367
)
373368

374-
return self._normalize_and_remove_none_values(
375-
{
376-
"address": download_settings.address,
377-
"port": self._select_port(download_settings.port),
378-
**stream_settings,
379-
}
380-
)
369+
return self._normalize_and_remove_none_values({
370+
"address": download_settings.address,
371+
"port": self._select_port(download_settings.port),
372+
**stream_settings,
373+
})
381374

382375
# ========== Protocol Builders (Registry Methods) ==========
383376

0 commit comments

Comments
 (0)