OpenVPN 3 Core Library
Loading...
Searching...
No Matches
serverprober.hpp
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
29#ifndef OPENVPN_CLIENT_SERVERPROBER_H
30#define OPENVPN_CLIENT_SERVERPROBER_H
31
32#include <map>
33#include <memory>
34#include <utility>
35#include <vector>
36
37#include <openvpn/io/io.hpp>
38
39#include <openvpn/addr/ip.hpp>
42#include <openvpn/time/time.hpp>
46#include <openvpn/ssl/proto.hpp>
48
49namespace openvpn {
50
51class ServerProber : public std::enable_shared_from_this<ServerProber>
52{
53 public:
54 using UDPSocket = openvpn_io::ip::udp::socket;
55 using UDPEndpoint = openvpn_io::ip::udp::endpoint;
56
58 struct Result
59 {
60 size_t remote_index = 0;
62 unsigned short port = 0;
63 Time::Duration rtt;
65 };
66
68 {
69 virtual ~NotifyCallback() = default;
71 virtual void server_probe_done(std::vector<Result> results) = 0;
72 };
73
77 ServerProber(openvpn_io::io_context &io_context_arg,
78 RemoteList::Ptr remote_list_arg,
79 ProtoContext::ProtoConfig::Ptr proto_config_arg,
80 SocketProtect *socket_protect_arg,
81 SessionStats::Ptr stats_arg,
82 const Time::Duration &probe_window_arg)
83 : io_context(io_context_arg),
84 remote_list(std::move(remote_list_arg)),
85 proto_config(std::move(proto_config_arg)),
86 socket_protect(socket_protect_arg),
87 stats(std::move(stats_arg)),
88 probe_window(probe_window_arg),
89 timer(io_context_arg),
91 {
92 }
93
96 {
97 stop();
98 }
99
105 {
106 notify_callback = cb;
107
108 std::vector<Target> targets;
109 gather_targets(targets);
110 if (targets.empty())
111 {
112 finish();
113 return;
114 }
115
116 for (const auto &t : targets)
117 send_probe(t);
118
120 timer.async_wait([self = shared_from_this()](const openvpn_io::error_code &error)
121 {
122 if (!error)
123 self->finish(); });
124 }
125
127 void stop()
128 {
129 if (halt)
130 return;
131 halt = true;
132 openvpn_io::error_code ec;
133 timer.cancel();
134 if (sock_v4)
135 sock_v4->cancel(ec);
136 if (sock_v6)
137 sock_v6->cancel(ec);
138 }
139
150 std::unique_ptr<UDPSocket> release_socket(const IP::Addr::Version v)
151 {
153 return nullptr;
154
155 std::unique_ptr<UDPSocket> &slot = (v == IP::Addr::Version::V4) ? sock_v4 : sock_v6;
156 if (slot)
157 {
158 openvpn_io::error_code ec;
159 slot->cancel(ec);
160 }
161 return std::move(slot);
162 }
163
164 private:
165 struct Target
166 {
167 size_t index;
169 };
170
171 struct Pending
172 {
173 size_t index;
175 };
176
182
184 void gather_targets(std::vector<Target> &targets)
185 {
186 if (!remote_list)
187 return;
188 for (size_t i = 0; i < remote_list->size(); ++i)
189 {
191 if (!item || !item->transport_protocol.is_udp())
192 continue;
193 try
194 {
195 UDPEndpoint ep;
196 for (size_t j = 0; item->get_endpoint(ep, j); ++j)
197 targets.push_back({i, ep});
198 }
199 catch (const std::exception &)
200 {
201 continue; // unparsable port: skip the whole remote
202 }
203 }
204 }
205
208 {
209 const bool v4 = target.address().is_v4();
210 std::unique_ptr<UDPSocket> &slot = v4 ? sock_v4 : sock_v6;
211 const openvpn_io::ip::udp proto = v4 ? openvpn_io::ip::udp::v4() : openvpn_io::ip::udp::v6();
212
213 if (!slot)
214 {
215 auto s = std::make_unique<UDPSocket>(io_context);
216 openvpn_io::error_code ec;
217 s->open(proto, ec);
218 if (ec)
219 {
220 OPENVPN_LOG("ServerProber: socket open failed: " << ec.message());
221 return nullptr;
222 }
224 && !socket_protect->socket_protect(s->native_handle(),
225 IP::Addr::from_asio(target.address())))
226 OPENVPN_LOG("ServerProber: socket_protect failed (continuing)");
227 slot = std::move(s);
228 queue_recv(v4);
229 }
230 return slot.get();
231 }
232
234 void send_probe(const Target &t)
235 {
236 UDPSocket *s = socket_for(t.ep);
237 if (!s)
238 return;
239
240 // encode the SERVER_PROBE payload, then wrap it with the control-channel
241 // protection so a standard server accepts it
242 BufferAllocated buf;
243 buf.reset(512, 2048, BufAllocFlags::NO_FLAGS);
244 const oob::ProbeParameter param{
245 .timestamp = static_cast<std::uint64_t>(Time::now().seconds_since_epoch()),
246 .flags = 0};
247 if (!oob::server_probe_write(buf, param))
248 return;
250 probe_wrap.wrap(buf, work);
251
252 pending[t.ep] = Pending{t.index, Time::now()};
253
254 openvpn_io::error_code ec;
255 s->send_to(buf.const_buffer(), t.ep, 0, ec);
256 if (ec)
257 {
258 OPENVPN_LOG("ServerProber: send to " << t.ep << " failed: " << ec.message());
259 pending.erase(t.ep);
260 }
261 }
262
265 void queue_recv(const bool v4)
266 {
267 std::unique_ptr<UDPSocket> &slot = v4 ? sock_v4 : sock_v6;
268 if (halt || !slot || !slot->is_open())
269 return;
270 auto rc = std::make_shared<RecvCtx>();
271 rc->buf.reset(0, 1600, BufAllocFlags::NO_FLAGS);
272 slot->async_receive_from(rc->buf.mutable_buffer(), rc->sender, [self = shared_from_this(), v4, rc](const openvpn_io::error_code &error, const size_t bytes_recvd) mutable
273 {
274 if (self->halt)
275 return;
276 if (!error && bytes_recvd)
277 {
278 rc->buf.set_size(bytes_recvd);
279 // Boundary guard: nothing may escape an asio handler -- it
280 // would unwind out of io_context::run() and kill the connect.
281 // Broad by intent: untrusted input parsed by buffer/crypto
282 // code whose exception types vary by SSL backend.
283 try
284 {
285 self->handle_reply(rc->sender, rc->buf);
286 }
287 catch (const std::exception &e)
288 {
289 self->stats->error(Error::CC_ERROR);
290 OPENVPN_LOG("ServerProber: exception processing reply from "
291 << rc->sender << ": " << e.what());
292 }
293 }
294 // re-arm unless the error is a cancel/close
295 if (!self->halt && error != openvpn_io::error::operation_aborted)
296 self->queue_recv(v4); });
297 }
298
300 void handle_reply(const UDPEndpoint &sender, BufferAllocated &buf)
301 {
302 auto it = pending.find(sender);
303 if (it == pending.end())
304 return; // unsolicited or already recorded
305 const Time::Duration rtt = Time::now() - it->second.sent;
306
308 ProtoSessionID src;
309 PacketIDControl pid;
311 return;
312
313 const auto reply = oob::client_reply_read(buf);
314 if (!reply)
315 return;
316 // the reply must echo the session id of our probe
317 if (!reply->peer_session_id.match(probe_wrap.self_psid()))
318 return;
319
320 results.push_back(Result{.remote_index = it->second.index,
321 .addr = IP::Addr::from_asio(it->first.address()),
322 .port = it->first.port(),
323 .rtt = rtt,
324 .reply = *reply});
325
326 pending.erase(it); // one result per endpoint
327 }
328
331 void finish()
332 {
333 if (finished || halt)
334 return;
335 finished = true;
336 stop(); // cancels timer + outstanding receives, leaves sockets open
337
339 notify_callback = nullptr;
340 if (cb)
341 cb->server_probe_done(std::move(results));
342 }
343
344 openvpn_io::io_context &io_context;
349 Time::Duration probe_window;
350
353
354 std::unique_ptr<UDPSocket> sock_v4;
355 std::unique_ptr<UDPSocket> sock_v6;
356
357 std::map<UDPEndpoint, Pending> pending;
358 std::vector<Result> results;
359
361 bool halt = false;
362 bool finished = false;
363};
364
365} // namespace openvpn
366
367#endif
std::size_t expires_after(const Time::Duration &d)
Definition asiotimer.hpp:69
virtual bool socket_protect(openvpn_io::detail::socket_type socket, IP::Addr endpoint)=0
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_asio(const openvpn_io::ip::address &addr)
Definition ip.hpp:576
Wrap an out-of-band SERVER_PROBE / unwrap the matching PROBE_REPLY.
Definition proto.hpp:1996
UnwrapStatus unwrap(BufferAllocated &recv, BufferAllocated &work, ProtoSessionID &src_psid, PacketIDControl &pid)
Unwrap a received PROBE_REPLY in place.
Definition proto.hpp:2069
const ProtoSessionID & self_psid() const
the client session id carried in the probe (echoed back in the reply)
Definition proto.hpp:2029
void wrap(BufferAllocated &buf, BufferAllocated &work)
Wrap an already-encoded SERVER_PROBE payload in place.
Definition proto.hpp:2040
Item::Ptr get_item(const size_t index) const
size_t size() const
void queue_recv(const bool v4)
void handle_reply(const UDPEndpoint &sender, BufferAllocated &buf)
Unwrap a PROBE_REPLY and record a Result, ignoring anything unsolicited.
NotifyCallback * notify_callback
std::unique_ptr< UDPSocket > release_socket(const IP::Addr::Version v)
Hand off the probe socket for an address family so it can be adopted as the connection socket (source...
std::unique_ptr< UDPSocket > sock_v6
std::unique_ptr< UDPSocket > sock_v4
~ServerProber()
Cancels any in-flight probe; see stop().
Time::Duration probe_window
void gather_targets(std::vector< Target > &targets)
Collect every resolved UDP endpoint from the remote list.
ProtoContext::ProbeWrap probe_wrap
void send_probe(const Target &t)
Wrap and send one SERVER_PROBE, recording the endpoint as pending.
openvpn_io::ip::udp::endpoint UDPEndpoint
void stop()
Cancel probing without invoking the callback. Sockets stay open (transferable).
UDPSocket * socket_for(const UDPEndpoint &target)
Lazily open (and socket_protect) the per-family socket, arming its receive.
void start(NotifyCallback *cb)
Begin probing. cb->server_probe_done() fires exactly once, when the probe window elapses (or immediat...
RemoteList::Ptr remote_list
ServerProber(openvpn_io::io_context &io_context_arg, RemoteList::Ptr remote_list_arg, ProtoContext::ProtoConfig::Ptr proto_config_arg, SocketProtect *socket_protect_arg, SessionStats::Ptr stats_arg, const Time::Duration &probe_window_arg)
openvpn_io::ip::udp::socket UDPSocket
openvpn_io::io_context & io_context
ProtoContext::ProtoConfig::Ptr proto_config
SocketProtect * socket_protect
std::map< UDPEndpoint, Pending > pending
SessionStats::Ptr stats
std::vector< Result > results
static TimeType now()
Definition time.hpp:302
base_type seconds_since_epoch() const
Definition time.hpp:289
#define OPENVPN_LOG(args)
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 server_probe_write(Buffer &buf, const ProbeParameter &param)
Write a complete SERVER_PROBE (message header + probe_parameter TLV). Client.
std::optional< ProbeReply > client_reply_read(Buffer &buf)
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 parameter TLV (sent by the client in a SERVER_PROBE).
std::uint64_t timestamp
client clock as a UNIX timestamp
probe reply TLV (sent by the server in a PROBE_REPLY).