diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..b354484 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,17 @@ +.git +.github +.idea +.vs +.env +.env.* +!.env.example +**/bin +**/obj +**/TestResults +**/*.user +**/*.suo +coverage +artifacts +data +Test.TruthGate +TruthGate-Web/TruthGate-Web.Tests diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..64c411a --- /dev/null +++ b/.env.example @@ -0,0 +1,55 @@ +# Published image. Local builds use the same Compose file and can override this. +TRUTHGATE_IMAGE=ghcr.io/magiccodingman/truthgate-ipfs:master + +# Build inputs. Both official base images are multi-platform. +DOTNET_VERSION=10.0 +KUBO_VERSION=v0.42.0 +TRUTHGATE_UID=1000 +TRUTHGATE_GID=1000 + +# Host ports. +TRUTHGATE_HTTP_PORT=80 +TRUTHGATE_HTTPS_PORT=443 +TRUTHGATE_DEV_HTTP_PORT=8080 +IPFS_SWARM_PORT=4001 + +# Persistent host paths. The blockstore is always separate so it can later be +# moved to different storage without changing the container's Kubo layout. +TRUTHGATE_DATA_HOST_PATH=./data/truthgate +IPFS_REPO_HOST_PATH=./data/ipfs/repo +IPFS_BLOCKS_HOST_PATH=./data/ipfs/blocks + +# Certificate behavior. +TRUTHGATE_ACME_STAGING=false +# Optional comma-separated IP addresses for the self-signed fallback cert. +TRUTHGATE_CERT_IPS= + +# TruthGate creates data/truthgate/config/kubo-settings.json on first boot. +# That persistent file contains the normal server defaults and is the future +# control-plane contract for the TruthGate UI. Uncomment any value below to +# override the corresponding persistent setting from Docker/Compose. +# +# TRUTHGATE_KUBO_APPLY_SERVER_PROFILE=true +# TRUTHGATE_KUBO_ENSURE_SWARM_LISTENERS=true +# TRUTHGATE_KUBO_ROUTING_TYPE=dhtserver +# TRUTHGATE_KUBO_HOLE_PUNCHING=true +# TRUTHGATE_KUBO_RELAY_CLIENT=true +# TRUTHGATE_KUBO_PROVIDE_ENABLED=true +# TRUTHGATE_KUBO_PROVIDE_STRATEGY=all +# TRUTHGATE_KUBO_PROVIDE_INTERVAL=22h +# TRUTHGATE_KUBO_PROVIDE_SWEEP=true +# TRUTHGATE_KUBO_PROVIDE_RESUME=true +# +# Storage can be "auto" or any Kubo size such as 750GB, 2TB, or 5TiB. +# Auto uses the configured percentage of the filesystem containing the blockstore. +# TRUTHGATE_KUBO_STORAGE_MAX=auto +# TRUTHGATE_KUBO_STORAGE_PERCENT=90 +# TRUTHGATE_KUBO_STORAGE_FALLBACK=200GB +# TRUTHGATE_KUBO_STORAGE_GC_WATERMARK=90 +# TRUTHGATE_KUBO_ENABLE_GC=true +# +# Public addresses can be "auto", "off", or a literal address. The announce +# port defaults to IPFS_SWARM_PORT so non-default Docker host mappings work. +# TRUTHGATE_KUBO_PUBLIC_IPV4=auto +# TRUTHGATE_KUBO_PUBLIC_IPV6=auto +# TRUTHGATE_KUBO_ANNOUNCE_PORT=4001 diff --git a/.github/workflows/docker-repo-migration.yml b/.github/workflows/docker-repo-migration.yml new file mode 100644 index 0000000..69dd808 --- /dev/null +++ b/.github/workflows/docker-repo-migration.yml @@ -0,0 +1,120 @@ +name: Docker repository migration + +on: + pull_request: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: docker-repo-migration-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + migrate-kubo-repository: + strategy: + fail-fast: false + matrix: + include: + - architecture: amd64 + runner: ubuntu-24.04 + - architecture: arm64 + runner: ubuntu-24.04-arm + runs-on: ${{ matrix.runner }} + + steps: + - uses: actions/checkout@v4 + + - name: Build production image (${{ matrix.architecture }}) + run: | + docker build \ + --platform linux/${{ matrix.architecture }} \ + --target production \ + --tag truthgate-ipfs:migration-test-${{ matrix.architecture }} \ + . + + - name: Create a Kubo 0.36 repository and verify automatic migration + shell: bash + run: | + exec > >(tee repo-migration-${{ matrix.architecture }}.log) 2>&1 + set -Eeuo pipefail + + export TRUTHGATE_IMAGE=truthgate-ipfs:migration-test-${{ matrix.architecture }} + export TRUTHGATE_HTTP_PORT=18080 + export TRUTHGATE_HTTPS_PORT=18443 + export IPFS_SWARM_PORT=14001 + export TRUTHGATE_KUBO_PUBLIC_IPV4=off + export TRUTHGATE_KUBO_PUBLIC_IPV6=off + export TRUTHGATE_DATA_HOST_PATH=./.ci-migration/truthgate + export IPFS_REPO_HOST_PATH=./.ci-migration/ipfs/repo + export IPFS_BLOCKS_HOST_PATH=./.ci-migration/ipfs/blocks + + cleanup() { + docker compose logs --no-color 2>/dev/null || true + docker compose down --remove-orphans 2>/dev/null || true + } + trap cleanup EXIT + + rm -rf .ci-migration + mkdir -p \ + .ci-migration/truthgate \ + .ci-migration/ipfs/repo \ + .ci-migration/ipfs/blocks + + docker run --rm \ + --user 0:0 \ + --entrypoint /usr/local/bin/ipfs \ + --env IPFS_PATH=/data/ipfs/repo \ + --volume "$PWD/.ci-migration/ipfs/repo:/data/ipfs/repo" \ + --volume "$PWD/.ci-migration/ipfs/blocks:/data/ipfs/repo/blocks" \ + ipfs/kubo:v0.36.0 \ + init --profile server + + test "$(sudo tr -d '[:space:]' < .ci-migration/ipfs/repo/version)" = "16" + peer_id_before="$(sudo jq -r '.Identity.PeerID' .ci-migration/ipfs/repo/config)" + test -n "${peer_id_before}" + + sudo chown -R 1000:1000 .ci-migration + + docker compose up --detach --no-build + + for attempt in $(seq 1 60); do + state="$(docker inspect --format '{{.State.Status}}' truthgate)" + health="$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' truthgate)" + echo "Attempt ${attempt}: state=${state} health=${health}" + + if [[ "${health}" == "healthy" ]]; then + break + fi + + if [[ "${state}" == "exited" || "${health}" == "unhealthy" ]]; then + docker compose logs --no-color + exit 1 + fi + + sleep 5 + done + + test "$(docker inspect --format '{{.State.Health.Status}}' truthgate)" = "healthy" + test "$(docker exec truthgate cat /data/ipfs/repo/version | tr -d '[:space:]')" = "18" + + peer_id_after="$(docker exec truthgate ipfs config Identity.PeerID)" + test "${peer_id_before}" = "${peer_id_after}" + + docker compose logs --no-color \ + | grep -F "Migrating Kubo repository from version 16 to 18 before applying managed configuration." + docker compose logs --no-color \ + | grep -F "Kubo repository migration completed at version 18." + + test "$(docker exec truthgate ipfs config Routing.Type)" = "dhtserver" + test "$(docker exec truthgate ipfs config Provide.Strategy)" = "all" + docker exec truthgate truthgate-kubo-status + + - name: Upload migration diagnostics + if: always() + uses: actions/upload-artifact@v4 + with: + name: repo-migration-${{ matrix.architecture }} + path: repo-migration-${{ matrix.architecture }}.log + if-no-files-found: ignore diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..7ec5080 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,270 @@ +name: Docker appliance + +on: + pull_request: + push: + branches: + - master + tags: + - "v*" + workflow_dispatch: + +permissions: + contents: read + packages: write + +concurrency: + group: docker-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + IMAGE_NAME: ghcr.io/magiccodingman/truthgate-ipfs + +jobs: + validate-compose: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - name: Validate production Compose + run: docker compose -f compose.yaml config + - name: Validate development override + run: docker compose -f compose.yaml -f compose.dev.yaml config + - name: Validate container scripts + run: | + bash -n docker/entrypoint.sh + bash -n docker/healthcheck.sh + bash -n docker/kubo-configure.sh + bash -n docker/kubo-status.sh + + smoke-test: + strategy: + fail-fast: false + matrix: + include: + - architecture: amd64 + runner: ubuntu-24.04 + - architecture: arm64 + runner: ubuntu-24.04-arm + runs-on: ${{ matrix.runner }} + steps: + - uses: actions/checkout@v4 + + - name: Build production image (${{ matrix.architecture }}) + shell: bash + run: | + set -o pipefail + docker build \ + --progress=plain \ + --platform linux/${{ matrix.architecture }} \ + --target production \ + --tag truthgate-ipfs:test-${{ matrix.architecture }} \ + . 2>&1 | tee production-image-build-${{ matrix.architecture }}.log + + - name: Upload production build diagnostics + if: always() + uses: actions/upload-artifact@v4 + with: + name: production-image-build-${{ matrix.architecture }} + path: production-image-build-${{ matrix.architecture }}.log + if-no-files-found: error + + - name: Build development image (${{ matrix.architecture }}) + shell: bash + run: | + set -o pipefail + docker build \ + --progress=plain \ + --platform linux/${{ matrix.architecture }} \ + --target development \ + --tag truthgate-ipfs:dev-test-${{ matrix.architecture }} \ + . 2>&1 | tee development-image-build-${{ matrix.architecture }}.log + + - name: Upload development build diagnostics + if: always() + uses: actions/upload-artifact@v4 + with: + name: development-image-build-${{ matrix.architecture }} + path: development-image-build-${{ matrix.architecture }}.log + if-no-files-found: ignore + + - name: Start, replace, and revalidate the appliance + shell: bash + run: | + exec > >(tee runtime-smoke-${{ matrix.architecture }}.log) 2>&1 + set -Eeuo pipefail + + export TRUTHGATE_IMAGE=truthgate-ipfs:test-${{ matrix.architecture }} + export TRUTHGATE_HTTP_PORT=18080 + export TRUTHGATE_HTTPS_PORT=18443 + export IPFS_SWARM_PORT=14001 + export TRUTHGATE_KUBO_PUBLIC_IPV4=off + export TRUTHGATE_KUBO_PUBLIC_IPV6=off + export TRUTHGATE_DATA_HOST_PATH=./.ci-data/truthgate + export IPFS_REPO_HOST_PATH=./.ci-data/ipfs/repo + export IPFS_BLOCKS_HOST_PATH=./.ci-data/ipfs/blocks + + cleanup() { + docker compose logs --no-color 2>/dev/null || true + docker compose down --remove-orphans 2>/dev/null || true + } + trap cleanup EXIT + + wait_for_health() { + for attempt in $(seq 1 60); do + status="$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' truthgate)" + if [[ "${status}" == "healthy" ]]; then + return 0 + fi + + if [[ "${status}" == "unhealthy" ]]; then + echo "TruthGate became unhealthy." + return 1 + fi + + sleep 5 + done + + echo "TruthGate did not become healthy in time." + return 1 + } + + read_peer_id() { + docker exec truthgate \ + ipfs --api=/ip4/127.0.0.1/tcp/5001 id \ + | python3 -c 'import json, sys; print(json.load(sys.stdin)["ID"])' + } + + read_bootstrap_hash() { + docker exec truthgate \ + sha256sum /data/truthgate/state/bootstrap-admin-password \ + | cut -d' ' -f1 + } + + read_settings_hash() { + docker exec truthgate \ + sha256sum /data/truthgate/config/kubo-settings.json \ + | cut -d' ' -f1 + } + + validate_kubo_server_defaults() { + test "$(docker exec truthgate ipfs config Routing.Type)" = "dhtserver" + test "$(docker exec truthgate ipfs config Swarm.EnableHolePunching)" = "true" + test "$(docker exec truthgate ipfs config Swarm.RelayClient.Enabled)" = "true" + test "$(docker exec truthgate ipfs config Provide.Enabled)" = "true" + test "$(docker exec truthgate ipfs config Provide.Strategy)" = "all" + test "$(docker exec truthgate ipfs config Provide.DHT.Interval)" = "22h" + test "$(docker exec truthgate ipfs config Provide.DHT.SweepEnabled)" = "true" + test "$(docker exec truthgate ipfs config Provide.DHT.ResumeEnabled)" = "true" + test "$(docker exec truthgate ipfs config Datastore.StorageGCWatermark)" = "90" + + storage_max="$(docker exec truthgate ipfs config Datastore.StorageMax)" + test "${storage_max}" != "10GB" + [[ "${storage_max}" =~ ^[0-9]+B$ ]] + + docker exec truthgate test -s /data/truthgate/state/kubo-server-profile-v1 + docker exec truthgate test -s /data/truthgate/config/kubo-settings.json + docker exec truthgate test -s /data/truthgate/config/kubo-overrides.json + + docker exec truthgate \ + ipfs config --json Addresses.Swarm \ + | jq -e ' + index("/ip4/0.0.0.0/tcp/4001") and + index("/ip6/::/tcp/4001") and + index("/ip4/0.0.0.0/udp/4001/quic-v1") and + index("/ip4/0.0.0.0/udp/4001/quic-v1/webtransport") and + index("/ip6/::/udp/4001/quic-v1") and + index("/ip6/::/udp/4001/quic-v1/webtransport") + ' + + test "$(docker exec truthgate ipfs config Addresses.API)" = "/ip4/127.0.0.1/tcp/5001" + test "$(docker exec truthgate ipfs config Addresses.Gateway)" = "/ip4/127.0.0.1/tcp/9010" + docker exec truthgate truthgate-kubo-status + } + + mkdir -p .ci-data/truthgate .ci-data/ipfs/repo .ci-data/ipfs/blocks + + docker compose up --detach --no-build + wait_for_health + + # Read protected state from inside the container. The host runner is + # intentionally unable to inspect the bootstrap secret directly. + docker exec truthgate test -s /data/ipfs/repo/config + docker exec truthgate test -s /data/truthgate/state/bootstrap-admin-password + validate_kubo_server_defaults + peer_id_before="$(read_peer_id)" + bootstrap_hash_before="$(read_bootstrap_hash)" + settings_hash_before="$(read_settings_hash)" + + # Replace the container while retaining only the documented bind-mounted + # state. Kubo identity, managed settings, and first-run credentials must + # remain stable. + docker compose down --remove-orphans + docker compose up --detach --no-build + wait_for_health + + docker exec truthgate test -s /data/ipfs/repo/config + docker exec truthgate test -s /data/truthgate/state/bootstrap-admin-password + validate_kubo_server_defaults + peer_id_after="$(read_peer_id)" + bootstrap_hash_after="$(read_bootstrap_hash)" + settings_hash_after="$(read_settings_hash)" + + test "${peer_id_before}" = "${peer_id_after}" + test "${bootstrap_hash_before}" = "${bootstrap_hash_after}" + test "${settings_hash_before}" = "${settings_hash_after}" + + docker compose logs --no-color + + - name: Upload runtime diagnostics + if: always() + uses: actions/upload-artifact@v4 + with: + name: runtime-smoke-${{ matrix.architecture }} + path: runtime-smoke-${{ matrix.architecture }}.log + if-no-files-found: ignore + + build-and-publish: + if: github.event_name != 'pull_request' + needs: + - validate-compose + - smoke-test + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + + - uses: docker/setup-qemu-action@v3 + + - uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Generate image metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.IMAGE_NAME }} + tags: | + type=raw,value=master,enable=${{ github.ref == 'refs/heads/master' }} + type=sha,prefix=sha- + type=ref,event=tag + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + + - name: Publish AMD64 + ARM64 under one image tag + uses: docker/build-push-action@v6 + with: + context: . + target: production + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + provenance: mode=max + sbom: true diff --git a/DOCKER.md b/DOCKER.md new file mode 100644 index 0000000..9222885 --- /dev/null +++ b/DOCKER.md @@ -0,0 +1,318 @@ +# TruthGate Docker appliance + +TruthGate ships as one container containing the TruthGate ASP.NET application, +Kubo, and the matching `ipfs` CLI. Docker owns the software lifecycle; mounted +state survives image replacement. + +The appliance is intentionally configured as a contributing IPFS server. It is +not a quiet desktop-node preset: DHT server mode, inbound TCP/UDP swarm +transports, content providing, and automatic repository GC are enabled unless +an operator explicitly overrides them. + +## Requirements + +- Docker Engine with the Compose v2 plugin +- Docker Compose 2.24.4 or newer for the development override's `!override` + merge directive +- TCP ports 80, 443, and 4001 plus UDP port 4001 available by default +- Public TCP and UDP forwarding for the selected swarm port when deployed + behind NAT + +## Production quick start + +```bash +cp .env.example .env +docker compose up --build -d +``` + +Open `https://localhost` (the first connection uses TruthGate's self-signed +fallback certificate unless a configured domain has an issued certificate). + +On the first boot, retrieve the generated administrator password with: + +```bash +docker compose logs truthgate +``` + +The username is `admin`. Change its password in the TruthGate UI. Until the +configuration is persisted, the bootstrap password is also retained at +`data/truthgate/state/bootstrap-admin-password` with restrictive permissions. + +## Pulling the published image + +After the multi-platform image has been published by GitHub Actions: + +```bash +docker compose pull +docker compose up -d +``` + +The same `master` tag resolves to the correct `linux/amd64` or `linux/arm64` +image automatically. Replacing the container does not replace mounted state. + +## Persistent storage contract + +The default deployment keeps three host paths separate: + +```text +data/ +├── truthgate/ # config, database, certificates, secrets, app state +└── ipfs/ + ├── repo/ # Kubo identity, config, datastore, keystore, repo version + └── blocks/ # Kubo block files +``` + +They are mounted as: + +```text +/data/truthgate +/data/ipfs/repo +/data/ipfs/repo/blocks +``` + +Kubo still sees a conventional `$IPFS_PATH/blocks` directory, while the host +can later relocate only the blockstore by changing `IPFS_BLOCKS_HOST_PATH`. +No network filesystem, FUSE, JuiceFS, mount management, or mount health logic +is included in this implementation. + +The software inside the image is disposable. Backups should target the mounted +state, not the .NET runtime, Kubo binary, application binaries, or build output. + +## Opinionated Kubo server defaults + +A new repository is initialized with Kubo's `server` profile. An existing +repository receives that profile once, tracked by +`data/truthgate/state/kubo-server-profile-v1`. The profile disables mDNS and +automatic NAT port mapping and filters non-public address ranges, matching a +public server deployment. Set `TRUTHGATE_KUBO_APPLY_SERVER_PROFILE=false` +before the first managed boot when that behavior is not appropriate. + +TruthGate then manages these defaults: + +```text +Routing.Type dhtserver +Addresses.Swarm current Kubo defaults, with missing + TCP/QUIC/WebTransport listeners restored +Swarm.EnableHolePunching true +Swarm.RelayClient.Enabled true +Provide.Enabled true +Provide.Strategy all +Provide.DHT.Interval 22h +Provide.DHT.SweepEnabled true +Provide.DHT.ResumeEnabled true +Datastore.StorageMax auto: 90% of the blockstore filesystem +Datastore.StorageGCWatermark 90 +Automatic repository GC enabled +Addresses.API /ip4/127.0.0.1/tcp/5001 +Addresses.Gateway /ip4/127.0.0.1/tcp/9010 +``` + +The entrypoint preserves Kubo's current listener array and only appends missing +standard listeners. That avoids deleting newer transports such as +WebTransport when Kubo changes its defaults. + +The RPC API and HTTP gateway are always loopback-only. TruthGate already +authenticates and proxies public `/api/v0`, `/ipfs`, `/ipns`, and WebUI access. +Those two Kubo listener settings cannot be overridden. + +The public swarm mapping on port 4001 is the current implementation stage. +TruthGate is expected to own and proxy swarm transports in a future +architecture; until that transport layer exists, Docker publishes Kubo's TCP +and UDP swarm traffic directly. + +## Persistent Kubo settings + +On first boot TruthGate creates: + +```text +data/truthgate/config/kubo-settings.json +``` + +This file is persistent and contains the normal managed settings. It is also +the contract intended for future live management from the TruthGate UI. +Editing the file and restarting the container changes the effective Kubo +configuration without rebuilding the image. + +Environment variables take precedence over the persistent settings file. The +available variables are documented in `.env.example`, including: + +```text +TRUTHGATE_KUBO_ROUTING_TYPE +TRUTHGATE_KUBO_PROVIDE_ENABLED +TRUTHGATE_KUBO_PROVIDE_STRATEGY +TRUTHGATE_KUBO_STORAGE_MAX +TRUTHGATE_KUBO_STORAGE_PERCENT +TRUTHGATE_KUBO_ENABLE_GC +TRUTHGATE_KUBO_PUBLIC_IPV4 +TRUTHGATE_KUBO_PUBLIC_IPV6 +TRUTHGATE_KUBO_ANNOUNCE_PORT +``` + +For advanced settings, edit: + +```text +data/truthgate/config/kubo-overrides.json +``` + +It is a JSON object whose keys are Kubo config paths and whose values are raw +JSON values: + +```json +{ + "Swarm.ConnMgr.LowWater": 100, + "Swarm.ConnMgr.HighWater": 300, + "Routing.AcceleratedDHTClient": false +} +``` + +Advanced overrides run after TruthGate's normal defaults. `Addresses.API` and +`Addresses.Gateway` are ignored in this file because they are protected +loopback interfaces. + +## Storage policy + +The default storage policy is: + +```json +{ + "storage": { + "max": "auto", + "percent": 90, + "fallback": "200GB", + "gcWatermark": 90, + "enableGc": true + } +} +``` + +`auto` reads the total capacity of the filesystem containing +`/data/ipfs/repo/blocks` and sets Kubo's `Datastore.StorageMax` to 90 percent of +that total. It uses total capacity rather than currently free space so the +limit does not shrink as content is added. If capacity detection fails, the +fallback is `200GB`. + +A fixed value is also supported: + +```env +TRUTHGATE_KUBO_STORAGE_MAX=2TB +``` + +Auto mode recalculates on every container start, so expanding a VPS disk only +requires a container restart. Fixed mode remains exactly the configured value. +`StorageMax` is a soft Kubo blockstore/GC threshold rather than a hard quota, +so filesystem and metadata headroom still matter. + +## Public announce addresses + +By default TruthGate attempts to detect public IPv4 and IPv6 addresses and +manages matching TCP, QUIC-v1, and WebTransport entries in +`Addresses.AppendAnnounce`. Previously managed entries are removed before new +ones are added, so public-IP changes do not accumulate stale addresses. + +Each address can be automatic, disabled, or literal: + +```env +TRUTHGATE_KUBO_PUBLIC_IPV4=auto +TRUTHGATE_KUBO_PUBLIC_IPV6=off +TRUTHGATE_KUBO_ANNOUNCE_PORT=4001 +``` + +The announce port defaults to `IPFS_SWARM_PORT`. If Docker publishes +`14001:4001`, set `IPFS_SWARM_PORT=14001`; TruthGate will announce `14001` while +Kubo continues listening on container port 4001. + +## Kubo startup and migrations + +Every start uses automatic repository migration. Automatic GC is included when +enabled: + +```bash +ipfs daemon --migrate=true --enable-gc +``` + +The entrypoint applies managed settings before starting the daemon, then starts +TruthGate only after Kubo's loopback RPC API is ready. + +## Diagnostics + +Inspect the effective configuration and live node state with: + +```bash +docker exec truthgate truthgate-kubo-status +``` + +The report includes: + +- peer ID and connected peer count +- effective routing, provide, swarm, and storage configuration +- configured and active listen addresses +- public append-announcement addresses +- repository statistics +- AutoNAT observations +- sweep-provider statistics + +Useful direct checks include: + +```bash +docker exec truthgate ipfs config Routing.Type +docker exec truthgate ipfs config --json Addresses.Swarm +docker exec truthgate ipfs config --json Addresses.AppendAnnounce +docker exec truthgate ipfs provide stat +docker exec truthgate ipfs stats dht +``` + +A real external validation should use another IPFS node: + +```bash +ipfs routing findpeer +``` + +Then add a unique file through TruthGate, obtain its CID, and retrieve it from +the separate node. That verifies routing and content providing, not merely +local daemon health. + +## Development + +Development uses the same Dockerfile and base Compose service: + +```bash +docker compose -f compose.yaml -f compose.dev.yaml up --build +``` + +The override changes the image target to the .NET SDK development stage, +bind-mounts the repository at `/workspace`, adds persistent NuGet caches, and +runs the web project with `dotnet watch`. TruthGate is available at +`http://localhost:8080` by default. + +Kubo initialization, server defaults, repository migrations, persistent paths, +startup order, and process supervision are shared with production. This +prevents development from silently using a different node layout. + +Rider can use `compose.yaml` followed by `compose.dev.yaml` directly in a Docker +Compose run configuration. The source directory is mounted directly, so normal +edits trigger `dotnet watch`. + +## General configuration + +Ordinary host-facing settings live in `.env`. Important values include: + +- `TRUTHGATE_IMAGE` +- `KUBO_VERSION` +- `TRUTHGATE_HTTP_PORT` +- `TRUTHGATE_HTTPS_PORT` +- `IPFS_SWARM_PORT` +- `TRUTHGATE_DATA_HOST_PATH` +- `IPFS_REPO_HOST_PATH` +- `IPFS_BLOCKS_HOST_PATH` + +Use absolute host paths for advanced deployments. Relative defaults are rooted +at the directory containing `compose.yaml`. + +## Process model + +`tini` is PID 1 in normal Docker execution. Rider's debugger may become PID 1, +in which case Tini registers as a child subreaper. The entrypoint starts Kubo +first, waits for its RPC API, then starts TruthGate. If either process exits, +the entrypoint terminates the other and exits so Docker's restart policy can +recover the appliance. SIGTERM and SIGINT are forwarded to both processes for +graceful shutdown. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..11fae41 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,148 @@ +# syntax=docker/dockerfile:1.7 + +ARG DOTNET_VERSION=10.0 +ARG KUBO_VERSION=v0.42.0 +ARG TRUTHGATE_UID=1000 +ARG TRUTHGATE_GID=1000 + +FROM ipfs/kubo:${KUBO_VERSION} AS kubo + +# Compile on the builder's native CPU while targeting the requested image +# architecture. This avoids running the full .NET build under QEMU for ARM64. +FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-resolute AS build +ARG TARGETARCH +WORKDIR /src + +COPY TruthGate-Web/TruthGate-Web/TruthGate-Web.csproj TruthGate-Web/TruthGate-Web/ +COPY TruthGate-Web/TruthGate-Web.Client/TruthGate-Web.Client.csproj TruthGate-Web/TruthGate-Web.Client/ +RUN dotnet restore TruthGate-Web/TruthGate-Web/TruthGate-Web.csproj --arch "${TARGETARCH}" + +COPY . . +RUN dotnet publish TruthGate-Web/TruthGate-Web/TruthGate-Web.csproj \ + --configuration Release \ + --arch "${TARGETARCH}" \ + --no-restore \ + --no-self-contained \ + --output /out \ + /p:UseAppHost=false + +# .NET 10 publishes the Blazor bootstrap as a static web asset instead of an +# embedded framework resource. Fail the image build if restore/publish ever +# omits it, because the portal would otherwise render static HTML while every +# interactive server component silently remains inactive. +RUN set -eu; \ + manifest=/out/TruthGate-Web.staticwebassets.endpoints.json; \ + test -s "${manifest}"; \ + grep -q '"Route":"_framework/blazor\.web\.js"' "${manifest}"; \ + asset="$(find /out/wwwroot/_framework -maxdepth 1 -type f \ + \( -name 'blazor.web.js' -o -name 'blazor.web.*.js' \) \ + -size +10000c -print -quit)"; \ + test -n "${asset}"; \ + printf 'Verified Blazor bootstrap asset: %s (%s bytes)\n' \ + "${asset}" "$(wc -c <"${asset}")" + +FROM mcr.microsoft.com/dotnet/aspnet:${DOTNET_VERSION}-resolute AS runtime-base +ARG TRUTHGATE_UID +ARG TRUTHGATE_GID + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + ca-certificates \ + curl \ + gosu \ + jq \ + tini \ + && rm -rf /var/lib/apt/lists/* \ + && if ! getent group "${TRUTHGATE_GID}" >/dev/null; then groupadd --gid "${TRUTHGATE_GID}" truthgate; fi \ + && if existing_user="$(getent passwd "${TRUTHGATE_UID}" | cut -d: -f1)" && [ -n "${existing_user}" ]; then \ + usermod --login truthgate --home /home/truthgate --move-home --shell /usr/sbin/nologin "${existing_user}"; \ + else \ + useradd --uid "${TRUTHGATE_UID}" --gid "${TRUTHGATE_GID}" --create-home --shell /usr/sbin/nologin truthgate; \ + fi \ + && mkdir -p /home/truthgate/.aspnet \ + && ln -s /data/truthgate/secrets/data-protection-keys /home/truthgate/.aspnet/DataProtection-Keys + +COPY --from=kubo /usr/local/bin/ipfs /usr/local/bin/ipfs +COPY --chmod=0755 docker/entrypoint.sh /usr/local/bin/truthgate-entrypoint +COPY --chmod=0755 docker/healthcheck.sh /usr/local/bin/truthgate-healthcheck +COPY --chmod=0755 docker/kubo-configure.sh /usr/local/bin/truthgate-configure-kubo +COPY --chmod=0755 docker/kubo-status.sh /usr/local/bin/truthgate-kubo-status + +ENV ASPNETCORE_ENVIRONMENT=Production \ + DOTNET_ENVIRONMENT=Production \ + DOTNET_NOLOGO=true \ + DOTNET_SKIP_FIRST_TIME_EXPERIENCE=true \ + HOME=/home/truthgate \ + IPFS_PATH=/data/ipfs/repo \ + TMPDIR=/run/truthgate \ + TRUTHGATE_CERT_PATH=/data/truthgate/certificates \ + TRUTHGATE_CONFIG_PATH=/data/truthgate/config/config.json \ + TRUTHGATE_DATABASE_PATH=/data/truthgate/database \ + TRUTHGATE_KUBO_OVERRIDES_PATH=/data/truthgate/config/kubo-overrides.json \ + TRUTHGATE_KUBO_SETTINGS_PATH=/data/truthgate/config/kubo-settings.json \ + TRUTHGATE_STATE_PATH=/data/truthgate/state \ + TRUTHGATE_HEALTH_URL=https://127.0.0.1:443/ + +EXPOSE 80 443 4001/tcp 4001/udp + +ENTRYPOINT ["/usr/bin/tini", "--", "/usr/local/bin/truthgate-entrypoint"] +HEALTHCHECK --interval=30s --timeout=10s --start-period=90s --retries=3 \ + CMD ["/usr/local/bin/truthgate-healthcheck"] + +FROM runtime-base AS production +WORKDIR /app +COPY --from=build /out . +CMD ["production"] + +FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-resolute AS development +ARG TRUTHGATE_UID +ARG TRUTHGATE_GID + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + ca-certificates \ + curl \ + git \ + gosu \ + jq \ + tini \ + && rm -rf /var/lib/apt/lists/* \ + && if ! getent group "${TRUTHGATE_GID}" >/dev/null; then groupadd --gid "${TRUTHGATE_GID}" truthgate; fi \ + && if existing_user="$(getent passwd "${TRUTHGATE_UID}" | cut -d: -f1)" && [ -n "${existing_user}" ]; then \ + usermod --login truthgate --home /home/truthgate --move-home --shell /bin/bash "${existing_user}"; \ + else \ + useradd --uid "${TRUTHGATE_UID}" --gid "${TRUTHGATE_GID}" --create-home --shell /bin/bash truthgate; \ + fi \ + && mkdir -p /workspace /home/truthgate/.aspnet \ + && chown "${TRUTHGATE_UID}:${TRUTHGATE_GID}" /workspace \ + && ln -s /data/truthgate/secrets/data-protection-keys /home/truthgate/.aspnet/DataProtection-Keys + +COPY --from=kubo /usr/local/bin/ipfs /usr/local/bin/ipfs +COPY --chmod=0755 docker/entrypoint.sh /usr/local/bin/truthgate-entrypoint +COPY --chmod=0755 docker/healthcheck.sh /usr/local/bin/truthgate-healthcheck +COPY --chmod=0755 docker/kubo-configure.sh /usr/local/bin/truthgate-configure-kubo +COPY --chmod=0755 docker/kubo-status.sh /usr/local/bin/truthgate-kubo-status + +ENV ASPNETCORE_ENVIRONMENT=Development \ + DOTNET_ENVIRONMENT=Development \ + DOTNET_CLI_HOME=/tmp/dotnet \ + DOTNET_NOLOGO=true \ + DOTNET_SKIP_FIRST_TIME_EXPERIENCE=true \ + HOME=/home/truthgate \ + IPFS_PATH=/data/ipfs/repo \ + TMPDIR=/run/truthgate \ + TRUTHGATE_CERT_PATH=/data/truthgate/certificates \ + TRUTHGATE_CONFIG_PATH=/data/truthgate/config/config.json \ + TRUTHGATE_DATABASE_PATH=/data/truthgate/database \ + TRUTHGATE_DEV_PROJECT=/workspace/TruthGate-Web/TruthGate-Web/TruthGate-Web.csproj \ + TRUTHGATE_KUBO_OVERRIDES_PATH=/data/truthgate/config/kubo-overrides.json \ + TRUTHGATE_KUBO_SETTINGS_PATH=/data/truthgate/config/kubo-settings.json \ + TRUTHGATE_STATE_PATH=/data/truthgate/state \ + TRUTHGATE_HEALTH_URL=http://127.0.0.1:80/ + +WORKDIR /workspace +EXPOSE 80 4001/tcp 4001/udp +ENTRYPOINT ["/usr/bin/tini", "--", "/usr/local/bin/truthgate-entrypoint"] +HEALTHCHECK --interval=30s --timeout=10s --start-period=120s --retries=3 \ + CMD ["/usr/local/bin/truthgate-healthcheck"] +CMD ["development"] diff --git a/README.md b/README.md index bb26024..437c784 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,5 @@ # TruthGate - > The Secure, Self-Hosted Edge Gateway IPFS Always Needed, with Logins, API Keys, GUI Control, and Web3 Site Publishing. > **[truthgate.io](https://truthgate.io)** for full docs, guides, and live demos. @@ -19,17 +18,58 @@ --- +## Docker quick start + +TruthGate, Kubo, and the matching `ipfs` CLI run together as one appliance +container. Application state, Kubo repository metadata, and Kubo blocks are +persisted separately by default. + +```bash +cp .env.example .env +docker compose up --build -d +docker compose logs truthgate +``` + +The first-start logs contain a generated password for the `admin` account. +Open `https://localhost`, accept the temporary self-signed fallback certificate, +and change the password. + +The appliance configures Kubo as a contributing server by default: DHT server +mode, TCP/QUIC/WebTransport swarm listeners, content providing, automatic +storage sizing, and repository GC are enabled with persistent per-setting +overrides. Inspect the live node with: + +```bash +docker exec truthgate truthgate-kubo-status +``` + +For the full persistence contract, Kubo settings, image update flow, +ARM64/AMD64 publishing, and Docker-based development setup, see +**[DOCKER.md](DOCKER.md)**. + +Development with hot reload uses the production definition plus a small +override: + +```bash +docker compose -f compose.yaml -f compose.dev.yaml up --build +``` + +Then open `http://localhost:8080`. + +--- + ## What Is It? TruthGate is a **secure edge layer for IPFS nodes**. -Think **Netlify, but for IPFS**, self-hosted, login-protected, and actually yours. +Think **Netlify, but for IPFS**, self-hosted, login-protected, and actually yours. + +It solves the problems developers hit with IPFS: -It solves the problems developers hit with IPFS: -- Node exposure risks -- SSL/domain linking headaches -- CLI-only publishing -- Zero protection for `/api` or `/webui` routes -- Reliance on public gateways +- Node exposure risks +- SSL/domain linking headaches +- CLI-only publishing +- Zero protection for `/api` or `/webui` routes +- Reliance on public gateways [See how it works](https://truthgate.io) @@ -37,15 +77,15 @@ It solves the problems developers hit with IPFS: ## Get Started -Head to **[truthgate.io](https://truthgate.io)** for installation, configuration, and publishing guides. +Head to **[truthgate.io](https://truthgate.io)** for installation, configuration, and publishing guides. --- ## A Note from the Creator -I built this out of frustration. +I built this out of frustration. -I wanted a way to serve Web3-native apps that *actually worked*, securely, reliably, and without selling my soul to centralized hosts. Now it’s real. +I wanted a way to serve Web3-native apps that *actually worked*, securely, reliably, and without selling my soul to centralized hosts. Now it’s real. If you’ve ever wrestled with IPFS routing, SSL certs, or gateway hacks just to get your site online, **TruthGate is for you.** @@ -55,11 +95,11 @@ If you’ve ever wrestled with IPFS routing, SSL certs, or gateway hacks just to Pull requests welcome. Stars encourage me. -Issues are sacred. +Issues are sacred. -Let’s fix decentralized hosting together. +Let’s fix decentralized hosting together. -[truthgate.io](https://truthgate.io) +[truthgate.io](https://truthgate.io) --- diff --git a/TruthGate-Web/TruthGate-Web/Models/Config.cs b/TruthGate-Web/TruthGate-Web/Models/Config.cs index 4a88011..f5e6608 100644 --- a/TruthGate-Web/TruthGate-Web/Models/Config.cs +++ b/TruthGate-Web/TruthGate-Web/Models/Config.cs @@ -1,8 +1,12 @@ -namespace TruthGate_Web.Models +using TruthGate_Web.Utils; + +namespace TruthGate_Web.Models { public class Config { public const string DefaultAdminHash = "/EUxvODjrpkTKnal6nVEAh2m+52H4OgXGEBLcE3xilcgZ8gbeE5ay/CfzYr9PCJ0"; + public const string BootstrapAdminPasswordEnvironmentVariable = "TRUTHGATE_BOOTSTRAP_ADMIN_PASSWORD"; + private List? _users; public IpnsWildCardSubDomain IpnsWildCardSubDomain { get; set; } public List Domains { get; set; } = new List(); @@ -29,7 +33,7 @@ public List Users _users.Add(new UserAccount { UserName = "admin", - PasswordHashed = DefaultAdminHash + PasswordHashed = CreateInitialAdminHash() }); } @@ -41,6 +45,14 @@ public List Users _users = value is null ? new List() : new List(value); } } + + private static string CreateInitialAdminHash() + { + var bootstrapPassword = Environment.GetEnvironmentVariable(BootstrapAdminPasswordEnvironmentVariable); + return string.IsNullOrWhiteSpace(bootstrapPassword) + ? DefaultAdminHash + : StringHasher.HashString(bootstrapPassword); + } } public class IpnsWildCardSubDomain @@ -49,7 +61,7 @@ public class IpnsWildCardSubDomain public string UseSSL { get; set; } = "true"; } - public class EdgeDomain + public class EdgeDomain { public string Domain { get; set; } = ""; public string UseSSL { get; set; } = "false"; @@ -75,6 +87,7 @@ public class ApiKey public string Name { get; set; } public string KeyHashed { get; set; } } + public class UserAccount { public string UserName { get; set; } diff --git a/TruthGate-Web/TruthGate-Web/Security/RateLimiterRegistration.cs b/TruthGate-Web/TruthGate-Web/Security/RateLimiterRegistration.cs index 7da9060..d9141a8 100644 --- a/TruthGate-Web/TruthGate-Web/Security/RateLimiterRegistration.cs +++ b/TruthGate-Web/TruthGate-Web/Security/RateLimiterRegistration.cs @@ -1,4 +1,4 @@ -using TruthGate_Web.Middleware; +using TruthGate_Web.Middleware; using TruthGate_Web.Services; using Microsoft.EntityFrameworkCore; @@ -12,20 +12,38 @@ public static IServiceCollection AddTruthGateRateLimiter( { services.AddDbContextFactory((sp, b) => { - // Safe to resolve dependencies here var cfg = sp.GetRequiredService(); - var tet = cfg.Get(); - // Figure out SQLite location from cfg.ConfigPath (directory portion) - var dir = Path.GetDirectoryName(cfg.ConfigPath) ?? AppContext.BaseDirectory; - Directory.CreateDirectory(dir); - var dbPath = Path.Combine(dir, "ratelimiter.db"); + var configuredDatabasePath = Environment.GetEnvironmentVariable("TRUTHGATE_DATABASE_PATH"); + + string dbPath; + if (string.IsNullOrWhiteSpace(configuredDatabasePath)) + { + // Preserve the historical behavior for non-container installations. + var configDirectory = Path.GetDirectoryName(cfg.ConfigPath) ?? AppContext.BaseDirectory; + Directory.CreateDirectory(configDirectory); + dbPath = Path.Combine(configDirectory, "ratelimiter.db"); + } + else + { + var resolved = Path.GetFullPath(configuredDatabasePath); + if (string.Equals(Path.GetExtension(resolved), ".db", StringComparison.OrdinalIgnoreCase)) + { + Directory.CreateDirectory(Path.GetDirectoryName(resolved)!); + dbPath = resolved; + } + else + { + Directory.CreateDirectory(resolved); + dbPath = Path.Combine(resolved, "ratelimiter.db"); + } + } + var cs = connectionString ?? $"Data Source={dbPath};Cache=Shared"; b.UseSqlite(cs); b.EnableSensitiveDataLogging(false); }); - services.AddSingleton(); services.AddHostedService(); services.AddHostedService(); @@ -37,5 +55,4 @@ public static IServiceCollection AddTruthGateRateLimiter( public static IApplicationBuilder UseTruthGateRateLimiter(this IApplicationBuilder app) => app.UseMiddleware(); } - } diff --git a/TruthGate-Web/TruthGate-Web/TruthGate-Web.csproj b/TruthGate-Web/TruthGate-Web/TruthGate-Web.csproj index d18a0fb..0a9f586 100644 --- a/TruthGate-Web/TruthGate-Web/TruthGate-Web.csproj +++ b/TruthGate-Web/TruthGate-Web/TruthGate-Web.csproj @@ -6,6 +6,7 @@ enable TruthGate_Web $(AssemblyName.Replace(' ', '_')) + true @@ -32,4 +33,4 @@ - + \ No newline at end of file diff --git a/compose.dev.yaml b/compose.dev.yaml new file mode 100644 index 0000000..c540ebb --- /dev/null +++ b/compose.dev.yaml @@ -0,0 +1,42 @@ +services: + truthgate: + image: truthgate-ipfs:dev + build: + target: development + command: ["development"] + restart: "no" + read_only: false + working_dir: /workspace + environment: + ASPNETCORE_ENVIRONMENT: Development + DOTNET_ENVIRONMENT: Development + ASPNETCORE_URLS: http://0.0.0.0:80 + DOTNET_USE_POLLING_FILE_WATCHER: "1" + DOTNET_CLI_HOME: /home/truthgate + HOME: /home/truthgate + NUGET_PACKAGES: /home/truthgate/.nuget/packages + NUGET_HTTP_CACHE_PATH: /home/truthgate/.nuget/http-cache + TINI_SUBREAPER: "1" + TRUTHGATE_DEV_PROJECT: /workspace/TruthGate-Web/TruthGate-Web/TruthGate-Web.csproj + TRUTHGATE_HEALTH_URL: http://127.0.0.1:80/ + ports: !override + - "${TRUTHGATE_DEV_HTTP_PORT:-8080}:80/tcp" + - "${IPFS_SWARM_PORT:-4001}:4001/tcp" + - "${IPFS_SWARM_PORT:-4001}:4001/udp" + volumes: + - type: bind + source: . + target: /workspace + - type: volume + source: truthgate_nuget + target: /home/truthgate/.nuget/packages + - type: volume + source: truthgate_nuget_http + target: /home/truthgate/.nuget/http-cache + tmpfs: !override + - /tmp:rw,noexec,nosuid,size=1g + - /run/truthgate:rw,noexec,nosuid,size=64m + +volumes: + truthgate_nuget: + truthgate_nuget_http: diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..d01c7f3 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,79 @@ +name: truthgate + +services: + truthgate: + image: ${TRUTHGATE_IMAGE:-ghcr.io/magiccodingman/truthgate-ipfs:master} + build: + context: . + dockerfile: Dockerfile + target: production + args: + DOTNET_VERSION: ${DOTNET_VERSION:-10.0} + KUBO_VERSION: ${KUBO_VERSION:-v0.42.0} + TRUTHGATE_UID: ${TRUTHGATE_UID:-1000} + TRUTHGATE_GID: ${TRUTHGATE_GID:-1000} + container_name: ${TRUTHGATE_CONTAINER_NAME:-truthgate} + restart: unless-stopped + read_only: true + security_opt: + - no-new-privileges:true + sysctls: + net.ipv4.ip_unprivileged_port_start: "0" + environment: + ASPNETCORE_ENVIRONMENT: Production + DOTNET_ENVIRONMENT: Production + IPFS_PATH: /data/ipfs/repo + IPFS_SWARM_PORT: ${IPFS_SWARM_PORT:-4001} + TMPDIR: /run/truthgate + TRUTHGATE_ACME_STAGING: ${TRUTHGATE_ACME_STAGING:-false} + TRUTHGATE_CERT_IPS: ${TRUTHGATE_CERT_IPS:-} + TRUTHGATE_CERT_PATH: /data/truthgate/certificates + TRUTHGATE_CONFIG_PATH: /data/truthgate/config/config.json + TRUTHGATE_DATABASE_PATH: /data/truthgate/database + TRUTHGATE_STATE_PATH: /data/truthgate/state + TRUTHGATE_HEALTH_URL: https://127.0.0.1:443/ + TRUTHGATE_KUBO_SETTINGS_PATH: /data/truthgate/config/kubo-settings.json + TRUTHGATE_KUBO_OVERRIDES_PATH: /data/truthgate/config/kubo-overrides.json + TRUTHGATE_KUBO_APPLY_SERVER_PROFILE: "${TRUTHGATE_KUBO_APPLY_SERVER_PROFILE:-}" + TRUTHGATE_KUBO_ENSURE_SWARM_LISTENERS: "${TRUTHGATE_KUBO_ENSURE_SWARM_LISTENERS:-}" + TRUTHGATE_KUBO_ROUTING_TYPE: "${TRUTHGATE_KUBO_ROUTING_TYPE:-}" + TRUTHGATE_KUBO_HOLE_PUNCHING: "${TRUTHGATE_KUBO_HOLE_PUNCHING:-}" + TRUTHGATE_KUBO_RELAY_CLIENT: "${TRUTHGATE_KUBO_RELAY_CLIENT:-}" + TRUTHGATE_KUBO_PROVIDE_ENABLED: "${TRUTHGATE_KUBO_PROVIDE_ENABLED:-}" + TRUTHGATE_KUBO_PROVIDE_STRATEGY: "${TRUTHGATE_KUBO_PROVIDE_STRATEGY:-}" + TRUTHGATE_KUBO_PROVIDE_INTERVAL: "${TRUTHGATE_KUBO_PROVIDE_INTERVAL:-}" + TRUTHGATE_KUBO_PROVIDE_SWEEP: "${TRUTHGATE_KUBO_PROVIDE_SWEEP:-}" + TRUTHGATE_KUBO_PROVIDE_RESUME: "${TRUTHGATE_KUBO_PROVIDE_RESUME:-}" + TRUTHGATE_KUBO_STORAGE_MAX: "${TRUTHGATE_KUBO_STORAGE_MAX:-}" + TRUTHGATE_KUBO_STORAGE_PERCENT: "${TRUTHGATE_KUBO_STORAGE_PERCENT:-}" + TRUTHGATE_KUBO_STORAGE_FALLBACK: "${TRUTHGATE_KUBO_STORAGE_FALLBACK:-}" + TRUTHGATE_KUBO_STORAGE_GC_WATERMARK: "${TRUTHGATE_KUBO_STORAGE_GC_WATERMARK:-}" + TRUTHGATE_KUBO_ENABLE_GC: "${TRUTHGATE_KUBO_ENABLE_GC:-}" + TRUTHGATE_KUBO_PUBLIC_IPV4: "${TRUTHGATE_KUBO_PUBLIC_IPV4:-}" + TRUTHGATE_KUBO_PUBLIC_IPV6: "${TRUTHGATE_KUBO_PUBLIC_IPV6:-}" + TRUTHGATE_KUBO_ANNOUNCE_PORT: "${TRUTHGATE_KUBO_ANNOUNCE_PORT:-}" + ports: + - "${TRUTHGATE_HTTP_PORT:-80}:80/tcp" + - "${TRUTHGATE_HTTPS_PORT:-443}:443/tcp" + - "${IPFS_SWARM_PORT:-4001}:4001/tcp" + - "${IPFS_SWARM_PORT:-4001}:4001/udp" + volumes: + - type: bind + source: ${TRUTHGATE_DATA_HOST_PATH:-./data/truthgate} + target: /data/truthgate + - type: bind + source: ${IPFS_REPO_HOST_PATH:-./data/ipfs/repo} + target: /data/ipfs/repo + - type: bind + source: ${IPFS_BLOCKS_HOST_PATH:-./data/ipfs/blocks} + target: /data/ipfs/repo/blocks + tmpfs: + - /tmp:rw,noexec,nosuid,size=256m + - /run/truthgate:rw,noexec,nosuid,size=64m + healthcheck: + test: ["CMD", "/usr/local/bin/truthgate-healthcheck"] + interval: 30s + timeout: 10s + start_period: 90s + retries: 3 + stop_grace_period: 45s diff --git a/data/.gitignore b/data/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/data/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100644 index 0000000..a9de22d --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,243 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +mode="${1:-production}" +truthgate_user="truthgate" +truthgate_group="$(id -g "${truthgate_user}")" + +log() { + printf '[truthgate] %s\n' "$*" +} + +fail() { + printf '[truthgate] ERROR: %s\n' "$*" >&2 + exit 1 +} + +as_truthgate() { + gosu "${truthgate_user}:${truthgate_group}" "$@" +} + +require_absolute_path() { + local name="$1" + local value="$2" + [[ "${value}" = /* ]] || fail "${name} must be an absolute path; received '${value}'." +} + +: "${IPFS_PATH:=/data/ipfs/repo}" +: "${TRUTHGATE_CONFIG_PATH:=/data/truthgate/config/config.json}" +: "${TRUTHGATE_DATABASE_PATH:=/data/truthgate/database}" +: "${TRUTHGATE_CERT_PATH:=/data/truthgate/certificates}" +: "${TRUTHGATE_STATE_PATH:=/data/truthgate/state}" +: "${TMPDIR:=/run/truthgate}" +: "${TRUTHGATE_KUBO_REPO_VERSION:=18}" + +export IPFS_PATH TRUTHGATE_CONFIG_PATH TRUTHGATE_DATABASE_PATH +export TRUTHGATE_CERT_PATH TRUTHGATE_STATE_PATH TMPDIR + +require_absolute_path IPFS_PATH "${IPFS_PATH}" +require_absolute_path TRUTHGATE_CONFIG_PATH "${TRUTHGATE_CONFIG_PATH}" +require_absolute_path TRUTHGATE_DATABASE_PATH "${TRUTHGATE_DATABASE_PATH}" +require_absolute_path TRUTHGATE_CERT_PATH "${TRUTHGATE_CERT_PATH}" +require_absolute_path TRUTHGATE_STATE_PATH "${TRUTHGATE_STATE_PATH}" +require_absolute_path TMPDIR "${TMPDIR}" +[[ "${TRUTHGATE_KUBO_REPO_VERSION}" =~ ^[0-9]+$ ]] \ + || fail "TRUTHGATE_KUBO_REPO_VERSION must be a positive integer." + +config_directory="$(dirname "${TRUTHGATE_CONFIG_PATH}")" +blocks_directory="${IPFS_PATH}/blocks" +data_protection_directory="/data/truthgate/secrets/data-protection-keys" + +: "${TRUTHGATE_KUBO_SETTINGS_PATH:=${config_directory}/kubo-settings.json}" +: "${TRUTHGATE_KUBO_OVERRIDES_PATH:=${config_directory}/kubo-overrides.json}" +require_absolute_path TRUTHGATE_KUBO_SETTINGS_PATH "${TRUTHGATE_KUBO_SETTINGS_PATH}" +require_absolute_path TRUTHGATE_KUBO_OVERRIDES_PATH "${TRUTHGATE_KUBO_OVERRIDES_PATH}" +export TRUTHGATE_KUBO_SETTINGS_PATH TRUTHGATE_KUBO_OVERRIDES_PATH + +install -d -m 0750 -o "${truthgate_user}" -g "${truthgate_group}" \ + "${config_directory}" \ + "${TRUTHGATE_DATABASE_PATH}" \ + "${TRUTHGATE_CERT_PATH}" \ + "${TRUTHGATE_STATE_PATH}" \ + "${data_protection_directory}" \ + "${IPFS_PATH}" \ + "${blocks_directory}" \ + "${TMPDIR}" + +# Named volumes are created as root-owned directories. Development tools such as +# NuGet run as the non-root truthgate user, so initialize the complete NuGet home +# (including its configuration directory) before dropping privileges. +if [[ "${mode}" == "development" ]]; then + : "${HOME:=/home/truthgate}" + : "${DOTNET_CLI_HOME:=${HOME}}" + : "${NUGET_PACKAGES:=${HOME}/.nuget/packages}" + : "${NUGET_HTTP_CACHE_PATH:=${HOME}/.nuget/http-cache}" + + export HOME DOTNET_CLI_HOME NUGET_PACKAGES NUGET_HTTP_CACHE_PATH + + nuget_root="${HOME}/.nuget" + nuget_config_directory="${nuget_root}/NuGet" + + install -d -m 0750 -o "${truthgate_user}" -g "${truthgate_group}" \ + "${HOME}" \ + "${nuget_root}" \ + "${nuget_config_directory}" \ + "${NUGET_PACKAGES}" \ + "${NUGET_HTTP_CACHE_PATH}" + + chown "${truthgate_user}:${truthgate_group}" \ + "${HOME}" \ + "${nuget_root}" \ + "${nuget_config_directory}" \ + "${NUGET_PACKAGES}" \ + "${NUGET_HTTP_CACHE_PATH}" +fi + +# The default Compose layout mounts the repo and blockstore separately. Changing +# ownership on the mount roots is cheap and avoids recursively walking a large +# existing blockstore on every container start. +chown "${truthgate_user}:${truthgate_group}" \ + "${IPFS_PATH}" \ + "${blocks_directory}" \ + "${config_directory}" \ + "${TRUTHGATE_DATABASE_PATH}" \ + "${TRUTHGATE_CERT_PATH}" \ + "${TRUTHGATE_STATE_PATH}" \ + "${data_protection_directory}" \ + "${TMPDIR}" + +migrate_existing_kubo_repository() { + local repo_config="${IPFS_PATH}/config" + local repo_version_file="${IPFS_PATH}/version" + local current_version migrated_version + + # Fresh repositories are initialized later by truthgate-configure-kubo and + # already use the format expected by the bundled Kubo binary. + [[ -s "${repo_config}" ]] || return 0 + + [[ -s "${repo_version_file}" ]] \ + || fail "Existing Kubo repository is missing its version file: ${repo_version_file}" + + current_version="$(tr -d '[:space:]' <"${repo_version_file}")" + [[ "${current_version}" =~ ^[0-9]+$ ]] \ + || fail "Kubo repository version is invalid: '${current_version}'." + + if (( current_version > TRUTHGATE_KUBO_REPO_VERSION )); then + fail "Kubo repository version ${current_version} is newer than the bundled Kubo supports (${TRUTHGATE_KUBO_REPO_VERSION}). Upgrade the TruthGate image before starting this repository." + fi + + if (( current_version == TRUTHGATE_KUBO_REPO_VERSION )); then + log "Kubo repository is already at version ${current_version}." + return 0 + fi + + log "Migrating Kubo repository from version ${current_version} to ${TRUTHGATE_KUBO_REPO_VERSION} before applying managed configuration." + as_truthgate ipfs repo migrate --to="${TRUTHGATE_KUBO_REPO_VERSION}" + + migrated_version="$(tr -d '[:space:]' <"${repo_version_file}")" + [[ "${migrated_version}" == "${TRUTHGATE_KUBO_REPO_VERSION}" ]] \ + || fail "Kubo repository migration completed without producing expected version ${TRUTHGATE_KUBO_REPO_VERSION}; found '${migrated_version}'." + + log "Kubo repository migration completed at version ${migrated_version}." +} + +migrate_existing_kubo_repository + +bootstrap_password_file="${TRUTHGATE_STATE_PATH}/bootstrap-admin-password" +if [[ ! -s "${TRUTHGATE_CONFIG_PATH}" ]]; then + if [[ -n "${TRUTHGATE_BOOTSTRAP_ADMIN_PASSWORD:-}" ]]; then + bootstrap_password="${TRUTHGATE_BOOTSTRAP_ADMIN_PASSWORD}" + elif [[ -s "${bootstrap_password_file}" ]]; then + bootstrap_password="$(<"${bootstrap_password_file}")" + else + bootstrap_password="$(od -An -N24 -tx1 /dev/urandom | tr -d ' \n')" + umask 077 + printf '%s' "${bootstrap_password}" >"${bootstrap_password_file}" + chown "${truthgate_user}:${truthgate_group}" "${bootstrap_password_file}" + fi + + export TRUTHGATE_BOOTSTRAP_ADMIN_PASSWORD="${bootstrap_password}" + log "First-run administrator account: admin" + log "First-run administrator password: ${bootstrap_password}" + log "The password is retained at ${bootstrap_password_file} until the TruthGate config is persisted." +else + rm -f "${bootstrap_password_file}" + unset TRUTHGATE_BOOTSTRAP_ADMIN_PASSWORD || true +fi + +/usr/local/bin/truthgate-configure-kubo + +daemon_args=(daemon --migrate=true) +if [[ "$(<"${TMPDIR}/kubo-enable-gc")" == "true" ]]; then + daemon_args+=(--enable-gc) +fi + +log "Starting Kubo with automatic repository migrations enabled." +as_truthgate ipfs "${daemon_args[@]}" & +ipfs_pid=$! + +stop_children() { + trap - TERM INT + + if [[ -n "${app_pid:-}" ]] && kill -0 "${app_pid}" 2>/dev/null; then + kill -TERM "${app_pid}" 2>/dev/null || true + fi + + if kill -0 "${ipfs_pid}" 2>/dev/null; then + kill -TERM "${ipfs_pid}" 2>/dev/null || true + fi + + [[ -z "${app_pid:-}" ]] || wait "${app_pid}" 2>/dev/null || true + wait "${ipfs_pid}" 2>/dev/null || true +} + +trap 'stop_children; exit 143' TERM +trap 'stop_children; exit 130' INT + +kubo_ready=false +for _ in $(seq 1 120); do + if as_truthgate ipfs --api=/ip4/127.0.0.1/tcp/5001 id >/dev/null 2>&1; then + kubo_ready=true + break + fi + + if ! kill -0 "${ipfs_pid}" 2>/dev/null; then + wait "${ipfs_pid}" || true + fail "Kubo exited before its RPC API became ready." + fi + + sleep 1 +done + +[[ "${kubo_ready}" == true ]] || fail "Kubo did not become ready within 120 seconds." +log "Kubo is ready." + +case "${mode}" in + production) + [[ -f /app/TruthGate-Web.dll ]] || fail "Production application was not found at /app/TruthGate-Web.dll." + log "Starting TruthGate in production mode." + as_truthgate dotnet /app/TruthGate-Web.dll & + ;; + development) + project="${TRUTHGATE_DEV_PROJECT:-/workspace/TruthGate-Web/TruthGate-Web/TruthGate-Web.csproj}" + [[ -f "${project}" ]] || fail "Development project was not found at ${project}. Is the repository mounted at /workspace?" + export ASPNETCORE_URLS="${ASPNETCORE_URLS:-http://0.0.0.0:80}" + export DOTNET_USE_POLLING_FILE_WATCHER="${DOTNET_USE_POLLING_FILE_WATCHER:-1}" + log "Starting TruthGate with dotnet watch (${project})." + as_truthgate dotnet watch --project "${project}" run --no-launch-profile --urls "${ASPNETCORE_URLS}" & + ;; + *) + fail "Unknown run mode '${mode}'. Expected 'production' or 'development'." + ;; +esac + +app_pid=$! + +set +e +wait -n "${ipfs_pid}" "${app_pid}" +exit_code=$? +set -e + +log "A managed process exited with status ${exit_code}; stopping the remaining process." +stop_children +exit "${exit_code}" diff --git a/docker/healthcheck.sh b/docker/healthcheck.sh new file mode 100644 index 0000000..4172a47 --- /dev/null +++ b/docker/healthcheck.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +ipfs --api=/ip4/127.0.0.1/tcp/5001 diag healthy >/dev/null + +health_url="${TRUTHGATE_HEALTH_URL:-https://127.0.0.1:443/}" +http_status="$(curl \ + --silent \ + --show-error \ + --insecure \ + --max-time 5 \ + --output /dev/null \ + --write-out '%{http_code}' \ + "${health_url}")" + +[[ "${http_status}" =~ ^[0-9]{3}$ ]] +(( 10#${http_status} >= 200 && 10#${http_status} < 500 )) diff --git a/docker/kubo-configure.sh b/docker/kubo-configure.sh new file mode 100644 index 0000000..074a137 --- /dev/null +++ b/docker/kubo-configure.sh @@ -0,0 +1,323 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +truthgate_user="truthgate" +truthgate_group="$(id -g "${truthgate_user}")" + +log() { printf '[truthgate] %s\n' "$*"; } +warn() { printf '[truthgate] WARNING: %s\n' "$*" >&2; } +fail() { printf '[truthgate] ERROR: %s\n' "$*" >&2; exit 1; } +as_truthgate() { gosu "${truthgate_user}:${truthgate_group}" "$@"; } + +normalize_bool() { + case "${1,,}" in + 1|true|yes|on) printf 'true' ;; + 0|false|no|off) printf 'false' ;; + *) return 1 ;; + esac +} + +json_setting() { + local filter="$1" fallback="$2" value + value="$(jq -er "${filter} // empty" "${TRUTHGATE_KUBO_SETTINGS_PATH}" 2>/dev/null || true)" + [[ -n "${value}" ]] && printf '%s' "${value}" || printf '%s' "${fallback}" +} + +setting() { + local environment_name="$1" filter="$2" fallback="$3" + local environment_value="${!environment_name:-}" + [[ -n "${environment_value}" ]] \ + && printf '%s' "${environment_value}" \ + || json_setting "${filter}" "${fallback}" +} + +set_string() { + as_truthgate ipfs config --json "$1" "$(jq -cn --arg value "$2" '$value')" +} + +set_json() { + as_truthgate ipfs config --json "$1" "$2" +} + +ensure_array_value() { + local key="$1" item="$2" current compact updated + current="$(as_truthgate ipfs config --json "${key}" 2>/dev/null || printf '[]')" + compact="$(jq -c . <<<"${current}")" + updated="$(jq -c --arg item "${item}" \ + 'if type != "array" then [$item] elif index($item) then . else . + [$item] end' \ + <<<"${current}")" + [[ "${updated}" == "${compact}" ]] || set_json "${key}" "${updated}" +} + +valid_ipv4() { + local a b c d extra octet + IFS=. read -r a b c d extra <<<"$1" + [[ -z "${extra:-}" && -n "${a:-}" && -n "${b:-}" && -n "${c:-}" && -n "${d:-}" ]] || return 1 + for octet in "${a}" "${b}" "${c}" "${d}"; do + [[ "${octet}" =~ ^[0-9]{1,3}$ ]] || return 1 + (( 10#${octet} <= 255 )) || return 1 + done +} + +valid_ipv6() { + [[ "$1" == *:* && "$1" =~ ^[0-9A-Fa-f:.]+$ ]] +} + +resolve_public_address() { + local family="$1" requested="$2" address="" + case "${requested,,}" in + ""|off|none|disabled|false) return 0 ;; + auto) + if [[ "${family}" == "4" ]]; then + address="$(curl -4fsS --max-time 5 https://api.ipify.org 2>/dev/null || true)" + else + address="$(curl -6fsS --max-time 5 https://api6.ipify.org 2>/dev/null || true)" + fi + address="${address//$'\r'/}" + address="${address//$'\n'/}" + ;; + *) address="${requested}" ;; + esac + + if [[ -z "${address}" ]]; then + warn "Could not detect a public IPv${family} address; no managed IPv${family} announce address will be added." + return 0 + fi + + if [[ "${family}" == "4" ]]; then + valid_ipv4 "${address}" || fail "Invalid public IPv4 address: '${address}'." + else + valid_ipv6 "${address}" || fail "Invalid public IPv6 address: '${address}'." + fi + printf '%s' "${address}" +} + +human_bytes() { + command -v numfmt >/dev/null 2>&1 \ + && numfmt --to=iec-i --suffix=B "$1" \ + || printf '%sB' "$1" +} + +: "${IPFS_PATH:=/data/ipfs/repo}" +: "${TRUTHGATE_CONFIG_PATH:=/data/truthgate/config/config.json}" +: "${TRUTHGATE_STATE_PATH:=/data/truthgate/state}" +: "${TMPDIR:=/run/truthgate}" + +config_directory="$(dirname "${TRUTHGATE_CONFIG_PATH}")" +blocks_directory="${IPFS_PATH}/blocks" +: "${TRUTHGATE_KUBO_SETTINGS_PATH:=${config_directory}/kubo-settings.json}" +: "${TRUTHGATE_KUBO_OVERRIDES_PATH:=${config_directory}/kubo-overrides.json}" +export IPFS_PATH TRUTHGATE_KUBO_SETTINGS_PATH TRUTHGATE_KUBO_OVERRIDES_PATH + +if [[ ! -e "${TRUTHGATE_KUBO_SETTINGS_PATH}" ]]; then + cat >"${TRUTHGATE_KUBO_SETTINGS_PATH}" <<'JSON' +{ + "serverProfile": true, + "routingType": "dhtserver", + "ensureSwarmListeners": true, + "holePunching": true, + "relayClient": true, + "provide": { + "enabled": true, + "strategy": "all", + "interval": "22h", + "sweepEnabled": true, + "resumeEnabled": true + }, + "storage": { + "max": "auto", + "percent": 90, + "fallback": "200GB", + "gcWatermark": 90, + "enableGc": true + }, + "publicAnnounce": { + "ipv4": "auto", + "ipv6": "auto", + "port": null + } +} +JSON + chown "${truthgate_user}:${truthgate_group}" "${TRUTHGATE_KUBO_SETTINGS_PATH}" + chmod 0640 "${TRUTHGATE_KUBO_SETTINGS_PATH}" +fi +jq -e 'type == "object"' "${TRUTHGATE_KUBO_SETTINGS_PATH}" >/dev/null \ + || fail "Kubo settings file is not a valid JSON object: ${TRUTHGATE_KUBO_SETTINGS_PATH}" + +if [[ ! -e "${TRUTHGATE_KUBO_OVERRIDES_PATH}" ]]; then + printf '{}\n' >"${TRUTHGATE_KUBO_OVERRIDES_PATH}" + chown "${truthgate_user}:${truthgate_group}" "${TRUTHGATE_KUBO_OVERRIDES_PATH}" + chmod 0640 "${TRUTHGATE_KUBO_OVERRIDES_PATH}" +fi +jq -e 'type == "object"' "${TRUTHGATE_KUBO_OVERRIDES_PATH}" >/dev/null \ + || fail "Kubo overrides file is not a valid JSON object: ${TRUTHGATE_KUBO_OVERRIDES_PATH}" + +server_profile_raw="$(setting TRUTHGATE_KUBO_APPLY_SERVER_PROFILE '.serverProfile' true)" +server_profile="$(normalize_bool "${server_profile_raw}")" \ + || fail "TRUTHGATE_KUBO_APPLY_SERVER_PROFILE must be true or false." +server_profile_marker="${TRUTHGATE_STATE_PATH}/kubo-server-profile-v1" + +if [[ ! -s "${IPFS_PATH}/config" ]]; then + if [[ "${server_profile}" == "true" ]]; then + log "Initializing a new Kubo repository with the public-server profile." + as_truthgate ipfs init --profile server + printf 'applied during ipfs init\n' >"${server_profile_marker}" + chown "${truthgate_user}:${truthgate_group}" "${server_profile_marker}" + else + log "Initializing a new Kubo repository without the server profile." + as_truthgate ipfs init + fi +else + log "Using the existing Kubo repository at ${IPFS_PATH}." + if [[ "${server_profile}" == "true" && ! -e "${server_profile_marker}" ]]; then + log "Applying Kubo's public-server profile to the existing repository." + as_truthgate ipfs config profile apply server + printf 'applied to existing repository\n' >"${server_profile_marker}" + chown "${truthgate_user}:${truthgate_group}" "${server_profile_marker}" + fi +fi + +set_string Routing.Type "$(setting TRUTHGATE_KUBO_ROUTING_TYPE '.routingType' dhtserver)" + +ensure_swarm_raw="$(setting TRUTHGATE_KUBO_ENSURE_SWARM_LISTENERS '.ensureSwarmListeners' true)" +ensure_swarm="$(normalize_bool "${ensure_swarm_raw}")" \ + || fail "TRUTHGATE_KUBO_ENSURE_SWARM_LISTENERS must be true or false." +if [[ "${ensure_swarm}" == "true" ]]; then + for address in \ + /ip4/0.0.0.0/tcp/4001 \ + /ip6/::/tcp/4001 \ + /ip4/0.0.0.0/udp/4001/quic-v1 \ + /ip4/0.0.0.0/udp/4001/quic-v1/webtransport \ + /ip6/::/udp/4001/quic-v1 \ + /ip6/::/udp/4001/quic-v1/webtransport + do + ensure_array_value Addresses.Swarm "${address}" + done +fi + +for spec in \ + "TRUTHGATE_KUBO_HOLE_PUNCHING|.holePunching|true|Swarm.EnableHolePunching" \ + "TRUTHGATE_KUBO_RELAY_CLIENT|.relayClient|true|Swarm.RelayClient.Enabled" \ + "TRUTHGATE_KUBO_PROVIDE_ENABLED|.provide.enabled|true|Provide.Enabled" \ + "TRUTHGATE_KUBO_PROVIDE_SWEEP|.provide.sweepEnabled|true|Provide.DHT.SweepEnabled" \ + "TRUTHGATE_KUBO_PROVIDE_RESUME|.provide.resumeEnabled|true|Provide.DHT.ResumeEnabled" +do + IFS='|' read -r env_name filter fallback key <<<"${spec}" + raw="$(setting "${env_name}" "${filter}" "${fallback}")" + value="$(normalize_bool "${raw}")" || fail "${env_name} must be true or false." + set_json "${key}" "${value}" +done + +set_string Provide.Strategy "$(setting TRUTHGATE_KUBO_PROVIDE_STRATEGY '.provide.strategy' all)" +set_string Provide.DHT.Interval "$(setting TRUTHGATE_KUBO_PROVIDE_INTERVAL '.provide.interval' 22h)" + +storage_max_requested="$(setting TRUTHGATE_KUBO_STORAGE_MAX '.storage.max' auto)" +storage_percent="$(setting TRUTHGATE_KUBO_STORAGE_PERCENT '.storage.percent' 90)" +storage_fallback="$(setting TRUTHGATE_KUBO_STORAGE_FALLBACK '.storage.fallback' 200GB)" +storage_gc_watermark="$(setting TRUTHGATE_KUBO_STORAGE_GC_WATERMARK '.storage.gcWatermark' 90)" + +[[ "${storage_percent}" =~ ^[0-9]+$ ]] && (( storage_percent >= 1 && storage_percent <= 100 )) \ + || fail "TRUTHGATE_KUBO_STORAGE_PERCENT must be an integer from 1 through 100." +[[ "${storage_gc_watermark}" =~ ^[0-9]+$ ]] && (( storage_gc_watermark <= 100 )) \ + || fail "TRUTHGATE_KUBO_STORAGE_GC_WATERMARK must be an integer from 0 through 100." + +storage_source="fixed" +if [[ "${storage_max_requested,,}" == "auto" ]]; then + storage_total_bytes="$(df -PB1 -- "${blocks_directory}" 2>/dev/null | awk 'NR == 2 { print $2 }' || true)" + if [[ "${storage_total_bytes}" =~ ^[0-9]+$ && "${storage_total_bytes}" -gt 0 ]]; then + storage_max_effective="$(( storage_total_bytes * storage_percent / 100 ))B" + storage_source="auto (${storage_percent}% of $(human_bytes "${storage_total_bytes}"))" + else + storage_max_effective="${storage_fallback}" + storage_source="fallback because filesystem capacity detection failed" + fi +else + storage_max_effective="${storage_max_requested}" +fi +set_string Datastore.StorageMax "${storage_max_effective}" +set_json Datastore.StorageGCWatermark "${storage_gc_watermark}" + +enable_gc_raw="$(setting TRUTHGATE_KUBO_ENABLE_GC '.storage.enableGc' true)" +enable_gc="$(normalize_bool "${enable_gc_raw}")" \ + || fail "TRUTHGATE_KUBO_ENABLE_GC must be true or false." +printf '%s\n' "${enable_gc}" >"${TMPDIR}/kubo-enable-gc" + +announce_port="${TRUTHGATE_KUBO_ANNOUNCE_PORT:-}" +[[ -n "${announce_port}" ]] || announce_port="$(json_setting '.publicAnnounce.port' '')" +[[ -n "${announce_port}" && "${announce_port}" != "null" ]] || announce_port="${IPFS_SWARM_PORT:-4001}" +[[ "${announce_port}" =~ ^[0-9]+$ ]] && (( announce_port >= 1 && announce_port <= 65535 )) \ + || fail "TRUTHGATE_KUBO_ANNOUNCE_PORT must be an integer from 1 through 65535." + +public_ipv4="$(resolve_public_address 4 "$(setting TRUTHGATE_KUBO_PUBLIC_IPV4 '.publicAnnounce.ipv4' auto)")" +public_ipv6="$(resolve_public_address 6 "$(setting TRUTHGATE_KUBO_PUBLIC_IPV6 '.publicAnnounce.ipv6' auto)")" + +managed_addresses=() +if [[ -n "${public_ipv4}" ]]; then + managed_addresses+=( + "/ip4/${public_ipv4}/tcp/${announce_port}" + "/ip4/${public_ipv4}/udp/${announce_port}/quic-v1" + "/ip4/${public_ipv4}/udp/${announce_port}/quic-v1/webtransport" + ) +fi +if [[ -n "${public_ipv6}" ]]; then + managed_addresses+=( + "/ip6/${public_ipv6}/tcp/${announce_port}" + "/ip6/${public_ipv6}/udp/${announce_port}/quic-v1" + "/ip6/${public_ipv6}/udp/${announce_port}/quic-v1/webtransport" + ) +fi + +managed_state="${TRUTHGATE_STATE_PATH}/kubo-managed-append-announce.json" +current="$(as_truthgate ipfs config --json Addresses.AppendAnnounce 2>/dev/null || printf '[]')" +previous='[]' +if [[ -s "${managed_state}" ]]; then + previous="$(cat "${managed_state}")" + jq -e 'type == "array"' <<<"${previous}" >/dev/null || previous='[]' +fi +without_previous="$(jq -c --argjson previous "${previous}" \ + '. as $current | [$current[] | select(. as $item | ($previous | index($item)) == null)]' \ + <<<"${current}")" +if (( ${#managed_addresses[@]} > 0 )); then + desired="$(printf '%s\n' "${managed_addresses[@]}" | jq -R . | jq -sc .)" +else + desired='[]' +fi +updated="$(jq -c --argjson desired "${desired}" \ + 'reduce $desired[] as $item (. ; if index($item) then . else . + [$item] end)' \ + <<<"${without_previous}")" +set_json Addresses.AppendAnnounce "${updated}" +printf '%s\n' "${desired}" >"${managed_state}" +chown "${truthgate_user}:${truthgate_group}" "${managed_state}" +chmod 0640 "${managed_state}" + +while IFS= read -r entry; do + key="$(jq -r '.key' <<<"${entry}")" + value="$(jq -c '.value' <<<"${entry}")" + case "${key}" in + Addresses.API|Addresses.Gateway) + warn "Ignoring protected Kubo override '${key}'; TruthGate requires loopback-only listeners." + ;; + *) + log "Applying advanced Kubo override: ${key}" + set_json "${key}" "${value}" + ;; + esac +done < <(jq -c 'to_entries[]' "${TRUTHGATE_KUBO_OVERRIDES_PATH}") + +set_string Addresses.API /ip4/127.0.0.1/tcp/5001 +set_string Addresses.Gateway /ip4/127.0.0.1/tcp/9010 + +log "Kubo server configuration:" +log " Routing.Type: $(as_truthgate ipfs config Routing.Type)" +log " Swarm listeners: $(as_truthgate ipfs config --json Addresses.Swarm | jq -c .)" +log " Managed public announce IPv4: ${public_ipv4:-disabled or unavailable}" +log " Managed public announce IPv6: ${public_ipv6:-disabled or unavailable}" +log " Public announce port: ${announce_port}" +log " Provide.Enabled: $(as_truthgate ipfs config Provide.Enabled)" +log " Provide.Strategy: $(as_truthgate ipfs config Provide.Strategy)" +log " Provide.DHT.Interval: $(as_truthgate ipfs config Provide.DHT.Interval)" +log " Provide.DHT.SweepEnabled: $(as_truthgate ipfs config Provide.DHT.SweepEnabled)" +log " Provide.DHT.ResumeEnabled: $(as_truthgate ipfs config Provide.DHT.ResumeEnabled)" +log " Datastore.StorageMax: $(as_truthgate ipfs config Datastore.StorageMax) (${storage_source})" +log " Datastore.StorageGCWatermark: $(as_truthgate ipfs config Datastore.StorageGCWatermark)%" +log " Automatic repository GC: ${enable_gc}" diff --git a/docker/kubo-status.sh b/docker/kubo-status.sh new file mode 100644 index 0000000..cd91767 --- /dev/null +++ b/docker/kubo-status.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +: "${IPFS_PATH:=/data/ipfs/repo}" +api_address="/ip4/127.0.0.1/tcp/5001" + +config_value() { + ipfs config "$1" 2>/dev/null || printf '' +} + +config_json() { + ipfs config --json "$1" 2>/dev/null | jq -c . || printf '' +} + +printf 'TruthGate Kubo status\n' +printf '=====================\n' +printf 'Repository: %s\n' "${IPFS_PATH}" +printf 'Kubo version: %s\n' "$(ipfs version --number 2>/dev/null || printf '')" +printf 'Routing.Type: %s\n' "$(config_value Routing.Type)" +printf 'Addresses.API: %s\n' "$(config_value Addresses.API)" +printf 'Addresses.Gateway: %s\n' "$(config_value Addresses.Gateway)" +printf 'Addresses.Swarm: %s\n' "$(config_json Addresses.Swarm)" +printf 'Addresses.AppendAnnounce: %s\n' "$(config_json Addresses.AppendAnnounce)" +printf 'Swarm.EnableHolePunching: %s\n' "$(config_value Swarm.EnableHolePunching)" +printf 'Swarm.RelayClient.Enabled: %s\n' "$(config_value Swarm.RelayClient.Enabled)" +printf 'Provide.Enabled: %s\n' "$(config_value Provide.Enabled)" +printf 'Provide.Strategy: %s\n' "$(config_value Provide.Strategy)" +printf 'Provide.DHT.Interval: %s\n' "$(config_value Provide.DHT.Interval)" +printf 'Provide.DHT.SweepEnabled: %s\n' "$(config_value Provide.DHT.SweepEnabled)" +printf 'Provide.DHT.ResumeEnabled: %s\n' "$(config_value Provide.DHT.ResumeEnabled)" +printf 'Datastore.StorageMax: %s\n' "$(config_value Datastore.StorageMax)" +printf 'Datastore.StorageGCWatermark: %s%%\n' "$(config_value Datastore.StorageGCWatermark)" + +if ! ipfs --api="${api_address}" id >/dev/null 2>&1; then + printf '\nDaemon: unavailable at %s\n' "${api_address}" + exit 1 +fi + +peer_id="$(ipfs --api="${api_address}" id -f='')" +peer_count="$(ipfs --api="${api_address}" swarm peers 2>/dev/null | awk 'NF { count++ } END { print count + 0 }')" + +printf '\nDaemon: ready\n' +printf 'Peer ID: %s\n' "${peer_id}" +printf 'Connected swarm peers: %s\n' "${peer_count}" +printf 'Active listen addresses:\n' +ipfs --api="${api_address}" swarm addrs listen 2>/dev/null | sed 's/^/ /' || true + +printf '\nRepository statistics:\n' +ipfs --api="${api_address}" repo stat --human 2>/dev/null | sed 's/^/ /' || true + +printf '\nAutoNAT observations:\n' +ipfs --api="${api_address}" swarm addrs autonat 2>/dev/null | sed 's/^/ /' || \ + printf ' unavailable on this Kubo build\n' + +printf '\nProvider statistics:\n' +ipfs --api="${api_address}" provide stat 2>/dev/null | sed 's/^/ /' || \ + printf ' unavailable or provider is still initializing\n'