libdpf/include/dpf/net/connect.hpp
Ryan Henry 0d22946a0e 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>
2026-09-28 05:59:19 -06:00

263 lines
8.7 KiB
C++

/// @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__