Subversion Repositories zfs_utils

Rev

Rev 34 | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
34 rodolico 1
# sneakernet
2
 
3
Lightweight perl script to perform zfs replication over a transport media like a disk drive. Data may be encrypted in transit using symentric key. Function (send/receive) determined by server name (hostname) which is matched in config file entries under 'source' and 'target'.
4
 
5
## Usage
6
 
7
CLI examples
8
 
9
```
10
# run
11
sneakernet
12
```
13
 
14
## $config structure
15
 
16
sneakernet uses a top-level $config object stored as YAML (e.g., sneakernet.config.yaml) to control runtime behavior. If this file does not exist, a sample is created on first run. **You must edit this config with correct values**. Failure to do so could result in lost data.
17
 
18
Example YAML:
19
 
20
```yaml
21
---
22
datasets:
23
  dataset1:
24
    filename: dataset1
25
    source: pool/dataset1
26
    target: backup/dataset1
27
  files_share:
28
    filename: files_share
29
    source: pool/files_share
30
    target: backup/files_share
31
source_server:
32
  hostname: nas
33
  poolname: pool
34
  status_file: ./sneakernet.status
35
  report:
36
    email: tech@example.org
37
    subject: 'AG Transport Report'
38
    targetDrive:
39
      fstype: ''
40
      label: ''
41
      mount_point: ''
42
target_server:
43
  hostname: airgap
44
  poolname: backup
45
  geli:
46
    diskList:
47
      - da0
48
      - da1
49
    keydiskname: replica
50
    keyfile: geli.key
51
    localKey: e98c660cccdae1226550484d62caa2b72f60632ae0c607528aba1ac9e7bfbc9c
52
    target: /media/geli.key
53
    poolname: backup
54
  report:
55
    email: ''
56
    subject: ''
57
    targetDrive:
58
      fstype: msdos
59
      label: sneakernet_report
60
      mount_point: /mnt/sneakernet_report
61
transport:
62
  disk_label: sneakernet
63
  mount_point: /mnt/sneakernet
64
  timeout: 600
65
  encryption:
66
    IV: '00000000000000000000000000000000'
67
    key: ''
68
```
69
 
70
Field reference
71
 
72
- datasets (hash, required): List of one or more definitions which are transferred
73
  - filename (string, optional): filename to be used when storing on disk. If not entered, the dataset name is used
74
  - source (string, required): full zfs path (no leading slash) on the source machine
75
  - target (string, required): full zfs path (no leading slash) on target machine
76
- source_server (hash, required): information used when processing on source server
77
  - hostname (string, required): output of hostname command on source server
78
  - poolname (string, required): name of the root pool on source server (not used)
79
  - status_file (string, required): full path to a status file to be created listing last snapshot have already been processed for each dataset/child dataset
80
  - report (hash, optional): contains parameters for sending report
81
    - email (hash, optional): information on how to send report via e-mail
82
      - address (string, optional): single e-mail address to send report to
83
      - subject (string, optional); Subject line of e-mail
84
    - targetDrive (hash, optional): information on saving the file on a local disk
85
      - fstype (string, optional): use either **msdos** or **ufs** depending on how your disk is formatted. Default is ufs, which is not easily read on Windows or MacOS machines.
86
      - label (string, optional): msdos label of partition to be mounted for the report
87
      - mount_point (string, optional): directory to mount the partition on. Calculated as /mnt/label if not set
88
- target_server (hash, required): information used when processing target server
89
  - hostname (string, required): output of hostname command on target server
90
  - poolname (string, required): name of the root pool on target server (not used)
91
  - geli (hash, optional): information to decrypt geli encrypted drives and mount zpool
92
    - disk_list (array, optional): list of geli encrypted disks which make up the pool. Searched for if not listed (drive names change on BSD)
93
    - keydiskname (string, required): label on ufs file system containing key file
94
    - keyfile (string, optional): name of the file on disk
95
    - localKey (hex string, optional): 64 hexadecimal characters (256 bit) which is xor'd with keyfile to calculate target
96
    - target (string, required): name of file on disk which is the key to unlock the geli disks. If keyfile and local key exist, this is created by xoring them, but may be found some other way.
