diff options
| author | info@mode42.com <info@mode42.com> | 2026-08-08 03:54:55 +0000 |
|---|---|---|
| committer | info@mode42.com <info@mode42.com> | 2026-08-08 03:54:55 +0000 |
| commit | 20cb29c2f8c5c87bc590896854a20b1473ceb358 (patch) | |
| tree | 2857f41513a56ad41af97b57362639298aa7f033 /src/core/circuit_tcp.c | |
#2
Diffstat (limited to 'src/core/circuit_tcp.c')
| -rw-r--r-- | src/core/circuit_tcp.c | 1837 |
1 files changed, 1837 insertions, 0 deletions
diff --git a/src/core/circuit_tcp.c b/src/core/circuit_tcp.c new file mode 100644 index 0000000..58f10e4 --- /dev/null +++ b/src/core/circuit_tcp.c @@ -0,0 +1,1837 @@ +#if defined(__linux__) || defined(__GLIBC__) +#define _DEFAULT_SOURCE 1 +#endif + +#include "hybbx/circuit_tcp.h" +#include "hybbx/mains_proxy.h" +#include "hybbx/circuit.h" +#include "hybbx/broadcast.h" +#include "hybbx/circuit_balance.h" +#include "hybbx/circuit_bridge.h" +#include "hybbx/bandwidth_policy.h" +#include "hybbx/session.h" +#include "hybbx/service.h" +#include "hybbx/security.h" +#include "hybbx/security_ban.h" +#include "hybbx/storage.h" +#include "hybbx/traffic.h" +#include "hybbx/link.h" +#include "hybbx/password.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 <pthread.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/socket.h> +#include <time.h> +#include <unistd.h> + +#define HYBBX_CIRCUIT_LINK_POLL_MS 50 +#define HYBBX_CIRCUIT_AUTH_TIMEOUT_MS 15000 + +typedef enum { + CIRCUIT_SLOT_FREE = 0, + CIRCUIT_SLOT_CONNECTING, + CIRCUIT_SLOT_ACTIVE +} circuit_slot_state_t; + +typedef struct hybbx_circuit_link_slot { + hybbx_circuit_hub_t *hub; + circuit_slot_state_t state; + int fd; + pthread_t thread; + char link_id[HYBBX_LINK_ID_MAX]; + hybbx_session_t *session; + hybbx_circuit_decoder_t decoder; + hybbx_circuit_balance_t *balance; + hybbx_circuit_link_profile_t profile; + int profile_set; + time_t last_rf_activity; +} hybbx_circuit_link_slot_t; + +struct hybbx_circuit_hub { + hybbx_service_t *service; + hybbx_circuit_config_t config; + hybbx_link_registry_t links; + hybbx_circuit_bridge_registry_t bridge; + unsigned max_links; + char link_password[128]; + int link_auth; + int listen_v4; + int listen_v6; + pthread_t accept_thread; + pthread_mutex_t lock; + volatile int running; + hybbx_circuit_link_slot_t slots[HYBBX_CIRCUIT_MAX_LINKS]; +}; + +void hybbx_circuit_config_defaults(hybbx_circuit_config_t *cfg) +{ + if (cfg == NULL) { + return; + } + + memset(cfg, 0, sizeof(*cfg)); + snprintf(cfg->bind4, sizeof(cfg->bind4), "127.0.0.1"); + snprintf(cfg->bind6, sizeof(cfg->bind6), "::1"); + cfg->port = HYBBX_CIRCUIT_DEFAULT_PORT; + cfg->ipv4 = 1; + cfg->ipv6 = 1; + cfg->link_stale_days = HYBBX_LINK_STALE_DAYS; + cfg->link_auth = 1; + hybbx_circuit_balance_config_defaults(&cfg->balance); + cfg->max_links = HYBBX_CIRCUIT_DEFAULT_MAX_LINKS; + hybbx_circuit_bridge_clear(&cfg->bridge); +} + +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 backlog) +{ + 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, backlog > 0 ? backlog : 8) != 0) { + close(fd); + return -1; + } + + return fd; +} + +static int circuit_slot_broadcast_qos(const hybbx_circuit_link_slot_t *slot) +{ + if (slot == NULL || !slot->profile_set) { + return 0; + } + + return slot->profile.bandwidth == HYBBX_CIRCUIT_BW_LOW && + slot->profile.duplex == HYBBX_CIRCUIT_DUPLEX_HALF; +} + +static int circuit_frame_is_ax25_broadcast_tx(const uint8_t *frame, size_t len) +{ + unsigned flags; + uint8_t proto; + + if (frame == NULL || len < HYBBX_CIRCUIT_HEADER_SIZE) { + return 0; + } + + if (frame[0] != HYBBX_CIRCUIT_MAGIC_0 || + frame[1] != HYBBX_CIRCUIT_MAGIC_1 || + frame[2] != HYBBX_CIRCUIT_MAGIC_2 || + frame[3] != HYBBX_CIRCUIT_VERSION) { + return 0; + } + + proto = frame[4]; + if (proto != (uint8_t)HYBBX_CIRCUIT_PROTO_AX25_UI && + proto != (uint8_t)HYBBX_CIRCUIT_PROTO_AX25) { + return 0; + } + + flags = ((unsigned)frame[5] << 8u) | (unsigned)frame[6]; + return (flags & HYBBX_CIRCUIT_FLAG_TX) != 0; +} + +static int circuit_slot_can_send_low_prio(const hybbx_circuit_hub_t *hub, + const hybbx_circuit_link_slot_t *slot) +{ + hybbx_circuit_balance_action_t action; + + if (hub == NULL || slot == NULL || slot->balance == NULL || + !hub->config.balance.enabled || !slot->profile_set) { + return 1; + } + + action = hybbx_circuit_balance_action(slot->balance); + if (action == HYBBX_CIRCUIT_BAL_CANCEL) { + return 0; + } + + /* + * Auto-beacon AX.25 uses circuit_slot_send_raw (no queue slot). Do not + * block on PAUSE/BREAK backlog — only CANCEL drops the link. + */ + return 1; +} + +static unsigned circuit_count_used_slots(const hybbx_circuit_hub_t *hub) +{ + unsigned i; + unsigned count = 0; + + if (hub == NULL) { + return 0; + } + + for (i = 0; i < HYBBX_CIRCUIT_MAX_LINKS; i++) { + if (hub->slots[i].state != CIRCUIT_SLOT_FREE) { + count++; + } + } + + return count; +} + +static int circuit_find_free_slot(hybbx_circuit_hub_t *hub) +{ + unsigned i; + + if (hub == NULL) { + return -1; + } + + for (i = 0; i < HYBBX_CIRCUIT_MAX_LINKS; i++) { + if (hub->slots[i].state == CIRCUIT_SLOT_FREE && hub->slots[i].fd < 0) { + return (int)i; + } + } + + return -1; +} + +static int circuit_find_active_slot_by_id(const hybbx_circuit_hub_t *hub, + const char *link_id, + const hybbx_circuit_link_slot_t *except) +{ + unsigned i; + + if (hub == NULL || link_id == NULL || link_id[0] == '\0') { + return -1; + } + + for (i = 0; i < HYBBX_CIRCUIT_MAX_LINKS; i++) { + const hybbx_circuit_link_slot_t *slot = &hub->slots[i]; + + if (slot == except) { + continue; + } + if (slot->state == CIRCUIT_SLOT_FREE || slot->link_id[0] == '\0') { + continue; + } + if (strcmp(slot->link_id, link_id) == 0) { + return (int)i; + } + } + + return -1; +} + +typedef struct balance_send_ctx { + hybbx_circuit_link_slot_t *slot; +} balance_send_ctx_t; + +typedef struct flow_ctrl_ctx { + hybbx_circuit_hub_t *hub; + hybbx_circuit_link_slot_t *slot; +} flow_ctrl_ctx_t; + +static hybbx_result_t circuit_slot_send_raw(hybbx_circuit_link_slot_t *slot, + const uint8_t *frame, size_t len) +{ + hybbx_circuit_hub_t *hub; + ssize_t sent; + size_t off = 0; + int fd; + + if (slot == NULL || frame == NULL || len == 0) { + return HYBBX_ERR_INVALID; + } + + hub = slot->hub; + if (hub == NULL) { + return HYBBX_ERR_INVALID; + } + + pthread_mutex_lock(&hub->lock); + fd = slot->fd; + if (fd < 0) { + pthread_mutex_unlock(&hub->lock); + return HYBBX_ERR_BUSY; + } + + while (off < len) { + sent = send(fd, frame + off, len - off, MSG_NOSIGNAL); + if (sent < 0) { + if (errno == EINTR) { + continue; + } + pthread_mutex_unlock(&hub->lock); + return HYBBX_ERR_IO; + } + if (sent == 0) { + pthread_mutex_unlock(&hub->lock); + return HYBBX_ERR_IO; + } + off += (size_t)sent; + } + + pthread_mutex_unlock(&hub->lock); + return HYBBX_OK; +} + +static hybbx_result_t balance_send_raw_cb(void *ctx, const uint8_t *frame, + size_t len) +{ + balance_send_ctx_t *bctx = (balance_send_ctx_t *)ctx; + + if (bctx == NULL || bctx->slot == NULL) { + return HYBBX_ERR_INVALID; + } + + return circuit_slot_send_raw(bctx->slot, frame, len); +} + +static int circuit_bandwidth_spare_link(hybbx_circuit_hub_t *hub, + hybbx_circuit_link_slot_t *slot, + hybbx_circuit_balance_action_t action) +{ + unsigned users_before; + unsigned affected; + + if (hub == NULL || hub->service == NULL || slot == NULL) { + return 0; + } + + users_before = hybbx_bandwidth_policy_user_count(hub->service); + if (users_before == 0) { + return 0; + } + + affected = hybbx_bandwidth_policy_apply(hub->service, action); + if (affected == 0) { + return 0; + } + + if (slot->balance != NULL && + hybbx_circuit_balance_action(slot->balance) == HYBBX_CIRCUIT_BAL_CANCEL) { + hybbx_circuit_balance_spared_cancel(slot->balance); + hybbx_log_stats("[circuit] secondary link %s spared — users sacrificed first", + slot->link_id[0] != '\0' ? slot->link_id : "?"); + } + + return 1; +} + +static void balance_flow_ctrl_cb(void *ctx, + hybbx_circuit_balance_action_t action, + const char *reason) +{ + flow_ctrl_ctx_t *fctx = (flow_ctrl_ctx_t *)ctx; + hybbx_circuit_hub_t *hub; + hybbx_circuit_link_slot_t *slot; + char payload[128]; + uint8_t frame[HYBBX_CIRCUIT_MAX_FRAME]; + size_t payload_len; + size_t frame_len; + const char *reason_str = reason != NULL ? reason : "-"; + + if (fctx == NULL || fctx->hub == NULL || fctx->slot == NULL || + action == HYBBX_CIRCUIT_BAL_NONE) { + return; + } + + hub = fctx->hub; + slot = fctx->slot; + + payload_len = hybbx_circuit_flow_ctrl_format(action, reason_str, + payload, sizeof(payload)); + if (payload_len == 0) { + return; + } + + frame_len = hybbx_circuit_encode_link_msg(HYBBX_CIRCUIT_PROTO_FLOW_CTRL, + payload, payload_len, + frame, sizeof(frame)); + if (frame_len > 0) { + (void)circuit_slot_send_raw(slot, frame, frame_len); + } + + if (action == HYBBX_CIRCUIT_BAL_CANCEL) { + if (circuit_bandwidth_spare_link(hub, slot, action)) { + return; + } + hybbx_log_warn("[circuit] load-balance cancelled link %s (%s)", + slot->link_id[0] != '\0' ? slot->link_id : "?", + reason_str); + } else if (action == HYBBX_CIRCUIT_BAL_PAUSE || + action == HYBBX_CIRCUIT_BAL_BREAK || + action == HYBBX_CIRCUIT_BAL_RESUME) { + hybbx_log_stats("[circuit] load-balance %s link=%s (%s)", + hybbx_circuit_balance_action_name(action), + slot->link_id[0] != '\0' ? slot->link_id : "?", + reason_str); + } + + if (hub->service != NULL && + (action == HYBBX_CIRCUIT_BAL_PAUSE || + action == HYBBX_CIRCUIT_BAL_BREAK || + action == HYBBX_CIRCUIT_BAL_RESUME)) { + (void)hybbx_bandwidth_policy_apply(hub->service, action); + } +} + +static void circuit_balance_tick_slot(hybbx_circuit_link_slot_t *slot, + int *cancel_link) +{ + balance_send_ctx_t bctx; + flow_ctrl_ctx_t fctx; + hybbx_circuit_balance_tick_result_t tr; + hybbx_circuit_hub_t *hub; + + if (cancel_link != NULL) { + *cancel_link = 0; + } + + if (slot == NULL || slot->hub == NULL || slot->balance == NULL || + !slot->profile_set) { + return; + } + + hub = slot->hub; + bctx.slot = slot; + fctx.hub = hub; + fctx.slot = slot; + + tr = hybbx_circuit_balance_tick(slot->balance, HYBBX_CIRCUIT_LINK_POLL_MS, + balance_send_raw_cb, &bctx, + balance_flow_ctrl_cb, &fctx); + if (slot->balance != NULL && slot->profile_set && + hub->config.balance.enabled && + slot->profile.bandwidth == HYBBX_CIRCUIT_BW_LOW && + hybbx_circuit_balance_action(slot->balance) == HYBBX_CIRCUIT_BAL_PAUSE && + hybbx_circuit_balance_queued_bytes(slot->balance) >= + hub->config.balance.queue_pause) { + (void)circuit_bandwidth_spare_link(hub, slot, HYBBX_CIRCUIT_BAL_PAUSE); + } + if (tr == HYBBX_CIRCUIT_BAL_TICK_CANCEL_LINK && cancel_link != NULL) { + if (circuit_bandwidth_spare_link(hub, slot, HYBBX_CIRCUIT_BAL_CANCEL)) { + tr = HYBBX_CIRCUIT_BAL_TICK_OK; + } else { + *cancel_link = 1; + } + } +} + +static hybbx_result_t circuit_slot_send_hbx(hybbx_circuit_link_slot_t *slot, + const uint8_t *frame, size_t len) +{ + balance_send_ctx_t bctx; + flow_ctrl_ctx_t fctx; + hybbx_circuit_hub_t *hub; + + if (slot == NULL || frame == NULL || len == 0) { + return HYBBX_ERR_INVALID; + } + + hub = slot->hub; + if (hub == NULL) { + return HYBBX_ERR_INVALID; + } + + if (slot->balance != NULL && slot->profile_set && + hub->config.balance.enabled) { + bctx.slot = slot; + fctx.hub = hub; + fctx.slot = slot; + return hybbx_circuit_balance_submit(slot->balance, frame, len, + balance_send_raw_cb, &bctx, + balance_flow_ctrl_cb, &fctx); + } + + return circuit_slot_send_raw(slot, frame, len); +} + +hybbx_result_t hybbx_circuit_hub_send_raw(hybbx_circuit_hub_t *hub, + const uint8_t *frame, size_t len) +{ + unsigned i; + + if (hub == NULL || frame == NULL || len == 0) { + return HYBBX_ERR_INVALID; + } + + for (i = 0; i < HYBBX_CIRCUIT_MAX_LINKS; i++) { + if (hub->slots[i].state != CIRCUIT_SLOT_FREE && hub->slots[i].fd >= 0) { + return circuit_slot_send_raw(&hub->slots[i], frame, len); + } + } + + return HYBBX_ERR_BUSY; +} + +static hybbx_result_t circuit_transport_write(hybbx_session_t *session, + const char *data, size_t len) +{ + hybbx_circuit_link_slot_t *slot; + uint8_t frame[HYBBX_CIRCUIT_MAX_FRAME]; + size_t frame_len; + + if (session == NULL || data == NULL || len == 0) { + return HYBBX_OK; + } + + slot = (hybbx_circuit_link_slot_t *)session->transport_data; + if (slot == NULL) { + return HYBBX_ERR_INVALID; + } + + frame_len = hybbx_circuit_encode_terminal(data, len, frame, sizeof(frame)); + if (frame_len == 0) { + return HYBBX_ERR_IO; + } + + return circuit_slot_send_hbx(slot, frame, frame_len); +} + +hybbx_result_t hybbx_circuit_hub_send_hbx(hybbx_circuit_hub_t *hub, + const uint8_t *frame, size_t len) +{ + if (hub == NULL || frame == NULL || len == 0) { + return HYBBX_ERR_INVALID; + } + + return hybbx_circuit_hub_multicast_hbx(hub, frame, len, 0.0, 0, NULL); +} + +unsigned hybbx_circuit_hub_active_link_count(const hybbx_circuit_hub_t *hub) +{ + unsigned i; + unsigned count = 0; + + if (hub == NULL) { + return 0; + } + + for (i = 0; i < HYBBX_CIRCUIT_MAX_LINKS; i++) { + if (hub->slots[i].state == CIRCUIT_SLOT_ACTIVE) { + count++; + } + } + + return count; +} + +static double circuit_slot_effective_frequency_mhz( + const hybbx_circuit_link_slot_t *slot, + const hybbx_circuit_hub_t *hub) +{ + double mhz; + + if (slot == NULL) { + return 0.0; + } + + mhz = slot->profile.frequency_mhz; + if (mhz <= 0.0 && slot->link_id[0] != '\0' && hub != NULL) { + const hybbx_circuit_bridge_entry_t *be = + hybbx_circuit_bridge_find(&hub->bridge, slot->link_id); + + if (be != NULL && be->frequency_mhz > 0.0) { + mhz = be->frequency_mhz; + } + } + + return mhz; +} + +hybbx_result_t hybbx_circuit_hub_multicast_hbx(hybbx_circuit_hub_t *hub, + const uint8_t *frame, size_t len, + double frequency_mhz, + int require_broadcast_qos, + unsigned *sent_out) +{ + unsigned i; + int sent = 0; + hybbx_result_t last_err = HYBBX_ERR_NOT_FOUND; + + if (sent_out != NULL) { + *sent_out = 0; + } + + if (hub == NULL || frame == NULL || len == 0) { + return HYBBX_ERR_INVALID; + } + + for (i = 0; i < HYBBX_CIRCUIT_MAX_LINKS; i++) { + hybbx_circuit_link_slot_t *slot = &hub->slots[i]; + hybbx_result_t rc; + + if (slot->state != CIRCUIT_SLOT_ACTIVE || slot->fd < 0) { + continue; + } + if (require_broadcast_qos && !circuit_slot_broadcast_qos(slot)) { + continue; + } + if (frequency_mhz > 0.0) { + double slot_mhz = circuit_slot_effective_frequency_mhz(slot, hub); + + if (slot_mhz <= 0.0 || + !hybbx_ax25_frequency_match(frequency_mhz, slot_mhz)) { + continue; + } + } + if (require_broadcast_qos && circuit_frame_is_ax25_broadcast_tx(frame, len) && + !circuit_slot_can_send_low_prio(hub, slot)) { + last_err = HYBBX_ERR_BUSY; + continue; + } + + if (require_broadcast_qos && circuit_frame_is_ax25_broadcast_tx(frame, len)) { + /* + * Low-priority AX.25 broadcast: admission is decided by balancer + * flow-control state, then transmit immediately (no queue slot). + * This avoids enqueue->break drops that can otherwise log as sent + * without reaching RF. + */ + rc = circuit_slot_send_raw(slot, frame, len); + if (rc == HYBBX_OK) { + hybbx_circuit_hub_note_rf_activity(hub, slot->link_id); + } + } else { + rc = circuit_slot_send_hbx(slot, frame, len); + } + if (rc == HYBBX_OK) { + sent++; + } else { + last_err = rc; + } + } + + if (sent > 0) { + if (sent_out != NULL) { + *sent_out = (unsigned)sent; + } + return HYBBX_OK; + } + + if (require_broadcast_qos) { + if (last_err != HYBBX_ERR_NOT_FOUND) { + return last_err; + } + return HYBBX_ERR_DENIED; + } + + return last_err; +} + +hybbx_result_t hybbx_circuit_hub_send_hbx_slot(hybbx_circuit_hub_t *hub, + unsigned slot_index, + const uint8_t *frame, size_t len, + int require_broadcast_qos) +{ + hybbx_circuit_link_slot_t *slot; + hybbx_result_t rc; + + if (hub == NULL || frame == NULL || len == 0 || + slot_index >= HYBBX_CIRCUIT_MAX_LINKS) { + return HYBBX_ERR_INVALID; + } + + slot = &hub->slots[slot_index]; + if (slot->state != CIRCUIT_SLOT_ACTIVE || slot->fd < 0) { + return HYBBX_ERR_DENIED; + } + if (require_broadcast_qos && !circuit_slot_broadcast_qos(slot)) { + return HYBBX_ERR_DENIED; + } + if (require_broadcast_qos && circuit_frame_is_ax25_broadcast_tx(frame, len) && + !circuit_slot_can_send_low_prio(hub, slot)) { + return HYBBX_ERR_BUSY; + } + + if (require_broadcast_qos && circuit_frame_is_ax25_broadcast_tx(frame, len)) { + rc = circuit_slot_send_raw(slot, frame, len); + if (rc == HYBBX_OK) { + hybbx_circuit_hub_note_rf_activity(hub, slot->link_id); + } + } else { + rc = circuit_slot_send_hbx(slot, frame, len); + } + + return rc; +} + +double hybbx_circuit_hub_link_frequency_mhz(const hybbx_circuit_hub_t *hub) +{ + unsigned i; + + if (hub == NULL) { + return 0.0; + } + + for (i = 0; i < HYBBX_CIRCUIT_MAX_LINKS; i++) { + if (hub->slots[i].state == CIRCUIT_SLOT_ACTIVE && hub->slots[i].profile_set) { + return hub->slots[i].profile.frequency_mhz; + } + } + + return 0.0; +} + +int hybbx_circuit_hub_link_broadcast_qos(const hybbx_circuit_hub_t *hub) +{ + unsigned i; + + if (hub == NULL) { + return 0; + } + + for (i = 0; i < HYBBX_CIRCUIT_MAX_LINKS; i++) { + if (hub->slots[i].state == CIRCUIT_SLOT_ACTIVE && + circuit_slot_broadcast_qos(&hub->slots[i])) { + return 1; + } + } + + return 0; +} + +static void circuit_broadcast_links_sort(hybbx_circuit_broadcast_link_t *links, + unsigned count) +{ + unsigned i; + unsigned j; + + for (i = 1; i < count; i++) { + hybbx_circuit_broadcast_link_t key = links[i]; + + j = i; + while (j > 0) { + double prev_mhz = links[j - 1].frequency_mhz; + double key_mhz = key.frequency_mhz; + int move = 0; + + if (key_mhz > 0.0 && prev_mhz <= 0.0) { + move = 0; + } else if (key_mhz <= 0.0 && prev_mhz > 0.0) { + move = 1; + } else if (key_mhz > 0.0 && prev_mhz > 0.0 && + key_mhz < prev_mhz) { + move = 1; + } else if (key_mhz <= 0.0 && prev_mhz <= 0.0 && + strcmp(key.link_id, links[j - 1].link_id) < 0) { + move = 1; + } + + if (!move) { + break; + } + + links[j] = links[j - 1]; + j--; + } + links[j] = key; + } +} + +unsigned hybbx_circuit_hub_broadcast_links(const hybbx_circuit_hub_t *hub, + hybbx_circuit_broadcast_link_t *out, + unsigned out_max) +{ + unsigned i; + unsigned count = 0; + + if (hub == NULL || out == NULL || out_max == 0) { + return 0; + } + + for (i = 0; i < HYBBX_CIRCUIT_MAX_LINKS; i++) { + const hybbx_circuit_link_slot_t *slot = &hub->slots[i]; + + if (slot->state != CIRCUIT_SLOT_ACTIVE || slot->fd < 0) { + continue; + } + if (!circuit_slot_broadcast_qos(slot)) { + continue; + } + if (count >= out_max) { + break; + } + + out[count].frequency_mhz = + circuit_slot_effective_frequency_mhz(slot, hub); + out[count].slot_index = i; + hybbx_strlcpy(out[count].link_id, slot->link_id, + sizeof(out[count].link_id)); + count++; + } + + if (count > 1) { + circuit_broadcast_links_sort(out, count); + } + + return count; +} + +static void on_circuit_frame(hybbx_circuit_proto_t proto, uint16_t flags, + const uint8_t *payload, size_t len, + void *userdata) +{ + hybbx_circuit_link_slot_t *slot = (hybbx_circuit_link_slot_t *)userdata; + uint8_t ui[HYBBX_AX25_PAYLOAD_MAX]; + hybbx_ax25_path_t path; + + (void)flags; + + if (slot == NULL) { + return; + } + + if (proto == HYBBX_CIRCUIT_PROTO_PROXY_MAIL || + proto == HYBBX_CIRCUIT_PROTO_PROXY_CHAT) { + if (slot->hub != NULL && slot->hub->service != NULL) { + hybbx_mains_proxy_inbound_frame(slot->hub->service, proto, + payload, len); + } + return; + } + + if (slot->session == NULL || len == 0) { + return; + } + + switch (proto) { + case HYBBX_CIRCUIT_PROTO_AX25: { + size_t ui_len = hybbx_ax25_parse_ui(payload, len, &path, ui, + sizeof(ui)); + if (ui_len > 0) { + (void)hybbx_session_handle_input(slot->session, ui, ui_len); + } + break; + } + case HYBBX_CIRCUIT_PROTO_AX25_UI: { + size_t ui_len = hybbx_circuit_unpack_ax25_ui(payload, len, &path, + ui, sizeof(ui)); + if (ui_len > 0) { + (void)hybbx_session_handle_input(slot->session, ui, ui_len); + } + break; + } + case HYBBX_CIRCUIT_PROTO_TERMINAL: + (void)hybbx_session_handle_input(slot->session, payload, len); + break; + default: + hybbx_log_debug("[circuit] link=%s ignored proto=%s (%u bytes)", + slot->link_id[0] != '\0' ? slot->link_id : "?", + hybbx_circuit_proto_name(proto), (unsigned)len); + break; + } +} + +static void circuit_slot_reset(hybbx_circuit_link_slot_t *slot) +{ + if (slot == NULL) { + return; + } + + if (slot->balance != NULL) { + hybbx_circuit_balance_destroy(slot->balance); + slot->balance = NULL; + } + + if (slot->session != NULL) { + hybbx_session_close(slot->session); + slot->session = NULL; + } + + slot->profile_set = 0; + memset(&slot->profile, 0, sizeof(slot->profile)); + slot->link_id[0] = '\0'; + slot->last_rf_activity = 0; + slot->state = CIRCUIT_SLOT_FREE; + slot->thread = (pthread_t)0; +} + +static void circuit_close_slot(hybbx_circuit_link_slot_t *slot) +{ + hybbx_circuit_hub_t *hub; + int fd; + + if (slot == NULL || slot->hub == NULL) { + return; + } + + hub = slot->hub; + + pthread_mutex_lock(&hub->lock); + fd = slot->fd; + if (fd >= 0) { + close(fd); + slot->fd = -1; + } + pthread_mutex_unlock(&hub->lock); + + circuit_slot_reset(slot); +} + +static void circuit_close_all_slots(hybbx_circuit_hub_t *hub) +{ + unsigned i; + + if (hub == NULL) { + return; + } + + for (i = 0; i < HYBBX_CIRCUIT_MAX_LINKS; i++) { + if (hub->slots[i].state != CIRCUIT_SLOT_FREE || hub->slots[i].fd >= 0) { + circuit_close_slot(&hub->slots[i]); + } + } +} + +typedef struct circuit_auth_ctx { + hybbx_circuit_hub_t *hub; + hybbx_circuit_link_slot_t *slot; + int done; + int ok; + hybbx_link_auth_t auth; +} circuit_auth_ctx_t; + +static void circuit_evict_stale_link(hybbx_circuit_hub_t *hub, + hybbx_circuit_link_slot_t *stale, + const char *link_id) +{ + int fd = -1; + + if (hub == NULL || stale == NULL) { + return; + } + + /* + * Reconnect race: close the stale TCP fd and drop link_id so the new + * auth can proceed. Do not reset session/balance here — the stale link + * thread still owns those until it exits and calls circuit_close_slot. + */ + pthread_mutex_lock(&hub->lock); + fd = stale->fd; + if (fd >= 0) { + (void)shutdown(fd, SHUT_RDWR); + close(fd); + stale->fd = -1; + } + stale->link_id[0] = '\0'; + pthread_mutex_unlock(&hub->lock); + + if (link_id != NULL && link_id[0] != '\0') { + hybbx_log_info("[circuit] replacing stale link id=%s (reconnect)", + link_id); + } +} + +static int circuit_auth_fail_counts_as_abuse(const char *reason) +{ + if (reason == NULL) { + return 0; + } + + /* + * duplicate_id is a reconnect race (stale hub slot), not brute-force. + * circuit_validate_link_auth evicts the stale slot; do not ban for it. + */ + if (strcmp(reason, "duplicate_id") == 0) { + return 0; + } + + return 1; +} + +static void circuit_log_auth_fail(int fd, const char *reason, const char *id) +{ + char ip[HYBBX_REMOTE_ADDR_MAX]; + int count_abuse = circuit_auth_fail_counts_as_abuse(reason); + + if (reason == NULL) { + return; + } + + if (fd >= 0 && hybbx_socket_peer_name(fd, ip, sizeof(ip)) == HYBBX_OK) { + if (id != NULL && id[0] != '\0') { + hybbx_security_log_write( + "link_auth_fail ip=%s id=%s reason=%s transport=circuit", + ip, id, reason); + } else { + hybbx_security_log_write( + "link_auth_fail ip=%s reason=%s transport=circuit", + ip, reason); + } + if (count_abuse) { + hybbx_security_ban_link_auth_fail(ip); + if (id != NULL && id[0] != '\0' && strcmp(reason, "banned") != 0) { + hybbx_security_ban_link_auth_fail_callid(id); + } + } + } else { + hybbx_security_log_write( + "link_auth_fail ip=? reason=%s transport=circuit", reason); + } +} + +static int circuit_auth_password_ok(const hybbx_circuit_hub_t *hub, + const hybbx_circuit_bridge_entry_t *entry, + const char *password) +{ + if (hub == NULL || password == NULL) { + return 0; + } + + if (entry != NULL && entry->link_password[0] != '\0') { + return hybbx_password_match(entry->link_password, password); + } + + if (hub->link_password[0] != '\0') { + return hybbx_password_match(hub->link_password, password); + } + + return 0; +} + +static int circuit_validate_link_auth(circuit_auth_ctx_t *ctx, const char **reason) +{ + const hybbx_circuit_bridge_entry_t *entry = NULL; + hybbx_circuit_hub_t *hub; + hybbx_circuit_link_slot_t *slot; + + if (ctx == NULL || ctx->hub == NULL || ctx->slot == NULL) { + if (reason != NULL) { + *reason = "invalid"; + } + return 0; + } + + hub = ctx->hub; + slot = ctx->slot; + + if (!hub->link_auth) { + return 1; + } + + if (hub->bridge.count > 0) { + entry = hybbx_circuit_bridge_find(&hub->bridge, ctx->auth.id); + if (entry == NULL) { + if (reason != NULL) { + *reason = "unknown_id"; + } + return 0; + } + } else if (hub->link_password[0] == '\0') { + if (reason != NULL) { + *reason = "no_password"; + } + return 0; + } + + if (!circuit_auth_password_ok(hub, entry, ctx->auth.password)) { + if (reason != NULL) { + *reason = "password"; + } + return 0; + } + + { + int stale_idx = circuit_find_active_slot_by_id(hub, ctx->auth.id, slot); + + if (stale_idx >= 0) { + circuit_evict_stale_link(hub, &hub->slots[(unsigned)stale_idx], + ctx->auth.id); + if (circuit_find_active_slot_by_id(hub, ctx->auth.id, slot) >= 0) { + if (reason != NULL) { + *reason = "duplicate_id"; + } + return 0; + } + } + } + + (void)entry; + (void)slot; + return 1; +} + +static void circuit_on_auth_frame(hybbx_circuit_proto_t proto, uint16_t flags, + const uint8_t *payload, size_t len, + void *userdata) +{ + circuit_auth_ctx_t *ctx = (circuit_auth_ctx_t *)userdata; + const hybbx_circuit_bridge_entry_t *entry; + char code[HYBBX_LINK_CODE_MAX]; + char ack[HYBBX_LINK_AUTH_PAYLOAD_MAX]; + uint8_t frame[HYBBX_CIRCUIT_MAX_FRAME]; + size_t frame_len; + const char *fail_reason = "invalid"; + int fd; + + (void)flags; + + if (ctx == NULL || ctx->done || ctx->slot == NULL) { + return; + } + + if (proto != HYBBX_CIRCUIT_PROTO_LINK_AUTH) { + return; + } + + fd = ctx->slot->fd; + + if (hybbx_link_auth_parse((const char *)payload, len, &ctx->auth) != HYBBX_OK) { + circuit_log_auth_fail(fd, "invalid", NULL); + ctx->done = 1; + ctx->ok = 0; + return; + } + + if (!hybbx_security_ban_callid_accept(ctx->auth.id)) { + circuit_log_auth_fail(fd, "banned", ctx->auth.id); + ctx->done = 1; + ctx->ok = 0; + return; + } + + if (!circuit_validate_link_auth(ctx, &fail_reason)) { + hybbx_log_warn("[circuit] link auth failed for id=%s (%s)", + ctx->auth.id, fail_reason); + circuit_log_auth_fail(fd, fail_reason, ctx->auth.id); + ctx->done = 1; + ctx->ok = 0; + return; + } + + code[0] = '\0'; + (void)hybbx_link_registry_touch(&ctx->hub->links, ctx->auth.id, + ctx->auth.role, code, sizeof(code)); + + snprintf(ack, sizeof(ack), "ok=yes\nid=%s\ncode=%s\n", + ctx->auth.id, code[0] != '\0' ? code : "-"); + frame_len = hybbx_circuit_encode_link_msg(HYBBX_CIRCUIT_PROTO_LINK_AUTH_ACK, + ack, strlen(ack), + frame, sizeof(frame)); + if (frame_len > 0) { + (void)circuit_slot_send_raw(ctx->slot, frame, frame_len); + } + + hybbx_strlcpy(ctx->slot->link_id, ctx->auth.id, sizeof(ctx->slot->link_id)); + + hybbx_circuit_link_profile_from_auth(&ctx->auth, &ctx->slot->profile); + entry = hybbx_circuit_bridge_find(&ctx->hub->bridge, ctx->auth.id); + if (entry != NULL && entry->frequency_mhz > 0.0 && + ctx->slot->profile.frequency_mhz <= 0.0) { + ctx->slot->profile.frequency_mhz = entry->frequency_mhz; + } + + if (ctx->slot->balance != NULL) { + hybbx_circuit_balance_set_profile(ctx->slot->balance, &ctx->slot->profile); + } + ctx->slot->profile_set = 1; + + hybbx_log_info("[circuit] link authenticated id=%s role=%s code=%s", + ctx->auth.id, ctx->auth.role, code[0] != '\0' ? code : "-"); + { + char qos_msg[128]; + int qos_len; + + qos_len = snprintf(qos_msg, sizeof(qos_msg), + "[circuit] link QoS bandwidth=%s baud=%u duplex=%s", + ctx->slot->profile.bandwidth == HYBBX_CIRCUIT_BW_LOW ? + "low" : "high", + ctx->slot->profile.baud, + ctx->slot->profile.duplex == HYBBX_CIRCUIT_DUPLEX_HALF ? + "half" : "full"); + if (qos_len > 0 && ctx->slot->profile.frequency_mhz > 0.0) { + snprintf(qos_msg + (size_t)qos_len, + sizeof(qos_msg) - (size_t)qos_len, + " %.3fMHz", ctx->slot->profile.frequency_mhz); + } + hybbx_log_info("%s", qos_msg); + } + + ctx->done = 1; + ctx->ok = 1; +} + +static int circuit_wait_link_auth(hybbx_circuit_link_slot_t *slot) +{ + circuit_auth_ctx_t ctx; + hybbx_circuit_decoder_t dec; + uint8_t buf[256]; + unsigned elapsed = 0; + hybbx_circuit_hub_t *hub; + + if (slot == NULL || slot->hub == NULL) { + return 0; + } + + hub = slot->hub; + + if (!hub->link_auth) { + return 1; + } + + memset(&ctx, 0, sizeof(ctx)); + ctx.hub = hub; + ctx.slot = slot; + hybbx_circuit_decoder_init(&dec); + + while (!ctx.done && elapsed < HYBBX_CIRCUIT_AUTH_TIMEOUT_MS) { + struct pollfd pfd; + ssize_t n; + int pr; + + pthread_mutex_lock(&hub->lock); + pfd.fd = slot->fd; + pthread_mutex_unlock(&hub->lock); + + if (pfd.fd < 0) { + return 0; + } + + pfd.events = POLLIN; + pfd.revents = 0; + pr = poll(&pfd, 1, HYBBX_CIRCUIT_LINK_POLL_MS); + if (pr < 0) { + if (errno == EINTR) { + continue; + } + return 0; + } + if (pr == 0) { + elapsed += HYBBX_CIRCUIT_LINK_POLL_MS; + continue; + } + if ((pfd.revents & POLLIN) == 0) { + return 0; + } + + n = recv(pfd.fd, buf, sizeof(buf), 0); + if (n <= 0) { + return 0; + } + + hybbx_circuit_decoder_feed(&dec, buf, (size_t)n, + circuit_on_auth_frame, &ctx); + } + + if (!ctx.done) { + circuit_log_auth_fail(slot->fd, "timeout_no_link_auth", NULL); + } + + return ctx.ok; +} + +static void *circuit_link_thread(void *arg) +{ + hybbx_circuit_link_slot_t *slot = (hybbx_circuit_link_slot_t *)arg; + hybbx_circuit_hub_t *hub; + uint8_t buf[512]; + hybbx_result_t rc; + + if (slot == NULL || slot->hub == NULL) { + return NULL; + } + + hub = slot->hub; + + if (!circuit_wait_link_auth(slot)) { + hybbx_log_warn("[circuit] link authentication failed or timed out"); + circuit_close_slot(slot); + return NULL; + } + + if (!slot->profile_set) { + hybbx_circuit_link_profile_from_auth(NULL, &slot->profile); + if (slot->balance != NULL) { + hybbx_circuit_balance_set_profile(slot->balance, &slot->profile); + } + slot->profile_set = 1; + } + + hybbx_circuit_decoder_init(&slot->decoder); + + rc = hybbx_session_open(hub->service, &hybbx_plugin_circuit, slot, + &slot->session); + if (rc != HYBBX_OK) { + hybbx_log_warn("[circuit] session open failed for link %s", + slot->link_id[0] != '\0' ? slot->link_id : "?"); + circuit_close_slot(slot); + return NULL; + } + + { + char remote[HYBBX_REMOTE_ADDR_MAX]; + int fd = slot->fd; + + if (fd >= 0 && + hybbx_socket_peer_name(fd, remote, sizeof(remote)) == HYBBX_OK) { + (void)hybbx_session_set_remote(slot->session, remote); + } + } + + slot->state = CIRCUIT_SLOT_ACTIVE; + slot->last_rf_activity = time(NULL); + hybbx_log_info("[circuit] link adapter attached id=%s (HBX bridge active)", + slot->link_id[0] != '\0' ? slot->link_id : "?"); + + while (hub->running) { + struct pollfd pfd; + ssize_t n; + int pr; + + pthread_mutex_lock(&hub->lock); + pfd.fd = slot->fd; + pthread_mutex_unlock(&hub->lock); + + if (pfd.fd < 0) { + break; + } + + pfd.events = POLLIN; + pfd.revents = 0; + pr = poll(&pfd, 1, HYBBX_CIRCUIT_LINK_POLL_MS); + if (pr < 0) { + if (errno == EINTR) { + continue; + } + break; + } + if (pr == 0) { + int cancel_link = 0; + + circuit_balance_tick_slot(slot, &cancel_link); + if (cancel_link) { + break; + } + if (slot->session != NULL) { + rc = hybbx_session_tick(slot->session); + if (rc == HYBBX_SESSION_END) { + break; + } + } + continue; + } + if ((pfd.revents & (POLLERR | POLLHUP | POLLNVAL)) != 0) { + break; + } + if ((pfd.revents & POLLIN) == 0) { + continue; + } + + n = recv(pfd.fd, buf, sizeof(buf), 0); + if (n < 0) { + if (errno == EINTR) { + continue; + } + break; + } + if (n == 0) { + break; + } + + hybbx_circuit_decoder_feed(&slot->decoder, buf, (size_t)n, + on_circuit_frame, slot); + + { + int cancel_link = 0; + circuit_balance_tick_slot(slot, &cancel_link); + if (cancel_link) { + break; + } + } + } + + hybbx_log_info("[circuit] link detached id=%s", + slot->link_id[0] != '\0' ? slot->link_id : "?"); + circuit_close_slot(slot); + return NULL; +} + +static void *circuit_accept_thread(void *arg) +{ + hybbx_circuit_hub_t *hub = (hybbx_circuit_hub_t *)arg; + + while (hub->running) { + struct pollfd pfds[2]; + int count = 0; + int pr; + int i; + + pfds[0].fd = hub->listen_v4; + pfds[0].events = POLLIN; + pfds[0].revents = 0; + if (hub->listen_v4 >= 0) { + count = 1; + } + + pfds[1].fd = hub->listen_v6; + pfds[1].events = POLLIN; + pfds[1].revents = 0; + if (hub->listen_v6 >= 0) { + count = 2; + } + + if (count == 0) { + break; + } + + pr = poll(pfds, (nfds_t)count, HYBBX_CIRCUIT_LINK_POLL_MS); + if (pr < 0) { + if (errno == EINTR) { + continue; + } + break; + } + if (pr == 0) { + continue; + } + + for (i = 0; i < count; i++) { + if ((pfds[i].revents & POLLIN) == 0) { + continue; + } + + { + int client = accept(pfds[i].fd, NULL, NULL); + int slot_idx; + hybbx_circuit_link_slot_t *slot; + + if (client < 0) { + continue; + } + + (void)set_socket_options(client, 0); + + if (!hybbx_security_ban_accept_fd(client)) { + close(client); + continue; + } + + pthread_mutex_lock(&hub->lock); + if (circuit_count_used_slots(hub) >= hub->max_links) { + pthread_mutex_unlock(&hub->lock); + hybbx_log_warn("[circuit] max_links=%u reached — rejecting connection", + hub->max_links); + close(client); + continue; + } + + slot_idx = circuit_find_free_slot(hub); + if (slot_idx < 0) { + pthread_mutex_unlock(&hub->lock); + close(client); + continue; + } + + slot = &hub->slots[slot_idx]; + slot->fd = client; + slot->state = CIRCUIT_SLOT_CONNECTING; + slot->balance = hybbx_circuit_balance_create(&hub->config.balance); + if (slot->balance == NULL) { + hybbx_log_warn("[circuit] balance alloc failed — rejecting link"); + close(client); + slot->fd = -1; + slot->state = CIRCUIT_SLOT_FREE; + pthread_mutex_unlock(&hub->lock); + continue; + } + pthread_mutex_unlock(&hub->lock); + + if (pthread_create(&slot->thread, NULL, circuit_link_thread, + slot) != 0) { + hybbx_log_warn("[circuit] link thread failed"); + circuit_close_slot(slot); + } else { + pthread_detach(slot->thread); + } + } + } + } + + return NULL; +} + +hybbx_circuit_hub_t *hybbx_circuit_hub_create(hybbx_service_t *service) +{ + hybbx_circuit_hub_t *hub; + unsigned i; + + hub = calloc(1, sizeof(*hub)); + if (hub == NULL) { + return NULL; + } + + hub->service = service; + hub->listen_v4 = -1; + hub->listen_v6 = -1; + hub->max_links = HYBBX_CIRCUIT_DEFAULT_MAX_LINKS; + pthread_mutex_init(&hub->lock, NULL); + + for (i = 0; i < HYBBX_CIRCUIT_MAX_LINKS; i++) { + hub->slots[i].hub = hub; + hub->slots[i].fd = -1; + hub->slots[i].state = CIRCUIT_SLOT_FREE; + } + + return hub; +} + +void hybbx_circuit_hub_destroy(hybbx_circuit_hub_t *hub) +{ + if (hub == NULL) { + return; + } + + hybbx_circuit_hub_stop(hub); + pthread_mutex_destroy(&hub->lock); + free(hub); +} + +hybbx_result_t hybbx_circuit_hub_start(hybbx_circuit_hub_t *hub, + const hybbx_circuit_config_t *cfg) +{ + int backlog; + + if (hub == NULL || cfg == NULL) { + return HYBBX_ERR_INVALID; + } + + hybbx_circuit_hub_stop(hub); + hub->config = *cfg; + hub->bridge = cfg->bridge; + hub->max_links = cfg->max_links; + if (hub->max_links == 0 || hub->max_links > HYBBX_CIRCUIT_MAX_LINKS) { + hub->max_links = HYBBX_CIRCUIT_DEFAULT_MAX_LINKS; + } + hybbx_strlcpy(hub->link_password, cfg->link_password, sizeof(hub->link_password)); + hub->link_auth = cfg->link_auth; + hybbx_link_registry_init(&hub->links, cfg->data_path, cfg->config_path, + cfg->link_stale_days); + (void)hybbx_link_registry_prune(&hub->links); + hub->running = 1; + + backlog = (int)hub->max_links; + if (backlog < 4) { + backlog = 4; + } + + if (cfg->ipv4) { + hub->listen_v4 = create_listen_socket(AF_INET, cfg->bind4, cfg->port, + backlog); + if (hub->listen_v4 < 0) { + hybbx_socket_log_bind_failure("circuit", cfg->bind4, cfg->port); + hybbx_circuit_hub_stop(hub); + return HYBBX_ERR_IO; + } + } + + if (cfg->ipv6) { + hub->listen_v6 = create_listen_socket(AF_INET6, cfg->bind6, cfg->port, + backlog); + if (hub->listen_v6 < 0) { + hybbx_log_warn("[circuit] IPv6 bind [%s]:%u skipped (%s)", + cfg->bind6, cfg->port, strerror(errno)); + } + } + + if (hub->listen_v4 < 0 && hub->listen_v6 < 0) { + hybbx_circuit_hub_stop(hub); + return HYBBX_ERR_IO; + } + + { + char hub_msg[256]; + size_t off = 0; + + off = (size_t)snprintf(hub_msg, sizeof(hub_msg), + "[circuit] internal TCP hub"); + if (hub->listen_v4 >= 0) { + off += (size_t)snprintf(hub_msg + off, sizeof(hub_msg) - off, + " %s:%u", cfg->bind4, cfg->port); + } + if (hub->listen_v6 >= 0) { + off += (size_t)snprintf(hub_msg + off, sizeof(hub_msg) - off, + " [%s]:%u", cfg->bind6, cfg->port); + } + snprintf(hub_msg + off, sizeof(hub_msg) - off, + " (HBX v%u, max_links=%u, bridge=%u)", + (unsigned)HYBBX_CIRCUIT_VERSION, hub->max_links, + hub->bridge.count); + hybbx_log_info("%s", hub_msg); + } + + if (pthread_create(&hub->accept_thread, NULL, circuit_accept_thread, + hub) != 0) { + hybbx_circuit_hub_stop(hub); + return HYBBX_ERR_IO; + } + + return HYBBX_OK; +} + +void hybbx_circuit_hub_stop(hybbx_circuit_hub_t *hub) +{ + if (hub == NULL) { + return; + } + + hybbx_broadcast_ax25_seq_cancel(); + hub->running = 0; + + if (hub->listen_v4 >= 0) { + close(hub->listen_v4); + hub->listen_v4 = -1; + } + if (hub->listen_v6 >= 0) { + close(hub->listen_v6); + hub->listen_v6 = -1; + } + + circuit_close_all_slots(hub); + + if (hub->accept_thread) { + pthread_join(hub->accept_thread, NULL); + hub->accept_thread = (pthread_t)0; + } +} + +int hybbx_circuit_hub_running(const hybbx_circuit_hub_t *hub) +{ + return hub != NULL && hub->running; +} + +unsigned hybbx_circuit_hub_port(const hybbx_circuit_hub_t *hub) +{ + if (hub == NULL) { + return HYBBX_CIRCUIT_DEFAULT_PORT; + } + + return hub->config.port; +} + +void hybbx_circuit_hub_prune_links(hybbx_circuit_hub_t *hub) +{ + if (hub != NULL) { + (void)hybbx_link_registry_prune(&hub->links); + } +} + +void hybbx_circuit_hub_note_rf_activity(hybbx_circuit_hub_t *hub, + const char *link_id) +{ + time_t now; + unsigned i; + + if (hub == NULL || link_id == NULL || link_id[0] == '\0') { + return; + } + + now = time(NULL); + + pthread_mutex_lock(&hub->lock); + for (i = 0; i < HYBBX_CIRCUIT_MAX_LINKS; i++) { + hybbx_circuit_link_slot_t *slot = &hub->slots[i]; + + if (slot->state != CIRCUIT_SLOT_ACTIVE || slot->fd < 0) { + continue; + } + if (strcmp(slot->link_id, link_id) != 0) { + continue; + } + + slot->last_rf_activity = now; + break; + } + pthread_mutex_unlock(&hub->lock); +} + +int hybbx_circuit_hub_link_band_idle(const hybbx_circuit_hub_t *hub, + unsigned slot_index, + unsigned min_idle_sec) +{ + const hybbx_circuit_link_slot_t *slot; + hybbx_circuit_hub_t *mutable_hub; + time_t now; + time_t last_rf; + time_t idle_since; + circuit_slot_state_t state; + int fd; + + if (hub == NULL || slot_index >= HYBBX_CIRCUIT_MAX_LINKS || + min_idle_sec == 0) { + return 0; + } + + mutable_hub = (hybbx_circuit_hub_t *)hub; + pthread_mutex_lock(&mutable_hub->lock); + slot = &hub->slots[slot_index]; + state = slot->state; + fd = slot->fd; + last_rf = slot->last_rf_activity; + pthread_mutex_unlock(&mutable_hub->lock); + + if (state != CIRCUIT_SLOT_ACTIVE || fd < 0) { + return 0; + } + if (last_rf == 0) { + return 1; + } + + now = time(NULL); + idle_since = now - last_rf; + if (idle_since < 0) { + return 0; + } + + return (unsigned)idle_since >= min_idle_sec; +} + +time_t hybbx_circuit_hub_link_band_ready_at(const hybbx_circuit_hub_t *hub, + unsigned slot_index, + unsigned min_idle_sec) +{ + hybbx_circuit_hub_t *mutable_hub; + const hybbx_circuit_link_slot_t *slot; + circuit_slot_state_t state; + int fd; + time_t last_rf; + time_t now; + + if (hub == NULL || slot_index >= HYBBX_CIRCUIT_MAX_LINKS || + min_idle_sec == 0) { + return (time_t)-1; + } + + mutable_hub = (hybbx_circuit_hub_t *)hub; + pthread_mutex_lock(&mutable_hub->lock); + slot = &hub->slots[slot_index]; + state = slot->state; + fd = slot->fd; + last_rf = slot->last_rf_activity; + pthread_mutex_unlock(&mutable_hub->lock); + + if (state != CIRCUIT_SLOT_ACTIVE || fd < 0) { + return (time_t)-1; + } + + now = time(NULL); + if (now == (time_t)-1) { + return (time_t)-1; + } + if (last_rf == 0) { + return now; + } + + if ((unsigned)(now - last_rf) >= min_idle_sec) { + return now; + } + + return last_rf + (time_t)min_idle_sec; +} + +static hybbx_result_t circuit_plugin_init(hybbx_service_t *service) +{ + (void)service; + return HYBBX_OK; +} + +static void circuit_plugin_shutdown(void) +{ +} + +static hybbx_result_t circuit_plugin_start(const char *config) +{ + (void)config; + return HYBBX_OK; +} + +static hybbx_result_t circuit_plugin_stop(void) +{ + return HYBBX_OK; +} + +const hybbx_transport_plugin_t hybbx_plugin_circuit = { + .name = "circuit", + .kind = HYBBX_TRANSPORT_CIRCUIT, + .version = 1, + .init = circuit_plugin_init, + .shutdown = circuit_plugin_shutdown, + .start = circuit_plugin_start, + .stop = circuit_plugin_stop, + .write = circuit_transport_write, +}; |
