The module mixed the `options` keyword attribute with a bare top-level
`system.*` setting. Once a module declares `options` (or `config`), every
other top-level attribute must be a reserved module keyword — the nixpkgs
unifyModuleSyntax check rejects anything else, so every nixos-rebuild
aborted at evaluation time with:
error: Module '.../modules/core/public-ip.nix' has an unsupported
attribute `system'. ... move all of them (namely: system) into the
`config' attribute.
Prefix the activation script with `config.` (equivalent to wrapping it in
`config = { ... };`) so the module evaluates again. The detector script
itself is unchanged.
Fixes: ac6c498615 (-feat(public-ip): unify public-IP detection into one privacy-first script-)
The public IP was previously detected independently in three places,
each contacting a different third party: the Hub (HTTPS echo via
api.ipify.org / ifconfig.me / icanhazip.com on every API call and
background tick), DDNS (myip.opendns.com via OpenDNS), and LiveKit
(embedded STUN). Consolidate into a single detector with one shared
cache so every consumer reads the same value with minimal exposure.
- add modules/core/public-ip.nix: installs /var/lib/sovran/public-ip.py
(pure Python stdlib, no new deps) writing /var/lib/secrets/external-ip
- detection chain (first success wins): explicit pin, fresh cache
(default TTL 300s), STUN binding request over UDP (one packet, no
metadata), DNS myip.opendns.com query, then OPT-IN HTTPS echo
(publicIP.httpsEcho, empty by default — never contacted unless listed)
- privacy: while the cache is fresh zero third parties are contacted;
at most one party learns the IP per refresh interval, via the least
exposing mechanism available
- hub (server.py): _get_external_ip() now reads the shared detector /
cache instead of calling ipify/ifconfig/icanhazip directly
- ddns (njalla.nix): use the shared detector instead of a separate
OpenDNS dig; allow the hardened service to write /var/lib/secrets
- element-calling: livekit-turn-setup falls back to the shared
detector on cold boot; add LiveKit webhooks to lk-jwt-service
(sfu_webhook) so abrupt disconnects are cleaned up immediately;
set LIVEKIT_SANITY_CHECK_INTERVAL_SECONDS=60 as a missed-webhook
guard; drop the dead services.livekit.settings block and set
openFirewall=false (Caddy fronts the SFU; no public 7880/tcp)
- new options: sovran_systemsOS.publicIP.{stunServer,stunPort,
dnsResolver,httpsEcho,cacheTTL}
The element-calling feature only advertised the LiveKit focus via the
well-known org.matrix.msc4143.rtc_foci file, and relied on STUN
auto-detection for the public IP. Element X queries the MatrixRTC
transports registry endpoint and fails with MISSING_MATRIX_RTC_TRANSPORT
when it is absent, and blocked STUN egress silently left LiveKit
advertising a private IP (call connects but no video across servers).
- synapse: enable msc4143_enabled and advertise matrix_rtc.transports
(MSC4519) with the site's element-calling URL, so Element X can
discover the LiveKit focus instead of erroring out
- livekit: determine the public IP to advertise at runtime —
explicit pin, then HTTPS egress detection (api.ipify.org /
checkip.amazonaws.com / ifconfig.me), then STUN fallback with a
warning; reject non-routable results (private/loopback/CGNAT)
- lk-jwt-service: append optional extra homeservers to
LIVEKIT_FULL_ACCESS_HOMESERVERS via the new
sovran_systemsOS.elementCalling.fullAccessHomeservers option
- add sovran_systemsOS.elementCalling.externalIP option to pin the
advertised public IP for multi-WAN/VPN setups
- add element-calling-public-check.service: boot-time diagnostics for
public DNS (via 1.1.1.1, bypassing local loopback overrides), JWT
healthz through Caddy and via the public IP, and the transports
endpoint — turns the silent -no media- failure into a visible error
- add restartTriggers so livekit/lk-jwt-service pick up regenerated
runtime configs on rebuild
NixOS already knows whether a reboot is pending: /nix/var/nix/profiles/
system vs /run/current-system. Marker files only the Hub's own updater
wrote desynced for terminal-updated machines (and markers from older
updaters could never clear), pinning the badge on forever. Reconcile
REBOOT_REQUIRED against live state on every read; the stale marker
self-heals to IDLE. The .generation marker write is now informational.
The local packages/lndinit/default.nix is a verbatim copy of the
upstream Nixpkgs expression, frozen at v0.1.3-beta (the version that
was vendored in from nix-bitcoin before the Aug 10 2026 refactor in
commit 1fbeafd). It carries no Sovran-specific patches, no local
overrides, and no behavioral modifications - it is byte-for-byte
identical to what Nixpkgs ships, except ~19 minor versions older
(Nixpkgs currently ships 0.1.22-beta; the developer upstream
lightninglabs/lndinit is at v0.1.36-beta as of June 10 2026).
Why this matters
----------------
The Aug 10 2026 refactor (1fbeafd, "refactor: move vendor/nix-bitcoin
to modules/bitcoin, remove overlays") stated the new convention:
No more random vendor/ or pkgs/ dirs - follows Sovran convention:
modules/ for NixOS modules, packages/ for packages
That refactor successfully removed:
* pkgs/sovran-overlay.nix
* pkgs/nbxplorer.nix
* pkgs/README.md
* modules/vendor/ (entire directory)
* overlay-sovran from flake.nix
It moved the lndinit expression into packages/lndinit/default.nix as
an intermediate step, but the file is still a verbatim upstream copy
and therefore still incurs the maintenance burden the refactor was
meant to eliminate: manual version bumps, manual hash refreshes, and
no upstream security or bug-fix flow. Removing it completes the
intent of 1fbeafd.
The change
----------
modules/bitcoin/lnd.nix (line 153):
- lndinit = "${(pkgs.callPackage ../../packages/lndinit {})}/bin/lndinit";
+ lndinit = "${pkgs.lndinit}/bin/lndinit";
The two later uses of `lndinit` in the same file (lines 243 and 247,
inside the systemd.services.lnd.preStart block that calls
`lndinit gen-seed` and `lndinit init-wallet`) are unchanged because
they reference the let-bound `lndinit` value, not the callPackage
expression. They continue to work with the new pkgs.lndinit binary
path transparently.
Removed:
* packages/lndinit/default.nix
* packages/lndinit/ (now empty directory)
No other files in the repository reference packages/lndinit. Verified
by:
* Git tree search for "packages/lndinit" -> only the file and its
parent directory match
* Content grep of flake.nix, configuration.nix,
modules/bitcoin/default.nix, modules/bitcoin/common.nix, and
iso/common.nix -> zero matches
Why this is safe
----------------
1. CLI compatibility. The preStart script only invokes two
lndinit subcommands:
* `lndinit gen-seed`
* `lndinit -v init-wallet --file.seed=... --file.wallet-password=... --init-file.output-wallet-dir=...`
Both subcommands and all four flags have been stable since the
0.1.x line. The Nixpkgs 0.1.22-beta binary produces a wallet.db
and admin.macaroon in the same on-disk format that 0.1.3-beta did
for the same LND version (LND is pinned separately by pkgs.lnd
from Nixpkgs and is unaffected by this change).
2. No coupled Go modules or shared vendor tree. The local
packages/lndinit/default.nix is a self-contained buildGoModule
derivation; it has no shared state with any other Sovran package.
3. Nixpkgs pin is current. flake.nix pins
github:NixOS/nixpkgs/nixos-unstable, which has shipped pkgs.lndinit
since 2022 and is currently at 0.1.22-beta. There is no
"missing attribute" risk.
4. Wallet data is forward-compatible. The wallet.db format is owned
by LND, not lndinit. lndinit is only used at first boot to create
the seed and initialize the wallet; subsequent LND restarts do
not invoke lndinit. So even if a user already initialized a
wallet with 0.1.3-beta, the binary being upgraded to 0.1.22-beta
is irrelevant - LND owns the wallet from that point on.
5. Single call site. Only modules/bitcoin/lnd.nix references
lndinit. No other modules, scripts, or tests need to change.
Operational notes
-----------------
* After this commit, lndinit updates flow through the normal
`nix flake update` workflow (or whatever automated dependency
tooling is already in use, e.g. for the recent "chore(deps):
update RTL to 0.15.10" commits). No Sovran-side action is needed
to pick up future lndinit versions.
* If a future LND version requires a specific lndinit version, the
pin can be done in flake.nix via a one-line overlay:
nixpkgs.overlays = [ (final: prev: {
lndinit = prev.lndinit.overrideAttrs (o: {
version = "X.Y.Z-beta";
src = prev.fetchFromGitHub { ... };
vendorHash = "...";
});
}) ];
This keeps the upgrade path explicit without bringing the entire
expression back into the Sovran tree.
* This drops ~20 lines of frozen derivation code, eliminates one
source of upstream drift, and reduces the surface area of what
Sovran needs to keep current.
The full-system updater runs as a detached systemd service and can finish
successfully even when the browser loses its status connection. In that
case the update log and status file correctly report REBOOT_REQUIRED, but
the Hub modal can remain on "Updating..." with its controls disabled.
There were four independent ways for the frontend to get stuck:
* update status fetches had no deadline, so a request that stayed pending
never rejected and never advanced the existing failure counter;
* setInterval started async polls without waiting for the previous poll,
allowing slow requests to overlap and responses to arrive out of order;
* each log chunk used textContent +=, replacing the complete and growing
Nix build log every two seconds, which could stall browser rendering and
was especially visible over RDP; and
* page reload, tab resume, and RDP reconnect did not reattach the modal to
the update status persisted by the backend.
This produced a dangerous UX mismatch: the machine had a fully staged
NixOS generation and was ready to reboot, while the Hub continued telling
the user that the update was still running.
Bound status requests with AbortController, prevent overlapping polls, and
replace the endless spinner after sustained failures with an explicit
"Update status unavailable" state and Retry Status action. Reconcile state
immediately on focus, visibility, online, page startup, and before starting
a new update. Use no-store requests and render verbose logs incrementally
with a bounded visible tail while retaining the complete report in memory.
Apply the same timeout and single-flight protection to rebuild polling.
Record the exact generation produced by `nixos-rebuild boot`. The Hub now
keeps REBOOT_REQUIRED visible until that generation matches
/run/current-system, then clears the marker after reboot. For an update
started by an older updater that did not write the marker, recover the
staged generation from the final nixos-rebuild log line. The dashboard
sidebar also distinguishes update-in-progress and restart-required states.
Regression coverage verifies generation marker/log recovery, pre- versus
post-reboot detection, request timeout wiring, single-flight polling,
connection-loss UX, RDP/tab resume reconciliation, bounded log rendering,
page-reload recovery, and JavaScript syntax.
Validation:
* python3 -m unittest discover -s tests -p 'test_*.py' -v (170 passed)
* node --check app/sovran_systemsos_web/static/js/*.js
* python3 -m py_compile for changed Python modules
* git diff --check
A Nix evaluation was not available in the development sandbox; the NixOS
module should still be evaluated and built in CI or on a test machine before
release.
nixpkgs commit c8f9654 refactored the services.i2pd module to use
an RFC42-style settings attribute set and removed services.i2pd.proto.
After updating the root nixpkgs input from f13ff45 to ec2d622, the
vendored bitcoind module failed evaluation on the obsolete
services.i2pd.proto.sam.enable definition.
The error occurred even with services.bitcoind.i2p at its false default:
bitcoind was enabled, so NixOS still validated the obsolete option path
inside the conditional i2pd integration.
Read the SAM endpoint from services.i2pd.settings.sam and configure its
new upstream-style fields explicitly. Keep 127.0.0.1:7656, matching the
old typed option defaults that bitcoind uses to generate its i2psam
setting.
This preserves optional I2P support without activating it by default.
i2pd remains disabled until services.bitcoind.i2p is set to true or
"only-outgoing".
Nixpkgs migration: https://github.com/NixOS/nixpkgs/commit/c8f965411e812060a9377fa4c2d7d0f84e8b10e0
livekit-turn-setup.service detects the primary interface from the IPv4
default route, but had no ordering against network-online.target. With
NetworkManager+DHCP the default route is applied late at boot, so the
oneshot could run before it existed, exit 1, and — being a hard
dependency of livekit.service — take livekit down with it. The Hub then
showed a 'failed' red dot until livekit was restarted manually.
Order both livekit.service and livekit-turn-setup.service after
network-online.target. Also add a bounded retry when copying Caddy's ACME
cert so we never write an empty turn.crt/turn.key on a fresh boot.
The LND-only rewrite of lndconnect.nix shipped a wrapper Zeus cannot
use: unknown flags (--cert/--macaroon), a non-existent onion path
(free/lnd.onion), and a REST hidden service that collided with LND's
P2P onion. Restore the nix-bitcoin contract — dedicated lnd-rest
onion on port 8080, --nocert over Tor, admin macaroon in the URI —
and only persist a valid lndconnect:// URI for the Hub QR.
The Hub launcher used an ephemeral /tmp profile deleted on exit, which
wiped the hub_manual_logout marker cookie. On reopen, /auto-login minted a
new session and logged the user straight back in without a password.
Use a persistent per-user profile under XDG_STATE_HOME and drop the
deletion trap so the logout marker survives close/reopen. Keep
--skip-origin-startup-dialog. Adds regression tests.
- modules/synapse.nix: replace tr|head pipeline (causes SIGPIPE under
set -euo pipefail) with pwgen -sA0 20 1 which is already in PATH
- modules/bitcoin/rtl.nix: lowercase Authentication->authentication and
Settings->settings per RTL v0.15.8 schema; add lnServerUrl pointing
to LND REST endpoint; move swapServerUrl/boltzServerUrl inside
settings; apply same fixes to CLN branch
Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
- Add btcexplorercookiefile to BTCPay deterministic config so NBXplorer
cookie authentication succeeds (fixes 401 Unauthorized)
- Set WorkingDirectory to package lib dir so ASP.NET Core can locate
wwwroot and LanguageService.ctor does not throw ArgumentNullException
- Update regression test to assert cookie file path and WorkingDirectory
Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
ensureClauses generates ALTER ROLE clauses, not GRANT statements, so
'"DATABASE btcpayserver" = "ALL PRIVILEGES"' produced invalid SQL.
ensureDBOwnership = true makes each user own its database, which is
the intended effect.
The services.lnd.macaroons submodule has no 'enable' option — macaroons
are implicitly enabled by being defined. Replace 'enable = true' with
'user = cfg.btcpayserver.user' which is the correct option for
controlling macaroon file ownership.
The option services.postgresql.ensureUsers.*.ensurePermissions was renamed
to ensureClauses in nixpkgs unstable. Update both btcpayserver and nbxplorer
database user configurations.
Replace all pkgs-stable references with pkgs.stable (from overlay-stable in flake.nix):
- Remove pkgs-stable from function arguments
- Use pkgs.stable.nbxplorer (2.6.10 from nixos-26.05)
- Use pkgs.stable.btcpayserver (2.4.2 from nixos-26.05)
1. flake.nix: Add pkgs-stable as specialArg for modules
2. btcpayserver.nix:
- Fix preStart script syntax error (closing brace on new line)
- Use pkgs-stable.btcpayserver (2.4.2) instead of pkgs.btcpayserver
- Use pkgs-stable.nbxplorer (2.6.10) instead of pkgs.nbxplorer
- Remove clightning references (Sovran is LND-only)
- Create packages/rtl/default.nix to build RTL from source
- Update modules/bitcoin/rtl.nix to use vendored package instead of pkgs.rtl
- Remove clightning/lightning-loop references (Sovran is LND-only)
- Simplify to LND-only configuration
The joinmarket-ob-watcher module was not imported from nix-bitcoin,
so referencing it in the onion-services defaults caused an
"attribute 'joinmarket-ob-watcher' missing" evaluation error.
- Previous lnd-only refactor removed the '++ optional clightning' line
but left 'extraGroups = [ cfg.nbxplorer.group ]' without trailing ';'
-> syntax error: unexpected '=', expecting ';' at home = ...
- Add ';' to fix build on both f13ff45 and 8b8c811