17507835fc
Don't put `clightning.replication` options in `examples/configuration.nix` until it is more "battle-tested."
228 lines
7.6 KiB
Nix
228 lines
7.6 KiB
Nix
{ 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
|
|
'';
|
|
})
|
|
];
|
|
};
|
|
}
|