97
  - report (hash, optional): contains parameters for sending report
98
    - email (hash, optional): information on how to send report via e-mail
99
      - address (string, optional): single e-mail address to send report to
100
      - subject (string, optional); Subject line of e-mail
101
    - targetDrive (hash, optional): information on saving the file on a local disk
102
      - fstype (string, optional): use either **msdos** or **ufs** depending on how your disk is formatted. Default is ufs, which is not easily read on Windows or MacOS machines.
103
      - label (string, optional): msdos label of partition to be mounted for the report
104
      - mount_point (string, optional): directory to mount the partition on. Calculated as /mnt/label if not set
105
- transport (hash, required): hash with information on transport media.
106
  - disk_label (string, required): label on transport disk. Assumed to be ufs disk with label
107
  - mount_point (string, optional): mount point of disk with disk_label label. Calculated as /mnt/disk_label if not defined
108
  - timeout (integer, optional): number of seconds to wait for transport disk to appear. Defaults to 600s (10 min).
109
  - encryption (hash, optional): information used to encrypt/decrypt data on transport disk. This is a symetric key, so must be the same on both source and target machines
110
    - IV (hex string, optional): Initial vector for encryption. Defaults to all zeros if not set
111
    - key (hex string, optional): 64 hexadecimal characters (256bit) used to encrypt files in transit (while on transport disk). If not set, files are unencrypted
112
- 
113
 
114
Notes on YAML
115
 
116
- Use a UTF-8 encoded file (config.yaml) and preserve numeric types (no quotes) for numeric fields.
117
- Tooling reads YAML and validates types; comments are allowed but not preserved in runtime exports.
118
 
119
## Overview
120
 
121
The script acts differently based on whether a call to *hostname* returns the source or target servers names. 
122
 
123
The script requires the library (perl module) ZFS_Utils.pm to be in the parent document of the script. Most functions are defined there.  See ZFS_Utils.md for documentation.
124
 
125
**Note:** the variable \$DEBUG, if set to non-zero, will perform all functions except actual writes. In source mode, it will not create the files on the transport disk, in target mode it will not actually perform the receive to the datasets. However, the "what I would do" shows up in the logs.
126
 
127
#### Common startup
128
 
129
- Load config fileinto hash \$config.
130
 
131
  - If config file does not exist, it will create it and exit with a message
132
 
133
  - If config file does not contain source or target server names, it assumes it is not edited and exits with a message
134
 
135
- Mount transport disk
136
 
137
  - wait *timeout* seconds for disk label to show up in /dev/gpt, displaying prompt every 14 seconds. Dies if it can not find it
138
 
139
  - mount transport disk on *mount_point*
140
 
141
- Get results of *hostname -s* to determine if we are in source mode or target. If the name matches neither, fails with an error message
142
 
143
#### Source Mode
144
 
145
- read status file
146
 
147
- Perform replication, placing files on transport disk.
148
 
149
  - The files are named with the dataset, with all slashes replaced with periods, so datasets with embedded period will fail when in Target Mode
150
 
151
  - if encryption is defined, pass send stream through openssl (*openssl enc*)
152
 
153
- Writes the status file to disk. Status file will contain one line for each dataset sent, containing the last snapshot sent
154
 
155
#### Target Mode
156
 
157
- If geli encryption defined
158
 
159
  - Find geli key
160
 
161
  - Find geli disks, either defined or by searching for all disks which are not
162
 
163
    - mounted
164
 
165
    - partitioned
166
 
167
    - a member of an existing zpool
168
 
169
  - decrypt all geli disks
170
 
171
  - Import the zpool
172
 
173
- for each file in the root ofthe transport disk
174
 
175
  - receive into correct dataset
176
 
177
    - if filename has periods, calculate dataset as a path in the zpool
178
 
179
    - If encryption is defined, pass the file through openssl in decrypt mode (*openssl enc -d*)
180
 
181
#### Common Cleanup
182
 
183
- Umount transport disk and remove its *mount_path*
184
 
185
- Send report if defined
186
 
187
## Other Notes
188
 
189
Some areas of the script require further explanation. This section covers that.
190
 
191
#### Transport Encryption
192
 
