feat: hardened scripts

This commit is contained in:
Krzysztof kuhy Rudnicki 2026-02-02 21:36:27 +01:00
parent 3b71cf33ee
commit 16e5a47321
25 changed files with 4847 additions and 1822 deletions

View File

@ -41,3 +41,23 @@ This repo automates Linux desktop bootstrap, hardening, and i3 setup. Its pri
- Add new periodic behaviors as templates under `scripts/system-maintenance/bin` and `.../systemd`, then extend `setup_periodic_system.sh` to install/enable them. - Add new periodic behaviors as templates under `scripts/system-maintenance/bin` and `.../systemd`, then extend `setup_periodic_system.sh` to install/enable them.
- Extend package policy by updating `scripts/digital_wellbeing/pacman/pacman_blocked_keywords.txt` or by adding `check_for_<pkg>` + `prompt_for_<pkg>_challenge` blocks in the wrapper. - Extend package policy by updating `scripts/digital_wellbeing/pacman/pacman_blocked_keywords.txt` or by adding `check_for_<pkg>` + `prompt_for_<pkg>_challenge` blocks in the wrapper.
- Run `scripts/meta/shell_check.sh` to detect things to fix before committing. - Run `scripts/meta/shell_check.sh` to detect things to fix before committing.
## Detailed LLM Documentation
For in-depth understanding of specific components, see these dedicated guides:
- **Hosts Guard**: [hosts/guard/README_FOR_LLM.md](../hosts/guard/README_FOR_LLM.md) - Protection layers, canonical copies, path watchers
- **Pacman Wrapper**: [scripts/digital_wellbeing/pacman/README_FOR_LLM.md](../scripts/digital_wellbeing/pacman/README_FOR_LLM.md) - Policy files, integrity checks, challenges
- **Midnight Shutdown**: [scripts/digital_wellbeing/README_MIDNIGHT_SHUTDOWN_LLM.md](../scripts/digital_wellbeing/README_MIDNIGHT_SHUTDOWN_LLM.md) - Schedule protection, timer system
- **Compulsive Block**: [scripts/digital_wellbeing/README_COMPULSIVE_BLOCK_LLM.md](../scripts/digital_wellbeing/README_COMPULSIVE_BLOCK_LLM.md) - App launch limiting
- **Security Analysis**: [docs/SECURITY_HARDENING_ANALYSIS.md](../docs/SECURITY_HARDENING_ANALYSIS.md) - Vulnerabilities and implementation roadmap
## Digital Wellbeing Components Summary
| Component | Purpose | Key Files |
|-----------|---------|-----------|
| Hosts Guard | Block websites via /etc/hosts | `hosts/install.sh`, `hosts/guard/*` |
| Pacman Wrapper | Block package installation | `scripts/digital_wellbeing/pacman/*` |
| Midnight Shutdown | Auto-shutdown at night | `scripts/digital_wellbeing/setup_midnight_shutdown.sh` |
| Compulsive Block | Limit app launches | `scripts/digital_wellbeing/block_compulsive_opening.sh` |
| Music Wrapper | Block music during focus | `scripts/digital_wellbeing/youtube-music-wrapper.sh` |
| Screen Locker | Require workout to unlock | External: `~/testsAndMisc/python_pkg/screen_locker/` |

View File

@ -0,0 +1,696 @@
# Security Hardening Analysis & Implementation Prompt
## Executive Summary
This document analyzes six digital wellbeing/security scripts and provides a detailed implementation prompt for hardening them against tampering. The analysis is based on thorough code review of the entire codebase.
---
## Part 1: Current State Analysis
### 1. `/etc/hosts` Protection System
**Files involved:**
- [hosts/install.sh](../hosts/install.sh) - Main hosts installer
- [hosts/guard/setup_hosts_guard.sh](../hosts/guard/setup_hosts_guard.sh) - Guard layer setup
- [hosts/guard/enforce-hosts.sh](../hosts/guard/enforce-hosts.sh) - Enforcement script
- [hosts/guard/psychological/unlock-hosts.sh](../hosts/guard/psychological/unlock-hosts.sh) - Delayed unlock
**Current Protection Layers:**
1. ✅ Immutable attribute (`chattr +i`)
2. ✅ Canonical copy at `/usr/local/share/locked-hosts`
3. ✅ Path watcher (`hosts-guard.path`) auto-restores on modification
4. ✅ Read-only bind mount (`hosts-bind-mount.service`)
5. ✅ Custom entries protection (blocks removal of blocked domains)
6. ✅ Shell history suppression for `unlock-hosts` command
**CRITICAL VULNERABILITY IDENTIFIED:**
- ❌ **NO protection for `/etc/nsswitch.conf`** - A user can simply edit nsswitch.conf and remove `files` from the `hosts:` line, completely bypassing ALL /etc/hosts protections without touching the hosts file itself!
**Example bypass:**
```bash
# Original: hosts: mymachines resolve [!UNAVAIL=return] files myhostname dns
# Tampered: hosts: mymachines resolve [!UNAVAIL=return] myhostname dns
# Result: /etc/hosts is completely ignored by the system
```
---
### 2. Midnight Shutdown System
**Files involved:**
- [scripts/digital_wellbeing/setup_midnight_shutdown.sh](../scripts/digital_wellbeing/setup_midnight_shutdown.sh) (1359 lines)
**Current Protection Layers:**
1. ✅ Immutable attribute on `/etc/shutdown-schedule.conf`
2. ✅ Canonical copy at `/usr/local/share/locked-shutdown-schedule.conf`
3. ✅ Path watcher restores config if tampered
4. ✅ Schedule protection blocks making schedule more lenient
5. ✅ Unlock script with psychological delay
**VULNERABILITIES IDENTIFIED:**
- ❌ The unlock script **explicitly tells users how to bypass**: "sudo /usr/local/sbin/unlock-shutdown-schedule"
- ❌ The schedule change logic is communicated in the error message
- ❌ No protection against stopping/disabling the timer services
- ❌ No protection against modifying the check script at `/usr/local/bin/day-specific-shutdown-check.sh`
---
### 3. Screen Locker (Python - External Repo)
**File:** `/home/kuhy/testsAndMisc/python_pkg/screen_locker/screen_lock.py`
**Current Workout Types:**
1. Running - distance, time, pace validation
2. Strength - exercises, sets, reps, weights, total calculation
3. Table Tennis - duration, sets, points won/lost
**VULNERABILITIES IDENTIFIED:**
- ❌ **Running option too easy to fake** - just enter plausible numbers
- ❌ **Table Tennis lacks real verification** - no mathematical cross-check
- ❌ Users can close the window via keyboard shortcuts (Alt+F4, etc.)
- ❌ The unlock mechanism is too simple once you know the forms
- ❌ Shutdown time adjustment is a REWARD for working out (can be exploited)
---
### 4. Pacman Wrapper
**Files involved:**
- [scripts/digital_wellbeing/pacman/pacman_wrapper.sh](../scripts/digital_wellbeing/pacman/pacman_wrapper.sh) (823 lines)
- [scripts/digital_wellbeing/pacman/pacman_blocked_keywords.txt](../scripts/digital_wellbeing/pacman/pacman_blocked_keywords.txt)
- [scripts/digital_wellbeing/pacman/install_pacman_wrapper.sh](../scripts/digital_wellbeing/pacman/install_pacman_wrapper.sh)
**Current Protection:**
1. ✅ Policy file integrity verification (SHA256)
2. ✅ Blocked keywords list
3. ✅ Greylist with challenge
4. ✅ VirtualBox hardcoded check (cannot bypass via policy files)
5. ✅ Steam weekend-only restriction
**VULNERABILITIES IDENTIFIED:**
- ❌ **Google Chrome not blocked** - `google-chrome` and `google-chrome-stable` missing from blocked list
- ❌ No automatic LeechBlock installation when browsers are detected
- ❌ User can download `.deb`/`.tar.gz` and install manually
---
### 5. Block Compulsive Opening
**File:** [scripts/digital_wellbeing/block_compulsive_opening.sh](../scripts/digital_wellbeing/block_compulsive_opening.sh) (507 lines)
**Current Behavior:**
- Records first open per hour in state file
- Blocks subsequent launches within same hour
- Shows notification when blocked
**CRITICAL VULNERABILITY:**
- ❌ **App stays running indefinitely** - User can:
1. Open app once per hour (allowed)
2. Minimize/hide the window
3. Keep it running forever in background
4. Compulsive checking still happens, just via Alt+Tab instead of launcher
---
### 6. YouTube Music Wrapper
**File:** [scripts/digital_wellbeing/youtube-music-wrapper.sh](../scripts/digital_wellbeing/youtube-music-wrapper.sh)
**Current Behavior:**
- Checks if focus apps (VSCode, games, etc.) are running
- Blocks YouTube Music launch if focus app detected
**REQUESTED ENHANCEMENT:**
- When Steam is open → Block ALL browsers, close any open browsers
- When browsers open → Block Steam, close Steam if running
- This creates mutual exclusion between gaming and browsing
---
## Part 2: Language Considerations
### Shell (Bash) Limitations
**Pros:**
- Native to the system, no dependencies
- Direct access to systemd, chattr, filesystem
- Fast for simple operations
**Cons:**
- No persistent daemon capability (need systemd for that)
- Race conditions in file operations
- Complex state management is fragile
- No proper event loop for window monitoring
- Cannot easily monitor process list in real-time
### Python Advantages for Certain Tasks
**Where Python would be better:**
1. **Process monitoring daemon** - Watch for Steam/browsers in real-time with proper event loop
2. **Window management** - Using `python-xlib` for proper X11 interaction
3. **Complex state machines** - Like the screen locker
4. **Cross-repo integration** - The screen_lock.py already shows good patterns
### Recommendation
| Component | Keep Bash | Move to Python | Reason |
|-----------|-----------|----------------|--------|
| hosts guard | ✅ | | Simple file ops, systemd integration |
| shutdown schedule | ✅ | | Systemd timers, config files |
| screen locker | | ✅ Already | Complex UI, state machine |
| pacman wrapper | ✅ | | Must intercept pacman |
| compulsive block | | ✅ | Needs daemon for auto-close |
| music wrapper | | ✅ | Needs real-time process monitoring |
**New Python Daemon Needed:** A single "digital wellbeing daemon" that:
1. Monitors running processes
2. Auto-closes apps after timeout
3. Enforces Steam/browser mutual exclusion
4. Can be controlled via DBus
---
## Part 3: Implementation Prompt
**Use this prompt in a new conversation to implement the changes:**
---
### IMPLEMENTATION PROMPT
```
I need to implement comprehensive security hardening for a Linux digital wellbeing system.
The codebase is at ~/linux-configuration/ with these components needing changes:
## 1. HOSTS PROTECTION - nsswitch.conf Guard
Location: hosts/guard/
Create a new protection layer for /etc/nsswitch.conf that:
- Monitors nsswitch.conf for changes (systemd path watcher)
- Ensures the "hosts:" line ALWAYS contains "files" before "dns"
- Creates canonical copy at /usr/local/share/locked-nsswitch.conf
- Enforces with chattr +i
- Add to setup_hosts_guard.sh installer
- Must restore automatically if tampered
The nsswitch.conf protection is CRITICAL because removing "files" from the
hosts line completely bypasses /etc/hosts without touching it.
## 2. MIDNIGHT SHUTDOWN - Silent Denial
Location: scripts/digital_wellbeing/setup_midnight_shutdown.sh
Changes needed:
- Remove ALL helpful messages about how to bypass (unlock-shutdown-schedule path)
- When user tries to make schedule more lenient:
- Simply say "Operation not permitted" with NO explanation
- Do NOT mention the unlock script
- Do NOT explain what's being blocked
- Silently restore canonical values
- The unlock script should still exist but be undiscoverable
- Consider renaming unlock script to an obscure name
- Remove the unlock script path from any logs
## 3. SCREEN LOCKER - External Repo
Location: ~/testsAndMisc/python_pkg/screen_locker/screen_lock.py
Changes needed:
- REMOVE the "Running" workout option entirely (too easy to fake)
- For "Table Tennis":
- Require minimum 15 sets played
- Add verification: total_points = points_won + points_lost
- Require that total_points >= sets_played * 11 (minimum points per set)
- Add random math verification question about the scores
- Increase submit delay to 60 seconds
- For "Strength":
- Already has good verification, keep as-is
- Add input focus grabbing to prevent Alt+Tab escape
- Disable window close keyboard shortcuts
## 4. PACMAN WRAPPER - Chrome Block + LeechBlock Auto-Install
Location: scripts/digital_wellbeing/pacman/
Changes needed to pacman_blocked_keywords.txt:
- Add: google-chrome
- Add: google-chrome-stable
- Add: chromium
- Add: ungoogled-chromium
New behavior in pacman_wrapper.sh:
- After ANY browser is detected installed (via pacman -Qq check):
- Automatically run install_leechblock.sh if it exists
- LeechBlock installer should:
- Detect browser type
- Install extension with pre-configured blocking rules
- Use firefox-addon-install method or chrome native messaging
- If LeechBlock installation fails, BLOCK the browser binary (wrap it)
## 5. BLOCK COMPULSIVE OPENING - Auto-Close Timer
Location: scripts/digital_wellbeing/block_compulsive_opening.sh
New behavior:
- After app is allowed to open, start a background timer
- After 10 minutes, forcefully close the app (pkill)
- Show warning notification at 8 minutes ("Closing in 2 minutes")
- The wrapper should spawn a detached monitoring process
- State tracking: record PID and launch time
- Check for zombie PIDs and clean up state
Implementation approach:
```bash
# After exec line in wrapper_main, instead of direct exec:
launch_with_timer() {
local app="$1"
local timeout_minutes=10
local real_binary="$2"
shift 2
# Launch app in background
"$real_binary" "$@" &
local app_pid=$!
# Record state
echo "$app_pid $(date +%s)" > "$STATE_DIR/${app}.running"
# Spawn killer daemon (detached)
(
sleep $((timeout_minutes * 60))
if kill -0 $app_pid 2>/dev/null; then
notify "$app" "Session timeout - closing now" critical
kill $app_pid 2>/dev/null
sleep 2
kill -9 $app_pid 2>/dev/null || true
fi
rm -f "$STATE_DIR/${app}.running"
) &
disown
# Wait for app to exit
wait $app_pid 2>/dev/null || true
}
```
## 6. YOUTUBE MUSIC → STEAM/BROWSER MUTUAL EXCLUSION
This requires a more sophisticated approach. Create a new Python daemon.
Location: scripts/digital_wellbeing/focus_mode_daemon.py (new file)
Behavior:
- Run as a systemd user service
- Monitor running processes continuously
- When Steam (steam_app_* or steam game processes) detected:
- Kill any running browsers (firefox, chrome, brave, etc.)
- Block browser launches (via wrapper modification or DBus signal)
- Show notification: "Gaming mode active - browsers disabled"
- When any browser detected:
- Kill Steam processes
- Block Steam launches
- Show notification: "Browsing mode active - Steam disabled"
- Mutual exclusion: whichever started first "wins"
- The youtube-music-wrapper.sh should also check for this daemon's signals
## ADDITIONAL REQUIREMENTS
1. All changes must be idempotent (can re-run safely)
2. All protection mechanisms should fail-closed (if service dies, restrictions remain)
3. Log all tampering attempts to /var/log/digital-wellbeing-guard.log
4. Create a single test script that verifies all protections work
5. Update the .github/copilot-instructions.md with the new components
## FILES TO CREATE/MODIFY
New files:
- hosts/guard/nsswitch-guard.path
- hosts/guard/nsswitch-guard.service
- hosts/guard/enforce-nsswitch.sh
- scripts/digital_wellbeing/focus_mode_daemon.py
- scripts/digital_wellbeing/install_focus_mode_daemon.sh
- tests/test_security_hardening.sh
Modified files:
- hosts/guard/setup_hosts_guard.sh (add nsswitch protection)
- scripts/digital_wellbeing/setup_midnight_shutdown.sh (remove helpful messages)
- scripts/digital_wellbeing/pacman/pacman_blocked_keywords.txt (add chrome)
- scripts/digital_wellbeing/pacman/pacman_wrapper.sh (leechblock auto-install)
- scripts/digital_wellbeing/block_compulsive_opening.sh (auto-close timer)
- scripts/digital_wellbeing/youtube-music-wrapper.sh (daemon integration)
External repo (separate changes):
- ~/testsAndMisc/python_pkg/screen_locker/screen_lock.py (remove running, harden table tennis)
```
---
## Part 4: Agent Personas
### Agent: Hosts Guard Expert
```
You are an expert on the linux-configuration hosts guard system. You understand:
FILES YOU KNOW:
- hosts/install.sh - Downloads StevenBlack hosts, adds custom entries, protects with chattr
- hosts/guard/setup_hosts_guard.sh - Installs all guard layers (path watcher, bind mount, unlock script)
- hosts/guard/enforce-hosts.sh - Called when tampering detected, restores from canonical
- hosts/guard/psychological/unlock-hosts.sh - 45-second delay, logs reason, opens editor
- hosts/guard/hosts-guard.path/.service - Systemd path watcher
- hosts/guard/hosts-bind-mount.service - Read-only bind mount
- hosts/guard/pacman-hooks/*.sh - Pre/post transaction hooks for pacman
KEY CONCEPTS:
- Canonical copy at /usr/local/share/locked-hosts
- Custom entries state at /etc/hosts.custom-entries.state
- Multi-layer defense: chattr + path watcher + bind mount
- Shell history suppression for unlock commands
COMMON TASKS:
- Adding new blocked domains: Edit hosts/install.sh heredoc section
- Temporarily allowing edits: sudo /usr/local/sbin/unlock-hosts
- Checking status: lsattr /etc/hosts, systemctl status hosts-guard.path
GOTCHAS:
- Must run hosts/install.sh BEFORE setup_hosts_guard.sh
- Removing custom entries is blocked by protection mechanism
- nsswitch.conf bypass is currently unprotected (needs fix)
```
### Agent: Shutdown Schedule Expert
```
You are an expert on the midnight shutdown system. You understand:
FILES YOU KNOW:
- scripts/digital_wellbeing/setup_midnight_shutdown.sh - Main installer (1300+ lines)
- /etc/shutdown-schedule.conf - Runtime config (MON_WED_HOUR, THU_SUN_HOUR, MORNING_END_HOUR)
- /usr/local/share/locked-shutdown-schedule.conf - Canonical protected copy
- /usr/local/bin/day-specific-shutdown-check.sh - Checks if in shutdown window
- /usr/local/bin/day-specific-shutdown-manager.sh - Status/management
- /etc/systemd/system/day-specific-shutdown.timer/.service - Systemd timer
- /etc/systemd/system/shutdown-schedule-guard.path/.service - Config protection
KEY CONCEPTS:
- Day-specific windows: Mon-Wed vs Thu-Sun have different hours
- Making schedule STRICTER (earlier) = allowed without delay
- Making schedule MORE LENIENT (later) = blocked or requires unlock
- MORNING_END_HOUR cannot be lowered (would shorten window)
- Monitor service re-enables timer if user disables it
PROTECTION LAYERS:
1. Script checks canonical config, blocks lenient changes
2. Config file has chattr +i
3. Path watcher restores if file modified
4. Canonical copy takes precedence
INTEGRATION:
- i3blocks shutdown_countdown.sh reads the config
- screen_lock.py can adjust shutdown time (reward/punishment)
```
### Agent: Pacman Wrapper Expert
```
You are an expert on the pacman wrapper security system. You understand:
FILES YOU KNOW:
- scripts/digital_wellbeing/pacman/pacman_wrapper.sh - Main wrapper (823 lines)
- scripts/digital_wellbeing/pacman/install_pacman_wrapper.sh - Backs up real pacman
- scripts/digital_wellbeing/pacman/pacman_blocked_keywords.txt - Always blocked
- scripts/digital_wellbeing/pacman/pacman_whitelist.txt - Exceptions to keywords
- scripts/digital_wellbeing/pacman/pacman_greylist.txt - Challenge required
- scripts/digital_wellbeing/pacman/words.txt - Word scramble challenge words
- /var/lib/pacman-wrapper/policy.sha256 - Integrity checksums
KEY CONCEPTS:
- Real pacman at /usr/bin/pacman.orig, wrapper symlinked to /usr/bin/pacman
- Policy integrity verification via SHA256 before ANY operation
- Three tiers: blocked (always denied), greylist (challenge), whitelist (bypass)
- VirtualBox check is HARDCODED (cannot bypass via policy files)
- Steam is weekend-only with word scramble challenge
POLICY ENFORCEMENT:
1. Load policy lists from text files
2. Verify integrity hashes match
3. Check if package matches blocked keywords (unless whitelisted)
4. Check if greylisted (requires challenge)
5. After transaction, remove any blocked packages that got installed
HOSTS INTEGRATION:
- Calls /usr/local/share/hosts-guard/pacman-pre-unlock-hosts.sh before transaction
- Calls pacman-post-relock-hosts.sh after transaction
- Enforces VirtualBox hosts sharing if vbox detected
MAINTENANCE INTEGRATION:
- Auto-runs setup_periodic_system.sh if maintenance services missing
```
### Agent: Compulsive Opening Blocker Expert
```
You are an expert on the block_compulsive_opening.sh script. You understand:
FILES YOU KNOW:
- scripts/digital_wellbeing/block_compulsive_opening.sh - Main script (507 lines)
- /usr/local/bin/block-compulsive-opening.sh - Installed location
- ~/.local/state/compulsive-block/*.lastopen - Per-app state files
- ~/.local/state/compulsive-block/compulsive-block.log - Activity log
- /etc/pacman.d/hooks/95-compulsive-block-rewrap.hook - Auto-rewrap hook
MANAGED APPS:
- beeper → /opt/beeper/beepertexts
- signal-desktop → /usr/lib/signal-desktop/signal-desktop
- discord → /opt/discord/Discord
KEY CONCEPTS:
- Wrapper replaces /usr/bin/<app>, original saved as .orig or SYMLINK: marker
- Hour-based tracking: YYYY-MM-DD-HH format
- First launch per hour allowed, subsequent launches blocked
- Pacman hook re-installs wrappers after package updates
WRAPPER FLOW:
1. wrapper_main() called with app name
2. Check was_opened_this_hour()
3. If yes: block_app() + notification + exit 1
4. If no: record_opening() + exec real binary
LIMITATION (needs fix):
- Once app is launched, it can run indefinitely
- User can minimize and keep checking via Alt+Tab
- Needs auto-close timer functionality
```
### Agent: Screen Locker Expert
```
You are an expert on the screen_lock.py workout locker. You understand:
FILE LOCATION: ~/testsAndMisc/python_pkg/screen_locker/screen_lock.py (1261 lines)
PURPOSE:
- Full-screen lock requiring workout verification to unlock
- Integrates with shutdown schedule system
WORKOUT TYPES:
1. Running: distance, time, pace with cross-validation
2. Strength: exercises, sets, reps, weights with total calculation
3. Table Tennis: duration, sets, points won/lost
4. Sick Day: 2-minute wait, shutdown moved 1.5h earlier
KEY FEATURES:
- 30-second delay before submit button enabled
- Cross-validation (e.g., pace = time / distance)
- 15% tolerance on calculated values
- Demo mode (10s lockout) vs Production mode (30min lockout)
- JSON workout log stored in same directory
SHUTDOWN INTEGRATION:
- _adjust_shutdown_time_earlier() - sick day penalty
- _adjust_shutdown_time_later() - workout reward (+1.5h)
- Uses adjust_shutdown_schedule.sh helper script
- Sick day state tracked in sick_day_state.json
SECURITY CONCERNS (needs fix):
- Running option too easy to fake
- Table tennis lacks rigorous validation
- Window can potentially be closed via keyboard
```
---
## Part 5: LLM README Files
These should be created in the respective directories:
### [hosts/guard/README_FOR_LLM.md](to be created)
```markdown
# Hosts Guard System - LLM Reference
## Purpose
Prevent tampering with /etc/hosts to maintain website blocking.
## Architecture
```
/etc/hosts (immutable) ←── canonical (/usr/local/share/locked-hosts)
path watcher detects changes
enforce-hosts.sh restores
```
## Critical Files
| File | Purpose | Protected By |
|------|---------|--------------|
| /etc/hosts | Actual hosts file | chattr +i, bind mount |
| /usr/local/share/locked-hosts | Canonical copy | chattr +i |
| /etc/hosts.custom-entries.state | Tracks blocked domains | chattr +i |
## Commands to Know
```bash
# Check protection status
lsattr /etc/hosts
systemctl status hosts-guard.path hosts-bind-mount.service
# Legitimate edit (with delay)
sudo /usr/local/sbin/unlock-hosts
# Reinstall/repair
sudo ~/linux-configuration/hosts/install.sh
sudo ~/linux-configuration/hosts/guard/setup_hosts_guard.sh
```
## DO NOT
- Edit /etc/nsswitch.conf (bypasses hosts entirely)
- Stop hosts-guard.path without understanding consequences
- Remove entries from install.sh without state file cleanup
```
### [scripts/digital_wellbeing/pacman/README_FOR_LLM.md](to be created)
```markdown
# Pacman Wrapper - LLM Reference
## Purpose
Intercept pacman to enforce package installation policies.
## Architecture
```
/usr/bin/pacman (symlink) → pacman_wrapper.sh
/usr/bin/pacman.orig (real)
```
## Policy Files
| File | Purpose |
|------|---------|
| pacman_blocked_keywords.txt | Substring match = always blocked |
| pacman_whitelist.txt | Exact names that bypass blocking |
| pacman_greylist.txt | Requires challenge to install |
| words.txt | Word scramble challenge source |
## Hardcoded Checks (cannot bypass via files)
- VirtualBox → security challenge + hosts enforcement
- Steam → weekend-only + word scramble
## Integration Points
1. Hosts guard (pre/post hooks)
2. Periodic maintenance (auto-setup if missing)
3. VirtualBox hosts enforcement
## Adding Blocks
```bash
# Edit the blocked keywords file
echo "newpackage" >> pacman_blocked_keywords.txt
# Re-run installer to update checksums
sudo ./install_pacman_wrapper.sh
```
```
---
## Part 6: Test Script Template
```bash
#!/bin/bash
# tests/test_security_hardening.sh
# Verify all security mechanisms are working
set -euo pipefail
PASS=0
FAIL=0
test_result() {
local name="$1"
local result="$2"
if [[ $result == "pass" ]]; then
echo "✅ PASS: $name"
((PASS++))
else
echo "❌ FAIL: $name"
((FAIL++))
fi
}
# Test 1: /etc/hosts is immutable
if lsattr /etc/hosts 2>/dev/null | grep -q '^....i'; then
test_result "/etc/hosts is immutable" "pass"
else
test_result "/etc/hosts is immutable" "fail"
fi
# Test 2: hosts-guard.path is active
if systemctl is-active --quiet hosts-guard.path; then
test_result "hosts-guard.path is active" "pass"
else
test_result "hosts-guard.path is active" "fail"
fi
# Test 3: shutdown-schedule.conf is immutable
if lsattr /etc/shutdown-schedule.conf 2>/dev/null | grep -q '^....i'; then
test_result "/etc/shutdown-schedule.conf is immutable" "pass"
else
test_result "/etc/shutdown-schedule.conf is immutable" "fail"
fi
# Test 4: pacman wrapper is installed
if [[ -L /usr/bin/pacman ]] && [[ -f /usr/bin/pacman.orig ]]; then
test_result "pacman wrapper installed" "pass"
else
test_result "pacman wrapper installed" "fail"
fi
# Test 5: google-chrome is blocked
if grep -qi "google-chrome" ~/linux-configuration/scripts/digital_wellbeing/pacman/pacman_blocked_keywords.txt; then
test_result "google-chrome in blocked list" "pass"
else
test_result "google-chrome in blocked list" "fail"
fi
# Summary
echo ""
echo "=========================================="
echo "Results: $PASS passed, $FAIL failed"
echo "=========================================="
exit $FAIL
```
---
## Conclusion
This analysis identifies critical vulnerabilities and provides a comprehensive implementation prompt. The most urgent issues are:
1. **nsswitch.conf bypass** - Completely unprotected, defeats all hosts protections
2. **Information disclosure** - Shutdown system tells users how to bypass
3. **App lifetime** - Compulsive blockers don't limit session duration
4. **Browser gaps** - Chrome not blocked, no LeechBlock auto-install
The implementation prompt above should be used in a focused coding session to address all issues systematically.

