End-to-end test against the canonical Comet daemon
(comet 1.2.1, both bbsnode2 and a local instance from
build/linux/comet) surfaced three wire-level mismatches.
fpc-comet now talks the existing daemon's protocol exactly:
1) BOTH sides emit NPKT_INITACK. My driver only had
the inbound (answerer) sending INITACK. The existing
daemon waits for the originator's INITACK after
sending its own; without it the answerer hangs in
its post-INIT wait loop and times out (TimeoutSecs).
SendOurInitAck now fires on the outbound side too,
after authentication completes.
2) Transfer-phase frame routing widened. The peer can
race ahead and send transfer frames (FINFO end-of-
batch, FINFOACK, etc.) before we've fully transitioned
out of cmpAuth. Driver now routes transfer-typed
frames to the xfer engine in any post-auth phase
(cmpAuth / cmpKeyExchange / cmpTransfer / cmpShutdown)
instead of dropping them with "unhandled frame in
phase 3" warnings.
3) TComFsProvider.NextOutbound now computes SHA-256 of
each outbound file before returning the TCometSendItem.
Previously left Item.SHA256 as zeros, which the receiver
compared against its own freshly-computed hash and
correctly reported "SHA-256 mismatch" -- the bytes
transferred fine, the hash advertised in FINFO didn't.
Added HashFileSHA256 helper.
example_outbound also takes:
- CM_AKA env var for outbound local AKA (e.g. "1:213/725")
- CM_PRIVKEY env var for ED25519 private-key seed (hex)
- CM_LOG=trace for verbose diagnostics
- CM_REQUEST="*.txt" to FREQ files from peer
- CM_LIST="*" to LSTREQ a listing
- File argument now optional (handshake-only is allowed)
Validated against bbsnode2 (1:213/721, FreeBSD x64,
unmodified canonical Comet daemon code at port 26638) AND
against a local Linux build of the same code:
bbsnode2 dial: ED25519 verified, 82-byte file accepted
+ EOF acked, clean cmpDone, byte-identical
landing in inbound dir.
local dial: NOPWD path, same end-to-end success.
12 unit tests still pass; all 7 cross-targets clean.
That closes the wire-format gap. fpc-comet v0.1.0 is
interop-validated against the canonical implementation.
fpc-comet
A Free Pascal library for the Comet native FidoNet mailer protocol (FSP-COMET-001). Designed to be embedded in mailer daemons, plugins, and test rigs that need to speak Comet's wire protocol without dragging in the entire Comet daemon.
Sibling library to fpc-binkp
(BinkP/1.1, FTS-1026/1027) and fpc-msgbase
(message storage). Both can coexist in the same consumer; the
Comet daemon does this today via per-connection banner
auto-detection on a single TCP port.
Status
Current: 0.1.0 — full FSP-COMET-001 spec parity. Twelve test programs green; all seven cross-platform targets clean.
| Phase | Feature | ✓ |
|---|---|---|
| handshake | Banner + BinkP-detect dispatch | ✓ |
| handshake | INIT/INITACK negotiation (asymmetric) | ✓ |
| auth | NOPWD via COPT_NOPWD bit | ✓ |
| auth | Plain password | ✓ |
| auth | CRAM-MD5 (HMAC-MD5, FTS-1027) | ✓ |
| auth | ED25519 signature | ✓ |
| keyex | X25519 + ChaCha20 (COPT_CRYPT) | ✓ |
| transfer | FINFO / DATA / EOF / SHA-256 verify | ✓ |
| transfer | Sliding window + cumulative DATAACK | ✓ |
| transfer | Bidirectional simultaneous TX + RX | ✓ |
| transfer | Continuous bidir (small interleaved big) | ✓ |
| transfer | RPOS in-flight reposition | ✓ |
| transfer | Cross-session FINFOACK resume + SHA seed | ✓ |
| transfer | ZLIB per-block compression (COPT_ZLIB) | ✓ |
| files | FREQ + FREQNAK | ✓ |
| files | LST (LSTREQ / LSTITEM / LSTEND) | ✓ |
| policy | OnPostAuth + secure routing override | ✓ |
Scope
| Concern | Owned by |
|---|---|
| Comet wire protocol (frames, packets, state machine) | fpc-comet |
| NOPWD, plain, CRAM-MD5, ED25519 authentication | fpc-comet |
| X25519 ephemeral key exchange + ChaCha20 stream encryption | fpc-comet |
| Per-block ZLIB compression | fpc-comet |
| RPOS, cross-session resume | fpc-comet |
| Inline FREQ + LST | fpc-comet |
| Banner-based BinkP coexistence detection | fpc-comet |
| PKT / ArcMail / BSO queue | fpc-ftn-transport |
| Message storage (JAM, Squish, ...) | fpc-msgbase |
| Session dispatch, config persistence, UI, logging backends | consumer |
| Filesystem layout, retry policy, traffic accounting | consumer |
The library handles the wire. Consumers own everything above and below: the socket, the disk, the logging sink, the decision of which file to send next, what "secure" means for an inbound directory, who has FREQ access.
Architecture
Step-based engine. The consumer drives:
CMConfigDefaults(Cfg);
Cfg.Transport := TComTcpTransport.CreateClient('peer.example', 24554);
Cfg.Provider := TComFsProvider.Create('inbound', 'tmp');
SetLength(Cfg.LocalAddrs, 1);
Cfg.LocalAddrs[0] := MakeFTNAddress(1, 218, 720, 0);
Cfg.SystemName := 'My BBS';
Cfg.MailerName := 'fpc-comet/0.1.0';
Cfg.Log := @Logger.Log;
Cfg.OnLookupPassword := @PolicyHook.LookupPwd;
Cfg.OnPostAuth := @PolicyHook.PostAuth;
Session := TComSession.Create(cmDirOutbound, Cfg);
Xfer := TCometXfer.Create(Session);
Session.SetTransferHooks(@Xfer.HandleFrame, @Xfer.Step, @Xfer.IsDone);
Xfer.Start;
while Session.NextStep do
Cfg.Transport.WaitReady(True, False, 50);
R := Session.Result_;
WriteLn(Format('files=%d/%d wire=%d/%d auth=%d',
[Xfer.FilesSent, Xfer.FilesReceived,
R.WireBytesSent, R.WireBytesRecv, Ord(R.AuthMethod)]));
Each NextStep call does at most one unit of I/O + one state
transition and returns. This gives:
- Testability. Replace
Transportwith an in-memory pipe; drive both ends in lock-step from a single thread; assert on emitted frames. No sockets required. - Cancellation. Stop calling
NextStep; free the session. - Multiplexing. An event loop can drive N sessions on one
thread;
WaitReadyis the cooperative yield. - Observability. Inspect
Session.Phase,Xfer.FilesSent, hook callbacks fire at every policy point.
cm.driver owns the handshake state machine (cmpInit through
cmpAuth and cmpKeyExchange). cm.xfer owns the transfer
state machine (independent TX and RX, true simultaneous bidir
on a single connection). Consumers wire the two together via
SetTransferHooks.
See docs/FSP-COMET.001 for the canonical wire specification.
Naming
Unit prefix: cm.<name>.pas — cm = "comet". All units use
{$mode objfpc}{$H+}{$modeswitch advancedrecords}.
Building
./build.sh # all 7 targets
./build.sh x86_64-linux # just one
./run_tests.sh # native build + run test suite
Cross-targets: x86_64-linux, x86_64-freebsd, i386-linux,
i386-freebsd, i386-go32v2 (DOS/DPMI), i386-os2,
i386-win32. Reference TCP transport is UNIX-only; Windows /
OS/2 / DOS consumers plug in their own IComTransport.
License
GPL-2.0 — same as the Comet daemon source.
Layout
fpc-comet/
├── build.sh — multi-target build driver
├── CHANGELOG.md — release notes, tagged vX.Y.Z
├── README.md — this file
├── run_tests.sh — native build + test runner
├── docs/
│ └── FSP-COMET.001 — wire-protocol specification
├── examples/
│ ├── example_outbound.pas — embed as originator (TCP)
│ └── example_inbound.pas — embed as answerer (TCP)
├── src/
│ ├── cm.version.pas — CM_VERSION constant
│ ├── cm.types.pas — enums, records, NPKT_* + COPT_* consts
│ ├── cm.frame.pas — wire framing (LEN/TYPE/SEQ/CRC32)
│ ├── cm.md5.pas — standalone MD5 (RFC 1321)
│ ├── cm.cram.pas — CRAM-MD5 (HMAC-MD5)
│ ├── cm.sha.pas — SHA-256/384/512
│ ├── cm.ed25519.pas — EdDSA over Curve25519
│ ├── cm.ed25519.{sc,bp,ge}.pas — ED25519 field arithmetic
│ ├── cm.crypto.pas — X25519 + ChaCha20
│ ├── cm.zlib.pas — paszlib wrapper for per-block compress
│ ├── cm.events.pas — hook signatures (TCometOn*)
│ ├── cm.transport.pas — IComTransport interface
│ ├── cm.transport.tcp.pas — UNIX BSD-socket reference impl
│ ├── cm.provider.pas — IComFileProvider interface
│ ├── cm.provider.fs.pas — TFileStream-backed reference impl
│ ├── cm.config.pas — TCometSessionConfig record
│ ├── cm.session.pas — INIT codec + negotiation helpers
│ ├── cm.driver.pas — TComSession state-machine engine
│ └── cm.xfer.pas — TCometXfer transfer state machine
├── tests/
│ ├── cmtestutil.pas — TByteQueue, TMemPipePair, TMemProvider
│ ├── test_frame.pas — frame round-trip + CRC + truncation
│ ├── test_init.pas — INIT codec + negotiation
│ ├── test_session.pas — handshake-only roundtrip
│ ├── test_auth_cram.pas — CRAM-MD5 success + mismatch paths
│ ├── test_auth_ed25519.pas — ED25519 success + wrong-key + NOPWD-fallback
│ ├── test_xfer.pas — file-transfer roundtrip
│ ├── test_xfer_bidir.pas — bidir simultaneous (4 files, 2 each side)
│ ├── test_xfer_continuous.pas — pins down "small files done before big"
│ ├── test_xfer_resume.pas — FINFOACK offset + SHA-seed
│ ├── test_xfer_zlib.pas — ZLIB ON vs OFF wire-byte comparison
│ ├── test_xfer_crypt.pas — KEYEX + ChaCha20 roundtrip
│ └── test_freq_lst.pas — FREQ + LST end-to-end
└── units/ — compiled .ppu/.o output, per target
Origin
The Comet protocol was designed by Ken Johnson (1:218/720)
for the Comet mailer. This library extracts the protocol
implementation from the Comet daemon into a reusable,
embeddable form, mirroring the split that produced fpc-binkp
from cometbinkp.pas.