← Skull Crash

Skull Crash for your board

Bring your callers into Skull Crash from your own BBS. They play as themselves, with no second username and no second password, and their characters follow them from visit to visit, keyed on their user number on your board.

SCLink is new. Every client below checks itself against the protocol, but not all of them have run on a live board yet; the sections say which. If yours does something odd, say what you saw. That report is worth a lot right now.

How it works

Your board does what BBSLink scripts do: one exchange over HTTPS, then a handoff to a gate your BBS package already has.

  1. Your board asks the game for a challenge.
  2. It signs the challenge with your board's secret and gets back a ticket, good for one caller, once, for 30 seconds.
  3. It connects the caller to the game, presenting the ticket (RLOGIN framing). The game answers, and the caller is at character select.

The secret never crosses the wire, and nothing depends on your clock being right.

Getting set up

Boards are set up by hand, one at a time. Get in touch with NuSkooler: on Xibalba itself, or by email at bryan@l33t.codes, with:

You get back:

ValueYoursNotes
Board tagDMB, say2–6 of A–Z / 0–9. Yours for good. Your callers show as Name@DMB
Secret64 hex charactersA password. Never post it, never paste it in a screenshot, and keep the file it lives in private
Hostsclink.xibalba.vip
Auth port4443HTTPS: the challenge and the ticket
Play port4514 TLS, or 4513 plainWhich one depends on your level
Leveltls / tls-auth / plainThe weakest your board may connect at

Levels: how much is encrypted

LevelChallenge and ticketPlay connectionWho uses it
tlsHTTPSTLS, port 4514ENiGMA½, and any board running sclink.py (STDIO or DOOR32) or sclink.sh
tls-authHTTPSplain TCP, port 4513Synchronet and Mystic: their RLOGIN gates have no TLS
plainHTTPplain TCP, port 4513A board whose software has no TLS at all

At tls-auth the game text and the caller's keystrokes cross the wire in the clear, as any telnet session's do. No password does: door accounts have none, and a ticket is spent the moment it is used. Each board is issued a level it may not go below.

The plain play port, 4513, is opened only for boards that need it. If yours is Synchronet or Mystic, say so when you get in touch.

Downloads

The clients, built from Skull Crash af3a0e21. MIT licensed (license). Each one checks itself against the protocol with --selftest; the SHA-256 is there so you can check the file you got is the file published here.

ENiGMA½

The same module Xibalba runs, in its SCLink mode. ENiGMA½ can always do TLS, so your level is tls.

  1. Copy sc_door.js to mods/sc_door/sc_door.js in your ENiGMA½ install. A mod lives in a directory of its own name. If you symlink it instead, set ENIGMA_ROOT to your install so the module can find ENiGMA's core.
  2. Add a menu entry. Keep the @userModule: prefix, or ENiGMA looks under core/ and the menu dies with MODULE_NOT_FOUND:
    skullCrash: {
        desc: Playing Skull Crash
        module: @userModule:sc_door
        config: {
            boardTag: DMB
            secret: "<the 64 hex characters you were issued>"
            host: sclink.xibalba.vip
            authPort: 4443
            playPort: 4514
            //  Optional:
            level: tls            //  tls (default) | tls-auth | plain
            connectTimeout: 10000
        }
    }
  3. Check your copy of the module:
    node -e "console.log(require('./mods/sc_door/sc_door.js')._sclink.selfTest())"
    true means its MAC matches the game's.

Certificates. The game's certificate is checked against your system's CA roots. If your roots are too old to trust it:

Everything Xibalba's own door does for its callers comes along: the music stops when they leave, slow links are paced, and UTF-8 callers are transcoded.

Synchronet

sclink.js asks for the ticket over HTTPS with the host name checked, then hands the caller over with Synchronet's own RLOGIN gate. That gate has no TLS, so your level is tls-auth, on the plain play port.

  1. Copy sclink.js to your mods/ directory.
  2. Add your values to ctrl/modopts.ini:
    [sclink]
    board     = DMB
    secret    = <the 64 hex characters you were issued>
    host      = sclink.xibalba.vip
    auth_port = 4443
    play_port = 4513
    level     = tls-auth
    ; optional
    timeout   = 10
    modopts.ini now holds a password: check who can read it.
  3. In SCFG › External Programs › Online Programs (Doors), in the section you want, add a program:
    • Command Line: ?sclink.js
    • Multiple Concurrent Users: Yes
    • No drop file. The I/O settings do not apply to a JavaScript module.
  4. Check your copy: jsexec sclink.js --selftest, from the mods/ directory, prints PASS.

Never add -p, -h or -H to an rlogin.js line pointed at Skull Crash. Those send the caller's password. sclink.js does not use rlogin.js at all.

Not yet checked on a live board: how Synchronet passes the CP437 byte 0xFF to a telnet caller, and its TLS against the game's certificate. Report what you see.

Mystic

sclink.mpy asks for the ticket over HTTPS with Mystic's Python, then hands the caller over with Mystic's IR (outbound RLOGIN) command. IR has no TLS, so your level is tls-auth, on the plain play port.

  1. Copy sclink.mpy to your Mystic scripts directory.
  2. Put sclink.json in the Mystic root, beside the mystic / mis binaries:
    { "board": "DMB",
      "secret": "<the 64 hex characters you were issued>",
      "host": "sclink.xibalba.vip",
      "auth_port": 4443,
      "play_port": 4513,
      "level": "tls-auth" }
    It holds a password: check who can read it. Optional keys: "timeout" (seconds), "ca" (a CA file to trust), "verify": false (last resort).
  3. Add a menu item that runs the Python script sclink. Check your Mystic version's menu command list for the "execute Python script" command.
  4. Check your copy: python sclink.mpy --selftest prints PASS.

