added handshake for dos protection

This commit is contained in:
2025-09-21 10:32:53 -05:00
parent b68a392d22
commit ee03167e43
8 changed files with 325 additions and 270 deletions

View File

@@ -5,6 +5,7 @@
### BREAKING CHANGES
* Remote mounts must be upgraded to v2.1.0+ to support new authentication scheme
* Protocol handshake added for DoS protection
### Issues

View File

@@ -0,0 +1,77 @@
/*
Copyright <2018-2025> <scott.e.graves@protonmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#ifndef REPERTORY_INCLUDE_COMM_PACKET_COMMON_HPP_
#define REPERTORY_INCLUDE_COMM_PACKET_COMMON_HPP_
#include "utils/common.hpp"
namespace repertory::comm {
constexpr const std::uint8_t max_read_attempts{5U};
constexpr const std::uint16_t packet_nonce_size{256U};
struct non_blocking_guard final {
boost::asio::ip::tcp::socket &sock;
bool non_blocking{};
non_blocking_guard(const non_blocking_guard &) = delete;
non_blocking_guard(non_blocking_guard &&) = delete;
auto operator=(const non_blocking_guard &) -> non_blocking_guard & = delete;
auto operator=(non_blocking_guard &&) -> non_blocking_guard & = delete;
explicit non_blocking_guard(boost::asio::ip::tcp::socket &sock_)
: sock(sock_), non_blocking(sock_.non_blocking()) {
boost::system::error_code err;
[[maybe_unused]] auto ret = sock_.non_blocking(true, err);
}
~non_blocking_guard() {
if (not sock.is_open()) {
return;
}
boost::system::error_code err;
[[maybe_unused]] auto ret = sock.non_blocking(non_blocking, err);
}
};
[[nodiscard]] auto is_socket_still_alive(boost::asio::ip::tcp::socket &sock)
-> bool;
void connect_with_deadline(
boost::asio::io_context &io_ctx, boost::asio::ip::tcp::socket &sock,
boost::asio::ip::basic_resolver<boost::asio::ip::tcp>::results_type
&endpoints,
std::chrono::milliseconds deadline);
void read_exact_with_deadline(boost::asio::io_context &io_ctx,
boost::asio::ip::tcp::socket &sock,
boost::asio::mutable_buffer buf,
std::chrono::milliseconds deadline);
void write_all_with_deadline(boost::asio::io_context &io_ctx,
boost::asio::ip::tcp::socket &sock,
boost::asio::mutable_buffer buf,
std::chrono::milliseconds deadline);
} // namespace repertory::comm
#endif // REPERTORY_INCLUDE_COMM_PACKET_COMMON_HPP_

View File

@@ -200,7 +200,7 @@ public:
void encode_top(remote::file_info val);
void encrypt(std::string_view token);
void encrypt(std::string_view token, bool include_size = true);
[[nodiscard]] auto get_size() const -> std::uint32_t {
return static_cast<std::uint32_t>(buffer_.size());

View File

@@ -23,6 +23,7 @@
#define REPERTORY_INCLUDE_COMM_PACKET_PACKET_SERVER_HPP_
#include "comm/packet/client_pool.hpp"
#include "comm/packet/common.hpp"
#include "utils/common.hpp"
using namespace boost::asio;
@@ -61,14 +62,16 @@ private:
std::string client_id;
std::string nonce;
void generate_nonce() { nonce = utils::generate_random_string(256U); }
void generate_nonce() {
nonce = utils::generate_random_string(comm::packet_nonce_size);
}
};
private:
std::string encryption_token_;
closed_callback closed_;
message_handler_callback message_handler_;
io_context io_context_;
mutable io_context io_context_;
std::unique_ptr<std::thread> server_thread_;
std::vector<std::thread> service_threads_;
std::recursive_mutex connection_mutex_;

View File

@@ -0,0 +1,191 @@
/*
Copyright <2018-2025> <scott.e.graves@protonmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#include "comm/packet/common.hpp"
#include "events/event_system.hpp"
#include "events/types/packet_client_timeout.hpp"
namespace repertory::comm {
auto is_socket_still_alive(boost::asio::ip::tcp::socket &sock) -> bool {
if (not sock.is_open()) {
return false;
}
non_blocking_guard guard{sock};
boost::system::error_code err{};
std::array<std::uint8_t, 1> tmp{};
auto available = sock.receive(boost::asio::buffer(tmp),
boost::asio::socket_base::message_peek, err);
if (err == boost::asio::error::would_block ||
err == boost::asio::error::try_again) {
return true;
}
if (err == boost::asio::error::eof ||
err == boost::asio::error::connection_reset ||
err == boost::asio::error::operation_aborted ||
err == boost::asio::error::not_connected ||
err == boost::asio::error::bad_descriptor ||
err == boost::asio::error::network_down) {
return false;
}
if (not err && available == 0) {
return false;
}
if (not err && available > 0) {
return true;
}
return false;
}
template <class op_t>
void run_with_deadline(boost::asio::io_context &io_ctx,
boost::asio::ip::tcp::socket &sock, op_t &&operation,
std::chrono::milliseconds deadline,
std::string_view event_name,
std::string_view function_name) {
deadline = std::max(deadline, std::chrono::milliseconds{250});
bool done = false;
bool timed_out = false;
boost::asio::steady_timer timer{io_ctx};
timer.expires_after(deadline);
timer.async_wait([&done, &sock, &timed_out](auto &&err_) {
if (not err_ && not done) {
timed_out = true;
sock.cancel();
}
});
boost::system::error_code err{};
operation([&done, &err](auto &&err_) {
err = err_;
done = true;
});
io_ctx.restart();
while (not done) {
io_ctx.run_one();
}
timer.cancel();
if (timed_out) {
repertory::event_system::instance().raise<repertory::packet_client_timeout>(
std::string(event_name), std::string(function_name));
throw std::runtime_error(std::string(event_name) + " timed-out");
}
if (err) {
throw std::runtime_error(std::string(event_name) + " failed|err|" +
err.message());
}
}
void connect_with_deadline(
boost::asio::io_context &io_ctx, boost::asio::ip::tcp::socket &sock,
boost::asio::ip::basic_resolver<boost::asio::ip::tcp>::results_type
&endpoints,
std::chrono::milliseconds deadline) {
REPERTORY_USES_FUNCTION_NAME();
run_with_deadline(
io_ctx, sock,
[&sock, &endpoints](auto &&handler) {
boost::asio::async_connect(
sock, endpoints, [handler](auto &&err, auto &&) { handler(err); });
},
deadline, "connect", function_name);
}
void read_exact_with_deadline(boost::asio::io_context &io_ctx,
boost::asio::ip::tcp::socket &sock,
boost::asio::mutable_buffer buf,
std::chrono::milliseconds deadline) {
REPERTORY_USES_FUNCTION_NAME();
auto *base = static_cast<std::uint8_t *>(buf.data());
std::size_t total = buf.size();
std::size_t offset = 0U;
while (offset < total) {
std::size_t bytes_read = 0U;
run_with_deadline(
io_ctx, sock,
[&](auto &&handler) {
sock.async_read_some(
boost::asio::buffer(base + offset, total - offset),
[&bytes_read, handler](auto &&err, auto &&count) {
bytes_read = count;
handler(err);
});
},
deadline, "read", function_name);
if (bytes_read == 0U) {
throw std::runtime_error("0 bytes read");
}
offset += bytes_read;
}
}
void write_all_with_deadline(boost::asio::io_context &io_ctx,
boost::asio::ip::tcp::socket &sock,
boost::asio::mutable_buffer buf,
std::chrono::milliseconds deadline) {
REPERTORY_USES_FUNCTION_NAME();
auto *base = static_cast<std::uint8_t *>(buf.data());
std::size_t total = buf.size();
std::size_t offset = 0U;
while (offset < total) {
std::size_t bytes_written = 0U;
run_with_deadline(
io_ctx, sock,
[&](auto &&handler) {
sock.async_write_some(
boost::asio::buffer(base + offset, total - offset),
[&bytes_written, handler](auto &&err, auto &&count) {
bytes_written = count;
handler(err);
});
},
deadline, "write", function_name);
if (bytes_written == 0U) {
throw std::runtime_error("0 bytes written");
}
offset += bytes_written;
}
}
} // namespace repertory::comm

View File

@@ -518,14 +518,16 @@ void packet::encode_top(remote::file_info val) {
encode_top(&val, sizeof(val), true);
}
void packet::encrypt(std::string_view token) {
void packet::encrypt(std::string_view token, bool include_size) {
REPERTORY_USES_FUNCTION_NAME();
try {
data_buffer result;
utils::encryption::encrypt_data(token, buffer_, result);
buffer_ = std::move(result);
encode_top(static_cast<std::uint32_t>(buffer_.size()));
if (include_size) {
encode_top(static_cast<std::uint32_t>(buffer_.size()));
}
} catch (const std::exception &e) {
utils::error::raise_error(function_name, e, "exception occurred");
}

View File

@@ -21,8 +21,7 @@
*/
#include "comm/packet/packet_client.hpp"
#include "events/event_system.hpp"
#include "events/types/packet_client_timeout.hpp"
#include "comm/packet/common.hpp"
#include "platform/platform.hpp"
#include "types/repertory.hpp"
#include "utils/collection.hpp"
@@ -30,199 +29,7 @@
#include "utils/error_utils.hpp"
#include "version.hpp"
namespace {
constexpr std::uint8_t max_attempts{5U};
struct non_blocking_guard final {
boost::asio::ip::tcp::socket &sock;
bool non_blocking{};
non_blocking_guard(const non_blocking_guard &) = delete;
non_blocking_guard(non_blocking_guard &&) = delete;
auto operator=(const non_blocking_guard &) -> non_blocking_guard & = delete;
auto operator=(non_blocking_guard &&) -> non_blocking_guard & = delete;
explicit non_blocking_guard(boost::asio::ip::tcp::socket &sock_)
: sock(sock_), non_blocking(sock_.non_blocking()) {
boost::system::error_code err;
[[maybe_unused]] auto ret = sock_.non_blocking(true, err);
}
~non_blocking_guard() {
if (not sock.is_open()) {
return;
}
boost::system::error_code err;
[[maybe_unused]] auto ret = sock.non_blocking(non_blocking, err);
}
};
[[nodiscard]] auto is_socket_still_alive(boost::asio::ip::tcp::socket &sock)
-> bool {
if (not sock.is_open()) {
return false;
}
non_blocking_guard guard{sock};
boost::system::error_code err{};
std::array<std::uint8_t, 1> tmp{};
auto available = sock.receive(boost::asio::buffer(tmp),
boost::asio::socket_base::message_peek, err);
if (err == boost::asio::error::would_block ||
err == boost::asio::error::try_again) {
return true;
}
if (err == boost::asio::error::eof ||
err == boost::asio::error::connection_reset ||
err == boost::asio::error::operation_aborted ||
err == boost::asio::error::not_connected ||
err == boost::asio::error::bad_descriptor ||
err == boost::asio::error::network_down) {
return false;
}
if (not err && available == 0) {
return false;
}
if (not err && available > 0) {
return true;
}
return false;
}
template <class op_t>
void run_with_deadline(boost::asio::io_context &io_ctx,
boost::asio::ip::tcp::socket &sock, op_t &&operation,
std::chrono::milliseconds deadline,
std::string_view event_name,
std::string_view function_name) {
deadline = std::max(deadline, std::chrono::milliseconds{250});
bool done = false;
bool timed_out = false;
boost::asio::steady_timer timer{io_ctx};
timer.expires_after(deadline);
timer.async_wait([&done, &sock, &timed_out](auto &&err_) {
if (not err_ && not done) {
timed_out = true;
sock.cancel();
}
});
boost::system::error_code err{};
operation([&done, &err](auto &&err_) {
err = err_;
done = true;
});
io_ctx.restart();
while (not done) {
io_ctx.run_one();
}
timer.cancel();
if (timed_out) {
repertory::event_system::instance().raise<repertory::packet_client_timeout>(
std::string(event_name), std::string(function_name));
throw std::runtime_error(std::string(event_name) + " timed-out");
}
if (err) {
throw std::runtime_error(std::string(event_name) + " failed|err|" +
err.message());
}
}
void connect_with_deadline(boost::asio::io_context &io_ctx,
boost::asio::ip::tcp::socket &sock,
const auto &endpoints,
std::chrono::milliseconds deadline) {
REPERTORY_USES_FUNCTION_NAME();
run_with_deadline(
io_ctx, sock,
[&sock, &endpoints](auto &&handler) {
boost::asio::async_connect(
sock, endpoints, [handler](auto &&err, auto &&) { handler(err); });
},
deadline, "connect", function_name);
}
void read_exact_with_deadline(boost::asio::io_context &io_ctx,
boost::asio::ip::tcp::socket &sock,
boost::asio::mutable_buffer buf,
std::chrono::milliseconds deadline) {
REPERTORY_USES_FUNCTION_NAME();
auto *base = static_cast<std::uint8_t *>(buf.data());
std::size_t total = buf.size();
std::size_t offset = 0U;
while (offset < total) {
std::size_t bytes_read = 0U;
run_with_deadline(
io_ctx, sock,
[&](auto &&handler) {
sock.async_read_some(
boost::asio::buffer(base + offset, total - offset),
[&bytes_read, handler](auto &&err, auto &&count) {
bytes_read = count;
handler(err);
});
},
deadline, "read", function_name);
if (bytes_read == 0U) {
throw std::runtime_error("0 bytes read");
}
offset += bytes_read;
}
}
void write_all_with_deadline(boost::asio::io_context &io_ctx,
boost::asio::ip::tcp::socket &sock,
boost::asio::mutable_buffer buf,
std::chrono::milliseconds deadline) {
REPERTORY_USES_FUNCTION_NAME();
auto *base = static_cast<std::uint8_t *>(buf.data());
std::size_t total = buf.size();
std::size_t offset = 0U;
while (offset < total) {
std::size_t bytes_written = 0U;
run_with_deadline(
io_ctx, sock,
[&](auto &&handler) {
sock.async_write_some(
boost::asio::buffer(base + offset, total - offset),
[&bytes_written, handler](auto &&err, auto &&count) {
bytes_written = count;
handler(err);
});
},
deadline, "write", function_name);
if (bytes_written == 0U) {
throw std::runtime_error("0 bytes written");
}
offset += bytes_written;
}
}
} // namespace
using namespace repertory::comm;
namespace repertory {
packet_client::packet_client(remote::remote_config cfg)
@@ -318,38 +125,21 @@ auto packet_client::handshake(client &cli) const -> bool {
data_buffer buffer;
{
packet tmp;
tmp.encode_top(cli.nonce);
tmp.encode_top(utils::generate_random_string(packet_nonce_size));
tmp.to_buffer(buffer);
}
auto to_read{buffer.size()};
std::uint32_t total_read{};
while ((total_read < to_read) && cli.socket.is_open()) {
auto bytes_read = boost::asio::read(
cli.socket,
boost::asio::buffer(&buffer[total_read], buffer.size() - total_read));
if (bytes_read <= 0) {
throw std::runtime_error("0 bytes read");
}
read_exact_with_deadline(io_context_, cli.socket,
boost::asio::buffer(buffer),
std::chrono::milliseconds(cfg_.recv_timeout_ms));
packet response(buffer);
response.encrypt(cfg_.encryption_token, false);
response.to_buffer(buffer);
total_read += static_cast<std::uint32_t>(bytes_read);
}
if (total_read == to_read) {
packet response(buffer);
response.encrypt(cfg_.encryption_token);
response.to_buffer(buffer);
auto written = boost::asio::write(
cli.socket, boost::asio::buffer(boost::asio::buffer(buffer)));
if (written == to_read) {
return true;
}
throw std::runtime_error("failed to send handshake");
}
throw std::runtime_error("failed to read handshake");
write_all_with_deadline(io_context_, cli.socket,
boost::asio::buffer(buffer),
std::chrono::milliseconds(cfg_.send_timeout_ms));
return true;
} catch (const std::exception &e) {
repertory::utils::error::raise_error(function_name, e, "handlshake failed");
}
@@ -427,7 +217,8 @@ auto packet_client::send(std::string_view method, packet &request,
request.encode_top(std::string{project_get_version()});
for (std::uint8_t retry = 1U;
allow_connections_ && not success && (retry <= max_attempts); ++retry) {
allow_connections_ && not success && (retry <= max_read_attempts);
++retry) {
auto current_client = get_client();
if (current_client) {
try {
@@ -456,7 +247,7 @@ auto packet_client::send(std::string_view method, packet &request,
utils::error::raise_error(function_name, e, "send failed");
close(*current_client);
if (allow_connections_ && (retry < max_attempts)) {
if (allow_connections_ && (retry < max_read_attempts)) {
std::this_thread::sleep_for(1s);
}
}

View File

@@ -21,6 +21,7 @@
*/
#include "comm/packet/packet_server.hpp"
#include "comm/packet/common.hpp"
#include "comm/packet/packet.hpp"
#include "events/event_system.hpp"
#include "events/types/service_start_begin.hpp"
@@ -31,9 +32,10 @@
#include "types/repertory.hpp"
#include "utils/error_utils.hpp"
namespace repertory {
using namespace repertory::comm;
using std::thread;
namespace repertory {
packet_server::packet_server(std::uint16_t port, std::string token,
std::uint8_t pool_size, closed_callback closed,
message_handler_callback message_handler)
@@ -89,49 +91,30 @@ auto packet_server::handshake(std::shared_ptr<connection> conn) const -> bool {
packet request;
request.encode_top(conn->nonce);
request.to_buffer(buffer);
auto to_read{buffer.size()};
auto to_read{buffer.size() + utils::encryption::encryption_header_size};
auto written = boost::asio::write(
conn->socket, boost::asio::buffer(boost::asio::buffer(buffer)));
if (written == to_read) {
conn->buffer.resize(to_read);
write_all_with_deadline(io_context_, conn->socket,
boost::asio::buffer(buffer),
std::chrono::milliseconds(3000U));
std::uint32_t total_read{};
while ((total_read < to_read) && conn->socket.is_open()) {
auto bytes_read = boost::asio::read(
conn->socket,
boost::asio::buffer(&conn->buffer[total_read],
conn->buffer.size() - total_read));
if (bytes_read <= 0) {
throw std::runtime_error("0 bytes read");
conn->buffer.resize(to_read);
read_exact_with_deadline(io_context_, conn->socket,
boost::asio::buffer(conn->buffer),
std::chrono::milliseconds(3000U));
packet response(conn->buffer);
if (response.decrypt(encryption_token_) == 0) {
std::string nonce;
if (response.decode(nonce) == 0) {
if (nonce == conn->nonce) {
conn->generate_nonce();
return true;
}
total_read += static_cast<std::uint32_t>(bytes_read);
}
if (total_read == to_read) {
packet response(conn->buffer);
if (response.decrypt(encryption_token_) == 0) {
std::string nonce;
if (response.decode(nonce) == 0) {
if (nonce == conn->nonce) {
conn->generate_nonce();
return true;
}
throw std::runtime_error("invalid nonce");
}
throw std::runtime_error("invalid nonce");
}
throw std::runtime_error("decryption failed");
}
throw std::runtime_error("invalid handshake");
throw std::runtime_error("invalid nonce");
}
throw std::runtime_error("failed to send handshake");
throw std::runtime_error("decryption failed");
} catch (const std::exception &e) {
repertory::utils::error::raise_error(function_name, e, "handlshake failed");
}
@@ -169,10 +152,16 @@ void packet_server::initialize(const uint16_t &port, uint8_t pool_size) {
}
void packet_server::listen_for_connection(tcp::acceptor &acceptor) {
REPERTORY_USES_FUNCTION_NAME();
auto conn =
std::make_shared<packet_server::connection>(io_context_, acceptor);
acceptor.async_accept(conn->socket, [this, conn](auto &&err) {
on_accept(conn, std::forward<decltype(err)>(err));
try {
on_accept(conn, std::forward<decltype(err)>(err));
} catch (const std::exception &e) {
utils::error::raise_error(function_name, e, "exception occurred");
}
});
}
@@ -331,9 +320,10 @@ void packet_server::send_response(std::shared_ptr<connection> conn,
if (err) {
remove_client(*conn);
utils::error::raise_error(function_name, err.message());
} else {
read_header(conn);
return;
}
read_header(conn);
});
}
} // namespace repertory