summaryrefslogtreecommitdiff
path: root/src/core/mains_proxy.c
diff options
context:
space:
mode:
authorinfo@mode42.com <info@mode42.com>2026-08-08 03:54:55 +0000
committerinfo@mode42.com <info@mode42.com>2026-08-08 03:54:55 +0000
commit20cb29c2f8c5c87bc590896854a20b1473ceb358 (patch)
tree2857f41513a56ad41af97b57362639298aa7f033 /src/core/mains_proxy.c
#2
Diffstat (limited to 'src/core/mains_proxy.c')
-rw-r--r--src/core/mains_proxy.c1034
1 files changed, 1034 insertions, 0 deletions
diff --git a/src/core/mains_proxy.c b/src/core/mains_proxy.c
new file mode 100644
index 0000000..f3475fe
--- /dev/null
+++ b/src/core/mains_proxy.c
@@ -0,0 +1,1034 @@
+#if defined(__linux__) || defined(__GLIBC__)
+#define _DEFAULT_SOURCE 1
+#endif
+
+#include "hybbx/mains_proxy.h"
+#include "hybbx/service.h"
+#include "hybbx/limits.h"
+#include "hybbx/util.h"
+#include "hybbx/circuit_tcp.h"
+#include "hybbx/proxymail.h"
+#include "hybbx/proxychat.h"
+#include "hybbx/log.h"
+
+#include <errno.h>
+#include <poll.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+/* Inter-node mesh: hybbx_circuit_link_connect() only — never peer Main TCP. */
+
+#define MAINS_PROXY_RECONNECT_MS 5000u
+#define MAINS_PROXY_CONNECT_ATTEMPTS 30u
+#define MAINS_PROXY_DEFAULT_TTL 8u
+
+typedef struct mains_proxy_peer_runtime {
+ hybbx_mains_proxy_peer_config_t config;
+ int fd;
+ hybbx_circuit_decoder_t dec;
+ unsigned reconnect_ms;
+} mains_proxy_peer_runtime_t;
+
+typedef struct mains_proxy_runtime {
+ hybbx_service_t *service;
+ hybbx_mains_proxy_mesh_t mesh;
+ mains_proxy_peer_runtime_t peers[HYBBX_MAINS_PROXY_MAX_PEERS];
+ unsigned live_links;
+} mains_proxy_runtime_t;
+
+static mains_proxy_runtime_t g_rt;
+
+static const char *mains_proxy_find_kv(const char *config, const char *key,
+ char *scratch, size_t scratch_len)
+{
+ const char *cursor = config;
+ size_t key_len = strlen(key);
+
+ if (scratch != NULL && scratch_len > 0) {
+ scratch[0] = '\0';
+ }
+
+ if (config == NULL || key == NULL) {
+ return NULL;
+ }
+
+ while (*cursor != '\0') {
+ const char *line = cursor;
+ const char *eq;
+ const char *end = cursor;
+
+ /*
+ * Config sections are serialized with ';' between key=value pairs
+ * (hybbx_config_format_section) but the parser used to look for '\n'
+ * only. Accept both separators so a single section can be parsed
+ * regardless of how it was produced.
+ */
+ while (*end != '\0' && *end != ';' && *end != '\n') {
+ end++;
+ }
+
+ if (line[0] == '[') {
+ cursor = (*end == '\0') ? end : end + 1;
+ continue;
+ }
+
+ eq = strchr(line, '=');
+ if (eq == NULL || eq >= end) {
+ cursor = (*end == '\0') ? end : end + 1;
+ continue;
+ }
+
+ if ((size_t)(eq - line) == key_len &&
+ strncmp(line, key, key_len) == 0) {
+ const char *value = eq + 1;
+
+ while (value < end && (*value == ' ' || *value == '\t')) {
+ value++;
+ }
+
+ if (scratch != NULL && scratch_len > 0) {
+ size_t vlen = (size_t)(end - value);
+
+ while (vlen > 0 &&
+ (value[vlen - 1] == ' ' || value[vlen - 1] == '\t' ||
+ value[vlen - 1] == '\r')) {
+ vlen--;
+ }
+
+ if (vlen >= scratch_len) {
+ vlen = scratch_len - 1;
+ }
+ memcpy(scratch, value, vlen);
+ scratch[vlen] = '\0';
+ return scratch;
+ }
+
+ return value;
+ }
+
+ cursor = (*end == '\0') ? end : end + 1;
+ }
+
+ return NULL;
+}
+
+static int str_ieq_local(const char *a, const char *b)
+{
+ if (a == NULL || b == NULL) {
+ return 0;
+ }
+
+ while (*a != '\0' && *b != '\0') {
+ char ca = (char)(*a >= 'A' && *a <= 'Z' ? *a + 32 : *a);
+ char cb = (char)(*b >= 'A' && *b <= 'Z' ? *b + 32 : *b);
+
+ if (ca != cb) {
+ return 0;
+ }
+ a++;
+ b++;
+ }
+
+ return *a == '\0' && *b == '\0';
+}
+
+static int proxy_payload_get_line(const char *payload, size_t len,
+ const char *key, char *out, size_t out_len)
+{
+ size_t key_len;
+ const char *cursor;
+ const char *end;
+
+ if (payload == NULL || key == NULL || out == NULL || out_len == 0) {
+ return 0;
+ }
+
+ out[0] = '\0';
+ key_len = strlen(key);
+ cursor = payload;
+ end = payload + len;
+
+ while (cursor < end) {
+ const char *line_end = memchr(cursor, '\n', (size_t)(end - cursor));
+ size_t line_len;
+
+ if (line_end == NULL) {
+ line_end = end;
+ }
+
+ line_len = (size_t)(line_end - cursor);
+ if (line_len >= key_len + 1 && memcmp(cursor, key, key_len) == 0 &&
+ cursor[key_len] == '=') {
+ const char *value = cursor + key_len + 1;
+ size_t vlen = line_len - key_len - 1;
+
+ while (vlen > 0 && (*value == ' ' || *value == '\t')) {
+ value++;
+ vlen--;
+ }
+ if (vlen >= out_len) {
+ vlen = out_len - 1;
+ }
+ memcpy(out, value, vlen);
+ out[vlen] = '\0';
+ return 1;
+ }
+
+ if (line_end >= end) {
+ break;
+ }
+ cursor = line_end + 1;
+ }
+
+ return 0;
+}
+
+static const char *proxy_payload_body(const char *payload, size_t len,
+ size_t *body_len)
+{
+ size_t off;
+
+ if (payload == NULL || body_len == NULL) {
+ return NULL;
+ }
+
+ *body_len = 0;
+
+ if (len >= 5 && memcmp(payload, "---\r\n", 5) == 0) {
+ off = 5;
+ } else if (len >= 4 && memcmp(payload, "---\n", 4) == 0) {
+ off = 4;
+ } else {
+ return NULL;
+ }
+
+ while (off < len && (payload[off] == '\r' || payload[off] == '\n')) {
+ off++;
+ }
+
+ *body_len = len - off;
+ return payload + off;
+}
+
+static unsigned proxy_payload_get_ttl(const char *payload, size_t len)
+{
+ char scratch[HYBBX_CONFIG_LINE_MAX];
+ const char *value;
+ unsigned long ttl;
+
+ if (payload == NULL || len == 0 || len >= sizeof(scratch)) {
+ return 0;
+ }
+
+ memcpy(scratch, payload, len);
+ scratch[len] = '\0';
+
+ value = NULL;
+ if (proxy_payload_get_line(scratch, len, "ttl", scratch, sizeof(scratch))) {
+ value = scratch;
+ }
+
+ if (value == NULL || value[0] == '\0') {
+ return 0;
+ }
+
+ ttl = strtoul(value, NULL, 10);
+ if (ttl > 255u) {
+ ttl = 255u;
+ }
+ return (unsigned)ttl;
+}
+
+static size_t proxy_build_mail_payload(char *out, size_t out_cap,
+ const char *from_address,
+ const char *to_address,
+ const char *subject,
+ const char *body,
+ unsigned ttl)
+{
+ int n;
+
+ if (out == NULL || out_cap == 0 || from_address == NULL ||
+ to_address == NULL || subject == NULL || body == NULL) {
+ return 0;
+ }
+
+ n = snprintf(out, out_cap, "from=%s\nto=%s\nsub=%s\nttl=%u\n---\n%s",
+ from_address, to_address, subject, ttl, body);
+ if (n < 0 || (size_t)n >= out_cap) {
+ return 0;
+ }
+
+ return (size_t)n;
+}
+
+static size_t proxy_build_chat_payload(char *out, size_t out_cap,
+ const char *from_address,
+ const char *line,
+ unsigned ttl)
+{
+ int n;
+
+ if (out == NULL || out_cap == 0 || from_address == NULL ||
+ line == NULL) {
+ return 0;
+ }
+
+ n = snprintf(out, out_cap, "from=%s\nttl=%u\n---\n%s", from_address, ttl, line);
+ if (n < 0 || (size_t)n >= out_cap) {
+ return 0;
+ }
+
+ return (size_t)n;
+}
+
+static void proxy_dispatch_payload(hybbx_service_t *service,
+ hybbx_circuit_proto_t proto,
+ const uint8_t *payload, size_t len)
+{
+ char from[HYBBX_PROXYMAIL_ADDRESS_MAX];
+ char to[HYBBX_PROXYMAIL_ADDRESS_MAX];
+ char subj[HYBBX_MAIL_SUBJECT_MAX + 1];
+ const char *body;
+ size_t body_len;
+ char scratch[HYBBX_CIRCUIT_MAX_PAYLOAD + 1];
+
+ if (service == NULL || payload == NULL || len == 0 ||
+ len > HYBBX_CIRCUIT_MAX_PAYLOAD) {
+ return;
+ }
+
+ memcpy(scratch, payload, len);
+ scratch[len] = '\0';
+
+ if (!proxy_payload_get_line(scratch, len, "from", from, sizeof(from))) {
+ return;
+ }
+
+ if (proto == HYBBX_CIRCUIT_PROTO_PROXY_MAIL) {
+ if (!proxy_payload_get_line(scratch, len, "to", to, sizeof(to))) {
+ return;
+ }
+ if (!proxy_payload_get_line(scratch, len, "sub", subj, sizeof(subj))) {
+ subj[0] = '\0';
+ }
+ body = proxy_payload_body(scratch, len, &body_len);
+ if (body == NULL) {
+ return;
+ }
+ scratch[0] = '\0';
+ if (body_len > 0) {
+ if (body_len >= sizeof(scratch)) {
+ body_len = sizeof(scratch) - 1;
+ }
+ memcpy(scratch, body, body_len);
+ scratch[body_len] = '\0';
+ }
+ hybbx_proxymail_receive(service, from, to, subj, scratch);
+ return;
+ }
+
+ if (proto == HYBBX_CIRCUIT_PROTO_PROXY_CHAT) {
+ body = proxy_payload_body(scratch, len, &body_len);
+ if (body == NULL) {
+ return;
+ }
+ scratch[0] = '\0';
+ if (body_len > 0) {
+ if (body_len >= sizeof(scratch)) {
+ body_len = sizeof(scratch) - 1;
+ }
+ memcpy(scratch, body, body_len);
+ scratch[body_len] = '\0';
+ }
+ hybbx_proxychat_receive(service, from, scratch);
+ }
+}
+
+static hybbx_result_t proxy_peer_send(int fd, hybbx_circuit_proto_t proto,
+ const uint8_t *payload, size_t len)
+{
+ uint8_t frame[HYBBX_CIRCUIT_MAX_FRAME];
+ size_t frame_len;
+
+ if (fd < 0 || payload == NULL || len == 0) {
+ return HYBBX_ERR_INVALID;
+ }
+
+ frame_len = hybbx_circuit_encode(proto, HYBBX_CIRCUIT_FLAG_NONE,
+ payload, len, frame, sizeof(frame));
+ if (frame_len == 0) {
+ return HYBBX_ERR_IO;
+ }
+
+ return hybbx_circuit_link_write(fd, frame, frame_len);
+}
+
+static void proxy_peer_disconnect(mains_proxy_peer_runtime_t *peer)
+{
+ if (peer == NULL) {
+ return;
+ }
+
+ if (peer->fd >= 0) {
+ close(peer->fd);
+ peer->fd = -1;
+ }
+
+ if (g_rt.live_links > 0) {
+ g_rt.live_links--;
+ }
+
+ peer->reconnect_ms = MAINS_PROXY_RECONNECT_MS;
+}
+
+static void on_mesh_frame(hybbx_circuit_proto_t proto, uint16_t flags,
+ const uint8_t *payload, size_t len,
+ void *userdata)
+{
+ mains_proxy_peer_runtime_t *from_peer = (mains_proxy_peer_runtime_t *)userdata;
+ char relay_buf[HYBBX_CIRCUIT_MAX_PAYLOAD + 1];
+ unsigned ttl;
+ unsigned i;
+
+ (void)flags;
+
+ if (g_rt.service == NULL || payload == NULL || len == 0 ||
+ len > HYBBX_CIRCUIT_MAX_PAYLOAD) {
+ return;
+ }
+
+ if (proto != HYBBX_CIRCUIT_PROTO_PROXY_MAIL &&
+ proto != HYBBX_CIRCUIT_PROTO_PROXY_CHAT) {
+ return;
+ }
+
+ /* Always deliver locally first. */
+ proxy_dispatch_payload(g_rt.service, proto, payload, len);
+
+ /* Relay chat to all other live peers with TTL loop prevention. */
+ if (proto != HYBBX_CIRCUIT_PROTO_PROXY_CHAT) {
+ return;
+ }
+
+ ttl = proxy_payload_get_ttl((const char *)payload, len);
+ if (ttl <= 1) {
+ return;
+ }
+ ttl--;
+
+ if (len >= sizeof(relay_buf)) {
+ return;
+ }
+ memcpy(relay_buf, payload, len);
+ relay_buf[len] = '\0';
+
+ {
+ char ttl_str[32];
+ char *ttl_pos;
+ int n;
+
+ n = snprintf(ttl_str, sizeof(ttl_str), "ttl=%u", ttl);
+ if (n < 0 || (size_t)n >= sizeof(ttl_str)) {
+ return;
+ }
+
+ ttl_pos = strstr(relay_buf, "\nttl=");
+ if (ttl_pos == NULL) {
+ /*
+ * Also accept ttl= at the very beginning of the payload
+ * (proxy_build_chat_payload puts it as the second line).
+ */
+ if (strncmp(relay_buf, "ttl=", 4) == 0) {
+ ttl_pos = relay_buf;
+ } else {
+ /* No TTL field; drop rather than inject into a foreign payload. */
+ return;
+ }
+ } else {
+ /* Point at the 't' of "ttl=", not the leading newline. */
+ ttl_pos++;
+ }
+
+ {
+ char *start = ttl_pos + 4;
+ char *end = start;
+ size_t old_len;
+ size_t new_len;
+ size_t tail_len;
+
+ if (start == ttl_pos || *start == '\0' || *start == '\n' ||
+ *start == '\r') {
+ return;
+ }
+
+ /* Find end of current ttl line. */
+ while (*end != '\0' && *end != '\n' && *end != '\r') {
+ end++;
+ }
+
+ old_len = (size_t)(end - start);
+ new_len = strlen(ttl_str + 4);
+ tail_len = strlen(end);
+
+ /*
+ * New value is shorter or equal because TTL only decreases,
+ * so in-place replacement with memmove is safe.
+ */
+ if (new_len != old_len) {
+ memmove(start + new_len, end, tail_len + 1);
+ }
+ memcpy(start, ttl_str + 4, new_len);
+ }
+ }
+
+ for (i = 0; i < g_rt.mesh.peer_count; i++) {
+ mains_proxy_peer_runtime_t *target = &g_rt.peers[i];
+
+ if (target->fd < 0) {
+ continue;
+ }
+ if (target == from_peer) {
+ continue;
+ }
+ (void)proxy_peer_send(target->fd, proto,
+ (const uint8_t *)relay_buf, strlen(relay_buf));
+ }
+}
+
+static int peer_name_matches(const hybbx_mains_proxy_peer_config_t *peer,
+ const char *remote_service)
+{
+ if (peer == NULL || remote_service == NULL || remote_service[0] == '\0') {
+ return 0;
+ }
+
+ if (peer->peer_id[0] != '\0' &&
+ str_ieq_local(peer->peer_id, remote_service)) {
+ return 1;
+ }
+
+ if (peer->link_id[0] != '\0' &&
+ str_ieq_local(peer->link_id, remote_service)) {
+ return 1;
+ }
+
+ return 0;
+}
+
+static mains_proxy_peer_runtime_t *proxy_find_peer_for_service(
+ const char *remote_service)
+{
+ unsigned i;
+
+ for (i = 0; i < g_rt.mesh.peer_count; i++) {
+ if (g_rt.peers[i].fd < 0) {
+ continue;
+ }
+ if (peer_name_matches(&g_rt.peers[i].config, remote_service)) {
+ return &g_rt.peers[i];
+ }
+ }
+
+ return NULL;
+}
+
+static hybbx_result_t proxy_peer_connect(mains_proxy_peer_runtime_t *peer)
+{
+ const hybbx_mains_proxy_peer_config_t *cfg;
+ const char *host;
+ unsigned port;
+ const char *link_id;
+ const char *peer_label;
+ unsigned attempt;
+ hybbx_result_t rc;
+
+ if (peer == NULL) {
+ return HYBBX_ERR_INVALID;
+ }
+
+ cfg = &peer->config;
+ peer_label = cfg->peer_id[0] != '\0' ? cfg->peer_id : "(unnamed)";
+
+ if (!cfg->enabled) {
+ return HYBBX_OK;
+ }
+
+ if (cfg->wire == HYBBX_MAINS_PROXY_WIRE_AX25) {
+ hybbx_log_warn("[mains_proxy] peer '%s': wire=ax25 not active — use circuit",
+ peer_label);
+ return HYBBX_ERR_UNSUPPORTED;
+ }
+
+ if (cfg->circuit_host[0] == '\0') {
+ return HYBBX_OK;
+ }
+
+ if (cfg->link_id[0] == '\0' || cfg->link_password[0] == '\0') {
+ hybbx_log_warn("[mains_proxy] peer '%s' missing link_id or link_password",
+ peer_label);
+ return HYBBX_ERR_INVALID;
+ }
+
+ host = cfg->circuit_host;
+ port = cfg->circuit_port;
+ if (port == 0) {
+ port = HYBBX_CIRCUIT_DEFAULT_PORT;
+ }
+
+ link_id = cfg->link_id;
+
+ for (attempt = 0; attempt < MAINS_PROXY_CONNECT_ATTEMPTS; attempt++) {
+ rc = hybbx_circuit_link_connect(host, port, &peer->fd);
+ if (rc != HYBBX_OK) {
+ usleep(100000);
+ continue;
+ }
+
+ hybbx_circuit_decoder_init(&peer->dec);
+ rc = hybbx_circuit_link_authenticate(peer->fd, cfg->link_password,
+ "proxy", link_id);
+ if (rc != HYBBX_OK) {
+ close(peer->fd);
+ peer->fd = -1;
+ usleep(100000);
+ continue;
+ }
+
+ g_rt.live_links++;
+ peer->reconnect_ms = 0;
+ hybbx_log_info("[mains_proxy] linked to peer %s via HBX %s:%u link_id=%s",
+ peer_label, host, port, link_id);
+ return HYBBX_OK;
+ }
+
+ hybbx_log_warn("[mains_proxy] could not link peer '%s' at %s:%u",
+ peer_label, host, port);
+ return HYBBX_ERR_IO;
+}
+
+hybbx_mains_proxy_wire_t hybbx_mains_proxy_wire_parse(const char *value)
+{
+ if (value == NULL || value[0] == '\0') {
+ return HYBBX_MAINS_PROXY_WIRE_CIRCUIT;
+ }
+
+ if (str_ieq_local(value, "circuit") || str_ieq_local(value, "tcp") ||
+ str_ieq_local(value, "tcpip") || str_ieq_local(value, "tcp/ip")) {
+ return HYBBX_MAINS_PROXY_WIRE_CIRCUIT;
+ }
+
+ if (str_ieq_local(value, "ax25") || str_ieq_local(value, "packet_radio")) {
+ return HYBBX_MAINS_PROXY_WIRE_AX25;
+ }
+
+ return HYBBX_MAINS_PROXY_WIRE_CIRCUIT;
+}
+
+const char *hybbx_mains_proxy_wire_name(hybbx_mains_proxy_wire_t wire)
+{
+ switch (wire) {
+ case HYBBX_MAINS_PROXY_WIRE_AX25:
+ return "ax25";
+ case HYBBX_MAINS_PROXY_WIRE_CIRCUIT:
+ default:
+ return "circuit";
+ }
+}
+
+hybbx_mains_proxy_duplex_t hybbx_mains_proxy_duplex_parse(const char *value)
+{
+ if (value == NULL || value[0] == '\0') {
+ return HYBBX_MAINS_PROXY_DUPLEX_FULL;
+ }
+
+ if (str_ieq_local(value, "half") || str_ieq_local(value, "half-duplex") ||
+ str_ieq_local(value, "half_duplex")) {
+ return HYBBX_MAINS_PROXY_DUPLEX_HALF;
+ }
+
+ return HYBBX_MAINS_PROXY_DUPLEX_FULL;
+}
+
+const char *hybbx_mains_proxy_duplex_name(hybbx_mains_proxy_duplex_t duplex)
+{
+ return duplex == HYBBX_MAINS_PROXY_DUPLEX_HALF ? "half" : "full";
+}
+
+void hybbx_mains_proxy_peer_defaults(hybbx_mains_proxy_peer_config_t *peer)
+{
+ if (peer == NULL) {
+ return;
+ }
+
+ memset(peer, 0, sizeof(*peer));
+ peer->circuit_port = HYBBX_CIRCUIT_DEFAULT_PORT;
+ peer->wire = HYBBX_MAINS_PROXY_WIRE_CIRCUIT;
+ peer->duplex = HYBBX_MAINS_PROXY_DUPLEX_FULL;
+ peer->use_secondary = 1;
+ peer->enabled = 1;
+}
+
+hybbx_result_t hybbx_mains_proxy_peer_parse(const char *config,
+ hybbx_mains_proxy_peer_config_t *out)
+{
+ char scratch[HYBBX_CONFIG_LINE_MAX];
+ const char *value;
+ int legacy_host = 0;
+ int legacy_port = 0;
+
+ if (out == NULL) {
+ return HYBBX_ERR_INVALID;
+ }
+
+ hybbx_mains_proxy_peer_defaults(out);
+
+ if (config == NULL || config[0] == '\0') {
+ return HYBBX_OK;
+ }
+
+ value = mains_proxy_find_kv(config, "enabled", scratch, sizeof(scratch));
+ if (value != NULL) {
+ out->enabled = hybbx_parse_bool(value, 1);
+ }
+
+ value = mains_proxy_find_kv(config, "peer_id", scratch, sizeof(scratch));
+ if (value != NULL && value[0] != '\0') {
+ hybbx_strlcpy(out->peer_id, value, sizeof(out->peer_id));
+ }
+
+ value = mains_proxy_find_kv(config, "circuit_host", scratch,
+ sizeof(scratch));
+ if (value != NULL && value[0] != '\0') {
+ hybbx_strlcpy(out->circuit_host, value, sizeof(out->circuit_host));
+ }
+
+ value = mains_proxy_find_kv(config, "circuit_port", scratch,
+ sizeof(scratch));
+ if (value != NULL && value[0] != '\0') {
+ unsigned long port = strtoul(value, NULL, 10);
+
+ if (port >= 1u && port <= 65535u) {
+ out->circuit_port = (unsigned)port;
+ }
+ }
+
+ value = mains_proxy_find_kv(config, "link_id", scratch, sizeof(scratch));
+ if (value != NULL && value[0] != '\0') {
+ hybbx_strlcpy(out->link_id, value, sizeof(out->link_id));
+ }
+
+ value = mains_proxy_find_kv(config, "link_password", scratch,
+ sizeof(scratch));
+ if (value != NULL && value[0] != '\0') {
+ hybbx_strlcpy(out->link_password, value, sizeof(out->link_password));
+ }
+
+ value = mains_proxy_find_kv(config, "host", scratch, sizeof(scratch));
+ if (value != NULL && value[0] != '\0') {
+ hybbx_strlcpy(out->host, value, sizeof(out->host));
+ legacy_host = 1;
+ }
+
+ value = mains_proxy_find_kv(config, "port", scratch, sizeof(scratch));
+ if (value != NULL && value[0] != '\0') {
+ unsigned long port = strtoul(value, NULL, 10);
+
+ if (port >= 1u && port <= 65535u) {
+ out->port = (unsigned)port;
+ }
+ legacy_port = 1;
+ }
+
+ if (legacy_host && out->circuit_host[0] == '\0') {
+ hybbx_strlcpy(out->circuit_host, out->host,
+ sizeof(out->circuit_host));
+ hybbx_log_warn("[mains_proxy] peer '%s': deprecated key host= — use "
+ "circuit_host= (mapped for now)",
+ out->peer_id[0] != '\0' ? out->peer_id : "(unnamed)");
+ } else if (legacy_host) {
+ hybbx_log_warn("[mains_proxy] peer '%s': deprecated key host= ignored "
+ "(circuit_host set)",
+ out->peer_id[0] != '\0' ? out->peer_id : "(unnamed)");
+ }
+
+ if (legacy_port) {
+ hybbx_log_warn("[mains_proxy] peer '%s': deprecated key port= ignored — "
+ "use circuit_port= (HBX hub, default %u)",
+ out->peer_id[0] != '\0' ? out->peer_id : "(unnamed)",
+ (unsigned)HYBBX_CIRCUIT_DEFAULT_PORT);
+ }
+
+ value = mains_proxy_find_kv(config, "wire", scratch, sizeof(scratch));
+ out->wire = hybbx_mains_proxy_wire_parse(value);
+
+ value = mains_proxy_find_kv(config, "duplex", scratch, sizeof(scratch));
+ out->duplex = hybbx_mains_proxy_duplex_parse(value);
+
+ value = mains_proxy_find_kv(config, "use_secondary", scratch,
+ sizeof(scratch));
+ if (value != NULL) {
+ out->use_secondary = hybbx_parse_bool(value, 1);
+ }
+
+ return HYBBX_OK;
+}
+
+void hybbx_mains_proxy_mesh_init(hybbx_mains_proxy_mesh_t *mesh)
+{
+ if (mesh == NULL) {
+ return;
+ }
+
+ memset(mesh, 0, sizeof(*mesh));
+}
+
+hybbx_result_t hybbx_mains_proxy_mesh_start(hybbx_service_t *service,
+ hybbx_mains_proxy_mesh_t *mesh)
+{
+ unsigned i;
+ unsigned configured = 0;
+ unsigned linked = 0;
+
+ if (mesh == NULL) {
+ return HYBBX_ERR_INVALID;
+ }
+
+ hybbx_mains_proxy_mesh_stop(mesh);
+
+ memset(&g_rt, 0, sizeof(g_rt));
+ g_rt.service = service;
+ g_rt.mesh = *mesh;
+
+ for (i = 0; i < mesh->peer_count; i++) {
+ g_rt.peers[i].config = mesh->peers[i];
+ g_rt.peers[i].fd = -1;
+ hybbx_circuit_decoder_init(&g_rt.peers[i].dec);
+
+ if (!mesh->peers[i].enabled) {
+ continue;
+ }
+
+ configured++;
+
+ if (mesh->peers[i].circuit_host[0] == '\0') {
+ continue;
+ }
+
+ if (proxy_peer_connect(&g_rt.peers[i]) == HYBBX_OK &&
+ g_rt.peers[i].fd >= 0) {
+ linked++;
+ }
+ }
+
+ if (configured == 0) {
+ hybbx_log_warn("[mains_proxy] no active peers configured");
+ return HYBBX_ERR_NOT_FOUND;
+ }
+
+ mesh->running = 1;
+ g_rt.mesh.running = 1;
+
+ hybbx_log_info("[mains_proxy] mesh started (%u peer(s) configured, %u HBX link(s))",
+ configured, linked);
+
+ return HYBBX_OK;
+}
+
+void hybbx_mains_proxy_mesh_stop(hybbx_mains_proxy_mesh_t *mesh)
+{
+ unsigned i;
+
+ if (mesh != NULL && mesh->running) {
+ hybbx_log_info("[mains_proxy] mesh stopped");
+ mesh->running = 0;
+ }
+
+ for (i = 0; i < HYBBX_MAINS_PROXY_MAX_PEERS; i++) {
+ proxy_peer_disconnect(&g_rt.peers[i]);
+ }
+
+ memset(&g_rt, 0, sizeof(g_rt));
+}
+
+void hybbx_mains_proxy_mesh_tick(hybbx_service_t *service,
+ hybbx_mains_proxy_mesh_t *mesh)
+{
+ unsigned i;
+ static unsigned tick_ms;
+
+ (void)service;
+
+ if (mesh == NULL || !mesh->running) {
+ return;
+ }
+
+ tick_ms += 50;
+ if (tick_ms >= 1000) {
+ tick_ms = 0;
+ }
+
+ for (i = 0; i < mesh->peer_count; i++) {
+ mains_proxy_peer_runtime_t *peer = &g_rt.peers[i];
+ struct pollfd pfd;
+ uint8_t buf[512];
+ size_t read_len;
+ hybbx_result_t rc;
+ int pr;
+
+ if (!peer->config.enabled) {
+ continue;
+ }
+
+ if (peer->fd < 0) {
+ if (peer->config.circuit_host[0] == '\0') {
+ continue;
+ }
+ if (peer->reconnect_ms > 0) {
+ if (tick_ms != 0) {
+ continue;
+ }
+ if (peer->reconnect_ms > 50) {
+ peer->reconnect_ms -= 50;
+ continue;
+ }
+ peer->reconnect_ms = 0;
+ }
+ (void)proxy_peer_connect(peer);
+ continue;
+ }
+
+ pfd.fd = peer->fd;
+ pfd.events = POLLIN;
+ pfd.revents = 0;
+ pr = poll(&pfd, 1, 0);
+ if (pr <= 0) {
+ continue;
+ }
+
+ if ((pfd.revents & (POLLERR | POLLHUP | POLLNVAL)) != 0) {
+ hybbx_log_stats("[mains_proxy] peer '%s' disconnected",
+ peer->config.peer_id[0] != '\0' ? peer->config.peer_id
+ : "(unnamed)");
+ proxy_peer_disconnect(peer);
+ continue;
+ }
+
+ if ((pfd.revents & POLLIN) == 0) {
+ continue;
+ }
+
+ rc = hybbx_circuit_link_read(peer->fd, buf, sizeof(buf), &read_len);
+ if (rc != HYBBX_OK || read_len == 0) {
+ proxy_peer_disconnect(peer);
+ continue;
+ }
+
+ hybbx_circuit_decoder_feed(&peer->dec, buf, read_len,
+ on_mesh_frame, peer);
+ }
+}
+
+int hybbx_mains_proxy_mesh_active(void)
+{
+ return g_rt.mesh.running && g_rt.live_links > 0;
+}
+
+hybbx_result_t hybbx_mains_proxy_send_mail(hybbx_service_t *service,
+ const char *from_address,
+ const char *to_address,
+ const char *subject,
+ const char *body)
+{
+ char user[HYBBX_USER_NAME_MAX];
+ char remote[HYBBX_PROXYMAIL_SERVICE_NAME_MAX];
+ char payload[HYBBX_CIRCUIT_MAX_PAYLOAD + 1];
+ size_t payload_len;
+ mains_proxy_peer_runtime_t *peer;
+
+ (void)service;
+
+ if (!g_rt.mesh.running || g_rt.live_links == 0) {
+ return HYBBX_ERR_UNSUPPORTED;
+ }
+
+ if (!hybbx_proxymail_parse_address(to_address, user, sizeof(user),
+ remote, sizeof(remote))) {
+ return HYBBX_ERR_INVALID;
+ }
+
+ peer = proxy_find_peer_for_service(remote);
+ if (peer == NULL) {
+ return HYBBX_ERR_NOT_FOUND;
+ }
+
+ payload_len = proxy_build_mail_payload(payload, sizeof(payload),
+ from_address, to_address,
+ subject != NULL ? subject : "",
+ body != NULL ? body : "",
+ MAINS_PROXY_DEFAULT_TTL);
+ if (payload_len == 0) {
+ return HYBBX_ERR_INVALID;
+ }
+
+ return proxy_peer_send(peer->fd, HYBBX_CIRCUIT_PROTO_PROXY_MAIL,
+ (const uint8_t *)payload, payload_len);
+}
+
+hybbx_result_t hybbx_mains_proxy_send_chat(hybbx_service_t *service,
+ const char *from_address,
+ const char *line)
+{
+ char payload[HYBBX_CIRCUIT_MAX_PAYLOAD + 1];
+ size_t payload_len;
+ unsigned i;
+ hybbx_result_t rc = HYBBX_ERR_NOT_FOUND;
+ int sent = 0;
+
+ (void)service;
+
+ if (!g_rt.mesh.running || g_rt.live_links == 0 || line == NULL ||
+ line[0] == '\0') {
+ return HYBBX_ERR_UNSUPPORTED;
+ }
+
+ payload_len = proxy_build_chat_payload(payload, sizeof(payload),
+ from_address, line,
+ MAINS_PROXY_DEFAULT_TTL);
+ if (payload_len == 0) {
+ return HYBBX_ERR_INVALID;
+ }
+
+ for (i = 0; i < g_rt.mesh.peer_count; i++) {
+ if (g_rt.peers[i].fd < 0) {
+ continue;
+ }
+
+ if (proxy_peer_send(g_rt.peers[i].fd, HYBBX_CIRCUIT_PROTO_PROXY_CHAT,
+ (const uint8_t *)payload,
+ payload_len) == HYBBX_OK) {
+ sent = 1;
+ rc = HYBBX_OK;
+ }
+ }
+
+ return sent ? rc : HYBBX_ERR_NOT_FOUND;
+}
+
+void hybbx_mains_proxy_inbound_frame(hybbx_service_t *service,
+ hybbx_circuit_proto_t proto,
+ const uint8_t *payload, size_t len)
+{
+ if (service == NULL || payload == NULL || len == 0) {
+ return;
+ }
+
+ if (proto != HYBBX_CIRCUIT_PROTO_PROXY_MAIL &&
+ proto != HYBBX_CIRCUIT_PROTO_PROXY_CHAT) {
+ return;
+ }
+
+ proxy_dispatch_payload(service, proto, payload, len);
+}
git clone -b <branch> https://cgit.mode42.com/<repo>.git
git clone -b <branch> git://cgit.mode42.com/<repo>.git

info@mode42.com