owenclave / app/src/main/java/io/nekohasekai/sagernet/fmt/trusttunnel/TrustTunnelFmt.kt

github - dev - back to tree - upstream
/****************************************************************************** * * * Copyright (C) 2026 dyhkwong * * * * 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. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see . * * * ******************************************************************************/ package io.nekohasekai.sagernet.fmt.trusttunnel import io.nekohasekai.sagernet.ktx.applyDefaultValues import io.nekohasekai.sagernet.ktx.joinHostPort import io.nekohasekai.sagernet.ktx.listByLineOrComma import libexclavecore.Libexclavecore import kotlin.io.encoding.Base64 // https://github.com/TrustTunnel/TrustTunnel/blob/8856e7ba83ae0c9faace78aaf9a95b1b291cd3ed/DEEP_LINK.md // https://github.com/TrustTunnel/TrustTunnel/blob/8ba8f34da84b54ff49c248ec8c940c38010b511e/DEEP_LINK.md // https://github.com/TrustTunnel/TrustTunnel/blob/9e144fe4bcd0c2cf8f6690a97ba9b4df571a8ec0/DEEP_LINK.md private enum class Tag(val code: Long) { Version(0x00), Hostname(0x01), Addresses(0x02), CustomSNI(0x03), HasIPv6(0x04), Username(0x05), Password(0x06), SkipVerification(0x07), Certificate(0x08), UpstreamProtocol(0x09), AntiDPI(0x0A), ClientRandomPrefix(0x0B), Name(0x0C), DNSUptreams(0x0D), } private enum class Version(val code: Byte) { Version0(0x00), Version1(0x01), } private enum class HasIPv6(val code: Byte) { False(0x00), True(0x01), } private enum class SkipVerification(val code: Byte) { False(0x00), True(0x01), } private enum class UpstreamProtocol(val code: Byte) { HTTP2(0x01), HTTP3(0x02), } private enum class AntiDPI(val code: Byte) { False(0x00), True(0x01), } @Suppress("ArrayInDataClass") private data class TLV( val tag: Long, val value: ByteArray, ) fun TrustTunnelBean.toUri(): String { require(serverAddress.isNotEmpty(), { "empty server address" }) require(username.isNotEmpty(), { "empty username" }) require(password.isNotEmpty(), { "empty password" }) require(serverPort in 0..65535, { "invalid port" }) require(protocol == "https" || protocol == "quic", { "invalid protocol" }) val byteArrayBuilder = ArrayList().apply { writeTLV(Tag.Addresses.code, joinHostPort(serverAddress, serverPort).toByteArray()) val serverNames = serverNameToVerify.listByLineOrComma() require(serverNames.size <= 1, { "only one serverNameToVerify value is supported" }) if (serverNames.size == 1) { require(serverNames[0].isNotEmpty(), { "serverNameToVerify contains empty value" }) // serverNameToVerify will always verify even if allowInsecure is true writeTLV(Tag.Hostname.code, serverNames[0].toByteArray()) require(!Libexclavecore.isIP(sni.ifEmpty { serverAddress }), { "IP address can't be CustomSNI" }) writeTLV(Tag.CustomSNI.code, sni.ifEmpty { serverAddress }.toByteArray()) } else { writeTLV(Tag.Hostname.code, sni.ifEmpty { serverAddress }.toByteArray()) } writeTLV(Tag.Username.code, username.toByteArray()) writeTLV(Tag.Password.code, password.toByteArray()) if (pinnedPeerCertificateChainSha256.isEmpty() && pinnedPeerCertificatePublicKeySha256.isEmpty() && pinnedPeerCertificateSha256.isEmpty() && serverNames.isEmpty() && allowInsecure) { writeTLV(Tag.SkipVerification.code, byteArrayOf(SkipVerification.True.code)) } when (protocol) { "https" -> writeTLV(Tag.UpstreamProtocol.code, byteArrayOf(UpstreamProtocol.HTTP2.code)) "quic" -> writeTLV(Tag.UpstreamProtocol.code, byteArrayOf(UpstreamProtocol.HTTP3.code)) } if (certificate.isNotEmpty()) { val der = Libexclavecore.pemToDer(certificate) require(der.isNotEmpty()) writeTLV(Tag.Certificate.code, der) } if (name.isNotEmpty()) { writeTLV(Tag.Name.code, name.toByteArray()) } writeTLV(Tag.Version.code, byteArrayOf(Version.Version1.code)) } return "tt://?" + Base64.UrlSafe.withPadding(Base64.PaddingOption.ABSENT).encode(byteArrayBuilder.toByteArray()) } fun parseTrustTunnel(url: String): List { try { val data = Base64.UrlSafe.withPadding(Base64.PaddingOption.ABSENT).decode( if (url.startsWith("tt://?")) url.substring("tt://?".length) else url.substring("tt://".length) ) val bean = TrustTunnelBean() val addresses = mutableListOf() var hasHostName = false var hasAddresses = false var hasUsername = false var hasPassword = false var hostname = "" var customSNI = "" var offset = 0 while (offset < data.size) { val tlv = data.readTLV(offset) val tag = tlv.tag val length = tlv.value.size val value = tlv.value offset += when { tag < 64 -> 1 tag < 16384 -> 2 tag < 1073741824 -> 4 else -> 8 } + when { length < 64 -> 1 length < 16384 -> 2 length < 1073741824 -> 4 else -> 8 } + length when (tlv.tag) { Tag.Version.code -> { require(length == 1, { "invalid Version" }) require(value[0] == Version.Version0.code || value[0] == Version.Version1.code) } Tag.Hostname.code -> { require(value.isNotEmpty(), { "empty Hostname" }) hostname = String(value) hasHostName = true } Tag.Addresses.code -> { addresses.add(String(value)) hasAddresses = true } Tag.CustomSNI.code -> { require(length > 0, { "empty CustomSNI" }) customSNI = String(value) } Tag.HasIPv6.code -> { require(length == 1, { "invalid HasIPv6" }) require(value[0] == HasIPv6.False.code || value[0] == HasIPv6.True.code, { "invalid HasIPv6" }) } Tag.Username.code -> { require(value.isNotEmpty(), { "empty Username" }) bean.username = String(value) hasUsername = true } Tag.Password.code -> { require(value.isNotEmpty(), { "empty Password" }) bean.password = String(value) hasPassword = true } Tag.SkipVerification.code -> { require(length == 1, { "invalid SkipVerification" }) require(value[0] == SkipVerification.False.code || value[0] == SkipVerification.True.code, { "invalid SkipVerification" }) bean.allowInsecure = value[0] == SkipVerification.True.code } Tag.Certificate.code -> { val pem = Libexclavecore.derToPem(value) require(pem.isNotEmpty(), { "invalid Certificate" }) bean.certificate = pem } Tag.UpstreamProtocol.code -> { require(length == 1, { "invalid UpstreamProtocol" }) require(value[0] == UpstreamProtocol.HTTP2.code || value[0] == UpstreamProtocol.HTTP3.code, { "invalid UpstreamProtocol" }) } Tag.AntiDPI.code -> { require(length == 1, { "invalid AntiDPI" }) require(value[0] == AntiDPI.False.code || value[0] == AntiDPI.True.code, { "invalid AntiDPI" }) } Tag.ClientRandomPrefix.code -> { // ignored } Tag.Name.code -> { bean.name = String(value) } Tag.DNSUptreams.code -> { // ignored } else -> { // "A parser MUST ignore unknown tags to allow forward-compatible extensions." } } } require(hasHostName, { "missing hostname" }) require(hasAddresses, { "missing addresses" }) require(hasUsername, { "missing username" }) require(hasPassword, { "missing password" }) if (customSNI.isNotEmpty()) { bean.sni = customSNI // Do not verify if SkipVerification is true. if (bean.allowInsecure != true) { bean.serverNameToVerify = hostname } } else { bean.sni = hostname } val beans = mutableListOf() addresses.forEach { if (Libexclavecore.isIP(it)) { beans.add(bean.applyDefaultValues().clone().apply { serverAddress = it serverPort = 443 }) return@forEach } require(it.contains(":"), { "missing colon in address" }) val port = it.substringAfterLast(":").toIntOrNull() require(port != null && port in 0..65535, { "invalid port" }) var host = it.substringBeforeLast(":") if (host.startsWith("[") && host.endsWith("]")) { host = host.substringAfter("[").substringBeforeLast("]") require(Libexclavecore.isIPv6(host), { "non IPv6 address in brackets" }) } beans.add(bean.applyDefaultValues().clone().apply { serverAddress = host serverPort = port }) } return beans } catch (e: Exception) { throw e } } private fun ArrayList.writeTLV(tag: Long, value: ByteArray) { writeUVarInt(tag) writeUVarInt(value.size.toLong()) addAll(value.toList()) } private fun ArrayList.writeUVarInt(i: Long) { require(i < 4611686018427387904) when { i < 64 -> { add((i and 0x3F).toByte()) } i < 16384 -> { val encoded = i or (0x1L shl 14) add(((encoded shr 8) and 0xFF).toByte()) add((encoded and 0xFF).toByte()) } i < 1073741824 -> { val encoded = i or (0x2L shl 30) add(((encoded shr 24) and 0xFF).toByte()) add(((encoded shr 16) and 0xFF).toByte()) add(((encoded shr 8) and 0xFF).toByte()) add((encoded and 0xFF).toByte()) } else -> { val encoded = i or (0x3L shl 62) add(((encoded shr 56) and 0xFF).toByte()) add(((encoded shr 48) and 0xFF).toByte()) add(((encoded shr 40) and 0xFF).toByte()) add(((encoded shr 32) and 0xFF).toByte()) add(((encoded shr 24) and 0xFF).toByte()) add(((encoded shr 16) and 0xFF).toByte()) add(((encoded shr 8) and 0xFF).toByte()) add((encoded and 0xFF).toByte()) } } } private fun ByteArray.readTLV(offset: Int = 0): TLV { try { var newOffset = offset val tag = readUVarInt(offset) newOffset += when { tag < 64 -> 1 tag < 16384 -> 2 tag < 1073741824 -> 4 else -> 8 } val length = readUVarInt(newOffset) require(length < Int.MAX_VALUE) newOffset += when { length < 64 -> 1 length < 16384 -> 2 length < 1073741824 -> 4 else -> 8 } val value = copyOfRange(newOffset, newOffset+length.toInt()) return TLV( tag = tag, value = value, ) } catch (e: Exception) { throw e } } private fun ByteArray.readUVarInt(offset: Int = 0): Long { try { val first = this[offset].toInt() and 0xFF val prefix = first shr 6 require(prefix in 0x00..0x03) when (prefix) { 0x00 -> { return (first and 0x3F).toLong() } 0x01 -> { val b1 = this[offset + 1].toInt() and 0xFF return ((first and 0x3F) shl 8 or b1).toLong() } 0x02 -> { val b1 = this[offset + 1].toInt() and 0xFF val b2 = this[offset + 2].toInt() and 0xFF val b3 = this[offset + 3].toInt() and 0xFF return ((first and 0x3F).toLong() shl 24) or (b1.toLong() shl 16) or (b2.toLong() shl 8) or b3.toLong() } else -> { var value = (first and 0x3F).toLong() shl 56 for (i in 1..7) { value = value or ((this[offset + i].toInt() and 0xFF).toLong() shl (56 - 8 * i)) } return value } } } catch (e: Exception) { throw e } }