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:
Ryan Henry 2026-09-28 05:59:19 -06:00
parent 695f8e84f7
commit 0d22946a0e
1835 changed files with 170291 additions and 2849 deletions

View 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__

View 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__

View 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__

File diff suppressed because it is too large Load diff

View 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
View 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__

View 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__

View 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
View 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__

View 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__

View 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__

View 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__

View 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

View 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__

View 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__

View 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__

View 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__

File diff suppressed because it is too large Load diff

View 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
View 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__

View 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__

View 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__

View 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__

View 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__

View 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__

View 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

View 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

View 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

View 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

View 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__

View 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__

View 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
View 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
View 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__