Skip to content

Fix dial-up modem path end-to-end: answer tone, PPP runtime, transparent V.22bis data - #26

Closed
nicolasumaras wants to merge 6 commits into
mainfrom
fix-v8-answer-tone-ansam
Closed

Fix dial-up modem path end-to-end: answer tone, PPP runtime, transparent V.22bis data#26
nicolasumaras wants to merge 6 commits into
mainfrom
fix-v8-answer-tone-ansam

Conversation

@nicolasumaras

Copy link
Copy Markdown
Owner

Gets a real Windows XP dial-up client all the way to an authenticated PPP/IP connection over the spandsp soft-modem.

Code fixes

  • Answer tone: worker sent fax CNG (modem_connect_tone = trueFAX_CNG) instead of ANSAM_PR; no modem could ever train against it.
  • Transparent PPP data path: pppd runs async HDLC itself; worker now passes the byte stream through (V.14 only) instead of double-framing it.
  • V.8→V.22bis handoff use-after-free: freed/realloced modem state inside the v8_rx callback; now deferred to the sample loop (spandsp v8_tests.c pattern). Verified by a new loopback test.
  • pppd-supervisor: passed a non-existent chap-secrets option (→ error 721); now renders /etc/ppp/chap-secrets and launches pppd directly.
  • Forced-mode pty-open race fixed so pppd starts reliably.

Deploy

  • Documents the PPP runtime that was never set up on the VM: ppp package, a kernel with PPP (cloud kernel has none → generic kernel), a systemd drop-in so the hardened unit can run setuid pppd, and pre-created secrets files.

Known limitation

  • Live service uses SIPFAX_MODEM_START_MODE=v22bis. Standards V.8 negotiation selects V.22bis but the real modem won't complete training (timing/interop; not reproducible in the spandsp loopback). Forced V.22bis trains reliably.

🤖 Generated with Claude Code

nicolasumaras and others added 6 commits June 26, 2026 18:24
`modem_connect_tone` in spandsp's `v8_parms_t` is a `MODEM_CONNECT_TONES_*`
enum, not a boolean. The worker set it to `true` (== 1 ==
MODEM_CONNECT_TONES_FAX_CNG), so as the V.8 answerer it emitted the fax
calling tone (1100 Hz, 0.5s on / 3s off) instead of a modem answer tone.

Calling modems never received a valid answer tone, so V.8 always failed and
the worker fell back to V.21, holding a 1650 Hz mark the caller could not
train against. Captured RTP confirmed the 1100 Hz CNG cadence outbound and
near-total silence inbound (the modem never engaged).

Set it to MODEM_CONNECT_TONES_ANSAM_PR (2100 Hz ANSam with phase reversals),
the standard V.8 answer tone. Verified offline (worker now emits continuous
2100 Hz ANSam) and on a live Windows dial-up call: V.8 now succeeds and
selects V.22bis at 2400 baud (v8-v22bis-selected) instead of failing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
V.22bis is a synchronous modem, but Windows dial-up carries asynchronous
PPP (8N1). The worker fed the raw V.22bis bit stream straight into an 8-bit
accumulator with no start/stop handling, so characters never aligned to PPP
HDLC flags and `decodedBytes` stayed 0 (PPP could never come up). The V.21
path already handled this via spandsp's FSK async framing; V.22bis did not.

Insert spandsp async_rx/async_tx (use_v14=TRUE) between the V.22bis bits and
the existing HDLC byte framing:
  rx: v22bis bits -> async_rx (strip start/stop) -> hdlc_rx_byte -> pty
  tx: pty -> hdlc bytes -> async_tx (add start/stop) -> v22bis bits

Also fix v22bis_status: spandsp SIG_STATUS_* codes are all negative, so the
old `status < 0` test could never report carrier-up or training-succeeded.
Decode the specific codes instead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Several fixes needed to carry real async PPP and to negotiate cleanly:

- Transparent data path: pppd runs async HDLC itself, so stop re-framing.
  async_put_byte() now writes the recovered octet stream (0x7E flags,
  byte-stuffing, FCS intact) straight to the pty, and poll_pty() queues the
  raw pty octets for V.14 transmission instead of HDLC-framing them. Without
  this, pppd saw de-framed payloads with no frame delimiters and LCP never
  ran.

- V.8 handoff use-after-free: v8_result() runs synchronously inside v8_rx(),
  which keeps using the v8 context afterwards. Freeing v8 and allocating the
  v22bis/async state there corrupted that context (the new state often reused
  the freed block). Now the callback only records the decision; main() does
  the v8_free()/data-modem start after v8_rx/v8_tx return (per spandsp's
  tests/v8_tests.c pattern). Verified with a spandsp loopback harness.

- V.8 modulation choice now masks the caller's offer with what we advertised,
  so we don't get pulled into V.22bis when only V.21 was offered.

- Forced-mode start (SIPFAX_MODEM_START_MODE) defers entry to the first input
  frame, so the operator side is wired before pty-opened fires (start-up race
  that intermittently prevented pppd from starting).
pppd has no command-line option to select a secrets file; it always reads
/etc/ppp/{chap,pap}-secrets. The supervisor wrapped pppd in a shell that
appended a bogus `chap-secrets <tmpfile>` option, which pppd rejects
("unrecognized option 'chap-secrets'"), so it exited before LCP and the
caller saw error 721.

Render the per-call credentials to the standard path before launch (single
active call), spawn pppd directly (no wrapper, no bogus option), and clear
the file in place on teardown -- we own the file but not the root-owned
/etc/ppp directory, so truncate rather than unlink.
Standalone spandsp loopback (caller <-> answerer) that runs full V.8
negotiation and the V.22bis handoff, in both forced and V.8 modes, with an
optional echo model. Used to prove the handoff logic is correct independent
of the real modem/channel.

  cc -O2 -o v8handoff_test tests/v8handoff_test.c -lspandsp -lm
  ./v8handoff_test forced          # PASS
  ./v8handoff_test v8              # PASS (handoff trains s2s)
  ECHO_GAIN=0.3 ECHO_DELAY=64 ./v8handoff_test v8   # PASS with echo
PPP never worked on the first VM because the runtime was never set up:
the ppp package was absent, the cloud kernel has no PPP modules, and the
hardened systemd unit blocks setuid pppd. Add the systemd drop-in that grants
pppd the privileges it needs (without privileging the node process) and a
runbook section covering the ppp package, generic kernel, the drop-in, and
the pre-created /etc/ppp secrets files. Also document why the live service
forces V.22bis instead of relying on V.8 negotiation.
@nicolasumaras
nicolasumaras deleted the fix-v8-answer-tone-ansam branch June 27, 2026 14:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant