From 69464ba3857d02c3bfa50ef4b3bef61cce0fb5f7 Mon Sep 17 00:00:00 2001 From: Gmer4Lfe Date: Fri, 8 May 2026 18:59:29 -0400 Subject: [PATCH] feat(partnership): add blocklist to block access after offboard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After offboard, the former partner's hostname is written to /boot/config/partnership_blocklist.db. This blocks passive reconnection: - --check skips the remote entirely (no auto-reconnect noise) - rsync.sh refuses to sync with a blocklisted host - --onboard warns about the previous offboard but proceeds, and clears the block on success (onboard = deliberate intent) New --unblock mode removes an entry to permit re-onboarding without running a full --onboard first. --status shows the full blocklist. Blocklist file survives reboots (/boot/config) and Tailscale reconnections — application-layer guard that complements Tailscale device removal. Co-Authored-By: Claude Sonnet 4.6 --- Partnership/partnership_manager.sh | 119 ++++++++++++++++++++++++++++- Rsync/rsync.sh | 8 ++ 2 files changed, 126 insertions(+), 1 deletion(-) diff --git a/Partnership/partnership_manager.sh b/Partnership/partnership_manager.sh index 3b40722..83a6be7 100644 --- a/Partnership/partnership_manager.sh +++ b/Partnership/partnership_manager.sh @@ -67,12 +67,26 @@ # PARTNERSHIP_SYNC_INTERVAL — informational — actual schedule in cron # TAILSCALE_API_KEY / TAILSCALE_TAILNET — required when PARTNERSHIP_REMOVE_TAILSCALE=true # +# ── BLOCKLIST ───────────────────────────────────────────────────────────────────────────────── +# After offboard, the former partner's hostname is written to: +# /boot/config/partnership_blocklist.db (format: hostname|timestamp|reason) +# +# --onboard is hard-blocked if the remote is on the blocklist — exits with error. +# --check silently skips remote state reads for blocklisted hosts (no noise every 15min). +# --unblock removes an entry to permit re-onboarding. +# --status shows the full blocklist. +# +# The blocklist persists until explicitly cleared — surviving reboots, array restarts, +# and Tailscale reconnections. Tailscale removal is a separate step at the network layer; +# the blocklist is the application-layer guard. +# # ── USAGE ───────────────────────────────────────────────────────────────────────────────────── # partnership_manage.sh --onboard # partnership_manage.sh --offboard # partnership_manage.sh --transfer --confirm=i-understand-this-transfers-ownership # partnership_manage.sh --check --remote-seen|--remote-unseen # partnership_manage.sh --status +# partnership_manage.sh --unblock # Any mode supports --dry-run and --log # ============================================================================================== @@ -81,6 +95,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/../load_config.sh" SSH_TIMEOUT=15 +BLOCKLIST_FILE="/boot/config/partnership_blocklist.db" # ── Parse mode flags before parse_args ──────────────────────────────────────────────────────── MODE="" @@ -88,6 +103,7 @@ TRANSFER_CONFIRM_INPUT="" REMOTE_SEEN=false REMOTE_UNSEEN=false REASON="manual" +UNBLOCK_HOST="" FILTERED_ARGS=() for arg in "$@"; do @@ -97,11 +113,18 @@ for arg in "$@"; do --transfer) MODE="transfer" ;; --check) MODE="check" ;; --status) MODE="status" ;; + --unblock) MODE="unblock" ;; --confirm=*) TRANSFER_CONFIRM_INPUT="${arg#--confirm=}" ;; --remote-seen) REMOTE_SEEN=true ;; --remote-unseen) REMOTE_UNSEEN=true ;; --reason=*) REASON="${arg#--reason=}" ;; - *) FILTERED_ARGS+=("$arg") ;; + *) + if [[ "$MODE" == "unblock" ]] && [[ -z "$UNBLOCK_HOST" ]] && [[ "$arg" != --* ]]; then + UNBLOCK_HOST="$arg" + else + FILTERED_ARGS+=("$arg") + fi + ;; esac done @@ -154,6 +177,7 @@ if [[ -z "$MODE" ]]; then echo " partnership_manage.sh --transfer --confirm=..." echo " partnership_manage.sh --check --remote-seen|--remote-unseen" echo " partnership_manage.sh --status" + echo " partnership_manage.sh --unblock " exit 1 fi @@ -331,6 +355,35 @@ remove_tailscale_device() { error "Failed to remove $hostname from Tailscale — remove manually" } +# ── Blocklist helpers ───────────────────────────────────────────────────────────────────────── + +is_blocklisted() { + local hostname="$1" + [[ -f "$BLOCKLIST_FILE" ]] && grep -q "^${hostname}|" "$BLOCKLIST_FILE" 2>/dev/null +} + +add_to_blocklist() { + local hostname="$1" reason="${2:-offboard}" + local now + now=$(date '+%Y-%m-%d %H:%M:%S') + if ! is_blocklisted "$hostname"; then + echo "${hostname}|${now}|${reason}" >> "$BLOCKLIST_FILE" + log "Blocklisted: $hostname (reason: $reason)" + else + log "$hostname already on blocklist" + fi +} + +remove_from_blocklist() { + local hostname="$1" + if [[ ! -f "$BLOCKLIST_FILE" ]]; then + log "Blocklist empty — nothing to remove" + return 0 + fi + sed -i "/^${hostname}|/d" "$BLOCKLIST_FILE" 2>/dev/null + log "Removed from blocklist: $hostname" +} + # ── FolderView3 integration ─────────────────────────────────────────────────────────────────── FOLDERVIEW3_DIR="/usr/local/emhttp/plugins/folder.view3" FOLDERVIEW3_JSON="/boot/config/plugins/folder.view3/docker.json" @@ -558,6 +611,40 @@ update_master_conf() { fi } +# ============================================================================================== +# ━━━ Unblock ━━━ +# ============================================================================================== +if [[ "$MODE" == "unblock" ]]; then + if [[ -z "$UNBLOCK_HOST" ]]; then + error "Usage: partnership_manager.sh --unblock " + error "Example: partnership_manager.sh --unblock unRAID-Jayred365" + if [[ -f "$BLOCKLIST_FILE" ]] && [[ -s "$BLOCKLIST_FILE" ]]; then + echo "" + echo "Currently blocklisted:" + while IFS='|' read -r host ts reason; do + echo " $host (blocked $ts — $reason)" + done < "$BLOCKLIST_FILE" + else + echo "Blocklist is empty." + fi + exit 1 + fi + + if ! is_blocklisted "$UNBLOCK_HOST"; then + warn "$UNBLOCK_HOST is not on the blocklist" + exit 0 + fi + + if [[ "$DRY_RUN" == true ]]; then + warn "DRY RUN — would remove $UNBLOCK_HOST from blocklist" + exit 0 + fi + + remove_from_blocklist "$UNBLOCK_HOST" + warn "$UNBLOCK_HOST unblocked — re-onboarding is now permitted ✅" + exit 0 +fi + # ============================================================================================== # ━━━ Status ━━━ # ============================================================================================== @@ -647,6 +734,16 @@ if [[ "$MODE" == "status" ]]; then echo " ⚠️ Remote offline counter: ${OFFLINE_DAYS}/${PARTNERSHIP_OFFLINE_THRESHOLD}" fi + echo "" + if [[ -f "$BLOCKLIST_FILE" ]] && [[ -s "$BLOCKLIST_FILE" ]]; then + echo " Blocklist:" + while IFS='|' read -r host ts reason; do + echo " ⛔ $host (blocked $ts — $reason)" + done < "$BLOCKLIST_FILE" + else + echo " Blocklist: empty" + fi + echo "━━━━━━━━━━━━━━━━━━━━━━━" exit 0 fi @@ -681,6 +778,12 @@ if [[ "$MODE" == "check" ]]; then log "Partnership check — remote unseen ($OFFLINE_COUNT/$THRESHOLD_INTERVALS)" fi + # Skip blocklisted partners — they can't auto-reconnect; only --onboard re-establishes + if is_blocklisted "$REMOTE_SERVER_NAME"; then + log "Partnership check — $REMOTE_SERVER_NAME is blocklisted, skipping" + exit 0 + fi + # Read remote state file REMOTE_IP=$(tailscale ip -4 "${REMOTE_SERVER_NAME,,}" 2>/dev/null) if [[ -z "$REMOTE_IP" ]]; then @@ -775,6 +878,12 @@ if [[ "$MODE" == "onboard" ]]; then fi fi + # Warn if re-onboarding a previously blocked partner — onboard is deliberate so it proceeds + if is_blocklisted "$MIRROR"; then + warn "$MIRROR is on the blocklist from a previous offboard" + warn "Proceeding — blocklist will be cleared on successful onboard" + fi + log "Owner: $OWNER_ID ($OWNER)" log "Mirror: $MIRROR_ID ($MIRROR)" @@ -838,6 +947,8 @@ if [[ "$MODE" == "onboard" ]]; then write_state_file "$LOCAL_STATE_FILE" "ACTIVE" "$NOW" "" "$LOCAL_SERVER_NAME" "onboard" log "Local state: ACTIVE ✅" + [[ "$DRY_RUN" == false ]] && remove_from_blocklist "$MIRROR" + push_state_to_remote "$LOCAL_STATE_FILE" "$MIRROR_IP" "$MIRROR_SSH_KEY" echo "0" > "$OFFLINE_COUNTER" @@ -924,6 +1035,8 @@ if [[ "$MODE" == "offboard" ]]; then "INACTIVE" "" "$NOW" "$LOCAL_SERVER_NAME" "$REASON" log "Local state: INACTIVE ✅" + [[ "$DRY_RUN" == false ]] && add_to_blocklist "$OWNER" "$REASON" + OWNER_IP=$(tailscale ip -4 "${OWNER,,}" 2>/dev/null) if [[ -n "$OWNER_IP" ]]; then push_state_to_remote "$LOCAL_STATE_FILE" "$OWNER_IP" "$MIRROR_SSH_KEY" @@ -937,6 +1050,7 @@ if [[ "$MODE" == "offboard" ]]; then echo "━━━━━ $ICON_SUMMARY OFFBOARD SUMMARY ━━━━━" echo " Your WebUIs: reconfigured → localhost ✅" echo " State: INACTIVE ✅" + echo " Blocklist: $OWNER blocked — re-onboard to permit access again ✅" echo " Owner: will finalise + final sync on next --check ✅" echo "━━━━━━━━━━━━━━━━━━━━━━━" exit 0 @@ -1007,6 +1121,8 @@ if [[ "$MODE" == "offboard" ]]; then "INACTIVE" "" "$NOW" "$LOCAL_SERVER_NAME" "$REASON" log "Local state: INACTIVE ✅" + [[ "$DRY_RUN" == false ]] && add_to_blocklist "$MIRROR" "$REASON" + if [[ "$MIRROR_REACHABLE" == true ]]; then push_state_to_remote "$LOCAL_STATE_FILE" "$MIRROR_IP" "$MIRROR_SSH_KEY" fi @@ -1057,6 +1173,7 @@ if [[ "$MODE" == "offboard" ]]; then echo " WebUI failures: $WEBUI_FAILURES" echo " Critical rsync: disabled ✅" echo " State: INACTIVE ✅" + echo " Blocklist: $MIRROR blocked — re-onboard to permit access again ✅" [[ "${PARTNERSHIP_FOLDERVIEW3:-false}" == true ]] && \ echo " FolderView3: ${PARTNER_FOLDER_NAME:-} cleaned ✅" [[ "${PARTNERSHIP_REMOVE_TAILSCALE:-true}" == true ]] && \ diff --git a/Rsync/rsync.sh b/Rsync/rsync.sh index f1b4921..960ecd7 100644 --- a/Rsync/rsync.sh +++ b/Rsync/rsync.sh @@ -107,6 +107,14 @@ if ! check_rsync_enabled; then exit 0 fi +# Blocklist gate — refuse to sync with a partner blocked after offboard +BLOCKLIST_FILE="/boot/config/partnership_blocklist.db" +if [[ -f "$BLOCKLIST_FILE" ]] && grep -q "^${REMOTE_SERVER_NAME}|" "$BLOCKLIST_FILE" 2>/dev/null; then + error "Rsync blocked — $REMOTE_SERVER_NAME is on the partnership blocklist" + error "Re-onboard the partnership to restore access: partnership_manager.sh --onboard" + exit 1 +fi + resolve_remote_ip # ── Profile inference ─────────────────────────────────────────────────────────────────────────