No description
  • Python 35.6%
  • Shell 24.1%
  • Rust 18%
  • JavaScript 14.3%
  • HTML 3.9%
  • Other 4%
Find a file
Sam Bent 8b516d2879
Some checks failed
ci / lint (push) Has been cancelled
ci / property-tests (push) Has been cancelled
ci / rust-tests (push) Has been cancelled
ci / build-and-test (push) Has been cancelled
i2pforge 0.1.0: hardened I2P server-tunnel sidecar
2026-07-18 12:46:25 -04:00
.github/workflows i2pforge 0.1.0: hardened I2P server-tunnel sidecar 2026-07-18 12:46:25 -04:00
companions i2pforge 0.1.0: hardened I2P server-tunnel sidecar 2026-07-18 12:46:25 -04:00
installer i2pforge 0.1.0: hardened I2P server-tunnel sidecar 2026-07-18 12:46:25 -04:00
scripts i2pforge 0.1.0: hardened I2P server-tunnel sidecar 2026-07-18 12:46:25 -04:00
signatures i2pforge 0.1.0: hardened I2P server-tunnel sidecar 2026-07-18 12:46:25 -04:00
tests i2pforge 0.1.0: hardened I2P server-tunnel sidecar 2026-07-18 12:46:25 -04:00
vanity-rs i2pforge 0.1.0: hardened I2P server-tunnel sidecar 2026-07-18 12:46:25 -04:00
wizard i2pforge 0.1.0: hardened I2P server-tunnel sidecar 2026-07-18 12:46:25 -04:00
.dockerignore i2pforge 0.1.0: hardened I2P server-tunnel sidecar 2026-07-18 12:46:25 -04:00
.gitleaks.toml i2pforge 0.1.0: hardened I2P server-tunnel sidecar 2026-07-18 12:46:25 -04:00
CHANGELOG.md i2pforge 0.1.0: hardened I2P server-tunnel sidecar 2026-07-18 12:46:25 -04:00
docker-compose.yml i2pforge 0.1.0: hardened I2P server-tunnel sidecar 2026-07-18 12:46:25 -04:00
Dockerfile i2pforge 0.1.0: hardened I2P server-tunnel sidecar 2026-07-18 12:46:25 -04:00
entrypoint.py i2pforge 0.1.0: hardened I2P server-tunnel sidecar 2026-07-18 12:46:25 -04:00
keycrypt.py i2pforge 0.1.0: hardened I2P server-tunnel sidecar 2026-07-18 12:46:25 -04:00
LICENSE i2pforge 0.1.0: hardened I2P server-tunnel sidecar 2026-07-18 12:46:25 -04:00
podman-compose.yml i2pforge 0.1.0: hardened I2P server-tunnel sidecar 2026-07-18 12:46:25 -04:00
README.md i2pforge 0.1.0: hardened I2P server-tunnel sidecar 2026-07-18 12:46:25 -04:00
SECURITY.md i2pforge 0.1.0: hardened I2P server-tunnel sidecar 2026-07-18 12:46:25 -04:00
vanity.py i2pforge 0.1.0: hardened I2P server-tunnel sidecar 2026-07-18 12:46:25 -04:00

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.i2p address 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 (no seccomp=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 … i2pforge download 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 the docker build line 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.html from this repo in any browser, or serve the wizard/ 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:latest with 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.