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:
15
tools/EVerest-main/lib/everest/io/test/CMakeLists.txt
Normal file
15
tools/EVerest-main/lib/everest/io/test/CMakeLists.txt
Normal 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.")
|
||||
121
tools/EVerest-main/lib/everest/io/test/datagram_selftest.sh
Executable file
121
tools/EVerest-main/lib/everest/io/test/datagram_selftest.sh
Executable 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"
|
||||
'
|
||||
149
tools/EVerest-main/lib/everest/io/test/endpoint_test.cpp
Normal file
149
tools/EVerest-main/lib/everest/io/test/endpoint_test.cpp
Normal 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);
|
||||
}
|
||||
110
tools/EVerest-main/lib/everest/io/test/udp_client_v6_test.cpp
Normal file
110
tools/EVerest-main/lib/everest/io/test/udp_client_v6_test.cpp
Normal 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);
|
||||
}
|
||||
@@ -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")));
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user