Lantern Box is the Lantern fork of sing-box -- the universal proxy platform -- with extra protocols built for places where the internet comes with walls. It adds Samizdat, WATER, Outline SDK smart dialer, AmneziaWG, and ALGeneva to the sing-box ecosystem.
The goal is to be as useful as possible to the censorship circumvention community. Operators are encouraged to run servers and hand configs to users. We contribute changes upstream whenever we can.
There are two ways to use lantern-box:
- Run the server binary -- deploy it on a VPS and hand JSON configs to users. You get every sing-box protocol plus the Lantern additions.
- Import it as a Go library -- integrate the protocols into your own tooling. One function call registers everything.
Build and run the lantern-box binary:
cd cmd
make
cd -
./cmd/lantern-box run --config config.jsonThis gives you a full sing-box server with all built-in protocols (Shadowsocks, VMess, Trojan, Hysteria, WireGuard, etc.) plus the Lantern protocols documented below. The rest of this README is about what goes inside config.json.
If you're building your own Go tool and want to use the Lantern protocols (client-side or server-side), import lantern-box as a module:
go get github.com/getlantern/lantern-box@latestThen call BaseContext() to get a context with all sing-box and lantern-box protocols registered:
package main
import (
"os"
box "github.com/sagernet/sing-box"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common/json"
lanternbox "github.com/getlantern/lantern-box"
)
func main() {
// Register all sing-box + lantern-box protocols
ctx := lanternbox.BaseContext()
// Load your config (same JSON format as the CLI)
content, _ := os.ReadFile("config.json")
options, _ := json.UnmarshalExtendedContext[option.Options](ctx, content)
// Create and start the instance
instance, _ := box.New(box.Options{
Context: ctx,
Options: options,
})
instance.Start()
}BaseContext() registers every protocol -- Samizdat, WATER, ALGeneva, Outline SDK, AmneziaWG, plus all the sing-box built-ins. The JSON config format is the same whether you use the CLI binary or the library.
Pick the protocol that fits your threat model.
| Protocol | Threat Model | Keys/Certs? | Server Needed? |
|---|---|---|---|
| Samizdat | Full DPI resistance (Russia-grade TSPU) | X25519 + TLS cert | Yes |
| WATER | Pluggable transport (swap WASM modules) | WASM hash | Yes |
| Outline SDK | DNS/SNI blocking (smart dialer) | No | No |
| AmneziaWG | WireGuard protocol fingerprinting | WireGuard keys | Yes |
| ALGeneva | HTTP-level DPI (header inspection) | No | Yes |
Samizdat is built to defeat the full spectrum of modern DPI, including Russian TSPU infrastructure. It makes proxy traffic look like a browser visiting a real website over HTTP/2:
- Single TLS 1.3 layer with a Chrome fingerprint via uTLS (no TLS-over-TLS)
- HTTP/2 CONNECT tunneling with multiplexed streams on one TCP connection
- PSK-based authentication embedded in the TLS SessionID field
- TCP-level masquerade to a real domain for active probe resistance
- Geneva-inspired TCP fragmentation of the ClientHello at the SNI boundary
- Traffic shaping with Chrome-profile padding and timing jitter
When to use it: The censor is doing deep packet inspection, active probing, and traffic analysis. You need the heavy artillery.
See the samizdat README for the full protocol design.
You need an X25519 keypair, a short ID, and a TLS certificate.
Option A -- samizdat tool:
go run github.com/getlantern/samizdat/cmd/samizdat-server --genkeysOption B -- openssl (for CI or scripting):
# X25519 keypair
openssl genpkey -algorithm X25519 -out priv.pem
PRIVATE_KEY=$(openssl pkey -in priv.pem -outform DER | tail -c 32 | xxd -p | tr -d '\n')
PUBLIC_KEY=$(openssl pkey -in priv.pem -pubout -outform DER | tail -c 32 | xxd -p | tr -d '\n')
# Random short ID
SHORT_ID=$(openssl rand -hex 8)
# Self-signed TLS certificate
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 \
-keyout key.pem -out cert.pem -days 365 -nodes \
-subj "/CN=example.com"{
"log": { "level": "info" },
"inbounds": [
{
"type": "samizdat",
"tag": "samizdat-in",
"listen": "::",
"listen_port": 443,
"private_key": "PRIVATE_KEY_HEX",
"short_ids": ["SHORT_ID_HEX"],
"cert_path": "/etc/ssl/certs/cert.pem",
"key_path": "/etc/ssl/private/key.pem",
"masquerade_domain": "ok.ru"
}
],
"outbounds": [
{
"type": "direct",
"tag": "direct"
}
]
}{
"inbounds": [
{
"type": "mixed",
"tag": "mixed-in",
"listen": "127.0.0.1",
"listen_port": 1080
}
],
"outbounds": [
{
"type": "samizdat",
"tag": "samizdat-out",
"server": "YOUR_SERVER_IP",
"server_port": 443,
"public_key": "PUBLIC_KEY_HEX",
"short_id": "SHORT_ID_HEX",
"server_name": "ok.ru"
}
]
}| Field | Type | Default | Description |
|---|---|---|---|
public_key |
string | required | Server X25519 public key (64 hex chars) |
short_id |
string | required | Pre-shared 8-byte ID (16 hex chars) |
server_name |
string | Cover site SNI (e.g. "ok.ru") |
|
fingerprint |
string | "chrome" |
TLS fingerprint: "chrome", "firefox", "safari" |
disable_padding |
bool | false |
Disable H2 DATA frame padding |
disable_jitter |
bool | false |
Disable timing jitter |
max_jitter_ms |
int | 30 |
Maximum jitter in milliseconds |
padding_profile |
string | "chrome" |
Padding profile: "chrome", "firefox" |
disable_tcp_fragmentation |
bool | false |
Disable ClientHello fragmentation |
disable_record_fragmentation |
bool | false |
Disable TLS record fragmentation |
max_streams_per_conn |
int | 100 |
Max H2 streams per TCP connection |
idle_timeout |
string | "5m" |
Close idle connections after this duration |
connect_timeout |
string | "15s" |
TCP+TLS connect timeout |
data_threshold |
int | 14000 |
Bytes before aggressive padding kicks in |
| Field | Type | Default | Description |
|---|---|---|---|
private_key |
string | required | Server X25519 private key (64 hex chars) |
short_ids |
string[] | required | Allowed client short IDs |
cert_path |
string | Path to TLS certificate PEM file | |
key_path |
string | Path to TLS key PEM file | |
cert_pem |
string | Inline TLS certificate PEM | |
key_pem |
string | Inline TLS key PEM | |
masquerade_domain |
string | Domain to masquerade as | |
masquerade_addr |
string | IP:port override for masquerade | |
masquerade_idle_timeout |
string | "5m" |
Masquerade connection idle timeout |
masquerade_max_duration |
string | "10m" |
Max masquerade connection duration |
max_concurrent_streams |
int | 250 |
Max H2 streams per connection |
WATER (WebAssembly Transport Executables Runtime) lets you swap transport logic at runtime by loading WASM modules. Both sides download the same WASM binary; the module handles how bytes move over the wire.
When to use it: You want pluggable transports without recompiling. New evasion logic ships as a .wasm file.
You need a WASM module and its SHA-256 hash. The plain.wasm test module is bundled in the WATER Go module:
# Download the WATER module
go mod download github.com/refraction-networking/water@v0.7.1-alpha
# Find plain.wasm in the module cache
WASM_PATH="$(go env GOPATH)/pkg/mod/github.com/refraction-networking/water@v0.7.1-alpha/transport/v1/testdata/plain.wasm"
# Compute the SHA-256 hash
shasum -a 256 "$WASM_PATH"Host the .wasm file on an HTTP server both sides can reach (a simple python3 -m http.server 8888 works).
{
"log": { "level": "info" },
"inbounds": [
{
"type": "water",
"tag": "water-in",
"listen": "::",
"listen_port": 9003,
"transport": "plain",
"hashsum": "WASM_SHA256_HASH",
"wasm_available_at": ["http://127.0.0.1:8888/plain.wasm"],
"config": {}
}
],
"outbounds": [
{
"type": "direct",
"tag": "direct"
}
]
}{
"inbounds": [
{
"type": "mixed",
"tag": "mixed-in",
"listen": "127.0.0.1",
"listen_port": 1080
}
],
"outbounds": [
{
"type": "water",
"tag": "water-out",
"server": "YOUR_SERVER_IP",
"server_port": 9003,
"transport": "plain",
"hashsum": "WASM_SHA256_HASH",
"wasm_available_at": ["http://YOUR_SERVER_IP:8888/plain.wasm"],
"download_timeout": "60s",
"water_dir": "/tmp/water",
"config": {}
}
]
}Gotcha: The
"config"field is optional. If omitted, WATER will still start, but it will not injectremote_addr/remote_portinto the module config. Include"config": {}when your WASM module expects those values.
| Field | Type | Description |
|---|---|---|
transport |
string | Identifier for WASM logs |
hashsum |
string | SHA-256 of the WASM file (integrity check) |
wasm_available_at |
string[] | URLs to download the WASM module |
download_timeout |
string | Required. Per-URL download timeout (e.g. "60s"). No default is applied; must be a valid Go duration string. |
water_dir |
string | Required. Local directory for WATER files; must not be empty. |
config |
object | Config passed to the WASM module (can be {}) |
skip_handshake |
bool | Set true if the WASM module handles its own handshake |
The Outline SDK smart dialer is an outbound-only protocol that automatically tries DNS and TLS evasion strategies to reach blocked sites. It cycles through DNS resolvers (system, DoH, DoT, UDP, TCP) and TLS tricks (record fragmentation, packet reordering) until something works.
When to use it: DNS or SNI-based blocking, no server infrastructure available. The client figures out how to get through on its own.
No server config needed. This is a client-side dialer.
{
"inbounds": [
{
"type": "mixed",
"tag": "mixed-in",
"listen": "127.0.0.1",
"listen_port": 1080
}
],
"outbounds": [
{
"type": "outline",
"tag": "outline-out",
"server": "blocked-site.example.com",
"server_port": 443,
"dns": [
{ "system": {} },
{ "https": { "name": "cloudflare-dns.com" } },
{ "tls": { "name": "dns.google", "address": "8.8.8.8:853" } },
{ "udp": { "address": "8.8.8.8:53" } },
{ "tcp": { "address": "1.1.1.1:53" } }
],
"tls": ["fragmentation", "reordering"],
"test_timeout": "10s",
"domains": ["blocked-site.example.com"]
}
]
}| Type | Config | Description |
|---|---|---|
system |
{} |
Use OS resolver |
https |
{ "name": "cloudflare-dns.com" } |
DNS-over-HTTPS |
tls |
{ "name": "dns.google", "address": "8.8.8.8:853" } |
DNS-over-TLS |
udp |
{ "address": "8.8.8.8:53" } |
Plain UDP DNS |
tcp |
{ "address": "1.1.1.1:53" } |
Plain TCP DNS |
AmneziaWG extends WireGuard with junk packet injection and magic header rewriting to defeat protocol fingerprinting. It's configured as a sing-box endpoint (not an inbound/outbound) with extra parameters on top of a standard WireGuard config.
When to use it: WireGuard is blocked by protocol fingerprinting. You need WireGuard semantics but with an unrecognizable wire format.
Add the Amnezia parameters alongside your WireGuard endpoint config:
{
"endpoints": [
{
"type": "amneziawg",
"tag": "awg-ep",
"address": ["10.0.0.2/32"],
"private_key": "WIREGUARD_PRIVATE_KEY",
"peers": [
{
"public_key": "WIREGUARD_SERVER_PUBLIC_KEY",
"endpoint": "YOUR_SERVER_IP:51820",
"allowed_ips": ["0.0.0.0/0"]
}
],
"junk_packet_count": 4,
"junk_packet_min_size": 40,
"junk_packet_max_size": 70,
"init_packet_junk_size": 10,
"response_packet_junk_size": 10,
"init_packet_magic_header": 1,
"response_packet_magic_header": 2,
"underload_packet_magic_header": 3,
"transport_packet_magic_header": 4
}
]
}| Field | JSON key | Description |
|---|---|---|
| Jc | junk_packet_count |
Number of junk packets sent before session init |
| Jmin | junk_packet_min_size |
Minimum junk packet size in bytes |
| Jmax | junk_packet_max_size |
Maximum junk packet size in bytes |
| S1 | init_packet_junk_size |
Junk bytes prepended to init handshake |
| S2 | response_packet_junk_size |
Junk bytes prepended to response handshake |
| H1 | init_packet_magic_header |
Magic header for init packets |
| H2 | response_packet_magic_header |
Magic header for response packets |
| H3 | underload_packet_magic_header |
Magic header for underload packets |
| H4 | transport_packet_magic_header |
Magic header for transport packets |
Both client and server must use identical Amnezia parameters. See the AmneziaWG docs for parameter tuning guidance.
Application-layer Geneva -- mutates HTTP headers on the fly to evade DPI that inspects header fields. No keys, no certs, no TLS. Just a strategy string that describes how to mangle traffic.
When to use it: The censor is doing simple HTTP header inspection. You want the fastest possible setup with zero credential management.
Strategy syntax: [trigger]-action-| where the trigger matches a header field and the action transforms it. Example: [HTTP:host:*]-changecase{lower}-| lowercases the Host header. The algeneva strategies file has pre-built strategies tested against censors in China, India, and Kazakhstan.
{
"log": { "level": "info" },
"inbounds": [
{
"type": "algeneva",
"tag": "algeneva-in",
"listen": "::",
"listen_port": 9001
}
],
"outbounds": [
{
"type": "direct",
"tag": "direct"
}
]
}The server doesn't need a strategy -- it just accepts connections and forwards them.
{
"inbounds": [
{
"type": "mixed",
"tag": "mixed-in",
"listen": "127.0.0.1",
"listen_port": 1080
}
],
"outbounds": [
{
"type": "algeneva",
"tag": "algeneva-out",
"server": "YOUR_SERVER_IP",
"server_port": 9001,
"strategy": "[HTTP:host:*]-changecase{lower}-|"
}
]
}Point your browser's SOCKS5 proxy at 127.0.0.1:1080 and you're done.
A single lantern-box instance can serve multiple protocols. Just put them all in one config:
{
"log": { "level": "info" },
"inbounds": [
{
"type": "algeneva",
"tag": "algeneva-in",
"listen": "::",
"listen_port": 9001
},
{
"type": "samizdat",
"tag": "samizdat-in",
"listen": "::",
"listen_port": 9002,
"private_key": "PRIVATE_KEY_HEX",
"short_ids": ["SHORT_ID_HEX"],
"cert_path": "/etc/ssl/certs/cert.pem",
"key_path": "/etc/ssl/private/key.pem",
"masquerade_domain": "ok.ru"
},
{
"type": "water",
"tag": "water-in",
"listen": "::",
"listen_port": 9003,
"transport": "plain",
"hashsum": "WASM_SHA256_HASH",
"wasm_available_at": ["http://127.0.0.1:8888/plain.wasm"],
"config": {}
}
],
"outbounds": [
{
"type": "direct",
"tag": "direct"
}
]
}The E2E test suite spins up a DigitalOcean droplet, deploys lantern-box, and runs traffic through ALGeneva, Samizdat, and WATER.
Trigger it manually:
gh workflow run e2e.yamlIt also runs automatically on pull requests to main that modify non-documentation files (docs-only changes are ignored).
What it tests: Each protocol gets a server instance on the droplet and a client instance on the CI runner. The test curls http://example.com through each proxy and checks for a valid response.
Required secrets: DO_API_TOKEN (DigitalOcean), CI_PRIVATE_REPOS_GH_TOKEN (private Go modules).
PRs welcome. The upstream goal means we prefer changes that are general enough to contribute back to sing-box when possible.
- Protocol adapters live in
protocol/ - Option structs live in
option/ - Build tags:
with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale
Run tests with:
go test -tags "with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale" ./...