From e1cda1be6627242cd240328cf861697c152064b6 Mon Sep 17 00:00:00 2001 From: akellavk Date: Tue, 7 Jul 2026 17:01:11 +0600 Subject: [PATCH 1/3] Implement group-level uTLS fingerprint selection that overrides profile config values MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changes made: 1. Database field addition (ProxyGroup.kt): - Added groupUtlsFingerprint: String field to ProxyGroup data class - Updated serialization/deserialization to persist the value 2. UI implementation (group_preferences.xml, GroupSettingsActivity.kt): - Added ListPreference for uTLS fingerprint selection in group settings - Uses existing utls_entry and utls_value arrays from arrays.xml - Added GROUP_UTLS_FINGERPRINT constant to Constants.kt - Added groupUtlsFingerprint variable to DataStore.kt - Updated init() and serialize() methods in GroupSettingsActivity to handle the new field - Named setting: "Force uTLS" (EN), "Принудительный uTLS" (RU) - Added summary display showing currently selected value 3. Application logic (ConfigBuilder.kt): - In buildChain() function, added group retrieval for each profile - Group uTLS is applied with priority: experimental override → group uTLS → profile uTLS - Updated all five locations where uTLS fingerprint is applied: - For StandardV2RayBean with TLS (2 locations) - For StandardV2RayBean with Reality (2 locations) - For TrustTunnelBean with HTTPS (1 location) Priority logic: When building configuration, the system checks in the following order: 1. Experimental override from settings (overrideUTLSFingerprintForREALITY/TLS) 2. Group uTLS fingerprint (groupUtlsFingerprint - if set in group settings) 3. Profile-specific fingerprint (bean.realityFingerprint or bean.utlsFingerprint) This allows centralized uTLS management for all profiles in a group, which is especially useful for circumventing blocks. --- .../36.json | 13 ++++++++++--- .../main/java/io/nekohasekai/sagernet/Constants.kt | 1 + .../io/nekohasekai/sagernet/database/DataStore.kt | 1 + .../io/nekohasekai/sagernet/database/ProxyGroup.kt | 11 ++++++++++- .../io/nekohasekai/sagernet/fmt/ConfigBuilder.kt | 14 ++++++++++++++ .../sagernet/ui/GroupSettingsActivity.kt | 2 ++ app/src/main/res/values-ru/strings.xml | 2 +- app/src/main/res/values/strings.xml | 2 +- app/src/main/res/xml/group_preferences.xml | 8 ++++++++ 9 files changed, 48 insertions(+), 6 deletions(-) diff --git a/app/schemas/io.nekohasekai.sagernet.database.SagerDatabase/36.json b/app/schemas/io.nekohasekai.sagernet.database.SagerDatabase/36.json index 61aec12a907..bb1277afaca 100644 --- a/app/schemas/io.nekohasekai.sagernet.database.SagerDatabase/36.json +++ b/app/schemas/io.nekohasekai.sagernet.database.SagerDatabase/36.json @@ -2,11 +2,11 @@ "formatVersion": 1, "database": { "version": 36, - "identityHash": "2ad66cce1d4ba5df1fd06814cfaae7e9", + "identityHash": "a767e2240b7af68735ec4f7b90813e3c", "entities": [ { "tableName": "proxy_groups", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userOrder` INTEGER NOT NULL, `ungrouped` INTEGER NOT NULL, `name` TEXT, `type` INTEGER NOT NULL, `subscription` BLOB, `order` INTEGER NOT NULL, `frontProxy` INTEGER NOT NULL DEFAULT -1, `landingProxy` INTEGER NOT NULL DEFAULT -1)", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userOrder` INTEGER NOT NULL, `ungrouped` INTEGER NOT NULL, `name` TEXT, `type` INTEGER NOT NULL, `subscription` BLOB, `order` INTEGER NOT NULL, `frontProxy` INTEGER NOT NULL DEFAULT -1, `landingProxy` INTEGER NOT NULL DEFAULT -1, `groupUtlsFingerprint` TEXT NOT NULL DEFAULT '')", "fields": [ { "fieldPath": "id", @@ -61,6 +61,13 @@ "affinity": "INTEGER", "notNull": true, "defaultValue": "-1" + }, + { + "fieldPath": "groupUtlsFingerprint", + "columnName": "groupUtlsFingerprint", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" } ], "primaryKey": { @@ -455,7 +462,7 @@ ], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '2ad66cce1d4ba5df1fd06814cfaae7e9')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'a767e2240b7af68735ec4f7b90813e3c')" ] } } \ No newline at end of file diff --git a/app/src/main/java/io/nekohasekai/sagernet/Constants.kt b/app/src/main/java/io/nekohasekai/sagernet/Constants.kt index a5228b63275..b6f856ef723 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/Constants.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/Constants.kt @@ -295,6 +295,7 @@ object Key { const val GROUP_LANDING_PROXY_OUTBOUND = "groupLandingOutbound" const val GROUP_FRONT_PROXY = "groupFrontProxy" const val GROUP_LANDING_PROXY = "groupLandingProxy" + const val GROUP_UTLS_FINGERPRINT = "groupUtlsFingerprint" const val GROUP_SUBSCRIPTION = "groupSubscription" const val SUBSCRIPTION_TYPE = "subscriptionType" diff --git a/app/src/main/java/io/nekohasekai/sagernet/database/DataStore.kt b/app/src/main/java/io/nekohasekai/sagernet/database/DataStore.kt index 50c3a5af3b0..d57a3228e3f 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/database/DataStore.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/database/DataStore.kt @@ -436,6 +436,7 @@ object DataStore : OnPreferenceDataStoreChangeListener { var landingProxyOutbound by profileCacheStore.long(Key.GROUP_LANDING_PROXY_OUTBOUND) var frontProxy by profileCacheStore.stringToInt(Key.GROUP_FRONT_PROXY) var landingProxy by profileCacheStore.stringToInt(Key.GROUP_LANDING_PROXY) + var groupUtlsFingerprint by profileCacheStore.string(Key.GROUP_UTLS_FINGERPRINT) var serverConfig by profileCacheStore.string(Key.SERVER_CONFIG) diff --git a/app/src/main/java/io/nekohasekai/sagernet/database/ProxyGroup.kt b/app/src/main/java/io/nekohasekai/sagernet/database/ProxyGroup.kt index 977450d40eb..7d31c7cdd12 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/database/ProxyGroup.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/database/ProxyGroup.kt @@ -39,7 +39,8 @@ data class ProxyGroup( var subscription: SubscriptionBean? = null, var order: Int = GroupOrder.ORIGIN, @ColumnInfo(defaultValue = (-1L).toString()) var frontProxy: Long = -1L, - @ColumnInfo(defaultValue = (-1L).toString()) var landingProxy: Long = -1L + @ColumnInfo(defaultValue = (-1L).toString()) var landingProxy: Long = -1L, + @ColumnInfo(defaultValue = "") var groupUtlsFingerprint: String = "" ) : Serializable() { @Transient @@ -69,6 +70,7 @@ data class ProxyGroup( output.writeInt(order) output.writeLong(frontProxy) output.writeLong(landingProxy) + output.writeString(groupUtlsFingerprint) } } @@ -97,6 +99,13 @@ data class ProxyGroup( frontProxy = input.readLong() landingProxy = input.readLong() } + if (version >= 1) { + try { + groupUtlsFingerprint = input.readString() + } catch (e: Exception) { + groupUtlsFingerprint = "" + } + } } } diff --git a/app/src/main/java/io/nekohasekai/sagernet/fmt/ConfigBuilder.kt b/app/src/main/java/io/nekohasekai/sagernet/fmt/ConfigBuilder.kt index 4b1a06edf4c..6a7a05f880b 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/fmt/ConfigBuilder.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/fmt/ConfigBuilder.kt @@ -602,6 +602,10 @@ fun buildV2RayConfig( val bean = proxyEntity.requireBean() currentOutbound = OutboundObject() + // Получаем группу для применения группового uTLS + val proxyGroup = SagerDatabase.groupDao.getById(proxyEntity.groupId) + val groupUtls = proxyGroup?.groupUtlsFingerprint?.takeIf { it.isNotEmpty() } + val tagIn: String var needGlobal: Boolean @@ -888,6 +892,8 @@ fun buildV2RayConfig( val overrideFingerprint = DataStore.experimentalFlagsProperties.getProperty("overrideUTLSFingerprintForTLS") if (!overrideFingerprint.isNullOrEmpty()) { fingerprint = overrideFingerprint + } else if (groupUtls != null) { + fingerprint = groupUtls } else if (bean.utlsFingerprint.isNotEmpty()) { fingerprint = bean.utlsFingerprint } @@ -918,6 +924,8 @@ fun buildV2RayConfig( val overrideFingerprint = DataStore.experimentalFlagsProperties.getProperty("overrideUTLSFingerprintForREALITY") if (!overrideFingerprint.isNullOrEmpty()) { fingerprint = overrideFingerprint + } else if (groupUtls != null) { + fingerprint = groupUtls } else if (bean.realityFingerprint.isNotEmpty()) { fingerprint = bean.realityFingerprint } @@ -1616,6 +1624,8 @@ fun buildV2RayConfig( val overrideFingerprint = DataStore.experimentalFlagsProperties.getProperty("overrideUTLSFingerprintForTLS") if (!overrideFingerprint.isNullOrEmpty()) { fingerprint = overrideFingerprint + } else if (groupUtls != null) { + fingerprint = groupUtls } else if (bean.utlsFingerprint.isNotEmpty()) { fingerprint = bean.utlsFingerprint } @@ -1643,6 +1653,8 @@ fun buildV2RayConfig( val overrideFingerprint = DataStore.experimentalFlagsProperties.getProperty("overrideUTLSFingerprintForREALITY") if (!overrideFingerprint.isNullOrEmpty()) { fingerprint = overrideFingerprint + } else if (groupUtls != null) { + fingerprint = groupUtls } else if (bean.realityFingerprint.isNotEmpty()) { fingerprint = bean.realityFingerprint } @@ -1833,6 +1845,8 @@ fun buildV2RayConfig( val overrideFingerprint = DataStore.experimentalFlagsProperties.getProperty("overrideUTLSFingerprintForTLS") if (!overrideFingerprint.isNullOrEmpty()) { fingerprint = overrideFingerprint + } else if (groupUtls != null) { + fingerprint = groupUtls } else if (bean.utlsFingerprint.isNotEmpty()) { fingerprint = bean.utlsFingerprint } diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/GroupSettingsActivity.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/GroupSettingsActivity.kt index 526d99614ba..bc39ba03bb6 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/GroupSettingsActivity.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/GroupSettingsActivity.kt @@ -102,6 +102,7 @@ class GroupSettingsActivity( DataStore.landingProxyOutbound = landingProxy DataStore.frontProxy = if (frontProxy >= 0) 1 else 0 DataStore.landingProxy = if (landingProxy >= 0) 1 else 0 + DataStore.groupUtlsFingerprint = groupUtlsFingerprint ?: "" } fun ProxyGroup.serialize() { @@ -116,6 +117,7 @@ class GroupSettingsActivity( frontProxy = if (DataStore.frontProxy == 1) DataStore.frontProxyOutbound else -1 landingProxy = if (DataStore.landingProxy == 1) DataStore.landingProxyOutbound else -1 + groupUtlsFingerprint = DataStore.groupUtlsFingerprint if (type == GroupType.SUBSCRIPTION) { subscription = SubscriptionBean().applyDefaultValues().apply { type = DataStore.subscriptionType diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 800a22f6944..08a9ea5e93c 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -441,7 +441,7 @@ URL test Изменить ID URL - uTLS-отпечаток + Принудительный uTLS Правило маршрутизации `%s` требует доступа к вашему **Wi-Fi SSID**, но для этого требуется **включенное местоположение** Правило маршрутизации `%s` требует доступа к вашему **Wi-Fi SSID**, но для этого требуется **разрешение на точное определение местоположения** Загрузочный DNS diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 499fe41b385..b25dac2ab81 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -334,7 +334,7 @@ Experimental settings Make the length of each payload segment no longer malleable. This experiment requires the server and client use the same version of v2ray-core. More breaking updates on this experiment is expected. Do not send connection single duplex termination signal for TCP connection when transferred over VMess. This will break some applications. - uTLS fingerprint + Force uTLS Sort By original order Alphabetically diff --git a/app/src/main/res/xml/group_preferences.xml b/app/src/main/res/xml/group_preferences.xml index eba545b17ac..9d62704e7e2 100644 --- a/app/src/main/res/xml/group_preferences.xml +++ b/app/src/main/res/xml/group_preferences.xml @@ -37,6 +37,14 @@ app:key="groupLandingProxy" app:title="@string/landing_proxy" /> + + From d11683cf6374ddfd46c6f7b782d741712d372041 Mon Sep 17 00:00:00 2001 From: dyhkwong <50692134+dyhkwong@users.noreply.github.com> Date: Sat, 11 Jul 2026 02:51:19 +0800 Subject: [PATCH 2/3] fixup - Restore 36.json and handle database version upgrade correctly. - Fix database value serialization and deserialization. - uTLS fingerprint and REALITY fingerprint are different values. They need to be two separate options. - Restore string resources. They are used in other locations. - Bump version code to prevent downgrade. --- .../36.json | 13 +- .../37.json | 475 ++++++++++++++++++ .../java/io/nekohasekai/sagernet/Constants.kt | 3 +- .../sagernet/database/DataStore.kt | 3 +- .../sagernet/database/ProxyGroup.kt | 17 +- .../sagernet/database/SagerDatabase.kt | 5 +- .../nekohasekai/sagernet/fmt/ConfigBuilder.kt | 24 +- .../sagernet/ui/GroupSettingsActivity.kt | 6 +- app/src/main/res/values-ru/strings.xml | 2 +- app/src/main/res/values/strings.xml | 2 +- app/src/main/res/xml/group_preferences.xml | 10 +- version.properties | 2 +- 12 files changed, 522 insertions(+), 40 deletions(-) create mode 100644 app/schemas/io.nekohasekai.sagernet.database.SagerDatabase/37.json diff --git a/app/schemas/io.nekohasekai.sagernet.database.SagerDatabase/36.json b/app/schemas/io.nekohasekai.sagernet.database.SagerDatabase/36.json index bb1277afaca..61aec12a907 100644 --- a/app/schemas/io.nekohasekai.sagernet.database.SagerDatabase/36.json +++ b/app/schemas/io.nekohasekai.sagernet.database.SagerDatabase/36.json @@ -2,11 +2,11 @@ "formatVersion": 1, "database": { "version": 36, - "identityHash": "a767e2240b7af68735ec4f7b90813e3c", + "identityHash": "2ad66cce1d4ba5df1fd06814cfaae7e9", "entities": [ { "tableName": "proxy_groups", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userOrder` INTEGER NOT NULL, `ungrouped` INTEGER NOT NULL, `name` TEXT, `type` INTEGER NOT NULL, `subscription` BLOB, `order` INTEGER NOT NULL, `frontProxy` INTEGER NOT NULL DEFAULT -1, `landingProxy` INTEGER NOT NULL DEFAULT -1, `groupUtlsFingerprint` TEXT NOT NULL DEFAULT '')", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userOrder` INTEGER NOT NULL, `ungrouped` INTEGER NOT NULL, `name` TEXT, `type` INTEGER NOT NULL, `subscription` BLOB, `order` INTEGER NOT NULL, `frontProxy` INTEGER NOT NULL DEFAULT -1, `landingProxy` INTEGER NOT NULL DEFAULT -1)", "fields": [ { "fieldPath": "id", @@ -61,13 +61,6 @@ "affinity": "INTEGER", "notNull": true, "defaultValue": "-1" - }, - { - "fieldPath": "groupUtlsFingerprint", - "columnName": "groupUtlsFingerprint", - "affinity": "TEXT", - "notNull": true, - "defaultValue": "''" } ], "primaryKey": { @@ -462,7 +455,7 @@ ], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'a767e2240b7af68735ec4f7b90813e3c')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '2ad66cce1d4ba5df1fd06814cfaae7e9')" ] } } \ No newline at end of file diff --git a/app/schemas/io.nekohasekai.sagernet.database.SagerDatabase/37.json b/app/schemas/io.nekohasekai.sagernet.database.SagerDatabase/37.json new file mode 100644 index 00000000000..0a08035367e --- /dev/null +++ b/app/schemas/io.nekohasekai.sagernet.database.SagerDatabase/37.json @@ -0,0 +1,475 @@ +{ + "formatVersion": 1, + "database": { + "version": 37, + "identityHash": "286ab111b45c46a16635823c2396e034", + "entities": [ + { + "tableName": "proxy_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userOrder` INTEGER NOT NULL, `ungrouped` INTEGER NOT NULL, `name` TEXT, `type` INTEGER NOT NULL, `subscription` BLOB, `order` INTEGER NOT NULL, `frontProxy` INTEGER NOT NULL DEFAULT -1, `landingProxy` INTEGER NOT NULL DEFAULT -1, `utlsFingerprintForTLS` TEXT NOT NULL DEFAULT '', `utlsFingerprintForReality` TEXT NOT NULL DEFAULT '')", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userOrder", + "columnName": "userOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ungrouped", + "columnName": "ungrouped", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT" + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subscription", + "columnName": "subscription", + "affinity": "BLOB" + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "frontProxy", + "columnName": "frontProxy", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "landingProxy", + "columnName": "landingProxy", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "utlsFingerprintForTLS", + "columnName": "utlsFingerprintForTLS", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "utlsFingerprintForReality", + "columnName": "utlsFingerprintForReality", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "proxy_entities", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `groupId` INTEGER NOT NULL, `type` INTEGER NOT NULL, `userOrder` INTEGER NOT NULL, `tx` INTEGER NOT NULL, `rx` INTEGER NOT NULL, `status` INTEGER NOT NULL, `ping` INTEGER NOT NULL, `uuid` TEXT NOT NULL, `error` TEXT, `socksBean` BLOB, `httpBean` BLOB, `ssBean` BLOB, `ssrBean` BLOB, `vmessBean` BLOB, `vlessBean` BLOB, `trojanBean` BLOB, `naiveBean` BLOB, `hysteria2Bean` BLOB, `mieruBean` BLOB, `tuic5Bean` BLOB, `sshBean` BLOB, `wgBean` BLOB, `juicityBean` BLOB, `http3Bean` BLOB, `anytlsBean` BLOB, `shadowquicBean` BLOB, `trustTunnelBean` BLOB, `configBean` BLOB, `chainBean` BLOB, `balancerBean` BLOB)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "groupId", + "columnName": "groupId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userOrder", + "columnName": "userOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "tx", + "columnName": "tx", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "rx", + "columnName": "rx", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ping", + "columnName": "ping", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "uuid", + "columnName": "uuid", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "error", + "columnName": "error", + "affinity": "TEXT" + }, + { + "fieldPath": "socksBean", + "columnName": "socksBean", + "affinity": "BLOB" + }, + { + "fieldPath": "httpBean", + "columnName": "httpBean", + "affinity": "BLOB" + }, + { + "fieldPath": "ssBean", + "columnName": "ssBean", + "affinity": "BLOB" + }, + { + "fieldPath": "ssrBean", + "columnName": "ssrBean", + "affinity": "BLOB" + }, + { + "fieldPath": "vmessBean", + "columnName": "vmessBean", + "affinity": "BLOB" + }, + { + "fieldPath": "vlessBean", + "columnName": "vlessBean", + "affinity": "BLOB" + }, + { + "fieldPath": "trojanBean", + "columnName": "trojanBean", + "affinity": "BLOB" + }, + { + "fieldPath": "naiveBean", + "columnName": "naiveBean", + "affinity": "BLOB" + }, + { + "fieldPath": "hysteria2Bean", + "columnName": "hysteria2Bean", + "affinity": "BLOB" + }, + { + "fieldPath": "mieruBean", + "columnName": "mieruBean", + "affinity": "BLOB" + }, + { + "fieldPath": "tuic5Bean", + "columnName": "tuic5Bean", + "affinity": "BLOB" + }, + { + "fieldPath": "sshBean", + "columnName": "sshBean", + "affinity": "BLOB" + }, + { + "fieldPath": "wgBean", + "columnName": "wgBean", + "affinity": "BLOB" + }, + { + "fieldPath": "juicityBean", + "columnName": "juicityBean", + "affinity": "BLOB" + }, + { + "fieldPath": "http3Bean", + "columnName": "http3Bean", + "affinity": "BLOB" + }, + { + "fieldPath": "anytlsBean", + "columnName": "anytlsBean", + "affinity": "BLOB" + }, + { + "fieldPath": "shadowquicBean", + "columnName": "shadowquicBean", + "affinity": "BLOB" + }, + { + "fieldPath": "trustTunnelBean", + "columnName": "trustTunnelBean", + "affinity": "BLOB" + }, + { + "fieldPath": "configBean", + "columnName": "configBean", + "affinity": "BLOB" + }, + { + "fieldPath": "chainBean", + "columnName": "chainBean", + "affinity": "BLOB" + }, + { + "fieldPath": "balancerBean", + "columnName": "balancerBean", + "affinity": "BLOB" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "groupId", + "unique": false, + "columnNames": [ + "groupId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `groupId` ON `${TABLE_NAME}` (`groupId`)" + } + ] + }, + { + "tableName": "rules", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `userOrder` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `domains` TEXT NOT NULL, `ip` TEXT NOT NULL, `port` TEXT NOT NULL, `sourcePort` TEXT NOT NULL, `network` TEXT NOT NULL, `source` TEXT NOT NULL, `protocol` TEXT NOT NULL, `attrs` TEXT NOT NULL, `outbound` INTEGER NOT NULL, `packages` TEXT NOT NULL, `ssid` TEXT NOT NULL DEFAULT '', `networkType` TEXT NOT NULL DEFAULT '', `customPackageNames` TEXT NOT NULL DEFAULT '')", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "userOrder", + "columnName": "userOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domains", + "columnName": "domains", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "ip", + "columnName": "ip", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "port", + "columnName": "port", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sourcePort", + "columnName": "sourcePort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "network", + "columnName": "network", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "source", + "columnName": "source", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "protocol", + "columnName": "protocol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "attrs", + "columnName": "attrs", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "outbound", + "columnName": "outbound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "packages", + "columnName": "packages", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "ssid", + "columnName": "ssid", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "networkType", + "columnName": "networkType", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "customPackageNames", + "columnName": "customPackageNames", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "trafficStats", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `uid` INTEGER NOT NULL, `tcpConnections` INTEGER NOT NULL, `udpConnections` INTEGER NOT NULL, `uplink` INTEGER NOT NULL, `downlink` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "tcpConnections", + "columnName": "tcpConnections", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "udpConnections", + "columnName": "udpConnections", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "uplink", + "columnName": "uplink", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "downlink", + "columnName": "downlink", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "assets", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `url` TEXT NOT NULL, `name` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '286ab111b45c46a16635823c2396e034')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/io/nekohasekai/sagernet/Constants.kt b/app/src/main/java/io/nekohasekai/sagernet/Constants.kt index b6f856ef723..cd59471f7ff 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/Constants.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/Constants.kt @@ -295,7 +295,8 @@ object Key { const val GROUP_LANDING_PROXY_OUTBOUND = "groupLandingOutbound" const val GROUP_FRONT_PROXY = "groupFrontProxy" const val GROUP_LANDING_PROXY = "groupLandingProxy" - const val GROUP_UTLS_FINGERPRINT = "groupUtlsFingerprint" + const val GROUP_UTLS_FINGERPRINT_FOR_TLS = "groupUTLSFingerprintForTLS" + const val GROUP_UTLS_FINGERPRINT_FOR_REALITY = "groupUTLSFingerprintForReality" const val GROUP_SUBSCRIPTION = "groupSubscription" const val SUBSCRIPTION_TYPE = "subscriptionType" diff --git a/app/src/main/java/io/nekohasekai/sagernet/database/DataStore.kt b/app/src/main/java/io/nekohasekai/sagernet/database/DataStore.kt index d57a3228e3f..90dc839c49d 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/database/DataStore.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/database/DataStore.kt @@ -436,7 +436,8 @@ object DataStore : OnPreferenceDataStoreChangeListener { var landingProxyOutbound by profileCacheStore.long(Key.GROUP_LANDING_PROXY_OUTBOUND) var frontProxy by profileCacheStore.stringToInt(Key.GROUP_FRONT_PROXY) var landingProxy by profileCacheStore.stringToInt(Key.GROUP_LANDING_PROXY) - var groupUtlsFingerprint by profileCacheStore.string(Key.GROUP_UTLS_FINGERPRINT) + var groupUTLSFingerprintForTLS by profileCacheStore.string(Key.GROUP_UTLS_FINGERPRINT_FOR_TLS) + var groupUTLSFingerprintForReality by profileCacheStore.string(Key.GROUP_UTLS_FINGERPRINT_FOR_REALITY) var serverConfig by profileCacheStore.string(Key.SERVER_CONFIG) diff --git a/app/src/main/java/io/nekohasekai/sagernet/database/ProxyGroup.kt b/app/src/main/java/io/nekohasekai/sagernet/database/ProxyGroup.kt index 7d31c7cdd12..7d3ab70aa42 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/database/ProxyGroup.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/database/ProxyGroup.kt @@ -40,7 +40,8 @@ data class ProxyGroup( var order: Int = GroupOrder.ORIGIN, @ColumnInfo(defaultValue = (-1L).toString()) var frontProxy: Long = -1L, @ColumnInfo(defaultValue = (-1L).toString()) var landingProxy: Long = -1L, - @ColumnInfo(defaultValue = "") var groupUtlsFingerprint: String = "" + @ColumnInfo(defaultValue = "") var utlsFingerprintForTLS: String = "", + @ColumnInfo(defaultValue = "") var utlsFingerprintForReality: String = "", ) : Serializable() { @Transient @@ -58,7 +59,7 @@ data class ProxyGroup( val subscription = subscription!! subscription.serializeForShare(output) } else { - output.writeInt(1) + output.writeInt(2) output.writeLong(id) output.writeLong(userOrder) output.writeBoolean(ungrouped) @@ -70,7 +71,8 @@ data class ProxyGroup( output.writeInt(order) output.writeLong(frontProxy) output.writeLong(landingProxy) - output.writeString(groupUtlsFingerprint) + output.writeString(utlsFingerprintForTLS) + output.writeString(utlsFingerprintForReality) } } @@ -99,12 +101,9 @@ data class ProxyGroup( frontProxy = input.readLong() landingProxy = input.readLong() } - if (version >= 1) { - try { - groupUtlsFingerprint = input.readString() - } catch (e: Exception) { - groupUtlsFingerprint = "" - } + if (version >= 2) { + utlsFingerprintForTLS = input.readString() + utlsFingerprintForReality = input.readString() } } } diff --git a/app/src/main/java/io/nekohasekai/sagernet/database/SagerDatabase.kt b/app/src/main/java/io/nekohasekai/sagernet/database/SagerDatabase.kt index a941276e17f..f7d1cc0f2e6 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/database/SagerDatabase.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/database/SagerDatabase.kt @@ -29,7 +29,7 @@ import kotlinx.coroutines.launch @Database( entities = [ProxyGroup::class, ProxyEntity::class, RuleEntity::class, StatsEntity::class, AssetEntity::class], - version = 36, + version = 37, autoMigrations = [AutoMigration( from = 12, to = 14, @@ -105,6 +105,9 @@ import kotlinx.coroutines.launch from = 35, to = 36, spec = SagerDatabase_Migration_35_36::class + ), AutoMigration( + from = 36, + to = 37, )] ) @TypeConverters(value = [KryoConverters::class, GsonConverters::class]) diff --git a/app/src/main/java/io/nekohasekai/sagernet/fmt/ConfigBuilder.kt b/app/src/main/java/io/nekohasekai/sagernet/fmt/ConfigBuilder.kt index 6a7a05f880b..bbde85c0348 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/fmt/ConfigBuilder.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/fmt/ConfigBuilder.kt @@ -602,9 +602,9 @@ fun buildV2RayConfig( val bean = proxyEntity.requireBean() currentOutbound = OutboundObject() - // Получаем группу для применения группового uTLS val proxyGroup = SagerDatabase.groupDao.getById(proxyEntity.groupId) - val groupUtls = proxyGroup?.groupUtlsFingerprint?.takeIf { it.isNotEmpty() } + val groupUTLSFingerprintForTLS = proxyGroup?.utlsFingerprintForTLS ?: "" + val groupUTLSFingerprintForReality = proxyGroup?.utlsFingerprintForReality ?: "" val tagIn: String var needGlobal: Boolean @@ -892,8 +892,8 @@ fun buildV2RayConfig( val overrideFingerprint = DataStore.experimentalFlagsProperties.getProperty("overrideUTLSFingerprintForTLS") if (!overrideFingerprint.isNullOrEmpty()) { fingerprint = overrideFingerprint - } else if (groupUtls != null) { - fingerprint = groupUtls + } else if (!groupUTLSFingerprintForTLS.isNotEmpty()) { + fingerprint = groupUTLSFingerprintForTLS } else if (bean.utlsFingerprint.isNotEmpty()) { fingerprint = bean.utlsFingerprint } @@ -924,8 +924,8 @@ fun buildV2RayConfig( val overrideFingerprint = DataStore.experimentalFlagsProperties.getProperty("overrideUTLSFingerprintForREALITY") if (!overrideFingerprint.isNullOrEmpty()) { fingerprint = overrideFingerprint - } else if (groupUtls != null) { - fingerprint = groupUtls + } else if (groupUTLSFingerprintForReality.isNotEmpty()) { + fingerprint = groupUTLSFingerprintForReality } else if (bean.realityFingerprint.isNotEmpty()) { fingerprint = bean.realityFingerprint } @@ -1624,8 +1624,8 @@ fun buildV2RayConfig( val overrideFingerprint = DataStore.experimentalFlagsProperties.getProperty("overrideUTLSFingerprintForTLS") if (!overrideFingerprint.isNullOrEmpty()) { fingerprint = overrideFingerprint - } else if (groupUtls != null) { - fingerprint = groupUtls + } else if (groupUTLSFingerprintForTLS.isNotEmpty()) { + fingerprint = groupUTLSFingerprintForTLS } else if (bean.utlsFingerprint.isNotEmpty()) { fingerprint = bean.utlsFingerprint } @@ -1653,8 +1653,8 @@ fun buildV2RayConfig( val overrideFingerprint = DataStore.experimentalFlagsProperties.getProperty("overrideUTLSFingerprintForREALITY") if (!overrideFingerprint.isNullOrEmpty()) { fingerprint = overrideFingerprint - } else if (groupUtls != null) { - fingerprint = groupUtls + } else if (groupUTLSFingerprintForReality.isNotEmpty()) { + fingerprint = groupUTLSFingerprintForReality } else if (bean.realityFingerprint.isNotEmpty()) { fingerprint = bean.realityFingerprint } @@ -1845,8 +1845,8 @@ fun buildV2RayConfig( val overrideFingerprint = DataStore.experimentalFlagsProperties.getProperty("overrideUTLSFingerprintForTLS") if (!overrideFingerprint.isNullOrEmpty()) { fingerprint = overrideFingerprint - } else if (groupUtls != null) { - fingerprint = groupUtls + } else if (groupUTLSFingerprintForTLS.isNotEmpty()) { + fingerprint = groupUTLSFingerprintForTLS } else if (bean.utlsFingerprint.isNotEmpty()) { fingerprint = bean.utlsFingerprint } diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/GroupSettingsActivity.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/GroupSettingsActivity.kt index bc39ba03bb6..468f57812f6 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/GroupSettingsActivity.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/GroupSettingsActivity.kt @@ -102,7 +102,8 @@ class GroupSettingsActivity( DataStore.landingProxyOutbound = landingProxy DataStore.frontProxy = if (frontProxy >= 0) 1 else 0 DataStore.landingProxy = if (landingProxy >= 0) 1 else 0 - DataStore.groupUtlsFingerprint = groupUtlsFingerprint ?: "" + DataStore.groupUTLSFingerprintForTLS = utlsFingerprintForTLS + DataStore.groupUTLSFingerprintForReality = utlsFingerprintForReality } fun ProxyGroup.serialize() { @@ -117,7 +118,8 @@ class GroupSettingsActivity( frontProxy = if (DataStore.frontProxy == 1) DataStore.frontProxyOutbound else -1 landingProxy = if (DataStore.landingProxy == 1) DataStore.landingProxyOutbound else -1 - groupUtlsFingerprint = DataStore.groupUtlsFingerprint + utlsFingerprintForTLS = DataStore.groupUTLSFingerprintForTLS + utlsFingerprintForReality = DataStore.groupUTLSFingerprintForReality if (type == GroupType.SUBSCRIPTION) { subscription = SubscriptionBean().applyDefaultValues().apply { type = DataStore.subscriptionType diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 08a9ea5e93c..800a22f6944 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -441,7 +441,7 @@ URL test Изменить ID URL - Принудительный uTLS + uTLS-отпечаток Правило маршрутизации `%s` требует доступа к вашему **Wi-Fi SSID**, но для этого требуется **включенное местоположение** Правило маршрутизации `%s` требует доступа к вашему **Wi-Fi SSID**, но для этого требуется **разрешение на точное определение местоположения** Загрузочный DNS diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b25dac2ab81..499fe41b385 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -334,7 +334,7 @@ Experimental settings Make the length of each payload segment no longer malleable. This experiment requires the server and client use the same version of v2ray-core. More breaking updates on this experiment is expected. Do not send connection single duplex termination signal for TCP connection when transferred over VMess. This will break some applications. - Force uTLS + uTLS fingerprint Sort By original order Alphabetically diff --git a/app/src/main/res/xml/group_preferences.xml b/app/src/main/res/xml/group_preferences.xml index 9d62704e7e2..2d9559715c7 100644 --- a/app/src/main/res/xml/group_preferences.xml +++ b/app/src/main/res/xml/group_preferences.xml @@ -41,10 +41,18 @@ app:icon="@drawable/ic_baseline_fingerprint_24" app:entries="@array/utls_entry" app:entryValues="@array/utls_value" - app:key="groupUtlsFingerprint" + app:key="groupUTLSFingerprintForTLS" app:title="@string/utls_fingerprint" app:useSimpleSummaryProvider="true" /> + + diff --git a/version.properties b/version.properties index 43342ab86b8..1905a5027bf 100644 --- a/version.properties +++ b/version.properties @@ -1,6 +1,6 @@ PACKAGE_NAME=com.github.dyhkwong.sagernet VERSION_NAME=0.17.46 -VERSION_CODE=364 +VERSION_CODE=365 NAIVE_VERSION_NAME=149.0.7827.114-1 NAIVE_VERSION=275 From 13ab72fc58808891a7952bef8f73a079747dc9c2 Mon Sep 17 00:00:00 2001 From: dyhkwong <50692134+dyhkwong@users.noreply.github.com> Date: Sat, 11 Jul 2026 03:22:21 +0800 Subject: [PATCH 3/3] fixup --- app/src/main/res/xml/group_preferences.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/xml/group_preferences.xml b/app/src/main/res/xml/group_preferences.xml index 2d9559715c7..c80b89032ac 100644 --- a/app/src/main/res/xml/group_preferences.xml +++ b/app/src/main/res/xml/group_preferences.xml @@ -37,7 +37,7 @@ app:key="groupLandingProxy" app:title="@string/landing_proxy" /> - -