MBBS Is Not an Emulator. Runs unmodified MajorBBS and Worldgroup modules natively on Linux.
These modules are protected-mode binaries from the early '90s: 16-bit Phar Lap NE images and, for Worldgroup 3 and later, 32-bit PE images, linked against Galacticomm's host library and Btrieve. For most of them no source survives.
They are not emulated. x86-64's compatibility mode executes a 16-bit code
segment directly (CS.L=0, CS.D=0, installed in the process LDT), so the
module's original instructions run on the bare CPU with no interpreter and no
hypervisor. 32-bit modules run flat. What this project supplies is everything
underneath: the Galacticomm host API, a Btrieve implementation, and a modern
socket.
x86-64 Linux only. That is architectural, not a porting gap: the whole approach rests on x86-64 compatibility mode. There is no arm64 story.
Your kernel must be built with:
CONFIG_X86_16BIT=y # modify_ldt accepts a 16-bit code descriptor
CONFIG_X86_ESPFIX64=y # safe signal delivery with a 16-bit SS
Check yours:
zgrep -E 'CONFIG_X86_16BIT|CONFIG_X86_ESPFIX64' /proc/config.gz
grep -E 'CONFIG_X86_16BIT|CONFIG_X86_ESPFIX64' /boot/config-$(uname -r)CONFIG_X86_16BIT=n is a legitimate and increasingly common hardening choice.
On such a kernel nothing here works, and it fails at the first modify_ldt.
To find out whether your machine can do this before building anything:
git clone https://github.com/dcorbe/x86-compat16
cd x86-compat16 && make testThat is the standalone falsification suite for the claim this host rests on: a 64-bit process can create a 16-bit code segment, far-jump into it, execute there, take a signal, and return. If your kernel cannot, it says exactly where it breaks.
Developed against Linux 6.18.
Early. A handful of 16-bit and 32-bit modules from different vendors boot and are playable end to end. The host API is not complete: a module that imports a routine this host does not serve stops at startup with the routine named.
| area | state |
|---|---|
| Module loading | Works for both 16-bit NE and 32-bit PE modules. Add-on modules load alongside the main one. |
| Terminals | Works. Modern clients get UTF-8; period clients get the original CP437 and ANSI bytes. |
| Full-screen forms | Works, in both line mode and ANSI full-screen mode. |
| Btrieve | Works. Modules read and write their data files through a complete record manager. |
| BBS doors | Works. A real BBS can hang a module as a door through the relay binary. |
| Accounts | Works. MajorBBS's own account and key files, telnet login and signup, rlogin, and a sysop CLI. |
| Scripting | Works. Lua scripts can add commands a module never had. |
| Host API | Partial. Each new module tends to import something not yet implemented. |
You supply the module. This project ships no game content and no vendor binaries; see Provenance.
cargo build --release
./target/release/mbbs-server \
--root /path/to/board-data \
--module /path/to/MODULE.DLL \
--bturno 12345678 \
--listen 127.0.0.1:2323Then telnet 127.0.0.1 2323.
Useful flags:
| flag | why |
|---|---|
--module P |
Repeatable. The first is the one a caller enters; the rest are add-ons whose exports the first can reach. NE or PE decides which machine boots. |
--listen-raw ADDR |
A second port for period clients (SyncTERM and the like) that already speak CP437/ANSI.SYS. |
--listen-door PATH |
A Unix socket for door sessions; mbbs-door connects here on a BBS caller's behalf. |
--listen-rlogin ADDR |
An rlogin port for callers a fronting board already authenticated. See Logging in. |
--keys A,B |
The ring a new account is written with. Default DEMO,NORMAL,USER. |
--terms N |
Channel count. Default 2. |
--bturno DIGITS |
The board's eight-digit registration number. Modules key their licensing on it. |
--syscyc HZ |
How often the idle syscyc vector fires. Some modules step their world from it. |
--scripts DIR |
Lua scripts to load above the module. |
--help documents the rest.
The host keeps MajorBBS's own account and key files in the board directory, so a live pair from a period board can be dropped in and used as it is. A board without one gets an empty pair on first boot. Which names it uses follows the module's format:
| module | account file | key file |
|---|---|---|
| 16-bit NE (MajorBBS 6, Worldgroup 2) | bbsusr.dat |
bbsk.dat |
| 32-bit PE (Worldgroup 3) | wgsusr2.dat |
wgskey2.dat |
Three ways in:
| path | who decides | what happens |
|---|---|---|
telnet (--listen, --listen-raw) |
the caller | Asks for a user ID and a password. NEW at the user ID prompt signs up. Ctrl-D leaves. Three refusals close the connection. |
rlogin (--listen-rlogin) |
a fronting board | Takes the name the board sends and provisions it if new. No password crosses the wire, so bind it to a trusted network only. --rlogin-name first matches Synchronet's swap flag. |
door (--listen-door) |
a fronting board | Same trust as rlogin, through mbbs-door and a DOOR32.SYS. sysop=1 grants the sysop keys for that session only. |
Every new account gets the ring --keys names, default DEMO,NORMAL,USER.
Sysop keys come from an account's own ring or the door's session grant, never
from the default. Names are validated the way MajorBBS's own signup validates
them: letters, digits, spaces and a few punctuation marks, at most 29
characters, no leading space or punctuation.
The telnet port is not a hardened public login: passwords travel in the clear, as MajorBBS stored them, and nothing rate-limits connections.
mbbs-user administers the accounts. With the board running it sends each
command to the server over mbbs-user.sock in the board directory and the
server applies it, so the next login sees the change. With the board stopped
it edits the files directly. An account somebody is logged in as cannot be
edited until they log off. list always reads the file, so a caller with a
session shows the flags they had at their last logoff, not whatever changed
since.
./target/release/mbbs-user --root /path/to/board-data list
./target/release/mbbs-user --root /path/to/board-data add Dan --password secret
./target/release/mbbs-user --root /path/to/board-data keys Dan --add SYSOP --add WCCSYSOP
./target/release/mbbs-user --root /path/to/board-data delete Dandelete only tags the account. The nightly maintenance run, or SIGUSR1,
purges tagged accounts and calls every module's own delete-account routine,
so MajorMUD removes the character too. master sets the MASTER flag, which
holds every lock including MajorMUD's negative ones; a sysop who plays wants
SYSOP and WCCSYSOP in the ring instead.
Execution. 16-bit code runs on the CPU in compatibility mode via LDT
descriptors; 32-bit modules run flat. Faults and signals are arbitrated
process-wide, because a process has exactly one LDT and one set of signal
dispositions. See crates/mbbs-machine.
The host API. Every entry point a module imports from MAJORBBS, GSBL
and GALME is reimplemented in Rust and dispatched at the ABI border, so one
host drives both word sizes. See crates/mbbs.
Btrieve. The record manager these modules store everything in, implemented
from the file format up: pages, keys, duplicate chains, transactions. See
crates/btrieve.
Transport. Tokio owns the sockets; the machine owns one thread. Terminal
translation happens at the socket, never in the module's view of the world.
See crates/mbbs-server.
| crate | what it does |
|---|---|
mbbs |
The host API: every entry point a module can call. |
mbbs-machine |
Execution: LDT, faults, NE/PE loading, the 16/32-bit ABI border. |
mbbs-server |
The socket edge: tokio, telnet, CP437, ANSI compatibility, channel pool, doors. |
mbbs-lua |
The Lua extension seam. |
btrieve |
The Btrieve 6.15 engine. |
btrieve-oracle |
The wire protocol for driving genuine Btrieve under Wine. |
dos, dos-runtime |
A DOS kernel and runtime, for the DOS services modules and their utilities reach. |
textscreen |
Codepage, cell grid and painter behind the full-screen work. |
cnf |
An editor for a module's sysop-configurable options. |
bropey |
A byte-first persistent rope. |
- Not an emulator. It runs the original binary on the CPU.
- Not MBBSEmu. MBBSEmu is the mature C# MajorBBS emulator and the prior art this project learned from. Different goal, different tradeoffs; not a competitor and not a replacement.
- Not a BBS. No logon, no menus, no user manager. It boots modules headless and puts a socket in front of them. A real BBS can hang them as doors.
- Not a preservation archive. It ships no game content and no vendor binaries.
- Not finished. See Status.
- MBBSEmu (MIT, © Nusbaum Consulting): prior art for running MajorBBS modules at all.
- The documentation community that kept thirty-year-old material alive, and The Internet Archive, without which much of it would simply be gone.
This host contains no Galacticomm, Borland, Pervasive, or Phar Lap IP. It is an original Rust implementation written against a documented API surface, including the Btrieve record manager, which is implemented from the file format up rather than wrapped.
The one vendor-derived artefact is a set of ordinal-to-symbol-name tables for
MAJORBBS, GSBL, GALME and Phar Lap's DOSCALLS, extracted from the
export tables of the corresponding binaries. That is interoperability
information of exactly the kind Wine has shipped as .spec files for thirty
years.
You supply your own module binaries and your own board data. Nothing here distributes either.
MIT. See LICENSE.md.