Checkpoint the party/runtime stack before share-program and malicious-mode work.
Ship the TLS mesh, composer, Beaver/Yao/leaf MPC, prep/online paths, apps, and docs so the tree is pushable before elevating share_expr, security_mode, and prep resume. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
695f8e84f7
commit
0d22946a0e
1835 changed files with 170291 additions and 2849 deletions
563
include/dpf/net/trio.hpp
Normal file
563
include/dpf/net/trio.hpp
Normal file
|
|
@ -0,0 +1,563 @@
|
|||
/// @file dpf/net/trio.hpp
|
||||
/// @brief Dial/accept three-party (2+1) socket mesh for libdpf protocols.
|
||||
/// @details Socket paths live under a driver-created directory:
|
||||
/// `p0-p1`, `p0-p2`, `p1-p2`. The lower role accepts; the higher
|
||||
/// dials. Helpers `deal` / `accept_deal` / `open_with` name the
|
||||
/// protocol steps without exposing ASIO.
|
||||
/// @copyright Copyright (c) 2019-2026 Ryan Henry and [others](@ref authors)
|
||||
/// @license Released under a GNU General Public v2.0 (GPLv2) license;
|
||||
/// see [LICENSE.md](@ref license) for details.
|
||||
|
||||
#ifndef LIBDPF_INCLUDE_DPF_NET_TRIO_HPP__
|
||||
#define LIBDPF_INCLUDE_DPF_NET_TRIO_HPP__
|
||||
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <unistd.h>
|
||||
|
||||
#include "dpf/net/asio_ns.hpp"
|
||||
|
||||
#include "dpf/net/channel.hpp"
|
||||
#include "dpf/net/comm_hook.hpp"
|
||||
#include "dpf/net/policy.hpp"
|
||||
#include "dpf/net/round_sink.hpp"
|
||||
#include "dpf/net/secure_channel.hpp"
|
||||
#include "dpf/net/security.hpp"
|
||||
#include "dpf/net/socket_tune.hpp"
|
||||
|
||||
#include "hedley/hedley.h"
|
||||
|
||||
namespace dpf
|
||||
{
|
||||
namespace net
|
||||
{
|
||||
|
||||
/// @brief Party roles in a (2+1) run. p2 is the dealer.
|
||||
enum class role : unsigned
|
||||
{
|
||||
p0 = 0,
|
||||
p1 = 1,
|
||||
p2 = 2,
|
||||
};
|
||||
|
||||
HEDLEY_CONST
|
||||
HEDLEY_NO_THROW
|
||||
inline constexpr unsigned to_u(role r) noexcept
|
||||
{
|
||||
return static_cast<unsigned>(r);
|
||||
}
|
||||
|
||||
HEDLEY_CONST
|
||||
HEDLEY_NO_THROW
|
||||
inline const char * role_name(role r) noexcept
|
||||
{
|
||||
switch (r)
|
||||
{
|
||||
case role::p0: return "p0";
|
||||
case role::p1: return "p1";
|
||||
case role::p2: return "p2";
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
|
||||
inline std::string link_path(const std::string & dir, role a, role b)
|
||||
{
|
||||
if (to_u(a) > to_u(b))
|
||||
std::swap(a, b);
|
||||
return dir + "/" + role_name(a) + "-" + role_name(b);
|
||||
}
|
||||
|
||||
/// @brief Connected view of the other two parties.
|
||||
class trio
|
||||
{
|
||||
public:
|
||||
trio() = default;
|
||||
|
||||
/// @brief Connect local unix-domain sockets under `dir`.
|
||||
/// @param self this process's role
|
||||
/// @param dir directory holding the three socket paths
|
||||
/// @param retries dial/accept attempts before giving up
|
||||
/// @param sec same peer TLS policy as `party_session` (default encrypt)
|
||||
static trio connect_local(role self, const std::string & dir,
|
||||
unsigned retries = 200, const peer_security & sec = {},
|
||||
const deadlines & lim = {})
|
||||
{
|
||||
trio t;
|
||||
t.self_ = self;
|
||||
t.io_ = std::make_unique<asio::io_context>();
|
||||
for (unsigned other = 0; other < 3; ++other)
|
||||
{
|
||||
if (other == to_u(self))
|
||||
continue;
|
||||
auto peer = static_cast<role>(other);
|
||||
t.link_[other] = connect_one(self, peer, dir, *t.io_, retries, sec,
|
||||
lim);
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
/// @brief Connect the p0–p1 socket only. The dealer link stays closed
|
||||
/// until `install_inbox`.
|
||||
static trio connect_pair(role self, const std::string & dir,
|
||||
unsigned retries = 200, const peer_security & sec = {},
|
||||
const deadlines & lim = {})
|
||||
{
|
||||
if (self == role::p2)
|
||||
throw std::invalid_argument("connect_pair is p0 and p1");
|
||||
trio t;
|
||||
t.self_ = self;
|
||||
t.io_ = std::make_unique<asio::io_context>();
|
||||
const role peer = self == role::p0 ? role::p1 : role::p0;
|
||||
t.link_[to_u(peer)] = connect_one(self, peer, dir, *t.io_, retries, sec,
|
||||
lim);
|
||||
return t;
|
||||
}
|
||||
|
||||
/// @brief Serve later `to(p2).recv` calls from a local frame buffer.
|
||||
void install_inbox(std::vector<std::uint8_t> frames)
|
||||
{
|
||||
if (self_ == role::p2)
|
||||
throw std::logic_error("p2 has no dealer inbox");
|
||||
link_[to_u(role::p2)] = channel::from_inbox(std::move(frames));
|
||||
}
|
||||
|
||||
/// @brief True when the installed dealer tape has been fully consumed.
|
||||
HEDLEY_NO_THROW
|
||||
bool dealer_inbox_done() const noexcept
|
||||
{
|
||||
if (self_ == role::p2)
|
||||
return false;
|
||||
return link_[to_u(role::p2)].inbox_done();
|
||||
}
|
||||
|
||||
/// @brief Connect over TCP loopback. Ports are `base + 10*lo + hi`.
|
||||
static trio connect_tcp(role self, std::uint16_t base_port,
|
||||
const std::string & host = "127.0.0.1", unsigned retries = 200,
|
||||
const peer_security & sec = {}, const deadlines & lim = {},
|
||||
const socket_options & so = {})
|
||||
{
|
||||
trio t;
|
||||
t.self_ = self;
|
||||
t.io_ = std::make_unique<asio::io_context>();
|
||||
for (unsigned other = 0; other < 3; ++other)
|
||||
{
|
||||
if (other == to_u(self))
|
||||
continue;
|
||||
auto peer = static_cast<role>(other);
|
||||
t.link_[other] = connect_one_tcp(self, peer, base_port, host,
|
||||
*t.io_, retries, sec, lim, so);
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
role self() const noexcept { return self_; }
|
||||
|
||||
/// @brief Sum of byte and frame counters on the open links.
|
||||
HEDLEY_NO_THROW
|
||||
io_tally tally() const noexcept
|
||||
{
|
||||
io_tally sum;
|
||||
for (const auto & link : link_)
|
||||
{
|
||||
if (link.open())
|
||||
sum += link.tally();
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
/// @brief Counters on the link to `peer` only.
|
||||
HEDLEY_NO_THROW
|
||||
io_tally tally_to(role peer) const noexcept
|
||||
{
|
||||
if (peer == self_ || !link_[to_u(peer)].open())
|
||||
return {};
|
||||
return link_[to_u(peer)].tally();
|
||||
}
|
||||
|
||||
/// @brief Bytes received from the dealer (p2). Empty for the dealer itself.
|
||||
HEDLEY_NO_THROW
|
||||
io_tally tally_from_p2() const noexcept
|
||||
{
|
||||
if (self_ == role::p2)
|
||||
return {};
|
||||
return tally_to(role::p2);
|
||||
}
|
||||
|
||||
/// @brief Bytes with the other computing party (p0↔p1).
|
||||
/// @details For p2 this is the sum of the links to p0 and p1.
|
||||
HEDLEY_NO_THROW
|
||||
io_tally tally_from_peer() const noexcept
|
||||
{
|
||||
if (self_ == role::p0)
|
||||
return tally_to(role::p1);
|
||||
if (self_ == role::p1)
|
||||
return tally_to(role::p0);
|
||||
io_tally sum;
|
||||
sum += tally_to(role::p0);
|
||||
sum += tally_to(role::p1);
|
||||
return sum;
|
||||
}
|
||||
|
||||
/// @brief Zero counters on every open link.
|
||||
HEDLEY_NO_THROW
|
||||
void reset_tally() noexcept
|
||||
{
|
||||
for (auto & link : link_)
|
||||
{
|
||||
if (link.open())
|
||||
link.reset_tally();
|
||||
}
|
||||
}
|
||||
|
||||
/// @brief The open channel to `peer` (raw bypass; does not use the hook).
|
||||
/// @param peer the other party
|
||||
/// @return that channel
|
||||
/// @throws std::invalid_argument if `peer` is this process
|
||||
/// @throws std::logic_error if that link was not connected
|
||||
channel & to(role peer)
|
||||
{
|
||||
if (peer == self_)
|
||||
throw std::invalid_argument("trio::to(self)");
|
||||
auto & c = link_[to_u(peer)];
|
||||
if (!c.open())
|
||||
throw std::logic_error("trio link is not open");
|
||||
return c;
|
||||
}
|
||||
|
||||
/// @brief Install a replaceable transport for helpers and `batch`.
|
||||
/// @details Non-owning. Null restores the framed mesh defaults.
|
||||
void set_hook(comm_hook * hook) noexcept { hook_ = hook; }
|
||||
|
||||
/// @brief Current hook, or null when helpers use the mesh directly.
|
||||
HEDLEY_NO_THROW
|
||||
comm_hook * hook() const noexcept { return hook_; }
|
||||
|
||||
/// @brief One-way send through the hook (or the mesh when unset).
|
||||
template <typename T>
|
||||
void send_to(role peer, msg tag, const T & value)
|
||||
{
|
||||
static_assert(std::is_trivially_copyable_v<T>,
|
||||
"trio::send_to requires a trivially copyable type");
|
||||
if (hook_)
|
||||
{
|
||||
hook_->send_bytes(*this, to_u(peer), tag, &value, sizeof(T));
|
||||
return;
|
||||
}
|
||||
to(peer).send(tag, value);
|
||||
}
|
||||
|
||||
/// @brief Homogeneous vector send through the hook (or the mesh).
|
||||
template <typename T>
|
||||
void send_vec_to(role peer, msg tag, const std::vector<T> & values)
|
||||
{
|
||||
static_assert(std::is_trivially_copyable_v<T>,
|
||||
"trio::send_vec_to requires a trivially copyable type");
|
||||
if (hook_)
|
||||
{
|
||||
const std::size_t nbytes = values.size() * sizeof(T);
|
||||
hook_->send_bytes(*this, to_u(peer), tag, values.data(), nbytes);
|
||||
return;
|
||||
}
|
||||
to(peer).send_vec(values, tag);
|
||||
}
|
||||
|
||||
/// @brief Untyped one-way send through the hook (or the mesh).
|
||||
void send_bytes_to(role peer, msg tag, const void * data, std::size_t n)
|
||||
{
|
||||
if (hook_)
|
||||
{
|
||||
hook_->send_bytes(*this, to_u(peer), tag, data, n);
|
||||
return;
|
||||
}
|
||||
to(peer).send_bytes(tag, data, n);
|
||||
}
|
||||
|
||||
/// @brief Untyped one-way receive through the hook (or the mesh).
|
||||
HEDLEY_WARN_UNUSED_RESULT
|
||||
std::vector<std::uint8_t> recv_bytes_from(role peer, msg tag)
|
||||
{
|
||||
if (hook_)
|
||||
return hook_->recv_bytes(*this, to_u(peer), tag);
|
||||
return to(peer).recv_bytes(tag);
|
||||
}
|
||||
|
||||
/// @brief One-way receive through the hook (or the mesh when unset).
|
||||
template <typename T>
|
||||
HEDLEY_WARN_UNUSED_RESULT
|
||||
T recv_from(role peer, msg tag)
|
||||
{
|
||||
static_assert(std::is_trivially_copyable_v<T>,
|
||||
"trio::recv_from requires a trivially copyable type");
|
||||
if (hook_)
|
||||
{
|
||||
auto bytes = hook_->recv_bytes(*this, to_u(peer), tag);
|
||||
if (bytes.size() != sizeof(T))
|
||||
throw std::runtime_error("trio::recv_from size mismatch");
|
||||
T value{};
|
||||
if constexpr (sizeof(T) != 0)
|
||||
std::memcpy(&value, bytes.data(), sizeof(T));
|
||||
return value;
|
||||
}
|
||||
return to(peer).template recv<T>(tag);
|
||||
}
|
||||
|
||||
/// @brief Homogeneous vector receive through the hook (or the mesh).
|
||||
template <typename T>
|
||||
HEDLEY_WARN_UNUSED_RESULT
|
||||
std::vector<T> recv_vec_from(role peer, msg tag = msg::beaver_tape)
|
||||
{
|
||||
static_assert(std::is_trivially_copyable_v<T>,
|
||||
"trio::recv_vec_from requires a trivially copyable type");
|
||||
if (hook_)
|
||||
{
|
||||
auto bytes = hook_->recv_bytes(*this, to_u(peer), tag);
|
||||
if constexpr (sizeof(T) == 0)
|
||||
{
|
||||
if (!bytes.empty())
|
||||
throw std::runtime_error("trio::recv_vec_from size");
|
||||
return {};
|
||||
}
|
||||
if (bytes.size() % sizeof(T) != 0)
|
||||
throw std::runtime_error("trio::recv_vec_from size mismatch");
|
||||
std::vector<T> out(bytes.size() / sizeof(T));
|
||||
if (!out.empty())
|
||||
std::memcpy(out.data(), bytes.data(), bytes.size());
|
||||
return out;
|
||||
}
|
||||
return to(peer).template recv_vec<T>(tag);
|
||||
}
|
||||
|
||||
/// @brief Round-batched peer sink. Default is a mux on the p0–p1 link.
|
||||
/// @details Defined next to `mux_sink` so the default factory can build one.
|
||||
std::unique_ptr<RoundSink> batch(std::size_t count,
|
||||
std::vector<std::size_t> slot_bytes);
|
||||
|
||||
/// @brief Dealer sends one side of a split to each computing party.
|
||||
/// @tparam Split a type with `p0` and `p1` members
|
||||
/// @param s the split
|
||||
/// @throws std::logic_error if this process is not p2
|
||||
template <typename Split>
|
||||
void deal(const Split & s)
|
||||
{
|
||||
if (self_ != role::p2)
|
||||
throw std::logic_error("only the dealer may deal");
|
||||
send_to(role::p0, msg::beaver_tape, s.p0);
|
||||
send_to(role::p1, msg::beaver_tape, s.p1);
|
||||
}
|
||||
|
||||
/// @brief Computing party receives its share of a dealt value.
|
||||
/// @tparam Ring the dealt type
|
||||
/// @return this party's share
|
||||
/// @throws std::logic_error if this process is p2
|
||||
template <typename Ring>
|
||||
HEDLEY_WARN_UNUSED_RESULT
|
||||
Ring accept_deal()
|
||||
{
|
||||
if (self_ == role::p2)
|
||||
throw std::logic_error("dealer does not accept_deal");
|
||||
return recv_from<Ring>(role::p2, msg::beaver_tape);
|
||||
}
|
||||
|
||||
/// @brief Open an additive share with the peer computing party.
|
||||
/// @tparam Ring additive ring
|
||||
/// @param peer the other computing party
|
||||
/// @param mine this party's share
|
||||
/// @return reconstructed public value (`mine + peer`)
|
||||
/// @throws std::logic_error if either side is p2
|
||||
template <typename Ring>
|
||||
HEDLEY_WARN_UNUSED_RESULT
|
||||
Ring open_with(role peer, const Ring & mine)
|
||||
{
|
||||
if (self_ == role::p2 || peer == role::p2)
|
||||
throw std::logic_error("open_with is between p0 and p1");
|
||||
Ring theirs = exchange_with(peer, mine, msg::delta);
|
||||
return static_cast<Ring>(mine + theirs);
|
||||
}
|
||||
|
||||
/// @brief Exchange raw values (no algebra) with a peer.
|
||||
/// @tparam T trivially copyable payload
|
||||
/// @param peer the other party
|
||||
/// @param mine this party's value
|
||||
/// @param tag the message tag
|
||||
/// @return the peer's value
|
||||
template <typename T>
|
||||
HEDLEY_WARN_UNUSED_RESULT
|
||||
T exchange_with(role peer, const T & mine, msg tag = msg::delta)
|
||||
{
|
||||
static_assert(std::is_trivially_copyable_v<T>,
|
||||
"trio::exchange_with requires a trivially copyable type");
|
||||
if (hook_)
|
||||
{
|
||||
auto bytes = hook_->exchange_bytes(*this, to_u(peer), tag, &mine,
|
||||
sizeof(T));
|
||||
if (bytes.size() != sizeof(T))
|
||||
throw std::runtime_error("trio::exchange_with size mismatch");
|
||||
T theirs{};
|
||||
if constexpr (sizeof(T) != 0)
|
||||
std::memcpy(&theirs, bytes.data(), sizeof(T));
|
||||
return theirs;
|
||||
}
|
||||
return to(peer).template exchange<T>(to_u(self_), to_u(peer), mine, tag);
|
||||
}
|
||||
|
||||
/// @brief Exchange a homogeneous vector with a peer (one barrier).
|
||||
/// @tparam T trivially copyable element
|
||||
/// @param peer the other party
|
||||
/// @param mine this party's values
|
||||
/// @param tag the message tag
|
||||
/// @return the peer's values
|
||||
template <typename T>
|
||||
HEDLEY_WARN_UNUSED_RESULT
|
||||
std::vector<T> exchange_vec_with(role peer, const std::vector<T> & mine,
|
||||
msg tag = msg::ring_vector)
|
||||
{
|
||||
static_assert(std::is_trivially_copyable_v<T>,
|
||||
"trio::exchange_vec_with requires a trivially copyable type");
|
||||
if (mine.empty())
|
||||
return {};
|
||||
if (hook_)
|
||||
{
|
||||
const std::size_t nbytes = mine.size() * sizeof(T);
|
||||
auto bytes = hook_->exchange_vec_bytes(*this, to_u(peer), tag,
|
||||
mine.data(), nbytes);
|
||||
if (bytes.size() != nbytes)
|
||||
throw std::runtime_error("trio::exchange_vec_with size mismatch");
|
||||
std::vector<T> theirs(mine.size());
|
||||
std::memcpy(theirs.data(), bytes.data(), nbytes);
|
||||
return theirs;
|
||||
}
|
||||
return to(peer).template exchange_vec<T>(
|
||||
to_u(self_), to_u(peer), mine, tag);
|
||||
}
|
||||
|
||||
/// @brief Open additive shares packed into one vector exchange.
|
||||
/// @tparam Ring additive ring
|
||||
/// @param peer the other computing party
|
||||
/// @param mine this party's shares
|
||||
/// @return reconstructed public values (`mine[i] + peer[i]`)
|
||||
template <typename Ring>
|
||||
HEDLEY_WARN_UNUSED_RESULT
|
||||
std::vector<Ring> open_vec_with(role peer, const std::vector<Ring> & mine)
|
||||
{
|
||||
if (self_ == role::p2 || peer == role::p2)
|
||||
throw std::logic_error("open_vec_with is between p0 and p1");
|
||||
auto theirs = exchange_vec_with(peer, mine);
|
||||
std::vector<Ring> out(mine.size());
|
||||
for (std::size_t i = 0; i < mine.size(); ++i)
|
||||
out[i] = static_cast<Ring>(mine[i] + theirs[i]);
|
||||
return out;
|
||||
}
|
||||
|
||||
private:
|
||||
role self_ = role::p0;
|
||||
std::unique_ptr<asio::io_context> io_;
|
||||
channel link_[3];
|
||||
comm_hook * hook_ = nullptr;
|
||||
|
||||
static channel connect_one(role self, role peer, const std::string & dir,
|
||||
asio::io_context & io, unsigned retries, const peer_security & sec,
|
||||
const deadlines & lim)
|
||||
{
|
||||
using proto = asio::local::stream_protocol;
|
||||
const bool accept = to_u(self) < to_u(peer);
|
||||
const auto path = link_path(dir, self, peer);
|
||||
const std::string who = role_name(peer);
|
||||
if (accept)
|
||||
{
|
||||
::unlink(path.c_str());
|
||||
proto::endpoint ep(path);
|
||||
proto::acceptor acc(io, ep);
|
||||
for (unsigned i = 0; i < retries; ++i)
|
||||
{
|
||||
asio::error_code ec;
|
||||
proto::socket sock(io);
|
||||
acc.accept(sock, ec);
|
||||
if (!ec)
|
||||
return secure_local_channel(io, std::move(sock), true,
|
||||
to_u(peer), sec, lim.handshake, who, "accept");
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
}
|
||||
throw std::runtime_error("trio accept failed: " + path);
|
||||
}
|
||||
proto::endpoint ep(path);
|
||||
for (unsigned i = 0; i < retries; ++i)
|
||||
{
|
||||
asio::error_code ec;
|
||||
proto::socket sock(io);
|
||||
sock.connect(ep, ec);
|
||||
if (!ec)
|
||||
return secure_local_channel(io, std::move(sock), false,
|
||||
to_u(peer), sec, lim.handshake, who, "connect");
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
}
|
||||
throw std::runtime_error("trio connect failed: " + path);
|
||||
}
|
||||
|
||||
static std::uint16_t tcp_port(std::uint16_t base, role a, role b)
|
||||
{
|
||||
if (to_u(a) > to_u(b))
|
||||
std::swap(a, b);
|
||||
return static_cast<std::uint16_t>(base + 10u * to_u(a) + to_u(b));
|
||||
}
|
||||
|
||||
static channel connect_one_tcp(role self, role peer, std::uint16_t base,
|
||||
const std::string & host, asio::io_context & io, unsigned retries,
|
||||
const peer_security & sec, const deadlines & lim,
|
||||
const socket_options & so)
|
||||
{
|
||||
using tcp = asio::ip::tcp;
|
||||
const bool accept = to_u(self) < to_u(peer);
|
||||
const auto port = tcp_port(base, self, peer);
|
||||
const std::string who = role_name(peer);
|
||||
if (accept)
|
||||
{
|
||||
tcp::endpoint ep(tcp::v4(), port);
|
||||
tcp::acceptor acc(io);
|
||||
asio::error_code ec;
|
||||
acc.open(ep.protocol(), ec);
|
||||
acc.set_option(tcp::acceptor::reuse_address(true), ec);
|
||||
acc.bind(ep, ec);
|
||||
if (ec)
|
||||
throw std::runtime_error("trio tcp bind: " + ec.message());
|
||||
acc.listen(1, ec);
|
||||
for (unsigned i = 0; i < retries; ++i)
|
||||
{
|
||||
tcp::socket sock(io);
|
||||
acc.accept(sock, ec);
|
||||
if (!ec)
|
||||
return secure_tcp_channel(io, std::move(sock), true,
|
||||
to_u(peer), sec, so, lim.handshake, who, "accept");
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
}
|
||||
throw std::runtime_error("trio tcp accept failed");
|
||||
}
|
||||
tcp::resolver resolver(io);
|
||||
auto endpoints = resolver.resolve(host, std::to_string(port));
|
||||
for (unsigned i = 0; i < retries; ++i)
|
||||
{
|
||||
asio::error_code ec;
|
||||
tcp::socket sock(io);
|
||||
asio::connect(sock, endpoints, ec);
|
||||
if (!ec)
|
||||
return secure_tcp_channel(io, std::move(sock), false,
|
||||
to_u(peer), sec, so, lim.handshake, who, "connect");
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
}
|
||||
throw std::runtime_error("trio tcp connect failed");
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace net
|
||||
} // namespace dpf
|
||||
|
||||
#endif // LIBDPF_INCLUDE_DPF_NET_TRIO_HPP__
|
||||
Loading…
Add table
Add a link
Reference in a new issue