Writing and Running a BBS on a Macintosh Plus

In 2015, I wrote a custom BBS server in Ruby and had been using it to run my Kludge BBS on a small OpenBSD server in my home office since then.

Last year after writing a lot of C on my Macintosh Plus, I had the itch to write a new BBS server so I could move my BBS to run on another Mac Plus. As with all software development projects, it took quite a bit longer than expected, but last month I finally got far enough with the development to deploy the new BBS on a Mac Plus.


My deployment target is a platinum Macintosh Plus with an 8 MHz Motorola 68000 processor, 4 MB of RAM (the maximum supported), and a SCSI2SD v5.5 external SCSI hard drive with a 2 GB HFS "disk" on a Samsung PRO Endurance microSD card. The Mac is running System 6.0.8.

Noctua fan

While the Mac Plus is normally fanless, I was concerned about heat buildup running it 24/7, so I installed a small Noctua fan inside the case that still operates at near silence. With the internal CRT also powered on 24/7, I thought it would be useful to add a toggle switch to reduce power usage and heat, and avoid screen burn-in, but this does not seem very straightforward. I did recap the analog board before setting everything up and it's been running for a couple months now without issue, but time will tell how long everything lasts.

For network connectivity, the Mac Plus has a DaynaPORT SCSI/Link-3 SCSI Ethernet adapter which connects to a managed switch on its own VLAN, connected to my OpenBSD firewall which routes the Mac Plus's static IP over my internet connection. TCP/IP is provided by MacTCP 2.1.

Side note: I implemented a virtual DaynaPORT SCSI/Link-3 Ethernet adapter in the PCE MacPlus emulator, allowing it to do real network connections through a tap device on the host.

For modem connectivity, I am using a US Robotics 5686 modem connected to a Grandstream HT814 VoIP ATA, which routes SIP traffic through the same OpenBSD firewall running Asterisk, which then routes that traffic through Twilio for PSTN service. I took the US Robotics modem board out of its normal case and rigged it up inside the case of an Apple Modem 300/1200, and soldered a green power LED to the front.

For backups and for transferring files between my Mac Plus workstation, I am using an Iomega Zip 100 SCSI drive. For those keeping track, that's a SCSI ethernet device, SCSI hard drive, and SCSI Zip drive on an 8 MHz computer from 1986. I don't know why the non-server PC market never caught on to using SCSI for hard drives and peripherals.

DaynaPORT SCSI/Link-3 Ethernet
Iomega Zip 100 Drive, and Apple 300/1200 Modem

Revision Control

As with all of my classic Mac OS projects, I would be writing the code for the BBS on a Mac Plus in the THINK C 5 IDE. Since THINK C's editor only has one level of undo, once a file is saved, I can't go back without restoring a file from a backup, and my previous method of making a date-stamped copy of my code directory was not scaling well.

Before I started writing the BBS, which I am calling Subtext, I wanted to have a proper revision control system so I could commit things in small chunks, view diffs of what's in my work directory, and if needed, revert changes to a file.

There are some proprietary development tools that work on newer machines running System 7, but since I am doing everything on System 6 and am limited to 4 MB of RAM, I opted to create my own lightweight tool that I call Amend.

I also created a lighthearted GitHub-like web interface to Amend repositories, which I call AmendHub. I periodically upload the Amend repo from my Mac Plus to my OpenBSD laptop, and then upload it to AmendHub where new amendments are imported.

Continued development of Amend along the way also took quite a bit of time, like having to change its on-disk database format, but at this point I consider it to be quite stable.


The next hurdle was needing a cooperative threading mechanism. In the same way that my Ruby BBS used Fibers, this would allow me to write code in a very top-down, procedural format without having to use annoying callbacks, while still allowing concurrent interactive user sessions. This took a bit of research and development on classic Mac OS since I had never built such a thing before, but it ultimately resulted in a small uthread library (code) that uses setjmp and longjmp to give each thread its own stack and then switch between them once a thread calls uthread_yield.

In Subtext, this is done whenever the user's session has to wait for input or wait for its per-node module to flush its output.


Some other BBS packages that I've come across for System 6 and 7 rely on the Communications ToolBox for doing TCP/IP communication and modem access, but I decided not to go this route.

Subtext is written to use the ubiquitous MacTCP system extension for TCP/IP access which is a bit quirky compared to Unix sockets, especially when implementing servers. A TCP stream is created to passively listen on a TCP port, but once a connection is established on it, a second stream must be immediately setup to listen for a second connection or that request will be lost.

There are also some quirks related to finding the state of a listening stream that were not documented and took a lot of real-world debugging to figure out why the server would just stop accepting new connections after a day or so. Luckily the amount of bot-scanning traffic that the BBS receives just being connected to the internet provides a lot of garbage traffic to experiment with.


Since Subtext runs on a Mac with a screen, it seemed appropriate to allow the sysop to log in locally on a console. This required writing a VT100-capable terminal client which could now be easily extracted out into a dedicated telnet client. I'm currently using ZTerm on my Mac Plus workstation to connect to other BBSes via modem, but it doesn't support telnet without a Serial->WiFi adapter.

Design Detours

Winding along its 8 month development, I had a few detours and over-engineering of obscure components. While I'm most likely the only one that will ever use Subtext, I avoided hard-coding anything specific to my BBS in it. I made a templating mechanism to dynamically expand variables in views, and a GUI-based view editor that can be used for creating things like the menu:

While I initially intended to add GUI-based editors for things like the user database and message boards, once I had a working sysop console and could access all of the BBS functionality locally, I ended up writing this into Subtext as interactive menus. This has the benefit that I can manage them remotely through telnet rather than having to be at the console.

Screen Saver

