- Python 35.6%
- Shell 24.1%
- Rust 18%
- JavaScript 14.3%
- HTML 3.9%
- Other 4%
| .github/workflows | ||
| companions | ||
| installer | ||
| scripts | ||
| signatures | ||
| tests | ||
| vanity-rs | ||
| wizard | ||
| .dockerignore | ||
| .gitleaks.toml | ||
| CHANGELOG.md | ||
| docker-compose.yml | ||
| Dockerfile | ||
| entrypoint.py | ||
| keycrypt.py | ||
| LICENSE | ||
| podman-compose.yml | ||
| README.md | ||
| SECURITY.md | ||
| vanity.py | ||
i2pforge
Hardened I2P server-tunnel sidecar. Expose any backend as an I2P
hidden service (.b32.i2p) without putting I2P code in your application.
Sibling of HiddenForge (Tor);
same shape, same hardening, drop-in interchangeable.
- Minimal surface. i2pd (C++ I2P router) only.
No SAM bridge, no web console, no HTTP/SOCKS proxy is ever exposed. The
.b32.i2paddress is derived locally from the destination key. - Hash-pinned build. Alpine pinned by digest; every package pinned by exact version.
- Locked-down runtime. Drops all capabilities,
no-new-privileges, read-only root filesystem, runs i2pd as a non-root user, and keeps Docker's default seccomp profile (noseccomp=unconfined). - Forensic minimisation. No host log file, no transit relaying, ephemeral data directory, persistent keys only.
- Post-quantum wire crypto. Ships i2pd 2.60 with OpenSSL 3.5.7, so the transport key exchange is a hybrid X25519 + ML-KEM-768 by default. See SECURITY.md.
Get the image
Everything below runs the container image sambent.dev/sam/i2pforge:latest.
There are two ways to obtain it.
Once a signed release is published (the normal path), just pull it; the
wizards and docker compose up do this for you automatically on first run:
docker pull sambent.dev/sam/i2pforge:latest
Right now / air-gapped / "trust nothing, build it yourself": build the image from this source tree. It is small (~60 MB) and the build is fully hash-pinned, so what you get is byte-for-byte what the source says:
git clone https://sambent.dev/sam/i2pforge && cd i2pforge
docker build -t sambent.dev/sam/i2pforge:latest .
That produces the same tag the rest of this README uses, so every
instruction below works identically whether you pulled or built. For a
bit-reproducible build (verifiable against a published release), use
scripts/build-reproducible.sh instead.
The AppImage and
curl … i2pforgedownload links below point at sambent.dev release assets. Until the first release is published, use the CLI/wizard from this checked-out tree (./scripts/i2pforge,wizard/index.html) and thedocker buildline above. They need no downloads.
Four ways to set this up
A. Drag-and-drop AppImage (GUI): a single 78 MB Linux AppImage. Download,
chmod +x, double-click. Wizard checks docker, picks deploy folder, writes
compose, launches the stack, shows the live address. No terminal involved.
curl -fsSLO https://sambent.dev/sam/i2pforge/releases/download/v0.1.0/i2pforge_0.1.0_amd64.AppImage
chmod +x i2pforge_0.1.0_amd64.AppImage
./i2pforge_0.1.0_amd64.AppImage
Other ways
B. CLI (terminal): one Python script, stdlib only. Interactive prompts by default, fully scriptable via flags for CI / automation:
curl -fsSLO https://sambent.dev/sam/i2pforge/raw/branch/main/scripts/i2pforge
chmod +x i2pforge
# interactive
./i2pforge setup
# scripted
./i2pforge setup --mode eepsite --backend web:80 --vanity sam \
--posture recommended --no-verify -o ./deploy/
Subcommands: setup, show, status, doctor, vanity, backup,
restore, rotate. See i2pforge <cmd> --help.
C. In-container wizard (zero install): the setup wizard ships inside
the image, so docker pull is all you need:
docker run -it -v "$PWD/deploy":/out sambent.dev/sam/i2pforge:latest setup -o /out
# (or just `docker run -it ... i2pforge` with no config, and it greets you with the wizard)
It writes the same hardened docker-compose.yml + deploy.sh to ./deploy.
D. Web wizard (browser): same output, click-through UI. Open i2pforge wizard (host it from any static file server or just open the file). Pure client-side; nothing leaves the tab.
Operator commands you will actually use
i2pforge show # what is my .b32.i2p?
i2pforge status # is the container healthy?
i2pforge doctor # are all hardening flags still on?
i2pforge backup --out keys.tar.age # encrypted offline backup
i2pforge restore --input keys.tar.age # bring it back on a new box
i2pforge vanity --prefix sam # brute-force a vanity prefix
i2pforge encrypt-key --delete-plaintext # wrap the on-disk key (PQ at-rest)
i2pforge decrypt-key # rare; the container does this at startup
i2pforge rotate # DESTRUCTIVE: fresh identity, double-confirm
Easiest path: the setup wizard
If you have never touched I2P before, use a wizard. Both produce the same
hardened docker-compose.yml, deploy.sh, and README.txt, and you just run
the script at the end. Two ways to open it:
- Browser wizard (click-through): open
wizard/index.htmlfrom this repo in any browser, or serve thewizard/folder from any static file server. It runs entirely client-side, nothing leaves the tab, and walks you through 8 steps (welcome, mode, backend, identity, encrypt, hardening, verify, deploy), handing back the three files as downloads. - In-container wizard (no install):
docker run -it sambent.dev/sam/i2pforge:latestwith no config greets you and asks the same questions in the terminal.
Conditional steps keep it short: it skips the multi-service editor if you only want one tunnel, skips signature verification if you opt out, and prompts for a vanity prefix only if you ask for one.
Quick start: one env var, one address (no wizard)
You point i2pforge at any HTTP backend, it gives you a .b32.i2p address.
mkdir -p ./i2p-keys
docker run -d --name i2p \
--cap-drop ALL --cap-add CHOWN --cap-add DAC_OVERRIDE \
--cap-add FOWNER --cap-add SETUID --cap-add SETGID \
--security-opt no-new-privileges:true --read-only \
--tmpfs /tmp:rw,noexec,nosuid,size=64m \
--tmpfs /etc/i2pd:rw,noexec,nosuid,size=8m \
--tmpfs /var/lib/i2pd:rw,noexec,nosuid,size=256m,uid=100,gid=101,mode=0700 \
-v "$PWD/i2p-keys:/var/lib/i2pd/destinations" \
-e I2P_EEPSITE="my-nginx:80" \
sambent.dev/sam/i2pforge:latest
# Wait ~30-60s for tunnel build-out, then:
docker logs i2p 2>&1 | grep -A1 "EEPSITE READY"
The startup banner prints your .b32.i2p address in plain sight; it is also
written to ./i2p-keys/eepsite.b32.i2p. Back that directory up. It IS
your address. Losing the keys means losing the address forever.
For a full hardened compose stack with backend container, see docker-compose.yml. Podman users: use podman-compose.yml.
Mapping an existing site (e.g. sambent.com → eepsite)
You don't tunnel sambent.com at the public DNS name. You tunnel the
backend that serves it. If nginx is already serving sambent.com from a
container called web, add i2pforge to the same compose project with
I2P_EEPSITE="web:80" and you have a .b32.i2p mirror of the same site,
with zero changes to nginx. No public IP is exposed, no logs cross the host.
Deployment modes: pick how you host
i2pforge covers three shapes. The wizard (i2pforge setup, the web wizard, or
the AppImage) asks which one; or pass flags:
1. Gateway: you already run a backend; give it an I2P address.
i2pforge setup --mode eepsite --backend web:80 # HTTP backend
i2pforge setup --mode tcp --backend sshd:22 # raw TCP
2. Direct hosting: you have no server; serve a folder of files.
i2pforge setup --mode direct # drop files in ./site, run ./deploy.sh
A hardened, read-only nginx (bundled, pinned) serves ./site as the eepsite.
3. Passive mirroring: put an existing clearnet site on I2P, untouched.
i2pforge setup --mode mirror --origin https://example.com # full anti-leak (default)
i2pforge setup --mode mirror --origin https://example.com --mirror-mode lite
A reverse-proxy companion fetches the origin server-side (the visitor never
touches clearnet) and, in full mode, rewrites same-origin URLs/cookies to
your .b32.i2p, strips HSTS, and ships a strict CSP so the browser cannot load
any third-party (clearnet) resource. lite only fixes the Host header and
redirects. Use it only for sites whose links are relative. See
companions/mirror and
SECURITY.md.
Modes 2 and 3 add a second container (stock pinned
nginx:alpine), hardened identically to i2pforge: unprivileged, read-only,cap_drop: [ALL],no-new-privileges. No second image to pull.
Configuration
Two shapes, pick whichever fits:
Simple mode (one backend, one address):
I2P_EEPSITE = "host:port" # HTTP server-tunnel (web backend; rewrites Host)
I2P_BACKEND = "host:port" # raw TCP forward (non-HTTP backend)
Full mode (named services, multi-port), mirroring HiddenForge's
*_TOR_SERVICE_HOSTS:
<NAME>_I2P_SERVICE_HOSTS = "i2p_port:backend_host:backend_port,..."
environment:
LATTICE_I2P_SERVICE_HOSTS: "80:lattice:8443" # I2P port 80 -> lattice:8443
I2PD_BANDWIDTH: "L" # L (low, default) / M / P / X
I2PD_NOTRANSIT: "1" # 1 = refuse to relay others' traffic (lower profile)
Each *_I2P_SERVICE_HOSTS variable becomes one I2P destination (one
.b32.i2p). Multiple comma-separated mappings share that destination on
different I2P-side ports. The backend host is resolved to an IP at startup,
so a later DNS change cannot silently repoint the tunnel.
How the address is derived
i2pd writes the destination key to destinations/<service>.dat. i2pforge
computes the address as base32(sha256(Destination)).b32.i2p directly from
that file and writes destinations/<service>.b32.i2p. No management port is
opened to read it back. There is nothing to attack.
Verifying a release (optional)
Release image digests carry two signatures: ML-DSA-65 (post-quantum) and an
SSH-FIDO2 YubiKey signature. Verification is optional for end users, since
docker pull works without it, but it is recommended for production:
./scripts/verify-image.sh sambent.dev/sam/i2pforge:0.1.0
The public keys ship in signatures/. See
SECURITY.md for the full flow.
What this is not
- Not an I2P client proxy. Outbound I2P access belongs in the consuming app, not here.
- Not Java I2P. i2pd only.
Threat model & reporting
See SECURITY.md. Designed against a state-level adversary: network surveillance, supply-chain compromise, and legal compulsion of the host operator.
License
See LICENSE.