Skip to content

gap(core-lightning): missing native V3 CLNRest provider contract blocks DR recovery and modern consumer apps #4785

Description

@satwise

Summary

Two related gaps in the core-lightning migration from core-lightning-rest (V2 API) to the native CLNRest (V3) API make modern CLN consumer apps like LNbits fragile under DR recovery and break Umbrel's provider contract for CLNRest-native consumers:

  1. Bind gap — CLNRest binds to a single static container IP instead of 0.0.0.0.
    In steady-state this is functionally equivalent (port 2107 is published via ports:
    and reachable from other containers and the LAN). The difference surfaces on failure
    paths: a DR restore or fresh install can start lightningd before the static IP is
    attached to its interface, causing a hard EADDRNOTAVAIL bind failure. Binding to
    0.0.0.0 removes that race entirely.

  2. Provider contract gapexports.sh exports zero CLNRest-native keys
    (CLNREST_HOST, CLNREST_URL, CLNREST_RUNE_PATH). CLNRest-native apps (LNbits,
    Boltz) cannot autodiscover the endpoint, and umbrelOS cannot reconstruct the V3
    environment during DR recovery — consumers fall back to empty strings or hardcoded
    V2 patterns that are wrong by construction on a fresh reinstall.

CLNRest (V3) has been built into lightningd since v23.08, but the Umbrel packaging
still reflects V2 (core-lightning-rest) naming conventions throughout.


Gap 1 — docker-compose.yml: CLNRest bind host

Before (master):

- --clnrest-host=${APP_CORE_LIGHTNING_DAEMON_IP}   # binds to 10.21.21.96 only

