diff --git a/CMakeLists.txt b/CMakeLists.txt index a9ceadd8..691ec1a5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -383,6 +383,7 @@ if(AE_BUILD_EXAMPLES) add_subdirectory(examples/common) add_subdirectory(examples/cloud) add_subdirectory(examples/a_b_message_exchange) + 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/aether/CMakeLists.txt b/aether/CMakeLists.txt index 2e0da7ba..d0fdc096 100644 --- a/aether/CMakeLists.txt +++ b/aether/CMakeLists.txt @@ -198,6 +198,8 @@ list(APPEND aether_srcs list(APPEND aether_srcs "server_connections/client_server_connection.cpp" + "prepared_packet/prepared_send_message.cpp" + "prepared_packet/packet_encoder.cpp" "server_connections/channel_select_action.cpp" "server_connections/server_connection.cpp") 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" 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 c202645f..12dcef53 100644 --- a/aether/client_messages/p2p_message_stream.cpp +++ b/aether/client_messages/p2p_message_stream.cpp @@ -24,6 +24,7 @@ #include "aether/cloud.h" #include "aether/cloud_connections/cloud_request.h" +#include "aether/cloud_connections/cloud_server_connection.h" #include "aether/cloud_connections/cloud_subscription.h" #include "aether/client_messages/client_messages_tele.h" @@ -48,6 +49,7 @@ class MessageSendStream final : public IStream { }}, request_policy_); } + StreamInfo stream_info() const override { return stream_info_; } OutDataEvent::Subscriber out_data_event() override { return out_data_event_; } StreamUpdateEvent::Subscriber stream_update_event() override { diff --git a/aether/client_messages/p2p_message_stream.h b/aether/client_messages/p2p_message_stream.h index 64da048b..f52f78f6 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; 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 new file mode 100644 index 00000000..e8374e54 --- /dev/null +++ b/aether/prepared_packet/packet_encoder.cpp @@ -0,0 +1,75 @@ +#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(PreparedSendMessage& block) + : block_{&block} {} + + Key GetKey() const override { return block_->client_to_server_key; } + + CryptoNonce const& Nonce() const override { return block_->next_nonce; } + + private: + PreparedSendMessage* block_; +}; + +} // namespace + +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. + send_message->next_nonce.Next(); + --send_message->message_left; + + auto key_provider = + std::make_unique(*send_message); + 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( + send_message->sender_ephemeral, + SubApi{ + [&](auto& auth_api) { + auth_api->send_message( + AeMessage{send_message->destination_uid, DataBuffer{payload}}); + }, + }); + + out = std::move(api_context).Pack(); + + return Ok{out.size()}; +} + +} // namespace ae::prepared_packet diff --git a/aether/prepared_packet/packet_encoder.h b/aether/prepared_packet/packet_encoder.h new file mode 100644 index 00000000..5372e064 --- /dev/null +++ b/aether/prepared_packet/packet_encoder.h @@ -0,0 +1,46 @@ +/* + * Prepared packet encoder. + * + * 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 "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 { + +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 + +#endif // AETHER_PREPARED_PACKET_PACKET_ENCODER_H_ 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 new file mode 100644 index 00000000..6b184d31 --- /dev/null +++ b/aether/prepared_packet/prepared_send_message.cpp @@ -0,0 +1,210 @@ +#include "aether/prepared_packet/prepared_send_message.h" + +#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(); +} + +// 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 (!addr) { + return std::nullopt; + } + + 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 new file mode 100644 index 00000000..922d0ca9 --- /dev/null +++ b/aether/prepared_packet/prepared_send_message.h @@ -0,0 +1,104 @@ +/* + * 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 "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/server_id.h" +#include "aether/types/uid.h" +#include "aether/types/variant_type.h" + +#include "aether/prepared_packet/prepared_block.h" + +namespace ae { +class Client; +} + +namespace ae::prepared_packet { + +struct PreparedAddr + : VariantType, + VPair> { + using VariantType::VariantType; + using VariantType::operator=; +}; + +struct PreparedEndpoint { + AE_REFLECT_MEMBERS(address, port, protocol) + PreparedAddr address; + std::uint16_t port; + Protocol protocol; +}; + +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; + + PreparedEndpoint endpoint; + ServerId server_id; + + Key client_to_server_key; + + CryptoNonce next_nonce; + + std::uint32_t message_left; +}; + +using PreparedSendMessageBlock = PreparedBlock; + +struct PreparedBlockError { + int ec; + std::string_view msg; +}; + +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 + +#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 d6a96675..ddff0bc2 100644 --- a/aether/server_connections/client_server_connection.cpp +++ b/aether/server_connections/client_server_connection.cpp @@ -142,6 +142,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()}, diff --git a/aether/server_connections/client_server_connection.h b/aether/server_connections/client_server_connection.h index 6996fc8a..9d12ac87 100644 --- a/aether/server_connections/client_server_connection.h +++ b/aether/server_connections/client_server_connection.h @@ -78,6 +78,7 @@ class ClientServerConnection { 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..382f7ec1 100644 --- a/aether/stream_api/api_call_adapter.h +++ b/aether/stream_api/api_call_adapter.h @@ -17,10 +17,14 @@ #ifndef AETHER_STREAM_API_API_CALL_ADAPTER_H_ #define AETHER_STREAM_API_API_CALL_ADAPTER_H_ +#include #include -#include "aether/stream_api/istream.h" #include "aether/api_protocol/api_context.h" +#include "aether/stream_api/istream.h" +#include "aether/types/data_buffer.h" + +#include "aether/tele.h" namespace ae { /** @@ -35,7 +39,9 @@ class ApiCallAdapter { AE_CLASS_MOVE_ONLY(ApiCallAdapter) - WriteAction& Flush() { return byte_stream_->Write(std::move(api_context_)); } + WriteAction& Flush() { + return byte_stream_->Write(DataBuffer{std::move(api_context_)}); + } ApiContext& operator->() { return api_context_; } diff --git a/aether/transport/system_sockets/udp/udp.h b/aether/transport/system_sockets/udp/udp.h index bdfa3a71..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); } diff --git a/aether/wifi/esp_wifi_driver.cpp b/aether/wifi/esp_wifi_driver.cpp index 44f73c45..db5c14be 100644 --- a/aether/wifi/esp_wifi_driver.cpp +++ b/aether/wifi/esp_wifi_driver.cpp @@ -22,18 +22,19 @@ # 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" # 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, @@ -82,9 +83,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 +235,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..4b3a0baf 100644 --- a/aether/write_action/buffer_write.h +++ b/aether/write_action/buffer_write.h @@ -45,6 +45,7 @@ namespace ae { do { \ AE_TELED_WARNING(__VA_ARGS__); \ } while (false) + #else # define BW_LOG_DEBUG(...) # define BW_LOG_WARNING(...) 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/message_server/CMakeLists.txt b/examples/message_server/CMakeLists.txt new file mode 100644 index 00000000..551f2926 --- /dev/null +++ b/examples/message_server/CMakeLists.txt @@ -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. + +cmake_minimum_required(VERSION 3.16.0) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +list(APPEND sources main.cpp message_server.cpp) + + +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") + 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() + 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..be150123 --- /dev/null +++ b/examples/message_server/message_server.cpp @@ -0,0 +1,126 @@ +/* + * 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 + +#if defined __unix__ || defined _WIN32 +# include +#endif + +#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/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", 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_; };