Add extracted tools: CitrineOS, OpenOCPP, ShapeShifter

- CitrineOS core extracted (CSMS OCPP 2.0.1)
- OpenOCPP extracted (firmware OCPP 1.6J/2.0.1)
- ShapeShifter library installed (pip install -e)
- ShapeShifter specification extracted
- EVerest extracted

TODO updated with progress
This commit is contained in:
Eric F
2026-06-08 00:38:27 -04:00
parent 468cfeaa50
commit d398a6ced2
7326 changed files with 1177561 additions and 7 deletions

View File

@@ -0,0 +1,15 @@
add_executable(everest_io_tests
endpoint_test.cpp
udp_client_v6_test.cpp
udp_unconnected_socket_test.cpp
udp_dualstack_server_socket_test.cpp
)
target_link_libraries(everest_io_tests
PRIVATE
GTest::gtest_main
everest::io
)
include(GoogleTest)
gtest_discover_tests(everest_io_tests TEST_PREFIX "io.")

View File

@@ -0,0 +1,121 @@
#!/usr/bin/env bash
# datagram_selftest.sh - privileged real-multicast self-test for the generic
# unconnected UDP client (udp_unconnected_client / udp_unconnected_socket).
#
# NOT a ctest case. The gtest suite (lib/everest/io/test/) covers parsing,
# loopback round-trips and connect-drop-absence on the loopback interface.
# This script exercises the one thing loopback cannot: a *real* multicast
# send on a dedicated NIC followed by a *unicast* reply from a source whose
# address differs from the multicast group. A connected socket would drop
# that reply; the unconnected client must deliver it.
#
# It is fully rootless: it runs inside a user+network namespace via
# unshare --user --map-root-user --net
# so it needs no sudo and touches no host interface. It builds its own
# `dummy` NIC, runs a group-joining responder that answers unicast, and
# checks the example client receives the reply from the responder's unicast
# address (!= group) for both an IPv4 and an IPv6 multicast group.
#
# Usage:
# lib/everest/io/test/datagram_selftest.sh [path-to-test_udp_unconnected_client]
#
# Exit status: 0 = both v4 and v6 passed; non-zero = failure.
set -euo pipefail
ROOT=$(git rev-parse --show-toplevel 2>/dev/null || echo "")
DEFAULT_BIN="${ROOT:-.}/build/lib/everest/io/examples/test_udp_unconnected_client"
CLIENT_BIN="${1:-$DEFAULT_BIN}"
if [ ! -x "$CLIENT_BIN" ]; then
echo "ERROR: client example not found/executable: $CLIENT_BIN" >&2
echo "Build it with: cmake ... -DBUILD_EXAMPLES=ON && ninja -C build test_udp_unconnected_client" >&2
exit 2
fi
if ! unshare --user --map-root-user --net true 2>/dev/null; then
echo "ERROR: rootless 'unshare --user --map-root-user --net' unavailable on this host" >&2
exit 2
fi
# Everything below runs inside the fresh user+net namespace. The inner script
# is single-quoted on purpose: it is interpreted by the namespaced bash, where
# CLIENT_BIN arrives via the exported environment.
export CLIENT_BIN
# shellcheck disable=SC2016
exec unshare --user --map-root-user --net -- bash -euo pipefail -c '
IFACE=dt0
V4_GROUP=239.55.0.1
V4_IF_ADDR=10.55.0.1
V6_GROUP=ff12::55
V6_IF_ADDR=fd00:55::1
PORT=49555
ip link set lo up
ip link add "$IFACE" type dummy
ip addr add "$V4_IF_ADDR"/24 dev "$IFACE"
ip -6 addr add "$V6_IF_ADDR"/64 dev "$IFACE" nodad
ip link set "$IFACE" up
# Route the admin-scoped multicast ranges out the dummy NIC.
ip route add 239.0.0.0/8 dev "$IFACE"
ip -6 route add ff00::/8 dev "$IFACE"
RESPONDER=$(mktemp)
trap "rm -f \"$RESPONDER\"" EXIT
cat > "$RESPONDER" <<"PY"
import socket, struct, sys
fam = socket.AF_INET6 if sys.argv[1] == "6" else socket.AF_INET
group, ifaddr, port = sys.argv[2], sys.argv[3], int(sys.argv[4])
s = socket.socket(fam, socket.SOCK_DGRAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
if fam == socket.AF_INET:
s.bind(("", port))
mreq = socket.inet_pton(socket.AF_INET, group) + socket.inet_aton(ifaddr)
s.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
else:
s.bind(("", port))
ifidx = socket.if_nametoindex("dt0")
mreq = socket.inet_pton(socket.AF_INET6, group) + struct.pack("I", ifidx)
s.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_JOIN_GROUP, mreq)
s.settimeout(8)
# Reply unicast from our interface address (source != multicast group).
r = socket.socket(fam, socket.SOCK_DGRAM)
r.bind((ifaddr, 0))
try:
while True:
data, addr = s.recvfrom(2048)
r.sendto(b"reply:" + data, addr)
except socket.timeout:
pass
PY
run_case() {
fam="$1"; group="$2"; ifaddr="$3"; label="$4"
python3 "$RESPONDER" "$fam" "$group" "$ifaddr" "$PORT" &
rpid=$!
sleep 0.7
out=$(stdbuf -oL timeout 4 stdbuf -oL "$CLIENT_BIN" "$group" "$PORT" "$IFACE" 2>&1 || true)
wait "$rpid" 2>/dev/null || true
echo "----- $label client output -----"
echo "$out" | grep -aE "TX:|RX \(" | head -6 || true
# Pass: an RX line whose source address is the responder unicast addr,
# which is NOT the multicast group.
if echo "$out" | grep -aq "RX (.*from \[${ifaddr}\]" && \
! echo "$out" | grep -aq "RX (.*from \[${group}\]"; then
echo "PASS ($label): unicast reply from $ifaddr delivered (group $group)"
return 0
fi
echo "FAIL ($label): no unicast reply from $ifaddr (group $group)"
return 1
}
rc=0
run_case 4 "$V4_GROUP" "$V4_IF_ADDR" "IPv4" || rc=1
run_case 6 "$V6_GROUP" "$V6_IF_ADDR" "IPv6" || rc=1
if [ "$rc" -eq 0 ]; then
echo "datagram_selftest: ALL PASS (v4 + v6)"
else
echo "datagram_selftest: FAILURES" >&2
fi
exit "$rc"
'

