From e014b56ab9c4ddad8edef6b1fdea8027a01d9007 Mon Sep 17 00:00:00 2001 From: Nikolay Chirkov Date: Thu, 25 Jun 2026 14:04:59 +0300 Subject: [PATCH 01/20] Add prepared send message packet encoder --- aether/client_messages/p2p_message_stream.cpp | 35 ++++ aether/client_messages/p2p_message_stream.h | 8 + aether/prepared_packet/packet_encoder.h | 187 ++++++++++++++++++ .../client_server_connection.cpp | 84 ++++++++ .../client_server_connection.h | 9 + aether/stream_api/api_call_adapter.h | 111 ++++++++++- 6 files changed, 433 insertions(+), 1 deletion(-) create mode 100644 aether/prepared_packet/packet_encoder.h diff --git a/aether/client_messages/p2p_message_stream.cpp b/aether/client_messages/p2p_message_stream.cpp index c202645f..5737c1da 100644 --- a/aether/client_messages/p2p_message_stream.cpp +++ b/aether/client_messages/p2p_message_stream.cpp @@ -25,6 +25,7 @@ #include "aether/cloud_connections/cloud_request.h" #include "aether/cloud_connections/cloud_subscription.h" +#include "aether/cloud_connections/cloud_server_connection.h" #include "aether/client_messages/client_messages_tele.h" @@ -48,6 +49,29 @@ class MessageSendStream final : public IStream { }}, request_policy_); } + + std::optional + ExportPreparedSendMessageBlock(Uid target_uid, + std::uint32_t reserve_nonce_count) { + for (auto* sc : cloud_connection_->servers()) { + if (sc == nullptr) { + continue; + } + + auto* conn = sc->client_connection(); + if (conn == nullptr) { + continue; + } + + auto block = conn->ExportPreparedSendMessageBlock(target_uid, + reserve_nonce_count); + if (block) { + return block; + } + } + + return std::nullopt; + } StreamInfo stream_info() const override { return stream_info_; } OutDataEvent::Subscriber out_data_event() override { return out_data_event_; } StreamUpdateEvent::Subscriber stream_update_event() override { @@ -203,6 +227,17 @@ void P2pStream::WriteOut(DataBuffer const& data) { Uid const& P2pStream::destination() const { return destination_; } +std::optional +P2pStream::ExportPreparedSendMessageBlock(std::uint32_t reserve_nonce_count) { + if (!message_send_stream_) { + return std::nullopt; + } + + return message_send_stream_->ExportPreparedSendMessageBlock( + destination_, reserve_nonce_count); +} + + void P2pStream::ConnectReceive() { out_data_sub_ = handle_.out_data_event().Subscribe(MethodPtr<&P2pStream::WriteOut>{this}); diff --git a/aether/client_messages/p2p_message_stream.h b/aether/client_messages/p2p_message_stream.h index 64da048b..78590dad 100644 --- a/aether/client_messages/p2p_message_stream.h +++ b/aether/client_messages/p2p_message_stream.h @@ -17,6 +17,9 @@ #ifndef AETHER_CLIENT_MESSAGES_P2P_MESSAGE_STREAM_H_ #define AETHER_CLIENT_MESSAGES_P2P_MESSAGE_STREAM_H_ +#include +#include + #include "aether/common.h" #include "aether/ae_context.h" @@ -29,6 +32,8 @@ #include "aether/cloud_connections/cloud_server_connections.h" #include "aether/connection_manager/client_cloud_manager.h" +#include "aether/prepared_packet/packet_encoder.h" + namespace ae { class Client; class Cloud; @@ -54,6 +59,9 @@ class P2pStream final : public ByteIStream { void WriteOut(DataBuffer const& data); Uid const& destination() const; + std::optional + ExportPreparedSendMessageBlock(std::uint32_t reserve_nonce_count); + private: void ConnectReceive(); void ConnectSend(); diff --git a/aether/prepared_packet/packet_encoder.h b/aether/prepared_packet/packet_encoder.h new file mode 100644 index 00000000..5d05686d --- /dev/null +++ b/aether/prepared_packet/packet_encoder.h @@ -0,0 +1,187 @@ +/* + * Prepared packet encoder experiment. + * + * This code does not send anything. + * It only builds Aether packet bytes from prepared state + payload. + */ +#ifndef AETHER_PREPARED_PACKET_PACKET_ENCODER_H_ +#define AETHER_PREPARED_PACKET_PACKET_ENCODER_H_ + +#include +#include +#include +#include +#include +#include + +#include "aether/types/uid.h" +#include "aether/types/data_buffer.h" + +#include "aether/crypto/key.h" +#include "aether/crypto/crypto_nonce.h" +#include "aether/crypto/ikey_provider.h" +#include "aether/crypto/sync_crypto_provider.h" + +#include "aether/api_protocol/api_context.h" +#include "aether/api_protocol/sub_api.h" + +#include "aether/work_cloud_api/ae_message.h" +#include "aether/work_cloud_api/work_server_api/login_api.h" +#include "aether/work_cloud_api/work_server_api/authorized_api.h" + +namespace ae::prepared_packet { + +enum class PreparedIpVersion : std::uint8_t { + kIpV4 = 4, + kIpV6 = 6, +}; + +struct PreparedUdpEndpoint { + PreparedIpVersion version = PreparedIpVersion::kIpV4; + std::uint16_t port = 0; + + // IPv4 uses first 4 bytes. + // IPv6 uses all 16 bytes. + std::array ip{}; +}; + +enum class EncodePacketError { + kNone = 0, + kNonceExhausted, + kEncodeFailed, +}; + +inline char const* ToString(EncodePacketError error) { + switch (error) { + case EncodePacketError::kNone: + return "none"; + case EncodePacketError::kNonceExhausted: + return "nonce_exhausted"; + case EncodePacketError::kEncodeFailed: + return "encode_failed"; + } + return "unknown"; +} + +struct EncodePacketResult { + EncodePacketError error = EncodePacketError::kNone; + std::size_t bytes_written = 0; + + // Debug counter inside prepared block. This is not the cryptographic nonce value. + std::uint64_t nonce_index = 0; + + explicit operator bool() const { + return error == EncodePacketError::kNone; + } +}; + +// One block for now. +// On MCU this can live in RTC RAM. +// Later it can be split into flash/static and rtc/mutable parts. +struct PreparedSendMessageBlock { + // Used by external sender after EncodePacket(). + // EncodePacket() itself does not use endpoint. + PreparedUdpEndpoint endpoint; + + // Used for login_by_alias(...) + Uid sender_ephemeral_uid; + + // Used for AuthorizedApi::send_message(AeMessage{target_uid, payload}) + Uid target_uid; + + // Already derived client -> server key. + Key client_to_server_key; + + // Mutable nonce state. + CryptoNonce next_nonce; + std::uint32_t nonce_left = 0; + + // Debug only. + std::uint64_t nonce_index = 0; +}; + +class PreparedSendMessageKeyProvider final : public ISyncKeyProvider { + public: + explicit PreparedSendMessageKeyProvider(PreparedSendMessageBlock& block) + : block_{&block} {} + + Key GetKey() const override { + return block_->client_to_server_key; + } + + CryptoNonce const& Nonce() const override { + return block_->next_nonce; + } + + private: + PreparedSendMessageBlock* block_; +}; + +inline EncodePacketResult EncodePacket(PreparedSendMessageBlock& block, + DataBuffer const& payload, + DataBuffer& out) { + if (block.nonce_left == 0) { + out.clear(); + return EncodePacketResult{EncodePacketError::kNonceExhausted, 0, + block.nonce_index}; + } + + // Match existing ClientKeyProvider semantics: + // consume next nonce before encryption. + block.next_nonce.Next(); + --block.nonce_left; + + auto nonce_index = block.nonce_index; + ++block.nonce_index; + + auto key_provider = + std::make_unique(block); + SyncEncryptProvider encrypt_provider{std::move(key_provider)}; + + ProtocolContext protocol_context; + LoginApi login_api{protocol_context, encrypt_provider}; + + auto api_context = ApiContext{login_api}; + + api_context->login_by_alias( + block.sender_ephemeral_uid, + SubApi{ + [&block, &payload](ApiContext& auth_api) { + auth_api->send_message( + AeMessage{block.target_uid, DataBuffer{payload}}); + }}); + + out = std::move(api_context).Pack(); + + return EncodePacketResult{EncodePacketError::kNone, out.size(), nonce_index}; +} + +// Existing internal seam. Keep normal Aether path working. +struct PreparedPacketContext { + std::uint64_t next_nonce = 0; + std::uint64_t nonce_limit = (std::numeric_limits::max)(); + + EncodePacketResult TakeNonce(std::size_t bytes_written) { + if (next_nonce == nonce_limit) { + return EncodePacketResult{EncodePacketError::kNonceExhausted, 0, + next_nonce}; + } + + auto nonce = next_nonce; + ++next_nonce; + + return EncodePacketResult{EncodePacketError::kNone, bytes_written, nonce}; + } +}; + +template +EncodePacketResult EncodePacket(PreparedPacketContext& prepared, + ApiContext&& data, + DataBuffer& out) { + out = std::move(data).Pack(); + return prepared.TakeNonce(out.size()); +} + +} // namespace ae::prepared_packet + +#endif // AETHER_PREPARED_PACKET_PACKET_ENCODER_H_ diff --git a/aether/server_connections/client_server_connection.cpp b/aether/server_connections/client_server_connection.cpp index d6a96675..c2df5c17 100644 --- a/aether/server_connections/client_server_connection.cpp +++ b/aether/server_connections/client_server_connection.cpp @@ -1,3 +1,6 @@ +#include +#include +#include /* * Copyright 2024 Aethernet Inc. * @@ -142,6 +145,7 @@ ClientServerConnection::ClientServerConnection(AeContext const& ae_context, Ptr const& client, Ptr const& server) : ae_context_{ae_context}, + client_{client}, server_{server}, uid_{client->uid()}, ephemeral_uid_{client->ephemeral_uid()}, @@ -198,6 +202,86 @@ ServerConnection& ClientServerConnection::server_connection() { return server_connection_.server_connection; } +std::optional +ClientServerConnection::ExportPreparedSendMessageBlock( + Uid target_uid, std::uint32_t reserve_nonce_count) { + auto client = client_.Lock(); + auto server = server_.Lock(); + + if (!client || !server || (reserve_nonce_count == 0)) { + return std::nullopt; + } + + std::optional prepared_endpoint; + + for (auto const& e : server->endpoints) { + if (e.protocol != Protocol::kUdp) { + continue; + } + + prepared_packet::PreparedUdpEndpoint candidate; + candidate.port = e.port; + + bool is_ip = false; + + std::visit( + [&](auto const& addr) { + using T = std::decay_t; + + if constexpr (std::is_same_v) { + candidate.version = prepared_packet::PreparedIpVersion::kIpV4; + for (std::size_t i = 0; i < 4; ++i) { + candidate.ip[i] = addr.ipv4_value[i]; + } + is_ip = true; + } else if constexpr (std::is_same_v) { + candidate.version = prepared_packet::PreparedIpVersion::kIpV6; + for (std::size_t i = 0; i < 16; ++i) { + candidate.ip[i] = addr.ipv6_value[i]; + } + is_ip = true; + } + }, + e.address); + + if (is_ip) { + prepared_endpoint = candidate; + break; + } + } + + if (!prepared_endpoint) { + return std::nullopt; + } + + auto* server_key = client->server_state(server->server_id); + if (server_key == nullptr) { + return std::nullopt; + } + + prepared_packet::PreparedSendMessageBlock block; + block.endpoint = *prepared_endpoint; + block.sender_ephemeral_uid = ephemeral_uid_; + block.target_uid = target_uid; + block.client_to_server_key = server_key->client_to_server(); + + // Store current nonce. EncodePacket() will call Next() before encryption, + // matching existing ClientKeyProvider behavior. + block.next_nonce = server_key->nonce(); + block.nonce_left = reserve_nonce_count; + block.nonce_index = 0; + + // Burn/reserve the same nonce range in the full client, + // so the normal Aether path cannot reuse it later. + for (std::uint32_t i = 0; i < reserve_nonce_count; ++i) { + server_key->Next(); + } + + return block; +} + + + void ClientServerConnection::OutData(DataBuffer const& data) { auto parser = ApiParser{protocol_context_, data}; parser.Parse(client_api_unsafe_); diff --git a/aether/server_connections/client_server_connection.h b/aether/server_connections/client_server_connection.h index 6996fc8a..b031c14c 100644 --- a/aether/server_connections/client_server_connection.h +++ b/aether/server_connections/client_server_connection.h @@ -17,6 +17,9 @@ #ifndef AETHER_SERVER_CONNECTIONS_CLIENT_SERVER_CONNECTION_H_ #define AETHER_SERVER_CONNECTIONS_CLIENT_SERVER_CONNECTION_H_ +#include +#include + #include "aether/ae_context.h" #include "aether/common.h" #include "aether/crypto/icrypto_provider.h" @@ -27,6 +30,7 @@ #include "aether/work_cloud_api/work_server_api/authorized_api.h" #include "aether/work_cloud_api/work_server_api/login_api.h" +#include "aether/prepared_packet/packet_encoder.h" #include "aether/server_connections/server_connection.h" namespace ae { @@ -74,10 +78,15 @@ class ClientServerConnection { ServerConnection& server_connection(); + std::optional + ExportPreparedSendMessageBlock(Uid target_uid, + std::uint32_t reserve_nonce_count); + private: void OutData(DataBuffer const& data); AeContext ae_context_; + PtrView client_; PtrView server_; Uid uid_; Uid ephemeral_uid_; diff --git a/aether/stream_api/api_call_adapter.h b/aether/stream_api/api_call_adapter.h index a5326a75..7ef95ca5 100644 --- a/aether/stream_api/api_call_adapter.h +++ b/aether/stream_api/api_call_adapter.h @@ -18,11 +18,106 @@ #define AETHER_STREAM_API_API_CALL_ADAPTER_H_ #include +#include +#include +#include +#include +#include #include "aether/stream_api/istream.h" #include "aether/api_protocol/api_context.h" +#include "aether/prepared_packet/packet_encoder.h" +#include "aether/types/data_buffer.h" namespace ae { + +// FASTTX_STEP3_ENCODE_PACKET_API +namespace fast_tx_internal { + +enum class EncodePacketError { + kNone = 0, + kNonceExhausted, + kOutputBufferTooSmall, + kPayloadTooLarge, + kEncodeFailed, +}; + +inline char const* ToString(EncodePacketError error) { + switch (error) { + case EncodePacketError::kNone: + return "none"; + case EncodePacketError::kNonceExhausted: + return "nonce_exhausted"; + case EncodePacketError::kOutputBufferTooSmall: + return "output_buffer_too_small"; + case EncodePacketError::kPayloadTooLarge: + return "payload_too_large"; + case EncodePacketError::kEncodeFailed: + return "encode_failed"; + } + return "unknown"; +} + +struct EncodePacketReport { + EncodePacketError error = EncodePacketError::kNone; + std::size_t bytes_written = 0; + std::uint64_t nonce_used = 0; + + explicit operator bool() const { return error == EncodePacketError::kNone; } +}; + +// Endpoint intentionally stays outside. +// External code will own ip:port and raw UDP socket. +// This struct must contain only data needed by Aether packet encoding. +struct PreparedPacketEncoder { + std::uint64_t next_nonce = 0; + std::uint64_t nonce_limit = (std::numeric_limits::max)(); + std::size_t max_packet_size = 1200; + + EncodePacketReport TakeNonce() { + if (next_nonce == nonce_limit) { + return EncodePacketReport{EncodePacketError::kNonceExhausted, 0, 0}; + } + + auto nonce = next_nonce; + ++next_nonce; + + return EncodePacketReport{EncodePacketError::kNone, 0, nonce}; + } +}; + +// Future external shape: +// +// EncodePacketResult EncodePacket(PreparedPacketEncoder& prepared, +// Span payload, +// DataBuffer& out); +// +// Current internal step keeps old Aether path alive by using ByteIStream as out. +struct EncodePacketResult { + EncodePacketReport report; + WriteAction* action = nullptr; + + explicit operator bool() const { + return static_cast(report) && (action != nullptr); + } +}; + +template +EncodePacketResult EncodePacket(PreparedPacketEncoder& prepared, + ApiContext&& data, + ByteIStream& out) { + auto report = prepared.TakeNonce(); + if (!report) { + return EncodePacketResult{report, nullptr}; + } + + auto& action = out.Write(std::move(data)); + + return EncodePacketResult{report, &action}; +} + +} // namespace fast_tx_internal + /** * \brief Api method call adapter to automatically flush the packet after all * method calls. @@ -35,7 +130,21 @@ class ApiCallAdapter { AE_CLASS_MOVE_ONLY(ApiCallAdapter) - WriteAction& Flush() { return byte_stream_->Write(std::move(api_context_)); } + WriteAction& Flush() { + fast_tx_internal::PreparedPacketEncoder prepared{}; + + auto result = fast_tx_internal::EncodePacket( + prepared, std::move(api_context_), *byte_stream_); + + if (!result) { + std::cerr << "FastTx EncodePacket failed: " + << fast_tx_internal::ToString(result.report.error) + << "\n"; + assert(false); + } + + return *result.action; + } ApiContext& operator->() { return api_context_; } From 9d1d975d8054a5e23665fe5f0713d7b8501c6075 Mon Sep 17 00:00:00 2001 From: Nikolay Chirkov Date: Thu, 25 Jun 2026 14:48:57 +0300 Subject: [PATCH 02/20] Clean up prepared message endpoint API --- aether/prepared_packet/packet_encoder.h | 25 +++++++------------ .../client_server_connection.cpp | 6 ++--- 2 files changed, 12 insertions(+), 19 deletions(-) diff --git a/aether/prepared_packet/packet_encoder.h b/aether/prepared_packet/packet_encoder.h index 5d05686d..3b02f40f 100644 --- a/aether/prepared_packet/packet_encoder.h +++ b/aether/prepared_packet/packet_encoder.h @@ -16,6 +16,7 @@ #include "aether/types/uid.h" #include "aether/types/data_buffer.h" +#include "aether/types/address.h" #include "aether/crypto/key.h" #include "aether/crypto/crypto_nonce.h" @@ -36,8 +37,9 @@ enum class PreparedIpVersion : std::uint8_t { kIpV6 = 6, }; -struct PreparedUdpEndpoint { +struct PreparedEndpoint { PreparedIpVersion version = PreparedIpVersion::kIpV4; + Protocol protocol = Protocol::kUdp; std::uint16_t port = 0; // IPv4 uses first 4 bytes. @@ -68,7 +70,6 @@ struct EncodePacketResult { std::size_t bytes_written = 0; // Debug counter inside prepared block. This is not the cryptographic nonce value. - std::uint64_t nonce_index = 0; explicit operator bool() const { return error == EncodePacketError::kNone; @@ -81,7 +82,7 @@ struct EncodePacketResult { struct PreparedSendMessageBlock { // Used by external sender after EncodePacket(). // EncodePacket() itself does not use endpoint. - PreparedUdpEndpoint endpoint; + PreparedEndpoint endpoint; // Used for login_by_alias(...) Uid sender_ephemeral_uid; @@ -97,7 +98,6 @@ struct PreparedSendMessageBlock { std::uint32_t nonce_left = 0; // Debug only. - std::uint64_t nonce_index = 0; }; class PreparedSendMessageKeyProvider final : public ISyncKeyProvider { @@ -122,8 +122,7 @@ inline EncodePacketResult EncodePacket(PreparedSendMessageBlock& block, DataBuffer& out) { if (block.nonce_left == 0) { out.clear(); - return EncodePacketResult{EncodePacketError::kNonceExhausted, 0, - block.nonce_index}; + return EncodePacketResult{EncodePacketError::kNonceExhausted, 0}; } // Match existing ClientKeyProvider semantics: @@ -131,9 +130,6 @@ inline EncodePacketResult EncodePacket(PreparedSendMessageBlock& block, block.next_nonce.Next(); --block.nonce_left; - auto nonce_index = block.nonce_index; - ++block.nonce_index; - auto key_provider = std::make_unique(block); SyncEncryptProvider encrypt_provider{std::move(key_provider)}; @@ -153,7 +149,7 @@ inline EncodePacketResult EncodePacket(PreparedSendMessageBlock& block, out = std::move(api_context).Pack(); - return EncodePacketResult{EncodePacketError::kNone, out.size(), nonce_index}; + return EncodePacketResult{EncodePacketError::kNone, out.size()}; } // Existing internal seam. Keep normal Aether path working. @@ -163,14 +159,11 @@ struct PreparedPacketContext { EncodePacketResult TakeNonce(std::size_t bytes_written) { if (next_nonce == nonce_limit) { - return EncodePacketResult{EncodePacketError::kNonceExhausted, 0, - next_nonce}; + return EncodePacketResult{EncodePacketError::kNonceExhausted, 0}; } +++next_nonce; - auto nonce = next_nonce; - ++next_nonce; - - return EncodePacketResult{EncodePacketError::kNone, bytes_written, nonce}; + return EncodePacketResult{EncodePacketError::kNone, bytes_written}; } }; diff --git a/aether/server_connections/client_server_connection.cpp b/aether/server_connections/client_server_connection.cpp index c2df5c17..3ac7e08b 100644 --- a/aether/server_connections/client_server_connection.cpp +++ b/aether/server_connections/client_server_connection.cpp @@ -212,14 +212,15 @@ ClientServerConnection::ExportPreparedSendMessageBlock( return std::nullopt; } - std::optional prepared_endpoint; + std::optional prepared_endpoint; for (auto const& e : server->endpoints) { if (e.protocol != Protocol::kUdp) { continue; } - prepared_packet::PreparedUdpEndpoint candidate; + prepared_packet::PreparedEndpoint candidate; + candidate.protocol = e.protocol; candidate.port = e.port; bool is_ip = false; @@ -269,7 +270,6 @@ ClientServerConnection::ExportPreparedSendMessageBlock( // matching existing ClientKeyProvider behavior. block.next_nonce = server_key->nonce(); block.nonce_left = reserve_nonce_count; - block.nonce_index = 0; // Burn/reserve the same nonce range in the full client, // so the normal Aether path cannot reuse it later. From c8a9589bed3fb6e3e8dbedae322c87e54742d72a Mon Sep 17 00:00:00 2001 From: Nikolay Chirkov Date: Thu, 25 Jun 2026 18:11:25 +0300 Subject: [PATCH 03/20] Split prepared packet encoder implementation --- aether/CMakeLists.txt | 1 + aether/prepared_packet/packet_encoder.cpp | 73 ++++++++ aether/prepared_packet/packet_encoder.h | 175 +----------------- .../prepared_packet/prepared_send_message.h | 80 ++++++++ .../client_server_connection.h | 2 +- 5 files changed, 163 insertions(+), 168 deletions(-) create mode 100644 aether/prepared_packet/packet_encoder.cpp create mode 100644 aether/prepared_packet/prepared_send_message.h diff --git a/aether/CMakeLists.txt b/aether/CMakeLists.txt index 2e0da7ba..24830200 100644 --- a/aether/CMakeLists.txt +++ b/aether/CMakeLists.txt @@ -199,6 +199,7 @@ list(APPEND aether_srcs list(APPEND aether_srcs "server_connections/client_server_connection.cpp" "server_connections/channel_select_action.cpp" + "prepared_packet/packet_encoder.cpp" "server_connections/server_connection.cpp") list(APPEND aether_srcs diff --git a/aether/prepared_packet/packet_encoder.cpp b/aether/prepared_packet/packet_encoder.cpp new file mode 100644 index 00000000..0c87e7bb --- /dev/null +++ b/aether/prepared_packet/packet_encoder.cpp @@ -0,0 +1,73 @@ +#include "aether/prepared_packet/packet_encoder.h" + +#include +#include + +#include "aether/crypto/ikey_provider.h" +#include "aether/crypto/sync_crypto_provider.h" + +#include "aether/api_protocol/api_context.h" +#include "aether/api_protocol/sub_api.h" + +#include "aether/work_cloud_api/ae_message.h" +#include "aether/work_cloud_api/work_server_api/authorized_api.h" +#include "aether/work_cloud_api/work_server_api/login_api.h" + +namespace ae::prepared_packet { +namespace { + +class PreparedSendMessageKeyProvider final : public ISyncKeyProvider { + public: + explicit PreparedSendMessageKeyProvider(PreparedSendMessageBlock& block) + : block_{&block} {} + + Key GetKey() const override { + return block_->client_to_server_key; + } + + CryptoNonce const& Nonce() const override { + return block_->next_nonce; + } + + private: + PreparedSendMessageBlock* block_; +}; + +} // namespace + +EncodePacketResult EncodePacket(PreparedSendMessageBlock& block, + DataBuffer const& payload, + DataBuffer& out) { + if (block.nonce_left == 0) { + out.clear(); + return EncodePacketResult{EncodePacketError::kNonceExhausted, 0}; + } + + // Match the existing ClientKeyProvider semantics: + // consume next nonce before encryption. + block.next_nonce.Next(); + --block.nonce_left; + + auto key_provider = + std::make_unique(block); + SyncEncryptProvider encrypt_provider{std::move(key_provider)}; + + ProtocolContext protocol_context; + LoginApi login_api{protocol_context, encrypt_provider}; + + auto api_context = ApiContext{login_api}; + + api_context->login_by_alias( + block.sender_ephemeral_uid, + SubApi{ + [&block, &payload](ApiContext& auth_api) { + auth_api->send_message( + AeMessage{block.target_uid, DataBuffer{payload}}); + }}); + + out = std::move(api_context).Pack(); + + return EncodePacketResult{EncodePacketError::kNone, out.size()}; +} + +} // namespace ae::prepared_packet diff --git a/aether/prepared_packet/packet_encoder.h b/aether/prepared_packet/packet_encoder.h index 3b02f40f..6f9d7371 100644 --- a/aether/prepared_packet/packet_encoder.h +++ b/aether/prepared_packet/packet_encoder.h @@ -1,179 +1,20 @@ /* - * Prepared packet encoder experiment. + * Prepared packet encoder. * - * This code does not send anything. - * It only builds Aether packet bytes from prepared state + payload. + * EncodePacket only builds Aether packet bytes and advances the reserved nonce + * range. It does not send, open sockets, resolve DNS, or know platform + * transport. */ #ifndef AETHER_PREPARED_PACKET_PACKET_ENCODER_H_ #define AETHER_PREPARED_PACKET_PACKET_ENCODER_H_ -#include -#include -#include -#include -#include -#include - -#include "aether/types/uid.h" -#include "aether/types/data_buffer.h" -#include "aether/types/address.h" - -#include "aether/crypto/key.h" -#include "aether/crypto/crypto_nonce.h" -#include "aether/crypto/ikey_provider.h" -#include "aether/crypto/sync_crypto_provider.h" - -#include "aether/api_protocol/api_context.h" -#include "aether/api_protocol/sub_api.h" - -#include "aether/work_cloud_api/ae_message.h" -#include "aether/work_cloud_api/work_server_api/login_api.h" -#include "aether/work_cloud_api/work_server_api/authorized_api.h" +#include "aether/prepared_packet/prepared_send_message.h" namespace ae::prepared_packet { -enum class PreparedIpVersion : std::uint8_t { - kIpV4 = 4, - kIpV6 = 6, -}; - -struct PreparedEndpoint { - PreparedIpVersion version = PreparedIpVersion::kIpV4; - Protocol protocol = Protocol::kUdp; - std::uint16_t port = 0; - - // IPv4 uses first 4 bytes. - // IPv6 uses all 16 bytes. - std::array ip{}; -}; - -enum class EncodePacketError { - kNone = 0, - kNonceExhausted, - kEncodeFailed, -}; - -inline char const* ToString(EncodePacketError error) { - switch (error) { - case EncodePacketError::kNone: - return "none"; - case EncodePacketError::kNonceExhausted: - return "nonce_exhausted"; - case EncodePacketError::kEncodeFailed: - return "encode_failed"; - } - return "unknown"; -} - -struct EncodePacketResult { - EncodePacketError error = EncodePacketError::kNone; - std::size_t bytes_written = 0; - - // Debug counter inside prepared block. This is not the cryptographic nonce value. - - explicit operator bool() const { - return error == EncodePacketError::kNone; - } -}; - -// One block for now. -// On MCU this can live in RTC RAM. -// Later it can be split into flash/static and rtc/mutable parts. -struct PreparedSendMessageBlock { - // Used by external sender after EncodePacket(). - // EncodePacket() itself does not use endpoint. - PreparedEndpoint endpoint; - - // Used for login_by_alias(...) - Uid sender_ephemeral_uid; - - // Used for AuthorizedApi::send_message(AeMessage{target_uid, payload}) - Uid target_uid; - - // Already derived client -> server key. - Key client_to_server_key; - - // Mutable nonce state. - CryptoNonce next_nonce; - std::uint32_t nonce_left = 0; - - // Debug only. -}; - -class PreparedSendMessageKeyProvider final : public ISyncKeyProvider { - public: - explicit PreparedSendMessageKeyProvider(PreparedSendMessageBlock& block) - : block_{&block} {} - - Key GetKey() const override { - return block_->client_to_server_key; - } - - CryptoNonce const& Nonce() const override { - return block_->next_nonce; - } - - private: - PreparedSendMessageBlock* block_; -}; - -inline EncodePacketResult EncodePacket(PreparedSendMessageBlock& block, - DataBuffer const& payload, - DataBuffer& out) { - if (block.nonce_left == 0) { - out.clear(); - return EncodePacketResult{EncodePacketError::kNonceExhausted, 0}; - } - - // Match existing ClientKeyProvider semantics: - // consume next nonce before encryption. - block.next_nonce.Next(); - --block.nonce_left; - - auto key_provider = - std::make_unique(block); - SyncEncryptProvider encrypt_provider{std::move(key_provider)}; - - ProtocolContext protocol_context; - LoginApi login_api{protocol_context, encrypt_provider}; - - auto api_context = ApiContext{login_api}; - - api_context->login_by_alias( - block.sender_ephemeral_uid, - SubApi{ - [&block, &payload](ApiContext& auth_api) { - auth_api->send_message( - AeMessage{block.target_uid, DataBuffer{payload}}); - }}); - - out = std::move(api_context).Pack(); - - return EncodePacketResult{EncodePacketError::kNone, out.size()}; -} - -// Existing internal seam. Keep normal Aether path working. -struct PreparedPacketContext { - std::uint64_t next_nonce = 0; - std::uint64_t nonce_limit = (std::numeric_limits::max)(); - - EncodePacketResult TakeNonce(std::size_t bytes_written) { - if (next_nonce == nonce_limit) { - return EncodePacketResult{EncodePacketError::kNonceExhausted, 0}; - } -++next_nonce; - - return EncodePacketResult{EncodePacketError::kNone, bytes_written}; - } -}; - -template -EncodePacketResult EncodePacket(PreparedPacketContext& prepared, - ApiContext&& data, - DataBuffer& out) { - out = std::move(data).Pack(); - return prepared.TakeNonce(out.size()); -} +EncodePacketResult EncodePacket(PreparedSendMessageBlock& block, + DataBuffer const& payload, + DataBuffer& out); } // namespace ae::prepared_packet diff --git a/aether/prepared_packet/prepared_send_message.h b/aether/prepared_packet/prepared_send_message.h new file mode 100644 index 00000000..efa30947 --- /dev/null +++ b/aether/prepared_packet/prepared_send_message.h @@ -0,0 +1,80 @@ +/* + * Prepared send_message block. + * + * This is transport-neutral state for encoding a send_message packet. + * It may contain an endpoint selected by the full Aether client, but it does + * not own sockets, DNS, connections, channels, or timers. + */ +#ifndef AETHER_PREPARED_PACKET_PREPARED_SEND_MESSAGE_H_ +#define AETHER_PREPARED_PACKET_PREPARED_SEND_MESSAGE_H_ + +#include +#include +#include + +#include "aether/types/address.h" +#include "aether/types/data_buffer.h" +#include "aether/types/uid.h" + +#include "aether/crypto/key.h" +#include "aether/crypto/crypto_nonce.h" + +namespace ae::prepared_packet { + +enum class PreparedIpVersion : std::uint8_t { + kIpV4 = 4, + kIpV6 = 6, +}; + +struct PreparedEndpoint { + PreparedIpVersion version = PreparedIpVersion::kIpV4; + Protocol protocol = Protocol::kUdp; + std::uint16_t port = 0; + + // IPv4 uses first 4 bytes. + // IPv6 uses all 16 bytes. + std::array ip{}; +}; + +struct PreparedSendMessageBlock { + PreparedEndpoint endpoint; + + Uid sender_ephemeral_uid; + Uid target_uid; + + Key client_to_server_key; + + CryptoNonce next_nonce; + std::uint32_t nonce_left = 0; +}; + +enum class EncodePacketError { + kNone = 0, + kNonceExhausted, + kEncodeFailed, +}; + +inline char const* ToString(EncodePacketError error) { + switch (error) { + case EncodePacketError::kNone: + return "none"; + case EncodePacketError::kNonceExhausted: + return "nonce_exhausted"; + case EncodePacketError::kEncodeFailed: + return "encode_failed"; + } + return "unknown"; +} + +struct EncodePacketResult { + EncodePacketError error = EncodePacketError::kNone; + std::size_t bytes_written = 0; + + explicit operator bool() const { + return error == EncodePacketError::kNone; + } +}; + +} // namespace ae::prepared_packet + +#endif // AETHER_PREPARED_PACKET_PREPARED_SEND_MESSAGE_H_ diff --git a/aether/server_connections/client_server_connection.h b/aether/server_connections/client_server_connection.h index b031c14c..ff0f6878 100644 --- a/aether/server_connections/client_server_connection.h +++ b/aether/server_connections/client_server_connection.h @@ -30,8 +30,8 @@ #include "aether/work_cloud_api/work_server_api/authorized_api.h" #include "aether/work_cloud_api/work_server_api/login_api.h" -#include "aether/prepared_packet/packet_encoder.h" #include "aether/server_connections/server_connection.h" +#include "aether/prepared_packet/prepared_send_message.h" namespace ae { class Client; From 452bd336db23b16f333c042b2576bcb7443cbc83 Mon Sep 17 00:00:00 2001 From: Nikolay Chirkov Date: Fri, 26 Jun 2026 11:03:59 +0300 Subject: [PATCH 04/20] Move prepared endpoint conversion into prepared packet module --- aether/CMakeLists.txt | 3 +- .../prepared_packet/prepared_send_message.cpp | 41 +++++++++++++++++++ .../prepared_packet/prepared_send_message.h | 4 ++ .../client_server_connection.cpp | 31 ++------------ 4 files changed, 50 insertions(+), 29 deletions(-) create mode 100644 aether/prepared_packet/prepared_send_message.cpp diff --git a/aether/CMakeLists.txt b/aether/CMakeLists.txt index 24830200..87f695d4 100644 --- a/aether/CMakeLists.txt +++ b/aether/CMakeLists.txt @@ -198,8 +198,9 @@ list(APPEND aether_srcs list(APPEND aether_srcs "server_connections/client_server_connection.cpp" - "server_connections/channel_select_action.cpp" + "prepared_packet/prepared_send_message.cpp" "prepared_packet/packet_encoder.cpp" + "server_connections/channel_selec_action.cpp" "server_connections/server_connection.cpp") list(APPEND aether_srcs diff --git a/aether/prepared_packet/prepared_send_message.cpp b/aether/prepared_packet/prepared_send_message.cpp new file mode 100644 index 00000000..db965ee6 --- /dev/null +++ b/aether/prepared_packet/prepared_send_message.cpp @@ -0,0 +1,41 @@ +#include "aether/prepared_packet/prepared_send_message.h" + +#include + +namespace ae::prepared_packet { + +std::optional MakePreparedEndpoint(Endpoint const& endpoint) { + PreparedEndpoint candidate; + candidate.protocol = endpoint.protocol; + candidate.port = endpoint.port; + + bool is_ip = false; + + std::visit( + [&](auto const& addr) { + using T = std::decay_t; + + if constexpr (std::is_same_v) { + candidate.version = PreparedIpVersion::kIpV4; + for (std::size_t i = 0; i < 4; ++i) { + candidate.ip[i] = addr.ipv4_value[i]; + } + is_ip = true; + } else if constexpr (std::is_same_v) { + candidate.version = PreparedIpVersion::kIpV6; + for (std::size_t i = 0; i < 16; ++i) { + candidate.ip[i] = addr.ipv6_value[i]; + } + is_ip = true; + } + }, + endpoint.address); + + if (!is_ip) { + return std::nullopt; + } + + return candidate; +} + +} // namespace ae::prepared_packet diff --git a/aether/prepared_packet/prepared_send_message.h b/aether/prepared_packet/prepared_send_message.h index efa30947..345e97b1 100644 --- a/aether/prepared_packet/prepared_send_message.h +++ b/aether/prepared_packet/prepared_send_message.h @@ -11,6 +11,7 @@ #include #include #include +#include #include "aether/types/address.h" #include "aether/types/data_buffer.h" @@ -75,6 +76,9 @@ struct EncodePacketResult { } }; + +std::optional MakePreparedEndpoint(Endpoint const& endpoint); + } // namespace ae::prepared_packet #endif // AETHER_PREPARED_PACKET_PREPARED_SEND_MESSAGE_H_ diff --git a/aether/server_connections/client_server_connection.cpp b/aether/server_connections/client_server_connection.cpp index 3ac7e08b..39bed9c7 100644 --- a/aether/server_connections/client_server_connection.cpp +++ b/aether/server_connections/client_server_connection.cpp @@ -219,34 +219,9 @@ ClientServerConnection::ExportPreparedSendMessageBlock( continue; } - prepared_packet::PreparedEndpoint candidate; - candidate.protocol = e.protocol; - candidate.port = e.port; - - bool is_ip = false; - - std::visit( - [&](auto const& addr) { - using T = std::decay_t; - - if constexpr (std::is_same_v) { - candidate.version = prepared_packet::PreparedIpVersion::kIpV4; - for (std::size_t i = 0; i < 4; ++i) { - candidate.ip[i] = addr.ipv4_value[i]; - } - is_ip = true; - } else if constexpr (std::is_same_v) { - candidate.version = prepared_packet::PreparedIpVersion::kIpV6; - for (std::size_t i = 0; i < 16; ++i) { - candidate.ip[i] = addr.ipv6_value[i]; - } - is_ip = true; - } - }, - e.address); - - if (is_ip) { - prepared_endpoint = candidate; + auto endpoint = prepared_packet::MakePreparedEndpoint(e); + if (endpoint) { + prepared_endpoint = *endpoint; break; } } From ce0b457189f9f3ceb8f0f50cf32b73bc7e95ef64 Mon Sep 17 00:00:00 2001 From: Nikolay Chirkov Date: Fri, 26 Jun 2026 11:27:44 +0300 Subject: [PATCH 05/20] Remove unused prepared packet encode error --- aether/prepared_packet/prepared_send_message.h | 3 --- 1 file changed, 3 deletions(-) diff --git a/aether/prepared_packet/prepared_send_message.h b/aether/prepared_packet/prepared_send_message.h index 345e97b1..432d98c4 100644 --- a/aether/prepared_packet/prepared_send_message.h +++ b/aether/prepared_packet/prepared_send_message.h @@ -52,7 +52,6 @@ struct PreparedSendMessageBlock { enum class EncodePacketError { kNone = 0, kNonceExhausted, - kEncodeFailed, }; inline char const* ToString(EncodePacketError error) { @@ -61,8 +60,6 @@ inline char const* ToString(EncodePacketError error) { return "none"; case EncodePacketError::kNonceExhausted: return "nonce_exhausted"; - case EncodePacketError::kEncodeFailed: - return "encode_failed"; } return "unknown"; } From e3b9087741c30b7f2b2592efa4f236c09a8e9d5f Mon Sep 17 00:00:00 2001 From: Nikolay Chirkov Date: Fri, 26 Jun 2026 12:19:58 +0300 Subject: [PATCH 06/20] Add prepared send message API --- aether/CMakeLists.txt | 2 +- .../prepared_packet/prepare_send_message.cpp | 12 +++++++++++ aether/prepared_packet/prepare_send_message.h | 21 +++++++++++++++++++ 3 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 aether/prepared_packet/prepare_send_message.cpp create mode 100644 aether/prepared_packet/prepare_send_message.h diff --git a/aether/CMakeLists.txt b/aether/CMakeLists.txt index 87f695d4..d0fdc096 100644 --- a/aether/CMakeLists.txt +++ b/aether/CMakeLists.txt @@ -200,7 +200,7 @@ list(APPEND aether_srcs "server_connections/client_server_connection.cpp" "prepared_packet/prepared_send_message.cpp" "prepared_packet/packet_encoder.cpp" - "server_connections/channel_selec_action.cpp" + "server_connections/channel_select_action.cpp" "server_connections/server_connection.cpp") list(APPEND aether_srcs diff --git a/aether/prepared_packet/prepare_send_message.cpp b/aether/prepared_packet/prepare_send_message.cpp new file mode 100644 index 00000000..45229f55 --- /dev/null +++ b/aether/prepared_packet/prepare_send_message.cpp @@ -0,0 +1,12 @@ +#include "aether/prepared_packet/prepare_send_message.h" + +#include "aether/client_messages/p2p_message_stream.h" + +namespace ae::prepared_packet { + +std::optional PrepareSendMessage( + P2pStream& stream, std::uint32_t reserve_nonce_count) { + return stream.ExportPreparedSendMessageBlock(reserve_nonce_count); +} + +} // namespace ae::prepared_packet diff --git a/aether/prepared_packet/prepare_send_message.h b/aether/prepared_packet/prepare_send_message.h new file mode 100644 index 00000000..52f1da23 --- /dev/null +++ b/aether/prepared_packet/prepare_send_message.h @@ -0,0 +1,21 @@ +#ifndef AETHER_PREPARED_PACKET_PREPARE_SEND_MESSAGE_H_ +#define AETHER_PREPARED_PACKET_PREPARE_SEND_MESSAGE_H_ + +#include +#include + +#include "aether/prepared_packet/prepared_send_message.h" + +namespace ae { + +class P2pStream; + +namespace prepared_packet { + +std::optional PrepareSendMessage( + P2pStream& stream, std::uint32_t reserve_nonce_count); + +} // namespace prepared_packet +} // namespace ae + +#endif From af707aea597dd7d845ca2106a2fab1ee1488c641 Mon Sep 17 00:00:00 2001 From: Nikolay Chirkov Date: Wed, 1 Jul 2026 16:16:56 +0300 Subject: [PATCH 07/20] Remove FASTTX_STEP debug marker from api_call_adapter. Co-authored-by: Cursor --- aether/stream_api/api_call_adapter.h | 1 - 1 file changed, 1 deletion(-) diff --git a/aether/stream_api/api_call_adapter.h b/aether/stream_api/api_call_adapter.h index 7ef95ca5..f8f42a71 100644 --- a/aether/stream_api/api_call_adapter.h +++ b/aether/stream_api/api_call_adapter.h @@ -31,7 +31,6 @@ namespace ae { -// FASTTX_STEP3_ENCODE_PACKET_API namespace fast_tx_internal { enum class EncodePacketError { From 7966de8cf71025bf2925309e6d181009f8da3f8a Mon Sep 17 00:00:00 2001 From: Kiryanov D V Date: Thu, 2 Jul 2026 14:58:42 +0300 Subject: [PATCH 08/20] Fix bugs. --- .../prepared_packet/prepared_send_message.h | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/aether/prepared_packet/prepared_send_message.h b/aether/prepared_packet/prepared_send_message.h index 432d98c4..fff70874 100644 --- a/aether/prepared_packet/prepared_send_message.h +++ b/aether/prepared_packet/prepared_send_message.h @@ -21,6 +21,19 @@ #include "aether/crypto/crypto_nonce.h" namespace ae::prepared_packet { +static constexpr std::uint32_t kMagic = 0x50534456; // "PSDV" +static constexpr std::uint32_t kVersion = 1; +// Serialized PreparedSendMessageBlock is a few hundred bytes. Keep the RTC +// footprint small enough for ESP32 RTC slow memory (8 KiB on ESP32-C6). +static constexpr std::size_t kMaxPreparedBlockBytes = 512; + +struct RetainedPreparedBlock { + std::uint32_t magic; + std::uint32_t version; + std::uint32_t size; + std::uint32_t checksum; + std::array bytes; +}; enum class PreparedIpVersion : std::uint8_t { kIpV4 = 4, @@ -28,6 +41,7 @@ enum class PreparedIpVersion : std::uint8_t { }; struct PreparedEndpoint { + AE_REFLECT_MEMBERS(version, protocol, port, ip) PreparedIpVersion version = PreparedIpVersion::kIpV4; Protocol protocol = Protocol::kUdp; std::uint16_t port = 0; @@ -38,6 +52,8 @@ struct PreparedEndpoint { }; struct PreparedSendMessageBlock { + AE_REFLECT_MEMBERS(endpoint, sender_ephemeral_uid, target_uid, + client_to_server_key, next_nonce, nonce_left) PreparedEndpoint endpoint; Uid sender_ephemeral_uid; @@ -68,12 +84,9 @@ struct EncodePacketResult { EncodePacketError error = EncodePacketError::kNone; std::size_t bytes_written = 0; - explicit operator bool() const { - return error == EncodePacketError::kNone; - } + explicit operator bool() const { return error == EncodePacketError::kNone; } }; - std::optional MakePreparedEndpoint(Endpoint const& endpoint); } // namespace ae::prepared_packet From 0e0b1142b2af6c6c3f00499544b14e48e9fd0769 Mon Sep 17 00:00:00 2001 From: Kiryanov D V Date: Thu, 9 Jul 2026 12:14:30 +0300 Subject: [PATCH 09/20] Add windows_message_receiver example. --- CMakeLists.txt | 3 + .../windows_message_receiver/CMakeLists.txt | 50 +++++ .../config/cloud_config.ini | 38 ++++ .../windows_message_receiver/user_config.h | 39 ++++ .../windows_message_receiver.cpp | 192 ++++++++++++++++++ 5 files changed, 322 insertions(+) create mode 100644 examples/windows_message_receiver/CMakeLists.txt create mode 100644 examples/windows_message_receiver/config/cloud_config.ini create mode 100644 examples/windows_message_receiver/user_config.h create mode 100644 examples/windows_message_receiver/windows_message_receiver.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index a9ceadd8..df8e2fd2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -383,6 +383,9 @@ if(AE_BUILD_EXAMPLES) add_subdirectory(examples/common) add_subdirectory(examples/cloud) add_subdirectory(examples/a_b_message_exchange) + if(CMAKE_SYSTEM_NAME STREQUAL "Windows") + add_subdirectory(examples/windows_message_receiver) + endif() add_subdirectory(examples/capi/oddity) add_subdirectory(examples/benches/send_message_delays) add_subdirectory(examples/benches/send_messages_bandwidth) diff --git a/examples/windows_message_receiver/CMakeLists.txt b/examples/windows_message_receiver/CMakeLists.txt new file mode 100644 index 00000000..ede10f15 --- /dev/null +++ b/examples/windows_message_receiver/CMakeLists.txt @@ -0,0 +1,50 @@ +# Copyright 2024 Aethernet Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +cmake_minimum_required(VERSION 3.16.0) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +list( APPEND src_list + windows_message_receiver.cpp +) + +if(NOT CM_PLATFORM) + project("aether-client-cpp-windows_message_receiver" VERSION "1.0.0" LANGUAGES C CXX) + + add_executable(${PROJECT_NAME} ${src_list}) + target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(${PROJECT_NAME} PRIVATE aether) +else() + idf_build_get_property(CM_PLATFORM CM_PLATFORM) + if(CM_PLATFORM STREQUAL "ESP32") + #ESP32 CMake + idf_component_register(SRCS ${src_list} + INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR} + REQUIRES + esp_wifi + esp_netif + nvs_flash + spiffs + esp_driver_uart + ) + + add_subdirectory("../../" aether) + target_link_libraries(${COMPONENT_LIB} PRIVATE aether) + else() + #Other platforms + message(FATAL_ERROR "Platform ${CM_PLATFORM} is not supported") + endif() +endif() diff --git a/examples/windows_message_receiver/config/cloud_config.ini b/examples/windows_message_receiver/config/cloud_config.ini new file mode 100644 index 00000000..0c275714 --- /dev/null +++ b/examples/windows_message_receiver/config/cloud_config.ini @@ -0,0 +1,38 @@ +; Static configuration for cloud test +; Register to save state as a header file by +; aether-registrator +; Use header file to build the client test cmake -DFS_INIT= . && cmake --build . + +; Required section +; Aether configuration +[Aether] +; Must have one of the following keys or both depending on the project configuration \see aether/config.h +ed25519_sign_key = 4F202A94AB729FE9B381613AE77A8A7D89EDAB9299C3320D1A0B994BA710CCEB +hydrogen_sign_key = 883B4D7E0FB04A38CA12B3A451B00942048858263EE6E6D61150F2EF15F40343 + +; Optional section +; If provided, wifi adapter would be configured and saved to the state +[AdapterWifi] +ssid = Test123 +pass = Test123 + +; Must have at least one registration server +; Registration server configuration +; Supported protocols: kTcp, kUdp +[RegServer_1] +address = registration.aethernet.io +port=9010 +protocol=kTcp + +[RegServer_2] +address = 34.60.244.148 +port=9010 +protocol=kTcp + +; List of clients +; Suffix after _ is used as user defined client id \see Aether::SelectClient +[Client_A] +parent_uid = 3ac93165-3d37-4970-87a6-fa4ee27744e4 + +[Client_B] +parent_uid = 3ac93165-3d37-4970-87a6-fa4ee27744e4 diff --git a/examples/windows_message_receiver/user_config.h b/examples/windows_message_receiver/user_config.h new file mode 100644 index 00000000..a2fd7142 --- /dev/null +++ b/examples/windows_message_receiver/user_config.h @@ -0,0 +1,39 @@ +/* + * Copyright 2024 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef USER_CONFIG_H_ +#define USER_CONFIG_H_ + +#include "aether/config_consts.h" +/** + * \brief For full config list and default values \see aether/config.h + */ + +// use hydrogen encryption +#define AE_CRYPTO_ASYNC AE_HYDRO_CRYPTO_PK +#define AE_CRYPTO_SYNC AE_HYDRO_CRYPTO_SK +#define AE_SIGNATURE AE_HYDRO_SIGNATURE +#define AE_KDF AE_HYDRO_KDF + +// disable debug telemetry on release +#define AE_TELE_ENABLED 1 +#define AE_TELE_LOG_CONSOLE 1 +#if defined NDEBUG +# define AE_TELE_DEBUG_MODULES 0 +#else +# define AE_TELE_DEBUG_MODULES AE_ALL +#endif +#endif // USER_CONFIG_H_ diff --git a/examples/windows_message_receiver/windows_message_receiver.cpp b/examples/windows_message_receiver/windows_message_receiver.cpp new file mode 100644 index 00000000..ada54558 --- /dev/null +++ b/examples/windows_message_receiver/windows_message_receiver.cpp @@ -0,0 +1,192 @@ +/* + * Copyright 2024 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include "aether/all.h" + +static constexpr auto kParentUid = + ae::Uid::FromString("3ac93165-3d37-4970-87a6-fa4ee27744e4"); + +class TimeSynchronizer { + public: + TimeSynchronizer() = default; + + void SetPingSentTime(ae::TimePoint ping_sent_time); + void SetPongSentTime(ae::TimePoint pong_sent_time); + + ae::Duration GetPingDuration() const; + ae::Duration GetPongDuration() const; + + private: + ae::TimePoint ping_sent_time_; + ae::TimePoint pong_sent_time_; +}; + +// Alice sends "ping"s to Bob +/* class Alice { + public: + explicit Alice(ae::AetherApp& aether_app, ae::Client::ptr client_alice, + TimeSynchronizer& time_synchronizer, ae::Uid bobs_uid); + + private: + void SendMessage(); + void ResponseReceived(ae::DataBuffer const& data_buffer); + + ae::AetherApp* aether_app_; + ae::Client::ptr client_alice_; + TimeSynchronizer* time_synchronizer_; + ae::P2pStream p2pstream_; + ae::RepeatableTask interval_sender_; + ae::Subscription receive_data_sub_; + ae::MultiSubscription send_subs_; +};*/ + +// Bob answers "pong" to each "ping" +class Bob { + public: + explicit Bob(ae::AetherApp& aether_app, ae::Client::ptr client_bob, + TimeSynchronizer& time_synchronizer); + + private: + void OnNewStream(ae::P2pPortHandle p2p_port); + void OnMessageReceived(ae::DataBuffer const& data_buffer); + + ae::AetherApp* aether_app_; + ae::Client::ptr client_bob_; + TimeSynchronizer* time_synchronizer_; + std::unique_ptr p2pstream_; + ae::Subscription new_stream_receive_sub_; + ae::Subscription message_receive_sub_; +}; + +int main() { + auto aether_app = ae::AetherApp::Construct(ae::AetherAppContext{}); + + //std::unique_ptr alice; + std::unique_ptr bob; + TimeSynchronizer time_synchronizer; + + // register or load clients + auto& bob_select = aether_app->aether()->SelectClient(kParentUid, "Bob"); + bob_select.result_event().Subscribe([&](auto const& bob_res) { + if (bob_res) { + bob = + ae::make_unique(*aether_app, bob_res.value(), time_synchronizer); + /* auto& alice_select = + aether_app->aether()->SelectClient(kParentUid, "Alice"); + alice_select.result_event().Subscribe( + [&, uid = bob_res.value()->uid()](auto const& alice_res) { + if (alice_res) { + alice = ae::make_unique(*aether_app, alice_res.value(), + time_synchronizer, uid); + // Save the current aether state + aether_app->aether().Save(); + } else { + aether_app->Exit(1); + } + });*/ + } else { + aether_app->Exit(1); + } + }); + + while (!aether_app->IsExited()) { + auto next_time = aether_app->Update(ae::Now()); + aether_app->WaitUntil(next_time); + } + return aether_app->ExitCode(); +} + +void TimeSynchronizer::SetPingSentTime(ae::TimePoint ping_sent_time) { + ping_sent_time_ = ping_sent_time; +} +void TimeSynchronizer::SetPongSentTime(ae::TimePoint pong_sent_time) { + pong_sent_time_ = pong_sent_time; +} +ae::Duration TimeSynchronizer::GetPingDuration() const { + return std::chrono::duration_cast(ae::Now() - ping_sent_time_); +} +ae::Duration TimeSynchronizer::GetPongDuration() const { + return std::chrono::duration_cast(ae::Now() - pong_sent_time_); +} + +/* Alice::Alice(ae::AetherApp& aether_app, ae::Client::ptr client_alice, + TimeSynchronizer& time_synchronizer, ae::Uid bobs_uid) + : aether_app_{&aether_app}, + client_alice_{std::move(client_alice)}, + time_synchronizer_{&time_synchronizer}, + p2pstream_{*aether_app_, client_alice_.Load(), bobs_uid, + client_alice_->message_stream_manager().CreatePort(bobs_uid)}, + interval_sender_{*aether_app_, [this]() { SendMessage(); }, + std::chrono::seconds{5}}, + receive_data_sub_{p2pstream_.out_data_event().Subscribe( + ae::MethodPtr<&Alice::ResponseReceived>{this})} {} + +void Alice::SendMessage() { + auto current_time = ae::Now(); + constexpr std::string_view ping_message = "ping"; + + time_synchronizer_->SetPingSentTime(current_time); + + std::cout << ae::Format("[{:%H:%M:%S}] Alice sends \"ping\"'\n", ae::Now()); + p2pstream_.Write({std::begin(ping_message), std::end(ping_message)}); +} + +void Alice::ResponseReceived(ae::DataBuffer const& data_buffer) { + auto pong_message = std::string_view{ + reinterpret_cast(data_buffer.data()), data_buffer.size()}; + std::cout << ae::Format( + "[{:%H:%M:%S}] Alice received \"{}\" within time {} ms\n", ae::Now(), + pong_message, + std::chrono::duration_cast( + time_synchronizer_->GetPongDuration()) + .count()); +}*/ + +Bob::Bob(ae::AetherApp& aether_app, ae::Client::ptr client_bob, + TimeSynchronizer& time_synchronizer) + : aether_app_{&aether_app}, + client_bob_{std::move(client_bob)}, + time_synchronizer_{&time_synchronizer}, + new_stream_receive_sub_{ + client_bob_->message_stream_manager().new_port_event().Subscribe( + ae::MethodPtr<&Bob::OnNewStream>{this})} {} + +void Bob::OnNewStream(ae::P2pPortHandle p2p_port) { + p2pstream_ = std::make_unique(*aether_app_, client_bob_.Load(), + p2p_port.destination(), + std::move(p2p_port)); + message_receive_sub_ = p2pstream_->out_data_event().Subscribe( + ae::MethodPtr<&Bob::OnMessageReceived>{this}); +} + +void Bob::OnMessageReceived(ae::DataBuffer const& data_buffer) { + auto ping_message = std::string_view{ + reinterpret_cast(data_buffer.data()), data_buffer.size()}; + std::cout << ae::Format( + "[{:%H:%M:%S}] Bob received \"{}\" within time {} ms\n", ae::Now(), + ping_message, + std::chrono::duration_cast( + time_synchronizer_->GetPingDuration()) + .count()); + std::cout << ae::Format("Hex string [{}]\n", data_buffer); + time_synchronizer_->SetPongSentTime(ae::Now()); + constexpr std::string_view pong_message = "pong"; + //std::cout << ae::Format("[{:%H:%M:%S}] Bob sends \"pong\"\n", ae::Now()); + //p2pstream_->Write({std::begin(pong_message), std::end(pong_message)}); +} From 89971208ee953ef49798d91398f3afd0e7fce035 Mon Sep 17 00:00:00 2001 From: Kiryanov D V Date: Thu, 9 Jul 2026 17:02:56 +0300 Subject: [PATCH 10/20] Fixing zero packet send. --- aether/client_messages/p2p_message_stream.cpp | 12 ++++ .../client_server_connection.cpp | 9 +++ aether/stream_api/api_call_adapter.h | 6 +- aether/transport/system_sockets/udp/udp.h | 2 + aether/wifi/esp_wifi_driver.cpp | 14 ++--- aether/write_action/buffer_write.h | 57 ++++++++++++++++++- 6 files changed, 91 insertions(+), 9 deletions(-) diff --git a/aether/client_messages/p2p_message_stream.cpp b/aether/client_messages/p2p_message_stream.cpp index 5737c1da..1c9355aa 100644 --- a/aether/client_messages/p2p_message_stream.cpp +++ b/aether/client_messages/p2p_message_stream.cpp @@ -43,8 +43,15 @@ class MessageSendStream final : public IStream { } WriteAction& Write(AeMessage&& message) override { + AE_TELED_ERROR("[CALL-CHAIN] MessageSendStream::Write data_size={} " + "cloud_connections={}", + message.data.size(), cloud_connection_->count_connections()); return cloud_connection_->CallApi( ApiCall{[&message](ApiContext& auth_api, auto*) { + AE_TELED_ERROR( + "[CALL-CHAIN] MessageSendStream::ApiCall send_message " + "data_size={}", + message.data.size()); auth_api->send_message(std::move(message)); }}, request_policy_); @@ -189,9 +196,12 @@ P2pStream::P2pStream(AeContext const& ae_context, Ptr const& client, P2pStream::~P2pStream() = default; WriteAction& P2pStream::Write(DataBuffer&& data) { + AE_TELED_ERROR("[CALL-CHAIN] P2pStream::Write input_size={}", data.size()); AE_TELED_DEBUG("Write message for uid {} size:{} data:{}", destination_, data.size(), data); AeMessage message_data{destination_, std::move(data)}; + AE_TELED_ERROR("[CALL-CHAIN] P2pStream::Write message_data_size={}", + message_data.data.size()); return buffer_write_.Write(std::move(message_data)); } @@ -280,6 +290,8 @@ std::unique_ptr P2pStream::MakeDestinationCloudConn( } WriteAction* P2pStream::OnWrite(AeMessage&& message) { + AE_TELED_ERROR("[CALL-CHAIN] P2pStream::OnWrite data_size={} connected={}", + message.data.size(), message_send_stream_ != nullptr); if (!message_send_stream_) { return {}; } diff --git a/aether/server_connections/client_server_connection.cpp b/aether/server_connections/client_server_connection.cpp index 39bed9c7..5e18cc41 100644 --- a/aether/server_connections/client_server_connection.cpp +++ b/aether/server_connections/client_server_connection.cpp @@ -98,6 +98,11 @@ BufferedServerConnection::BufferedServerConnection(AeContext const& ae_context, Ptr const& server) : buffer_write{ae_context, [&](DataBuffer&& in_data) -> WriteAction* { + AE_TELED_ERROR( + "[CALL-CHAIN] BufferedServerConnection::direct " + "data_size={} writable={}", + in_data.size(), + server_connection.stream_info().is_writable); if (server_connection.stream_info().is_writable) { return &server_connection.Write(std::move(in_data)); } @@ -121,6 +126,8 @@ BufferedServerConnection::BufferedServerConnection(AeContext const& ae_context, } WriteAction& BufferedServerConnection::Write(DataBuffer&& in_data) { + AE_TELED_ERROR("[CALL-CHAIN] BufferedServerConnection::Write data_size={}", + in_data.size()); return buffer_write.Write(std::move(in_data)); } @@ -187,8 +194,10 @@ WriteAction& ClientServerConnection::LoginApiCall(SubApi login_api) { WriteAction& ClientServerConnection::AuthorizedApiCall( SubApi auth_api) { + AE_TELED_ERROR("[CALL-CHAIN] ClientServerConnection::AuthorizedApiCall begin"); auto api_call = ApiCallAdapter{ApiContext{login_api_}, server_connection_}; api_call->login_by_alias(ephemeral_uid_, std::move(auth_api)); + AE_TELED_ERROR("[CALL-CHAIN] ClientServerConnection::AuthorizedApiCall flush"); // cppcheck reports false positive // cppcheck-suppress returnReference return api_call.Flush(); diff --git a/aether/stream_api/api_call_adapter.h b/aether/stream_api/api_call_adapter.h index f8f42a71..28c6e782 100644 --- a/aether/stream_api/api_call_adapter.h +++ b/aether/stream_api/api_call_adapter.h @@ -28,6 +28,7 @@ #include "aether/api_protocol/api_context.h" #include "aether/prepared_packet/packet_encoder.h" #include "aether/types/data_buffer.h" +#include "aether/tele/tele.h" namespace ae { @@ -110,7 +111,10 @@ EncodePacketResult EncodePacket(PreparedPacketEncoder& prepared, return EncodePacketResult{report, nullptr}; } - auto& action = out.Write(std::move(data)); + auto packet = std::move(data).Pack(); + AE_TELED_ERROR("[CALL-CHAIN] ApiCallAdapter::EncodePacket packet_size={}", + packet.size()); + auto& action = out.Write(std::move(packet)); return EncodePacketResult{report, &action}; } diff --git a/aether/transport/system_sockets/udp/udp.h b/aether/transport/system_sockets/udp/udp.h index bdfa3a71..642f5052 100644 --- a/aether/transport/system_sockets/udp/udp.h +++ b/aether/transport/system_sockets/udp/udp.h @@ -156,6 +156,8 @@ class UdpTransport final : public upd_internal::UdpBase { } WriteAction& Write(DataBuffer&& in_data) override { + AE_TELED_ERROR("[CALL-CHAIN] UdpTransport::Write endpoint={} data_size={}", + endpoint_, in_data.size()); assert(in_data.size() != 0); AE_TELE_DEBUG(kUdpTransportSend, "Socket {} send data size:{}", endpoint_, in_data.size()); diff --git a/aether/wifi/esp_wifi_driver.cpp b/aether/wifi/esp_wifi_driver.cpp index 44f73c45..a5435b41 100644 --- a/aether/wifi/esp_wifi_driver.cpp +++ b/aether/wifi/esp_wifi_driver.cpp @@ -22,12 +22,11 @@ # include "esp_event.h" # include "esp_log.h" +# include "esp_mac.h" # include "esp_private/wifi.h" # include "esp_system.h" # include "esp_wifi.h" # include "nvs_flash.h" -# include "esp_mac.h" -# include "esp_event.h" # include "lwip/err.h" # include "lwip/ip4_addr.h" @@ -82,9 +81,11 @@ void EventHandler(void* arg, esp_event_base_t event_base, int32_t event_id, esp_err_t SetupBssid(wifi_config_t& wifi_config, WiFiBaseStation const& base_station) { - ESP_LOGD(kTag, "Restored from cache BSSID:" MACSTR " CHN:%u", - MAC2STR(base_station.target_bssid), - static_cast(base_station.target_channel)); + std::array debug_bssid; + memcpy(debug_bssid.data(), base_station.target_bssid, + sizeof(base_station.target_bssid)); + AE_TELED_DEBUG("Restored from cash BSSID:{} CHN:{}", debug_bssid, + static_cast(base_station.target_channel)); wifi_config.sta.scan_method = WIFI_FAST_SCAN; // Fast scan wifi_config.sta.bssid_set = true; // Enable BSSID binding @@ -232,9 +233,8 @@ esp_err_t StartWifiConnection( // Restore saved Base Station auto err = esp_wifi_driver_internal::SetupBssid(wifi_config, *base_station); if (err != ESP_OK) { - ESP_LOGE(kTag, "Failed to set BSSID."); + AE_TELED_ERROR("Failed to set BSSID"); // If an error occurs, exit - return err; } } diff --git a/aether/write_action/buffer_write.h b/aether/write_action/buffer_write.h index 12ef6215..8492c2ff 100644 --- a/aether/write_action/buffer_write.h +++ b/aether/write_action/buffer_write.h @@ -45,9 +45,23 @@ namespace ae { do { \ AE_TELED_WARNING(__VA_ARGS__); \ } while (false) + +template +inline std::size_t DebugPayloadSize(TValue const& value) { + if constexpr (requires { value.size(); }) { + return value.size(); + } else if constexpr (requires { value.data.size(); }) { + return value.data.size(); + } else { + return 0; + } +} + #else # define BW_LOG_DEBUG(...) # define BW_LOG_WARNING(...) +template +inline std::size_t DebugPayloadSize(TValue const&){return 0} #endif class BufferedWriteAction final : public WriteAction { @@ -90,8 +104,14 @@ class BufferWrite { /** * \brief Control buffering. */ - void buffer_on() { buffer_on_ = true; } + void buffer_on() { + BW_LOG_WARNING("[CALL-CHAIN] BufferWrite::buffer_on buffered={}", + buffer_.size()); + buffer_on_ = true; + } void buffer_off() { + BW_LOG_WARNING("[CALL-CHAIN] BufferWrite::buffer_off buffered={}", + buffer_.size()); buffer_on_ = false; DrainBuffer(); } @@ -101,6 +121,10 @@ class BufferWrite { * \brief Write data to or through buffer. */ WriteAction& Write(T&& data) { + BW_LOG_WARNING( + "[CALL-CHAIN] BufferWrite::Write payload_size={} " + "buffer_on={} buffered={}", + DebugPayloadSize(data), buffer_on_, buffer_.size()); auto should_be_buffered = buffer_on_ || !buffer_.empty(); if (should_be_buffered) { return WriteToBuffer(std::move(data)); @@ -122,6 +146,10 @@ class BufferWrite { private: WriteAction& WriteToBuffer(T&& data) { + BW_LOG_WARNING( + "[CALL-CHAIN] BufferWrite::WriteToBuffer payload_size={} " + "buffer_on={} buffered={}", + DebugPayloadSize(data), buffer_on_, buffer_.size()); if (buffer_.full()) { BW_LOG_WARNING("Buffer is full"); return FailedWrite(); @@ -143,8 +171,14 @@ class BufferWrite { } WriteAction& DirectWrite(T&& data) { + BW_LOG_WARNING( + "[CALL-CHAIN] BufferWrite::DirectWrite payload_size={} " + "buffered={}", + DebugPayloadSize(data), buffer_.size()); BW_LOG_DEBUG("BufferWrite: direct write"); auto* write_action = direct_write_(std::move(data)); + BW_LOG_WARNING("[CALL-CHAIN] BufferWrite::DirectWrite result={}", + write_action != nullptr); if (write_action == nullptr) { return FailedWrite(); } @@ -155,9 +189,17 @@ class BufferWrite { * Write all buffered data in FIFO order. */ void DrainBuffer() { + BW_LOG_WARNING( + "[CALL-CHAIN] BufferWrite::DrainBuffer begin buffer_on={} " + "buffered={}", + buffer_on_, buffer_.size()); BW_LOG_DEBUG("BufferWrite: drain the buffer, size {}", buffer_.size()); // try send as many as possible for (auto& be : buffer_) { + BW_LOG_WARNING( + "[CALL-CHAIN] BufferWrite::DrainBuffer entry finished={} " + "sent={} payload_size={}", + be.wa.is_finished(), be.sent, DebugPayloadSize(be.data)); if (be.wa.is_finished()) { // notice! circular buffer pop is just incrementing the out pointer it // does not invalidate the iterators @@ -165,13 +207,26 @@ class BufferWrite { continue; } if (be.sent) { + BW_LOG_WARNING( + "[CALL-CHAIN] BufferWrite::DrainBuffer skip in-flight " + "payload_size={}", + DebugPayloadSize(be.data)); continue; } // buffer state might change during direct write if (buffer_on_) { + BW_LOG_WARNING("[CALL-CHAIN] BufferWrite::DrainBuffer stop buffer_on"); break; } + BW_LOG_WARNING( + "[CALL-CHAIN] BufferWrite::DrainBuffer direct before_move " + "payload_size={}", + DebugPayloadSize(be.data)); auto* dwa = direct_write_(std::move(be.data)); + BW_LOG_WARNING( + "[CALL-CHAIN] BufferWrite::DrainBuffer direct result={} " + "after_move_payload_size={}", + dwa != nullptr, DebugPayloadSize(be.data)); // empty write action means no data has been written if (dwa == nullptr) { break; From 42e84e13d4b73de642abb2ead4624f7bc0352695 Mon Sep 17 00:00:00 2001 From: Kiryanov D V Date: Fri, 10 Jul 2026 16:50:44 +0300 Subject: [PATCH 11/20] Removing comments. --- .../windows_message_receiver.cpp | 50 +------------------ 1 file changed, 1 insertion(+), 49 deletions(-) diff --git a/examples/windows_message_receiver/windows_message_receiver.cpp b/examples/windows_message_receiver/windows_message_receiver.cpp index ada54558..55531d32 100644 --- a/examples/windows_message_receiver/windows_message_receiver.cpp +++ b/examples/windows_message_receiver/windows_message_receiver.cpp @@ -87,19 +87,6 @@ int main() { if (bob_res) { bob = ae::make_unique(*aether_app, bob_res.value(), time_synchronizer); - /* auto& alice_select = - aether_app->aether()->SelectClient(kParentUid, "Alice"); - alice_select.result_event().Subscribe( - [&, uid = bob_res.value()->uid()](auto const& alice_res) { - if (alice_res) { - alice = ae::make_unique(*aether_app, alice_res.value(), - time_synchronizer, uid); - // Save the current aether state - aether_app->aether().Save(); - } else { - aether_app->Exit(1); - } - });*/ } else { aether_app->Exit(1); } @@ -125,39 +112,6 @@ ae::Duration TimeSynchronizer::GetPongDuration() const { return std::chrono::duration_cast(ae::Now() - pong_sent_time_); } -/* Alice::Alice(ae::AetherApp& aether_app, ae::Client::ptr client_alice, - TimeSynchronizer& time_synchronizer, ae::Uid bobs_uid) - : aether_app_{&aether_app}, - client_alice_{std::move(client_alice)}, - time_synchronizer_{&time_synchronizer}, - p2pstream_{*aether_app_, client_alice_.Load(), bobs_uid, - client_alice_->message_stream_manager().CreatePort(bobs_uid)}, - interval_sender_{*aether_app_, [this]() { SendMessage(); }, - std::chrono::seconds{5}}, - receive_data_sub_{p2pstream_.out_data_event().Subscribe( - ae::MethodPtr<&Alice::ResponseReceived>{this})} {} - -void Alice::SendMessage() { - auto current_time = ae::Now(); - constexpr std::string_view ping_message = "ping"; - - time_synchronizer_->SetPingSentTime(current_time); - - std::cout << ae::Format("[{:%H:%M:%S}] Alice sends \"ping\"'\n", ae::Now()); - p2pstream_.Write({std::begin(ping_message), std::end(ping_message)}); -} - -void Alice::ResponseReceived(ae::DataBuffer const& data_buffer) { - auto pong_message = std::string_view{ - reinterpret_cast(data_buffer.data()), data_buffer.size()}; - std::cout << ae::Format( - "[{:%H:%M:%S}] Alice received \"{}\" within time {} ms\n", ae::Now(), - pong_message, - std::chrono::duration_cast( - time_synchronizer_->GetPongDuration()) - .count()); -}*/ - Bob::Bob(ae::AetherApp& aether_app, ae::Client::ptr client_bob, TimeSynchronizer& time_synchronizer) : aether_app_{&aether_app}, @@ -179,7 +133,7 @@ void Bob::OnMessageReceived(ae::DataBuffer const& data_buffer) { auto ping_message = std::string_view{ reinterpret_cast(data_buffer.data()), data_buffer.size()}; std::cout << ae::Format( - "[{:%H:%M:%S}] Bob received \"{}\" within time {} ms\n", ae::Now(), + ">>>\n>>>\n>>>\n[{:%H:%M:%S}] Bob received \"{}\" within time {} ms\n>>>\n>>>\n>>>\n", ae::Now(), ping_message, std::chrono::duration_cast( time_synchronizer_->GetPingDuration()) @@ -187,6 +141,4 @@ void Bob::OnMessageReceived(ae::DataBuffer const& data_buffer) { std::cout << ae::Format("Hex string [{}]\n", data_buffer); time_synchronizer_->SetPongSentTime(ae::Now()); constexpr std::string_view pong_message = "pong"; - //std::cout << ae::Format("[{:%H:%M:%S}] Bob sends \"pong\"\n", ae::Now()); - //p2pstream_->Write({std::begin(pong_message), std::end(pong_message)}); } From 2fd778e271c566784d9816ce596cb9e0bf356f7e Mon Sep 17 00:00:00 2001 From: Kiryanov D V Date: Tue, 14 Jul 2026 10:20:38 +0300 Subject: [PATCH 12/20] New parent UID. --- examples/windows_message_receiver/windows_message_receiver.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/windows_message_receiver/windows_message_receiver.cpp b/examples/windows_message_receiver/windows_message_receiver.cpp index 55531d32..647760fe 100644 --- a/examples/windows_message_receiver/windows_message_receiver.cpp +++ b/examples/windows_message_receiver/windows_message_receiver.cpp @@ -20,7 +20,7 @@ #include "aether/all.h" static constexpr auto kParentUid = - ae::Uid::FromString("3ac93165-3d37-4970-87a6-fa4ee27744e4"); + ae::Uid::FromString("B1AC52C8-8D94-BD39-4C01-A631AC594165"); class TimeSynchronizer { public: From aa345a38d3cd81404df3cad4827dbd50130b0653 Mon Sep 17 00:00:00 2001 From: Kiryanov D V Date: Wed, 12 Aug 2026 12:43:14 +0300 Subject: [PATCH 13/20] Removing debug output. --- aether/client_messages/p2p_message_stream.cpp | 17 +----- .../client_server_connection.cpp | 11 ---- aether/stream_api/api_call_adapter.h | 11 ++-- aether/transport/system_sockets/udp/udp.h | 4 +- aether/write_action/buffer_write.h | 56 +------------------ 5 files changed, 8 insertions(+), 91 deletions(-) diff --git a/aether/client_messages/p2p_message_stream.cpp b/aether/client_messages/p2p_message_stream.cpp index 1c9355aa..a1e9905b 100644 --- a/aether/client_messages/p2p_message_stream.cpp +++ b/aether/client_messages/p2p_message_stream.cpp @@ -43,15 +43,8 @@ class MessageSendStream final : public IStream { } WriteAction& Write(AeMessage&& message) override { - AE_TELED_ERROR("[CALL-CHAIN] MessageSendStream::Write data_size={} " - "cloud_connections={}", - message.data.size(), cloud_connection_->count_connections()); return cloud_connection_->CallApi( ApiCall{[&message](ApiContext& auth_api, auto*) { - AE_TELED_ERROR( - "[CALL-CHAIN] MessageSendStream::ApiCall send_message " - "data_size={}", - message.data.size()); auth_api->send_message(std::move(message)); }}, request_policy_); @@ -70,8 +63,8 @@ class MessageSendStream final : public IStream { continue; } - auto block = conn->ExportPreparedSendMessageBlock(target_uid, - reserve_nonce_count); + auto block = + conn->ExportPreparedSendMessageBlock(target_uid, reserve_nonce_count); if (block) { return block; } @@ -196,12 +189,9 @@ P2pStream::P2pStream(AeContext const& ae_context, Ptr const& client, P2pStream::~P2pStream() = default; WriteAction& P2pStream::Write(DataBuffer&& data) { - AE_TELED_ERROR("[CALL-CHAIN] P2pStream::Write input_size={}", data.size()); AE_TELED_DEBUG("Write message for uid {} size:{} data:{}", destination_, data.size(), data); AeMessage message_data{destination_, std::move(data)}; - AE_TELED_ERROR("[CALL-CHAIN] P2pStream::Write message_data_size={}", - message_data.data.size()); return buffer_write_.Write(std::move(message_data)); } @@ -247,7 +237,6 @@ P2pStream::ExportPreparedSendMessageBlock(std::uint32_t reserve_nonce_count) { destination_, reserve_nonce_count); } - void P2pStream::ConnectReceive() { out_data_sub_ = handle_.out_data_event().Subscribe(MethodPtr<&P2pStream::WriteOut>{this}); @@ -290,8 +279,6 @@ std::unique_ptr P2pStream::MakeDestinationCloudConn( } WriteAction* P2pStream::OnWrite(AeMessage&& message) { - AE_TELED_ERROR("[CALL-CHAIN] P2pStream::OnWrite data_size={} connected={}", - message.data.size(), message_send_stream_ != nullptr); if (!message_send_stream_) { return {}; } diff --git a/aether/server_connections/client_server_connection.cpp b/aether/server_connections/client_server_connection.cpp index 5e18cc41..d3cfa3ac 100644 --- a/aether/server_connections/client_server_connection.cpp +++ b/aether/server_connections/client_server_connection.cpp @@ -98,11 +98,6 @@ BufferedServerConnection::BufferedServerConnection(AeContext const& ae_context, Ptr const& server) : buffer_write{ae_context, [&](DataBuffer&& in_data) -> WriteAction* { - AE_TELED_ERROR( - "[CALL-CHAIN] BufferedServerConnection::direct " - "data_size={} writable={}", - in_data.size(), - server_connection.stream_info().is_writable); if (server_connection.stream_info().is_writable) { return &server_connection.Write(std::move(in_data)); } @@ -126,8 +121,6 @@ BufferedServerConnection::BufferedServerConnection(AeContext const& ae_context, } WriteAction& BufferedServerConnection::Write(DataBuffer&& in_data) { - AE_TELED_ERROR("[CALL-CHAIN] BufferedServerConnection::Write data_size={}", - in_data.size()); return buffer_write.Write(std::move(in_data)); } @@ -194,10 +187,8 @@ WriteAction& ClientServerConnection::LoginApiCall(SubApi login_api) { WriteAction& ClientServerConnection::AuthorizedApiCall( SubApi auth_api) { - AE_TELED_ERROR("[CALL-CHAIN] ClientServerConnection::AuthorizedApiCall begin"); auto api_call = ApiCallAdapter{ApiContext{login_api_}, server_connection_}; api_call->login_by_alias(ephemeral_uid_, std::move(auth_api)); - AE_TELED_ERROR("[CALL-CHAIN] ClientServerConnection::AuthorizedApiCall flush"); // cppcheck reports false positive // cppcheck-suppress returnReference return api_call.Flush(); @@ -264,8 +255,6 @@ ClientServerConnection::ExportPreparedSendMessageBlock( return block; } - - void ClientServerConnection::OutData(DataBuffer const& data) { auto parser = ApiParser{protocol_context_, data}; parser.Parse(client_api_unsafe_); diff --git a/aether/stream_api/api_call_adapter.h b/aether/stream_api/api_call_adapter.h index 28c6e782..dcf4ff20 100644 --- a/aether/stream_api/api_call_adapter.h +++ b/aether/stream_api/api_call_adapter.h @@ -92,7 +92,8 @@ struct PreparedPacketEncoder { // Span payload, // DataBuffer& out); // -// Current internal step keeps old Aether path alive by using ByteIStream as out. +// Current internal step keeps old Aether path alive by using ByteIStream as +// out. struct EncodePacketResult { EncodePacketReport report; WriteAction* action = nullptr; @@ -104,16 +105,13 @@ struct EncodePacketResult { template EncodePacketResult EncodePacket(PreparedPacketEncoder& prepared, - ApiContext&& data, - ByteIStream& out) { + ApiContext&& data, ByteIStream& out) { auto report = prepared.TakeNonce(); if (!report) { return EncodePacketResult{report, nullptr}; } auto packet = std::move(data).Pack(); - AE_TELED_ERROR("[CALL-CHAIN] ApiCallAdapter::EncodePacket packet_size={}", - packet.size()); auto& action = out.Write(std::move(packet)); return EncodePacketResult{report, &action}; @@ -141,8 +139,7 @@ class ApiCallAdapter { if (!result) { std::cerr << "FastTx EncodePacket failed: " - << fast_tx_internal::ToString(result.report.error) - << "\n"; + << fast_tx_internal::ToString(result.report.error) << "\n"; assert(false); } diff --git a/aether/transport/system_sockets/udp/udp.h b/aether/transport/system_sockets/udp/udp.h index 642f5052..7f7e7b5e 100644 --- a/aether/transport/system_sockets/udp/udp.h +++ b/aether/transport/system_sockets/udp/udp.h @@ -70,7 +70,7 @@ class SendAction final : public PacketSendAction { AE_TELED_ERROR("Send error, sent size isn't same as packet size"); SetStatus(WriteAction::Status::kFail); return; - } + } SetStatus(WriteAction::Status::kSuccess); } @@ -156,8 +156,6 @@ class UdpTransport final : public upd_internal::UdpBase { } WriteAction& Write(DataBuffer&& in_data) override { - AE_TELED_ERROR("[CALL-CHAIN] UdpTransport::Write endpoint={} data_size={}", - endpoint_, in_data.size()); assert(in_data.size() != 0); AE_TELE_DEBUG(kUdpTransportSend, "Socket {} send data size:{}", endpoint_, in_data.size()); diff --git a/aether/write_action/buffer_write.h b/aether/write_action/buffer_write.h index 8492c2ff..4b3a0baf 100644 --- a/aether/write_action/buffer_write.h +++ b/aether/write_action/buffer_write.h @@ -46,22 +46,9 @@ namespace ae { AE_TELED_WARNING(__VA_ARGS__); \ } while (false) -template -inline std::size_t DebugPayloadSize(TValue const& value) { - if constexpr (requires { value.size(); }) { - return value.size(); - } else if constexpr (requires { value.data.size(); }) { - return value.data.size(); - } else { - return 0; - } -} - #else # define BW_LOG_DEBUG(...) # define BW_LOG_WARNING(...) -template -inline std::size_t DebugPayloadSize(TValue const&){return 0} #endif class BufferedWriteAction final : public WriteAction { @@ -104,14 +91,8 @@ class BufferWrite { /** * \brief Control buffering. */ - void buffer_on() { - BW_LOG_WARNING("[CALL-CHAIN] BufferWrite::buffer_on buffered={}", - buffer_.size()); - buffer_on_ = true; - } + void buffer_on() { buffer_on_ = true; } void buffer_off() { - BW_LOG_WARNING("[CALL-CHAIN] BufferWrite::buffer_off buffered={}", - buffer_.size()); buffer_on_ = false; DrainBuffer(); } @@ -121,10 +102,6 @@ class BufferWrite { * \brief Write data to or through buffer. */ WriteAction& Write(T&& data) { - BW_LOG_WARNING( - "[CALL-CHAIN] BufferWrite::Write payload_size={} " - "buffer_on={} buffered={}", - DebugPayloadSize(data), buffer_on_, buffer_.size()); auto should_be_buffered = buffer_on_ || !buffer_.empty(); if (should_be_buffered) { return WriteToBuffer(std::move(data)); @@ -146,10 +123,6 @@ class BufferWrite { private: WriteAction& WriteToBuffer(T&& data) { - BW_LOG_WARNING( - "[CALL-CHAIN] BufferWrite::WriteToBuffer payload_size={} " - "buffer_on={} buffered={}", - DebugPayloadSize(data), buffer_on_, buffer_.size()); if (buffer_.full()) { BW_LOG_WARNING("Buffer is full"); return FailedWrite(); @@ -171,14 +144,8 @@ class BufferWrite { } WriteAction& DirectWrite(T&& data) { - BW_LOG_WARNING( - "[CALL-CHAIN] BufferWrite::DirectWrite payload_size={} " - "buffered={}", - DebugPayloadSize(data), buffer_.size()); BW_LOG_DEBUG("BufferWrite: direct write"); auto* write_action = direct_write_(std::move(data)); - BW_LOG_WARNING("[CALL-CHAIN] BufferWrite::DirectWrite result={}", - write_action != nullptr); if (write_action == nullptr) { return FailedWrite(); } @@ -189,17 +156,9 @@ class BufferWrite { * Write all buffered data in FIFO order. */ void DrainBuffer() { - BW_LOG_WARNING( - "[CALL-CHAIN] BufferWrite::DrainBuffer begin buffer_on={} " - "buffered={}", - buffer_on_, buffer_.size()); BW_LOG_DEBUG("BufferWrite: drain the buffer, size {}", buffer_.size()); // try send as many as possible for (auto& be : buffer_) { - BW_LOG_WARNING( - "[CALL-CHAIN] BufferWrite::DrainBuffer entry finished={} " - "sent={} payload_size={}", - be.wa.is_finished(), be.sent, DebugPayloadSize(be.data)); if (be.wa.is_finished()) { // notice! circular buffer pop is just incrementing the out pointer it // does not invalidate the iterators @@ -207,26 +166,13 @@ class BufferWrite { continue; } if (be.sent) { - BW_LOG_WARNING( - "[CALL-CHAIN] BufferWrite::DrainBuffer skip in-flight " - "payload_size={}", - DebugPayloadSize(be.data)); continue; } // buffer state might change during direct write if (buffer_on_) { - BW_LOG_WARNING("[CALL-CHAIN] BufferWrite::DrainBuffer stop buffer_on"); break; } - BW_LOG_WARNING( - "[CALL-CHAIN] BufferWrite::DrainBuffer direct before_move " - "payload_size={}", - DebugPayloadSize(be.data)); auto* dwa = direct_write_(std::move(be.data)); - BW_LOG_WARNING( - "[CALL-CHAIN] BufferWrite::DrainBuffer direct result={} " - "after_move_payload_size={}", - dwa != nullptr, DebugPayloadSize(be.data)); // empty write action means no data has been written if (dwa == nullptr) { break; From e32cdb66beb65513a0f9e9e1ce8939bd424f62c3 Mon Sep 17 00:00:00 2001 From: Kiryanov D V Date: Wed, 12 Aug 2026 14:24:39 +0300 Subject: [PATCH 14/20] Removing the Alice class from the example. --- .../windows_message_receiver.cpp | 26 +++---------------- 1 file changed, 4 insertions(+), 22 deletions(-) diff --git a/examples/windows_message_receiver/windows_message_receiver.cpp b/examples/windows_message_receiver/windows_message_receiver.cpp index 647760fe..f728ae4c 100644 --- a/examples/windows_message_receiver/windows_message_receiver.cpp +++ b/examples/windows_message_receiver/windows_message_receiver.cpp @@ -37,25 +37,6 @@ class TimeSynchronizer { ae::TimePoint pong_sent_time_; }; -// Alice sends "ping"s to Bob -/* class Alice { - public: - explicit Alice(ae::AetherApp& aether_app, ae::Client::ptr client_alice, - TimeSynchronizer& time_synchronizer, ae::Uid bobs_uid); - - private: - void SendMessage(); - void ResponseReceived(ae::DataBuffer const& data_buffer); - - ae::AetherApp* aether_app_; - ae::Client::ptr client_alice_; - TimeSynchronizer* time_synchronizer_; - ae::P2pStream p2pstream_; - ae::RepeatableTask interval_sender_; - ae::Subscription receive_data_sub_; - ae::MultiSubscription send_subs_; -};*/ - // Bob answers "pong" to each "ping" class Bob { public: @@ -77,7 +58,7 @@ class Bob { int main() { auto aether_app = ae::AetherApp::Construct(ae::AetherAppContext{}); - //std::unique_ptr alice; + // std::unique_ptr alice; std::unique_ptr bob; TimeSynchronizer time_synchronizer; @@ -133,8 +114,9 @@ void Bob::OnMessageReceived(ae::DataBuffer const& data_buffer) { auto ping_message = std::string_view{ reinterpret_cast(data_buffer.data()), data_buffer.size()}; std::cout << ae::Format( - ">>>\n>>>\n>>>\n[{:%H:%M:%S}] Bob received \"{}\" within time {} ms\n>>>\n>>>\n>>>\n", ae::Now(), - ping_message, + ">>>\n>>>\n>>>\n[{:%H:%M:%S}] Bob received \"{}\" within time {} " + "ms\n>>>\n>>>\n>>>\n", + ae::Now(), ping_message, std::chrono::duration_cast( time_synchronizer_->GetPingDuration()) .count()); From b2e7590814f7b4db79cbc01728adcd59ba73f1c1 Mon Sep 17 00:00:00 2001 From: Kiryanov D V Date: Wed, 12 Aug 2026 14:30:08 +0300 Subject: [PATCH 15/20] Changing the build type from Debug to Release. --- .../vscode/aether-client-cpp/.vscode/settings.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/projects/espressif_riscv/vscode/aether-client-cpp/.vscode/settings.json b/projects/espressif_riscv/vscode/aether-client-cpp/.vscode/settings.json index 49566852..e487d68c 100644 --- a/projects/espressif_riscv/vscode/aether-client-cpp/.vscode/settings.json +++ b/projects/espressif_riscv/vscode/aether-client-cpp/.vscode/settings.json @@ -2,7 +2,8 @@ "idf.cmakeCompilerArgs": [ "-G", "Ninja", - "-DCMAKE_BUILD_TYPE=Release" + "-DCMAKE_BUILD_TYPE=Release", + "-DUSER_CONFIG=../../../../../config/user_config_hydrogen.h" ], "C_Cpp.intelliSenseEngine": "Tag Parser", "idf.adapterTargetName": "esp32c6", From b11639c1694ed72c9352001bc4d0b156b1063523 Mon Sep 17 00:00:00 2001 From: BartolomeyKant Date: Wed, 19 Aug 2026 15:06:05 +0500 Subject: [PATCH 16/20] fix include tele --- aether/stream_api/api_call_adapter.h | 15 ++++++++------- aether/wifi/esp_wifi_driver.cpp | 2 ++ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/aether/stream_api/api_call_adapter.h b/aether/stream_api/api_call_adapter.h index dcf4ff20..f89657af 100644 --- a/aether/stream_api/api_call_adapter.h +++ b/aether/stream_api/api_call_adapter.h @@ -17,18 +17,19 @@ #ifndef AETHER_STREAM_API_API_CALL_ADAPTER_H_ #define AETHER_STREAM_API_API_CALL_ADAPTER_H_ -#include -#include -#include -#include -#include #include +#include +#include +#include +#include +#include -#include "aether/stream_api/istream.h" #include "aether/api_protocol/api_context.h" #include "aether/prepared_packet/packet_encoder.h" +#include "aether/stream_api/istream.h" #include "aether/types/data_buffer.h" -#include "aether/tele/tele.h" + +#include "aether/tele.h" namespace ae { diff --git a/aether/wifi/esp_wifi_driver.cpp b/aether/wifi/esp_wifi_driver.cpp index a5435b41..db5c14be 100644 --- a/aether/wifi/esp_wifi_driver.cpp +++ b/aether/wifi/esp_wifi_driver.cpp @@ -33,6 +33,8 @@ # include "lwip/ip6_addr.h" # include "lwip/sys.h" +# include "aether/tele.h" + extern "C" esp_err_t esp_wifi_internal_set_retry_counter(uint8_t short_retry, uint8_t long_retry); extern "C" esp_err_t esp_wifi_internal_get_fix_rate(wifi_interface_t ifx, From 0015a404fc8c43fd8fc375695e29b20f19cc9e89 Mon Sep 17 00:00:00 2001 From: BartolomeyKant Date: Mon, 24 Aug 2026 20:17:11 +0500 Subject: [PATCH 17/20] refactored prepared send message block add endpoint() property for channel add method to get cached cloud --- aether/channels/channel.h | 10 +- aether/channels/ethernet_channel.h | 2 + aether/channels/lora_module_channel.h | 2 + aether/channels/modem_channel.h | 2 + aether/channels/wifi_channel.h | 2 + aether/client_messages/p2p_message_stream.cpp | 34 +- aether/client_messages/p2p_message_stream.h | 3 - .../client_cloud_manager.cpp | 8 + .../connection_manager/client_cloud_manager.h | 9 + aether/prepared_packet/packet_encoder.cpp | 46 +- aether/prepared_packet/packet_encoder.h | 31 +- .../prepared_packet/prepare_send_message.cpp | 12 - aether/prepared_packet/prepare_send_message.h | 21 - aether/prepared_packet/prepared_block.h | 145 +++++ .../prepared_packet/prepared_send_message.cpp | 225 ++++++- .../prepared_packet/prepared_send_message.h | 124 ++-- .../client_server_connection.cpp | 56 -- .../client_server_connection.h | 8 - aether/stream_api/api_call_adapter.h | 106 +--- tests/CMakeLists.txt | 1 + tests/test-prepared-packet/CMakeLists.txt | 35 ++ tests/test-prepared-packet/main.cpp | 24 + .../test-prepared-packet.cpp | 581 ++++++++++++++++++ .../test_server_connection_recovery.cpp | 2 + 24 files changed, 1138 insertions(+), 351 deletions(-) delete mode 100644 aether/prepared_packet/prepare_send_message.cpp delete mode 100644 aether/prepared_packet/prepare_send_message.h create mode 100644 aether/prepared_packet/prepared_block.h create mode 100644 tests/test-prepared-packet/CMakeLists.txt create mode 100644 tests/test-prepared-packet/main.cpp create mode 100644 tests/test-prepared-packet/test-prepared-packet.cpp diff --git a/aether/channels/channel.h b/aether/channels/channel.h index 107b23f4..7ae7082e 100644 --- a/aether/channels/channel.h +++ b/aether/channels/channel.h @@ -17,12 +17,15 @@ #ifndef AETHER_CHANNELS_CHANNEL_H_ #define AETHER_CHANNELS_CHANNEL_H_ +#include + +#include "aether/channels/channel_statistics.h" +#include "aether/channels/channels_types.h" +#include "aether/executors/executors.h" #include "aether/memory.h" #include "aether/obj/obj.h" -#include "aether/executors/executors.h" #include "aether/stream_api/istream.h" -#include "aether/channels/channels_types.h" -#include "aether/channels/channel_statistics.h" +#include "aether/types/address.h" namespace ae { using TransportBuildSender = @@ -44,6 +47,7 @@ class Channel : public Obj { */ virtual TransportBuildSender TransportBuilder() = 0; + virtual std::optional endpoint() const = 0; ChannelTransportProperties const& transport_properties() const; ChannelStatistics& channel_statistics(); diff --git a/aether/channels/ethernet_channel.h b/aether/channels/ethernet_channel.h index e2e02d85..2ca0841a 100644 --- a/aether/channels/ethernet_channel.h +++ b/aether/channels/ethernet_channel.h @@ -41,6 +41,8 @@ class EthernetChannel : public Channel { AE_OBJECT_REFLECT(AE_MMBRS(aether_, poller_, dns_resolver_, address)) + std::optional endpoint() const override { return address; } + TransportBuildSender TransportBuilder() override; Endpoint address; diff --git a/aether/channels/lora_module_channel.h b/aether/channels/lora_module_channel.h index e8be61b7..1e91f89f 100644 --- a/aether/channels/lora_module_channel.h +++ b/aether/channels/lora_module_channel.h @@ -37,6 +37,8 @@ class LoraModuleChannel final : public Channel { AE_OBJECT_REFLECT(AE_MMBRS(access_point_)) + std::optional endpoint() const override { return std::nullopt; } + ActionPtr TransportBuilder() override; Duration TransportBuildTimeout() const override; diff --git a/aether/channels/modem_channel.h b/aether/channels/modem_channel.h index 343e6953..50741757 100644 --- a/aether/channels/modem_channel.h +++ b/aether/channels/modem_channel.h @@ -37,6 +37,8 @@ class ModemChannel final : public Channel { AE_OBJECT_REFLECT(AE_MMBRS(access_point_, address)) + std::optional endpoint() const override { return address; } + TransportBuildSender TransportBuilder() override; Duration TransportBuildTimeout() const override; diff --git a/aether/channels/wifi_channel.h b/aether/channels/wifi_channel.h index ac732a73..fc34c54a 100644 --- a/aether/channels/wifi_channel.h +++ b/aether/channels/wifi_channel.h @@ -42,6 +42,8 @@ class WifiChannel final : public Channel { AE_OBJECT_REFLECT(AE_MMBRS(aether_, poller_, resolver_, access_point_, address)) + std::optional endpoint() const override { return address; } + Duration TransportBuildTimeout() const override; TransportBuildSender TransportBuilder() override; diff --git a/aether/client_messages/p2p_message_stream.cpp b/aether/client_messages/p2p_message_stream.cpp index a1e9905b..12dcef53 100644 --- a/aether/client_messages/p2p_message_stream.cpp +++ b/aether/client_messages/p2p_message_stream.cpp @@ -24,8 +24,8 @@ #include "aether/cloud.h" #include "aether/cloud_connections/cloud_request.h" -#include "aether/cloud_connections/cloud_subscription.h" #include "aether/cloud_connections/cloud_server_connection.h" +#include "aether/cloud_connections/cloud_subscription.h" #include "aether/client_messages/client_messages_tele.h" @@ -50,28 +50,6 @@ class MessageSendStream final : public IStream { request_policy_); } - std::optional - ExportPreparedSendMessageBlock(Uid target_uid, - std::uint32_t reserve_nonce_count) { - for (auto* sc : cloud_connection_->servers()) { - if (sc == nullptr) { - continue; - } - - auto* conn = sc->client_connection(); - if (conn == nullptr) { - continue; - } - - auto block = - conn->ExportPreparedSendMessageBlock(target_uid, reserve_nonce_count); - if (block) { - return block; - } - } - - return std::nullopt; - } StreamInfo stream_info() const override { return stream_info_; } OutDataEvent::Subscriber out_data_event() override { return out_data_event_; } StreamUpdateEvent::Subscriber stream_update_event() override { @@ -227,16 +205,6 @@ void P2pStream::WriteOut(DataBuffer const& data) { Uid const& P2pStream::destination() const { return destination_; } -std::optional -P2pStream::ExportPreparedSendMessageBlock(std::uint32_t reserve_nonce_count) { - if (!message_send_stream_) { - return std::nullopt; - } - - return message_send_stream_->ExportPreparedSendMessageBlock( - destination_, reserve_nonce_count); -} - void P2pStream::ConnectReceive() { out_data_sub_ = handle_.out_data_event().Subscribe(MethodPtr<&P2pStream::WriteOut>{this}); diff --git a/aether/client_messages/p2p_message_stream.h b/aether/client_messages/p2p_message_stream.h index 78590dad..f52f78f6 100644 --- a/aether/client_messages/p2p_message_stream.h +++ b/aether/client_messages/p2p_message_stream.h @@ -59,9 +59,6 @@ class P2pStream final : public ByteIStream { void WriteOut(DataBuffer const& data); Uid const& destination() const; - std::optional - ExportPreparedSendMessageBlock(std::uint32_t reserve_nonce_count); - private: void ConnectReceive(); void ConnectSend(); diff --git a/aether/connection_manager/client_cloud_manager.cpp b/aether/connection_manager/client_cloud_manager.cpp index 5de4863e..f9df3dd5 100644 --- a/aether/connection_manager/client_cloud_manager.cpp +++ b/aether/connection_manager/client_cloud_manager.cpp @@ -199,6 +199,14 @@ GetCloudAction& ClientCloudManager::GetCloud(Uid client_uid) { return *action; } +Cloud::ptr ClientCloudManager::GetCachedCloud(Uid client_uid) { + auto cached = cloud_cache_.find(client_uid); + if ((cached == cloud_cache_.end()) || !cached->second.cloud.is_valid()) { + return {}; + } + return cached->second.cloud; +} + void ClientCloudManager::StartListenForCloudUpdate() { auto aether = aether_.Load(); assert(aether && "Aether must be loaded"); diff --git a/aether/connection_manager/client_cloud_manager.h b/aether/connection_manager/client_cloud_manager.h index f7b86293..782aff13 100644 --- a/aether/connection_manager/client_cloud_manager.h +++ b/aether/connection_manager/client_cloud_manager.h @@ -89,8 +89,17 @@ class ClientCloudManager : public Obj { CloudUpdateEvent::Subscriber cloud_update_event(); + /** + * \brief Make request for cloud for client_uid. + * New request to client cloud is performed or cloud returned from cache. + */ GetCloudAction& GetCloud(Uid client_uid); + /** + * \brief Returns cloud from cache for client_uid or empty Cloud::ptr. + */ + Cloud::ptr GetCachedCloud(Uid client_uid); + AE_OBJECT_REFLECT(AE_MMBRS(aether_, client_, cloud_cache_)) void StartListenForCloudUpdate(); diff --git a/aether/prepared_packet/packet_encoder.cpp b/aether/prepared_packet/packet_encoder.cpp index 0c87e7bb..e8374e54 100644 --- a/aether/prepared_packet/packet_encoder.cpp +++ b/aether/prepared_packet/packet_encoder.cpp @@ -18,38 +18,39 @@ namespace { class PreparedSendMessageKeyProvider final : public ISyncKeyProvider { public: - explicit PreparedSendMessageKeyProvider(PreparedSendMessageBlock& block) + explicit PreparedSendMessageKeyProvider(PreparedSendMessage& block) : block_{&block} {} - Key GetKey() const override { - return block_->client_to_server_key; - } + Key GetKey() const override { return block_->client_to_server_key; } - CryptoNonce const& Nonce() const override { - return block_->next_nonce; - } + CryptoNonce const& Nonce() const override { return block_->next_nonce; } private: - PreparedSendMessageBlock* block_; + PreparedSendMessage* block_; }; } // namespace -EncodePacketResult EncodePacket(PreparedSendMessageBlock& block, - DataBuffer const& payload, - DataBuffer& out) { - if (block.nonce_left == 0) { - out.clear(); - return EncodePacketResult{EncodePacketError::kNonceExhausted, 0}; +Result EncodePacket( + PreparedSendMessageBlock& prepared_block, DataBuffer const& payload, + DataBuffer& out) { + if (!prepared_block.is_valid()) { + return Error{block_is_invalid}; + } + + auto send_message = prepared_block.Resolve(); + + if (send_message->message_left == 0) { + return Error{messages_exhausted}; } // Match the existing ClientKeyProvider semantics: // consume next nonce before encryption. - block.next_nonce.Next(); - --block.nonce_left; + send_message->next_nonce.Next(); + --send_message->message_left; auto key_provider = - std::make_unique(block); + std::make_unique(*send_message); SyncEncryptProvider encrypt_provider{std::move(key_provider)}; ProtocolContext protocol_context; @@ -58,16 +59,17 @@ EncodePacketResult EncodePacket(PreparedSendMessageBlock& block, auto api_context = ApiContext{login_api}; api_context->login_by_alias( - block.sender_ephemeral_uid, + send_message->sender_ephemeral, SubApi{ - [&block, &payload](ApiContext& auth_api) { + [&](auto& auth_api) { auth_api->send_message( - AeMessage{block.target_uid, DataBuffer{payload}}); - }}); + AeMessage{send_message->destination_uid, DataBuffer{payload}}); + }, + }); out = std::move(api_context).Pack(); - return EncodePacketResult{EncodePacketError::kNone, out.size()}; + return Ok{out.size()}; } } // namespace ae::prepared_packet diff --git a/aether/prepared_packet/packet_encoder.h b/aether/prepared_packet/packet_encoder.h index 6f9d7371..5372e064 100644 --- a/aether/prepared_packet/packet_encoder.h +++ b/aether/prepared_packet/packet_encoder.h @@ -8,13 +8,38 @@ #ifndef AETHER_PREPARED_PACKET_PACKET_ENCODER_H_ #define AETHER_PREPARED_PACKET_PACKET_ENCODER_H_ +#include + +#include "aether-miscpp/types/result.h" + +// IWYU pragma: begin_exports #include "aether/prepared_packet/prepared_send_message.h" +#include "aether/types/data_buffer.h" +// IWYU pragma: end_exports namespace ae::prepared_packet { -EncodePacketResult EncodePacket(PreparedSendMessageBlock& block, - DataBuffer const& payload, - DataBuffer& out); +struct EncodePacketError { + int ec; + std::string_view msg; +}; + +static constexpr inline auto ok = EncodePacketError{0, "Ok!"}; +static constexpr inline auto block_is_invalid = + EncodePacketError{1, "PreparedSendMessageBlock is invalid"}; +static constexpr inline auto messages_exhausted = + EncodePacketError{2, "Reserved message count exhausted"}; + +/** + * \brief Encode send_message packet for PreparedSendMessageBlock + * \param block - prepared send message block; block must be valid. + * \param payload - message payload. + * \param out - output buffer where result is stored. + * \return Result with either out size or error. + */ +Result EncodePacket( + PreparedSendMessageBlock& block, DataBuffer const& payload, + DataBuffer& out); } // namespace ae::prepared_packet diff --git a/aether/prepared_packet/prepare_send_message.cpp b/aether/prepared_packet/prepare_send_message.cpp deleted file mode 100644 index 45229f55..00000000 --- a/aether/prepared_packet/prepare_send_message.cpp +++ /dev/null @@ -1,12 +0,0 @@ -#include "aether/prepared_packet/prepare_send_message.h" - -#include "aether/client_messages/p2p_message_stream.h" - -namespace ae::prepared_packet { - -std::optional PrepareSendMessage( - P2pStream& stream, std::uint32_t reserve_nonce_count) { - return stream.ExportPreparedSendMessageBlock(reserve_nonce_count); -} - -} // namespace ae::prepared_packet diff --git a/aether/prepared_packet/prepare_send_message.h b/aether/prepared_packet/prepare_send_message.h deleted file mode 100644 index 52f1da23..00000000 --- a/aether/prepared_packet/prepare_send_message.h +++ /dev/null @@ -1,21 +0,0 @@ -#ifndef AETHER_PREPARED_PACKET_PREPARE_SEND_MESSAGE_H_ -#define AETHER_PREPARED_PACKET_PREPARE_SEND_MESSAGE_H_ - -#include -#include - -#include "aether/prepared_packet/prepared_send_message.h" - -namespace ae { - -class P2pStream; - -namespace prepared_packet { - -std::optional PrepareSendMessage( - P2pStream& stream, std::uint32_t reserve_nonce_count); - -} // namespace prepared_packet -} // namespace ae - -#endif diff --git a/aether/prepared_packet/prepared_block.h b/aether/prepared_packet/prepared_block.h new file mode 100644 index 00000000..5573a568 --- /dev/null +++ b/aether/prepared_packet/prepared_block.h @@ -0,0 +1,145 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AETHER_PREPARED_PACKET_PREPARED_BLOCK_H_ +#define AETHER_PREPARED_PACKET_PREPARED_BLOCK_H_ + +#include +#include +#include +#include +#include + +#include "aether-miscpp/serialization/binary_archive.h" +#include "aether/types/packed_size.h" + +namespace ae::prepared_packet { +// magic value to indicate block is valid and contain no garbage +static constexpr std::uint32_t kMagic = 0x50534456; // "PSDV" + +// Raw block with raw_data is needed because we could store only trivial types +// as RTC_DATA +template +struct RawBlock { + std::uint32_t magic; + std::array raw_data; +}; + +template +struct PreparedBlock { + /** + * \brief RAII access to T value. + * after usage updated value will be retained in the prepared block + */ + struct PreparedProxy { + PreparedProxy(PreparedProxy&&) noexcept = delete; + PreparedProxy(PreparedProxy const&) noexcept = delete; + PreparedProxy& operator=(PreparedProxy&&) noexcept = delete; + PreparedProxy& operator=(PreparedProxy const&) noexcept = delete; + + constexpr explicit PreparedProxy(T&& v, PreparedBlock& h) noexcept( + std::is_nothrow_move_constructible_v) + : value{std::move(v)}, host{&h} {} + + constexpr ~PreparedProxy() noexcept(std::is_nothrow_destructible_v) { + host->Retain(std::move(value)); + } + + constexpr T& operator*() noexcept { return value; } + constexpr T const& operator*() const noexcept { return value; } + constexpr T* operator->() noexcept { return &value; } + constexpr T const* operator->() const noexcept { return &value; } + constexpr T* operator&() noexcept { return &value; } + constexpr T const* operator&() const noexcept { return &value; } + constexpr explicit operator T() noexcept { return value; } + constexpr explicit operator T() const noexcept { return value; } + + T value; + PreparedBlock* host; + }; + + PreparedProxy Resolve(); + void Retain(T&& value); + + constexpr bool is_valid() const { return raw.magic == kMagic; } + + RawBlock raw; +}; + +namespace prepared_block_internal { +struct SpanBuffer { + seri::SeriResult Write(seri::SizeWriteTag size) { + // write into temp buffer first, then actually write to the main buff + auto psize = PackedSize{size.size}; + auto buff = std::array{}; + auto seri_size = ae::Serialize(psize, buff.data()); + return Write(seri::DataWriteTag{buff.data(), seri_size}); + } + seri::SeriResult Write(seri::DataWriteTag data) { + if ((pos + data.size) > buffer.size()) { + return Error{seri::write_eof}; + } + std::memcpy(buffer.data() + pos, data.data, data.size); + pos += data.size; + return Ok{seri::good}; + } + + seri::SeriResult Read(seri::SizeReadTag size) { + auto res = + ae::Deserialize(buffer.data() + pos, buffer.size() - pos); + if (res.bytes_read == 0) { + return Error{seri::read_error}; + } + pos += res.bytes_read; + size.size = static_cast(res.value); + return Ok{seri::good}; + } + seri::SeriResult Read(seri::DataReadTag data) { + if ((pos + data.size) > buffer.size()) { + return Error{seri::read_eof}; + } + std::memcpy(data.data, buffer.data() + pos, data.size); + pos += data.size; + return Ok{seri::good}; + } + + std::span buffer; + std::size_t pos; +}; +} // namespace prepared_block_internal + +template +PreparedBlock::PreparedProxy PreparedBlock::Resolve() { + auto archive = seri::BinaryArchive{ + prepared_block_internal::SpanBuffer{.buffer = raw.raw_data, .pos = {}}}; + T v{}; + archive.Load(v); + return PreparedProxy{std::move(v), *this}; +} + +template +void PreparedBlock::Retain(T&& value) { + auto archive = seri::BinaryArchive{ + prepared_block_internal::SpanBuffer{.buffer = raw.raw_data, .pos = {}}}; + auto&& v = std::move(value); + [[maybe_unused]] auto res = archive.Save(v); + assert(!!res && "Object should be saved in archive"); + raw.magic = kMagic; +} + +} // namespace ae::prepared_packet + +#endif // AETHER_PREPARED_PACKET_PREPARED_BLOCK_H_ diff --git a/aether/prepared_packet/prepared_send_message.cpp b/aether/prepared_packet/prepared_send_message.cpp index db965ee6..6b184d31 100644 --- a/aether/prepared_packet/prepared_send_message.cpp +++ b/aether/prepared_packet/prepared_send_message.cpp @@ -1,41 +1,210 @@ #include "aether/prepared_packet/prepared_send_message.h" -#include +#include +#include +#include +#include + +#include "aether-miscpp/misc/override.h" + +#include "aether/channels/channel.h" +#include "aether/client.h" +#include "aether/cloud.h" +#include "aether/connection_manager/client_cloud_manager.h" +#include "aether/server.h" namespace ae::prepared_packet { +namespace prepare_send_message_internal { +bool FilterChannel(Channel::ptr const& c) { + if (!c.is_valid()) { + return false; + } + auto c_ptr = c.Load(); + if (!c_ptr) { + return false; + } + auto e = c_ptr->endpoint(); + if (!e) { + return false; + } + // filter out named addresses + if ((e->address.Index() != AddrVersion::kIpV4) && + (e->address.Index() != AddrVersion::kIpV6)) { + return false; + } + // filter out non Udp protocol + if (e->protocol != Protocol::kUdp) { + return false; + } + return true; +} + +bool CompareChannels(Channel::ptr const& left, Channel::ptr const& right) { + assert(left.is_valid() && right.is_valid() && "Channels must be valid"); + + // select only loadable channels with endpoint + auto left_ptr = left.Load(); + if (!left_ptr || !left_ptr->endpoint()) { + return false; + } + auto right_ptr = right.Load(); + if (!right_ptr || !right_ptr->endpoint()) { + return true; + } + + auto l_conn_type = left_ptr->transport_properties().connection_type; + auto r_conn_type = right_ptr->transport_properties().connection_type; + // select the fastest connection type + if (l_conn_type != r_conn_type) { + return l_conn_type > r_conn_type; + } + // select the lower connection time + auto l_build_time = left_ptr->TransportBuildTimeout(); + auto r_build_time = right_ptr->TransportBuildTimeout(); + if (l_build_time != r_build_time) { + return l_build_time < r_build_time; + } + // select the lower ping time + return left_ptr->ResponseTimeout() < right_ptr->ResponseTimeout(); +} -std::optional MakePreparedEndpoint(Endpoint const& endpoint) { - PreparedEndpoint candidate; - candidate.protocol = endpoint.protocol; - candidate.port = endpoint.port; - - bool is_ip = false; - - std::visit( - [&](auto const& addr) { - using T = std::decay_t; - - if constexpr (std::is_same_v) { - candidate.version = PreparedIpVersion::kIpV4; - for (std::size_t i = 0; i < 4; ++i) { - candidate.ip[i] = addr.ipv4_value[i]; - } - is_ip = true; - } else if constexpr (std::is_same_v) { - candidate.version = PreparedIpVersion::kIpV6; - for (std::size_t i = 0; i < 16; ++i) { - candidate.ip[i] = addr.ipv6_value[i]; - } - is_ip = true; - } - }, +// Converts IP endpoints to the transport-neutral prepared representation. +// Named endpoints require DNS and therefore cannot be prepared. +auto MakePreparedEndpoint(Endpoint const& endpoint) + -> std::optional { + auto addr = std::visit( + Override{ + [](IpV4Addr const& ipv4) noexcept -> std::optional { + return PreparedAddr{ipv4}; + }, + [](IpV6Addr const& ipv6) noexcept -> std::optional { + return PreparedAddr{ipv6}; + }, + [](NamedAddr const&) noexcept -> std::optional { + return std::nullopt; + }}, endpoint.address); - if (!is_ip) { + if (!addr) { return std::nullopt; } - return candidate; + return PreparedEndpoint{.address = addr.value(), + .port = endpoint.port, + .protocol = endpoint.protocol}; } +auto SelectChannel(std::ranges::range auto const& channels) + -> std::optional { + for (auto const& ch : channels) { + auto ch_ptr = ch.Load(); + if (!ch_ptr || !ch_ptr->endpoint()) { + continue; + } + return ch_ptr->endpoint(); + } + return std::nullopt; +} + +struct SelectedServer { + ServerId sid; + Endpoint endpoint; +}; + +auto SelectServer(std::vector servers) + -> std::optional { + std::ranges::sort(servers, [](auto const& left, auto const& right) { + return left.priority < right.priority; + }); + + for (auto const& cloud_server : servers) { + auto s_ptr = cloud_server.server.Load(); + if (!s_ptr) { + continue; + } + auto ch_filtered = + std::ranges::views::filter(s_ptr->channels, FilterChannel); + auto ch_sorted = + std::vector(std::begin(ch_filtered), std::end(ch_filtered)); + std::ranges::sort(ch_sorted, CompareChannels); + if (ch_sorted.empty()) { + continue; + } + auto endpoint = SelectChannel(ch_sorted); + if (!endpoint) { + continue; + } + return SelectedServer{ + .sid = s_ptr->server_id, + .endpoint = *endpoint, + }; + } + + return std::nullopt; +} + +} // namespace prepare_send_message_internal + +Result PrepareSendMessageBlock( + ObjPtr const& client, Uid destination_uid, + std::uint32_t message_count) { + auto client_ptr = client.Load(); + if (!client_ptr) { + return Error{client_is_not_valid}; + } + + auto dest_cloud = + client_ptr->cloud_manager()->GetCachedCloud(destination_uid); + auto dest_cloud_ptr = dest_cloud.Load(); + if (!dest_cloud_ptr) { + return Error{dest_cloud_is_not_in_cache}; + } + + auto servers = std::vector{}; + servers.reserve(dest_cloud_ptr->servers().size()); + for (auto const& [_, server] : dest_cloud_ptr->servers()) { + servers.emplace_back(server); + } + auto selected_server = + prepare_send_message_internal::SelectServer(std::move(servers)); + + if (!selected_server) { + return Error{unable_to_get_server}; + } + + // after filter and sorting channel must have endpoint + assert(selected_server->endpoint.protocol == Protocol::kUdp); + + auto prep_endpoint = prepare_send_message_internal::MakePreparedEndpoint( + selected_server->endpoint); + if (!prep_endpoint) { + return Error{unable_to_get_endpoint}; + } + + // get crypto keys + auto* server_state = client_ptr->server_state(selected_server->sid); + if (server_state == nullptr) { + return Error{unable_to_get_server_state}; + } + + auto key = server_state->client_to_server(); + auto nonce = server_state->nonce(); + // reserver nonces for message count + for (std::uint32_t i = 0; i < message_count; ++i) { + server_state->Next(); + } + + PreparedSendMessageBlock block; + block.Retain(PreparedSendMessage{ + client_ptr->ephemeral_uid(), + destination_uid, + prep_endpoint.value(), + selected_server->sid, + key, + nonce, + message_count, + }); + + return Ok{block}; +} } // namespace ae::prepared_packet diff --git a/aether/prepared_packet/prepared_send_message.h b/aether/prepared_packet/prepared_send_message.h index fff70874..922d0ca9 100644 --- a/aether/prepared_packet/prepared_send_message.h +++ b/aether/prepared_packet/prepared_send_message.h @@ -8,86 +8,96 @@ #ifndef AETHER_PREPARED_PACKET_PREPARED_SEND_MESSAGE_H_ #define AETHER_PREPARED_PACKET_PREPARED_SEND_MESSAGE_H_ -#include -#include -#include -#include +#include +#include "aether-miscpp/reflect/reflect.h" +#include "aether-miscpp/types/result.h" + +#include "aether/crypto/crypto_nonce.h" +#include "aether/crypto/key.h" +#include "aether/obj/obj_ptr.h" #include "aether/types/address.h" -#include "aether/types/data_buffer.h" +#include "aether/types/server_id.h" #include "aether/types/uid.h" +#include "aether/types/variant_type.h" -#include "aether/crypto/key.h" -#include "aether/crypto/crypto_nonce.h" +#include "aether/prepared_packet/prepared_block.h" + +namespace ae { +class Client; +} namespace ae::prepared_packet { -static constexpr std::uint32_t kMagic = 0x50534456; // "PSDV" -static constexpr std::uint32_t kVersion = 1; -// Serialized PreparedSendMessageBlock is a few hundred bytes. Keep the RTC -// footprint small enough for ESP32 RTC slow memory (8 KiB on ESP32-C6). -static constexpr std::size_t kMaxPreparedBlockBytes = 512; - -struct RetainedPreparedBlock { - std::uint32_t magic; - std::uint32_t version; - std::uint32_t size; - std::uint32_t checksum; - std::array bytes; -}; -enum class PreparedIpVersion : std::uint8_t { - kIpV4 = 4, - kIpV6 = 6, +struct PreparedAddr + : VariantType, + VPair> { + using VariantType::VariantType; + using VariantType::operator=; }; struct PreparedEndpoint { - AE_REFLECT_MEMBERS(version, protocol, port, ip) - PreparedIpVersion version = PreparedIpVersion::kIpV4; - Protocol protocol = Protocol::kUdp; - std::uint16_t port = 0; - - // IPv4 uses first 4 bytes. - // IPv6 uses all 16 bytes. - std::array ip{}; + AE_REFLECT_MEMBERS(address, port, protocol) + PreparedAddr address; + std::uint16_t port; + Protocol protocol; }; -struct PreparedSendMessageBlock { - AE_REFLECT_MEMBERS(endpoint, sender_ephemeral_uid, target_uid, - client_to_server_key, next_nonce, nonce_left) - PreparedEndpoint endpoint; +struct PreparedSendMessage { + AE_REFLECT_MEMBERS(sender_ephemeral, destination_uid, endpoint, server_id, + client_to_server_key, next_nonce, message_left) + // Client ephemeral UID sent to login_by_alias. + Uid sender_ephemeral; + Uid destination_uid; - Uid sender_ephemeral_uid; - Uid target_uid; + PreparedEndpoint endpoint; + ServerId server_id; Key client_to_server_key; CryptoNonce next_nonce; - std::uint32_t nonce_left = 0; -}; -enum class EncodePacketError { - kNone = 0, - kNonceExhausted, + std::uint32_t message_left; }; -inline char const* ToString(EncodePacketError error) { - switch (error) { - case EncodePacketError::kNone: - return "none"; - case EncodePacketError::kNonceExhausted: - return "nonce_exhausted"; - } - return "unknown"; -} - -struct EncodePacketResult { - EncodePacketError error = EncodePacketError::kNone; - std::size_t bytes_written = 0; +using PreparedSendMessageBlock = PreparedBlock; - explicit operator bool() const { return error == EncodePacketError::kNone; } +struct PreparedBlockError { + int ec; + std::string_view msg; }; -std::optional MakePreparedEndpoint(Endpoint const& endpoint); +static constexpr inline auto client_is_not_valid = + PreparedBlockError{1, "Client is not valid"}; +static constexpr inline auto dest_cloud_is_not_in_cache = + PreparedBlockError{2, "Dest cloud is not cached"}; +static constexpr inline auto unable_to_get_server = + PreparedBlockError{3, "Unable to select a usable server"}; +static constexpr inline auto unable_to_get_endpoint = + PreparedBlockError{4, "Unable to get destination endpoint"}; +static constexpr inline auto unable_to_get_server_state = + PreparedBlockError{5, "Unable to get client server state"}; + +/** + * \brief Make prepared send message block. + * Reserves message_count nonces for sending through a user-provided fast path. + * The destination cloud must already be cached by the client. This function + * does not retrieve a destination cloud when it is absent from the cache. + * The selected server is the usable server with the lowest numeric priority; + * higher-priority servers without a loadable IPv4 or IPv6 UDP endpoint are + * skipped. Selection does not retry after a server and endpoint are selected. + * Use EncodePacket to build the Aether packet; it does not send the packet. + * After the block is ready, do not send regular Aether messages because they + * invalidate the prepared block. + * \param client - Client object to send messages from + * \param destination_uid - Client's uid to send messages to + * \param message_count - Reserved message count. Messages must be reserved in + * aether's crypto layer to prevent nonce collisions. + * \return Result with either PreparedSendMessageBlock or PreparedBlockError. + */ +Result PrepareSendMessageBlock( + ObjPtr const& client, Uid destination_uid, + std::uint32_t message_count); } // namespace ae::prepared_packet diff --git a/aether/server_connections/client_server_connection.cpp b/aether/server_connections/client_server_connection.cpp index d3cfa3ac..ddff0bc2 100644 --- a/aether/server_connections/client_server_connection.cpp +++ b/aether/server_connections/client_server_connection.cpp @@ -1,6 +1,3 @@ -#include -#include -#include /* * Copyright 2024 Aethernet Inc. * @@ -202,59 +199,6 @@ ServerConnection& ClientServerConnection::server_connection() { return server_connection_.server_connection; } -std::optional -ClientServerConnection::ExportPreparedSendMessageBlock( - Uid target_uid, std::uint32_t reserve_nonce_count) { - auto client = client_.Lock(); - auto server = server_.Lock(); - - if (!client || !server || (reserve_nonce_count == 0)) { - return std::nullopt; - } - - std::optional prepared_endpoint; - - for (auto const& e : server->endpoints) { - if (e.protocol != Protocol::kUdp) { - continue; - } - - auto endpoint = prepared_packet::MakePreparedEndpoint(e); - if (endpoint) { - prepared_endpoint = *endpoint; - break; - } - } - - if (!prepared_endpoint) { - return std::nullopt; - } - - auto* server_key = client->server_state(server->server_id); - if (server_key == nullptr) { - return std::nullopt; - } - - prepared_packet::PreparedSendMessageBlock block; - block.endpoint = *prepared_endpoint; - block.sender_ephemeral_uid = ephemeral_uid_; - block.target_uid = target_uid; - block.client_to_server_key = server_key->client_to_server(); - - // Store current nonce. EncodePacket() will call Next() before encryption, - // matching existing ClientKeyProvider behavior. - block.next_nonce = server_key->nonce(); - block.nonce_left = reserve_nonce_count; - - // Burn/reserve the same nonce range in the full client, - // so the normal Aether path cannot reuse it later. - for (std::uint32_t i = 0; i < reserve_nonce_count; ++i) { - server_key->Next(); - } - - return block; -} - void ClientServerConnection::OutData(DataBuffer const& data) { auto parser = ApiParser{protocol_context_, data}; parser.Parse(client_api_unsafe_); diff --git a/aether/server_connections/client_server_connection.h b/aether/server_connections/client_server_connection.h index ff0f6878..9d12ac87 100644 --- a/aether/server_connections/client_server_connection.h +++ b/aether/server_connections/client_server_connection.h @@ -17,9 +17,6 @@ #ifndef AETHER_SERVER_CONNECTIONS_CLIENT_SERVER_CONNECTION_H_ #define AETHER_SERVER_CONNECTIONS_CLIENT_SERVER_CONNECTION_H_ -#include -#include - #include "aether/ae_context.h" #include "aether/common.h" #include "aether/crypto/icrypto_provider.h" @@ -31,7 +28,6 @@ #include "aether/work_cloud_api/work_server_api/login_api.h" #include "aether/server_connections/server_connection.h" -#include "aether/prepared_packet/prepared_send_message.h" namespace ae { class Client; @@ -78,10 +74,6 @@ class ClientServerConnection { ServerConnection& server_connection(); - std::optional - ExportPreparedSendMessageBlock(Uid target_uid, - std::uint32_t reserve_nonce_count); - private: void OutData(DataBuffer const& data); diff --git a/aether/stream_api/api_call_adapter.h b/aether/stream_api/api_call_adapter.h index f89657af..382f7ec1 100644 --- a/aether/stream_api/api_call_adapter.h +++ b/aether/stream_api/api_call_adapter.h @@ -18,108 +18,15 @@ #define AETHER_STREAM_API_API_CALL_ADAPTER_H_ #include -#include -#include -#include -#include #include #include "aether/api_protocol/api_context.h" -#include "aether/prepared_packet/packet_encoder.h" #include "aether/stream_api/istream.h" #include "aether/types/data_buffer.h" #include "aether/tele.h" namespace ae { - -namespace fast_tx_internal { - -enum class EncodePacketError { - kNone = 0, - kNonceExhausted, - kOutputBufferTooSmall, - kPayloadTooLarge, - kEncodeFailed, -}; - -inline char const* ToString(EncodePacketError error) { - switch (error) { - case EncodePacketError::kNone: - return "none"; - case EncodePacketError::kNonceExhausted: - return "nonce_exhausted"; - case EncodePacketError::kOutputBufferTooSmall: - return "output_buffer_too_small"; - case EncodePacketError::kPayloadTooLarge: - return "payload_too_large"; - case EncodePacketError::kEncodeFailed: - return "encode_failed"; - } - return "unknown"; -} - -struct EncodePacketReport { - EncodePacketError error = EncodePacketError::kNone; - std::size_t bytes_written = 0; - std::uint64_t nonce_used = 0; - - explicit operator bool() const { return error == EncodePacketError::kNone; } -}; - -// Endpoint intentionally stays outside. -// External code will own ip:port and raw UDP socket. -// This struct must contain only data needed by Aether packet encoding. -struct PreparedPacketEncoder { - std::uint64_t next_nonce = 0; - std::uint64_t nonce_limit = (std::numeric_limits::max)(); - std::size_t max_packet_size = 1200; - - EncodePacketReport TakeNonce() { - if (next_nonce == nonce_limit) { - return EncodePacketReport{EncodePacketError::kNonceExhausted, 0, 0}; - } - - auto nonce = next_nonce; - ++next_nonce; - - return EncodePacketReport{EncodePacketError::kNone, 0, nonce}; - } -}; - -// Future external shape: -// -// EncodePacketResult EncodePacket(PreparedPacketEncoder& prepared, -// Span payload, -// DataBuffer& out); -// -// Current internal step keeps old Aether path alive by using ByteIStream as -// out. -struct EncodePacketResult { - EncodePacketReport report; - WriteAction* action = nullptr; - - explicit operator bool() const { - return static_cast(report) && (action != nullptr); - } -}; - -template -EncodePacketResult EncodePacket(PreparedPacketEncoder& prepared, - ApiContext&& data, ByteIStream& out) { - auto report = prepared.TakeNonce(); - if (!report) { - return EncodePacketResult{report, nullptr}; - } - - auto packet = std::move(data).Pack(); - auto& action = out.Write(std::move(packet)); - - return EncodePacketResult{report, &action}; -} - -} // namespace fast_tx_internal - /** * \brief Api method call adapter to automatically flush the packet after all * method calls. @@ -133,18 +40,7 @@ class ApiCallAdapter { AE_CLASS_MOVE_ONLY(ApiCallAdapter) WriteAction& Flush() { - fast_tx_internal::PreparedPacketEncoder prepared{}; - - auto result = fast_tx_internal::EncodePacket( - prepared, std::move(api_context_), *byte_stream_); - - if (!result) { - std::cerr << "FastTx EncodePacket failed: " - << fast_tx_internal::ToString(result.report.error) << "\n"; - assert(false); - } - - return *result.action; + return byte_stream_->Write(DataBuffer{std::move(api_context_)}); } ApiContext& operator->() { return api_context_; } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index f9449056..64f56845 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -34,6 +34,7 @@ generate_inline_tests(${CMAKE_CURRENT_LIST_DIR}/../aether aether) add_subdirectory(test-types) add_subdirectory(test-object-system) add_subdirectory(test-api-protocol) +add_subdirectory(test-prepared-packet) add_subdirectory(test-actions) add_subdirectory(test-events) add_subdirectory(test-transport) diff --git a/tests/test-prepared-packet/CMakeLists.txt b/tests/test-prepared-packet/CMakeLists.txt new file mode 100644 index 00000000..519b0735 --- /dev/null +++ b/tests/test-prepared-packet/CMakeLists.txt @@ -0,0 +1,35 @@ +# Copyright 2026 Aethernet Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +cmake_minimum_required( VERSION 3.16 ) + +list(APPEND test_srcs + main.cpp + test-prepared-packet.cpp + ${CMAKE_CURRENT_LIST_DIR}/../test-object-system/map_domain_storage.cpp +) + +if(NOT CM_PLATFORM) + + project(test-prepared-packet LANGUAGES CXX) + + add_executable(${PROJECT_NAME}) + target_sources(${PROJECT_NAME} PRIVATE ${test_srcs}) + target_include_directories(${PROJECT_NAME} PRIVATE ${ROOT_DIR}) + target_link_libraries(${PROJECT_NAME} PRIVATE aether unity gcem) + + add_test(NAME ${PROJECT_NAME} COMMAND $) +else() + message(WARNING "Not implemented for ${CM_PLATFORM}") +endif() diff --git a/tests/test-prepared-packet/main.cpp b/tests/test-prepared-packet/main.cpp new file mode 100644 index 00000000..e693ecf9 --- /dev/null +++ b/tests/test-prepared-packet/main.cpp @@ -0,0 +1,24 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +void setUp() {} +void tearDown() {} + +extern int test_prepared_packet(); + +int main() { return test_prepared_packet(); } diff --git a/tests/test-prepared-packet/test-prepared-packet.cpp b/tests/test-prepared-packet/test-prepared-packet.cpp new file mode 100644 index 00000000..819ac750 --- /dev/null +++ b/tests/test-prepared-packet/test-prepared-packet.cpp @@ -0,0 +1,581 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "aether/adapter_registry.h" +#include "aether/aether.h" +#include "aether/channels/channel.h" +#include "aether/client.h" +#include "aether/cloud.h" +#include "aether/config.h" +#include "aether/crypto/ikey_provider.h" +#include "aether/crypto/key_gen.h" +#include "aether/crypto/sync_crypto_provider.h" +#include "aether/obj/domain.h" +#include "aether/prepared_packet/packet_encoder.h" +#include "aether/prepared_packet/prepared_send_message.h" +#include "aether/server.h" +#include "aether/server_keys.h" + +#include "../test-api-protocol/assert_packet.h" +#include "../test-object-system/map_domain_storage.h" + +namespace ae::test_prepared_packet { + +class TestChannel final : public Channel { + AE_OBJECT(TestChannel, Channel, 0) + + protected: + TestChannel() = default; + + public: + TestChannel(ObjProp prop, Endpoint endpoint, ConnectionType connection_type, + Duration build_timeout, Duration response_timeout) + : Channel{prop}, + endpoint_{std::move(endpoint)}, + build_timeout_{build_timeout}, + response_timeout_{response_timeout} { + transport_properties_.connection_type = connection_type; + } + + AE_OBJECT_REFLECT() + + TransportBuildSender TransportBuilder() override { return ex::just_error(0); } + + std::optional endpoint() const override { return endpoint_; } + + Duration TransportBuildTimeout() const override { return build_timeout_; } + + Duration ResponseTimeout() const override { return response_timeout_; } + + private: + Endpoint endpoint_; + Duration build_timeout_; + Duration response_timeout_; +}; + +Endpoint UdpEndpoint(std::uint16_t port) { + return Endpoint{{IpV4Addr{{192, 0, 2, 1}}, port}, Protocol::kUdp}; +} + +struct PreparedPacketFixture { + static constexpr auto kServerId = ServerId{42}; + static constexpr auto kEndpointPort = std::uint16_t{4242}; + static constexpr auto kCandidateEndpointPort = std::uint16_t{1001}; + static constexpr auto kPreferredEndpointPort = std::uint16_t{1002}; + static constexpr auto kEncryptedPayloadMessageId = MessageId{6}; + static constexpr auto kDocumentationIpv6Address = + std::array{0x20, 0x01, 0x0d, 0xb8}; + static constexpr auto kUncachedDestinationUid = + Uid{std::array{4}}; + + PreparedPacketFixture() + : domain{Now(), storage}, + aether{Aether::ptr::Create(CreateWith{domain})}, + registry{AdapterRegistry::ptr::Create(CreateWith{domain})} { + aether->adapter_registry = registry; + aether->client_prefab = Client::ptr::Create(CreateWith{domain}, aether); + aether->client_prefab.Save(); + + auto master_key = Key{}; + TEST_ASSERT_TRUE(CryptoSyncKeygen(master_key)); + auto const config = ClientConfig{ + .parent_uid = Uid{{1}}, + .uid = Uid{{2}}, + .ephemeral_uid = Uid{{3}}, + .master_key = std::move(master_key), + .cloud = {{kServerId, {UdpEndpoint(kEndpointPort)}}}, + }; + client = aether->CreateClient(config, "prepared-packet-client"); + + auto server = aether->GetServer(kServerId).Load(); + TEST_ASSERT_NOT_NULL(server); + server->channels = { + TestChannel::ptr::Create(CreateWith{domain}, UdpEndpoint(kEndpointPort), + ConnectionType::kConnectionLess, + std::chrono::seconds{1}, + std::chrono::seconds{1}), + }; + } + + Server::ptr MakeServer(ServerId id, std::vector channels) { + auto server = Server::ptr::Create(CreateWith{domain}, id, + std::vector{}, registry); + server->channels = std::move(channels); + return server; + } + + TestChannel::ptr MakeChannel( + Endpoint endpoint, + ConnectionType connection_type = ConnectionType::kConnectionFull, + Duration build_timeout = std::chrono::seconds{1}, + Duration response_timeout = std::chrono::seconds{1}) { + return TestChannel::ptr::Create(CreateWith{domain}, std::move(endpoint), + connection_type, build_timeout, + response_timeout); + } + + void SetCloudServers(std::vector const& servers) { + client->cloud().Load()->SetServers(servers); + } + + MapDomainStorage storage; + Domain domain; + Aether::ptr aether; + AdapterRegistry::ptr registry; + Client::ptr client; +}; + +bool NoncesEqual(CryptoNonce const& left, CryptoNonce const& right) { +#if AE_CRYPTO_SYNC == AE_CHACHA20_POLY1305 + return left.value == right.value; +#elif AE_CRYPTO_SYNC == AE_HYDRO_CRYPTO_SK + return left.value == right.value; +#else + static_cast(left); + static_cast(right); + return true; +#endif +} + +#if AE_CRYPTO_SYNC == AE_CHACHA20_POLY1305 || \ + AE_CRYPTO_SYNC == AE_HYDRO_CRYPTO_SK +class FixedKeyProvider final : public ISyncKeyProvider { + public: + FixedKeyProvider(Key key, CryptoNonce const& nonce) + : key_{std::move(key)}, nonce_{&nonce} {} + + Key GetKey() const override { return key_; } + + CryptoNonce const& Nonce() const override { return *nonce_; } + + private: + Key key_; + CryptoNonce const* nonce_; +}; + +prepared_packet::PreparedSendMessageBlock PrepareConfiguredBlock( + PreparedPacketFixture& fixture) { + auto prepared = prepared_packet::PrepareSendMessageBlock( + fixture.client, fixture.client->uid(), 2); + + TEST_ASSERT_TRUE(prepared.IsOk()); + auto block = std::move(prepared).value(); + TEST_ASSERT_TRUE(block.is_valid()); + return block; +} + +void AssertPreparedBlock(prepared_packet::PreparedSendMessageBlock& block, + CryptoNonce const& initial_nonce) { + ServerId server_id; + std::uint16_t endpoint_port; + Protocol endpoint_protocol; + AddrVersion address_version; + CryptoNonce next_nonce; + { + auto send_message = block.Resolve(); + server_id = send_message->server_id; + endpoint_port = send_message->endpoint.port; + endpoint_protocol = send_message->endpoint.protocol; + address_version = send_message->endpoint.address.Index(); + next_nonce = send_message->next_nonce; + } + + TEST_ASSERT_EQUAL(PreparedPacketFixture::kServerId, server_id); + TEST_ASSERT_EQUAL(PreparedPacketFixture::kEndpointPort, endpoint_port); + TEST_ASSERT_EQUAL(Protocol::kUdp, endpoint_protocol); + TEST_ASSERT_EQUAL(AddrVersion::kIpV4, address_version); + TEST_ASSERT_TRUE(NoncesEqual(initial_nonce, next_nonce)); +} + +void AssertEncodedPacket(PreparedPacketFixture& fixture, + prepared_packet::PreparedSendMessageBlock& block, + CryptoNonce const& encoded_nonce, DataBuffer& packet, + DataBuffer const& payload) { + auto archive = seri::BinaryArchive{ + VectorBuffer{packet}, + }; + MessageId login_message{}; + Uid sender_ephemeral; + DataBuffer encrypted_message; + archive.Load(login_message); + archive.Load(sender_ephemeral); + archive.Load(encrypted_message); + TEST_ASSERT_EQUAL(5, login_message); + TEST_ASSERT_TRUE(sender_ephemeral == fixture.client->ephemeral_uid()); + + auto client_to_server_key = Key{}; + { + auto send_message = block.Resolve(); + client_to_server_key = send_message->client_to_server_key; + } + SyncDecryptProvider decrypt_provider{std::make_unique( + std::move(client_to_server_key), encoded_nonce)}; + auto const message = decrypt_provider.Decrypt(encrypted_message); + AssertPacket(message, PreparedPacketFixture::kEncryptedPayloadMessageId, + fixture.client->uid(), payload); +} +#endif + +void test_PublicHeaderExposesCorrectedNames() { + prepared_packet::PreparedBlockError const error{7, "error"}; + + TEST_ASSERT_EQUAL(7, error.ec); +} + +void test_PrepareSendMessageBlockInvalidClientUsesPreparedBlockError() { + auto result = + prepared_packet::PrepareSendMessageBlock(ObjPtr{}, Uid{}, 1); + + TEST_ASSERT_FALSE(result.IsOk()); + prepared_packet::PreparedBlockError const error = result.error(); + TEST_ASSERT_EQUAL(prepared_packet::client_is_not_valid.ec, error.ec); + TEST_ASSERT_EQUAL_STRING(prepared_packet::client_is_not_valid.msg.data(), + error.msg.data()); +} + +void test_UnableToGetEndpointErrorHasCorrectedText() { + TEST_ASSERT_EQUAL_STRING("Unable to get destination endpoint", + prepared_packet::unable_to_get_endpoint.msg.data()); +} + +void test_PrepareSendMessageBlockChoosesLowestPriorityUsableServer() { + PreparedPacketFixture f; + auto lower_priority = + f.MakeServer(ServerId{2}, {f.MakeChannel(UdpEndpoint(1002))}); + auto higher_priority = + f.MakeServer(ServerId{1}, {f.MakeChannel(UdpEndpoint(1001))}); + f.SetCloudServers({lower_priority, higher_priority}); + + auto result = + prepared_packet::PrepareSendMessageBlock(f.client, f.client->uid(), 1); + + TEST_ASSERT_TRUE(result.IsOk()); + auto block = std::move(result).value(); + ServerId server_id; + std::uint16_t endpoint_port; + { + auto prepared = block.Resolve(); + server_id = prepared->server_id; + endpoint_port = prepared->endpoint.port; + } + TEST_ASSERT_EQUAL(2, server_id); + TEST_ASSERT_EQUAL(1002, endpoint_port); +} + +void test_PrepareSendMessageBlockSkipsUnusableHigherPriorityServer() { + PreparedPacketFixture f; + auto unusable = f.MakeServer( + ServerId{1}, {f.MakeChannel(Endpoint{{NamedAddr{"server.example"}, 1001}, + Protocol::kUdp}), + f.MakeChannel(Endpoint{{IpV4Addr{{192, 0, 2, 1}}, 1002}, + Protocol::kTcp})}); + auto usable = f.MakeServer(ServerId{2}, {f.MakeChannel(UdpEndpoint(1003))}); + f.SetCloudServers({unusable, usable}); + + auto result = + prepared_packet::PrepareSendMessageBlock(f.client, f.client->uid(), 1); + + TEST_ASSERT_TRUE(result.IsOk()); + auto block = std::move(result).value(); + ServerId server_id; + std::uint16_t endpoint_port; + { + auto prepared = block.Resolve(); + server_id = prepared->server_id; + endpoint_port = prepared->endpoint.port; + } + TEST_ASSERT_EQUAL(2, server_id); + TEST_ASSERT_EQUAL(1003, endpoint_port); +} + +void test_PrepareSendMessageBlockRejectsNonUdpAndNamedEndpoints() { + PreparedPacketFixture f; + auto named = f.MakeServer( + ServerId{1}, {f.MakeChannel(Endpoint{{NamedAddr{"server.example"}, 1}, + Protocol::kUdp})}); + auto tcp = f.MakeServer( + ServerId{2}, + {f.MakeChannel(Endpoint{{IpV4Addr{{192, 0, 2, 1}}, 2}, Protocol::kTcp})}); + + f.SetCloudServers({named, tcp}); + + auto result = + prepared_packet::PrepareSendMessageBlock(f.client, f.client->uid(), 1); + + TEST_ASSERT_FALSE(result.IsOk()); + TEST_ASSERT_EQUAL(prepared_packet::unable_to_get_server.ec, + result.error().ec); +} + +void test_PrepareSendMessageBlockAcceptsIpv6UdpEndpoint() { + PreparedPacketFixture f; + auto server = f.MakeServer( + ServerId{1}, + {f.MakeChannel(Endpoint{ + {IpV6Addr{{PreparedPacketFixture::kDocumentationIpv6Address[0], + PreparedPacketFixture::kDocumentationIpv6Address[1], + PreparedPacketFixture::kDocumentationIpv6Address[2], + PreparedPacketFixture::kDocumentationIpv6Address[3], + PreparedPacketFixture::kDocumentationIpv6Address[4], + PreparedPacketFixture::kDocumentationIpv6Address[5], + PreparedPacketFixture::kDocumentationIpv6Address[6], + PreparedPacketFixture::kDocumentationIpv6Address[7], + PreparedPacketFixture::kDocumentationIpv6Address[8], + PreparedPacketFixture::kDocumentationIpv6Address[9], + PreparedPacketFixture::kDocumentationIpv6Address[10], + PreparedPacketFixture::kDocumentationIpv6Address[11], + PreparedPacketFixture::kDocumentationIpv6Address[12], + PreparedPacketFixture::kDocumentationIpv6Address[13], + PreparedPacketFixture::kDocumentationIpv6Address[14], + PreparedPacketFixture::kDocumentationIpv6Address[15]}}, + PreparedPacketFixture::kCandidateEndpointPort}, + Protocol::kUdp})}); + f.SetCloudServers({server}); + + auto result = + prepared_packet::PrepareSendMessageBlock(f.client, f.client->uid(), 1); + + TEST_ASSERT_TRUE(result.IsOk()); + auto block = std::move(result).value(); + AddrVersion address_version; + std::uint16_t endpoint_port; + { + auto prepared = block.Resolve(); + address_version = prepared->endpoint.address.Index(); + endpoint_port = prepared->endpoint.port; + } + TEST_ASSERT_EQUAL(AddrVersion::kIpV6, address_version); + TEST_ASSERT_EQUAL(PreparedPacketFixture::kCandidateEndpointPort, + endpoint_port); +} + +void test_PrepareSendMessageBlockRanksEligibleChannels() { + PreparedPacketFixture f; + auto server = f.MakeServer( + ServerId{1}, + {f.MakeChannel(UdpEndpoint(1001), ConnectionType::kConnectionFull, + std::chrono::seconds{1}, std::chrono::seconds{1}), + f.MakeChannel(UdpEndpoint(1002), ConnectionType::kConnectionLess, + std::chrono::seconds{2}, std::chrono::seconds{2})}); + + f.SetCloudServers({server}); + + auto result = + prepared_packet::PrepareSendMessageBlock(f.client, f.client->uid(), 1); + + TEST_ASSERT_TRUE(result.IsOk()); + auto block = std::move(result).value(); + std::uint16_t endpoint_port; + { + auto prepared = block.Resolve(); + endpoint_port = prepared->endpoint.port; + } + TEST_ASSERT_EQUAL(1002, endpoint_port); +} + +void test_PrepareSendMessageBlockRanksChannelsByBuildTimeout() { + PreparedPacketFixture f; + auto server = f.MakeServer( + ServerId{1}, + {f.MakeChannel(UdpEndpoint(PreparedPacketFixture::kCandidateEndpointPort), + ConnectionType::kConnectionLess, std::chrono::seconds{2}, + std::chrono::seconds{1}), + f.MakeChannel(UdpEndpoint(PreparedPacketFixture::kPreferredEndpointPort), + ConnectionType::kConnectionLess, std::chrono::seconds{1}, + std::chrono::seconds{2})}); + f.SetCloudServers({server}); + + auto result = + prepared_packet::PrepareSendMessageBlock(f.client, f.client->uid(), 1); + + TEST_ASSERT_TRUE(result.IsOk()); + auto block = std::move(result).value(); + std::uint16_t endpoint_port; + { + auto prepared = block.Resolve(); + endpoint_port = prepared->endpoint.port; + } + TEST_ASSERT_EQUAL(PreparedPacketFixture::kPreferredEndpointPort, + endpoint_port); +} + +void test_PrepareSendMessageBlockRanksChannelsByResponseTimeout() { + PreparedPacketFixture f; + auto server = f.MakeServer( + ServerId{1}, + {f.MakeChannel(UdpEndpoint(PreparedPacketFixture::kCandidateEndpointPort), + ConnectionType::kConnectionLess, std::chrono::seconds{1}, + std::chrono::seconds{2}), + f.MakeChannel(UdpEndpoint(PreparedPacketFixture::kPreferredEndpointPort), + ConnectionType::kConnectionLess, std::chrono::seconds{1}, + std::chrono::seconds{1})}); + f.SetCloudServers({server}); + + auto result = + prepared_packet::PrepareSendMessageBlock(f.client, f.client->uid(), 1); + + TEST_ASSERT_TRUE(result.IsOk()); + auto block = std::move(result).value(); + std::uint16_t endpoint_port; + { + auto prepared = block.Resolve(); + endpoint_port = prepared->endpoint.port; + } + TEST_ASSERT_EQUAL(PreparedPacketFixture::kPreferredEndpointPort, + endpoint_port); +} + +void test_PrepareSendMessageBlockUncachedDestinationReturnsError() { + PreparedPacketFixture f; + + auto result = prepared_packet::PrepareSendMessageBlock( + f.client, PreparedPacketFixture::kUncachedDestinationUid, 1); + + TEST_ASSERT_FALSE(result.IsOk()); + TEST_ASSERT_EQUAL(prepared_packet::dest_cloud_is_not_in_cache.ec, + result.error().ec); +} + +void test_EncodePacketReportsInvalidBlock() { + auto block = prepared_packet::PreparedSendMessageBlock{}; + auto packet = DataBuffer{}; + + auto result = prepared_packet::EncodePacket(block, DataBuffer{0x01}, packet); + + TEST_ASSERT_FALSE(result.IsOk()); + TEST_ASSERT_EQUAL(prepared_packet::block_is_invalid.ec, result.error().ec); +} + +void test_EncodePacketReportsExhaustedBlock() { + auto block = prepared_packet::PreparedSendMessageBlock{}; + auto send_message = prepared_packet::PreparedSendMessage{}; + send_message.message_left = 0; + block.Retain(std::move(send_message)); + TEST_ASSERT_TRUE(block.is_valid()); + auto packet = DataBuffer{}; + + auto result = prepared_packet::EncodePacket(block, DataBuffer{0x01}, packet); + + TEST_ASSERT_FALSE(result.IsOk()); + TEST_ASSERT_EQUAL(prepared_packet::messages_exhausted.ec, result.error().ec); +} + +#if AE_CRYPTO_SYNC == AE_CHACHA20_POLY1305 || \ + AE_CRYPTO_SYNC == AE_HYDRO_CRYPTO_SK +void test_PrepareSendMessageBlockForConfiguredClientEncodesPacket() { + PreparedPacketFixture f; + auto* const server_state = + f.client->server_state(PreparedPacketFixture::kServerId); + TEST_ASSERT_NOT_NULL(server_state); + auto const initial_nonce = server_state->nonce(); + auto expected_server_nonce = initial_nonce; + expected_server_nonce.Next(); + expected_server_nonce.Next(); + + auto block = PrepareConfiguredBlock(f); + AssertPreparedBlock(block, initial_nonce); + TEST_ASSERT_TRUE(NoncesEqual(expected_server_nonce, server_state->nonce())); + + auto payload = DataBuffer{0x01, 0x02, 0x03}; + auto packet = DataBuffer{}; + auto encoded_nonce = initial_nonce; + encoded_nonce.Next(); + auto result = prepared_packet::EncodePacket(block, payload, packet); + + TEST_ASSERT_TRUE(result.IsOk()); + std::uint16_t message_left; + CryptoNonce next_nonce; + { + auto send_message = block.Resolve(); + message_left = send_message->message_left; + next_nonce = send_message->next_nonce; + } + TEST_ASSERT_EQUAL(1, message_left); + TEST_ASSERT_TRUE(NoncesEqual(encoded_nonce, next_nonce)); + TEST_ASSERT_TRUE(NoncesEqual(expected_server_nonce, server_state->nonce())); + + AssertEncodedPacket(f, block, encoded_nonce, packet, payload); +} + +void test_EncodePacketUsesSenderEphemeralForLoginAlias() { + auto block = prepared_packet::PreparedSendMessageBlock{}; + auto send_message = prepared_packet::PreparedSendMessage{}; + send_message.sender_ephemeral = Uid{{1}}; + send_message.destination_uid = Uid{{2}}; + send_message.message_left = 1; + send_message.next_nonce.Init(); +# if AE_CRYPTO_SYNC == AE_CHACHA20_POLY1305 + send_message.client_to_server_key = Key{SodiumChacha20Poly1305Key{}}; +# elif AE_CRYPTO_SYNC == AE_HYDRO_CRYPTO_SK + send_message.client_to_server_key = Key{HydrogenSecretBoxKey{}}; +# endif + block.Retain(std::move(send_message)); + TEST_ASSERT_TRUE(block.is_valid()); + + auto packet = DataBuffer{}; + auto result = prepared_packet::EncodePacket(block, DataBuffer{0x01}, packet); + + TEST_ASSERT_TRUE(result.IsOk()); + AssertPacket(packet, MessageId{5}, Uid{{1}}, Skip{}); +} +#endif + +} // namespace ae::test_prepared_packet + +int test_prepared_packet() { + UNITY_BEGIN(); + RUN_TEST(ae::test_prepared_packet::test_PublicHeaderExposesCorrectedNames); + RUN_TEST(ae::test_prepared_packet:: + test_PrepareSendMessageBlockInvalidClientUsesPreparedBlockError); + RUN_TEST( + ae::test_prepared_packet::test_UnableToGetEndpointErrorHasCorrectedText); + RUN_TEST(ae::test_prepared_packet:: + test_PrepareSendMessageBlockChoosesLowestPriorityUsableServer); + RUN_TEST(ae::test_prepared_packet:: + test_PrepareSendMessageBlockSkipsUnusableHigherPriorityServer); + RUN_TEST(ae::test_prepared_packet:: + test_PrepareSendMessageBlockRejectsNonUdpAndNamedEndpoints); + RUN_TEST(ae::test_prepared_packet:: + test_PrepareSendMessageBlockAcceptsIpv6UdpEndpoint); + RUN_TEST(ae::test_prepared_packet:: + test_PrepareSendMessageBlockRanksEligibleChannels); + RUN_TEST(ae::test_prepared_packet:: + test_PrepareSendMessageBlockRanksChannelsByBuildTimeout); + RUN_TEST(ae::test_prepared_packet:: + test_PrepareSendMessageBlockRanksChannelsByResponseTimeout); + RUN_TEST(ae::test_prepared_packet:: + test_PrepareSendMessageBlockUncachedDestinationReturnsError); + RUN_TEST(ae::test_prepared_packet::test_EncodePacketReportsInvalidBlock); + RUN_TEST(ae::test_prepared_packet::test_EncodePacketReportsExhaustedBlock); +#if AE_CRYPTO_SYNC == AE_CHACHA20_POLY1305 || \ + AE_CRYPTO_SYNC == AE_HYDRO_CRYPTO_SK + RUN_TEST(ae::test_prepared_packet:: + test_PrepareSendMessageBlockForConfiguredClientEncodesPacket); + RUN_TEST(ae::test_prepared_packet:: + test_EncodePacketUsesSenderEphemeralForLoginAlias); +#endif + return UNITY_END(); +} diff --git a/tests/test-server-connection/test_server_connection_recovery.cpp b/tests/test-server-connection/test_server_connection_recovery.cpp index 1cf063b9..bec78a84 100644 --- a/tests/test-server-connection/test_server_connection_recovery.cpp +++ b/tests/test-server-connection/test_server_connection_recovery.cpp @@ -185,6 +185,8 @@ class FakeChannel final : public Channel { return std::chrono::milliseconds{50}; } + std::optional endpoint() const override { return std::nullopt; }; + private: FakeBuildPolicy policy_; }; From e7ed37433eb6fd1b99733c9118f897774d1fd47c Mon Sep 17 00:00:00 2001 From: BartolomeyKant Date: Wed, 26 Aug 2026 12:31:53 +0500 Subject: [PATCH 18/20] fix export includes to all --- aether/all.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/aether/all.h b/aether/all.h index 20f5be39..9b305ed6 100644 --- a/aether/all.h +++ b/aether/all.h @@ -22,6 +22,7 @@ #include "aether/aether_app.h" #include "aether/common.h" #include "aether/config.h" +#include "aether/env.h" #include "aether/memory.h" #include "aether/actions/action_context.h" @@ -94,6 +95,9 @@ #include "aether/modems/imodem_driver.h" #include "aether/modems/modem_factory.h" +#include "aether/prepared_packet/packet_encoder.h" +#include "aether/prepared_packet/prepared_send_message.h" + #include "aether/aether.h" #include "aether/channels/channel.h" #include "aether/client.h" From 9cfe0b7a7df702b47c69a7bf738033da2e6218db Mon Sep 17 00:00:00 2001 From: BartolomeyKant Date: Fri, 28 Aug 2026 13:12:23 +0500 Subject: [PATCH 19/20] add message server example instead of windows_message_receiver --- CMakeLists.txt | 4 +- examples/a_b_message_exchange/CMakeLists.txt | 14 ++ .../a_b_message_exchange.cpp | 2 +- examples/a_b_message_exchange/main.cpp | 32 ++++- .../CMakeLists.txt | 32 ++--- examples/message_server/main.cpp | 42 ++++++ examples/message_server/message_server.cpp | 124 +++++++++++++++++ .../config/cloud_config.ini | 38 ------ .../windows_message_receiver/user_config.h | 39 ------ .../windows_message_receiver.cpp | 126 ------------------ 10 files changed, 219 insertions(+), 234 deletions(-) rename examples/{windows_message_receiver => message_server}/CMakeLists.txt (60%) create mode 100644 examples/message_server/main.cpp create mode 100644 examples/message_server/message_server.cpp delete mode 100644 examples/windows_message_receiver/config/cloud_config.ini delete mode 100644 examples/windows_message_receiver/user_config.h delete mode 100644 examples/windows_message_receiver/windows_message_receiver.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index df8e2fd2..691ec1a5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -383,9 +383,7 @@ if(AE_BUILD_EXAMPLES) add_subdirectory(examples/common) add_subdirectory(examples/cloud) add_subdirectory(examples/a_b_message_exchange) - if(CMAKE_SYSTEM_NAME STREQUAL "Windows") - add_subdirectory(examples/windows_message_receiver) - endif() + add_subdirectory(examples/message_server) add_subdirectory(examples/capi/oddity) add_subdirectory(examples/benches/send_message_delays) add_subdirectory(examples/benches/send_messages_bandwidth) diff --git a/examples/a_b_message_exchange/CMakeLists.txt b/examples/a_b_message_exchange/CMakeLists.txt index 6c0e3e74..9683a558 100644 --- a/examples/a_b_message_exchange/CMakeLists.txt +++ b/examples/a_b_message_exchange/CMakeLists.txt @@ -1,3 +1,17 @@ +# Copyright 2026 Aethernet Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + cmake_minimum_required(VERSION 3.16.0) set(CMAKE_CXX_STANDARD 20) diff --git a/examples/a_b_message_exchange/a_b_message_exchange.cpp b/examples/a_b_message_exchange/a_b_message_exchange.cpp index e6032266..3f6c6a99 100644 --- a/examples/a_b_message_exchange/a_b_message_exchange.cpp +++ b/examples/a_b_message_exchange/a_b_message_exchange.cpp @@ -171,7 +171,7 @@ static auto SendMessageBtoA(State* state, int message_num) { } // namespace ae::examples -int AetherABMessageExchangeExample() { +int MessageServerExample() { using namespace ae::examples; // NOLINT Log("app.create.start"); auto aether_app = ae::examples::construct_aether_app(); diff --git a/examples/a_b_message_exchange/main.cpp b/examples/a_b_message_exchange/main.cpp index 82383b3e..d7c6d975 100644 --- a/examples/a_b_message_exchange/main.cpp +++ b/examples/a_b_message_exchange/main.cpp @@ -1,27 +1,45 @@ -/* Copyright 2024 Aethernet Inc. */ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ #include "aether/config.h" #include "aether/tele.h" #if (defined(CM_ESP32)) -# include # include +# include #endif extern "C" void app_main(); -extern int AetherABMessageExchangeExample(); +extern int MessageServerExample(); -int test(void) { return AetherABMessageExchangeExample(); } +int test(void) { return MessageServerExample(); } #if (defined(ESP_PLATFORM)) void app_main(void) { - esp_task_wdt_config_t config_wdt = {.timeout_ms = 60000, .idle_core_mask = 0, .trigger_panic = true}; + esp_task_wdt_config_t config_wdt = { + .timeout_ms = 60000, .idle_core_mask = 0, .trigger_panic = true}; auto err = esp_task_wdt_reconfigure(&config_wdt); - if (err != 0) { std::cerr << "Reconfigure WDT is failed!\n"; } + if (err != 0) { + std::cerr << "Reconfigure WDT is failed!\n"; + } test(); } #endif -#if (defined(__linux__) || defined(__unix__) || defined(__APPLE__) || defined(__FreeBSD__) || defined(_WIN64) || defined(_WIN32)) +#if (defined(__linux__) || defined(__unix__) || defined(__APPLE__) || \ + defined(__FreeBSD__) || defined(_WIN64) || defined(_WIN32)) int main() { return test(); } #endif diff --git a/examples/windows_message_receiver/CMakeLists.txt b/examples/message_server/CMakeLists.txt similarity index 60% rename from examples/windows_message_receiver/CMakeLists.txt rename to examples/message_server/CMakeLists.txt index ede10f15..551f2926 100644 --- a/examples/windows_message_receiver/CMakeLists.txt +++ b/examples/message_server/CMakeLists.txt @@ -1,4 +1,4 @@ -# Copyright 2024 Aethernet Inc. +# Copyright 2026 Aethernet Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,34 +17,26 @@ cmake_minimum_required(VERSION 3.16.0) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) -list( APPEND src_list - windows_message_receiver.cpp -) +list(APPEND sources main.cpp message_server.cpp) -if(NOT CM_PLATFORM) - project("aether-client-cpp-windows_message_receiver" VERSION "1.0.0" LANGUAGES C CXX) - add_executable(${PROJECT_NAME} ${src_list}) - target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) - target_link_libraries(${PROJECT_NAME} PRIVATE aether) +if(NOT CM_PLATFORM) + project("message-server" VERSION "1.0.0" LANGUAGES C CXX) + set(TARGET_NAME ${PROJECT_NAME}) + add_executable(${TARGET_NAME} ${sources}) + target_include_directories(${TARGET_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(${TARGET_NAME} PRIVATE aether_examples_common) else() idf_build_get_property(CM_PLATFORM CM_PLATFORM) if(CM_PLATFORM STREQUAL "ESP32") - #ESP32 CMake - idf_component_register(SRCS ${src_list} - INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR} - REQUIRES - esp_wifi - esp_netif - nvs_flash - spiffs - esp_driver_uart - ) + idf_component_register( + SRCS ${sources} + INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR} ../common + REQUIRES esp_wifi esp_netif nvs_flash spiffs esp_driver_uart) add_subdirectory("../../" aether) target_link_libraries(${COMPONENT_LIB} PRIVATE aether) else() - #Other platforms message(FATAL_ERROR "Platform ${CM_PLATFORM} is not supported") endif() endif() diff --git a/examples/message_server/main.cpp b/examples/message_server/main.cpp new file mode 100644 index 00000000..482c13ae --- /dev/null +++ b/examples/message_server/main.cpp @@ -0,0 +1,42 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#if (defined(CM_ESP32)) +# include +# include +#endif + +extern "C" void app_main(); +extern int MessageServerExample(); + +int run(void) { return MessageServerExample(); } + +#if (defined(ESP_PLATFORM)) +void app_main(void) { + esp_task_wdt_config_t config_wdt = { + .timeout_ms = 60000, .idle_core_mask = 0, .trigger_panic = true}; + auto err = esp_task_wdt_reconfigure(&config_wdt); + if (err != 0) { + std::cerr << "Reconfigure WDT is failed!\n"; + } + run(); +} +#endif + +#if (defined(__linux__) || defined(__unix__) || defined(__APPLE__) || \ + defined(__FreeBSD__) || defined(_WIN64) || defined(_WIN32)) +int main() { return run(); } +#endif diff --git a/examples/message_server/message_server.cpp b/examples/message_server/message_server.cpp new file mode 100644 index 00000000..86830f8e --- /dev/null +++ b/examples/message_server/message_server.cpp @@ -0,0 +1,124 @@ +/* + * Copyright 2026 Aethernet Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include + +#include + +#include "aether/all.h" + +// IWYU pragma: begin_keeps +// common aether app construction logic for different scenarios +#if defined ESP_PLATFORM +# define AE_EXAMPLE_ESP_WIFI 1 +#else +# define AE_EXAMPLE_ETHERNET 1 +#endif + +#include "aether_construct.h" +#include "aether_construct_esp_wifi.h" +#include "aether_construct_ethernet.h" +#include "aether_construct_lora_module.h" +#include "aether_construct_modem.h" +// IWYU pragma: end_keeps + +void SetInterruptHandler([[maybe_unused]] ae::AetherApp& app) { +#if defined __unix__ || defined _WIN32 + static void* app_ptr; + app_ptr = &app; + auto handler = +[](int) { + std::cout << "\n >>> Interrupted, exiting...\n\n"; + static_cast(app_ptr)->Exit(0); + }; + signal(SIGINT, handler); +#endif +} + +static constexpr auto kParentUid = + ae::Uid::FromString("3ac93165-3d37-4970-87a6-fa4ee27744e4"); + +void MessageReceived(ae::Uid sender, ae::DataBuffer const& message) { + ae::Format( + std::cout, + "\n >>> Received message from {}\n >>> blob: {}\n >>> as text: {}\n\n", + sender, message, + std::string_view{reinterpret_cast(message.data()), + message.size()}); +} + +void SubscribeToMessages(ae::Client::ptr const& client, + ae::AeContext const& context, + std::map>& + client_streams) noexcept { + auto client_ptr = client.Load(); + assert(client_ptr); + + // listen for new port open events from the other clients + client_ptr->message_stream_manager().new_port_event().Subscribe( + [&, c_ = ae::Ptr(client_ptr), + ctx_ = context](ae::P2pPortHandle&& p2p_handle) { + auto dest = p2p_handle.destination(); + // insert new stream into client streams map + auto [stream, _] = client_streams.insert_or_assign( + dest, std::make_shared(ctx_, c_, dest, + std::move(p2p_handle))); + // subscribe to out data event for receiving messages + stream->second->out_data_event().Subscribe( + [dest](ae::DataBuffer const& data) { + MessageReceived(dest, data); + }); + }); +} + +int MessageServerExample() { + auto aether_app = ae::examples::construct_aether_app(); + SetInterruptHandler(*aether_app); + + std::map> client_streams; + + // build async initialization pipeline + auto pipeline = + ae::ex::action_wait( + aether_app->aether()->SelectClient(kParentUid, "message_server")) | + ae::ex::then([&](ae::Client::ptr const& client) noexcept { + ae::Format(std::cout, + "\n >>> Message server client selected\n >>> Uid: {}\n\n", + client->uid()); + SubscribeToMessages(client, *aether_app, client_streams); + }); + + // wait for the initialization to complete + auto waiter = ae::ex::AsyncWaiter{ + ae::AeContext{*aether_app}, std::move(pipeline), + [&](auto&& res) noexcept { + if (res && res->IsOk()) { + std::cout << "\n >>> Message server is started\n\n"; + } else { + std::cerr << "\n >>> Message server startup failed\n\n"; + aether_app->Exit(1); + } + }}; + + // run main app logic loop until the app is exited + while (!aether_app->IsExited()) { + auto wait_time = aether_app->Update(ae::Now()); + aether_app->WaitUntil(wait_time); + } + return aether_app->ExitCode(); +} diff --git a/examples/windows_message_receiver/config/cloud_config.ini b/examples/windows_message_receiver/config/cloud_config.ini deleted file mode 100644 index 0c275714..00000000 --- a/examples/windows_message_receiver/config/cloud_config.ini +++ /dev/null @@ -1,38 +0,0 @@ -; Static configuration for cloud test -; Register to save state as a header file by -; aether-registrator -; Use header file to build the client test cmake -DFS_INIT= . && cmake --build . - -; Required section -; Aether configuration -[Aether] -; Must have one of the following keys or both depending on the project configuration \see aether/config.h -ed25519_sign_key = 4F202A94AB729FE9B381613AE77A8A7D89EDAB9299C3320D1A0B994BA710CCEB -hydrogen_sign_key = 883B4D7E0FB04A38CA12B3A451B00942048858263EE6E6D61150F2EF15F40343 - -; Optional section -; If provided, wifi adapter would be configured and saved to the state -[AdapterWifi] -ssid = Test123 -pass = Test123 - -; Must have at least one registration server -; Registration server configuration -; Supported protocols: kTcp, kUdp -[RegServer_1] -address = registration.aethernet.io -port=9010 -protocol=kTcp - -[RegServer_2] -address = 34.60.244.148 -port=9010 -protocol=kTcp - -; List of clients -; Suffix after _ is used as user defined client id \see Aether::SelectClient -[Client_A] -parent_uid = 3ac93165-3d37-4970-87a6-fa4ee27744e4 - -[Client_B] -parent_uid = 3ac93165-3d37-4970-87a6-fa4ee27744e4 diff --git a/examples/windows_message_receiver/user_config.h b/examples/windows_message_receiver/user_config.h deleted file mode 100644 index a2fd7142..00000000 --- a/examples/windows_message_receiver/user_config.h +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2024 Aethernet Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#ifndef USER_CONFIG_H_ -#define USER_CONFIG_H_ - -#include "aether/config_consts.h" -/** - * \brief For full config list and default values \see aether/config.h - */ - -// use hydrogen encryption -#define AE_CRYPTO_ASYNC AE_HYDRO_CRYPTO_PK -#define AE_CRYPTO_SYNC AE_HYDRO_CRYPTO_SK -#define AE_SIGNATURE AE_HYDRO_SIGNATURE -#define AE_KDF AE_HYDRO_KDF - -// disable debug telemetry on release -#define AE_TELE_ENABLED 1 -#define AE_TELE_LOG_CONSOLE 1 -#if defined NDEBUG -# define AE_TELE_DEBUG_MODULES 0 -#else -# define AE_TELE_DEBUG_MODULES AE_ALL -#endif -#endif // USER_CONFIG_H_ diff --git a/examples/windows_message_receiver/windows_message_receiver.cpp b/examples/windows_message_receiver/windows_message_receiver.cpp deleted file mode 100644 index f728ae4c..00000000 --- a/examples/windows_message_receiver/windows_message_receiver.cpp +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright 2024 Aethernet Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include -#include - -#include "aether/all.h" - -static constexpr auto kParentUid = - ae::Uid::FromString("B1AC52C8-8D94-BD39-4C01-A631AC594165"); - -class TimeSynchronizer { - public: - TimeSynchronizer() = default; - - void SetPingSentTime(ae::TimePoint ping_sent_time); - void SetPongSentTime(ae::TimePoint pong_sent_time); - - ae::Duration GetPingDuration() const; - ae::Duration GetPongDuration() const; - - private: - ae::TimePoint ping_sent_time_; - ae::TimePoint pong_sent_time_; -}; - -// Bob answers "pong" to each "ping" -class Bob { - public: - explicit Bob(ae::AetherApp& aether_app, ae::Client::ptr client_bob, - TimeSynchronizer& time_synchronizer); - - private: - void OnNewStream(ae::P2pPortHandle p2p_port); - void OnMessageReceived(ae::DataBuffer const& data_buffer); - - ae::AetherApp* aether_app_; - ae::Client::ptr client_bob_; - TimeSynchronizer* time_synchronizer_; - std::unique_ptr p2pstream_; - ae::Subscription new_stream_receive_sub_; - ae::Subscription message_receive_sub_; -}; - -int main() { - auto aether_app = ae::AetherApp::Construct(ae::AetherAppContext{}); - - // std::unique_ptr alice; - std::unique_ptr bob; - TimeSynchronizer time_synchronizer; - - // register or load clients - auto& bob_select = aether_app->aether()->SelectClient(kParentUid, "Bob"); - bob_select.result_event().Subscribe([&](auto const& bob_res) { - if (bob_res) { - bob = - ae::make_unique(*aether_app, bob_res.value(), time_synchronizer); - } else { - aether_app->Exit(1); - } - }); - - while (!aether_app->IsExited()) { - auto next_time = aether_app->Update(ae::Now()); - aether_app->WaitUntil(next_time); - } - return aether_app->ExitCode(); -} - -void TimeSynchronizer::SetPingSentTime(ae::TimePoint ping_sent_time) { - ping_sent_time_ = ping_sent_time; -} -void TimeSynchronizer::SetPongSentTime(ae::TimePoint pong_sent_time) { - pong_sent_time_ = pong_sent_time; -} -ae::Duration TimeSynchronizer::GetPingDuration() const { - return std::chrono::duration_cast(ae::Now() - ping_sent_time_); -} -ae::Duration TimeSynchronizer::GetPongDuration() const { - return std::chrono::duration_cast(ae::Now() - pong_sent_time_); -} - -Bob::Bob(ae::AetherApp& aether_app, ae::Client::ptr client_bob, - TimeSynchronizer& time_synchronizer) - : aether_app_{&aether_app}, - client_bob_{std::move(client_bob)}, - time_synchronizer_{&time_synchronizer}, - new_stream_receive_sub_{ - client_bob_->message_stream_manager().new_port_event().Subscribe( - ae::MethodPtr<&Bob::OnNewStream>{this})} {} - -void Bob::OnNewStream(ae::P2pPortHandle p2p_port) { - p2pstream_ = std::make_unique(*aether_app_, client_bob_.Load(), - p2p_port.destination(), - std::move(p2p_port)); - message_receive_sub_ = p2pstream_->out_data_event().Subscribe( - ae::MethodPtr<&Bob::OnMessageReceived>{this}); -} - -void Bob::OnMessageReceived(ae::DataBuffer const& data_buffer) { - auto ping_message = std::string_view{ - reinterpret_cast(data_buffer.data()), data_buffer.size()}; - std::cout << ae::Format( - ">>>\n>>>\n>>>\n[{:%H:%M:%S}] Bob received \"{}\" within time {} " - "ms\n>>>\n>>>\n>>>\n", - ae::Now(), ping_message, - std::chrono::duration_cast( - time_synchronizer_->GetPingDuration()) - .count()); - std::cout << ae::Format("Hex string [{}]\n", data_buffer); - time_synchronizer_->SetPongSentTime(ae::Now()); - constexpr std::string_view pong_message = "pong"; -} From 660f31061dd7b630026078a65077bd9d9a84df1e Mon Sep 17 00:00:00 2001 From: BartolomeyKant Date: Fri, 28 Aug 2026 14:19:06 +0500 Subject: [PATCH 20/20] fix includes --- examples/message_server/message_server.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/message_server/message_server.cpp b/examples/message_server/message_server.cpp index 86830f8e..be150123 100644 --- a/examples/message_server/message_server.cpp +++ b/examples/message_server/message_server.cpp @@ -19,7 +19,9 @@ #include #include -#include +#if defined __unix__ || defined _WIN32 +# include +#endif #include "aether/all.h"