vendor: replace nix-bitcoin flake input with minimal vendored modules (nixpkgs-only)
- Remove inputs.nix-bitcoin (fort-nix/nix-bitcoin/release) from flake.nix - Vendor only 6 services actually used by Sovran: bitcoind, electrs, lnd (+lndconnect), rtl, btcpayserver, mempool + supporting infra: secrets, onion-services/addresses, operator, nodeinfo, security, versioning - All packages now from nixpkgs directly (pkgs.*) — no pinned pkgs - Keep nix-bitcoin.* option namespace for compatibility - backups.nix removed: Sovran uses rsnapshot to Second_Drive (configuration.nix: hourly/daily to BTCEcoandBackup) — duplicity remote backup not needed - netns-isolation.nix replaced with stub (5 lines): original 365-line bridge/iptables/ip-netns broke Caddy/AlbyHub/RTL a year ago and is incompatible with nwc-wallets (requires enable=false). Stub keeps option valid but warns if enabled. - Add pkgs/sovran-overlay.nix for gaps only: lndinit + netns-exec stub
This commit is contained in:
@@ -3,13 +3,12 @@
|
|||||||
|
|
||||||
inputs = {
|
inputs = {
|
||||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
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-26.05";
|
nixpkgs-stable.url = "github:nixos/nixpkgs/nixos-26.05";
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs = { self, nixpkgs, nix-bitcoin, nixvim, btc-clients, nixpkgs-stable, ... }:
|
outputs = { self, nixpkgs, nixvim, btc-clients, nixpkgs-stable, ... }:
|
||||||
|
|
||||||
let
|
let
|
||||||
overlay-stable = final: prev: {
|
overlay-stable = final: prev: {
|
||||||
@@ -18,11 +17,12 @@
|
|||||||
config.allowUnfree = true;
|
config.allowUnfree = true;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
overlay-sovran = import ./pkgs/sovran-overlay.nix;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
nixosConfigurations.nixos = nixpkgs.lib.nixosSystem {
|
nixosConfigurations.nixos = nixpkgs.lib.nixosSystem {
|
||||||
modules = [
|
modules = [
|
||||||
{ nixpkgs.hostPlatform = "x86_64-linux"; }
|
{ nixpkgs.hostPlatform = "x86_64-linux"; nixpkgs.overlays = [ overlay-stable overlay-sovran ]; }
|
||||||
self.nixosModules.Sovran_SystemsOS
|
self.nixosModules.Sovran_SystemsOS
|
||||||
./hardware-configuration.nix
|
./hardware-configuration.nix
|
||||||
./role-state.nix
|
./role-state.nix
|
||||||
@@ -32,10 +32,9 @@
|
|||||||
|
|
||||||
nixosConfigurations.sovran_systemsos-iso = nixpkgs.lib.nixosSystem {
|
nixosConfigurations.sovran_systemsos-iso = nixpkgs.lib.nixosSystem {
|
||||||
modules = [
|
modules = [
|
||||||
{ nixpkgs.hostPlatform = "x86_64-linux"; }
|
{ nixpkgs.hostPlatform = "x86_64-linux"; nixpkgs.overlays = [ overlay-stable overlay-sovran ]; }
|
||||||
({ config, pkgs, ... }: { nixpkgs.overlays = [ overlay-stable ]; })
|
|
||||||
./iso/common.nix
|
./iso/common.nix
|
||||||
nix-bitcoin.nixosModules.default
|
./modules/vendor/nix-bitcoin/modules.nix
|
||||||
nixvim.nixosModules.nixvim
|
nixvim.nixosModules.nixvim
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
@@ -43,10 +42,10 @@
|
|||||||
nixosModules.Sovran_SystemsOS = { pkgs, lib, config, ... }: {
|
nixosModules.Sovran_SystemsOS = { pkgs, lib, config, ... }: {
|
||||||
imports = [
|
imports = [
|
||||||
({ config, pkgs, ... }: {
|
({ config, pkgs, ... }: {
|
||||||
nixpkgs.overlays = [ overlay-stable ];
|
nixpkgs.overlays = [ overlay-stable overlay-sovran ];
|
||||||
})
|
})
|
||||||
./configuration.nix
|
./configuration.nix
|
||||||
nix-bitcoin.nixosModules.default
|
./modules/vendor/nix-bitcoin/modules.nix
|
||||||
nixvim.nixosModules.nixvim
|
nixvim.nixosModules.nixvim
|
||||||
];
|
];
|
||||||
config = {
|
config = {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{ config, pkgs, lib, ... }:
|
{ config, pkgs, lib, ... }:
|
||||||
|
|
||||||
lib.mkIf config.sovran_systemsOS.features.bitcoin-core {
|
lib.mkIf config.sovran_systemsOS.features.bitcoin-core {
|
||||||
|
# Vendored nix-bitcoin now uses nixpkgs directly; no nix-bitcoin.pkgs indirection.
|
||||||
services.bitcoind.package = lib.mkForce config.nix-bitcoin.pkgs.bitcoind;
|
# Use standard bitcoind from nixpkgs (override to knots if desired via pkgs.bitcoind-knots)
|
||||||
|
services.bitcoind.package = lib.mkForce pkgs.bitcoind;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ lib.mkIf config.sovran_systemsOS.services.bitcoin {
|
|||||||
name = "free";
|
name = "free";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
# vendored: now no-op (always uses nixpkgs)
|
||||||
nix-bitcoin.useVersionLockedPkgs = false;
|
nix-bitcoin.useVersionLockedPkgs = false;
|
||||||
|
|
||||||
systemd.services.bitcoind = {
|
systemd.services.bitcoind = {
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
{
|
{
|
||||||
config = lib.mkMerge [
|
config = lib.mkMerge [
|
||||||
|
|
||||||
# nix-bitcoin is globally imported by the flake (nixosModules.Sovran_SystemsOS).
|
# Vendored nix-bitcoin is always imported via modules/vendor/nix-bitcoin/modules.nix.
|
||||||
# This default satisfies nix-bitcoin's generateSecrets assertion so that Desktop
|
# This default satisfies the secrets assertion so Desktop-Only systems evaluate
|
||||||
# Only systems can evaluate without enabling any Bitcoin services.
|
# without enabling any Bitcoin services.
|
||||||
{
|
{
|
||||||
nix-bitcoin.generateSecrets = lib.mkDefault true;
|
nix-bitcoin.generateSecrets = lib.mkDefault true;
|
||||||
}
|
}
|
||||||
|
|||||||
+479
@@ -0,0 +1,479 @@
|
|||||||
|
{ config, pkgs, lib, ... }:
|
||||||
|
|
||||||
|
with lib;
|
||||||
|
let
|
||||||
|
options = {
|
||||||
|
services.bitcoind = {
|
||||||
|
enable = mkEnableOption "Bitcoin daemon";
|
||||||
|
address = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "127.0.0.1";
|
||||||
|
description = "Address to listen for peer connections.";
|
||||||
|
};
|
||||||
|
port = mkOption {
|
||||||
|
type = types.port;
|
||||||
|
default = if !cfg.regtest then 8333 else 18444;
|
||||||
|
defaultText = "if !cfg.regtest then 8333 else 18444";
|
||||||
|
description = "Port to listen for peer connections.";
|
||||||
|
};
|
||||||
|
onionPort = mkOption {
|
||||||
|
type = types.nullOr types.port;
|
||||||
|
# When the bitcoind onion service is enabled, add an onion-tagged socket
|
||||||
|
# to distinguish local connections from Tor connections
|
||||||
|
default = if (config.nix-bitcoin.onionServices.bitcoind.enable or false) then 8334 else null;
|
||||||
|
description = ''
|
||||||
|
Port to listen for Tor peer connections.
|
||||||
|
If set, inbound connections to this port are tagged as onion peers.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
listen = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = ''
|
||||||
|
Listen for peer connections at `address:port`
|
||||||
|
and `address:onionPort` (if {option}`onionPort` is set).
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
listenWhitelisted = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = ''
|
||||||
|
Listen for peer connections at `address:whitelistedPort`.
|
||||||
|
Peers connected through this socket are automatically whitelisted.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
whitelistedPort = mkOption {
|
||||||
|
type = types.port;
|
||||||
|
default = 8335;
|
||||||
|
description = "See `listenWhitelisted`.";
|
||||||
|
};
|
||||||
|
getPublicAddressCmd = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "";
|
||||||
|
description = ''
|
||||||
|
Bash expression which outputs the public service address to announce to peers.
|
||||||
|
If left empty, no address is announced.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
package = mkOption {
|
||||||
|
type = types.package;
|
||||||
|
default = pkgs.bitcoind;
|
||||||
|
defaultText = "pkgs.bitcoind";
|
||||||
|
description = ''
|
||||||
|
The package providing bitcoind binaries.
|
||||||
|
|
||||||
|
You can use this option to select other bitcoind-compatible implementations.
|
||||||
|
Example:
|
||||||
|
```nix
|
||||||
|
services.bitcoind.package = pkgs.bitcoind-knots;
|
||||||
|
```
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
extraConfig = mkOption {
|
||||||
|
type = types.lines;
|
||||||
|
default = "";
|
||||||
|
example = ''
|
||||||
|
par=16
|
||||||
|
logips=1
|
||||||
|
'';
|
||||||
|
description = "Extra lines appended to {file}`bitcoin.conf`.";
|
||||||
|
};
|
||||||
|
dataDir = mkOption {
|
||||||
|
type = types.path;
|
||||||
|
default = "/var/lib/bitcoind";
|
||||||
|
description = "The data directory for bitcoind.";
|
||||||
|
};
|
||||||
|
rpc = {
|
||||||
|
address = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "127.0.0.1";
|
||||||
|
description = ''
|
||||||
|
Address to listen for JSON-RPC connections.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
port = mkOption {
|
||||||
|
type = types.port;
|
||||||
|
default = if !cfg.regtest then 8332 else 18443;
|
||||||
|
defaultText = "if !cfg.regtest then 8332 else 18443";
|
||||||
|
description = "Port to listen for JSON-RPC connections.";
|
||||||
|
};
|
||||||
|
threads = mkOption {
|
||||||
|
type = types.nullOr types.ints.u16;
|
||||||
|
default = null;
|
||||||
|
description = "The number of threads to service RPC calls.";
|
||||||
|
};
|
||||||
|
allowip = mkOption {
|
||||||
|
type = types.listOf types.str;
|
||||||
|
default = [ "127.0.0.1" ];
|
||||||
|
description = ''
|
||||||
|
Allow JSON-RPC connections from specified sources.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
users = mkOption {
|
||||||
|
default = {};
|
||||||
|
description = ''
|
||||||
|
Allowed users for JSON-RPC connections.
|
||||||
|
'';
|
||||||
|
example = {
|
||||||
|
alice = {
|
||||||
|
passwordHMAC = "f7efda5c189b999524f151318c0c86$d5b51b3beffbc02b724e5d095828e0bc8b2456e9ac8757ae3211a5d9b16a22ae";
|
||||||
|
rpcwhitelist = [ "sendtoaddress" "getnewaddress" ];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
type = with types; attrsOf (submodule ({ name, ... }: {
|
||||||
|
options = {
|
||||||
|
name = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = name;
|
||||||
|
example = "alice";
|
||||||
|
description = ''
|
||||||
|
Username for JSON-RPC connections.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
passwordHMAC = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
example = "f7efda5c189b999524f151318c0c86$d5b51b3beffbc02b724e5d095828e0bc8b2456e9ac8757ae3211a5d9b16a22ae";
|
||||||
|
description = ''
|
||||||
|
Password HMAC-SHA-256 for JSON-RPC connections. Must be a string of the
|
||||||
|
format `<SALT-HEX>$<HMAC-HEX>`.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
passwordHMACFromFile = mkOption {
|
||||||
|
type = lib.types.bool;
|
||||||
|
internal = true;
|
||||||
|
default = false;
|
||||||
|
};
|
||||||
|
rpcwhitelist = mkOption {
|
||||||
|
type = types.listOf types.str;
|
||||||
|
default = [];
|
||||||
|
description = ''
|
||||||
|
List of allowed rpc calls for each user.
|
||||||
|
If empty list, rpcwhitelist is disabled for that user.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
};
|
||||||
|
regtest = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = "Enable regtest mode.";
|
||||||
|
};
|
||||||
|
network = mkOption {
|
||||||
|
readOnly = true;
|
||||||
|
default = if cfg.regtest then "regtest" else "mainnet";
|
||||||
|
};
|
||||||
|
makeNetworkName = mkOption {
|
||||||
|
readOnly = true;
|
||||||
|
default = mainnet: regtest: if cfg.regtest then regtest else mainnet;
|
||||||
|
};
|
||||||
|
proxy = mkOption {
|
||||||
|
type = types.nullOr types.str;
|
||||||
|
default = if cfg.tor.proxy then config.nix-bitcoin.torClientAddressWithPort else null;
|
||||||
|
description = "Connect through SOCKS5 proxy";
|
||||||
|
};
|
||||||
|
i2p = mkOption {
|
||||||
|
type = types.enum [ false true "only-outgoing" ];
|
||||||
|
default = false;
|
||||||
|
description = ''
|
||||||
|
Enable peer connections via i2p.
|
||||||
|
With `only-outgoing`, incoming i2p connections are disabled.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
dataDirReadableByGroup = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = ''
|
||||||
|
If enabled, data dir content is readable by the bitcoind service group.
|
||||||
|
Warning: This disables bitcoind's wallet support.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
sysperms = mkOption {
|
||||||
|
type = types.nullOr types.bool;
|
||||||
|
default = null;
|
||||||
|
description = ''
|
||||||
|
Create new files with system default permissions, instead of umask 077
|
||||||
|
(only effective with disabled wallet functionality)
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
disablewallet = mkOption {
|
||||||
|
type = types.nullOr types.bool;
|
||||||
|
default = null;
|
||||||
|
description = ''
|
||||||
|
Do not load the wallet and disable wallet RPC calls
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
dbCache = mkOption {
|
||||||
|
type = types.nullOr (intAtLeast 4);
|
||||||
|
default = null;
|
||||||
|
example = 4000;
|
||||||
|
description = "Override the default database cache size in MiB.";
|
||||||
|
};
|
||||||
|
prune = mkOption {
|
||||||
|
type = types.ints.unsigned;
|
||||||
|
default = 0;
|
||||||
|
example = 10000;
|
||||||
|
description = ''
|
||||||
|
Automatically prune block files to stay under the specified target size in MiB.
|
||||||
|
Value 0 disables pruning.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
txindex = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = "Enable the transaction index.";
|
||||||
|
};
|
||||||
|
zmqpubrawblock = mkOption {
|
||||||
|
type = types.nullOr types.str;
|
||||||
|
default = null;
|
||||||
|
example = "tcp://127.0.0.1:28332";
|
||||||
|
description = "ZMQ address for zmqpubrawblock notifications";
|
||||||
|
};
|
||||||
|
zmqpubrawtx = mkOption {
|
||||||
|
type = types.nullOr types.str;
|
||||||
|
default = null;
|
||||||
|
example = "tcp://127.0.0.1:28333";
|
||||||
|
description = "ZMQ address for zmqpubrawtx notifications";
|
||||||
|
};
|
||||||
|
assumevalid = mkOption {
|
||||||
|
type = types.nullOr types.str;
|
||||||
|
default = null;
|
||||||
|
example = "00000000000000000000e5abc3a74fe27dc0ead9c70ea1deb456f11c15fd7bc6";
|
||||||
|
description = ''
|
||||||
|
If this block is in the chain assume that it and its ancestors are
|
||||||
|
valid and potentially skip their script verification.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
addnodes = mkOption {
|
||||||
|
type = types.listOf types.str;
|
||||||
|
default = [];
|
||||||
|
example = [ "ecoc5q34tmbq54wl.onion" ];
|
||||||
|
description = "Add nodes to connect to and attempt to keep the connections open";
|
||||||
|
};
|
||||||
|
discover = mkOption {
|
||||||
|
type = types.nullOr types.bool;
|
||||||
|
default = null;
|
||||||
|
description = "Discover own IP addresses";
|
||||||
|
};
|
||||||
|
addresstype = mkOption {
|
||||||
|
type = types.nullOr types.str;
|
||||||
|
default = null;
|
||||||
|
example = "bech32";
|
||||||
|
description = "The type of addresses to use";
|
||||||
|
};
|
||||||
|
user = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "bitcoin";
|
||||||
|
description = "The user as which to run bitcoind.";
|
||||||
|
};
|
||||||
|
group = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = cfg.user;
|
||||||
|
description = "The group as which to run bitcoind.";
|
||||||
|
};
|
||||||
|
cli = mkOption {
|
||||||
|
readOnly = true;
|
||||||
|
type = types.package;
|
||||||
|
default = pkgs.writers.writeBashBin "bitcoin-cli" ''
|
||||||
|
exec ${cfg.package}/bin/bitcoin-cli -datadir='${cfg.dataDir}' "$@"
|
||||||
|
'';
|
||||||
|
defaultText = "(See source)";
|
||||||
|
description = "Binary to connect with the bitcoind instance.";
|
||||||
|
};
|
||||||
|
tor = nbLib.tor;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
cfg = config.services.bitcoind;
|
||||||
|
nbLib = config.nix-bitcoin.lib;
|
||||||
|
secretsDir = config.nix-bitcoin.secretsDir;
|
||||||
|
|
||||||
|
i2pSAM = config.services.i2pd.proto.sam;
|
||||||
|
|
||||||
|
configFile = builtins.toFile "bitcoin.conf" ''
|
||||||
|
# We're already logging via journald
|
||||||
|
nodebuglogfile=1
|
||||||
|
logtimestamps=0
|
||||||
|
|
||||||
|
startupnotify=/run/current-system/systemd/bin/systemd-notify --ready
|
||||||
|
|
||||||
|
${optionalString cfg.regtest ''
|
||||||
|
regtest=1
|
||||||
|
[regtest]
|
||||||
|
''}
|
||||||
|
${optionalString (cfg.dbCache != null) "dbcache=${toString cfg.dbCache}"}
|
||||||
|
prune=${toString cfg.prune}
|
||||||
|
${optionalString cfg.txindex "txindex=1"}
|
||||||
|
${optionalString (cfg.sysperms != null) "sysperms=${if cfg.sysperms then "1" else "0"}"}
|
||||||
|
${optionalString (cfg.disablewallet != null) "disablewallet=${if cfg.disablewallet then "1" else "0"}"}
|
||||||
|
${optionalString (cfg.assumevalid != null) "assumevalid=${cfg.assumevalid}"}
|
||||||
|
|
||||||
|
# Connection options
|
||||||
|
listen=${if (cfg.listen || cfg.listenWhitelisted) then "1" else "0"}
|
||||||
|
${optionalString cfg.listen
|
||||||
|
"bind=${cfg.address}:${toString cfg.port}"}
|
||||||
|
${optionalString (cfg.listen && cfg.onionPort != null)
|
||||||
|
"bind=${cfg.address}:${toString cfg.onionPort}=onion"}
|
||||||
|
${optionalString cfg.listenWhitelisted
|
||||||
|
"whitebind=${cfg.address}:${toString cfg.whitelistedPort}"}
|
||||||
|
${optionalString (cfg.proxy != null) "proxy=${cfg.proxy}"}
|
||||||
|
${optionalString (cfg.i2p != false) "i2psam=${nbLib.addressWithPort i2pSAM.address i2pSAM.port}"}
|
||||||
|
${optionalString (cfg.i2p == "only-outgoing") "i2pacceptincoming=0"}
|
||||||
|
|
||||||
|
${optionalString (cfg.discover != null) "discover=${if cfg.discover then "1" else "0"}"}
|
||||||
|
${lib.concatMapStrings (node: "addnode=${node}\n") cfg.addnodes}
|
||||||
|
|
||||||
|
# RPC server options
|
||||||
|
rpcbind=${cfg.rpc.address}
|
||||||
|
rpcport=${toString cfg.rpc.port}
|
||||||
|
rpcconnect=${cfg.rpc.address}
|
||||||
|
${optionalString (cfg.rpc.threads != null) "rpcthreads=${toString cfg.rpc.threads}"}
|
||||||
|
rpcwhitelistdefault=0
|
||||||
|
${concatMapStrings (user: ''
|
||||||
|
${optionalString (!user.passwordHMACFromFile) "rpcauth=${user.name}:${user.passwordHMAC}"}
|
||||||
|
${optionalString (user.rpcwhitelist != [])
|
||||||
|
"rpcwhitelist=${user.name}:${lib.strings.concatStringsSep "," user.rpcwhitelist}"}
|
||||||
|
'') (builtins.attrValues cfg.rpc.users)
|
||||||
|
}
|
||||||
|
${lib.concatMapStrings (rpcallowip: "rpcallowip=${rpcallowip}\n") cfg.rpc.allowip}
|
||||||
|
|
||||||
|
# Wallet options
|
||||||
|
${optionalString (cfg.addresstype != null) "addresstype=${cfg.addresstype}"}
|
||||||
|
|
||||||
|
# ZMQ options
|
||||||
|
${optionalString (cfg.zmqpubrawblock != null) "zmqpubrawblock=${cfg.zmqpubrawblock}"}
|
||||||
|
${optionalString (cfg.zmqpubrawtx != null) "zmqpubrawtx=${cfg.zmqpubrawtx}"}
|
||||||
|
|
||||||
|
# Extra options
|
||||||
|
${cfg.extraConfig}
|
||||||
|
'';
|
||||||
|
|
||||||
|
zmqServerEnabled = (cfg.zmqpubrawblock != null) || (cfg.zmqpubrawtx != null);
|
||||||
|
|
||||||
|
intAtLeast = n: types.addCheck types.int (x: x >= n) // {
|
||||||
|
name = "intAtLeast";
|
||||||
|
description = "integer >= ${toString n}";
|
||||||
|
};
|
||||||
|
in {
|
||||||
|
inherit options;
|
||||||
|
|
||||||
|
config = mkIf cfg.enable {
|
||||||
|
environment.systemPackages = [ cfg.package (hiPrio cfg.cli) ];
|
||||||
|
|
||||||
|
services.bitcoind = mkMerge [
|
||||||
|
(mkIf cfg.dataDirReadableByGroup {
|
||||||
|
disablewallet = true;
|
||||||
|
sysperms = true;
|
||||||
|
})
|
||||||
|
{
|
||||||
|
rpc.users.privileged = {
|
||||||
|
passwordHMACFromFile = true;
|
||||||
|
};
|
||||||
|
rpc.users.public = {
|
||||||
|
passwordHMACFromFile = true;
|
||||||
|
rpcwhitelist = import ./bitcoind-rpc-public-whitelist.nix;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
services.i2pd = mkIf (cfg.i2p != false) {
|
||||||
|
enable = true;
|
||||||
|
proto.sam.enable = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.tmpfiles.rules = [
|
||||||
|
"d '${cfg.dataDir}' 0770 ${cfg.user} ${cfg.group} - -"
|
||||||
|
];
|
||||||
|
|
||||||
|
systemd.services.bitcoind = rec {
|
||||||
|
wants = [
|
||||||
|
"network-online.target"
|
||||||
|
# Use `wants` instead of `requires` for `nix-bitcoin-secrets.target`
|
||||||
|
# so that bitcoind and all dependent services are not restarted when
|
||||||
|
# the secrets target restarts.
|
||||||
|
# The secrets target always restarts when deploying with one of the methods
|
||||||
|
# in ./deployment.
|
||||||
|
#
|
||||||
|
# TODO-EXTERNAL: Instead of `wants`, use a future systemd dependency type
|
||||||
|
# that propagates initial start failures but no restarts
|
||||||
|
"nix-bitcoin-secrets.target"
|
||||||
|
];
|
||||||
|
after = wants;
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
|
||||||
|
preStart = let
|
||||||
|
extraRpcauth = concatMapStrings (name: let
|
||||||
|
user = cfg.rpc.users.${name};
|
||||||
|
in optionalString user.passwordHMACFromFile ''
|
||||||
|
echo "rpcauth=${user.name}:$(cat ${secretsDir}/bitcoin-HMAC-${name})"
|
||||||
|
''
|
||||||
|
) (builtins.attrNames cfg.rpc.users);
|
||||||
|
in ''
|
||||||
|
${optionalString cfg.dataDirReadableByGroup ''
|
||||||
|
if [[ -e '${cfg.dataDir}/blocks' ]]; then
|
||||||
|
chmod -R g+rX '${cfg.dataDir}/blocks'
|
||||||
|
fi
|
||||||
|
''}
|
||||||
|
|
||||||
|
cfg=$(
|
||||||
|
cat ${configFile}
|
||||||
|
${extraRpcauth}
|
||||||
|
echo
|
||||||
|
${optionalString (cfg.getPublicAddressCmd != "") ''
|
||||||
|
echo "externalip=$(${cfg.getPublicAddressCmd})"
|
||||||
|
''}
|
||||||
|
)
|
||||||
|
confFile='${cfg.dataDir}/bitcoin.conf'
|
||||||
|
if [[ ! -e $confFile || $cfg != $(cat $confFile) ]]; then
|
||||||
|
install -o '${cfg.user}' -g '${cfg.group}' -m 640 <(echo "$cfg") $confFile
|
||||||
|
fi
|
||||||
|
'';
|
||||||
|
|
||||||
|
# Enable RPC access for group
|
||||||
|
postStart = ''
|
||||||
|
chmod g=r '${cfg.dataDir}/${optionalString cfg.regtest "regtest/"}.cookie'
|
||||||
|
'' + (optionalString cfg.regtest) ''
|
||||||
|
chmod g=x '${cfg.dataDir}/regtest'
|
||||||
|
'';
|
||||||
|
|
||||||
|
serviceConfig = nbLib.defaultHardening // {
|
||||||
|
Type = "notify";
|
||||||
|
NotifyAccess = "all";
|
||||||
|
User = cfg.user;
|
||||||
|
Group = cfg.group;
|
||||||
|
TimeoutStartSec = "30min";
|
||||||
|
TimeoutStopSec = "30min";
|
||||||
|
ExecStart = "${cfg.package}/bin/bitcoind -datadir='${cfg.dataDir}'";
|
||||||
|
Restart = "on-failure";
|
||||||
|
UMask = mkIf cfg.dataDirReadableByGroup "0027";
|
||||||
|
ReadWritePaths = [ cfg.dataDir ];
|
||||||
|
} // nbLib.allowedIPAddresses cfg.tor.enforce
|
||||||
|
// optionalAttrs zmqServerEnabled nbLib.allowNetlink;
|
||||||
|
};
|
||||||
|
|
||||||
|
users.users.${cfg.user} = {
|
||||||
|
isSystemUser = true;
|
||||||
|
group = cfg.group;
|
||||||
|
};
|
||||||
|
users.groups.${cfg.group} = {};
|
||||||
|
users.groups.bitcoinrpc-public = {};
|
||||||
|
|
||||||
|
nix-bitcoin.operator.groups = [ cfg.group ];
|
||||||
|
|
||||||
|
nix-bitcoin.secrets = {
|
||||||
|
bitcoin-rpcpassword-privileged.user = cfg.user;
|
||||||
|
bitcoin-rpcpassword-public = {
|
||||||
|
user = cfg.user;
|
||||||
|
group = "bitcoinrpc-public";
|
||||||
|
};
|
||||||
|
|
||||||
|
bitcoin-HMAC-privileged.user = cfg.user;
|
||||||
|
bitcoin-HMAC-public.user = cfg.user;
|
||||||
|
};
|
||||||
|
nix-bitcoin.generateSecretsCmds.bitcoind = ''
|
||||||
|
makeBitcoinRPCPassword privileged
|
||||||
|
makeBitcoinRPCPassword public
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}
|
||||||
+282
@@ -0,0 +1,282 @@
|
|||||||
|
{ config, lib, pkgs, ... }:
|
||||||
|
|
||||||
|
with lib;
|
||||||
|
let
|
||||||
|
options.services = {
|
||||||
|
btcpayserver = {
|
||||||
|
enable = mkEnableOption "btcpayserver, a self-hosted Bitcoin payment processor";
|
||||||
|
address = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "127.0.0.1";
|
||||||
|
description = "Address to listen on.";
|
||||||
|
};
|
||||||
|
port = mkOption {
|
||||||
|
type = types.port;
|
||||||
|
default = 23000;
|
||||||
|
description = "Port to listen on.";
|
||||||
|
};
|
||||||
|
package = mkOption {
|
||||||
|
type = types.package;
|
||||||
|
default = if cfg.btcpayserver.lbtc then
|
||||||
|
pkgs.btcpayserver.override { altcoinSupport = true; }
|
||||||
|
else
|
||||||
|
pkgs.btcpayserver;
|
||||||
|
defaultText = "(See source)";
|
||||||
|
description = "The package providing btcpayserver binaries.";
|
||||||
|
};
|
||||||
|
dataDir = mkOption {
|
||||||
|
type = types.path;
|
||||||
|
default = "/var/lib/btcpayserver";
|
||||||
|
description = "The data directory for btcpayserver.";
|
||||||
|
};
|
||||||
|
lightningBackend = mkOption {
|
||||||
|
type = types.nullOr (types.enum [ "clightning" "lnd" ]);
|
||||||
|
default = null;
|
||||||
|
description = "The lightning node implementation to use.";
|
||||||
|
};
|
||||||
|
lbtc = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = "Enable liquid support in btcpayserver.";
|
||||||
|
};
|
||||||
|
rootpath = mkOption {
|
||||||
|
type = types.nullOr types.str;
|
||||||
|
default = null;
|
||||||
|
example = "btcpayserver";
|
||||||
|
description = "The prefix for root-relative btcpayserver URLs.";
|
||||||
|
};
|
||||||
|
user = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "btcpayserver";
|
||||||
|
description = "The user as which to run btcpayserver.";
|
||||||
|
};
|
||||||
|
group = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = cfg.btcpayserver.user;
|
||||||
|
description = "The group as which to run btcpayserver.";
|
||||||
|
};
|
||||||
|
tor.enforce = nbLib.tor.enforce;
|
||||||
|
};
|
||||||
|
|
||||||
|
nbxplorer = {
|
||||||
|
enable = mkOption {
|
||||||
|
# This option is only used by netns-isolation
|
||||||
|
internal = true;
|
||||||
|
default = cfg.btcpayserver.enable;
|
||||||
|
description = ''
|
||||||
|
nbxplorer is always enabled when btcpayserver is enabled.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
package = mkOption {
|
||||||
|
type = types.package;
|
||||||
|
default = pkgs.nbxplorer;
|
||||||
|
defaultText = "pkgs.nbxplorer";
|
||||||
|
description = "The package providing nbxplorer binaries.";
|
||||||
|
};
|
||||||
|
address = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "127.0.0.1";
|
||||||
|
description = "Address to listen on.";
|
||||||
|
};
|
||||||
|
port = mkOption {
|
||||||
|
type = types.port;
|
||||||
|
default = 24444;
|
||||||
|
description = "Port to listen on.";
|
||||||
|
};
|
||||||
|
dataDir = mkOption {
|
||||||
|
type = types.path;
|
||||||
|
default = "/var/lib/nbxplorer";
|
||||||
|
description = "The data directory for nbxplorer.";
|
||||||
|
};
|
||||||
|
user = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "nbxplorer";
|
||||||
|
description = "The user as which to run nbxplorer.";
|
||||||
|
};
|
||||||
|
group = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = cfg.nbxplorer.user;
|
||||||
|
description = "The group as which to run nbxplorer.";
|
||||||
|
};
|
||||||
|
tor.enforce = nbLib.tor.enforce;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
cfg = config.services;
|
||||||
|
nbLib = config.nix-bitcoin.lib;
|
||||||
|
|
||||||
|
inherit (config.services) bitcoind liquidd;
|
||||||
|
in {
|
||||||
|
inherit options;
|
||||||
|
|
||||||
|
config = mkIf cfg.btcpayserver.enable {
|
||||||
|
services.bitcoind = {
|
||||||
|
enable = true;
|
||||||
|
rpc.users.btcpayserver = {
|
||||||
|
passwordHMACFromFile = true;
|
||||||
|
rpcwhitelist = cfg.bitcoind.rpc.users.public.rpcwhitelist ++ [
|
||||||
|
"setban"
|
||||||
|
"generatetoaddress"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
listenWhitelisted = true;
|
||||||
|
};
|
||||||
|
services.clightning.enable = mkIf (cfg.btcpayserver.lightningBackend == "clightning") true;
|
||||||
|
services.lnd = mkIf (cfg.btcpayserver.lightningBackend == "lnd") {
|
||||||
|
enable = true;
|
||||||
|
macaroons.btcpayserver = {
|
||||||
|
inherit (cfg.btcpayserver) user;
|
||||||
|
permissions = ''{"entity":"info","action":"read"},{"entity":"onchain","action":"read"},{"entity":"offchain","action":"read"},{"entity":"address","action":"read"},{"entity":"message","action":"read"},{"entity":"peers","action":"read"},{"entity":"signer","action":"read"},{"entity":"invoices","action":"read"},{"entity":"invoices","action":"write"},{"entity":"address","action":"write"}'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
services.liquidd = mkIf cfg.btcpayserver.lbtc {
|
||||||
|
enable = true;
|
||||||
|
listenWhitelisted = true;
|
||||||
|
};
|
||||||
|
services.postgresql = {
|
||||||
|
enable = true;
|
||||||
|
ensureDatabases = [
|
||||||
|
"btcpaydb" # This name is kept for backwards compatibility
|
||||||
|
"nbxplorer"
|
||||||
|
];
|
||||||
|
ensureUsers = [
|
||||||
|
{ name = cfg.btcpayserver.user; }
|
||||||
|
{ name = cfg.nbxplorer.user; }
|
||||||
|
];
|
||||||
|
};
|
||||||
|
systemd.services.postgresql-setup.postStart = ''
|
||||||
|
psql -tAc '
|
||||||
|
ALTER DATABASE "btcpaydb" OWNER TO "${cfg.btcpayserver.user}";
|
||||||
|
ALTER DATABASE "nbxplorer" OWNER TO "${cfg.nbxplorer.user}";
|
||||||
|
'
|
||||||
|
'';
|
||||||
|
|
||||||
|
systemd.tmpfiles.rules = [
|
||||||
|
"d '${cfg.nbxplorer.dataDir}' 0770 ${cfg.nbxplorer.user} ${cfg.nbxplorer.group} - -"
|
||||||
|
"d '${cfg.btcpayserver.dataDir}' 0770 ${cfg.btcpayserver.user} ${cfg.btcpayserver.group} - -"
|
||||||
|
];
|
||||||
|
|
||||||
|
systemd.services.nbxplorer = let
|
||||||
|
configFile = builtins.toFile "config" ''
|
||||||
|
network=${bitcoind.network}
|
||||||
|
btcrpcuser=${cfg.bitcoind.rpc.users.btcpayserver.name}
|
||||||
|
btcrpcurl=http://${nbLib.addressWithPort bitcoind.rpc.address cfg.bitcoind.rpc.port}
|
||||||
|
btcnodeendpoint=${nbLib.addressWithPort bitcoind.address bitcoind.whitelistedPort}
|
||||||
|
bind=${cfg.nbxplorer.address}
|
||||||
|
port=${toString cfg.nbxplorer.port}
|
||||||
|
${optionalString cfg.btcpayserver.lbtc ''
|
||||||
|
chains=btc,lbtc
|
||||||
|
lbtcrpcuser=${liquidd.rpcuser}
|
||||||
|
lbtcrpcurl=http://${nbLib.addressWithPort liquidd.rpc.address liquidd.rpc.port}
|
||||||
|
lbtcnodeendpoint=${nbLib.addressWithPort liquidd.address liquidd.whitelistedPort}
|
||||||
|
''}
|
||||||
|
postgres=User ID=${cfg.nbxplorer.user};Host=/run/postgresql;Database=nbxplorer
|
||||||
|
'';
|
||||||
|
in rec {
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
requires = [ "postgresql.target" ];
|
||||||
|
wants = [ "bitcoind.service" ] ++ optional cfg.btcpayserver.lbtc "liquidd.service";
|
||||||
|
after = requires ++ wants ++ [ "nix-bitcoin-secrets.target" ];
|
||||||
|
preStart = ''
|
||||||
|
install -m 600 ${configFile} '${cfg.nbxplorer.dataDir}/settings.config'
|
||||||
|
{
|
||||||
|
echo "btcrpcpassword=$(cat ${config.nix-bitcoin.secretsDir}/bitcoin-rpcpassword-btcpayserver)"
|
||||||
|
${optionalString cfg.btcpayserver.lbtc ''
|
||||||
|
echo "lbtcrpcpassword=$(cat ${config.nix-bitcoin.secretsDir}/liquid-rpcpassword)"
|
||||||
|
''}
|
||||||
|
} >> '${cfg.nbxplorer.dataDir}/settings.config'
|
||||||
|
'';
|
||||||
|
serviceConfig = nbLib.defaultHardening // {
|
||||||
|
ExecStart = ''
|
||||||
|
${cfg.nbxplorer.package}/bin/nbxplorer --conf=${cfg.nbxplorer.dataDir}/settings.config \
|
||||||
|
--datadir=${cfg.nbxplorer.dataDir}
|
||||||
|
'';
|
||||||
|
User = cfg.nbxplorer.user;
|
||||||
|
Restart = "on-failure";
|
||||||
|
RestartSec = "10s";
|
||||||
|
ReadWritePaths = [ cfg.nbxplorer.dataDir ];
|
||||||
|
MemoryDenyWriteExecute = false;
|
||||||
|
} // nbLib.allowedIPAddresses cfg.nbxplorer.tor.enforce;
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.services.btcpayserver = let
|
||||||
|
nbExplorerUrl = "http://${nbLib.addressWithPort cfg.nbxplorer.address cfg.nbxplorer.port}/";
|
||||||
|
nbExplorerCookie = "${cfg.nbxplorer.dataDir}/${bitcoind.makeNetworkName "Main" "RegTest"}/.cookie";
|
||||||
|
configFile = builtins.toFile "btcpayserver-config" (''
|
||||||
|
network=${bitcoind.network}
|
||||||
|
bind=${cfg.btcpayserver.address}
|
||||||
|
port=${toString cfg.btcpayserver.port}
|
||||||
|
socksendpoint=${config.nix-bitcoin.torClientAddressWithPort}
|
||||||
|
btcexplorerurl=${nbExplorerUrl}
|
||||||
|
btcexplorercookiefile=${nbExplorerCookie}
|
||||||
|
postgres=User ID=${cfg.btcpayserver.user};Host=/run/postgresql;Database=btcpaydb
|
||||||
|
'' + optionalString (cfg.btcpayserver.rootpath != null) ''
|
||||||
|
rootpath=${cfg.btcpayserver.rootpath}
|
||||||
|
'' + optionalString (cfg.btcpayserver.lightningBackend == "clightning") ''
|
||||||
|
btclightning=type=clightning;server=unix:///${cfg.clightning.dataDir}/${bitcoind.makeNetworkName "bitcoin" "regtest"}/lightning-rpc
|
||||||
|
'' + optionalString (cfg.btcpayserver.lightningBackend == "lnd")
|
||||||
|
(
|
||||||
|
"btclightning=type=lnd-rest;" +
|
||||||
|
"server=https://${nbLib.address cfg.lnd.restAddress}:${toString cfg.lnd.restPort}/;" +
|
||||||
|
"macaroonfilepath=/run/lnd/btcpayserver.macaroon;" +
|
||||||
|
"certfilepath=${config.services.lnd.certPath}" +
|
||||||
|
"\n"
|
||||||
|
)
|
||||||
|
+ optionalString cfg.btcpayserver.lbtc ''
|
||||||
|
chains=btc,lbtc
|
||||||
|
lbtcexplorerurl=${nbExplorerUrl}
|
||||||
|
lbtcexplorercookiefile=${nbExplorerCookie}
|
||||||
|
'');
|
||||||
|
in rec {
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
requires = [ "postgresql.target" ];
|
||||||
|
wants = [ "nbxplorer.service" ]
|
||||||
|
++ optional (cfg.btcpayserver.lightningBackend != null) "${cfg.btcpayserver.lightningBackend}.service";
|
||||||
|
after = requires ++ wants;
|
||||||
|
serviceConfig = nbLib.defaultHardening // {
|
||||||
|
ExecStart = ''
|
||||||
|
${cfg.btcpayserver.package}/bin/btcpayserver --conf=${configFile} \
|
||||||
|
--datadir='${cfg.btcpayserver.dataDir}'
|
||||||
|
'';
|
||||||
|
User = cfg.btcpayserver.user;
|
||||||
|
# Also restart after the program has exited successfully.
|
||||||
|
# This is required to support restarting from the web interface after
|
||||||
|
# interactive plugin installation.
|
||||||
|
# Restart rate limiting is implemented via the `startLimit*` options below.
|
||||||
|
Restart = "always";
|
||||||
|
ReadWritePaths = [ cfg.btcpayserver.dataDir ];
|
||||||
|
MemoryDenyWriteExecute = false;
|
||||||
|
} // nbLib.allowedIPAddresses cfg.btcpayserver.tor.enforce;
|
||||||
|
startLimitIntervalSec = 30;
|
||||||
|
startLimitBurst = 10;
|
||||||
|
};
|
||||||
|
|
||||||
|
users.users.${cfg.nbxplorer.user} = {
|
||||||
|
isSystemUser = true;
|
||||||
|
group = cfg.nbxplorer.group;
|
||||||
|
extraGroups = [ "bitcoinrpc-public" ]
|
||||||
|
++ optional cfg.btcpayserver.lbtc liquidd.group;
|
||||||
|
home = cfg.nbxplorer.dataDir;
|
||||||
|
};
|
||||||
|
users.groups.${cfg.nbxplorer.group} = {};
|
||||||
|
users.users.${cfg.btcpayserver.user} = {
|
||||||
|
isSystemUser = true;
|
||||||
|
group = cfg.btcpayserver.group;
|
||||||
|
extraGroups = [ cfg.nbxplorer.group ]
|
||||||
|
++ optional (cfg.btcpayserver.lightningBackend == "clightning") cfg.clightning.user;
|
||||||
|
home = cfg.btcpayserver.dataDir;
|
||||||
|
};
|
||||||
|
users.groups.${cfg.btcpayserver.group} = {};
|
||||||
|
|
||||||
|
nix-bitcoin.secrets = {
|
||||||
|
bitcoin-rpcpassword-btcpayserver = {
|
||||||
|
user = cfg.bitcoind.user;
|
||||||
|
group = cfg.nbxplorer.group;
|
||||||
|
};
|
||||||
|
bitcoin-HMAC-btcpayserver.user = cfg.bitcoind.user;
|
||||||
|
};
|
||||||
|
nix-bitcoin.generateSecretsCmds.btcpayserver = ''
|
||||||
|
makeBitcoinRPCPassword btcpayserver
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}
|
||||||
+106
@@ -0,0 +1,106 @@
|
|||||||
|
{ config, lib, pkgs, ... }:
|
||||||
|
|
||||||
|
with lib;
|
||||||
|
let
|
||||||
|
options.services.electrs = {
|
||||||
|
enable = mkEnableOption "electrs, an Electrum server implemented in Rust";
|
||||||
|
address = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "127.0.0.1";
|
||||||
|
description = "Address to listen for RPC connections.";
|
||||||
|
};
|
||||||
|
port = mkOption {
|
||||||
|
type = types.port;
|
||||||
|
default = 50001;
|
||||||
|
description = "Port to listen for RPC connections.";
|
||||||
|
};
|
||||||
|
dataDir = mkOption {
|
||||||
|
type = types.path;
|
||||||
|
default = "/var/lib/electrs";
|
||||||
|
description = "The data directory for electrs.";
|
||||||
|
};
|
||||||
|
monitoringPort = mkOption {
|
||||||
|
type = types.port;
|
||||||
|
default = 4224;
|
||||||
|
description = "Prometheus monitoring port.";
|
||||||
|
};
|
||||||
|
extraArgs = mkOption {
|
||||||
|
type = types.separatedString " ";
|
||||||
|
default = "";
|
||||||
|
description = "Extra command line arguments passed to electrs.";
|
||||||
|
};
|
||||||
|
user = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "electrs";
|
||||||
|
description = "The user as which to run electrs.";
|
||||||
|
};
|
||||||
|
group = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = cfg.user;
|
||||||
|
description = "The group as which to run electrs.";
|
||||||
|
};
|
||||||
|
tor.enforce = nbLib.tor.enforce;
|
||||||
|
};
|
||||||
|
|
||||||
|
cfg = config.services.electrs;
|
||||||
|
nbLib = config.nix-bitcoin.lib;
|
||||||
|
secretsDir = config.nix-bitcoin.secretsDir;
|
||||||
|
bitcoind = config.services.bitcoind;
|
||||||
|
in {
|
||||||
|
inherit options;
|
||||||
|
|
||||||
|
config = mkIf cfg.enable {
|
||||||
|
assertions = [
|
||||||
|
{ assertion = bitcoind.prune == 0;
|
||||||
|
message = "electrs does not support bitcoind pruning.";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
services.bitcoind = {
|
||||||
|
enable = true;
|
||||||
|
listenWhitelisted = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.tmpfiles.rules = [
|
||||||
|
"d '${cfg.dataDir}' 0770 ${cfg.user} ${cfg.group} - -"
|
||||||
|
];
|
||||||
|
|
||||||
|
systemd.services.electrs = {
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
requires = [ "bitcoind.service" ];
|
||||||
|
after = [ "bitcoind.service" "nix-bitcoin-secrets.target" ];
|
||||||
|
preStart = ''
|
||||||
|
echo "auth = \"${bitcoind.rpc.users.public.name}:$(cat ${secretsDir}/bitcoin-rpcpassword-public)\"" \
|
||||||
|
> electrs.toml
|
||||||
|
'';
|
||||||
|
serviceConfig = nbLib.defaultHardening // {
|
||||||
|
# electrs only uses the working directory for reading electrs.toml
|
||||||
|
WorkingDirectory = cfg.dataDir;
|
||||||
|
ExecStart = ''
|
||||||
|
${pkgs.electrs}/bin/electrs \
|
||||||
|
--log-filters=INFO \
|
||||||
|
--network=${bitcoind.makeNetworkName "bitcoin" "regtest"} \
|
||||||
|
--db-dir='${cfg.dataDir}' \
|
||||||
|
--daemon-dir='${bitcoind.dataDir}' \
|
||||||
|
--electrum-rpc-addr=${cfg.address}:${toString cfg.port} \
|
||||||
|
--monitoring-addr=${cfg.address}:${toString cfg.monitoringPort} \
|
||||||
|
--daemon-rpc-addr=${nbLib.addressWithPort bitcoind.rpc.address bitcoind.rpc.port} \
|
||||||
|
--daemon-p2p-addr=${nbLib.addressWithPort bitcoind.address bitcoind.whitelistedPort} \
|
||||||
|
${cfg.extraArgs}
|
||||||
|
'';
|
||||||
|
User = cfg.user;
|
||||||
|
Group = cfg.group;
|
||||||
|
Restart = "on-failure";
|
||||||
|
RestartSec = "10s";
|
||||||
|
ReadWritePaths = [ cfg.dataDir ];
|
||||||
|
} // nbLib.allowedIPAddresses cfg.tor.enforce;
|
||||||
|
};
|
||||||
|
|
||||||
|
users.users.${cfg.user} = {
|
||||||
|
isSystemUser = true;
|
||||||
|
group = cfg.group;
|
||||||
|
extraGroups = [ "bitcoinrpc-public" ];
|
||||||
|
};
|
||||||
|
users.groups.${cfg.group} = {};
|
||||||
|
};
|
||||||
|
}
|
||||||
+54
@@ -0,0 +1,54 @@
|
|||||||
|
{ lib, config, ... }:
|
||||||
|
let
|
||||||
|
defaultTrue = lib.mkDefault true;
|
||||||
|
defaultEnableTorProxy = {
|
||||||
|
tor.proxy = defaultTrue;
|
||||||
|
tor.enforce = defaultTrue;
|
||||||
|
};
|
||||||
|
defaultEnforceTor = {
|
||||||
|
tor.enforce = defaultTrue;
|
||||||
|
};
|
||||||
|
in {
|
||||||
|
services.tor = {
|
||||||
|
enable = true;
|
||||||
|
client.enable = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
services = {
|
||||||
|
# Use Tor as a proxy for outgoing connections
|
||||||
|
# and restrict all connections to Tor
|
||||||
|
#
|
||||||
|
bitcoind = defaultEnableTorProxy;
|
||||||
|
clightning = defaultEnableTorProxy;
|
||||||
|
lnd = defaultEnableTorProxy;
|
||||||
|
lightning-loop = defaultEnableTorProxy;
|
||||||
|
liquidd = defaultEnableTorProxy;
|
||||||
|
# TODO-EXTERNAL:
|
||||||
|
# disable Tor enforcement until btcpayserver can fetch rates over Tor
|
||||||
|
# btcpayserver = defaultEnableTorProxy;
|
||||||
|
lightning-pool = defaultEnableTorProxy;
|
||||||
|
mempool = defaultEnableTorProxy;
|
||||||
|
|
||||||
|
# These services don't make outgoing connections
|
||||||
|
# (or use Tor by default in case of joinmarket)
|
||||||
|
# but we restrict them to Tor just to be safe.
|
||||||
|
#
|
||||||
|
electrs = defaultEnforceTor;
|
||||||
|
fulcrum = defaultEnforceTor;
|
||||||
|
nbxplorer = defaultEnforceTor;
|
||||||
|
rtl = defaultEnforceTor;
|
||||||
|
joinmarket = defaultEnforceTor;
|
||||||
|
joinmarket-ob-watcher = defaultEnforceTor;
|
||||||
|
clightning-rest = defaultEnforceTor;
|
||||||
|
};
|
||||||
|
|
||||||
|
# Add onion services for incoming connections
|
||||||
|
nix-bitcoin.onionServices = {
|
||||||
|
bitcoind.enable = defaultTrue;
|
||||||
|
liquidd.enable = defaultTrue;
|
||||||
|
electrs.enable = defaultTrue;
|
||||||
|
fulcrum.enable = defaultTrue;
|
||||||
|
joinmarket-ob-watcher.enable = defaultTrue;
|
||||||
|
rtl.enable = defaultTrue;
|
||||||
|
};
|
||||||
|
}
|
||||||
Vendored
+132
@@ -0,0 +1,132 @@
|
|||||||
|
lib: pkgs: config:
|
||||||
|
|
||||||
|
with lib;
|
||||||
|
|
||||||
|
# See `man systemd.exec` and `man systemd.resource-control` for an explanation
|
||||||
|
# of the systemd-related options available through this file.
|
||||||
|
let self = {
|
||||||
|
# These settings roughly follow systemd's "strict" security profile
|
||||||
|
defaultHardening = {
|
||||||
|
PrivateTmp = true;
|
||||||
|
ProtectSystem = "strict";
|
||||||
|
ProtectHome = true;
|
||||||
|
NoNewPrivileges = true;
|
||||||
|
PrivateDevices = true;
|
||||||
|
MemoryDenyWriteExecute = true;
|
||||||
|
ProtectKernelTunables = true;
|
||||||
|
ProtectKernelModules = true;
|
||||||
|
ProtectKernelLogs = true;
|
||||||
|
ProtectClock = true;
|
||||||
|
ProtectProc = "invisible";
|
||||||
|
ProcSubset = "pid";
|
||||||
|
ProtectControlGroups = true;
|
||||||
|
RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6";
|
||||||
|
RestrictNamespaces = true;
|
||||||
|
LockPersonality = true;
|
||||||
|
IPAddressDeny = "any";
|
||||||
|
PrivateUsers = true;
|
||||||
|
RestrictSUIDSGID = true;
|
||||||
|
RemoveIPC = true;
|
||||||
|
RestrictRealtime = true;
|
||||||
|
ProtectHostname = true;
|
||||||
|
CapabilityBoundingSet = "";
|
||||||
|
# @system-service whitelist and docker seccomp blacklist (except for "clone"
|
||||||
|
# which is a core requirement for systemd services)
|
||||||
|
# @system-service is defined in src/shared/seccomp-util.c (systemd source)
|
||||||
|
SystemCallFilter = [ "@system-service" "~add_key kcmp keyctl mbind move_pages name_to_handle_at personality process_vm_readv process_vm_writev request_key setns unshare userfaultfd" ];
|
||||||
|
SystemCallArchitectures = "native";
|
||||||
|
};
|
||||||
|
|
||||||
|
allowNetlink = {
|
||||||
|
RestrictAddressFamilies = self.defaultHardening.RestrictAddressFamilies + " AF_NETLINK";
|
||||||
|
};
|
||||||
|
|
||||||
|
nodejs = {
|
||||||
|
# Required for JIT compilation
|
||||||
|
MemoryDenyWriteExecute = false;
|
||||||
|
# Required by nodejs >= 18
|
||||||
|
SystemCallFilter = self.defaultHardening.SystemCallFilter ++ [ "@pkey" ];
|
||||||
|
};
|
||||||
|
|
||||||
|
# Allow takes precedence over Deny.
|
||||||
|
allowLocalIPAddresses = {
|
||||||
|
IPAddressAllow = [
|
||||||
|
"127.0.0.1/32"
|
||||||
|
"::1/128"
|
||||||
|
"169.254.0.0/16"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
allowAllIPAddresses = { IPAddressAllow = "any"; };
|
||||||
|
allowTor = self.allowLocalIPAddresses;
|
||||||
|
allowedIPAddresses = onlyLocal:
|
||||||
|
if onlyLocal
|
||||||
|
then self.allowLocalIPAddresses
|
||||||
|
else self.allowAllIPAddresses;
|
||||||
|
|
||||||
|
tor = {
|
||||||
|
proxy = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = "Whether to proxy outgoing connections with Tor.";
|
||||||
|
};
|
||||||
|
enforce = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = ''
|
||||||
|
Whether to enforce Tor on this service by only allowing connections
|
||||||
|
from and to localhost and link-local addresses.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
script = name: src: pkgs.writers.writeBash name ''
|
||||||
|
set -eo pipefail
|
||||||
|
${src}
|
||||||
|
'';
|
||||||
|
|
||||||
|
# Used for ExecStart*
|
||||||
|
rootScript = name: src: "+${self.script name src}";
|
||||||
|
|
||||||
|
cliExec = mkOption {
|
||||||
|
# Used by netns-isolation to execute the cli in the service's private netns
|
||||||
|
internal = true;
|
||||||
|
type = types.str;
|
||||||
|
default = "exec";
|
||||||
|
};
|
||||||
|
|
||||||
|
mkOnionService = map: {
|
||||||
|
map = [ map ];
|
||||||
|
version = 3;
|
||||||
|
};
|
||||||
|
|
||||||
|
# Convert a bind address, which may be a special INADDR_ANY address,
|
||||||
|
# to an actual IP address
|
||||||
|
address = addr:
|
||||||
|
if addr == "0.0.0.0" then
|
||||||
|
"127.0.0.1"
|
||||||
|
else if addr == "::" then
|
||||||
|
"::1"
|
||||||
|
else
|
||||||
|
addr;
|
||||||
|
|
||||||
|
addressWithPort = addr: port: "${self.address addr}:${toString port}";
|
||||||
|
|
||||||
|
optionalAttr = cond: name: if cond then name else null;
|
||||||
|
|
||||||
|
mkCertExtraAltNames = cert:
|
||||||
|
builtins.concatStringsSep "," (
|
||||||
|
(map (domain: "DNS:${domain}") cert.extraDomains) ++
|
||||||
|
(map (ip: "IP:${ip}") cert.extraIPs)
|
||||||
|
);
|
||||||
|
|
||||||
|
test = {
|
||||||
|
mkIfTest = test: mkIf (config.tests.${test} or false);
|
||||||
|
};
|
||||||
|
|
||||||
|
mkAlias = default: mkOption {
|
||||||
|
internal = true;
|
||||||
|
readOnly = true;
|
||||||
|
inherit default;
|
||||||
|
};
|
||||||
|
|
||||||
|
}; in self
|
||||||
Vendored
+314
@@ -0,0 +1,314 @@
|
|||||||
|
{ config, lib, pkgs, ... }:
|
||||||
|
|
||||||
|
with lib;
|
||||||
|
let
|
||||||
|
options.services.lnd = {
|
||||||
|
enable = mkEnableOption "Lightning Network daemon, a Lightning Network implementation in Go";
|
||||||
|
address = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "127.0.0.1";
|
||||||
|
description = "Address to listen for peer connections";
|
||||||
|
};
|
||||||
|
port = mkOption {
|
||||||
|
type = types.port;
|
||||||
|
default = 9735;
|
||||||
|
description = "Port to listen for peer connections";
|
||||||
|
};
|
||||||
|
rpcAddress = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "127.0.0.1";
|
||||||
|
description = "Address to listen for RPC connections.";
|
||||||
|
};
|
||||||
|
rpcPort = mkOption {
|
||||||
|
type = types.port;
|
||||||
|
default = 10009;
|
||||||
|
description = "Port to listen for gRPC connections.";
|
||||||
|
};
|
||||||
|
restAddress = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "127.0.0.1";
|
||||||
|
description = "Address to listen for REST connections.";
|
||||||
|
};
|
||||||
|
restPort = mkOption {
|
||||||
|
type = types.port;
|
||||||
|
default = 8080;
|
||||||
|
description = "Port to listen for REST connections.";
|
||||||
|
};
|
||||||
|
dataDir = mkOption {
|
||||||
|
type = types.path;
|
||||||
|
default = "/var/lib/lnd";
|
||||||
|
description = "The data directory for LND.";
|
||||||
|
};
|
||||||
|
networkDir = mkOption {
|
||||||
|
readOnly = true;
|
||||||
|
default = "${cfg.dataDir}/chain/bitcoin/${bitcoind.network}";
|
||||||
|
description = "The network data directory.";
|
||||||
|
};
|
||||||
|
tor-socks = mkOption {
|
||||||
|
type = types.nullOr types.str;
|
||||||
|
default = if cfg.tor.proxy then config.nix-bitcoin.torClientAddressWithPort else null;
|
||||||
|
description = "Socks proxy for connecting to Tor nodes";
|
||||||
|
};
|
||||||
|
macaroons = mkOption {
|
||||||
|
default = {};
|
||||||
|
type = with types; attrsOf (submodule {
|
||||||
|
options = {
|
||||||
|
user = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
description = "User who owns the macaroon.";
|
||||||
|
};
|
||||||
|
permissions = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
example = ''
|
||||||
|
{"entity":"info","action":"read"},{"entity":"onchain","action":"read"}
|
||||||
|
'';
|
||||||
|
description = "List of granted macaroon permissions.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
});
|
||||||
|
description = ''
|
||||||
|
Extra macaroon definitions.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
certificate = {
|
||||||
|
extraIPs = mkOption {
|
||||||
|
type = with types; listOf str;
|
||||||
|
default = [];
|
||||||
|
example = [ "60.100.0.1" ];
|
||||||
|
description = ''
|
||||||
|
Extra `subjectAltName` IPs added to the certificate.
|
||||||
|
This works the same as lnd option {option}`tlsextraip`.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
extraDomains = mkOption {
|
||||||
|
type = with types; listOf str;
|
||||||
|
default = [];
|
||||||
|
example = [ "example.com" ];
|
||||||
|
description = ''
|
||||||
|
Extra `subjectAltName` domain names added to the certificate.
|
||||||
|
This works the same as lnd option {option}`tlsextradomain`.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
extraConfig = mkOption {
|
||||||
|
type = types.lines;
|
||||||
|
default = "";
|
||||||
|
example = ''
|
||||||
|
autopilot.active=1
|
||||||
|
'';
|
||||||
|
description = ''
|
||||||
|
Extra lines appended to {file}`lnd.conf`.
|
||||||
|
See here for all available options:
|
||||||
|
https://github.com/lightningnetwork/lnd/blob/master/sample-lnd.conf
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
package = mkOption {
|
||||||
|
type = types.package;
|
||||||
|
default = pkgs.lnd;
|
||||||
|
defaultText = "pkgs.lnd";
|
||||||
|
description = "The package providing lnd binaries.";
|
||||||
|
};
|
||||||
|
cli = mkOption {
|
||||||
|
default = pkgs.writers.writeBashBin "lncli"
|
||||||
|
# Switch user because lnd makes datadir contents readable by user only
|
||||||
|
''
|
||||||
|
${runAsUser} ${cfg.user} ${cfg.package}/bin/lncli \
|
||||||
|
--rpcserver ${cfg.rpcAddress}:${toString cfg.rpcPort} \
|
||||||
|
--tlscertpath '${cfg.certPath}' \
|
||||||
|
--macaroonpath '${networkDir}/admin.macaroon' "$@"
|
||||||
|
'';
|
||||||
|
defaultText = "(See source)";
|
||||||
|
description = "Binary to connect with the lnd instance.";
|
||||||
|
};
|
||||||
|
getPublicAddressCmd = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "";
|
||||||
|
description = ''
|
||||||
|
Bash expression which outputs the public service address to announce to peers.
|
||||||
|
If left empty, no address is announced.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
user = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "lnd";
|
||||||
|
description = "The user as which to run LND.";
|
||||||
|
};
|
||||||
|
group = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = cfg.user;
|
||||||
|
description = "The group as which to run LND.";
|
||||||
|
};
|
||||||
|
certPath = mkOption {
|
||||||
|
readOnly = true;
|
||||||
|
default = "${secretsDir}/lnd-cert";
|
||||||
|
description = "LND TLS certificate path.";
|
||||||
|
};
|
||||||
|
tor = nbLib.tor;
|
||||||
|
};
|
||||||
|
|
||||||
|
cfg = config.services.lnd;
|
||||||
|
nbLib = config.nix-bitcoin.lib;
|
||||||
|
secretsDir = config.nix-bitcoin.secretsDir;
|
||||||
|
runAsUser = config.nix-bitcoin.runAsUserCmd;
|
||||||
|
lndinit = "${pkgs.lndinit}/bin/lndinit";
|
||||||
|
|
||||||
|
bitcoind = config.services.bitcoind;
|
||||||
|
|
||||||
|
bitcoindRpcAddress = nbLib.address bitcoind.rpc.address;
|
||||||
|
networkDir = cfg.networkDir;
|
||||||
|
configFile = pkgs.writeText "lnd.conf" ''
|
||||||
|
datadir=${cfg.dataDir}
|
||||||
|
tlscertpath=${cfg.certPath}
|
||||||
|
tlskeypath=${secretsDir}/lnd-key
|
||||||
|
|
||||||
|
# We're logging via journald
|
||||||
|
logging.file.disable=1
|
||||||
|
logging.console.no-timestamps=1
|
||||||
|
|
||||||
|
listen=${toString cfg.address}:${toString cfg.port}
|
||||||
|
rpclisten=${cfg.rpcAddress}:${toString cfg.rpcPort}
|
||||||
|
restlisten=${cfg.restAddress}:${toString cfg.restPort}
|
||||||
|
|
||||||
|
bitcoin.${bitcoind.network}=1
|
||||||
|
bitcoin.node=bitcoind
|
||||||
|
|
||||||
|
${optionalString (cfg.tor.proxy) "tor.active=true"}
|
||||||
|
${optionalString (cfg.tor-socks != null) "tor.socks=${cfg.tor-socks}"}
|
||||||
|
|
||||||
|
bitcoind.rpchost=${bitcoindRpcAddress}:${toString bitcoind.rpc.port}
|
||||||
|
bitcoind.rpcuser=${bitcoind.rpc.users.public.name}
|
||||||
|
bitcoind.zmqpubrawblock=${zmqHandleSpecialAddress bitcoind.zmqpubrawblock}
|
||||||
|
bitcoind.zmqpubrawtx=${zmqHandleSpecialAddress bitcoind.zmqpubrawtx}
|
||||||
|
|
||||||
|
wallet-unlock-password-file=${secretsDir}/lnd-wallet-password
|
||||||
|
|
||||||
|
${cfg.extraConfig}
|
||||||
|
'';
|
||||||
|
|
||||||
|
zmqHandleSpecialAddress = builtins.replaceStrings [ "0.0.0.0" "[::]" ] [ "127.0.0.1" "[::1]" ];
|
||||||
|
in {
|
||||||
|
|
||||||
|
inherit options;
|
||||||
|
|
||||||
|
config = mkIf cfg.enable {
|
||||||
|
assertions = [
|
||||||
|
{ assertion =
|
||||||
|
!(config.services ? clightning)
|
||||||
|
|| !config.services.clightning.enable
|
||||||
|
|| config.services.clightning.port != cfg.port;
|
||||||
|
message = ''
|
||||||
|
LND and clightning can't both bind to lightning port 9735. Either
|
||||||
|
disable LND/clightning or change services.clightning.port or
|
||||||
|
services.lnd.port to a port other than 9735.
|
||||||
|
'';
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
services.bitcoind = {
|
||||||
|
enable = true;
|
||||||
|
|
||||||
|
# Increase rpc thread count due to reports that lightning implementations fail
|
||||||
|
# under high bitcoind rpc load
|
||||||
|
rpc.threads = 16;
|
||||||
|
|
||||||
|
zmqpubrawblock = mkDefault "tcp://${bitcoindRpcAddress}:28332";
|
||||||
|
zmqpubrawtx = mkDefault "tcp://${bitcoindRpcAddress}:28333";
|
||||||
|
};
|
||||||
|
|
||||||
|
environment.systemPackages = [ cfg.package (hiPrio cfg.cli) ];
|
||||||
|
|
||||||
|
systemd.tmpfiles.rules = [
|
||||||
|
"d '${cfg.dataDir}' 0770 ${cfg.user} ${cfg.group} - -"
|
||||||
|
];
|
||||||
|
|
||||||
|
services.lnd.certificate.extraIPs = mkIf (cfg.rpcAddress != "127.0.0.1") [ "${cfg.rpcAddress}" ];
|
||||||
|
|
||||||
|
systemd.services.lnd = {
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
requires = [ "bitcoind.service" ];
|
||||||
|
after = [ "bitcoind.service" "nix-bitcoin-secrets.target" ];
|
||||||
|
preStart = ''
|
||||||
|
install -m600 ${configFile} '${cfg.dataDir}/lnd.conf'
|
||||||
|
{
|
||||||
|
echo "bitcoind.rpcpass=$(cat ${secretsDir}/bitcoin-rpcpassword-public)"
|
||||||
|
${optionalString (cfg.getPublicAddressCmd != "") ''
|
||||||
|
echo "externalip=$(${cfg.getPublicAddressCmd})"
|
||||||
|
''}
|
||||||
|
} >> '${cfg.dataDir}/lnd.conf'
|
||||||
|
|
||||||
|
if [[ ! -f ${networkDir}/wallet.db ]]; then
|
||||||
|
seed='${cfg.dataDir}/lnd-seed-mnemonic'
|
||||||
|
|
||||||
|
if [[ ! -f "$seed" ]]; then
|
||||||
|
echo "Create lnd seed"
|
||||||
|
(umask u=r,go=; ${lndinit} gen-seed > "$seed")
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Create lnd wallet"
|
||||||
|
${lndinit} -v init-wallet \
|
||||||
|
--file.seed="$seed" \
|
||||||
|
--file.wallet-password='${secretsDir}/lnd-wallet-password' \
|
||||||
|
--init-file.output-wallet-dir='${cfg.networkDir}'
|
||||||
|
fi
|
||||||
|
'';
|
||||||
|
serviceConfig = nbLib.defaultHardening // {
|
||||||
|
Type = "notify";
|
||||||
|
RuntimeDirectory = "lnd"; # Only used to store custom macaroons
|
||||||
|
RuntimeDirectoryMode = "711";
|
||||||
|
ExecStart = "${cfg.package}/bin/lnd --configfile='${cfg.dataDir}/lnd.conf'";
|
||||||
|
User = cfg.user;
|
||||||
|
TimeoutSec = "15min";
|
||||||
|
Restart = "on-failure";
|
||||||
|
RestartSec = "10s";
|
||||||
|
ReadWritePaths = [ cfg.dataDir ];
|
||||||
|
ExecStartPost = let
|
||||||
|
curl = "${pkgs.curl}/bin/curl -fsS --cacert ${cfg.certPath}";
|
||||||
|
restUrl = "https://${nbLib.addressWithPort cfg.restAddress cfg.restPort}/v1";
|
||||||
|
# Setting macaroon permissions for other users needs root permissions
|
||||||
|
script = nbLib.rootScript "lnd-create-macaroons" ''
|
||||||
|
umask ug=r,o=
|
||||||
|
${lib.concatMapStrings (macaroon: ''
|
||||||
|
echo "Create custom macaroon ${macaroon}"
|
||||||
|
macaroonPath="$RUNTIME_DIRECTORY/${macaroon}.macaroon"
|
||||||
|
${curl} \
|
||||||
|
-H "Grpc-Metadata-macaroon: $(${pkgs.xxd}/bin/xxd -ps -u -c 99999 '${networkDir}/admin.macaroon')" \
|
||||||
|
-X POST \
|
||||||
|
-d '{"permissions":[${cfg.macaroons.${macaroon}.permissions}]}' \
|
||||||
|
${restUrl}/macaroon |\
|
||||||
|
${pkgs.jq}/bin/jq -c '.macaroon' | ${pkgs.xxd}/bin/xxd -p -r > "$macaroonPath"
|
||||||
|
chown ${cfg.macaroons.${macaroon}.user}: "$macaroonPath"
|
||||||
|
'') (attrNames cfg.macaroons)}
|
||||||
|
'';
|
||||||
|
in [
|
||||||
|
script
|
||||||
|
];
|
||||||
|
} // nbLib.allowedIPAddresses cfg.tor.enforce;
|
||||||
|
};
|
||||||
|
|
||||||
|
users.users.${cfg.user} = {
|
||||||
|
isSystemUser = true;
|
||||||
|
group = cfg.group;
|
||||||
|
extraGroups = [ "bitcoinrpc-public" ];
|
||||||
|
home = cfg.dataDir; # lnd creates .lnd dir in HOME
|
||||||
|
};
|
||||||
|
users.groups.${cfg.group} = {};
|
||||||
|
nix-bitcoin.operator = {
|
||||||
|
groups = [ cfg.group ];
|
||||||
|
allowRunAsUsers = [ cfg.user ];
|
||||||
|
};
|
||||||
|
|
||||||
|
nix-bitcoin.secrets = {
|
||||||
|
lnd-wallet-password.user = cfg.user;
|
||||||
|
lnd-key.user = cfg.user;
|
||||||
|
lnd-cert.user = cfg.user;
|
||||||
|
lnd-cert.permissions = "444"; # world readable
|
||||||
|
};
|
||||||
|
# Advantages of manually pre-generating certs:
|
||||||
|
# - Reduces dynamic state
|
||||||
|
# - Enables deployment of a mesh of server plus client nodes with predefined certs
|
||||||
|
nix-bitcoin.generateSecretsCmds.lnd = ''
|
||||||
|
makePasswordSecret lnd-wallet-password
|
||||||
|
makeCert lnd '${nbLib.mkCertExtraAltNames cfg.certificate}'
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}
|
||||||
+282
@@ -0,0 +1,282 @@
|
|||||||
|
{ config, lib, pkgs, ... }:
|
||||||
|
|
||||||
|
with lib;
|
||||||
|
let
|
||||||
|
options = {
|
||||||
|
services.lnd.lndconnect = {
|
||||||
|
enable = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = ''
|
||||||
|
Add a `lndconnect` binary to the system environment which prints
|
||||||
|
connection info for lnd clients.
|
||||||
|
See: https://github.com/LN-Zap/lndconnect
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
```bash
|
||||||
|
# Print QR code
|
||||||
|
lndconnect
|
||||||
|
|
||||||
|
# Print URL
|
||||||
|
lndconnect --url
|
||||||
|
```
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
onion = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = ''
|
||||||
|
Create an onion service for the lnd REST server,
|
||||||
|
which is used by lndconnect.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
services.clightning.plugins.clnrest.lnconnect = {
|
||||||
|
enable = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = ''
|
||||||
|
Add a `lnconnect-clnrest` binary to the system environment which prints
|
||||||
|
connection info for clightning clients.
|
||||||
|
See: https://github.com/LN-Zap/lndconnect
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
```bash
|
||||||
|
# Print QR code
|
||||||
|
lnconnect-clnrest
|
||||||
|
|
||||||
|
# Print URL
|
||||||
|
lnconnect-clnrest --url
|
||||||
|
```
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
onion = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = ''
|
||||||
|
Create an onion service for the clnrest server,
|
||||||
|
which is used by lnconnect.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
services.clightning-rest.lndconnect = {
|
||||||
|
enable = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = ''
|
||||||
|
Add a `lndconnect-clightning` binary to the system environment which prints
|
||||||
|
connection info for clightning clients.
|
||||||
|
See: https://github.com/LN-Zap/lndconnect
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
```bash
|
||||||
|
# Print QR code
|
||||||
|
lndconnect-clightning
|
||||||
|
|
||||||
|
# Print URL
|
||||||
|
lndconnect-clightning --url
|
||||||
|
```
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
onion = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = ''
|
||||||
|
Create an onion service for the clightning REST server,
|
||||||
|
which is used by lndconnect.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
nix-bitcoin.mkLndconnect = mkOption {
|
||||||
|
readOnly = true;
|
||||||
|
default = mkLndconnect;
|
||||||
|
description = ''
|
||||||
|
A function to create a lndconnect binary.
|
||||||
|
See the source for further details.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
nbLib = config.nix-bitcoin.lib;
|
||||||
|
runAsUser = config.nix-bitcoin.runAsUserCmd;
|
||||||
|
|
||||||
|
inherit (config.services)
|
||||||
|
lnd
|
||||||
|
clightning
|
||||||
|
clightning-rest;
|
||||||
|
|
||||||
|
inherit (clightning.plugins) clnrest;
|
||||||
|
|
||||||
|
mkLndconnect = {
|
||||||
|
name,
|
||||||
|
shebang ? "#!${pkgs.stdenv.shell} -e",
|
||||||
|
isClightning ? false,
|
||||||
|
isClnrest ? false,
|
||||||
|
port,
|
||||||
|
authSecretPath,
|
||||||
|
enableOnion,
|
||||||
|
onionService ? null,
|
||||||
|
certPath ? null
|
||||||
|
}:
|
||||||
|
# TODO-EXTERNAL:
|
||||||
|
# lndconnect requires a --configfile argument, although it's unused
|
||||||
|
# https://github.com/LN-Zap/lndconnect/issues/25
|
||||||
|
lib.hiPrio (pkgs.writeScriptBin name ''
|
||||||
|
${shebang}
|
||||||
|
url=$(
|
||||||
|
${getExe pkgs.lndconnect} --url \
|
||||||
|
${optionalString enableOnion "--host=$(cat ${config.nix-bitcoin.onionAddresses.dataDir}/${onionService})"} \
|
||||||
|
--port=${toString port} \
|
||||||
|
${if enableOnion || certPath == null then "--nocert" else "--tlscertpath='${certPath}'"} \
|
||||||
|
--adminmacaroonpath='${authSecretPath}' \
|
||||||
|
--configfile=/dev/null "$@"
|
||||||
|
)
|
||||||
|
|
||||||
|
${optionalString isClightning
|
||||||
|
# - Change URL procotcol to c-lightning-rest
|
||||||
|
# - Encode macaroon as hex (in uppercase) instead of base 64.
|
||||||
|
# Because `macaroon` is always the last URL fragment, the
|
||||||
|
# sed replacement below works correctly.
|
||||||
|
''
|
||||||
|
macaroonHex=$(${getExe pkgs.xxd} -p -u -c 99999 '${authSecretPath}')
|
||||||
|
url=$(
|
||||||
|
echo "$url" | ${getExe pkgs.gnused} "
|
||||||
|
s|^lndconnect|c-lightning-rest|
|
||||||
|
s|macaroon=.*|macaroon=$macaroonHex|
|
||||||
|
";
|
||||||
|
)
|
||||||
|
''
|
||||||
|
}
|
||||||
|
|
||||||
|
${optionalString isClnrest
|
||||||
|
# Change URL procotcol to clnrest
|
||||||
|
''
|
||||||
|
url=$(
|
||||||
|
echo "$url" | ${getExe pkgs.gnused} "
|
||||||
|
s|^lndconnect|clnrest|
|
||||||
|
s|macaroon=.*|rune=$(cat '${authSecretPath}')|
|
||||||
|
";
|
||||||
|
)
|
||||||
|
''
|
||||||
|
}
|
||||||
|
|
||||||
|
# If --url is in args
|
||||||
|
if [[ " $* " =~ " --url " ]]; then
|
||||||
|
echo "$url"
|
||||||
|
else
|
||||||
|
# This UTF-8 encoding yields a smaller, more convenient output format
|
||||||
|
# compared to the native lndconnect output
|
||||||
|
echo -n "$url" | ${getExe pkgs.qrencode} -t UTF8 -o -
|
||||||
|
fi
|
||||||
|
'');
|
||||||
|
|
||||||
|
operatorName = config.nix-bitcoin.operator.name;
|
||||||
|
in {
|
||||||
|
inherit options;
|
||||||
|
|
||||||
|
config = mkMerge [
|
||||||
|
(mkIf (lnd.enable && lnd.lndconnect.enable)
|
||||||
|
(mkMerge [
|
||||||
|
{
|
||||||
|
environment.systemPackages = [(
|
||||||
|
mkLndconnect {
|
||||||
|
name = "lndconnect";
|
||||||
|
# Run as lnd user because the macaroon and cert are not group-readable
|
||||||
|
shebang = "#!/usr/bin/env -S ${runAsUser} ${lnd.user} ${pkgs.bash}/bin/bash";
|
||||||
|
enableOnion = lnd.lndconnect.onion;
|
||||||
|
onionService = "${lnd.user}/lnd-rest";
|
||||||
|
port = lnd.restPort;
|
||||||
|
certPath = lnd.certPath;
|
||||||
|
authSecretPath = "${lnd.networkDir}/admin.macaroon";
|
||||||
|
}
|
||||||
|
)];
|
||||||
|
|
||||||
|
services.lnd.restAddress = mkIf (!lnd.lndconnect.onion) "0.0.0.0";
|
||||||
|
}
|
||||||
|
|
||||||
|
(mkIf lnd.lndconnect.onion {
|
||||||
|
services.tor = {
|
||||||
|
enable = true;
|
||||||
|
relay.onionServices.lnd-rest = nbLib.mkOnionService {
|
||||||
|
target.addr = nbLib.address lnd.restAddress;
|
||||||
|
target.port = lnd.restPort;
|
||||||
|
port = lnd.restPort;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
nix-bitcoin.onionAddresses.access = {
|
||||||
|
${lnd.user} = [ "lnd-rest" ];
|
||||||
|
${operatorName} = [ "lnd-rest" ];
|
||||||
|
};
|
||||||
|
})
|
||||||
|
]))
|
||||||
|
|
||||||
|
(mkIf (clnrest.enable && clnrest.lnconnect.enable)
|
||||||
|
(mkMerge [
|
||||||
|
{
|
||||||
|
environment.systemPackages = [(
|
||||||
|
mkLndconnect {
|
||||||
|
name = "lnconnect-clnrest";
|
||||||
|
isClnrest = true;
|
||||||
|
enableOnion = clnrest.lnconnect.onion;
|
||||||
|
onionService = "${operatorName}/clnrest";
|
||||||
|
port = clnrest.port;
|
||||||
|
certPath = "${clightning.networkDir}/client.pem";
|
||||||
|
authSecretPath = "${clightning.networkDir}/admin-rune";
|
||||||
|
}
|
||||||
|
)];
|
||||||
|
|
||||||
|
services.clightning.plugins.clnrest.address = mkIf (!clnrest.lnconnect.onion) "0.0.0.0";
|
||||||
|
}
|
||||||
|
|
||||||
|
(mkIf clnrest.lnconnect.onion {
|
||||||
|
services.tor = {
|
||||||
|
enable = true;
|
||||||
|
relay.onionServices.clnrest = nbLib.mkOnionService {
|
||||||
|
target.addr = nbLib.address clnrest.address;
|
||||||
|
target.port = clnrest.port;
|
||||||
|
port = clnrest.port;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
# This also allows nodeinfo to show the clnrest onion address
|
||||||
|
nix-bitcoin.onionAddresses.access.${operatorName} = [ "clnrest" ];
|
||||||
|
})
|
||||||
|
])
|
||||||
|
)
|
||||||
|
|
||||||
|
(mkIf (clightning-rest.enable && clightning-rest.lndconnect.enable)
|
||||||
|
(mkMerge [
|
||||||
|
{
|
||||||
|
environment.systemPackages = [(
|
||||||
|
mkLndconnect {
|
||||||
|
name = "lndconnect-clightning";
|
||||||
|
isClightning = true;
|
||||||
|
enableOnion = clightning-rest.lndconnect.onion;
|
||||||
|
onionService = "${operatorName}/clightning-rest";
|
||||||
|
port = clightning-rest.port;
|
||||||
|
certPath = "${clightning-rest.dataDir}/certs/certificate.pem";
|
||||||
|
authSecretPath = "${clightning-rest.dataDir}/certs/access.macaroon";
|
||||||
|
}
|
||||||
|
)];
|
||||||
|
|
||||||
|
# clightning-rest always binds to all interfaces
|
||||||
|
}
|
||||||
|
|
||||||
|
(mkIf clightning-rest.lndconnect.onion {
|
||||||
|
services.tor = {
|
||||||
|
enable = true;
|
||||||
|
relay.onionServices.clightning-rest = nbLib.mkOnionService {
|
||||||
|
target.addr = nbLib.address clightning-rest.address;
|
||||||
|
target.port = clightning-rest.port;
|
||||||
|
port = clightning-rest.port;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
# This also allows nodeinfo to show the clightning-rest onion address
|
||||||
|
nix-bitcoin.onionAddresses.access.${operatorName} = [ "clightning-rest" ];
|
||||||
|
})
|
||||||
|
])
|
||||||
|
)
|
||||||
|
];
|
||||||
|
}
|
||||||
+349
@@ -0,0 +1,349 @@
|
|||||||
|
{ config, lib, pkgs, ... }:
|
||||||
|
|
||||||
|
with lib;
|
||||||
|
let
|
||||||
|
options.services = {
|
||||||
|
mempool = {
|
||||||
|
enable = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = ''
|
||||||
|
Enable Mempool, a fully featured Bitcoin visualizer, explorer, and API service.
|
||||||
|
|
||||||
|
Note: Mempool enables `txindex` in bitcoind (this is a requirement).
|
||||||
|
|
||||||
|
This module has two components:
|
||||||
|
- A backend service (systemd service `mempool`)
|
||||||
|
|
||||||
|
- An optional web interface run by nginx, defined by options `services.mempool.frontend.*`.
|
||||||
|
The frontend is enabled by default when mempool is enabled.
|
||||||
|
For details, see `services.mempool.frontend.enable`.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
frontend = {
|
||||||
|
enable = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = cfg.enable;
|
||||||
|
description = ''
|
||||||
|
Enable the mempool frontend (web interface).
|
||||||
|
This starts a simple nginx instance, configured for local usage with
|
||||||
|
settings similar to the `mempool/frontend` Docker image.
|
||||||
|
|
||||||
|
IMPORTANT:
|
||||||
|
If you want to expose the mempool frontend to the internet, you
|
||||||
|
should create a custom nginx config that includes TLS, backend caching, rate limiting
|
||||||
|
and performance tuning.
|
||||||
|
For this task, reuse the config snippets from option `services.mempool.frontend.nginxConfig`.
|
||||||
|
See also: https://github.com/fort-nix/nixbitcoin.org/blob/master/website/mempool.nix,
|
||||||
|
which contains a mempool nginx config for public hosting (running at
|
||||||
|
https://mempool.nixbitcoin.org).
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
address = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "127.0.0.1";
|
||||||
|
description = "HTTP server address.";
|
||||||
|
};
|
||||||
|
port = mkOption {
|
||||||
|
type = types.port;
|
||||||
|
default = 60845; # A random private port
|
||||||
|
description = "HTTP server port.";
|
||||||
|
};
|
||||||
|
settings = mkOption {
|
||||||
|
type = with types; attrsOf anything;
|
||||||
|
default = {};
|
||||||
|
example = {
|
||||||
|
TESTNET_ENABLED = true;
|
||||||
|
MEMPOOL_WEBSITE_URL = "mempool.mynode.org";
|
||||||
|
};
|
||||||
|
description = ''
|
||||||
|
Mempool frontend settings.
|
||||||
|
See here for available options:
|
||||||
|
https://github.com/mempool/mempool/blob/master/frontend/src/app/services/state.service.ts
|
||||||
|
(`interface Env` and `defaultEnv`)
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
staticContentRoot = mkOption {
|
||||||
|
type = types.path;
|
||||||
|
default = pkgs.mempool-frontend.withConfig cfg.frontend.settings;
|
||||||
|
defaultText = "pkgs.mempool-frontend";
|
||||||
|
description = "
|
||||||
|
Path of the static frontend content root.
|
||||||
|
";
|
||||||
|
};
|
||||||
|
nginxConfig = mkOption {
|
||||||
|
readOnly = true;
|
||||||
|
default = frontend.nginxConfig;
|
||||||
|
defaultText = "(See source)";
|
||||||
|
description = "
|
||||||
|
An attrset of nginx config snippets for assembling a custom
|
||||||
|
mempool nginx config.
|
||||||
|
For details, see the source comments at the point of definition.
|
||||||
|
";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
address = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "127.0.0.1";
|
||||||
|
description = "Mempool backend address.";
|
||||||
|
};
|
||||||
|
port = mkOption {
|
||||||
|
type = types.port;
|
||||||
|
default = 8999;
|
||||||
|
description = "Mempool backend port.";
|
||||||
|
};
|
||||||
|
electrumServer = mkOption {
|
||||||
|
type = types.enum [ "electrs" "fulcrum" ];
|
||||||
|
default = "electrs";
|
||||||
|
description = ''
|
||||||
|
The Electrum server to use for fetching address information.
|
||||||
|
|
||||||
|
Possible options:
|
||||||
|
- electrs:
|
||||||
|
Small database size, slow when querying new addresses.
|
||||||
|
- fulcrum:
|
||||||
|
Large database size, quickly serves arbitrary address queries.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
settings = mkOption {
|
||||||
|
type = with types; attrsOf (attrsOf anything);
|
||||||
|
example = {
|
||||||
|
MEMPOOL = {
|
||||||
|
POLL_RATE_MS = 3000;
|
||||||
|
STDOUT_LOG_MIN_PRIORITY = "debug";
|
||||||
|
};
|
||||||
|
PRICE_DATA_SERVER = {
|
||||||
|
CLEARNET_URL = "https://myserver.org/prices";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
description = ''
|
||||||
|
Mempool backend settings.
|
||||||
|
See here for available options:
|
||||||
|
https://github.com/mempool/mempool/blob/master/backend/src/config.ts
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
database = {
|
||||||
|
name = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "mempool";
|
||||||
|
description = "Database name.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
package = mkOption {
|
||||||
|
type = types.package;
|
||||||
|
default = pkgs.mempool-backend;
|
||||||
|
defaultText = "pkgs.mempool-backend";
|
||||||
|
description = "The package providing mempool binaries.";
|
||||||
|
};
|
||||||
|
user = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "mempool";
|
||||||
|
description = "The user as which to run Mempool.";
|
||||||
|
};
|
||||||
|
group = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = cfg.user;
|
||||||
|
description = "The group as which to run Mempool.";
|
||||||
|
};
|
||||||
|
tor = nbLib.tor;
|
||||||
|
};
|
||||||
|
|
||||||
|
# Internal read-only options used by `./nodeinfo.nix` and `./onion-services.nix`
|
||||||
|
mempool-frontend = let
|
||||||
|
inherit (nbLib) mkAlias;
|
||||||
|
in {
|
||||||
|
enable = mkAlias cfg.frontend.enable;
|
||||||
|
address = mkAlias cfg.frontend.address;
|
||||||
|
port = mkAlias cfg.frontend.port;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
cfg = config.services.mempool;
|
||||||
|
nbLib = config.nix-bitcoin.lib;
|
||||||
|
nbPkgs = pkgs; # vendored: now alias to pkgs
|
||||||
|
secretsDir = config.nix-bitcoin.secretsDir;
|
||||||
|
|
||||||
|
configFile = builtins.toFile "mempool-config" (builtins.toJSON cfg.settings);
|
||||||
|
cacheDir = "/var/cache/mempool";
|
||||||
|
|
||||||
|
inherit (config.services)
|
||||||
|
bitcoind
|
||||||
|
electrs
|
||||||
|
fulcrum;
|
||||||
|
|
||||||
|
torSocket = config.services.tor.client.socksListenAddress;
|
||||||
|
|
||||||
|
# See the `services.nginx` definition further below below
|
||||||
|
# on how to use these snippets.
|
||||||
|
frontend.nginxConfig = {
|
||||||
|
# This must be added to `services.nginx.commonHttpConfig` when
|
||||||
|
# `mempool/location-static.conf` is used
|
||||||
|
httpConfig = ''
|
||||||
|
include ${if pkgs ? mempool-nginx-conf then "${pkgs.mempool-nginx-conf}/http-language.conf" else "/dev/null"};
|
||||||
|
'';
|
||||||
|
|
||||||
|
# Config for static website content.
|
||||||
|
# This should be added to `services.nginx.virtualHosts.<mempool server name>.extraConfig`.
|
||||||
|
# Adapted from mempool/nginx-mempool.conf and mempool/production/nginx/location-redirects.conf
|
||||||
|
staticContent = ''
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
add_header Cache-Control "public, no-transform";
|
||||||
|
add_header Vary Accept-Language;
|
||||||
|
add_header Vary Cookie;
|
||||||
|
|
||||||
|
include ${if pkgs ? mempool-nginx-conf then "${pkgs.mempool-nginx-conf}/location-static.conf" else "/dev/null"};
|
||||||
|
|
||||||
|
# Redirect /api to /docs/api
|
||||||
|
location = /api {
|
||||||
|
return 308 https://$host/docs/api;
|
||||||
|
}
|
||||||
|
location = /api/ {
|
||||||
|
return 308 https://$host/docs/api;
|
||||||
|
}
|
||||||
|
'';
|
||||||
|
|
||||||
|
# Config for backend API.
|
||||||
|
# This should be added to `services.nginx.virtualHosts.<mempool server name>.extraConfig`.
|
||||||
|
# Adapted from mempool/nginx-mempool.conf and mempool/production/nginx/location-api.conf.
|
||||||
|
proxyApi = let
|
||||||
|
backend = "http://${nbLib.addressWithPort cfg.address cfg.port}";
|
||||||
|
in ''
|
||||||
|
location /api/ {
|
||||||
|
proxy_pass ${backend}/api/v1/;
|
||||||
|
}
|
||||||
|
location /api/v1 {
|
||||||
|
proxy_pass ${backend};
|
||||||
|
}
|
||||||
|
# Websocket API
|
||||||
|
location /api/v1/ws {
|
||||||
|
proxy_pass ${backend};
|
||||||
|
|
||||||
|
# Websocket header settings
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "Upgrade";
|
||||||
|
|
||||||
|
# Relevant settings from `recommendedProxyConfig` (nixos/nginx/default.nix)
|
||||||
|
# (In the above api locations, these are inherited from the parent scope)
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
in {
|
||||||
|
inherit options;
|
||||||
|
|
||||||
|
config = mkIf cfg.enable {
|
||||||
|
services.bitcoind.txindex = true;
|
||||||
|
services.electrs.enable = mkIf (cfg.electrumServer == "electrs" ) true;
|
||||||
|
services.fulcrum.enable = mkIf (cfg.electrumServer == "fulcrum" ) true;
|
||||||
|
services.mysql = {
|
||||||
|
enable = true;
|
||||||
|
package = pkgs.mariadb;
|
||||||
|
ensureDatabases = [ cfg.database.name ];
|
||||||
|
ensureUsers = [
|
||||||
|
{
|
||||||
|
name = cfg.user;
|
||||||
|
ensurePermissions."${cfg.database.name}.*" = "ALL PRIVILEGES";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
# Available options:
|
||||||
|
# https://github.com/mempool/mempool/blob/master/backend/src/config.ts
|
||||||
|
services.mempool.settings = {
|
||||||
|
MEMPOOL = {
|
||||||
|
# mempool doesn't support regtest
|
||||||
|
NETWORK = "mainnet";
|
||||||
|
BACKEND = "electrum";
|
||||||
|
HTTP_PORT = cfg.port;
|
||||||
|
CACHE_DIR = "${cacheDir}/cache";
|
||||||
|
STDOUT_LOG_MIN_PRIORITY = mkDefault "info";
|
||||||
|
AUTOMATIC_POOLS_UPDATE = true;
|
||||||
|
};
|
||||||
|
CORE_RPC = {
|
||||||
|
HOST = bitcoind.rpc.address;
|
||||||
|
PORT = bitcoind.rpc.port;
|
||||||
|
USERNAME = bitcoind.rpc.users.public.name;
|
||||||
|
PASSWORD = "@btcRpcPassword@";
|
||||||
|
};
|
||||||
|
ELECTRUM = let
|
||||||
|
server = config.services.${cfg.electrumServer};
|
||||||
|
in {
|
||||||
|
HOST = server.address;
|
||||||
|
PORT = server.port;
|
||||||
|
TLS_ENABLED = false;
|
||||||
|
};
|
||||||
|
DATABASE = {
|
||||||
|
ENABLED = true;
|
||||||
|
DATABASE = cfg.database.name;
|
||||||
|
SOCKET = "/run/mysqld/mysqld.sock";
|
||||||
|
PID_DIR = cacheDir;
|
||||||
|
};
|
||||||
|
} // optionalAttrs (cfg.tor.proxy) {
|
||||||
|
# Use Tor for rate fetching and pool updating
|
||||||
|
SOCKS5PROXY = {
|
||||||
|
ENABLED = true;
|
||||||
|
USE_ONION = true;
|
||||||
|
HOST = torSocket.addr;
|
||||||
|
PORT = torSocket.port;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.services.mempool = rec {
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
requires = [ "mysql.service" ];
|
||||||
|
wants = [ "${cfg.electrumServer}.service" ];
|
||||||
|
after = requires ++ wants;
|
||||||
|
preStart = ''
|
||||||
|
mkdir -p '${cacheDir}/cache'
|
||||||
|
<${configFile} sed \
|
||||||
|
-e "s|@btcRpcPassword@|$(cat ${secretsDir}/bitcoin-rpcpassword-public)|" \
|
||||||
|
> '${cacheDir}/config.json'
|
||||||
|
'';
|
||||||
|
environment.MEMPOOL_CONFIG_FILE = "${cacheDir}/config.json";
|
||||||
|
serviceConfig = nbLib.defaultHardening // {
|
||||||
|
ExecStart = "${cfg.package}/bin/mempool-backend";
|
||||||
|
CacheDirectory = "mempool";
|
||||||
|
CacheDirectoryMode = "770";
|
||||||
|
# Show "mempool" instead of "node" in the journal
|
||||||
|
SyslogIdentifier = "mempool";
|
||||||
|
User = cfg.user;
|
||||||
|
Restart = "on-failure";
|
||||||
|
RestartSec = "10s";
|
||||||
|
} // nbLib.allowedIPAddresses cfg.tor.enforce
|
||||||
|
// nbLib.nodejs;
|
||||||
|
};
|
||||||
|
|
||||||
|
services.nginx = mkIf cfg.frontend.enable {
|
||||||
|
enable = true;
|
||||||
|
enableReload = true;
|
||||||
|
recommendedBrotliSettings = true;
|
||||||
|
recommendedGzipSettings = true;
|
||||||
|
recommendedOptimisation = true;
|
||||||
|
recommendedProxySettings = true;
|
||||||
|
recommendedTlsSettings = true;
|
||||||
|
commonHttpConfig = frontend.nginxConfig.httpConfig;
|
||||||
|
virtualHosts."mempool" = {
|
||||||
|
serverName = "_";
|
||||||
|
listen = [ { addr = cfg.frontend.address; port = cfg.frontend.port; } ];
|
||||||
|
root = cfg.frontend.staticContentRoot;
|
||||||
|
extraConfig =
|
||||||
|
frontend.nginxConfig.staticContent +
|
||||||
|
frontend.nginxConfig.proxyApi;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
users.users.${cfg.user} = {
|
||||||
|
isSystemUser = true;
|
||||||
|
group = cfg.group;
|
||||||
|
extraGroups = [ "bitcoinrpc-public" ];
|
||||||
|
};
|
||||||
|
users.groups.${cfg.group} = {};
|
||||||
|
};
|
||||||
|
}
|
||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
# Vendored nix-bitcoin — MINIMAL subset used by Sovran_SystemsOS
|
||||||
|
# Original source: https://github.com/fort-nix/nix-bitcoin
|
||||||
|
# Only services actually used by Sovran are kept (6 services vs 20+ upstream)
|
||||||
|
# - backups.nix removed: Sovran uses rsnapshot to Second_Drive (configuration.nix)
|
||||||
|
# - netns-isolation.nix is now a stub (requires false for nwc-wallets)
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
./nix-bitcoin.nix
|
||||||
|
./secrets/secrets.nix
|
||||||
|
./operator.nix
|
||||||
|
./bitcoind.nix
|
||||||
|
./electrs.nix
|
||||||
|
./lnd.nix
|
||||||
|
./lndconnect.nix
|
||||||
|
./rtl.nix
|
||||||
|
./btcpayserver.nix
|
||||||
|
./mempool.nix
|
||||||
|
./security.nix
|
||||||
|
./onion-addresses.nix
|
||||||
|
./onion-services.nix
|
||||||
|
./netns-isolation.nix
|
||||||
|
./nodeinfo.nix
|
||||||
|
./versioning.nix
|
||||||
|
];
|
||||||
|
|
||||||
|
disabledModules = [ "services/networking/bitcoind.nix" ];
|
||||||
|
}
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
{ config, lib, ... }:
|
||||||
|
with lib;
|
||||||
|
{
|
||||||
|
options.nix-bitcoin.netns-isolation = {
|
||||||
|
enable = mkEnableOption "netns isolation (stub — disabled in Sovran_SystemsOS)";
|
||||||
|
};
|
||||||
|
# No config when enabled — isolation is intentionally not implemented.
|
||||||
|
# Sovran requires enable = false (see modules/nwc-wallets.nix assertion).
|
||||||
|
# The original nix-bitcoin implementation (365 lines, bridge nb-br, iptables,
|
||||||
|
# ip netns, 169.254.x.x) broke Caddy/AlbyHub/RTL and is not needed for
|
||||||
|
# desktop/server roles. Keep stub so `nix-bitcoin.netns-isolation.enable`
|
||||||
|
# remains a valid option.
|
||||||
|
config = mkIf config.nix-bitcoin.netns-isolation.enable {
|
||||||
|
warnings = [ "nix-bitcoin.netns-isolation.enable is a stub in vendored Sovran and does nothing. Set it to false." ];
|
||||||
|
};
|
||||||
|
}
|
||||||
+60
@@ -0,0 +1,60 @@
|
|||||||
|
{ config, pkgs, lib, ... }:
|
||||||
|
|
||||||
|
with lib;
|
||||||
|
{
|
||||||
|
options = {
|
||||||
|
nix-bitcoin = {
|
||||||
|
# Kept for compatibility, now simply aliases system pkgs
|
||||||
|
pkgs = mkOption {
|
||||||
|
type = types.attrs;
|
||||||
|
default = pkgs;
|
||||||
|
defaultText = "pkgs";
|
||||||
|
description = "Alias to system pkgs (vendored nix-bitcoin now uses nixpkgs directly).";
|
||||||
|
};
|
||||||
|
|
||||||
|
useVersionLockedPkgs = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = "Deprecated — vendored modules always use system pkgs.";
|
||||||
|
};
|
||||||
|
|
||||||
|
pkgOverlays = mkOption {
|
||||||
|
internal = true;
|
||||||
|
type = with types; functionTo attrs;
|
||||||
|
default = _: _: {};
|
||||||
|
description = "Deprecated stub.";
|
||||||
|
};
|
||||||
|
|
||||||
|
lib = mkOption {
|
||||||
|
readOnly = true;
|
||||||
|
default = import ./lib.nix lib pkgs config;
|
||||||
|
defaultText = "vendor/nix-bitcoin/lib.nix";
|
||||||
|
};
|
||||||
|
|
||||||
|
torClientAddressWithPort = mkOption {
|
||||||
|
readOnly = true;
|
||||||
|
default = with config.services.tor.client.socksListenAddress;
|
||||||
|
"${addr}:${toString port}";
|
||||||
|
defaultText = "(See source)";
|
||||||
|
};
|
||||||
|
|
||||||
|
torify = mkOption {
|
||||||
|
readOnly = true;
|
||||||
|
default = pkgs.writers.writeBashBin "torify" ''
|
||||||
|
${pkgs.tor}/bin/torify \
|
||||||
|
--address ${config.services.tor.client.socksListenAddress.addr} \
|
||||||
|
"$@"
|
||||||
|
'';
|
||||||
|
defaultText = "(See source)";
|
||||||
|
};
|
||||||
|
|
||||||
|
runAsUserCmd = mkOption {
|
||||||
|
readOnly = true;
|
||||||
|
default = if config.security.doas.enable
|
||||||
|
then "doas -u"
|
||||||
|
else "sudo -u";
|
||||||
|
defaultText = "(See source)";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
+167
@@ -0,0 +1,167 @@
|
|||||||
|
{ config, lib, pkgs, ... }:
|
||||||
|
|
||||||
|
with lib;
|
||||||
|
let
|
||||||
|
options = {
|
||||||
|
nix-bitcoin.nodeinfo = {
|
||||||
|
enable = mkEnableOption "nodeinfo";
|
||||||
|
|
||||||
|
program = mkOption {
|
||||||
|
readOnly = true;
|
||||||
|
default = script;
|
||||||
|
defaultText = "(See source)";
|
||||||
|
};
|
||||||
|
|
||||||
|
services = mkOption {
|
||||||
|
internal = true;
|
||||||
|
type = types.attrs;
|
||||||
|
default = {};
|
||||||
|
defaultText = "(See source)";
|
||||||
|
description = ''
|
||||||
|
Nodeinfo service definitions.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
lib = mkOption {
|
||||||
|
internal = true;
|
||||||
|
readOnly = true;
|
||||||
|
default = nodeinfoLib;
|
||||||
|
defaultText = "(See source)";
|
||||||
|
description = ''
|
||||||
|
Helper functions for defining nodeinfo services.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
cfg = config.nix-bitcoin.nodeinfo;
|
||||||
|
nbLib = config.nix-bitcoin.lib;
|
||||||
|
|
||||||
|
script = pkgs.writeScriptBin "nodeinfo" ''
|
||||||
|
#!${pkgs.python3}/bin/python
|
||||||
|
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
def success(*args):
|
||||||
|
return subprocess.call(args, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) == 0
|
||||||
|
|
||||||
|
def is_active(unit):
|
||||||
|
return success("systemctl", "is-active", "--quiet", unit)
|
||||||
|
|
||||||
|
def is_enabled(unit):
|
||||||
|
return success("systemctl", "is-enabled", "--quiet", unit)
|
||||||
|
|
||||||
|
def cmd(*args):
|
||||||
|
return subprocess.run(args, stdout=subprocess.PIPE).stdout.decode('utf-8')
|
||||||
|
|
||||||
|
def shell(*args):
|
||||||
|
return cmd("bash", "-c", *args).strip()
|
||||||
|
|
||||||
|
infos = OrderedDict()
|
||||||
|
operator = "${config.nix-bitcoin.operator.name}"
|
||||||
|
|
||||||
|
def get_onion_address(name, port):
|
||||||
|
path = f"/var/lib/onion-addresses/{operator}/{name}"
|
||||||
|
try:
|
||||||
|
with open(path, "r") as f:
|
||||||
|
onion_address = f.read().strip()
|
||||||
|
except OSError:
|
||||||
|
print(f"error reading file {path}", file=sys.stderr)
|
||||||
|
return
|
||||||
|
return f"{onion_address}:{port}"
|
||||||
|
|
||||||
|
def add_service(service, make_info, systemd_service = None):
|
||||||
|
systemd_service = systemd_service or service
|
||||||
|
if not is_active(systemd_service):
|
||||||
|
infos[service] = f"'{systemd_service}.service' is not running"
|
||||||
|
else:
|
||||||
|
info = OrderedDict()
|
||||||
|
exec(make_info, globals(), locals())
|
||||||
|
infos[service] = info
|
||||||
|
|
||||||
|
if is_enabled("onion-adresses") and not is_active("onion-adresses"):
|
||||||
|
print("error: service 'onion-adresses' is not running")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
${concatStrings infos}
|
||||||
|
|
||||||
|
print(json.dumps(infos, indent=2))
|
||||||
|
'';
|
||||||
|
|
||||||
|
infos = map (serviceName:
|
||||||
|
let serviceCfg = config.services.${serviceName};
|
||||||
|
in optionalString serviceCfg.enable (cfg.services.${serviceName} serviceName serviceCfg)
|
||||||
|
) (builtins.attrNames cfg.services);
|
||||||
|
|
||||||
|
nodeinfoLib = rec {
|
||||||
|
mkInfo = extraCode: name: cfg:
|
||||||
|
mkInfoLong {
|
||||||
|
inherit extraCode name cfg;
|
||||||
|
};
|
||||||
|
|
||||||
|
mkInfoLong = { extraCode ? "", name, cfg, systemdServiceName ? name }: ''
|
||||||
|
add_service("${name}", """
|
||||||
|
info["local_address"] = "${nbLib.addressWithPort cfg.address cfg.port}"
|
||||||
|
'' + mkIfOnionPort name (onionPort: ''
|
||||||
|
info["onion_address"] = get_onion_address("${name}", ${onionPort})
|
||||||
|
'') + extraCode + ''
|
||||||
|
|
||||||
|
""", "${systemdServiceName}")
|
||||||
|
'';
|
||||||
|
|
||||||
|
mkIfOnionPort = name: fn:
|
||||||
|
if onionServices ? ${name} then
|
||||||
|
fn (toString (builtins.elemAt onionServices.${name}.map 0).port)
|
||||||
|
else
|
||||||
|
"";
|
||||||
|
};
|
||||||
|
|
||||||
|
inherit (config.services.tor.relay) onionServices;
|
||||||
|
in {
|
||||||
|
inherit options;
|
||||||
|
|
||||||
|
config = mkIf cfg.enable {
|
||||||
|
environment.systemPackages = [ script ];
|
||||||
|
|
||||||
|
nix-bitcoin.operator.enable = true;
|
||||||
|
|
||||||
|
nix-bitcoin.nodeinfo.services = with nodeinfoLib; {
|
||||||
|
bitcoind = mkInfo "";
|
||||||
|
clightning = mkInfo ''
|
||||||
|
info["nodeid"] = shell("lightning-cli getinfo | jq -r '.id'")
|
||||||
|
if 'onion_address' in info:
|
||||||
|
info["id"] = f"{info['nodeid']}@{info['onion_address']}"
|
||||||
|
'';
|
||||||
|
lnd = name: cfg: mkInfo (''
|
||||||
|
info["rest_address"] = "${nbLib.addressWithPort cfg.restAddress cfg.restPort}"
|
||||||
|
'' + mkIfOnionPort "lnd-rest" (onionPort: ''
|
||||||
|
info["onion_rest_address"] = get_onion_address("lnd-rest", ${onionPort})
|
||||||
|
'') + ''
|
||||||
|
info["nodeid"] = shell("lncli getinfo | jq -r '.identity_pubkey'")
|
||||||
|
'') name cfg;
|
||||||
|
clnrest = name: cfg: mkInfoLong {
|
||||||
|
inherit name cfg;
|
||||||
|
systemdServiceName = "clightning";
|
||||||
|
};
|
||||||
|
clightning-rest = mkInfo "";
|
||||||
|
electrs = mkInfo "";
|
||||||
|
fulcrum = mkInfo "";
|
||||||
|
btcpayserver = mkInfo "";
|
||||||
|
liquidd = mkInfo "";
|
||||||
|
joinmarket-ob-watcher = mkInfo "";
|
||||||
|
rtl = mkInfo "";
|
||||||
|
mempool = mkInfo "";
|
||||||
|
mempool-frontend = name: cfg: mkInfoLong {
|
||||||
|
inherit name cfg;
|
||||||
|
systemdServiceName = "nginx";
|
||||||
|
};
|
||||||
|
# Only add sshd when it has an onion service
|
||||||
|
sshd = name: cfg: mkIfOnionPort "sshd" (onionPort: ''
|
||||||
|
add_service("sshd", """info["onion_address"] = get_onion_address("sshd", ${onionPort})""")
|
||||||
|
'');
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
+106
@@ -0,0 +1,106 @@
|
|||||||
|
# This module enables unprivileged users to read onion addresses.
|
||||||
|
# By default, onion addresses in /var/lib/tor/onion are only readable by the
|
||||||
|
# tor user.
|
||||||
|
# The included service copies onion addresses to /var/lib/onion-addresses/<user>/
|
||||||
|
# and sets permissions according to option 'access'.
|
||||||
|
|
||||||
|
{ config, lib, ... }:
|
||||||
|
|
||||||
|
with lib;
|
||||||
|
let
|
||||||
|
options.nix-bitcoin.onionAddresses = {
|
||||||
|
access = mkOption {
|
||||||
|
type = with types; attrsOf (listOf str);
|
||||||
|
default = {};
|
||||||
|
description = ''
|
||||||
|
This option controls who is allowed to access onion addresses.
|
||||||
|
For example, the following allows user 'myuser' to access bitcoind
|
||||||
|
and clightning onion addresses:
|
||||||
|
```nix
|
||||||
|
{
|
||||||
|
"myuser" = [ "bitcoind" "clightning" ];
|
||||||
|
};
|
||||||
|
```
|
||||||
|
The onion hostnames can then be read from
|
||||||
|
{file}`/var/lib/onion-addresses/myuser`.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
services = mkOption {
|
||||||
|
type = with types; listOf str;
|
||||||
|
default = [];
|
||||||
|
description = ''
|
||||||
|
Services that can access their onion address via file
|
||||||
|
{file}`/var/lib/onion-addresses/<service>`
|
||||||
|
The file is readable only by the service user.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
dataDir = mkOption {
|
||||||
|
readOnly = true;
|
||||||
|
default = "/var/lib/onion-addresses";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
cfg = config.nix-bitcoin.onionAddresses;
|
||||||
|
nbLib = config.nix-bitcoin.lib;
|
||||||
|
in {
|
||||||
|
inherit options;
|
||||||
|
|
||||||
|
config = mkIf (cfg.access != {} || cfg.services != []) {
|
||||||
|
systemd.services.onion-addresses = {
|
||||||
|
wantedBy = [ "tor.service" ];
|
||||||
|
bindsTo = [ "tor.service" ];
|
||||||
|
after = [ "tor.service" ];
|
||||||
|
serviceConfig = nbLib.defaultHardening // {
|
||||||
|
Type = "oneshot";
|
||||||
|
RemainAfterExit = true;
|
||||||
|
StateDirectory = "onion-addresses";
|
||||||
|
StateDirectoryMode = "771";
|
||||||
|
PrivateNetwork = true; # This service needs no network access
|
||||||
|
PrivateUsers = false;
|
||||||
|
CapabilityBoundingSet = "CAP_CHOWN CAP_FSETID CAP_SETFCAP CAP_DAC_OVERRIDE CAP_DAC_READ_SEARCH CAP_FOWNER CAP_IPC_OWNER";
|
||||||
|
};
|
||||||
|
script = ''
|
||||||
|
waitForFile() {
|
||||||
|
file=$1
|
||||||
|
for ((i=0; i<300; i++)); do
|
||||||
|
if [[ -e $file ]]; then
|
||||||
|
return;
|
||||||
|
fi
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
|
echo "Error: File $file did not appear after 30 sec."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Wait until tor is up
|
||||||
|
waitForFile /var/lib/tor/state
|
||||||
|
|
||||||
|
cd ${cfg.dataDir}
|
||||||
|
rm -rf ./*
|
||||||
|
|
||||||
|
${concatMapStrings
|
||||||
|
(user: ''
|
||||||
|
mkdir -p -m 0700 ${user}
|
||||||
|
chown ${user} ${user}
|
||||||
|
${concatMapStrings
|
||||||
|
(service: ''
|
||||||
|
onionFile='/var/lib/tor/onion/${service}/hostname'
|
||||||
|
waitForFile "$onionFile"
|
||||||
|
cp "$onionFile" '${user}/${service}'
|
||||||
|
chown '${user}' '${user}/${service}'
|
||||||
|
'')
|
||||||
|
cfg.access.${user}
|
||||||
|
}
|
||||||
|
'')
|
||||||
|
(builtins.attrNames cfg.access)
|
||||||
|
}
|
||||||
|
|
||||||
|
${concatMapStrings (service: ''
|
||||||
|
onionFile=/var/lib/tor/onion/${service}/hostname
|
||||||
|
waitForFile "$onionFile"
|
||||||
|
install -D -o ${config.systemd.services.${service}.serviceConfig.User} -m 400 "$onionFile" services/${service}
|
||||||
|
'') cfg.services}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
+122
@@ -0,0 +1,122 @@
|
|||||||
|
# This module creates onion-services for NixOS services.
|
||||||
|
# An onion service can be enabled for every service that defines
|
||||||
|
# options 'address', 'port' and optionally 'getPublicAddressCmd'.
|
||||||
|
#
|
||||||
|
# See it in use at ./presets/enable-tor.nix
|
||||||
|
|
||||||
|
{ config, lib, pkgs, ... }:
|
||||||
|
|
||||||
|
with lib;
|
||||||
|
let
|
||||||
|
options.nix-bitcoin.onionServices = mkOption {
|
||||||
|
default = {};
|
||||||
|
type = with types; attrsOf (submodule (
|
||||||
|
{ config, ... }: {
|
||||||
|
options = {
|
||||||
|
enable = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = config.public;
|
||||||
|
description = ''
|
||||||
|
Create an onion service for the given service.
|
||||||
|
The service must define options {option}`address` and {option}`onionPort` (or `port`).
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
public = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = ''
|
||||||
|
Make the onion address accessible to the service.
|
||||||
|
If enabled, the onion service is automatically enabled.
|
||||||
|
Only available for services that define option {option}`getPublicAddressCmd`.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
externalPort = mkOption {
|
||||||
|
type = types.nullOr types.port;
|
||||||
|
default = null;
|
||||||
|
description = "Override the external port of the onion service.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
cfg = config.nix-bitcoin.onionServices;
|
||||||
|
nbLib = config.nix-bitcoin.lib;
|
||||||
|
|
||||||
|
onionServices = builtins.attrNames cfg;
|
||||||
|
|
||||||
|
activeServices = builtins.filter (service:
|
||||||
|
config.services.${service}.enable && cfg.${service}.enable
|
||||||
|
) onionServices;
|
||||||
|
|
||||||
|
publicServices = builtins.filter (service: cfg.${service}.public) activeServices;
|
||||||
|
in {
|
||||||
|
inherit options;
|
||||||
|
|
||||||
|
config = mkMerge [
|
||||||
|
(mkIf (activeServices != []) {
|
||||||
|
# Define hidden services
|
||||||
|
services.tor = {
|
||||||
|
enable = true;
|
||||||
|
relay.onionServices = genAttrs activeServices (name:
|
||||||
|
let
|
||||||
|
service = config.services.${name};
|
||||||
|
inherit (cfg.${name}) externalPort;
|
||||||
|
in nbLib.mkOnionService {
|
||||||
|
port = if externalPort != null then externalPort else service.port;
|
||||||
|
target.port = service.onionPort or service.port;
|
||||||
|
target.addr = nbLib.address service.address;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
nix-bitcoin.onionAddresses = {
|
||||||
|
# Enable public services to access their own onion addresses
|
||||||
|
services = publicServices;
|
||||||
|
|
||||||
|
# Allow the operator user to access onion addresses for all active services
|
||||||
|
access.${config.nix-bitcoin.operator.name} = mkIf config.nix-bitcoin.operator.enable activeServices;
|
||||||
|
};
|
||||||
|
systemd.services = let
|
||||||
|
onionAddresses = [ "onion-addresses.service" ];
|
||||||
|
in genAttrs publicServices (service: {
|
||||||
|
# TODO-EXTERNAL: Instead of `wants`, use a future systemd dependency type
|
||||||
|
# that propagates initial start failures but no restarts
|
||||||
|
wants = onionAddresses;
|
||||||
|
after = onionAddresses;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
|
# Set getPublicAddressCmd for public services
|
||||||
|
{
|
||||||
|
services = let
|
||||||
|
# publicServices' doesn't depend on config.services.*.enable,
|
||||||
|
# so we can use it to define config.services without causing infinite recursion
|
||||||
|
publicServices' = builtins.filter (service:
|
||||||
|
let srv = cfg.${service};
|
||||||
|
in srv.public && srv.enable
|
||||||
|
) onionServices;
|
||||||
|
in genAttrs publicServices' (service: {
|
||||||
|
getPublicAddressCmd = "cat ${config.nix-bitcoin.onionAddresses.dataDir}/services/${service}";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
# Set sensible defaults for some services
|
||||||
|
{
|
||||||
|
nix-bitcoin.onionServices = {
|
||||||
|
btcpayserver = {
|
||||||
|
externalPort = 80;
|
||||||
|
};
|
||||||
|
joinmarket-ob-watcher = {
|
||||||
|
externalPort = 80;
|
||||||
|
};
|
||||||
|
rtl = {
|
||||||
|
externalPort = 80;
|
||||||
|
};
|
||||||
|
mempool-frontend = {
|
||||||
|
externalPort = 80;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
+58
@@ -0,0 +1,58 @@
|
|||||||
|
{ config, lib, pkgs, ... }:
|
||||||
|
|
||||||
|
with lib;
|
||||||
|
let
|
||||||
|
options.nix-bitcoin.operator = {
|
||||||
|
enable = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = ''
|
||||||
|
Whether to define a user named `operator` for convenient interactive access
|
||||||
|
to nix-bitcoin features (like `bitcoin-cli`).
|
||||||
|
|
||||||
|
When using nix-bitcoin as part of a larger system config, it makes sense
|
||||||
|
to set your main system user as the operator, by setting option
|
||||||
|
`nix-bitcoin.operator.name = "MAIN_USER_NAME";`.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
name = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "operator";
|
||||||
|
description = "Name of the operator user.";
|
||||||
|
};
|
||||||
|
groups = mkOption {
|
||||||
|
type = with types; listOf str;
|
||||||
|
default = [];
|
||||||
|
description = "Extra groups of the operatur user.";
|
||||||
|
};
|
||||||
|
allowRunAsUsers = mkOption {
|
||||||
|
type = with types; listOf str;
|
||||||
|
default = [];
|
||||||
|
description = "Users as which the operator is allowed to run commands.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
cfg = config.nix-bitcoin.operator;
|
||||||
|
in {
|
||||||
|
inherit options;
|
||||||
|
|
||||||
|
config = mkIf cfg.enable {
|
||||||
|
users.users.${cfg.name} = {
|
||||||
|
isNormalUser = true;
|
||||||
|
extraGroups = [
|
||||||
|
"systemd-journal"
|
||||||
|
"proc" # Enable full /proc access and systemd-status
|
||||||
|
] ++ cfg.groups;
|
||||||
|
};
|
||||||
|
|
||||||
|
security = mkIf (cfg.allowRunAsUsers != []) {
|
||||||
|
# Use doas instead of sudo if enabled
|
||||||
|
doas.extraConfig = mkIf config.security.doas.enable ''
|
||||||
|
${lib.concatMapStrings (user: "permit nopass ${cfg.name} as ${user}\n") cfg.allowRunAsUsers}
|
||||||
|
'';
|
||||||
|
sudo.extraConfig = mkIf (!config.security.doas.enable) ''
|
||||||
|
${cfg.name} ALL=(${builtins.concatStringsSep "," cfg.allowRunAsUsers}) NOPASSWD: ALL
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
Vendored
+239
@@ -0,0 +1,239 @@
|
|||||||
|
{ config, lib, pkgs, ... }:
|
||||||
|
|
||||||
|
with lib;
|
||||||
|
let
|
||||||
|
options.services.rtl = {
|
||||||
|
enable = mkEnableOption "Ride The Lightning, a web interface for lnd and clightning";
|
||||||
|
address = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "127.0.0.1";
|
||||||
|
description = "HTTP server address.";
|
||||||
|
};
|
||||||
|
port = mkOption {
|
||||||
|
type = types.port;
|
||||||
|
default = 3000;
|
||||||
|
description = "HTTP server port.";
|
||||||
|
};
|
||||||
|
dataDir = mkOption {
|
||||||
|
type = types.path;
|
||||||
|
default = "/var/lib/rtl";
|
||||||
|
description = "The data directory for RTL.";
|
||||||
|
};
|
||||||
|
nodes = {
|
||||||
|
clightning = {
|
||||||
|
enable = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = "Enable the clightning node interface.";
|
||||||
|
};
|
||||||
|
extraConfig = mkOption {
|
||||||
|
type = with types; attrsOf anything;
|
||||||
|
default = {};
|
||||||
|
example = {
|
||||||
|
Settings.userPersona = "MERCHANT";
|
||||||
|
Settings.logLevel = "DEBUG";
|
||||||
|
};
|
||||||
|
description = ''
|
||||||
|
Extra clightning node configuration.
|
||||||
|
See here for all available options:
|
||||||
|
https://github.com/Ride-The-Lightning/RTL/blob/master/.github/docs/Application_configurations.md
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
lnd = {
|
||||||
|
enable = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = "Enable the lnd node interface.";
|
||||||
|
};
|
||||||
|
loop = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = "Enable swaps with lightning-loop.";
|
||||||
|
};
|
||||||
|
extraConfig = mkOption {
|
||||||
|
type = with types; attrsOf anything;
|
||||||
|
default = {};
|
||||||
|
example = {
|
||||||
|
Settings.userPersona = "MERCHANT";
|
||||||
|
Settings.logLevel = "DEBUG";
|
||||||
|
};
|
||||||
|
description = ''
|
||||||
|
Extra lnd node configuration.
|
||||||
|
See here for all available options:
|
||||||
|
https://github.com/Ride-The-Lightning/RTL/blob/master/.github/docs/Application_configurations.md
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
reverseOrder = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = ''
|
||||||
|
Reverse the order of nodes shown in the UI.
|
||||||
|
By default, clightning is shown before lnd.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
nightTheme = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = "Enable the Night UI Theme.";
|
||||||
|
};
|
||||||
|
extraCurrency = mkOption {
|
||||||
|
type = with types; nullOr str;
|
||||||
|
default = null;
|
||||||
|
example = "USD";
|
||||||
|
description = ''
|
||||||
|
Currency code (ISO 4217) of the extra currency used for displaying balances.
|
||||||
|
When set, this option enables online currency rate fetching.
|
||||||
|
Warning: Rate fetching requires outgoing clearnet connections, so option
|
||||||
|
{option}`tor.enforce` is automatically disabled.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
user = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "rtl";
|
||||||
|
description = "The user as which to run RTL.";
|
||||||
|
};
|
||||||
|
group = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = cfg.user;
|
||||||
|
description = "The group as which to run RTL.";
|
||||||
|
};
|
||||||
|
tor.enforce = nbLib.tor.enforce;
|
||||||
|
};
|
||||||
|
|
||||||
|
cfg = config.services.rtl;
|
||||||
|
nbLib = config.nix-bitcoin.lib;
|
||||||
|
nbPkgs = pkgs;
|
||||||
|
secretsDir = config.nix-bitcoin.secretsDir;
|
||||||
|
runePath = "${cfg.dataDir}/clightning-admin-rune";
|
||||||
|
|
||||||
|
inherit (nbLib) optionalAttr;
|
||||||
|
|
||||||
|
node = { isLnd, index }: {
|
||||||
|
inherit index;
|
||||||
|
lnNode = "Node";
|
||||||
|
lnImplementation = if isLnd then "LND" else "CLT";
|
||||||
|
Authentication = {
|
||||||
|
${optionalAttr (isLnd && lndLoopEnabled) "swapMacaroonPath"} = "${lightning-loop.dataDir}/${bitcoind.network}";
|
||||||
|
${optionalAttr (isLnd) "macaroonPath"} = "${cfg.dataDir}/macaroons";
|
||||||
|
${optionalAttr (!isLnd) "runePath"} = runePath;
|
||||||
|
};
|
||||||
|
Settings = {
|
||||||
|
userPersona = "OPERATOR";
|
||||||
|
themeMode = if cfg.nightTheme then "NIGHT" else "DAY";
|
||||||
|
themeColor = "PURPLE";
|
||||||
|
${optionalAttr isLnd "channelBackupPath"} = "${cfg.dataDir}/backup/lnd";
|
||||||
|
logLevel = "INFO";
|
||||||
|
fiatConversion = cfg.extraCurrency != null;
|
||||||
|
${optionalAttr (cfg.extraCurrency != null) "currencyUnit"} = cfg.extraCurrency;
|
||||||
|
${optionalAttr (isLnd && lndLoopEnabled) "swapServerUrl"} =
|
||||||
|
"https://${nbLib.addressWithPort lightning-loop.restAddress lightning-loop.restPort}";
|
||||||
|
lnServerUrl = "https://${
|
||||||
|
if isLnd
|
||||||
|
then nbLib.addressWithPort lnd.restAddress lnd.restPort
|
||||||
|
else nbLib.addressWithPort clightning.plugins.clnrest.address clightning.plugins.clnrest.port
|
||||||
|
}";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
nodes' =
|
||||||
|
optional cfg.nodes.clightning.enable
|
||||||
|
(recursiveUpdate (node { isLnd = false; index = 1; }) cfg.nodes.clightning.extraConfig) ++
|
||||||
|
optional cfg.nodes.lnd.enable
|
||||||
|
(recursiveUpdate (node { isLnd = true; index = 2; }) cfg.nodes.lnd.extraConfig);
|
||||||
|
|
||||||
|
nodes = if cfg.nodes.reverseOrder then reverseList nodes' else nodes';
|
||||||
|
|
||||||
|
rtlConfig = {
|
||||||
|
multiPass = "@multiPass@";
|
||||||
|
host = cfg.address;
|
||||||
|
port = cfg.port;
|
||||||
|
SSO.rtlSSO = 0;
|
||||||
|
inherit nodes;
|
||||||
|
};
|
||||||
|
|
||||||
|
configFile = builtins.toFile "config" (builtins.toJSON rtlConfig);
|
||||||
|
|
||||||
|
inherit (config.services)
|
||||||
|
bitcoind
|
||||||
|
lnd
|
||||||
|
clightning
|
||||||
|
lightning-loop;
|
||||||
|
|
||||||
|
lndLoopEnabled = cfg.nodes.lnd.enable && cfg.nodes.lnd.loop;
|
||||||
|
in {
|
||||||
|
inherit options;
|
||||||
|
|
||||||
|
config = mkIf cfg.enable {
|
||||||
|
assertions = [
|
||||||
|
{ assertion = cfg.nodes.clightning.enable || cfg.nodes.lnd.enable;
|
||||||
|
message = ''
|
||||||
|
RTL: At least one of `nodes.lnd.enable` or `nodes.clightning.enable` must be `true`.
|
||||||
|
'';
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
services.lnd.enable = mkIf cfg.nodes.lnd.enable true;
|
||||||
|
services.lightning-loop.enable = mkIf lndLoopEnabled true;
|
||||||
|
services.clightning = mkIf cfg.nodes.clightning.enable {
|
||||||
|
enable = true;
|
||||||
|
plugins.clnrest.enable = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.tmpfiles.rules = [
|
||||||
|
"d '${cfg.dataDir}' 0770 ${cfg.user} ${cfg.group} - -"
|
||||||
|
];
|
||||||
|
|
||||||
|
services.rtl.tor.enforce = mkIf (cfg.extraCurrency != null) false;
|
||||||
|
|
||||||
|
systemd.services.rtl = rec {
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
wants = optional cfg.nodes.clightning.enable "clightning.service" ++
|
||||||
|
optional cfg.nodes.lnd.enable "lnd.service";
|
||||||
|
after = wants ++ [ "nix-bitcoin-secrets.target" ];
|
||||||
|
environment.RTL_CONFIG_PATH = cfg.dataDir;
|
||||||
|
environment.DB_DIRECTORY_PATH = cfg.dataDir;
|
||||||
|
serviceConfig = nbLib.defaultHardening // {
|
||||||
|
ExecStartPre = [
|
||||||
|
(nbLib.script "rtl-setup-config" ''
|
||||||
|
<${configFile} sed "s|@multiPass@|$(cat ${secretsDir}/rtl-password)|" \
|
||||||
|
> '${cfg.dataDir}/RTL-Config.json'
|
||||||
|
'')
|
||||||
|
]
|
||||||
|
++ optional cfg.nodes.lnd.enable
|
||||||
|
# The lnd admin macaroon is not readable by group `lnd`, so copy it
|
||||||
|
(nbLib.rootScript "rtl-copy-macaroon" ''
|
||||||
|
install --compare -m 640 -o ${cfg.user} -g ${cfg.group} -D ${lnd.networkDir}/admin.macaroon \
|
||||||
|
'${cfg.dataDir}/macaroons/admin.macaroon'
|
||||||
|
'')
|
||||||
|
++ optional cfg.nodes.clightning.enable
|
||||||
|
(nbLib.rootScript "rtl-create-clnrest-rune-file" ''
|
||||||
|
rune=$(cat '${clightning.networkDir}/admin-rune')
|
||||||
|
install --compare -m 640 -o ${cfg.user} -g ${cfg.group} <(printf 'LIGHTNING_RUNE="%s"\n' "$rune") '${runePath}'
|
||||||
|
'');
|
||||||
|
ExecStart = "${pkgs.rtl}/bin/rtl";
|
||||||
|
# Show "rtl" instead of "node" in the journal
|
||||||
|
SyslogIdentifier = "rtl";
|
||||||
|
User = cfg.user;
|
||||||
|
Restart = "on-failure";
|
||||||
|
RestartSec = "10s";
|
||||||
|
ReadWritePaths = [ cfg.dataDir ];
|
||||||
|
} // nbLib.allowedIPAddresses cfg.tor.enforce
|
||||||
|
// nbLib.nodejs;
|
||||||
|
};
|
||||||
|
|
||||||
|
users.users.${cfg.user} = {
|
||||||
|
isSystemUser = true;
|
||||||
|
group = cfg.group;
|
||||||
|
extraGroups = optional lndLoopEnabled lnd.group;
|
||||||
|
};
|
||||||
|
users.groups.${cfg.group} = {};
|
||||||
|
|
||||||
|
nix-bitcoin.secrets.rtl-password.user = cfg.user;
|
||||||
|
nix-bitcoin.generateSecretsCmds.rtl = ''
|
||||||
|
makePasswordSecret rtl-password
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}
|
||||||
+235
@@ -0,0 +1,235 @@
|
|||||||
|
{ config, pkgs, lib, ... }:
|
||||||
|
|
||||||
|
with lib;
|
||||||
|
let
|
||||||
|
options.nix-bitcoin = {
|
||||||
|
secretsDir = mkOption {
|
||||||
|
type = types.path;
|
||||||
|
default = "/etc/nix-bitcoin-secrets";
|
||||||
|
description = "Directory to store secrets";
|
||||||
|
};
|
||||||
|
|
||||||
|
setupSecrets = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = ''
|
||||||
|
Set permissions for existing secrets in {option}`nix-bitcoin.secretsDir`
|
||||||
|
before services are started.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
generateSecrets = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = ''
|
||||||
|
Automatically generate all required secrets before services are started.
|
||||||
|
Note: Make sure to create a backup of the generated secrets.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
generateSecretsCmds = mkOption {
|
||||||
|
type = types.attrsOf types.lines;
|
||||||
|
default = {};
|
||||||
|
description = ''
|
||||||
|
Bash expressions for generating secrets.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
# Currently, this is used only by ../deployment/nixops.nix
|
||||||
|
deployment.secretsDir = mkOption {
|
||||||
|
type = types.path;
|
||||||
|
description = ''
|
||||||
|
Directory of local secrets that are transferred to the nix-bitcoin node on deployment
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
secrets = mkOption {
|
||||||
|
default = {};
|
||||||
|
type = with types; attrsOf (submodule (
|
||||||
|
{ config, ... }: {
|
||||||
|
options = {
|
||||||
|
user = mkOption {
|
||||||
|
type = str;
|
||||||
|
default = "root";
|
||||||
|
};
|
||||||
|
group = mkOption {
|
||||||
|
type = str;
|
||||||
|
default = config.user;
|
||||||
|
};
|
||||||
|
permissions = mkOption {
|
||||||
|
type = str;
|
||||||
|
default = "440";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
secretsSetupMethod = mkOption {
|
||||||
|
type = with types; nullOr str;
|
||||||
|
default = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
generateSecretsScript = mkOption {
|
||||||
|
internal = true;
|
||||||
|
default = let
|
||||||
|
rpcauthSrc = pkgs.fetchurl {
|
||||||
|
url = "https://raw.githubusercontent.com/bitcoin/bitcoin/d6cde007db9d3e6ee93bd98a9bbfdce9bfa9b15b/share/rpcauth/rpcauth.py";
|
||||||
|
sha256 = "189mpplam6yzizssrgiyv70c9899ggh8cac76j4n7v0xqzfip07n";
|
||||||
|
};
|
||||||
|
rpcauth = pkgs.writers.writeBash "rpcauth" ''
|
||||||
|
exec ${pkgs.python3}/bin/python ${rpcauthSrc} "$@"
|
||||||
|
'';
|
||||||
|
# Writes secrets to PWD
|
||||||
|
in pkgs.writers.writeBash "generate-secrets" ''
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
export PATH=${lib.makeBinPath (with pkgs; [ coreutils gnugrep ])}
|
||||||
|
|
||||||
|
makePasswordSecret() {
|
||||||
|
# Passwords have alphabet {a-z, A-Z, 0-9} and ~119 bits of entropy
|
||||||
|
[[ -e $1 ]] || ${pkgs.pwgen}/bin/pwgen -s 20 1 > "$1"
|
||||||
|
}
|
||||||
|
makeBitcoinRPCPassword() {
|
||||||
|
user=$1
|
||||||
|
file=bitcoin-rpcpassword-$user
|
||||||
|
HMACfile=bitcoin-HMAC-$user
|
||||||
|
makePasswordSecret "$file"
|
||||||
|
if [[ $file -nt $HMACfile ]]; then
|
||||||
|
${rpcauth} $user $(cat "$file") | grep rpcauth | cut -d ':' -f 2 > "$HMACfile"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
makeCert() {
|
||||||
|
name=$1
|
||||||
|
# Add leading comma if not empty
|
||||||
|
extraAltNames=''${2:+,}''${2:-}
|
||||||
|
if [[ ! -e $name-key ]]; then
|
||||||
|
# Create new key and cert
|
||||||
|
doMakeCert "-newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -nodes -keyout $name-key"
|
||||||
|
elif [[ ! -e $name-cert \
|
||||||
|
|| $(cat "$name-cert-alt-names" 2>/dev/null) != $extraAltNames ]]; then
|
||||||
|
# Create cert from existing key
|
||||||
|
doMakeCert "-key $name-key"
|
||||||
|
fi;
|
||||||
|
}
|
||||||
|
doMakeCert() {
|
||||||
|
# This fn uses global variables `name` and `extraAltNames`
|
||||||
|
keyOpts=$1
|
||||||
|
${pkgs.openssl}/bin/openssl req -x509 \
|
||||||
|
-sha256 -days 3650 $keyOpts -out "$name-cert" \
|
||||||
|
-subj "/CN=localhost/O=$name" \
|
||||||
|
-addext "subjectAltName=DNS:localhost,IP:127.0.0.1$extraAltNames"
|
||||||
|
echo "$extraAltNames" > "$name-cert-alt-names"
|
||||||
|
}
|
||||||
|
|
||||||
|
umask u=rw,go=
|
||||||
|
${builtins.concatStringsSep "\n" (builtins.attrValues cfg.generateSecretsCmds)}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
cfg = config.nix-bitcoin;
|
||||||
|
in {
|
||||||
|
inherit options;
|
||||||
|
|
||||||
|
config = {
|
||||||
|
assertions = [
|
||||||
|
{ assertion = cfg.secretsSetupMethod != null;
|
||||||
|
message = ''
|
||||||
|
No secrets setup method has been defined.
|
||||||
|
To fix this, choose one of the following:
|
||||||
|
|
||||||
|
- Use one of the deployment methods in ${toString ./../deployment}
|
||||||
|
|
||||||
|
- Set `nix-bitcoin.generateSecrets = true` to automatically generate secrets
|
||||||
|
|
||||||
|
- Set `nix-bitcoin.secretsSetupMethod = "manual"` if you want to manually setup secrets
|
||||||
|
'';
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
# This target is active when secrets have been setup successfully.
|
||||||
|
systemd.targets.nix-bitcoin-secrets = mkIf (cfg.secretsSetupMethod != "manual") {
|
||||||
|
# This ensures that the secrets target is always activated when switching
|
||||||
|
# configurations.
|
||||||
|
# In this way `switch-to-configuration` is guaranteed to show an error
|
||||||
|
# when activating the secrets target fails on deployment.
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
};
|
||||||
|
|
||||||
|
nix-bitcoin.setupSecrets = mkIf cfg.generateSecrets true;
|
||||||
|
|
||||||
|
nix-bitcoin.secretsSetupMethod = mkIf cfg.setupSecrets "setup-secrets";
|
||||||
|
|
||||||
|
# Operation of this service:
|
||||||
|
# - Set owner and permissions for all used secrets
|
||||||
|
# - Make all other secrets accessible to root only
|
||||||
|
# For all steps make sure that no secrets are copied to the nix store.
|
||||||
|
#
|
||||||
|
systemd.services.setup-secrets = mkIf cfg.setupSecrets {
|
||||||
|
requiredBy = [ "nix-bitcoin-secrets.target" ];
|
||||||
|
before = [ "nix-bitcoin-secrets.target" ];
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "oneshot";
|
||||||
|
RemainAfterExit = true;
|
||||||
|
};
|
||||||
|
script = ''
|
||||||
|
# Use the same sort order for globbing and sorting as in Nix attrsets.
|
||||||
|
# Required for `comm` below.
|
||||||
|
export LC_COLLATE=C
|
||||||
|
|
||||||
|
${optionalString cfg.generateSecrets ''
|
||||||
|
mkdir -p "${cfg.secretsDir}"
|
||||||
|
cd "${cfg.secretsDir}"
|
||||||
|
chown root: .
|
||||||
|
chmod 0700 .
|
||||||
|
${cfg.generateSecretsScript}
|
||||||
|
''}
|
||||||
|
|
||||||
|
setupSecret() {
|
||||||
|
file="$1"
|
||||||
|
user="$2"
|
||||||
|
group="$3"
|
||||||
|
permissions="$4"
|
||||||
|
if [[ ! -e $file ]]; then
|
||||||
|
echo "Error: Secret file '$file' is missing"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
chown "$user:$group" "$file"
|
||||||
|
chmod "$permissions" "$file"
|
||||||
|
processedFiles+=("$file")
|
||||||
|
}
|
||||||
|
|
||||||
|
dir="${cfg.secretsDir}"
|
||||||
|
if [[ ! -e $dir ]]; then
|
||||||
|
echo "Error: Secrets dir '$dir' is missing"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
chown root: "$dir"
|
||||||
|
cd "$dir"
|
||||||
|
|
||||||
|
processedFiles=()
|
||||||
|
${
|
||||||
|
concatStrings (mapAttrsToList (n: v: ''
|
||||||
|
setupSecret ${n} ${v.user} ${v.group} ${v.permissions}
|
||||||
|
'') cfg.secrets)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Make all other files accessible to root only
|
||||||
|
unprocessedFiles=$(
|
||||||
|
comm -23 <(shopt -s nullglob; printf '%s\n' *) <(printf '%s\n' "''${processedFiles[@]}")
|
||||||
|
)
|
||||||
|
if [[ $unprocessedFiles ]]; then
|
||||||
|
IFS=$'\n'
|
||||||
|
# shellcheck disable=SC2086
|
||||||
|
chown root: $unprocessedFiles
|
||||||
|
# shellcheck disable=SC2086
|
||||||
|
chmod 0440 $unprocessedFiles
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Now make the secrets dir accessible to other users
|
||||||
|
chmod 0751 "$dir"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
+62
@@ -0,0 +1,62 @@
|
|||||||
|
{ config, lib, pkgs, ... }:
|
||||||
|
|
||||||
|
with lib;
|
||||||
|
|
||||||
|
let
|
||||||
|
cfg = config.services;
|
||||||
|
nbLib = config.nix-bitcoin.lib;
|
||||||
|
operatorName = config.nix-bitcoin.operator.name;
|
||||||
|
in {
|
||||||
|
imports = [
|
||||||
|
../modules.nix
|
||||||
|
./enable-tor.nix
|
||||||
|
];
|
||||||
|
|
||||||
|
options = {
|
||||||
|
# Used by ../versioning.nix
|
||||||
|
nix-bitcoin.secure-node-preset-enabled = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = {
|
||||||
|
networking.firewall.enable = true;
|
||||||
|
|
||||||
|
nix-bitcoin.security.dbusHideProcessInformation = true;
|
||||||
|
|
||||||
|
# Use doas instead of sudo
|
||||||
|
security.doas.enable = true;
|
||||||
|
security.sudo.enable = false;
|
||||||
|
environment.shellAliases.sudo = "doas";
|
||||||
|
|
||||||
|
environment.systemPackages = with pkgs; [
|
||||||
|
jq
|
||||||
|
];
|
||||||
|
|
||||||
|
# Add a SSH onion service
|
||||||
|
services.tor.relay.onionServices.sshd = nbLib.mkOnionService { port = 22; };
|
||||||
|
nix-bitcoin.onionAddresses.access.${operatorName} = [ "sshd" ];
|
||||||
|
|
||||||
|
services.bitcoind = {
|
||||||
|
enable = true;
|
||||||
|
listen = true;
|
||||||
|
dbCache = 1000;
|
||||||
|
};
|
||||||
|
|
||||||
|
services.liquidd = {
|
||||||
|
# Enable `validatepegin` to verify that a transaction sending BTC into
|
||||||
|
# Liquid exists on Bitcoin. Without it, a malicious liquid federation can
|
||||||
|
# make the node accept a sidechain that is not fully backed.
|
||||||
|
validatepegin = true;
|
||||||
|
listen = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
nix-bitcoin.nodeinfo.enable = true;
|
||||||
|
|
||||||
|
# vendored: backups removed — was services.backups.frequency = "daily"
|
||||||
|
|
||||||
|
# operator
|
||||||
|
nix-bitcoin.operator.enable = true;
|
||||||
|
users.users.${operatorName} = {
|
||||||
|
openssh.authorizedKeys.keys = config.users.users.root.openssh.authorizedKeys.keys;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
+48
@@ -0,0 +1,48 @@
|
|||||||
|
{ config, lib, pkgs, ... }:
|
||||||
|
|
||||||
|
with lib;
|
||||||
|
{
|
||||||
|
options = {
|
||||||
|
nix-bitcoin.security.dbusHideProcessInformation = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = ''
|
||||||
|
Only allow users with group `proc` to retrieve systemd unit information like
|
||||||
|
cgroup paths (i.e. (sub)process command lines) via D-Bus.
|
||||||
|
|
||||||
|
This mitigates a systemd security issue where (sub)process command lines can
|
||||||
|
be retrieved by services even when their access to /proc is restricted
|
||||||
|
(via ProtectProc).
|
||||||
|
|
||||||
|
This option works by restricting the D-Bus method `GetUnitProcesses`, which
|
||||||
|
is also used internally by {command}`systemctl status`.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = mkIf config.nix-bitcoin.security.dbusHideProcessInformation {
|
||||||
|
users.groups.proc = {};
|
||||||
|
nix-bitcoin.operator.groups = [ "proc" ]; # Enable operator access to systemd-status
|
||||||
|
|
||||||
|
services.dbus.packages = lib.mkAfter [ # Apply at the end to override the default policy
|
||||||
|
(pkgs.writeTextDir "etc/dbus-1/system.d/dbus.conf" ''
|
||||||
|
<busconfig>
|
||||||
|
<policy context="default">
|
||||||
|
<deny
|
||||||
|
send_destination="org.freedesktop.systemd1"
|
||||||
|
send_interface="org.freedesktop.systemd1.Manager"
|
||||||
|
send_member="GetUnitProcesses"
|
||||||
|
/>
|
||||||
|
</policy>
|
||||||
|
<policy group="proc">
|
||||||
|
<allow
|
||||||
|
send_destination="org.freedesktop.systemd1"
|
||||||
|
send_interface="org.freedesktop.systemd1.Manager"
|
||||||
|
send_member="GetUnitProcesses"
|
||||||
|
/>
|
||||||
|
</policy>
|
||||||
|
</busconfig>
|
||||||
|
'')
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
{ config, lib, ... }:
|
||||||
|
with lib;
|
||||||
|
let
|
||||||
|
options.nix-bitcoin.configVersion = mkOption {
|
||||||
|
type = with types; nullOr str;
|
||||||
|
default = null;
|
||||||
|
description = "Vendored stub — no version migration needed.";
|
||||||
|
};
|
||||||
|
in {
|
||||||
|
inherit options;
|
||||||
|
config = {};
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
These packages are provided by nixpkgs directly. No extra pinning needed.
|
||||||
|
Missing helpers (lndinit) are vendored below if needed.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Fallback nbxplorer package if not in nixpkgs
|
||||||
|
# This is rarely needed — nixpkgs-unstable usually has it
|
||||||
|
{ lib, buildDotnetModule, fetchFromGitHub, dotnetCorePackages }:
|
||||||
|
buildDotnetModule rec {
|
||||||
|
pname = "nbxplorer";
|
||||||
|
version = "2.5.22";
|
||||||
|
src = fetchFromGitHub {
|
||||||
|
owner = "dgarage";
|
||||||
|
repo = "NBXplorer";
|
||||||
|
rev = "v${version}";
|
||||||
|
sha256 = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
|
||||||
|
};
|
||||||
|
projectFile = "NBXplorer/NBXplorer.csproj";
|
||||||
|
nugetDeps = ./nbxplorer-deps.nix; # not needed if using nixpkgs version
|
||||||
|
dotnet-sdk = dotnetCorePackages.sdk_8_0;
|
||||||
|
dotnet-runtime = dotnetCorePackages.aspnetcore_8_0;
|
||||||
|
meta = with lib; { description = "NBXplorer fallback"; license = licenses.mit; };
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# Sovran overlay — provides packages not in nixpkgs or needing overrides
|
||||||
|
# All Bitcoin packages are now sourced from nixpkgs (unstable) directly.
|
||||||
|
# This overlay only fills gaps where nixpkgs is missing/broken.
|
||||||
|
final: prev: let
|
||||||
|
# lndinit is not in nixpkgs — vendor it from nix-bitcoin source
|
||||||
|
lndinit = prev.buildGoModule rec {
|
||||||
|
pname = "lndinit";
|
||||||
|
version = "0.1.3-beta";
|
||||||
|
src = prev.fetchFromGitHub {
|
||||||
|
owner = "lightninglabs";
|
||||||
|
repo = pname;
|
||||||
|
rev = "v${version}";
|
||||||
|
sha256 = "sha256-sO1DpbppCurxr9g9nUl9Vx82FJK1mTcUw3rY1Fm1wEU=";
|
||||||
|
};
|
||||||
|
vendorHash = "sha256-El44BS5Bu0K/klMxkajciU/R6uqiXBMOiLN536QztbE=";
|
||||||
|
subPackages = [ "." ];
|
||||||
|
meta = with prev.lib; {
|
||||||
|
description = "Wallet initializer utility for lnd (vendored from nix-bitcoin)";
|
||||||
|
homepage = "https://github.com/lightninglabs/lndinit";
|
||||||
|
license = licenses.mit;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# netns-exec stub — netns isolation is stubbed, this is no-op
|
||||||
|
# If not needed, stub it to coreutils
|
||||||
|
netns-exec = prev.writeShellScriptBin "netns-exec" ''
|
||||||
|
exec "$@"
|
||||||
|
'';
|
||||||
|
|
||||||
|
# nbxplorer is needed by btcpayserver but was removed from nixpkgs in some versions
|
||||||
|
# Use nixpkgs version if available, otherwise build from nix-bitcoin pin
|
||||||
|
nbxplorer = prev.nbxplorer or (prev.callPackage ./nbxplorer.nix {} );
|
||||||
|
in {
|
||||||
|
inherit lndinit netns-exec;
|
||||||
|
# Re-expose nbxplorer only if missing
|
||||||
|
nbxplorer = if prev ? nbxplorer then prev.nbxplorer else nbxplorer;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user