-
Notifications
You must be signed in to change notification settings - Fork 2
/
qcryptd
executable file
·1350 lines (1167 loc) · 43.2 KB
/
qcryptd
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
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/bin/bash
#
#See usage().
#
#Copyright (C) 2020 David Hobach GPLv3
#version: 0.9
#
#This program is free software: you can redistribute it and/or modify
#it under the terms of the GNU General Public License as published by
#the Free Software Foundation, either version 3 of the License, or
#(at your option) any later version.
#
#This program is distributed in the hope that it will be useful,
#but WITHOUT ANY WARRANTY; without even the implied warranty of
#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
#GNU General Public License for more details.
#
#You should have received a copy of the GNU General Public License
#along with this program. If not, see <https://www.gnu.org/licenses/>.
#
#init blib
source blib
b_checkVersion 1 6 || { >&2 echo "This script depends on blib (https://github.com/3hhh/blib) version 1.6 or higher. Please install a supported version." ; exit 1 ; }
eval "$B_SCRIPT"
b_import "arr"
b_import "args"
b_import "daemon"
b_import "proc"
b_import "ini"
b_import "fs"
b_import "notify"
b_import "multithreading/mtx"
b_import "os/qubes4/dom0"
#path to the qcrypt binary
QCRYPT="$B_SCRIPT_DIR/qcrypt"
#main & secondary configuration directory
QCRYPTD_CONF="/etc/$B_SCRIPT_NAME"
QCRYPTD_CONF_2="$B_SCRIPT_DIR/conf"
#distinguish the B_E exit code from the "normal" error
#shellcheck disable=SC2034
B_RC=6
#default options for b_dom0_qvmRun & b_dom0_exec*
#shellcheck disable=SC2034
B_DOM0_QVM_RUN_PARAMS=("--no-gui")
#daemon ID to uniquely identify the background process
DID="$B_SCRIPT_NAME"
#whether or not the daemon was initialized
DAEMON_INIT_DONE=1
#debug mode on/off switch (-v flag)
DEBUG=1
#where to log stdout & stderr daemon output, if DEBUG=0
DEBUG_OUT="$B_SCRIPT_DIR/qcryptd.log"
DEBUG_ERR="$DEBUG_OUT"
#maximum number of lines for the debug log to keep upon initialization
DEBUG_LINES_MAX=100000
#used by the daemon to store the configuration target
CONF_TARGET=
#0 = the main loop should process events; everything else = it should exit
PROCESS_EVENTS=0
#qrexec timeout
QREXEC_TIMEOUT="$(qubes-prefs "default_qrexec_timeout")" || { B_ERR="Failed to retrieve the default qrexec timeout." ; B_E ; }
#internal configuration representation
#array chains : all chains managed by qcryptd
#map chains2info : chain ids --> all config input, chain start/stop command
#map vms2cains : all VMs --> list of related chain ids
#
#terminology:
#chain id: anything uniquely identifying a chain (can be the config file or a sourceVM:device:file combination)
# here: config file (without .ini)
#device: identified by its full path (recommended: /dev/disk/* ) inside a specifc VM, can be related to multiple chains (e.g. different file on the same device)
# here: [source VM]:[source device]
declare -ga CHAINS=()
declare -gA CHAINS2INFO=()
declare -gA VMS2CHAINS=()
#maps tracking currently active devices, VMs and chains
#ACTIVE_VMS / V: VM name --> 0/1/2 (2 = paused, use isRunning [vm] / isPaused [vm] to obtain the run state instead of this one)
#ACTIVE_CHAINS / C: chain id --> 0/1 (use isActive [chain] to obtain the most recent state rather than this one)
declare -gA ACTIVE_VMS=()
declare -gA ACTIVE_CHAINS=()
#helper maps for handleQubesEvent
#LAST_ATTACHED: stores events and their times related to device attachments
#LAST_START_ATTEMPT/LAST_SHUTDOWN_ATTEMPT: vm --> last start/shutdown attempt (not necessarily successful) timestamp in seconds since daemon start
#SUPPRESSION: used to suppress the same event occurring in a very short time
declare -gA LAST_ATTACHED=()
declare -gA LAST_START_ATTEMPT=()
declare -gA LAST_SHUTDOWN_ATTEMPT=()
declare -gA SUPPRESSION=()
function usage {
echo "
Usage: $B_SCRIPT_NAME [options] [command]
Automatically manage qcrypt chains on VM starts/stops as well as device attachments.
The daemon must be configured via ini files at the below configuration directory before it can be used. Each configuration file represents
a single chain. You can find configuration examples inside the 'examples' directory.
Configuration directories:
$QCRYPTD_CONF
$QCRYPTD_CONF_2
[command] may be one of:
start [target]
Start the qcrypt daemon in the background.
[target] Defines the configuration directory to use (default: default).
Options:
-v Start the daemon in verbose mode. This will produce output suitable for debugging, but possibly with a performance
impact. Verbose mode is supported by all commands.
stop
Stop the running qcrypt daemon.
Options:
-c Close all qcrypt chains and their VMs before shutting the daemon down. This can also fix partially closed chains. By
default, all qcrypt chains are left as-is. It is recommended to stop with -c before switching to a different config-
uration.
IMPORTANT: This will shut down all VMs for which the current target has qcrypt configurations - even VMs with inactive
chains. So make sure to save any open data before!
restart [target]
Restart the qcrypt daemon. Configuration changes always require a daemon restart to become effective.
[target] The target for the start operation (default: default).
Options:
-c See stop.
check [target]
Check the given target configuration for correctness. If no issues are found, a zero exit code is set. It is recommended to use this
operation before deploying a new configuration.
[target] The target to check (default: default).
chains [target]
Show the qcrypt status commands for all chains related to the given configuration target (default: default).
Options:
-n Don't display the configuration files.
-e Execute the qcrypt status commands and provide a short summary for each. The qcryptd exit code indicates the number
of chains in an invalid state.
status
Check whether a qcrypt daemon is running in the background. If you need information about the currently active qcrypt chains, please
use the qcrypt status output. Sets a zero exit code, if a qcrypt daemon is running and a non-zero exit code otherwise.
help
print this help"
exit 1
}
#parseIniKey [chain id] [key] [type] [required] [fallback]
#Parse ini information to CHAINS2INFO.
#[chain id]: Chain to do the parsing for.
#[key]: Name of the ini key to parse.
#[type]: 1=string, 2=bool, 3=int
#[required]: whether the value is required (0) or not
#[fallback]: fallback value to set if no value is specified (for the optional ones only) (default: empty)
#returns: Prints errors and sets a non-zero exit code if they occur.
function parseIniKey {
local chainId="$1"
local key="$2"
local type="$3"
local required="${4:-0}"
local fallback="$5"
local ret=0
case $type in
1)
CHAINS2INFO["${chainId}_$key"]="$(b_ini_getString "$key")"
ret=$?
;;
2)
CHAINS2INFO["${chainId}_$key"]="$(b_ini_getBool "$key")"
ret=$?
;;
3)
CHAINS2INFO["${chainId}_$key"]="$(b_ini_getInt "$key")"
ret=$?
;;
*)
B_ERR="Unexpected type: $type"
B_E
esac
case $ret in
0)
#NOTE: the value may still be ""
:
;;
1)
if [ $required -eq 0 ] ; then
echo "Missing: $key"
return 1
fi
;;
2)
echo "Parsing error: $key"
return 2
;;
*)
B_ERR="Unexpected exit code: $ret"
B_E
esac
#set fallback if required
if [ -z "${CHAINS2INFO["${chainId}_$key"]}" ] ; then
if [ -n "$fallback" ] ; then
CHAINS2INFO["${chainId}_$key"]="$fallback"
elif [ $required -eq 0 ] ; then
echo "Missing: $key"
return 1
fi
fi
return 0
}
#parseConfigFile [file]
#Parse the given configuration file and update the current configuration state accordingly.
#[file]: Full path to the ini file to parse.
#returns: Nothing, but triggers [B_E](#B_E) on errors.
#@B_E
#@StateChanging
function parseConfigFile {
local file="$1"
b_ini_read "$file" || { B_ERR="Failed to parse the file $file." ; B_E ; }
local chainId="${file##*/}"
chainId="${chainId%.ini}"
[ -z "$chainId" ] && B_ERR="Empty chain ID. Programming mistake?!" && B_E
CHAINS+=("$chainId")
CHAINS2INFO["${chainId}_config file"]="$file"
#populate parts of CHAINS2INFO
local errCnt=0
parseIniKey "$chainId" "source vm" 1 0 || ((errCnt+=1))
parseIniKey "$chainId" "source device" 1 1 || ((errCnt+=1))
parseIniKey "$chainId" "source mount point" 1 1 || ((errCnt+=1))
parseIniKey "$chainId" "source file" 1 0 || ((errCnt+=1))
parseIniKey "$chainId" "key" 1 0 || ((errCnt+=1))
local i=
for ((i=1;;i++)) ; do
if [ $i -eq 1 ] ; then
parseIniKey "$chainId" "destination vm $i" 1 0 || ((errCnt+=1))
else
#probe first, then parse
(parseIniKey "$chainId" "destination vm $i" 1 0 &> /dev/null) || break
parseIniKey "$chainId" "destination vm $i" 1 0
fi
parseIniKey "$chainId" "destination inj $i" 1 1 || ((errCnt+=1))
parseIniKey "$chainId" "destination opt $i" 1 1 || ((errCnt+=1))
done
parseIniKey "$chainId" "destination mount point" 1 1 || ((errCnt+=1))
parseIniKey "$chainId" "autostart" 2 1 "1" || ((errCnt+=1))
#NOTE: read-only is set to true by default to prevent accidents (always make the more secure alternative the default)
parseIniKey "$chainId" "read-only" 2 1 "0" || ((errCnt+=1))
parseIniKey "$chainId" "startup interval" 3 1 "300" || ((errCnt+=1))
parseIniKey "$chainId" "pre open command" 1 1 || ((errCnt+=1))
parseIniKey "$chainId" "post open command" 1 1 || ((errCnt+=1))
parseIniKey "$chainId" "pre close command" 1 1 || ((errCnt+=1))
parseIniKey "$chainId" "post close command" 1 1 || ((errCnt+=1))
#check for errors
[ $errCnt -ne 0 ] && B_ERR="Exiting due to the above configuration errors." && B_E
#validate keys
local toCheck=("source vm" "source device" "source mount point" "source file" "key" "destination mount point" "autostart" "read-only" "startup interval" "pre open command" "post open command" "pre close command" "post close command")
for ((i=1;i<=10;i++)) ; do
toCheck+=("destination vm $i")
toCheck+=("destination inj $i")
toCheck+=("destination opt $i")
done
b_ini_assertNames "" "${toCheck[@]}"
#generate open, close & status commands for the chain (required for initializeActiveChains)
generateCommandsFor "$chainId"
#add chain mutex
CHAINS2INFO["${chainId}_mutex"]="$(b_mtx_create)"
#populate VMS2CHAINS (may add duplicates for circular chains) & set CHAINS2INFO["${chainId}_final destination vm"]
local vm="${CHAINS2INFO["${chainId}_source vm"]}"
VMS2CHAINS["$vm"]="$chainId"$'\n'"${VMS2CHAINS["$vm"]}"
for ((i=1;;i++)) ; do
vm="${CHAINS2INFO["${chainId}_destination vm $i"]}"
[ -z "$vm" ] && break
VMS2CHAINS["$vm"]="$chainId"$'\n'"${VMS2CHAINS["$vm"]}"
CHAINS2INFO["${chainId}_final destination vm"]="$vm"
done
}
#generateCommandsFor [chain id]
#Generates the relevant qcrypt commands for the given chain and stores them inside CHAINS2INFO.
#Make sure to call it again whenever CHAINS2INFO changes (e.g. the source mount point happens to be a different one).
#[chain id]: Chain to generate commands for.
#returns: Nothing, but triggers [B_E](#B_E) on errors.
#@B_E
function generateCommandsFor {
local chainId="$1"
#generate generic argument chain common to all commands
local srcFull="${CHAINS2INFO["${chainId}_source mount point"]}/${CHAINS2INFO["${chainId}_source file"]}"
local argChain=
printf -v argChain '%q %q %q' "${CHAINS2INFO["${chainId}_source vm"]}" "$srcFull" "${CHAINS2INFO["${chainId}_key"]}"
local i=
local vm=
local options=""
local inj=
local copt=
for ((i=1;;i++)) ; do
vm="${CHAINS2INFO["${chainId}_destination vm $i"]}"
[ -z "$vm" ] && break
printf -v argChain '%s %q' "$argChain" "$vm"
inj="${CHAINS2INFO["${chainId}_destination inj $i"]}"
if [ -n "$inj" ] ; then
printf -v options '%s --inj %q %q' "$options" "$vm" "$inj"
fi
copt="${CHAINS2INFO["${chainId}_destination opt $i"]}"
if [ -n "$copt" ] ; then
[[ "$copt" == *"'"* ]] && B_ERR="Found a disallowed single quote inside the cryptsetup option: $copt" && B_E
printf -v options "%s --cy %q '%s'" "$options" "$vm" "$copt"
fi
done
#open command
local cmd=
[ ${CHAINS2INFO["${chainId}_autostart"]} -eq 0 ] && options="$options -a"
[ ${CHAINS2INFO["${chainId}_read-only"]} -eq 0 ] && options="$options --ro"
local statusOptions=""
[ -n "${CHAINS2INFO["${chainId}_destination mount point"]}" ] && printf -v options '%s --mp %q' "$options" "${CHAINS2INFO["${chainId}_destination mount point"]}" && printf -v statusOptions '%s --mp ""' "$statusOptions"
printf -v cmd '%q %s open -- %s' "$QCRYPT" "$options" "$argChain"
CHAINS2INFO["${chainId}_open"]="$cmd"
#status command
printf -v cmd '%q status%s -- %s' "$QCRYPT" "$statusOptions" "$argChain"
CHAINS2INFO["${chainId}_status"]="$cmd"
#close command
printf -v cmd '%q close --force -- %s' "$QCRYPT" "$argChain"
CHAINS2INFO["${chainId}_close"]="$cmd"
}
#getConfigFolder [target]
#Get the configuration folder for the given target.
#returns: The folder or errors out with [B_E](#B_E), if no such config folder exists.
#@B_E
function getConfigFolder {
local target="${1:-default}"
local targetFolder="$QCRYPTD_CONF/$target"
[ -d "$targetFolder" ] && { echo "$targetFolder" ; return 0 ; } || targetFolder="$QCRYPTD_CONF_2/$target"
[ -d "$targetFolder" ] && { echo "$targetFolder" ; return 0 ; } || { B_ERR="No configuration for the target $target found in either $QCRYPTD_CONF or $QCRYPTD_CONF_2." ; B_E ; }
}
#parseConfigFiles [target]
#Parse all config files for the given target.
#[target]: Configuration target.
#returns: Nothing, but triggers [B_E](#B_E) on errors.
#@B_E
#@StateChanging
function parseConfigFiles {
local target="${1:-default}"
local targetFolder=""
targetFolder="$(getConfigFolder "$target")" || { B_ERR="The configuration for the target $target does not exist." ; B_E ; }
local file=
for file in "$targetFolder/"*.ini ; do
if [ -f "$file" ] ; then
parseConfigFile "$file"
fi
done
#remove duplicates from VMS2CHAINS
local list=
local key=
for key in "${!VMS2CHAINS[@]}" ; do
list="${VMS2CHAINS["$key"]}"
list="$(echo "$list" | sort -u)"
VMS2CHAINS["$key"]="$list"
done
return 0
}
#see check @usage
#@B_E
function checkC {
parseConfigFiles "$(b_args_get 1)"
if [ $DEBUG -eq 0 ] ; then
declare -p CHAINS
declare -p CHAINS2INFO
declare -p VMS2CHAINS
fi
echo "All good."
exit 0
}
#see chains @usage
#@B_E
function chainsC {
parseConfigFiles "$(b_args_get 1)"
local execute=1
b_args_getOption "-e" > /dev/null && execute=0
local showConfigFiles=0
b_args_getOption "-n" > /dev/null && showConfigFiles=1
local ret=0
local chain=
local badChains=""
if [ $execute -eq 0 ] ; then
badChains="$(getBadChains "$(b_arr_toList "${CHAINS[@]}")" 2> /dev/null)" || { B_ERR="Failed to obtain the list of chains which are in a bad state." ; B_E ; }
fi
local prefix=""
local format='%s%s'$'\n'
[ $showConfigFiles -eq 0 ] && format='%-25s%s'$'\n'
for chain in "${CHAINS[@]}" ; do
[ $showConfigFiles -eq 0 ] && prefix="${CHAINS2INFO["${chain}_config file"]##*/}: "
printf "$format" "$prefix" "${CHAINS2INFO["${chain}_status"]}"
if [ $execute -eq 0 ] ; then
local state="good"
b_listContains "$badChains" "$chain" && state="bad" && ret=$(( $ret +1 ))
printf ' state: %s'$'\n' "$state"
fi
done
exit $ret
}
#see stop @usage
#@B_E
function stopC {
#parse params
local termSignal="SIGUSR1"
b_args_getOption "-c" > /dev/null && termSignal="SIGUSR2"
b_daemon_stop "$DID" "$termSignal" 0
}
#logError [message] [notify] [notification summary]
#[notify]: If set to 0, send a user notification as well (default: 1)
function logError {
local msg="$1"
local notify="${2:-1}"
local summary="${3:-"$B_SCRIPT_NAME: ERROR"}"
#NOTE: we write to stderr (which is logged in debug mode) to avoid conflicts with echoed return values from inside functions
[ $DEBUG -eq 0 ] && >&2 echo "$SECONDS [$BASHPID] ERROR: $msg"
logger -p daemon.err -t "$B_SCRIPT_NAME" "[$BASHPID] $msg (target: $CONF_TARGET)"
if [ $notify -eq 0 ] ; then
b_notify_sendNoError -u critical -t 60000 "$summary" "$msg" &
disown
fi
return 0
}
#logInfo [message] [notify] [notification summary]
#[notify]: If set to 0, send a user notification as well (default: 1)
function logInfo {
local msg="$1"
local notify="${2:-1}"
local summary="${3:-"$B_SCRIPT_NAME: INFO"}"
#NOTE: we write to stderr (which is logged in debug mode) to avoid conflicts with echoed return values from inside functions
[ $DEBUG -eq 0 ] && >&2 echo "$SECONDS [$BASHPID] INFO: $msg"
logger -p daemon.notice -t "$B_SCRIPT_NAME" "[$BASHPID] $msg (target: $CONF_TARGET)"
if [ $notify -eq 0 ] ; then
b_notify_sendNoError "$summary" "$msg" &
disown
fi
return 0
}
function logState {
[ $DEBUG -eq 0 ] && echo $'\n'"STATE BEGIN"
logInfo "$(declare -p CHAINS)"
logInfo "$(declare -p CHAINS2INFO)"
logInfo "$(declare -p VMS2CHAINS)"
logInfo "$(declare -p ACTIVE_VMS)"
logInfo "$(declare -p ACTIVE_CHAINS)"
logInfo "$(declare -p LAST_ATTACHED)"
logInfo "$(declare -p SUPPRESSION)"
#logInfo "$(qvm-ls -q --no-spinner)"
#logInfo "$(qvm-block ls)"
[ $DEBUG -eq 0 ] && echo "STATE END"$'\n'
}
#loggingDaemonErrorHandler [error out]
function loggingDaemonErrorHandler {
local errorOut=${1:-0}
#set the proper exit code
if [ $errorOut -eq 0 ] ; then
#only the daemon itself should cause FATALs
if [ $BASHPID -eq $$ ] ; then
[ $DEBUG -eq 0 ] && logState
logError "FATAL: $B_ERR" 0
logError "Daemon exiting..."
else
logError "$B_ERR Child thread exiting..."
fi
return 2
else
logInfo "$B_ERR"
return 1
fi
}
#isRunning [vm] [allow paused]
#Check whether the given VM is running according to our internal state.
#[allow paused]: If set to 0, also consider paused VMs to be running (default: 1).
#returns: A zero exit code, if it is running and a nonzero exit code otherwise.
function isRunning {
local vm="$1"
local allowPaused="${2:-1}"
local ePaused=0
[ $allowPaused -eq 0 ] && ePaused=2
[[ -n "${ACTIVE_VMS["$vm"]}" && ( ${ACTIVE_VMS["$vm"]} -eq 0 || ${ACTIVE_VMS["$vm"]} -eq $ePaused ) ]]
}
#isPaused [vm]
#Check whether the given VM is paused according to our internal state.
#returns: A zero exit code, if it is running and a nonzero exit code otherwise.
function isPaused {
local vm="$1"
[ -n "${ACTIVE_VMS["$vm"]}" ] && [ ${ACTIVE_VMS["$vm"]} -eq 2 ]
}
#chainVMsAreRunning [chain id]
#Check whether all VMs of the given chain which are necessary to open it are running.
#[chain id]: ID of a single chain.
#returns: Sets a zero exit code, if all involved VMs are running and a non-zero exit code otherwise.
function chainVMsAreRunning {
local chain="$1"
#check source VM & final destination VM (all intermediaries are autostarted by qcrypt)
isRunning "${CHAINS2INFO["${chain}_source vm"]}" && isRunning "${CHAINS2INFO["${chain}_final destination vm"]}"
}
#prepareChainOpen [chain id]
#Do all preparations necessary to open the given qcrypt chain, which _must_ run in the main thread/daemon as they are _not_ thread safe.
#[chain id]: ID of a single chain.
#returns: Sets a zero exit code, if the preparations were successful and a chain start may now proceed. Otherwise a non-zero exit code is set. Unexpected errors are logged.
#@StateChanging
function prepareChainOpen {
#NOTE: qcrypt does any autostart, the started VMs will trigger new events and the daemon state will be updated accordingly --> nothing to do for that
local chain="$1"
local srcVM="${CHAINS2INFO["${chain}_source vm"]}"
local srcDev="${CHAINS2INFO["${chain}_source device"]}"
local srcMp="${CHAINS2INFO["${chain}_source mount point"]}"
#special case: autostart enabled & source VM not running (yet)
local ret=3
if [ ${CHAINS2INFO["${chain}_autostart"]} -eq 0 ] && ! isRunning "$srcVM" ; then
b_setBE 1
b_dom0_ensureRunning "$srcVM"
ret=$?
b_resetErrorHandler 1
if [ $ret -eq 0 ] ; then
ACTIVE_VMS["$srcVM"]=0
else
logError "Failed to start the VM $srcVM. Original error: $B_ERR"
B_ERR=""
return $ret
fi
fi
#mount source device, if necessary
#NOTE: _not_ thread safe (e.g. b_dom0_mountIfNecessary for the same source VM & device)!
if [ -n "$srcDev" ] && [ -n "$srcMp" ] && [[ "$srcMp" != "/" ]] ; then
b_setBE 1
#NOTE: for security reasons we enforce our mount point (we don't want to read the untrusted VM output and pass it to our internal state)
b_silence b_dom0_mountIfNecessary "$srcVM" "$srcDev" "$srcMp" 0
ret=$?
b_resetErrorHandler 1
[ $ret -ne 0 ] && logError "Failed to mount the source device $srcVM:$srcDev to $srcMp (chain $chain). Device not available? Original error: $B_ERR"
B_ERR=""
return $ret
else
#local file, opening should work
return 0
fi
}
#updateChainState [chain] [operation] [wait flag]
#Updates the ACTIVE_CHAINS array for the given chain with the latest information available.
#[operation]: 1=open, 0=close
#[wait flag]: If set to 0, even wait for on-going operations to complete (default: 1).
#returns: Nothing.
function updateChainState {
local chain="$1"
local op="$2"
local waitFlag="${3:-1}"
local ret=
local pid=
[ $op -eq 1 ] && pid="${CHAINS2INFO["${chain}_last open pid"]}" || pid="${CHAINS2INFO["${chain}_last close pid"]}"
[ -z "$pid" ] && return 0
if [ $waitFlag -ne 0 ] ; then
#is the background process still running?
b_proc_childExists "$pid" && return 0
fi
if wait "$pid" ; then
#we thought the chain open/closed anyway --> nothing to do (especially since we might have closed in the meantime)
:
else
#the last chain open/close failed --> update the internal state
ACTIVE_CHAINS["$chain"]=$op
fi
[ $op -eq 1 ] && CHAINS2INFO["${chain}_last open pid"]="" || CHAINS2INFO["${chain}_last close pid"]=""
return 0
}
#isActive [chain] [wait flag]
#Check whether the given chain is active according to the latest information.
#Must be run in the foreground by the daemon itself.
#[wait flag]: If set to 0, even wait for on-going operations to complete (default: 1).
#returns: A zero exit code if and only if it is active according to the latest information.
function isActive {
local chain="$1"
local waitFlag="$2"
updateChainState "$chain" 0 "$waitFlag"
updateChainState "$chain" 1 "$waitFlag"
[ -n "${ACTIVE_CHAINS["$chain"]}" ] && [ ${ACTIVE_CHAINS["$chain"]} -eq 0 ]
}
#openSingleChain [chain id]
#Open a single chain in the foreground. Helper function for [openChains](#openChains), which must be used over this one.
#returns: An exit code of 0, if and only if the chain was successfully opened. Errors are logged.
#@B_E
function openSingleChain {
local chain="$1"
#make sure previous operations are completed
local release=
release="$(b_mtx_waitFor "${CHAINS2INFO["${chain}_mutex"]}" "$BASHPID")" || { B_ERR="Failed to obtain a mutex." ; B_E ; }
#shellcheck disable=SC2064
trap "$release" EXIT RETURN
#execute the pre open command
local cmd="${CHAINS2INFO["${chain}_pre open command"]}"
if [ -n "$cmd" ] ; then
eval "$cmd" || { B_ERR="The pre open command for the chain $chain returned a non-zero exit code. Thus aborting the chain open process." ; B_E ; }
fi
#open the chain
cmd="${CHAINS2INFO["${chain}_open"]}"
[ -z "$cmd" ] && B_ERR="Missing open command for chain $chain. Programming error?!" && B_E
local notifySummary="$B_SCRIPT_NAME status: $chain"
if ! eval "$cmd" ; then
local msg="Failed to open the chain $chain. Closing again..."
logError "$msg" 0 "$notifySummary"
#NOTES:
# - failed open operations often result in partially open chains --> thus we attempt to close
# - we don't need to use mutexes etc. as we still have the open mutex
closeSingleChainNaively "$chain"
B_ERR="$msg"
B_E
fi
#execute the post open command
cmd="${CHAINS2INFO["${chain}_post open command"]}"
[ -n "$cmd" ] && eval "$cmd"
logInfo "Opened the chain $chain." 0 "$notifySummary"
return 0
}
#openChains [wait flag] [chain id 1] ... [chain id n]
#[wait flag]: If set to 0, wait for on-going chain status changes possibly affecting our decision to open a chain.
#Check whether it makes sense to open the given chains and if so, attempt it.
#returns: Nothing. Errors are logged.
function openChains {
local waitFlag="$1"
shift
local chain=
for chain in "$@" ; do
#filter active chains
isActive "$chain" "$waitFlag" && continue
#filter chains with shut down VMs
if [ ${CHAINS2INFO["${chain}_autostart"]} -ne 0 ] ; then
chainVMsAreRunning "$chain" || continue
fi
#do not attempt to open chains with on-going close operation
local last="${CHAINS2INFO["${chain}_last close pid"]}"
[ -n "$last" ] && continue
#do not open too often to prevent continuous re-open attempts in case of errors
local last="${CHAINS2INFO["${chain}_last open"]}"
[ -n "$last" ] && [ $(( $SECONDS - $last )) -le ${CHAINS2INFO["${chain}_startup interval"]} ] && continue
#prepare (not thread safe, so must be run here)
prepareChainOpen "$chain" || continue
#open in background
#NOTE: we aggressively assume that the chain goes active in the background, but re-check that assumption with isActive whenever needed
logInfo "Attempting to open the chain $chain..."
openSingleChain "$chain" < /dev/null &
ACTIVE_CHAINS["$chain"]=0
CHAINS2INFO["${chain}_last open"]="$SECONDS"
CHAINS2INFO["${chain}_last open pid"]="$!"
done
return 0
}
#openChainsByVM [v] [all]
#Attempt to open all chains related to the given VM.
#An implementation of openChainsByVM(v,all).
#[v]: A VM name.
#[all]: all VMs are relevant (default, 0); if set to 1, only source VMs are relevant
#returns: Nothing. Startup errors not caused by parts of the chain not being available will be logged.
function openChainsByVM {
local vm="$1"
local all="${2:-0}"
#get the applicable chains
local chains="${VMS2CHAINS["$vm"]}"
local chain=
local toOpen=()
while b_readLine chain ; do
[ -z "$chain" ] && continue
[ $all -ne 0 ] && [[ "$vm" != "${CHAINS2INFO["${chain}_source vm"]}" ]] && continue
toOpen+=("$chain")
done <<< "$chains"
openChains 1 "${toOpen[@]}"
}
#closeSingleChainNaively [chain]
#Close a single chain in the foreground naively (no checks, mutex obtaining, ... whatsoever). Helper function only. Usually [closeChains](#closeChains) should be used.
#returns: An exit code of 0, if and only if the chain was successfully closed.
#@B_E
function closeSingleChainNaively {
local chain="$1"
local cmd="${CHAINS2INFO["${chain}_close"]}"
[ -z "$cmd" ] && B_ERR="Missing close command for chain $chain. Programming error?!" && B_E
eval "$cmd" || logError "Failed to correctly close the chain $chain. Assuming it closed anyway and proceeding..."
return 0
}
#closeSingleChain [chain id] [force flag]
#Close a single chain in the foreground. Helper function for [closeChains](#closeChains), which must be used over this one.
#returns: An exit code of 0, if and only if the chain was successfully closed.
#@B_E
function closeSingleChain {
local chain="$1"
local forceFlag="${2:-1}"
if [ $forceFlag -ne 0 ] ; then
#only close chains whose final destination VM is down & warn the user about others
#Reason: The close would shut down the destination VM, but the user might still be working inside it.
#we double check with hasRecentShutdownAttempt() & b_dom0_isRunning() as multiple VMs may have been shut down at the same time and we received only the first few events so far
#(in particular VMs shut down from inside the VM / not via qvm-shutdown appear to require the b_dom0_isRunning check)
local final="${CHAINS2INFO["${chain}_final destination vm"]}"
b_setBE 1
if isRunning "$final" 0 && ! hasRecentShutdownAttempt "$final" && sleep 2 && b_dom0_isRunning "$final" &> /dev/null ; then
logError "The qcrypt chain $chain is not working anymore and should be closed. However it seems that the $final VM is still running."$'\n'"Please shut it down manually to fix the issue. qcryptd should close the chain afterwards." 0
exit 3
fi
b_resetErrorHandler
fi
#make sure previous operations are completed
local release=
release="$(b_mtx_waitFor "${CHAINS2INFO["${chain}_mutex"]}" "$BASHPID")" || { B_ERR="Failed to obtain a mutex." ; B_E ; }
#shellcheck disable=SC2064
trap "$release" EXIT RETURN
#execute the pre close command
local cmd="${CHAINS2INFO["${chain}_pre close command"]}"
[ -n "$cmd" ] && eval "$cmd"
#close the chain
closeSingleChainNaively "$chain"
#execute the post close command
cmd="${CHAINS2INFO["${chain}_post close command"]}"
[ -n "$cmd" ] && eval "$cmd"
logInfo "Closed the chain $chain." 0 "$B_SCRIPT_NAME status: $chain"
return 0
}
#closeChains [force flag] [wait flag] [chain id 1] ... [chain id n]
#[force flag]: Force a close attempt regardless of the current state. May shut down VMs. Default: 1
#[wait flag]: If set to 0, wait for on-going chain status changes possibly affecting our decision to close a chain. Default: 1
#returns: Nothing.
function closeChains {
local forceFlag="${1:-1}"
local waitFlag="${2:-1}"
shift 2
local chain=
for chain in "$@" ; do
#filter inactive chains
[ $forceFlag -ne 0 ] && ! isActive "$chain" "$waitFlag" && continue
#close in background
#NOTE: this will shut down _ALL_ destination VMs in that chain
logInfo "Attempting to close the chain $chain..."
closeSingleChain "$chain" "$forceFlag" < /dev/null &
#NOTE: the next open will wait for the last close and another overlapping close is not possible since the chain is now marked inactive
CHAINS2INFO["${chain}_last close"]="$SECONDS"
CHAINS2INFO["${chain}_last close pid"]="$!"
ACTIVE_CHAINS["$chain"]=1
done
return 0
}
#getBadChains [list]
#Check the status of all given chains and filter the chain list to only contain the ones with a bad status.
#[list] List of chains to check the status for.
#returns: Filtered list of chains. Errors are logged.
function getBadChains {
local list="$1"
local chain=
local cmd=
#chain id --> pid
declare -A pids=()
while b_readLine chain ; do
[ -z "$chain" ] && continue
#NOTE: all VMs are likely running (we only get here for device attachments), no need to check
#the open operation might not have completed yet
local pid="${CHAINS2INFO["${chain}_last open pid"]}"
if [ -n "$pid" ] ; then
if b_proc_childExists "$pid" ; then
[ $DEBUG -eq 0 ] && logInfo "$chain: Chain open still on-going @${pid}."
continue
else
CHAINS2INFO["${chain}_last open pid"]=""
fi
fi
cmd="${CHAINS2INFO["${chain}_status"]}"
[ -z "$cmd" ] && B_ERR="Missing status command for chain $chain. Programming error?!" && B_E
[ $DEBUG -eq 0 ] && logInfo "$chain: Checking the status...."
eval "$cmd" &> /dev/null &
pids["$chain"]=$!
done <<< "$list"
for chain in "${!pids[@]}" ; do
if wait "${pids["$chain"]}" ; then
[ $DEBUG -eq 0 ] && logInfo "$chain: Good status."
else
echo "$chain"
fi
done
return 0
}
#filterChainsWithPausedDest [list]
#[list]: List of chains.
#returns: The input list; those chains with a paused destination VM were removed.
function filterChainsWithPausedDest {
local list="$1"
local chain=
while b_readLine chain ; do
[ -z "$chain" ] && continue
local final="${CHAINS2INFO["${chain}_final destination vm"]}"
if isPaused "$final" ; then
[ $DEBUG -eq 0 ] && logInfo "$chain: The final destination VM $final appears to be paused. Ignoring."
else
echo "$chain"
fi
done <<< "$list"
return 0
}
#closeChainsByVM [v] [shutdown flag] [paused flag]
#Close all chains related to the given VM.
#An implementation of closeChainsByVM(v,VM shutdown=0|1).
#[v]: A VM name.
#[shutdown flag]: If the VM [v] was shut down and the chain requires closing for that reason (0) or the chains are expected to have other issues (e.g. device detached) (default: 1).
#[paused flag]: If the VM [v] was paused and the chain requires closing for that reason (0) or the chains are expected to have other issues (e.g. device detached) (default: 1).
#returns: Nothing. Close errors will be logged.
function closeChainsByVM {
local vm="$1"
local shutdown="${2:-1}"
local paused="${3:-1}"
#get the applicable chains
local chains=
if [ $shutdown -eq 0 ] ; then
chains="${VMS2CHAINS["$vm"]}"
elif [ $paused -eq 0 ] ; then
#special case: do not consider the chain broken, if the final destination VM is paused
#reason: it'll fail, but in fact qcryptd should be alright once the user unpauses the VM --> unclear state --> do nothing now and check again on unpause
#for paused intermediate VMs we still error out (unless their dest is paused) as that may cause data loss otherwise
chains="$(filterChainsWithPausedDest "${VMS2CHAINS["$vm"]}")"
else
chains="$(filterChainsWithPausedDest "${VMS2CHAINS["$vm"]}")" #see above
chains="$(getBadChains "$chains")"
fi
[ $DEBUG -eq 0 ] && logInfo "Chains about to be closed (if active):"$'\n'"$chains"$'\n'
local chain=
local toClose=()
while b_readLine chain ; do
[ -z "$chain" ] && continue
toClose+=("$chain")
done <<< "$chains"
closeChains 1 1 "${toClose[@]}"
return 0
}
#initializeActiveVMs
#Initialize the ACTIVE_VMS map with the currently active VMs.
#returns: A zero exit code on success and a non-zero exit code otherwise. Unexpected errors will trigger [B_E](#B_E).
#@B_E
#@StateChanging
function initializeActiveVMs {
local running=
running="$(qvm-ls --running -O NAME --raw-list)" || { B_ERR="Faled to execute qvm-ls." ; B_E ; }
local vm=
while b_readLine vm ; do
[ -z "$vm" ] && continue
[[ "$vm" == "dom0" ]] && continue
ACTIVE_VMS["$vm"]=0
done <<< "$running"
local paused=
paused="$(qvm-ls --paused -O NAME --raw-list)" || { B_ERR="Faled to execute qvm-ls." ; B_E ; }
while b_readLine vm ; do
[ -z "$vm" ] && continue
[[ "$vm" == "dom0" ]] && continue
ACTIVE_VMS["$vm"]=2
done <<< "$paused"
return 0
}
#initializeActiveChains
#Initialize the ACTIVE_CHAINS map with the currently active qcrypt chain ids.
#Currently depends on a previous run of [initializeActiveVMs](#initializeActiveVMs).
#returns: A zero exit code on success and a non-zero exit code otherwise. Unexpected errors will trigger [B_E](#B_E).
#@B_E
#@StateChanging
function initializeActiveChains {
local chain=
local cmd=
for chain in "${CHAINS[@]}" ; do
cmd="${CHAINS2INFO["${chain}_status"]}"
[ -z "$cmd" ] && B_ERR="Failed to find the status command. Programming error?!" && B_E
chainVMsAreRunning "$chain" && eval "$cmd" && ACTIVE_CHAINS["$chain"]=0
done
return 0
}
#needsSuppression [vm] [event name] [event time]
#Check whether the given event requires to be suppressed as it did recently occur already.
#[event time]: in seconds since EPOCH
#returns: A zero exit code, if the event should be suppressed.
function needsSuppression {
local vm="$1"
local eventName="$2"
local timestamp="$3"
local key="${vm}_${eventName}"