@tikoci/centrs 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +48 -2
- package/GLOSSARY.txt +16 -0
- package/README.md +1 -1
- package/commands/AGENTS.md +3 -2
- package/commands/api/AGENTS.md +212 -0
- package/commands/api/README.md +159 -0
- package/commands/api/examples.md +325 -0
- package/commands/btest/README.md +56 -31
- package/commands/btest/examples.md +35 -10
- package/commands/execute/README.md +9 -3
- package/commands/retrieve/README.md +1 -1
- package/docs/CONSTITUTION.md +6 -6
- package/docs/MATRIX.md +46 -15
- package/docs/errors/routeros/btest-connection-count-single-stream.md +22 -0
- package/docs/errors/routeros/btest-udp-tx-size-ignored.md +15 -0
- package/docs/errors/usage/invalid-method.md +23 -0
- package/package.json +5 -5
- package/src/api.ts +1555 -0
- package/src/btest.ts +61 -1
- package/src/cli/api.ts +536 -0
- package/src/cli/common.ts +64 -0
- package/src/cli.ts +22 -0
- package/src/core/error-catalog.ts +14 -0
- package/src/core/fanout.ts +160 -0
- package/src/core/inspect.ts +170 -0
- package/src/execute.ts +14 -47
- package/src/index.ts +61 -8
- package/src/protocols/adapter.ts +328 -0
- package/src/protocols/btest-session.ts +279 -46
- package/src/protocols/btest.ts +6 -2
- package/src/protocols/native-api.ts +223 -30
- package/src/retrieve-fanout.ts +13 -141
- package/src/retrieve.ts +30 -87
- package/commands/stream/README.md +0 -70
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
# api — examples
|
|
2
|
+
|
|
3
|
+
Each numbered example is an executable spec, asserted by CHR integration tests
|
|
4
|
+
(booted by `@tikoci/quickchr`): `test/integration/api.test.ts` (rest-api
|
|
5
|
+
examples), `test/integration/api-native.test.ts` (native-api examples, N…), and
|
|
6
|
+
`test/integration/api-listen.test.ts` (streaming examples, L…), with example N ↔
|
|
7
|
+
assertion N. If a line here is not exercised by a test, the test file is wrong; if
|
|
8
|
+
a line passes only with `--validate=false`, the implementation is wrong (see
|
|
9
|
+
[`docs/CONSTITUTION.md`](../../docs/CONSTITUTION.md)).
|
|
10
|
+
|
|
11
|
+
`$R` is `<host>:<rest-port>` resolved by quickchr. `$A` is `<host>` and
|
|
12
|
+
`$API_PORT` is the native API port (`chr.ports.api`). `$U` / `$P` are CHR
|
|
13
|
+
credentials provided by the harness. `$ID` is the `.id` returned by the preceding
|
|
14
|
+
add in the same transport section. All write-shaped examples pass `--yes`.
|
|
15
|
+
|
|
16
|
+
## rest-api (`--via rest-api`, the default)
|
|
17
|
+
|
|
18
|
+
### 1. GET a list
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
centrs api $R ip/address --username $U --password $P
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Envelope: `ok: true`, `data` is an array of rest-style records,
|
|
25
|
+
`meta.via=rest-api`, `meta.validation.source` includes `/console/inspect request=child`.
|
|
26
|
+
|
|
27
|
+
### 2. Endpoint normalization variants resolve to one path
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
centrs api $R "/rest/ip/address" --username $U --password $P
|
|
31
|
+
centrs api $R "rest/ip/address" --username $U --password $P
|
|
32
|
+
centrs api $R "ip address" --username $U --password $P
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Each resolves to the same canonical path; `meta.request.path == "/ip/address"`
|
|
36
|
+
and the `data` array matches example 1.
|
|
37
|
+
|
|
38
|
+
### 3. GET a singleton
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
centrs api $R system/resource --username $U --password $P
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Envelope: `ok: true`, `data` is a single object (not an array) with `uptime`,
|
|
45
|
+
`version`, etc.
|
|
46
|
+
|
|
47
|
+
### 4. PUT add (RouterOS create; documents the PUT≠POST trap)
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
centrs api $R ip/address -X PUT -f address=198.51.100.10/32 -f interface=ether1 -f comment=centrs-api-rest --username $U --password $P --yes
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Envelope: `ok: true`, `data[".id"]` matches `/^\*[0-9A-F]+$/`, `meta.via=rest-api`.
|
|
54
|
+
A subsequent GET of `/ip/address` contains the new row.
|
|
55
|
+
|
|
56
|
+
### 5. PATCH set by id-in-path
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
centrs api $R ip/address/$ID -X PATCH -f comment=centrs-api-rest-set --username $U --password $P --yes
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Envelope: `ok: true`; re-reading `ip/address/$ID` shows the new comment.
|
|
63
|
+
|
|
64
|
+
### 6. DELETE remove by id-in-path
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
centrs api $R ip/address/$ID -X DELETE --username $U --password $P --yes
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Envelope: `ok: true`; a subsequent GET of `/ip/address` does not contain `$ID`.
|
|
71
|
+
|
|
72
|
+
### 7. Raw JSON body with `-d`
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
centrs api $R ip/address -X PUT -d '{"address":"198.51.100.11/32","interface":"ether1"}' --username $U --password $P --yes
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Envelope: `ok: true`, `data[".id"]` present.
|
|
79
|
+
|
|
80
|
+
### 8. Body from stdin with `--input -`
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
echo '{"address":"198.51.100.12/32","interface":"ether1"}' | centrs api $R ip/address -X PUT --input - --username $U --password $P --yes
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Envelope: `ok: true`, `data[".id"]` present.
|
|
87
|
+
|
|
88
|
+
### 9. Server-side filter with `--query`
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
centrs api $R interface --query type=ether --username $U --password $P
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Envelope: `ok: true`, every returned record has `type=ether`; the request carried
|
|
95
|
+
a REST `.query` (not client-side filtering).
|
|
96
|
+
|
|
97
|
+
### 10. Projection with `--proplist`
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
centrs api $R ip/address --proplist address,interface --username $U --password $P
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Envelope: `ok: true`, each record has only the requested properties (plus `.id`).
|
|
104
|
+
|
|
105
|
+
### 11. Validation rejects an unknown attribute before the write
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
centrs api $R ip/address -X PUT -f address=198.51.100.13/32 -f interface=ether1 -f no-such-arg=x --username $U --password $P --yes
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Envelope: `ok: false`, `error.code=validation/unknown-attribute`,
|
|
112
|
+
`meta.validation.source=/console/inspect`, and no address is added.
|
|
113
|
+
|
|
114
|
+
### 12. Validation rejects an unknown path
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
centrs api $R ip/no-such-menu --username $U --password $P
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Envelope: `ok: false`, `error.code=validation/unknown-path`, no request issued.
|
|
121
|
+
|
|
122
|
+
### 13. `--raw` success prints the bare RouterOS body
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
centrs api $R ip/address --raw --username $U --password $P
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
stdout is the bare RouterOS JSON array (no `meta`/`warnings`); validation is
|
|
129
|
+
skipped; exit code 0.
|
|
130
|
+
|
|
131
|
+
### 14. `--raw` RouterOS error → stderr + nonzero exit
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
centrs api $R ip/address -X PUT -f address=not-an-ip -f interface=ether1 --raw --username $U --password $P --yes
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
stdout is empty; stderr carries the RouterOS error payload as JSON; exit code is
|
|
138
|
+
nonzero.
|
|
139
|
+
|
|
140
|
+
### 15. Mutating + non-TTY + no `--yes` is refused
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
centrs api $R ip/address -X PUT -f address=198.51.100.14/32 -f interface=ether1 --username $U --password $P </dev/null
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Envelope: `ok: false`, `error.code=usage/confirmation-required`, no request
|
|
147
|
+
issued, no address added. (Holds under `--raw` too.)
|
|
148
|
+
|
|
149
|
+
### 16. POST run a console command via `/rest/execute`
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
centrs api $R execute -X POST -f script=':put [/system/identity/get name]' --username $U --password $P --yes
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Envelope: `ok: true`, `data` is string-shaped and contains the CHR identity;
|
|
156
|
+
`meta.validation.semantic=not-applicable` (script, not a path).
|
|
157
|
+
|
|
158
|
+
### 17. `--via rest-api --listen` is rejected
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
centrs api $R ip/address --listen --via rest-api --username $U --password $P
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Envelope: `ok: false`, `error.code=transport/capability-unsupported` (REST cannot
|
|
165
|
+
follow); no silent open-ended poll.
|
|
166
|
+
|
|
167
|
+
### 18. `--query` not-equal maps to a negated stack word
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
centrs api $R interface --query type!=ether --username $U --password $P
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Envelope: `ok: true`, no returned record has `type=ether`; the request carried
|
|
174
|
+
REST `.query` words `["type=ether","#!"]` (eq then NOT-top).
|
|
175
|
+
|
|
176
|
+
### 19. `--raw-query` expresses OR
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
centrs api $R interface --raw-query type=ether --raw-query type=loopback --raw-query '#|' --username $U --password $P
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Envelope: `ok: true`, returns the union of `ether` and `loopback` interfaces; the
|
|
183
|
+
raw words are passed through verbatim as `.query` (REST) / `?` words (native).
|
|
184
|
+
|
|
185
|
+
### 20. Bounded `duration=` command returns a `.section` array (not a stream)
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
centrs api $R interface/monitor-traffic -X POST -f interface=ether1 -f duration=5s --username $U --password $P --yes
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Envelope: `ok: true`, `data` is an **array** of `.section`-keyed records (one per
|
|
192
|
+
~second), `meta.via=rest-api`. This is an ordinary bounded call — no `--listen`,
|
|
193
|
+
no NDJSON. `monitor-traffic` is a command (not `print`/`get`), so it is a
|
|
194
|
+
`POST` and write-classed (needs `--yes`); centrs never relaxes that to read-only.
|
|
195
|
+
(REST bounds it at the 60 s cap; native has no cap.)
|
|
196
|
+
|
|
197
|
+
### 21. GET one row by id together with `--proplist`
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
centrs api $R ip/address/$ID --proplist address --username $U --password $P
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Envelope: `ok: true`, `data` is a **single object** (not an array) carrying only
|
|
204
|
+
the projected property `address` (RouterOS omits `.id` unless it is in the
|
|
205
|
+
proplist). Combining an id with `--query`/`--proplist` folds the id into the REST
|
|
206
|
+
`.query` (`.id=$ID`) and rides a `POST …/print`, then unwraps the one row —
|
|
207
|
+
matching the native `?.id=` read. (Plain id-only reads still use the `GET …/$ID`
|
|
208
|
+
URL form; see example 5's re-read.)
|
|
209
|
+
|
|
210
|
+
## native-api (`--via native-api`)
|
|
211
|
+
|
|
212
|
+
The same contract over the binary API. Validation still runs through
|
|
213
|
+
`/console/inspect`, issued as native commands. Values are strings.
|
|
214
|
+
|
|
215
|
+
### N1. GET a list
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
centrs api $A interface -X GET --via native-api --port $API_PORT --username $U --password $P
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Envelope: `ok: true`, `data` is an array of rest-style records (string values),
|
|
222
|
+
`meta.via=native-api`.
|
|
223
|
+
|
|
224
|
+
### N2. PUT add (returns `.id`)
|
|
225
|
+
|
|
226
|
+
```bash
|
|
227
|
+
centrs api $A ip/address -X PUT -f address=198.51.100.20/32 -f interface=ether1 --via native-api --port $API_PORT --username $U --password $P --yes
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Envelope: `ok: true`, `data[".id"]` present.
|
|
231
|
+
|
|
232
|
+
### N3. PATCH set by id (id → `=.id=` word)
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
centrs api $A ip/address/$ID -X PATCH -f comment=centrs-api-native-set --via native-api --port $API_PORT --username $U --password $P --yes
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Envelope: `ok: true`; re-reading shows the new comment.
|
|
239
|
+
|
|
240
|
+
### N4. DELETE remove by id (id → `=.id=` word)
|
|
241
|
+
|
|
242
|
+
```bash
|
|
243
|
+
centrs api $A ip/address/$ID -X DELETE --via native-api --port $API_PORT --username $U --password $P --yes
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Envelope: `ok: true`; the row is gone.
|
|
247
|
+
|
|
248
|
+
### N5. GET one by id (→ `print ?.id=`)
|
|
249
|
+
|
|
250
|
+
```bash
|
|
251
|
+
centrs api $A ip/address/$ID2 -X GET --via native-api --port $API_PORT --username $U --password $P
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Envelope: `ok: true`, `data` is a single object whose `.id` is `$ID2`.
|
|
255
|
+
|
|
256
|
+
### N6. Validation rejects an unknown attribute over native
|
|
257
|
+
|
|
258
|
+
```bash
|
|
259
|
+
centrs api $A ip/address -X PUT -f address=198.51.100.21/32 -f interface=ether1 -f no-such-arg=x --via native-api --port $API_PORT --username $U --password $P --yes
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
Envelope: `ok: false`, `error.code=validation/unknown-attribute`, no row added —
|
|
263
|
+
caught by the inspect gate, not a native `!trap`.
|
|
264
|
+
|
|
265
|
+
### N7. `--raw` over native
|
|
266
|
+
|
|
267
|
+
```bash
|
|
268
|
+
centrs api $A interface --raw --via native-api --port $API_PORT --username $U --password $P
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
stdout is the bare rest-style array (string values); exit code 0.
|
|
272
|
+
|
|
273
|
+
### N8. POST run a console command via native `/execute`
|
|
274
|
+
|
|
275
|
+
```bash
|
|
276
|
+
centrs api $A execute -X POST -f script=':put [/system/identity/get name]' --via native-api --port $API_PORT --username $U --password $P --yes
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
Envelope: `ok: true`, `data` contains the CHR identity.
|
|
280
|
+
|
|
281
|
+
## listen / `--stream` (native-api only)
|
|
282
|
+
|
|
283
|
+
Open-ended follow. `--stream` is the primary flag; `--listen` is an accepted
|
|
284
|
+
alias and a trailing `/listen` endpoint segment infers it. Output is NDJSON: one
|
|
285
|
+
envelope per change frame, then a terminating summary envelope.
|
|
286
|
+
|
|
287
|
+
### L1. `--stream` streams a change as an NDJSON frame, then a summary
|
|
288
|
+
|
|
289
|
+
```bash
|
|
290
|
+
centrs api $A ip/address --stream --count 1 --via native-api --port $API_PORT --username $U --password $P
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
While listening, the harness adds an address over REST. stdout is NDJSON: at least
|
|
294
|
+
one envelope frame for the new row (`meta.operation.stream.kind=frame`), then a
|
|
295
|
+
final summary envelope (`meta.operation.stream.kind=summary`,
|
|
296
|
+
`data.stopReason=count-reached`, `data.frames>=1`). Exit code 0 (stream started
|
|
297
|
+
cleanly).
|
|
298
|
+
|
|
299
|
+
### L2. A deletion frame carries `.dead`
|
|
300
|
+
|
|
301
|
+
```bash
|
|
302
|
+
centrs api $A ip/address --stream --duration 3s --via native-api --port $API_PORT --username $U --password $P
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
The harness removes a pre-seeded address; an emitted frame's record carries
|
|
306
|
+
`.dead=true` (a minimal `{ ".id", ".dead" }` record, per the CHR-grounded
|
|
307
|
+
`commands/api/AGENTS.md`).
|
|
308
|
+
|
|
309
|
+
### L3. `/listen` endpoint infers native + streaming
|
|
310
|
+
|
|
311
|
+
```bash
|
|
312
|
+
centrs api $A "ip/address/listen" --count 1 --port $API_PORT --username $U --password $P
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
No `--stream` / `--via` given; the `/listen` endpoint infers `--stream` and
|
|
316
|
+
`--via native-api` (`meta.via=native-api`). Same NDJSON shape as L1.
|
|
317
|
+
|
|
318
|
+
### L4. Bounded `--duration` reports its stop reason
|
|
319
|
+
|
|
320
|
+
```bash
|
|
321
|
+
centrs api $A ip/address --stream --duration 2s --via native-api --port $API_PORT --username $U --password $P
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
With no change during the window, the stream ends after ~2 s with a summary
|
|
325
|
+
envelope whose `data.stopReason=duration-elapsed`. Exit code 0.
|
package/commands/btest/README.md
CHANGED
|
@@ -17,7 +17,10 @@ on the centrs **server**; `test/integration/btest-client.test.ts` runs the centr
|
|
|
17
17
|
**client** against a real RouterOS `/tool/bandwidth-server` over a host→guest
|
|
18
18
|
`tcp:2000` forward — unauthenticated TCP receive, an **EC-SRP5 client proof
|
|
19
19
|
verified by RouterOS's own verifier**, and a wrong-password reject. TCP
|
|
20
|
-
multi-connection (`connection-count > 1`) data fan-out is
|
|
20
|
+
multi-connection (`connection-count > 1`) data fan-out is implemented for
|
|
21
|
+
unauthenticated tests (the client joins the negotiated extra connections; the join
|
|
22
|
+
format is grounded against RouterOS 7.23.1); authenticated sessions stay
|
|
23
|
+
single-stream. See
|
|
21
24
|
`docs/MATRIX.md` ("Peer measurement (`btest`)") for the cell states — that is the
|
|
22
25
|
only status surface. v1 scope: **both** modes (client + server), **EC-SRP5 +
|
|
23
26
|
unauthenticated** auth, **TCP and UDP** tests. Legacy pre-6.43 double-MD5 is out of
|
|
@@ -85,7 +88,7 @@ centrs btest server [--authenticate[=false]] [--user U] [--password P] \
|
|
|
85
88
|
| `--direction <receive\|transmit\|both>` | `direction`, default `receive` | `receive` = download (server→client), `transmit` = upload. |
|
|
86
89
|
| `--duration <dur>` | `duration` | Bounds the run; reuses `parseDuration`. Omit for open-ended (Ctrl-C). |
|
|
87
90
|
| `--interval <dur>` | `interval`, default `1s` | Report cadence (`20ms`..`5s`). One report frame per interval. |
|
|
88
|
-
| `--connection-count <n>` | `connection-count` (1..255) | **TCP only.** Parallel TCP data connections. |
|
|
91
|
+
| `--connection-count <n>` | `connection-count` (1..255) | **TCP only.** Parallel TCP data connections. centrs opens the negotiated extra connections (join format grounded against RouterOS 7.23.1) so throughput aggregates across streams. **Authenticated (EC-SRP5) sessions stay single-stream** — the post-auth token is not captured — and emit a `routeros/btest-connection-count-single-stream` warning. |
|
|
89
92
|
| `--local-udp-tx-size <n>` | `local-udp-tx-size` (28..64000) | **UDP only.** Client→server packet size. |
|
|
90
93
|
| `--remote-udp-tx-size <n>` | `remote-udp-tx-size` (28..64000) | **UDP only.** Server→client packet size. |
|
|
91
94
|
| `--local-tx-speed <bps>` | `local-tx-speed` | Cap on client→server rate (bits/sec). |
|
|
@@ -120,10 +123,20 @@ test, data then flows on the server's allocated UDP ports
|
|
|
120
123
|
from sequence gaps. For a TCP test, the bulk data rides `connection-count` TCP
|
|
121
124
|
connections and goodput is measured on the data stream (TCP/IP headers excluded,
|
|
122
125
|
matching RouterOS accounting). For multi-connection TCP the server's OK response
|
|
123
|
-
carries a 2-byte **session token**; the
|
|
124
|
-
|
|
126
|
+
carries a 2-byte **session token**; the client opens `connection-count − 1`
|
|
127
|
+
additional connections, each presenting a 16-byte join `[token:u16 BE][0x02][0 …]`
|
|
128
|
+
(grounded against RouterOS 7.23.1, direction-independent) to join the same test
|
|
129
|
+
without re-auth, and drives them into the shared counters so throughput aggregates.
|
|
130
|
+
Throughput and CPU are exchanged
|
|
125
131
|
once per `interval` as a 12-byte **status message** on the TCP channel
|
|
126
|
-
(`[0x07][0x80|cpu][00 00][seq u32 LE][bytesReceived u32 LE]`).
|
|
132
|
+
(`[0x07][0x80|cpu][00 00][seq u32 LE][bytesReceived u32 LE]`). The receiver of a
|
|
133
|
+
transmit (server for `transmit`, both peers for `both`) sends this so the
|
|
134
|
+
transmitter paces from the peer's `bytesReceived`. For `direction=both` the
|
|
135
|
+
status frames are **interleaved into the bulk data stream** (no length framing),
|
|
136
|
+
so the client demuxes them out of its receive loop by their structural marker —
|
|
137
|
+
without that feedback the client's TX saturates the link and starves
|
|
138
|
+
server→client RX. RouterOS clears the CPU high bit the centrs client sets, so the
|
|
139
|
+
marker matches on the two reserved zero bytes, not the high bit.
|
|
127
140
|
|
|
128
141
|
### Authentication (EC-SRP5)
|
|
129
142
|
|
|
@@ -141,7 +154,7 @@ will not offer EC-SRP5 against an `authenticate=yes` server) maps to
|
|
|
141
154
|
|
|
142
155
|
btest runs over time, so it emits **live records** during the run plus a final
|
|
143
156
|
**summary**. v1 keeps the formats simple and **defers a broader JSON-streaming
|
|
144
|
-
decision** — it does *not* adopt
|
|
157
|
+
decision** — it does *not* adopt `api --stream`'s NDJSON-stream-of-envelopes
|
|
145
158
|
contract yet (see Open questions):
|
|
146
159
|
|
|
147
160
|
- **`text`** (default) — human-readable live reports during the run
|
|
@@ -214,10 +227,11 @@ Decided with the user. Both cells have direct gated `CHR-passed` evidence:
|
|
|
214
227
|
maps onto the guest bandwidth server on 2000 (the same SLIRP inbound path that
|
|
215
228
|
already carries REST/SSH — no firewall change). The centrs client dials
|
|
216
229
|
`127.0.0.1:<host port>` and proves **TCP receive**, unauth and **EC-SRP5** (its
|
|
217
|
-
client proof verified by RouterOS's real server verifier),
|
|
218
|
-
reject
|
|
219
|
-
|
|
220
|
-
|
|
230
|
+
client proof verified by RouterOS's real server verifier), a wrong-password
|
|
231
|
+
reject, **TCP multi-connection fan-out** (`connection-count=4`), and **UDP
|
|
232
|
+
receive/both**. The UDP server→client return needs no UDP forward: it rides the
|
|
233
|
+
guest→host SLIRP gateway (`10.0.2.2:clientUdpPort`, the same path the server cell
|
|
234
|
+
uses), which works because the client socket is left unconnected (#86, #88).
|
|
221
235
|
|
|
222
236
|
The fast/deterministic backstop under both is **centrs client ↔ centrs server
|
|
223
237
|
loopback** (`127.0.0.1`, injected sockets) covering the full client↔server matrix,
|
|
@@ -226,32 +240,43 @@ EC-SRP5 both roles, and the option-grammar rejects.
|
|
|
226
240
|
while implementing the EC-SRP5 framing and the server-side verifier; it is **not**
|
|
227
241
|
an integration test (it cannot run in CI) and **not** part of the long-term plan.
|
|
228
242
|
|
|
229
|
-
**Honest grounding caveat:** both cells are now directly CHR-passed
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
243
|
+
**Honest grounding caveat:** both cells are now directly CHR-passed, **including
|
|
244
|
+
TCP multi-connection fan-out** — `test/integration/btest-client.test.ts` gates a
|
|
245
|
+
centrs client opening `connection-count=4` against a real RouterOS
|
|
246
|
+
`/tool/bandwidth-server`; RouterOS accepts all 3 secondary joins and data flows on
|
|
247
|
+
every connection (`activeConnections: 4`). The *throughput increase* multiple
|
|
248
|
+
streams give is a WAN/latency property and is not observable over the near-zero-
|
|
249
|
+
latency SLIRP loopback, so the gate asserts the fan-out, not a higher number (the
|
|
250
|
+
per-connection drive is asserted deterministically by the loopback unit test).
|
|
251
|
+
**UDP client receive/both is now CHR-gated too** — the server→client return rides
|
|
252
|
+
the guest→host SLIRP gateway, no UDP forward needed (#88). The remaining fan-out
|
|
253
|
+
edge is **authenticated** multi-connection (capturing the EC-SRP5 post-auth token),
|
|
254
|
+
still single-stream.
|
|
233
255
|
|
|
234
256
|
## Open questions
|
|
235
257
|
|
|
236
|
-
- **JSON streaming is deliberately deferred.** btest does not adopt
|
|
237
|
-
|
|
238
|
-
returns a single summary envelope for `json` / `yaml`. Revisit a
|
|
239
|
-
shape — ideally shared with `stream` — once
|
|
240
|
-
- **UDP receive/both
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
- **
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
258
|
+
- **JSON streaming is deliberately deferred.** btest does not adopt
|
|
259
|
+
`api --stream`'s NDJSON-stream-of-envelopes contract yet; v1 streams `text` /
|
|
260
|
+
`csv` and returns a single summary envelope for `json` / `yaml`. Revisit a
|
|
261
|
+
streaming-JSON shape — ideally shared with `api --stream` — once it has settled.
|
|
262
|
+
- **UDP client receive/both is gated (#88).** The client cell
|
|
263
|
+
(`test/integration/btest-client.test.ts`) lands real UDP server→client throughput:
|
|
264
|
+
the return rides the guest→host SLIRP gateway (`10.0.2.2:clientUdpPort`), needing
|
|
265
|
+
no UDP forward and no quickchr change — it works because the client socket is left
|
|
266
|
+
unconnected (#86).
|
|
267
|
+
- **Only the server-cell host→guest UDP direction stays unproven.** The server cell
|
|
268
|
+
gates UDP *transmit* (guest→host, which SLIRP NATs cleanly); the reverse (centrs
|
|
269
|
+
server → CHR client, host→guest) would need a UDP hostfwd for the data ports, so it
|
|
270
|
+
is not yet asserted. `btest.exe` (wine) is the coding-time grounding peer, not CI.
|
|
271
|
+
- **Authenticated multi-connection stays single-stream.** Fan-out negotiates the
|
|
272
|
+
session token from the unauthenticated OK; capturing the EC-SRP5 post-auth token
|
|
273
|
+
to fan out authenticated tests is a follow-up (#103). centrs warns
|
|
274
|
+
(`routeros/btest-connection-count-single-stream`) when the realized connection
|
|
275
|
+
count falls short of the request. The **server-side** secondary-accept is its own
|
|
276
|
+
follow-up (#100).
|
|
251
277
|
|
|
252
278
|
## Out of scope (v1)
|
|
253
279
|
|
|
254
|
-
Legacy pre-6.43 double-MD5 auth;
|
|
255
|
-
UDP needs SLIRP reverse-NAT); the ≥3-router "test-through" topology RouterOS
|
|
280
|
+
Legacy pre-6.43 double-MD5 auth; the ≥3-router "test-through" topology RouterOS
|
|
256
281
|
recommends for measuring a *transit* device (centrs is an endpoint peer); TUI/proxy
|
|
257
282
|
btest frontends (later, over the stable core).
|
|
@@ -15,17 +15,19 @@ matrix's honest grounding split (`docs/MATRIX.md`, "Peer measurement (`btest`)")
|
|
|
15
15
|
loss / direction against the CHR's own `/tool/bandwidth-test` status output
|
|
16
16
|
(CHR 7.23.1, `bun run test:integration`). Examples **4** (wrong-credentials) and
|
|
17
17
|
**5** (multi-connection TCP) are **designed but not yet gated** — the auth-reject
|
|
18
|
-
path is covered by the loopback unit test, and
|
|
19
|
-
|
|
18
|
+
path is covered by the loopback unit test, and example 5 is the **server-side**
|
|
19
|
+
secondary-accept (distinct from the now-gated **client** fan-out in example 11),
|
|
20
|
+
still a documented follow-up — so they are not part of the asserted run.
|
|
20
21
|
- **Gated `CHR-passed` (client cell): centrs client → CHR bandwidth-server** —
|
|
21
22
|
examples **6** (TCP receive) and **8** (EC-SRP5 client role + wrong-password
|
|
22
23
|
reject) are *also* asserted directly against a real RouterOS
|
|
23
24
|
`/tool/bandwidth-server`, over a host→guest `tcp:2000` forward
|
|
24
25
|
(`test/integration/btest-client.test.ts`, CHR 7.23.1). This is what makes the
|
|
25
26
|
**client** cell `CHR-passed`: centrs's EC-SRP5 **client proof** is verified by
|
|
26
|
-
RouterOS's own server verifier.
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
RouterOS's own server verifier. Example **11** (TCP **multi-connection**
|
|
28
|
+
fan-out), a TCP `direction=both` #85 regression guard, and **UDP receive/both**
|
|
29
|
+
(#88 — the server→client return over the guest→host SLIRP gateway) are gated in
|
|
30
|
+
the same suite.
|
|
29
31
|
- **Unit / loopback (client cell + codec)** — examples 6–10. centrs client ↔
|
|
30
32
|
centrs server on `127.0.0.1` with injected sockets; the EC-SRP5 both-roles math
|
|
31
33
|
and the pre-socket option-grammar rejects (`test/unit/btest.test.ts`). These are
|
|
@@ -113,12 +115,13 @@ contains a `transport/auth-failed` entry (the EC-SRP5 proof did not verify) and
|
|
|
113
115
|
`data.sessions` is empty — no session opened, no throughput. The CHR side reports
|
|
114
116
|
an authentication failure.
|
|
115
117
|
|
|
116
|
-
### 5. Multi-connection TCP transmit
|
|
118
|
+
### 5. Multi-connection TCP transmit (server-side accept)
|
|
117
119
|
|
|
118
|
-
> *Designed, not yet gated* — the server
|
|
119
|
-
> `
|
|
120
|
-
>
|
|
121
|
-
>
|
|
120
|
+
> *Designed, not yet gated* — this is the **server-side** fan-out: centrs's
|
|
121
|
+
> `btest server` accepting parallel secondary joins from a client. The session
|
|
122
|
+
> token is negotiated but centrs's server does not yet serve secondary TCP joins.
|
|
123
|
+
> Distinct from the **client** fan-out (example 11), which **is** implemented and
|
|
124
|
+
> gated. Tracked as a follow-up (#100); not part of the asserted run.
|
|
122
125
|
|
|
123
126
|
```bash
|
|
124
127
|
centrs btest server --authenticate=false --bind 0.0.0.0 --duration 20s --format json
|
|
@@ -188,3 +191,25 @@ centrs btest client $SRV --protocol udp --local-udp-tx-size 99999
|
|
|
188
191
|
`ok: false`, `error.code = "validation/option"` (UDP size must be `28..64000`), no
|
|
189
192
|
socket opened. The same gate rejects `connection-count` outside `1..255`,
|
|
190
193
|
`interval` outside `20ms..5s`, and (server) `max-sessions` outside `1..1000`.
|
|
194
|
+
|
|
195
|
+
## centrs client TCP multi-connection (gated; client cell)
|
|
196
|
+
|
|
197
|
+
### 11. Multi-connection TCP receive fan-out
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
centrs btest client <router> --protocol tcp --direction receive --connection-count 4 --duration 4s --format json
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Against a real RouterOS `/tool/bandwidth-server` (`authenticate=no`), centrs reads
|
|
204
|
+
the session token from the primary's OK and opens the 3 extra TCP data
|
|
205
|
+
connections, each presenting the grounded join `[token:u16 BE][0x02][0 …]`. The
|
|
206
|
+
summary is `ok: true` with `data.activeConnections: 4` and non-zero aggregate
|
|
207
|
+
throughput — real RouterOS accepted all 3 secondary joins and data flowed on every
|
|
208
|
+
connection (the per-connection drive is also asserted deterministically by the
|
|
209
|
+
loopback unit test). The *throughput increase* multiple streams provide is a
|
|
210
|
+
high-latency/WAN-link property and is **not** observable over the near-zero-latency
|
|
211
|
+
SLIRP loopback (a single TCP stream already saturates it), so the gate asserts the
|
|
212
|
+
fan-out itself, not a higher number. Gated in
|
|
213
|
+
`test/integration/btest-client.test.ts` (CHR 7.23.1). An **authenticated**
|
|
214
|
+
`--connection-count > 1` run instead stays single-stream (`activeConnections: 1`)
|
|
215
|
+
and carries a `routeros/btest-connection-count-single-stream` warning.
|
|
@@ -52,6 +52,12 @@ Status: `rest-api`, `native-api`, `mac-telnet`, and `ssh` are `CHR-passed` (see
|
|
|
52
52
|
else passes its `detail`/body plus the HTTP status through
|
|
53
53
|
`mapRouterOsError({ transport: "rest-api", httpStatus })`. There is no longer
|
|
54
54
|
a hand-rolled `routeros/*` table on the REST side.
|
|
55
|
+
- **Run-time fault mapping — later (JG-28).** `mapRouterOsError`'s table covers
|
|
56
|
+
the faults seen so far; a known gap is errors that *pass* `:parse`/validation
|
|
57
|
+
but fail at **run time** (a syntactically valid command the router rejects when
|
|
58
|
+
executed). `tikoci/vscode-tikbook` has an error-string mapping for these worth
|
|
59
|
+
porting into `src/core/routeros-errors.ts`. Background research, not yet
|
|
60
|
+
scheduled — captured here so it isn't lost (no GitHub issue by design).
|
|
55
61
|
- Output is *string*-shaped for script-mode; richer parsing is a future
|
|
56
62
|
concern. The envelope must still distinguish RouterOS errors from successful
|
|
57
63
|
runs (a 200 with a RouterOS error string is still an error — see
|
|
@@ -195,9 +201,9 @@ Protocol selection: an unresolved MAC target auto-selects mac-telnet (covered by
|
|
|
195
201
|
## Open shape questions
|
|
196
202
|
|
|
197
203
|
- ~~How to expose multi-line / async / progress output~~ — **resolved:**
|
|
198
|
-
open-ended follow/streaming reads are
|
|
199
|
-
|
|
200
|
-
|
|
204
|
+
open-ended follow/streaming reads are `api … --stream` (NDJSON stream of
|
|
205
|
+
envelopes; see `commands/api/README.md`). `execute` stays single-shot
|
|
206
|
+
read/write; it does not grow a follow mode.
|
|
201
207
|
- ~~Whether `--strict` should reject "succeeded with stderr-like content."~~ —
|
|
202
208
|
**decided:** `--strict` promotes any warning on an otherwise-successful run to
|
|
203
209
|
`ok: false` (nonzero exit), like `-Werror`; the default stays lenient
|
|
@@ -45,7 +45,7 @@ centrs retrieve <router> snmp <oid|MIB name> [flags]
|
|
|
45
45
|
| `--attributes <a,b,c>` | Alias for `--attribute` with comma-separated input. |
|
|
46
46
|
| `--all-attributes` | Equivalent to RouterOS `details=true`. Mutually exclusive with `--attribute(s)`. |
|
|
47
47
|
| `--list-attributes` (alias `--list`) | Return the available attribute names for the path. No `print`/`get` is run. |
|
|
48
|
-
| `--once` | Bounded single read of a monitor-style menu (RouterOS `once`): returns **one** envelope and never follows. Open-ended follow is
|
|
48
|
+
| `--once` | Bounded single read of a monitor-style menu (RouterOS `once`): returns **one** envelope and never follows. Open-ended follow is `api … --stream`. See constitution: protocol selection. |
|
|
49
49
|
| `--query <expr>` | RouterOS-side **row** filter (maps to `.query`), repeatable; returns matching rows as an **array**. This is how you read one row by name (`--query name=ether1`). **Not Implemented** yet — returns `validation/not-implemented`. |
|
|
50
50
|
| `--filter <expr>` | RouterOS row filter; same `.query` mapping and not-implemented handling as `--query`. |
|
|
51
51
|
| `--where <attr>=<value>` | Device-class selector: fan out across CDB records whose stored fact/comment-kv matches (e.g. `--where board=RB5009`). Repeatable (AND). Filters *which devices*, not RouterOS rows. See constitution: target selection. |
|
package/docs/CONSTITUTION.md
CHANGED
|
@@ -228,8 +228,8 @@ repeats. The union is **de-duped by CDB record index** and run through the same
|
|
|
228
228
|
per-target pipeline; the `data` array is reassembled in resolved record order,
|
|
229
229
|
not completion order, so repeated runs diff cleanly. Destructive multi-target
|
|
230
230
|
operations (e.g. `devices remove` across a group) require `--force`. `terminal`
|
|
231
|
-
and single-session `stream` are not fan-out surfaces and reject
|
|
232
|
-
`usage/fanout-not-supported`.
|
|
231
|
+
and a single-session `api --stream` follow are not fan-out surfaces and reject
|
|
232
|
+
N>1 with `usage/fanout-not-supported`.
|
|
233
233
|
|
|
234
234
|
`--where` filters *which devices* by CDB-stored facts; keep it distinct from
|
|
235
235
|
`retrieve`/`execute`'s `--query`/`--filter`, which filter *RouterOS rows* in the
|
|
@@ -266,7 +266,7 @@ Per-operation preferences, downgrade order in parens:
|
|
|
266
266
|
| Operation | Preferred | Downgrade order |
|
|
267
267
|
| --------- | --------- | --------------- |
|
|
268
268
|
| retrieve | rest-api, native-api; snmp for OID/MIB reads (future) | rest-api, native-api |
|
|
269
|
-
|
|
|
269
|
+
| api | rest-api, native-api; `--stream` follow native-api only | rest-api, native-api (no silent downgrade) |
|
|
270
270
|
| execute | native-api → rest-api → mac-telnet | native-api, rest-api, mac-telnet, ssh, romon, winbox-terminal |
|
|
271
271
|
| terminal | ssh | mac-telnet (L2 only when ssh fails or MAC given) |
|
|
272
272
|
| transfer | size/direction-aware: rest-api/native-api for ≤60 KB writes & all reads (chunked), sftp for large uploads | sftp ⇄ rest/native by size; scp · fetch · ftp explicit-only |
|
|
@@ -288,9 +288,9 @@ Rules:
|
|
|
288
288
|
access needs explicit ARP opt-in.
|
|
289
289
|
- **`measure` (btest) is explicit-only** — never in the auto-select/downgrade
|
|
290
290
|
chain, never a substitute, and no other command rides it.
|
|
291
|
-
- Per-command selection **depth** lives in the command READMEs: `
|
|
292
|
-
follow/NDJSON contract and the REST-cannot-follow constraint
|
|
293
|
-
(`commands/
|
|
291
|
+
- Per-command selection **depth** lives in the command READMEs: `api`'s
|
|
292
|
+
`--stream` follow/NDJSON contract and the REST-cannot-follow constraint
|
|
293
|
+
(`commands/api/README.md`), `transfer`'s size/direction-aware method
|
|
294
294
|
selection and the explicit-only `scp`/`ftp`/`fetch`
|
|
295
295
|
(`commands/transfer/README.md`), SNMP's MIB-cache
|
|
296
296
|
(`commands/retrieve/README.md`), and btest's port/auth
|