From 3542ed0cd629a8a64bdee167419e6c2bc10ed86e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ausi=C3=A0s=20Armesto?= Date: Wed, 12 Aug 2026 16:31:42 +0200 Subject: [PATCH 1/4] fix: Test package generation --- .github/workflows/pr.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 255437a..4143a5f 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -3,7 +3,6 @@ name: PR on: pull_request: types: [opened, synchronize, reopened, labeled] - jobs: test: name: Test package From f5556adbdf276d09225036a6352806213acb5a77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ausi=C3=A0s=20Armesto?= Date: Thu, 3 Sep 2026 09:20:25 +0200 Subject: [PATCH 2/4] fix: Fix network conflict --- .github/workflows/pr.yaml | 2 +- DEVELOPING.md | 2 +- Dockerfile | 2 +- README.md | 50 ++++++++++++++++++---------- docker-compose.local.yml | 1 + docker-compose.yml | 19 +++++------ entrypoint.sh | 42 ++++++++++++++++++++--- gnosisvpn-server/entrypoint.sh | 9 ++--- gnosisvpn-server/wggvpn.conf.tpl | 16 ++++----- hoprd.cfg.yaml => hoprd.cfg.yaml.tpl | 14 ++++---- setup-wizard.yml | 14 +++++--- test/README.md | 24 ++++++++----- test/validate-hoprd-config.sh | 17 +++++++--- 13 files changed, 140 insertions(+), 72 deletions(-) rename hoprd.cfg.yaml => hoprd.cfg.yaml.tpl (84%) diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 4143a5f..93c5fe2 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -11,7 +11,7 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 - - name: Validate hoprd.cfg.yaml + - name: Validate hoprd.cfg.yaml.tpl run: ./test/validate-hoprd-config.sh - name: Set up Docker Buildx diff --git a/DEVELOPING.md b/DEVELOPING.md index d10177d..eb1e174 100644 --- a/DEVELOPING.md +++ b/DEVELOPING.md @@ -39,5 +39,5 @@ upstream image's own defaults/shape changed. Update `docker-compose.yml`'s `UPSTREAM_VERSION` build arg, then re-run `just validate-config` - hoprd 4.0's config schema is strict (unknown top-level keys in -`hoprd.cfg.yaml` are a hard error), so a minor version bump can break it silently +`hoprd.cfg.yaml.tpl` are a hard error), so a minor version bump can break it silently otherwise. diff --git a/Dockerfile b/Dockerfile index 18680e3..4bd511c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,7 @@ FROM europe-west3-docker.pkg.dev/hoprassociation/docker-images/hoprd:${UPSTREAM_ # not used at the moment, but might be useful in the future ENV DAPPNODE=true -ADD hoprd.cfg.yaml /app/hoprd.cfg.yaml +ADD hoprd.cfg.yaml.tpl /app/hoprd.cfg.yaml.tpl ADD entrypoint.sh /bin/entrypoint.sh RUN chmod +rx /bin/entrypoint.sh diff --git a/README.md b/README.md index 8791fa8..d2f4359 100644 --- a/README.md +++ b/README.md @@ -8,20 +8,28 @@ Runs a [GnosisVPN](https://vpn.gnosis.eth.limo) exit node on DAppNode: a HOPR no - **`gnosisvpn-server`** - runs [`gnosis_vpn-server`](https://github.com/gnosis/gnosis_vpn-server), which manages a real WireGuard interface (`wggvpn`) and terminates/forwards client traffic to the internet. `gnosisvpn-server` runs as an ordinary container with just `cap_add: NET_ADMIN`. That's still enough to create a real kernel WireGuard interface and manage it via `wg-quick` - (`--sync-wg-interface`) - verified directly. It shares `node`'s network namespace - (`network_mode: "service:node"`) rather than getting its own: `hoprd`'s - `session_ip_forwarding` needs a literal, stable `ip:port` for `gnosisvpn-server`, and a - non-core DAppNode package can't pin it a static IP on a custom docker network (see below), - so instead it's reached over loopback (`127.0.0.1` - see `hoprd.cfg.yaml`), which is stable - by construction. + (`--sync-wg-interface`) - verified directly. It runs as a normal DAppNode service, on the + same `dncore_network` every DAppNode package service gets attached to (a non-core package + can't get `network_mode: "service:node"` here either - DAppNode's `dappmanager` forces that + `networks:` attachment onto every service regardless, and Docker Compose rejects + `network_mode` and `networks` together on the same service). Since `hoprd`'s + `session_ip_forwarding` needs a literal `ip:port` (hostnames are rejected by hoprd's own + config parser) and a non-core package can't pin a static IP on `dncore_network` either (see + below), `node`'s `entrypoint.sh` resolves `gnosisvpn-server`'s DAppNode DNS alias + (`gnosisvpn-server..dappnode`) to its current IP at boot and renders that into + `hoprd.cfg.yaml.tpl` before `hoprd` starts (see `GNOSISVPN_SERVER_HOST` in + `docker-compose.yml`). Unlike the loopback address this replaced, this IP is **not** fixed + across installs, and isn't guaranteed stable across a `gnosisvpn-server` recreate - see + "Known limitations" below. ## Known limitations vs. production A regular (non-core) DAppNode package can't get `network_mode: host` or a custom docker network with a pinned subnet (DAppNode's SDK validates this: non-`dncore_network` names in a compose file's `networks:` are rejected outright), so `gnosisvpn-server` never reaches the -DAppNode host's own network namespace, only `node`'s (see above). That rules out explicit -host firewall exceptions: a host-level deployment punches two rules through the host's +DAppNode host's own network namespace, only its own container namespace on `dncore_network` +(see above). That rules out explicit host firewall exceptions: a host-level deployment +punches two rules through the host's `DOCKER-USER` iptables chain - one accepting `wggvpn -> ` traffic, and one accepting the return leg (` -> wggvpn`) but only for `RELATED,ESTABLISHED` connections, deliberately not `NEW`, so the exit node can't be used to originate unsolicited @@ -35,19 +43,27 @@ packets to it in the first place. The same effect - only established/related tra reaches `wggvpn` - is achieved structurally, by not being reachable, rather than by an explicit firewall rule. -For the same reason, this exit node's internal `gnosisvpn-server` address (`127.0.0.1`) is -**not** the `172.30.0.1` a production, Ansible-managed exit node presents (a fixed docker -bridge gateway, identical across that whole fleet because every host pins the same subnet - -something this package structurally can't do). A `gnosis_vpn-client` connecting to *this* -exit node needs its `[connection.bridge]`/`[connection.wg]` `target` overridden to -`127.0.0.1:8000`/`127.0.0.1:51820` accordingly; since that's a single global client setting -(not per-destination), a client configured that way can no longer also reach a standard -`172.30.0.1`-convention exit node without changing it back. +For the same reason, this exit node's internal `gnosisvpn-server` address is **not** the +fixed `172.30.0.1` a production, Ansible-managed exit node presents (a fixed docker bridge +gateway, identical across that whole fleet because every host pins the same subnet - +something this package structurally can't do). Worse, unlike a first attempt at working +around this (sharing `node`'s network namespace to get a fixed loopback address - see git +history), the address DAppNode assigns `gnosisvpn-server` on `dncore_network` is **not fixed +across installs either**: it's a normal dynamic bridge IP, different on every install and not +guaranteed stable across a `gnosisvpn-server` recreate on the same install (though it survives +an ordinary restart). `entrypoint.sh` logs it on every `node` boot +(`gnosisvpn-server resolved to ...`), so it's discoverable per-install from DAppNode's log +viewer, but there's currently no single documented value a `gnosis_vpn-client` can be +pre-configured with the way `172.30.0.1` works for the production fleet - a client would need +to be told this specific exit node's current address out of band. The proper fix is upstream, +in `hoprd`: an in-progress change to have `hoprd` inform a connecting client of the correct +target itself, so clients don't need this hardcoded at all. Until that lands, this is a real +gap for this package's exit nodes specifically. ## Local testing See [`test/README.md`](test/README.md) - `just local-up`/`local-down` runs the real -docker-compose stack outside DAppNode, and `just validate-config` checks `hoprd.cfg.yaml` +docker-compose stack outside DAppNode, and `just validate-config` checks `hoprd.cfg.yaml.tpl` against the real hoprd binary's config parser. ## Building the package diff --git a/docker-compose.local.yml b/docker-compose.local.yml index 736a25f..02f1a65 100644 --- a/docker-compose.local.yml +++ b/docker-compose.local.yml @@ -11,6 +11,7 @@ services: HOPRD_PASSWORD: ${HOPRD_PASSWORD} HOPRD_API_TOKEN: ${HOPRD_API_TOKEN} HOPRD_HOST: ${HOPRD_HOST:-127.0.0.1:9091} + GNOSISVPN_SERVER_HOST: gnosisvpn-server gnosisvpn-server: environment: WIREGUARD_CIDR: ${WIREGUARD_CIDR:-10.128.0.0/24} diff --git a/docker-compose.yml b/docker-compose.yml index 381fd7a..f6de1cd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,3 @@ -version: "3.6" services: node: # Do not set image field here, it will be set automatically by the sdk. @@ -34,14 +33,22 @@ services: HOPRD_MODULE_ADDRESS: "0x0000000000000000000000000000000000000000" HOPRD_PASSWORD: "" HOPRD_API_TOKEN: "" - # Set this to override the value in hoprd.cfg.yaml + # Set this to override the value in hoprd.cfg.yaml.tpl HOPRD_BLOKLI_URL: "" # Not exposed by `hoprd --help` in 4.0.3, but still read directly from the # environment rather than as a registered CLI flag. HOPRD_NAT: "true" ADDITIONAL_CMDLINE_ARGS: "" ADDITIONAL_ENVIRONMENT_VARS: "" + # DAppNode's dappmanager forces every service (this one included) onto dncore_network + # with a DNS alias of ..dappnode - see getShortUniqueDnp in + # DNP_DAPPMANAGER. hoprd's session_ip_forwarding needs a literal ip:port (hostnames are + # rejected by hoprd's own config parser), so entrypoint.sh resolves this at boot and + # renders it into hoprd.cfg.yaml.tpl - see entrypoint.sh and README. + GNOSISVPN_SERVER_HOST: "gnosisvpn-server.gnosisvpn-exit-node.public.dappnode" restart: unless-stopped + depends_on: + - gnosisvpn-server deploy: resources: limits: @@ -50,14 +57,6 @@ services: # Do not set image field here, it will be set automatically by the sdk. build: context: ./gnosisvpn-server - depends_on: - - node - # Shares node's network namespace instead of getting its own: hoprd's - # session_ip_forwarding needs a literal, stable ip:port for gnosisvpn-server, and this - # package can't pin gnosisvpn-server a static IP on a custom docker network (not allowed - # for non-core DAppNode packages - see README). Loopback is stable by construction, no - # per-boot resolution needed - see hoprd.cfg.yaml. - network_mode: "service:node" cap_add: - NET_ADMIN volumes: diff --git a/entrypoint.sh b/entrypoint.sh index d5a9967..db7f8a1 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -2,22 +2,54 @@ set -euo pipefail # hoprd's --blokliUrl has strict URL validation that rejects an explicit empty string (it -# doesn't treat "" as "not set") - unset it so the value already in hoprd.cfg.yaml wins, +# doesn't treat "" as "not set") - unset it so the value already in hoprd.cfg.yaml.tpl wins, # unless the setup wizard actually provided one. if [ -z "${HOPRD_BLOKLI_URL:-}" ]; then unset HOPRD_BLOKLI_URL fi CUSTOM_CONFIG=/app/hoprd/conf/hoprd.custom.cfg.yaml -STATIC_CONFIG=/app/hoprd.cfg.yaml +TEMPLATE_CONFIG=/app/hoprd.cfg.yaml.tpl +RENDERED_CONFIG=/app/hoprd/conf/hoprd.generated.cfg.yaml if [ -s "${CUSTOM_CONFIG}" ]; then # A config was uploaded via the setup wizard's "Custom HOPR node configuration file" - # field - use it as-is, so we never clobber a deliberate override. + # field - use it as-is (modulo the same __GNOSISVPN_SERVER_IP__ substitution below), so we + # never clobber a deliberate override otherwise. echo "entrypoint.sh: using uploaded ${CUSTOM_CONFIG}" - export HOPRD_CONFIGURATION_FILE_PATH="${CUSTOM_CONFIG}" + SOURCE_CONFIG="${CUSTOM_CONFIG}" else - export HOPRD_CONFIGURATION_FILE_PATH="${STATIC_CONFIG}" + SOURCE_CONFIG="${TEMPLATE_CONFIG}" fi +: "${GNOSISVPN_SERVER_HOST:?GNOSISVPN_SERVER_HOST must be set}" +echo "entrypoint.sh: resolving gnosisvpn-server (${GNOSISVPN_SERVER_HOST})..." +gnosisvpn_server_ip="" +for _ in $(seq 1 30); do + gnosisvpn_server_ip="$(dig +short "${GNOSISVPN_SERVER_HOST}" A | head -n1)" + [ -n "${gnosisvpn_server_ip}" ] && break + sleep 1 +done +if [ -z "${gnosisvpn_server_ip}" ]; then + echo "entrypoint.sh: FATAL: could not resolve ${GNOSISVPN_SERVER_HOST} after 30s - is gnosisvpn-server running?" >&2 + exit 1 +fi +# Logged deliberately: this is currently the only way to find gnosisvpn-server's address, +# which a connecting gnosis_vpn-client needs as its session target - see README. hoprd is +# expected to gain a way to tell clients this directly (tracked upstream), making this +# per-install manual lookup unnecessary. +echo "entrypoint.sh: gnosisvpn-server resolved to ${gnosisvpn_server_ip}" + +# __GNOSISVPN_SERVER_IP__ is substituted the same way regardless of which file above won - +# see setup-wizard.yml's "Custom HOPR node configuration file" field, which documents using +# the same placeholder in an uploaded config too, instead of requiring the uploader to +# hardcode an address that isn't fixed and that hoprd can't resolve itself. +# No `sed` in this image (it's a minimal coreutils/util-linux build, no busybox either) - do +# the substitution with bash builtins instead. The `; echo x` / `%x` dance preserves a +# trailing newline that a bare `$(cat ...)` would otherwise strip. +config_contents="$(cat "${SOURCE_CONFIG}"; echo x)" +config_contents="${config_contents%x}" +printf '%s' "${config_contents//__GNOSISVPN_SERVER_IP__/${gnosisvpn_server_ip}}" >"${RENDERED_CONFIG}" +export HOPRD_CONFIGURATION_FILE_PATH="${RENDERED_CONFIG}" + env ${ADDITIONAL_ENVIRONMENT_VARS} /bin/docker-entrypoint.sh ${ADDITIONAL_CMDLINE_ARGS} diff --git a/gnosisvpn-server/entrypoint.sh b/gnosisvpn-server/entrypoint.sh index 6a9e14c..917b936 100755 --- a/gnosisvpn-server/entrypoint.sh +++ b/gnosisvpn-server/entrypoint.sh @@ -41,10 +41,11 @@ sed \ # Without network_mode: host (not available to regular DAppNode packages - see README), # /proc/sys/net/ipv4/ip_forward is read-only from inside this container, so it can't be set -# here even with NET_ADMIN. In practice it's already 1 on any host capable of running -# Docker/DAppNode at all (dockerd itself needs it host-wide for container networking), and -# this container's network namespace - shared with the node service via docker-compose.yml's -# `network_mode: "service:node"` - inherits that as its default. This is just a sanity check. +# here even with NET_ADMIN. In practice it's already 1: new network namespaces inherit the +# current value from their parent netns at creation time (verified directly - not reset to a +# hard-coded default), and any host capable of running Docker at all already has it enabled +# for dockerd's own bridge networking. This is just a sanity check, unrelated to which +# network(s) this container is on. current_ip_forward="$(cat /proc/sys/net/ipv4/ip_forward 2>/dev/null || echo unknown)" if [ "${current_ip_forward}" != "1" ]; then echo "gnosisvpn-server: WARNING: net.ipv4.ip_forward is '${current_ip_forward}', not '1', and this container cannot set it itself. VPN client traffic will not reach the internet until it's enabled on the DAppNode host." >&2 diff --git a/gnosisvpn-server/wggvpn.conf.tpl b/gnosisvpn-server/wggvpn.conf.tpl index ae62915..07e5979 100644 --- a/gnosisvpn-server/wggvpn.conf.tpl +++ b/gnosisvpn-server/wggvpn.conf.tpl @@ -1,13 +1,13 @@ # Rendered by entrypoint.sh from this template. PostUp/PreDown do the NAT a WireGuard exit # node needs, scoped down to what's possible for a regular (non-core) DAppNode package: no -# network_mode: host, so this container's network namespace (shared with the node service - -# see docker-compose.yml's `network_mode: "service:node"` on this service) never reaches the -# DAppNode host's. MASQUERADE targets "eth0" (that shared network namespace's interface on -# its DAppNode-managed network) rather than a real public NIC; Docker's own host-level NAT -# then carries that traffic the rest of the way out, same as it does for any other -# container's egress traffic. Explicit DOCKER-USER firewall exceptions aren't included for -# the same reason (no host namespace access) - see README's "Known limitations vs. -# production" section for why that's not actually a gap here. +# network_mode: host, so this container's own network namespace (on dncore_network, like any +# other DAppNode package service - see docker-compose.yml) never reaches the DAppNode host's. +# MASQUERADE targets "eth0" (this container's own interface on its DAppNode-managed network) +# rather than a real public NIC; Docker's own host-level NAT then carries that traffic the +# rest of the way out, same as it does for any other container's egress traffic. Explicit +# DOCKER-USER firewall exceptions aren't included for the same reason (no host namespace +# access) - see README's "Known limitations vs. production" section for why that's not +# actually a gap here. [Interface] Address = __WG_ADDRESS__/32 ListenPort = 51820 diff --git a/hoprd.cfg.yaml b/hoprd.cfg.yaml.tpl similarity index 84% rename from hoprd.cfg.yaml rename to hoprd.cfg.yaml.tpl index 3b5146c..a7a9b1e 100644 --- a/hoprd.cfg.yaml +++ b/hoprd.cfg.yaml.tpl @@ -3,13 +3,13 @@ blokli_url: https://blokli-jura.prod.hoprnet.link session_ip_forwarding: target_allow_list: # session_ip_forwarding requires a literal ip:port (hostnames are rejected by hoprd's own - # config parser). This package can't pin gnosisvpn-server a static IP on its own docker - # network (no custom docker networks allowed for non-core DAppNode packages - see - # README), so instead gnosisvpn-server shares this container's network namespace - # (docker-compose.yml's `network_mode: "service:node"`) and is reached over loopback - - # a fixed address that needs no per-boot resolution. - - "127.0.0.1:51820" # gnosisvpn-server's WireGuard interface - - "127.0.0.1:8000" # gnosisvpn-server's control endpoint + # config parser). This package can't pin gnosisvpn-server a static IP on dncore_network + # (not allowed for non-core DAppNode packages - see README), so entrypoint.sh resolves + # gnosisvpn-server's DAppNode DNS alias to its current IP at boot and renders this + # template (see GNOSISVPN_SERVER_HOST in docker-compose.yml) - this file is not used + # directly, only /app/hoprd/conf/hoprd.generated.cfg.yaml, its rendered output. + - "__GNOSISVPN_SERVER_IP__:51820" # gnosisvpn-server's WireGuard interface + - "__GNOSISVPN_SERVER_IP__:8000" # gnosisvpn-server's control endpoint strategy: allow_recursive: false execution_interval: 15s diff --git a/setup-wizard.yml b/setup-wizard.yml index 6c1004f..9c5f3e0 100644 --- a/setup-wizard.yml +++ b/setup-wizard.yml @@ -37,7 +37,7 @@ fields: description: |- URL of the Blokli provider used by the node to connect to the blockchain. - Leave empty to use the value already shipped in `hoprd.cfg.yaml` (the same one used by + Leave empty to use the value already shipped in `hoprd.cfg.yaml.tpl` (the same one used by HOPR Association's own GnosisVPN exit-node fleet). Only set this if you need to point the node at a different Blokli endpoint. secret: false @@ -50,9 +50,15 @@ fields: required: false description: |- Upload a custom configuration file for the HOPR node, replacing the shipped - `hoprd.cfg.yaml` (jura-prod GnosisVPN exit-node fleet defaults) entirely - including its - `session_ip_forwarding` entries, which you'd then need to point at gnosisvpn-server - yourself. Uploading here takes priority over the auto-generated config on every start. + `hoprd.cfg.yaml.tpl` (jura-prod GnosisVPN exit-node fleet defaults) entirely - including + its `session_ip_forwarding` entries. gnosisvpn-server's address isn't fixed (it can + differ per install and across a gnosisvpn-server recreate) and hoprd can't resolve it + itself (its config parser requires a literal ip:port, not a hostname), so if you + reference gnosisvpn-server here, use the literal placeholder `__GNOSISVPN_SERVER_IP__` + in place of its address (e.g. `__GNOSISVPN_SERVER_IP__:51820`) - the node substitutes + it with gnosisvpn-server's actual current address on every start, the same way it does + for the shipped config. Uploading here takes priority over the auto-generated config on + every start. secret: false - id: HOPRD_SAFE_ADDRESS diff --git a/test/README.md b/test/README.md index a91d711..80f5820 100644 --- a/test/README.md +++ b/test/README.md @@ -31,22 +31,28 @@ generated WireGuard public key and `wg show wggvpn`. "Working" looks like: - `wg show wggvpn` reporting the interface up with the address from `WIREGUARD_CIDR` `gnosisvpn-server` doesn't use `network_mode: host` (regular DAppNode packages aren't allowed -to - see README); instead it shares `node`'s network namespace (`network_mode: -"service:node"` in `docker-compose.yml`), so `wggvpn` and its `iptables` rules live inside -that shared namespace, not on your real machine - `just local-down` (or `docker compose -down`) tears it down along with both containers, nothing to clean up on the host afterward. -This is also why `hoprd.cfg.yaml`'s `session_ip_forwarding.target_allow_list` can just say -`127.0.0.1` - from `node`'s point of view, `gnosisvpn-server`'s ports are on loopback. +to - see README); it runs as an ordinary service with its own network namespace, so `wggvpn` +and its `iptables` rules live inside that container, not on your real machine - `just +local-down` (or `docker compose down`) tears it down along with both containers, nothing to +clean up on the host afterward. -## Validating hoprd.cfg.yaml on its own +`node`'s `entrypoint.sh` resolves `gnosisvpn-server`'s address at boot (hoprd's +`session_ip_forwarding.target_allow_list` requires a literal ip:port, hostnames are rejected) +and renders it into `hoprd.cfg.yaml.tpl` - see that file and `docker-compose.yml`'s +`GNOSISVPN_SERVER_HOST`. Outside a real DAppNode install there's no DAppNode DNS alias to +resolve, so `docker-compose.local.yml` overrides `GNOSISVPN_SERVER_HOST` to plain +`gnosisvpn-server`, which plain `docker compose` resolves via the project's default network. + +## Validating hoprd.cfg.yaml.tpl on its own ```sh just validate-config # or: test/validate-hoprd-config.sh ``` Runs the real hoprd 4.0.3 binary's own config validator (`hoprd-cfg --validate-args`) against -`hoprd.cfg.yaml`, without starting anything. Useful after editing it - hoprd 4.0's config -schema is strict (unknown top-level keys are a hard error). +`hoprd.cfg.yaml.tpl` (rendered with a dummy IP first), without starting anything. Useful +after editing it - hoprd 4.0's config schema is strict (unknown top-level keys are a hard +error). ```bash diff --git a/test/validate-hoprd-config.sh b/test/validate-hoprd-config.sh index 5101a9d..c82f6e9 100755 --- a/test/validate-hoprd-config.sh +++ b/test/validate-hoprd-config.sh @@ -1,14 +1,21 @@ #!/usr/bin/env bash -# Validates hoprd.cfg.yaml against the real hoprd binary's own config parser -# (`hoprd-cfg --validate-args`), the same check the container's entrypoint runs before -# launching hoprd. Keep HOPRD_IMAGE in sync with docker-compose.yml's UPSTREAM_VERSION. +# Validates hoprd.cfg.yaml.tpl against the real hoprd binary's own config parser +# (`hoprd-cfg --validate-args`), the same check the container's entrypoint effectively runs +# (via hoprd itself) before launching. Keep HOPRD_IMAGE in sync with docker-compose.yml's +# UPSTREAM_VERSION. Renders the template with a dummy IP first - entrypoint.sh does the same +# substitution with gnosisvpn-server's real resolved IP - since hoprd's parser requires a +# literal ip:port and won't accept the __GNOSISVPN_SERVER_IP__ placeholder as-is. set -euo pipefail cd "$(dirname "${BASH_SOURCE[0]}")/.." HOPRD_IMAGE="europe-west3-docker.pkg.dev/hoprassociation/docker-images/hoprd:4.0.3" +RENDERED_CONFIG="$(mktemp)" +trap 'rm -f "${RENDERED_CONFIG}"' EXIT +sed "s|__GNOSISVPN_SERVER_IP__|127.0.0.1|g" hoprd.cfg.yaml.tpl >"${RENDERED_CONFIG}" + docker run --rm \ - -v "$(pwd)/hoprd.cfg.yaml:/app/hoprd.cfg.yaml:ro" \ + -v "${RENDERED_CONFIG}:/app/hoprd.cfg.yaml:ro" \ --entrypoint /bin/hoprd-cfg \ "${HOPRD_IMAGE}" \ --validate-args -- \ @@ -21,4 +28,4 @@ docker run --rm \ --host 1.2.3.4:9091 \ --configurationFilePath /app/hoprd.cfg.yaml -echo "hoprd.cfg.yaml is a valid config for ${HOPRD_IMAGE}." +echo "hoprd.cfg.yaml.tpl is a valid config for ${HOPRD_IMAGE} (rendered with a dummy IP)." From 150859a76c3751549a5634497cfde184be1e1e58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ausi=C3=A0s=20Armesto?= Date: Thu, 3 Sep 2026 09:35:22 +0200 Subject: [PATCH 3/4] fix: Add version to pass validation --- docker-compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker-compose.yml b/docker-compose.yml index f6de1cd..58b77b8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,3 +1,4 @@ +version: "3.6" services: node: # Do not set image field here, it will be set automatically by the sdk. From 204d8bcd56c874bbba8a25c38b5d433ea8fabbd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ausi=C3=A0s=20Armesto?= Date: Thu, 3 Sep 2026 13:29:27 +0200 Subject: [PATCH 4/4] Bound variables --- entrypoint.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/entrypoint.sh b/entrypoint.sh index db7f8a1..07f6bf2 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -52,4 +52,4 @@ config_contents="${config_contents%x}" printf '%s' "${config_contents//__GNOSISVPN_SERVER_IP__/${gnosisvpn_server_ip}}" >"${RENDERED_CONFIG}" export HOPRD_CONFIGURATION_FILE_PATH="${RENDERED_CONFIG}" -env ${ADDITIONAL_ENVIRONMENT_VARS} /bin/docker-entrypoint.sh ${ADDITIONAL_CMDLINE_ARGS} +env ${ADDITIONAL_ENVIRONMENT_VARS:-} /bin/docker-entrypoint.sh ${ADDITIONAL_CMDLINE_ARGS:-}