diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9ffd2d425ce..b07ea31cd3a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -198,6 +198,8 @@ android:name="io.nekohasekai.sagernet.ui.profile.ShadowQUICSettingsActivity" /> + anytlsBean = KryoConverters.anytlsDeserialize(byteArray) TYPE_SHADOWQUIC -> shadowquicBean = KryoConverters.shadowquicDeserialize(byteArray) TYPE_TRUSTTUNNEL -> trustTunnelBean = KryoConverters.trusttunnelDeserialize(byteArray) + TYPE_SNELL -> snellBean = KryoConverters.snellDeserialize(byteArray) TYPE_CONFIG -> configBean = KryoConverters.configDeserialize(byteArray) TYPE_CHAIN -> chainBean = KryoConverters.chainDeserialize(byteArray) @@ -246,6 +252,7 @@ data class ProxyEntity( TYPE_ANYTLS -> "AnyTLS" TYPE_SHADOWQUIC -> "ShadowQUIC" TYPE_TRUSTTUNNEL -> "TrustTunnel" + TYPE_SNELL -> "Snell" TYPE_CHAIN -> chainName TYPE_CONFIG -> configName @@ -276,6 +283,7 @@ data class ProxyEntity( TYPE_ANYTLS -> anytlsBean TYPE_SHADOWQUIC -> shadowquicBean TYPE_TRUSTTUNNEL -> trustTunnelBean + TYPE_SNELL -> snellBean TYPE_CONFIG -> configBean TYPE_CHAIN -> chainBean @@ -317,6 +325,7 @@ data class ProxyEntity( is Http3Bean -> toUri() is AnyTLSBean -> toUri() is TrustTunnelBean -> toUri() + is SnellBean -> toUri() is ShadowQUICBean -> toUri() else -> null } @@ -382,6 +391,7 @@ data class ProxyEntity( anytlsBean = null shadowquicBean = null trustTunnelBean = null + snellBean = null configBean = null chainBean = null @@ -460,6 +470,10 @@ data class ProxyEntity( type = TYPE_TRUSTTUNNEL trustTunnelBean = bean } + is SnellBean -> { + type = TYPE_SNELL + snellBean = bean + } is ConfigBean -> { type = TYPE_CONFIG @@ -498,6 +512,7 @@ data class ProxyEntity( TYPE_ANYTLS -> AnyTLSSettingsActivity::class.java TYPE_SHADOWQUIC -> ShadowQUICSettingsActivity::class.java TYPE_TRUSTTUNNEL -> TrustTunnelSettingsActivity::class.java + TYPE_SNELL -> SnellSettingsActivity::class.java TYPE_CONFIG -> ConfigSettingsActivity::class.java TYPE_CHAIN -> ChainSettingsActivity::class.java 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..bfb36bba699 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, @@ -126,7 +126,8 @@ abstract class SagerDatabase : RoomDatabase() { SagerDatabase_Migration_8_9, SagerDatabase_Migration_9_10, SagerDatabase_Migration_10_11, - SagerDatabase_Migration_11_12 + SagerDatabase_Migration_11_12, + SagerDatabase_Migration_36_37 ) .fallbackToDestructiveMigrationOnDowngrade() .allowMainThreadQueries() 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..4e57be74d08 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/fmt/ConfigBuilder.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/fmt/ConfigBuilder.kt @@ -54,6 +54,7 @@ import io.nekohasekai.sagernet.fmt.socks.SOCKSBean import io.nekohasekai.sagernet.fmt.ssh.SSHBean import io.nekohasekai.sagernet.fmt.trojan.TrojanBean import io.nekohasekai.sagernet.fmt.trusttunnel.TrustTunnelBean +import io.nekohasekai.sagernet.fmt.snell.SnellBean import io.nekohasekai.sagernet.fmt.tuic5.Tuic5Bean import io.nekohasekai.sagernet.fmt.v2ray.StandardV2RayBean import io.nekohasekai.sagernet.fmt.v2ray.V2RayConfig @@ -1740,6 +1741,19 @@ fun buildV2RayConfig( } } } + } else if (bean is SnellBean) { + protocol = "snell" + settings = LazyOutboundConfigurationObject(this, V2RayConfig.SnellOutboundConfigurationObject().apply { + address = bean.serverAddress + port = bean.serverPort + psk = bean.psk + userPSK = bean.userPSK + obfsMode = bean.obfsMode + obfsHost = bean.obfsHost + version = bean.version + reuse = bean.reuse + mode = bean.mode + }) } else if (bean is MieruBean) { protocol = "mieru" settings = LazyOutboundConfigurationObject(this, diff --git a/app/src/main/java/io/nekohasekai/sagernet/fmt/KryoConverters.java b/app/src/main/java/io/nekohasekai/sagernet/fmt/KryoConverters.java index 5d99f76628b..60052ce3df4 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/fmt/KryoConverters.java +++ b/app/src/main/java/io/nekohasekai/sagernet/fmt/KryoConverters.java @@ -46,6 +46,7 @@ import io.nekohasekai.sagernet.fmt.ssh.SSHBean; import io.nekohasekai.sagernet.fmt.trojan.TrojanBean; import io.nekohasekai.sagernet.fmt.trusttunnel.TrustTunnelBean; +import io.nekohasekai.sagernet.fmt.snell.SnellBean; import io.nekohasekai.sagernet.fmt.tuic5.Tuic5Bean; import io.nekohasekai.sagernet.fmt.v2ray.VLESSBean; import io.nekohasekai.sagernet.fmt.v2ray.VMessBean; @@ -189,6 +190,12 @@ public static TrustTunnelBean trusttunnelDeserialize(byte[] bytes) { return deserialize(new TrustTunnelBean(), bytes); } + @TypeConverter + public static SnellBean snellDeserialize(byte[] bytes) { + if (bytes == null || bytes.length == 0) return null; + return deserialize(new SnellBean(), bytes); + } + @TypeConverter public static ConfigBean configDeserialize(byte[] bytes) { if (bytes == null || bytes.length == 0) return null; diff --git a/app/src/main/java/io/nekohasekai/sagernet/fmt/TypeMap.kt b/app/src/main/java/io/nekohasekai/sagernet/fmt/TypeMap.kt index 9f7498c7dd8..ce2be09f484 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/fmt/TypeMap.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/fmt/TypeMap.kt @@ -42,6 +42,7 @@ object TypeMap : HashMap() { this["anytls"] = ProxyEntity.TYPE_ANYTLS this["shadowquic"] = ProxyEntity.TYPE_SHADOWQUIC this["trusttunnel"] = ProxyEntity.TYPE_TRUSTTUNNEL + this["snell"] = ProxyEntity.TYPE_SNELL } val reversed = HashMap() diff --git a/app/src/main/java/io/nekohasekai/sagernet/fmt/snell/SnellBean.java b/app/src/main/java/io/nekohasekai/sagernet/fmt/snell/SnellBean.java new file mode 100644 index 00000000000..b1966da5f76 --- /dev/null +++ b/app/src/main/java/io/nekohasekai/sagernet/fmt/snell/SnellBean.java @@ -0,0 +1,124 @@ +/****************************************************************************** + * * + * Copyright (C) 2026 Snell support for Exclave * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + ******************************************************************************/ + +package io.nekohasekai.sagernet.fmt.snell; + +import androidx.annotation.NonNull; + +import com.esotericsoftware.kryo.io.ByteBufferInput; +import com.esotericsoftware.kryo.io.ByteBufferOutput; + +import org.jetbrains.annotations.NotNull; + +import io.nekohasekai.sagernet.fmt.AbstractBean; +import io.nekohasekai.sagernet.fmt.KryoConverters; + +public class SnellBean extends AbstractBean { + + public static final int VERSION_4 = 4; + public static final int VERSION_6 = 6; + + public static final String OBFS_NONE = "none"; + public static final String OBFS_HTTP = "http"; + public static final String OBFS_TLS = "tls"; + + public static final String MODE_DEFAULT = "default"; + public static final String MODE_UNSHAPED = "unshaped"; + public static final String MODE_UNSAFE_RAW = "unsafe-raw"; + + public String psk; + /** sing-box private extension; only compatible with sing-box Snell server. */ + public String userPSK; + public String obfsMode; + /** v4-only */ + public String obfsHost; + public Integer version; + public Boolean reuse; + /** v6-only */ + public String mode; + + @Override + public void initializeDefaultValues() { + super.initializeDefaultValues(); + if (psk == null) psk = ""; + if (userPSK == null) userPSK = ""; + if (obfsMode == null || obfsMode.isEmpty()) obfsMode = OBFS_NONE; + if (obfsHost == null) obfsHost = ""; + if (version == null || version == 0) version = VERSION_4; + if (reuse == null) reuse = true; + if (mode == null || mode.isEmpty()) mode = MODE_DEFAULT; + } + + @Override + public void serialize(ByteBufferOutput output) { + // v3: rename obfs->obfsMode, add userPSK; version 4/6 only + output.writeInt(3); + super.serialize(output); + output.writeString(psk); + output.writeString(obfsMode); + output.writeInt(version); + output.writeBoolean(reuse); + output.writeString(obfsHost); + output.writeString(mode); + output.writeString(userPSK); + } + + @Override + public void deserialize(ByteBufferInput input) { + int ver = input.readInt(); + super.deserialize(input); + psk = input.readString(); + obfsMode = input.readString(); + // migrate legacy "off" -> "none" for old profiles only + if ("off".equals(obfsMode)) { + obfsMode = OBFS_NONE; + } + version = input.readInt(); + // migrate legacy 3/5 to 4 + if (version != null && version != VERSION_4 && version != VERSION_6) { + version = VERSION_4; + } + if (ver >= 1) { + reuse = input.readBoolean(); + } + if (ver >= 2) { + obfsHost = input.readString(); + mode = input.readString(); + } + if (ver >= 3) { + userPSK = input.readString(); + } + } + + @Override + public String network() { + return "tcp,udp"; + } + + @NotNull + @Override + public SnellBean clone() { + return KryoConverters.deserialize(new SnellBean(), KryoConverters.serialize(this)); + } + + public static final Creator CREATOR = new CREATOR<>() { + @NonNull + @Override + public SnellBean newInstance() { + return new SnellBean(); + } + + @Override + public SnellBean[] newArray(int size) { + return new SnellBean[size]; + } + }; +} diff --git a/app/src/main/java/io/nekohasekai/sagernet/fmt/snell/SnellFmt.kt b/app/src/main/java/io/nekohasekai/sagernet/fmt/snell/SnellFmt.kt new file mode 100644 index 00000000000..4ad6568a578 --- /dev/null +++ b/app/src/main/java/io/nekohasekai/sagernet/fmt/snell/SnellFmt.kt @@ -0,0 +1,64 @@ +package io.nekohasekai.sagernet.fmt.snell + +import io.nekohasekai.sagernet.ktx.* +import libexclavecore.Libexclavecore + +fun parseSnell(url: String): SnellBean { + val link = Libexclavecore.parseURL(url) + return SnellBean().apply { + name = link.fragment + serverAddress = link.host.ifEmpty { error("empty host") } + serverPort = link.port + psk = when { + link.username.isNotEmpty() -> link.username + link.password.isNotEmpty() -> link.password + else -> link.queryParameter("psk") ?: "" + } + link.queryParameter("version")?.toIntOrNull()?.also { + if (it != 4 && it != 6) error("snell version must be 4 or 6") + version = it + } + // accept only exact values; map legacy "off" only from share links that used it historically + (link.queryParameter("obfsMode") ?: link.queryParameter("obfs"))?.also { + obfsMode = if (it == "off") SnellBean.OBFS_NONE else it + } + link.queryParameter("obfs-host")?.also { obfsHost = it } + link.queryParameter("mode")?.also { mode = it } + link.queryParameter("user-psk")?.also { userPSK = it } + link.queryParameter("userPSK")?.also { userPSK = it } + link.queryParameter("reuse")?.also { + reuse = it == "1" || it.equals("true", ignoreCase = true) + } + initializeDefaultValues() + } +} + +fun SnellBean.toUri(): String? { + val builder = Libexclavecore.newURL("snell").apply { + setHostPort(serverAddress.ifEmpty { error("empty server address") }, serverPort) + if (!psk.isNullOrEmpty()) { + username = psk + } + if (name.isNotEmpty()) { + fragment = name + } + } + val v = version ?: 4 + builder.addQueryParameter("version", v.toString()) + if (!obfsMode.isNullOrEmpty() && obfsMode != SnellBean.OBFS_NONE) { + builder.addQueryParameter("obfsMode", obfsMode) + } + if (!obfsHost.isNullOrEmpty()) { + builder.addQueryParameter("obfs-host", obfsHost) + } + if (v >= 6 && !mode.isNullOrEmpty() && mode != SnellBean.MODE_DEFAULT) { + builder.addQueryParameter("mode", mode) + } + if (!userPSK.isNullOrEmpty()) { + builder.addQueryParameter("user-psk", userPSK) + } + if (reuse == true) { + builder.addQueryParameter("reuse", "1") + } + return builder.string +} diff --git a/app/src/main/java/io/nekohasekai/sagernet/fmt/v2ray/V2RayConfig.java b/app/src/main/java/io/nekohasekai/sagernet/fmt/v2ray/V2RayConfig.java index 0ecbbd9906a..f618744168f 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/fmt/v2ray/V2RayConfig.java +++ b/app/src/main/java/io/nekohasekai/sagernet/fmt/v2ray/V2RayConfig.java @@ -696,6 +696,8 @@ protected Class getType() { return MieruOutboundConfigurationObject.class; case "trusttunnel": return TrustTunnelOutboundConfigurationObject.class; + case "snell": + return SnellOutboundConfigurationObject.class; } return null; } @@ -985,7 +987,22 @@ public static class JuicityOutboundConfigurationObject implements OutboundConfig } - public static class MieruOutboundConfigurationObject implements OutboundConfigurationObject { + + public static class SnellOutboundConfigurationObject implements OutboundConfigurationObject { + + public String address; + public Integer port; + public String psk; + public String userPSK; + public String obfsMode; + public String obfsHost; + public Integer version; + public Boolean reuse; + public String mode; + + } + +public static class MieruOutboundConfigurationObject implements OutboundConfigurationObject { public String address; public Integer port; diff --git a/app/src/main/java/io/nekohasekai/sagernet/group/ClashYAMLParser.kt b/app/src/main/java/io/nekohasekai/sagernet/group/ClashYAMLParser.kt index bcc79e8fe94..871c83fe755 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/group/ClashYAMLParser.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/group/ClashYAMLParser.kt @@ -38,6 +38,7 @@ import io.nekohasekai.sagernet.fmt.socks.SOCKSBean import io.nekohasekai.sagernet.fmt.ssh.SSHBean import io.nekohasekai.sagernet.fmt.trojan.TrojanBean import io.nekohasekai.sagernet.fmt.trusttunnel.TrustTunnelBean +import io.nekohasekai.sagernet.fmt.snell.SnellBean import io.nekohasekai.sagernet.fmt.tuic5.Tuic5Bean import io.nekohasekai.sagernet.fmt.tuic5.supportedTuic5CongestionControl import io.nekohasekai.sagernet.fmt.tuic5.supportedTuic5RelayMode @@ -771,6 +772,29 @@ fun parseClashProxy(proxy: Map): List { }*/ name = proxy.getString("name") }) + } + "snell" -> { + return listOf(SnellBean().apply { + serverAddress = proxy.getString("server") ?: return listOf() + serverPort = proxy.getInt("port")?.takeIf { it > 0 } ?: return listOf() + psk = proxy.getString("psk") ?: "" + proxy.getString("user-psk")?.also { userPSK = it } + proxy.getInt("version")?.also { + if (it != 4 && it != 6) return listOf() + version = it + } + proxy.getString("obfs")?.also { + obfsMode = if (it == "off") SnellBean.OBFS_NONE else it + } + proxy.getObject("obfs-opts")?.also { opts -> + opts.getString("mode")?.also { + obfsMode = if (it == "off") SnellBean.OBFS_NONE else it + } + opts.getString("host")?.also { obfsHost = it } + } + proxy.getString("mode")?.also { mode = it } + name = proxy.getString("name") + }) } "anytls" -> { return listOf(AnyTLSBean().apply { diff --git a/app/src/main/java/io/nekohasekai/sagernet/ktx/Formats.kt b/app/src/main/java/io/nekohasekai/sagernet/ktx/Formats.kt index 3b822747c73..9e45aa5a772 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ktx/Formats.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ktx/Formats.kt @@ -36,6 +36,7 @@ import io.nekohasekai.sagernet.fmt.shadowsocksr.parseShadowsocksR import io.nekohasekai.sagernet.fmt.socks.parseSOCKS import io.nekohasekai.sagernet.fmt.ssh.parseSSH import io.nekohasekai.sagernet.fmt.trusttunnel.parseTrustTunnel +import io.nekohasekai.sagernet.fmt.snell.parseSnell import io.nekohasekai.sagernet.fmt.tuic5.parseTuic import io.nekohasekai.sagernet.fmt.v2ray.parseV2Ray import io.nekohasekai.sagernet.fmt.wireguard.parseWireGuard @@ -125,6 +126,10 @@ fun parseShareLinks(text: String): List { runCatching { entities.add(parseAnyTLS(this)) } + } else if (startsWith("snell://", ignoreCase = true)) { + runCatching { + entities.add(parseSnell(this)) + } } else if (startsWith("ssh://", ignoreCase = true)) { runCatching { entities.add(parseSSH(this)) diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/ConfigurationFragment.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/ConfigurationFragment.kt index a524a223a70..c812e240c81 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/ConfigurationFragment.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/ConfigurationFragment.kt @@ -520,6 +520,9 @@ class ConfigurationFragment @JvmOverloads constructor( R.id.action_new_trusttunnel -> { startActivity(Intent(requireActivity(), TrustTunnelSettingsActivity::class.java)) } + R.id.action_new_snell -> { + startActivity(Intent(requireActivity(), SnellSettingsActivity::class.java)) + } R.id.action_new_config -> { startActivity(Intent(requireActivity(), ConfigSettingsActivity::class.java)) } diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/profile/SnellSettingsActivity.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/profile/SnellSettingsActivity.kt new file mode 100644 index 00000000000..ce8b997b75d --- /dev/null +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/profile/SnellSettingsActivity.kt @@ -0,0 +1,76 @@ +package io.nekohasekai.sagernet.ui.profile + +import android.os.Bundle +import androidx.preference.EditTextPreference +import androidx.preference.PreferenceFragmentCompat +import io.nekohasekai.sagernet.Key +import io.nekohasekai.sagernet.R +import io.nekohasekai.sagernet.database.DataStore +import io.nekohasekai.sagernet.database.preference.EditTextPreferenceModifiers +import io.nekohasekai.sagernet.fmt.snell.SnellBean +import io.nekohasekai.sagernet.ktx.unwrapIDN +import io.nekohasekai.sagernet.widget.SimpleMenuPreference + +class SnellSettingsActivity : ProfileSettingsActivity() { + + override fun createEntity() = SnellBean() + + override fun SnellBean.init() { + DataStore.profileName = name + DataStore.serverAddress = serverAddress + DataStore.serverPort = serverPort + DataStore.serverPassword = psk + DataStore.serverPassword1 = userPSK + DataStore.serverObfs = obfsMode + DataStore.serverHost = obfsHost + DataStore.serverProtocolVersion = version ?: SnellBean.VERSION_4 + DataStore.serverMux = reuse == true + DataStore.serverProtocolParam = mode + } + + override fun SnellBean.serialize() { + name = DataStore.profileName + serverAddress = DataStore.serverAddress.unwrapIDN() + serverPort = DataStore.serverPort + psk = DataStore.serverPassword + userPSK = DataStore.serverPassword1 + obfsMode = DataStore.serverObfs?.ifEmpty { SnellBean.OBFS_NONE } ?: SnellBean.OBFS_NONE + obfsHost = DataStore.serverHost + version = DataStore.serverProtocolVersion.takeIf { it == 4 || it == 6 } ?: SnellBean.VERSION_4 + reuse = DataStore.serverMux + mode = DataStore.serverProtocolParam?.ifEmpty { SnellBean.MODE_DEFAULT } ?: SnellBean.MODE_DEFAULT + } + + override fun PreferenceFragmentCompat.createPreferences( + savedInstanceState: Bundle?, + rootKey: String?, + ) { + addPreferencesFromResource(R.xml.snell_preferences) + findPreference(Key.SERVER_PORT)!!.apply { + setOnBindEditTextListener(EditTextPreferenceModifiers.Port) + } + findPreference(Key.SERVER_PASSWORD)!!.apply { + summaryProvider = PasswordSummaryProvider + } + findPreference(Key.SERVER_PASSWORD1)!!.apply { + summaryProvider = PasswordSummaryProvider + } + val versionPref = findPreference(Key.SERVER_PROTOCOL)!! + val modePref = findPreference(Key.SERVER_PROTOCOL_PARAM)!! + val obfsPref = findPreference(Key.SERVER_OBFS)!! + val obfsHostPref = findPreference(Key.SERVER_HOST)!! + fun updateVisibility(v: Int) { + val isV6 = v >= 6 + modePref.isVisible = isV6 + // v4-only: obfs mode/host + obfsPref.isVisible = !isV6 + obfsHostPref.isVisible = !isV6 + } + val cur = versionPref.value?.toIntOrNull() ?: DataStore.serverProtocolVersion + updateVisibility(cur) + versionPref.setOnPreferenceChangeListener { _, newValue -> + updateVisibility((newValue as? String)?.toIntOrNull() ?: 4) + true + } + } +} diff --git a/app/src/main/res/menu/add_profile_menu.xml b/app/src/main/res/menu/add_profile_menu.xml index 195413bdde9..fe3a91319f4 100644 --- a/app/src/main/res/menu/add_profile_menu.xml +++ b/app/src/main/res/menu/add_profile_menu.xml @@ -55,6 +55,9 @@ + diff --git a/app/src/main/res/values/snell_arrays.xml b/app/src/main/res/values/snell_arrays.xml new file mode 100644 index 00000000000..6c3818733f4 --- /dev/null +++ b/app/src/main/res/values/snell_arrays.xml @@ -0,0 +1,31 @@ + + + + 4 + 6 (beta) + + + 4 + 6 + + + none + http + tls + + + none + http + tls + + + default + unshaped + unsafe-raw + + + default + unshaped + unsafe-raw + + diff --git a/app/src/main/res/values/snell_strings.xml b/app/src/main/res/values/snell_strings.xml new file mode 100644 index 00000000000..f709422d2ca --- /dev/null +++ b/app/src/main/res/values/snell_strings.xml @@ -0,0 +1,11 @@ + + + Snell version + Connection reuse + Reuse the TCP connection + Snell + Obfs mode + Obfs host (v4) + Snell v6 mode + User PSK (sing-box extension) + diff --git a/app/src/main/res/xml/snell_preferences.xml b/app/src/main/res/xml/snell_preferences.xml new file mode 100644 index 00000000000..648babdc047 --- /dev/null +++ b/app/src/main/res/xml/snell_preferences.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + +