A lightweight TCP proxy firewall for BBS telnet connections, built with Node.js.
By Sysop Network — https://github.com/SysopNetwork/BBSFirewall
| Feature | Description | |
|---|---|---|
| 🔀 | TCP Proxy | Forwards telnet connections to a backend BBS server |
| 🔒 | SSH | Encrypted SSH access — answered by the firewall and proxied to the telnet backend, or passed through to a backend with its own SSH server |
| 🌐 | HTTPS Redirect | Redirects both HTTP (port 80) and HTTPS (port 443) to a configured URL |
| 🔑 | Let's Encrypt | Built-in cert setup script with auto-renewal, no downtime required |
| 🛠️ | Web Config Editor | HTTPS admin UI (port 8443) to edit .env and IP lists; trusted-host + login gated |
| 🔐 | Two-Factor Auth | TOTP authenticator app + one-time backup codes protect the admin UI, on top of scrypt-hashed passwords |
| 📜 | Logs | Search every log file at once, read any file with live follow, and download one file or all of them as a .zip — grouped by service, with older files folded into per-month sections |
| 👥 | Admin Roles | Global Admin and Firewall Admin accounts — helpers can run the firewall day to day without being able to change who reaches the admin tools |
| 🧱 | Host Firewall (UFW) | Keeps the host's own ufw rules in step with your settings, and copies blocked addresses into the kernel (Linux, optional) |
| 📊 | System Stats | Live CPU, memory, disk, and bandwidth metrics for the running firewall process |
| 🔌 | Management API | Key-authenticated REST API to automate firewall configuration from your own tooling |
| 📡 | PROXY Protocol v1 | Passes the real client IP to the backend BBS (requires compatible backend) |
| 🌍 | Country Blocking | Block connections by country using a local GeoIP database |
| ✅ | IP Whitelist | Trusted IPs that bypass all firewall rules |
| 🚫 | IP Blocklist | Permanently block specific IPs or CIDR ranges |
| ⚡ | Rate Limiting | Automatic flood protection with configurable temporary blocks |
| 🎯 | Auto-Block Triggers | Blacklist an IP that sends a known bot/scanner/exploit string in its first bytes |
| 🔗 | Per-IP Connection Limit | Cap simultaneous connections from a single IP |
| 🖥️ | Encoding Detection | Automatic UTF-8/CP437 detection with separate backend routing |
| 🛑 | Graceful Shutdown | Clean shutdown on SIGTERM/SIGINT, active sessions drain properly |
| ⚙️ | PM2 Ready | Includes ecosystem.config.js for process management |
- Node.js 14 or higher
- Root or
CAP_NET_BIND_SERVICEto bind ports below 1024 (23, 80, 443) - Linux recommended for production; runs anywhere Node.js runs
# 1. Clone the repo
git clone https://github.com/SysopNetwork/BBSFirewall.git
cd BBSFirewall
# 2. Install dependencies
npm install
# 3. Create your config
cp .env.example .env
nano .env # set BACKEND_HOST, BACKEND_PORT at minimum
# 4. Start it
npm startnpm install -g pm2
pm2 start ecosystem.config.js
pm2 save
pm2 startup # follow the printed command to enable auto-start on bootPM2 commands:
pm2 start ecosystem.config.js # start
pm2 stop bbsfirewall # stop
pm2 restart bbsfirewall # restart
pm2 logs bbsfirewall # tail logs
pm2 list # statusnode update.js --check # see if a newer release exists — changes nothing
node update.js # check, confirm, download, apply, then offer to restart
node update.js --yes # skip the "apply this update?" confirmation (still asks about restart)
node update.js --rollback # list backups made by previous updates
node update.js --rollback pre-update-1.3.7-2026-09-22T12-00-00-000Z # restore oneEvery update backs up the current install first, downloads the tagged GitHub release,
overlays it, reinstalls dependencies, and re-validates your .env before restarting —
if anything in that chain fails, it automatically restores the pre-update backup so the
old version keeps running instead of a broken one. Your .env and list files (whitelist,
blocklist, trusted hosts, triggers) are never touched by an update.
The same thing is available from the web editor's Tools tab (an "Update now" button
next to GeoIP/SSH key/certs), and over the Management API
(GET /api/update/check, POST /api/update/apply, POST /api/update/rollback).
Self-update is Linux-only (it shells out to tar and pm2) — on another OS, or a
host missing tar, the check still works so you can see whether an update exists, but
applying one is refused; update that install manually (git pull or re-download +
npm install) instead. It also never runs on a schedule — always an explicit command or
button click.
All settings live in .env. Copy .env.example to get started — every option is documented there.
| Variable | Description | Default |
|---|---|---|
LISTEN_PORT | Port to listen on for incoming telnet connections | 23 |
BACKEND_HOST | Backend BBS server hostname or IP | 127.0.0.1 |
BACKEND_PORT | Backend BBS server port | 23 |
ENCODING_DETECTION | Enable automatic UTF-8/CP437 encoding detection | false |
BACKEND_PORT_CP437 | Backend port for CP437 (DOS/ANSI) clients | 2323 |
BACKEND_PORT_UTF8 | Backend port for UTF-8 (Unicode) clients | 2423 |
| Variable | Description | Default |
|---|---|---|
MAX_CONNECTIONS | Maximum total simultaneous connections | 100 |
MAX_CONNECTIONS_PER_IP | Max simultaneous connections from a single IP (0 = unlimited) | 0 |
CONNECTION_TIMEOUT | Idle connection timeout in milliseconds (0 to disable) | 300000 |
BACKEND_CONNECT_TIMEOUT_MS | Max wait for the backend TCP connection to establish (0 to disable) | 10000 |
| Variable | Description | Default |
|---|---|---|
BLOCKED_COUNTRIES | Comma-separated ISO country codes to block (e.g. CN,RU,KP) | (empty) |
BLOCK_UNKNOWN_COUNTRIES | Block connections with undetermined country | false |
| Variable | Description | Default |
|---|---|---|
WHITELIST_PATH | Path to IP whitelist file (bypasses all firewall rules) | (empty) |
BLOCKLIST_PATH | Path to IP blocklist file (permanent blocks) | (empty) |
| Variable | Description | Default |
|---|---|---|
RATE_LIMIT_ENABLED | Enable connection flood protection | true |
MAX_CONNECTIONS_PER_WINDOW | Max connection attempts per IP per time window | 10 |
RATE_LIMIT_WINDOW_MS | Time window in milliseconds | 60000 |
RATE_LIMIT_BLOCK_DURATION_MS | Temporary block duration in milliseconds | 300000 |
Scan the first bytes a telnet/SSH caller sends for known bot / scanner / exploit strings and blacklist the source IP on a match. Patterns are read from a list file — plain text is a case-insensitive substring, /regex/flags is a JS regex, and \xNN \r \n \t \0 escapes work in plain patterns (for TLS/binary probes). Also applies to SSH shell input and remote commands in terminate mode. Whitelisted IPs are exempt. Copy triggers.txt.example to triggers.txt to start.
| Variable | Description | Default |
|---|---|---|
TRIGGER_BLOCK_ENABLED | Enable auto-block on a trigger match | false |
TRIGGER_LIST_PATH | Path to the trigger pattern file | ./triggers.txt |
TRIGGER_SCAN_BYTES | Bytes of the caller's first data to scan (16–65536) | 1024 |
TRIGGER_BLOCK_MODE | blocklist = append to blocklist.txt (permanent); temp = in-memory block | blocklist |
TRIGGER_BLOCK_DURATION_MS | Block length for temp mode | 86400000 |
| Variable | Description | Default |
|---|---|---|
PROXY_PROTOCOL_ENABLED | Prepend real client IP to backend TCP stream | false |
| Variable | Description | Default |
|---|---|---|
WEB_REDIRECT_ENABLED | Redirect HTTP traffic on port 80 | false |
WEB_REDIRECT_URL | Destination URL for redirects (used by both HTTP and HTTPS) | (empty) |
| Variable | Description | Default |
|---|---|---|
HTTPS_REDIRECT_ENABLED | Redirect HTTPS traffic on port 443 | false |
HTTPS_REDIRECT_PORT | Port to listen on for HTTPS | 443 |
HTTPS_CERT_PATH | Path to TLS certificate (fullchain) | ./certs/fullchain.pem |
HTTPS_KEY_PATH | Path to TLS private key | ./certs/privkey.pem |
ACME_WEBROOT | Directory for Let's Encrypt challenge files | ./certs/webroot |
| Variable | Description | Default |
|---|---|---|
CONFIG_EDITOR_ENABLED | Enable the HTTPS web config editor | false |
CONFIG_EDITOR_PORT | Port the editor listens on (must differ from LISTEN_PORT / SSH port) | 8443 |
CONFIG_EDITOR_BIND | Address to bind the editor listener to | 0.0.0.0 |
CONFIG_EDITOR_CERT_PATH | Path to the editor's TLS certificate (fullchain) | ./certs/config-editor/fullchain.pem |
CONFIG_EDITOR_KEY_PATH | Path to the editor's TLS private key | ./certs/config-editor/privkey.pem |
CONFIG_EDITOR_TRUSTEDHOSTS_PATH | IPv4/IPv6 CIDR allowlist file — empty/missing denies all | ./trustedhosts.txt |
CONFIG_EDITOR_SESSION_TIMEOUT_MS | Idle session timeout (min 60000) | 1800000 |
CONFIG_EDITOR_PM2_APP | pm2 process name the Restart button acts on | bbsfirewall |
CONFIG_EDITOR_HTTP_REDIRECT_ENABLED | Answer plain-HTTP requests on the editor's own port with a 301 to https:// (same port, no extra listener) | true |
| Variable | Description | Default |
|---|---|---|
API_ENABLED | Enable the management API (requires CONFIG_EDITOR_ENABLED=true) | false |
API_KEY | Bearer key clients send; minimum 24 characters | (required when enabled) |
API_TRUSTEDHOSTS_PATH | Optional source-IP allowlist file; empty/missing = any IP (key still required) | ./api-trustedhosts.txt |
| Variable | Description | Default |
|---|---|---|
LOG_LEVEL | Console log level: debug, info, warn, error | info |
LOG_FILE_ENABLED | Write daily log files, one folder per service | true |
LOG_DIR | Folder for the log files | ./logs |
LOG_FILE_LEVEL | File log level: off, blocked, connections (also admin logins and changes), info, debug | connections |
LOG_RETENTION_DAYS | Delete log files older than this (1–3650) | 30 |
Each service can override the file level or switch its file off: TELNET_, SSH_, SSH_PASSTHROUGH_, WEB_ and CONFIG_EDITOR_ + LOG_LEVEL / LOG_ENABLED. See .env.example.
| Variable | Description | Default |
|---|---|---|
SSH_MODE | off, terminate (the firewall answers SSH and connects callers to the telnet backend) or passthrough (forwards the encrypted stream to a backend SSH server) | off |
SSH_LISTEN_PORT | Port callers use for SSH | 2222 |
SSH_HOST_KEY | Host private key file (terminate) | ./ssh_host_key |
SSH_CIPHERS | Comma-separated list of allowed SSH ciphers (terminate) | (see below) |
SSH_BACKEND_HOST | Backend SSH server (passthrough) | BACKEND_HOST |
SSH_BACKEND_PORT | Backend SSH port (passthrough) | 22 |
SSH_PROXY_PROTOCOL | Send a PROXY header to the backend SSH server — only if it understands it (passthrough) | false |
SSH_ENABLED=true from older versions still works and means SSH_MODE=terminate.
| Variable | Description | Default |
|---|---|---|
UFW_ENABLED | Let BBSFirewall manage its own tagged ufw rules (Preview / Apply in the Tools tab). Needs ufw installed and already turned on. | false |
HOST_ADMIN_SSH_PORT | The port you log in to the server on (not the BBS's SSH port). Required with UFW_ENABLED. | (empty) |
UFW_AUTO_APPLY | Apply rule changes automatically at startup and after each Save, instead of Preview + Apply | false |
UFW_PUSH_BLOCKS | Copy blocklist entries and trigger auto-blocks into ufw as kernel deny rules | false |
UFW_BLOCK_MAX_RULES | Most block rules to push (1–10000) | 1000 |
SSH_MODE picks what answers on the BBS's SSH port:
terminate— the firewall is the SSH server. It accepts any username and password and connects the caller to the BBS over telnet, so callers can use an SSH client even when the BBS has no SSH of its own. The BBS's own login screen is what protects accounts.passthrough— the firewall only filters by IP and forwards the encrypted SSH stream to a backend that runs its own SSH server, so public-key logins and SFTP keep working end to end.
Every caller goes through the same checks as telnet (blocklist, rate limit, connection caps, country blocking) the moment it connects.
Generate an SSH host key (only needed once — or use the Tools tab):
ssh-keygen -t rsa -b 4096 -f ssh_host_key -N "" -m PEMEnable in .env:
SSH_MODE=terminate
SSH_LISTEN_PORT=22
SSH_HOST_KEY=./ssh_host_keyConnect from any SSH client — any username and password works:
ssh yourserver.example.comThe firewall answers the BBS's telnet option negotiation itself, so callers never see stray negotiation bytes, and binary transfers such as Zmodem pass through intact. Callers must finish logging in within 2 minutes.
SSH_MODE=passthrough
SSH_LISTEN_PORT=22
SSH_BACKEND_HOST=127.0.0.1
SSH_BACKEND_PORT=2222
SSH_PROXY_PROTOCOL=false # true only if the backend SSH server reads PROXY headersIncludes both modern and legacy ciphers for old terminal clients:
aes128-gcm@openssh.com,aes256-gcm@openssh.comaes128-ctr,aes192-ctr,aes256-ctraes128-cbc,aes192-cbc,aes256-cbc3des-cbc(for very old clients)
On Linux, BBSFirewall can manage the host's own ufw rules, adding a kernel-level layer that keeps working while the firewall restarts. It is off by default and never turns ufw itself on or off.
-
Install ufw, allow the port you log in on, and turn it on yourself:
ufw allow 22/tcp # your admin SSH port ufw enable
-
Set
UFW_ENABLED=trueandHOST_ADMIN_SSH_PORT, restart, and open Tools → Host firewall (UFW). Preview shows what would change; Apply makes it so. The BBS ports are opened to everyone, while the config editor and your admin SSH port are limited to Trusted Hosts. Apply refuses any change that would leave your admin SSH port without an allow rule. BBSFirewall only touches rules it tagged (bbsfw); your own rules are left alone.With
UFW_AUTO_APPLY=truethe same changes are made automatically at startup and after each Save, so adding or removing a Trusted Host updates the rules right away. It is stricter than a manual Apply: if it can't tell which ports sshd listens on, or the change would remove more than half of the rules, it changes nothing and the Tools tab asks you to Preview and Apply yourself. -
Optionally set
UFW_PUSH_BLOCKS=trueto copy blocklist entries and trigger auto-blocks into ufw asdenyrules (taggedbbsfw-auto). Anything overlapping Trusted Hosts, the Whitelist or localhost is never pushed, and pushing is refused while Trusted Hosts is empty. Turning it off leaves the rules in place; remove them with Remove pushed rules in the same card.
A cloud provider's own network firewall sits in front of ufw and is invisible to it. If ufw says a port is open but callers still can't connect, check the provider's firewall too.
BBSFirewall can redirect web browsers that hit your firewall's IP address to your BBS website.
WEB_REDIRECT_ENABLED=true
WEB_REDIRECT_URL=https://yourbbs.example.comRequires a TLS certificate. Run setup-certs.sh to get a free one from Let's Encrypt (see below).
HTTPS_REDIRECT_ENABLED=true
HTTPS_REDIRECT_PORT=443
HTTPS_CERT_PATH=./certs/fullchain.pem
HTTPS_KEY_PATH=./certs/privkey.pemWEB_REDIRECT_URL is used as the destination for both HTTP and HTTPS redirects.
Note: On Linux, binding ports 80 and 443 requires root or the
CAP_NET_BIND_SERVICEcapability.
BBSFirewall includes setup-certs.sh to get a free TLS certificate from Let's Encrypt using certbot's webroot method. The HTTP server stays running the whole time — no downtime.
- Port 80 must be reachable from the internet
WEB_REDIRECT_ENABLED=truein your.env- BBSFirewall must be running before you run the script
- DNS must already point your domain to this server
bash setup-certs.sh yourdomain.com --email you@example.comThe script will:
- Install certbot if it's not already there
- Verify port 80 is listening
- Run certbot in webroot mode (no service interruption)
- Copy the certificates to
./certs/ - Install a renewal hook that auto-copies new certs and restarts BBSFirewall
- Print the
.envlines to add
bash setup-certs.sh yourdomain.com --renewRenewal is also automatic via certbot's built-in systemd timer — you don't have to think about it.
An HTTPS admin UI (default port 8443) for editing .env and the whitelist / blocklist / trustedhosts files from a browser instead of SSH. It runs in-process with the firewall on its own port and its own TLS certificate.
- Trusted-host allowlist —
trustedhosts.txt, one IPv4/IPv6 address or CIDR per line. Any address not matched is refused before the login page is even shown. An empty or missing file locks everyone out (fail-closed). - Login — a scrypt-hashed password (
node setup-admin.jscreates it in.admin-security.json, kept out of.env), checked with a constant-time compare and exchanged for a short-lived, IP-bound session cookie (HttpOnly,Secure,SameSite=Strict). Optional MFA (TOTP + one-time backup codes) can be turned on from Security Settings in the header. Failed logins/MFA codes lock the source IP for 15 minutes after 5 tries. Mutating requests also require a per-session CSRF token.
# 1. Certificate — Let's Encrypt for a public hostname...
bash setup-config-cert.sh admin.example.com --email you@example.com
# ...or self-signed for a bare IP / internal hostname
bash setup-config-cert.sh 95.182.86.146 --self-signed
# 2. Allowlist the IP(s) you will administer from
cp trustedhosts.txt.example trustedhosts.txt
$EDITOR trustedhosts.txt
# 3. Enable it in .env
CONFIG_EDITOR_ENABLED=true
CONFIG_EDITOR_PORT=8443
CONFIG_EDITOR_CERT_PATH=./certs/config-editor/fullchain.pem
CONFIG_EDITOR_KEY_PATH=./certs/config-editor/privkey.pem
CONFIG_EDITOR_TRUSTEDHOSTS_PATH=./trustedhosts.txt
# 4. Create the admin account (username/password no longer live in .env)
node setup-admin.js
# 5. Restart
pm2 restart bbsfirewallThen browse to https://your-host:8443/. Enable MFA (TOTP + backup codes) afterward
from Security Settings, under your username in the top-right of the header.
Expose the editor port directly. The trusted-host gate matches the real TCP peer address — it deliberately ignores
X-Forwarded-For. Behind a reverse proxy every request would appear to come from the proxy, breaking the gate; the editor logs a warning if it sees a forwarding header. For defence in depth, also restrict the port at the host firewall (ufw allow from <admin-ip> to any port 8443) or bind it to a private interface /127.0.0.1(SSH tunnel) withCONFIG_EDITOR_BIND.
- Settings — the whole
.env, grouped into collapsible sections with per-field help and a more toggle for longer explanations (e.g. SSH terminate vs passthrough). - Lists — the Whitelist / Blocklist / Trusted Hosts / Triggers / API Trusted Hosts editors, labeled by name, not filename, with "Add my IP" / "/24" / "/29" quick-add buttons on Whitelist and Blocklist.
- Tools — one-click download / update of the MaxMind GeoIP database (needs
MAXMIND_LICENSE_KEYsaved), generate an SSH host key for terminate mode, issue a Let's Encrypt certificate for the editor or the port-443 redirect (installs certbot if missing; console output shown on success or failure),, check for / apply updates or roll back to a backup (see Updating — Linux-only, restarts on success, automatic rollback on failure), and manage host firewall (UFW) rules. Updates, rollback and UFW are Global Admin only. - System Stats — live CPU %, load average, memory, process RSS, disk, bandwidth (current/avg/peak), BBSFirewall folder and log-file disk usage, per-interface network throughput, and firewall counters (active connections, accepted/rejected, blocklist size, temp-blocked IPs). Auto-refreshes every 4s.
- Logs — browse the per-proxy rotated log files written by
file-logger.js(LOG_FILE_ENABLED, on by default — see Retention below). Files are grouped by proxy type, with anything older than 30 days automatically folded into per-month sections so a long-running board's log list stays readable instead of scrolling forever. Search every file at once, open a file at any point (with Follow for today's file), download one file or all of them as a.zip, or delete one (Global Admin only). - Security Settings — under your username in the header: change your password, enable/disable MFA (TOTP QR code + one-time backup codes), regenerate backup codes, and whitelist your own IP.
- Save writes
.env(only when a setting actually changed) after a timestamped backup into anENVBACKUPS/folder next to.env(.env.bak.<ISO>, newest 15 kept), then validates the result in a fresh process. A save that only changes lists leaves.envalone and needs no restart. If validation fails the previous.envis restored and the errors are returned — the running config is never left broken. .envcomments and layout are preserved; disabling an optional field comments its line out; new keys are appended under a marked block.- Editing
trustedhosts.txtin the UI takes effect immediately (no restart)..envchanges need a restart — the Restart firewall button runspm2 restart, with a Stay signed in after restart checkbox: leave it checked and the page carries your session over and reconnects on its own; uncheck it to be logged out on restart. - Downloading/updating the GeoIP database and issuing the editor's own certificate both take effect live (no restart). Issuing the port-443 redirect certificate needs a restart.
- The
setup-config-cert.shLet's Encrypt mode installs its own renewal hook; renew manually withbash setup-config-cert.sh --renew.
A browser that visits http://your-host:8443 (plain HTTP on the editor's HTTPS port) would otherwise fail the TLS handshake with ERR_EMPTY_RESPONSE. With CONFIG_EDITOR_HTTP_REDIRECT_ENABLED=true (the default) the editor peeks the first byte of each connection: a TLS handshake is served normally, and a plain-HTTP request gets a 301 to https://<CONFIG_EDITOR_CERT_DOMAIN or the request's Host>:<port><path>. Same port, no extra listener. Set it to false to just drop such requests.
bash setup-config-cert.sh --renewEnable API_ENABLED=true and set API_KEY (min 24 chars) to expose the same operations as the editor UI over REST, on the same HTTPS port under /api/*. Meant for a remote dashboard. The config editor must be enabled — the API has no listener of its own.
Full reference, response shapes, and sample code (curl / Node / Python): API.md. A ready-to-run check is bundled as
api-smoke.sh.
Auth — every request carries the key, not a cookie:
Authorization: Bearer <API_KEY> # or: X-API-Key: <API_KEY>
The key is compared in constant time and never logged; 5 bad keys from one IP lock that IP out of the API for 15 minutes (its own counter — separate from the browser login). The API lane is independent of trustedhosts.txt — a caller does not need to be a trusted editor host — but you can narrow it by source IP with api-trustedhosts.txt (same file format; empty or missing = any IP, the key still applies). Edit it live from the api-trustedhosts.txt tab.
| Method + path | Does |
|---|---|
GET /api/config |
Full .env (grouped, with the current list-file contents and status.version) |
GET /api/health |
Cert/GeoIP/SSH host key health (Tools tab) |
GET /api/stats |
Live host + firewall metrics (CPU, memory, disk, network, connection counters) |
POST /api/save |
Write .env and/or the list files — same body the UI sends: {"env":{"KEY":{"value":"...","enabled":true}},"files":{"blocklist":"..."}}. Same backup → validate-in-child → restore-on-failure guard. |
POST /api/restart |
pm2 restart the firewall |
POST /api/geoip |
Download/update the MaxMind database ({"action":"download"|"update"}) |
POST /api/sshkey |
Generate the SSH host key ({"type":"rsa"|"ed25519","overwrite":false}) |
POST /api/cert |
Issue a Let's Encrypt certificate ({"target":"editor"|"redirect"}) |
GET /api/update/check |
Check GitHub for a newer release |
POST /api/update/apply |
Download and apply an update, then restart ({} or {"tag":"v1.4.0"}) |
POST /api/update/rollback |
Restore a previous update backup, then restart ({"backup":"pre-update-…"}) |
curl -s https://your-host:8443/api/stats -H "Authorization: Bearer $API_KEY"
curl -s https://your-host:8443/api/save -H "Authorization: Bearer $API_KEY" \
-H 'Content-Type: application/json' \
-d '{"env":{"BLOCKED_COUNTRIES":{"value":"CN,RU,KP","enabled":true}},"files":{}}'The API has the same power as the admin UI — including rewriting
.env(its ownAPI_KEY, the editor password, everything). A changedAPI_KEYor any.envmode change applies on the next restart, not immediately.
A lightweight GET /status on the config editor's HTTPS port, for monitoring tools like Uptime Kuma that just need a quick liveness check — no API key, no login. Requires CONFIG_EDITOR_ENABLED=true (it shares that listener; no port of its own).
curl -s https://your-host:8443/status{
"ok": true,
"version": "1.4.0",
"uptimeSec": 86412,
"listeners": { "telnet": true, "ssh": true, "webRedirect": true, "configEditor": true }
}ok reflects the telnet listener specifically (the app's core job); listeners gives a per-service breakdown if your monitor supports a JSON-path check (Uptime Kuma's "HTTP(s) - Json Query" monitor type, checking $.ok == true, for example). Values are read from the actual running servers, not just from .env flags, so a bind failure shows up as down.
Since there's no key or session to fall back on, status-trustedhosts.txt is fail-closed like trustedhosts.txt — an empty or missing file blocks everyone, including you. Add your monitoring host's IP before relying on this:
# status-trustedhosts.txt
203.0.113.10 # your Uptime Kuma server
Edit it live from the Lists tab ("Status Endpoint Trusted Hosts") or STATUS_TRUSTEDHOSTS_PATH in .env.
BBSFirewall can prepend a PROXY Protocol v1 header to every backend connection so the destination BBS can see the real client IP instead of the firewall's IP.
PROXY_PROTOCOL_ENABLED=trueWhen enabled, every connection to the backend starts with:
PROXY TCP4 203.0.113.45 192.0.2.1 56324 23
Fields: protocol, real client IP, proxy IP, client port, proxy port.
This works for both telnet and SSH connections.
⚠️ Important: The backend BBS software must support PROXY Protocol, or have a module/plugin that reads and strips the header before the BBS sees it. Enabling this against an incompatible backend will break all connections — the BBS will receive the header line as garbage data at the start of every session.
Compatible backends include HAProxy, Nginx, Synchronet, WWIV, Mystic, and any software with a PROXY Protocol module. Standard Major BBS v10 requires a companion MBBS module to handle the header.
Full spec: https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt
Block connections from specific countries using a local MaxMind GeoLite2 database. No external API calls — the lookup happens entirely on your server.
npm run setup-geoipFollow the on-screen instructions. A free MaxMind account is required at maxmind.com.
BLOCKED_COUNTRIES=CN,RU,KP,IR
BLOCK_UNKNOWN_COUNTRIES=falseUse ISO 3166-1 alpha-2 two-letter country codes.
IPs in the whitelist bypass all firewall rules — country blocking, rate limiting, blocklist, per-IP limits. Use this for trusted admin IPs.
cp whitelist.txt.example whitelist.txt
# Add your trusted IPs — one per line, CIDR ranges supportedWHITELIST_PATH=./whitelist.txtPermanently blocked IPs. Rejected immediately on connect regardless of any other setting.
cp blocklist.txt.example blocklist.txt
# Add IPs to block — one per line, CIDR ranges supportedBLOCKLIST_PATH=./blocklist.txtAutomatically blocks IPs that hammer the connection too frequently. Temporary blocks expire after RATE_LIMIT_BLOCK_DURATION_MS.
RATE_LIMIT_ENABLED=true
MAX_CONNECTIONS_PER_WINDOW=10
RATE_LIMIT_WINDOW_MS=60000
RATE_LIMIT_BLOCK_DURATION_MS=300000Every incoming connection goes through these checks in order:
| Step | Check | Action |
|---|---|---|
| 1 | ✅ Whitelist | Matched IPs are allowed immediately — all other checks skipped |
| 2 | 🚫 Blocklist | Permanent block |
| 3 | ⚡ Rate limit | Temporary block if threshold exceeded |
| 4 | 🔗 Per-IP connection limit | Reject if over MAX_CONNECTIONS_PER_IP |
| 5 | 🌍 GeoIP country check | Block if country is in BLOCKED_COUNTRIES |
| 6 | 🔀 Forward | Connect to backend BBS |
| 7 | 🎯 Trigger scan | While forwarding, scan the caller's first TRIGGER_SCAN_BYTES; blacklist + drop on a match |
BBSFirewall can detect whether a connecting SSH client prefers UTF-8 or CP437 and route them to separate backend ports. Useful if your BBS software runs separate instances for each encoding.
ENCODING_DETECTION=true
BACKEND_PORT_CP437=2323
BACKEND_PORT_UTF8=2423Detection for SSH clients is based on the client's LANG/LC_ALL environment variables and terminal type. Telnet connections always default to CP437 — there's no reliable way to detect encoding before the connection is established.
BBSFirewall/
├── server.js # Main entry point, connection manager, graceful shutdown
├── proxy.js # Bidirectional TCP proxy handler
├── ssh.js # SSH server — accepts any credentials, proxies to telnet
├── web-redirect.js # HTTP + HTTPS redirect server with ACME challenge support
├── config-editor.js # HTTPS web config editor (server, sessions, .env writer, tools, stats)
├── config-editor-ui.js # Config editor HTML views (login, MFA challenge, app shell)
├── security.js # Admin password (scrypt) + MFA (TOTP/backup codes) store
├── wordlist.js # Word list backing 4-word MFA backup codes
├── setup-admin.js # One-time admin account creation (node setup-admin.js)
├── disable-mfa.js # Emergency MFA disable (node disable-mfa.js --yes)
├── trustedhosts.js # IPv4/IPv6 CIDR allowlist matching for the config editor
├── telnet-filter.js # Telnet client used by SSH terminate mode toward the BBS
├── ufw.js # Host firewall (UFW) rule sync
├── ufw-blocks.js # Pushes blocked addresses into ufw
├── file-logger.js # Daily per-service log files, search, retention
├── zip-stream.js # Streams "download all logs" as a .zip
├── updater.js # Self-update and rollback engine
├── update.js # Command-line updater (node update.js)
├── metrics.js # Shared live counters read by the config editor's System Stats tab
├── proxy-protocol.js # PROXY Protocol v1 header builder
├── config.js # Configuration loading and validation
├── logger.js # Log level filtering
├── geoip.js # MaxMind GeoLite2 country lookup
├── ipfilter.js # IP lists, rate limiting, per-IP connection tracking
├── encoding-detector.js # UTF-8/CP437 detection logic
├── download-geoip.js # GeoIP database setup helper
├── setup-certs.sh # Let's Encrypt certificate setup script (web redirect)
├── setup-config-cert.sh # TLS cert setup for the config editor (LE or self-signed)
├── api-smoke.sh # Management API smoke test
├── ecosystem.config.js # PM2 process config
├── package.json
├── API.md # Management API reference + sample code
├── SECURITY.md # Security guide for sysops + how to report a vulnerability
├── CHANGELOG.md # Release notes
├── .env.example # Documented example configuration
├── whitelist.txt.example
├── blocklist.txt.example
├── trustedhosts.txt.example
├── triggers.txt.example
├── api-trustedhosts.txt.example
└── data/ # GeoIP database (not included, run setup-geoip)
BBSFirewall is developed and maintained by Mark Laudenbach at Sysop Network.
Built on the foundation of bbsfw by Ryan Fantus. Solid starting point — thanks for putting that together.
Running BBSFirewall on the open internet? Read the Security guide — how to lock down the admin editor, accounts and MFA, the API, SSH and PROXY Protocol, and the server itself. Found a vulnerability? Please report it privately as described there, not in a public issue.
- Cash App: $laudenbachm
- Ko-fi: ko-fi.com/laudenbachm
MIT — Copyright (c) 2026 Sysop Network