193
Transport encryption is optionally set by placing a hex string representation of a 256bit key in transport/encryption. This key can be generated by hand, or you can use openssl, xxd and tr with the command:
194
 
195
`openssl rand 32 | xxd -p | tr -d '\n'`
196
 
197
If this key exists, the contents of the data on the transport script are encrypted with it, and can not be viewed without using the same key to decrypt (symetric encryption, or one time key pad)
198
 
199
You can further boost security by setting the IV parameter to something other than 0's.
200
 
201
#### Status file
202
 
203
Since the source server can not contact the target to see what is on there, the status file on the source server keeps track of what the last snapshot sent was. If it does not exist, the system assumes we need a full copy, which can require a very large media to do the transfer (size of files plus size of all deltas).
204
 
205
If your target server already has some snapshots on it, you can find the newest snapshot on it for each dataset, then make the file yourself.
206
 
207
A sample from my test machine shows two datasets on the zpool *storage* name ComputerFiles and android. android has a child dataset which was out of sync, so that is show also.
208
 
209
Running the command `zfs list -rt snap -H -o name storage` shows the following.
210
 
211
```
212
storage@daily_2025-12-07_16.09.00--7d
213
storage/ComputerFiles@daily_2025-12-07_16.09.00--7d
214
storage/ComputerFiles@daily_2025-12-10_02.10.00--7d
215
storage/android@daily_2025-12-07_16.09.00--7d
216
storage/android@daily_2025-12-10_16.54.00--7d
217
storage/android/old@daily_2025-12-07_16.09.00--7d
218
storage/android/old@daily_2025-12-10_16.54.00--7d
219
storage/android/old@daily_2025-12-10_16.58.00--7d
220
```
221
 
222
From that, I can grab only the latest snapshots from each and create a status file containing
223
 
224
```
225
storage/ComputerFiles@daily_2025-12-07_16.09.00--7d
226
storage/android@daily_2025-12-07_16.09.00--7d
227
storage/android/old@daily_2025-12-07_16.09.00--7d
228
```
229
 
230
#### geli Encryption
231
 
232
This system was designed for an air gap server holding some sensitive information, in an insecure location. As such, we encrypt the disks that make up the zpool using geli, BSD's full disk encryption system. If your system does not use geli encryption, just leave the geli section blank or don't even include it.
233
 
234
###### Finding Disks
235
 
236
Since BSD has a habit of changing disk drive names when a drive is added or removed from the system (on reboot), it is likely not best to rely on the drive names. However, in some cases it is fine. Listing the drives as an array will limit the drives which are decrypted, resulting in a slightly faster decryptions.
237
 
238
However, if the drives are not listed, a search will be made of all drives which are not mounted, have no slices (partitions), and are not a member of any other zpool. It then attempts to decrypt those. If it finds a drive or two which are not geli disks, is simply moves on to the next one.
239
 
240
###### Encryption Key
241
 
242
To make it easier to run without user interaction, we use a key file for the encryption. This file is passed to geli to decrypt the disks and make them available.
243
 
244
To make it a little safer, we randomly generated two keys of 256bit (32 bytes), then ran the binary XOR function on them to create a third key. When you do this, having two keys allow you to calculate the third one by simply XOR'ing them together.
245
 
246
The script in utilities named *makeGeliKey* can do all this for you. It will generate three keys, arbitrarily naming them combined_geli.key, local_geli_1.key and local_geli_2.key.
247
 
248
The idea is that you use combined_geli.key to encrypt your disks, then you put local_geli_1.key on a thumb drive and the hex representation of local_geli_2.key into the configuration file.
249
 
250
When you have indicated you have a geli setup, you plug the thumb drive in, the system finds it, XOR's it with the hex representation in the config file, generating the combined_geli.key file. After all disks have been decrypted, the system them shreds (erases as per DOD specs) the combined_geli.key. That means even if you lose your thumb drive and it is found by someone else, they can not do anything with it without access to the server, and you can change the local geli keys and generate new ones.
251
 
252
If you do not need this additional layer of security, you can simply the combined_geli.key file on your thumb drive and leave the keyfile and localKey empty. In that case, the decryption will assume the file is on the thumb drive and use it.
253
 
