349 lines
13 KiB
C++
349 lines
13 KiB
C++
|
|
#include "BLEPeerSessionManager.h"
|
||
|
|
|
||
|
|
#include <algorithm>
|
||
|
|
#include <stdexcept>
|
||
|
|
|
||
|
|
namespace ble_reticulum {
|
||
|
|
|
||
|
|
namespace {
|
||
|
|
|
||
|
|
constexpr uint16_t BLE_MINIMUM_MTU = 23;
|
||
|
|
|
||
|
|
SessionAction make_action(SessionActionType type, const ConnectionId& target) {
|
||
|
|
SessionAction action;
|
||
|
|
action.type = type;
|
||
|
|
action.target = target;
|
||
|
|
return action;
|
||
|
|
}
|
||
|
|
|
||
|
|
SessionAction make_message_action(SessionActionType type,
|
||
|
|
const ConnectionId& target,
|
||
|
|
const std::string& message) {
|
||
|
|
SessionAction action = make_action(type, target);
|
||
|
|
action.message = message;
|
||
|
|
return action;
|
||
|
|
}
|
||
|
|
|
||
|
|
} // namespace
|
||
|
|
|
||
|
|
BLEPeerSessionManager::BLEPeerSessionManager(double pending_identity_timeout,
|
||
|
|
double zombie_timeout)
|
||
|
|
: pending_identity_timeout_(pending_identity_timeout),
|
||
|
|
zombie_timeout_(zombie_timeout) {}
|
||
|
|
|
||
|
|
bool BLEPeerSessionManager::isIdentityHandshakePayload(const uint8_t* data,
|
||
|
|
size_t data_size) {
|
||
|
|
return data != nullptr && data_size == 16;
|
||
|
|
}
|
||
|
|
|
||
|
|
PeerIdentity BLEPeerSessionManager::identityFromPayload(const uint8_t* data,
|
||
|
|
size_t data_size) {
|
||
|
|
if (!isIdentityHandshakePayload(data, data_size)) {
|
||
|
|
throw std::invalid_argument("identity payload must be exactly 16 bytes");
|
||
|
|
}
|
||
|
|
|
||
|
|
PeerIdentity identity{};
|
||
|
|
std::copy(data, data + identity.size(), identity.begin());
|
||
|
|
return identity;
|
||
|
|
}
|
||
|
|
|
||
|
|
std::string BLEPeerSessionManager::computeIdentityKey(const PeerIdentity& identity) {
|
||
|
|
static constexpr char hex[] = "0123456789abcdef";
|
||
|
|
std::string out;
|
||
|
|
out.reserve(16);
|
||
|
|
for (size_t i = 0; i < 8; ++i) {
|
||
|
|
const uint8_t byte = identity[i];
|
||
|
|
out.push_back(hex[(byte >> 4) & 0x0f]);
|
||
|
|
out.push_back(hex[byte & 0x0f]);
|
||
|
|
}
|
||
|
|
return out;
|
||
|
|
}
|
||
|
|
|
||
|
|
std::string BLEPeerSessionManager::computeFragmenterKey(const PeerIdentity& identity) {
|
||
|
|
static constexpr char hex[] = "0123456789abcdef";
|
||
|
|
std::string out;
|
||
|
|
out.reserve(32);
|
||
|
|
for (uint8_t byte : identity) {
|
||
|
|
out.push_back(hex[(byte >> 4) & 0x0f]);
|
||
|
|
out.push_back(hex[byte & 0x0f]);
|
||
|
|
}
|
||
|
|
return out;
|
||
|
|
}
|
||
|
|
|
||
|
|
HandshakeResult BLEPeerSessionManager::handleIdentityHandshake(
|
||
|
|
const ConnectionSnapshot& connection,
|
||
|
|
const uint8_t* data,
|
||
|
|
size_t data_size,
|
||
|
|
double now_seconds) {
|
||
|
|
|
||
|
|
HandshakeResult result;
|
||
|
|
result.mtu = connection.negotiated_mtu.value_or(BLE_MINIMUM_MTU);
|
||
|
|
|
||
|
|
if (!isIdentityHandshakePayload(data, data_size)) {
|
||
|
|
result.decision = InputDecision::PassToReassembler;
|
||
|
|
result.consumed = false;
|
||
|
|
result.actions.push_back(make_action(SessionActionType::PassToReassembler,
|
||
|
|
connection.current));
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
|
||
|
|
PeerIdentity payload_identity = identityFromPayload(data, data_size);
|
||
|
|
result.peer_identity = payload_identity;
|
||
|
|
result.identity_key = computeIdentityKey(payload_identity);
|
||
|
|
result.fragmenter_key = computeFragmenterKey(payload_identity);
|
||
|
|
result.consumed = true;
|
||
|
|
|
||
|
|
if (connection.known_identity_for_address.has_value()) {
|
||
|
|
if (connection.known_identity_for_address.value() == payload_identity) {
|
||
|
|
result.decision = InputDecision::ConsumedDuplicateSameIdentity;
|
||
|
|
result.actions.push_back(make_action(SessionActionType::ConsumeInput,
|
||
|
|
connection.current));
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
|
||
|
|
result.decision = InputDecision::ConsumedDuplicateMismatchedIdentity;
|
||
|
|
result.actions.push_back(make_message_action(
|
||
|
|
SessionActionType::Warn,
|
||
|
|
connection.current,
|
||
|
|
"16-byte data differs from known identity; consumed as identity-like data"));
|
||
|
|
result.actions.push_back(make_action(SessionActionType::ConsumeInput,
|
||
|
|
connection.current));
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
|
||
|
|
const PeerSession* existing_session = findSessionByIdentityKey(result.identity_key);
|
||
|
|
std::optional<std::string> existing_address = connection.existing_address_for_identity;
|
||
|
|
if (!existing_address.has_value() && existing_session &&
|
||
|
|
!existing_session->current_address.empty()) {
|
||
|
|
existing_address = existing_session->current_address;
|
||
|
|
}
|
||
|
|
|
||
|
|
const bool duplicate_elsewhere =
|
||
|
|
existing_address.has_value() && existing_address.value() != connection.current.address;
|
||
|
|
|
||
|
|
if (duplicate_elsewhere) {
|
||
|
|
const bool stale_or_pending =
|
||
|
|
connection.identity_has_pending_detach ||
|
||
|
|
(!connection.existing_address_connected &&
|
||
|
|
!connection.existing_address_in_peer_table);
|
||
|
|
|
||
|
|
if (connection.existing_connection_is_zombie || stale_or_pending) {
|
||
|
|
SessionAction cleanup = make_action(SessionActionType::CleanupOldAddress,
|
||
|
|
connection.current);
|
||
|
|
cleanup.old_address = existing_address.value();
|
||
|
|
cleanup.new_address = connection.current.address;
|
||
|
|
result.actions.push_back(cleanup);
|
||
|
|
|
||
|
|
if (connection.existing_connection_is_zombie) {
|
||
|
|
result.should_disconnect_old = true;
|
||
|
|
SessionAction disconnect_old =
|
||
|
|
make_action(SessionActionType::DisconnectOldPeer, connection.current);
|
||
|
|
disconnect_old.old_address = existing_address.value();
|
||
|
|
result.actions.push_back(disconnect_old);
|
||
|
|
}
|
||
|
|
|
||
|
|
SessionAction update = make_action(SessionActionType::UpdatePeerAddress,
|
||
|
|
connection.current);
|
||
|
|
update.old_address = existing_address.value();
|
||
|
|
update.new_address = connection.current.address;
|
||
|
|
result.actions.push_back(update);
|
||
|
|
} else {
|
||
|
|
result.decision = InputDecision::RejectedDuplicateIdentity;
|
||
|
|
result.should_disconnect_current = true;
|
||
|
|
result.actions.push_back(make_action(SessionActionType::RejectDuplicateIdentity,
|
||
|
|
connection.current));
|
||
|
|
result.actions.push_back(make_action(SessionActionType::DisconnectCurrentPeer,
|
||
|
|
connection.current));
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
upsertAcceptedSession(connection, payload_identity, result.mtu, now_seconds);
|
||
|
|
removePendingIdentity(connection.current);
|
||
|
|
|
||
|
|
result.decision = InputDecision::AcceptedNewIdentity;
|
||
|
|
result.accepted = true;
|
||
|
|
result.actions.push_back(make_action(SessionActionType::AcceptNewIdentity,
|
||
|
|
connection.current));
|
||
|
|
result.actions.push_back(make_action(SessionActionType::CreateFragmentationState,
|
||
|
|
connection.current));
|
||
|
|
result.actions.push_back(make_action(SessionActionType::MarkPeerReady,
|
||
|
|
connection.current));
|
||
|
|
result.actions.push_back(make_action(SessionActionType::RemovePendingIdentity,
|
||
|
|
connection.current));
|
||
|
|
result.actions.push_back(make_action(SessionActionType::MarkRealData,
|
||
|
|
connection.current));
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
|
||
|
|
void BLEPeerSessionManager::markConnected(const ConnectionSnapshot& connection,
|
||
|
|
double now_seconds) {
|
||
|
|
if (connection.known_identity_for_address.has_value()) {
|
||
|
|
upsertAcceptedSession(connection,
|
||
|
|
connection.known_identity_for_address.value(),
|
||
|
|
connection.negotiated_mtu.value_or(BLE_MINIMUM_MTU),
|
||
|
|
now_seconds);
|
||
|
|
} else {
|
||
|
|
markPendingIdentity(connection.current, now_seconds);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void BLEPeerSessionManager::markDisconnected(const ConnectionId& connection,
|
||
|
|
double /*now_seconds*/) {
|
||
|
|
sessions_.erase(std::remove_if(sessions_.begin(), sessions_.end(),
|
||
|
|
[&](const PeerSession& session) {
|
||
|
|
return session.current_address == connection.address ||
|
||
|
|
session.current_handle == connection.handle;
|
||
|
|
}),
|
||
|
|
sessions_.end());
|
||
|
|
removePendingIdentity(connection);
|
||
|
|
}
|
||
|
|
|
||
|
|
void BLEPeerSessionManager::markMtu(const ConnectionId& connection, uint16_t mtu) {
|
||
|
|
PeerSession* session = findSessionByAddress(connection.address);
|
||
|
|
if (session) {
|
||
|
|
session->mtu = mtu;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void BLEPeerSessionManager::markPendingIdentity(const ConnectionId& connection,
|
||
|
|
double now_seconds) {
|
||
|
|
for (PendingIdentity& pending : pending_identities_) {
|
||
|
|
if (pending.connection.address == connection.address) {
|
||
|
|
pending.connection = connection;
|
||
|
|
pending.started_at = now_seconds;
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
PendingIdentity pending;
|
||
|
|
pending.connection = connection;
|
||
|
|
pending.started_at = now_seconds;
|
||
|
|
pending_identities_.push_back(pending);
|
||
|
|
}
|
||
|
|
|
||
|
|
void BLEPeerSessionManager::removePendingIdentity(const ConnectionId& connection) {
|
||
|
|
pending_identities_.erase(
|
||
|
|
std::remove_if(pending_identities_.begin(), pending_identities_.end(),
|
||
|
|
[&](const PendingIdentity& pending) {
|
||
|
|
return pending.connection.address == connection.address;
|
||
|
|
}),
|
||
|
|
pending_identities_.end());
|
||
|
|
}
|
||
|
|
|
||
|
|
std::vector<ConnectionId> BLEPeerSessionManager::expiredPendingIdentities(
|
||
|
|
double now_seconds) const {
|
||
|
|
std::vector<ConnectionId> expired;
|
||
|
|
for (const PendingIdentity& pending : pending_identities_) {
|
||
|
|
if (now_seconds - pending.started_at > pending_identity_timeout_) {
|
||
|
|
expired.push_back(pending.connection);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return expired;
|
||
|
|
}
|
||
|
|
|
||
|
|
std::optional<PeerSessionView> BLEPeerSessionManager::sessionByAddress(
|
||
|
|
const std::string& address) const {
|
||
|
|
const PeerSession* session = findSessionByAddress(address);
|
||
|
|
if (!session) {
|
||
|
|
return std::nullopt;
|
||
|
|
}
|
||
|
|
PeerSessionView view;
|
||
|
|
view.identity = session->identity;
|
||
|
|
view.identity_key = session->identity_key;
|
||
|
|
view.current_address = session->current_address;
|
||
|
|
view.current_handle = session->current_handle;
|
||
|
|
view.mtu = session->mtu;
|
||
|
|
view.has_fragmentation_state = session->has_fragmentation_state;
|
||
|
|
view.peer_ready = session->peer_ready;
|
||
|
|
view.pending_identity_since = session->pending_identity_since;
|
||
|
|
view.last_real_data = session->last_real_data;
|
||
|
|
return view;
|
||
|
|
}
|
||
|
|
|
||
|
|
std::optional<PeerSessionView> BLEPeerSessionManager::sessionByIdentity(
|
||
|
|
const PeerIdentity& identity) const {
|
||
|
|
const PeerSession* session = findSessionByIdentityKey(computeIdentityKey(identity));
|
||
|
|
if (!session) {
|
||
|
|
return std::nullopt;
|
||
|
|
}
|
||
|
|
PeerSessionView view;
|
||
|
|
view.identity = session->identity;
|
||
|
|
view.identity_key = session->identity_key;
|
||
|
|
view.current_address = session->current_address;
|
||
|
|
view.current_handle = session->current_handle;
|
||
|
|
view.mtu = session->mtu;
|
||
|
|
view.has_fragmentation_state = session->has_fragmentation_state;
|
||
|
|
view.peer_ready = session->peer_ready;
|
||
|
|
view.pending_identity_since = session->pending_identity_since;
|
||
|
|
view.last_real_data = session->last_real_data;
|
||
|
|
return view;
|
||
|
|
}
|
||
|
|
|
||
|
|
BLEPeerSessionManager::PeerSession* BLEPeerSessionManager::findSessionByIdentityKey(
|
||
|
|
const std::string& identity_key) {
|
||
|
|
for (PeerSession& session : sessions_) {
|
||
|
|
if (session.identity_key == identity_key) {
|
||
|
|
return &session;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return nullptr;
|
||
|
|
}
|
||
|
|
|
||
|
|
const BLEPeerSessionManager::PeerSession* BLEPeerSessionManager::findSessionByIdentityKey(
|
||
|
|
const std::string& identity_key) const {
|
||
|
|
for (const PeerSession& session : sessions_) {
|
||
|
|
if (session.identity_key == identity_key) {
|
||
|
|
return &session;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return nullptr;
|
||
|
|
}
|
||
|
|
|
||
|
|
BLEPeerSessionManager::PeerSession* BLEPeerSessionManager::findSessionByAddress(
|
||
|
|
const std::string& address) {
|
||
|
|
for (PeerSession& session : sessions_) {
|
||
|
|
if (session.current_address == address) {
|
||
|
|
return &session;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return nullptr;
|
||
|
|
}
|
||
|
|
|
||
|
|
const BLEPeerSessionManager::PeerSession* BLEPeerSessionManager::findSessionByAddress(
|
||
|
|
const std::string& address) const {
|
||
|
|
for (const PeerSession& session : sessions_) {
|
||
|
|
if (session.current_address == address) {
|
||
|
|
return &session;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return nullptr;
|
||
|
|
}
|
||
|
|
|
||
|
|
void BLEPeerSessionManager::upsertAcceptedSession(
|
||
|
|
const ConnectionSnapshot& connection,
|
||
|
|
const PeerIdentity& identity,
|
||
|
|
uint16_t mtu,
|
||
|
|
double now_seconds) {
|
||
|
|
const std::string identity_key = computeIdentityKey(identity);
|
||
|
|
PeerSession* session = findSessionByIdentityKey(identity_key);
|
||
|
|
if (!session) {
|
||
|
|
PeerSession new_session;
|
||
|
|
new_session.identity = identity;
|
||
|
|
new_session.identity_key = identity_key;
|
||
|
|
sessions_.push_back(new_session);
|
||
|
|
session = &sessions_.back();
|
||
|
|
}
|
||
|
|
|
||
|
|
session->identity = identity;
|
||
|
|
session->identity_key = identity_key;
|
||
|
|
session->current_address = connection.current.address;
|
||
|
|
session->current_handle = connection.current.handle;
|
||
|
|
session->mtu = mtu;
|
||
|
|
session->has_fragmentation_state = true;
|
||
|
|
session->peer_ready = true;
|
||
|
|
session->pending_identity_since = 0.0;
|
||
|
|
session->last_real_data = now_seconds;
|
||
|
|
}
|
||
|
|
|
||
|
|
} // namespace ble_reticulum
|