libe3 0.2.5
C++ library for E3AP RAN vendor integration
Loading...
Searching...
No Matches
posix_connector.cpp
Go to the documentation of this file.
1/**
2 * @file posix_connector.cpp
3 * @brief POSIX socket connector implementation
4 *
5 * Ported from the original C implementation's posix_* functions in e3_connector.c
6 *
7 * SPDX-FileCopyrightText: Copyright (c) 2026 Northeastern University
8 * SPDX-License-Identifier: Apache-2.0
9 */
10
11#include "posix_connector.hpp"
12#include "libe3/latrec.h"
13#include "libe3/logger.hpp"
14#include <sys/socket.h>
15#include <sys/uio.h>
16#include <sys/un.h>
17#include <sys/stat.h>
18#include <netinet/in.h>
19#include <netinet/tcp.h>
20#ifdef LIBE3_ENABLE_SCTP
21#include <netinet/sctp.h>
22#endif
23#include <arpa/inet.h>
24#include <unistd.h>
25#include <grp.h>
26#include <fcntl.h>
27#include <sys/time.h>
28#include <algorithm>
29#include <cstring>
30#include <cerrno>
31#include <poll.h>
32#include <thread>
33#include <chrono>
34
35namespace libe3 {
36
37namespace {
38constexpr const char* LOG_TAG = "PosixConn";
39constexpr const char* IPC_BASE_DIR = "/tmp/dapps";
40constexpr int POLL_TIMEOUT_MS = 500; // Timeout for graceful shutdown
41constexpr int SEND_TIMEOUT_MS = 500; // Per-peer broadcast send timeout (drop a stalled peer)
42constexpr int SETUP_RECV_TIMEOUT_MS = 2000; // Give up on a silent setup peer (bounded, not forever)
43
44/**
45 * @brief Wait for socket to become ready with timeout
46 * @param sockfd Socket file descriptor
47 * @param timeout_ms Timeout in milliseconds
48 * @return 1 if ready, 0 if timeout, -1 on error
49 */
50int wait_for_socket(int sockfd, int timeout_ms) {
51 struct pollfd pfd;
52 pfd.fd = sockfd;
53 pfd.events = POLLIN;
54 pfd.revents = 0;
55 return poll(&pfd, 1, timeout_ms);
56}
57
58/**
59 * @brief Disable Nagle-style segment bundling on a connected data socket.
60 *
61 * Without this every framed message (small length prefix + payload) stalls
62 * roughly one RTT waiting for the previous segment's SACK/ACK: measured as a
63 * hard ~214 msg/s ceiling on SCTP regardless of offered rate. Real-time
64 * control-loop traffic must be flushed per message, so NODELAY is set on
65 * every accepted and connected TCP/SCTP data socket (Linux does not reliably
66 * inherit it from the listener). No-op for UNIX-domain (IPC) sockets.
67 */
68/**
69 * @brief Create the AF_INET stream socket for an SCTP endpoint.
70 *
71 * SCTP is optional at build time (LIBE3_ENABLE_SCTP, off by default): it is the
72 * only transport needing a kernel module and a distro -dev package for one
73 * header, and the deployed E3 links are IPC or TCP. The enumerator stays in the
74 * ABI either way, so a caller can still ask for SCTP against a build without it
75 * -- that has to fail loudly here rather than quietly fall through to TCP, which
76 * would look like it worked while putting traffic on the wrong protocol.
77 *
78 * Returns -1 with errno set, matching socket(), so every existing caller's error
79 * path already handles it.
80 */
81int sctp_stream_socket() {
82#ifdef LIBE3_ENABLE_SCTP
83 return socket(AF_INET, SOCK_STREAM, IPPROTO_SCTP);
84#else
85 E3_LOG_ERROR(LOG_TAG) << "SCTP transport requested but libe3 was built without it. "
86 "Rebuild with -DLIBE3_ENABLE_SCTP=ON (needs libsctp-dev), "
87 "or use the TCP or IPC transport.";
88 errno = EPROTONOSUPPORT;
89 return -1;
90#endif
91}
92
93void set_nodelay(int sockfd, E3TransportLayer transport) {
94 int one = 1;
95 int ret = 0;
96 if (transport == E3TransportLayer::TCP) {
97 ret = setsockopt(sockfd, IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one));
98 } else if (transport == E3TransportLayer::SCTP) {
99#ifdef LIBE3_ENABLE_SCTP
100 ret = setsockopt(sockfd, IPPROTO_SCTP, SCTP_NODELAY, &one, sizeof(one));
101#else
102 // Unreachable: sctp_stream_socket() already failed for this transport.
103 (void)one;
104#endif
105 }
106 if (ret != 0) {
107 E3_LOG_WARN(LOG_TAG) << "Failed to set NODELAY: " << strerror(errno);
108 }
109}
110
111/**
112 * @brief Bound how long a broadcast send() blocks on a single peer socket.
113 *
114 * A dApp that connects and then stops reading fills its socket buffer.
115 * Without a send timeout the RAN's outbound I/O thread would block forever in
116 * send_in_chunks, stalling delivery to every other peer and hanging stop().
117 * With SO_SNDTIMEO a stalled send fails with EAGAIN/EWOULDBLOCK, the peer is
118 * dropped, and the rest keep receiving, mirroring ZMQ PUB's "drop the slow
119 * subscriber" behavior.
120 */
121void set_send_timeout(int sockfd, int timeout_ms) {
122 struct timeval tv;
123 tv.tv_sec = timeout_ms / 1000;
124 tv.tv_usec = (timeout_ms % 1000) * 1000;
125 if (setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)) != 0) {
126 E3_LOG_WARN(LOG_TAG) << "Failed to set SO_SNDTIMEO: " << strerror(errno);
127 }
128}
129
130/**
131 * @brief Bound how long a blocking recv() waits on a socket.
132 *
133 * Used on the accepted setup connection: without it a peer that connects to
134 * the setup port and never sends would wedge the setup thread in recv()
135 * forever, blocking every later handshake and hanging stop() on the join
136 * (shutdown() only unblocks the listeners). On timeout recv() returns
137 * EAGAIN/EWOULDBLOCK and the caller drops the stale connection.
138 */
139void set_recv_timeout(int sockfd, int timeout_ms) {
140 struct timeval tv;
141 tv.tv_sec = timeout_ms / 1000;
142 tv.tv_usec = (timeout_ms % 1000) * 1000;
143 if (setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)) != 0) {
144 E3_LOG_WARN(LOG_TAG) << "Failed to set SO_RCVTIMEO: " << strerror(errno);
145 }
146}
147
148/**
149 * @brief Put a listening socket in non-blocking mode.
150 *
151 * If a client resets the connection between the poll() that reported the
152 * listener readable and the following accept(), a blocking listener makes
153 * accept() wait for the next connection and stalls the whole I/O thread. A
154 * non-blocking listener returns EAGAIN instead, which the accept sites treat
155 * as "nothing to accept right now".
156 */
157void set_nonblocking(int sockfd) {
158 int flags = fcntl(sockfd, F_GETFL, 0);
159 if (flags < 0 || fcntl(sockfd, F_SETFL, flags | O_NONBLOCK) < 0) {
160 E3_LOG_WARN(LOG_TAG) << "Failed to set O_NONBLOCK: " << strerror(errno);
161 }
162}
163
164/**
165 * @brief Accept every connection currently pending on a listener.
166 *
167 * Non-blocking (0-timeout poll before each accept, and the listener itself is
168 * O_NONBLOCK). Each accepted peer socket gets NODELAY and a bounded send
169 * timeout, then is appended to @p peers. Returns the number of peers accepted
170 * in this call.
171 */
172int drain_accept(int listener, E3TransportLayer transport,
173 std::vector<int>& peers, const char* label) {
174 int accepted = 0;
175 while (listener >= 0 && wait_for_socket(listener, 0) == 1) {
176 int fd = accept(listener, nullptr, nullptr);
177 if (fd < 0) {
178 break; // EAGAIN/EINTR/shutdown: nothing more to accept now
179 }
180 set_nodelay(fd, transport);
181 set_send_timeout(fd, SEND_TIMEOUT_MS);
182 peers.push_back(fd);
183 ++accepted;
184 E3_LOG_INFO(LOG_TAG) << label << " peer connected ("
185 << peers.size() << " total)";
186 }
187 return accepted;
188}
189}
190
191namespace {
192/**
193 * @brief Strip ZMQ-style URI prefix (e.g. "ipc://", "tcp://") from endpoint strings
194 *
195 * POSIX sockets need plain filesystem paths or addresses, not ZMQ URIs.
196 * For IPC: "ipc:///tmp/dapps/setup" -> "/tmp/dapps/setup"
197 * For TCP: "tcp://127.0.0.1:9990" -> "127.0.0.1:9990"
198 */
199std::string strip_uri_prefix(const std::string& endpoint) {
200 auto pos = endpoint.find("://");
201 if (pos != std::string::npos) {
202 return endpoint.substr(pos + 3);
203 }
204 return endpoint;
205}
206} // anonymous namespace
207
210 const std::string& setup_endpoint,
211 const std::string& inbound_endpoint,
212 const std::string& outbound_endpoint,
213 uint16_t setup_port,
214 uint16_t inbound_port,
215 uint16_t outbound_port
216)
217 : transport_layer_(transport_layer)
218 , setup_connection_socket_(UNUSED_SOCKET)
219 , inbound_connection_socket_(UNUSED_SOCKET)
220 , outbound_connection_socket_(UNUSED_SOCKET)
221 , setup_port_(setup_port)
222 , inbound_port_(inbound_port)
223 , outbound_port_(outbound_port)
224{
225 setup_endpoint_ = strip_uri_prefix(setup_endpoint);
226 inbound_endpoint_ = strip_uri_prefix(inbound_endpoint);
227 outbound_endpoint_ = strip_uri_prefix(outbound_endpoint);
228
229 E3_LOG_INFO(LOG_TAG) << "Creating POSIX connector";
230 E3_LOG_DEBUG(LOG_TAG) << " Setup endpoint: " << setup_endpoint_;
231 E3_LOG_DEBUG(LOG_TAG) << " Inbound endpoint: " << inbound_endpoint_;
232 E3_LOG_DEBUG(LOG_TAG) << " Outbound endpoint: " << outbound_endpoint_;
233 E3_LOG_DEBUG(LOG_TAG) << " Setup port: " << setup_port_;
234 E3_LOG_DEBUG(LOG_TAG) << " Inbound port: " << inbound_port_;
235 E3_LOG_DEBUG(LOG_TAG) << " Outbound port: " << outbound_port_;
236}
237
241
243 int sock;
244 int ret;
245
246 // Create IPC directory if needed
247 if (transport_layer_ == E3TransportLayer::IPC) {
248 struct stat st{};
249 if (stat(IPC_BASE_DIR, &st) == -1) {
250 if (mkdir(IPC_BASE_DIR, 0777) == -1 && errno != EEXIST) {
251 E3_LOG_ERROR(LOG_TAG) << "Failed to create IPC directory: " << strerror(errno);
252 return ErrorCode::CONNECTION_FAILED;
253 }
254 }
255 chmod(IPC_BASE_DIR, 0777);
256 }
257
258 if (transport_layer_ == E3TransportLayer::SCTP) {
259 sock = sctp_stream_socket();
260 int reuse = 1;
261 setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
262 struct sockaddr_in addr{};
263 addr.sin_family = AF_INET;
264 addr.sin_port = htons(setup_port_);
265 addr.sin_addr.s_addr = INADDR_ANY;
266 ret = bind(sock, reinterpret_cast<struct sockaddr*>(&addr), sizeof(addr));
267 }
268 else if (transport_layer_ == E3TransportLayer::TCP) {
269 sock = socket(AF_INET, SOCK_STREAM, 0);
270 int reuse = 1;
271 setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
272 struct sockaddr_in addr{};
273 addr.sin_family = AF_INET;
274 addr.sin_port = htons(setup_port_);
275 addr.sin_addr.s_addr = INADDR_ANY;
276 ret = bind(sock, reinterpret_cast<struct sockaddr*>(&addr), sizeof(addr));
277 }
278 else { // IPC
279 sock = socket(AF_UNIX, SOCK_STREAM, 0);
280 struct sockaddr_un addr{};
281 addr.sun_family = AF_UNIX;
282 strncpy(addr.sun_path, setup_endpoint_.c_str(), sizeof(addr.sun_path) - 1);
283 unlink(setup_endpoint_.c_str()); // Remove existing socket file
284 ret = bind(sock, reinterpret_cast<struct sockaddr*>(&addr), sizeof(addr));
285 }
286
287 if (sock < 0) {
288 E3_LOG_ERROR(LOG_TAG) << "Failed to create setup socket: " << strerror(errno);
289 return ErrorCode::CONNECTION_FAILED;
290 }
291
292 if (ret < 0) {
293 E3_LOG_ERROR(LOG_TAG) << "Failed to bind setup socket: " << strerror(errno);
294 close(sock);
295 return ErrorCode::CONNECTION_FAILED;
296 }
297
298 ret = listen(sock, 5);
299 if (ret < 0) {
300 E3_LOG_ERROR(LOG_TAG) << "Failed to listen on setup socket: " << strerror(errno);
301 close(sock);
302 return ErrorCode::CONNECTION_FAILED;
303 }
304 set_nonblocking(sock); // accept() must not block the setup thread on a client RST
305
306 setup_socket_ = sock;
307 connected_ = true;
308
309 E3_LOG_INFO(LOG_TAG) << "Setup socket listening on " << setup_endpoint_;
310 return ErrorCode::SUCCESS;
311}
312
313int PosixE3Connector::recv_setup_request(std::vector<uint8_t>& buffer) {
314 if (setup_socket_ < 0) {
315 return static_cast<int>(ErrorCode::NOT_CONNECTED);
316 }
317
318 // Wait for incoming connection with timeout
319 int poll_ret = wait_for_socket(setup_socket_, POLL_TIMEOUT_MS);
320 if (poll_ret == 0) {
321 // Timeout - return 0 to allow shutdown check
322 return 0;
323 }
324 if (poll_ret < 0) {
325 if (errno == EINTR) {
326 return 0; // Interrupted, allow shutdown check
327 }
328 E3_LOG_ERROR(LOG_TAG) << "Poll failed on setup socket: " << strerror(errno);
329 return static_cast<int>(ErrorCode::TRANSPORT_ERROR);
330 }
331
332 // Close the previous accepted setup connection before accepting a new one;
333 // otherwise every dApp after the first leaks one fd. The prior setup
334 // exchange's send_response has already completed by the time the setup
335 // loop calls recv_setup_request again.
336 if (setup_connection_socket_ >= 0) {
337 close(setup_connection_socket_);
338 setup_connection_socket_ = -1;
339 }
340
341 // Accept connection
342 setup_connection_socket_ = accept(setup_socket_, nullptr, nullptr);
343 if (setup_connection_socket_ < 0) {
344 if (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK || shutdown_requested_.load()) {
345 return 0; // Allow shutdown check
346 }
347 E3_LOG_ERROR(LOG_TAG) << "Failed to accept setup connection: " << strerror(errno);
348 return static_cast<int>(ErrorCode::TRANSPORT_ERROR);
349 }
350 set_nodelay(setup_connection_socket_, transport_layer_);
351 // Bound the blocking recv below so a peer that connects and never sends
352 // cannot wedge the setup thread (and hang stop()) or block later handshakes.
353 set_recv_timeout(setup_connection_socket_, SETUP_RECV_TIMEOUT_MS);
354
355 buffer.resize(DEFAULT_BUFFER_SIZE);
356 ssize_t ret = recv(setup_connection_socket_, buffer.data(), buffer.size(), 0);
357 if (ret < 0) {
358 if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) {
359 // Silent (or interrupted) peer: drop this connection and let the
360 // setup loop re-poll the listener / observe shutdown, rather than
361 // blocking here and starving other dApps' handshakes.
362 close(setup_connection_socket_);
363 setup_connection_socket_ = -1;
364 return 0;
365 }
366 E3_LOG_ERROR(LOG_TAG) << "Failed to receive setup request: " << strerror(errno);
367 return static_cast<int>(ErrorCode::TRANSPORT_ERROR);
368 }
369
370 buffer.resize(static_cast<size_t>(ret));
371 E3_LOG_DEBUG(LOG_TAG) << "Received setup request: " << ret << " bytes";
372 return static_cast<int>(ret);
373}
374
375ErrorCode PosixE3Connector::send_response(const std::vector<uint8_t>& data) {
376 if (setup_connection_socket_ < 0) {
377 return ErrorCode::NOT_CONNECTED;
378 }
379
380 int ret = send_in_chunks(setup_connection_socket_, data.data(), data.size());
381 if (ret < 0) {
382 return ErrorCode::TRANSPORT_ERROR;
383 }
384
385 E3_LOG_DEBUG(LOG_TAG) << "Sent response: " << data.size() << " bytes";
386 return ErrorCode::SUCCESS;
387}
388
390 int sock;
391 int ret;
392
393 // Ensure IPC directory exists (defensive — setup_initial_connection normally creates it)
394 if (transport_layer_ == E3TransportLayer::IPC) {
395 struct stat st{};
396 if (stat(IPC_BASE_DIR, &st) == -1) {
397 if (mkdir(IPC_BASE_DIR, 0777) == -1 && errno != EEXIST) {
398 E3_LOG_ERROR(LOG_TAG) << "Failed to create IPC directory: " << strerror(errno);
399 return ErrorCode::CONNECTION_FAILED;
400 }
401 }
402 chmod(IPC_BASE_DIR, 0777);
403 }
404
405 if (transport_layer_ == E3TransportLayer::SCTP) {
406 sock = sctp_stream_socket();
407 int reuse = 1;
408 setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
409 struct sockaddr_in addr{};
410 addr.sin_family = AF_INET;
411 addr.sin_port = htons(inbound_port_);
412 addr.sin_addr.s_addr = INADDR_ANY;
413 ret = bind(sock, reinterpret_cast<struct sockaddr*>(&addr), sizeof(addr));
414 }
415 else if (transport_layer_ == E3TransportLayer::TCP) {
416 sock = socket(AF_INET, SOCK_STREAM, 0);
417 int reuse = 1;
418 setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
419 struct sockaddr_in addr{};
420 addr.sin_family = AF_INET;
421 addr.sin_port = htons(inbound_port_);
422 addr.sin_addr.s_addr = INADDR_ANY;
423 ret = bind(sock, reinterpret_cast<struct sockaddr*>(&addr), sizeof(addr));
424 }
425 else { // IPC
426 sock = socket(AF_UNIX, SOCK_STREAM, 0);
427 struct sockaddr_un addr{};
428 addr.sun_family = AF_UNIX;
429 strncpy(addr.sun_path, inbound_endpoint_.c_str(), sizeof(addr.sun_path) - 1);
430 unlink(inbound_endpoint_.c_str());
431 ret = bind(sock, reinterpret_cast<struct sockaddr*>(&addr), sizeof(addr));
432 }
433
434 if (sock < 0) {
435 E3_LOG_ERROR(LOG_TAG) << "Failed to create inbound socket: " << strerror(errno);
436 return ErrorCode::CONNECTION_FAILED;
437 }
438
439 if (ret < 0) {
440 E3_LOG_ERROR(LOG_TAG) << "Failed to bind inbound socket: " << strerror(errno);
441 close(sock);
442 return ErrorCode::CONNECTION_FAILED;
443 }
444
445 ret = listen(sock, 5);
446 if (ret < 0) {
447 E3_LOG_ERROR(LOG_TAG) << "Failed to listen on inbound socket: " << strerror(errno);
448 close(sock);
449 return ErrorCode::CONNECTION_FAILED;
450 }
451 set_nonblocking(sock); // drain_accept() must not block the inbound thread on a client RST
452
453 inbound_socket_ = sock;
454
455 // Multi-peer: do not block waiting for the first dApp here. receive()
456 // polls the listener alongside every accepted peer socket, so peers can
457 // attach (and re-attach) at any point in the connector's lifetime.
458 E3_LOG_INFO(LOG_TAG) << "Inbound listener ready (multi-peer)";
459 return ErrorCode::SUCCESS;
460}
461
462int PosixE3Connector::receive(std::vector<uint8_t>& buffer) {
463 // On the dApp role our peer (RAN) uses send() = send_in_chunks() which
464 // emits a 4-byte length-prefix frame, so we decode it accordingly. The
465 // dApp keeps its original single-socket path.
466 if (role_ == E3Role::DAPP) {
467 if (inbound_connection_socket_ < 0) {
468 return static_cast<int>(ErrorCode::NOT_CONNECTED);
469 }
470 int poll_ret = wait_for_socket(inbound_connection_socket_, POLL_TIMEOUT_MS);
471 if (poll_ret == 0) {
472 return 0; // Timeout - allow shutdown check
473 }
474 if (poll_ret < 0) {
475 if (errno == EINTR) {
476 return 0; // Interrupted, allow shutdown check
477 }
478 E3_LOG_ERROR(LOG_TAG) << "Poll failed on receive: " << strerror(errno);
479 return static_cast<int>(ErrorCode::TRANSPORT_ERROR);
480 }
481 return recv_with_size(inbound_connection_socket_, buffer);
482 }
483
484 // RAN role, multi-peer: poll the listener (new peers) plus every
485 // accepted subscriber socket. Raw (unframed) recv per peer is preserved
486 // for compatibility with the Python dApp framework's framing convention.
487 if (inbound_socket_ < 0) {
488 return static_cast<int>(ErrorCode::NOT_CONNECTED);
489 }
490
491 std::vector<struct pollfd> pfds;
492 pfds.reserve(1 + inbound_peer_sockets_.size());
493 pfds.push_back({inbound_socket_, POLLIN, 0});
494 for (int fd : inbound_peer_sockets_) {
495 pfds.push_back({fd, POLLIN, 0});
496 }
497
498 int poll_ret = poll(pfds.data(), pfds.size(), POLL_TIMEOUT_MS);
499 if (poll_ret == 0) {
500 return 0; // Timeout - allow shutdown check
501 }
502 if (poll_ret < 0) {
503 if (errno == EINTR) {
504 return 0; // Interrupted, allow shutdown check
505 }
506 E3_LOG_ERROR(LOG_TAG) << "Poll failed on receive: " << strerror(errno);
507 return static_cast<int>(ErrorCode::TRANSPORT_ERROR);
508 }
509
510 if (pfds[0].revents & POLLIN) {
511 drain_accept(inbound_socket_, transport_layer_, inbound_peer_sockets_,
512 "Inbound (subscriber)");
513 }
514
515 // Read from the next ready peer, returning its data (remaining ready peers
516 // stay readable and are picked up on the next call; the inbound loop calls
517 // receive() continuously). The scan starts from a rotating cursor so a peer
518 // with continuously-queued data cannot starve the others.
519 const size_t n_peers = pfds.size() - 1; // pfds[0] is the listener
520 for (size_t k = 0; k < n_peers; ++k) {
521 const size_t idx = (inbound_rr_ + k) % n_peers; // 0-based peer index
522 const size_t i = idx + 1; // pfds index (0 == listener)
523 const short re = pfds[i].revents;
524 if (!(re & (POLLIN | POLLHUP | POLLERR | POLLNVAL))) {
525 continue;
526 }
527 const int fd = pfds[i].fd;
528 bool broken = (re & (POLLHUP | POLLERR | POLLNVAL)) != 0;
529 ssize_t ret = -1;
530 if (re & POLLIN) {
531 buffer.resize(DEFAULT_BUFFER_SIZE);
532 ret = recv(fd, buffer.data(), buffer.size(), 0);
533 if (ret == 0) {
534 broken = true;
535 } else if (ret < 0 && errno != EINTR && errno != EAGAIN && errno != EWOULDBLOCK) {
536 broken = true;
537 }
538 }
539 // Deliver any bytes we actually read before acting on a broken flag: on
540 // AF_UNIX/IPC a clean disconnect after a final message (control, report,
541 // release) arrives as POLLIN|POLLHUP on the same poll, so dropping first
542 // would silently lose that last message. A broken peer with nothing left
543 // to read is dropped on the next call, where recv() returns 0/EAGAIN and
544 // ret > 0 is false (this also keeps the 100% CPU busy-loop guarded: a
545 // half-broken socket with no data drops immediately below).
546 if (ret > 0) {
547 buffer.resize(static_cast<size_t>(ret));
548 E3_LOG_TRACE(LOG_TAG) << "Received: " << ret << " bytes";
549 inbound_rr_ = idx + 1; // next call starts after the peer we just served
550 return static_cast<int>(ret);
551 }
552 if (broken) {
553 // Peer closed or hard error and nothing left to deliver: drop it and
554 // keep serving the rest. pfds is a local copy, so continuing to scan
555 // its other fds is safe.
556 close(fd);
557 inbound_peer_sockets_.erase(
558 std::remove(inbound_peer_sockets_.begin(),
559 inbound_peer_sockets_.end(), fd),
560 inbound_peer_sockets_.end());
561 E3_LOG_INFO(LOG_TAG) << "Inbound (subscriber) peer disconnected ("
562 << inbound_peer_sockets_.size() << " remain)";
563 continue;
564 }
565 }
566 // Nothing read this round: advance the cursor so the start point keeps rotating.
567 if (n_peers > 0) {
568 inbound_rr_ = (inbound_rr_ + 1) % n_peers;
569 }
570 return 0;
571}
572
574 int sock;
575 int ret;
576
577 // Ensure IPC directory exists (defensive — setup_initial_connection normally creates it)
578 if (transport_layer_ == E3TransportLayer::IPC) {
579 struct stat st{};
580 if (stat(IPC_BASE_DIR, &st) == -1) {
581 if (mkdir(IPC_BASE_DIR, 0777) == -1 && errno != EEXIST) {
582 E3_LOG_ERROR(LOG_TAG) << "Failed to create IPC directory: " << strerror(errno);
583 return ErrorCode::CONNECTION_FAILED;
584 }
585 }
586 chmod(IPC_BASE_DIR, 0777);
587 }
588
589 if (transport_layer_ == E3TransportLayer::SCTP) {
590 sock = sctp_stream_socket();
591 struct sockaddr_in addr{};
592 addr.sin_family = AF_INET;
593 addr.sin_port = htons(outbound_port_);
594 addr.sin_addr.s_addr = INADDR_ANY;
595 ret = bind(sock, reinterpret_cast<struct sockaddr*>(&addr), sizeof(addr));
596 }
597 else if (transport_layer_ == E3TransportLayer::TCP) {
598 sock = socket(AF_INET, SOCK_STREAM, 0);
599 struct sockaddr_in addr{};
600 addr.sin_family = AF_INET;
601 addr.sin_port = htons(outbound_port_);
602 addr.sin_addr.s_addr = INADDR_ANY;
603 ret = bind(sock, reinterpret_cast<struct sockaddr*>(&addr), sizeof(addr));
604 }
605 else { // IPC
606 sock = socket(AF_UNIX, SOCK_STREAM, 0);
607 struct sockaddr_un addr{};
608 addr.sun_family = AF_UNIX;
609 strncpy(addr.sun_path, outbound_endpoint_.c_str(), sizeof(addr.sun_path) - 1);
610 unlink(outbound_endpoint_.c_str());
611 ret = bind(sock, reinterpret_cast<struct sockaddr*>(&addr), sizeof(addr));
612 }
613
614 if (sock < 0) {
615 E3_LOG_ERROR(LOG_TAG) << "Failed to create outbound socket: " << strerror(errno);
616 return ErrorCode::CONNECTION_FAILED;
617 }
618
619 if (ret < 0) {
620 E3_LOG_ERROR(LOG_TAG) << "Failed to bind outbound socket: " << strerror(errno);
621 close(sock);
622 return ErrorCode::CONNECTION_FAILED;
623 }
624
625 ret = listen(sock, 5);
626 if (ret < 0) {
627 E3_LOG_ERROR(LOG_TAG) << "Failed to listen on outbound socket: " << strerror(errno);
628 close(sock);
629 return ErrorCode::CONNECTION_FAILED;
630 }
631 set_nonblocking(sock); // drain_accept() must not block the outbound thread on a client RST
632
633 outbound_socket_ = sock;
634
635 // Multi-peer: no blocking accept here. send() drains the listener before
636 // each broadcast, so peers can attach at any time; with no peer attached
637 // a send is dropped, mirroring ZMQ PUB semantics with no subscriber.
638 E3_LOG_INFO(LOG_TAG) << "Outbound listener ready (multi-peer)";
639 return ErrorCode::SUCCESS;
640}
641
642ErrorCode PosixE3Connector::send(const std::vector<uint8_t>& data) {
643 // Checked first, same as the recv() paths: shutdown() only calls
644 // ::shutdown(SHUT_RDWR) on the socket (the fd itself stays valid until
645 // dispose() closes it), so a send racing a concurrent shutdown() would
646 // otherwise hit a real EPIPE/ECONNRESET and be indistinguishable from a
647 // genuine transport failure. Narrows, but does not eliminate, that race:
648 // this check and the syscall below are not atomic with shutdown_requested_
649 // being set.
650 if (shutdown_requested_.load()) {
651 return ErrorCode::CANCELLED;
652 }
653
654 // dApp role sends to RAN's receive() which uses raw recv. Use a raw
655 // send to match. The dApp keeps its original single-socket path.
656 if (role_ == E3Role::DAPP) {
657 if (outbound_connection_socket_ < 0) {
658 return ErrorCode::NOT_CONNECTED;
659 }
660 ssize_t sent = ::send(outbound_connection_socket_, data.data(), data.size(), 0);
661 if (sent < 0 || static_cast<size_t>(sent) != data.size()) {
662 E3_LOG_ERROR(LOG_TAG) << "Failed to send dApp outbound: " << strerror(errno);
663 return ErrorCode::TRANSPORT_ERROR;
664 }
665 E3_LOG_TRACE(LOG_TAG) << "Sent (dApp raw): " << data.size() << " bytes";
666 return ErrorCode::SUCCESS;
667 }
668
669 // RAN role, multi-peer: accept any newly connected peers, then broadcast
670 // the framed message to all of them. Delivery mirrors ZMQ PUB/SUB: every
671 // peer gets every message and each dApp filters by its own identifier;
672 // with no peer attached the message is dropped (PUB with no subscriber).
673 if (outbound_socket_ < 0) {
674 return ErrorCode::NOT_CONNECTED;
675 }
676 drain_accept(outbound_socket_, transport_layer_, outbound_peer_sockets_,
677 "Outbound (indication)");
678
679 bool any_failed = false;
680 for (auto it = outbound_peer_sockets_.begin();
681 it != outbound_peer_sockets_.end();) {
682 if (send_in_chunks(*it, data.data(), data.size()) < 0) {
683 // Dead peer: drop it and keep serving the rest.
684 close(*it);
685 it = outbound_peer_sockets_.erase(it);
686 any_failed = true;
687 E3_LOG_INFO(LOG_TAG) << "Outbound (indication) peer disconnected ("
688 << outbound_peer_sockets_.size() << " remain)";
689 } else {
690 ++it;
691 }
692 }
693
694 if (any_failed && outbound_peer_sockets_.empty()) {
695 return ErrorCode::TRANSPORT_ERROR;
696 }
697 E3_LOG_TRACE(LOG_TAG) << "Sent: " << data.size() << " bytes to "
698 << outbound_peer_sockets_.size() << " peer(s)";
699 return ErrorCode::SUCCESS;
700}
701
702// ===========================================================================
703// Client-side (dApp role) implementations
704//
705// Mirrors of the server-side methods but with connect() in place of
706// bind()/listen()/accept(). For IPC the dApp connects to setup_endpoint_
707// (same as RAN binds). For TCP the dApp connects to 127.0.0.1:port. For
708// SCTP the same TCP-style address is used; ubuntu-latest runners lack
709// the sctp kernel module so SCTP client creation may fail at socket()
710// time — callers should treat that as "transport not supported".
711// ===========================================================================
712
713namespace {
714// Connect a fresh socket to (addr, port_or_path) for the given transport.
715// Returns the fd on success or -1 on error.
716int posix_connect_for(E3TransportLayer transport,
717 const std::string& endpoint,
718 uint16_t port) {
719 if (transport == E3TransportLayer::IPC) {
720 int sock = socket(AF_UNIX, SOCK_STREAM, 0);
721 if (sock < 0) return -1;
722 struct sockaddr_un addr{};
723 addr.sun_family = AF_UNIX;
724 strncpy(addr.sun_path, endpoint.c_str(), sizeof(addr.sun_path) - 1);
725 if (connect(sock, reinterpret_cast<struct sockaddr*>(&addr), sizeof(addr)) < 0) {
726 close(sock);
727 return -1;
728 }
729 return sock;
730 }
731 int sock;
732 if (transport == E3TransportLayer::SCTP) {
733 sock = sctp_stream_socket();
734 } else {
735 sock = socket(AF_INET, SOCK_STREAM, 0);
736 }
737 if (sock < 0) return -1;
738 struct sockaddr_in addr{};
739 addr.sin_family = AF_INET;
740 addr.sin_port = htons(port);
741 addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); // 127.0.0.1
742 if (connect(sock, reinterpret_cast<struct sockaddr*>(&addr), sizeof(addr)) < 0) {
743 close(sock);
744 return -1;
745 }
746 set_nodelay(sock, transport);
747 return sock;
748}
749} // anonymous namespace
750
752 // Retry with a short backoff so we don't race the RAN's bind on startup.
753 int sock = -1;
754 for (int retry = 0; retry < 10 && sock < 0; ++retry) {
755 sock = posix_connect_for(transport_layer_, setup_endpoint_, setup_port_);
756 if (sock < 0) {
757 if (shutdown_requested_.load()) return ErrorCode::CANCELLED;
758 std::this_thread::sleep_for(std::chrono::milliseconds(200));
759 }
760 }
761 if (sock < 0) {
762 E3_LOG_ERROR(LOG_TAG) << "Failed to connect setup socket: " << strerror(errno);
763 return ErrorCode::CONNECTION_FAILED;
764 }
765 setup_connection_socket_ = sock;
766 connected_ = true;
767 E3_LOG_INFO(LOG_TAG) << "Setup socket connected (client) to "
768 << setup_endpoint_ << " port=" << setup_port_;
769 return ErrorCode::SUCCESS;
770}
771
773 if (setup_connection_socket_ < 0) return ErrorCode::NOT_CONNECTED;
774 // The RAN's recv_setup_request reads via raw recv() (no length prefix),
775 // so the dApp must send the SetupRequest the same way. The setup
776 // channel doesn't carry chunked messages so a single send() is fine.
777 ssize_t sent = ::send(setup_connection_socket_, data.data(), data.size(), 0);
778 if (sent < 0 || static_cast<size_t>(sent) != data.size()) {
779 E3_LOG_ERROR(LOG_TAG) << "Failed to send setup request: " << strerror(errno);
780 return ErrorCode::TRANSPORT_ERROR;
781 }
782 E3_LOG_DEBUG(LOG_TAG) << "Sent SetupRequest: " << data.size() << " bytes";
783 return ErrorCode::SUCCESS;
784}
785
786int PosixE3Connector::recv_setup_response_client(std::vector<uint8_t>& buffer) {
787 if (setup_connection_socket_ < 0) return static_cast<int>(ErrorCode::NOT_CONNECTED);
788
789 // Wait for data with timeout to allow cooperative shutdown.
790 int poll_ret = wait_for_socket(setup_connection_socket_, POLL_TIMEOUT_MS);
791 if (poll_ret == 0) return 0;
792 if (poll_ret < 0) return static_cast<int>(ErrorCode::TRANSPORT_ERROR);
793
794 return recv_with_size(setup_connection_socket_, buffer);
795}
796
798 // dApp inbound = RAN's outbound endpoint (publisher).
799 int sock = -1;
800 for (int retry = 0; retry < 10 && sock < 0; ++retry) {
801 sock = posix_connect_for(transport_layer_, outbound_endpoint_, outbound_port_);
802 if (sock < 0) {
803 if (shutdown_requested_.load()) return ErrorCode::CANCELLED;
804 std::this_thread::sleep_for(std::chrono::milliseconds(200));
805 }
806 }
807 if (sock < 0) {
808 E3_LOG_ERROR(LOG_TAG) << "Failed to connect inbound socket: " << strerror(errno);
809 return ErrorCode::CONNECTION_FAILED;
810 }
811 inbound_connection_socket_ = sock;
812 E3_LOG_INFO(LOG_TAG) << "Inbound socket connected (client) to "
813 << outbound_endpoint_ << " port=" << outbound_port_;
814 return ErrorCode::SUCCESS;
815}
816
818 // dApp outbound = RAN's inbound endpoint (subscriber).
819 int sock = -1;
820 for (int retry = 0; retry < 10 && sock < 0; ++retry) {
821 sock = posix_connect_for(transport_layer_, inbound_endpoint_, inbound_port_);
822 if (sock < 0) {
823 if (shutdown_requested_.load()) return ErrorCode::CANCELLED;
824 std::this_thread::sleep_for(std::chrono::milliseconds(200));
825 }
826 }
827 if (sock < 0) {
828 E3_LOG_ERROR(LOG_TAG) << "Failed to connect outbound socket: " << strerror(errno);
829 return ErrorCode::CONNECTION_FAILED;
830 }
831 outbound_connection_socket_ = sock;
832 E3_LOG_INFO(LOG_TAG) << "Outbound socket connected (client) to "
833 << inbound_endpoint_ << " port=" << inbound_port_;
834 return ErrorCode::SUCCESS;
835}
836
838 E3_LOG_DEBUG(LOG_TAG) << "Shutting down POSIX connector";
839 shutdown_requested_.store(true);
840
841 // Close listening sockets to interrupt blocking accept() calls
842 if (setup_socket_ >= 0) {
843 ::shutdown(setup_socket_, SHUT_RDWR);
844 }
845 if (inbound_socket_ >= 0) {
846 ::shutdown(inbound_socket_, SHUT_RDWR);
847 }
848 // The outbound listener was missing here, so a RAN whose outbound
849 // accept() was still waiting for a dApp could never be joined by stop().
850 if (outbound_socket_ >= 0) {
851 ::shutdown(outbound_socket_, SHUT_RDWR);
852 }
853}
854
856 E3_LOG_DEBUG(LOG_TAG) << "Disposing POSIX connector";
857
858 if (inbound_connection_socket_ != UNUSED_SOCKET && inbound_connection_socket_ >= 0) {
859 close(inbound_connection_socket_);
860 }
861
862 if (setup_connection_socket_ != UNUSED_SOCKET && setup_connection_socket_ >= 0) {
863 close(setup_connection_socket_);
864 }
865
866 if (outbound_connection_socket_ != UNUSED_SOCKET && outbound_connection_socket_ >= 0) {
867 close(outbound_connection_socket_);
868 }
869
870 for (int fd : inbound_peer_sockets_) {
871 close(fd);
872 }
873 inbound_peer_sockets_.clear();
874 for (int fd : outbound_peer_sockets_) {
875 close(fd);
876 }
877 outbound_peer_sockets_.clear();
878
879 if (inbound_socket_ >= 0) {
880 close(inbound_socket_);
881 }
882
883 if (outbound_socket_ >= 0) {
884 close(outbound_socket_);
885 }
886
887 if (setup_socket_ >= 0) {
888 close(setup_socket_);
889 }
890
891 // Clean up IPC socket files only if we created them (RAN side binds).
892 // The dApp side connects, so it has nothing to unlink.
893 if (transport_layer_ == E3TransportLayer::IPC && role_ == E3Role::RAN) {
894 unlink(setup_endpoint_.c_str());
895 unlink(inbound_endpoint_.c_str());
896 unlink(outbound_endpoint_.c_str());
897 // Deliberately do NOT rmdir(IPC_BASE_DIR): it is shared across agents
898 // and tests. Removing it races with another agent binding a socket
899 // inside it (ENOENT). Leaving the empty directory behind is harmless.
900 }
901
902 setup_socket_ = -1;
903 setup_connection_socket_ = UNUSED_SOCKET;
904 inbound_socket_ = -1;
905 inbound_connection_socket_ = UNUSED_SOCKET;
906 outbound_socket_ = -1;
907 outbound_connection_socket_ = UNUSED_SOCKET;
908 connected_ = false;
909
910 E3_LOG_INFO(LOG_TAG) << "POSIX connector disposed";
911}
912
913int PosixE3Connector::send_in_chunks(int sockfd, const uint8_t* buffer, size_t buffer_size) {
914 // Coalesce the 4-byte length prefix (network byte order) and the payload
915 // into a single writev() so each message costs one syscall and one wire
916 // segment instead of two. The old separate send() for the prefix produced
917 // a tiny segment per message, which interacted badly with Nagle-style
918 // bundling (see set_nodelay) and doubled the syscall cost of the data path.
919 uint32_t network_order_size = htonl(static_cast<uint32_t>(buffer_size));
920 struct iovec iov[2];
921 iov[0].iov_base = &network_order_size;
922 iov[0].iov_len = sizeof(network_order_size);
923 iov[1].iov_base = const_cast<uint8_t*>(buffer);
924 iov[1].iov_len = buffer_size;
925
926 size_t total_len = sizeof(network_order_size) + buffer_size;
927 size_t total_sent = 0;
928 while (total_sent < total_len) {
929 ssize_t sent;
930 if (total_sent < sizeof(network_order_size)) {
931 // First pass (or short write inside the prefix): send prefix +
932 // payload together, skipping whatever already went out.
933 struct iovec cur[2];
934 cur[0].iov_base = reinterpret_cast<uint8_t*>(&network_order_size) + total_sent;
935 cur[0].iov_len = sizeof(network_order_size) - total_sent;
936 cur[1] = iov[1];
937 sent = ::writev(sockfd, cur, 2);
938 } else {
939 size_t payload_sent = total_sent - sizeof(network_order_size);
940 size_t to_send = std::min(static_cast<size_t>(CHUNK_SIZE),
941 buffer_size - payload_sent);
942 sent = ::send(sockfd, buffer + payload_sent, to_send, 0);
943 }
944 if (sent < 0) {
945 if (errno == EINTR) continue;
946 E3_LOG_ERROR(LOG_TAG) << "Failed to send data: " << strerror(errno);
947 return -1;
948 }
949 total_sent += static_cast<size_t>(sent);
950 }
951
952 return 0;
953}
954
955int PosixE3Connector::recv_with_size(int sockfd, std::vector<uint8_t>& buffer) {
956 // Read the 4-byte size prefix (network byte order) — mirrors send_in_chunks
957 uint32_t network_order_size = 0;
958 size_t header_received = 0;
959 while (header_received < sizeof(network_order_size)) {
960 ssize_t ret = ::recv(sockfd,
961 reinterpret_cast<uint8_t*>(&network_order_size) + header_received,
962 sizeof(network_order_size) - header_received, 0);
963 if (ret <= 0) {
964 if (ret == 0) {
965 E3_LOG_DEBUG(LOG_TAG) << "Connection closed while reading size header";
966 } else {
967 E3_LOG_ERROR(LOG_TAG) << "Failed to receive size header: " << strerror(errno);
968 }
969 return ret == 0 ? 0 : static_cast<int>(ErrorCode::TRANSPORT_ERROR);
970 }
971 header_received += static_cast<size_t>(ret);
972 }
973
974 uint32_t payload_size = ntohl(network_order_size);
975 if (payload_size == 0 || payload_size > MAX_PROTOCOL_DATA_SIZE * 2) {
976 E3_LOG_ERROR(LOG_TAG) << "Invalid payload size: " << payload_size;
977 return static_cast<int>(ErrorCode::TRANSPORT_ERROR);
978 }
979
980 buffer.resize(payload_size);
981 size_t total_received = 0;
982 while (total_received < payload_size) {
983 size_t to_read = std::min(static_cast<size_t>(CHUNK_SIZE), payload_size - total_received);
984 ssize_t ret = ::recv(sockfd, buffer.data() + total_received, to_read, 0);
985 if (ret <= 0) {
986 if (ret == 0) {
987 E3_LOG_ERROR(LOG_TAG) << "Connection closed during chunked receive";
988 } else {
989 E3_LOG_ERROR(LOG_TAG) << "Failed to receive chunk: " << strerror(errno);
990 }
991 return static_cast<int>(ErrorCode::TRANSPORT_ERROR);
992 }
993 total_received += static_cast<size_t>(ret);
994 }
995
996 E3_LOG_TRACE(LOG_TAG) << "recv_with_size: " << payload_size << " bytes";
997 return static_cast<int>(payload_size);
998}
999
1000} // namespace libe3
std::string inbound_endpoint_
std::string outbound_endpoint_
std::string setup_endpoint_
const std::string & inbound_endpoint() const noexcept
Get inbound endpoint.
const std::string & setup_endpoint() const noexcept
Get setup endpoint.
const std::string & outbound_endpoint() const noexcept
Get outbound endpoint.
ErrorCode send_setup_request_client(const std::vector< uint8_t > &data) override
Send a SetupRequest on the setup channel (dApp role).
int recv_setup_request(std::vector< uint8_t > &buffer) override
Receive a setup request.
void dispose() override
Clean up and release all resources.
int receive(std::vector< uint8_t > &buffer) override
Receive data from the inbound channel.
ErrorCode send(const std::vector< uint8_t > &data) override
Send data on the outbound channel.
ErrorCode setup_initial_connection_client() override
Connect the setup channel (dApp role: REQ connect).
E3TransportLayer transport_layer() const noexcept override
Get the transport layer type.
int recv_setup_response_client(std::vector< uint8_t > &buffer) override
Receive a SetupResponse on the setup channel (dApp role).
PosixE3Connector(E3TransportLayer transport_layer, const std::string &setup_endpoint, const std::string &inbound_endpoint, const std::string &outbound_endpoint, uint16_t setup_port, uint16_t inbound_port, uint16_t outbound_port)
ErrorCode send_response(const std::vector< uint8_t > &data) override
Send a setup response.
void shutdown() override
Interrupt blocking operations for shutdown.
ErrorCode setup_outbound_connection_client() override
Connect the outbound channel (dApp role: PUB connect to the RAN's subscriber endpoint).
ErrorCode setup_inbound_connection_client() override
Connect the inbound channel (dApp role: SUB connect to the RAN's publisher endpoint).
ErrorCode setup_initial_connection() override
Set up the initial connection (setup channel).
ErrorCode setup_inbound_connection() override
Set up the inbound connection (SUB channel).
ErrorCode setup_outbound_connection() override
Set up the outbound connection (PUB channel).
Lock-free per-thread latency stamp recorder (header-only, C99).
Logging facility for libe3.
#define E3_LOG_TRACE(component)
Definition logger.hpp:229
#define E3_LOG_WARN(component)
Definition logger.hpp:226
#define E3_LOG_DEBUG(component)
Definition logger.hpp:228
#define E3_LOG_INFO(component)
Definition logger.hpp:227
#define E3_LOG_ERROR(component)
Definition logger.hpp:225
ErrorCode
Error codes returned by libe3 operations Use shared error-code list (see libe3/error_codes....
Definition types.hpp:141
E3TransportLayer
Transport layer types (matches Python E3TransportLayer).
Definition types.hpp:83
@ SCTP
SCTP (O-RAN standard).
Definition types.hpp:84
@ IPC
Unix Domain Sockets / IPC.
Definition types.hpp:86
@ RAN
RAN function (binds sockets; today's only behaviour).
Definition types.hpp:76
@ DAPP
dApp (connects to a RAN agent)
Definition types.hpp:77
constexpr size_t MAX_PROTOCOL_DATA_SIZE
Definition types.hpp:160
constexpr size_t DEFAULT_BUFFER_SIZE
Definition types.hpp:165
POSIX socket-based E3 Connector.