summaryrefslogtreecommitdiff
path: root/include/hybbx
diff options
context:
space:
mode:
Diffstat (limited to 'include/hybbx')
-rw-r--r--include/hybbx/ardop.h50
-rw-r--r--include/hybbx/auth.h117
-rw-r--r--include/hybbx/ax25.h68
-rw-r--r--include/hybbx/bandwidth_policy.h35
-rw-r--r--include/hybbx/baycom.h82
-rw-r--r--include/hybbx/broadcast.h110
-rw-r--r--include/hybbx/chat.h82
-rw-r--r--include/hybbx/circuit.h119
-rw-r--r--include/hybbx/circuit_balance.h131
-rw-r--r--include/hybbx/circuit_bridge.h46
-rw-r--r--include/hybbx/circuit_tcp.h162
-rw-r--r--include/hybbx/command.h71
-rw-r--r--include/hybbx/commands_registry.h73
-rw-r--r--include/hybbx/conference.h69
-rw-r--r--include/hybbx/config.h126
-rw-r--r--include/hybbx/crdop.h44
-rw-r--r--include/hybbx/crypto.h101
-rw-r--r--include/hybbx/crypto_config.h69
-rw-r--r--include/hybbx/daemon_wrap.h36
-rw-r--r--include/hybbx/hybbx.h10
-rw-r--r--include/hybbx/instance.h61
-rw-r--r--include/hybbx/kiss.h61
-rw-r--r--include/hybbx/limits.h136
-rw-r--r--include/hybbx/link.h86
-rw-r--r--include/hybbx/log.h64
-rw-r--r--include/hybbx/mail.h129
-rw-r--r--include/hybbx/mains_proxy.h120
-rw-r--r--include/hybbx/max25.h72
-rw-r--r--include/hybbx/messages.h58
-rw-r--r--include/hybbx/monitor.h74
-rw-r--r--include/hybbx/networks.h63
-rw-r--r--include/hybbx/packet_radio.h57
-rw-r--r--include/hybbx/password.h53
-rw-r--r--include/hybbx/plugin.h75
-rw-r--r--include/hybbx/posix_time.h21
-rw-r--r--include/hybbx/privilege.h24
-rw-r--r--include/hybbx/proxychat.h40
-rw-r--r--include/hybbx/proxymail.h73
-rw-r--r--include/hybbx/registry.h28
-rw-r--r--include/hybbx/rf_tx_pace.h16
-rw-r--r--include/hybbx/security.h31
-rw-r--r--include/hybbx/security_ban.h80
-rw-r--r--include/hybbx/service.h174
-rw-r--r--include/hybbx/session.h226
-rw-r--r--include/hybbx/socket.h35
-rw-r--r--include/hybbx/ssh.h57
-rw-r--r--include/hybbx/storage.h178
-rw-r--r--include/hybbx/telnet.h40
-rw-r--r--include/hybbx/terminal.h39
-rw-r--r--include/hybbx/texts.h84
-rw-r--r--include/hybbx/tnc.h240
-rw-r--r--include/hybbx/tnc2c.h27
-rw-r--r--include/hybbx/traffic.h59
-rw-r--r--include/hybbx/types.h42
-rw-r--r--include/hybbx/util.h143
-rw-r--r--include/hybbx/websocket.h53
56 files changed, 4420 insertions, 0 deletions
diff --git a/include/hybbx/ardop.h b/include/hybbx/ardop.h
new file mode 100644
index 0000000..b0869b0
--- /dev/null
+++ b/include/hybbx/ardop.h
@@ -0,0 +1,50 @@
+#ifndef HYBBX_ARDOP_H
+#define HYBBX_ARDOP_H
+
+/**
+ * ARDOP link adapter — HyBBX Host-Client subset (external ARDOP modem).
+ *
+ * HyBBX is never a sound-modem service. Operator runs ARDOPC or ardopcf as a
+ * separate process; this plugin speaks the TNC Host Interface over TCP.
+ * For CRDOP, use the `crdop` plugin and external CRDOPC instead.
+ */
+
+#include "hybbx/types.h"
+#include "hybbx/crdop.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/** Default ARDOPC TCP control port (data = port + 1). */
+#define HYBBX_ARDOP_DEFAULT_PORT 8515u
+
+/** Max payload per ARDOP host data frame (spec timing guidance). */
+#define HYBBX_ARDOP_DATA_MAX 2000u
+
+typedef struct hybbx_ardop_config {
+ char *ardop_host;
+ unsigned ardop_port;
+ char *mycall;
+ char *arq_bandwidth;
+ char *peer_call;
+ int listen;
+ char *circuit_host;
+ unsigned circuit_port;
+ char *link_id;
+ char *link_password;
+ char *link_role;
+ double frequency_mhz;
+ /** Experimental CRDOP preview: cb | amateur (see docs/CRDOP.md). */
+ hybbx_crdop_radio_profile_t radio_profile;
+} hybbx_ardop_config_t;
+
+hybbx_result_t hybbx_ardop_config_parse(const char *config,
+ hybbx_ardop_config_t *out);
+void hybbx_ardop_config_free(hybbx_ardop_config_t *config);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* HYBBX_ARDOP_H */
diff --git a/include/hybbx/auth.h b/include/hybbx/auth.h
new file mode 100644
index 0000000..10c27ce
--- /dev/null
+++ b/include/hybbx/auth.h
@@ -0,0 +1,117 @@
+#ifndef HYBBX_AUTH_H
+#define HYBBX_AUTH_H
+
+#include "hybbx/password.h"
+
+#include <stddef.h>
+#include <stdint.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define HYBBX_AUTH_GUEST_PREFIX_MAX 32
+
+/** Default guest name prefix: Guest1, Guest2, … */
+#define HYBBX_AUTH_DEFAULT_GUEST_PREFIX "Guest"
+
+/** Highest guest number issued (Guest1 … Guest25); max simultaneous guests. */
+#define HYBBX_GUEST_NUMBER_MAX 25u
+
+/**
+ * Synthetic user IDs for ephemeral guest sessions (not stored in user files).
+ * Slot @p n is 1 … @ref HYBBX_GUEST_NUMBER_MAX.
+ */
+#define HYBBX_GUEST_USER_ID(n) (0xffffffffffffff00ULL + (uint64_t)(n))
+
+#define HYBBX_USERNAME_MIN_LEN 4u
+#define HYBBX_USERNAME_MAX_LEN 12u
+#define HYBBX_USERNAME_MAX_DIGITS 4u
+
+/** Default Sysop account (auto-created when no Sysop exists in the user database). */
+#define HYBBX_DEFAULT_SYSOP_USERNAME "Sysop"
+#define HYBBX_SYSOP_INIT_PASSWORD_MIN 10u
+#define HYBBX_SYSOP_INIT_PASSWORD_MAX 14u
+
+/** Plain-text password length policy for new passwords (/changeme, future set-password). */
+#define HYBBX_PASSWORD_MIN_LEN 8u
+#define HYBBX_PASSWORD_MAX_LEN 24u
+
+/**
+ * User privilege levels (highest to lowest).
+ * Sysop: exactly one account on the system.
+ */
+typedef enum hybbx_user_level {
+ HYBBX_LEVEL_SYSOP = 1,
+ HYBBX_LEVEL_ADMIN = 2,
+ HYBBX_LEVEL_MOD = 3,
+ HYBBX_LEVEL_USER = 4,
+ HYBBX_LEVEL_GUEST = 5
+} hybbx_user_level_t;
+
+typedef struct hybbx_auth_config {
+ int auto_login;
+ char guest_prefix[HYBBX_AUTH_GUEST_PREFIX_MAX];
+} hybbx_auth_config_t;
+
+void hybbx_auth_config_defaults(hybbx_auth_config_t *auth);
+
+const char *hybbx_user_level_name(hybbx_user_level_t level);
+hybbx_user_level_t hybbx_user_level_parse(const char *name);
+int hybbx_user_level_is_guest(hybbx_user_level_t level);
+
+/** Non-zero when @p level is Sysop or Admin. */
+int hybbx_user_level_is_sysop_or_admin(hybbx_user_level_t level);
+
+/** Non-zero when @p level is the protected Sysop account tier. */
+int hybbx_user_level_is_sysop(hybbx_user_level_t level);
+
+/** Non-zero when @p password meets the plain-text password policy (8–24 characters). */
+int hybbx_password_plain_valid(const char *password);
+
+/**
+ * Return non-zero when @p username is valid for registration:
+ * 4–12 characters, letters (a–z), digits (max 4), and at most one `_` or one `-`
+ * (not both); case-insensitive.
+ */
+int hybbx_username_valid(const char *username, const char *guest_prefix);
+
+/** Fold @p username to lowercase in place (for case-insensitive lookup). */
+void hybbx_username_normalize(char *username);
+
+/** Presentation form for output (e.g. Sysop instead of stored sysop). */
+const char *hybbx_username_display(const char *username,
+ hybbx_user_level_t level);
+
+struct hybbx_user_record;
+
+/** User-facing display name (nickname when set, else username). */
+const char *hybbx_user_display_name(const struct hybbx_user_record *user);
+
+/**
+ * Derive a default nickname from a legacy stored username when none is set.
+ */
+void hybbx_nickname_infer(const char *stored_username,
+ char *nickname,
+ size_t nickname_len);
+
+/**
+ * Parse @p username as @c <guest_prefix><1-25> (case-insensitive prefix).
+ * Writes slot to @p slot_out. Returns 1 on match, else 0.
+ */
+int hybbx_guest_slot_from_username(const char *guest_prefix,
+ const char *username,
+ unsigned *slot_out);
+
+struct hybbx_user_record;
+
+/** Fill @p out with an ephemeral guest record for @p slot (1 … 25). */
+void hybbx_guest_fill_record(const char *guest_prefix,
+ unsigned slot,
+ struct hybbx_user_record *out);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* HYBBX_AUTH_H */
diff --git a/include/hybbx/ax25.h b/include/hybbx/ax25.h
new file mode 100644
index 0000000..753de15
--- /dev/null
+++ b/include/hybbx/ax25.h
@@ -0,0 +1,68 @@
+#ifndef HYBBX_AX25_H
+#define HYBBX_AX25_H
+
+#include "hybbx/types.h"
+
+#include <stddef.h>
+#include <stdint.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define HYBBX_AX25_CALL_MAX 6
+#define HYBBX_AX25_SSID_MAX 15
+#define HYBBX_AX25_ADDR_ENCODED 7
+#define HYBBX_AX25_MAX_DIGI 8
+#define HYBBX_AX25_FRAME_MAX 330
+#define HYBBX_AX25_PID_NOL3 0xF0u
+#define HYBBX_AX25_CTRL_UI 0x03u
+
+typedef struct hybbx_ax25_address {
+ char call[HYBBX_AX25_CALL_MAX + 1];
+ unsigned ssid;
+} hybbx_ax25_address_t;
+
+typedef struct hybbx_ax25_path {
+ hybbx_ax25_address_t dest;
+ hybbx_ax25_address_t source;
+ hybbx_ax25_address_t digi[HYBBX_AX25_MAX_DIGI];
+ unsigned digi_count;
+} hybbx_ax25_path_t;
+
+/**
+ * Parse "CALL" or "CALL-SSID" into @p out.
+ */
+hybbx_result_t hybbx_ax25_address_parse(const char *text,
+ hybbx_ax25_address_t *out);
+
+/**
+ * Encode one AX.25 address field (7 bytes).
+ * @p last non-zero on the final address field before control byte.
+ */
+void hybbx_ax25_encode_address(uint8_t *out, const hybbx_ax25_address_t *addr,
+ int last);
+
+/** CRC-CCITT (0x1021), init 0xFFFF — AX.25 FCS. */
+uint16_t hybbx_ax25_crc(const uint8_t *data, size_t len);
+
+/**
+ * Build an AX.25 UI frame (PID 0xF0) into @p out.
+ * @return frame length or 0 on error.
+ */
+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);
+
+/**
+ * Parse a UI frame. Returns payload length or 0 if not a UI frame / invalid FCS.
+ */
+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);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* HYBBX_AX25_H */
diff --git a/include/hybbx/bandwidth_policy.h b/include/hybbx/bandwidth_policy.h
new file mode 100644
index 0000000..6c12296
--- /dev/null
+++ b/include/hybbx/bandwidth_policy.h
@@ -0,0 +1,35 @@
+#ifndef HYBBX_BANDWIDTH_POLICY_H
+#define HYBBX_BANDWIDTH_POLICY_H
+
+/**
+ * Per-user bandwidth limits when a low-bandwidth circuit link is under pressure.
+ * Circuit (secondary) sessions are never targeted. Among users, AX.25 sessions
+ * are sacrificed before full-duplex TCP/telnet; within each class, newest first.
+ */
+
+#include "hybbx/circuit_balance.h"
+#include "hybbx/types.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define HYBBX_BANDWIDTH_DISCONNECT_MSG "You are disconnected by bandwidth limits."
+
+struct hybbx_service;
+
+/** Logged-in telnet and packet_radio users subject to limits (not circuit links). */
+unsigned hybbx_bandwidth_policy_user_count(struct hybbx_service *service);
+
+/**
+ * Apply pause / break / cancel / resume to online users (AX.25 before TCP, then LIFO).
+ * Circuit (secondary) sessions are never targeted. Returns users affected.
+ */
+unsigned hybbx_bandwidth_policy_apply(struct hybbx_service *service,
+ hybbx_circuit_balance_action_t action);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* HYBBX_BANDWIDTH_POLICY_H */
diff --git a/include/hybbx/baycom.h b/include/hybbx/baycom.h
new file mode 100644
index 0000000..a351e55
--- /dev/null
+++ b/include/hybbx/baycom.h
@@ -0,0 +1,82 @@
+#ifndef HYBBX_BAYCOM_H
+#define HYBBX_BAYCOM_H
+
+/**
+ * BayCom PR-Stack link adapter — Linux kernel HDLC modem (SER12 / PAR96 / EPP)
+ * or serial KISS firmware path.
+ *
+ * Native PR-Stack L2 uses Thomas Sailer kernel drivers (baycom_ser_fdx,
+ * baycom_ser_hdx, baycom_par, baycom_epp) and hdlcdrv channel access.
+ * See docs/BAYCOM.md.
+ */
+
+#include "hybbx/types.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/** Default Linux baycom_ser_fdx netdev (SER12 fullduplex). */
+#define HYBBX_BAYCOM_DEFAULT_INTERFACE "bcsf0"
+
+/** Default SER12 UART I/O base (COM1). */
+#define HYBBX_BAYCOM_DEFAULT_IOBASE 0x3f8u
+
+/** Default SER12 IRQ (COM1). */
+#define HYBBX_BAYCOM_DEFAULT_IRQ 4u
+
+/** Default SER12 on-air baud (1200 AFSK). */
+#define HYBBX_BAYCOM_DEFAULT_RADIO_BAUD 1200u
+
+typedef enum hybbx_baycom_backend {
+ HYBBX_BAYCOM_BACKEND_KERNEL = 1,
+ HYBBX_BAYCOM_BACKEND_KISS = 2,
+} hybbx_baycom_backend_t;
+
+typedef enum hybbx_baycom_modem_mode {
+ HYBBX_BAYCOM_MODE_SER12 = 1,
+ HYBBX_BAYCOM_MODE_SER12_SOFTDCD,
+ HYBBX_BAYCOM_MODE_SER12_INV,
+ HYBBX_BAYCOM_MODE_SER12HDX,
+ HYBBX_BAYCOM_MODE_PAR96,
+ HYBBX_BAYCOM_MODE_PAR96_SOFTDCD,
+ HYBBX_BAYCOM_MODE_EPP,
+} hybbx_baycom_modem_mode_t;
+
+typedef struct hybbx_baycom_config {
+ hybbx_baycom_backend_t backend;
+ hybbx_baycom_modem_mode_t mode;
+ char *interface;
+ char *kernel_module;
+ unsigned iobase;
+ unsigned irq;
+ unsigned radio_baud;
+ int kernel_autoload;
+ unsigned txdelay;
+ unsigned txtail;
+ unsigned slot;
+ unsigned persist;
+ int full_duplex;
+ char *device;
+ unsigned serial_baud;
+ char *mycall;
+ char *circuit_host;
+ unsigned circuit_port;
+ char *link_id;
+ char *link_password;
+ char *link_role;
+ char *frequency_mhz;
+} hybbx_baycom_config_t;
+
+hybbx_result_t hybbx_baycom_config_parse(const char *config,
+ hybbx_baycom_config_t *out);
+void hybbx_baycom_config_free(hybbx_baycom_config_t *config);
+
+const char *hybbx_baycom_modem_mode_name(hybbx_baycom_modem_mode_t mode);
+const char *hybbx_baycom_backend_name(hybbx_baycom_backend_t backend);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* HYBBX_BAYCOM_H */
diff --git a/include/hybbx/broadcast.h b/include/hybbx/broadcast.h
new file mode 100644
index 0000000..a4002b7
--- /dev/null
+++ b/include/hybbx/broadcast.h
@@ -0,0 +1,110 @@
+#ifndef HYBBX_BROADCAST_H
+#define HYBBX_BROADCAST_H
+
+/**
+ * HyBBX broadcast — Main instance only.
+ *
+ * User command `/broadcast` (alias `/announce`): instant `+++` message to every
+ * online local user session on this Main (telnet/SSH/WebSocket).
+ * Format: `+++ <from>: <message>` (manual Sysop/grant announce).
+ * Automatic system notices use `***` instead.
+ *
+ * `/broadcast ax25`: manual instant RF beacon — ax25_auto_message to each
+ * qualifying packet-radio link sequentially (no custom RF text).
+ *
+ * INI `[broadcast]` ax25_auto: periodic AX.25 QST UI beacon over HBX to
+ * Secondary extenders (low-bandwidth + half-duplex links). Separate from
+ * `/broadcast`; not a user command. One sequential cycle per interval (min
+ * 900 s); 180 s own-channel idle before each link TX; 180 s between links;
+ * per-link min 900 s. Busy channel defers until idle.
+ */
+
+#include "hybbx/ax25.h"
+#include "hybbx/config.h"
+#include "hybbx/limits.h"
+#include "hybbx/types.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define HYBBX_AX25_FREQUENCY_MAX 40u
+/** Local Main announce message cap. */
+#define HYBBX_BROADCAST_MESSAGE_MAX 240u
+
+#define HYBBX_BROADCAST_AUTO_MESSAGE_DEFAULT "Broadcast: @service@ online"
+
+typedef struct hybbx_ax25_frequency_table {
+ unsigned count;
+ double mhz[HYBBX_AX25_FREQUENCY_MAX];
+ char labels[HYBBX_AX25_FREQUENCY_MAX][32];
+} hybbx_ax25_frequency_table_t;
+
+typedef struct hybbx_broadcast_config {
+ int enabled;
+ int ax25_enabled;
+ int ax25_auto;
+ unsigned ax25_auto_interval_sec;
+ /** Ignored (legacy INI); links always send sequentially with link gap. */
+ unsigned ax25_auto_stagger_sec;
+ char ax25_mycall[HYBBX_AX25_CALL_MAX + 1];
+ char ax25_dest[HYBBX_AX25_CALL_MAX + 1];
+ char ax25_auto_message[HYBBX_BROADCAST_AX25_MESSAGE_MAX + 1];
+ hybbx_ax25_frequency_table_t frequencies;
+} hybbx_broadcast_config_t;
+
+struct hybbx_service;
+struct hybbx_session;
+
+void hybbx_ax25_frequency_table_clear(hybbx_ax25_frequency_table_t *table);
+
+/** Load `[ax25]` frequency1 … frequencyN (MHz) and optional labels. */
+void hybbx_ax25_frequency_apply(hybbx_ax25_frequency_table_t *table,
+ const hybbx_config_t *config);
+
+/** Compare two MHz values (tolerance for INI rounding). */
+int hybbx_ax25_frequency_match(double a_mhz, double b_mhz);
+
+void hybbx_broadcast_config_defaults(hybbx_broadcast_config_t *cfg);
+
+/** Load `[broadcast]` + `[ax25]` from INI. */
+void hybbx_broadcast_config_apply(hybbx_broadcast_config_t *cfg,
+ const hybbx_config_t *config);
+
+const hybbx_broadcast_config_t *hybbx_service_get_broadcast(
+ const struct hybbx_service *service);
+
+/**
+ * Send @p message to every online local user on this Main (Sysop `/broadcast`).
+ */
+hybbx_result_t hybbx_broadcast_announce(struct hybbx_service *service,
+ struct hybbx_session *from,
+ const char *message);
+
+/**
+ * Manual AX.25 beacon (Sysop `/broadcast ax25`): INI ax25_auto_message to each
+ * qualifying link immediately, one after another. No custom RF text.
+ */
+hybbx_result_t hybbx_broadcast_ax25_manual(struct hybbx_service *service);
+
+/**
+ * AX.25 auto-beacon over HBX (INI ax25_auto only).
+ * Link must be low-bandwidth and half-duplex (QoS).
+ */
+hybbx_result_t hybbx_broadcast_ax25(struct hybbx_service *service,
+ double frequency_mhz,
+ const char *message);
+
+/**
+ * Periodic AX.25 auto-beacon (call once per second from Main service loop).
+ */
+void hybbx_broadcast_ax25_tick(struct hybbx_service *service);
+
+/** Abort an in-flight sequential AX.25 cycle (hub stop / shutdown). */
+void hybbx_broadcast_ax25_seq_cancel(void);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* HYBBX_BROADCAST_H */
diff --git a/include/hybbx/chat.h b/include/hybbx/chat.h
new file mode 100644
index 0000000..4f03cb4
--- /dev/null
+++ b/include/hybbx/chat.h
@@ -0,0 +1,82 @@
+#ifndef HYBBX_CHAT_H
+#define HYBBX_CHAT_H
+
+#include "hybbx/config.h"
+#include "hybbx/types.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct hybbx_service;
+struct hybbx_session;
+
+/** Hard limit on chat channels (also the default configured count). */
+#define HYBBX_CHAT_CHANNEL_MAX 10u
+
+#define HYBBX_CHAT_CHANNEL_NAME_MAX 32
+
+#define HYBBX_DEFAULT_CHAT_CHANNELS HYBBX_CHAT_CHANNEL_MAX
+
+/** Maximum characters per chat message line (fits 80-column wrap). */
+#define HYBBX_CHAT_MESSAGE_MAX 72u
+
+typedef struct hybbx_chat_config {
+ /** Active channels (1 … @ref HYBBX_CHAT_CHANNEL_MAX). */
+ unsigned channel_count;
+ /** Maximum characters per chat message line. */
+ unsigned message_max;
+ /**
+ * Channel names (index 0 = channel 1). Default: Channel1 … Channel10.
+ * Override with [chat] channel1 … channel10 in INI.
+ */
+ char names[HYBBX_CHAT_CHANNEL_MAX][HYBBX_CHAT_CHANNEL_NAME_MAX];
+} hybbx_chat_config_t;
+
+void hybbx_chat_config_defaults(hybbx_chat_config_t *chat);
+
+/** Load `[chat]` settings from an INI file. */
+void hybbx_chat_config_apply(hybbx_chat_config_t *chat,
+ const hybbx_config_t *config);
+
+const hybbx_chat_config_t *hybbx_service_get_chat(const struct hybbx_service *service);
+
+/** Return channel name for @p channel_index (1-based), or NULL when invalid. */
+const char *hybbx_chat_channel_name(const hybbx_chat_config_t *chat,
+ unsigned channel_index);
+
+/**
+ * Resolve @p spec as channel number (1-based) or name (case-insensitive).
+ * @p out_index receives 1-based channel index on success.
+ */
+hybbx_result_t hybbx_chat_resolve_channel(const hybbx_chat_config_t *chat,
+ const char *spec,
+ unsigned *out_index);
+
+/** List configured channels on @p session. */
+void hybbx_chat_list_channels(struct hybbx_session *session,
+ const hybbx_chat_config_t *chat);
+
+/** Broadcast a chat line within the sender's channel.
+ * Sender sees "ME: …"; others see "<username>: …".
+ */
+hybbx_result_t hybbx_chat_post(struct hybbx_service *service,
+ struct hybbx_session *from,
+ const char *message);
+
+/** List display names in the requester's current chat channel only. */
+void hybbx_chat_show_channel(struct hybbx_service *service,
+ struct hybbx_session *session);
+
+/**
+ * List all users in public chat channels as nick@ChannelN, sorted by channel,
+ * wrapped at 80 columns (conference sessions excluded).
+ */
+void hybbx_chat_show_all(struct hybbx_service *service,
+ struct hybbx_session *session);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* HYBBX_CHAT_H */
diff --git a/include/hybbx/circuit.h b/include/hybbx/circuit.h
new file mode 100644
index 0000000..c732602
--- /dev/null
+++ b/include/hybbx/circuit.h
@@ -0,0 +1,119 @@
+#ifndef HYBBX_CIRCUIT_H
+#define HYBBX_CIRCUIT_H
+
+/**
+ * HBX — Hybrid Bridge eXchange (v1).
+ *
+ * HyBBX internal framed protocol on the circuit TCP hub. Multiplexes
+ * link-layer payloads (AX.25 and future stacks) and application streams
+ * (terminal, proxymail, proxychat) between HyBBX processes. Header magic:
+ * `H` `B` `X`. Transport is internal TCP/IPv4+IPv6 only — the application
+ * core never sees KISS, AX.25 on-air framing, or serial.
+ *
+ * Short form in docs: **HBX** or **HBX/Circuit** — Circuit = TCP hub
+ * (:7323), HBX = framing on top.
+ */
+
+#include "hybbx/ax25.h"
+#include "hybbx/types.h"
+
+#include <stddef.h>
+#include <stdint.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define HYBBX_CIRCUIT_MAGIC_0 'H'
+#define HYBBX_CIRCUIT_MAGIC_1 'B'
+#define HYBBX_CIRCUIT_MAGIC_2 'X'
+#define HYBBX_CIRCUIT_VERSION 0x01u
+
+#define HYBBX_CIRCUIT_HEADER_SIZE 12u
+#define HYBBX_CIRCUIT_MAX_PAYLOAD 4096u
+#define HYBBX_CIRCUIT_MAX_FRAME (HYBBX_CIRCUIT_HEADER_SIZE + HYBBX_CIRCUIT_MAX_PAYLOAD)
+
+/** Default internal circuit TCP port (loopback). */
+#define HYBBX_CIRCUIT_DEFAULT_PORT 7323u
+
+/**
+ * Protocol identifiers — extensible for future link stacks.
+ * Values 0x01..0x7F: link-layer payloads; 0x80..0xFF: application streams.
+ */
+typedef enum hybbx_circuit_proto {
+ HYBBX_CIRCUIT_PROTO_AX25 = 0x01, /**< Raw AX.25 frame incl. FCS */
+ HYBBX_CIRCUIT_PROTO_AX25_UI = 0x02, /**< Masked UI: path + payload */
+ HYBBX_CIRCUIT_PROTO_LINK_AUTH = 0x03, /**< Edge link password auth (no ping) */
+ HYBBX_CIRCUIT_PROTO_LINK_AUTH_ACK = 0x04,/**< Central auth response */
+ HYBBX_CIRCUIT_PROTO_FLOW_CTRL = 0x05, /**< Load-balance pause/break/cancel */
+ HYBBX_CIRCUIT_PROTO_TERMINAL = 0x10, /**< BBX / terminal byte stream */
+ HYBBX_CIRCUIT_PROTO_PROXY_MAIL = 0x80, /**< Inter-Main proxymail (service only) */
+ HYBBX_CIRCUIT_PROTO_PROXY_CHAT = 0x81, /**< Inter-Main proxychat (service only) */
+ HYBBX_CIRCUIT_PROTO_RESERVED_APRS = 0x20,
+ HYBBX_CIRCUIT_PROTO_RESERVED_NETROM = 0x21
+} hybbx_circuit_proto_t;
+
+typedef enum hybbx_circuit_flags {
+ HYBBX_CIRCUIT_FLAG_NONE = 0x0000,
+ HYBBX_CIRCUIT_FLAG_RX = 0x0001, /**< Frame originated on RF RX */
+ HYBBX_CIRCUIT_FLAG_TX = 0x0002, /**< Frame destined for RF TX */
+ HYBBX_CIRCUIT_FLAG_PATH = 0x0004, /**< AX.25_UI payload carries path */
+ HYBBX_CIRCUIT_FLAG_G3RUH_FSK = 0x0008 /**< G3RUH 9600 FSK on-air (vs AFSK) */
+} hybbx_circuit_flags_t;
+
+typedef void (*hybbx_circuit_frame_cb)(hybbx_circuit_proto_t proto,
+ uint16_t flags,
+ const uint8_t *payload, size_t len,
+ void *userdata);
+
+typedef struct hybbx_circuit_decoder {
+ uint8_t buf[HYBBX_CIRCUIT_MAX_FRAME];
+ size_t len;
+ size_t need;
+ int have_header;
+ hybbx_circuit_proto_t proto;
+ uint16_t flags;
+} hybbx_circuit_decoder_t;
+
+void hybbx_circuit_decoder_init(hybbx_circuit_decoder_t *dec);
+
+void hybbx_circuit_decoder_feed(hybbx_circuit_decoder_t *dec,
+ const uint8_t *data, size_t len,
+ hybbx_circuit_frame_cb cb, void *userdata);
+
+size_t hybbx_circuit_encode(hybbx_circuit_proto_t proto, uint16_t flags,
+ const uint8_t *payload, size_t payload_len,
+ uint8_t *out, size_t out_cap);
+
+size_t hybbx_circuit_encode_ax25(const uint8_t *frame, size_t frame_len,
+ uint16_t flags,
+ uint8_t *out, size_t out_cap);
+
+size_t hybbx_circuit_encode_ax25_ui(const hybbx_ax25_path_t *path,
+ const uint8_t *payload, size_t payload_len,
+ uint16_t flags,
+ uint8_t *out, size_t out_cap);
+
+size_t hybbx_circuit_encode_terminal(const char *data, size_t len,
+ uint8_t *out, size_t out_cap);
+
+/** Encode LINK_AUTH or LINK_AUTH_ACK line-oriented payload. */
+size_t hybbx_circuit_encode_link_msg(hybbx_circuit_proto_t proto,
+ const char *payload, size_t payload_len,
+ uint8_t *out, size_t out_cap);
+
+/**
+ * Unpack AX.25_UI masked payload into path + UI bytes.
+ * @return UI payload length or 0 on error.
+ */
+size_t hybbx_circuit_unpack_ax25_ui(const uint8_t *payload, size_t payload_len,
+ hybbx_ax25_path_t *path,
+ uint8_t *ui, size_t ui_cap);
+
+const char *hybbx_circuit_proto_name(hybbx_circuit_proto_t proto);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* HYBBX_CIRCUIT_H */
diff --git a/include/hybbx/circuit_balance.h b/include/hybbx/circuit_balance.h
new file mode 100644
index 0000000..80edbb0
--- /dev/null
+++ b/include/hybbx/circuit_balance.h
@@ -0,0 +1,131 @@
+#ifndef HYBBX_CIRCUIT_BALANCE_H
+#define HYBBX_CIRCUIT_BALANCE_H
+
+/**
+ * HBX circuit auto load-balancing — pace and stabilize mixed link speeds,
+ * duplex modes, and sync/async downlink traffic toward edge adapters.
+ *
+ * Escalation (low-bandwidth links): pause → break → cancel connection.
+ * A few seconds of lag on slow links is normal; the balancer auto-stabilizes.
+ */
+
+#include "hybbx/circuit.h"
+#include "hybbx/link.h"
+#include "hybbx/types.h"
+
+#include <stddef.h>
+#include <stdint.h>
+#include <time.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define HYBBX_CIRCUIT_BALANCE_QUEUE_MAX 64u
+#define HYBBX_CIRCUIT_BALANCE_DEFAULT_BAUD 115200u
+#define HYBBX_CIRCUIT_BALANCE_LOW_BAUD_THRESHOLD 9600u
+
+typedef enum hybbx_circuit_bandwidth {
+ HYBBX_CIRCUIT_BW_HIGH = 0,
+ HYBBX_CIRCUIT_BW_LOW = 1
+} hybbx_circuit_bandwidth_t;
+
+typedef enum hybbx_circuit_duplex {
+ HYBBX_CIRCUIT_DUPLEX_UNSET = 0,
+ HYBBX_CIRCUIT_DUPLEX_HALF = 1,
+ HYBBX_CIRCUIT_DUPLEX_FULL = 2
+} hybbx_circuit_duplex_t;
+
+typedef enum hybbx_circuit_balance_action {
+ HYBBX_CIRCUIT_BAL_NONE = 0,
+ HYBBX_CIRCUIT_BAL_PAUSE = 1,
+ HYBBX_CIRCUIT_BAL_BREAK = 2,
+ HYBBX_CIRCUIT_BAL_CANCEL = 3,
+ HYBBX_CIRCUIT_BAL_RESUME = 4
+} hybbx_circuit_balance_action_t;
+
+typedef enum hybbx_circuit_balance_tick_result {
+ HYBBX_CIRCUIT_BAL_TICK_OK = 0,
+ HYBBX_CIRCUIT_BAL_TICK_CANCEL_LINK = 1
+} hybbx_circuit_balance_tick_result_t;
+
+typedef struct hybbx_circuit_balance_config {
+ int enabled;
+ unsigned lag_sec;
+ size_t queue_pause;
+ size_t queue_break;
+ size_t queue_cancel;
+} hybbx_circuit_balance_config_t;
+
+typedef struct hybbx_circuit_link_profile {
+ hybbx_circuit_bandwidth_t bandwidth;
+ hybbx_circuit_duplex_t duplex;
+ unsigned baud;
+ double frequency_mhz;
+} hybbx_circuit_link_profile_t;
+
+typedef struct hybbx_circuit_link_qos {
+ unsigned baud;
+ int duplex;
+ const char *bandwidth;
+ const char *frequency_mhz;
+} hybbx_circuit_link_qos_t;
+
+typedef hybbx_result_t (*hybbx_circuit_balance_send_fn)(void *ctx,
+ const uint8_t *frame,
+ size_t len);
+
+typedef void (*hybbx_circuit_balance_flow_fn)(void *ctx,
+ hybbx_circuit_balance_action_t action,
+ const char *reason);
+
+typedef struct hybbx_circuit_balance hybbx_circuit_balance_t;
+
+void hybbx_circuit_balance_config_defaults(hybbx_circuit_balance_config_t *cfg);
+
+void hybbx_circuit_link_profile_from_auth(const hybbx_link_auth_t *auth,
+ hybbx_circuit_link_profile_t *out);
+
+const char *hybbx_circuit_balance_action_name(hybbx_circuit_balance_action_t action);
+
+hybbx_result_t hybbx_circuit_flow_ctrl_parse(const char *payload, size_t len,
+ hybbx_circuit_balance_action_t *action_out,
+ char *reason_out, size_t reason_cap);
+
+size_t hybbx_circuit_flow_ctrl_format(hybbx_circuit_balance_action_t action,
+ const char *reason,
+ char *out, size_t out_cap);
+
+hybbx_circuit_balance_t *hybbx_circuit_balance_create(
+ const hybbx_circuit_balance_config_t *cfg);
+
+void hybbx_circuit_balance_destroy(hybbx_circuit_balance_t *bal);
+
+void hybbx_circuit_balance_set_profile(hybbx_circuit_balance_t *bal,
+ const hybbx_circuit_link_profile_t *profile);
+
+hybbx_circuit_balance_action_t hybbx_circuit_balance_action(
+ const hybbx_circuit_balance_t *bal);
+
+size_t hybbx_circuit_balance_queued_bytes(const hybbx_circuit_balance_t *bal);
+
+/** De-escalate from cancel after user sacrifice — keep secondary link up. */
+void hybbx_circuit_balance_spared_cancel(hybbx_circuit_balance_t *bal);
+
+hybbx_result_t hybbx_circuit_balance_submit(hybbx_circuit_balance_t *bal,
+ const uint8_t *frame, size_t len,
+ hybbx_circuit_balance_send_fn send_fn,
+ void *send_ctx,
+ hybbx_circuit_balance_flow_fn flow_fn,
+ void *flow_ctx);
+
+hybbx_circuit_balance_tick_result_t hybbx_circuit_balance_tick(
+ hybbx_circuit_balance_t *bal, unsigned poll_ms,
+ hybbx_circuit_balance_send_fn send_fn, void *send_ctx,
+ hybbx_circuit_balance_flow_fn flow_fn, void *flow_ctx);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* HYBBX_CIRCUIT_BALANCE_H */
diff --git a/include/hybbx/circuit_bridge.h b/include/hybbx/circuit_bridge.h
new file mode 100644
index 0000000..1eb24b4
--- /dev/null
+++ b/include/hybbx/circuit_bridge.h
@@ -0,0 +1,46 @@
+#ifndef HYBBX_CIRCUIT_BRIDGE_H
+#define HYBBX_CIRCUIT_BRIDGE_H
+
+/**
+ * Main-side bridge registry for remote Secondaries — one entry per
+ * [transport.packet_radioN], [transport.ardopN], or [transport.crdopN] section
+ * (link metadata; RF runs on the Secondary host).
+ * Secondaries (edge extenders/repeaters, not telnet users or local Main transports)
+ * authenticate with matching link_id + link_password over the HBX/TCP circuit hub.
+ */
+
+#include "hybbx/config.h"
+#include "hybbx/link.h"
+#include "hybbx/limits.h"
+#include "hybbx/types.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef struct hybbx_circuit_bridge_entry {
+ char link_id[HYBBX_LINK_ID_MAX];
+ char link_password[128];
+ char link_role[HYBBX_LINK_ROLE_MAX];
+ double frequency_mhz;
+} hybbx_circuit_bridge_entry_t;
+
+typedef struct hybbx_circuit_bridge_registry {
+ hybbx_circuit_bridge_entry_t entries[HYBBX_CIRCUIT_MAX_LINKS];
+ unsigned count;
+} hybbx_circuit_bridge_registry_t;
+
+void hybbx_circuit_bridge_clear(hybbx_circuit_bridge_registry_t *reg);
+
+/** Load transport.packet_radioN, transport.ardopN, and transport.crdopN sections. */
+hybbx_result_t hybbx_circuit_bridge_load(hybbx_circuit_bridge_registry_t *reg,
+ const hybbx_config_t *config);
+
+const hybbx_circuit_bridge_entry_t *hybbx_circuit_bridge_find(
+ const hybbx_circuit_bridge_registry_t *reg, const char *link_id);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* HYBBX_CIRCUIT_BRIDGE_H */
diff --git a/include/hybbx/circuit_tcp.h b/include/hybbx/circuit_tcp.h
new file mode 100644
index 0000000..900c379
--- /dev/null
+++ b/include/hybbx/circuit_tcp.h
@@ -0,0 +1,162 @@
+#ifndef HYBBX_CIRCUIT_TCP_H
+#define HYBBX_CIRCUIT_TCP_H
+
+/**
+ * HBX/Circuit TCP hub on Main (Circuit = TCP listener :7323; HBX = framed
+ * payloads on that stream). Remote Secondary processes connect via LINK_AUTH.
+ * Secondaries are separate edge machines (extenders/repeaters), not telnet users
+ * or local [transport.*] adapters on Main.
+ * Hub: [circuit] bind/port. Client: hybbx_circuit_link_* + LINK_AUTH.
+ * All inter-node paths (Secondary, mains_proxy mesh, future relays) use this
+ * client pattern only — never a direct socket to a remote HyBBX Main.
+ * Formal name: HBX — Hybrid Bridge eXchange (v1); see docs/TOPOLOGY.md.
+ */
+
+#include "hybbx/circuit.h"
+#include "hybbx/circuit_balance.h"
+#include "hybbx/circuit_bridge.h"
+#include "hybbx/service.h"
+#include "hybbx/types.h"
+#include "hybbx/limits.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef struct hybbx_circuit_hub hybbx_circuit_hub_t;
+
+typedef struct hybbx_circuit_config {
+ char bind4[64];
+ char bind6[64];
+ unsigned port;
+ int ipv4;
+ int ipv6;
+ char link_password[128];
+ unsigned link_stale_days;
+ int link_auth;
+ char data_path[HYBBX_PATH_MAX];
+ char config_path[HYBBX_PATH_MAX];
+ unsigned max_links;
+ hybbx_circuit_bridge_registry_t bridge;
+ hybbx_circuit_balance_config_t balance;
+} hybbx_circuit_config_t;
+
+/** Internal transport plugin used for sessions bridged over the circuit hub. */
+extern const hybbx_transport_plugin_t hybbx_plugin_circuit;
+
+void hybbx_circuit_config_defaults(hybbx_circuit_config_t *cfg);
+
+hybbx_circuit_hub_t *hybbx_circuit_hub_create(hybbx_service_t *service);
+void hybbx_circuit_hub_destroy(hybbx_circuit_hub_t *hub);
+
+hybbx_result_t hybbx_circuit_hub_start(hybbx_circuit_hub_t *hub,
+ const hybbx_circuit_config_t *cfg);
+
+void hybbx_circuit_hub_stop(hybbx_circuit_hub_t *hub);
+
+int hybbx_circuit_hub_running(const hybbx_circuit_hub_t *hub);
+
+unsigned hybbx_circuit_hub_port(const hybbx_circuit_hub_t *hub);
+
+/**
+ * Send a pre-encoded HBX frame to the attached link adapter.
+ */
+hybbx_result_t hybbx_circuit_hub_send_raw(hybbx_circuit_hub_t *hub,
+ const uint8_t *frame, size_t len);
+
+hybbx_result_t hybbx_circuit_hub_send_hbx(hybbx_circuit_hub_t *hub,
+ const uint8_t *frame, size_t len);
+
+unsigned hybbx_circuit_hub_active_link_count(const hybbx_circuit_hub_t *hub);
+
+/**
+ * Send HBX to all active links. @p frequency_mhz 0 = any MHz.
+ * @p require_broadcast_qos filters to low-bandwidth + half-duplex links.
+ * Returns OK when at least one link accepted the frame.
+ */
+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);
+
+/**
+ * Send HBX to one circuit slot (sequential AX.25 broadcast).
+ * @p slot_index must be an active broadcast-QoS link when @p require_broadcast_qos.
+ */
+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);
+
+double hybbx_circuit_hub_link_frequency_mhz(const hybbx_circuit_hub_t *hub);
+
+/** Non-zero when link QoS is low-bandwidth and half-duplex (AX.25 broadcast allowed). */
+int hybbx_circuit_hub_link_broadcast_qos(const hybbx_circuit_hub_t *hub);
+
+typedef struct hybbx_circuit_broadcast_link {
+ double frequency_mhz;
+ unsigned slot_index;
+ char link_id[HYBBX_LINK_ID_MAX];
+} hybbx_circuit_broadcast_link_t;
+
+/**
+ * Active broadcast-QoS links sorted by MHz (then link_id).
+ * Returns count written to @p out (may be 0).
+ */
+unsigned hybbx_circuit_hub_broadcast_links(const hybbx_circuit_hub_t *hub,
+ hybbx_circuit_broadcast_link_t *out,
+ unsigned out_max);
+
+void hybbx_circuit_hub_prune_links(hybbx_circuit_hub_t *hub);
+
+/** Record RF activity on a link adapter (RX or TX). */
+void hybbx_circuit_hub_note_rf_activity(hybbx_circuit_hub_t *hub,
+ const char *link_id);
+
+/** Non-zero when @p slot_index has had no RF activity for @p min_idle_sec. */
+int hybbx_circuit_hub_link_band_idle(const hybbx_circuit_hub_t *hub,
+ unsigned slot_index,
+ unsigned min_idle_sec);
+
+/**
+ * Earliest time a broadcast may TX on @p slot_index (now if already idle).
+ * Returns (time_t)-1 when the slot is unavailable.
+ */
+time_t hybbx_circuit_hub_link_band_ready_at(const hybbx_circuit_hub_t *hub,
+ unsigned slot_index,
+ unsigned min_idle_sec);
+
+/** Link adapter: connect to the internal circuit hub (TCP client). */
+hybbx_result_t hybbx_circuit_link_connect(const char *host, unsigned port,
+ int *out_fd);
+
+hybbx_result_t hybbx_circuit_link_write(int fd, const uint8_t *frame,
+ size_t len);
+
+hybbx_result_t hybbx_circuit_link_read(int fd, uint8_t *buf, size_t buf_len,
+ size_t *read_len);
+
+/**
+ * Send LINK_AUTH HBX frame after TCP connect (link/repeater edge daemon
+ * toward the centralized daemon). Password only — no heartbeat/ping.
+ */
+hybbx_result_t hybbx_circuit_link_authenticate(int fd,
+ const char *password,
+ const char *role,
+ const char *id);
+
+/**
+ * LINK_AUTH with optional QoS (baud, duplex, bandwidth) for auto load-balancing.
+ */
+hybbx_result_t hybbx_circuit_link_authenticate_ex(int fd,
+ const char *password,
+ const char *role,
+ const char *id,
+ const hybbx_circuit_link_qos_t *qos);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* HYBBX_CIRCUIT_TCP_H */
diff --git a/include/hybbx/command.h b/include/hybbx/command.h
new file mode 100644
index 0000000..5d8ba5c
--- /dev/null
+++ b/include/hybbx/command.h
@@ -0,0 +1,71 @@
+#ifndef HYBBX_COMMAND_H
+#define HYBBX_COMMAND_H
+
+#include "hybbx/types.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct hybbx_service;
+struct hybbx_session;
+
+/**
+ * Input line routing scope.
+ *
+ * HyBBX accepts only lines starting with '/'. Valid forms:
+ * / or /command /cmd /commands → /help
+ * /<verb> or / <verb> → command
+ * /command <verb> or /cmd <verb> → same as / <verb>
+ *
+ * - COMMENT: starts with ';' or '#' — ignored (like empty line)
+ * - LOCAL: anything else — not HyBBX; silently ignored
+ */
+typedef enum hybbx_command_scope {
+ HYBBX_CMD_SCOPE_HYBBX = 1,
+ HYBBX_CMD_SCOPE_LOCAL = 2,
+ HYBBX_CMD_SCOPE_COMMENT = 3
+} hybbx_command_scope_t;
+
+typedef struct hybbx_parsed_command {
+ hybbx_command_scope_t scope;
+ char *line;
+ char *verb;
+ char **argv;
+ size_t argc;
+} hybbx_parsed_command_t;
+
+/**
+ * Classify a single input line.
+ * Returns HYBBX_CMD_SCOPE_HYBBX only when the first non-whitespace
+ * character is '/'.
+ */
+hybbx_command_scope_t hybbx_command_classify(const char *line);
+
+/** Non-zero when @p line is a HyBBX system command (starts with '/'). */
+int hybbx_command_is_hybbx(const char *line);
+
+/** Non-zero when @p line is a local comment (';' or '#' after whitespace). */
+int hybbx_command_is_comment(const char *line);
+
+/**
+ * Parse a trimmed command line into verb and arguments.
+ * For HyBBX commands the leading '/' is stripped from the verb.
+ */
+hybbx_result_t hybbx_command_parse(const char *line,
+ hybbx_parsed_command_t *out);
+
+void hybbx_command_free(hybbx_parsed_command_t *cmd);
+
+/**
+ * Dispatch a parsed HyBBX command (scope must be HYBBX_CMD_SCOPE_HYBBX).
+ */
+hybbx_result_t hybbx_command_dispatch(struct hybbx_service *service,
+ struct hybbx_session *session,
+ const hybbx_parsed_command_t *cmd);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* HYBBX_COMMAND_H */
diff --git a/include/hybbx/commands_registry.h b/include/hybbx/commands_registry.h
new file mode 100644
index 0000000..22b1143
--- /dev/null
+++ b/include/hybbx/commands_registry.h
@@ -0,0 +1,73 @@
+#ifndef HYBBX_COMMANDS_REGISTRY_H
+#define HYBBX_COMMANDS_REGISTRY_H
+
+#include "hybbx/auth.h"
+#include "hybbx/limits.h"
+#include "hybbx/types.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct hybbx_session;
+
+typedef struct hybbx_command_def {
+ char verb[HYBBX_CMD_VERB_MAX];
+ char group[16];
+ hybbx_user_level_t min_level;
+ hybbx_user_level_t max_level; /* 0 = no upper cap */
+ int only_level; /* HYBBX_LEVEL_* when set; else 0 */
+ char line1[HYBBX_COMMANDS_HELP_LINE_MAX];
+ char line2[HYBBX_COMMANDS_HELP_LINE_MAX];
+} hybbx_command_def_t;
+
+/** Load areas.yaml then commands.yaml from install root. Call once at service startup. */
+hybbx_result_t hybbx_commands_registry_init(void);
+
+void hybbx_commands_registry_shutdown(void);
+
+const hybbx_command_def_t *hybbx_commands_registry_find(const char *verb);
+
+/** Map alias or topic name to canonical verb; returns @p topic if unknown. */
+const char *hybbx_commands_registry_canonical(const char *topic);
+
+int hybbx_commands_registry_verb_allowed(hybbx_user_level_t level,
+ const char *verb);
+
+int hybbx_commands_registry_help_allowed(hybbx_user_level_t level,
+ const char *verb);
+
+int hybbx_commands_registry_may_userchange(hybbx_user_level_t actor,
+ hybbx_user_level_t target);
+
+int hybbx_commands_registry_may_userdelete(hybbx_user_level_t actor,
+ hybbx_user_level_t target);
+
+int hybbx_commands_registry_may_promote(hybbx_user_level_t actor,
+ hybbx_user_level_t target,
+ int target_active,
+ hybbx_user_level_t new_level);
+
+int hybbx_commands_registry_may_demote(hybbx_user_level_t actor,
+ hybbx_user_level_t target);
+
+int hybbx_commands_registry_may_delete(hybbx_user_level_t actor,
+ hybbx_user_level_t target);
+
+void hybbx_commands_registry_show_menu(struct hybbx_session *session);
+void hybbx_commands_registry_show_index(struct hybbx_session *session);
+void hybbx_commands_registry_show_aliases(struct hybbx_session *session);
+void hybbx_commands_registry_show_help(struct hybbx_session *session,
+ const char *canonical);
+
+/**
+ * List line1 for every registry command with min Admin or Sysop
+ * (the admin set a Sysop may run while staying invisible via /monitor).
+ */
+void hybbx_commands_registry_show_sysop_cmds(struct hybbx_session *session);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* HYBBX_COMMANDS_REGISTRY_H */
diff --git a/include/hybbx/conference.h b/include/hybbx/conference.h
new file mode 100644
index 0000000..53baa27
--- /dev/null
+++ b/include/hybbx/conference.h
@@ -0,0 +1,69 @@
+#ifndef HYBBX_CONFERENCE_H
+#define HYBBX_CONFERENCE_H
+
+#include "hybbx/types.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct hybbx_service;
+struct hybbx_session;
+
+#define HYBBX_CONFERENCE_TOPIC_MAX 32
+
+/** Invite rate limit: max invites per target user within the window. */
+#define HYBBX_CONFERENCE_INVITE_MAX_PER_TARGET 2u
+
+/** Invite rate limit window (seconds). */
+#define HYBBX_CONFERENCE_INVITE_WINDOW_SEC 1800u
+
+/**
+ * Send a conference invite: @p topic to @p partner (login or nickname).
+ * Partner must accept with y/n or yes/no. Max two invites per target per 30 min.
+ */
+hybbx_result_t hybbx_conference_start(struct hybbx_service *service,
+ struct hybbx_session *initiator,
+ const char *topic,
+ const char *partner);
+
+/**
+ * Monitor/sysop meet: invite @p partner with timeout, or @p force join immediately.
+ * Topic defaults to "monitor". Status lines go to initiator.
+ */
+hybbx_result_t hybbx_conference_monitor_meet(struct hybbx_service *service,
+ struct hybbx_session *initiator,
+ const char *partner,
+ int force);
+
+/** Expire timed-out invites (monitor path). Safe to call every session tick. */
+void hybbx_conference_invite_tick(struct hybbx_service *service,
+ struct hybbx_session *session);
+
+/** Non-zero when @p session has a pending conference invite to answer. */
+int hybbx_conference_invite_pending(const struct hybbx_session *session);
+
+/**
+ * Handle invite reply line (y/n, yes/no). Call for local input while invite pending.
+ * Returns HYBBX_OK when consumed.
+ */
+hybbx_result_t hybbx_conference_reply_invite(struct hybbx_service *service,
+ struct hybbx_session *session,
+ const char *line);
+
+/** Clear pending invites involving @p session (disconnect). */
+void hybbx_conference_session_closed(struct hybbx_session *session);
+
+/** Deliver a line within an active conference (sender must be in conference area). */
+hybbx_result_t hybbx_conference_post(struct hybbx_service *service,
+ struct hybbx_session *from,
+ const char *message);
+
+/** Tear down conference state when leaving the conference area. */
+void hybbx_conference_area_leaving(struct hybbx_session *session);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* HYBBX_CONFERENCE_H */
diff --git a/include/hybbx/config.h b/include/hybbx/config.h
new file mode 100644
index 0000000..c2083ab
--- /dev/null
+++ b/include/hybbx/config.h
@@ -0,0 +1,126 @@
+#ifndef HYBBX_CONFIG_H
+#define HYBBX_CONFIG_H
+
+#include "hybbx/types.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef struct hybbx_config_entry {
+ char *section;
+ char *key;
+ char *value;
+} hybbx_config_entry_t;
+
+typedef struct hybbx_config {
+ hybbx_config_entry_t *entries;
+ size_t count;
+} hybbx_config_t;
+
+/** Load configuration from an INI file. */
+hybbx_result_t hybbx_config_load(hybbx_config_t *config, const char *path);
+
+/** Release all memory held by @p config. */
+void hybbx_config_free(hybbx_config_t *config);
+
+/**
+ * Return the value for @p key in @p section, or @p default_value if missing.
+ * Both @p section and @p key are case-sensitive.
+ */
+const char *hybbx_config_get(const hybbx_config_t *config,
+ const char *section,
+ const char *key,
+ const char *default_value);
+
+/** Parse HyBBX boolean tokens (yes/no and aliases; see hybbx_parse_bool). */
+int hybbx_config_get_bool(const hybbx_config_t *config,
+ const char *section,
+ const char *key,
+ int default_value);
+
+/**
+ * Parse an unsigned decimal integer in [@p min_value, @p max_value].
+ * Returns @p default_value when missing or invalid.
+ */
+unsigned hybbx_config_get_uint(const hybbx_config_t *config,
+ const char *section,
+ const char *key,
+ unsigned default_value,
+ unsigned min_value,
+ unsigned max_value);
+
+/**
+ * Build a semicolon-separated key=value string for all keys in @p section.
+ * Caller must free the returned string.
+ */
+char *hybbx_config_format_section(const hybbx_config_t *config,
+ const char *section);
+
+/**
+ * Format all @c transport.&lt;plugin&gt; and @c transport.&lt;plugin&gt;N
+ * sections (in file order) into one start string. Multiple sections are
+ * joined with @ref HYBBX_PACKET_RADIO_INSTANCE_SEP (packet_radio only).
+ * Caller must free the returned string.
+ */
+char *hybbx_config_format_transport_sections(const hybbx_config_t *config,
+ const char *plugin_name);
+
+/**
+ * Like @ref hybbx_config_format_transport_sections for @c packet_radio, but
+ * prepends a `[max25]` key chunk when that INI section exists.
+ */
+char *hybbx_config_format_packet_radio_start(const hybbx_config_t *config);
+
+/** Like packet_radio start — prepends `[max25]` keys for baycom transport. */
+char *hybbx_config_format_baycom_start(const hybbx_config_t *config);
+
+/**
+ * Prepend @c [max25] keys as a packet_radio start prefix (before instance sep).
+ * Caller must free the returned string.
+ */
+char *hybbx_config_prepend_packet_radio_max25(const hybbx_config_t *config,
+ const char *body);
+
+/**
+ * Resolve INI section for a transport plugin: @c transport.&lt;plugin&gt; first,
+ * else the first @c transport.&lt;plugin&gt;&lt;digits&gt; section with keys
+ * (e.g. @c transport.packet_radio1). Writes the chosen name to @p out_section.
+ * Returns 1 when a numbered (or legacy) section with keys was found, else 0.
+ */
+int hybbx_config_resolve_transport_section(const hybbx_config_t *config,
+ const char *plugin_name,
+ char *out_section,
+ size_t out_size);
+
+typedef void (*hybbx_config_iter_fn)(const char *section, const char *key,
+ const char *value, void *ctx);
+
+typedef void (*hybbx_config_section_iter_fn)(const char *section, void *ctx);
+
+/** Invoke @p fn for every entry in @p config (in file order). */
+void hybbx_config_foreach(const hybbx_config_t *config,
+ hybbx_config_iter_fn fn, void *ctx);
+
+/** Invoke @p fn once per unique section name (first occurrence order). */
+void hybbx_config_foreach_section(const hybbx_config_t *config,
+ hybbx_config_section_iter_fn fn, void *ctx);
+
+/** Set or replace a key in @p config (in-memory only until saved). */
+hybbx_result_t hybbx_config_set(hybbx_config_t *config,
+ const char *section,
+ const char *key,
+ const char *value);
+
+/** Remove all keys in @p section from @p config. */
+void hybbx_config_remove_section(hybbx_config_t *config, const char *section);
+
+/** Write @p config to @p path (INI format). */
+hybbx_result_t hybbx_config_save(const hybbx_config_t *config,
+ const char *path);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* HYBBX_CONFIG_H */
diff --git a/include/hybbx/crdop.h b/include/hybbx/crdop.h
new file mode 100644
index 0000000..be98200
--- /dev/null
+++ b/include/hybbx/crdop.h
@@ -0,0 +1,44 @@
+#ifndef HYBBX_CRDOP_H
+#define HYBBX_CRDOP_H
+
+/**
+ * CRDOP — CB Radio Digital Open Protocol (Level 2, experimental).
+ *
+ * HyBBX `crdop` plugin: CB host-client bridge to external CRDOPC.
+ * ARDOP uses the `ardop` plugin with ARDOPC/ardopcf.
+ * Modem DSP stays outside HyBBX. Profile helpers shared with `ardop`.
+ */
+
+#include "hybbx/types.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef enum hybbx_crdop_radio_profile {
+ HYBBX_CRDOP_PROFILE_AMATEUR = 0,
+ HYBBX_CRDOP_PROFILE_CB = 1
+} hybbx_crdop_radio_profile_t;
+
+/** Parse radio_profile INI value (cb, amateur). */
+hybbx_crdop_radio_profile_t hybbx_crdop_profile_parse(const char *value);
+
+const char *hybbx_crdop_profile_name(hybbx_crdop_radio_profile_t profile);
+
+/** Default ARQ bandwidth string for profile when INI omits arq_bandwidth. */
+const char *hybbx_crdop_default_arq_bandwidth(hybbx_crdop_radio_profile_t profile);
+
+/** Non-zero when bandwidth string is above CB-safe ceiling (1000MAX). */
+int hybbx_crdop_bandwidth_exceeds_cb(const char *arq_bandwidth);
+
+struct hybbx_ardop_config;
+
+/** Parse `[transport.crdop]` config (CB defaults). */
+hybbx_result_t hybbx_crdop_config_parse(const char *config,
+ struct hybbx_ardop_config *out);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* HYBBX_CRDOP_H */
diff --git a/include/hybbx/crypto.h b/include/hybbx/crypto.h
new file mode 100644
index 0000000..5411e5e
--- /dev/null
+++ b/include/hybbx/crypto.h
@@ -0,0 +1,101 @@
+#ifndef HYBBX_CRYPTO_H
+#define HYBBX_CRYPTO_H
+
+#include "hybbx/types.h"
+
+#include <stddef.h>
+#include <stdint.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * HyBBX reversible cryptography.
+ *
+ * Defaults use bundled in-tree sources (tinyaes, monocypher). Backends are
+ * selectable system-wide via `[crypto]` in hybbx.ini when optional OpenSSL or
+ * libsodium support is enabled at build time (see hybbx/crypto_config.h).
+ *
+ * Three modern AEAD schemes:
+ * 1. AES-256-GCM (NIST, TLS, IPsec)
+ * 2. XChaCha20-Poly1305 (extended-nonce ChaCha, Monocypher)
+ * 3. X25519 + XChaCha20-Poly1305 sealed messages (ECIES-style, Monocypher)
+ *
+ * Password hashing (one-way) remains in hybbx/password.h (SHA-256 / MD5).
+ */
+
+typedef enum hybbx_crypto_alg {
+ HYBBX_CRYPTO_AES_256_GCM = 1,
+ HYBBX_CRYPTO_XCHACHA20_POLY1305 = 2,
+ HYBBX_CRYPTO_X25519_AEAD = 3
+} hybbx_crypto_alg_t;
+
+#define HYBBX_CRYPTO_KEY_SIZE 32
+#define HYBBX_CRYPTO_TAG_SIZE 16
+#define HYBBX_CRYPTO_AES_GCM_NONCE 12
+#define HYBBX_CRYPTO_XCHACHA_NONCE 24
+#define HYBBX_CRYPTO_X25519_PUBLIC_KEY 32
+#define HYBBX_CRYPTO_X25519_SECRET_KEY 32
+/** Sealed blob overhead: ephemeral_pk(32) + nonce(24) + tag(16). */
+#define HYBBX_CRYPTO_X25519_SEAL_OVERHEAD 72
+
+/** Fill @p buf with system random bytes. */
+hybbx_result_t hybbx_crypto_random(uint8_t *buf, size_t len);
+
+/** Human-readable cipher name. */
+const char *hybbx_crypto_alg_name(hybbx_crypto_alg_t alg);
+
+/** Required nonce length for @p alg (0 for X25519 sealed mode). */
+size_t hybbx_crypto_nonce_size(hybbx_crypto_alg_t alg);
+
+/**
+ * Symmetric AEAD encrypt (algorithms 1 and 2).
+ * @p ciphertext length equals @p plaintext_len; authentication tag is separate.
+ */
+hybbx_result_t hybbx_crypto_encrypt(hybbx_crypto_alg_t alg,
+ const uint8_t key[HYBBX_CRYPTO_KEY_SIZE],
+ const uint8_t *nonce, size_t nonce_len,
+ const uint8_t *aad, size_t aad_len,
+ const uint8_t *plaintext,
+ size_t plaintext_len,
+ uint8_t *ciphertext,
+ uint8_t tag[HYBBX_CRYPTO_TAG_SIZE]);
+
+/** Symmetric AEAD decrypt; returns HYBBX_ERR_DENIED when the tag is invalid. */
+hybbx_result_t hybbx_crypto_decrypt(hybbx_crypto_alg_t alg,
+ const uint8_t key[HYBBX_CRYPTO_KEY_SIZE],
+ const uint8_t *nonce, size_t nonce_len,
+ const uint8_t *aad, size_t aad_len,
+ const uint8_t *ciphertext,
+ size_t ciphertext_len,
+ uint8_t *plaintext,
+ const uint8_t tag[HYBBX_CRYPTO_TAG_SIZE]);
+
+/** Generate an X25519 key pair (Montgomery curve, RFC 7748). */
+hybbx_result_t hybbx_crypto_x25519_keypair(
+ uint8_t public_key[HYBBX_CRYPTO_X25519_PUBLIC_KEY],
+ uint8_t secret_key[HYBBX_CRYPTO_X25519_SECRET_KEY]);
+
+/**
+ * Asymmetric seal for @p recipient_pk.
+ * Output: ephemeral_pk || nonce || ciphertext || tag.
+ */
+hybbx_result_t hybbx_crypto_x25519_seal(
+ const uint8_t recipient_pk[HYBBX_CRYPTO_X25519_PUBLIC_KEY],
+ const uint8_t *aad, size_t aad_len,
+ const uint8_t *plaintext, size_t plaintext_len,
+ uint8_t *sealed, size_t *sealed_len, size_t sealed_cap);
+
+/** Open a sealed message with @p recipient_sk. */
+hybbx_result_t hybbx_crypto_x25519_open(
+ const uint8_t recipient_sk[HYBBX_CRYPTO_X25519_SECRET_KEY],
+ const uint8_t *aad, size_t aad_len,
+ const uint8_t *sealed, size_t sealed_len,
+ uint8_t *plaintext, size_t *plaintext_len, size_t plaintext_cap);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* HYBBX_CRYPTO_H */
diff --git a/include/hybbx/crypto_config.h b/include/hybbx/crypto_config.h
new file mode 100644
index 0000000..ec1f61d
--- /dev/null
+++ b/include/hybbx/crypto_config.h
@@ -0,0 +1,69 @@
+#ifndef HYBBX_CRYPTO_CONFIG_H
+#define HYBBX_CRYPTO_CONFIG_H
+
+#include "hybbx/types.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct hybbx_config;
+
+/** Password hashing backend for new hashes and verification. */
+typedef enum hybbx_password_hash_backend {
+ HYBBX_PASSWORD_HASH_TINYSHA256 = 1,
+ HYBBX_PASSWORD_HASH_OPENSSL = 2
+} hybbx_password_hash_backend_t;
+
+/** AES-256-GCM implementation backend. */
+typedef enum hybbx_aes_gcm_backend {
+ HYBBX_AES_GCM_TINYAES = 1,
+ HYBBX_AES_GCM_OPENSSL = 2
+} hybbx_aes_gcm_backend_t;
+
+/** XChaCha20-Poly1305 implementation backend. */
+typedef enum hybbx_chacha_backend {
+ HYBBX_CHACHA_MONOCYPHER = 1,
+ HYBBX_CHACHA_LIBSODIUM = 2
+} hybbx_chacha_backend_t;
+
+/** X25519 key agreement backend (sealed-box mode). */
+typedef enum hybbx_x25519_backend {
+ HYBBX_X25519_MONOCYPHER = 1,
+ HYBBX_X25519_LIBSODIUM = 2
+} hybbx_x25519_backend_t;
+
+/** OS random bytes source. */
+typedef enum hybbx_random_backend {
+ HYBBX_RANDOM_SYSTEM = 1,
+ HYBBX_RANDOM_OPENSSL = 2
+} hybbx_random_backend_t;
+
+typedef struct hybbx_crypto_config {
+ hybbx_password_hash_backend_t password_hash;
+ hybbx_aes_gcm_backend_t aes_gcm;
+ hybbx_chacha_backend_t chacha;
+ hybbx_x25519_backend_t x25519;
+ hybbx_random_backend_t random;
+} hybbx_crypto_config_t;
+
+/** Bundled defaults: tinysha256, tinyaes, monocypher, system random. */
+void hybbx_crypto_config_defaults(hybbx_crypto_config_t *cfg);
+
+/** Load `[crypto]` from @p config (missing keys keep defaults). */
+void hybbx_crypto_config_apply(const struct hybbx_config *config);
+
+/** Active system-wide crypto settings (defaults until apply is called). */
+const hybbx_crypto_config_t *hybbx_crypto_config_get(void);
+
+const char *hybbx_password_hash_backend_name(hybbx_password_hash_backend_t b);
+const char *hybbx_aes_gcm_backend_name(hybbx_aes_gcm_backend_t b);
+const char *hybbx_chacha_backend_name(hybbx_chacha_backend_t b);
+const char *hybbx_x25519_backend_name(hybbx_x25519_backend_t b);
+const char *hybbx_random_backend_name(hybbx_random_backend_t b);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* HYBBX_CRYPTO_CONFIG_H */
diff --git a/include/hybbx/daemon_wrap.h b/include/hybbx/daemon_wrap.h
new file mode 100644
index 0000000..0144e5c
--- /dev/null
+++ b/include/hybbx/daemon_wrap.h
@@ -0,0 +1,36 @@
+#ifndef HYBBX_DAEMON_WRAP_H
+#define HYBBX_DAEMON_WRAP_H
+
+#include "hybbx/types.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define HYBBX_DAEMON_DEFAULT_SESSION "hybbxd"
+#define HYBBX_DAEMON_WRAP_ENV "HYBBXD_WRAPPED"
+
+typedef struct hybbx_daemon_launch_opts {
+ int foreground;
+ int attach;
+ int use_screen;
+ int use_tmux;
+ char session[32];
+} hybbx_daemon_launch_opts_t;
+
+void hybbx_daemon_launch_opts_defaults(hybbx_daemon_launch_opts_t *opts);
+
+/**
+ * Handle --screen / --tmux / --attach before the service loop.
+ * Spawns or attaches via GNU screen or tmux when requested.
+ * Returns HYBBX_OK to continue a foreground run; otherwise exits the process.
+ */
+hybbx_result_t hybbx_daemon_apply_launch_opts(const hybbx_daemon_launch_opts_t *opts,
+ const char *binary_path,
+ int argc, char **argv);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* HYBBX_DAEMON_WRAP_H */
diff --git a/include/hybbx/hybbx.h b/include/hybbx/hybbx.h
new file mode 100644
index 0000000..e086c2a
--- /dev/null
+++ b/include/hybbx/hybbx.h
@@ -0,0 +1,10 @@
+#ifndef HYBBX_H
+#define HYBBX_H
+
+#define HYBBX_VERSION_MAJOR 2
+#define HYBBX_VERSION_MINOR 8
+#define HYBBX_VERSION_PATCH 0
+
+#define HYBBX_VERSION_STRING "2.8.0"
+
+#endif /* HYBBX_H */
diff --git a/include/hybbx/instance.h b/include/hybbx/instance.h
new file mode 100644
index 0000000..9ed07d3
--- /dev/null
+++ b/include/hybbx/instance.h
@@ -0,0 +1,61 @@
+#ifndef HYBBX_INSTANCE_H
+#define HYBBX_INSTANCE_H
+
+#include "hybbx/networks.h"
+#include "hybbx/types.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * Hardcoded network layout instance (compile-time per binary).
+ *
+ * hybbxd — Main (local BBX; WebSocket user path; hub + mains_proxy)
+ * hybbxsd — Secondary (other-Main peers via mains_proxy; no user BBX)
+ * hybbxpd — Proxy (hybrid bridge; no WebSocket; no in-process AX.25)
+ *
+ * SSoT: vault projects/hybbx/2026-07-16-network-layout-instances.md
+ */
+typedef enum hybbx_instance_role {
+ HYBBX_INSTANCE_MAIN = 1,
+ HYBBX_INSTANCE_SECONDARY = 2,
+ HYBBX_INSTANCE_PROXY = 3
+} hybbx_instance_role_t;
+
+hybbx_instance_role_t hybbx_instance_role(void);
+
+/**
+ * Standalone Main (hybbxd + local RF on one host).
+ * Set from INI `[instance] standalone=yes` or auto when Main INI enables local RF.
+ */
+void hybbx_instance_set_standalone(int enabled);
+int hybbx_instance_standalone(void);
+
+/** "hybbxd" / "hybbxsd" / "hybbxpd" */
+const char *hybbx_instance_binary_name(void);
+
+/** Short role label: "Main" / "Secondary" / "Proxy". */
+const char *hybbx_instance_role_name(void);
+
+/** Non-zero when this instance runs local user BBX (login/mail/chat). */
+int hybbx_instance_offers_user_bbx(void);
+
+/**
+ * Apply hard layout rules to @p networks (force/clear flags).
+ * Logs warnings when INI requested a forbidden combination.
+ * Returns HYBBX_OK, or HYBBX_ERR_INVALID when the role cannot start safely.
+ */
+hybbx_result_t hybbx_networks_enforce_instance(hybbx_networks_config_t *networks);
+
+/**
+ * Non-zero when @p plugin_name may be registered/started for this instance.
+ * Complements [networks] wanted checks.
+ */
+int hybbx_instance_plugin_allowed(const char *plugin_name);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* HYBBX_INSTANCE_H */
diff --git a/include/hybbx/kiss.h b/include/hybbx/kiss.h
new file mode 100644
index 0000000..9cd378d
--- /dev/null
+++ b/include/hybbx/kiss.h
@@ -0,0 +1,61 @@
+#ifndef HYBBX_KISS_H
+#define HYBBX_KISS_H
+
+#include "hybbx/types.h"
+
+#include <stddef.h>
+#include <stdint.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define HYBBX_KISS_FEND 0xC0u
+#define HYBBX_KISS_FESC 0xDBu
+#define HYBBX_KISS_TFEND 0xDCu
+#define HYBBX_KISS_TFESC 0xDDu
+
+typedef enum hybbx_kiss_command {
+ HYBBX_KISS_CMD_DATA = 0x00,
+ HYBBX_KISS_CMD_TXDELAY = 0x01,
+ HYBBX_KISS_CMD_PERSIST = 0x02,
+ HYBBX_KISS_CMD_SLOTTIME = 0x03,
+ HYBBX_KISS_CMD_TXTAIL = 0x04,
+ HYBBX_KISS_CMD_FULLDUPLEX = 0x05,
+ HYBBX_KISS_CMD_SETHARDWARE = 0x06
+} hybbx_kiss_command_t;
+
+#define HYBBX_KISS_MAX_FRAME 2048
+
+typedef void (*hybbx_kiss_frame_cb)(uint8_t port, const uint8_t *frame,
+ size_t len, void *userdata);
+
+typedef struct hybbx_kiss_decoder {
+ uint8_t buf[HYBBX_KISS_MAX_FRAME];
+ size_t len;
+ int in_frame;
+ int escape;
+} hybbx_kiss_decoder_t;
+
+void hybbx_kiss_decoder_init(hybbx_kiss_decoder_t *dec);
+
+/**
+ * Feed raw serial bytes; complete KISS frames invoke @p cb.
+ */
+void hybbx_kiss_decoder_feed(hybbx_kiss_decoder_t *dec,
+ const uint8_t *data, size_t len,
+ hybbx_kiss_frame_cb cb, void *userdata);
+
+/**
+ * Encode a KISS frame into @p out (must hold at least @p out_cap bytes).
+ * Returns encoded length or 0 on error.
+ */
+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);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* HYBBX_KISS_H */
diff --git a/include/hybbx/limits.h b/include/hybbx/limits.h
new file mode 100644
index 0000000..081af1f
--- /dev/null
+++ b/include/hybbx/limits.h
@@ -0,0 +1,136 @@
+#ifndef HYBBX_LIMITS_H
+#define HYBBX_LIMITS_H
+
+#include "hybbx/traffic.h"
+
+/** INI parser line buffer size. */
+#define HYBBX_CONFIG_LINE_MAX 512
+
+/** Maximum path length for files and directories. */
+#define HYBBX_PATH_MAX 512
+
+/** Relative paths under the HyBBX install root (see @ref HYBBX_ENV_ROOT). */
+#define HYBBX_DIR_DATA "data"
+#define HYBBX_DIR_TEXT "text"
+#define HYBBX_DIR_LOGS "logs"
+#define HYBBX_FILE_CONFIG "hybbx.ini"
+#define HYBBX_FILE_COMMANDS "commands.yaml"
+#define HYBBX_FILE_AREAS "areas.yaml"
+#define HYBBX_DAEMON_BINARY "hybbxd"
+
+/** Command registry (share/commands.yaml + share/areas.yaml). */
+#define HYBBX_COMMANDS_MAX 64u
+#define HYBBX_AREAS_MAX 12u
+#define HYBBX_AREAS_SUB_MAX 4u
+#define HYBBX_COMMANDS_VERBS_PER_GROUP 16u
+#define HYBBX_COMMANDS_ALIASES_MAX 48u
+#define HYBBX_COMMANDS_ALIAS_PER 8u
+#define HYBBX_COMMANDS_ALIAS_LINES 8u
+#define HYBBX_MENU_LEVELS_MAX 5u
+#define HYBBX_MENU_AREAS_PER_LEVEL 12u
+#define HYBBX_RIGHTS_TARGET_RULES_MAX 16u
+#define HYBBX_RIGHTS_PROMOTE_RULES_MAX 8u
+#define HYBBX_RIGHTS_DEMOTE_RULES_MAX 4u
+#define HYBBX_COMMANDS_HELP_LINE_MAX 96u
+#define HYBBX_COMMANDS_HEADER_MAX 128u
+
+/** Environment variable: absolute path to the HyBBX install directory. */
+#define HYBBX_ENV_ROOT "HYBBX_ROOT"
+
+#define HYBBX_CONFIG_SECTION_MAX 128
+#define HYBBX_CONFIG_KEY_MAX 128
+#define HYBBX_CONFIG_VALUE_MAX 256
+
+/** Maximum tokens per HyBBX command line (verb + args). */
+#define HYBBX_CMD_TOKEN_MAX 16
+
+#define HYBBX_CMD_VERB_MAX 32
+
+/** Maximum registered transport plugins. */
+#define HYBBX_MAX_PLUGINS 16
+
+/** Maximum length of the global session prompt (empty = no visible prompt). */
+#define HYBBX_PROMPT_MAX 32
+
+/** Maximum single allocation for duplicated strings (bytes). */
+#define HYBBX_ALLOC_MAX (256u * 1024u)
+
+/** Default maximum simultaneous online sessions (guests included). */
+#define HYBBX_DEFAULT_MAX_ONLINE 35u
+
+/** Maximum concurrent HBX circuit links (secondaries) on Main. */
+#define HYBBX_CIRCUIT_MAX_LINKS 16u
+
+/** Default [circuit] max_links when not set in INI. */
+#define HYBBX_CIRCUIT_DEFAULT_MAX_LINKS 8u
+
+/** Maximum concurrent TNC devices in one packet_radio transport. */
+#define HYBBX_PACKET_RADIO_MAX_INSTANCES 8u
+
+/** Separates multiple INI sections in one packet_radio start config. */
+#define HYBBX_PACKET_RADIO_INSTANCE_SEP '\x1e'
+
+/** Maximum concurrent BayCom modems in one baycom transport. */
+#define HYBBX_BAYCOM_MAX_INSTANCES 4u
+
+/** Separates multiple INI sections in one baycom start config. */
+#define HYBBX_BAYCOM_INSTANCE_SEP '\x1e'
+
+/** Separates multiple INI sections in one mains_proxy start config. */
+#define HYBBX_MAINS_PROXY_INSTANCE_SEP '\x1e'
+
+/** Default guest session lifetime before auto-disconnect (minutes). */
+#define HYBBX_DEFAULT_GUEST_TIMEOUT_MINUTES 30u
+
+/** Command history depth for interactive line editors. */
+#define HYBBX_HISTORY_MAX 25u
+
+/** AX.25 auto-beacon minimum interval (seconds); INI may only increase this. */
+#define HYBBX_BROADCAST_AX25_INTERVAL_MIN_SEC 900u
+
+/** Per-link minimum between any AX.25 broadcast TX (manual or auto). */
+#define HYBBX_BROADCAST_AX25_LINK_MIN_SEC 900u
+
+/** RF channel must be idle this long before an auto AX.25 beacon may TX. */
+#define HYBBX_BROADCAST_AX25_BAND_IDLE_SEC 180u
+
+/** Minimum pause between successive link TX in one broadcast (seconds). */
+#define HYBBX_BROADCAST_AX25_LINK_GAP_SEC 180u
+
+/** Retry delay when a sequential broadcast step is deferred (band busy, etc.). */
+#define HYBBX_BROADCAST_AX25_DEFER_RETRY_SEC 30u
+
+/** Default max25d TCP listen port (MAX25 max25d.ini.example). */
+#define HYBBX_MAX25_DEFAULT_PORT 7325u
+
+/** Default TCP connect timeout for max25d reachability probe (ms). */
+#define HYBBX_MAX25_PROBE_TIMEOUT_MS 3000u
+
+/** Wait up to this long for max25d when [max25] check=yes (ms). */
+#define HYBBX_MAX25_PROBE_WAIT_MS 120000u
+
+/** AX.25 on-air payload cap (1200 baud; keep UI frames short). */
+#define HYBBX_BROADCAST_AX25_MESSAGE_MAX 48u
+
+/** SQLite fallback copy suffix (see `[storage] backup_path`). */
+#define HYBBX_STORAGE_BACKUP_SUFFIX ".flb"
+
+/** Default seconds between SQLite DB fallback copies. */
+#define HYBBX_STORAGE_BACKUP_INTERVAL_DEFAULT_SEC 300u
+
+/** [security] defaults — short cool-down bans (seconds). */
+#define HYBBX_SECURITY_DEFAULT_MAXRETRY 5u
+#define HYBBX_SECURITY_DEFAULT_FINDTIME_SEC 600u
+#define HYBBX_SECURITY_DEFAULT_BANTIME_SEC 600u
+/** Abuse (excessive spam/flood) — higher bar than login failures; no ban for normal use. */
+#define HYBBX_SECURITY_DEFAULT_ABUSE_MAXRETRY 30u
+#define HYBBX_SECURITY_DEFAULT_ABUSE_FINDTIME_SEC 600u
+#define HYBBX_SECURITY_DEFAULT_RATE_LIMIT 30u
+#define HYBBX_SECURITY_DEFAULT_RATE_WINDOW_SEC 60u
+#define HYBBX_SECURITY_BAN_MAX 256u
+#define HYBBX_SECURITY_TRACK_MAX 512u
+
+/** AX.25 CALL-SSID or HBX link_id (same ban table). */
+#define HYBBX_CALLID_MAX 64u
+
+#endif /* HYBBX_LIMITS_H */
diff --git a/include/hybbx/link.h b/include/hybbx/link.h
new file mode 100644
index 0000000..3bd60d1
--- /dev/null
+++ b/include/hybbx/link.h
@@ -0,0 +1,86 @@
+#ifndef HYBBX_LINK_H
+#define HYBBX_LINK_H
+
+/**
+ * HyBBX link/repeater edge daemon registry.
+ *
+ * Roles: gateway, digipeater, repeater, link — edge daemons toward centralized
+ * daemon [circuit]. Password auth only (LINK_AUTH); stale prune link_stale_days.
+ * INI: [circuit] link_* ; data/links/<id>.ini ; [link.<id>] in hybbx.ini.
+ */
+
+#include "hybbx/types.h"
+#include "hybbx/limits.h"
+
+#include <stddef.h>
+#include <time.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define HYBBX_LINK_STALE_DAYS 10u
+#define HYBBX_LINK_ID_MAX 64
+#define HYBBX_LINK_ROLE_MAX 32
+#define HYBBX_LINK_CODE_MAX 16
+#define HYBBX_LINK_AUTH_PAYLOAD_MAX 512
+
+typedef struct hybbx_link_auth {
+ char password[128];
+ char role[HYBBX_LINK_ROLE_MAX];
+ char id[HYBBX_LINK_ID_MAX];
+ unsigned baud;
+ int duplex;
+ char bandwidth[16];
+ char frequency_mhz[16];
+} hybbx_link_auth_t;
+
+typedef struct hybbx_link_registry {
+ char dir[HYBBX_PATH_MAX];
+ char config_path[HYBBX_PATH_MAX];
+ unsigned stale_days;
+} hybbx_link_registry_t;
+
+void hybbx_link_auth_clear(hybbx_link_auth_t *auth);
+
+/**
+ * Build line-oriented LINK_AUTH payload (password=, role=, id=, optional
+ * baud=, duplex=, bandwidth= for circuit load-balancing).
+ * @return payload length or 0 on error.
+ */
+size_t hybbx_link_auth_format(const hybbx_link_auth_t *auth,
+ char *out, size_t out_cap);
+
+/** Parse LINK_AUTH payload into @p auth. Returns HYBBX_OK on success. */
+hybbx_result_t hybbx_link_auth_parse(const char *payload, size_t len,
+ hybbx_link_auth_t *auth);
+
+void hybbx_link_registry_init(hybbx_link_registry_t *reg,
+ const char *data_dir,
+ const char *config_path,
+ unsigned stale_days);
+
+/**
+ * Record successful password authentication for @p id.
+ * Creates or updates @c data/links/<id>.ini and @c [link.<id>] in hybbx.ini.
+ */
+hybbx_result_t hybbx_link_registry_touch(hybbx_link_registry_t *reg,
+ const char *id,
+ const char *role,
+ char *link_code_out,
+ size_t link_code_cap);
+
+/**
+ * Remove link entries with @c last_seen older than @p stale_days.
+ * Also drops matching @c [link.*] sections from hybbx.ini when @p config_path set.
+ */
+hybbx_result_t hybbx_link_registry_prune(hybbx_link_registry_t *reg);
+
+/** Generate a short link code (uppercase alphanumeric). */
+void hybbx_link_generate_code(char *out, size_t out_cap);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* HYBBX_LINK_H */
diff --git a/include/hybbx/log.h b/include/hybbx/log.h
new file mode 100644
index 0000000..aa70b5b
--- /dev/null
+++ b/include/hybbx/log.h
@@ -0,0 +1,64 @@
+#ifndef HYBBX_LOG_H
+#define HYBBX_LOG_H
+
+#include "hybbx/types.h"
+#include "hybbx/limits.h"
+
+#include <stddef.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * File + console log severity (least to most severe for filtering):
+ * debug → stats → info → warn (default threshold).
+ * Messages at or above the configured [log] level are emitted.
+ */
+typedef enum hybbx_log_level {
+ HYBBX_LOG_DEBUG = 0,
+ HYBBX_LOG_STATS = 1,
+ HYBBX_LOG_INFO = 2,
+ HYBBX_LOG_WARN = 3
+} hybbx_log_level_t;
+
+typedef struct hybbx_log_config {
+ int enabled;
+ char dir[HYBBX_PATH_MAX];
+ hybbx_log_level_t level;
+} hybbx_log_config_t;
+
+void hybbx_log_config_defaults(hybbx_log_config_t *cfg);
+void hybbx_log_config_apply(const struct hybbx_config *config);
+const hybbx_log_config_t *hybbx_log_config_get(void);
+
+/** Non-zero when file logging is active. */
+int hybbx_log_enabled(void);
+
+/**
+ * Copy absolute path of the current hybbx log file into @p out.
+ * Returns HYBBX_OK when logging is enabled and a path is available.
+ */
+hybbx_result_t hybbx_log_current_path(char *out, size_t out_len);
+
+const char *hybbx_log_level_name(hybbx_log_level_t level);
+hybbx_log_level_t hybbx_log_parse_level(const char *value);
+
+/** Non-zero when @p level would be written (console and file). */
+int hybbx_log_level_visible(hybbx_log_level_t level);
+
+void hybbx_log_write(hybbx_log_level_t level, const char *fmt, ...)
+ __attribute__((format(printf, 2, 3)));
+
+void hybbx_log_shutdown(void);
+
+#define hybbx_log_debug(...) hybbx_log_write(HYBBX_LOG_DEBUG, __VA_ARGS__)
+#define hybbx_log_stats(...) hybbx_log_write(HYBBX_LOG_STATS, __VA_ARGS__)
+#define hybbx_log_info(...) hybbx_log_write(HYBBX_LOG_INFO, __VA_ARGS__)
+#define hybbx_log_warn(...) hybbx_log_write(HYBBX_LOG_WARN, __VA_ARGS__)
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* HYBBX_LOG_H */
diff --git a/include/hybbx/mail.h b/include/hybbx/mail.h
new file mode 100644
index 0000000..f96e1b6
--- /dev/null
+++ b/include/hybbx/mail.h
@@ -0,0 +1,129 @@
+#ifndef HYBBX_MAIL_H
+#define HYBBX_MAIL_H
+
+#include "hybbx/config.h"
+#include "hybbx/storage.h"
+#include "hybbx/types.h"
+
+#include <time.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct hybbx_service;
+struct hybbx_session;
+
+/** Maximum stored messages per user inbox. */
+#define HYBBX_MAIL_MAX_MESSAGES 50u
+
+/** Subject line limit (80-column profile). */
+#define HYBBX_MAIL_SUBJECT_MAX 72u
+
+/** Total message body size (all lines). */
+#define HYBBX_MAIL_BODY_MAX 2048u
+
+/** Default days before recycled mail is permanently removed. */
+#define HYBBX_MAIL_DEFAULT_RECYCLE_DAYS 10u
+
+typedef struct hybbx_mail_config {
+ int enabled;
+ unsigned max_messages;
+ unsigned subject_max;
+ unsigned body_max;
+ unsigned recycle_days;
+ /** Root mail directory (typically <storage>/mail). */
+ char root[512];
+} hybbx_mail_config_t;
+
+typedef struct hybbx_mail_entry {
+ uint64_t id;
+ char from[64];
+ char subject[HYBBX_MAIL_SUBJECT_MAX];
+ time_t received_at;
+ int read;
+} hybbx_mail_entry_t;
+
+void hybbx_mail_config_defaults(hybbx_mail_config_t *mail);
+
+/** Load `[mail]` and set @p mail->root under @p storage_path. */
+void hybbx_mail_config_apply(hybbx_mail_config_t *mail,
+ const hybbx_config_t *config,
+ const char *storage_path);
+
+const hybbx_mail_config_t *hybbx_service_get_mail(const struct hybbx_service *service);
+
+/** List inbox on @p session (registered users only). */
+void hybbx_mail_list_inbox(struct hybbx_service *service,
+ struct hybbx_session *session);
+
+/**
+ * List inbox rows @p from..@p to (1-based, newest first).
+ * @p to = 0 means through the last message.
+ */
+void hybbx_mail_list_inbox_range(struct hybbx_service *service,
+ struct hybbx_session *session,
+ unsigned from, unsigned to);
+
+/**
+ * After login: if mail arrived since @p since_login, announce the count.
+ * No output when mail is disabled, the user is a guest, or the count is zero.
+ * @p since_login 0 means first login (all inbox messages count as new).
+ */
+void hybbx_mail_announce_since_last_login(struct hybbx_service *service,
+ struct hybbx_session *session,
+ time_t since_login);
+
+/**
+ * Parse @p spec as @c from-to (e.g. @c 1-15 , @c 5-20 ) or a single index.
+ * Returns 0 on invalid input.
+ */
+int hybbx_mail_parse_list_range(const char *spec,
+ unsigned *from, unsigned *to);
+
+/**
+ * Read message by 1-based list index (newest first).
+ * Marks the message as read.
+ */
+hybbx_result_t hybbx_mail_read(struct hybbx_service *service,
+ struct hybbx_session *session,
+ unsigned list_index);
+
+/** Move inbox message(s) to recycle (1-based index or range). */
+hybbx_result_t hybbx_mail_delete(struct hybbx_service *service,
+ struct hybbx_session *session,
+ unsigned list_index);
+
+hybbx_result_t hybbx_mail_delete_range(struct hybbx_service *service,
+ struct hybbx_session *session,
+ unsigned from, unsigned to);
+
+/** Permanently remove all messages in the user's recycle bin. */
+hybbx_result_t hybbx_mail_recycle_empty(struct hybbx_service *service,
+ struct hybbx_session *session);
+
+/**
+ * Deliver a message from @p from_user to @p to_user.
+ * @p to_user must exist and be an active registered account.
+ */
+hybbx_result_t hybbx_mail_deliver(struct hybbx_service *service,
+ const char *from_user,
+ const char *to_user,
+ const char *subject,
+ const char *body);
+
+/**
+ * Mail active Sysop and Admin when a guest self-registers.
+ * Body lists every field from `/register` plus assigned account metadata.
+ * No-op when mail is disabled. Individual delivery failures are ignored.
+ */
+hybbx_result_t hybbx_mail_notify_staff_registration(
+ struct hybbx_service *service,
+ const hybbx_user_registration_t *reg,
+ const hybbx_user_record_t *user);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* HYBBX_MAIL_H */
diff --git a/include/hybbx/mains_proxy.h b/include/hybbx/mains_proxy.h
new file mode 100644
index 0000000..7d91c87
--- /dev/null
+++ b/include/hybbx/mains_proxy.h
@@ -0,0 +1,120 @@
+#ifndef HYBBX_MAINS_PROXY_H
+#define HYBBX_MAINS_PROXY_H
+
+/**
+ * Main-to-Main mesh proxy — link HyBBX instances for user services only.
+ *
+ * SECURITY: inter-node mesh links MUST attach via HBX/Circuit only
+ * (hybbx_circuit_link_connect + LINK_AUTH). Never open a raw TCP socket
+ * to a remote Main or peer process — same rule as Secondary edge links.
+ *
+ * Only proxymail and proxychat service payloads cross the mesh. No user
+ * accounts, rights, or other Main data are transferred.
+ */
+
+#include "hybbx/circuit.h"
+#include "hybbx/config.h"
+#include "hybbx/link.h"
+#include "hybbx/types.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define HYBBX_MAINS_PROXY_PEER_ID_MAX 64
+#define HYBBX_MAINS_PROXY_HOST_MAX 256
+#define HYBBX_MAINS_PROXY_MAX_PEERS 8u
+
+typedef enum hybbx_mains_proxy_wire {
+ /** IP path carried on HBX circuit frames (not a direct Main socket). */
+ HYBBX_MAINS_PROXY_WIRE_CIRCUIT = 0,
+ HYBBX_MAINS_PROXY_WIRE_AX25,
+} hybbx_mains_proxy_wire_t;
+
+typedef enum hybbx_mains_proxy_duplex {
+ HYBBX_MAINS_PROXY_DUPLEX_FULL = 0,
+ HYBBX_MAINS_PROXY_DUPLEX_HALF,
+} hybbx_mains_proxy_duplex_t;
+
+typedef struct hybbx_mains_proxy_peer_config {
+ char peer_id[HYBBX_MAINS_PROXY_PEER_ID_MAX];
+ char circuit_host[HYBBX_MAINS_PROXY_HOST_MAX];
+ unsigned circuit_port;
+ char link_id[HYBBX_LINK_ID_MAX];
+ char link_password[128];
+ /** @deprecated Ignored for mesh connect — use circuit_host. */
+ char host[HYBBX_MAINS_PROXY_HOST_MAX];
+ /** @deprecated Ignored for mesh connect — use circuit_port. */
+ unsigned port;
+ hybbx_mains_proxy_wire_t wire;
+ hybbx_mains_proxy_duplex_t duplex;
+ int use_secondary;
+ int enabled;
+} hybbx_mains_proxy_peer_config_t;
+
+typedef struct hybbx_mains_proxy_mesh {
+ unsigned peer_count;
+ hybbx_mains_proxy_peer_config_t peers[HYBBX_MAINS_PROXY_MAX_PEERS];
+ int running;
+} hybbx_mains_proxy_mesh_t;
+
+struct hybbx_service;
+
+hybbx_mains_proxy_wire_t hybbx_mains_proxy_wire_parse(const char *value);
+const char *hybbx_mains_proxy_wire_name(hybbx_mains_proxy_wire_t wire);
+
+hybbx_mains_proxy_duplex_t hybbx_mains_proxy_duplex_parse(const char *value);
+const char *hybbx_mains_proxy_duplex_name(hybbx_mains_proxy_duplex_t duplex);
+
+void hybbx_mains_proxy_peer_defaults(hybbx_mains_proxy_peer_config_t *peer);
+
+/** Parse one `transport.mains_proxy` / `transport.mains_proxyN` section. */
+hybbx_result_t hybbx_mains_proxy_peer_parse(const char *config,
+ hybbx_mains_proxy_peer_config_t *out);
+
+void hybbx_mains_proxy_mesh_init(hybbx_mains_proxy_mesh_t *mesh);
+
+/**
+ * Register peers and start mesh. Existing mesh state is replaced when called
+ * again while running.
+ */
+hybbx_result_t hybbx_mains_proxy_mesh_start(struct hybbx_service *service,
+ hybbx_mains_proxy_mesh_t *mesh);
+
+void hybbx_mains_proxy_mesh_stop(hybbx_mains_proxy_mesh_t *mesh);
+
+/** Periodic mesh I/O. Call from service main loop when running. */
+void hybbx_mains_proxy_mesh_tick(struct hybbx_service *service,
+ hybbx_mains_proxy_mesh_t *mesh);
+
+/** Non-zero when the mesh relay is active with at least one live peer link. */
+int hybbx_mains_proxy_mesh_active(void);
+
+/**
+ * Deliver proxymail to a configured peer (HBX PROXY_MAIL frame).
+ * @p from_address and @p to_address use @c user@service form.
+ */
+hybbx_result_t hybbx_mains_proxy_send_mail(struct hybbx_service *service,
+ const char *from_address,
+ const char *to_address,
+ const char *subject,
+ const char *body);
+
+/** Relay a proxychat line to all connected peers (HBX PROXY_CHAT frame). */
+hybbx_result_t hybbx_mains_proxy_send_chat(struct hybbx_service *service,
+ const char *from_address,
+ const char *line);
+
+/** Handle PROXY_MAIL / PROXY_CHAT received on the circuit hub (inbound link). */
+void hybbx_mains_proxy_inbound_frame(struct hybbx_service *service,
+ hybbx_circuit_proto_t proto,
+ const uint8_t *payload, size_t len);
+
+/** Service-loop tick (implemented by plugin when built). */
+void hybbx_mains_proxy_plugin_tick(void);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* HYBBX_MAINS_PROXY_H */
diff --git a/include/hybbx/max25.h b/include/hybbx/max25.h
new file mode 100644
index 0000000..5bae5c3
--- /dev/null
+++ b/include/hybbx/max25.h
@@ -0,0 +1,72 @@
+#ifndef HYBBX_MAX25_H
+#define HYBBX_MAX25_H
+
+#include "hybbx/config.h"
+#include "hybbx/limits.h"
+#include "hybbx/types.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/** Default max25d TCP listen address. */
+#define HYBBX_MAX25_DEFAULT_HOST "127.0.0.1"
+
+typedef struct hybbx_max25_config {
+ int check;
+ char host[HYBBX_CONFIG_VALUE_MAX];
+ unsigned port;
+ unsigned timeout_ms;
+} hybbx_max25_config_t;
+
+/** Snapshot from max25d M25/1 STATUS (read-only; HyBBX never overrides). */
+typedef struct hybbx_max25_status {
+ int handshake_ok;
+ char error[16];
+ char voice[16];
+ char stack[32];
+ char serial[64];
+} hybbx_max25_status_t;
+
+void hybbx_max25_config_defaults(hybbx_max25_config_t *cfg);
+
+/** Load `[max25]` from INI. `check` defaults to `yes`. Reporting is MAX25-only. */
+void hybbx_max25_config_apply(hybbx_max25_config_t *cfg,
+ const hybbx_config_t *config);
+
+/** Parse a semicolon key=value chunk (`max25_check=…` keys). */
+void hybbx_max25_config_parse_kv(const char *max25_kv,
+ hybbx_max25_config_t *cfg);
+
+/**
+ * When @p config starts with a max25 prefix (before
+ * @ref HYBBX_PACKET_RADIO_INSTANCE_SEP), parse it and return the remainder.
+ * When no prefix is present, @p cfg->check is cleared.
+ */
+const char *hybbx_max25_config_skip_prefix(const char *config,
+ hybbx_max25_config_t *cfg);
+
+void hybbx_max25_status_clear(hybbx_max25_status_t *status);
+
+/**
+ * TCP connect + M25/1 connect handshake (`OK` + `STATUS …`).
+ * Returns @ref HYBBX_OK when max25d responds; @p status_out receives
+ * `error=` / `voice=` / `stack=` / `serial=` as reported by MAX25 (valid or invalid).
+ * Data-quality rules (3×20s passes, CALLID, min % good) live in max25d only —
+ * HyBBX accepts STATUS without override; only TCP/handshake failure is fatal.
+ */
+hybbx_result_t hybbx_max25_probe(const hybbx_max25_config_t *cfg,
+ hybbx_max25_status_t *status_out);
+
+/**
+ * When @p cfg->check is set, block until max25d responds or
+ * @ref HYBBX_MAX25_PROBE_WAIT_MS elapses.
+ * Returns @ref HYBBX_OK when max25d is reachable; @ref HYBBX_ERR_IO on timeout.
+ */
+hybbx_result_t hybbx_max25_wait_ready(const hybbx_max25_config_t *cfg);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* HYBBX_MAX25_H */
diff --git a/include/hybbx/messages.h b/include/hybbx/messages.h
new file mode 100644
index 0000000..d1c7345
--- /dev/null
+++ b/include/hybbx/messages.h
@@ -0,0 +1,58 @@
+#ifndef HYBBX_MESSAGES_H
+#define HYBBX_MESSAGES_H
+
+#include "hybbx/types.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct hybbx_session;
+
+/** Automatic system notices (login, mail, timeouts, conference status, …). */
+#define HYBBX_MSG_PREFIX_SYSTEM "*** "
+
+/** Sysop / granted manual system-wide notices (`/broadcast`, `/announce`). */
+#define HYBBX_MSG_PREFIX_SYSOP "+++ "
+
+/** Private directed messages (system or Sysop+granted); v1 half-stub. */
+#define HYBBX_MSG_PREFIX_PRIVATE "### "
+
+/**
+ * Format a system line: `*** <body>`
+ */
+hybbx_result_t hybbx_msg_format_system(char *out, size_t out_len,
+ const char *body);
+
+/**
+ * Format a Sysop/granted system-wide line: `+++ <from>: <body>`
+ * @p from_label may be NULL → `+++ <body>`
+ */
+hybbx_result_t hybbx_msg_format_sysop(char *out, size_t out_len,
+ const char *from_label,
+ const char *body);
+
+/**
+ * Format a private line: `### <from>: <body>` or `### <body>` when @p from_label is NULL.
+ */
+hybbx_result_t hybbx_msg_format_private(char *out, size_t out_len,
+ const char *from_label,
+ const char *body);
+
+hybbx_result_t hybbx_msg_send_system(struct hybbx_session *session,
+ const char *body);
+
+hybbx_result_t hybbx_msg_send_sysop(struct hybbx_session *session,
+ const char *from_label,
+ const char *body);
+
+/** Half-stub: directed private message to one session. */
+hybbx_result_t hybbx_msg_send_private(struct hybbx_session *session,
+ const char *from_label,
+ const char *body);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* HYBBX_MESSAGES_H */
diff --git a/include/hybbx/monitor.h b/include/hybbx/monitor.h
new file mode 100644
index 0000000..a4d8db5
--- /dev/null
+++ b/include/hybbx/monitor.h
@@ -0,0 +1,74 @@
+#ifndef HYBBX_MONITOR_H
+#define HYBBX_MONITOR_H
+
+#include "hybbx/limits.h"
+#include "hybbx/types.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct hybbx_config;
+struct hybbx_service;
+struct hybbx_session;
+
+#define HYBBX_MONITOR_ALLOW_MAX 16u
+#define HYBBX_MONITOR_ALLOW_NAME_MAX 64u
+
+typedef struct hybbx_monitor_config {
+ int enabled;
+ int follow_hybbx;
+ int follow_security;
+ /** When set, Sysop sessions are always hidden from /who and /online. */
+ int invisible_sysop;
+ unsigned invite_timeout_sec;
+ char allow[HYBBX_MONITOR_ALLOW_MAX][HYBBX_MONITOR_ALLOW_NAME_MAX];
+ unsigned allow_count;
+} hybbx_monitor_config_t;
+
+void hybbx_monitor_config_defaults(hybbx_monitor_config_t *cfg);
+void hybbx_monitor_config_apply(const struct hybbx_config *config);
+const hybbx_monitor_config_t *hybbx_monitor_config_get(void);
+
+/** Non-zero when [monitor] enabled=yes. */
+int hybbx_monitor_enabled(void);
+
+/** Non-zero when [monitor] invisible-sysop=yes (Sysop always /who-hidden). */
+int hybbx_monitor_invisible_sysop(void);
+
+/** Non-zero when @p username is listed in [monitor] allow=. */
+int hybbx_monitor_user_allowed(const char *username);
+
+/**
+ * Non-zero when @p session may use /monitor (Sysop/registry OR allow list).
+ * Caller still runs normal command access for registry min-level.
+ */
+int hybbx_monitor_session_may_use(const struct hybbx_session *session);
+
+/** Enable/disable monitor mode on @p session. */
+hybbx_result_t hybbx_monitor_set_active(struct hybbx_session *session, int on);
+
+int hybbx_monitor_is_active(const struct hybbx_session *session);
+
+/**
+ * Push a live line to every session with monitor mode on.
+ * Prefixes with "[monitor] " for clarity.
+ */
+void hybbx_monitor_broadcast(struct hybbx_service *service, const char *line);
+
+/** Format Username@plugin event and push live (always when any monitor on). */
+void hybbx_monitor_event(struct hybbx_service *service,
+ const char *username,
+ const char *plugin,
+ const char *detail);
+
+/** Poll log file tails (call from service loop). */
+void hybbx_monitor_tick(struct hybbx_service *service);
+
+void hybbx_monitor_shutdown(void);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* HYBBX_MONITOR_H */
diff --git a/include/hybbx/networks.h b/include/hybbx/networks.h
new file mode 100644
index 0000000..ab8e954
--- /dev/null
+++ b/include/hybbx/networks.h
@@ -0,0 +1,63 @@
+#ifndef HYBBX_NETWORKS_H
+#define HYBBX_NETWORKS_H
+
+#include "hybbx/config.h"
+#include "hybbx/types.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * Connection / link adapter switches from INI `[networks]`.
+ *
+ * Static transport depends on instance role (see hybbx/instance.h):
+ * Main — websocket (always on when built)
+ * Secondary — mains_proxy
+ * Proxy — none (optional TCP transports)
+ */
+typedef struct hybbx_networks_config {
+ /** Telnet (`transport.telnet`) — not used on Main (WebSocket only). */
+ int telnet;
+ /** AX.25 / packet radio — always forced off (MAX25-Stack owns RF). */
+ int ax25;
+ /** BayCom — always forced off (MAX25-Stack owns RF). */
+ int baycom;
+ /** ARDOP — always forced off (MAX25-Stack owns RF / soft-modem). */
+ int ardop;
+ /** CRDOP — always forced off (MAX25-Stack owns RF / soft-modem). */
+ int crdop;
+ /** SSH transport (`transport.ssh`). */
+ int ssh;
+ /** WebSocket forward-proxy (`transport.websocket`) — Main static path. */
+ int websocket;
+ /** Internal HBX circuit hub / attach. */
+ int circuit;
+ /** Main-to-Main mesh (`transport.mains_proxy`) — Secondary primary. */
+ int mains_proxy;
+} hybbx_networks_config_t;
+
+void hybbx_networks_config_defaults(hybbx_networks_config_t *networks);
+
+/** Load `[networks]` from an INI file. */
+void hybbx_networks_config_apply(hybbx_networks_config_t *networks,
+ const hybbx_config_t *config);
+
+/**
+ * Non-zero when @p plugin_name is the role's static core transport.
+ * Ignores `[transport.*] enabled` and is started when built.
+ */
+int hybbx_networks_is_static_transport(const char *plugin_name);
+
+/**
+ * Non-zero when transport @p plugin_name should be started for this config.
+ * Respects instance plugin allow-list and `[networks]` toggles.
+ */
+int hybbx_networks_transport_wanted(const char *plugin_name,
+ const hybbx_networks_config_t *networks);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* HYBBX_NETWORKS_H */
diff --git a/include/hybbx/packet_radio.h b/include/hybbx/packet_radio.h
new file mode 100644
index 0000000..51f79a0
--- /dev/null
+++ b/include/hybbx/packet_radio.h
@@ -0,0 +1,57 @@
+#ifndef HYBBX_PACKET_RADIO_H
+#define HYBBX_PACKET_RADIO_H
+
+#include "hybbx/types.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/** Default line speed for packet radio (2400 baud). */
+#define HYBBX_PACKET_RADIO_DEFAULT_BAUD 2400
+
+/** Default USB AX.25 TNC/modem device (Linux). */
+#define HYBBX_PACKET_RADIO_DEFAULT_DEVICE_USB "/dev/ttyUSB0"
+
+#if defined(_WIN32)
+#define HYBBX_PACKET_RADIO_DEFAULT_DEVICE_SERIAL "COM1"
+#elif defined(__AMIGA__)
+#define HYBBX_PACKET_RADIO_DEFAULT_DEVICE_SERIAL "serial.device"
+#else
+#define HYBBX_PACKET_RADIO_DEFAULT_DEVICE_SERIAL "/dev/ttyS0"
+#endif
+
+typedef enum hybbx_packet_radio_device_type {
+ HYBBX_PACKET_RADIO_DEVICE_USB = 1,
+ HYBBX_PACKET_RADIO_DEVICE_SERIAL = 2
+} hybbx_packet_radio_device_type_t;
+
+struct hybbx_packet_radio_config;
+
+/**
+ * Parse a semicolon-separated key=value transport config string.
+ * Missing keys receive HyBBX defaults (TNC2C, USB, KISS @ 2400 baud).
+ * Full config type: include hybbx/tnc.h.
+ */
+hybbx_result_t hybbx_packet_radio_config_parse(const char *config,
+ struct hybbx_packet_radio_config *out);
+
+void hybbx_packet_radio_config_free(struct hybbx_packet_radio_config *config);
+
+/**
+ * Non-zero when @p config starts a local TNC (device/tnc/protocol/circuit_host).
+ * Main bridge-registry rows (link_id only) return zero.
+ */
+int hybbx_packet_radio_section_is_local_edge(const char *config);
+
+/**
+ * Return non-zero when @p baud is valid for packet radio configuration.
+ * HyBBX accepts any positive host baud rate; TNC2C documents 300..38400.
+ */
+int hybbx_packet_radio_baud_valid(unsigned int baud);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* HYBBX_PACKET_RADIO_H */
diff --git a/include/hybbx/password.h b/include/hybbx/password.h
new file mode 100644
index 0000000..cc1b38e
--- /dev/null
+++ b/include/hybbx/password.h
@@ -0,0 +1,53 @@
+#ifndef HYBBX_PASSWORD_H
+#define HYBBX_PASSWORD_H
+
+#include "hybbx/types.h"
+
+#include <stddef.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/** Stored password prefix for SHA-256 (default, tinysha256). */
+#define HYBBX_PASSWORD_SHA256_PREFIX "{sha256}"
+/** Stored password prefix for legacy MD5 (verify-only fallback). */
+#define HYBBX_PASSWORD_MD5_PREFIX "{md5}"
+
+/** Max stored password field: prefix + 64 hex digits + NUL. */
+#define HYBBX_PASSWORD_STORED_MAX 96
+
+/** Non-zero when @p stored is a hashed password (sha256 or md5 prefix). */
+int hybbx_password_is_hashed(const char *stored);
+
+/** Non-zero when @p stored is plain text and should be upgraded on load. */
+int hybbx_password_is_plain(const char *stored);
+
+/**
+ * Hash @p plain into @p out as {sha256}<hex> using the configured backend
+ * (default: bundled tinysha256; see [crypto] password_hash in hybbx.ini).
+ */
+hybbx_result_t hybbx_password_hash_sha256(const char *plain,
+ char *out,
+ size_t out_size);
+
+/** Hash @p plain with the configured default algorithm (SHA-256). */
+hybbx_result_t hybbx_password_hash(const char *plain, char *out, size_t out_size);
+
+/** Return non-zero when @p provided matches @p stored (plain, sha256, or md5). */
+int hybbx_password_match(const char *stored, const char *provided);
+
+/**
+ * Fill @p out with a random password of length @p min_len … @p max_len using
+ * characters a-z and 0-9 only.
+ */
+hybbx_result_t hybbx_password_generate_alnum(char *out,
+ size_t out_size,
+ size_t min_len,
+ size_t max_len);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* HYBBX_PASSWORD_H */
diff --git a/include/hybbx/plugin.h b/include/hybbx/plugin.h
new file mode 100644
index 0000000..ea72a65
--- /dev/null
+++ b/include/hybbx/plugin.h
@@ -0,0 +1,75 @@
+#ifndef HYBBX_PLUGIN_H
+#define HYBBX_PLUGIN_H
+
+#include "hybbx/types.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct hybbx_service;
+struct hybbx_session;
+
+/**
+ * Transport plugin interface (link adapters / host-client bridges).
+ *
+ * HyBBX is plugin-only: session core + plugins. Modems, TNCs, sound-card
+ * software, ARDOPC, and CRDOPC are external — never embedded in HyBBX.
+ *
+ * Core uses TCP/IPv4+IPv6 and HBX internally. Plugins terminate the wire
+ * to external services (telnet TCP, serial TNC, ARDOP host TCP, …) and
+ * expose a byte stream to hybbx_session via the write callback.
+ */
+typedef struct hybbx_transport_plugin {
+ const char *name;
+ hybbx_transport_kind_t kind;
+ unsigned int version;
+
+ /** Called once when the plugin is loaded. */
+ hybbx_result_t (*init)(struct hybbx_service *service);
+
+ /** Called once when the plugin is unloaded. */
+ void (*shutdown)(void);
+
+ /**
+ * Start listening / accepting connections on the given bind address.
+ * @p config is a semicolon-separated key=value string built from the
+ * transport's INI section (e.g. "bind=0.0.0.0;port=2323").
+ */
+ hybbx_result_t (*start)(const char *config);
+
+ /** Stop accepting new connections; existing sessions may continue. */
+ hybbx_result_t (*stop)(void);
+
+ /**
+ * Send raw bytes to the connected client for this session.
+ * NULL → core writes to stdout (development fallback).
+ */
+ hybbx_result_t (*write)(struct hybbx_session *session,
+ const char *data, size_t len);
+
+} hybbx_transport_plugin_t;
+
+/**
+ * Session callbacks invoked by a transport plugin when a client connects.
+ */
+typedef struct hybbx_session_ops {
+ hybbx_result_t (*on_connect)(struct hybbx_session *session, void *userdata);
+ hybbx_result_t (*on_data)(struct hybbx_session *session,
+ const uint8_t *data, size_t len,
+ void *userdata);
+ void (*on_disconnect)(struct hybbx_session *session, void *userdata);
+} hybbx_session_ops_t;
+
+/** Opaque session handle passed between core and transport plugins. */
+typedef struct hybbx_session {
+ const hybbx_transport_plugin_t *transport;
+ void *transport_data;
+ void *core_data;
+} hybbx_session_t;
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* HYBBX_PLUGIN_H */
diff --git a/include/hybbx/posix_time.h b/include/hybbx/posix_time.h
new file mode 100644
index 0000000..6790120
--- /dev/null
+++ b/include/hybbx/posix_time.h
@@ -0,0 +1,21 @@
+#ifndef HYBBX_POSIX_TIME_H
+#define HYBBX_POSIX_TIME_H
+
+/*
+ * clock_gettime(CLOCK_MONOTONIC) and usleep() on glibc/Linux (GCC 15+ C99).
+ * Include before <time.h> / <unistd.h> in .c files, or rely on CMake
+ * _DEFAULT_SOURCE on Linux builds.
+ */
+#if defined(__linux__) || defined(__GLIBC__)
+#ifndef _DEFAULT_SOURCE
+#define _DEFAULT_SOURCE 1
+#endif
+#endif
+
+#include <time.h>
+
+#if !defined(_WIN32)
+#include <unistd.h>
+#endif
+
+#endif /* HYBBX_POSIX_TIME_H */
diff --git a/include/hybbx/privilege.h b/include/hybbx/privilege.h
new file mode 100644
index 0000000..df646c8
--- /dev/null
+++ b/include/hybbx/privilege.h
@@ -0,0 +1,24 @@
+#ifndef HYBBX_PRIVILEGE_H
+#define HYBBX_PRIVILEGE_H
+
+#include "hybbx/config.h"
+#include "hybbx/types.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * Apply [service] user=/group=/uid=/gid= from @p config.
+ *
+ * Linux: hybbxd must not keep running as root. When euid==0, @c user is
+ * required and privileges drop immediately via setuid/setgid.
+ * Non-root starts are unchanged (optional @c user must match effective uid).
+ */
+hybbx_result_t hybbx_privilege_apply_from_config(const hybbx_config_t *config);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* HYBBX_PRIVILEGE_H */
diff --git a/include/hybbx/proxychat.h b/include/hybbx/proxychat.h
new file mode 100644
index 0000000..74f71f2
--- /dev/null
+++ b/include/hybbx/proxychat.h
@@ -0,0 +1,40 @@
+#ifndef HYBBX_PROXYCHAT_H
+#define HYBBX_PROXYCHAT_H
+
+/**
+ * Inter-Main chat via mains_proxy.
+ *
+ * Local `/chat` stays on this Main. `/proxychat` is the mains-proxy sub-area
+ * under `/chat` (or `/chat proxychat`). Only chat lines cross the mesh — no
+ * user accounts or rights.
+ */
+
+#include "hybbx/types.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct hybbx_service;
+struct hybbx_session;
+
+/** Show proxychat area banner. */
+void hybbx_proxychat_show_banner(struct hybbx_session *session);
+
+/**
+ * Post a line in proxychat (relay when mesh is active).
+ */
+hybbx_result_t hybbx_proxychat_post(struct hybbx_service *service,
+ struct hybbx_session *session,
+ const char *line);
+
+/** Deliver a line received from a remote Main. */
+void hybbx_proxychat_receive(struct hybbx_service *service,
+ const char *from_address,
+ const char *line);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* HYBBX_PROXYCHAT_H */
diff --git a/include/hybbx/proxymail.h b/include/hybbx/proxymail.h
new file mode 100644
index 0000000..123b663
--- /dev/null
+++ b/include/hybbx/proxymail.h
@@ -0,0 +1,73 @@
+#ifndef HYBBX_PROXYMAIL_H
+#define HYBBX_PROXYMAIL_H
+
+/**
+ * Inter-Main mail via mains_proxy — separate from local `/mail`.
+ *
+ * Local mail uses flat-file or SQL under `[mail]`; proxymail uses flat files
+ * under `data/proxymail/` on each node. Only service payloads cross the mesh.
+ */
+
+#include "hybbx/mail.h"
+#include "hybbx/types.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/** `username@remote-main-service` address string. */
+#define HYBBX_PROXYMAIL_ADDRESS_MAX 128u
+#define HYBBX_PROXYMAIL_SERVICE_NAME_MAX 64u
+
+struct hybbx_service;
+struct hybbx_session;
+
+/**
+ * Parse @p address as @c user@remote-service .
+ * @return 0 when invalid.
+ */
+int hybbx_proxymail_parse_address(const char *address,
+ char *user, size_t user_len,
+ char *remote_service, size_t remote_len);
+
+/** List proxymail inbox (stub). */
+void hybbx_proxymail_list_inbox(struct hybbx_service *service,
+ struct hybbx_session *session);
+
+void hybbx_proxymail_list_inbox_range(struct hybbx_service *service,
+ struct hybbx_session *session,
+ unsigned from, unsigned to);
+
+hybbx_result_t hybbx_proxymail_read(struct hybbx_service *service,
+ struct hybbx_session *session,
+ unsigned list_index);
+
+hybbx_result_t hybbx_proxymail_delete_range(struct hybbx_service *service,
+ struct hybbx_session *session,
+ unsigned from, unsigned to);
+
+hybbx_result_t hybbx_proxymail_recycle_empty(struct hybbx_service *service,
+ struct hybbx_session *session);
+
+/**
+ * Queue outbound proxymail for mesh delivery.
+ * @p to_address must be @c user@remote-main-service .
+ */
+hybbx_result_t hybbx_proxymail_deliver(struct hybbx_service *service,
+ const char *from_user,
+ const char *to_address,
+ const char *subject,
+ const char *body);
+
+/** Store proxymail received from a remote Main. */
+void hybbx_proxymail_receive(struct hybbx_service *service,
+ const char *from_address,
+ const char *to_address,
+ const char *subject,
+ const char *body);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* HYBBX_PROXYMAIL_H */
diff --git a/include/hybbx/registry.h b/include/hybbx/registry.h
new file mode 100644
index 0000000..51577ee
--- /dev/null
+++ b/include/hybbx/registry.h
@@ -0,0 +1,28 @@
+#ifndef HYBBX_REGISTRY_H
+#define HYBBX_REGISTRY_H
+
+#include "hybbx/plugin.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/** Register a transport plugin (called at plugin init time). */
+hybbx_result_t hybbx_registry_register(const hybbx_transport_plugin_t *plugin);
+
+/** Unregister a transport plugin by name. */
+hybbx_result_t hybbx_registry_unregister(const char *name);
+
+/** Look up a registered plugin by name. Returns NULL if not found. */
+const hybbx_transport_plugin_t *hybbx_registry_find(const char *name);
+
+/** Iterate over all registered plugins. */
+typedef void (*hybbx_registry_iter_fn)(const hybbx_transport_plugin_t *plugin,
+ void *userdata);
+void hybbx_registry_foreach(hybbx_registry_iter_fn fn, void *userdata);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* HYBBX_REGISTRY_H */
diff --git a/include/hybbx/rf_tx_pace.h b/include/hybbx/rf_tx_pace.h
new file mode 100644
index 0000000..f987490
--- /dev/null
+++ b/include/hybbx/rf_tx_pace.h
@@ -0,0 +1,16 @@
+#ifndef HYBBX_RF_TX_PACE_H
+#define HYBBX_RF_TX_PACE_H
+
+/** Minimum idle time between RF frame sends (HyBBX packet radio). */
+#define HYBBX_RF_TX_MIN_GAP_MS 1500u
+
+/** Maximum continuous RF send burst (wall clock); new burst after MIN_GAP idle. */
+#define HYBBX_RF_TX_MAX_BURST_MS 15000u
+
+/** Min gap + max burst (TNC / direct serial). */
+void hybbx_rf_tx_pace(void);
+
+/** Max burst only — use when max25-bcpr enforces min gap on KISS (baycom plugin). */
+void hybbx_rf_tx_burst_guard(void);
+
+#endif
diff --git a/include/hybbx/security.h b/include/hybbx/security.h
new file mode 100644
index 0000000..f4a9476
--- /dev/null
+++ b/include/hybbx/security.h
@@ -0,0 +1,31 @@
+#ifndef HYBBX_SECURITY_H
+#define HYBBX_SECURITY_H
+
+#include "hybbx/types.h"
+
+#include <stddef.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct hybbx_config;
+
+/** Log file name under the configured logs directory. */
+#define HYBBX_SECURITY_LOG_FILE "security.log"
+
+void hybbx_security_log_config_apply(const struct hybbx_config *config);
+void hybbx_security_log_shutdown(void);
+
+/** Append one line to security.log (timestamp added). Always enabled when the log directory is available. */
+void hybbx_security_log_write(const char *fmt, ...)
+ __attribute__((format(printf, 1, 2)));
+
+/** Absolute path to security.log when the security log is ready. */
+hybbx_result_t hybbx_security_log_current_path(char *out, size_t out_len);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* HYBBX_SECURITY_H */
diff --git a/include/hybbx/security_ban.h b/include/hybbx/security_ban.h
new file mode 100644
index 0000000..9e63757
--- /dev/null
+++ b/include/hybbx/security_ban.h
@@ -0,0 +1,80 @@
+#ifndef HYBBX_SECURITY_BAN_H
+#define HYBBX_SECURITY_BAN_H
+
+#include "hybbx/types.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct hybbx_config;
+
+typedef enum hybbx_ban_backend {
+ HYBBX_BAN_BACKEND_INTERNAL = 0,
+ HYBBX_BAN_BACKEND_LOG,
+ HYBBX_BAN_BACKEND_IPTABLES,
+ HYBBX_BAN_BACKEND_NFTABLES,
+ HYBBX_BAN_BACKEND_HOSTS
+} hybbx_ban_backend_t;
+
+void hybbx_security_ban_config_apply(const struct hybbx_config *config);
+void hybbx_security_ban_shutdown(void);
+
+/** Expire bans and prune stale failure/rate records (service loop). */
+void hybbx_security_ban_tick(void);
+
+/** Non-zero when @p ip is currently banned. */
+int hybbx_security_ban_is_banned(const char *ip);
+
+/**
+ * Normalize @p in into @p out (uppercase CALL/CALL-SSID or link_id).
+ * Returns non-zero when valid.
+ */
+int hybbx_security_callid_normalize(const char *in, char *out, size_t out_cap);
+
+/** Non-zero when @p callid is banned (AX.25 callsign or HBX link_id). */
+int hybbx_security_ban_callid_is_banned(const char *callid);
+
+/**
+ * Accept gate for HBX link_id / AX.25 source before circuit or RF uplink.
+ * Returns non-zero when allowed.
+ */
+int hybbx_security_ban_callid_accept(const char *callid);
+
+/**
+ * Accept gate: rate-limit check + ban check.
+ * Returns non-zero when the connection may proceed.
+ */
+int hybbx_security_ban_accept(const char *ip);
+
+/**
+ * Same as @ref hybbx_security_ban_accept using the peer address of @p fd.
+ * Allows the connection when the peer address cannot be resolved.
+ */
+int hybbx_security_ban_accept_fd(int fd);
+
+/** Record a failed login for @p transport (telnet, ssh, websocket, …). */
+void hybbx_security_ban_login_fail(const char *ip, const char *transport);
+
+/** Record a failed HBX circuit link authentication (IP). */
+void hybbx_security_ban_link_auth_fail(const char *ip);
+
+/** Record a failed HBX circuit link authentication (link_id / CALLID). */
+void hybbx_security_ban_link_auth_fail_callid(const char *callid);
+
+/**
+ * Record excessive abuse (chat/mail flood, register spam, …).
+ * Uses @c abuse_maxretry / @c abuse_findtime — not login @c maxretry.
+ * Normal traffic limits ([chat], [mail], [traffic]) never call this.
+ */
+void hybbx_security_ban_abuse_report(const char *ip, const char *category);
+
+/** Same as @ref hybbx_security_ban_abuse_report for @p callid. */
+void hybbx_security_ban_callid_abuse_report(const char *callid,
+ const char *category);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* HYBBX_SECURITY_BAN_H */
diff --git a/include/hybbx/service.h b/include/hybbx/service.h
new file mode 100644
index 0000000..4730b8b
--- /dev/null
+++ b/include/hybbx/service.h
@@ -0,0 +1,174 @@
+#ifndef HYBBX_SERVICE_H
+#define HYBBX_SERVICE_H
+
+/**
+ * Centralized HyBBX daemon API (`hybbx` binary).
+ *
+ * Loads INI ([service], [storage], [auth], [transport.*], [circuit]), starts
+ * transport plugins and the HBX circuit hub. Edge link/repeater daemons attach
+ * to [circuit]; see share/hybbx.ini.example and docs/TOPOLOGY.md.
+ */
+
+#include "hybbx/config.h"
+#include "hybbx/plugin.h"
+#include "hybbx/auth.h"
+#include "hybbx/storage.h"
+#include "hybbx/texts.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/** Default global service name (`[service] name` in INI). */
+#define HYBBX_DEFAULT_SERVICE_NAME "hyBBX"
+
+typedef struct hybbx_service {
+ const char *name;
+ void *userdata;
+} hybbx_service_t;
+
+/** Create the main HyBBX service instance. */
+hybbx_service_t *hybbx_service_create(const char *name);
+
+/** Destroy the service and shut down all active transports. */
+void hybbx_service_destroy(hybbx_service_t *service);
+
+/**
+ * Apply settings from an INI configuration (service name, enabled transports).
+ */
+hybbx_result_t hybbx_service_apply_config(hybbx_service_t *service,
+ const hybbx_config_t *config,
+ const char *config_path);
+
+/**
+ * Load a transport plugin by name (e.g. "telnet", "packet_radio").
+ * Built-in plugins are linked statically; dynamic loading comes later.
+ */
+hybbx_result_t hybbx_service_load_transport(hybbx_service_t *service,
+ const char *plugin_name);
+
+/** Start a loaded transport with transport-specific configuration. */
+hybbx_result_t hybbx_service_start_transport(hybbx_service_t *service,
+ const char *plugin_name,
+ const char *config);
+
+/** Stop a running transport. */
+hybbx_result_t hybbx_service_stop_transport(hybbx_service_t *service,
+ const char *plugin_name);
+
+/** Run the main event loop until hybbx_service_stop() is called. */
+hybbx_result_t hybbx_service_run(hybbx_service_t *service);
+
+/** Request the main loop to exit. */
+void hybbx_service_stop(hybbx_service_t *service);
+
+typedef enum hybbx_shutdown_mode {
+ HYBBX_SHUTDOWN_NONE = 0,
+ HYBBX_SHUTDOWN_STOP = 1,
+ HYBBX_SHUTDOWN_RESTART = 2
+} hybbx_shutdown_mode_t;
+
+/** Store the daemon binary path used for /restart (typically argv[0]). */
+void hybbx_service_set_launch_binary(hybbx_service_t *service, const char *argv0);
+
+/** Sysop /shutdown or /restart — stops the main loop; restart re-execs after exit. */
+void hybbx_service_request_shutdown(hybbx_service_t *service, int restart);
+
+/** Shutdown mode requested while the service is still running. */
+hybbx_shutdown_mode_t hybbx_service_shutdown_mode(const hybbx_service_t *service);
+
+/**
+ * Re-exec the daemon when @ref hybbx_service_shutdown_mode is
+ * @ref HYBBX_SHUTDOWN_RESTART. Call before @ref hybbx_service_destroy.
+ * Does not return on success.
+ */
+void hybbx_service_restart_exec(const hybbx_service_t *service);
+
+const char *hybbx_service_config_path(const hybbx_service_t *service);
+
+hybbx_storage_t *hybbx_service_get_storage(hybbx_service_t *service);
+const hybbx_auth_config_t *hybbx_service_get_auth(hybbx_service_t *service);
+const hybbx_texts_config_t *hybbx_service_get_texts(hybbx_service_t *service);
+
+/**
+ * Global input prompt shown to every connected user.
+ * Empty string when unset (default): no visible prompt before input.
+ */
+const char *hybbx_service_get_prompt(const hybbx_service_t *service);
+
+/** Global service name from configuration (default @ref HYBBX_DEFAULT_SERVICE_NAME). */
+const char *hybbx_service_get_name(const hybbx_service_t *service);
+
+/** Configured maximum simultaneous online sessions (default @ref HYBBX_DEFAULT_MAX_ONLINE). */
+unsigned hybbx_service_max_online(const hybbx_service_t *service);
+
+/**
+ * Non-zero when `[service] login_announce=yes`:
+ * broadcast "*** User login: user@plugin" and list /who as user@plugin.
+ */
+int hybbx_service_login_announce(const hybbx_service_t *service);
+
+/** Currently connected sessions (all transports). */
+unsigned hybbx_service_active_nodes(const hybbx_service_t *service);
+
+/** Guest auto-disconnect timeout in seconds (from INI minutes setting). */
+unsigned hybbx_service_guest_timeout_seconds(const hybbx_service_t *service);
+
+struct hybbx_user_record;
+
+/**
+ * Assign the lowest free ephemeral guest slot (Guest1 … Guest25).
+ * Guests are not written to user files.
+ */
+hybbx_result_t hybbx_service_guest_assign(hybbx_service_t *service,
+ const char *guest_prefix,
+ struct hybbx_user_record *out,
+ unsigned *slot_out);
+
+/** Release a guest slot when the session ends or leaves guest mode. */
+void hybbx_service_guest_release(hybbx_service_t *service, unsigned slot);
+
+/**
+ * Reserve one node slot for a new connection.
+ * @return HYBBX_ERR_BUSY when @ref hybbx_service_max_online is reached.
+ */
+hybbx_result_t hybbx_service_acquire_node(hybbx_service_t *service);
+
+/** Release a node slot (pair with @ref hybbx_service_acquire_node). */
+void hybbx_service_release_node(hybbx_service_t *service);
+
+struct hybbx_session;
+
+/** Track an active connection for chat broadcast and similar features. */
+hybbx_result_t hybbx_service_attach_session(hybbx_service_t *service,
+ struct hybbx_session *session);
+
+/** Remove a session from the active connection list. */
+void hybbx_service_detach_session(hybbx_service_t *service,
+ struct hybbx_session *session);
+
+typedef void (*hybbx_service_session_visit_fn)(struct hybbx_session *session,
+ void *userdata);
+
+/** Invoke @p fn for each attached session (snapshot; lock not held in @p fn). */
+void hybbx_service_visit_sessions(hybbx_service_t *service,
+ hybbx_service_session_visit_fn fn,
+ void *userdata);
+
+/**
+ * Return a logged-in registered session for @p user_id on another connection,
+ * or NULL when the account is not online elsewhere.
+ */
+struct hybbx_session *hybbx_service_find_registered_session(
+ hybbx_service_t *service,
+ uint64_t user_id,
+ struct hybbx_session *exclude);
+
+struct hybbx_circuit_hub;
+struct hybbx_circuit_hub *hybbx_service_circuit_hub(hybbx_service_t *service);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* HYBBX_SERVICE_H */
diff --git a/include/hybbx/session.h b/include/hybbx/session.h
new file mode 100644
index 0000000..79f5343
--- /dev/null
+++ b/include/hybbx/session.h
@@ -0,0 +1,226 @@
+#ifndef HYBBX_SESSION_H
+#define HYBBX_SESSION_H
+
+#include "hybbx/plugin.h"
+#include "hybbx/storage.h"
+#include "hybbx/mail.h"
+#include "hybbx/proxymail.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct hybbx_service;
+
+typedef enum hybbx_session_area {
+ HYBBX_AREA_MAIN = 1,
+ HYBBX_AREA_MAIL = 2,
+ HYBBX_AREA_CHAT = 3,
+ HYBBX_AREA_CONFERENCE = 4,
+ /** Inter-Main mail sub-area under mail (`/proxymail`, `/mail proxymail`). */
+ HYBBX_AREA_PROXYMAIL = 5,
+ /** Inter-Main chat sub-area under chat (`/proxychat`, `/chat proxychat`). */
+ HYBBX_AREA_PROXYCHAT = 6
+} hybbx_session_area_t;
+
+/** Maximum nested area depth (main + sub-areas). */
+#define HYBBX_AREA_STACK_MAX 8u
+
+/**
+ * Open a new connection session.
+ * When auto-login is enabled (default), assigns the next free Guest1 … Guest25
+ * slot in memory (not written to user files).
+ * When auto-login is disabled, the session stays at a login prompt for
+ * registered `/login` and `/register` only (no guest slots).
+ */
+hybbx_result_t hybbx_session_open(struct hybbx_service *service,
+ const hybbx_transport_plugin_t *transport,
+ void *transport_data,
+ hybbx_session_t **out);
+
+void hybbx_session_close(hybbx_session_t *session);
+
+hybbx_result_t hybbx_session_handle_input(hybbx_session_t *session,
+ const uint8_t *data, size_t len);
+
+const char *hybbx_session_username(const hybbx_session_t *session);
+/** User-facing name (nickname) for the logged-in account. */
+const char *hybbx_session_display_name(const hybbx_session_t *session);
+/**
+ * Format `username@plugin` (transport name) into @p out.
+ * Uses display name and session transport / record.
+ */
+hybbx_result_t hybbx_session_format_user_at_plugin(const hybbx_session_t *session,
+ char *out, size_t out_len);
+uint64_t hybbx_session_id(const hybbx_session_t *session);
+const hybbx_session_record_t *hybbx_session_record(const hybbx_session_t *session);
+/** Store the client peer address on the session record (for security.log). */
+hybbx_result_t hybbx_session_set_remote(hybbx_session_t *session,
+ const char *remote);
+hybbx_user_level_t hybbx_session_user_level(const hybbx_session_t *session);
+int hybbx_session_is_guest(const hybbx_session_t *session);
+/** Non-zero for telnet, SSH, or WebSocket — not plugin/link transports. */
+int hybbx_session_is_interactive_user(const hybbx_session_t *session);
+
+/**
+ * Non-zero when this Sysop is hidden from /who and /online:
+ * [monitor] invisible-sysop=yes, or /monitor on.
+ * Non-Sysop grant users are never hidden.
+ */
+int hybbx_session_hidden_from_who(const hybbx_session_t *session);
+
+int hybbx_session_monitor_active(const hybbx_session_t *session);
+void hybbx_session_set_monitor_active(hybbx_session_t *session, int on);
+/** Non-zero when the session has completed login (guest or registered). */
+int hybbx_session_logged_in(const hybbx_session_t *session);
+/** Non-zero when auto_login is off: banner + prompt, registered login only. */
+int hybbx_session_login_prompt(const hybbx_session_t *session);
+
+/** Write raw text to the connected client (no line ending, no prompt). */
+hybbx_result_t hybbx_session_write(hybbx_session_t *session, const char *text);
+
+/** Write a line and append CRLF. */
+hybbx_result_t hybbx_session_write_line(hybbx_session_t *session,
+ const char *text);
+
+/** One blank line before `/command` output (not chat/mail compose). */
+hybbx_result_t hybbx_session_command_gap(hybbx_session_t *session);
+
+/**
+ * Show the global input prompt when configured.
+ * Default (empty prompt): no output — users type on a blank line.
+ */
+hybbx_result_t hybbx_session_show_prompt(hybbx_session_t *session);
+
+/** Clear screen and discard the current input line; show prompt when set. */
+hybbx_result_t hybbx_session_clear_terminal(hybbx_session_t *session);
+
+/** Non-zero when typed characters are echoed back to the client. */
+int hybbx_session_input_echo(const hybbx_session_t *session);
+
+/** Enable or disable per-session input echo. */
+hybbx_result_t hybbx_session_set_input_echo(hybbx_session_t *session, int enabled);
+
+/** Current session area (main, mail, chat, …). */
+hybbx_session_area_t hybbx_session_area(const hybbx_session_t *session);
+
+/** Area name: main, mail, chat. */
+const char *hybbx_session_area_name(hybbx_session_area_t area);
+
+/** Parse area name (main, mail, chat). */
+hybbx_session_area_t hybbx_session_area_parse(const char *name);
+
+/** Go up one area level (alias: /back). Clears state for the area being left. */
+hybbx_result_t hybbx_session_leave_area(hybbx_session_t *session);
+
+/** Return to main from any depth (alias: /menu). Clears all sub-area state. */
+hybbx_result_t hybbx_session_go_main(hybbx_session_t *session);
+
+/** Enter an area (pushes onto the area stack). */
+hybbx_result_t hybbx_session_enter_area(hybbx_session_t *session,
+ hybbx_session_area_t area);
+
+/** Switch the session to a registered (non-guest) account after login. */
+hybbx_result_t hybbx_session_switch_user(hybbx_session_t *session,
+ const hybbx_user_record_t *user);
+
+/**
+ * Periodic session maintenance (guest time limit, etc.).
+ * @return HYBBX_SESSION_END when the session should be closed.
+ */
+hybbx_result_t hybbx_session_tick(hybbx_session_t *session);
+
+/** Join a chat channel (enters chat area). @p channel_index is 1-based. */
+hybbx_result_t hybbx_session_join_chat_channel(hybbx_session_t *session,
+ unsigned channel_index);
+
+/** Current chat channel (1-based), or 0 when not in chat. */
+unsigned hybbx_session_chat_channel(const hybbx_session_t *session);
+
+struct hybbx_service *hybbx_session_service(hybbx_session_t *session);
+
+/** Join a private conference (conference area). Internal — use hybbx_conference_start. */
+hybbx_result_t hybbx_session_join_conference(hybbx_session_t *session,
+ const char *topic,
+ const char *partner_username);
+
+void hybbx_session_clear_conference(hybbx_session_t *session);
+
+int hybbx_session_conference_may_invite(hybbx_session_t *session,
+ const char *target);
+void hybbx_session_conference_invite_sent(hybbx_session_t *session,
+ const char *target);
+void hybbx_session_set_conference_invite(hybbx_session_t *session,
+ const char *from_username,
+ const char *topic);
+/** Optional deadline (0 = none). Used by monitor-initiated invites. */
+void hybbx_session_set_conference_invite_deadline(hybbx_session_t *session,
+ time_t deadline);
+time_t hybbx_session_conference_invite_deadline(const hybbx_session_t *session);
+void hybbx_session_clear_conference_invite(hybbx_session_t *session);
+const char *hybbx_session_conference_invite_from(const hybbx_session_t *session);
+const char *hybbx_session_conference_invite_topic(const hybbx_session_t *session);
+const char *hybbx_session_conference_partner(const hybbx_session_t *session);
+
+/** Enter local mail area (does not enter proxymail). */
+hybbx_result_t hybbx_session_enter_mail(hybbx_session_t *session);
+
+/** Enter proxymail sub-area (pushes mail when needed). */
+hybbx_result_t hybbx_session_enter_proxymail(hybbx_session_t *session);
+
+/** Enter local chat area without joining a channel. */
+hybbx_result_t hybbx_session_enter_chat(hybbx_session_t *session);
+
+/** Enter proxychat sub-area (pushes chat when needed). */
+hybbx_result_t hybbx_session_enter_proxychat(hybbx_session_t *session);
+
+/** Non-zero when composing an outbound mail message. */
+int hybbx_session_mail_composing(const hybbx_session_t *session);
+
+/** Start mail compose to @p to_user with @p subject; enters mail area. */
+hybbx_result_t hybbx_session_mail_compose_start(hybbx_session_t *session,
+ const char *to_user,
+ const char *subject);
+
+/** Cancel an in-progress mail compose. */
+void hybbx_session_mail_compose_cancel(hybbx_session_t *session);
+
+/** Body accumulated so far during compose (NUL-terminated). */
+const char *hybbx_session_mail_compose_body(const hybbx_session_t *session);
+
+const char *hybbx_session_mail_compose_to(const hybbx_session_t *session);
+const char *hybbx_session_mail_compose_subject(const hybbx_session_t *session);
+
+/** Non-zero when composing proxymail (`user@remote-main` recipient). */
+int hybbx_session_proxymail_composing(const hybbx_session_t *session);
+
+hybbx_result_t hybbx_session_proxymail_compose_start(hybbx_session_t *session,
+ const char *to_address,
+ const char *subject);
+
+void hybbx_session_proxymail_compose_cancel(hybbx_session_t *session);
+
+const char *hybbx_session_proxymail_compose_body(const hybbx_session_t *session);
+const char *hybbx_session_proxymail_compose_to(const hybbx_session_t *session);
+const char *hybbx_session_proxymail_compose_subject(
+ const hybbx_session_t *session);
+
+/** Wall-clock time when this session connected (for bandwidth policy ordering). */
+time_t hybbx_session_connected_at(const hybbx_session_t *session);
+
+/** Non-zero when the session is frozen by circuit bandwidth limits. */
+int hybbx_session_bandwidth_paused(const hybbx_session_t *session);
+
+void hybbx_session_set_bandwidth_paused(hybbx_session_t *session, int paused);
+
+/**
+ * End the session after sending @ref HYBBX_BANDWIDTH_DISCONNECT_MSG.
+ * Newest-first under low-bandwidth link pressure.
+ */
+void hybbx_session_disconnect_bandwidth(hybbx_session_t *session);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* HYBBX_SESSION_H */
diff --git a/include/hybbx/socket.h b/include/hybbx/socket.h
new file mode 100644
index 0000000..1baa639
--- /dev/null
+++ b/include/hybbx/socket.h
@@ -0,0 +1,35 @@
+#ifndef HYBBX_SOCKET_H
+#define HYBBX_SOCKET_H
+
+#if defined(__linux__) || defined(__GLIBC__)
+#define _DEFAULT_SOURCE 1
+#endif
+
+#include <sys/socket.h>
+
+#ifndef MSG_NOSIGNAL
+#define MSG_NOSIGNAL 0
+#endif
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/** Suppress SIGPIPE on send() (SO_NOSIGPIPE on BSD/MacOS; noop on Linux). */
+void hybbx_socket_nosigpipe(int fd);
+
+/**
+ * Format the peer IP address of a connected socket into @p buf.
+ * IPv4 dotted-quad or IPv6 colon form (no brackets).
+ */
+hybbx_result_t hybbx_socket_peer_name(int fd, char *buf, size_t buf_len);
+
+/** Log bind(2) failure; hints when @c EADDRINUSE (HyBBX already running). */
+void hybbx_socket_log_bind_failure(const char *component, const char *addr,
+ unsigned port);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* HYBBX_SOCKET_H */
diff --git a/include/hybbx/ssh.h b/include/hybbx/ssh.h
new file mode 100644
index 0000000..2555198
--- /dev/null
+++ b/include/hybbx/ssh.h
@@ -0,0 +1,57 @@
+#ifndef HYBBX_SSH_H
+#define HYBBX_SSH_H
+
+#include "hybbx/limits.h"
+#include "hybbx/types.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/** Default HyBBX SSH TCP port (non-privileged; parallels telnet 2323). */
+#define HYBBX_SSH_DEFAULT_PORT 3232u
+
+#define HYBBX_SSH_BIND_V4_MAX 64
+#define HYBBX_SSH_BIND_V6_MAX 64
+#define HYBBX_SSH_HOSTKEY_DIR_MAX HYBBX_PATH_MAX
+
+#define HYBBX_SSH_DEFAULT_BIND_V4 "0.0.0.0"
+#define HYBBX_SSH_DEFAULT_BIND_V6 "::"
+#define HYBBX_SSH_DEFAULT_HOSTKEY_DIR "keys"
+
+#define HYBBX_SSH_HOSTKEY_ED25519 "ssh_host_ed25519_key"
+
+/** Regenerate Ed25519 host keys when older than this (days). */
+#define HYBBX_SSH_HOSTKEY_VALID_DAYS 1825u
+
+typedef struct hybbx_ssh_config {
+ char bind_v4[HYBBX_SSH_BIND_V4_MAX];
+ char bind_v6[HYBBX_SSH_BIND_V6_MAX];
+ char hostkey_dir[HYBBX_SSH_HOSTKEY_DIR_MAX];
+ unsigned int port;
+ int ipv4;
+ int ipv6;
+} hybbx_ssh_config_t;
+
+void hybbx_ssh_config_defaults(hybbx_ssh_config_t *config);
+
+/**
+ * Parse a semicolon-separated key=value transport config string
+ * (from transport.ssh INI section).
+ */
+hybbx_result_t hybbx_ssh_config_parse(const char *config,
+ hybbx_ssh_config_t *out);
+
+/**
+ * Ensure Ed25519 host keys exist under @p keys_dir (resolved path).
+ * Writes the private key path into @p hostkey_path.
+ */
+hybbx_result_t hybbx_ssh_keys_ensure(const char *keys_dir,
+ char *hostkey_path,
+ size_t hostkey_path_len);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* HYBBX_SSH_H */
diff --git a/include/hybbx/storage.h b/include/hybbx/storage.h
new file mode 100644
index 0000000..f4b4802
--- /dev/null
+++ b/include/hybbx/storage.h
@@ -0,0 +1,178 @@
+#ifndef HYBBX_STORAGE_H
+#define HYBBX_STORAGE_H
+
+#include "hybbx/auth.h"
+#include "hybbx/limits.h"
+#include "hybbx/types.h"
+
+#include <stddef.h>
+#include <stdint.h>
+#include <time.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef enum hybbx_storage_backend_kind {
+ HYBBX_STORAGE_FLATFILE = 1,
+ HYBBX_STORAGE_SQLITE = 2,
+ HYBBX_STORAGE_MYSQL = 3,
+ HYBBX_STORAGE_MARIADB = 4
+} hybbx_storage_backend_kind_t;
+
+#define HYBBX_USER_NAME_MAX 64
+#define HYBBX_USER_NICKNAME_MAX 64
+#define HYBBX_USER_FULL_NAME_MAX 128
+#define HYBBX_USER_COUNTRY_MAX 64
+#define HYBBX_USER_LOCATION_MAX 128
+#define HYBBX_USER_EMAIL_MAX 128
+#define HYBBX_USER_PASSWORD_MAX HYBBX_PASSWORD_STORED_MAX
+#define HYBBX_TRANSPORT_NAME_MAX 32
+/** Peer address string (IPv4 or bracketed IPv6). */
+#define HYBBX_REMOTE_ADDR_MAX 64
+
+/** Maximum registered user records per INI user-file shard. */
+#define HYBBX_USERS_PER_FILE 50u
+
+/** INI section prefix for one user: [user.<id>]. */
+#define HYBBX_USER_INI_SECTION_PREFIX "user."
+
+typedef struct hybbx_user_registration {
+ char username[HYBBX_USER_NAME_MAX];
+ char nickname[HYBBX_USER_NICKNAME_MAX];
+ char full_name[HYBBX_USER_FULL_NAME_MAX];
+ char country[HYBBX_USER_COUNTRY_MAX];
+ char location[HYBBX_USER_LOCATION_MAX];
+ char email[HYBBX_USER_EMAIL_MAX];
+ char password[HYBBX_USER_PASSWORD_MAX];
+} hybbx_user_registration_t;
+
+/** Validate profile fields only (full name, country, location, email). */
+int hybbx_user_profile_valid(const hybbx_user_registration_t *reg);
+
+/** Validate registration profile fields (username + full name, country, location, email). */
+int hybbx_registration_valid(const hybbx_user_registration_t *reg,
+ const char *guest_prefix);
+
+typedef struct hybbx_user_record {
+ uint64_t id;
+ char username[HYBBX_USER_NAME_MAX];
+ char nickname[HYBBX_USER_NICKNAME_MAX];
+ hybbx_user_level_t level;
+ int active;
+ time_t created_at;
+ char full_name[HYBBX_USER_FULL_NAME_MAX];
+ char country[HYBBX_USER_COUNTRY_MAX];
+ char location[HYBBX_USER_LOCATION_MAX];
+ char email[HYBBX_USER_EMAIL_MAX];
+ char password[HYBBX_USER_PASSWORD_MAX];
+ time_t last_login_at;
+} hybbx_user_record_t;
+
+typedef struct hybbx_session_record {
+ uint64_t session_id;
+ uint64_t user_id;
+ char username[HYBBX_USER_NAME_MAX];
+ char transport[HYBBX_TRANSPORT_NAME_MAX];
+ char remote[HYBBX_REMOTE_ADDR_MAX];
+ time_t connected_at;
+ time_t disconnected_at;
+ int active;
+} hybbx_session_record_t;
+
+typedef struct hybbx_storage hybbx_storage_t;
+
+/** Paths and backup settings for SQLite (`[storage]`). */
+typedef struct hybbx_storage_sql_config {
+ char user_db[HYBBX_PATH_MAX];
+ char mail_db[HYBBX_PATH_MAX];
+ unsigned backup_interval_sec;
+ char backup_path[HYBBX_PATH_MAX];
+} hybbx_storage_sql_config_t;
+
+typedef struct hybbx_storage_options {
+ hybbx_storage_backend_kind_t backend;
+ const char *path;
+ const char *guest_prefix;
+ const hybbx_storage_sql_config_t *sql_cfg;
+} hybbx_storage_options_t;
+
+struct hybbx_config;
+
+void hybbx_storage_sql_config_defaults(hybbx_storage_sql_config_t *cfg);
+
+/** Resolve `user_db` / `mail_db` under @p storage_path from INI `[storage]`. */
+void hybbx_storage_sql_config_apply(hybbx_storage_sql_config_t *cfg,
+ const struct hybbx_config *config,
+ const char *storage_path);
+
+const hybbx_storage_sql_config_t *hybbx_storage_sql_config(
+ const hybbx_storage_t *storage);
+
+/** Copy SQLite DB files to fallback paths (no-op when not SQLite). */
+void hybbx_storage_backup_tick(hybbx_storage_t *storage);
+
+hybbx_storage_t *hybbx_storage_open(const hybbx_storage_options_t *options);
+void hybbx_storage_close(hybbx_storage_t *storage);
+
+hybbx_storage_backend_kind_t hybbx_storage_backend(const hybbx_storage_t *storage);
+
+/** Root data directory path, or NULL when unset. */
+const char *hybbx_storage_root_path(const hybbx_storage_t *storage);
+
+/**
+ * Register a new user account (level user, inactive until Sysop/Admin activates).
+ */
+hybbx_result_t hybbx_storage_register_user(hybbx_storage_t *storage,
+ const hybbx_user_registration_t *reg,
+ hybbx_user_record_t *out);
+
+/** Look up a user by login name (case-insensitive; stored lowercase). */
+hybbx_result_t hybbx_storage_find_user(hybbx_storage_t *storage,
+ const char *username,
+ hybbx_user_record_t *out);
+
+/** Resolve by login name or nickname (case-insensitive). */
+hybbx_result_t hybbx_storage_resolve_user(hybbx_storage_t *storage,
+ const char *name,
+ hybbx_user_record_t *out);
+
+/** Count existing accounts at a given level (e.g. enforce one Sysop). */
+hybbx_result_t hybbx_storage_count_level(hybbx_storage_t *storage,
+ hybbx_user_level_t level,
+ size_t *count);
+
+/**
+ * Iterate all user records. @p fn returns HYBBX_OK to continue; any other
+ * result stops iteration and is returned from this function.
+ */
+typedef hybbx_result_t (*hybbx_storage_user_fn)(const hybbx_user_record_t *user,
+ void *ctx);
+
+hybbx_result_t hybbx_storage_foreach_user(hybbx_storage_t *storage,
+ hybbx_storage_user_fn fn,
+ void *ctx);
+
+hybbx_result_t hybbx_storage_session_begin(hybbx_storage_t *storage,
+ const hybbx_user_record_t *user,
+ const char *transport,
+ hybbx_session_record_t *out);
+
+hybbx_result_t hybbx_storage_session_end(hybbx_storage_t *storage,
+ uint64_t session_id);
+
+/**
+ * Replace an existing user record (matched by @p user->id).
+ */
+hybbx_result_t hybbx_storage_update_user(hybbx_storage_t *storage,
+ const hybbx_user_record_t *user);
+
+/** Remove a user record from storage (matched by @p user_id). */
+hybbx_result_t hybbx_storage_delete_user(hybbx_storage_t *storage,
+ uint64_t user_id);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* HYBBX_STORAGE_H */
diff --git a/include/hybbx/telnet.h b/include/hybbx/telnet.h
new file mode 100644
index 0000000..956f1f5
--- /dev/null
+++ b/include/hybbx/telnet.h
@@ -0,0 +1,40 @@
+#ifndef HYBBX_TELNET_H
+#define HYBBX_TELNET_H
+
+#include "hybbx/types.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/** Default HyBBX telnet TCP port (BBX / terminal convention). */
+#define HYBBX_TELNET_DEFAULT_PORT 2323u
+
+#define HYBBX_TELNET_BIND_V4_MAX 64
+#define HYBBX_TELNET_BIND_V6_MAX 64
+
+#define HYBBX_TELNET_DEFAULT_BIND_V4 "0.0.0.0"
+#define HYBBX_TELNET_DEFAULT_BIND_V6 "::"
+
+typedef struct hybbx_telnet_config {
+ char bind_v4[HYBBX_TELNET_BIND_V4_MAX];
+ char bind_v6[HYBBX_TELNET_BIND_V6_MAX];
+ unsigned int port;
+ int ipv4;
+ int ipv6;
+} hybbx_telnet_config_t;
+
+void hybbx_telnet_config_defaults(hybbx_telnet_config_t *config);
+
+/**
+ * Parse a semicolon-separated key=value transport config string
+ * (from transport.telnet INI section).
+ */
+hybbx_result_t hybbx_telnet_config_parse(const char *config,
+ hybbx_telnet_config_t *out);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* HYBBX_TELNET_H */
diff --git a/include/hybbx/terminal.h b/include/hybbx/terminal.h
new file mode 100644
index 0000000..fec0871
--- /dev/null
+++ b/include/hybbx/terminal.h
@@ -0,0 +1,39 @@
+#ifndef HYBBX_TERMINAL_H
+#define HYBBX_TERMINAL_H
+
+#include "hybbx/types.h"
+
+#include <stddef.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct hybbx_session;
+
+/**
+ * HyBBX uses a single terminal palette: light gray text on a black background.
+ * ANSI SGR: foreground 37, background 40.
+ */
+#define HYBBX_TERM_SGR_LIGHTGRAY_ON_BLACK "\033[37;40m"
+
+/** Clear screen and move cursor home (requires `traffic.ansi = yes`). */
+#define HYBBX_TERM_CLEAR_SCREEN "\033[2J\033[H"
+
+/** Apply the HyBBX terminal colors on a session (sent once at connect). */
+hybbx_result_t hybbx_term_init_session(struct hybbx_session *session);
+
+/** Clear the client screen (ANSI or plain newlines). */
+hybbx_result_t hybbx_term_clear_screen(struct hybbx_session *session);
+
+/**
+ * Copy @p src to @p dst without ANSI escape sequences (colors, cursor, etc.).
+ * @return length of written string excluding the terminator.
+ */
+size_t hybbx_term_copy_plain(const char *src, char *dst, size_t dst_size);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* HYBBX_TERMINAL_H */
diff --git a/include/hybbx/texts.h b/include/hybbx/texts.h
new file mode 100644
index 0000000..ef1b9e3
--- /dev/null
+++ b/include/hybbx/texts.h
@@ -0,0 +1,84 @@
+#ifndef HYBBX_TEXTS_H
+#define HYBBX_TEXTS_H
+
+#include "hybbx/types.h"
+#include "hybbx/limits.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct hybbx_session;
+
+#define HYBBX_DEFAULT_TEXTS_PATH HYBBX_DIR_TEXT
+
+#define HYBBX_TEXT_BANNER "banner.txt"
+#define HYBBX_TEXT_MOTD "motd.txt"
+#define HYBBX_TEXT_NEWS "news.txt"
+#define HYBBX_TEXT_RULES "rules.txt"
+#define HYBBX_TEXT_VERSION "version.txt"
+
+/** Substituted in banner.txt / version.txt — HyBBX version number (e.g. 2.4.0). */
+#define HYBBX_BANNER_TOKEN_VERSION "@version@"
+/** Substituted in banner.txt line 2. */
+#define HYBBX_BANNER_TOKEN_SERVICE "@service@"
+/** Substituted in motd.txt (current session nickname). */
+#define HYBBX_TEXT_TOKEN_USERNAME "@username@"
+/** Substituted in version.txt — host operating system name (e.g. Linux). */
+#define HYBBX_TEXT_TOKEN_OS "@os@"
+/** Substituted in text files — local time per `[time]` (default HH:MM:SS). */
+#define HYBBX_TEXT_TOKEN_TIME "%time%"
+/** Substituted in text files — local date per `[time] date=` (default YYYY/MM/DD). */
+#define HYBBX_TEXT_TOKEN_DATE "%date%"
+
+#define HYBBX_TEXTS_PATH_MAX 512
+
+typedef struct hybbx_texts_config {
+ char path[HYBBX_TEXTS_PATH_MAX];
+} hybbx_texts_config_t;
+
+void hybbx_texts_config_defaults(hybbx_texts_config_t *texts);
+
+/**
+ * Build full path to a text file under the configured texts directory.
+ * Returns HYBBX_OK on success.
+ */
+hybbx_result_t hybbx_texts_resolve(const hybbx_texts_config_t *texts,
+ const char *filename,
+ char *out, size_t out_len);
+
+/**
+ * Send a text file line-by-line to the session (connection output).
+ * Expands @version@, @service@, @username@, @os@, %time%, and %date%.
+ * Missing files are skipped silently.
+ */
+hybbx_result_t hybbx_texts_send_file(const hybbx_texts_config_t *texts,
+ struct hybbx_session *session,
+ const char *filename);
+
+/**
+ * Send banner.txt with @version@ and @service@ tokens expanded.
+ */
+hybbx_result_t hybbx_texts_send_banner(const hybbx_texts_config_t *texts,
+ struct hybbx_session *session,
+ const char *version,
+ const char *service_name);
+
+/**
+ * Send motd.txt with @username@ expanded from @p session.
+ */
+hybbx_result_t hybbx_texts_send_motd(const hybbx_texts_config_t *texts,
+ struct hybbx_session *session);
+
+/**
+ * Send version.txt with @version@ and @os@ expanded from this host.
+ * Falls back to the built-in two-line version block if the file is missing.
+ */
+hybbx_result_t hybbx_texts_send_version(const hybbx_texts_config_t *texts,
+ struct hybbx_session *session);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* HYBBX_TEXTS_H */
diff --git a/include/hybbx/tnc.h b/include/hybbx/tnc.h
new file mode 100644
index 0000000..9655aa8
--- /dev/null
+++ b/include/hybbx/tnc.h
@@ -0,0 +1,240 @@
+#ifndef HYBBX_TNC_H
+#define HYBBX_TNC_H
+
+/**
+ * Unified TNC / AX.25 link driver for HyBBX packet radio.
+ *
+ * Supported hardware profiles (see docs/TNCS.md):
+ * - Landolt TNC2C, classic TNC-2 / PK-TNC2 (TheFirmware)
+ * - AEA PK-232, MFJ-1278, Kantronics KPC
+ * - Generic TNC2-class controllers
+ *
+ * Not in packet_radio: BayCom/based SER12 / PC-COM TCM3105 — use MAX25
+ * bcpr (`max25e0`) then optional HyBBX baycom transport or [max25] attach.
+ * UN1TME = TNCs only.
+ *
+ * Host protocols: KISS, TNC2 host mode, 6PACK (DF6BU).
+ */
+
+#include "hybbx/ax25.h"
+#include "hybbx/packet_radio.h"
+#include "hybbx/types.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define HYBBX_TNC2C_DEFAULT_HOST_BAUD HYBBX_PACKET_RADIO_DEFAULT_BAUD
+#define HYBBX_TNC2C_DEFAULT_RADIO_BAUD 1200u
+#define HYBBX_AFSK_RADIO_BAUD 1200u
+#define HYBBX_G3RUH_RADIO_BAUD 9600u
+#define HYBBX_TNC2C_CLOCK_4_9_MHZ 4.9f
+#define HYBBX_TNC2C_CLOCK_9_8_MHZ 9.8f
+#define HYBBX_TNC2C_CLOCK_10_MHZ 10.0f
+#define HYBBX_TNC2C_HOST_BAUD_MIN 300u
+#define HYBBX_TNC2C_HOST_BAUD_MAX 38400u
+#define HYBBX_TNC2C_DATA_BITS 7u
+
+typedef enum hybbx_tnc_serial_parity {
+ HYBBX_TNC_SERIAL_PARITY_UNSET = 0,
+ HYBBX_TNC_SERIAL_PARITY_NONE = 1,
+ HYBBX_TNC_SERIAL_PARITY_EVEN = 2,
+ HYBBX_TNC_SERIAL_PARITY_ODD = 3
+} hybbx_tnc_serial_parity_t;
+
+#define HYBBX_PACKET_RADIO_VIA_MAX 8
+
+typedef enum hybbx_tnc2c_modem {
+ HYBBX_TNC2C_MODEM_TCM3105 = 1,
+ HYBBX_TNC2C_MODEM_9600 = 2, /**< G3RUH-compatible 9600 baud FSK */
+ HYBBX_TNC2C_MODEM_19200 = 3
+} hybbx_tnc2c_modem_t;
+
+/**
+ * Over-the-air modulation standard.
+ * AFSK (Bell 202 / TCM3105 @ 1200) is the classic CB packet default.
+ * G3RUH FSK @ 9600 baud for higher throughput (requires compatible modem/TNC).
+ */
+typedef enum hybbx_packet_radio_modulation {
+ HYBBX_PACKET_RADIO_MOD_UNSET = 0,
+ HYBBX_PACKET_RADIO_MOD_AFSK = 1,
+ HYBBX_PACKET_RADIO_MOD_G3RUH_FSK = 2
+} hybbx_packet_radio_modulation_t;
+
+#define HYBBX_PACKET_RADIO_MOD_DEFAULT HYBBX_PACKET_RADIO_MOD_AFSK
+
+typedef enum hybbx_packet_radio_protocol {
+ HYBBX_PACKET_RADIO_PROTO_KISS = 1,
+ HYBBX_PACKET_RADIO_PROTO_HOSTMODE = 2,
+ HYBBX_PACKET_RADIO_PROTO_SIXPACK = 3
+} hybbx_packet_radio_protocol_t;
+
+typedef enum hybbx_packet_radio_tnc {
+ HYBBX_PACKET_RADIO_TNC_TNC2C = 1,
+ HYBBX_PACKET_RADIO_TNC_BAYCOM = 2,
+ HYBBX_PACKET_RADIO_TNC_PCCOM = 3,
+ HYBBX_PACKET_RADIO_TNC_GENERIC = 4,
+ HYBBX_PACKET_RADIO_TNC_TNC2 = 5,
+ HYBBX_PACKET_RADIO_TNC_PK232 = 6,
+ HYBBX_PACKET_RADIO_TNC_MFJ1278 = 7,
+ HYBBX_PACKET_RADIO_TNC_KANTRONICS = 8
+} hybbx_packet_radio_tnc_t;
+
+/** How HyBBX enters KISS mode on the host serial link. */
+typedef enum hybbx_kiss_entry {
+ HYBBX_KISS_ENTRY_UNSET = 0,
+ HYBBX_KISS_ENTRY_NONE = 1,
+ HYBBX_KISS_ENTRY_KISS_ON = 2,
+ HYBBX_KISS_ENTRY_ESC_AT_K = 3,
+ HYBBX_KISS_ENTRY_AUTO = 4
+} hybbx_kiss_entry_t;
+
+/** How HyBBX leaves KISS mode on shutdown. */
+typedef enum hybbx_kiss_exit {
+ HYBBX_KISS_EXIT_UNSET = 0,
+ HYBBX_KISS_EXIT_NONE = 1,
+ HYBBX_KISS_EXIT_KISS_OFF = 2,
+ HYBBX_KISS_EXIT_KISS_FRAME = 3,
+ HYBBX_KISS_EXIT_AUTO = 4
+} hybbx_kiss_exit_t;
+
+/**
+ * RF service band — drives safe duplex defaults.
+ * CB is half-duplex only (shared channel). Amateur may use full-duplex.
+ */
+typedef enum hybbx_packet_radio_band {
+ HYBBX_PACKET_RADIO_BAND_UNSET = 0,
+ HYBBX_PACKET_RADIO_BAND_CB = 1,
+ HYBBX_PACKET_RADIO_BAND_AMATEUR = 2
+} hybbx_packet_radio_band_t;
+
+/**
+ * Radio link duplex mode (distinct from host serial full-duplex).
+ * half = one direction at a time on the RF channel (CB / classic packet).
+ * full = TNC may key TX while receiving (amateur split / full-dup setups).
+ */
+typedef enum hybbx_packet_radio_duplex {
+ HYBBX_PACKET_RADIO_DUPLEX_UNSET = 0,
+ HYBBX_PACKET_RADIO_DUPLEX_HALF = 1,
+ HYBBX_PACKET_RADIO_DUPLEX_FULL = 2
+} hybbx_packet_radio_duplex_t;
+
+#define HYBBX_PACKET_RADIO_DUPLEX_DEFAULT HYBBX_PACKET_RADIO_DUPLEX_HALF
+
+typedef struct hybbx_tnc_params {
+ float clock_mhz;
+ hybbx_tnc2c_modem_t modem;
+ hybbx_packet_radio_modulation_t modulation;
+ unsigned int radio_baud;
+ unsigned int kiss_port;
+ unsigned int txdelay;
+ unsigned int persist;
+ unsigned int slot;
+ unsigned int txtail;
+ hybbx_packet_radio_band_t band;
+ hybbx_packet_radio_duplex_t duplex;
+ int fullduplex;
+ hybbx_kiss_entry_t kiss_entry;
+ hybbx_kiss_exit_t kiss_exit;
+ int host_connect_on_start;
+ unsigned int data_bits;
+ hybbx_tnc_serial_parity_t serial_parity;
+ unsigned int stop_bits;
+ int assert_modem_lines;
+} hybbx_tnc_params_t;
+
+typedef struct hybbx_packet_radio_config {
+ hybbx_packet_radio_device_type_t device_type;
+ char *device;
+ unsigned int baud;
+ hybbx_packet_radio_tnc_t tnc;
+ hybbx_packet_radio_protocol_t protocol;
+ hybbx_tnc_params_t params;
+ char *mycall;
+ char *dest_call;
+ char *via[HYBBX_PACKET_RADIO_VIA_MAX];
+ unsigned via_count;
+ char *circuit_host;
+ unsigned circuit_port;
+ char *link_id;
+ char *link_password;
+ char *link_role;
+ char *frequency_mhz;
+} hybbx_packet_radio_config_t;
+
+typedef struct hybbx_tnc hybbx_tnc_t;
+
+typedef void (*hybbx_tnc_ui_cb)(const uint8_t *payload, size_t len,
+ const hybbx_ax25_path_t *path,
+ void *userdata);
+
+/** Raw AX.25 frame from KISS/6PACK (before UI parsing). */
+typedef void (*hybbx_tnc_frame_cb)(const uint8_t *frame, size_t len,
+ void *userdata);
+
+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);
+
+void hybbx_tnc_close(hybbx_tnc_t *tnc);
+
+hybbx_result_t hybbx_tnc_poll(hybbx_tnc_t *tnc);
+
+/** POSIX serial fd for poll/select; returns -1 when unavailable. */
+int hybbx_tnc_serial_fd(const hybbx_tnc_t *tnc);
+
+/** Send a raw AX.25 frame (already built) to the TNC. */
+hybbx_result_t hybbx_tnc_send_frame(hybbx_tnc_t *tnc,
+ const uint8_t *frame, size_t len);
+
+/** Build and send an AX.25 UI frame from text payload. */
+hybbx_result_t hybbx_tnc_send_ui(hybbx_tnc_t *tnc,
+ const uint8_t *payload, size_t len);
+
+/** Send bytes during host-mode connected converse. */
+hybbx_result_t hybbx_tnc_send_converse(hybbx_tnc_t *tnc,
+ const char *data, size_t len);
+
+int hybbx_tnc_host_connected(const hybbx_tnc_t *tnc);
+
+hybbx_packet_radio_duplex_t hybbx_tnc_duplex(const hybbx_tnc_t *tnc);
+hybbx_packet_radio_band_t hybbx_tnc_band(const hybbx_tnc_t *tnc);
+int hybbx_tnc_duplex_is_full(const hybbx_tnc_t *tnc);
+
+const char *hybbx_packet_radio_duplex_name(hybbx_packet_radio_duplex_t duplex);
+const char *hybbx_packet_radio_band_name(hybbx_packet_radio_band_t band);
+
+const char *hybbx_packet_radio_modulation_name(
+ hybbx_packet_radio_modulation_t modulation);
+
+int hybbx_packet_radio_modulation_is_g3ruh(
+ hybbx_packet_radio_modulation_t modulation);
+
+/** Apply g3ruh_fsk / modulation keys to modem, radio_baud, and timing. */
+hybbx_result_t hybbx_tnc_finalize_modulation(hybbx_packet_radio_config_t *cfg);
+
+/** Apply band/duplex rules and sync @p fullduplex for the TNC firmware. */
+hybbx_result_t hybbx_tnc_finalize_radio_duplex(hybbx_packet_radio_config_t *cfg);
+
+/** Apply profile-specific defaults (TNC2C, generic, …). */
+hybbx_result_t hybbx_tnc_profile_apply_defaults(hybbx_packet_radio_config_t *cfg);
+
+/** Apply host serial line defaults (TNC2C: 7E1 + RTS/DTR). */
+hybbx_result_t hybbx_tnc_finalize_serial_line(hybbx_packet_radio_config_t *cfg);
+
+/** Apply kiss_entry / kiss_exit defaults from TNC profile when unset. */
+hybbx_result_t hybbx_tnc_finalize_kiss_entry(hybbx_packet_radio_config_t *cfg);
+
+/** Apply CSMA defaults (Persist/SlotTime) from band when unset in INI. */
+hybbx_result_t hybbx_tnc_finalize_csma(hybbx_packet_radio_config_t *cfg);
+
+const char *hybbx_packet_radio_tnc_name(hybbx_packet_radio_tnc_t tnc);
+const char *hybbx_kiss_entry_name(hybbx_kiss_entry_t entry);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* HYBBX_TNC_H */
diff --git a/include/hybbx/tnc2c.h b/include/hybbx/tnc2c.h
new file mode 100644
index 0000000..42f5277
--- /dev/null
+++ b/include/hybbx/tnc2c.h
@@ -0,0 +1,27 @@
+#ifndef HYBBX_TNC2C_H
+#define HYBBX_TNC2C_H
+
+/**
+ * Backward-compatible header for the Landolt TNC2C driver.
+ * New code should include hybbx/tnc.h.
+ */
+
+#include "hybbx/tnc.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef hybbx_tnc_t hybbx_tnc2c_t;
+typedef hybbx_tnc_params_t hybbx_tnc2c_params_t;
+
+/* hybbx_tnc2c_open removed — use hybbx_tnc_open() with RX callback. */
+#define hybbx_tnc2c_close hybbx_tnc_close
+#define hybbx_tnc2c_poll hybbx_tnc_poll
+#define hybbx_tnc2c_send_frame hybbx_tnc_send_frame
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* HYBBX_TNC2C_H */
diff --git a/include/hybbx/traffic.h b/include/hybbx/traffic.h
new file mode 100644
index 0000000..8d06cf3
--- /dev/null
+++ b/include/hybbx/traffic.h
@@ -0,0 +1,59 @@
+#ifndef HYBBX_TRAFFIC_H
+#define HYBBX_TRAFFIC_H
+
+#include "hybbx/types.h"
+
+#include <stddef.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct hybbx_config;
+struct hybbx_session;
+
+/** Host link speed HyBBX is optimized for (8N1, ~1 byte per 10 bit times). */
+#define HYBBX_BAUD2400 2400u
+
+/** Default terminal width (columns). */
+#define HYBBX_LINE_WIDTH 80u
+
+/** Maximum configurable `line_width` in INI. */
+#define HYBBX_LINE_WIDTH_MAX 132u
+
+/** Maximum user input line (commands / chat). */
+#define HYBBX_LINE_MAX 80u
+
+/** Typical AX.25 UI payload ceiling used for outbound chunk sizing. */
+#define HYBBX_AX25_PAYLOAD_MAX 256u
+
+typedef struct hybbx_traffic_config {
+ unsigned baud;
+ unsigned line_width;
+ int pace_output;
+ int ansi;
+ int input_echo;
+} hybbx_traffic_config_t;
+
+void hybbx_traffic_config_defaults(hybbx_traffic_config_t *cfg);
+void hybbx_traffic_config_apply(const struct hybbx_config *config);
+const hybbx_traffic_config_t *hybbx_traffic_config_get(void);
+
+/**
+ * Microseconds to wait after one 8N1 byte at @p baud (0 when pacing is off).
+ */
+unsigned hybbx_traffic_byte_delay_us(unsigned baud);
+
+/**
+ * Emit @p data to @p session with plain-ASCII policy, column wrap, and pacing.
+ * @p out_col is updated (wrap position on the current output line).
+ */
+hybbx_result_t hybbx_traffic_emit(struct hybbx_session *session,
+ unsigned *out_col,
+ const char *data, size_t len);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* HYBBX_TRAFFIC_H */
diff --git a/include/hybbx/types.h b/include/hybbx/types.h
new file mode 100644
index 0000000..775a711
--- /dev/null
+++ b/include/hybbx/types.h
@@ -0,0 +1,42 @@
+#ifndef HYBBX_TYPES_H
+#define HYBBX_TYPES_H
+
+#include <stddef.h>
+#include <stdint.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef enum hybbx_result {
+ HYBBX_OK = 0,
+ /** Input is a local/mailbox command (not a HyBBX '/' command). */
+ HYBBX_LOCAL_CMD = 1,
+ /** Session should be closed (e.g. /exit). */
+ HYBBX_SESSION_END = 2,
+ HYBBX_ERR_INVALID = -1,
+ HYBBX_ERR_NOMEM = -2,
+ HYBBX_ERR_NOT_FOUND = -3,
+ HYBBX_ERR_IO = -4,
+ HYBBX_ERR_UNSUPPORTED = -5,
+ HYBBX_ERR_BUSY = -6,
+ HYBBX_ERR_DENIED = -7
+} hybbx_result_t;
+
+typedef enum hybbx_transport_kind {
+ HYBBX_TRANSPORT_TELNET = 1,
+ HYBBX_TRANSPORT_PACKET_RADIO = 2,
+ HYBBX_TRANSPORT_CIRCUIT = 3,
+ HYBBX_TRANSPORT_ARDOP = 4,
+ HYBBX_TRANSPORT_CRDOP = 5,
+ HYBBX_TRANSPORT_SSH = 6,
+ HYBBX_TRANSPORT_WEBSOCKET = 7,
+ HYBBX_TRANSPORT_BAYCOM = 8,
+ HYBBX_TRANSPORT_MAINS_PROXY = 9,
+} hybbx_transport_kind_t;
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* HYBBX_TYPES_H */
diff --git a/include/hybbx/util.h b/include/hybbx/util.h
new file mode 100644
index 0000000..de439b4
--- /dev/null
+++ b/include/hybbx/util.h
@@ -0,0 +1,143 @@
+#ifndef HYBBX_UTIL_H
+#define HYBBX_UTIL_H
+
+#include "hybbx/types.h"
+
+#include <stddef.h>
+#include <time.h>
+
+struct tm;
+
+struct hybbx_config;
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * Safe string copy (always NUL-terminates when @p dst_size > 0).
+ * Returns bytes copied excluding the terminator.
+ */
+size_t hybbx_strlcpy(char *dst, const char *src, size_t dst_size);
+
+/**
+ * Join @p base and @p name into @p out with a single '/'.
+ * Rejects empty components and path traversal ("..").
+ */
+hybbx_result_t hybbx_path_join(char *out, size_t out_len,
+ const char *base, const char *name);
+
+/** Default relative storage path (@ref HYBBX_DIR_DATA). */
+hybbx_result_t hybbx_default_user_data_path(char *out, size_t out_len);
+
+/**
+ * Expand a config path: `~` → $HOME, `~/foo` → $HOME/foo.
+ * Empty or NULL @p path → @ref HYBBX_DIR_DATA. Other paths copied unchanged.
+ */
+hybbx_result_t hybbx_path_expand(char *out, size_t out_len, const char *path);
+
+/** Set the HyBBX install root used by @ref hybbx_path_resolve. */
+void hybbx_install_root_set(const char *root);
+
+/** Install root set by @ref hybbx_install_root_set, or NULL when unset. */
+const char *hybbx_install_root_get(void);
+
+/**
+ * Resolve a config path: @ref hybbx_path_expand, then prefix relative paths
+ * with the install root when set.
+ */
+hybbx_result_t hybbx_path_resolve(char *out, size_t out_len, const char *path);
+
+/** Parent directory of @p path (POSIX `/` rules). */
+hybbx_result_t hybbx_path_dirname(const char *path, char *out, size_t out_len);
+
+/**
+ * Host operating system name for display (e.g. Linux, FreeBSD, MacOS, Windows).
+ * Does not include OS version or kernel release.
+ */
+hybbx_result_t hybbx_platform_os_name(char *out, size_t out_len);
+
+/** Return non-zero when @p len is safe for HyBBX allocations. */
+int hybbx_size_ok(size_t len);
+
+/**
+ * HyBBX boolean configuration standard.
+ * Canonical written form: @c yes / @c no (see @ref HYBBX_BOOL_YES / @ref HYBBX_BOOL_NO).
+ * Accepted true: yes, true, enable, enabled, on, 1
+ * Accepted false: no, false, disable, disabled, off, 0
+ * Matching is case-insensitive.
+ */
+#define HYBBX_BOOL_YES "yes"
+#define HYBBX_BOOL_NO "no"
+
+/** Return non-zero when @p value is a recognized true token. */
+int hybbx_bool_is_true(const char *value);
+
+/** Return non-zero when @p value is a recognized false token. */
+int hybbx_bool_is_false(const char *value);
+
+/**
+ * Parse a boolean string. Returns 1 or 0 when recognized; otherwise
+ * returns @p default_value (also used when @p value is NULL or empty).
+ */
+int hybbx_parse_bool(const char *value, int default_value);
+
+/** Canonical @c yes / @c no string for a boolean value. */
+const char *hybbx_bool_to_string(int value);
+
+/** Short name for @p rc (logging / tests). */
+const char *hybbx_result_name(hybbx_result_t rc);
+
+/**
+ * HyBBX system-local date/time formatting for logs and text/ tokens.
+ * Default: 24-hour clock with seconds, ISO date YYYY/MM/DD.
+ */
+typedef enum hybbx_date_format {
+ HYBBX_DATE_ISO = 0,
+ HYBBX_DATE_ISO_SHORT = 1,
+ HYBBX_DATE_US = 2,
+ HYBBX_DATE_EU = 3,
+} hybbx_date_format_t;
+
+typedef struct hybbx_time_format {
+ int clock_12h;
+ int seconds;
+ hybbx_date_format_t date_format;
+} hybbx_time_format_t;
+
+void hybbx_time_format_defaults(hybbx_time_format_t *fmt);
+const hybbx_time_format_t *hybbx_time_format_get(void);
+void hybbx_time_config_apply(const struct hybbx_config *config);
+
+/** Local wall-clock time for text tokens and stamps. */
+hybbx_result_t hybbx_time_local_now(struct tm *out);
+
+const char *hybbx_date_format_name(hybbx_date_format_t fmt);
+
+/**
+ * Format @p tm as clock time (default @c HH:MM:SS, 12h when configured).
+ */
+hybbx_result_t hybbx_time_format_time(char *out, size_t out_len,
+ const struct tm *tm,
+ const hybbx_time_format_t *fmt);
+
+/**
+ * Format @p tm as calendar date (default @c YYYY/MM/DD).
+ */
+hybbx_result_t hybbx_time_format_date(char *out, size_t out_len,
+ const struct tm *tm,
+ const hybbx_time_format_t *fmt);
+
+/**
+ * Format @p tm as @c yyyymmdd HH:MM per @p fmt (compact log stamp).
+ * @return HYBBX_OK on success.
+ */
+hybbx_result_t hybbx_time_format_stamp(char *out, size_t out_len,
+ const struct tm *tm,
+ const hybbx_time_format_t *fmt);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* HYBBX_UTIL_H */
diff --git a/include/hybbx/websocket.h b/include/hybbx/websocket.h
new file mode 100644
index 0000000..d14e870
--- /dev/null
+++ b/include/hybbx/websocket.h
@@ -0,0 +1,53 @@
+#ifndef HYBBX_WEBSOCKET_H
+#define HYBBX_WEBSOCKET_H
+
+#include "hybbx/limits.h"
+#include "hybbx/types.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/** Default WebSocket listen port (behind TLS reverse proxy). */
+#define HYBBX_WEBSOCKET_DEFAULT_PORT 4591u
+
+/** Default simultaneous WebSocket client connections. */
+#define HYBBX_WEBSOCKET_DEFAULT_MAX_CONNECTIONS 10u
+
+#define HYBBX_WEBSOCKET_BIND_V4_MAX 64
+#define HYBBX_WEBSOCKET_BIND_V6_MAX 64
+#define HYBBX_WEBSOCKET_PATH_MAX 128
+
+#define HYBBX_WEBSOCKET_DEFAULT_BIND_V4 "127.0.0.1"
+#define HYBBX_WEBSOCKET_DEFAULT_BIND_V6 "::1"
+#define HYBBX_WEBSOCKET_DEFAULT_PATH "/hybbx"
+#define HYBBX_WEBSOCKET_DEFAULT_CERT_DIR "keys"
+
+/** Filenames inside @c cert_dir (auto-generated on first start when TLS). */
+#define HYBBX_WS_TLS_CERT_FILENAME "hybbx_ws.crt"
+#define HYBBX_WS_TLS_KEY_FILENAME "hybbx_ws.key"
+
+/** Self-signed WebSocket TLS certificate validity (days). */
+#define HYBBX_WS_TLS_CERT_VALID_DAYS 1825u
+
+typedef struct hybbx_websocket_config {
+ char bind_v4[HYBBX_WEBSOCKET_BIND_V4_MAX];
+ char bind_v6[HYBBX_WEBSOCKET_BIND_V6_MAX];
+ char path[HYBBX_WEBSOCKET_PATH_MAX];
+ char cert_dir[HYBBX_PATH_MAX];
+ unsigned int port;
+ unsigned int max_connections;
+ int ipv4;
+ int ipv6;
+} hybbx_websocket_config_t;
+
+void hybbx_websocket_config_defaults(hybbx_websocket_config_t *config);
+
+hybbx_result_t hybbx_websocket_config_parse(const char *config,
+ hybbx_websocket_config_t *out);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* HYBBX_WEBSOCKET_H */
git clone -b <branch> https://cgit.mode42.com/<repo>.git
git clone -b <branch> git://cgit.mode42.com/<repo>.git

info@mode42.com