VLink  2.0.0
A high-performance communication middleware
proxy_server.h
Go to the documentation of this file.
1 /*
2  * Copyright (C) 2026 by Thun Lu. All rights reserved.
3  * Author: Thun Lu <thun.lu@zohomail.cn>
4  * Repo: https://github.com/thun-res/vlink
5  * _ __ __ _ __
6  * | | / / / / (_) ____ / /__
7  * | | / / / / / / / __ \ / //_/
8  * | |/ / / /___ / / / / / / / ,<
9  * |___/ /_____/ /_/ /_/ /_/ /_/|_|
10  *
11  * Licensed under the Apache License, Version 2.0 (the "License");
12  * you may not use this file except in compliance with the License.
13  * You may obtain a copy of the License at
14  *
15  * http://www.apache.org/licenses/LICENSE-2.0
16  *
17  * Unless required by applicable law or agreed to in writing, software
18  * distributed under the License is distributed on an "AS IS" BASIS,
19  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20  * See the License for the specific language governing permissions and
21  * limitations under the License.
22  */
23 
24 /**
25  * @file proxy_server.h
26  * @brief Daemon-side half of the VLink proxy subsystem -- one server per process.
27  *
28  * @details
29  * @c ProxyServer is the in-process daemon that mediates between the VLink core (publishers,
30  * subscribers, setters, getters on the local DDS domain) and one or more @c ProxyAPI clients.
31  * It derives from @c MessageLoop, so once @c async_run() is called all scheduled work,
32  * timers, and asynchronous relays run inside the inherited loop's worker thread.
33  *
34  * Behaviourally the server is responsible for the following duties:
35  *
36  * -# Hosts an internal @c DiscoveryViewer that enumerates every active publisher and
37  * subscriber on the DDS domain.
38  * -# Accepts @c Control messages from @c ProxyAPI clients over a security-authenticated
39  * DDS channel and dispatches them to the discovery layer.
40  * -# Broadcasts a 1-second @c Time heartbeat carrying CPU/memory usage, the VLink version
41  * string, hostname, machine ID, and wall-clock plus boot-time.
42  * -# Publishes per-topic statistics -- @c freq, @c rate, @c loss, @c latency -- once per
43  * second through a security-authenticated @c InfoList channel.
44  * -# Relays raw payload bytes from observed publishers and setters back to connected
45  * @c ProxyAPI listeners. In direct mode the data pubs/subs are created by the API
46  * side and the server stays exclusively on the control/discovery path; setter
47  * endpoints are observed with getter semantics so field last-value delivery is
48  * preserved.
49  * -# Optionally launches an embedded Iceoryx RouDi daemon when @c Config::use_iox is
50  * @c true.
51  * -# Loads @c RunablePluginInterface shared-library plugins listed in
52  * @c Config::runnable_list at construction and drives their lifecycle from the
53  * inherited @c MessageLoop callbacks.
54  *
55  * @par Singleton Constraint
56  * Only one @c ProxyServer may exist per operating-system process. A second
57  * construction logs a fatal message and throws before any DDS channels are wired up;
58  * running multiple servers in the same process is not supported and not safe.
59  *
60  * @par Architecture Diagram
61  * @code
62  * +-----------------------+ +---------------------+
63  * | ProxyAPI (Controller) | | ProxyAPI (Listener) |
64  * +-----------+-----------+ +----------+----------+
65  * | |
66  * | HandshakeCli ---[DDS secure]---> HandshakeSrv (issues token)
67  * |<--------------- token --------------|
68  * | ControlPub ---[DDS secure]---> ControlSub (token-stamped)
69  * v v
70  * +-------------------------------------------------------------+
71  * | ProxyServer (this) |
72  * | +-------------------+ +-------------------------------+ |
73  * | | DiscoveryViewer | | Heartbeat / Info / Data path | |
74  * | +-------------------+ +-------------------------------+ |
75  * | ^ | | |
76  * +-------|--------------------------|----------|---------------+
77  * | v v
78  * [VLink core nodes] TimePub / InfoPub ---[DDS secure]--->
79  * DataPub ---[DDS / SHM]--->
80  * @endcode
81  *
82  * @par Authentication Token
83  * At construction the server generates a 128-bit hex session token via
84  * @c vlink::Uuid::random_hex() and exposes it through @c get_token(). The token is a
85  * process-lifetime protocol nonce, @b not a long-term cryptographic key. Clients fetch
86  * it by invoking the handshake RPC over the security-authenticated DDS channel;
87  * subsequent @c Control messages without a matching token are dropped server-side.
88  * Restarting the server invalidates every previously-issued token. Clients re-handshake
89  * after a same-identity token mismatch heartbeat, an identity-mismatch heartbeat that
90  * also carries a different token, or a local reset, before they will accept further
91  * heartbeats or publish new controls.
92  *
93  * @par Runnable Plugin Lifecycle
94  * Plugins named in @c Config::runnable_list are loaded inside the constructor. When
95  * the inherited @c MessageLoop starts (@c on_begin), each plugin's @c on_init() and
96  * @c async_run() are invoked in list order. On loop stop (@c on_end) each plugin's
97  * @c on_deinit(), @c quit(), and @c wait_for_quit() are invoked in the same order.
98  *
99  * @par Environment Variables
100  * | Variable | Meaning |
101  * | -------------------- | ------------------------------------------------------------- |
102  * | @c VLINK_INTRA_BIND | When set to any value, also subscribe to @c intra:// topics. |
103  *
104  * @par Example
105  * @code
106  * vlink::ProxyServer::Config cfg;
107  * cfg.dds_impl = "dds";
108  * cfg.domain_id = 0;
109  * cfg.reliable = false;
110  * cfg.async = true;
111  * cfg.use_iox = false;
112  *
113  * vlink::ProxyServer server(cfg);
114  * server.async_run(); // start the inherited MessageLoop in a background thread
115  * // ... application runs ...
116  * server.quit(true);
117  * server.wait_for_quit();
118  * @endcode
119  *
120  * @note
121  * - Construction must happen on the main thread, before any @c ProxyAPI client connects
122  * on the same domain.
123  * - Destruction stops every timer, joins the @c DiscoveryViewer, and releases each DDS
124  * handle in a deterministic order so no dangling callback can fire after teardown.
125  */
126 
127 #pragma once
128 
129 #undef VLINK_PROXY_SERVER_EXPORT
130 #ifdef VLINK_PROXY_SERVER_LIBRARY_STATIC
131 #define VLINK_PROXY_SERVER_EXPORT
132 #elif defined(_WIN32) || defined(__CYGWIN__)
133 #ifdef VLINK_PROXY_SERVER_LIBRARY
134 #define VLINK_PROXY_SERVER_EXPORT __declspec(dllexport)
135 #else
136 #define VLINK_PROXY_SERVER_EXPORT __declspec(dllimport)
137 #endif
138 #else
139 #define VLINK_PROXY_SERVER_EXPORT __attribute__((visibility("default")))
140 #endif
141 
142 #include <cstdint>
143 #include <memory>
144 #include <string>
145 #include <vector>
146 
147 #include "../base/message_loop.h"
148 
149 namespace vlink {
150 
151 /**
152  * @class ProxyServer
153  * @brief In-process VLink proxy daemon backed by a @c MessageLoop.
154  *
155  * @details
156  * Owns the discovery layer, the handshake/control/time/info DDS channels, the data
157  * relay path, and any embedded Iceoryx daemon or runnable plugins. Construction
158  * starts the @c DiscoveryViewer plus its 1-second heartbeat and statistics timers,
159  * but it does @b not start the server's own inherited loop. Call @c async_run()
160  * (or @c run()) on the @c MessageLoop base to enable asynchronous relays and the
161  * runnable-plugin lifecycle hooks. Only one instance is allowed per process.
162  */
164  public:
165  /**
166  * @struct Config
167  * @brief Construction-time configuration aggregate for @c ProxyServer.
168  *
169  * @details
170  * Every @c reliable, @c enable_tcp, and @c direct flag is echoed inside each @c Time
171  * heartbeat, so connecting @c ProxyAPI clients can verify wire-compatibility and
172  * refuse to attach when their own configuration would deviate.
173  *
174  * | Field | Default | Description |
175  * | -------------------------- | ------- | ----------------------------------------------------------------- |
176  * | @c async | false | Forward data on the MessageLoop thread; false = inline relay. |
177  * | @c reliable | false | Use reliable DDS QoS for data channels. |
178  * | @c enable_tcp | false | Use TCP transport for data channels. |
179  * | @c direct | false | Use SHM (Iceoryx) instead of DDS for data forwarding. |
180  * | @c native_mode | false | Restrict all DDS traffic to 127.0.0.1 (loopback). |
181  * | @c domain_id | 0 | DDS domain ID shared with all clients. |
182  * | @c buf_size | 0 | DDS socket send/receive buffer in bytes; 0 = built-in default. |
183  * | @c mtu_size | 0 | DDS MTU size in bytes; 0 = built-in default. |
184  * | @c max_packet_size | 0 | Maximum relayed payload in MiB; see note below. |
185  * | @c security_key | "" | Security key for control channels; empty = default slot. |
186  * | @c bind_ip | "" | Bind DDS sockets to this IP; empty = any interface. |
187  * | @c peer_ip | "" | Unicast peer IP for discovery; empty = multicast. |
188  * | @c dds_impl | "dds" | DDS implementation: "dds", "ddsc", "ddsr", etc. |
189  * | @c use_iox | false | Launch an embedded Iceoryx RouDi daemon at startup. |
190  * | @c iox_monitoring | true | Enable Iceoryx introspection/monitoring. |
191  * | @c iox_strategy | 1 | Iceoryx memory strategy index passed to @c ShmConf::init_roudi(). |
192  * | @c iox_config | "" | Path to a custom Iceoryx TOML; empty = default. |
193  * | @c runnable_version_major | 1 | Required major ABI version for runnable plugins. |
194  * | @c runnable_version_minor | 0 | Required minor ABI version for runnable plugins. |
195  * | @c runnable_prefix | "" | Library filename prefix for plugin discovery. |
196  * | @c runnable_list | {} | Ordered names of @c RunablePluginInterface plugins to load. |
197  *
198  * @note
199  * @c max_packet_size is interpreted in MiB. The default value @c 0 drops every
200  * non-empty message -- there is no special case in the implementation. Set it to
201  * a positive number to forward larger packets. The @c vlink-proxy CLI defaults
202  * this field to @c 4.0.
203  */
204  struct Config final {
205  bool async{false}; ///< Forward data asynchronously on the MessageLoop thread.
206  bool reliable{false}; ///< Use reliable DDS QoS; must match every client.
207  bool enable_tcp{false}; ///< Use TCP transport for DDS data channels.
208  bool direct{false}; ///< Use ProxyAPI-managed local SHM channels for data.
209  bool native_mode{false}; ///< Restrict every DDS endpoint to loopback (127.0.0.1).
210  int domain_id{0}; ///< DDS domain ID.
211  uint32_t buf_size{0}; ///< DDS socket buffer in bytes; 0 = default.
212  uint32_t mtu_size{0}; ///< DDS fragment MTU in bytes; 0 = default.
213  double max_packet_size{
214  0}; ///< Maximum relayed payload in MiB; 0 drops every non-empty message (set > 0 to forward).
215  std::string security_key; ///< Security key; empty = default security slot.
216  std::string bind_ip; ///< Local IP for DDS sockets; empty = any.
217  std::string peer_ip; ///< Peer unicast IP for DDS; empty = multicast.
218  std::string dds_impl{"dds"}; ///< DDS implementation transport identifier.
219  bool use_iox{false}; ///< Launch an embedded Iceoryx RouDi.
220  bool iox_monitoring{true}; ///< Enable Iceoryx introspection.
221  int iox_strategy{1}; ///< Iceoryx memory allocation strategy index.
222  std::string iox_config; ///< Iceoryx TOML config path; empty = default.
223  uint16_t runnable_version_major{1}; ///< Required major ABI version for plugins.
224  uint16_t runnable_version_minor{0}; ///< Required minor ABI version for plugins.
225  std::string runnable_prefix; ///< Plugin library filename prefix.
226  std::vector<std::string> runnable_list; ///< Ordered plugin names to load on startup.
227  };
228 
229  /**
230  * @brief Constructs a @c ProxyServer and brings every proxy subsystem online.
231  *
232  * @details
233  * The constructor performs the following steps in order:
234  *
235  * -# Acquires the process-global singleton guard; on contention it logs a fatal
236  * message and throws before touching any DDS handle.
237  * -# Reads the @c VLINK_INTRA_BIND environment variable.
238  * -# When @c config.use_iox is @c true, calls @c init_shm_roudi() to spin up an
239  * embedded Iceoryx RouDi process.
240  * -# Calls @c init_server() to create the handshake, control, time, info, and data
241  * channels, subscribe to @c Control, and start the heartbeat plus statistics
242  * timers on the @c DiscoveryViewer's loop.
243  * -# Calls @c init_runnable() to load every plugin listed in @c config.runnable_list.
244  *
245  * The inherited @c MessageLoop is @b not started here -- call @c async_run() or
246  * @c run() explicitly when asynchronous relays and plugin lifecycle hooks must run.
247  *
248  * @param config Server configuration. See @c Config for per-field semantics.
249  *
250  * @note A second @c ProxyServer in the same process is unsupported and the
251  * constructor throws.
252  */
253  explicit ProxyServer(const Config& config);
254 
255  /**
256  * @brief Destroys the @c ProxyServer in a deterministic shutdown order.
257  *
258  * @details
259  * Requests the inherited @c MessageLoop to stop, waits for it, then stops the proxy
260  * timers, joins the @c DiscoveryViewer, and releases each DDS/SHM handle. When the
261  * loop has been started, runnable plugins receive @c on_deinit(), @c quit(), and
262  * @c wait_for_quit() from @c on_end() before the destructor drops them. The
263  * process-global singleton guard remains set for the rest of the process lifetime.
264  */
265  ~ProxyServer() override;
266 
267  /**
268  * @brief Returns the authentication token issued by this server.
269  *
270  * @details
271  * When @c VLINK_PROXY_ENABLE_HANDSHAKE is non-zero (the default), the token is
272  * generated once at construction via @c vlink::Uuid::random_hex() and remains
273  * constant for the server's lifetime. Clients learn it through the
274  * security-authenticated handshake RPC. The server then validates the token on
275  * every inbound @c Control and echoes it (alongside the server identity) inside
276  * every @c Time heartbeat so clients can detect both server restarts and identity
277  * mismatches. When the macro is @c 0, the token is empty, validation is disabled,
278  * and the handshake channel is not created.
279  *
280  * @return Hex-encoded token string, or empty when handshake is compiled out.
281  *
282  * @note Thread-safe; the returned string is a copy.
283  */
284  [[nodiscard]] std::string get_token() const;
285 
286  protected:
287  size_t get_max_task_count() const override;
288 
289  uint32_t get_max_elapsed_time() const override;
290 
291  void on_begin() override;
292 
293  void on_end() override;
294 
295  private:
296  void init_shm_roudi();
297 
298  void init_server();
299 
300  void init_runnable();
301 
302  void send_time();
303 
304  void send_control(const void* control_data);
305 
306  void update_all();
307 
308  struct Impl;
309  std::unique_ptr<Impl> impl_;
310 
312 };
313 
314 } // namespace vlink
#define VLINK_DISALLOW_COPY_AND_ASSIGN(classname)
Deletes the copy constructor and copy-assignment operator of classname.
Definition: macros.h:174
#define VLINK_PROXY_SERVER_EXPORT
Definition: proxy_server.h:139