Ken Johnson 24a2350a51 Live interop fixes against the existing Comet daemon
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.
2026-04-22 14:58:36 -07:00
2026-04-22 08:03:00 -07:00

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 Transport with 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; WaitReady is 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>.pascm = "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.

Description
FPC/FreePascal Comet native protocol library (FSP-COMET-001) -- ED25519 + ChaCha20 + sliding-window file transfer
Readme 403 KiB
Languages
Pascal 98.7%
Shell 1.3%