Sign inSign up

getlantern/sing-box-extensions

By getlantern

•Updated 8 months ago

Sing Box Extensions

Image
0

1.7K

getlantern/sing-box-extensions repository overview

⁠lantern-box

Lantern Box is a censorship circumvention proxy and client platform that's built on 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:

  1. 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.
  2. Import it as a Go library -- integrate the protocols into your own tooling. One function call registers everything.

⁠Quick Start: Run the Server

Build and run the lantern-box binary:

cd cmd
make
cd -
./cmd/lantern-box run --config config.json

This 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.

⁠Quick Start: Go Library Integration

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@latest

Then 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.


⁠Protocol Guide

Pick the protocol that fits your threat model.

ProtocolThreat ModelKeys/Certs?Server Needed?
SamizdatFull DPI resistance (Russia-grade TSPU)X25519 + TLS certYes
WATERPluggable transport (swap WASM modules)WASM hashYes
Outline SDKDNS/SNI blocking (smart dialer)NoNo
AmneziaWGWireGuard protocol fingerprintingWireGuard keysYes
ALGenevaHTTP-level DPI (header inspection)NoYes

⁠Samizdat

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.

⁠Credential generation

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 --genkeys

Option 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"
⁠Server config
{
  "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"
    }
  ]
}
⁠Client config
{
  "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"
    }
  ]
}
⁠All client options
FieldTypeDefaultDescription
public_keystringrequiredServer X25519 public key (64 hex chars)
short_idstringrequiredPre-shared 8-byte ID (16 hex chars)
server_namestringCover site SNI (e.g. "ok.ru")
fingerprintstring"chrome"TLS fingerprint: "chrome", "firefox", "safari"
disable_paddingboolfalseDisable H2 DATA frame padding
disable_jitterboolfalseDisable timing jitter
max_jitter_msint30Maximum jitter in milliseconds
padding_profilestring"chrome"Padding profile: "chrome", "firefox"
disable_tcp_fragmentationboolfalseDisable ClientHello fragmentation
disable_record_fragmentationboolfalseDisable TLS record fragmentation
max_streams_per_connint100Max H2 streams per TCP connection
idle_timeoutstring"5m"Close idle connections after this duration
connect_timeoutstring"15s"TCP+TLS connect timeout
data_thresholdint14000Bytes before aggressive padding kicks in
⁠All server options
FieldTypeDefaultDescription
private_keystringrequiredServer X25519 private key (64 hex chars)
short_idsstring[]requiredAllowed client short IDs
cert_pathstringPath to TLS certificate PEM file
key_pathstringPath to TLS key PEM file
cert_pemstringInline TLS certificate PEM
key_pemstringInline TLS key PEM
masquerade_domainstringDomain to masquerade as
masquerade_addrstringIP:port override for masquerade
masquerade_idle_timeoutstring"5m"Masquerade connection idle timeout
masquerade_max_durationstring"10m"Max masquerade connection duration
max_concurrent_streamsint250Max H2 streams per connection

⁠WATER

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.

⁠Credentials

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/[email protected]

# Find plain.wasm in the module cache
WASM_PATH="$(go env GOPATH)/pkg/mod/github.com/refraction-networking/[email protected]/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).

⁠Server config
{
  "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"
    }
  ]
}
⁠Client config
{
  "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 inject remote_addr/remote_port into the module config. Include "config": {} when your WASM module expects those values.

⁠All client options
FieldTypeDescription
transportstringIdentifier for WASM logs
hashsumstringSHA-256 of the WASM file (integrity check)
wasm_available_atstring[]URLs to download the WASM module
download_timeoutstringRequired. Per-URL download timeout (e.g. "60s"). No default is applied; must be a valid Go duration string.
water_dirstringRequired. Local directory for WATER files; must not be empty.
configobjectConfig passed to the WASM module (can be {})
skip_handshakeboolSet true if the WASM module handles its own handshake

⁠Outline SDK (Smart Dialer)

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.

⁠Client config
{
  "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"]
    }
  ]
}
⁠DNS resolver types
TypeConfigDescription
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

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.

⁠Endpoint config

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
    }
  ]
}
⁠Amnezia parameters
FieldJSON keyDescription
Jcjunk_packet_countNumber of junk packets sent before session init
Jminjunk_packet_min_sizeMinimum junk packet size in bytes
Jmaxjunk_packet_max_sizeMaximum junk packet size in bytes
S1init_packet_junk_sizeJunk bytes prepended to init handshake
S2response_packet_junk_sizeJunk bytes prepended to response handshake
H1init_packet_magic_headerMagic header for init packets
H2response_packet_magic_headerMagic header for response packets
H3underload_packet_magic_headerMagic header for underload packets
H4transport_packet_magic_headerMagic header for transport packets

Both client and server must use identical Amnezia parameters. See the AmneziaWG docs⁠ for parameter tuning guidance.


⁠ALGeneva

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.

⁠Server config
{
  "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.

⁠Client config
{
  "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.


⁠Running Multiple Protocols

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"
    }
  ]
}

⁠E2E Testing

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.yaml

It 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).


⁠Contributing

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" ./...

Tag summary

Content type

Image

Digest

sha256:779577f26…

Size

195.2 MB

Last updated

8 months ago

docker pull getlantern/sing-box-extensions