Add explicit server and channel link kinds to shared codec

This commit is contained in:
evgeny committed 2026-10-28 10:55:25 +01:00
1 parent b773656dad
commit bd50140d26
3 files changed
+65 -28

No files matched your search

+35 -17
View File
@@ -6,7 +6,7 @@
#include <stdio.h>
#include <string.h>
#define INVITE_RAW_MAX 1157u
#define INVITE_RAW_MAX 1158u
#define INVITE_PREFIX "utun://"
static int reject(char* error, size_t size, const char* reason) {
@@ -21,8 +21,15 @@ static int nonzero(const uint8_t* data, size_t len) {
}
int invite_link_validate(const struct InviteData* d) {
if (!d || !d->channelId || d->channelId > INT64_MAX || !d->join_key || !nonzero(d->pubkey, 32) || !sc_derive_node_id_from_pubkey(d->pubkey))
return reject(NULL, 0, "missing channel, join key or node identity");
if (!d || !nonzero(d->pubkey, 32) || !sc_derive_node_id_from_pubkey(d->pubkey))
return reject(NULL, 0, "missing node identity");
if (d->kind == INVITE_KIND_CHANNEL) {
if (!d->channelId || d->channelId > INT64_MAX || !d->join_key || nonzero(d->add_key, sizeof(d->add_key)))
return reject(NULL, 0, "invalid channel invitation credentials");
} else if (d->kind == INVITE_KIND_SERVER) {
if (d->channelId || d->join_key || d->password_len || d->password[0] || !nonzero(d->add_key, sizeof(d->add_key)))
return reject(NULL, 0, "invalid server addition credentials");
} else return reject(NULL, 0, "unknown link kind");
if (!d->addrCount || d->addrCount > INVITE_ADDR_MAX) return reject(NULL, 0, "invalid address count");
if (d->reality.has > 1) return reject(NULL, 0, "invalid REALITY marker");
if (d->reality.has && (!memchr(d->reality.server_name, 0, sizeof(d->reality.server_name)) ||
@@ -65,14 +72,21 @@ int invite_link_decode(const char* link, size_t len, struct InviteData* out, cha
#define NEED(n) do { if ((size_t)(n) > count - pos) return reject(error, error_size, "truncated invitation"); } while (0)
NEED(2);
if (raw[pos++] != INVITE_LINK_VERSION) return reject(error, error_size, "unsupported invitation version");
value.password_len = raw[pos++];
if (value.password_len >= INVITE_PASS_MAX) return reject(error, error_size, "password too long");
NEED(value.password_len);
if (memchr(raw + pos, 0, value.password_len)) return reject(error, error_size, "password contains NUL");
memcpy(value.password, raw + pos, value.password_len); pos += value.password_len;
NEED(17);
for (unsigned i = 0; i < 8; i++) value.channelId = (value.channelId << 8) | raw[pos++];
for (unsigned i = 0; i < 8; i++) value.join_key = (value.join_key << 8) | raw[pos++];
value.kind = raw[pos++];
if (value.kind == INVITE_KIND_CHANNEL) {
NEED(1); value.password_len = raw[pos++];
if (value.password_len >= INVITE_PASS_MAX) return reject(error, error_size, "password too long");
NEED(value.password_len);
if (memchr(raw + pos, 0, value.password_len)) return reject(error, error_size, "password contains NUL");
memcpy(value.password, raw + pos, value.password_len); pos += value.password_len;
NEED(16);
for (unsigned i = 0; i < 8; i++) value.channelId = (value.channelId << 8) | raw[pos++];
for (unsigned i = 0; i < 8; i++) value.join_key = (value.join_key << 8) | raw[pos++];
} else if (value.kind == INVITE_KIND_SERVER) {
NEED(INVITE_ADD_KEY_SIZE);
memcpy(value.add_key, raw + pos, sizeof(value.add_key)); pos += sizeof(value.add_key);
} else return reject(error, error_size, "unknown link kind");
NEED(1);
value.reality.has = raw[pos++];
if (value.reality.has > 1) return reject(error, error_size, "invalid REALITY marker");
if (value.reality.has) {
@@ -110,8 +124,8 @@ int invite_link_decode(const char* link, size_t len, struct InviteData* out, cha
value.nodeId = sc_derive_node_id_from_pubkey(value.pubkey);
*out = value;
if (error && error_size) error[0] = 0;
DEBUG_DEBUG(DEBUG_CATEGORY_CHAT, "invite decoded channel=%llu node=%016llx addresses=%u",
(unsigned long long)value.channelId, (unsigned long long)value.nodeId, value.addrCount);
DEBUG_DEBUG(DEBUG_CATEGORY_CHAT, "link decoded kind=%u channel=%llu node=%016llx addresses=%u",
value.kind, (unsigned long long)value.channelId, (unsigned long long)value.nodeId, value.addrCount);
return 0;
}
@@ -143,11 +157,15 @@ int invite_link_encode(const struct InviteData* data, const char* password, char
if (invite_link_validate(data)) return -1;
size_t pass_len = password ? strnlen(password, INVITE_PASS_MAX) : 0;
if (pass_len >= INVITE_PASS_MAX) return reject(NULL, 0, "password too long");
if (data->kind == INVITE_KIND_SERVER && pass_len) return reject(NULL, 0, "server link cannot contain channel password");
uint8_t raw[INVITE_RAW_MAX]; size_t pos = 0;
raw[pos++] = INVITE_LINK_VERSION; raw[pos++] = (uint8_t)pass_len;
if (pass_len) { memcpy(raw + pos, password, pass_len); pos += pass_len; }
for (int i = 7; i >= 0; i--) raw[pos++] = (uint8_t)(data->channelId >> (8 * i));
for (int i = 7; i >= 0; i--) raw[pos++] = (uint8_t)(data->join_key >> (8 * i));
raw[pos++] = INVITE_LINK_VERSION; raw[pos++] = data->kind;
if (data->kind == INVITE_KIND_CHANNEL) {
raw[pos++] = (uint8_t)pass_len;
if (pass_len) { memcpy(raw + pos, password, pass_len); pos += pass_len; }
for (int i = 7; i >= 0; i--) raw[pos++] = (uint8_t)(data->channelId >> (8 * i));
for (int i = 7; i >= 0; i--) raw[pos++] = (uint8_t)(data->join_key >> (8 * i));
} else { memcpy(raw + pos, data->add_key, sizeof(data->add_key)); pos += sizeof(data->add_key); }
uint8_t addresses[INVITE_RAW_MAX];
int address_len = invite_serialize_addrs(data, addresses, sizeof(addresses));
if (address_len < 0) return -1;
+9 -4
View File
@@ -1,9 +1,9 @@
/*
* invite_link.h — invite-ссылки utun:// для каналов
*
* encode/decode используют только версию 0x03. Проверка общая для всех клиентов.
* Формат 0x03: utun:// + обычный base64 с padding(version | pass_len/password | channel_id | join_key |
* reality_has/params | блоки pubkey/addrs). ID/port кодируются big-endian; nodeId выводится из pubkey.
* Версия 0x04: utun:// + base64(version | kind | реквизиты | reality | блоки pubkey/addrs).
* CHANNEL: pass_len/password | channel_id | join_key; SERVER: add_key[32], без полей канала.
* Проверка общая для всех клиентов. ID/port — big-endian; nodeId выводится из pubkey.
* Сам кодек не подключает узел, не регистрирует join_key и не проверяет подпись/членство.
* decode: 0/-1; encode: длина base64 без utun:// и NUL/-1; serialize_addrs: число байтов/-1.
* Буферы предоставляет вызывающий; encode завершает строку NUL.
@@ -14,7 +14,10 @@
#include <stdint.h>
#include <stddef.h>
#define INVITE_LINK_VERSION 0x03
#define INVITE_LINK_VERSION 0x04
#define INVITE_KIND_CHANNEL 0
#define INVITE_KIND_SERVER 1
#define INVITE_ADD_KEY_SIZE 32
#define INVITE_LINK_MAX 1552 /* максимальная ссылка с NUL: 32 IPv6, REALITY и пароль */
#define INVITE_PUBKEY_SIZE 32
#define INVITE_ADDR_MAX 32
@@ -47,6 +50,8 @@ struct InviteReality {
};
struct InviteData {
uint8_t kind; /* INVITE_KIND_*; SERVER не разрешает обычный JOIN */
uint8_t add_key[INVITE_ADD_KEY_SIZE]; /* постоянный секрет добавления сервера; у CHANNEL нули */
uint64_t channelId; /* числовой ID канала */
uint64_t join_key; /* 64-битный ключ входа (генерируется инвайтером, хранится на connection-узле 10 мин) */
uint8_t pubkey[INVITE_PUBKEY_SIZE]; /* X25519 узла подключения */
+21 -7
View File
@@ -80,14 +80,14 @@ int main(void) {
raw_len -= link[strlen(link) - 1] == '='; raw_len -= link[strlen(link) - 2] == '=';
uint8_t saved[1200]; memcpy(saved, raw, (size_t)raw_len);
raw[0] = 2; rejected_raw(raw, (size_t)raw_len); memcpy(raw, saved, (size_t)raw_len);
raw[18] = 2; rejected_raw(raw, (size_t)raw_len); memcpy(raw, saved, (size_t)raw_len);
raw[62] = 64; rejected_raw(raw, (size_t)raw_len); memcpy(raw, saved, (size_t)raw_len);
raw[63] = 0; rejected_raw(raw, (size_t)raw_len); memcpy(raw, saved, (size_t)raw_len);
raw[126] |= 0x80; rejected_raw(raw, (size_t)raw_len); memcpy(raw, saved, (size_t)raw_len);
size_t second = 126 + 33 + 4 * 20; raw[second + 1] ^= 1;
raw[19] = 2; rejected_raw(raw, (size_t)raw_len); memcpy(raw, saved, (size_t)raw_len);
raw[63] = 64; rejected_raw(raw, (size_t)raw_len); memcpy(raw, saved, (size_t)raw_len);
raw[64] = 0; rejected_raw(raw, (size_t)raw_len); memcpy(raw, saved, (size_t)raw_len);
raw[127] |= 0x80; rejected_raw(raw, (size_t)raw_len); memcpy(raw, saved, (size_t)raw_len);
size_t second = 127 + 33 + 4 * 20; raw[second + 1] ^= 1;
rejected_raw(raw, (size_t)raw_len); memcpy(raw, saved, (size_t)raw_len);
rejected_raw(raw, 19); /* обязательный узел отсутствует */
memcpy(raw, saved, (size_t)raw_len); raw[1] = 128; rejected_raw(raw, (size_t)raw_len);
rejected_raw(raw, 20); /* обязательный узел отсутствует */
memcpy(raw, saved, (size_t)raw_len); raw[2] = 128; rejected_raw(raw, (size_t)raw_len);
struct InviteData bad = input; bad.addrCount = 0; CHECK(invite_link_validate(&bad) < 0);
bad = input; bad.channelId = UINT64_MAX; CHECK(invite_link_validate(&bad) < 0);
bad = input; bad.addrCount = 33; CHECK(invite_link_validate(&bad) < 0);
@@ -96,6 +96,20 @@ int main(void) {
bad = input; bad.addrs[0].port = 0; CHECK(invite_link_validate(&bad) < 0);
bad = input; bad.addrs[0].proto = 3; CHECK(invite_link_validate(&bad) < 0);
bad = input; bad.reality.has = 0; CHECK(invite_link_validate(&bad) < 0);
struct InviteData server = input, decoded_server;
server.kind = INVITE_KIND_SERVER; server.channelId = 0; server.join_key = 0;
memset(server.add_key, 0x57, sizeof(server.add_key));
CHECK(invite_link_encode(&server, NULL, link, sizeof(link)) > 0);
CHECK(!invite_link_decode(link, strlen(link), &decoded_server, NULL, 0));
CHECK(decoded_server.kind == INVITE_KIND_SERVER && !decoded_server.channelId && !decoded_server.join_key);
CHECK(!memcmp(server.add_key, decoded_server.add_key, sizeof(server.add_key)) && decoded_server.addrCount == server.addrCount);
for (size_t cut = 0; cut < 34; cut++) {
uint8_t server_raw[34] = {INVITE_LINK_VERSION, INVITE_KIND_SERVER};
rejected_raw(server_raw, cut);
}
server.channelId = 1; CHECK(invite_link_validate(&server) < 0); server.channelId = 0;
memset(server.add_key, 0, sizeof(server.add_key)); CHECK(invite_link_validate(&server) < 0);
bad = input; bad.kind = 2; CHECK(invite_link_validate(&bad) < 0);
rejected("utun://", 7); rejected(NULL, 1);
printf("invite codec: %u contract cases PASS\n", cases); return 0;
}