Browse Source

invite: add proto field (UDP/TCP/BOTH) to invite link binary format

Format per v4 addr: family(1) + socketId(1) + proto(1) + addr(4) + port(2) = 9 bytes (was 8)
Proto values: 0x01=UDP, 0x02=TCP, 0x03=BOTH
Affects: invite_link_c.h/c (Android), invite_link.h/cpp (desktop), chat_sync.c, conn_mgr.c, joindialog.cpp, utun_node.cpp
topo_upd
evgeny 2 months ago
parent
commit
d0cca27edd
  1. 54
      BUILD_OPENWRT.md
  2. 124
      build_openwrt.sh
  3. 11
      opencode.json
  4. 7
      src/chat/chat_sync.c
  5. 2
      src/routing_layer/conn_mgr.c
  6. 15
      tools/chatgui-android/app/src/main/java/com/utun/chat/ChatApplication.kt
  7. 6
      tools/chatgui-android/app/src/main/java/com/utun/chat/MainActivity.kt
  8. 51
      tools/chatgui-android/app/src/main/java/com/utun/chat/data/ChatRepository.kt
  9. 2
      tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt
  10. 7
      tools/chatgui-android/app/src/main/java/com/utun/chat/headless/HeadlessService.kt
  11. 15
      tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/MessageBubble.kt
  12. 14
      tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/ChannelListScreen.kt
  13. 17
      tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/ChatScreen.kt
  14. 2
      tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt
  15. 27
      tools/chatgui-android/jni_bridge/android_jni_bridge.c
  16. 3
      tools/chatgui-android/jni_bridge/android_jni_bridge.h
  17. 7
      tools/chatgui-android/libutun_lite/invite_link_c.c
  18. 7
      tools/chatgui-android/libutun_lite/invite_link_c.h
  19. 6
      tools/chatgui/src/invite_link.cpp
  20. 5
      tools/chatgui/src/invite_link.h
  21. 1
      tools/chatgui/src/joindialog.cpp
  22. 3
      tools/chatgui/transport/utun_node.cpp
  23. 4
      utun.sh

54
BUILD_OPENWRT.md

