333 lines
23 KiB
C
333 lines
23 KiB
C
/**
|
|
* @file topo_node.h
|
|
* @brief Модель данных узла сети — структуры, сериализация, wire-формат.
|
|
*
|
|
* TOPO_NODE — общие ключи, имя и адреса; реестр хранит указатель с подсчётом ссылок.
|
|
* Подписанная запись заменяется целиком при более свежем timestamp, вместе с подписью.
|
|
* TOPO_GROUP_NODE — пути, подсети и состояние узла в конкретной группе.
|
|
* TOPOMSG_NODE — сетевой формат. Модуль сериализует, подписывает и проверяет записи.
|
|
* Работа с реестром и группами выполняется в потоке uasync экземпляра.
|
|
*/
|
|
#ifndef TOPO_NODE_H
|
|
#define TOPO_NODE_H
|
|
|
|
#ifdef __cplusplus
|
|
extern "C" {
|
|
#endif
|
|
|
|
#include <stdint.h>
|
|
#include <stddef.h>
|
|
#include "../lib/ll_queue.h"
|
|
#include "secure_channel.h"
|
|
#include "address_profile.h"
|
|
|
|
struct TOPO_GROUP;
|
|
struct TOPO_GROUPS;
|
|
struct ETCP_SOCKET;
|
|
struct ETCP_LINK;
|
|
struct NODE_CONN_DIRECT;
|
|
struct ETCP_CONN;
|
|
struct UTUN_INSTANCE;
|
|
struct reality_client_config;
|
|
|
|
#define TOPO_ADDR_INTERFACE 0
|
|
#define TOPO_ADDR_NAT 1
|
|
#define TOPO_ADDR_REAL 2
|
|
|
|
#define TOPO_PROTO_UDP 0x01
|
|
#define TOPO_PROTO_TCP 0x02
|
|
#define TOPO_PROTO_REALITY 0x04 /* флаг reality-камуфляжа поверх TCP (reality-TCP = 0x06) */
|
|
|
|
#define ADDR_TYPE_NETIF 0
|
|
#define ADDR_TYPE_DIRECT 1
|
|
#define ADDR_TYPE_NAT_EIM 2
|
|
#define ADDR_TYPE_NAT_STRICT 3
|
|
#define ADDR_TYPE_NAT 4 /* за NAT, тип неизвестен */
|
|
#define ADDR_TYPE_CONTACT 5 /* адрес не изменён, вход требует предварительного контакта */
|
|
#define ADDR_TYPE_NAT_EIM_UNVERIFIED 6 /* EIM наблюдается, незнакомый вход не проверен: EIM? */
|
|
|
|
#define PROBE_STATUS_NONE 0
|
|
#define PROBE_STATUS_IN_PROGRESS 1
|
|
#define PROBE_STATUS_DONE 2
|
|
#define PROBE_STATUS_DEFERRED 3 /* отложена до следующего burst (standby_wait) */
|
|
|
|
#define PROBE_RESULT_UNKNOWN 0
|
|
#define PROBE_RESULT_REACHABLE 1
|
|
#define PROBE_RESULT_UNREACHABLE 2
|
|
|
|
#define CONN_PROBE_COUNT 3
|
|
#define CONN_PROBE_TIMEOUT_MS 1000
|
|
|
|
#define CONN_TYPE_NONE 0
|
|
#define CONN_TYPE_DIRECT 1
|
|
#define CONN_TYPE_REVERSE 2
|
|
#define CONN_TYPE_INDIRECT 3
|
|
|
|
#define CLIENT_TYPE_SERVER 0
|
|
#define CLIENT_TYPE_DESKTOP 1
|
|
#define CLIENT_TYPE_MOBILE 2
|
|
|
|
#define CLIENT_ACTIVITY_STANDBY 0
|
|
#define CLIENT_ACTIVITY_ACTIVE 1
|
|
#define CONN_MGR_MAX_INTERMEDIARIES 3
|
|
|
|
#define NCONN_DIRECT (1 << 0) // прямое ETCP подключение
|
|
#define NCONN_INDIRECT (1 << 1) // через посредника
|
|
#define NCONN_BGP (1 << 2) // узел виден в топологии (BGP)
|
|
|
|
#define TOPO_FLAG_SEND_SUBNETS 0x01
|
|
#define TOPO_FLAG_RADIO 0x02
|
|
#define TOPO_FLAG_PHONE_KNOWN 0x04 /* состояние телефона опубликовано */
|
|
#define TOPO_FLAG_PHONE_BUSY 0x08 /* занят телефонным либо обычным uTun-вызовом */
|
|
#define TOPO_GROUP_UTUN 0x8000000000000000ULL
|
|
#define TOPO_GROUP_TYPE_UTUN 1
|
|
#define TOPO_GROUP_TYPE_CHAT 2
|
|
|
|
/* Результаты проб адресов. *_time — timebase (0.1 ms), *_rtt — те же единицы;
|
|
* *_status — PROBE_RESULT_*, probe_status — PROBE_STATUS_*. */
|
|
struct TOPO_CONNECTIVITY {
|
|
uint8_t probe_status;
|
|
uint8_t pending_count; // незавершённые пробы
|
|
uint64_t probe_start_time;
|
|
uint16_t interface_min_rtt;
|
|
uint16_t nat_min_rtt;
|
|
uint16_t real_min_rtt;
|
|
uint8_t interface_status;
|
|
uint8_t nat_status;
|
|
uint8_t real_status;
|
|
uint8_t reserved;
|
|
uint64_t interface_probe_time;
|
|
uint64_t nat_probe_time;
|
|
uint64_t real_probe_time;
|
|
uint64_t ping_req_time;
|
|
uint64_t last_ping_time;
|
|
void* probe_list; // принадлежащие узлу контексты текущих проб
|
|
void* probe_deferred_wait; /* контекст отложенной пробы (standby_wait, Android) */
|
|
void* nat_probe_wait; /* контекст таймера: NAT сохраняет холодный ingress перед фоновыми ping */
|
|
};
|
|
|
|
#define PING_TTL_TB (86400ULL * 10000ULL)
|
|
|
|
struct TOPOMSG_SOCKMETA4 {
|
|
uint8_t id, config_type, mapping, inbound, source;
|
|
} __attribute__((packed));
|
|
struct TOPOMSG_ADDR4 { uint8_t addr[4]; uint16_t port; uint8_t type, socket_id, protocol; } __attribute__((packed));
|
|
struct TOPOMSG_SOCKMETA6 {
|
|
uint8_t id, config_type, mapping, inbound, source;
|
|
} __attribute__((packed));
|
|
struct TOPOMSG_ADDR6 { uint8_t addr[16]; uint16_t port; uint8_t type, socket_id, protocol; } __attribute__((packed));
|
|
struct TOPOMSG_SUBNET4 { uint8_t addr[4]; uint8_t prefix_length; } __attribute__((packed));
|
|
struct TOPOMSG_SUBNET6 { uint8_t addr[16]; uint8_t prefix_length; } __attribute__((packed));
|
|
|
|
/* Reality-камуфляж сокета: что нужно клиенту для подключения к reality TCP-сокету.
|
|
* short_id(8) + server_pubkey(32) + version(3) + server_name(фиксированный 256). */
|
|
#define TOPO_REALITY_SNAME_MAX 256
|
|
struct TOPOMSG_REALITY_SOCK {
|
|
uint8_t socket_id;
|
|
uint8_t short_id[8];
|
|
uint8_t server_pubkey[32];
|
|
uint8_t version[3];
|
|
uint8_t server_name[TOPO_REALITY_SNAME_MAX];
|
|
} __attribute__((packed));
|
|
|
|
struct TOPOMSG_NODE {
|
|
uint8_t flags; uint64_t group_id, node_id; uint8_t ver;
|
|
uint64_t timestamp; // версия всей подписанной записи, Unix microseconds
|
|
uint8_t public_key[SC_PUBKEY_SIZE], ed25519_public_key[SC_PUBKEY_SIZE];
|
|
uint8_t x25519_self_sig[64];
|
|
uint8_t node_name_len, local_v4_sockets, local_v4_addrs, local_v6_sockets, local_v6_addrs;
|
|
uint8_t local_v4_subnets, local_v6_subnets, hop_count;
|
|
uint8_t local_reality_socks; // кол-во reality-сокетов
|
|
uint8_t client_type; // CLIENT_TYPE_SERVER/DESKTOP/MOBILE
|
|
uint8_t client_activity; // CLIENT_ACTIVITY_STANDBY/ACTIVE
|
|
uint16_t cumulative_rtt;
|
|
} __attribute__((packed));
|
|
|
|
#define TOPOMSG_NODE_HDR_SIZE sizeof(struct TOPOMSG_NODE)
|
|
|
|
struct TOPO_SOCKMETA4 { struct TOPO_SOCKMETA4* next; uint8_t id, config_type; struct address_profile address_profile; };
|
|
/* port=0 — известен только внешний TCP IP; адрес хранится, но не участвует в подключении. */
|
|
struct TOPO_ADDR4 { struct TOPO_ADDR4* next; uint8_t addr[4]; uint16_t port; uint8_t type, socket_id, protocol; };
|
|
struct TOPO_SOCKMETA6 { struct TOPO_SOCKMETA6* next; uint8_t id, config_type; struct address_profile address_profile; };
|
|
struct TOPO_ADDR6 { struct TOPO_ADDR6* next; uint8_t addr[16]; uint16_t port; uint8_t type, socket_id, protocol; };
|
|
struct TOPO_SUBNET4 { struct TOPO_SUBNET4* next; uint8_t addr[4]; uint8_t prefix_length; };
|
|
struct TOPO_SUBNET6 { struct TOPO_SUBNET6* next; uint8_t addr[16]; uint8_t prefix_length; };
|
|
|
|
/* Reality-камуфляж сокета (in-memory): per-socket список, значения на узел одинаковы. */
|
|
struct TOPO_REALITY_SOCK {
|
|
struct TOPO_REALITY_SOCK* next;
|
|
uint8_t socket_id;
|
|
uint8_t short_id[8];
|
|
uint8_t server_pubkey[32];
|
|
uint8_t version[3];
|
|
char server_name[TOPO_REALITY_SNAME_MAX];
|
|
};
|
|
|
|
/* Reality-параметры адреса для персистенции в node_addresses.options (BLOB, фиксированный размер). */
|
|
#define TOPO_ADDR_REALITY_OPTS_SIZE (8 + 32 + 3 + TOPO_REALITY_SNAME_MAX)
|
|
struct TOPO_ADDR_REALITY_OPTS {
|
|
uint8_t short_id[8];
|
|
uint8_t server_pubkey[32];
|
|
uint8_t version[3];
|
|
char server_name[TOPO_REALITY_SNAME_MAX];
|
|
};
|
|
|
|
struct _topo_head { struct _topo_head* next; };
|
|
static inline int topo_list_count(struct _topo_head* head) { int n = 0; while (head) { n++; head = head->next; } return n; }
|
|
|
|
/** Общая запись узла; node_registry хранит указатель на неё в отдельном ll_entry. */
|
|
struct TOPO_NODE {
|
|
struct ll_entry ll;
|
|
uint32_t group_ref_count; // ссылки групп, ядра и временных пользователей
|
|
uint64_t node_id; uint8_t ver;
|
|
uint64_t timestamp; // 0 = неподписанные bootstrap-сведения
|
|
uint8_t public_key[SC_PUBKEY_SIZE], ed25519_public_key[SC_PUBKEY_SIZE];
|
|
uint8_t x25519_self_sig[64]; // Ed25519-подпись общей записи
|
|
uint8_t client_type; // CLIENT_TYPE_SERVER/DESKTOP/MOBILE
|
|
uint8_t client_activity; // CLIENT_ACTIVITY_STANDBY/ACTIVE
|
|
char* node_name; // собственная строка; освобождается с записью
|
|
struct TOPO_SOCKMETA4* v4_sock_meta;
|
|
struct TOPO_ADDR4* v4_addrs;
|
|
struct TOPO_SOCKMETA6* v6_sock_meta;
|
|
struct TOPO_ADDR6* v6_addrs;
|
|
struct TOPO_REALITY_SOCK* reality_socks; // reality-камуфляж сокетов (per-socket, значения на узел)
|
|
};
|
|
|
|
struct TOPO_NODESUBNETS {
|
|
struct TOPO_SUBNET4* v4_subnets;
|
|
struct TOPO_SUBNET6* v6_subnets;
|
|
};
|
|
|
|
struct TOPO_NODEPATH {
|
|
struct ll_entry ll;
|
|
struct ETCP_CONN* conn; // заимствованный next-hop; транспорт удерживает сессия
|
|
uint8_t hop_count; // число node_id в массиве сразу после структуры
|
|
uint16_t cumulative_rtt; // RTT оставшейся цепочки, без локального линка (0.1 ms)
|
|
uint64_t exchange; // local_epoch сессии, в которой путь подтверждён NODEINFO
|
|
};
|
|
|
|
/** Узел в контексте группы (per-group данные). Хранится в TOPO_GROUP->nodes (ll_queue). */
|
|
struct TOPO_GROUP_NODE {
|
|
struct ll_entry ll; // queue entry, хеш-ключ 8 байт → node_id
|
|
uint64_t node_id; // идентификатор узла в этой группе
|
|
struct TOPO_NODESUBNETS* subnets; // подсети узла (v4/v6), для route_insert()
|
|
struct ll_queue* paths; // TOPO_NODEPATH{conn,hop_count,rtt} — маршруты
|
|
uint8_t last_ver; // диагностический циклический ver; свежесть определяется timestamp
|
|
uint64_t last_timestamp; // последняя принятая версия записи в группе
|
|
uint8_t conn_mgr_type; // CONN_TYPE_DIRECT/REVERSE/INDIRECT/NONE
|
|
uint64_t conn_mgr_intermediaries[CONN_MGR_MAX_INTERMEDIARIES]; // посредники для INDIRECT
|
|
uint8_t conn_mgr_intermediariy_count; // количество посредников
|
|
struct TOPO_CONNECTIVITY connectivity; // результаты ping-проб (interface/nat/real)
|
|
uint8_t conn_presence; // маска NCONN_*: известные виды подключения
|
|
uint8_t conn_up; // маска NCONN_*: работающие виды подключения
|
|
uint8_t radio; // 1 = узел слушает рацию канала (TOPO_FLAG_RADIO)
|
|
uint8_t phone_status; // TOPO_FLAG_PHONE_KNOWN/BUSY; подсказка интерфейсу, не основание для отказа
|
|
struct NODE_CONN_DIRECT* handle; // не владеет транспортом; NCD принадлежит сессии группы
|
|
};
|
|
|
|
// API — глобальный реестр TOPO_NODE
|
|
/* Заимствованный указатель; ref нужен, если запись должна пережить удаление из группы. */
|
|
struct TOPO_NODE* topo_node_registry_find(struct TOPO_GROUPS* groups, uint64_t node_id);
|
|
/* Принять ni и вернуть запись с одной новой ссылкой. Может вернуть существующий объект.
|
|
* При NULL входной ni остаётся у вызывающего. Подписанные записи сравниваются по timestamp. */
|
|
struct TOPO_NODE* topo_node_registry_store(struct TOPO_GROUPS* groups, struct TOPO_NODE* ni);
|
|
/* Добавить/снять ссылку; последняя unref удаляет запись из реестра и освобождает её. */
|
|
void topo_node_registry_ref(struct TOPO_GROUPS* groups, uint64_t node_id);
|
|
void topo_node_registry_unref(struct TOPO_GROUPS* groups, uint64_t node_id);
|
|
/* Освободить отдельную запись и её списки; зарегистрированную запись отпускать через unref. */
|
|
void topo_node_destroy(struct TOPO_GROUPS* groups, struct TOPO_NODE* ni);
|
|
|
|
// API — per-group
|
|
/* Заимствованный узел группы, NULL если не найден. */
|
|
struct TOPO_GROUP_NODE* topo_node_find_by_id(struct TOPO_GROUP* group, uint64_t node_id);
|
|
/* Отпустить ссылку реестра и подсети; nq и paths не освобождает. */
|
|
void topo_nodeq_free_group_fields(struct TOPO_GROUPS* groups, struct TOPO_GROUP_NODE* nq);
|
|
/* Освободить принадлежащие узлу записи путей и их контейнер; ETCP-соединения заимствованы. */
|
|
void topo_nodeq_clear_paths(struct TOPO_GROUP_NODE* nq);
|
|
/* Полное удаление узла из группы: отменяет влётные пробы, освобождает paths/subnets,
|
|
* убирает из group->nodes и освобождает сам nq. Единственный корректный способ удаления узла. */
|
|
void topo_nodeq_remove_node(struct TOPO_GROUP* group, struct TOPO_GROUP_NODE* nq);
|
|
|
|
/* Размер переменной части NODEINFO или -1 при NULL/счётчиках подсетей без TOPO_FLAG_SEND_SUBNETS. */
|
|
int topo_node_dyn_size(const struct TOPOMSG_NODE* msg);
|
|
/* Собрать NODEINFO в out[out_max]; длина или -1. Входные структуры остаются у вызывающего.
|
|
* Без TOPO_FLAG_SEND_SUBNETS обе секции подсетей отсутствуют, их счётчики равны 0. */
|
|
int topo_node_serialize(struct TOPO_NODE* ni, struct TOPO_GROUP_NODE* nq,
|
|
uint64_t group_id, uint8_t flags,
|
|
uint8_t* out, size_t out_max, uint16_t cumulative_rtt);
|
|
/* Разобрать ровно len байт NODEINFO; 0/-1. group->instance->topo_groups содержит пулы списков.
|
|
* out_ni обязателен, остальные выходы независимы и необязательны. Отсутствие выхода не меняет обход/проверки секций.
|
|
* Перед разбором все переданные выходы обнуляются, прежние объекты не освобождаются.
|
|
* При успехе node уничтожается topo_node_destroy, подсети возвращаются в пулы groups, hops освобождается u_free.
|
|
* Пустые секции дают NULL/0; при ошибке выходы остаются NULL/0, частичные объекты освобождены.
|
|
* Проверяет размер, профили и /0..32, /0..128; подпись и допустимость BGP-пути проверяет вызывающий. */
|
|
int topo_node_deserialize(struct TOPO_GROUP* group, const uint8_t* data, size_t len, struct TOPO_NODE** out_ni, struct TOPO_NODESUBNETS** out_subnets,
|
|
uint64_t** out_hop_list, uint8_t* out_hop_count,
|
|
uint16_t* out_cumulative_rtt);
|
|
/* Заимствованный список hops выбранного живого пути nq: минимум переходов, затем ID соседа.
|
|
* out_count обязателен; out_rtt необязателен и получает RTT цепочки без локального линка (0.1 ms).
|
|
* Если живого пути нет, возвращает NULL и обнуляет выходные значения. Память списка не освобождать. */
|
|
uint64_t* topo_node_best_hop_list(struct TOPO_GROUP_NODE* nq, uint8_t* out_count, uint16_t* out_rtt);
|
|
/* Лучший живой путь: минимум переходов, затем ID соседа. Указатель заимствованный. */
|
|
struct TOPO_NODEPATH* topo_node_best_path(const struct TOPO_GROUP_NODE* nq);
|
|
|
|
/* Подготовить и опубликовать local_node с актуальными подсетями/состоянием; число IPv4-подсетей или -1.
|
|
* Учитывает адреса и префиксы, а не только счётчики; CHAT не анонсирует подсети и возвращает 0.
|
|
* При отказе подготовки/подписи или конфликте IPv4 сохраняет прежние local_node и маршруты.
|
|
* Вызывает обновление общей подписанной записи; рассылку NODEINFO выполняет вызывающий. */
|
|
int topo_group_update_my_nodeinfo(struct UTUN_INSTANCE* instance, struct TOPO_GROUP* group);
|
|
/* Создать подписанную локальную идентичность с отдельной ссылкой ядра. 0/-1. */
|
|
int topo_node_init_self(struct UTUN_INSTANCE* instance);
|
|
/* Пересобрать общие адреса из рабочих UDP/TCP-сокетов; 1=изменились, 0=прежние, -1=ошибка.
|
|
* Новая запись подписывается/сохраняется перед заменой, затем рассылается во все группы.
|
|
* Ошибка SQL-проекции даёт -1 и после применённого изменения; при неизменных адресах проекция также обновляется. */
|
|
int topo_node_update_my_addresses(struct UTUN_INSTANCE* instance);
|
|
/* Обработчик изменений локального сокета: обновляет адреса узла. */
|
|
void topo_node_on_socket_changed(struct ETCP_SOCKET* sock, int event, void* arg);
|
|
/* Вывести диагностику узлов группы в лог категории BGP на уровне INFO. */
|
|
void topo_node_dump_all(struct TOPO_GROUP* group);
|
|
/* Записать дамп в buf: длина без NUL или -1 при ошибке форматирования. Ёмкость ограничена INT_MAX.
|
|
* При усечении завершает запись сразу, заменяет хвост доступной частью "\n[TRUNCATED]\n" и возвращает реальную длину.
|
|
* При buf != NULL и buf_size > 0 строка всегда завершена NUL. Некорректные аргументы дают 0 и пустую строку, если есть буфер. */
|
|
int topo_node_format_all(struct TOPO_GROUP* group, char* buf, size_t buf_size);
|
|
/* 1, если хотя бы в одной группе пора повторить ping этого узла; иначе 0. */
|
|
int topo_node_ping_request_cbk(struct TOPO_GROUPS* groups, uint64_t node_id);
|
|
/* Обновить время ping и минимальные RTT во всех группах и БД; rtt в единицах 0.1 ms. */
|
|
void topo_node_ping_update_rtt(struct TOPO_GROUPS* groups, uint64_t node_id, uint16_t rtt);
|
|
/* Суммарный RTT выбранного живого пути nq: RTT локального соединения + RTT оставшейся цепочки (0.1 ms).
|
|
* Путь выбирается по числу переходов и ID соседа. UINT16_MAX — нет пути, неизвестен локальный RTT или сумма насыщена. */
|
|
uint16_t topo_get_chain_rtt(struct TOPO_GROUP_NODE* nq);
|
|
|
|
/* Лимиты буфера подписываемых данных и полного сетевого NODEINFO. */
|
|
#define TOPO_SIG_MSG_MAX_SIZE 8192
|
|
#define TOPO_NODE_WIRE_MAX_SIZE 16384
|
|
/* Канонические байты подписи: node_id, ver, timestamp, X25519-ключ, имя,
|
|
* тип/активность и полные списки сокетов/адресов/Reality. Длина или -1. */
|
|
int topo_node_build_sig_msg(struct TOPO_NODE* ni, uint8_t* buf, size_t buf_size);
|
|
|
|
/* Увеличить timestamp (выше текущего/сохранённого), подписать локальный NODEINFO; при наличии SQLite сохранить снимок.
|
|
* 0/-1; при ошибке возвращает прежние timestamp/подпись, остальные изменённые вызывающим поля не откатывает. */
|
|
int topo_node_sign_self(struct UTUN_INSTANCE* instance, struct TOPO_NODE* ni);
|
|
/* Проверить timestamp, привязку node_id к ключу и подпись. 0/-1. */
|
|
int topo_node_verify(const struct TOPO_NODE* ni);
|
|
|
|
static inline const struct TOPO_SOCKMETA4* topo_v4_sock_meta(const struct TOPO_NODE* ni) { return ni->v4_sock_meta; }
|
|
static inline const struct TOPO_ADDR4* topo_v4_addrs(const struct TOPO_NODE* ni) { return ni->v4_addrs; }
|
|
static inline const struct TOPO_SOCKMETA6* topo_v6_sock_meta(const struct TOPO_NODE* ni) { return ni->v6_sock_meta; }
|
|
static inline const struct TOPO_ADDR6* topo_v6_addrs(const struct TOPO_NODE* ni) { return ni->v6_addrs; }
|
|
static inline const struct TOPO_SUBNET4* topo_v4_subnets(const struct TOPO_NODESUBNETS* r) { return r ? r->v4_subnets : NULL; }
|
|
static inline const struct TOPO_SUBNET6* topo_v6_subnets(const struct TOPO_NODESUBNETS* r) { return r ? r->v6_subnets : NULL; }
|
|
|
|
/* Найти reality-инфо сокета по socket_id (NULL, если сокет без reality). */
|
|
const struct TOPO_REALITY_SOCK* topo_node_find_reality_sock(const struct TOPO_NODE* ni, uint8_t socket_id);
|
|
|
|
/* Скопировать REALITY-параметры пира rs в cfg и задать fingerprint OPENSSL; оба указателя обязательны. Возврата нет. */
|
|
void topo_node_reality_config(struct reality_client_config *cfg, const struct TOPO_REALITY_SOCK *rs);
|
|
/* Заполнить link->reality из rs и установить link->reality_set=1; при NULL аргументе ничего не делает. Возврата нет. */
|
|
void topo_node_apply_reality(struct ETCP_LINK* link, const struct TOPO_REALITY_SOCK* rs);
|
|
|
|
#ifdef __cplusplus
|
|
}
|
|
#endif
|
|
#endif
|