PortManager: replace uint64_t hash token with exact-tuple PortRangeKey (closes #1805)#1812
Conversation
|
One disclosure on what I validated locally vs what I'm trusting CI to catch on this PR: Validated locally:
Not validated locally, deferred to CI:
If you'd prefer I spin up a Linux container to run the full The diff is bounded (10 production files, 1 new test file, no Rust changes, no FBS changes, no platform-conditional code added) so the CI feedback loop should be short. |
|
Thanks for your great work. mediasoup/worker/include/RTC/WebRtcServer.hpp Lines 122 to 123 in 04bca70 |
|
Nice. Please give us some time to come to this, I think I'll have some time to properly review it on next week. |
|
Thanks both! @penguinol Good catch on
I would vote (1) for review cleanliness, but defer to your preference. @ibc No rush, take the time you need. CI is fixed in 3d418e6 (a test helper I missed when changing the |
|
@999purple999 we want to remove Changes in this PR would be: #include <ankerl/unordered_dense.h>size_t PortManager::PortRangeKeyHash::operator()(const PortRangeKey& key) const noexcept
{
const auto protocolBits = static_cast<uint8_t>(key.protocol);
const auto familyBits = static_cast<uint16_t>(key.bindAddr.ss_family);
auto hashCombine = [](size_t& seed, size_t value)
{
seed ^= value + 0x9e3779b9 + (seed << 6) + (seed >> 2);
};
size_t seed = 0;
switch (key.bindAddr.ss_family)
{
case AF_INET:
{
const auto* in = reinterpret_cast<const sockaddr_in*>(&key.bindAddr);
hashCombine(seed, ankerl::unordered_dense::hash<uint8_t>{}(protocolBits));
hashCombine(seed, ankerl::unordered_dense::hash<uint16_t>{}(familyBits));
hashCombine(seed, ankerl::unordered_dense::hash<uint32_t>{}(in->sin_addr.s_addr));
hashCombine(seed, ankerl::unordered_dense::hash<uint16_t>{}(key.minPort));
hashCombine(seed, ankerl::unordered_dense::hash<uint16_t>{}(key.maxPort));
break;
}
case AF_INET6:
{
const auto* in6 = reinterpret_cast<const sockaddr_in6*>(&key.bindAddr);
const auto* addr = in6->sin6_addr.s6_addr;
uint64_t hi, lo;
std::memcpy(&hi, addr, sizeof(uint64_t));
std::memcpy(&lo, addr + 8, sizeof(uint64_t));
hashCombine(seed, ankerl::unordered_dense::hash<uint8_t>{}(protocolBits));
hashCombine(seed, ankerl::unordered_dense::hash<uint16_t>{}(familyBits));
hashCombine(seed, ankerl::unordered_dense::hash<uint64_t>{}(hi));
hashCombine(seed, ankerl::unordered_dense::hash<uint64_t>{}(lo));
hashCombine(seed, ankerl::unordered_dense::hash<uint16_t>{}(key.minPort));
hashCombine(seed, ankerl::unordered_dense::hash<uint16_t>{}(key.maxPort));
break;
}
default:
{
hashCombine(seed, ankerl::unordered_dense::hash<uint8_t>{}(protocolBits));
hashCombine(seed, ankerl::unordered_dense::hash<uint16_t>{}(familyBits));
hashCombine(seed, ankerl::unordered_dense::hash<uint16_t>{}(key.minPort));
hashCombine(seed, ankerl::unordered_dense::hash<uint16_t>{}(key.maxPort));
break;
}
}
return seed;
} |
closes versatica#1805) The previous GeneratePortRangeHash collapsed distinct (protocol, address, port range) tuples into one uint64_t bucket because of two lossy operations: IPv4: hash |= (address >> 2) << 2; // bottom 2 bits dropped IPv6: address = a[0] ^ a[1] ^ a[2] ^ a[3]; // 128 bits XOR-folded to 32 Downstream, `GetOrCreatePortRange` treats a hash hit as "same range" and `Unbind(hash, port)` releases ports against whatever PortRange the hash maps to. In a multi-tenant or multi-interface mediasoup deployment with nearby IPv4 addresses (any /30 block) or XOR-colliding IPv6 addresses, two unrelated bindings get silently merged into one PortRange. The `Unbind` path then releases ports from the wrong tenant's range. This replaces the hash-as-key design with a struct-as-key design: class PortRangeKey { Protocol protocol; sockaddr_storage bindAddr; uint16_t minPort; uint16_t maxPort; bool operator==(const PortRangeKey&) const noexcept; }; struct PortRangeKeyHash { size_t operator()(const PortRangeKey&) const noexcept; }; absl::flat_hash_map<PortRangeKey, PortRange, PortRangeKeyHash> mapPortRanges; Map equality is exact-tuple. The hash function (absl::HashOf over protocol + family + raw address bytes + range bounds) is for bucket distribution only; even on hash collision, key equality keeps distinct tuples in distinct entries. API change (per @ibc 2026-05-28: "I am fine with the proposed changes, no need to keep the uint64_t token"): `Bind` now outputs a `PortRangeKey` and `Unbind` takes one. Callers updated: UdpSocket, TcpServer, PipeTransport, PlainTransport, WebRtcServer, WebRtcTransport. Tests: new TestPortManager.cpp asserts that - identical tuples compare equal (correctness), - all 4 IPv4 addresses in 192.168.1.0/30 produce distinct keys (regression for the IPv4 /30 collision), - IPv6 word-swap collisions produce distinct keys (regression for the IPv6 XOR fold), - protocol, range bounds, and family each independently differentiate keys. Co-authored-by: penguinol <penguinol@users.noreply.github.com>
makeUdpSocket lambda was still constructing UdpSocket range ctor with uint64_t portRangeHash as the 6th argument. After replacing the token type with PortRangeKey in b6ff2d8, the helper needed the new type too.
clang-tidy on worker-clang-tidy job flagged readability-identifier-naming violations on the two helper functions in TestPortManager.cpp: MakeV4 → makeV4 MakeV6 → makeV6 mediasoup convention is lowerCamelCase for free functions. Mechanical fix of the only two warnings clang-tidy raised on this PR.
4720cb9 to
bdf22cc
Compare
|
Done @ibc. Rebased the branch on One tiny deviation worth flagging: I split the IPv6 16-byte address into two Branch: https://github.com/999purple999/mediasoup/tree/fix/1805-port-range-tuple-key Let me know if you want anything else trimmed before merge. |
|
@999purple999 please run "npm run release:check" and, within worker folder) "make format". And ping here so I notice the commit and run CI again. |
|
Hey @ibc, quick update. I tried Looking at the CI run on So I'm reluctant to push a "format-only" commit that I can't verify locally on the right toolchain. Could you re-run CI on the current head and ping me with the actual failure if any? If something specific is off-style I'll fix it surgically. Thanks for the patience. |
|
Can you please give me write access to your PR so I can format it? |
|
I'm fixing lint errors and some cosmetic minor things. |
- Do not include headers already present in `common.hpp`. - Add missing `#include "RTC/PortManager.hpp" in some files. - Reorder classes/structs/methods. - Use `std::addressof(x)` instead of `&c`. - Do not use `using`. Be always explicit and include all namespaces.
|
@999purple999 I've pushed cosmetic changes and CHANGELOG to your branch. |
|
I've created a separate ticket to refactor/replace the |
|
@999purple999 I think this is ready to merge, right? |
|
Yes @ibc, ready to merge from my side. Thanks for the cosmetic pass, the CHANGELOG entry, the @penguinol attribution, and the v3 merge. Re #1815: happy to take the TransportTuple follow-up once this lands. The same struct + hashCombine pattern should drop in cleanly; the only piece worth thinking through is whether Thanks both, this has been a good review cycle. |
|
Great, merging. Thanks a lot, guys! |
Let's please discuss about this in #1815 |
|
Thanks @ibc, @jmillan, @penguinol. Will open the #1815 follow-up within the week as agreed. |
Closes #1805.
Picks up the design @penguinol proposed in #1805 (comment) and the architectural greenlight @ibc gave in #1805 (comment) (no need to keep the
uint64_ttoken).Root cause (in current code, pre-fix)
PortManager::GeneratePortRangeHashmangles the bind address before placing it in theuint64_thash:Both branches are lossy. Downstream,
mapPortRangesis keyed on the hash andGetOrCreatePortRangetreats a hash hit as "same range":Then
Unbind(hash, port)releases ports against the mergedPortRange. In a multi-tenant or multi-interface deployment with nearby IPv4 addresses or XOR-colliding IPv6 addresses, two unrelated bindings get silently merged, andUnbindreleases ports from whichever tuple landed second.Concrete collision examples:
192.168.1.0,192.168.1.1,192.168.1.2,192.168.1.3(any /30) all map to the same hash for the same range. Common in container networks with sequential IPs or load-balanced front-ends.Fix
Replace the hash-as-key design with a struct-as-key design. The map's equality is exact-tuple; the hash function is purely for bucket distribution and is allowed to collide without harm (key equality keeps distinct tuples in distinct entries).
PortRangeKeyHash::operator()usesabsl::HashOfoverprotocol,family, raw address bytes (sin_addr.s_addrorabsl::MakeSpan(sin6_addr.s6_addr)),minPort,maxPort. No bit-mangling.PortRangeKey::operator==is field-equal withstd::memcmpon the IPv6 address bytes and direct uint32 compare on IPv4.API change
Bindnow outputs aPortRangeKeyandUnbindtakes one:@ibc green-lit the breaking change in the issue thread. The new struct is movable, copyable, and trivially storable as a member by callers (already done in
UdpSocket,TcpServer).Callers updated to hold a
PortRangeKeyinstead ofuint64_t portRangeHash:UdpSocket(ctor + dtor signatures + member)TcpServer(ctor + dtor signatures + member)PipeTransport(2 local variables)PlainTransport(3 local variables)WebRtcServer(4 local variables)WebRtcTransport(4 local variables)Total: 10 production source files touched.
GeneratePortRangeHashis removed.Tests
New
worker/test/src/RTC/TestPortManager.cppasserts:(proto, addr, range)produces equal keys and equal hashes.192.168.1.{0,1,2,3}produce 4 distinct keys.(addr, range)for UDP and TCP produces distinct keys.(40000, 40099)vs(40000, 40100)vs(40001, 40099)are 3 distinct keys.0.0.0.0and::are distinct keys.The full suite (
npm testinworker/) is what CI will run on this PR.Notes
Protocolis moved from private to public nested soPortRangeKeycallers can construct keys in tests. The enum has no runtime cost change.#ifdefbranches, no platform-conditional code.Dump()method previously printedhash: %lu. It now printsprotocol,family,minPort,maxPort. Operational logs that grepped for the hash value will need to adapt, but the protocol/family/range tuple is much more useful for debugging anyway.Co-authored
@penguinol drafted the design in the issue thread (struct +
absl::HashOflayout) and gave the explicit "I'm ok if you've already fix it" go-ahead. Credit lands in the commit trailer (Co-authored-by: penguinol).@ibc cleared the breaking change in the same thread ("no need to keep the uint64_t token").
Happy to iterate on naming, struct visibility, or test coverage if you'd prefer a different shape.