# krot
**Self-hosted ngrok in Rust. Zero-knowledge, thread-per-core, passwordless.**
`krot` is an open-source tunneling service: your own VPS, one Docker
container, no subscription and no trust in a third-party relay. HTTPS
traffic passes through the server encrypted end-to-end to the tunnel
client, authorization keys are SSH-style (`authorized_keys`), and the
apex-domain certificate is obtained and renewed by the server itself
via ACME.
Written in stable Rust on top of `tokio`, `quinn` (QUIC) and `rustls`.
A single binary (~7 MB, release + LTO + strip), Docker image ~85 MB on
debian-slim.
Table of contents
- [What's inside](#whats-inside)
- [Installation](#installation)
- [Quick start](#quick-start)
- [CLI reference](#cli-reference)
- [Admin API](#admin-api)
- [Architecture](#architecture)
- [Federated relays](#federated-relays)
- [Security](#security)
- [Known limitations](#known-limitations)
- [Development](#development)
- [Contributing](#contributing)
- [Reporting security issues](#reporting-security-issues)
- [License](#license)
---
## What's inside
| | |
|---|---|
| **QUIC + TCP fallback** | Primary transport is QUIC over UDP. Clients behind corporate NATs that block UDP transparently fall back to TLS 1.3 over TCP with a mini-mux. Same port, distinguished by ALPN. |
| **Zero-knowledge apex TLS** | The server terminates TLS **only** on the apex (`krot.example`). Connections to `alice.krot.example` go via SNI-passthrough — the server sees only an encrypted stream. Subdomain private keys never touch the server. |
| **Thread-per-core** | Every core gets an isolated `tokio` current-thread runtime and its own QUIC endpoint via `SO_REUSEPORT`. Rate limits are partitioned per-core — each worker has its own bandwidth bucket, the aggregate cap is respected. |
| **Passwordless Ed25519 bootstrap** | The client generates an Ed25519 key and knocks with a one-shot admin token (32 bytes of entropy, BLAKE3-hashed on disk, 10-minute TTL). The server appends the pubkey to `authorized_keys`. |
| **ACME auto-renewal** | Built-in HTTP-01 responder on port 80. Let's Encrypt certificate for the apex, renewal 30 days before expiry. Cache in `data_dir/acme/`, mode `0700`. |
| **Session resume** | Client crashed? On reconnect it re-attaches to the same `public_url` within a 30-second grace period. Works **across transports** — you can drop a QUIC session and restore it over the TCP fallback. |
| **Rate limiting** | Per-identity token-bucket via `governor` plus a period quota. Fully partitioned per-core, the aggregate cap is respected. When the quota is exhausted the server sends the client `retry_after_ms`. |
| **Passive HTTP inspector** | `--inspect` enables a local admin UI on `localhost:4040` — method, path, status, and duration of every request. |
| **Login-page auth** | `krot http --auth user:pass` serves a styled sign-in page with the krot brand mark; a successful login installs a session cookie (`HttpOnly; SameSite=Lax`, 8h rolling TTL). For machines — `--api-key SECRET` (`X-API-Key` / `Authorization: Bearer`). File/env variants for non-dev use. Constant-time compare, no secrets in logs. |
| **Admin API** | `/admin/v1/{tunnels,keys,metrics}` HTTP endpoint on `127.0.0.1:9700`. Bearer-token auth, Prometheus metrics, key revocation in under 1 second. |
| **Federated relays** | Multi-relay setup: `federation=peer1,peer2` in `authorized_keys`. The client publishes one tunnel on several relays, DNS/CDN handles failover. |
| **Hot-reload of authorization** | Key removed from `authorized_keys`? Active sessions get a revoke notification within <1s via the `notify` watcher. Same mechanism for the peer list. |
| **Graceful shutdown** | `Ctrl+C` sends a clean `ServerBye` to every open session, waits for ACKs, then closes the endpoint. Deadline 5 seconds. |
---
## Installation
Four ways to get `krot-server` and `krot-client` binaries.
### Prebuilt binaries (recommended)
Download from the [latest release](https://github.com/krottunnel/krot/releases/latest):
| Platform | Archive |
|---|---|
| Linux · x86_64 | `krot-vX.Y.Z-x86_64-unknown-linux-musl.tar.gz` |
| Linux · ARM64 | `krot-vX.Y.Z-aarch64-unknown-linux-musl.tar.gz` |
| macOS · Apple Silicon | `krot-vX.Y.Z-aarch64-apple-darwin.tar.gz` |
| Windows · 64-bit | `krot-vX.Y.Z-x86_64-pc-windows-msvc.zip` |
| Windows · 32-bit | `krot-vX.Y.Z-i686-pc-windows-msvc.zip` |
Each archive contains `krot-server`, `krot` (the client CLI), `LICENSE-*`, `README.md`. Verify the accompanying `.sha256`, extract, and move the binaries into your `PATH`. Intel Mac users: run the `aarch64-apple-darwin` build under Rosetta 2, or use `cargo install --git` below.
### With cargo
Requires a stable Rust toolchain (get one from [rustup.rs](https://rustup.rs)):
```bash
cargo install --git https://github.com/krottunnel/krot krot-server krot-client
```
Binaries land in `~/.cargo/bin/` on unix or `%USERPROFILE%\.cargo\bin\` on Windows.
### With Docker (server only)
```bash
docker pull krottunnel/krot-server:latest
```
Multi-arch image (`linux/amd64`, `linux/arm64`).
### From source
```bash
git clone https://github.com/krottunnel/krot
cd krot
cargo build --release --workspace
# → target/release/krot-server, target/release/krot-client
```
---
## Quick start
Prerequisites: a VPS with a public IP, your own domain, wildcard DNS
`*.krot.example → `, ports 80/443/7853 open.
### 1. Server
```bash
# prebuilt image from Docker Hub — recommended:
docker pull krottunnel/krot-server:latest
# or build locally:
# docker build -t krottunnel/krot-server:dev .
docker run -d --name krot \
-p 7853:7853/udp \
-p 7853:7853/tcp \
-p 80:80/tcp \
-p 443:443/tcp \
-v krot-data:/var/lib/krot \
-v krot-config:/etc/krot \
krottunnel/krot-server:latest \
--domain krot.example \
--acme-contact mailto:admin@krot.example \
--acme-production \
--tcp-fallback-bind 0.0.0.0:7853
# the server prints a one-shot admin token:
docker logs krot | grep KROT_ADMIN_TOKEN
# KROT_ADMIN_TOKEN=R54VHZ9FD0JJ44GPDZFEJZ7DV8WFB77JHYHVPFF64BF677X7WX70
```
> **Root/privileges.** Defaults `--http-bind 0.0.0.0:80` and
> `--https-bind 0.0.0.0:443` are privileged ports. The Docker container
> above runs via `-p 80:80/tcp`, where Docker handles the mapping.
> Running **without Docker** requires either `sudo` or `setcap
> 'cap_net_bind_service=+ep' target/release/krot-server`. For local
> development, remap to unprivileged ports: `--http-bind 127.0.0.1:8080
> --https-bind 127.0.0.1:8443`.
### 2. Client
```bash
krot init --server krot.example --admin-token R54VHZ9FD0JJ44...
# → enrolled at krot.example:7853
```
### 3. Publish a local service
```bash
krot http 3000 --name alice --inspect
# → https://alice.krot.example
# → inspector: http://127.0.0.1:4040
```
Done. External requests to `https://alice.krot.example` flow through
the server, TLS-encrypted end-to-end to your client.
---
## CLI reference
### `krot-server`
| Flag | Default | What it does |
|---|---|---|
| `--domain ` | — | Enables DomainMode. Without it — IpMode (TCP tunnels only, self-signed cert with pinned fingerprint). |
| `--acme-contact mailto:...` | — | Obtain the apex certificate via ACME (Let's Encrypt **staging** by default). |
| `--acme-production` | off | Switch to LE production. |
| `--tls-cert / --tls-key` | — | Alternative to ACME — bring your own PEM certificate. |
| `--bind` | `0.0.0.0:7853` | UDP address of the QUIC endpoint. |
| `--tcp-fallback-bind ` | disabled | TCP+TLS listener for clients with UDP blocked. |
| `--http-bind` / `--https-bind` | `0.0.0.0:80` / `:443` | Addresses for the 80/443 routers. |
| `--tcp-port-pool ` | `10000-19999` | Port pool for TCP tunnels. |
| `--cores N` | `available_parallelism()` | Number of worker threads. The `bw=` cap is partitioned per-core. |
| `--data-dir` | `/var/lib/krot` | Persistent state (identity cert, ACME cache, admin_token hash). |
| `--authorized-keys` | `/etc/krot/authorized_keys` | SSH-style file of authorized keys. Hot-reload. |
| `--peer-list` | `/etc/krot/peers.txt` | Static list of federated relays (one apex per line). Hot-reload. |
| `--admin-bind` | `127.0.0.1:9700` | Structured admin API. Empty value disables it. |
| `--issue-admin-token` | off | Issue a new admin token even if `authorized_keys` is non-empty. |
### `krot` (client)
| Subcommand | What it does |
|---|---|
| `krot init --server HOST --admin-token TOKEN [--fingerprint sha256:HEX]` | Generate identity, enroll the public key. IpMode uses the pinned fingerprint; DomainMode uses a real CA. |
| `krot tcp ` | Publish a TCP service (`ssh`, DB, minecraft, anything) as `tcp://:`. |
| `krot http [flags]` | Publish an HTTP service as `https://