Resume USB approval verification after transient failures
This commit is contained in:
1 parent
90d604f41d
commit
db6ab5a8bf
2 files changed
+184
-51
No files matched your search
@@ -12,72 +12,103 @@ if [[ ! $token =~ ^request-[0-9a-f]{64}$ ]]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
request="$HOME/.local/state/omarchy/usb-authorization/requests/$token.json"
|
request="$HOME/.local/state/omarchy/usb-authorization/requests/$token.json"
|
||||||
|
# Serialize request updates with watcher events, but release the lock while the
|
||||||
|
# user chooses. A changed request must never inherit an old dialog's approval.
|
||||||
|
if [[ ! -d ${request%/*} ]]; then
|
||||||
|
echo "This USB authorization request is no longer available." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
exec 9>"${request%/*}/.lock"
|
||||||
|
flock 9
|
||||||
if [[ ! -f $request || -L $request ]]; then
|
if [[ ! -f $request || -L $request ]]; then
|
||||||
echo "This USB authorization request is no longer available." >&2
|
echo "This USB authorization request is no longer available." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
id=$(jq -er '.id | select(test("^[0-9]+$"))' "$request")
|
snapshot=$(cat "$request")
|
||||||
expected_rule=$(jq -er '.rule | select(type == "string" and length > 0)' "$request")
|
id=$(jq -er '.id | select(test("^[0-9]+$"))' <<<"$snapshot")
|
||||||
read_blocked_rule() {
|
expected_rule=$(jq -er '.rule | select(type == "string" and startswith("block "))' <<<"$snapshot")
|
||||||
local line
|
choice=$(jq -er '.approval // "" | select(. == "" or . == "Allow once" or . == "Always allow this device")' <<<"$snapshot")
|
||||||
|
allowed_rule="allow ${expected_rule#block }"
|
||||||
while IFS= read -r line; do
|
|
||||||
if [[ $line == "$id: "* ]]; then
|
|
||||||
printf '%s\n' "${line#"$id: "}"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
done < <(usbguard list-devices --blocked)
|
|
||||||
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
current_rule=$(read_blocked_rule || true)
|
|
||||||
|
|
||||||
if [[ -z $current_rule || $current_rule != "$expected_rule" ]]; then
|
|
||||||
rm -f "$request"
|
|
||||||
echo "That USB device was removed or changed. Connect it again to review it." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
gum style --foreground 212 --bold "Blocked USB accessory"
|
|
||||||
echo
|
|
||||||
echo "The following details come from the device and can be forged:"
|
|
||||||
printf '%s\n' "$current_rule" | LC_ALL=C tr -c '\11\12\15\40-\176' '?'
|
|
||||||
echo
|
|
||||||
|
|
||||||
choice=$(gum choose \
|
|
||||||
"Allow once" \
|
|
||||||
"Always allow this device" \
|
|
||||||
"Keep blocked") || exit 130
|
|
||||||
|
|
||||||
if [[ $choice != "Keep blocked" ]]; then
|
|
||||||
current_rule=$(read_blocked_rule || true)
|
|
||||||
if [[ -z $current_rule || $current_rule != "$expected_rule" ]]; then
|
|
||||||
rm -f "$request"
|
|
||||||
echo "That USB device was removed or changed. Connect it again to review it." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
approval_failed() {
|
approval_failed() {
|
||||||
echo "Could not confirm USB approval. The request has been kept; try again." >&2
|
echo "Could not confirm USB approval. The request has been kept; try again." >&2
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
read_current_rule() {
|
||||||
|
local devices line
|
||||||
|
|
||||||
|
current_rule=""
|
||||||
|
devices=$(usbguard list-devices) || approval_failed
|
||||||
|
while IFS= read -r line; do
|
||||||
|
if [[ $line == "$id: "* ]]; then
|
||||||
|
current_rule="${line#"$id: "}"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
done <<<"$devices"
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_device() {
|
||||||
|
read_current_rule
|
||||||
|
if [[ $current_rule == "$expected_rule" ]] ||
|
||||||
|
[[ -n $choice && $current_rule == "$allowed_rule" ]]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
rm -f "$request"
|
||||||
|
echo "That USB device was removed or changed. Connect it again to review it." >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_device
|
||||||
|
|
||||||
|
if [[ -z $choice ]]; then
|
||||||
|
flock -u 9
|
||||||
|
gum style --foreground 212 --bold "Blocked USB accessory"
|
||||||
|
echo
|
||||||
|
echo "The following details come from the device and can be forged:"
|
||||||
|
printf '%s\n' "$current_rule" | LC_ALL=C tr -c '\11\12\15\40-\176' '?'
|
||||||
|
echo
|
||||||
|
|
||||||
|
choice=$(gum choose \
|
||||||
|
"Allow once" \
|
||||||
|
"Always allow this device" \
|
||||||
|
"Keep blocked") || exit 130
|
||||||
|
|
||||||
|
flock 9
|
||||||
|
if [[ ! -f $request || -L $request || $(cat "$request") != "$snapshot" ]]; then
|
||||||
|
echo "This USB authorization request changed. Open the latest notification to review it." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# A fresh choice can authorize only the blocked identity shown in the dialog.
|
||||||
|
read_current_rule
|
||||||
|
if [[ $current_rule != "$expected_rule" ]]; then
|
||||||
|
rm -f "$request"
|
||||||
|
echo "That USB device was removed or changed. Connect it again to review it." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
case "$choice" in
|
case "$choice" in
|
||||||
"Allow once" | "Always allow this device")
|
"Allow once" | "Always allow this device")
|
||||||
# Target the complete identity rule instead of USBGuard's numeric ID. IDs can
|
# Save intent before applying policy: a later query can fail after the device
|
||||||
# be reassigned when the daemon restarts while the review dialog is open.
|
# was authorized. Retry must verify that approval, including persistent trust.
|
||||||
options=()
|
temporary=$(mktemp "${request%/*}/.request.XXXXXXXXXX")
|
||||||
if [[ $choice == "Always allow this device" ]]; then
|
trap 'rm -f "$temporary"' EXIT
|
||||||
options+=(--permanent)
|
jq --arg approval "$choice" '.approval = $approval' "$request" >"$temporary"
|
||||||
|
mv -f "$temporary" "$request"
|
||||||
|
|
||||||
|
if [[ $current_rule == "$expected_rule" ]]; then
|
||||||
|
# Match the complete identity, since numeric IDs can be reassigned.
|
||||||
|
options=()
|
||||||
|
if [[ $choice == "Always allow this device" ]]; then
|
||||||
|
options+=(--permanent)
|
||||||
|
fi
|
||||||
|
usbguard allow-device "${options[@]}" "$expected_rule" || approval_failed
|
||||||
fi
|
fi
|
||||||
usbguard allow-device "${options[@]}" "$expected_rule" || approval_failed
|
|
||||||
|
|
||||||
# Rule-based USBGuard commands swallow applyDevicePolicy exceptions and can
|
# Rule-based USBGuard commands swallow applyDevicePolicy exceptions and can
|
||||||
# return success without authorizing anything. Verify the exact device.
|
# return success without authorizing anything. Verify the exact device.
|
||||||
allowed_rule="allow ${expected_rule#block }"
|
|
||||||
allowed=$(usbguard list-devices --allowed) || approval_failed
|
allowed=$(usbguard list-devices --allowed) || approval_failed
|
||||||
grep -Fqx -- "$id: $allowed_rule" <<<"$allowed" || approval_failed
|
grep -Fqx -- "$id: $allowed_rule" <<<"$allowed" || approval_failed
|
||||||
|
|
||||||
@@ -107,6 +138,7 @@ case "$choice" in
|
|||||||
"Keep blocked")
|
"Keep blocked")
|
||||||
echo "USB accessory remains blocked."
|
echo "USB accessory remains blocked."
|
||||||
;;
|
;;
|
||||||
|
*) exit 1 ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
rm -f "$request"
|
rm -f "$request"
|
||||||
@@ -74,6 +74,7 @@ list-devices)
|
|||||||
printf '17: %s\n' "$(<"$TEST_ALLOWED_RULE")"
|
printf '17: %s\n' "$(<"$TEST_ALLOWED_RULE")"
|
||||||
fi
|
fi
|
||||||
elif [[ ${2:-} == "--blocked" ]]; then
|
elif [[ ${2:-} == "--blocked" ]]; then
|
||||||
|
[[ ${BLOCKED_QUERY_FAIL:-0} == 0 ]] || exit 1
|
||||||
[[ ! -f $TEST_ALLOWED_RULE ]] || exit 0
|
[[ ! -f $TEST_ALLOWED_RULE ]] || exit 0
|
||||||
if [[ -n ${BLOCKED_DEVICES_FILE:-} ]]; then cat "$BLOCKED_DEVICES_FILE"; exit 0; fi
|
if [[ -n ${BLOCKED_DEVICES_FILE:-} ]]; then cat "$BLOCKED_DEVICES_FILE"; exit 0; fi
|
||||||
if [[ ${BLOCKED_DEVICE_PRESENT:-1} == 1 ]]; then
|
if [[ ${BLOCKED_DEVICE_PRESENT:-1} == 1 ]]; then
|
||||||
@@ -84,6 +85,17 @@ list-devices)
|
|||||||
printf '17: %s\n' "$rule"
|
printf '17: %s\n' "$rule"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
|
[[ ${BLOCKED_QUERY_FAIL:-0} == 0 ]] || exit 1
|
||||||
|
if [[ -f $TEST_ALLOWED_RULE ]]; then
|
||||||
|
[[ ${APPROVAL_QUERY_FAIL:-0} == 0 ]] || exit 1
|
||||||
|
printf '17: %s\n' "$(<"$TEST_ALLOWED_RULE")"
|
||||||
|
elif [[ -n ${BLOCKED_DEVICE_RULE:-} && ${BLOCKED_DEVICE_PRESENT:-1} == 1 ]]; then
|
||||||
|
rule="$BLOCKED_DEVICE_RULE"
|
||||||
|
if [[ -n ${BLOCKED_DEVICE_RULE_FILE:-} && -f $BLOCKED_DEVICE_RULE_FILE ]]; then
|
||||||
|
rule=$(<"$BLOCKED_DEVICE_RULE_FILE")
|
||||||
|
fi
|
||||||
|
printf '17: %s\n' "$rule"
|
||||||
|
fi
|
||||||
echo '4: allow id 1d6b:0002 name "Linux Foundation root hub" hash "root"'
|
echo '4: allow id 1d6b:0002 name "Linux Foundation root hub" hash "root"'
|
||||||
echo '5: allow id 0627:0001 name "QEMU USB Tablet" hash "tablet"'
|
echo '5: allow id 0627:0001 name "QEMU USB Tablet" hash "tablet"'
|
||||||
fi
|
fi
|
||||||
@@ -142,6 +154,12 @@ cat >"$stub_bin/gum" <<'STUB'
|
|||||||
case "$1" in
|
case "$1" in
|
||||||
style) exit 0 ;;
|
style) exit 0 ;;
|
||||||
choose)
|
choose)
|
||||||
|
echo "gum choose" >>"$CALLS"
|
||||||
|
[[ ${GUM_CANCEL:-0} == 0 ]] || exit 130
|
||||||
|
if [[ -n ${GUM_REPLACE_REQUEST:-} ]]; then
|
||||||
|
jq '.generation = "replacement-generation"' "$GUM_REPLACE_REQUEST" >"$GUM_REPLACE_REQUEST.new"
|
||||||
|
mv "$GUM_REPLACE_REQUEST.new" "$GUM_REPLACE_REQUEST"
|
||||||
|
fi
|
||||||
if [[ -n ${GUM_REASSIGN_RULE:-} ]]; then
|
if [[ -n ${GUM_REASSIGN_RULE:-} ]]; then
|
||||||
printf '%s\n' "$GUM_REASSIGN_RULE" >"$BLOCKED_DEVICE_RULE_FILE"
|
printf '%s\n' "$GUM_REASSIGN_RULE" >"$BLOCKED_DEVICE_RULE_FILE"
|
||||||
fi
|
fi
|
||||||
@@ -648,7 +666,7 @@ pass "USB review binds approval to the device snapshot"
|
|||||||
|
|
||||||
for choice in 'Allow once' 'Always allow this device'; do
|
for choice in 'Allow once' 'Always allow this device'; do
|
||||||
for failure in APPROVAL_SILENT_FAIL APPROVAL_EXIT_FAIL APPROVAL_QUERY_FAIL APPROVAL_REPLACEMENT_RULE; do
|
for failure in APPROVAL_SILENT_FAIL APPROVAL_EXIT_FAIL APPROVAL_QUERY_FAIL APPROVAL_REPLACEMENT_RULE; do
|
||||||
rm -f "$TEST_ALLOWED_RULE" "$TEST_SAVED_RULE"
|
rm -f "$TEST_ALLOWED_RULE" "$TEST_SAVED_RULE" "$request"
|
||||||
USBGUARD_IPC_SIGNAL=IPC.Connected "$ROOT/bin/omarchy-usb-authorization-event"
|
USBGUARD_IPC_SIGNAL=IPC.Connected "$ROOT/bin/omarchy-usb-authorization-event"
|
||||||
value=1
|
value=1
|
||||||
[[ $failure != "APPROVAL_REPLACEMENT_RULE" ]] || value='allow id 9999:9999 hash "replacement"'
|
[[ $failure != "APPROVAL_REPLACEMENT_RULE" ]] || value='allow id 9999:9999 hash "replacement"'
|
||||||
@@ -660,7 +678,8 @@ for choice in 'Allow once' 'Always allow this device'; do
|
|||||||
done
|
done
|
||||||
done
|
done
|
||||||
for failure in APPROVAL_NO_POLICY APPROVAL_POLICY_QUERY_FAIL; do
|
for failure in APPROVAL_NO_POLICY APPROVAL_POLICY_QUERY_FAIL; do
|
||||||
rm -f "$TEST_ALLOWED_RULE" "$TEST_SAVED_RULE"
|
rm -f "$TEST_ALLOWED_RULE" "$TEST_SAVED_RULE" "$request"
|
||||||
|
USBGUARD_IPC_SIGNAL=IPC.Connected "$ROOT/bin/omarchy-usb-authorization-event"
|
||||||
if env "$failure=1" GUM_CHOICE='Always allow this device' "$ROOT/bin/omarchy-usb-authorization-review" "$token" >"$scratch/review-no-policy" 2>&1; then
|
if env "$failure=1" GUM_CHOICE='Always allow this device' "$ROOT/bin/omarchy-usb-authorization-review" "$token" >"$scratch/review-no-policy" 2>&1; then
|
||||||
fail "permanent approval must reject $failure"
|
fail "permanent approval must reject $failure"
|
||||||
fi
|
fi
|
||||||
@@ -672,6 +691,88 @@ GUM_CHOICE='Always allow this device' "$ROOT/bin/omarchy-usb-authorization-revie
|
|||||||
rm -f "$TEST_ALLOWED_RULE" "$TEST_SAVED_RULE"
|
rm -f "$TEST_ALLOWED_RULE" "$TEST_SAVED_RULE"
|
||||||
pass "USB approvals verify device and permanent policy before consuming requests"
|
pass "USB approvals verify device and permanent policy before consuming requests"
|
||||||
|
|
||||||
|
# Exercise retries without resetting the device state left by the first attempt.
|
||||||
|
for choice in 'Allow once' 'Always allow this device'; do
|
||||||
|
USBGUARD_IPC_SIGNAL=IPC.Connected "$ROOT/bin/omarchy-usb-authorization-event"
|
||||||
|
failure=APPROVAL_QUERY_FAIL
|
||||||
|
[[ $choice != 'Always allow this device' ]] || failure=APPROVAL_POLICY_QUERY_FAIL
|
||||||
|
if env "$failure=1" GUM_CHOICE="$choice" "$ROOT/bin/omarchy-usb-authorization-review" "$token" >"$scratch/retry-first" 2>&1; then
|
||||||
|
fail "transient verification failure must retain the approval"
|
||||||
|
fi
|
||||||
|
[[ -s $TEST_ALLOWED_RULE && -f $request ]] || fail "fixture must retain an already-allowed device"
|
||||||
|
[[ $(jq -r .approval "$request") == "$choice" ]] || fail "retry must remember the selected approval"
|
||||||
|
[[ $(stat -c %a "$request") == 600 ]] || fail "saved approval intent stays private"
|
||||||
|
allow_count=$(grep -c '^usbguard <allow-device>' "$calls")
|
||||||
|
prompt_count=$(grep -c '^gum choose' "$calls")
|
||||||
|
GUM_CANCEL=1 "$ROOT/bin/omarchy-usb-authorization-review" "$token" >"$scratch/retry-second"
|
||||||
|
[[ ! -e $request ]] || fail "verified approval retry consumes the request"
|
||||||
|
[[ $(grep -c '^usbguard <allow-device>' "$calls") == "$allow_count" ]] || fail "already-allowed retry must not reapply authorization"
|
||||||
|
[[ $(grep -c '^gum choose' "$calls") == "$prompt_count" ]] || fail "retry must resume the original approval without another choice"
|
||||||
|
if [[ $choice == 'Always allow this device' ]]; then
|
||||||
|
grep -Fq 'added to the trusted policy' "$scratch/retry-second" || fail "permanent retry must finish permanent verification"
|
||||||
|
else
|
||||||
|
grep -Fq 'allowed until it is disconnected' "$scratch/retry-second" || fail "once retry must finish temporary verification"
|
||||||
|
fi
|
||||||
|
rm -f "$TEST_ALLOWED_RULE" "$TEST_SAVED_RULE"
|
||||||
|
done
|
||||||
|
pass "USB approval retries resume successful temporary and permanent authorization"
|
||||||
|
|
||||||
|
USBGUARD_IPC_SIGNAL=IPC.Connected "$ROOT/bin/omarchy-usb-authorization-event"
|
||||||
|
if APPROVAL_NO_POLICY=1 GUM_CHOICE='Always allow this device' "$ROOT/bin/omarchy-usb-authorization-review" "$token" >"$scratch/missing-policy" 2>&1; then
|
||||||
|
fail "permanent approval requires saved policy"
|
||||||
|
fi
|
||||||
|
allow_count=$(grep -c '^usbguard <allow-device>' "$calls")
|
||||||
|
if GUM_CHOICE='Allow once' "$ROOT/bin/omarchy-usb-authorization-review" "$token" >"$scratch/missing-policy-retry" 2>&1; then
|
||||||
|
fail "retry cannot downgrade permanent approval when policy is missing"
|
||||||
|
fi
|
||||||
|
[[ -f $request && $(jq -r .approval "$request") == 'Always allow this device' ]] || fail "missing policy retains original permanent intent"
|
||||||
|
[[ $(grep -c '^usbguard <allow-device>' "$calls") == "$allow_count" ]] || fail "retry must not repeat authorization on an allowed device"
|
||||||
|
for failure in BLOCKED_QUERY_FAIL APPROVAL_QUERY_FAIL; do
|
||||||
|
if env "$failure=1" "$ROOT/bin/omarchy-usb-authorization-review" "$token" >"$scratch/query-retry" 2>&1; then
|
||||||
|
fail "retry must fail when inventory cannot be read"
|
||||||
|
fi
|
||||||
|
[[ -f $request ]] || fail "inventory query failures must preserve pending approvals"
|
||||||
|
done
|
||||||
|
printf '%s\n' "allow ${replacement_rule#block }" >"$TEST_ALLOWED_RULE"
|
||||||
|
if "$ROOT/bin/omarchy-usb-authorization-review" "$token" >"$scratch/replaced-retry" 2>&1; then
|
||||||
|
fail "retry must reject a replacement allowed device"
|
||||||
|
fi
|
||||||
|
[[ ! -e $request ]] || fail "a proven replacement consumes the stale request"
|
||||||
|
[[ $(grep -c '^usbguard <allow-device>' "$calls") == "$allow_count" ]] || fail "replacement retry must not authorize anything"
|
||||||
|
rm -f "$TEST_ALLOWED_RULE" "$TEST_SAVED_RULE"
|
||||||
|
pass "USB approval retries preserve permanent intent and validate allowed identity"
|
||||||
|
|
||||||
|
USBGUARD_IPC_SIGNAL=IPC.Connected "$ROOT/bin/omarchy-usb-authorization-event"
|
||||||
|
if BLOCKED_QUERY_FAIL=1 "$ROOT/bin/omarchy-usb-authorization-review" "$token" >"$scratch/fresh-query-failure" 2>&1; then
|
||||||
|
fail "fresh review must fail on inventory query failure"
|
||||||
|
fi
|
||||||
|
[[ -f $request ]] || fail "fresh inventory failure must not delete the request"
|
||||||
|
jq '.approval = "invalid"' "$request" >"$scratch/invalid-approval"
|
||||||
|
mv "$scratch/invalid-approval" "$request"
|
||||||
|
if "$ROOT/bin/omarchy-usb-authorization-review" "$token" >"$scratch/invalid-intent" 2>&1; then
|
||||||
|
fail "invalid saved intent must not authorize"
|
||||||
|
fi
|
||||||
|
jq 'del(.approval)' "$request" >"$scratch/fresh-request"
|
||||||
|
mv "$scratch/fresh-request" "$request"
|
||||||
|
if GUM_CANCEL=1 "$ROOT/bin/omarchy-usb-authorization-review" "$token" >"$scratch/cancel" 2>&1; then
|
||||||
|
fail "canceled review must not authorize"
|
||||||
|
fi
|
||||||
|
jq -e '.approval == null' "$request" >/dev/null || fail "canceling must not store an approval"
|
||||||
|
if GUM_REPLACE_REQUEST="$request" GUM_CHOICE='Always allow this device' "$ROOT/bin/omarchy-usb-authorization-review" "$token" >"$scratch/replaced-request" 2>&1; then
|
||||||
|
fail "open dialog must not approve a replaced request"
|
||||||
|
fi
|
||||||
|
[[ $(jq -r .generation "$request") == 'replacement-generation' ]] || fail "stale dialog must leave replacement request intact"
|
||||||
|
[[ $(grep -c '^usbguard <allow-device>' "$calls") == "$allow_count" ]] || fail "canceled or superseded requests cannot authorize"
|
||||||
|
rm -f "$request"
|
||||||
|
USBGUARD_IPC_SIGNAL=IPC.Connected "$ROOT/bin/omarchy-usb-authorization-event"
|
||||||
|
if APPROVAL_EXIT_FAIL=1 GUM_CHOICE='Always allow this device' "$ROOT/bin/omarchy-usb-authorization-review" "$token" >"$scratch/blocked-retry-first" 2>&1; then
|
||||||
|
fail "failed apply must retain a blocked request"
|
||||||
|
fi
|
||||||
|
GUM_CANCEL=1 "$ROOT/bin/omarchy-usb-authorization-review" "$token" >"$scratch/blocked-retry-second"
|
||||||
|
[[ -s $TEST_SAVED_RULE && ! -e $request ]] || fail "blocked retry must apply its saved permanent choice"
|
||||||
|
rm -f "$TEST_ALLOWED_RULE" "$TEST_SAVED_RULE"
|
||||||
|
pass "USB review preserves transient failures and rejects canceled or replaced dialogs"
|
||||||
|
|
||||||
notification_count=$(grep -c '^notification' "$calls")
|
notification_count=$(grep -c '^notification' "$calls")
|
||||||
printf '17: %s\n18: %s\n' "$malicious_rule" 'block id 1234:5678 name "Second blocked device" hash "second"' >"$scratch/two-blocked"
|
printf '17: %s\n18: %s\n' "$malicious_rule" 'block id 1234:5678 name "Second blocked device" hash "second"' >"$scratch/two-blocked"
|
||||||
NOTIFICATION_READY_FILE="$scratch/notification-ready" \
|
NOTIFICATION_READY_FILE="$scratch/notification-ready" \
|
||||||
|
|||||||
Reference in new issue
Block a user