diff --git a/app/src/main/java/io/nekohasekai/sagernet/Constants.kt b/app/src/main/java/io/nekohasekai/sagernet/Constants.kt index 32dca19..351e401 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/Constants.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/Constants.kt @@ -78,14 +78,11 @@ object Key { const val SERVER_USERNAME = "serverUsername" const val SERVER_PASSWORD = "serverPassword" const val SERVER_METHOD = "serverMethod" - const val SERVER_PLUGIN = "serverPlugin" - const val SERVER_PLUGIN_CONFIGURE = "serverPluginConfigure" const val SERVER_PASSWORD1 = "serverPassword1" const val SERVER_PROTOCOL = "serverProtocol" const val SERVER_OBFS = "serverObfs" - const val SERVER_SECURITY = "serverSecurity" const val SERVER_NETWORK = "serverNetwork" const val SERVER_HOST = "serverHost" const val SERVER_PATH = "serverPath" diff --git a/app/src/main/java/io/nekohasekai/sagernet/bg/proto/BoxInstance.kt b/app/src/main/java/io/nekohasekai/sagernet/bg/proto/BoxInstance.kt index 74cd658..d1a5e99 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/bg/proto/BoxInstance.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/bg/proto/BoxInstance.kt @@ -17,6 +17,7 @@ import io.nekohasekai.sagernet.fmt.trojan_go.TrojanGoBean import io.nekohasekai.sagernet.fmt.trojan_go.buildTrojanGoConfig import io.nekohasekai.sagernet.fmt.tuic.TuicBean import io.nekohasekai.sagernet.fmt.tuic.buildTuicConfig +import io.nekohasekai.sagernet.fmt.tuic.pluginId import io.nekohasekai.sagernet.ktx.* import io.nekohasekai.sagernet.plugin.PluginManager import kotlinx.coroutines.* @@ -86,7 +87,7 @@ abstract class BoxInstance( } is TuicBean -> { - initPlugin("tuic-plugin") + initPlugin(bean.pluginId()) pluginConfigs[port] = profile.type to bean.buildTuicConfig(port) { File( app.noBackupFilesDir, @@ -252,7 +253,7 @@ abstract class BoxInstance( cacheFiles.add(configFile) val commands = mutableListOf( - initPlugin("tuic-plugin").path, + initPlugin(bean.pluginId()).path, "-c", configFile.absolutePath, ) 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 d4a6127..307a16b 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/fmt/ConfigBuilder.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/fmt/ConfigBuilder.kt @@ -19,6 +19,7 @@ import io.nekohasekai.sagernet.fmt.socks.buildSingBoxOutboundSocksBean import io.nekohasekai.sagernet.fmt.ssh.SSHBean import io.nekohasekai.sagernet.fmt.ssh.buildSingBoxOutboundSSHBean import io.nekohasekai.sagernet.fmt.tuic.TuicBean +import io.nekohasekai.sagernet.fmt.tuic.pluginId import io.nekohasekai.sagernet.fmt.v2ray.StandardV2RayBean import io.nekohasekai.sagernet.fmt.v2ray.buildSingBoxOutboundStandardV2RayBean import io.nekohasekai.sagernet.fmt.wireguard.WireGuardBean @@ -35,6 +36,7 @@ import moe.matsuri.nb4a.proxy.config.ConfigBean import moe.matsuri.nb4a.proxy.shadowtls.ShadowTLSBean import moe.matsuri.nb4a.proxy.shadowtls.buildSingBoxOutboundShadowTLSBean import moe.matsuri.nb4a.utils.JavaUtil.gson +import moe.matsuri.nb4a.utils.Util import okhttp3.HttpUrl.Companion.toHttpUrlOrNull const val TAG_MIXED = "mixed-in" @@ -63,20 +65,6 @@ class ConfigBuildResult( data class IndexEntity(var chain: LinkedHashMap) } -fun mergeJSON(j: String, to: MutableMap) { - if (j.isBlank()) return - val m = gson.fromJson(j, to.javaClass) - m.forEach { (k, v) -> - if (v is Map<*, *> && to[k] is Map<*, *>) { - val currentMap = (to[k] as Map<*, *>).toMutableMap() - currentMap += v - to[k] = currentMap - } else { - to[k] = v - } - } -} - fun buildConfig( proxy: ProxyEntity, forTest: Boolean = false, forExport: Boolean = false ): ConfigBuildResult { @@ -439,7 +427,7 @@ fun buildConfig( // custom JSON merge if (bean.customOutboundJson.isNotBlank()) { - mergeJSON(bean.customOutboundJson, currentOutbound) + Util.mergeJSON(bean.customOutboundJson, currentOutbound) } } @@ -458,18 +446,19 @@ fun buildConfig( // External proxy need a dokodemo-door inbound to forward the traffic // For external proxy software, their traffic must goes to v2ray-core to use protected fd. + bean.finalPort = bean.serverPort if (bean.canMapping() && proxyEntity.needExternal()) { // With ss protect, don't use mapping var needExternal = true if (index == profileList.lastIndex) { val pluginId = when (bean) { is HysteriaBean -> "hysteria-plugin" - is TuicBean -> "tuic-plugin" + is TuicBean -> bean.pluginId() else -> "" } if (Plugins.isUsingMatsuriExe(pluginId)) { needExternal = false - } else if (pluginId.isNotBlank()) { + } else if (Plugins.getPlugin(pluginId) != null) { throw Exception("You are using an unsupported $pluginId, please download the correct plugin.") } } @@ -593,19 +582,25 @@ fun buildConfig( if (uidList.isNotEmpty()) user_id = uidList domainList?.let { makeSingBoxRule(it) } } - if (rule.outbound == -1L) { - userDNSRuleList += dnsRuleObj.apply { server = "dns-direct" } - } else if (rule.outbound == 0L) { - if (useFakeDns) userDNSRuleList += dnsRuleObj.apply { - server = "dns-fake" - inbound = listOf("tun-in") + when (rule.outbound) { + -1L -> { + userDNSRuleList += dnsRuleObj.apply { server = "dns-direct" } } - userDNSRuleList += dnsRuleObj.apply { - server = "dns-remote" - inbound = null + + 0L -> { + if (useFakeDns) userDNSRuleList += dnsRuleObj.apply { + server = "dns-fake" + inbound = listOf("tun-in") + } + userDNSRuleList += dnsRuleObj.apply { + server = "dns-remote" + inbound = null + } + } + + -2L -> { + userDNSRuleList += dnsRuleObj.apply { server = "dns-block" } } - } else if (rule.outbound == -2L) { - userDNSRuleList += dnsRuleObj.apply { server = "dns-block" } } outbound = when (val outId = rule.outbound) { @@ -786,7 +781,7 @@ fun buildConfig( }.let { ConfigBuildResult( gson.toJson(it.asMap().apply { - mergeJSON(optionsToMerge, this) + Util.mergeJSON(optionsToMerge, this) }), externalIndexMap, proxy.id, diff --git a/app/src/main/java/io/nekohasekai/sagernet/fmt/PluginEntry.kt b/app/src/main/java/io/nekohasekai/sagernet/fmt/PluginEntry.kt index 6cd7658..200343b 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/fmt/PluginEntry.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/fmt/PluginEntry.kt @@ -22,7 +22,8 @@ enum class PluginEntry( Hysteria( "hysteria-plugin", SagerNet.application.getString(R.string.action_hysteria), - "moe.matsuri.exe.hysteria", DownloadSource( + "moe.matsuri.exe.hysteria", + DownloadSource( playStore = false, fdroid = false, downloadLink = "https://github.com/MatsuriDayo/plugins/releases?q=Hysteria" @@ -30,8 +31,19 @@ enum class PluginEntry( ), TUIC( "tuic-plugin", - SagerNet.application.getString(R.string.action_tuic), - "moe.matsuri.exe.tuic", DownloadSource( + "TUIC(v4)", + "moe.matsuri.exe.tuic", + DownloadSource( + playStore = false, + fdroid = false, + downloadLink = "https://github.com/MatsuriDayo/plugins/releases?q=tuic" + ) + ), + TUIC5( + "tuic-v5-plugin", + "TUIC(v5)", + "moe.matsuri.exe.tuic5", + DownloadSource( playStore = false, fdroid = false, downloadLink = "https://github.com/MatsuriDayo/plugins/releases?q=tuic" diff --git a/app/src/main/java/io/nekohasekai/sagernet/fmt/tuic/TuicBean.java b/app/src/main/java/io/nekohasekai/sagernet/fmt/tuic/TuicBean.java index 4ce148f..51322b8 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/fmt/tuic/TuicBean.java +++ b/app/src/main/java/io/nekohasekai/sagernet/fmt/tuic/TuicBean.java @@ -24,6 +24,12 @@ public class TuicBean extends AbstractBean { public Boolean fastConnect; public Boolean allowInsecure; + // TUIC v5 + + public String customJSON; + public Integer protocolVersion; + public String uuid; + @Override public void initializeDefaultValues() { super.initializeDefaultValues(); @@ -38,11 +44,14 @@ public class TuicBean extends AbstractBean { if (sni == null) sni = ""; if (fastConnect == null) fastConnect = false; if (allowInsecure == null) allowInsecure = false; + if (customJSON == null) customJSON = ""; + if (protocolVersion == null) protocolVersion = 4; + if (uuid == null) uuid = ""; } @Override public void serialize(ByteBufferOutput output) { - output.writeInt(1); + output.writeInt(2); super.serialize(output); output.writeString(token); output.writeString(caText); @@ -55,6 +64,9 @@ public class TuicBean extends AbstractBean { output.writeString(sni); output.writeBoolean(fastConnect); output.writeBoolean(allowInsecure); + output.writeString(customJSON); + output.writeInt(protocolVersion); + output.writeString(uuid); } @Override @@ -74,6 +86,11 @@ public class TuicBean extends AbstractBean { fastConnect = input.readBoolean(); allowInsecure = input.readBoolean(); } + if (version >= 2) { + customJSON = input.readString(); + protocolVersion = input.readInt(); + uuid = input.readString(); + } } @Override diff --git a/app/src/main/java/io/nekohasekai/sagernet/fmt/tuic/TuicFmt.kt b/app/src/main/java/io/nekohasekai/sagernet/fmt/tuic/TuicFmt.kt index ce31363..65ba3be 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/fmt/tuic/TuicFmt.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/fmt/tuic/TuicFmt.kt @@ -1,39 +1,82 @@ package io.nekohasekai.sagernet.fmt.tuic -import io.nekohasekai.sagernet.database.DataStore import io.nekohasekai.sagernet.fmt.LOCALHOST import io.nekohasekai.sagernet.ktx.isIpAddress -import io.nekohasekai.sagernet.ktx.toStringPretty -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.withContext -import moe.matsuri.nb4a.plugin.Plugins +import io.nekohasekai.sagernet.ktx.wrapIPV6Host +import moe.matsuri.nb4a.utils.JavaUtil +import moe.matsuri.nb4a.utils.Util import org.json.JSONArray import org.json.JSONObject import java.io.File -import java.net.InetAddress + +fun TuicBean.pluginId(): String { + return when (protocolVersion) { + 5 -> "tuic-v5-plugin" + else -> "tuic-plugin" + } +} fun TuicBean.buildTuicConfig(port: Int, cacheFile: (() -> File)?): String { - if (Plugins.isUsingMatsuriExe("tuic-plugin")) { - if (!serverAddress.isIpAddress()) { - runBlocking { - finalAddress = withContext(Dispatchers.IO) { - InetAddress.getAllByName(serverAddress) - }?.firstOrNull()?.hostAddress ?: "127.0.0.1" - // TODO network on main thread, tuic don't support "sni" - } - } - } + val config = when (protocolVersion) { + 5 -> buildTuicConfigV5(port, cacheFile) + else -> buildTuicConfigV4(port, cacheFile) + }.toString() + var gsonMap = mutableMapOf() + gsonMap = JavaUtil.gson.fromJson(config, gsonMap.javaClass) + Util.mergeJSON(customJSON, gsonMap) + return JavaUtil.gson.toJson(gsonMap) +} + +fun TuicBean.buildTuicConfigV5(port: Int, cacheFile: (() -> File)?): JSONObject { return JSONObject().apply { put("relay", JSONObject().apply { - if (sni.isNotBlank()) { - put("server", sni) - put("ip", finalAddress) - } else if (serverAddress.isIpAddress()) { - put("server", finalAddress) + if (sni.isNotBlank() && !disableSNI) { + put("server", "$sni:$finalPort") + if (finalAddress.isIpAddress()) { + put("ip", finalAddress) + } else { + throw Exception("TUIC must use IP address when you need spoof SNI.") + } } else { - put("server", serverAddress) - put("ip", finalAddress) + put("server", serverAddress.wrapIPV6Host() + ":" + finalPort) + } + + put("uuid", uuid) + put("password", token) + + if (caText.isNotBlank() && cacheFile != null) { + val caFile = cacheFile() + caFile.writeText(caText) + put("certificates", JSONArray(listOf(caFile.absolutePath))) + } + + put("udp_relay_mode", udpRelayMode) + if (alpn.isNotBlank()) { + put("alpn", JSONArray(alpn.split("\n"))) + } + put("congestion_control", congestionController) + put("disable_sni", disableSNI) + put("zero_rtt_handshake", disableSNI) + }) + put("local", JSONObject().apply { + put("server", "127.0.0.1:$port") + }) + put("log_level", "debug") + } +} + +fun TuicBean.buildTuicConfigV4(port: Int, cacheFile: (() -> File)?): JSONObject { + return JSONObject().apply { + put("relay", JSONObject().apply { + if (sni.isNotBlank() && !disableSNI) { + put("server", sni) + if (finalAddress.isIpAddress()) { + put("ip", finalAddress) + } else { + throw Exception("TUIC must use IP address when you need spoof SNI.") + } + } else { + put("server", finalAddress) } put("port", finalPort) put("token", token) @@ -59,6 +102,6 @@ fun TuicBean.buildTuicConfig(port: Int, cacheFile: (() -> File)?): String { put("ip", LOCALHOST) put("port", port) }) - put("log_level", if (DataStore.logLevel > 0) "debug" else "info") - }.toStringPretty() + put("log_level", "debug") + } } diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/profile/TuicSettingsActivity.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/profile/TuicSettingsActivity.kt index b548499..431bc7e 100644 --- a/app/src/main/java/io/nekohasekai/sagernet/ui/profile/TuicSettingsActivity.kt +++ b/app/src/main/java/io/nekohasekai/sagernet/ui/profile/TuicSettingsActivity.kt @@ -9,6 +9,8 @@ import io.nekohasekai.sagernet.R import io.nekohasekai.sagernet.database.DataStore import io.nekohasekai.sagernet.fmt.tuic.TuicBean import io.nekohasekai.sagernet.ktx.applyDefaultValues +import moe.matsuri.nb4a.ui.EditConfigPreference +import moe.matsuri.nb4a.ui.SimpleMenuPreference class TuicSettingsActivity : ProfileSettingsActivity() { @@ -27,8 +29,13 @@ class TuicSettingsActivity : ProfileSettingsActivity() { DataStore.serverSNI = sni DataStore.serverReduceRTT = reduceRTT DataStore.serverMTU = mtu + // DataStore.serverFastConnect = fastConnect DataStore.serverAllowInsecure = allowInsecure + // + DataStore.serverConfig = customJSON + DataStore.serverProtocolVersion = protocolVersion + DataStore.serverUsername = uuid } override fun TuicBean.serialize() { @@ -44,16 +51,48 @@ class TuicSettingsActivity : ProfileSettingsActivity() { sni = DataStore.serverSNI reduceRTT = DataStore.serverReduceRTT mtu = DataStore.serverMTU + // fastConnect = DataStore.serverFastConnect allowInsecure = DataStore.serverAllowInsecure + // + customJSON = DataStore.serverConfig + protocolVersion = DataStore.serverProtocolVersion + uuid = DataStore.serverUsername } + private lateinit var editConfigPreference: EditConfigPreference + override fun PreferenceFragmentCompat.createPreferences( savedInstanceState: Bundle?, rootKey: String?, ) { addPreferencesFromResource(R.xml.tuic_preferences) + editConfigPreference = findPreference(Key.SERVER_CONFIG)!! + + val uuid = findPreference(Key.SERVER_USERNAME)!! + val mtu = findPreference(Key.SERVER_MTU)!! + val fastConnect = findPreference(Key.SERVER_FAST_CONNECT)!! + val allowInsecure = findPreference(Key.SERVER_ALLOW_INSECURE)!! + fun updateVersion(v: Int) { + if (v == 5) { + uuid.isVisible = true + mtu.isVisible = false + fastConnect.isVisible = false + allowInsecure.isVisible = false + } else { + uuid.isVisible = false + mtu.isVisible = true + fastConnect.isVisible = true + allowInsecure.isVisible = true + } + } + findPreference(Key.SERVER_PROTOCOL)!!.setOnPreferenceChangeListener { _, newValue -> + updateVersion(newValue.toString().toIntOrNull() ?: 4) + true + } + updateVersion(DataStore.serverProtocolVersion) + val disableSNI = findPreference(Key.SERVER_DISABLE_SNI)!! val sni = findPreference(Key.SERVER_SNI)!! sni.isEnabled = !disableSNI.isChecked @@ -67,4 +106,12 @@ class TuicSettingsActivity : ProfileSettingsActivity() { } } + override fun onResume() { + super.onResume() + + if (::editConfigPreference.isInitialized) { + editConfigPreference.notifyChanged() + } + } + } \ No newline at end of file diff --git a/app/src/main/java/moe/matsuri/nb4a/utils/KotlinUtil.kt b/app/src/main/java/moe/matsuri/nb4a/utils/KotlinUtil.kt index 0b45095..51a9560 100644 --- a/app/src/main/java/moe/matsuri/nb4a/utils/KotlinUtil.kt +++ b/app/src/main/java/moe/matsuri/nb4a/utils/KotlinUtil.kt @@ -1,5 +1,6 @@ package moe.matsuri.nb4a.utils +import android.annotation.SuppressLint import android.content.Context import android.graphics.drawable.Drawable import androidx.appcompat.content.res.AppCompatResources @@ -34,6 +35,7 @@ fun File.recreate(dir: Boolean) { // Context utils +@SuppressLint("DiscouragedApi") fun Context.getDrawableByName(name: String?): Drawable? { val resourceId: Int = resources.getIdentifier(name, "drawable", packageName) return AppCompatResources.getDrawable(this, resourceId) diff --git a/app/src/main/java/moe/matsuri/nb4a/utils/Util.kt b/app/src/main/java/moe/matsuri/nb4a/utils/Util.kt index 66cdfbb..8db2d02 100644 --- a/app/src/main/java/moe/matsuri/nb4a/utils/Util.kt +++ b/app/src/main/java/moe/matsuri/nb4a/utils/Util.kt @@ -113,6 +113,21 @@ object Util { } } + + fun mergeJSON(j: String, to: MutableMap) { + if (j.isBlank()) return + val m = JavaUtil.gson.fromJson(j, to.javaClass) + m.forEach { (k, v) -> + if (v is Map<*, *> && to[k] is Map<*, *>) { + val currentMap = (to[k] as Map<*, *>).toMutableMap() + currentMap += v + to[k] = currentMap + } else { + to[k] = v + } + } + } + // Format Time @SuppressLint("SimpleDateFormat") diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 665073e..a0f2a81 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -386,6 +386,16 @@ BASE64 + + v4 + v5 + + + + 4 + 5 + + SOCKS4 SOCKS4A diff --git a/app/src/main/res/xml/tuic_preferences.xml b/app/src/main/res/xml/tuic_preferences.xml index c29b67b..dee6bb5 100644 --- a/app/src/main/res/xml/tuic_preferences.xml +++ b/app/src/main/res/xml/tuic_preferences.xml @@ -6,6 +6,15 @@ app:title="@string/profile_name" app:useSimpleSummaryProvider="true" /> + + + + app:title="@string/password" /> + \ No newline at end of file