diff options
Diffstat (limited to 'plugins')
55 files changed, 12932 insertions, 0 deletions
diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt new file mode 100644 index 0000000..9736302 --- /dev/null +++ b/plugins/CMakeLists.txt @@ -0,0 +1,44 @@ +# Link a transport plugin into all HyBBX instance binaries (hybbxd/hybbxsd/hybbxpd). +function(hybbx_link_plugin_instances plugin_target define_name) + foreach(_bin IN ITEMS hybbxd hybbxsd hybbxpd) + if(TARGET ${_bin}) + target_link_libraries(${_bin} PRIVATE ${plugin_target}) + if(NOT "${define_name}" STREQUAL "") + target_compile_definitions(${_bin} PRIVATE ${define_name}) + endif() + endif() + endforeach() +endfunction() + +if(HYBBX_PLUGIN_TELNET) + add_subdirectory(telnet) +endif() + +if(HYBBX_PLUGIN_PACKET_RADIO) + add_subdirectory(packet_radio) +endif() + +if(HYBBX_PLUGIN_ARDOP OR HYBBX_PLUGIN_CRDOP) + add_subdirectory(ardop) +endif() + +if(HYBBX_PLUGIN_CRDOP) + add_subdirectory(crdop) +endif() + +if(HYBBX_PLUGIN_BAYCOM) + add_subdirectory(baycom) +endif() + +if(HYBBX_PLUGIN_MAINS_PROXY) + add_subdirectory(mains_proxy) + target_compile_definitions(hybbx_core PRIVATE HYBBX_HAVE_PLUGIN_MAINS_PROXY) +endif() + +if(HYBBX_PLUGIN_SSH AND HYBBX_HAVE_LIBSSH) + add_subdirectory(ssh) +endif() + +if(HYBBX_PLUGIN_WEBSOCKET) + add_subdirectory(websocket) +endif() diff --git a/plugins/ardop/CMakeLists.txt b/plugins/ardop/CMakeLists.txt new file mode 100644 index 0000000..c39ab91 --- /dev/null +++ b/plugins/ardop/CMakeLists.txt @@ -0,0 +1,57 @@ +add_library(hybbx_ardop_common STATIC + ardop_link.c + ardop_host.c + ardop_crc.c + ardop_config.c +) + +target_include_directories(hybbx_ardop_common + PUBLIC + ${CMAKE_SOURCE_DIR}/include + ${CMAKE_CURRENT_SOURCE_DIR} +) + +target_compile_definitions(hybbx_ardop_common + PRIVATE HYBBX_PLUGIN_BUILD +) + +find_package(Threads REQUIRED) +target_link_libraries(hybbx_ardop_common PUBLIC hybbx_core Threads::Threads) +if(HYBBX_PLATFORM_AMIGAOS) + target_link_libraries(hybbx_ardop_common PUBLIC amiga) +endif() +hybbx_apply_hardening(hybbx_ardop_common) + +if(HYBBX_PLUGIN_ARDOP) + add_library(hybbx_plugin_ardop STATIC + ardop.c + ) + + target_include_directories(hybbx_plugin_ardop + PRIVATE + ${CMAKE_SOURCE_DIR}/include + ${CMAKE_CURRENT_SOURCE_DIR} + ) + + target_compile_definitions(hybbx_plugin_ardop + PRIVATE HYBBX_PLUGIN_BUILD + ) + + target_link_libraries(hybbx_plugin_ardop PRIVATE hybbx_ardop_common) + hybbx_link_plugin_instances(hybbx_plugin_ardop HYBBX_HAVE_PLUGIN_ARDOP) + hybbx_apply_hardening(hybbx_plugin_ardop) + + if(HYBBX_INSTALL_DEV) + install(TARGETS hybbx_plugin_ardop + ARCHIVE DESTINATION ${HYBBX_PLUGIN_INSTALL_DIR} + LIBRARY DESTINATION ${HYBBX_PLUGIN_INSTALL_DIR} + ) + endif() +endif() + +if(HYBBX_INSTALL_DEV) +install(TARGETS hybbx_ardop_common + ARCHIVE DESTINATION ${HYBBX_PLUGIN_INSTALL_DIR} + LIBRARY DESTINATION ${HYBBX_PLUGIN_INSTALL_DIR} +) +endif() diff --git a/plugins/ardop/ardop.c b/plugins/ardop/ardop.c new file mode 100644 index 0000000..e07a158 --- /dev/null +++ b/plugins/ardop/ardop.c @@ -0,0 +1,49 @@ +/* + * ardop — ARDOP host-client link adapter (external ARDOPC / ardopcf). + * INI: [transport.ardop] or [transport.ardopN]. + */ +#include "hybbx/plugin.h" +#include "hybbx/service.h" +#include "hybbx/ardop.h" +#include "ardop_link.h" + +static const ardop_link_plugin_t g_ardop_link = { + .name = "ardop", + .kind = HYBBX_TRANSPORT_ARDOP, + .default_link_id = "ardop-link", + .modem_hint = "ARDOPC or ardopcf", + .parse_config = (ardop_link_config_fn)hybbx_ardop_config_parse, + .config_free = (void (*)(void *))hybbx_ardop_config_free, + .config_size = sizeof(hybbx_ardop_config_t), +}; + +static hybbx_result_t ardop_init(hybbx_service_t *service) +{ + return ardop_link_init(service, &g_ardop_link); +} + +static void ardop_shutdown(void) +{ + ardop_link_shutdown(&g_ardop_link); +} + +static hybbx_result_t ardop_start(const char *config) +{ + return ardop_link_start(&g_ardop_link, config); +} + +static hybbx_result_t ardop_stop(void) +{ + return ardop_link_stop(&g_ardop_link); +} + +const hybbx_transport_plugin_t hybbx_plugin_ardop = { + .name = "ardop", + .kind = HYBBX_TRANSPORT_ARDOP, + .version = 1, + .init = ardop_init, + .shutdown = ardop_shutdown, + .start = ardop_start, + .stop = ardop_stop, + .write = NULL, +}; diff --git a/plugins/ardop/ardop_config.c b/plugins/ardop/ardop_config.c new file mode 100644 index 0000000..71b40dc --- /dev/null +++ b/plugins/ardop/ardop_config.c @@ -0,0 +1,186 @@ +#include "hybbx/ardop.h" +#include "hybbx/crdop.h" +#include "hybbx/circuit.h" +#include "hybbx/util.h" +#include "hybbx/log.h" + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +static char *ardop_strdup(const char *s) +{ + size_t len; + char *copy; + + if (s == NULL) { + return NULL; + } + + len = strlen(s) + 1; + copy = malloc(len); + if (copy != NULL) { + memcpy(copy, s, len); + } + return copy; +} + +static int str_ieq(const char *a, const char *b) +{ + if (a == NULL || b == NULL) { + return 0; + } + + while (*a != '\0' && *b != '\0') { + char ca = (char)(*a >= 'A' && *a <= 'Z' ? *a + 32 : *a); + char cb = (char)(*b >= 'A' && *b <= 'Z' ? *b + 32 : *b); + + if (ca != cb) { + return 0; + } + a++; + b++; + } + + return *a == '\0' && *b == '\0'; +} + +static const char *find_kv(const char *config, const char *key, + char *scratch, size_t scratch_len) +{ + const char *cursor = config; + size_t key_len = strlen(key); + + if (config == NULL || key == NULL) { + return NULL; + } + + while (*cursor != '\0') { + const char *sep = strchr(cursor, ';'); + const char *end = sep != NULL ? sep : cursor + strlen(cursor); + const char *eq = strchr(cursor, '='); + + if (eq != NULL && eq < end && (size_t)(eq - cursor) == key_len && + strncmp(cursor, key, key_len) == 0) { + const char *value = eq + 1; + size_t value_len = (size_t)(end - value); + + if (value_len >= scratch_len) { + value_len = scratch_len - 1; + } + memcpy(scratch, value, value_len); + scratch[value_len] = '\0'; + return scratch; + } + + if (sep == NULL) { + break; + } + cursor = sep + 1; + } + + return NULL; +} + +void hybbx_ardop_config_free(hybbx_ardop_config_t *config) +{ + if (config == NULL) { + return; + } + + free(config->ardop_host); + free(config->mycall); + free(config->arq_bandwidth); + free(config->peer_call); + free(config->circuit_host); + free(config->link_id); + free(config->link_password); + free(config->link_role); + memset(config, 0, sizeof(*config)); +} + +hybbx_result_t hybbx_ardop_config_parse(const char *config, + hybbx_ardop_config_t *out) +{ + char scratch[128]; + const char *value; + + if (out == NULL) { + return HYBBX_ERR_INVALID; + } + + hybbx_ardop_config_free(out); + + if (config == NULL) { + config = ""; + } + + value = find_kv(config, "ardop_host", scratch, sizeof(scratch)); + out->ardop_host = ardop_strdup(value != NULL ? value : "127.0.0.1"); + + out->ardop_port = (unsigned)strtoul( + find_kv(config, "ardop_port", scratch, sizeof(scratch)) != NULL ? + scratch : "8515", NULL, 10); + + value = find_kv(config, "mycall", scratch, sizeof(scratch)); + out->mycall = ardop_strdup(value != NULL ? value : "NOCALL"); + + value = find_kv(config, "radio_profile", scratch, sizeof(scratch)); + out->radio_profile = hybbx_crdop_profile_parse(value); + + value = find_kv(config, "arq_bandwidth", scratch, sizeof(scratch)); + out->arq_bandwidth = ardop_strdup( + value != NULL ? value : + hybbx_crdop_default_arq_bandwidth(out->radio_profile)); + + value = find_kv(config, "peer_call", scratch, sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + out->peer_call = ardop_strdup(value); + } + + value = find_kv(config, "listen", scratch, sizeof(scratch)); + out->listen = value == NULL || str_ieq(value, "yes") || str_ieq(value, "1") || + str_ieq(value, "true"); + + value = find_kv(config, "circuit_host", scratch, sizeof(scratch)); + out->circuit_host = ardop_strdup(value != NULL ? value : "127.0.0.1"); + + out->circuit_port = (unsigned)strtoul( + find_kv(config, "circuit_port", scratch, sizeof(scratch)) != NULL ? + scratch : "7323", NULL, 10); + + value = find_kv(config, "link_id", scratch, sizeof(scratch)); + out->link_id = ardop_strdup(value != NULL ? value : "ardop-link"); + + value = find_kv(config, "link_password", scratch, sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + out->link_password = ardop_strdup(value); + } + + value = find_kv(config, "link_role", scratch, sizeof(scratch)); + out->link_role = ardop_strdup(value != NULL ? value : "link"); + + value = find_kv(config, "frequency_mhz", scratch, sizeof(scratch)); + if (value == NULL || value[0] == '\0') { + value = find_kv(config, "frequency", scratch, sizeof(scratch)); + } + if (value != NULL && value[0] != '\0') { + out->frequency_mhz = strtod(value, NULL); + } + + if (out->radio_profile == HYBBX_CRDOP_PROFILE_CB && + hybbx_crdop_bandwidth_exceeds_cb(out->arq_bandwidth)) { + hybbx_log_warn("[ardop] warning: arq_bandwidth=%s high for CB profile " + "(CRDOP Level 2 experimental)", + out->arq_bandwidth); + } + + if (out->ardop_host == NULL || out->mycall == NULL || + out->arq_bandwidth == NULL || out->circuit_host == NULL || + out->link_id == NULL || out->link_role == NULL) { + hybbx_ardop_config_free(out); + return HYBBX_ERR_NOMEM; + } + + return HYBBX_OK; +} diff --git a/plugins/ardop/ardop_crc.c b/plugins/ardop/ardop_crc.c new file mode 100644 index 0000000..4cf205a --- /dev/null +++ b/plugins/ardop/ardop_crc.c @@ -0,0 +1,69 @@ +#include "ardop_crc.h" + +/* Algorithm from ARDOPC (KN6KB / G8BPQ) — CRC-16-CCITT variant, poly 0x8810. */ + +unsigned int ardop_crc16(const unsigned char *data, unsigned short length) +{ + int int_register = 0xffff; + int i; + int j; + int bit; + const int int_poly = 0x8810; + + if (data == NULL || length == 0) { + return 0xffffu; + } + + for (j = 0; j < (int)length; j++) { + int mask = 0x80; + + for (i = 0; i < 8; i++) { + bit = data[j] & mask; + mask >>= 1; + + if (int_register & 0x8000) { + if (bit) { + int_register = 0xffff & (1 + (int_register << 1)); + } else { + int_register = 0xffff & (int_register << 1); + } + int_register = int_register ^ int_poly; + } else { + if (bit) { + int_register = 0xffff & (1 + (int_register << 1)); + } else { + int_register = 0xffff & (int_register << 1); + } + } + } + } + + return (unsigned int)int_register; +} + +int ardop_crc16_valid(const unsigned char *data, unsigned short length) +{ + unsigned int crc; + + if (data == NULL || length < 3) { + return 0; + } + + crc = ardop_crc16(data, (unsigned short)(length - 2)); + return data[length - 2] == (unsigned char)(crc >> 8) && + data[length - 1] == (unsigned char)(crc & 0xff); +} + +size_t ardop_crc16_append(unsigned char *data, size_t length, size_t cap) +{ + unsigned int crc; + + if (data == NULL || length + 2 > cap) { + return 0; + } + + crc = ardop_crc16(data, (unsigned short)length); + data[length] = (unsigned char)(crc >> 8); + data[length + 1] = (unsigned char)(crc & 0xff); + return length + 2; +} diff --git a/plugins/ardop/ardop_crc.h b/plugins/ardop/ardop_crc.h new file mode 100644 index 0000000..69c2f07 --- /dev/null +++ b/plugins/ardop/ardop_crc.h @@ -0,0 +1,15 @@ +#ifndef ARDOP_CRC_H +#define ARDOP_CRC_H + +#include <stddef.h> + +/** ARDOP host/TNC CRC-16 (poly 0x8810, init 0xFFFF) per ARDOPC reference. */ +unsigned int ardop_crc16(const unsigned char *data, unsigned short length); + +/** Non-zero when @p length includes trailing 2-byte CRC that validates. */ +int ardop_crc16_valid(const unsigned char *data, unsigned short length); + +/** Append CRC16 (MSB, LSB) after @p length bytes; returns new length. */ +size_t ardop_crc16_append(unsigned char *data, size_t length, size_t cap); + +#endif /* ARDOP_CRC_H */ diff --git a/plugins/ardop/ardop_host.c b/plugins/ardop/ardop_host.c new file mode 100644 index 0000000..aeac403 --- /dev/null +++ b/plugins/ardop/ardop_host.c @@ -0,0 +1,650 @@ +#include "ardop_host.h" +#include "ardop_crc.h" + +#include "hybbx/socket.h" +#include "hybbx/util.h" +#include "hybbx/log.h" + +#include <arpa/inet.h> +#include <errno.h> +#include <netinet/in.h> +#include <netinet/tcp.h> +#include <poll.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#define ARDOP_HOST_RX_MAX 8192u +#define ARDOP_HOST_CMD_MAX 256u +#define ARDOP_HOST_FRAME_MAX (HYBBX_ARDOP_DATA_MAX + 64u) + +struct ardop_host { + hybbx_ardop_config_t cfg; + ardop_host_data_fn on_data; + ardop_host_event_fn on_event; + void *userdata; + int ctrl_fd; + int data_fd; + int tnc_ready; + int link_up; + char peer_call[16]; + unsigned char ctrl_rx[ARDOP_HOST_RX_MAX]; + size_t ctrl_len; + unsigned char data_rx[ARDOP_HOST_RX_MAX]; + size_t data_len; + int init_sent; + const char *log_tag; +}; + +static const char *host_log_tag(const ardop_host_t *host) +{ + if (host != NULL && host->log_tag != NULL && host->log_tag[0] != '\0') { + return host->log_tag; + } + return "ardop"; +} + +static const char *host_modem_name(const ardop_host_t *host) +{ + if (host != NULL && host->log_tag != NULL && + strcmp(host->log_tag, "crdop") == 0) { + return "CRDOPC"; + } + return "ARDOPC"; +} + +static int str_ieq(const char *a, const char *b) +{ + if (a == NULL || b == NULL) { + return 0; + } + + while (*a != '\0' && *b != '\0') { + char ca = (char)(*a >= 'A' && *a <= 'Z' ? *a + 32 : *a); + char cb = (char)(*b >= 'A' && *b <= 'Z' ? *b + 32 : *b); + + if (ca != cb) { + return 0; + } + a++; + b++; + } + + return *a == '\0' && *b == '\0'; +} + +static void ardop_host_fire_event(ardop_host_t *host, const char *event, + const char *detail) +{ + if (host != NULL && host->on_event != NULL) { + host->on_event(event, detail, host->userdata); + } +} + +static int ardop_host_set_socket_opts(int fd) +{ + int on = 1; + + if (setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &on, sizeof(on)) != 0) { + return -1; + } + + return 0; +} + +static int ardop_host_tcp_connect(const char *host, unsigned port) +{ + struct sockaddr_in addr; + int fd; + int rc; + + if (host == NULL || host[0] == '\0' || port == 0) { + return -1; + } + + fd = socket(AF_INET, SOCK_STREAM, 0); + if (fd < 0) { + return -1; + } + + if (ardop_host_set_socket_opts(fd) != 0) { + close(fd); + return -1; + } + + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_port = htons((uint16_t)port); + + if (inet_pton(AF_INET, host, &addr.sin_addr) != 1) { + close(fd); + return -1; + } + + rc = connect(fd, (struct sockaddr *)&addr, sizeof(addr)); + if (rc != 0) { + close(fd); + return -1; + } + + return fd; +} + +static hybbx_result_t ardop_host_write_all(int fd, const unsigned char *buf, + size_t len) +{ + size_t off = 0; + + if (fd < 0 || buf == NULL || len == 0) { + return HYBBX_ERR_INVALID; + } + + while (off < len) { + ssize_t n = send(fd, buf + off, len - off, MSG_NOSIGNAL); + + if (n < 0) { + if (errno == EINTR) { + continue; + } + return HYBBX_ERR_IO; + } + if (n == 0) { + return HYBBX_ERR_IO; + } + off += (size_t)n; + } + + return HYBBX_OK; +} + +static hybbx_result_t ardop_host_send_cmd_frame(ardop_host_t *host, + const char *cmd) +{ + unsigned char frame[ARDOP_HOST_CMD_MAX + 4]; + size_t len; + + if (host == NULL || cmd == NULL || host->ctrl_fd < 0) { + return HYBBX_ERR_INVALID; + } + + len = strlen(cmd); + if (len + 3 > sizeof(frame)) { + return HYBBX_ERR_INVALID; + } + + memcpy(frame, cmd, len); + frame[len++] = '\r'; + len = ardop_crc16_append(frame, len, sizeof(frame)); + if (len == 0) { + return HYBBX_ERR_IO; + } + + return ardop_host_write_all(host->ctrl_fd, frame, len); +} + +static hybbx_result_t ardop_host_send_rdy(ardop_host_t *host) +{ + return ardop_host_send_cmd_frame(host, "RDY"); +} + +static void ardop_host_strip_prefix(char *line) +{ + if (line == NULL) { + return; + } + + if (strncmp(line, "c:", 2) == 0 || strncmp(line, "C:", 2) == 0) { + memmove(line, line + 2, strlen(line + 2) + 1); + } +} + +static void ardop_host_handle_ctrl_line(ardop_host_t *host, char *line) +{ + char *sp; + + if (host == NULL || line == NULL) { + return; + } + + ardop_host_strip_prefix(line); + + if (str_ieq(line, "RDY")) { + host->tnc_ready = 1; + return; + } + + if (strncmp(line, "VERSION", 7) == 0) { + hybbx_log_debug("[%s] modem %s", host_log_tag(host), line); + (void)ardop_host_send_rdy(host); + return; + } + + if (strncmp(line, "CONNECTED", 9) == 0) { + char *tok; + + sp = line + 9; + while (*sp == ' ') { + sp++; + } + hybbx_strlcpy(host->peer_call, sp, sizeof(host->peer_call)); + tok = strchr(host->peer_call, ' '); + if (tok != NULL) { + *tok = '\0'; + } + host->link_up = 1; + ardop_host_fire_event(host, "connected", host->peer_call); + (void)ardop_host_send_rdy(host); + return; + } + + if (str_ieq(line, "DISCONNECTED")) { + host->link_up = 0; + host->peer_call[0] = '\0'; + ardop_host_fire_event(host, "disconnected", NULL); + (void)ardop_host_send_rdy(host); + return; + } + + if (strncmp(line, "FAULT", 5) == 0 || + strncmp(line, "CRCFAULT", 8) == 0) { + hybbx_log_warn("[%s] TNC fault: %s", host_log_tag(host), line); + (void)ardop_host_send_rdy(host); + return; + } + + if (strncmp(line, "BUSY", 4) == 0 || + strncmp(line, "NEWSTATE", 8) == 0 || + strncmp(line, "BUFFER", 6) == 0 || + strncmp(line, "STATUS", 6) == 0 || + strncmp(line, "PENDING", 7) == 0 || + strncmp(line, "TARGET", 6) == 0) { + (void)ardop_host_send_rdy(host); + return; + } + + (void)ardop_host_send_rdy(host); +} + +static void ardop_host_process_ctrl_rx(ardop_host_t *host) +{ + unsigned char *cr; + unsigned char frame[ARDOP_HOST_CMD_MAX + 8]; + size_t msg_len; + char line[ARDOP_HOST_CMD_MAX]; + + if (host == NULL) { + return; + } + + for (;;) { + cr = memchr(host->ctrl_rx, '\r', host->ctrl_len); + if (cr == NULL) { + return; + } + + msg_len = (size_t)(cr - host->ctrl_rx) + 1u + 2u; + if (host->ctrl_len < msg_len) { + return; + } + + if (msg_len > sizeof(frame)) { + host->ctrl_len = 0; + return; + } + + memcpy(frame, host->ctrl_rx, msg_len); + if (!ardop_crc16_valid(frame, (unsigned short)msg_len)) { + hybbx_log_warn("[%s] control CRC fault", host_log_tag(host)); + host->ctrl_len = 0; + return; + } + + frame[msg_len - 3] = '\0'; + hybbx_strlcpy(line, (const char *)frame, sizeof(line)); + ardop_host_handle_ctrl_line(host, line); + + memmove(host->ctrl_rx, host->ctrl_rx + msg_len, host->ctrl_len - msg_len); + host->ctrl_len -= msg_len; + } +} + +static void ardop_host_process_data_rx(ardop_host_t *host) +{ + size_t off = 0; + + if (host == NULL) { + return; + } + + while (host->data_len >= off + 6u) { + const unsigned char *buf = host->data_rx + off; + size_t remain = host->data_len - off; + size_t payload_len; + size_t frame_len; + const uint8_t *payload; + const char *tag; + + if (buf[0] != 'd' && buf[0] != 'D') { + off++; + continue; + } + if (buf[1] != ':') { + off++; + continue; + } + + payload_len = ((size_t)buf[2] << 8) | (size_t)buf[3]; + if (payload_len < 3 || payload_len > HYBBX_ARDOP_DATA_MAX + 8u) { + off++; + continue; + } + + frame_len = 2u + 2u + payload_len + 2u; + if (remain < frame_len) { + break; + } + + if (!ardop_crc16_valid(buf, (unsigned short)frame_len)) { + off++; + continue; + } + + tag = (const char *)(buf + 4); + payload = buf + 7; + payload_len -= 3u; + + if (strncmp(tag, "ARQ", 3) == 0 && host->on_data != NULL && + payload_len > 0) { + host->on_data(payload, payload_len, host->userdata); + } + + (void)ardop_host_send_cmd_frame(host, "RDY"); + off += frame_len; + } + + if (off > 0) { + memmove(host->data_rx, host->data_rx + off, host->data_len - off); + host->data_len -= off; + } +} + +static hybbx_result_t ardop_host_send_init(ardop_host_t *host) +{ + char cmd[96]; + hybbx_result_t rc; + + if (host == NULL) { + return HYBBX_ERR_INVALID; + } + + rc = ardop_host_send_cmd_frame(host, "INITIALIZE"); + if (rc != HYBBX_OK) { + return rc; + } + + snprintf(cmd, sizeof(cmd), "MYCALL %s", + host->cfg.mycall != NULL ? host->cfg.mycall : "NOCALL"); + rc = ardop_host_send_cmd_frame(host, cmd); + if (rc != HYBBX_OK) { + return rc; + } + + rc = ardop_host_send_cmd_frame(host, "PROTOCOLMODE ARQ"); + if (rc != HYBBX_OK) { + return rc; + } + + snprintf(cmd, sizeof(cmd), "ARQBW %s", + host->cfg.arq_bandwidth != NULL ? host->cfg.arq_bandwidth : + "500MAX"); + rc = ardop_host_send_cmd_frame(host, cmd); + if (rc != HYBBX_OK) { + return rc; + } + + rc = ardop_host_send_cmd_frame(host, + host->cfg.listen ? "LISTEN TRUE" : + "LISTEN FALSE"); + if (rc != HYBBX_OK) { + return rc; + } + + if (host->cfg.peer_call != NULL && host->cfg.peer_call[0] != '\0') { + snprintf(cmd, sizeof(cmd), "ARQCALL %s 5", host->cfg.peer_call); + rc = ardop_host_send_cmd_frame(host, cmd); + if (rc != HYBBX_OK) { + return rc; + } + } + + host->init_sent = 1; + hybbx_log_info("[%s] host init sent (external %s at %s:%u)", + host_log_tag(host), host_modem_name(host), + host->cfg.ardop_host != NULL ? host->cfg.ardop_host : "?", + host->cfg.ardop_port); + return HYBBX_OK; +} + +static void ardop_host_read_fd(int fd, unsigned char *buf, size_t *len, + size_t cap) +{ + ssize_t n; + + if (fd < 0 || buf == NULL || len == NULL || *len >= cap) { + return; + } + + n = recv(fd, buf + *len, cap - *len, 0); + if (n > 0) { + *len += (size_t)n; + } else if (n == 0) { + close(fd); + } +} + +ardop_host_t *ardop_host_create(const hybbx_ardop_config_t *cfg, + ardop_host_data_fn on_data, + ardop_host_event_fn on_event, + void *userdata, + const char *log_tag) +{ + ardop_host_t *host; + + if (cfg == NULL) { + return NULL; + } + + host = calloc(1, sizeof(*host)); + if (host == NULL) { + return NULL; + } + + host->cfg = *cfg; + host->on_data = on_data; + host->on_event = on_event; + host->userdata = userdata; + host->ctrl_fd = -1; + host->data_fd = -1; + host->log_tag = log_tag; + return host; +} + +void ardop_host_destroy(ardop_host_t *host) +{ + if (host == NULL) { + return; + } + + ardop_host_disconnect(host); + free(host); +} + +hybbx_result_t ardop_host_connect(ardop_host_t *host) +{ + const char *h; + unsigned port; + + if (host == NULL) { + return HYBBX_ERR_INVALID; + } + + ardop_host_disconnect(host); + + h = host->cfg.ardop_host; + if (h == NULL || h[0] == '\0') { + h = "127.0.0.1"; + } + port = host->cfg.ardop_port; + if (port == 0) { + port = HYBBX_ARDOP_DEFAULT_PORT; + } + + host->ctrl_fd = ardop_host_tcp_connect(h, port); + if (host->ctrl_fd < 0) { + hybbx_log_warn("[%s] cannot connect to %s control %s:%u", + host_log_tag(host), host_modem_name(host), h, port); + return HYBBX_ERR_IO; + } + + host->data_fd = ardop_host_tcp_connect(h, port + 1u); + if (host->data_fd < 0) { + hybbx_log_warn("[%s] cannot connect to %s data %s:%u", + host_log_tag(host), host_modem_name(host), h, port + 1u); + ardop_host_disconnect(host); + return HYBBX_ERR_IO; + } + + host->tnc_ready = 0; + host->init_sent = 0; + hybbx_log_info("[%s] connected to external %s %s:%u (data %u)", + host_log_tag(host), host_modem_name(host), h, port, port + 1u); + + (void)ardop_host_send_init(host); + { + unsigned spin; + + for (spin = 0; spin < 8u; spin++) { + ardop_host_poll(host); + if (host->tnc_ready) { + break; + } + } + } + return HYBBX_OK; +} + +void ardop_host_disconnect(ardop_host_t *host) +{ + if (host == NULL) { + return; + } + + if (host->ctrl_fd >= 0) { + close(host->ctrl_fd); + host->ctrl_fd = -1; + } + if (host->data_fd >= 0) { + close(host->data_fd); + host->data_fd = -1; + } + + host->ctrl_len = 0; + host->data_len = 0; + host->tnc_ready = 0; + host->link_up = 0; + host->init_sent = 0; + host->peer_call[0] = '\0'; +} + +void ardop_host_poll(ardop_host_t *host) +{ + struct pollfd pfds[2]; + int count = 0; + int pr; + + if (host == NULL || host->ctrl_fd < 0) { + return; + } + + pfds[0].fd = host->ctrl_fd; + pfds[0].events = POLLIN; + count = 1; + + if (host->data_fd >= 0) { + pfds[1].fd = host->data_fd; + pfds[1].events = POLLIN; + count = 2; + } + + pr = poll(pfds, (nfds_t)count, 0); + if (pr <= 0) { + if (host->tnc_ready && !host->init_sent) { + (void)ardop_host_send_init(host); + } + return; + } + + if ((pfds[0].revents & POLLIN) != 0) { + ardop_host_read_fd(host->ctrl_fd, host->ctrl_rx, &host->ctrl_len, + ARDOP_HOST_RX_MAX); + ardop_host_process_ctrl_rx(host); + } + + if (count > 1 && (pfds[1].revents & POLLIN) != 0) { + ardop_host_read_fd(host->data_fd, host->data_rx, &host->data_len, + ARDOP_HOST_RX_MAX); + ardop_host_process_data_rx(host); + } + + if (host->tnc_ready && !host->init_sent) { + (void)ardop_host_send_init(host); + } +} + +int ardop_host_link_up(const ardop_host_t *host) +{ + return host != NULL && host->link_up; +} + +int ardop_host_modem_connected(const ardop_host_t *host) +{ + return host != NULL && host->ctrl_fd >= 0; +} + +hybbx_result_t ardop_host_send_data(ardop_host_t *host, + const uint8_t *data, size_t len) +{ + unsigned char frame[ARDOP_HOST_FRAME_MAX]; + size_t frame_len; + + if (host == NULL || data == NULL || len == 0) { + return HYBBX_ERR_INVALID; + } + + if (!host->link_up || host->data_fd < 0) { + return HYBBX_ERR_BUSY; + } + + if (len > HYBBX_ARDOP_DATA_MAX) { + return HYBBX_ERR_INVALID; + } + + if (len + 8 > sizeof(frame)) { + return HYBBX_ERR_INVALID; + } + + frame[0] = 'D'; + frame[1] = ':'; + frame[2] = (unsigned char)((len >> 8) & 0xff); + frame[3] = (unsigned char)(len & 0xff); + memcpy(frame + 4, data, len); + + frame_len = 4u + len; + frame_len = ardop_crc16_append(frame, frame_len, sizeof(frame)); + if (frame_len == 0) { + return HYBBX_ERR_IO; + } + + return ardop_host_write_all(host->data_fd, frame, frame_len); +} diff --git a/plugins/ardop/ardop_host.h b/plugins/ardop/ardop_host.h new file mode 100644 index 0000000..4aee53d --- /dev/null +++ b/plugins/ardop/ardop_host.h @@ -0,0 +1,47 @@ +#ifndef ARDOP_HOST_H +#define ARDOP_HOST_H + +/** + * Minimal ARDOP TNC Host Interface client (TCP control + data ports). + * Implements the subset HyBBX needs — see docs/MANUAL.md § ARDOP. + */ + +#include "hybbx/types.h" +#include "hybbx/ardop.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct ardop_host ardop_host_t; + +typedef void (*ardop_host_data_fn)(const uint8_t *data, size_t len, void *ctx); +typedef void (*ardop_host_event_fn)(const char *event, const char *detail, + void *ctx); + +ardop_host_t *ardop_host_create(const hybbx_ardop_config_t *cfg, + ardop_host_data_fn on_data, + ardop_host_event_fn on_event, + void *userdata, + const char *log_tag); +void ardop_host_destroy(ardop_host_t *host); + +hybbx_result_t ardop_host_connect(ardop_host_t *host); +void ardop_host_disconnect(ardop_host_t *host); + +/** Drive I/O and state machine; call periodically from plugin poll thread. */ +void ardop_host_poll(ardop_host_t *host); + +int ardop_host_link_up(const ardop_host_t *host); + +/** Control TCP session to external ARDOPC is open. */ +int ardop_host_modem_connected(const ardop_host_t *host); + +hybbx_result_t ardop_host_send_data(ardop_host_t *host, + const uint8_t *data, size_t len); + +#ifdef __cplusplus +} +#endif + +#endif /* ARDOP_HOST_H */ diff --git a/plugins/ardop/ardop_link.c b/plugins/ardop/ardop_link.c new file mode 100644 index 0000000..e0500b9 --- /dev/null +++ b/plugins/ardop/ardop_link.c @@ -0,0 +1,446 @@ +#include "ardop_link.h" +#include "ardop_host.h" + +#include "hybbx/ardop.h" +#include "hybbx/circuit.h" +#include "hybbx/circuit_tcp.h" +#include "hybbx/circuit_balance.h" +#include "hybbx/crdop.h" +#include "hybbx/log.h" + +#include <pthread.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#if defined(_WIN32) +#include <windows.h> +#else +#include <sys/select.h> +#endif + +static const ardop_link_plugin_t *g_plugin; +static hybbx_ardop_config_t g_active_config; +static ardop_host_t *g_host; +static int g_circuit_fd = -1; +static hybbx_circuit_decoder_t g_circuit_dec; +static pthread_t g_poll_thread; +static volatile int g_running; +static int g_poll_thread_started; +static volatile int g_circuit_flow_paused; +static volatile int g_circuit_flow_cancel; +static unsigned g_modem_reconnect_backoff_ms; + +static const char *link_tag(void) +{ + return g_plugin != NULL && g_plugin->name != NULL ? g_plugin->name : "ardop"; +} + +static int circuit_uplink_allowed(void) +{ + return !g_circuit_flow_paused && !g_circuit_flow_cancel; +} + +static hybbx_result_t circuit_uplink_terminal(const uint8_t *payload, size_t len) +{ + uint8_t hbx[HYBBX_CIRCUIT_MAX_FRAME]; + size_t hbx_len; + + if (g_circuit_fd < 0 || len == 0) { + return HYBBX_ERR_IO; + } + + if (!circuit_uplink_allowed()) { + return HYBBX_OK; + } + + hbx_len = hybbx_circuit_encode(HYBBX_CIRCUIT_PROTO_TERMINAL, + HYBBX_CIRCUIT_FLAG_RX, + payload, len, hbx, sizeof(hbx)); + if (hbx_len == 0) { + return HYBBX_ERR_IO; + } + + return hybbx_circuit_link_write(g_circuit_fd, hbx, hbx_len); +} + +static void on_modem_data(const uint8_t *data, size_t len, void *userdata) +{ + (void)userdata; + + if (len == 0 || g_circuit_fd < 0 || !circuit_uplink_allowed()) { + return; + } + + (void)circuit_uplink_terminal(data, len); +} + +static void on_modem_event(const char *event, const char *detail, void *userdata) +{ + (void)userdata; + + if (event == NULL) { + return; + } + + if (strcmp(event, "connected") == 0) { + if (detail != NULL && detail[0] != '\0') { + hybbx_log_stats("[%s] RF link up (%s)", link_tag(), detail); + } else { + hybbx_log_stats("[%s] RF link up", link_tag()); + } + g_modem_reconnect_backoff_ms = 1000u; + return; + } + + if (strcmp(event, "disconnected") == 0) { + hybbx_log_stats("[%s] RF link down", link_tag()); + } +} + +static void on_circuit_flow_ctrl(hybbx_circuit_proto_t proto, uint16_t flags, + const uint8_t *payload, size_t len, + void *userdata) +{ + hybbx_circuit_balance_action_t action; + char reason[64]; + + (void)proto; + (void)flags; + (void)userdata; + + if (hybbx_circuit_flow_ctrl_parse((const char *)payload, len, &action, + reason, sizeof(reason)) != HYBBX_OK) { + return; + } + + switch (action) { + case HYBBX_CIRCUIT_BAL_PAUSE: + g_circuit_flow_paused = 1; + hybbx_log_stats("[%s] circuit flow pause (%s)", link_tag(), reason); + break; + case HYBBX_CIRCUIT_BAL_BREAK: + g_circuit_flow_paused = 0; + hybbx_circuit_decoder_init(&g_circuit_dec); + hybbx_log_stats("[%s] circuit flow break (%s)", link_tag(), reason); + break; + case HYBBX_CIRCUIT_BAL_CANCEL: + g_circuit_flow_cancel = 1; + hybbx_log_warn("[%s] circuit flow cancel (%s)", link_tag(), reason); + break; + case HYBBX_CIRCUIT_BAL_RESUME: + g_circuit_flow_paused = 0; + hybbx_log_stats("[%s] circuit flow resume (%s)", link_tag(), reason); + break; + default: + break; + } +} + +static void on_circuit_downlink(hybbx_circuit_proto_t proto, uint16_t flags, + const uint8_t *payload, size_t len, + void *userdata) +{ + (void)flags; + (void)userdata; + + if (proto == HYBBX_CIRCUIT_PROTO_FLOW_CTRL) { + on_circuit_flow_ctrl(proto, flags, payload, len, userdata); + return; + } + + if (g_host == NULL || len == 0 || g_circuit_flow_paused) { + return; + } + + if (proto != HYBBX_CIRCUIT_PROTO_TERMINAL) { + return; + } + + if (!ardop_host_link_up(g_host)) { + return; + } + + (void)ardop_host_send_data(g_host, payload, len); +} + +static void link_poll_sleep_ms(unsigned ms) +{ +#if defined(_WIN32) + Sleep(ms); +#else + struct timeval tv; + + tv.tv_sec = (time_t)(ms / 1000u); + tv.tv_usec = (suseconds_t)((ms % 1000u) * 1000u); + (void)select(0, NULL, NULL, NULL, &tv); +#endif +} + +static hybbx_result_t link_connect_circuit(void); + +static void link_maybe_reconnect_modem(void) +{ + static unsigned ticks; + + if (g_host == NULL) { + return; + } + + if (ardop_host_modem_connected(g_host)) { + ticks = 0; + return; + } + + ticks++; + if (ticks < (g_modem_reconnect_backoff_ms / 20u)) { + return; + } + + ticks = 0; + if (ardop_host_connect(g_host) != HYBBX_OK) { + if (g_modem_reconnect_backoff_ms < 30000u) { + g_modem_reconnect_backoff_ms += 1000u; + } + } +} + +static void *link_poll_thread(void *arg) +{ + uint8_t buf[512]; + + (void)arg; + + while (g_running) { + if (g_host != NULL) { + ardop_host_poll(g_host); + link_maybe_reconnect_modem(); + } + + if (g_circuit_fd >= 0) { + size_t read_len = 0; + hybbx_result_t rc = hybbx_circuit_link_read(g_circuit_fd, buf, + sizeof(buf), &read_len); + if (rc == HYBBX_OK && read_len > 0) { + hybbx_circuit_decoder_feed(&g_circuit_dec, buf, read_len, + on_circuit_downlink, NULL); + } + } else { + static unsigned circuit_ticks; + + circuit_ticks++; + if (circuit_ticks >= 50u) { + circuit_ticks = 0; + (void)link_connect_circuit(); + } + } + + if (g_circuit_flow_cancel) { + g_circuit_flow_cancel = 0; + g_circuit_flow_paused = 0; + if (g_circuit_fd >= 0) { + close(g_circuit_fd); + g_circuit_fd = -1; + } + (void)link_connect_circuit(); + } + + link_poll_sleep_ms(20); + } + + return NULL; +} + +static hybbx_result_t link_connect_circuit(void) +{ + const char *host = g_active_config.circuit_host; + unsigned port = g_active_config.circuit_port; + const char *link_id = g_active_config.link_id; + unsigned attempt; + + if (host == NULL || host[0] == '\0') { + host = "127.0.0.1"; + } + if (port == 0) { + port = HYBBX_CIRCUIT_DEFAULT_PORT; + } + if (link_id == NULL || link_id[0] == '\0') { + link_id = g_plugin != NULL && g_plugin->default_link_id != NULL ? + g_plugin->default_link_id : "ardop-link"; + } + + for (attempt = 0; attempt < 50; attempt++) { + hybbx_result_t rc = hybbx_circuit_link_connect(host, port, + &g_circuit_fd); + if (rc == HYBBX_OK) { + hybbx_circuit_decoder_init(&g_circuit_dec); + if (g_active_config.link_password != NULL && + g_active_config.link_password[0] != '\0') { + const char *link_role = g_active_config.link_role; + hybbx_circuit_link_qos_t qos; + char freq_buf[16]; + const char *freq_ptr = NULL; + + if (link_role == NULL || link_role[0] == '\0') { + link_role = "link"; + } + + memset(&qos, 0, sizeof(qos)); + qos.bandwidth = "low"; + qos.duplex = 1; + if (g_active_config.frequency_mhz > 0.0) { + snprintf(freq_buf, sizeof(freq_buf), "%.3f", + g_active_config.frequency_mhz); + freq_ptr = freq_buf; + } + qos.frequency_mhz = freq_ptr; + + rc = hybbx_circuit_link_authenticate_ex( + g_circuit_fd, g_active_config.link_password, + link_role, link_id, &qos); + if (rc != HYBBX_OK) { + close(g_circuit_fd); + g_circuit_fd = -1; + link_poll_sleep_ms(100); + continue; + } + } + hybbx_log_info("[%s] linked to internal circuit %s:%u (HBX)", + link_tag(), host, port); + return HYBBX_OK; + } + link_poll_sleep_ms(100); + } + + hybbx_log_warn("[%s] could not connect to internal circuit %s:%u", + link_tag(), host, port); + return HYBBX_ERR_IO; +} + +static void link_shutdown(void) +{ + g_running = 0; + if (g_poll_thread_started) { + pthread_join(g_poll_thread, NULL); + g_poll_thread_started = 0; + } + + if (g_circuit_fd >= 0) { + close(g_circuit_fd); + g_circuit_fd = -1; + } + + if (g_host != NULL) { + ardop_host_destroy(g_host); + g_host = NULL; + } + + if (g_plugin != NULL && g_plugin->config_free != NULL) { + g_plugin->config_free(&g_active_config); + } +} + +hybbx_result_t ardop_link_init(struct hybbx_service *service, + const ardop_link_plugin_t *plugin) +{ + (void)service; + g_plugin = plugin; + g_host = NULL; + g_circuit_fd = -1; + g_running = 0; + g_circuit_flow_paused = 0; + g_circuit_flow_cancel = 0; + g_modem_reconnect_backoff_ms = 1000u; + return HYBBX_OK; +} + +void ardop_link_shutdown(const ardop_link_plugin_t *plugin) +{ + (void)plugin; + link_shutdown(); +} + +hybbx_result_t ardop_link_start(const ardop_link_plugin_t *plugin, + const char *config) +{ + hybbx_result_t rc; + const char *modem_hint; + + if (plugin == NULL || plugin->parse_config == NULL) { + return HYBBX_ERR_INVALID; + } + + g_plugin = plugin; + link_shutdown(); + memset(&g_active_config, 0, sizeof(g_active_config)); + g_poll_thread = (pthread_t)0; + + rc = plugin->parse_config(config, &g_active_config); + if (rc != HYBBX_OK) { + hybbx_log_warn("[%s] invalid configuration", link_tag()); + return rc; + } + + hybbx_log_info("[%s] external modem %s:%u mycall=%s arq=%s profile=%s circuit=%s:%u " + "link_id=%s", + link_tag(), + g_active_config.ardop_host, + g_active_config.ardop_port, + g_active_config.mycall, + g_active_config.arq_bandwidth, + hybbx_crdop_profile_name(g_active_config.radio_profile), + g_active_config.circuit_host, + g_active_config.circuit_port, + g_active_config.link_id != NULL ? g_active_config.link_id : "?"); + + if (g_active_config.radio_profile == HYBBX_CRDOP_PROFILE_CB) { + hybbx_log_info("[%s] CB half-duplex profile (experimental Level 2)", + link_tag()); + } + if (g_active_config.frequency_mhz > 0.0) { + hybbx_log_info("[%s] frequency_mhz=%.3f", link_tag(), + g_active_config.frequency_mhz); + } + + modem_hint = plugin->modem_hint != NULL ? plugin->modem_hint : "ARDOPC"; + hybbx_log_info("[%s] operator must run external %s separately (e.g. ardopc TCPIP %u)", + link_tag(), modem_hint, g_active_config.ardop_port); + + g_host = ardop_host_create(&g_active_config, on_modem_data, on_modem_event, + NULL, link_tag()); + if (g_host == NULL) { + link_shutdown(); + return HYBBX_ERR_NOMEM; + } + + rc = ardop_host_connect(g_host); + if (rc != HYBBX_OK) { + hybbx_log_warn("[%s] external modem not reachable yet — will retry in poll loop", + link_tag()); + } + + rc = link_connect_circuit(); + if (rc != HYBBX_OK) { + hybbx_log_warn("[%s] circuit hub not reachable — HBX uplink deferred", + link_tag()); + } + + g_running = 1; + if (pthread_create(&g_poll_thread, NULL, link_poll_thread, NULL) != 0) { + hybbx_log_warn("[%s] poll thread failed", link_tag()); + link_shutdown(); + return HYBBX_ERR_IO; + } + g_poll_thread_started = 1; + + return HYBBX_OK; +} + +hybbx_result_t ardop_link_stop(const ardop_link_plugin_t *plugin) +{ + (void)plugin; + hybbx_log_info("[%s] stop", link_tag()); + link_shutdown(); + return HYBBX_OK; +} diff --git a/plugins/ardop/ardop_link.h b/plugins/ardop/ardop_link.h new file mode 100644 index 0000000..0f10dba --- /dev/null +++ b/plugins/ardop/ardop_link.h @@ -0,0 +1,37 @@ +#ifndef ARDOP_LINK_H +#define ARDOP_LINK_H + +#include "hybbx/plugin.h" +#include "hybbx/types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +struct hybbx_service; + +typedef hybbx_result_t (*ardop_link_config_fn)(const char *config, + void *config_out); + +typedef struct ardop_link_plugin { + const char *name; + hybbx_transport_kind_t kind; + const char *default_link_id; + const char *modem_hint; + ardop_link_config_fn parse_config; + void (*config_free)(void *config_out); + size_t config_size; +} ardop_link_plugin_t; + +hybbx_result_t ardop_link_init(struct hybbx_service *service, + const ardop_link_plugin_t *plugin); +void ardop_link_shutdown(const ardop_link_plugin_t *plugin); +hybbx_result_t ardop_link_start(const ardop_link_plugin_t *plugin, + const char *config); +hybbx_result_t ardop_link_stop(const ardop_link_plugin_t *plugin); + +#ifdef __cplusplus +} +#endif + +#endif /* ARDOP_LINK_H */ diff --git a/plugins/baycom/CMakeLists.txt b/plugins/baycom/CMakeLists.txt new file mode 100644 index 0000000..8a2dc2d --- /dev/null +++ b/plugins/baycom/CMakeLists.txt @@ -0,0 +1,36 @@ +add_library(hybbx_plugin_baycom STATIC + baycom.c + baycom_config.c + baycom_modem.c + baycom_kernel.c + baycom_kiss.c + ../packet_radio/ax25.c + ../packet_radio/kiss.c + ../packet_radio/serial_port.c +) + +target_include_directories(hybbx_plugin_baycom + PRIVATE + ${CMAKE_SOURCE_DIR}/include + ${CMAKE_SOURCE_DIR}/plugins/packet_radio + ${CMAKE_CURRENT_SOURCE_DIR} +) + +target_compile_definitions(hybbx_plugin_baycom + PRIVATE HYBBX_PLUGIN_BUILD +) + +find_package(Threads REQUIRED) +target_link_libraries(hybbx_plugin_baycom PRIVATE hybbx_core Threads::Threads) +if(HYBBX_PLATFORM_AMIGAOS) + target_link_libraries(hybbx_plugin_baycom PRIVATE amiga) +endif() +hybbx_link_plugin_instances(hybbx_plugin_baycom HYBBX_HAVE_PLUGIN_BAYCOM) +hybbx_apply_hardening(hybbx_plugin_baycom) + +if(HYBBX_INSTALL_DEV) +install(TARGETS hybbx_plugin_baycom + ARCHIVE DESTINATION ${HYBBX_PLUGIN_INSTALL_DIR} + LIBRARY DESTINATION ${HYBBX_PLUGIN_INSTALL_DIR} +) +endif() diff --git a/plugins/baycom/baycom.c b/plugins/baycom/baycom.c new file mode 100644 index 0000000..e2fb5c9 --- /dev/null +++ b/plugins/baycom/baycom.c @@ -0,0 +1,623 @@ +/* + * baycom — BayCom PR-Stack link adapter (Linux kernel SER12/PAR96/EPP or KISS serial). + * INI: [transport.baycom] or [transport.baycomN]. See docs/BAYCOM.md. + */ +#include "hybbx/plugin.h" +#include "hybbx/service.h" +#include "hybbx/baycom.h" +#include "hybbx/circuit.h" +#include "hybbx/circuit_tcp.h" +#include "hybbx/circuit_balance.h" +#include "hybbx/limits.h" +#include "hybbx/max25.h" +#include "hybbx/util.h" +#include "hybbx/log.h" +#include "hybbx/ax25.h" + +#include "baycom_modem.h" + +#include <pthread.h> +#include <stdio.h> +#include <string.h> +#include <stdlib.h> +#include <unistd.h> + +#if defined(_WIN32) +#include <windows.h> +#else +#include <sys/select.h> +#endif + +typedef struct baycom_instance { + hybbx_baycom_config_t config; + baycom_modem_t *modem; + int circuit_fd; + hybbx_circuit_decoder_t circuit_dec; + volatile int flow_paused; + volatile int flow_cancel; + unsigned circuit_reconnect_polls; + unsigned index; +} baycom_instance_t; + +static hybbx_service_t *g_service; + +static baycom_instance_t g_instances[HYBBX_BAYCOM_MAX_INSTANCES]; +static unsigned g_instance_count; +static pthread_t g_poll_thread; +static volatile int g_running; +static int g_poll_thread_started; + +static void baycom_poll_sleep_ms(unsigned ms); +static hybbx_result_t instance_connect_circuit(baycom_instance_t *inst); +static void on_circuit_downlink(hybbx_circuit_proto_t proto, uint16_t flags, + const uint8_t *payload, size_t len, + void *userdata); + +static int instance_circuit_uplink_allowed(const baycom_instance_t *inst) +{ + return inst != NULL && !inst->flow_paused && !inst->flow_cancel; +} + +static hybbx_result_t instance_circuit_uplink(baycom_instance_t *inst, + const uint8_t *frame, + size_t frame_len) +{ + uint8_t hbx[HYBBX_CIRCUIT_MAX_FRAME]; + size_t hbx_len; + + if (inst == NULL || inst->circuit_fd < 0 || frame_len == 0) { + return HYBBX_ERR_IO; + } + + if (!instance_circuit_uplink_allowed(inst)) { + return HYBBX_OK; + } + + hbx_len = hybbx_circuit_encode_ax25(frame, frame_len, HYBBX_CIRCUIT_FLAG_RX, + hbx, sizeof(hbx)); + if (hbx_len == 0) { + return HYBBX_ERR_IO; + } + + return hybbx_circuit_link_write(inst->circuit_fd, hbx, hbx_len); +} + +static void instance_format_call(const hybbx_ax25_address_t *addr, + char *out, size_t out_len) +{ + if (out == NULL || out_len == 0) { + return; + } + + out[0] = '\0'; + if (addr == NULL || addr->call[0] == '\0') { + return; + } + + if (addr->ssid > 0u) { + unsigned ssid = addr->ssid; + + if (ssid > HYBBX_AX25_SSID_MAX) { + ssid = HYBBX_AX25_SSID_MAX; + } + snprintf(out, out_len, "%s-%u", addr->call, ssid); + } else { + hybbx_strlcpy(out, addr->call, out_len); + } +} + +static void instance_note_rf_activity(const baycom_instance_t *inst) +{ + hybbx_circuit_hub_t *hub; + const char *link_id; + + if (inst == NULL || g_service == NULL) { + return; + } + + hub = hybbx_service_circuit_hub(g_service); + link_id = inst->config.link_id; + if (hub != NULL && link_id != NULL && link_id[0] != '\0') { + hybbx_circuit_hub_note_rf_activity(hub, link_id); + } +} + +static void instance_log_rf_tx(const baycom_instance_t *inst, + hybbx_result_t rc, + const hybbx_ax25_path_t *path, + size_t payload_len) +{ + char src[HYBBX_AX25_CALL_MAX + 8]; + char dst[HYBBX_AX25_CALL_MAX + 8]; + + if (inst == NULL) { + return; + } + + instance_format_call(path != NULL ? &path->source : NULL, src, sizeof(src)); + instance_format_call(path != NULL ? &path->dest : NULL, dst, sizeof(dst)); + + if (rc == HYBBX_OK) { + hybbx_log_stats("[baycom%u] RF TX %s>%s (%zu bytes)", + inst->index + 1, src, dst, payload_len); + } else { + hybbx_log_warn("[baycom%u] RF TX failed %s>%s (%zu bytes, rc=%d)", + inst->index + 1, src, dst, payload_len, (int)rc); + } +} + +static void instance_circuit_disconnect(baycom_instance_t *inst) +{ + if (inst == NULL) { + return; + } + + if (inst->circuit_fd >= 0) { + close(inst->circuit_fd); + inst->circuit_fd = -1; + } + hybbx_circuit_decoder_init(&inst->circuit_dec); + inst->circuit_reconnect_polls = 0; +} + +static void instance_circuit_reconnect(baycom_instance_t *inst) +{ + if (inst == NULL) { + return; + } + + instance_circuit_disconnect(inst); + baycom_poll_sleep_ms(250); + if (instance_connect_circuit(inst) == HYBBX_OK) { + hybbx_log_info("[baycom%u] circuit reconnected", inst->index + 1); + } +} + +static void instance_drain_circuit(baycom_instance_t *inst) +{ + uint8_t buf[512]; + + if (inst == NULL || inst->circuit_fd < 0) { + return; + } + + for (;;) { + size_t read_len = 0; + hybbx_result_t rc = hybbx_circuit_link_read(inst->circuit_fd, buf, + sizeof(buf), &read_len); + + if (rc == HYBBX_ERR_IO) { + hybbx_log_warn("[baycom%u] circuit disconnected — reconnecting", + inst->index + 1); + instance_circuit_reconnect(inst); + return; + } + if (rc != HYBBX_OK || read_len == 0) { + break; + } + + hybbx_circuit_decoder_feed(&inst->circuit_dec, buf, read_len, + on_circuit_downlink, inst); + } +} + +static void on_modem_ax25_frame(const uint8_t *frame, size_t len, void *userdata) +{ + baycom_instance_t *inst = (baycom_instance_t *)userdata; + + if (len == 0 || inst == NULL) { + return; + } + + (void)instance_circuit_uplink(inst, frame, len); +} + +static void baycom_poll_sleep_ms(unsigned ms) +{ +#if defined(_WIN32) + Sleep(ms); +#else + struct timeval tv; + + tv.tv_sec = (time_t)(ms / 1000u); + tv.tv_usec = (suseconds_t)((ms % 1000u) * 1000u); + (void)select(0, NULL, NULL, NULL, &tv); +#endif +} + +static hybbx_result_t instance_connect_circuit(baycom_instance_t *inst) +{ + const char *host; + unsigned port; + unsigned attempt; + + if (inst == NULL) { + return HYBBX_ERR_INVALID; + } + + host = inst->config.circuit_host; + port = inst->config.circuit_port; + + if (host == NULL || host[0] == '\0') { + host = "127.0.0.1"; + } + if (port == 0) { + port = HYBBX_CIRCUIT_DEFAULT_PORT; + } + + for (attempt = 0; attempt < 50; attempt++) { + hybbx_result_t rc = hybbx_circuit_link_connect(host, port, + &inst->circuit_fd); + if (rc == HYBBX_OK) { + hybbx_circuit_decoder_init(&inst->circuit_dec); + if (inst->config.link_password != NULL && + inst->config.link_password[0] != '\0') { + const char *link_id = inst->config.link_id; + const char *link_role = inst->config.link_role; + hybbx_circuit_link_qos_t qos; + + if (link_id == NULL || link_id[0] == '\0') { + link_id = "baycom-link"; + } + if (link_role == NULL || link_role[0] == '\0') { + link_role = "link"; + } + + memset(&qos, 0, sizeof(qos)); + qos.baud = inst->config.radio_baud; + qos.duplex = inst->config.full_duplex ? 2 : 1; + qos.bandwidth = "low"; + qos.frequency_mhz = inst->config.frequency_mhz; + + rc = hybbx_circuit_link_authenticate_ex( + inst->circuit_fd, inst->config.link_password, + link_role, link_id, &qos); + if (rc != HYBBX_OK) { + close(inst->circuit_fd); + inst->circuit_fd = -1; + baycom_poll_sleep_ms(100); + continue; + } + } + hybbx_log_info("[baycom%u] linked to internal circuit %s:%u (HBX)", + inst->index + 1, host, port); + return HYBBX_OK; + } + baycom_poll_sleep_ms(100); + } + + hybbx_log_warn("[baycom%u] could not connect to internal circuit %s:%u", + inst->index + 1, host, port); + return HYBBX_ERR_IO; +} + +static void instance_on_circuit_flow_ctrl(baycom_instance_t *inst, + const uint8_t *payload, size_t len) +{ + hybbx_circuit_balance_action_t action; + char reason[64]; + + if (inst == NULL) { + return; + } + + if (hybbx_circuit_flow_ctrl_parse((const char *)payload, len, &action, + reason, sizeof(reason)) != HYBBX_OK) { + return; + } + + switch (action) { + case HYBBX_CIRCUIT_BAL_PAUSE: + inst->flow_paused = 1; + hybbx_log_stats("[baycom%u] circuit flow pause (%s)", inst->index + 1, reason); + break; + case HYBBX_CIRCUIT_BAL_BREAK: + inst->flow_paused = 0; + hybbx_circuit_decoder_init(&inst->circuit_dec); + hybbx_log_stats("[baycom%u] circuit flow break (%s)", inst->index + 1, reason); + break; + case HYBBX_CIRCUIT_BAL_CANCEL: + inst->flow_cancel = 1; + hybbx_log_warn("[baycom%u] circuit flow cancel (%s)", + inst->index + 1, reason); + break; + case HYBBX_CIRCUIT_BAL_RESUME: + inst->flow_paused = 0; + hybbx_log_stats("[baycom%u] circuit flow resume (%s)", inst->index + 1, reason); + break; + default: + break; + } +} + +static void on_circuit_downlink(hybbx_circuit_proto_t proto, uint16_t flags, + const uint8_t *payload, size_t len, + void *userdata) +{ + baycom_instance_t *inst = (baycom_instance_t *)userdata; + + (void)flags; + + if (inst == NULL) { + return; + } + + if (proto == HYBBX_CIRCUIT_PROTO_FLOW_CTRL) { + instance_on_circuit_flow_ctrl(inst, payload, len); + return; + } + + if (inst->modem == NULL || len == 0 || inst->flow_paused) { + return; + } + + if (proto == HYBBX_CIRCUIT_PROTO_AX25) { + hybbx_result_t rc = baycom_modem_send_frame(inst->modem, payload, len); + hybbx_ax25_path_t path; + uint8_t ui[HYBBX_AX25_PAYLOAD_MAX]; + size_t ui_len = hybbx_ax25_parse_ui(payload, len, &path, ui, sizeof(ui)); + + if (rc == HYBBX_OK) { + instance_note_rf_activity(inst); + if (ui_len > 0) { + instance_log_rf_tx(inst, rc, &path, ui_len); + } + } else { + hybbx_log_warn("[baycom%u] RF TX frame failed (rc=%d, %zu bytes)", + inst->index + 1, (int)rc, len); + } + } +} + +static void instance_shutdown(baycom_instance_t *inst) +{ + if (inst == NULL) { + return; + } + + if (inst->modem != NULL) { + baycom_modem_close(inst->modem); + inst->modem = NULL; + } + + if (inst->circuit_fd >= 0) { + close(inst->circuit_fd); + inst->circuit_fd = -1; + } + + hybbx_baycom_config_free(&inst->config); + memset(inst, 0, sizeof(*inst)); + inst->circuit_fd = -1; +} + +static void *baycom_poll_thread(void *arg) +{ + unsigned i; + + (void)arg; + + while (g_running) { + for (i = 0; i < g_instance_count; i++) { + baycom_instance_t *inst = &g_instances[i]; + + if (inst->modem != NULL) { + (void)baycom_modem_poll(inst->modem); + } + + instance_drain_circuit(inst); + + if (inst->circuit_fd < 0) { + inst->circuit_reconnect_polls++; + if (inst->circuit_reconnect_polls >= 50u) { + inst->circuit_reconnect_polls = 0; + instance_circuit_reconnect(inst); + } + } else { + inst->circuit_reconnect_polls = 0; + } + + if (inst->flow_cancel) { + inst->flow_cancel = 0; + inst->flow_paused = 0; + if (inst->circuit_fd >= 0) { + close(inst->circuit_fd); + inst->circuit_fd = -1; + } + (void)instance_connect_circuit(inst); + } + } + + baycom_poll_sleep_ms(20); + } + + return NULL; +} + +static hybbx_result_t instance_start(baycom_instance_t *inst, + const char *config, + unsigned index) +{ + hybbx_result_t rc; + + if (inst == NULL || config == NULL) { + return HYBBX_ERR_INVALID; + } + + memset(inst, 0, sizeof(*inst)); + inst->circuit_fd = -1; + inst->index = index; + + rc = hybbx_baycom_config_parse(config, &inst->config); + if (rc != HYBBX_OK) { + hybbx_log_warn("[baycom%u] invalid configuration", index + 1); + instance_shutdown(inst); + return rc; + } + + { + char msg[512]; + size_t pos = 0; + + pos += (size_t)snprintf(msg + pos, sizeof(msg) - pos, + "[baycom%u] backend=%s mode=%s", + index + 1, + hybbx_baycom_backend_name(inst->config.backend), + hybbx_baycom_modem_mode_name(inst->config.mode)); + + if (inst->config.backend == HYBBX_BAYCOM_BACKEND_KERNEL) { + pos += (size_t)snprintf(msg + pos, sizeof(msg) - pos, + " interface=%s module=%s iobase=0x%x irq=%u radio_baud=%u", + inst->config.interface != NULL ? inst->config.interface : "?", + inst->config.kernel_module != NULL ? inst->config.kernel_module + : "?", + inst->config.iobase, inst->config.irq, inst->config.radio_baud); + } else { + pos += (size_t)snprintf(msg + pos, sizeof(msg) - pos, + " device=%s serial_baud=%u", + inst->config.device != NULL ? inst->config.device : "?", + inst->config.serial_baud); + } + + pos += (size_t)snprintf(msg + pos, sizeof(msg) - pos, + " circuit=%s:%u (HBX over internal TCP)", + inst->config.circuit_host, inst->config.circuit_port); + hybbx_log_info("%s", msg); + } + + if (inst->config.frequency_mhz != NULL && + inst->config.frequency_mhz[0] != '\0') { + hybbx_log_info("[baycom%u] frequency_mhz=%s", index + 1, + inst->config.frequency_mhz); + } + + rc = instance_connect_circuit(inst); + if (rc != HYBBX_OK) { + instance_shutdown(inst); + return rc; + } + + rc = baycom_modem_open(&inst->modem, &inst->config, on_modem_ax25_frame, + inst); + if (rc != HYBBX_OK) { + hybbx_log_warn("[baycom%u] modem open failed (%s)", index + 1, + hybbx_result_name(rc)); + instance_shutdown(inst); + return rc; + } + + return HYBBX_OK; +} + +static hybbx_result_t baycom_init(hybbx_service_t *service) +{ + g_service = service; + g_instance_count = 0; + g_running = 0; + return HYBBX_OK; +} + +static void baycom_shutdown(void) +{ + unsigned i; + + g_running = 0; + if (g_poll_thread_started) { + pthread_join(g_poll_thread, NULL); + g_poll_thread_started = 0; + } + + for (i = 0; i < g_instance_count; i++) { + instance_shutdown(&g_instances[i]); + } + g_instance_count = 0; +} + +static hybbx_result_t baycom_start(const char *config) +{ + hybbx_max25_config_t max25; + hybbx_result_t rc = HYBBX_OK; + unsigned started = 0; + const char *cursor; + const char *cursor_after_max25; + + if (config == NULL) { + return HYBBX_ERR_INVALID; + } + + baycom_shutdown(); + g_poll_thread = (pthread_t)0; + + cursor_after_max25 = hybbx_max25_config_skip_prefix(config, &max25); + if (max25.check && cursor_after_max25 != config) { + rc = hybbx_max25_wait_ready(&max25); + if (rc != HYBBX_OK) { + hybbx_log_warn("[baycom] local RF requires max25d — continuing without BayCom"); + return HYBBX_OK; + } + } + + cursor = cursor_after_max25; + while (*cursor != '\0' && g_instance_count < HYBBX_BAYCOM_MAX_INSTANCES) { + char scratch[4096]; + const char *sep = strchr(cursor, HYBBX_BAYCOM_INSTANCE_SEP); + size_t chunk_len; + + if (sep != NULL) { + chunk_len = (size_t)(sep - cursor); + } else { + chunk_len = strlen(cursor); + } + + if (chunk_len >= sizeof(scratch)) { + return HYBBX_ERR_INVALID; + } + + memcpy(scratch, cursor, chunk_len); + scratch[chunk_len] = '\0'; + + rc = instance_start(&g_instances[g_instance_count], scratch, + g_instance_count); + if (rc == HYBBX_OK) { + g_instance_count++; + started++; + } + + if (sep == NULL) { + break; + } + cursor = sep + 1; + } + + if (started == 0) { + /* Soft-fail: keep HyBBX up (TNCs/telnet/…) if BayCom/based open fails. */ + hybbx_log_warn("[baycom] no instances started (%s) — continuing without BayCom", + rc != HYBBX_OK ? hybbx_result_name(rc) : "invalid"); + return HYBBX_OK; + } + + g_running = 1; + if (pthread_create(&g_poll_thread, NULL, baycom_poll_thread, NULL) != 0) { + baycom_shutdown(); + return HYBBX_ERR_IO; + } + g_poll_thread_started = 1; + + return HYBBX_OK; +} + +static hybbx_result_t baycom_stop(void) +{ + baycom_shutdown(); + return HYBBX_OK; +} + +const hybbx_transport_plugin_t hybbx_plugin_baycom = { + .name = "baycom", + .kind = HYBBX_TRANSPORT_BAYCOM, + .version = 1, + .init = baycom_init, + .shutdown = baycom_shutdown, + .start = baycom_start, + .stop = baycom_stop, + .write = NULL, +}; diff --git a/plugins/baycom/baycom_config.c b/plugins/baycom/baycom_config.c new file mode 100644 index 0000000..4e740f1 --- /dev/null +++ b/plugins/baycom/baycom_config.c @@ -0,0 +1,395 @@ +#include "hybbx/baycom.h" +#include "hybbx/circuit.h" +#include "hybbx/util.h" + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +static char *baycom_strdup(const char *s) +{ + size_t len; + char *copy; + + if (s == NULL) { + return NULL; + } + + len = strlen(s) + 1; + copy = malloc(len); + if (copy != NULL) { + memcpy(copy, s, len); + } + return copy; +} + +static char *baycom_normalize_kiss_path(const char *device) +{ + static const struct { + const char *from; + const char *to; + } map[] = { + {"/tmp/bcpr/", "/tmp/max25-bcpr/"}, + {"/tmp/bcpr", "/tmp/max25-bcpr"}, + {"/var/run/bcpr/", "/tmp/max25-bcpr/"}, + {"/var/run/bcpr", "/tmp/max25-bcpr"}, + }; + size_t i; + size_t flen; + char *norm; + + if (device == NULL || device[0] == '\0') { + return NULL; + } + + for (i = 0; i < sizeof(map) / sizeof(map[0]); i++) { + flen = strlen(map[i].from); + if (strncmp(device, map[i].from, flen) != 0) { + continue; + } + if (device[flen] != '\0' && device[flen] != '/') { + continue; + } + norm = malloc(strlen(map[i].to) + strlen(device + flen) + 1); + if (norm == NULL) { + return baycom_strdup(device); + } + snprintf(norm, strlen(map[i].to) + strlen(device + flen) + 1, "%s%s", + map[i].to, device + flen); + return norm; + } + + return baycom_strdup(device); +} + +static int str_ieq(const char *a, const char *b) +{ + if (a == NULL || b == NULL) { + return 0; + } + + while (*a != '\0' && *b != '\0') { + char ca = (char)(*a >= 'A' && *a <= 'Z' ? *a + 32 : *a); + char cb = (char)(*b >= 'A' && *b <= 'Z' ? *b + 32 : *b); + + if (ca != cb) { + return 0; + } + a++; + b++; + } + + return *a == '\0' && *b == '\0'; +} + +static const char *find_kv(const char *config, const char *key, + char *scratch, size_t scratch_len) +{ + const char *cursor = config; + size_t key_len = strlen(key); + + if (config == NULL || key == NULL) { + return NULL; + } + + while (*cursor != '\0') { + const char *sep = strchr(cursor, ';'); + const char *end = sep != NULL ? sep : cursor + strlen(cursor); + const char *eq = strchr(cursor, '='); + + if (eq != NULL && eq < end && (size_t)(eq - cursor) == key_len && + strncmp(cursor, key, key_len) == 0) { + const char *value = eq + 1; + size_t value_len = (size_t)(end - value); + + if (value_len >= scratch_len) { + value_len = scratch_len - 1; + } + memcpy(scratch, value, value_len); + scratch[value_len] = '\0'; + return scratch; + } + + if (sep == NULL) { + break; + } + cursor = sep + 1; + } + + return NULL; +} + +const char *hybbx_baycom_backend_name(hybbx_baycom_backend_t backend) +{ + switch (backend) { + case HYBBX_BAYCOM_BACKEND_KISS: + return "kiss"; + case HYBBX_BAYCOM_BACKEND_KERNEL: + default: + return "kernel"; + } +} + +const char *hybbx_baycom_modem_mode_name(hybbx_baycom_modem_mode_t mode) +{ + switch (mode) { + case HYBBX_BAYCOM_MODE_SER12_SOFTDCD: + return "ser12*"; + case HYBBX_BAYCOM_MODE_SER12_INV: + return "ser12+"; + case HYBBX_BAYCOM_MODE_SER12HDX: + return "ser12hdx"; + case HYBBX_BAYCOM_MODE_PAR96: + return "par96"; + case HYBBX_BAYCOM_MODE_PAR96_SOFTDCD: + return "par96*"; + case HYBBX_BAYCOM_MODE_EPP: + return "epp"; + case HYBBX_BAYCOM_MODE_SER12: + default: + return "ser12"; + } +} + +static hybbx_baycom_backend_t parse_backend(const char *value) +{ + if (value == NULL || str_ieq(value, "kernel") || str_ieq(value, "hdlc") || + str_ieq(value, "prstack") || str_ieq(value, "l2")) { + return HYBBX_BAYCOM_BACKEND_KERNEL; + } + + if (str_ieq(value, "kiss") || str_ieq(value, "serial")) { + return HYBBX_BAYCOM_BACKEND_KISS; + } + + return HYBBX_BAYCOM_BACKEND_KERNEL; +} + +static hybbx_baycom_modem_mode_t parse_mode(const char *value) +{ + if (value == NULL || str_ieq(value, "ser12")) { + return HYBBX_BAYCOM_MODE_SER12; + } + if (str_ieq(value, "ser12*") || str_ieq(value, "ser12_softdcd")) { + return HYBBX_BAYCOM_MODE_SER12_SOFTDCD; + } + if (str_ieq(value, "ser12+") || str_ieq(value, "ser12_inv")) { + return HYBBX_BAYCOM_MODE_SER12_INV; + } + if (str_ieq(value, "ser12hdx") || str_ieq(value, "ser12_hdx") || + str_ieq(value, "hdx")) { + return HYBBX_BAYCOM_MODE_SER12HDX; + } + if (str_ieq(value, "par96")) { + return HYBBX_BAYCOM_MODE_PAR96; + } + if (str_ieq(value, "par96*") || str_ieq(value, "par96_softdcd")) { + return HYBBX_BAYCOM_MODE_PAR96_SOFTDCD; + } + if (str_ieq(value, "epp") || str_ieq(value, "par97") || + str_ieq(value, "baycom_epp")) { + return HYBBX_BAYCOM_MODE_EPP; + } + + return HYBBX_BAYCOM_MODE_SER12_SOFTDCD; +} + +void hybbx_baycom_config_free(hybbx_baycom_config_t *config) +{ + if (config == NULL) { + return; + } + + free(config->interface); + free(config->kernel_module); + free(config->device); + free(config->mycall); + free(config->circuit_host); + free(config->link_id); + free(config->link_password); + free(config->link_role); + free(config->frequency_mhz); + memset(config, 0, sizeof(*config)); +} + +static void apply_mode_defaults(hybbx_baycom_config_t *out) +{ + if (out->interface == NULL) { + switch (out->mode) { + case HYBBX_BAYCOM_MODE_SER12HDX: + out->interface = baycom_strdup("bcsh0"); + break; + case HYBBX_BAYCOM_MODE_PAR96: + case HYBBX_BAYCOM_MODE_PAR96_SOFTDCD: + out->interface = baycom_strdup("bcp0"); + break; + case HYBBX_BAYCOM_MODE_EPP: + out->interface = baycom_strdup("bce0"); + break; + case HYBBX_BAYCOM_MODE_SER12: + case HYBBX_BAYCOM_MODE_SER12_SOFTDCD: + case HYBBX_BAYCOM_MODE_SER12_INV: + default: + out->interface = baycom_strdup(HYBBX_BAYCOM_DEFAULT_INTERFACE); + break; + } + } + + if (out->kernel_module == NULL) { + switch (out->mode) { + case HYBBX_BAYCOM_MODE_SER12HDX: + out->kernel_module = baycom_strdup("baycom_ser_hdx"); + break; + case HYBBX_BAYCOM_MODE_PAR96: + case HYBBX_BAYCOM_MODE_PAR96_SOFTDCD: + out->kernel_module = baycom_strdup("baycom_par"); + break; + case HYBBX_BAYCOM_MODE_EPP: + out->kernel_module = baycom_strdup("baycom_epp"); + break; + default: + out->kernel_module = baycom_strdup("baycom_ser_fdx"); + break; + } + } + + if (out->device == NULL) { + if (out->backend == HYBBX_BAYCOM_BACKEND_KISS) { + out->device = baycom_strdup("/tmp/max25-bcpr/kiss-bc0"); + } else { + out->device = baycom_strdup("/dev/ttyS0"); + } + } +} + +hybbx_result_t hybbx_baycom_config_parse(const char *config, + hybbx_baycom_config_t *out) +{ + char scratch[128]; + const char *value; + + if (out == NULL) { + return HYBBX_ERR_INVALID; + } + + hybbx_baycom_config_free(out); + + if (config == NULL) { + config = ""; + } + + value = find_kv(config, "backend", scratch, sizeof(scratch)); + out->backend = parse_backend(value); + + value = find_kv(config, "mode", scratch, sizeof(scratch)); + out->mode = parse_mode(value); + + value = find_kv(config, "interface", scratch, sizeof(scratch)); + if (value != NULL) { + out->interface = baycom_strdup(value); + } + + value = find_kv(config, "kernel_module", scratch, sizeof(scratch)); + if (value != NULL) { + out->kernel_module = baycom_strdup(value); + } + + value = find_kv(config, "iobase", scratch, sizeof(scratch)); + out->iobase = value != NULL ? (unsigned)strtoul(value, NULL, 0) + : HYBBX_BAYCOM_DEFAULT_IOBASE; + + value = find_kv(config, "irq", scratch, sizeof(scratch)); + out->irq = value != NULL ? (unsigned)strtoul(value, NULL, 0) + : HYBBX_BAYCOM_DEFAULT_IRQ; + + value = find_kv(config, "radio_baud", scratch, sizeof(scratch)); + if (value == NULL) { + value = find_kv(config, "baud", scratch, sizeof(scratch)); + } + out->radio_baud = value != NULL ? (unsigned)strtoul(value, NULL, 0) + : HYBBX_BAYCOM_DEFAULT_RADIO_BAUD; + + value = find_kv(config, "kernel_autoload", scratch, sizeof(scratch)); + out->kernel_autoload = value != NULL ? hybbx_parse_bool(value, 0) : 0; + + value = find_kv(config, "txdelay", scratch, sizeof(scratch)); + out->txdelay = value != NULL ? (unsigned)strtoul(value, NULL, 0) : 30u; + + value = find_kv(config, "txtail", scratch, sizeof(scratch)); + out->txtail = value != NULL ? (unsigned)strtoul(value, NULL, 0) : 1u; + + value = find_kv(config, "slot", scratch, sizeof(scratch)); + if (value == NULL) { + value = find_kv(config, "slottime", scratch, sizeof(scratch)); + } + out->slot = value != NULL ? (unsigned)strtoul(value, NULL, 0) : 10u; + + value = find_kv(config, "persist", scratch, sizeof(scratch)); + if (value == NULL) { + value = find_kv(config, "ppersist", scratch, sizeof(scratch)); + } + out->persist = value != NULL ? (unsigned)strtoul(value, NULL, 0) : 128u; + + value = find_kv(config, "full_duplex", scratch, sizeof(scratch)); + if (value != NULL) { + out->full_duplex = hybbx_parse_bool(value, 0); + } else { + value = find_kv(config, "duplex", scratch, sizeof(scratch)); + out->full_duplex = value != NULL && str_ieq(value, "full"); + } + + value = find_kv(config, "device", scratch, sizeof(scratch)); + if (value != NULL) { + out->device = baycom_strdup(value); + } + + value = find_kv(config, "serial_baud", scratch, sizeof(scratch)); + if (value == NULL) { + value = find_kv(config, "host_baud", scratch, sizeof(scratch)); + } + out->serial_baud = value != NULL ? (unsigned)strtoul(value, NULL, 0) : 1200u; + + value = find_kv(config, "mycall", scratch, sizeof(scratch)); + if (value != NULL) { + out->mycall = baycom_strdup(value); + } + + value = find_kv(config, "circuit_host", scratch, sizeof(scratch)); + out->circuit_host = baycom_strdup(value != NULL ? value : "127.0.0.1"); + + value = find_kv(config, "circuit_port", scratch, sizeof(scratch)); + out->circuit_port = value != NULL ? (unsigned)strtoul(value, NULL, 0) + : HYBBX_CIRCUIT_DEFAULT_PORT; + + value = find_kv(config, "link_id", scratch, sizeof(scratch)); + out->link_id = baycom_strdup(value != NULL ? value : "baycom-link"); + + value = find_kv(config, "link_password", scratch, sizeof(scratch)); + out->link_password = baycom_strdup(value != NULL ? value : ""); + + value = find_kv(config, "link_role", scratch, sizeof(scratch)); + out->link_role = baycom_strdup(value != NULL ? value : "link"); + + value = find_kv(config, "frequency_mhz", scratch, sizeof(scratch)); + if (value != NULL) { + out->frequency_mhz = baycom_strdup(value); + } + + apply_mode_defaults(out); + + if (out->backend == HYBBX_BAYCOM_BACKEND_KISS && out->device != NULL) { + char *norm = baycom_normalize_kiss_path(out->device); + + if (norm != NULL) { + free(out->device); + out->device = norm; + } + } + + if (out->circuit_host == NULL || out->link_id == NULL) { + hybbx_baycom_config_free(out); + return HYBBX_ERR_NOMEM; + } + + return HYBBX_OK; +} diff --git a/plugins/baycom/baycom_kernel.c b/plugins/baycom/baycom_kernel.c new file mode 100644 index 0000000..0babc08 --- /dev/null +++ b/plugins/baycom/baycom_kernel.c @@ -0,0 +1,472 @@ +#if defined(__linux__) +#define _DEFAULT_SOURCE +#endif + +#include "baycom_kernel.h" + +#include "hybbx/ax25.h" +#include "hybbx/baycom.h" +#include "hybbx/log.h" +#include "hybbx/rf_tx_pace.h" + +#include <errno.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#if defined(__linux__) + +#include <arpa/inet.h> +#include <fcntl.h> +#include <net/if.h> +#include <sys/ioctl.h> +#include <sys/socket.h> +#include <linux/if_packet.h> +#include <linux/if_ether.h> +#include <linux/hdlcdrv.h> + +static char *baycom_strdup(const char *s) +{ + size_t len; + char *copy; + + if (s == NULL) { + return NULL; + } + + len = strlen(s) + 1; + copy = malloc(len); + if (copy != NULL) { + memcpy(copy, s, len); + } + return copy; +} + +struct baycom_kernel_modem { + hybbx_baycom_config_t cfg; + baycom_modem_frame_cb frame_cb; + void *frame_userdata; + int ctl_fd; + int pkt_fd; + int ifindex; +}; + +static void baycom_kernel_mode_string(const hybbx_baycom_config_t *cfg, + char *out, size_t out_cap) +{ + unsigned rate; + char suffix = '\0'; + + if (out == NULL || out_cap == 0 || cfg == NULL) { + return; + } + + switch (cfg->mode) { + case HYBBX_BAYCOM_MODE_SER12_SOFTDCD: + case HYBBX_BAYCOM_MODE_PAR96_SOFTDCD: + suffix = '*'; + break; + case HYBBX_BAYCOM_MODE_SER12_INV: + suffix = '+'; + break; + default: + break; + } + + if (cfg->mode == HYBBX_BAYCOM_MODE_PAR96 || + cfg->mode == HYBBX_BAYCOM_MODE_PAR96_SOFTDCD) { + snprintf(out, out_cap, "par96%c", suffix); + return; + } + + if (cfg->mode == HYBBX_BAYCOM_MODE_EPP) { + snprintf(out, out_cap, "epp"); + return; + } + + rate = cfg->radio_baud / 100u; + if (rate == 0) { + rate = 12; + } + snprintf(out, out_cap, "ser%u%c", rate, suffix); +} + +static int baycom_kernel_hdlc_ioctl(baycom_kernel_modem_t *modem, + struct hdlcdrv_ioctl *hi) +{ + struct ifreq ifr; + + if (modem == NULL || hi == NULL || modem->ctl_fd < 0 || + modem->cfg.interface == NULL) { + return -1; + } + + memset(&ifr, 0, sizeof(ifr)); + strncpy(ifr.ifr_name, modem->cfg.interface, IFNAMSIZ - 1); + ifr.ifr_data = (void *)hi; + + if (ioctl(modem->ctl_fd, SIOCDEVPRIVATE, &ifr) < 0) { + return -1; + } + + return 0; +} + +static int baycom_kernel_interface_flags(baycom_kernel_modem_t *modem, + unsigned short *flags_out) +{ + struct ifreq ifr; + + if (modem == NULL || flags_out == NULL || modem->ctl_fd < 0) { + return -1; + } + + memset(&ifr, 0, sizeof(ifr)); + strncpy(ifr.ifr_name, modem->cfg.interface, IFNAMSIZ - 1); + if (ioctl(modem->ctl_fd, SIOCGIFFLAGS, &ifr) < 0) { + return -1; + } + + *flags_out = ifr.ifr_flags; + return 0; +} + +static int baycom_kernel_interface_up(baycom_kernel_modem_t *modem, int up) +{ + struct ifreq ifr; + unsigned short flags = 0; + + if (baycom_kernel_interface_flags(modem, &flags) < 0) { + return -1; + } + + memset(&ifr, 0, sizeof(ifr)); + strncpy(ifr.ifr_name, modem->cfg.interface, IFNAMSIZ - 1); + ifr.ifr_flags = flags; + if (up) { + ifr.ifr_flags |= IFF_UP; + } else { + ifr.ifr_flags &= (unsigned short)~IFF_UP; + } + + return ioctl(modem->ctl_fd, SIOCSIFFLAGS, &ifr); +} + +static int baycom_kernel_set_modem_params(baycom_kernel_modem_t *modem) +{ + struct hdlcdrv_ioctl hi; + + memset(&hi, 0, sizeof(hi)); + hi.cmd = HDLCDRVCTL_SETMODEMPAR; + hi.data.mp.iobase = (int)modem->cfg.iobase; + hi.data.mp.irq = (int)modem->cfg.irq; + + return baycom_kernel_hdlc_ioctl(modem, &hi); +} + +static int baycom_kernel_set_mode(baycom_kernel_modem_t *modem) +{ + struct hdlcdrv_ioctl hi; + char modestr[32]; + + baycom_kernel_mode_string(&modem->cfg, modestr, sizeof(modestr)); + + memset(&hi, 0, sizeof(hi)); + hi.cmd = HDLCDRVCTL_SETMODE; + strncpy(hi.data.modename, modestr, sizeof(hi.data.modename) - 1); + + return baycom_kernel_hdlc_ioctl(modem, &hi); +} + +static int baycom_kernel_set_channel_params(baycom_kernel_modem_t *modem) +{ + struct hdlcdrv_ioctl hi; + + memset(&hi, 0, sizeof(hi)); + hi.cmd = HDLCDRVCTL_SETCHANNELPAR; + hi.data.cp.tx_delay = (int)modem->cfg.txdelay; + hi.data.cp.tx_tail = (int)modem->cfg.txtail; + hi.data.cp.slottime = (int)modem->cfg.slot; + hi.data.cp.ppersist = (int)modem->cfg.persist; + hi.data.cp.fulldup = modem->cfg.full_duplex ? 1 : 0; + + return baycom_kernel_hdlc_ioctl(modem, &hi); +} + +static int baycom_kernel_modprobe(const hybbx_baycom_config_t *cfg) +{ + char cmd[384]; + char modestr[32]; + int rc; + + if (cfg == NULL || cfg->kernel_module == NULL) { + return -1; + } + + baycom_kernel_mode_string(cfg, modestr, sizeof(modestr)); + snprintf(cmd, sizeof(cmd), + "modprobe %s mode=%s iobase=0x%x irq=%u baud=%u 2>/dev/null", + cfg->kernel_module, modestr, cfg->iobase, cfg->irq, + cfg->radio_baud); + + rc = system(cmd); + if (rc != 0) { + hybbx_log_warn("[baycom] modprobe failed (%s)", cfg->kernel_module); + return -1; + } + + return 0; +} + +static int baycom_kernel_ifindex(baycom_kernel_modem_t *modem) +{ + struct ifreq ifr; + + memset(&ifr, 0, sizeof(ifr)); + strncpy(ifr.ifr_name, modem->cfg.interface, IFNAMSIZ - 1); + if (ioctl(modem->ctl_fd, SIOCGIFINDEX, &ifr) < 0) { + return -1; + } + + modem->ifindex = ifr.ifr_ifindex; + return 0; +} + +static int baycom_kernel_open_packet(baycom_kernel_modem_t *modem) +{ + struct sockaddr_ll bind_addr; + + modem->pkt_fd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_AX25)); + if (modem->pkt_fd < 0) { + return -1; + } + + if (baycom_kernel_ifindex(modem) < 0) { + return -1; + } + + memset(&bind_addr, 0, sizeof(bind_addr)); + bind_addr.sll_family = AF_PACKET; + bind_addr.sll_protocol = htons(ETH_P_AX25); + bind_addr.sll_ifindex = modem->ifindex; + + if (bind(modem->pkt_fd, (struct sockaddr *)&bind_addr, + sizeof(bind_addr)) < 0) { + return -1; + } + + return 0; +} + +hybbx_result_t baycom_kernel_open(baycom_kernel_modem_t **out, + const hybbx_baycom_config_t *cfg, + baycom_modem_frame_cb cb, void *userdata) +{ + baycom_kernel_modem_t *km; + unsigned short flags = 0; + + if (out == NULL || cfg == NULL || cfg->interface == NULL) { + return HYBBX_ERR_INVALID; + } + + *out = NULL; + km = calloc(1, sizeof(*km)); + if (km == NULL) { + return HYBBX_ERR_NOMEM; + } + + /* Own string copies only — avoid double-free with caller config. */ + memset(&km->cfg, 0, sizeof(km->cfg)); + km->cfg.backend = cfg->backend; + km->cfg.mode = cfg->mode; + km->cfg.iobase = cfg->iobase; + km->cfg.irq = cfg->irq; + km->cfg.radio_baud = cfg->radio_baud; + km->cfg.kernel_autoload = cfg->kernel_autoload; + km->cfg.txdelay = cfg->txdelay; + km->cfg.txtail = cfg->txtail; + km->cfg.slot = cfg->slot; + km->cfg.persist = cfg->persist; + km->cfg.full_duplex = cfg->full_duplex; + km->cfg.serial_baud = cfg->serial_baud; + km->cfg.circuit_port = cfg->circuit_port; + km->cfg.interface = baycom_strdup(cfg->interface); + km->cfg.kernel_module = baycom_strdup(cfg->kernel_module); + km->cfg.device = baycom_strdup(cfg->device); + km->cfg.mycall = baycom_strdup(cfg->mycall); + km->cfg.circuit_host = baycom_strdup(cfg->circuit_host); + km->cfg.link_id = baycom_strdup(cfg->link_id); + km->cfg.link_password = baycom_strdup(cfg->link_password); + km->cfg.link_role = baycom_strdup(cfg->link_role); + km->cfg.frequency_mhz = baycom_strdup(cfg->frequency_mhz); + km->frame_cb = cb; + km->frame_userdata = userdata; + km->ctl_fd = -1; + km->pkt_fd = -1; + + if (km->cfg.interface == NULL) { + free(km); + return HYBBX_ERR_NOMEM; + } + + if (cfg->kernel_autoload) { + if (baycom_kernel_modprobe(cfg) < 0) { + hybbx_baycom_config_free(&km->cfg); + free(km); + return HYBBX_ERR_IO; + } + } + + km->ctl_fd = socket(AF_INET, SOCK_DGRAM, 0); + if (km->ctl_fd < 0) { + hybbx_baycom_config_free(&km->cfg); + free(km); + return HYBBX_ERR_IO; + } + + if (baycom_kernel_interface_flags(km, &flags) < 0) { + hybbx_log_warn("[baycom] interface %s not found (load %s first)", + km->cfg.interface, + km->cfg.kernel_module != NULL ? km->cfg.kernel_module + : "baycom module"); + baycom_kernel_close(km); + return HYBBX_ERR_IO; + } + + if (!(flags & IFF_UP)) { + if (baycom_kernel_set_modem_params(km) < 0) { + hybbx_log_warn("[baycom] SETMODEMPAR failed (need CAP_SYS_RAWIO, iface down)"); + } + if (baycom_kernel_set_mode(km) < 0) { + hybbx_log_warn("[baycom] SETMODE failed (need CAP_NET_ADMIN, iface down)"); + } + if (baycom_kernel_interface_up(km, 1) < 0) { + hybbx_log_warn("[baycom] failed to bring %s up", km->cfg.interface); + baycom_kernel_close(km); + return HYBBX_ERR_IO; + } + } + + if (baycom_kernel_set_channel_params(km) < 0) { + hybbx_log_warn("[baycom] channel params ioctl failed"); + } + + if (baycom_kernel_open_packet(km) < 0) { + baycom_kernel_close(km); + return HYBBX_ERR_IO; + } + + *out = km; + return HYBBX_OK; +} + +void baycom_kernel_close(baycom_kernel_modem_t *km) +{ + if (km == NULL) { + return; + } + + if (km->pkt_fd >= 0) { + close(km->pkt_fd); + } + if (km->ctl_fd >= 0) { + close(km->ctl_fd); + } + + hybbx_baycom_config_free(&km->cfg); + free(km); +} + +hybbx_result_t baycom_kernel_poll(baycom_kernel_modem_t *km) +{ + uint8_t buf[HYBBX_AX25_FRAME_MAX + 64]; + ssize_t n; + + if (km == NULL || km->pkt_fd < 0) { + return HYBBX_ERR_IO; + } + + for (;;) { + n = recv(km->pkt_fd, buf, sizeof(buf), MSG_DONTWAIT); + if (n < 0) { + if (errno == EAGAIN +#if EAGAIN != EWOULDBLOCK + || errno == EWOULDBLOCK +#endif + ) { + return HYBBX_OK; + } + return HYBBX_ERR_IO; + } + if (n == 0) { + return HYBBX_OK; + } + + if (km->frame_cb != NULL && (size_t)n > 0) { + km->frame_cb(buf, (size_t)n, km->frame_userdata); + } + } +} + +hybbx_result_t baycom_kernel_send_frame(baycom_kernel_modem_t *km, + const uint8_t *frame, size_t len) +{ + struct sockaddr_ll dest; + ssize_t n; + + if (km == NULL || frame == NULL || len == 0 || km->pkt_fd < 0) { + return HYBBX_ERR_INVALID; + } + + hybbx_rf_tx_pace(); + + memset(&dest, 0, sizeof(dest)); + dest.sll_family = AF_PACKET; + dest.sll_protocol = htons(ETH_P_AX25); + dest.sll_ifindex = km->ifindex; + dest.sll_halen = 0; + + n = sendto(km->pkt_fd, frame, len, 0, (struct sockaddr *)&dest, + sizeof(dest)); + if (n < 0 || (size_t)n != len) { + return HYBBX_ERR_IO; + } + + return HYBBX_OK; +} + +#else /* !__linux__ */ + +hybbx_result_t baycom_kernel_open(baycom_kernel_modem_t **out, + const hybbx_baycom_config_t *cfg, + baycom_modem_frame_cb cb, void *userdata) +{ + (void)out; + (void)cfg; + (void)cb; + (void)userdata; + return HYBBX_ERR_UNSUPPORTED; +} + +void baycom_kernel_close(baycom_kernel_modem_t *modem) +{ + (void)modem; +} + +hybbx_result_t baycom_kernel_poll(baycom_kernel_modem_t *modem) +{ + (void)modem; + return HYBBX_ERR_UNSUPPORTED; +} + +hybbx_result_t baycom_kernel_send_frame(baycom_kernel_modem_t *modem, + const uint8_t *frame, size_t len) +{ + (void)modem; + (void)frame; + (void)len; + return HYBBX_ERR_UNSUPPORTED; +} + +#endif /* __linux__ */ diff --git a/plugins/baycom/baycom_kernel.h b/plugins/baycom/baycom_kernel.h new file mode 100644 index 0000000..b35e3b9 --- /dev/null +++ b/plugins/baycom/baycom_kernel.h @@ -0,0 +1,26 @@ +#ifndef BAYCOM_KERNEL_H +#define BAYCOM_KERNEL_H + +#include "baycom_modem.h" +#include "hybbx/baycom.h" +#include "hybbx/types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct baycom_kernel_modem baycom_kernel_modem_t; + +hybbx_result_t baycom_kernel_open(baycom_kernel_modem_t **out, + const hybbx_baycom_config_t *cfg, + baycom_modem_frame_cb cb, void *userdata); +void baycom_kernel_close(baycom_kernel_modem_t *modem); +hybbx_result_t baycom_kernel_poll(baycom_kernel_modem_t *modem); +hybbx_result_t baycom_kernel_send_frame(baycom_kernel_modem_t *modem, + const uint8_t *frame, size_t len); + +#ifdef __cplusplus +} +#endif + +#endif /* BAYCOM_KERNEL_H */ diff --git a/plugins/baycom/baycom_kiss.c b/plugins/baycom/baycom_kiss.c new file mode 100644 index 0000000..c66b220 --- /dev/null +++ b/plugins/baycom/baycom_kiss.c @@ -0,0 +1,262 @@ +#if defined(__linux__) || defined(__GLIBC__) +#define _DEFAULT_SOURCE 1 +#endif + +#include "baycom_kiss.h" + +#include "hybbx/kiss.h" +#include "hybbx/rf_tx_pace.h" +#include "serial_port.h" +#include "hybbx/log.h" +#include "hybbx/limits.h" + +#include <errno.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +static hybbx_result_t kiss_wait_device(const char *path) +{ + unsigned waited_ms = 0; + const unsigned step_ms = 500u; + const unsigned max_wait_ms = 30000u; + + if (path == NULL || path[0] == '\0') { + return HYBBX_ERR_INVALID; + } + + if (access(path, F_OK) == 0) { + return HYBBX_OK; + } + + while (access(path, F_OK) != 0) { + if (waited_ms >= max_wait_ms) { + hybbx_log_warn("[baycom] kiss device not ready: %s", path); + return HYBBX_ERR_IO; + } + if (waited_ms == 0u) { + hybbx_log_info("[baycom] waiting for kiss device %s (up to %u s)", + path, max_wait_ms / 1000u); + } + usleep((useconds_t)step_ms * 1000u); + waited_ms += step_ms; + } + + return HYBBX_OK; +} + +struct baycom_kiss_modem { + hybbx_baycom_config_t cfg; + hybbx_serial_port_t *serial; + hybbx_kiss_decoder_t kiss_dec; + baycom_modem_frame_cb frame_cb; + void *frame_userdata; +}; + +static char *baycom_strdup(const char *s) +{ + size_t len; + char *copy; + + if (s == NULL) { + return NULL; + } + + len = strlen(s) + 1; + copy = malloc(len); + if (copy != NULL) { + memcpy(copy, s, len); + } + return copy; +} + +static void on_kiss_frame(uint8_t port, const uint8_t *frame, size_t len, + void *userdata) +{ + baycom_kiss_modem_t *km = (baycom_kiss_modem_t *)userdata; + + (void)port; + + if (km == NULL || km->frame_cb == NULL || len == 0) { + return; + } + + km->frame_cb(frame, len, km->frame_userdata); +} + +static hybbx_result_t kiss_send_param(baycom_kiss_modem_t *km, + hybbx_kiss_command_t cmd, uint8_t value) +{ + uint8_t out[16]; + size_t out_len; + + out_len = hybbx_kiss_encode(0, cmd, &value, 1, out, sizeof(out)); + if (out_len == 0) { + return HYBBX_ERR_IO; + } + + return hybbx_serial_write(km->serial, out, out_len); +} + +static hybbx_result_t kiss_apply_channel_params(baycom_kiss_modem_t *km) +{ + hybbx_result_t rc; + + rc = kiss_send_param(km, HYBBX_KISS_CMD_TXDELAY, (uint8_t)km->cfg.txdelay); + if (rc != HYBBX_OK) { + return rc; + } + rc = kiss_send_param(km, HYBBX_KISS_CMD_PERSIST, (uint8_t)km->cfg.persist); + if (rc != HYBBX_OK) { + return rc; + } + rc = kiss_send_param(km, HYBBX_KISS_CMD_SLOTTIME, (uint8_t)km->cfg.slot); + if (rc != HYBBX_OK) { + return rc; + } + rc = kiss_send_param(km, HYBBX_KISS_CMD_TXTAIL, (uint8_t)km->cfg.txtail); + if (rc != HYBBX_OK) { + return rc; + } + return kiss_send_param(km, HYBBX_KISS_CMD_FULLDUPLEX, + km->cfg.full_duplex ? 1u : 0u); +} + +hybbx_result_t baycom_kiss_open(baycom_kiss_modem_t **out, + const hybbx_baycom_config_t *cfg, + baycom_modem_frame_cb cb, void *userdata) +{ + baycom_kiss_modem_t *km; + hybbx_serial_params_t sparams; + hybbx_result_t rc; + + if (out == NULL || cfg == NULL || cfg->device == NULL) { + return HYBBX_ERR_INVALID; + } + + *out = NULL; + km = calloc(1, sizeof(*km)); + if (km == NULL) { + return HYBBX_ERR_NOMEM; + } + + /* Own string copies only — never shallow-copy pointers from caller config + * (hybbx_baycom_config_free on close would double-free inst->config). */ + memset(&km->cfg, 0, sizeof(km->cfg)); + km->cfg.backend = cfg->backend; + km->cfg.mode = cfg->mode; + km->cfg.iobase = cfg->iobase; + km->cfg.irq = cfg->irq; + km->cfg.radio_baud = cfg->radio_baud; + km->cfg.kernel_autoload = cfg->kernel_autoload; + km->cfg.txdelay = cfg->txdelay; + km->cfg.txtail = cfg->txtail; + km->cfg.slot = cfg->slot; + km->cfg.persist = cfg->persist; + km->cfg.full_duplex = cfg->full_duplex; + km->cfg.serial_baud = cfg->serial_baud; + km->cfg.circuit_port = cfg->circuit_port; + km->cfg.device = baycom_strdup(cfg->device); + km->cfg.mycall = baycom_strdup(cfg->mycall); + km->cfg.circuit_host = baycom_strdup(cfg->circuit_host); + km->cfg.link_id = baycom_strdup(cfg->link_id); + km->cfg.link_password = baycom_strdup(cfg->link_password); + km->cfg.link_role = baycom_strdup(cfg->link_role); + km->cfg.frequency_mhz = baycom_strdup(cfg->frequency_mhz); + km->frame_cb = cb; + km->frame_userdata = userdata; + hybbx_kiss_decoder_init(&km->kiss_dec); + + hybbx_serial_params_default(&sparams, cfg->serial_baud); + sparams.data_bits = 8; + sparams.parity = HYBBX_SERIAL_PARITY_NONE; + sparams.stop_bits = 1; + sparams.assert_modem_lines = 0; + + rc = kiss_wait_device(km->cfg.device); + if (rc != HYBBX_OK) { + baycom_kiss_close(km); + return rc; + } + + if (access(km->cfg.device, R_OK | W_OK) != 0) { + hybbx_log_warn("[baycom] kiss device not readable/writable: %s (%s; user in dialout?)", + km->cfg.device, strerror(errno)); + baycom_kiss_close(km); + return HYBBX_ERR_IO; + } + + rc = hybbx_serial_open(&km->serial, km->cfg.device, &sparams); + if (rc != HYBBX_OK) { + hybbx_log_warn("[baycom] kiss serial open failed: %s (see [serial] log)", + km->cfg.device); + baycom_kiss_close(km); + return rc; + } + + rc = kiss_apply_channel_params(km); + if (rc != HYBBX_OK) { + hybbx_log_warn("[baycom] kiss channel params failed"); + } + + *out = km; + return HYBBX_OK; +} + +void baycom_kiss_close(baycom_kiss_modem_t *km) +{ + if (km == NULL) { + return; + } + + if (km->serial != NULL) { + hybbx_serial_close(km->serial); + } + + hybbx_baycom_config_free(&km->cfg); + free(km); +} + +hybbx_result_t baycom_kiss_poll(baycom_kiss_modem_t *km) +{ + uint8_t buf[256]; + size_t read_len = 0; + hybbx_result_t rc; + + if (km == NULL || km->serial == NULL) { + return HYBBX_ERR_IO; + } + + rc = hybbx_serial_read(km->serial, buf, sizeof(buf), &read_len); + if (rc != HYBBX_OK) { + return rc; + } + + if (read_len > 0) { + hybbx_kiss_decoder_feed(&km->kiss_dec, buf, read_len, on_kiss_frame, km); + } + + return HYBBX_OK; +} + +hybbx_result_t baycom_kiss_send_frame(baycom_kiss_modem_t *km, + const uint8_t *frame, size_t len) +{ + uint8_t out[HYBBX_KISS_MAX_FRAME + 8]; + size_t out_len; + + if (km == NULL || km->serial == NULL || frame == NULL || len == 0) { + return HYBBX_ERR_INVALID; + } + + hybbx_rf_tx_burst_guard(); + + out_len = hybbx_kiss_encode(0, HYBBX_KISS_CMD_DATA, frame, len, + out, sizeof(out)); + if (out_len == 0) { + return HYBBX_ERR_IO; + } + + return hybbx_serial_write(km->serial, out, out_len); +} diff --git a/plugins/baycom/baycom_kiss.h b/plugins/baycom/baycom_kiss.h new file mode 100644 index 0000000..49c7b36 --- /dev/null +++ b/plugins/baycom/baycom_kiss.h @@ -0,0 +1,26 @@ +#ifndef BAYCOM_KISS_H +#define BAYCOM_KISS_H + +#include "baycom_modem.h" +#include "hybbx/baycom.h" +#include "hybbx/types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct baycom_kiss_modem baycom_kiss_modem_t; + +hybbx_result_t baycom_kiss_open(baycom_kiss_modem_t **out, + const hybbx_baycom_config_t *cfg, + baycom_modem_frame_cb cb, void *userdata); +void baycom_kiss_close(baycom_kiss_modem_t *modem); +hybbx_result_t baycom_kiss_poll(baycom_kiss_modem_t *modem); +hybbx_result_t baycom_kiss_send_frame(baycom_kiss_modem_t *modem, + const uint8_t *frame, size_t len); + +#ifdef __cplusplus +} +#endif + +#endif /* BAYCOM_KISS_H */ diff --git a/plugins/baycom/baycom_modem.c b/plugins/baycom/baycom_modem.c new file mode 100644 index 0000000..e4e332f --- /dev/null +++ b/plugins/baycom/baycom_modem.c @@ -0,0 +1,94 @@ +#include "baycom_modem.h" +#include "baycom_kernel.h" +#include "baycom_kiss.h" + +#include <stdlib.h> + +struct baycom_modem { + hybbx_baycom_backend_t backend; + baycom_kernel_modem_t *kernel; + baycom_kiss_modem_t *kiss; +}; + +hybbx_result_t baycom_modem_open(baycom_modem_t **out, + const hybbx_baycom_config_t *cfg, + baycom_modem_frame_cb cb, void *userdata) +{ + baycom_modem_t *modem; + hybbx_result_t rc; + + if (out == NULL || cfg == NULL) { + return HYBBX_ERR_INVALID; + } + + *out = NULL; + modem = calloc(1, sizeof(*modem)); + if (modem == NULL) { + return HYBBX_ERR_NOMEM; + } + + modem->backend = cfg->backend; + + if (cfg->backend == HYBBX_BAYCOM_BACKEND_KISS) { + rc = baycom_kiss_open(&modem->kiss, cfg, cb, userdata); + } else { + rc = baycom_kernel_open(&modem->kernel, cfg, cb, userdata); + } + + if (rc != HYBBX_OK) { + baycom_modem_close(modem); + return rc; + } + + *out = modem; + return HYBBX_OK; +} + +void baycom_modem_close(baycom_modem_t *modem) +{ + if (modem == NULL) { + return; + } + + if (modem->kernel != NULL) { + baycom_kernel_close(modem->kernel); + } + if (modem->kiss != NULL) { + baycom_kiss_close(modem->kiss); + } + + free(modem); +} + +hybbx_result_t baycom_modem_poll(baycom_modem_t *modem) +{ + if (modem == NULL) { + return HYBBX_ERR_INVALID; + } + + if (modem->kiss != NULL) { + return baycom_kiss_poll(modem->kiss); + } + if (modem->kernel != NULL) { + return baycom_kernel_poll(modem->kernel); + } + + return HYBBX_ERR_IO; +} + +hybbx_result_t baycom_modem_send_frame(baycom_modem_t *modem, + const uint8_t *frame, size_t len) +{ + if (modem == NULL) { + return HYBBX_ERR_INVALID; + } + + if (modem->kiss != NULL) { + return baycom_kiss_send_frame(modem->kiss, frame, len); + } + if (modem->kernel != NULL) { + return baycom_kernel_send_frame(modem->kernel, frame, len); + } + + return HYBBX_ERR_IO; +} diff --git a/plugins/baycom/baycom_modem.h b/plugins/baycom/baycom_modem.h new file mode 100644 index 0000000..2365636 --- /dev/null +++ b/plugins/baycom/baycom_modem.h @@ -0,0 +1,31 @@ +#ifndef BAYCOM_MODEM_H +#define BAYCOM_MODEM_H + +#include "hybbx/baycom.h" +#include "hybbx/types.h" + +#include <stddef.h> +#include <stdint.h> + +#ifdef __cplusplus +extern "C" { +#endif + +typedef void (*baycom_modem_frame_cb)(const uint8_t *frame, size_t len, + void *userdata); + +typedef struct baycom_modem baycom_modem_t; + +hybbx_result_t baycom_modem_open(baycom_modem_t **out, + const hybbx_baycom_config_t *cfg, + baycom_modem_frame_cb cb, void *userdata); +void baycom_modem_close(baycom_modem_t *modem); +hybbx_result_t baycom_modem_poll(baycom_modem_t *modem); +hybbx_result_t baycom_modem_send_frame(baycom_modem_t *modem, + const uint8_t *frame, size_t len); + +#ifdef __cplusplus +} +#endif + +#endif /* BAYCOM_MODEM_H */ diff --git a/plugins/crdop/CMakeLists.txt b/plugins/crdop/CMakeLists.txt new file mode 100644 index 0000000..03c4f1d --- /dev/null +++ b/plugins/crdop/CMakeLists.txt @@ -0,0 +1,26 @@ +add_library(hybbx_plugin_crdop STATIC + crdop.c + crdop_config.c +) + +target_include_directories(hybbx_plugin_crdop + PRIVATE + ${CMAKE_SOURCE_DIR}/include + ${CMAKE_SOURCE_DIR}/plugins/ardop + ${CMAKE_CURRENT_SOURCE_DIR} +) + +target_compile_definitions(hybbx_plugin_crdop + PRIVATE HYBBX_PLUGIN_BUILD +) + +target_link_libraries(hybbx_plugin_crdop PRIVATE hybbx_ardop_common) +hybbx_link_plugin_instances(hybbx_plugin_crdop HYBBX_HAVE_PLUGIN_CRDOP) +hybbx_apply_hardening(hybbx_plugin_crdop) + +if(HYBBX_INSTALL_DEV) +install(TARGETS hybbx_plugin_crdop + ARCHIVE DESTINATION ${HYBBX_PLUGIN_INSTALL_DIR} + LIBRARY DESTINATION ${HYBBX_PLUGIN_INSTALL_DIR} +) +endif() diff --git a/plugins/crdop/crdop.c b/plugins/crdop/crdop.c new file mode 100644 index 0000000..d2c6f3d --- /dev/null +++ b/plugins/crdop/crdop.c @@ -0,0 +1,51 @@ +/* + * crdop — CB Radio Digital Open Protocol host-client (external ARDOPC/ardopcf). + * INI: [transport.crdop] or [transport.crdopN]. No embedded modem in HyBBX. + */ +#include "hybbx/plugin.h" +#include "hybbx/service.h" +#include "hybbx/ardop.h" +#include "hybbx/crdop.h" + +#include "../ardop/ardop_link.h" + +static const ardop_link_plugin_t g_crdop_link = { + .name = "crdop", + .kind = HYBBX_TRANSPORT_CRDOP, + .default_link_id = "crdop-link", + .modem_hint = "CRDOPC (ARDOP-compatible host TCP)", + .parse_config = (ardop_link_config_fn)hybbx_crdop_config_parse, + .config_free = (void (*)(void *))hybbx_ardop_config_free, + .config_size = sizeof(hybbx_ardop_config_t), +}; + +static hybbx_result_t crdop_init(hybbx_service_t *service) +{ + return ardop_link_init(service, &g_crdop_link); +} + +static void crdop_shutdown(void) +{ + ardop_link_shutdown(&g_crdop_link); +} + +static hybbx_result_t crdop_start(const char *config) +{ + return ardop_link_start(&g_crdop_link, config); +} + +static hybbx_result_t crdop_stop(void) +{ + return ardop_link_stop(&g_crdop_link); +} + +const hybbx_transport_plugin_t hybbx_plugin_crdop = { + .name = "crdop", + .kind = HYBBX_TRANSPORT_CRDOP, + .version = 1, + .init = crdop_init, + .shutdown = crdop_shutdown, + .start = crdop_start, + .stop = crdop_stop, + .write = NULL, +}; diff --git a/plugins/crdop/crdop_config.c b/plugins/crdop/crdop_config.c new file mode 100644 index 0000000..e1393bd --- /dev/null +++ b/plugins/crdop/crdop_config.c @@ -0,0 +1,186 @@ +#include "hybbx/ardop.h" +#include "hybbx/crdop.h" +#include "hybbx/log.h" + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +static char *crdop_strdup(const char *s) +{ + size_t len; + char *copy; + + if (s == NULL) { + return NULL; + } + + len = strlen(s) + 1; + copy = malloc(len); + if (copy != NULL) { + memcpy(copy, s, len); + } + return copy; +} + +static int str_ieq(const char *a, const char *b) +{ + if (a == NULL || b == NULL) { + return 0; + } + + while (*a != '\0' && *b != '\0') { + char ca = (char)(*a >= 'A' && *a <= 'Z' ? *a + 32 : *a); + char cb = (char)(*b >= 'A' && *b <= 'Z' ? *b + 32 : *b); + + if (ca != cb) { + return 0; + } + a++; + b++; + } + + return *a == '\0' && *b == '\0'; +} + +static const char *find_kv(const char *config, const char *key, + char *scratch, size_t scratch_len) +{ + const char *cursor = config; + size_t key_len = strlen(key); + + if (config == NULL || key == NULL) { + return NULL; + } + + while (*cursor != '\0') { + const char *sep = strchr(cursor, ';'); + const char *end = sep != NULL ? sep : cursor + strlen(cursor); + const char *eq = strchr(cursor, '='); + + if (eq != NULL && eq < end && (size_t)(eq - cursor) == key_len && + strncmp(cursor, key, key_len) == 0) { + const char *value = eq + 1; + size_t value_len = (size_t)(end - value); + + if (value_len >= scratch_len) { + value_len = scratch_len - 1; + } + memcpy(scratch, value, value_len); + scratch[value_len] = '\0'; + return scratch; + } + + if (sep == NULL) { + break; + } + cursor = sep + 1; + } + + return NULL; +} + +static const char *find_host(const char *config, char *scratch, size_t len) +{ + const char *value; + + value = find_kv(config, "modem_host", scratch, len); + if (value != NULL && value[0] != '\0') { + return value; + } + return find_kv(config, "ardop_host", scratch, len); +} + +static const char *find_port_str(const char *config, char *scratch, size_t len) +{ + const char *value; + + value = find_kv(config, "modem_port", scratch, len); + if (value != NULL && value[0] != '\0') { + return value; + } + return find_kv(config, "ardop_port", scratch, len); +} + +hybbx_result_t hybbx_crdop_config_parse(const char *config, + hybbx_ardop_config_t *out) +{ + char scratch[128]; + const char *value; + + if (out == NULL) { + return HYBBX_ERR_INVALID; + } + + hybbx_ardop_config_free(out); + + if (config == NULL) { + config = ""; + } + + value = find_host(config, scratch, sizeof(scratch)); + out->ardop_host = crdop_strdup(value != NULL ? value : "127.0.0.1"); + + out->ardop_port = (unsigned)strtoul( + find_port_str(config, scratch, sizeof(scratch)) != NULL ? + scratch : "8515", NULL, 10); + + value = find_kv(config, "mycall", scratch, sizeof(scratch)); + out->mycall = crdop_strdup(value != NULL ? value : "NOCALL"); + + out->radio_profile = HYBBX_CRDOP_PROFILE_CB; + + value = find_kv(config, "arq_bandwidth", scratch, sizeof(scratch)); + out->arq_bandwidth = crdop_strdup( + value != NULL ? value : + hybbx_crdop_default_arq_bandwidth(HYBBX_CRDOP_PROFILE_CB)); + + value = find_kv(config, "peer_call", scratch, sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + out->peer_call = crdop_strdup(value); + } + + value = find_kv(config, "listen", scratch, sizeof(scratch)); + out->listen = value == NULL || str_ieq(value, "yes") || str_ieq(value, "1") || + str_ieq(value, "true"); + + value = find_kv(config, "circuit_host", scratch, sizeof(scratch)); + out->circuit_host = crdop_strdup(value != NULL ? value : "127.0.0.1"); + + out->circuit_port = (unsigned)strtoul( + find_kv(config, "circuit_port", scratch, sizeof(scratch)) != NULL ? + scratch : "7323", NULL, 10); + + value = find_kv(config, "link_id", scratch, sizeof(scratch)); + out->link_id = crdop_strdup(value != NULL ? value : "crdop-link"); + + value = find_kv(config, "link_password", scratch, sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + out->link_password = crdop_strdup(value); + } + + value = find_kv(config, "link_role", scratch, sizeof(scratch)); + out->link_role = crdop_strdup(value != NULL ? value : "link"); + + value = find_kv(config, "frequency_mhz", scratch, sizeof(scratch)); + if (value == NULL || value[0] == '\0') { + value = find_kv(config, "frequency", scratch, sizeof(scratch)); + } + if (value != NULL && value[0] != '\0') { + out->frequency_mhz = strtod(value, NULL); + } + + if (hybbx_crdop_bandwidth_exceeds_cb(out->arq_bandwidth)) { + hybbx_log_warn("[crdop] warning: arq_bandwidth=%s high for CB profile", + out->arq_bandwidth); + } + + if (out->ardop_host == NULL || out->mycall == NULL || + out->arq_bandwidth == NULL || out->circuit_host == NULL || + out->link_id == NULL || out->link_role == NULL) { + hybbx_ardop_config_free(out); + return HYBBX_ERR_NOMEM; + } + + return HYBBX_OK; +} diff --git a/plugins/mains_proxy/CMakeLists.txt b/plugins/mains_proxy/CMakeLists.txt new file mode 100644 index 0000000..0d2922e --- /dev/null +++ b/plugins/mains_proxy/CMakeLists.txt @@ -0,0 +1,23 @@ +add_library(hybbx_plugin_mains_proxy STATIC + mains_proxy.c +) + +target_include_directories(hybbx_plugin_mains_proxy + PRIVATE ${CMAKE_SOURCE_DIR}/include +) + +target_compile_definitions(hybbx_plugin_mains_proxy + PRIVATE HYBBX_PLUGIN_BUILD +) + +target_link_libraries(hybbx_plugin_mains_proxy PRIVATE hybbx_core) +hybbx_link_plugin_instances(hybbx_plugin_mains_proxy HYBBX_HAVE_PLUGIN_MAINS_PROXY) + +hybbx_apply_hardening(hybbx_plugin_mains_proxy) + +if(HYBBX_INSTALL_DEV) +install(TARGETS hybbx_plugin_mains_proxy + ARCHIVE DESTINATION ${HYBBX_PLUGIN_INSTALL_DIR} + LIBRARY DESTINATION ${HYBBX_PLUGIN_INSTALL_DIR} +) +endif() diff --git a/plugins/mains_proxy/mains_proxy.c b/plugins/mains_proxy/mains_proxy.c new file mode 100644 index 0000000..8b6edff --- /dev/null +++ b/plugins/mains_proxy/mains_proxy.c @@ -0,0 +1,137 @@ +/* + * mains_proxy — Main-to-Main mesh proxy. + * + * Peers attach via HBX/Circuit only (circuit_host, circuit_port, link_id). + * Service linking only — proxymail and proxychat payloads; no user data. + */ +#include "hybbx/plugin.h" +#include "hybbx/service.h" +#include "hybbx/mains_proxy.h" +#include "hybbx/limits.h" +#include "hybbx/util.h" +#include "hybbx/log.h" + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +typedef struct mains_proxy_plugin_state { + hybbx_service_t *service; + hybbx_mains_proxy_mesh_t mesh; + int loaded; +} mains_proxy_plugin_state_t; + +static mains_proxy_plugin_state_t g_state; + +static hybbx_result_t mains_proxy_parse_instances(const char *config, + hybbx_mains_proxy_mesh_t *mesh) +{ + const char *cursor; + const char *end; + char section_buf[HYBBX_CONFIG_LINE_MAX]; + unsigned count = 0; + + if (mesh == NULL) { + return HYBBX_ERR_INVALID; + } + + hybbx_mains_proxy_mesh_init(mesh); + + if (config == NULL || config[0] == '\0') { + return HYBBX_OK; + } + + cursor = config; + while (*cursor != '\0' && count < HYBBX_MAINS_PROXY_MAX_PEERS) { + size_t len; + + end = strchr(cursor, HYBBX_MAINS_PROXY_INSTANCE_SEP); + if (end == NULL) { + len = strlen(cursor); + } else { + len = (size_t)(end - cursor); + } + + if (len >= sizeof(section_buf)) { + return HYBBX_ERR_INVALID; + } + + memcpy(section_buf, cursor, len); + section_buf[len] = '\0'; + + if (hybbx_mains_proxy_peer_parse(section_buf, &mesh->peers[count]) != + HYBBX_OK) { + return HYBBX_ERR_INVALID; + } + + if (mesh->peers[count].enabled) { + count++; + } + + if (end == NULL) { + break; + } + cursor = end + 1; + } + + mesh->peer_count = count; + return HYBBX_OK; +} + +static hybbx_result_t mains_proxy_init(hybbx_service_t *service) +{ + memset(&g_state, 0, sizeof(g_state)); + g_state.service = service; + hybbx_mains_proxy_mesh_init(&g_state.mesh); + g_state.loaded = 1; + return HYBBX_OK; +} + +static void mains_proxy_shutdown(void) +{ + hybbx_mains_proxy_mesh_stop(&g_state.mesh); + memset(&g_state, 0, sizeof(g_state)); +} + +static hybbx_result_t mains_proxy_start(const char *config) +{ + hybbx_result_t rc; + + if (!g_state.loaded) { + return HYBBX_ERR_INVALID; + } + + rc = mains_proxy_parse_instances(config, &g_state.mesh); + if (rc != HYBBX_OK) { + hybbx_log_warn("[mains_proxy] invalid peer configuration"); + return rc; + } + + return hybbx_mains_proxy_mesh_start(g_state.service, &g_state.mesh); +} + +static hybbx_result_t mains_proxy_stop(void) +{ + hybbx_mains_proxy_mesh_stop(&g_state.mesh); + return HYBBX_OK; +} + +const hybbx_transport_plugin_t hybbx_plugin_mains_proxy = { + .name = "mains_proxy", + .kind = HYBBX_TRANSPORT_MAINS_PROXY, + .version = 1, + .init = mains_proxy_init, + .shutdown = mains_proxy_shutdown, + .start = mains_proxy_start, + .stop = mains_proxy_stop, + .write = NULL, +}; + +void hybbx_mains_proxy_plugin_tick(void) +{ + if (!g_state.loaded || !g_state.mesh.running) { + return; + } + + hybbx_mains_proxy_mesh_tick(g_state.service, &g_state.mesh); +}
\ No newline at end of file diff --git a/plugins/packet_radio/CMakeLists.txt b/plugins/packet_radio/CMakeLists.txt new file mode 100644 index 0000000..1dc07a5 --- /dev/null +++ b/plugins/packet_radio/CMakeLists.txt @@ -0,0 +1,36 @@ +add_library(hybbx_plugin_packet_radio STATIC + packet_radio.c + packet_radio_config.c + ax25.c + kiss.c + serial_port.c + sixpack.c + tnc.c + tnc_host.c + tnc_profiles.c +) + +target_include_directories(hybbx_plugin_packet_radio + PRIVATE + ${CMAKE_SOURCE_DIR}/include + ${CMAKE_CURRENT_SOURCE_DIR} +) + +target_compile_definitions(hybbx_plugin_packet_radio + PRIVATE HYBBX_PLUGIN_BUILD +) + +find_package(Threads REQUIRED) +target_link_libraries(hybbx_plugin_packet_radio PRIVATE hybbx_core Threads::Threads) +if(HYBBX_PLATFORM_AMIGAOS) + target_link_libraries(hybbx_plugin_packet_radio PRIVATE amiga) +endif() +hybbx_link_plugin_instances(hybbx_plugin_packet_radio HYBBX_HAVE_PLUGIN_PACKET_RADIO) +hybbx_apply_hardening(hybbx_plugin_packet_radio) + +if(HYBBX_INSTALL_DEV) +install(TARGETS hybbx_plugin_packet_radio + ARCHIVE DESTINATION ${HYBBX_PLUGIN_INSTALL_DIR} + LIBRARY DESTINATION ${HYBBX_PLUGIN_INSTALL_DIR} +) +endif() diff --git a/plugins/packet_radio/ax25.c b/plugins/packet_radio/ax25.c new file mode 100644 index 0000000..5cbcfd8 --- /dev/null +++ b/plugins/packet_radio/ax25.c @@ -0,0 +1,250 @@ +#include "hybbx/ax25.h" + +#include <ctype.h> +#include <stdlib.h> +#include <string.h> + +static void address_upper(char *call) +{ + size_t i; + + for (i = 0; call[i] != '\0'; i++) { + call[i] = (char)toupper((unsigned char)call[i]); + } +} + +hybbx_result_t hybbx_ax25_address_parse(const char *text, + hybbx_ax25_address_t *out) +{ + const char *dash; + size_t call_len; + char *end = NULL; + unsigned long ssid; + + if (text == NULL || out == NULL) { + return HYBBX_ERR_INVALID; + } + + memset(out, 0, sizeof(*out)); + dash = strchr(text, '-'); + if (dash != NULL) { + call_len = (size_t)(dash - text); + ssid = strtoul(dash + 1, &end, 10); + if (end == dash + 1 || ssid > HYBBX_AX25_SSID_MAX) { + return HYBBX_ERR_INVALID; + } + out->ssid = (unsigned)ssid; + } else { + call_len = strlen(text); + } + + if (call_len == 0 || call_len > HYBBX_AX25_CALL_MAX) { + return HYBBX_ERR_INVALID; + } + + memcpy(out->call, text, call_len); + out->call[call_len] = '\0'; + address_upper(out->call); + return HYBBX_OK; +} + +void hybbx_ax25_encode_address(uint8_t *out, const hybbx_ax25_address_t *addr, + int last) +{ + size_t i; + char padded[HYBBX_AX25_CALL_MAX]; + + memset(padded, ' ', sizeof(padded)); + for (i = 0; i < HYBBX_AX25_CALL_MAX && addr->call[i] != '\0'; i++) { + padded[i] = addr->call[i]; + } + + for (i = 0; i < HYBBX_AX25_CALL_MAX; i++) { + out[i] = (uint8_t)((unsigned char)padded[i] << 1); + } + + out[6] = (uint8_t)(((addr->ssid & 0x0Fu) << 1) | 0x60u); + if (last) { + out[6] |= 0x01u; + } +} + +uint16_t hybbx_ax25_crc(const uint8_t *data, size_t len) +{ + uint16_t crc = 0xFFFFu; + size_t i; + int bit; + + for (i = 0; i < len; i++) { + crc ^= (uint16_t)data[i] << 8; + for (bit = 0; bit < 8; bit++) { + if (crc & 0x8000u) { + crc = (uint16_t)((crc << 1) ^ 0x1021u); + } else { + crc = (uint16_t)(crc << 1); + } + } + } + + return crc; +} + +static size_t append_addresses(const hybbx_ax25_path_t *path, + uint8_t *out, size_t out_cap) +{ + size_t pos = 0; + unsigned i; + + if (out_cap < (size_t)(2 + path->digi_count) * HYBBX_AX25_ADDR_ENCODED) { + return 0; + } + + hybbx_ax25_encode_address(out + pos, &path->dest, 0); + pos += HYBBX_AX25_ADDR_ENCODED; + + for (i = 0; i < path->digi_count; i++) { + hybbx_ax25_encode_address(out + pos, &path->digi[i], 0); + pos += HYBBX_AX25_ADDR_ENCODED; + } + + hybbx_ax25_encode_address(out + pos, &path->source, 1); + pos += HYBBX_AX25_ADDR_ENCODED; + return pos; +} + +size_t hybbx_ax25_build_ui(const hybbx_ax25_path_t *path, + const uint8_t *payload, size_t payload_len, + uint8_t *out, size_t out_cap) +{ + size_t pos; + uint16_t crc; + + if (path == NULL || out == NULL || + (payload_len > 0 && payload == NULL)) { + return 0; + } + + if (payload_len + 32 > out_cap || payload_len > HYBBX_AX25_FRAME_MAX) { + return 0; + } + + pos = append_addresses(path, out, out_cap); + if (pos + 4 + payload_len > out_cap) { + return 0; + } + + out[pos++] = HYBBX_AX25_CTRL_UI; + out[pos++] = HYBBX_AX25_PID_NOL3; + + if (payload_len > 0) { + memcpy(out + pos, payload, payload_len); + pos += payload_len; + } + + crc = hybbx_ax25_crc(out, pos); + out[pos++] = (uint8_t)(crc & 0xFFu); + out[pos++] = (uint8_t)((crc >> 8) & 0xFFu); + return pos; +} + +static void decode_address(const uint8_t *in, hybbx_ax25_address_t *out) +{ + size_t i; + + for (i = 0; i < HYBBX_AX25_CALL_MAX; i++) { + char ch = (char)((in[i] >> 1) & 0x7Fu); + out->call[i] = ch == ' ' ? '\0' : ch; + } + out->call[HYBBX_AX25_CALL_MAX] = '\0'; + + for (i = HYBBX_AX25_CALL_MAX; i > 0; i--) { + if (out->call[i - 1] != '\0') { + break; + } + } + out->call[i] = '\0'; + out->ssid = (unsigned)((in[6] >> 1) & 0x0Fu); +} + +size_t hybbx_ax25_parse_ui(const uint8_t *frame, size_t frame_len, + hybbx_ax25_path_t *path, + uint8_t *payload, size_t payload_cap) +{ + size_t pos = 0; + unsigned digi = 0; + uint16_t crc_rx; + uint16_t crc_calc; + + if (frame == NULL || frame_len < 16) { + return 0; + } + + if (path != NULL) { + memset(path, 0, sizeof(*path)); + } + + while (pos + HYBBX_AX25_ADDR_ENCODED <= frame_len) { + int last = frame[pos + 6] & 0x01; + hybbx_ax25_address_t tmp; + + decode_address(frame + pos, &tmp); + if (path != NULL) { + if (digi == 0) { + path->dest = tmp; + } else if (!last) { + if (digi - 1 < HYBBX_AX25_MAX_DIGI) { + path->digi[digi - 1] = tmp; + path->digi_count = digi; + } + } else { + path->source = tmp; + } + } + + pos += HYBBX_AX25_ADDR_ENCODED; + digi++; + + if (last) { + break; + } + + if (digi > HYBBX_AX25_MAX_DIGI + 1) { + return 0; + } + } + + if (pos + 4 > frame_len) { + return 0; + } + + if (frame[pos] != HYBBX_AX25_CTRL_UI) { + return 0; + } + + pos += 2; /* control + PID */ + + if (frame_len < pos + 2) { + return 0; + } + + crc_rx = (uint16_t)frame[frame_len - 2] | + ((uint16_t)frame[frame_len - 1] << 8); + crc_calc = hybbx_ax25_crc(frame, frame_len - 2); + if (crc_rx != crc_calc) { + return 0; + } + + { + size_t payload_len = frame_len - pos - 2; + + if (payload_len > payload_cap) { + return 0; + } + + if (payload_len > 0 && payload != NULL) { + memcpy(payload, frame + pos, payload_len); + } + + return payload_len; + } +} diff --git a/plugins/packet_radio/kiss.c b/plugins/packet_radio/kiss.c new file mode 100644 index 0000000..a075442 --- /dev/null +++ b/plugins/packet_radio/kiss.c @@ -0,0 +1,152 @@ +#include "hybbx/kiss.h" + +#include <string.h> + +void hybbx_kiss_decoder_init(hybbx_kiss_decoder_t *dec) +{ + if (dec == NULL) { + return; + } + + memset(dec, 0, sizeof(*dec)); +} + +static void deliver_frame(hybbx_kiss_decoder_t *dec, + hybbx_kiss_frame_cb cb, void *userdata) +{ + uint8_t port; + const uint8_t *payload; + size_t payload_len; + + if (dec->len < 1) { + return; + } + + port = (uint8_t)((dec->buf[0] >> 4) & 0x0Fu); + payload = dec->buf + 1; + payload_len = dec->len - 1; + + if (cb != NULL) { + cb(port, payload, payload_len, userdata); + } +} + +void hybbx_kiss_decoder_feed(hybbx_kiss_decoder_t *dec, + const uint8_t *data, size_t len, + hybbx_kiss_frame_cb cb, void *userdata) +{ + size_t i; + + if (dec == NULL || data == NULL) { + return; + } + + for (i = 0; i < len; i++) { + uint8_t byte = data[i]; + + if (byte == HYBBX_KISS_FEND) { + if (dec->in_frame && dec->len > 0) { + deliver_frame(dec, cb, userdata); + } + dec->in_frame = 1; + dec->escape = 0; + dec->len = 0; + continue; + } + + if (!dec->in_frame) { + continue; + } + + if (dec->escape) { + if (byte == HYBBX_KISS_TFEND) { + byte = HYBBX_KISS_FEND; + } else if (byte == HYBBX_KISS_TFESC) { + byte = HYBBX_KISS_FESC; + } + dec->escape = 0; + } else if (byte == HYBBX_KISS_FESC) { + dec->escape = 1; + continue; + } + + if (dec->len >= HYBBX_KISS_MAX_FRAME) { + dec->len = 0; + dec->in_frame = 0; + continue; + } + + dec->buf[dec->len++] = byte; + } +} + +static size_t kiss_write_byte(uint8_t byte, uint8_t *out, size_t out_cap, size_t pos) +{ + if (pos + 1 > out_cap) { + return 0; + } + + if (byte == HYBBX_KISS_FEND) { + if (pos + 2 > out_cap) { + return 0; + } + out[pos++] = HYBBX_KISS_FESC; + out[pos++] = HYBBX_KISS_TFEND; + return pos; + } + + if (byte == HYBBX_KISS_FESC) { + if (pos + 2 > out_cap) { + return 0; + } + out[pos++] = HYBBX_KISS_FESC; + out[pos++] = HYBBX_KISS_TFESC; + return pos; + } + + out[pos++] = byte; + return pos; +} + +size_t hybbx_kiss_encode(uint8_t port, hybbx_kiss_command_t cmd, + const uint8_t *payload, size_t payload_len, + uint8_t *out, size_t out_cap) +{ + size_t pos = 0; + uint8_t cmd_byte; + size_t i; + + if (out == NULL || out_cap < 3) { + return 0; + } + + if (payload_len > 0 && payload == NULL) { + return 0; + } + + if (payload_len + 4 > out_cap) { + return 0; + } + + out[pos++] = HYBBX_KISS_FEND; + + cmd_byte = (uint8_t)(((port & 0x0Fu) << 4) | ((uint8_t)cmd & 0x0Fu)); + pos = kiss_write_byte(cmd_byte, out, out_cap, pos); + if (pos == 0) { + return 0; + } + + for (i = 0; i < payload_len; i++) { + pos = kiss_write_byte(payload[i], out, out_cap, pos); + if (pos == 0) { + return 0; + } + } + + if (pos + 1 > out_cap) { + return 0; + } + + out[pos++] = HYBBX_KISS_FEND; + return pos; +} diff --git a/plugins/packet_radio/packet_radio.c b/plugins/packet_radio/packet_radio.c new file mode 100644 index 0000000..e8952ae --- /dev/null +++ b/plugins/packet_radio/packet_radio.c @@ -0,0 +1,1005 @@ +/* + * packet_radio — link/repeater edge adapter: serial TNC, KISS/AX.25 → HBX client. + * INI: [transport.packet_radio] or [transport.packet_radioN]. See share/hybbx.ini.example. + */ +#include "hybbx/plugin.h" +#include "hybbx/service.h" +#include "hybbx/broadcast.h" +#include "hybbx/packet_radio.h" +#include "hybbx/tnc.h" +#include "hybbx/circuit.h" +#include "hybbx/circuit_tcp.h" +#include "hybbx/circuit_balance.h" +#include "hybbx/traffic.h" +#include "hybbx/limits.h" +#include "hybbx/security_ban.h" +#include "hybbx/ax25.h" +#include "hybbx/util.h" +#include "hybbx/log.h" +#include "hybbx/max25.h" + +#include <pthread.h> +#include <stdio.h> +#include <string.h> +#include <stdlib.h> +#include <unistd.h> + +#if defined(_WIN32) +#include <windows.h> +#else +#include <errno.h> +#include <poll.h> +#include <sys/select.h> +#endif + +#define PACKET_RADIO_POLL_MS 20u + +static hybbx_service_t *g_service; + +typedef struct packet_radio_instance { + hybbx_packet_radio_config_t config; + hybbx_tnc_t *tnc; + int circuit_fd; + hybbx_circuit_decoder_t circuit_dec; + volatile int flow_paused; + volatile int flow_cancel; + unsigned index; + unsigned circuit_reconnect_polls; +} packet_radio_instance_t; + +static packet_radio_instance_t g_instances[HYBBX_PACKET_RADIO_MAX_INSTANCES]; +static unsigned g_instance_count; +static pthread_t g_poll_thread; +static volatile int g_radio_running; +static int g_poll_thread_started; + +extern const hybbx_transport_plugin_t hybbx_plugin_packet_radio; + +static void instance_note_rf_activity(packet_radio_instance_t *inst); + +static unsigned packet_radio_count_local_edges(const char *config) +{ + char scratch[4096]; + const char *cursor = config; + unsigned count = 0; + + while (*cursor != '\0') { + const char *sep = strchr(cursor, HYBBX_PACKET_RADIO_INSTANCE_SEP); + size_t len = sep != NULL ? (size_t)(sep - cursor) : strlen(cursor); + + if (len == 0) { + cursor = sep != NULL ? sep + 1 : cursor + len; + if (sep == NULL) { + break; + } + continue; + } + + if (len >= sizeof(scratch)) { + return count; + } + + memcpy(scratch, cursor, len); + scratch[len] = '\0'; + + if (hybbx_packet_radio_section_is_local_edge(scratch)) { + count++; + } + + cursor = sep != NULL ? sep + 1 : cursor + len; + if (sep == NULL) { + break; + } + } + + return count; +} + +static hybbx_result_t instance_connect_circuit(packet_radio_instance_t *inst); +static void packet_radio_poll_sleep_ms(unsigned ms); + +static const char *device_type_name(hybbx_packet_radio_device_type_t type) +{ + return type == HYBBX_PACKET_RADIO_DEVICE_SERIAL ? "serial" : "usb"; +} + +static const char *protocol_name(hybbx_packet_radio_protocol_t proto) +{ + switch (proto) { + case HYBBX_PACKET_RADIO_PROTO_HOSTMODE: + return "hostmode"; + case HYBBX_PACKET_RADIO_PROTO_SIXPACK: + return "6pack"; + case HYBBX_PACKET_RADIO_PROTO_KISS: + default: + return "kiss"; + } +} + +static const char *tnc_name(hybbx_packet_radio_tnc_t tnc) +{ + switch (tnc) { + case HYBBX_PACKET_RADIO_TNC_BAYCOM: + return "baycom"; + case HYBBX_PACKET_RADIO_TNC_PCCOM: + return "pccom"; + case HYBBX_PACKET_RADIO_TNC_GENERIC: + return "generic"; + case HYBBX_PACKET_RADIO_TNC_TNC2C: + default: + return "tnc2c"; + } +} + +static int instance_circuit_uplink_allowed(const packet_radio_instance_t *inst) +{ + return inst != NULL && !inst->flow_paused && !inst->flow_cancel; +} + +static int packet_radio_source_banned(const hybbx_ax25_path_t *path) +{ + char raw[HYBBX_CALLID_MAX]; + char norm[HYBBX_CALLID_MAX]; + + if (path == NULL || path->source.call[0] == '\0') { + return 0; + } + + if (path->source.ssid > 0u) { + snprintf(raw, sizeof(raw), "%s-%u", path->source.call, + path->source.ssid); + } else { + hybbx_strlcpy(raw, path->source.call, sizeof(raw)); + } + + if (!hybbx_security_callid_normalize(raw, norm, sizeof(norm))) { + return 0; + } + + return hybbx_security_ban_callid_is_banned(norm); +} + +static int packet_radio_frame_source_banned(const uint8_t *frame, size_t len) +{ + hybbx_ax25_path_t path; + uint8_t scratch[HYBBX_AX25_FRAME_MAX]; + + if (frame == NULL || len == 0) { + return 0; + } + + if (hybbx_ax25_parse_ui(frame, len, &path, scratch, sizeof(scratch)) == 0) { + return 0; + } + + return packet_radio_source_banned(&path); +} + +static hybbx_result_t instance_circuit_uplink(packet_radio_instance_t *inst, + const uint8_t *frame, + size_t frame_len) +{ + uint8_t hbx[HYBBX_CIRCUIT_MAX_FRAME]; + size_t hbx_len; + uint16_t flags = HYBBX_CIRCUIT_FLAG_RX; + + if (inst == NULL || inst->circuit_fd < 0) { + return HYBBX_ERR_IO; + } + + if (!instance_circuit_uplink_allowed(inst)) { + return HYBBX_OK; + } + + if (hybbx_packet_radio_modulation_is_g3ruh(inst->config.params.modulation)) { + flags |= HYBBX_CIRCUIT_FLAG_G3RUH_FSK; + } + + hbx_len = hybbx_circuit_encode_ax25(frame, frame_len, flags, + hbx, sizeof(hbx)); + if (hbx_len == 0) { + return HYBBX_ERR_IO; + } + + return hybbx_circuit_link_write(inst->circuit_fd, hbx, hbx_len); +} + +static void on_tnc_ax25_frame(const uint8_t *frame, size_t len, void *userdata) +{ + packet_radio_instance_t *inst = (packet_radio_instance_t *)userdata; + + if (len == 0 || inst == NULL) { + return; + } + + if (packet_radio_frame_source_banned(frame, len)) { + return; + } + + instance_note_rf_activity(inst); + (void)instance_circuit_uplink(inst, frame, len); +} + +static void on_tnc_host_ui(const uint8_t *payload, size_t len, + const hybbx_ax25_path_t *path, void *userdata) +{ + packet_radio_instance_t *inst = (packet_radio_instance_t *)userdata; + uint8_t hbx[HYBBX_CIRCUIT_MAX_FRAME]; + size_t hbx_len; + + (void)path; + + if (inst == NULL || inst->circuit_fd < 0 || len == 0) { + return; + } + + if (path != NULL && packet_radio_source_banned(path)) { + return; + } + + if (!instance_circuit_uplink_allowed(inst)) { + return; + } + + hbx_len = hybbx_circuit_encode(HYBBX_CIRCUIT_PROTO_TERMINAL, + HYBBX_CIRCUIT_FLAG_RX, + payload, len, hbx, sizeof(hbx)); + if (hbx_len > 0) { + (void)hybbx_circuit_link_write(inst->circuit_fd, hbx, hbx_len); + } +} + +static void instance_format_call(const hybbx_ax25_address_t *addr, + char *out, size_t out_len) +{ + if (out == NULL || out_len == 0) { + return; + } + + out[0] = '\0'; + if (addr == NULL || addr->call[0] == '\0') { + return; + } + + if (addr->ssid > 0u) { + unsigned ssid = addr->ssid; + + if (ssid > HYBBX_AX25_SSID_MAX) { + ssid = HYBBX_AX25_SSID_MAX; + } + snprintf(out, out_len, "%s-%u", addr->call, ssid); + } else { + hybbx_strlcpy(out, addr->call, out_len); + } +} + +static void instance_log_rf_tx(packet_radio_instance_t *inst, + hybbx_result_t rc, + const hybbx_ax25_path_t *path, + size_t payload_len) +{ + char src[HYBBX_AX25_CALL_MAX + 8]; + char dst[HYBBX_AX25_CALL_MAX + 8]; + + if (inst == NULL) { + return; + } + + instance_format_call(path != NULL ? &path->source : NULL, src, sizeof(src)); + instance_format_call(path != NULL ? &path->dest : NULL, dst, sizeof(dst)); + + if (rc == HYBBX_OK) { + hybbx_log_stats("[packet_radio%u] RF TX %s>%s (%zu bytes)", + inst->index + 1, src, dst, payload_len); + } else { + hybbx_log_warn("[packet_radio%u] RF TX failed %s>%s (%zu bytes, rc=%d)", + inst->index + 1, src, dst, payload_len, (int)rc); + } +} + +static void instance_note_rf_activity(packet_radio_instance_t *inst) +{ + hybbx_circuit_hub_t *hub; + const char *link_id; + + if (inst == NULL || g_service == NULL) { + return; + } + + hub = hybbx_service_circuit_hub(g_service); + link_id = inst->config.link_id; + if (link_id == NULL || link_id[0] == '\0') { + link_id = "packet-radio"; + } + + if (hub != NULL) { + hybbx_circuit_hub_note_rf_activity(hub, link_id); + } +} + +static hybbx_result_t instance_tx_ax25_ui(packet_radio_instance_t *inst, + const hybbx_ax25_path_t *path, + const uint8_t *payload, size_t len) +{ + uint8_t frame[HYBBX_AX25_FRAME_MAX]; + size_t frame_len; + hybbx_result_t rc; + + if (inst == NULL || inst->tnc == NULL || path == NULL || + payload == NULL || len == 0) { + return HYBBX_ERR_INVALID; + } + + frame_len = hybbx_ax25_build_ui(path, payload, len, frame, sizeof(frame)); + if (frame_len == 0) { + return HYBBX_ERR_INVALID; + } + + rc = hybbx_tnc_send_frame(inst->tnc, frame, frame_len); + if (rc == HYBBX_OK) { + instance_note_rf_activity(inst); + } + instance_log_rf_tx(inst, rc, path, len); + return rc; +} + +static hybbx_result_t instance_send_bytes(packet_radio_instance_t *inst, + const uint8_t *data, size_t len) +{ + if (inst == NULL || inst->tnc == NULL || data == NULL || len == 0) { + return HYBBX_ERR_INVALID; + } + + if (inst->config.protocol == HYBBX_PACKET_RADIO_PROTO_HOSTMODE) { + if (hybbx_tnc_host_connected(inst->tnc)) { + return hybbx_tnc_send_converse(inst->tnc, (const char *)data, len); + } + return HYBBX_ERR_BUSY; + } + + return hybbx_tnc_send_ui(inst->tnc, data, len); +} + +static void instance_circuit_disconnect(packet_radio_instance_t *inst) +{ + if (inst == NULL) { + return; + } + + if (inst->circuit_fd >= 0) { + close(inst->circuit_fd); + inst->circuit_fd = -1; + } + hybbx_circuit_decoder_init(&inst->circuit_dec); + inst->circuit_reconnect_polls = 0; +} + +static void instance_circuit_reconnect(packet_radio_instance_t *inst) +{ + if (inst == NULL) { + return; + } + + instance_circuit_disconnect(inst); + packet_radio_poll_sleep_ms(250); + if (instance_connect_circuit(inst) == HYBBX_OK) { + hybbx_log_info("[packet_radio%u] circuit reconnected", inst->index + 1); + } +} + +static void instance_on_circuit_flow_ctrl(packet_radio_instance_t *inst, + hybbx_circuit_proto_t proto, + uint16_t flags, + const uint8_t *payload, size_t len) +{ + hybbx_circuit_balance_action_t action; + char reason[64]; + + (void)proto; + (void)flags; + + if (inst == NULL) { + return; + } + + if (hybbx_circuit_flow_ctrl_parse((const char *)payload, len, &action, + reason, sizeof(reason)) != HYBBX_OK) { + return; + } + + switch (action) { + case HYBBX_CIRCUIT_BAL_PAUSE: + inst->flow_paused = 1; + hybbx_log_stats("[packet_radio%u] circuit flow pause (%s)", + inst->index + 1, reason); + break; + case HYBBX_CIRCUIT_BAL_BREAK: + inst->flow_paused = 0; + hybbx_circuit_decoder_init(&inst->circuit_dec); + hybbx_log_stats("[packet_radio%u] circuit flow break (%s)", + inst->index + 1, reason); + break; + case HYBBX_CIRCUIT_BAL_CANCEL: + inst->flow_cancel = 1; + hybbx_log_warn("[packet_radio%u] circuit flow cancel (%s)", + inst->index + 1, reason); + break; + case HYBBX_CIRCUIT_BAL_RESUME: + inst->flow_paused = 0; + hybbx_log_stats("[packet_radio%u] circuit flow resume (%s)", + inst->index + 1, reason); + break; + default: + break; + } +} + +static void on_circuit_downlink(hybbx_circuit_proto_t proto, uint16_t flags, + const uint8_t *payload, size_t len, + void *userdata) +{ + packet_radio_instance_t *inst = (packet_radio_instance_t *)userdata; + + if (inst == NULL) { + return; + } + + if (proto == HYBBX_CIRCUIT_PROTO_FLOW_CTRL) { + instance_on_circuit_flow_ctrl(inst, proto, flags, payload, len); + return; + } + + if (inst->tnc == NULL || len == 0) { + return; + } + + switch (proto) { + case HYBBX_CIRCUIT_PROTO_TERMINAL: + (void)instance_send_bytes(inst, payload, len); + break; + case HYBBX_CIRCUIT_PROTO_AX25: { + hybbx_result_t rc = hybbx_tnc_send_frame(inst->tnc, payload, len); + if (rc == HYBBX_OK) { + hybbx_ax25_path_t path; + uint8_t ui[HYBBX_AX25_PAYLOAD_MAX]; + size_t ui_len = hybbx_ax25_parse_ui(payload, len, &path, + ui, sizeof(ui)); + + instance_note_rf_activity(inst); + if (ui_len > 0) { + instance_log_rf_tx(inst, rc, &path, ui_len); + } + } else { + hybbx_log_warn("[packet_radio%u] RF TX frame failed (rc=%d, %zu bytes)", + inst->index + 1, (int)rc, len); + } + break; + } + case HYBBX_CIRCUIT_PROTO_AX25_UI: { + uint8_t ui[HYBBX_AX25_PAYLOAD_MAX]; + hybbx_ax25_path_t path; + size_t ui_len = hybbx_circuit_unpack_ax25_ui(payload, len, &path, + ui, sizeof(ui)); + if (ui_len > 0) { + (void)instance_tx_ax25_ui(inst, &path, ui, ui_len); + } else { + hybbx_log_warn("[packet_radio%u] AX25_UI unpack failed (%zu bytes)", + inst->index + 1, len); + } + break; + } + default: + break; + } +} + +static void packet_radio_poll_sleep_ms(unsigned ms) +{ +#if defined(_WIN32) + Sleep(ms); +#else + struct timeval tv; + + tv.tv_sec = (time_t)(ms / 1000u); + tv.tv_usec = (suseconds_t)((ms % 1000u) * 1000u); + (void)select(0, NULL, NULL, NULL, &tv); +#endif +} + +static hybbx_result_t instance_connect_circuit(packet_radio_instance_t *inst) +{ + const char *host; + unsigned port; + unsigned attempt; + + if (inst == NULL) { + return HYBBX_ERR_INVALID; + } + + host = inst->config.circuit_host; + port = inst->config.circuit_port; + + if (host == NULL || host[0] == '\0') { + host = "127.0.0.1"; + } + if (port == 0) { + port = HYBBX_CIRCUIT_DEFAULT_PORT; + } + + if (inst->config.link_password == NULL || + inst->config.link_password[0] == '\0') { + hybbx_log_warn("[packet_radio%u] missing link_password for HBX LINK_AUTH; " + "set link_password in [transport.packet_radioN] " + "(hub logs link_auth_fail reason=timeout_no_link_auth)", + inst->index + 1); + return HYBBX_ERR_INVALID; + } + + for (attempt = 0; attempt < 50; attempt++) { + hybbx_result_t rc = hybbx_circuit_link_connect(host, port, + &inst->circuit_fd); + if (rc == HYBBX_OK) { + hybbx_circuit_decoder_init(&inst->circuit_dec); + { + const char *link_id = inst->config.link_id; + const char *link_role = inst->config.link_role; + + if (link_id == NULL || link_id[0] == '\0') { + link_id = "packet-radio"; + } + if (link_role == NULL || link_role[0] == '\0') { + link_role = "link"; + } + + { + hybbx_circuit_link_qos_t qos; + int duplex = (int)inst->config.params.duplex; + + memset(&qos, 0, sizeof(qos)); + qos.baud = inst->config.params.radio_baud; + if (duplex == HYBBX_PACKET_RADIO_DUPLEX_HALF) { + qos.duplex = 1; + } else if (duplex == HYBBX_PACKET_RADIO_DUPLEX_FULL) { + qos.duplex = 2; + } + qos.bandwidth = "low"; + qos.frequency_mhz = inst->config.frequency_mhz; + + rc = hybbx_circuit_link_authenticate_ex( + inst->circuit_fd, inst->config.link_password, + link_role, link_id, &qos); + } + if (rc != HYBBX_OK) { + close(inst->circuit_fd); + inst->circuit_fd = -1; + packet_radio_poll_sleep_ms(100); + continue; + } + } + hybbx_log_info("[packet_radio%u] linked to internal circuit %s:%u (HBX)", + inst->index + 1, host, port); + return HYBBX_OK; + } + packet_radio_poll_sleep_ms(100); + } + + hybbx_log_warn("[packet_radio%u] could not connect to internal circuit %s:%u", + inst->index + 1, host, port); + return HYBBX_ERR_IO; +} + +static void instance_shutdown(packet_radio_instance_t *inst) +{ + if (inst == NULL) { + return; + } + + if (inst->circuit_fd >= 0) { + close(inst->circuit_fd); + inst->circuit_fd = -1; + } + + if (inst->tnc != NULL) { + hybbx_tnc_close(inst->tnc); + inst->tnc = NULL; + } + + hybbx_packet_radio_config_free(&inst->config); + memset(inst, 0, sizeof(*inst)); + inst->circuit_fd = -1; +} + +typedef enum { + PR_POLL_CIRCUIT = 0, + PR_POLL_SERIAL = 1 +} packet_radio_poll_kind_t; + +typedef struct { + unsigned instance_index; + packet_radio_poll_kind_t kind; +} packet_radio_poll_map_t; + +static void instance_drain_circuit(packet_radio_instance_t *inst) +{ + uint8_t buf[512]; + + if (inst == NULL || inst->circuit_fd < 0) { + return; + } + + for (;;) { + size_t read_len = 0; + hybbx_result_t rc = hybbx_circuit_link_read(inst->circuit_fd, buf, + sizeof(buf), &read_len); + + if (rc == HYBBX_ERR_IO) { + hybbx_log_warn("[packet_radio%u] circuit disconnected — reconnecting", + inst->index + 1); + instance_circuit_reconnect(inst); + return; + } + if (rc != HYBBX_OK || read_len == 0) { + break; + } + + hybbx_circuit_decoder_feed(&inst->circuit_dec, buf, read_len, + on_circuit_downlink, inst); + } +} + +#if !defined(_WIN32) +static void packet_radio_poll_instances(void) +{ + struct pollfd pfds[HYBBX_PACKET_RADIO_MAX_INSTANCES * 2u]; + packet_radio_poll_map_t map[HYBBX_PACKET_RADIO_MAX_INSTANCES * 2u]; + nfds_t nfds = 0; + unsigned i; + + for (i = 0; i < g_instance_count; i++) { + packet_radio_instance_t *inst = &g_instances[i]; + int circuit_fd = inst->circuit_fd; + int serial_fd = inst->tnc != NULL ? hybbx_tnc_serial_fd(inst->tnc) : -1; + + if (circuit_fd >= 0) { + pfds[nfds].fd = circuit_fd; + pfds[nfds].events = POLLIN; + pfds[nfds].revents = 0; + map[nfds].instance_index = i; + map[nfds].kind = PR_POLL_CIRCUIT; + nfds++; + } + + if (serial_fd >= 0) { + pfds[nfds].fd = serial_fd; + pfds[nfds].events = POLLIN; + pfds[nfds].revents = 0; + map[nfds].instance_index = i; + map[nfds].kind = PR_POLL_SERIAL; + nfds++; + } + } + + if (nfds > 0) { + int pr = poll(pfds, nfds, (int)PACKET_RADIO_POLL_MS); + + if (pr < 0) { + if (errno != EINTR) { + hybbx_log_warn("[packet_radio] poll failed"); + } + } else if (pr > 0) { + nfds_t j; + + for (j = 0; j < nfds; j++) { + short rev = pfds[j].revents; + + if (rev == 0) { + continue; + } + + if ((rev & (POLLERR | POLLHUP | POLLNVAL)) != 0) { + if (map[j].kind == PR_POLL_CIRCUIT) { + packet_radio_instance_t *inst = + &g_instances[map[j].instance_index]; + + hybbx_log_warn("[packet_radio%u] circuit hangup — " + "reconnecting", + inst->index + 1); + instance_circuit_reconnect(inst); + } + continue; + } + + if ((rev & POLLIN) == 0) { + continue; + } + + if (map[j].kind == PR_POLL_CIRCUIT) { + instance_drain_circuit(&g_instances[map[j].instance_index]); + } else { + packet_radio_instance_t *inst = + &g_instances[map[j].instance_index]; + + if (inst->tnc != NULL) { + (void)hybbx_tnc_poll(inst->tnc); + } + } + } + } + } else { + packet_radio_poll_sleep_ms(PACKET_RADIO_POLL_MS); + } +} +#endif + +static void packet_radio_service_maintenance(void) +{ + unsigned i; + + for (i = 0; i < g_instance_count; i++) { + packet_radio_instance_t *inst = &g_instances[i]; + + if (inst->circuit_fd < 0) { + inst->circuit_reconnect_polls++; + if (inst->circuit_reconnect_polls >= 50u) { + inst->circuit_reconnect_polls = 0; + (void)instance_connect_circuit(inst); + } + } + + if (inst->flow_cancel) { + inst->flow_cancel = 0; + inst->flow_paused = 0; + instance_circuit_reconnect(inst); + } + } +} + +#if defined(_WIN32) +static void packet_radio_poll_win32(void) +{ + unsigned i; + + for (i = 0; i < g_instance_count; i++) { + packet_radio_instance_t *inst = &g_instances[i]; + + if (inst->tnc != NULL) { + (void)hybbx_tnc_poll(inst->tnc); + } + + if (inst->circuit_fd >= 0) { + instance_drain_circuit(inst); + } + } +} +#endif + +static void *packet_radio_poll_thread(void *arg) +{ + (void)arg; + + while (g_radio_running) { +#if !defined(_WIN32) + packet_radio_poll_instances(); +#else + packet_radio_poll_win32(); + packet_radio_poll_sleep_ms(PACKET_RADIO_POLL_MS); +#endif + packet_radio_service_maintenance(); + } + + return NULL; +} + +static hybbx_result_t instance_start(packet_radio_instance_t *inst, + const char *config, + unsigned index) +{ + hybbx_result_t rc; + hybbx_tnc_frame_cb frame_cb = on_tnc_ax25_frame; + hybbx_tnc_ui_cb ui_cb = NULL; + + if (inst == NULL || config == NULL) { + return HYBBX_ERR_INVALID; + } + + memset(inst, 0, sizeof(*inst)); + inst->circuit_fd = -1; + inst->index = index; + + rc = hybbx_packet_radio_config_parse(config, &inst->config); + if (rc != HYBBX_OK) { + hybbx_log_warn("[packet_radio%u] invalid configuration", index + 1); + instance_shutdown(inst); + return rc; + } + + if (g_service != NULL) { + const hybbx_broadcast_config_t *bc = + hybbx_service_get_broadcast(g_service); + + if (bc != NULL && bc->ax25_mycall[0] != '\0') { + size_t n = strlen(bc->ax25_mycall) + 1; + char *mc = malloc(n); + + if (mc != NULL) { + memcpy(mc, bc->ax25_mycall, n); + free(inst->config.mycall); + inst->config.mycall = mc; + hybbx_log_info("[packet_radio%u] ax25 source=%s (from [broadcast] ax25_mycall)", + index + 1, inst->config.mycall); + } + } + } + + hybbx_log_info("[packet_radio%u] tnc=%s protocol=%s device_type=%s device=%s " + "band=%s duplex=%s modulation=%s circuit=%s:%u " + "(HBX over internal TCP)", + index + 1, + tnc_name(inst->config.tnc), + protocol_name(inst->config.protocol), + device_type_name(inst->config.device_type), + inst->config.device, + hybbx_packet_radio_band_name(inst->config.params.band), + hybbx_packet_radio_duplex_name(inst->config.params.duplex), + hybbx_packet_radio_modulation_name(inst->config.params.modulation), + inst->config.circuit_host, + inst->config.circuit_port); + if (inst->config.frequency_mhz != NULL && + inst->config.frequency_mhz[0] != '\0') { + hybbx_log_info("[packet_radio%u] frequency_mhz=%s", index + 1, + inst->config.frequency_mhz); + } + + rc = instance_connect_circuit(inst); + if (rc != HYBBX_OK) { + instance_shutdown(inst); + return rc; + } + + if (inst->config.protocol == HYBBX_PACKET_RADIO_PROTO_HOSTMODE) { + frame_cb = NULL; + ui_cb = on_tnc_host_ui; + } + + rc = hybbx_tnc_open(&inst->tnc, &inst->config, frame_cb, ui_cb, inst); + if (rc != HYBBX_OK) { + instance_shutdown(inst); + return rc; + } + + return HYBBX_OK; +} + +static hybbx_result_t packet_radio_init(hybbx_service_t *service) +{ + g_service = service; + g_instance_count = 0; + g_radio_running = 0; + return HYBBX_OK; +} + +static void packet_radio_shutdown(void) +{ + unsigned i; + + g_radio_running = 0; + if (g_poll_thread_started) { + pthread_join(g_poll_thread, NULL); + g_poll_thread_started = 0; + } + + for (i = 0; i < g_instance_count; i++) { + instance_shutdown(&g_instances[i]); + } + g_instance_count = 0; +} + +static hybbx_result_t packet_radio_start(const char *config) +{ + char scratch[4096]; + const char *cursor; + hybbx_max25_config_t max25; + hybbx_max25_status_t max25_status; + hybbx_result_t rc = HYBBX_OK; + unsigned started = 0; + unsigned section_num = 0; + unsigned local_edges; + + (void)g_service; + + if (config == NULL) { + return HYBBX_ERR_INVALID; + } + + packet_radio_shutdown(); + g_poll_thread = (pthread_t)0; + + cursor = hybbx_max25_config_skip_prefix(config, &max25); + local_edges = packet_radio_count_local_edges(cursor); + if (local_edges > 0u) { + rc = hybbx_max25_wait_ready(&max25); + if (rc != HYBBX_OK) { + return rc; + } + } + + while (*cursor != '\0' && g_instance_count < HYBBX_PACKET_RADIO_MAX_INSTANCES) { + const char *sep = strchr(cursor, HYBBX_PACKET_RADIO_INSTANCE_SEP); + size_t len = sep != NULL ? (size_t)(sep - cursor) : strlen(cursor); + + if (len == 0) { + cursor = sep != NULL ? sep + 1 : cursor + len; + if (sep == NULL) { + break; + } + continue; + } + + if (len >= sizeof(scratch)) { + packet_radio_shutdown(); + return HYBBX_ERR_INVALID; + } + + memcpy(scratch, cursor, len); + scratch[len] = '\0'; + section_num++; + + if (!hybbx_packet_radio_section_is_local_edge(scratch)) { + hybbx_log_info("[packet_radio%u] bridge registry only — no local TNC", + section_num); + cursor = sep != NULL ? sep + 1 : cursor + len; + if (sep == NULL) { + break; + } + continue; + } + + rc = instance_start(&g_instances[g_instance_count], scratch, + g_instance_count); + if (rc != HYBBX_OK) { + packet_radio_shutdown(); + return rc; + } + + g_instance_count++; + started++; + cursor = sep != NULL ? sep + 1 : cursor + len; + if (sep == NULL) { + break; + } + } + + if (started == 0) { + return HYBBX_OK; + } + + g_radio_running = 1; + if (pthread_create(&g_poll_thread, NULL, packet_radio_poll_thread, + NULL) != 0) { + hybbx_log_warn("[packet_radio] poll thread failed"); + packet_radio_shutdown(); + return HYBBX_ERR_IO; + } + g_poll_thread_started = 1; + + hybbx_log_info("[packet_radio] %u TNC instance(s) active", started); + return HYBBX_OK; +} + +static hybbx_result_t packet_radio_stop(void) +{ + hybbx_log_info("[packet_radio] stop"); + packet_radio_shutdown(); + return HYBBX_OK; +} + +const hybbx_transport_plugin_t hybbx_plugin_packet_radio = { + .name = "packet_radio", + .kind = HYBBX_TRANSPORT_PACKET_RADIO, + .version = 1, + .init = packet_radio_init, + .shutdown = packet_radio_shutdown, + .start = packet_radio_start, + .stop = packet_radio_stop, + .write = NULL, +}; diff --git a/plugins/packet_radio/packet_radio_config.c b/plugins/packet_radio/packet_radio_config.c new file mode 100644 index 0000000..f8e2d65 --- /dev/null +++ b/plugins/packet_radio/packet_radio_config.c @@ -0,0 +1,786 @@ +#include "hybbx/packet_radio.h" +#include "hybbx/tnc.h" +#include "hybbx/circuit.h" +#include "hybbx/util.h" +#include "hybbx/log.h" + +#include <stdlib.h> +#include <string.h> + +static char *hybbx_strdup(const char *s) +{ + size_t len; + char *copy; + + if (s == NULL) { + return NULL; + } + + len = strlen(s) + 1; + copy = malloc(len); + if (copy != NULL) { + memcpy(copy, s, len); + } + return copy; +} + +static int str_ieq(const char *a, const char *b) +{ + if (a == NULL || b == NULL) { + return 0; + } + + while (*a != '\0' && *b != '\0') { + char ca = (char)(*a >= 'A' && *a <= 'Z' ? *a + 32 : *a); + char cb = (char)(*b >= 'A' && *b <= 'Z' ? *b + 32 : *b); + + if (ca != cb) { + return 0; + } + a++; + b++; + } + + return *a == '\0' && *b == '\0'; +} + +static const char *find_kv(const char *config, const char *key, + char *scratch, size_t scratch_len) +{ + const char *cursor = config; + size_t key_len = strlen(key); + + if (config == NULL || key == NULL) { + return NULL; + } + + while (*cursor != '\0') { + const char *sep = strchr(cursor, ';'); + const char *end = sep != NULL ? sep : cursor + strlen(cursor); + const char *eq = strchr(cursor, '='); + + if (eq != NULL && eq < end && (size_t)(eq - cursor) == key_len && + strncmp(cursor, key, key_len) == 0) { + const char *value = eq + 1; + size_t value_len = (size_t)(end - value); + + if (value_len >= scratch_len) { + value_len = scratch_len - 1; + } + memcpy(scratch, value, value_len); + scratch[value_len] = '\0'; + return scratch; + } + + if (sep == NULL) { + break; + } + cursor = sep + 1; + } + + return NULL; +} + +int hybbx_packet_radio_section_is_local_edge(const char *config) +{ + char scratch[64]; + + if (config == NULL) { + return 0; + } + + if (find_kv(config, "circuit_host", scratch, sizeof(scratch)) != NULL) { + return 1; + } + if (find_kv(config, "device", scratch, sizeof(scratch)) != NULL) { + return 1; + } + if (find_kv(config, "tnc", scratch, sizeof(scratch)) != NULL) { + return 1; + } + if (find_kv(config, "protocol", scratch, sizeof(scratch)) != NULL) { + return 1; + } + + return 0; +} + +static hybbx_packet_radio_device_type_t parse_device_type(const char *value) +{ + if (value == NULL) { + return HYBBX_PACKET_RADIO_DEVICE_USB; + } + + if (str_ieq(value, "usb") || str_ieq(value, "ax25") || + str_ieq(value, "ax.25")) { + return HYBBX_PACKET_RADIO_DEVICE_USB; + } + + if (str_ieq(value, "serial") || str_ieq(value, "com") || + str_ieq(value, "rs232") || str_ieq(value, "v24")) { + return HYBBX_PACKET_RADIO_DEVICE_SERIAL; + } + + return HYBBX_PACKET_RADIO_DEVICE_USB; +} + +static const char *default_device_for_type(hybbx_packet_radio_device_type_t type) +{ + if (type == HYBBX_PACKET_RADIO_DEVICE_SERIAL) { + return HYBBX_PACKET_RADIO_DEFAULT_DEVICE_SERIAL; + } + return HYBBX_PACKET_RADIO_DEFAULT_DEVICE_USB; +} + +static hybbx_packet_radio_tnc_t parse_tnc(const char *value) +{ + if (value == NULL || str_ieq(value, "tnc2c") || str_ieq(value, "tnc2-c")) { + return HYBBX_PACKET_RADIO_TNC_TNC2C; + } + + if (str_ieq(value, "tnc2") || str_ieq(value, "tnc-2") || + str_ieq(value, "pktnc2") || str_ieq(value, "pk-tnc2") || + str_ieq(value, "tapr") || str_ieq(value, "thefirmware") || + str_ieq(value, "tf")) { + return HYBBX_PACKET_RADIO_TNC_TNC2; + } + + if (str_ieq(value, "pk232") || str_ieq(value, "pk-232") || + str_ieq(value, "aea") || str_ieq(value, "pk87") || + str_ieq(value, "pk-87")) { + return HYBBX_PACKET_RADIO_TNC_PK232; + } + + if (str_ieq(value, "mfj1278") || str_ieq(value, "mfj-1278") || + str_ieq(value, "mfj1278b") || str_ieq(value, "mfj")) { + return HYBBX_PACKET_RADIO_TNC_MFJ1278; + } + + if (str_ieq(value, "kantronics") || str_ieq(value, "kpc") || + str_ieq(value, "kpc3") || str_ieq(value, "kpc9612")) { + return HYBBX_PACKET_RADIO_TNC_KANTRONICS; + } + + if (str_ieq(value, "baycom") || str_ieq(value, "bay-com")) { + return HYBBX_PACKET_RADIO_TNC_BAYCOM; + } + + if (str_ieq(value, "pccom") || str_ieq(value, "pc-com") || + str_ieq(value, "albrecht")) { + return HYBBX_PACKET_RADIO_TNC_PCCOM; + } + + if (str_ieq(value, "generic") || str_ieq(value, "tnc")) { + return HYBBX_PACKET_RADIO_TNC_GENERIC; + } + + return HYBBX_PACKET_RADIO_TNC_TNC2C; +} + +static hybbx_packet_radio_protocol_t parse_protocol(const char *value) +{ + if (value == NULL || str_ieq(value, "kiss")) { + return HYBBX_PACKET_RADIO_PROTO_KISS; + } + + if (str_ieq(value, "hostmode") || str_ieq(value, "host") || + str_ieq(value, "tnc2")) { + return HYBBX_PACKET_RADIO_PROTO_HOSTMODE; + } + + if (str_ieq(value, "6pack") || str_ieq(value, "sixpack")) { + return HYBBX_PACKET_RADIO_PROTO_SIXPACK; + } + + return HYBBX_PACKET_RADIO_PROTO_KISS; +} + +static hybbx_tnc2c_modem_t parse_modem(const char *value) +{ + if (value == NULL || str_ieq(value, "tcm3105") || str_ieq(value, "1200")) { + return HYBBX_TNC2C_MODEM_TCM3105; + } + + if (str_ieq(value, "9600")) { + return HYBBX_TNC2C_MODEM_9600; + } + + if (str_ieq(value, "g3ruh") || str_ieq(value, "g3ruh_fsk") || + str_ieq(value, "fsk9600")) { + return HYBBX_TNC2C_MODEM_9600; + } + + if (str_ieq(value, "19200")) { + return HYBBX_TNC2C_MODEM_19200; + } + + return HYBBX_TNC2C_MODEM_TCM3105; +} + +static float parse_clock_mhz(const char *value) +{ + if (value == NULL) { + return HYBBX_TNC2C_CLOCK_4_9_MHZ; + } + + if (str_ieq(value, "9.8") || str_ieq(value, "9,8")) { + return HYBBX_TNC2C_CLOCK_9_8_MHZ; + } + + if (str_ieq(value, "10") || str_ieq(value, "10.0")) { + return HYBBX_TNC2C_CLOCK_10_MHZ; + } + + return HYBBX_TNC2C_CLOCK_4_9_MHZ; +} + +static unsigned int parse_uint(const char *value, unsigned int default_value) +{ + char *end = NULL; + unsigned long n; + + if (value == NULL || value[0] == '\0') { + return default_value; + } + + n = strtoul(value, &end, 10); + if (end == value || n == 0) { + return default_value; + } + + return (unsigned int)n; +} + +static hybbx_packet_radio_band_t parse_radio_band(const char *value) +{ + if (value == NULL || value[0] == '\0') { + return HYBBX_PACKET_RADIO_BAND_UNSET; + } + + if (str_ieq(value, "cb") || str_ieq(value, "11m") || + str_ieq(value, "27mhz") || str_ieq(value, "citizen")) { + return HYBBX_PACKET_RADIO_BAND_CB; + } + + if (str_ieq(value, "amateur") || str_ieq(value, "ham") || + str_ieq(value, "amateurfunk") || str_ieq(value, "afu")) { + return HYBBX_PACKET_RADIO_BAND_AMATEUR; + } + + return HYBBX_PACKET_RADIO_BAND_UNSET; +} + +static hybbx_packet_radio_duplex_t parse_radio_duplex(const char *value) +{ + if (value == NULL || value[0] == '\0') { + return HYBBX_PACKET_RADIO_DUPLEX_UNSET; + } + + if (str_ieq(value, "full") || str_ieq(value, "full-duplex") || + str_ieq(value, "fullduplex") || str_ieq(value, "fulldup")) { + return HYBBX_PACKET_RADIO_DUPLEX_FULL; + } + + if (str_ieq(value, "half") || str_ieq(value, "half-duplex") || + str_ieq(value, "duplex") || str_ieq(value, "simplex")) { + return HYBBX_PACKET_RADIO_DUPLEX_HALF; + } + + return HYBBX_PACKET_RADIO_DUPLEX_UNSET; +} + +static hybbx_packet_radio_modulation_t parse_modulation(const char *value) +{ + if (value == NULL || value[0] == '\0') { + return HYBBX_PACKET_RADIO_MOD_UNSET; + } + + if (str_ieq(value, "g3ruh") || str_ieq(value, "g3ruh_fsk") || + str_ieq(value, "fsk9600") || str_ieq(value, "9600fsk")) { + return HYBBX_PACKET_RADIO_MOD_G3RUH_FSK; + } + + if (str_ieq(value, "afsk") || str_ieq(value, "bell202") || + str_ieq(value, "tcm3105") || str_ieq(value, "1200")) { + return HYBBX_PACKET_RADIO_MOD_AFSK; + } + + return HYBBX_PACKET_RADIO_MOD_UNSET; +} + +static hybbx_tnc_serial_parity_t parse_serial_parity(const char *value) +{ + if (value == NULL || value[0] == '\0') { + return HYBBX_TNC_SERIAL_PARITY_UNSET; + } + + if (str_ieq(value, "even") || str_ieq(value, "e")) { + return HYBBX_TNC_SERIAL_PARITY_EVEN; + } + + if (str_ieq(value, "odd") || str_ieq(value, "o")) { + return HYBBX_TNC_SERIAL_PARITY_ODD; + } + + if (str_ieq(value, "none") || str_ieq(value, "n") || str_ieq(value, "off")) { + return HYBBX_TNC_SERIAL_PARITY_NONE; + } + + return HYBBX_TNC_SERIAL_PARITY_UNSET; +} + +static void apply_serial_line(hybbx_packet_radio_config_t *out, const char *value) +{ + if (value == NULL || value[0] == '\0') { + return; + } + + if (str_ieq(value, "7e1")) { + out->params.data_bits = 7; + out->params.serial_parity = HYBBX_TNC_SERIAL_PARITY_EVEN; + out->params.stop_bits = 1; + return; + } + + if (str_ieq(value, "8n1")) { + out->params.data_bits = 8; + out->params.serial_parity = HYBBX_TNC_SERIAL_PARITY_NONE; + out->params.stop_bits = 1; + return; + } + + if (str_ieq(value, "7o1")) { + out->params.data_bits = 7; + out->params.serial_parity = HYBBX_TNC_SERIAL_PARITY_ODD; + out->params.stop_bits = 1; + } +} + +static hybbx_kiss_entry_t parse_kiss_entry(const char *value) +{ + if (value == NULL || value[0] == '\0') { + return HYBBX_KISS_ENTRY_UNSET; + } + + if (str_ieq(value, "none") || str_ieq(value, "skip") || + str_ieq(value, "off") || str_ieq(value, "no")) { + return HYBBX_KISS_ENTRY_NONE; + } + + if (str_ieq(value, "kiss_on") || str_ieq(value, "kiss-on") || + str_ieq(value, "command") || str_ieq(value, "on") || + str_ieq(value, "yes")) { + return HYBBX_KISS_ENTRY_KISS_ON; + } + + if (str_ieq(value, "esc_at_k") || str_ieq(value, "esc@k") || + str_ieq(value, "esc") || str_ieq(value, "@k")) { + return HYBBX_KISS_ENTRY_ESC_AT_K; + } + + if (str_ieq(value, "auto")) { + return HYBBX_KISS_ENTRY_AUTO; + } + + return HYBBX_KISS_ENTRY_UNSET; +} + +static hybbx_kiss_exit_t parse_kiss_exit(const char *value) +{ + if (value == NULL || value[0] == '\0') { + return HYBBX_KISS_EXIT_UNSET; + } + + if (str_ieq(value, "none") || str_ieq(value, "skip") || + str_ieq(value, "off") || str_ieq(value, "no")) { + return HYBBX_KISS_EXIT_NONE; + } + + if (str_ieq(value, "kiss_off") || str_ieq(value, "kiss-off") || + str_ieq(value, "command")) { + return HYBBX_KISS_EXIT_KISS_OFF; + } + + if (str_ieq(value, "kiss_frame") || str_ieq(value, "frame") || + str_ieq(value, "fend")) { + return HYBBX_KISS_EXIT_KISS_FRAME; + } + + if (str_ieq(value, "auto")) { + return HYBBX_KISS_EXIT_AUTO; + } + + return HYBBX_KISS_EXIT_UNSET; +} + +static unsigned int default_radio_baud_for_modem(hybbx_tnc2c_modem_t modem) +{ + switch (modem) { + case HYBBX_TNC2C_MODEM_9600: + return 9600; + case HYBBX_TNC2C_MODEM_19200: + return 19200; + case HYBBX_TNC2C_MODEM_TCM3105: + default: + return HYBBX_TNC2C_DEFAULT_RADIO_BAUD; + } +} + +static hybbx_result_t parse_via_list(hybbx_packet_radio_config_t *out, + const char *value) +{ + char scratch[256]; + char *cursor; + char *token; + + if (value == NULL || value[0] == '\0') { + return HYBBX_OK; + } + + if (strlen(value) >= sizeof(scratch)) { + return HYBBX_ERR_INVALID; + } + + memcpy(scratch, value, strlen(value) + 1); + cursor = scratch; + + while ((token = strtok(cursor, ",")) != NULL && out->via_count < HYBBX_PACKET_RADIO_VIA_MAX) { + char *item = hybbx_strdup(token); + if (item == NULL) { + return HYBBX_ERR_NOMEM; + } + out->via[out->via_count++] = item; + cursor = NULL; + } + + return HYBBX_OK; +} + +int hybbx_packet_radio_baud_valid(unsigned int baud) +{ + return baud > 0; +} + +hybbx_result_t hybbx_packet_radio_config_parse(const char *config, + hybbx_packet_radio_config_t *out) +{ + char scratch[256]; + const char *value; + unsigned long baud; + hybbx_result_t rc; + + if (out == NULL) { + return HYBBX_ERR_INVALID; + } + + memset(out, 0, sizeof(*out)); + + out->tnc = HYBBX_PACKET_RADIO_TNC_TNC2C; + out->protocol = 0; /* profile may choose */ + out->params.clock_mhz = HYBBX_TNC2C_CLOCK_4_9_MHZ; + out->params.modem = HYBBX_TNC2C_MODEM_TCM3105; + out->params.radio_baud = HYBBX_TNC2C_DEFAULT_RADIO_BAUD; + out->params.kiss_port = 0; + out->params.txdelay = 50; + out->params.persist = 0; + out->params.slot = 10; + out->params.txtail = 0; + out->params.band = HYBBX_PACKET_RADIO_BAND_UNSET; + out->params.duplex = HYBBX_PACKET_RADIO_DUPLEX_UNSET; + out->params.modulation = HYBBX_PACKET_RADIO_MOD_UNSET; + out->params.kiss_entry = HYBBX_KISS_ENTRY_UNSET; + out->params.kiss_exit = HYBBX_KISS_EXIT_UNSET; + out->params.host_connect_on_start = 0; + out->params.assert_modem_lines = -1; + + value = find_kv(config, "device_type", scratch, sizeof(scratch)); + out->device_type = parse_device_type(value); + + value = find_kv(config, "device", scratch, sizeof(scratch)); + if (value == NULL) { + out->device = hybbx_strdup(default_device_for_type(out->device_type)); + } else { + out->device = hybbx_strdup(value); + } + + if (out->device == NULL) { + hybbx_packet_radio_config_free(out); + return HYBBX_ERR_NOMEM; + } + + value = find_kv(config, "baud", scratch, sizeof(scratch)); + if (value == NULL || value[0] == '\0') { + out->baud = HYBBX_PACKET_RADIO_DEFAULT_BAUD; + } else { + baud = strtoul(value, NULL, 10); + if (!hybbx_packet_radio_baud_valid((unsigned int)baud)) { + hybbx_packet_radio_config_free(out); + return HYBBX_ERR_INVALID; + } + out->baud = (unsigned int)baud; + } + + value = find_kv(config, "tnc", scratch, sizeof(scratch)); + out->tnc = parse_tnc(value); + + if (out->tnc == HYBBX_PACKET_RADIO_TNC_BAYCOM || + out->tnc == HYBBX_PACKET_RADIO_TNC_PCCOM) { + hybbx_log_warn("[packet_radio] tnc=%s is not a packet_radio profile — " + "use tnc=tnc2c (or generic) with kiss_entry=none after " + "max25d prep; BayCom/based SER12 = MAX25 max25e0 (bcpr), " + "optional [networks] baycom=yes on non-UN1TME sites only", + hybbx_packet_radio_tnc_name(out->tnc)); + hybbx_packet_radio_config_free(out); + return HYBBX_ERR_INVALID; + } + + value = find_kv(config, "protocol", scratch, sizeof(scratch)); + if (value != NULL) { + out->protocol = parse_protocol(value); + } + + value = find_kv(config, "clock_mhz", scratch, sizeof(scratch)); + out->params.clock_mhz = parse_clock_mhz(value); + + value = find_kv(config, "modem", scratch, sizeof(scratch)); + out->params.modem = parse_modem(value); + + value = find_kv(config, "radio_baud", scratch, sizeof(scratch)); + if (value == NULL) { + out->params.radio_baud = default_radio_baud_for_modem(out->params.modem); + } else { + unsigned int radio_baud = parse_uint(value, 0); + if (!hybbx_packet_radio_baud_valid(radio_baud)) { + hybbx_packet_radio_config_free(out); + return HYBBX_ERR_INVALID; + } + out->params.radio_baud = radio_baud; + } + + value = find_kv(config, "kiss_port", scratch, sizeof(scratch)); + out->params.kiss_port = parse_uint(value, 0); + + value = find_kv(config, "txdelay", scratch, sizeof(scratch)); + out->params.txdelay = parse_uint(value, out->params.txdelay); + + value = find_kv(config, "persist", scratch, sizeof(scratch)); + out->params.persist = parse_uint(value, out->params.persist); + + value = find_kv(config, "slot", scratch, sizeof(scratch)); + out->params.slot = parse_uint(value, out->params.slot); + + value = find_kv(config, "txtail", scratch, sizeof(scratch)); + out->params.txtail = parse_uint(value, out->params.txtail); + + value = find_kv(config, "radio_band", scratch, sizeof(scratch)); + if (value != NULL) { + out->params.band = parse_radio_band(value); + } + + value = find_kv(config, "radio_duplex", scratch, sizeof(scratch)); + if (value == NULL) { + value = find_kv(config, "duplex", scratch, sizeof(scratch)); + } + if (value != NULL) { + out->params.duplex = parse_radio_duplex(value); + } + + value = find_kv(config, "fullduplex", scratch, sizeof(scratch)); + if (value != NULL && + out->params.duplex == HYBBX_PACKET_RADIO_DUPLEX_UNSET) { + out->params.duplex = hybbx_parse_bool(value, 0) ? + HYBBX_PACKET_RADIO_DUPLEX_FULL : + HYBBX_PACKET_RADIO_DUPLEX_HALF; + } + + value = find_kv(config, "g3ruh_fsk", scratch, sizeof(scratch)); + if (value != NULL) { + out->params.modulation = hybbx_parse_bool(value, 0) ? + HYBBX_PACKET_RADIO_MOD_G3RUH_FSK : + HYBBX_PACKET_RADIO_MOD_AFSK; + } + + value = find_kv(config, "modulation", scratch, sizeof(scratch)); + if (value != NULL) { + hybbx_packet_radio_modulation_t mod = parse_modulation(value); + if (mod != HYBBX_PACKET_RADIO_MOD_UNSET) { + out->params.modulation = mod; + } + } + + value = find_kv(config, "kiss_entry", scratch, sizeof(scratch)); + if (value != NULL) { + hybbx_kiss_entry_t kiss_entry = parse_kiss_entry(value); + if (kiss_entry != HYBBX_KISS_ENTRY_UNSET) { + out->params.kiss_entry = kiss_entry; + } + } + + value = find_kv(config, "kiss_exit", scratch, sizeof(scratch)); + if (value != NULL) { + hybbx_kiss_exit_t kiss_exit = parse_kiss_exit(value); + if (kiss_exit != HYBBX_KISS_EXIT_UNSET) { + out->params.kiss_exit = kiss_exit; + } + } + + value = find_kv(config, "host_connect", scratch, sizeof(scratch)); + out->params.host_connect_on_start = hybbx_parse_bool(value, + out->params.host_connect_on_start); + + value = find_kv(config, "serial_line", scratch, sizeof(scratch)); + if (value != NULL) { + apply_serial_line(out, value); + } + + value = find_kv(config, "data_bits", scratch, sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + unsigned int data_bits = parse_uint(value, 0); + if (data_bits == 7 || data_bits == 8) { + out->params.data_bits = data_bits; + } + } + + value = find_kv(config, "parity", scratch, sizeof(scratch)); + if (value != NULL) { + hybbx_tnc_serial_parity_t parity = parse_serial_parity(value); + if (parity != HYBBX_TNC_SERIAL_PARITY_UNSET) { + out->params.serial_parity = parity; + } + } + + value = find_kv(config, "stop_bits", scratch, sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + unsigned int stop_bits = parse_uint(value, 0); + if (stop_bits == 1 || stop_bits == 2) { + out->params.stop_bits = stop_bits; + } + } + + value = find_kv(config, "assert_modem_lines", scratch, sizeof(scratch)); + if (value == NULL) { + value = find_kv(config, "rts_dtr", scratch, sizeof(scratch)); + } + if (value != NULL) { + out->params.assert_modem_lines = hybbx_parse_bool(value, 0) ? 1 : 0; + } + + value = find_kv(config, "mycall", scratch, sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + out->mycall = hybbx_strdup(value); + if (out->mycall == NULL) { + hybbx_packet_radio_config_free(out); + return HYBBX_ERR_NOMEM; + } + } + + value = find_kv(config, "dest", scratch, sizeof(scratch)); + if (value == NULL) { + value = find_kv(config, "dest_call", scratch, sizeof(scratch)); + } + if (value != NULL && value[0] != '\0') { + out->dest_call = hybbx_strdup(value); + if (out->dest_call == NULL) { + hybbx_packet_radio_config_free(out); + return HYBBX_ERR_NOMEM; + } + } + + value = find_kv(config, "via", scratch, sizeof(scratch)); + rc = parse_via_list(out, value); + if (rc != HYBBX_OK) { + hybbx_packet_radio_config_free(out); + return rc; + } + + value = find_kv(config, "circuit_host", scratch, sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + out->circuit_host = hybbx_strdup(value); + if (out->circuit_host == NULL) { + hybbx_packet_radio_config_free(out); + return HYBBX_ERR_NOMEM; + } + } + + value = find_kv(config, "circuit_port", scratch, sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + out->circuit_port = parse_uint(value, HYBBX_CIRCUIT_DEFAULT_PORT); + } + + if (out->circuit_port == 0) { + out->circuit_port = HYBBX_CIRCUIT_DEFAULT_PORT; + } + if (out->circuit_host == NULL) { + out->circuit_host = hybbx_strdup("127.0.0.1"); + if (out->circuit_host == NULL) { + hybbx_packet_radio_config_free(out); + return HYBBX_ERR_NOMEM; + } + } + + value = find_kv(config, "link_id", scratch, sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + out->link_id = hybbx_strdup(value); + if (out->link_id == NULL) { + hybbx_packet_radio_config_free(out); + return HYBBX_ERR_NOMEM; + } + } + + value = find_kv(config, "link_password", scratch, sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + out->link_password = hybbx_strdup(value); + if (out->link_password == NULL) { + hybbx_packet_radio_config_free(out); + return HYBBX_ERR_NOMEM; + } + } + + value = find_kv(config, "link_role", scratch, sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + out->link_role = hybbx_strdup(value); + if (out->link_role == NULL) { + hybbx_packet_radio_config_free(out); + return HYBBX_ERR_NOMEM; + } + } + + value = find_kv(config, "frequency_mhz", scratch, sizeof(scratch)); + if (value == NULL || value[0] == '\0') { + value = find_kv(config, "frequency", scratch, sizeof(scratch)); + } + if (value != NULL && value[0] != '\0') { + out->frequency_mhz = hybbx_strdup(value); + if (out->frequency_mhz == NULL) { + hybbx_packet_radio_config_free(out); + return HYBBX_ERR_NOMEM; + } + } + + (void)hybbx_tnc_profile_apply_defaults(out); + return HYBBX_OK; +} + +void hybbx_packet_radio_config_free(hybbx_packet_radio_config_t *config) +{ + unsigned i; + + if (config == NULL) { + return; + } + + free(config->device); + free(config->mycall); + free(config->dest_call); + free(config->circuit_host); + free(config->link_id); + free(config->link_password); + free(config->link_role); + free(config->frequency_mhz); + for (i = 0; i < config->via_count; i++) { + free(config->via[i]); + config->via[i] = NULL; + } + + memset(config, 0, sizeof(*config)); +} diff --git a/plugins/packet_radio/serial_port.c b/plugins/packet_radio/serial_port.c new file mode 100644 index 0000000..70dd92c --- /dev/null +++ b/plugins/packet_radio/serial_port.c @@ -0,0 +1,705 @@ +#if defined(__linux__) || defined(__GLIBC__) +#define _DEFAULT_SOURCE 1 +#endif + +#include "serial_port.h" +#include "hybbx/log.h" +#include "hybbx/util.h" + +#include <stdlib.h> +#include <string.h> +#include <stdio.h> + +void hybbx_serial_params_default(hybbx_serial_params_t *params, + unsigned int baud) +{ + if (params == NULL) { + return; + } + + memset(params, 0, sizeof(*params)); + params->baud = baud; + params->data_bits = 8; + params->parity = HYBBX_SERIAL_PARITY_NONE; + params->stop_bits = 1; + params->assert_modem_lines = 0; +} + +#if defined(_WIN32) + +#define WIN32_LEAN_AND_MEAN +#include <windows.h> + +struct hybbx_serial_port { + HANDLE handle; + unsigned int baud; +}; + +static int win32_normalize_device(const char *device, char *out, size_t out_len) +{ + const char *name; + unsigned com_num = 0; + + if (device == NULL || out_len == 0) { + return 0; + } + + if (strncmp(device, "\\\\.\\", 4) == 0) { + return hybbx_strlcpy(out, device, out_len) < out_len; + } + + name = device; + if (name[0] == '/' && strncmp(name, "/dev/ttyS", 9) == 0 && + name[9] >= '0' && name[9] <= '9' && name[10] == '\0') { + com_num = (unsigned)(name[9] - '0') + 1u; + return (size_t)snprintf(out, out_len, "\\\\.\\COM%u", com_num) < out_len; + } + + if ((name[0] == 'C' || name[0] == 'c') && + (name[1] == 'O' || name[1] == 'o') && + (name[2] == 'M' || name[2] == 'm')) { + name += 3; + } + + if (*name == '\0') { + return 0; + } + + while (*name >= '0' && *name <= '9') { + com_num = com_num * 10u + (unsigned)(*name - '0'); + name++; + } + if (*name != '\0' || com_num == 0) { + return 0; + } + + return (size_t)snprintf(out, out_len, "\\\\.\\COM%u", com_num) < out_len; +} + +static hybbx_result_t win32_configure(HANDLE handle, + const hybbx_serial_params_t *params) +{ + DCB dcb; + COMMTIMEOUTS timeouts; + + memset(&dcb, 0, sizeof(dcb)); + dcb.DCBlength = sizeof(dcb); + if (!GetCommState(handle, &dcb)) { + return HYBBX_ERR_IO; + } + + dcb.BaudRate = (DWORD)params->baud; + dcb.ByteSize = (BYTE)(params->data_bits == 7 ? 7 : 8); + switch (params->parity) { + case HYBBX_SERIAL_PARITY_EVEN: + dcb.Parity = EVENPARITY; + dcb.fParity = TRUE; + break; + case HYBBX_SERIAL_PARITY_ODD: + dcb.Parity = ODDPARITY; + dcb.fParity = TRUE; + break; + case HYBBX_SERIAL_PARITY_NONE: + default: + dcb.Parity = NOPARITY; + dcb.fParity = FALSE; + break; + } + dcb.StopBits = params->stop_bits == 2 ? TWOSTOPBITS : ONESTOPBIT; + dcb.fBinary = TRUE; + dcb.fOutxCtsFlow = FALSE; + dcb.fOutxDsrFlow = FALSE; + dcb.fDtrControl = params->assert_modem_lines ? + DTR_CONTROL_ENABLE : DTR_CONTROL_DISABLE; + dcb.fRtsControl = params->assert_modem_lines ? + RTS_CONTROL_ENABLE : RTS_CONTROL_DISABLE; + dcb.fOutX = FALSE; + dcb.fInX = FALSE; + dcb.fNull = FALSE; + + if (!SetCommState(handle, &dcb)) { + return HYBBX_ERR_IO; + } + + memset(&timeouts, 0, sizeof(timeouts)); + timeouts.ReadIntervalTimeout = MAXDWORD; + timeouts.ReadTotalTimeoutConstant = 0; + timeouts.ReadTotalTimeoutMultiplier = 0; + timeouts.WriteTotalTimeoutConstant = 0; + timeouts.WriteTotalTimeoutMultiplier = 0; + if (!SetCommTimeouts(handle, &timeouts)) { + return HYBBX_ERR_IO; + } + + PurgeComm(handle, PURGE_RXCLEAR | PURGE_TXCLEAR); + return HYBBX_OK; +} + +hybbx_result_t hybbx_serial_open(hybbx_serial_port_t **out, + const char *device, + const hybbx_serial_params_t *params) +{ + hybbx_serial_port_t *port; + char path[64]; + hybbx_result_t rc; + + if (out == NULL || device == NULL || params == NULL || params->baud == 0) { + return HYBBX_ERR_INVALID; + } + + if (!win32_normalize_device(device, path, sizeof(path))) { + return HYBBX_ERR_INVALID; + } + + port = calloc(1, sizeof(*port)); + if (port == NULL) { + return HYBBX_ERR_NOMEM; + } + + port->handle = CreateFileA(path, GENERIC_READ | GENERIC_WRITE, 0, NULL, + OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); + if (port->handle == INVALID_HANDLE_VALUE) { + free(port); + return HYBBX_ERR_IO; + } + + rc = win32_configure(port->handle, params); + if (rc != HYBBX_OK) { + CloseHandle(port->handle); + free(port); + return rc; + } + + port->baud = params->baud; + *out = port; + return HYBBX_OK; +} + +void hybbx_serial_close(hybbx_serial_port_t *port) +{ + if (port == NULL) { + return; + } + + if (port->handle != INVALID_HANDLE_VALUE) { + CloseHandle(port->handle); + } + free(port); +} + +hybbx_result_t hybbx_serial_write(hybbx_serial_port_t *port, + const uint8_t *data, size_t len) +{ + size_t written = 0; + + if (port == NULL || data == NULL) { + return HYBBX_ERR_INVALID; + } + + while (written < len) { + DWORD chunk = 0; + + if (!WriteFile(port->handle, data + written, + (DWORD)(len - written), &chunk, NULL)) { + return HYBBX_ERR_IO; + } + if (chunk == 0) { + return HYBBX_ERR_IO; + } + written += (size_t)chunk; + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_serial_read(hybbx_serial_port_t *port, + uint8_t *buf, size_t buf_len, + size_t *read_len) +{ + DWORD n = 0; + + if (port == NULL || buf == NULL || read_len == NULL) { + return HYBBX_ERR_INVALID; + } + + if (!ReadFile(port->handle, buf, (DWORD)buf_len, &n, NULL)) { + return HYBBX_ERR_IO; + } + + *read_len = (size_t)n; + return HYBBX_OK; +} + +int hybbx_serial_fd(const hybbx_serial_port_t *port) +{ + (void)port; + return -1; +} + +#elif defined(__AMIGA__) + +#include <proto/exec.h> +#include <devices/serial.h> + +struct hybbx_serial_port { + struct IOExtSer *io; + struct MsgPort *port; + char device_name[32]; + unsigned long unit; + unsigned int baud; +}; + +static int amiga_parse_device(const char *device, char *name, size_t name_len, + unsigned long *unit) +{ + const char *colon; + char *end; + + if (unit == NULL || name == NULL || name_len == 0) { + return 0; + } + + *unit = 0; + if (device == NULL || device[0] == '\0') { + hybbx_strlcpy(name, SERIALNAME, name_len); + return 1; + } + + colon = strchr(device, ':'); + if (colon != NULL) { + size_t prefix = (size_t)(colon - device); + + if (prefix == 0 || prefix >= name_len) { + return 0; + } + memcpy(name, device, prefix); + name[prefix] = '\0'; + *unit = strtoul(colon + 1, &end, 10); + return end != colon + 1 && *end == '\0'; + } + + if (device[0] >= '0' && device[0] <= '9') { + *unit = strtoul(device, &end, 10); + if (end != device && *end == '\0') { + hybbx_strlcpy(name, SERIALNAME, name_len); + return 1; + } + } + + hybbx_strlcpy(name, device, name_len); + return 1; +} + +static hybbx_result_t amiga_configure(struct IOExtSer *io, + const hybbx_serial_params_t *params) +{ + io->IOSer.io_Command = SDCMD_SETPARAMS; + io->io_Baud = (ULONG)params->baud; + io->io_ReadLen = (UBYTE)(params->data_bits == 7 ? 7 : 8); + io->io_WriteLen = (UBYTE)(params->data_bits == 7 ? 7 : 8); + io->io_StopBits = (UBYTE)(params->stop_bits == 2 ? 2 : 1); + io->io_SerFlags = (UBYTE)(io->io_SerFlags & (UBYTE)~SERF_PARTY_ON); + if (params->parity == HYBBX_SERIAL_PARITY_EVEN || + params->parity == HYBBX_SERIAL_PARITY_ODD) { + io->io_SerFlags |= SERF_PARTY_ON; + } + io->io_SerFlags |= SERF_XDISABLED; + + if (DoIO((struct IORequest *)io) != 0) { + return HYBBX_ERR_IO; + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_serial_open(hybbx_serial_port_t **out, + const char *device, + const hybbx_serial_params_t *params) +{ + hybbx_serial_port_t *port; + hybbx_result_t rc; + + if (out == NULL || device == NULL || params == NULL || params->baud == 0) { + return HYBBX_ERR_INVALID; + } + + port = calloc(1, sizeof(*port)); + if (port == NULL) { + return HYBBX_ERR_NOMEM; + } + + if (!amiga_parse_device(device, port->device_name, sizeof(port->device_name), + &port->unit)) { + free(port); + return HYBBX_ERR_INVALID; + } + + port->port = CreatePort(NULL, 0); + if (port->port == NULL) { + free(port); + return HYBBX_ERR_NOMEM; + } + + port->io = (struct IOExtSer *)CreateExtIO(port->port, + (LONG)sizeof(*port->io)); + if (port->io == NULL) { + DeletePort(port->port); + free(port); + return HYBBX_ERR_NOMEM; + } + + port->io->io_SerFlags = SERF_SHARED | SERF_XDISABLED; + if (OpenDevice(port->device_name, port->unit, + (struct IORequest *)port->io, 0) != 0) { + DeleteExtIO((struct IORequest *)port->io); + DeletePort(port->port); + free(port); + return HYBBX_ERR_IO; + } + + rc = amiga_configure(port->io, params); + if (rc != HYBBX_OK) { + CloseDevice((struct IORequest *)port->io); + DeleteExtIO((struct IORequest *)port->io); + DeletePort(port->port); + free(port); + return rc; + } + + port->baud = params->baud; + *out = port; + return HYBBX_OK; +} + +void hybbx_serial_close(hybbx_serial_port_t *port) +{ + if (port == NULL) { + return; + } + + if (port->io != NULL) { + CloseDevice((struct IORequest *)port->io); + DeleteExtIO((struct IORequest *)port->io); + } + if (port->port != NULL) { + DeletePort(port->port); + } + free(port); +} + +hybbx_result_t hybbx_serial_write(hybbx_serial_port_t *port, + const uint8_t *data, size_t len) +{ + if (port == NULL || data == NULL || len == 0) { + return HYBBX_ERR_INVALID; + } + + port->io->IOSer.io_Command = CMD_WRITE; + port->io->IOSer.io_Data = (APTR)data; + port->io->IOSer.io_Length = (ULONG)len; + port->io->IOSer.io_Flags = 0; + + if (DoIO((struct IORequest *)port->io) != 0) { + return HYBBX_ERR_IO; + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_serial_read(hybbx_serial_port_t *port, + uint8_t *buf, size_t buf_len, + size_t *read_len) +{ + if (port == NULL || buf == NULL || read_len == NULL || buf_len == 0) { + return HYBBX_ERR_INVALID; + } + + port->io->IOSer.io_Command = CMD_READ; + port->io->IOSer.io_Data = (APTR)buf; + port->io->IOSer.io_Length = (ULONG)buf_len; + port->io->IOSer.io_Flags = IOF_QUICK; + + if (DoIO((struct IORequest *)port->io) != 0) { + *read_len = 0; + return HYBBX_OK; + } + + *read_len = (size_t)port->io->IOSer.io_Actual; + return HYBBX_OK; +} + +int hybbx_serial_fd(const hybbx_serial_port_t *port) +{ + (void)port; + return -1; +} + +#else /* POSIX */ + +#include <errno.h> +#include <fcntl.h> +#include <stdio.h> +#include <sys/ioctl.h> +#include <termios.h> +#include <unistd.h> + +struct hybbx_serial_port { + int fd; + unsigned int baud; + int assert_modem_lines; +}; + +#ifndef cfmakeraw +static void hybbx_cfmakeraw(struct termios *tty) +{ + tty->c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | + IGNCR | ICRNL | IXON); + tty->c_oflag &= ~OPOST; + tty->c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN); + tty->c_cflag &= ~(CSIZE | PARENB); + tty->c_cflag |= CS8; +} +#define cfmakeraw hybbx_cfmakeraw +#endif + +static hybbx_result_t set_baud_rate(struct termios *tty, unsigned int baud) +{ +#if defined(__linux__) || defined(__GLIBC__) + if (cfsetspeed(tty, (speed_t)baud) == 0) { + return HYBBX_OK; + } +#endif + + { + speed_t speed = B0; + switch (baud) { + case 50: speed = B50; break; + case 75: speed = B75; break; + case 110: speed = B110; break; + case 134: speed = B134; break; + case 150: speed = B150; break; + case 200: speed = B200; break; + case 300: speed = B300; break; + case 600: speed = B600; break; + case 1200: speed = B1200; break; + case 1800: speed = B1800; break; + case 2400: speed = B2400; break; + case 4800: speed = B4800; break; + case 9600: speed = B9600; break; +#ifdef B19200 + case 19200: speed = B19200; break; +#endif +#ifdef B38400 + case 38400: speed = B38400; break; +#endif +#ifdef B57600 + case 57600: speed = B57600; break; +#endif +#ifdef B115200 + case 115200: speed = B115200; break; +#endif + default: + return HYBBX_ERR_UNSUPPORTED; + } + + cfsetispeed(tty, speed); + cfsetospeed(tty, speed); + } + + return HYBBX_OK; +} + +static hybbx_result_t posix_assert_modem_lines(int fd, int on) +{ +#ifdef TIOCMGET + int flags; + + /* BayCom KISS PTY (max25-bcpr): assert_modem_lines=0 — no UART modem bits. + * TIOCMGET on /dev/pts/* often fails; skip when not asserting. */ + if (!on) { + return HYBBX_OK; + } + + if (ioctl(fd, TIOCMGET, &flags) != 0) { + return HYBBX_ERR_IO; + } + flags |= TIOCM_RTS | TIOCM_DTR; + if (ioctl(fd, TIOCMSET, &flags) != 0) { + return HYBBX_ERR_IO; + } +#else + (void)fd; + (void)on; +#endif + return HYBBX_OK; +} + +static hybbx_result_t configure_port(int fd, const hybbx_serial_params_t *params) +{ + struct termios tty; + tcflag_t databits; + + if (tcgetattr(fd, &tty) != 0) { + return HYBBX_ERR_IO; + } + + cfmakeraw(&tty); + tty.c_cflag |= (CLOCAL | CREAD); + tty.c_cflag &= ~(CSIZE | PARENB | PARODD | CSTOPB); + if (params->data_bits == 7) { + databits = CS7; + } else { + databits = CS8; + } + tty.c_cflag |= databits; + if (params->parity == HYBBX_SERIAL_PARITY_EVEN) { + tty.c_cflag |= PARENB; + } else if (params->parity == HYBBX_SERIAL_PARITY_ODD) { + tty.c_cflag |= PARENB | PARODD; + } + if (params->stop_bits == 2) { + tty.c_cflag |= CSTOPB; + } + tty.c_cc[VMIN] = 0; + tty.c_cc[VTIME] = 1; + + if (set_baud_rate(&tty, params->baud) != HYBBX_OK) { + return HYBBX_ERR_UNSUPPORTED; + } + + if (tcsetattr(fd, TCSANOW, &tty) != 0) { + return HYBBX_ERR_IO; + } + + tcflush(fd, TCIOFLUSH); + return posix_assert_modem_lines(fd, params->assert_modem_lines); +} + +hybbx_result_t hybbx_serial_open(hybbx_serial_port_t **out, + const char *device, + const hybbx_serial_params_t *params) +{ + hybbx_serial_port_t *port; + hybbx_result_t rc; + + if (out == NULL || device == NULL || params == NULL || params->baud == 0) { + return HYBBX_ERR_INVALID; + } + + port = calloc(1, sizeof(*port)); + if (port == NULL) { + return HYBBX_ERR_NOMEM; + } + + port->fd = open(device, O_RDWR | O_NOCTTY | O_NONBLOCK); + if (port->fd < 0) { + hybbx_log_warn("[serial] open %s failed: %s", device, strerror(errno)); + free(port); + return HYBBX_ERR_IO; + } + + rc = configure_port(port->fd, params); + if (rc != HYBBX_OK) { + close(port->fd); + free(port); + return rc; + } + + port->baud = params->baud; + port->assert_modem_lines = params->assert_modem_lines; + *out = port; + return HYBBX_OK; +} + +void hybbx_serial_close(hybbx_serial_port_t *port) +{ + if (port == NULL) { + return; + } + + if (port->fd >= 0) { + close(port->fd); + } + free(port); +} + +hybbx_result_t hybbx_serial_write(hybbx_serial_port_t *port, + const uint8_t *data, size_t len) +{ + size_t written = 0; + + if (port == NULL || data == NULL) { + return HYBBX_ERR_INVALID; + } + + if (port->assert_modem_lines) { + hybbx_result_t rc = posix_assert_modem_lines(port->fd, 1); + + if (rc != HYBBX_OK) { + return rc; + } + } + + while (written < len) { + ssize_t n = write(port->fd, data + written, len - written); + + if (n < 0) { +#if EAGAIN == EWOULDBLOCK + if (errno == EAGAIN) { +#else + if (errno == EAGAIN || errno == EWOULDBLOCK) { +#endif + continue; + } + return HYBBX_ERR_IO; + } + if (n == 0) { + return HYBBX_ERR_IO; + } + written += (size_t)n; + } + + if (tcdrain(port->fd) != 0) { + return HYBBX_ERR_IO; + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_serial_read(hybbx_serial_port_t *port, + uint8_t *buf, size_t buf_len, + size_t *read_len) +{ + ssize_t n; + + if (port == NULL || buf == NULL || read_len == NULL) { + return HYBBX_ERR_INVALID; + } + + n = read(port->fd, buf, buf_len); + if (n < 0) { +#if EAGAIN == EWOULDBLOCK + if (errno == EAGAIN) { +#else + if (errno == EAGAIN || errno == EWOULDBLOCK) { +#endif + *read_len = 0; + return HYBBX_OK; + } + return HYBBX_ERR_IO; + } + + *read_len = (size_t)n; + return HYBBX_OK; +} + +int hybbx_serial_fd(const hybbx_serial_port_t *port) +{ + if (port == NULL) { + return -1; + } + + return port->fd; +} + +#endif diff --git a/plugins/packet_radio/serial_port.h b/plugins/packet_radio/serial_port.h new file mode 100644 index 0000000..549d3c5 --- /dev/null +++ b/plugins/packet_radio/serial_port.h @@ -0,0 +1,52 @@ +#ifndef HYBBX_SERIAL_PORT_H +#define HYBBX_SERIAL_PORT_H + +#include "hybbx/types.h" + +#include <stddef.h> +#include <stdint.h> + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct hybbx_serial_port hybbx_serial_port_t; + +typedef enum hybbx_serial_parity { + HYBBX_SERIAL_PARITY_NONE = 0, + HYBBX_SERIAL_PARITY_EVEN = 1, + HYBBX_SERIAL_PARITY_ODD = 2 +} hybbx_serial_parity_t; + +typedef struct hybbx_serial_params { + unsigned int baud; + unsigned int data_bits; + hybbx_serial_parity_t parity; + unsigned int stop_bits; + int assert_modem_lines; +} hybbx_serial_params_t; + +void hybbx_serial_params_default(hybbx_serial_params_t *params, + unsigned int baud); + +hybbx_result_t hybbx_serial_open(hybbx_serial_port_t **out, + const char *device, + const hybbx_serial_params_t *params); + +void hybbx_serial_close(hybbx_serial_port_t *port); + +hybbx_result_t hybbx_serial_write(hybbx_serial_port_t *port, + const uint8_t *data, size_t len); + +hybbx_result_t hybbx_serial_read(hybbx_serial_port_t *port, + uint8_t *buf, size_t buf_len, + size_t *read_len); + +/** POSIX serial fd for poll/select; returns -1 when unavailable. */ +int hybbx_serial_fd(const hybbx_serial_port_t *port); + +#ifdef __cplusplus +} +#endif + +#endif /* HYBBX_SERIAL_PORT_H */ diff --git a/plugins/packet_radio/sixpack.c b/plugins/packet_radio/sixpack.c new file mode 100644 index 0000000..28c0510 --- /dev/null +++ b/plugins/packet_radio/sixpack.c @@ -0,0 +1,146 @@ +#include "sixpack.h" + +#include <string.h> + +void hybbx_sixpack_decoder_init(hybbx_sixpack_decoder_t *dec) +{ + if (dec == NULL) { + return; + } + + memset(dec, 0, sizeof(*dec)); +} + +static void sixpack_reset(hybbx_sixpack_decoder_t *dec) +{ + dec->len = 0; + dec->count = 0; + dec->checksum = 0; + dec->in_frame = 0; +} + +static void sixpack_deliver(hybbx_sixpack_decoder_t *dec, + hybbx_sixpack_frame_cb cb, void *userdata) +{ + if (cb != NULL && dec->len > 0) { + cb(dec->buf, dec->len, userdata); + } + + sixpack_reset(dec); +} + +void hybbx_sixpack_decoder_feed(hybbx_sixpack_decoder_t *dec, + const uint8_t *data, size_t len, + hybbx_sixpack_frame_cb cb, void *userdata) +{ + size_t i; + + if (dec == NULL || data == NULL) { + return; + } + + for (i = 0; i < len; i++) { + uint8_t raw = data[i]; + uint8_t byte = raw; + + if (!dec->in_frame) { + if (raw == HYBBX_SIXPACK_LEAD) { + dec->in_frame = 1; + dec->len = 0; + dec->count = 0; + dec->checksum = 0; + } + continue; + } + + if (dec->count == 0) { + dec->count = raw; + dec->checksum = raw; + continue; + } + + dec->count--; + dec->checksum = (uint8_t)(dec->checksum + raw); + + if (raw < 0x30u) { + byte = (uint8_t)(raw + 0x40u); + } + + if (dec->count > 0) { + if (dec->len >= HYBBX_KISS_MAX_FRAME) { + sixpack_reset(dec); + continue; + } + dec->buf[dec->len++] = byte; + continue; + } + + if (dec->checksum == 0) { + sixpack_deliver(dec, cb, userdata); + } else { + sixpack_reset(dec); + } + } +} + +static size_t sixpack_write_encoded(uint8_t byte, uint8_t *out, size_t out_cap, + size_t pos) +{ + if (byte < 0x30u) { + if (pos + 1 > out_cap) { + return 0; + } + out[pos++] = (uint8_t)(byte + 0x40u); + return pos; + } + + if (pos + 1 > out_cap) { + return 0; + } + + out[pos++] = byte; + return pos; +} + +size_t hybbx_sixpack_encode(const uint8_t *frame, size_t frame_len, + uint8_t *out, size_t out_cap) +{ + uint8_t scratch[HYBBX_KISS_MAX_FRAME + 4]; + size_t enc_len = 0; + size_t i; + uint8_t count; + uint8_t sum; + size_t pos = 0; + + if (frame == NULL || out == NULL || frame_len == 0 || + frame_len > HYBBX_KISS_MAX_FRAME) { + return 0; + } + + for (i = 0; i < frame_len; i++) { + enc_len = sixpack_write_encoded(frame[i], scratch, sizeof(scratch), + enc_len); + if (enc_len == 0) { + return 0; + } + } + + if (enc_len + 4 > out_cap || enc_len + 1 > 255) { + return 0; + } + + count = (uint8_t)(enc_len + 1); + sum = count; + for (i = 0; i < enc_len; i++) { + sum = (uint8_t)(sum + scratch[i]); + } + + out[pos++] = HYBBX_SIXPACK_LEAD; + out[pos++] = count; + for (i = 0; i < enc_len; i++) { + out[pos++] = scratch[i]; + } + out[pos++] = (uint8_t)(0x100u - (unsigned)sum); + + return pos; +} diff --git a/plugins/packet_radio/sixpack.h b/plugins/packet_radio/sixpack.h new file mode 100644 index 0000000..0d60e87 --- /dev/null +++ b/plugins/packet_radio/sixpack.h @@ -0,0 +1,40 @@ +#ifndef HYBBX_SIXPACK_H +#define HYBBX_SIXPACK_H + +#include "hybbx/kiss.h" +#include "hybbx/types.h" + +#include <stddef.h> +#include <stdint.h> + +#ifdef __cplusplus +extern "C" { +#endif + +#define HYBBX_SIXPACK_LEAD 0x53u /* 'S' */ + +typedef void (*hybbx_sixpack_frame_cb)(const uint8_t *frame, size_t len, + void *userdata); + +typedef struct hybbx_sixpack_decoder { + uint8_t buf[HYBBX_KISS_MAX_FRAME]; + size_t len; + int in_frame; + uint8_t count; + uint8_t checksum; +} hybbx_sixpack_decoder_t; + +void hybbx_sixpack_decoder_init(hybbx_sixpack_decoder_t *dec); + +void hybbx_sixpack_decoder_feed(hybbx_sixpack_decoder_t *dec, + const uint8_t *data, size_t len, + hybbx_sixpack_frame_cb cb, void *userdata); + +size_t hybbx_sixpack_encode(const uint8_t *frame, size_t frame_len, + uint8_t *out, size_t out_cap); + +#ifdef __cplusplus +} +#endif + +#endif /* HYBBX_SIXPACK_H */ diff --git a/plugins/packet_radio/tnc.c b/plugins/packet_radio/tnc.c new file mode 100644 index 0000000..a42480d --- /dev/null +++ b/plugins/packet_radio/tnc.c @@ -0,0 +1,1080 @@ +#if defined(__linux__) || defined(__GLIBC__) +#define _DEFAULT_SOURCE 1 +#endif + +#include "hybbx/tnc.h" +#include "hybbx/kiss.h" +#include "hybbx/rf_tx_pace.h" +#include "hybbx/traffic.h" +#include "serial_port.h" +#include "sixpack.h" +#include "tnc_host.h" + +#include "hybbx/log.h" + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> + +#if !defined(_WIN32) +#include <sys/select.h> +#endif + +struct hybbx_tnc { + hybbx_packet_radio_config_t config; + hybbx_serial_port_t *serial; + hybbx_kiss_decoder_t kiss_dec; + hybbx_sixpack_decoder_t sixpack_dec; + hybbx_tnc_host_t host; + hybbx_ax25_path_t tx_path; + int kiss_active; + hybbx_kiss_entry_t kiss_entry_used; + int host_ready; + hybbx_tnc_frame_cb frame_cb; + hybbx_tnc_ui_cb ui_cb; + void *rx_userdata; + unsigned long long last_rx_ms; +}; + +static int tnc_profile_thefirmware(hybbx_packet_radio_tnc_t tnc); + +static void config_copy(hybbx_packet_radio_config_t *dst, + const hybbx_packet_radio_config_t *src) +{ + unsigned i; + + memset(dst, 0, sizeof(*dst)); + dst->device_type = src->device_type; + dst->device = src->device; + dst->baud = src->baud; + dst->tnc = src->tnc; + dst->protocol = src->protocol; + dst->params = src->params; + dst->mycall = src->mycall; + dst->dest_call = src->dest_call; + dst->via_count = src->via_count; + for (i = 0; i < src->via_count; i++) { + dst->via[i] = src->via[i]; + } +} + +static unsigned long long tnc_monotonic_ms(void) +{ +#if defined(CLOCK_MONOTONIC) + struct timespec ts; + + if (clock_gettime(CLOCK_MONOTONIC, &ts) == 0) { + return (unsigned long long)ts.tv_sec * 1000ull + + (unsigned long long)ts.tv_nsec / 1000000ull; + } +#endif + return (unsigned long long)time(NULL) * 1000ull; +} + +static void tnc_note_rx(hybbx_tnc_t *tnc) +{ + if (tnc != NULL) { + tnc->last_rx_ms = tnc_monotonic_ms(); + } +} + +static hybbx_result_t tnc_wait_half_duplex_clear(hybbx_tnc_t *tnc) +{ + unsigned guard_ms; + unsigned long long now; + unsigned long long elapsed; + + if (tnc == NULL || hybbx_tnc_duplex_is_full(tnc)) { + return HYBBX_OK; + } + + guard_ms = tnc->config.params.slot * 10u; + if (guard_ms < 50u) { + guard_ms = 50u; + } + + now = tnc_monotonic_ms(); + if (tnc->last_rx_ms == 0 || now <= tnc->last_rx_ms) { + return HYBBX_OK; + } + + elapsed = now - tnc->last_rx_ms; + if (elapsed >= guard_ms) { + return HYBBX_OK; + } + +#if defined(_WIN32) + Sleep((DWORD)(guard_ms - elapsed)); +#else + { + struct timeval tv; + + tv.tv_sec = (time_t)((guard_ms - elapsed) / 1000u); + tv.tv_usec = (suseconds_t)(((guard_ms - elapsed) % 1000u) * 1000u); + (void)select(0, NULL, NULL, NULL, &tv); + } +#endif + + return HYBBX_OK; +} + +static hybbx_result_t send_raw(hybbx_tnc_t *tnc, const uint8_t *data, size_t len) +{ + return hybbx_serial_write(tnc->serial, data, len); +} + +static hybbx_result_t send_cmd(hybbx_tnc_t *tnc, const char *cmd) +{ + char line[128]; + size_t len; + + len = hybbx_tnc_host_format_cmd(cmd, line, sizeof(line)); + if (len == 0) { + return HYBBX_ERR_INVALID; + } + + return send_raw(tnc, (const uint8_t *)line, len); +} + +static hybbx_result_t drain_serial(hybbx_tnc_t *tnc, unsigned rounds) +{ + uint8_t buf[128]; + size_t read_len; + unsigned i; + + for (i = 0; i < rounds; i++) { + if (hybbx_serial_read(tnc->serial, buf, sizeof(buf), + &read_len) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + if (read_len == 0) { + break; + } + } + + return HYBBX_OK; +} + +static hybbx_result_t send_kiss_param(hybbx_tnc_t *tnc, + hybbx_kiss_command_t cmd, + uint8_t value) +{ + uint8_t frame[16]; + size_t len; + + len = hybbx_kiss_encode((uint8_t)tnc->config.params.kiss_port, cmd, + &value, 1, frame, sizeof(frame)); + if (len == 0) { + return HYBBX_ERR_IO; + } + + return send_raw(tnc, frame, len); +} + +static hybbx_result_t tnc_apply_kiss_params(hybbx_tnc_t *tnc) +{ + hybbx_result_t rc; + + rc = send_kiss_param(tnc, HYBBX_KISS_CMD_TXDELAY, + (uint8_t)tnc->config.params.txdelay); + if (rc != HYBBX_OK) { + return rc; + } + + rc = send_kiss_param(tnc, HYBBX_KISS_CMD_PERSIST, + (uint8_t)tnc->config.params.persist); + if (rc != HYBBX_OK) { + return rc; + } + + rc = send_kiss_param(tnc, HYBBX_KISS_CMD_SLOTTIME, + (uint8_t)tnc->config.params.slot); + if (rc != HYBBX_OK) { + return rc; + } + + rc = send_kiss_param(tnc, HYBBX_KISS_CMD_TXTAIL, + (uint8_t)tnc->config.params.txtail); + if (rc != HYBBX_OK) { + return rc; + } + + return send_kiss_param(tnc, HYBBX_KISS_CMD_FULLDUPLEX, + (uint8_t)(tnc->config.params.fullduplex ? 1 : 0)); +} + +static hybbx_result_t tnc_send_esc_at_k(hybbx_tnc_t *tnc) +{ + const uint8_t seq[] = { 0x1b, '@', 'K' }; + + return send_raw(tnc, seq, sizeof(seq)); +} + +static hybbx_result_t tnc_send_kiss_return_frame(hybbx_tnc_t *tnc) +{ + const uint8_t frame[] = { HYBBX_KISS_FEND, 0xFFu, HYBBX_KISS_FEND }; + + return send_raw(tnc, frame, sizeof(frame)); +} + +static void tnc_serial_pause_ms(unsigned ms) +{ +#if defined(_WIN32) + Sleep(ms); +#else + struct timespec ts; + + ts.tv_sec = (time_t)(ms / 1000u); + ts.tv_nsec = (long)((ms % 1000u) * 1000000u); + (void)nanosleep(&ts, NULL); +#endif +} + +static hybbx_result_t tnc_send_jhost0(hybbx_tnc_t *tnc) +{ + static const uint8_t nuls[300] = { 0 }; + static const uint8_t jhost0[] = { + 0x00, 0x01, 0x06, 'J', 'H', 'O', 'S', 'T', ' ', '0', '\r' + }; + hybbx_result_t rc; + + rc = send_raw(tnc, nuls, sizeof(nuls)); + if (rc != HYBBX_OK) { + return rc; + } + tnc_serial_pause_ms(150); + return send_raw(tnc, jhost0, sizeof(jhost0)); +} + +static hybbx_result_t tnc_send_restart_escape(hybbx_tnc_t *tnc) +{ + unsigned i; + + for (i = 0; i < 3u; i++) { + if (send_raw(tnc, (const uint8_t *)"\x03", 1) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + tnc_serial_pause_ms(80); + } + tnc_serial_pause_ms(300); + return send_cmd(tnc, "RESTART"); +} + +/** Leave KISS/host/transparency → terminal (TheFirmware ladder). */ +static hybbx_result_t tnc_recover_terminal_mode(hybbx_tnc_t *tnc) +{ + if (tnc == NULL) { + return HYBBX_ERR_INVALID; + } + + tnc->kiss_active = 0; + (void)tnc_send_kiss_return_frame(tnc); + tnc_serial_pause_ms(400); + drain_serial(tnc, 8); + + (void)tnc_send_jhost0(tnc); + tnc_serial_pause_ms(800); + drain_serial(tnc, 8); + + (void)send_cmd(tnc, "kiss off"); + tnc_serial_pause_ms(400); + drain_serial(tnc, 6); + (void)send_cmd(tnc, "INFO"); + drain_serial(tnc, 12); + + (void)tnc_send_restart_escape(tnc); + tnc_serial_pause_ms(1200); + drain_serial(tnc, 8); + (void)send_cmd(tnc, "kiss off"); + tnc_serial_pause_ms(300); + drain_serial(tnc, 4); + + return HYBBX_OK; +} + +/** Leave KISS (crash/kill) so host commands (MYCALL, kiss on) work again. */ +static hybbx_result_t tnc_kiss_wakeup_host(hybbx_tnc_t *tnc) +{ + if (tnc == NULL) { + return HYBBX_ERR_INVALID; + } + + return tnc_recover_terminal_mode(tnc); +} + +static hybbx_result_t tnc_enter_kiss_by_entry(hybbx_tnc_t *tnc, + hybbx_kiss_entry_t entry) +{ + if (tnc == NULL) { + return HYBBX_ERR_INVALID; + } + + switch (entry) { + case HYBBX_KISS_ENTRY_NONE: + tnc->kiss_active = 1; + tnc->kiss_entry_used = HYBBX_KISS_ENTRY_NONE; + return HYBBX_OK; + + case HYBBX_KISS_ENTRY_ESC_AT_K: + if (tnc_send_esc_at_k(tnc) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + drain_serial(tnc, 8); + tnc->kiss_active = 1; + tnc->kiss_entry_used = HYBBX_KISS_ENTRY_ESC_AT_K; + return HYBBX_OK; + + case HYBBX_KISS_ENTRY_KISS_ON: + if (send_cmd(tnc, "kiss on") != HYBBX_OK) { + return HYBBX_ERR_IO; + } + drain_serial(tnc, 8); + tnc->kiss_active = 1; + tnc->kiss_entry_used = HYBBX_KISS_ENTRY_KISS_ON; + return HYBBX_OK; + + case HYBBX_KISS_ENTRY_AUTO: + if (send_cmd(tnc, "kiss on") == HYBBX_OK) { + drain_serial(tnc, 8); + tnc->kiss_active = 1; + tnc->kiss_entry_used = HYBBX_KISS_ENTRY_KISS_ON; + return HYBBX_OK; + } + if (tnc_send_esc_at_k(tnc) == HYBBX_OK) { + drain_serial(tnc, 8); + tnc->kiss_active = 1; + tnc->kiss_entry_used = HYBBX_KISS_ENTRY_ESC_AT_K; + return HYBBX_OK; + } + return HYBBX_ERR_IO; + + case HYBBX_KISS_ENTRY_UNSET: + default: + return HYBBX_ERR_INVALID; + } +} + +static void tnc_exit_kiss_mode(hybbx_tnc_t *tnc) +{ + hybbx_kiss_exit_t exit_mode; + + if (tnc == NULL || !tnc->kiss_active || + tnc->config.protocol != HYBBX_PACKET_RADIO_PROTO_KISS) { + return; + } + + exit_mode = tnc->config.params.kiss_exit; + if (exit_mode == HYBBX_KISS_EXIT_UNSET) { + exit_mode = HYBBX_KISS_EXIT_AUTO; + } + if (exit_mode == HYBBX_KISS_EXIT_AUTO) { + if (tnc->kiss_entry_used == HYBBX_KISS_ENTRY_ESC_AT_K) { + exit_mode = HYBBX_KISS_EXIT_KISS_FRAME; + } else { + exit_mode = HYBBX_KISS_EXIT_KISS_OFF; + } + } + + switch (exit_mode) { + case HYBBX_KISS_EXIT_KISS_FRAME: + (void)tnc_send_kiss_return_frame(tnc); + break; + case HYBBX_KISS_EXIT_KISS_OFF: + (void)send_cmd(tnc, "kiss off"); + break; + case HYBBX_KISS_EXIT_NONE: + case HYBBX_KISS_EXIT_UNSET: + default: + break; + } +} + +static hybbx_result_t tnc_enter_kiss_mode(hybbx_tnc_t *tnc) +{ + return tnc_enter_kiss_by_entry(tnc, tnc->config.params.kiss_entry); +} + +static void tnc_deliver_ui(hybbx_tnc_t *tnc, const uint8_t *frame, size_t len) +{ + uint8_t payload[HYBBX_AX25_PAYLOAD_MAX]; + hybbx_ax25_path_t path; + size_t payload_len; + + payload_len = hybbx_ax25_parse_ui(frame, len, &path, payload, + sizeof(payload)); + if (payload_len == 0) { + return; + } + + if (tnc->ui_cb != NULL) { + tnc->ui_cb(payload, payload_len, &path, tnc->rx_userdata); + } + + tnc_note_rx(tnc); +} + +static void on_kiss_frame(uint8_t port, const uint8_t *frame, size_t len, + void *userdata) +{ + hybbx_tnc_t *tnc = (hybbx_tnc_t *)userdata; + uint8_t payload[HYBBX_AX25_PAYLOAD_MAX]; + hybbx_ax25_path_t path; + size_t payload_len; + + (void)port; + + if (tnc->frame_cb != NULL) { + tnc->frame_cb(frame, len, tnc->rx_userdata); + } + + if (tnc->ui_cb == NULL) { + return; + } + + payload_len = hybbx_ax25_parse_ui(frame, len, &path, payload, + sizeof(payload)); + if (payload_len > 0) { + tnc->ui_cb(payload, payload_len, &path, tnc->rx_userdata); + } +} + +static void on_sixpack_frame(const uint8_t *frame, size_t len, void *userdata) +{ + hybbx_tnc_t *tnc = (hybbx_tnc_t *)userdata; + + if (tnc->frame_cb != NULL) { + tnc->frame_cb(frame, len, tnc->rx_userdata); + } + + if (tnc->ui_cb != NULL) { + tnc_deliver_ui(tnc, frame, len); + } +} + +static void on_host_text(const uint8_t *data, size_t len, void *userdata) +{ + hybbx_tnc_t *tnc = (hybbx_tnc_t *)userdata; + + if (tnc->ui_cb != NULL) { + tnc->ui_cb(data, len, NULL, tnc->rx_userdata); + } + + tnc_note_rx(tnc); +} + +static void on_host_event(hybbx_tnc_host_state_t state, void *userdata) +{ + hybbx_tnc_t *tnc = (hybbx_tnc_t *)userdata; + + if (state == HYBBX_TNC_HOST_CMD) { + tnc->host_ready = 1; + } + + hybbx_log_debug("[tnc] host state -> %d", (int)state); +} + +static hybbx_result_t tnc_build_tx_path(hybbx_tnc_t *tnc) +{ + unsigned i; + hybbx_result_t rc; + + memset(&tnc->tx_path, 0, sizeof(tnc->tx_path)); + + if (tnc->config.dest_call != NULL && tnc->config.dest_call[0] != '\0') { + rc = hybbx_ax25_address_parse(tnc->config.dest_call, + &tnc->tx_path.dest); + if (rc != HYBBX_OK) { + return rc; + } + } else { + (void)hybbx_ax25_address_parse("QST", &tnc->tx_path.dest); + } + + if (tnc->config.mycall != NULL && tnc->config.mycall[0] != '\0') { + rc = hybbx_ax25_address_parse(tnc->config.mycall, + &tnc->tx_path.source); + if (rc != HYBBX_OK) { + return rc; + } + } else { + (void)hybbx_ax25_address_parse("HYBBX", &tnc->tx_path.source); + } + + for (i = 0; i < tnc->config.via_count; i++) { + rc = hybbx_ax25_address_parse(tnc->config.via[i], + &tnc->tx_path.digi[i]); + if (rc != HYBBX_OK) { + return rc; + } + } + tnc->tx_path.digi_count = tnc->config.via_count; + + return HYBBX_OK; +} + +static const char *serial_parity_letter(hybbx_tnc_serial_parity_t parity) +{ + switch (parity) { + case HYBBX_TNC_SERIAL_PARITY_EVEN: + return "E"; + case HYBBX_TNC_SERIAL_PARITY_ODD: + return "O"; + case HYBBX_TNC_SERIAL_PARITY_NONE: + default: + return "N"; + } +} + +static void tnc_serial_params_from_config(const hybbx_packet_radio_config_t *config, + hybbx_serial_params_t *out) +{ + hybbx_serial_params_default(out, config->baud); + out->data_bits = config->params.data_bits; + out->stop_bits = config->params.stop_bits; + out->assert_modem_lines = config->params.assert_modem_lines; + + switch (config->params.serial_parity) { + case HYBBX_TNC_SERIAL_PARITY_EVEN: + out->parity = HYBBX_SERIAL_PARITY_EVEN; + break; + case HYBBX_TNC_SERIAL_PARITY_ODD: + out->parity = HYBBX_SERIAL_PARITY_ODD; + break; + case HYBBX_TNC_SERIAL_PARITY_NONE: + default: + out->parity = HYBBX_SERIAL_PARITY_NONE; + break; + } +} + +static const char *tnc_profile_name(hybbx_packet_radio_tnc_t tnc) +{ + return hybbx_packet_radio_tnc_name(tnc); +} + +static const char *modem_name(hybbx_tnc2c_modem_t modem) +{ + switch (modem) { + case HYBBX_TNC2C_MODEM_9600: + return "9600"; + case HYBBX_TNC2C_MODEM_19200: + return "19200"; + case HYBBX_TNC2C_MODEM_TCM3105: + default: + return "tcm3105"; + } +} + + +static int tnc_profile_thefirmware(hybbx_packet_radio_tnc_t tnc); + +static hybbx_result_t tnc_profile_init(hybbx_tnc_t *tnc) +{ + hybbx_result_t rc; + char cmd[96]; + + rc = tnc_build_tx_path(tnc); + if (rc != HYBBX_OK) { + return rc; + } + + if (tnc->config.protocol == HYBBX_PACKET_RADIO_PROTO_KISS) { + if (tnc->config.params.kiss_entry == HYBBX_KISS_ENTRY_NONE) { + tnc->kiss_active = 1; + tnc->kiss_entry_used = HYBBX_KISS_ENTRY_NONE; + hybbx_log_info("[tnc] KISS attach (MAX25 prep assumed)"); + return HYBBX_OK; + } + + if (tnc_profile_thefirmware(tnc->config.tnc)) { + (void)tnc_recover_terminal_mode(tnc); + hybbx_log_info("[tnc] TheFirmware terminal recovery before KISS entry"); + } + + rc = tnc_enter_kiss_mode(tnc); + if (rc != HYBBX_OK) { + return rc; + } + return tnc_apply_kiss_params(tnc); + } + + if (tnc->config.protocol == HYBBX_PACKET_RADIO_PROTO_SIXPACK) { + tnc->kiss_active = 0; + return HYBBX_OK; + } + + /* Host mode setup (firmware TNC profiles only; not BayCom/based SER12 / PC-COM modem). */ + drain_serial(tnc, 4); + (void)send_cmd(tnc, ""); + + if (tnc->config.mycall != NULL && tnc->config.mycall[0] != '\0') { + snprintf(cmd, sizeof(cmd), "MYCALL %s", tnc->config.mycall); + rc = send_cmd(tnc, cmd); + if (rc != HYBBX_OK) { + return rc; + } + } + + snprintf(cmd, sizeof(cmd), "TXDELAY %u", tnc->config.params.txdelay); + (void)send_cmd(tnc, cmd); + snprintf(cmd, sizeof(cmd), "Persist %u", tnc->config.params.persist); + (void)send_cmd(tnc, cmd); + snprintf(cmd, sizeof(cmd), "SlotTime %u", tnc->config.params.slot); + (void)send_cmd(tnc, cmd); + snprintf(cmd, sizeof(cmd), "TXTAIL %u", tnc->config.params.txtail); + (void)send_cmd(tnc, cmd); + + if (tnc->config.params.fullduplex) { + (void)send_cmd(tnc, "FULLDUP ON"); + } else { + (void)send_cmd(tnc, "FULLDUP OFF"); + } + + if (tnc->config.tnc == HYBBX_PACKET_RADIO_TNC_PCCOM) { + (void)send_cmd(tnc, "PACLEN 256"); + (void)send_cmd(tnc, "MAXFRAME 4"); + } + + if (tnc->config.tnc == HYBBX_PACKET_RADIO_TNC_BAYCOM) { + (void)send_cmd(tnc, "PACLEN 256"); + } + + if (tnc->config.params.host_connect_on_start && + tnc->config.dest_call != NULL && tnc->config.dest_call[0] != '\0') { + snprintf(cmd, sizeof(cmd), "C %s", tnc->config.dest_call); + rc = send_cmd(tnc, cmd); + if (rc != HYBBX_OK) { + return rc; + } + } + + drain_serial(tnc, 6); + return HYBBX_OK; +} + +hybbx_result_t hybbx_tnc_open(hybbx_tnc_t **out, + const hybbx_packet_radio_config_t *config, + hybbx_tnc_frame_cb frame_cb, + hybbx_tnc_ui_cb ui_cb, + void *rx_userdata) +{ + hybbx_tnc_t *tnc; + hybbx_result_t rc; + + if (out == NULL || config == NULL || config->device == NULL) { + return HYBBX_ERR_INVALID; + } + + if (frame_cb == NULL && ui_cb == NULL) { + return HYBBX_ERR_INVALID; + } + + tnc = calloc(1, sizeof(*tnc)); + if (tnc == NULL) { + return HYBBX_ERR_NOMEM; + } + + config_copy(&tnc->config, config); + tnc->frame_cb = frame_cb; + tnc->ui_cb = ui_cb; + tnc->rx_userdata = rx_userdata; + + hybbx_kiss_decoder_init(&tnc->kiss_dec); + hybbx_sixpack_decoder_init(&tnc->sixpack_dec); + hybbx_tnc_host_init(&tnc->host, on_host_text, on_host_event, tnc); + + hybbx_serial_params_t serial_params; + + tnc_serial_params_from_config(config, &serial_params); + + rc = hybbx_serial_open(&tnc->serial, config->device, &serial_params); + if (rc != HYBBX_OK) { + hybbx_log_warn("[tnc] cannot open %s at %u baud (%u%c%u) — " + "device not available or permission denied", + config->device, config->baud, serial_params.data_bits, + *serial_parity_letter(config->params.serial_parity), + serial_params.stop_bits); + free(tnc); + return rc; + } + + hybbx_log_info("[tnc] profile=%s protocol=%u device=%s host=%u %u%c%u rts_dtr=%s " + "kiss_entry=%s radio=%u modem=%s modulation=%s band=%s duplex=%s " + "txdelay=%u persist=%u", + tnc_profile_name(config->tnc), (unsigned)config->protocol, + config->device, config->baud, serial_params.data_bits, + *serial_parity_letter(config->params.serial_parity), + serial_params.stop_bits, + serial_params.assert_modem_lines ? "on" : "off", + hybbx_kiss_entry_name(config->params.kiss_entry), + config->params.radio_baud, + modem_name(config->params.modem), + hybbx_packet_radio_modulation_name(config->params.modulation), + hybbx_packet_radio_band_name(config->params.band), + hybbx_packet_radio_duplex_name(config->params.duplex), + config->params.txdelay, + config->params.persist); + + if (config->tnc == HYBBX_PACKET_RADIO_TNC_TNC2C && + serial_params.data_bits == 8 && + config->params.serial_parity == HYBBX_TNC_SERIAL_PARITY_NONE) { + hybbx_log_warn("[tnc] tnc2c %s: 8N1 host line — use serial_line=7E1 if KISS/UNPROTO TX fails", + config->device); + } + + rc = tnc_profile_init(tnc); + if (rc != HYBBX_OK) { + hybbx_tnc_close(tnc); + return rc; + } + + if (config->protocol == HYBBX_PACKET_RADIO_PROTO_KISS) { + hybbx_log_info("[tnc] KISS active (port %u)", config->params.kiss_port); + if (tnc_profile_thefirmware(config->tnc)) { + hybbx_log_info("[tnc] UI transmit=TheFirmware UNPROTO (KISS for RX)"); + } + } else if (config->protocol == HYBBX_PACKET_RADIO_PROTO_SIXPACK) { + hybbx_log_info("[tnc] 6PACK active"); + } else { + hybbx_log_info("[tnc] TNC2 host mode active"); + } + + *out = tnc; + return HYBBX_OK; +} + +void hybbx_tnc_close(hybbx_tnc_t *tnc) +{ + if (tnc == NULL) { + return; + } + + if (tnc->serial != NULL) { + tnc_exit_kiss_mode(tnc); + hybbx_serial_close(tnc->serial); + } + + free(tnc); +} + +int hybbx_tnc_serial_fd(const hybbx_tnc_t *tnc) +{ + if (tnc == NULL || tnc->serial == NULL) { + return -1; + } + + return hybbx_serial_fd(tnc->serial); +} + +static hybbx_result_t tnc_ensure_kiss_tx(hybbx_tnc_t *tnc) +{ + hybbx_result_t rc; + + if (tnc == NULL || + tnc->config.protocol != HYBBX_PACKET_RADIO_PROTO_KISS) { + return HYBBX_OK; + } + + if (!tnc->kiss_active) { + rc = tnc_enter_kiss_mode(tnc); + if (rc != HYBBX_OK) { + hybbx_log_warn("[tnc] KISS TX blocked — host mode (re-entry failed)"); + return rc; + } + hybbx_log_info("[tnc] KISS re-entered before RF TX"); + return tnc_apply_kiss_params(tnc); + } + + return HYBBX_OK; +} + +/** KISS DATA: host sends AX.25 body only; TheFirmware adds FCS on air. */ +static size_t tnc_kiss_ax25_len(const uint8_t *frame, size_t len) +{ + uint16_t crc_rx; + uint16_t crc_calc; + + if (len < 16) { + return len; + } + + crc_rx = (uint16_t)frame[len - 2] | ((uint16_t)frame[len - 1] << 8); + crc_calc = hybbx_ax25_crc(frame, len - 2); + if (crc_rx == crc_calc) { + return len - 2; + } + + return len; +} + +static int tnc_profile_thefirmware(hybbx_packet_radio_tnc_t tnc) +{ + switch (tnc) { + case HYBBX_PACKET_RADIO_TNC_TNC2: + case HYBBX_PACKET_RADIO_TNC_TNC2C: + case HYBBX_PACKET_RADIO_TNC_GENERIC: + return 1; + default: + return 0; + } +} + +static void tnc_format_ax25_call(const hybbx_ax25_address_t *addr, + char *out, size_t out_len) +{ + if (addr == NULL || out == NULL || out_len == 0) { + return; + } + + snprintf(out, out_len, "%s", addr->call); + if (addr->ssid > 0) { + size_t n = strlen(out); + + if (n + 4 < out_len) { + snprintf(out + n, out_len - n, "-%u", addr->ssid); + } + } +} + +/** + * TheFirmware (TNC-2 class) often ignores KISS DATA on hybrid host/KISS builds + * while UNPROTO from cmd: mode keys PTT reliably. Exit KISS, send, re-enter. + */ +static hybbx_result_t tnc_thefirmware_unproto_send(hybbx_tnc_t *tnc, + const hybbx_ax25_path_t *path, + const uint8_t *payload, + size_t payload_len) +{ + char cmd[320]; + char dest[HYBBX_AX25_CALL_MAX + 8]; + hybbx_result_t rc; + + if (tnc == NULL || path == NULL || payload == NULL || payload_len == 0) { + return HYBBX_ERR_INVALID; + } + + if (payload_len + 64 >= sizeof(cmd)) { + return HYBBX_ERR_INVALID; + } + + tnc_format_ax25_call(&path->dest, dest, sizeof(dest)); + + rc = tnc_kiss_wakeup_host(tnc); + if (rc != HYBBX_OK) { + return rc; + } + + (void)send_cmd(tnc, "MON OFF"); + + snprintf(cmd, sizeof(cmd), "UNPROTO %s 0 %.*s", + dest, (int)payload_len, (const char *)payload); + rc = send_cmd(tnc, cmd); + if (rc != HYBBX_OK) { + hybbx_log_warn("[tnc] UNPROTO %s host send failed", dest); + return rc; + } + + drain_serial(tnc, 24); + tnc_serial_pause_ms(500); + + hybbx_log_info("[tnc] TheFirmware UNPROTO %s (%zu bytes)", dest, payload_len); + + rc = tnc_enter_kiss_mode(tnc); + if (rc != HYBBX_OK) { + hybbx_log_warn("[tnc] KISS re-entry after UNPROTO failed"); + return rc; + } + + return tnc_apply_kiss_params(tnc); +} + +hybbx_result_t hybbx_tnc_send_frame(hybbx_tnc_t *tnc, + const uint8_t *frame, size_t len) +{ + uint8_t buf[HYBBX_KISS_MAX_FRAME + 8]; + size_t encoded; + hybbx_result_t rc; + + if (tnc == NULL || frame == NULL || len == 0) { + return HYBBX_ERR_INVALID; + } + + hybbx_rf_tx_pace(); + + rc = tnc_wait_half_duplex_clear(tnc); + if (rc != HYBBX_OK) { + return rc; + } + + if (tnc->config.protocol == HYBBX_PACKET_RADIO_PROTO_SIXPACK) { + encoded = hybbx_sixpack_encode(frame, len, buf, sizeof(buf)); + if (encoded == 0) { + return HYBBX_ERR_IO; + } + return send_raw(tnc, buf, encoded); + } + + if (!tnc->kiss_active && + tnc->config.protocol != HYBBX_PACKET_RADIO_PROTO_KISS) { + return HYBBX_ERR_UNSUPPORTED; + } + + if (tnc->config.protocol == HYBBX_PACKET_RADIO_PROTO_KISS && + tnc_profile_thefirmware(tnc->config.tnc)) { + hybbx_ax25_path_t path; + uint8_t ui[HYBBX_AX25_PAYLOAD_MAX]; + size_t ui_len = hybbx_ax25_parse_ui(frame, len, &path, ui, sizeof(ui)); + + if (ui_len > 0) { + return tnc_thefirmware_unproto_send(tnc, &path, ui, ui_len); + } + } + + if (tnc->config.protocol == HYBBX_PACKET_RADIO_PROTO_KISS) { + rc = tnc_ensure_kiss_tx(tnc); + if (rc != HYBBX_OK) { + return rc; + } + } + + { + const uint8_t *tx_frame = frame; + size_t tx_len = len; + + if (tnc->config.protocol == HYBBX_PACKET_RADIO_PROTO_KISS) { + tx_len = tnc_kiss_ax25_len(frame, len); + tx_frame = frame; + (void)send_kiss_param(tnc, HYBBX_KISS_CMD_PERSIST, + (uint8_t)tnc->config.params.persist); + if (tnc_profile_thefirmware(tnc->config.tnc)) { + hybbx_log_warn("[tnc] UI parse failed (%zu bytes) — KISS DATA fallback", + len); + } + } + + encoded = hybbx_kiss_encode((uint8_t)tnc->config.params.kiss_port, + HYBBX_KISS_CMD_DATA, tx_frame, tx_len, + buf, sizeof(buf)); + if (encoded == 0) { + return HYBBX_ERR_IO; + } + + hybbx_log_debug("[tnc] KISS DATA ax25=%zu kiss=%zu%s", + tx_len, encoded, + tx_len + 2 == len ? " (fcs stripped)" : ""); + + return send_raw(tnc, buf, encoded); + } +} + +hybbx_result_t hybbx_tnc_send_ui(hybbx_tnc_t *tnc, + const uint8_t *payload, size_t len) +{ + uint8_t frame[HYBBX_AX25_FRAME_MAX]; + size_t frame_len; + + if (tnc == NULL) { + return HYBBX_ERR_INVALID; + } + + frame_len = hybbx_ax25_build_ui(&tnc->tx_path, payload, len, + frame, sizeof(frame)); + if (frame_len == 0) { + return HYBBX_ERR_INVALID; + } + + return hybbx_tnc_send_frame(tnc, frame, frame_len); +} + +hybbx_result_t hybbx_tnc_send_converse(hybbx_tnc_t *tnc, + const char *data, size_t len) +{ + hybbx_result_t rc; + + if (tnc == NULL || data == NULL) { + return HYBBX_ERR_INVALID; + } + + if (tnc->config.protocol != HYBBX_PACKET_RADIO_PROTO_HOSTMODE) { + return HYBBX_ERR_UNSUPPORTED; + } + + rc = tnc_wait_half_duplex_clear(tnc); + if (rc != HYBBX_OK) { + return rc; + } + + return send_raw(tnc, (const uint8_t *)data, len); +} + +hybbx_packet_radio_duplex_t hybbx_tnc_duplex(const hybbx_tnc_t *tnc) +{ + if (tnc == NULL) { + return HYBBX_PACKET_RADIO_DUPLEX_HALF; + } + + return tnc->config.params.duplex; +} + +hybbx_packet_radio_band_t hybbx_tnc_band(const hybbx_tnc_t *tnc) +{ + if (tnc == NULL) { + return HYBBX_PACKET_RADIO_BAND_UNSET; + } + + return tnc->config.params.band; +} + +int hybbx_tnc_duplex_is_full(const hybbx_tnc_t *tnc) +{ + return tnc != NULL && + tnc->config.params.duplex == HYBBX_PACKET_RADIO_DUPLEX_FULL; +} + +int hybbx_tnc_host_connected(const hybbx_tnc_t *tnc) +{ + if (tnc == NULL) { + return 0; + } + + return tnc->host.state == HYBBX_TNC_HOST_CONNECTED; +} + +hybbx_result_t hybbx_tnc_poll(hybbx_tnc_t *tnc) +{ + uint8_t buf[256]; + size_t read_len; + hybbx_result_t rc; + + if (tnc == NULL) { + return HYBBX_ERR_INVALID; + } + + rc = hybbx_serial_read(tnc->serial, buf, sizeof(buf), &read_len); + if (rc != HYBBX_OK) { + return rc; + } + + if (read_len == 0) { + return HYBBX_OK; + } + + switch (tnc->config.protocol) { + case HYBBX_PACKET_RADIO_PROTO_KISS: + hybbx_kiss_decoder_feed(&tnc->kiss_dec, buf, read_len, + on_kiss_frame, tnc); + break; + case HYBBX_PACKET_RADIO_PROTO_SIXPACK: + hybbx_sixpack_decoder_feed(&tnc->sixpack_dec, buf, read_len, + on_sixpack_frame, tnc); + break; + case HYBBX_PACKET_RADIO_PROTO_HOSTMODE: + hybbx_tnc_host_feed(&tnc->host, buf, read_len); + break; + default: + break; + } + + return HYBBX_OK; +} diff --git a/plugins/packet_radio/tnc_host.c b/plugins/packet_radio/tnc_host.c new file mode 100644 index 0000000..8700e61 --- /dev/null +++ b/plugins/packet_radio/tnc_host.c @@ -0,0 +1,143 @@ +#include "tnc_host.h" + +#include <ctype.h> +#include <string.h> + +void hybbx_tnc_host_init(hybbx_tnc_host_t *host, + hybbx_tnc_host_text_cb text_cb, + hybbx_tnc_host_event_cb event_cb, + void *userdata) +{ + if (host == NULL) { + return; + } + + memset(host, 0, sizeof(*host)); + host->state = HYBBX_TNC_HOST_BOOT; + host->text_cb = text_cb; + host->event_cb = event_cb; + host->userdata = userdata; +} + +void hybbx_tnc_host_reset(hybbx_tnc_host_t *host) +{ + if (host == NULL) { + return; + } + + host->line_len = 0; + host->line[0] = '\0'; + host->state = HYBBX_TNC_HOST_BOOT; +} + +static int host_line_ieq(const char *line, const char *prefix) +{ + size_t i; + size_t plen = strlen(prefix); + + for (i = 0; i < plen; i++) { + char a = (char)tolower((unsigned char)line[i]); + char b = (char)tolower((unsigned char)prefix[i]); + + if (a != b) { + return 0; + } + } + + return 1; +} + +static void host_emit_event(hybbx_tnc_host_t *host, hybbx_tnc_host_state_t state) +{ + if (host->state == state) { + return; + } + + host->state = state; + if (host->event_cb != NULL) { + host->event_cb(state, host->userdata); + } +} + +static void host_process_line(hybbx_tnc_host_t *host) +{ + const char *line = host->line; + + if (host_line_ieq(line, "cmd:") || host_line_ieq(line, "cmd: ")) { + host_emit_event(host, HYBBX_TNC_HOST_CMD); + return; + } + + if (strstr(line, "***") != NULL && + (host_line_ieq(line, "*** connected") || + host_line_ieq(line, "*** connected ") || + strstr(line, "CONNECTED") != NULL)) { + host_emit_event(host, HYBBX_TNC_HOST_CONNECTED); + return; + } + + if (strstr(line, "***") != NULL && + (host_line_ieq(line, "*** disconnected") || + strstr(line, "DISCONNECTED") != NULL)) { + host_emit_event(host, HYBBX_TNC_HOST_CMD); + return; + } + + if (host->state == HYBBX_TNC_HOST_CONNECTED && host->text_cb != NULL && + line[0] != '\0') { + host->text_cb((const uint8_t *)line, strlen(line), host->userdata); + host->text_cb((const uint8_t *)"\r\n", 2, host->userdata); + } +} + +static void host_push_byte(hybbx_tnc_host_t *host, uint8_t byte) +{ + if (byte == '\r' || byte == '\n') { + if (host->line_len > 0) { + host->line[host->line_len] = '\0'; + host_process_line(host); + host->line_len = 0; + } + return; + } + + if (host->line_len + 1 >= sizeof(host->line)) { + host->line_len = 0; + return; + } + + host->line[host->line_len++] = (char)byte; +} + +void hybbx_tnc_host_feed(hybbx_tnc_host_t *host, + const uint8_t *data, size_t len) +{ + size_t i; + + if (host == NULL || data == NULL) { + return; + } + + for (i = 0; i < len; i++) { + host_push_byte(host, data[i]); + } +} + +size_t hybbx_tnc_host_format_cmd(const char *cmd, char *out, size_t out_cap) +{ + size_t len; + + if (cmd == NULL || out == NULL || out_cap < 3) { + return 0; + } + + len = strlen(cmd); + if (len + 2 >= out_cap) { + return 0; + } + + memcpy(out, cmd, len); + out[len++] = '\r'; + out[len] = '\0'; + return len; +} diff --git a/plugins/packet_radio/tnc_host.h b/plugins/packet_radio/tnc_host.h new file mode 100644 index 0000000..af55505 --- /dev/null +++ b/plugins/packet_radio/tnc_host.h @@ -0,0 +1,51 @@ +#ifndef HYBBX_TNC_HOST_H +#define HYBBX_TNC_HOST_H + +#include "hybbx/types.h" + +#include <stddef.h> +#include <stdint.h> + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum hybbx_tnc_host_state { + HYBBX_TNC_HOST_BOOT = 0, + HYBBX_TNC_HOST_CMD = 1, + HYBBX_TNC_HOST_CONNECTED = 2 +} hybbx_tnc_host_state_t; + +typedef void (*hybbx_tnc_host_text_cb)(const uint8_t *data, size_t len, + void *userdata); + +typedef void (*hybbx_tnc_host_event_cb)(hybbx_tnc_host_state_t state, + void *userdata); + +typedef struct hybbx_tnc_host { + hybbx_tnc_host_state_t state; + char line[256]; + size_t line_len; + hybbx_tnc_host_text_cb text_cb; + hybbx_tnc_host_event_cb event_cb; + void *userdata; +} hybbx_tnc_host_t; + +void hybbx_tnc_host_init(hybbx_tnc_host_t *host, + hybbx_tnc_host_text_cb text_cb, + hybbx_tnc_host_event_cb event_cb, + void *userdata); + +void hybbx_tnc_host_reset(hybbx_tnc_host_t *host); + +void hybbx_tnc_host_feed(hybbx_tnc_host_t *host, + const uint8_t *data, size_t len); + +/** Build a TNC2 command line (adds CR). Returns length or 0. */ +size_t hybbx_tnc_host_format_cmd(const char *cmd, char *out, size_t out_cap); + +#ifdef __cplusplus +} +#endif + +#endif /* HYBBX_TNC_HOST_H */ diff --git a/plugins/packet_radio/tnc_profiles.c b/plugins/packet_radio/tnc_profiles.c new file mode 100644 index 0000000..05a2397 --- /dev/null +++ b/plugins/packet_radio/tnc_profiles.c @@ -0,0 +1,399 @@ +#include "hybbx/tnc.h" +#include "hybbx/log.h" + +#include <stdio.h> + +const char *hybbx_packet_radio_duplex_name(hybbx_packet_radio_duplex_t duplex) +{ + switch (duplex) { + case HYBBX_PACKET_RADIO_DUPLEX_FULL: + return "full"; + case HYBBX_PACKET_RADIO_DUPLEX_HALF: + return "half"; + case HYBBX_PACKET_RADIO_DUPLEX_UNSET: + default: + return "unset"; + } +} + +const char *hybbx_packet_radio_band_name(hybbx_packet_radio_band_t band) +{ + switch (band) { + case HYBBX_PACKET_RADIO_BAND_CB: + return "cb"; + case HYBBX_PACKET_RADIO_BAND_AMATEUR: + return "amateur"; + case HYBBX_PACKET_RADIO_BAND_UNSET: + default: + return "unset"; + } +} + +const char *hybbx_packet_radio_modulation_name( + hybbx_packet_radio_modulation_t modulation) +{ + switch (modulation) { + case HYBBX_PACKET_RADIO_MOD_G3RUH_FSK: + return "g3ruh_fsk"; + case HYBBX_PACKET_RADIO_MOD_AFSK: + return "afsk"; + case HYBBX_PACKET_RADIO_MOD_UNSET: + default: + return "unset"; + } +} + +int hybbx_packet_radio_modulation_is_g3ruh( + hybbx_packet_radio_modulation_t modulation) +{ + return modulation == HYBBX_PACKET_RADIO_MOD_G3RUH_FSK; +} + +hybbx_result_t hybbx_tnc_finalize_modulation(hybbx_packet_radio_config_t *cfg) +{ + if (cfg == NULL) { + return HYBBX_ERR_INVALID; + } + + if (cfg->params.modulation == HYBBX_PACKET_RADIO_MOD_UNSET) { + cfg->params.modulation = HYBBX_PACKET_RADIO_MOD_AFSK; + } + + if (cfg->params.modulation == HYBBX_PACKET_RADIO_MOD_G3RUH_FSK) { + cfg->params.modem = HYBBX_TNC2C_MODEM_9600; + cfg->params.radio_baud = HYBBX_G3RUH_RADIO_BAUD; + if (cfg->tnc == HYBBX_PACKET_RADIO_TNC_TNC2C && + cfg->params.clock_mhz <= HYBBX_TNC2C_CLOCK_4_9_MHZ) { + cfg->params.clock_mhz = HYBBX_TNC2C_CLOCK_9_8_MHZ; + } + cfg->params.txdelay = 15; + cfg->params.persist = 128; + cfg->params.slot = 3; + return HYBBX_OK; + } + + if (cfg->params.radio_baud == 0) { + cfg->params.radio_baud = HYBBX_AFSK_RADIO_BAUD; + } + if (cfg->params.modem == 0) { + cfg->params.modem = HYBBX_TNC2C_MODEM_TCM3105; + } + + return HYBBX_OK; +} + +static hybbx_packet_radio_band_t default_band_for_tnc(hybbx_packet_radio_tnc_t tnc) +{ + switch (tnc) { + case HYBBX_PACKET_RADIO_TNC_PCCOM: + case HYBBX_PACKET_RADIO_TNC_BAYCOM: + return HYBBX_PACKET_RADIO_BAND_CB; + case HYBBX_PACKET_RADIO_TNC_TNC2C: + case HYBBX_PACKET_RADIO_TNC_TNC2: + case HYBBX_PACKET_RADIO_TNC_PK232: + case HYBBX_PACKET_RADIO_TNC_MFJ1278: + case HYBBX_PACKET_RADIO_TNC_KANTRONICS: + case HYBBX_PACKET_RADIO_TNC_GENERIC: + default: + return HYBBX_PACKET_RADIO_BAND_AMATEUR; + } +} + +const char *hybbx_packet_radio_tnc_name(hybbx_packet_radio_tnc_t tnc) +{ + switch (tnc) { + case HYBBX_PACKET_RADIO_TNC_BAYCOM: + return "baycom"; + case HYBBX_PACKET_RADIO_TNC_PCCOM: + return "pccom"; + case HYBBX_PACKET_RADIO_TNC_GENERIC: + return "generic"; + case HYBBX_PACKET_RADIO_TNC_TNC2: + return "tnc2"; + case HYBBX_PACKET_RADIO_TNC_PK232: + return "pk232"; + case HYBBX_PACKET_RADIO_TNC_MFJ1278: + return "mfj1278"; + case HYBBX_PACKET_RADIO_TNC_KANTRONICS: + return "kantronics"; + case HYBBX_PACKET_RADIO_TNC_TNC2C: + default: + return "tnc2c"; + } +} + +const char *hybbx_kiss_entry_name(hybbx_kiss_entry_t entry) +{ + switch (entry) { + case HYBBX_KISS_ENTRY_NONE: + return "none"; + case HYBBX_KISS_ENTRY_KISS_ON: + return "kiss_on"; + case HYBBX_KISS_ENTRY_ESC_AT_K: + return "esc_at_k"; + case HYBBX_KISS_ENTRY_AUTO: + return "auto"; + case HYBBX_KISS_ENTRY_UNSET: + default: + return "unset"; + } +} + +static hybbx_kiss_entry_t default_kiss_entry_for_tnc(hybbx_packet_radio_tnc_t tnc) +{ + (void)tnc; + return HYBBX_KISS_ENTRY_NONE; +} + +hybbx_result_t hybbx_tnc_finalize_kiss_entry(hybbx_packet_radio_config_t *cfg) +{ + if (cfg == NULL) { + return HYBBX_ERR_INVALID; + } + + if (cfg->params.kiss_entry == HYBBX_KISS_ENTRY_UNSET) { + cfg->params.kiss_entry = default_kiss_entry_for_tnc(cfg->tnc); + } + + if (cfg->params.kiss_exit == HYBBX_KISS_EXIT_UNSET) { + if (cfg->params.kiss_entry == HYBBX_KISS_ENTRY_NONE) { + cfg->params.kiss_exit = HYBBX_KISS_EXIT_NONE; + } else { + cfg->params.kiss_exit = HYBBX_KISS_EXIT_AUTO; + } + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_tnc_finalize_radio_duplex(hybbx_packet_radio_config_t *cfg) +{ + if (cfg == NULL) { + return HYBBX_ERR_INVALID; + } + + if (cfg->params.band == HYBBX_PACKET_RADIO_BAND_UNSET) { + cfg->params.band = default_band_for_tnc(cfg->tnc); + } + + if (cfg->params.duplex == HYBBX_PACKET_RADIO_DUPLEX_UNSET) { + cfg->params.duplex = HYBBX_PACKET_RADIO_DUPLEX_HALF; + } + + if (cfg->params.band == HYBBX_PACKET_RADIO_BAND_CB && + cfg->params.duplex == HYBBX_PACKET_RADIO_DUPLEX_FULL) { + hybbx_log_warn("[tnc] radio_band=cb allows half-duplex only; " + "ignoring radio_duplex=full"); + cfg->params.duplex = HYBBX_PACKET_RADIO_DUPLEX_HALF; + } + + cfg->params.fullduplex = + (cfg->params.duplex == HYBBX_PACKET_RADIO_DUPLEX_FULL) ? 1 : 0; + + return HYBBX_OK; +} + +hybbx_result_t hybbx_tnc_finalize_csma(hybbx_packet_radio_config_t *cfg) +{ + if (cfg == NULL) { + return HYBBX_ERR_INVALID; + } + + if (cfg->params.persist == 0) { + if (cfg->params.band == HYBBX_PACKET_RADIO_BAND_CB) { + cfg->params.persist = 255; + } else { + cfg->params.persist = 63; + } + } else if (cfg->params.band == HYBBX_PACKET_RADIO_BAND_CB && + cfg->params.persist < 255) { + hybbx_log_warn("[tnc] CB band persist=%u overridden to 255 for beacon TX", + cfg->params.persist); + cfg->params.persist = 255; + } + + if (cfg->params.txdelay == 0) { + cfg->params.txdelay = 50; + } + + if (cfg->params.slot == 0) { + cfg->params.slot = 10; + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_tnc_finalize_serial_line(hybbx_packet_radio_config_t *cfg) +{ + if (cfg == NULL) { + return HYBBX_ERR_INVALID; + } + + if (cfg->params.data_bits == 0) { + if (cfg->tnc == HYBBX_PACKET_RADIO_TNC_TNC2C || + cfg->tnc == HYBBX_PACKET_RADIO_TNC_MFJ1278) { + cfg->params.data_bits = HYBBX_TNC2C_DATA_BITS; + } else { + cfg->params.data_bits = 8; + } + } + + if (cfg->params.serial_parity == HYBBX_TNC_SERIAL_PARITY_UNSET) { + if (cfg->tnc == HYBBX_PACKET_RADIO_TNC_TNC2C || + cfg->tnc == HYBBX_PACKET_RADIO_TNC_MFJ1278) { + cfg->params.serial_parity = HYBBX_TNC_SERIAL_PARITY_EVEN; + } else { + cfg->params.serial_parity = HYBBX_TNC_SERIAL_PARITY_NONE; + } + } + + if (cfg->params.stop_bits == 0) { + cfg->params.stop_bits = 1; + } + + if (cfg->params.assert_modem_lines < 0) { + cfg->params.assert_modem_lines = + (cfg->tnc == HYBBX_PACKET_RADIO_TNC_TNC2C) ? 1 : 0; + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_tnc_profile_apply_defaults(hybbx_packet_radio_config_t *cfg) +{ + if (cfg == NULL) { + return HYBBX_ERR_INVALID; + } + + switch (cfg->tnc) { + case HYBBX_PACKET_RADIO_TNC_BAYCOM: + if (cfg->baud == 0) { + cfg->baud = HYBBX_PACKET_RADIO_DEFAULT_BAUD; + } + if (cfg->params.band == HYBBX_PACKET_RADIO_BAND_UNSET) { + cfg->params.band = HYBBX_PACKET_RADIO_BAND_CB; + } + cfg->params.radio_baud = 1200; + cfg->params.modem = HYBBX_TNC2C_MODEM_TCM3105; + if (cfg->protocol == 0) { + cfg->protocol = HYBBX_PACKET_RADIO_PROTO_KISS; + } + if (cfg->params.txdelay == 0) { + cfg->params.txdelay = 30; + } + if (cfg->params.persist == 0) { + cfg->params.persist = 63; + } + if (cfg->params.slot == 0) { + cfg->params.slot = 10; + } + break; + + case HYBBX_PACKET_RADIO_TNC_PCCOM: + if (cfg->baud == 0) { + cfg->baud = HYBBX_PACKET_RADIO_DEFAULT_BAUD; + } + if (cfg->params.band == HYBBX_PACKET_RADIO_BAND_UNSET) { + cfg->params.band = HYBBX_PACKET_RADIO_BAND_CB; + } + cfg->params.radio_baud = 1200; + cfg->params.modem = HYBBX_TNC2C_MODEM_TCM3105; + if (cfg->protocol == 0) { + cfg->protocol = HYBBX_PACKET_RADIO_PROTO_HOSTMODE; + } + if (cfg->params.duplex == HYBBX_PACKET_RADIO_DUPLEX_UNSET) { + cfg->params.duplex = HYBBX_PACKET_RADIO_DUPLEX_HALF; + } + if (cfg->params.txdelay == 0) { + cfg->params.txdelay = 25; + } + if (cfg->params.persist == 0) { + cfg->params.persist = 128; + } + if (cfg->params.slot == 0) { + cfg->params.slot = 10; + } + cfg->params.host_connect_on_start = 1; + break; + + case HYBBX_PACKET_RADIO_TNC_GENERIC: + if (cfg->baud == 0) { + cfg->baud = HYBBX_PACKET_RADIO_DEFAULT_BAUD; + } + if (cfg->protocol == 0) { + cfg->protocol = HYBBX_PACKET_RADIO_PROTO_KISS; + } + break; + + case HYBBX_PACKET_RADIO_TNC_TNC2: + if (cfg->baud == 0) { + cfg->baud = 9600; + } + if (cfg->params.radio_baud == 0) { + cfg->params.radio_baud = HYBBX_TNC2C_DEFAULT_RADIO_BAUD; + } + if (cfg->protocol == 0) { + cfg->protocol = HYBBX_PACKET_RADIO_PROTO_KISS; + } + break; + + case HYBBX_PACKET_RADIO_TNC_PK232: + if (cfg->baud == 0) { + cfg->baud = 9600; + } + if (cfg->params.radio_baud == 0) { + cfg->params.radio_baud = HYBBX_TNC2C_DEFAULT_RADIO_BAUD; + } + if (cfg->protocol == 0) { + cfg->protocol = HYBBX_PACKET_RADIO_PROTO_KISS; + } + break; + + case HYBBX_PACKET_RADIO_TNC_MFJ1278: + if (cfg->baud == 0) { + cfg->baud = 4800; + } + if (cfg->params.radio_baud == 0) { + cfg->params.radio_baud = HYBBX_TNC2C_DEFAULT_RADIO_BAUD; + } + if (cfg->protocol == 0) { + cfg->protocol = HYBBX_PACKET_RADIO_PROTO_KISS; + } + break; + + case HYBBX_PACKET_RADIO_TNC_KANTRONICS: + if (cfg->baud == 0) { + cfg->baud = 9600; + } + if (cfg->params.radio_baud == 0) { + cfg->params.radio_baud = HYBBX_TNC2C_DEFAULT_RADIO_BAUD; + } + if (cfg->protocol == 0) { + cfg->protocol = HYBBX_PACKET_RADIO_PROTO_KISS; + } + break; + + case HYBBX_PACKET_RADIO_TNC_TNC2C: + default: + if (cfg->params.radio_baud == 0) { + cfg->params.radio_baud = HYBBX_TNC2C_DEFAULT_RADIO_BAUD; + } + if (cfg->params.clock_mhz == 0.0f) { + cfg->params.clock_mhz = HYBBX_TNC2C_CLOCK_4_9_MHZ; + } + if (cfg->protocol == 0) { + cfg->protocol = HYBBX_PACKET_RADIO_PROTO_KISS; + } + break; + } + + if (cfg->protocol == 0) { + cfg->protocol = HYBBX_PACKET_RADIO_PROTO_KISS; + } + + (void)hybbx_tnc_finalize_modulation(cfg); + (void)hybbx_tnc_finalize_radio_duplex(cfg); + (void)hybbx_tnc_finalize_csma(cfg); + (void)hybbx_tnc_finalize_serial_line(cfg); + return hybbx_tnc_finalize_kiss_entry(cfg); +} diff --git a/plugins/ssh/CMakeLists.txt b/plugins/ssh/CMakeLists.txt new file mode 100644 index 0000000..9c656b8 --- /dev/null +++ b/plugins/ssh/CMakeLists.txt @@ -0,0 +1,35 @@ +add_library(hybbx_plugin_ssh STATIC + ssh.c + ssh_config.c + ssh_keys.c +) + +find_package(PkgConfig REQUIRED) +pkg_check_modules(LIBSSH REQUIRED IMPORTED_TARGET libssh) + +find_package(Threads REQUIRED) + +target_include_directories(hybbx_plugin_ssh + PRIVATE ${CMAKE_SOURCE_DIR}/include +) + +target_compile_definitions(hybbx_plugin_ssh + PRIVATE HYBBX_PLUGIN_BUILD +) + +target_link_libraries(hybbx_plugin_ssh + PRIVATE + hybbx_core + Threads::Threads + PkgConfig::LIBSSH +) + +hybbx_link_plugin_instances(hybbx_plugin_ssh HYBBX_HAVE_PLUGIN_SSH) +hybbx_apply_hardening(hybbx_plugin_ssh) + +if(HYBBX_INSTALL_DEV) +install(TARGETS hybbx_plugin_ssh + ARCHIVE DESTINATION ${HYBBX_PLUGIN_INSTALL_DIR} + LIBRARY DESTINATION ${HYBBX_PLUGIN_INSTALL_DIR} +) +endif() diff --git a/plugins/ssh/ssh.c b/plugins/ssh/ssh.c new file mode 100644 index 0000000..442653d --- /dev/null +++ b/plugins/ssh/ssh.c @@ -0,0 +1,728 @@ +/* + * ssh — libssh transport plugin (port 3232). INI: [transport.ssh]. + * + * Links dynamically against libssh (LGPL-2.1+). See share/THIRD_PARTY_NOTICES.txt. + */ +#include "hybbx/plugin.h" +#include "hybbx/service.h" +#include "hybbx/session.h" +#include "hybbx/socket.h" +#include "hybbx/security_ban.h" +#include "hybbx/ssh.h" +#include "hybbx/traffic.h" +#include "hybbx/util.h" +#include "hybbx/log.h" + +#include <libssh/callbacks.h> +#include <libssh/libssh.h> +#include <libssh/server.h> + +#include <arpa/inet.h> +#include <errno.h> +#include <netinet/in.h> +#include <netinet/tcp.h> +#include <poll.h> +#include <pthread.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#define SSH_CLIENT_POLL_MS 30000 +#define SSH_READ_BUF 512 + +typedef struct ssh_client { + int fd; + ssh_session session; + ssh_channel channel; + ssh_event event; + hybbx_session_t *hbx_session; + int ssh_authenticated; + int auth_attempts; + hybbx_result_t last_rc; + volatile int shell_ready; + struct ssh_server_callbacks_struct server_cb; + struct ssh_channel_callbacks_struct channel_cb; +} ssh_client_t; + +typedef struct ssh_client_ctx { + ssh_client_t *client; +} ssh_client_ctx_t; + +extern const hybbx_transport_plugin_t hybbx_plugin_ssh; + +static hybbx_service_t *g_service; +static hybbx_ssh_config_t g_config; +static char g_hostkey_path[HYBBX_PATH_MAX]; +static pthread_t g_accept_thread; +static int g_listen_v4 = -1; +static int g_listen_v6 = -1; +static volatile int g_ssh_running = 0; + +static hybbx_result_t ssh_plugin_stop(void); + +static int set_socket_options(int fd, int family) +{ + int on = 1; + + if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) != 0) { + return -1; + } + + if (setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &on, sizeof(on)) != 0) { + return -1; + } + + hybbx_socket_nosigpipe(fd); + +#ifdef IPV6_V6ONLY + if (family == AF_INET6) { + if (setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &on, sizeof(on)) != 0) { + return -1; + } + } +#else + (void)family; +#endif + + return 0; +} + +static int create_listen_socket(int family, const char *bind_addr, unsigned port) +{ + int fd; + int rc; + + fd = socket(family, SOCK_STREAM, 0); + if (fd < 0) { + return -1; + } + + if (set_socket_options(fd, family) != 0) { + close(fd); + return -1; + } + + if (family == AF_INET6) { + struct sockaddr_in6 addr6; + + memset(&addr6, 0, sizeof(addr6)); + addr6.sin6_family = AF_INET6; + addr6.sin6_port = htons((uint16_t)port); + + if (inet_pton(AF_INET6, bind_addr, &addr6.sin6_addr) != 1) { + close(fd); + return -1; + } + + rc = bind(fd, (struct sockaddr *)&addr6, sizeof(addr6)); + } else { + struct sockaddr_in addr4; + + memset(&addr4, 0, sizeof(addr4)); + addr4.sin_family = AF_INET; + addr4.sin_port = htons((uint16_t)port); + + if (inet_pton(AF_INET, bind_addr, &addr4.sin_addr) != 1) { + close(fd); + return -1; + } + + rc = bind(fd, (struct sockaddr *)&addr4, sizeof(addr4)); + } + + if (rc != 0) { + close(fd); + return -1; + } + + if (listen(fd, 16) != 0) { + close(fd); + return -1; + } + + return fd; +} + +/* + * SSH wire authentication only — accepts any username/password so the client + * can open a shell channel. HyBBX login (guest auto-login or /login prompt) + * follows [auth] in hybbx.ini, same as telnet. + */ +static int ssh_auth_password(ssh_session session, const char *user, + const char *password, void *userdata) +{ + ssh_client_t *client = (ssh_client_t *)userdata; + + (void)session; + (void)user; + (void)password; + + if (client == NULL) { + return SSH_AUTH_DENIED; + } + + client->auth_attempts++; + client->ssh_authenticated = 1; + return SSH_AUTH_SUCCESS; +} + +static hybbx_result_t ssh_plugin_stop(void); + +static int ssh_channel_pty_request(ssh_session session, ssh_channel channel, + const char *term, int cols, int rows, + int py, int px, void *userdata) +{ + (void)session; + (void)channel; + (void)term; + (void)cols; + (void)rows; + (void)py; + (void)px; + (void)userdata; + return SSH_OK; +} + +static int ssh_channel_shell_request(ssh_session session, ssh_channel channel, + void *userdata) +{ + ssh_client_t *client = (ssh_client_t *)userdata; + + (void)session; + (void)channel; + + if (client == NULL) { + return SSH_ERROR; + } + + client->shell_ready = 1; + return SSH_OK; +} + +static ssh_channel ssh_channel_open(ssh_session session, void *userdata) +{ + ssh_client_t *client = (ssh_client_t *)userdata; + + (void)session; + + if (client == NULL) { + return NULL; + } + + client->channel = ssh_channel_new(session); + if (client->channel == NULL) { + return NULL; + } + + memset(&client->channel_cb, 0, sizeof(client->channel_cb)); + client->channel_cb.userdata = client; + client->channel_cb.channel_pty_request_function = ssh_channel_pty_request; + client->channel_cb.channel_shell_request_function = ssh_channel_shell_request; + ssh_callbacks_init(&client->channel_cb); + ssh_set_channel_callbacks(client->channel, &client->channel_cb); + + return client->channel; +} + +static hybbx_result_t ssh_plugin_write(hybbx_session_t *session, + const char *data, size_t len) +{ + ssh_client_t *client; + size_t i; + int rc; + + if (session == NULL || data == NULL) { + return HYBBX_ERR_INVALID; + } + + client = (ssh_client_t *)session->transport_data; + if (client == NULL || client->channel == NULL) { + return HYBBX_ERR_INVALID; + } + + for (i = 0; i < len; i++) { + char out[2]; + size_t out_len = 1; + + out[0] = data[i]; + if (data[i] == '\n' && (i == 0 || data[i - 1] != '\r')) { + out[0] = '\r'; + out[1] = '\n'; + out_len = 2; + } + + rc = ssh_channel_write(client->channel, out, out_len); + if (rc < 0) { + return HYBBX_ERR_IO; + } + } + + return HYBBX_OK; +} + +static void ssh_send_busy(ssh_channel channel) +{ + static const char msg[] = "All nodes in use. Try later.\r\n"; + + if (channel == NULL) { + return; + } + + (void)ssh_channel_write(channel, msg, sizeof(msg) - 1u); +} + +static int ssh_wait_auth_and_shell(ssh_client_t *client) +{ + int ticks = 0; + + while (g_ssh_running) { + if (client->ssh_authenticated && client->channel != NULL && + client->shell_ready) { + return 0; + } + + if (client->auth_attempts >= 5 || ticks >= 300) { + return -1; + } + + if (ssh_event_dopoll(client->event, 100) == SSH_ERROR) { + return -1; + } + ticks++; + } + + return -1; +} + +static void ssh_on_user_data(const uint8_t *data, size_t len, void *ctx) +{ + ssh_client_ctx_t *cctx = (ssh_client_ctx_t *)ctx; + hybbx_result_t rc; + + if (cctx == NULL || cctx->client == NULL || + cctx->client->hbx_session == NULL || data == NULL || len == 0) { + return; + } + + rc = hybbx_session_handle_input(cctx->client->hbx_session, data, len); + if (rc != HYBBX_OK) { + cctx->client->last_rc = rc; + } +} + +static int ssh_plugin_service_request(ssh_session session, const char *service, + void *userdata) +{ + (void)session; + (void)userdata; + + if (service == NULL) { + return SSH_ERROR; + } + + if (strcmp(service, "ssh-userauth") == 0 || + strcmp(service, "ssh-connection") == 0) { + return SSH_OK; + } + + return SSH_ERROR; +} + +static void *ssh_client_thread(void *arg) +{ + ssh_client_t *client = (ssh_client_t *)arg; + ssh_bind sshbind = NULL; + ssh_client_ctx_t cctx; + uint8_t buf[SSH_READ_BUF]; + hybbx_result_t rc; + char remote[64]; + + if (client == NULL || g_service == NULL) { + free(client); + return NULL; + } + + remote[0] = '\0'; + (void)hybbx_socket_peer_name(client->fd, remote, sizeof(remote)); + + sshbind = ssh_bind_new(); + if (sshbind == NULL) { + goto cleanup; + } + + if (ssh_bind_options_set(sshbind, SSH_BIND_OPTIONS_HOSTKEY, + g_hostkey_path) != SSH_OK) { + hybbx_log_warn("[ssh] host key load failed: %s", g_hostkey_path); + goto cleanup; + } + + client->session = ssh_new(); + if (client->session == NULL) { + goto cleanup; + } + + if (ssh_bind_accept_fd(sshbind, client->session, client->fd) != SSH_OK) { + goto cleanup; + } + + memset(&client->server_cb, 0, sizeof(client->server_cb)); + client->server_cb.userdata = client; + client->server_cb.auth_password_function = ssh_auth_password; + client->server_cb.service_request_function = ssh_plugin_service_request; + client->server_cb.channel_open_request_session_function = ssh_channel_open; + ssh_callbacks_init(&client->server_cb); + ssh_set_server_callbacks(client->session, &client->server_cb); + ssh_set_auth_methods(client->session, SSH_AUTH_METHOD_PASSWORD); + + if (ssh_server_init_kex(client->session) != SSH_OK) { + goto cleanup; + } + + if (ssh_handle_key_exchange(client->session) != SSH_OK) { + goto cleanup; + } + + client->event = ssh_event_new(); + if (client->event == NULL) { + goto cleanup; + } + + if (ssh_event_add_session(client->event, client->session) != SSH_OK) { + goto cleanup; + } + + if (ssh_wait_auth_and_shell(client) != 0) { + goto cleanup; + } + + rc = hybbx_session_open(g_service, &hybbx_plugin_ssh, client, + &client->hbx_session); + if (rc != HYBBX_OK || client->hbx_session == NULL) { + if (rc == HYBBX_ERR_BUSY) { + ssh_send_busy(client->channel); + } + goto cleanup; + } + + if (remote[0] != '\0') { + (void)hybbx_session_set_remote(client->hbx_session, remote); + } + + /* + * Telnet clients echo locally when the server sends WONT ECHO; SSH PTY + * clients do not. Mirror telnet UX by enabling HyBBX input echo unless + * [traffic] input_echo=yes already turned it on. + */ + { + const hybbx_traffic_config_t *traffic = hybbx_traffic_config_get(); + + if (traffic == NULL || !traffic->input_echo) { + (void)hybbx_session_set_input_echo(client->hbx_session, 1); + } + } + + cctx.client = client; + client->last_rc = HYBBX_OK; + + while (g_ssh_running && ssh_channel_is_open(client->channel)) { + int n; + int ev; + + ev = ssh_event_dopoll(client->event, SSH_CLIENT_POLL_MS); + if (ev == SSH_ERROR) { + break; + } + + if (ev == 0) { + rc = hybbx_session_tick(client->hbx_session); + if (rc == HYBBX_SESSION_END) { + client->last_rc = HYBBX_SESSION_END; + break; + } + } + + while ((n = ssh_channel_read(client->channel, buf, sizeof(buf), 0)) > 0) { + ssh_on_user_data((const uint8_t *)buf, (size_t)n, &cctx); + if (client->last_rc == HYBBX_SESSION_END) { + break; + } + } + if (client->last_rc == HYBBX_SESSION_END) { + break; + } + if (n < 0 && !ssh_channel_is_open(client->channel)) { + break; + } + + rc = hybbx_session_tick(client->hbx_session); + if (rc == HYBBX_SESSION_END) { + client->last_rc = HYBBX_SESSION_END; + break; + } + } + +cleanup: + if (client->hbx_session != NULL) { + hybbx_session_close(client->hbx_session); + client->hbx_session = NULL; + } + + if (client->channel != NULL) { + if (ssh_channel_is_open(client->channel)) { + ssh_channel_send_eof(client->channel); + ssh_channel_close(client->channel); + } + ssh_channel_free(client->channel); + client->channel = NULL; + } + + if (client->event != NULL) { + ssh_event_free(client->event); + client->event = NULL; + } + + if (client->session != NULL) { + ssh_disconnect(client->session); + ssh_free(client->session); + client->session = NULL; + } + + if (sshbind != NULL) { + ssh_bind_free(sshbind); + sshbind = NULL; + } + + if (client->fd >= 0) { + client->fd = -1; + } + + free(client); + return NULL; +} + +static void accept_client(int client_fd) +{ + ssh_client_t *client; + pthread_t thread; + pthread_attr_t attr; + + client = calloc(1, sizeof(*client)); + if (client == NULL) { + close(client_fd); + return; + } + + client->fd = client_fd; + + pthread_attr_init(&attr); + pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); + + if (pthread_create(&thread, &attr, ssh_client_thread, client) != 0) { + close(client_fd); + free(client); + } + + pthread_attr_destroy(&attr); +} + +static void *ssh_accept_thread(void *arg) +{ + (void)arg; + + while (g_ssh_running) { + struct pollfd fds[2]; + nfds_t nfds = 0; + int i; + int ready; + + memset(fds, 0, sizeof(fds)); + + if (g_listen_v4 >= 0) { + fds[nfds].fd = g_listen_v4; + fds[nfds].events = POLLIN; + nfds++; + } + + if (g_listen_v6 >= 0) { + fds[nfds].fd = g_listen_v6; + fds[nfds].events = POLLIN; + nfds++; + } + + if (nfds == 0) { + break; + } + + ready = poll(fds, nfds, 500); + if (ready < 0) { + if (errno == EINTR) { + continue; + } + break; + } + + if (ready == 0) { + continue; + } + + for (i = 0; i < (int)nfds; i++) { + if (fds[i].revents & POLLIN) { + int client_fd = accept(fds[i].fd, NULL, NULL); + + if (client_fd >= 0) { + if (!hybbx_security_ban_accept_fd(client_fd)) { + close(client_fd); + continue; + } + accept_client(client_fd); + } + } + } + } + + return NULL; +} + +static hybbx_result_t ssh_plugin_init(hybbx_service_t *service) +{ + int rc; + + g_service = service; + + rc = ssh_init(); + if (rc < 0) { + return HYBBX_ERR_IO; + } + + return HYBBX_OK; +} + +static void ssh_plugin_shutdown(void) +{ + ssh_plugin_stop(); + ssh_finalize(); +} + +static hybbx_result_t ssh_plugin_start(const char *config) +{ + char keys_resolved[HYBBX_PATH_MAX]; + hybbx_result_t rc; + + if (g_ssh_running) { + return HYBBX_ERR_BUSY; + } + + rc = hybbx_ssh_config_parse(config, &g_config); + if (rc != HYBBX_OK) { + return rc; + } + + if (hybbx_path_resolve(keys_resolved, sizeof(keys_resolved), + g_config.hostkey_dir) != HYBBX_OK) { + hybbx_strlcpy(keys_resolved, g_config.hostkey_dir, + sizeof(keys_resolved)); + } + + rc = hybbx_ssh_keys_ensure(keys_resolved, g_hostkey_path, + sizeof(g_hostkey_path)); + if (rc != HYBBX_OK) { + return rc; + } + + g_listen_v4 = -1; + g_listen_v6 = -1; + + if (g_config.ipv4) { + g_listen_v4 = create_listen_socket(AF_INET, g_config.bind_v4, + g_config.port); + if (g_listen_v4 < 0) { + hybbx_socket_log_bind_failure("ssh", g_config.bind_v4, + g_config.port); + return HYBBX_ERR_IO; + } + } + + if (g_config.ipv6) { + g_listen_v6 = create_listen_socket(AF_INET6, g_config.bind_v6, + g_config.port); + if (g_listen_v6 < 0) { + hybbx_log_warn("[ssh] IPv6 bind [%s]:%u skipped (%s)", + g_config.bind_v6, g_config.port, strerror(errno)); + } + } + + if (g_listen_v4 < 0 && g_listen_v6 < 0) { + return HYBBX_ERR_IO; + } + + g_ssh_running = 1; + + if (pthread_create(&g_accept_thread, NULL, ssh_accept_thread, NULL) != 0) { + g_ssh_running = 0; + if (g_listen_v4 >= 0) { + close(g_listen_v4); + g_listen_v4 = -1; + } + if (g_listen_v6 >= 0) { + close(g_listen_v6); + g_listen_v6 = -1; + } + return HYBBX_ERR_IO; + } + + { + char msg[256]; + size_t pos = 0; + + pos += (size_t)snprintf(msg + pos, sizeof(msg) - pos, "[ssh] listening"); + if (g_listen_v4 >= 0) { + pos += (size_t)snprintf(msg + pos, sizeof(msg) - pos, " IPv4 %s:%u", + g_config.bind_v4, g_config.port); + } + if (g_listen_v6 >= 0) { + pos += (size_t)snprintf(msg + pos, sizeof(msg) - pos, " IPv6 [%s]:%u", + g_config.bind_v6, g_config.port); + } + pos += (size_t)snprintf(msg + pos, sizeof(msg) - pos, " (libssh)"); + hybbx_log_info("%s", msg); + } + + return HYBBX_OK; +} + +static hybbx_result_t ssh_plugin_stop(void) +{ + if (!g_ssh_running) { + return HYBBX_OK; + } + + g_ssh_running = 0; + + if (g_listen_v4 >= 0) { + shutdown(g_listen_v4, SHUT_RDWR); + close(g_listen_v4); + g_listen_v4 = -1; + } + + if (g_listen_v6 >= 0) { + shutdown(g_listen_v6, SHUT_RDWR); + close(g_listen_v6); + g_listen_v6 = -1; + } + + pthread_join(g_accept_thread, NULL); + hybbx_log_info("[ssh] stop"); + return HYBBX_OK; +} + +const hybbx_transport_plugin_t hybbx_plugin_ssh = { + .name = "ssh", + .kind = HYBBX_TRANSPORT_SSH, + .version = 1, + .init = ssh_plugin_init, + .shutdown = ssh_plugin_shutdown, + .start = ssh_plugin_start, + .stop = ssh_plugin_stop, + .write = ssh_plugin_write, +}; diff --git a/plugins/ssh/ssh_config.c b/plugins/ssh/ssh_config.c new file mode 100644 index 0000000..77fb7db --- /dev/null +++ b/plugins/ssh/ssh_config.c @@ -0,0 +1,132 @@ +#include "hybbx/ssh.h" +#include "hybbx/util.h" + +#include <stdlib.h> +#include <string.h> + +static unsigned int parse_port(const char *value) +{ + char *end; + unsigned long port; + + if (value == NULL || value[0] == '\0') { + return HYBBX_SSH_DEFAULT_PORT; + } + + port = strtoul(value, &end, 10); + if (end == value || *end != '\0' || port == 0 || port > 65535u) { + return HYBBX_SSH_DEFAULT_PORT; + } + + return (unsigned int)port; +} + +static const char *find_kv(const char *config, const char *key, + char *scratch, size_t scratch_len) +{ + const char *cursor = config; + size_t key_len = strlen(key); + + if (config == NULL || key == NULL) { + return NULL; + } + + while (*cursor != '\0') { + const char *sep = strchr(cursor, ';'); + const char *end = sep != NULL ? sep : cursor + strlen(cursor); + const char *eq = strchr(cursor, '='); + + if (eq != NULL && eq < end && (size_t)(eq - cursor) == key_len && + strncmp(cursor, key, key_len) == 0) { + const char *value = eq + 1; + size_t value_len = (size_t)(end - value); + + if (scratch != NULL && scratch_len > 0) { + if (value_len >= scratch_len) { + value_len = scratch_len - 1; + } + memcpy(scratch, value, value_len); + scratch[value_len] = '\0'; + return scratch; + } + + return value; + } + + if (sep == NULL) { + break; + } + cursor = sep + 1; + } + + return NULL; +} + +void hybbx_ssh_config_defaults(hybbx_ssh_config_t *config) +{ + if (config == NULL) { + return; + } + + memset(config, 0, sizeof(*config)); + hybbx_strlcpy(config->bind_v4, HYBBX_SSH_DEFAULT_BIND_V4, + sizeof(config->bind_v4)); + hybbx_strlcpy(config->bind_v6, HYBBX_SSH_DEFAULT_BIND_V6, + sizeof(config->bind_v6)); + hybbx_strlcpy(config->hostkey_dir, HYBBX_SSH_DEFAULT_HOSTKEY_DIR, + sizeof(config->hostkey_dir)); + config->port = HYBBX_SSH_DEFAULT_PORT; + config->ipv4 = 1; + config->ipv6 = 1; +} + +hybbx_result_t hybbx_ssh_config_parse(const char *config, + hybbx_ssh_config_t *out) +{ + char scratch[HYBBX_SSH_BIND_V6_MAX]; + const char *value; + + if (out == NULL) { + return HYBBX_ERR_INVALID; + } + + hybbx_ssh_config_defaults(out); + + if (config == NULL || config[0] == '\0') { + return HYBBX_OK; + } + + value = find_kv(config, "port", scratch, sizeof(scratch)); + out->port = parse_port(value); + + value = find_kv(config, "bind", scratch, sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + if (strchr(value, ':') != NULL) { + hybbx_strlcpy(out->bind_v6, value, sizeof(out->bind_v6)); + } else { + hybbx_strlcpy(out->bind_v4, value, sizeof(out->bind_v4)); + } + } + + value = find_kv(config, "bind6", scratch, sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + hybbx_strlcpy(out->bind_v6, value, sizeof(out->bind_v6)); + } + + value = find_kv(config, "hostkey_dir", scratch, sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + hybbx_strlcpy(out->hostkey_dir, value, sizeof(out->hostkey_dir)); + } + + value = find_kv(config, "ipv4", scratch, sizeof(scratch)); + if (value != NULL) { + out->ipv4 = hybbx_parse_bool(value, 1); + } + + value = find_kv(config, "ipv6", scratch, sizeof(scratch)); + if (value != NULL) { + out->ipv6 = hybbx_parse_bool(value, 1); + } + + return HYBBX_OK; +} diff --git a/plugins/ssh/ssh_keys.c b/plugins/ssh/ssh_keys.c new file mode 100644 index 0000000..656b9d4 --- /dev/null +++ b/plugins/ssh/ssh_keys.c @@ -0,0 +1,146 @@ +#include "hybbx/ssh.h" +#include "hybbx/util.h" +#include "hybbx/log.h" + +#include <libssh/libssh.h> +#include <stdio.h> +#include <string.h> +#include <sys/stat.h> +#include <time.h> +#include <unistd.h> + +static hybbx_result_t mkdir_keys_dir(const char *keys_dir) +{ + char parent[HYBBX_PATH_MAX]; + struct stat st; + + if (keys_dir == NULL || keys_dir[0] == '\0') { + return HYBBX_ERR_INVALID; + } + + if (stat(keys_dir, &st) == 0) { + return S_ISDIR(st.st_mode) ? HYBBX_OK : HYBBX_ERR_IO; + } + + if (hybbx_path_dirname(keys_dir, parent, sizeof(parent)) == HYBBX_OK && + parent[0] != '\0' && strcmp(parent, keys_dir) != 0 && + stat(parent, &st) != 0) { + if (mkdir_keys_dir(parent) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + } + + if (mkdir(keys_dir, 0700) != 0) { + return HYBBX_ERR_IO; + } + + return HYBBX_OK; +} + +static hybbx_result_t generate_ed25519_keypair(const char *priv_path, + const char *pub_path) +{ + ssh_key key = NULL; + int rc; + + rc = ssh_pki_generate(SSH_KEYTYPE_ED25519, 0, &key); + if (rc != SSH_OK || key == NULL) { + hybbx_log_warn("[ssh] Ed25519 generate failed (rc=%d)", rc); + return HYBBX_ERR_IO; + } + + rc = ssh_pki_export_privkey_file(key, NULL, NULL, NULL, priv_path); + if (rc != SSH_OK) { + hybbx_log_warn("[ssh] export private key failed: %s", + ssh_get_error(key)); + ssh_key_free(key); + return HYBBX_ERR_IO; + } + + rc = ssh_pki_export_pubkey_file(key, pub_path); + ssh_key_free(key); + if (rc != SSH_OK) { + hybbx_log_warn("[ssh] export public key failed"); + return HYBBX_ERR_IO; + } + + (void)chmod(priv_path, 0600); + (void)chmod(pub_path, 0644); + return HYBBX_OK; +} + +static int hostkey_needs_rotation(const char *priv_path) +{ + struct stat st; + time_t now; + time_t age; + + if (stat(priv_path, &st) != 0) { + return 0; + } + + now = time(NULL); + if (now <= st.st_mtime) { + return 0; + } + + age = now - st.st_mtime; + return age > (time_t)HYBBX_SSH_HOSTKEY_VALID_DAYS * 86400L; +} + +hybbx_result_t hybbx_ssh_keys_ensure(const char *keys_dir, + char *hostkey_path, + size_t hostkey_path_len) +{ + char resolved_dir[HYBBX_PATH_MAX]; + char priv_path[HYBBX_PATH_MAX]; + char pub_path[HYBBX_PATH_MAX]; + struct stat st; + + if (keys_dir == NULL || hostkey_path == NULL || hostkey_path_len == 0) { + return HYBBX_ERR_INVALID; + } + + if (ssh_init() < 0) { + return HYBBX_ERR_IO; + } + + if (hybbx_path_resolve(resolved_dir, sizeof(resolved_dir), + keys_dir) != HYBBX_OK) { + hybbx_strlcpy(resolved_dir, keys_dir, sizeof(resolved_dir)); + } + + if (mkdir_keys_dir(resolved_dir) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + + if (hybbx_path_join(priv_path, sizeof(priv_path), resolved_dir, + HYBBX_SSH_HOSTKEY_ED25519) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + + if (hybbx_path_join(pub_path, sizeof(pub_path), resolved_dir, + HYBBX_SSH_HOSTKEY_ED25519 ".pub") != HYBBX_OK) { + return HYBBX_ERR_IO; + } + + if (stat(priv_path, &st) == 0 && hostkey_needs_rotation(priv_path)) { + hybbx_log_warn("[ssh] host key older than %u days — rotating %s", + HYBBX_SSH_HOSTKEY_VALID_DAYS, priv_path); + (void)unlink(priv_path); + (void)unlink(pub_path); + } + + if (stat(priv_path, &st) != 0) { + if (generate_ed25519_keypair(priv_path, pub_path) != HYBBX_OK) { + hybbx_log_warn("[ssh] failed to generate host key in %s", + resolved_dir); + return HYBBX_ERR_IO; + } + hybbx_log_info("[ssh] generated host key %s (valid %u days)", priv_path, + HYBBX_SSH_HOSTKEY_VALID_DAYS); + } + + hybbx_strlcpy(hostkey_path, priv_path, hostkey_path_len); + return HYBBX_OK; +} diff --git a/plugins/telnet/CMakeLists.txt b/plugins/telnet/CMakeLists.txt new file mode 100644 index 0000000..144ba1d --- /dev/null +++ b/plugins/telnet/CMakeLists.txt @@ -0,0 +1,26 @@ +add_library(hybbx_plugin_telnet STATIC + telnet.c + telnet_config.c + telnet_proto.c +) + +find_package(Threads REQUIRED) + +target_include_directories(hybbx_plugin_telnet + PRIVATE ${CMAKE_SOURCE_DIR}/include +) + +target_compile_definitions(hybbx_plugin_telnet + PRIVATE HYBBX_PLUGIN_BUILD +) + +target_link_libraries(hybbx_plugin_telnet PRIVATE hybbx_core Threads::Threads) +hybbx_link_plugin_instances(hybbx_plugin_telnet HYBBX_HAVE_PLUGIN_TELNET) +hybbx_apply_hardening(hybbx_plugin_telnet) + +if(HYBBX_INSTALL_DEV) +install(TARGETS hybbx_plugin_telnet + ARCHIVE DESTINATION ${HYBBX_PLUGIN_INSTALL_DIR} + LIBRARY DESTINATION ${HYBBX_PLUGIN_INSTALL_DIR} +) +endif() diff --git a/plugins/telnet/telnet.c b/plugins/telnet/telnet.c new file mode 100644 index 0000000..a857b7d --- /dev/null +++ b/plugins/telnet/telnet.c @@ -0,0 +1,516 @@ +/* + * telnet — TCP transport plugin (port 2323). INI: [transport.telnet]. + */ +#include "hybbx/plugin.h" +#include "hybbx/service.h" +#include "hybbx/session.h" +#include "hybbx/socket.h" +#include "hybbx/security_ban.h" +#include "hybbx/telnet.h" +#include "telnet_proto.h" +#include "hybbx/log.h" + +#include <arpa/inet.h> +#include <errno.h> +#include <netinet/in.h> +#include <netinet/tcp.h> +#include <poll.h> +#include <pthread.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +typedef struct telnet_client { + int fd; + hybbx_telnet_parser_t parser; +} telnet_client_t; + +typedef struct telnet_client_ctx { + hybbx_session_t *session; + hybbx_result_t last_rc; +} telnet_client_ctx_t; + +extern const hybbx_transport_plugin_t hybbx_plugin_telnet; + +static hybbx_service_t *g_service; +static hybbx_telnet_config_t g_config; +static pthread_t g_accept_thread; +static int g_listen_v4 = -1; +static int g_listen_v6 = -1; +static volatile int g_telnet_running = 0; + +static hybbx_result_t telnet_stop(void); + +static int set_socket_options(int fd, int family) +{ + int on = 1; + + if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) != 0) { + return -1; + } + + if (setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &on, sizeof(on)) != 0) { + return -1; + } + + hybbx_socket_nosigpipe(fd); + +#ifdef IPV6_V6ONLY + if (family == AF_INET6) { + if (setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &on, sizeof(on)) != 0) { + return -1; + } + } +#else + (void)family; +#endif + + return 0; +} + +static int create_listen_socket(int family, const char *bind_addr, unsigned port) +{ + int fd; + int rc; + + fd = socket(family, SOCK_STREAM, 0); + if (fd < 0) { + return -1; + } + + if (set_socket_options(fd, family) != 0) { + close(fd); + return -1; + } + + if (family == AF_INET6) { + struct sockaddr_in6 addr6; + + memset(&addr6, 0, sizeof(addr6)); + addr6.sin6_family = AF_INET6; + addr6.sin6_port = htons((uint16_t)port); + + if (inet_pton(AF_INET6, bind_addr, &addr6.sin6_addr) != 1) { + close(fd); + return -1; + } + + rc = bind(fd, (struct sockaddr *)&addr6, sizeof(addr6)); + } else { + struct sockaddr_in addr4; + + memset(&addr4, 0, sizeof(addr4)); + addr4.sin_family = AF_INET; + addr4.sin_port = htons((uint16_t)port); + + if (inet_pton(AF_INET, bind_addr, &addr4.sin_addr) != 1) { + close(fd); + return -1; + } + + rc = bind(fd, (struct sockaddr *)&addr4, sizeof(addr4)); + } + + if (rc != 0) { + close(fd); + return -1; + } + + if (listen(fd, 16) != 0) { + close(fd); + return -1; + } + + return fd; +} + +static hybbx_result_t telnet_send_byte(int fd, char ch) +{ + ssize_t sent; + const char *p = &ch; + + sent = send(fd, p, 1, MSG_NOSIGNAL); + if (sent < 0) { + return HYBBX_ERR_IO; + } + + return HYBBX_OK; +} + +static hybbx_result_t telnet_write(hybbx_session_t *session, + const char *data, size_t len) +{ + telnet_client_t *client; + size_t i; + hybbx_result_t rc; + + if (session == NULL || data == NULL) { + return HYBBX_ERR_INVALID; + } + + client = (telnet_client_t *)session->transport_data; + if (client == NULL || client->fd < 0) { + return HYBBX_ERR_INVALID; + } + + for (i = 0; i < len; i++) { + char ch = data[i]; + + if (ch == '\n' && (i == 0 || data[i - 1] != '\r')) { + rc = telnet_send_byte(client->fd, '\r'); + if (rc != HYBBX_OK) { + return rc; + } + } + + { + ssize_t sent = send(client->fd, &ch, 1, MSG_NOSIGNAL); + + if (sent < 0) { + if (errno == EINTR) { + continue; + } + return HYBBX_ERR_IO; + } + if (sent == 0) { + return HYBBX_ERR_IO; + } + } + } + + return HYBBX_OK; +} + +static void telnet_on_user_data(void *ctx, const uint8_t *data, size_t len) +{ + telnet_client_ctx_t *tctx = (telnet_client_ctx_t *)ctx; + hybbx_result_t rc; + + if (tctx == NULL || tctx->session == NULL || data == NULL || len == 0) { + return; + } + + rc = hybbx_session_handle_input(tctx->session, data, len); + if (rc != HYBBX_OK) { + tctx->last_rc = rc; + } +} + +#define TELNET_CLIENT_POLL_MS 30000 + +static void telnet_send_busy(int fd) +{ + static const char msg[] = "All nodes in use. Try later.\r\n"; + + if (fd < 0) { + return; + } + + (void)send(fd, msg, sizeof(msg) - 1u, 0); +} + +static void *telnet_client_thread(void *arg) +{ + telnet_client_t *client = (telnet_client_t *)arg; + hybbx_session_t *session = NULL; + telnet_client_ctx_t tctx; + uint8_t buf[256]; + ssize_t n; + hybbx_result_t rc; + + if (client == NULL || g_service == NULL) { + free(client); + return NULL; + } + + (void)set_socket_options(client->fd, 0); + + rc = hybbx_telnet_send_greeting(client->fd); + if (rc != HYBBX_OK) { + close(client->fd); + free(client); + return NULL; + } + + rc = hybbx_session_open(g_service, &hybbx_plugin_telnet, client, &session); + if (rc != HYBBX_OK || session == NULL) { + if (rc == HYBBX_ERR_BUSY) { + telnet_send_busy(client->fd); + } + close(client->fd); + free(client); + return NULL; + } + + { + char remote[64]; + + if (hybbx_socket_peer_name(client->fd, remote, sizeof(remote)) == HYBBX_OK) { + (void)hybbx_session_set_remote(session, remote); + } + } + + hybbx_telnet_parser_init(&client->parser); + tctx.session = session; + tctx.last_rc = HYBBX_OK; + + while (g_telnet_running) { + struct pollfd pfd; + int pr; + + pfd.fd = client->fd; + pfd.events = POLLIN; + pfd.revents = 0; + + pr = poll(&pfd, 1, TELNET_CLIENT_POLL_MS); + if (pr < 0) { + if (errno == EINTR) { + continue; + } + break; + } + if (pr == 0) { + rc = hybbx_session_tick(session); + if (rc == HYBBX_SESSION_END) { + tctx.last_rc = HYBBX_SESSION_END; + break; + } + continue; + } + if ((pfd.revents & (POLLERR | POLLHUP | POLLNVAL)) != 0) { + break; + } + if ((pfd.revents & POLLIN) == 0) { + continue; + } + + n = recv(client->fd, buf, sizeof(buf), 0); + if (n < 0) { + if (errno == EINTR) { + continue; + } + break; + } + if (n == 0) { + break; + } + + rc = hybbx_telnet_parser_feed(&client->parser, client->fd, buf, (size_t)n, + telnet_on_user_data, &tctx); + if (rc != HYBBX_OK) { + break; + } + + if (tctx.last_rc == HYBBX_SESSION_END) { + break; + } + } + + hybbx_session_close(session); + shutdown(client->fd, SHUT_RDWR); + close(client->fd); + free(client); + return NULL; +} + +static void accept_client(int client_fd) +{ + telnet_client_t *client; + pthread_t thread; + pthread_attr_t attr; + + client = calloc(1, sizeof(*client)); + if (client == NULL) { + close(client_fd); + return; + } + + client->fd = client_fd; + + pthread_attr_init(&attr); + pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); + + if (pthread_create(&thread, &attr, telnet_client_thread, client) != 0) { + close(client_fd); + free(client); + } + + pthread_attr_destroy(&attr); +} + +static void *telnet_accept_thread(void *arg) +{ + (void)arg; + + while (g_telnet_running) { + struct pollfd fds[2]; + nfds_t nfds = 0; + int i; + int ready; + + memset(fds, 0, sizeof(fds)); + + if (g_listen_v4 >= 0) { + fds[nfds].fd = g_listen_v4; + fds[nfds].events = POLLIN; + nfds++; + } + + if (g_listen_v6 >= 0) { + fds[nfds].fd = g_listen_v6; + fds[nfds].events = POLLIN; + nfds++; + } + + if (nfds == 0) { + break; + } + + ready = poll(fds, nfds, 500); + if (ready < 0) { + if (errno == EINTR) { + continue; + } + break; + } + + if (ready == 0) { + continue; + } + + for (i = 0; i < (int)nfds; i++) { + if (fds[i].revents & POLLIN) { + int client_fd = accept(fds[i].fd, NULL, NULL); + + if (client_fd >= 0) { + if (!hybbx_security_ban_accept_fd(client_fd)) { + close(client_fd); + continue; + } + accept_client(client_fd); + } + } + } + } + + return NULL; +} + +static hybbx_result_t telnet_init(hybbx_service_t *service) +{ + g_service = service; + return HYBBX_OK; +} + +static void telnet_shutdown(void) +{ + telnet_stop(); +} + +static hybbx_result_t telnet_start(const char *config) +{ + hybbx_result_t rc; + + if (g_telnet_running) { + return HYBBX_ERR_BUSY; + } + + rc = hybbx_telnet_config_parse(config, &g_config); + if (rc != HYBBX_OK) { + return rc; + } + + g_listen_v4 = -1; + g_listen_v6 = -1; + + if (g_config.ipv4) { + g_listen_v4 = create_listen_socket(AF_INET, g_config.bind_v4, g_config.port); + if (g_listen_v4 < 0) { + hybbx_socket_log_bind_failure("telnet", g_config.bind_v4, + g_config.port); + return HYBBX_ERR_IO; + } + } + + if (g_config.ipv6) { + g_listen_v6 = create_listen_socket(AF_INET6, g_config.bind_v6, g_config.port); + if (g_listen_v6 < 0) { + hybbx_log_warn("[telnet] IPv6 bind [%s]:%u skipped (%s)", + g_config.bind_v6, g_config.port, strerror(errno)); + } + } + + if (g_listen_v4 < 0 && g_listen_v6 < 0) { + return HYBBX_ERR_IO; + } + + g_telnet_running = 1; + + if (pthread_create(&g_accept_thread, NULL, telnet_accept_thread, NULL) != 0) { + g_telnet_running = 0; + if (g_listen_v4 >= 0) { + close(g_listen_v4); + g_listen_v4 = -1; + } + if (g_listen_v6 >= 0) { + close(g_listen_v6); + g_listen_v6 = -1; + } + return HYBBX_ERR_IO; + } + + { + char msg[256]; + size_t pos = 0; + + pos += (size_t)snprintf(msg + pos, sizeof(msg) - pos, "[telnet] listening"); + if (g_listen_v4 >= 0) { + pos += (size_t)snprintf(msg + pos, sizeof(msg) - pos, " IPv4 %s:%u", + g_config.bind_v4, g_config.port); + } + if (g_listen_v6 >= 0) { + pos += (size_t)snprintf(msg + pos, sizeof(msg) - pos, " IPv6 [%s]:%u", + g_config.bind_v6, g_config.port); + } + hybbx_log_info("%s", msg); + } + + return HYBBX_OK; +} + +static hybbx_result_t telnet_stop(void) +{ + if (!g_telnet_running) { + return HYBBX_OK; + } + + g_telnet_running = 0; + + if (g_listen_v4 >= 0) { + shutdown(g_listen_v4, SHUT_RDWR); + close(g_listen_v4); + g_listen_v4 = -1; + } + + if (g_listen_v6 >= 0) { + shutdown(g_listen_v6, SHUT_RDWR); + close(g_listen_v6); + g_listen_v6 = -1; + } + + pthread_join(g_accept_thread, NULL); + hybbx_log_info("[telnet] stop"); + return HYBBX_OK; +} + +const hybbx_transport_plugin_t hybbx_plugin_telnet = { + .name = "telnet", + .kind = HYBBX_TRANSPORT_TELNET, + .version = 1, + .init = telnet_init, + .shutdown = telnet_shutdown, + .start = telnet_start, + .stop = telnet_stop, + .write = telnet_write, +}; diff --git a/plugins/telnet/telnet_config.c b/plugins/telnet/telnet_config.c new file mode 100644 index 0000000..d154f88 --- /dev/null +++ b/plugins/telnet/telnet_config.c @@ -0,0 +1,125 @@ +#include "hybbx/telnet.h" +#include "hybbx/util.h" + +#include <stdlib.h> +#include <string.h> + +static unsigned int parse_port(const char *value) +{ + char *end; + unsigned long port; + + if (value == NULL || value[0] == '\0') { + return HYBBX_TELNET_DEFAULT_PORT; + } + + port = strtoul(value, &end, 10); + if (end == value || *end != '\0' || port == 0 || port > 65535u) { + return HYBBX_TELNET_DEFAULT_PORT; + } + + return (unsigned int)port; +} + +static const char *find_kv(const char *config, const char *key, + char *scratch, size_t scratch_len) +{ + const char *cursor = config; + size_t key_len = strlen(key); + + if (config == NULL || key == NULL) { + return NULL; + } + + while (*cursor != '\0') { + const char *sep = strchr(cursor, ';'); + const char *end = sep != NULL ? sep : cursor + strlen(cursor); + const char *eq = strchr(cursor, '='); + + if (eq != NULL && eq < end && (size_t)(eq - cursor) == key_len && + strncmp(cursor, key, key_len) == 0) { + const char *value = eq + 1; + size_t value_len = (size_t)(end - value); + + if (scratch != NULL && scratch_len > 0) { + if (value_len >= scratch_len) { + value_len = scratch_len - 1; + } + memcpy(scratch, value, value_len); + scratch[value_len] = '\0'; + return scratch; + } + + return value; + } + + if (sep == NULL) { + break; + } + cursor = sep + 1; + } + + return NULL; +} + +void hybbx_telnet_config_defaults(hybbx_telnet_config_t *config) +{ + if (config == NULL) { + return; + } + + memset(config, 0, sizeof(*config)); + hybbx_strlcpy(config->bind_v4, HYBBX_TELNET_DEFAULT_BIND_V4, + sizeof(config->bind_v4)); + hybbx_strlcpy(config->bind_v6, HYBBX_TELNET_DEFAULT_BIND_V6, + sizeof(config->bind_v6)); + config->port = HYBBX_TELNET_DEFAULT_PORT; + config->ipv4 = 1; + config->ipv6 = 1; +} + +hybbx_result_t hybbx_telnet_config_parse(const char *config, + hybbx_telnet_config_t *out) +{ + char scratch[HYBBX_TELNET_BIND_V6_MAX]; + const char *value; + + if (out == NULL) { + return HYBBX_ERR_INVALID; + } + + hybbx_telnet_config_defaults(out); + + if (config == NULL || config[0] == '\0') { + return HYBBX_OK; + } + + value = find_kv(config, "port", scratch, sizeof(scratch)); + out->port = parse_port(value); + + value = find_kv(config, "bind", scratch, sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + if (strchr(value, ':') != NULL) { + hybbx_strlcpy(out->bind_v6, value, sizeof(out->bind_v6)); + } else { + hybbx_strlcpy(out->bind_v4, value, sizeof(out->bind_v4)); + } + } + + value = find_kv(config, "bind6", scratch, sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + hybbx_strlcpy(out->bind_v6, value, sizeof(out->bind_v6)); + } + + value = find_kv(config, "ipv4", scratch, sizeof(scratch)); + if (value != NULL) { + out->ipv4 = hybbx_parse_bool(value, 1); + } + + value = find_kv(config, "ipv6", scratch, sizeof(scratch)); + if (value != NULL) { + out->ipv6 = hybbx_parse_bool(value, 1); + } + + return HYBBX_OK; +} diff --git a/plugins/telnet/telnet_proto.c b/plugins/telnet/telnet_proto.c new file mode 100644 index 0000000..523c68c --- /dev/null +++ b/plugins/telnet/telnet_proto.c @@ -0,0 +1,213 @@ +#include "telnet_proto.h" + +#include "hybbx/socket.h" + +#include <errno.h> +#include <string.h> +#include <unistd.h> + +#define TELNET_IAC 255 +#define TELNET_DONT 254 +#define TELNET_DO 253 +#define TELNET_WONT 252 +#define TELNET_WILL 251 +#define TELNET_SB 250 +#define TELNET_GA 249 +#define TELNET_EL 248 +#define TELNET_EC 247 +#define TELNET_AYT 246 +#define TELNET_AO 245 +#define TELNET_IP 244 +#define TELNET_BRK 243 +#define TELNET_DM 242 +#define TELNET_NOP 241 +#define TELNET_SE 240 + +#define TELNET_OPT_ECHO 1 +#define TELNET_OPT_SGA 3 +#define TELNET_OPT_TTYPE 24 +#define TELNET_OPT_NAWS 31 +#define TELNET_OPT_LINEMODE 34 +#define TELNET_OPT_CHARSET 42 + +enum telnet_parse_state { + TS_DATA = 0, + TS_IAC, + TS_CMD, + TS_SUBNEG, + TS_SUBNEG_IAC +}; + +static hybbx_result_t telnet_send_raw(int fd, const uint8_t *data, size_t len) +{ + size_t sent_total = 0; + + while (sent_total < len) { + ssize_t sent; + + sent = send(fd, data + sent_total, len - sent_total, MSG_NOSIGNAL); + if (sent < 0) { + if (errno == EINTR) { + continue; + } + return HYBBX_ERR_IO; + } + if (sent == 0) { + return HYBBX_ERR_IO; + } + + sent_total += (size_t)sent; + } + + return HYBBX_OK; +} + +static hybbx_result_t telnet_send_option(int fd, uint8_t cmd, uint8_t option) +{ + uint8_t buf[3]; + + buf[0] = TELNET_IAC; + buf[1] = cmd; + buf[2] = option; + return telnet_send_raw(fd, buf, sizeof(buf)); +} + +static hybbx_result_t telnet_reply_do(int fd, uint8_t option) +{ + switch (option) { + case TELNET_OPT_ECHO: + return telnet_send_option(fd, TELNET_WONT, option); + case TELNET_OPT_SGA: + return telnet_send_option(fd, TELNET_WILL, option); + case TELNET_OPT_TTYPE: + case TELNET_OPT_NAWS: + case TELNET_OPT_LINEMODE: + case TELNET_OPT_CHARSET: + return telnet_send_option(fd, TELNET_WONT, option); + default: + return telnet_send_option(fd, TELNET_WONT, option); + } +} + +static hybbx_result_t telnet_reply_will(int fd, uint8_t option) +{ + switch (option) { + case TELNET_OPT_SGA: + return telnet_send_option(fd, TELNET_DO, option); + case TELNET_OPT_ECHO: + return telnet_send_option(fd, TELNET_DONT, option); + default: + return telnet_send_option(fd, TELNET_DONT, option); + } +} + +static void emit_byte(hybbx_telnet_data_cb on_data, void *ctx, uint8_t byte) +{ + if (on_data != NULL) { + on_data(ctx, &byte, 1); + } +} + +void hybbx_telnet_parser_init(hybbx_telnet_parser_t *parser) +{ + if (parser == NULL) { + return; + } + + parser->state = TS_DATA; + parser->command = 0; +} + +hybbx_result_t hybbx_telnet_send_greeting(int fd) +{ + static const uint8_t greeting[] = { + TELNET_IAC, TELNET_WONT, TELNET_OPT_ECHO, + TELNET_IAC, TELNET_WILL, TELNET_OPT_SGA, + TELNET_IAC, TELNET_DONT, TELNET_OPT_LINEMODE, + }; + + if (fd < 0) { + return HYBBX_ERR_INVALID; + } + + return telnet_send_raw(fd, greeting, sizeof(greeting)); +} + +hybbx_result_t hybbx_telnet_parser_feed(hybbx_telnet_parser_t *parser, int fd, + const uint8_t *data, size_t len, + hybbx_telnet_data_cb on_data, + void *ctx) +{ + size_t i; + hybbx_result_t rc; + + if (parser == NULL || data == NULL) { + return HYBBX_ERR_INVALID; + } + + for (i = 0; i < len; i++) { + uint8_t byte = data[i]; + + switch (parser->state) { + case TS_DATA: + if (byte == TELNET_IAC) { + parser->state = TS_IAC; + } else { + emit_byte(on_data, ctx, byte); + } + break; + + case TS_IAC: + if (byte == TELNET_IAC) { + emit_byte(on_data, ctx, TELNET_IAC); + parser->state = TS_DATA; + } else if (byte == TELNET_WILL || byte == TELNET_WONT || + byte == TELNET_DO || byte == TELNET_DONT) { + parser->command = (int)byte; + parser->state = TS_CMD; + } else if (byte == TELNET_SB) { + parser->state = TS_SUBNEG; + } else { + parser->state = TS_DATA; + } + break; + + case TS_CMD: + if (fd >= 0) { + if (parser->command == TELNET_DO) { + rc = telnet_reply_do(fd, byte); + if (rc != HYBBX_OK) { + return rc; + } + } else if (parser->command == TELNET_WILL) { + rc = telnet_reply_will(fd, byte); + if (rc != HYBBX_OK) { + return rc; + } + } + } + parser->state = TS_DATA; + break; + + case TS_SUBNEG: + if (byte == TELNET_IAC) { + parser->state = TS_SUBNEG_IAC; + } + break; + + case TS_SUBNEG_IAC: + if (byte == TELNET_SE) { + parser->state = TS_DATA; + } else { + parser->state = TS_SUBNEG; + } + break; + + default: + parser->state = TS_DATA; + break; + } + } + + return HYBBX_OK; +} diff --git a/plugins/telnet/telnet_proto.h b/plugins/telnet/telnet_proto.h new file mode 100644 index 0000000..aa29122 --- /dev/null +++ b/plugins/telnet/telnet_proto.h @@ -0,0 +1,37 @@ +#ifndef HYBBX_TELNET_PROTO_H +#define HYBBX_TELNET_PROTO_H + +#include "hybbx/types.h" + +#include <stddef.h> +#include <stdint.h> + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct hybbx_telnet_parser { + int state; + int command; +} hybbx_telnet_parser_t; + +typedef void (*hybbx_telnet_data_cb)(void *ctx, const uint8_t *data, size_t len); + +void hybbx_telnet_parser_init(hybbx_telnet_parser_t *parser); + +/** Send initial option negotiation (echo, suppress go ahead). */ +hybbx_result_t hybbx_telnet_send_greeting(int fd); + +/** + * Strip telnet protocol bytes and invoke @p on_data for user payload. + */ +hybbx_result_t hybbx_telnet_parser_feed(hybbx_telnet_parser_t *parser, int fd, + const uint8_t *data, size_t len, + hybbx_telnet_data_cb on_data, + void *ctx); + +#ifdef __cplusplus +} +#endif + +#endif /* HYBBX_TELNET_PROTO_H */ diff --git a/plugins/websocket/CMakeLists.txt b/plugins/websocket/CMakeLists.txt new file mode 100644 index 0000000..a63f83d --- /dev/null +++ b/plugins/websocket/CMakeLists.txt @@ -0,0 +1,37 @@ +add_library(hybbx_plugin_websocket STATIC + websocket.c + ws_config.c + ws_proto.c + ws_sha1.c + ws_tls.c +) + +find_package(Threads REQUIRED) + +target_include_directories(hybbx_plugin_websocket + PRIVATE ${CMAKE_SOURCE_DIR}/include +) + +target_compile_definitions(hybbx_plugin_websocket + PRIVATE HYBBX_PLUGIN_BUILD +) + +find_package(OpenSSL QUIET) +if(OpenSSL_FOUND) + target_compile_definitions(hybbx_plugin_websocket PRIVATE HYBBX_WS_HAVE_TLS) + target_link_libraries(hybbx_plugin_websocket PRIVATE OpenSSL::SSL OpenSSL::Crypto) + message(STATUS "WebSocket plugin: OpenSSL found — wss + self-signed certs") +else() + message(STATUS "WebSocket plugin: OpenSSL not found — plain ws only") +endif() + +target_link_libraries(hybbx_plugin_websocket PRIVATE hybbx_core Threads::Threads) +hybbx_link_plugin_instances(hybbx_plugin_websocket HYBBX_HAVE_PLUGIN_WEBSOCKET) +hybbx_apply_hardening(hybbx_plugin_websocket) + +if(HYBBX_INSTALL_DEV) +install(TARGETS hybbx_plugin_websocket + ARCHIVE DESTINATION ${HYBBX_PLUGIN_INSTALL_DIR} + LIBRARY DESTINATION ${HYBBX_PLUGIN_INSTALL_DIR} +) +endif() diff --git a/plugins/websocket/websocket.c b/plugins/websocket/websocket.c new file mode 100644 index 0000000..4ff2230 --- /dev/null +++ b/plugins/websocket/websocket.c @@ -0,0 +1,723 @@ +/* + * websocket — RFC6455 forward-proxy transport. INI: [transport.websocket]. + * + * Raw byte stream to the session core (no HyBBX auth at the wire layer). + * Intended behind a TLS reverse proxy; see docs/WEBSOCKET.md. + */ +#include "hybbx/plugin.h" +#include "hybbx/service.h" +#include "hybbx/session.h" +#include "hybbx/socket.h" +#include "hybbx/security_ban.h" +#include "hybbx/websocket.h" +#include "hybbx/util.h" +#include "ws_proto.h" +#include "ws_tls.h" +#include "hybbx/log.h" + +#include <arpa/inet.h> +#include <errno.h> +#include <netinet/in.h> +#include <netinet/tcp.h> +#include <poll.h> +#include <pthread.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> +#include <unistd.h> + +#define WS_CLIENT_POLL_MS 5000 +#define WS_CLIENT_PING_IDLE_SEC 20 + +typedef struct ws_client { + hybbx_ws_connection_t ws; + hybbx_session_t *hbx_session; + hybbx_result_t last_rc; + int slot_held; + time_t last_io_at; + uint8_t tx_buf[HYBBX_WS_FRAME_PAYLOAD_MAX]; + size_t tx_len; +} ws_client_t; + +typedef struct ws_client_ctx { + ws_client_t *client; +} ws_client_ctx_t; + +extern const hybbx_transport_plugin_t hybbx_plugin_websocket; + +static hybbx_service_t *g_service; +static hybbx_websocket_config_t g_config; +static pthread_t g_accept_thread; +static int g_listen_v4 = -1; +static int g_listen_v6 = -1; +static volatile int g_ws_running = 0; +static unsigned g_active_clients; +static pthread_mutex_t g_client_lock = PTHREAD_MUTEX_INITIALIZER; + +static hybbx_result_t ws_plugin_stop(void); + +static int set_socket_options(int fd, int family) +{ + int on = 1; + + if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) != 0) { + return -1; + } + + if (setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &on, sizeof(on)) != 0) { + return -1; + } + + hybbx_socket_nosigpipe(fd); + +#ifdef IPV6_V6ONLY + if (family == AF_INET6) { + if (setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &on, sizeof(on)) != 0) { + return -1; + } + } +#else + (void)family; +#endif + + return 0; +} + +static int create_listen_socket(int family, const char *bind_addr, unsigned port) +{ + int fd; + int rc; + + fd = socket(family, SOCK_STREAM, 0); + if (fd < 0) { + return -1; + } + + if (set_socket_options(fd, family) != 0) { + close(fd); + return -1; + } + + if (family == AF_INET6) { + struct sockaddr_in6 addr6; + + memset(&addr6, 0, sizeof(addr6)); + addr6.sin6_family = AF_INET6; + addr6.sin6_port = htons((uint16_t)port); + + if (inet_pton(AF_INET6, bind_addr, &addr6.sin6_addr) != 1) { + close(fd); + return -1; + } + + rc = bind(fd, (struct sockaddr *)&addr6, sizeof(addr6)); + } else { + struct sockaddr_in addr4; + + memset(&addr4, 0, sizeof(addr4)); + addr4.sin_family = AF_INET; + addr4.sin_port = htons((uint16_t)port); + + if (inet_pton(AF_INET, bind_addr, &addr4.sin_addr) != 1) { + close(fd); + return -1; + } + + rc = bind(fd, (struct sockaddr *)&addr4, sizeof(addr4)); + } + + if (rc != 0) { + close(fd); + return -1; + } + + if (listen(fd, 16) != 0) { + close(fd); + return -1; + } + + return fd; +} + +static void log_bind_failure(const char *bind_addr, unsigned port) +{ + int err = errno; + + hybbx_socket_log_bind_failure("websocket", bind_addr, port); + if (err == EACCES && port < 1024u) { + hybbx_log_warn("[websocket] port %u is privileged — run as root, grant " + "bind permission, or set port >= 1024 in hybbx.ini", + port); + } +} + +static size_t utf8_char_span(const uint8_t *buf, size_t len, size_t pos) +{ + uint8_t b; + size_t n; + size_t i; + + if (pos >= len) { + return 0; + } + + b = buf[pos]; + if (b < 0x80u) { + return 1; + } + if ((b & 0xE0u) == 0xC0u) { + n = 2; + } else if ((b & 0xF0u) == 0xE0u) { + n = 3; + } else if ((b & 0xF8u) == 0xF0u) { + n = 4; + } else { + return 1; + } + + if (pos + n > len) { + return 0; + } + + for (i = 1; i < n; i++) { + if ((buf[pos + i] & 0xC0u) != 0x80u) { + return 1; + } + } + + return n; +} + +static size_t utf8_complete_prefix(const uint8_t *buf, size_t len) +{ + size_t pos = 0; + + while (pos < len) { + size_t span = utf8_char_span(buf, len, pos); + + if (span == 0) { + break; + } + pos += span; + } + + return pos; +} + +static hybbx_result_t ws_tx_flush(ws_client_t *client, int force_tail) +{ + size_t n; + hybbx_result_t rc; + + if (client == NULL || client->tx_len == 0) { + return HYBBX_OK; + } + + n = utf8_complete_prefix(client->tx_buf, client->tx_len); + if (n == 0 && force_tail) { + client->tx_buf[0] = '?'; + n = 1; + } + if (n == 0) { + return HYBBX_OK; + } + + rc = hybbx_ws_write_text(&client->ws, (const char *)client->tx_buf, n); + if (rc != HYBBX_OK) { + return rc; + } + client->last_io_at = time(NULL); + + if (n < client->tx_len) { + memmove(client->tx_buf, client->tx_buf + n, client->tx_len - n); + } + client->tx_len -= n; + + return HYBBX_OK; +} + +static hybbx_result_t ws_plugin_write(hybbx_session_t *session, + const char *data, size_t len) +{ + ws_client_t *client; + size_t i; + hybbx_result_t rc; + + if (session == NULL || data == NULL) { + return HYBBX_ERR_INVALID; + } + + client = (ws_client_t *)session->transport_data; + if (client == NULL || !client->ws.established) { + return HYBBX_ERR_INVALID; + } + + for (i = 0; i < len; i++) { + char ch = data[i]; + + if (ch == '\n' && (i == 0 || data[i - 1] != '\r')) { + if (client->tx_len + 2 > sizeof(client->tx_buf)) { + rc = ws_tx_flush(client, 1); + if (rc != HYBBX_OK) { + return rc; + } + } + client->tx_buf[client->tx_len++] = '\r'; + } + + if (client->tx_len >= sizeof(client->tx_buf)) { + rc = ws_tx_flush(client, 1); + if (rc != HYBBX_OK) { + return rc; + } + } + + client->tx_buf[client->tx_len++] = (uint8_t)ch; + + rc = ws_tx_flush(client, 0); + if (rc != HYBBX_OK) { + return rc; + } + } + + return HYBBX_OK; +} + +static void ws_send_busy(ws_client_t *client) +{ + static const char msg[] = "All nodes in use. Try later.\r\n"; + + if (client == NULL) { + return; + } + + (void)hybbx_ws_write_text(&client->ws, msg, sizeof(msg) - 1u); +} + +static void ws_send_limit(ws_client_t *client) +{ + static const char msg[] = + "All WebSocket connections in use. Try later.\r\n"; + + if (client == NULL) { + return; + } + + (void)hybbx_ws_write_text(&client->ws, msg, sizeof(msg) - 1u); +} + +static int ws_client_acquire_slot(ws_client_t *client) +{ + if (client == NULL) { + return 0; + } + + pthread_mutex_lock(&g_client_lock); + if (g_active_clients >= g_config.max_connections) { + pthread_mutex_unlock(&g_client_lock); + return 0; + } + + g_active_clients++; + client->slot_held = 1; + pthread_mutex_unlock(&g_client_lock); + return 1; +} + +static void ws_client_release_slot(ws_client_t *client) +{ + if (client == NULL || !client->slot_held) { + return; + } + + pthread_mutex_lock(&g_client_lock); + if (g_active_clients > 0) { + g_active_clients--; + } + client->slot_held = 0; + pthread_mutex_unlock(&g_client_lock); +} + +static void ws_on_user_data(void *ctx, const uint8_t *data, size_t len) +{ + ws_client_ctx_t *wctx = (ws_client_ctx_t *)ctx; + hybbx_result_t rc; + + if (wctx == NULL || wctx->client == NULL || + wctx->client->hbx_session == NULL || data == NULL || len == 0) { + return; + } + + rc = hybbx_session_handle_input(wctx->client->hbx_session, data, len); + if (rc != HYBBX_OK) { + wctx->client->last_rc = rc; + } +} + +static void *ws_client_thread(void *arg) +{ + ws_client_t *client = (ws_client_t *)arg; + ws_client_ctx_t wctx; + hybbx_result_t rc; + char remote[64]; + + if (client == NULL || g_service == NULL) { + free(client); + return NULL; + } + + remote[0] = '\0'; + (void)hybbx_socket_peer_name(client->ws.fd, remote, sizeof(remote)); + + if (hybbx_ws_tls_server_enabled()) { + void *tls = hybbx_ws_tls_accept(client->ws.fd); + + if (tls == NULL) { + goto cleanup; + } + hybbx_ws_connection_set_tls(&client->ws, tls); + } + + rc = hybbx_ws_server_handshake(&client->ws, g_config.path); + if (rc != HYBBX_OK) { + goto cleanup; + } + + if (!ws_client_acquire_slot(client)) { + ws_send_limit(client); + goto cleanup; + } + + rc = hybbx_session_open(g_service, &hybbx_plugin_websocket, client, + &client->hbx_session); + if (rc != HYBBX_OK || client->hbx_session == NULL) { + if (rc == HYBBX_ERR_BUSY) { + ws_send_busy(client); + } + goto cleanup; + } + + if (remote[0] != '\0') { + (void)hybbx_session_set_remote(client->hbx_session, remote); + } + + wctx.client = client; + client->last_rc = HYBBX_OK; + client->last_io_at = time(NULL); + + while (g_ws_running && client->ws.established) { + struct pollfd pfd; + int pr; + + pfd.fd = client->ws.fd; + pfd.events = POLLIN; + pfd.revents = 0; + + pr = poll(&pfd, 1, WS_CLIENT_POLL_MS); + if (pr < 0) { + if (errno == EINTR) { + continue; + } + break; + } + if (pr == 0) { + time_t now = time(NULL); + + if (now != (time_t)-1 && + (client->last_io_at == 0 || + now - client->last_io_at >= WS_CLIENT_PING_IDLE_SEC)) { + rc = hybbx_ws_ping(&client->ws); + if (rc != HYBBX_OK) { + break; + } + client->last_io_at = now; + } + + rc = hybbx_session_tick(client->hbx_session); + if (rc == HYBBX_SESSION_END) { + client->last_rc = HYBBX_SESSION_END; + break; + } + continue; + } + if ((pfd.revents & (POLLERR | POLLHUP | POLLNVAL)) != 0) { + break; + } + if ((pfd.revents & POLLIN) == 0) { + continue; + } + + rc = hybbx_ws_read_frames(&client->ws, ws_on_user_data, &wctx); + if (rc != HYBBX_OK) { + if (client->last_rc == HYBBX_SESSION_END) { + break; + } + break; + } + client->last_io_at = time(NULL); + + if (client->last_rc == HYBBX_SESSION_END) { + break; + } + + rc = hybbx_session_tick(client->hbx_session); + if (rc == HYBBX_SESSION_END) { + client->last_rc = HYBBX_SESSION_END; + break; + } + } + +cleanup: + if (client->hbx_session != NULL) { + hybbx_session_close(client->hbx_session); + client->hbx_session = NULL; + } + + ws_client_release_slot(client); + + (void)ws_tx_flush(client, 1); + + if (client->ws.established) { + (void)hybbx_ws_close(&client->ws); + } + + hybbx_ws_connection_cleanup(&client->ws); + + if (client->ws.fd >= 0) { + shutdown(client->ws.fd, SHUT_RDWR); + close(client->ws.fd); + client->ws.fd = -1; + } + + free(client); + return NULL; +} + +static void accept_client(int client_fd) +{ + ws_client_t *client; + pthread_t thread; + pthread_attr_t attr; + + client = calloc(1, sizeof(*client)); + if (client == NULL) { + close(client_fd); + return; + } + + hybbx_ws_connection_init(&client->ws, client_fd); + + pthread_attr_init(&attr); + pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); + + if (pthread_create(&thread, &attr, ws_client_thread, client) != 0) { + close(client_fd); + free(client); + } + + pthread_attr_destroy(&attr); +} + +static void *ws_accept_thread(void *arg) +{ + (void)arg; + + while (g_ws_running) { + struct pollfd fds[2]; + nfds_t nfds = 0; + int i; + int ready; + + memset(fds, 0, sizeof(fds)); + + if (g_listen_v4 >= 0) { + fds[nfds].fd = g_listen_v4; + fds[nfds].events = POLLIN; + nfds++; + } + + if (g_listen_v6 >= 0) { + fds[nfds].fd = g_listen_v6; + fds[nfds].events = POLLIN; + nfds++; + } + + if (nfds == 0) { + break; + } + + ready = poll(fds, nfds, 500); + if (ready < 0) { + if (errno == EINTR) { + continue; + } + break; + } + + if (ready == 0) { + continue; + } + + for (i = 0; i < (int)nfds; i++) { + if (fds[i].revents & POLLIN) { + int client_fd = accept(fds[i].fd, NULL, NULL); + + if (client_fd >= 0) { + if (!hybbx_security_ban_accept_fd(client_fd)) { + close(client_fd); + continue; + } + accept_client(client_fd); + } + } + } + } + + return NULL; +} + +static hybbx_result_t ws_plugin_init(hybbx_service_t *service) +{ + g_service = service; + return HYBBX_OK; +} + +static void ws_plugin_shutdown(void) +{ + ws_plugin_stop(); +} + +static hybbx_result_t ws_plugin_start(const char *config) +{ + hybbx_result_t rc; + + if (g_ws_running) { + return HYBBX_ERR_BUSY; + } + + rc = hybbx_websocket_config_parse(config, &g_config); + if (rc != HYBBX_OK) { + return rc; + } + + g_listen_v4 = -1; + g_listen_v6 = -1; + + if (g_config.ipv4) { + g_listen_v4 = create_listen_socket(AF_INET, g_config.bind_v4, + g_config.port); + if (g_listen_v4 < 0) { + log_bind_failure(g_config.bind_v4, g_config.port); + return HYBBX_ERR_IO; + } + } + + if (g_config.ipv6) { + g_listen_v6 = create_listen_socket(AF_INET6, g_config.bind_v6, + g_config.port); + if (g_listen_v6 < 0) { + hybbx_log_warn("[websocket] IPv6 bind [%s]:%u skipped (%s)", + g_config.bind_v6, g_config.port, strerror(errno)); + } + } + + if (g_listen_v4 < 0 && g_listen_v6 < 0) { + return HYBBX_ERR_IO; + } + + if (hybbx_ws_tls_compiled()) { + char cert_dir[HYBBX_PATH_MAX]; + hybbx_result_t tls_rc; + + if (hybbx_path_resolve(cert_dir, sizeof(cert_dir), + g_config.cert_dir) != HYBBX_OK) { + hybbx_strlcpy(cert_dir, g_config.cert_dir, sizeof(cert_dir)); + } + + tls_rc = hybbx_ws_tls_ensure_certs(cert_dir); + + if (tls_rc == HYBBX_OK) { + tls_rc = hybbx_ws_tls_server_start(cert_dir); + } + if (tls_rc != HYBBX_OK) { + hybbx_log_warn("[websocket] TLS init failed (%d), plain ws only", + (int)tls_rc); + } + } + + g_ws_running = 1; + g_active_clients = 0; + + if (pthread_create(&g_accept_thread, NULL, ws_accept_thread, NULL) != 0) { + g_ws_running = 0; + if (g_listen_v4 >= 0) { + close(g_listen_v4); + g_listen_v4 = -1; + } + if (g_listen_v6 >= 0) { + close(g_listen_v6); + g_listen_v6 = -1; + } + return HYBBX_ERR_IO; + } + + { + char msg[384]; + size_t pos = 0; + + pos += (size_t)snprintf(msg + pos, sizeof(msg) - pos, "[websocket] listening"); + if (g_listen_v4 >= 0) { + pos += (size_t)snprintf(msg + pos, sizeof(msg) - pos, " IPv4 %s:%u", + g_config.bind_v4, g_config.port); + } + if (g_listen_v6 >= 0) { + pos += (size_t)snprintf(msg + pos, sizeof(msg) - pos, " IPv6 [%s]:%u", + g_config.bind_v6, g_config.port); + } + pos += (size_t)snprintf(msg + pos, sizeof(msg) - pos, + " path=%s max_connections=%u (%s, forward-proxy)", + g_config.path, g_config.max_connections, + hybbx_ws_tls_server_enabled() ? "wss" : "ws"); + hybbx_log_info("%s", msg); + } + + return HYBBX_OK; +} + +static hybbx_result_t ws_plugin_stop(void) +{ + if (!g_ws_running) { + return HYBBX_OK; + } + + g_ws_running = 0; + + if (g_listen_v4 >= 0) { + shutdown(g_listen_v4, SHUT_RDWR); + close(g_listen_v4); + g_listen_v4 = -1; + } + + if (g_listen_v6 >= 0) { + shutdown(g_listen_v6, SHUT_RDWR); + close(g_listen_v6); + g_listen_v6 = -1; + } + + pthread_join(g_accept_thread, NULL); + hybbx_ws_tls_server_stop(); + hybbx_log_info("[websocket] stop"); + return HYBBX_OK; +} + +const hybbx_transport_plugin_t hybbx_plugin_websocket = { + .name = "websocket", + .kind = HYBBX_TRANSPORT_WEBSOCKET, + .version = 1, + .init = ws_plugin_init, + .shutdown = ws_plugin_shutdown, + .start = ws_plugin_start, + .stop = ws_plugin_stop, + .write = ws_plugin_write, +}; diff --git a/plugins/websocket/ws_config.c b/plugins/websocket/ws_config.c new file mode 100644 index 0000000..baf88b2 --- /dev/null +++ b/plugins/websocket/ws_config.c @@ -0,0 +1,163 @@ +#include "hybbx/websocket.h" +#include "hybbx/util.h" + +#include <stdlib.h> +#include <string.h> + +static unsigned int parse_port(const char *value) +{ + char *end; + unsigned long port; + + if (value == NULL || value[0] == '\0') { + return HYBBX_WEBSOCKET_DEFAULT_PORT; + } + + port = strtoul(value, &end, 10); + if (end == value || *end != '\0' || port == 0 || port > 65535u) { + return HYBBX_WEBSOCKET_DEFAULT_PORT; + } + + return (unsigned int)port; +} + +static unsigned int parse_max_connections(const char *value) +{ + char *end; + unsigned long n; + + if (value == NULL || value[0] == '\0') { + return HYBBX_WEBSOCKET_DEFAULT_MAX_CONNECTIONS; + } + + n = strtoul(value, &end, 10); + if (end == value || *end != '\0' || n == 0 || n > 65535u) { + return HYBBX_WEBSOCKET_DEFAULT_MAX_CONNECTIONS; + } + + return (unsigned int)n; +} + +static const char *find_kv(const char *config, const char *key, + char *scratch, size_t scratch_len) +{ + const char *cursor = config; + size_t key_len = strlen(key); + + if (config == NULL || key == NULL) { + return NULL; + } + + while (*cursor != '\0') { + const char *sep = strchr(cursor, ';'); + const char *end = sep != NULL ? sep : cursor + strlen(cursor); + const char *eq = strchr(cursor, '='); + + if (eq != NULL && eq < end && (size_t)(eq - cursor) == key_len && + strncmp(cursor, key, key_len) == 0) { + const char *value = eq + 1; + size_t value_len = (size_t)(end - value); + + if (scratch != NULL && scratch_len > 0) { + if (value_len >= scratch_len) { + value_len = scratch_len - 1; + } + memcpy(scratch, value, value_len); + scratch[value_len] = '\0'; + return scratch; + } + + return value; + } + + if (sep == NULL) { + break; + } + cursor = sep + 1; + } + + return NULL; +} + +void hybbx_websocket_config_defaults(hybbx_websocket_config_t *config) +{ + if (config == NULL) { + return; + } + + memset(config, 0, sizeof(*config)); + hybbx_strlcpy(config->bind_v4, HYBBX_WEBSOCKET_DEFAULT_BIND_V4, + sizeof(config->bind_v4)); + hybbx_strlcpy(config->bind_v6, HYBBX_WEBSOCKET_DEFAULT_BIND_V6, + sizeof(config->bind_v6)); + hybbx_strlcpy(config->path, HYBBX_WEBSOCKET_DEFAULT_PATH, + sizeof(config->path)); + hybbx_strlcpy(config->cert_dir, HYBBX_WEBSOCKET_DEFAULT_CERT_DIR, + sizeof(config->cert_dir)); + config->port = HYBBX_WEBSOCKET_DEFAULT_PORT; + config->max_connections = HYBBX_WEBSOCKET_DEFAULT_MAX_CONNECTIONS; + config->ipv4 = 1; + config->ipv6 = 1; +} + +hybbx_result_t hybbx_websocket_config_parse(const char *config, + hybbx_websocket_config_t *out) +{ + char scratch[HYBBX_WEBSOCKET_PATH_MAX]; + const char *value; + + if (out == NULL) { + return HYBBX_ERR_INVALID; + } + + hybbx_websocket_config_defaults(out); + + if (config == NULL || config[0] == '\0') { + return HYBBX_OK; + } + + value = find_kv(config, "port", scratch, sizeof(scratch)); + out->port = parse_port(value); + + value = find_kv(config, "max_connections", scratch, sizeof(scratch)); + if (value == NULL) { + value = find_kv(config, "max_online", scratch, sizeof(scratch)); + } + out->max_connections = parse_max_connections(value); + + value = find_kv(config, "bind", scratch, sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + if (strchr(value, ':') != NULL) { + hybbx_strlcpy(out->bind_v6, value, sizeof(out->bind_v6)); + } else { + hybbx_strlcpy(out->bind_v4, value, sizeof(out->bind_v4)); + } + } + + value = find_kv(config, "bind6", scratch, sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + hybbx_strlcpy(out->bind_v6, value, sizeof(out->bind_v6)); + } + + value = find_kv(config, "path", scratch, sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + hybbx_strlcpy(out->path, value, sizeof(out->path)); + } + + value = find_kv(config, "cert_dir", scratch, sizeof(scratch)); + if (value != NULL && value[0] != '\0') { + hybbx_strlcpy(out->cert_dir, value, sizeof(out->cert_dir)); + } + + value = find_kv(config, "ipv4", scratch, sizeof(scratch)); + if (value != NULL) { + out->ipv4 = hybbx_parse_bool(value, 1); + } + + value = find_kv(config, "ipv6", scratch, sizeof(scratch)); + if (value != NULL) { + out->ipv6 = hybbx_parse_bool(value, 1); + } + + return HYBBX_OK; +} diff --git a/plugins/websocket/ws_proto.c b/plugins/websocket/ws_proto.c new file mode 100644 index 0000000..b0e63b5 --- /dev/null +++ b/plugins/websocket/ws_proto.c @@ -0,0 +1,565 @@ +#include "ws_proto.h" +#include "ws_sha1.h" +#include "ws_tls.h" + +#include <errno.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <strings.h> +#include <sys/socket.h> +#include <unistd.h> + +#define WS_GUID "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" + +static int header_has_token(const char *value, const char *token) +{ + const char *p; + + if (value == NULL || token == NULL) { + return 0; + } + + for (p = value; *p != '\0'; p++) { + if (strncasecmp(p, token, strlen(token)) == 0) { + return 1; + } + } + + return 0; +} + +static int header_value_equals(const char *value, const char *expected) +{ + size_t expected_len; + + if (value == NULL || expected == NULL) { + return 0; + } + + expected_len = strlen(expected); + if (strncasecmp(value, expected, expected_len) != 0) { + return 0; + } + + { + char c = value[expected_len]; + + return c == '\0' || c == '\r' || c == ' ' || c == '\t' || c == ','; + } +} + +static ssize_t recv_some(int fd, void *buf, size_t len) +{ + ssize_t n; + + n = recv(fd, buf, len, 0); + if (n < 0 && errno == EINTR) { + return 0; + } + return n; +} + +static ssize_t ws_recv_some(hybbx_ws_connection_t *ws, void *buf, size_t len) +{ + if (ws == NULL) { + return -1; + } + + if (ws->tls != NULL) { + return hybbx_ws_tls_recv(ws->tls, buf, len); + } + + return recv_some(ws->fd, buf, len); +} + +static ssize_t send_all(int fd, const void *buf, size_t len) +{ + const uint8_t *p = (const uint8_t *)buf; + size_t sent = 0; + + while (sent < len) { + ssize_t n = send(fd, p + sent, len - sent, MSG_NOSIGNAL); + + if (n < 0) { + if (errno == EINTR) { + continue; + } + return -1; + } + if (n == 0) { + return -1; + } + sent += (size_t)n; + } + + return (ssize_t)sent; +} + +static ssize_t ws_send_all(hybbx_ws_connection_t *ws, const void *buf, + size_t len) +{ + if (ws == NULL) { + return -1; + } + + if (ws->tls != NULL) { + return hybbx_ws_tls_send(ws->tls, buf, len); + } + + return send_all(ws->fd, buf, len); +} + +static int base64_encode(const uint8_t *in, size_t in_len, char *out, + size_t out_len) +{ + static const char tbl[] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + size_t i; + size_t o = 0; + + if (out_len < ((in_len + 2) / 3) * 4 + 1) { + return -1; + } + + for (i = 0; i < in_len; i += 3) { + uint32_t v = (uint32_t)in[i] << 16; + + if (i + 1 < in_len) { + v |= (uint32_t)in[i + 1] << 8; + } + if (i + 2 < in_len) { + v |= (uint32_t)in[i + 2]; + } + + out[o++] = tbl[(v >> 18) & 63u]; + out[o++] = tbl[(v >> 12) & 63u]; + out[o++] = (i + 1 < in_len) ? tbl[(v >> 6) & 63u] : '='; + out[o++] = (i + 2 < in_len) ? tbl[v & 63u] : '='; + } + + out[o] = '\0'; + return 0; +} + +static const char *header_value(const char *headers, const char *name) +{ + size_t name_len = strlen(name); + const char *p = headers; + + while (p != NULL && *p != '\0') { + const char *line_end = strstr(p, "\r\n"); + + if (line_end == NULL) { + break; + } + + if ((size_t)(line_end - p) > name_len + 2 && + strncasecmp(p, name, name_len) == 0 && p[name_len] == ':') { + const char *value = p + name_len + 1; + + while (*value == ' ' || *value == '\t') { + value++; + } + return value; + } + + p = line_end + 2; + } + + return NULL; +} + +static int path_matches(const char *request_line, const char *path) +{ + const char *sp1; + const char *sp2; + size_t path_len; + + if (path == NULL || path[0] == '\0') { + return 1; + } + + sp1 = strchr(request_line, ' '); + if (sp1 == NULL) { + return 0; + } + sp1++; + sp2 = strchr(sp1, ' '); + if (sp2 == NULL) { + return 0; + } + + path_len = strlen(path); + if ((size_t)(sp2 - sp1) < path_len) { + return 0; + } + if (strncmp(sp1, path, path_len) != 0) { + return 0; + } + if (sp1[path_len] != ' ' && sp1[path_len] != '?' && + sp1[path_len] != '\0') { + return 0; + } + + return 1; +} + +void hybbx_ws_connection_init(hybbx_ws_connection_t *ws, int fd) +{ + if (ws == NULL) { + return; + } + + memset(ws, 0, sizeof(*ws)); + ws->fd = fd; +} + +void hybbx_ws_connection_set_tls(hybbx_ws_connection_t *ws, void *tls) +{ + if (ws == NULL) { + return; + } + + ws->tls = tls; +} + +void hybbx_ws_connection_cleanup(hybbx_ws_connection_t *ws) +{ + if (ws == NULL) { + return; + } + + if (ws->tls != NULL) { + hybbx_ws_tls_shutdown(ws->tls); + hybbx_ws_tls_free(ws->tls); + ws->tls = NULL; + } +} + +hybbx_result_t hybbx_ws_server_handshake(hybbx_ws_connection_t *ws, + const char *path) +{ + char buf[HYBBX_WS_HANDSHAKE_MAX]; + size_t total = 0; + const char *key_hdr; + char key[128]; + char accept_src[256]; + uint8_t digest[20]; + char accept_b64[64]; + char response[512]; + const char *upgrade; + const char *connection; + + if (ws == NULL || ws->fd < 0) { + return HYBBX_ERR_INVALID; + } + + while (total + 1 < sizeof(buf)) { + ssize_t n = ws_recv_some(ws, buf + total, sizeof(buf) - 1 - total); + + if (n < 0) { + return HYBBX_ERR_IO; + } + if (n == 0) { + return HYBBX_ERR_IO; + } + + total += (size_t)n; + buf[total] = '\0'; + + if (strstr(buf, "\r\n\r\n") != NULL) { + break; + } + } + + if (strncmp(buf, "GET ", 4) != 0) { + return HYBBX_ERR_DENIED; + } + + { + const char *eol = strstr(buf, "\r\n"); + char first_line[256]; + size_t line_len; + + if (eol == NULL) { + return HYBBX_ERR_DENIED; + } + line_len = (size_t)(eol - buf); + if (line_len >= sizeof(first_line)) { + return HYBBX_ERR_DENIED; + } + memcpy(first_line, buf, line_len); + first_line[line_len] = '\0'; + if (!path_matches(first_line, path)) { + return HYBBX_ERR_DENIED; + } + } + + upgrade = header_value(buf, "Upgrade"); + connection = header_value(buf, "Connection"); + if (upgrade == NULL || connection == NULL || + !header_value_equals(upgrade, "websocket") || + !header_has_token(connection, "upgrade")) { + return HYBBX_ERR_DENIED; + } + + key_hdr = header_value(buf, "Sec-WebSocket-Key"); + if (key_hdr == NULL) { + return HYBBX_ERR_DENIED; + } + + { + const char *key_end = strstr(key_hdr, "\r\n"); + size_t key_len; + + if (key_end == NULL) { + return HYBBX_ERR_DENIED; + } + key_len = (size_t)(key_end - key_hdr); + while (key_len > 0 && + (key_hdr[key_len - 1] == ' ' || key_hdr[key_len - 1] == '\t')) { + key_len--; + } + if (key_len >= sizeof(key)) { + return HYBBX_ERR_DENIED; + } + memcpy(key, key_hdr, key_len); + key[key_len] = '\0'; + } + + snprintf(accept_src, sizeof(accept_src), "%s%s", key, WS_GUID); + hybbx_ws_sha1((const uint8_t *)accept_src, strlen(accept_src), digest); + if (base64_encode(digest, sizeof(digest), accept_b64, + sizeof(accept_b64)) != 0) { + return HYBBX_ERR_IO; + } + + snprintf(response, sizeof(response), + "HTTP/1.1 101 Switching Protocols\r\n" + "Upgrade: websocket\r\n" + "Connection: Upgrade\r\n" + "Sec-WebSocket-Accept: %s\r\n" + "\r\n", + accept_b64); + + if (ws_send_all(ws, response, strlen(response)) < 0) { + return HYBBX_ERR_IO; + } + + ws->established = 1; + ws->rx_len = 0; + return HYBBX_OK; +} + +static hybbx_result_t ws_handle_frame(hybbx_ws_connection_t *ws, + const uint8_t *frame, size_t frame_len, + hybbx_ws_data_cb on_data, void *ctx) +{ + uint8_t opcode; + uint64_t payload_len; + size_t header_len; + uint8_t mask[4]; + uint8_t payload[HYBBX_WS_FRAME_PAYLOAD_MAX]; + size_t i; + + if (frame_len < 2) { + return HYBBX_ERR_IO; + } + + opcode = frame[0] & 0x0Fu; + if ((frame[0] & 0x80) == 0) { + return HYBBX_ERR_UNSUPPORTED; + } + + if ((frame[1] & 0x80) == 0) { + return HYBBX_ERR_IO; + } + + payload_len = frame[1] & 0x7Fu; + header_len = 2; + + if (payload_len == 126) { + if (frame_len < 4) { + return HYBBX_ERR_IO; + } + payload_len = ((uint64_t)frame[2] << 8) | frame[3]; + header_len = 4; + } else if (payload_len == 127) { + return HYBBX_ERR_UNSUPPORTED; + } + + if (payload_len > HYBBX_WS_FRAME_PAYLOAD_MAX) { + return HYBBX_ERR_UNSUPPORTED; + } + + if (frame_len < header_len + 4 + payload_len) { + return HYBBX_ERR_IO; + } + + memcpy(mask, frame + header_len, 4); + header_len += 4; + + for (i = 0; i < payload_len; i++) { + payload[i] = frame[header_len + i] ^ mask[i % 4]; + } + + switch (opcode) { + case 0x1: + case 0x2: + if (on_data != NULL && payload_len > 0) { + on_data(ctx, payload, (size_t)payload_len); + } + return HYBBX_OK; + case 0x8: + return HYBBX_ERR_IO; + case 0x9: + { + uint8_t pong[2]; + + pong[0] = 0x8A; + pong[1] = 0x00; + (void)ws_send_all(ws, pong, 2); + } + return HYBBX_OK; + case 0xA: + return HYBBX_OK; + default: + return HYBBX_OK; + } +} + +hybbx_result_t hybbx_ws_read_frames(hybbx_ws_connection_t *ws, + hybbx_ws_data_cb on_data, + void *ctx) +{ + uint8_t tmp[256]; + ssize_t n; + + if (ws == NULL || !ws->established) { + return HYBBX_ERR_INVALID; + } + + n = ws_recv_some(ws, tmp, sizeof(tmp)); + if (n < 0) { + return HYBBX_ERR_IO; + } + if (n == 0) { + return HYBBX_ERR_IO; + } + + if (ws->rx_len + (size_t)n > sizeof(ws->rx_buf)) { + ws->rx_len = 0; + return HYBBX_ERR_IO; + } + + memcpy(ws->rx_buf + ws->rx_len, tmp, (size_t)n); + ws->rx_len += (size_t)n; + + while (ws->rx_len >= 2) { + uint64_t payload_len = ws->rx_buf[1] & 0x7Fu; + size_t header_len = 2; + size_t frame_len; + hybbx_result_t rc; + + if (payload_len == 126) { + if (ws->rx_len < 4) { + return HYBBX_OK; + } + payload_len = ((uint64_t)ws->rx_buf[2] << 8) | ws->rx_buf[3]; + header_len = 4; + } else if (payload_len == 127) { + return HYBBX_ERR_UNSUPPORTED; + } + + if (payload_len > HYBBX_WS_FRAME_PAYLOAD_MAX) { + return HYBBX_ERR_UNSUPPORTED; + } + + frame_len = header_len + 4 + (size_t)payload_len; + if (ws->rx_len < frame_len) { + return HYBBX_OK; + } + + rc = ws_handle_frame(ws, ws->rx_buf, frame_len, on_data, ctx); + if (rc != HYBBX_OK) { + return rc; + } + + if (ws->rx_len > frame_len) { + memmove(ws->rx_buf, ws->rx_buf + frame_len, + ws->rx_len - frame_len); + } + ws->rx_len -= frame_len; + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_ws_write_text(hybbx_ws_connection_t *ws, + const char *data, size_t len) +{ + uint8_t hdr[10]; + size_t hdr_len = 2; + + if (ws == NULL || data == NULL || !ws->established) { + return HYBBX_ERR_INVALID; + } + + if (len > HYBBX_WS_FRAME_PAYLOAD_MAX) { + return HYBBX_ERR_INVALID; + } + + hdr[0] = 0x81; + if (len < 126) { + hdr[1] = (uint8_t)len; + hdr_len = 2; + } else { + hdr[1] = 126; + hdr[2] = (uint8_t)((len >> 8) & 0xFFu); + hdr[3] = (uint8_t)(len & 0xFFu); + hdr_len = 4; + } + + if (ws_send_all(ws, hdr, hdr_len) < 0) { + return HYBBX_ERR_IO; + } + if (len > 0 && ws_send_all(ws, data, len) < 0) { + return HYBBX_ERR_IO; + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_ws_ping(hybbx_ws_connection_t *ws) +{ + uint8_t frame[2]; + + if (ws == NULL || !ws->established) { + return HYBBX_ERR_INVALID; + } + + frame[0] = 0x89; + frame[1] = 0x00; + + if (ws_send_all(ws, frame, sizeof(frame)) < 0) { + return HYBBX_ERR_IO; + } + + return HYBBX_OK; +} + +hybbx_result_t hybbx_ws_close(hybbx_ws_connection_t *ws) +{ + uint8_t frame[2]; + + if (ws == NULL || ws->fd < 0) { + return HYBBX_ERR_INVALID; + } + + frame[0] = 0x88; + frame[1] = 0x00; + (void)ws_send_all(ws, frame, 2); + ws->established = 0; + return HYBBX_OK; +} diff --git a/plugins/websocket/ws_proto.h b/plugins/websocket/ws_proto.h new file mode 100644 index 0000000..5a1cb23 --- /dev/null +++ b/plugins/websocket/ws_proto.h @@ -0,0 +1,63 @@ +#ifndef HYBBX_WS_PROTO_H +#define HYBBX_WS_PROTO_H + +#include "hybbx/types.h" + +#include <stddef.h> +#include <stdint.h> + +#ifdef __cplusplus +extern "C" { +#endif + +#define HYBBX_WS_HANDSHAKE_MAX 4096u +#define HYBBX_WS_FRAME_PAYLOAD_MAX 4096u + +typedef void (*hybbx_ws_data_cb)(void *ctx, const uint8_t *data, size_t len); + +typedef struct hybbx_ws_connection { + int fd; + void *tls; + int established; + uint8_t rx_buf[HYBBX_WS_FRAME_PAYLOAD_MAX + 16]; + size_t rx_len; +} hybbx_ws_connection_t; + +void hybbx_ws_connection_init(hybbx_ws_connection_t *ws, int fd); + +/** Attach TLS session (opaque SSL*) after accept; plain ws when NULL. */ +void hybbx_ws_connection_set_tls(hybbx_ws_connection_t *ws, void *tls); + +/** Release TLS resources; does not close @c fd. */ +void hybbx_ws_connection_cleanup(hybbx_ws_connection_t *ws); + +/** + * Perform server-side HTTP Upgrade handshake. + * @p path must match the request URI path (e.g. /hybbx) when non-empty. + */ +hybbx_result_t hybbx_ws_server_handshake(hybbx_ws_connection_t *ws, + const char *path); + +/** + * Read WebSocket frames; invoke @p on_data for each complete text/binary + * payload from the client. Returns HYBBX_ERR_IO on disconnect. + */ +hybbx_result_t hybbx_ws_read_frames(hybbx_ws_connection_t *ws, + hybbx_ws_data_cb on_data, + void *ctx); + +/** Send one server text frame (unmasked). */ +hybbx_result_t hybbx_ws_write_text(hybbx_ws_connection_t *ws, + const char *data, size_t len); + +/** Send one server ping frame (unmasked, empty payload). */ +hybbx_result_t hybbx_ws_ping(hybbx_ws_connection_t *ws); + +/** Send WebSocket close frame. */ +hybbx_result_t hybbx_ws_close(hybbx_ws_connection_t *ws); + +#ifdef __cplusplus +} +#endif + +#endif /* HYBBX_WS_PROTO_H */ diff --git a/plugins/websocket/ws_sha1.c b/plugins/websocket/ws_sha1.c new file mode 100644 index 0000000..53ad4c0 --- /dev/null +++ b/plugins/websocket/ws_sha1.c @@ -0,0 +1,138 @@ +/* + * Minimal SHA-1 for WebSocket handshake (RFC 6455). Public-domain style. + */ +#include "ws_sha1.h" + +#include <stdint.h> +#include <string.h> +#include <string.h> + +typedef struct ws_sha1_ctx { + uint32_t state[5]; + uint64_t count; + uint8_t buffer[64]; +} ws_sha1_ctx_t; + +static uint32_t rol(uint32_t v, unsigned bits) +{ + return (v << bits) | (v >> (32u - bits)); +} + +static void ws_sha1_transform(ws_sha1_ctx_t *ctx, const uint8_t block[64]) +{ + uint32_t w[80]; + uint32_t a, b, c, d, e, f, k, t; + unsigned i; + + for (i = 0; i < 16; i++) { + w[i] = ((uint32_t)block[i * 4] << 24) | + ((uint32_t)block[i * 4 + 1] << 16) | + ((uint32_t)block[i * 4 + 2] << 8) | + (uint32_t)block[i * 4 + 3]; + } + for (i = 16; i < 80; i++) { + w[i] = rol(w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16], 1); + } + + a = ctx->state[0]; + b = ctx->state[1]; + c = ctx->state[2]; + d = ctx->state[3]; + e = ctx->state[4]; + + for (i = 0; i < 80; i++) { + if (i < 20) { + f = (b & c) | ((~b) & d); + k = 0x5A827999u; + } else if (i < 40) { + f = b ^ c ^ d; + k = 0x6ED9EBA1u; + } else if (i < 60) { + f = (b & c) | (b & d) | (c & d); + k = 0x8F1BBCDCu; + } else { + f = b ^ c ^ d; + k = 0xCA62C1D6u; + } + t = rol(a, 5) + f + e + k + w[i]; + e = d; + d = c; + c = rol(b, 30); + b = a; + a = t; + } + + ctx->state[0] += a; + ctx->state[1] += b; + ctx->state[2] += c; + ctx->state[3] += d; + ctx->state[4] += e; +} + +static void ws_sha1_init(ws_sha1_ctx_t *ctx) +{ + ctx->state[0] = 0x67452301u; + ctx->state[1] = 0xEFCDAB89u; + ctx->state[2] = 0x98BADCFEu; + ctx->state[3] = 0x10325476u; + ctx->state[4] = 0xC3D2E1F0u; + ctx->count = 0; +} + +static void ws_sha1_update(ws_sha1_ctx_t *ctx, const uint8_t *data, size_t len) +{ + size_t i = 0; + size_t idx = (size_t)((ctx->count >> 3) & 63u); + + ctx->count += (uint64_t)len * 8u; + + if (idx + len > 63) { + memcpy(ctx->buffer + idx, data, 64 - idx); + ws_sha1_transform(ctx, ctx->buffer); + for (i = 64 - idx; i + 63 < len; i += 64) { + ws_sha1_transform(ctx, data + i); + } + idx = 0; + } else { + i = 0; + } + + memcpy(ctx->buffer + idx, data + i, len - i); +} + +static void ws_sha1_final(ws_sha1_ctx_t *ctx, uint8_t out[20]) +{ + uint8_t final[64]; + size_t idx = (size_t)((ctx->count >> 3) & 63u); + unsigned i; + + memcpy(final, ctx->buffer, idx); + final[idx++] = 0x80; + if (idx > 56) { + memset(final + idx, 0, 64 - idx); + ws_sha1_transform(ctx, final); + idx = 0; + } + memset(final + idx, 0, 56 - idx); + + for (i = 0; i < 8; i++) { + final[56 + i] = (uint8_t)(ctx->count >> (56 - i * 8)); + } + ws_sha1_transform(ctx, final); + + for (i = 0; i < 5; i++) { + out[i * 4] = (uint8_t)(ctx->state[i] >> 24); + out[i * 4 + 1] = (uint8_t)(ctx->state[i] >> 16); + out[i * 4 + 2] = (uint8_t)(ctx->state[i] >> 8); + out[i * 4 + 3] = (uint8_t)(ctx->state[i]); + } +} + +void hybbx_ws_sha1(const uint8_t *data, size_t len, uint8_t out[20]) +{ + ws_sha1_ctx_t ctx; + + ws_sha1_init(&ctx); + ws_sha1_update(&ctx, data, len); + ws_sha1_final(&ctx, out); +} diff --git a/plugins/websocket/ws_sha1.h b/plugins/websocket/ws_sha1.h new file mode 100644 index 0000000..b685cf7 --- /dev/null +++ b/plugins/websocket/ws_sha1.h @@ -0,0 +1,9 @@ +#ifndef HYBBX_WS_SHA1_H +#define HYBBX_WS_SHA1_H + +#include <stddef.h> +#include <stdint.h> + +void hybbx_ws_sha1(const uint8_t *data, size_t len, uint8_t out[20]); + +#endif /* HYBBX_WS_SHA1_H */ diff --git a/plugins/websocket/ws_tls.c b/plugins/websocket/ws_tls.c new file mode 100644 index 0000000..679ce59 --- /dev/null +++ b/plugins/websocket/ws_tls.c @@ -0,0 +1,400 @@ +#include "ws_tls.h" + +#include "hybbx/limits.h" +#include "hybbx/util.h" +#include "hybbx/websocket.h" +#include "hybbx/log.h" + +#include <errno.h> +#include <stdio.h> +#include <string.h> +#include <sys/stat.h> +#include <unistd.h> + +#ifdef HYBBX_WS_HAVE_TLS + +#include <openssl/err.h> +#include <openssl/evp.h> +#include <openssl/pem.h> +#include <openssl/ssl.h> +#include <openssl/x509.h> + +static SSL_CTX *g_ssl_ctx; +static int g_tls_enabled; + +static hybbx_result_t mkdir_cert_dir(const char *cert_dir) +{ + char parent[HYBBX_PATH_MAX]; + struct stat st; + + if (cert_dir == NULL || cert_dir[0] == '\0') { + return HYBBX_ERR_INVALID; + } + + if (stat(cert_dir, &st) == 0) { + return S_ISDIR(st.st_mode) ? HYBBX_OK : HYBBX_ERR_IO; + } + + if (hybbx_path_dirname(cert_dir, parent, sizeof(parent)) == HYBBX_OK && + parent[0] != '\0' && strcmp(parent, cert_dir) != 0 && + stat(parent, &st) != 0) { + if (mkdir_cert_dir(parent) != HYBBX_OK) { + return HYBBX_ERR_IO; + } + } + + if (mkdir(cert_dir, 0700) != 0) { + return HYBBX_ERR_IO; + } + + return HYBBX_OK; +} + +static void build_cert_path(const char *cert_dir, const char *name, + char *out, size_t out_len) +{ + if (cert_dir == NULL || name == NULL || out == NULL || out_len == 0) { + if (out != NULL && out_len > 0) { + out[0] = '\0'; + } + return; + } + + snprintf(out, out_len, "%s/%s", cert_dir, name); +} + +static int cert_files_exist(const char *cert_path, const char *key_path) +{ + struct stat st; + + return stat(cert_path, &st) == 0 && S_ISREG(st.st_mode) && + stat(key_path, &st) == 0 && S_ISREG(st.st_mode); +} + +static hybbx_result_t generate_self_signed(const char *cert_path, + const char *key_path) +{ + EVP_PKEY *pkey = NULL; + EVP_PKEY_CTX *pctx = NULL; + X509 *cert = NULL; + X509_NAME *name; + FILE *fp; + int rc = HYBBX_ERR_IO; + + pctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, NULL); + if (pctx == NULL || + EVP_PKEY_keygen_init(pctx) <= 0 || + EVP_PKEY_CTX_set_rsa_keygen_bits(pctx, 2048) <= 0 || + EVP_PKEY_keygen(pctx, &pkey) <= 0 || pkey == NULL) { + goto done; + } + + cert = X509_new(); + if (cert == NULL) { + goto done; + } + + if (X509_set_version(cert, 2) != 1 || + ASN1_INTEGER_set(X509_get_serialNumber(cert), 1) != 1 || + X509_gmtime_adj(X509_get_notBefore(cert), 0) == NULL || + X509_gmtime_adj(X509_get_notAfter(cert), + (long)HYBBX_WS_TLS_CERT_VALID_DAYS * 24L * 3600L) == + NULL || + X509_set_pubkey(cert, pkey) != 1) { + goto done; + } + + name = X509_get_subject_name(cert); + if (name == NULL || + X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC, + (const unsigned char *)"hybbx", -1, -1, + 0) != 1 || + X509_set_issuer_name(cert, name) != 1 || + X509_sign(cert, pkey, EVP_sha256()) <= 0) { + goto done; + } + + fp = fopen(key_path, "w"); + if (fp == NULL || + PEM_write_PrivateKey(fp, pkey, NULL, NULL, 0, NULL, NULL) != 1) { + if (fp != NULL) { + fclose(fp); + unlink(key_path); + } + goto done; + } + fclose(fp); + (void)chmod(key_path, 0600); + + fp = fopen(cert_path, "w"); + if (fp == NULL || PEM_write_X509(fp, cert) != 1) { + if (fp != NULL) { + fclose(fp); + unlink(cert_path); + } + goto done; + } + fclose(fp); + (void)chmod(cert_path, 0644); + + rc = HYBBX_OK; + +done: + if (cert != NULL) { + X509_free(cert); + } + if (pkey != NULL) { + EVP_PKEY_free(pkey); + } + if (pctx != NULL) { + EVP_PKEY_CTX_free(pctx); + } + + return rc; +} + +int hybbx_ws_tls_compiled(void) +{ + return 1; +} + +hybbx_result_t hybbx_ws_tls_ensure_certs(const char *cert_dir) +{ + char cert_path[HYBBX_PATH_MAX]; + char key_path[HYBBX_PATH_MAX]; + hybbx_result_t rc; + + if (cert_dir == NULL || cert_dir[0] == '\0') { + return HYBBX_ERR_INVALID; + } + + rc = mkdir_cert_dir(cert_dir); + if (rc != HYBBX_OK) { + return rc; + } + + build_cert_path(cert_dir, HYBBX_WS_TLS_CERT_FILENAME, cert_path, + sizeof(cert_path)); + build_cert_path(cert_dir, HYBBX_WS_TLS_KEY_FILENAME, key_path, + sizeof(key_path)); + + if (cert_files_exist(cert_path, key_path)) { + return HYBBX_OK; + } + + rc = generate_self_signed(cert_path, key_path); + if (rc == HYBBX_OK) { + hybbx_log_info("[websocket] created self-signed TLS certificate in %s", + cert_dir); + } + + return rc; +} + +hybbx_result_t hybbx_ws_tls_server_start(const char *cert_dir) +{ + char cert_path[HYBBX_PATH_MAX]; + char key_path[HYBBX_PATH_MAX]; + + if (g_tls_enabled) { + return HYBBX_OK; + } + + if (cert_dir == NULL || cert_dir[0] == '\0') { + return HYBBX_ERR_INVALID; + } + + build_cert_path(cert_dir, HYBBX_WS_TLS_CERT_FILENAME, cert_path, + sizeof(cert_path)); + build_cert_path(cert_dir, HYBBX_WS_TLS_KEY_FILENAME, key_path, + sizeof(key_path)); + + if (!cert_files_exist(cert_path, key_path)) { + return HYBBX_ERR_IO; + } + + if (OPENSSL_init_ssl(OPENSSL_INIT_LOAD_SSL_STRINGS | + OPENSSL_INIT_LOAD_CRYPTO_STRINGS, + NULL) != 1) { + return HYBBX_ERR_IO; + } + + g_ssl_ctx = SSL_CTX_new(TLS_server_method()); + if (g_ssl_ctx == NULL) { + return HYBBX_ERR_IO; + } + + SSL_CTX_set_min_proto_version(g_ssl_ctx, TLS1_2_VERSION); + + if (SSL_CTX_use_certificate_file(g_ssl_ctx, cert_path, SSL_FILETYPE_PEM) != + 1 || + SSL_CTX_use_PrivateKey_file(g_ssl_ctx, key_path, SSL_FILETYPE_PEM) != + 1 || + SSL_CTX_check_private_key(g_ssl_ctx) != 1) { + SSL_CTX_free(g_ssl_ctx); + g_ssl_ctx = NULL; + return HYBBX_ERR_IO; + } + + g_tls_enabled = 1; + return HYBBX_OK; +} + +void hybbx_ws_tls_server_stop(void) +{ + if (g_ssl_ctx != NULL) { + SSL_CTX_free(g_ssl_ctx); + g_ssl_ctx = NULL; + } + g_tls_enabled = 0; +} + +int hybbx_ws_tls_server_enabled(void) +{ + return g_tls_enabled; +} + +void *hybbx_ws_tls_accept(int fd) +{ + SSL *ssl; + + if (!g_tls_enabled || g_ssl_ctx == NULL || fd < 0) { + return NULL; + } + + ssl = SSL_new(g_ssl_ctx); + if (ssl == NULL) { + return NULL; + } + + if (SSL_set_fd(ssl, fd) != 1 || SSL_accept(ssl) <= 0) { + SSL_free(ssl); + return NULL; + } + + return ssl; +} + +ssize_t hybbx_ws_tls_recv(void *tls, void *buf, size_t len) +{ + SSL *ssl = (SSL *)tls; + int n; + + if (ssl == NULL || buf == NULL || len == 0) { + return -1; + } + + do { + n = SSL_read(ssl, buf, (int)len); + } while (n < 0 && SSL_get_error(ssl, n) == SSL_ERROR_SYSCALL && + errno == EINTR); + + return (ssize_t)n; +} + +ssize_t hybbx_ws_tls_send(void *tls, const void *buf, size_t len) +{ + const uint8_t *p = (const uint8_t *)buf; + SSL *ssl = (SSL *)tls; + size_t sent = 0; + + if (ssl == NULL || buf == NULL) { + return -1; + } + + while (sent < len) { + int n; + + do { + n = SSL_write(ssl, p + sent, (int)(len - sent)); + } while (n < 0 && SSL_get_error(ssl, n) == SSL_ERROR_SYSCALL && + errno == EINTR); + + if (n <= 0) { + return -1; + } + sent += (size_t)n; + } + + return (ssize_t)sent; +} + +void hybbx_ws_tls_shutdown(void *tls) +{ + SSL *ssl = (SSL *)tls; + + if (ssl != NULL) { + (void)SSL_shutdown(ssl); + } +} + +void hybbx_ws_tls_free(void *tls) +{ + SSL *ssl = (SSL *)tls; + + if (ssl != NULL) { + SSL_free(ssl); + } +} + +#else /* !HYBBX_WS_HAVE_TLS */ + +int hybbx_ws_tls_compiled(void) +{ + return 0; +} + +hybbx_result_t hybbx_ws_tls_ensure_certs(const char *cert_dir) +{ + (void)cert_dir; + return HYBBX_OK; +} + +hybbx_result_t hybbx_ws_tls_server_start(const char *cert_dir) +{ + (void)cert_dir; + return HYBBX_ERR_UNSUPPORTED; +} + +void hybbx_ws_tls_server_stop(void) +{ +} + +int hybbx_ws_tls_server_enabled(void) +{ + return 0; +} + +void *hybbx_ws_tls_accept(int fd) +{ + (void)fd; + return NULL; +} + +ssize_t hybbx_ws_tls_recv(void *tls, void *buf, size_t len) +{ + (void)tls; + (void)buf; + (void)len; + return -1; +} + +ssize_t hybbx_ws_tls_send(void *tls, const void *buf, size_t len) +{ + (void)tls; + (void)buf; + (void)len; + return -1; +} + +void hybbx_ws_tls_shutdown(void *tls) +{ + (void)tls; +} + +void hybbx_ws_tls_free(void *tls) +{ + (void)tls; +} + +#endif /* HYBBX_WS_HAVE_TLS */ diff --git a/plugins/websocket/ws_tls.h b/plugins/websocket/ws_tls.h new file mode 100644 index 0000000..2615fab --- /dev/null +++ b/plugins/websocket/ws_tls.h @@ -0,0 +1,43 @@ +#ifndef HYBBX_WS_TLS_H +#define HYBBX_WS_TLS_H + +#include "hybbx/types.h" + +#include <stddef.h> +#include <sys/types.h> + +#ifdef __cplusplus +extern "C" { +#endif + +/** Return 1 when this build links OpenSSL for WebSocket TLS. */ +int hybbx_ws_tls_compiled(void); + +/** + * Create @p cert_dir if needed and generate a self-signed certificate on first + * start when files are missing. No-op when OpenSSL is not compiled in. + */ +hybbx_result_t hybbx_ws_tls_ensure_certs(const char *cert_dir); + +/** Load server TLS context after @p cert_dir is ready. */ +hybbx_result_t hybbx_ws_tls_server_start(const char *cert_dir); + +void hybbx_ws_tls_server_stop(void); + +/** Return 1 when the server accepts TLS (OpenSSL + certs loaded). */ +int hybbx_ws_tls_server_enabled(void); + +/** Perform TLS server handshake on @p fd; returns opaque SSL* or NULL. */ +void *hybbx_ws_tls_accept(int fd); + +ssize_t hybbx_ws_tls_recv(void *tls, void *buf, size_t len); +ssize_t hybbx_ws_tls_send(void *tls, const void *buf, size_t len); + +void hybbx_ws_tls_shutdown(void *tls); +void hybbx_ws_tls_free(void *tls); + +#ifdef __cplusplus +} +#endif + +#endif /* HYBBX_WS_TLS_H */ |
