chat: invite-ссылки с несколькими адресами + ncd-адреса узлов (join)

This commit is contained in:
evgeny committed 2026-09-15 05:16:04 +03:00
1 parent d88dccfa19
commit 481872d5db
24 files changed
+517 -114

No files matched your search

@@ -4,19 +4,28 @@ import android.util.Base64
import com.utun.chat.data.LogManager
import java.security.MessageDigest
const val INVITE_LINK_VERSION: Int = 0x01
const val INVITE_LINK_VERSION: Int = 0x03
const val INVITE_PUBKEY_SIZE: Int = 32
const val INVITE_SNAME_MAX: Int = 64
const val INVITE_PROTO_UDP: Int = 0x01
const val INVITE_PROTO_TCP: Int = 0x02
const val INVITE_PROTO_BOTH: Int = 0x03
const val INVITE_PROTO_REALITY: Int = 0x04 // флаг reality-камуфляжа поверх TCP (reality-TCP = 0x06)
data class InviteReality(
val has: Boolean,
val shortId: ByteArray,
val serverPubkey: ByteArray,
val version: ByteArray,
val serverName: String
)
data class InviteAddr(
val family: Int, // 4 or 6
val address: ByteArray,
val port: Int,
val socketId: Int,
val proto: Int // INVITE_PROTO_UDP/TCP/BOTH
val proto: Int // INVITE_PROTO_*
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
@@ -32,13 +41,26 @@ data class InviteData(
val joinKey: Long,
val pubkey: ByteArray,
val nodeId: Long,
val addrs: List<InviteAddr>
val addrs: List<InviteAddr>,
val password: String = "",
val reality: InviteReality? = null
) {
/** Serialize addresses for C bridge: family(1) + socketId(1) + proto(1) + address(4|16) + port(2 BE) */
/** Serialize addresses for C bridge: reality-префикс + family(1)+socketId(1)+proto(1)+address(4|16)+port(2 BE) */
fun serializeAddrs(): ByteArray {
val size = addrs.fold(0) { acc, a -> acc + if (a.family == 4) 9 else 21 }
var size = 1
if (reality != null && reality.has) size += 8 + 32 + 3 + 1 + reality.serverName.toByteArray(Charsets.UTF_8).let { minOf(it.size, INVITE_SNAME_MAX - 1) }
size = addrs.fold(size) { acc, a -> acc + if (a.family == 4) 9 else 21 }
val buf = ByteArray(size)
var pos = 0
buf[pos] = (if (reality != null && reality.has) 1 else 0).toByte(); pos++
if (reality != null && reality.has) {
System.arraycopy(reality.shortId, 0, buf, pos, 8); pos += 8
System.arraycopy(reality.serverPubkey, 0, buf, pos, 32); pos += 32
System.arraycopy(reality.version, 0, buf, pos, 3); pos += 3
val sn = reality.serverName.toByteArray(Charsets.UTF_8).let { if (it.size > INVITE_SNAME_MAX - 1) it.copyOf(INVITE_SNAME_MAX - 1) else it }
buf[pos] = sn.size.toByte(); pos++
System.arraycopy(sn, 0, buf, pos, sn.size); pos += sn.size
}
for (a in addrs) {
buf[pos] = a.family.toByte(); pos++
buf[pos] = a.socketId.toByte(); pos++
@@ -85,13 +107,25 @@ object InviteLink {
}
var off = 0
if ((raw[off].toInt() and 0xFF) != INVITE_LINK_VERSION) {
val ver = raw[off].toInt() and 0xFF
if (ver != INVITE_LINK_VERSION && ver != 0x02 && ver != 0x01) {
LogManager.addLog("ERROR", "INVITE", "decode: wrong version 0x%02x expected 0x%02x".format(
raw[off].toInt() and 0xFF, INVITE_LINK_VERSION))
return null
}
off++
var password = ""
if (ver == 0x02 || ver == INVITE_LINK_VERSION) {
if (off >= raw.size) return null
val plen = raw[off].toInt() and 0xFF; off++
if (plen > 0) {
if (off + plen > raw.size) return null
password = String(raw.copyOfRange(off, off + plen), Charsets.UTF_8)
off += plen
}
}
var chId = 0L
for (i in 0 until 8) {
if (off + i >= raw.size) return null
@@ -106,6 +140,24 @@ object InviteLink {
}
off += 8
var reality: InviteReality? = null
if (ver == INVITE_LINK_VERSION) {
if (off >= raw.size) return null
val has = (raw[off].toInt() and 0xFF) != 0; off++
if (has) {
if (off + 8 + 32 + 3 + 1 > raw.size) return null
val shortId = raw.copyOfRange(off, off + 8); off += 8
val serverPubkey = raw.copyOfRange(off, off + 32); off += 32
val version = raw.copyOfRange(off, off + 3); off += 3
var snlen = raw[off].toInt() and 0xFF; off++
if (snlen > INVITE_SNAME_MAX - 1) snlen = INVITE_SNAME_MAX - 1
if (off + snlen > raw.size) return null
val serverName = String(raw.copyOfRange(off, off + snlen), Charsets.UTF_8)
off += snlen
reality = InviteReality(has = true, shortId = shortId, serverPubkey = serverPubkey, version = version, serverName = serverName)
}
}
LogManager.addLog("DEBUG", "INVITE", "decode: chId=0x%016x joinKey=0x%016x".format(chId, joinKey))
var pubkey: ByteArray? = null
@@ -143,14 +195,15 @@ object InviteLink {
val pkHex = pubkey.take(8).joinToString("") { "%02x".format(it) }
LogManager.addLog("INFO", "INVITE", "decode: ch=0x%016x node=0x%016x pk=%s... addrs=%d".format(
chId, nodeId, pkHex, addrs.size))
return InviteData(channelId = chId, joinKey = joinKey, pubkey = pubkey, nodeId = nodeId, addrs = addrs)
return InviteData(channelId = chId, joinKey = joinKey, pubkey = pubkey, nodeId = nodeId,
addrs = addrs, password = password, reality = reality)
}
fun addrToString(addr: InviteAddr): String {
val protoStr = when (addr.proto) {
INVITE_PROTO_UDP -> "UDP"
INVITE_PROTO_TCP -> "TCP"
INVITE_PROTO_BOTH -> "UDP+TCP"
val protoStr = when {
(addr.proto and INVITE_PROTO_REALITY) != 0 -> "TCP+REALITY"
addr.proto == INVITE_PROTO_UDP -> "UDP"
addr.proto == INVITE_PROTO_TCP -> "TCP"
else -> "?${addr.proto}"
}
val ip = if (addr.family == 4 && addr.address.size == 4) {