View File

@@ -0,0 +1,149 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2026 Pionix GmbH and Contributors to EVerest
#include <everest/io/udp/endpoint.hpp>
#include <cstring>
#include <stdexcept>
#include <arpa/inet.h>
#include <net/if.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>
#include <gtest/gtest.h>
using everest::lib::io::udp::endpoint;
TEST(endpoint_test, ipv4_literal) {
endpoint ep("127.0.0.1", 8080);
EXPECT_EQ(ep.family(), AF_INET);
EXPECT_EQ(ep.port(), 8080);
EXPECT_EQ(ep.addr_str(), "127.0.0.1");
EXPECT_EQ(ep.sa_len(), sizeof(sockaddr_in));
EXPECT_NE(ep.sa(), nullptr);
}
TEST(endpoint_test, ipv6_loopback_literal) {
endpoint ep("::1", 9000);
EXPECT_EQ(ep.family(), AF_INET6);
EXPECT_EQ(ep.port(), 9000);
EXPECT_EQ(ep.addr_str(), "::1");
EXPECT_EQ(ep.sa_len(), sizeof(sockaddr_in6));
}
TEST(endpoint_test, ipv6_multicast_literal) {
endpoint ep("ff02::1", 5353);
EXPECT_EQ(ep.family(), AF_INET6);
EXPECT_EQ(ep.port(), 5353);
}
TEST(endpoint_test, port_is_host_order) {
endpoint ep("127.0.0.1", 0x1234);
// Raw sockaddr must carry network byte order.
auto const* sin = reinterpret_cast<sockaddr_in const*>(ep.sa());
EXPECT_EQ(sin->sin_port, htons(0x1234));
EXPECT_EQ(ep.port(), 0x1234);
}
TEST(endpoint_test, ipv6_link_local_scope_id_from_iface) {
// Loopback always exists; it is not link-local, so no scope is applied.
// Use a synthetic link-local address with the loopback interface name to
// exercise the scope-id path deterministically.
unsigned int lo = if_nametoindex("lo");
ASSERT_NE(lo, 0u);
endpoint ep("fe80::1", 1234, "lo");
auto const* sin6 = reinterpret_cast<sockaddr_in6 const*>(ep.sa());
EXPECT_EQ(sin6->sin6_scope_id, lo);
EXPECT_EQ(ep.iface(), "lo");
}
TEST(endpoint_test, hostname_resolves) {
endpoint ep("localhost", 4711);
EXPECT_TRUE(ep.family() == AF_INET || ep.family() == AF_INET6);
EXPECT_EQ(ep.port(), 4711);
}
TEST(endpoint_test, invalid_host_throws) {
EXPECT_THROW(endpoint("definitely.not.a.host.invalid.", 80), std::runtime_error);
}
TEST(endpoint_test, from_recvfrom_source_round_trips) {
endpoint origin("127.0.0.1", 6543);
sockaddr_storage ss{};
std::memcpy(&ss, origin.sa(), origin.sa_len());
endpoint restored(ss, origin.sa_len());
EXPECT_EQ(restored.family(), AF_INET);
EXPECT_EQ(restored.port(), 6543);
EXPECT_EQ(restored.addr_str(), "127.0.0.1");
EXPECT_TRUE(restored == origin);
}
TEST(endpoint_test, equality_distinguishes_port) {
endpoint a("127.0.0.1", 1000);
endpoint b("127.0.0.1", 1001);
EXPECT_FALSE(a == b);
EXPECT_TRUE(a != b);
}
TEST(endpoint_test, v4_mapped_detected_and_unmapped) {
// Build a v4-mapped endpoint the way recvfrom on a dual-stack socket would.
sockaddr_storage ss{};
auto* s6 = reinterpret_cast<sockaddr_in6*>(&ss);
s6->sin6_family = AF_INET6;
s6->sin6_port = htons(40123);
inet_pton(AF_INET6, "::ffff:127.0.0.1", &s6->sin6_addr);
endpoint mapped(ss, sizeof(sockaddr_in6));
EXPECT_TRUE(mapped.is_v4_mapped());
auto v4 = mapped.as_v4();
EXPECT_EQ(v4.family(), AF_INET);
EXPECT_EQ(v4.addr_str(), "127.0.0.1");
EXPECT_EQ(v4.port(), 40123);
}
TEST(endpoint_test, native_v6_is_not_v4_mapped_and_as_v4_is_empty) {
endpoint v6("::1", 9000);
EXPECT_FALSE(v6.is_v4_mapped());
auto e = v6.as_v4();
EXPECT_EQ(e.family(), AF_UNSPEC);
EXPECT_EQ(e.sa_len(), 0);
}
TEST(endpoint_test, already_v4_is_not_mapped_and_as_v4_is_empty) {
endpoint v4("127.0.0.1", 1234);
EXPECT_FALSE(v4.is_v4_mapped());
EXPECT_EQ(v4.as_v4().family(), AF_UNSPEC);
}
// The raw sockaddr/len pair must be directly usable by sendto(): send one
// datagram to a bound loopback UDP socket and read it back.
TEST(endpoint_test, sa_usable_by_sendto) {
int rx = ::socket(AF_INET, SOCK_DGRAM, 0);
ASSERT_GE(rx, 0);
sockaddr_in bind_addr{};
bind_addr.sin_family = AF_INET;
bind_addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
bind_addr.sin_port = 0; // ephemeral
ASSERT_EQ(::bind(rx, reinterpret_cast<sockaddr*>(&bind_addr), sizeof(bind_addr)), 0);
sockaddr_in bound{};
socklen_t blen = sizeof(bound);
ASSERT_EQ(::getsockname(rx, reinterpret_cast<sockaddr*>(&bound), &blen), 0);
endpoint target("127.0.0.1", ntohs(bound.sin_port));
int tx = ::socket(AF_INET, SOCK_DGRAM, 0);
ASSERT_GE(tx, 0);
const char msg[] = "hello";
ASSERT_EQ(::sendto(tx, msg, sizeof(msg), 0, target.sa(), target.sa_len()), static_cast<ssize_t>(sizeof(msg)));
char buf[16]{};
ASSERT_EQ(::recv(rx, buf, sizeof(buf), 0), static_cast<ssize_t>(sizeof(msg)));
EXPECT_STREQ(buf, "hello");
::close(tx);
::close(rx);
}