After (PR #5261):

- --clnrest-host=${CLNREST_HOST}   # exported as 0.0.0.0 → all interfaces

With container_name pins on lightningd, app, and tor:

lightningd:
  container_name: core-lightning_lightningd_1
app:
  container_name: core-lightning_app_1
tor:
  container_name: core-lightning_tor_1

Impact — DR-safe bind address

lightningd honors --clnrest-host literally. Binding to the daemon's static
container IP is safe in steady state but fails on two recovery paths:

  • DR / fresh-install orderinglightningd can start before the static IP is
    attached to its interface; EADDRNOTAVAIL causes a hard bind failure. 0.0.0.0
    removes that race entirely.
  • Subnet drift — if 10.21.21.0/24 is renumbered in a future umbrelOS release,
    every CLN compose pinned to a literal IP breaks silently. 0.0.0.0 survives.
  • No exposure delta — the container netns is the security boundary; 0.0.0.0
    inside the container exposes nothing that the published ports: mapping doesn't
    already expose.

Impact — container_name pins

Before #3931, the separate c-lightning-rest service was the API surface; consumers
never resolved lightningd directly. After #3931 removed that service, lightningd
itself (with CLNRest built in since v23.08) became the consumer-facing endpoint.
Without container_name, downstream consumers (RTL, LNbits, Boltz) can race Docker
DNS registration on parallel container recreation after an OS update or DR restore.
Pinning makes the name a compose-time guarantee. Scope is bounded: only lightningd
(V3 API surface), app (Commando rune owner, already hardcoded as APP_HOST), and
tor (lightningd's hard startup dependency, see #5529).


Gap 2 — exports.sh: missing V3 native provider contract

Before (master) — zero CLNRest-native exports:

export APP_CORE_LIGHTNING_DAEMON_IP="10.21.21.96"
export CORE_LIGHTNING_REST_PORT="2107"
# nothing else — no assembled endpoint, no CLNRest-native keys

After (PR #5261):

# DNS-stable container hostnames (resilient to IP drift across restarts / DR recovery)
export APP_CORE_LIGHTNING_APP_HOST="core-lightning_app_1"
export APP_CORE_LIGHTNING_DAEMON_HOST="core-lightning_lightningd_1"

# Canonical CLNREST exports for consumer apps (RTL, LNbits)
export APP_CORE_LIGHTNING_CLNREST_PORT="${CORE_LIGHTNING_REST_PORT}"
export APP_CORE_LIGHTNING_CLNREST_HOST="${APP_CORE_LIGHTNING_DAEMON_HOST}"

# Backward-compat aliases — V2 names preserved verbatim for RTL and existing consumers
export APP_CORE_LIGHTNING_REST_PORT="${APP_CORE_LIGHTNING_CLNREST_PORT}"
export APP_CORE_LIGHTNING_REST_HOST="${APP_CORE_LIGHTNING_DAEMON_IP}"

# Native CLNRest (V3) keys — what CLNRest-native apps (LNbits, Boltz) expect
export CLNREST_HOST="0.0.0.0"
export CLNREST_URL="https://${APP_CORE_LIGHTNING_DAEMON_HOST}:${CORE_LIGHTNING_REST_PORT}"
export CLNREST_RUNE_PATH="${COMMANDO_CONFIG}"

Impact — native consumer contract (LNbits, Boltz, Zap Bridge)

CLNRest-native apps are built against CLNRest's own config key names:
CLNREST_HOST, CLNREST_URL, CLNREST_RUNE_PATH. The current exports.sh exports
only V2-era names. Every consumer app must therefore hardcode V2 fallbacks and document
manual configuration steps — defeating the Umbrel provider contract pattern where
"install app → it just works" is the standard experience.

The same reason bitcoin exports BITCOIN_RPC_* and electrs exports ELECTRUM_*:
the Provider publishes the native contract its Consumers expect. CLN is the outlier
still shaped around the deprecated V2 naming, six months after #3931 retired the
binary.

The Umbrel-native names (APP_CORE_LIGHTNING_*) stay verbatim as backward-compatible
aliases. RTL and every existing consumer continue working unchanged. The new exports
are purely additive.

Impact — DR recovery

During recovery, umbrelOS sources exports.sh to reconstruct the environment for all
dependent apps. With no CLNREST_* exports, V3-native consumers reading ${CLNREST_URL}
get an empty string and silently fall back to hardcoded V2 patterns. On a fresh
reinstall the daemon IP may also drift, making any hardcoded IP fallback wrong. Full
manual remapping of V3 API keys was required to bring consumer apps back online after
a real DR restore.


What's verifiable in master today

# docker-compose.yml — binds to static internal IP, not 0.0.0.0
- --clnrest-host=${APP_CORE_LIGHTNING_DAEMON_IP}

# exports.sh — no CLNREST_* exports exist
grep CLNREST core-lightning/exports.sh   # returns nothing

# exports.sh — no DNS-stable container hostname exports
grep _HOST core-lightning/exports.sh     # returns nothing

# core-lightning-rtl/docker-compose.yml — consumer hardcodes raw IP+port
LN_SERVER_URL: "https://$APP_CORE_LIGHTNING_DAEMON_IP:$CORE_LIGHTNING_REST_PORT"

Dependency chain

[Issue #4785] Missing native V3 CLNRest provider contract — DR recovery + consumer apps
       │
       ▼
[PR #5261] gap(core-lightning): native V3 CLNRest provider contract + DR-safe binding
  • core-lightning/docker-compose.yml — clnrest-host=0.0.0.0 + container_name pins
  • core-lightning/exports.sh — CLNREST_* native keys + V2 compat aliases verbatim
  • core-lightning/data/app/.gitkeep — missing mount source placeholder
  • Linter clean. Tested on umbrelOS / Pi5 with CLN v25.09.3-hotfix.1
       │
       ├──────────────────────────────────────────┐
       ▼                                          ▼
[PR #5465]                               [PR #5457]
feat(lnbits-cln):                        refactor(core-lightning-rtl):
add LNbits for Core Lightning            adopt CLNRest V3 provider contract
• First CLNRest-native consumer          • Replace hardcoded IP+port with
  app in Umbrel app store                  ${CLNREST_URL} and ${CLNREST_RUNE_PATH}
• Validates full V3 provider             • Decouples RTL from CLN internals
  contract end-to-end                    • container_name pins on web + boltz
• Closes #4753                           • Draft. Depends on #5261
• Draft. Depends on #5261

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions