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.
- Your board asks the game for a challenge.
- It signs the challenge with your board's secret and gets back a ticket, good for one caller, once, for 30 seconds.
- 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:
- Your board's name, and what it runs.
- Its public IP address(es), if they are fixed. A board whose addresses are pinned cannot be impersonated from anywhere else. If your board is dual-stack, give both the IPv4 and IPv6 addresses: a board that asks over one and connects over the other is refused unless both are pinned.
- Whether your callers' terminals are CP437 (SyncTERM, NetRunner, icy_term: most BBS callers) or UTF-8, and whether your package passes the game's bytes straight through. If they are UTF-8 and your gate does not transcode, the game can transcode for your board.
You get back:
| Value | Yours | Notes |
|---|---|---|
| Board tag | DMB, say | 2–6 of A–Z / 0–9. Yours for good. Your callers show as Name@DMB |
| Secret | 64 hex characters | A password. Never post it, never paste it in a screenshot, and keep the file it lives in private |
| Host | sclink.xibalba.vip | |
| Auth port | 4443 | HTTPS: the challenge and the ticket |
| Play port | 4514 TLS, or 4513 plain | Which one depends on your level |
| Level | tls / tls-auth / plain | The weakest your board may connect at |
Levels: how much is encrypted
| Level | Challenge and ticket | Play connection | Who uses it |
|---|---|---|---|
tls | HTTPS | TLS, port 4514 | ENiGMA½, and any board running sclink.py (STDIO or DOOR32) or sclink.sh |
tls-auth | HTTPS | plain TCP, port 4513 | Synchronet and Mystic: their RLOGIN gates have no TLS |
plain | HTTP | plain TCP, port 4513 | A 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.
SHA-256 c475872746b764b4ca46b348a1dc7892072147f5f10da20c65f16164c6fafab4
SHA-256 173214d5c3cad2c9b5bbdb2db68bb4484ae19e8ece89f22a38f2311702646fe1
SHA-256 6262a0b6f1eaf5dd844f0784ecacec280855f4c31aa53b6eafdf7e9bb3a50bc1
SHA-256 bffe6e7084313242548c7447cab5948b94561a60bc0a78af610218317b1bd51c
SHA-256 6bb62151776e5cf5c5c08691a8d63d0ce4bfba4c074ee1efcf4642b9be80e3bb
ENiGMA½
The same module Xibalba runs, in its SCLink mode. ENiGMA½ can always do TLS, so your level is tls.
- Copy
sc_door.jstomods/sc_door/sc_door.jsin your ENiGMA½ install. A mod lives in a directory of its own name. If you symlink it instead, setENIGMA_ROOTto your install so the module can find ENiGMA's core. - Add a menu entry. Keep the
@userModule:prefix, or ENiGMA looks undercore/and the menu dies withMODULE_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 } } - Check your copy of the module:
node -e "console.log(require('./mods/sc_door/sc_door.js')._sclink.selfTest())"truemeans 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:
ca: /path/ca.pemtrusts exactly that CA.pin: "<sha256 hex>"trusts exactly one certificate. Compute the value yourself:
A pin breaks when the certificate is renewed, so preferopenssl s_client -connect sclink.xibalba.vip:4443 </dev/null 2>/dev/null | openssl x509 -outform der | openssl dgst -sha256ca.verify: falseturns the check off. This is a last resort: anyone between you and the game could then pose as 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.
- Copy
sclink.jsto yourmods/directory. - 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 = 10modopts.ininow holds a password: check who can read it. - 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.
- Command Line:
- Check your copy:
jsexec sclink.js --selftest, from themods/directory, printsPASS.
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.
- Copy
sclink.mpyto your Mystic scripts directory. - Put
sclink.jsonin the Mystic root, beside themystic/misbinaries:
It holds a password: check who can read it. Optional keys:{ "board": "DMB", "secret": "<the 64 hex characters you were issued>", "host": "sclink.xibalba.vip", "auth_port": 4443, "play_port": 4513, "level": "tls-auth" }"timeout"(seconds),"ca"(a CA file to trust),"verify": false(last resort). - Add a menu item that runs the Python script
sclink. Check your Mystic version's menu command list for the "execute Python script" command. - Check your copy:
python sclink.mpy --selftestprintsPASS.
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=tlsOptional 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]sclink.jsonis the same file as for a STDIO door, with"level": "tls"and"play_port": 4514.- The DOOR32.SYS path is wherever your board writes it. For EleBBS that is the node directory; on a multi-node board, use your package's node-number placeholder.
- The script reads three lines of it: line 1 (must be
2, telnet), line 2 (the socket handle) and line 5 (the user record number, which becomes the caller's user number). CRLF line endings are fine. rowsandcolsare optional, as for a STDIO door.- Run it directly from the door's command line, as a Windows program, so the handle your board made inheritable reaches it. A
.batin between should pass it on, though that has not been checked. A second program that would need the handle handed to it again will not work. SCLINK_CALLERworks here too, if your package can set it.
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 sees | Meaning | What to check |
|---|---|---|
Access denied. Tell your sysop. | The game refused your board | Your 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 address | Wait a minute. If it persists, get in touch |
| (the connection is simply reset) | Too many connections from your address at once, or in a minute | The same: wait, then get in touch |
No new players from this board today. | Your board's daily cap on new accounts | Returning callers still get in; new ones can try tomorrow |
Connection error. Tell your sysop. | Something malformed reached the game | A 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 off | The 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 incomplete | The 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 handle | The 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 -> \0The 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>
ENDThe 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.