Skip to content

Commit 840be47

Browse files
committed
clusterer_controller: a consumer messaging API over the encrypted plane
The controller already owns an authenticated, encrypted, rate-limited UDP plane between the nodes of a cluster, plus the membership that says who is on it. Any other module wanting to exchange a message between nodes had to build that transport again from scratch. This exports it instead. A consumer registers a named channel pre-fork and sends on it: clctr_api_t clctr; if (load_clctr_api(&clctr) < 0) ... /* mod_init */ clctr.register_channel(&ch, my_cb); /* mod_init - PRE-FORK */ clctr.send_mcast(cluster_id, &ch, &payload, 0); clctr.send_ucast(cluster_id, node_id, &ch, &payload, 0); and inherits the XChaCha20-Poly1305 group session key and its rotation, the per-packet receive gauntlet (magic gate, cluster_id filter, size bound, per-source rate limiting) and the membership view, with no transport code of its own. The delivery contract is the part worth reading, because it is what a consumer gets wrong: - the receive callback runs in the CONTROLLER's worker process for that cluster, not in the process that called send. A consumer needing to wake a different process brings its own mechanism (shm + eventfd, ipc_send_rpc). Callbacks run on the cluster's receive path, so they must stay short. - sends are marshalled to that same worker over IPC, which is what keeps ordering per node and leaves the anti-replay sequence space single-writer. - CLCTR_SEND_TO_SELF dispatches locally rather than listening for our own packet, and a unicast to our own node id degenerates to exactly that, with nothing on the wire. - src_node_id is 0 when the sender had not been assigned an id yet. CLCTR_SEND_RELIABLE asks for acknowledgement and resend while unacknowledged. It is opt-in per send rather than per channel on purpose: it costs one ACK per recipient, so a reliable broadcast turns a single packet into N-1 packets back. Most consumer traffic is better served by being idempotent and retried by its own logic. Payload sizing has a deliberate split: CLCTR_MAX_PAYLOAD is a compile-time lower bound for consumers that size local buffers statically, while the real runtime limit is cc_max_payload, derived from the interface MTU at mod_init and larger on jumbo-frame links. Ships with tests for the consumer sequence, channel filtering, reliable broadcast, and the script-facing messaging surface.
1 parent 22cb916 commit 840be47

9 files changed

Lines changed: 1966 additions & 34 deletions

