diff --git a/.env.example b/.env.example index 6dd0975b..7f335c89 100644 --- a/.env.example +++ b/.env.example @@ -67,3 +67,8 @@ WORKER_COUNT=2 # Defaults to CPU count. Use 1 or 2 for local testing. # TOR_CONTROL_PORT=9051 # TOR_PASSWORD= # HIDDEN_SERVICE_PORT=80 + +# --- I2P (Optional) --- +# To enable I2P, use: ./scripts/start_with_i2p +# I2P tunnel configuration lives in i2p/tunnels.conf and i2p/i2pd.conf. +# No application-level env vars are needed; the i2pd sidecar handles everything. diff --git a/CONFIGURATION.md b/CONFIGURATION.md index 908fbe76..5e4faa7b 100644 --- a/CONFIGURATION.md +++ b/CONFIGURATION.md @@ -58,6 +58,24 @@ The following environment variables can be set: | DEBUG | Debugging filter | | | ZEBEDEE_API_KEY | Zebedee Project API Key | | +## I2P + +I2P support is provided as a sidecar container (i2pd) via `docker-compose.i2p.yml`, mirroring the Tor setup. No application-level environment variables are needed — the i2pd container creates an I2P server tunnel that forwards traffic to nostream's WebSocket port. + +Configuration files live in the `i2p/` directory: + +| File | Description | +|------|-------------| +| `i2p/tunnels.conf` | Defines the I2P server tunnel pointing at nostream (port 8008). | +| `i2p/i2pd.conf` | Minimal i2pd daemon configuration. | + +Tunnel keys are persisted at `.nostr/i2p/data/` so the `.b32.i2p` address survives container restarts. + +The i2pd web console (tunnel status, `.b32.i2p` destinations) is published to the host on **`127.0.0.1:7070`** only. Remove the `ports:` mapping in `docker-compose.i2p.yml` to disable host-side access. + +- Start with I2P: `./scripts/start_with_i2p` +- Print hostname hints: `./scripts/print_i2p_hostname` + If you've set READ_REPLICAS to 4, you should configure RR0_ through RR3_. # Settings diff --git a/README.md b/README.md index 728e3980..5ddb1b6d 100644 --- a/README.md +++ b/README.md @@ -234,6 +234,16 @@ Print the Tor hostname: ./scripts/print_tor_hostname ``` +Start with I2P: + ``` + ./scripts/start_with_i2p + ``` + +Print the I2P hostname: + ``` + ./scripts/print_i2p_hostname + ``` + ### Importing events from JSON Lines You can import NIP-01 events from a `.jsonl` file directly into the relay database. @@ -626,7 +636,7 @@ To observe client and subscription counts in real-time during a test, you can in ```bash docker compose logs -f nostream ``` -======= + ## Export Events Export all stored events to a [JSON Lines](https://jsonlines.org/) (`.jsonl`) file. Each line is a valid NIP-01 Nostr event JSON object. The export streams rows from the database using cursors, so it works safely on relays with millions of events without loading them into memory. diff --git a/docker-compose.i2p.yml b/docker-compose.i2p.yml new file mode 100644 index 00000000..ecca502d --- /dev/null +++ b/docker-compose.i2p.yml @@ -0,0 +1,19 @@ +services: + i2pd: + image: purplei2p/i2pd:release-2.59.0 + container_name: i2pd + depends_on: + - nostream + volumes: + - ${PWD}/.nostr/i2p/data:/home/i2pd/data + - ${PWD}/i2p/i2pd.conf:/home/i2pd/data/i2pd.conf:ro + - ${PWD}/i2p/tunnels.conf:/home/i2pd/data/tunnels.conf:ro + ports: + # i2pd web console — bound to 127.0.0.1 on the host so operators can + # look up the .b32.i2p destination without exposing router state + # to the LAN. Remove this mapping to disable host-side access. + - 127.0.0.1:7070:7070 + restart: on-failure + networks: + default: + ipv4_address: 10.10.10.252 diff --git a/docker-compose.yml b/docker-compose.yml index 46b50904..ee7fd472 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -75,6 +75,7 @@ services: restart: on-failure networks: default: + ipv4_address: 10.10.10.2 nostream-db: image: postgres:15 diff --git a/i2p/i2pd.conf b/i2p/i2pd.conf new file mode 100644 index 00000000..c41c83a9 --- /dev/null +++ b/i2p/i2pd.conf @@ -0,0 +1,18 @@ +# Minimal i2pd configuration for nostream. +# Data and keys are persisted via the Docker volume mount at /home/i2pd/data. + +[http] +# Bind the web console on all container interfaces so Docker port forwarding +# (127.0.0.1:7070 on the host) can reach it. Host binding is restricted in +# docker-compose.i2p.yml. +address = 0.0.0.0 +port = 7070 +# Accept requests whose Host header is 127.0.0.1 (port-forwarded) or the +# container's IP. Without this, i2pd returns a "host mismatch" error. +strictheaders = false + +[limits] +transittunnels = 256 + +[precomputation] +elgamal = true diff --git a/i2p/tunnels.conf b/i2p/tunnels.conf new file mode 100644 index 00000000..d98d9431 --- /dev/null +++ b/i2p/tunnels.conf @@ -0,0 +1,5 @@ +[nostream] +type = http +host = 10.10.10.2 +port = 8008 +keys = nostream.dat diff --git a/package.json b/package.json index 1d92e762..3cd8c71e 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,9 @@ "tor:docker:compose:start": "./scripts/start_with_tor", "tor:hostname": "./scripts/print_tor_hostname", "tor:docker:compose:stop": "./scripts/stop", + "i2p:docker:compose:start": "./scripts/start_with_i2p", + "i2p:hostname": "./scripts/print_i2p_hostname", + "i2p:docker:compose:stop": "./scripts/stop", "docker:integration:run": "docker compose -f ./test/integration/docker-compose.yml run --rm tests", "docker:test:integration": "npm run docker:integration:run -- npm run test:integration", "docker:cover:integration": "npm run docker:integration:run -- npm run cover:integration", diff --git a/scripts/print_i2p_hostname b/scripts/print_i2p_hostname new file mode 100644 index 00000000..4a9b02ed --- /dev/null +++ b/scripts/print_i2p_hostname @@ -0,0 +1,23 @@ +#!/bin/bash +set -euo pipefail + +PROJECT_ROOT="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/.." +KEYS_FILE="${PROJECT_ROOT}/.nostr/i2p/data/nostream.dat" + +if [ ! -f "${KEYS_FILE}" ]; then + echo "I2P destination keys not found. Is the i2pd container running?" + echo "Expected: ${KEYS_FILE}" + exit 1 +fi + +# The .b32.i2p address is derived from a SHA-256 hash of the Destination +# inside nostream.dat, so we cannot compute it portably from the host. +# Query the running i2pd container instead. +echo "I2P destination keys exist at: ${KEYS_FILE}" +echo "" +echo "To find your nostream .b32.i2p address, use one of these methods:" +echo " 1. Open the i2pd web console: http://127.0.0.1:7070/?page=i2p_tunnels" +echo " (published by docker-compose.i2p.yml, bound to 127.0.0.1 only)" +echo " 2. Query the console from inside the container:" +echo " docker exec i2pd wget -qO- 'http://127.0.0.1:7070/?page=i2p_tunnels' \\" +echo " | grep -oE '[a-z2-7]{52}\\.b32\\.i2p' | sort -u" diff --git a/scripts/start_with_i2p b/scripts/start_with_i2p new file mode 100644 index 00000000..cc458a3d --- /dev/null +++ b/scripts/start_with_i2p @@ -0,0 +1,40 @@ +#!/bin/bash +set -euo pipefail + +PROJECT_ROOT="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/.." +DOCKER_COMPOSE_FILE="${PROJECT_ROOT}/docker-compose.yml" +DOCKER_COMPOSE_I2P_FILE="${PROJECT_ROOT}/docker-compose.i2p.yml" +I2P_DATA_DIR="${PROJECT_ROOT}/.nostr/i2p/data" +NOSTR_CONFIG_DIR="${PROJECT_ROOT}/.nostr" +SETTINGS_FILE="${NOSTR_CONFIG_DIR}/settings.yaml" +DEFAULT_SETTINGS_FILE="${PROJECT_ROOT}/resources/default-settings.yaml" +CURRENT_DIR="$(pwd)" + +if [[ ${CURRENT_DIR} =~ /scripts$ ]]; then + echo "Please run this script from the Nostream root folder, not the scripts directory." + echo "To do this, change up one directory, and then run the following command:" + echo "./scripts/start_with_i2p" + exit 1 +fi + +if [ "$EUID" -eq 0 ]; then + echo "Error: Nostream should not be run as root." + exit 1 +fi + +if [[ ! -d "${NOSTR_CONFIG_DIR}" ]]; then + echo "Creating folder ${NOSTR_CONFIG_DIR}" + mkdir -p "${NOSTR_CONFIG_DIR}" +fi + +if [[ ! -f "${SETTINGS_FILE}" ]]; then + echo "Copying ${DEFAULT_SETTINGS_FILE} to ${SETTINGS_FILE}" + cp "${DEFAULT_SETTINGS_FILE}" "${SETTINGS_FILE}" +fi + +mkdir -p "${I2P_DATA_DIR}" + +docker compose \ + -f "${DOCKER_COMPOSE_FILE}" \ + -f "${DOCKER_COMPOSE_I2P_FILE}" \ + up --build --remove-orphans "$@" diff --git a/scripts/stop b/scripts/stop index 7122edf3..788cf020 100755 --- a/scripts/stop +++ b/scripts/stop @@ -1,11 +1,15 @@ #!/bin/bash -PROJECT_ROOT="$(dirname $(readlink -f "${BASH_SOURCE[0]}"))/.." +set -euo pipefail + +PROJECT_ROOT="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/.." DOCKER_COMPOSE_FILE="${PROJECT_ROOT}/docker-compose.yml" DOCKER_COMPOSE_TOR_FILE="${PROJECT_ROOT}/docker-compose.tor.yml" +DOCKER_COMPOSE_I2P_FILE="${PROJECT_ROOT}/docker-compose.i2p.yml" DOCKER_COMPOSE_LOCAL_FILE="${PROJECT_ROOT}/docker-compose.local.yml" docker compose \ - -f $DOCKER_COMPOSE_FILE \ - -f $DOCKER_COMPOSE_TOR_FILE \ - -f $DOCKER_COMPOSE_LOCAL_FILE \ - down $@ + -f "${DOCKER_COMPOSE_FILE}" \ + -f "${DOCKER_COMPOSE_TOR_FILE}" \ + -f "${DOCKER_COMPOSE_I2P_FILE}" \ + -f "${DOCKER_COMPOSE_LOCAL_FILE}" \ + down "$@" diff --git a/test/unit/utils/nip05.spec.ts b/test/unit/utils/nip05.spec.ts index e5d47790..5e38193a 100644 --- a/test/unit/utils/nip05.spec.ts +++ b/test/unit/utils/nip05.spec.ts @@ -2,8 +2,10 @@ import axios from 'axios' import chai from 'chai' import chaiAsPromised from 'chai-as-promised' import Sinon from 'sinon' +import sinonChai from 'sinon-chai' chai.use(chaiAsPromised) +chai.use(sinonChai) import { extractNip05FromEvent,