Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions cmake/libe3Tests.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,16 @@ target_include_directories(libe3_test_framework INTERFACE

file(GLOB LIBE3_TEST_SOURCES RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "tests/*.cpp")

# Tests that hardcode the ASN.1 wire format: test_asn1_size exercises the
# APER encoder directly, and test_e2e_report_path configures its agent and
# fake-dApp peer with EncodingFormat::ASN1. On builds with
# LIBE3_ENABLE_ASN1=OFF the encoder factory cannot create an ASN.1 encoder,
# so these tests can only fail; exclude them instead of weakening them.
set(LIBE3_ASN1_ONLY_TESTS
asn1_size
e2e_report_path
)

foreach(test_src IN LISTS LIBE3_TEST_SOURCES)
# Derive a target name from the source file name: tests/test_foo.cpp -> test_foo
get_filename_component(test_name ${test_src} NAME_WE)
Expand All @@ -27,6 +37,10 @@ foreach(test_src IN LISTS LIBE3_TEST_SOURCES)
message(STATUS "Skipping test_json_encoder: JSON support disabled")
continue()
endif()
if(NOT LIBE3_ENABLE_ASN1 AND simple_name IN_LIST LIBE3_ASN1_ONLY_TESTS)
message(STATUS "Skipping test_${simple_name}: ASN.1 support disabled")
continue()
endif()

add_executable(${target_name} "${CMAKE_CURRENT_SOURCE_DIR}/${test_src}")
target_link_libraries(${target_name}
Expand Down
20 changes: 20 additions & 0 deletions include/libe3/e3_interface.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,26 @@ class E3Interface {
void handle_release_message(const ReleaseMessage &release);
void handle_dapp_disconnection(uint32_t dapp_id);

/**
* @brief Reply to a setup request that cannot be answered positively.
*
* The RAN side of the setup channel is a REQ/REP exchange: exactly one
* reply must be sent per received request before the next one can be
* received (a ZMQ REP socket enforces this in its state machine).
* Bailing out without replying (undecodable bytes, wrong PDU type,
* response-encode failure) wedges the setup channel for every
* subsequent dApp until the agent restarts. The encoding is fixed and
* known a priori by both peers, so a negative SetupResponse can always
* be produced; the peer gets an explicit rejection instead of a
* silent timeout.
*
* @param request_id Message ID of the offending request, or 0 when the
* request bytes never decoded — a fresh id is then
* substituted on the wire (E3-MessageID excludes 0;
* dApps do not correlate setup replies by ID).
*/
void send_negative_setup_reply(uint32_t request_id);

// dApp-role handlers
void handle_setup_response(const SetupResponse& response);
void handle_subscription_response(const SubscriptionResponse& response);
Expand Down
61 changes: 52 additions & 9 deletions src/core/e3_interface.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -352,26 +352,32 @@ void E3Interface::setup_loop_ran() {

E3_LOG_INFO(LOG_TAG) << "Setup request received: " << ret << " bytes";

// Decode the setup request
// Decode the setup request. On any failure the REQ/REP exchange
// must still be completed (see send_negative_setup_reply) or the
// setup channel wedges for every subsequent dApp.
auto decode_result = encoder_->decode(buffer.data(), static_cast<size_t>(ret));
if (!decode_result) {
E3_LOG_ERROR(LOG_TAG) << "Failed to decode setup request; ret=" << ret;
E3_LOG_WARN(LOG_TAG) << "Undecodable setup message (" << ret
<< " bytes, wrong encoding or garbage); rejecting";
send_negative_setup_reply(0);
continue;
}

Pdu& pdu = *decode_result;
if (pdu.type != PduType::SETUP_REQUEST) {
E3_LOG_ERROR(LOG_TAG) << "Unexpected PDU type in setup: "
<< pdu_type_to_string(pdu.type);
E3_LOG_WARN(LOG_TAG) << "Unexpected PDU type in setup: "
<< pdu_type_to_string(pdu.type) << "; rejecting";
send_negative_setup_reply(pdu.message_id);
continue;
}

auto* request = std::get_if<SetupRequest>(&pdu.choice);
if (!request) {
E3_LOG_ERROR(LOG_TAG) << "Failed to get SetupRequest from PDU";
E3_LOG_WARN(LOG_TAG) << "Failed to get SetupRequest from PDU; rejecting";
send_negative_setup_reply(pdu.message_id);
continue;
}

handle_setup_request(*request, pdu.message_id);
}

Expand Down Expand Up @@ -640,9 +646,11 @@ void E3Interface::handle_setup_request(const SetupRequest& request, uint32_t req

if (!encode_result) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this happens then there's a bug on our end, so probably the solution is abort

E3_LOG_ERROR(LOG_TAG) << "Failed to encode setup response for request id " << request_message_id;
// Still complete the REQ/REP exchange so the setup channel survives.
send_negative_setup_reply(request_message_id);
return;
}

ErrorCode send_result = connector_->send_response(encode_result->buffer);
if (send_result != ErrorCode::SUCCESS) {
E3_LOG_ERROR(LOG_TAG) << "Failed to send setup response for request id " << request_message_id
Expand All @@ -652,6 +660,41 @@ void E3Interface::handle_setup_request(const SetupRequest& request, uint32_t req
}
}

void E3Interface::send_negative_setup_reply(uint32_t request_id) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's have every new function documented in such a way that is picked up by the documentation.

// A bare negative SetupResponse omits every OPTIONAL field
// (ranFunctionList included), so with the fixed, mutually known
// encoding it always encodes. The mandatory fields have to satisfy
// their schema constraints, though: requestId is E3-MessageID
// INTEGER (1..1000) — an unrecoverable id (0) falls back to a freshly
// generated one — and ranIdentifier is SIZE (1..64), hence the
// placeholder when the config left it empty.
uint32_t message_id = generate_message_id();
if (request_id == 0) {
request_id = message_id;
}
auto encode_result = encoder_->encode_setup_response(
message_id,
request_id,
ResponseCode::NEGATIVE,
config_.e3ap_version,
std::nullopt, // no dApp identifier assigned
config_.ran_identifier.empty() ? "unknown" : config_.ran_identifier,
{} // no RAN functions advertised

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think probably #30 outcome is that this should be null, thoughts?

);

if (!encode_result) {
E3_LOG_ERROR(LOG_TAG) << "Failed to encode negative setup response for request id "
<< request_id << "; setup channel may be left unusable";
return;
}

ErrorCode send_result = connector_->send_response(encode_result->buffer);
if (send_result != ErrorCode::SUCCESS) {
E3_LOG_WARN(LOG_TAG) << "Failed to send negative setup response for request id "
<< request_id << "; error=" << error_code_to_string(send_result);
}
}

void E3Interface::handle_subscription_request(const SubscriptionRequest& request, uint32_t request_message_id) {
E3_LOG_INFO(LOG_TAG) << "Handling subscription request from dApp " << request.dapp_identifier
<< " for RAN function " << request.ran_function_identifier;
Expand Down
167 changes: 167 additions & 0 deletions tests/test_setup_bad_request.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
/**
* @file test_setup_bad_request.cpp
* @brief Regression test: a malformed setup request must not wedge the
* RAN's setup REP socket.
*
* The RAN side of the E3 setup channel is a ZMQ REP socket. The REP state
* machine requires exactly one reply per received request before the socket
* can receive again. If the agent bails out of the setup loop without
* replying (e.g. the incoming bytes do not decode — wrong encoding dialect,
* or plain garbage), the socket is stuck in the send state and every later
* setup request from any dApp stalls until the agent restarts.
*
* Properties verified, using a raw ZMQ REQ peer against a real E3Agent:
*
* (1) Sending undecodable bytes on the setup channel produces an explicit
* rejection: a decodable SetupResponse with ResponseCode::NEGATIVE
* (the original request id is unrecoverable from garbage bytes, so
* the agent substitutes a fresh one — E3-MessageID excludes 0),
* i.e. the REQ/REP exchange always completes.
*
* (2) After the garbage exchange, a well-formed SetupRequest on the very
* same channel still receives a positive SetupResponse — the setup
* channel survived the malformed message.
*
* The test uses whichever encoding the build provides (JSON preferred) so
* it runs on JSON-only, ASN.1-only, and dual-encoder builds.
*
* SPDX-License-Identifier: Apache-2.0
*/

#include "test_framework.hpp"
#include "libe3/libe3.hpp"
#include "libe3/e3_agent.hpp"
#include "libe3/e3_encoder.hpp"
#include "libe3/types.hpp"

#include <zmq.h>

#include <atomic>
#include <chrono>
#include <cstdint>
#include <cstring>
#include <sstream>
#include <string>
#include <thread>
#include <vector>

#include <unistd.h> // getpid

using namespace libe3;
using namespace std::chrono_literals;

namespace {

inline int error_to_int(ErrorCode e) { return static_cast<int>(e); }

/// Counter so each test instance uses distinct IPC namespaces — required
/// when running ctest -j to avoid socket-file collisions.
std::atomic<int> g_seq{0};

std::string unique_ipc(const char* tag) {
std::ostringstream oss;
oss << "ipc:///tmp/dapps/badsetup_test_" << getpid() << "_"
<< g_seq.fetch_add(1) << "_" << tag;
return oss.str();
}

/// Pick an encoding that is compiled into this build (JSON preferred so the
/// test also runs on JSON-only builds).
EncodingFormat pick_encoding() {
#if defined(LIBE3_ENABLE_JSON)
return EncodingFormat::JSON;
#else
return EncodingFormat::ASN1;
#endif
}

} // namespace

TEST(SetupChannel_garbageRequest_repliesAndChannelSurvives) {
const std::string setup_ep = unique_ipc("setup");
const std::string sub_ep = unique_ipc("inbound");
const std::string pub_ep = unique_ipc("outbound");
const EncodingFormat encoding = pick_encoding();

E3Config cfg;
cfg.role = E3Role::RAN;
cfg.link_layer = E3LinkLayer::ZMQ;
cfg.transport_layer = E3TransportLayer::IPC;
cfg.setup_endpoint = setup_ep;
cfg.subscriber_endpoint = sub_ep;
cfg.publisher_endpoint = pub_ep;
cfg.encoding = encoding;
cfg.log_level = 0;
cfg.ran_identifier = "badsetup-test";

E3Agent agent(std::move(cfg));
ASSERT_EQ(error_to_int(agent.start()), error_to_int(ErrorCode::SUCCESS));
ASSERT_TRUE(agent.is_running());

// The REP socket is bound synchronously inside start(); a short settle
// keeps the IPC connect race-free.
std::this_thread::sleep_for(100ms);

void* ctx = zmq_ctx_new();
ASSERT_TRUE(ctx != nullptr);
void* req = zmq_socket(ctx, ZMQ_REQ);
ASSERT_TRUE(req != nullptr);
int recv_timeout = 5000; // a wedged REP socket shows up as a timeout
zmq_setsockopt(req, ZMQ_RCVTIMEO, &recv_timeout, sizeof(recv_timeout));
int linger = 0;
zmq_setsockopt(req, ZMQ_LINGER, &linger, sizeof(linger));
ASSERT_EQ(zmq_connect(req, setup_ep.c_str()), 0);

auto encoder = create_encoder(encoding);
ASSERT_TRUE(encoder != nullptr);

// (1) Garbage on the setup channel: an explicit rejection — a decodable
// negative SetupResponse — must arrive. Without the fix the agent
// never replies and this recv times out with -1/EAGAIN.
const char garbage[] = "definitely-not-an-e3ap-setup-request";
ASSERT_GE(zmq_send(req, garbage, sizeof(garbage) - 1, 0), 0);

uint8_t buf[4096];
int n = zmq_recv(req, buf, sizeof(buf), 0);
ASSERT_GT(n, 0);
auto rejection = encoder->decode(buf, static_cast<size_t>(n));
ASSERT_TRUE(rejection.has_value());
ASSERT_EQ(static_cast<int>(rejection->type),
static_cast<int>(PduType::SETUP_RESPONSE));
auto* neg = std::get_if<SetupResponse>(&rejection->choice);
ASSERT_TRUE(neg != nullptr);
ASSERT_EQ(static_cast<int>(neg->response_code),
static_cast<int>(ResponseCode::NEGATIVE));
ASSERT_GE(neg->request_id, 1u); // id unrecoverable → substituted, never 0
ASSERT_TRUE(neg->ran_function_list.empty());
ASSERT_TRUE(!neg->dapp_identifier.has_value());

// (2) The same channel must still serve a well-formed SetupRequest.
auto setup = encoder->encode_setup_request(
42, "1.0.0", "badsetup-dapp", "0.0.1", "test-vendor");
ASSERT_TRUE(setup.has_value());
ASSERT_GE(zmq_send(req, setup->buffer.data(), setup->buffer.size(), 0), 0);

n = zmq_recv(req, buf, sizeof(buf), 0);
ASSERT_GT(n, 0);
auto decoded = encoder->decode(buf, static_cast<size_t>(n));
ASSERT_TRUE(decoded.has_value());
ASSERT_EQ(static_cast<int>(decoded->type),
static_cast<int>(PduType::SETUP_RESPONSE));
auto* resp = std::get_if<SetupResponse>(&decoded->choice);
ASSERT_TRUE(resp != nullptr);
ASSERT_EQ(static_cast<int>(resp->response_code),
static_cast<int>(ResponseCode::POSITIVE));
ASSERT_EQ(resp->request_id, 42u);
ASSERT_TRUE(resp->dapp_identifier.has_value());

zmq_close(req);
zmq_ctx_destroy(ctx);
agent.stop();
}

// ---------------------------------------------------------------------------

int main() {
return RUN_ALL_TESTS();
}
Loading