clightning: native database replication
Don't put `clightning.replication` options in `examples/configuration.nix` until it is more "battle-tested."
This commit is contained in:
227
modules/clightning-replication.nix
Normal file
227
modules/clightning-replication.nix
Normal file
@@ -0,0 +1,227 @@
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
let
|
||||
options.services.clightning.replication = {
|
||||
enable = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
Enable live replication of the clightning database.
|
||||
This prevents losing off-chain funds when the primary wallet file becomes
|
||||
inaccessible.
|
||||
|
||||
For setting the destination, you can either define option `sshfs.destination`
|
||||
or `local.directory`.
|
||||
|
||||
When `encrypt` is `false`, file `lightningd.sqlite3` is written to the destination.
|
||||
When `encrypt` is `true`, directory `lightningd-db` is written to the destination.
|
||||
It includes the encrypted database and gocryptfs metadata.
|
||||
|
||||
See also: https://github.com/ElementsProject/lightning/blob/master/doc/BACKUP.md
|
||||
'';
|
||||
};
|
||||
sshfs = {
|
||||
destination = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
example = "user@10.0.0.1:directory";
|
||||
description = ''
|
||||
The SSH destination for which a SSHFS will be mounted.
|
||||
`directory` is relative to the home of `user`.
|
||||
|
||||
A SSH key is automatically generated and stored in file
|
||||
`$secretsDir/clightning-replication-ssh`.
|
||||
The SSH server must allow logins via this key.
|
||||
I.e., the `authorized_keys` file of `user` must contain
|
||||
`$secretsDir/clightning-replication-ssh.pub`.
|
||||
'';
|
||||
};
|
||||
port = mkOption {
|
||||
type = types.port;
|
||||
default = 22;
|
||||
description = "SSH port of the remote server.";
|
||||
};
|
||||
sshOptions = mkOption {
|
||||
type = with types; listOf str;
|
||||
default = [ "reconnect" "ServerAliveInterval=50" ];
|
||||
description = "SSH options used for mounting the SSHFS.";
|
||||
};
|
||||
};
|
||||
local = {
|
||||
directory = mkOption {
|
||||
type = types.nullOr types.path;
|
||||
default = null;
|
||||
example = "/var/backup/clightning";
|
||||
description = ''
|
||||
This option can be specified instead of `sshfs.destination` to enable
|
||||
replication to a local directory.
|
||||
|
||||
If `local.setupDirectory` is disabled, the directory
|
||||
- must already exist when `clightning.service` (or `clightning-replication-mounts.service`
|
||||
if `encrypt` is `true`) starts.
|
||||
- must have write permissions for the `clightning` user.
|
||||
|
||||
This option is also useful if you want to use a custom remote destination,
|
||||
like a NFS or SMB share.
|
||||
'';
|
||||
};
|
||||
setupDirectory = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description = ''
|
||||
Create `local.directory` if it doesn't exist and set write permissions
|
||||
for the `clightning` user.
|
||||
'';
|
||||
};
|
||||
};
|
||||
encrypt = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
Whether to encrypt the replicated database with gocryptfs.
|
||||
The encryption password is automatically generated and stored
|
||||
in file `$secretsDir/clightning-replication-password`.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
cfg = config.services.clightning.replication;
|
||||
inherit (config.services) clightning;
|
||||
|
||||
secretsDir = config.nix-bitcoin.secretsDir;
|
||||
network = config.services.bitcoind.makeNetworkName "bitcoin" "regtest";
|
||||
user = clightning.user;
|
||||
group = clightning.group;
|
||||
|
||||
useSshfs = cfg.sshfs.destination != null;
|
||||
useMounts = useSshfs || cfg.encrypt;
|
||||
|
||||
localDir = cfg.local.directory;
|
||||
mountsDir = "/var/cache/clightning-replication";
|
||||
sshfsDir = "${mountsDir}/sshfs";
|
||||
plaintextDir = "${mountsDir}/plaintext";
|
||||
destDir =
|
||||
if cfg.encrypt then
|
||||
plaintextDir
|
||||
else if useSshfs then
|
||||
sshfsDir
|
||||
else
|
||||
localDir;
|
||||
in {
|
||||
inherit options;
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
assertions = [
|
||||
{ assertion = useSshfs || (localDir != null);
|
||||
message = ''
|
||||
services.clightning.replication: One of `sshfs.destination` or
|
||||
`local.directory` must be set.
|
||||
'';
|
||||
}
|
||||
{ assertion = !useSshfs || (localDir == null);
|
||||
message = ''
|
||||
services.clightning.replication: Only one of `sshfs.destination` and
|
||||
`local.directory` must be set.
|
||||
'';
|
||||
}
|
||||
];
|
||||
|
||||
environment.systemPackages = optionals cfg.encrypt [ pkgs.gocryptfs ];
|
||||
|
||||
systemd.tmpfiles.rules = optional (localDir != null && cfg.local.setupDirectory)
|
||||
"d '${localDir}' 0770 ${user} ${group} - -";
|
||||
|
||||
services.clightning.wallet = let
|
||||
mainDB = "${clightning.dataDir}/${network}/lightningd.sqlite3";
|
||||
replicaDB = "${destDir}/lightningd.sqlite3";
|
||||
in "sqlite3://${mainDB}:${replicaDB}";
|
||||
|
||||
systemd.services.clightning = {
|
||||
bindsTo = mkIf useMounts [ "clightning-replication-mounts.service" ];
|
||||
serviceConfig.ReadWritePaths = [
|
||||
# We can't simply set `destDir` here because it might point to
|
||||
# a FUSE mount.
|
||||
# FUSE mounts can only be set up as `ReadWritePaths` by systemd when they
|
||||
# are accessible by root. This would require FUSE-mounting with option
|
||||
# `allow_other`.
|
||||
(if useMounts then mountsDir else localDir)
|
||||
];
|
||||
};
|
||||
|
||||
systemd.services.clightning-replication-mounts = mkIf useMounts {
|
||||
requiredBy = [ "clightning.service" ];
|
||||
before = [ "clightning.service" ];
|
||||
wants = [ "nix-bitcoin-secrets.target" ];
|
||||
after = [ "nix-bitcoin-secrets.target" ];
|
||||
path = [
|
||||
# Includes
|
||||
# - The SUID-wrapped `fusermount` binary which enables FUSE
|
||||
# for non-root users
|
||||
# - The SUID-wrapped `mount` binary, used for unmounting
|
||||
"/run/wrappers"
|
||||
] ++ optionals cfg.encrypt [
|
||||
# Includes `logger`, required by gocryptfs
|
||||
pkgs.util-linux
|
||||
];
|
||||
|
||||
script =
|
||||
optionalString useSshfs ''
|
||||
mkdir -p ${sshfsDir}
|
||||
${pkgs.sshfs}/bin/sshfs ${cfg.sshfs.destination} -p ${toString cfg.sshfs.port} ${sshfsDir} \
|
||||
-o ${builtins.concatStringsSep "," ([
|
||||
"IdentityFile='${secretsDir}'/clightning-replication-ssh-key"
|
||||
] ++ cfg.sshfs.sshOptions)}
|
||||
'' +
|
||||
optionalString cfg.encrypt ''
|
||||
cipherDir="${if useSshfs then sshfsDir else localDir}/lightningd-db"
|
||||
mkdir -p "$cipherDir" ${plaintextDir}
|
||||
gocryptfs=(${pkgs.gocryptfs}/bin/gocryptfs -passfile '${secretsDir}/clightning-replication-password')
|
||||
# 1. init
|
||||
if [[ ! -e $cipherDir/gocryptfs.conf ]]; then
|
||||
"''${gocryptfs[@]}" -init "$cipherDir"
|
||||
fi
|
||||
# 2. mount
|
||||
"''${gocryptfs[@]}" "$cipherDir" ${plaintextDir}
|
||||
'';
|
||||
|
||||
postStop =
|
||||
optionalString cfg.encrypt ''
|
||||
umount ${plaintextDir} || true
|
||||
'' +
|
||||
optionalString useSshfs ''
|
||||
umount ${sshfsDir}
|
||||
'';
|
||||
|
||||
serviceConfig = {
|
||||
StopPropagatedFrom = [ "clightning.service" ];
|
||||
CacheDirectory = "clightning-replication";
|
||||
CacheDirectoryMode = "770";
|
||||
User = user;
|
||||
RemainAfterExit = "yes";
|
||||
Type = "oneshot";
|
||||
};
|
||||
};
|
||||
|
||||
nix-bitcoin = mkMerge [
|
||||
(mkIf useSshfs {
|
||||
secrets.clightning-replication-ssh-key = {
|
||||
user = user;
|
||||
permissions = "400";
|
||||
};
|
||||
generateSecretsCmds.clightning-replication-ssh-key = ''
|
||||
if [[ ! -f clightning-replication-ssh-key ]]; then
|
||||
${pkgs.openssh}/bin/ssh-keygen -t ed25519 -q -N "" -C "" -f clightning-replication-ssh-key
|
||||
fi
|
||||
'';
|
||||
})
|
||||
|
||||
(mkIf cfg.encrypt {
|
||||
secrets.clightning-replication-password.user = user;
|
||||
generateSecretsCmds.clightning-replication-password = ''
|
||||
makePasswordSecret clightning-replication-password
|
||||
'';
|
||||
})
|
||||
];
|
||||
};
|
||||
}
|
||||
@@ -40,6 +40,15 @@ let
|
||||
default = "${cfg.dataDir}/${network}";
|
||||
description = "The network data directory.";
|
||||
};
|
||||
wallet = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
example = "sqlite3:///var/lib/clightning/bitcoin/lightningd.sqlite3";
|
||||
description = ''
|
||||
Wallet data scheme (sqlite3 or postgres) and location/connection
|
||||
parameters, as fully qualified data source name.
|
||||
'';
|
||||
};
|
||||
extraConfig = mkOption {
|
||||
type = types.lines;
|
||||
default = "";
|
||||
@@ -105,6 +114,7 @@ let
|
||||
bitcoin-rpcuser=${config.services.bitcoind.rpc.users.public.name}
|
||||
rpc-file-mode=0660
|
||||
log-timestamps=false
|
||||
${optionalString (cfg.wallet != null) "wallet=${cfg.wallet}"}
|
||||
${cfg.extraConfig}
|
||||
'';
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
./clightning.nix
|
||||
./clightning-plugins
|
||||
./clightning-rest.nix
|
||||
./clightning-replication.nix
|
||||
./spark-wallet.nix
|
||||
./lnd.nix
|
||||
./lightning-loop.nix
|
||||
|
||||
Reference in New Issue
Block a user