An open API service indexing awesome lists of open source software.

https://github.com/muyleanging/mekongtunnel

Expose localhost to the internet via SSH remote port forwarding. Self-hosted ngrok alternative written in Go. Self-hosted SSH tunnel server in Go. One command to get a public HTTPS URL for your local app.
https://github.com/muyleanging/mekongtunnel

Last synced: about 2 months ago
JSON representation

Expose localhost to the internet via SSH remote port forwarding. Self-hosted ngrok alternative written in Go. Self-hosted SSH tunnel server in Go. One command to get a public HTTPS URL for your local app.

Awesome Lists containing this project

README

          

# MekongTunnel

> Expose your local app to the internet in one command — no config, no account required.

**Open Source by [KhmerStack](https://github.com/KhmerStack)**

| | |
|---|---|
| Author | Ing Muyleang (អុឹង មួយលៀង) |
| Web UI | [angkorsearch.dev](https://angkorsearch.dev) |
| API | [api.angkorsearch.dev](https://api.angkorsearch.dev) |
| Docs | [docs.mekongtunnel.dev](https://docs.mekongtunnel.dev/) |
| Tunnel Edge | `proxy.angkorsearch.dev` |
| License | MIT |
| Current Version | v1.5.8 |

---

## Install

**macOS / Linux**
```bash
curl -fsSL https://mekongtunnel.dev/install.sh | sh
```

**Windows (PowerShell)**
```powershell
irm https://mekongtunnel.dev/install.ps1 | iex
```

Auto-detects OS and architecture, installs to `PATH`, removes macOS Gatekeeper quarantine, unblocks Windows SmartScreen.

---

## Quick start

```bash
mekong login
mekong subdomain myapp
mekong 3000 --subdomain myapp
```

Plain `mekong 3000` keeps using a random generated tunnel URL. Add `--subdomain myapp`
only when you want a specific reserved name.

Generated tunnels use `*.proxy.angkorsearch.dev` by default. Branded custom domains such as
`app.mekongtunnel.dev` are supported through `mekong domain connect ...`.

Browser visitors to generated tunnel URLs see a one-time shared-tunnel notice first. If the
developer stops Mekong or the local app goes offline, Mekong serves branded tunnel status pages
instead of a raw 404 or generic bad gateway response.

For DNS setup:

- root / apex domains such as `example.com` usually use `A` / `AAAA` records
- subdomains such as `app.example.com` usually use a `CNAME`
- invalid hostnames such as `ttt..example.com` are rejected by both the CLI and API
- deleting a custom domain removes the MekongTunnel route only; DNS stays at the provider until you change it there

## Local dev workflow

For a normal frontend app such as Next.js, Vite, Nuxt, or React, run your app and the tunnel as
two separate processes:

```bash
# Terminal 1
npm run dev

# Terminal 2
mekong 3000
```

`mekong 3000` exposes an already-running local app. It does not start `npm run dev` for you unless
you use a wrapper such as `mekong-cli --with ...`.

If your local app depends on a vhost hostname such as `myapp.test`, use:

```bash
mekong 80 --upstream-host myapp.test
```

## Browser tunnel pages

- The first browser visit to a generated tunnel shows a one-time shared-tunnel notice.
- The `Continue to site` button is a one-click server redirect that sets the warning cookie before returning to the shared URL.
- If the tunnel process is offline, Mekong serves branded offline or custom-domain-pending pages instead of a raw server error.
- If the tunnel is live but the local app is still booting or not responding, Mekong shows a `Tunnel Status` page with a 4-step connection flow:
`Internet -> Mekong Edge -> Mekong Agent -> Local Service`
- The first three steps stay green while the local service step fails in gray/red.
- That page retries automatically every 2 seconds and reloads into the real app once localhost starts responding.
- When the client reported a real local port, the page shows the expected local app target such as `localhost:3000`.
- Raw `ssh -R` sessions stay generic because the server cannot reliably know the client-side local port.

---

## CLI reference

```bash
# Expose ports
mekong 3000 # single port with a random URL
mekong 3000 --subdomain myapp # single port with a specific reserved subdomain
mekong 3000 8080 # multi-port, each gets its own URL
mekong 3000 --expire 48h # with expiry (-e also works)

# Background (daemon) mode
mekong -d 3000 # run in background
mekong status # list active tunnels
mekong logs # tail daemon log
mekong logs -f # follow daemon log
mekong logs 3000 # log for specific port
mekong stop 3000 # stop specific tunnel
mekong stop --all # stop all tunnels

# Auth
mekong login # browser device flow
mekong whoami # show email + plan
mekong logout # clear saved token
mekong subdomain # list reserved subdomains
mekong subdomain myapp # reserve a reserved subdomain
mekong subdomain delete myapp # remove a reserved subdomain
mekong domains # list custom domains
mekong domain add app.example.com
mekong domain connect app.example.com myapp
mekong doctor # connectivity/auth checks
mekong doctor app.example.com # custom-domain DNS + HTTPS checks

# Deploy (static hosting)
mekong deploy ./dist # deploy built site — static/Vue/React/Next.js/PHP
mekong deploy ./ # deploy from current directory (auto-detects type)
mekong deploy list # list active deployments
mekong deploy stop # stop a deployment
mekong deploy redeploy # push a new build to an existing deployment
mekong deploy open # open deployment URL in browser
mekong deploy quota # show storage quota usage
mekong deploy info # show deployment details

# Project setup
mekong detect # detect the local stack in the current project
mekong init # write .mekong.json from detection
mekong help php # Laragon/XAMPP/WAMP/Laravel examples

# Local virtual hosts
mekong 80 --upstream-host myapp.test

# Maintenance
mekong update # self-update binary (checksum-verified)
mekong test # run self-test
mekong --version # print version
mekong --help # usage info
```

---

## Raw SSH (no install)

```bash
# Basic
ssh -t -R 80:localhost:3000 proxy.angkorsearch.dev

# With keep-alive
ssh -t -R 80:localhost:3000 -o ServerAliveInterval=60 -o ServerAliveCountMax=3 proxy.angkorsearch.dev

# With expiry
ssh -o SetEnv=MEKONG_EXPIRE=48h -t -R 80:localhost:3000 proxy.angkorsearch.dev
```

---

## Ecosystem

| Package | Install | Frameworks |
|---------|---------|------------|
| **mekong-cli** (npm) | `npm install -g mekong-cli` | Next.js, Vite, Nuxt, Remix, SvelteKit, Astro, Express |
| **mekong-tunnel** (PyPI) | `pip install mekong-tunnel` | FastAPI, Flask, Django, uvicorn, gunicorn, Granian, Hypercorn |
| **VS Code Extension** | `ext install KhmerStack.mekong-tunnel` | Sidebar panel, Login UI, Live Server |

Local repo folders:

- `mekong-node-sdk/` → npm package source for `mekong-cli`
- `mekong-python-sdk/` → PyPI package source for `mekong-tunnel`
- `mekong-vscode-extension/` → VS Code extension source

```bash
# Node.js
mekong-cli --with "next dev" --port 3000

# Python
uvicorn-mekong main:app --port 8000 --domain
flask-mekong run --port 5000
django-mekong runserver 8000
```

---

## Self-hosting

```bash
# Local runtime env files
cp .env.dev.example .env.dev
cp .env.prod.example .env.prod

# Run the API with the matching env file
./scripts/run-api.sh dev

# Build from source
make build # server + CLI
make build-all # cross-compile server (Linux + macOS, amd64 + arm64)
make build-client-all # cross-compile CLI (all platforms)
make release-cli-assets TAG=v1.5.8 # 6 CLI assets + SHA256SUMS + release-notes.md
make release-cli-publish TAG=v1.5.8 # push tag only; GitHub release workflow publishes assets

# Local API stack with Postgres + Redis
cp .env.compose.dev.example .env.compose.dev
docker compose --env-file .env.compose.dev -f docker-compose.yml -f docker-compose.dev.yml up -d
./scripts/init-stack.sh dev

# Production Compose stack
cp .env.compose.prod.example .env.compose.prod
docker compose --env-file .env.compose.prod -f docker-compose.yml -f docker-compose.prod.yml up -d
./scripts/init-stack.sh prod

# Optional tunnel edge locally or in staging
docker compose --env-file .env.compose.dev -f docker-compose.yml -f docker-compose.dev.yml --profile tunnel up -d mekong-tunnel

# Deploy scripts for existing VM / systemd workflows
./scripts/deploy-api.sh
./scripts/deploy-tunnel.sh

# GCP deploy scripts
LOCAL_ENV_FILE=.env.prod bash ./scripts/gcp-deploy-api.sh
LOCAL_ENV_FILE=.env.prod bash ./scripts/gcp-deploy-tunnel.sh
bash ./scripts/gcp-deploy-frontend.sh
```

`deploy-tunnel.sh` and `gcp-deploy-tunnel.sh` both upload your local `.env.prod` to the tunnel host. `.env` and `.env.api` are no longer part of the supported workflow.

Supported env files now:

- `.env.dev`
- `.env.prod`
- `.env.compose.dev`
- `.env.compose.prod`

If your real servers still use `systemd`, GitHub Actions can run those same deploy scripts for you:

- run `Deploy Dev` manually from the Actions tab
- publish a GitHub Release -> `Deploy Production`

See [docs/GITHUB_DEPLOY.md](./docs/GITHUB_DEPLOY.md) for the required GitHub Environment secrets and variables, including the optional `API_ENV_FILE` and required `TUNNEL_ENV_FILE` multi-line secrets.

`api-init` runs the bootstrap path inside the API image:

- runs migrations
- ensures `server_config` exists
- promotes `ADMIN_EMAIL` to admin
- creates the admin account when `ADMIN_PASSWORD` is provided and the user does not exist yet

Optional Redis is recommended once you run more than one API instance or more than one tunnel edge. With `REDIS_URL` configured, Mekong uses Redis for:

- server config caching
- verified custom-domain target caching
- notification pub/sub across API instances
- email OTP code storage
- distributed API rate limiting

Example:

```bash
export REDIS_URL=redis://127.0.0.1:6379/0
export REDIS_PREFIX=mekong
export REDIS_CACHE_TTL=30s
export REDIS_DOMAIN_CACHE_TTL=1m
export REDIS_NOTIFICATION_CHANNEL=notifications
```

Without `REDIS_URL`, the API and tunnel edge still work normally in single-node mode.

### Deploy hosting (optional)

To enable `mekong deploy` static/PHP/Next.js hosting:

```bash
export DEPLOY_DIR=/opt/mekong/deployments
export DEPLOY_DOMAIN=proxy.mekongtunnel.dev
export TUNNEL_EDGE_SECRET=
export DEPLOY_TUNNEL_ADDR=:2222
```

The API and tunnel server must share the same `TUNNEL_EDGE_SECRET`.

### Koma / Bakong payments (optional)

To enable Bakong KHQR subscription checkout and wallet top-up:

```bash
export KOMA_API_URL=https://koma.khqr.site
export KOMA_MERCHANT_ID=
export KOMA_SECRET_KEY=
export BAKONG_ACCOUNT_NAME=MekongTunnel
export BAKONG_ACCOUNT_ID=
```

### Telegram bot (optional)

```bash
export TELEGRAM_BOT_ENABLED=true
export TELEGRAM_BOT_TOKEN=
export TELEGRAM_BOT_USERNAME=MekongTunnelBot
export TELEGRAM_WEBHOOK_SECRET=
export TELEGRAM_APPROVE_PATH=/telegram-link
```

See [docs/TELEGRAM_BOT.md](./docs/TELEGRAM_BOT.md) for the full setup guide.

See:

- [HANDBOOK.md](./HANDBOOK.md) for architecture, API, data model, and release notes
- [SETUP.md](./SETUP.md) for DNS, TLS, proxy host setup, and production deploy steps
- [docs/GITHUB_DEPLOY.md](./docs/GITHUB_DEPLOY.md) for GitHub Actions deployment on existing `systemd` servers
- [docs/API_FLOW.md](./docs/API_FLOW.md) for current API flow and target service-layer structure
- [docs/PERFORMANCE.md](./docs/PERFORMANCE.md) for stress testing and benchmark guidance

## Stress test

Local API benchmark:

```bash
go run ./cmd/apibench -base-url http://127.0.0.1:8080 -users 1000 -tunnels 5000 -concurrency 100
```

Or:

```bash
USERS=1000 TUNNELS=5000 CONCURRENCY=100 ./scripts/stress-local.sh
```

This measures the API control plane only: register/login-style throughput, tunnel report throughput, latency, and API-side bytes. It does not measure real SSH/HTTPS proxy bandwidth.

---

## Architecture

```
Internet
│ HTTPS :443

┌─────────────────────────────────────────┐
│ proxy.angkorsearch.dev │
│ │
│ SSH Server :22 → assigns subdomain │
│ HTTPS Proxy :443 → reverse proxy │
│ HTTP :80 → redirect to HTTPS │
│ Dashboard :9090 → admin stats (local) │
└─────────────────────────────────────────┘
│ SSH tunnel (tcpip-forward)

localhost:3000 (your app)
```

Redis is optional in development. In multi-instance production it is used as a coordination layer for API-side cache, notification fan-out, email OTP codes, and shared rate limits, while PostgreSQL remains the source of truth.

Every tunnel gets a random subdomain such as:

- `adjective-noun-8hexchars.proxy.angkorsearch.dev`
- or a reserved/branded domain like `myapp.proxy.angkorsearch.dev`

Login for a reserved subdomain that stays the same across reconnects.

---

## Links

- Web UI: [angkorsearch.dev](https://angkorsearch.dev)
- API: [api.angkorsearch.dev](https://api.angkorsearch.dev)
- GitHub: [github.com/MuyleangIng/MekongTunnel](https://github.com/MuyleangIng/MekongTunnel)
- npm: [npmjs.com/package/mekong-cli](https://www.npmjs.com/package/mekong-cli)
- PyPI: [pypi.org/project/mekong-tunnel](https://pypi.org/project/mekong-tunnel/)
- VS Code: [marketplace.visualstudio.com — KhmerStack.mekong-tunnel](https://marketplace.visualstudio.com/items?itemName=KhmerStack.mekong-tunnel)