Drop the hosted simulators and name what goes untested with them

This commit is contained in:
2026-09-06 21:46:43 +02:00
parent ffda0e9c98
commit ab93833cee
+29 -11
View File
@@ -70,10 +70,13 @@ manual runs, never CI. Tags below were current on 2026-09-05.
| **Jasmin 0.11.0** — Python/Twisted, a production gateway, not a toy | Independent codec (`smpp.pdu`), SAR segmentation by default, UUID message ids, a real DLR pipeline that can use `data_sm` | `jookies/jasmin:0.11.0` + `redis:8.8.2-alpine` + `rabbitmq:4.3.5-management-alpine` (env `REDIS_CLIENT_HOST`, `AMQP_BROKER_HOST`; fall back to `redis:7.4.x` / `rabbitmq:3.13.x` if 0.11.0 balks). User, group, `mtrouter` and `morouter` over `jcli` on 8990 | `[smpp-server]`: `enquireLinkTimerSecs` 30, `inactivityTimerSecs` 300, `responseTimerSecs` 60; `submit_throughput` for throttling; `dlr_level` | | **Jasmin 0.11.0** — Python/Twisted, a production gateway, not a toy | Independent codec (`smpp.pdu`), SAR segmentation by default, UUID message ids, a real DLR pipeline that can use `data_sm` | `jookies/jasmin:0.11.0` + `redis:8.8.2-alpine` + `rabbitmq:4.3.5-management-alpine` (env `REDIS_CLIENT_HOST`, `AMQP_BROKER_HOST`; fall back to `redis:7.4.x` / `rabbitmq:3.13.x` if 0.11.0 balks). User, group, `mtrouter` and `morouter` over `jcli` on 8990 | `[smpp-server]`: `enquireLinkTimerSecs` 30, `inactivityTimerSecs` 300, `responseTimerSecs` 60; `submit_throughput` for throttling; `dlr_level` |
| **SMPPSim 2.6.x** — Java, the classic | Richest fault injection of anything open: per-state receipt percentages, delayed and intermediate receipts, queue-full, `LOOPBACK` (echoes our `submit_sm` back as `deliver_sm`), SMSC-initiated `outbind`, receipts with or without TLVs, its own decoded PDU log | Vendor site is down (522); build from `kwahome/smpp-sim-docker` and tag locally. Port 2775, HTTP 8884, `smppclient1`/`password` | `DELIVERY_RECEIPT_OPTIONAL_PARAMS` (off = Kannel-style text-only receipts), `PERCENTAGE_DELIVERED/UNDELIVERABLE/ACCEPTED/REJECTED`, `PERCENTAGE_THAT_TRANSITION`, `MAX_TIME_ENROUTE`, `DELAY_DELIVERY_RECEIPTS_BY`, `OUTBOUND_QUEUE_MAX_SIZE`, `OUTBIND_ENABLED`, `CAPTURE_*_DECODED_TO_FILE` | | **SMPPSim 2.6.x** — Java, the classic | Richest fault injection of anything open: per-state receipt percentages, delayed and intermediate receipts, queue-full, `LOOPBACK` (echoes our `submit_sm` back as `deliver_sm`), SMSC-initiated `outbind`, receipts with or without TLVs, its own decoded PDU log | Vendor site is down (522); build from `kwahome/smpp-sim-docker` and tag locally. Port 2775, HTTP 8884, `smppclient1`/`password` | `DELIVERY_RECEIPT_OPTIONAL_PARAMS` (off = Kannel-style text-only receipts), `PERCENTAGE_DELIVERED/UNDELIVERABLE/ACCEPTED/REJECTED`, `PERCENTAGE_THAT_TRANSITION`, `MAX_TIME_ENROUTE`, `DELAY_DELIVERY_RECEIPTS_BY`, `OUTBOUND_QUEUE_MAX_SIZE`, `OUTBIND_ENABLED`, `CAPTURE_*_DECODED_TO_FILE` |
| **ukarim/smscsim 0.2.0** — Go | Zero setup; MO injection from a web page; the smoke test | `ukarim/smscsim:0.2.0`, ports 2775 and 12775, no auth | `FAILED_SUBMITS=1` fails even sequence numbers and undelivers odd ones. No PDU validation, so it proves nothing about strictness | | **ukarim/smscsim 0.2.0** — Go | Zero setup; MO injection from a web page; the smoke test | `ukarim/smscsim:0.2.0`, ports 2775 and 12775, no auth | `FAILED_SUBMITS=1` fails even sequence numbers and undelivers odd ones. No PDU validation, so it proves nothing about strictness |
| **Melrose Labs SMSC Simulator** — hosted, closed | A peer nobody here can special-case; TLS 1.2 on 8775 with a public CA; `message_payload` on `deliver_sm`; 64-char ids; SMPP 3.3/3.4/5; sends `enquire_link` itself after 45 s idle; rejects a destination under 8 digits | `smscsim.melroselabs.com:2775`, free developer account; MO by writing the bound `system_id`'s digits into the destination address | 100 SMS/s cap; delivered-only receipts on the free tier, TLVs plus text; MO echoes the submit's `data_coding` |
| **smscsim.smpp.org** — hosted | Second closed peer for the same assertions | port 2775, 2 SMS/s cap | — |
| **jsmpp `SMPPServerSimulator`** — Java | Strict PDU validation on the SMSC side, from the best-maintained SMPP codebase found (pushed 2026-06) | Build from `opentelecoms-org/jsmpp` `jsmpp-examples`; no image | Only if the Java clients below leave a gap | | **jsmpp `SMPPServerSimulator`** — Java | Strict PDU validation on the SMSC side, from the best-maintained SMPP codebase found (pushed 2026-06) | Build from `opentelecoms-org/jsmpp` `jsmpp-examples`; no image | Only if the Java clients below leave a gap |
The two hosted simulators, Melrose Labs and smscsim.smpp.org, are **dropped**, maintainer's call,
2026-09-06: the useful tiers cost money a hobby project does not have, and the peers above already
carry the traffic they would have. What goes with them is named under [Untested](#untested) rather
than left to be assumed.
Skipped, with the reason: Kannel's `opensmppbox` is not in any package and last saw a commit in Skipped, with the reason: Kannel's `opensmppbox` is not in any package and last saw a commit in
2014; `fakesmsc` does not speak SMPP; OsmoMSC and Restcomm need a telecom stack for no extra 2014; `fakesmsc` does not speak SMPP; OsmoMSC and Restcomm need a telecom stack for no extra
coverage. coverage.
@@ -118,23 +121,23 @@ from real-world documentation, not interop runs, and belong in `test/`.
| # | Scenario | Assert | Peers | | # | Scenario | Assert | Peers |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| C1 | Bind each type, `enquire_link` both ways, `unbind` | Bound; SMSC-initiated `enquire_link` answered; close after our `unbind` is clean, no reconnect | all SMSC peers; Melrose (45 s idle), Jasmin (30 s) | | C1 | Bind each type, `enquire_link` both ways, `unbind` | Bound; SMSC-initiated `enquire_link` answered; close after our `unbind` is clean, no reconnect | all SMSC peers; Jasmin (30 s) |
| C2 | Text-only receipts, no TLVs | `dlr.smsId` equals the `submit_sm_resp` id; `statusMsg` right; `messageDlr` merges per segment | SMPPSim with `DELIVERY_RECEIPT_OPTIONAL_PARAMS` off | | C2 | Text-only receipts, no TLVs | `dlr.smsId` equals the `submit_sm_resp` id; `statusMsg` right; `messageDlr` merges per segment | SMPPSim with `DELIVERY_RECEIPT_OPTIONAL_PARAMS` off |
| C3 | Receipts with `receipted_message_id` + `message_state` | Same, and TLV wins over body when both present | Jasmin, Melrose, SMPPSim TLVs on | | C3 | Receipts with `receipted_message_id` + `message_state` | Same, and TLV wins over body when both present | Jasmin, SMPPSim TLVs on |
| C4 | Intermediate then final receipt | First arrives `intermediate: true` and never counts in `messageDlr`; final settles | SMPPSim `PERCENTAGE_THAT_TRANSITION` 100, `MAX_TIME_ENROUTE` | | C4 | Intermediate then final receipt | First arrives `intermediate: true` and never counts in `messageDlr`; final settles | SMPPSim `PERCENTAGE_THAT_TRANSITION` 100, `MAX_TIME_ENROUTE` |
| C5 | Failure states | UNDELIV, REJECTD, EXPIRED, ACCEPTD mapped; `messageDlr` carries the worst segment | SMPPSim percentages; Melrose dedicated tier | | C5 | Failure states | UNDELIV, REJECTD, EXPIRED, ACCEPTD mapped; `messageDlr` carries the worst segment | SMPPSim percentages |
| C6 | Receipt delayed past a link drop | Merge survives the reconnect; late receipt still reaches `dlr` | SMPPSim `DELAY_DELIVERY_RECEIPTS_BY` + kill the TCP link | | C6 | Receipt delayed past a link drop | Merge survives the reconnect; late receipt still reaches `dlr` | SMPPSim `DELAY_DELIVERY_RECEIPTS_BY` + kill the TCP link |
| C7 | Long MT: GSM with extension chars, UCS-2 with emoji, 2/3/10 segments | One id per segment; peer reassembles (loopback or MO route shows the whole text); receipt per segment | SMPPSim `LOOPBACK`, Jasmin MO route, Melrose | | C7 | Long MT: GSM with extension chars, UCS-2 with emoji, 2/3/10 segments | One id per segment; peer reassembles (loopback or MO route shows the whole text); receipt per segment | SMPPSim `LOOPBACK`, Jasmin MO route |
| C8 | Long MO as UDH 8-bit, UDH 16-bit, `sar_*`, `message_payload` | One `sms` with the whole text in every spelling (targets 2, 3) | Jasmin (SAR), Melrose (`message_payload`), SMPPSim loopback (UDH 8), fixture (UDH 16) | | C8 | Long MO as UDH 8-bit, UDH 16-bit, `sar_*`, `message_payload` | One `sms` with the whole text in every spelling (targets 2, 3) | Jasmin (SAR), jsmpp (`message_payload`, UDH 16), SMPPSim loopback (UDH 8) |
| C9 | MO or receipt on `data_sm` | Not refused; reaches `sms`/`dlr` (target 4) | Jasmin if configurable, else fixture | | C9 | MO or receipt on `data_sm` | Not refused; reaches `sms`/`dlr` (target 4) | Jasmin if configurable, else fixture |
| C10 | Unknown command id, unknown or malformed TLV, vendor TLV, zero-length integer TLV from the SMSC | Link stays up; `generic_nack`/`*_resp` with the right status; `sessionError` logged once (target 1) | fixture | | C10 | Unknown command id, unknown or malformed TLV, vendor TLV, zero-length integer TLV from the SMSC | Link stays up; `generic_nack`/`*_resp` with the right status; `sessionError` logged once (target 1) | fixture |
| C11 | Bind refused: bad password, `ESME_RALYBND`, TCP refused | Backoff 1 s → 30 s, one attempt per interval, no flood (target 10) | Jasmin bad creds, SMPPSim `SYSTEM_IDS`, closed port | | C11 | Bind refused: bad password, `ESME_RALYBND`, TCP refused | Backoff 1 s → 30 s, one attempt per interval, no flood (target 10) | Jasmin bad creds, SMPPSim `SYSTEM_IDS`, closed port |
| C12 | `ESME_RTHROTTLED` and `ESME_RMSGQFUL` on submit | `sendSms` returns `err` naming the status; session stays bound; next send works | Jasmin `submit_throughput`, SMPPSim queue sizes, smscsim `FAILED_SUBMITS` | | C12 | `ESME_RTHROTTLED` and `ESME_RMSGQFUL` on submit | `sendSms` returns `err` naming the status; session stays bound; next send works | Jasmin `submit_throughput`, SMPPSim queue sizes, smscsim `FAILED_SUBMITS` |
| C13 | Slow SMSC and a full window | `maxOutstanding` queues, nothing overruns; `close()` drains; `responseTimeout` fires as documented (target 11) | Jasmin `responseTimerSecs`, SMPPSim delays | | C13 | Slow SMSC and a full window | `maxOutstanding` queues, nothing overruns; `close()` drains; `responseTimeout` fires as documented (target 11) | Jasmin `responseTimerSecs`, SMPPSim delays |
| C14 | TLS bind against a public CA | Handshake, bind, send, receipt | Melrose 8775 | | C14 | TLS bind against a public CA | Handshake, bind, send, receipt | dropped — see Untested |
| C15 | `interfaceVersion` 0x50 and a peer answering 3.3 or no `sc_interface_version` | Bound; no TLVs sent to a pre-3.4 peer; TLVs sent to 3.4+ (target 8) | Melrose (v5), SMPPSim, fixture | | C15 | `interfaceVersion` 0x50 and a peer answering 3.3 or no `sc_interface_version` | Bound; no TLVs sent to a pre-3.4 peer; TLVs sent to 3.4+ (target 8) | SMPPSim, fixture; no 5.0 peer — see Untested |
| C16 | Receipt text variants from operator docs | LINK `sub:000 dlvrd:000 text:`; Vonage `stat:FAILED`; tyntec two receipts one id; Infobip `stat:ENROUTE` under 0x04; no `id:`; hex id in `submit_sm_resp`, decimal in receipt (target 7) | fixture | | C16 | Receipt text variants from operator docs | LINK `sub:000 dlvrd:000 text:`; Vonage `stat:FAILED`; tyntec two receipts one id; Infobip `stat:ENROUTE` under 0x04; no `id:`; hex id in `submit_sm_resp`, decimal in receipt (target 7) | fixture |
| C17 | Encodings round trip | Latin-1, UCS-2 big-endian, flash 0x10 and 0xF0, 8-bit binary with UDH — what comes back matches (target 12) | SMPPSim loopback, Melrose MO echo | | C17 | Encodings round trip | Latin-1, UCS-2 big-endian, flash 0x10 and 0xF0, 8-bit binary with UDH — what comes back matches (target 12) | SMPPSim loopback |
| C18 | `outbind` from the SMSC to our server | No bogus response PDU; logged (target 6) | SMPPSim `OUTBIND_ENABLED` | | C18 | `outbind` from the SMSC to our server | No bogus response PDU; logged (target 6) | SMPPSim `OUTBIND_ENABLED` |
### An ESME against our server ### An ESME against our server
@@ -168,7 +171,7 @@ fix before the next phase starts, per AGENTS.md.
| 5 | jsmpp + Cloudhopper (S2, S3, S5, S10) | 1 day | | 5 | jsmpp + Cloudhopper (S2, S3, S5, S10) | 1 day |
| 6 | python-smpplib + php-smpp (S2, S4, S11) | ½ day | | 6 | python-smpplib + php-smpp (S2, S4, S11) | ½ day |
| 7 | Load: smppload + smpp-dumb-client (S6, S8, S9) | ½ day | | 7 | Load: smppload + smpp-dumb-client (S6, S8, S9) | ½ day |
| 8 | Hosted, manual: Melrose and smpp.org (C1, C7, C8, C14, C15, C17) | 2 h | | 8 | ~~Hosted, manual~~ — dropped, see [Untested](#untested) | |
| 9 | Fixtures from the quirk list into `test/` (C8 UDH-16, C9, C10, C15, C16) | 1 day | | 9 | Fixtures from the quirk list into `test/` (C8 UDH-16, C9, C10, C15, C16) | 1 day |
| 10 | Decide 1.0 scope for targets 25: fix, or document as a limitation with the peers that refuse | — | | 10 | Decide 1.0 scope for targets 25: fix, or document as a limitation with the peers that refuse | — |
| 11 | One product-owner pass over everything the fixes changed on the public surface — see below | 2 h | | 11 | One product-owner pass over everything the fixes changed on the public surface — see below | 2 h |
@@ -214,6 +217,21 @@ Cheaper model, with these instructions and the tables above, no judgement calls:
Stronger model: reading a failure against the spec and the peer's docs, deciding fix versus Stronger model: reading a failure against the spec and the peer's docs, deciding fix versus
document, library changes, and the AGENTS.md decision record each one needs. document, library changes, and the AGENTS.md decision record each one needs.
## Untested
Dropping the hosted simulators leaves three things this suite never exercises. None is a known
defect; each is a claim resting on the specification and on Node rather than on a peer having agreed.
- **A TLS handshake against a certificate a public authority signed.** Every TLS test here, our own
and Cloudhopper's, uses a certificate generated for the test, so what is proven is that the
handshake works and that a bad certificate is refused. Verifying a real chain is Node's job and we
pass `tls.ConnectionOptions` through untouched, which is why this is a thin risk rather than none.
- **A peer that genuinely speaks SMPP 5.0.** `interfaceVersion: 0x50` is tested against peers that
answer 3.4 or answer nothing, so what 5.0 declares back is unobserved.
- **An SMSC written by someone who never sees this code.** Every peer here is open source and
configured by us. A closed commercial SMSC is the one thing a free suite cannot buy, and the first
operator integration is where that gets answered.
## Status ## Status
| Phase | State | Findings | | Phase | State | Findings |