forked from ElliotKillick/qvm-create-windows-qube
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathqvm-create-windows-qube
executable file
·466 lines (398 loc) · 15.6 KB
/
qvm-create-windows-qube
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
#!/bin/bash
# Copyright (C) 2023 Elliot Killick <[email protected]>
# Licensed under the MIT License. See LICENSE file for details.
# Test for 4-bit color (16 colors)
if [ "0$(tput colors 2> /dev/null)" -ge 16 ]; then
RED='\033[0;31m'
BLUE='\033[0;34m'
GREEN='\033[0;32m'
NC='\033[0m'
fi
# Avoid printing messages as potential terminal escape sequences
echo_ok() { printf "%b%s%b" "${GREEN}[+]${NC} " "$1" "\n" >&2; }
echo_info() { printf "%b%s%b" "${BLUE}[i]${NC} " "$1" "\n" >&2; }
echo_err() { printf "%b%s%b" "${RED}[!]${NC} " "$1" "\n" >&2; }
error() {
exit_code="$?"
echo_err "An unexpected error has occurred! Exiting..."
exit "$exit_code"
}
trap error ERR
# Allow externally setting custom resources qube for advanced users
resources_qube="${RESOURCES_QUBE:-windows-mgmt}"
resources_dir="/home/user/qvm-create-windows-qube"
wait_for_shutdown() {
# There is a small delay upon booting a qube before qvm-check will detect it as running
# To account for this as well as scenarios where the qube is already running and is shutting down we need both loops
until qvm-check --running "$qube" &> /dev/null; do
sleep 1
done
while qvm-check --running "$qube" &> /dev/null; do
sleep 1
done
}
airgap() {
qvm-firewall "$qube" del accept
qvm-firewall "$qube" add drop
qvm-prefs "$qube" netvm "$netvm"
}
break_airgap() {
echo_info "Breaking air gap so Windows can connect to the Internet..."
qvm-firewall "$qube" del drop
qvm-firewall "$qube" add accept
}
is_dom0() {
[ "$(hostname)" == "dom0" ]
}
print_isos() {
echo_info "Available ISOs (Make sure to download them with Mido as instructed in the README):"
qvm-run -p "$resources_qube" "cd '$resources_dir/windows/isos' && find -type f -name '*.iso' -printf '%P\n'"
}
print_answer_files() {
echo_info "Available answer files:"
qvm-run -p "$resources_qube" "cd '$resources_dir/windows/answer-files' && find -type f -name '*.xml' -printf '%P\n'"
exit 1
}
usage() {
echo "Usage: $(basename "$0") [options] -i <iso> -a <answer file> <name>"
echo " -h, --help"
echo " -c, --count <number> Number of Windows qubes with given basename desired"
echo " -t, --template Make this qube a TemplateVM instead of a StandaloneVM"
echo " -n, --netvm <qube> NetVM for Windows to use"
echo " -s, --seamless Enable seamless mode persistently across reboots"
echo " -o, --optimize Optimize Windows by disabling unnecessary functionality for a qube"
echo " -y, --spyless Configure Windows telemetry settings to respect privacy"
echo " -w, --whonix Apply Whonix recommended settings for a Windows-Whonix-Workstation"
echo " -p, --packages <packages> Comma-separated list of packages to pre-install (see available packages at: https://chocolatey.org/packages)"
echo " -P, --pool <name> LVM storage pool to install Windows on (https://www.qubes-os.org/doc/secondary-storage/)"
echo " -i, --iso <file> Windows media to automatically install and setup"
echo " -a, --answer-file <xml file> Settings for Windows installation"
if is_dom0; then
echo ""
print_isos
echo ""
print_answer_files
fi
}
# Option strings
short="hc:tn:soywp:P:i:a:"
long="help,count:,template,netvm:,seamless,optimize,spyless,whonix,packages:,pool:,iso:,answer-file:"
# Read options
if ! opts=$(getopt --options=$short --longoptions=$long --name "$0" -- "$@"); then
exit 1
fi
eval set -- "$opts"
# Set defaults
count="1"
# Put options into variables
while true; do
case "$1" in
-h | --help)
usage
exit
;;
-c | --count)
count="$2"
shift
;;
-t | --template)
template="true"
;;
-n | --netvm)
netvm="$2"
shift
;;
-s | --seamless)
seamless="true"
;;
-o | --optimize)
optimize="true"
;;
-y | --spyless)
spyless="true"
;;
-w | --whonix)
whonix="true"
;;
-p | --packages)
packages="$2"
shift
;;
-P | --pool)
pool="$2"
shift
;;
-i | --iso)
iso="$2"
shift
;;
-a | --answer-file)
answer_file="$2"
shift
;;
--)
shift
break
;;
esac
shift
done
# Handle positional arguments
if [ $# != 1 ]; then
usage >&2
exit 1
fi
name="$1"
# Validate this is Dom0
if ! is_dom0; then
echo_err "This script must be run in Dom0"
exit 1
fi
# Validate Qubes OS version support
qubes_version="$(awk -F"[()]" '{ print $2 }' /etc/qubes-release)"
case "$qubes_version" in
"R4.0")
;;
"R4.1" | "R4.2")
if ! [ -f "/usr/lib/qubes/qubes-windows-tools.iso" ]; then
echo_err "Qubes version $qubes_version is supported. However, Qubes OS does not currently offer an official build of Qubes Windows Tools for $qubes_version. To continue, please build Qubes Windows Tools yourself and place it at: /usr/lib/qubes/qubes-windows-tools.iso"
exit 1
fi
;;
*)
echo_err "Qubes version is unknown and therefore support cannot be guaranteed. Exiting..."
exit 1
;;
esac
# Validate name
if [ "$count" == 1 ]; then
if qvm-check "$name" &> /dev/null; then
echo_err "Qube already exists: $name"
exit 1
fi
fi
# Validate count
if ! [[ "$count" =~ ^[0-9]+$ ]]; then
echo_err "Count is not a number"
exit 1
elif [ "$count" -lt 1 ]; then
echo_err "Count should be 1 or more"
exit 1
fi
# Parse template
if [ "$template" == "true" ]; then
class="TemplateVM"
else
class="StandaloneVM"
fi
# Validate netvm
if [ "$netvm" ]; then
if ! qvm-check "$netvm" &> /dev/null; then
echo_err "NetVM does not exist: $netvm"
exit 1
elif [ "$(qvm-prefs "$netvm" provides_network)" != "True" ]; then
echo_err "Not a NetVM: $netvm"
exit 1
fi
fi
if ! qvm-check --running "$resources_qube" &> /dev/null; then
echo_info "Starting $resources_qube..."
if ! qvm-start "$resources_qube"; then
echo_err "Failed to start $qube! Not enough memory?"
exit 1
fi
fi
# Validate packages
if [ "$packages" ]; then
if ! [ "$netvm" ]; then
echo_err "A NetVM must be configured to use packages"
exit 1
fi
if qvm-tags "$netvm" list anon-gateway &> /dev/null; then
echo_err "Due to Chocolatey blocking Tor, packages cannot be used with NetVM: $netvm"
exit 1
fi
if [ "$choco_pkgs" ]; then
# Use same NetVM as configured for Windows VM to check if packages exist
# This way, there are no info leaks if an anon-gateway or custom VPN NetVM is configured
# Chocolatey doesn't block Tor here
IFS="," read -ra choco_pkgs_arr <<< "$choco_pkgs"
for package in "${choco_pkgs_arr[@]}"; do
if qvm-run -q "$netvm" "if [ \"\$(curl -so /dev/null -w '%{http_code}' 'https://community.chocolatey.org/api/v2/package/$package')\" != 404 ]; then exit 1; fi"; then
echo_err "Package not found: $package"
exit 1
fi
done
fi
fi
# Validate iso
if ! [ "$iso" ]; then
echo_err "ISO not specified"
print_isos
exit 1
elif ! qvm-run -q "$resources_qube" "cd '$resources_dir/windows/isos' && if ! [ -f '$iso' ]; then exit 1; fi"; then
echo_err "File not found in $resources_qube:$resources_dir/windows/isos: $iso"
print_isos
exit 1
fi
# Validate answer-file
if ! [ "$answer_file" ]; then
echo_err "Answer file not specified"
print_answer_files
exit 1
elif ! qvm-run -q "$resources_qube" "cd '$resources_dir/windows/answer-files' && if ! [ -f '$answer_file' ]; then exit 1; fi"; then
echo_err "File not found in $resources_qube:$resources_dir/windows/answer-files: $answer_file"
print_answer_files
exit 1
fi
# Put answer file into Windows media
echo_info "Preparing Windows media for automatic installation..."
if ! qvm-run -p "$resources_qube" "cd '$resources_dir/windows' && if ! [ -f out/$iso ]; then './create-media.sh' 'isos/$iso' 'answer-files/$answer_file'; fi"; then
echo_err "Failed to create media! Out of disk space? Exiting..."
exit 1
fi
# Create Windows qube the number of times specified using name as the basename if creating more than one
for (( counter = 1; counter <= count; counter++ )); do
if [ "$count" -gt 1 ]; then
qube="$name-$counter"
# If qube with that name already exists, keep incrementing the number until one that does not exist is found
i=0
while qvm-check "$qube" &> /dev/null; do
((i++)) || true
qube="$name-$i"
done
else
qube="$name"
fi
echo_info "Starting creation of $qube"
qvm-create --class "$class" --label red "$qube" -P "$pool"
qvm-prefs "$qube" virt_mode hvm
qvm-prefs "$qube" memory 2048 # Minimum starting with newer versions of Windows 10
qvm-prefs "$qube" maxmem 0 # Disable currently unstable Qubes memory manager
qvm-prefs "$qube" kernel ""
qvm-prefs "$qube" qrexec_timeout 999999 # Windows startup can take longer, especially if a chkdsk is performed. Also, to account for the duration of a Windows update
qvm-features "$qube" video-model cirrus
qvm-volume extend "$qube:root" 30GiB
qvm-volume extend "$qube:private" 10GiB # Minimum starting with newer versions of Windows 10
qvm-prefs "$qube" netvm ""
echo_info "Starting first part of Windows installation process..."
until qvm-start --cdrom "$resources_qube:$resources_dir/windows/out/$iso" "$qube"; do
echo_err "Failed to start $qube! Not enough memory? Retrying in 10 seconds..."
sleep 10
done
# Waiting for first part of Windows installation process to finish...
wait_for_shutdown
echo_info "Starting second part of Windows installation process..."
qvm-features --unset "$qube" video-model
until qvm-start "$qube"; do
echo_err "Failed to start $qube! Not enough memory? Retrying in 10 seconds..."
sleep 10
done
# Waiting for second part of Windows installation process to finish...
wait_for_shutdown
echo_info "Preparing Qubes Windows Tools for automatic installation..."
# Unpack latest QWT into auto-qwt
qvm-run -p "$resources_qube" "cat > '$resources_dir/tools/qwt-installer.iso'" < "/usr/lib/qubes/qubes-windows-tools.iso"
qvm-run -q "$resources_qube" "cd '$resources_dir/tools' && './unpack-qwt-installer.sh'"
# Create auto-qwt media
qvm-run -q "$resources_qube" "cd '$resources_dir/tools' && './pack-auto-qwt.sh'"
echo_info "Installing Qubes Windows Tools..."
# NetVM must be attached for Xen PV network driver setup
# However, to keep Windows air gapped for the entire setup we drop all packets at the firewall so Windows cannot connect to the Internet yet
if [ "$netvm" ]; then
airgap
fi
until qvm-start --cdrom "$resources_qube:$resources_dir/tools/auto-qwt.iso" "$qube"; do
echo_err "Failed to start $qube! Not enough memory? Retrying in 10 seconds..."
sleep 10
done
# Waiting for automatic shutdown after Qubes Windows Tools installation...
wait_for_shutdown
echo_info "Completing setup of Qubes Windows Tools..."
until qvm-start "$qube"; do
echo_err "Failed to start $qube! Not enough memory? Retrying in 10 seconds..."
sleep 10
done
# Wait until QWT installation is advertised to Dom0
until [ "$(qvm-features "$qube" os)" == "Windows" ]; do
sleep 1
done
# At this point, qvm-run is working
# Wait for app menu to synchronize (Automatically done once Qubes detects QWT)
command_pattern="/usr/bin/python3 /usr/bin/qvm-sync-appmenus $qube"
# Sync usually starts right away but due to a race condition we need both loops to make sure we catch when the sync begins and wait until it ends
until pgrep -fx "$command_pattern" &> /dev/null; do
sleep 1
done
while pgrep -fx "$command_pattern" &> /dev/null; do
sleep 1
done
# Some of the app menus may not appear in the XFCE Applications menu although they will show up in a "qvm-appmenus --get-available"
# This issue only seems to occur with Windows 10 (it didn't happen with Windows 7). More research is required
# Running this fixes it (rebooting the computer also fixes this issue)
qvm-appmenus --update --force "$qube" &> /dev/null
# Post QWT scripts
# Temporarily update qubes.Filecopy policy for resource qube and Windows qubes only, to allow copying post QWT scripts without user interaction
if [ "$qubes_version" == "R4.0" ]; then
# For R4.0: Prepend allowing policy to existing qubes.Filecopy file
policy="$resources_qube $qube allow"
policy_file="/etc/qubes-rpc/policy/qubes.Filecopy"
sed -i "1i$policy" "$policy_file"
else
# For R4.1 and later: Create a temporary allowing policy file based on the shell PID in the policy.d directory
policy="qubes.Filecopy * $resources_qube $qube allow"
policy_file="/etc/qubes/policy.d/25-qvm-create-windows-qube-$$.policy"
echo "$policy" | sudo tee "$policy_file" > /dev/null
fi
qvm-run -q "$resources_qube" "cd '$resources_dir' && qvm-copy-to-vm $qube post"
post_incoming_dir="%USERPROFILE%\\Documents\\QubesIncoming\\$resources_qube\\post"
if [ "$seamless" == "true" ]; then
echo_info "Enabling seamless mode persistently..."
qvm-run -q "$qube" "cd $post_incoming_dir && seamless.bat" || true
fi
if [ "$optimize" == "true" ]; then
echo_info "Optimizing Windows..."
qvm-run -q "$qube" "cd $post_incoming_dir && optimize.bat" || true
fi
if [ "$spyless" == "true" ]; then
echo_info "Disabling Windows telemetry..."
qvm-run -q "$qube" "cd $post_incoming_dir && spyless.bat" || true
fi
if [ "$whonix" == "true" ]; then
echo_ok "Applying Whonix recommended settings for a Windows-Whonix-Workstation..."
qvm-tags "$qube" add anon-vm
qvm-run -q "$qube" "cd $post_incoming_dir && whonix.bat" || true
fi
# Let Windows connect to the Internet earlier for package installation
if [ "$packages" ]; then
break_airgap
fi
if [ "$packages" ]; then
echo_info "Installing packages..."
qvm-run -p "$qube" "cd $post_incoming_dir && powershell -ExecutionPolicy Bypass -Command .\\packages.ps1 $packages <nul" || true
# Add new apps to app menu
qvm-sync-appmenus "$qube" &> /dev/null
fi
echo_info "Running user-defined custom commands..."
qvm-run -p "$qube" "cd $post_incoming_dir && run.bat" || true
# Clean up post scripts and remove policy
qvm-run -q "$qube" "rmdir /s /q $post_incoming_dir\\..\\.." || true
if [ "$qubes_version" == "R4.0" ]; then
# For R4.0: Remove modification from policy file
sed -i "/^$policy$/d" "$policy_file"
else
# For R4.1 and later: Remove temporary policy file from policy.d directory
sudo rm -f "$policy_file"
fi
echo_info "Shutting down Windows..."
# Shutdown and wait until complete before finishing or starting next installation
qvm-shutdown --wait "$qube"
# Let Windows connect to the Internet with the user selected NetVM later when package installation is disabled
if [ "$netvm" ] && [ ! "$packages" ]; then
break_airgap
fi
if [ "$count" -gt 1 ]; then
echo_ok "Finished creation of $qube successfully!"
fi
done
echo_ok "Completed successfully!"