View File

@@ -0,0 +1,110 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2026 Pionix GmbH and Contributors to EVerest
//
// Characterization test: the connected udp_client already works over IPv6.
// This locks that guarantee so the unconnected-client refactor cannot regress
// it. No udp_client / udp_socket production code is touched by this work.
#include <everest/io/udp/udp_client.hpp>
#include <everest/io/udp/udp_payload.hpp>
#include <atomic>
#include <chrono>
#include <thread>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <unistd.h>
#include <gtest/gtest.h>
using everest::lib::io::udp::udp_client;
using everest::lib::io::udp::udp_payload;
using namespace std::chrono_literals;
namespace {
// A minimal blocking IPv6 echo socket on [::1]. Bound in the test body so the
// ephemeral port is known before the client connects; serviced by a thread.
class v6_echo {
public:
v6_echo() {
m_fd = ::socket(AF_INET6, SOCK_DGRAM, 0);
EXPECT_GE(m_fd, 0);
sockaddr_in6 addr{};
addr.sin6_family = AF_INET6;
addr.sin6_addr = in6addr_loopback;
addr.sin6_port = 0;
EXPECT_EQ(::bind(m_fd, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)), 0);
socklen_t len = sizeof(addr);
EXPECT_EQ(::getsockname(m_fd, reinterpret_cast<sockaddr*>(&addr), &len), 0);
m_port = ntohs(addr.sin6_port);
timeval tv{};
tv.tv_usec = 200000; // 200ms so the loop can observe the stop flag
::setsockopt(m_fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
m_thread = std::thread([this] { run(); });
}
~v6_echo() {
m_stop = true;
if (m_thread.joinable()) {
m_thread.join();
}
::close(m_fd);
}
std::uint16_t port() const {
return m_port;
}
private:
void run() {
char buf[256];
while (not m_stop) {
sockaddr_in6 src{};
socklen_t slen = sizeof(src);
auto n = ::recvfrom(m_fd, buf, sizeof(buf), 0, reinterpret_cast<sockaddr*>(&src), &slen);
if (n > 0) {
::sendto(m_fd, buf, static_cast<size_t>(n), 0, reinterpret_cast<sockaddr*>(&src), slen);
}
}
}
int m_fd{-1};
std::uint16_t m_port{0};
std::atomic<bool> m_stop{false};
std::thread m_thread;
};
} // namespace
TEST(udp_client_v6_test, connected_roundtrip_over_ipv6_loopback) {
v6_echo echo;
udp_client client("::1", echo.port(), 1000);
std::atomic<bool> got{false};
udp_payload received;
client.set_rx_handler([&](udp_payload const& p, auto&) {
received = p;
got = true;
});
udp_payload msg("v6-roundtrip");
auto deadline = std::chrono::steady_clock::now() + 5s;
auto next_send = std::chrono::steady_clock::now();
while (std::chrono::steady_clock::now() < deadline && not got) {
if (std::chrono::steady_clock::now() >= next_send) {
client.tx(msg);
next_send = std::chrono::steady_clock::now() + 500ms;
}
client.sync(100ms);
}
ASSERT_TRUE(got) << "no IPv6 loopback echo received within timeout";
EXPECT_EQ(received, msg);
}

