Gunnel
A modern, lightweight, and secure tunneling solution written in Go.

Overview
Gunnel is a tunneling solution designed to securely expose your local services to the internet. It supports both HTTP and TCP protocols, offering features like connection pooling, automatic reconnection, and load balancing. Built on top of the QUIC (HTTP/3) protocol, it ensures modern security and high performance.
Features
- Secure tunneling with QUIC (HTTP/3) protocol
- Automatic reconnection with exponential backoff
- Load balancing across multiple connections
- Support for both HTTP and TCP protocols
- High performance with minimal overhead
- Detailed logging using logrus
- Modern and user-friendly CLI interface
- Reliable connection handshake with ready state confirmation
TODO
- Remove insecure TLS configurations
- Implement a improved dashboard for monitoring connections and performance
- Add metrics
- Add support for subdomain generation for dynamic tunnels
- Support TCP tunneling with custom ports
Architecture
sequenceDiagram
participant D as Local App
participant A as Client
participant B as Server
participant C as User
A->>B: 1. Register (QUIC)
B->>A: 2. Confirm
C->>B: 3. Request
B->>A: 4. Begin Connection
A->>D: 5. Connect to Local Service
A->>B: 6. Connection Ready
B->>A: 7. Forward Request Data
A->>D: 8. Local Service
D->>A: 9. Response
A->>B: 10. Forward
B->>C: 11. Response
Installation
go install github.com/snakeice/gunnel@latest
Usage
Gunnel can be run in either server or client mode and is configured using YAML configuration files.
Docker
Docker images are available on GitHub Container Registry (ghcr.io). All images are built with Alpine Linux for minimal size (~20MB):
# Pull the latest image
docker pull ghcr.io/snakeice/gunnel:latest
# Run server with Docker
docker run -d \
--name gunnel-server \
-p 8081:8081 -p 80:80 -p 443:443 \
-v $(pwd)/example/server.yaml:/etc/gunnel/server.yaml \
ghcr.io/snakeice/gunnel:latest \
server -c /etc/gunnel/server.yaml
# Run client with Docker
docker run -d \
--name gunnel-client \
-v $(pwd)/example/client.yaml:/etc/gunnel/client.yaml \
ghcr.io/snakeice/gunnel:latest \
client -c /etc/gunnel/client.yaml
For complex deployments, use the provided docker-compose.example.yml:
docker-compose -f docker-compose.example.yml up -d
Server Mode
Start the server on a public machine:
# Using default configuration
gunnel server
# Using a custom configuration file
gunnel server -c ./example/server.yaml
Client Mode
Start the client on your local machine:
# Using default configuration file (gunnel.yaml)
gunnel client
# Using a custom configuration file (with token)
export GUNNEL_TOKEN=YOUR_SHARED_TOKEN
gunnel client -c ./example/client.yaml
Using Configuration Files
Gunnel uses YAML configuration files for both server and client modes. Example files are provided in the example/ directory.
Server Configuration Example
# example/server.yaml
domain: test.example.com
token: YOUR_SHARED_TOKEN
cert:
enabled: true
email: admin@example.com
Client Configuration Example
# example/client.yaml
server:
host: your-server.com
port: 8080
tunnels:
- subdomain: myapp
local_port: 3000
protocol: http
- subdomain: db
local_port: 3306
protocol: tcp
Command-Line Options
Global Options
--log-level, -l: Set the logging level (trace, debug, info, warn, error, fatal, panic) (default: trace)
Server Options
--config, -c: Path to the server configuration file
Client Options
--config, -c: Path to the client configuration file (default: gunnel.yaml)
Examples
Exposing a Local Web Server
# Start your local web server
python -m http.server 8000
# Start the tunnel client
gunnel client --subdomain myweb --local-port 8000
Exposing a Local Database
# Start the tunnel client for MySQL
gunnel client --subdomain db --local-port 3306 --protocol tcp
Development
Prerequisites
- Mise for task management and tool versioning
- Go 1.24 or later (automatically managed by Mise)
Building
# Clone the repository
git clone https://github.com/snakeice/gunnel.git
cd gunnel
# Install dependencies and tools
mise run deps
# Build
mise run build
# Run tests
mise run test
Available Tasks
mise run build: Build the application
mise run test: Run tests
mise run clean: Clean build artifacts
mise run lint: Run the linter
mise run deps: Install dependencies
mise run run:server: Run the server locally
mise run run:client: Run the client locally
mise run air:server: Run the server with Air
mise run air:client: Run the client with Air
Contributing
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature)
- Commit your changes (
git commit -m 'Add some amazing feature')
- Push to the branch (
git push origin feature/amazing-feature)
- Open a Pull Request
License
This project is licensed under the MIT License - see the LICENSE file for details.
Acknowledgments
- logrus for beautiful logging
- cobra for the CLI framework
- mise for task management and tool versioning
- quic-go for QUIC protocol implementation
Quickstart
This quickstart uses the example configs and a simple local HTTP server included in this repo.
- Install or build
go install github.com/snakeice/gunnel@latest
# or from source
git clone https://github.com/snakeice/gunnel.git
cd gunnel
go build ./...
- Start a local app to expose (in a new terminal)
go run ./scripts/fake.go
- Start the gunnel server (in another terminal)
# from repo root
go run . server -c ./example/server.yaml
# or if you installed the binary
gunnel server -c ./example/server.yaml
- Start the gunnel client (in another terminal)
# from repo root
go run . client -c ./example/client.yaml
# or if you installed the binary
gunnel client -c ./example/client.yaml
- Test the tunnel
# If your system resolves *.localhost, this works:
curl http://test.localhost:8080/
# Otherwise, add this to /etc/hosts:
# 127.0.0.1 test.localhost
# and then:
curl -H "Host: test.localhost" http://127.0.0.1:8080/
You should see the JSON response from the example app running on port 3000.
Configuration Reference
Gunnel uses YAML configuration files. This repo includes ready-to-use examples under example/.
- Server configuration (example/server.yaml as provided in the repo):
# example/server.yaml
domain: test.example.com
tls:
enable: true
email: admin@example.com
production: false # Set to true for production Let's Encrypt certificates
# The following fields are only needed if you're configuring a custom backend
# host: localhost
# port: 3000
Notes:
-
The server listens for HTTP users on server_port (default 8080) and for QUIC clients on quic_port (default 8081).
-
For TLS via Let's Encrypt, the current server supports a cert section in config (preferred):
- cert.enabled: true|false
- cert.email: your-email
If you use the provided example (tls block), the server will still start but TLS will only be enabled when cert.enabled is set under cert.
-
Client configuration (example/client.yaml as provided in the repo):
server_addr: localhost:8081
backend:
test:
port: 3000
subdomain: test
protocol: http
svc:
host:
port: 3000
subdomain: svc
protocol: http
Client config fields:
- server_addr: host:port of the server QUIC endpoint (default on server is :8081)
- backend: a map of named backends
- host: defaults to localhost
- port: required (e.g., 3000)
- subdomain: required (e.g., test → test.)
- protocol: http or tcp (defaults to http)
Testing
go test ./...
mise run lint
Option A: Manual (as in Quickstart)
- go run ./scripts/fake.go
- go run . server -c ./example/server.yaml
- go run . client -c ./example/client.yaml
- curl -H "Host: test.localhost" http://127.0.0.1:8080/
Option B: Dev workflow with hot-reload (requires tmux, mise, and air configured)
bash ./scripts/debug-watch.sh
This launches:
- a fake local server (scripts/fake.go)
- the gunnel server with Air reloader
- the gunnel client with Air reloader
- a periodic curl to test.localhost:8080
Examples using provided configs
- Expose a local web server on subdomain test:
# local app
go run ./scripts/fake.go
# server (on a public or local machine)
gunnel server -c ./example/server.yaml
# client (on your machine running the app)
gunnel client -c ./example/client.yaml
# test
curl -H "Host: test.localhost" http://127.0.0.1:8080/
- Expose another service (svc) on port 3000:
Add under backend in example/client.yaml:
svc:
host: localhost
port: 3000
subdomain: svc
protocol: http
Then:
gunnel client -c ./example/client.yaml
curl -H "Host: svc.localhost" http://127.0.0.1:8080/
Tip: The web dashboard is served at the special subdomain gunnel (used internally) to expose basic stats and health.