-
Notifications
You must be signed in to change notification settings - Fork 0
Fix/setup robustness and multi agent #29
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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); | ||
| } | ||
|
|
||
|
|
@@ -640,9 +646,11 @@ void E3Interface::handle_setup_request(const SetupRequest& request, uint32_t req | |
|
|
||
| if (!encode_result) { | ||
| 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 | ||
|
|
@@ -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) { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
|
|
||
| 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(); | ||
| } |
There was a problem hiding this comment.
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