feat(btest): TCP multi-connection fan-out (#87) + UDP client receive CHR gate (#88) - #99
Conversation
📝 WalkthroughWalkthroughThe PR adds TCP multi-connection fan-out for unauthenticated btest client sessions, records realized active connections, changes the connection-count warning to use realized fan-out, and updates validation coverage and documentation. Changesbtest client fan-out and validation
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related issues
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
@coderabbitai review Note for reviewers: this PR is stacked on #98 (base is its branch) to keep this diff scoped to the #87 fan-out. CI workflows and CodeRabbit auto-review only run on |
|
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Pull request overview
Implements TCP multi-connection fan-out for centrs btest client when --connection-count > 1 by opening negotiated secondary data connections and aggregating counters across streams, along with updated reporting (activeConnections), warnings, tests, and docs.
Changes:
- Open
connection-count - 1secondary TCP connections using the negotiated session token and drive bulk loops into shared counters (activeConnectionsadded to outputs). - Update warning behavior to reflect realized fan-out (post-session) rather than pre-session “single stream” messaging.
- Add/expand unit + CHR integration coverage and refresh btest docs/error-page content for the new behavior.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| test/unit/btest.test.ts | Updates secondary-join codec expectation to include the 0x02 marker byte. |
| test/unit/btest-session.test.ts | Adds loopback unit coverage asserting N TCP connections open and carry bulk bytes across all connections. |
| test/unit/btest-command.test.ts | Updates mocked summaries for activeConnections and adjusts warning tests to use realized fan-out. |
| test/integration/btest-client.test.ts | Extends CHR integration coverage to assert activeConnections == 4 for TCP fan-out. |
| src/protocols/btest.ts | Updates encodeSecondaryJoin to include the 0x02 join marker (and related comment text). |
| src/protocols/btest-session.ts | Implements client-side secondary connection opening and secondary bulk loops; adds activeConnections to summary. |
| src/core/error-catalog.ts | Refines the catalog summary for the connection-count warning to match new semantics. |
| src/btest.ts | Plumbs activeConnections into output and changes warning emission to trigger only when realized < requested. |
| docs/MATRIX.md | Updates btest matrix narrative to reflect fan-out implementation status. |
| docs/errors/routeros/btest-connection-count-single-stream.md | Updates the warning page to describe “requested vs realized” fan-out and authenticated fallback behavior. |
| commands/btest/README.md | Updates CLI docs and grounding notes to reflect client fan-out behavior and limitations. |
| commands/btest/examples.md | Clarifies server-side fan-out is still ungated and adds a new gated client multi-connection example. |
| * Scope: UDP, plus TCP single- **and** multi-connection. For `connection-count > 1` | ||
| * the client reads the session token from the primary's OK, opens the extra data | ||
| * connections (each: server HELLO → 16-byte join `[token:u16 BE][0x02][0 …]` → | ||
| * server OK), and drives them into one shared `BandwidthCounters` so throughput | ||
| * aggregates. The secondary-join format is grounded byte-for-byte against RouterOS |
| * The 16-byte "command" a secondary TCP connection sends to join an existing | ||
| * session — the token in bytes 0-1 (big-endian), the rest zero. The server | ||
| * answers `01 <tokenHi> <tokenLo> 00` and the connection joins without auth. | ||
| * session — the token in bytes 0-1 (big-endian), a constant `0x02` join marker in | ||
| * byte 2, the rest zero. The server answers `01 <tokenHi> <tokenLo> 00` and the | ||
| * connection joins without auth. Grounded byte-for-byte against RouterOS 7.23.1 | ||
| * (`12 34 02 00 …`, direction-independent — see the btest-session.ts header). |
| `connection-count` reaches the server's command packet (#84) and centrs opens the | ||
| negotiated extra TCP data connections — multi-connection **fan-out** (#87), | ||
| grounded byte-for-byte against RouterOS 7.23.1 (secondary join | ||
| `[token:u16 BE][0x02][0 …]`) and CHR-gated for throughput-rises; authenticated | ||
| sessions stay single-stream (warned); TCP `direction=both` demuxes the server's |
Added: #88 UDP client receive/both CHR gate (commit c0492c2)The plan flagged #88 (UDP client→server has no CI coverage) for socket-connect L2-bridge research. Outcome: no quickchr change needed. A CHR probe showed the server→client UDP return already lands over the guest→host SLIRP gateway (
|
…MATRIX fan-out wording Copilot/CodeRabbit on #99: - Module header + encodeSecondaryJoin comment no longer imply a "server OK" on the secondary join — RouterOS sends no acknowledgement before bulk data (the implementation deliberately does not read one). - MATRIX: the fan-out is CHR-gated for the realized connection count (activeConnections == count), not a throughput rise (SLIRP loopback is bandwidth-bound) — wording now matches what the test asserts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Addressed the review nits (commit 040e6b3):
|
|
@coderabbitai help |
ChatThere are 3 ways to chat with CodeRabbit:
CodeRabbit commands
Other keywords and placeholders
Status, support, documentation and community
|
|
@coderabbitai full review |
✅ Action performedFull review finished. |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@commands/btest/README.md`:
- Around line 267-273: The UDP caveat in the README is stale and still implies
client receive/both is unproven through SLIRP, even though that path is already
covered. Update the bullet in the btest UDP section to narrow the remaining open
question to only the server-cell host→guest direction, and keep the wording
aligned with the existing client cell and server cell terminology used in the
surrounding text.
In `@src/protocols/btest-session.ts`:
- Around line 1396-1411: The secondary TCP join path in btest-session is reusing
options.host instead of the primary peer address, which can send follow-up
connections to a different backend. Update the openSecondaryConnection call
inside the TCP secondaryChannels loop to use the established channel’s remote
address fallback (channel.remoteAddress ?? options.host), matching the UDP join
logic and keeping all secondary joins pinned to the primary peer.
- Around line 1433-1445: The cleanup in btest-session’s session shutdown path is
only happening after Promise.all resolves, so any rejection from driveSession or
a secondary loop skips udp.close(), channel.close(), and the secondary channel
closes. Move the socket cleanup in btest-session’s main session flow into a
finally block (or equivalent error-safe cleanup around the Promise.all in the
driveSession/secondaryLoops section) so udp, channel, and every secondaryChannel
are always closed on both success and failure.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 53269341-983f-4831-8d74-e7bc6c1165a9
📒 Files selected for processing (12)
commands/btest/README.mdcommands/btest/examples.mddocs/MATRIX.mddocs/errors/routeros/btest-connection-count-single-stream.mdsrc/btest.tssrc/core/error-catalog.tssrc/protocols/btest-session.tssrc/protocols/btest.tstest/integration/btest-client.test.tstest/unit/btest-command.test.tstest/unit/btest-session.test.tstest/unit/btest.test.ts
…MATRIX fan-out wording Copilot/CodeRabbit on #99: - Module header + encodeSecondaryJoin comment no longer imply a "server OK" on the secondary join — RouterOS sends no acknowledgement before bulk data (the implementation deliberately does not read one). - MATRIX: the fan-out is CHR-gated for the realized connection count (activeConnections == count), not a throughput rise (SLIRP loopback is bandwidth-bound) — wording now matches what the test asserts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
8c21cdc to
ee6f4b9
Compare
…inally-cleanup, narrow UDP caveat - runBtestClientSession: open secondaries at `channel.remoteAddress ?? options.host` (the IP the primary control socket resolved to), so a load-balancing hostname can't land a secondary on a different RouterOS where the token is unknown. - Wrap the primary+secondary `Promise.all` in try/finally so a rejected loop still closes the UDP socket and every TCP channel (no leak on the error path). - README Open questions: the stale "UDP receive/both through SLIRP is unproven" bullet contradicted the now-gated client cell — narrow it to the one open edge (server-cell host→guest). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Rebased onto the updated #98 and addressed CodeRabbit's review (commit ee6f4b9):
Re-validated on CHR 7.23.1: fan-out opens 4 connections, UDP receive/both land throughput, tcp-both RX sustained. (This PR also now carries #98's random-data demux guard via the rebase.) @coderabbitai review |
|
✏️ Learnings added
If you found this review helpful, would you consider giving us a shout-out on X? Thank you for using CodeRabbit! ✅ Action performedReview finished.
|
|
@coderabbitai full review |
✅ Action performedFull review finished. |
…ated secondaries centrs's btest client now realizes `--connection-count > 1`: it reads the session token from the primary's OK and opens connection-count-1 additional TCP data connections, driving them into one shared BandwidthCounters so throughput aggregates. Closes the #87 fan-out (#84 wired the flag; this moves the data). Wire format grounded byte-for-byte against real RouterOS 7.23.1 (probed via CHR, all directions): - Server OK carries the session token in bytes 1-2 BE; RouterOS generates a non-zero token for connection-count > 1. - Each secondary sends a 16-byte join `[token:u16 BE][0x02][0 …]` — byte[2]=0x02 is a constant, direction-independent marker (encodeSecondaryJoin was missing it). - The server sends a 4-byte HELLO per connection, then **no** ack before data — it waits for all connections to join before streaming. So the join must not block on a reply read, or the sequential opens deadlock (this was the first implementation's bug, caught on CHR: secondary#1 stalled, server timed out and dropped it). Confirmed: a CHR run opens all 4 connections and data flows on each. Behavior: - Unauthenticated TCP fan-out works end to end. Authenticated (EC-SRP5) sessions stay single-stream (the post-auth token is not captured) and warn when the realized `activeConnections` falls short of the request. The pre-session "single stream" warning is replaced by this accurate post-session one. - `BtestRunSummary`/`BtestClientData` gain `activeConnections` (the realized count). Tests: - Unit: secondary-join byte format (`abcd0200…`); loopback fan-out opens N connections, sends the grounded join, and drives data on all N; full-fan-out and short-fan-out warning cases. - Integration (CHR 7.23.1): `btest-client.test.ts` example 11 — centrs client `connection-count=4` → real `/tool/bandwidth-server` opens 4 connections (`activeConnections == 4`), data flows. No throughput-rise assertion: the near-zero-latency SLIRP loopback is bandwidth-bound, so multi-connection does not raise aggregate throughput there (a WAN/latency property); the per-connection drive is asserted deterministically by the unit test. Docs: README (status, how-it-works, flag, honest-grounding, open questions), examples.md (example 5 clarified as server-side accept; new example 11), MATRIX caveat, error catalog + page, module header. Closes #87 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…erse-path gap #88 asked for CI coverage of the btest client's UDP path (validated manually only). Research outcome: **no quickchr change needed.** The user's socket-connect L2-bridge idea would work but is unnecessary — a CHR probe showed the server→client UDP return already lands over the guest→host SLIRP **gateway** (`10.0.2.2:clientUdpPort`, the same path the server cell's UDP-transmit uses), with only the existing TCP control forward. It works because PR #86 left the client UDP socket unconnected (a `connect()` filter previously dropped every datagram); the gap was simply that this was never CI-tested. - `btest-client.test.ts`: add UDP `receive` (rx > 0) and `both` (tx > 0 && rx > 0) cycles against real CHR `/tool/bandwidth-server`. CHR 7.23.1: receive rx≈474KB, both tx>0 rx≈582KB with UDP loss accounting. The reverse path (rx) is asserted; client→server transmit verification needs server-side stats and stays covered by the server cell (symmetric guest→host). - Docs: README (validation policy, honest-grounding, open questions, out-of-scope), MATRIX caveat, examples client-cell bullet — UDP client receive/both now gated; the one remaining unproven UDP edge is the server cell's host→guest direction (would need a UDP hostfwd). Closes #88 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…MATRIX fan-out wording Copilot/CodeRabbit on #99: - Module header + encodeSecondaryJoin comment no longer imply a "server OK" on the secondary join — RouterOS sends no acknowledgement before bulk data (the implementation deliberately does not read one). - MATRIX: the fan-out is CHR-gated for the realized connection count (activeConnections == count), not a throughput rise (SLIRP loopback is bandwidth-bound) — wording now matches what the test asserts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…om client fan-out (example 11) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…inally-cleanup, narrow UDP caveat - runBtestClientSession: open secondaries at `channel.remoteAddress ?? options.host` (the IP the primary control socket resolved to), so a load-balancing hostname can't land a secondary on a different RouterOS where the token is unknown. - Wrap the primary+secondary `Promise.all` in try/finally so a rejected loop still closes the UDP socket and every TCP channel (no leak on the error path). - README Open questions: the stale "UDP receive/both through SLIRP is unproven" bullet contradicted the now-gated client cell — narrow it to the one open edge (server-cell host→guest). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…(auth multi-conn) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… gateway (no contradiction) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
c72a924 to
8596fab
Compare
Targets
main(#98, the #84/#85 fixes, is merged). Implements the TCP multi-connection fan-out (#87) and gates the btest client's UDP receive/both path (#88), both validated against real CHR 7.23.1.#87 — TCP multi-connection fan-out
The centrs btest client now realizes
--connection-count > 1: it reads the session token from the primary's OK and opensconnection-count − 1additional TCP data connections, driving them into one sharedBandwidthCountersso throughput aggregates. (#98 wired the flag into the command packet; this moves the data.)Grounded byte-for-byte against RouterOS 7.23.1 (CHR probes, all directions):
connection-count > 1.[token:u16 BE][0x02][0 …]—byte[2]=0x02is a constant, direction-independent marker.Unauthenticated fan-out works end to end (CHR:
activeConnections == 4, data on every connection). Authenticated (EC-SRP5) sessions stay single-stream and warn — the post-auth token isn't captured (follow-up #103). No throughput-rise assertion: the near-zero-latency SLIRP loopback is bandwidth-bound, so that's a WAN/latency property; the per-connection drive is asserted deterministically by the unit test.#88 — UDP client receive/both CHR gate
The reverse path (server→client UDP) already lands over the guest→host SLIRP gateway (
10.0.2.2:clientUdpPort) with only the existing TCP control forward — it works because PR #86 left the client socket unconnected. So no quickchr change was needed (the socket-connect L2-bridge idea from the plan turned out unnecessary). Added UDPreceive/bothcycles tobtest-client.test.ts. The one remaining unproven UDP edge is the server-cell host→guest direction (documented).Review
Copilot + CodeRabbit comments addressed (secondary-IP pin,
try/finallysocket cleanup, doc-consistency); the random-data demux guard from #98 is on main. Follow-ups filed: #100 (server-side secondary-accept), #103 (EC-SRP5 multi-conn). quickchr networking-discoverability feedback: tikoci/quickchr#18.Closes #87
Closes #88
🤖 Generated with Claude Code