1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
|
mod error;
use error::*;
use hbak_common::config::{NodeConfig, RemoteNode, RemoteNodeAuth};
use hbak_common::conn::{AuthConn, DEFAULT_PORT};
use hbak_common::message::SyncInfo;
use hbak_common::proto::{LocalNode, Node, Volume};
use hbak_common::system;
use hbak_common::LocalNodeError;
use std::collections::HashMap;
use std::net::SocketAddr;
use clap::{Parser, Subcommand};
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
/// Perform basic initialization of the local node.
Init {
/// Initialize the configuration file but not the btrfs subvolumes.
#[arg(short, long)]
config_only: bool,
/// The device file the local btrfs file system is located at.
device: String,
/// The name to use for this node.
node_name: String,
/// The network address `hbakd` binds to. The default is `[::]:20406` (dual stack).
bind_addr: Option<SocketAddr>,
},
/// Fully clean the local node of non-binary files with optional backup removal.
Clean {
/// Remove the btrfs subvolumes that contain the snapshots and backups.
#[arg(short, long)]
backups: bool,
},
/// Mark a subvolume as owned by the local node.
Track {
/// The name of the subvolume to mark as owned.
subvol: String,
},
/// Remove the local node ownership mark from a subvolume.
Untrack {
/// The name of the subvolume to unmark as owned.
subvol: String,
},
/// Add or modify a remote to push to or pull from.
AddRemote {
/// The network address and port of the remote node.
address: String,
/// The volumes to push to the remote node.
push: Vec<String>,
/// The volumes to pull from the remote node.
/// Subvolumes owned by the local node are silently ignored.
pull: Vec<String>,
},
/// Remove a remote without deleting anything.
RmRemote {
/// The network address and port of the node to forget.
address: String,
},
/// Add or modify authentication and authorization information for a remote client.
Grant {
/// The name of the remote node to apply the information to.
node_name: String,
/// The volumes the remote node is allowed to push.
/// Subvolumes owned by the local node are silently ignored.
#[arg(long = "push")]
push: Vec<String>,
/// The volumes the remote node is allowed to pull.
#[arg(long = "pull")]
pull: Vec<String>,
},
/// Modify permissions for a remote client without changing the passphrase.
SetPerms {
/// The name of the remote node to apply the information to.
node_name: String,
/// The volumes the remote node is allowed to push.
/// Subvolumes owned by the local node are silently ignored.
#[arg(long = "push")]
push: Vec<String>,
/// The volumes the remote node is allowed to pull.
#[arg(long = "pull")]
pull: Vec<String>,
},
/// Revoke a remote client all access and delete local configuration about it.
Revoke {
/// The name of the remote node to remove from the security configuration.
node_name: String,
},
/// Export a random verifier and key of the local encryption passphrase.
ExportPass,
/// Take a (local) snapshot of the specified subvolumes.
Snapshot {
/// Take incremental snapshots rather than full snapshots.
#[arg(short, long)]
incremental: bool,
/// The subvolumes to limit snapshotting to.
subvols: Vec<String>,
},
/// Synchronize snapshots with remote nodes.
Synchronize {
/// The volumes to limit pushing to.
#[arg(long = "push")]
push: Vec<String>,
/// The volumes to limit pulling to.
#[arg(long = "pull")]
pull: Vec<String>,
/// The nodes to limit synchronization to.
nodes: Vec<String>,
},
}
fn logic() -> Result<()> {
let cli = Cli::parse();
match cli.command {
Commands::Init {
config_only,
device,
node_name,
bind_addr,
} => {
let passphrase = rpassword::prompt_password("Enter new encryption passphrase: ")?;
system::init(config_only, device, bind_addr, node_name, passphrase)?;
}
Commands::Clean { backups } => {
system::deinit(backups)?;
}
Commands::Track { subvol } => {
let mut node_config = NodeConfig::load()?;
node_config.subvols.retain(|item| *item != subvol);
node_config.subvols.push(subvol);
node_config.save()?;
}
Commands::Untrack { subvol } => {
let mut node_config = NodeConfig::load()?;
node_config.subvols.retain(|item| *item != subvol);
node_config.save()?;
}
Commands::AddRemote {
address,
push,
pull,
} => {
let mut node_config = NodeConfig::load()?;
node_config.remotes.retain(|item| item.address != address);
node_config.remotes.push(RemoteNode {
address,
push: Volume::try_from_bulk(push)?,
pull: Volume::try_from_bulk(pull)?,
});
node_config.save()?;
}
Commands::RmRemote { address } => {
let mut node_config = NodeConfig::load()?;
node_config.remotes.retain(|item| item.address != address);
node_config.save()?;
}
Commands::Grant {
node_name,
mut push,
pull,
} => {
// Unmount the btrfs before potentially getting killed at prompts.
{
let local_node = LocalNode::new()?;
push.retain(|subvol| !local_node.owns_subvol(subvol));
}
println!("Use the passphrase export results from the remote node below.");
let verifier_hex = rpassword::prompt_password("Enter verifier: ")?;
let verifier = hex::decode(verifier_hex)?;
let key_hex = rpassword::prompt_password("Enter key: ")?;
let key = hex::decode(key_hex)?;
let mut node_config = NodeConfig::load()?;
node_config.auth.retain(|item| item.node_name != node_name);
node_config.auth.push(RemoteNodeAuth {
node_name,
verifier,
key,
push: Volume::try_from_bulk(push)?,
pull: Volume::try_from_bulk(pull)?,
});
node_config.save()?;
}
Commands::SetPerms {
node_name,
mut push,
pull,
} => {
let local_node = LocalNode::new()?;
push.retain(|subvol| !local_node.owns_subvol(subvol));
let mut node_config = NodeConfig::load()?;
for item in &mut node_config.auth {
if item.node_name == node_name {
item.push = Volume::try_from_bulk(push)?;
item.pull = Volume::try_from_bulk(pull)?;
break;
}
}
node_config.save()?;
}
Commands::Revoke { node_name } => {
let mut node_config = NodeConfig::load()?;
node_config.auth.retain(|item| item.node_name != node_name);
node_config.save()?;
}
Commands::ExportPass => {
let node_config = NodeConfig::load()?;
let (verifier, key) = system::hash_passphrase(node_config.passphrase)?;
println!("Verifier: {}", hex::encode(verifier));
println!("Key: {}", hex::encode(key));
}
Commands::Snapshot {
incremental,
subvols,
} => {
let local_node = LocalNode::new()?;
let subvols = if subvols.is_empty() {
&local_node.config().subvols
} else {
&subvols
}
.iter();
for subvol in subvols {
if !local_node.owns_subvol(subvol) {
return Err(LocalNodeError::ForeignSubvolume(subvol.clone()).into());
}
println!("Snapshotting {}...", subvol);
local_node.snapshot_now(subvol.clone(), incremental)?;
}
}
Commands::Synchronize { push, pull, nodes } => {
let local_node = LocalNode::new()?;
for node in local_node
.config()
.remotes
.iter()
.filter(|item| nodes.is_empty() || nodes.contains(&item.address))
{
println!("Synchronizing with {}...", node.address);
sync(&local_node, node, &push, &pull)?;
}
}
}
Ok(())
}
fn main() {
match logic() {
Ok(_) => {}
Err(e) => eprintln!("Error: {}", e),
}
}
fn sync(
local_node: &LocalNode,
remote_node: &RemoteNode,
push: &[String],
pull: &[String],
) -> Result<()> {
let address = match remote_node.address.parse() {
Ok(address) => address,
Err(_) => SocketAddr::new(remote_node.address.parse()?, DEFAULT_PORT),
};
let auth_conn = AuthConn::new(&address)?;
let stream_conn = auth_conn.secure_stream(
local_node.name().to_string(),
remote_node.address.to_string(),
&local_node.config().passphrase,
)?;
println!("Authentication to {} successful", remote_node.address);
let mut local_sync_info = SyncInfo {
volumes: HashMap::new(),
};
for volume in &remote_node.push {
if push.is_empty() || push.contains(&volume.to_string()) {
let latest_snapshots = local_node.latest_snapshots(volume.clone())?;
local_sync_info
.volumes
.insert(volume.clone(), latest_snapshots);
}
}
let (stream_conn, remote_sync_info) = stream_conn.meta_sync(local_sync_info)?;
todo!()
}
|