@ -0,0 +1,54 @@
# Сборка uTun под OpenWRT
## Быстрый старт
```bash
cd ~/utun2
./build_openwrt.sh # ASUS RT-AX53U (по умолчанию)
```
Первый запуск соберёт toolchain (30-120 минут), последующие — только пакет.
## Другие платформы
```bash
./build_openwrt.sh ramips/mt7621 asus_rt-ax54 # ASUS RT-AX54
./build_openwrt.sh ramips/mt7621 xiaomi_mi-router-4a-gigabit
./build_openwrt.sh ath79/generic tplink_archer-c7-v2
./build_openwrt.sh x86/64 # x86_64 PC
./build_openwrt.sh --list # список целей
```
## Ручная сборка (menuconfig)
```bash
cd /home/user/openwrt
make menuconfig # Target System → MediaTek Ralink MIPS
# Subtarget → MT7621 based boards
# Target Profile → ASUS RT-AX53U
# Network → utun (M)
make -j$(nproc) # полная сборка прошивки
# или только пакет:
make package/utun2/compile -j$(nproc) V=s
```
Пакет: `bin/packages/*/base/utun_*.ipk`
## Структура
- `~/openwrt/package/utun2/Makefile` — OpenWRT-пакет
- `~/openwrt/` — клон buildroot (main, https://github.com/openwrt/openwrt)
- `~/utun2/build_openwrt.sh` — скрипт сборки
## Зависимости (host)
```bash
apt-get install -y build-essential git subversion libncurses-dev \
libssl-dev ccache xsltproc gawk gettext unzip python3 libtool
```
## Примечания
- `configure` генерируется через `autoreconf -fi` (уже сделан)
- Пакет использует `USE_SOURCE_DIR` — билд из /home/user/utun2 напрямую
- Для чистой пересборки: `make package/utun2/clean && make package/utun2/compile -j$(nproc)`

124
build_openwrt.sh

@ -0,0 +1,124 @@
#!/bin/bash
# OpenWRT build script for uTun
# Usage: ./build_openwrt.sh [TARGET] [PROFILE]
#
# Examples:
# ./build_openwrt.sh # ASUS RT-AX53U (default)
# ./build_openwrt.sh ramips/mt7621 asus_rt-ax53u
# ./build_openwrt.sh ath79/generic tplink_archer-c7-v2
# ./build_openwrt.sh x86/64
# ./build_openwrt.sh --list # list available targets
#
# First run builds the full toolchain (30-120 min).
# Subsequent runs only rebuild the utun package.
set -e
OPENWRT_ROOT=/home/user/openwrt
# --- Default target: ASUS RT-AX53U ---
TARGET="${1:-ramips/mt7621}"
PROFILE="${2:-asus_rt-ax53u}"
J="${J:-$(nproc)}"
# --- Help / list ---
if [ "$1" = "--list" ] || [ "$1" = "-l" ]; then
echo "=== Predefined targets ==="
echo " ramips/mt7621 asus_rt-ax53u ASUS RT-AX53U (default)"
echo " ramips/mt7621 asus_rt-ax54 ASUS RT-AX54"
echo " ramips/mt7621 xiaomi_mi-router-4a-gigabit Xiaomi Mi Router 4A"
echo " ath79/generic tplink_archer-c7-v2 TP-Link Archer C7 v2"
echo " x86/64 Generic x86_64 PC"
echo " x86/generic Generic x86 PC"
echo ""
echo "Full target list: grep 'TARGET_DEVICES' $OPENWRT_ROOT/target/linux/*/image/*.mk"
exit 0
fi
# --- Check OpenWRT exists ---
if [ ! -d "$OPENWRT_ROOT" ]; then
echo "ERROR: OpenWRT not found at $OPENWRT_ROOT"
echo "Clone: git clone --depth 1 https://github.com/openwrt/openwrt.git $OPENWRT_ROOT"
exit 1
fi
# --- Check utun package exists ---
if [ ! -f "$OPENWRT_ROOT/package/utun2/Makefile" ]; then
echo "ERROR: utun package not found in OpenWRT"
echo "Expected: $OPENWRT_ROOT/package/utun2/Makefile"
exit 1
fi
echo "============================================"
echo " uTun OpenWRT Build"
echo " Target: $TARGET"
echo " Profile: $PROFILE"
echo " Jobs: $J"
echo "============================================"
cd "$OPENWRT_ROOT"
# --- Select target ---
TARGET_NAME=$(echo "$TARGET" | cut -d/ -f1)
SUBTARGET_NAME=$(echo "$TARGET" | cut -d/ -f2)
echo ""
echo "[1/3] Configuring target..."
# Write .config
cat > .config << EOF
CONFIG_TARGET_${TARGET_NAME}=y
CONFIG_TARGET_${TARGET_NAME}_${SUBTARGET_NAME}=y
CONFIG_TARGET_${TARGET_NAME}_${SUBTARGET_NAME}_DEVICE_${PROFILE}=y
CONFIG_PACKAGE_utun=m
# Basic system
CONFIG_PACKAGE_libc=y
CONFIG_PACKAGE_libopenssl=y
CONFIG_PACKAGE_libpthread=y
CONFIG_ALL_NONSHARED=n
CONFIG_ALL_KMODS=n
CONFIG_ALL=n
CONFIG_DEVEL=y
CONFIG_CCACHE=y
# Build options
CONFIG_BUILD_LOG=y
EOF
echo " Target: ${TARGET_NAME}/${SUBTARGET_NAME} Device: ${PROFILE}"
# Expand config
make defconfig V=s 2>/dev/null || true
echo ""
echo "[2/3] Building toolchain (if needed)..."
if [ ! -f "staging_dir/toolchain-*/bin/mips-openwrt-linux-gcc" ] && \
[ ! -f "staging_dir/toolchain-*/bin/mipsel-openwrt-linux-gcc" ] && \
[ ! -f "staging_dir/toolchain-*/bin/x86_64-openwrt-linux-gcc" ]; then
echo " Toolchain not found. Building... (this may take 30-120 min)"
make tools/install -j$J V=s 2>&1 | tail -5
make toolchain/install -j$J V=s 2>&1 | tail -5
echo " Toolchain built."
else
echo " Toolchain already built. Skipping."
fi
echo ""
echo "[3/3] Building utun package..."
make package/utun2/compile -j$J V=s 2>&1 | tail -10
# --- Show result ---
IPK=$(find bin/packages -name "utun_*.ipk" 2>/dev/null | head -1)
if [ -n "$IPK" ]; then
echo ""
echo "============================================"
echo " BUILD SUCCESS!"
echo " Package: $IPK"
echo " Size: $(du -h "$IPK" | cut -f1)"
echo "============================================"
else
echo ""
echo "ERROR: Package not found. Check build log above."
# Try to find any built binary
find bin -name "utun*" 2>/dev/null
exit 1
fi

11
opencode.json

@ -0,0 +1,11 @@
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["@isac322/kwin-mcp-opencode"],
"mcp": {
"kwin-mcp": {
"type": "local",
"command": ["/home/user/.local/bin/uvx", "kwin-mcp"],
"enabled": true
}
}
}