View File

@@ -0,0 +1,163 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2026 Pionix GmbH and Contributors to EVerest
#include <everest/io/socket/socket.hpp>
#include <everest/io/udp/endpoint.hpp>
#include <everest/io/udp/udp_dualstack_server.hpp>
#include <everest/io/udp/udp_dualstack_server_socket.hpp>
// event_client_async_policy_v arrives transitively via udp_dualstack_server.hpp
// (fd_event_client.hpp); that header has no include guard, so do not include it
// a second time directly here.
#include <cstring>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <poll.h>
#include <sys/socket.h>
#include <unistd.h>
#include <gtest/gtest.h>
using namespace everest::lib::io;
using everest::lib::io::udp::endpoint;
using everest::lib::io::udp::udp_dualstack_server_socket;
using everest::lib::io::udp::udp_payload;
using everest::lib::io::utilities::event_client_async_policy_v;
// Open only, no setup/connect: must be the synchronous client policy.
static_assert(not event_client_async_policy_v<udp_dualstack_server_socket>,
"udp_dualstack_server_socket must be a synchronous client policy");
namespace {
bool wait_readable(int fd, int timeout_ms) {
pollfd pfd{fd, POLLIN, 0};
return ::poll(&pfd, 1, timeout_ms) > 0 && (pfd.revents & POLLIN) != 0;
}
bool rx_with_timeout(udp_dualstack_server_socket& s, udp_payload& out, int timeout_ms = 1000) {
if (not wait_readable(s.get_fd(), timeout_ms)) {
return false;
}
return s.rx(out);
}
std::uint16_t bound_port(int fd) {
sockaddr_storage ss{};
socklen_t len = sizeof(ss);
EXPECT_EQ(::getsockname(fd, reinterpret_cast<sockaddr*>(&ss), &len), 0);
return ntohs(ss.ss_family == AF_INET6 ? reinterpret_cast<sockaddr_in6*>(&ss)->sin6_port
: reinterpret_cast<sockaddr_in*>(&ss)->sin_port);
}
} // namespace
TEST(udp_dualstack_factory_test, binds_dualstack_v6_any) {
auto fd = socket::open_udp_dualstack_server_socket(0); // ephemeral
ASSERT_GE(static_cast<int>(fd), 0);
int v6only = 1;
socklen_t l = sizeof(v6only);
ASSERT_EQ(::getsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &v6only, &l), 0);
EXPECT_EQ(v6only, 0); // dual-stack
sockaddr_storage ss{};
socklen_t sl = sizeof(ss);
ASSERT_EQ(::getsockname(fd, reinterpret_cast<sockaddr*>(&ss), &sl), 0);
EXPECT_EQ(ss.ss_family, AF_INET6);
}
TEST(udp_dualstack_factory_test, device_empty_is_ok) {
EXPECT_GE(static_cast<int>(socket::open_udp_dualstack_server_socket(0, {})), 0);
}
TEST(udp_dualstack_server_socket_test, v6_roundtrip_loopback) {
udp_dualstack_server_socket S;
ASSERT_TRUE(S.open(0));
const std::uint16_t srv_port = bound_port(S.get_fd());
int cl = ::socket(AF_INET6, SOCK_DGRAM, 0);
ASSERT_GE(cl, 0);
sockaddr_in6 cl_bind{};
cl_bind.sin6_family = AF_INET6;
cl_bind.sin6_addr = in6addr_loopback;
ASSERT_EQ(::bind(cl, reinterpret_cast<sockaddr*>(&cl_bind), sizeof(cl_bind)), 0);
const std::uint16_t cl_port = bound_port(cl);
sockaddr_in6 dst{};
dst.sin6_family = AF_INET6;
dst.sin6_addr = in6addr_loopback;
dst.sin6_port = htons(srv_port);
udp_payload ping("ping6");
ASSERT_EQ(::sendto(cl, ping.buffer.data(), ping.size(), 0, reinterpret_cast<sockaddr*>(&dst), sizeof(dst)),
static_cast<ssize_t>(ping.size()));
udp_payload got;
ASSERT_TRUE(rx_with_timeout(S, got));
EXPECT_EQ(got, udp_payload("ping6"));
auto src = S.last_source();
ASSERT_TRUE(src.has_value());
EXPECT_EQ(src->family(), AF_INET6);
EXPECT_EQ(src->port(), cl_port);
EXPECT_FALSE(src->is_v4_mapped());
ASSERT_TRUE(S.tx(udp_payload("pong6")));
char buf[64]{};
ASSERT_TRUE(wait_readable(cl, 1000));
auto n = ::recv(cl, buf, sizeof(buf), 0);
ASSERT_GT(n, 0);
EXPECT_STREQ(buf, "pong6");
::close(cl);
}
TEST(udp_dualstack_server_socket_test, v4_mapped_roundtrip_loopback) {
udp_dualstack_server_socket S;
ASSERT_TRUE(S.open(0));
const std::uint16_t srv_port = bound_port(S.get_fd());
int cl = ::socket(AF_INET, SOCK_DGRAM, 0);
ASSERT_GE(cl, 0);
sockaddr_in cl_bind{};
cl_bind.sin_family = AF_INET;
cl_bind.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
ASSERT_EQ(::bind(cl, reinterpret_cast<sockaddr*>(&cl_bind), sizeof(cl_bind)), 0);
const std::uint16_t cl_port = bound_port(cl);
sockaddr_in dst{};
dst.sin_family = AF_INET;
dst.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
dst.sin_port = htons(srv_port);
udp_payload ping("ping4");
ASSERT_EQ(::sendto(cl, ping.buffer.data(), ping.size(), 0, reinterpret_cast<sockaddr*>(&dst), sizeof(dst)),
static_cast<ssize_t>(ping.size()));
udp_payload got;
ASSERT_TRUE(rx_with_timeout(S, got));
EXPECT_EQ(got, udp_payload("ping4"));
auto src = S.last_source();
ASSERT_TRUE(src.has_value());
EXPECT_EQ(src->family(), AF_INET6); // v4-mapped is carried in a v6 sockaddr
EXPECT_TRUE(src->is_v4_mapped());
auto v4 = src->as_v4();
EXPECT_EQ(v4.family(), AF_INET);
EXPECT_EQ(v4.addr_str(), "127.0.0.1");
EXPECT_EQ(v4.port(), cl_port);
// Reply must reach the v4 client via the verbatim mapped sockaddr.
ASSERT_TRUE(S.tx(udp_payload("pong4")));
char buf[64]{};
ASSERT_TRUE(wait_readable(cl, 1000));
auto n = ::recv(cl, buf, sizeof(buf), 0);
ASSERT_GT(n, 0);
EXPECT_STREQ(buf, "pong4");
::close(cl);
}
TEST(udp_dualstack_server_socket_test, tx_before_rx_is_false) {
udp_dualstack_server_socket S;
ASSERT_TRUE(S.open(0));
EXPECT_FALSE(S.tx(udp_payload("nope")));
}

