Checkpoint the party/runtime stack before share-program and malicious-mode work.
Ship the TLS mesh, composer, Beaver/Yao/leaf MPC, prep/online paths, apps, and docs so the tree is pushable before elevating share_expr, security_mode, and prep resume. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
695f8e84f7
commit
0d22946a0e
1835 changed files with 170291 additions and 2849 deletions
43
include/dpf/net/asio_ns.hpp
Normal file
43
include/dpf/net/asio_ns.hpp
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
/// @file dpf/net/asio_ns.hpp
|
||||
/// @brief Standalone asio as `asio::` inside the networking namespaces.
|
||||
/// @details `dpf/asio.hpp` declares `dpf::asio` (DPF key shipping), which
|
||||
/// would otherwise capture unqualified `asio::` lookups made inside
|
||||
/// `namespace dpf` once both are included, in either order.
|
||||
#ifndef LIBDPF_INCLUDE_DPF_NET_ASIO_NS_HPP__
|
||||
#define LIBDPF_INCLUDE_DPF_NET_ASIO_NS_HPP__
|
||||
|
||||
#include <asio.hpp>
|
||||
|
||||
namespace dpf
|
||||
{
|
||||
namespace net
|
||||
{
|
||||
namespace asio = ::asio;
|
||||
} // namespace net
|
||||
namespace protocol
|
||||
{
|
||||
namespace asio = ::asio;
|
||||
} // namespace protocol
|
||||
namespace session
|
||||
{
|
||||
namespace asio = ::asio;
|
||||
} // namespace session
|
||||
namespace run
|
||||
{
|
||||
namespace asio = ::asio;
|
||||
} // namespace run
|
||||
namespace app
|
||||
{
|
||||
namespace asio = ::asio;
|
||||
} // namespace app
|
||||
namespace async
|
||||
{
|
||||
namespace asio = ::asio;
|
||||
} // namespace async
|
||||
namespace factory
|
||||
{
|
||||
namespace asio = ::asio;
|
||||
} // namespace factory
|
||||
} // namespace dpf
|
||||
|
||||
#endif // LIBDPF_INCLUDE_DPF_NET_ASIO_NS_HPP__
|
||||
876
include/dpf/net/async_round_sink.hpp
Normal file
876
include/dpf/net/async_round_sink.hpp
Normal file
|
|
@ -0,0 +1,876 @@
|
|||
/// @file dpf/net/async_round_sink.hpp
|
||||
/// @brief Event-driven `RoundSink` over an `async_stream_array`.
|
||||
/// @details Both ends first exchange a hello on lane 0 carrying the plan shape
|
||||
/// (rounds, slot widths, lanes, instances, framing) and an epoch. A
|
||||
/// mismatch fails both ends with a message that names the field.
|
||||
/// Unframed lanes carry one round each (round == lane). Framed lanes
|
||||
/// carry `{round, nbytes}` headers; payloads are read straight into the
|
||||
/// round's inbox, and a partial prefix of instances is ready as soon
|
||||
/// as it lands. Every outbound round stays in memory, so after a
|
||||
/// transport error the sink can take a replacement link from
|
||||
/// `sink_options::reconnect`, exchange what each side received, and
|
||||
/// resend only the missing bytes. The schedule above never rewinds.
|
||||
#ifndef LIBDPF_INCLUDE_DPF_NET_ASYNC_ROUND_SINK_HPP__
|
||||
#define LIBDPF_INCLUDE_DPF_NET_ASYNC_ROUND_SINK_HPP__
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <system_error>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "dpf/net/asio_ns.hpp"
|
||||
|
||||
#include "dpf/net/async_stream_array.hpp"
|
||||
#include "dpf/net/policy.hpp"
|
||||
#include "dpf/net/round_lane.hpp"
|
||||
#include "dpf/net/round_sink.hpp"
|
||||
|
||||
namespace dpf
|
||||
{
|
||||
namespace net
|
||||
{
|
||||
|
||||
/// @brief Construction options for `async_round_sink`.
|
||||
struct sink_options
|
||||
{
|
||||
framing_mode framing = framing_mode::automatic;
|
||||
/// Exchange the plan-shape hello. Both ends must agree.
|
||||
bool hello = true;
|
||||
/// Bound on waiting for a full write window to drain.
|
||||
std::chrono::milliseconds drain_timeout{30000};
|
||||
/// Bound on the hello exchange after a reconnect.
|
||||
std::chrono::milliseconds handshake_timeout{30000};
|
||||
/// On a transport error, return a replacement link (the peer must replace
|
||||
/// its end too) or nullptr to fail. Runs on the drive thread.
|
||||
std::function<async_stream_array *(const std::error_code &)> reconnect;
|
||||
unsigned max_reconnects = 3;
|
||||
};
|
||||
|
||||
/// @brief Per-sink counters.
|
||||
struct sink_stats
|
||||
{
|
||||
std::uint64_t bytes_sent = 0;
|
||||
std::uint64_t bytes_received = 0;
|
||||
std::uint64_t flushes = 0;
|
||||
std::uint64_t window_waits = 0;
|
||||
std::uint64_t window_wait_ns = 0;
|
||||
std::uint64_t resumes = 0;
|
||||
std::uint64_t resent_bytes = 0;
|
||||
bool framed = false;
|
||||
std::size_t lanes = 0;
|
||||
};
|
||||
|
||||
/// @brief `RoundSink` whose I/O completes through an `io_context`, never spins.
|
||||
class async_round_sink : public RoundSink
|
||||
{
|
||||
public:
|
||||
async_round_sink(async_stream_array & streams,
|
||||
std::vector<std::size_t> slot_bytes_per_round, std::size_t count = 1,
|
||||
sink_options opt = {})
|
||||
: core_(std::make_shared<core>(streams, streams,
|
||||
std::move(slot_bytes_per_round), count, std::move(opt)))
|
||||
{
|
||||
core_->start();
|
||||
}
|
||||
|
||||
/// @brief Send on `out`, receive on `in` (a ring edge: to the previous
|
||||
/// party, from the next). Both must run on the same `io_context`.
|
||||
/// Reconnect resume needs a single link.
|
||||
async_round_sink(async_stream_array & out, async_stream_array & in,
|
||||
std::vector<std::size_t> slot_bytes_per_round, std::size_t count = 1,
|
||||
sink_options opt = {})
|
||||
: core_(std::make_shared<core>(out, in, std::move(slot_bytes_per_round),
|
||||
count, std::move(opt)))
|
||||
{
|
||||
core_->start();
|
||||
}
|
||||
|
||||
async_round_sink(const async_round_sink &) = delete;
|
||||
async_round_sink & operator=(const async_round_sink &) = delete;
|
||||
|
||||
~async_round_sink() override
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(core_->mu);
|
||||
core_->dead = true;
|
||||
}
|
||||
|
||||
std::size_t count() const noexcept override { return core_->count; }
|
||||
std::size_t rounds() const noexcept override { return core_->slots.size(); }
|
||||
bool framed() const noexcept { return core_->map.framed; }
|
||||
std::size_t lanes() const noexcept { return core_->map.n_lanes; }
|
||||
|
||||
std::size_t slot_bytes(std::uint16_t round) const override
|
||||
{
|
||||
if (round >= core_->slots.size())
|
||||
throw std::out_of_range("async_round_sink round");
|
||||
return core_->slots[round];
|
||||
}
|
||||
|
||||
asio::io_context & context() noexcept { return *core_->io; }
|
||||
async_stream_array & link() noexcept { return *core_->streams; }
|
||||
async_stream_array & in_link() noexcept { return *core_->in; }
|
||||
|
||||
sink_stats stats() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(core_->mu);
|
||||
sink_stats s = core_->st;
|
||||
s.framed = core_->map.framed;
|
||||
s.lanes = core_->map.n_lanes;
|
||||
return s;
|
||||
}
|
||||
|
||||
void submit(std::uint16_t round, std::size_t index,
|
||||
const std::uint8_t * bytes, std::size_t n) override
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(core_->mu);
|
||||
core_->window(round).submit(index, bytes, n);
|
||||
}
|
||||
|
||||
bool peer_ready(std::uint16_t round, std::size_t index) const override
|
||||
{
|
||||
{
|
||||
// Bytes that already arrived stay readable after a link error.
|
||||
std::lock_guard<std::mutex> lock(core_->mu);
|
||||
if (core_->ready_locked(round, index))
|
||||
return true;
|
||||
}
|
||||
core_->raise_if_failed();
|
||||
std::lock_guard<std::mutex> lock(core_->mu);
|
||||
return core_->ready_locked(round, index);
|
||||
}
|
||||
|
||||
void read_peer(std::uint16_t round, std::size_t index, std::uint8_t * out,
|
||||
std::size_t n) const override
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(core_->mu);
|
||||
if (round < core_->slots.size() && core_->ready_locked(round, index))
|
||||
{
|
||||
if (n != core_->slots[round])
|
||||
throw std::invalid_argument("async_round_sink read size");
|
||||
if (n != 0)
|
||||
std::memcpy(out, core_->inbox[round].data() + index * n, n);
|
||||
return;
|
||||
}
|
||||
}
|
||||
core_->raise_if_failed();
|
||||
std::lock_guard<std::mutex> lock(core_->mu);
|
||||
if (round >= core_->slots.size())
|
||||
throw std::out_of_range("async_round_sink read_peer round");
|
||||
if (!core_->ready_locked(round, index))
|
||||
throw std::logic_error("async_round_sink: peer not ready");
|
||||
if (n != core_->slots[round])
|
||||
throw std::invalid_argument("async_round_sink read size");
|
||||
if (n != 0)
|
||||
std::memcpy(out, core_->inbox[round].data() + index * n, n);
|
||||
}
|
||||
|
||||
void flush() override
|
||||
{
|
||||
for (std::uint16_t r = 0; r < core_->slots.size(); ++r)
|
||||
{
|
||||
bool due = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(core_->mu);
|
||||
const auto & w = core_->win[r];
|
||||
due = w.next_unwritten() > w.flushed()
|
||||
|| (core_->slots[r] == 0 && !core_->announced[r]);
|
||||
}
|
||||
if (due)
|
||||
flush_round(r);
|
||||
}
|
||||
}
|
||||
|
||||
void flush_round(std::uint16_t round) override
|
||||
{
|
||||
core_->raise_if_failed();
|
||||
const std::size_t lane = core_->flush_one(round);
|
||||
core_->await_window(lane);
|
||||
}
|
||||
|
||||
void poll() override
|
||||
{
|
||||
core_->restart_if_stopped();
|
||||
core_->io->poll();
|
||||
}
|
||||
|
||||
bool wait_io() override
|
||||
{
|
||||
core_->raise_if_failed();
|
||||
core_->restart_if_stopped();
|
||||
const bool ran = core_->io->run_one() > 0;
|
||||
core_->raise_if_failed();
|
||||
return ran;
|
||||
}
|
||||
|
||||
bool wait_io_for(std::chrono::milliseconds budget) override
|
||||
{
|
||||
core_->raise_if_failed();
|
||||
core_->restart_if_stopped();
|
||||
const bool ran = budget.count() <= 0 ? core_->io->poll_one() > 0
|
||||
: core_->io->run_one_for(budget) > 0;
|
||||
core_->raise_if_failed();
|
||||
return ran;
|
||||
}
|
||||
|
||||
bool can_block() const noexcept override { return true; }
|
||||
|
||||
bool can_send_ahead() const noexcept override
|
||||
{
|
||||
for (std::size_t l = 0; l < core_->map.n_lanes; ++l)
|
||||
{
|
||||
const std::size_t w = core_->streams->lane_window_bytes(l);
|
||||
if (w != 0 && core_->streams->lane_buffered_bytes(l) > w)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::uint64_t progress() const noexcept override
|
||||
{
|
||||
return core_->progress.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
const void * wait_domain() const noexcept override { return core_->io; }
|
||||
|
||||
private:
|
||||
static constexpr std::uint32_t k_magic = 0x48535044u; // 'DPSH'
|
||||
static constexpr std::uint16_t k_version = 1;
|
||||
static constexpr std::size_t k_hello_fixed = 36;
|
||||
static constexpr std::uint32_t k_zero_seen = 1;
|
||||
|
||||
struct core : std::enable_shared_from_this<core>
|
||||
{
|
||||
core(async_stream_array & s, async_stream_array & rx,
|
||||
std::vector<std::size_t> slots_, std::size_t count_, sink_options opt_)
|
||||
: streams(&s),
|
||||
in(&rx),
|
||||
io(&s.context()),
|
||||
count(count_),
|
||||
slots(std::move(slots_)),
|
||||
map(std::min(s.size(), rx.size()), slots.size(), opt_.framing),
|
||||
opt(std::move(opt_))
|
||||
{
|
||||
if (count == 0)
|
||||
throw std::invalid_argument("async_round_sink: count 0");
|
||||
if (s.size() == 0 || rx.size() == 0)
|
||||
throw std::invalid_argument("async_round_sink: empty streams");
|
||||
if (&s.context() != &rx.context())
|
||||
throw std::invalid_argument(
|
||||
"async_round_sink: out and in links need one io_context");
|
||||
if (&s != &rx && opt.reconnect)
|
||||
throw std::invalid_argument(
|
||||
"async_round_sink: reconnect needs a single link");
|
||||
const std::size_t nr = slots.size();
|
||||
win.reserve(nr);
|
||||
inbox.resize(nr);
|
||||
in_filled.assign(nr, 0);
|
||||
zero_seen.assign(nr, false);
|
||||
announced.assign(nr, false);
|
||||
legacy_started.assign(nr, false);
|
||||
legacy_wanted.assign(nr, false);
|
||||
for (std::size_t k = 0; k < nr; ++k)
|
||||
{
|
||||
win.emplace_back(count, slots[k]);
|
||||
inbox[k].assign(count * slots[k], 0);
|
||||
}
|
||||
fingerprint = slots_fingerprint(slots);
|
||||
}
|
||||
|
||||
// --- lifecycle -------------------------------------------------
|
||||
void start()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mu);
|
||||
if (opt.hello)
|
||||
begin_hello_locked();
|
||||
else
|
||||
hello_done_locked();
|
||||
}
|
||||
|
||||
void restart_if_stopped()
|
||||
{
|
||||
if (io->stopped())
|
||||
io->restart();
|
||||
}
|
||||
|
||||
// --- hello ------------------------------------------------------
|
||||
std::shared_ptr<std::vector<std::uint8_t>> build_hello_locked() const
|
||||
{
|
||||
const std::size_t r = slots.size();
|
||||
auto buf = acquire_buffer(k_hello_fixed + 4 * r);
|
||||
auto * p = buf->data();
|
||||
std::memset(p, 0, buf->size());
|
||||
detail::put_u32(p + 0, k_magic);
|
||||
p[4] = static_cast<std::uint8_t>(k_version & 0xffu);
|
||||
p[5] = static_cast<std::uint8_t>(k_version >> 8);
|
||||
p[6] = map.framed ? 1 : 0;
|
||||
detail::put_u32(p + 8, static_cast<std::uint32_t>(r));
|
||||
detail::put_u32(p + 12, static_cast<std::uint32_t>(map.n_lanes));
|
||||
detail::put_u32(p + 16, static_cast<std::uint32_t>(count));
|
||||
detail::put_u32(p + 20, gen);
|
||||
for (int i = 0; i < 8; ++i)
|
||||
p[24 + i] = static_cast<std::uint8_t>((fingerprint >> (8 * i)) & 0xffu);
|
||||
for (std::size_t k = 0; k < r; ++k)
|
||||
{
|
||||
const std::uint32_t got = slots[k] == 0
|
||||
? (zero_seen[k] ? k_zero_seen : 0)
|
||||
: static_cast<std::uint32_t>(in_filled[k]);
|
||||
detail::put_u32(p + k_hello_fixed + 4 * k, got);
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
|
||||
void begin_hello_locked()
|
||||
{
|
||||
hello_ok = false;
|
||||
auto self = shared_from_this();
|
||||
const std::uint32_t g = gen;
|
||||
streams->async_write_owned(0, build_hello_locked(),
|
||||
[self, g](const std::error_code & ec) { self->on_write(g, ec); });
|
||||
hello_in = std::make_shared<std::vector<std::uint8_t>>(
|
||||
k_hello_fixed + 4 * slots.size());
|
||||
auto buf = hello_in;
|
||||
in->async_read(0, buf->data(), buf->size(),
|
||||
[self, g, buf](const std::error_code & ec) {
|
||||
self->on_hello(g, buf, ec);
|
||||
});
|
||||
}
|
||||
|
||||
void on_hello(std::uint32_t g,
|
||||
const std::shared_ptr<std::vector<std::uint8_t>> & buf,
|
||||
const std::error_code & ec)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mu);
|
||||
if (dead || g != gen)
|
||||
return;
|
||||
if (ec)
|
||||
{
|
||||
fail_locked(ec, "hello read", false);
|
||||
return;
|
||||
}
|
||||
const auto * p = buf->data();
|
||||
if (detail::get_u32(p) != k_magic)
|
||||
{
|
||||
fail_locked(std::make_error_code(std::errc::protocol_error),
|
||||
"peer did not send a sink hello (is it an async_round_sink "
|
||||
"or stream_array_sink with hello enabled?)", true);
|
||||
return;
|
||||
}
|
||||
const std::uint16_t ver = static_cast<std::uint16_t>(p[4] | (p[5] << 8));
|
||||
const bool peer_framed = (p[6] & 1) != 0;
|
||||
const std::uint32_t pr = detail::get_u32(p + 8);
|
||||
const std::uint32_t pl = detail::get_u32(p + 12);
|
||||
const std::uint32_t pc = detail::get_u32(p + 16);
|
||||
const std::uint32_t pg = detail::get_u32(p + 20);
|
||||
std::uint64_t pf = 0;
|
||||
for (int i = 0; i < 8; ++i)
|
||||
pf |= static_cast<std::uint64_t>(p[24 + i]) << (8 * i);
|
||||
std::string why;
|
||||
if (ver != k_version)
|
||||
why += " version " + std::to_string(ver) + " vs "
|
||||
+ std::to_string(k_version) + ";";
|
||||
if (pr != slots.size())
|
||||
why += " rounds " + std::to_string(pr) + " vs "
|
||||
+ std::to_string(slots.size()) + ";";
|
||||
else if (pf != fingerprint)
|
||||
why += " slot widths differ;";
|
||||
if (pl != map.n_lanes)
|
||||
why += " lanes " + std::to_string(pl) + " vs "
|
||||
+ std::to_string(map.n_lanes) + ";";
|
||||
if (pc != count)
|
||||
why += " instances " + std::to_string(pc) + " vs "
|
||||
+ std::to_string(count) + ";";
|
||||
if (peer_framed != map.framed)
|
||||
why += std::string(" framing ") + (peer_framed ? "on" : "off")
|
||||
+ " vs " + (map.framed ? "on" : "off") + ";";
|
||||
if (pg != gen)
|
||||
why += " epoch " + std::to_string(pg) + " vs "
|
||||
+ std::to_string(gen) + ";";
|
||||
if (!why.empty())
|
||||
{
|
||||
fail_locked(std::make_error_code(std::errc::protocol_error),
|
||||
"peer sink disagrees (peer vs this side):" + why, true);
|
||||
return;
|
||||
}
|
||||
std::vector<std::uint32_t> peer_got(slots.size());
|
||||
for (std::size_t k = 0; k < slots.size(); ++k)
|
||||
peer_got[k] = detail::get_u32(p + k_hello_fixed + 4 * k);
|
||||
if (gen != 0 && !resend_locked(peer_got))
|
||||
return;
|
||||
hello_done_locked();
|
||||
}
|
||||
|
||||
void hello_done_locked()
|
||||
{
|
||||
hello_ok = true;
|
||||
if (map.framed)
|
||||
{
|
||||
hdr_bufs.clear();
|
||||
for (std::size_t lane = 0; lane < map.n_lanes; ++lane)
|
||||
{
|
||||
hdr_bufs.push_back(
|
||||
std::make_shared<std::array<std::uint8_t, round_lane_hdr::size>>());
|
||||
read_lane_hdr_locked(lane);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (std::size_t r = 0; r < slots.size(); ++r)
|
||||
if (legacy_wanted[r])
|
||||
issue_legacy_locked(static_cast<std::uint16_t>(r));
|
||||
}
|
||||
}
|
||||
|
||||
/// @brief After a reconnect: send what the peer reported missing.
|
||||
bool resend_locked(const std::vector<std::uint32_t> & peer_got)
|
||||
{
|
||||
auto self = shared_from_this();
|
||||
const std::uint32_t g = gen;
|
||||
for (std::size_t k = 0; k < slots.size(); ++k)
|
||||
{
|
||||
const std::size_t sb = slots[k];
|
||||
const auto round = static_cast<std::uint16_t>(k);
|
||||
const std::size_t lane = map.lane(round);
|
||||
if (sb == 0)
|
||||
{
|
||||
if (map.framed && announced[k] && peer_got[k] != k_zero_seen)
|
||||
{
|
||||
auto frame = acquire_buffer(round_lane_hdr::size);
|
||||
round_lane_hdr{round, 0}.pack(frame->data());
|
||||
streams->async_write_owned(lane, std::move(frame),
|
||||
[self, g](const std::error_code & ec) {
|
||||
self->on_write(g, ec);
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const std::size_t sent = win[k].flushed() * sb;
|
||||
const std::size_t got = peer_got[k];
|
||||
if (got > sent || got % sb != 0)
|
||||
{
|
||||
fail_locked(std::make_error_code(std::errc::protocol_error),
|
||||
"resume: peer reports " + std::to_string(got)
|
||||
+ " bytes of round " + std::to_string(k)
|
||||
+ ", this side sent " + std::to_string(sent),
|
||||
true);
|
||||
return false;
|
||||
}
|
||||
if (got == sent)
|
||||
continue;
|
||||
const std::size_t nbytes = sent - got;
|
||||
const std::uint8_t * src = win[k].out_at(got / sb);
|
||||
st.resent_bytes += nbytes;
|
||||
if (map.framed)
|
||||
{
|
||||
auto frame = acquire_buffer(round_lane_hdr::size + nbytes);
|
||||
round_lane_hdr{round, static_cast<std::uint32_t>(nbytes)}.pack(
|
||||
frame->data());
|
||||
std::memcpy(frame->data() + round_lane_hdr::size, src, nbytes);
|
||||
streams->async_write_owned(lane, std::move(frame),
|
||||
[self, g](const std::error_code & ec) {
|
||||
self->on_write(g, ec);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
streams->async_write(lane, src, nbytes,
|
||||
[self, g](const std::error_code & ec) {
|
||||
self->on_write(g, ec);
|
||||
});
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// --- errors and resume ------------------------------------------
|
||||
void fail_locked(const std::error_code & ec, const std::string & what,
|
||||
bool is_fatal)
|
||||
{
|
||||
if (fail)
|
||||
return;
|
||||
fail = ec;
|
||||
fail_what = "async_round_sink: " + what;
|
||||
fatal = is_fatal;
|
||||
}
|
||||
|
||||
void on_write(std::uint32_t g, const std::error_code & ec)
|
||||
{
|
||||
if (!ec)
|
||||
return;
|
||||
std::lock_guard<std::mutex> lock(mu);
|
||||
if (dead || g != gen)
|
||||
return;
|
||||
fail_locked(ec, "write", false);
|
||||
}
|
||||
|
||||
void raise_if_failed()
|
||||
{
|
||||
std::error_code ec;
|
||||
std::string what;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mu);
|
||||
if (!fail)
|
||||
return;
|
||||
if (fatal || !opt.reconnect || reconnects >= opt.max_reconnects)
|
||||
throw std::system_error(fail, fail_what);
|
||||
ec = fail;
|
||||
what = fail_what;
|
||||
}
|
||||
async_stream_array * next = nullptr;
|
||||
try
|
||||
{
|
||||
next = opt.reconnect(ec);
|
||||
}
|
||||
catch (const std::exception & e)
|
||||
{
|
||||
throw std::system_error(ec, what + "; reconnect failed: " + e.what());
|
||||
}
|
||||
if (next == nullptr)
|
||||
throw std::system_error(ec, what + "; no replacement link");
|
||||
resume(*next);
|
||||
}
|
||||
|
||||
void resume(async_stream_array & next)
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mu);
|
||||
if (next.size() < map.n_lanes)
|
||||
throw std::invalid_argument(
|
||||
"async_round_sink resume: replacement link has fewer lanes");
|
||||
++gen;
|
||||
++reconnects;
|
||||
++st.resumes;
|
||||
fail.clear();
|
||||
fail_what.clear();
|
||||
fatal = false;
|
||||
streams = &next;
|
||||
in = &next;
|
||||
io = &next.context();
|
||||
for (std::size_t r = 0; r < slots.size(); ++r)
|
||||
{
|
||||
if (legacy_started[r] && in_filled[r] < count * slots[r])
|
||||
{
|
||||
legacy_started[r] = false;
|
||||
legacy_wanted[r] = true;
|
||||
}
|
||||
}
|
||||
begin_hello_locked();
|
||||
}
|
||||
const auto deadline = std::chrono::steady_clock::now()
|
||||
+ opt.handshake_timeout;
|
||||
for (;;)
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mu);
|
||||
if (hello_ok)
|
||||
return;
|
||||
if (fail)
|
||||
throw std::system_error(fail, fail_what + " (during resume)");
|
||||
}
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (now >= deadline)
|
||||
throw std::system_error(std::make_error_code(std::errc::timed_out),
|
||||
"async_round_sink: resume hello timed out");
|
||||
restart_if_stopped();
|
||||
io->run_one_for(std::min<std::chrono::milliseconds>(
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
deadline - now),
|
||||
std::chrono::milliseconds(10)));
|
||||
}
|
||||
}
|
||||
|
||||
// --- reads ------------------------------------------------------
|
||||
/// @brief Every round's peer bytes (and zero-width announcements) are in.
|
||||
bool complete_locked() const
|
||||
{
|
||||
for (std::size_t r = 0; r < slots.size(); ++r)
|
||||
{
|
||||
const std::size_t sb = slots[r];
|
||||
if (sb == 0 ? (map.framed && !zero_seen[r])
|
||||
: in_filled[r] < count * sb)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ready_locked(std::uint16_t round, std::size_t index)
|
||||
{
|
||||
if (round >= slots.size())
|
||||
return false;
|
||||
const std::size_t sb = slots[round];
|
||||
if (map.framed)
|
||||
{
|
||||
if (sb == 0)
|
||||
return zero_seen[round];
|
||||
return in_filled[round] >= (index + 1) * sb;
|
||||
}
|
||||
if (sb == 0)
|
||||
return true;
|
||||
if (!legacy_started[round])
|
||||
{
|
||||
if (hello_ok)
|
||||
issue_legacy_locked(round);
|
||||
else
|
||||
legacy_wanted[round] = true;
|
||||
}
|
||||
return in_filled[round] == count * sb;
|
||||
}
|
||||
|
||||
void issue_legacy_locked(std::uint16_t round)
|
||||
{
|
||||
if (legacy_started[round])
|
||||
return;
|
||||
legacy_started[round] = true;
|
||||
legacy_wanted[round] = false;
|
||||
const std::size_t bytes = count * slots[round];
|
||||
if (bytes == 0)
|
||||
{
|
||||
in_filled[round] = 0;
|
||||
return;
|
||||
}
|
||||
auto self = shared_from_this();
|
||||
const std::uint32_t g = gen;
|
||||
this->in->async_read(round, inbox[round].data(), bytes,
|
||||
[self, g, round, bytes](const std::error_code & ec) {
|
||||
std::lock_guard<std::mutex> lock(self->mu);
|
||||
if (self->dead || g != self->gen)
|
||||
return;
|
||||
if (ec)
|
||||
{
|
||||
self->fail_locked(ec, "read round "
|
||||
+ std::to_string(round), false);
|
||||
return;
|
||||
}
|
||||
self->in_filled[round] = bytes;
|
||||
self->st.bytes_received += bytes;
|
||||
self->progress.fetch_add(bytes, std::memory_order_relaxed);
|
||||
});
|
||||
}
|
||||
|
||||
void read_lane_hdr_locked(std::size_t lane)
|
||||
{
|
||||
auto self = shared_from_this();
|
||||
const std::uint32_t g = gen;
|
||||
auto buf = hdr_bufs[lane];
|
||||
in->async_read(lane, buf->data(), buf->size(),
|
||||
[self, g, lane, buf](const std::error_code & ec) {
|
||||
self->on_lane_hdr(g, lane, buf, ec);
|
||||
});
|
||||
}
|
||||
|
||||
void on_lane_hdr(std::uint32_t g, std::size_t lane,
|
||||
const std::shared_ptr<std::array<std::uint8_t, round_lane_hdr::size>> & buf,
|
||||
const std::error_code & ec)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mu);
|
||||
if (dead || g != gen)
|
||||
return;
|
||||
if (ec)
|
||||
{
|
||||
// A peer that closes after its last frame finished cleanly.
|
||||
if (!complete_locked())
|
||||
fail_locked(ec, "read lane " + std::to_string(lane), false);
|
||||
return;
|
||||
}
|
||||
const auto hdr = round_lane_hdr::unpack(buf->data());
|
||||
if (hdr.round >= slots.size() || map.lane(hdr.round) != lane)
|
||||
{
|
||||
fail_locked(std::make_error_code(std::errc::protocol_error),
|
||||
"frame for round " + std::to_string(hdr.round) + " on lane "
|
||||
+ std::to_string(lane),
|
||||
true);
|
||||
return;
|
||||
}
|
||||
const std::size_t sb = slots[hdr.round];
|
||||
const std::size_t cap = count * sb;
|
||||
if (hdr.nbytes == 0)
|
||||
{
|
||||
if (sb == 0 && !zero_seen[hdr.round])
|
||||
{
|
||||
zero_seen[hdr.round] = true;
|
||||
progress.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
read_lane_hdr_locked(lane);
|
||||
return;
|
||||
}
|
||||
if (sb == 0 || hdr.nbytes % sb != 0
|
||||
|| in_filled[hdr.round] + hdr.nbytes > cap)
|
||||
{
|
||||
fail_locked(std::make_error_code(std::errc::message_size),
|
||||
"round " + std::to_string(hdr.round) + " frame of "
|
||||
+ std::to_string(hdr.nbytes) + " bytes does not fit",
|
||||
true);
|
||||
return;
|
||||
}
|
||||
const std::uint16_t round = hdr.round;
|
||||
const std::size_t nbytes = hdr.nbytes;
|
||||
auto self = shared_from_this();
|
||||
in->async_read(lane, inbox[round].data() + in_filled[round], nbytes,
|
||||
[self, g, lane, round, nbytes](const std::error_code & xec) {
|
||||
std::lock_guard<std::mutex> lk(self->mu);
|
||||
if (self->dead || g != self->gen)
|
||||
return;
|
||||
if (xec)
|
||||
{
|
||||
self->fail_locked(xec, "read round "
|
||||
+ std::to_string(round), false);
|
||||
return;
|
||||
}
|
||||
self->in_filled[round] += nbytes;
|
||||
self->st.bytes_received += nbytes;
|
||||
self->progress.fetch_add(nbytes, std::memory_order_relaxed);
|
||||
self->read_lane_hdr_locked(lane);
|
||||
});
|
||||
}
|
||||
|
||||
// --- writes -----------------------------------------------------
|
||||
round_window & window(std::uint16_t round)
|
||||
{
|
||||
if (round >= win.size())
|
||||
throw std::out_of_range("async_round_sink round");
|
||||
return win[round];
|
||||
}
|
||||
|
||||
std::size_t flush_one(std::uint16_t round)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mu);
|
||||
auto & w = window(round);
|
||||
std::size_t begin = 0;
|
||||
std::size_t nslots = 0;
|
||||
const std::uint8_t * pend = w.pending_out(begin, nslots);
|
||||
const std::size_t sb = slots[round];
|
||||
const std::size_t nbytes = nslots * sb;
|
||||
const std::size_t lane = map.lane(round);
|
||||
auto self = shared_from_this();
|
||||
const std::uint32_t g = gen;
|
||||
auto done = [self, g](const std::error_code & ec) {
|
||||
self->on_write(g, ec);
|
||||
};
|
||||
if (map.framed)
|
||||
{
|
||||
if (nslots != 0 || (sb == 0 && !announced[round]))
|
||||
{
|
||||
auto frame = acquire_buffer(round_lane_hdr::size + nbytes);
|
||||
round_lane_hdr{round, static_cast<std::uint32_t>(nbytes)}.pack(
|
||||
frame->data());
|
||||
if (nbytes != 0)
|
||||
std::memcpy(frame->data() + round_lane_hdr::size, pend,
|
||||
nbytes);
|
||||
streams->async_write_owned(lane, std::move(frame), done);
|
||||
if (sb == 0)
|
||||
announced[round] = true;
|
||||
++st.flushes;
|
||||
st.bytes_sent += nbytes;
|
||||
}
|
||||
if (nslots != 0)
|
||||
w.mark_flushed(nslots);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (nslots != 0 && sb != 0)
|
||||
{
|
||||
streams->async_write(lane, pend, nbytes, done);
|
||||
++st.flushes;
|
||||
st.bytes_sent += nbytes;
|
||||
}
|
||||
if (nslots != 0)
|
||||
w.mark_flushed(nslots);
|
||||
if (!legacy_started[round])
|
||||
{
|
||||
if (hello_ok)
|
||||
issue_legacy_locked(round);
|
||||
else
|
||||
legacy_wanted[round] = true;
|
||||
}
|
||||
}
|
||||
return lane;
|
||||
}
|
||||
|
||||
/// @brief Stall the producer while lane `lane`'s window is full.
|
||||
void await_window(std::size_t lane)
|
||||
{
|
||||
const std::size_t w = streams->lane_window_bytes(lane);
|
||||
if (w == 0 || streams->lane_buffered_bytes(lane) <= w)
|
||||
return;
|
||||
const auto t0 = std::chrono::steady_clock::now();
|
||||
auto since = t0;
|
||||
std::size_t last = streams->lane_buffered_bytes(lane);
|
||||
for (;;)
|
||||
{
|
||||
raise_if_failed();
|
||||
const std::size_t now_buf = streams->lane_buffered_bytes(lane);
|
||||
if (now_buf <= streams->lane_window_bytes(lane))
|
||||
break;
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (now_buf < last)
|
||||
{
|
||||
last = now_buf;
|
||||
since = now;
|
||||
}
|
||||
else if (now - since > opt.drain_timeout)
|
||||
{
|
||||
throw std::runtime_error("async_round_sink: lane "
|
||||
+ std::to_string(lane) + " did not drain for "
|
||||
+ std::to_string(opt.drain_timeout.count()) + " ms ("
|
||||
+ std::to_string(now_buf) + " bytes buffered, window "
|
||||
+ std::to_string(w) + ")");
|
||||
}
|
||||
restart_if_stopped();
|
||||
io->run_one_for(std::chrono::milliseconds(10));
|
||||
}
|
||||
const auto dt = std::chrono::steady_clock::now() - t0;
|
||||
std::lock_guard<std::mutex> lock(mu);
|
||||
++st.window_waits;
|
||||
st.window_wait_ns += static_cast<std::uint64_t>(
|
||||
std::chrono::duration_cast<std::chrono::nanoseconds>(dt).count());
|
||||
}
|
||||
|
||||
async_stream_array * streams = nullptr;
|
||||
async_stream_array * in = nullptr;
|
||||
asio::io_context * io = nullptr;
|
||||
std::size_t count = 1;
|
||||
std::vector<std::size_t> slots;
|
||||
round_lane_map map;
|
||||
sink_options opt;
|
||||
std::uint64_t fingerprint = 0;
|
||||
|
||||
std::mutex mu;
|
||||
std::vector<round_window> win;
|
||||
std::vector<std::vector<std::uint8_t>> inbox;
|
||||
std::vector<std::size_t> in_filled;
|
||||
std::vector<bool> zero_seen;
|
||||
std::vector<bool> announced;
|
||||
std::vector<bool> legacy_started;
|
||||
std::vector<bool> legacy_wanted;
|
||||
std::vector<std::shared_ptr<std::array<std::uint8_t, round_lane_hdr::size>>>
|
||||
hdr_bufs;
|
||||
std::shared_ptr<std::vector<std::uint8_t>> hello_in;
|
||||
std::uint32_t gen = 0;
|
||||
bool hello_ok = false;
|
||||
std::error_code fail;
|
||||
std::string fail_what;
|
||||
bool fatal = false;
|
||||
unsigned reconnects = 0;
|
||||
bool dead = false;
|
||||
std::atomic<std::uint64_t> progress{0};
|
||||
sink_stats st;
|
||||
};
|
||||
|
||||
std::shared_ptr<core> core_;
|
||||
};
|
||||
|
||||
} // namespace net
|
||||
} // namespace dpf
|
||||
|
||||
#endif // LIBDPF_INCLUDE_DPF_NET_ASYNC_ROUND_SINK_HPP__
|
||||
773
include/dpf/net/async_sctp_stream_array.hpp
Normal file
773
include/dpf/net/async_sctp_stream_array.hpp
Normal file
|
|
@ -0,0 +1,773 @@
|
|||
/// @file dpf/net/async_sctp_stream_array.hpp
|
||||
/// @brief Truly asynchronous SCTP-backed indexed byte streams (Linux/libsctp).
|
||||
/// @details One SCTP association carries every lane: index `i` maps to SCTP
|
||||
/// stream `i`. Receive is head-of-line free across streams. Writes are
|
||||
/// split into `wire_policy::chunk_bytes` messages and sent round-robin
|
||||
/// across streams, so a large write on one stream does not hold small
|
||||
/// writes on another behind it. The window covers the association.
|
||||
///
|
||||
/// Platform contract:
|
||||
/// * Linux with `<netinet/sctp.h>` (libsctp) → real backend, needs `-lsctp`.
|
||||
/// * Everything else → the class exists, every constructor throws.
|
||||
#ifndef LIBDPF_INCLUDE_DPF_NET_ASYNC_SCTP_STREAM_ARRAY_HPP__
|
||||
#define LIBDPF_INCLUDE_DPF_NET_ASYNC_SCTP_STREAM_ARRAY_HPP__
|
||||
|
||||
#if !defined(DPF_HAS_LIBSCTP)
|
||||
# if defined(__linux__) && defined(__has_include)
|
||||
# if __has_include(<netinet/sctp.h>)
|
||||
# define DPF_HAS_LIBSCTP 1
|
||||
# else
|
||||
# define DPF_HAS_LIBSCTP 0
|
||||
# endif
|
||||
# else
|
||||
# define DPF_HAS_LIBSCTP 0
|
||||
# endif
|
||||
#endif
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <deque>
|
||||
#include <exception>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <system_error>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "dpf/net/asio_ns.hpp"
|
||||
|
||||
#include "hedley/hedley.h"
|
||||
|
||||
#include "dpf/net/async_stream_array.hpp"
|
||||
#include "dpf/net/connect.hpp"
|
||||
#include "dpf/net/policy.hpp"
|
||||
|
||||
#if DPF_HAS_LIBSCTP
|
||||
# include <arpa/inet.h>
|
||||
# include <fcntl.h>
|
||||
# include <netinet/in.h>
|
||||
# include <netinet/sctp.h>
|
||||
# include <sys/socket.h>
|
||||
# include <unistd.h>
|
||||
# include <cerrno>
|
||||
#endif
|
||||
|
||||
namespace dpf
|
||||
{
|
||||
namespace net
|
||||
{
|
||||
|
||||
/// @brief True when this build has a real SCTP backend.
|
||||
inline constexpr bool sctp_available() noexcept
|
||||
{
|
||||
return DPF_HAS_LIBSCTP != 0;
|
||||
}
|
||||
|
||||
#if DPF_HAS_LIBSCTP
|
||||
|
||||
namespace detail
|
||||
{
|
||||
|
||||
/// @brief Stream counts, per-message `sinfo`, and socket options.
|
||||
inline void sctp_configure(int fd, std::size_t nstreams,
|
||||
const socket_options & o = {})
|
||||
{
|
||||
struct sctp_initmsg im;
|
||||
std::memset(&im, 0, sizeof(im));
|
||||
im.sinit_num_ostreams = static_cast<std::uint16_t>(nstreams);
|
||||
im.sinit_max_instreams = static_cast<std::uint16_t>(nstreams);
|
||||
(void)::setsockopt(fd, IPPROTO_SCTP, SCTP_INITMSG, &im, sizeof(im));
|
||||
|
||||
struct sctp_event_subscribe ev;
|
||||
std::memset(&ev, 0, sizeof(ev));
|
||||
ev.sctp_data_io_event = 1;
|
||||
(void)::setsockopt(fd, IPPROTO_SCTP, SCTP_EVENTS, &ev, sizeof(ev));
|
||||
|
||||
const int nodelay = o.no_delay ? 1 : 0;
|
||||
(void)::setsockopt(fd, IPPROTO_SCTP, SCTP_NODELAY, &nodelay, sizeof(nodelay));
|
||||
if (o.send_buffer > 0)
|
||||
(void)::setsockopt(fd, SOL_SOCKET, SO_SNDBUF, &o.send_buffer,
|
||||
sizeof(o.send_buffer));
|
||||
if (o.recv_buffer > 0)
|
||||
(void)::setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &o.recv_buffer,
|
||||
sizeof(o.recv_buffer));
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
/// @brief N lanes over one SCTP association, fully overlapped.
|
||||
class async_sctp_stream_array final : public async_stream_array
|
||||
{
|
||||
public:
|
||||
/// @brief Adopt a connected one-to-one SCTP fd.
|
||||
async_sctp_stream_array(asio::io_context & io, int fd, std::size_t nstreams,
|
||||
const wire_policy & pol = {})
|
||||
{
|
||||
if (nstreams == 0)
|
||||
throw std::invalid_argument("async_sctp_stream_array empty");
|
||||
if (nstreams > 0xffffu)
|
||||
throw std::invalid_argument("async_sctp_stream_array: stream ids are u16");
|
||||
pol.validate();
|
||||
detail::sctp_configure(fd, nstreams, pol.socket);
|
||||
impl_ = std::make_shared<impl>(io, fd, nstreams, pol);
|
||||
impl_->start();
|
||||
}
|
||||
|
||||
async_sctp_stream_array(const async_sctp_stream_array &) = delete;
|
||||
async_sctp_stream_array & operator=(const async_sctp_stream_array &) = delete;
|
||||
|
||||
~async_sctp_stream_array() override
|
||||
{
|
||||
if (impl_)
|
||||
impl_->close_graceful();
|
||||
}
|
||||
|
||||
std::size_t size() const noexcept override { return impl_->n; }
|
||||
asio::io_context & context() noexcept override { return impl_->io; }
|
||||
|
||||
void async_write(std::size_t i, const void * src, std::size_t n,
|
||||
async_handler h) override
|
||||
{
|
||||
impl_->write(i, n == 0 ? nullptr : copy_buffer(src, n), std::move(h));
|
||||
}
|
||||
|
||||
void async_write_owned(std::size_t i,
|
||||
std::shared_ptr<std::vector<std::uint8_t>> buf, async_handler h) override
|
||||
{
|
||||
impl_->write(i, std::move(buf), std::move(h));
|
||||
}
|
||||
|
||||
void async_read(std::size_t i, void * dst, std::size_t n,
|
||||
async_handler h) override
|
||||
{
|
||||
impl_->read(i, dst, n, std::move(h));
|
||||
}
|
||||
|
||||
std::size_t buffered_bytes() const noexcept override
|
||||
{
|
||||
return impl_->buffered.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
std::size_t window_bytes() const noexcept override
|
||||
{
|
||||
return impl_->window.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void set_window_bytes(std::size_t bytes) override
|
||||
{
|
||||
impl_->window.store(bytes, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
stream_stats stats() const override { return impl_->stats(); }
|
||||
|
||||
void close() noexcept override
|
||||
{
|
||||
impl_->close(asio::error::operation_aborted);
|
||||
}
|
||||
|
||||
private:
|
||||
struct chunk
|
||||
{
|
||||
std::shared_ptr<std::vector<std::uint8_t>> payload;
|
||||
std::size_t off = 0;
|
||||
std::size_t len = 0;
|
||||
std::shared_ptr<detail::write_op> op;
|
||||
};
|
||||
|
||||
struct impl : std::enable_shared_from_this<impl>
|
||||
{
|
||||
impl(asio::io_context & io_, int fd, std::size_t nstreams,
|
||||
const wire_policy & pol_)
|
||||
: io(io_),
|
||||
sd(io_),
|
||||
strand(asio::make_strand(io_)),
|
||||
n(nstreams),
|
||||
pol(pol_),
|
||||
inbox(nstreams),
|
||||
partial(nstreams),
|
||||
wait(nstreams),
|
||||
outq(nstreams),
|
||||
rbuf(std::max<std::size_t>(pol_.chunk_bytes, std::size_t{1} << 16)),
|
||||
window(pol_.window_bytes)
|
||||
{
|
||||
const int fl = ::fcntl(fd, F_GETFL, 0);
|
||||
if (fl >= 0)
|
||||
(void)::fcntl(fd, F_SETFL, fl | O_NONBLOCK);
|
||||
sd.assign(fd);
|
||||
}
|
||||
|
||||
void check(std::size_t i) const
|
||||
{
|
||||
if (i >= n)
|
||||
throw std::out_of_range("async_sctp_stream_array index "
|
||||
+ std::to_string(i));
|
||||
}
|
||||
|
||||
void start()
|
||||
{
|
||||
auto self = shared_from_this();
|
||||
asio::post(strand, [self] { self->arm_read(); });
|
||||
}
|
||||
|
||||
// --- reads ---------------------------------------------------------
|
||||
void read(std::size_t i, void * dst, std::size_t nbytes, async_handler h)
|
||||
{
|
||||
check(i);
|
||||
std::lock_guard<std::mutex> lock(mu);
|
||||
if (fail)
|
||||
{
|
||||
detail::post_handler(io, std::move(h), fail);
|
||||
return;
|
||||
}
|
||||
auto & w = wait[i];
|
||||
if (w.active)
|
||||
throw std::logic_error(
|
||||
"async_sctp_stream_array: overlapping read on stream "
|
||||
+ std::to_string(i));
|
||||
const std::size_t k = inbox[i].take(dst, nbytes, pol.compact_bytes);
|
||||
if (k == nbytes)
|
||||
{
|
||||
detail::post_handler(io, std::move(h), {});
|
||||
return;
|
||||
}
|
||||
w.active = true;
|
||||
w.dst = dst;
|
||||
w.n = nbytes;
|
||||
w.filled = k;
|
||||
w.h = std::move(h);
|
||||
}
|
||||
|
||||
void satisfy_locked(std::size_t i)
|
||||
{
|
||||
auto & w = wait[i];
|
||||
if (!w.active)
|
||||
return;
|
||||
w.filled += inbox[i].take(static_cast<std::uint8_t *>(w.dst) + w.filled,
|
||||
w.n - w.filled, pol.compact_bytes);
|
||||
if (w.filled == w.n)
|
||||
{
|
||||
w.active = false;
|
||||
detail::post_handler(io, std::move(w.h), {});
|
||||
}
|
||||
}
|
||||
|
||||
void arm_read()
|
||||
{
|
||||
auto self = shared_from_this();
|
||||
sd.async_wait(asio::posix::stream_descriptor::wait_read,
|
||||
asio::bind_executor(strand, [self](const std::error_code & ec) {
|
||||
self->on_readable(ec);
|
||||
}));
|
||||
}
|
||||
|
||||
void on_readable(const std::error_code & ec)
|
||||
{
|
||||
if (ec)
|
||||
{
|
||||
deliver_error(ec);
|
||||
return;
|
||||
}
|
||||
for (;;)
|
||||
{
|
||||
struct sctp_sndrcvinfo sinfo;
|
||||
std::memset(&sinfo, 0, sizeof(sinfo));
|
||||
int flags = 0;
|
||||
const ssize_t r = ::sctp_recvmsg(sd.native_handle(), rbuf.data(),
|
||||
rbuf.size(), nullptr, nullptr, &sinfo, &flags);
|
||||
if (r < 0)
|
||||
{
|
||||
if (errno == EAGAIN || errno == EWOULDBLOCK)
|
||||
break;
|
||||
deliver_error(std::error_code(errno, std::generic_category()));
|
||||
return;
|
||||
}
|
||||
if (r == 0)
|
||||
{
|
||||
deliver_error(asio::error::eof);
|
||||
return;
|
||||
}
|
||||
if (flags & MSG_NOTIFICATION)
|
||||
continue;
|
||||
const std::size_t stream = sinfo.sinfo_stream;
|
||||
if (stream >= n)
|
||||
{
|
||||
deliver_error(std::make_error_code(std::errc::protocol_error));
|
||||
return;
|
||||
}
|
||||
bool too_big = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mu);
|
||||
if (closed)
|
||||
return;
|
||||
auto & part = partial[stream];
|
||||
part.insert(part.end(), rbuf.begin(), rbuf.begin() + r);
|
||||
if (part.size() > pol.max_frame)
|
||||
too_big = true;
|
||||
else if ((flags & MSG_EOR) != 0)
|
||||
{
|
||||
counters.read(part.size(), part.size(), 1);
|
||||
auto & box = inbox[stream].bytes;
|
||||
box.insert(box.end(), part.begin(), part.end());
|
||||
part.clear();
|
||||
satisfy_locked(stream);
|
||||
}
|
||||
}
|
||||
if (too_big)
|
||||
{
|
||||
deliver_error(std::make_error_code(std::errc::message_size));
|
||||
return;
|
||||
}
|
||||
}
|
||||
arm_read();
|
||||
}
|
||||
|
||||
void fail_waiters_locked(const std::error_code & ec)
|
||||
{
|
||||
for (auto & w : wait)
|
||||
{
|
||||
if (!w.active)
|
||||
continue;
|
||||
w.active = false;
|
||||
detail::post_handler(io, std::move(w.h), ec);
|
||||
}
|
||||
}
|
||||
|
||||
void deliver_error(const std::error_code & ec)
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mu);
|
||||
if (!fail)
|
||||
fail = ec;
|
||||
fail_waiters_locked(fail);
|
||||
}
|
||||
auto self = shared_from_this();
|
||||
asio::post(strand, [self, ec] { self->fail_queued(ec); });
|
||||
}
|
||||
|
||||
void close(const std::error_code & ec)
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mu);
|
||||
if (!fail)
|
||||
fail = ec;
|
||||
if (closed && aborted)
|
||||
return;
|
||||
closed = true;
|
||||
aborted = true;
|
||||
fail_waiters_locked(ec);
|
||||
}
|
||||
auto self = shared_from_this();
|
||||
asio::post(strand, [self, ec] {
|
||||
std::error_code e;
|
||||
self->sd.close(e);
|
||||
self->fail_queued(ec);
|
||||
});
|
||||
}
|
||||
|
||||
void close_graceful()
|
||||
{
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mu);
|
||||
if (closed)
|
||||
return;
|
||||
closed = true;
|
||||
fail_waiters_locked(asio::error::operation_aborted);
|
||||
}
|
||||
auto self = shared_from_this();
|
||||
asio::post(strand, [self] {
|
||||
self->draining = true;
|
||||
if (!self->writing)
|
||||
{
|
||||
std::error_code e;
|
||||
self->sd.close(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
stream_stats stats() const
|
||||
{
|
||||
stream_stats s;
|
||||
counters.fill(s);
|
||||
s.buffered = buffered.load(std::memory_order_relaxed);
|
||||
std::lock_guard<std::mutex> lock(mu);
|
||||
for (const auto & b : inbox)
|
||||
s.unread += b.avail();
|
||||
s.error = fail;
|
||||
s.closed = closed;
|
||||
return s;
|
||||
}
|
||||
|
||||
// --- writes (strand) ----------------------------------------------
|
||||
void write(std::size_t i, std::shared_ptr<std::vector<std::uint8_t>> payload,
|
||||
async_handler h)
|
||||
{
|
||||
check(i);
|
||||
const std::size_t len = payload ? payload->size() : 0;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mu);
|
||||
if (fail || closed)
|
||||
{
|
||||
detail::post_handler(io, std::move(h),
|
||||
fail ? fail : asio::error::operation_aborted);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (len == 0)
|
||||
{
|
||||
detail::post_handler(io, std::move(h), {});
|
||||
return;
|
||||
}
|
||||
const auto pieces = detail::split_chunks(len, pol.chunk_bytes);
|
||||
auto op = std::make_shared<detail::write_op>();
|
||||
op->h = std::move(h);
|
||||
op->left = pieces.size();
|
||||
std::vector<chunk> cs;
|
||||
cs.reserve(pieces.size());
|
||||
for (const auto & pc : pieces)
|
||||
cs.push_back(chunk{payload, pc.first, pc.second, op});
|
||||
buffered.fetch_add(len, std::memory_order_relaxed);
|
||||
auto self = shared_from_this();
|
||||
asio::post(strand, [self, i, cs = std::move(cs)]() mutable {
|
||||
std::error_code ec;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(self->mu);
|
||||
ec = self->fail;
|
||||
}
|
||||
if (ec)
|
||||
{
|
||||
for (auto & c : cs)
|
||||
self->finish_chunk(c, ec);
|
||||
return;
|
||||
}
|
||||
for (auto & c : cs)
|
||||
self->outq[i].push_back(std::move(c));
|
||||
if (!self->writing)
|
||||
self->write_next();
|
||||
});
|
||||
}
|
||||
|
||||
void finish_chunk(chunk & c, const std::error_code & ec)
|
||||
{
|
||||
buffered.fetch_sub(std::min(buffered.load(std::memory_order_relaxed),
|
||||
c.len),
|
||||
std::memory_order_relaxed);
|
||||
auto & op = *c.op;
|
||||
if (ec && !op.ec)
|
||||
op.ec = ec;
|
||||
if (--op.left == 0)
|
||||
detail::post_handler(io, std::move(op.h), op.ec);
|
||||
}
|
||||
|
||||
bool pick(std::size_t & lane)
|
||||
{
|
||||
for (std::size_t k = 0; k < n; ++k)
|
||||
{
|
||||
const std::size_t l = (rr + k) % n;
|
||||
if (!outq[l].empty())
|
||||
{
|
||||
lane = l;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void write_next()
|
||||
{
|
||||
std::size_t lane = 0;
|
||||
if (!pick(lane))
|
||||
{
|
||||
writing = false;
|
||||
if (draining)
|
||||
{
|
||||
std::error_code e;
|
||||
sd.close(e);
|
||||
}
|
||||
return;
|
||||
}
|
||||
writing = true;
|
||||
auto & front = outq[lane].front();
|
||||
const ssize_t r = ::sctp_sendmsg(sd.native_handle(),
|
||||
front.payload->data() + front.off, front.len, nullptr, 0, 0, 0,
|
||||
static_cast<std::uint16_t>(lane), 0, 0);
|
||||
if (r < 0)
|
||||
{
|
||||
if (errno == EAGAIN || errno == EWOULDBLOCK)
|
||||
{
|
||||
auto self = shared_from_this();
|
||||
sd.async_wait(asio::posix::stream_descriptor::wait_write,
|
||||
asio::bind_executor(strand,
|
||||
[self](const std::error_code & ec) {
|
||||
if (ec)
|
||||
{
|
||||
self->writing = false;
|
||||
self->deliver_error(ec);
|
||||
return;
|
||||
}
|
||||
self->write_next();
|
||||
}));
|
||||
return;
|
||||
}
|
||||
writing = false;
|
||||
deliver_error(std::error_code(errno, std::generic_category()));
|
||||
return;
|
||||
}
|
||||
const std::size_t sent = static_cast<std::size_t>(r);
|
||||
counters.wrote(sent, sent, 1);
|
||||
if (sent < front.len)
|
||||
{
|
||||
buffered.fetch_sub(std::min(buffered.load(), sent),
|
||||
std::memory_order_relaxed);
|
||||
front.off += sent;
|
||||
front.len -= sent;
|
||||
write_next();
|
||||
return;
|
||||
}
|
||||
chunk done = std::move(front);
|
||||
outq[lane].pop_front();
|
||||
rr = (lane + 1) % n;
|
||||
finish_chunk(done, {});
|
||||
auto self = shared_from_this();
|
||||
asio::post(strand, [self] { self->write_next(); });
|
||||
}
|
||||
|
||||
void fail_queued(const std::error_code & ec)
|
||||
{
|
||||
writing = false;
|
||||
for (auto & q : outq)
|
||||
{
|
||||
for (auto & c : q)
|
||||
finish_chunk(c, ec);
|
||||
q.clear();
|
||||
}
|
||||
}
|
||||
|
||||
asio::io_context & io;
|
||||
asio::posix::stream_descriptor sd;
|
||||
asio::strand<asio::io_context::executor_type> strand;
|
||||
std::size_t n = 0;
|
||||
wire_policy pol;
|
||||
mutable std::mutex mu;
|
||||
std::vector<detail::stream_inbox> inbox;
|
||||
std::vector<std::vector<std::uint8_t>> partial;
|
||||
std::vector<detail::stream_waiter> wait;
|
||||
std::error_code fail;
|
||||
bool closed = false;
|
||||
bool aborted = false;
|
||||
std::vector<std::deque<chunk>> outq;
|
||||
std::size_t rr = 0;
|
||||
bool writing = false;
|
||||
bool draining = false;
|
||||
std::vector<std::uint8_t> rbuf;
|
||||
std::atomic<std::size_t> buffered{0};
|
||||
std::atomic<std::size_t> window;
|
||||
detail::io_counters counters;
|
||||
};
|
||||
|
||||
std::shared_ptr<impl> impl_;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Association setup (deadline-bounded)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// @brief Listening one-to-one SCTP socket.
|
||||
class sctp_listener
|
||||
{
|
||||
public:
|
||||
sctp_listener(unsigned short port, std::size_t nstreams,
|
||||
const socket_options & o = {})
|
||||
: nstreams_(nstreams), opts_(o)
|
||||
{
|
||||
if (nstreams == 0)
|
||||
throw std::invalid_argument("sctp_listener: no streams");
|
||||
fd_ = ::socket(AF_INET, SOCK_STREAM, IPPROTO_SCTP);
|
||||
if (fd_ < 0)
|
||||
throw std::system_error(errno, std::generic_category(),
|
||||
"sctp_listener: socket");
|
||||
int one = 1;
|
||||
(void)::setsockopt(fd_, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
|
||||
detail::sctp_configure(fd_, nstreams, o);
|
||||
struct sockaddr_in addr;
|
||||
std::memset(&addr, 0, sizeof(addr));
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_addr.s_addr = htonl(INADDR_ANY);
|
||||
addr.sin_port = htons(port);
|
||||
if (::bind(fd_, reinterpret_cast<struct sockaddr *>(&addr), sizeof(addr)) < 0
|
||||
|| ::listen(fd_, 16) < 0)
|
||||
{
|
||||
const int e = errno;
|
||||
::close(fd_);
|
||||
fd_ = -1;
|
||||
throw std::system_error(e, std::generic_category(),
|
||||
"sctp_listener: bind/listen on port " + std::to_string(port));
|
||||
}
|
||||
socklen_t alen = sizeof(addr);
|
||||
if (::getsockname(fd_, reinterpret_cast<struct sockaddr *>(&addr), &alen) == 0)
|
||||
port_ = ntohs(addr.sin_port);
|
||||
}
|
||||
|
||||
sctp_listener(const sctp_listener &) = delete;
|
||||
sctp_listener & operator=(const sctp_listener &) = delete;
|
||||
|
||||
~sctp_listener()
|
||||
{
|
||||
if (fd_ >= 0)
|
||||
::close(fd_);
|
||||
}
|
||||
|
||||
unsigned short port() const noexcept { return port_; }
|
||||
int native_handle() const noexcept { return fd_; }
|
||||
|
||||
/// @brief Accept one association within `budget`; returns the fd.
|
||||
int accept(std::chrono::milliseconds budget)
|
||||
{
|
||||
const auto deadline = setup_clock::now() + budget;
|
||||
detail::wait_fd(fd_, POLLIN, deadline,
|
||||
"sctp accept on port " + std::to_string(port_));
|
||||
const int cfd = ::accept(fd_, nullptr, nullptr);
|
||||
if (cfd < 0)
|
||||
throw std::system_error(errno, std::generic_category(), "sctp accept");
|
||||
detail::sctp_configure(cfd, nstreams_, opts_);
|
||||
return cfd;
|
||||
}
|
||||
|
||||
private:
|
||||
int fd_ = -1;
|
||||
unsigned short port_ = 0;
|
||||
std::size_t nstreams_ = 0;
|
||||
socket_options opts_{};
|
||||
};
|
||||
|
||||
/// @brief Connect one SCTP association, retrying refusals until `budget`.
|
||||
HEDLEY_WARN_UNUSED_RESULT
|
||||
inline int connect_sctp(const std::string & host, unsigned short port,
|
||||
std::size_t nstreams, std::chrono::milliseconds budget,
|
||||
const socket_options & o = {})
|
||||
{
|
||||
const auto deadline = setup_clock::now() + budget;
|
||||
struct sockaddr_in addr;
|
||||
std::memset(&addr, 0, sizeof(addr));
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_port = htons(port);
|
||||
const std::string h = host == "localhost" ? "127.0.0.1" : host;
|
||||
if (::inet_pton(AF_INET, h.c_str(), &addr.sin_addr) != 1)
|
||||
throw std::invalid_argument("connect_sctp: bad host " + host);
|
||||
int last = ETIMEDOUT;
|
||||
for (;;)
|
||||
{
|
||||
const int fd = ::socket(AF_INET, SOCK_STREAM, IPPROTO_SCTP);
|
||||
if (fd < 0)
|
||||
throw std::system_error(errno, std::generic_category(),
|
||||
"connect_sctp: socket");
|
||||
detail::sctp_configure(fd, nstreams, o);
|
||||
if (::connect(fd, reinterpret_cast<struct sockaddr *>(&addr),
|
||||
sizeof(addr)) == 0)
|
||||
return fd;
|
||||
last = errno;
|
||||
::close(fd);
|
||||
if (setup_clock::now() >= deadline)
|
||||
throw std::system_error(last, std::generic_category(),
|
||||
"connect_sctp " + host + ":" + std::to_string(port));
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(20));
|
||||
}
|
||||
}
|
||||
|
||||
/// @brief Accept one association on an ephemeral (or given) port.
|
||||
HEDLEY_WARN_UNUSED_RESULT
|
||||
inline int accept_sctp_association(asio::io_context &,
|
||||
std::atomic<unsigned short> & port, std::size_t nstreams,
|
||||
std::chrono::milliseconds budget = deadlines{}.accept)
|
||||
{
|
||||
sctp_listener lst(port.load(), nstreams);
|
||||
port.store(lst.port());
|
||||
return lst.accept(budget);
|
||||
}
|
||||
|
||||
/// @brief Connect one association to `host:port`.
|
||||
HEDLEY_WARN_UNUSED_RESULT
|
||||
inline int connect_sctp_association(asio::io_context &, const std::string & host,
|
||||
std::atomic<unsigned short> & port, std::size_t nstreams,
|
||||
std::chrono::milliseconds budget = deadlines{}.connect)
|
||||
{
|
||||
return connect_sctp(host, port.load(), nstreams, budget);
|
||||
}
|
||||
|
||||
#else // !DPF_HAS_LIBSCTP
|
||||
|
||||
class async_sctp_stream_array final : public async_stream_array
|
||||
{
|
||||
public:
|
||||
async_sctp_stream_array(asio::io_context &, int, std::size_t,
|
||||
const wire_policy & = {})
|
||||
{
|
||||
throw std::logic_error(
|
||||
"async_sctp_stream_array: real SCTP requires Linux + libsctp "
|
||||
"(<netinet/sctp.h>, link -lsctp)");
|
||||
}
|
||||
|
||||
std::size_t size() const noexcept override { return 0; }
|
||||
asio::io_context & context() noexcept override
|
||||
{
|
||||
std::terminate();
|
||||
}
|
||||
void async_write(std::size_t, const void *, std::size_t,
|
||||
async_handler) override
|
||||
{
|
||||
throw std::logic_error("async_sctp_stream_array: not available");
|
||||
}
|
||||
void async_read(std::size_t, void *, std::size_t, async_handler) override
|
||||
{
|
||||
throw std::logic_error("async_sctp_stream_array: not available");
|
||||
}
|
||||
};
|
||||
|
||||
class sctp_listener
|
||||
{
|
||||
public:
|
||||
sctp_listener(unsigned short, std::size_t, const socket_options & = {})
|
||||
{
|
||||
throw std::logic_error("sctp_listener: SCTP requires Linux + libsctp");
|
||||
}
|
||||
unsigned short port() const noexcept { return 0; }
|
||||
int native_handle() const noexcept { return -1; }
|
||||
int accept(std::chrono::milliseconds) { return -1; }
|
||||
};
|
||||
|
||||
HEDLEY_WARN_UNUSED_RESULT
|
||||
inline int connect_sctp(const std::string &, unsigned short, std::size_t,
|
||||
std::chrono::milliseconds, const socket_options & = {})
|
||||
{
|
||||
throw std::logic_error("connect_sctp: SCTP requires Linux + libsctp");
|
||||
}
|
||||
|
||||
HEDLEY_WARN_UNUSED_RESULT
|
||||
inline int accept_sctp_association(asio::io_context &,
|
||||
std::atomic<unsigned short> &, std::size_t,
|
||||
std::chrono::milliseconds = deadlines{}.accept)
|
||||
{
|
||||
throw std::logic_error(
|
||||
"accept_sctp_association: SCTP requires Linux + libsctp");
|
||||
}
|
||||
|
||||
HEDLEY_WARN_UNUSED_RESULT
|
||||
inline int connect_sctp_association(asio::io_context &, const std::string &,
|
||||
std::atomic<unsigned short> &, std::size_t,
|
||||
std::chrono::milliseconds = deadlines{}.connect)
|
||||
{
|
||||
throw std::logic_error(
|
||||
"connect_sctp_association: SCTP requires Linux + libsctp");
|
||||
}
|
||||
|
||||
#endif // DPF_HAS_LIBSCTP
|
||||
|
||||
} // namespace net
|
||||
} // namespace dpf
|
||||
|
||||
#endif // LIBDPF_INCLUDE_DPF_NET_ASYNC_SCTP_STREAM_ARRAY_HPP__
|
||||
2069
include/dpf/net/async_stream_array.hpp
Normal file
2069
include/dpf/net/async_stream_array.hpp
Normal file
File diff suppressed because it is too large
Load diff
98
include/dpf/net/buffer_pool.hpp
Normal file
98
include/dpf/net/buffer_pool.hpp
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
/// @file dpf/net/buffer_pool.hpp
|
||||
/// @brief Recycled byte buffers for framed writes.
|
||||
/// @details `acquire_buffer` hands out a `shared_ptr` whose deleter returns the
|
||||
/// vector to a process-wide free list (capped). Callers that already
|
||||
/// own a buffer pass it to `async_write_owned` so the socket path can
|
||||
/// scatter-gather without a second payload copy.
|
||||
#ifndef LIBDPF_INCLUDE_DPF_NET_BUFFER_POOL_HPP__
|
||||
#define LIBDPF_INCLUDE_DPF_NET_BUFFER_POOL_HPP__
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "dpf/net/policy.hpp"
|
||||
|
||||
namespace dpf
|
||||
{
|
||||
namespace net
|
||||
{
|
||||
|
||||
/// @brief Default outstanding-byte window (see `wire_policy::window_bytes`).
|
||||
inline std::size_t default_wire_window() noexcept
|
||||
{
|
||||
return wire_policy{}.window_bytes;
|
||||
}
|
||||
|
||||
/// @brief Default receive cap (see `wire_policy::max_frame`).
|
||||
inline constexpr std::size_t k_max_frame = std::size_t{16} << 20;
|
||||
|
||||
namespace detail
|
||||
{
|
||||
|
||||
struct pool_state
|
||||
{
|
||||
std::mutex mu;
|
||||
std::vector<std::unique_ptr<std::vector<std::uint8_t>>> free;
|
||||
};
|
||||
|
||||
inline pool_state & buffers()
|
||||
{
|
||||
static pool_state s;
|
||||
return s;
|
||||
}
|
||||
|
||||
inline void recycle(std::vector<std::uint8_t> * p)
|
||||
{
|
||||
std::unique_ptr<std::vector<std::uint8_t>> owned(p);
|
||||
owned->clear();
|
||||
if (owned->capacity() > (std::size_t{1} << 20))
|
||||
return;
|
||||
auto & st = buffers();
|
||||
std::lock_guard<std::mutex> lock(st.mu);
|
||||
if (st.free.size() < 128)
|
||||
st.free.push_back(std::move(owned));
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
/// @brief Buffer of at least `n` bytes, returned to the pool on last release.
|
||||
inline std::shared_ptr<std::vector<std::uint8_t>> acquire_buffer(std::size_t n)
|
||||
{
|
||||
std::unique_ptr<std::vector<std::uint8_t>> raw;
|
||||
{
|
||||
auto & st = detail::buffers();
|
||||
std::lock_guard<std::mutex> lock(st.mu);
|
||||
if (!st.free.empty())
|
||||
{
|
||||
raw = std::move(st.free.back());
|
||||
st.free.pop_back();
|
||||
}
|
||||
}
|
||||
if (!raw)
|
||||
raw.reset(new std::vector<std::uint8_t>());
|
||||
if (raw->capacity() < n)
|
||||
raw->reserve(n);
|
||||
raw->resize(n);
|
||||
std::vector<std::uint8_t> * p = raw.release();
|
||||
return std::shared_ptr<std::vector<std::uint8_t>>(p, &detail::recycle);
|
||||
}
|
||||
|
||||
/// @brief Copy `n` bytes into a pooled buffer.
|
||||
inline std::shared_ptr<std::vector<std::uint8_t>> copy_buffer(const void * src,
|
||||
std::size_t n)
|
||||
{
|
||||
auto buf = acquire_buffer(n);
|
||||
if (n != 0 && src != nullptr)
|
||||
std::memcpy(buf->data(), src, n);
|
||||
return buf;
|
||||
}
|
||||
|
||||
} // namespace net
|
||||
} // namespace dpf
|
||||
|
||||
#endif
|
||||
484
include/dpf/net/channel.hpp
Normal file
484
include/dpf/net/channel.hpp
Normal file
|
|
@ -0,0 +1,484 @@
|
|||
/// @file dpf/net/channel.hpp
|
||||
/// @brief Framed duplex stream for party message exchange.
|
||||
/// @details Every message is `u32` little-endian length, `u16` type tag, then
|
||||
/// payload. `exchange` orders send/recv by role so a single stream
|
||||
/// cannot deadlock. Call sites name `send` / `recv` / `exchange`;
|
||||
/// they do not touch ASIO buffers.
|
||||
/// @copyright Copyright (c) 2019-2026 Ryan Henry and [others](@ref authors)
|
||||
/// @license Released under a GNU General Public v2.0 (GPLv2) license;
|
||||
/// see [LICENSE.md](@ref license) for details.
|
||||
|
||||
#ifndef LIBDPF_INCLUDE_DPF_NET_CHANNEL_HPP__
|
||||
#define LIBDPF_INCLUDE_DPF_NET_CHANNEL_HPP__
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "dpf/net/asio_ns.hpp"
|
||||
#include "dpf/net/tls.hpp"
|
||||
|
||||
#include "hedley/hedley.h"
|
||||
|
||||
namespace dpf
|
||||
{
|
||||
namespace net
|
||||
{
|
||||
|
||||
/// @brief Bytes and frames observed on one channel since the last reset.
|
||||
/// @details Counts include the 6-byte frame header. A failed read or write
|
||||
/// does not add to the tally. `payload_*` is the body alone;
|
||||
/// `exchanges` counts `exchange()` calls (one logical round each).
|
||||
struct io_tally
|
||||
{
|
||||
std::uint64_t bytes_sent = 0;
|
||||
std::uint64_t bytes_recv = 0;
|
||||
std::uint64_t frames_sent = 0;
|
||||
std::uint64_t frames_recv = 0;
|
||||
std::uint64_t payload_sent = 0;
|
||||
std::uint64_t payload_recv = 0;
|
||||
std::uint64_t exchanges = 0;
|
||||
|
||||
io_tally & operator+=(const io_tally & other) noexcept
|
||||
{
|
||||
bytes_sent += other.bytes_sent;
|
||||
bytes_recv += other.bytes_recv;
|
||||
frames_sent += other.frames_sent;
|
||||
frames_recv += other.frames_recv;
|
||||
payload_sent += other.payload_sent;
|
||||
payload_recv += other.payload_recv;
|
||||
exchanges += other.exchanges;
|
||||
return *this;
|
||||
}
|
||||
};
|
||||
|
||||
enum class msg : std::uint16_t
|
||||
{
|
||||
hangup = 0,
|
||||
beaver_tape = 1,
|
||||
ring_vector = 2,
|
||||
delta = 3,
|
||||
dpf_key = 4,
|
||||
proof_token = 5,
|
||||
case_ok = 6,
|
||||
case_fail = 7,
|
||||
bytes = 8,
|
||||
mac_key = 9,
|
||||
mac_share = 10,
|
||||
sketch_share = 11,
|
||||
round_batch = 12,
|
||||
};
|
||||
|
||||
inline constexpr std::uint16_t to_u16(msg t) noexcept
|
||||
{
|
||||
return static_cast<std::uint16_t>(t);
|
||||
}
|
||||
|
||||
/// @brief One framed duplex byte stream.
|
||||
class channel
|
||||
{
|
||||
public:
|
||||
using tcp_socket = asio::ip::tcp::socket;
|
||||
using local_socket = asio::local::stream_protocol::socket;
|
||||
|
||||
channel() = default;
|
||||
|
||||
explicit channel(tcp_socket sock)
|
||||
: tcp_(std::make_unique<tcp_socket>(std::move(sock)))
|
||||
{ }
|
||||
|
||||
explicit channel(local_socket sock)
|
||||
: local_(std::make_unique<local_socket>(std::move(sock)))
|
||||
{ }
|
||||
|
||||
#if DPF_HAS_OPENSSL
|
||||
/// @brief Framed channel over an established TLS 1.3 TCP stream.
|
||||
static channel from_tls(asio::io_context &, tls_stream s,
|
||||
std::shared_ptr<tls_context> ctx)
|
||||
{
|
||||
channel c;
|
||||
c.tls_ctx_ = std::move(ctx);
|
||||
c.tls_ = std::make_unique<tls_stream>(std::move(s));
|
||||
return c;
|
||||
}
|
||||
|
||||
/// @brief Framed channel over an established TLS 1.3 unix-domain stream.
|
||||
static channel from_tls_local(asio::io_context &, tls_local_stream s,
|
||||
std::shared_ptr<tls_context> ctx)
|
||||
{
|
||||
channel c;
|
||||
c.tls_ctx_ = std::move(ctx);
|
||||
c.tls_local_ = std::make_unique<tls_local_stream>(std::move(s));
|
||||
return c;
|
||||
}
|
||||
#endif
|
||||
|
||||
channel(channel &&) noexcept = default;
|
||||
channel & operator=(channel &&) noexcept = default;
|
||||
|
||||
channel(const channel &) = delete;
|
||||
channel & operator=(const channel &) = delete;
|
||||
|
||||
/// @brief Replay `frames` as a recv-only dealer tape. `send` throws.
|
||||
static channel from_inbox(std::vector<std::uint8_t> frames)
|
||||
{
|
||||
channel c;
|
||||
c.inbox_ = std::make_unique<inbox_buf>();
|
||||
c.inbox_->bytes = std::move(frames);
|
||||
return c;
|
||||
}
|
||||
|
||||
/// @brief True when an inbox tape has been read through its last byte.
|
||||
HEDLEY_NO_THROW
|
||||
bool inbox_done() const noexcept
|
||||
{
|
||||
return inbox_ != nullptr && inbox_->pos == inbox_->bytes.size();
|
||||
}
|
||||
|
||||
HEDLEY_NO_THROW
|
||||
bool open() const noexcept
|
||||
{
|
||||
return tcp_ != nullptr || local_ != nullptr || inbox_ != nullptr
|
||||
#if DPF_HAS_OPENSSL
|
||||
|| tls_ != nullptr || tls_local_ != nullptr
|
||||
#endif
|
||||
;
|
||||
}
|
||||
|
||||
/// @brief Underlying socket descriptor, or -1 for an inbox tape / TLS edge.
|
||||
/// @details Prefer framed `send` / `recv` on TLS channels. Raw descriptor
|
||||
/// I/O bypasses TLS and is rejected when the channel is encrypted.
|
||||
HEDLEY_NO_THROW
|
||||
int native_handle() noexcept
|
||||
{
|
||||
if (tcp_)
|
||||
return tcp_->native_handle();
|
||||
if (local_)
|
||||
return local_->native_handle();
|
||||
#if DPF_HAS_OPENSSL
|
||||
if (tls_ || tls_local_)
|
||||
return -1;
|
||||
#endif
|
||||
return -1;
|
||||
}
|
||||
|
||||
/// @brief True when application bytes ride TLS 1.3 under the frame header.
|
||||
HEDLEY_NO_THROW
|
||||
bool encrypted() const noexcept
|
||||
{
|
||||
#if DPF_HAS_OPENSSL
|
||||
return tls_ != nullptr || tls_local_ != nullptr;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
/// @brief Bytes and frames since construction or the last `reset_tally`.
|
||||
HEDLEY_NO_THROW
|
||||
io_tally tally() const noexcept
|
||||
{
|
||||
return tally_;
|
||||
}
|
||||
|
||||
/// @brief Zero the byte and frame counters. Does not touch the socket.
|
||||
HEDLEY_NO_THROW
|
||||
void reset_tally() noexcept
|
||||
{
|
||||
tally_ = {};
|
||||
}
|
||||
|
||||
/// @name Framed messages
|
||||
/// @brief Each frame is a little-endian length, a tag, then the payload.
|
||||
/// `T` must be trivially copyable. A zero-length payload is a
|
||||
/// header only.
|
||||
/// @throws std::runtime_error if the socket closes or the tag/size disagree
|
||||
/// @throws std::invalid_argument if a frame exceeds 2^32-1 bytes, or
|
||||
/// `exchange` is called with `self_id == peer_id`
|
||||
/// @throws std::logic_error if the channel is closed
|
||||
/// @{
|
||||
|
||||
/// @brief Send one value.
|
||||
/// @tparam T trivially copyable payload
|
||||
/// @param tag the message tag
|
||||
/// @param value the payload
|
||||
template <typename T>
|
||||
void send(msg tag, const T & value)
|
||||
{
|
||||
static_assert(std::is_trivially_copyable_v<T>,
|
||||
"net::channel::send requires a trivially copyable type");
|
||||
write_frame(tag, &value, sizeof(T));
|
||||
}
|
||||
|
||||
/// @brief Receive one value.
|
||||
/// @tparam T trivially copyable payload
|
||||
/// @param tag the expected message tag
|
||||
/// @return the payload
|
||||
template <typename T>
|
||||
HEDLEY_WARN_UNUSED_RESULT
|
||||
T recv(msg tag)
|
||||
{
|
||||
static_assert(std::is_trivially_copyable_v<T>,
|
||||
"net::channel::recv requires a trivially copyable type");
|
||||
T value{};
|
||||
read_frame(tag, &value, sizeof(T));
|
||||
return value;
|
||||
}
|
||||
|
||||
/// @brief Send `n` values.
|
||||
/// @param data the values
|
||||
/// @param n the value count
|
||||
/// @param tag the message tag
|
||||
template <typename T>
|
||||
void send_vec(const T * data, std::size_t n, msg tag = msg::ring_vector)
|
||||
{
|
||||
static_assert(std::is_trivially_copyable_v<T>,
|
||||
"net::channel::send_vec requires a trivially copyable type");
|
||||
write_frame(tag, data, n * sizeof(T));
|
||||
}
|
||||
|
||||
/// @brief Send a vector of values.
|
||||
/// @param v the values
|
||||
/// @param tag the message tag
|
||||
template <typename T>
|
||||
void send_vec(const std::vector<T> & v, msg tag = msg::ring_vector)
|
||||
{
|
||||
send_vec(v.data(), v.size(), tag);
|
||||
}
|
||||
|
||||
/// @brief Receive a homogeneous vector.
|
||||
/// @tparam T trivially copyable element
|
||||
/// @param tag the expected message tag
|
||||
/// @return the elements. Empty when the payload length is 0.
|
||||
template <typename T>
|
||||
HEDLEY_WARN_UNUSED_RESULT
|
||||
std::vector<T> recv_vec(msg tag = msg::ring_vector)
|
||||
{
|
||||
static_assert(std::is_trivially_copyable_v<T>,
|
||||
"net::channel::recv_vec requires a trivially copyable type");
|
||||
auto bytes = read_frame_bytes(tag);
|
||||
if (bytes.size() % sizeof(T) != 0)
|
||||
throw std::runtime_error("net::channel::recv_vec size mismatch");
|
||||
std::vector<T> out(bytes.size() / sizeof(T));
|
||||
if (!out.empty())
|
||||
std::memcpy(out.data(), bytes.data(), bytes.size());
|
||||
return out;
|
||||
}
|
||||
|
||||
/// @brief Send `n` bytes.
|
||||
/// @param tag the message tag
|
||||
/// @param data the bytes
|
||||
/// @param n the byte count
|
||||
void send_bytes(msg tag, const void * data, std::size_t n)
|
||||
{
|
||||
write_frame(tag, data, n);
|
||||
}
|
||||
|
||||
/// @brief Send a byte vector.
|
||||
/// @param tag the message tag
|
||||
/// @param bytes the bytes
|
||||
void send_bytes(msg tag, const std::vector<std::uint8_t> & bytes)
|
||||
{
|
||||
send_bytes(tag, bytes.data(), bytes.size());
|
||||
}
|
||||
|
||||
/// @brief Receive an untyped payload.
|
||||
/// @param tag the expected message tag
|
||||
/// @return the payload bytes
|
||||
HEDLEY_WARN_UNUSED_RESULT
|
||||
std::vector<std::uint8_t> recv_bytes(msg tag)
|
||||
{
|
||||
return read_frame_bytes(tag);
|
||||
}
|
||||
|
||||
/// @brief Exchange one value. The lower id sends first.
|
||||
/// @tparam T trivially copyable payload
|
||||
/// @param self_id this party's role, as an integer
|
||||
/// @param peer_id the peer's role, as an integer
|
||||
/// @param mine this party's value
|
||||
/// @param tag the message tag
|
||||
/// @return the peer's value
|
||||
template <typename T>
|
||||
HEDLEY_WARN_UNUSED_RESULT
|
||||
T exchange(unsigned self_id, unsigned peer_id, const T & mine, msg tag = msg::delta)
|
||||
{
|
||||
static_assert(std::is_trivially_copyable_v<T>,
|
||||
"net::channel::exchange requires a trivially copyable type");
|
||||
++tally_.exchanges;
|
||||
if (self_id < peer_id)
|
||||
{
|
||||
send(tag, mine);
|
||||
return recv<T>(tag);
|
||||
}
|
||||
if (self_id > peer_id)
|
||||
{
|
||||
T theirs = recv<T>(tag);
|
||||
send(tag, mine);
|
||||
return theirs;
|
||||
}
|
||||
throw std::invalid_argument("net::channel::exchange with self");
|
||||
}
|
||||
|
||||
/// @brief Exchange a homogeneous vector. One barrier; lower id sends first.
|
||||
/// @tparam T trivially copyable element
|
||||
/// @param self_id this party's role, as an integer
|
||||
/// @param peer_id the peer's role, as an integer
|
||||
/// @param mine this party's values
|
||||
/// @param tag the message tag
|
||||
/// @return the peer's values (same length as `mine`)
|
||||
/// @throws std::runtime_error if the peer's vector length disagrees
|
||||
template <typename T>
|
||||
HEDLEY_WARN_UNUSED_RESULT
|
||||
std::vector<T> exchange_vec(unsigned self_id, unsigned peer_id,
|
||||
const std::vector<T> & mine, msg tag = msg::ring_vector)
|
||||
{
|
||||
static_assert(std::is_trivially_copyable_v<T>,
|
||||
"net::channel::exchange_vec requires a trivially copyable type");
|
||||
if (mine.empty())
|
||||
return {};
|
||||
++tally_.exchanges;
|
||||
std::vector<T> theirs;
|
||||
if (self_id < peer_id)
|
||||
{
|
||||
send_vec(mine, tag);
|
||||
theirs = recv_vec<T>(tag);
|
||||
}
|
||||
else if (self_id > peer_id)
|
||||
{
|
||||
theirs = recv_vec<T>(tag);
|
||||
send_vec(mine, tag);
|
||||
}
|
||||
else
|
||||
throw std::invalid_argument("net::channel::exchange_vec with self");
|
||||
if (theirs.size() != mine.size())
|
||||
throw std::runtime_error("net::channel::exchange_vec size mismatch");
|
||||
return theirs;
|
||||
}
|
||||
|
||||
/// @}
|
||||
|
||||
private:
|
||||
struct inbox_buf
|
||||
{
|
||||
std::vector<std::uint8_t> bytes;
|
||||
std::size_t pos = 0;
|
||||
};
|
||||
|
||||
std::unique_ptr<tcp_socket> tcp_;
|
||||
std::unique_ptr<local_socket> local_;
|
||||
#if DPF_HAS_OPENSSL
|
||||
std::unique_ptr<tls_stream> tls_;
|
||||
std::unique_ptr<tls_local_stream> tls_local_;
|
||||
std::shared_ptr<tls_context> tls_ctx_;
|
||||
#endif
|
||||
std::unique_ptr<inbox_buf> inbox_;
|
||||
io_tally tally_{};
|
||||
|
||||
void write_all(const void * data, std::size_t n)
|
||||
{
|
||||
if (inbox_)
|
||||
throw std::logic_error("net::channel inbox is recv-only");
|
||||
asio::const_buffer buf(data, n);
|
||||
asio::error_code ec;
|
||||
if (tcp_)
|
||||
asio::write(*tcp_, buf, ec);
|
||||
else if (local_)
|
||||
asio::write(*local_, buf, ec);
|
||||
#if DPF_HAS_OPENSSL
|
||||
else if (tls_)
|
||||
asio::write(*tls_, buf, ec);
|
||||
else if (tls_local_)
|
||||
asio::write(*tls_local_, buf, ec);
|
||||
#endif
|
||||
else
|
||||
throw std::logic_error("net::channel is closed");
|
||||
if (ec)
|
||||
throw std::runtime_error("net::channel write: " + ec.message());
|
||||
tally_.bytes_sent += n;
|
||||
}
|
||||
|
||||
void read_all(void * data, std::size_t n)
|
||||
{
|
||||
if (inbox_)
|
||||
{
|
||||
if (inbox_->pos + n > inbox_->bytes.size())
|
||||
throw std::runtime_error("net::channel inbox underrun");
|
||||
if (n != 0)
|
||||
std::memcpy(data, inbox_->bytes.data() + inbox_->pos, n);
|
||||
inbox_->pos += n;
|
||||
tally_.bytes_recv += n;
|
||||
return;
|
||||
}
|
||||
asio::mutable_buffer buf(data, n);
|
||||
asio::error_code ec;
|
||||
if (tcp_)
|
||||
asio::read(*tcp_, buf, ec);
|
||||
else if (local_)
|
||||
asio::read(*local_, buf, ec);
|
||||
#if DPF_HAS_OPENSSL
|
||||
else if (tls_)
|
||||
asio::read(*tls_, buf, ec);
|
||||
else if (tls_local_)
|
||||
asio::read(*tls_local_, buf, ec);
|
||||
#endif
|
||||
else
|
||||
throw std::logic_error("net::channel is closed");
|
||||
if (ec)
|
||||
throw std::runtime_error("net::channel read: " + ec.message());
|
||||
tally_.bytes_recv += n;
|
||||
}
|
||||
|
||||
void write_frame(msg tag, const void * payload, std::size_t n)
|
||||
{
|
||||
if (n > 0xffffffffu)
|
||||
throw std::invalid_argument("net::channel frame too large");
|
||||
std::uint32_t len = static_cast<std::uint32_t>(n);
|
||||
std::uint16_t t = to_u16(tag);
|
||||
std::array<std::uint8_t, 6> hdr{};
|
||||
std::memcpy(hdr.data(), &len, 4);
|
||||
std::memcpy(hdr.data() + 4, &t, 2);
|
||||
write_all(hdr.data(), hdr.size());
|
||||
if (n != 0)
|
||||
write_all(payload, n);
|
||||
tally_.payload_sent += n;
|
||||
++tally_.frames_sent;
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> read_frame_bytes(msg expected)
|
||||
{
|
||||
std::array<std::uint8_t, 6> hdr{};
|
||||
read_all(hdr.data(), hdr.size());
|
||||
std::uint32_t len = 0;
|
||||
std::uint16_t t = 0;
|
||||
std::memcpy(&len, hdr.data(), 4);
|
||||
std::memcpy(&t, hdr.data() + 4, 2);
|
||||
if (t != to_u16(expected))
|
||||
throw std::runtime_error("net::channel unexpected message tag");
|
||||
std::vector<std::uint8_t> body(len);
|
||||
if (len != 0)
|
||||
read_all(body.data(), body.size());
|
||||
tally_.payload_recv += len;
|
||||
++tally_.frames_recv;
|
||||
return body;
|
||||
}
|
||||
|
||||
void read_frame(msg expected, void * dest, std::size_t expect_n)
|
||||
{
|
||||
auto body = read_frame_bytes(expected);
|
||||
if (body.size() != expect_n)
|
||||
throw std::runtime_error("net::channel frame size mismatch");
|
||||
if (expect_n != 0)
|
||||
std::memcpy(dest, body.data(), expect_n);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace net
|
||||
} // namespace dpf
|
||||
|
||||
#endif // LIBDPF_INCLUDE_DPF_NET_CHANNEL_HPP__
|
||||
218
include/dpf/net/client_link.hpp
Normal file
218
include/dpf/net/client_link.hpp
Normal file
|
|
@ -0,0 +1,218 @@
|
|||
/// @file dpf/net/client_link.hpp
|
||||
/// @brief Client-to-server links: TLS 1.3, the server always verified.
|
||||
/// @details A client that supplies inputs (for example, one share to each
|
||||
/// party) connects with `connect_server`. It verifies the server
|
||||
/// unless `client_security::verify` is off: a pinned server key, a CA
|
||||
/// chain for the host name, or, when neither is configured, the
|
||||
/// built-in development certificate, which a `client_listener` with no
|
||||
/// certificate or identity presents. The development certificate's
|
||||
/// private key is public, so that pairing works out of the box and is
|
||||
/// logged as providing no security. A server may also check client
|
||||
/// keys (`server_security::client_pins`). Both ends get an ordinary
|
||||
/// `async_stream_array` of `lanes` lanes and the link's
|
||||
/// `link_security`.
|
||||
#ifndef LIBDPF_INCLUDE_DPF_NET_CLIENT_LINK_HPP__
|
||||
#define LIBDPF_INCLUDE_DPF_NET_CLIENT_LINK_HPP__
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
#include "dpf/net/asio_ns.hpp"
|
||||
|
||||
#include "dpf/log.hpp"
|
||||
#include "dpf/net/async_stream_array.hpp"
|
||||
#include "dpf/net/connect.hpp"
|
||||
#include "dpf/net/link_log.hpp"
|
||||
#include "dpf/net/policy.hpp"
|
||||
#include "dpf/net/security.hpp"
|
||||
#include "dpf/net/socket_tune.hpp"
|
||||
#include "dpf/net/tls.hpp"
|
||||
|
||||
namespace dpf
|
||||
{
|
||||
namespace net
|
||||
{
|
||||
|
||||
/// @brief One client link and how it was secured.
|
||||
struct client_connection
|
||||
{
|
||||
#if DPF_HAS_OPENSSL
|
||||
std::shared_ptr<tls_context> context;
|
||||
#endif
|
||||
std::unique_ptr<async_stream_array> link;
|
||||
link_security security;
|
||||
};
|
||||
|
||||
namespace detail
|
||||
{
|
||||
|
||||
inline constexpr std::uint32_t client_magic = 0x4c435044u; // 'DPCL'
|
||||
|
||||
#if DPF_HAS_OPENSSL
|
||||
/// @brief Both ends send `{magic, lanes}` inside TLS and must agree.
|
||||
inline void client_hello(asio::io_context & io, tls_stream & s, std::size_t lanes,
|
||||
bool server, std::chrono::milliseconds budget, const std::string & who)
|
||||
{
|
||||
std::uint8_t mine[8];
|
||||
std::uint8_t theirs[8];
|
||||
put_u32(mine, client_magic);
|
||||
put_u32(mine + 4, static_cast<std::uint32_t>(lanes));
|
||||
if (server)
|
||||
{
|
||||
tls_read(io, s, theirs, sizeof(theirs), budget, who);
|
||||
tls_write(io, s, mine, sizeof(mine), budget, who);
|
||||
}
|
||||
else
|
||||
{
|
||||
tls_write(io, s, mine, sizeof(mine), budget, who);
|
||||
tls_read(io, s, theirs, sizeof(theirs), budget, who);
|
||||
}
|
||||
if (get_u32(theirs) != client_magic)
|
||||
throw std::runtime_error(who + ": the peer is not a libdpf client link");
|
||||
if (get_u32(theirs + 4) != lanes)
|
||||
throw std::runtime_error(who + ": lanes " + std::to_string(get_u32(theirs + 4))
|
||||
+ " vs " + std::to_string(lanes) + " (peer vs this side)");
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace detail
|
||||
|
||||
/// @brief Connect to the server at `host:port` and verify it.
|
||||
inline client_connection connect_server(asio::io_context & io, const std::string & host,
|
||||
unsigned short port, const client_security & sec, std::size_t lanes = 1,
|
||||
const wire_policy & pol = {}, const deadlines & lim = {})
|
||||
{
|
||||
#if DPF_HAS_OPENSSL
|
||||
const std::string where = host + ":" + std::to_string(port);
|
||||
client_connection out;
|
||||
out.context = make_client_tls_context(sec);
|
||||
asio::ip::tcp::socket sock(io);
|
||||
connect_until(sock, host, port, lim.connect);
|
||||
tune_tcp(sock, pol.socket);
|
||||
const int fd = sock.native_handle();
|
||||
tls_stream s(std::move(sock), *out.context);
|
||||
if (sec.verify && !sec.ca_file.empty())
|
||||
tls_expect_host(s, sec.server_name.empty() ? host : sec.server_name);
|
||||
tls_handshake(io, s, false, lim.handshake, "client: TLS handshake with " + where);
|
||||
out.security = tls_describe(s);
|
||||
try
|
||||
{
|
||||
check_server(out.security, s, sec, where);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
std::error_code e;
|
||||
s.lowest_layer().close(e);
|
||||
throw;
|
||||
}
|
||||
if (!sec.verify)
|
||||
DPF_LOG(error, "client.verify_off").kv("server", where)
|
||||
.kv("detail", "client_verify=off: any server certificate is accepted, so "
|
||||
"this connection is encrypted but the server is not authenticated");
|
||||
else if (out.security.peer_auth == "development"
|
||||
&& log::first_time("client.development." + where))
|
||||
DPF_LOG(warning, "client.development_certificate").kv("server", where)
|
||||
.kv("detail", "the server presented the built-in development certificate, "
|
||||
"whose private key is public: this connection is encrypted but the "
|
||||
"server is not authenticated (pin its key or configure client_ca)");
|
||||
detail::client_hello(io, s, lanes, false, lim.handshake, "client link to " + where);
|
||||
out.link = std::make_unique<async_tls_mux_stream_array>(io, std::move(s), 1, 0, lanes,
|
||||
pol);
|
||||
log_link_up("connect", "server", transport::mux, lanes, 0, 0, fd, pol.socket,
|
||||
&out.security);
|
||||
return out;
|
||||
#else
|
||||
(void)io;
|
||||
(void)host;
|
||||
(void)port;
|
||||
(void)sec;
|
||||
(void)lanes;
|
||||
(void)pol;
|
||||
(void)lim;
|
||||
throw std::logic_error("connect_server: built without OpenSSL");
|
||||
#endif
|
||||
}
|
||||
|
||||
/// @brief Server side: accept clients, each on its own TLS link.
|
||||
class client_listener
|
||||
{
|
||||
public:
|
||||
client_listener(asio::io_context & io, server_security sec, std::size_t lanes = 1,
|
||||
wire_policy pol = {}, deadlines lim = {})
|
||||
: io_(&io), sec_(std::move(sec)), lanes_(lanes), pol_(pol), lim_(lim)
|
||||
{
|
||||
#if DPF_HAS_OPENSSL
|
||||
ctx_ = make_server_tls_context(sec_, development_);
|
||||
if (development_ && log::first_time("server.development"))
|
||||
DPF_LOG(warning, "server.development_certificate")
|
||||
.kv("detail", "presenting the built-in development certificate, whose "
|
||||
"private key is public: clients cannot tell this server from any "
|
||||
"other (set server_cert/server_key or server_identity)");
|
||||
#else
|
||||
throw std::logic_error("client_listener: built without OpenSSL");
|
||||
#endif
|
||||
}
|
||||
|
||||
/// @brief Bind `port` (0 = ephemeral) and return it.
|
||||
unsigned short listen(unsigned short port = 0)
|
||||
{
|
||||
if (!acceptor_)
|
||||
{
|
||||
acceptor_ = std::make_unique<asio::ip::tcp::acceptor>(*io_);
|
||||
open_listener(*acceptor_, port);
|
||||
log_listen(acceptor_->local_endpoint().port(), false, true);
|
||||
}
|
||||
return acceptor_->local_endpoint().port();
|
||||
}
|
||||
|
||||
bool development() const noexcept { return development_; }
|
||||
|
||||
/// @brief Wait (up to the accept deadline) for one client.
|
||||
client_connection accept()
|
||||
{
|
||||
listen(0);
|
||||
client_connection out;
|
||||
#if DPF_HAS_OPENSSL
|
||||
out.context = ctx_;
|
||||
asio::ip::tcp::socket sock(*io_);
|
||||
accept_until(*acceptor_, sock, lim_.accept);
|
||||
tune_tcp(sock, pol_.socket);
|
||||
const int fd = sock.native_handle();
|
||||
tls_stream s(std::move(sock), *ctx_);
|
||||
tls_handshake(*io_, s, true, lim_.handshake, "server: TLS handshake with a client");
|
||||
out.security = tls_describe(s);
|
||||
check_client(out.security, sec_);
|
||||
detail::client_hello(*io_, s, lanes_, true, lim_.handshake, "client link");
|
||||
out.link = std::make_unique<async_tls_mux_stream_array>(*io_, std::move(s), 0, 1,
|
||||
lanes_, pol_);
|
||||
log_link_up("accept", "client", transport::mux, lanes_, 0, 0, fd, pol_.socket,
|
||||
&out.security);
|
||||
if (!sec_.client_pins.empty() && out.security.peer_auth == "none")
|
||||
DPF_LOG(warning, "server.client_unauthenticated")
|
||||
.kv("client_key", out.security.peer_key ? out.security.peer_key->base64()
|
||||
: std::string("none"))
|
||||
.kv("detail", "the client presented no pinned key");
|
||||
#endif
|
||||
return out;
|
||||
}
|
||||
|
||||
private:
|
||||
asio::io_context * io_ = nullptr;
|
||||
server_security sec_;
|
||||
std::size_t lanes_ = 1;
|
||||
wire_policy pol_{};
|
||||
deadlines lim_{};
|
||||
bool development_ = false;
|
||||
#if DPF_HAS_OPENSSL
|
||||
std::shared_ptr<tls_context> ctx_;
|
||||
#endif
|
||||
std::unique_ptr<asio::ip::tcp::acceptor> acceptor_;
|
||||
};
|
||||
|
||||
} // namespace net
|
||||
} // namespace dpf
|
||||
|
||||
#endif // LIBDPF_INCLUDE_DPF_NET_CLIENT_LINK_HPP__
|
||||
58
include/dpf/net/comm_hook.hpp
Normal file
58
include/dpf/net/comm_hook.hpp
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
/// @file dpf/net/comm_hook.hpp
|
||||
/// @brief Replaceable transport under trio helpers.
|
||||
/// @details Protocols call `trio::exchange_with`, `send_to` / `recv_from`,
|
||||
/// `deal` / `accept_deal`, and `trio::batch`. Those helpers go through
|
||||
/// a `comm_hook` when one is installed. A null hook keeps the current
|
||||
/// framed mesh. `channel::send` / `recv` stay the raw bypass.
|
||||
#ifndef LIBDPF_INCLUDE_DPF_NET_COMM_HOOK_HPP__
|
||||
#define LIBDPF_INCLUDE_DPF_NET_COMM_HOOK_HPP__
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "dpf/net/channel.hpp"
|
||||
#include "dpf/net/round_sink.hpp"
|
||||
|
||||
namespace dpf
|
||||
{
|
||||
namespace net
|
||||
{
|
||||
|
||||
class trio;
|
||||
|
||||
/// @brief Byte-level transport for trio helpers and RoundSink factories.
|
||||
/// @details Overrides replace the mesh without changing protocol call sites.
|
||||
/// Peer ids are `0..2` (`role` as unsigned). Default implementations
|
||||
/// in `mesh_comm_hook` match today's `channel` framing.
|
||||
class comm_hook
|
||||
{
|
||||
public:
|
||||
virtual ~comm_hook() = default;
|
||||
|
||||
/// @brief One-way send on the link to `peer_id`.
|
||||
virtual void send_bytes(trio & net, unsigned peer_id, msg tag,
|
||||
const void * data, std::size_t n) = 0;
|
||||
|
||||
/// @brief One-way receive from `peer_id`.
|
||||
virtual std::vector<std::uint8_t> recv_bytes(trio & net, unsigned peer_id,
|
||||
msg tag) = 0;
|
||||
|
||||
/// @brief Duplex exchange. Lower role id sends first.
|
||||
virtual std::vector<std::uint8_t> exchange_bytes(trio & net,
|
||||
unsigned peer_id, msg tag, const void * data, std::size_t n) = 0;
|
||||
|
||||
/// @brief Homogeneous vector exchange (one frame each way, same length).
|
||||
virtual std::vector<std::uint8_t> exchange_vec_bytes(trio & net,
|
||||
unsigned peer_id, msg tag, const void * data, std::size_t nbytes) = 0;
|
||||
|
||||
/// @brief Round-batched peer sink for `count` instances.
|
||||
virtual std::unique_ptr<RoundSink> batch(trio & net, std::size_t count,
|
||||
std::vector<std::size_t> slot_bytes) = 0;
|
||||
};
|
||||
|
||||
} // namespace net
|
||||
} // namespace dpf
|
||||
|
||||
#endif // LIBDPF_INCLUDE_DPF_NET_COMM_HOOK_HPP__
|
||||
263
include/dpf/net/connect.hpp
Normal file
263
include/dpf/net/connect.hpp
Normal file
|
|
@ -0,0 +1,263 @@
|
|||
/// @file dpf/net/connect.hpp
|
||||
/// @brief Deadline-bounded connect, accept, and handshake I/O for setup.
|
||||
/// @details Setup is blocking by design (it happens before protocol traffic),
|
||||
/// but every step has a wall-clock bound. `connect_until` retries
|
||||
/// refused connections until the deadline, so processes may start in
|
||||
/// any order.
|
||||
#ifndef LIBDPF_INCLUDE_DPF_NET_CONNECT_HPP__
|
||||
#define LIBDPF_INCLUDE_DPF_NET_CONNECT_HPP__
|
||||
|
||||
#include <cerrno>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <system_error>
|
||||
#include <thread>
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <poll.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/types.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "dpf/net/asio_ns.hpp"
|
||||
|
||||
#include "dpf/log.hpp"
|
||||
|
||||
namespace dpf
|
||||
{
|
||||
namespace net
|
||||
{
|
||||
|
||||
using setup_clock = std::chrono::steady_clock;
|
||||
|
||||
namespace detail
|
||||
{
|
||||
|
||||
inline int remaining_ms(setup_clock::time_point deadline)
|
||||
{
|
||||
const auto left = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
deadline - setup_clock::now());
|
||||
if (left.count() <= 0)
|
||||
return 0;
|
||||
if (left.count() > 0x7fffffff)
|
||||
return 0x7fffffff;
|
||||
return static_cast<int>(left.count());
|
||||
}
|
||||
|
||||
/// @brief Wait until `fd` is ready for `events` or throw at `deadline`.
|
||||
inline void wait_fd(int fd, short events, setup_clock::time_point deadline,
|
||||
const std::string & what)
|
||||
{
|
||||
for (;;)
|
||||
{
|
||||
const int ms = remaining_ms(deadline);
|
||||
if (ms == 0)
|
||||
throw std::system_error(std::make_error_code(std::errc::timed_out),
|
||||
what + ": timed out");
|
||||
pollfd pfd{};
|
||||
pfd.fd = fd;
|
||||
pfd.events = events;
|
||||
const int rc = ::poll(&pfd, 1, ms);
|
||||
if (rc > 0)
|
||||
{
|
||||
if ((pfd.revents & (POLLERR | POLLNVAL)) != 0
|
||||
&& (pfd.revents & events) == 0)
|
||||
throw std::system_error(
|
||||
std::make_error_code(std::errc::connection_reset), what);
|
||||
return;
|
||||
}
|
||||
if (rc < 0 && errno != EINTR)
|
||||
throw std::system_error(errno, std::generic_category(), what);
|
||||
}
|
||||
}
|
||||
|
||||
inline void send_all_until(int fd, const void * data, std::size_t n,
|
||||
setup_clock::time_point deadline, const std::string & what)
|
||||
{
|
||||
const auto * p = static_cast<const std::uint8_t *>(data);
|
||||
std::size_t done = 0;
|
||||
while (done < n)
|
||||
{
|
||||
const ssize_t r = ::send(fd, p + done, n - done,
|
||||
MSG_DONTWAIT | MSG_NOSIGNAL);
|
||||
if (r > 0)
|
||||
{
|
||||
done += static_cast<std::size_t>(r);
|
||||
continue;
|
||||
}
|
||||
if (r < 0 && (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR))
|
||||
{
|
||||
wait_fd(fd, POLLOUT, deadline, what);
|
||||
continue;
|
||||
}
|
||||
throw std::system_error(errno, std::generic_category(), what);
|
||||
}
|
||||
}
|
||||
|
||||
inline void recv_all_until(int fd, void * data, std::size_t n,
|
||||
setup_clock::time_point deadline, const std::string & what)
|
||||
{
|
||||
auto * p = static_cast<std::uint8_t *>(data);
|
||||
std::size_t done = 0;
|
||||
while (done < n)
|
||||
{
|
||||
const ssize_t r = ::recv(fd, p + done, n - done, MSG_DONTWAIT);
|
||||
if (r > 0)
|
||||
{
|
||||
done += static_cast<std::size_t>(r);
|
||||
continue;
|
||||
}
|
||||
if (r == 0)
|
||||
throw std::system_error(std::make_error_code(
|
||||
std::errc::connection_reset), what + ": peer closed");
|
||||
if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR)
|
||||
{
|
||||
wait_fd(fd, POLLIN, deadline, what);
|
||||
continue;
|
||||
}
|
||||
throw std::system_error(errno, std::generic_category(), what);
|
||||
}
|
||||
}
|
||||
|
||||
inline void put_u32(std::uint8_t * dst, std::uint32_t v) noexcept
|
||||
{
|
||||
dst[0] = static_cast<std::uint8_t>(v & 0xffu);
|
||||
dst[1] = static_cast<std::uint8_t>((v >> 8) & 0xffu);
|
||||
dst[2] = static_cast<std::uint8_t>((v >> 16) & 0xffu);
|
||||
dst[3] = static_cast<std::uint8_t>((v >> 24) & 0xffu);
|
||||
}
|
||||
|
||||
inline std::uint32_t get_u32(const std::uint8_t * src) noexcept
|
||||
{
|
||||
return static_cast<std::uint32_t>(src[0]) | (static_cast<std::uint32_t>(src[1]) << 8)
|
||||
| (static_cast<std::uint32_t>(src[2]) << 16)
|
||||
| (static_cast<std::uint32_t>(src[3]) << 24);
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
/// @brief Connect `sock` to `host:port`, retrying refusals until `budget`.
|
||||
inline void connect_until(asio::ip::tcp::socket & sock, const std::string & host,
|
||||
unsigned short port, std::chrono::milliseconds budget)
|
||||
{
|
||||
const auto started = setup_clock::now();
|
||||
const auto deadline = started + budget;
|
||||
const std::string what = "connect " + host + ":" + std::to_string(port);
|
||||
asio::ip::tcp::resolver res(sock.get_executor());
|
||||
std::error_code rec;
|
||||
auto eps = res.resolve(host, std::to_string(port), rec);
|
||||
if (rec)
|
||||
throw std::system_error(rec, what + ": resolve");
|
||||
std::error_code last = std::make_error_code(std::errc::timed_out);
|
||||
std::size_t attempts = 0;
|
||||
for (;;)
|
||||
{
|
||||
for (const auto & entry : eps)
|
||||
{
|
||||
std::error_code ec;
|
||||
if (sock.is_open())
|
||||
sock.close(ec);
|
||||
sock.open(entry.endpoint().protocol(), ec);
|
||||
if (ec)
|
||||
{
|
||||
last = ec;
|
||||
continue;
|
||||
}
|
||||
const int fd = sock.native_handle();
|
||||
const int fl = ::fcntl(fd, F_GETFL, 0);
|
||||
(void)::fcntl(fd, F_SETFL, fl | O_NONBLOCK);
|
||||
++attempts;
|
||||
int rc = ::connect(fd, entry.endpoint().data(),
|
||||
static_cast<socklen_t>(entry.endpoint().size()));
|
||||
int err = rc == 0 ? 0 : errno;
|
||||
if (rc != 0 && err == EINPROGRESS)
|
||||
{
|
||||
pollfd pfd{};
|
||||
pfd.fd = fd;
|
||||
pfd.events = POLLOUT;
|
||||
const int prc = ::poll(&pfd, 1, detail::remaining_ms(deadline));
|
||||
if (prc <= 0)
|
||||
err = ETIMEDOUT;
|
||||
else
|
||||
{
|
||||
socklen_t len = sizeof(err);
|
||||
err = 0;
|
||||
::getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &len);
|
||||
}
|
||||
}
|
||||
if (err == 0)
|
||||
{
|
||||
(void)::fcntl(fd, F_SETFL, fl);
|
||||
DPF_LOG(debug, "connect").kv("target", host + ":" + std::to_string(port))
|
||||
.kv("resolved", entry.endpoint().address().to_string())
|
||||
.kv("attempts", attempts)
|
||||
.kv("elapsed_ms", std::chrono::duration<double, std::milli>(
|
||||
setup_clock::now() - started).count());
|
||||
return;
|
||||
}
|
||||
last = std::error_code(err, std::generic_category());
|
||||
sock.close(ec);
|
||||
}
|
||||
if (setup_clock::now() >= deadline)
|
||||
{
|
||||
DPF_LOG(error, "connect.failed").kv("target", host + ":" + std::to_string(port))
|
||||
.kv("attempts", attempts).kv("budget_ms", budget.count())
|
||||
.kv("last_error", last.message());
|
||||
throw std::system_error(last, what + " failed after "
|
||||
+ std::to_string(budget.count()) + " ms");
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(20));
|
||||
}
|
||||
}
|
||||
|
||||
/// @brief Accept one connection on `acc` within `budget`.
|
||||
inline void accept_until(asio::ip::tcp::acceptor & acc,
|
||||
asio::ip::tcp::socket & sock, std::chrono::milliseconds budget)
|
||||
{
|
||||
const auto deadline = setup_clock::now() + budget;
|
||||
const std::string what = "accept on port "
|
||||
+ std::to_string(acc.local_endpoint().port());
|
||||
detail::wait_fd(acc.native_handle(), POLLIN, deadline, what);
|
||||
acc.accept(sock);
|
||||
}
|
||||
|
||||
/// @brief Open, bind, and listen on `port` (0 = ephemeral), reusing the address.
|
||||
inline void open_listener(asio::ip::tcp::acceptor & acc, unsigned short port)
|
||||
{
|
||||
const asio::ip::tcp::endpoint ep(asio::ip::tcp::v4(), port);
|
||||
acc.open(ep.protocol());
|
||||
acc.set_option(asio::socket_base::reuse_address(true));
|
||||
acc.bind(ep);
|
||||
acc.listen();
|
||||
}
|
||||
|
||||
/// @brief Exchange one `u32` each way within `budget` (setup handshake).
|
||||
inline std::uint32_t exchange_u32(int fd, std::uint32_t mine,
|
||||
std::chrono::milliseconds budget, const std::string & what)
|
||||
{
|
||||
const auto deadline = setup_clock::now() + budget;
|
||||
std::uint8_t out[4];
|
||||
detail::put_u32(out, mine);
|
||||
detail::send_all_until(fd, out, 4, deadline, what);
|
||||
std::uint8_t in[4];
|
||||
detail::recv_all_until(fd, in, 4, deadline, what);
|
||||
return detail::get_u32(in);
|
||||
}
|
||||
|
||||
/// @brief Send then receive a fixed-size record within `budget`.
|
||||
inline void exchange_record(int fd, const void * mine, void * theirs,
|
||||
std::size_t n, std::chrono::milliseconds budget, const std::string & what)
|
||||
{
|
||||
const auto deadline = setup_clock::now() + budget;
|
||||
detail::send_all_until(fd, mine, n, deadline, what);
|
||||
detail::recv_all_until(fd, theirs, n, deadline, what);
|
||||
}
|
||||
|
||||
} // namespace net
|
||||
} // namespace dpf
|
||||
|
||||
#endif // LIBDPF_INCLUDE_DPF_NET_CONNECT_HPP__
|
||||
221
include/dpf/net/dealer_cursor.hpp
Normal file
221
include/dpf/net/dealer_cursor.hpp
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
/// @file dpf/net/dealer_cursor.hpp
|
||||
/// @brief Per-(round, index) correction words for a batch session.
|
||||
#ifndef LIBDPF_INCLUDE_DPF_NET_DEALER_CURSOR_HPP__
|
||||
#define LIBDPF_INCLUDE_DPF_NET_DEALER_CURSOR_HPP__
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <stdexcept>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "dpf/net/stream_array.hpp"
|
||||
|
||||
namespace dpf
|
||||
{
|
||||
namespace net
|
||||
{
|
||||
|
||||
/// @brief Empty correction / blind marker. Protocols that need no dealer
|
||||
/// material specialize on this type and skip the cursor.
|
||||
struct empty_pad
|
||||
{
|
||||
};
|
||||
|
||||
/// @brief Seekable table of dealer corrections, one lane per round.
|
||||
/// @details Size is fixed at construction: `count` instances and one slot
|
||||
/// width per round. Live dealers and inbox tapes both fill this
|
||||
/// table before the online session runs, or fill it lazily through
|
||||
/// `put`. Sessions read with `at(round, index)`.
|
||||
class dealer_cursor
|
||||
{
|
||||
public:
|
||||
dealer_cursor() = default;
|
||||
|
||||
dealer_cursor(std::size_t count, std::vector<std::size_t> slot_bytes)
|
||||
: count_(count),
|
||||
slot_bytes_(std::move(slot_bytes)),
|
||||
offsets_(slot_bytes_.size() + 1, 0)
|
||||
{
|
||||
for (std::size_t r = 0; r < slot_bytes_.size(); ++r)
|
||||
offsets_[r + 1] = offsets_[r] + count_ * slot_bytes_[r];
|
||||
store_.assign(offsets_.back(), 0);
|
||||
ready_.assign(slot_bytes_.size() * count_, false);
|
||||
}
|
||||
|
||||
std::size_t count() const noexcept { return count_; }
|
||||
std::size_t rounds() const noexcept { return slot_bytes_.size(); }
|
||||
|
||||
std::size_t slot_bytes(std::uint16_t round) const
|
||||
{
|
||||
if (round >= slot_bytes_.size())
|
||||
throw std::out_of_range("dealer_cursor round");
|
||||
return slot_bytes_[round];
|
||||
}
|
||||
|
||||
void put(std::uint16_t round, std::size_t index, const std::uint8_t * bytes,
|
||||
std::size_t n)
|
||||
{
|
||||
check(round, index, n);
|
||||
std::memcpy(ptr(round, index), bytes, n);
|
||||
ready_[ready_index(round, index)] = true;
|
||||
}
|
||||
|
||||
/// @brief Fill every index of `round` from a contiguous dealer tape.
|
||||
void put_round(std::uint16_t round, const std::uint8_t * bytes,
|
||||
std::size_t nbytes)
|
||||
{
|
||||
if (round >= slot_bytes_.size())
|
||||
throw std::out_of_range("dealer_cursor round");
|
||||
const std::size_t need = count_ * slot_bytes_[round];
|
||||
if (nbytes != need)
|
||||
throw std::invalid_argument("dealer_cursor put_round size");
|
||||
if (need != 0)
|
||||
std::memcpy(store_.data() + offsets_[round], bytes, need);
|
||||
for (std::size_t i = 0; i < count_; ++i)
|
||||
ready_[ready_index(round, i)] = true;
|
||||
}
|
||||
|
||||
bool ready(std::uint16_t round, std::size_t index) const
|
||||
{
|
||||
if (round >= slot_bytes_.size() || index >= count_)
|
||||
return false;
|
||||
if (slot_bytes_[round] == 0)
|
||||
return true;
|
||||
return ready_[ready_index(round, index)];
|
||||
}
|
||||
|
||||
void at(std::uint16_t round, std::size_t index, std::uint8_t * out,
|
||||
std::size_t n) const
|
||||
{
|
||||
check(round, index, n);
|
||||
if (n == 0)
|
||||
return;
|
||||
if (!ready_[ready_index(round, index)])
|
||||
throw std::logic_error("dealer_cursor not ready");
|
||||
std::memcpy(out, ptr(round, index), n);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
T at(std::uint16_t round, std::size_t index) const
|
||||
{
|
||||
static_assert(std::is_trivially_copyable_v<T>,
|
||||
"dealer_cursor::at requires a trivially copyable type");
|
||||
if constexpr (std::is_same_v<T, empty_pad>)
|
||||
{
|
||||
(void)round;
|
||||
(void)index;
|
||||
return empty_pad{};
|
||||
}
|
||||
else
|
||||
{
|
||||
T out{};
|
||||
at(round, index, reinterpret_cast<std::uint8_t *>(&out), sizeof(T));
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
/// @brief Load each round from consecutive stream indexes (one tape per round).
|
||||
static dealer_cursor from_streams(stream_array & src, std::size_t count,
|
||||
std::vector<std::size_t> slot_bytes, std::size_t first_stream = 0)
|
||||
{
|
||||
dealer_cursor out(count, std::move(slot_bytes));
|
||||
for (std::uint16_t r = 0; r < out.rounds(); ++r)
|
||||
{
|
||||
const std::size_t need = count * out.slot_bytes(r);
|
||||
if (need == 0)
|
||||
continue;
|
||||
std::vector<std::uint8_t> buf(need);
|
||||
src.read(first_stream + r, buf.data(), need);
|
||||
out.put_round(r, buf.data(), need);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private:
|
||||
void check(std::uint16_t round, std::size_t index, std::size_t n) const
|
||||
{
|
||||
if (round >= slot_bytes_.size() || index >= count_)
|
||||
throw std::out_of_range("dealer_cursor index");
|
||||
if (n != slot_bytes_[round])
|
||||
throw std::invalid_argument("dealer_cursor size");
|
||||
}
|
||||
|
||||
std::size_t ready_index(std::uint16_t round, std::size_t index) const noexcept
|
||||
{
|
||||
return static_cast<std::size_t>(round) * count_ + index;
|
||||
}
|
||||
|
||||
std::uint8_t * ptr(std::uint16_t round, std::size_t index) noexcept
|
||||
{
|
||||
return store_.data() + offsets_[round] + index * slot_bytes_[round];
|
||||
}
|
||||
|
||||
const std::uint8_t * ptr(std::uint16_t round, std::size_t index) const noexcept
|
||||
{
|
||||
return store_.data() + offsets_[round] + index * slot_bytes_[round];
|
||||
}
|
||||
|
||||
std::size_t count_ = 0;
|
||||
std::vector<std::size_t> slot_bytes_;
|
||||
std::vector<std::size_t> offsets_;
|
||||
std::vector<std::uint8_t> store_;
|
||||
std::vector<bool> ready_;
|
||||
};
|
||||
|
||||
/// @brief Prefetch a full dealer table from `stream_array` tapes (see `from_streams`).
|
||||
class stream_dealer_cursor
|
||||
{
|
||||
public:
|
||||
stream_dealer_cursor(stream_array & src, std::size_t count,
|
||||
std::vector<std::size_t> slot_bytes, std::size_t first_stream = 0)
|
||||
: inner_(dealer_cursor::from_streams(src, count, std::move(slot_bytes),
|
||||
first_stream))
|
||||
{
|
||||
}
|
||||
|
||||
std::size_t count() const noexcept { return inner_.count(); }
|
||||
std::size_t rounds() const noexcept { return inner_.rounds(); }
|
||||
|
||||
std::size_t slot_bytes(std::uint16_t round) const
|
||||
{
|
||||
return inner_.slot_bytes(round);
|
||||
}
|
||||
|
||||
bool ready(std::uint16_t round, std::size_t index) const
|
||||
{
|
||||
return inner_.ready(round, index);
|
||||
}
|
||||
|
||||
void at(std::uint16_t round, std::size_t index, std::uint8_t * out,
|
||||
std::size_t n) const
|
||||
{
|
||||
inner_.at(round, index, out, n);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
T at(std::uint16_t round, std::size_t index) const
|
||||
{
|
||||
return inner_.at<T>(round, index);
|
||||
}
|
||||
|
||||
const dealer_cursor & cursor() const noexcept { return inner_; }
|
||||
dealer_cursor & cursor() noexcept { return inner_; }
|
||||
|
||||
private:
|
||||
dealer_cursor inner_;
|
||||
};
|
||||
|
||||
/// @brief One-round table: `nbytes` must equal `count * slot_bytes[0]`.
|
||||
inline dealer_cursor dealer_cursor_from_stream(stream_array & a,
|
||||
std::size_t stream_index, std::size_t count, std::size_t slot_nbytes)
|
||||
{
|
||||
return dealer_cursor::from_streams(a, count, {slot_nbytes}, stream_index);
|
||||
}
|
||||
|
||||
} // namespace net
|
||||
} // namespace dpf
|
||||
|
||||
#endif // LIBDPF_INCLUDE_DPF_NET_DEALER_CURSOR_HPP__
|
||||
246
include/dpf/net/edge_mesh.hpp
Normal file
246
include/dpf/net/edge_mesh.hpp
Normal file
|
|
@ -0,0 +1,246 @@
|
|||
/// @file dpf/net/edge_mesh.hpp
|
||||
/// @brief N-edge RoundSink mesh for star / dealer / 4PC topologies.
|
||||
#ifndef LIBDPF_INCLUDE_DPF_NET_EDGE_MESH_HPP__
|
||||
#define LIBDPF_INCLUDE_DPF_NET_EDGE_MESH_HPP__
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "dpf/net/memory_sink.hpp"
|
||||
#include "dpf/net/round_sink.hpp"
|
||||
|
||||
namespace dpf
|
||||
{
|
||||
namespace net
|
||||
{
|
||||
|
||||
/// @brief Index of a duplex link in an `edge_mesh`.
|
||||
using edge_id = std::uint16_t;
|
||||
|
||||
/// @brief Named edges for the common 2PC / RSS / dealer trio (also mesh ids 0..2).
|
||||
/// @details For the dealer (party 2), `edge_dealer` is its link to party 0 and
|
||||
/// `edge_dealer_p1` its link to party 1.
|
||||
inline constexpr edge_id edge_peer = 0;
|
||||
inline constexpr edge_id edge_rss_next = 1;
|
||||
inline constexpr edge_id edge_dealer = 2;
|
||||
inline constexpr edge_id edge_dealer_p1 = 3;
|
||||
|
||||
inline std::string edge_name(edge_id e)
|
||||
{
|
||||
switch (e)
|
||||
{
|
||||
case edge_peer:
|
||||
return "peer";
|
||||
case edge_rss_next:
|
||||
return "rss_next";
|
||||
case edge_dealer:
|
||||
return "dealer";
|
||||
case edge_dealer_p1:
|
||||
return "dealer->p1";
|
||||
default:
|
||||
return "edge " + std::to_string(e);
|
||||
}
|
||||
}
|
||||
|
||||
/// @brief Collection of duplex RoundSinks keyed by `edge_id`.
|
||||
struct edge_mesh
|
||||
{
|
||||
std::vector<RoundSink *> sinks;
|
||||
|
||||
RoundSink & at(edge_id id) const
|
||||
{
|
||||
if (static_cast<std::size_t>(id) >= sinks.size() || sinks[id] == nullptr)
|
||||
throw std::logic_error("edge_mesh: edge not bound");
|
||||
return *sinks[id];
|
||||
}
|
||||
|
||||
bool has(edge_id id) const noexcept
|
||||
{
|
||||
return static_cast<std::size_t>(id) < sinks.size()
|
||||
&& sinks[id] != nullptr;
|
||||
}
|
||||
|
||||
std::size_t size() const noexcept { return sinks.size(); }
|
||||
|
||||
void flush_all()
|
||||
{
|
||||
for (auto * s : sinks)
|
||||
{
|
||||
if (s == nullptr)
|
||||
continue;
|
||||
s->flush();
|
||||
s->poll();
|
||||
}
|
||||
}
|
||||
|
||||
/// @brief Sum of `progress()` over distinct bound sinks.
|
||||
std::uint64_t progress_total() const noexcept
|
||||
{
|
||||
std::uint64_t total = 0;
|
||||
for (std::size_t i = 0; i < sinks.size(); ++i)
|
||||
{
|
||||
const RoundSink * s = sinks[i];
|
||||
if (s == nullptr)
|
||||
continue;
|
||||
bool seen = false;
|
||||
for (std::size_t j = 0; j < i && !seen; ++j)
|
||||
seen = sinks[j] == s;
|
||||
if (!seen)
|
||||
total += s->progress();
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
/// @brief Block until at least one bound sink makes I/O progress.
|
||||
/// @details Returns true if any sink's `wait_io()` ran a real event (an
|
||||
/// async sink slept in `epoll`); false if none did (memory sinks),
|
||||
/// so the caller can fall back to its spin guard.
|
||||
bool wait_io_all()
|
||||
{
|
||||
bool progressed = false;
|
||||
for (auto * s : sinks)
|
||||
{
|
||||
if (s == nullptr)
|
||||
continue;
|
||||
if (s->wait_io())
|
||||
progressed = true;
|
||||
}
|
||||
return progressed;
|
||||
}
|
||||
};
|
||||
|
||||
/// @brief In-process star: one client edge per server, matching server ends.
|
||||
struct memory_star
|
||||
{
|
||||
std::size_t servers = 0;
|
||||
std::vector<std::shared_ptr<memory_sink_hub>> hubs;
|
||||
std::vector<memory_sink> client; ///< client side of edge i
|
||||
std::vector<memory_sink> server; ///< server i side of edge i
|
||||
|
||||
/// @brief Client mesh: edges `[0, servers)`.
|
||||
edge_mesh client_mesh()
|
||||
{
|
||||
edge_mesh m;
|
||||
m.sinks.resize(client.size());
|
||||
for (std::size_t i = 0; i < client.size(); ++i)
|
||||
m.sinks[i] = &client[i];
|
||||
return m;
|
||||
}
|
||||
|
||||
/// @brief Server `i` mesh with a single live edge at `edge_id{i}` (sparse).
|
||||
/// Prefer `server_edge(i)` when the schedule uses `edge_id{0}` locally.
|
||||
edge_mesh server_mesh_at(std::size_t i)
|
||||
{
|
||||
if (i >= server.size())
|
||||
throw std::out_of_range("memory_star server");
|
||||
edge_mesh m;
|
||||
m.sinks.assign(server.size(), nullptr);
|
||||
m.sinks[i] = &server[i];
|
||||
return m;
|
||||
}
|
||||
|
||||
/// @brief Server `i` as a one-edge mesh (`edge_id` 0 → that duplex).
|
||||
edge_mesh server_edge(std::size_t i)
|
||||
{
|
||||
if (i >= server.size())
|
||||
throw std::out_of_range("memory_star server");
|
||||
return edge_mesh{{&server[i]}};
|
||||
}
|
||||
};
|
||||
|
||||
/// @brief Build an in-process client↔N-server star.
|
||||
/// @param slot_bytes Round widths shared by every edge (same schedule shape).
|
||||
inline memory_star make_memory_star(std::size_t n_servers, std::size_t count,
|
||||
std::vector<std::size_t> slot_bytes)
|
||||
{
|
||||
if (n_servers < 2)
|
||||
throw std::invalid_argument("make_memory_star needs >= 2 servers");
|
||||
memory_star star;
|
||||
star.servers = n_servers;
|
||||
star.hubs.reserve(n_servers);
|
||||
star.client.reserve(n_servers);
|
||||
star.server.reserve(n_servers);
|
||||
for (std::size_t i = 0; i < n_servers; ++i)
|
||||
{
|
||||
auto hub = std::make_shared<memory_sink_hub>(count, slot_bytes);
|
||||
star.hubs.push_back(hub);
|
||||
star.client.emplace_back(hub, true);
|
||||
star.server.emplace_back(hub, false);
|
||||
}
|
||||
return star;
|
||||
}
|
||||
|
||||
/// @brief Fully connected memory clique of `n` roles (every unordered pair).
|
||||
/// @details Edge id for ordered pair (a,b) with a<b is the combinatorial
|
||||
/// index; both directions share one hub (a is side_a).
|
||||
struct memory_clique
|
||||
{
|
||||
std::size_t roles = 0;
|
||||
std::vector<std::shared_ptr<memory_sink_hub>> hubs;
|
||||
/// hubs[edge], ends[edge].first = lower role, .second = higher role
|
||||
std::vector<std::pair<memory_sink, memory_sink>> ends;
|
||||
|
||||
static edge_id pair_edge(std::size_t a, std::size_t b, std::size_t n)
|
||||
{
|
||||
if (a == b || a >= n || b >= n)
|
||||
throw std::invalid_argument("memory_clique pair");
|
||||
if (a > b)
|
||||
std::swap(a, b);
|
||||
// Index among pairs (i,j) with i<j.
|
||||
edge_id e = 0;
|
||||
for (std::size_t i = 0; i < a; ++i)
|
||||
e = static_cast<edge_id>(e + (n - 1 - i));
|
||||
e = static_cast<edge_id>(e + (b - a - 1));
|
||||
return e;
|
||||
}
|
||||
|
||||
RoundSink & end(std::size_t role, std::size_t peer)
|
||||
{
|
||||
const auto e = pair_edge(role, peer, roles);
|
||||
if (role < peer)
|
||||
return ends[e].first;
|
||||
return ends[e].second;
|
||||
}
|
||||
|
||||
edge_mesh mesh_for(std::size_t role)
|
||||
{
|
||||
edge_mesh m;
|
||||
m.sinks.assign(ends.size(), nullptr);
|
||||
for (std::size_t p = 0; p < roles; ++p)
|
||||
{
|
||||
if (p == role)
|
||||
continue;
|
||||
m.sinks[pair_edge(role, p, roles)] = &end(role, p);
|
||||
}
|
||||
return m;
|
||||
}
|
||||
};
|
||||
|
||||
inline memory_clique make_memory_clique(std::size_t n_roles, std::size_t count,
|
||||
std::vector<std::size_t> slot_bytes)
|
||||
{
|
||||
if (n_roles < 2)
|
||||
throw std::invalid_argument("make_memory_clique needs >= 2 roles");
|
||||
memory_clique c;
|
||||
c.roles = n_roles;
|
||||
const std::size_t n_edges = n_roles * (n_roles - 1) / 2;
|
||||
c.hubs.reserve(n_edges);
|
||||
c.ends.reserve(n_edges);
|
||||
for (std::size_t e = 0; e < n_edges; ++e)
|
||||
{
|
||||
auto hub = std::make_shared<memory_sink_hub>(count, slot_bytes);
|
||||
c.hubs.push_back(hub);
|
||||
c.ends.emplace_back(memory_sink(hub, true), memory_sink(hub, false));
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
} // namespace net
|
||||
} // namespace dpf
|
||||
|
||||
#endif // LIBDPF_INCLUDE_DPF_NET_EDGE_MESH_HPP__
|
||||
378
include/dpf/net/identity.hpp
Normal file
378
include/dpf/net/identity.hpp
Normal file
|
|
@ -0,0 +1,378 @@
|
|||
/// @file dpf/net/identity.hpp
|
||||
/// @brief Party identity keys (Ed25519) and their text and file forms.
|
||||
/// @details A party is identified by a raw 32-byte Ed25519 public key, written
|
||||
/// as 44 characters of base64 (the same shape as a WireGuard key). A
|
||||
/// key file holds the 32-byte private seed as one base64 line and is
|
||||
/// created mode 0600. TLS needs a certificate, so `identity` also
|
||||
/// carries a self-signed certificate generated in memory from the
|
||||
/// key; peers check the key inside it, never the certificate fields.
|
||||
/// `development()` is a fixed, publicly known identity: it keeps the
|
||||
/// client path working with no configuration and provides no security.
|
||||
#ifndef LIBDPF_INCLUDE_DPF_NET_IDENTITY_HPP__
|
||||
#define LIBDPF_INCLUDE_DPF_NET_IDENTITY_HPP__
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cerrno>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#ifndef DPF_HAS_OPENSSL
|
||||
#if defined(__has_include)
|
||||
#if __has_include(<openssl/ssl.h>)
|
||||
#define DPF_HAS_OPENSSL 1
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
#ifndef DPF_HAS_OPENSSL
|
||||
#define DPF_HAS_OPENSSL 0
|
||||
#endif
|
||||
|
||||
#if DPF_HAS_OPENSSL
|
||||
#include <openssl/err.h>
|
||||
#include <openssl/evp.h>
|
||||
#include <openssl/rand.h>
|
||||
#include <openssl/x509.h>
|
||||
#endif
|
||||
|
||||
#include "dpf/log.hpp"
|
||||
|
||||
namespace dpf
|
||||
{
|
||||
namespace net
|
||||
{
|
||||
namespace detail
|
||||
{
|
||||
|
||||
inline std::string base64_encode(const std::uint8_t * p, std::size_t n)
|
||||
{
|
||||
static const char tab[] =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
std::string out;
|
||||
out.reserve((n + 2) / 3 * 4);
|
||||
for (std::size_t i = 0; i < n; i += 3)
|
||||
{
|
||||
const std::uint32_t a = p[i];
|
||||
const std::uint32_t b = i + 1 < n ? p[i + 1] : 0;
|
||||
const std::uint32_t c = i + 2 < n ? p[i + 2] : 0;
|
||||
const std::uint32_t v = (a << 16) | (b << 8) | c;
|
||||
out += tab[(v >> 18) & 63];
|
||||
out += tab[(v >> 12) & 63];
|
||||
out += i + 1 < n ? tab[(v >> 6) & 63] : '=';
|
||||
out += i + 2 < n ? tab[v & 63] : '=';
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/// @brief Strict base64 (standard alphabet, padded). Returns false on any
|
||||
/// malformed input.
|
||||
inline bool base64_decode(const std::string & s, std::string & out)
|
||||
{
|
||||
auto val = [](char ch) -> int {
|
||||
if (ch >= 'A' && ch <= 'Z')
|
||||
return ch - 'A';
|
||||
if (ch >= 'a' && ch <= 'z')
|
||||
return ch - 'a' + 26;
|
||||
if (ch >= '0' && ch <= '9')
|
||||
return ch - '0' + 52;
|
||||
if (ch == '+')
|
||||
return 62;
|
||||
if (ch == '/')
|
||||
return 63;
|
||||
return -1;
|
||||
};
|
||||
out.clear();
|
||||
if (s.size() % 4 != 0)
|
||||
return false;
|
||||
for (std::size_t i = 0; i < s.size(); i += 4)
|
||||
{
|
||||
const bool last = i + 4 == s.size();
|
||||
int v[4];
|
||||
for (int k = 0; k < 4; ++k)
|
||||
{
|
||||
const char ch = s[i + k];
|
||||
if (ch == '=' && last && k >= 2)
|
||||
v[k] = -2;
|
||||
else
|
||||
v[k] = val(ch);
|
||||
if (v[k] == -1)
|
||||
return false;
|
||||
}
|
||||
if (v[0] < 0 || v[1] < 0 || (v[2] == -2 && v[3] != -2))
|
||||
return false;
|
||||
const std::uint32_t x = (static_cast<std::uint32_t>(v[0]) << 18)
|
||||
| (static_cast<std::uint32_t>(v[1]) << 12)
|
||||
| (static_cast<std::uint32_t>(v[2] < 0 ? 0 : v[2]) << 6)
|
||||
| static_cast<std::uint32_t>(v[3] < 0 ? 0 : v[3]);
|
||||
out += static_cast<char>((x >> 16) & 0xff);
|
||||
if (v[2] >= 0)
|
||||
out += static_cast<char>((x >> 8) & 0xff);
|
||||
if (v[3] >= 0)
|
||||
out += static_cast<char>(x & 0xff);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// @brief First line of `path` that is neither blank nor a `#` comment.
|
||||
inline std::string first_key_line(const std::string & path, const char * what)
|
||||
{
|
||||
std::ifstream in(path);
|
||||
if (!in)
|
||||
throw std::runtime_error(std::string(what) + ": cannot read '" + path + "'");
|
||||
std::string line;
|
||||
while (std::getline(in, line))
|
||||
{
|
||||
while (!line.empty()
|
||||
&& (line.back() == '\r' || line.back() == ' ' || line.back() == '\t'))
|
||||
line.pop_back();
|
||||
std::size_t b = 0;
|
||||
while (b < line.size() && (line[b] == ' ' || line[b] == '\t'))
|
||||
++b;
|
||||
line.erase(0, b);
|
||||
if (!line.empty() && line[0] != '#')
|
||||
return line;
|
||||
}
|
||||
throw std::runtime_error(std::string(what) + ": '" + path + "' holds no key");
|
||||
}
|
||||
|
||||
#if DPF_HAS_OPENSSL
|
||||
inline std::string openssl_error(const char * what)
|
||||
{
|
||||
std::string out = what;
|
||||
unsigned long e = 0;
|
||||
bool first = true;
|
||||
while ((e = ERR_get_error()) != 0)
|
||||
{
|
||||
char buf[256];
|
||||
ERR_error_string_n(e, buf, sizeof(buf));
|
||||
out += first ? ": " : "; ";
|
||||
out += buf;
|
||||
first = false;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
struct pkey_free
|
||||
{
|
||||
void operator()(EVP_PKEY * k) const noexcept { EVP_PKEY_free(k); }
|
||||
};
|
||||
struct x509_free
|
||||
{
|
||||
void operator()(X509 * x) const noexcept { X509_free(x); }
|
||||
};
|
||||
#endif
|
||||
|
||||
} // namespace detail
|
||||
|
||||
/// @brief A party's raw Ed25519 public key.
|
||||
struct public_key
|
||||
{
|
||||
static constexpr std::size_t size = 32;
|
||||
std::array<std::uint8_t, size> bytes{};
|
||||
|
||||
/// @brief 44 characters of base64.
|
||||
std::string base64() const { return detail::base64_encode(bytes.data(), size); }
|
||||
|
||||
/// @brief Parse base64, or read a public-key file given as `file:PATH`.
|
||||
static public_key parse(const std::string & text)
|
||||
{
|
||||
std::string s = text;
|
||||
if (s.rfind("file:", 0) == 0)
|
||||
s = detail::first_key_line(s.substr(5), "public key");
|
||||
std::string raw;
|
||||
if (!detail::base64_decode(s, raw) || raw.size() != size)
|
||||
throw std::invalid_argument("public key must be 32 bytes of base64 "
|
||||
"(44 characters), got '" + s + "'");
|
||||
public_key k;
|
||||
std::memcpy(k.bytes.data(), raw.data(), size);
|
||||
return k;
|
||||
}
|
||||
|
||||
friend bool operator==(const public_key & a, const public_key & b) noexcept
|
||||
{
|
||||
return a.bytes == b.bytes;
|
||||
}
|
||||
friend bool operator!=(const public_key & a, const public_key & b) noexcept
|
||||
{
|
||||
return !(a == b);
|
||||
}
|
||||
};
|
||||
|
||||
/// @brief A party's private key plus the self-signed certificate TLS presents.
|
||||
/// @details Copies share the key. Without OpenSSL every constructor throws.
|
||||
class identity
|
||||
{
|
||||
public:
|
||||
/// @brief A fresh random key.
|
||||
static identity generate()
|
||||
{
|
||||
#if DPF_HAS_OPENSSL
|
||||
std::uint8_t seed[32];
|
||||
if (RAND_bytes(seed, sizeof(seed)) != 1)
|
||||
throw std::runtime_error(detail::openssl_error("identity: RAND_bytes"));
|
||||
auto id = from_seed(seed);
|
||||
OPENSSL_cleanse(seed, sizeof(seed));
|
||||
return id;
|
||||
#else
|
||||
throw std::logic_error("identity: built without OpenSSL");
|
||||
#endif
|
||||
}
|
||||
|
||||
/// @brief The key whose private seed is `seed`.
|
||||
static identity from_seed(const std::uint8_t * seed, const char * cn = "libdpf party")
|
||||
{
|
||||
#if DPF_HAS_OPENSSL
|
||||
identity id;
|
||||
EVP_PKEY * k = EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, nullptr, seed, 32);
|
||||
if (k == nullptr)
|
||||
throw std::runtime_error(detail::openssl_error("identity: bad Ed25519 seed"));
|
||||
id.key_.reset(k, detail::pkey_free{});
|
||||
std::size_t n = public_key::size;
|
||||
if (EVP_PKEY_get_raw_public_key(k, id.pub_.bytes.data(), &n) != 1
|
||||
|| n != public_key::size)
|
||||
throw std::runtime_error(detail::openssl_error("identity: public key"));
|
||||
id.cert_ = self_signed(k, cn);
|
||||
return id;
|
||||
#else
|
||||
(void)seed;
|
||||
(void)cn;
|
||||
throw std::logic_error("identity: built without OpenSSL");
|
||||
#endif
|
||||
}
|
||||
|
||||
/// @brief Read a key file (one base64 line holding the 32-byte seed).
|
||||
/// @details Warns when the file is readable by group or others.
|
||||
static identity load(const std::string & path)
|
||||
{
|
||||
std::string raw;
|
||||
const auto line = detail::first_key_line(path, "identity");
|
||||
if (!detail::base64_decode(line, raw) || raw.size() != 32)
|
||||
throw std::invalid_argument("identity: '" + path
|
||||
+ "' is not a key file (expected 32 bytes of base64)");
|
||||
struct stat st{};
|
||||
if (::stat(path.c_str(), &st) == 0 && (st.st_mode & 077) != 0)
|
||||
DPF_LOG(warning, "security.key_file_mode").kv("path", path)
|
||||
.kv("detail", "identity key file is readable by group or others; "
|
||||
"chmod 600 it");
|
||||
auto id = from_seed(reinterpret_cast<const std::uint8_t *>(raw.data()));
|
||||
std::fill(raw.begin(), raw.end(), '\0');
|
||||
return id;
|
||||
}
|
||||
|
||||
/// @brief The fixed development identity. Its private key is in this
|
||||
/// source file, so it authenticates nothing.
|
||||
static const identity & development()
|
||||
{
|
||||
static const identity dev = [] {
|
||||
#if DPF_HAS_OPENSSL
|
||||
static const char phrase[] =
|
||||
"libdpf development identity (public; provides no security)";
|
||||
std::uint8_t seed[32];
|
||||
unsigned int n = sizeof(seed);
|
||||
if (EVP_Digest(phrase, sizeof(phrase) - 1, seed, &n, EVP_sha256(), nullptr)
|
||||
!= 1)
|
||||
throw std::runtime_error(detail::openssl_error("identity: digest"));
|
||||
auto id = from_seed(seed, "libdpf development certificate (no security)");
|
||||
id.development_ = true;
|
||||
return id;
|
||||
#else
|
||||
return identity();
|
||||
#endif
|
||||
}();
|
||||
if (!dev.key_)
|
||||
throw std::logic_error("identity: built without OpenSSL");
|
||||
return dev;
|
||||
}
|
||||
|
||||
/// @brief Write the private seed to a new file with mode 0600. Refuses to
|
||||
/// replace an existing file unless `overwrite`.
|
||||
void save(const std::string & path, bool overwrite = false) const
|
||||
{
|
||||
#if DPF_HAS_OPENSSL
|
||||
std::uint8_t seed[32];
|
||||
std::size_t n = sizeof(seed);
|
||||
if (!key_ || EVP_PKEY_get_raw_private_key(key_.get(), seed, &n) != 1 || n != 32)
|
||||
throw std::runtime_error(detail::openssl_error("identity: private key"));
|
||||
const std::string text = "# libdpf identity key (private; keep mode 600)\n"
|
||||
+ detail::base64_encode(seed, sizeof(seed)) + "\n";
|
||||
OPENSSL_cleanse(seed, sizeof(seed));
|
||||
const int flags = O_WRONLY | O_CREAT | O_CLOEXEC | (overwrite ? O_TRUNC : O_EXCL);
|
||||
const int fd = ::open(path.c_str(), flags, 0600);
|
||||
if (fd < 0)
|
||||
throw std::runtime_error("identity: cannot create '" + path + "': "
|
||||
+ std::strerror(errno));
|
||||
const bool ok = ::fchmod(fd, 0600) == 0
|
||||
&& ::write(fd, text.data(), text.size()) == static_cast<ssize_t>(text.size());
|
||||
::close(fd);
|
||||
if (!ok)
|
||||
throw std::runtime_error("identity: cannot write '" + path + "'");
|
||||
#else
|
||||
(void)path;
|
||||
(void)overwrite;
|
||||
throw std::logic_error("identity: built without OpenSSL");
|
||||
#endif
|
||||
}
|
||||
|
||||
const public_key & key() const noexcept { return pub_; }
|
||||
bool is_development() const noexcept { return development_; }
|
||||
|
||||
#if DPF_HAS_OPENSSL
|
||||
EVP_PKEY * pkey() const noexcept { return key_.get(); }
|
||||
X509 * cert() const noexcept { return cert_.get(); }
|
||||
#endif
|
||||
|
||||
private:
|
||||
identity() = default;
|
||||
|
||||
#if DPF_HAS_OPENSSL
|
||||
static std::shared_ptr<X509> self_signed(EVP_PKEY * k, const char * cn)
|
||||
{
|
||||
std::shared_ptr<X509> x(X509_new(), detail::x509_free{});
|
||||
if (!x)
|
||||
throw std::runtime_error(detail::openssl_error("identity: X509_new"));
|
||||
std::uint8_t serial[8];
|
||||
if (RAND_bytes(serial, sizeof(serial)) != 1)
|
||||
throw std::runtime_error(detail::openssl_error("identity: serial"));
|
||||
std::uint64_t s = 0;
|
||||
for (auto b : serial)
|
||||
s = (s << 8) | b;
|
||||
s &= 0x7fffffffffffffffull;
|
||||
bool ok = X509_set_version(x.get(), 2) == 1
|
||||
&& ASN1_INTEGER_set_uint64(X509_get_serialNumber(x.get()), s) == 1
|
||||
&& X509_gmtime_adj(X509_getm_notBefore(x.get()), -86400) != nullptr
|
||||
&& X509_time_adj_ex(X509_getm_notAfter(x.get()), 36500, 0, nullptr) != nullptr
|
||||
&& X509_set_pubkey(x.get(), k) == 1;
|
||||
X509_NAME * name = X509_get_subject_name(x.get());
|
||||
ok = ok && name != nullptr
|
||||
&& X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC,
|
||||
reinterpret_cast<const unsigned char *>(cn), -1, -1, 0)
|
||||
== 1
|
||||
&& X509_set_issuer_name(x.get(), name) == 1
|
||||
&& X509_sign(x.get(), k, nullptr) > 0;
|
||||
if (!ok)
|
||||
throw std::runtime_error(detail::openssl_error("identity: certificate"));
|
||||
return x;
|
||||
}
|
||||
|
||||
std::shared_ptr<EVP_PKEY> key_;
|
||||
std::shared_ptr<X509> cert_;
|
||||
#else
|
||||
std::shared_ptr<void> key_;
|
||||
#endif
|
||||
public_key pub_{};
|
||||
bool development_ = false;
|
||||
};
|
||||
|
||||
} // namespace net
|
||||
} // namespace dpf
|
||||
|
||||
#endif // LIBDPF_INCLUDE_DPF_NET_IDENTITY_HPP__
|
||||
88
include/dpf/net/io_pool.hpp
Normal file
88
include/dpf/net/io_pool.hpp
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
/// @file dpf/net/io_pool.hpp
|
||||
/// @brief Socket completion threads and a separate compute pool.
|
||||
/// @details `context()` is run by `io_threads` workers (0 = hardware
|
||||
/// concurrency). `post_compute` runs on `compute_threads` workers
|
||||
/// (0 = same as io), so a DPF evaluation never occupies a thread that
|
||||
/// should be completing reads and writes. Shutdown stops socket I/O
|
||||
/// first, then joins compute.
|
||||
#ifndef LIBDPF_INCLUDE_DPF_NET_IO_POOL_HPP__
|
||||
#define LIBDPF_INCLUDE_DPF_NET_IO_POOL_HPP__
|
||||
|
||||
#include <cstddef>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "dpf/net/asio_ns.hpp"
|
||||
#include <asio/thread_pool.hpp>
|
||||
|
||||
namespace dpf
|
||||
{
|
||||
namespace net
|
||||
{
|
||||
|
||||
class io_pool
|
||||
{
|
||||
public:
|
||||
explicit io_pool(std::size_t io_threads = 0, std::size_t compute_threads = 0)
|
||||
: work_(asio::make_work_guard(io_)),
|
||||
compute_n_(pick(compute_threads == 0 ? io_threads : compute_threads)),
|
||||
compute_(compute_n_)
|
||||
{
|
||||
const std::size_t n = pick(io_threads);
|
||||
threads_.reserve(n);
|
||||
for (std::size_t i = 0; i < n; ++i)
|
||||
threads_.emplace_back([this] { io_.run(); });
|
||||
}
|
||||
|
||||
io_pool(const io_pool &) = delete;
|
||||
io_pool & operator=(const io_pool &) = delete;
|
||||
|
||||
~io_pool()
|
||||
{
|
||||
work_.reset();
|
||||
io_.stop();
|
||||
for (auto & t : threads_)
|
||||
if (t.joinable())
|
||||
t.join();
|
||||
compute_.join();
|
||||
}
|
||||
|
||||
asio::io_context & context() noexcept { return io_; }
|
||||
std::size_t size() const noexcept { return threads_.size(); }
|
||||
std::size_t compute_size() const noexcept { return compute_n_; }
|
||||
|
||||
/// @brief Run `fn` on a socket worker.
|
||||
template <typename Fn>
|
||||
void post(Fn && fn)
|
||||
{
|
||||
asio::post(io_, std::forward<Fn>(fn));
|
||||
}
|
||||
|
||||
/// @brief Run `fn` on the compute pool, leaving socket workers free.
|
||||
template <typename Fn>
|
||||
void post_compute(Fn && fn)
|
||||
{
|
||||
asio::post(compute_, std::forward<Fn>(fn));
|
||||
}
|
||||
|
||||
private:
|
||||
static std::size_t pick(std::size_t n)
|
||||
{
|
||||
if (n != 0)
|
||||
return n;
|
||||
const auto hw = std::thread::hardware_concurrency();
|
||||
return hw == 0 ? 1 : hw;
|
||||
}
|
||||
|
||||
asio::io_context io_;
|
||||
asio::executor_work_guard<asio::io_context::executor_type> work_;
|
||||
std::size_t compute_n_ = 1;
|
||||
asio::thread_pool compute_;
|
||||
std::vector<std::thread> threads_;
|
||||
};
|
||||
|
||||
} // namespace net
|
||||
} // namespace dpf
|
||||
|
||||
#endif
|
||||
161
include/dpf/net/link_log.hpp
Normal file
161
include/dpf/net/link_log.hpp
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
/// @file dpf/net/link_log.hpp
|
||||
/// @brief Run-log records for listeners and established party links.
|
||||
/// @details `log_link_up` is called once per socket after the handshake and
|
||||
/// after the stream array has adopted and tuned it, so the socket
|
||||
/// options it reads back are the ones the kernel applied (Linux
|
||||
/// doubles `SO_SNDBUF`/`SO_RCVBUF` and clamps them to `wmem_max` and
|
||||
/// `rmem_max`). `TCP_INFO` at that point carries the kernel's RTT
|
||||
/// estimate from the connection setup and the handshake exchange.
|
||||
/// Encrypted links record the TLS version and cipher, how this side
|
||||
/// authenticated the peer (`auth=key` or `none`), the peer's key, and
|
||||
/// whether the peer authenticated this side. With encryption off the
|
||||
/// record says `auth=none encryption=none`, and the first plaintext
|
||||
/// link to an address off this host also raises one warning.
|
||||
#ifndef LIBDPF_INCLUDE_DPF_NET_LINK_LOG_HPP__
|
||||
#define LIBDPF_INCLUDE_DPF_NET_LINK_LOG_HPP__
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <netinet/in.h>
|
||||
#include <netinet/tcp.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
|
||||
#include "dpf/log.hpp"
|
||||
#include "dpf/net/policy.hpp"
|
||||
#include "dpf/net/security.hpp"
|
||||
|
||||
namespace dpf
|
||||
{
|
||||
namespace net
|
||||
{
|
||||
namespace detail
|
||||
{
|
||||
|
||||
inline std::string sockaddr_text(const sockaddr_storage & ss)
|
||||
{
|
||||
char host[INET6_ADDRSTRLEN] = {};
|
||||
if (ss.ss_family == AF_INET)
|
||||
{
|
||||
const auto & a = reinterpret_cast<const sockaddr_in &>(ss);
|
||||
if (::inet_ntop(AF_INET, &a.sin_addr, host, sizeof(host)) == nullptr)
|
||||
return "unknown";
|
||||
return std::string(host) + ":" + std::to_string(ntohs(a.sin_port));
|
||||
}
|
||||
if (ss.ss_family == AF_INET6)
|
||||
{
|
||||
const auto & a = reinterpret_cast<const sockaddr_in6 &>(ss);
|
||||
if (::inet_ntop(AF_INET6, &a.sin6_addr, host, sizeof(host)) == nullptr)
|
||||
return "unknown";
|
||||
return "[" + std::string(host) + "]:" + std::to_string(ntohs(a.sin6_port));
|
||||
}
|
||||
if (ss.ss_family == AF_UNIX)
|
||||
{
|
||||
const auto & a = reinterpret_cast<const sockaddr_un &>(ss);
|
||||
return std::string("unix:") + (a.sun_path[0] != '\0' ? a.sun_path : "(unnamed)");
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
inline bool loopback(const sockaddr_storage & ss)
|
||||
{
|
||||
if (ss.ss_family == AF_INET)
|
||||
return (ntohl(reinterpret_cast<const sockaddr_in &>(ss).sin_addr.s_addr) >> 24)
|
||||
== 127u;
|
||||
if (ss.ss_family == AF_INET6)
|
||||
{
|
||||
const auto & a = reinterpret_cast<const sockaddr_in6 &>(ss).sin6_addr;
|
||||
if (IN6_IS_ADDR_LOOPBACK(&a))
|
||||
return true;
|
||||
return IN6_IS_ADDR_V4MAPPED(&a) && a.s6_addr[12] == 127;
|
||||
}
|
||||
return ss.ss_family == AF_UNIX;
|
||||
}
|
||||
|
||||
inline int int_opt(int fd, int level, int name)
|
||||
{
|
||||
int v = -1;
|
||||
socklen_t len = sizeof(v);
|
||||
if (::getsockopt(fd, level, name, &v, &len) != 0)
|
||||
return -1;
|
||||
return v;
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
/// @brief Record a listener: this process accepts any address on `port`.
|
||||
/// @param encrypted whether connections on it must complete TLS 1.3 first
|
||||
inline void log_listen(unsigned short port, bool sctp, bool encrypted = false)
|
||||
{
|
||||
DPF_LOG(info, "listen").kv("addr", "0.0.0.0").kv("port", port)
|
||||
.kv("tcp", true).kv("sctp", sctp)
|
||||
.kv("encryption", encrypted ? "tls1.3" : "none");
|
||||
}
|
||||
|
||||
/// @brief Record one established socket of a party link.
|
||||
/// @param how `accept` or `connect`
|
||||
/// @param peer the other end's role (`p1`, `dealer`, ...)
|
||||
/// @param lane which socket of a `parallel` link (0 otherwise)
|
||||
/// @param sec how the link was secured (null or unencrypted: plaintext)
|
||||
inline void log_link_up(const char * how, const std::string & peer, transport kind,
|
||||
std::size_t lanes, std::uint32_t lane, std::uint32_t epoch, int fd,
|
||||
const socket_options & requested, const link_security * sec = nullptr)
|
||||
{
|
||||
const bool encrypted = sec != nullptr && sec->encrypted;
|
||||
if (!log::enabled(log::level::info) || fd < 0)
|
||||
return;
|
||||
sockaddr_storage local{};
|
||||
sockaddr_storage remote{};
|
||||
socklen_t local_len = sizeof(local);
|
||||
socklen_t remote_len = sizeof(remote);
|
||||
const bool have_local =
|
||||
::getsockname(fd, reinterpret_cast<sockaddr *>(&local), &local_len) == 0;
|
||||
const bool have_remote =
|
||||
::getpeername(fd, reinterpret_cast<sockaddr *>(&remote), &remote_len) == 0;
|
||||
{
|
||||
log::record rec(log::level::info, "link.up");
|
||||
rec.kv("how", how).kv("peer", peer).kv("transport", transport_name(kind))
|
||||
.kv("lanes", lanes).kv("lane", lane).kv("epoch", epoch)
|
||||
.kv("local", have_local ? detail::sockaddr_text(local) : std::string("unknown"))
|
||||
.kv("remote", have_remote ? detail::sockaddr_text(remote) : std::string("unknown"))
|
||||
.kv("auth", encrypted ? sec->peer_auth : std::string("none"))
|
||||
.kv("encryption",
|
||||
encrypted ? sec->protocol + "/" + sec->cipher : std::string("none"));
|
||||
if (encrypted)
|
||||
rec.kv("peer_key", sec->peer_key ? sec->peer_key->base64() : std::string("none"))
|
||||
.kv("peer_verified_us", sec->peer_verified_us);
|
||||
if (kind != transport::sctp)
|
||||
{
|
||||
rec.kv("nodelay", detail::int_opt(fd, IPPROTO_TCP, TCP_NODELAY))
|
||||
.kv("quickack_req", requested.quickack)
|
||||
.kv("keepalive", detail::int_opt(fd, SOL_SOCKET, SO_KEEPALIVE))
|
||||
.kv("sndbuf_req", requested.send_buffer)
|
||||
.kv("sndbuf", detail::int_opt(fd, SOL_SOCKET, SO_SNDBUF))
|
||||
.kv("rcvbuf_req", requested.recv_buffer)
|
||||
.kv("rcvbuf", detail::int_opt(fd, SOL_SOCKET, SO_RCVBUF));
|
||||
#if defined(TCP_INFO)
|
||||
tcp_info ti{};
|
||||
socklen_t ti_len = sizeof(ti);
|
||||
if (::getsockopt(fd, IPPROTO_TCP, TCP_INFO, &ti, &ti_len) == 0)
|
||||
rec.kv("rtt_us", ti.tcpi_rtt).kv("rttvar_us", ti.tcpi_rttvar)
|
||||
.kv("pmtu", ti.tcpi_pmtu).kv("snd_mss", ti.tcpi_snd_mss)
|
||||
.kv("snd_cwnd", ti.tcpi_snd_cwnd)
|
||||
.kv("retrans", ti.tcpi_total_retrans);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
if (!encrypted && have_remote && !detail::loopback(remote)
|
||||
&& log::first_time("net.plaintext_remote"))
|
||||
DPF_LOG(warning, "link.plaintext").kv("remote", detail::sockaddr_text(remote))
|
||||
.kv("detail", "encryption is off: this party link is unauthenticated "
|
||||
"and unencrypted, and the handshake's party id is not verified");
|
||||
}
|
||||
|
||||
} // namespace net
|
||||
} // namespace dpf
|
||||
|
||||
#endif // LIBDPF_INCLUDE_DPF_NET_LINK_LOG_HPP__
|
||||
179
include/dpf/net/memory_sink.hpp
Normal file
179
include/dpf/net/memory_sink.hpp
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
/// @file dpf/net/memory_sink.hpp
|
||||
/// @brief In-process paired RoundSink for correctness tests.
|
||||
#ifndef LIBDPF_INCLUDE_DPF_NET_MEMORY_SINK_HPP__
|
||||
#define LIBDPF_INCLUDE_DPF_NET_MEMORY_SINK_HPP__
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <stdexcept>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "dpf/net/round_sink.hpp"
|
||||
|
||||
namespace dpf
|
||||
{
|
||||
namespace net
|
||||
{
|
||||
|
||||
/// @brief Shared state between the two ends of a memory sink pair.
|
||||
struct memory_sink_hub
|
||||
{
|
||||
std::size_t count = 0;
|
||||
std::vector<std::size_t> slot_bytes;
|
||||
std::vector<round_window> a;
|
||||
std::vector<round_window> b;
|
||||
mutable std::mutex mu;
|
||||
|
||||
explicit memory_sink_hub(std::size_t n, std::vector<std::size_t> slots)
|
||||
: count(n), slot_bytes(std::move(slots))
|
||||
{
|
||||
a.reserve(slot_bytes.size());
|
||||
b.reserve(slot_bytes.size());
|
||||
for (std::size_t sb : slot_bytes)
|
||||
{
|
||||
a.emplace_back(count, sb);
|
||||
b.emplace_back(count, sb);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/// @brief One end of a memory-paired RoundSink.
|
||||
class memory_sink : public RoundSink
|
||||
{
|
||||
public:
|
||||
memory_sink(std::shared_ptr<memory_sink_hub> hub, bool side_a)
|
||||
: hub_(std::move(hub)), side_a_(side_a)
|
||||
{
|
||||
if (!hub_)
|
||||
throw std::invalid_argument("memory_sink needs a hub");
|
||||
}
|
||||
|
||||
std::size_t count() const noexcept override { return hub_->count; }
|
||||
std::size_t rounds() const noexcept override
|
||||
{
|
||||
return hub_->slot_bytes.size();
|
||||
}
|
||||
|
||||
std::size_t slot_bytes(std::uint16_t round) const override
|
||||
{
|
||||
if (round >= hub_->slot_bytes.size())
|
||||
throw std::out_of_range("memory_sink round");
|
||||
return hub_->slot_bytes[round];
|
||||
}
|
||||
|
||||
void submit(std::uint16_t round, std::size_t index,
|
||||
const std::uint8_t * bytes, std::size_t n) override
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(hub_->mu);
|
||||
mine(round).submit(index, bytes, n);
|
||||
}
|
||||
|
||||
bool peer_ready(std::uint16_t round, std::size_t index) const override
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(hub_->mu);
|
||||
return mine(round).peer_ready(index);
|
||||
}
|
||||
|
||||
void read_peer(std::uint16_t round, std::size_t index, std::uint8_t * out,
|
||||
std::size_t n) const override
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(hub_->mu);
|
||||
mine(round).read_peer(index, out, n);
|
||||
}
|
||||
|
||||
void flush() override
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(hub_->mu);
|
||||
flush_unlocked();
|
||||
}
|
||||
|
||||
void flush_round(std::uint16_t round) override
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(hub_->mu);
|
||||
flush_round_unlocked(round);
|
||||
}
|
||||
|
||||
void poll() override {}
|
||||
|
||||
private:
|
||||
void flush_unlocked()
|
||||
{
|
||||
for (std::uint16_t r = 0; r < hub_->slot_bytes.size(); ++r)
|
||||
{
|
||||
std::size_t begin = 0;
|
||||
std::size_t n = 0;
|
||||
mine(r).pending_out(begin, n);
|
||||
if (n != 0)
|
||||
{
|
||||
flush_round_unlocked(r);
|
||||
continue;
|
||||
}
|
||||
peer(r).pending_out(begin, n);
|
||||
if (n != 0)
|
||||
flush_round_unlocked(r);
|
||||
}
|
||||
}
|
||||
|
||||
void flush_round_unlocked(std::uint16_t round)
|
||||
{
|
||||
if (round >= hub_->slot_bytes.size())
|
||||
throw std::out_of_range("memory_sink flush_round");
|
||||
auto & local = mine(round);
|
||||
auto & remote = peer(round);
|
||||
std::size_t begin_l = 0;
|
||||
std::size_t n_l = 0;
|
||||
const std::uint8_t * pend_l = local.pending_out(begin_l, n_l);
|
||||
std::size_t begin_r = 0;
|
||||
std::size_t n_r = 0;
|
||||
const std::uint8_t * pend_r = remote.pending_out(begin_r, n_r);
|
||||
if (n_l != 0)
|
||||
{
|
||||
remote.accept_peer_at(begin_l, pend_l, n_l);
|
||||
local.mark_flushed(n_l);
|
||||
}
|
||||
if (n_r != 0)
|
||||
{
|
||||
local.accept_peer_at(begin_r, pend_r, n_r);
|
||||
remote.mark_flushed(n_r);
|
||||
}
|
||||
}
|
||||
round_window & mine(std::uint16_t round)
|
||||
{
|
||||
if (round >= hub_->slot_bytes.size())
|
||||
throw std::out_of_range("memory_sink round");
|
||||
return side_a_ ? hub_->a[round] : hub_->b[round];
|
||||
}
|
||||
|
||||
const round_window & mine(std::uint16_t round) const
|
||||
{
|
||||
if (round >= hub_->slot_bytes.size())
|
||||
throw std::out_of_range("memory_sink round");
|
||||
return side_a_ ? hub_->a[round] : hub_->b[round];
|
||||
}
|
||||
|
||||
round_window & peer(std::uint16_t round)
|
||||
{
|
||||
if (round >= hub_->slot_bytes.size())
|
||||
throw std::out_of_range("memory_sink round");
|
||||
return side_a_ ? hub_->b[round] : hub_->a[round];
|
||||
}
|
||||
|
||||
std::shared_ptr<memory_sink_hub> hub_;
|
||||
bool side_a_ = true;
|
||||
};
|
||||
|
||||
/// @brief Build a connected pair of memory sinks that share one hub.
|
||||
inline std::pair<memory_sink, memory_sink> make_memory_sink_pair(
|
||||
std::size_t count, std::vector<std::size_t> slot_bytes)
|
||||
{
|
||||
auto hub = std::make_shared<memory_sink_hub>(count, std::move(slot_bytes));
|
||||
return {memory_sink(hub, true), memory_sink(hub, false)};
|
||||
}
|
||||
|
||||
} // namespace net
|
||||
} // namespace dpf
|
||||
|
||||
#endif // LIBDPF_INCLUDE_DPF_NET_MEMORY_SINK_HPP__
|
||||
28
include/dpf/net/mesh_rendezvous.hpp
Normal file
28
include/dpf/net/mesh_rendezvous.hpp
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
/// @file dpf/net/mesh_rendezvous.hpp
|
||||
/// @brief Shared in-process port table for party clique joins.
|
||||
#ifndef LIBDPF_INCLUDE_DPF_NET_MESH_RENDEZVOUS_HPP__
|
||||
#define LIBDPF_INCLUDE_DPF_NET_MESH_RENDEZVOUS_HPP__
|
||||
|
||||
#include <atomic>
|
||||
#include <vector>
|
||||
|
||||
namespace dpf
|
||||
{
|
||||
namespace net
|
||||
{
|
||||
|
||||
/// @brief Shared port table: `ports[i]` is party `i`'s listen port (0 until bound).
|
||||
using mesh_ports = std::vector<std::atomic<unsigned short>>;
|
||||
|
||||
inline mesh_ports make_mesh_ports(unsigned n)
|
||||
{
|
||||
mesh_ports ports(n);
|
||||
for (auto & p : ports)
|
||||
p.store(0);
|
||||
return ports;
|
||||
}
|
||||
|
||||
} // namespace net
|
||||
} // namespace dpf
|
||||
|
||||
#endif // LIBDPF_INCLUDE_DPF_NET_MESH_RENDEZVOUS_HPP__
|
||||
269
include/dpf/net/mux_sink.hpp
Normal file
269
include/dpf/net/mux_sink.hpp
Normal file
|
|
@ -0,0 +1,269 @@
|
|||
/// @file dpf/net/mux_sink.hpp
|
||||
/// @brief RoundSink multiplexed on one framed trio channel.
|
||||
#ifndef LIBDPF_INCLUDE_DPF_NET_MUX_SINK_HPP__
|
||||
#define LIBDPF_INCLUDE_DPF_NET_MUX_SINK_HPP__
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "dpf/net/channel.hpp"
|
||||
#include "dpf/net/comm_hook.hpp"
|
||||
#include "dpf/net/round_sink.hpp"
|
||||
#include "dpf/net/trio.hpp"
|
||||
|
||||
namespace dpf
|
||||
{
|
||||
namespace net
|
||||
{
|
||||
|
||||
#pragma pack(push, 1)
|
||||
struct round_batch_hdr
|
||||
{
|
||||
std::uint16_t round = 0;
|
||||
std::uint32_t begin = 0;
|
||||
std::uint32_t count = 0;
|
||||
};
|
||||
#pragma pack(pop)
|
||||
|
||||
/// @brief Prefix-flush RoundSink over a single duplex `channel`.
|
||||
/// @details Each flush sends one `msg::round_batch` frame per round that has
|
||||
/// a new contiguous prefix: header `(round, begin, count)` then
|
||||
/// `count * slot_bytes` payload. The peer's matching frames are
|
||||
/// read in the same flush (lower role sends first).
|
||||
class mux_sink : public RoundSink
|
||||
{
|
||||
public:
|
||||
mux_sink(channel & link, unsigned self_id, unsigned peer_id,
|
||||
std::size_t count, std::vector<std::size_t> slot_bytes)
|
||||
: link_(link),
|
||||
self_id_(self_id),
|
||||
peer_id_(peer_id),
|
||||
count_(count),
|
||||
slot_bytes_(std::move(slot_bytes))
|
||||
{
|
||||
windows_.reserve(slot_bytes_.size());
|
||||
for (std::size_t sb : slot_bytes_)
|
||||
windows_.emplace_back(count_, sb);
|
||||
}
|
||||
|
||||
std::size_t count() const noexcept override { return count_; }
|
||||
std::size_t rounds() const noexcept override { return slot_bytes_.size(); }
|
||||
|
||||
std::size_t slot_bytes(std::uint16_t round) const override
|
||||
{
|
||||
if (round >= slot_bytes_.size())
|
||||
throw std::out_of_range("mux_sink round");
|
||||
return slot_bytes_[round];
|
||||
}
|
||||
|
||||
void submit(std::uint16_t round, std::size_t index,
|
||||
const std::uint8_t * bytes, std::size_t n) override
|
||||
{
|
||||
window(round).submit(index, bytes, n);
|
||||
}
|
||||
|
||||
bool peer_ready(std::uint16_t round, std::size_t index) const override
|
||||
{
|
||||
return window(round).peer_ready(index);
|
||||
}
|
||||
|
||||
void read_peer(std::uint16_t round, std::size_t index, std::uint8_t * out,
|
||||
std::size_t n) const override
|
||||
{
|
||||
window(round).read_peer(index, out, n);
|
||||
}
|
||||
|
||||
void flush() override
|
||||
{
|
||||
std::vector<std::uint8_t> payload;
|
||||
for (std::uint16_t r = 0; r < slot_bytes_.size(); ++r)
|
||||
append_pending(r, payload);
|
||||
exchange_payload(payload);
|
||||
}
|
||||
|
||||
void flush_round(std::uint16_t round) override
|
||||
{
|
||||
if (round >= slot_bytes_.size())
|
||||
throw std::out_of_range("mux_sink flush_round");
|
||||
std::vector<std::uint8_t> payload;
|
||||
append_pending(round, payload);
|
||||
exchange_payload(payload);
|
||||
}
|
||||
|
||||
void poll() override {}
|
||||
|
||||
std::uint64_t exchanges() const noexcept { return link_exchanges_; }
|
||||
|
||||
private:
|
||||
round_window & window(std::uint16_t round)
|
||||
{
|
||||
if (round >= windows_.size())
|
||||
throw std::out_of_range("mux_sink round");
|
||||
return windows_[round];
|
||||
}
|
||||
|
||||
const round_window & window(std::uint16_t round) const
|
||||
{
|
||||
if (round >= windows_.size())
|
||||
throw std::out_of_range("mux_sink round");
|
||||
return windows_[round];
|
||||
}
|
||||
|
||||
void append_pending(std::uint16_t r, std::vector<std::uint8_t> & payload)
|
||||
{
|
||||
std::size_t begin = 0;
|
||||
std::size_t nslots = 0;
|
||||
const std::uint8_t * pending = windows_[r].pending_out(begin, nslots);
|
||||
if (nslots == 0)
|
||||
return;
|
||||
round_batch_hdr hdr{};
|
||||
hdr.round = r;
|
||||
hdr.begin = static_cast<std::uint32_t>(begin);
|
||||
hdr.count = static_cast<std::uint32_t>(nslots);
|
||||
const std::size_t body = nslots * slot_bytes_[r];
|
||||
const std::size_t old = payload.size();
|
||||
payload.resize(old + sizeof(hdr) + body);
|
||||
std::memcpy(payload.data() + old, &hdr, sizeof(hdr));
|
||||
if (body != 0)
|
||||
std::memcpy(payload.data() + old + sizeof(hdr), pending, body);
|
||||
windows_[r].mark_flushed(nslots);
|
||||
}
|
||||
|
||||
void exchange_payload(const std::vector<std::uint8_t> & payload)
|
||||
{
|
||||
// Always exchange so a party with nothing new still receives.
|
||||
std::vector<std::uint8_t> theirs;
|
||||
if (self_id_ < peer_id_)
|
||||
{
|
||||
link_.send_bytes(msg::round_batch, payload);
|
||||
theirs = link_.recv_bytes(msg::round_batch);
|
||||
}
|
||||
else if (self_id_ > peer_id_)
|
||||
{
|
||||
theirs = link_.recv_bytes(msg::round_batch);
|
||||
link_.send_bytes(msg::round_batch, payload);
|
||||
}
|
||||
else
|
||||
throw std::invalid_argument("mux_sink flush with self");
|
||||
++link_exchanges_;
|
||||
ingest(theirs);
|
||||
}
|
||||
|
||||
void ingest(const std::vector<std::uint8_t> & bytes)
|
||||
{
|
||||
std::size_t off = 0;
|
||||
while (off < bytes.size())
|
||||
{
|
||||
if (off + sizeof(round_batch_hdr) > bytes.size())
|
||||
throw std::runtime_error("mux_sink truncated header");
|
||||
round_batch_hdr hdr{};
|
||||
std::memcpy(&hdr, bytes.data() + off, sizeof(hdr));
|
||||
off += sizeof(hdr);
|
||||
if (hdr.round >= slot_bytes_.size())
|
||||
throw std::runtime_error("mux_sink bad round");
|
||||
const std::size_t body =
|
||||
static_cast<std::size_t>(hdr.count) * slot_bytes_[hdr.round];
|
||||
if (off + body > bytes.size())
|
||||
throw std::runtime_error("mux_sink truncated body");
|
||||
windows_[hdr.round].accept_peer_at(hdr.begin, bytes.data() + off,
|
||||
hdr.count);
|
||||
off += body;
|
||||
}
|
||||
}
|
||||
|
||||
channel & link_;
|
||||
unsigned self_id_ = 0;
|
||||
unsigned peer_id_ = 0;
|
||||
std::size_t count_ = 0;
|
||||
std::vector<std::size_t> slot_bytes_;
|
||||
std::vector<round_window> windows_;
|
||||
std::uint64_t link_exchanges_ = 0;
|
||||
};
|
||||
|
||||
/// @brief Bind a mux sink to the p0–p1 link of an existing trio.
|
||||
inline mux_sink make_mux_sink(trio & net, std::size_t count,
|
||||
std::vector<std::size_t> slot_bytes)
|
||||
{
|
||||
const role self = net.self();
|
||||
if (self == role::p2)
|
||||
throw std::logic_error("mux_sink is for computing parties");
|
||||
const role peer = self == role::p0 ? role::p1 : role::p0;
|
||||
return mux_sink(net.to(peer), to_u(self), to_u(peer), count,
|
||||
std::move(slot_bytes));
|
||||
}
|
||||
|
||||
/// @brief Default `comm_hook`: framed mesh channels and a mux batch sink.
|
||||
/// @details Subclass to reroute selected calls; leave the rest to these
|
||||
/// defaults. Installing this hook is equivalent to a null hook for
|
||||
/// framing, but lets overrides replace individual methods.
|
||||
class mesh_comm_hook : public comm_hook
|
||||
{
|
||||
public:
|
||||
void send_bytes(trio & net, unsigned peer_id, msg tag, const void * data,
|
||||
std::size_t n) override
|
||||
{
|
||||
net.to(static_cast<role>(peer_id)).send_bytes(tag, data, n);
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> recv_bytes(trio & net, unsigned peer_id,
|
||||
msg tag) override
|
||||
{
|
||||
return net.to(static_cast<role>(peer_id)).recv_bytes(tag);
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> exchange_bytes(trio & net, unsigned peer_id,
|
||||
msg tag, const void * data, std::size_t n) override
|
||||
{
|
||||
auto & link = net.to(static_cast<role>(peer_id));
|
||||
const unsigned self_id = to_u(net.self());
|
||||
std::vector<std::uint8_t> theirs(n);
|
||||
if (self_id < peer_id)
|
||||
{
|
||||
link.send_bytes(tag, data, n);
|
||||
theirs = link.recv_bytes(tag);
|
||||
}
|
||||
else if (self_id > peer_id)
|
||||
{
|
||||
theirs = link.recv_bytes(tag);
|
||||
link.send_bytes(tag, data, n);
|
||||
}
|
||||
else
|
||||
throw std::invalid_argument("mesh_comm_hook exchange with self");
|
||||
if (theirs.size() != n)
|
||||
throw std::runtime_error("mesh_comm_hook exchange size");
|
||||
return theirs;
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> exchange_vec_bytes(trio & net, unsigned peer_id,
|
||||
msg tag, const void * data, std::size_t nbytes) override
|
||||
{
|
||||
return exchange_bytes(net, peer_id, tag, data, nbytes);
|
||||
}
|
||||
|
||||
std::unique_ptr<RoundSink> batch(trio & net, std::size_t count,
|
||||
std::vector<std::size_t> slot_bytes) override
|
||||
{
|
||||
return std::make_unique<mux_sink>(make_mux_sink(net, count,
|
||||
std::move(slot_bytes)));
|
||||
}
|
||||
};
|
||||
|
||||
inline std::unique_ptr<RoundSink> trio::batch(std::size_t count,
|
||||
std::vector<std::size_t> slot_bytes)
|
||||
{
|
||||
if (hook_)
|
||||
return hook_->batch(*this, count, std::move(slot_bytes));
|
||||
return std::make_unique<mux_sink>(make_mux_sink(*this, count,
|
||||
std::move(slot_bytes)));
|
||||
}
|
||||
|
||||
} // namespace net
|
||||
} // namespace dpf
|
||||
|
||||
#endif // LIBDPF_INCLUDE_DPF_NET_MUX_SINK_HPP__
|
||||
1171
include/dpf/net/party_session.hpp
Normal file
1171
include/dpf/net/party_session.hpp
Normal file
File diff suppressed because it is too large
Load diff
217
include/dpf/net/party_tape_io.hpp
Normal file
217
include/dpf/net/party_tape_io.hpp
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
/// @file dpf/net/party_tape_io.hpp
|
||||
/// @brief Deal / accept a `dpf::beavers::party_tape` over a framed channel.
|
||||
#ifndef LIBDPF_INCLUDE_DPF_NET_PARTY_TAPE_IO_HPP__
|
||||
#define LIBDPF_INCLUDE_DPF_NET_PARTY_TAPE_IO_HPP__
|
||||
|
||||
#include <cstdint>
|
||||
#include <stdexcept>
|
||||
#include <type_traits>
|
||||
#include <vector>
|
||||
|
||||
#include "dpf/beaver.hpp"
|
||||
#include "dpf/net/channel.hpp"
|
||||
#include "dpf/net/trio.hpp"
|
||||
|
||||
namespace dpf
|
||||
{
|
||||
namespace net
|
||||
{
|
||||
namespace detail
|
||||
{
|
||||
|
||||
template <typename Ring>
|
||||
void send_ring_vec(channel & c, const std::vector<Ring> & v)
|
||||
{
|
||||
static_assert(std::is_trivially_copyable_v<Ring>,
|
||||
"party_tape ring must be trivially copyable");
|
||||
std::uint64_t n = v.size();
|
||||
c.send(msg::ring_vector, n);
|
||||
if (n != 0)
|
||||
c.send_vec(v.data(), v.size(), msg::ring_vector);
|
||||
}
|
||||
|
||||
template <typename Ring>
|
||||
std::vector<Ring> recv_ring_vec(channel & c)
|
||||
{
|
||||
auto n = c.recv<std::uint64_t>(msg::ring_vector);
|
||||
if (n == 0)
|
||||
return {};
|
||||
return c.recv_vec<Ring>(msg::ring_vector);
|
||||
}
|
||||
|
||||
inline void send_flags(channel & c, const std::vector<std::uint8_t> & v)
|
||||
{
|
||||
std::uint64_t n = v.size();
|
||||
c.send(msg::bytes, n);
|
||||
if (n != 0)
|
||||
c.send_bytes(msg::bytes, v);
|
||||
}
|
||||
|
||||
inline std::vector<std::uint8_t> recv_flags(channel & c)
|
||||
{
|
||||
auto n = c.recv<std::uint64_t>(msg::bytes);
|
||||
if (n == 0)
|
||||
return {};
|
||||
auto body = c.recv_bytes(msg::bytes);
|
||||
if (body.size() != n)
|
||||
throw std::runtime_error("party_tape flag size mismatch");
|
||||
return body;
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
template <typename Ring>
|
||||
void send_party_tape(channel & c, const beavers::party_tape<Ring> & tape)
|
||||
{
|
||||
c.send(msg::beaver_tape, std::uint8_t{2});
|
||||
c.send(msg::beaver_tape, static_cast<std::uint8_t>(tape.has_mac ? 1 : 0));
|
||||
detail::send_ring_vec(c, tape.lambda);
|
||||
detail::send_flags(c, tape.lambda_ready);
|
||||
detail::send_ring_vec(c, tape.monomial);
|
||||
detail::send_flags(c, tape.monomial_ready);
|
||||
detail::send_ring_vec(c, tape.bundles);
|
||||
detail::send_flags(c, tape.bundles_ready);
|
||||
detail::send_ring_vec(c, tape.dot_cross);
|
||||
detail::send_flags(c, tape.dot_ready);
|
||||
if (tape.has_mac)
|
||||
{
|
||||
detail::send_ring_vec(c, tape.lambda_tag);
|
||||
detail::send_ring_vec(c, tape.monomial_tag);
|
||||
detail::send_ring_vec(c, tape.bundles_tag);
|
||||
detail::send_ring_vec(c, tape.dot_cross_tag);
|
||||
}
|
||||
}
|
||||
|
||||
template <typename Ring>
|
||||
beavers::party_tape<Ring> recv_party_tape(channel & c)
|
||||
{
|
||||
const auto ver = c.recv<std::uint8_t>(msg::beaver_tape);
|
||||
beavers::party_tape<Ring> tape;
|
||||
if (ver >= 2)
|
||||
tape.has_mac = c.recv<std::uint8_t>(msg::beaver_tape) != 0;
|
||||
tape.lambda = detail::recv_ring_vec<Ring>(c);
|
||||
tape.lambda_ready = detail::recv_flags(c);
|
||||
tape.monomial = detail::recv_ring_vec<Ring>(c);
|
||||
tape.monomial_ready = detail::recv_flags(c);
|
||||
tape.bundles = detail::recv_ring_vec<Ring>(c);
|
||||
tape.bundles_ready = detail::recv_flags(c);
|
||||
tape.dot_cross = detail::recv_ring_vec<Ring>(c);
|
||||
tape.dot_ready = detail::recv_flags(c);
|
||||
if (tape.has_mac)
|
||||
{
|
||||
tape.lambda_tag = detail::recv_ring_vec<Ring>(c);
|
||||
tape.monomial_tag = detail::recv_ring_vec<Ring>(c);
|
||||
tape.bundles_tag = detail::recv_ring_vec<Ring>(c);
|
||||
tape.dot_cross_tag = detail::recv_ring_vec<Ring>(c);
|
||||
}
|
||||
return tape;
|
||||
}
|
||||
|
||||
/// @brief Dealer exports and sends each party's tape.
|
||||
/// @tparam Ring Beaver ring
|
||||
/// @param net the connected trio
|
||||
/// @param s the sampled session
|
||||
/// @throws std::logic_error if this process is not p2
|
||||
template <typename Ring>
|
||||
void send_party_tape(trio & net, role peer, const beavers::party_tape<Ring> & tape)
|
||||
{
|
||||
net.send_to(peer, msg::beaver_tape, std::uint8_t{2});
|
||||
net.send_to(peer, msg::beaver_tape,
|
||||
static_cast<std::uint8_t>(tape.has_mac ? 1 : 0));
|
||||
auto send_ring_vec = [&](const std::vector<Ring> & v) {
|
||||
const std::uint64_t n = v.size();
|
||||
net.send_to(peer, msg::ring_vector, n);
|
||||
if (n != 0)
|
||||
net.send_vec_to(peer, msg::ring_vector, v);
|
||||
};
|
||||
auto send_flags = [&](const std::vector<std::uint8_t> & v) {
|
||||
const std::uint64_t n = v.size();
|
||||
net.send_to(peer, msg::bytes, n);
|
||||
if (n != 0)
|
||||
net.send_bytes_to(peer, msg::bytes, v.data(), v.size());
|
||||
};
|
||||
send_ring_vec(tape.lambda);
|
||||
send_flags(tape.lambda_ready);
|
||||
send_ring_vec(tape.monomial);
|
||||
send_flags(tape.monomial_ready);
|
||||
send_ring_vec(tape.bundles);
|
||||
send_flags(tape.bundles_ready);
|
||||
send_ring_vec(tape.dot_cross);
|
||||
send_flags(tape.dot_ready);
|
||||
if (tape.has_mac)
|
||||
{
|
||||
send_ring_vec(tape.lambda_tag);
|
||||
send_ring_vec(tape.monomial_tag);
|
||||
send_ring_vec(tape.bundles_tag);
|
||||
send_ring_vec(tape.dot_cross_tag);
|
||||
}
|
||||
}
|
||||
|
||||
template <typename Ring>
|
||||
beavers::party_tape<Ring> recv_party_tape(trio & net, role peer)
|
||||
{
|
||||
const auto ver = net.recv_from<std::uint8_t>(peer, msg::beaver_tape);
|
||||
beavers::party_tape<Ring> tape;
|
||||
if (ver >= 2)
|
||||
tape.has_mac = net.recv_from<std::uint8_t>(peer, msg::beaver_tape) != 0;
|
||||
auto recv_ring_vec = [&]() {
|
||||
const auto n = net.recv_from<std::uint64_t>(peer, msg::ring_vector);
|
||||
if (n == 0)
|
||||
return std::vector<Ring>{};
|
||||
return net.recv_vec_from<Ring>(peer, msg::ring_vector);
|
||||
};
|
||||
auto recv_flags = [&]() {
|
||||
const auto n = net.recv_from<std::uint64_t>(peer, msg::bytes);
|
||||
if (n == 0)
|
||||
return std::vector<std::uint8_t>{};
|
||||
auto body = net.recv_bytes_from(peer, msg::bytes);
|
||||
if (body.size() != n)
|
||||
throw std::runtime_error("party_tape flag size mismatch");
|
||||
return body;
|
||||
};
|
||||
tape.lambda = recv_ring_vec();
|
||||
tape.lambda_ready = recv_flags();
|
||||
tape.monomial = recv_ring_vec();
|
||||
tape.monomial_ready = recv_flags();
|
||||
tape.bundles = recv_ring_vec();
|
||||
tape.bundles_ready = recv_flags();
|
||||
tape.dot_cross = recv_ring_vec();
|
||||
tape.dot_ready = recv_flags();
|
||||
if (tape.has_mac)
|
||||
{
|
||||
tape.lambda_tag = recv_ring_vec();
|
||||
tape.monomial_tag = recv_ring_vec();
|
||||
tape.bundles_tag = recv_ring_vec();
|
||||
tape.dot_cross_tag = recv_ring_vec();
|
||||
}
|
||||
return tape;
|
||||
}
|
||||
|
||||
template <typename Ring>
|
||||
void deal_session(trio & net, const beavers::session<Ring> & s)
|
||||
{
|
||||
if (net.self() != role::p2)
|
||||
throw std::logic_error("deal_session is for the dealer");
|
||||
send_party_tape(net, role::p0, s.export_party(0));
|
||||
send_party_tape(net, role::p1, s.export_party(1));
|
||||
}
|
||||
|
||||
/// @brief Computing party receives its tape from the dealer.
|
||||
/// @tparam Ring Beaver ring
|
||||
/// @param net the connected trio
|
||||
/// @return this party's tape
|
||||
/// @throws std::logic_error if this process is p2
|
||||
/// @throws std::runtime_error if a flag vector's length disagrees with its header
|
||||
template <typename Ring>
|
||||
HEDLEY_WARN_UNUSED_RESULT
|
||||
beavers::party_tape<Ring> accept_session(trio & net)
|
||||
{
|
||||
if (net.self() == role::p2)
|
||||
throw std::logic_error("dealer does not accept_session");
|
||||
return recv_party_tape<Ring>(net, role::p2);
|
||||
}
|
||||
|
||||
} // namespace net
|
||||
} // namespace dpf
|
||||
|
||||
#endif // LIBDPF_INCLUDE_DPF_NET_PARTY_TAPE_IO_HPP__
|
||||
177
include/dpf/net/policy.hpp
Normal file
177
include/dpf/net/policy.hpp
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
/// @file dpf/net/policy.hpp
|
||||
/// @brief Explicit transport, framing, socket, window, and deadline policy.
|
||||
/// @details Library code never reads the environment. Every knob here is a
|
||||
/// field the caller sets; `app::run_config::from_env` is the one place
|
||||
/// that turns `DPF_*` variables into these values.
|
||||
#ifndef LIBDPF_INCLUDE_DPF_NET_POLICY_HPP__
|
||||
#define LIBDPF_INCLUDE_DPF_NET_POLICY_HPP__
|
||||
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
namespace dpf
|
||||
{
|
||||
namespace net
|
||||
{
|
||||
|
||||
/// @brief Which byte transport carries a party-to-party edge.
|
||||
enum class transport : unsigned char
|
||||
{
|
||||
memory_sink, ///< in-process paired `memory_sink` (no stream arrays)
|
||||
memory_stream, ///< in-process synchronous `stream_array`
|
||||
async_memory, ///< in-process event-driven stream array
|
||||
mux, ///< all lanes on one TCP connection
|
||||
parallel, ///< one TCP connection per lane
|
||||
sctp, ///< one SCTP association, lane i = SCTP stream i (Linux)
|
||||
local ///< one unix-domain socket per lane (same host)
|
||||
};
|
||||
|
||||
inline const char * transport_name(transport t) noexcept
|
||||
{
|
||||
switch (t)
|
||||
{
|
||||
case transport::memory_sink:
|
||||
return "memory";
|
||||
case transport::memory_stream:
|
||||
return "stream";
|
||||
case transport::async_memory:
|
||||
return "async";
|
||||
case transport::mux:
|
||||
return "mux";
|
||||
case transport::parallel:
|
||||
return "parallel";
|
||||
case transport::sctp:
|
||||
return "sctp";
|
||||
case transport::local:
|
||||
return "local";
|
||||
}
|
||||
return "async";
|
||||
}
|
||||
|
||||
/// @brief Parse a transport name. Unknown names throw.
|
||||
inline transport parse_transport(const std::string & s)
|
||||
{
|
||||
if (s == "memory")
|
||||
return transport::memory_sink;
|
||||
if (s == "stream")
|
||||
return transport::memory_stream;
|
||||
if (s == "async" || s == "async_memory")
|
||||
return transport::async_memory;
|
||||
if (s == "mux")
|
||||
return transport::mux;
|
||||
if (s == "parallel")
|
||||
return transport::parallel;
|
||||
if (s == "sctp")
|
||||
return transport::sctp;
|
||||
if (s == "local")
|
||||
return transport::local;
|
||||
throw std::invalid_argument("unknown transport: " + s
|
||||
+ " (memory|stream|async|mux|parallel|sctp|local)");
|
||||
}
|
||||
|
||||
/// @brief True for transports that cross a kernel socket.
|
||||
inline bool is_socket_transport(transport t) noexcept
|
||||
{
|
||||
return t == transport::mux || t == transport::parallel
|
||||
|| t == transport::sctp || t == transport::local;
|
||||
}
|
||||
|
||||
/// @brief Whether round payloads on a lane carry a `{round,nbytes}` header.
|
||||
/// @details `automatic` frames only when rounds outnumber lanes. `always`
|
||||
/// frames even with one lane per round (to measure the header, or to
|
||||
/// let partial prefixes and reconnect resume work on every round).
|
||||
/// `never` requires at least one lane per round.
|
||||
enum class framing_mode : unsigned char
|
||||
{
|
||||
automatic,
|
||||
always,
|
||||
never
|
||||
};
|
||||
|
||||
inline const char * framing_name(framing_mode m) noexcept
|
||||
{
|
||||
switch (m)
|
||||
{
|
||||
case framing_mode::automatic:
|
||||
return "auto";
|
||||
case framing_mode::always:
|
||||
return "always";
|
||||
case framing_mode::never:
|
||||
return "never";
|
||||
}
|
||||
return "auto";
|
||||
}
|
||||
|
||||
inline framing_mode parse_framing(const std::string & s)
|
||||
{
|
||||
if (s == "auto" || s == "automatic")
|
||||
return framing_mode::automatic;
|
||||
if (s == "always" || s == "on")
|
||||
return framing_mode::always;
|
||||
if (s == "never" || s == "off")
|
||||
return framing_mode::never;
|
||||
throw std::invalid_argument("unknown framing: " + s + " (auto|always|never)");
|
||||
}
|
||||
|
||||
/// @brief Options applied to every TCP socket a backend adopts.
|
||||
/// @details Zero means "leave the kernel default".
|
||||
struct socket_options
|
||||
{
|
||||
bool no_delay = true;
|
||||
/// Re-armed after every read and write: Linux clears it after one ACK.
|
||||
bool quickack = true;
|
||||
bool keepalive = true;
|
||||
int keepalive_idle_s = 0;
|
||||
int keepalive_interval_s = 0;
|
||||
int keepalive_count = 0;
|
||||
int send_buffer = 0;
|
||||
int recv_buffer = 0;
|
||||
};
|
||||
|
||||
/// @brief Per-connection wire policy for the async stream backends.
|
||||
struct wire_policy
|
||||
{
|
||||
/// Outstanding-byte high-water mark per lane (per connection on mux and
|
||||
/// SCTP, per socket on parallel). `0` is unlimited.
|
||||
std::size_t window_bytes = std::size_t{1} << 20;
|
||||
/// Largest frame accepted from the peer.
|
||||
std::size_t max_frame = std::size_t{16} << 20;
|
||||
/// Split writes into frames of at most this many payload bytes and
|
||||
/// round-robin them across lanes. `0` sends each write as one frame.
|
||||
std::size_t chunk_bytes = std::size_t{64} << 10;
|
||||
/// Frames and bytes gathered into one write syscall.
|
||||
std::size_t coalesce_frames = 16;
|
||||
std::size_t coalesce_bytes = std::size_t{256} << 10;
|
||||
/// Drop the consumed prefix of an inbox once it passes this many bytes.
|
||||
std::size_t compact_bytes = std::size_t{64} << 10;
|
||||
socket_options socket{};
|
||||
|
||||
void validate() const
|
||||
{
|
||||
if (max_frame == 0)
|
||||
throw std::invalid_argument("wire_policy: max_frame is 0");
|
||||
if (chunk_bytes > max_frame)
|
||||
throw std::invalid_argument(
|
||||
"wire_policy: chunk_bytes exceeds max_frame");
|
||||
if (coalesce_frames == 0)
|
||||
throw std::invalid_argument("wire_policy: coalesce_frames is 0");
|
||||
}
|
||||
};
|
||||
|
||||
/// @brief Bounds for session setup and for draining a write window.
|
||||
struct deadlines
|
||||
{
|
||||
std::chrono::milliseconds join{30000};
|
||||
std::chrono::milliseconds connect{30000};
|
||||
std::chrono::milliseconds accept{30000};
|
||||
std::chrono::milliseconds handshake{30000};
|
||||
std::chrono::milliseconds drain{30000};
|
||||
};
|
||||
|
||||
} // namespace net
|
||||
} // namespace dpf
|
||||
|
||||
#endif // LIBDPF_INCLUDE_DPF_NET_POLICY_HPP__
|
||||
149
include/dpf/net/round_lane.hpp
Normal file
149
include/dpf/net/round_lane.hpp
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
/// @file dpf/net/round_lane.hpp
|
||||
/// @brief Round→lane mapping and wire framing when rounds share lanes.
|
||||
/// @details A fixed pool of duplex streams can carry any number of exchange
|
||||
/// rounds: lane = `round % n_lanes`. A framed lane prefixes each write
|
||||
/// with a little-endian `{u16 round, u32 nbytes}` header, and the
|
||||
/// receiver appends those bytes to that round's inbox. Unframed lanes
|
||||
/// carry one round each (round == lane) with no header.
|
||||
#ifndef LIBDPF_INCLUDE_DPF_NET_ROUND_LANE_HPP__
|
||||
#define LIBDPF_INCLUDE_DPF_NET_ROUND_LANE_HPP__
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "dpf/net/policy.hpp"
|
||||
|
||||
namespace dpf
|
||||
{
|
||||
namespace net
|
||||
{
|
||||
|
||||
/// @brief On-wire header for a framed round payload on a shared lane.
|
||||
struct round_lane_hdr
|
||||
{
|
||||
static constexpr std::size_t size = 6;
|
||||
|
||||
std::uint16_t round = 0;
|
||||
std::uint32_t nbytes = 0;
|
||||
|
||||
void pack(std::uint8_t * dst) const noexcept
|
||||
{
|
||||
dst[0] = static_cast<std::uint8_t>(round & 0xffu);
|
||||
dst[1] = static_cast<std::uint8_t>((round >> 8) & 0xffu);
|
||||
dst[2] = static_cast<std::uint8_t>(nbytes & 0xffu);
|
||||
dst[3] = static_cast<std::uint8_t>((nbytes >> 8) & 0xffu);
|
||||
dst[4] = static_cast<std::uint8_t>((nbytes >> 16) & 0xffu);
|
||||
dst[5] = static_cast<std::uint8_t>((nbytes >> 24) & 0xffu);
|
||||
}
|
||||
|
||||
static round_lane_hdr unpack(const std::uint8_t * src) noexcept
|
||||
{
|
||||
round_lane_hdr h;
|
||||
h.round = static_cast<std::uint16_t>(src[0] | (src[1] << 8));
|
||||
h.nbytes = static_cast<std::uint32_t>(src[2] | (src[3] << 8) |
|
||||
(src[4] << 16) | (src[5] << 24));
|
||||
return h;
|
||||
}
|
||||
};
|
||||
|
||||
/// @brief Lane count used when a caller passes `0`.
|
||||
inline constexpr std::size_t default_lanes = 8;
|
||||
/// @brief Request one stream per round.
|
||||
inline constexpr std::size_t lanes_one_per_round = static_cast<std::size_t>(-1);
|
||||
/// @brief Round ids travel as `u16`.
|
||||
inline constexpr std::size_t max_rounds = 0xffffu;
|
||||
|
||||
/// @brief Streams to open for `n_rounds`. `requested` 0 → `default_lanes`.
|
||||
inline std::size_t lane_count_for_rounds(std::size_t n_rounds,
|
||||
std::size_t requested = 0)
|
||||
{
|
||||
if (n_rounds == 0)
|
||||
return 1;
|
||||
if (requested == lanes_one_per_round)
|
||||
return n_rounds;
|
||||
std::size_t want = requested == 0 ? default_lanes : requested;
|
||||
if (want > n_rounds)
|
||||
want = n_rounds;
|
||||
return want == 0 ? 1 : want;
|
||||
}
|
||||
|
||||
/// @brief Lane index and whether the wire uses round headers.
|
||||
struct round_lane_map
|
||||
{
|
||||
std::size_t n_lanes = 1;
|
||||
std::size_t n_rounds = 0;
|
||||
bool framed = false;
|
||||
|
||||
round_lane_map() = default;
|
||||
|
||||
round_lane_map(std::size_t streams, std::size_t rounds,
|
||||
framing_mode mode = framing_mode::automatic)
|
||||
: n_lanes(streams == 0 ? 1 : streams),
|
||||
n_rounds(rounds)
|
||||
{
|
||||
if (rounds > max_rounds)
|
||||
throw std::invalid_argument("round_lane_map: "
|
||||
+ std::to_string(rounds) + " rounds exceed the u16 round id");
|
||||
switch (mode)
|
||||
{
|
||||
case framing_mode::automatic:
|
||||
framed = rounds > n_lanes;
|
||||
break;
|
||||
case framing_mode::always:
|
||||
framed = true;
|
||||
break;
|
||||
case framing_mode::never:
|
||||
if (rounds > n_lanes)
|
||||
throw std::invalid_argument("round_lane_map: framing never needs "
|
||||
"one lane per round (" + std::to_string(rounds)
|
||||
+ " rounds, " + std::to_string(n_lanes) + " lanes)");
|
||||
framed = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
std::size_t lane(std::uint16_t round) const
|
||||
{
|
||||
if (static_cast<std::size_t>(round) >= n_rounds && n_rounds != 0)
|
||||
throw std::out_of_range("round_lane_map round");
|
||||
if (!framed)
|
||||
return static_cast<std::size_t>(round);
|
||||
return static_cast<std::size_t>(round) % n_lanes;
|
||||
}
|
||||
};
|
||||
|
||||
/// @brief Build `hdr || payload` for a framed write.
|
||||
inline std::vector<std::uint8_t> pack_round_frame(std::uint16_t round,
|
||||
const std::uint8_t * payload, std::size_t n)
|
||||
{
|
||||
std::vector<std::uint8_t> out(round_lane_hdr::size + n);
|
||||
round_lane_hdr{round, static_cast<std::uint32_t>(n)}.pack(out.data());
|
||||
if (n != 0)
|
||||
std::memcpy(out.data() + round_lane_hdr::size, payload, n);
|
||||
return out;
|
||||
}
|
||||
|
||||
/// @brief FNV-1a over a slot-width vector (plan shape fingerprint).
|
||||
inline std::uint64_t slots_fingerprint(const std::vector<std::size_t> & slots)
|
||||
{
|
||||
std::uint64_t h = 14695981039346656037ull;
|
||||
for (auto s : slots)
|
||||
{
|
||||
std::uint64_t v = static_cast<std::uint64_t>(s);
|
||||
for (int i = 0; i < 8; ++i)
|
||||
{
|
||||
h ^= (v >> (8 * i)) & 0xffu;
|
||||
h *= 1099511628211ull;
|
||||
}
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
} // namespace net
|
||||
} // namespace dpf
|
||||
|
||||
#endif // LIBDPF_INCLUDE_DPF_NET_ROUND_LANE_HPP__
|
||||
238
include/dpf/net/round_sink.hpp
Normal file
238
include/dpf/net/round_sink.hpp
Normal file
|
|
@ -0,0 +1,238 @@
|
|||
/// @file dpf/net/round_sink.hpp
|
||||
/// @brief Transport for round-batched protocol slots.
|
||||
/// @details A session submits one slot per `(round, index)`. The sink writes
|
||||
/// only a contiguous prefix of each round and demuxes the peer's
|
||||
/// matching prefixes into that round's incoming buffer. Memory,
|
||||
/// muxed-trio, and per-round stream sinks all implement this.
|
||||
#ifndef LIBDPF_INCLUDE_DPF_NET_ROUND_SINK_HPP__
|
||||
#define LIBDPF_INCLUDE_DPF_NET_ROUND_SINK_HPP__
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
namespace dpf
|
||||
{
|
||||
namespace net
|
||||
{
|
||||
|
||||
/// @brief Byte transport for one round of a count-sized batch.
|
||||
class RoundSink
|
||||
{
|
||||
public:
|
||||
virtual ~RoundSink() = default;
|
||||
|
||||
/// @brief How many protocol instances this sink was sized for.
|
||||
virtual std::size_t count() const noexcept = 0;
|
||||
|
||||
/// @brief How many interactive rounds this sink was sized for.
|
||||
virtual std::size_t rounds() const noexcept = 0;
|
||||
|
||||
/// @brief Slot width for `round`. Every index of that round uses it.
|
||||
virtual std::size_t slot_bytes(std::uint16_t round) const = 0;
|
||||
|
||||
/// @brief Store this party's outgoing slot. Index order on the wire is
|
||||
/// enforced at flush time; submits may arrive out of order.
|
||||
virtual void submit(std::uint16_t round, std::size_t index,
|
||||
const std::uint8_t * bytes, std::size_t n) = 0;
|
||||
|
||||
/// @brief True when the peer's slot for `(round, index)` is available.
|
||||
virtual bool peer_ready(std::uint16_t round, std::size_t index) const = 0;
|
||||
|
||||
/// @brief Copy the peer's slot into `out`. Requires `peer_ready`.
|
||||
virtual void read_peer(std::uint16_t round, std::size_t index,
|
||||
std::uint8_t * out, std::size_t n) const = 0;
|
||||
|
||||
/// @brief Send every newly contiguous prefix and pull any peer prefixes.
|
||||
virtual void flush() = 0;
|
||||
|
||||
/// @brief Flush only `round`'s pending prefix (one barrier on that round).
|
||||
/// @details `sink_exchange` and bit-AND layers call this so a stream sink
|
||||
/// does not touch idle round channels. Default falls back to
|
||||
/// `flush()` for sinks that only implement a global barrier.
|
||||
virtual void flush_round(std::uint16_t /*round*/)
|
||||
{
|
||||
flush();
|
||||
}
|
||||
|
||||
/// @brief Service non-blocking I/O. Memory sinks are a no-op.
|
||||
virtual void poll() = 0;
|
||||
|
||||
/// @brief Block (briefly) until I/O progresses, instead of busy-spinning.
|
||||
/// @details Drive loops call this while waiting on a peer slot. The default
|
||||
/// just `poll()`s and reports "no blocking wait happened" (`false`)
|
||||
/// so existing sinks keep their old behaviour. An event-driven sink
|
||||
/// (e.g. `async_round_sink`) overrides this to sleep in `epoll` via
|
||||
/// `io_context::run_one()`, returning `true` when it ran a handler.
|
||||
/// Returning `false` lets the caller fall back to its spin guard.
|
||||
virtual bool wait_io()
|
||||
{
|
||||
poll();
|
||||
return false;
|
||||
}
|
||||
|
||||
/// @brief Wait up to `budget` for one completion.
|
||||
/// @details Memory sinks ignore the budget and behave like `wait_io()`.
|
||||
/// Async sinks sleep in `run_one_for`.
|
||||
virtual bool wait_io_for(std::chrono::milliseconds)
|
||||
{
|
||||
return wait_io();
|
||||
}
|
||||
|
||||
/// @brief True when `wait_io_for` sleeps until I/O arrives. Drive loops
|
||||
/// yield instead of sleeping on sinks that cannot block.
|
||||
virtual bool can_block() const noexcept { return false; }
|
||||
|
||||
/// @brief False while an outbound window is full; pipelined sends pause.
|
||||
virtual bool can_send_ahead() const noexcept { return true; }
|
||||
|
||||
/// @brief Monotonic count of receive events (peer bytes plus zero-width
|
||||
/// round announcements). Drive loops re-check readiness whenever it
|
||||
/// moves, including when a `poll()` completed the read.
|
||||
virtual std::uint64_t progress() const noexcept { return 0; }
|
||||
|
||||
/// @brief Sinks that return the same key share one event loop, so blocking
|
||||
/// in one's `wait_io_for` also completes the others' I/O.
|
||||
virtual const void * wait_domain() const noexcept { return this; }
|
||||
};
|
||||
|
||||
/// @brief Wait until `sink.peer_ready(round, index)`, for at most `budget`.
|
||||
/// @details Sleeps in the sink's reactor when it can block and yields
|
||||
/// otherwise, so a slow peer costs no CPU and a dead one fails on
|
||||
/// time rather than after a spin count.
|
||||
inline void wait_peer_ready(RoundSink & sink, std::uint16_t round,
|
||||
std::size_t index, std::chrono::milliseconds budget, const char * what)
|
||||
{
|
||||
const auto start = std::chrono::steady_clock::now();
|
||||
while (!sink.peer_ready(round, index))
|
||||
{
|
||||
const auto waited = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now() - start);
|
||||
if (waited >= budget)
|
||||
throw std::runtime_error(std::string(what) + ": no peer bytes for "
|
||||
+ std::to_string(waited.count()) + " ms (budget "
|
||||
+ std::to_string(budget.count()) + " ms)");
|
||||
if (sink.can_block())
|
||||
sink.wait_io_for(std::min(budget - waited, std::chrono::milliseconds(1000)));
|
||||
else if (!sink.wait_io())
|
||||
std::this_thread::yield();
|
||||
}
|
||||
}
|
||||
|
||||
/// @brief One round's local and peer slot storage with prefix-flush cursors.
|
||||
class round_window
|
||||
{
|
||||
public:
|
||||
round_window() = default;
|
||||
|
||||
round_window(std::size_t count, std::size_t slot_bytes)
|
||||
: count_(count),
|
||||
slot_bytes_(slot_bytes),
|
||||
written_(count, false),
|
||||
out_(count * slot_bytes),
|
||||
in_(count * slot_bytes)
|
||||
{ }
|
||||
|
||||
std::size_t count() const noexcept { return count_; }
|
||||
std::size_t slot_bytes() const noexcept { return slot_bytes_; }
|
||||
std::size_t next_unwritten() const noexcept { return next_unwritten_; }
|
||||
std::size_t peer_filled() const noexcept { return peer_filled_; }
|
||||
std::size_t flushed() const noexcept { return flushed_; }
|
||||
|
||||
void submit(std::size_t index, const std::uint8_t * bytes, std::size_t n)
|
||||
{
|
||||
if (index >= count_)
|
||||
throw std::out_of_range("round_window submit index");
|
||||
if (n != slot_bytes_)
|
||||
throw std::invalid_argument("round_window submit size");
|
||||
if (written_[index])
|
||||
throw std::logic_error("round_window double submit");
|
||||
std::memcpy(out_.data() + index * slot_bytes_, bytes, n);
|
||||
written_[index] = true;
|
||||
while (next_unwritten_ < count_ && written_[next_unwritten_])
|
||||
++next_unwritten_;
|
||||
}
|
||||
|
||||
bool peer_ready(std::size_t index) const noexcept
|
||||
{
|
||||
return index < peer_filled_;
|
||||
}
|
||||
|
||||
void read_peer(std::size_t index, std::uint8_t * out, std::size_t n) const
|
||||
{
|
||||
if (!peer_ready(index))
|
||||
throw std::logic_error("round_window peer not ready");
|
||||
if (n != slot_bytes_)
|
||||
throw std::invalid_argument("round_window read size");
|
||||
std::memcpy(out, in_.data() + index * slot_bytes_, n);
|
||||
}
|
||||
|
||||
/// @brief Bytes of the contiguous prefix that have not been flushed yet.
|
||||
const std::uint8_t * pending_out(std::size_t & begin, std::size_t & nslots) const
|
||||
{
|
||||
begin = flushed_;
|
||||
if (next_unwritten_ <= flushed_)
|
||||
{
|
||||
nslots = 0;
|
||||
return nullptr;
|
||||
}
|
||||
nslots = next_unwritten_ - flushed_;
|
||||
return out_.data() + flushed_ * slot_bytes_;
|
||||
}
|
||||
|
||||
void mark_flushed(std::size_t nslots)
|
||||
{
|
||||
flushed_ += nslots;
|
||||
if (flushed_ > next_unwritten_)
|
||||
throw std::logic_error("round_window flushed past written");
|
||||
}
|
||||
|
||||
/// @brief Outbound bytes starting at slot `slot` (retained for resends).
|
||||
const std::uint8_t * out_at(std::size_t slot) const noexcept
|
||||
{
|
||||
return out_.data() + slot * slot_bytes_;
|
||||
}
|
||||
|
||||
/// @brief Append `nslots` peer slots starting at `peer_filled_`.
|
||||
void accept_peer(const std::uint8_t * bytes, std::size_t nslots)
|
||||
{
|
||||
if (peer_filled_ + nslots > count_)
|
||||
throw std::logic_error("round_window peer overrun");
|
||||
if (nslots != 0)
|
||||
{
|
||||
std::memcpy(in_.data() + peer_filled_ * slot_bytes_, bytes,
|
||||
nslots * slot_bytes_);
|
||||
}
|
||||
peer_filled_ += nslots;
|
||||
}
|
||||
|
||||
/// @brief Accept peer slots that begin at an absolute index (mux frames).
|
||||
void accept_peer_at(std::size_t begin, const std::uint8_t * bytes,
|
||||
std::size_t nslots)
|
||||
{
|
||||
if (begin != peer_filled_)
|
||||
throw std::logic_error("round_window peer gap");
|
||||
accept_peer(bytes, nslots);
|
||||
}
|
||||
|
||||
private:
|
||||
std::size_t count_ = 0;
|
||||
std::size_t slot_bytes_ = 0;
|
||||
std::size_t next_unwritten_ = 0;
|
||||
std::size_t flushed_ = 0;
|
||||
std::size_t peer_filled_ = 0;
|
||||
std::vector<bool> written_;
|
||||
std::vector<std::uint8_t> out_;
|
||||
std::vector<std::uint8_t> in_;
|
||||
};
|
||||
|
||||
} // namespace net
|
||||
} // namespace dpf
|
||||
|
||||
#endif // LIBDPF_INCLUDE_DPF_NET_ROUND_SINK_HPP__
|
||||
177
include/dpf/net/secure_channel.hpp
Normal file
177
include/dpf/net/secure_channel.hpp
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
/// @file dpf/net/secure_channel.hpp
|
||||
/// @brief Framed `channel` edges using the same peer TLS policy as `party_session`.
|
||||
/// @details Mux paths go through `party_session`. Framed APIs (`tcp_pair`,
|
||||
/// `trio`) keep length/tag framing but run the same `peer_security`
|
||||
/// defaults: TLS 1.3 when `encrypt` is on, optional per-direction
|
||||
/// authentication, socket tuning, and link logging.
|
||||
#ifndef LIBDPF_INCLUDE_DPF_NET_SECURE_CHANNEL_HPP__
|
||||
#define LIBDPF_INCLUDE_DPF_NET_SECURE_CHANNEL_HPP__
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include "dpf/log.hpp"
|
||||
#include "dpf/net/channel.hpp"
|
||||
#include "dpf/net/identity.hpp"
|
||||
#include "dpf/net/link_log.hpp"
|
||||
#include "dpf/net/policy.hpp"
|
||||
#include "dpf/net/security.hpp"
|
||||
#include "dpf/net/socket_tune.hpp"
|
||||
#include "dpf/net/tls.hpp"
|
||||
|
||||
namespace dpf
|
||||
{
|
||||
namespace net
|
||||
{
|
||||
|
||||
/// @brief This process's key for a party link, or a fresh one (logged).
|
||||
inline std::shared_ptr<const identity> resolve_peer_identity(
|
||||
const peer_security & sec, const std::string & who)
|
||||
{
|
||||
if (sec.self)
|
||||
{
|
||||
DPF_LOG(info, "security.identity").kv("who", who)
|
||||
.kv("key", sec.self->key().base64()).kv("ephemeral", false)
|
||||
.kv("trusted", sec.trusted.size());
|
||||
return sec.self;
|
||||
}
|
||||
auto id = std::make_shared<identity>(identity::generate());
|
||||
DPF_LOG(info, "security.identity").kv("who", who).kv("key", id->key().base64())
|
||||
.kv("ephemeral", true).kv("trusted", sec.trusted.size());
|
||||
if (log::first_time("security.no_identity." + log::role() + "." + who))
|
||||
DPF_LOG(warning, "security.no_identity").kv("who", who)
|
||||
.kv("detail", "no identity key configured: links are encrypted to a fresh "
|
||||
"key for this run, so peers cannot authenticate " + who);
|
||||
return id;
|
||||
}
|
||||
|
||||
inline void note_channel_security(const std::string & peer_role,
|
||||
const link_security & sec)
|
||||
{
|
||||
if (!sec.encrypted)
|
||||
return;
|
||||
const std::string me = log::role().empty() ? std::string("this party") : log::role();
|
||||
if (sec.peer_auth == "none"
|
||||
&& log::first_time("security.unauthenticated." + me + "." + peer_role))
|
||||
DPF_LOG(warning, "security.unauthenticated").kv("peer", peer_role)
|
||||
.kv("peer_key", sec.peer_key ? sec.peer_key->base64() : std::string("none"))
|
||||
.kv("detail", "no key configured for " + peer_role + ": the link is "
|
||||
"encrypted but " + peer_role + " is not authenticated");
|
||||
}
|
||||
|
||||
/// @brief Adopt a TCP socket as a framed channel under `peer_security`.
|
||||
inline channel secure_tcp_channel(asio::io_context & io, asio::ip::tcp::socket sock,
|
||||
bool server, std::uint32_t peer_party, const peer_security & sec = {},
|
||||
const socket_options & so = {},
|
||||
std::chrono::milliseconds handshake = std::chrono::milliseconds(30000),
|
||||
const std::string & peer_role = {}, const char * how = "connect")
|
||||
{
|
||||
const std::string who = peer_role.empty()
|
||||
? ("party " + std::to_string(peer_party))
|
||||
: peer_role;
|
||||
tune_tcp(sock, so);
|
||||
if (!sec.encrypt)
|
||||
{
|
||||
const int fd = sock.native_handle();
|
||||
log_link_up(how, who, transport::mux, 1, 0, 0, fd, so, nullptr);
|
||||
return channel(std::move(sock));
|
||||
}
|
||||
#if DPF_HAS_OPENSSL
|
||||
auto self = resolve_peer_identity(sec, log::role().empty() ? "channel" : log::role());
|
||||
auto ctx = make_peer_tls_context(*self);
|
||||
auto tls = std::make_unique<tls_stream>(std::move(sock), *ctx);
|
||||
try
|
||||
{
|
||||
tls_handshake(io, *tls, server, handshake, who + " TLS");
|
||||
}
|
||||
catch (const std::system_error & e)
|
||||
{
|
||||
throw std::runtime_error(std::string(e.what())
|
||||
+ " (if the peer has encryption off, set it the same at both ends)");
|
||||
}
|
||||
link_security desc = tls_describe(*tls);
|
||||
try
|
||||
{
|
||||
check_peer(desc, sec, peer_party, who);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
std::error_code e;
|
||||
tls->lowest_layer().close(e);
|
||||
throw;
|
||||
}
|
||||
note_channel_security(who, desc);
|
||||
const int fd = tls->lowest_layer().native_handle();
|
||||
log_link_up(how, who, transport::mux, 1, 0, 0, fd, so, &desc);
|
||||
return channel::from_tls(io, std::move(*tls), std::move(ctx));
|
||||
#else
|
||||
(void)io;
|
||||
(void)server;
|
||||
(void)handshake;
|
||||
(void)how;
|
||||
throw std::logic_error("secure_tcp_channel: built without OpenSSL; set "
|
||||
"encryption=off for plaintext links");
|
||||
#endif
|
||||
}
|
||||
|
||||
#if DPF_HAS_OPENSSL
|
||||
/// @brief Adopt a unix-domain socket under the same peer TLS policy.
|
||||
inline channel secure_local_channel(asio::io_context & io,
|
||||
asio::local::stream_protocol::socket sock, bool server,
|
||||
std::uint32_t peer_party, const peer_security & sec = {},
|
||||
std::chrono::milliseconds handshake = std::chrono::milliseconds(30000),
|
||||
const std::string & peer_role = {}, const char * how = "connect")
|
||||
{
|
||||
const std::string who = peer_role.empty()
|
||||
? ("party " + std::to_string(peer_party))
|
||||
: peer_role;
|
||||
if (!sec.encrypt)
|
||||
return channel(std::move(sock));
|
||||
auto self = resolve_peer_identity(sec, log::role().empty() ? "channel" : log::role());
|
||||
auto ctx = make_peer_tls_context(*self);
|
||||
auto tls = std::make_unique<tls_local_stream>(std::move(sock), *ctx);
|
||||
try
|
||||
{
|
||||
tls_handshake(io, *tls, server, handshake, who + " TLS");
|
||||
}
|
||||
catch (const std::system_error & e)
|
||||
{
|
||||
throw std::runtime_error(std::string(e.what())
|
||||
+ " (if the peer has encryption off, set it the same at both ends)");
|
||||
}
|
||||
link_security desc = tls_describe(*tls);
|
||||
try
|
||||
{
|
||||
check_peer(desc, sec, peer_party, who);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
std::error_code e;
|
||||
tls->lowest_layer().close(e);
|
||||
throw;
|
||||
}
|
||||
note_channel_security(who, desc);
|
||||
(void)how;
|
||||
return channel::from_tls_local(io, std::move(*tls), std::move(ctx));
|
||||
}
|
||||
#else
|
||||
inline channel secure_local_channel(asio::io_context &,
|
||||
asio::local::stream_protocol::socket sock, bool, std::uint32_t,
|
||||
const peer_security & sec = {}, std::chrono::milliseconds = {},
|
||||
const std::string & = {}, const char * = nullptr)
|
||||
{
|
||||
if (sec.encrypt)
|
||||
throw std::logic_error("secure_local_channel: built without OpenSSL; set "
|
||||
"encryption=off for plaintext links");
|
||||
return channel(std::move(sock));
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace net
|
||||
} // namespace dpf
|
||||
|
||||
#endif // LIBDPF_INCLUDE_DPF_NET_SECURE_CHANNEL_HPP__
|
||||
100
include/dpf/net/security.hpp
Normal file
100
include/dpf/net/security.hpp
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
/// @file dpf/net/security.hpp
|
||||
/// @brief What each link encrypts and whom it authenticates.
|
||||
/// @details Party links run TLS 1.3 on every socket edge unless `encrypt` is
|
||||
/// off. A party with no `self` key uses a fresh key for this
|
||||
/// process, so peers can encrypt to it but cannot authenticate it. A
|
||||
/// party authenticates exactly the peers whose keys it holds in
|
||||
/// `trusted`; a peer without an entry is accepted unauthenticated and
|
||||
/// the link logs that. Client links: the server always presents a
|
||||
/// certificate and the client verifies it unless `verify` is off,
|
||||
/// against a pinned key, a CA chain plus host name, or, when neither
|
||||
/// is configured, the built-in development certificate (public, and
|
||||
/// logged as providing no security).
|
||||
#ifndef LIBDPF_INCLUDE_DPF_NET_SECURITY_HPP__
|
||||
#define LIBDPF_INCLUDE_DPF_NET_SECURITY_HPP__
|
||||
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "dpf/net/identity.hpp"
|
||||
|
||||
namespace dpf
|
||||
{
|
||||
namespace net
|
||||
{
|
||||
|
||||
/// @brief How one link was secured, for logs and callers.
|
||||
struct link_security
|
||||
{
|
||||
bool encrypted = false;
|
||||
std::string protocol; ///< e.g. `TLSv1.3`
|
||||
std::string cipher; ///< e.g. `TLS_AES_128_GCM_SHA256`
|
||||
std::optional<public_key> peer_key;
|
||||
/// How this side authenticated the peer: `key` (a key it holds), `ca`
|
||||
/// (CA chain and name), `development`, or `none`.
|
||||
std::string peer_auth = "none";
|
||||
/// Whether the peer reported that it authenticated this side.
|
||||
bool peer_verified_us = false;
|
||||
};
|
||||
|
||||
/// @brief The dealer's id in handshakes and trust tables.
|
||||
inline constexpr std::uint32_t dealer_id = 0xfffffffeu;
|
||||
|
||||
/// @brief Party-to-party (and dealer) links.
|
||||
struct peer_security
|
||||
{
|
||||
/// TLS 1.3 on socket links; off leaves them plaintext.
|
||||
bool encrypt = true;
|
||||
/// This party's key; empty means a fresh key for this process.
|
||||
std::shared_ptr<const identity> self;
|
||||
/// Keys this party checks, by party id (`party_session::k_dealer_id` for
|
||||
/// the dealer).
|
||||
std::map<std::uint32_t, public_key> trusted;
|
||||
|
||||
const public_key * trusted_key(std::uint32_t party) const
|
||||
{
|
||||
const auto it = trusted.find(party);
|
||||
return it == trusted.end() ? nullptr : &it->second;
|
||||
}
|
||||
};
|
||||
|
||||
/// @brief What a client accepts from a server.
|
||||
struct client_security
|
||||
{
|
||||
/// Off accepts any certificate (logged as an error on every connect).
|
||||
bool verify = true;
|
||||
/// Accept a server presenting one of these raw keys.
|
||||
std::vector<public_key> pins;
|
||||
/// PEM bundle for CA-issued server certificates; `system` uses OpenSSL's
|
||||
/// default trust store.
|
||||
std::string ca_file;
|
||||
/// Name the CA-issued certificate must carry (default: the host dialed).
|
||||
std::string server_name;
|
||||
/// Optional client key, presented to servers that check clients.
|
||||
std::shared_ptr<const identity> self;
|
||||
|
||||
bool configured() const noexcept { return !pins.empty() || !ca_file.empty(); }
|
||||
};
|
||||
|
||||
/// @brief What a server presents to clients.
|
||||
struct server_security
|
||||
{
|
||||
/// PEM certificate chain and its private key (CA-issued certificates).
|
||||
std::string cert_file;
|
||||
std::string key_file;
|
||||
/// Raw-key identity when no PEM files are set; with neither, the server
|
||||
/// presents the development certificate.
|
||||
std::shared_ptr<const identity> self;
|
||||
/// Client keys this server checks; a client without a matching key is
|
||||
/// accepted unauthenticated and logged.
|
||||
std::vector<public_key> client_pins;
|
||||
};
|
||||
|
||||
} // namespace net
|
||||
} // namespace dpf
|
||||
|
||||
#endif // LIBDPF_INCLUDE_DPF_NET_SECURITY_HPP__
|
||||
169
include/dpf/net/sink_exchange.hpp
Normal file
169
include/dpf/net/sink_exchange.hpp
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
/// @file dpf/net/sink_exchange.hpp
|
||||
/// @brief One-at-a-time typed exchange on a RoundSink (count lanes).
|
||||
#ifndef LIBDPF_INCLUDE_DPF_NET_SINK_EXCHANGE_HPP__
|
||||
#define LIBDPF_INCLUDE_DPF_NET_SINK_EXCHANGE_HPP__
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <stdexcept>
|
||||
#include <type_traits>
|
||||
#include <vector>
|
||||
|
||||
#include "dpf/net/round_sink.hpp"
|
||||
|
||||
namespace dpf
|
||||
{
|
||||
namespace net
|
||||
{
|
||||
|
||||
/// @brief Advance through RoundSink rounds with trivially copyable slots.
|
||||
class sink_exchange
|
||||
{
|
||||
public:
|
||||
sink_exchange(RoundSink & sink, std::size_t index = 0)
|
||||
: sink_(&sink), index_(index)
|
||||
{ }
|
||||
|
||||
std::uint16_t round() const noexcept { return round_; }
|
||||
std::uint16_t & round_ref() noexcept { return round_; }
|
||||
|
||||
template <typename T>
|
||||
T operator()(const T & mine)
|
||||
{
|
||||
static_assert(std::is_trivially_copyable_v<T>,
|
||||
"sink_exchange requires a trivially copyable type");
|
||||
if (!sink_)
|
||||
throw std::logic_error("sink_exchange: no sink");
|
||||
if (round_ >= sink_->rounds())
|
||||
throw std::runtime_error("sink_exchange: out of rounds");
|
||||
const std::size_t slot = sink_->slot_bytes(round_);
|
||||
if (sizeof(T) > slot)
|
||||
throw std::runtime_error("sink_exchange: slot too small");
|
||||
std::vector<std::uint8_t> buf(slot, 0);
|
||||
if constexpr (sizeof(T) != 0)
|
||||
std::memcpy(buf.data(), &mine, sizeof(T));
|
||||
sink_->submit(round_, index_, buf.data(), buf.size());
|
||||
sink_->flush_round(round_);
|
||||
sink_->poll();
|
||||
if (!sink_->peer_ready(round_, index_))
|
||||
throw std::runtime_error("sink_exchange: peer missing");
|
||||
std::vector<std::uint8_t> peer_buf(slot);
|
||||
sink_->read_peer(round_, index_, peer_buf.data(), slot);
|
||||
T peer{};
|
||||
if constexpr (sizeof(T) != 0)
|
||||
std::memcpy(&peer, peer_buf.data(), sizeof(T));
|
||||
++round_;
|
||||
return peer;
|
||||
}
|
||||
|
||||
private:
|
||||
RoundSink * sink_ = nullptr;
|
||||
std::size_t index_ = 0;
|
||||
std::uint16_t round_ = 0;
|
||||
};
|
||||
|
||||
/// @brief Slot widths for a classic non-verifiable point walk of `depth` levels.
|
||||
/// @details Per level: blind_msg, share_msg, advice_msg, and one 16-byte
|
||||
/// bit-block product. Callers pad every slot to `max` so a single
|
||||
/// sink sizing works for the whole walk.
|
||||
inline std::vector<std::size_t> point_walk_slot_bytes(std::size_t depth,
|
||||
std::size_t max_slot)
|
||||
{
|
||||
std::vector<std::size_t> out;
|
||||
out.reserve(depth * 4);
|
||||
for (std::size_t i = 0; i < depth * 4; ++i)
|
||||
out.push_back(max_slot);
|
||||
return out;
|
||||
}
|
||||
|
||||
/// @brief Conservative RoundSink sizing for a Doerner–Shelat point or
|
||||
/// comparison walk, including optional oblivious-hash AND layers.
|
||||
/// @param depth tree depth (`dpf_type::depth`)
|
||||
/// @param with_oblivious_hash verifiable non-reveal path (shared prefix hash)
|
||||
/// @param lg_outputs `dpf_type::lg_outputs_per_leaf` — packed leaf mux cost
|
||||
|
||||
/// Boyar–Peralta SubBytes AND-depth layers recorded by compose / party OH walks.
|
||||
inline constexpr std::size_t ds_oh_subbytes_per_level = 10;
|
||||
inline constexpr std::size_t ds_oh_and_layers_per_subbytes = 8;
|
||||
inline constexpr std::size_t ds_oh_exchanges_per_level =
|
||||
ds_oh_subbytes_per_level * ds_oh_and_layers_per_subbytes;
|
||||
inline constexpr std::size_t ds_oh_slot_bytes = 4096;
|
||||
|
||||
/// @brief Slot widths matching a composed DS walk (4 opens/level + OH layers).
|
||||
/// @details Prefer this when sizing a sink for `composer::level_walk_ds` /
|
||||
/// `level_walk_ds_sized`. `ds_walk_slot_bytes` stays the conservative
|
||||
/// party oversize used by hand `dist_ds` paths.
|
||||
inline std::vector<std::size_t> compose_ds_slot_bytes(std::size_t depth,
|
||||
std::size_t fss_slot_bytes, bool with_oblivious_hash,
|
||||
std::size_t lg_outputs = 0)
|
||||
{
|
||||
if (depth == 0)
|
||||
throw std::invalid_argument("compose_ds_slot_bytes depth must be > 0");
|
||||
if (fss_slot_bytes == 0)
|
||||
throw std::invalid_argument("compose_ds_slot_bytes slot must be > 0");
|
||||
std::vector<std::size_t> out;
|
||||
out.reserve(depth * (5 + (with_oblivious_hash ? ds_oh_exchanges_per_level : 0))
|
||||
+ 64);
|
||||
for (std::size_t level = 0; level < depth; ++level)
|
||||
{
|
||||
// blind, share, advice, AND round 1, AND round 2 — same as point_party.
|
||||
for (int i = 0; i < 5; ++i)
|
||||
out.push_back(fss_slot_bytes);
|
||||
if (with_oblivious_hash)
|
||||
{
|
||||
for (std::size_t i = 0; i < ds_oh_exchanges_per_level; ++i)
|
||||
out.push_back(ds_oh_slot_bytes);
|
||||
}
|
||||
}
|
||||
if (lg_outputs > 0 && lg_outputs < 32)
|
||||
{
|
||||
const std::size_t mux_nodes =
|
||||
2 * ((std::size_t{1} << lg_outputs) - 1);
|
||||
for (std::size_t i = 0; i < mux_nodes; ++i)
|
||||
out.push_back(fss_slot_bytes);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
inline std::vector<std::size_t> ds_walk_slot_bytes(std::size_t depth,
|
||||
bool with_oblivious_hash, std::size_t lg_outputs = 0)
|
||||
{
|
||||
// Envelope of the hand walk (typed frames, leaf extras) and a composed
|
||||
// walk at 16-byte FSS slots. Every compose round fits in this sink;
|
||||
// extra rounds keep the hand walk from running out of slots.
|
||||
const auto composed = compose_ds_slot_bytes(depth, 16, with_oblivious_hash,
|
||||
lg_outputs);
|
||||
// Per level: blind, share, advice, two beaver_bit_block exchanges, plus
|
||||
// reveal / leaf extras. Oblivious hash: 10 SubBytes × ~8 BP AND-depth
|
||||
// layers (one schedule round each), not one round per AND gate.
|
||||
std::size_t n = depth * 10 + 16;
|
||||
// Packed leaf mux: two sink exchanges per internal node of a complete
|
||||
// binary tree with 2^lg candidates. Verifiable non-reveal wildcards run
|
||||
// a second mux for the payload vector — budget both.
|
||||
if (lg_outputs > 0 && lg_outputs < 32)
|
||||
{
|
||||
const std::size_t mux =
|
||||
2 * ((std::size_t{1} << lg_outputs) - 1) + 8;
|
||||
n += mux;
|
||||
if (with_oblivious_hash)
|
||||
n += mux;
|
||||
}
|
||||
std::size_t slot = 64;
|
||||
if (with_oblivious_hash)
|
||||
{
|
||||
n += depth * ds_oh_exchanges_per_level;
|
||||
// Packed mask bits for a full SubBytes AND layer across 8×16 bytes.
|
||||
slot = ds_oh_slot_bytes;
|
||||
}
|
||||
std::vector<std::size_t> out(std::max(n, composed.size()), slot);
|
||||
for (std::size_t i = 0; i < composed.size(); ++i)
|
||||
out[i] = std::max(out[i], composed[i]);
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace net
|
||||
} // namespace dpf
|
||||
|
||||
#endif // LIBDPF_INCLUDE_DPF_NET_SINK_EXCHANGE_HPP__
|
||||
135
include/dpf/net/socket_tune.hpp
Normal file
135
include/dpf/net/socket_tune.hpp
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
/// @file dpf/net/socket_tune.hpp
|
||||
/// @brief Apply `socket_options` to TCP and unix-domain sockets.
|
||||
#ifndef LIBDPF_INCLUDE_DPF_NET_SOCKET_TUNE_HPP__
|
||||
#define LIBDPF_INCLUDE_DPF_NET_SOCKET_TUNE_HPP__
|
||||
|
||||
#include <system_error>
|
||||
#include <type_traits>
|
||||
|
||||
#include "dpf/net/asio_ns.hpp"
|
||||
|
||||
#if defined(__linux__)
|
||||
#include <netinet/in.h>
|
||||
#include <netinet/tcp.h>
|
||||
#include <sys/socket.h>
|
||||
#endif
|
||||
|
||||
#include "dpf/net/policy.hpp"
|
||||
|
||||
namespace dpf
|
||||
{
|
||||
namespace net
|
||||
{
|
||||
|
||||
/// @brief Linux clears `TCP_QUICKACK` after one ACK. Call again after I/O.
|
||||
inline void rearm_quickack(asio::ip::tcp::socket & sock)
|
||||
{
|
||||
#if defined(TCP_QUICKACK)
|
||||
std::error_code ec;
|
||||
using quickack =
|
||||
asio::detail::socket_option::boolean<IPPROTO_TCP, TCP_QUICKACK>;
|
||||
sock.set_option(quickack(true), ec);
|
||||
#else
|
||||
(void)sock;
|
||||
#endif
|
||||
}
|
||||
|
||||
namespace detail
|
||||
{
|
||||
|
||||
template <typename Socket>
|
||||
inline void apply_buffers(Socket & sock, const socket_options & o)
|
||||
{
|
||||
std::error_code ec;
|
||||
if (o.send_buffer > 0)
|
||||
sock.set_option(asio::socket_base::send_buffer_size(o.send_buffer), ec);
|
||||
if (o.recv_buffer > 0)
|
||||
sock.set_option(asio::socket_base::receive_buffer_size(o.recv_buffer),
|
||||
ec);
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
inline void tune_tcp(asio::ip::tcp::socket & sock, const socket_options & o = {})
|
||||
{
|
||||
std::error_code ec;
|
||||
sock.set_option(asio::ip::tcp::no_delay(o.no_delay), ec);
|
||||
sock.set_option(asio::socket_base::keep_alive(o.keepalive), ec);
|
||||
#if defined(__linux__)
|
||||
const int fd = sock.native_handle();
|
||||
if (o.keepalive && o.keepalive_idle_s > 0)
|
||||
(void)::setsockopt(fd, IPPROTO_TCP, TCP_KEEPIDLE, &o.keepalive_idle_s,
|
||||
sizeof(o.keepalive_idle_s));
|
||||
if (o.keepalive && o.keepalive_interval_s > 0)
|
||||
(void)::setsockopt(fd, IPPROTO_TCP, TCP_KEEPINTVL,
|
||||
&o.keepalive_interval_s, sizeof(o.keepalive_interval_s));
|
||||
if (o.keepalive && o.keepalive_count > 0)
|
||||
(void)::setsockopt(fd, IPPROTO_TCP, TCP_KEEPCNT, &o.keepalive_count,
|
||||
sizeof(o.keepalive_count));
|
||||
#endif
|
||||
detail::apply_buffers(sock, o);
|
||||
if (o.quickack)
|
||||
rearm_quickack(sock);
|
||||
}
|
||||
|
||||
namespace detail
|
||||
{
|
||||
|
||||
template <typename S, typename = void>
|
||||
struct is_layered_stream : std::false_type
|
||||
{
|
||||
};
|
||||
|
||||
template <typename S>
|
||||
struct is_layered_stream<S, std::void_t<typename S::next_layer_type>> : std::true_type
|
||||
{
|
||||
};
|
||||
|
||||
} // namespace detail
|
||||
|
||||
/// @brief The socket under `s`: itself, or the TCP socket under a TLS stream.
|
||||
template <typename S>
|
||||
inline auto & base_socket(S & s)
|
||||
{
|
||||
if constexpr (detail::is_layered_stream<S>::value)
|
||||
return base_socket(s.next_layer());
|
||||
else
|
||||
return s;
|
||||
}
|
||||
|
||||
inline void tune_stream(asio::ip::tcp::socket & sock, const socket_options & o = {})
|
||||
{
|
||||
tune_tcp(sock, o);
|
||||
}
|
||||
|
||||
template <typename Socket>
|
||||
inline void tune_stream(Socket & sock, const socket_options & o = {})
|
||||
{
|
||||
if constexpr (detail::is_layered_stream<Socket>::value)
|
||||
tune_stream(base_socket(sock), o);
|
||||
else
|
||||
detail::apply_buffers(sock, o);
|
||||
}
|
||||
|
||||
inline void rearm_stream(asio::ip::tcp::socket & sock, const socket_options & o)
|
||||
{
|
||||
if (o.quickack)
|
||||
rearm_quickack(sock);
|
||||
}
|
||||
|
||||
template <typename Socket>
|
||||
inline void rearm_stream(Socket & sock, const socket_options & o)
|
||||
{
|
||||
if constexpr (detail::is_layered_stream<Socket>::value)
|
||||
rearm_stream(base_socket(sock), o);
|
||||
else
|
||||
{
|
||||
(void)sock;
|
||||
(void)o;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace net
|
||||
} // namespace dpf
|
||||
|
||||
#endif
|
||||
572
include/dpf/net/stream_array.hpp
Normal file
572
include/dpf/net/stream_array.hpp
Normal file
|
|
@ -0,0 +1,572 @@
|
|||
/// @file dpf/net/stream_array.hpp
|
||||
/// @brief Indexed pack of duplex byte streams for dealer and online rounds.
|
||||
/// @details `read` / `write` / `flush` are the only operations. Backends are
|
||||
/// memory (tests), file (stored prep), and one-channel mux (TCP).
|
||||
/// SCTP is a later backend: index `i` maps to association stream `i`.
|
||||
/// Protocol code must not call TCP or SCTP sockets directly.
|
||||
#ifndef LIBDPF_INCLUDE_DPF_NET_STREAM_ARRAY_HPP__
|
||||
#define LIBDPF_INCLUDE_DPF_NET_STREAM_ARRAY_HPP__
|
||||
|
||||
#include <algorithm>
|
||||
#include <cerrno>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <system_error>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <poll.h>
|
||||
#include <sys/socket.h>
|
||||
|
||||
#include "hedley/hedley.h"
|
||||
|
||||
#include "dpf/net/channel.hpp"
|
||||
#include "dpf/net/policy.hpp"
|
||||
#include "dpf/net/round_lane.hpp"
|
||||
#include "dpf/net/round_sink.hpp"
|
||||
|
||||
namespace dpf
|
||||
{
|
||||
namespace net
|
||||
{
|
||||
|
||||
/// @brief Virtual base for N duplex streams.
|
||||
class stream_array
|
||||
{
|
||||
public:
|
||||
virtual ~stream_array() = default;
|
||||
|
||||
virtual std::size_t size() const noexcept = 0;
|
||||
|
||||
virtual void write(std::size_t i, const void * src, std::size_t n) = 0;
|
||||
virtual void read(std::size_t i, void * dst, std::size_t n) = 0;
|
||||
virtual void flush(std::size_t i) = 0;
|
||||
|
||||
void write_all(std::size_t i, const void * src, std::size_t n)
|
||||
{
|
||||
write(i, src, n);
|
||||
flush(i);
|
||||
}
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Memory
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
struct memory_stream_hub
|
||||
{
|
||||
std::size_t n = 0;
|
||||
std::vector<std::vector<std::uint8_t>> a_to_b;
|
||||
std::vector<std::vector<std::uint8_t>> b_to_a;
|
||||
std::vector<std::size_t> a_pos;
|
||||
std::vector<std::size_t> b_pos;
|
||||
mutable std::mutex mu;
|
||||
std::condition_variable cv;
|
||||
|
||||
explicit memory_stream_hub(std::size_t streams)
|
||||
: n(streams),
|
||||
a_to_b(streams),
|
||||
b_to_a(streams),
|
||||
a_pos(streams, 0),
|
||||
b_pos(streams, 0)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
/// @brief One end of a paired in-process stream array.
|
||||
class memory_stream_array : public stream_array
|
||||
{
|
||||
public:
|
||||
memory_stream_array(std::shared_ptr<memory_stream_hub> hub, bool side_a)
|
||||
: hub_(std::move(hub)), side_a_(side_a)
|
||||
{
|
||||
if (!hub_)
|
||||
throw std::invalid_argument("memory_stream_array needs a hub");
|
||||
}
|
||||
|
||||
std::size_t size() const noexcept override { return hub_->n; }
|
||||
|
||||
void write(std::size_t i, const void * src, std::size_t n) override
|
||||
{
|
||||
check(i);
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(hub_->mu);
|
||||
auto & q = side_a_ ? hub_->a_to_b[i] : hub_->b_to_a[i];
|
||||
const auto * p = static_cast<const std::uint8_t *>(src);
|
||||
q.insert(q.end(), p, p + n);
|
||||
}
|
||||
hub_->cv.notify_all();
|
||||
}
|
||||
|
||||
void read(std::size_t i, void * dst, std::size_t n) override
|
||||
{
|
||||
check(i);
|
||||
std::unique_lock<std::mutex> lock(hub_->mu);
|
||||
auto & q = side_a_ ? hub_->b_to_a[i] : hub_->a_to_b[i];
|
||||
auto & pos = side_a_ ? hub_->b_pos[i] : hub_->a_pos[i];
|
||||
hub_->cv.wait(lock, [&] { return pos + n <= q.size(); });
|
||||
std::memcpy(dst, q.data() + pos, n);
|
||||
pos += n;
|
||||
}
|
||||
|
||||
void flush(std::size_t) override {}
|
||||
|
||||
private:
|
||||
void check(std::size_t i) const
|
||||
{
|
||||
if (i >= hub_->n)
|
||||
throw std::out_of_range("memory_stream_array index");
|
||||
}
|
||||
|
||||
std::shared_ptr<memory_stream_hub> hub_;
|
||||
bool side_a_ = true;
|
||||
};
|
||||
|
||||
HEDLEY_WARN_UNUSED_RESULT
|
||||
inline std::pair<memory_stream_array, memory_stream_array>
|
||||
make_memory_stream_pair(std::size_t streams)
|
||||
{
|
||||
auto hub = std::make_shared<memory_stream_hub>(streams);
|
||||
return {memory_stream_array(hub, true), memory_stream_array(hub, false)};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// File (one file per stream index)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// @brief Read-only or write-only file per stream. Dealer writes; online reads.
|
||||
class file_stream_array : public stream_array
|
||||
{
|
||||
public:
|
||||
/// @brief Open `basename + "-" + i` for each index.
|
||||
/// @param write_mode true for dealer output, false for online input.
|
||||
file_stream_array(const std::string & basename, std::size_t streams,
|
||||
bool write_mode)
|
||||
: write_mode_(write_mode), files_(streams)
|
||||
{
|
||||
for (std::size_t i = 0; i < streams; ++i)
|
||||
{
|
||||
const auto path = basename + "-" + std::to_string(i);
|
||||
files_[i].open(path,
|
||||
write_mode ? (std::ios::binary | std::ios::trunc | std::ios::out)
|
||||
: (std::ios::binary | std::ios::in));
|
||||
if (!files_[i])
|
||||
throw std::runtime_error("file_stream_array: " + path);
|
||||
}
|
||||
}
|
||||
|
||||
std::size_t size() const noexcept override { return files_.size(); }
|
||||
|
||||
void write(std::size_t i, const void * src, std::size_t n) override
|
||||
{
|
||||
if (!write_mode_)
|
||||
throw std::logic_error("file_stream_array: write on read-only");
|
||||
check(i);
|
||||
files_[i].write(static_cast<const char *>(src),
|
||||
static_cast<std::streamsize>(n));
|
||||
if (!files_[i])
|
||||
throw std::runtime_error("file_stream_array: write failed");
|
||||
}
|
||||
|
||||
void read(std::size_t i, void * dst, std::size_t n) override
|
||||
{
|
||||
if (write_mode_)
|
||||
throw std::logic_error("file_stream_array: read on write-only");
|
||||
check(i);
|
||||
files_[i].read(static_cast<char *>(dst), static_cast<std::streamsize>(n));
|
||||
if (static_cast<std::size_t>(files_[i].gcount()) != n)
|
||||
throw std::runtime_error("file_stream_array: short read");
|
||||
}
|
||||
|
||||
void flush(std::size_t i) override
|
||||
{
|
||||
check(i);
|
||||
if (write_mode_)
|
||||
files_[i].flush();
|
||||
}
|
||||
|
||||
private:
|
||||
void check(std::size_t i) const
|
||||
{
|
||||
if (i >= files_.size())
|
||||
throw std::out_of_range("file_stream_array index");
|
||||
}
|
||||
|
||||
bool write_mode_ = false;
|
||||
std::vector<std::fstream> files_;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mux wire header (shared with async_mux_stream_array)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#pragma pack(push, 1)
|
||||
struct stream_frame_hdr
|
||||
{
|
||||
std::uint16_t index = 0;
|
||||
std::uint32_t length = 0;
|
||||
};
|
||||
#pragma pack(pop)
|
||||
|
||||
// Synchronous mux is `sync_stream_array` / `mux_stream_array` in
|
||||
// dpf/net/sync_stream_array.hpp: a blocking face over the async mux backends
|
||||
// (including TLS), so sync call sites share party_session encryption and
|
||||
// wire policy.
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// RoundSink adapter (round → stream, with lane reuse when rounds > streams)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// @brief Multi-round RoundSink over a synchronous `stream_array`.
|
||||
/// @details Same wire contract as `async_round_sink`: a plan-shape hello on
|
||||
/// lane 0 (sent at construction, read before the first peer bytes),
|
||||
/// unframed lanes when every round has its own lane, and framed
|
||||
/// `{round, nbytes}` lanes otherwise (or when `framing` says so). A
|
||||
/// framed round accepts partial instance prefixes. Call
|
||||
/// `flush_round(r)` before starting another round's pending writes.
|
||||
class stream_array_sink : public RoundSink
|
||||
{
|
||||
public:
|
||||
stream_array_sink(stream_array & streams,
|
||||
std::vector<std::size_t> slot_bytes_per_round, std::size_t count = 1,
|
||||
framing_mode framing = framing_mode::automatic, bool hello = true)
|
||||
: streams_(&streams),
|
||||
count_(count),
|
||||
slot_bytes_(std::move(slot_bytes_per_round)),
|
||||
map_(streams.size(), slot_bytes_.size(), framing),
|
||||
windows_(slot_bytes_.size()),
|
||||
in_(slot_bytes_.size()),
|
||||
in_filled_(slot_bytes_.size(), 0),
|
||||
zero_seen_(slot_bytes_.size(), false),
|
||||
announced_(slot_bytes_.size(), false),
|
||||
hello_(hello)
|
||||
{
|
||||
if (streams_->size() == 0)
|
||||
throw std::invalid_argument("stream_array_sink: empty streams");
|
||||
if (count_ == 0)
|
||||
throw std::invalid_argument("stream_array_sink: count 0");
|
||||
for (std::size_t r = 0; r < slot_bytes_.size(); ++r)
|
||||
{
|
||||
windows_[r] = round_window(count_, slot_bytes_[r]);
|
||||
in_[r].assign(count_ * slot_bytes_[r], 0);
|
||||
}
|
||||
if (hello_)
|
||||
send_hello();
|
||||
}
|
||||
|
||||
/// @brief Legacy single-round sink on stream `index` (no hello).
|
||||
stream_array_sink(stream_array & streams, std::size_t index, std::size_t slot)
|
||||
: streams_(&streams),
|
||||
count_(1),
|
||||
legacy_index_(index),
|
||||
use_legacy_index_(true),
|
||||
map_(1, 1)
|
||||
{
|
||||
slot_bytes_.push_back(slot);
|
||||
windows_.emplace_back(count_, slot);
|
||||
in_.emplace_back(count_ * slot, 0);
|
||||
in_filled_.push_back(0);
|
||||
zero_seen_.push_back(false);
|
||||
announced_.push_back(false);
|
||||
}
|
||||
|
||||
std::size_t count() const noexcept override { return count_; }
|
||||
std::size_t rounds() const noexcept override { return slot_bytes_.size(); }
|
||||
bool framed() const noexcept { return map_.framed && !use_legacy_index_; }
|
||||
|
||||
std::size_t slot_bytes(std::uint16_t round) const override
|
||||
{
|
||||
if (round >= slot_bytes_.size())
|
||||
throw std::out_of_range("stream_array_sink round");
|
||||
return slot_bytes_[round];
|
||||
}
|
||||
|
||||
void submit(std::uint16_t round, std::size_t index, const std::uint8_t * bytes,
|
||||
std::size_t n) override
|
||||
{
|
||||
window(round).submit(index, bytes, n);
|
||||
}
|
||||
|
||||
bool peer_ready(std::uint16_t round, std::size_t index) const override
|
||||
{
|
||||
if (round >= windows_.size())
|
||||
return false;
|
||||
if (framed())
|
||||
{
|
||||
if (slot_bytes_[round] == 0)
|
||||
return zero_seen_[round];
|
||||
return in_filled_[round] >= (index + 1) * slot_bytes_[round];
|
||||
}
|
||||
return window(round).peer_ready(index);
|
||||
}
|
||||
|
||||
void read_peer(std::uint16_t round, std::size_t index, std::uint8_t * out,
|
||||
std::size_t n) const override
|
||||
{
|
||||
if (framed())
|
||||
{
|
||||
if (!peer_ready(round, index))
|
||||
throw std::logic_error("stream_array_sink: peer not ready");
|
||||
if (n != slot_bytes_[round])
|
||||
throw std::invalid_argument("stream_array_sink read size");
|
||||
if (n != 0)
|
||||
std::memcpy(out, in_[round].data() + index * n, n);
|
||||
return;
|
||||
}
|
||||
window(round).read_peer(index, out, n);
|
||||
}
|
||||
|
||||
void flush() override
|
||||
{
|
||||
for (std::uint16_t r = 0; r < slot_bytes_.size(); ++r)
|
||||
flush_round(r);
|
||||
}
|
||||
|
||||
void flush_round(std::uint16_t round) override
|
||||
{
|
||||
auto & win = window(round);
|
||||
std::size_t begin = 0;
|
||||
std::size_t nslots = 0;
|
||||
const auto * pend = win.pending_out(begin, nslots);
|
||||
const std::size_t sb = slot_bytes_[round];
|
||||
const std::size_t stream_i = stream_index(round);
|
||||
const std::size_t nbytes = nslots * sb;
|
||||
if (framed())
|
||||
{
|
||||
const bool announce = sb == 0 && !announced_[round];
|
||||
if (nslots == 0 && !announce)
|
||||
return;
|
||||
auto frame = pack_round_frame(round, pend, nbytes);
|
||||
streams_->write(stream_i, frame.data(), frame.size());
|
||||
streams_->flush(stream_i);
|
||||
if (announce)
|
||||
announced_[round] = true;
|
||||
if (nslots != 0)
|
||||
win.mark_flushed(nslots);
|
||||
ensure_hello();
|
||||
while (sb == 0 ? !zero_seen_[round]
|
||||
: in_filled_[round] < win.flushed() * sb)
|
||||
pull_one_frame(stream_i);
|
||||
return;
|
||||
}
|
||||
if (nslots != 0)
|
||||
{
|
||||
streams_->write(stream_i, pend, nbytes);
|
||||
streams_->flush(stream_i);
|
||||
win.mark_flushed(nslots);
|
||||
}
|
||||
if (nslots == 0)
|
||||
return;
|
||||
ensure_hello();
|
||||
std::vector<std::uint8_t> peer(nbytes);
|
||||
streams_->read(stream_i, peer.data(), peer.size());
|
||||
win.accept_peer_at(begin, peer.data(), nslots);
|
||||
}
|
||||
|
||||
void poll() override {}
|
||||
|
||||
private:
|
||||
static constexpr std::uint32_t k_magic = 0x48535044u;
|
||||
static constexpr std::size_t k_fixed = 36;
|
||||
|
||||
std::size_t stream_index(std::uint16_t round) const
|
||||
{
|
||||
if (use_legacy_index_)
|
||||
return legacy_index_;
|
||||
return map_.lane(round);
|
||||
}
|
||||
|
||||
static void put32(std::uint8_t * p, std::uint32_t v)
|
||||
{
|
||||
for (int i = 0; i < 4; ++i)
|
||||
p[i] = static_cast<std::uint8_t>((v >> (8 * i)) & 0xffu);
|
||||
}
|
||||
|
||||
static std::uint32_t get32(const std::uint8_t * p)
|
||||
{
|
||||
return static_cast<std::uint32_t>(p[0]) | (static_cast<std::uint32_t>(p[1]) << 8)
|
||||
| (static_cast<std::uint32_t>(p[2]) << 16)
|
||||
| (static_cast<std::uint32_t>(p[3]) << 24);
|
||||
}
|
||||
|
||||
void send_hello()
|
||||
{
|
||||
const std::size_t r = slot_bytes_.size();
|
||||
std::vector<std::uint8_t> h(k_fixed + 4 * r, 0);
|
||||
put32(h.data(), k_magic);
|
||||
h[4] = 1;
|
||||
h[6] = map_.framed ? 1 : 0;
|
||||
put32(h.data() + 8, static_cast<std::uint32_t>(r));
|
||||
put32(h.data() + 12, static_cast<std::uint32_t>(map_.n_lanes));
|
||||
put32(h.data() + 16, static_cast<std::uint32_t>(count_));
|
||||
const std::uint64_t fp = slots_fingerprint(slot_bytes_);
|
||||
for (int i = 0; i < 8; ++i)
|
||||
h[24 + i] = static_cast<std::uint8_t>((fp >> (8 * i)) & 0xffu);
|
||||
streams_->write(0, h.data(), h.size());
|
||||
streams_->flush(0);
|
||||
}
|
||||
|
||||
void ensure_hello()
|
||||
{
|
||||
if (!hello_ || hello_read_)
|
||||
return;
|
||||
hello_read_ = true;
|
||||
std::uint8_t f[k_fixed];
|
||||
streams_->read(0, f, k_fixed);
|
||||
if (get32(f) != k_magic)
|
||||
throw std::runtime_error("stream_array_sink: peer did not send a "
|
||||
"sink hello (both ends must use sinks with hello enabled)");
|
||||
const std::uint32_t pr = get32(f + 8);
|
||||
const std::uint32_t pl = get32(f + 12);
|
||||
const std::uint32_t pc = get32(f + 16);
|
||||
const bool pfr = (f[6] & 1) != 0;
|
||||
std::uint64_t pf = 0;
|
||||
for (int i = 0; i < 8; ++i)
|
||||
pf |= static_cast<std::uint64_t>(f[24 + i]) << (8 * i);
|
||||
std::string why;
|
||||
if (pr != slot_bytes_.size())
|
||||
why += " rounds " + std::to_string(pr) + " vs "
|
||||
+ std::to_string(slot_bytes_.size()) + ";";
|
||||
else if (pf != slots_fingerprint(slot_bytes_))
|
||||
why += " slot widths differ;";
|
||||
if (pl != map_.n_lanes)
|
||||
why += " lanes " + std::to_string(pl) + " vs "
|
||||
+ std::to_string(map_.n_lanes) + ";";
|
||||
if (pc != count_)
|
||||
why += " instances " + std::to_string(pc) + " vs "
|
||||
+ std::to_string(count_) + ";";
|
||||
if (pfr != map_.framed)
|
||||
why += std::string(" framing ") + (pfr ? "on" : "off") + " vs "
|
||||
+ (map_.framed ? "on" : "off") + ";";
|
||||
if (!why.empty())
|
||||
throw std::runtime_error(
|
||||
"stream_array_sink: peer sink disagrees (peer vs this side):" + why);
|
||||
std::vector<std::uint8_t> rest(4 * pr);
|
||||
if (!rest.empty())
|
||||
streams_->read(0, rest.data(), rest.size());
|
||||
}
|
||||
|
||||
void pull_one_frame(std::size_t stream_i)
|
||||
{
|
||||
std::uint8_t raw[round_lane_hdr::size];
|
||||
streams_->read(stream_i, raw, round_lane_hdr::size);
|
||||
const auto hdr = round_lane_hdr::unpack(raw);
|
||||
if (hdr.round >= slot_bytes_.size() || map_.lane(hdr.round) != stream_i)
|
||||
throw std::runtime_error("stream_array_sink: frame for round "
|
||||
+ std::to_string(hdr.round) + " on lane "
|
||||
+ std::to_string(stream_i));
|
||||
const std::size_t sb = slot_bytes_[hdr.round];
|
||||
if (hdr.nbytes == 0)
|
||||
{
|
||||
if (sb == 0)
|
||||
zero_seen_[hdr.round] = true;
|
||||
return;
|
||||
}
|
||||
if (sb == 0 || hdr.nbytes % sb != 0
|
||||
|| in_filled_[hdr.round] + hdr.nbytes > count_ * sb)
|
||||
throw std::runtime_error("stream_array_sink: round "
|
||||
+ std::to_string(hdr.round) + " frame of "
|
||||
+ std::to_string(hdr.nbytes) + " bytes does not fit");
|
||||
streams_->read(stream_i, in_[hdr.round].data() + in_filled_[hdr.round],
|
||||
hdr.nbytes);
|
||||
in_filled_[hdr.round] += hdr.nbytes;
|
||||
}
|
||||
|
||||
round_window & window(std::uint16_t round)
|
||||
{
|
||||
if (round >= windows_.size())
|
||||
throw std::out_of_range("stream_array_sink round");
|
||||
return windows_[round];
|
||||
}
|
||||
|
||||
const round_window & window(std::uint16_t round) const
|
||||
{
|
||||
if (round >= windows_.size())
|
||||
throw std::out_of_range("stream_array_sink round");
|
||||
return windows_[round];
|
||||
}
|
||||
|
||||
stream_array * streams_ = nullptr;
|
||||
std::size_t count_ = 1;
|
||||
std::size_t legacy_index_ = 0;
|
||||
bool use_legacy_index_ = false;
|
||||
std::vector<std::size_t> slot_bytes_;
|
||||
round_lane_map map_;
|
||||
mutable std::vector<round_window> windows_;
|
||||
std::vector<std::vector<std::uint8_t>> in_;
|
||||
std::vector<std::size_t> in_filled_;
|
||||
std::vector<bool> zero_seen_;
|
||||
std::vector<bool> announced_;
|
||||
bool hello_ = false;
|
||||
bool hello_read_ = false;
|
||||
};
|
||||
|
||||
/// @brief Map index `i` to SCTP stream `i` on one association.
|
||||
/// @details Synchronous stub: this blocking `stream_array` face has no linked OS
|
||||
/// backend, so its I/O throws. Protocol code should depend on
|
||||
/// `stream_array` only; swap in a linked backend without API changes.
|
||||
/// The real, event-driven SCTP backend already exists on Linux as
|
||||
/// `dpf::net::async_sctp_stream_array` (see
|
||||
/// `net/async_sctp_stream_array.hpp`, guarded by `DPF_HAS_LIBSCTP`).
|
||||
/// It implements `dpf::net::async_stream_array`, still maps index `i`
|
||||
/// to SCTP stream `i`, and drops in behind
|
||||
/// `dpf::async::overlapped_byte_protocol` with no protocol changes.
|
||||
/// Link `-lsctp`. Windows has no supported SCTP path (SctpDrv is
|
||||
/// unstable), so the async class throws there.
|
||||
struct sctp_association
|
||||
{
|
||||
int fd = -1;
|
||||
};
|
||||
|
||||
class sctp_stream_array : public stream_array
|
||||
{
|
||||
public:
|
||||
/// @brief Reserved for a linked backend (`libsctp` association fd).
|
||||
explicit sctp_stream_array(sctp_association /*assoc*/)
|
||||
{
|
||||
throw std::logic_error("sctp_stream_array: not linked");
|
||||
}
|
||||
|
||||
/// @brief Size-only stub: every I/O op throws until a backend is linked.
|
||||
explicit sctp_stream_array(std::size_t streams) : n_(streams)
|
||||
{
|
||||
if (streams == 0)
|
||||
throw std::invalid_argument("sctp_stream_array empty");
|
||||
}
|
||||
|
||||
std::size_t size() const noexcept override { return n_; }
|
||||
|
||||
void write(std::size_t, const void *, std::size_t) override
|
||||
{
|
||||
throw std::logic_error(
|
||||
"sctp_stream_array: write — SCTP backend not linked (index i -> SCTP stream i)");
|
||||
}
|
||||
|
||||
void read(std::size_t, void *, std::size_t) override
|
||||
{
|
||||
throw std::logic_error(
|
||||
"sctp_stream_array: read — SCTP backend not linked (index i -> SCTP stream i)");
|
||||
}
|
||||
|
||||
void flush(std::size_t) override
|
||||
{
|
||||
throw std::logic_error(
|
||||
"sctp_stream_array: flush — SCTP backend not linked (index i -> SCTP stream i)");
|
||||
}
|
||||
|
||||
private:
|
||||
std::size_t n_ = 0;
|
||||
};
|
||||
|
||||
} // namespace net
|
||||
} // namespace dpf
|
||||
|
||||
#endif
|
||||
70
include/dpf/net/stream_edge_mesh.hpp
Normal file
70
include/dpf/net/stream_edge_mesh.hpp
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
/// @file dpf/net/stream_edge_mesh.hpp
|
||||
/// @brief Multi-edge `stream_array_sink` trio (peer / rss_next / dealer).
|
||||
#ifndef LIBDPF_INCLUDE_DPF_NET_STREAM_EDGE_MESH_HPP__
|
||||
#define LIBDPF_INCLUDE_DPF_NET_STREAM_EDGE_MESH_HPP__
|
||||
|
||||
#include <cstddef>
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "dpf/net/edge_mesh.hpp"
|
||||
#include "dpf/net/stream_array.hpp"
|
||||
#include "dpf/net/stream_mesh.hpp"
|
||||
|
||||
namespace dpf
|
||||
{
|
||||
namespace net
|
||||
{
|
||||
|
||||
/// @brief Owns up to three multi-round sinks bound to stream arrays.
|
||||
struct stream_edge_sinks
|
||||
{
|
||||
std::unique_ptr<stream_array_sink> peer;
|
||||
std::unique_ptr<stream_array_sink> rss_next;
|
||||
std::unique_ptr<stream_array_sink> dealer;
|
||||
|
||||
edge_mesh mesh() const
|
||||
{
|
||||
edge_mesh m;
|
||||
m.sinks = {peer.get(), rss_next.get(), dealer.get()};
|
||||
return m;
|
||||
}
|
||||
};
|
||||
|
||||
/// @brief Build sinks from optional stream arrays and per-channel slot widths.
|
||||
/// @details Null arrays or empty slot lists leave that edge unbound.
|
||||
inline stream_edge_sinks make_stream_edge_sinks(stream_array * peer,
|
||||
std::vector<std::size_t> peer_slots, stream_array * rss,
|
||||
std::vector<std::size_t> rss_slots, stream_array * dealer,
|
||||
std::vector<std::size_t> dealer_slots, std::size_t lanes = 1)
|
||||
{
|
||||
stream_edge_sinks out;
|
||||
if (peer != nullptr && !peer_slots.empty())
|
||||
out.peer = std::make_unique<stream_array_sink>(*peer,
|
||||
std::move(peer_slots), lanes);
|
||||
if (rss != nullptr && !rss_slots.empty())
|
||||
out.rss_next = std::make_unique<stream_array_sink>(*rss,
|
||||
std::move(rss_slots), lanes);
|
||||
if (dealer != nullptr && !dealer_slots.empty())
|
||||
out.dealer = std::make_unique<stream_array_sink>(*dealer,
|
||||
std::move(dealer_slots), lanes);
|
||||
return out;
|
||||
}
|
||||
|
||||
/// @brief RSS ring edge `(me → (me+1)%3)` on a 3-party stream clique.
|
||||
inline stream_array & rss_next_ring_edge(memory_stream_clique & clique,
|
||||
unsigned me)
|
||||
{
|
||||
if (clique.parties != 3)
|
||||
throw std::invalid_argument("rss_next_ring_edge: need 3 parties");
|
||||
if (me > 2)
|
||||
throw std::invalid_argument("rss_next_ring_edge party");
|
||||
const unsigned next = static_cast<unsigned>((me + 1) % 3);
|
||||
return clique.end(me, next);
|
||||
}
|
||||
|
||||
} // namespace net
|
||||
} // namespace dpf
|
||||
|
||||
#endif
|
||||
123
include/dpf/net/stream_mesh.hpp
Normal file
123
include/dpf/net/stream_mesh.hpp
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
/// @file dpf/net/stream_mesh.hpp
|
||||
/// @brief Fully connected clique of `stream_array` duplex links.
|
||||
#ifndef LIBDPF_INCLUDE_DPF_NET_STREAM_MESH_HPP__
|
||||
#define LIBDPF_INCLUDE_DPF_NET_STREAM_MESH_HPP__
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "hedley/hedley.h"
|
||||
|
||||
#include "dpf/net/stream_array.hpp"
|
||||
|
||||
namespace dpf
|
||||
{
|
||||
namespace net
|
||||
{
|
||||
|
||||
/// @brief Pairwise memory stream arrays for `n` parties (one hub per edge).
|
||||
struct memory_stream_clique
|
||||
{
|
||||
std::size_t parties = 0;
|
||||
std::size_t streams_per_edge = 0;
|
||||
std::vector<std::shared_ptr<memory_stream_hub>> hubs;
|
||||
std::vector<std::pair<memory_stream_array, memory_stream_array>> ends;
|
||||
|
||||
static std::size_t pair_edge(std::size_t a, std::size_t b, std::size_t n)
|
||||
{
|
||||
if (a == b || a >= n || b >= n)
|
||||
throw std::invalid_argument("memory_stream_clique pair");
|
||||
if (a > b)
|
||||
std::swap(a, b);
|
||||
std::size_t e = 0;
|
||||
for (std::size_t i = 0; i < a; ++i)
|
||||
e += n - 1 - i;
|
||||
e += b - a - 1;
|
||||
return e;
|
||||
}
|
||||
|
||||
/// @brief Duplex link from `from` toward `to` (same hub as `to`/`from`).
|
||||
memory_stream_array & end(std::size_t from, std::size_t to)
|
||||
{
|
||||
const auto e = pair_edge(from, to, parties);
|
||||
if (from < to)
|
||||
return ends[e].first;
|
||||
return ends[e].second;
|
||||
}
|
||||
|
||||
const memory_stream_array & end(std::size_t from, std::size_t to) const
|
||||
{
|
||||
const auto e = pair_edge(from, to, parties);
|
||||
if (from < to)
|
||||
return ends[e].first;
|
||||
return ends[e].second;
|
||||
}
|
||||
|
||||
/// @brief Sum additive shares: send `mine` to every peer, add incoming.
|
||||
/// @details Works for any `parties >= 2` (not only 3-party rings).
|
||||
void declassify(unsigned me, const std::uint8_t * mine, std::size_t n,
|
||||
std::uint8_t * sum_out)
|
||||
{
|
||||
if (parties < 2 || me >= parties)
|
||||
throw std::invalid_argument("memory_stream_clique declassify party");
|
||||
for (unsigned peer = 0; peer < parties; ++peer)
|
||||
{
|
||||
if (peer == me)
|
||||
continue;
|
||||
end(me, peer).write(0, mine, n);
|
||||
end(me, peer).flush(0);
|
||||
}
|
||||
std::memcpy(sum_out, mine, n);
|
||||
for (unsigned peer = 0; peer < parties; ++peer)
|
||||
{
|
||||
if (peer == me)
|
||||
continue;
|
||||
std::vector<std::uint8_t> got(n);
|
||||
end(me, peer).read(0, got.data(), n);
|
||||
for (std::size_t i = 0; i < n; i += 8)
|
||||
{
|
||||
std::uint64_t a = 0, b = 0;
|
||||
const std::size_t k = std::min<std::size_t>(8, n - i);
|
||||
std::memcpy(&a, sum_out + i, k);
|
||||
std::memcpy(&b, got.data() + i, k);
|
||||
a += b;
|
||||
std::memcpy(sum_out + i, &a, k);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
HEDLEY_WARN_UNUSED_RESULT
|
||||
inline memory_stream_clique make_memory_stream_clique(std::size_t n,
|
||||
std::size_t streams_per_edge)
|
||||
{
|
||||
if (n < 2)
|
||||
throw std::invalid_argument("make_memory_stream_clique needs >= 2");
|
||||
if (streams_per_edge == 0)
|
||||
throw std::invalid_argument("make_memory_stream_clique streams");
|
||||
memory_stream_clique c;
|
||||
c.parties = n;
|
||||
c.streams_per_edge = streams_per_edge;
|
||||
const std::size_t n_edges = n * (n - 1) / 2;
|
||||
c.hubs.reserve(n_edges);
|
||||
c.ends.reserve(n_edges);
|
||||
for (std::size_t e = 0; e < n_edges; ++e)
|
||||
{
|
||||
auto hub = std::make_shared<memory_stream_hub>(streams_per_edge);
|
||||
c.hubs.push_back(hub);
|
||||
c.ends.emplace_back(memory_stream_array(hub, true),
|
||||
memory_stream_array(hub, false));
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
} // namespace net
|
||||
} // namespace dpf
|
||||
|
||||
#endif
|
||||
235
include/dpf/net/stream_sink.hpp
Normal file
235
include/dpf/net/stream_sink.hpp
Normal file
|
|
@ -0,0 +1,235 @@
|
|||
/// @file dpf/net/stream_sink.hpp
|
||||
/// @brief RoundSink with one duplex channel per interactive round.
|
||||
#ifndef LIBDPF_INCLUDE_DPF_NET_STREAM_SINK_HPP__
|
||||
#define LIBDPF_INCLUDE_DPF_NET_STREAM_SINK_HPP__
|
||||
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <unistd.h>
|
||||
|
||||
#include "dpf/net/asio_ns.hpp"
|
||||
|
||||
#include "dpf/net/channel.hpp"
|
||||
#include "dpf/net/round_sink.hpp"
|
||||
#include "dpf/net/trio.hpp"
|
||||
|
||||
namespace dpf
|
||||
{
|
||||
namespace net
|
||||
{
|
||||
|
||||
/// @brief Prefix-flush RoundSink with an independent stream per round.
|
||||
class stream_sink : public RoundSink
|
||||
{
|
||||
public:
|
||||
stream_sink(std::vector<channel> links, unsigned self_id, unsigned peer_id,
|
||||
std::size_t count, std::vector<std::size_t> slot_bytes)
|
||||
: links_(std::move(links)),
|
||||
self_id_(self_id),
|
||||
peer_id_(peer_id),
|
||||
count_(count),
|
||||
slot_bytes_(std::move(slot_bytes))
|
||||
{
|
||||
if (links_.size() != slot_bytes_.size())
|
||||
throw std::invalid_argument("stream_sink link count");
|
||||
windows_.reserve(slot_bytes_.size());
|
||||
for (std::size_t sb : slot_bytes_)
|
||||
windows_.emplace_back(count_, sb);
|
||||
}
|
||||
|
||||
std::size_t count() const noexcept override { return count_; }
|
||||
std::size_t rounds() const noexcept override { return slot_bytes_.size(); }
|
||||
|
||||
std::size_t slot_bytes(std::uint16_t round) const override
|
||||
{
|
||||
if (round >= slot_bytes_.size())
|
||||
throw std::out_of_range("stream_sink round");
|
||||
return slot_bytes_[round];
|
||||
}
|
||||
|
||||
void submit(std::uint16_t round, std::size_t index,
|
||||
const std::uint8_t * bytes, std::size_t n) override
|
||||
{
|
||||
window(round).submit(index, bytes, n);
|
||||
}
|
||||
|
||||
bool peer_ready(std::uint16_t round, std::size_t index) const override
|
||||
{
|
||||
return window(round).peer_ready(index);
|
||||
}
|
||||
|
||||
void read_peer(std::uint16_t round, std::size_t index, std::uint8_t * out,
|
||||
std::size_t n) const override
|
||||
{
|
||||
window(round).read_peer(index, out, n);
|
||||
}
|
||||
|
||||
void flush() override
|
||||
{
|
||||
// Global barrier: every round channel must rendezvous. Parties can have
|
||||
// pending on different rounds (schedule_session holes / staggered
|
||||
// indices); skipping idle links deadlocks the peer that still waits
|
||||
// there. Lockstep callers should use flush_round to avoid O(rounds)
|
||||
// exchanges on large sinks.
|
||||
for (std::uint16_t r = 0; r < slot_bytes_.size(); ++r)
|
||||
flush_one(r);
|
||||
}
|
||||
|
||||
void flush_round(std::uint16_t r) override
|
||||
{
|
||||
if (r >= slot_bytes_.size())
|
||||
throw std::out_of_range("stream_sink flush_round");
|
||||
flush_one(r);
|
||||
}
|
||||
|
||||
void poll() override {}
|
||||
|
||||
std::uint64_t exchanges() const noexcept { return link_exchanges_; }
|
||||
|
||||
private:
|
||||
round_window & window(std::uint16_t round)
|
||||
{
|
||||
if (round >= windows_.size())
|
||||
throw std::out_of_range("stream_sink round");
|
||||
return windows_[round];
|
||||
}
|
||||
|
||||
const round_window & window(std::uint16_t round) const
|
||||
{
|
||||
if (round >= windows_.size())
|
||||
throw std::out_of_range("stream_sink round");
|
||||
return windows_[round];
|
||||
}
|
||||
|
||||
void flush_one(std::uint16_t r)
|
||||
{
|
||||
std::size_t begin = 0;
|
||||
std::size_t nslots = 0;
|
||||
const std::uint8_t * pending = windows_[r].pending_out(begin, nslots);
|
||||
std::vector<std::uint8_t> payload;
|
||||
if (nslots != 0)
|
||||
{
|
||||
payload.resize(sizeof(std::uint32_t) * 2 + nslots * slot_bytes_[r]);
|
||||
const std::uint32_t b = static_cast<std::uint32_t>(begin);
|
||||
const std::uint32_t c = static_cast<std::uint32_t>(nslots);
|
||||
std::memcpy(payload.data(), &b, 4);
|
||||
std::memcpy(payload.data() + 4, &c, 4);
|
||||
if (nslots * slot_bytes_[r] != 0)
|
||||
std::memcpy(payload.data() + 8, pending,
|
||||
nslots * slot_bytes_[r]);
|
||||
windows_[r].mark_flushed(nslots);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Peer may still be delivering; exchange an empty prefix header.
|
||||
payload.resize(8, 0);
|
||||
}
|
||||
std::vector<std::uint8_t> theirs;
|
||||
if (self_id_ < peer_id_)
|
||||
{
|
||||
links_[r].send_bytes(msg::round_batch, payload);
|
||||
theirs = links_[r].recv_bytes(msg::round_batch);
|
||||
}
|
||||
else
|
||||
{
|
||||
theirs = links_[r].recv_bytes(msg::round_batch);
|
||||
links_[r].send_bytes(msg::round_batch, payload);
|
||||
}
|
||||
if (theirs.size() < 8)
|
||||
throw std::runtime_error("stream_sink short frame");
|
||||
std::uint32_t pb = 0;
|
||||
std::uint32_t pc = 0;
|
||||
std::memcpy(&pb, theirs.data(), 4);
|
||||
std::memcpy(&pc, theirs.data() + 4, 4);
|
||||
const std::size_t body = static_cast<std::size_t>(pc) * slot_bytes_[r];
|
||||
if (theirs.size() != 8 + body)
|
||||
throw std::runtime_error("stream_sink size mismatch");
|
||||
if (pc != 0)
|
||||
windows_[r].accept_peer_at(pb, theirs.data() + 8, pc);
|
||||
++link_exchanges_;
|
||||
}
|
||||
|
||||
std::vector<channel> links_;
|
||||
unsigned self_id_ = 0;
|
||||
unsigned peer_id_ = 0;
|
||||
std::size_t count_ = 0;
|
||||
std::vector<std::size_t> slot_bytes_;
|
||||
std::vector<round_window> windows_;
|
||||
std::uint64_t link_exchanges_ = 0;
|
||||
};
|
||||
|
||||
inline std::string stream_link_path(const std::string & dir, role a, role b,
|
||||
std::uint16_t round)
|
||||
{
|
||||
if (to_u(a) > to_u(b))
|
||||
std::swap(a, b);
|
||||
return dir + "/" + role_name(a) + "-" + role_name(b) + "-r"
|
||||
+ std::to_string(round);
|
||||
}
|
||||
|
||||
/// @brief Connect one unix-domain stream per round under `dir`.
|
||||
inline stream_sink connect_stream_sink(role self, role peer,
|
||||
const std::string & dir, std::size_t count,
|
||||
std::vector<std::size_t> slot_bytes, unsigned retries = 200)
|
||||
{
|
||||
if (self == role::p2 || peer == role::p2)
|
||||
throw std::invalid_argument("stream_sink is for computing parties");
|
||||
asio::io_context io;
|
||||
std::vector<channel> links;
|
||||
links.reserve(slot_bytes.size());
|
||||
using proto = asio::local::stream_protocol;
|
||||
for (std::uint16_t r = 0; r < slot_bytes.size(); ++r)
|
||||
{
|
||||
const auto path = stream_link_path(dir, self, peer, r);
|
||||
const bool accept = to_u(self) < to_u(peer);
|
||||
bool opened = false;
|
||||
for (unsigned i = 0; i < retries && !opened; ++i)
|
||||
{
|
||||
asio::error_code ec;
|
||||
if (accept)
|
||||
{
|
||||
::unlink(path.c_str());
|
||||
proto::endpoint ep(path);
|
||||
proto::acceptor acc(io, ep);
|
||||
proto::socket sock(io);
|
||||
acc.accept(sock, ec);
|
||||
if (!ec)
|
||||
{
|
||||
links.emplace_back(std::move(sock));
|
||||
opened = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
proto::endpoint ep(path);
|
||||
proto::socket sock(io);
|
||||
sock.connect(ep, ec);
|
||||
if (!ec)
|
||||
{
|
||||
links.emplace_back(std::move(sock));
|
||||
opened = true;
|
||||
}
|
||||
}
|
||||
if (!opened)
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
}
|
||||
if (!opened)
|
||||
throw std::runtime_error("stream_sink connect failed: " + path);
|
||||
}
|
||||
return stream_sink(std::move(links), to_u(self), to_u(peer), count,
|
||||
std::move(slot_bytes));
|
||||
}
|
||||
|
||||
} // namespace net
|
||||
} // namespace dpf
|
||||
|
||||
#endif // LIBDPF_INCLUDE_DPF_NET_STREAM_SINK_HPP__
|
||||
111
include/dpf/net/sync_stream_array.hpp
Normal file
111
include/dpf/net/sync_stream_array.hpp
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
/// @file dpf/net/sync_stream_array.hpp
|
||||
/// @brief Blocking `stream_array` face over an `async_stream_array`.
|
||||
/// @details Setup (TLS, windows, coalescing, reconnect) is the async
|
||||
/// framework's job. This type only turns each `write` / `read` /
|
||||
/// `flush` into one async call and pumps that stream's `io_context`
|
||||
/// until it finishes, so older sync call sites share the same
|
||||
/// encryption and wire policy as `party_session`.
|
||||
#ifndef LIBDPF_INCLUDE_DPF_NET_SYNC_STREAM_ARRAY_HPP__
|
||||
#define LIBDPF_INCLUDE_DPF_NET_SYNC_STREAM_ARRAY_HPP__
|
||||
|
||||
#include <atomic>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <system_error>
|
||||
#include <vector>
|
||||
|
||||
#include "dpf/net/async_stream_array.hpp"
|
||||
#include "dpf/net/buffer_pool.hpp"
|
||||
#include "dpf/net/stream_array.hpp"
|
||||
|
||||
namespace dpf
|
||||
{
|
||||
namespace net
|
||||
{
|
||||
|
||||
/// @brief Synchronous view of an `async_stream_array` on one `io_context`.
|
||||
class sync_stream_array final : public stream_array
|
||||
{
|
||||
public:
|
||||
/// @brief Non-owning: `streams` and its context must outlive this object.
|
||||
explicit sync_stream_array(async_stream_array & streams)
|
||||
: streams_(&streams), io_(&streams.context())
|
||||
{
|
||||
if (streams_->size() == 0)
|
||||
throw std::invalid_argument("sync_stream_array empty");
|
||||
}
|
||||
|
||||
std::size_t size() const noexcept override { return streams_->size(); }
|
||||
|
||||
void write(std::size_t i, const void * src, std::size_t n) override
|
||||
{
|
||||
check(i);
|
||||
if (n == 0)
|
||||
return;
|
||||
auto buf = copy_buffer(src, n);
|
||||
wait_op([&](auto done) {
|
||||
streams_->async_write_owned(i, std::move(buf), std::move(done));
|
||||
});
|
||||
}
|
||||
|
||||
void read(std::size_t i, void * dst, std::size_t n) override
|
||||
{
|
||||
check(i);
|
||||
if (n == 0)
|
||||
return;
|
||||
wait_op([&](auto done) {
|
||||
streams_->async_read(i, dst, n, std::move(done));
|
||||
});
|
||||
}
|
||||
|
||||
/// @brief No-op: each `write` already waits for the bytes to leave.
|
||||
void flush(std::size_t) override {}
|
||||
|
||||
async_stream_array & async() noexcept { return *streams_; }
|
||||
const async_stream_array & async() const noexcept { return *streams_; }
|
||||
|
||||
private:
|
||||
void check(std::size_t i) const
|
||||
{
|
||||
if (i >= streams_->size())
|
||||
throw std::out_of_range("sync_stream_array index "
|
||||
+ std::to_string(i));
|
||||
}
|
||||
|
||||
template <typename Start>
|
||||
void wait_op(Start start)
|
||||
{
|
||||
std::atomic<bool> done{false};
|
||||
std::error_code ec;
|
||||
start([&](const std::error_code & e) {
|
||||
ec = e;
|
||||
done.store(true, std::memory_order_release);
|
||||
});
|
||||
while (!done.load(std::memory_order_acquire))
|
||||
{
|
||||
if (io_->stopped())
|
||||
io_->restart();
|
||||
io_->run_one();
|
||||
}
|
||||
if (ec)
|
||||
throw std::system_error(ec, "sync_stream_array");
|
||||
}
|
||||
|
||||
async_stream_array * streams_ = nullptr;
|
||||
asio::io_context * io_ = nullptr;
|
||||
};
|
||||
|
||||
/// @brief Same face as the old fd-based mux: N lanes on one connection.
|
||||
/// @details Built from an already-established framework link (typically a
|
||||
/// `party_session` peer). Prefer this over constructing a mux from a
|
||||
/// raw `channel` socket.
|
||||
using mux_stream_array = sync_stream_array;
|
||||
|
||||
} // namespace net
|
||||
} // namespace dpf
|
||||
|
||||
#endif // LIBDPF_INCLUDE_DPF_NET_SYNC_STREAM_ARRAY_HPP__
|
||||
140
include/dpf/net/tcp_mesh.hpp
Normal file
140
include/dpf/net/tcp_mesh.hpp
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
/// @file dpf/net/tcp_mesh.hpp
|
||||
/// @brief N-party TCP clique bootstrap via `party_session`.
|
||||
/// @details Thin wrappers around `party_session::join` so sync and async mesh
|
||||
/// helpers share TLS, wire policy, deadlines, and reconnect with the
|
||||
/// rest of the framework. Prefer constructing a `party_session`
|
||||
/// directly in new code.
|
||||
#ifndef LIBDPF_INCLUDE_DPF_NET_TCP_MESH_HPP__
|
||||
#define LIBDPF_INCLUDE_DPF_NET_TCP_MESH_HPP__
|
||||
|
||||
#include <cstddef>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "dpf/net/asio_ns.hpp"
|
||||
|
||||
#include "dpf/net/mesh_rendezvous.hpp"
|
||||
#include "dpf/net/party_session.hpp"
|
||||
#include "dpf/net/policy.hpp"
|
||||
#include "dpf/net/security.hpp"
|
||||
#include "dpf/net/sync_stream_array.hpp"
|
||||
|
||||
namespace dpf
|
||||
{
|
||||
namespace net
|
||||
{
|
||||
|
||||
/// @brief One party's view of a TCP mux clique backed by `party_session`.
|
||||
struct async_tcp_mesh
|
||||
{
|
||||
unsigned me = 0;
|
||||
unsigned parties = 0;
|
||||
std::unique_ptr<party_session> session;
|
||||
|
||||
async_stream_array & peer(unsigned p)
|
||||
{
|
||||
if (!session)
|
||||
throw std::logic_error("async_tcp_mesh: not joined");
|
||||
return session->peer(p);
|
||||
}
|
||||
|
||||
bool has(unsigned p) const
|
||||
{
|
||||
return session && p < parties && p != me;
|
||||
}
|
||||
|
||||
party_session & sess()
|
||||
{
|
||||
if (!session)
|
||||
throw std::logic_error("async_tcp_mesh: not joined");
|
||||
return *session;
|
||||
}
|
||||
};
|
||||
|
||||
/// @brief Sync mux clique view (same topology as `async_tcp_mesh`).
|
||||
/// @details Owns the `io_context` and a sync facade per peer edge.
|
||||
struct tcp_mesh
|
||||
{
|
||||
unsigned me = 0;
|
||||
unsigned parties = 0;
|
||||
std::shared_ptr<asio::io_context> io;
|
||||
std::unique_ptr<party_session> session;
|
||||
std::vector<std::unique_ptr<mux_stream_array>> links;
|
||||
|
||||
mux_stream_array & peer(unsigned p)
|
||||
{
|
||||
if (p == me || p >= parties || !links[p])
|
||||
throw std::invalid_argument("tcp_mesh: bad peer");
|
||||
return *links[p];
|
||||
}
|
||||
|
||||
party_session & sess()
|
||||
{
|
||||
if (!session)
|
||||
throw std::logic_error("tcp_mesh: not joined");
|
||||
return *session;
|
||||
}
|
||||
};
|
||||
|
||||
inline session_options mesh_session_options(std::size_t nstreams,
|
||||
const wire_policy & pol = {}, const deadlines & lim = {},
|
||||
const peer_security & sec = {})
|
||||
{
|
||||
session_options opt;
|
||||
opt.n_lanes = nstreams;
|
||||
opt.kind = transport::mux;
|
||||
opt.policy = pol;
|
||||
opt.limits = lim;
|
||||
opt.security = sec;
|
||||
return opt;
|
||||
}
|
||||
|
||||
/// @brief Party `me` joins an N-party async mux clique on `host`.
|
||||
inline async_tcp_mesh join_async_tcp_mesh(asio::io_context & io, unsigned me,
|
||||
unsigned n, const std::string & host, mesh_ports & ports,
|
||||
std::size_t nstreams, const wire_policy & pol = {},
|
||||
const deadlines & lim = {}, const peer_security & sec = {})
|
||||
{
|
||||
if (nstreams == 0)
|
||||
throw std::invalid_argument("join_async_tcp_mesh: nstreams");
|
||||
async_tcp_mesh mesh;
|
||||
mesh.me = me;
|
||||
mesh.parties = n;
|
||||
mesh.session = std::make_unique<party_session>(io, me, n,
|
||||
mesh_session_options(nstreams, pol, lim, sec));
|
||||
mesh.session->join(host, ports, nstreams);
|
||||
return mesh;
|
||||
}
|
||||
|
||||
/// @brief Sync variant: each edge is a `mux_stream_array` over the session.
|
||||
inline tcp_mesh join_tcp_mesh(unsigned me, unsigned n, const std::string & host,
|
||||
mesh_ports & ports, std::size_t nstreams, const wire_policy & pol = {},
|
||||
const deadlines & lim = {}, const peer_security & sec = {})
|
||||
{
|
||||
if (nstreams == 0)
|
||||
throw std::invalid_argument("join_tcp_mesh: nstreams");
|
||||
tcp_mesh mesh;
|
||||
mesh.me = me;
|
||||
mesh.parties = n;
|
||||
mesh.io = std::make_shared<asio::io_context>();
|
||||
mesh.session = std::make_unique<party_session>(*mesh.io, me, n,
|
||||
mesh_session_options(nstreams, pol, lim, sec));
|
||||
mesh.session->join(host, ports, nstreams);
|
||||
mesh.links.resize(n);
|
||||
for (unsigned peer = 0; peer < n; ++peer)
|
||||
{
|
||||
if (peer == me)
|
||||
continue;
|
||||
mesh.links[peer] =
|
||||
std::make_unique<mux_stream_array>(mesh.session->peer(peer));
|
||||
}
|
||||
return mesh;
|
||||
}
|
||||
|
||||
} // namespace net
|
||||
} // namespace dpf
|
||||
|
||||
#endif // LIBDPF_INCLUDE_DPF_NET_TCP_MESH_HPP__
|
||||
345
include/dpf/net/tls.hpp
Normal file
345
include/dpf/net/tls.hpp
Normal file
|
|
@ -0,0 +1,345 @@
|
|||
/// @file dpf/net/tls.hpp
|
||||
/// @brief TLS 1.3 for party links and client links (OpenSSL through asio).
|
||||
/// @details Only TLS 1.3 is offered, without session tickets or resumption.
|
||||
/// The TLS layer accepts any certificate; the decision is made right
|
||||
/// after the handshake, before any application byte is read:
|
||||
/// `check_peer` compares a party's key with the one this side holds
|
||||
/// for that party (none held means unauthenticated in that
|
||||
/// direction), and `check_server` applies a client's pins, CA chain
|
||||
/// and host name, or the development-certificate default. Every
|
||||
/// blocking step here pumps the socket's `io_context` until it
|
||||
/// completes or its budget runs out, then closes the socket.
|
||||
#ifndef LIBDPF_INCLUDE_DPF_NET_TLS_HPP__
|
||||
#define LIBDPF_INCLUDE_DPF_NET_TLS_HPP__
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <system_error>
|
||||
|
||||
#include "dpf/net/asio_ns.hpp"
|
||||
#include "dpf/net/connect.hpp"
|
||||
#include "dpf/net/identity.hpp"
|
||||
#include "dpf/net/security.hpp"
|
||||
|
||||
#if DPF_HAS_OPENSSL
|
||||
#include <asio/ssl.hpp>
|
||||
#include <openssl/ssl.h>
|
||||
#include <openssl/x509v3.h>
|
||||
#endif
|
||||
|
||||
namespace dpf
|
||||
{
|
||||
namespace net
|
||||
{
|
||||
|
||||
#if DPF_HAS_OPENSSL
|
||||
|
||||
using tls_stream = asio::ssl::stream<asio::ip::tcp::socket>;
|
||||
using tls_local_stream = asio::ssl::stream<asio::local::stream_protocol::socket>;
|
||||
using tls_context = asio::ssl::context;
|
||||
|
||||
namespace detail
|
||||
{
|
||||
|
||||
inline int tls_accept_any(int, X509_STORE_CTX *) { return 1; }
|
||||
|
||||
inline void tls_common(SSL_CTX * c)
|
||||
{
|
||||
if (SSL_CTX_set_min_proto_version(c, TLS1_3_VERSION) != 1
|
||||
|| SSL_CTX_set_max_proto_version(c, TLS1_3_VERSION) != 1
|
||||
|| SSL_CTX_set_ciphersuites(c,
|
||||
"TLS_AES_128_GCM_SHA256:TLS_CHACHA20_POLY1305_SHA256:"
|
||||
"TLS_AES_256_GCM_SHA384")
|
||||
!= 1)
|
||||
throw std::runtime_error(openssl_error("tls: context"));
|
||||
SSL_CTX_set_num_tickets(c, 0);
|
||||
SSL_CTX_set_session_cache_mode(c, SSL_SESS_CACHE_OFF);
|
||||
SSL_CTX_set_options(c, SSL_OP_NO_TICKET);
|
||||
}
|
||||
|
||||
inline void use_identity(SSL_CTX * c, const identity & id)
|
||||
{
|
||||
if (SSL_CTX_use_certificate(c, id.cert()) != 1
|
||||
|| SSL_CTX_use_PrivateKey(c, id.pkey()) != 1
|
||||
|| SSL_CTX_check_private_key(c) != 1)
|
||||
throw std::runtime_error(openssl_error("tls: identity"));
|
||||
}
|
||||
|
||||
/// @brief Run one async step on `s`, pumping `io`, for at most `budget`.
|
||||
template <typename Stream, typename Start>
|
||||
inline std::error_code tls_step(asio::io_context & io, Stream & s,
|
||||
std::chrono::milliseconds budget, Start start)
|
||||
{
|
||||
bool done = false;
|
||||
std::error_code result;
|
||||
start([&done, &result](const std::error_code & ec) {
|
||||
result = ec;
|
||||
done = true;
|
||||
});
|
||||
const auto deadline = setup_clock::now() + budget;
|
||||
bool expired = false;
|
||||
while (!done)
|
||||
{
|
||||
if (!expired && setup_clock::now() >= deadline)
|
||||
{
|
||||
expired = true;
|
||||
std::error_code e;
|
||||
s.lowest_layer().cancel(e);
|
||||
s.lowest_layer().close(e);
|
||||
}
|
||||
if (io.stopped())
|
||||
io.restart();
|
||||
io.run_one_for(std::chrono::milliseconds(10));
|
||||
}
|
||||
// Running out of work stops `io`; leave it ready for the caller's run().
|
||||
if (io.stopped())
|
||||
io.restart();
|
||||
if (expired && result)
|
||||
return std::make_error_code(std::errc::timed_out);
|
||||
return result;
|
||||
}
|
||||
|
||||
inline std::string tls_failure(const std::string & what, const std::error_code & ec,
|
||||
std::chrono::milliseconds budget)
|
||||
{
|
||||
if (ec == std::errc::timed_out)
|
||||
return what + ": no answer within " + std::to_string(budget.count()) + " ms";
|
||||
return what + ": " + ec.message();
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
/// @brief Party links: both ends present `self` and request the peer's key.
|
||||
inline std::shared_ptr<tls_context> make_peer_tls_context(const identity & self)
|
||||
{
|
||||
auto ctx = std::make_shared<tls_context>(tls_context::tls);
|
||||
SSL_CTX * c = ctx->native_handle();
|
||||
detail::tls_common(c);
|
||||
detail::use_identity(c, self);
|
||||
SSL_CTX_set_verify(c, SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT,
|
||||
detail::tls_accept_any);
|
||||
return ctx;
|
||||
}
|
||||
|
||||
/// @brief Server side of client links. `development` reports whether the
|
||||
/// development certificate is presented.
|
||||
inline std::shared_ptr<tls_context> make_server_tls_context(const server_security & sec,
|
||||
bool & development)
|
||||
{
|
||||
auto ctx = std::make_shared<tls_context>(tls_context::tls);
|
||||
SSL_CTX * c = ctx->native_handle();
|
||||
detail::tls_common(c);
|
||||
development = false;
|
||||
if (!sec.cert_file.empty())
|
||||
{
|
||||
const std::string key = sec.key_file.empty() ? sec.cert_file : sec.key_file;
|
||||
if (SSL_CTX_use_certificate_chain_file(c, sec.cert_file.c_str()) != 1
|
||||
|| SSL_CTX_use_PrivateKey_file(c, key.c_str(), SSL_FILETYPE_PEM) != 1
|
||||
|| SSL_CTX_check_private_key(c) != 1)
|
||||
throw std::runtime_error(detail::openssl_error(
|
||||
("tls: server certificate '" + sec.cert_file + "'").c_str()));
|
||||
}
|
||||
else if (sec.self)
|
||||
detail::use_identity(c, *sec.self);
|
||||
else
|
||||
{
|
||||
detail::use_identity(c, identity::development());
|
||||
development = true;
|
||||
}
|
||||
// Client keys are optional; `check_client` reads whichever arrived.
|
||||
SSL_CTX_set_verify(c, SSL_VERIFY_PEER, detail::tls_accept_any);
|
||||
return ctx;
|
||||
}
|
||||
|
||||
/// @brief Client side of client links. Verification happens in `check_server`.
|
||||
inline std::shared_ptr<tls_context> make_client_tls_context(const client_security & sec)
|
||||
{
|
||||
auto ctx = std::make_shared<tls_context>(tls_context::tls);
|
||||
SSL_CTX * c = ctx->native_handle();
|
||||
detail::tls_common(c);
|
||||
if (sec.self)
|
||||
detail::use_identity(c, *sec.self);
|
||||
if (sec.ca_file == "system")
|
||||
{
|
||||
if (SSL_CTX_set_default_verify_paths(c) != 1)
|
||||
throw std::runtime_error(detail::openssl_error("tls: system trust store"));
|
||||
}
|
||||
else if (!sec.ca_file.empty()
|
||||
&& SSL_CTX_load_verify_locations(c, sec.ca_file.c_str(), nullptr) != 1)
|
||||
throw std::runtime_error(detail::openssl_error(
|
||||
("tls: CA file '" + sec.ca_file + "'").c_str()));
|
||||
SSL_CTX_set_verify(c, SSL_VERIFY_PEER, detail::tls_accept_any);
|
||||
return ctx;
|
||||
}
|
||||
|
||||
/// @brief Set the name a CA-issued server certificate must carry (and SNI).
|
||||
template <typename Stream>
|
||||
inline void tls_expect_host(Stream & s, const std::string & name)
|
||||
{
|
||||
if (name.empty())
|
||||
return;
|
||||
SSL * ssl = s.native_handle();
|
||||
SSL_set_tlsext_host_name(ssl, name.c_str());
|
||||
if (SSL_set1_host(ssl, name.c_str()) != 1)
|
||||
throw std::runtime_error(detail::openssl_error("tls: server name"));
|
||||
}
|
||||
|
||||
template <typename Stream>
|
||||
inline void tls_handshake(asio::io_context & io, Stream & s, bool server,
|
||||
std::chrono::milliseconds budget, const std::string & what)
|
||||
{
|
||||
const auto ec = detail::tls_step(io, s, budget, [&](auto h) {
|
||||
s.async_handshake(server ? asio::ssl::stream_base::server
|
||||
: asio::ssl::stream_base::client,
|
||||
h);
|
||||
});
|
||||
if (ec)
|
||||
throw std::system_error(ec, detail::tls_failure(what + ": TLS handshake", ec,
|
||||
budget));
|
||||
}
|
||||
|
||||
template <typename Stream>
|
||||
inline void tls_write(asio::io_context & io, Stream & s, const void * p,
|
||||
std::size_t n, std::chrono::milliseconds budget, const std::string & what)
|
||||
{
|
||||
const auto ec = detail::tls_step(io, s, budget, [&](auto h) {
|
||||
asio::async_write(s, asio::buffer(p, n),
|
||||
[h](const std::error_code & e, std::size_t) { h(e); });
|
||||
});
|
||||
if (ec)
|
||||
throw std::system_error(ec, detail::tls_failure(what, ec, budget));
|
||||
}
|
||||
|
||||
template <typename Stream>
|
||||
inline void tls_read(asio::io_context & io, Stream & s, void * p, std::size_t n,
|
||||
std::chrono::milliseconds budget, const std::string & what)
|
||||
{
|
||||
const auto ec = detail::tls_step(io, s, budget, [&](auto h) {
|
||||
asio::async_read(s, asio::buffer(p, n),
|
||||
[h](const std::error_code & e, std::size_t) { h(e); });
|
||||
});
|
||||
if (ec)
|
||||
throw std::system_error(ec, detail::tls_failure(what, ec, budget));
|
||||
}
|
||||
|
||||
/// @brief The Ed25519 key in the peer's certificate, if it sent one.
|
||||
template <typename Stream>
|
||||
inline std::optional<public_key> tls_peer_key(Stream & s)
|
||||
{
|
||||
#if OPENSSL_VERSION_NUMBER >= 0x30000000L
|
||||
X509 * x = SSL_get1_peer_certificate(s.native_handle());
|
||||
#else
|
||||
X509 * x = SSL_get_peer_certificate(s.native_handle());
|
||||
#endif
|
||||
if (x == nullptr)
|
||||
return std::nullopt;
|
||||
std::optional<public_key> out;
|
||||
EVP_PKEY * k = X509_get0_pubkey(x);
|
||||
public_key pk;
|
||||
std::size_t n = public_key::size;
|
||||
if (k != nullptr && EVP_PKEY_id(k) == EVP_PKEY_ED25519
|
||||
&& EVP_PKEY_get_raw_public_key(k, pk.bytes.data(), &n) == 1
|
||||
&& n == public_key::size)
|
||||
out = pk;
|
||||
X509_free(x);
|
||||
return out;
|
||||
}
|
||||
|
||||
/// @brief Protocol, cipher, and peer key of an established stream.
|
||||
template <typename Stream>
|
||||
inline link_security tls_describe(Stream & s)
|
||||
{
|
||||
link_security out;
|
||||
out.encrypted = true;
|
||||
SSL * ssl = s.native_handle();
|
||||
out.protocol = SSL_get_version(ssl);
|
||||
const char * cipher = SSL_get_cipher_name(ssl);
|
||||
out.cipher = cipher != nullptr ? cipher : "unknown";
|
||||
out.peer_key = tls_peer_key(s);
|
||||
return out;
|
||||
}
|
||||
|
||||
/// @brief Party link: authenticate `who` if this side holds its key.
|
||||
/// @details Throws when a held key does not match. Otherwise fills
|
||||
/// `sec.peer_auth` with `key` or `none`.
|
||||
inline void check_peer(link_security & sec, const peer_security & policy,
|
||||
std::uint32_t party, const std::string & who)
|
||||
{
|
||||
const public_key * want = policy.trusted_key(party);
|
||||
if (want == nullptr)
|
||||
{
|
||||
sec.peer_auth = "none";
|
||||
return;
|
||||
}
|
||||
if (!sec.peer_key || *sec.peer_key != *want)
|
||||
throw std::runtime_error("security: " + who + " presented key "
|
||||
+ (sec.peer_key ? sec.peer_key->base64() : std::string("(not Ed25519)"))
|
||||
+ " but this side holds " + want->base64() + " for it");
|
||||
sec.peer_auth = "key";
|
||||
}
|
||||
|
||||
/// @brief Client link: apply `policy` to the server behind `s`.
|
||||
/// @details Accepts a pinned key, then a CA chain that matched the expected
|
||||
/// name, then (only when neither is configured) the development
|
||||
/// certificate. `verify` off accepts anything.
|
||||
inline void check_server(link_security & sec, tls_stream & s,
|
||||
const client_security & policy, const std::string & where)
|
||||
{
|
||||
if (!policy.verify)
|
||||
{
|
||||
sec.peer_auth = "none";
|
||||
return;
|
||||
}
|
||||
if (sec.peer_key
|
||||
&& std::find(policy.pins.begin(), policy.pins.end(), *sec.peer_key)
|
||||
!= policy.pins.end())
|
||||
{
|
||||
sec.peer_auth = "key";
|
||||
return;
|
||||
}
|
||||
if (!policy.ca_file.empty() && SSL_get_verify_result(s.native_handle()) == X509_V_OK)
|
||||
{
|
||||
sec.peer_auth = "ca";
|
||||
return;
|
||||
}
|
||||
if (!policy.configured() && sec.peer_key
|
||||
&& *sec.peer_key == identity::development().key())
|
||||
{
|
||||
sec.peer_auth = "development";
|
||||
return;
|
||||
}
|
||||
std::string why;
|
||||
if (!policy.ca_file.empty())
|
||||
why = std::string(" (CA check: ")
|
||||
+ X509_verify_cert_error_string(SSL_get_verify_result(s.native_handle()))
|
||||
+ ")";
|
||||
throw std::runtime_error("security: server " + where + " presented "
|
||||
+ (sec.peer_key ? "key " + sec.peer_key->base64()
|
||||
: std::string("a certificate without an Ed25519 key"))
|
||||
+ ", which this client does not trust" + why
|
||||
+ "; pin it (client_pin=...), configure client_ca, or set client_verify=off");
|
||||
}
|
||||
|
||||
/// @brief Server side: authenticate the client if it presented a pinned key.
|
||||
inline void check_client(link_security & sec, const server_security & policy)
|
||||
{
|
||||
sec.peer_auth = sec.peer_key
|
||||
&& std::find(policy.client_pins.begin(), policy.client_pins.end(),
|
||||
*sec.peer_key)
|
||||
!= policy.client_pins.end()
|
||||
? "key"
|
||||
: "none";
|
||||
}
|
||||
|
||||
#endif // DPF_HAS_OPENSSL
|
||||
|
||||
} // namespace net
|
||||
} // namespace dpf
|
||||
|
||||
#endif // LIBDPF_INCLUDE_DPF_NET_TLS_HPP__
|
||||
563
include/dpf/net/trio.hpp
Normal file
563
include/dpf/net/trio.hpp
Normal file
|
|
@ -0,0 +1,563 @@
|
|||
/// @file dpf/net/trio.hpp
|
||||
/// @brief Dial/accept three-party (2+1) socket mesh for libdpf protocols.
|
||||
/// @details Socket paths live under a driver-created directory:
|
||||
/// `p0-p1`, `p0-p2`, `p1-p2`. The lower role accepts; the higher
|
||||
/// dials. Helpers `deal` / `accept_deal` / `open_with` name the
|
||||
/// protocol steps without exposing ASIO.
|
||||
/// @copyright Copyright (c) 2019-2026 Ryan Henry and [others](@ref authors)
|
||||
/// @license Released under a GNU General Public v2.0 (GPLv2) license;
|
||||
/// see [LICENSE.md](@ref license) for details.
|
||||
|
||||
#ifndef LIBDPF_INCLUDE_DPF_NET_TRIO_HPP__
|
||||
#define LIBDPF_INCLUDE_DPF_NET_TRIO_HPP__
|
||||
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <unistd.h>
|
||||
|
||||
#include "dpf/net/asio_ns.hpp"
|
||||
|
||||
#include "dpf/net/channel.hpp"
|
||||
#include "dpf/net/comm_hook.hpp"
|
||||
#include "dpf/net/policy.hpp"
|
||||
#include "dpf/net/round_sink.hpp"
|
||||
#include "dpf/net/secure_channel.hpp"
|
||||
#include "dpf/net/security.hpp"
|
||||
#include "dpf/net/socket_tune.hpp"
|
||||
|
||||
#include "hedley/hedley.h"
|
||||
|
||||
namespace dpf
|
||||
{
|
||||
namespace net
|
||||
{
|
||||
|
||||
/// @brief Party roles in a (2+1) run. p2 is the dealer.
|
||||
enum class role : unsigned
|
||||
{
|
||||
p0 = 0,
|
||||
p1 = 1,
|
||||
p2 = 2,
|
||||
};
|
||||
|
||||
HEDLEY_CONST
|
||||
HEDLEY_NO_THROW
|
||||
inline constexpr unsigned to_u(role r) noexcept
|
||||
{
|
||||
return static_cast<unsigned>(r);
|
||||
}
|
||||
|
||||
HEDLEY_CONST
|
||||
HEDLEY_NO_THROW
|
||||
inline const char * role_name(role r) noexcept
|
||||
{
|
||||
switch (r)
|
||||
{
|
||||
case role::p0: return "p0";
|
||||
case role::p1: return "p1";
|
||||
case role::p2: return "p2";
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
|
||||
inline std::string link_path(const std::string & dir, role a, role b)
|
||||
{
|
||||
if (to_u(a) > to_u(b))
|
||||
std::swap(a, b);
|
||||
return dir + "/" + role_name(a) + "-" + role_name(b);
|
||||
}
|
||||
|
||||
/// @brief Connected view of the other two parties.
|
||||
class trio
|
||||
{
|
||||
public:
|
||||
trio() = default;
|
||||
|
||||
/// @brief Connect local unix-domain sockets under `dir`.
|
||||
/// @param self this process's role
|
||||
/// @param dir directory holding the three socket paths
|
||||
/// @param retries dial/accept attempts before giving up
|
||||
/// @param sec same peer TLS policy as `party_session` (default encrypt)
|
||||
static trio connect_local(role self, const std::string & dir,
|
||||
unsigned retries = 200, const peer_security & sec = {},
|
||||
const deadlines & lim = {})
|
||||
{
|
||||
trio t;
|
||||
t.self_ = self;
|
||||
t.io_ = std::make_unique<asio::io_context>();
|
||||
for (unsigned other = 0; other < 3; ++other)
|
||||
{
|
||||
if (other == to_u(self))
|
||||
continue;
|
||||
auto peer = static_cast<role>(other);
|
||||
t.link_[other] = connect_one(self, peer, dir, *t.io_, retries, sec,
|
||||
lim);
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
/// @brief Connect the p0–p1 socket only. The dealer link stays closed
|
||||
/// until `install_inbox`.
|
||||
static trio connect_pair(role self, const std::string & dir,
|
||||
unsigned retries = 200, const peer_security & sec = {},
|
||||
const deadlines & lim = {})
|
||||
{
|
||||
if (self == role::p2)
|
||||
throw std::invalid_argument("connect_pair is p0 and p1");
|
||||
trio t;
|
||||
t.self_ = self;
|
||||
t.io_ = std::make_unique<asio::io_context>();
|
||||
const role peer = self == role::p0 ? role::p1 : role::p0;
|
||||
t.link_[to_u(peer)] = connect_one(self, peer, dir, *t.io_, retries, sec,
|
||||
lim);
|
||||
return t;
|
||||
}
|
||||
|
||||
/// @brief Serve later `to(p2).recv` calls from a local frame buffer.
|
||||
void install_inbox(std::vector<std::uint8_t> frames)
|
||||
{
|
||||
if (self_ == role::p2)
|
||||
throw std::logic_error("p2 has no dealer inbox");
|
||||
link_[to_u(role::p2)] = channel::from_inbox(std::move(frames));
|
||||
}
|
||||
|
||||
/// @brief True when the installed dealer tape has been fully consumed.
|
||||
HEDLEY_NO_THROW
|
||||
bool dealer_inbox_done() const noexcept
|
||||
{
|
||||
if (self_ == role::p2)
|
||||
return false;
|
||||
return link_[to_u(role::p2)].inbox_done();
|
||||
}
|
||||
|
||||
/// @brief Connect over TCP loopback. Ports are `base + 10*lo + hi`.
|
||||
static trio connect_tcp(role self, std::uint16_t base_port,
|
||||
const std::string & host = "127.0.0.1", unsigned retries = 200,
|
||||
const peer_security & sec = {}, const deadlines & lim = {},
|
||||
const socket_options & so = {})
|
||||
{
|
||||
trio t;
|
||||
t.self_ = self;
|
||||
t.io_ = std::make_unique<asio::io_context>();
|
||||
for (unsigned other = 0; other < 3; ++other)
|
||||
{
|
||||
if (other == to_u(self))
|
||||
continue;
|
||||
auto peer = static_cast<role>(other);
|
||||
t.link_[other] = connect_one_tcp(self, peer, base_port, host,
|
||||
*t.io_, retries, sec, lim, so);
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
role self() const noexcept { return self_; }
|
||||
|
||||
/// @brief Sum of byte and frame counters on the open links.
|
||||
HEDLEY_NO_THROW
|
||||
io_tally tally() const noexcept
|
||||
{
|
||||
io_tally sum;
|
||||
for (const auto & link : link_)
|
||||
{
|
||||
if (link.open())
|
||||
sum += link.tally();
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
/// @brief Counters on the link to `peer` only.
|
||||
HEDLEY_NO_THROW
|
||||
io_tally tally_to(role peer) const noexcept
|
||||
{
|
||||
if (peer == self_ || !link_[to_u(peer)].open())
|
||||
return {};
|
||||
return link_[to_u(peer)].tally();
|
||||
}
|
||||
|
||||
/// @brief Bytes received from the dealer (p2). Empty for the dealer itself.
|
||||
HEDLEY_NO_THROW
|
||||
io_tally tally_from_p2() const noexcept
|
||||
{
|
||||
if (self_ == role::p2)
|
||||
return {};
|
||||
return tally_to(role::p2);
|
||||
}
|
||||
|
||||
/// @brief Bytes with the other computing party (p0↔p1).
|
||||
/// @details For p2 this is the sum of the links to p0 and p1.
|
||||
HEDLEY_NO_THROW
|
||||
io_tally tally_from_peer() const noexcept
|
||||
{
|
||||
if (self_ == role::p0)
|
||||
return tally_to(role::p1);
|
||||
if (self_ == role::p1)
|
||||
return tally_to(role::p0);
|
||||
io_tally sum;
|
||||
sum += tally_to(role::p0);
|
||||
sum += tally_to(role::p1);
|
||||
return sum;
|
||||
}
|
||||
|
||||
/// @brief Zero counters on every open link.
|
||||
HEDLEY_NO_THROW
|
||||
void reset_tally() noexcept
|
||||
{
|
||||
for (auto & link : link_)
|
||||
{
|
||||
if (link.open())
|
||||
link.reset_tally();
|
||||
}
|
||||
}
|
||||
|
||||
/// @brief The open channel to `peer` (raw bypass; does not use the hook).
|
||||
/// @param peer the other party
|
||||
/// @return that channel
|
||||
/// @throws std::invalid_argument if `peer` is this process
|
||||
/// @throws std::logic_error if that link was not connected
|
||||
channel & to(role peer)
|
||||
{
|
||||
if (peer == self_)
|
||||
throw std::invalid_argument("trio::to(self)");
|
||||
auto & c = link_[to_u(peer)];
|
||||
if (!c.open())
|
||||
throw std::logic_error("trio link is not open");
|
||||
return c;
|
||||
}
|
||||
|
||||
/// @brief Install a replaceable transport for helpers and `batch`.
|
||||
/// @details Non-owning. Null restores the framed mesh defaults.
|
||||
void set_hook(comm_hook * hook) noexcept { hook_ = hook; }
|
||||
|
||||
/// @brief Current hook, or null when helpers use the mesh directly.
|
||||
HEDLEY_NO_THROW
|
||||
comm_hook * hook() const noexcept { return hook_; }
|
||||
|
||||
/// @brief One-way send through the hook (or the mesh when unset).
|
||||
template <typename T>
|
||||
void send_to(role peer, msg tag, const T & value)
|
||||
{
|
||||
static_assert(std::is_trivially_copyable_v<T>,
|
||||
"trio::send_to requires a trivially copyable type");
|
||||
if (hook_)
|
||||
{
|
||||
hook_->send_bytes(*this, to_u(peer), tag, &value, sizeof(T));
|
||||
return;
|
||||
}
|
||||
to(peer).send(tag, value);
|
||||
}
|
||||
|
||||
/// @brief Homogeneous vector send through the hook (or the mesh).
|
||||
template <typename T>
|
||||
void send_vec_to(role peer, msg tag, const std::vector<T> & values)
|
||||
{
|
||||
static_assert(std::is_trivially_copyable_v<T>,
|
||||
"trio::send_vec_to requires a trivially copyable type");
|
||||
if (hook_)
|
||||
{
|
||||
const std::size_t nbytes = values.size() * sizeof(T);
|
||||
hook_->send_bytes(*this, to_u(peer), tag, values.data(), nbytes);
|
||||
return;
|
||||
}
|
||||
to(peer).send_vec(values, tag);
|
||||
}
|
||||
|
||||
/// @brief Untyped one-way send through the hook (or the mesh).
|
||||
void send_bytes_to(role peer, msg tag, const void * data, std::size_t n)
|
||||
{
|
||||
if (hook_)
|
||||
{
|
||||
hook_->send_bytes(*this, to_u(peer), tag, data, n);
|
||||
return;
|
||||
}
|
||||
to(peer).send_bytes(tag, data, n);
|
||||
}
|
||||
|
||||
/// @brief Untyped one-way receive through the hook (or the mesh).
|
||||
HEDLEY_WARN_UNUSED_RESULT
|
||||
std::vector<std::uint8_t> recv_bytes_from(role peer, msg tag)
|
||||
{
|
||||
if (hook_)
|
||||
return hook_->recv_bytes(*this, to_u(peer), tag);
|
||||
return to(peer).recv_bytes(tag);
|
||||
}
|
||||
|
||||
/// @brief One-way receive through the hook (or the mesh when unset).
|
||||
template <typename T>
|
||||
HEDLEY_WARN_UNUSED_RESULT
|
||||
T recv_from(role peer, msg tag)
|
||||
{
|
||||
static_assert(std::is_trivially_copyable_v<T>,
|
||||
"trio::recv_from requires a trivially copyable type");
|
||||
if (hook_)
|
||||
{
|
||||
auto bytes = hook_->recv_bytes(*this, to_u(peer), tag);
|
||||
if (bytes.size() != sizeof(T))
|
||||
throw std::runtime_error("trio::recv_from size mismatch");
|
||||
T value{};
|
||||
if constexpr (sizeof(T) != 0)
|
||||
std::memcpy(&value, bytes.data(), sizeof(T));
|
||||
return value;
|
||||
}
|
||||
return to(peer).template recv<T>(tag);
|
||||
}
|
||||
|
||||
/// @brief Homogeneous vector receive through the hook (or the mesh).
|
||||
template <typename T>
|
||||
HEDLEY_WARN_UNUSED_RESULT
|
||||
std::vector<T> recv_vec_from(role peer, msg tag = msg::beaver_tape)
|
||||
{
|
||||
static_assert(std::is_trivially_copyable_v<T>,
|
||||
"trio::recv_vec_from requires a trivially copyable type");
|
||||
if (hook_)
|
||||
{
|
||||
auto bytes = hook_->recv_bytes(*this, to_u(peer), tag);
|
||||
if constexpr (sizeof(T) == 0)
|
||||
{
|
||||
if (!bytes.empty())
|
||||
throw std::runtime_error("trio::recv_vec_from size");
|
||||
return {};
|
||||
}
|
||||
if (bytes.size() % sizeof(T) != 0)
|
||||
throw std::runtime_error("trio::recv_vec_from size mismatch");
|
||||
std::vector<T> out(bytes.size() / sizeof(T));
|
||||
if (!out.empty())
|
||||
std::memcpy(out.data(), bytes.data(), bytes.size());
|
||||
return out;
|
||||
}
|
||||
return to(peer).template recv_vec<T>(tag);
|
||||
}
|
||||
|
||||
/// @brief Round-batched peer sink. Default is a mux on the p0–p1 link.
|
||||
/// @details Defined next to `mux_sink` so the default factory can build one.
|
||||
std::unique_ptr<RoundSink> batch(std::size_t count,
|
||||
std::vector<std::size_t> slot_bytes);
|
||||
|
||||
/// @brief Dealer sends one side of a split to each computing party.
|
||||
/// @tparam Split a type with `p0` and `p1` members
|
||||
/// @param s the split
|
||||
/// @throws std::logic_error if this process is not p2
|
||||
template <typename Split>
|
||||
void deal(const Split & s)
|
||||
{
|
||||
if (self_ != role::p2)
|
||||
throw std::logic_error("only the dealer may deal");
|
||||
send_to(role::p0, msg::beaver_tape, s.p0);
|
||||
send_to(role::p1, msg::beaver_tape, s.p1);
|
||||
}
|
||||
|
||||
/// @brief Computing party receives its share of a dealt value.
|
||||
/// @tparam Ring the dealt type
|
||||
/// @return this party's share
|
||||
/// @throws std::logic_error if this process is p2
|
||||
template <typename Ring>
|
||||
HEDLEY_WARN_UNUSED_RESULT
|
||||
Ring accept_deal()
|
||||
{
|
||||
if (self_ == role::p2)
|
||||
throw std::logic_error("dealer does not accept_deal");
|
||||
return recv_from<Ring>(role::p2, msg::beaver_tape);
|
||||
}
|
||||
|
||||
/// @brief Open an additive share with the peer computing party.
|
||||
/// @tparam Ring additive ring
|
||||
/// @param peer the other computing party
|
||||
/// @param mine this party's share
|
||||
/// @return reconstructed public value (`mine + peer`)
|
||||
/// @throws std::logic_error if either side is p2
|
||||
template <typename Ring>
|
||||
HEDLEY_WARN_UNUSED_RESULT
|
||||
Ring open_with(role peer, const Ring & mine)
|
||||
{
|
||||
if (self_ == role::p2 || peer == role::p2)
|
||||
throw std::logic_error("open_with is between p0 and p1");
|
||||
Ring theirs = exchange_with(peer, mine, msg::delta);
|
||||
return static_cast<Ring>(mine + theirs);
|
||||
}
|
||||
|
||||
/// @brief Exchange raw values (no algebra) with a peer.
|
||||
/// @tparam T trivially copyable payload
|
||||
/// @param peer the other party
|
||||
/// @param mine this party's value
|
||||
/// @param tag the message tag
|
||||
/// @return the peer's value
|
||||
template <typename T>
|
||||
HEDLEY_WARN_UNUSED_RESULT
|
||||
T exchange_with(role peer, const T & mine, msg tag = msg::delta)
|
||||
{
|
||||
static_assert(std::is_trivially_copyable_v<T>,
|
||||
"trio::exchange_with requires a trivially copyable type");
|
||||
if (hook_)
|
||||
{
|
||||
auto bytes = hook_->exchange_bytes(*this, to_u(peer), tag, &mine,
|
||||
sizeof(T));
|
||||
if (bytes.size() != sizeof(T))
|
||||
throw std::runtime_error("trio::exchange_with size mismatch");
|
||||
T theirs{};
|
||||
if constexpr (sizeof(T) != 0)
|
||||
std::memcpy(&theirs, bytes.data(), sizeof(T));
|
||||
return theirs;
|
||||
}
|
||||
return to(peer).template exchange<T>(to_u(self_), to_u(peer), mine, tag);
|
||||
}
|
||||
|
||||
/// @brief Exchange a homogeneous vector with a peer (one barrier).
|
||||
/// @tparam T trivially copyable element
|
||||
/// @param peer the other party
|
||||
/// @param mine this party's values
|
||||
/// @param tag the message tag
|
||||
/// @return the peer's values
|
||||
template <typename T>
|
||||
HEDLEY_WARN_UNUSED_RESULT
|
||||
std::vector<T> exchange_vec_with(role peer, const std::vector<T> & mine,
|
||||
msg tag = msg::ring_vector)
|
||||
{
|
||||
static_assert(std::is_trivially_copyable_v<T>,
|
||||
"trio::exchange_vec_with requires a trivially copyable type");
|
||||
if (mine.empty())
|
||||
return {};
|
||||
if (hook_)
|
||||
{
|
||||
const std::size_t nbytes = mine.size() * sizeof(T);
|
||||
auto bytes = hook_->exchange_vec_bytes(*this, to_u(peer), tag,
|
||||
mine.data(), nbytes);
|
||||
if (bytes.size() != nbytes)
|
||||
throw std::runtime_error("trio::exchange_vec_with size mismatch");
|
||||
std::vector<T> theirs(mine.size());
|
||||
std::memcpy(theirs.data(), bytes.data(), nbytes);
|
||||
return theirs;
|
||||
}
|
||||
return to(peer).template exchange_vec<T>(
|
||||
to_u(self_), to_u(peer), mine, tag);
|
||||
}
|
||||
|
||||
/// @brief Open additive shares packed into one vector exchange.
|
||||
/// @tparam Ring additive ring
|
||||
/// @param peer the other computing party
|
||||
/// @param mine this party's shares
|
||||
/// @return reconstructed public values (`mine[i] + peer[i]`)
|
||||
template <typename Ring>
|
||||
HEDLEY_WARN_UNUSED_RESULT
|
||||
std::vector<Ring> open_vec_with(role peer, const std::vector<Ring> & mine)
|
||||
{
|
||||
if (self_ == role::p2 || peer == role::p2)
|
||||
throw std::logic_error("open_vec_with is between p0 and p1");
|
||||
auto theirs = exchange_vec_with(peer, mine);
|
||||
std::vector<Ring> out(mine.size());
|
||||
for (std::size_t i = 0; i < mine.size(); ++i)
|
||||
out[i] = static_cast<Ring>(mine[i] + theirs[i]);
|
||||
return out;
|
||||
}
|
||||
|
||||
private:
|
||||
role self_ = role::p0;
|
||||
std::unique_ptr<asio::io_context> io_;
|
||||
channel link_[3];
|
||||
comm_hook * hook_ = nullptr;
|
||||
|
||||
static channel connect_one(role self, role peer, const std::string & dir,
|
||||
asio::io_context & io, unsigned retries, const peer_security & sec,
|
||||
const deadlines & lim)
|
||||
{
|
||||
using proto = asio::local::stream_protocol;
|
||||
const bool accept = to_u(self) < to_u(peer);
|
||||
const auto path = link_path(dir, self, peer);
|
||||
const std::string who = role_name(peer);
|
||||
if (accept)
|
||||
{
|
||||
::unlink(path.c_str());
|
||||
proto::endpoint ep(path);
|
||||
proto::acceptor acc(io, ep);
|
||||
for (unsigned i = 0; i < retries; ++i)
|
||||
{
|
||||
asio::error_code ec;
|
||||
proto::socket sock(io);
|
||||
acc.accept(sock, ec);
|
||||
if (!ec)
|
||||
return secure_local_channel(io, std::move(sock), true,
|
||||
to_u(peer), sec, lim.handshake, who, "accept");
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
}
|
||||
throw std::runtime_error("trio accept failed: " + path);
|
||||
}
|
||||
proto::endpoint ep(path);
|
||||
for (unsigned i = 0; i < retries; ++i)
|
||||
{
|
||||
asio::error_code ec;
|
||||
proto::socket sock(io);
|
||||
sock.connect(ep, ec);
|
||||
if (!ec)
|
||||
return secure_local_channel(io, std::move(sock), false,
|
||||
to_u(peer), sec, lim.handshake, who, "connect");
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
}
|
||||
throw std::runtime_error("trio connect failed: " + path);
|
||||
}
|
||||
|
||||
static std::uint16_t tcp_port(std::uint16_t base, role a, role b)
|
||||
{
|
||||
if (to_u(a) > to_u(b))
|
||||
std::swap(a, b);
|
||||
return static_cast<std::uint16_t>(base + 10u * to_u(a) + to_u(b));
|
||||
}
|
||||
|
||||
static channel connect_one_tcp(role self, role peer, std::uint16_t base,
|
||||
const std::string & host, asio::io_context & io, unsigned retries,
|
||||
const peer_security & sec, const deadlines & lim,
|
||||
const socket_options & so)
|
||||
{
|
||||
using tcp = asio::ip::tcp;
|
||||
const bool accept = to_u(self) < to_u(peer);
|
||||
const auto port = tcp_port(base, self, peer);
|
||||
const std::string who = role_name(peer);
|
||||
if (accept)
|
||||
{
|
||||
tcp::endpoint ep(tcp::v4(), port);
|
||||
tcp::acceptor acc(io);
|
||||
asio::error_code ec;
|
||||
acc.open(ep.protocol(), ec);
|
||||
acc.set_option(tcp::acceptor::reuse_address(true), ec);
|
||||
acc.bind(ep, ec);
|
||||
if (ec)
|
||||
throw std::runtime_error("trio tcp bind: " + ec.message());
|
||||
acc.listen(1, ec);
|
||||
for (unsigned i = 0; i < retries; ++i)
|
||||
{
|
||||
tcp::socket sock(io);
|
||||
acc.accept(sock, ec);
|
||||
if (!ec)
|
||||
return secure_tcp_channel(io, std::move(sock), true,
|
||||
to_u(peer), sec, so, lim.handshake, who, "accept");
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
}
|
||||
throw std::runtime_error("trio tcp accept failed");
|
||||
}
|
||||
tcp::resolver resolver(io);
|
||||
auto endpoints = resolver.resolve(host, std::to_string(port));
|
||||
for (unsigned i = 0; i < retries; ++i)
|
||||
{
|
||||
asio::error_code ec;
|
||||
tcp::socket sock(io);
|
||||
asio::connect(sock, endpoints, ec);
|
||||
if (!ec)
|
||||
return secure_tcp_channel(io, std::move(sock), false,
|
||||
to_u(peer), sec, so, lim.handshake, who, "connect");
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
}
|
||||
throw std::runtime_error("trio tcp connect failed");
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace net
|
||||
} // namespace dpf
|
||||
|
||||
#endif // LIBDPF_INCLUDE_DPF_NET_TRIO_HPP__
|
||||
Loading…
Add table
Add a link
Reference in a new issue