OpenVPN 3 Core Library
Loading...
Searching...
No Matches
test_serverprober.cpp
Go to the documentation of this file.
1// OpenVPN -- An application to securely tunnel IP networks
2// over a single port, with support for SSL/TLS-based
3// session authentication and key exchange,
4// packet encryption, packet authentication, and
5// packet compression.
6//
7// Copyright (C) 2026- OpenVPN Inc.
8//
9// SPDX-License-Identifier: MPL-2.0 OR AGPL-3.0-only WITH openvpn3-openssl-exception
10//
11
12// Tests for ServerProber: drive it against a loopback fake server that answers a
13// SERVER_PROBE with a PROBE_REPLY, and check the prober reports the responding
14// remote with its advertised parameters and a measured round-trip time. Uses the
15// plaintext control-channel mode so the fake server needs no key material.
16
17#include "test_common.hpp"
18
19#include <memory>
20#include <string>
21
22#include <openvpn/io/io.hpp>
23#include <openvpn/addr/ip.hpp>
25#include <openvpn/time/time.hpp>
30
31using namespace openvpn;
32
33namespace {
34
35// Minimal client/server ProtoConfig with no control-channel keys -> plaintext
36// wrapping. ProbeWrap only needs frame/now/rng and a stats object.
37ProtoContext::ProtoConfig::Ptr make_plain_config(Frame::Ptr frame, StrongRandomAPI::Ptr rng, Time &time)
38{
40 cp->frame = frame;
41 cp->now = &time;
42 cp->rng = rng;
43 cp->prng = rng;
44 return cp;
45}
46
47// A loopback UDP server that answers a single SERVER_PROBE with a PROBE_REPLY,
48// echoing the client's probe session id and advertising fixed parameters.
49class FakeProbeServer
50{
51 public:
52 FakeProbeServer(openvpn_io::io_context &io, const ProtoContext::ProtoConfig::Ptr &cfg, const SessionStats::Ptr &stats)
53 : socket(io), wrap(cfg, stats)
54 {
55 socket.open(openvpn_io::ip::udp::v4());
56 socket.bind(openvpn_io::ip::udp::endpoint(openvpn_io::ip::make_address("127.0.0.1"), 0));
57 }
58
59 unsigned short port() const
60 {
61 return socket.local_endpoint().port();
62 }
63
64 void start()
65 {
66 do_recv();
67 }
68
69 void stop()
70 {
71 openvpn_io::error_code ec;
72 socket.close(ec);
73 }
74
75 static constexpr std::uint16_t REPLY_PRIORITY = 5;
76 static constexpr std::uint16_t REPLY_WEIGHT = 9;
77 static constexpr std::uint16_t REPLY_CONNECT_LIFETIME = 42;
78
79 private:
80 struct RecvCtx
81 {
83 openvpn_io::ip::udp::endpoint sender;
84 };
85
86 void do_recv()
87 {
88 auto rc = std::make_shared<RecvCtx>();
89 rc->buf.reset(0, 1600, BufAllocFlags::NO_FLAGS);
90 socket.async_receive_from(rc->buf.mutable_buffer(), rc->sender, [this, rc](const openvpn_io::error_code &error, const size_t n)
91 {
92 if (error || !n)
93 return;
94 rc->buf.set_size(n);
95 respond(rc->buf, rc->sender); });
96 }
97
98 void respond(BufferAllocated &buf, const openvpn_io::ip::udp::endpoint &sender)
99 {
101 ProtoSessionID client_psid;
102 PacketIDControl pid;
103 if (wrap.unwrap(buf, work, client_psid, pid) != ProtoContext::UnwrapStatus::OK)
104 return;
105
106 if (!oob::server_probe_read(buf))
107 return;
108
109 const oob::ProbeReply reply{.peer_session_id = client_psid,
110 .priority = REPLY_PRIORITY,
111 .weight = REPLY_WEIGHT,
112 .max_latency_diff = 0,
113 .connect_lifetime = REPLY_CONNECT_LIFETIME,
114 .flags = 0};
115
116 BufferAllocated rbuf;
117 rbuf.reset(512, 2048, BufAllocFlags::NO_FLAGS);
118 if (!oob::client_reply_write(rbuf, reply))
119 return;
120 BufferAllocated rwork;
121 wrap.wrap(rbuf, rwork);
122
123 openvpn_io::error_code ec;
124 socket.send_to(rbuf.const_buffer(), sender, 0, ec);
125 }
126
127 openvpn_io::ip::udp::socket socket;
129};
130
131struct Collector : public ServerProber::NotifyCallback
132{
133 std::vector<ServerProber::Result> results;
134 bool done = false;
135
136 void server_probe_done(std::vector<ServerProber::Result> r) override
137 {
138 results = std::move(r);
139 done = true;
140 }
141};
142
143} // namespace
144
145TEST(ServerProber, plaintext_probe_roundtrip)
146{
147 openvpn_io::io_context io(1);
148 Frame::Ptr frame(new Frame(Frame::Context(512, 2048, 512, 0, 16, BufAllocFlags::NO_FLAGS)));
150 Time time = Time::now();
151 SessionStats::Ptr stats(new SessionStats());
152
153 // fake server on an ephemeral loopback port
154 FakeProbeServer server(io, make_plain_config(frame, rng, time), stats);
155 server.start();
156 const unsigned short port = server.port();
157
158 // one already-resolved UDP remote pointing at the fake server
159 RemoteList::Ptr rl(new RemoteList("127.0.0.1", std::to_string(port), Protocol(Protocol::UDPv4), "test"));
160 rl->get_item(0)->set_ip_addr(IP::Addr::from_string("127.0.0.1"));
161
162 auto prober = std::make_shared<ServerProber>(io,
163 rl,
164 make_plain_config(frame, rng, time),
165 nullptr, // no socket_protect
166 stats,
167 Time::Duration::milliseconds(300));
168
169 Collector cb;
170 prober->start(&cb);
171 io.run();
172
173 ASSERT_TRUE(cb.done);
174 ASSERT_EQ(cb.results.size(), 1u);
175 const ServerProber::Result &r = cb.results[0];
176 EXPECT_EQ(r.remote_index, 0u);
177 EXPECT_EQ(r.addr.to_string(), "127.0.0.1");
178 EXPECT_EQ(r.port, port);
179 EXPECT_EQ(r.reply.priority, FakeProbeServer::REPLY_PRIORITY);
180 EXPECT_EQ(r.reply.weight, FakeProbeServer::REPLY_WEIGHT);
181 EXPECT_EQ(r.reply.connect_lifetime, FakeProbeServer::REPLY_CONNECT_LIFETIME);
182 EXPECT_LE(r.rtt, Time::Duration::seconds(2)); // sane, non-garbage latency
183
184 server.stop();
185}
186
187// No server listening: the probe window elapses and the prober reports nothing.
188TEST(ServerProber, no_reply_times_out_empty)
189{
190 openvpn_io::io_context io(1);
191 Frame::Ptr frame(new Frame(Frame::Context(512, 2048, 512, 0, 16, BufAllocFlags::NO_FLAGS)));
193 Time time = Time::now();
194 SessionStats::Ptr stats(new SessionStats());
195
196 // 127.0.0.1:1 -- nothing listening
197 RemoteList::Ptr rl(new RemoteList("127.0.0.1", "1", Protocol(Protocol::UDPv4), "test"));
198 rl->get_item(0)->set_ip_addr(IP::Addr::from_string("127.0.0.1"));
199
200 auto prober = std::make_shared<ServerProber>(io,
201 rl,
202 make_plain_config(frame, rng, time),
203 nullptr,
204 stats,
205 Time::Duration::milliseconds(200));
206
207 Collector cb;
208 prober->start(&cb);
209 io.run();
210
211 ASSERT_TRUE(cb.done);
212 EXPECT_EQ(cb.results.size(), 0u);
213}
214
215// Nothing resolved -> the callback still fires (immediately), with no results.
216TEST(ServerProber, empty_remote_list_completes)
217{
218 openvpn_io::io_context io(1);
219 Frame::Ptr frame(new Frame(Frame::Context(512, 2048, 512, 0, 16, BufAllocFlags::NO_FLAGS)));
221 Time time = Time::now();
222 SessionStats::Ptr stats(new SessionStats());
223
224 // resolved list intentionally left empty (no set_ip_addr)
225 RemoteList::Ptr rl(new RemoteList("127.0.0.1", "1194", Protocol(Protocol::UDPv4), "test"));
226
227 auto prober = std::make_shared<ServerProber>(io,
228 rl,
229 make_plain_config(frame, rng, time),
230 nullptr,
231 stats,
232 Time::Duration::milliseconds(200));
233
234 Collector cb;
235 prober->start(&cb);
236 io.run();
237
238 ASSERT_TRUE(cb.done);
239 EXPECT_EQ(cb.results.size(), 0u);
240}
241
242namespace {
243
244// A server that answers every probe with a runt datagram, i.e. the malformed
245// input a hostile or broken peer can put on the wire.
246class FakeRuntServer
247{
248 public:
249 explicit FakeRuntServer(openvpn_io::io_context &io)
250 : socket(io)
251 {
252 socket.open(openvpn_io::ip::udp::v4());
253 socket.bind(openvpn_io::ip::udp::endpoint(openvpn_io::ip::make_address("127.0.0.1"), 0));
254 }
255
256 unsigned short port() const
257 {
258 return socket.local_endpoint().port();
259 }
260
261 void start()
262 {
263 do_recv();
264 }
265
266 void stop()
267 {
268 openvpn_io::error_code ec;
269 socket.close(ec);
270 }
271
272 private:
273 struct RecvCtx
274 {
275 BufferAllocated buf;
276 openvpn_io::ip::udp::endpoint sender;
277 };
278
279 void do_recv()
280 {
281 auto rc = std::make_shared<RecvCtx>();
282 rc->buf.reset(0, 1600, BufAllocFlags::NO_FLAGS);
283 socket.async_receive_from(rc->buf.mutable_buffer(), rc->sender, [this, rc](const openvpn_io::error_code &error, const size_t n)
284 {
285 if (error || !n)
286 return;
287 // 3 bytes: an opcode and not much else
288 static const unsigned char runt[3] = {0x60, 0x01, 0x02};
289 openvpn_io::error_code ec;
290 socket.send_to(openvpn_io::buffer(runt, sizeof(runt)), rc->sender, 0, ec); });
291 }
292
293 openvpn_io::ip::udp::socket socket;
294};
295
296} // namespace
297
298// A truncated reply must be dropped without disturbing the probe: the window
299// still closes normally and the prober reports no responder. Before the header
300// length check this threw out of the asio receive handler.
301TEST(ServerProber, truncated_reply_is_dropped)
302{
303 openvpn_io::io_context io(1);
304 Frame::Ptr frame(new Frame(Frame::Context(512, 2048, 512, 0, 16, BufAllocFlags::NO_FLAGS)));
306 Time time = Time::now();
307 SessionStats::Ptr stats(new SessionStats());
308
309 FakeRuntServer server(io);
310 server.start();
311
312 RemoteList::Ptr rl(new RemoteList("127.0.0.1", std::to_string(server.port()), Protocol(Protocol::UDPv4), "test"));
313 rl->get_item(0)->set_ip_addr(IP::Addr::from_string("127.0.0.1"));
314
315 auto prober = std::make_shared<ServerProber>(io,
316 rl,
317 make_plain_config(frame, rng, time),
318 nullptr,
319 stats,
320 Time::Duration::milliseconds(300));
321
322 Collector cb;
323 prober->start(&cb);
324 io.run(); // must not propagate an exception
325
326 ASSERT_TRUE(cb.done);
327 EXPECT_EQ(cb.results.size(), 0u);
328
329 server.stop();
330}
void reset(const size_t min_capacity, const BufferFlags flags=BufAllocFlags::NO_FLAGS)
Resets the buffer with the specified minimum capacity and flags.
Definition buffer.hpp:1773
openvpn_io::const_buffer const_buffer() const
Return an openvpn_io::const_buffer object used by asio write methods.
Definition buffer.hpp:1311
static Addr from_string(const std::string &ipstr, const TITLE &title, const Version required_version)
Definition ip.hpp:105
std::string to_string() const
Definition ip.hpp:528
Wrap an out-of-band SERVER_PROBE / unwrap the matching PROBE_REPLY.
Definition proto.hpp:1996
RCPtr< ProtoConfig > Ptr
Definition proto.hpp:365
Item::Ptr get_item(const size_t index) const
std::vector< Result > results
static TimeType now()
Definition time.hpp:302
void work(openvpn_io::io_context &io_context, ThreadCommon &tc, MyRunContext &runctx, const unsigned int unit)
constexpr BufferFlags NO_FLAGS(0U)
no flags set
bool client_reply_write(Buffer &buf, const ProbeReply &reply)
Write a complete PROBE_REPLY (message header + probe_reply TLV). Server.
std::optional< ProbeParameter > server_probe_read(Buffer &buf)
auto port(const bool valid=true) -> Gen< int >
Generates a port number value.
virtual void server_probe_done(std::vector< Result > results)=0
called once when the probe window closes, with every reply collected
One responding server, with its measured latency and advertised parameters.
Time::Duration rtt
measured probe round-trip time
size_t remote_index
index of the remote in the RemoteList
unsigned short port
the port that answered
oob::ProbeReply reply
priority / weight / connect_lifetime / flags
IP::Addr addr
the address that answered
probe reply TLV (sent by the server in a PROBE_REPLY).
ProtoSessionID peer_session_id
echoes the session id of the request
std::uint16_t weight
DNS-SRV style weight.
std::uint16_t connect_lifetime
seconds the reply is valid as a handshake shortcut
std::uint16_t priority
DNS-SRV style priority (lower preferred)
TEST(ServerProber, plaintext_probe_roundtrip)