View File

@ -0,0 +1,205 @@
# Hosts Guard System - LLM Reference Guide
> **For AI assistants**: This document explains how the hosts guard system works so you can make correct modifications.
## System Purpose
Prevent tampering with `/etc/hosts` to maintain website blocking (YouTube, social media, etc.) as part of a digital wellbeing system.
## Architecture Overview
```
┌─────────────────────────────────────────────────────────────────────┐
│ PROTECTION LAYERS │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Layer 1: Immutable Attribute │
│ ───────────────────────────── │
│ /etc/hosts has chattr +i (cannot be modified even by root) │
│ │
│ Layer 2: Canonical Copy │
│ ─────────────────────── │
│ /usr/local/share/locked-hosts contains the "true" version │
│ If /etc/hosts differs, it gets overwritten from this copy │
│ │
│ Layer 3: Path Watcher (systemd) │
│ ────────────────────────────── │
│ hosts-guard.path watches /etc/hosts for ANY change │
│ hosts-guard.service runs enforce-hosts.sh when triggered │
│ │
│ Layer 4: Read-Only Bind Mount │
│ ──────────────────────────── │
│ hosts-bind-mount.service mounts /etc/hosts read-only │
│ Even if chattr is removed, write operations fail │
│ │
│ Layer 5: Custom Entries Protection │
│ ───────────────────────────────── │
│ /etc/hosts.custom-entries.state tracks blocked domains │
│ Prevents removal of domains from install.sh │
│ │
│ Layer 6: nsswitch.conf Protection (NEW) │
│ ─────────────────────────────────────── │
│ Prevents bypass via /etc/nsswitch.conf manipulation │
│ Ensures "files" always appears in hosts: line before "dns" │
│ nsswitch-guard.path watches for changes │
│ Canonical copy at /usr/local/share/locked-nsswitch.conf │
│ │
└─────────────────────────────────────────────────────────────────────┘
```
## File Locations
| File | Purpose | Protection |
|------|---------|------------|
| `/etc/hosts` | Active hosts file | chattr +i, bind mount |
| `/usr/local/share/locked-hosts` | Canonical source of truth | chattr +i |
| `/etc/hosts.custom-entries.state` | Tracks custom blocked domains | chattr +i |
| `/etc/hosts.stevenblack` | Cached upstream hosts file | None |
| `/etc/nsswitch.conf` | Name service switch config | chattr +i, path watcher |
| `/usr/local/share/locked-nsswitch.conf` | Canonical nsswitch copy | chattr +i |
| `/usr/local/sbin/enforce-hosts.sh` | Restoration script | File permissions |
| `/usr/local/sbin/enforce-nsswitch.sh` | nsswitch enforcement | File permissions |
| `/usr/local/sbin/unlock-hosts` | Psychological unlock script | File permissions |
| `/etc/systemd/system/hosts-guard.path` | Path watcher unit | systemd |
| `/etc/systemd/system/hosts-guard.service` | Enforcement service | systemd |
| `/etc/systemd/system/hosts-bind-mount.service` | RO bind mount | systemd |
| `/etc/systemd/system/nsswitch-guard.path` | nsswitch watcher | systemd |
| `/etc/systemd/system/nsswitch-guard.service` | nsswitch enforce | systemd |
## Key Scripts
### hosts/install.sh
- Downloads StevenBlack hosts list (cached at `/etc/hosts.stevenblack`)
- Adds custom blocking entries (YouTube, etc.)
- Comments out allowed sites (4chan, Facebook)
- Runs protection check for custom entries
- Sets up initial immutable attribute
### hosts/guard/setup_hosts_guard.sh
Installs all protection layers:
- Creates canonical snapshot
- Installs enforce-hosts.sh and unlock-hosts scripts
- Enables systemd path watcher
- Enables bind mount service
- Installs shell history suppression hooks
### hosts/guard/enforce-hosts.sh
Called when tampering detected:
```bash
# Compares /etc/hosts to canonical
# If different: restores from canonical, logs event
# Re-applies chattr +i
```
### hosts/guard/psychological/unlock-hosts.sh
Legitimate edit workflow:
1. Prompts for reason (logged)
2. Stops protection services
3. Waits 45 seconds (cooling off)
4. Opens editor
5. Updates canonical if changes made
6. Re-enables all protections
## Pacman Integration
The pacman wrapper calls these hooks during package transactions:
- `/usr/local/share/hosts-guard/pacman-pre-unlock-hosts.sh` - Before transaction
- `/usr/local/share/hosts-guard/pacman-post-relock-hosts.sh` - After transaction
These temporarily unlock hosts for package manager operations.
## Common Tasks
### Adding a New Blocked Domain
1. Edit `hosts/install.sh`
2. Find the heredoc section after `# Custom blocking entries`
3. Add line: `0.0.0.0 newdomain.com`
4. Run: `sudo ~/linux-configuration/hosts/install.sh`
```bash
# Example: Block example.com
# In hosts/install.sh, add to heredoc:
0.0.0.0 example.com
0.0.0.0 www.example.com
```
### Allowing a Previously Blocked Domain
**This is intentionally difficult.** You must:
1. Remove entry from install.sh heredoc
2. Remove protection: `sudo chattr -i /etc/hosts.custom-entries.state`
3. Edit state file to remove domain
4. Re-run install.sh
### Checking Protection Status
```bash
# Check immutable attribute
lsattr /etc/hosts
# Should show: ----i--------e-- /etc/hosts
# Check services
systemctl status hosts-guard.path hosts-guard.service hosts-bind-mount.service
# Check canonical exists
ls -la /usr/local/share/locked-hosts
```
### Legitimate Editing
```bash
sudo /usr/local/sbin/unlock-hosts
# Enter reason when prompted
# Wait 45 seconds
# Edit in your $EDITOR
# Changes auto-saved to canonical
```
## nsswitch.conf Protection (Layer 6)
**Why this matters:** A user could bypass ALL /etc/hosts protections by simply editing `/etc/nsswitch.conf` and removing `files` from the `hosts:` line. This protection layer prevents that.
### How it works:
- `nsswitch-guard.path` watches `/etc/nsswitch.conf` for changes
- `nsswitch-guard.service` runs `enforce-nsswitch.sh` when triggered
- Canonical copy stored at `/usr/local/share/locked-nsswitch.conf`
- Validates that `hosts:` line contains `files` before `dns`
- Auto-restores from canonical if tampered
### Check nsswitch protection status:
```bash
lsattr /etc/nsswitch.conf
systemctl status nsswitch-guard.path
```
## Troubleshooting
### "Cannot modify /etc/hosts"
This is expected! Use the unlock script:
```bash
sudo /usr/local/sbin/unlock-hosts
```
### Path watcher not running
```bash
sudo systemctl start hosts-guard.path
sudo systemctl enable hosts-guard.path
```
### Bind mount preventing access
```bash
# Temporarily disable (not recommended)
sudo systemctl stop hosts-bind-mount.service
```
### Custom entries protection blocking install
The protection mechanism detected you're trying to remove previously blocked domains. This is intentional. To proceed, manually edit the state file (see "Allowing a Previously Blocked Domain").
## DO NOT
1. ❌ Edit `/etc/nsswitch.conf` to bypass hosts (this defeats the purpose)
2. ❌ Stop `hosts-guard.path` without understanding consequences
3. ❌ Delete `/usr/local/share/locked-hosts` (breaks restoration)
4. ❌ Remove entries from install.sh without updating state file
5. ❌ Use `chattr -i` without going through unlock-hosts

View File

@ -0,0 +1,103 @@
#!/bin/bash
# Template guard script to enforce canonical /etc/nsswitch.conf
# Ensures "hosts:" line always contains "files" before "dns"
# This prevents bypassing /etc/hosts by removing "files" from nsswitch.conf
# Installed to /usr/local/sbin/enforce-nsswitch.sh by setup_hosts_guard.sh
set -euo pipefail
CANONICAL_SOURCE="/usr/local/share/locked-nsswitch.conf"
TARGET="/etc/nsswitch.conf"
LOG_FILE="/var/log/nsswitch-guard.log"
log() {
printf '%s - %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" | tee -a "$LOG_FILE" >&2
}
# Function to validate that "hosts:" line has correct format
# Must contain "files" before "dns" (or "dns" not present)
validate_hosts_line() {
local line="$1"
# Check if "files" is present
if ! echo "$line" | grep -qw "files"; then
return 1
fi
# If dns is present, files must come before it
if echo "$line" | grep -qw "dns"; then
local files_pos dns_pos
files_pos=$(echo "$line" | grep -bo '\bfiles\b' | head -1 | cut -d: -f1)
dns_pos=$(echo "$line" | grep -bo '\bdns\b' | head -1 | cut -d: -f1)
if [[ -n "$files_pos" && -n "$dns_pos" && "$files_pos" -gt "$dns_pos" ]]; then
return 1
fi
fi
return 0
}
# Check current nsswitch.conf hosts line
current_hosts_line=$(grep '^hosts:' "$TARGET" 2>/dev/null || echo "")
if [[ -z "$current_hosts_line" ]]; then
log "CRITICAL: No 'hosts:' line found in $TARGET - restoring from canonical"
if [[ -f "$CANONICAL_SOURCE" ]]; then
chattr -i "$TARGET" 2>/dev/null || true
cp "$CANONICAL_SOURCE" "$TARGET"
chmod 644 "$TARGET"
chattr +i "$TARGET" 2>/dev/null || log "Failed to set immutable attribute on $TARGET"
log "Restored $TARGET from canonical copy"
else
log "ERROR: Canonical source not found at $CANONICAL_SOURCE"
exit 1
fi
exit 0
fi
if ! validate_hosts_line "$current_hosts_line"; then
log "TAMPERING DETECTED: 'hosts:' line is invalid or missing 'files' before 'dns'"
log "Current line: $current_hosts_line"
if [[ -f "$CANONICAL_SOURCE" ]]; then
chattr -i "$TARGET" 2>/dev/null || true
cp "$CANONICAL_SOURCE" "$TARGET"
chmod 644 "$TARGET"
chattr +i "$TARGET" 2>/dev/null || log "Failed to set immutable attribute on $TARGET"
log "Restored $TARGET from canonical copy"
else
log "ERROR: Canonical source not found at $CANONICAL_SOURCE"
# Emergency fix: add "files" back to hosts line
chattr -i "$TARGET" 2>/dev/null || true
sed -i 's/^hosts:\(.*\)dns/hosts:\1files dns/' "$TARGET"
chattr +i "$TARGET" 2>/dev/null || true
log "Emergency fix applied: added 'files' before 'dns'"
fi
exit 0
fi
# If canonical exists, check for any drift
if [[ -f "$CANONICAL_SOURCE" ]]; then
if ! cmp -s "$CANONICAL_SOURCE" "$TARGET"; then
log "Drift detected in $TARGET (but hosts line valid) - restoring canonical"
chattr -i "$TARGET" 2>/dev/null || true
cp "$CANONICAL_SOURCE" "$TARGET"
chmod 644 "$TARGET"
chattr +i "$TARGET" 2>/dev/null || log "Failed to set immutable attribute"
log "Restored $TARGET from canonical copy"
else
log "No drift detected in $TARGET"
fi
else
log "Creating initial canonical snapshot"
mkdir -p "$(dirname "$CANONICAL_SOURCE")"
cp "$TARGET" "$CANONICAL_SOURCE"
chmod 644 "$CANONICAL_SOURCE"
chattr +i "$CANONICAL_SOURCE" 2>/dev/null || log "Failed to protect canonical copy"
fi
# Ensure immutable attribute is set
chattr -i "$TARGET" 2>/dev/null || true
chattr +i "$TARGET" 2>/dev/null || log "Failed to set immutable attribute on $TARGET"
log "nsswitch.conf enforcement complete"

View File

@ -0,0 +1,9 @@
[Unit]
Description=Watch /etc/nsswitch.conf for tampering (hosts bypass protection)
[Path]
PathChanged=/etc/nsswitch.conf
Unit=nsswitch-guard.service
[Install]
WantedBy=multi-user.target

View File

@ -0,0 +1,12 @@
[Unit]
Description=Enforce canonical /etc/nsswitch.conf (prevents hosts bypass)
After=local-fs.target
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/enforce-nsswitch.sh
Nice=10
IOSchedulingClass=idle
[Install]
WantedBy=multi-user.target

38
hosts/guard/pacman-hooks/hosts-guard-common.sh Normal file → Executable file
View File

