diff --git a/3rdparty/i18n b/3rdparty/i18n index 1ed4613c29..1dcf71f870 160000 --- a/3rdparty/i18n +++ b/3rdparty/i18n @@ -1 +1 @@ -Subproject commit 1ed4613c2997585b5287dd2b8a5d220494509176 +Subproject commit 1dcf71f87012c304a5b8a493c1886b7badd87a9c diff --git a/Cargo.lock b/Cargo.lock index 3d77bed0ac..b8d067fe72 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -155,7 +155,7 @@ dependencies = [ "nom", "num-traits", "rusticata-macros", - "thiserror", + "thiserror 1.0.69", "time 0.3.36", ] @@ -280,7 +280,7 @@ dependencies = [ "semver", "serde", "serde_json", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -671,7 +671,7 @@ dependencies = [ "serde", "serde_json", "tempfile", - "thiserror", + "thiserror 1.0.69", "time 0.1.45", "uniffi", "uuid", @@ -1481,7 +1481,7 @@ dependencies = [ "paste", "serde", "serde_derive", - "thiserror", + "thiserror 1.0.69", "url", "uuid", ] @@ -1678,7 +1678,7 @@ dependencies = [ "hex", "oid-registry", "ring", - "thiserror", + "thiserror 2.0.3", "x509-parser", ] @@ -1830,18 +1830,38 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.67" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3c6efbfc763e64eb85c11c25320f0737cb7364c4b6336db90aa9ebe27a0bbd" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c006c85c7651b3cf2ada4584faa36773bd07bac24acfb39f3c431b36d7e667aa" +dependencies = [ + "thiserror-impl 2.0.3", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] name = "thiserror-impl" -version = "1.0.67" +version = "2.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b607164372e89797d78b8e23a6d67d5d1038c1c65efd52e1389ef8b77caba2a6" +checksum = "f077553d607adc1caf65430528a576c757a71ed73944b66ebb58ef2bbd243568" dependencies = [ "proc-macro2", "quote", @@ -2674,7 +2694,7 @@ dependencies = [ "oid-registry", "ring", "rusticata-macros", - "thiserror", + "thiserror 1.0.69", "time 0.3.36", ] diff --git a/extension/socks5proxy/bin/sockslogger.cpp b/extension/socks5proxy/bin/sockslogger.cpp index b8cfb2d038..9aa8fe37ae 100644 --- a/extension/socks5proxy/bin/sockslogger.cpp +++ b/extension/socks5proxy/bin/sockslogger.cpp @@ -120,14 +120,35 @@ void SocksLogger::dataSentReceived(qint64 sent, qint64 received) { m_rx_bytes.addSample(received); } +QDebug& SocksLogger::printEventStack(QDebug& msg, Socks5Connection* conn) { + bool first = true; + for (const QString& hostname : conn->hostLookupStack()) { + if (!first) { + msg << "->"; + } + first = false; + msg << hostname; + } + return msg; +} + void SocksLogger::connectionStateChanged() { Socks5Connection* conn = qobject_cast(QObject::sender()); if (conn->state() == Socks5Connection::Proxy) { auto msg = qDebug() << "Connecting" << conn->clientName() << "to"; - for (const QString& hostname : conn->dnsLookupStack()) { - msg << hostname << "->"; + printEventStack(msg, conn); + } + if (conn->state() == Socks5Connection::Closed) { + if (!conn->errorString().isEmpty()) { + // Failed connection + auto msg = qDebug() << "Failed"; + printEventStack(msg, conn) << "->" << conn->errorString(); + } else { + // Successful connection + auto msg = qDebug() << "Closed"; + printEventStack(msg, conn) << "txbuf" << conn->sendHighWaterMark() + << "rxbuf" << conn->recvHighWaterMark(); } - msg << conn->destAddress().toString(); } } diff --git a/extension/socks5proxy/bin/sockslogger.h b/extension/socks5proxy/bin/sockslogger.h index 1264214014..5914c09eab 100644 --- a/extension/socks5proxy/bin/sockslogger.h +++ b/extension/socks5proxy/bin/sockslogger.h @@ -54,6 +54,7 @@ class SocksLogger final : public QObject { ~SocksLogger(); static QString bytesToString(qint64 value); + static QDebug& printEventStack(QDebug& msg, Socks5Connection* conn); void printStatus(); const QString& logfile() const { return m_logFileName; } diff --git a/extension/socks5proxy/src/socks5connection.cpp b/extension/socks5proxy/src/socks5connection.cpp index 9dca6e439a..2d97d36476 100644 --- a/extension/socks5proxy/src/socks5connection.cpp +++ b/extension/socks5proxy/src/socks5connection.cpp @@ -18,6 +18,8 @@ constexpr const int MAX_DNS_LOOKUP_ATTEMPTS = 5; +constexpr const int MAX_CONNECTION_BUFFER = 16 * 1024; + namespace { #ifdef Q_OS_WIN @@ -79,12 +81,28 @@ Socks5Connection::Socks5Connection(QIODevice* socket) : QObject(socket), m_inSocket(socket) { connect(m_inSocket, &QIODevice::readyRead, this, &Socks5Connection::readyRead); + + connect(m_inSocket, &QIODevice::bytesWritten, this, + &Socks5Connection::bytesWritten); + readyRead(); } Socks5Connection::Socks5Connection(QTcpSocket* socket) : Socks5Connection(static_cast(socket)) { - connect(socket, &QTcpSocket::disconnected, this, &QObject::deleteLater); + connect(socket, &QTcpSocket::disconnected, this, + [this]() { setState(Closed); }); + + connect(socket, &QTcpSocket::errorOccurred, this, + [this](QAbstractSocket::SocketError error) { + if (error == QAbstractSocket::RemoteHostClosedError) { + setState(Closed); + } else { + setError(ErrorGeneral, m_inSocket->errorString()); + } + }); + + socket->setReadBufferSize(MAX_CONNECTION_BUFFER); m_socksPort = socket->localPort(); m_clientName = socket->peerAddress().toString(); @@ -92,7 +110,19 @@ Socks5Connection::Socks5Connection(QTcpSocket* socket) Socks5Connection::Socks5Connection(QLocalSocket* socket) : Socks5Connection(static_cast(socket)) { - connect(socket, &QLocalSocket::disconnected, this, &QObject::deleteLater); + connect(socket, &QLocalSocket::disconnected, this, + [this]() { setState(Closed); }); + + connect(socket, &QLocalSocket::errorOccurred, this, + [this](QLocalSocket::LocalSocketError error) { + if (error == QLocalSocket::PeerClosedError) { + setState(Closed); + } else { + setError(ErrorGeneral, m_inSocket->errorString()); + } + }); + + socket->setReadBufferSize(MAX_CONNECTION_BUFFER); // TODO: Some magic may be required here to resolve the entity of which client // tried to connect. Some breadcrumbs: @@ -106,9 +136,49 @@ Socks5Connection::Socks5Connection(QLocalSocket* socket) m_clientName = localClientName(socket); } +void Socks5Connection::setError(Socks5Replies reason, + const QString& errorString) { + // A Server response message is only sent in certain states. + switch (m_state) { + case ClientGreeting: + [[fallthrough]]; + case ClientConnectionRequest: + [[fallthrough]]; + case ClientConnectionAddress: { + ServerResponsePacket packet(createServerResponsePacket(reason)); + m_inSocket->write((char*)&packet, sizeof(ServerResponsePacket)); + break; + } + + default: + break; + } + + m_errorString = errorString; + setState(Closed); +} + void Socks5Connection::setState(Socks5State newstate) { m_state = newstate; emit stateChanged(); + + // If the new state is Proxy, keep track of how many bytes are yet to be + // written to finish the negotiation. We should suppress the statistics + // signals for such traffic. + if (m_state == Proxy) { + m_recvIgnoreBytes = m_inSocket->bytesToWrite(); + } + + // If the state is closing. Shutdown the sockets. + if (m_state == Closed) { + m_inSocket->close(); + if (m_outSocket != nullptr) { + m_outSocket->close(); + } + + // Request self-destruction + deleteLater(); + } } void Socks5Connection::readyRead() { @@ -118,12 +188,12 @@ void Socks5Connection::readyRead() { if (!packet) { return; } - if (packet.value().version != 0x5) { + uint8_t version = packet.value().version; + if (version != 0x5) { // We only currently want to support socks5. // as otherwise we could not support udp or auth. - ServerResponsePacket packet(createServerResponsePacket(ErrorGeneral)); - m_inSocket->write((char*)&packet, sizeof(ServerResponsePacket)); - m_inSocket->close(); + auto msg = QString("SOCKS version %1 not supported").arg(version); + setError(ErrorGeneral, msg); return; } m_authNumber = packet.value().nauth; @@ -138,7 +208,7 @@ void Socks5Connection::readyRead() { char buffer[INT8_MAX]; if (m_inSocket->read(buffer, m_authNumber) != m_authNumber) { - m_inSocket->close(); + setError(ErrorGeneral, m_inSocket->errorString()); return; } @@ -148,7 +218,7 @@ void Socks5Connection::readyRead() { if (m_inSocket->write((const char*)&packet, sizeof(ServerChoicePacket)) != sizeof(ServerChoicePacket)) { - m_inSocket->close(); + setError(ErrorGeneral, m_inSocket->errorString()); return; } @@ -162,16 +232,11 @@ void Socks5Connection::readyRead() { return; } if (packet.value().version != 0x5 || packet.value().rsv != 0x00) { - ServerResponsePacket packet(createServerResponsePacket(ErrorGeneral)); - m_inSocket->write((char*)&packet, sizeof(ServerResponsePacket)); - m_inSocket->close(); + setError(ErrorGeneral, "Malformed connection request"); return; } if (packet.value().cmd != 0x01u /* connection */) { - ServerResponsePacket packet( - createServerResponsePacket(ErrorCommandNotSupported)); - m_inSocket->write((char*)&packet, sizeof(ServerResponsePacket)); - m_inSocket->close(); + setError(ErrorCommandNotSupported, "Command not supported"); return; } m_addressType = packet.value().atype; @@ -200,25 +265,25 @@ void Socks5Connection::readyRead() { uint8_t length; if (m_inSocket->read((char*)&length, 1) != 1) { - m_inSocket->close(); + setError(ErrorGeneral, m_inSocket->errorString()); return; } char buffer[UINT8_MAX]; if (m_inSocket->read(buffer, length) != length) { - m_inSocket->close(); + setError(ErrorGeneral, m_inSocket->errorString()); return; } uint16_t port; if (m_inSocket->read((char*)&port, sizeof(port)) != sizeof(port)) { - m_inSocket->close(); + setError(ErrorGeneral, m_inSocket->errorString()); return; } // Todo VPN-6510: Let's resolve the Host with the local device DNS QString hostname = QString::fromUtf8(buffer, length); - m_dnsLookupStack.append(hostname); + m_hostLookupStack.append(hostname); // Start a DNS lookup to handle this request. m_dnsLookupAttempts = MAX_DNS_LOOKUP_ATTEMPTS; @@ -245,22 +310,16 @@ void Socks5Connection::readyRead() { } else { - ServerResponsePacket packet( - createServerResponsePacket(ErrorAddressNotSupported)); - m_inSocket->write((char*)&packet, sizeof(ServerResponsePacket)); - m_inSocket->close(); + setError(ErrorAddressNotSupported, "Address type not supported"); return; } } break; - case Proxy: { - qint64 bytes = proxy(m_inSocket, m_outSocket); - if (bytes) { - emit dataSentReceived(bytes, 0); - } - } break; + case Proxy: + proxy(m_inSocket, m_outSocket, m_sendHighWaterMark); + break; default: Q_ASSERT(false); @@ -268,23 +327,57 @@ void Socks5Connection::readyRead() { } } -// TODO: VPN-6513 make sure we cannot drop bytes even under pressure. -qint64 Socks5Connection::proxy(QIODevice* a, QIODevice* b) { - Q_ASSERT(a && b); - - qint64 bytes = 0; - char buffer[4096]; - while (a->bytesAvailable()) { - qint64 val = - a->read(buffer, qMin(a->bytesAvailable(), (qint64)sizeof(buffer))); - if (val <= 0 || b->write(buffer, val) != val) { - return -1; +void Socks5Connection::bytesWritten(qint64 bytes) { + // Ignore this signal outside of the proxy state. + if (m_state != Proxy) { + return; + } + + // Ignore this signal if it's just reporting a negotiation packet. + if (m_recvIgnoreBytes >= bytes) { + m_recvIgnoreBytes -= bytes; + return; + } else if (m_recvIgnoreBytes > 0) { + bytes -= m_recvIgnoreBytes; + m_recvIgnoreBytes = 0; + } + + // Drive statistics and proxy data. + emit dataSentReceived(0, bytes); + proxy(m_inSocket, m_outSocket, m_recvHighWaterMark); +} + +void Socks5Connection::proxy(QIODevice* from, QIODevice* to, + quint64& watermark) { + Q_ASSERT(from && to); + + for (;;) { + qint64 available = from->bytesAvailable(); + if (available <= 0) { + break; + } + + qint64 capacity = MAX_CONNECTION_BUFFER - to->bytesToWrite(); + if (capacity <= 0) { + break; } - bytes += val; + QByteArray data = from->read(qMin(available, capacity)); + if (data.length() == 0) { + break; + } + qint64 sent = to->write(data); + if (sent != data.length()) { + qDebug() << "Truncated write. Sent" << sent << "of" << data.length(); + break; + } } - return bytes; + // Update buffer high watermark. + qint64 queued = to->bytesToWrite(); + if (queued > watermark) { + watermark = queued; + } } void Socks5Connection::dnsResolutionFinished(quint16 port) { @@ -299,11 +392,7 @@ void Socks5Connection::dnsResolutionFinished(quint16 port) { }); if (lookup->error() != QDnsLookup::NoError) { - qDebug() << "Received error:" << lookup->errorString(); - ServerResponsePacket packet( - createServerResponsePacket(ErrorHostUnreachable)); - m_inSocket->write((char*)&packet, sizeof(ServerResponsePacket)); - m_inSocket->close(); + setError(ErrorHostUnreachable, lookup->errorString()); return; } @@ -320,10 +409,7 @@ void Socks5Connection::dnsResolutionFinished(quint16 port) { // resolution and report the host as unreachable. This safeguards us from // recursive DNS loops and other unresolvable situations. if (m_dnsLookupAttempts <= 0) { - ServerResponsePacket packet( - createServerResponsePacket(ErrorHostUnreachable)); - m_inSocket->write((char*)&packet, sizeof(ServerResponsePacket)); - m_inSocket->close(); + setError(ErrorHostUnreachable, "Maximum DNS attempts exhausted"); return; } @@ -331,7 +417,7 @@ void Socks5Connection::dnsResolutionFinished(quint16 port) { auto cnameRecords = lookup->canonicalNameRecords(); if (cnameRecords.length() > 0) { QString target = cnameRecords.first().value(); - m_dnsLookupStack.append(target); + m_hostLookupStack.append(target); lookup->setName(target); lookup->setType(QDnsLookup::ANY); lookup->lookup(); @@ -350,10 +436,7 @@ void Socks5Connection::dnsResolutionFinished(quint16 port) { // // We can also receive more than one service record and we are expected // to load balance/fallback amongst them. - ServerResponsePacket packet( - createServerResponsePacket(ErrorHostUnreachable)); - m_inSocket->write((char*)&packet, sizeof(ServerResponsePacket)); - m_inSocket->close(); + setError(ErrorHostUnreachable, "SRV records not supported"); return; } @@ -367,13 +450,12 @@ void Socks5Connection::dnsResolutionFinished(quint16 port) { } // Otherwise, no such host was found. - ServerResponsePacket packet(createServerResponsePacket(ErrorHostUnreachable)); - m_inSocket->write((char*)&packet, sizeof(ServerResponsePacket)); - m_inSocket->close(); + setError(ErrorHostUnreachable, "DNS hostname not found"); } void Socks5Connection::configureOutSocket(quint16 port) { Q_ASSERT(!m_destAddress.isNull()); + m_hostLookupStack.append(m_destAddress.toString()); m_outSocket = new QTcpSocket(this); emit setupOutSocket(m_outSocket, m_destAddress); @@ -383,7 +465,7 @@ void Socks5Connection::configureOutSocket(quint16 port) { ServerResponsePacket packet(createServerResponsePacket(0x00, m_socksPort)); if (m_inSocket->write((char*)&packet, sizeof(ServerResponsePacket)) != sizeof(ServerResponsePacket)) { - m_inSocket->close(); + setError(ErrorGeneral, m_inSocket->errorString()); return; } @@ -391,27 +473,24 @@ void Socks5Connection::configureOutSocket(quint16 port) { readyRead(); }); - connect(m_outSocket, &QTcpSocket::readyRead, this, [this]() { - qint64 bytes = proxy(m_outSocket, m_inSocket); - if (bytes > 0) { - emit dataSentReceived(0, bytes); - } else if (bytes < 0) { - // We hit an error. close. - m_inSocket->close(); - } + connect(m_outSocket, &QIODevice::bytesWritten, this, [this](qint64 bytes) { + emit dataSentReceived(bytes, 0); + proxy(m_inSocket, m_outSocket, m_sendHighWaterMark); }); + connect(m_outSocket, &QTcpSocket::readyRead, this, + [this]() { proxy(m_outSocket, m_inSocket, m_recvHighWaterMark); }); + connect(m_outSocket, &QTcpSocket::disconnected, this, - [this]() { m_inSocket->close(); }); + [this]() { setState(Closed); }); connect(m_outSocket, &QTcpSocket::errorOccurred, this, [this](QAbstractSocket::SocketError error) { - if (m_state != Proxy) { - ServerResponsePacket packet( - createServerResponsePacket(socketErrorToSocks5Rep(error))); - m_inSocket->write((char*)&packet, sizeof(ServerResponsePacket)); + if (error == QAbstractSocket::RemoteHostClosedError) { + setState(Closed); + } else { + setError(ErrorGeneral, m_outSocket->errorString()); } - m_inSocket->close(); }); } diff --git a/extension/socks5proxy/src/socks5connection.h b/extension/socks5proxy/src/socks5connection.h index 4fef2b54ee..d49612feb7 100644 --- a/extension/socks5proxy/src/socks5connection.h +++ b/extension/socks5proxy/src/socks5connection.h @@ -25,11 +25,11 @@ class Socks5Connection final : public QObject { /** * @brief Copies incoming bytes to another QIODevice * - * @param rx- the source device - * @param tx- the output device - * @return qint64 - The Bytes Written, -1 on error. + * @param from- the source device + * @param to- the output device + * @param watermark- reference to the buffer high watermark */ - static qint64 proxy(QIODevice* rx, QIODevice* tx); + static void proxy(QIODevice* rx, QIODevice* tx, quint64& watermark); enum Socks5State { ClientGreeting, @@ -37,6 +37,7 @@ class Socks5Connection final : public QObject { ClientConnectionRequest, ClientConnectionAddress, Proxy, + Closed, }; enum Socks5Replies : uint8_t { @@ -75,10 +76,14 @@ class Socks5Connection final : public QObject { const QString& clientName() const { return m_clientName; } const QHostAddress& destAddress() const { return m_destAddress; } - const QStringList& dnsLookupStack() const { return m_dnsLookupStack; } + const QStringList& hostLookupStack() const { return m_hostLookupStack; } const Socks5State& state() const { return m_state; } + quint64 sendHighWaterMark() const { return m_sendHighWaterMark; } + quint64 recvHighWaterMark() const { return m_recvHighWaterMark; } + const QString& errorString() const { return m_errorString; } + signals: void setupOutSocket(QAbstractSocket* socket, const QHostAddress& dest); void dataSentReceived(qint64 sent, qint64 received); @@ -86,14 +91,17 @@ class Socks5Connection final : public QObject { private: void setState(Socks5State state); + void setError(Socks5Replies reply, const QString& errorString); void configureOutSocket(quint16 port); void dnsResolutionFinished(quint16 port); void readyRead(); + void bytesWritten(qint64 bytes); // Implemented by platform-specific code in socks5local_.cpp static QString localClientName(QLocalSocket* s); Socks5State m_state = ClientGreeting; + QString m_errorString; uint8_t m_authNumber = 0; QIODevice* m_inSocket = nullptr; @@ -106,7 +114,11 @@ class Socks5Connection final : public QObject { QHostAddress m_destAddress; int m_dnsLookupAttempts = 0; - QStringList m_dnsLookupStack; + QStringList m_hostLookupStack; + + quint64 m_sendHighWaterMark = 0; + quint64 m_recvHighWaterMark = 0; + quint64 m_recvIgnoreBytes = 0; }; #endif // Socks5Connection_H diff --git a/extension/socks5proxy/tests/testsocks5connection.cpp b/extension/socks5proxy/tests/testsocks5connection.cpp index 0cf8fe7d6c..214e433d6c 100644 --- a/extension/socks5proxy/tests/testsocks5connection.cpp +++ b/extension/socks5proxy/tests/testsocks5connection.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include "socks5connection.h" @@ -17,12 +18,13 @@ void TestSocks5Connection::proxy() { QBuffer rxBuffer; QBuffer txBuffer; + quint64 watermark = 0; rxBuffer.open(QIODevice::ReadWrite); rxBuffer.write("abc"); rxBuffer.seek(0); txBuffer.open(QIODevice::ReadWrite); - const auto bytesWritten = Socks5Connection::proxy(&rxBuffer, &txBuffer); - QCOMPARE(bytesWritten, 3); + Socks5Connection::proxy(&rxBuffer, &txBuffer, watermark); + QCOMPARE(txBuffer.buffer().size(), 3); txBuffer.seek(0); const auto output = txBuffer.readAll(); @@ -36,27 +38,100 @@ void TestSocks5Connection::proxy() { void TestSocks5Connection::proxyEmpty() { QBuffer rxBuffer; QBuffer txBuffer; + quint64 watermark = 0; rxBuffer.open(QIODevice::ReadWrite); txBuffer.open(QIODevice::ReadWrite); - const auto bytesWritten = Socks5Connection::proxy(&rxBuffer, &txBuffer); - QCOMPARE(bytesWritten, 0); + Socks5Connection::proxy(&rxBuffer, &txBuffer, watermark); + QCOMPARE(txBuffer.buffer().size(), 0); } /** - * In case of writing to a closed - * IODevice it should return an error (-1) + * No data should be written to a closed device. */ void TestSocks5Connection::proxyClosed() { QBuffer rxBuffer; QBuffer txBuffer; + quint64 watermark = 0; QByteArray data = {10, 'x'}; rxBuffer.open(QIODevice::ReadWrite); rxBuffer.write(data); rxBuffer.seek(0); - const auto bytesWritten = Socks5Connection::proxy(&rxBuffer, &txBuffer); - QCOMPARE(bytesWritten, -1); + Socks5Connection::proxy(&rxBuffer, &txBuffer, watermark); + QCOMPARE(txBuffer.buffer().size(), 0); +} + +/** + * Writes should be flow controlled so that `bytesToWrite()` never exceeds + * 16kB + */ +void TestSocks5Connection::proxyFlowControl() { + constexpr const int FIRST_FRAGMENT_SIZE = 1234; + constexpr const int PROXY_MAX_BUFFER_SIZE = 16 * 1024; + + // We'll need to use a TCP to test out flow control. So create a TCP server. + QTcpServer server; + QCOMPARE(server.listen(QHostAddress::LocalHost), true); + + // Create a TCP socket and connect to the server. + QTcpSocket sendSocket; + sendSocket.connectToHost(QHostAddress::LocalHost, server.serverPort()); + + // There should be a socket we can now accept. + QCOMPARE(server.waitForNewConnection(500), true); + QTcpSocket* recvSocket = server.nextPendingConnection(); + recvSocket->setParent(&server); + QVERIFY(recvSocket != nullptr); + + // The dummy socket should now be connected too. + QCOMPARE(sendSocket.waitForConnected(500), true); + QCOMPARE(sendSocket.state(), QAbstractSocket::ConnectedState); + + // Prepare a small fragment of data to send through the socket. + QByteArray firstData(FIRST_FRAGMENT_SIZE, 'Q'); + QBuffer firstBuffer(&firstData); + firstBuffer.open(QIODevice::ReadOnly); + + // Send the first fragment of data, and check the high watermark. + quint64 watermark = 0; + Socks5Connection::proxy(&firstBuffer, &sendSocket, watermark); + QCOMPARE(watermark, FIRST_FRAGMENT_SIZE); + QCOMPARE(firstBuffer.pos(), FIRST_FRAGMENT_SIZE); + QCOMPARE(sendSocket.bytesToWrite(), FIRST_FRAGMENT_SIZE); + + // Send more data to try and overflow the outbound socket buffer. + QByteArray hugeData(PROXY_MAX_BUFFER_SIZE, 'Z'); + QBuffer hugeBuffer(&hugeData); + hugeBuffer.open(QIODevice::ReadOnly); + Socks5Connection::proxy(&hugeBuffer, &sendSocket, watermark); + QCOMPARE(watermark, PROXY_MAX_BUFFER_SIZE); + QCOMPARE(hugeBuffer.pos(), PROXY_MAX_BUFFER_SIZE - FIRST_FRAGMENT_SIZE); + QCOMPARE(sendSocket.bytesToWrite(), PROXY_MAX_BUFFER_SIZE); + + // Trying to send again should have no effect. + Socks5Connection::proxy(&hugeBuffer, &sendSocket, watermark); + QCOMPARE(watermark, PROXY_MAX_BUFFER_SIZE); + QCOMPARE(hugeBuffer.pos(), PROXY_MAX_BUFFER_SIZE - FIRST_FRAGMENT_SIZE); + QCOMPARE(sendSocket.bytesToWrite(), PROXY_MAX_BUFFER_SIZE); + + // Send the data on the socket. + while (sendSocket.bytesToWrite() > 0) { + QCOMPARE(sendSocket.waitForBytesWritten(500), true); + } + + // On the receiving end of the socket we should be able to read one + // buffer's worth of data. + QCOMPARE(recvSocket->waitForReadyRead(500), true); + QCOMPARE(recvSocket->bytesAvailable(), PROXY_MAX_BUFFER_SIZE); + + // And we should now be able to send the last fragment of unwritten data. + watermark = 0; + qint64 expect = hugeData.length() - hugeBuffer.pos(); + Socks5Connection::proxy(&hugeBuffer, &sendSocket, watermark); + QCOMPARE(watermark, expect); + QCOMPARE(hugeBuffer.pos(), hugeData.length()); + QCOMPARE(sendSocket.bytesToWrite(), expect); } QTEST_MAIN(TestSocks5Connection) diff --git a/extension/socks5proxy/tests/testsocks5connection.h b/extension/socks5proxy/tests/testsocks5connection.h index 3ea8d93e9c..7ab44b4f17 100644 --- a/extension/socks5proxy/tests/testsocks5connection.h +++ b/extension/socks5proxy/tests/testsocks5connection.h @@ -14,4 +14,5 @@ class TestSocks5Connection final : public QObject { void proxyEmpty(); void proxy(); void proxyClosed(); + void proxyFlowControl(); }; diff --git a/linux/flatpak/flatpak-vpn-crates.json b/linux/flatpak/flatpak-vpn-crates.json index a83812e189..c77f715489 100644 --- a/linux/flatpak/flatpak-vpn-crates.json +++ b/linux/flatpak/flatpak-vpn-crates.json @@ -2394,27 +2394,53 @@ { "type": "archive", "archive-type": "tar-gzip", - "url": "https://static.crates.io/crates/thiserror/thiserror-1.0.67.crate", - "sha256": "3b3c6efbfc763e64eb85c11c25320f0737cb7364c4b6336db90aa9ebe27a0bbd", - "dest": "cargo/vendor/thiserror-1.0.67" + "url": "https://static.crates.io/crates/thiserror/thiserror-1.0.69.crate", + "sha256": "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52", + "dest": "cargo/vendor/thiserror-1.0.69" }, { "type": "inline", - "contents": "{\"package\": \"3b3c6efbfc763e64eb85c11c25320f0737cb7364c4b6336db90aa9ebe27a0bbd\", \"files\": {}}", - "dest": "cargo/vendor/thiserror-1.0.67", + "contents": "{\"package\": \"b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52\", \"files\": {}}", + "dest": "cargo/vendor/thiserror-1.0.69", "dest-filename": ".cargo-checksum.json" }, { "type": "archive", "archive-type": "tar-gzip", - "url": "https://static.crates.io/crates/thiserror-impl/thiserror-impl-1.0.67.crate", - "sha256": "b607164372e89797d78b8e23a6d67d5d1038c1c65efd52e1389ef8b77caba2a6", - "dest": "cargo/vendor/thiserror-impl-1.0.67" + "url": "https://static.crates.io/crates/thiserror/thiserror-2.0.3.crate", + "sha256": "c006c85c7651b3cf2ada4584faa36773bd07bac24acfb39f3c431b36d7e667aa", + "dest": "cargo/vendor/thiserror-2.0.3" }, { "type": "inline", - "contents": "{\"package\": \"b607164372e89797d78b8e23a6d67d5d1038c1c65efd52e1389ef8b77caba2a6\", \"files\": {}}", - "dest": "cargo/vendor/thiserror-impl-1.0.67", + "contents": "{\"package\": \"c006c85c7651b3cf2ada4584faa36773bd07bac24acfb39f3c431b36d7e667aa\", \"files\": {}}", + "dest": "cargo/vendor/thiserror-2.0.3", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/thiserror-impl/thiserror-impl-1.0.69.crate", + "sha256": "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1", + "dest": "cargo/vendor/thiserror-impl-1.0.69" + }, + { + "type": "inline", + "contents": "{\"package\": \"4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1\", \"files\": {}}", + "dest": "cargo/vendor/thiserror-impl-1.0.69", + "dest-filename": ".cargo-checksum.json" + }, + { + "type": "archive", + "archive-type": "tar-gzip", + "url": "https://static.crates.io/crates/thiserror-impl/thiserror-impl-2.0.3.crate", + "sha256": "f077553d607adc1caf65430528a576c757a71ed73944b66ebb58ef2bbd243568", + "dest": "cargo/vendor/thiserror-impl-2.0.3" + }, + { + "type": "inline", + "contents": "{\"package\": \"f077553d607adc1caf65430528a576c757a71ed73944b66ebb58ef2bbd243568\", \"files\": {}}", + "dest": "cargo/vendor/thiserror-impl-2.0.3", "dest-filename": ".cargo-checksum.json" }, { diff --git a/nebula/ui/components/MZFlickable.qml b/nebula/ui/components/MZFlickable.qml index a456e2e021..943da1d249 100644 --- a/nebula/ui/components/MZFlickable.qml +++ b/nebula/ui/components/MZFlickable.qml @@ -11,7 +11,7 @@ import Mozilla.VPN 1.0 Flickable { id: vpnFlickable - property var flickContentHeight + property var flickContentHeight: 0 property bool contentExceedsHeight: height < contentHeight property bool hideScollBarOnStackTransition: false property bool addNavbarHeightOffset: navbar.visible diff --git a/nebula/ui/components/MZHelpSheet.qml b/nebula/ui/components/MZHelpSheet.qml index 631cfb6ed5..a24ce49086 100644 --- a/nebula/ui/components/MZHelpSheet.qml +++ b/nebula/ui/components/MZHelpSheet.qml @@ -150,11 +150,10 @@ MZBottomSheet { id: flickable readonly property int maxheight: bottomSheet.maxSheetHeight - headerLayout.implicitHeight - headerLayout.Layout.topMargin + flickContentHeight: layout.implicitHeight + layout.anchors.topMargin + layout.anchors.bottomMargin Layout.fillWidth: true - Layout.preferredHeight: Math.min(layout.implicitHeight + layout.anchors.topMargin + layout.anchors.bottomMargin, maxheight) - - flickContentHeight: layout.implicitHeight + layout.anchors.topMargin + layout.anchors.bottomMargin + implicitHeight: Math.min(flickContentHeight, maxheight) addNavbarHeightOffset: false diff --git a/signature/Cargo.toml b/signature/Cargo.toml index fbdac41f1f..cb12553600 100644 --- a/signature/Cargo.toml +++ b/signature/Cargo.toml @@ -8,7 +8,7 @@ asn1-rs = { version = "0.6.2", features=["datetime"] } oid-registry = "0.7" data-encoding = "2.6.0" ffi-support = "0.4.4" -thiserror = "1.0.67" +thiserror = "2.0.3" hex = "0.4" ring = "0.17.8" x509-parser = { version = "0.16.0", features = ["verify", "validate"] }