██████╗ ██████╗ ███████╗ ███████╗██╗██████╗ ███████╗██╗ ██╗ █████╗ ██╗ ██╗
██╔══██╗██╔══██╗██╔════╝ ██╔════╝██║██╔══██╗██╔════╝██║ ██║██╔══██╗██║ ██║
██████╔╝██████╔╝███████╗ █████╗ ██║██████╔╝█████╗ ██║ █╗ ██║███████║██║ ██║
██╔══██╗██╔══██╗╚════██║ ██╔══╝ ██║██╔══██╗██╔══╝ ██║███╗██║██╔══██║██║ ██║
██████╔╝██████╔╝███████║ ██║ ██║██║ ██║███████╗╚███╔███╔╝██║ ██║███████╗███████╗
╚═════╝ ╚═════╝ ╚══════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝ ╚══╝╚══╝ ╚═╝ ╚═╝╚══════╝╚══════╝
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 Server | Encrypted SSH access on any port, proxied to the telnet backend |
| 🌐 | 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 |
| 📜 | Log Viewer | Browse, view, and delete per-proxy log files from the browser — automatically grouped by type, with anything older than 30 days folded into per-month sections |
| 📊 | 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 # statusAll 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 | Log level: debug, info, warn, error | info |
| Variable | Description | Default |
|---|---|---|
SSH_ENABLED | Enable the SSH server | false |
SSH_LISTEN_PORT | Port to listen on for SSH connections | 2222 |
SSH_HOST_KEY | Path to SSH host private key file | ./ssh_host_key |
SSH_CIPHERS | Comma-separated list of allowed SSH ciphers | (see below) |
BBSFirewall includes an optional SSH server that accepts any username and password and proxies the session to the backend BBS via telnet. This lets users connect with a modern SSH client instead of a raw telnet client.
Generate an SSH host key (only needed once):
ssh-keygen -t rsa -b 4096 -f ssh_host_key -N "" -m PEMEnable in .env:
SSH_ENABLED=true
SSH_LISTEN_PORT=22
SSH_HOST_KEY=./ssh_host_keyConnect from any SSH client — any username and password works:
ssh yourserver.example.comIncludes 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)
Note: Binary file transfers (Zmodem, Ymodem, etc.) do not work reliably over SSH due to PTY character processing. Use the telnet connection for file transfers and SSH for interactive browsing.
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, and 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). - 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. View a file's content (large files are tailed to the last 512 KB) or permanently delete one. - 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
.envafter a timestamped backup into anENVBACKUPS/folder next to.env(.env.bak.<ISO>, newest 15 kept), then validates the result in a fresh process. 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"}) |
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.
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 MajorBBS/Worldgroup 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
├── 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
├── .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.
MIT — Copyright (c) 2026 Sysop Network