Compare commits
71 Commits
91cc0152ba
...
v1.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
| dabb96e1b3 | |||
| 2b5a154b99 | |||
| e475b0f47d | |||
| 8f81f8f1e2 | |||
| cd753a7e28 | |||
| 7ac1985508 | |||
| 0ecf2eb651 | |||
| 18c7095aaf | |||
| dcad276c59 | |||
| 06988d0ff0 | |||
| 69b84153b4 | |||
| df08a7c413 | |||
| 602464189f | |||
| 67f4cdc99e | |||
| f8c717db25 | |||
| 268abddb28 | |||
| c1119b03a8 | |||
| 0c273b758d | |||
| 6f98c478e8 | |||
| 875a6a9297 | |||
| 1dbfe3cd94 | |||
| e0d4b3544d | |||
| 3e3fbed470 | |||
| ada9f25c41 | |||
| 66cacaaf9d | |||
| 15cd07d12f | |||
| fae57c0375 | |||
| 3745eedd74 | |||
| 1cd4fc8b40 | |||
| 732ab6f2aa | |||
| bea26c55c3 | |||
| a841665b07 | |||
| d574f96379 | |||
| 2388039b63 | |||
| aa69d40f08 | |||
| 31cb48cc2b | |||
| 170bd14a34 | |||
| 2553e0dce0 | |||
| b8e7b2b4cc | |||
| 24098a209a | |||
| 8ad7509b02 | |||
| a350d4e2f7 | |||
| ec3782991d | |||
| bc4b4630a3 | |||
| 342f60ce0d | |||
| 8c8e8f43a2 | |||
| 559e0218eb | |||
| fd2e12ced1 | |||
| efdf1e05d0 | |||
| cd3ab47aa0 | |||
| c12a680d27 | |||
| c728eee924 | |||
| ca704b24a2 | |||
| db068ba994 | |||
| 53ee31c5d2 | |||
| 283b439d59 | |||
| 9e09f9eb40 | |||
| 08bfa73e74 | |||
| 2b76a766ad | |||
| d245e2ce0b | |||
| bb2603bea0 | |||
| 8f96625c26 | |||
| 9c0ddf0dbe | |||
| 2a352c35f9 | |||
| 56b965b847 | |||
| 8e5bb766a6 | |||
| 646d877c4d | |||
| 045c5d6a12 | |||
| f0f690eae4 | |||
| 459536d478 | |||
| 6c5f261d8a |
@@ -1 +1,151 @@
|
|||||||
### Testing Branch
|
<div align="center">
|
||||||
|
|
||||||
|
<img src="iso/assets/sovran-hub-icon.svg" alt="Sovran Systems" width="160" />
|
||||||
|
|
||||||
|
# Sovran_SystemsOS
|
||||||
|
|
||||||
|
`Base Development` · NixOS Flake · AGPL-3.0
|
||||||
|
|
||||||
|
[Sovran Systems](https://sovransystems.com)
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
1. [What This Repo Is](#what-this-repo-is)
|
||||||
|
2. [Architecture](#architecture)
|
||||||
|
3. [Module Catalog](#module-catalog)
|
||||||
|
4. [The Three Modes (internal reference)](#the-three-modes-internal-reference)
|
||||||
|
5. [Build & Deploy Reference](#build--deploy-reference)
|
||||||
|
6. [Networking & Reverse Proxy](#networking--reverse-proxy)
|
||||||
|
7. [Security Posture](#security-posture)
|
||||||
|
8. [Backups & Recovery](#backups--recovery)
|
||||||
|
9. [License](#license)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What This Repo Is
|
||||||
|
|
||||||
|
Sovran_SystemsOS is defined entirely as a **Nix flake** (`flake.nix`) and built from source. There is no pre-built binary — the System Installer is produced from this tree. Everything the system does is declared here.
|
||||||
|
|
||||||
|
The control center is the **Hub** — a built-in panel that lets the operator launch, monitor, and toggle services without touching a terminal. Under the hood, the Hub writes to `custom.nix`, which feeds back into the flake.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────┐
|
||||||
|
│ flake.nix │
|
||||||
|
│ inputs: nixpkgs, │
|
||||||
|
│ nix-bitcoin, nixvim, │
|
||||||
|
│ btc-clients │
|
||||||
|
└───────────┬─────────────┘
|
||||||
|
│ nixosModules.Sovran_SystemsOS
|
||||||
|
▼
|
||||||
|
┌──────────────────────────┐ imports ┌──────────────────────────┐
|
||||||
|
│ configuration.nix │────────────▶│ modules/modules.nix │
|
||||||
|
│ boot / fs / users / │ │ core/* + services + opt │
|
||||||
|
│ desktop / nix settings │ │ features │
|
||||||
|
└──────────────────────────┘ └──────────┬───────────────┘
|
||||||
|
▲ │
|
||||||
|
│ ./role-state.nix (mode/role) ▼
|
||||||
|
│ ./custom.nix (user overrides) ┌────────────────────┐
|
||||||
|
│ │ modules/*.nix │
|
||||||
|
└───────── sovran-hub writes ───────▶│ synapse / wordpress│
|
||||||
|
│ nextcloud / etc. │
|
||||||
|
└────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`flake.nix`** declares two NixOS configurations:
|
||||||
|
- `nixosConfigurations.nixos` — the running system.
|
||||||
|
- `nixosConfigurations.sovran_systemsos-iso` — the System Installer.
|
||||||
|
- **`configuration.nix`** owns host concerns (boot, filesystems, users, desktop, locale, Nix settings, firewall, audio, backups).
|
||||||
|
- **`modules/modules.nix`** is the service router. Every other module is opt-in via flags read from `role-state.nix` and `custom.nix`.
|
||||||
|
|
||||||
|
## Module Catalog
|
||||||
|
|
||||||
|
Defaults follow the import order in `modules/modules.nix`. Toggles live in `custom.nix` (the Hub writes them) and `role-state.nix`.
|
||||||
|
|
||||||
|
| Module | Default | Purpose |
|
||||||
|
|---|---|---|
|
||||||
|
| `core/*` | **on** | Roles, Caddy, Njalla, Hub, desktop, perf, ssh-bootstrap |
|
||||||
|
| `php.nix`, `credentials.nix` | **on** | Required by web services & secrets |
|
||||||
|
| `synapse.nix` | **on** | Matrix homeserver |
|
||||||
|
| `wordpress.nix` | **on** | WordPress + PHP-FPM vhost |
|
||||||
|
| `nextcloud.nix` | **on** | Files / calendar / contacts |
|
||||||
|
| `vaultwarden.nix` | **on** | Bitwarden-compatible secrets vault |
|
||||||
|
| `bitcoinecosystem.nix` | **on** | bitcoind/electrs/LND/RTL/BTCPay (over Tor) |
|
||||||
|
| `wallet-autoconnect.nix` | **on** | Sparrow/Bisq ↔ node handshake |
|
||||||
|
| `haven.nix` | off | Nostr relay |
|
||||||
|
| `element-calling.nix` | off | LiveKit + JWT for E2E calling |
|
||||||
|
| `mempool.nix` | off | Mempool.space dashboard |
|
||||||
|
| `bitcoin-core.nix` | off | Switch node to Bitcoin Core (replaces default Bitcoin Knots + BIP110) |
|
||||||
|
| `rdp.nix` | off | xrdp remote desktop |
|
||||||
|
| `sshd.nix` | off | Public-facing OpenSSH |
|
||||||
|
|
||||||
|
> Tor is wired directly into the Bitcoin stack. In `modules/bitcoinecosystem.nix`, `bitcoind`, `electrs`, and `lnd` all set `tor.enforce = true` and `tor.proxy = true`, and onion services are exposed for them.
|
||||||
|
|
||||||
|
## The Three Modes (internal reference)
|
||||||
|
|
||||||
|
Selected by `role-state.nix`, resolved by `modules/core/role-logic.nix`. All three configurations are produced from this same flake.
|
||||||
|
|
||||||
|
| Mode | What's enabled on top of the base NixOS + GNOME |
|
||||||
|
|---|---|
|
||||||
|
| **Desktop** | Private daily-driver. Sparrow + Bisq included. |
|
||||||
|
| **Node** | Desktop + full Bitcoin stack (bitcoind/electrs/LND/RTL/BTCPay over Tor). |
|
||||||
|
| **Server+Desktop** | Node + self-hosting services (Synapse, Nextcloud, WordPress, Vaultwarden, Element Calling, etc.). |
|
||||||
|
|
||||||
|
## Build & Deploy Reference
|
||||||
|
|
||||||
|
Internal commands. Run from the flake root.
|
||||||
|
|
||||||
|
| Action | Command |
|
||||||
|
|---|---|
|
||||||
|
| Build the System Installer | `nix build .#nixosConfigurations.sovran_systemsos-iso.config.system.build.isoImage` |
|
||||||
|
| Switch now | `sudo nixos-rebuild switch --flake .#nixos` |
|
||||||
|
| Test in current boot only | `sudo nixos-rebuild test --flake .#nixos` |
|
||||||
|
| Stage for next boot | `sudo nixos-rebuild boot --flake .#nixos` |
|
||||||
|
| Build only (no activation) | `nixos-rebuild build --flake .#nixos` |
|
||||||
|
| Update pinned inputs | `nix flake update` (then rebuild) |
|
||||||
|
| Rollback last switch | `sudo nixos-rebuild switch --rollback` |
|
||||||
|
| Garbage-collect (>7 days) | Automatic weekly; manual: `sudo nix-collect-garbage -d` |
|
||||||
|
|
||||||
|
## Networking & Reverse Proxy
|
||||||
|
|
||||||
|
- **Firewall on by default** (`networking.firewall.enable = true`). Port are opened by the module that needs it.
|
||||||
|
- **Caddy** (`modules/core/caddy.nix`) terminates TLS for all HTTP services.
|
||||||
|
- **Njalla** dynamic DNS (`modules/core/njalla.nix`) keeps records in sync via a 15-minute cron job.
|
||||||
|
- **Tor** is enabled with `torsocks` available. The Bitcoin stack uses it directly — see [Security Posture](#security-posture).
|
||||||
|
- **SSH:** localhost-only by default (`core/sshd-localhost.nix`).
|
||||||
|
|
||||||
|
## Security Posture
|
||||||
|
|
||||||
|
Facts about the defaults, straight from `configuration.nix` and the modules:
|
||||||
|
|
||||||
|
- **Reproducible builds.** Every artifact derives from `flake.lock`. The same commit produces the same OS.
|
||||||
|
- **Bitcoin stack over Tor.** In `modules/bitcoinecosystem.nix`, `bitcoind`, `electrs`, and `lnd` all set `tor.enforce = true`, and onion services are exposed for `bitcoind`, `electrs`, `lnd`, and friends.
|
||||||
|
- **Firewall on, public sshd off, RDP off, auto-login off, fail2bain active**
|
||||||
|
- **Kernel surface trimmed.** `boot.blacklistedKernelModules = [ "rxrpc" ];`
|
||||||
|
- **Weekly garbage collection** with `--delete-older-than 7d`.
|
||||||
|
|
||||||
|
## Backups & Recovery
|
||||||
|
|
||||||
|
`services.rsnapshot` snapshots hourly and daily to `/run/media/Second_Drive/BTCEcoandBackup/NixOS_Snapshot_Backup`:
|
||||||
|
|
||||||
|
```
|
||||||
|
backup /home/ localhost/
|
||||||
|
backup /var/lib/ localhost/
|
||||||
|
backup /etc/nixos/ localhost/
|
||||||
|
backup /etc/nix-bitcoin-secrets/ localhost/
|
||||||
|
retain hourly 5
|
||||||
|
retain daily 5
|
||||||
|
cron hourly 0 * * * *
|
||||||
|
cron daily 50 21 * * *
|
||||||
|
```
|
||||||
|
|
||||||
|
The second drive is mounted by label (`BTCEcoandBackup`) with `nofail` so a missing drive doesn't block boot.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Licensed under the **GNU Affero General Public License v3.0** — see [`LICENSE`](./LICENSE).
|
||||||
|
|||||||
@@ -204,7 +204,7 @@ FEATURE_REGISTRY = [
|
|||||||
{"port": "80", "protocol": "TCP", "description": "HTTP (redirect to HTTPS)"},
|
{"port": "80", "protocol": "TCP", "description": "HTTP (redirect to HTTPS)"},
|
||||||
{"port": "443", "protocol": "TCP", "description": "HTTPS (domain)"},
|
{"port": "443", "protocol": "TCP", "description": "HTTPS (domain)"},
|
||||||
{"port": "7881", "protocol": "TCP", "description": "LiveKit WebRTC signalling"},
|
{"port": "7881", "protocol": "TCP", "description": "LiveKit WebRTC signalling"},
|
||||||
{"port": "7882-7894", "protocol": "UDP", "description": "LiveKit media streams"},
|
{"port": "7882", "protocol": "UDP", "description": "LiveKit media (UDP mux)"},
|
||||||
{"port": "5349", "protocol": "TCP", "description": "TURN over TLS"},
|
{"port": "5349", "protocol": "TCP", "description": "TURN over TLS"},
|
||||||
{"port": "3478", "protocol": "UDP", "description": "TURN (STUN/relay)"},
|
{"port": "3478", "protocol": "UDP", "description": "TURN (STUN/relay)"},
|
||||||
{"port": "30000-40000", "protocol": "TCP/UDP", "description": "TURN relay (WebRTC)"},
|
{"port": "30000-40000", "protocol": "TCP/UDP", "description": "TURN relay (WebRTC)"},
|
||||||
@@ -222,28 +222,16 @@ FEATURE_REGISTRY = [
|
|||||||
"conflicts_with": [],
|
"conflicts_with": [],
|
||||||
"port_requirements": [],
|
"port_requirements": [],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "bip110",
|
|
||||||
"name": "Bitcoin Knots + BIP110",
|
|
||||||
"description": "Only one Bitcoin node implementation can be active at a time: Bitcoin Knots (default), Bitcoin Knots + BIP110, or Bitcoin Core. Enabling this option replaces the default Bitcoin Knots with Bitcoin Knots + BIP110 consensus changes. It will disable the currently active alternative.",
|
|
||||||
"category": "bitcoin",
|
|
||||||
"needs_domain": False,
|
|
||||||
"domain_name": None,
|
|
||||||
"needs_ddns": False,
|
|
||||||
"extra_fields": [],
|
|
||||||
"conflicts_with": ["bitcoin-core"],
|
|
||||||
"port_requirements": [],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "bitcoin-core",
|
"id": "bitcoin-core",
|
||||||
"name": "Bitcoin Core",
|
"name": "Bitcoin Core",
|
||||||
"description": "Only one Bitcoin node implementation can be active at a time: Bitcoin Knots (default), Bitcoin Knots + BIP110, or Bitcoin Core. Enabling this option replaces the default Bitcoin Knots with Bitcoin Core. It will disable the currently active alternative.",
|
"description": "Only one Bitcoin node implementation can be active: Bitcoin Knots + BIP110 (default) or Bitcoin Core. Enabling this replaces Knots + BIP110 with Bitcoin Core. Your timechain data is preserved.",
|
||||||
"category": "bitcoin",
|
"category": "bitcoin",
|
||||||
"needs_domain": False,
|
"needs_domain": False,
|
||||||
"domain_name": None,
|
"domain_name": None,
|
||||||
"needs_ddns": False,
|
"needs_ddns": False,
|
||||||
"extra_fields": [],
|
"extra_fields": [],
|
||||||
"conflicts_with": ["bip110"],
|
"conflicts_with": [],
|
||||||
"port_requirements": [],
|
"port_requirements": [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -277,13 +265,16 @@ FEATURE_REGISTRY = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Feature ids that have been removed/deprecated. The Hub must never write these
|
||||||
|
# back into custom.nix, and should strip any it finds (see startup migration).
|
||||||
|
DEPRECATED_FEATURE_IDS: set[str] = {"bip110"}
|
||||||
|
|
||||||
# Map feature IDs to their systemd units in config.json
|
# Map feature IDs to their systemd units in config.json
|
||||||
FEATURE_SERVICE_MAP = {
|
FEATURE_SERVICE_MAP = {
|
||||||
"rdp": "gnome-remote-desktop.service",
|
"rdp": "gnome-remote-desktop.service",
|
||||||
"haven": "haven-relay.service",
|
"haven": "haven-relay.service",
|
||||||
"element-calling": "livekit.service",
|
"element-calling": "livekit.service",
|
||||||
"mempool": "mempool.service",
|
"mempool": "mempool.service",
|
||||||
"bip110": None,
|
|
||||||
"bitcoin-core": None,
|
"bitcoin-core": None,
|
||||||
"btcpay-web": "btcpayserver.service",
|
"btcpay-web": "btcpayserver.service",
|
||||||
"sshd": "sshd.service",
|
"sshd": "sshd.service",
|
||||||
@@ -295,7 +286,7 @@ _PORTS_MATRIX_FEDERATION = [
|
|||||||
]
|
]
|
||||||
_PORTS_ELEMENT_CALLING = [
|
_PORTS_ELEMENT_CALLING = [
|
||||||
{"port": "7881", "protocol": "TCP", "description": "LiveKit WebRTC signalling"},
|
{"port": "7881", "protocol": "TCP", "description": "LiveKit WebRTC signalling"},
|
||||||
{"port": "7882-7894", "protocol": "UDP", "description": "LiveKit media streams"},
|
{"port": "7882", "protocol": "UDP", "description": "LiveKit media (UDP mux)"},
|
||||||
{"port": "5349", "protocol": "TCP", "description": "TURN over TLS"},
|
{"port": "5349", "protocol": "TCP", "description": "TURN over TLS"},
|
||||||
{"port": "3478", "protocol": "UDP", "description": "TURN (STUN/relay)"},
|
{"port": "3478", "protocol": "UDP", "description": "TURN (STUN/relay)"},
|
||||||
{"port": "30000-40000", "protocol": "TCP/UDP", "description": "TURN relay (WebRTC)"},
|
{"port": "30000-40000", "protocol": "TCP/UDP", "description": "TURN relay (WebRTC)"},
|
||||||
@@ -331,7 +322,6 @@ SERVICE_DOMAIN_MAP: dict[str, str] = {
|
|||||||
|
|
||||||
# For features that share a unit, disambiguate by icon field
|
# For features that share a unit, disambiguate by icon field
|
||||||
FEATURE_ICON_MAP = {
|
FEATURE_ICON_MAP = {
|
||||||
"bip110": "bip110",
|
|
||||||
"bitcoin-core": "bitcoin-core",
|
"bitcoin-core": "bitcoin-core",
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -352,7 +342,7 @@ ROLE_CATEGORIES: dict[str, set[str] | None] = {
|
|||||||
ROLE_FEATURES: dict[str, set[str] | None] = {
|
ROLE_FEATURES: dict[str, set[str] | None] = {
|
||||||
"server_plus_desktop": None,
|
"server_plus_desktop": None,
|
||||||
"desktop": {"rdp", "sshd"},
|
"desktop": {"rdp", "sshd"},
|
||||||
"node": {"rdp", "bip110", "bitcoin-core", "mempool", "btcpay-web", "sshd"},
|
"node": {"rdp", "bitcoin-core", "mempool", "btcpay-web", "sshd"},
|
||||||
}
|
}
|
||||||
|
|
||||||
SERVICE_DESCRIPTIONS: dict[str, str] = {
|
SERVICE_DESCRIPTIONS: dict[str, str] = {
|
||||||
@@ -652,6 +642,37 @@ templates = Jinja2Templates(directory=os.path.join(_BASE_DIR, "templates"))
|
|||||||
|
|
||||||
# ── Static asset cache-busting ────────────────────────────────────
|
# ── Static asset cache-busting ────────────────────────────────────
|
||||||
|
|
||||||
|
def _compute_asset_version() -> str:
|
||||||
|
"""Return a 16-char asset version from Nix store hash or static/template metadata."""
|
||||||
|
nix_match = re.search(r"/nix/store/([a-z0-9]{32})-", os.path.realpath(_BASE_DIR))
|
||||||
|
if nix_match:
|
||||||
|
return nix_match.group(1)[:16]
|
||||||
|
|
||||||
|
hasher = hashlib.sha256()
|
||||||
|
for root in (
|
||||||
|
os.path.join(_BASE_DIR, "static"),
|
||||||
|
os.path.join(_BASE_DIR, "templates"),
|
||||||
|
):
|
||||||
|
if not os.path.isdir(root):
|
||||||
|
continue
|
||||||
|
for dirpath, dirnames, filenames in os.walk(root):
|
||||||
|
dirnames.sort()
|
||||||
|
for filename in sorted(filenames):
|
||||||
|
path = os.path.join(dirpath, filename)
|
||||||
|
try:
|
||||||
|
stat = os.stat(path)
|
||||||
|
except OSError:
|
||||||
|
continue
|
||||||
|
hasher.update(path.encode())
|
||||||
|
hasher.update(b"\0")
|
||||||
|
hasher.update(f"{stat.st_mtime_ns}:{stat.st_size}".encode())
|
||||||
|
hasher.update(b"\0")
|
||||||
|
return hasher.hexdigest()[:16]
|
||||||
|
|
||||||
|
|
||||||
|
ASSET_VERSION = _compute_asset_version()
|
||||||
|
|
||||||
|
|
||||||
def _file_hash(filename: str) -> str:
|
def _file_hash(filename: str) -> str:
|
||||||
"""Return first 8 chars of the MD5 hex digest for a static file."""
|
"""Return first 8 chars of the MD5 hex digest for a static file."""
|
||||||
path = os.path.join(_BASE_DIR, "static", filename)
|
path = os.path.join(_BASE_DIR, "static", filename)
|
||||||
@@ -894,7 +915,7 @@ def _get_firewall_allowed_ports() -> dict[str, set[int]]:
|
|||||||
|
|
||||||
|
|
||||||
def _port_range_to_ints(port_str: str) -> list[int]:
|
def _port_range_to_ints(port_str: str) -> list[int]:
|
||||||
"""Convert a port string like ``"443"``, ``"7882-7894"`` to a list of ints."""
|
"""Convert a port string like ``"443"``, ``"30000-40000"`` to a list of ints."""
|
||||||
port_str = port_str.strip()
|
port_str = port_str.strip()
|
||||||
if re.match(r'^\d+$', port_str):
|
if re.match(r'^\d+$', port_str):
|
||||||
return [int(port_str)]
|
return [int(port_str)]
|
||||||
@@ -1488,7 +1509,9 @@ def _read_hub_overrides() -> tuple[dict, str | None, str | None, str | None]:
|
|||||||
r'sovran_systemsOS\.features\.([a-zA-Z0-9_-]+)\s*=\s*(?:lib\.mkForce\s+)?(true|false)\s*;',
|
r'sovran_systemsOS\.features\.([a-zA-Z0-9_-]+)\s*=\s*(?:lib\.mkForce\s+)?(true|false)\s*;',
|
||||||
section,
|
section,
|
||||||
):
|
):
|
||||||
features[m.group(1)] = m.group(2) == "true"
|
feat_id = m.group(1)
|
||||||
|
if feat_id not in DEPRECATED_FEATURE_IDS:
|
||||||
|
features[feat_id] = m.group(2) == "true"
|
||||||
for m in re.finditer(
|
for m in re.finditer(
|
||||||
r'sovran_systemsOS\.web\.btcpayserver\s*=\s*(?:lib\.mkForce\s+)?(true|false)\s*;',
|
r'sovran_systemsOS\.web\.btcpayserver\s*=\s*(?:lib\.mkForce\s+)?(true|false)\s*;',
|
||||||
section,
|
section,
|
||||||
@@ -1521,6 +1544,8 @@ def _write_hub_overrides(features: dict, nostr_npub: str | None, timezone: str |
|
|||||||
"""Write the Hub Managed section inside custom.nix."""
|
"""Write the Hub Managed section inside custom.nix."""
|
||||||
lines = []
|
lines = []
|
||||||
for feat_id, enabled in features.items():
|
for feat_id, enabled in features.items():
|
||||||
|
if feat_id in DEPRECATED_FEATURE_IDS:
|
||||||
|
continue
|
||||||
val = "true" if enabled else "false"
|
val = "true" if enabled else "false"
|
||||||
if feat_id == "btcpay-web":
|
if feat_id == "btcpay-web":
|
||||||
lines.append(f" sovran_systemsOS.web.btcpayserver = lib.mkForce {val};")
|
lines.append(f" sovran_systemsOS.web.btcpayserver = lib.mkForce {val};")
|
||||||
@@ -1566,6 +1591,40 @@ def _write_hub_overrides(features: dict, nostr_npub: str | None, timezone: str |
|
|||||||
f.write(content)
|
f.write(content)
|
||||||
|
|
||||||
|
|
||||||
|
def _migrate_strip_deprecated_features() -> None:
|
||||||
|
"""One-time migration: remove deprecated feature lines from the Hub Managed
|
||||||
|
section of custom.nix. Any feature id in DEPRECATED_FEATURE_IDS is dropped
|
||||||
|
while all other Hub-managed settings (other features, nostr_npub, timezone,
|
||||||
|
locale) are preserved byte-for-byte in meaning.
|
||||||
|
|
||||||
|
This is a no-op (and never raises) if CUSTOM_NIX is missing, unreadable, or
|
||||||
|
contains no deprecated lines.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with open(CUSTOM_NIX, "r") as f:
|
||||||
|
content = f.read()
|
||||||
|
except (FileNotFoundError, OSError):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Quick-exit: if none of the deprecated ids appear, nothing to do.
|
||||||
|
hub_begin = content.find(HUB_BEGIN)
|
||||||
|
hub_end = content.find(HUB_END)
|
||||||
|
if hub_begin == -1 or hub_end == -1:
|
||||||
|
return
|
||||||
|
section = content[hub_begin:hub_end]
|
||||||
|
if not any(f"features.{dep_id}" in section for dep_id in DEPRECATED_FEATURE_IDS):
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
features, nostr_npub, timezone, locale = _read_hub_overrides()
|
||||||
|
# _read_hub_overrides already excludes DEPRECATED_FEATURE_IDS, so
|
||||||
|
# calling _write_hub_overrides with its output drops the stale lines.
|
||||||
|
_write_hub_overrides(features, nostr_npub, timezone, locale)
|
||||||
|
except Exception:
|
||||||
|
# Never let a migration failure break startup.
|
||||||
|
logger.exception("_migrate_strip_deprecated_features: unexpected error (non-fatal)")
|
||||||
|
|
||||||
|
|
||||||
# ── Feature status helpers ─────────────────────────────────────────
|
# ── Feature status helpers ─────────────────────────────────────────
|
||||||
|
|
||||||
def _is_feature_enabled_in_config(feature_id: str) -> bool | None:
|
def _is_feature_enabled_in_config(feature_id: str) -> bool | None:
|
||||||
@@ -1575,7 +1634,7 @@ def _is_feature_enabled_in_config(feature_id: str) -> bool | None:
|
|||||||
return False # Default off in Node role; only on via explicit hub toggle
|
return False # Default off in Node role; only on via explicit hub toggle
|
||||||
unit = FEATURE_SERVICE_MAP.get(feature_id)
|
unit = FEATURE_SERVICE_MAP.get(feature_id)
|
||||||
if unit is None:
|
if unit is None:
|
||||||
return None # bip110, bitcoin-core — can't determine from config
|
return None # bitcoin-core — can't determine from config
|
||||||
cfg = load_config()
|
cfg = load_config()
|
||||||
for svc in cfg.get("services", []):
|
for svc in cfg.get("services", []):
|
||||||
if svc.get("unit") == unit:
|
if svc.get("unit") == unit:
|
||||||
@@ -1894,7 +1953,10 @@ def _verify_support_removed() -> bool:
|
|||||||
|
|
||||||
@app.get("/login", response_class=HTMLResponse)
|
@app.get("/login", response_class=HTMLResponse)
|
||||||
async def login_page(request: Request):
|
async def login_page(request: Request):
|
||||||
return templates.TemplateResponse("login.html", {"request": request})
|
return templates.TemplateResponse("login.html", {
|
||||||
|
"request": request,
|
||||||
|
"asset_version": ASSET_VERSION,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
@app.get("/auto-login")
|
@app.get("/auto-login")
|
||||||
@@ -1961,6 +2023,7 @@ async def api_logout(request: Request):
|
|||||||
async def index(request: Request):
|
async def index(request: Request):
|
||||||
return templates.TemplateResponse("index.html", {
|
return templates.TemplateResponse("index.html", {
|
||||||
"request": request,
|
"request": request,
|
||||||
|
"asset_version": ASSET_VERSION,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -1969,6 +2032,7 @@ async def onboarding(request: Request):
|
|||||||
_ensure_onboarding_reopened_for_migration()
|
_ensure_onboarding_reopened_for_migration()
|
||||||
return templates.TemplateResponse("onboarding.html", {
|
return templates.TemplateResponse("onboarding.html", {
|
||||||
"request": request,
|
"request": request,
|
||||||
|
"asset_version": ASSET_VERSION,
|
||||||
"onboarding_js_hash": _ONBOARDING_JS_HASH,
|
"onboarding_js_hash": _ONBOARDING_JS_HASH,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -2189,6 +2253,16 @@ _BTC_VERSION_CACHE_TTL = 60 # seconds — version doesn't change at runtime
|
|||||||
# Cache for ``bitcoind --version`` output (available even before RPC is ready)
|
# Cache for ``bitcoind --version`` output (available even before RPC is ready)
|
||||||
_btcd_version_cache: tuple[float, str | None] = (0.0, None)
|
_btcd_version_cache: tuple[float, str | None] = (0.0, None)
|
||||||
|
|
||||||
|
# Cache for ``bitcoin-cli getdeploymentinfo`` output (BIP-110 live status)
|
||||||
|
_btc_deployment_cache: tuple[float, dict | None] = (0.0, None)
|
||||||
|
|
||||||
|
# Bitcoin Knots exposes BIP-110 as the `reduced_data` versionbits deployment
|
||||||
|
# (RDTS, bit 4) in getdeploymentinfo. See Knots src/deploymentinfo.cpp,
|
||||||
|
# src/kernel/chainparams.cpp, and doc/bips.md.
|
||||||
|
BIP110_DEPLOYMENT_NAMES = {"reduced_data", "rdts", "bip110", "uasf-bip110"}
|
||||||
|
BIP110_VERSIONBITS_BIT = 4
|
||||||
|
BIP110_SUBVERSION_MARKERS = {"bip110", "uasf-bip110", "reduced_data", "rdts"}
|
||||||
|
|
||||||
|
|
||||||
# ── Generic service version detection (NixOS store path) ─────────
|
# ── Generic service version detection (NixOS store path) ─────────
|
||||||
|
|
||||||
@@ -2303,12 +2377,160 @@ def _get_bitcoin_version_info() -> dict | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _get_bitcoin_deployment_info() -> dict | None:
|
||||||
|
"""Call bitcoin-cli getdeploymentinfo and return parsed JSON, or None on error.
|
||||||
|
|
||||||
|
Results are cached for _BTC_VERSION_CACHE_TTL seconds. Never raises.
|
||||||
|
"""
|
||||||
|
global _btc_deployment_cache
|
||||||
|
now = time.monotonic()
|
||||||
|
cached_at, cached_val = _btc_deployment_cache
|
||||||
|
if now - cached_at < _BTC_VERSION_CACHE_TTL:
|
||||||
|
return cached_val
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["bitcoin-cli", f"-datadir={BITCOIN_DATADIR}", "getdeploymentinfo"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=10,
|
||||||
|
)
|
||||||
|
if result.returncode != 0:
|
||||||
|
_btc_deployment_cache = (now, None)
|
||||||
|
return None
|
||||||
|
info = json.loads(result.stdout)
|
||||||
|
_btc_deployment_cache = (now, info)
|
||||||
|
return info
|
||||||
|
except Exception:
|
||||||
|
_btc_deployment_cache = (now, None)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _get_bip110_status() -> dict:
|
||||||
|
"""Return a dict describing the live BIP-110 deployment/signaling state.
|
||||||
|
|
||||||
|
The returned struct has four stable keys::
|
||||||
|
|
||||||
|
{
|
||||||
|
"supported": bool, # node build is BIP-110-capable
|
||||||
|
"signaling": bool, # node is actively signaling / locked-in / active
|
||||||
|
"state": str, # "active" | "locked_in" | "signaling" |
|
||||||
|
# "not_signaling" | "unsupported" | "unknown"
|
||||||
|
"source": str, # "getdeploymentinfo" | "subversion" | "none"
|
||||||
|
}
|
||||||
|
|
||||||
|
Resolution order (authoritative → fallback → honest unknown):
|
||||||
|
|
||||||
|
1. ``getdeploymentinfo`` (authoritative) — scan ``deployments`` for BIP-110.
|
||||||
|
Bitcoin Knots currently exposes BIP-110 as ``reduced_data`` (RDTS, bit 4;
|
||||||
|
see Knots deploymentinfo.cpp / chainparams.cpp / doc/bips.md), so matching
|
||||||
|
first uses known deployment names, then falls back to versionbits bit 4.
|
||||||
|
|
||||||
|
2. Subversion fallback — if getdeploymentinfo is unavailable or yields no
|
||||||
|
recognisable BIP-110 entry, inspect the ``subversion`` field from
|
||||||
|
``getnetworkinfo``. A case-insensitive match for known BIP-110 markers
|
||||||
|
(including "bip110", "uasf-bip110", "reduced_data", "rdts") is treated as
|
||||||
|
"signaling".
|
||||||
|
|
||||||
|
3. Unknown — if the node is entirely unreachable or neither source is
|
||||||
|
conclusive, return state="unknown", signaling=False, source="none".
|
||||||
|
"""
|
||||||
|
_unknown: dict = {"supported": False, "signaling": False, "state": "unknown", "source": "none"}
|
||||||
|
|
||||||
|
def _deployment_bit(entry: dict) -> int | None:
|
||||||
|
bip9 = entry.get("bip9", {}) or {}
|
||||||
|
bip8 = entry.get("bip8", {}) or {}
|
||||||
|
bit = bip9.get("bit")
|
||||||
|
if bit is None:
|
||||||
|
bit = bip8.get("bit")
|
||||||
|
if bit is None:
|
||||||
|
bit = entry.get("bit")
|
||||||
|
return bit
|
||||||
|
|
||||||
|
# ── 1. getdeploymentinfo (authoritative) ──────────────────────────
|
||||||
|
deploy_info = _get_bitcoin_deployment_info()
|
||||||
|
if deploy_info is not None:
|
||||||
|
deployments = deploy_info.get("deployments", {})
|
||||||
|
if isinstance(deployments, dict):
|
||||||
|
matched_entry: dict | None = None
|
||||||
|
|
||||||
|
# Primary match: known deployment names (case-insensitive exact match)
|
||||||
|
for key, entry in deployments.items():
|
||||||
|
if not isinstance(entry, dict):
|
||||||
|
continue
|
||||||
|
key_lower = key.lower()
|
||||||
|
if key_lower not in BIP110_DEPLOYMENT_NAMES:
|
||||||
|
continue
|
||||||
|
matched_entry = entry
|
||||||
|
break
|
||||||
|
|
||||||
|
# Secondary match: versionbits bit (fallback only)
|
||||||
|
if matched_entry is None:
|
||||||
|
for _, entry in deployments.items():
|
||||||
|
if not isinstance(entry, dict):
|
||||||
|
continue
|
||||||
|
if _deployment_bit(entry) != BIP110_VERSIONBITS_BIT:
|
||||||
|
continue
|
||||||
|
matched_entry = entry
|
||||||
|
break
|
||||||
|
|
||||||
|
if matched_entry is not None:
|
||||||
|
entry = matched_entry
|
||||||
|
|
||||||
|
# bip9 / bip8 status field
|
||||||
|
bip9 = entry.get("bip9", {}) or {}
|
||||||
|
bip8 = entry.get("bip8", {}) or {}
|
||||||
|
status = (
|
||||||
|
bip9.get("status")
|
||||||
|
or bip8.get("status")
|
||||||
|
or entry.get("status")
|
||||||
|
or ""
|
||||||
|
).lower()
|
||||||
|
active = entry.get("active", False)
|
||||||
|
|
||||||
|
if active or status == "active":
|
||||||
|
return {"supported": True, "signaling": True, "state": "active", "source": "getdeploymentinfo"}
|
||||||
|
if status == "locked_in":
|
||||||
|
return {"supported": True, "signaling": True, "state": "locked_in", "source": "getdeploymentinfo"}
|
||||||
|
if status in ("started", "defined"):
|
||||||
|
# Check whether deployment is currently signaling in this period.
|
||||||
|
stats = bip9.get("statistics") or bip8.get("statistics") or {}
|
||||||
|
# Some Knots outputs expose only ``count`` (not explicit signaling bool),
|
||||||
|
# so treat count>0 as a conservative signaling indicator for this period.
|
||||||
|
count = stats.get("count")
|
||||||
|
signaling = bool(
|
||||||
|
stats.get("signaling")
|
||||||
|
or stats.get("signalling")
|
||||||
|
or (isinstance(count, int) and count > 0)
|
||||||
|
)
|
||||||
|
if signaling:
|
||||||
|
return {"supported": True, "signaling": True, "state": "signaling", "source": "getdeploymentinfo"}
|
||||||
|
return {"supported": True, "signaling": False, "state": "not_signaling", "source": "getdeploymentinfo"}
|
||||||
|
if status == "failed":
|
||||||
|
return {"supported": True, "signaling": False, "state": "not_signaling", "source": "getdeploymentinfo"}
|
||||||
|
# Entry found but status unrecognised — node supports BIP-110 but state unclear
|
||||||
|
return {"supported": True, "signaling": False, "state": "unknown", "source": "getdeploymentinfo"}
|
||||||
|
|
||||||
|
# ── 2. Subversion fallback ─────────────────────────────────────────
|
||||||
|
net_info = _get_bitcoin_version_info()
|
||||||
|
if net_info is not None:
|
||||||
|
subversion = net_info.get("subversion", "") or ""
|
||||||
|
sv_lower = subversion.lower()
|
||||||
|
if any(marker in sv_lower for marker in BIP110_SUBVERSION_MARKERS):
|
||||||
|
return {"supported": True, "signaling": True, "state": "signaling", "source": "subversion"}
|
||||||
|
# Node is reachable via RPC but no BIP-110 marker found anywhere
|
||||||
|
return {"supported": False, "signaling": False, "state": "unsupported", "source": "subversion"}
|
||||||
|
|
||||||
|
# ── 3. Node unreachable / RPC not ready ───────────────────────────
|
||||||
|
return _unknown
|
||||||
|
|
||||||
|
|
||||||
def _get_bitcoind_version() -> str | None:
|
def _get_bitcoind_version() -> str | None:
|
||||||
"""Run ``bitcoind --version`` and return the raw version string, or None on error.
|
"""Run ``bitcoind --version`` and return the raw version string, or None on error.
|
||||||
|
|
||||||
Parses the first output line to extract the token after "version ".
|
Parses the first output line to extract the token after "version ".
|
||||||
For example: "Bitcoin Knots daemon version v29.3.knots20260210+bip110-v0.4.1"
|
For example: "Bitcoin Knots daemon version v29.3.knots20260508"
|
||||||
returns "v29.3.knots20260210+bip110-v0.4.1".
|
returns "v29.3.knots20260508".
|
||||||
|
|
||||||
Works regardless of whether the RPC server is ready (IBD, warmup, etc.).
|
Works regardless of whether the RPC server is ready (IBD, warmup, etc.).
|
||||||
Results are cached for 60 seconds (_BTC_VERSION_CACHE_TTL).
|
Results are cached for 60 seconds (_BTC_VERSION_CACHE_TTL).
|
||||||
@@ -2343,25 +2565,12 @@ def _get_bitcoind_version() -> str | None:
|
|||||||
def _format_bitcoin_version(raw_version: str, icon: str = "") -> str:
|
def _format_bitcoin_version(raw_version: str, icon: str = "") -> str:
|
||||||
"""Format a raw version string from ``bitcoind --version`` for tile display.
|
"""Format a raw version string from ``bitcoind --version`` for tile display.
|
||||||
|
|
||||||
Strips the ``+bip110-vX.Y.Z`` patch suffix so the base version is shown
|
For the BIP110 tile (icon == "bip110") a " (bip110)" tag is appended,
|
||||||
cleanly (e.g. "v29.3.knots20260210+bip110-v0.4.1" → "v29.3.knots20260210").
|
since mainline Bitcoin Knots (29.3.knots20260508+) now includes BIP-110
|
||||||
For the BIP110 tile (icon == "bip110") a " (bip110 vX.Y.Z)" tag is appended
|
and no longer carries a separate ``+bip110-vX.Y.Z`` suffix.
|
||||||
including the patch version.
|
|
||||||
"""
|
"""
|
||||||
# Extract the BIP110 patch version before stripping the suffix
|
display = raw_version
|
||||||
bip110_ver = ""
|
if icon == "bip110" and "(bip110)" not in display.lower():
|
||||||
bip_match = re.search(r"\+bip110-v(\S+)", raw_version)
|
|
||||||
if bip_match:
|
|
||||||
bip110_ver = bip_match.group(1)
|
|
||||||
|
|
||||||
# Strip the +bip110... suffix for the base Knots version
|
|
||||||
display = re.sub(r"\+bip110\S*", "", raw_version)
|
|
||||||
|
|
||||||
# For BIP110 tile, append both the tag and the patch version
|
|
||||||
if icon == "bip110":
|
|
||||||
if bip110_ver:
|
|
||||||
display += f" (bip110 v{bip110_ver})"
|
|
||||||
elif "(bip110)" not in display.lower():
|
|
||||||
display += " (bip110)"
|
display += " (bip110)"
|
||||||
return display
|
return display
|
||||||
|
|
||||||
@@ -2430,6 +2639,19 @@ async def api_bitcoin_version():
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/bitcoin/bip110")
|
||||||
|
async def api_bitcoin_bip110():
|
||||||
|
"""Return live BIP-110 deployment/signaling status from bitcoin-cli.
|
||||||
|
|
||||||
|
Always returns HTTP 200. When bitcoind is unreachable or the node is mid-IBD
|
||||||
|
the response will contain ``state = "unknown"`` so the UI can render a neutral
|
||||||
|
badge rather than an error toast.
|
||||||
|
"""
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
status = await loop.run_in_executor(None, _get_bip110_status)
|
||||||
|
return status
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/services")
|
@app.get("/api/services")
|
||||||
async def api_services():
|
async def api_services():
|
||||||
cfg = load_config()
|
cfg = load_config()
|
||||||
@@ -2610,6 +2832,8 @@ async def api_services():
|
|||||||
btc_ver = _format_bitcoin_version(raw_ver, icon=icon)
|
btc_ver = _format_bitcoin_version(raw_ver, icon=icon)
|
||||||
service_data["bitcoin_version"] = btc_ver # backwards compat
|
service_data["bitcoin_version"] = btc_ver # backwards compat
|
||||||
service_data["version"] = btc_ver
|
service_data["version"] = btc_ver
|
||||||
|
if icon == "bip110":
|
||||||
|
service_data["bip110"] = await loop.run_in_executor(None, _get_bip110_status)
|
||||||
return service_data
|
return service_data
|
||||||
|
|
||||||
results = await asyncio.gather(*[get_status(s) for s in services])
|
results = await asyncio.gather(*[get_status(s) for s in services])
|
||||||
@@ -2894,6 +3118,8 @@ async def api_service_detail(unit: str, icon: str | None = None):
|
|||||||
btc_ver = _format_bitcoin_version(raw_ver, icon=icon)
|
btc_ver = _format_bitcoin_version(raw_ver, icon=icon)
|
||||||
service_detail["bitcoin_version"] = btc_ver # backwards compat
|
service_detail["bitcoin_version"] = btc_ver # backwards compat
|
||||||
service_detail["version"] = btc_ver
|
service_detail["version"] = btc_ver
|
||||||
|
if icon == "bip110":
|
||||||
|
service_detail["bip110"] = await loop.run_in_executor(None, _get_bip110_status)
|
||||||
return service_detail
|
return service_detail
|
||||||
|
|
||||||
|
|
||||||
@@ -3795,6 +4021,9 @@ async def api_security_reset():
|
|||||||
"/home/free/.ssh",
|
"/home/free/.ssh",
|
||||||
"/var/lib/lnd",
|
"/var/lib/lnd",
|
||||||
"/var/lib/vaultwarden",
|
"/var/lib/vaultwarden",
|
||||||
|
"/etc/nix-bitcoin-secrets",
|
||||||
|
"/home/free/.local/share/Bisq",
|
||||||
|
"/home/free/.bisq",
|
||||||
]
|
]
|
||||||
|
|
||||||
errors: list[str] = []
|
errors: list[str] = []
|
||||||
@@ -4558,6 +4787,14 @@ async def _startup_recover_stale_status():
|
|||||||
await loop.run_in_executor(None, _recover_stale_status, REBUILD_STATUS, REBUILD_LOG, REBUILD_UNIT)
|
await loop.run_in_executor(None, _recover_stale_status, REBUILD_STATUS, REBUILD_LOG, REBUILD_UNIT)
|
||||||
|
|
||||||
|
|
||||||
|
@app.on_event("startup")
|
||||||
|
async def _startup_migrate_deprecated_features():
|
||||||
|
"""Strip deprecated feature lines (e.g. bip110) from the Hub Managed section
|
||||||
|
of custom.nix so they are never re-written and do not cause stale warnings."""
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
await loop.run_in_executor(None, _migrate_strip_deprecated_features)
|
||||||
|
|
||||||
|
|
||||||
async def _background_domain_reachability_checker():
|
async def _background_domain_reachability_checker():
|
||||||
"""Periodically curl configured domains and cache reachability results."""
|
"""Periodically curl configured domains and cache reachability results."""
|
||||||
await asyncio.sleep(_DOMAIN_REACHABILITY_STARTUP_DELAY)
|
await asyncio.sleep(_DOMAIN_REACHABILITY_STARTUP_DELAY)
|
||||||
|
|||||||
@@ -155,6 +155,69 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── BIP-110 status badge (tile + detail modal) ───────────────────── */
|
||||||
|
|
||||||
|
.tile-bip110-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 3px;
|
||||||
|
font-size: 0.64rem;
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
margin-top: 4px;
|
||||||
|
white-space: nowrap;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-bip110-badge--active {
|
||||||
|
background: rgba(109, 191, 139, 0.18);
|
||||||
|
color: var(--green);
|
||||||
|
border: 1px solid rgba(109, 191, 139, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-bip110-badge--locked_in {
|
||||||
|
background: rgba(94, 173, 138, 0.15);
|
||||||
|
color: var(--accent-color);
|
||||||
|
border: 1px solid rgba(94, 173, 138, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-bip110-badge--signaling {
|
||||||
|
background: rgba(94, 173, 138, 0.12);
|
||||||
|
color: var(--accent-color);
|
||||||
|
border: 1px solid rgba(94, 173, 138, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-bip110-badge--not_signaling {
|
||||||
|
background: rgba(229, 165, 10, 0.12);
|
||||||
|
color: var(--yellow);
|
||||||
|
border: 1px solid rgba(229, 165, 10, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-bip110-badge--unsupported {
|
||||||
|
background: rgba(94, 122, 106, 0.12);
|
||||||
|
color: var(--grey);
|
||||||
|
border: 1px solid rgba(94, 122, 106, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-bip110-badge--unknown {
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-dim);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bip110-status-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bip110-source-label {
|
||||||
|
color: var(--text-dim);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Service detail modal sections ───────────────────────────────── */
|
/* ── Service detail modal sections ───────────────────────────────── */
|
||||||
|
|
||||||
.svc-detail-section {
|
.svc-detail-section {
|
||||||
|
|||||||
@@ -413,16 +413,11 @@ function handleFeatureToggle(feat, newEnabled) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (conflictNames.length > 0) {
|
if (feat.id === "bitcoin-core") {
|
||||||
var confirmMsg;
|
var confirmMsg = "Only one Bitcoin node implementation can be active. Enabling Bitcoin Core will replace Bitcoin Knots + BIP110 as the active node. Your timechain data will be preserved — you will not need to re-download the timechain. Continue?";
|
||||||
if (feat.id === "bip110") {
|
|
||||||
confirmMsg = "Only one Bitcoin node implementation can be active. Enabling Bitcoin Knots + BIP110 will disable Bitcoin Core (if active). Your timechain data will be preserved — you will not need to re-download the timechain. Continue?";
|
|
||||||
} else if (feat.id === "bitcoin-core") {
|
|
||||||
confirmMsg = "Only one Bitcoin node implementation can be active. Enabling Bitcoin Core will disable Bitcoin Knots + BIP110 (if active). Your timechain data will be preserved — you will not need to re-download the timechain. Continue?";
|
|
||||||
} else {
|
|
||||||
confirmMsg = "This will disable " + conflictNames.join(", ") + ". Continue?";
|
|
||||||
}
|
|
||||||
openFeatureConfirm(confirmMsg, proceedAfterConflictCheck);
|
openFeatureConfirm(confirmMsg, proceedAfterConflictCheck);
|
||||||
|
} else if (conflictNames.length > 0) {
|
||||||
|
openFeatureConfirm("This will disable " + conflictNames.join(", ") + ". Continue?", proceedAfterConflictCheck);
|
||||||
} else {
|
} else {
|
||||||
proceedAfterConflictCheck();
|
proceedAfterConflictCheck();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,3 +60,17 @@ async function apiFetch(path, options) {
|
|||||||
}
|
}
|
||||||
return res.json();
|
return res.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ── BIP-110 badge state config ────────────────────────────────────
|
||||||
|
// Shared lookup used by tiles.js and service-detail.js.
|
||||||
|
// Keys match the "state" values returned by /api/bitcoin/bip110.
|
||||||
|
|
||||||
|
var BIP110_BADGE_CONFIG = {
|
||||||
|
active: { cls: 'tile-bip110-badge--active', label: 'Active', title: 'BIP-110 is active on this node' },
|
||||||
|
locked_in: { cls: 'tile-bip110-badge--locked_in', label: 'Locked In', title: 'BIP-110 is locked in and will activate shortly' },
|
||||||
|
signaling: { cls: 'tile-bip110-badge--signaling', label: 'Signaling', title: 'Node is signaling readiness for BIP-110' },
|
||||||
|
not_signaling: { cls: 'tile-bip110-badge--not_signaling',label: 'Not Signaling', title: 'Node supports BIP-110 but is not signaling this period' },
|
||||||
|
unsupported: { cls: 'tile-bip110-badge--unsupported', label: 'Not Supported', title: 'This node build does not include BIP-110' },
|
||||||
|
unknown: { cls: 'tile-bip110-badge--unknown', label: '\u2014', title: 'Status unavailable (node syncing or RPC not ready)' }
|
||||||
|
};
|
||||||
|
|||||||
@@ -107,6 +107,21 @@ async function openServiceDetailModal(unit, name, icon) {
|
|||||||
'</div>' +
|
'</div>' +
|
||||||
'</div>';
|
'</div>';
|
||||||
|
|
||||||
|
// Section B2: BIP-110 live status (bip110 tile only)
|
||||||
|
if (icon === 'bip110' && data.bip110) {
|
||||||
|
var bip110 = data.bip110;
|
||||||
|
var bip110State = bip110.state || 'unknown';
|
||||||
|
var bip110Cfg = BIP110_BADGE_CONFIG[bip110State] || BIP110_BADGE_CONFIG.unknown;
|
||||||
|
var bip110Source = bip110.source ? ' <span class="bip110-source-label">(source: ' + escHtml(bip110.source) + ')</span>' : '';
|
||||||
|
html += '<div class="svc-detail-section">' +
|
||||||
|
'<div class="svc-detail-section-title">BIP-110 Deployment Status</div>' +
|
||||||
|
'<div class="bip110-status-row">' +
|
||||||
|
'<span class="tile-bip110-badge ' + bip110Cfg.cls + '" title="' + escHtml(bip110Cfg.title) + '">' + escHtml(bip110Cfg.label) + '</span>' +
|
||||||
|
bip110Source +
|
||||||
|
'</div>' +
|
||||||
|
'</div>';
|
||||||
|
}
|
||||||
|
|
||||||
// Section C: Domain diagnostics (domain services)
|
// Section C: Domain diagnostics (domain services)
|
||||||
if (data.needs_domain) {
|
if (data.needs_domain) {
|
||||||
var steps = data.domain_check_steps || [];
|
var steps = data.domain_check_steps || [];
|
||||||
@@ -242,7 +257,7 @@ async function openServiceDetailModal(unit, name, icon) {
|
|||||||
var addonBtnCls = feat.enabled ? "btn btn-close-modal" : "btn btn-primary";
|
var addonBtnCls = feat.enabled ? "btn btn-close-modal" : "btn btn-primary";
|
||||||
|
|
||||||
// Section title: use a more specific label for mutually-exclusive Bitcoin node features
|
// Section title: use a more specific label for mutually-exclusive Bitcoin node features
|
||||||
var addonSectionTitle = (feat.id === "bip110" || feat.id === "bitcoin-core")
|
var addonSectionTitle = (feat.id === "bitcoin-core")
|
||||||
? "\u20BF Bitcoin Node Selection"
|
? "\u20BF Bitcoin Node Selection"
|
||||||
: "\uD83D\uDD27 Addon Feature";
|
: "\uD83D\uDD27 Addon Feature";
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,21 @@
|
|||||||
// Keyed by tileId: { progress: float, timestamp: ms }
|
// Keyed by tileId: { progress: float, timestamp: ms }
|
||||||
var _btcSyncPrev = {};
|
var _btcSyncPrev = {};
|
||||||
|
|
||||||
|
// ── BIP-110 badge helper ──────────────────────────────────────────
|
||||||
|
|
||||||
|
function _renderBip110Badge(bip110) {
|
||||||
|
if (!bip110) return '';
|
||||||
|
var state = bip110.state || 'unknown';
|
||||||
|
var cfg = BIP110_BADGE_CONFIG[state] || BIP110_BADGE_CONFIG.unknown;
|
||||||
|
return '<div class="tile-bip110-badge ' + cfg.cls + '" title="' + escHtml(cfg.title) + '">' + escHtml(cfg.label) + '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function _firstElementFromHtml(html) {
|
||||||
|
var tmp = document.createElement("div");
|
||||||
|
tmp.innerHTML = html;
|
||||||
|
return tmp.firstElementChild || null;
|
||||||
|
}
|
||||||
|
|
||||||
// ── Render: initial build ─────────────────────────────────────────
|
// ── Render: initial build ─────────────────────────────────────────
|
||||||
|
|
||||||
function buildTiles(services, categoryLabels) {
|
function buildTiles(services, categoryLabels) {
|
||||||
@@ -165,7 +180,8 @@ function buildTile(svc) {
|
|||||||
|
|
||||||
var ver = svc.version || svc.bitcoin_version || '';
|
var ver = svc.version || svc.bitcoin_version || '';
|
||||||
var versionLabel = ver ? '<div class="tile-version">' + escHtml(ver) + '</div>' : '';
|
var versionLabel = ver ? '<div class="tile-version">' + escHtml(ver) + '</div>' : '';
|
||||||
tile.innerHTML = '<img class="tile-icon" src="/static/icons/' + escHtml(svc.icon) + '.svg" alt="' + escHtml(svc.name) + '" onerror="this.style.display=\'none\';this.nextElementSibling.style.display=\'flex\'"><div class="tile-icon-fallback" style="display:none">?</div><div class="tile-name">' + escHtml(svc.name) + '</div>' + versionLabel + '<div class="tile-status"><span class="status-dot ' + sc + '"></span><span class="status-text">' + st + '</span></div>';
|
var bip110Badge = (svc.icon === 'bip110') ? _renderBip110Badge(svc.bip110) : '';
|
||||||
|
tile.innerHTML = '<img class="tile-icon" src="/static/icons/' + escHtml(svc.icon) + '.svg" alt="' + escHtml(svc.name) + '" onerror="this.style.display=\'none\';this.nextElementSibling.style.display=\'flex\'"><div class="tile-icon-fallback" style="display:none">?</div><div class="tile-name">' + escHtml(svc.name) + '</div>' + versionLabel + bip110Badge + '<div class="tile-status"><span class="status-dot ' + sc + '"></span><span class="status-text">' + st + '</span></div>';
|
||||||
|
|
||||||
tile.style.cursor = "pointer";
|
tile.style.cursor = "pointer";
|
||||||
tile.addEventListener("click", function() {
|
tile.addEventListener("click", function() {
|
||||||
@@ -265,6 +281,23 @@ function updateTiles(services) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Update BIP-110 badge for bip110 tiles
|
||||||
|
if (svc.icon === 'bip110') {
|
||||||
|
var badgeHtml = _renderBip110Badge(svc.bip110);
|
||||||
|
var badgeEl = tile.querySelector(".tile-bip110-badge");
|
||||||
|
if (badgeEl) {
|
||||||
|
// Replace existing badge in-place
|
||||||
|
var newBadge = _firstElementFromHtml(badgeHtml);
|
||||||
|
if (newBadge) { badgeEl.replaceWith(newBadge); } else { badgeEl.remove(); }
|
||||||
|
} else if (badgeHtml) {
|
||||||
|
// Insert badge after version label (or after tile-name if no version)
|
||||||
|
var anchorEl = tile.querySelector(".tile-version") || tile.querySelector(".tile-name");
|
||||||
|
if (anchorEl) {
|
||||||
|
var newBadgeEl = _firstElementFromHtml(badgeHtml);
|
||||||
|
if (newBadgeEl) anchorEl.insertAdjacentElement("afterend", newBadgeEl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -557,7 +557,7 @@ async function loadStep4() {
|
|||||||
html += '<thead><tr><th>Port</th><th>Protocol</th><th>Forward to</th><th>Purpose</th></tr></thead>';
|
html += '<thead><tr><th>Port</th><th>Protocol</th><th>Forward to</th><th>Purpose</th></tr></thead>';
|
||||||
html += '<tbody>';
|
html += '<tbody>';
|
||||||
html += '<tr><td class="port-req-port">7881</td><td class="port-req-proto">TCP</td><td class="port-req-internal-ip">' + ip + '</td><td class="port-req-desc">LiveKit WebRTC signalling</td></tr>';
|
html += '<tr><td class="port-req-port">7881</td><td class="port-req-proto">TCP</td><td class="port-req-internal-ip">' + ip + '</td><td class="port-req-desc">LiveKit WebRTC signalling</td></tr>';
|
||||||
html += '<tr><td class="port-req-port">7882–7894</td><td class="port-req-proto">UDP</td><td class="port-req-internal-ip">' + ip + '</td><td class="port-req-desc">LiveKit media streams</td></tr>';
|
html += '<tr><td class="port-req-port">7882</td><td class="port-req-proto">UDP</td><td class="port-req-internal-ip">' + ip + '</td><td class="port-req-desc">LiveKit media (UDP mux)</td></tr>';
|
||||||
html += '<tr><td class="port-req-port">5349</td><td class="port-req-proto">TCP</td><td class="port-req-internal-ip">' + ip + '</td><td class="port-req-desc">TURN over TLS</td></tr>';
|
html += '<tr><td class="port-req-port">5349</td><td class="port-req-proto">TCP</td><td class="port-req-internal-ip">' + ip + '</td><td class="port-req-desc">TURN over TLS</td></tr>';
|
||||||
html += '<tr><td class="port-req-port">3478</td><td class="port-req-proto">UDP</td><td class="port-req-internal-ip">' + ip + '</td><td class="port-req-desc">TURN (STUN/relay)</td></tr>';
|
html += '<tr><td class="port-req-port">3478</td><td class="port-req-proto">UDP</td><td class="port-req-internal-ip">' + ip + '</td><td class="port-req-desc">TURN (STUN/relay)</td></tr>';
|
||||||
html += '<tr><td class="port-req-port">30000–40000</td><td class="port-req-proto">TCP/UDP</td><td class="port-req-internal-ip">' + ip + '</td><td class="port-req-desc">TURN relay (WebRTC)</td></tr>';
|
html += '<tr><td class="port-req-port">30000–40000</td><td class="port-req-proto">TCP/UDP</td><td class="port-req-internal-ip">' + ip + '</td><td class="port-req-desc">TURN relay (WebRTC)</td></tr>';
|
||||||
|
|||||||
@@ -4,17 +4,17 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Sovran_SystemsOS Hub</title>
|
<title>Sovran_SystemsOS Hub</title>
|
||||||
<link rel="stylesheet" href="/static/css/base.css" />
|
<link rel="stylesheet" href="/static/css/base.css?v={{ asset_version }}" />
|
||||||
<link rel="stylesheet" href="/static/css/buttons.css" />
|
<link rel="stylesheet" href="/static/css/buttons.css?v={{ asset_version }}" />
|
||||||
<link rel="stylesheet" href="/static/css/header.css" />
|
<link rel="stylesheet" href="/static/css/header.css?v={{ asset_version }}" />
|
||||||
<link rel="stylesheet" href="/static/css/layout.css" />
|
<link rel="stylesheet" href="/static/css/layout.css?v={{ asset_version }}" />
|
||||||
<link rel="stylesheet" href="/static/css/tiles.css" />
|
<link rel="stylesheet" href="/static/css/tiles.css?v={{ asset_version }}" />
|
||||||
<link rel="stylesheet" href="/static/css/modals.css" />
|
<link rel="stylesheet" href="/static/css/modals.css?v={{ asset_version }}" />
|
||||||
<link rel="stylesheet" href="/static/css/features.css" />
|
<link rel="stylesheet" href="/static/css/features.css?v={{ asset_version }}" />
|
||||||
<link rel="stylesheet" href="/static/css/onboarding.css" />
|
<link rel="stylesheet" href="/static/css/onboarding.css?v={{ asset_version }}" />
|
||||||
<link rel="stylesheet" href="/static/css/support.css" />
|
<link rel="stylesheet" href="/static/css/support.css?v={{ asset_version }}" />
|
||||||
<link rel="stylesheet" href="/static/css/domain-setup.css" />
|
<link rel="stylesheet" href="/static/css/domain-setup.css?v={{ asset_version }}" />
|
||||||
<link rel="stylesheet" href="/static/css/security.css" />
|
<link rel="stylesheet" href="/static/css/security.css?v={{ asset_version }}" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
@@ -263,16 +263,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/static/js/constants.js"></script>
|
<script src="/static/js/constants.js?v={{ asset_version }}"></script>
|
||||||
<script src="/static/js/state.js"></script>
|
<script src="/static/js/state.js?v={{ asset_version }}"></script>
|
||||||
<script src="/static/js/helpers.js"></script>
|
<script src="/static/js/helpers.js?v={{ asset_version }}"></script>
|
||||||
<script src="/static/js/tiles.js"></script>
|
<script src="/static/js/tiles.js?v={{ asset_version }}"></script>
|
||||||
<script src="/static/js/service-detail.js"></script>
|
<script src="/static/js/service-detail.js?v={{ asset_version }}"></script>
|
||||||
<script src="/static/js/support.js"></script>
|
<script src="/static/js/support.js?v={{ asset_version }}"></script>
|
||||||
<script src="/static/js/update.js"></script>
|
<script src="/static/js/update.js?v={{ asset_version }}"></script>
|
||||||
<script src="/static/js/rebuild.js"></script>
|
<script src="/static/js/rebuild.js?v={{ asset_version }}"></script>
|
||||||
<script src="/static/js/features.js"></script>
|
<script src="/static/js/features.js?v={{ asset_version }}"></script>
|
||||||
<script src="/static/js/security.js"></script>
|
<script src="/static/js/security.js?v={{ asset_version }}"></script>
|
||||||
<script src="/static/js/events.js"></script>
|
<script src="/static/js/events.js?v={{ asset_version }}"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Sovran Hub — Login</title>
|
<title>Sovran Hub — Login</title>
|
||||||
<link rel="stylesheet" href="/static/css/base.css" />
|
<link rel="stylesheet" href="/static/css/base.css?v={{ asset_version }}" />
|
||||||
<link rel="stylesheet" href="/static/css/buttons.css" />
|
<link rel="stylesheet" href="/static/css/buttons.css?v={{ asset_version }}" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="login-wrapper">
|
<div class="login-wrapper">
|
||||||
|
|||||||
@@ -0,0 +1,166 @@
|
|||||||
|
import unittest
|
||||||
|
from unittest.mock import patch
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
import types
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||||
|
|
||||||
|
|
||||||
|
def _install_web_stubs():
|
||||||
|
if "fastapi" in sys.modules:
|
||||||
|
return
|
||||||
|
|
||||||
|
class _HTTPException(Exception):
|
||||||
|
def __init__(self, status_code=None, detail=None):
|
||||||
|
super().__init__(detail)
|
||||||
|
self.status_code = status_code
|
||||||
|
self.detail = detail
|
||||||
|
|
||||||
|
class _FastAPI:
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def mount(self, *args, **kwargs):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def add_middleware(self, *args, **kwargs):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def __getattr__(self, _name):
|
||||||
|
def _decorator_factory(*args, **kwargs):
|
||||||
|
def _decorator(func):
|
||||||
|
return func
|
||||||
|
|
||||||
|
return _decorator
|
||||||
|
|
||||||
|
return _decorator_factory
|
||||||
|
|
||||||
|
class _BaseModel:
|
||||||
|
pass
|
||||||
|
|
||||||
|
class _StaticFiles:
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class _Jinja2Templates:
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class _BaseHTTPMiddleware:
|
||||||
|
pass
|
||||||
|
|
||||||
|
fastapi_module = types.ModuleType("fastapi")
|
||||||
|
fastapi_module.FastAPI = _FastAPI
|
||||||
|
fastapi_module.HTTPException = _HTTPException
|
||||||
|
sys.modules["fastapi"] = fastapi_module
|
||||||
|
|
||||||
|
responses_module = types.ModuleType("fastapi.responses")
|
||||||
|
responses_module.HTMLResponse = object
|
||||||
|
responses_module.JSONResponse = object
|
||||||
|
responses_module.RedirectResponse = object
|
||||||
|
sys.modules["fastapi.responses"] = responses_module
|
||||||
|
|
||||||
|
staticfiles_module = types.ModuleType("fastapi.staticfiles")
|
||||||
|
staticfiles_module.StaticFiles = _StaticFiles
|
||||||
|
sys.modules["fastapi.staticfiles"] = staticfiles_module
|
||||||
|
|
||||||
|
templating_module = types.ModuleType("fastapi.templating")
|
||||||
|
templating_module.Jinja2Templates = _Jinja2Templates
|
||||||
|
sys.modules["fastapi.templating"] = templating_module
|
||||||
|
|
||||||
|
requests_module = types.ModuleType("fastapi.requests")
|
||||||
|
requests_module.Request = object
|
||||||
|
sys.modules["fastapi.requests"] = requests_module
|
||||||
|
|
||||||
|
pydantic_module = types.ModuleType("pydantic")
|
||||||
|
pydantic_module.BaseModel = _BaseModel
|
||||||
|
sys.modules["pydantic"] = pydantic_module
|
||||||
|
|
||||||
|
starlette_base_module = types.ModuleType("starlette.middleware.base")
|
||||||
|
starlette_base_module.BaseHTTPMiddleware = _BaseHTTPMiddleware
|
||||||
|
sys.modules["starlette.middleware.base"] = starlette_base_module
|
||||||
|
|
||||||
|
starlette_middleware_module = types.ModuleType("starlette.middleware")
|
||||||
|
starlette_middleware_module.base = starlette_base_module
|
||||||
|
sys.modules["starlette.middleware"] = starlette_middleware_module
|
||||||
|
|
||||||
|
starlette_module = types.ModuleType("starlette")
|
||||||
|
starlette_module.middleware = starlette_middleware_module
|
||||||
|
sys.modules["starlette"] = starlette_module
|
||||||
|
|
||||||
|
|
||||||
|
_install_web_stubs()
|
||||||
|
from sovran_systemsos_web import server
|
||||||
|
|
||||||
|
|
||||||
|
class Bip110StatusTests(unittest.TestCase):
|
||||||
|
def _status(self, deploy_info, net_info):
|
||||||
|
with patch.object(server, "_get_bitcoin_deployment_info", return_value=deploy_info), patch.object(
|
||||||
|
server, "_get_bitcoin_version_info", return_value=net_info
|
||||||
|
):
|
||||||
|
return server._get_bip110_status()
|
||||||
|
|
||||||
|
def test_started_reduced_data_reports_signaling(self):
|
||||||
|
deploy_info = {
|
||||||
|
"deployments": {
|
||||||
|
"reduced_data": {
|
||||||
|
"type": "bip9",
|
||||||
|
"active": False,
|
||||||
|
"bip9": {
|
||||||
|
"bit": 4,
|
||||||
|
"status": "started",
|
||||||
|
"statistics": {"elapsed": 833, "count": 4, "threshold": 1109},
|
||||||
|
"signalling": "--#--",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = self._status(deploy_info, {"subversion": "/Satoshi:29.0.0/"})
|
||||||
|
self.assertEqual(
|
||||||
|
result,
|
||||||
|
{"supported": True, "signaling": True, "state": "signaling", "source": "getdeploymentinfo"},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_active_reduced_data_reports_active(self):
|
||||||
|
deploy_info = {
|
||||||
|
"deployments": {"reduced_data": {"active": True, "bip9": {"bit": 4, "status": "active"}}}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = self._status(deploy_info, {"subversion": "/Satoshi:29.0.0/"})
|
||||||
|
self.assertEqual(result["state"], "active")
|
||||||
|
self.assertTrue(result["supported"])
|
||||||
|
self.assertTrue(result["signaling"])
|
||||||
|
self.assertEqual(result["source"], "getdeploymentinfo")
|
||||||
|
|
||||||
|
def test_locked_in_reduced_data_reports_locked_in(self):
|
||||||
|
deploy_info = {
|
||||||
|
"deployments": {"reduced_data": {"active": False, "bip9": {"bit": 4, "status": "locked_in"}}}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = self._status(deploy_info, {"subversion": "/Satoshi:29.0.0/"})
|
||||||
|
self.assertEqual(result["state"], "locked_in")
|
||||||
|
self.assertTrue(result["supported"])
|
||||||
|
self.assertTrue(result["signaling"])
|
||||||
|
self.assertEqual(result["source"], "getdeploymentinfo")
|
||||||
|
|
||||||
|
def test_no_bip110_deployment_and_plain_subversion_reports_unsupported(self):
|
||||||
|
deploy_info = {
|
||||||
|
"deployments": {
|
||||||
|
"taproot": {"type": "bip9", "active": True, "bip9": {"bit": 2, "status": "active"}},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = self._status(deploy_info, {"subversion": "/Satoshi:27.0.0/"})
|
||||||
|
self.assertEqual(
|
||||||
|
result,
|
||||||
|
{"supported": False, "signaling": False, "state": "unsupported", "source": "subversion"},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_node_unreachable_reports_unknown(self):
|
||||||
|
result = self._status(None, None)
|
||||||
|
self.assertEqual(result, {"supported": False, "signaling": False, "state": "unknown", "source": "none"})
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" width="256" height="256">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#153126"/>
|
||||||
|
<stop offset="55%" stop-color="#0F241B"/>
|
||||||
|
<stop offset="100%" stop-color="#091C14"/>
|
||||||
|
</linearGradient>
|
||||||
|
|
||||||
|
<linearGradient id="outerArc" x1="70" y1="40" x2="190" y2="210" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0%" stop-color="#42F39A"/>
|
||||||
|
<stop offset="45%" stop-color="#28D978"/>
|
||||||
|
<stop offset="100%" stop-color="#1AA45D"/>
|
||||||
|
</linearGradient>
|
||||||
|
|
||||||
|
<linearGradient id="innerArc" x1="90" y1="60" x2="180" y2="190" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0%" stop-color="#27C86F"/>
|
||||||
|
<stop offset="100%" stop-color="#157E49"/>
|
||||||
|
</linearGradient>
|
||||||
|
|
||||||
|
<filter id="innerShade" x="-10%" y="-10%" width="120%" height="120%">
|
||||||
|
<feOffset dx="0" dy="2"/>
|
||||||
|
<feGaussianBlur stdDeviation="5" result="blur"/>
|
||||||
|
<feComposite in="blur" in2="SourceAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||||
|
<feColorMatrix type="matrix" values="
|
||||||
|
0 0 0 0 0
|
||||||
|
0 0 0 0 0
|
||||||
|
0 0 0 0 0
|
||||||
|
0 0 0 .18 0"/>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<rect width="256" height="256" rx="48" ry="48" fill="url(#bg)"/>
|
||||||
|
<rect x="1.5" y="1.5" width="253" height="253" rx="46.5" ry="46.5"
|
||||||
|
fill="none" stroke="rgba(255,255,255,0.08)"/>
|
||||||
|
<rect x="6" y="6" width="244" height="244" rx="42" ry="42"
|
||||||
|
fill="none" filter="url(#innerShade)"/>
|
||||||
|
|
||||||
|
<path d="M128 32 A96 96 0 1 1 58 196"
|
||||||
|
fill="none"
|
||||||
|
stroke="url(#outerArc)"
|
||||||
|
stroke-width="12"
|
||||||
|
stroke-linecap="round"/>
|
||||||
|
|
||||||
|
<path d="M128 56 A72 72 0 1 1 76 178"
|
||||||
|
fill="none"
|
||||||
|
stroke="url(#innerArc)"
|
||||||
|
stroke-width="10"
|
||||||
|
stroke-linecap="round"/>
|
||||||
|
|
||||||
|
<circle cx="128" cy="128" r="8" fill="#F2FFF7"/>
|
||||||
|
<circle cx="128" cy="128" r="18" fill="none" stroke="#7BFFC0" stroke-opacity="0.14" stroke-width="4"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.9 KiB |
+30
-10
@@ -11,6 +11,7 @@
|
|||||||
boot.loader.efi.efiSysMountPoint = "/boot/efi";
|
boot.loader.efi.efiSysMountPoint = "/boot/efi";
|
||||||
boot.kernelPackages = pkgs.linuxPackages_latest;
|
boot.kernelPackages = pkgs.linuxPackages_latest;
|
||||||
boot.kernelParams = [ "quiet" "loglevel=3" "rd.systemd.show_status=false" "udev.log_level=3" ];
|
boot.kernelParams = [ "quiet" "loglevel=3" "rd.systemd.show_status=false" "udev.log_level=3" ];
|
||||||
|
boot.blacklistedKernelModules = [ "rxrpc" ];
|
||||||
|
|
||||||
# ── Filesystems ─────────────────────────────────────────────
|
# ── Filesystems ─────────────────────────────────────────────
|
||||||
fileSystems."/run/media/Second_Drive" = {
|
fileSystems."/run/media/Second_Drive" = {
|
||||||
@@ -25,6 +26,13 @@
|
|||||||
nix.settings = {
|
nix.settings = {
|
||||||
experimental-features = [ "nix-command" "flakes" ];
|
experimental-features = [ "nix-command" "flakes" ];
|
||||||
download-buffer-size = 524288000;
|
download-buffer-size = 524288000;
|
||||||
|
|
||||||
|
# Network resilience for cache.nixos.org (Fastly) flakiness.
|
||||||
|
connect-timeout = 10; # fail-fast on dead TCP connects (default: 0 = unlimited)
|
||||||
|
stalled-download-timeout = 90; # default 300s; retry sooner on stalled transfers
|
||||||
|
download-attempts = 7; # default 5
|
||||||
|
http-connections = 25; # cap concurrency (helps MTU/middlebox paths)
|
||||||
|
fallback = true; # build locally if a substitute can't be fetched
|
||||||
};
|
};
|
||||||
|
|
||||||
# ── Networking ──────────────────────────────────────────────
|
# ── Networking ──────────────────────────────────────────────
|
||||||
@@ -62,7 +70,6 @@
|
|||||||
# ── Desktop ────────────────────────────────────────────────
|
# ── Desktop ────────────────────────────────────────────────
|
||||||
services.displayManager.gdm.enable = true;
|
services.displayManager.gdm.enable = true;
|
||||||
services.displayManager.gdm.autoSuspend = false;
|
services.displayManager.gdm.autoSuspend = false;
|
||||||
services.displayManager.gdm.wayland = true;
|
|
||||||
services.desktopManager.gnome.enable = true;
|
services.desktopManager.gnome.enable = true;
|
||||||
services.printing.enable = true;
|
services.printing.enable = true;
|
||||||
systemd.enableEmergencyMode = false;
|
systemd.enableEmergencyMode = false;
|
||||||
@@ -70,13 +77,16 @@
|
|||||||
security.pam.services.gdm-password.enableGnomeKeyring = true;
|
security.pam.services.gdm-password.enableGnomeKeyring = true;
|
||||||
security.pam.services.gdm-autologin.enableGnomeKeyring = true;
|
security.pam.services.gdm-autologin.enableGnomeKeyring = true;
|
||||||
|
|
||||||
# Declaratively guarantee the GNOME Keyring default pointer exists for the free user.
|
# Declaratively guarantee the GNOME Keyring default pointer exists.
|
||||||
# Running this at the user level prevents root from corrupting ~/.local permissions on fresh installs.
|
# Defining the full path ensures root doesn't accidentally lock the user out of .local
|
||||||
systemd.user.tmpfiles.rules = [
|
systemd.tmpfiles.rules = [
|
||||||
"d %h/.local/share/keyrings 0700 - - - -"
|
"d /home/free/.local 0700 free users -"
|
||||||
"f %h/.local/share/keyrings/default 0600 - - - login\n"
|
"d /home/free/.local/share 0700 free users -"
|
||||||
|
"d /home/free/.local/share/keyrings 0700 free users -"
|
||||||
|
"f /home/free/.local/share/keyrings/default 0600 free users - login\n"
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
# ── Audio ──────────────────────────────────────────────────
|
# ── Audio ──────────────────────────────────────────────────
|
||||||
services.pulseaudio.enable = false;
|
services.pulseaudio.enable = false;
|
||||||
security.rtkit.enable = true;
|
security.rtkit.enable = true;
|
||||||
@@ -100,9 +110,19 @@
|
|||||||
services.flatpak.enable = true;
|
services.flatpak.enable = true;
|
||||||
systemd.services.flatpak-repo = {
|
systemd.services.flatpak-repo = {
|
||||||
wantedBy = [ "multi-user.target" ];
|
wantedBy = [ "multi-user.target" ];
|
||||||
after = [ "network-online.target" ];
|
after = [ "network-online.target" "nss-lookup.target" ];
|
||||||
wants = [ "network-online.target" ];
|
wants = [ "network-online.target" "nss-lookup.target" ];
|
||||||
path = [ pkgs.flatpak ];
|
path = [ pkgs.flatpak ];
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "oneshot";
|
||||||
|
RemainAfterExit = true;
|
||||||
|
Restart = "on-failure";
|
||||||
|
RestartSec = "15s";
|
||||||
|
};
|
||||||
|
unitConfig = {
|
||||||
|
StartLimitIntervalSec = 120;
|
||||||
|
StartLimitBurst = 5;
|
||||||
|
};
|
||||||
script = ''
|
script = ''
|
||||||
flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
|
flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
|
||||||
'';
|
'';
|
||||||
@@ -125,10 +145,10 @@
|
|||||||
ranger fastfetch gedit openssl pwgen
|
ranger fastfetch gedit openssl pwgen
|
||||||
aspell aspellDicts.en lm_sensors
|
aspell aspellDicts.en lm_sensors
|
||||||
hunspell hunspellDicts.en_US
|
hunspell hunspellDicts.en_US
|
||||||
synadm brave dua bitwarden-desktop
|
synadm brave dua
|
||||||
gparted pv unzip parted screen zenity
|
gparted pv unzip parted screen zenity
|
||||||
libargon2 gnome-terminal libreoffice-fresh
|
libargon2 gnome-terminal libreoffice-fresh
|
||||||
dig firefox element-desktop wp-cli axel
|
dig firefox wp-cli axel
|
||||||
lk-jwt-service livekit-libwebrtc livekit-cli livekit
|
lk-jwt-service livekit-libwebrtc livekit-cli livekit
|
||||||
matrix-synapse age
|
matrix-synapse age
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,93 +0,0 @@
|
|||||||
# Sovran Hub — Manual Backup
|
|
||||||
|
|
||||||
The manual backup service copies critical system data from your Sovran Pro to an external USB drive, providing a third copy of your data (your Sovran Pro already maintains an automatic internal backup on its second drive).
|
|
||||||
|
|
||||||
Backups are written to:
|
|
||||||
|
|
||||||
```
|
|
||||||
<USB drive>/Sovran_SystemsOS_Backup/<timestamp>/
|
|
||||||
```
|
|
||||||
|
|
||||||
where `<timestamp>` is formatted as `YYYYMMDD_HHMMSS`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Backup Stages
|
|
||||||
|
|
||||||
The script always attempts all four stages, but skips stages that are irrelevant to the system's configured role (see [Per-Role Breakdown](#per-role-breakdown) below).
|
|
||||||
|
|
||||||
| Stage | Directory | Contents |
|
|
||||||
|-------|-----------|----------|
|
|
||||||
| **1/4 — NixOS config** | `/etc/nixos/` | Full NixOS system configuration: `role-state.nix`, `custom.nix`, flake files, and any other config managed by the Hub |
|
|
||||||
| **2/4 — Secrets** | `/etc/nix-bitcoin-secrets` | Bitcoin/LND secrets stored under `/etc/` |
|
|
||||||
| **3/4 — Home directory** | `/home/` | All user home directories (`.cache/` and Trash are excluded) |
|
|
||||||
| **4/4 — System data** | `/var/lib/` | Full service data tree, including Vaultwarden, bitcoind, LND, sovran-hub config, domains, secrets, and other `/var/lib` service directories (logs excluded as appropriate) |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Per-Role Breakdown
|
|
||||||
|
|
||||||
The script detects the system role at runtime by reading `/var/lib/sovran-hub/config.json` (falling back to `/etc/nixos/role-state.nix`) and adjusts its behaviour accordingly.
|
|
||||||
|
|
||||||
### Server + Desktop (default)
|
|
||||||
|
|
||||||
All services are enabled: Bitcoin, Matrix Synapse, Vaultwarden, WordPress, Nextcloud.
|
|
||||||
|
|
||||||
| Stage | Status | Notes |
|
|
||||||
|-------|--------|-------|
|
|
||||||
| Stage 1 — NixOS config | ✅ Backed up | Full server configuration |
|
|
||||||
| Stage 2 — Secrets | ✅ Backed up | `/etc/nix-bitcoin-secrets` |
|
|
||||||
| Stage 3 — Home directory | ✅ Backed up | Desktop user data |
|
|
||||||
| Stage 4 — System data (`/var/lib`) | ✅ Backed up | Includes Vaultwarden, bitcoind, LND, sovran-hub config, domains, secrets, and all other service data under `/var/lib` (logs excluded) |
|
|
||||||
|
|
||||||
This produces the largest backup. All four stages generate meaningful data.
|
|
||||||
|
|
||||||
### Desktop Only
|
|
||||||
|
|
||||||
All server services are disabled (`bitcoin = false`, `synapse = false`, `vaultwarden = false`, `wordpress = false`, `nextcloud = false`). Only GNOME desktop is active.
|
|
||||||
|
|
||||||
| Stage | Status | Notes |
|
|
||||||
|-------|--------|-------|
|
|
||||||
| Stage 1 — NixOS config | ✅ Backed up | Simpler config (no server services) |
|
|
||||||
| Stage 2 — Secrets | ⏭️ Skipped | `/etc/nix-bitcoin-secrets` is not applicable for Desktop Only role |
|
|
||||||
| Stage 3 — Home directory | ✅ Backed up | **The most important data for this role** |
|
|
||||||
| Stage 4 — System data (`/var/lib`) | ✅ Backed up | Full `/var/lib` backup with `/var/lib/lnd` excluded for Desktop Only role |
|
|
||||||
|
|
||||||
This produces the smallest and fastest backup. Stages 1 and 3 are the primary sources of meaningful data.
|
|
||||||
|
|
||||||
### Node (Bitcoin-only)
|
|
||||||
|
|
||||||
Only the Bitcoin ecosystem is active: `bitcoind`, `electrs`, `lnd`, `rtl`, `btcpay`, `mempool`, and `bip110`. All other server services are disabled.
|
|
||||||
|
|
||||||
| Stage | Status | Notes |
|
|
||||||
|-------|--------|-------|
|
|
||||||
| Stage 1 — NixOS config | ✅ Backed up | Node-specific configuration |
|
|
||||||
| Stage 2 — Secrets | ✅ Backed up | `/etc/nix-bitcoin-secrets` |
|
|
||||||
| Stage 3 — Home directory | ✅ Backed up | User data |
|
|
||||||
| Stage 4 — System data (`/var/lib`) | ✅ Backed up | **Critical** — includes Lightning wallet/channel data plus all other `/var/lib` service data |
|
|
||||||
|
|
||||||
All four stages run, matching Server + Desktop behaviour. Some non-Bitcoin service directories under `/var/lib` may be sparse or absent depending on role.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Backup Manifest
|
|
||||||
|
|
||||||
After all stages complete, the script writes a `BACKUP_MANIFEST.txt` file inside the timestamped backup directory. This file records the date, hostname, detected role, target drive, and a directory listing of everything that was backed up.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Running the Backup
|
|
||||||
|
|
||||||
The backup is triggered from the Sovran Hub web UI. You can also run it directly:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Auto-detect the first external USB drive
|
|
||||||
sudo bash /path/to/sovran-hub-backup.sh
|
|
||||||
|
|
||||||
# Specify a target drive explicitly
|
|
||||||
sudo BACKUP_TARGET=/run/media/<user>/<drive> bash /path/to/sovran-hub-backup.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
The script requires at least **10 GB** of free space on the target drive and will refuse to write to internal system drives.
|
|
||||||
|
|
||||||
Logs are written to `/var/log/sovran-hub-backup.log` and the current status (`RUNNING`, `SUCCESS`, or `FAILED`) is tracked in `/var/log/sovran-hub-backup.status`.
|
|
||||||
@@ -1,472 +0,0 @@
|
|||||||
# Remote Deployment via Headscale (Self-Hosted Tailscale)
|
|
||||||
|
|
||||||
This guide covers the Sovran Systems remote deployment system built on [Headscale](https://headscale.net) — a self-hosted, open-source implementation of the Tailscale coordination server. Freshly booted ISOs automatically join a private WireGuard mesh VPN without any per-machine key pre-generation.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Architecture Overview
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────────────────────┐
|
|
||||||
│ Internet │
|
|
||||||
└────────────┬─────────────────────┬──────────────────────┘
|
|
||||||
│ │
|
|
||||||
▼ ▼
|
|
||||||
┌────────────────────┐ ┌─────────────────────────────────┐
|
|
||||||
│ Admin Workstation │ │ Sovran VPS │
|
|
||||||
│ │ │ ┌─────────────────────────────┐ │
|
|
||||||
│ tailscale up │ │ │ Headscale (port 8080) │ │
|
|
||||||
│ --login-server │◄──┼─►│ Coordination server │ │
|
|
||||||
│ hs.example.com │ │ ├─────────────────────────────┤ │
|
|
||||||
│ │ │ │ Provisioning API (9090) │ │
|
|
||||||
└────────────────────┘ │ │ POST /register │ │
|
|
||||||
│ │ GET /machines │ │
|
|
||||||
│ │ GET /health │ │
|
|
||||||
│ ├─────────────────────────────┤ │
|
|
||||||
│ │ Caddy (80/443) │ │
|
|
||||||
│ │ hs.example.com → :8080 │ │
|
|
||||||
│ │ prov.example.com → :9090 │ │
|
|
||||||
│ └─────────────────────────────┘ │
|
|
||||||
└─────────────────────────────────┘
|
|
||||||
▲
|
|
||||||
│ WireGuard mesh (Tailnet)
|
|
||||||
▼
|
|
||||||
┌─────────────────────────────────┐
|
|
||||||
│ Deploy Target Machine │
|
|
||||||
│ │
|
|
||||||
│ Boot live ISO → │
|
|
||||||
│ sovran-auto-provision → │
|
|
||||||
│ POST /register → │
|
|
||||||
│ tailscale up --authkey=... │
|
|
||||||
└─────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
**Components:**
|
|
||||||
- **`sovran-provisioner.nix`** — NixOS module deployed on a separate VPS; runs Headscale + provisioning API + Caddy.
|
|
||||||
- **Live ISO** (`iso/common.nix`) — Auto-registers with the provisioning server and joins the Tailnet on boot.
|
|
||||||
- **`remote-deploy.nix`** — Post-install NixOS module that uses Tailscale/Headscale for ongoing access.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Part 1: VPS Setup — Deploy `sovran-provisioner.nix`
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
|
|
||||||
- A NixOS VPS (any provider) with a public IP
|
|
||||||
- Two DNS A records pointing to your VPS:
|
|
||||||
- `hs.yourdomain.com` → VPS IP (Headscale coordination server)
|
|
||||||
- `prov.yourdomain.com` → VPS IP (Provisioning API)
|
|
||||||
- Ports 80, 443 (TCP) and 3478 (UDP, STUN/DERP) open in your VPS firewall
|
|
||||||
|
|
||||||
### DNS Records
|
|
||||||
|
|
||||||
| Type | Name | Value |
|
|
||||||
|------|-----------------------|------------|
|
|
||||||
| A | `hs.yourdomain.com` | `<VPS IP>` |
|
|
||||||
| A | `prov.yourdomain.com` | `<VPS IP>` |
|
|
||||||
|
|
||||||
### NixOS Configuration
|
|
||||||
|
|
||||||
Add the following to your VPS's `/etc/nixos/configuration.nix`:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
{ config, lib, pkgs, ... }:
|
|
||||||
|
|
||||||
{
|
|
||||||
imports = [
|
|
||||||
./hardware-configuration.nix
|
|
||||||
/path/to/sovran-provisioner.nix # or fetch from the repo
|
|
||||||
];
|
|
||||||
|
|
||||||
sovranProvisioner = {
|
|
||||||
enable = true;
|
|
||||||
domain = "prov.yourdomain.com";
|
|
||||||
headscaleDomain = "hs.yourdomain.com";
|
|
||||||
|
|
||||||
# Optional: customise defaults
|
|
||||||
headscaleUser = "sovran-deploy"; # namespace for deploy machines
|
|
||||||
adminUser = "admin"; # namespace for your workstation
|
|
||||||
keyExpiry = "1h"; # pre-auth keys expire after 1 hour
|
|
||||||
rateLimitMax = 10; # max registrations per window
|
|
||||||
rateLimitWindow = 60; # window in seconds
|
|
||||||
};
|
|
||||||
|
|
||||||
# Required for Caddy ACME (Let's Encrypt)
|
|
||||||
networking.hostName = "sovran-vps";
|
|
||||||
system.stateVersion = "24.11";
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Deploy
|
|
||||||
|
|
||||||
```bash
|
|
||||||
nixos-rebuild switch
|
|
||||||
```
|
|
||||||
|
|
||||||
Caddy will automatically obtain TLS certificates via Let's Encrypt.
|
|
||||||
|
|
||||||
### Retrieve the Enrollment Token
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cat /var/lib/sovran-provisioner/enroll-token
|
|
||||||
```
|
|
||||||
|
|
||||||
Keep this token secret — it is used to authenticate ISO registrations. The token is auto-generated on first boot and stored at this path. You never need to set it manually. Just `cat` it from the VPS and copy it to `iso/secrets/enroll-token` before building the ISO.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Part 2: Admin Workstation Setup
|
|
||||||
|
|
||||||
Join your Tailnet as an admin so you can reach deployed machines:
|
|
||||||
|
|
||||||
### Install Tailscale
|
|
||||||
|
|
||||||
Follow the [Tailscale installation guide](https://tailscale.com/download) for your OS, or on NixOS:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
services.tailscale.enable = true;
|
|
||||||
```
|
|
||||||
|
|
||||||
### Join the Tailnet
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo tailscale up --login-server https://hs.yourdomain.com --accept-dns=false
|
|
||||||
```
|
|
||||||
|
|
||||||
> **Note:** The `--accept-dns=false` flag prevents Tailscale from taking over your system DNS resolver. This is important if you are behind a VPN (see [Troubleshooting](#troubleshooting) below).
|
|
||||||
|
|
||||||
Tailscale prints a URL. Open it and copy the node key (starts with `mkey:`).
|
|
||||||
|
|
||||||
### Approve the Node in Headscale
|
|
||||||
|
|
||||||
On the VPS, first find the numeric user ID for the `admin` user, then register the node:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Look up the numeric ID for the admin user (Headscale 0.28.0 requires -u <id>)
|
|
||||||
headscale users list -o json
|
|
||||||
|
|
||||||
# Register the node using the numeric user ID
|
|
||||||
headscale nodes register -u <admin-user-id> --key mkey:xxxxxxxxxxxxxxxx
|
|
||||||
```
|
|
||||||
|
|
||||||
Your workstation is now on the Tailnet. You can list nodes:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
headscale nodes list
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Part 3: Building the Deploy ISO
|
|
||||||
|
|
||||||
### Add Secrets (gitignored)
|
|
||||||
|
|
||||||
The secrets directory `iso/secrets/` is gitignored. Populate it before building:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Copy the enrollment token from the VPS
|
|
||||||
ssh root@<VPS> cat /var/lib/sovran-provisioner/enroll-token > iso/secrets/enroll-token
|
|
||||||
|
|
||||||
# Set the provisioner URL
|
|
||||||
echo "https://prov.yourdomain.com" > iso/secrets/provisioner-url
|
|
||||||
```
|
|
||||||
|
|
||||||
These files are baked into the ISO at build time. If the files are absent the ISO still builds — the auto-provision service exits cleanly with "No enroll token found, skipping auto-provision", leaving DIY users unaffected.
|
|
||||||
|
|
||||||
### Build the ISO
|
|
||||||
|
|
||||||
```bash
|
|
||||||
nix build .#nixosConfigurations.sovran_systemsos-iso.config.system.build.isoImage
|
|
||||||
```
|
|
||||||
|
|
||||||
The resulting ISO is in `./result/iso/`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Part 4: Deployment Workflow
|
|
||||||
|
|
||||||
### Step-by-Step
|
|
||||||
|
|
||||||
1. **Hand the ISO to the remote person** — they burn it to a USB drive and boot.
|
|
||||||
|
|
||||||
2. **ISO boots and auto-registers** — `sovran-auto-provision.service` runs automatically:
|
|
||||||
- Reads `enroll-token` and `provisioner-url` from `/etc/sovran/`
|
|
||||||
- `POST https://prov.yourdomain.com/register` with hostname + MAC
|
|
||||||
- Receives a Headscale pre-auth key
|
|
||||||
- Runs `tailscale up --login-server=... --authkey=...`
|
|
||||||
- The machine appears in `headscale nodes list` within ~30 seconds
|
|
||||||
|
|
||||||
3. **Approve the node (if not using auto-approve)** — on the VPS:
|
|
||||||
```bash
|
|
||||||
headscale nodes list
|
|
||||||
# Note the node key for the new machine
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **SSH from your workstation** — once the machine is on the Tailnet:
|
|
||||||
```bash
|
|
||||||
# Get the machine's Tailscale IP
|
|
||||||
headscale nodes list | grep sovran-deploy-
|
|
||||||
|
|
||||||
# SSH in
|
|
||||||
ssh root@100.64.x.x # password: sovran-remote (live ISO default)
|
|
||||||
```
|
|
||||||
|
|
||||||
5. **Run the headless installer**:
|
|
||||||
|
|
||||||
The `--deploy-key` is your SSH public key that gets injected into `root`'s `authorized_keys` on the deployed machine. This grants full root access for initial setup. Generate it once on your workstation if you haven't already:
|
|
||||||
```bash
|
|
||||||
ssh-keygen -t ed25519 -f ~/.ssh/sovran-deploy -C "sovran-deploy"
|
|
||||||
```
|
|
||||||
After deployment is complete and you disable deploy mode, this key is removed.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo sovran-install-headless.sh \
|
|
||||||
--disk /dev/sda \
|
|
||||||
--role server \
|
|
||||||
--deploy-key "$(cat ~/.ssh/sovran-deploy.pub)" \
|
|
||||||
--headscale-server "https://hs.yourdomain.com" \
|
|
||||||
--headscale-key "$(headscale preauthkeys create -u $(headscale users list -o json | jq -r '.[] | select(.name=="sovran-deploy") | .id') -e 2h -o json | jq -r '.key')"
|
|
||||||
```
|
|
||||||
|
|
||||||
6. **Machine reboots into Sovran_SystemsOS** — `deploy-tailscale-connect.service` runs:
|
|
||||||
- Reads `/var/lib/secrets/headscale-authkey`
|
|
||||||
- Joins the Tailnet with a deterministic hostname (`sovran-<hostname>`)
|
|
||||||
|
|
||||||
7. **Post-install SSH and RDP**:
|
|
||||||
```bash
|
|
||||||
# SSH over Tailnet
|
|
||||||
ssh root@<tailscale-ip>
|
|
||||||
|
|
||||||
# RDP over Tailnet (desktop role) — Sovran_SystemsOS uses GNOME Remote Desktop (native Wayland RDP)
|
|
||||||
# Retrieve the auto-generated RDP password:
|
|
||||||
ssh root@<tailscale-ip> cat /var/lib/gnome-remote-desktop/rdp-password
|
|
||||||
# Then connect with any RDP client (Remmina, GNOME Connections, Microsoft Remote Desktop):
|
|
||||||
# Host: <tailscale-ip>:3389 User: sovran Password: <from above>
|
|
||||||
```
|
|
||||||
|
|
||||||
8. **Disable deploy mode** — edit `/etc/nixos/custom.nix` on the target, set `enable = false`, then:
|
|
||||||
```bash
|
|
||||||
sudo nixos-rebuild switch
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Part 5: Post-Install Access
|
|
||||||
|
|
||||||
### SSH
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Over Tailnet
|
|
||||||
ssh root@100.64.x.x
|
|
||||||
```
|
|
||||||
|
|
||||||
### RDP (desktop/server roles)
|
|
||||||
|
|
||||||
Sovran_SystemsOS uses **GNOME Remote Desktop** (native Wayland RDP — not xfreerdp). The RDP service auto-generates credentials on first boot.
|
|
||||||
|
|
||||||
**Username:** `sovran`
|
|
||||||
**Password:** auto-generated — retrieve it via SSH:
|
|
||||||
```bash
|
|
||||||
ssh root@<tailscale-ip> cat /var/lib/gnome-remote-desktop/rdp-password
|
|
||||||
```
|
|
||||||
|
|
||||||
Connect using any RDP client (Remmina, GNOME Connections, Microsoft Remote Desktop) to `<tailscale-ip>:3389`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Security Model
|
|
||||||
|
|
||||||
| Concern | Mitigation |
|
|
||||||
|---------|-----------|
|
|
||||||
| Enrollment token theft | Token only triggers key generation; it does not grant access to the machine itself |
|
|
||||||
| Rogue device joins Tailnet | Visible in `headscale nodes list`; removable instantly with `headscale nodes delete` |
|
|
||||||
| Pre-auth key reuse | Keys are ephemeral and expire in 1 hour (configurable via `keyExpiry`) |
|
|
||||||
| Rate limiting | Provisioning API limits to 10 registrations/minute by default (configurable) |
|
|
||||||
| SSH access | Requires ed25519 key injected at install time; password authentication disabled |
|
|
||||||
| Credential storage | Auth key written to `/var/lib/secrets/headscale-authkey` (mode 600) on the installed OS |
|
|
||||||
|
|
||||||
### Token Rotation
|
|
||||||
|
|
||||||
To rotate the enrollment token:
|
|
||||||
|
|
||||||
1. On the VPS:
|
|
||||||
```bash
|
|
||||||
openssl rand -hex 32 > /var/lib/sovran-provisioner/enroll-token
|
|
||||||
chmod 600 /var/lib/sovran-provisioner/enroll-token
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Update `iso/secrets/enroll-token` and rebuild the ISO.
|
|
||||||
|
|
||||||
Old ISOs with the previous token will fail to register (receive 401).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Monitoring
|
|
||||||
|
|
||||||
### List Active Tailnet Nodes
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# On the VPS
|
|
||||||
headscale nodes list
|
|
||||||
```
|
|
||||||
|
|
||||||
### List Registered Machines (Provisioning API)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -s -H "Authorization: Bearer $(cat /var/lib/sovran-provisioner/enroll-token)" \
|
|
||||||
https://prov.yourdomain.com/machines | jq .
|
|
||||||
```
|
|
||||||
|
|
||||||
### Health Check
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl https://prov.yourdomain.com/health
|
|
||||||
# {"status": "ok"}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Provisioner Logs
|
|
||||||
|
|
||||||
```bash
|
|
||||||
journalctl -u sovran-provisioner -f
|
|
||||||
```
|
|
||||||
|
|
||||||
### Headscale Logs
|
|
||||||
|
|
||||||
```bash
|
|
||||||
journalctl -u headscale -f
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Cleanup
|
|
||||||
|
|
||||||
### Remove a Machine from the Tailnet
|
|
||||||
|
|
||||||
```bash
|
|
||||||
headscale nodes list
|
|
||||||
headscale nodes delete --identifier <id>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Disable Deploy Mode on an Installed Machine
|
|
||||||
|
|
||||||
Edit `/etc/nixos/custom.nix`:
|
|
||||||
|
|
||||||
```nix
|
|
||||||
sovran_systemsOS.deploy.enable = false;
|
|
||||||
```
|
|
||||||
|
|
||||||
Then rebuild:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
nixos-rebuild switch
|
|
||||||
```
|
|
||||||
|
|
||||||
This stops the Tailscale connect service.
|
|
||||||
|
|
||||||
### Revoke All Active Pre-Auth Keys
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# List pre-auth keys (Headscale 0.28.0: no --user flag on list)
|
|
||||||
headscale preauthkeys list
|
|
||||||
|
|
||||||
# Expire a specific key — use numeric user ID (-u <id>)
|
|
||||||
# First find the user ID:
|
|
||||||
headscale users list -o json
|
|
||||||
# Then expire the key:
|
|
||||||
headscale preauthkeys expire -u <user-id> --key <key>
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### VPN Conflicts (Mullvad, WireGuard, etc.)
|
|
||||||
|
|
||||||
**Symptom:** `tailscale up` hangs or fails with `connection refused` on port 443, even though `curl https://hs.yourdomain.com/health` works fine.
|
|
||||||
|
|
||||||
**Cause:** VPNs like Mullvad route all traffic — including Tailscale's control-plane connections — through the VPN tunnel. Additionally, Tailscale's DNS handler (`--accept-dns=true` by default) hijacks DNS resolution and may prevent correct resolution of your Headscale server even when logged out.
|
|
||||||
|
|
||||||
**Solution:**
|
|
||||||
1. Disconnect your VPN temporarily and retry `tailscale up`.
|
|
||||||
2. If you need the VPN active, use split tunneling to exclude `tailscaled`:
|
|
||||||
```bash
|
|
||||||
# Mullvad CLI
|
|
||||||
mullvad split-tunnel add $(pidof tailscaled)
|
|
||||||
```
|
|
||||||
Or in the Mullvad GUI: **Settings → Split tunneling → Add tailscaled**.
|
|
||||||
3. Always pass `--accept-dns=false` when enrolling to avoid DNS hijacking:
|
|
||||||
```bash
|
|
||||||
sudo tailscale up --login-server https://hs.yourdomain.com --authkey <key> --accept-dns=false
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### "RATELIMIT" in tailscaled Logs
|
|
||||||
|
|
||||||
**Symptom:** `journalctl -u tailscaled` shows lines like:
|
|
||||||
```
|
|
||||||
[RATELIMIT] format("Received error: %v")
|
|
||||||
```
|
|
||||||
|
|
||||||
**Cause:** This is **NOT** a server-side rate limit from Headscale. It is tailscaled's internal log suppressor de-duplicating repeated connection-refused error messages. The real underlying error is `connection refused`.
|
|
||||||
|
|
||||||
**What to check:**
|
|
||||||
1. Is Headscale actually running? `curl https://hs.yourdomain.com/health`
|
|
||||||
2. Is your VPN blocking the connection? (see VPN Conflicts above)
|
|
||||||
3. Is there a firewall blocking port 443?
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### "connection refused" on Port 443
|
|
||||||
|
|
||||||
If `tailscale up` fails but `curl` works, the issue is usually DNS or VPN:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Does curl reach Headscale successfully?
|
|
||||||
curl -v https://hs.yourdomain.com/health
|
|
||||||
|
|
||||||
# Force IPv4 vs IPv6 to identify if it's an address-family issue
|
|
||||||
curl -4 https://hs.yourdomain.com/health
|
|
||||||
curl -6 https://hs.yourdomain.com/health
|
|
||||||
|
|
||||||
# Check what IP headscale resolves to
|
|
||||||
dig +short hs.yourdomain.com
|
|
||||||
|
|
||||||
# What resolver is the system using?
|
|
||||||
cat /etc/resolv.conf
|
|
||||||
```
|
|
||||||
|
|
||||||
If curl works but tailscale doesn't, tailscaled may be using a different DNS resolver (e.g. its own `100.100.100.100` stub resolver). Fix: pass `--accept-dns=false`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Headscale User ID Lookup (0.28.0)
|
|
||||||
|
|
||||||
Headscale 0.28.0 removed `--user <name>` in favour of `-u <numeric-id>`. To find the numeric ID for a user:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
headscale users list -o json
|
|
||||||
# Output: [{"id": "1", "name": "sovran-deploy", ...}, ...]
|
|
||||||
|
|
||||||
# One-liner to get the ID for a specific user
|
|
||||||
headscale users list -o json | jq -r '.[] | select(.name=="sovran-deploy") | .id'
|
|
||||||
```
|
|
||||||
|
|
||||||
Then use the numeric ID in subsequent commands:
|
|
||||||
```bash
|
|
||||||
headscale preauthkeys create -u 1 -e 1h -o json
|
|
||||||
headscale nodes register -u 1 --key mkey:xxxx
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Reference
|
|
||||||
|
|
||||||
| Component | Port | Protocol | Description |
|
|
||||||
|-----------|------|----------|-------------|
|
|
||||||
| Caddy | 80 | TCP | HTTP → HTTPS redirect |
|
|
||||||
| Caddy | 443 | TCP | HTTPS (Let's Encrypt) |
|
|
||||||
| Headscale | 8080 | TCP | Coordination server (proxied by Caddy) |
|
|
||||||
| Provisioner | 9090 | TCP | Registration API (proxied by Caddy) |
|
|
||||||
| DERP/STUN | 3478 | UDP | WireGuard relay fallback |
|
|
||||||
| Tailscale | N/A | WireGuard | Mesh VPN between nodes |
|
|
||||||
@@ -1,259 +0,0 @@
|
|||||||
# Tech Support: Security Design, User Flow, and Incident Response
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
The Sovran Hub includes a **Tech Support** feature that lets Sovran Systems
|
|
||||||
staff remotely diagnose and fix issues on a user's machine via SSH — without
|
|
||||||
ever having access to private keys or wallet funds.
|
|
||||||
|
|
||||||
Wallet protection is the default. The user must make an active, time-limited
|
|
||||||
choice to grant support staff access to wallet files, and can revoke that
|
|
||||||
access at any time.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Implementation Details
|
|
||||||
|
|
||||||
### Restricted User Instead of Root
|
|
||||||
|
|
||||||
When a user enables support access the Hub:
|
|
||||||
|
|
||||||
1. Ensures the `sovran-support` system user exists (declared declaratively in
|
|
||||||
`modules/core/tech-support.nix`; the Hub also provisions it on demand as a
|
|
||||||
fallback on non-NixOS systems).
|
|
||||||
2. Writes the Sovran Systems public SSH key **only** to
|
|
||||||
`/var/lib/sovran-support/.ssh/authorized_keys`, not to root's
|
|
||||||
`authorized_keys`.
|
|
||||||
3. Applies POSIX ACLs (`setfacl -R -m u:sovran-support:---`) to every wallet
|
|
||||||
directory that exists on disk, denying all access by the support user.
|
|
||||||
4. Records a timestamped `SUPPORT_ENABLED` event in the audit log at
|
|
||||||
`/var/log/sovran-support-audit.log`.
|
|
||||||
|
|
||||||
When the session ends (or if the Hub cannot create the restricted user), the
|
|
||||||
key is removed and all ACLs are revoked immediately.
|
|
||||||
|
|
||||||
### Protected Wallet Paths
|
|
||||||
|
|
||||||
The following directories are locked by default when a support session starts:
|
|
||||||
|
|
||||||
| Path | Contents |
|
|
||||||
|------|----------|
|
|
||||||
| `/etc/nix-bitcoin-secrets` | nix-bitcoin generated secrets |
|
|
||||||
| `/var/lib/bitcoind` | Bitcoin Core chainstate and wallet |
|
|
||||||
| `/var/lib/lnd` | LND wallet and channel database |
|
|
||||||
| `/home` | User home directories |
|
|
||||||
|
|
||||||
Paths are only locked if they exist on disk at the time the session starts.
|
|
||||||
|
|
||||||
### POSIX ACL Mechanics
|
|
||||||
|
|
||||||
POSIX ACLs on Linux handle access checks in this order:
|
|
||||||
|
|
||||||
1. If the process UID matches the file owner UID → use owner permissions
|
|
||||||
2. **If there is a matching named-user ACL entry → use that entry's
|
|
||||||
permissions** (clamped by the mask entry)
|
|
||||||
3. If any group matches → use group permissions
|
|
||||||
4. Otherwise → use "other" permissions
|
|
||||||
|
|
||||||
Setting `u:sovran-support:---` creates a named-user ACL entry with no
|
|
||||||
permissions. Because the named-user entry is checked before the group/other
|
|
||||||
entries, the support user cannot access those directories regardless of the
|
|
||||||
"other" permission bits.
|
|
||||||
|
|
||||||
`setfacl` and `getfacl` are provided by the `acl` package, which is added to
|
|
||||||
`environment.systemPackages` by `modules/core/tech-support.nix`.
|
|
||||||
|
|
||||||
### Fallback to Root (When Restricted User Cannot Be Created)
|
|
||||||
|
|
||||||
If the `sovran-support` user does not exist and cannot be created (e.g.,
|
|
||||||
`users.mutableUsers = false` and the declarative module has not been deployed
|
|
||||||
yet), the Hub falls back to adding the support key to root's
|
|
||||||
`authorized_keys`. The modal prominently warns the user when this has happened
|
|
||||||
so they can decide whether to end the session.
|
|
||||||
|
|
||||||
### Audit Log
|
|
||||||
|
|
||||||
Every session event is appended to `/var/log/sovran-support-audit.log`:
|
|
||||||
|
|
||||||
```
|
|
||||||
[2025-01-15 14:32:01 UTC] SUPPORT_ENABLED: restricted_user=True acl_applied=True protected_paths=4
|
|
||||||
[2025-01-15 14:45:00 UTC] WALLET_UNLOCKED: duration=3600s expires=2025-01-15 15:45:00 UTC
|
|
||||||
[2025-01-15 15:45:00 UTC] WALLET_RELOCKED: auto-expired
|
|
||||||
[2025-01-15 16:01:22 UTC] SUPPORT_DISABLED
|
|
||||||
```
|
|
||||||
|
|
||||||
The last 100 lines of this log are accessible from the Hub UI while a session
|
|
||||||
is active (or after it ends, until the page is refreshed).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Security Tradeoffs
|
|
||||||
|
|
||||||
### What This Protects Against
|
|
||||||
|
|
||||||
- **Accidental wallet exposure** — support staff cannot read wallet files
|
|
||||||
during a normal session; they must ask the user to explicitly grant access.
|
|
||||||
- **Credential theft** — private keys in the wallet directories are not
|
|
||||||
visible to the `sovran-support` user by default.
|
|
||||||
- **Scope creep** — the restricted user account limits the blast radius of an
|
|
||||||
SSH session compared to direct root access.
|
|
||||||
|
|
||||||
### Known Limitations
|
|
||||||
|
|
||||||
| Limitation | Mitigation |
|
|
||||||
|------------|------------|
|
|
||||||
| Support user still has system-wide bash access | Restrict with `ForceCommand` or AppArmor in the NixOS config if a narrower scope is required |
|
|
||||||
| ACLs apply only to directories that exist at session start | If new wallet directories are created during a session, they are not auto-protected. Re-lock and re-enable support to pick up new paths |
|
|
||||||
| Root fallback grants full access | The Hub UI warns the user prominently; users should end the session if they are uncomfortable |
|
|
||||||
| `setfacl` / ACL filesystem support required | The `acl` package is declared in `tech-support.nix`; most Linux filesystems (ext4, btrfs, xfs) support ACLs by default |
|
|
||||||
| Wallet access grant is time-limited but lazy-expired | Expiry is checked on the next `/api/support/status` poll (every 10 seconds in the UI); there is a small window after expiry |
|
|
||||||
|
|
||||||
### Defense-in-Depth Recommendations
|
|
||||||
|
|
||||||
For environments that require stronger isolation, consider layering one or
|
|
||||||
more additional controls:
|
|
||||||
|
|
||||||
- **`ForceCommand`** in `sshd_config` (or `~/.ssh/authorized_keys` command
|
|
||||||
prefix) to restrict the support user to a specific diagnostic script.
|
|
||||||
- **`ChrootDirectory`** in the `sshd_config` `Match User sovran-support` block
|
|
||||||
to confine the session to a prepared directory tree.
|
|
||||||
- **AppArmor or SELinux** profiles that deny the support process read access
|
|
||||||
to wallet paths at the kernel level.
|
|
||||||
- **Namespace/bind-mount overlays** (e.g., via a wrapper systemd unit) to
|
|
||||||
present a sanitized filesystem view.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## User Flow
|
|
||||||
|
|
||||||
```
|
|
||||||
User opens Hub → Clicks "Tech Support" in sidebar
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
Modal: "Need help from Sovran Systems?"
|
|
||||||
• Explains what will happen
|
|
||||||
• Shows Wallet Protection notice
|
|
||||||
• User clicks "Enable Support Access"
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
Hub: 1. Creates / verifies sovran-support user
|
|
||||||
2. Writes SSH key to that user's authorized_keys
|
|
||||||
3. Applies POSIX ACL deny on all existing wallet paths
|
|
||||||
4. Saves session metadata + writes SUPPORT_ENABLED to audit log
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
Modal: "Support Access is Active"
|
|
||||||
• Live session duration timer
|
|
||||||
• Wallet Files: Protected panel
|
|
||||||
– Optional: "Grant Wallet Access" (time-limited, user-chosen)
|
|
||||||
• "End Support Session" button
|
|
||||||
• "View Audit Log" button
|
|
||||||
│
|
|
||||||
(User grants wallet access)
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
Hub: • Removes ACL deny entries
|
|
||||||
• Records WALLET_UNLOCKED event with expiry time
|
|
||||||
• Starts countdown timer in UI
|
|
||||||
│
|
|
||||||
(Timer expires or user clicks "Re-lock Wallet Now")
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
Hub: • Re-applies ACL deny entries
|
|
||||||
• Removes WALLET_UNLOCK_FILE
|
|
||||||
• Records WALLET_RELOCKED event
|
|
||||||
│
|
|
||||||
(User clicks "End Support Session")
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
Hub: 1. Removes SSH key from sovran-support authorized_keys
|
|
||||||
2. Removes SSH key from root authorized_keys (legacy cleanup)
|
|
||||||
3. Revokes any wallet unlock, re-applies ACL deny
|
|
||||||
4. Verifies key is gone
|
|
||||||
5. Records SUPPORT_DISABLED event
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
Modal: "Support Session Ended — SSH key removed"
|
|
||||||
• Shows verified removal status
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Incident Response
|
|
||||||
|
|
||||||
### Scenario 1 — You accidentally granted wallet access and are unsure what was copied
|
|
||||||
|
|
||||||
**Immediate steps:**
|
|
||||||
|
|
||||||
1. Click **"Re-lock Wallet Now"** in the Hub modal, or click
|
|
||||||
**"End Support Session"** to simultaneously revoke SSH access and wallet
|
|
||||||
access.
|
|
||||||
2. Open the **Audit Log** from the Hub modal and note the timestamps of
|
|
||||||
`WALLET_UNLOCKED` and `WALLET_RELOCKED` events.
|
|
||||||
3. Check `/var/log/auth.log` (or `journalctl -u sshd`) for SSH login events
|
|
||||||
by `sovran-support` during the unlocked window.
|
|
||||||
|
|
||||||
**Assessment:**
|
|
||||||
|
|
||||||
- If no SSH login occurred during the wallet-unlocked window, your keys are
|
|
||||||
safe.
|
|
||||||
- If an SSH login did occur, treat private keys as potentially compromised.
|
|
||||||
|
|
||||||
**Recovery if keys may be compromised:**
|
|
||||||
|
|
||||||
| Wallet | Recovery action |
|
|
||||||
|--------|----------------|
|
|
||||||
| LND | Move all funds out using `lncli sendcoins` to a freshly generated on-chain address; close channels; recreate wallet |
|
|
||||||
| Sparrow | Sweep funds to a new wallet generated on an air-gapped device |
|
|
||||||
| Bisq | Withdraw all BSQ and BTC to external wallets; delete the Bisq data directory and recreate |
|
|
||||||
| nix-bitcoin secrets | Rotate all secrets with `nix-bitcoin-secrets generate` and redeploy |
|
|
||||||
|
|
||||||
**Report the incident:**
|
|
||||||
|
|
||||||
Contact Sovran Systems immediately at support@sovransystems.com with:
|
|
||||||
- The audit log output (`/var/log/sovran-support-audit.log`)
|
|
||||||
- The SSH auth log for the affected time window
|
|
||||||
- A description of what you were troubleshooting
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Scenario 2 — Support session cannot be ended (button fails or server is unresponsive)
|
|
||||||
|
|
||||||
**Manual key removal (run as root on the device):**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Remove from support user's authorized_keys
|
|
||||||
rm -f /var/lib/sovran-support/.ssh/authorized_keys
|
|
||||||
|
|
||||||
# Remove from root's authorized_keys (fallback / legacy)
|
|
||||||
sed -i '/sovransystemsos-support/d' /root/.ssh/authorized_keys
|
|
||||||
|
|
||||||
# Remove wallet unlock state
|
|
||||||
rm -f /var/lib/secrets/support-wallet-unlock
|
|
||||||
|
|
||||||
# Re-apply wallet ACL protections
|
|
||||||
setfacl -R -m u:sovran-support:--- /etc/nix-bitcoin-secrets \
|
|
||||||
/var/lib/bitcoind /var/lib/lnd /home 2>/dev/null || true
|
|
||||||
|
|
||||||
# Restart sshd to drop any active connections
|
|
||||||
systemctl restart sshd
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Scenario 3 — You see an unexpected SUPPORT_ENABLED in the audit log
|
|
||||||
|
|
||||||
This should never happen without physical or remote access to the Hub web
|
|
||||||
interface. If you see an unexpected entry:
|
|
||||||
|
|
||||||
1. Immediately run the manual key removal commands above.
|
|
||||||
2. Change the Sovran Hub web interface password.
|
|
||||||
3. Check `/var/log/nginx/access.log` (or Caddy access logs) for unexpected
|
|
||||||
requests to `/api/support/enable`.
|
|
||||||
4. Consider rebooting the device to clear any in-memory state.
|
|
||||||
5. Report the incident to Sovran Systems.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*This document is part of the Sovran_SystemsOS repository. For the
|
|
||||||
authoritative and up-to-date version, see the repository.*
|
|
||||||
Generated
+44
-95
@@ -1,34 +1,15 @@
|
|||||||
{
|
{
|
||||||
"nodes": {
|
"nodes": {
|
||||||
"bip110": {
|
"btc-clients": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"nixpkgs": "nixpkgs"
|
"nixpkgs": "nixpkgs"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1775155316,
|
"lastModified": 1780397635,
|
||||||
"narHash": "sha256-4H8aEChZ6rra9jd8OcVHgHs3IuzKzpDt4PPtsPJrkyM=",
|
"narHash": "sha256-6WH7LKD6i91VLWoz4mEpoULtqVinCEZxG7ZjJPMSi3k=",
|
||||||
"owner": "emmanuelrosa",
|
|
||||||
"repo": "bitcoin-knots-bip-110-nix",
|
|
||||||
"rev": "663ea34f6f846f48c385a73d4581ba599bb5bbc0",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "emmanuelrosa",
|
|
||||||
"repo": "bitcoin-knots-bip-110-nix",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"btc-clients": {
|
|
||||||
"inputs": {
|
|
||||||
"nixpkgs": "nixpkgs_2",
|
|
||||||
"oldNixpkgs": "oldNixpkgs"
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1776253358,
|
|
||||||
"narHash": "sha256-PApGu30OTySvNZ8H9sgiRfe6VjTuL7PyhzC/o9ghLRA=",
|
|
||||||
"owner": "emmanuelrosa",
|
"owner": "emmanuelrosa",
|
||||||
"repo": "btc-clients-nix",
|
"repo": "btc-clients-nix",
|
||||||
"rev": "89c65cd67be5bff678deffe36ca2ae7b1175c4e0",
|
"rev": "feacd7684dc6bfcd49c57764944a2049bbd71924",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -71,11 +52,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1775087534,
|
"lastModified": 1778716662,
|
||||||
"narHash": "sha256-91qqW8lhL7TLwgQWijoGBbiD4t7/q75KTi8NxjVmSmA=",
|
"narHash": "sha256-m1Yf0wZ8j1OHjTc2UwHwyQRSnNeSgLJOd7q5Y45hzi4=",
|
||||||
"owner": "hercules-ci",
|
"owner": "hercules-ci",
|
||||||
"repo": "flake-parts",
|
"repo": "flake-parts",
|
||||||
"rev": "3107b77cd68437b9a76194f0f7f9c55f2329ca5b",
|
"rev": "f7c1a2d347e4c52d5fb8d10cb4d94b5884e546fb",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -106,16 +87,16 @@
|
|||||||
"inputs": {
|
"inputs": {
|
||||||
"extra-container": "extra-container",
|
"extra-container": "extra-container",
|
||||||
"flake-utils": "flake-utils",
|
"flake-utils": "flake-utils",
|
||||||
"nixpkgs": "nixpkgs_3",
|
"nixpkgs": "nixpkgs_2",
|
||||||
"nixpkgs-25_05": "nixpkgs-25_05",
|
"nixpkgs-25_05": "nixpkgs-25_05",
|
||||||
"nixpkgs-unstable": "nixpkgs-unstable"
|
"nixpkgs-unstable": "nixpkgs-unstable"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1767721199,
|
"lastModified": 1779253922,
|
||||||
"narHash": "sha256-UzRxDiJlopBGPTjyhCdMP+QdTwXK+l+y45urXCyH69A=",
|
"narHash": "sha256-k5DpYVfyy27ELuEiV+51EfVg7B6vKUW63NWeA6eKGd0=",
|
||||||
"owner": "fort-nix",
|
"owner": "fort-nix",
|
||||||
"repo": "nix-bitcoin",
|
"repo": "nix-bitcoin",
|
||||||
"rev": "5b532698ce9e8bd79b07d77ab4fc60e1a8408f73",
|
"rev": "1496f842477976c085cd96f1837ea12444014088",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -127,27 +108,26 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1775054576,
|
"lastModified": 1780218263,
|
||||||
"narHash": "sha256-iiIr1hlTMu2LLARsUYtiqlE90tqocqIMVLK2fIzB/UY=",
|
"narHash": "sha256-T/f0pPDrH3Qc1VXyQXbK7yfHWRn90l3xwplc/nsxin4=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "fc4b9b74d4b0bdbf3c97fef4bd34c05225172912",
|
"rev": "7fc393d1b46fa000d48ff14e8b6a3c9985f03af0",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"ref": "master",
|
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nixpkgs-25_05": {
|
"nixpkgs-25_05": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1767051569,
|
"lastModified": 1767313136,
|
||||||
"narHash": "sha256-0MnuWoN+n1UYaGBIpqpPs9I9ZHW4kynits4mrnh1Pk4=",
|
"narHash": "sha256-16KkgfdYqjaeRGBaYsNrhPRRENs0qzkQVUooNHtoy2w=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "40ee5e1944bebdd128f9fbada44faefddfde29bd",
|
"rev": "ac62194c3917d5f474c1a844b6fd6da2db95077d",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -159,27 +139,27 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs-stable": {
|
"nixpkgs-stable": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1751274312,
|
"lastModified": 1780453794,
|
||||||
"narHash": "sha256-/bVBlRpECLVzjV19t5KMdMFWSwKLtb5RyXdjz3LJT+g=",
|
"narHash": "sha256-bXMRa9VTsHSPXL4Cw8R6JJLQeY3Y/IP4+YJCYVmQ7FY=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "50ab793786d9de88ee30ec4e4c24fb4236fc2674",
|
"rev": "6b316287bae2ee04c9b93c8c858d930fd07d7338",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"ref": "nixos-24.11",
|
"ref": "nixos-26.05",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nixpkgs-unstable": {
|
"nixpkgs-unstable": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1767364772,
|
"lastModified": 1778869304,
|
||||||
"narHash": "sha256-fFUnEYMla8b7UKjijLnMe+oVFOz6HjijGGNS1l7dYaQ=",
|
"narHash": "sha256-30sZNZoA1cqF5JNO9fVX+wgiQYjB7HJqqJ4ztCDeBZE=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "16c7794d0a28b5a37904d55bcca36003b9109aaa",
|
"rev": "d233902339c02a9c334e7e593de68855ad26c4cb",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -191,26 +171,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs_2": {
|
"nixpkgs_2": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1775054576,
|
"lastModified": 1778737229,
|
||||||
"narHash": "sha256-iiIr1hlTMu2LLARsUYtiqlE90tqocqIMVLK2fIzB/UY=",
|
"narHash": "sha256-6xWoytx8jFW4PF1GjRm/i/53trbpKGfz6zjzQGBr4cI=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "fc4b9b74d4b0bdbf3c97fef4bd34c05225172912",
|
"rev": "d7a713c0b7e47c908258e71cba7a2d77cc8d71d5",
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "nixos",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nixpkgs_3": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1767480499,
|
|
||||||
"narHash": "sha256-8IQQUorUGiSmFaPnLSo2+T+rjHtiNWc+OAzeHck7N48=",
|
|
||||||
"owner": "NixOS",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"rev": "30a3c519afcf3f99e2c6df3b359aec5692054d92",
|
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -220,13 +185,13 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nixpkgs_4": {
|
"nixpkgs_3": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1777268161,
|
"lastModified": 1780243769,
|
||||||
"narHash": "sha256-bxrdOn8SCOv8tN4JbTF/TXq7kjo9ag4M+C8yzzIRYbE=",
|
"narHash": "sha256-x5UQuRsH3MqI0U9afaXSNqzTPSeZlRLvFAav2Ux1pNw=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "1c3fe55ad329cbcb28471bb30f05c9827f724c76",
|
"rev": "331800de5053fcebacf6813adb5db9c9dca22a0c",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -236,13 +201,13 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nixpkgs_5": {
|
"nixpkgs_4": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1776255774,
|
"lastModified": 1780336545,
|
||||||
"narHash": "sha256-psVTpH6PK3q1htMJpmdz1hLF5pQgEshu7gQWgKO6t6Y=",
|
"narHash": "sha256-vhVhuXzFrIOfcssC/9hDHx7MHzDKjF3keHuREOQqQiQ=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "566acc07c54dc807f91625bb286cb9b321b5f42a",
|
"rev": "4df1b885d76a54e1aa1a318f8d16fd6005b6401f",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -255,15 +220,15 @@
|
|||||||
"nixvim": {
|
"nixvim": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"flake-parts": "flake-parts",
|
"flake-parts": "flake-parts",
|
||||||
"nixpkgs": "nixpkgs_5",
|
"nixpkgs": "nixpkgs_4",
|
||||||
"systems": "systems_2"
|
"systems": "systems_2"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1777236345,
|
"lastModified": 1780646548,
|
||||||
"narHash": "sha256-ALOqlq7bE30lsX4rA76hXeQ2aLLEpb44hS+D1+jWS88=",
|
"narHash": "sha256-Ckyl/l1XBmEwnaHcHD8PvBZk1uph0NqwbJ//CAvB7iE=",
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"repo": "nixvim",
|
"repo": "nixvim",
|
||||||
"rev": "a67d9cd6ff725a763afe88727aac73208ded3bf4",
|
"rev": "816a15282e58678dde831477964987d0262d4293",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -272,28 +237,11 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"oldNixpkgs": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1727619874,
|
|
||||||
"narHash": "sha256-a4Jcd+vjQAzF675/7B1LN3U2ay22jfDAVA8pOml5J/0=",
|
|
||||||
"owner": "nixos",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"rev": "6710d0dd013f55809648dfb1265b8f85447d30a6",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "nixos",
|
|
||||||
"ref": "6710d0dd013f55809648dfb1265b8f85447d30a6",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": {
|
"root": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"bip110": "bip110",
|
|
||||||
"btc-clients": "btc-clients",
|
"btc-clients": "btc-clients",
|
||||||
"nix-bitcoin": "nix-bitcoin",
|
"nix-bitcoin": "nix-bitcoin",
|
||||||
"nixpkgs": "nixpkgs_4",
|
"nixpkgs": "nixpkgs_3",
|
||||||
"nixpkgs-stable": "nixpkgs-stable",
|
"nixpkgs-stable": "nixpkgs-stable",
|
||||||
"nixvim": "nixvim"
|
"nixvim": "nixvim"
|
||||||
}
|
}
|
||||||
@@ -315,15 +263,16 @@
|
|||||||
},
|
},
|
||||||
"systems_2": {
|
"systems_2": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1681028828,
|
"lastModified": 1774449309,
|
||||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
"narHash": "sha256-brhZ8DmuGtzkCYHJg4HEd602amKm89Y9ytsFZ5uWD1w=",
|
||||||
"owner": "nix-systems",
|
"owner": "nix-systems",
|
||||||
"repo": "default",
|
"repo": "default",
|
||||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
"rev": "c29398b59d2048c4ab79345812849c9bd15e9150",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "nix-systems",
|
"owner": "nix-systems",
|
||||||
|
"ref": "future-26.11",
|
||||||
"repo": "default",
|
"repo": "default",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,11 +6,10 @@
|
|||||||
nix-bitcoin.url = "github:fort-nix/nix-bitcoin/release";
|
nix-bitcoin.url = "github:fort-nix/nix-bitcoin/release";
|
||||||
nixvim.url = "github:nix-community/nixvim";
|
nixvim.url = "github:nix-community/nixvim";
|
||||||
btc-clients.url = "github:emmanuelrosa/btc-clients-nix";
|
btc-clients.url = "github:emmanuelrosa/btc-clients-nix";
|
||||||
nixpkgs-stable.url = "github:nixos/nixpkgs/nixos-24.11";
|
nixpkgs-stable.url = "github:nixos/nixpkgs/nixos-26.05";
|
||||||
bip110.url = "github:emmanuelrosa/bitcoin-knots-bip-110-nix";
|
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs = { self, nixpkgs, nix-bitcoin, nixvim, btc-clients, nixpkgs-stable, bip110, ... }:
|
outputs = { self, nixpkgs, nix-bitcoin, nixvim, btc-clients, nixpkgs-stable, ... }:
|
||||||
|
|
||||||
let
|
let
|
||||||
overlay-stable = final: prev: {
|
overlay-stable = final: prev: {
|
||||||
@@ -56,7 +55,6 @@
|
|||||||
btc-clients.packages.${pkgs.system}.bisq2
|
btc-clients.packages.${pkgs.system}.bisq2
|
||||||
btc-clients.packages.${pkgs.system}.sparrow
|
btc-clients.packages.${pkgs.system}.sparrow
|
||||||
];
|
];
|
||||||
sovran_systemsOS.packages.bip110 = bip110.packages.${pkgs.system}.bitcoind-knots-bip-110;
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" width="256" height="256">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0%" stop-color="#153126"/>
|
||||||
|
<stop offset="55%" stop-color="#0F241B"/>
|
||||||
|
<stop offset="100%" stop-color="#091C14"/>
|
||||||
|
</linearGradient>
|
||||||
|
|
||||||
|
<linearGradient id="outerArc" x1="70" y1="40" x2="190" y2="210" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0%" stop-color="#42F39A"/>
|
||||||
|
<stop offset="45%" stop-color="#28D978"/>
|
||||||
|
<stop offset="100%" stop-color="#1AA45D"/>
|
||||||
|
</linearGradient>
|
||||||
|
|
||||||
|
<linearGradient id="innerArc" x1="90" y1="60" x2="180" y2="190" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0%" stop-color="#27C86F"/>
|
||||||
|
<stop offset="100%" stop-color="#157E49"/>
|
||||||
|
</linearGradient>
|
||||||
|
|
||||||
|
<filter id="innerShade" x="-10%" y="-10%" width="120%" height="120%">
|
||||||
|
<feOffset dx="0" dy="2"/>
|
||||||
|
<feGaussianBlur stdDeviation="5" result="blur"/>
|
||||||
|
<feComposite in="blur" in2="SourceAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||||
|
<feColorMatrix type="matrix" values="
|
||||||
|
0 0 0 0 0
|
||||||
|
0 0 0 0 0
|
||||||
|
0 0 0 0 0
|
||||||
|
0 0 0 .18 0"/>
|
||||||
|
</filter>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<rect width="256" height="256" rx="48" ry="48" fill="url(#bg)"/>
|
||||||
|
<rect x="1.5" y="1.5" width="253" height="253" rx="46.5" ry="46.5"
|
||||||
|
fill="none" stroke="rgba(255,255,255,0.08)"/>
|
||||||
|
<rect x="6" y="6" width="244" height="244" rx="42" ry="42"
|
||||||
|
fill="none" filter="url(#innerShade)"/>
|
||||||
|
|
||||||
|
<path d="M128 32 A96 96 0 1 1 58 196"
|
||||||
|
fill="none"
|
||||||
|
stroke="url(#outerArc)"
|
||||||
|
stroke-width="12"
|
||||||
|
stroke-linecap="round"/>
|
||||||
|
|
||||||
|
<path d="M128 56 A72 72 0 1 1 76 178"
|
||||||
|
fill="none"
|
||||||
|
stroke="url(#innerArc)"
|
||||||
|
stroke-width="10"
|
||||||
|
stroke-linecap="round"/>
|
||||||
|
|
||||||
|
<circle cx="128" cy="128" r="8" fill="#F2FFF7"/>
|
||||||
|
<circle cx="128" cy="128" r="18" fill="none" stroke="#7BFFC0" stroke-opacity="0.14" stroke-width="4"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.9 KiB |
+37
-2
@@ -21,7 +21,7 @@ DEPLOYED_FLAKE = """\
|
|||||||
description = "Sovran_SystemsOS for the Sovran Pro from Sovran Systems";
|
description = "Sovran_SystemsOS for the Sovran Pro from Sovran Systems";
|
||||||
|
|
||||||
inputs = {
|
inputs = {
|
||||||
Sovran_Systems.url = "git+https://git.sovransystems.com/Sovran_Systems/Sovran_SystemsOS?ref=staging-dev";
|
Sovran_Systems.url = "git+https://git.sovransystems.com/Sovran_Systems/Sovran_SystemsOS?ref=stable";
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs = { self, Sovran_Systems, ... }@inputs: {
|
outputs = { self, Sovran_Systems, ... }@inputs: {
|
||||||
@@ -363,6 +363,40 @@ class InstallerWindow(Adw.ApplicationWindow):
|
|||||||
sep.set_margin_end(40)
|
sep.set_margin_end(40)
|
||||||
outer.append(sep)
|
outer.append(sep)
|
||||||
|
|
||||||
|
notice_frame = Gtk.Frame()
|
||||||
|
notice_frame.add_css_class("card")
|
||||||
|
notice_frame.set_margin_start(40)
|
||||||
|
notice_frame.set_margin_end(40)
|
||||||
|
notice_frame.set_margin_top(20)
|
||||||
|
notice_frame.set_margin_bottom(4)
|
||||||
|
|
||||||
|
notice_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=12)
|
||||||
|
notice_box.set_margin_top(12)
|
||||||
|
notice_box.set_margin_bottom(12)
|
||||||
|
notice_box.set_margin_start(16)
|
||||||
|
notice_box.set_margin_end(16)
|
||||||
|
|
||||||
|
notice_icon = symbolic_icon("dialog-information-symbolic")
|
||||||
|
notice_icon.set_valign(Gtk.Align.START)
|
||||||
|
notice_box.append(notice_icon)
|
||||||
|
|
||||||
|
notice_lbl = Gtk.Label()
|
||||||
|
notice_lbl.set_use_markup(True)
|
||||||
|
notice_lbl.set_wrap(True)
|
||||||
|
notice_lbl.set_xalign(0)
|
||||||
|
notice_lbl.set_halign(Gtk.Align.FILL)
|
||||||
|
notice_lbl.set_markup(
|
||||||
|
"<span weight='bold'>Heads up — Server + Desktop prerequisites</span>\n"
|
||||||
|
"• A domain or subdomain from <span weight='bold'>https://njal.la</span>\n"
|
||||||
|
"• The ability to open / forward ports on your router\n\n"
|
||||||
|
"Don't worry — after install, the onboarding wizard walks you through every step.\n"
|
||||||
|
"<span size='small'>Desktop Only and Node Only do not require a domain or port forwarding.</span>"
|
||||||
|
)
|
||||||
|
notice_box.append(notice_lbl)
|
||||||
|
|
||||||
|
notice_frame.set_child(notice_box)
|
||||||
|
outer.append(notice_frame)
|
||||||
|
|
||||||
# Role label
|
# Role label
|
||||||
role_lbl = Gtk.Label()
|
role_lbl = Gtk.Label()
|
||||||
role_lbl.set_markup("<span size='medium' weight='bold'>Choose your installation type:</span>")
|
role_lbl.set_markup("<span size='medium' weight='bold'>Choose your installation type:</span>")
|
||||||
@@ -948,6 +982,7 @@ class InstallerWindow(Adw.ApplicationWindow):
|
|||||||
if proc.returncode != 0:
|
if proc.returncode != 0:
|
||||||
raise RuntimeError(f"Failed to write role-state.nix: {proc.stderr}")
|
raise RuntimeError(f"Failed to write role-state.nix: {proc.stderr}")
|
||||||
run(["sudo", "cp", "/mnt/etc/nixos/custom.template.nix", "/mnt/etc/nixos/custom.nix"])
|
run(["sudo", "cp", "/mnt/etc/nixos/custom.template.nix", "/mnt/etc/nixos/custom.nix"])
|
||||||
|
run(["sudo", "chmod", "644", "/mnt/etc/nixos/custom.nix"])
|
||||||
|
|
||||||
# ── Step 4: Ready to install ──────────────────────────────────────────
|
# ── Step 4: Ready to install ──────────────────────────────────────────
|
||||||
|
|
||||||
@@ -1059,7 +1094,7 @@ class InstallerWindow(Adw.ApplicationWindow):
|
|||||||
if proc.returncode != 0:
|
if proc.returncode != 0:
|
||||||
log(proc.stderr)
|
log(proc.stderr)
|
||||||
raise RuntimeError(proc.stderr.strip() or "Failed to write deployed flake.nix")
|
raise RuntimeError(proc.stderr.strip() or "Failed to write deployed flake.nix")
|
||||||
GLib.idle_add(append_text, buf, "Locking flake to staging-dev...\n")
|
GLib.idle_add(append_text, buf, "Locking flake to stable...\n")
|
||||||
run_stream(["sudo", "nix", "--extra-experimental-features", "nix-command flakes",
|
run_stream(["sudo", "nix", "--extra-experimental-features", "nix-command flakes",
|
||||||
"flake", "lock", "/mnt/etc/nixos"], buf)
|
"flake", "lock", "/mnt/etc/nixos"], buf)
|
||||||
|
|
||||||
|
|||||||
@@ -245,6 +245,7 @@ if [[ -n "$DEPLOY_KEY" || -n "$HEADSCALE_SERVER" ]]; then
|
|||||||
} > /mnt/etc/nixos/custom.nix
|
} > /mnt/etc/nixos/custom.nix
|
||||||
else
|
else
|
||||||
cp /mnt/etc/nixos/custom.template.nix /mnt/etc/nixos/custom.nix
|
cp /mnt/etc/nixos/custom.template.nix /mnt/etc/nixos/custom.nix
|
||||||
|
chmod 644 /mnt/etc/nixos/custom.nix
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ── Write Headscale auth key if provided ─────────────────────────────────────
|
# ── Write Headscale auth key if provided ─────────────────────────────────────
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
{ config, lib, pkgs, ... }:
|
|
||||||
|
|
||||||
let
|
|
||||||
cfg = config.sovran_systemsOS;
|
|
||||||
in
|
|
||||||
{
|
|
||||||
options.sovran_systemsOS.packages.bip110 = lib.mkOption {
|
|
||||||
type = lib.types.nullOr lib.types.package;
|
|
||||||
default = null;
|
|
||||||
description = "BIP110 Bitcoin package";
|
|
||||||
};
|
|
||||||
|
|
||||||
config = lib.mkIf (
|
|
||||||
cfg.features.bip110 &&
|
|
||||||
cfg.packages.bip110 != null
|
|
||||||
) {
|
|
||||||
services.bitcoind.package = lib.mkForce cfg.packages.bip110;
|
|
||||||
|
|
||||||
environment.systemPackages = [
|
|
||||||
cfg.packages.bip110
|
|
||||||
];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -4,7 +4,7 @@ lib.mkIf config.sovran_systemsOS.services.bitcoin {
|
|||||||
|
|
||||||
services.bitcoind = {
|
services.bitcoind = {
|
||||||
enable = true;
|
enable = true;
|
||||||
package = config.nix-bitcoin.pkgs.bitcoind-knots;
|
package = pkgs.bitcoind-knots;
|
||||||
dataDir = "/run/media/Second_Drive/BTCEcoandBackup/Bitcoin_Node";
|
dataDir = "/run/media/Second_Drive/BTCEcoandBackup/Bitcoin_Node";
|
||||||
txindex = true;
|
txindex = true;
|
||||||
tor.proxy = true;
|
tor.proxy = true;
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
})
|
})
|
||||||
|
|
||||||
# ── Bitcoin Node Only Role ────────────────────────────────
|
# ── Bitcoin Node Only Role ────────────────────────────────
|
||||||
# Bitcoin ecosystem + mempool + bip110, BTCPay runs but not exposed via Caddy
|
# Bitcoin ecosystem + mempool, BTCPay runs but not exposed via Caddy
|
||||||
(lib.mkIf config.sovran_systemsOS.roles.node {
|
(lib.mkIf config.sovran_systemsOS.roles.node {
|
||||||
sovran_systemsOS.services = {
|
sovran_systemsOS.services = {
|
||||||
bitcoin = lib.mkDefault true;
|
bitcoin = lib.mkDefault true;
|
||||||
@@ -36,7 +36,6 @@
|
|||||||
|
|
||||||
sovran_systemsOS.features = {
|
sovran_systemsOS.features = {
|
||||||
mempool = lib.mkDefault true;
|
mempool = lib.mkDefault true;
|
||||||
bip110 = lib.mkDefault true;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
sovran_systemsOS.web.btcpayserver = lib.mkDefault false;
|
sovran_systemsOS.web.btcpayserver = lib.mkDefault false;
|
||||||
|
|||||||
+24
-1
@@ -43,12 +43,24 @@
|
|||||||
# ── Features (default OFF — user can enable in custom.nix) ──
|
# ── Features (default OFF — user can enable in custom.nix) ──
|
||||||
features = {
|
features = {
|
||||||
haven = lib.mkEnableOption "Haven NOSTR relay";
|
haven = lib.mkEnableOption "Haven NOSTR relay";
|
||||||
bip110 = lib.mkEnableOption "BIP-110 Bitcoin Better Money";
|
|
||||||
mempool = lib.mkEnableOption "Bitcoin Mempool Explorer";
|
mempool = lib.mkEnableOption "Bitcoin Mempool Explorer";
|
||||||
element-calling = lib.mkEnableOption "Element Video and Audio Calling";
|
element-calling = lib.mkEnableOption "Element Video and Audio Calling";
|
||||||
bitcoin-core = lib.mkEnableOption "Bitcoin Core";
|
bitcoin-core = lib.mkEnableOption "Bitcoin Core";
|
||||||
rdp = lib.mkEnableOption "Gnome Remote Desktop";
|
rdp = lib.mkEnableOption "Gnome Remote Desktop";
|
||||||
sshd = lib.mkEnableOption "SSH remote access";
|
sshd = lib.mkEnableOption "SSH remote access";
|
||||||
|
|
||||||
|
# Deprecated: BIP-110 is now built into mainline Bitcoin Knots and is the
|
||||||
|
# default node. This option is retained ONLY so that existing machines with
|
||||||
|
# `sovran_systemsOS.features.bip110 = lib.mkForce true;` left in their local
|
||||||
|
# custom.nix continue to evaluate. It has no effect and will be removed in a
|
||||||
|
# future release once the Hub has cleaned up old custom.nix files.
|
||||||
|
bip110 = lib.mkOption {
|
||||||
|
type = lib.types.nullOr lib.types.bool;
|
||||||
|
default = null;
|
||||||
|
internal = true;
|
||||||
|
visible = false;
|
||||||
|
description = "(Deprecated, no-op) BIP-110 is now built into Bitcoin Knots.";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
# ── Web exposure (controls Caddy vhosts) ──────────────────
|
# ── Web exposure (controls Caddy vhosts) ──────────────────
|
||||||
@@ -89,4 +101,15 @@
|
|||||||
description = "Nostr public key (npub1...) for Haven relay";
|
description = "Nostr public key (npub1...) for Haven relay";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
config = lib.mkIf (config.sovran_systemsOS.features.bip110 != null) {
|
||||||
|
warnings = [
|
||||||
|
''
|
||||||
|
sovran_systemsOS.features.bip110 is deprecated and has no effect:
|
||||||
|
BIP-110 is now built into mainline Bitcoin Knots, which is the default node.
|
||||||
|
You can safely remove the `sovran_systemsOS.features.bip110` line from
|
||||||
|
/etc/nixos/custom.nix. The Sovran Hub will also remove it automatically.
|
||||||
|
''
|
||||||
|
];
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+34
-11
@@ -29,10 +29,7 @@ let
|
|||||||
]
|
]
|
||||||
# ── Bitcoin Base (node implementations) ────────────────────
|
# ── Bitcoin Base (node implementations) ────────────────────
|
||||||
++ lib.optionals cfg.services.bitcoin [
|
++ lib.optionals cfg.services.bitcoin [
|
||||||
{ name = "Bitcoin Knots + BIP110"; unit = "bitcoind.service"; type = "system"; icon = "bip110"; enabled = cfg.features.bip110; category = "bitcoin-base"; credentials = [
|
{ name = "Bitcoin Knots + BIP110"; unit = "bitcoind.service"; type = "system"; icon = "bip110"; enabled = cfg.services.bitcoin && !cfg.features.bitcoin-core; category = "bitcoin-base"; credentials = [
|
||||||
{ label = "Tor Address — Access from anywhere via Tor Browser"; file = "/var/lib/tor/onion/bitcoind/hostname"; prefix = "http://"; }
|
|
||||||
]; }
|
|
||||||
{ name = "Bitcoin Knots"; unit = "bitcoind.service"; type = "system"; icon = "bitcoind"; enabled = cfg.services.bitcoin && !cfg.features.bitcoin-core && !cfg.features.bip110; category = "bitcoin-base"; credentials = [
|
|
||||||
{ label = "Tor Address — Access from anywhere via Tor Browser"; file = "/var/lib/tor/onion/bitcoind/hostname"; prefix = "http://"; }
|
{ label = "Tor Address — Access from anywhere via Tor Browser"; file = "/var/lib/tor/onion/bitcoind/hostname"; prefix = "http://"; }
|
||||||
]; }
|
]; }
|
||||||
{ name = "Bitcoin Core"; unit = "bitcoind.service"; type = "system"; icon = "bitcoin-core"; enabled = cfg.features.bitcoin-core; category = "bitcoin-base"; credentials = [
|
{ name = "Bitcoin Core"; unit = "bitcoind.service"; type = "system"; icon = "bitcoin-core"; enabled = cfg.features.bitcoin-core; category = "bitcoin-base"; credentials = [
|
||||||
@@ -138,7 +135,11 @@ let
|
|||||||
RC=0
|
RC=0
|
||||||
|
|
||||||
echo "── Step 1/3: nix flake update ────────────────────"
|
echo "── Step 1/3: nix flake update ────────────────────"
|
||||||
if ! nix flake update --flake /etc/nixos --print-build-logs 2>&1; then
|
if ! nix flake update --flake /etc/nixos --print-build-logs \
|
||||||
|
--option connect-timeout 10 \
|
||||||
|
--option stalled-download-timeout 90 \
|
||||||
|
--option download-attempts 7 \
|
||||||
|
--option fallback true 2>&1; then
|
||||||
echo "[ERROR] nix flake update failed"
|
echo "[ERROR] nix flake update failed"
|
||||||
RC=1
|
RC=1
|
||||||
fi
|
fi
|
||||||
@@ -146,13 +147,24 @@ let
|
|||||||
|
|
||||||
if [ "$RC" -eq 0 ]; then
|
if [ "$RC" -eq 0 ]; then
|
||||||
echo "── Step 2/3: nixos-rebuild ──────────────────────────"
|
echo "── Step 2/3: nixos-rebuild ──────────────────────────"
|
||||||
if nixos-rebuild switch --flake /etc/nixos --print-build-logs 2>&1; then
|
SWITCH_OUT=$(nixos-rebuild switch --flake /etc/nixos --print-build-logs \
|
||||||
|
--option connect-timeout 10 \
|
||||||
|
--option stalled-download-timeout 90 \
|
||||||
|
--option download-attempts 7 \
|
||||||
|
--option fallback true 2>&1)
|
||||||
|
SWITCH_RC=$?
|
||||||
|
echo "$SWITCH_OUT"
|
||||||
|
if [ "$SWITCH_RC" -eq 0 ]; then
|
||||||
echo "[OK] switch succeeded"
|
echo "[OK] switch succeeded"
|
||||||
elif grep -q "switchInhibitors\|Pre-switch checks failed" "$LOG" 2>/dev/null; then
|
elif echo "$SWITCH_OUT" | grep -q "switchInhibitors\|Pre-switch checks failed"; then
|
||||||
echo ""
|
echo ""
|
||||||
echo " ✓ Build succeeded — a reboot is required to apply this update"
|
echo " ✓ Build succeeded — a reboot is required to apply this update"
|
||||||
echo " (Critical system components changed; running nixos-rebuild boot instead)"
|
echo " (Critical system components changed; running nixos-rebuild boot instead)"
|
||||||
if nixos-rebuild boot --flake /etc/nixos --print-build-logs 2>&1; then
|
if nixos-rebuild boot --flake /etc/nixos --print-build-logs \
|
||||||
|
--option connect-timeout 10 \
|
||||||
|
--option stalled-download-timeout 90 \
|
||||||
|
--option download-attempts 7 \
|
||||||
|
--option fallback true 2>&1; then
|
||||||
echo "REBOOT_REQUIRED" > "$STATUS"
|
echo "REBOOT_REQUIRED" > "$STATUS"
|
||||||
exit 0
|
exit 0
|
||||||
else
|
else
|
||||||
@@ -206,17 +218,28 @@ let
|
|||||||
echo "══════════════════════════════════════════════════"
|
echo "══════════════════════════════════════════════════"
|
||||||
echo ""
|
echo ""
|
||||||
echo "── Rebuilding system configuration ──────────────"
|
echo "── Rebuilding system configuration ──────────────"
|
||||||
if nixos-rebuild switch --flake /etc/nixos --print-build-logs 2>&1; then
|
SWITCH_OUT=$(nixos-rebuild switch --flake /etc/nixos --print-build-logs \
|
||||||
|
--option connect-timeout 10 \
|
||||||
|
--option stalled-download-timeout 90 \
|
||||||
|
--option download-attempts 7 \
|
||||||
|
--option fallback true 2>&1)
|
||||||
|
SWITCH_RC=$?
|
||||||
|
echo "$SWITCH_OUT"
|
||||||
|
if [ "$SWITCH_RC" -eq 0 ]; then
|
||||||
echo ""
|
echo ""
|
||||||
echo "══════════════════════════════════════════════════"
|
echo "══════════════════════════════════════════════════"
|
||||||
echo " ✓ Rebuild completed successfully"
|
echo " ✓ Rebuild completed successfully"
|
||||||
echo "══════════════════════════════════════════════════"
|
echo "══════════════════════════════════════════════════"
|
||||||
echo "SUCCESS" > "$STATUS"
|
echo "SUCCESS" > "$STATUS"
|
||||||
elif grep -q "switchInhibitors\|Pre-switch checks failed" "$LOG" 2>/dev/null; then
|
elif echo "$SWITCH_OUT" | grep -q "switchInhibitors\|Pre-switch checks failed"; then
|
||||||
echo ""
|
echo ""
|
||||||
echo " ✓ Build succeeded — a reboot is required to apply this rebuild"
|
echo " ✓ Build succeeded — a reboot is required to apply this rebuild"
|
||||||
echo " (Critical system components changed; running nixos-rebuild boot instead)"
|
echo " (Critical system components changed; running nixos-rebuild boot instead)"
|
||||||
if nixos-rebuild boot --flake /etc/nixos --print-build-logs 2>&1; then
|
if nixos-rebuild boot --flake /etc/nixos --print-build-logs \
|
||||||
|
--option connect-timeout 10 \
|
||||||
|
--option stalled-download-timeout 90 \
|
||||||
|
--option download-attempts 7 \
|
||||||
|
--option fallback true 2>&1; then
|
||||||
echo "REBOOT_REQUIRED" > "$STATUS"
|
echo "REBOOT_REQUIRED" > "$STATUS"
|
||||||
else
|
else
|
||||||
echo "[ERROR] nixos-rebuild boot also failed"
|
echo "[ERROR] nixos-rebuild boot also failed"
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ EOF
|
|||||||
keyFile = livekitKeyFile;
|
keyFile = livekitKeyFile;
|
||||||
settings = {
|
settings = {
|
||||||
rtc.use_external_ip = true;
|
rtc.use_external_ip = true;
|
||||||
rtc.udp_port = "7882-7894";
|
rtc.udp_port = 7882;
|
||||||
room.auto_create = false;
|
room.auto_create = false;
|
||||||
turn = {
|
turn = {
|
||||||
enabled = true;
|
enabled = true;
|
||||||
@@ -141,10 +141,9 @@ EOF
|
|||||||
};
|
};
|
||||||
|
|
||||||
networking.firewall.allowedTCPPorts = [ 5349 7881 ];
|
networking.firewall.allowedTCPPorts = [ 5349 7881 ];
|
||||||
networking.firewall.allowedUDPPorts = [ 3478 ];
|
networking.firewall.allowedUDPPorts = [ 3478 7882 ];
|
||||||
networking.firewall.allowedUDPPortRanges = [
|
networking.firewall.allowedUDPPortRanges = [
|
||||||
{ from = 7882; to = 7894; }
|
{ from = 30000; to = 40000; }
|
||||||
{ from = 30000; to = 40000;}
|
|
||||||
];
|
];
|
||||||
networking.firewall.allowedTCPPortRanges = [
|
networking.firewall.allowedTCPPortRanges = [
|
||||||
{ from = 30000; to = 40000; }
|
{ from = 30000; to = 40000; }
|
||||||
|
|||||||
@@ -31,7 +31,6 @@
|
|||||||
|
|
||||||
# ── Features (default OFF — enable in custom.nix) ─────────
|
# ── Features (default OFF — enable in custom.nix) ─────────
|
||||||
./haven.nix
|
./haven.nix
|
||||||
./bip110.nix
|
|
||||||
./element-calling.nix
|
./element-calling.nix
|
||||||
./mempool.nix
|
./mempool.nix
|
||||||
./bitcoin-core.nix
|
./bitcoin-core.nix
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ lib.mkIf config.sovran_systemsOS.services.bitcoin {
|
|||||||
|
|
||||||
cat > "$CONFIG_FILE" << 'EOF'
|
cat > "$CONFIG_FILE" << 'EOF'
|
||||||
{
|
{
|
||||||
|
"mode": "ONLINE",
|
||||||
"serverType": "ELECTRUM_SERVER",
|
"serverType": "ELECTRUM_SERVER",
|
||||||
"electrumServer": "tcp://127.0.0.1:50001",
|
"electrumServer": "tcp://127.0.0.1:50001",
|
||||||
"useProxy": false
|
"useProxy": false
|
||||||
|
|||||||
Reference in New Issue
Block a user