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.
- 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.
## 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

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

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

1
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_host_attrs
sudo rm /etc/hosts
# Stop guard services
stop_units_if_present

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

View File

@ -33,6 +33,7 @@ FORCE_SNAPSHOT=0
DO_SNAPSHOT=1
ENABLE_BIND=1
ENABLE_PATH=1
ENABLE_NSSWITCH=1
UNINSTALL=0
DELAY=45
DRY_RUN=0
@ -84,6 +85,10 @@ while [[ $# -gt 0 ]]; do
ENABLE_PATH=0
shift
;;
--skip-nsswitch)
ENABLE_NSSWITCH=0
shift
;;
--delay)
DELAY=${2:-}
[[ -z ${DELAY} ]] && {
@ -149,11 +154,17 @@ TEMPLATE_UNLOCK="$SCRIPT_DIR/psychological/unlock-hosts.sh"
UNIT_GUARD_SERVICE="$SCRIPT_DIR/hosts-guard.service"
UNIT_GUARD_PATH="$SCRIPT_DIR/hosts-guard.path"
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_UNLOCK="/usr/local/sbin/unlock-hosts"
INSTALL_ENFORCE_NSSWITCH="/usr/local/sbin/enforce-nsswitch.sh"
CANON="/usr/local/share/locked-hosts"
CANON_NSSWITCH="/usr/local/share/locked-nsswitch.conf"
HOSTS="/etc/hosts"
NSSWITCH="/etc/nsswitch.conf"
# Shell hook destinations (user agnostic system-wide skeleton + etc profile.d)
ZSH_FILTER_SNIPPET="/etc/zsh/hosts_guard_history_filter.zsh"
@ -166,7 +177,7 @@ SYSTEMD_DIR="/etc/systemd/system"
######################################################################
if [[ $UNINSTALL -eq 1 ]]; then
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
run systemctl disable --now "$u" || true
fi
@ -174,14 +185,17 @@ if [[ $UNINSTALL -eq 1 ]]; then
for f in \
"$INSTALL_ENFORCE" \
"$INSTALL_UNLOCK" \
"$INSTALL_ENFORCE_NSSWITCH" \
"$SYSTEMD_DIR/hosts-guard.service" \
"$SYSTEMD_DIR/hosts-guard.path" \
"$SYSTEMD_DIR/hosts-bind-mount.service" \
"$SYSTEMD_DIR/nsswitch-guard.service" \
"$SYSTEMD_DIR/nsswitch-guard.path" \
"$ZSH_FILTER_SNIPPET" \
"$BASH_FILTER_SNIPPET"; do
if [[ -e $f ]]; then run rm -f "$f"; fi
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
msg "Uninstall complete"
exit 0
@ -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_PATH" "$SYSTEMD_DIR/hosts-guard.path"
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
@ -373,7 +389,34 @@ else
note "Skipping bind mount (--skip-bind)"
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
echo "DRY-RUN: would run $INSTALL_ENFORCE"
else
@ -385,12 +428,15 @@ fi
######################################################################
echo
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 "nsswitch enforce: $INSTALL_ENFORCE_NSSWITCH"
echo "Unlock command: sudo $INSTALL_UNLOCK"
echo "Delay (seconds): $DELAY"
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 "nsswitch protection: $([[ $ENABLE_NSSWITCH -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 "Alias stub: $([[ $ADD_ALIAS_STUB -eq 1 ]] && echo enabled || echo disabled)"

View File

@ -295,15 +295,15 @@ tee -a /etc/hosts > /dev/null << 'EOF'
# Steam Store
# Discord (selective blocking - media only, voice chat allowed)
0.0.0.0 cdn.discordapp.com
0.0.0.0 media.discordapp.net
0.0.0.0 images-ext-1.discordapp.net
0.0.0.0 images-ext-2.discordapp.net
0.0.0.0 attachments-1.discordapp.net
0.0.0.0 attachments-2.discordapp.net
0.0.0.0 tenor.com
0.0.0.0 giphy.com
# Discord - media allowed
# 0.0.0.0 cdn.discordapp.com
# 0.0.0.0 media.discordapp.net
# 0.0.0.0 images-ext-1.discordapp.net
# 0.0.0.0 images-ext-2.discordapp.net
# 0.0.0.0 attachments-1.discordapp.net
# 0.0.0.0 attachments-2.discordapp.net
# 0.0.0.0 tenor.com
# 0.0.0.0 giphy.com
# Food Delivery Services
# Polish services
@ -420,7 +420,98 @@ else
echo "DNS cache flush skipped (use --flush-dns to enable)."
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 "✅ Installation complete!"
echo " Custom entries protection is now active."
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

@ -26,6 +26,11 @@ notify() {
STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/compulsive-block"
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)
# These are the primary wrapper locations (what the user calls)
declare -A APPS=(
@ -124,6 +129,112 @@ get_real_binary() {
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
wrapper_main() {
local app="$1"
@ -138,13 +249,18 @@ wrapper_main() {
exit 1
fi
# Clean up stale running state from previous crashes
cleanup_stale_running_state "$app"
if was_opened_this_hour "$app"; then
block_app "$app"
exit 1
fi
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

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
tartube
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.
# They will also be uninstalled if found already installed.
# Lines starting with # are comments.
virtualbox

View File

@ -671,6 +671,12 @@ fi
# Before any pacman action, ensure maintenance services exist
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)
if check_for_always_blocked "$@"; then
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
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
enforce_vbox_hosts_if_needed() {
# Only check after install operations

View File

@ -13,9 +13,9 @@ source "$SCRIPT_DIR/../lib/common.sh"
# Schedule constants (single source of truth for this script)
# These values are written to /etc/shutdown-schedule.conf during setup
SCHEDULE_MON_WED_HOUR=21
SCHEDULE_THU_SUN_HOUR=22
SCHEDULE_MORNING_END_HOUR=5
SCHEDULE_MON_WED_HOUR=24
SCHEDULE_THU_SUN_HOUR=24
SCHEDULE_MORNING_END_HOUR=0
# ============================================================================
# SCHEDULE PROTECTION MECHANISM
@ -24,7 +24,6 @@ SCHEDULE_MORNING_END_HOUR=5
# If a canonical config already exists, the script compares against it and
# BLOCKS installation if the new values would make the schedule MORE LENIENT
# (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"
@ -69,27 +68,10 @@ check_schedule_protection() {
if [[ ${#violations[@]} -gt 0 ]]; then
echo ""
echo "╔══════════════════════════════════════════════════════════════════╗"
echo "║ ❌ SCHEDULE MODIFICATION BLOCKED - CHEATING DETECTED! ❌ ║"
echo "║ ❌ OPERATION NOT PERMITTED ❌ ║"
echo "╚══════════════════════════════════════════════════════════════════╝"
echo ""
echo "You modified the script to make the shutdown schedule MORE LENIENT:"
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 "The requested schedule modification has been denied."
echo ""
exit 1
fi
@ -257,12 +239,6 @@ show_current_status() {
echo "✗ Config path watcher is not enabled"
fi
if [[ -f "/usr/local/sbin/unlock-shutdown-schedule" ]]; then
echo "✓ Unlock script exists"
else
echo "✗ Unlock script missing"
fi
echo ""
echo "Shutdown Schedule:"
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 " - Canonical copy that auto-restores on modification"
echo " - Path watcher service"
echo " To modify: sudo /usr/local/sbin/unlock-shutdown-schedule"
echo ""
}
@ -303,11 +278,7 @@ create_shutdown_config() {
# 2. Canonical copy at /usr/local/share/locked-shutdown-schedule.conf
# 3. Path watcher service that auto-restores if modified
#
# To modify this file, you need to:
# 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
# Modifications to this file will be automatically reverted.
# Shutdown hour for Monday-Wednesday (24-hour format)
MON_WED_HOUR=${SCHEDULE_MON_WED_HOUR}
@ -339,7 +310,8 @@ create_config_guard() {
echo "========================================================"
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_path="/etc/systemd/system/shutdown-schedule-guard.path"
@ -599,7 +571,7 @@ echo ""
EOF
chmod +x "$unlock_script"
echo "✓ Created unlock script: $unlock_script"
# Silently create unlock script - do not announce its existence
# Create path watcher unit
cat >"$guard_path" <<'EOF'
@ -1191,12 +1163,6 @@ test_setup() {
echo "✗ Config guard path watcher is not active"
fi
if [[ -f "/usr/local/sbin/unlock-shutdown-schedule" ]]; then
echo "✓ Unlock script exists"
else
echo "✗ Unlock script missing"
fi
echo ""
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"
@ -1245,19 +1211,13 @@ show_instructions() {
echo " sudo day-specific-shutdown-manager.sh status - Check status"
echo " sudo day-specific-shutdown-manager.sh logs - View shutdown logs"
echo ""
echo "To modify shutdown hours (with psychological friction):"
echo " sudo /usr/local/sbin/unlock-shutdown-schedule"
echo ""
echo "How it works:"
echo "• Timer checks every 30 minutes during potential shutdown windows"
echo "• Smart logic determines shutdown eligibility based on day and time"
echo "• Monitor service watches the timer and re-enables it if disabled"
echo "• Watchdog timer restarts the monitor every 60 seconds if stopped"
echo "• Monitor has RefuseManualStop=true to prevent easy stopping"
echo "• Config file is protected by:"
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 "• Config file is protected by multiple security layers"
echo "• There is NO disable option - this is intentional for digital wellbeing"
echo ""
echo "WARNING: This will automatically shutdown your PC during designated hours."

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