View File

@@ -0,0 +1,215 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2026 Pionix GmbH and Contributors to EVerest
#include <everest/io/udp/udp_unconnected_client.hpp>
#include <everest/io/udp/udp_unconnected_socket.hpp>
// event_client_async_policy_v arrives transitively via udp_unconnected_client.hpp
// (fd_event_client.hpp); that header has no include guard, so do not include it
// a second time directly here.
#include <cstring>
#include <string>
#include <arpa/inet.h>
#include <net/if.h>
#include <netinet/in.h>
#include <poll.h>
#include <sys/socket.h>
#include <unistd.h>
#include <gtest/gtest.h>
using everest::lib::io::udp::endpoint;
using everest::lib::io::udp::udp_payload;
using everest::lib::io::udp::udp_unconnected_socket;
using everest::lib::io::utilities::event_client_async_policy_v;
// Policy must be the synchronous variant (open only; no setup/connect), so
// fd_event_client uses the in-thread open() path with no detached connect.
static_assert(not event_client_async_policy_v<udp_unconnected_socket>,
"udp_unconnected_socket must be a synchronous client policy");
namespace {
const char* loopback(int family) {
return family == AF_INET6 ? "::1" : "127.0.0.1";
}
// A raw datagram peer bound to loopback on an ephemeral port.
struct peer {
int fd{-1};
std::uint16_t port{0};
explicit peer(int family) {
fd = ::socket(family, SOCK_DGRAM, 0);
EXPECT_GE(fd, 0);
sockaddr_storage ss{};
socklen_t len = 0;
if (family == AF_INET6) {
auto* a = reinterpret_cast<sockaddr_in6*>(&ss);
a->sin6_family = AF_INET6;
a->sin6_addr = in6addr_loopback;
len = sizeof(*a);
} else {
auto* a = reinterpret_cast<sockaddr_in*>(&ss);
a->sin_family = AF_INET;
a->sin_addr.s_addr = htonl(INADDR_LOOPBACK);
len = sizeof(*a);
}
EXPECT_EQ(::bind(fd, reinterpret_cast<sockaddr*>(&ss), len), 0);
socklen_t blen = sizeof(ss);
EXPECT_EQ(::getsockname(fd, reinterpret_cast<sockaddr*>(&ss), &blen), 0);
port = ntohs(family == AF_INET6 ? reinterpret_cast<sockaddr_in6*>(&ss)->sin6_port
: reinterpret_cast<sockaddr_in*>(&ss)->sin_port);
}
~peer() {
if (fd >= 0) {
::close(fd);
}
}
};
std::uint16_t bound_port(int fd) {
sockaddr_storage ss{};
socklen_t len = sizeof(ss);
EXPECT_EQ(::getsockname(fd, reinterpret_cast<sockaddr*>(&ss), &len), 0);
return ntohs(ss.ss_family == AF_INET6 ? reinterpret_cast<sockaddr_in6*>(&ss)->sin6_port
: reinterpret_cast<sockaddr_in*>(&ss)->sin_port);
}
bool wait_readable(int fd, int timeout_ms) {
pollfd pfd{fd, POLLIN, 0};
return ::poll(&pfd, 1, timeout_ms) > 0 && (pfd.revents & POLLIN) != 0;
}
bool rx_with_timeout(udp_unconnected_socket& sock, udp_payload& out, int timeout_ms = 1000) {
if (not wait_readable(sock.get_fd(), timeout_ms)) {
return false;
}
return sock.rx(out);
}
void roundtrip_for_family(int family) {
peer p(family);
udp_unconnected_socket u;
ASSERT_TRUE(u.open(endpoint(loopback(family), p.port)));
udp_payload msg("ping");
ASSERT_TRUE(u.tx(msg));
// Peer receives, learns u's source, echoes back.
char buf[64];
sockaddr_storage src{};
socklen_t slen = sizeof(src);
ASSERT_TRUE(wait_readable(p.fd, 1000));
auto n = ::recvfrom(p.fd, buf, sizeof(buf), 0, reinterpret_cast<sockaddr*>(&src), &slen);
ASSERT_GT(n, 0);
ASSERT_EQ(::sendto(p.fd, buf, static_cast<size_t>(n), 0, reinterpret_cast<sockaddr*>(&src), slen),
static_cast<ssize_t>(n));
udp_payload reply;
ASSERT_TRUE(rx_with_timeout(u, reply));
EXPECT_EQ(reply, msg);
auto last = u.last_source();
ASSERT_TRUE(last.has_value());
EXPECT_EQ(last->family(), family);
EXPECT_EQ(last->port(), p.port);
EXPECT_EQ(last->addr_str(), loopback(family));
}
// connect-drop-absence: u targets an arbitrary endpoint, but a *different*
// peer sends straight to u's ephemeral port. Because open() does no ::connect,
// the unrelated source is still delivered, and last_source() reflects that
// sender, not the configured target.
void connect_drop_absence_for_family(int family) {
peer target(family); // configured tx destination; never receives here
udp_unconnected_socket u;
ASSERT_TRUE(u.open(endpoint(loopback(family), target.port)));
std::uint16_t u_port = bound_port(u.get_fd());
peer other(family); // a sender unrelated to the configured target
sockaddr_storage dst{};
socklen_t dlen = 0;
if (family == AF_INET6) {
auto* a = reinterpret_cast<sockaddr_in6*>(&dst);
a->sin6_family = AF_INET6;
a->sin6_addr = in6addr_loopback;
a->sin6_port = htons(u_port);
dlen = sizeof(*a);
} else {
auto* a = reinterpret_cast<sockaddr_in*>(&dst);
a->sin_family = AF_INET;
a->sin_addr.s_addr = htonl(INADDR_LOOPBACK);
a->sin_port = htons(u_port);
dlen = sizeof(*a);
}
const char payload[] = "from-other";
ASSERT_EQ(::sendto(other.fd, payload, sizeof(payload), 0, reinterpret_cast<sockaddr*>(&dst), dlen),
static_cast<ssize_t>(sizeof(payload)));
udp_payload got;
ASSERT_TRUE(rx_with_timeout(u, got)) << "datagram from a non-target source was dropped (socket is connected?)";
auto last = u.last_source();
ASSERT_TRUE(last.has_value());
EXPECT_EQ(last->port(), other.port);
EXPECT_NE(last->port(), target.port);
EXPECT_FALSE(*last == endpoint(loopback(family), target.port));
}
} // namespace
TEST(udp_unconnected_socket_test, roundtrip_ipv4) {
roundtrip_for_family(AF_INET);
}
TEST(udp_unconnected_socket_test, roundtrip_ipv6) {
roundtrip_for_family(AF_INET6);
}
TEST(udp_unconnected_socket_test, connect_drop_absence_ipv4) {
connect_drop_absence_for_family(AF_INET);
}
TEST(udp_unconnected_socket_test, connect_drop_absence_ipv6) {
connect_drop_absence_for_family(AF_INET6);
}
TEST(udp_unconnected_socket_test, multicast_egress_iface_set_ipv6) {
unsigned int lo = if_nametoindex("lo");
ASSERT_NE(lo, 0u);
udp_unconnected_socket u;
ASSERT_TRUE(u.open(endpoint("ff02::1", 5353, "lo")));
int ifindex = 0;
socklen_t len = sizeof(ifindex);
ASSERT_EQ(::getsockopt(u.get_fd(), IPPROTO_IPV6, IPV6_MULTICAST_IF, &ifindex, &len), 0);
EXPECT_EQ(static_cast<unsigned int>(ifindex), lo);
}
TEST(udp_unconnected_socket_test, multicast_egress_iface_set_ipv4) {
unsigned int lo = if_nametoindex("lo");
ASSERT_NE(lo, 0u);
udp_unconnected_socket u;
// set_multicast_if() throws if setsockopt(IP_MULTICAST_IF) fails, which
// would make open() return false; a successful open for a multicast v4
// target on a named interface proves the egress option was applied.
// (Linux getsockopt(IP_MULTICAST_IF) returns only a zeroed in_addr when
// set by interface index, so a value read-back is not possible here; the
// v6 case below asserts the index strictly via getsockopt.)
EXPECT_TRUE(u.open(endpoint("239.1.2.3", 5000, "lo")));
}
TEST(udp_unconnected_socket_test, client_alias_constructs) {
// The fd_event_client alias must instantiate against the sync policy and
// open the underlying socket synchronously in its constructor.
peer p(AF_INET);
everest::lib::io::udp::udp_unconnected_client client(endpoint("127.0.0.1", p.port));
EXPECT_GE(client.get_poll_fd(), 0);
auto const& raw = client.get_raw_handler();
ASSERT_NE(raw, nullptr);
EXPECT_GE(raw->get_fd(), 0);
}