@ -7,20 +7,20 @@ LOGTAG=hosts-guard-hook
# Check if target has a read-only mount # Check if target has a read-only mount
is_ro_mount() { is_ro_mount() {
findmnt -no OPTIONS -T "$TARGET" 2> /dev/null | grep -qw ro findmnt -no OPTIONS -T "$TARGET" 2>/dev/null | grep -qw ro
} }
# Count mount layers for the target # Count mount layers for the target
mount_layers_count() { mount_layers_count() {
awk '$5=="/etc/hosts"{c++} END{print c+0}' /proc/self/mountinfo 2> /dev/null || echo 0 awk '$5=="/etc/hosts"{c++} END{print c+0}' /proc/self/mountinfo 2>/dev/null || echo 0
} }
# Collapse all bind mount layers # Collapse all bind mount layers
collapse_mounts() { collapse_mounts() {
local i=0 local i=0
if command -v mountpoint > /dev/null 2>&1; then if command -v mountpoint >/dev/null 2>&1; then
while mountpoint -q "$TARGET"; do while mountpoint -q "$TARGET"; do
umount -l "$TARGET" > /dev/null 2>&1 || break umount -l "$TARGET" >/dev/null 2>&1 || break
i=$((i + 1)) i=$((i + 1))
((i > 20)) && break ((i > 20)) && break
done done
@ -28,7 +28,7 @@ collapse_mounts() {
local cnt local cnt
cnt=$(mount_layers_count) cnt=$(mount_layers_count)
while ((cnt > 1)); do while ((cnt > 1)); do
umount -l "$TARGET" > /dev/null 2>&1 || break umount -l "$TARGET" >/dev/null 2>&1 || break
i=$((i + 1)) i=$((i + 1))
((i > 20)) && break ((i > 20)) && break
cnt=$(mount_layers_count) cnt=$(mount_layers_count)
@ -40,9 +40,9 @@ collapse_mounts() {
stop_units_if_present() { stop_units_if_present() {
local units=(hosts-bind-mount.service hosts-guard.path) local units=(hosts-bind-mount.service hosts-guard.path)
for u in "${units[@]}"; do for u in "${units[@]}"; do
if command -v systemctl > /dev/null 2>&1; then if command -v systemctl >/dev/null 2>&1; then
if systemctl list-unit-files 2> /dev/null | grep -q "^$u"; then if systemctl list-unit-files 2>/dev/null | grep -q "^$u"; then
systemctl stop "$u" > /dev/null 2>&1 || true systemctl stop "$u" >/dev/null 2>&1 || true
fi fi
fi fi
done done
@ -50,35 +50,35 @@ stop_units_if_present() {
# Remove immutable/append-only attributes # Remove immutable/append-only attributes
remove_host_attrs() { remove_host_attrs() {
if command -v lsattr > /dev/null 2>&1; then if command -v lsattr >/dev/null 2>&1; then
local attrs local attrs
attrs=$(lsattr -d "$TARGET" 2> /dev/null || true) attrs=$(lsattr -d "$TARGET" 2>/dev/null || true)
if echo "$attrs" | grep -q " i "; then if echo "$attrs" | grep -q " i "; then
chattr -i "$TARGET" > /dev/null 2>&1 || true chattr -i "$TARGET" >/dev/null 2>&1 || true
fi fi
if echo "$attrs" | grep -q " a "; then if echo "$attrs" | grep -q " a "; then
chattr -a "$TARGET" > /dev/null 2>&1 || true chattr -a "$TARGET" >/dev/null 2>&1 || true
fi fi
fi fi
} }
# Apply immutable attribute # Apply immutable attribute
apply_immutable() { apply_immutable() {
if command -v chattr > /dev/null 2>&1; then if command -v chattr >/dev/null 2>&1; then
chattr +i "$TARGET" > /dev/null 2>&1 || true chattr +i "$TARGET" >/dev/null 2>&1 || true
fi fi
} }
# Apply a single read-only bind mount layer # Apply a single read-only bind mount layer
apply_ro_bind_mount() { apply_ro_bind_mount() {
mount --bind "$TARGET" "$TARGET" > /dev/null 2>&1 || true mount --bind "$TARGET" "$TARGET" >/dev/null 2>&1 || true
mount -o remount,ro,bind "$TARGET" > /dev/null 2>&1 || true mount -o remount,ro,bind "$TARGET" >/dev/null 2>&1 || true
} }
# Start the path watcher service # Start the path watcher service
start_path_watcher() { start_path_watcher() {
if command -v systemctl > /dev/null 2>&1; then if command -v systemctl >/dev/null 2>&1; then
systemctl start hosts-guard.path > /dev/null 2>&1 || true systemctl start hosts-guard.path >/dev/null 2>&1 || true
fi fi
} }
@ -87,5 +87,5 @@ log_hook() {
local phase="$1" local phase="$1"
local state="$2" local state="$2"
logger -t "$LOGTAG" "$phase: $state" logger -t "$LOGTAG" "$phase: $state"
echo "$(date -Is) $phase-$state" >> /run/hosts-guard-hook.log 2> /dev/null || true echo "$(date -Is) $phase-$state" >>/run/hosts-guard-hook.log 2>/dev/null || true
} }

2
hosts/guard/pacman-hooks/pacman-post-relock-hosts.sh Normal file → Executable file
View File

@ -16,7 +16,7 @@ collapse_mounts
# Run enforcement script if available # Run enforcement script if available
if [[ -x $ENFORCE ]]; then if [[ -x $ENFORCE ]]; then
"$ENFORCE" > /dev/null 2>&1 || true "$ENFORCE" >/dev/null 2>&1 || true
fi fi
# Apply protections # Apply protections

3
hosts/guard/pacman-hooks/pacman-pre-unlock-hosts.sh Normal file → Executable file
View File

@ -9,6 +9,7 @@ source "$SCRIPT_DIR/hosts-guard-common.sh"
# Remove protective attributes # Remove protective attributes
remove_host_attrs remove_host_attrs
sudo rm /etc/hosts
# Stop guard services # Stop guard services
stop_units_if_present stop_units_if_present
@ -20,7 +21,7 @@ collapse_mounts
# Ensure writable by remounting if still read-only # Ensure writable by remounting if still read-only
if is_ro_mount; then if is_ro_mount; then
mount -o remount,rw "$TARGET" > /dev/null 2>&1 || collapse_mounts mount -o remount,rw "$TARGET" >/dev/null 2>&1 || collapse_mounts
fi fi
log_hook "pre" "unlocking(done)" log_hook "pre" "unlocking(done)"

2
hosts/guard/psychological/unlock-hosts.sh Normal file → Executable file
View File

@ -35,7 +35,7 @@ for s in hosts-bind-mount.service hosts-guard.path; do
done done
# Remove attributes to allow edit # Remove attributes to allow edit
chattr -i -a "$TARGET" 2> /dev/null || true chattr -i -a "$TARGET" 2>/dev/null || true
echo "Countdown:" >&2 echo "Countdown:" >&2
for ((i = DELAY_SECONDS; i > 0; i--)); do for ((i = DELAY_SECONDS; i > 0; i--)); do

View File

@ -33,6 +33,7 @@ FORCE_SNAPSHOT=0
DO_SNAPSHOT=1 DO_SNAPSHOT=1
ENABLE_BIND=1 ENABLE_BIND=1
ENABLE_PATH=1 ENABLE_PATH=1
ENABLE_NSSWITCH=1
UNINSTALL=0 UNINSTALL=0
DELAY=45 DELAY=45
DRY_RUN=0 DRY_RUN=0
@ -84,6 +85,10 @@ while [[ $# -gt 0 ]]; do
ENABLE_PATH=0 ENABLE_PATH=0
shift shift
;; ;;
--skip-nsswitch)
ENABLE_NSSWITCH=0
shift
;;
--delay) --delay)
DELAY=${2:-} DELAY=${2:-}
[[ -z ${DELAY} ]] && { [[ -z ${DELAY} ]] && {
@ -149,11 +154,17 @@ TEMPLATE_UNLOCK="$SCRIPT_DIR/psychological/unlock-hosts.sh"
UNIT_GUARD_SERVICE="$SCRIPT_DIR/hosts-guard.service" UNIT_GUARD_SERVICE="$SCRIPT_DIR/hosts-guard.service"
UNIT_GUARD_PATH="$SCRIPT_DIR/hosts-guard.path" UNIT_GUARD_PATH="$SCRIPT_DIR/hosts-guard.path"
UNIT_BIND_SERVICE="$SCRIPT_DIR/hosts-bind-mount.service" UNIT_BIND_SERVICE="$SCRIPT_DIR/hosts-bind-mount.service"
TEMPLATE_ENFORCE_NSSWITCH="$SCRIPT_DIR/enforce-nsswitch.sh"
UNIT_NSSWITCH_SERVICE="$SCRIPT_DIR/nsswitch-guard.service"
UNIT_NSSWITCH_PATH="$SCRIPT_DIR/nsswitch-guard.path"
INSTALL_ENFORCE="/usr/local/sbin/enforce-hosts.sh" INSTALL_ENFORCE="/usr/local/sbin/enforce-hosts.sh"
INSTALL_UNLOCK="/usr/local/sbin/unlock-hosts" INSTALL_UNLOCK="/usr/local/sbin/unlock-hosts"
INSTALL_ENFORCE_NSSWITCH="/usr/local/sbin/enforce-nsswitch.sh"
CANON="/usr/local/share/locked-hosts" CANON="/usr/local/share/locked-hosts"
CANON_NSSWITCH="/usr/local/share/locked-nsswitch.conf"
HOSTS="/etc/hosts" HOSTS="/etc/hosts"
NSSWITCH="/etc/nsswitch.conf"
# Shell hook destinations (user agnostic system-wide skeleton + etc profile.d) # Shell hook destinations (user agnostic system-wide skeleton + etc profile.d)
ZSH_FILTER_SNIPPET="/etc/zsh/hosts_guard_history_filter.zsh" ZSH_FILTER_SNIPPET="/etc/zsh/hosts_guard_history_filter.zsh"
@ -166,7 +177,7 @@ SYSTEMD_DIR="/etc/systemd/system"
###################################################################### ######################################################################
if [[ $UNINSTALL -eq 1 ]]; then if [[ $UNINSTALL -eq 1 ]]; then
note "Uninstalling hosts guard components ( protections removed )" note "Uninstalling hosts guard components ( protections removed )"
for u in hosts-guard.path hosts-guard.service hosts-bind-mount.service; do for u in hosts-guard.path hosts-guard.service hosts-bind-mount.service nsswitch-guard.path nsswitch-guard.service; do
if systemctl list-unit-files | grep -q "^$u"; then if systemctl list-unit-files | grep -q "^$u"; then
run systemctl disable --now "$u" || true run systemctl disable --now "$u" || true
fi fi
@ -174,14 +185,17 @@ if [[ $UNINSTALL -eq 1 ]]; then
for f in \ for f in \
"$INSTALL_ENFORCE" \ "$INSTALL_ENFORCE" \
"$INSTALL_UNLOCK" \ "$INSTALL_UNLOCK" \
"$INSTALL_ENFORCE_NSSWITCH" \
"$SYSTEMD_DIR/hosts-guard.service" \ "$SYSTEMD_DIR/hosts-guard.service" \
"$SYSTEMD_DIR/hosts-guard.path" \ "$SYSTEMD_DIR/hosts-guard.path" \
"$SYSTEMD_DIR/hosts-bind-mount.service" \ "$SYSTEMD_DIR/hosts-bind-mount.service" \
"$SYSTEMD_DIR/nsswitch-guard.service" \
"$SYSTEMD_DIR/nsswitch-guard.path" \
"$ZSH_FILTER_SNIPPET" \ "$ZSH_FILTER_SNIPPET" \
"$BASH_FILTER_SNIPPET"; do "$BASH_FILTER_SNIPPET"; do
if [[ -e $f ]]; then run rm -f "$f"; fi if [[ -e $f ]]; then run rm -f "$f"; fi
done done
note "Leaving canonical snapshot at $CANON (remove manually if undesired)." note "Leaving canonical snapshots at $CANON and $CANON_NSSWITCH (remove manually if undesired)."
if [[ $DRY_RUN -eq 0 ]]; then systemctl daemon-reload; fi if [[ $DRY_RUN -eq 0 ]]; then systemctl daemon-reload; fi
msg "Uninstall complete" msg "Uninstall complete"
exit 0 exit 0
@ -246,11 +260,11 @@ if [[ $INSTALL_SHELL_HOOKS -eq 1 ]]; then
msg "Installing shell history suppression hooks for unlock command" msg "Installing shell history suppression hooks for unlock command"
# Pattern matches commands invoking unlock-hosts (with or without sudo) & setup script force snapshot # Pattern matches commands invoking unlock-hosts (with or without sudo) & setup script force snapshot
# Zsh: use zshaddhistory function # Zsh: use zshaddhistory function
if command -v zsh > /dev/null 2>&1; then if command -v zsh >/dev/null 2>&1; then
if [[ $DRY_RUN -eq 1 ]]; then if [[ $DRY_RUN -eq 1 ]]; then
echo "DRY-RUN: would create $ZSH_FILTER_SNIPPET" echo "DRY-RUN: would create $ZSH_FILTER_SNIPPET"
else else
cat > "$ZSH_FILTER_SNIPPET" << 'ZEOF' cat >"$ZSH_FILTER_SNIPPET" <<'ZEOF'
# Added by hosts guard setup suppress unlock-hosts commands from Zsh history # Added by hosts guard setup suppress unlock-hosts commands from Zsh history
autoload -Uz add-zsh-hook 2>/dev/null || true autoload -Uz add-zsh-hook 2>/dev/null || true
_hosts_guard_history_filter() { _hosts_guard_history_filter() {
@ -274,11 +288,11 @@ ZEOF
fi fi
# Bash: rely on HISTCONTROL and PROMPT_COMMAND filter # Bash: rely on HISTCONTROL and PROMPT_COMMAND filter
if command -v bash > /dev/null 2>&1; then if command -v bash >/dev/null 2>&1; then
if [[ $DRY_RUN -eq 1 ]]; then if [[ $DRY_RUN -eq 1 ]]; then
echo "DRY-RUN: would create $BASH_FILTER_SNIPPET" echo "DRY-RUN: would create $BASH_FILTER_SNIPPET"
else else
cat > "$BASH_FILTER_SNIPPET" << 'BEOF' cat >"$BASH_FILTER_SNIPPET" <<'BEOF'
# Added by hosts guard setup suppress unlock-hosts commands from Bash history # Added by hosts guard setup suppress unlock-hosts commands from Bash history
export HISTCONTROL=ignoredups:erasedups export HISTCONTROL=ignoredups:erasedups
_hosts_guard_hist_filter() { _hosts_guard_hist_filter() {
@ -314,7 +328,7 @@ if [[ $ADD_ALIAS_STUB -eq 1 ]]; then
if [[ $DRY_RUN -eq 1 ]]; then if [[ $DRY_RUN -eq 1 ]]; then
echo "DRY-RUN: would create $PROFILE_STUB" echo "DRY-RUN: would create $PROFILE_STUB"
else else
cat > "$PROFILE_STUB" << 'ASTUB' cat >"$PROFILE_STUB" <<'ASTUB'
# Added by hosts guard setup discourages casual use of unlock-hosts name # Added by hosts guard setup discourages casual use of unlock-hosts name
if command -v unlock-hosts >/dev/null 2>&1; then if command -v unlock-hosts >/dev/null 2>&1; then
alias unlock-hosts='command_not_found_handle 2>/dev/null || echo "Use: sudo /usr/local/sbin/unlock-hosts (logged & delayed)"' alias unlock-hosts='command_not_found_handle 2>/dev/null || echo "Use: sudo /usr/local/sbin/unlock-hosts (logged & delayed)"'
@ -328,17 +342,17 @@ fi
# Audit rule to record executions (requires auditd) # Audit rule to record executions (requires auditd)
###################################################################### ######################################################################
if [[ $INSTALL_AUDIT_RULE -eq 1 ]]; then if [[ $INSTALL_AUDIT_RULE -eq 1 ]]; then
if command -v auditctl > /dev/null 2>&1; then if command -v auditctl >/dev/null 2>&1; then
audit_rule_str="-w /usr/local/sbin/unlock-hosts -p x -k hosts_unlock" audit_rule_str="-w /usr/local/sbin/unlock-hosts -p x -k hosts_unlock"
audit_rule_args=(-w /usr/local/sbin/unlock-hosts -p x -k hosts_unlock) audit_rule_args=(-w /usr/local/sbin/unlock-hosts -p x -k hosts_unlock)
if auditctl -l 2> /dev/null | grep -Fq "/usr/local/sbin/unlock-hosts"; then if auditctl -l 2>/dev/null | grep -Fq "/usr/local/sbin/unlock-hosts"; then
note "Audit rule already present" note "Audit rule already present"
else else
run auditctl "${audit_rule_args[@]}" || warn "Failed to add audit rule (runtime)" run auditctl "${audit_rule_args[@]}" || warn "Failed to add audit rule (runtime)"
if [[ $DRY_RUN -eq 1 ]]; then if [[ $DRY_RUN -eq 1 ]]; then
echo "DRY-RUN: would create /etc/audit/rules.d/hosts_unlock.rules" echo "DRY-RUN: would create /etc/audit/rules.d/hosts_unlock.rules"
else else
echo "$audit_rule_str" > /etc/audit/rules.d/hosts_unlock.rules echo "$audit_rule_str" >/etc/audit/rules.d/hosts_unlock.rules
fi fi
fi fi
else else
@ -353,6 +367,8 @@ msg "Deploying systemd units"
run install -m 644 "$UNIT_GUARD_SERVICE" "$SYSTEMD_DIR/hosts-guard.service" run install -m 644 "$UNIT_GUARD_SERVICE" "$SYSTEMD_DIR/hosts-guard.service"
run install -m 644 "$UNIT_GUARD_PATH" "$SYSTEMD_DIR/hosts-guard.path" run install -m 644 "$UNIT_GUARD_PATH" "$SYSTEMD_DIR/hosts-guard.path"
run install -m 644 "$UNIT_BIND_SERVICE" "$SYSTEMD_DIR/hosts-bind-mount.service" run install -m 644 "$UNIT_BIND_SERVICE" "$SYSTEMD_DIR/hosts-bind-mount.service"
run install -m 644 "$UNIT_NSSWITCH_SERVICE" "$SYSTEMD_DIR/nsswitch-guard.service"
run install -m 644 "$UNIT_NSSWITCH_PATH" "$SYSTEMD_DIR/nsswitch-guard.path"
if [[ $DRY_RUN -eq 0 ]]; then systemctl daemon-reload; fi if [[ $DRY_RUN -eq 0 ]]; then systemctl daemon-reload; fi
@ -373,7 +389,34 @@ else
note "Skipping bind mount (--skip-bind)" note "Skipping bind mount (--skip-bind)"
fi fi
msg "Performing initial enforcement" if [[ $ENABLE_NSSWITCH -eq 1 ]]; then
msg "Enabling nsswitch.conf protection (hosts bypass prevention)"
msg "Installing nsswitch enforcement script -> $INSTALL_ENFORCE_NSSWITCH"
run install -m 755 "$TEMPLATE_ENFORCE_NSSWITCH" "$INSTALL_ENFORCE_NSSWITCH"
# Create nsswitch canonical snapshot if needed
if [[ -f "$NSSWITCH" ]]; then
if [[ ! -f "$CANON_NSSWITCH" ]]; then
msg "Creating canonical nsswitch.conf snapshot at $CANON_NSSWITCH"
run cp "$NSSWITCH" "$CANON_NSSWITCH"
run chmod 644 "$CANON_NSSWITCH"
chattr +i "$CANON_NSSWITCH" 2>/dev/null || warn "Failed to protect canonical nsswitch copy"
fi
fi
run systemctl enable --now nsswitch-guard.path
# Perform initial nsswitch enforcement
if [[ $DRY_RUN -eq 1 ]]; then
echo "DRY-RUN: would run $INSTALL_ENFORCE_NSSWITCH"
else
"$INSTALL_ENFORCE_NSSWITCH" || warn "nsswitch enforcement returned non-zero"
fi
else
note "Skipping nsswitch protection (--skip-nsswitch)"
fi
msg "Performing initial hosts enforcement"
if [[ $DRY_RUN -eq 1 ]]; then if [[ $DRY_RUN -eq 1 ]]; then
echo "DRY-RUN: would run $INSTALL_ENFORCE" echo "DRY-RUN: would run $INSTALL_ENFORCE"
else else
@ -385,12 +428,15 @@ fi
###################################################################### ######################################################################
echo echo
msg "Hosts guard setup complete" msg "Hosts guard setup complete"
echo "Canonical copy: $CANON" echo "Canonical hosts copy: $CANON"
echo "Canonical nsswitch copy: $CANON_NSSWITCH"
echo "Enforce script: $INSTALL_ENFORCE" echo "Enforce script: $INSTALL_ENFORCE"
echo "nsswitch enforce: $INSTALL_ENFORCE_NSSWITCH"
echo "Unlock command: sudo $INSTALL_UNLOCK" echo "Unlock command: sudo $INSTALL_UNLOCK"
echo "Delay (seconds): $DELAY" echo "Delay (seconds): $DELAY"
echo "Auto-revert path watch: $([[ $ENABLE_PATH -eq 1 ]] && echo enabled || echo disabled)" echo "Auto-revert path watch: $([[ $ENABLE_PATH -eq 1 ]] && echo enabled || echo disabled)"
echo "Read-only bind mount: $([[ $ENABLE_BIND -eq 1 ]] && echo enabled || echo disabled)" echo "Read-only bind mount: $([[ $ENABLE_BIND -eq 1 ]] && echo enabled || echo disabled)"
echo "nsswitch protection: $([[ $ENABLE_NSSWITCH -eq 1 ]] && echo enabled || echo disabled)"
echo "Shell history suppression: $([[ $INSTALL_SHELL_HOOKS -eq 1 ]] && echo enabled || echo disabled)" echo "Shell history suppression: $([[ $INSTALL_SHELL_HOOKS -eq 1 ]] && echo enabled || echo disabled)"
echo "Audit rule: $([[ $INSTALL_AUDIT_RULE -eq 1 ]] && echo enabled || echo disabled)" echo "Audit rule: $([[ $INSTALL_AUDIT_RULE -eq 1 ]] && echo enabled || echo disabled)"
echo "Alias stub: $([[ $ADD_ALIAS_STUB -eq 1 ]] && echo enabled || echo disabled)" echo "Alias stub: $([[ $ADD_ALIAS_STUB -eq 1 ]] && echo enabled || echo disabled)"

View File

@ -69,9 +69,9 @@ load_saved_custom_entries() {
# Save current custom entries to state file # Save current custom entries to state file
save_custom_entries_state() { save_custom_entries_state() {
local entries="$1" local entries="$1"
echo "$entries" | sort -u > "$CUSTOM_ENTRIES_STATE_FILE" echo "$entries" | sort -u >"$CUSTOM_ENTRIES_STATE_FILE"
chmod 644 "$CUSTOM_ENTRIES_STATE_FILE" chmod 644 "$CUSTOM_ENTRIES_STATE_FILE"
chattr +i "$CUSTOM_ENTRIES_STATE_FILE" 2> /dev/null || true chattr +i "$CUSTOM_ENTRIES_STATE_FILE" 2>/dev/null || true
} }
# Helper function to count non-empty lines # Helper function to count non-empty lines
@ -80,7 +80,7 @@ count_lines() {
if [[ -z $input ]]; then if [[ -z $input ]]; then
echo 0 echo 0
else else
echo "$input" | grep -c . 2> /dev/null || echo 0 echo "$input" | grep -c . 2>/dev/null || echo 0
fi fi
} }
@ -144,7 +144,7 @@ check_custom_entries_protection() {
echo "You are attempting to REMOVE the following blocked entries:" echo "You are attempting to REMOVE the following blocked entries:"
while IFS= read -r entry; do while IFS= read -r entry; do
echo " - $entry" echo " - $entry"
done <<< "$removed_entries" done <<<"$removed_entries"
echo "" echo ""
echo "This is NOT allowed. The only way to unblock sites is to:" echo "This is NOT allowed. The only way to unblock sites is to:"
echo "" echo ""
@ -168,7 +168,7 @@ fi
sudo systemctl enable systemd-resolved sudo systemctl enable systemd-resolved
# Remove all attributes from /etc/hosts to allow modifications # Remove all attributes from /etc/hosts to allow modifications
sudo chattr -i -a /etc/hosts 2> /dev/null || true sudo chattr -i -a /etc/hosts 2>/dev/null || true
# Source and local cache configuration # Source and local cache configuration
URL="https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/fakenews-gambling-porn-social/hosts" URL="https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/fakenews-gambling-porn-social/hosts"
@ -180,9 +180,9 @@ extract_date_epoch_from_file() {
# Grep "# Date:" line and convert to epoch seconds (UTC) # Grep "# Date:" line and convert to epoch seconds (UTC)
local f="$1" local f="$1"
local line local line
line=$(grep -m1 '^# Date:' "$f" 2> /dev/null | sed -E 's/^# Date:[[:space:]]*(.*)[[:space:]]*\(UTC\).*/\1 UTC/') line=$(grep -m1 '^# Date:' "$f" 2>/dev/null | sed -E 's/^# Date:[[:space:]]*(.*)[[:space:]]*\(UTC\).*/\1 UTC/')
if [[ -n $line ]]; then if [[ -n $line ]]; then
date -u -d "$line" +%s 2> /dev/null || echo "" date -u -d "$line" +%s 2>/dev/null || echo ""
else else
echo "" echo ""
fi fi
@ -195,7 +195,7 @@ fetch_remote_header() {
return 0 return 0
fi fi
# Fallback may download more, but we only keep first lines # Fallback may download more, but we only keep first lines
if curl -LfsS --max-time 10 "$URL" | head -n 20 > "$out"; then if curl -LfsS --max-time 10 "$URL" | head -n 20 >"$out"; then
return 0 return 0
fi fi
return 1 return 1
@ -272,7 +272,7 @@ sudo sed -i 's/^0\.0\.0\.0 messenger\.com/#0.0.0.0 messenger.com/' /etc/hosts
# Add custom entries for YouTube and Discord # Add custom entries for YouTube and Discord
echo "Adding custom entries for YouTube and Discord..." echo "Adding custom entries for YouTube and Discord..."
tee -a /etc/hosts > /dev/null << 'EOF' tee -a /etc/hosts >/dev/null <<'EOF'
# Custom blocking entries # Custom blocking entries
# YouTube # YouTube
@ -295,15 +295,15 @@ tee -a /etc/hosts > /dev/null << 'EOF'
# Steam Store # Steam Store
# Discord (selective blocking - media only, voice chat allowed) # Discord - media allowed
0.0.0.0 cdn.discordapp.com # 0.0.0.0 cdn.discordapp.com
0.0.0.0 media.discordapp.net # 0.0.0.0 media.discordapp.net
0.0.0.0 images-ext-1.discordapp.net # 0.0.0.0 images-ext-1.discordapp.net
0.0.0.0 images-ext-2.discordapp.net # 0.0.0.0 images-ext-2.discordapp.net
0.0.0.0 attachments-1.discordapp.net # 0.0.0.0 attachments-1.discordapp.net
0.0.0.0 attachments-2.discordapp.net # 0.0.0.0 attachments-2.discordapp.net
0.0.0.0 tenor.com # 0.0.0.0 tenor.com
0.0.0.0 giphy.com # 0.0.0.0 giphy.com
# Food Delivery Services # Food Delivery Services
# Polish services # Polish services
@ -407,7 +407,7 @@ echo "Saving custom entries state for protection mechanism..."
script_path="$(readlink -f "$0")" script_path="$(readlink -f "$0")"
current_custom_entries=$(extract_custom_entries_from_script "$script_path") current_custom_entries=$(extract_custom_entries_from_script "$script_path")
# Remove immutable from state file if it exists # Remove immutable from state file if it exists
chattr -i "$CUSTOM_ENTRIES_STATE_FILE" 2> /dev/null || true chattr -i "$CUSTOM_ENTRIES_STATE_FILE" 2>/dev/null || true
save_custom_entries_state "$current_custom_entries" save_custom_entries_state "$current_custom_entries"
echo "✅ Custom entries state saved to $CUSTOM_ENTRIES_STATE_FILE" echo "✅ Custom entries state saved to $CUSTOM_ENTRIES_STATE_FILE"
@ -420,7 +420,98 @@ else
echo "DNS cache flush skipped (use --flush-dns to enable)." echo "DNS cache flush skipped (use --flush-dns to enable)."
fi fi
# ============================================================================
# DISABLE DNS OVER HTTPS (DoH) IN BROWSERS
# ============================================================================
# DoH bypasses /etc/hosts entirely, defeating all our blocking!
# We disable it in Firefox profiles for all users.
echo ""
echo "Disabling DNS over HTTPS (DoH) in browsers..."
# Get the actual user (not root) who invoked this script
REAL_USER="${SUDO_USER:-$USER}"
REAL_HOME=$(getent passwd "$REAL_USER" | cut -d: -f6)
# Firefox: disable DoH via user.js
if [[ -d "$REAL_HOME/.mozilla/firefox" ]]; then
for profile in "$REAL_HOME/.mozilla/firefox"/*.default*; do
if [[ -d "$profile" ]]; then
cat >>"$profile/user.js" <<'FIREFOXEOF'
// Disable DNS over HTTPS (DoH) to ensure /etc/hosts blocking works
// Added by linux-configuration hosts installer
user_pref("network.trr.mode", 5); // 5 = Off by user choice
user_pref("doh-rollout.enabled", false);
user_pref("doh-rollout.disable-heuristics", true);
FIREFOXEOF
chown "$REAL_USER:$REAL_USER" "$profile/user.js"
echo " Firefox DoH disabled in: $(basename "$profile")"
fi
done
else
echo " No Firefox profiles found"
fi
# Chromium-based browsers: use policy file
CHROME_POLICY_DIR="/etc/chromium/policies/managed"
if [[ -d "/etc/chromium" ]] || command -v chromium &>/dev/null; then
mkdir -p "$CHROME_POLICY_DIR"
cat >"$CHROME_POLICY_DIR/disable-doh.json" <<'CHROMEEOF'
{
"DnsOverHttpsMode": "off",
"BuiltInDnsClientEnabled": false
}
CHROMEEOF
echo " Chromium DoH disabled via policy"
fi
# Google Chrome policy
GCHROME_POLICY_DIR="/etc/opt/chrome/policies/managed"
if [[ -d "/etc/opt/chrome" ]] || command -v google-chrome &>/dev/null; then
mkdir -p "$GCHROME_POLICY_DIR"
cat >"$GCHROME_POLICY_DIR/disable-doh.json" <<'GCHROMEEOF'
{
"DnsOverHttpsMode": "off",
"BuiltInDnsClientEnabled": false
}
GCHROMEEOF
echo " Google Chrome DoH disabled via policy"
fi
echo "" echo ""
echo "✅ Installation complete!" echo "✅ Installation complete!"
echo " Custom entries protection is now active." echo " Custom entries protection is now active."
echo " Removing blocked entries from the script will be blocked." echo " Removing blocked entries from the script will be blocked."
echo " DNS over HTTPS (DoH) has been disabled in browsers."
# ============================================================================
# FORCE BROWSER RESTART TO APPLY DOH CHANGES
# ============================================================================
# Kill all browser processes so DoH changes take effect immediately
echo ""
echo "Killing browsers to apply DoH policy changes..."
BROWSERS_KILLED=0
for browser in chrome chromium chromium-browser brave brave-browser firefox firefox-esr thorium vivaldi opera; do
if pgrep -x "$browser" &>/dev/null || pgrep -f "/opt/.*/$browser" &>/dev/null; then
echo " Killing $browser..."
pkill -9 -f "$browser" 2>/dev/null || true
BROWSERS_KILLED=1
fi
done
# Also kill by common binary paths
for pattern in "/opt/google/chrome" "/opt/brave" "/opt/thorium" "/usr/lib/firefox" "/usr/lib/chromium"; do
if pgrep -f "$pattern" &>/dev/null; then
echo " Killing processes matching $pattern..."
pkill -9 -f "$pattern" 2>/dev/null || true
BROWSERS_KILLED=1
fi
done
if [[ $BROWSERS_KILLED -eq 1 ]]; then
echo ""
echo "⚠️ Browsers were killed to apply DNS settings."
echo " Reopen your browser - hosts blocking is now enforced."
else
echo " No browsers were running."
fi

View File

@ -0,0 +1,234 @@
# Block Compulsive Opening - LLM Reference Guide
> **For AI assistants**: This document explains the compulsive opening blocker so you can make correct modifications.
## System Purpose
Limit messaging apps (Beeper, Signal, Discord) to **one launch per hour** to reduce compulsive checking behavior.
## How It Works
```
┌─────────────────────────────────────────────────────────────────────┐
│ LAUNCH INTERCEPTION │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ User clicks "Discord" in app launcher │
│ ↓ │
│ /usr/bin/discord (wrapper script) │
│ ↓ │
│ exec /usr/local/bin/block-compulsive-opening.sh wrapper discord │
│ ↓ │
│ Check: ~/.local/state/compulsive-block/discord.lastopen │
│ ↓ │
│ ┌─────────────────┴─────────────────┐ │
│ │ │ │
│ ▼ Not opened this hour ▼ Already opened │
│ Record opening time Show notification │
│ Launch real binary Exit with error │
│ /opt/discord/Discord │
│ │
└─────────────────────────────────────────────────────────────────────┘
```
## File Locations
| File | Purpose |
|------|---------|
| `/usr/local/bin/block-compulsive-opening.sh` | Installed main script |
| `/usr/bin/beeper` | Wrapper (replaces original) |
| `/usr/bin/signal-desktop` | Wrapper (replaces original) |
| `/usr/bin/discord` | Wrapper (replaces original) |
| `/usr/bin/*.orig` or `SYMLINK:*` | Original binaries/links |
| `~/.local/state/compulsive-block/*.lastopen` | Per-app hour tracking |
| `~/.local/state/compulsive-block/compulsive-block.log` | Activity log |
| `/etc/pacman.d/hooks/95-compulsive-block-rewrap.hook` | Auto-rewrap hook |
## Managed Applications
```bash
declare -A APPS=(
["beeper"]="/usr/bin/beeper"
["signal-desktop"]="/usr/bin/signal-desktop"
["discord"]="/usr/bin/discord"
)
declare -A REAL_BINARIES=(
["beeper"]="/opt/beeper/beepertexts"
["signal-desktop"]="/usr/lib/signal-desktop/signal-desktop"
["discord"]="/opt/discord/Discord"
)
```
## State Tracking
Hour key format: `YYYY-MM-DD-HH` (e.g., `2026-02-02-14`)
State file content: Just the hour key string
```bash
# Check if opened this hour
cat ~/.local/state/compulsive-block/discord.lastopen
# Output: 2026-02-02-14
# Current hour
date '+%Y-%m-%d-%H'
# Output: 2026-02-02-15 (different = can open again)
```
## Wrapper Installation Process
When `install_all()` runs:
1. Copies script to `/usr/local/bin/block-compulsive-opening.sh`
2. For each app:
- If original is a symlink: Save `SYMLINK:/target/path` to `.orig`
- If original is a file: Move to `.orig`
- Create wrapper script at original location:
```bash
#!/bin/bash
exec /usr/local/bin/block-compulsive-opening.sh wrapper "discord" "$@"
```
3. Install pacman hook for auto-rewrap
## Pacman Hook
After beeper/signal/discord package updates, the hook re-wraps them:
```ini
[Trigger]
Operation = Upgrade
Operation = Install
Type = Package
Target = beeper
Target = signal-desktop
Target = discord
[Action]
When = PostTransaction
Exec = /usr/local/bin/block-compulsive-opening.sh rewrap-quiet
```
The `rewrap-quiet` command:
- Checks if wrapper was overwritten (doesn't contain "block-compulsive-opening")
- If overwritten: removes stale `.orig`, re-installs wrapper
- Logs to activity log
## Commands
```bash
# Install all wrappers (requires root)
sudo ./block_compulsive_opening.sh install
# Uninstall all wrappers (requires root)
sudo ./block_compulsive_opening.sh uninstall
# Check status of all apps
./block_compulsive_opening.sh status
# Reset a specific app (allow opening again this hour)
./block_compulsive_opening.sh reset discord
# Reset all apps
./block_compulsive_opening.sh reset-all
```
## Log Format
```
2026-02-02 14:30:15 - ALLOWED: discord opened (first time this hour: 2026-02-02-14)
2026-02-02 14:30:15 - LAUNCHED: discord with PID 12345 (auto-close in 10m)
2026-02-02 14:38:15 - (notification: "Session will end in 2 minutes")
2026-02-02 14:40:15 - AUTO-CLOSED: discord (PID 12345) after 10m
2026-02-02 14:45:22 - BLOCKED: discord launch prevented (already opened this hour: 2026-02-02-14)
2026-02-02 15:01:03 - ALLOWED: discord opened (first time this hour: 2026-02-02-15)
2026-02-02 15:30:00 - RESET: discord state cleared by user
```
## Auto-Close Timer (Session Limit)
Apps are automatically closed after **10 minutes** to prevent indefinite usage:
1. When app launches, a background daemon is spawned
2. At **8 minutes**: Warning notification "Session will end in 2 minutes"
3. At **10 minutes**: App is closed with SIGTERM, then SIGKILL if needed
4. State file `~/.local/state/compulsive-block/<app>.running` tracks PID and start time
**Configuration variables** (in script):
```bash
AUTO_CLOSE_TIMEOUT_MINUTES=10 # Total session length
AUTO_CLOSE_WARNING_MINUTES=2 # Warning before close
```
## Adding a New App
1. Add to `APPS` associative array:
```bash
declare -A APPS=(
# ... existing apps ...
["newapp"]="/usr/bin/newapp"
)
```
2. Add to `REAL_BINARIES`:
```bash
declare -A REAL_BINARIES=(
# ... existing apps ...
["newapp"]="/opt/newapp/actual-binary"
)
```
3. Add to pacman hook targets (if installed via pacman):
```ini
Target = newapp
```
4. Reinstall:
```bash
sudo ./block_compulsive_opening.sh install
```
## Debugging
### Check if wrapper is installed
```bash
cat /usr/bin/discord
# Should show wrapper script, not binary
ls -la /usr/bin/discord.orig
# Should exist (or check for SYMLINK: content)
```
### Check current state
```bash
./block_compulsive_opening.sh status
# Shows: which apps are wrapped, last open times, current hour
```
### Test manually
```bash
# Simulate wrapper call
/usr/local/bin/block-compulsive-opening.sh wrapper discord
```
### View logs
```bash
tail -f ~/.local/state/compulsive-block/compulsive-block.log
```
## Notification Behavior
When blocked, shows desktop notification:
- Title: "🚫 discord Blocked"
- Message: "Already opened this hour. Wait until the next hour."
- Urgency: critical
- Timeout: 5000ms
Uses `notify-send` (falls back silently if not available).
## DO NOT
1. ❌ Delete `.orig` files (cannot restore original binaries)
2. ❌ Manually edit wrapper scripts at `/usr/bin/` (will be overwritten)
3. ❌ Assume app is "blocked" once notification shows (it ran, just not again)
4. ❌ Remove pacman hook without understanding auto-rewrap won't work

View File

@ -0,0 +1,277 @@
# Midnight Shutdown System - LLM Reference Guide
> **For AI assistants**: This document explains the automatic shutdown system so you can make correct modifications.
## System Purpose
Automatically shut down the PC during configured time windows to enforce healthy sleep schedules:
- **Monday-Wednesday**: Shutdown at 24:00 (midnight)
- **Thursday-Sunday**: Shutdown at 24:00 (midnight)
- **Morning**: Safe time starts at 00:00 (effectively no morning block)
The times above are defaults; actual values in `/etc/shutdown-schedule.conf`.
## Architecture Overview
```
┌─────────────────────────────────────────────────────────────────────┐
│ SHUTDOWN SYSTEM LAYERS │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Layer 1: Systemd Timer │
│ ───────────────────── │
│ day-specific-shutdown.timer fires every minute │
│ day-specific-shutdown.service runs the check script │
│ │
│ Layer 2: Check Script │
│ ──────────────────── │
│ /usr/local/bin/day-specific-shutdown-check.sh │
│ Reads config, checks current time, initiates shutdown if in window │
│ │
│ Layer 3: Config Protection │
│ ──────────────────────── │
│ /etc/shutdown-schedule.conf has chattr +i │
│ Canonical copy at /usr/local/share/locked-shutdown-schedule.conf │
│ Path watcher auto-restores if tampered │
│ │
│ Layer 4: Timer Monitor │
│ ───────────────────── │
│ shutdown-timer-monitor.service watches timer status │
│ Re-enables timer if user tries to disable it │
│ │
│ Layer 5: Script Protection │
│ ──────────────────────── │
│ Setup script blocks making schedule MORE LENIENT │
│ Can only make it STRICTER without the unlock script │
│ │
└─────────────────────────────────────────────────────────────────────┘
```
## File Locations
| File | Purpose | Protection |
|------|---------|------------|
| `/etc/shutdown-schedule.conf` | Runtime config | chattr +i, path watcher |
| `/usr/local/share/locked-shutdown-schedule.conf` | Canonical copy | chattr +i |
| `/usr/local/bin/day-specific-shutdown-check.sh` | Shutdown logic | None |
| `/usr/local/bin/day-specific-shutdown-manager.sh` | Status/management | None |
| `/usr/local/bin/shutdown-timer-monitor.sh` | Timer re-enabler | None |
| `/usr/local/sbin/enforce-shutdown-schedule.sh` | Config restoration | None |
| `/usr/local/sbin/unlock-shutdown-schedule` | Delayed config edit | None |
| `/etc/systemd/system/day-specific-shutdown.timer` | Timer unit | systemd |
| `/etc/systemd/system/day-specific-shutdown.service` | Service unit | systemd |
| `/etc/systemd/system/shutdown-schedule-guard.path` | Config watcher | systemd |
| `/etc/systemd/system/shutdown-schedule-guard.service` | Enforcement | systemd |
| `/etc/systemd/system/shutdown-timer-monitor.service` | Timer guardian | systemd |
| `/var/log/shutdown-schedule-guard.log` | Tampering log | None |
## Config File Format
```bash
# /etc/shutdown-schedule.conf
# Shutdown hour for Monday-Wednesday (24-hour format)
MON_WED_HOUR=21
# Shutdown hour for Thursday-Sunday (24-hour format)
THU_SUN_HOUR=22
# Morning end hour (shutdown window ends at this hour)
MORNING_END_HOUR=5
```
**Interpretation**:
- Mon-Wed: Shutdown if current hour >= 21 OR current hour < 5
- Thu-Sun: Shutdown if current hour >= 22 OR current hour < 5
## Schedule Protection Logic
The setup script (`setup_midnight_shutdown.sh`) has constants at the top:
```bash
SCHEDULE_MON_WED_HOUR=24
SCHEDULE_THU_SUN_HOUR=24
SCHEDULE_MORNING_END_HOUR=0
```
When re-run, it compares these to the canonical config:
| Change Type | Action |
|-------------|--------|
| Making shutdown EARLIER | ✅ Allowed without unlock |
| Making shutdown LATER | ❌ Blocked, requires unlock |
| Making morning end EARLIER | ❌ Always blocked |
| Making morning end LATER | ✅ Allowed (extends shutdown window) |
Example blocked attempt:
```
╔══════════════════════════════════════════════════════════════════╗
║ ❌ SCHEDULE MODIFICATION BLOCKED - CHEATING DETECTED! ❌ ║
╚══════════════════════════════════════════════════════════════════╝
You modified the script to make the shutdown schedule MORE LENIENT:
• Mon-Wed shutdown: 21:00 → 23:00 (later)
Nice try! But this is exactly the kind of late-night bargaining
that this protection is designed to prevent. 😉
```
## Unlock Script Behavior
`/usr/local/sbin/unlock-shutdown-schedule`:
1. Stops `shutdown-schedule-guard.path`
2. Removes chattr from both config files
3. Opens editor on temp copy
4. Checks what changed:
- **Stricter (earlier)**: No delay, applies immediately
- **Lenient (later)**: 45-second countdown, then applies
- **Lower morning end**: **ALWAYS BLOCKED** (cannot shorten window)
5. Updates both config and canonical
6. Re-applies chattr +i
7. Restarts path watcher
## Integration Points
### i3blocks Countdown
`i3blocks/shutdown_countdown.sh` reads the config to show time remaining:
```bash
source /etc/shutdown-schedule.conf
# Calculates and displays "Shutdown in X:XX"
```
### Screen Locker
`screen_lock.py` can adjust shutdown time:
- **Sick day**: Moves shutdown 1.5 hours EARLIER (penalty)
- **Workout completed**: Moves shutdown 1.5 hours LATER (reward)
Uses `adjust_shutdown_schedule.sh` helper script.
## Systemd Units
### Timer (fires every minute)
```ini
[Timer]
OnCalendar=*:*:00
Persistent=false
AccuracySec=1s
```
### Check Service
```ini
[Service]
Type=oneshot
ExecStart=/usr/local/bin/day-specific-shutdown-check.sh
```
### Path Watcher
```ini
[Path]
PathChanged=/etc/shutdown-schedule.conf
Unit=shutdown-schedule-guard.service
```
## Check Script Logic
```bash
# Pseudocode for day-specific-shutdown-check.sh
source /etc/shutdown-schedule.conf
day=$(date +%u) # 1=Monday, 7=Sunday
hour=$(date +%H)
if [[ $day -le 3 ]]; then
shutdown_hour=$MON_WED_HOUR
else
shutdown_hour=$THU_SUN_HOUR
fi
# Check if in shutdown window
if [[ $hour -ge $shutdown_hour ]] || [[ $hour -lt $MORNING_END_HOUR ]]; then
systemctl poweroff
fi
```
## Common Tasks
### Check Current Status
```bash
/usr/local/bin/day-specific-shutdown-manager.sh status
# Or run setup script with 'status' argument
```
### Make Schedule Stricter
Edit the constants in `setup_midnight_shutdown.sh`:
```bash
SCHEDULE_MON_WED_HOUR=20 # Changed from 21 to 20 (earlier)
```
Then re-run:
```bash
sudo ./setup_midnight_shutdown.sh
```
### Make Schedule More Lenient (Requires Unlock)
```bash
sudo /usr/local/sbin/unlock-shutdown-schedule
# Wait for delay, edit config, save
```
### Disable Timer (Will Be Re-Enabled!)
```bash
sudo systemctl disable --now day-specific-shutdown.timer
# Monitor service will re-enable it automatically
```
### Check Protection Status
```bash
lsattr /etc/shutdown-schedule.conf
# Should show: ----i--------e--
systemctl status shutdown-schedule-guard.path
systemctl status shutdown-timer-monitor.service
```
## KNOWN VULNERABILITIES
1. **Information Disclosure**: Error messages tell user exactly how to bypass
2. **Unlock Script Discoverable**: Path mentioned in error messages
3. **Timer Monitor Killable**: User can stop the monitor then the timer
4. **Check Script Unprotected**: `/usr/local/bin/day-specific-shutdown-check.sh` can be edited
**TODO**:
- Remove helpful bypass instructions from error messages
- Rename unlock script to obscure name
- Protect check script with integrity verification
## Troubleshooting
### Timer not firing
```bash
systemctl status day-specific-shutdown.timer
systemctl list-timers | grep shutdown
```
### Config not being enforced
```bash
# Check path watcher
systemctl status shutdown-schedule-guard.path
# Manually trigger enforcement
sudo /usr/local/sbin/enforce-shutdown-schedule.sh
```
### Wrong time shown in i3blocks
```bash
# Verify config
cat /etc/shutdown-schedule.conf
# Check i3blocks config
cat ~/.config/i3blocks/config | grep shutdown
```
## DO NOT
1. ❌ Edit setup script constants to make schedule later (will be blocked)
2. ❌ Delete canonical config (breaks restoration)
3. ❌ Stop `shutdown-timer-monitor.service` (timer will be re-enabled anyway)
4. ❌ Modify check script to skip shutdown (defeats purpose)
5. ❌ Lower `MORNING_END_HOUR` (always blocked, shortens shutdown window)

View File

@ -17,8 +17,8 @@ notify() {
local urgency="${3:-normal}" local urgency="${3:-normal}"
local timeout="${4:-5000}" local timeout="${4:-5000}"
if command -v notify-send &> /dev/null; then if command -v notify-send &>/dev/null; then
notify-send -u "$urgency" -t "$timeout" "$title" "$message" 2> /dev/null || true notify-send -u "$urgency" -t "$timeout" "$title" "$message" 2>/dev/null || true
fi fi
} }
@ -26,6 +26,11 @@ notify() {
STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/compulsive-block" STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/compulsive-block"
LOG_FILE="$STATE_DIR/compulsive-block.log" LOG_FILE="$STATE_DIR/compulsive-block.log"
# Auto-close timeout in minutes (apps forcefully closed after this)
AUTO_CLOSE_TIMEOUT_MINUTES=10
# Warning before auto-close (in minutes before timeout)
AUTO_CLOSE_WARNING_MINUTES=2
# Apps to limit (name -> binary path) # Apps to limit (name -> binary path)
# These are the primary wrapper locations (what the user calls) # These are the primary wrapper locations (what the user calls)
declare -A APPS=( declare -A APPS=(
@ -44,7 +49,7 @@ declare -A REAL_BINARIES=(
# Ensure state directory exists # Ensure state directory exists
ensure_state_dir() { ensure_state_dir() {
mkdir -p "$STATE_DIR" 2> /dev/null || true mkdir -p "$STATE_DIR" 2>/dev/null || true
} }
# Log message with timestamp # Log message with timestamp
@ -52,7 +57,7 @@ log_message() {
local msg local msg
msg="$(date '+%Y-%m-%d %H:%M:%S') - $1" msg="$(date '+%Y-%m-%d %H:%M:%S') - $1"
echo "$msg" >&2 echo "$msg" >&2
echo "$msg" >> "$LOG_FILE" 2> /dev/null || true echo "$msg" >>"$LOG_FILE" 2>/dev/null || true
} }
# Get current hour key (YYYY-MM-DD-HH format) # Get current hour key (YYYY-MM-DD-HH format)
@ -76,7 +81,7 @@ was_opened_this_hour() {
if [[ -f $state_file ]]; then if [[ -f $state_file ]]; then
local last_hour local last_hour
last_hour=$(cat "$state_file" 2> /dev/null || echo "") last_hour=$(cat "$state_file" 2>/dev/null || echo "")
if [[ $last_hour == "$current_hour" ]]; then if [[ $last_hour == "$current_hour" ]]; then
return 0 # Was opened this hour return 0 # Was opened this hour
fi fi
@ -92,7 +97,7 @@ record_opening() {
local current_hour local current_hour
current_hour=$(get_hour_key) current_hour=$(get_hour_key)
echo "$current_hour" > "$state_file" echo "$current_hour" >"$state_file"
log_message "ALLOWED: $app opened (first time this hour: $current_hour)" log_message "ALLOWED: $app opened (first time this hour: $current_hour)"
} }
@ -124,6 +129,112 @@ get_real_binary() {
return 1 return 1
} }
# Get running state file path for an app (tracks PID and start time)
get_running_file() {
local app="$1"
echo "$STATE_DIR/${app}.running"
}
# Clean up stale running state (process no longer running)
cleanup_stale_running_state() {
local app="$1"
local running_file
running_file=$(get_running_file "$app")
if [[ ! -f $running_file ]]; then
return 0
fi
local pid
pid=$(awk '{print $1}' "$running_file" 2>/dev/null || echo "")
if [[ -z $pid ]]; then
rm -f "$running_file"
return 0
fi
# Check if process is still running
if ! kill -0 "$pid" 2>/dev/null; then
log_message "CLEANUP: Stale running state for $app (PID $pid no longer exists)"
rm -f "$running_file"
fi
}
# Launch app with auto-close timer
launch_with_timer() {
local app="$1"
local real_binary="$2"
shift 2
local warning_seconds=$(((AUTO_CLOSE_TIMEOUT_MINUTES - AUTO_CLOSE_WARNING_MINUTES) * 60))
local running_file
running_file=$(get_running_file "$app")
# Launch the app in background
"$real_binary" "$@" &
local app_pid=$!
# Record state
echo "$app_pid $(date +%s)" >"$running_file"
log_message "LAUNCHED: $app with PID $app_pid (auto-close in ${AUTO_CLOSE_TIMEOUT_MINUTES}m)"
# Spawn the auto-close daemon in a completely detached subshell
(
# Detach from terminal
exec </dev/null >/dev/null 2>&1
# Wait for warning time
sleep "$warning_seconds"
# Check if still running before warning
if kill -0 "$app_pid" 2>/dev/null; then
# Send warning notification
notify-send -u critical -t 30000 "$app Closing Soon" \
"Session will end in ${AUTO_CLOSE_WARNING_MINUTES} minutes. Save your work!" 2>/dev/null || true
else
# Process already exited
rm -f "$running_file" 2>/dev/null || true
exit 0
fi
# Wait remaining time
sleep $((AUTO_CLOSE_WARNING_MINUTES * 60))
# Check if still running
if kill -0 "$app_pid" 2>/dev/null; then
# Send final notification
notify-send -u critical -t 5000 "🚫 $app Session Ended" \
"Time's up! Closing $app now." 2>/dev/null || true
# Graceful kill first
kill "$app_pid" 2>/dev/null || true
# Wait a moment for graceful shutdown
sleep 2
# Force kill if still running
if kill -0 "$app_pid" 2>/dev/null; then
kill -9 "$app_pid" 2>/dev/null || true
fi
echo "$(date '+%Y-%m-%d %H:%M:%S') - AUTO-CLOSED: $app (PID $app_pid) after ${AUTO_CLOSE_TIMEOUT_MINUTES}m" >>"$LOG_FILE" 2>/dev/null || true
fi
rm -f "$running_file" 2>/dev/null || true
) &
disown
# Wait for the app to exit (keeps wrapper process alive while app is running)
wait "$app_pid" 2>/dev/null || true
local exit_code=$?
# Clean up running state
rm -f "$running_file" 2>/dev/null || true
log_message "EXITED: $app (PID $app_pid) with code $exit_code"
return $exit_code
}
# Main wrapper function - called when wrapping app launches # Main wrapper function - called when wrapping app launches
wrapper_main() { wrapper_main() {
local app="$1" local app="$1"
@ -138,13 +249,18 @@ wrapper_main() {
exit 1 exit 1
fi fi
# Clean up stale running state from previous crashes
cleanup_stale_running_state "$app"
if was_opened_this_hour "$app"; then if was_opened_this_hour "$app"; then
block_app "$app" block_app "$app"
exit 1 exit 1
fi fi
record_opening "$app" record_opening "$app"
exec "$real_binary" "$@"
# Launch with auto-close timer (replaces direct exec)
launch_with_timer "$app" "$real_binary" "$@"
} }
# Install wrapper for a specific app # Install wrapper for a specific app
@ -179,7 +295,7 @@ install_wrapper() {
link_target=$(readlink "$wrapper_path") link_target=$(readlink "$wrapper_path")
echo " Saving symlink $wrapper_path -> $link_target as ${wrapper_path}.orig" echo " Saving symlink $wrapper_path -> $link_target as ${wrapper_path}.orig"
# Remove symlink and create .orig that stores the link target info # Remove symlink and create .orig that stores the link target info
echo "SYMLINK:$link_target" > "${wrapper_path}.orig" echo "SYMLINK:$link_target" >"${wrapper_path}.orig"
rm "$wrapper_path" rm "$wrapper_path"
else else
echo " Backing up $wrapper_path -> ${wrapper_path}.orig" echo " Backing up $wrapper_path -> ${wrapper_path}.orig"
@ -187,7 +303,7 @@ install_wrapper() {
fi fi
echo " Creating wrapper at $wrapper_path" echo " Creating wrapper at $wrapper_path"
cat > "$wrapper_path" << WRAPPER_EOF cat >"$wrapper_path" <<WRAPPER_EOF
#!/bin/bash #!/bin/bash
# Auto-generated wrapper for $app - blocks compulsive opening # Auto-generated wrapper for $app - blocks compulsive opening
# Real binary: $real_binary # Real binary: $real_binary
@ -214,7 +330,7 @@ uninstall_wrapper() {
# Check if it was a symlink (stored as SYMLINK:target in .orig) # Check if it was a symlink (stored as SYMLINK:target in .orig)
local orig_content local orig_content
orig_content=$(cat "${wrapper_path}.orig" 2> /dev/null || echo "") orig_content=$(cat "${wrapper_path}.orig" 2>/dev/null || echo "")
if [[ $orig_content == SYMLINK:* ]]; then if [[ $orig_content == SYMLINK:* ]]; then
local link_target="${orig_content#SYMLINK:}" local link_target="${orig_content#SYMLINK:}"
echo " Restoring symlink $wrapper_path -> $link_target" echo " Restoring symlink $wrapper_path -> $link_target"
@ -276,7 +392,7 @@ install_pacman_hook() {
mkdir -p "$hook_dir" mkdir -p "$hook_dir"
cat > "$hook_file" << 'HOOK_EOF' cat >"$hook_file" <<'HOOK_EOF'
[Trigger] [Trigger]
Operation = Upgrade Operation = Upgrade
Operation = Install Operation = Install
@ -313,7 +429,7 @@ rewrap_quiet() {
local wrapper_path="${APPS[$app]}" local wrapper_path="${APPS[$app]}"
# Check if wrapper was overwritten (no longer our wrapper script) # Check if wrapper was overwritten (no longer our wrapper script)
if [[ -f $wrapper_path ]] && ! grep -q "block-compulsive-opening" "$wrapper_path" 2> /dev/null; then if [[ -f $wrapper_path ]] && ! grep -q "block-compulsive-opening" "$wrapper_path" 2>/dev/null; then
# Wrapper was overwritten by package update # Wrapper was overwritten by package update
log_message "REWRAP: $app wrapper was overwritten, re-installing" log_message "REWRAP: $app wrapper was overwritten, re-installing"
@ -321,7 +437,7 @@ rewrap_quiet() {
rm -f "${wrapper_path}.orig" rm -f "${wrapper_path}.orig"
# Re-install wrapper # Re-install wrapper
install_wrapper "$app" >> "$LOG_FILE" 2>&1 || true install_wrapper "$app" >>"$LOG_FILE" 2>&1 || true
fi fi
done done
@ -365,7 +481,7 @@ show_status() {
if [[ -f $state_file ]]; then if [[ -f $state_file ]]; then
local last_hour local last_hour
last_hour=$(cat "$state_file" 2> /dev/null || echo "") last_hour=$(cat "$state_file" 2>/dev/null || echo "")
if [[ $last_hour == "$current_hour" ]]; then if [[ $last_hour == "$current_hour" ]]; then
status="already opened (blocked until next hour)" status="already opened (blocked until next hour)"
icon="●" icon="●"
@ -415,7 +531,7 @@ reset_all() {
# Show usage # Show usage
show_usage() { show_usage() {
cat << EOF cat <<EOF
Block Compulsive Opening Script Block Compulsive Opening Script
================================ ================================

View File

@ -0,0 +1,275 @@
#!/usr/bin/env python3
"""
Focus Mode Daemon - Steam/Browser Mutual Exclusion
This daemon monitors running processes and enforces mutual exclusion between
Steam (gaming) and web browsers. Whichever starts first "wins" and the other
category is blocked/killed.
Run as a systemd user service for continuous monitoring.
"""
import os
import signal
import subprocess
import sys
import time
from datetime import datetime
from pathlib import Path
from typing import Set, Optional
# Configuration
STATE_DIR = Path(os.environ.get("XDG_STATE_HOME", Path.home() / ".local/state")) / "focus-mode"
LOG_FILE = STATE_DIR / "focus-mode.log"
POLL_INTERVAL = 2 # seconds between process checks
# Process patterns
STEAM_PATTERNS = frozenset([
"steam",
"steamwebhelper",
"steam_ocompati", # Proton compatibility tool
])
# Games often have steam_app_ prefix in process name
STEAM_GAME_PREFIX = "steam_app_"
BROWSER_PATTERNS = frozenset([
"firefox",
"firefox-esr",
"librewolf",
"chromium",
"chrome",
"google-chrome",
"brave",
"vivaldi",
"opera",
"microsoft-edge",
"ungoogled-chromium",
])
# Patterns to ignore (browser helpers that aren't the main browser)
IGNORE_PATTERNS = frozenset([
"crashhandler",
"update",
"helper",
])
def log(message: str) -> None:
"""Log message with timestamp."""
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
log_line = f"{timestamp} - {message}"
print(log_line)
try:
STATE_DIR.mkdir(parents=True, exist_ok=True)
with open(LOG_FILE, "a") as f:
f.write(log_line + "\n")
except Exception:
pass
def notify(title: str, message: str, urgency: str = "normal") -> None:
"""Send desktop notification."""
try:
subprocess.run(
["notify-send", "-u", urgency, title, message],
capture_output=True,
timeout=5,
)
except Exception:
pass
def get_running_processes() -> Set[str]:
"""Get set of currently running process names."""
processes = set()
try:
result = subprocess.run(
["ps", "-eo", "comm="],
capture_output=True,
text=True,
timeout=10,
)
if result.returncode == 0:
for line in result.stdout.strip().split("\n"):
proc_name = line.strip().lower()
if proc_name:
processes.add(proc_name)
except Exception as e:
log(f"Error getting processes: {e}")
return processes
def is_steam_running(processes: Set[str]) -> bool:
"""Check if Steam or any Steam game is running."""
for proc in processes:
# Check for Steam main processes
if proc in STEAM_PATTERNS:
return True
# Check for Steam games (have steam_app_ prefix)
if proc.startswith(STEAM_GAME_PREFIX):
return True
return False
def is_browser_running(processes: Set[str]) -> bool:
"""Check if any browser is running."""
for proc in processes:
# Skip ignored patterns
if any(ign in proc for ign in IGNORE_PATTERNS):
continue
# Check browser patterns
for pattern in BROWSER_PATTERNS:
if pattern in proc:
return True
return False
def kill_steam() -> None:
"""Kill all Steam-related processes."""
log("Killing Steam processes...")
notify("🎮 Gaming Blocked", "Browser is active. Closing Steam.", "critical")
try:
# First try graceful shutdown
subprocess.run(["pkill", "-f", "steam"], capture_output=True, timeout=5)
time.sleep(2)
# Force kill if still running
subprocess.run(["pkill", "-9", "-f", "steam"], capture_output=True, timeout=5)
except Exception as e:
log(f"Error killing Steam: {e}")
def kill_browsers() -> None:
"""Kill all browser processes."""
log("Killing browser processes...")
notify("🌐 Browsers Blocked", "Steam is active. Closing browsers.", "critical")
for browser in BROWSER_PATTERNS:
try:
subprocess.run(["pkill", "-f", browser], capture_output=True, timeout=5)
except Exception:
pass
time.sleep(2)
# Force kill if still running
for browser in BROWSER_PATTERNS:
try:
subprocess.run(["pkill", "-9", "-f", browser], capture_output=True, timeout=5)
except Exception:
pass
class FocusMode:
"""Tracks current focus mode and enforces mutual exclusion."""
def __init__(self):
self.current_mode: Optional[str] = None # "gaming" or "browsing" or None
self.mode_start_time: Optional[datetime] = None
def update(self, processes: Set[str]) -> None:
"""Update focus mode based on running processes."""
steam_running = is_steam_running(processes)
browser_running = is_browser_running(processes)
if self.current_mode is None:
# No mode set yet - first to start wins
if steam_running and browser_running:
# Both running at startup - prefer gaming mode (close browsers)
log("Both Steam and browsers detected at startup - entering GAMING mode")
self.current_mode = "gaming"
self.mode_start_time = datetime.now()
kill_browsers()
elif steam_running:
log("Steam detected - entering GAMING mode")
self.current_mode = "gaming"
self.mode_start_time = datetime.now()
notify("🎮 Gaming Mode", "Steam detected. Browsers are now blocked.", "normal")
elif browser_running:
log("Browser detected - entering BROWSING mode")
self.current_mode = "browsing"
self.mode_start_time = datetime.now()
notify("🌐 Browsing Mode", "Browser detected. Steam is now blocked.", "normal")
elif self.current_mode == "gaming":
if not steam_running:
# Steam closed - exit gaming mode
log("Steam closed - exiting GAMING mode")
self.current_mode = None
self.mode_start_time = None
notify("🎮 Gaming Mode Ended", "You can now use browsers.", "normal")
elif browser_running:
# Browser started while in gaming mode - kill it
log("Browser detected during GAMING mode - killing browsers")
kill_browsers()
elif self.current_mode == "browsing":
if not browser_running:
# Browsers closed - exit browsing mode
log("Browsers closed - exiting BROWSING mode")
self.current_mode = None
self.mode_start_time = None
notify("🌐 Browsing Mode Ended", "You can now use Steam.", "normal")
elif steam_running:
# Steam started while in browsing mode - kill it
log("Steam detected during BROWSING mode - killing Steam")
kill_steam()
def get_status(self) -> str:
"""Get current status string."""
if self.current_mode is None:
return "No active focus mode"
duration = ""
if self.mode_start_time:
elapsed = datetime.now() - self.mode_start_time
minutes = int(elapsed.total_seconds() // 60)
duration = f" (active for {minutes}m)"
if self.current_mode == "gaming":
return f"🎮 GAMING mode{duration} - browsers blocked"
else:
return f"🌐 BROWSING mode{duration} - Steam blocked"
def write_status(focus: FocusMode) -> None:
"""Write current status to state file for external queries."""
try:
STATE_DIR.mkdir(parents=True, exist_ok=True)
status_file = STATE_DIR / "status"
with open(status_file, "w") as f:
f.write(focus.get_status() + "\n")
f.write(f"mode={focus.current_mode or 'none'}\n")
except Exception:
pass
def main():
"""Main daemon loop."""
log("Focus Mode Daemon starting...")
# Setup signal handlers
def handle_signal(signum, frame):
log(f"Received signal {signum} - shutting down")
sys.exit(0)
signal.signal(signal.SIGTERM, handle_signal)
signal.signal(signal.SIGINT, handle_signal)
focus = FocusMode()
while True:
try:
processes = get_running_processes()
focus.update(processes)
write_status(focus)
except Exception as e:
log(f"Error in main loop: {e}")
time.sleep(POLL_INTERVAL)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,211 @@
#!/bin/bash
# Install Focus Mode Daemon
# Sets up Steam/Browser mutual exclusion as a systemd user service
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DAEMON_SCRIPT="$SCRIPT_DIR/focus_mode_daemon.py"
INSTALL_PATH="/usr/local/bin/focus-mode-daemon"
SERVICE_DIR="$HOME/.config/systemd/user"
SERVICE_FILE="$SERVICE_DIR/focus-mode.service"
msg() { printf '\e[1;32m[+]\e[0m %s\n' "$*"; }
note() { printf '\e[1;34m[i]\e[0m %s\n' "$*"; }
warn() { printf '\e[1;33m[!]\e[0m %s\n' "$*"; }
err() { printf '\e[1;31m[x]\e[0m %s\n' "$*" >&2; }
usage() {
cat <<EOF
Focus Mode Daemon Installer
Usage: $0 [install|uninstall|status]
Commands:
install - Install and enable the focus mode daemon
uninstall - Remove the daemon and disable the service
status - Show current daemon status
The daemon enforces mutual exclusion between Steam and web browsers:
- If Steam starts first: browsers are blocked/killed
- If browser starts first: Steam is blocked/killed
- Whichever started first "wins" until it exits
EOF
}
check_deps() {
local missing=0
if ! command -v python3 &>/dev/null; then
err "python3 is required but not installed"
missing=1
fi
if ! command -v systemctl &>/dev/null; then
err "systemd is required but systemctl not found"
missing=1
fi
if [[ $missing -eq 1 ]]; then
exit 1
fi
}
install_daemon() {
msg "Installing Focus Mode Daemon..."
check_deps
if [[ ! -f "$DAEMON_SCRIPT" ]]; then
err "Daemon script not found: $DAEMON_SCRIPT"
exit 1
fi
# Install the daemon script
msg "Installing daemon script to $INSTALL_PATH"
if [[ $EUID -eq 0 ]]; then
install -m 755 "$DAEMON_SCRIPT" "$INSTALL_PATH"
else
sudo install -m 755 "$DAEMON_SCRIPT" "$INSTALL_PATH"
fi
# Create systemd user directory
mkdir -p "$SERVICE_DIR"
# Create the systemd user service
msg "Creating systemd user service: $SERVICE_FILE"
cat >"$SERVICE_FILE" <<'EOF'
[Unit]
Description=Focus Mode Daemon (Steam/Browser mutual exclusion)
After=graphical-session.target
[Service]
Type=simple
ExecStart=/usr/local/bin/focus-mode-daemon
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
# Don't allow easy stopping (psychological friction)
RefuseManualStop=false
[Install]
WantedBy=default.target
EOF
# Reload systemd user daemon
msg "Reloading systemd user daemon..."
systemctl --user daemon-reload
# Enable and start the service
msg "Enabling and starting focus-mode.service..."
systemctl --user enable focus-mode.service
systemctl --user start focus-mode.service
msg "Focus Mode Daemon installed successfully!"
echo ""
echo "The daemon is now running and will:"
echo " 🎮 Block browsers when Steam is running"
echo " 🌐 Block Steam when a browser is running"
echo ""
echo "Status: $(systemctl --user is-active focus-mode.service 2>/dev/null || echo 'unknown')"
echo ""
echo "Commands:"
echo " systemctl --user status focus-mode - Check daemon status"
echo " journalctl --user -u focus-mode -f - View daemon logs"
echo " cat ~/.local/state/focus-mode/status - View current mode"
echo ""
}
uninstall_daemon() {
msg "Uninstalling Focus Mode Daemon..."
# Stop and disable service
if systemctl --user is-active focus-mode.service &>/dev/null; then
msg "Stopping focus-mode.service..."
systemctl --user stop focus-mode.service || true
fi
if systemctl --user is-enabled focus-mode.service &>/dev/null; then
msg "Disabling focus-mode.service..."
systemctl --user disable focus-mode.service || true
fi
# Remove service file
if [[ -f "$SERVICE_FILE" ]]; then
msg "Removing service file..."
rm -f "$SERVICE_FILE"
fi
# Reload daemon
systemctl --user daemon-reload 2>/dev/null || true
# Remove installed script
if [[ -f "$INSTALL_PATH" ]]; then
msg "Removing daemon script..."
if [[ $EUID -eq 0 ]]; then
rm -f "$INSTALL_PATH"
else
sudo rm -f "$INSTALL_PATH"
fi
fi
msg "Focus Mode Daemon uninstalled"
note "State files in ~/.local/state/focus-mode/ were NOT removed"
}
show_status() {
echo "Focus Mode Daemon Status"
echo "========================"
echo ""
# Service status
if systemctl --user is-active focus-mode.service &>/dev/null; then
echo "Service: ✓ Running"
else
echo "Service: ✗ Not running"
fi
if systemctl --user is-enabled focus-mode.service &>/dev/null; then
echo "Enabled: ✓ Yes"
else
echo "Enabled: ✗ No"
fi
echo ""
# Current mode
local status_file="$HOME/.local/state/focus-mode/status"
if [[ -f "$status_file" ]]; then
echo "Current Mode:"
cat "$status_file"
else
echo "Current Mode: Unknown (status file not found)"
fi
echo ""
echo "Recent Logs:"
journalctl --user -u focus-mode --no-pager -n 10 2>/dev/null || echo " (no logs available)"
}
# Main
case "${1:-install}" in
install)
install_daemon
;;
uninstall)
uninstall_daemon
;;
status)
show_status
;;
-h | --help | help)
usage
;;
*)
err "Unknown command: $1"
usage
exit 1
;;
esac

View File

@ -0,0 +1,282 @@
# Pacman Wrapper Security System - LLM Reference Guide
> **For AI assistants**: This document explains the pacman wrapper architecture so you can make correct modifications.
## System Purpose
Intercept all `pacman` commands to:
1. Block installation of restricted packages (browsers, games, etc.)
2. Require challenges for greylisted packages
3. Enforce hosts file sharing on VirtualBox VMs
4. Auto-setup maintenance services if missing
5. Handle stale database locks gracefully
## Architecture Overview
```
┌─────────────────────────────────────────────────────────────────────┐
│ PACMAN WRAPPER │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ User runs: pacman -S firefox │
│ ↓ │
│ /usr/bin/pacman (symlink) → pacman_wrapper.sh │
│ ↓ │
│ 1. Verify policy file integrity (SHA256) │
│ 2. Check if package matches blocked keywords │
│ 3. Check if package requires challenge (greylist) │
│ 4. Run hosts-guard pre-unlock hook │
│ 5. Execute real pacman: /usr/bin/pacman.orig │
│ 6. Run hosts-guard post-relock hook │
│ 7. Remove any blocked packages that slipped through │
│ 8. Enforce VirtualBox hosts if vbox detected │
│ │
└─────────────────────────────────────────────────────────────────────┘
```
## File Locations
| File | Purpose |
|------|---------|
| `/usr/bin/pacman` | Symlink to wrapper |
| `/usr/bin/pacman.orig` | Real pacman binary |
| `pacman_wrapper.sh` | Main wrapper script (823 lines) |
| `install_pacman_wrapper.sh` | Installer script |
| `pacman_blocked_keywords.txt` | Substrings that cause blocking |
| `pacman_whitelist.txt` | Exact names that bypass blocking |
| `pacman_greylist.txt` | Packages requiring challenge |
| `words.txt` | Word scramble challenge dictionary |
| `/var/lib/pacman-wrapper/policy.sha256` | Integrity checksums |
## Policy Files Explained
### pacman_blocked_keywords.txt
```
# Lines starting with # are comments
# Any package containing these substrings is BLOCKED
firefox
brave
chromium
youtube
stremio
```
If user tries `pacman -S firefox-developer-edition`, it's blocked because it contains "firefox".
### pacman_whitelist.txt
```
# Exact package names that bypass keyword blocking
minizip # Contains nothing bad but might match a pattern
python-requests # Safe despite containing blocked substrings
```
### pacman_greylist.txt
```
# Packages requiring word scramble challenge
# Currently empty - add packages here for challenge requirement
```
## Hardcoded Security Checks
These checks are in the script itself and **cannot be bypassed by editing policy files**:
### VirtualBox Check
```bash
function is_virtualbox_package() {
local pkg_lower="${1,,}"
[[ $pkg_lower == *"virtualbox"* || $pkg_lower == *"vbox"* ]]
}
```
- Detects any package with "virtualbox" or "vbox" in name
- Requires word scramble challenge (7-letter words, 120s timeout)
- Auto-enforces hosts file sharing on all VMs after install
### Steam Check
```bash
function is_steam_package() {
[[ $1 == "steam" ]]
}
```
- Only exact match "steam" (not steam-native-runtime etc.)
- **Weekend only** - blocked Monday through Friday 4PM
- Requires word scramble challenge (5-letter words, 60s timeout)
## Word Scramble Challenge
Used for Steam, VirtualBox, and greylisted packages:
```
Challenge: Words with 5 letters
Here are 160 random words. Remember them:
APPLE BRAVE CHAIR DANCE ...
One of those words has been scrambled to: ELPPA
Unscramble the word to proceed (you have 60 seconds):
```
Parameters vary by package type:
| Package Type | Word Length | Words Shown | Timeout | Initial Delay |
|--------------|-------------|-------------|---------|---------------|
| Steam | 5 | 160 | 60s | 0-20s |
| VirtualBox | 7 | 150 | 120s | 0-45s |
| Greylist | 6 | 120 | 90s | 0-30s |
## Integrity Verification
On every invocation, the wrapper verifies policy files haven't been tampered with:
```bash
verify_policy_integrity() {
# Reads /var/lib/pacman-wrapper/policy.sha256
# Compares SHA256 of each policy file
# If mismatch: BLOCKS all operations
}
```
If tampering detected:
```
SECURITY WARNING: Policy file integrity check failed!
CRITICAL: Policy files have been tampered with!
Wrapper operation DENIED. Please reinstall using: sudo install_pacman_wrapper.sh
```
## Hosts Integration
The wrapper integrates with the hosts guard system:
```bash
pre_unlock_hosts() {
# Called before any transaction (-S, -U, -R)
/usr/local/share/hosts-guard/pacman-pre-unlock-hosts.sh
}
post_relock_hosts() {
# Called after transaction completes
/usr/local/share/hosts-guard/pacman-post-relock-hosts.sh
}
```
This allows package installations to modify `/etc/hosts` temporarily (e.g., for network setup) while maintaining protection.
## Common Tasks
### Adding a Blocked Package
1. Edit `pacman_blocked_keywords.txt`:
```bash
echo "newkeyword" >> pacman_blocked_keywords.txt
```
2. Reinstall wrapper to update checksums:
```bash
sudo ./install_pacman_wrapper.sh
```
### Whitelisting a Package
If a legitimate package is being blocked (e.g., `python-firefox-sync` blocked by "firefox" keyword):
1. Edit `pacman_whitelist.txt`:
```bash
echo "python-firefox-sync" >> pacman_whitelist.txt
```
2. Reinstall wrapper:
```bash
sudo ./install_pacman_wrapper.sh
```
### Adding a Challenge Requirement
1. Edit `pacman_greylist.txt`:
```bash
echo "suspicious-package" >> pacman_greylist.txt
```
2. Reinstall wrapper.
### Bypassing the Wrapper (Emergency)
If wrapper is broken and you need real pacman:
```bash
sudo /usr/bin/pacman.orig -S package
```
**Warning**: This bypasses all security checks.
## Post-Transaction Cleanup
After every transaction, the wrapper:
1. Scans installed packages for blocked keywords
2. Removes any that match (shouldn't happen normally)
3. Scans for greylisted packages and removes them
4. Checks if VirtualBox is installed and enforces hosts
```bash
remove_installed_blocked_packages() {
mapfile -t installed_names < <("$PACMAN_BIN" -Qq)
for name in "${installed_names[@]}"; do
if is_blocked_package_name "$name"; then
pacman -Rns --noconfirm "$name"
fi
done
}
```
## Stale Lock Handling
If `/var/lib/pacman/db.lck` exists but no pacman is running:
- Interactive: Prompts user to remove (15s timeout)
- Non-interactive (`--noconfirm`): Auto-removes if lock is >10 minutes old
- If another pacman is actually running: Blocks with error
## Maintenance Auto-Setup
On first run, wrapper checks if periodic maintenance services exist:
```bash
ensure_periodic_maintenance() {
# Checks: periodic-system-maintenance.timer
# periodic-system-startup.service
# hosts-file-monitor.service
# If missing: runs setup_periodic_system.sh
}
```
## Known Gaps (TODO)
1. ❌ `google-chrome` and `google-chrome-stable` not in blocked list
2. ❌ No automatic LeechBlock installation when browsers detected
3. ❌ User can download and install `.deb`/`.tar.gz` manually
4. ❌ AUR packages bypass wrapper (yay/paru call pacman internally)
## Debugging
### Check if wrapper is installed
```bash
ls -la /usr/bin/pacman
# Should show: /usr/bin/pacman -> /path/to/pacman_wrapper.sh
ls -la /usr/bin/pacman.orig
# Should exist and be the real binary
```
### Test policy integrity
```bash
cat /var/lib/pacman-wrapper/policy.sha256
sha256sum /path/to/pacman_blocked_keywords.txt
# Hashes should match
```
### Verbose mode
The wrapper outputs colored status messages to stderr. To see them:
```bash
pacman -S package 2>&1 | cat
```
## DO NOT
1. ❌ Edit policy files without reinstalling wrapper (breaks integrity check)
2. ❌ Remove `/usr/bin/pacman.orig` (breaks all pacman operations)
3. ❌ Symlink pacman to something other than the wrapper
4. ❌ Clear `/var/lib/pacman-wrapper/` without understanding consequences

View File

@ -53,4 +53,8 @@ netsurf
amfora amfora
tartube tartube
youtube youtube
virtualbox # Chrome/Chromium variants
google-chrome
chromium
ungoogled-chromium
thorium

View File

@ -1,4 +1,3 @@
# Packages matching any of these substrings require a challenge to install. # Packages matching any of these substrings require a challenge to install.
# They will also be uninstalled if found already installed. # They will also be uninstalled if found already installed.
# Lines starting with # are comments. # Lines starting with # are comments.
virtualbox

View File

@ -50,7 +50,7 @@ verify_policy_integrity() {
failed=1 failed=1
fi fi
fi fi
done < "$INTEGRITY_FILE" done <"$INTEGRITY_FILE"
if [[ $failed -eq 1 ]]; then if [[ $failed -eq 1 ]]; then
echo -e "${RED}CRITICAL: Policy files have been tampered with!${NC}" >&2 echo -e "${RED}CRITICAL: Policy files have been tampered with!${NC}" >&2
@ -140,7 +140,7 @@ post_relock_hosts() {
# Ensure periodic system services (timer/monitor) are set up; if not, trigger setup # Ensure periodic system services (timer/monitor) are set up; if not, trigger setup
ensure_periodic_maintenance() { ensure_periodic_maintenance() {
# Only proceed if systemd/systemctl is available # Only proceed if systemd/systemctl is available
if ! command -v systemctl > /dev/null 2>&1; then if ! command -v systemctl >/dev/null 2>&1; then
return 0 return 0
fi fi
@ -279,10 +279,10 @@ function has_noconfirm_flag() {
get_lock_holders() { get_lock_holders() {
local lock_file="$1" local lock_file="$1"
holders=() holders=()
if command -v fuser > /dev/null 2>&1; then if command -v fuser >/dev/null 2>&1; then
mapfile -t holders < <(fuser "$lock_file" 2> /dev/null | tr ' ' '\n' | grep -E '^[0-9]+$' || true) mapfile -t holders < <(fuser "$lock_file" 2>/dev/null | tr ' ' '\n' | grep -E '^[0-9]+$' || true)
elif command -v lsof > /dev/null 2>&1; then elif command -v lsof >/dev/null 2>&1; then
mapfile -t holders < <(lsof -t "$lock_file" 2> /dev/null | grep -E '^[0-9]+$' || true) mapfile -t holders < <(lsof -t "$lock_file" 2>/dev/null | grep -E '^[0-9]+$' || true)
fi fi
# Filter out our own PID # Filter out our own PID
if [[ ${#holders[@]} -gt 0 ]]; then if [[ ${#holders[@]} -gt 0 ]]; then
@ -312,8 +312,8 @@ check_and_handle_db_lock() {
local gui_holder=0 local gui_holder=0
for pid in "${holders[@]}"; do for pid in "${holders[@]}"; do
local comm args lower local comm args lower
comm=$(ps -p "$pid" -o comm= 2> /dev/null || true) comm=$(ps -p "$pid" -o comm= 2>/dev/null || true)
args=$(ps -p "$pid" -o args= 2> /dev/null || true) args=$(ps -p "$pid" -o args= 2>/dev/null || true)
lower="${comm,,} ${args,,}" lower="${comm,,} ${args,,}"
if [[ $lower == *" pacman"* || $lower == pacman* || $lower == *"/pacman "* || $lower == *" pamac"* ]]; then if [[ $lower == *" pacman"* || $lower == pacman* || $lower == *"/pacman "* || $lower == *" pamac"* ]]; then
pac_holder=1 pac_holder=1
@ -329,13 +329,13 @@ check_and_handle_db_lock() {
if [[ $gui_holder -eq 1 ]]; then if [[ $gui_holder -eq 1 ]]; then
echo -e "${YELLOW}A background software updater is holding the pacman lock. Attempting to stop it...${NC}" >&2 echo -e "${YELLOW}A background software updater is holding the pacman lock. Attempting to stop it...${NC}" >&2
if command -v systemctl > /dev/null 2>&1; then if command -v systemctl >/dev/null 2>&1; then
systemctl --quiet stop packagekit.service 2> /dev/null || true systemctl --quiet stop packagekit.service 2>/dev/null || true
systemctl --quiet stop packagekit 2> /dev/null || true systemctl --quiet stop packagekit 2>/dev/null || true
fi fi
pkill -x packagekitd 2> /dev/null || true pkill -x packagekitd 2>/dev/null || true
pkill -f gnome-software 2> /dev/null || true pkill -f gnome-software 2>/dev/null || true
pkill -f discover 2> /dev/null || true pkill -f discover 2>/dev/null || true
sleep 1 sleep 1
# Re-check holders # Re-check holders
@ -359,7 +359,7 @@ check_and_handle_db_lock() {
# Decide whether to remove the lock # Decide whether to remove the lock
local now epoch age local now epoch age
if epoch=$(stat -c %Y "$lock_file" 2> /dev/null); then if epoch=$(stat -c %Y "$lock_file" 2>/dev/null); then
now=$(date +%s) now=$(date +%s)
age=$((now - epoch)) age=$((now - epoch))
else else
@ -391,7 +391,7 @@ function remove_installed_packages_matching() {
local check_function="$1" local check_function="$1"
local label="$2" local label="$2"
mapfile -t installed_names < <("$PACMAN_BIN" -Qq 2> /dev/null) mapfile -t installed_names < <("$PACMAN_BIN" -Qq 2>/dev/null)
local to_remove=() local to_remove=()
for name in "${installed_names[@]}"; do for name in "${installed_names[@]}"; do
if "$check_function" "$name"; then if "$check_function" "$name"; then
@ -585,8 +585,8 @@ function run_word_challenge() {
read -t "$timeout_seconds" -r user_input read -t "$timeout_seconds" -r user_input
read_status=$? read_status=$?
kill "$display_pid" 2> /dev/null kill "$display_pid" 2>/dev/null
wait "$display_pid" 2> /dev/null wait "$display_pid" 2>/dev/null
echo echo
if [[ $read_status -ne 0 ]]; then if [[ $read_status -ne 0 ]]; then
@ -671,6 +671,12 @@ fi
# Before any pacman action, ensure maintenance services exist # Before any pacman action, ensure maintenance services exist
ensure_periodic_maintenance ensure_periodic_maintenance
# PROACTIVE CLEANUP: Always check and remove blocked packages at startup
# This catches packages that were installed before the wrapper or via other means
echo -e "${CYAN}Checking for blocked packages...${NC}" >&2
remove_installed_blocked_packages "$@"
remove_installed_greylisted_packages "$@"
# Check for always blocked packages first (highest priority) # Check for always blocked packages first (highest priority)
if check_for_always_blocked "$@"; then if check_for_always_blocked "$@"; then
echo -e "${RED}Installation BLOCKED: This package is permanently restricted and cannot be installed.${NC}" echo -e "${RED}Installation BLOCKED: This package is permanently restricted and cannot be installed.${NC}"
@ -744,6 +750,69 @@ remove_installed_blocked_packages "$@"
# Also remove installed greylisted packages # Also remove installed greylisted packages
remove_installed_greylisted_packages "$@" remove_installed_greylisted_packages "$@"
# Auto-install LeechBlock if a browser is detected
auto_install_leechblock() {
# Only check after install operations
if [[ -z ${1:-} ]] || [[ $1 != "-S"* && $1 != "-U"* ]]; then
return 0
fi
# List of browser packages to check for
local browsers=("firefox" "librewolf" "chromium" "brave" "vivaldi" "google-chrome" "ungoogled-chromium")
local browser_found=0
for browser in "${browsers[@]}"; do
if "$PACMAN_BIN" -Qq "$browser" 2>/dev/null; then
browser_found=1
break
fi
done
if [[ $browser_found -eq 0 ]]; then
return 0
fi
# Find the LeechBlock installer
local script_dir
script_dir="$(dirname "$(readlink -f "$0")")"
local leechblock_installer=""
if [[ -f "$script_dir/../install_leechblock.sh" ]]; then
leechblock_installer="$script_dir/../install_leechblock.sh"
elif [[ -f "$HOME/linux-configuration/scripts/digital_wellbeing/install_leechblock.sh" ]]; then
leechblock_installer="$HOME/linux-configuration/scripts/digital_wellbeing/install_leechblock.sh"
elif [[ -f "/usr/local/share/digital_wellbeing/install_leechblock.sh" ]]; then
leechblock_installer="/usr/local/share/digital_wellbeing/install_leechblock.sh"
fi
if [[ -z $leechblock_installer ]]; then
echo -e "${YELLOW}Browser detected but LeechBlock installer not found.${NC}" >&2
return 0
fi
# Check if LeechBlock is already installed (by looking for the extension directory)
if [[ -d "$HOME/.local/share/leechblockng" ]]; then
return 0
fi
echo -e "${CYAN}Browser detected. Installing LeechBlock extension for website blocking...${NC}" >&2
# Run the LeechBlock installer (as current user, not root)
if [[ $EUID -eq 0 && -n "${SUDO_USER:-}" ]]; then
sudo -u "$SUDO_USER" bash "$leechblock_installer" --install-firefox 2>&1 || {
echo -e "${YELLOW}LeechBlock auto-install failed. Please install manually:${NC}" >&2
echo -e "${YELLOW} $leechblock_installer${NC}" >&2
}
else
bash "$leechblock_installer" --install-firefox 2>&1 || {
echo -e "${YELLOW}LeechBlock auto-install failed. Please install manually:${NC}" >&2
echo -e "${YELLOW} $leechblock_installer${NC}" >&2
}
fi
}
auto_install_leechblock "$@"
# If VirtualBox was involved in this operation, enforce hosts file sharing # If VirtualBox was involved in this operation, enforce hosts file sharing
enforce_vbox_hosts_if_needed() { enforce_vbox_hosts_if_needed() {
# Only check after install operations # Only check after install operations
@ -785,7 +854,7 @@ enforce_vbox_hosts_if_needed() {
fi fi
# Check if enforcement is already applied # Check if enforcement is already applied
if bash "$vbox_enforce_script" check > /dev/null 2>&1; then if bash "$vbox_enforce_script" check >/dev/null 2>&1; then
return 0 return 0
fi fi

View File

@ -13,9 +13,9 @@ source "$SCRIPT_DIR/../lib/common.sh"
# Schedule constants (single source of truth for this script) # Schedule constants (single source of truth for this script)
# These values are written to /etc/shutdown-schedule.conf during setup # These values are written to /etc/shutdown-schedule.conf during setup
SCHEDULE_MON_WED_HOUR=21 SCHEDULE_MON_WED_HOUR=24
SCHEDULE_THU_SUN_HOUR=22 SCHEDULE_THU_SUN_HOUR=24
SCHEDULE_MORNING_END_HOUR=5 SCHEDULE_MORNING_END_HOUR=0
# ============================================================================ # ============================================================================
# SCHEDULE PROTECTION MECHANISM # SCHEDULE PROTECTION MECHANISM
@ -24,7 +24,6 @@ SCHEDULE_MORNING_END_HOUR=5
# If a canonical config already exists, the script compares against it and # If a canonical config already exists, the script compares against it and
# BLOCKS installation if the new values would make the schedule MORE LENIENT # BLOCKS installation if the new values would make the schedule MORE LENIENT
# (i.e., later shutdown hours or earlier morning end). # (i.e., later shutdown hours or earlier morning end).
# To legitimately change the schedule, use: sudo /usr/local/sbin/unlock-shutdown-schedule
# ============================================================================ # ============================================================================
CANONICAL_CONFIG="/usr/local/share/locked-shutdown-schedule.conf" CANONICAL_CONFIG="/usr/local/share/locked-shutdown-schedule.conf"
@ -39,7 +38,7 @@ check_schedule_protection() {
# Load canonical values # Load canonical values
local canonical_mon_wed canonical_thu_sun canonical_morning_end local canonical_mon_wed canonical_thu_sun canonical_morning_end
# shellcheck source=/dev/null # shellcheck source=/dev/null
source "$CANONICAL_CONFIG" 2> /dev/null || return 0 source "$CANONICAL_CONFIG" 2>/dev/null || return 0
canonical_mon_wed="${MON_WED_HOUR:-}" canonical_mon_wed="${MON_WED_HOUR:-}"
canonical_thu_sun="${THU_SUN_HOUR:-}" canonical_thu_sun="${THU_SUN_HOUR:-}"
canonical_morning_end="${MORNING_END_HOUR:-}" canonical_morning_end="${MORNING_END_HOUR:-}"
@ -69,27 +68,10 @@ check_schedule_protection() {
if [[ ${#violations[@]} -gt 0 ]]; then if [[ ${#violations[@]} -gt 0 ]]; then
echo "" echo ""
echo "╔══════════════════════════════════════════════════════════════════╗" echo "╔══════════════════════════════════════════════════════════════════╗"
echo "║ ❌ SCHEDULE MODIFICATION BLOCKED - CHEATING DETECTED! ❌ ║" echo "║ ❌ OPERATION NOT PERMITTED ❌ ║"
echo "╚══════════════════════════════════════════════════════════════════╝" echo "╚══════════════════════════════════════════════════════════════════╝"
echo "" echo ""
echo "You modified the script to make the shutdown schedule MORE LENIENT:" echo "The requested schedule modification has been denied."
echo ""
for v in "${violations[@]}"; do
echo "$v"
done
echo ""
echo "Current protected schedule:"
echo " Monday-Wednesday: ${canonical_mon_wed}:00 - 0${canonical_morning_end}:00"
echo " Thursday-Sunday: ${canonical_thu_sun}:00 - 0${canonical_morning_end}:00"
echo ""
echo "Nice try! But this is exactly the kind of late-night bargaining"
echo "that this protection is designed to prevent. 😉"
echo ""
echo "If you REALLY need to change the schedule, use the proper unlock:"
echo " sudo /usr/local/sbin/unlock-shutdown-schedule"
echo ""
echo "This requires waiting through a psychological delay to give you"
echo "time to reconsider whether you actually need more screen time."
echo "" echo ""
exit 1 exit 1
fi fi
@ -189,13 +171,13 @@ show_current_status() {
# Check systemd status # Check systemd status
if $timer_exists; then if $timer_exists; then
if systemctl is-enabled day-specific-shutdown.timer &> /dev/null; then if systemctl is-enabled day-specific-shutdown.timer &>/dev/null; then
echo "✓ Timer is enabled" echo "✓ Timer is enabled"
if systemctl is-active day-specific-shutdown.timer &> /dev/null; then if systemctl is-active day-specific-shutdown.timer &>/dev/null; then
echo "✓ Timer is active" echo "✓ Timer is active"
echo "" echo ""
echo "Next scheduled shutdown check:" echo "Next scheduled shutdown check:"
systemctl list-timers day-specific-shutdown.timer --no-pager 2> /dev/null | grep day-specific-shutdown || echo "Timer information not available" systemctl list-timers day-specific-shutdown.timer --no-pager 2>/dev/null | grep day-specific-shutdown || echo "Timer information not available"
else else
echo "✗ Timer is not active" echo "✗ Timer is not active"
fi fi
@ -210,9 +192,9 @@ show_current_status() {
# Check monitor service status # Check monitor service status
echo "Monitor Service Status:" echo "Monitor Service Status:"
if systemctl is-enabled shutdown-timer-monitor.service &> /dev/null; then if systemctl is-enabled shutdown-timer-monitor.service &>/dev/null; then
echo "✓ Monitor is enabled" echo "✓ Monitor is enabled"
if systemctl is-active shutdown-timer-monitor.service &> /dev/null; then if systemctl is-active shutdown-timer-monitor.service &>/dev/null; then
echo "✓ Monitor is active (will re-enable timer if disabled)" echo "✓ Monitor is active (will re-enable timer if disabled)"
else else
echo "✗ Monitor is not active" echo "✗ Monitor is not active"
@ -231,7 +213,7 @@ show_current_status() {
if [[ -f $config_file ]]; then if [[ -f $config_file ]]; then
echo "✓ Config file exists" echo "✓ Config file exists"
# Check immutable attribute # Check immutable attribute
if lsattr "$config_file" 2> /dev/null | grep -q '^....i'; then if lsattr "$config_file" 2>/dev/null | grep -q '^....i'; then
echo "✓ Config file is immutable (chattr +i)" echo "✓ Config file is immutable (chattr +i)"
else else
echo "✗ Config file is NOT immutable" echo "✗ Config file is NOT immutable"
@ -246,9 +228,9 @@ show_current_status() {
echo "✗ Canonical copy missing" echo "✗ Canonical copy missing"
fi fi
if systemctl is-enabled shutdown-schedule-guard.path &> /dev/null; then if systemctl is-enabled shutdown-schedule-guard.path &>/dev/null; then
echo "✓ Config path watcher is enabled" echo "✓ Config path watcher is enabled"
if systemctl is-active shutdown-schedule-guard.path &> /dev/null; then if systemctl is-active shutdown-schedule-guard.path &>/dev/null; then
echo "✓ Config path watcher is active" echo "✓ Config path watcher is active"
else else
echo "✗ Config path watcher is not active" echo "✗ Config path watcher is not active"
@ -257,12 +239,6 @@ show_current_status() {
echo "✗ Config path watcher is not enabled" echo "✗ Config path watcher is not enabled"
fi fi
if [[ -f "/usr/local/sbin/unlock-shutdown-schedule" ]]; then
echo "✓ Unlock script exists"
else
echo "✗ Unlock script missing"
fi
echo "" echo ""
echo "Shutdown Schedule:" echo "Shutdown Schedule:"
echo " Monday-Wednesday: ${SCHEDULE_MON_WED_HOUR}:00-0${SCHEDULE_MORNING_END_HOUR}:00" echo " Monday-Wednesday: ${SCHEDULE_MON_WED_HOUR}:00-0${SCHEDULE_MORNING_END_HOUR}:00"
@ -275,7 +251,6 @@ show_current_status() {
echo " - Immutable attribute (chattr +i)" echo " - Immutable attribute (chattr +i)"
echo " - Canonical copy that auto-restores on modification" echo " - Canonical copy that auto-restores on modification"
echo " - Path watcher service" echo " - Path watcher service"
echo " To modify: sudo /usr/local/sbin/unlock-shutdown-schedule"
echo "" echo ""
} }
@ -290,10 +265,10 @@ create_shutdown_config() {
local canonical_file="/usr/local/share/locked-shutdown-schedule.conf" local canonical_file="/usr/local/share/locked-shutdown-schedule.conf"
# Remove immutable attribute if it exists (to allow update) # Remove immutable attribute if it exists (to allow update)
chattr -i "$config_file" 2> /dev/null || true chattr -i "$config_file" 2>/dev/null || true
chattr -i "$canonical_file" 2> /dev/null || true chattr -i "$canonical_file" 2>/dev/null || true
cat > "$config_file" << EOF cat >"$config_file" <<EOF
# Shutdown schedule configuration # Shutdown schedule configuration
# This file is managed by setup_midnight_shutdown.sh # This file is managed by setup_midnight_shutdown.sh
# Used by: day-specific-shutdown-check.sh, shutdown_countdown.sh (i3blocks) # Used by: day-specific-shutdown-check.sh, shutdown_countdown.sh (i3blocks)
@ -303,11 +278,7 @@ create_shutdown_config() {
# 2. Canonical copy at /usr/local/share/locked-shutdown-schedule.conf # 2. Canonical copy at /usr/local/share/locked-shutdown-schedule.conf
# 3. Path watcher service that auto-restores if modified # 3. Path watcher service that auto-restores if modified
# #
# To modify this file, you need to: # Modifications to this file will be automatically reverted.
# 1. Run: sudo /usr/local/sbin/unlock-shutdown-schedule
# 2. Wait through the psychological delay
# 3. Edit the file during the brief unlock window
# 4. The file will be re-locked automatically
# Shutdown hour for Monday-Wednesday (24-hour format) # Shutdown hour for Monday-Wednesday (24-hour format)
MON_WED_HOUR=${SCHEDULE_MON_WED_HOUR} MON_WED_HOUR=${SCHEDULE_MON_WED_HOUR}
@ -339,12 +310,13 @@ create_config_guard() {
echo "========================================================" echo "========================================================"
local enforce_script="/usr/local/sbin/enforce-shutdown-schedule.sh" local enforce_script="/usr/local/sbin/enforce-shutdown-schedule.sh"
local unlock_script="/usr/local/sbin/unlock-shutdown-schedule" # Obscure name for unlock script - not documented anywhere
local unlock_script="/usr/local/sbin/.sd-sched-mgmt"
local guard_service="/etc/systemd/system/shutdown-schedule-guard.service" local guard_service="/etc/systemd/system/shutdown-schedule-guard.service"
local guard_path="/etc/systemd/system/shutdown-schedule-guard.path" local guard_path="/etc/systemd/system/shutdown-schedule-guard.path"
# Create enforcement script # Create enforcement script
cat > "$enforce_script" << 'EOF' cat >"$enforce_script" <<'EOF'
#!/bin/bash #!/bin/bash
# Enforce canonical /etc/shutdown-schedule.conf contents # Enforce canonical /etc/shutdown-schedule.conf contents
# This script restores the config from canonical copy if tampered # This script restores the config from canonical copy if tampered
@ -386,7 +358,7 @@ EOF
echo "✓ Created enforcement script: $enforce_script" echo "✓ Created enforcement script: $enforce_script"
# Create unlock script with psychological delay # Create unlock script with psychological delay
cat > "$unlock_script" << 'EOF' cat >"$unlock_script" <<'EOF'
#!/bin/bash #!/bin/bash
# Unlock shutdown schedule config for editing with smart friction # Unlock shutdown schedule config for editing with smart friction
# This script: # This script:
@ -599,10 +571,10 @@ echo ""
EOF EOF
chmod +x "$unlock_script" chmod +x "$unlock_script"
echo "✓ Created unlock script: $unlock_script" # Silently create unlock script - do not announce its existence
# Create path watcher unit # Create path watcher unit
cat > "$guard_path" << 'EOF' cat >"$guard_path" <<'EOF'
[Unit] [Unit]
Description=Watch /etc/shutdown-schedule.conf and trigger enforcement Description=Watch /etc/shutdown-schedule.conf and trigger enforcement
@ -617,7 +589,7 @@ EOF
echo "✓ Created path watcher: $guard_path" echo "✓ Created path watcher: $guard_path"
# Create enforcement service # Create enforcement service
cat > "$guard_service" << 'EOF' cat >"$guard_service" <<'EOF'
[Unit] [Unit]
Description=Enforce canonical /etc/shutdown-schedule.conf contents Description=Enforce canonical /etc/shutdown-schedule.conf contents
After=local-fs.target After=local-fs.target
@ -652,7 +624,7 @@ create_shutdown_service() {
local service_file="/etc/systemd/system/day-specific-shutdown.service" local service_file="/etc/systemd/system/day-specific-shutdown.service"
cat > "$service_file" << 'EOF' cat >"$service_file" <<'EOF'
[Unit] [Unit]
Description=Automatic PC shutdown with day-specific time windows Description=Automatic PC shutdown with day-specific time windows
DefaultDependencies=false DefaultDependencies=false
@ -686,7 +658,7 @@ create_shutdown_timer() {
# Generate timer entries dynamically from earliest_hour to MORNING_END_HOUR # Generate timer entries dynamically from earliest_hour to MORNING_END_HOUR
# This ensures timer fires at all possible shutdown times # This ensures timer fires at all possible shutdown times
{ {
cat << EOF cat <<EOF
[Unit] [Unit]
Description=Timer for automatic PC shutdown with day-specific windows Description=Timer for automatic PC shutdown with day-specific windows
Requires=day-specific-shutdown.service Requires=day-specific-shutdown.service
@ -707,7 +679,7 @@ EOF
fi fi
done done
cat << EOF cat <<EOF
Persistent=false Persistent=false
AccuracySec=1s AccuracySec=1s
WakeSystem=false WakeSystem=false
@ -716,7 +688,7 @@ RandomizedDelaySec=0
[Install] [Install]
WantedBy=timers.target WantedBy=timers.target
EOF EOF
} > "$timer_file" } >"$timer_file"
echo "✓ Created systemd timer: $timer_file" echo "✓ Created systemd timer: $timer_file"
echo " Timer covers: ${earliest_hour}:00 to 0${SCHEDULE_MORNING_END_HOUR}:00" echo " Timer covers: ${earliest_hour}:00 to 0${SCHEDULE_MORNING_END_HOUR}:00"
@ -730,7 +702,7 @@ create_management_script() {
local script_file="/usr/local/bin/day-specific-shutdown-manager.sh" local script_file="/usr/local/bin/day-specific-shutdown-manager.sh"
cat > "$script_file" << 'EOF' cat >"$script_file" <<'EOF'
#!/bin/bash #!/bin/bash
# Day-Specific Auto-Shutdown Manager # Day-Specific Auto-Shutdown Manager
# Provides easy management of the day-specific shutdown feature # Provides easy management of the day-specific shutdown feature
@ -822,7 +794,7 @@ create_shutdown_check_script() {
local check_script="/usr/local/bin/day-specific-shutdown-check.sh" local check_script="/usr/local/bin/day-specific-shutdown-check.sh"
cat > "$check_script" << 'EOF' cat >"$check_script" <<'EOF'
#!/bin/bash #!/bin/bash
# Smart day-specific shutdown check script # Smart day-specific shutdown check script
# Reads shutdown windows from /etc/shutdown-schedule.conf # Reads shutdown windows from /etc/shutdown-schedule.conf
@ -937,7 +909,7 @@ install_monitor_service() {
local monitor_watchdog_service="/etc/systemd/system/shutdown-timer-monitor-watchdog.service" local monitor_watchdog_service="/etc/systemd/system/shutdown-timer-monitor-watchdog.service"
# Create the monitor script # Create the monitor script
cat > "$monitor_script" << 'EOF' cat >"$monitor_script" <<'EOF'
#!/bin/bash #!/bin/bash
# Shutdown timer monitor script # Shutdown timer monitor script
# Watches the day-specific-shutdown timer and re-enables it if disabled # Watches the day-specific-shutdown timer and re-enables it if disabled
@ -1019,7 +991,7 @@ EOF
echo "✓ Created monitor script: $monitor_script" echo "✓ Created monitor script: $monitor_script"
# Create the monitor service with RefuseManualStop to prevent manual stopping # Create the monitor service with RefuseManualStop to prevent manual stopping
cat > "$monitor_service" << 'EOF' cat >"$monitor_service" <<'EOF'
[Unit] [Unit]
Description=Shutdown Timer Monitor and Auto-Restore Service Description=Shutdown Timer Monitor and Auto-Restore Service
After=network-online.target day-specific-shutdown.timer After=network-online.target day-specific-shutdown.timer
@ -1051,7 +1023,7 @@ EOF
echo "✓ Created monitor service: $monitor_service" echo "✓ Created monitor service: $monitor_service"
# Create a watchdog timer that ensures the monitor stays running # Create a watchdog timer that ensures the monitor stays running
cat > "$monitor_watchdog_service" << 'EOF' cat >"$monitor_watchdog_service" <<'EOF'
[Unit] [Unit]
Description=Watchdog for Shutdown Timer Monitor Description=Watchdog for Shutdown Timer Monitor
After=multi-user.target After=multi-user.target
@ -1064,7 +1036,7 @@ EOF
echo "✓ Created watchdog service: $monitor_watchdog_service" echo "✓ Created watchdog service: $monitor_watchdog_service"
cat > "$monitor_timer" << 'EOF' cat >"$monitor_timer" <<'EOF'
[Unit] [Unit]
Description=Watchdog Timer for Shutdown Timer Monitor Description=Watchdog Timer for Shutdown Timer Monitor
After=multi-user.target After=multi-user.target
@ -1117,13 +1089,13 @@ test_setup() {
echo "" echo ""
echo "Timer status:" echo "Timer status:"
if systemctl is-enabled day-specific-shutdown.timer &> /dev/null; then if systemctl is-enabled day-specific-shutdown.timer &>/dev/null; then
echo "✓ Timer is enabled" echo "✓ Timer is enabled"
else else
echo "✗ Timer is not enabled" echo "✗ Timer is not enabled"
fi fi
if systemctl is-active day-specific-shutdown.timer &> /dev/null; then if systemctl is-active day-specific-shutdown.timer &>/dev/null; then
echo "✓ Timer is active" echo "✓ Timer is active"
else else
echo "✗ Timer is not active" echo "✗ Timer is not active"
@ -1131,13 +1103,13 @@ test_setup() {
echo "" echo ""
echo "Monitor status:" echo "Monitor status:"
if systemctl is-enabled shutdown-timer-monitor.service &> /dev/null; then if systemctl is-enabled shutdown-timer-monitor.service &>/dev/null; then
echo "✓ Monitor is enabled" echo "✓ Monitor is enabled"
else else
echo "✗ Monitor is not enabled" echo "✗ Monitor is not enabled"
fi fi
if systemctl is-active shutdown-timer-monitor.service &> /dev/null; then if systemctl is-active shutdown-timer-monitor.service &>/dev/null; then
echo "✓ Monitor is active" echo "✓ Monitor is active"
else else
echo "✗ Monitor is not active" echo "✗ Monitor is not active"
@ -1145,13 +1117,13 @@ test_setup() {
echo "" echo ""
echo "Watchdog timer status:" echo "Watchdog timer status:"
if systemctl is-enabled shutdown-timer-monitor-watchdog.timer &> /dev/null; then if systemctl is-enabled shutdown-timer-monitor-watchdog.timer &>/dev/null; then
echo "✓ Watchdog timer is enabled" echo "✓ Watchdog timer is enabled"
else else
echo "✗ Watchdog timer is not enabled" echo "✗ Watchdog timer is not enabled"
fi fi
if systemctl is-active shutdown-timer-monitor-watchdog.timer &> /dev/null; then if systemctl is-active shutdown-timer-monitor-watchdog.timer &>/dev/null; then
echo "✓ Watchdog timer is active" echo "✓ Watchdog timer is active"
else else
echo "✗ Watchdog timer is not active" echo "✗ Watchdog timer is not active"
@ -1164,7 +1136,7 @@ test_setup() {
if [[ -f $config_file ]]; then if [[ -f $config_file ]]; then
echo "✓ Config file exists" echo "✓ Config file exists"
if lsattr "$config_file" 2> /dev/null | grep -q '^....i'; then if lsattr "$config_file" 2>/dev/null | grep -q '^....i'; then
echo "✓ Config file is immutable" echo "✓ Config file is immutable"
else else
echo "✗ Config file is NOT immutable" echo "✗ Config file is NOT immutable"
@ -1179,27 +1151,21 @@ test_setup() {
echo "✗ Canonical copy missing" echo "✗ Canonical copy missing"
fi fi
if systemctl is-enabled shutdown-schedule-guard.path &> /dev/null; then if systemctl is-enabled shutdown-schedule-guard.path &>/dev/null; then
echo "✓ Config guard path watcher is enabled" echo "✓ Config guard path watcher is enabled"
else else
echo "✗ Config guard path watcher is not enabled" echo "✗ Config guard path watcher is not enabled"
fi fi
if systemctl is-active shutdown-schedule-guard.path &> /dev/null; then if systemctl is-active shutdown-schedule-guard.path &>/dev/null; then
echo "✓ Config guard path watcher is active" echo "✓ Config guard path watcher is active"
else else
echo "✗ Config guard path watcher is not active" echo "✗ Config guard path watcher is not active"
fi fi
if [[ -f "/usr/local/sbin/unlock-shutdown-schedule" ]]; then
echo "✓ Unlock script exists"
else
echo "✗ Unlock script missing"
fi
echo "" echo ""
echo "Next scheduled checks:" echo "Next scheduled checks:"
systemctl list-timers day-specific-shutdown.timer --no-pager 2> /dev/null | head -5 | grep day-specific-shutdown || echo "Timer information not available" systemctl list-timers day-specific-shutdown.timer --no-pager 2>/dev/null | head -5 | grep day-specific-shutdown || echo "Timer information not available"
} }
# Display the shutdown schedule (used in multiple places) # Display the shutdown schedule (used in multiple places)
@ -1245,19 +1211,13 @@ show_instructions() {
echo " sudo day-specific-shutdown-manager.sh status - Check status" echo " sudo day-specific-shutdown-manager.sh status - Check status"
echo " sudo day-specific-shutdown-manager.sh logs - View shutdown logs" echo " sudo day-specific-shutdown-manager.sh logs - View shutdown logs"
echo "" echo ""
echo "To modify shutdown hours (with psychological friction):"
echo " sudo /usr/local/sbin/unlock-shutdown-schedule"
echo ""
echo "How it works:" echo "How it works:"
echo "• Timer checks every 30 minutes during potential shutdown windows" echo "• Timer checks every 30 minutes during potential shutdown windows"
echo "• Smart logic determines shutdown eligibility based on day and time" echo "• Smart logic determines shutdown eligibility based on day and time"
echo "• Monitor service watches the timer and re-enables it if disabled" echo "• Monitor service watches the timer and re-enables it if disabled"
echo "• Watchdog timer restarts the monitor every 60 seconds if stopped" echo "• Watchdog timer restarts the monitor every 60 seconds if stopped"
echo "• Monitor has RefuseManualStop=true to prevent easy stopping" echo "• Monitor has RefuseManualStop=true to prevent easy stopping"
echo "• Config file is protected by:" echo "• Config file is protected by multiple security layers"
echo " - Immutable attribute (chattr +i)"
echo " - Canonical copy at /usr/local/share/locked-shutdown-schedule.conf"
echo " - Path watcher that auto-restores if you modify the file"
echo "• There is NO disable option - this is intentional for digital wellbeing" echo "• There is NO disable option - this is intentional for digital wellbeing"
echo "" echo ""
echo "WARNING: This will automatically shutdown your PC during designated hours." echo "WARNING: This will automatically shutdown your PC during designated hours."
@ -1338,18 +1298,18 @@ enable_midnight_shutdown() {
# Parse command line arguments # Parse command line arguments
case "${1:-enable}" in case "${1:-enable}" in
"enable") "enable")
check_sudo "$@" check_sudo "$@"
enable_midnight_shutdown enable_midnight_shutdown
;; ;;
"status") "status")
check_sudo "$@" check_sudo "$@"
show_current_status show_current_status
;; ;;
"help" | "-h" | "--help") "help" | "-h" | "--help")
show_usage show_usage
;; ;;
*) *)
echo "Error: Unknown command '$1'" echo "Error: Unknown command '$1'"
echo "" echo ""
show_usage show_usage

22
scripts/fixes/fix_waifu2x.sh Executable file
View File

@ -0,0 +1,22 @@
#!/bin/bash
# Fix waifu2x-converter-cpp-cuda-git for CUDA 13+
# CUDA 13 minimum supported arch is sm_75 (Turing)
PKGBUILD="$HOME/.cache/yay/waifu2x-converter-cpp-cuda-git/PKGBUILD"
if [[ ! -f "$PKGBUILD" ]]; then
echo "PKGBUILD not found. Run 'yay waifu2x-converter-cpp-cuda-git' first to download it."
exit 1
fi
# Add sed commands to prepare() function to replace sm_52/ptx52 with sm_75/ptx75
if grep -q 's/sm_52/sm_75' "$PKGBUILD"; then
echo "PKGBUILD already patched."
else
sed -i '/^prepare() {$/a\
# Fix for CUDA 13+ which requires sm_75+ (Turing)\
sed -i "s/sm_52/sm_75/g" waifu2x-converter-cpp/CMakeLists.txt\
sed -i "s/ptx52/ptx75/g" waifu2x-converter-cpp/CMakeLists.txt\
sed -i "s/ptx52/ptx75/g" waifu2x-converter-cpp/src/modelHandler_CUDA.cpp' "$PKGBUILD"
echo "PKGBUILD patched. Now run 'yay waifu2x-converter-cpp-cuda-git' again."
fi

8
test_results.log Normal file
View File

@ -0,0 +1,8 @@
==========================================
Security Hardening Test Suite
==========================================
Testing components in: /home/kuhy/linux-configuration
--- HOSTS GUARD ---
✅ PASS: /etc/hosts is immutable

385
tests/test_security_hardening.sh Executable file
View File

@ -0,0 +1,385 @@
#!/bin/bash
# tests/test_security_hardening.sh
# Verify all security mechanisms are working
#
# Run with: bash tests/test_security_hardening.sh
# Some tests require root privileges
set -uo pipefail
# Note: NOT using -e because we need to handle test failures gracefully
PASS=0
FAIL=0
SKIP=0
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
test_result() {
local name="$1"
local result="$2"
local reason="${3:-}"
case "$result" in
pass)
echo -e "${GREEN}✅ PASS${NC}: $name"
((PASS++))
;;
fail)
echo -e "${RED}❌ FAIL${NC}: $name"
[[ -n "$reason" ]] && echo -e " ${RED}Reason: $reason${NC}"
((FAIL++))
;;
skip)
echo -e "${YELLOW}⏭️ SKIP${NC}: $name"
[[ -n "$reason" ]] && echo -e " ${YELLOW}Reason: $reason${NC}"
((SKIP++))
;;
esac
}
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_DIR="$(dirname "$SCRIPT_DIR")"
echo "=========================================="
echo "Security Hardening Test Suite"
echo "=========================================="
echo ""
echo "Testing components in: $REPO_DIR"
echo ""
# ==================================================================
# HOSTS GUARD TESTS
# ==================================================================
echo "--- HOSTS GUARD ---"
# Test 1: /etc/hosts is immutable
if [[ -f /etc/hosts ]]; then
if lsattr /etc/hosts 2>/dev/null | grep -q '^....i'; then
test_result "/etc/hosts is immutable" "pass"
else
test_result "/etc/hosts is immutable" "fail" "chattr +i not set"
fi
else
test_result "/etc/hosts is immutable" "skip" "File not found"
fi
# Test 2: hosts-guard.path is active
if systemctl is-active --quiet hosts-guard.path 2>/dev/null; then
test_result "hosts-guard.path is active" "pass"
else
test_result "hosts-guard.path is active" "fail" "Service not running"
fi
# Test 3: hosts-bind-mount.service is active
if systemctl is-active --quiet hosts-bind-mount.service 2>/dev/null; then
test_result "hosts-bind-mount.service is active" "pass"
else
test_result "hosts-bind-mount.service is active" "fail" "Service not running"
fi
# Test 4: Canonical hosts copy exists
if [[ -f /usr/local/share/locked-hosts ]]; then
test_result "Canonical hosts copy exists" "pass"
else
test_result "Canonical hosts copy exists" "fail" "Not found at /usr/local/share/locked-hosts"
fi
# Test 5: nsswitch-guard.path is active (NEW)
if systemctl is-active --quiet nsswitch-guard.path 2>/dev/null; then
test_result "nsswitch-guard.path is active" "pass"
else
test_result "nsswitch-guard.path is active" "fail" "Service not running"
fi
# Test 6: /etc/nsswitch.conf is immutable (NEW)
if [[ -f /etc/nsswitch.conf ]]; then
if lsattr /etc/nsswitch.conf 2>/dev/null | grep -q '^....i'; then
test_result "/etc/nsswitch.conf is immutable" "pass"
else
test_result "/etc/nsswitch.conf is immutable" "fail" "chattr +i not set"
fi
else
test_result "/etc/nsswitch.conf is immutable" "skip" "File not found"
fi
# Test 7: nsswitch.conf has correct hosts line
if [[ -f /etc/nsswitch.conf ]]; then
hosts_line=$(grep "^hosts:" /etc/nsswitch.conf 2>/dev/null || true)
if echo "$hosts_line" | grep -q 'files.*dns\|files.*myhostname'; then
test_result "nsswitch.conf has 'files' before 'dns'" "pass"
elif [[ -z "$hosts_line" ]]; then
test_result "nsswitch.conf has 'files' before 'dns'" "fail" "No hosts: line found"
else
test_result "nsswitch.conf has 'files' before 'dns'" "fail" "hosts line: $hosts_line"
fi
else
test_result "nsswitch.conf has 'files' before 'dns'" "skip" "File not found"
fi
echo ""
# ==================================================================
# SHUTDOWN SCHEDULE TESTS
# ==================================================================
echo "--- SHUTDOWN SCHEDULE ---"
# Test 8: shutdown-schedule.conf is immutable
if [[ -f /etc/shutdown-schedule.conf ]]; then
if lsattr /etc/shutdown-schedule.conf 2>/dev/null | grep -q '^....i'; then
test_result "/etc/shutdown-schedule.conf is immutable" "pass"
else
test_result "/etc/shutdown-schedule.conf is immutable" "fail" "chattr +i not set"
fi
else
test_result "/etc/shutdown-schedule.conf is immutable" "skip" "Not installed"
fi
# Test 9: shutdown timer is active
if systemctl is-active --quiet day-specific-shutdown.timer 2>/dev/null; then
test_result "day-specific-shutdown.timer is active" "pass"
else
test_result "day-specific-shutdown.timer is active" "fail" "Timer not running"
fi
# Test 10: shutdown schedule guard is active
if systemctl is-active --quiet shutdown-schedule-guard.path 2>/dev/null; then
test_result "shutdown-schedule-guard.path is active" "pass"
else
test_result "shutdown-schedule-guard.path is active" "fail" "Guard not running"
fi
# Test 11: Unlock script has obscure name (no helpful path)
if [[ -f /usr/local/sbin/.sd-sched-mgmt ]]; then
test_result "Unlock script uses obscure name" "pass"
elif [[ -f /usr/local/sbin/unlock-shutdown-schedule ]]; then
test_result "Unlock script uses obscure name" "fail" "Still using obvious name"
else
test_result "Unlock script uses obscure name" "skip" "Not installed"
fi
echo ""
# ==================================================================
# PACMAN WRAPPER TESTS
# ==================================================================
echo "--- PACMAN WRAPPER ---"
# Test 12: pacman wrapper is installed
if [[ -L /usr/bin/pacman ]] && [[ -f /usr/bin/pacman.orig ]]; then
test_result "pacman wrapper installed" "pass"
else
if [[ ! -L /usr/bin/pacman ]]; then
test_result "pacman wrapper installed" "fail" "/usr/bin/pacman is not a symlink"
else
test_result "pacman wrapper installed" "fail" "/usr/bin/pacman.orig not found"
fi
fi
# Test 13: google-chrome is blocked
blocked_file="$REPO_DIR/scripts/digital_wellbeing/pacman/pacman_blocked_keywords.txt"
if [[ -f "$blocked_file" ]]; then
if grep -qi "google-chrome" "$blocked_file"; then
test_result "google-chrome in blocked list" "pass"
else
test_result "google-chrome in blocked list" "fail" "Not found in $blocked_file"
fi
else
test_result "google-chrome in blocked list" "skip" "Blocked keywords file not found"
fi
# Test 14: chromium is blocked
if [[ -f "$blocked_file" ]]; then
if grep -qi "^chromium$" "$blocked_file"; then
test_result "chromium in blocked list" "pass"
else
test_result "chromium in blocked list" "fail" "Not found in $blocked_file"
fi
else
test_result "chromium in blocked list" "skip" "Blocked keywords file not found"
fi
# Test 15: Policy integrity file exists
if [[ -f /var/lib/pacman-wrapper/policy.sha256 ]]; then
test_result "Pacman policy integrity file exists" "pass"
else
test_result "Pacman policy integrity file exists" "fail" "Not found"
fi
# Test 16: LeechBlock auto-install function exists in wrapper
wrapper_file="$REPO_DIR/scripts/digital_wellbeing/pacman/pacman_wrapper.sh"
if [[ -f "$wrapper_file" ]]; then
if grep -q "auto_install_leechblock" "$wrapper_file"; then
test_result "LeechBlock auto-install function exists" "pass"
else
test_result "LeechBlock auto-install function exists" "fail" "Function not found"
fi
else
test_result "LeechBlock auto-install function exists" "skip" "Wrapper file not found"
fi
echo ""
# ==================================================================
# COMPULSIVE BLOCK TESTS
# ==================================================================
echo "--- COMPULSIVE OPENING BLOCK ---"
compulsive_file="$REPO_DIR/scripts/digital_wellbeing/block_compulsive_opening.sh"
# Test 17: Auto-close timer configuration exists
if [[ -f "$compulsive_file" ]]; then
if grep -q "AUTO_CLOSE_TIMEOUT_MINUTES" "$compulsive_file"; then
test_result "Auto-close timer configuration exists" "pass"
else
test_result "Auto-close timer configuration exists" "fail" "Variable not found"
fi
else
test_result "Auto-close timer configuration exists" "skip" "Script not found"
fi
# Test 18: launch_with_timer function exists
if [[ -f "$compulsive_file" ]]; then
if grep -q "launch_with_timer" "$compulsive_file"; then
test_result "launch_with_timer function exists" "pass"
else
test_result "launch_with_timer function exists" "fail" "Function not found"
fi
else
test_result "launch_with_timer function exists" "skip" "Script not found"
fi
# Test 19: Compulsive block wrappers installed
wrappers_ok=true
for app in beeper signal-desktop discord; do
if [[ -f "/usr/bin/$app" ]]; then
if grep -q "block-compulsive-opening" "/usr/bin/$app" 2>/dev/null; then
: # OK
else
wrappers_ok=false
fi
fi
done
if [[ "$wrappers_ok" == true ]]; then
test_result "Compulsive block wrappers installed" "pass"
else
test_result "Compulsive block wrappers installed" "fail" "Some wrappers missing or incorrect"
fi
echo ""
# ==================================================================
# FOCUS MODE DAEMON TESTS
# ==================================================================
echo "--- FOCUS MODE DAEMON ---"
focus_daemon="$REPO_DIR/scripts/digital_wellbeing/focus_mode_daemon.py"
focus_installer="$REPO_DIR/scripts/digital_wellbeing/install_focus_mode_daemon.sh"
# Test 20: Focus mode daemon script exists
if [[ -f "$focus_daemon" ]]; then
test_result "Focus mode daemon script exists" "pass"
else
test_result "Focus mode daemon script exists" "fail" "Not found at $focus_daemon"
fi
# Test 21: Focus mode installer exists
if [[ -f "$focus_installer" ]]; then
test_result "Focus mode installer exists" "pass"
else
test_result "Focus mode installer exists" "fail" "Not found at $focus_installer"
fi
# Test 22: Focus mode daemon is running (user service)
if systemctl --user is-active --quiet focus-mode.service 2>/dev/null; then
test_result "Focus mode daemon is running" "pass"
else
test_result "Focus mode daemon is running" "fail" "User service not running"
fi
echo ""
# ==================================================================
# SCREEN LOCKER TESTS
# ==================================================================
echo "--- SCREEN LOCKER ---"
screen_locker="$HOME/testsAndMisc/python_pkg/screen_locker/screen_lock.py"
# Test 23: Screen locker exists
if [[ -f "$screen_locker" ]]; then
test_result "Screen locker script exists" "pass"
else
test_result "Screen locker script exists" "skip" "Not found at expected location"
fi
# Test 24: Running option removed
if [[ -f "$screen_locker" ]]; then
# Check that there's no "Running" button in ask_workout_type
if grep -A 20 "def ask_workout_type" "$screen_locker" | grep -q '"Running"'; then
test_result "Running workout option removed" "fail" "Still present in ask_workout_type"
else
test_result "Running workout option removed" "pass"
fi
else
test_result "Running workout option removed" "skip" "Script not found"
fi
# Test 25: Table tennis minimum sets validation
if [[ -f "$screen_locker" ]]; then
if grep -q "MIN_TABLE_TENNIS_SETS" "$screen_locker"; then
test_result "Table tennis minimum sets validation exists" "pass"
else
test_result "Table tennis minimum sets validation exists" "fail" "Constant not found"
fi
else
test_result "Table tennis minimum sets validation exists" "skip" "Script not found"
fi
# Test 26: Table tennis verification question
if [[ -f "$screen_locker" ]]; then
if grep -q "ask_table_tennis_verification" "$screen_locker"; then
test_result "Table tennis verification question exists" "pass"
else
test_result "Table tennis verification question exists" "fail" "Function not found"
fi
else
test_result "Table tennis verification question exists" "skip" "Script not found"
fi
# Test 27: 60 second submit delay for table tennis
if [[ -f "$screen_locker" ]]; then
if grep -q "TABLE_TENNIS_SUBMIT_DELAY = 60" "$screen_locker"; then
test_result "Table tennis 60-second submit delay" "pass"
else
test_result "Table tennis 60-second submit delay" "fail" "Constant not set to 60"
fi
else
test_result "Table tennis 60-second submit delay" "skip" "Script not found"
fi
echo ""
# ==================================================================
# SUMMARY
# ==================================================================
echo "=========================================="
echo "RESULTS SUMMARY"
echo "=========================================="
echo ""
echo -e "${GREEN}Passed: $PASS${NC}"
echo -e "${RED}Failed: $FAIL${NC}"
echo -e "${YELLOW}Skipped: $SKIP${NC}"
echo ""
echo "=========================================="
if [[ $FAIL -gt 0 ]]; then
echo -e "${RED}Some tests failed! Review the output above.${NC}"
exit 1
else
echo -e "${GREEN}All active tests passed!${NC}"
exit 0
fi