Skip to content

Commit 8e31bef

Browse files
committed
fix(cooperation): give tunnel streams collision-free ids
Both peers allocate ids for the streams they initiate, but the ids share one namespace. Starting both counters at 1 made the barrier stream and a reverse file transfer collide: the peer rejected the second OPEN as a duplicate stream, so the transfer never established and stayed at 1%. Pick a random non-zero id and skip ids already in use locally. Log: give tunnel streams collision-free ids PMS: TASK-396845 Influence: concurrent barrier and file-transfer streams no longer collide.
1 parent 57490cf commit 8e31bef

3 files changed

Lines changed: 100 additions & 4 deletions

File tree

‎src/lib/common/manager/tunnelmanager.cpp‎

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
#include <QJsonDocument>
1313
#include <QJsonObject>
1414
#include <QJsonParseError>
15+
#include <QRandomGenerator>
1516
#include <QTcpServer>
1617
#include <QTcpSocket>
1718
#include <QTimer>
@@ -195,6 +196,19 @@ quint16 TunnelManager::localPort(const QString &serviceId) const
195196
return it->port;
196197
}
197198

199+
quint32 TunnelManager::nextStreamId()
200+
{
201+
// Stream ids are chosen by the initiating side but live in a namespace
202+
// shared by both peers. A counter starting at 1 on each side collides as
203+
// soon as both initiate a stream (barrier plus a reverse file transfer),
204+
// so pick a random id and skip ids already known locally.
205+
for (;;) {
206+
const quint32 id = QRandomGenerator::global()->generate();
207+
if (id != 0 && !_streams.contains(id))
208+
return id;
209+
}
210+
}
211+
198212
void TunnelManager::acceptLocalConnection(const QString &serviceId)
199213
{
200214
const auto forward = _forwards.constFind(serviceId);
@@ -207,9 +221,7 @@ void TunnelManager::acceptLocalConnection(const QString &serviceId)
207221
if (!socket)
208222
break;
209223

210-
quint32 id = _nextStreamId++;
211-
if (_nextStreamId == 0)
212-
_nextStreamId = 1;
224+
const quint32 id = nextStreamId();
213225

214226
const QString token = makeToken();
215227
Stream *stream = createStream(id, serviceId, token, false);

‎src/lib/common/manager/tunnelmanager.h‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ public slots:
101101
Stream *findStream(quint32 id) const;
102102
Stream *createStream(quint32 id, const QString &serviceId, const QString &token, bool dialer);
103103
void destroyStream(quint32 id, const QString &reason, bool notifyPeer);
104+
quint32 nextStreamId();
104105

105106
void acceptLocalConnection(const QString &serviceId);
106107
void setupSocket(Stream *stream);
@@ -135,7 +136,6 @@ public slots:
135136
QHash<quint32, Stream *> _streams;
136137
QString _peerIp;
137138
QString _peerRemote;
138-
quint32 _nextStreamId { 1 };
139139
};
140140

141141
#endif // TUNNELMANAGER_H

‎tests/manager/tunnelmanager_test.cpp‎

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
#include <QAbstractSocket>
1919
#include <QFile>
20+
#include <QElapsedTimer>
2021
#include <QHostAddress>
2122
#include <QRandomGenerator>
2223
#include <QSignalSpy>
@@ -26,9 +27,23 @@
2627
#include <QTest>
2728

2829
#include <atomic>
30+
#include <functional>
2931
#include <memory>
3032
#include <mutex>
3133

34+
namespace {
35+
// QTest's QTRY_* macros abort the test function without registering a failure
36+
// with GoogleTest, so a timed out condition would silently pass here.
37+
bool waitFor(const std::function<bool()> &condition, int timeoutMs = 3000)
38+
{
39+
QElapsedTimer timer;
40+
timer.start();
41+
while (!condition() && timer.elapsed() < timeoutMs)
42+
QTest::qWait(10);
43+
return condition();
44+
}
45+
}
46+
3247
TEST(TunnelManagerTest, EchoThroughReverseTunnel)
3348
{
3449
QTcpServer echo;
@@ -258,6 +273,75 @@ TEST(TunnelManagerTest, MultipleForwardsRunIndependently)
258273
a.stopAll();
259274
}
260275