Your callers are identified by their Mystic user record number.

Not yet checked on a live board: the order in which IR fills its two RLOGIN fields. The game accepts either order, so this should not matter.

WWIV, Talisman, and any board that runs a STDIO door

Two scripts, run as a STDIO door with the caller's user number as an argument. Each fetches the ticket, then pipes the door's input and output to the game.

sclink.py (Python 2.7.9+ or 3) is the one to use: it does TLS on every platform, Windows included.

python3 /path/to/sclink.py /path/to/sclink.json <user number> [rows] [cols]

sclink.json takes the same keys as Mystic's, with "level": "tls" and the TLS play port:

{ "board": "DMB",
  "secret": "<the 64 hex characters you were issued>",
  "host": "sclink.xibalba.vip",
  "auth_port": 4443,
  "play_port": 4514,
  "level": "tls" }

sclink.sh (bash, curl, OpenSSL 1.0.2+) is for a Unix board without Python.

/path/to/sclink.sh /path/to/sclink.conf <user number> [rows] [cols]
# sclink.conf -- keep it private
SCLINK_BOARD=DMB
SCLINK_SECRET=<the 64 hex characters you were issued>
SCLINK_HOST=sclink.xibalba.vip
SCLINK_AUTH_PORT=4443
SCLINK_PLAY_PORT=4514
SCLINK_LEVEL=tls

Optional for either script: set SCLINK_CALLER in the environment to the caller's IP. It is sent only over TLS, and the game keeps only a keyed hash of it.

On Windows, use sclink.py. The Windows builds of openssl s_client never read their input from a pipe, so sclink.sh refuses level tls there. Both scripts print PASS for --selftest. If your Windows board runs DOOR32 doors rather than STDIO ones, see below.

How to pass the user number, and how to declare a STDIO door, is up to your package: WWIV calls it a chain (Exec Mode: STDIO).

Not yet checked: Talisman.

EleBBS and other DOOR32 boards on Windows

Some Windows boards do not run STDIO doors. They write DOOR32.SYS and hand the door the caller's socket handle instead: EleBBS/Win32, and other Windows packages in their DOOR32 mode. sclink.py can read and write that socket itself, with no FOSSIL driver, NetFoss or bridge in between.

This mode needs Python 3 (checked on 3.13). Your level is tls, on the TLS play port.

python C:\sclink\sclink.py C:\sclink\sclink.json --door32 <path to DOOR32.SYS> [rows] [cols]

The socket stays your board's. The script never shuts it down: when the game is over it closes only its own handle and exits, and the caller is back on the board. The game's byte 0xFF (a CP437 glyph) is doubled on the way to the caller, and the telnet sequences the caller's client still sends are taken out on the way in. Negotiation is left to your board.

If the script cannot use DOOR32.SYS (it is missing, the comm type is 0 or 1, there is no handle, or the user number is not a number), it stops with the reason on its error output. Without a socket it cannot speak to the caller, who simply lands back on your board. Problems with sclink.json do reach the caller, as Skull Crash is not set up on this board yet.

Check your copy: python sclink.py --selftest prints PASS.

Not yet checked on a live board. The hand-off has been tested against a stand-in board on Windows 11 with Python 3.13, using a real inherited Winsock handle. EleBBS itself has not been run, and that EleBBS/Win32 has no STDIO door comes from third-party notes. Report what you see: whether the caller returns to the board when the game ends, and how art carrying 0xFF arrives.

If it does not work

What your caller sees is one of these. The game says nothing more, on purpose: a stranger probing the port learns nothing about which boards exist.

Caller seesMeaningWhat to check
Access denied. Tell your sysop.The game refused your boardYour secret, tag and level; that your board is on the address you gave; that your board has not been switched off
Skull Crash is busy. Try again shortly.Your board's limits, or too many failed attempts from your addressWait a minute. If it persists, get in touch
(the connection is simply reset)Too many connections from your address at once, or in a minuteThe same: wait, then get in touch
No new players from this board today.Your board's daily cap on new accountsReturning callers still get in; new ones can try tomorrow
Connection error. Tell your sysop.Something malformed reached the gameA broken script or an old copy: run --selftest
Skull Crash is unreachable right now.The game did not answer: it is down, or its remote door is offThe host and ports; your firewall's outbound rules; then get in touch
Skull Crash is not set up on this board yet.Your own config is incompleteThe config file. Synchronet logs which key is wrong; the other scripts print it on their error output
(nothing: straight back to your board)DOOR32 only: the script could not use DOOR32.SYS or the socket handleThe reason on its error output; that it is running on Python 3; that it was started directly

Every refusal of your board is logged with its exact reason on the game's side. When you get in touch, send your tag and the time it happened.

Writing your own client

For a package none of the above covers. The wire format, in brief:

GET /sclink/v1/challenge?board=<TAG>      -> "OK <challenge>"
GET /sclink/v1/ticket   (X-SC-* headers)  -> "OK <ticket>"
play connection: \0 <user> \0 <ticket> \0 <term>[/<speed>] \0  -> \0

The ticket request carries its fields as X-SC-* headers. The MAC is HMAC-SHA256 over these lines, joined with \n, with no trailing newline, ending in the literal END. An absent field is an empty line:

SCLINK1
<alg>
<board>
<user>
<challenge>
<rows>
<cols>
<term>
<caller>
END

The key is the ASCII text of the board's 64-hex secret, not the 32 bytes it decodes to. That is what openssl dgst -hmac "$secret" and Python's hmac.new(secret.encode('ascii'), …) both do. SCLink.vectors.json pins the expected results, so a new client can check itself, and the clients above are readable reference implementations. If you build one for another package, get in touch: it may be worth shipping here.