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 721 : SvcTunnelChannelHandler::SvcTunnelChannelHandler(const std::shared_ptr<JamiAccount>& acc,
60 : dhtnet::ConnectionManager& cm,
61 721 : std::shared_ptr<asio::io_context> io)
62 : : ChannelHandlerInterface()
63 721 : , account_(acc)
64 721 : , connectionManager_(cm)
65 721 : , io_(std::move(io))
66 1442 : , rng_(dht::crypto::getDerivedRandomEngine(acc->rand))
67 721 : {}
68 :
69 1442 : SvcTunnelChannelHandler::~SvcTunnelChannelHandler()
70 : {
71 721 : std::vector<std::shared_ptr<ClientTunnel>> snapshot;
72 : {
73 721 : std::lock_guard lk(mtx_);
74 721 : for (auto& [_id, t] : tunnels_)
75 0 : snapshot.push_back(t);
76 721 : tunnels_.clear();
77 721 : }
78 721 : 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 1442 : }
85 :
86 : std::string
87 2 : SvcTunnelChannelHandler::parseServiceId(const std::string& channelName)
88 : {
89 2 : constexpr std::string_view prefix = "svc://";
90 2 : if (channelName.size() <= prefix.size())
91 0 : return {};
92 2 : if (channelName.compare(0, prefix.size(), prefix) != 0)
93 0 : return {};
94 2 : 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 1 : SvcTunnelChannelHandler::onRequest(const std::shared_ptr<dht::crypto::Certificate>& peer, const std::string& name)
113 : {
114 1 : if (!peer || !peer->issuer)
115 0 : return false;
116 1 : auto serviceId = parseServiceId(name);
117 1 : if (serviceId.empty())
118 0 : return false;
119 1 : auto acc = account_.lock();
120 1 : if (!acc)
121 0 : return false;
122 1 : const auto peerUri = peer->issuer->getId().toString();
123 0 : auto checker = [&acc](const std::string& uri) {
124 0 : return acc->isContact(uri);
125 1 : };
126 1 : return acc->serviceManager().isAuthorized(serviceId, peerUri, checker);
127 1 : }
128 :
129 : void
130 2 : SvcTunnelChannelHandler::onReady(const std::shared_ptr<dht::crypto::Certificate>& peer,
131 : const std::string& name,
132 : std::shared_ptr<dhtnet::ChannelSocket> channel)
133 : {
134 2 : 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 2 : if (channel->isInitiator())
140 1 : return;
141 1 : auto serviceId = parseServiceId(name);
142 1 : auto acc = account_.lock();
143 1 : if (!acc || serviceId.empty()) {
144 0 : channel->shutdown();
145 0 : return;
146 : }
147 1 : auto rec = acc->serviceManager().getService(serviceId);
148 1 : if (!rec || !rec->enabled) {
149 0 : channel->shutdown();
150 0 : return;
151 : }
152 1 : if (!io_) {
153 0 : channel->shutdown();
154 0 : return;
155 : }
156 1 : 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 1 : auto tcp = std::make_shared<asio::ip::tcp::socket>(*io_);
163 1 : asio::ip::tcp::resolver resolver(*io_);
164 1 : std::error_code ec;
165 1 : auto endpoints = resolver.resolve(rec->localHost, std::to_string(rec->localPort), ec);
166 1 : 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 1 : auto pre = std::make_shared<PreConnectBuf>();
182 1 : pre->tcp = tcp;
183 1 : auto channelKeep = channel;
184 1 : 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 1 : asio::async_connect(*tcp,
205 : endpoints,
206 2 : [this, channel = channelKeep, tcp, pre, serviceId](const std::error_code& cec,
207 : const asio::ip::tcp::endpoint&) {
208 1 : if (cec) {
209 0 : JAMI_WARNING("[SvcTunnel] connect failed: {}", cec.message());
210 0 : channel->shutdown();
211 0 : return;
212 : }
213 1 : trackServerChannel(serviceId, channel, tcp);
214 1 : std::vector<uint8_t> drained;
215 : {
216 1 : std::lock_guard lk(pre->m);
217 1 : pre->tcpReady = true;
218 1 : drained.swap(pre->bytes);
219 1 : }
220 1 : 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 1 : channel->setOnRecv([tcp, channel](const uint8_t* data, size_t n) -> ssize_t {
235 1 : auto buf = std::make_shared<std::vector<uint8_t>>(data, data + n);
236 1 : asio::async_write(*tcp,
237 1 : asio::buffer(*buf),
238 2 : [buf, channel, tcp](const std::error_code& ec, std::size_t) {
239 1 : 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 1 : });
247 1 : return static_cast<ssize_t>(n);
248 1 : });
249 1 : relayTcpToChannel(channel, tcp);
250 1 : });
251 1 : }
252 :
253 : void
254 1 : SvcTunnelChannelHandler::relay(std::shared_ptr<dhtnet::ChannelSocket> channel,
255 : std::shared_ptr<asio::ip::tcp::socket> tcp)
256 : {
257 : // ChannelSocket -> TCP
258 1 : auto channelHold = channel;
259 1 : channel->setOnRecv([tcp, channelHold](const uint8_t* data, size_t size) -> ssize_t {
260 1 : auto buf = std::make_shared<std::vector<uint8_t>>(data, data + size);
261 1 : asio::async_write(*tcp,
262 1 : asio::buffer(*buf),
263 2 : [buf, channelHold, tcp](const std::error_code& ec, std::size_t /*n*/) {
264 1 : 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 1 : });
271 1 : return static_cast<ssize_t>(size);
272 1 : });
273 :
274 : // TCP -> ChannelSocket
275 1 : relayTcpToChannel(channel, tcp);
276 1 : }
277 :
278 : void
279 2 : SvcTunnelChannelHandler::relayTcpToChannel(std::shared_ptr<dhtnet::ChannelSocket> channel,
280 : std::shared_ptr<asio::ip::tcp::socket> tcp)
281 : {
282 2 : auto buf = std::make_shared<std::vector<uint8_t>>(kRelayBufSize);
283 2 : auto channelKeep = channel;
284 2 : auto tcpKeep = tcp;
285 2 : auto reader = std::make_shared<std::function<void()>>();
286 4 : *reader = [channelKeep, tcpKeep, buf, reader]() {
287 8 : tcpKeep->async_read_some(asio::buffer(*buf),
288 8 : [channelKeep, tcpKeep, buf, reader](const std::error_code& ec, std::size_t n) {
289 4 : if (ec || n == 0) {
290 2 : channelKeep->shutdown();
291 2 : std::error_code ig;
292 2 : tcpKeep->close(ig);
293 2 : *reader = nullptr; // break the cycle
294 2 : return;
295 : }
296 2 : std::error_code wec;
297 2 : channelKeep->write(buf->data(), n, wec);
298 2 : 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 2 : (*reader)();
306 : });
307 6 : };
308 2 : (*reader)();
309 :
310 2 : channel->onShutdown([tcp = tcpKeep](const std::error_code&) {
311 2 : std::error_code ig;
312 2 : tcp->close(ig);
313 2 : });
314 2 : }
315 :
316 : std::string
317 1 : 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 1 : if (!io_ || serviceId.empty() || peerUri.empty())
326 0 : return {};
327 :
328 1 : auto t = std::make_shared<ClientTunnel>();
329 1 : t->id = generateServiceUuid(rng_);
330 1 : t->peerUri = std::move(peerUri);
331 1 : t->peerDevice = peerDevice;
332 1 : t->serviceId = std::move(serviceId);
333 1 : t->serviceName = std::move(serviceName);
334 1 : 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 1 : asio::ip::tcp::endpoint ep(asio::ip::address_v4::loopback(), localPort);
339 1 : t->acceptor = std::make_shared<asio::ip::tcp::acceptor>(*io_);
340 1 : t->acceptor->open(ep.protocol());
341 1 : t->acceptor->set_option(asio::socket_base::reuse_address(true));
342 1 : t->acceptor->bind(ep);
343 1 : 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 1 : auto bound = static_cast<uint16_t>(t->acceptor->local_endpoint().port());
360 :
361 : {
362 1 : std::lock_guard lk(mtx_);
363 1 : tunnels_[t->id] = t;
364 1 : }
365 1 : 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 1 : if (onOpened)
372 1 : onOpened(t->id, bound);
373 :
374 1 : acceptLoop(t);
375 1 : return t->id;
376 1 : }
377 :
378 : void
379 2 : SvcTunnelChannelHandler::acceptLoop(const std::shared_ptr<ClientTunnel>& tunnel)
380 : {
381 2 : auto self = tunnel;
382 2 : auto sock = std::make_shared<asio::ip::tcp::socket>(*io_);
383 2 : self->acceptor->async_accept(*sock, [this, self, sock](const std::error_code& ec) {
384 2 : if (self->closed) {
385 1 : return;
386 : }
387 1 : 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 2 : std::string channelName = std::string(svc_protocol::TunnelChannelPrefix) + self->serviceId;
395 1 : connectionManager_.connectDevice(self->peerDevice,
396 : channelName,
397 2 : [this, self, sock](std::shared_ptr<dhtnet::ChannelSocket> channel,
398 : const DeviceId&) {
399 1 : 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 1 : onClientChannelReady(self, sock, std::move(channel));
409 : });
410 : // Continue accepting.
411 1 : acceptLoop(self);
412 2 : });
413 2 : }
414 :
415 : void
416 1 : 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 1 : 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 1 : trackClientConnection(tunnel, channel, tcp);
429 1 : relay(std::move(channel), std::move(tcp));
430 : }
431 :
432 : void
433 1 : 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 1 : std::lock_guard lk(tunnel->connsMtx);
439 : // Compact dead entries opportunistically.
440 3 : tunnel->activeConns.erase(std::remove_if(tunnel->activeConns.begin(),
441 1 : tunnel->activeConns.end(),
442 0 : [](const ClientTunnel::Conn& c) {
443 0 : return !c.channel.lock() && !c.tcp.lock();
444 : }),
445 1 : tunnel->activeConns.end());
446 2 : tunnel->activeConns.push_back({channel, tcp});
447 1 : }
448 1 : std::weak_ptr<ClientTunnel> wt = tunnel;
449 1 : std::weak_ptr<dhtnet::ChannelSocket> wc = channel;
450 1 : 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 2 : }
464 :
465 : bool
466 1 : SvcTunnelChannelHandler::closeTunnel(const std::string& tunnelId)
467 : {
468 2 : return closeTunnelInternal(tunnelId, "closed");
469 : }
470 :
471 : bool
472 1 : SvcTunnelChannelHandler::closeTunnelInternal(const std::string& tunnelId, const std::string& reason)
473 : {
474 1 : std::shared_ptr<ClientTunnel> t;
475 : {
476 1 : std::lock_guard lk(mtx_);
477 1 : auto it = tunnels_.find(tunnelId);
478 1 : if (it == tunnels_.end())
479 0 : return false;
480 1 : t = it->second;
481 1 : tunnels_.erase(it);
482 1 : }
483 1 : t->closed = true;
484 1 : std::error_code ec;
485 1 : if (t->acceptor)
486 1 : 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 1 : std::vector<ClientTunnel::Conn> conns;
490 : {
491 1 : std::lock_guard lk(t->connsMtx);
492 1 : conns.swap(t->activeConns);
493 1 : }
494 2 : for (auto& c : conns) {
495 1 : if (auto ch = c.channel.lock())
496 1 : ch->shutdown();
497 1 : if (auto sock = c.tcp.lock()) {
498 1 : std::error_code ig;
499 1 : sock->close(ig);
500 1 : }
501 : }
502 1 : JAMI_LOG("[SvcTunnel] closed tunnel id={} reason={} ({} live connection(s) torn down)",
503 : tunnelId,
504 : reason,
505 : conns.size());
506 1 : if (t->onClosed)
507 1 : t->onClosed(tunnelId, reason);
508 1 : return true;
509 1 : }
510 :
511 : void
512 1 : 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 1 : if (!channel)
517 0 : return;
518 : {
519 1 : std::lock_guard lk(mtx_);
520 1 : auto& vec = serverChannels_[serviceId];
521 : // Drop dead entries opportunistically.
522 1 : vec.erase(std::remove_if(vec.begin(), vec.end(), [](const ServerConn& c) { return !c.channel.lock(); }),
523 1 : vec.end());
524 1 : vec.push_back({channel, tcp});
525 1 : }
526 1 : std::weak_ptr<dhtnet::ChannelSocket> wc = channel;
527 1 : std::weak_ptr<SvcTunnelChannelHandler> wself; // not needed: handler owns map
528 1 : auto sid = serviceId;
529 1 : 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 2 : }
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
|