mirror of
https://github.com/Chatterino/chatterino2.git
synced 2026-08-24 10:04:53 -05:00
refactor(websockets): Balance IPv4 and IPv6 attempts (#6804)
Reviewed-by: Rasmus Karlsson <rasmus.karlsson@pajlada.com>
This commit is contained in:
@@ -113,6 +113,7 @@
|
||||
- Dev: Updated `pajlada-settings` to v0.5.0. (#6797)
|
||||
- Dev: Updated `pajlada-serialize` to v0.2.1. (#6797)
|
||||
- Dev: Updated `pajlada-signals` to v0.1.1. (#6797)
|
||||
- Dev: Balance IPv4 and IPv6 connection attempts. (#6804)
|
||||
|
||||
## 2.5.4
|
||||
|
||||
|
||||
@@ -61,6 +61,8 @@ set(SOURCE_FILES
|
||||
|
||||
common/websockets/WebSocketPool.cpp
|
||||
common/websockets/WebSocketPool.hpp
|
||||
common/websockets/detail/BalancedResolverResults.cpp
|
||||
common/websockets/detail/BalancedResolverResults.hpp
|
||||
common/websockets/detail/WebSocketConnection.cpp
|
||||
common/websockets/detail/WebSocketConnection.hpp
|
||||
common/websockets/detail/WebSocketConnectionImpl.cpp
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
// SPDX-FileCopyrightText: 2026 Contributors to Chatterino <https://chatterino.com>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "common/websockets/detail/BalancedResolverResults.hpp"
|
||||
|
||||
namespace chatterino::ws::detail {
|
||||
|
||||
BalancedResolverResults::BalancedResolverResults(
|
||||
const Protocol::resolver::results_type &results)
|
||||
: entries(results.begin(), results.end())
|
||||
{
|
||||
}
|
||||
|
||||
BalancedResolverResults::BalancedResolverResults(std::vector<Entry> entries)
|
||||
: entries(std::move(entries))
|
||||
{
|
||||
}
|
||||
|
||||
std::optional<BalancedResolverResults::Entry>
|
||||
BalancedResolverResults::advanceEntry()
|
||||
{
|
||||
if (this->nextIsIPv6)
|
||||
{
|
||||
// advance v6 first
|
||||
if (this->advanceIPv6() || this->advanceIPv4())
|
||||
{
|
||||
return this->currentEntry();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// advance v4 first
|
||||
if (this->advanceIPv4() || this->advanceIPv6())
|
||||
{
|
||||
return this->currentEntry();
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<BalancedResolverResults::Entry>
|
||||
BalancedResolverResults::currentEntry() const
|
||||
{
|
||||
if (this->currentIdx < this->entries.size())
|
||||
{
|
||||
return this->entries[this->currentIdx];
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
void BalancedResolverResults::reset()
|
||||
{
|
||||
this->nextIPv4Idx = 0;
|
||||
this->nextIPv6Idx = 0;
|
||||
this->currentIdx = std::numeric_limits<size_t>::max();
|
||||
this->nextIsIPv6 = true;
|
||||
}
|
||||
|
||||
bool BalancedResolverResults::advanceIPv4()
|
||||
{
|
||||
for (; this->nextIPv4Idx < this->entries.size(); this->nextIPv4Idx++)
|
||||
{
|
||||
if (this->entries[this->nextIPv4Idx].endpoint().address().is_v4())
|
||||
{
|
||||
this->currentIdx = this->nextIPv4Idx++;
|
||||
this->nextIsIPv6 = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool BalancedResolverResults::advanceIPv6()
|
||||
{
|
||||
for (; this->nextIPv6Idx < this->entries.size(); this->nextIPv6Idx++)
|
||||
{
|
||||
if (this->entries[this->nextIPv6Idx].endpoint().address().is_v6())
|
||||
{
|
||||
this->currentIdx = this->nextIPv6Idx++;
|
||||
this->nextIsIPv6 = false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace chatterino::ws::detail
|
||||
@@ -0,0 +1,62 @@
|
||||
// SPDX-FileCopyrightText: 2026 Contributors to Chatterino <https://chatterino.com>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <boost/asio/ip/basic_resolver_results.hpp>
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
namespace chatterino::ws::detail {
|
||||
|
||||
/// An iterator over resolver results ensuring IPv4 and IPv6 entries are
|
||||
/// balanced when iterating where possible.
|
||||
///
|
||||
/// If enough records are available, an IPv4 record is followed by an IPv6 one
|
||||
/// and vice versa. The resolver starts with IPv4 entries.
|
||||
class BalancedResolverResults
|
||||
{
|
||||
public:
|
||||
using Protocol = boost::asio::ip::tcp;
|
||||
using Entry = boost::asio::ip::basic_resolver_entry<Protocol>;
|
||||
|
||||
BalancedResolverResults() = default;
|
||||
explicit BalancedResolverResults(
|
||||
const Protocol::resolver::results_type &results);
|
||||
explicit BalancedResolverResults(std::vector<Entry> entries);
|
||||
|
||||
/// Advance to the next entry and return that one.
|
||||
///
|
||||
/// Once the end is reached, `std::nullopt` is returned.
|
||||
std::optional<Entry> advanceEntry();
|
||||
|
||||
/// Get the current entry.
|
||||
///
|
||||
/// This only returns an entry after `advanceEntry` has been called.
|
||||
std::optional<Entry> currentEntry() const;
|
||||
|
||||
/// Reset the iteration state.
|
||||
void reset();
|
||||
|
||||
private:
|
||||
/// Advance the IPv4 iterator.
|
||||
/// Returns true if `currentIdx` now points to an IPv4 entry.
|
||||
bool advanceIPv4();
|
||||
|
||||
/// Advance the IPv6 iterator.
|
||||
/// Returns true if `currentIdx` now points to an IPv6 entry.
|
||||
bool advanceIPv6();
|
||||
|
||||
size_t nextIPv4Idx = 0;
|
||||
size_t nextIPv6Idx = 0;
|
||||
bool nextIsIPv6 = true;
|
||||
|
||||
size_t currentIdx = std::numeric_limits<size_t>::max();
|
||||
|
||||
std::vector<Entry> entries;
|
||||
};
|
||||
|
||||
} // namespace chatterino::ws::detail
|
||||
@@ -99,23 +99,22 @@ void WebSocketConnectionHelper<Derived, Inner>::onResolve(
|
||||
return;
|
||||
}
|
||||
|
||||
this->resolvedEndpoints = results;
|
||||
this->resolvedEndpoints = BalancedResolverResults(results);
|
||||
|
||||
this->tryConnect(this->resolvedEndpoints.begin());
|
||||
this->tryConnect(this->resolvedEndpoints.advanceEntry());
|
||||
}
|
||||
|
||||
template <typename Derived, typename Inner>
|
||||
void WebSocketConnectionHelper<Derived, Inner>::tryConnect(
|
||||
boost::asio::ip::tcp::resolver::results_type::const_iterator
|
||||
endpointIterator)
|
||||
std::optional<BalancedResolverResults::Entry> entry)
|
||||
{
|
||||
if (endpointIterator == this->resolvedEndpoints.end())
|
||||
if (!entry)
|
||||
{
|
||||
this->fail("Ran out of resolved endpoints"sv, u"connect");
|
||||
return;
|
||||
}
|
||||
|
||||
const auto &endpoint = endpointIterator->endpoint();
|
||||
auto endpoint = entry->endpoint();
|
||||
|
||||
qCDebug(chatterinoWebsocket)
|
||||
<< *this << "connect to" << endpoint.address().to_string();
|
||||
@@ -127,16 +126,14 @@ void WebSocketConnectionHelper<Derived, Inner>::tryConnect(
|
||||
.async_connect(endpoint,
|
||||
beast::bind_front_handler(
|
||||
&WebSocketConnectionHelper::onTcpHandshake,
|
||||
this->shared_from_this(), endpointIterator));
|
||||
this->shared_from_this(), *std::move(entry)));
|
||||
}
|
||||
|
||||
template <typename Derived, typename Inner>
|
||||
void WebSocketConnectionHelper<Derived, Inner>::onTcpHandshake(
|
||||
boost::asio::ip::tcp::resolver::results_type::const_iterator
|
||||
endpointIterator,
|
||||
boost::system::error_code ec)
|
||||
const BalancedResolverResults::Entry &entry, boost::system::error_code ec)
|
||||
{
|
||||
const auto &ep = endpointIterator->endpoint();
|
||||
const auto &ep = entry.endpoint();
|
||||
|
||||
if (ec)
|
||||
{
|
||||
@@ -151,7 +148,7 @@ void WebSocketConnectionHelper<Derived, Inner>::onTcpHandshake(
|
||||
<< *this << "closing websocket after error" << ec.message();
|
||||
}
|
||||
|
||||
this->tryConnect(++endpointIterator);
|
||||
this->tryConnect(this->resolvedEndpoints.advanceEntry());
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "common/websockets/detail/BalancedResolverResults.hpp"
|
||||
#include "common/websockets/detail/WebSocketConnection.hpp"
|
||||
|
||||
#include <boost/asio/ssl/context.hpp>
|
||||
@@ -69,12 +70,9 @@ private:
|
||||
///
|
||||
/// If the iterator is invalid, we have run out of endpoints to try, and deem this
|
||||
/// connection a failure.
|
||||
void tryConnect(boost::asio::ip::tcp::resolver::results_type::const_iterator
|
||||
endpointIterator);
|
||||
void onTcpHandshake(
|
||||
boost::asio::ip::tcp::resolver::results_type::const_iterator
|
||||
endpointIterator,
|
||||
boost::system::error_code ec);
|
||||
void tryConnect(std::optional<BalancedResolverResults::Entry> entry);
|
||||
void onTcpHandshake(const BalancedResolverResults::Entry &entry,
|
||||
boost::system::error_code ec);
|
||||
void onWsHandshake(boost::system::error_code ec);
|
||||
|
||||
void onReadDone(boost::system::error_code ec, size_t bytesRead);
|
||||
@@ -86,7 +84,7 @@ private:
|
||||
///
|
||||
/// When we successfully resolve the host, we try to connect by
|
||||
/// iterating over these results.
|
||||
boost::asio::ip::tcp::resolver::results_type resolvedEndpoints;
|
||||
BalancedResolverResults resolvedEndpoints;
|
||||
};
|
||||
|
||||
/// A WebSocket connection over TLS (wss://).
|
||||
|
||||
@@ -61,6 +61,7 @@ set(test_SOURCES
|
||||
${CMAKE_CURRENT_LIST_DIR}/src/TwitchUserColor.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/src/FunctionRef.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/src/InputHighlighter.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/src/BalancedResolverResults.cpp
|
||||
|
||||
${CMAKE_CURRENT_LIST_DIR}/src/lib/Snapshot.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/src/lib/Snapshot.hpp
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
// SPDX-FileCopyrightText: 2026 Contributors to Chatterino <https://chatterino.com>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "common/websockets/detail/BalancedResolverResults.hpp"
|
||||
|
||||
#include "Test.hpp"
|
||||
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
|
||||
using namespace chatterino::ws::detail;
|
||||
|
||||
namespace {
|
||||
|
||||
using Entry = BalancedResolverResults::Entry;
|
||||
|
||||
void runCheck(BalancedResolverResults results,
|
||||
const std::vector<Entry> &expectedOrder)
|
||||
{
|
||||
ASSERT_FALSE(expectedOrder.empty());
|
||||
|
||||
for (size_t j = 0; j < 4; j++)
|
||||
{
|
||||
EXPECT_EQ(results.currentEntry(), std::nullopt);
|
||||
for (size_t i = 0; i < expectedOrder.size(); i++)
|
||||
{
|
||||
EXPECT_EQ(results.advanceEntry()->endpoint(),
|
||||
expectedOrder[i].endpoint())
|
||||
<< i;
|
||||
EXPECT_EQ(results.currentEntry()->endpoint(),
|
||||
expectedOrder[i].endpoint())
|
||||
<< i;
|
||||
}
|
||||
EXPECT_EQ(results.advanceEntry(), std::nullopt);
|
||||
EXPECT_EQ(results.currentEntry()->endpoint(),
|
||||
expectedOrder.back().endpoint());
|
||||
|
||||
results.reset();
|
||||
}
|
||||
}
|
||||
|
||||
Entry makeEntry(const boost::asio::ip::address &addr)
|
||||
{
|
||||
return {
|
||||
boost::asio::ip::tcp::endpoint{addr, 443},
|
||||
"host.example.com",
|
||||
"service",
|
||||
};
|
||||
}
|
||||
|
||||
Entry makeV4(const char *spec)
|
||||
{
|
||||
return makeEntry(boost::asio::ip::make_address_v4(spec));
|
||||
}
|
||||
|
||||
Entry makeV6(const char *spec)
|
||||
{
|
||||
return makeEntry(boost::asio::ip::make_address_v6(spec));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(BalancedResolverResults, empty)
|
||||
{
|
||||
BalancedResolverResults results;
|
||||
ASSERT_EQ(results.currentEntry(), std::nullopt);
|
||||
ASSERT_EQ(results.advanceEntry(), std::nullopt);
|
||||
ASSERT_EQ(results.currentEntry(), std::nullopt);
|
||||
ASSERT_EQ(results.advanceEntry(), std::nullopt);
|
||||
ASSERT_EQ(results.currentEntry(), std::nullopt);
|
||||
ASSERT_EQ(results.advanceEntry(), std::nullopt);
|
||||
ASSERT_EQ(results.currentEntry(), std::nullopt);
|
||||
ASSERT_EQ(results.advanceEntry(), std::nullopt);
|
||||
results.reset();
|
||||
ASSERT_EQ(results.currentEntry(), std::nullopt);
|
||||
ASSERT_EQ(results.advanceEntry(), std::nullopt);
|
||||
}
|
||||
|
||||
TEST(BalancedResolverResults, singleV4)
|
||||
{
|
||||
std::vector<Entry> entries{makeV4("10.10.10.1")};
|
||||
runCheck(BalancedResolverResults(entries), entries);
|
||||
}
|
||||
|
||||
TEST(BalancedResolverResults, singleV6)
|
||||
{
|
||||
std::vector<Entry> entries{makeV6("1234:5678:9abc:def::1")};
|
||||
runCheck(BalancedResolverResults(entries), entries);
|
||||
}
|
||||
|
||||
TEST(BalancedResolverResults, singleV4AndV6)
|
||||
{
|
||||
std::vector<Entry> entries{
|
||||
makeV6("1234:5678:9abc:def::1"),
|
||||
makeV4("10.10.10.1"),
|
||||
};
|
||||
runCheck(BalancedResolverResults(entries), entries);
|
||||
}
|
||||
|
||||
TEST(BalancedResolverResults, doubleV4)
|
||||
{
|
||||
runCheck(BalancedResolverResults({
|
||||
makeV4("10.10.10.1"),
|
||||
makeV4("10.10.10.2"),
|
||||
}),
|
||||
{
|
||||
makeV4("10.10.10.1"),
|
||||
makeV4("10.10.10.2"),
|
||||
});
|
||||
}
|
||||
|
||||
TEST(BalancedResolverResults, doubleV6)
|
||||
{
|
||||
runCheck(BalancedResolverResults({
|
||||
makeV6("1234:5678:9abc:def::1"),
|
||||
makeV6("1234:5678:9abc:def::2"),
|
||||
}),
|
||||
{
|
||||
makeV6("1234:5678:9abc:def::1"),
|
||||
makeV6("1234:5678:9abc:def::2"),
|
||||
});
|
||||
}
|
||||
|
||||
TEST(BalancedResolverResults, doubleV4SingleV6)
|
||||
{
|
||||
runCheck(BalancedResolverResults({
|
||||
makeV6("1234:5678:9abc:def::1"),
|
||||
makeV4("10.10.10.1"),
|
||||
makeV4("10.10.10.2"),
|
||||
}),
|
||||
{
|
||||
makeV6("1234:5678:9abc:def::1"),
|
||||
makeV4("10.10.10.1"),
|
||||
makeV4("10.10.10.2"),
|
||||
});
|
||||
}
|
||||
|
||||
TEST(BalancedResolverResults, singleV4DoubleV6)
|
||||
{
|
||||
runCheck(BalancedResolverResults({
|
||||
makeV6("1234:5678:9abc:def::1"),
|
||||
makeV6("1234:5678:9abc:def::2"),
|
||||
makeV4("10.10.10.1"),
|
||||
}),
|
||||
{
|
||||
makeV6("1234:5678:9abc:def::1"),
|
||||
makeV4("10.10.10.1"),
|
||||
makeV6("1234:5678:9abc:def::2"),
|
||||
});
|
||||
}
|
||||
|
||||
TEST(BalancedResolverResults, doubleV4AndV6)
|
||||
{
|
||||
runCheck(BalancedResolverResults({
|
||||
makeV4("10.10.10.1"),
|
||||
makeV4("10.10.10.2"),
|
||||
makeV6("1234:5678:9abc:def::1"),
|
||||
makeV6("1234:5678:9abc:def::2"),
|
||||
}),
|
||||
{
|
||||
makeV6("1234:5678:9abc:def::1"),
|
||||
makeV4("10.10.10.1"),
|
||||
makeV6("1234:5678:9abc:def::2"),
|
||||
makeV4("10.10.10.2"),
|
||||
});
|
||||
}
|
||||
|
||||
TEST(BalancedResolverResults, tripleV6)
|
||||
{
|
||||
runCheck(BalancedResolverResults({
|
||||
makeV6("1234:5678:9abc:def::1"),
|
||||
makeV6("1234:5678:9abc:def::2"),
|
||||
makeV6("1234:5678:9abc:def::3"),
|
||||
}),
|
||||
{
|
||||
makeV6("1234:5678:9abc:def::1"),
|
||||
makeV6("1234:5678:9abc:def::2"),
|
||||
makeV6("1234:5678:9abc:def::3"),
|
||||
});
|
||||
}
|
||||
|
||||
TEST(BalancedResolverResults, tripleV4)
|
||||
{
|
||||
runCheck(BalancedResolverResults({
|
||||
makeV4("10.10.10.1"),
|
||||
makeV4("10.10.10.2"),
|
||||
makeV4("10.10.10.3"),
|
||||
}),
|
||||
{
|
||||
makeV4("10.10.10.1"),
|
||||
makeV4("10.10.10.2"),
|
||||
makeV4("10.10.10.3"),
|
||||
});
|
||||
}
|
||||
|
||||
TEST(BalancedResolverResults, tripleV4SingleV6)
|
||||
{
|
||||
runCheck(BalancedResolverResults({
|
||||
makeV4("10.10.10.1"),
|
||||
makeV4("10.10.10.2"),
|
||||
makeV6("1234:5678:9abc:def::1"),
|
||||
makeV4("10.10.10.3"),
|
||||
}),
|
||||
{
|
||||
makeV6("1234:5678:9abc:def::1"),
|
||||
makeV4("10.10.10.1"),
|
||||
makeV4("10.10.10.2"),
|
||||
makeV4("10.10.10.3"),
|
||||
});
|
||||
}
|
||||
|
||||
TEST(BalancedResolverResults, singleV4TripleV6)
|
||||
{
|
||||
runCheck(BalancedResolverResults({
|
||||
makeV6("1234:5678:9abc:def::1"),
|
||||
makeV6("1234:5678:9abc:def::2"),
|
||||
makeV4("10.10.10.1"),
|
||||
makeV6("1234:5678:9abc:def::3"),
|
||||
}),
|
||||
{
|
||||
makeV6("1234:5678:9abc:def::1"),
|
||||
makeV4("10.10.10.1"),
|
||||
makeV6("1234:5678:9abc:def::2"),
|
||||
makeV6("1234:5678:9abc:def::3"),
|
||||
});
|
||||
}
|
||||
|
||||
TEST(BalancedResolverResults, tripleV4AndV6)
|
||||
{
|
||||
runCheck(BalancedResolverResults({
|
||||
makeV4("10.10.10.1"),
|
||||
makeV4("10.10.10.2"),
|
||||
makeV4("10.10.10.3"),
|
||||
makeV6("1234:5678:9abc:def::1"),
|
||||
makeV6("1234:5678:9abc:def::2"),
|
||||
makeV6("1234:5678:9abc:def::3"),
|
||||
}),
|
||||
{
|
||||
makeV6("1234:5678:9abc:def::1"),
|
||||
makeV4("10.10.10.1"),
|
||||
makeV6("1234:5678:9abc:def::2"),
|
||||
makeV4("10.10.10.2"),
|
||||
makeV6("1234:5678:9abc:def::3"),
|
||||
makeV4("10.10.10.3"),
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user