feat(net): recognise eMuleAI peer capabilities and demux reserved frames - #1288
feat(net): recognise eMuleAI peer capabilities and demux reserved frames#1288kno wants to merge 16 commits into
Conversation
|
Read the whole diff, and checked every value it puts on or takes off the wire against the implementation that defines it: eMuleAI v1.6 (
Merging while 3.1.0 is in flight3.1.0 is close and master is its release line, while NAT-T and IPv6 are 3.2.0 work. So the question for merging now is not whether a piece is finished but whether a peer can observe it. This PR touches the network in exactly three places, and all three are reads:
The advertise side is what buys that: the assert binds bit-on, tag emission and tagcount together so a later change cannot half-do it. Per-type drop cases rather than a shared fallthrough serve the same end, and the note that a drop never reaches For the pieces that follow, the ones that do change what we send: a compile-time gate defaulting off, not a long-lived branch. A branch must be synced against a fast-moving master, and that drift is what stranded the earlier NAT-T branch; a gate has nothing to sync, gets CI on every commit, and 3.2.0 becomes a one-line default flip. We would ask for two guards with it: CI building both gate states, and the release job asserting it is off on 3.1.x. Stated once, the criterion for the series: each PR must be inert on the wire or gated off by default. Both are checkable from a diff. 1. The user-visible changeClient Details grows a "Vendor capabilities:" row on local and remote GUI, reading "Extended SX", "uTP NAT-T", "IPv6", "Buddy pull", "QUIC NAT-T" or "None". Catalogs are regenerated properly, The capability names are bare literals while 2. My guidance on the 0.72a opcodes named the wrong dispatch pointI asked in #1177 for Fix: nothing in this PR. If the door is still worth propping open it is a case in 3.
|
|
Following up on the review with a UI point I under-weighted first time. Nothing here is a defect, and none of it touches the protocol work. The row is noise on almost every peer
Fix: hide the row when the capability word is empty, rather than showing "None". The label and value can both be skipped; "Vendor capabilities" is opaque in a way its neighbours are notClient Details currently reads: Username, Userhash, Client software, Client version, IP address, User ID, Server IP, Server name, Obfuscation, Kad. Technical, but every row describes something aMule itself participates in, and the two jargon terms both have a home elsewhere in the UI, a preference and a tab. "Vendor capabilities: Extended SX, uTP NAT-T, Buddy pull" is further out on two axes at once. The terms appear nowhere else in aMule, and aMule implements none of them, so there is no action attached. Your own comment says the line reads as "this peer would support X if we did", which is an odd thing for a user-facing string to be. Fix, label: I would avoid naming eMuleAI in the string. Partly to keep a third-party name out of our UI, but mainly because the Fix, values: spell them out. Once the row only appears when it has content, length costs nothing.
That reads as "Protocol extensions: Extended source exchange, NAT traversal (uTP)", which needs no glossary. One consequence worth deciding now rather than after translators have seen the strings: the values are currently bare literals, and I said in the last review that this looked like the right call for protocol identifiers. Spelling them out makes them prose, which argues for wrapping them in Not asking forAnother option I considered was dropping the GUI row entirely until aMule implements one of these, leaving the parsing, the EC tag and the tests to land as they are. I am not asking for that: the row is genuinely useful for diagnosing an interop question in the field, which is exactly the kind of thing this series will generate. But if you would rather defer it, nothing else in the PR depends on it. |
15b9f9b to
fd02d34
Compare
|
On the compile-time gate for the pieces that follow: agreed, and for the reason Two questions before piece 2, both about who writes what. 1. The two guards — yours or ours?They touch your workflows, so we would rather ask than open a PR against For CI building both states: For the release job asserting it is off on 3.1.x: we have not read Happy either way: you add them and we build on top, or you tell us the shape you 2. What the option is called, and its granularity
Granularity matters more than the name. One Piece 3, separatelyYou asked for the address widening to be split into a mechanical type |
|
Three answers, and the third is simpler than you are treating it. 1. GuardsCI: one dedicated job, not a matrix dimension. A second dimension doubles every OS job for asymmetric value. Gate-off breakage is already caught by every existing job, since off is the default; only gate-on is uncovered. One Ubuntu job at one build type closes that, and a compile break in gated code will not be Debug-only or macOS-only in a way the rest would miss. Write it in the same PR as the first gated piece, so it never references a flag that does not exist yet. Release assertion: not yet, and I am withdrawing my own request. The default is off and no release path passes the flag, so the only route into a release build is a packaging-script edit, which is a diff a human reads rather than something 2.
|
|
All four are applied — pushed at 13:46 as
Three things in there you did not ask for and may want to push back on.
A new control id, Not verified: the dialog itself. No display session here, so "the sizer leaves no On your three answers: one dedicated gate-on job written in the same PR as the |
There was a problem hiding this comment.
Reviewed the whole diff, built the monolithic GUI on macOS, and ran the suite: 51/51, including the three new ones. I checked the wire constants against the eMuleAI and emule-qt sources rather than against the description, since that is the part with no runtime signal. CT_MOD_MISCOPTIONS 0xAA and CT_MOD_IP_V6 0xAE, the five UModMiscOptions bits in that order with Reserved : 27, the five OP_NATT_FRAME_* values, and "ip6" / "bi6" all match, and the IsHash() guard on 0xAE matches how eMuleAI both writes and reads it. The frame-length edge lines up too: a type-byte-only datagram is a valid zero-payload frame on both sides, and a protocol-byte-only one is dropped on both. The hidden capability row also leaves no gap, since Build() calls SetSizeHints() after OnInitDialog() has hidden the pair.
One constant does not hold up, and there is one gap.
-
CT_MOD_SVR_IP_V6(0xAF) is not a hello tag. It means the server's own public IPv6 and only ever appears inOP_SERVERIDENT. emule-qt is the only tree that parses it, inside itsOP_SERVERIDENTloop and only to log it (ServerSocket.cpp:328, "informational, we still reach it on the address we dialed"). Nothing emits it at all, not eMuleAI, not ed2k-server. In eMuleAI it is a parked id: its onecase(BaseClient.cpp:1210) sits inside the// RESERVED TAGS FOR THE FUTURE RELEASES OF THIS MOD -->/<--block and only setsm_bUnofficialOpcodes = truefor ghost-mod detection. eMuleAI implemented "advertise my own v6" (0xAE) and never implemented "learn the server's v6".So
m_hasModServerIPv6can only become true when a peer sends a tag nobody sends, in a packet where the reference implementation reads it as evidence against that peer. The field is unreachable by any honest peer, and those 16 bytes are attacker-chosen by construction, stored under a name that reads as server-authoritative and waiting for the dual-stack change to trust it.Fix: drop the case along with
m_modServerIPv6,m_hasModServerIPv6and the two accessors. A server IPv6 belongs in theOP_SERVERIDENTtag loop inServerSocket.cppwhen we get there, not inProcessHelloTypePacket. For a v6 address carried in a hello the tag isCT_MOD_IP_V6(0xAE) for the peer's own, orCT_EMULE_SERVINGBUDDYIPV6(0xA0) for a buddy's, which eMuleAI parses as a hash and which pairs with the"bi6"Kad tag this PR already defines. -
amuleapi does not see the new field.
src/webapipublishes per-client obfuscation status but not the capability word, so the EC surface and the REST surface now differ. Could you addEC_TAG_CLIENT_MOD_CAPABILITIESto the amuleapi client view in this PR, with the curl test and thedocs/api/REFERENCE.mdentry? Same masked word the remote GUI gets.
Everything else I stand behind, including CPeerCapabilities::Set() having no production caller yet. Keep it, it is the setter the change that ships a transport will want.
|
Correction to my review above, and an addition. I checked every constant this PR introduces against the reference sources, but I did not run the sweep in the other direction, and that is the one that decides whether this is a foundation we can build a client on that speaks to eMuleAI, emule-qt and the eMule 0.72a NAT-T edition. Not "is what is here correct" but "is what is correct here". Having now enumerated the hello tag surface of all three trees and diffed it against this PR, two tags are missing.
0.72a parses no vendor tags in its hello at all, only the stock eMule set, so its NAT-T surface falls outside this file entirely. Which means the hello side is complete for all three once these land.
The |
|
All four look right on inspection: I checked the corroboration keying hardest, since the whole property rests on it. Could you rebase onto master? The branch conflicts, and GitHub will not build a merge commit for a conflicting branch, so no CI has run on any of this yet. The four conflicts are all in the amuleapi surface I asked you to add: I will do the full pass and a local monolithic build once it is green. |
Reads eMuleAI's CT_MOD_* vendor tags and misc-options capability bits from a peer hello, and dispatches OP_UDPRESERVEDPROT2 payloads by frame type instead of treating them as malformed traffic. An unknown or unsupported frame type is dropped without advancing the sender's flood or ban counters. aMule advertises none of the five new bits: this change only teaches it to read what eMuleAI sends. Unknown tags never abort a handshake. Negotiated capabilities are surfaced both in the client-details dialog and as External Connection tags, so they remain observable where no GUI can start. (cherry picked from commit abb71be)
The three CT_MOD_* tag ids and the two Kad tag names are wire format shared with eMuleAI, and a wrong value has no runtime signal: aMule would write its IPv6 address under an id the peer decodes as something else, or under a name the peer never looks up, and the handshake would carry on regardless. Pinned as literals rather than read from the symbol under test, so a renumbering or a rename cannot pass. (cherry picked from commit ab9f7ec, test file only; the rest of that commit is Kad identity-rotation work that does not belong to this change.)
ToWire() never touched the eD2k wire; it only fed a debug log line and the EC_TAG_CLIENT_MOD_CAPABILITIES tag for client-details display. The real advertise path is AdvertisedModMiscOptions(), guarded by the static_assert in CUpDownClient::SendHelloTypePacket(). The name invited exactly the misreading amule-org#1177 reported: that a bit added later would be silently stripped on send. Also drop the mask in the renamed accessor. m_bits is masked once on write, in SetFromWire(); Set() and Reset() never introduce reserved bits, so re-masking on every read was dead code. Reported in amule-org#1177, which this does not close: that issue tracks eMuleAI network parity as a whole. (cherry picked from commit 8f7aeed)
The Client Details dialog gained a 'Vendor capabilities:' row and one label per capability bit; the catalogues had not been regenerated to carry them, and the i18n check fails on that drift. Regenerated with scripts/update-po.sh; no translation was hand-edited.
amule-org#1271 replaced the dialog's live CClientRef with a ClientDetailInfo snapshot so a stored Known row can be shown without a session. The capabilities line still read m_client, which no longer exists. They come from the peer's hello, so they are session state: filled from the live client, shown as '-' for a row that has no session, the same way Kad is just below.
The 0xAA capability word is not eMuleAI's alone. emule-qt allocates bit 5 (MODMISC_EXTXS_SKIPTAGS) and bit 10 (MODMISC_HTTPCACHE) in the same word, and MOD_MISCOPT_KNOWN_MASK clears both on the way in, so nothing downstream can see them. That is the behaviour we want today, because aMule acts on neither and a bit it cannot use is a bit it must not relay. It stops being right the moment aMule wants to gate on one: a cleared bit reads as absent rather than as unknown, and no query site can recover it. Say so at the mask, and name widening it as the change that has to come first. Comment only, no behaviour change.
The capability list rendered abbreviations aimed at someone who already
knows the wire format -- "Extended SX", "uTP NAT-T", "Buddy pull" -- and
left them untranslated beside a translated label, which invites the next
reader to wrap them in _() for consistency and get a half-translated row.
Spell them out as prose and translate them, since that is what they now
are. The names carry no protocol meaning: the bit positions are pinned as
literal words in PeerCapabilitiesTest and the bit-to-name pairing by the
new display test, so a reword or a translation cannot move a bit. Keep the
single bit-and-name table so a new bit cannot be added in one place and
forgotten in the other; wxTRANSLATE with wxGetTranslation() at use is what
lets the table stay one table, and it also avoids translating a static
initialiser once, before the locale is loaded, and never again.
GetDisplayText() now returns an empty string for a peer that claims
nothing, instead of _("None"). An empty list is an empty string, and the
caller is the only place that knows how to present the absence -- the
dialog hides its row on exactly this contract, and could not tell "None"
apart from a real claim. Pinned by ClaimingNothingDisplaysAsEmpty, which
also covers a peer that sets only a reserved bit.
Two changes to the same row in Client Details. The label read "Vendor capabilities:", which named eMuleAI's word in the UI. The 0xAA word is not theirs alone -- emule-qt allocates bits in it too -- so a vendor-specific label is wrong as soon as a second implementation turns up in the field. "Protocol extensions:" is accurate, since these are extensions to the eD2k protocol, says nothing about who defined them, and sits beside "Obfuscation:" and "Kad:" without standing out. The row also showed a placeholder for every peer that claimed nothing, which is very nearly every peer on the network, and "-" for every stored row with no session. A row that permanently says nothing takes the space of one that says something, so hide the label and the value together. The sizer closes over a hidden pair and Layout() reflows the rest, so no gap is left behind. The label needs an id of its own for that -- id -1 cannot be found to hide -- appended at the end of the id space like IDT_MOD_CAPABILITIES, since the ids are bound by value.
scripts/update-po.sh. Picks up "Protocol extensions:" and the five spelled-out extension names, and drops "Vendor capabilities:" and the abbreviations they replaced.
… all Two corrections to the eMuleAI hello tags this branch recognises, both from reading the reference trees rather than the tag names. 0xAF is dropped. The name CT_MOD_SVR_IP_V6 reads as the server's own address, and recording it from a peer would state something the wire does not say. In eMuleAI the id appears exactly twice in the whole tree -- the #define, and one case inside the block bracketed by "RESERVED TAGS FOR THE FUTURE RELEASES OF THIS MOD", beside CT_MOD_RESERVED_B, whose only effect is to flag the sender as running an unofficial build. Nothing writes it and the server-ident path never reads it. emule-qt does define it as an address, but as the server's own public IPv6, sent server->client -- which is evidence about a server, not a claim a peer gets to make about one. Reading it as either would be wrong, so it is not named at all: 0xAF now falls through to the unknown-vendor-tag log like any other 0xA? id, and why it is unnamed is written where the next reader will look for it. 0xA0 is added. CT_EMULE_SERVINGBUDDYIPV6 is the v6 counterpart of CT_EMULE_BUDDYIP (0xFC), which this file already parses two cases above: eMuleAI reads it into m_ServingBuddyIP, emule-qt into m_buddyIPv6. It also pairs with the "bi6" Kad tag this branch already decodes, so without it the buddy's v6 address arrived over Kad and was dropped in the hello. Parsed as a hash, the same shape as CT_MOD_IP_V6, and recorded for the dual-stack change: aMule has no IPv6 stack, so like 0xAE nothing reads it yet. The tag ids are pinned as literals in PeerCapabilitiesTest, where a renumbering would otherwise have no runtime signal.
…elieving it 0xAD carries a peer's opinion of the address we are reachable at. The two reference implementations disagree about what to do with it, and the disagreement is the whole content of this change. eMuleAI accepts the tag's integer form and sets its own public IPv4 from it: one peer, unverified, decides what that client believes its own address to be. A client that is wrong about its own address is wrong about whether it is firewalled, about what it publishes to Kad and about where it asks to be called back -- and none of that fails loudly. emule-qt refuses the integer form outright and takes only the 128-bit hash form, then feeds it to a corroboration tracker rather than believing it on one peer's word. This follows emule-qt. The tracker lives in a header of its own, CUpDownClient being unreachable from a unit test, and keys on the address the packet was observed arriving from rather than on the sender's user hash: a hash costs nothing to invent, so a single host could otherwise manufacture as many distinct corroborating peers as any threshold demands. A routable source address cannot, because a reply has to come back through it. Three distinct observed addresses is the threshold. Two would be wrong -- two source addresses is one dual-homed host, one host that reconnected on a new lease, or one attacker holding a second socket, none of which is a second opinion. Three is the smallest count that forces a claimant to hold addresses it does not control alone, and is still reachable in an ordinary session. It is a floor rather than a proof, and the comment says so: three addresses under one operator still agree with each other, which is acceptable only while nothing acts on the result. The candidate table is bounded for the same reason the key is the observed address -- the input is attacker-chosen. Nothing consumes the corroborated address. Like the rest of this branch this is recognition only: aMule has no IPv6 stack, and nothing about what it sends changes. The test suite states the rule rather than waiting for a symptom, since there is no symptom to wait for: one peer is not believed, two are not either, one host repeating itself is one host, disagreeing peers never pool into a quorum, and a claim with no observed source address is ignored rather than counted under a zero key.
amuled has carried EC_TAG_CLIENT_MOD_CAPABILITIES since this branch added it, and the desktop GUI renders it as its "Protocol extensions" row, but the REST surface published a peer's obfuscation status and not its capability word. The two surfaces onto the same peer therefore disagreed about what was known of it, and a headless caller had no way to see the word at all. It reaches the client view by the path obfuscation status already takes: a field on ClientSnapshot, decoded in the refresher off the INC_UPDATE wire, written by WriteClientBaseFields so the list row, the detail object and the client_* SSE payloads all carry it -- a field in the REST writer and not in the diff writer is a row that silently stops updating, which is why EventDiff's Equal() compares it too. Taken as delivered, never re-derived. The daemon has already dropped the bits it does not define (CPeerCapabilities::SetFromWire), and a second copy of that mask in the webapi process would be free to drift from the one that actually saw the handshake, at which point the surface reports something no peer ever claimed. The curl test asserts exactly that: the word carries no bit outside MOD_MISCOPT_KNOWN_MASK, on all three views. A number rather than one of this surface's enumerated tokens, because it is not an enumeration: a peer claims any combination of the bits, so there is no single value to name. The key is unconditional -- 0 is a real answer here, and what nearly every peer on the network reports.
0x0632 went to EC_TAG_CLIENT_CONNECTED in amule-org#1292 while this branch was open, and the generated ECCodes.h then had two cases with value 1586, which the compiler rejects. Moved to 0x0633, the first free code after the client block, and the literal the test pins updated with it.
41559e8 to
c2e7151
Compare
Reinserted by hand while resolving the rebase against amule-org#1291 and amule-org#1292, so the line wrapping no longer matched clang-format 18.
muleunit pulls in MuleDebug.cpp and StringFunctions.cpp, which use CFormat, so every suite needs mulecommon whether its own code touches CFormat or not -- ReservedProtocolFramesTest links it for the same reason. macOS resolved the symbols anyway; gcc and lld did not, so the link failed on Ubuntu and mingw-w64 in both build types and took both clang-tidy jobs down with them.
clang-tidy Tier-2 flags push_back(Candidate()). Its return value is unused: emplace_back() only returns a reference from C++17, and the unit-test targets carry no -std flag and take clang's default of C++14.
|
Rebased onto master. The four conflicts were all in the amuleapi surface, as you Two things the rebase turned up that were invisible before it.
Green now, 14/14. |
got3nks
left a comment
There was a problem hiding this comment.
Rebased clean, 14/14 CI green, and a local monolithic build on macOS passes 52/52. Good catch on the EC code: 0x0632 really had been taken by EC_TAG_CLIENT_CONNECTED from #1295, and a duplicate there is a collision only a Debug build would have caught. Updating Equal() in EventDiff.cpp is the other easy one to miss, since without it the SSE diff suppresses every change to the new field. Appending 43- after 42 rather than slotting it in is right as well.
Three things.
1. The corroboration tracker is missing its primary control, and that is what makes its other gaps matter.
emule-qt filters a claim before tallying it: recordPeerObservedIPv6() drops anything not global unicast, then drops anything not assigned to a local interface, and only then records a vote. Its reasoning is the part worth quoting: "Peers are unauthenticated, so corroboration alone must never be able to make us advertise a foreign address. Requiring the candidate to be one we actually hold bounds the damage to 'picks the wrong one of our own addresses', which is exactly what this tier exists to disambiguate."
So the locally-assigned filter is the security control, and corroboration is only the tie-breaker between addresses we genuinely hold. Here corroboration is the whole of it, and three distinct source addresses is the entire bar for making aMule believe an address it does not own.
That also explains the candidate table. Because emule-qt only ever tallies addresses the host actually holds, its set is naturally tiny and needs no cap at all. PUBLIC_IPV6_CORROBORATION_MAX_CANDIDATES is standing in for the absent filter, and it is a poor substitute: refusing the ninth value permanently converts unbounded memory into permanent denial, and since occupying a slot costs one claim while corroborating a value costs three, denial is the cheaper attack and it never expires. Separately, the file has no timestamps at all, so "three distinct peers agree" really means "three peers have said this at any point since the daemon started", and a corroborated address is never re-validated against a rotating prefix.
Suggested shape, in this order:
- Lift the interface enumeration out of
PrefsUnifiedDlg.cppinto a shared translation unit the daemon links, and add the v6 branch. The cross-platform work is already done and already skips loopback: the Windows path asks forAF_UNSPECand the POSIX path walks every family, and both then filter v6 out. It only has to move becauseamuleddoes not linkmuleappgui.LibSocketAsio.cppholds a second copy of the same enumeration, so there is a call site to consolidate while you are in there. - Gate
AddClaim()on global-unicast and locally-assigned before it tallies, and keep no per-candidate state for rejections, for the reason emule-qt gives. - Then add the window and re-election. Pass the time in rather than calling
time()internally, the wayCUnknownFrameLogThrottlealready takes its tick, so the tests can fail.
With the filter in place the cap stops carrying weight it was never suited to, and the tracker shrinks to the job it is actually good at.
2. protocol_extensions is the only raw bitfield on the API surface. Every other multi-state field there is an enumerated token with an unknown fallback. An integer makes every consumer hardcode aMule's bit meanings, so PeerCapabilities.h's table gets reimplemented in JS for the Web UI and again in every third-party client. Your own justification, that a peer claims any combination of the bits rather than one state, argues for an array of tokens rather than an integer: ["extended_source_exchange", "ipv6"]. That reuses naming that already exists, and v0 has no compatibility to keep, so it is cheap now and awkward later.
3. Field order disagrees three ways. REST writes connected, protocol_extensions, friend_slot; the SSE serialiser writes obfuscation_state, protocol_extensions, connected; both docs show it after connected. Nothing breaks, JSON is unordered, but the sample payloads are meant to mirror real output.
|
One thing my point 1 left implicit, and the obvious reading of "locally-assigned" would get wrong: that list has to be a refreshed cache. Not a startup snapshot, and not a re-scan per claim. emule-qt's What makes the cache safe is what a refresh does, not how often it runs: "The set of addresses we actually hold gates every reflected address, so publish it first. This call also re-validates any reflection already adopted, which is how a prefix renumber gets noticed." So a refresh is not only an allow-list update, it re-checks whatever was believed on the strength of the old list and drops what no longer holds. Without that half, a corroborated address outlives the prefix it came from. Two details in their scan that aMule's IPv4-only enumeration has no reason to have yet: it skips tentative addresses, which are not ours until duplicate address detection finishes, and on Linux it reads Re-scanning on every claim would be wrong in the other direction, since it puts an interface walk on a path any peer can drive. |
Refs #1177
This is piece 1 of the seven items listed in #1177 — peer capability recognition — offered on its own and first, as requested in that thread.
What this adds
Reading of eMuleAI's vendor capability tags from a peer hello, and classification of
OP_UDPRESERVEDPROT2frames by frame type instead of treating them as malformed traffic.src/PeerCapabilities.h— theCT_MOD_MISCOPTIONSbitfield: the five bit positions eMuleAI defines, a decoder that masks reserved bits out of what a peer sends, per-bit accessors, and the display text for each bit.src/ReservedProtocolFrames.h— classification of the byte after0xB2into truncated / known-type / unknown-type, the payload window handed to a handler, and a rate limit so an unknown-frame flood cannot become a log flood.CT_MOD_IP_V6(0xAE) andCT_MOD_SVR_IP_V6(0xAF) tags, and theip6/bi6Kad tag address encoding.KnownBits(). Its earlier name,ToWire(), invited the misreading eMuleAI network parity: interest, and the libutp vendoring question #1177 reports — that a bit added later would be stripped on send — when it only ever fed a log line and the EC tag below.EC_TAG_CLIENT_MOD_CAPABILITIES(0x0632), so a peer's recognised capabilities are readable over External Connections. The Client Details dialog renders the same data, but it needs an X server; the EC tag is what makes the display checkable without one.Wire format
Three constants are fixed by the peer implementation and are pinned in the tests as literals:
CT_MOD_MISCOPTIONS0xAA1u << 1OP_UDPRESERVEDPROT20xB2All three match the maintainer's own reference branch,
got3nks/amule@feat/nat-t-utp, which uses tag id0xAA(src/include/tags/ClientTags.h:61), decodes bit 1 of that word as NAT-T support (MOD_MISCOPT_BIT_NAT_TRAVERSAL = 0x00000002,src/NatTraversal.h:110), and reads0xB2datagrams with a type byte following the protocol byte (src/ClientUDPSocket.cpp:188).Beyond that reference, this piece adds:
0xB2frame-type classification insrc/ReservedProtocolFrames.h. The reference reads a type byte on its own uTP path; this is a classification of the whole frame-type namespace (0x00uTP,0x01QUIC,0x02caps,0x03caps-ack,0xFFkey) with an explicit disposition for a truncated frame and for an unregistered type, so an unknown frame is dropped as a known-unknown rather than counted against the sender.The connect-options byte is deliberately not used
This piece signals nothing through the connect-options byte. As read out of eMuleAI's
Opcodes.h:215-221in the #1177 discussion, that byte claims0x01, 0x02, 0x04, 0x08, 0x20, 0x40, 0x80, leaving0x10as its only free bit and nothing safe to take.A reader coming from the reference branch will find
CONNECT_OPT_BIT_NAT_TRAVERSAL = 0x80there (src/NatTraversal.h:67). That is left out here on purpose, and for that reason:0x80is not free.Not covered
LocalAdvertisedModMiscOptions()returns zero, so aMule advertises none of the five bits and emits noCT_MOD_MISCOPTIONStag at all — an absent tag and an all-zero word mean the same thing to the peer, and the absent one costs no bytes. Negotiation is pieces 5–6.0xB2frame type is classified and then dropped, because the transports that would serve it do not exist in this tree.Checks
Three new suites (
PeerCapabilitiesTest, 11 cases;ReservedProtocolFramesTest, 7;ECClientCapabilitiesTest, 5) bringctestfrom 48 to 51, all passing; clang-format 18 reports no violations on the 22 touched sources.