A lightweight TCP proxy firewall for BBS telnet connections, built with Node.js.
- TCP Proxy: Forwards telnet connections to an internal BBS server
- SSH Server: Built-in SSH server that proxies to telnet backend (accepts any credentials)
- Note: Use telnet for binary file transfers (Zmodem, etc.); SSH is best for browsing
- Encoding Detection: Automatically detects UTF-8 vs CP437 and routes to appropriate backend
- Connection Management: Tracks active connections and enforces limits
- Logging: Detailed connection and traffic logging
- Configurable: Easy configuration via environment variables
- Graceful Shutdown: Handles SIGTERM/SIGINT for clean shutdowns
- Legacy Cipher Support: Configurable SSH ciphers for old BBS clients
- Few External Dependencies: Uses only Node.js built-in modules and minimal packages
- Clone or download this repository
- Ensure Node.js 14+ is installed
- Install dependencies:
npm install- (Optional) Set up GeoIP database for country blocking:
npm run setup-geoipFollow the on-screen instructions to download the MaxMind GeoLite2 Country database. You'll need to sign up for a free account at https://dev.maxmind.com/geoip/geolite2-free-geolocation-data
- Copy
.env.exampleto.envand configure as needed
cp .env.example .envConfigure the firewall by setting environment variables or editing .env:
| Variable | Description | Default |
|---|---|---|
LISTEN_PORT |
Port to listen on for incoming telnet connections | 2323 |
BACKEND_HOST |
Backend BBS server hostname/IP (use 127.0.0.1 for IPv4) | 127.0.0.1 |
BACKEND_PORT |
Backend BBS server port | 23 |
ENCODING_DETECTION |
Enable automatic UTF-8/CP437 encoding detection and routing | false |
BACKEND_PORT_CP437 |
Backend port for CP437 (DOS/ANSI) clients | 2323 |
BACKEND_PORT_UTF8 |
Backend port for UTF-8 (Unicode) clients | 2423 |
MAX_CONNECTIONS |
Maximum simultaneous connections | 100 |
CONNECTION_TIMEOUT |
Connection timeout in milliseconds (0 to disable) | 300000 (5 min) |
SSH_ENABLED |
Enable SSH server | false |
SSH_LISTEN_PORT |
Port to listen on for incoming 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) |
BLOCKED_COUNTRIES |
Comma-separated ISO country codes to block (e.g., CN,RU,KP) | (empty) |
BLOCK_UNKNOWN_COUNTRIES |
Block connections when country cannot be determined | false |
WHITELIST_PATH |
Path to IP whitelist file (exempt from all firewall rules) | (empty) |
BLOCKLIST_PATH |
Path to IP blocklist file | (empty) |
RATE_LIMIT_ENABLED |
Enable connection flood protection | true |
MAX_CONNECTIONS_PER_WINDOW |
Max connections per IP within time window | 10 |
RATE_LIMIT_WINDOW_MS |
Time window for rate limiting in milliseconds | 60000 (1 min) |
RATE_LIMIT_BLOCK_DURATION_MS |
How long to block IPs that exceed rate limit (ms) | 300000 (5 min) |
LOG_LEVEL |
Logging level: debug, info, warn, error | info |
npm startOr with custom configuration:
LISTEN_PORT=2323 BACKEND_HOST=192.168.1.100 BACKEND_PORT=23 npm startUse any telnet client to connect:
telnet localhost 2323The connection will be forwarded to your configured backend server.
bbsfw can automatically detect whether a client supports UTF-8 (Unicode) or CP437 (DOS/ANSI) and route them to different backend ports. This allows you to run separate BBS instances optimized for each encoding.
- Default: All connections go to CP437 backend (port 2323 by default)
- SSH Connections: Detects UTF-8 from environment variables (
LANG,LC_ALL) and terminal type - Telnet Connections: Uses default CP437 (terminal type detection is unreliable for telnet)
- Enable encoding detection in your
.envfile:
ENCODING_DETECTION=true
BACKEND_PORT_CP437=2323
BACKEND_PORT_UTF8=2423-
Configure your backend BBS instances:
- Run one BBS instance on port 2323 configured for CP437/DOS
- Run another instance on port 2423 configured for UTF-8/Unicode
-
Restart the firewall:
npm startFor SSH connections, encoding is detected from:
-
Environment variables (highest priority):
LANGcontaining "UTF-8" or "UTF8" → routes to UTF-8 backendLC_ALLcontaining "UTF-8" or "UTF8" → routes to UTF-8 backendLC_CTYPEcontaining "UTF-8" or "UTF8" → routes to UTF-8 backend
-
Terminal type (fallback):
- Modern terminals (
xterm,screen,linux, etc.) → UTF-8 - DOS/ANSI terminals (
ansi,pcansi,scoansi) → CP437
- Modern terminals (
-
Default: If no indicators found → CP437
For Telnet connections:
- Always uses CP437 backend (default)
- Terminal type detection is unreliable over telnet
# User connects via SSH with LANG=en_US.UTF-8
# bbsfw detects UTF-8 and routes to port 2423
# User connects via SSH with no LANG set
# bbsfw defaults to CP437 and routes to port 2323
# User connects via telnet
# bbsfw uses CP437 and routes to port 2323When encoding detection is enabled, you'll see log messages like:
[INFO] Detected UTF-8 encoding from SSH environment for ::ffff:192.168.1.100
[INFO] SSH client ::ffff:192.168.1.100 using backend port 2423 for encoding: utf8
bbsfw includes an optional SSH server that allows users to connect via SSH instead of raw telnet. The SSH server accepts any username and password combination and immediately proxies the connection to your backend BBS via telnet.
- Encrypted connections: All traffic between client and firewall is encrypted
- Legacy client support: Many old BBS terminal programs support SSH with older ciphers
- Drop-in replacement: Users can connect via SSH without changes to your backend BBS
- Best for browsing: Ideal for reading messages, viewing content, and interactive BBS use
- Generate an SSH host key:
ssh-keygen -t rsa -b 4096 -f ssh_host_key -N "" -m PEMThis creates a private key file (ssh_host_key) in PEM format that the SSH server will use.
Note: The -m PEM flag is important - it generates the key in the traditional PEM format which is compatible with the ssh2 library. If you already have a key in OpenSSH format, you can convert it:
ssh-keygen -p -m PEM -f ssh_host_key -N ""- Enable SSH in your
.envfile:
SSH_ENABLED=true
SSH_LISTEN_PORT=2222
SSH_HOST_KEY=./ssh_host_key- Start the firewall:
npm startThe SSH server will start alongside the telnet proxy.
Users can connect with any SSH client:
ssh -p 2222 [email protected]When prompted for a password, they can enter anything - all credentials are accepted.
Many older BBS terminal programs (like SyncTERM) only support older SSH ciphers. bbsfw includes these by default, but you can customize the cipher list:
Default ciphers:
[email protected][email protected]aes128-ctr,aes192-ctr,aes256-ctraes128-cbc,aes192-cbc,aes256-cbc3des-cbc(for very old clients)
Custom cipher configuration:
# In .env file
SSH_CIPHERS=aes128-ctr,aes256-ctr,aes128-cbc,3des-cbcSeparate ciphers with commas. Order matters - the first matching cipher will be used.
- Client connects to SSH server on
SSH_LISTEN_PORT - SSH handshake occurs (encryption negotiation)
- Client authenticates with any username/password (all accepted)
- Shell session is established
- SSH server opens a telnet connection to
BACKEND_HOST:BACKEND_PORT - All data is proxied bidirectionally:
- Client ↔ SSH (encrypted) ↔ bbsfw ↔ Telnet (unencrypted) ↔ Backend BBS
- All firewall rules apply (country blocking, rate limiting, IP filtering)
- No authentication: The SSH server accepts any credentials. Security comes from IP filtering, country blocking, and rate limiting.
- Backend connection is unencrypted: The connection from bbsfw to your backend BBS is still telnet (unencrypted). Only the client-to-firewall connection is encrypted.
- Host key verification: Clients will see a host key fingerprint on first connection. They should verify this matches your server.
Binary File Transfers (Zmodem, Ymodem, etc.)
Due to SSH PTY (pseudo-terminal) character processing, binary file transfer protocols like Zmodem may not work reliably over SSH connections. The SSH protocol performs terminal emulation which can modify or corrupt binary data streams, resulting in CRC errors and failed transfers.
Workaround: Use the telnet connection for file transfers. This is a known limitation of SSH PTY mode and affects most SSH-based BBS proxy implementations.
Recommended workflow:
- Connect via SSH for regular BBS browsing (encrypted, more secure)
- When you need to download/upload files, switch to telnet connection
- Return to SSH after the file transfer is complete
This limitation is inherent to how SSH handles terminal sessions and cannot be fully resolved without using alternative file transfer methods (such as SFTP, which would require a different server implementation).
Test your SSH server:
# Connect with verbose output
ssh -v -p 2222 test@localhost
# Test with a specific cipher
ssh -c aes128-cbc -p 2222 test@localhostBlock connections from specific countries using GeoIP lookup:
- Download the GeoIP database:
npm run setup-geoip- Configure blocked countries in your environment:
# Block China, Russia, and North Korea
BLOCKED_COUNTRIES=CN,RU,KP npm startOr add to your .env file:
BLOCKED_COUNTRIES=CN,RU,KP
BLOCK_UNKNOWN_COUNTRIES=false
Use ISO 3166-1 alpha-2 country codes (2 letters). Common examples:
CN- ChinaRU- RussiaKP- North KoreaIR- IranUS- United StatesGB- United KingdomDE- GermanyJP- Japan
See full list: https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2
- When a connection is received, the client's IP address is looked up in the local GeoLite2 database
- If the country is in the blocked list, the connection is immediately rejected
- If the country cannot be determined and
BLOCK_UNKNOWN_COUNTRIES=true, the connection is rejected - All blocking events are logged for monitoring
The GeoLite2 database is updated monthly by MaxMind. To update:
- Delete the old database:
rm data/GeoLite2-Country.mmdb - Re-run setup:
npm run setup-geoip
- Lookups are performed against a local database (no API calls)
- Typical lookup time: < 1ms
- Database size: ~6MB in memory
- No external dependencies or network latency
Protect your BBS from connection floods and block specific IP addresses.
Built-in rate limiting automatically blocks IPs that connect too frequently:
# Default: 10 connections per minute
npm start
# Custom settings: 5 connections per 30 seconds, block for 10 minutes
MAX_CONNECTIONS_PER_WINDOW=5 RATE_LIMIT_WINDOW_MS=30000 RATE_LIMIT_BLOCK_DURATION_MS=600000 npm startOr add to your .env file:
RATE_LIMIT_ENABLED=true
MAX_CONNECTIONS_PER_WINDOW=10
RATE_LIMIT_WINDOW_MS=60000
RATE_LIMIT_BLOCK_DURATION_MS=300000
How it works:
- Tracks connection attempts per IP address
- If an IP exceeds
MAX_CONNECTIONS_PER_WINDOWwithinRATE_LIMIT_WINDOW_MS, it's temporarily blocked - Blocked IPs are automatically unblocked after
RATE_LIMIT_BLOCK_DURATION_MS - All blocks are logged for monitoring
To disable rate limiting:
RATE_LIMIT_ENABLED=false npm startWhitelist specific IP addresses or ranges to bypass all firewall rules (country blocking, rate limiting, and blocklist):
- Create a whitelist file:
cp whitelist.txt.example whitelist.txt- Add IPs or CIDR ranges to whitelist (one per line):
# whitelist.txt
192.168.1.100 # Single trusted IP
10.0.0.0/8 # Entire private network
172.16.0.0/12 # Another private range
- Enable the whitelist:
WHITELIST_PATH=whitelist.txt npm startWhitelist features:
- IPs in the whitelist bypass ALL firewall rules
- Supports single IPs (e.g.,
192.168.1.100) - Supports CIDR ranges (e.g.,
192.168.1.0/24,10.0.0.0/8) - Comments supported (lines starting with #)
- IPv4 and IPv6-mapped IPv4 addresses supported
- Changes require restart to take effect
Use cases:
- Allow connections from trusted networks or IPs
- Exempt monitoring systems from rate limiting
- Allow administrative access regardless of country
Block specific IP addresses or ranges from a file:
- Create a blocklist file:
cp blocklist.txt.example blocklist.txt- Add IPs or CIDR ranges to block (one per line):
# blocklist.txt
192.168.1.100 # Single IP
10.0.0.0/24 # CIDR range
203.0.113.0
# IPv6 also supported
2001:0db8:85a3::8a2e:0370:7334
- Enable the blocklist:
BLOCKLIST_PATH=blocklist.txt npm startBlocklist features:
- One IP or CIDR range per line
- Supports CIDR notation (e.g.,
192.168.1.0/24) - Comments supported (lines starting with #)
- IPv4 and IPv6 addresses
- Changes require restart to take effect
- Permanent blocking (not temporary like rate limiting)
Use all protection methods together:
# In .env file
WHITELIST_PATH=whitelist.txt
BLOCKLIST_PATH=blocklist.txt
BLOCKED_COUNTRIES=CN,RU,KP
RATE_LIMIT_ENABLED=true
MAX_CONNECTIONS_PER_WINDOW=10
RATE_LIMIT_WINDOW_MS=60000Processing order:
- Whitelist check - If matched, allow immediately (skip all other checks)
- IP blocklist check (permanent block)
- Rate limit check (temporary block)
- Country check (if GeoIP enabled)
- If all pass, connection forwarded to BBS
The firewall consists of several modules:
- server.js: Main server logic and connection management
- proxy.js: Handles bidirectional TCP proxy connections
- ssh.js: SSH server implementation with credential bypass
- config.js: Configuration management and validation
- logger.js: Logging utility with configurable levels
- geoip.js: GeoIP database integration for country lookups
- ipfilter.js: IP blocklist and rate limiting module
- encoding-detector.js: UTF-8/CP437 encoding detection for smart routing
- download-geoip.js: Helper script to download GeoLite2 database
- The firewall listens on
LISTEN_PORTfor telnet and optionally onSSH_LISTEN_PORTfor SSH - When a client connects (via telnet or SSH), the IP is checked in this order:
- Whitelist (if matched, skip all other checks)
- IP blocklist (permanent block)
- Rate limiting (temporary block for floods)
- GeoIP country check (if enabled)
- If any check fails, the connection is rejected immediately
- For SSH connections, any username/password is accepted (no authentication)
- If all checks pass, a connection is established to
BACKEND_HOST:BACKEND_PORT - Data is forwarded bidirectionally between client and backend
- All connections are logged with traffic statistics and filtering decisions
- Connections are tracked and limited by
MAX_CONNECTIONS
✅ TCP Proxy: Forwards telnet connections to backend BBS server
✅ SSH Server: Optional encrypted SSH access (accepts any credentials)
⚠️ Note: Telnet recommended for binary file transfers; SSH best for browsing
✅ Encoding Detection: Automatic UTF-8/CP437 detection routes clients to appropriate backends
✅ Legacy Cipher Support: Configurable SSH ciphers for old BBS clients
✅ IP Whitelist: Always allow specific IPs/ranges (bypass all firewall rules)
✅ Country Blocking: Block connections from specific countries using local GeoIP database
✅ IP Blocklist: Block specific IP addresses/ranges from a file (supports CIDR)
✅ Rate Limiting: Automatic flood protection with temporary blocking
✅ Connection Management: Track and limit simultaneous connections
✅ Logging: Detailed logging with configurable levels and filtering decisions
✅ Performance: Local database lookups, no external API calls
✅ Minimal Dependencies: Only Node.js built-ins plus ssh2 and maxmind
Planned features for future releases:
- Connection statistics and monitoring dashboard
- HTTP API for management and dynamic blocklist/whitelist updates
- Configuration reload without restart (live reload)
- IPv6 support improvements
- Custom ban messages/responses
bbsfw/
├── server.js # Main entry point
├── proxy.js # Proxy connection handler
├── ssh.js # SSH server module
├── config.js # Configuration management
├── logger.js # Logging utility
├── geoip.js # GeoIP lookup module
├── ipfilter.js # IP blocklist and rate limiting
├── encoding-detector.js # UTF-8/CP437 encoding detection
├── download-geoip.js # Database download helper
├── package.json # Project metadata
├── .env.example # Example configuration
├── whitelist.txt.example # Example IP whitelist
├── blocklist.txt.example # Example IP blocklist
├── data/ # GeoIP database directory
└── README.md # Documentation
npm run devTo test the firewall, you'll need:
- A backend telnet server running
- Configure bbsfw to point to it
- Start bbsfw
- Connect with a telnet client
MIT