276+
TEST(TunnelManagerTest, BothSidesCanInitiateStreams)
277+
{
278+
auto makeEcho = [](QTcpServer &server) {
279+
EXPECT_TRUE(server.listen(QHostAddress::LocalHost, 0));
280+
QObject::connect(&server, &QTcpServer::newConnection, &server, [&server] {
281+
while (QTcpSocket *socket = server.nextPendingConnection()) {
282+
QObject::connect(socket, &QTcpSocket::readyRead, socket, [socket] {
283+
socket->write(socket->readAll());
284+
});
285+
QObject::connect(socket, &QTcpSocket::disconnected, socket, &QObject::deleteLater);
286+
}
287+
});
288+
};
289+
290+
QTcpServer echoA;
291+
QTcpServer echoB;
292+
makeEcho(echoA);
293+
makeEcho(echoB);
294+
295+
TunnelManager a;
296+
TunnelManager b;
297+
a.setPeer(QStringLiteral("B"));
298+
b.setPeer(QStringLiteral("A"));
299+
// A hosts the barrier web service, B hosts a file transfer service.
300+
a.registerService(QStringLiteral("barrier"), QStringLiteral("127.0.0.1"), echoA.serverPort());
301+
b.registerService(QStringLiteral("transfer"), QStringLiteral("127.0.0.1"), echoB.serverPort());
302+
303+
a.setSendHandler([&b](const QString &, int mask, const QByteArray &payload) {
304+
b.onTunnelMessage(mask, payload, QStringLiteral("A"));
305+
});
306+
b.setSendHandler([&a](const QString &, int mask, const QByteArray &payload) {
307+
a.onTunnelMessage(mask, payload, QStringLiteral("B"));
308+
});
309+
310+
// B opens the barrier stream first, as in an A -> B collaboration.
311+
const quint16 barrierPort = b.startLocalForward(QStringLiteral("barrier"));
312+
ASSERT_NE(barrierPort, 0);
313+
QTcpSocket barrierClient;
314+
barrierClient.connectToHost(QHostAddress::LocalHost, barrierPort);
315+
ASSERT_TRUE(waitFor([&] {
316+
return barrierClient.state() == QAbstractSocket::ConnectedState;
317+
}));
318+
barrierClient.write(QByteArrayLiteral("barrier"));
319+
ASSERT_TRUE(waitFor([&] { return barrierClient.bytesAvailable() >= 7; }));
320+
EXPECT_EQ(barrierClient.readAll(), QByteArrayLiteral("barrier"));
321+
322+
// While that stream is still open, A opens the transfer stream, as in a
323+
// B -> A file transfer. Both sides used to allocate stream id 1 here and
324+
// B rejected the second OPEN as a duplicate.
325+
const quint16 transferPort = a.startLocalForward(QStringLiteral("transfer"));
326+
ASSERT_NE(transferPort, 0);
327+
QTcpSocket transferClient;
328+
transferClient.connectToHost(QHostAddress::LocalHost, transferPort);
329+
ASSERT_TRUE(waitFor([&] {
330+
return transferClient.state() == QAbstractSocket::ConnectedState;
331+
}));
332+
transferClient.write(QByteArrayLiteral("transfer"));
333+
ASSERT_TRUE(waitFor([&] { return transferClient.bytesAvailable() >= 8; }));
334+
EXPECT_EQ(transferClient.readAll(), QByteArrayLiteral("transfer"));
335+
336+
// The first stream must survive the second one being opened.
337+
barrierClient.write(QByteArrayLiteral("still-alive"));
338+
ASSERT_TRUE(waitFor([&] { return barrierClient.bytesAvailable() >= 11; }));
339+
EXPECT_EQ(barrierClient.readAll(), QByteArrayLiteral("still-alive"));
340+
341+
b.stopAll();
342+
a.stopAll();
343+
}
344+
261345
TEST(TunnelManagerTest, IncomingRequestIsNotSwallowedByPendingAsyncRequest)
262346
{
263347
auto service = std::make_shared<AsioService>();

0 commit comments

Comments
 (0)