____ _ _______ __
/ __ \ | /| / / __/ |/ /
/ /_/ / |/ |/ / _// /
\____/|__/|__/___/_||_/
owenclave / app/src/main/java/io/nekohasekai/sagernet/fmt/tuic5/Tuic5Fmt.kt
/******************************************************************************
* *
* Copyright (C) 2023 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.tuic5
import io.nekohasekai.sagernet.fmt.AbstractBean
import io.nekohasekai.sagernet.ktx.listByLineOrComma
import io.nekohasekai.sagernet.ktx.queryParameter
import libexclavecore.Libexclavecore
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid
val supportedTuic5CongestionControl = arrayOf("cubic", "bbr", "new_reno")
val supportedTuic5RelayMode = arrayOf("native", "quic")
@OptIn(ExperimentalUuidApi::class)
fun parseTuic(server: String): AbstractBean {
var link = Libexclavecore.parseURL(server)
if (link.queryParameter("version") == "4") {
error("unsupported")
}
if (server.length >= 46
&& server.substring(7, 15).all {
(it in '0'..'9') || (it in 'a'..'f') || (it in 'A'..'F')
} && server[15] == '-'
&& server.substring(16, 20).all {
(it in '0'..'9') || (it in 'a'..'f') || (it in 'A'..'F')
} && server[20] == '-'
&& server.substring(21, 25).all {
(it in '0'..'9') || (it in 'a'..'f') || (it in 'A'..'F')
} && server[25] == '-'
&& server.substring(26, 30).all {
(it in '0'..'9') || (it in 'a'..'f') || (it in 'A'..'F')
} && server[30] == '-'
&& server.substring(31, 43).all {
(it in '0'..'9') || (it in 'a'..'f') || (it in 'A'..'F')
} && server.substring(43, 46) == "%3A"
) {
// v2rayN broken format
link = Libexclavecore.parseURL(server.take(43) + ":" + server.substring(46, server.length))
}
try {
Uuid.parse(link.username)
} catch (_: Exception) {
error("unsupported")
}
return Tuic5Bean().apply {
serverAddress = link.host.ifEmpty { error("empty host") }
serverPort = link.port
if (link.port == 0) {
serverPort = 443
}
uuid = link.username
password = link.password
link.queryParameter("sni")?.let {
sni = it
}
link.queryParameter("alpn")?.let {
alpn = it.split(",").joinToString("\n")
}
(link.queryParameter("congestion_controller") ?:
link.queryParameter("congestion-controller") ?:
link.queryParameter("congestion_control") ?:
link.queryParameter("congestion-control"))?.let {
congestionControl = when (it) {
in supportedTuic5CongestionControl -> it
"new-reno" -> "new_reno"
else -> "cubic"
}
}
(link.queryParameter("udp-relay-mode") ?:
link.queryParameter("udp_relay_mode") ?:
link.queryParameter("udp-relay_mode") ?:
link.queryParameter("udp_relay-mode"))?.let {
udpRelayMode = when (it) {
in supportedTuic5RelayMode -> it
else -> "native"
}
}
(link.queryParameter("disable_sni") ?: link.queryParameter("disable-sni"))
?.takeIf { it == "1" || it == "true" }?.let {
disableSNI = true
}
(link.queryParameter("reduce_rtt") ?: link.queryParameter("reduce-rtt"))
?.takeIf { it == "1" || it == "true" }?.let {
zeroRTTHandshake = true
}
(link.queryParameter("allow_insecure") ?: link.queryParameter("allow-insecure") ?:
link.queryParameter("insecure") ?: link.queryParameter("allowInsecure"))
?.takeIf { it == "1" || it == "true" }?.let {
allowInsecure = true
}
link.fragment?.let {
name = it
}
}
}
fun Tuic5Bean.toUri(): String? {
val builder = Libexclavecore.newURL("tuic").apply {
setHostPort(serverAddress.ifEmpty { error("empty server address") }, serverPort)
username = uuid.ifEmpty { error("empty uuid") }
if (name.isNotEmpty()) {
fragment = name
}
}
if (password.isNotEmpty()) {
builder.password = password
}
builder.addQueryParameter("version", "5")
builder.addQueryParameter("udp_relay_mode", udpRelayMode)
builder.addQueryParameter("congestion_control", congestionControl)
builder.addQueryParameter("congestion_controller", congestionControl)
if (sni.isNotEmpty()) {
builder.addQueryParameter("sni", sni)
}
if (alpn.isNotEmpty()) {
builder.addQueryParameter("alpn", alpn.listByLineOrComma().joinToString(","))
}
if (disableSNI) {
builder.addQueryParameter("disable_sni", "1")
}
if (zeroRTTHandshake) {
builder.addQueryParameter("reduce_rtt", "1")
}
// as pinned certificate is not exportable, only add `allow_insecure=1` if pinned certificate is not used
if (allowInsecure && pinnedPeerCertificateSha256.isEmpty() &&
pinnedPeerCertificatePublicKeySha256.isEmpty() && pinnedPeerCertificateChainSha256.isEmpty() &&
serverNameToVerify.listByLineOrComma().isEmpty()) {
builder.addQueryParameter("allow_insecure", "1")
}
return builder.string
}