Line data Source code
1 : /*
2 : * Copyright (C) 2004-2026 Savoir-faire Linux Inc.
3 : *
4 : * This program is free software: you can redistribute it and/or modify
5 : * it under the terms of the GNU General Public License as published by
6 : * the Free Software Foundation, either version 3 of the License, or
7 : * (at your option) any later version.
8 : *
9 : * This program is distributed in the hope that it will be useful,
10 : * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 : * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 : * GNU General Public License for more details.
13 : *
14 : * You should have received a copy of the GNU General Public License
15 : * along with this program. If not, see <https://www.gnu.org/licenses/>.
16 : */
17 : #include "jamidht/svc_tunnel_channel_handler.h"
18 :
19 : #include "jamidht/account_manager.h"
20 : #include "jamidht/contact_list.h"
21 : #include "jamidht/service_manager.h"
22 : #include "jamidht/svc_protocol.h"
23 : #include "logger.h"
24 : #include "manager.h"
25 :
26 : #include <asio/connect.hpp>
27 : #include <asio/post.hpp>
28 : #include <asio/read.hpp>
29 : #include <asio/write.hpp>
30 :
31 : #include <atomic>
32 :
33 : namespace jami {
34 :
35 : namespace {
36 : constexpr size_t kRelayBufSize = 16 * 1024;
37 : }
38 :
39 : struct SvcTunnelChannelHandler::ClientTunnel
40 : {
41 : std::string id;
42 : std::string peerUri;
43 : DeviceId peerDevice;
44 : std::string serviceId;
45 : std::string serviceName;
46 : std::shared_ptr<asio::ip::tcp::acceptor> acceptor;
47 : OnTunnelClosed onClosed;
48 : std::atomic_bool closed {false};
49 : /// Per-tunnel mutex protecting the active-connection list.
50 : std::mutex connsMtx;
51 : struct Conn
52 : {
53 : std::weak_ptr<dhtnet::ChannelSocket> channel;
54 : std::weak_ptr<asio::ip::tcp::socket> tcp;
55 : };
56 : std::vector<Conn> activeConns;
57 : };
58 :
59 672 : SvcTunnelChannelHandler::SvcTunnelChannelHandler(const std::shared_ptr<JamiAccount>& acc,
60 : dhtnet::ConnectionManager& cm,
61 672 : std::shared_ptr<asio::io_context> io)
62 : : ChannelHandlerInterface()
63 672 : , account_(acc)
64 672 : , connectionManager_(cm)
65 672 : , io_(std::move(io))
66 1344 : , rng_(dht::crypto::getDerivedRandomEngine(acc->rand))
67 672 : {}
68 :
69 1344 : SvcTunnelChannelHandler::~SvcTunnelChannelHandler()
70 : {
71 672 : std::vector<std::shared_ptr<ClientTunnel>> snapshot;
72 : {
73 672 : std::lock_guard lk(mtx_);
74 672 : for (auto& [_id, t] : tunnels_)
75 0 : snapshot.push_back(t);
76 672 : tunnels_.clear();
77 672 : }
78 672 : for (auto& t : snapshot) {
79 0 : t->closed = true;
80 0 : std::error_code ec;
81 0 : if (t->acceptor)
82 0 : t->acceptor->close(ec);
83 : }
84 1344 : }
85 :
86 : std::string
87 0 : SvcTunnelChannelHandler::parseServiceId(const std::string& channelName)
88 : {
89 0 : constexpr std::string_view prefix = "svc://";
90 0 : if (channelName.size() <= prefix.size())
91 0 : return {};
92 0 : if (channelName.compare(0, prefix.size(), prefix) != 0)
93 0 : return {};
94 0 : return channelName.substr(prefix.size());
95 : }
96 :
97 : void
98 0 : SvcTunnelChannelHandler::connect(const DeviceId& deviceId,
99 : const std::string& /*name*/,
100 : ConnectCb&& cb,
101 : const std::string& /*connectionType*/,
102 : bool /*forceNewConnection*/)
103 : {
104 : // Tunnels are managed at a higher level via openTunnel(); the generic
105 : // channel-handler entry-point is unused but must not block. Just signal
106 : // back asynchronously with a null socket so callers can fall through.
107 0 : if (cb && io_)
108 0 : asio::post(*io_, [cb = std::move(cb), deviceId]() mutable { cb(nullptr, deviceId); });
109 0 : }
110 :
111 : bool
112 0 : SvcTunnelChannelHandler::onRequest(const std::shared_ptr<dht::crypto::Certificate>& peer, const std::string& name)
113 : {
114 0 : if (!peer || !peer->issuer)
115 0 : return false;
116 0 : auto serviceId = parseServiceId(name);
117 0 : if (serviceId.empty())
118 0 : return false;
119 0 : auto acc = account_.lock();
120 0 : if (!acc)
121 0 : return false;
122 0 : const auto peerUri = peer->issuer->getId().toString();
123 0 : auto checker = [&acc](const std::string& uri) {
124 0 : return acc->isContact(uri);
125 0 : };
126 0 : return acc->serviceManager().isAuthorized(serviceId, peerUri, checker);
127 0 : }
128 :
129 : void
130 0 : SvcTunnelChannelHandler::onReady(const std::shared_ptr<dht::crypto::Certificate>& peer,
131 : const std::string& name,
132 : std::shared_ptr<dhtnet::ChannelSocket> channel)
133 : {
134 0 : if (!channel)
135 0 : return;
136 : // The initiator side wires its own onClientChannelReady callback via
137 : // connectDevice(). Only the receiving (server) side performs the local
138 : // TCP connect here.
139 0 : if (channel->isInitiator())
140 0 : return;
141 0 : auto serviceId = parseServiceId(name);
142 0 : auto acc = account_.lock();
143 0 : if (!acc || serviceId.empty()) {
144 0 : channel->shutdown();
145 0 : return;
146 : }
147 0 : auto rec = acc->serviceManager().getService(serviceId);
148 0 : if (!rec || !rec->enabled) {
149 0 : channel->shutdown();
150 0 : return;
151 : }
152 0 : if (!io_) {
153 0 : channel->shutdown();
154 0 : return;
155 : }
156 0 : JAMI_LOG("[SvcTunnel] peer {} opened tunnel to service \"{}\" -> {}:{}",
157 : peer && peer->issuer ? peer->issuer->getId().toString() : std::string("<unknown>"),
158 : rec->name,
159 : rec->localHost,
160 : rec->localPort);
161 :
162 0 : auto tcp = std::make_shared<asio::ip::tcp::socket>(*io_);
163 0 : asio::ip::tcp::resolver resolver(*io_);
164 0 : std::error_code ec;
165 0 : auto endpoints = resolver.resolve(rec->localHost, std::to_string(rec->localPort), ec);
166 0 : if (ec) {
167 0 : JAMI_WARNING("[SvcTunnel] resolve {}:{} failed: {}", rec->localHost, rec->localPort, ec.message());
168 0 : channel->shutdown();
169 0 : return;
170 : }
171 :
172 : // Buffer any bytes received from the remote peer before we have a local
173 : // TCP connection up; flush them through once async_connect succeeds.
174 : struct PreConnectBuf
175 : {
176 : std::mutex m;
177 : std::vector<uint8_t> bytes;
178 : bool tcpReady {false};
179 : std::shared_ptr<asio::ip::tcp::socket> tcp;
180 : };
181 0 : auto pre = std::make_shared<PreConnectBuf>();
182 0 : pre->tcp = tcp;
183 0 : auto channelKeep = channel;
184 0 : channel->setOnRecv([pre, channelKeep](const uint8_t* data, size_t size) -> ssize_t {
185 0 : std::lock_guard lk(pre->m);
186 0 : if (pre->tcpReady) {
187 0 : auto buf = std::make_shared<std::vector<uint8_t>>(data, data + size);
188 0 : asio::async_write(*pre->tcp,
189 0 : asio::buffer(*buf),
190 0 : [buf, pre, channelKeep](const std::error_code& ec, std::size_t) {
191 0 : if (ec) {
192 0 : JAMI_DEBUG("[SvcTunnel] write to TCP failed: {}", ec.message());
193 0 : channelKeep->shutdown();
194 0 : std::error_code ig;
195 0 : pre->tcp->close(ig);
196 : }
197 0 : });
198 0 : } else {
199 0 : pre->bytes.insert(pre->bytes.end(), data, data + size);
200 : }
201 0 : return static_cast<ssize_t>(size);
202 0 : });
203 :
204 0 : asio::async_connect(*tcp,
205 : endpoints,
206 0 : [this, channel = channelKeep, tcp, pre, serviceId](const std::error_code& cec,
207 : const asio::ip::tcp::endpoint&) {
208 0 : if (cec) {
209 0 : JAMI_WARNING("[SvcTunnel] connect failed: {}", cec.message());
210 0 : channel->shutdown();
211 0 : return;
212 : }
213 0 : trackServerChannel(serviceId, channel, tcp);
214 0 : std::vector<uint8_t> drained;
215 : {
216 0 : std::lock_guard lk(pre->m);
217 0 : pre->tcpReady = true;
218 0 : drained.swap(pre->bytes);
219 0 : }
220 0 : if (!drained.empty()) {
221 0 : auto buf = std::make_shared<std::vector<uint8_t>>(std::move(drained));
222 0 : asio::async_write(*tcp,
223 0 : asio::buffer(*buf),
224 0 : [buf, channel, tcp](const std::error_code& ec, std::size_t) {
225 0 : if (ec) {
226 0 : JAMI_DEBUG("[SvcTunnel] flush failed: {}", ec.message());
227 0 : channel->shutdown();
228 0 : std::error_code ig;
229 0 : tcp->close(ig);
230 : }
231 0 : });
232 0 : }
233 : // Switch to a hot-path setOnRecv now that tcp is up.
234 0 : channel->setOnRecv([tcp, channel](const uint8_t* data, size_t n) -> ssize_t {
235 0 : auto buf = std::make_shared<std::vector<uint8_t>>(data, data + n);
236 0 : asio::async_write(*tcp,
237 0 : asio::buffer(*buf),
238 0 : [buf, channel, tcp](const std::error_code& ec, std::size_t) {
239 0 : if (ec) {
240 0 : JAMI_DEBUG("[SvcTunnel] write to TCP failed: {}",
241 : ec.message());
242 0 : channel->shutdown();
243 0 : std::error_code ig;
244 0 : tcp->close(ig);
245 : }
246 0 : });
247 0 : return static_cast<ssize_t>(n);
248 0 : });
249 0 : relayTcpToChannel(channel, tcp);
250 0 : });
251 0 : }
252 :
253 : void
254 0 : SvcTunnelChannelHandler::relay(std::shared_ptr<dhtnet::ChannelSocket> channel,
255 : std::shared_ptr<asio::ip::tcp::socket> tcp)
256 : {
257 : // ChannelSocket -> TCP
258 0 : auto channelHold = channel;
259 0 : channel->setOnRecv([tcp, channelHold](const uint8_t* data, size_t size) -> ssize_t {
260 0 : auto buf = std::make_shared<std::vector<uint8_t>>(data, data + size);
261 0 : asio::async_write(*tcp,
262 0 : asio::buffer(*buf),
263 0 : [buf, channelHold, tcp](const std::error_code& ec, std::size_t /*n*/) {
264 0 : if (ec) {
265 0 : JAMI_DEBUG("[SvcTunnel] write to TCP failed: {}", ec.message());
266 0 : channelHold->shutdown();
267 0 : std::error_code ig;
268 0 : tcp->close(ig);
269 : }
270 0 : });
271 0 : return static_cast<ssize_t>(size);
272 0 : });
273 :
274 : // TCP -> ChannelSocket
275 0 : relayTcpToChannel(channel, tcp);
276 0 : }
277 :
278 : void
279 0 : SvcTunnelChannelHandler::relayTcpToChannel(std::shared_ptr<dhtnet::ChannelSocket> channel,
280 : std::shared_ptr<asio::ip::tcp::socket> tcp)
281 : {
282 0 : auto buf = std::make_shared<std::vector<uint8_t>>(kRelayBufSize);
283 0 : auto channelKeep = channel;
284 0 : auto tcpKeep = tcp;
285 0 : auto reader = std::make_shared<std::function<void()>>();
286 0 : *reader = [channelKeep, tcpKeep, buf, reader]() {
287 0 : tcpKeep->async_read_some(asio::buffer(*buf),
288 0 : [channelKeep, tcpKeep, buf, reader](const std::error_code& ec, std::size_t n) {
289 0 : if (ec || n == 0) {
290 0 : channelKeep->shutdown();
291 0 : std::error_code ig;
292 0 : tcpKeep->close(ig);
293 0 : *reader = nullptr; // break the cycle
294 0 : return;
295 : }
296 0 : std::error_code wec;
297 0 : channelKeep->write(buf->data(), n, wec);
298 0 : if (wec) {
299 0 : channelKeep->shutdown();
300 0 : std::error_code ig;
301 0 : tcpKeep->close(ig);
302 0 : *reader = nullptr; // break the cycle
303 0 : return;
304 : }
305 0 : (*reader)();
306 : });
307 0 : };
308 0 : (*reader)();
309 :
310 0 : channel->onShutdown([tcp = tcpKeep](const std::error_code&) {
311 0 : std::error_code ig;
312 0 : tcp->close(ig);
313 0 : });
314 0 : }
315 :
316 : std::string
317 0 : SvcTunnelChannelHandler::openTunnel(std::string peerUri,
318 : DeviceId peerDevice,
319 : std::string serviceId,
320 : std::string serviceName,
321 : uint16_t localPort,
322 : OnTunnelOpened onOpened,
323 : OnTunnelClosed onClosed)
324 : {
325 0 : if (!io_ || serviceId.empty() || peerUri.empty())
326 0 : return {};
327 :
328 0 : auto t = std::make_shared<ClientTunnel>();
329 0 : t->id = generateServiceUuid(rng_);
330 0 : t->peerUri = std::move(peerUri);
331 0 : t->peerDevice = peerDevice;
332 0 : t->serviceId = std::move(serviceId);
333 0 : t->serviceName = std::move(serviceName);
334 0 : t->onClosed = std::move(onClosed);
335 : try {
336 : // Try dual-stack loopback: prefer IPv4 loopback but fallback to IPv6
337 : // for IPv6-only systems where 127.0.0.1 may not be available.
338 0 : asio::ip::tcp::endpoint ep(asio::ip::address_v4::loopback(), localPort);
339 0 : t->acceptor = std::make_shared<asio::ip::tcp::acceptor>(*io_);
340 0 : t->acceptor->open(ep.protocol());
341 0 : t->acceptor->set_option(asio::socket_base::reuse_address(true));
342 0 : t->acceptor->bind(ep);
343 0 : t->acceptor->listen();
344 0 : } catch (const std::exception&) {
345 : // IPv4 loopback failed, try IPv6 loopback
346 : try {
347 0 : asio::ip::tcp::endpoint ep6(asio::ip::address_v6::loopback(), localPort);
348 0 : t->acceptor = std::make_shared<asio::ip::tcp::acceptor>(*io_);
349 0 : t->acceptor->open(ep6.protocol());
350 0 : t->acceptor->set_option(asio::socket_base::reuse_address(true));
351 0 : t->acceptor->bind(ep6);
352 0 : t->acceptor->listen();
353 0 : } catch (const std::exception& e) {
354 0 : JAMI_WARNING("[SvcTunnel] cannot bind loopback:{}: {}", localPort, e.what());
355 0 : return {};
356 0 : }
357 0 : }
358 :
359 0 : auto bound = static_cast<uint16_t>(t->acceptor->local_endpoint().port());
360 :
361 : {
362 0 : std::lock_guard lk(mtx_);
363 0 : tunnels_[t->id] = t;
364 0 : }
365 0 : JAMI_LOG("[SvcTunnel] opened tunnel id={} listening on {}:{} -> peer={} service=\"{}\"",
366 : t->id,
367 : t->acceptor->local_endpoint().address().to_string(),
368 : bound,
369 : t->peerUri,
370 : t->serviceName);
371 0 : if (onOpened)
372 0 : onOpened(t->id, bound);
373 :
374 0 : acceptLoop(t);
375 0 : return t->id;
376 0 : }
377 :
378 : void
379 0 : SvcTunnelChannelHandler::acceptLoop(const std::shared_ptr<ClientTunnel>& tunnel)
380 : {
381 0 : auto self = tunnel;
382 0 : auto sock = std::make_shared<asio::ip::tcp::socket>(*io_);
383 0 : self->acceptor->async_accept(*sock, [this, self, sock](const std::error_code& ec) {
384 0 : if (self->closed) {
385 0 : return;
386 : }
387 0 : if (ec) {
388 0 : if (ec == asio::error::operation_aborted)
389 0 : return;
390 0 : JAMI_DEBUG("[SvcTunnel] accept error: {}", ec.message());
391 0 : return;
392 : }
393 : // Open a fresh dhtnet channel for this TCP connection.
394 0 : std::string channelName = std::string(svc_protocol::TunnelChannelPrefix) + self->serviceId;
395 0 : connectionManager_.connectDevice(self->peerDevice,
396 : channelName,
397 0 : [this, self, sock](std::shared_ptr<dhtnet::ChannelSocket> channel,
398 : const DeviceId&) {
399 0 : if (!channel) {
400 0 : JAMI_WARNING("[SvcTunnel] tunnel id={}: connectDevice to peer "
401 : "returned null; closing tunnel",
402 : self->id);
403 0 : std::error_code ig;
404 0 : sock->close(ig);
405 0 : closeTunnelInternal(self->id, "connect-failed");
406 0 : return;
407 : }
408 0 : onClientChannelReady(self, sock, std::move(channel));
409 : });
410 : // Continue accepting.
411 0 : acceptLoop(self);
412 0 : });
413 0 : }
414 :
415 : void
416 0 : SvcTunnelChannelHandler::onClientChannelReady(const std::shared_ptr<ClientTunnel>& tunnel,
417 : std::shared_ptr<asio::ip::tcp::socket> tcp,
418 : std::shared_ptr<dhtnet::ChannelSocket> channel)
419 : {
420 0 : if (tunnel->closed) {
421 : // The tunnel was torn down while connectDevice was in flight; drop
422 : // this late connection instead of wiring up a dangling relay.
423 0 : channel->shutdown();
424 0 : std::error_code ig;
425 0 : tcp->close(ig);
426 0 : return;
427 : }
428 0 : trackClientConnection(tunnel, channel, tcp);
429 0 : relay(std::move(channel), std::move(tcp));
430 : }
431 :
432 : void
433 0 : SvcTunnelChannelHandler::trackClientConnection(const std::shared_ptr<ClientTunnel>& tunnel,
434 : const std::shared_ptr<dhtnet::ChannelSocket>& channel,
435 : const std::shared_ptr<asio::ip::tcp::socket>& tcp)
436 : {
437 : {
438 0 : std::lock_guard lk(tunnel->connsMtx);
439 : // Compact dead entries opportunistically.
440 0 : tunnel->activeConns.erase(std::remove_if(tunnel->activeConns.begin(),
441 0 : tunnel->activeConns.end(),
442 0 : [](const ClientTunnel::Conn& c) {
443 0 : return !c.channel.lock() && !c.tcp.lock();
444 : }),
445 0 : tunnel->activeConns.end());
446 0 : tunnel->activeConns.push_back({channel, tcp});
447 0 : }
448 0 : std::weak_ptr<ClientTunnel> wt = tunnel;
449 0 : std::weak_ptr<dhtnet::ChannelSocket> wc = channel;
450 0 : channel->onShutdown([wt, wc](const std::error_code&) {
451 0 : auto t = wt.lock();
452 0 : if (!t)
453 0 : return;
454 0 : auto c = wc.lock();
455 0 : std::lock_guard lk(t->connsMtx);
456 0 : t->activeConns.erase(std::remove_if(t->activeConns.begin(),
457 0 : t->activeConns.end(),
458 0 : [&](const ClientTunnel::Conn& cn) {
459 0 : return cn.channel.lock() == c || !cn.channel.lock();
460 : }),
461 0 : t->activeConns.end());
462 0 : });
463 0 : }
464 :
465 : bool
466 0 : SvcTunnelChannelHandler::closeTunnel(const std::string& tunnelId)
467 : {
468 0 : return closeTunnelInternal(tunnelId, "closed");
469 : }
470 :
471 : bool
472 0 : SvcTunnelChannelHandler::closeTunnelInternal(const std::string& tunnelId, const std::string& reason)
473 : {
474 0 : std::shared_ptr<ClientTunnel> t;
475 : {
476 0 : std::lock_guard lk(mtx_);
477 0 : auto it = tunnels_.find(tunnelId);
478 0 : if (it == tunnels_.end())
479 0 : return false;
480 0 : t = it->second;
481 0 : tunnels_.erase(it);
482 0 : }
483 0 : t->closed = true;
484 0 : std::error_code ec;
485 0 : if (t->acceptor)
486 0 : t->acceptor->close(ec);
487 : // Tear down every per-connection relay (channel + local TCP) currently
488 : // serving this tunnel so that close actually severs the byte streams.
489 0 : std::vector<ClientTunnel::Conn> conns;
490 : {
491 0 : std::lock_guard lk(t->connsMtx);
492 0 : conns.swap(t->activeConns);
493 0 : }
494 0 : for (auto& c : conns) {
495 0 : if (auto ch = c.channel.lock())
496 0 : ch->shutdown();
497 0 : if (auto sock = c.tcp.lock()) {
498 0 : std::error_code ig;
499 0 : sock->close(ig);
500 0 : }
501 : }
502 0 : JAMI_LOG("[SvcTunnel] closed tunnel id={} reason={} ({} live connection(s) torn down)",
503 : tunnelId,
504 : reason,
505 : conns.size());
506 0 : if (t->onClosed)
507 0 : t->onClosed(tunnelId, reason);
508 0 : return true;
509 0 : }
510 :
511 : void
512 0 : SvcTunnelChannelHandler::trackServerChannel(const std::string& serviceId,
513 : const std::shared_ptr<dhtnet::ChannelSocket>& channel,
514 : const std::shared_ptr<asio::ip::tcp::socket>& tcp)
515 : {
516 0 : if (!channel)
517 0 : return;
518 : {
519 0 : std::lock_guard lk(mtx_);
520 0 : auto& vec = serverChannels_[serviceId];
521 : // Drop dead entries opportunistically.
522 0 : vec.erase(std::remove_if(vec.begin(), vec.end(), [](const ServerConn& c) { return !c.channel.lock(); }),
523 0 : vec.end());
524 0 : vec.push_back({channel, tcp});
525 0 : }
526 0 : std::weak_ptr<dhtnet::ChannelSocket> wc = channel;
527 0 : std::weak_ptr<SvcTunnelChannelHandler> wself; // not needed: handler owns map
528 0 : auto sid = serviceId;
529 0 : channel->onShutdown([this, sid, wc](const std::error_code&) {
530 0 : auto c = wc.lock();
531 0 : std::lock_guard lk(mtx_);
532 0 : auto it = serverChannels_.find(sid);
533 0 : if (it == serverChannels_.end())
534 0 : return;
535 0 : auto& vec = it->second;
536 0 : vec.erase(std::remove_if(vec.begin(),
537 : vec.end(),
538 0 : [&](const ServerConn& sc) {
539 0 : auto sch = sc.channel.lock();
540 0 : return !sch || sch == c;
541 0 : }),
542 0 : vec.end());
543 0 : if (vec.empty())
544 0 : serverChannels_.erase(it);
545 0 : });
546 0 : }
547 :
548 : void
549 0 : SvcTunnelChannelHandler::closeServerChannelsForService(const std::string& serviceId)
550 : {
551 0 : std::vector<ServerConn> conns;
552 : {
553 0 : std::lock_guard lk(mtx_);
554 0 : auto it = serverChannels_.find(serviceId);
555 0 : if (it == serverChannels_.end())
556 0 : return;
557 0 : conns.swap(it->second);
558 0 : serverChannels_.erase(it);
559 0 : }
560 0 : for (auto& c : conns) {
561 0 : if (auto ch = c.channel.lock())
562 0 : ch->shutdown();
563 0 : if (auto sock = c.tcp.lock()) {
564 0 : std::error_code ig;
565 0 : sock->close(ig);
566 0 : }
567 : }
568 0 : if (!conns.empty())
569 0 : JAMI_LOG("[SvcTunnel] closed {} inbound connection(s) for service id={}", conns.size(), serviceId);
570 0 : }
571 :
572 : std::vector<SvcTunnelChannelHandler::Tunnel>
573 0 : SvcTunnelChannelHandler::activeTunnels() const
574 : {
575 0 : std::lock_guard lk(mtx_);
576 0 : std::vector<Tunnel> out;
577 0 : out.reserve(tunnels_.size());
578 0 : for (const auto& [_id, t] : tunnels_) {
579 0 : Tunnel info;
580 0 : info.id = t->id;
581 0 : info.peerUri = t->peerUri;
582 0 : info.peerDevice = t->peerDevice.toString();
583 0 : info.serviceId = t->serviceId;
584 0 : info.serviceName = t->serviceName;
585 0 : info.localPort = t->acceptor ? static_cast<uint16_t>(t->acceptor->local_endpoint().port()) : 0;
586 0 : out.push_back(std::move(info));
587 0 : }
588 0 : return out;
589 0 : }
590 :
591 : } // namespace jami
|