A lightweight, secure HTTP tunneling system for exposing local applications to the internet
Boring Machine is a WebSocket-based HTTP tunnel that allows you to securely expose local services running behind firewalls or NAT to the public internet.
- HTTP/HTTPS Tunneling - Forward public requests to local applications
- User Authentication - Built-in user registration and token-based auth
- WebSocket Protocol - Efficient binary communication via encoding/gob
- Admin Dashboard - Real-time metrics and monitoring
- TLS/WSS Support - Secure communications with HTTPS/WSS
- SQLite Database - Embedded database with zero configuration
- Custom Error Pages - Branded error pages with diagnostics
- System Monitoring - CPU, memory, and connection metrics
- Graceful Shutdown - Proper cleanup with configurable timeouts
- Development Mode - Skip authentication for quick testing
- Quick Start
- Installation
- Usage
- Authentication
- Configuration
- Architecture
- Admin Dashboard
- TLS/HTTPS Setup
- Development
Terminal 1 - Start Server:
make dev-server
# Server listens on http://localhost:8443Terminal 2 - Start Client:
# Assuming local app running on http://localhost:3000
make dev-client
# Output:
# ďż˝ Connected to localhost:8443
# ďż˝ Client ID: a1b2c3d4e5f6g7h8
# ďż˝ Forwarding requests to 127.0.0.1:3000
# ďż˝ Public URL: http://a1b2c3d4e5f6g7h8.localhost:8443Terminal 3 - Test the tunnel:
curl http://a1b2c3d4e5f6g7h8.localhost:8443/api/users
# Request tunnels through WebSocket to your local app!- Go 1.24.5 or higher
- Make (optional, for convenience)
# Clone the repository
git clone https://github.com/imrraaj/boring-machine.git
cd boring-machine
# Build both binaries
make
# Or build individually
make client # Creates bin/brc
make server # Creates bin/brsAfter building, you'll find two binaries in bin/:
brc- Boring Machine Clientbrs- Boring Machine Server
You can move these to your $PATH for convenience:
sudo cp bin/brs /usr/local/bin/
sudo cp bin/brc /usr/local/bin/The server accepts WebSocket connections from clients and routes HTTP requests to them.
./bin/brs [flags]| Flag | Default | Description |
|---|---|---|
-port |
:8443 |
HTTP/HTTPS listening port |
-db |
boringmachine.db |
SQLite database file path |
-skip-auth |
false |
Disable authentication (dev/benchmark only) |
-verbose |
false |
Enable verbose/debug logging |
-cert-file |
"" |
Path to TLS certificate (enables HTTPS) |
-key-file |
"" |
Path to TLS private key (enables HTTPS) |
-read_timeout |
10s |
HTTP read timeout |
-write_timeout |
10s |
HTTP write timeout |
-tunnel_timeout |
30s |
Timeout for tunnel responses |
Production server with authentication:
./bin/brs \
-port=:8443 \
-db=/var/lib/boring-machine/data.dbHTTPS server with TLS:
./bin/brs \
-port=:443 \
-cert-file=/etc/boring-machine/cert.pem \
-key-file=/etc/boring-machine/key.pemDevelopment server (skip auth, verbose):
./bin/brs -skip-auth -verbose| Endpoint | Method | Purpose |
|---|---|---|
/auth/register |
POST | Register new user |
/auth/login |
POST | Login and get token |
/auth/rotate |
POST | Rotate authentication token |
/tunnel/ws |
WebSocket | Client tunnel connection |
/admin/dashboard |
GET | Admin metrics dashboard |
/admin/api/metrics |
GET | Metrics JSON API |
/* |
Any | HTTP tunnel proxy |
The client connects to the server and forwards requests to your local application.
./bin/brc <command> [flags]| Flag | Description |
|---|---|
--verbose |
Enable verbose/debug logging |
Register a new account:
./bin/brc auth register [--server URL]
# Interactive prompts:
# Enter username: alice
# Enter email: alice@example.com
# Enter password: ********
# Registration successful
# Token saved to ~/.boring-client/credentialsLogin to existing account:
./bin/brc auth login [--server URL]
# Interactive prompts:
# Enter username: alice
# Enter password: ********
# Login successful
# Token saved to ~/.boring-client/credentialsRotate authentication token:
./bin/brc auth rotate [--server URL]
# Token rotated successfully
# New token saved to ~/.boring-client/credentialsAuth flags:
--server: Server URL (default:http://localhost:8443)
Start tunnel to local application:
./bin/brc tunnel [flags]Tunnel Flags:
| Flag | Default | Description |
|---|---|---|
--server |
localhost:8443 |
Server address |
--network |
127.0.0.1 |
Local application network |
--port |
3000 |
Local application port |
--secure |
false |
Use WSS (secure WebSocket) |
--skip-auth |
false |
Skip authentication |
--token |
"" |
Override token from credentials file |
Tunnel to local app on port 3000:
./bin/brc tunnel
# Uses stored credentials from ~/.boring-client/credentialsTunnel with specific port:
./bin/brc tunnel --port 8080Tunnel with secure WebSocket:
./bin/brc tunnel \
--server example.com:443 \
--secure \
--port 3000Tunnel with manual token:
./bin/brc tunnel \
--server example.com:8443 \
--token abc123def456...Development mode (skip auth):
./bin/brc tunnel --skip-auth --verboseOnce connected, your local application is accessible at:
http://{client-id}.{server-hostname}:{port}
Example:
http://a1b2c3d4e5f6g7h8.localhost:8443
The client ID is automatically generated (8-byte hex) and displayed when you connect.
Boring Machine uses token-based authentication with bcrypt password hashing.
- User registers with username, email, and password
- Password is hashed with bcrypt (cost 12)
- User receives a 96-character hex token
- Token is valid for 90 days
- Token is stored in
~/.boring-client/credentials
Credentials are stored in JSON format at ~/.boring-client/credentials:
{
"token": "abc123def456...",
"username": "alice",
"expires_at": "2026-03-15T10:30:00Z"
}File permissions are automatically set to 0600 (readable only by owner).
When the client connects:
- Token is sent in the WebSocket registration message
- Server validates token exists and hasn't expired
- Server associates the tunnel with the user ID
last_used_attimestamp is updated
Rotate your token to invalidate the old one and get a fresh 90-day token:
./bin/brc auth rotate --server https://example.comCreate a .env file in the project root:
# Database path (server)
DATABASE_PATH=/var/lib/boring-machine/data.dbThe .env file is automatically loaded by both server and client.
Boring Machine uses SQLite for user and token storage.
Schema:
userstable: username, email, password_hashauth_tokenstable: token, user_id, expires_at, last_used_at
Features:
- Automatic schema initialization on first run
- Foreign key constraints enabled
- Indexes on frequently queried columns
- Connection pooling optimized for SQLite (max 1 connection)
Location:
- Default:
boringmachine.db(current directory) - Override with
-dbflag orDATABASE_PATHenv var
+------------------+
| External User |
+--------+---------+
| HTTP
v
+------------------+
| Server | * Assigns client-id.hostname.com
| (Public IP) |
+--------+---------+
| WebSocket (binary protocol)
v
+------------------+
| Client | * Runs on local machine
| (Behind NAT) |
+--------+---------+
| HTTP
v
+------------------+
| Local App | * localhost:3000
| (localhost) |
+------------------+
-
Client Registration:
- Client connects via WebSocket to
/tunnel/ws - Sends
ClientRegisterwith auth token - Server validates and assigns unique client ID
- Server responds with
RegistrationResponse
- Client connects via WebSocket to
-
HTTP Request Flow:
- External user makes HTTP request to
{client-id}.server.com - Server looks up client by ID from hostname
- Server creates
TunnelRequestwith request details - Request is gob-encoded and sent via WebSocket
- Client decodes request and forwards to local app
- Client receives HTTP response from local app
- Client creates
TunnelResponsewith response details - Response is gob-encoded and sent back via WebSocket
- Server decodes and writes HTTP response to external user
- External user makes HTTP request to
-
Keepalive:
- Server sends WebSocket ping every 300 seconds
- Client must respond with pong to maintain connection
- Missed pongs result in connection closure
// Client -> Server (registration)
type ClientRegister struct {
Token string
}
// Server -> Client (registration response)
type RegistrationResponse struct {
Success bool
ClientID string
Error string
}
// Server -> Client (HTTP request)
type TunnelRequest struct {
RequestID string // Unique ID for matching
Method string // HTTP method
URL string // Request URL
Headers http.Header // HTTP headers
Body []byte // Request body
}
// Client -> Server (HTTP response)
type TunnelResponse struct {
RequestID string // Matches request ID
StatusCode int // HTTP status
Headers http.Header // Response headers
Body []byte // Response body
}Access real-time metrics and monitoring at:
http://your-server:8443/admin/dashboard
- Server Status: Uptime, protocol (HTTP/HTTPS), start time
- Connection Metrics: Active tunnels, total connections
- Request Metrics: Forwarded requests, failed requests, success rate
- System Resources: CPU usage, memory usage, goroutines
- Client Details: Client ID, user ID, IP address, request count, last activity
- Auto-refresh: Updates every 5 seconds
Get raw metrics in JSON format:
curl http://your-server:8443/admin/api/metricsResponse includes:
{
"server": {
"uptime_seconds": 3600,
"start_time": "2025-12-13T10:00:00Z"
},
"connections": {
"active": 5,
"total_accepted": 150,
"total_closed": 145
},
"requests": {
"forwarded": 12500,
"failed": 23
},
"clients": [...],
"system": {
"cpu_percent": 15.2,
"memory_used_mb": 45.3,
"memory_total_mb": 8192,
"memory_percent": 0.55,
"goroutines": 42,
"cpu_cores": 8
}
}For development/testing:
make certs
# Certificates created in certs/For production with Let's Encrypt or commercial certs:
./bin/brs \
-port=:443 \
-cert-file=/etc/letsencrypt/live/example.com/fullchain.pem \
-key-file=/etc/letsencrypt/live/example.com/privkey.pemWhen server uses HTTPS, client must use secure WebSocket:
./bin/brc tunnel --server example.com:443 --secureNote: Both -cert-file and -key-file must be provided. The server will reject configuration with only one.
make # Build both client and server
make client # Build client only
make server # Build server only
make clean # Remove build artifacts
make certs # Generate TLS certificates
make dev-server # Run server (skip-auth, verbose)
make dev-client # Run client (skip-auth, verbose)Key dependencies managed in go.mod:
github.com/gorilla/websocket- WebSocket librarygolang.org/x/crypto- Bcrypt password hashinggolang.org/x/term- Terminal password inputmodernc.org/sqlite- Pure Go SQLite driver
Start a local test server (runs on port 5664):
go run cmd/benchmark/test_server.goThen benchmark the tunnel:
# Terminal 1: Start boring-machine server
./bin/brs -skip-auth
# Terminal 2: Start client tunneling to test server
./bin/brc tunnel --skip-auth --port 5664
# Terminal 3: Benchmark
wrk -t12 -c400 -d30s http://{client-id}.localhost:8443/api/usersTypical performance metrics (tested with wrk):
- Throughput: ~7,500 requests/second
- Latency: ~50-60ms average (includes tunnel overhead)
- Concurrent Connections: Tested with 400+ concurrent connections
- Memory Usage: ~40-50MB per server instance
Performance characteristics:
- WebSocket binary protocol using
encoding/gob - Connection pooling for local HTTP requests
- Concurrent request handling with goroutines
- Efficient request/response matching via maps
- Authentication: Always use authentication in production (
-skip-authis for development only) - TLS/HTTPS: Use HTTPS in production to encrypt WebSocket traffic
- Token Storage: Credentials file is created with 0600 permissions
- Password Hashing: Bcrypt with cost factor 12
- Token Expiry: 90-day expiration with rotation support
- Database: Foreign key constraints prevent orphaned tokens
This project is licensed under the MIT License.
Contributions are welcome! Please:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
For issues, questions, or contributions, please open an issue on GitHub.