254
#### Disk Labels
255
 
256
The transport disk, the optional security key USB drive, and the optional report USB are all identified by labels. When the system decides it needs a disk, it will look for the label in /dev/gpt or /dev/msdos (depending on what it thinks it needs), then mount by label.
257
 
258
I chose to use UFS for both the keyfile and the transport disk. Security for the former (most people will assume it is a bad disk and reformat) and file system robustness for the latter. May give me time to create another key pair to invalidate the one lost.
259
 
260
For the report USB, I used FAT32 so the disk could be plugged into a Windows or MacOS machine and read easily.
261
 
262
To create the disk structure for the transport and key file media, identify it, then run the following, changing *da9* to the disk name for your device. There are 5 locations for it (don't forget da9p1, where the da9 needs to be changed)
263
 
264
```bash
265
# create the GPT schema
266
gpart create -s GPT da9
267
# show what is there (nothing yet)
268
gpart list /dev/da9
269
# add a new slice of type UFS, and give it a label, sneakernet
270
# use all of the available disk space for this (ie, only one slice)
271
gpart add -t freebsd-ufs -l sneakernet da9
272
# show what is up
273
gpart list /dev/da9
274
# format the partition with UFS
275
newfs /dev/da9p1
276
# it should now show up as an alias in /dev/gpt
277
ls /dev/gpt
278
# make a place to mount it
279
mkdir /mnt/sneakernet
280
# mount it
281
mount /dev/gpt/sneakernet /mnt/sneakernet
282
# unmount it
283
umount /mnt/sneakernet
284
# clean up
285
rmdir /mnt/sneakernet
286
```
287
 
288
For the report disk, formatted with FAT32, use the following. Again, change all *da9* to the actual device you are using. There are five locations.
289
 
290
```bash
291
# remove any existing schema
292
gpart destroy -F /dev/da9
293
# create an mbr schema
294
gpart create -s mbr /dev/da9
295
# add a fat32 file system labeled REPORT using all the space
296
gpart add -t fat32 -l REPORT /dev/da9
297
# format it, using msdos and the label.
298
newfs_msdos -L REPORT -F 32 /dev/da9s1
299
# show what you have
300
gpart show /dev/da9
301
# make a place to mount it
302
mkdir /mnt/REPORT
303
# mount it
304
mount -t msdosfs /dev/msdosfs/REPORT /mnt/REPORT
305
# ummount it
306
umount /mnt/REPORT
307
# clean up
308
rmdir /mnt/REPORT/
309
```
310
 
311
## Functions
312
 
313
- getStatusFile
314
 
315
  - Reads the status file
316
 
317
  - Parameters: file name
318
 
319
  - Returns: contents of status file as an array, one entry per line
320
 
321
- writeStatusFile
322
 
323
  - writes updated status file. Old status file saved with .bak suffix (only one backup)
324
 
325
  - Paramters: file name, array ref
326
 
327
  - Returns: nothing
328
 
329
- replaceSlashWithDot
330
 
331
  - Replaces all slashes (used in pathnames) with periods so the path can be used as a file name. Removes first element
332
 
333
  - Parameters: string
334
 
335
  - Returns: string with all slashes removed and first entry removed
336
 
337
- doSourceReplication
338
 
339
  - This is the meat of the script when in source mode; it finds all snapshots, calculates the new ones, creates and executes the commands to write them
340
 
341
  - Parameters: config, status file contents
342
 
343
  - Returns: hashref of new "last snapshot used" for new Status File
344
 
345
- cleanDirectory
346
 
347
  - Removes all files from a given directory. Will not touch subdirectories. Used to clean Transport Disk before source server begins transfer
348
 
349
  - Parameters: directory name
350
 
351
  - Returns: nothing
352
 
353
- fatalError
354
 
355
  - Adds a message to the log, then performs a die. Used for critical failure
356
 
357
  - Parameters: string to add to log
358
 
359
  - Returns: nothing, kills program
360
 
361
Contributing
362
 
363
- Follow semantic versioning for manifest schema changes.
364
- Tests should simulate inconsistent snapshots in child datasets
365
 
366
License
367
 
368
- Choose an appropriate license (MIT, Apache-2.0, etc.) for your project.