#!/bin/sh # shellcheck shell=sh ss_case_refresh_state_machine() ( set -eu TMP="$(ss_spec_tmpdir)" trap 'rm -rf "$TMP"' EXIT HUP INT TERM CORE="$SS_SPEC_ROOT/files/usr/lib/safeshield/core.sh" HARNESS="$TMP/core-state-machine.sh" sed -n '/^safeshield_apply_local_rules() {/,${p;}' "$CORE" >"$HARNESS" # shellcheck disable=SC1090 . "$HARNESS" SS_TMP_DIR="$TMP/runtime" SS_DNSMASQ_DIR="$TMP/dnsmasq.d" SS_BLOCKLIST_FILE="$SS_DNSMASQ_DIR/safeshield.blocklist" SS_INACTIVE_BLOCKLIST_FILE="$SS_TMP_DIR/inactive.blocklist" SS_PREV_BLOCKLIST_GZ="$TMP/previous.gz" mkdir -p "$SS_TMP_DIR" "$SS_DNSMASQ_DIR" CALLS="$TMP/calls" : >"$CALLS" record() { printf '%s\n' "$*" >>"$CALLS"; } log_info() { :; } log_warn() { :; } log_error() { :; } log_ok() { :; } ss_status_prepare_refresh() { record status_prepare_refresh; } ss_status_set() { record "status_set $1 ${2:-}"; } ss_status_set_now() { record "status_set_now $1"; } ss_status_add_error() { record "status_error $1"; } ss_status_mark_failure() { record "failure $1"; } ss_status_mark_success() { record "success $1"; } ss_load_config() { ss_enabled=1 export ss_initial_dnsmasq_restart=0 ss_apply_local_overrides=1 return 0 } ss_config_get() { case "$2" in refresh_interval_s) printf '%s\n' '300' ;; *) printf '%s\n' "${3:-}" ;; esac } is_valid_integer() { case "$1" in '' | *[!0-9]*) return 1 ;; *) return 0 ;; esac } ss_sync_detected_device_config() { record sync_device_config; } ss_identity_ensure() { record identity_ensure [ "${FAIL_STAGE:-}" != 'identity' ] } ss_detect_device_model() { printf '%s\n' 'Test Router'; } ss_detect_device_arch() { printf '%s\n' 'test-arch'; } ss_should_stop() { return 1; } ss_mkdirs() { record mkdirs [ "${FAIL_STAGE:-}" != 'mkdir' ] } ss_clean_tmp() { record clean_tmp; } ss_require_supported_dnsmasq() { record require_dnsmasq [ "${FAIL_STAGE:-}" != 'dnsmasq_version' ] } ss_dnsmasq_check_error() { printf '%s\n' 'dnsmasq_version_unsupported'; } ss_ensure_dnsmasq_confdir() { record ensure_confdir [ "${FAIL_STAGE:-}" != 'confdir' ] } ss_export_existing_blocklist() { record export_blocklist printf '%s\n' 'backup' >"$SS_PREV_BLOCKLIST_GZ" return 0 } DNSMASQ_CALL=0 dnsmasq_restart() { DNSMASQ_CALL=$((DNSMASQ_CALL + 1)) record "dnsmasq_restart $DNSMASQ_CALL" [ "${FAIL_STAGE:-}" != 'restart' ] } ss_resolve_artifact() { record resolve_artifact [ "${FAIL_STAGE:-}" != 'resolve' ] } ss_resolve_error_code() { printf '%s\n' "${RESOLVE_ERROR_CODE:-artifact_resolve_failed}"; } ss_download_api_artifacts() { record download_artifacts [ "${FAIL_STAGE:-}" != 'download' ] } ss_artifact_download_error_code() { printf '%s\n' "${DOWNLOAD_ERROR_CODE:-artifact_download_failed}"; } ss_prepare_local_rule_files() { record prepare_local_rules [ "${FAIL_STAGE:-}" != 'local_rules' ] } ss_merge_lists() { record merge_lists [ "${FAIL_STAGE:-}" != 'merge' ] } ss_install_blocklist() { record install_blocklist [ "${FAIL_STAGE:-}" != 'install' ] } check_dns_runtime() { record check_dns_runtime [ "${FAIL_STAGE:-}" != 'runtime' ] } check_blocklist_applied_multi_with_stats() { record "verify_blocklist $*" [ "${FAIL_STAGE:-}" != 'verify' ] } ss_restore_and_restart() { record restore_and_restart; } ss_refresh_lock_open() { record lock_open [ "${LOCK_AVAILABLE:-1}" = '1' ] } ss_refresh_lock_close() { record lock_close; } ss_local_rules_fingerprint_from_tmp() { printf '%s\n' 'fingerprint-new'; } ss_local_rules_state_write() { record "local_state_write $1"; } ss_abort_refresh() { record abort_refresh return 130 } assert_refresh_failure() { stage="$1" code="$2" expect_restore="$3" FAIL_STAGE="$stage" RESOLVE_ERROR_CODE='artifact_resolve_failed' DOWNLOAD_ERROR_CODE='artifact_download_failed' DNSMASQ_CALL=0 : >"$CALLS" if safeshield_force_download; then return 1 fi ss_spec_assert_file_line "$CALLS" "failure $code" ss_spec_assert_file_line "$CALLS" 'lock_close' if [ "$expect_restore" = '1' ]; then ss_spec_assert_file_line "$CALLS" 'restore_and_restart' else ! grep -Fx 'restore_and_restart' "$CALLS" >/dev/null ! grep -F 'dnsmasq_restart ' "$CALLS" >/dev/null fi } FAIL_STAGE='' LOCK_AVAILABLE=1 : >"$CALLS" safeshield_force_download ss_spec_assert_file_line "$CALLS" 'status_set stage resolve_api' ss_spec_assert_file_line "$CALLS" 'status_set stage download_artifact' ss_spec_assert_file_line "$CALLS" 'status_set stage merge' ss_spec_assert_file_line "$CALLS" 'status_set stage install' ss_spec_assert_file_line "$CALLS" 'status_set stage restart_dnsmasq' ss_spec_assert_file_line "$CALLS" 'status_set stage runtime_check' ss_spec_assert_file_line "$CALLS" 'status_set stage blocklist_verify' ss_spec_assert_file_line "$CALLS" 'success 300' ss_spec_assert_file_line "$CALLS" 'local_state_write fingerprint-new' ss_spec_assert_file_line "$CALLS" 'lock_close' ! grep -Fx 'restore_and_restart' "$CALLS" >/dev/null assert_refresh_failure merge merge_failed 0 assert_refresh_failure resolve artifact_resolve_failed 0 assert_refresh_failure download artifact_download_failed 0 assert_refresh_failure local_rules allowlist_build_failed 0 assert_refresh_failure install install_failed 1 assert_refresh_failure restart dnsmasq_restart_failed 1 assert_refresh_failure runtime dns_runtime_check_failed 1 assert_refresh_failure verify blocklist_verification_failed 1 FAIL_STAGE='resolve' RESOLVE_ERROR_CODE='safeshield_upgrade_required' : >"$CALLS" ! safeshield_force_download ss_spec_assert_file_line "$CALLS" 'failure safeshield_upgrade_required' ! grep -Fx 'restore_and_restart' "$CALLS" >/dev/null FAIL_STAGE='download' DOWNLOAD_ERROR_CODE='safeshield_upgrade_required' : >"$CALLS" ! safeshield_force_download ss_spec_assert_file_line "$CALLS" 'failure safeshield_upgrade_required' ! grep -Fx 'restore_and_restart' "$CALLS" >/dev/null FAIL_STAGE='' LOCK_AVAILABLE=0 : >"$CALLS" safeshield_force_download ss_spec_assert_file_line "$CALLS" 'lock_open' ! grep -F 'status_prepare_refresh' "$CALLS" >/dev/null ) ss_case_local_apply_state_machine() ( set -eu TMP="$(ss_spec_tmpdir)" trap 'rm -rf "$TMP"' EXIT HUP INT TERM CORE="$SS_SPEC_ROOT/files/usr/lib/safeshield/core.sh" HARNESS="$TMP/local-apply.sh" sed -n '/^safeshield_apply_local_rules() {/,/^safeshield_force_download() {/p' "$CORE" | sed '$d' >"$HARNESS" # shellcheck disable=SC1090 . "$HARNESS" SS_TMP_DIR="$TMP/runtime" SS_DNSMASQ_DIR="$TMP/dnsmasq.d" SS_BLOCKLIST_FILE="$SS_DNSMASQ_DIR/safeshield.blocklist" SS_PREV_BLOCKLIST_GZ="$TMP/previous.gz" SS_INACTIVE_BLOCKLIST_FILE="$SS_TMP_DIR/inactive.blocklist" mkdir -p "$SS_TMP_DIR" "$SS_DNSMASQ_DIR" CALLS="$TMP/calls" : >"$CALLS" record() { printf '%s\n' "$*" >>"$CALLS"; } log_info() { :; } log_warn() { :; } log_error() { :; } log_ok() { :; } sleep() { :; } ss_load_config() { ss_enabled="${MOCK_ENABLED:-1}" export ss_apply_local_overrides="${MOCK_OVERRIDES:-1}" return 0 } ss_mkdirs() { return 0; } ss_refresh_lock_open_wait() { record "lock_wait $1" return "${LOCK_RC:-0}" } ss_refresh_lock_close() { record lock_close; } ss_cached_api_sources_available() { [ "${CACHE_AVAILABLE:-1}" = '1' ]; } ss_status_set() { record "status_set $1 ${2:-}"; } ss_status_set_now() { record "status_set_now $1"; } ss_status_add_error() { record "status_error $1"; } ss_prepare_local_rule_files() { record prepare_local_rules [ "${LOCAL_FAIL:-}" != 'local_rules' ] } ss_local_rules_fingerprint_from_tmp() { printf '%s\n' "${NEW_FINGERPRINT:-fingerprint-new}"; } ss_local_rules_state_read() { [ -n "${APPLIED_FINGERPRINT:-}" ] && printf '%s\n' "$APPLIED_FINGERPRINT"; } ss_local_rules_state_write() { record "local_state_write $1"; } ss_require_supported_dnsmasq() { [ "${LOCAL_FAIL:-}" != 'dnsmasq_version' ]; } ss_dnsmasq_check_error() { printf '%s\n' 'dnsmasq_version_unsupported'; } ss_ensure_dnsmasq_confdir() { record ensure_confdir [ "${LOCAL_FAIL:-}" != 'confdir' ] } ss_export_existing_blocklist() { printf '%s\n' backup >"$SS_PREV_BLOCKLIST_GZ" return 0 } ss_merge_lists() { record merge_lists [ "${LOCAL_FAIL:-}" != 'merge' ] } dnsmasq_restart() { record dnsmasq_restart [ "${LOCAL_FAIL:-}" != 'restart' ] } check_dns_runtime() { record check_dns_runtime [ "${LOCAL_FAIL:-}" != 'runtime' ] } check_blocklist_applied_multi_with_stats() { record "verify_blocklist $*" [ "${LOCAL_FAIL:-}" != 'verify' ] } ss_restore_and_restart() { record restore_and_restart; } ss_mark_local_apply_failure() { record "local_failure $1"; } ss_mark_local_apply_success() { record local_success; } FALLBACK_RC=0 safeshield_force_download() { record fallback_refresh return "$FALLBACK_RC" } CACHE_AVAILABLE=0 LOCK_RC=0 LOCAL_FAIL='' : >"$CALLS" safeshield_apply_local_rules ss_spec_assert_file_line "$CALLS" 'fallback_refresh' ss_spec_assert_file_line "$CALLS" 'lock_close' CACHE_AVAILABLE=1 LOCK_RC=1 : >"$CALLS" ! safeshield_apply_local_rules ss_spec_assert_file_line "$CALLS" 'local_failure local_apply_lock_timeout' LOCK_RC=130 : >"$CALLS" set +e safeshield_apply_local_rules rc=$? set -e ss_spec_assert_eq "$rc" '130' ! grep -F 'local_failure' "$CALLS" >/dev/null LOCK_RC=0 APPLIED_FINGERPRINT='fingerprint-new' NEW_FINGERPRINT='fingerprint-new' printf '%s\n' 'safeshield-block=/cached.example/' >"$SS_BLOCKLIST_FILE" : >"$CALLS" safeshield_apply_local_rules ss_spec_assert_file_line "$CALLS" 'status_set stage done' ! grep -Fx 'merge_lists' "$CALLS" >/dev/null ! grep -Fx 'dnsmasq_restart' "$CALLS" >/dev/null APPLIED_FINGERPRINT='fingerprint-old' NEW_FINGERPRINT='fingerprint-new' for scenario in merge restart runtime verify; do LOCAL_FAIL="$scenario" : >"$CALLS" ! safeshield_apply_local_rules case "$scenario" in merge) code='local_merge_failed' ;; restart) code='local_dnsmasq_restart_failed' ;; runtime) code='local_dns_runtime_check_failed' ;; verify) code='local_blocklist_verification_failed' ;; esac ss_spec_assert_file_line "$CALLS" "local_failure $code" if [ "$scenario" = 'merge' ]; then ! grep -Fx 'restore_and_restart' "$CALLS" >/dev/null ! grep -Fx 'dnsmasq_restart' "$CALLS" >/dev/null else ss_spec_assert_file_line "$CALLS" 'restore_and_restart' fi ss_spec_assert_file_line "$CALLS" 'lock_close' done LOCAL_FAIL='' : >"$CALLS" safeshield_apply_local_rules ss_spec_assert_file_line "$CALLS" 'merge_lists' ss_spec_assert_file_line "$CALLS" 'dnsmasq_restart' ss_spec_assert_file_line "$CALLS" 'check_dns_runtime' ss_spec_assert_file_line "$CALLS" 'verify_blocklist 5 3' ss_spec_assert_file_line "$CALLS" 'local_state_write fingerprint-new' ss_spec_assert_file_line "$CALLS" 'local_success' ! grep -Fx 'restore_and_restart' "$CALLS" >/dev/null ) ss_case_refresh_locking() ( set -eu TMP="$(ss_spec_tmpdir)" trap 'rm -rf "$TMP"' EXIT HUP INT TERM SS_REFRESH_LOCK="$TMP/safeshield-refresh.lock" # shellcheck disable=SC1090 . "$SS_SPEC_ROOT/files/usr/lib/safeshield/locking.sh" # shellcheck disable=SC1090 . "$SS_SPEC_ROOT/files/usr/lib/safeshield/status.sh" is_valid_integer() { case "$1" in '' | *[!0-9]*) return 1 ;; *) return 0 ;; esac } ss_should_stop() { return 1; } ss_refresh_lock_open ss_spec_assert_eq "$(cat "${SS_REFRESH_LOCK}.d/pid")" "$$" # A separate shell must not acquire or release our live lock. ! sh -c '. "$1"; ss_lock_try "$2"' sh "$SS_SPEC_ROOT/files/usr/lib/safeshield/locking.sh" "${SS_REFRESH_LOCK}.d" ss_refresh_lock_close ss_spec_assert_not_exists "${SS_REFRESH_LOCK}.d" # Simulate an owner which crashed after writing pid. mkdir "${SS_REFRESH_LOCK}.d" printf '99999999\n' >"${SS_REFRESH_LOCK}.d/pid" ss_refresh_lock_open ss_refresh_lock_close # A freshly-created empty lock must not be stolen while the owner writes pid. mkdir "${SS_REFRESH_LOCK}.d" ! ss_refresh_lock_open ! ss_refresh_lock_open_wait 0 rmdir "${SS_REFRESH_LOCK}.d" ss_should_stop() { return 0; } mkdir "${SS_REFRESH_LOCK}.d" printf '%s\n' "$$" >"${SS_REFRESH_LOCK}.d/pid" set +e ss_refresh_lock_open_wait 10 rc=$? set -e ss_spec_assert_eq "$rc" '130' ss_refresh_lock_close ) ss_case_service_lifecycle() ( set -eu TMP="$(ss_spec_tmpdir)" trap 'rm -rf "$TMP"' EXIT HUP INT TERM INIT="$SS_SPEC_ROOT/files/etc/init.d/safeshield" HARNESS="$TMP/init-lifecycle.sh" sed -n '/^start_service() {/,/^reconcile_statistics() {/p' "$INIT" | sed '$d' >"$HARNESS" # shellcheck disable=SC1090 . "$HARNESS" export PKG_NAME='safeshield' SS_BLOCKLIST_FILE="$TMP/dnsmasq.d/safeshield.blocklist" export SS_INACTIVE_BLOCKLIST_FILE="$TMP/inactive.blocklist" mkdir -p "${SS_BLOCKLIST_FILE%/*}" CALLS="$TMP/calls" : >"$CALLS" record() { printf '%s\n' "$*" >>"$CALLS"; } log_error() { :; } log_warn() { :; } ss_load_config() { export ss_enabled="${MOCK_ENABLED:-1}" export ss_statistics_enabled="${MOCK_STATISTICS:-1}" return "${LOAD_RC:-0}" } ss_identity_ensure() { record identity_ensure return 0 } ss_detect_device_model() { printf '%s\n' 'Test Router'; } ss_detect_device_arch() { printf '%s\n' 'test-arch'; } ss_mkdirs() { record mkdirs return 0 } ss_statistics_request_rebaseline() { record request_rebaseline return 0 } ss_statistics_configure_dnsmasq() { record "statistics_configure $1" printf '%s\n' "${STATISTICS_CHANGED:-0}" } ss_runtime_deactivate_blocklist() { record deactivate_blocklist return "${DEACTIVATE_RC:-0}" } ss_runtime_activate_blocklist() { record activate_blocklist return "${ACTIVATE_RC:-0}" } ss_status_set() { record "status_set $1 ${2:-}"; } ss_status_mark_failure() { record "failure $1"; } ss_require_supported_dnsmasq() { record require_dnsmasq return 0 } ss_dnsmasq_check_error() { printf '%s\n' 'dnsmasq_version_unsupported'; } ss_statistics_disable_if_dnsmasq_unpatched() { record statistics_guard return 0 } ss_reconcile_installed_blocklists_for_dnsmasq() { record reconcile_blocklists return "${MIGRATION_RC:-0}" } ss_ensure_dnsmasq_confdir() { record ensure_confdir return 0 } dnsmasq_restart() { record dnsmasq_restart return "${DNSMASQ_RC:-0}" } procd_open_instance() { record procd_open_instance; } procd_set_param() { record "procd_set_param $*"; } procd_close_instance() { record procd_close_instance; } ss_statistics_add_procd_instance() { record add_statistics_instance; } ss_statistics_add_upload_procd_instance() { record add_statistics_upload_instance; } MOCK_ENABLED=0 MOCK_STATISTICS=1 STATISTICS_CHANGED=1 MIGRATION_RC=0 DNSMASQ_RC=0 : >"$CALLS" start_service ss_spec_assert_file_line "$CALLS" 'request_rebaseline' ss_spec_assert_file_line "$CALLS" 'statistics_configure 0' ss_spec_assert_file_line "$CALLS" 'deactivate_blocklist' ss_spec_assert_file_line "$CALLS" 'dnsmasq_restart' ss_spec_assert_file_line "$CALLS" 'status_set stage disabled' ! grep -Fx 'procd_open_instance' "$CALLS" >/dev/null printf '%s\n' 'safeshield-block=/active.example/' >"$SS_BLOCKLIST_FILE" : >"$CALLS" start_service ss_spec_assert_file_line "$CALLS" 'deactivate_blocklist' ! grep -Fx 'dnsmasq_restart' "$CALLS" >/dev/null rm -f "$SS_BLOCKLIST_FILE" MOCK_ENABLED=1 MOCK_STATISTICS=1 STATISTICS_CHANGED=0 MIGRATION_RC=0 : >"$CALLS" start_service ss_spec_assert_file_line "$CALLS" 'require_dnsmasq' ss_spec_assert_file_line "$CALLS" 'statistics_guard' ss_spec_assert_file_line "$CALLS" 'reconcile_blocklists' ss_spec_assert_file_line "$CALLS" 'statistics_configure 1' ss_spec_assert_file_line "$CALLS" 'activate_blocklist' ss_spec_assert_file_line "$CALLS" 'procd_set_param command /usr/libexec/safeshield-refreshd' ss_spec_assert_file_line "$CALLS" 'add_statistics_instance' ss_spec_assert_file_line "$CALLS" 'add_statistics_upload_instance' MIGRATION_RC=1 : >"$CALLS" ! start_service ss_spec_assert_file_line "$CALLS" 'failure blocklist_directive_migration_failed' ! grep -Fx 'activate_blocklist' "$CALLS" >/dev/null MIGRATION_RC=0 MOCK_ENABLED=1 STATISTICS_CHANGED=1 rm -f "$SS_BLOCKLIST_FILE" : >"$CALLS" stop_service ss_spec_assert_file_line "$CALLS" 'statistics_configure 0' ss_spec_assert_file_line "$CALLS" 'deactivate_blocklist' ss_spec_assert_file_line "$CALLS" 'dnsmasq_restart' ss_spec_assert_file_line "$CALLS" 'status_set stage stopped' printf '%s\n' 'safeshield-block=/active.example/' >"$SS_BLOCKLIST_FILE" : >"$CALLS" stop_service ss_spec_assert_file_line "$CALLS" 'deactivate_blocklist' ! grep -Fx 'dnsmasq_restart' "$CALLS" >/dev/null rm -f "$SS_BLOCKLIST_FILE" MOCK_ENABLED=0 STATISTICS_CHANGED=0 : >"$CALLS" stop_service ss_spec_assert_file_line "$CALLS" 'status_set status disabled' ss_spec_assert_file_line "$CALLS" 'status_set last_result disabled' )