‎modules/clusterer_controller/api.h‎

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
/*
2+
* Copyright (C) 2026 VoIPcloud
3+
*
4+
* This file is part of opensips, a free SIP server.
5+
*
6+
* opensips is free software; you can redistribute it and/or modify
7+
* it under the terms of the GNU General Public License as published by
8+
* the Free Software Foundation; either version 2 of the License, or
9+
* (at your option) any later version.
10+
*
11+
* opensips is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
* GNU General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU General Public License
17+
* along with this program; if not, write to the Free Software
18+
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19+
*/
20+
21+
/*
22+
* Consumer messaging API - lets other modules (and, through them, the
23+
* script) exchange messages over the controller's encrypted UDP plane
24+
* instead of building their own transport. A consumer inherits the
25+
* XChaCha20-Poly1305 group session key (with its rotation), the
26+
* per-packet receive gauntlet (magic gate, cluster_id filter, size
27+
* bound, per-source rate limiting) and the controller's membership,
28+
* with zero transport code of its own.
29+
*
30+
* Usage:
31+
* clctr_api_t clctr;
32+
* if (load_clctr_api(&clctr) < 0) ... (mod_init)
33+
* clctr.register_channel(&ch, my_cb); (mod_init - PRE-FORK)
34+
* clctr.send_mcast(cluster_id, &ch, &payload, 0);
35+
* clctr.send_ucast(cluster_id, node_id, &ch, &payload, 0);
36+
*
37+
* Delivery contract:
38+
* - the receive callback runs in the CONTROLLER'S WORKER PROCESS for
39+
* that cluster, not in the process that called send. A consumer
40+
* that must wake another process brings its own mechanism (shm +
41+
* eventfd, ipc_send_rpc, ...). Keep callbacks short - they run on
42+
* the cluster's receive path.
43+
* - sends are marshalled to the same worker over IPC, so ordering is
44+
* preserved per node and the anti-replay sequence space stays
45+
* single-writer.
46+
* - CLCTR_SEND_TO_SELF delivers to this node by invoking the local
47+
* callback directly - never by hearing our own packet back. A
48+
* unicast addressed to our own node id degenerates to exactly that
49+
* local dispatch, with no packet on the wire.
50+
* - src_node_id is 0 when the sender had not been assigned an id yet.
51+
*/
52+
53+
#ifndef CL_CTR_API_H
54+
#define CL_CTR_API_H
55+
56+
#include "../../str.h"
57+
#include "../../sr_module.h"
58+
59+
/* deliver to this node too, via local callback dispatch (never off the wire) */
60+
#define CLCTR_SEND_TO_SELF (1 << 0)
61+
/* Ask for the message to be acknowledged, and resent while it is not.
62+
*
63+
* Costs one ACK per recipient, so a reliable send to the whole cluster is
64+
* N-1 packets back where a plain one was nothing - deliberately opt-in, and
65+
* deliberately per send rather than per channel: most consumer traffic is
66+
* better served by being idempotent and retried by its own logic, which is
67+
* how the cross-node cache fetch works and why it asks for nothing here. */
68+
#define CLCTR_SEND_RELIABLE (1 << 1)
69+
70+
/* limits a consumer can rely on */
71+
#define CLCTR_MAX_CHAN_LEN 31
72+
/* Compile-time lower bound for consumer payload; the actual runtime limit is
73+
* cc_max_payload, which is derived from the interface MTU at mod_init and may
74+
* be larger on jumbo-frame links. Consumers that size local buffers at
75+
* compile time should use CLCTR_MAX_PAYLOAD; consumers that want to send the
76+
* largest possible message at runtime should check cc_max_payload instead. */
77+
#define CLCTR_MAX_PAYLOAD 1300
78+
extern int cc_max_payload;
79+
80+
typedef void (*clctr_msg_cb_f)(int cluster_id, int src_node_id,
81+
str *channel, str *payload);
82+
83+
/* register a named channel; PRE-FORK only (call from mod_init).
84+
* Returns 0 on success, -1 on bad name / duplicate / table full. */
85+
typedef int (*clctr_register_channel_f)(str *channel, clctr_msg_cb_f cb);
86+
87+
/* send to the cluster's multicast group / to one node by id.
88+
* Returns 0 = accepted for sending, -1 = bad arguments or unknown
89+
* cluster, -2 = cluster not ready (no worker / not joined yet).
90+
* "Accepted" means handed to the cluster worker - UDP gives no
91+
* delivery guarantee, by design (consumers must be loss-tolerant). */
92+
typedef int (*clctr_send_mcast_f)(int cluster_id, str *channel,
93+
str *payload, int flags);
94+
typedef int (*clctr_send_ucast_f)(int cluster_id, int node_id,
95+
str *channel, str *payload, int flags);
96+
97+
/* this node's controller-assigned id in the cluster; 0 = none yet */
98+
/* Send to a named set of nodes. Deliberately N unicasts rather than one
99+
* multicast with the receivers filtering: a multicast is decrypted by every
100+
* member, so "addressed to three of you" would still hand the payload to all
101+
* of them. The cost is linear in the size of the list, which is the honest
102+
* price of addressing a subset.
103+
*
104+
* Returns the number of nodes the message was sent to, or -1 on error. Nodes
105+
* in the list that are not current members are skipped and counted in
106+
* @unknown when it is not NULL. */
107+
typedef int (*clctr_send_list_f)(int cluster_id, const int *node_ids, int n,
108+
str *channel, str *payload, int flags,
109+
int *unknown);
110+
111+
typedef int (*clctr_get_my_node_id_f)(int cluster_id);
112+
113+
/*
114+
* This node's own address on the cluster plane, as RESOLVED at startup - not
115+
* the raw modparam. It comes from one of three places (explicit `my_ip`, the
116+
* IPv4 of an explicit `interface`, or a default-route probe towards the
117+
* multicast group), and which one won is not otherwise visible to a consumer
118+
* module. @src, when non-NULL, receives a short constant describing that
119+
* origin. Both outputs point at module-static storage, valid for the process
120+
* lifetime; neither must be freed. Returns 0 on success, -1 before mod_init
121+
* has resolved it.
122+
*
123+
* Worth exposing because a wrong answer here is not cosmetic: joining the
124+
* multicast group on the wrong interface is exactly how a node ends up unable
125+
* to decrypt its peers' traffic.
126+
*/
127+
typedef int (*clctr_get_my_ip_f)(const char **ip, const char **iface,
128+
const char **src);
129+
130+
typedef struct clctr_api {
131+
clctr_register_channel_f register_channel;
132+
clctr_send_mcast_f send_mcast;
133+
clctr_send_ucast_f send_ucast;
134+
clctr_send_list_f send_list;
135+
clctr_get_my_node_id_f get_my_node_id;
136+
clctr_get_my_ip_f get_my_ip;
137+
} clctr_api_t;
138+
139+
typedef int (*load_clctr_f)(clctr_api_t *api);
140+
141+
static inline int load_clctr_api(clctr_api_t *api)
142+
{
143+
load_clctr_f load_clctr;
144+
145+
load_clctr = (load_clctr_f)(void *)find_export("load_clctr", 0);
146+
if (!load_clctr)
147+
return -1;
148+
return load_clctr(api);
149+
}
150+
151+
#endif /* CL_CTR_API_H */

0 commit comments

Comments
 (0)