7
src/chat/chat_sync.c

@ -606,6 +606,7 @@ static void cm_invite_trampoline(void* arg) {
const uint8_t* ap = inv->addrs_data;
for (int i = 0; i < inv->addr_count; i++) {
uint8_t family = *ap++; ap++; /* skip sock_id */
uint8_t proto = *ap++; /* read proto */
if (family != 4) { ap += 18; continue; }
sqlite3_bind_int64(st, 1, (sqlite3_int64)node_id); sqlite3_bind_int(st, 2, 4);
sqlite3_bind_blob(st, 3, ap, 4, SQLITE_STATIC); ap += 4;
@ -632,13 +633,13 @@ static void cm_invite_trampoline(void* arg) {
ni->node_id = node_id; memcpy(ni->public_key, inv->pubkey, 32);
const uint8_t* ap = inv->addrs_data;
for (int i = 0; i < inv->addr_count; i++) {
uint8_t family = *ap++; ap++; /* skip sock_id */
uint8_t family = *ap++; uint8_t sock_id = *ap++; uint8_t proto = *ap++;
if (family != 4) { ap += 18; continue; }
struct TOPO_ADDR4* a4 = u_calloc(1, sizeof(*a4));
if (!a4) break;
memcpy(a4->addr, ap, 4); ap += 4;
a4->port = ((uint16_t)ap[0] << 8) | ap[1]; ap += 2;
a4->type = TOPO_ADDR_NAT; a4->protocol = TOPO_PROTO_UDP;
a4->type = TOPO_ADDR_NAT; a4->protocol = proto;
a4->next = ni->v4_addrs; ni->v4_addrs = a4;
}
@ -683,7 +684,7 @@ void chat_sync_connect_from_invite(uint64_t channel_id, uint64_t node_id,
inv->node_id = node_id;
memcpy(inv->pubkey, pubkey_bin, 32);
size_t addrs_sz = (size_t)addr_count * 8;
size_t addrs_sz = (size_t)addr_count * 9;
inv->addrs_data = u_malloc(addrs_sz);
if (!inv->addrs_data) { u_free(inv); return; }
memcpy(inv->addrs_data, addrs_data, addrs_sz);

2
src/routing_layer/conn_mgr.c

@ -1355,7 +1355,7 @@ int conn_mgr_connect_from_invite(struct CONN_MGR* mgr, struct TOPO_NODE* ni,
struct TOPO_ADDR4* a4 = memory_pool_alloc(groups->v4_addr_pool);
if (!a4) continue;
memset(a4, 0, sizeof(*a4)); memcpy(a4->addr, src->addr, 4); a4->port = src->port;
a4->type = TOPO_ADDR_NAT; a4->protocol = TOPO_PROTO_UDP; a4->socket_id = 0;
a4->type = TOPO_ADDR_NAT; a4->protocol = src->protocol; a4->socket_id = 0;
a4->next = group_ni->v4_addrs; group_ni->v4_addrs = a4;
}
if (!group_ni->v4_addrs) { u_free(group_ni); if (cb) cb(CONN_MGR_ERR_NO_ADDRESSES, node_id, cb_arg); return CONN_MGR_ERR_NO_ADDRESSES; }

15
tools/chatgui-android/app/src/main/java/com/utun/chat/ChatApplication.kt

@ -1,13 +1,8 @@
package com.utun.chat
import android.app.Application
import android.content.Intent
import com.utun.chat.data.LogManager
import com.utun.chat.data.ConfigProvider
import com.utun.chat.headless.HeadlessService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class ChatApplication : Application() {
lateinit var logManager: LogManager
@ -19,19 +14,15 @@ class ChatApplication : Application() {
logManager = LogManager
configProvider = ConfigProvider(this)
// First launch: generate keys + detect interfaces
val scope = CoroutineScope(Dispatchers.IO)
scope.launch {
if (configProvider.isFirstLaunch()) {
/* First launch config */
if (configProvider.isFirstLaunch()) {
kotlinx.coroutines.runBlocking {
LogManager.addLog("INFO", "APP", "First launch — configuring")
configProvider.generateKeys()
configProvider.detectInterfaces()
configProvider.markLaunched()
LogManager.addLog("INFO", "APP", "First launch done, ${configProvider.loadServers().size} interfaces detected")
}
// Start uTun service
val intent = Intent(this@ChatApplication, HeadlessService::class.java)
startService(intent)
}
}

6
tools/chatgui-android/app/src/main/java/com/utun/chat/MainActivity.kt

@ -5,12 +5,15 @@ import androidx.activity.ComponentActivity
import androidx.activity.compose.BackHandler
import androidx.activity.compose.setContent
import androidx.compose.runtime.*
import androidx.core.content.ContextCompat
import androidx.lifecycle.viewmodel.compose.viewModel
import com.utun.chat.data.ChatRepository
import com.utun.chat.data.InviteData
import com.utun.chat.data.NativeLib
import com.utun.chat.headless.HeadlessService
import com.utun.chat.ui.screens.*
import com.utun.chat.viewmodel.ChatViewModel
import android.content.Intent
import java.io.File
class MainActivity : ComponentActivity() {
@ -19,6 +22,9 @@ class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
/* Start uTun headless service (must be from foreground Activity on API 34+) */
ContextCompat.startForegroundService(this, Intent(this, HeadlessService::class.java))
val dbFile = File(filesDir, "chats.db")
val repo = ChatRepository(dbFile.absolutePath)

51
tools/chatgui-android/app/src/main/java/com/utun/chat/data/ChatRepository.kt

@ -3,7 +3,8 @@ package com.utun.chat.data
import android.database.sqlite.SQLiteDatabase
import org.json.JSONObject
data class Channel(val id: String, val name: String, val lastMsgAt: Long = 0, val lastMsg: String = "")
data class Channel(val id: String, val name: String, val lastMsgAt: Long = 0, val lastMsg: String = "",
val peersOnline: Int = 0)
data class Message(val id: Long, val author: String, val text: String, val ts: Long, val isOutgoing: Boolean = false)
class ChatRepository(dbPath: String) {
@ -11,12 +12,10 @@ class ChatRepository(dbPath: String) {
private var myNodeId: Long? = null
init {
/* Ensure DB file exists — wait up to 5 seconds for service to create it */
val f = java.io.File(dbPath)
var tries = 0
while (tries < 50 && !f.exists()) { Thread.sleep(100); tries++ }
db = SQLiteDatabase.openDatabase(dbPath, null, SQLiteDatabase.OPEN_READONLY)
/* Don't crash if tables aren't created yet */
try {
db.rawQuery("SELECT node_id FROM local_identity WHERE id=1", null).use { cur ->
if (cur.moveToFirst()) myNodeId = cur.getLong(0)
@ -31,18 +30,29 @@ class ChatRepository(dbPath: String) {
"SELECT channel_id, name, last_msg_at FROM channels ORDER BY last_msg_at DESC, name ASC",
null
).use { cur ->
while (cur.moveToNext()) {
val chId = cur.getString(0) ?: continue
val name = cur.getString(1) ?: chId
val lastAt = cur.getLong(2)
val last = getLastMsgText(chId)
list.add(Channel(id = chId, name = name, lastMsgAt = lastAt, lastMsg = last))
while (cur.moveToNext()) {
val chId = cur.getString(0) ?: continue
val name = cur.getString(1) ?: chId
val lastAt = cur.getLong(2)
val last = getLastMsgText(chId)
val online = getPeersOnline(chId)
list.add(Channel(id = chId, name = name, lastMsgAt = lastAt, lastMsg = last, peersOnline = online))
}
}
}
} catch (_: Exception) {}
return list
}
fun getPeersOnline(channelId: String): Int {
val tbl = peersTableName(channelId)
try {
db.rawQuery("SELECT COUNT(*) FROM \"$tbl\"", null).use { cur ->
if (cur.moveToFirst()) return cur.getInt(0)
}
} catch (_: Exception) {}
return 0
}
fun getMessages(channelId: String, limit: Int = 50): List<Message> {
val tbl = msgTableName(channelId)
val list = mutableListOf<Message>()
@ -63,17 +73,14 @@ class ChatRepository(dbPath: String) {
isOutgoing = mid != null && nodeId == mid))
}
}
} catch (_: Exception) { /* table may not exist yet */ }
} catch (_: Exception) {}
return list
}
private fun getLastMsgText(channelId: String): String {
val tbl = msgTableName(channelId)
try {
db.rawQuery(
"SELECT node_id, data FROM \"$tbl\" ORDER BY id DESC LIMIT 1",
null
).use { cur ->
db.rawQuery("SELECT node_id, data FROM \"$tbl\" ORDER BY id DESC LIMIT 1", null).use { cur ->
if (cur.moveToFirst()) {
val author = cur.getLong(0).toHex()
val text = parseMsgText(cur.getBlob(1))
@ -89,16 +96,20 @@ class ChatRepository(dbPath: String) {
fun connectNode(address: String, port: Int, pubkeyHex: String) = NativeLib.connectNode(address, port, pubkeyHex)
fun joinChannel(channelId: Long, nodeId: Long, pubkey: ByteArray, addrs: ByteArray, addrCount: Int) =
NativeLib.joinChannel(channelId, nodeId, pubkey, addrs, addrCount)
fun connectChannel(channelId: String) = NativeLib.connectChannel(channelId)
fun isReady() = NativeLib.isReady()
companion object {
fun msgTableName(chId: String): String {
val san = StringBuilder()
fun msgTableName(chId: String): String = "msg_" + sanitize(chId)
fun peersTableName(chId: String): String = "peers_" + sanitize(chId)
private fun sanitize(chId: String): String {
val sb = StringBuilder()
for (c in chId) {
if (c in 'a'..'z' || c in 'A'..'Z' || c in '0'..'9' || c == '_') san.append(c)
else san.append('_')
if (c in 'a'..'z' || c in 'A'..'Z' || c in '0'..'9' || c == '_') sb.append(c)
else sb.append('_')
}
return "msg_$san"
return sb.toString()
}
fun parseMsgText(data: ByteArray?): String {

2
tools/chatgui-android/app/src/main/java/com/utun/chat/data/NativeLib.kt

@ -65,6 +65,7 @@ object NativeLib {
fun joinChannel(channelId: Long, nodeId: Long, pubkey: ByteArray, addrs: ByteArray, addrCount: Int) {
nativeJoinChannel(channelId, nodeId, pubkey, addrs, addrCount)
}
fun connectChannel(channelId: String) { nativeConnectChannel(channelId) }
fun getChannels(): String = nativeGetChannels() ?: "[]"
fun getMessages(channelId: String, limit: Int = 50): String = nativeGetMessages(channelId, limit) ?: "[]"
fun setDebugLevel(category: String, level: String) { nativeSetDebugLevel(category, level) }
@ -81,6 +82,7 @@ object NativeLib {
private external fun nativeCreateChannel(name: String, channelId: String)
private external fun nativeConnectNode(address: String, port: Int, pubkeyHex: String)
private external fun nativeJoinChannel(channelId: Long, nodeId: Long, pubkey: ByteArray, addrs: ByteArray, addrCount: Int)
private external fun nativeConnectChannel(channelId: String)
private external fun nativeGetChannels(): String?
private external fun nativeGetMessages(channelId: String, limit: Int): String?
private external fun nativeSetDebugLevel(category: String, level: String)

7
tools/chatgui-android/app/src/main/java/com/utun/chat/headless/HeadlessService.kt

@ -40,6 +40,7 @@ class HeadlessService : Service() {
val listenPort = provider.getInt("node.listen_port")
val configText = buildString {
appendLine("[global]")
if (nodeName.isNotBlank()) appendLine("my_node_name=$nodeName")
if (pubKey.length == 64) appendLine("my_public_key=$pubKey")
if (privKey.length == 64) appendLine("my_private_key=$privKey")
@ -64,12 +65,6 @@ class HeadlessService : Service() {
/* Init callbacks first (log + config) — required before start */
NativeLib.init(dbDir, 9999)
NativeLib.setEventCallback(object : com.utun.chat.data.ChatEventCallback {
override fun onEvent(type: Int, data: ByteArray?) {
LogManager.addLog("DEBUG", "EVENT", "type=$type len=${data?.size ?: 0}")
}
})
val ok = NativeLib.start(configText)
if (ok) {
LogManager.addLog("INFO", "SRV", "uTun core started")

15
tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/MessageBubble.kt

@ -26,18 +26,18 @@ fun MessageBubble(message: Message) {
) {
Column(
modifier = Modifier
.widthIn(max = 280.dp)
.widthIn(max = 300.dp)
.clip(RoundedCornerShape(12.dp))
.background(bgColor)
.padding(10.dp)
.padding(12.dp)
) {
Text(message.author, fontSize = 12.sp, fontWeight = FontWeight.Bold, color = textColor.copy(alpha = 0.7f))
Text(message.author, fontSize = 13.sp, fontWeight = FontWeight.Bold, color = textColor.copy(alpha = 0.7f))
Spacer(Modifier.height(2.dp))
Text(message.text, fontSize = 15.sp, color = textColor)
Text(message.text, fontSize = 17.sp, color = textColor)
Spacer(Modifier.height(2.dp))
Text(
formatTime(message.ts),
fontSize = 10.sp,
fontSize = 11.sp,
color = textColor.copy(alpha = 0.5f),
modifier = Modifier.align(Alignment.End)
)
@ -50,10 +50,11 @@ fun InputBar(
text: String,
onTextChange: (String) -> Unit,
onSend: () -> Unit,
enabled: Boolean = true
enabled: Boolean = true,
modifier: Modifier = Modifier
) {
Row(
modifier = Modifier.fillMaxWidth().padding(8.dp),
modifier = modifier.fillMaxWidth().padding(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
OutlinedTextField(

14
tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/ChannelListScreen.kt

@ -36,8 +36,8 @@ fun ChannelListScreen(
title = { Text("uTun Chat") },
actions = {
Text(
if (connected) "ON" else "OFF",
color = if (connected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.error,
"ON",
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(end = 8.dp)
)
IconButton(onClick = onJoinChannel) { Icon(Icons.Default.QrCode, "Join") }
@ -73,7 +73,15 @@ fun ChannelListScreen(
}
Spacer(Modifier.width(12.dp))
Column {
Text(ch.name, fontWeight = FontWeight.Bold, fontSize = 16.sp)
Row {
Text(ch.name, fontWeight = FontWeight.Bold, fontSize = 16.sp)
if (ch.peersOnline > 0) {
Text(" (${ch.peersOnline})",
fontSize = 13.sp,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(start = 4.dp))
}
}
if (ch.lastMsg.isNotEmpty())
Text(ch.lastMsg, fontSize = 13.sp, color = MaterialTheme.colorScheme.onSurfaceVariant, maxLines = 1)
}

17
tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/ChatScreen.kt

@ -3,6 +3,7 @@ package com.utun.chat.ui.screens
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material3.*
@ -24,6 +25,11 @@ fun ChatScreen(
onBack: () -> Unit
) {
var text by remember { mutableStateOf("") }
val listState = rememberLazyListState()
LaunchedEffect(messages.size) {
if (messages.isNotEmpty()) listState.animateScrollToItem(messages.size - 1)
}
Scaffold(
topBar = {
@ -42,15 +48,18 @@ fun ChatScreen(
onSendMessage(text)
text = ""
},
enabled = connected
enabled = true,
modifier = Modifier.navigationBarsPadding()
)
}
) { padding ->
LazyColumn(
modifier = Modifier.fillMaxSize().padding(padding),
reverseLayout = false
modifier = Modifier.fillMaxSize().padding(padding).imePadding(),
state = listState
) {
items(messages) { msg -> MessageBubble(msg) }
items(messages.size) { idx ->
MessageBubble(messages[idx])
}
item { Spacer(Modifier.height(8.dp)) }
}
}

2
tools/chatgui-android/app/src/main/java/com/utun/chat/viewmodel/ChatViewModel.kt

@ -35,6 +35,7 @@ class ChatViewModel(private val repo: ChatRepository) : ViewModel() {
}
}
}
8 -> refreshChannels() /* CHANNEL_PEERS_ONLINE */
2 -> { /* CONNECT_RESULT: [node_id:8][result:4][channel_id:8] */
if (data != null && data.size >= 20) {
val buf = java.nio.ByteBuffer.wrap(data).order(java.nio.ByteOrder.LITTLE_ENDIAN)
@ -68,6 +69,7 @@ class ChatViewModel(private val repo: ChatRepository) : ViewModel() {
fun selectChannel(channel: Channel) {
_currentChannel.value = channel
repo.connectChannel(channel.id)
refreshMessages(channel.id)
}

27
tools/chatgui-android/jni_bridge/android_jni_bridge.c

@ -11,6 +11,8 @@
#include "../libutun_lite/invite_link_c.h"
#include "../libutun_lite/instance_lite.h"
#include "../../../lib/debug_config.h"
#include "../../../lib/u_async.h"
#include "../../../lib/mem.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@ -173,6 +175,7 @@ void utun_bridge_connect_node(const char* address, int port, const char* pubkey_
/* Real implementation: calls chat_sync_connect_from_invite() */
#include "chat/chat_sync.h"
#include "chat/chat_core.h"
void utun_bridge_join_channel(uint64_t channel_id, uint64_t node_id,
const uint8_t* pubkey_bin,
@ -187,6 +190,22 @@ void utun_bridge_join_channel(uint64_t channel_id, uint64_t node_id,
chat_sync_connect_from_invite(channel_id, node_id, pubkey_bin, addrs_data, addr_count);
}
void utun_bridge_connect_channel(const char* channel_id) {
if (!channel_id || !channel_id[0]) {
bridge_log(BLEV_ERROR, "connect_channel: null ch_id");
return;
}
struct UASYNC* ua = instance_lite_get_uasync();
if (!ua) {
bridge_log(BLEV_ERROR, "connect_channel: ua not ready, ch=%s", channel_id);
return;
}
char* cid = u_strdup(channel_id);
if (!cid) return;
bridge_log(BLEV_INFO, "connect_channel ch=%s", channel_id);
uasync_post(ua, chat_core_connect_channel_trampoline, cid);
}
char* utun_bridge_get_channels_json(void) {
return strdup("[]");
}
@ -451,6 +470,14 @@ JNIEXPORT jstring JNICALL Java_com_utun_chat_data_NativeLib_nativeGetMessages(
return result;
}
JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeConnectChannel(
JNIEnv* env, jobject thiz, jstring channelId) {
(void)thiz;
const char* ch = (*env)->GetStringUTFChars(env, channelId, NULL);
utun_bridge_connect_channel(ch);
if (ch) (*env)->ReleaseStringUTFChars(env, channelId, ch);
}
JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeSetDebugLevel(
JNIEnv* env, jobject thiz, jstring category, jstring level) {
(void)thiz;

3
tools/chatgui-android/jni_bridge/android_jni_bridge.h

@ -56,6 +56,9 @@ void utun_bridge_join_channel(uint64_t channel_id, uint64_t node_id,
const uint8_t* pubkey_bin,
const uint8_t* addrs_data, int addr_count);
/* Connect to all peers of a channel (auto-connect on select) */
void utun_bridge_connect_channel(const char* channel_id);
/* ── Queries ── */
char* utun_bridge_get_channels_json(void);

7
tools/chatgui-android/libutun_lite/invite_link_c.c

@ -105,13 +105,14 @@ int invite_link_decode(const char* link, size_t link_len, struct InviteDataC* ou
for (int j = 0; j < cnt; j++) {
int is_v6 = (header >> (2 + j)) & 1;
int ip_len = is_v6 ? 16 : 4;
if (off + 1 + ip_len + 2 > raw_len) {
if (off + 2 + ip_len + 2 > raw_len) {
if (error_buf && error_buf_size) snprintf(error_buf, error_buf_size, "truncated at addr %d", j);
return -1;
}
if (out->addrCount >= INVITE_ADDR_MAX) break;
struct InviteAddrC* a = &out->addrs[out->addrCount++];
a->socketId = raw[off++];
a->proto = raw[off++];
a->family = is_v6 ? 6 : 4;
memcpy(a->address, raw + off, (size_t)ip_len);
off += ip_len;
@ -126,7 +127,7 @@ int invite_serialize_addrs(const struct InviteDataC* data, uint8_t* buf, size_t
if (!data || !buf) return -1;
size_t needed = 0;
for (int i = 0; i < (int)data->addrCount; i++)
needed += 8U; /* family(1) + sockId(1) + ip4(4) + port(2) = 8; IPv6 skipped */
needed += 9U; /* family(1) + sockId(1) + proto(1) + ip4(4) + port(2) = 9; IPv6 skipped */
if (needed > buf_size) return -1;
size_t pos = 0;
for (int i = 0; i < (int)data->addrCount; i++) {
@ -134,6 +135,7 @@ int invite_serialize_addrs(const struct InviteDataC* data, uint8_t* buf, size_t
if (a->family != 4) continue;
buf[pos++] = (uint8_t)a->family;
buf[pos++] = a->socketId;
buf[pos++] = a->proto;
memcpy(buf + pos, a->address, 4); pos += 4;
buf[pos++] = (uint8_t)(a->port >> 8);
buf[pos++] = (uint8_t)(a->port & 0xFF);
@ -165,6 +167,7 @@ int invite_link_encode(const struct InviteDataC* data, char* out, size_t out_siz
const struct InviteAddrC* a = &data->addrs[ia + j];
int ip_len = (a->family == 6) ? 16 : 4;
raw[pos++] = a->socketId;
raw[pos++] = a->proto;
memcpy(raw + pos, a->address, (size_t)ip_len); pos += (size_t)ip_len;
raw[pos++] = (uint8_t)(a->port >> 8);
raw[pos++] = (uint8_t)(a->port & 0xFF);

7
tools/chatgui-android/libutun_lite/invite_link_c.h

@ -8,6 +8,10 @@
#define INVITE_PUBKEY_SIZE 32
#define INVITE_ADDR_MAX 32
#define INVITE_PROTO_UDP 0x01
#define INVITE_PROTO_TCP 0x02
#define INVITE_PROTO_BOTH 0x03
#ifdef __cplusplus
extern "C" {
#endif
@ -16,6 +20,7 @@ struct InviteAddrC {
int family;
uint8_t address[16];
uint8_t socketId;
uint8_t proto;
uint16_t port;
};
@ -38,7 +43,7 @@ int invite_link_decode(const char* link, size_t link_len, struct InviteDataC* ou
int invite_link_encode(const struct InviteDataC* data, char* out, size_t out_size);
/* serialize per-addr format for chat_sync_connect_from_invite():
family(1) + socketId(1) + address(4|16) + port(2 BE)
family(1) + socketId(1) + proto(1) + address(4|16) + port(2 BE)
returns total bytes written, or -1 if buf too small */
int invite_serialize_addrs(const struct InviteDataC* data, uint8_t* buf, size_t buf_size);

6
tools/chatgui/src/invite_link.cpp

@ -14,7 +14,7 @@ QString encodeInviteLink(uint64_t channelId, const QByteArray& pubkey,
}
QByteArray blob;
blob.reserve(1 + 8 + addrs.size() * (1 + 32 + 19));
blob.reserve(1 + 8 + addrs.size() * (1 + 32 + 20));
blob.append(static_cast<char>(INVITE_LINK_VERSION));
@ -38,6 +38,7 @@ QString encodeInviteLink(uint64_t channelId, const QByteArray& pubkey,
for (int j = 0; j < cnt; j++) {
blob.append(static_cast<char>(addrs[i + j].socketId));
blob.append(static_cast<char>(addrs[i + j].protocol));
blob.append(addrs[i + j].address);
quint16 p = addrs[i + j].port;
blob.append(static_cast<char>((p >> 8) & 0xFF));
@ -112,13 +113,14 @@ InviteData decodeInviteLink(const QString& link) {
for (int j = 0; j < cnt; j++) {
bool isV6 = header & (1 << (2 + j));
int ipLen = isV6 ? 16 : 4;
if (off + 1 + ipLen + 2 > len) {
if (off + 2 + ipLen + 2 > len) {
d.error = QStringLiteral("truncated at addr %1 of block").arg(j);
GUI_ERROR("decodeInviteLink: truncated at addr %d of block", j);
return d;
}
InviteAddr a;
a.socketId = data[off++];
a.proto = data[off++];
a.family = isV6 ? 6 : 4;
a.address = QByteArray(reinterpret_cast<const char*>(data + off), ipLen);
off += ipLen;

5
tools/chatgui/src/invite_link.h

@ -8,11 +8,16 @@
#define INVITE_LINK_VERSION 0x01
#define INVITE_PROTO_UDP 0x01
#define INVITE_PROTO_TCP 0x02
#define INVITE_PROTO_BOTH 0x03
struct InviteAddr {
int family; // 4=IPv4, 6=IPv6
QByteArray address;
quint16 port;
uint8_t socketId; // socket_id из ETCP
uint8_t proto; // INVITE_PROTO_UDP/TCP/BOTH
};
struct InviteData {

1
tools/chatgui/src/joindialog.cpp

@ -179,6 +179,7 @@ void JoinDialog::onConnectClicked() {
for (const auto& a : d.addrs) {
addrsBuf.append(static_cast<char>(a.family));
addrsBuf.append(static_cast<char>(a.socketId));
addrsBuf.append(static_cast<char>(a.proto));
addrsBuf.append(a.address);
addrsBuf.append(static_cast<char>((a.port >> 8) & 0xFF));
addrsBuf.append(static_cast<char>(a.port & 0xFF));

3
tools/chatgui/transport/utun_node.cpp

@ -6,6 +6,7 @@
#include "../../lib/socket_compat.h"
#include "../db/db_manager.h"
#include "../src/invite_link.h"
// === C headers ===
extern "C" {
@ -157,6 +158,7 @@ QList<NodeAddr> UtunNode::getInviteAddresses(DbManager* db) {
if (link->link_state == 3 && link->nat_type == NAT_TYPE_DIRECT) {
NodeAddr a;
a.socketId = link->conn ? (int)link->conn->sock_id : 0;
a.protocol = INVITE_PROTO_UDP;
struct sockaddr_storage* sa = &link->remote_addr;
if (sa->ss_family == AF_INET) {
struct sockaddr_in* sin = (struct sockaddr_in*)sa;
@ -190,6 +192,7 @@ QList<NodeAddr> UtunNode::getInviteAddresses(DbManager* db) {
NodeAddr a;
a.socketId = (int)sock->sock_id;
a.protocol = INVITE_PROTO_UDP;
if (sa->ss_family == AF_INET) {
struct sockaddr_in* sin = (struct sockaddr_in*)sa;
a.family = 4;

4
utun.sh

@ -0,0 +1,4 @@
#!/bin/sh
ip route add 85.192.42.96/32 via 192.168.40.1 dev wlo1
ip route delete default
ip route add default via 10.99.0.1 dev tun_tcp
Loading…
Cancel
Save