Since the Mac Plus will always have its screen on, I wanted to avoid CRT burn-in while it's just showing the log window waiting for a call. I can't rely on another screen saver like After Dark because it doesn't know anything about the BBS activity and can't automatically deactivate when there is activity that I want to see. It's also very CPU-intensive to make toasters fly, so I want it stopped as soon as a new connection comes in.

I wrote a simple opportunistic screen blanker that kicks in at a configured timeout of inactivity when there are no active sessions, and just blacks out the entire screen for a small amount of configured time or until something is logged.

Web Login over WebSockets

Once telnet and modem connectivity were complete, I wanted to bring back web access. My Ruby BBS had an integrated web server which served some custom JavaScript that talked back to the BBS over a WebSockets connection. My JavaScript implemented a VT100-capable terminal so the BBS could send out ANSI escape sequences just as if it were talking to a telnet client.

While the Mac Plus is capable of serving telnet clients on its own, I was not going to implement the whole WebSockets spec in addition to a custom HTTP server. Additionally, due to MacTCP's single-connection-on-a-socket limitation, it would be difficult to make the HTTP server not drop HTTP requests during the listen->open->listen state change.

Since my OpenBSD firewall is already in between, I setup nginx with websocketd to execute a stripped-down telnet binary on the OpenBSD firewall.

A supervisor script runs websocketd on port 1080 with the path to the custom telnet binary:

websocketd --binary=true --port=1080 env TERM=ansi .../telnet -EK

When a new web connection comes in, nginx serves the front-end HTML and JavaScript. The JavaScript makes a WebSocket connection back to nginx at the /bbs path, and nginx then passes it to websocketd, which launches telnet.

server {

	location = /bbs {
		proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
		proxy_set_header Upgrade $http_upgrade;
		proxy_set_header Connection "upgrade";

The telnet binary connects to the Mac Plus through its normal telnet server, but passes the web client's IP address as a telnet environment variable, which Subtext will honor and display it as the remote IP it's talking to, even though it's actually communicating with the firewall's IP. websocketd automatically handles the shuffling of input and output data between the telnet client and the WebSockets connection.

Since the previous HTML displayed a fake Windows 3.1-era telnet client, I changed it to resemble a Mac running ZTerm. The old interface used my DOS 437-codepage font to display high-ascii ("ANSI") box characters properly in the browser, but since the new BBS is only using plain ASCII and is being displayed on a fake Mac screen, I needed a TrueType version of the old Monaco 9 font from System 6. I couldn't find a proper one, so I made one by creating a BDF-format font with all of the characters from a screenshot, then converted it to TrueType with Bits'N'Picas.

Fake Telix web interface to old BBS
Fake ZTerm web interface of new BBS

File Transfers

The last major part of the BBS was handling file transfers with ZMODEM. The ZMODEM protocol is rather large and has a complicated state machine, so I wasn't looking forward to writing my own implementation, especially for something that has to be compatible with other terminal software. The few open-source C implementations available have questionable licensing or are GPL'd, but I eventually found a BSD-licensed version from Tera Term that was easy to separate.

Integrating it with Subtext was not terribly difficult but it did require a week of debugging with other terminal software to fix bugs and set timeout limits appropriately. There is still no ZMODEM support in the web front-end, so file transfers don't yet work there, and I'm sure there will be other minor compatibility issues that need work, but to be honest, the file areas were barely used at all on the old BBS.

Telnet Bots

Once the new BBS was deployed last month, I started to get annoying bot traffic. Thousands of bots and infected machines constantly scanning the internet are a minor inconvenience to a modern server, but are troublesome to a very slow server. Every few minutes of every day, a random IP tries to login to my BBS with default usernames and passwords like "root", "sysadmin", "enable", etc. If the connection is closed, they will try again, over and over as it runs through its dictionary. Since the Mac is very limited in resources, these connections can prevent legitimate traffic from connecting or make their sessions very slow since the BBS is using cooperative threading.

Subtext has a list of banned usernames and when a login is attempted with one of these, the connection is dropped and the IP is banned. Future connections from that IP will immediately close, but unfortunately there is no way to refuse the connection before it opens so the BBS still has to do its dance of accepting the new connection, then starting a new listening socket, then closing the first connection.

A recent change goes a step further and sends a UDP packet to my OpenBSD firewall containing the IP to be banned, and a small Ruby server running there adds the IP to a pf table, immediately blocking all further IP access from the bot.

Future Work

I haven't implemented SSH logins yet, which would also forward from the OpenBSD firewall. For my old Ruby BBS, it ran a small, custom Go-based SSH server. When a user connected and provided a username and password over SSH, the Go SSH server would connect to a local socket that the BBS was listening on and send the username and password, the BBS server would authenticate it and respond with an acknowledgment or failure, and if it succeeded, the Go SSH server would open a new TCP connection to the BBS, proxying data between the BBS and the client.

I still need to implement this, but using the telnet client in-between. The telnet protocol makes it easy to send data out-of-band, such as the already-authenticated username. However, since SSH-scanning bots are more prevalent and aggressive than the occasional telnet-scanning bot, I'm not terribly motivated to write this new SSH server.


If you read this far and want to try out the BBS, you can telnet to klud.ge, call +1 312-654-0090 with a modem (8N1), or login with a fancy web browser at klud.ge. If the BBS doesn't answer, please try again later as it might be seeing a large spike in traffic (or has crashed and I'm scrambling to find the bug).

The freely-licensed C source code for the Subtext BBS server is available on AmendHub.

If you're interested in classic Mac development, check out my C Programming on System 6 videos or chat with us in the #cyberpals IRC channel on Libera Chat.

Questions or comments?
Please feel free to contact me.