From 038642ebff2ea87ca512f4ffc4722dbb227b3fb2 Mon Sep 17 00:00:00 2001 From: etm Date: Sat, 22 Aug 2026 21:44:45 +0330 Subject: [PATCH 1/2] fix(singbox): handle unsafe uTLS fingerprint --- app/subscription/singbox.py | 13 ++++++++++--- tests/test_subscription_singbox_tls.py | 22 ++++++++++++++++++++++ 2 files changed, 32 insertions(+), 3 deletions(-) create mode 100644 tests/test_subscription_singbox_tls.py diff --git a/app/subscription/singbox.py b/app/subscription/singbox.py index 7a73231e7..d57226d4f 100644 --- a/app/subscription/singbox.py +++ b/app/subscription/singbox.py @@ -215,6 +215,13 @@ def _apply_transport(self, network: str, inbound: SubscriptionInboundData, path: def _apply_tls(self, tls_config: TLSConfig, fragment_settings: dict | None = None) -> dict: """Apply TLS settings - receives TLS config and optional fragment settings""" + fingerprint = tls_config.fingerprint + if fingerprint == "unsafe": + # Xray uses "unsafe" to select the native TLS client, but sing-box + # rejects it as an unknown uTLS fingerprint. Reality requires uTLS, + # so keep it enabled there with sing-box's default fingerprint. + fingerprint = "chrome" if tls_config.tls == "reality" else "" + config = { "enabled": tls_config.tls in ("tls", "reality"), "server_name": tls_config.sni @@ -225,10 +232,10 @@ def _apply_tls(self, tls_config: TLSConfig, fragment_settings: dict | None = Non if tls_config.pinned_peer_cert_sha256 else None, "utls": { - "enabled": bool(tls_config.fingerprint) or tls_config.tls == "reality", - "fingerprint": tls_config.fingerprint, + "enabled": bool(fingerprint) or tls_config.tls == "reality", + "fingerprint": fingerprint, } - if tls_config.fingerprint or tls_config.tls == "reality" + if fingerprint or tls_config.tls == "reality" else None, "alpn": tls_config.alpn_singbox, # Pre-formatted for sing-box! "ech": { diff --git a/tests/test_subscription_singbox_tls.py b/tests/test_subscription_singbox_tls.py new file mode 100644 index 000000000..0c3825a51 --- /dev/null +++ b/tests/test_subscription_singbox_tls.py @@ -0,0 +1,22 @@ +from app.models.subscription import TLSConfig +from app.subscription.singbox import SingBoxConfiguration + + +def test_unsafe_fingerprint_uses_native_tls_in_singbox(): + tls = SingBoxConfiguration()._apply_tls(TLSConfig(tls="tls", sni="example.com", fingerprint="unsafe")) + + assert "utls" not in tls + + +def test_unsafe_fingerprint_falls_back_to_chrome_for_singbox_reality(): + tls = SingBoxConfiguration()._apply_tls( + TLSConfig( + tls="reality", + sni="example.com", + fingerprint="unsafe", + reality_public_key="public-key", + reality_short_id="12345678", + ) + ) + + assert tls["utls"] == {"enabled": True, "fingerprint": "chrome"} From b1b0a266c8fd1b28b56e65e8ab5afdafe1d98110 Mon Sep 17 00:00:00 2001 From: etm Date: Sat, 22 Aug 2026 21:56:25 +0330 Subject: [PATCH 2/2] fix(subscriptions): normalize unsafe client fingerprints --- app/subscription/clash.py | 10 +++++++--- app/subscription/singbox.py | 7 +------ tests/test_subscription_singbox_tls.py | 22 ---------------------- 3 files changed, 8 insertions(+), 31 deletions(-) delete mode 100644 tests/test_subscription_singbox_tls.py diff --git a/app/subscription/clash.py b/app/subscription/clash.py index c515155be..f96c1ac9b 100644 --- a/app/subscription/clash.py +++ b/app/subscription/clash.py @@ -244,7 +244,7 @@ def _mihomo_download_settings_from_xray(self, download_settings: dict) -> dict: "alpn": tls_settings.get("alpn"), "skip-cert-verify": tls_settings.get("allowInsecure"), "servername": tls_settings.get("serverName"), - "client-fingerprint": tls_settings.get("fingerprint"), + "client-fingerprint": self._mihomo_client_fingerprint(tls_settings.get("fingerprint")), "reality-opts": { "public-key": tls_settings.get("publicKey"), "short-id": tls_settings.get("shortId") or "", @@ -292,6 +292,10 @@ def _select_address(address: list[str] | str) -> str: return address[0] return "" + @staticmethod + def _mihomo_client_fingerprint(fingerprint: str | None) -> str | None: + return "chrome" if fingerprint == "unsafe" else fingerprint + def _apply_mihomo_download_tls(self, node: dict, tls_config: TLSConfig): if not tls_config.tls: return @@ -306,7 +310,7 @@ def _apply_mihomo_download_tls(self, node: dict, tls_config: TLSConfig): node["skip-cert-verify"] = tls_config.allowinsecure if tls_config.fingerprint: - node["client-fingerprint"] = tls_config.fingerprint + node["client-fingerprint"] = self._mihomo_client_fingerprint(tls_config.fingerprint) if tls_config.tls == "reality" and tls_config.reality_public_key: # Do not map mldsa65Verify → support-x25519mlkem768; those are different PQ features. @@ -609,7 +613,7 @@ def _apply_tls(self, node: dict, tls_config: TLSConfig, protocol: str): # Add fingerprint if tls_config.fingerprint: - node["client-fingerprint"] = tls_config.fingerprint + node["client-fingerprint"] = self._mihomo_client_fingerprint(tls_config.fingerprint) # Add Reality opts if tls_config.tls == "reality" and tls_config.reality_public_key: diff --git a/app/subscription/singbox.py b/app/subscription/singbox.py index d57226d4f..499312253 100644 --- a/app/subscription/singbox.py +++ b/app/subscription/singbox.py @@ -215,12 +215,7 @@ def _apply_transport(self, network: str, inbound: SubscriptionInboundData, path: def _apply_tls(self, tls_config: TLSConfig, fragment_settings: dict | None = None) -> dict: """Apply TLS settings - receives TLS config and optional fragment settings""" - fingerprint = tls_config.fingerprint - if fingerprint == "unsafe": - # Xray uses "unsafe" to select the native TLS client, but sing-box - # rejects it as an unknown uTLS fingerprint. Reality requires uTLS, - # so keep it enabled there with sing-box's default fingerprint. - fingerprint = "chrome" if tls_config.tls == "reality" else "" + fingerprint = "chrome" if tls_config.fingerprint == "unsafe" else tls_config.fingerprint config = { "enabled": tls_config.tls in ("tls", "reality"), diff --git a/tests/test_subscription_singbox_tls.py b/tests/test_subscription_singbox_tls.py deleted file mode 100644 index 0c3825a51..000000000 --- a/tests/test_subscription_singbox_tls.py +++ /dev/null @@ -1,22 +0,0 @@ -from app.models.subscription import TLSConfig -from app.subscription.singbox import SingBoxConfiguration - - -def test_unsafe_fingerprint_uses_native_tls_in_singbox(): - tls = SingBoxConfiguration()._apply_tls(TLSConfig(tls="tls", sni="example.com", fingerprint="unsafe")) - - assert "utls" not in tls - - -def test_unsafe_fingerprint_falls_back_to_chrome_for_singbox_reality(): - tls = SingBoxConfiguration()._apply_tls( - TLSConfig( - tls="reality", - sni="example.com", - fingerprint="unsafe", - reality_public_key="public-key", - reality_short_id="12345678", - ) - ) - - assert tls["utls"] == {"enabled": True, "fingerprint": "chrome"}