tuic v5 support

This commit is contained in:
arm64v8a 2023-06-08 20:30:59 +09:00
parent 34a3012772
commit 4b1800d782
11 changed files with 223 additions and 65 deletions

View File

@ -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"

View File

@ -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,
)

View File

@ -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<Int, ProxyEntity>)
}
fun mergeJSON(j: String, to: MutableMap<String, Any>) {
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,

View File

@ -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"

View File

@ -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

View File

@ -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<String, Any>()
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")
}
}

View File

@ -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<TuicBean>() {
@ -27,8 +29,13 @@ class TuicSettingsActivity : ProfileSettingsActivity<TuicBean>() {
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<TuicBean>() {
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<EditTextPreference>(Key.SERVER_USERNAME)!!
val mtu = findPreference<EditTextPreference>(Key.SERVER_MTU)!!
val fastConnect = findPreference<SwitchPreference>(Key.SERVER_FAST_CONNECT)!!
val allowInsecure = findPreference<SwitchPreference>(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<SimpleMenuPreference>(Key.SERVER_PROTOCOL)!!.setOnPreferenceChangeListener { _, newValue ->
updateVersion(newValue.toString().toIntOrNull() ?: 4)
true
}
updateVersion(DataStore.serverProtocolVersion)
val disableSNI = findPreference<SwitchPreference>(Key.SERVER_DISABLE_SNI)!!
val sni = findPreference<EditTextPreference>(Key.SERVER_SNI)!!
sni.isEnabled = !disableSNI.isChecked
@ -67,4 +106,12 @@ class TuicSettingsActivity : ProfileSettingsActivity<TuicBean>() {
}
}
override fun onResume() {
super.onResume()
if (::editConfigPreference.isInitialized) {
editConfigPreference.notifyChanged()
}
}
}

View File

@ -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)

View File

@ -113,6 +113,21 @@ object Util {
}
}
fun mergeJSON(j: String, to: MutableMap<String, Any>) {
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")

View File

@ -386,6 +386,16 @@
<item>BASE64</item>
</string-array>
<string-array name="tuic_version">
<item>v4</item>
<item>v5</item>
</string-array>
<string-array name="tuic_version_value">
<item>4</item>
<item>5</item>
</string-array>
<string-array name="socks_versions">
<item>SOCKS4</item>
<item>SOCKS4A</item>

View File

@ -6,6 +6,15 @@
app:title="@string/profile_name"
app:useSimpleSummaryProvider="true" />
<moe.matsuri.nb4a.ui.SimpleMenuPreference
app:defaultValue="4"
app:entries="@array/tuic_version"
app:entryValues="@array/tuic_version_value"
app:icon="@drawable/ic_baseline_nfc_24"
app:key="serverProtocol"
app:title="@string/app_version"
app:useSimpleSummaryProvider="true" />
<PreferenceCategory app:title="@string/proxy_cat">
<EditTextPreference
@ -18,11 +27,16 @@
app:key="serverPort"
app:title="@string/server_port"
app:useSimpleSummaryProvider="true" />
<EditTextPreference
app:icon="@drawable/ic_baseline_person_24"
app:key="serverUsername"
app:title="@string/uuid"
app:useSimpleSummaryProvider="true" />
<EditTextPreference
app:dialogLayout="@layout/layout_password_dialog"
app:icon="@drawable/ic_settings_password"
app:key="serverPassword"
app:title="@string/tuic_token" />
app:title="@string/password" />
<EditTextPreference
app:icon="@drawable/ic_baseline_legend_toggle_24"
app:key="serverALPN"
@ -77,6 +91,11 @@
app:key="serverMTU"
app:title="@string/mtu"
app:useSimpleSummaryProvider="true" />
<moe.matsuri.nb4a.ui.EditConfigPreference
app:icon="@drawable/ic_baseline_layers_24"
app:key="serverConfig"
app:title="@string/custom_config"
app:useSimpleSummaryProvider="true" />
</PreferenceCategory>
</PreferenceScreen>