mirror of
https://github.com/kuhyx/scripts.git
synced 2026-07-04 11:43:03 +02:00
feat: hardened scripts
This commit is contained in:
parent
3b71cf33ee
commit
16e5a47321
20
.github/copilot-instructions.md
vendored
20
.github/copilot-instructions.md
vendored
@ -41,3 +41,23 @@ This repo automates Linux desktop bootstrap, hardening, and i3 setup. It’s pri
|
|||||||
- Add new periodic behaviors as templates under `scripts/system-maintenance/bin` and `.../systemd`, then extend `setup_periodic_system.sh` to install/enable them.
|
- Add new periodic behaviors as templates under `scripts/system-maintenance/bin` and `.../systemd`, then extend `setup_periodic_system.sh` to install/enable them.
|
||||||
- Extend package policy by updating `scripts/digital_wellbeing/pacman/pacman_blocked_keywords.txt` or by adding `check_for_<pkg>` + `prompt_for_<pkg>_challenge` blocks in the wrapper.
|
- Extend package policy by updating `scripts/digital_wellbeing/pacman/pacman_blocked_keywords.txt` or by adding `check_for_<pkg>` + `prompt_for_<pkg>_challenge` blocks in the wrapper.
|
||||||
- Run `scripts/meta/shell_check.sh` to detect things to fix before committing.
|
- Run `scripts/meta/shell_check.sh` to detect things to fix before committing.
|
||||||
|
## Detailed LLM Documentation
|
||||||
|
|
||||||
|
For in-depth understanding of specific components, see these dedicated guides:
|
||||||
|
|
||||||
|
- **Hosts Guard**: [hosts/guard/README_FOR_LLM.md](../hosts/guard/README_FOR_LLM.md) - Protection layers, canonical copies, path watchers
|
||||||
|
- **Pacman Wrapper**: [scripts/digital_wellbeing/pacman/README_FOR_LLM.md](../scripts/digital_wellbeing/pacman/README_FOR_LLM.md) - Policy files, integrity checks, challenges
|
||||||
|
- **Midnight Shutdown**: [scripts/digital_wellbeing/README_MIDNIGHT_SHUTDOWN_LLM.md](../scripts/digital_wellbeing/README_MIDNIGHT_SHUTDOWN_LLM.md) - Schedule protection, timer system
|
||||||
|
- **Compulsive Block**: [scripts/digital_wellbeing/README_COMPULSIVE_BLOCK_LLM.md](../scripts/digital_wellbeing/README_COMPULSIVE_BLOCK_LLM.md) - App launch limiting
|
||||||
|
- **Security Analysis**: [docs/SECURITY_HARDENING_ANALYSIS.md](../docs/SECURITY_HARDENING_ANALYSIS.md) - Vulnerabilities and implementation roadmap
|
||||||
|
|
||||||
|
## Digital Wellbeing Components Summary
|
||||||
|
|
||||||
|
| Component | Purpose | Key Files |
|
||||||
|
|-----------|---------|-----------|
|
||||||
|
| Hosts Guard | Block websites via /etc/hosts | `hosts/install.sh`, `hosts/guard/*` |
|
||||||
|
| Pacman Wrapper | Block package installation | `scripts/digital_wellbeing/pacman/*` |
|
||||||
|
| Midnight Shutdown | Auto-shutdown at night | `scripts/digital_wellbeing/setup_midnight_shutdown.sh` |
|
||||||
|
| Compulsive Block | Limit app launches | `scripts/digital_wellbeing/block_compulsive_opening.sh` |
|
||||||
|
| Music Wrapper | Block music during focus | `scripts/digital_wellbeing/youtube-music-wrapper.sh` |
|
||||||
|
| Screen Locker | Require workout to unlock | External: `~/testsAndMisc/python_pkg/screen_locker/` |
|
||||||
696
docs/SECURITY_HARDENING_ANALYSIS.md
Normal file
696
docs/SECURITY_HARDENING_ANALYSIS.md
Normal 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.
|
||||||
205
hosts/guard/README_FOR_LLM.md
Normal file
205
hosts/guard/README_FOR_LLM.md
Normal 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
|
||||||
103
hosts/guard/enforce-nsswitch.sh
Normal file
103
hosts/guard/enforce-nsswitch.sh
Normal 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"
|
||||||
9
hosts/guard/nsswitch-guard.path
Normal file
9
hosts/guard/nsswitch-guard.path
Normal 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
|
||||||
12
hosts/guard/nsswitch-guard.service
Normal file
12
hosts/guard/nsswitch-guard.service
Normal 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
|
||||||
98
hosts/guard/pacman-hooks/hosts-guard-common.sh
Normal file → Executable file
98
hosts/guard/pacman-hooks/hosts-guard-common.sh
Normal file → Executable file
@ -7,85 +7,85 @@ LOGTAG=hosts-guard-hook
|
|||||||
|
|
||||||
# Check if target has a read-only mount
|
# Check if target has a read-only mount
|
||||||
is_ro_mount() {
|
is_ro_mount() {
|
||||||
findmnt -no OPTIONS -T "$TARGET" 2> /dev/null | grep -qw ro
|
findmnt -no OPTIONS -T "$TARGET" 2>/dev/null | grep -qw ro
|
||||||
}
|
}
|
||||||
|
|
||||||
# Count mount layers for the target
|
# Count mount layers for the target
|
||||||
mount_layers_count() {
|
mount_layers_count() {
|
||||||
awk '$5=="/etc/hosts"{c++} END{print c+0}' /proc/self/mountinfo 2> /dev/null || echo 0
|
awk '$5=="/etc/hosts"{c++} END{print c+0}' /proc/self/mountinfo 2>/dev/null || echo 0
|
||||||
}
|
}
|
||||||
|
|
||||||
# Collapse all bind mount layers
|
# Collapse all bind mount layers
|
||||||
collapse_mounts() {
|
collapse_mounts() {
|
||||||
local i=0
|
local i=0
|
||||||
if command -v mountpoint > /dev/null 2>&1; then
|
if command -v mountpoint >/dev/null 2>&1; then
|
||||||
while mountpoint -q "$TARGET"; do
|
while mountpoint -q "$TARGET"; do
|
||||||
umount -l "$TARGET" > /dev/null 2>&1 || break
|
umount -l "$TARGET" >/dev/null 2>&1 || break
|
||||||
i=$((i + 1))
|
i=$((i + 1))
|
||||||
((i > 20)) && break
|
((i > 20)) && break
|
||||||
done
|
done
|
||||||
else
|
else
|
||||||
local cnt
|
local cnt
|
||||||
cnt=$(mount_layers_count)
|
cnt=$(mount_layers_count)
|
||||||
while ((cnt > 1)); do
|
while ((cnt > 1)); do
|
||||||
umount -l "$TARGET" > /dev/null 2>&1 || break
|
umount -l "$TARGET" >/dev/null 2>&1 || break
|
||||||
i=$((i + 1))
|
i=$((i + 1))
|
||||||
((i > 20)) && break
|
((i > 20)) && break
|
||||||
cnt=$(mount_layers_count)
|
cnt=$(mount_layers_count)
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# Stop systemd units related to hosts guard
|
# Stop systemd units related to hosts guard
|
||||||
stop_units_if_present() {
|
stop_units_if_present() {
|
||||||
local units=(hosts-bind-mount.service hosts-guard.path)
|
local units=(hosts-bind-mount.service hosts-guard.path)
|
||||||
for u in "${units[@]}"; do
|
for u in "${units[@]}"; do
|
||||||
if command -v systemctl > /dev/null 2>&1; then
|
if command -v systemctl >/dev/null 2>&1; then
|
||||||
if systemctl list-unit-files 2> /dev/null | grep -q "^$u"; then
|
if systemctl list-unit-files 2>/dev/null | grep -q "^$u"; then
|
||||||
systemctl stop "$u" > /dev/null 2>&1 || true
|
systemctl stop "$u" >/dev/null 2>&1 || true
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
# Remove immutable/append-only attributes
|
# Remove immutable/append-only attributes
|
||||||
remove_host_attrs() {
|
remove_host_attrs() {
|
||||||
if command -v lsattr > /dev/null 2>&1; then
|
if command -v lsattr >/dev/null 2>&1; then
|
||||||
local attrs
|
local attrs
|
||||||
attrs=$(lsattr -d "$TARGET" 2> /dev/null || true)
|
attrs=$(lsattr -d "$TARGET" 2>/dev/null || true)
|
||||||
if echo "$attrs" | grep -q " i "; then
|
if echo "$attrs" | grep -q " i "; then
|
||||||
chattr -i "$TARGET" > /dev/null 2>&1 || true
|
chattr -i "$TARGET" >/dev/null 2>&1 || true
|
||||||
fi
|
fi
|
||||||
if echo "$attrs" | grep -q " a "; then
|
if echo "$attrs" | grep -q " a "; then
|
||||||
chattr -a "$TARGET" > /dev/null 2>&1 || true
|
chattr -a "$TARGET" >/dev/null 2>&1 || true
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# Apply immutable attribute
|
# Apply immutable attribute
|
||||||
apply_immutable() {
|
apply_immutable() {
|
||||||
if command -v chattr > /dev/null 2>&1; then
|
if command -v chattr >/dev/null 2>&1; then
|
||||||
chattr +i "$TARGET" > /dev/null 2>&1 || true
|
chattr +i "$TARGET" >/dev/null 2>&1 || true
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# Apply a single read-only bind mount layer
|
# Apply a single read-only bind mount layer
|
||||||
apply_ro_bind_mount() {
|
apply_ro_bind_mount() {
|
||||||
mount --bind "$TARGET" "$TARGET" > /dev/null 2>&1 || true
|
mount --bind "$TARGET" "$TARGET" >/dev/null 2>&1 || true
|
||||||
mount -o remount,ro,bind "$TARGET" > /dev/null 2>&1 || true
|
mount -o remount,ro,bind "$TARGET" >/dev/null 2>&1 || true
|
||||||
}
|
}
|
||||||
|
|
||||||
# Start the path watcher service
|
# Start the path watcher service
|
||||||
start_path_watcher() {
|
start_path_watcher() {
|
||||||
if command -v systemctl > /dev/null 2>&1; then
|
if command -v systemctl >/dev/null 2>&1; then
|
||||||
systemctl start hosts-guard.path > /dev/null 2>&1 || true
|
systemctl start hosts-guard.path >/dev/null 2>&1 || true
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# Log to system logger and run log file
|
# Log to system logger and run log file
|
||||||
log_hook() {
|
log_hook() {
|
||||||
local phase="$1"
|
local phase="$1"
|
||||||
local state="$2"
|
local state="$2"
|
||||||
logger -t "$LOGTAG" "$phase: $state"
|
logger -t "$LOGTAG" "$phase: $state"
|
||||||
echo "$(date -Is) $phase-$state" >> /run/hosts-guard-hook.log 2> /dev/null || true
|
echo "$(date -Is) $phase-$state" >>/run/hosts-guard-hook.log 2>/dev/null || true
|
||||||
}
|
}
|
||||||
|
|||||||
2
hosts/guard/pacman-hooks/pacman-post-relock-hosts.sh
Normal file → Executable file
2
hosts/guard/pacman-hooks/pacman-post-relock-hosts.sh
Normal file → Executable file
@ -16,7 +16,7 @@ collapse_mounts
|
|||||||
|
|
||||||
# Run enforcement script if available
|
# Run enforcement script if available
|
||||||
if [[ -x $ENFORCE ]]; then
|
if [[ -x $ENFORCE ]]; then
|
||||||
"$ENFORCE" > /dev/null 2>&1 || true
|
"$ENFORCE" >/dev/null 2>&1 || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Apply protections
|
# Apply protections
|
||||||
|
|||||||
3
hosts/guard/pacman-hooks/pacman-pre-unlock-hosts.sh
Normal file → Executable file
3
hosts/guard/pacman-hooks/pacman-pre-unlock-hosts.sh
Normal file → Executable file
@ -9,6 +9,7 @@ source "$SCRIPT_DIR/hosts-guard-common.sh"
|
|||||||
|
|
||||||
# Remove protective attributes
|
# Remove protective attributes
|
||||||
remove_host_attrs
|
remove_host_attrs
|
||||||
|
sudo rm /etc/hosts
|
||||||
|
|
||||||
# Stop guard services
|
# Stop guard services
|
||||||
stop_units_if_present
|
stop_units_if_present
|
||||||
@ -20,7 +21,7 @@ collapse_mounts
|
|||||||
|
|
||||||
# Ensure writable by remounting if still read-only
|
# Ensure writable by remounting if still read-only
|
||||||
if is_ro_mount; then
|
if is_ro_mount; then
|
||||||
mount -o remount,rw "$TARGET" > /dev/null 2>&1 || collapse_mounts
|
mount -o remount,rw "$TARGET" >/dev/null 2>&1 || collapse_mounts
|
||||||
fi
|
fi
|
||||||
|
|
||||||
log_hook "pre" "unlocking(done)"
|
log_hook "pre" "unlocking(done)"
|
||||||
|
|||||||
34
hosts/guard/psychological/unlock-hosts.sh
Normal file → Executable file
34
hosts/guard/psychological/unlock-hosts.sh
Normal file → Executable file
@ -17,30 +17,30 @@ require_root "$@"
|
|||||||
echo "Reason for editing /etc/hosts (will be logged):" >&2
|
echo "Reason for editing /etc/hosts (will be logged):" >&2
|
||||||
read -r -p "Enter reason: " REASON
|
read -r -p "Enter reason: " REASON
|
||||||
if [[ -z ${REASON// /} ]]; then
|
if [[ -z ${REASON// /} ]]; then
|
||||||
echo "Empty reason not allowed. Aborting." >&2
|
echo "Empty reason not allowed. Aborting." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
log "Requested intentional /etc/hosts modification session. Reason: $REASON"
|
log "Requested intentional /etc/hosts modification session. Reason: $REASON"
|
||||||
logger -t "$SYSLOG_TAG" "session_start user=${SUDO_USER:-$USER} reason='$REASON'"
|
logger -t "$SYSLOG_TAG" "session_start user=${SUDO_USER:-$USER} reason='$REASON'"
|
||||||
echo "This action is logged. A cooling-off delay of $DELAY_SECONDS seconds applies." >&2
|
echo "This action is logged. A cooling-off delay of $DELAY_SECONDS seconds applies." >&2
|
||||||
|
|
||||||
for s in hosts-bind-mount.service hosts-guard.path; do
|
for s in hosts-bind-mount.service hosts-guard.path; do
|
||||||
if systemctl is-active --quiet "$s"; then
|
if systemctl is-active --quiet "$s"; then
|
||||||
log "Stopping $s"
|
log "Stopping $s"
|
||||||
systemctl stop "$s" || true
|
systemctl stop "$s" || true
|
||||||
fi
|
fi
|
||||||
if systemctl is-enabled --quiet "$s"; then
|
if systemctl is-enabled --quiet "$s"; then
|
||||||
log "(Will re-enable later)"
|
log "(Will re-enable later)"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
# Remove attributes to allow edit
|
# Remove attributes to allow edit
|
||||||
chattr -i -a "$TARGET" 2> /dev/null || true
|
chattr -i -a "$TARGET" 2>/dev/null || true
|
||||||
|
|
||||||
echo "Countdown:" >&2
|
echo "Countdown:" >&2
|
||||||
for ((i = DELAY_SECONDS; i > 0; i--)); do
|
for ((i = DELAY_SECONDS; i > 0; i--)); do
|
||||||
printf '\rEdit window opens in %2d seconds... Press Ctrl+C to abort.' "$i" >&2
|
printf '\rEdit window opens in %2d seconds... Press Ctrl+C to abort.' "$i" >&2
|
||||||
sleep 1
|
sleep 1
|
||||||
done
|
done
|
||||||
echo >&2
|
echo >&2
|
||||||
|
|
||||||
@ -50,12 +50,12 @@ sha_before=$(sha256sum "$TARGET" | awk '{print $1}')
|
|||||||
sha_after=$(sha256sum "$TARGET" | awk '{print $1}')
|
sha_after=$(sha256sum "$TARGET" | awk '{print $1}')
|
||||||
|
|
||||||
if [[ $sha_before == "$sha_after" ]]; then
|
if [[ $sha_before == "$sha_after" ]]; then
|
||||||
log "No changes made to $TARGET. Reason: $REASON"
|
log "No changes made to $TARGET. Reason: $REASON"
|
||||||
logger -t "$SYSLOG_TAG" "no_change user=${SUDO_USER:-$USER} reason='$REASON'"
|
logger -t "$SYSLOG_TAG" "no_change user=${SUDO_USER:-$USER} reason='$REASON'"
|
||||||
else
|
else
|
||||||
log "Changes detected. Updating canonical copy and re-enforcing. Reason: $REASON"
|
log "Changes detected. Updating canonical copy and re-enforcing. Reason: $REASON"
|
||||||
logger -t "$SYSLOG_TAG" "modified user=${SUDO_USER:-$USER} reason='$REASON'"
|
logger -t "$SYSLOG_TAG" "modified user=${SUDO_USER:-$USER} reason='$REASON'"
|
||||||
cp "$TARGET" "$CANON"
|
cp "$TARGET" "$CANON"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Re-run enforcement
|
# Re-run enforcement
|
||||||
|
|||||||
@ -33,6 +33,7 @@ FORCE_SNAPSHOT=0
|
|||||||
DO_SNAPSHOT=1
|
DO_SNAPSHOT=1
|
||||||
ENABLE_BIND=1
|
ENABLE_BIND=1
|
||||||
ENABLE_PATH=1
|
ENABLE_PATH=1
|
||||||
|
ENABLE_NSSWITCH=1
|
||||||
UNINSTALL=0
|
UNINSTALL=0
|
||||||
DELAY=45
|
DELAY=45
|
||||||
DRY_RUN=0
|
DRY_RUN=0
|
||||||
@ -48,15 +49,15 @@ note() { printf '\e[1;34m[i]\e[0m %s\n' "$*"; }
|
|||||||
warn() { printf '\e[1;33m[!]\e[0m %s\n' "$*"; }
|
warn() { printf '\e[1;33m[!]\e[0m %s\n' "$*"; }
|
||||||
err() { printf '\e[1;31m[x]\e[0m %s\n' "$*" >&2; }
|
err() { printf '\e[1;31m[x]\e[0m %s\n' "$*" >&2; }
|
||||||
run() {
|
run() {
|
||||||
if [[ $DRY_RUN -eq 1 ]]; then
|
if [[ $DRY_RUN -eq 1 ]]; then
|
||||||
printf 'DRY-RUN:'
|
printf 'DRY-RUN:'
|
||||||
if [ "$#" -gt 0 ]; then
|
if [ "$#" -gt 0 ]; then
|
||||||
printf ' %q' "$@"
|
printf ' %q' "$@"
|
||||||
fi
|
fi
|
||||||
printf '\n'
|
printf '\n'
|
||||||
else
|
else
|
||||||
"$@"
|
"$@"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
require_root() { if [[ $EUID -ne 0 ]]; then exec sudo -E bash "$0" "$@"; fi; }
|
require_root() { if [[ $EUID -ne 0 ]]; then exec sudo -E bash "$0" "$@"; fi; }
|
||||||
@ -67,73 +68,77 @@ usage() { sed -n '1,/^set -euo pipefail/p' "$0" | sed 's/^# \{0,1\}//'; }
|
|||||||
# Parse args
|
# Parse args
|
||||||
######################################################################
|
######################################################################
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
--force-snapshot)
|
--force-snapshot)
|
||||||
FORCE_SNAPSHOT=1
|
FORCE_SNAPSHOT=1
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
--no-snapshot)
|
--no-snapshot)
|
||||||
DO_SNAPSHOT=0
|
DO_SNAPSHOT=0
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
--skip-bind)
|
--skip-bind)
|
||||||
ENABLE_BIND=0
|
ENABLE_BIND=0
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
--skip-path-watch)
|
--skip-path-watch)
|
||||||
ENABLE_PATH=0
|
ENABLE_PATH=0
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
--delay)
|
--skip-nsswitch)
|
||||||
DELAY=${2:-}
|
ENABLE_NSSWITCH=0
|
||||||
[[ -z ${DELAY} ]] && {
|
shift
|
||||||
err '--delay requires value'
|
;;
|
||||||
exit 2
|
--delay)
|
||||||
}
|
DELAY=${2:-}
|
||||||
shift 2
|
[[ -z ${DELAY} ]] && {
|
||||||
;;
|
err '--delay requires value'
|
||||||
--dry-run)
|
exit 2
|
||||||
DRY_RUN=1
|
}
|
||||||
shift
|
shift 2
|
||||||
;;
|
;;
|
||||||
--no-shell-hooks)
|
--dry-run)
|
||||||
INSTALL_SHELL_HOOKS=0
|
DRY_RUN=1
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
--shell-hooks)
|
--no-shell-hooks)
|
||||||
INSTALL_SHELL_HOOKS=1
|
INSTALL_SHELL_HOOKS=0
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
--no-audit)
|
--shell-hooks)
|
||||||
INSTALL_AUDIT_RULE=0
|
INSTALL_SHELL_HOOKS=1
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
--audit)
|
--no-audit)
|
||||||
INSTALL_AUDIT_RULE=1
|
INSTALL_AUDIT_RULE=0
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
--no-alias-stub)
|
--audit)
|
||||||
ADD_ALIAS_STUB=0
|
INSTALL_AUDIT_RULE=1
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
--alias-stub)
|
--no-alias-stub)
|
||||||
ADD_ALIAS_STUB=1
|
ADD_ALIAS_STUB=0
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
--uninstall)
|
--alias-stub)
|
||||||
UNINSTALL=1
|
ADD_ALIAS_STUB=1
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
-h | --help)
|
--uninstall)
|
||||||
usage
|
UNINSTALL=1
|
||||||
exit 0
|
shift
|
||||||
;;
|
;;
|
||||||
*)
|
-h | --help)
|
||||||
err "Unknown argument: $1"
|
usage
|
||||||
usage
|
exit 0
|
||||||
exit 2
|
;;
|
||||||
;;
|
*)
|
||||||
esac
|
err "Unknown argument: $1"
|
||||||
|
usage
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
require_root "$@"
|
require_root "$@"
|
||||||
@ -149,11 +154,17 @@ TEMPLATE_UNLOCK="$SCRIPT_DIR/psychological/unlock-hosts.sh"
|
|||||||
UNIT_GUARD_SERVICE="$SCRIPT_DIR/hosts-guard.service"
|
UNIT_GUARD_SERVICE="$SCRIPT_DIR/hosts-guard.service"
|
||||||
UNIT_GUARD_PATH="$SCRIPT_DIR/hosts-guard.path"
|
UNIT_GUARD_PATH="$SCRIPT_DIR/hosts-guard.path"
|
||||||
UNIT_BIND_SERVICE="$SCRIPT_DIR/hosts-bind-mount.service"
|
UNIT_BIND_SERVICE="$SCRIPT_DIR/hosts-bind-mount.service"
|
||||||
|
TEMPLATE_ENFORCE_NSSWITCH="$SCRIPT_DIR/enforce-nsswitch.sh"
|
||||||
|
UNIT_NSSWITCH_SERVICE="$SCRIPT_DIR/nsswitch-guard.service"
|
||||||
|
UNIT_NSSWITCH_PATH="$SCRIPT_DIR/nsswitch-guard.path"
|
||||||
|
|
||||||
INSTALL_ENFORCE="/usr/local/sbin/enforce-hosts.sh"
|
INSTALL_ENFORCE="/usr/local/sbin/enforce-hosts.sh"
|
||||||
INSTALL_UNLOCK="/usr/local/sbin/unlock-hosts"
|
INSTALL_UNLOCK="/usr/local/sbin/unlock-hosts"
|
||||||
|
INSTALL_ENFORCE_NSSWITCH="/usr/local/sbin/enforce-nsswitch.sh"
|
||||||
CANON="/usr/local/share/locked-hosts"
|
CANON="/usr/local/share/locked-hosts"
|
||||||
|
CANON_NSSWITCH="/usr/local/share/locked-nsswitch.conf"
|
||||||
HOSTS="/etc/hosts"
|
HOSTS="/etc/hosts"
|
||||||
|
NSSWITCH="/etc/nsswitch.conf"
|
||||||
|
|
||||||
# Shell hook destinations (user agnostic system-wide skeleton + etc profile.d)
|
# Shell hook destinations (user agnostic system-wide skeleton + etc profile.d)
|
||||||
ZSH_FILTER_SNIPPET="/etc/zsh/hosts_guard_history_filter.zsh"
|
ZSH_FILTER_SNIPPET="/etc/zsh/hosts_guard_history_filter.zsh"
|
||||||
@ -165,26 +176,29 @@ SYSTEMD_DIR="/etc/systemd/system"
|
|||||||
# Uninstall flow
|
# Uninstall flow
|
||||||
######################################################################
|
######################################################################
|
||||||
if [[ $UNINSTALL -eq 1 ]]; then
|
if [[ $UNINSTALL -eq 1 ]]; then
|
||||||
note "Uninstalling hosts guard components ( protections removed )"
|
note "Uninstalling hosts guard components ( protections removed )"
|
||||||
for u in hosts-guard.path hosts-guard.service hosts-bind-mount.service; do
|
for u in hosts-guard.path hosts-guard.service hosts-bind-mount.service nsswitch-guard.path nsswitch-guard.service; do
|
||||||
if systemctl list-unit-files | grep -q "^$u"; then
|
if systemctl list-unit-files | grep -q "^$u"; then
|
||||||
run systemctl disable --now "$u" || true
|
run systemctl disable --now "$u" || true
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
for f in \
|
for f in \
|
||||||
"$INSTALL_ENFORCE" \
|
"$INSTALL_ENFORCE" \
|
||||||
"$INSTALL_UNLOCK" \
|
"$INSTALL_UNLOCK" \
|
||||||
"$SYSTEMD_DIR/hosts-guard.service" \
|
"$INSTALL_ENFORCE_NSSWITCH" \
|
||||||
"$SYSTEMD_DIR/hosts-guard.path" \
|
"$SYSTEMD_DIR/hosts-guard.service" \
|
||||||
"$SYSTEMD_DIR/hosts-bind-mount.service" \
|
"$SYSTEMD_DIR/hosts-guard.path" \
|
||||||
"$ZSH_FILTER_SNIPPET" \
|
"$SYSTEMD_DIR/hosts-bind-mount.service" \
|
||||||
"$BASH_FILTER_SNIPPET"; do
|
"$SYSTEMD_DIR/nsswitch-guard.service" \
|
||||||
if [[ -e $f ]]; then run rm -f "$f"; fi
|
"$SYSTEMD_DIR/nsswitch-guard.path" \
|
||||||
done
|
"$ZSH_FILTER_SNIPPET" \
|
||||||
note "Leaving canonical snapshot at $CANON (remove manually if undesired)."
|
"$BASH_FILTER_SNIPPET"; do
|
||||||
if [[ $DRY_RUN -eq 0 ]]; then systemctl daemon-reload; fi
|
if [[ -e $f ]]; then run rm -f "$f"; fi
|
||||||
msg "Uninstall complete"
|
done
|
||||||
exit 0
|
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
|
||||||
fi
|
fi
|
||||||
|
|
||||||
######################################################################
|
######################################################################
|
||||||
@ -194,29 +208,29 @@ note "Script directory: $SCRIPT_DIR"
|
|||||||
note "Repository root: $REPO_ROOT"
|
note "Repository root: $REPO_ROOT"
|
||||||
|
|
||||||
for req in "$TEMPLATE_ENFORCE" "$TEMPLATE_UNLOCK" "$UNIT_GUARD_SERVICE"; do
|
for req in "$TEMPLATE_ENFORCE" "$TEMPLATE_UNLOCK" "$UNIT_GUARD_SERVICE"; do
|
||||||
[[ -f $req ]] || {
|
[[ -f $req ]] || {
|
||||||
err "Missing template: $req"
|
err "Missing template: $req"
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
done
|
done
|
||||||
|
|
||||||
if [[ ! -f $HOSTS ]]; then
|
if [[ ! -f $HOSTS ]]; then
|
||||||
err "$HOSTS does not exist. Run your hosts/install.sh first."
|
err "$HOSTS does not exist. Run your hosts/install.sh first."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
######################################################################
|
######################################################################
|
||||||
# Snapshot
|
# Snapshot
|
||||||
######################################################################
|
######################################################################
|
||||||
if [[ $DO_SNAPSHOT -eq 1 ]]; then
|
if [[ $DO_SNAPSHOT -eq 1 ]]; then
|
||||||
if [[ -f $CANON && $FORCE_SNAPSHOT -eq 0 ]]; then
|
if [[ -f $CANON && $FORCE_SNAPSHOT -eq 0 ]]; then
|
||||||
note "Canonical snapshot exists (use --force-snapshot to overwrite)"
|
note "Canonical snapshot exists (use --force-snapshot to overwrite)"
|
||||||
else
|
else
|
||||||
msg "Creating canonical snapshot at $CANON"
|
msg "Creating canonical snapshot at $CANON"
|
||||||
run install -m 644 -D "$HOSTS" "$CANON"
|
run install -m 644 -D "$HOSTS" "$CANON"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
note "Skipping snapshot creation (--no-snapshot)"
|
note "Skipping snapshot creation (--no-snapshot)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
######################################################################
|
######################################################################
|
||||||
@ -230,27 +244,27 @@ run install -m 755 "$TEMPLATE_UNLOCK" "$INSTALL_UNLOCK"
|
|||||||
|
|
||||||
# Adjust delay in unlock script if different from default
|
# Adjust delay in unlock script if different from default
|
||||||
if [[ $DELAY -ne 45 ]]; then
|
if [[ $DELAY -ne 45 ]]; then
|
||||||
msg "Adjusting unlock delay to $DELAY seconds"
|
msg "Adjusting unlock delay to $DELAY seconds"
|
||||||
if [[ $DRY_RUN -eq 1 ]]; then
|
if [[ $DRY_RUN -eq 1 ]]; then
|
||||||
echo "DRY-RUN: would patch $INSTALL_UNLOCK"
|
echo "DRY-RUN: would patch $INSTALL_UNLOCK"
|
||||||
else
|
else
|
||||||
# Replace DELAY_SECONDS=... line
|
# Replace DELAY_SECONDS=... line
|
||||||
sed -i -E "s/^(DELAY_SECONDS=).*/\\1$DELAY/" "$INSTALL_UNLOCK" || warn "Failed to adjust delay"
|
sed -i -E "s/^(DELAY_SECONDS=).*/\\1$DELAY/" "$INSTALL_UNLOCK" || warn "Failed to adjust delay"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
######################################################################
|
######################################################################
|
||||||
# Install shell history filters (optional)
|
# Install shell history filters (optional)
|
||||||
######################################################################
|
######################################################################
|
||||||
if [[ $INSTALL_SHELL_HOOKS -eq 1 ]]; then
|
if [[ $INSTALL_SHELL_HOOKS -eq 1 ]]; then
|
||||||
msg "Installing shell history suppression hooks for unlock command"
|
msg "Installing shell history suppression hooks for unlock command"
|
||||||
# Pattern matches commands invoking unlock-hosts (with or without sudo) & setup script force snapshot
|
# Pattern matches commands invoking unlock-hosts (with or without sudo) & setup script force snapshot
|
||||||
# Zsh: use zshaddhistory function
|
# Zsh: use zshaddhistory function
|
||||||
if command -v zsh > /dev/null 2>&1; then
|
if command -v zsh >/dev/null 2>&1; then
|
||||||
if [[ $DRY_RUN -eq 1 ]]; then
|
if [[ $DRY_RUN -eq 1 ]]; then
|
||||||
echo "DRY-RUN: would create $ZSH_FILTER_SNIPPET"
|
echo "DRY-RUN: would create $ZSH_FILTER_SNIPPET"
|
||||||
else
|
else
|
||||||
cat > "$ZSH_FILTER_SNIPPET" << 'ZEOF'
|
cat >"$ZSH_FILTER_SNIPPET" <<'ZEOF'
|
||||||
# Added by hosts guard setup – suppress unlock-hosts commands from Zsh history
|
# Added by hosts guard setup – suppress unlock-hosts commands from Zsh history
|
||||||
autoload -Uz add-zsh-hook 2>/dev/null || true
|
autoload -Uz add-zsh-hook 2>/dev/null || true
|
||||||
_hosts_guard_history_filter() {
|
_hosts_guard_history_filter() {
|
||||||
@ -269,16 +283,16 @@ else
|
|||||||
zshaddhistory() { _hosts_guard_history_filter "$1"; }
|
zshaddhistory() { _hosts_guard_history_filter "$1"; }
|
||||||
fi
|
fi
|
||||||
ZEOF
|
ZEOF
|
||||||
chmod 644 "$ZSH_FILTER_SNIPPET"
|
chmod 644 "$ZSH_FILTER_SNIPPET"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Bash: rely on HISTCONTROL and PROMPT_COMMAND filter
|
# Bash: rely on HISTCONTROL and PROMPT_COMMAND filter
|
||||||
if command -v bash > /dev/null 2>&1; then
|
if command -v bash >/dev/null 2>&1; then
|
||||||
if [[ $DRY_RUN -eq 1 ]]; then
|
if [[ $DRY_RUN -eq 1 ]]; then
|
||||||
echo "DRY-RUN: would create $BASH_FILTER_SNIPPET"
|
echo "DRY-RUN: would create $BASH_FILTER_SNIPPET"
|
||||||
else
|
else
|
||||||
cat > "$BASH_FILTER_SNIPPET" << 'BEOF'
|
cat >"$BASH_FILTER_SNIPPET" <<'BEOF'
|
||||||
# Added by hosts guard setup – suppress unlock-hosts commands from Bash history
|
# Added by hosts guard setup – suppress unlock-hosts commands from Bash history
|
||||||
export HISTCONTROL=ignoredups:erasedups
|
export HISTCONTROL=ignoredups:erasedups
|
||||||
_hosts_guard_hist_filter() {
|
_hosts_guard_hist_filter() {
|
||||||
@ -299,51 +313,51 @@ case :${PROMPT_COMMAND-}: in
|
|||||||
* ) PROMPT_COMMAND="_hosts_guard_hist_filter${PROMPT_COMMAND:+;${PROMPT_COMMAND}}" ;;
|
* ) PROMPT_COMMAND="_hosts_guard_hist_filter${PROMPT_COMMAND:+;${PROMPT_COMMAND}}" ;;
|
||||||
esac
|
esac
|
||||||
BEOF
|
BEOF
|
||||||
chmod 644 "$BASH_FILTER_SNIPPET"
|
chmod 644 "$BASH_FILTER_SNIPPET"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
note "Skipping shell history hooks (--no-shell-hooks)"
|
note "Skipping shell history hooks (--no-shell-hooks)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
######################################################################
|
######################################################################
|
||||||
# Add alias stub to discourage raw invocation (shell-level friction)
|
# Add alias stub to discourage raw invocation (shell-level friction)
|
||||||
######################################################################
|
######################################################################
|
||||||
if [[ $ADD_ALIAS_STUB -eq 1 ]]; then
|
if [[ $ADD_ALIAS_STUB -eq 1 ]]; then
|
||||||
PROFILE_STUB="/etc/profile.d/hosts_guard_alias_stub.sh"
|
PROFILE_STUB="/etc/profile.d/hosts_guard_alias_stub.sh"
|
||||||
if [[ $DRY_RUN -eq 1 ]]; then
|
if [[ $DRY_RUN -eq 1 ]]; then
|
||||||
echo "DRY-RUN: would create $PROFILE_STUB"
|
echo "DRY-RUN: would create $PROFILE_STUB"
|
||||||
else
|
else
|
||||||
cat > "$PROFILE_STUB" << 'ASTUB'
|
cat >"$PROFILE_STUB" <<'ASTUB'
|
||||||
# Added by hosts guard setup – discourages casual use of unlock-hosts name
|
# Added by hosts guard setup – discourages casual use of unlock-hosts name
|
||||||
if command -v unlock-hosts >/dev/null 2>&1; then
|
if command -v unlock-hosts >/dev/null 2>&1; then
|
||||||
alias unlock-hosts='command_not_found_handle 2>/dev/null || echo "Use: sudo /usr/local/sbin/unlock-hosts (logged & delayed)"'
|
alias unlock-hosts='command_not_found_handle 2>/dev/null || echo "Use: sudo /usr/local/sbin/unlock-hosts (logged & delayed)"'
|
||||||
fi
|
fi
|
||||||
ASTUB
|
ASTUB
|
||||||
chmod 644 "$PROFILE_STUB"
|
chmod 644 "$PROFILE_STUB"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
######################################################################
|
######################################################################
|
||||||
# Audit rule to record executions (requires auditd)
|
# Audit rule to record executions (requires auditd)
|
||||||
######################################################################
|
######################################################################
|
||||||
if [[ $INSTALL_AUDIT_RULE -eq 1 ]]; then
|
if [[ $INSTALL_AUDIT_RULE -eq 1 ]]; then
|
||||||
if command -v auditctl > /dev/null 2>&1; then
|
if command -v auditctl >/dev/null 2>&1; then
|
||||||
audit_rule_str="-w /usr/local/sbin/unlock-hosts -p x -k hosts_unlock"
|
audit_rule_str="-w /usr/local/sbin/unlock-hosts -p x -k hosts_unlock"
|
||||||
audit_rule_args=(-w /usr/local/sbin/unlock-hosts -p x -k hosts_unlock)
|
audit_rule_args=(-w /usr/local/sbin/unlock-hosts -p x -k hosts_unlock)
|
||||||
if auditctl -l 2> /dev/null | grep -Fq "/usr/local/sbin/unlock-hosts"; then
|
if auditctl -l 2>/dev/null | grep -Fq "/usr/local/sbin/unlock-hosts"; then
|
||||||
note "Audit rule already present"
|
note "Audit rule already present"
|
||||||
else
|
else
|
||||||
run auditctl "${audit_rule_args[@]}" || warn "Failed to add audit rule (runtime)"
|
run auditctl "${audit_rule_args[@]}" || warn "Failed to add audit rule (runtime)"
|
||||||
if [[ $DRY_RUN -eq 1 ]]; then
|
if [[ $DRY_RUN -eq 1 ]]; then
|
||||||
echo "DRY-RUN: would create /etc/audit/rules.d/hosts_unlock.rules"
|
echo "DRY-RUN: would create /etc/audit/rules.d/hosts_unlock.rules"
|
||||||
else
|
else
|
||||||
echo "$audit_rule_str" > /etc/audit/rules.d/hosts_unlock.rules
|
echo "$audit_rule_str" >/etc/audit/rules.d/hosts_unlock.rules
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
warn "auditctl not found; skipping audit rule (install auditd to enable)"
|
warn "auditctl not found; skipping audit rule (install auditd to enable)"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
######################################################################
|
######################################################################
|
||||||
@ -353,6 +367,8 @@ msg "Deploying systemd units"
|
|||||||
run install -m 644 "$UNIT_GUARD_SERVICE" "$SYSTEMD_DIR/hosts-guard.service"
|
run install -m 644 "$UNIT_GUARD_SERVICE" "$SYSTEMD_DIR/hosts-guard.service"
|
||||||
run install -m 644 "$UNIT_GUARD_PATH" "$SYSTEMD_DIR/hosts-guard.path"
|
run install -m 644 "$UNIT_GUARD_PATH" "$SYSTEMD_DIR/hosts-guard.path"
|
||||||
run install -m 644 "$UNIT_BIND_SERVICE" "$SYSTEMD_DIR/hosts-bind-mount.service"
|
run install -m 644 "$UNIT_BIND_SERVICE" "$SYSTEMD_DIR/hosts-bind-mount.service"
|
||||||
|
run install -m 644 "$UNIT_NSSWITCH_SERVICE" "$SYSTEMD_DIR/nsswitch-guard.service"
|
||||||
|
run install -m 644 "$UNIT_NSSWITCH_PATH" "$SYSTEMD_DIR/nsswitch-guard.path"
|
||||||
|
|
||||||
if [[ $DRY_RUN -eq 0 ]]; then systemctl daemon-reload; fi
|
if [[ $DRY_RUN -eq 0 ]]; then systemctl daemon-reload; fi
|
||||||
|
|
||||||
@ -360,24 +376,51 @@ if [[ $DRY_RUN -eq 0 ]]; then systemctl daemon-reload; fi
|
|||||||
# Enable / Start
|
# Enable / Start
|
||||||
######################################################################
|
######################################################################
|
||||||
if [[ $ENABLE_PATH -eq 1 ]]; then
|
if [[ $ENABLE_PATH -eq 1 ]]; then
|
||||||
msg "Enabling path watch (auto-revert)"
|
msg "Enabling path watch (auto-revert)"
|
||||||
run systemctl enable --now hosts-guard.path
|
run systemctl enable --now hosts-guard.path
|
||||||
else
|
else
|
||||||
note "Skipping path watch (--skip-path-watch)"
|
note "Skipping path watch (--skip-path-watch)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ $ENABLE_BIND -eq 1 ]]; then
|
if [[ $ENABLE_BIND -eq 1 ]]; then
|
||||||
msg "Enabling read-only bind mount"
|
msg "Enabling read-only bind mount"
|
||||||
run systemctl enable --now hosts-bind-mount.service
|
run systemctl enable --now hosts-bind-mount.service
|
||||||
else
|
else
|
||||||
note "Skipping bind mount (--skip-bind)"
|
note "Skipping bind mount (--skip-bind)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
msg "Performing initial enforcement"
|
if [[ $ENABLE_NSSWITCH -eq 1 ]]; then
|
||||||
if [[ $DRY_RUN -eq 1 ]]; then
|
msg "Enabling nsswitch.conf protection (hosts bypass prevention)"
|
||||||
echo "DRY-RUN: would run $INSTALL_ENFORCE"
|
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
|
else
|
||||||
"$INSTALL_ENFORCE" || warn "Enforcement returned non-zero"
|
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
|
||||||
|
"$INSTALL_ENFORCE" || warn "Enforcement returned non-zero"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
######################################################################
|
######################################################################
|
||||||
@ -385,12 +428,15 @@ fi
|
|||||||
######################################################################
|
######################################################################
|
||||||
echo
|
echo
|
||||||
msg "Hosts guard setup complete"
|
msg "Hosts guard setup complete"
|
||||||
echo "Canonical copy: $CANON"
|
echo "Canonical hosts copy: $CANON"
|
||||||
|
echo "Canonical nsswitch copy: $CANON_NSSWITCH"
|
||||||
echo "Enforce script: $INSTALL_ENFORCE"
|
echo "Enforce script: $INSTALL_ENFORCE"
|
||||||
|
echo "nsswitch enforce: $INSTALL_ENFORCE_NSSWITCH"
|
||||||
echo "Unlock command: sudo $INSTALL_UNLOCK"
|
echo "Unlock command: sudo $INSTALL_UNLOCK"
|
||||||
echo "Delay (seconds): $DELAY"
|
echo "Delay (seconds): $DELAY"
|
||||||
echo "Auto-revert path watch: $([[ $ENABLE_PATH -eq 1 ]] && echo enabled || echo disabled)"
|
echo "Auto-revert path watch: $([[ $ENABLE_PATH -eq 1 ]] && echo enabled || echo disabled)"
|
||||||
echo "Read-only bind mount: $([[ $ENABLE_BIND -eq 1 ]] && echo enabled || echo disabled)"
|
echo "Read-only bind mount: $([[ $ENABLE_BIND -eq 1 ]] && echo enabled || echo disabled)"
|
||||||
|
echo "nsswitch protection: $([[ $ENABLE_NSSWITCH -eq 1 ]] && echo enabled || echo disabled)"
|
||||||
echo "Shell history suppression: $([[ $INSTALL_SHELL_HOOKS -eq 1 ]] && echo enabled || echo disabled)"
|
echo "Shell history suppression: $([[ $INSTALL_SHELL_HOOKS -eq 1 ]] && echo enabled || echo disabled)"
|
||||||
echo "Audit rule: $([[ $INSTALL_AUDIT_RULE -eq 1 ]] && echo enabled || echo disabled)"
|
echo "Audit rule: $([[ $INSTALL_AUDIT_RULE -eq 1 ]] && echo enabled || echo disabled)"
|
||||||
echo "Alias stub: $([[ $ADD_ALIAS_STUB -eq 1 ]] && echo enabled || echo disabled)"
|
echo "Alias stub: $([[ $ADD_ALIAS_STUB -eq 1 ]] && echo enabled || echo disabled)"
|
||||||
|
|||||||
427
hosts/install.sh
427
hosts/install.sh
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
# Re-run with sudo if not root
|
# Re-run with sudo if not root
|
||||||
if [[ $EUID -ne 0 ]]; then
|
if [[ $EUID -ne 0 ]]; then
|
||||||
exec sudo -E bash "$0" "$@"
|
exec sudo -E bash "$0" "$@"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Options
|
# Options
|
||||||
@ -11,18 +11,18 @@ FLUSH_DNS=0
|
|||||||
|
|
||||||
# Parse CLI flags
|
# Parse CLI flags
|
||||||
for arg in "$@"; do
|
for arg in "$@"; do
|
||||||
case "$arg" in
|
case "$arg" in
|
||||||
--flush-dns)
|
--flush-dns)
|
||||||
FLUSH_DNS=1
|
FLUSH_DNS=1
|
||||||
;;
|
;;
|
||||||
--no-flush-dns)
|
--no-flush-dns)
|
||||||
FLUSH_DNS=0
|
FLUSH_DNS=0
|
||||||
;;
|
;;
|
||||||
-h | --help)
|
-h | --help)
|
||||||
echo "Usage: $0 [--flush-dns|--no-flush-dns]"
|
echo "Usage: $0 [--flush-dns|--no-flush-dns]"
|
||||||
exit 0
|
exit 0
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
@ -39,136 +39,136 @@ CUSTOM_ENTRIES_STATE_FILE="/etc/hosts.custom-entries.state"
|
|||||||
# Extract custom blocked entries from a hosts file or heredoc section
|
# Extract custom blocked entries from a hosts file or heredoc section
|
||||||
# Returns only the "0.0.0.0 domain.com" lines (normalized, sorted, unique)
|
# Returns only the "0.0.0.0 domain.com" lines (normalized, sorted, unique)
|
||||||
extract_custom_entries_from_script() {
|
extract_custom_entries_from_script() {
|
||||||
# Extract entries from the heredoc in this script (between EOF markers after "Custom blocking entries")
|
# Extract entries from the heredoc in this script (between EOF markers after "Custom blocking entries")
|
||||||
local script_path="$1"
|
local script_path="$1"
|
||||||
sed -n '/^# Custom blocking entries$/,/^EOF$/p' "$script_path" |
|
sed -n '/^# Custom blocking entries$/,/^EOF$/p' "$script_path" |
|
||||||
grep -E '^0\.0\.0\.0[[:space:]]+' |
|
grep -E '^0\.0\.0\.0[[:space:]]+' |
|
||||||
awk '{print $2}' |
|
awk '{print $2}' |
|
||||||
sort -u
|
sort -u
|
||||||
}
|
}
|
||||||
|
|
||||||
# Extract custom entries from the current /etc/hosts (entries after "# Custom blocking entries" marker)
|
# Extract custom entries from the current /etc/hosts (entries after "# Custom blocking entries" marker)
|
||||||
extract_custom_entries_from_hosts() {
|
extract_custom_entries_from_hosts() {
|
||||||
local hosts_file="$1"
|
local hosts_file="$1"
|
||||||
if [[ ! -f $hosts_file ]]; then
|
if [[ ! -f $hosts_file ]]; then
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
sed -n '/^# Custom blocking entries$/,$p' "$hosts_file" |
|
sed -n '/^# Custom blocking entries$/,$p' "$hosts_file" |
|
||||||
grep -E '^0\.0\.0\.0[[:space:]]+' |
|
grep -E '^0\.0\.0\.0[[:space:]]+' |
|
||||||
awk '{print $2}' |
|
awk '{print $2}' |
|
||||||
sort -u
|
sort -u
|
||||||
}
|
}
|
||||||
|
|
||||||
# Load previously saved custom entries state
|
# Load previously saved custom entries state
|
||||||
load_saved_custom_entries() {
|
load_saved_custom_entries() {
|
||||||
if [[ -f $CUSTOM_ENTRIES_STATE_FILE ]]; then
|
if [[ -f $CUSTOM_ENTRIES_STATE_FILE ]]; then
|
||||||
sort -u "$CUSTOM_ENTRIES_STATE_FILE"
|
sort -u "$CUSTOM_ENTRIES_STATE_FILE"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# Save current custom entries to state file
|
# Save current custom entries to state file
|
||||||
save_custom_entries_state() {
|
save_custom_entries_state() {
|
||||||
local entries="$1"
|
local entries="$1"
|
||||||
echo "$entries" | sort -u > "$CUSTOM_ENTRIES_STATE_FILE"
|
echo "$entries" | sort -u >"$CUSTOM_ENTRIES_STATE_FILE"
|
||||||
chmod 644 "$CUSTOM_ENTRIES_STATE_FILE"
|
chmod 644 "$CUSTOM_ENTRIES_STATE_FILE"
|
||||||
chattr +i "$CUSTOM_ENTRIES_STATE_FILE" 2> /dev/null || true
|
chattr +i "$CUSTOM_ENTRIES_STATE_FILE" 2>/dev/null || true
|
||||||
}
|
}
|
||||||
|
|
||||||
# Helper function to count non-empty lines
|
# Helper function to count non-empty lines
|
||||||
count_lines() {
|
count_lines() {
|
||||||
local input="$1"
|
local input="$1"
|
||||||
if [[ -z $input ]]; then
|
if [[ -z $input ]]; then
|
||||||
echo 0
|
echo 0
|
||||||
else
|
else
|
||||||
echo "$input" | grep -c . 2> /dev/null || echo 0
|
echo "$input" | grep -c . 2>/dev/null || echo 0
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# Main protection check
|
# Main protection check
|
||||||
check_custom_entries_protection() {
|
check_custom_entries_protection() {
|
||||||
local script_path
|
local script_path
|
||||||
script_path="$(readlink -f "$0")"
|
script_path="$(readlink -f "$0")"
|
||||||
|
|
||||||
# Get new entries from the script's heredoc
|
# Get new entries from the script's heredoc
|
||||||
local new_entries
|
local new_entries
|
||||||
new_entries=$(extract_custom_entries_from_script "$script_path")
|
new_entries=$(extract_custom_entries_from_script "$script_path")
|
||||||
local new_count
|
local new_count
|
||||||
new_count=$(count_lines "$new_entries")
|
new_count=$(count_lines "$new_entries")
|
||||||
|
|
||||||
# Get saved/existing entries (prefer state file, fall back to current /etc/hosts)
|
# Get saved/existing entries (prefer state file, fall back to current /etc/hosts)
|
||||||
local saved_entries
|
local saved_entries
|
||||||
saved_entries=$(load_saved_custom_entries)
|
saved_entries=$(load_saved_custom_entries)
|
||||||
if [[ -z $saved_entries ]]; then
|
if [[ -z $saved_entries ]]; then
|
||||||
# First run or state file missing - extract from current /etc/hosts if it has our marker
|
# First run or state file missing - extract from current /etc/hosts if it has our marker
|
||||||
saved_entries=$(extract_custom_entries_from_hosts "/etc/hosts")
|
saved_entries=$(extract_custom_entries_from_hosts "/etc/hosts")
|
||||||
fi
|
fi
|
||||||
local saved_count
|
local saved_count
|
||||||
saved_count=$(count_lines "$saved_entries")
|
saved_count=$(count_lines "$saved_entries")
|
||||||
|
|
||||||
# If no saved state exists, this is first installation - allow it
|
# If no saved state exists, this is first installation - allow it
|
||||||
if [[ $saved_count -eq 0 ]]; then
|
if [[ $saved_count -eq 0 ]]; then
|
||||||
echo "ℹ️ First installation detected - no protection check needed."
|
echo "ℹ️ First installation detected - no protection check needed."
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Find entries that were removed
|
# Find entries that were removed
|
||||||
local removed_entries
|
local removed_entries
|
||||||
removed_entries=$(comm -23 <(echo "$saved_entries") <(echo "$new_entries"))
|
removed_entries=$(comm -23 <(echo "$saved_entries") <(echo "$new_entries"))
|
||||||
local removed_count
|
local removed_count
|
||||||
removed_count=$(count_lines "$removed_entries")
|
removed_count=$(count_lines "$removed_entries")
|
||||||
|
|
||||||
# Find entries that are new
|
# Find entries that are new
|
||||||
local added_entries
|
local added_entries
|
||||||
added_entries=$(comm -13 <(echo "$saved_entries") <(echo "$new_entries"))
|
added_entries=$(comm -13 <(echo "$saved_entries") <(echo "$new_entries"))
|
||||||
local added_count
|
local added_count
|
||||||
added_count=$(count_lines "$added_entries")
|
added_count=$(count_lines "$added_entries")
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "📊 Custom Entries Protection Check:"
|
echo "📊 Custom Entries Protection Check:"
|
||||||
echo " Previously blocked: $saved_count entries"
|
echo " Previously blocked: $saved_count entries"
|
||||||
echo " Currently in script: $new_count entries"
|
echo " Currently in script: $new_count entries"
|
||||||
echo " Removed: $removed_count | Added: $added_count"
|
echo " Removed: $removed_count | Added: $added_count"
|
||||||
|
|
||||||
# RULE 1: No entries removed - always OK
|
# RULE 1: No entries removed - always OK
|
||||||
if [[ $removed_count -eq 0 ]]; then
|
if [[ $removed_count -eq 0 ]]; then
|
||||||
echo " ✅ No entries removed - protection check passed."
|
echo " ✅ No entries removed - protection check passed."
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# RULE 2: Entries were removed - BLOCK INSTALLATION
|
# RULE 2: Entries were removed - BLOCK INSTALLATION
|
||||||
echo ""
|
echo ""
|
||||||
echo "============================================================"
|
echo "============================================================"
|
||||||
echo " ❌ INSTALLATION BLOCKED - CUSTOM ENTRIES REMOVED"
|
echo " ❌ INSTALLATION BLOCKED - CUSTOM ENTRIES REMOVED"
|
||||||
echo "============================================================"
|
echo "============================================================"
|
||||||
echo ""
|
echo ""
|
||||||
echo "You are attempting to REMOVE the following blocked entries:"
|
echo "You are attempting to REMOVE the following blocked entries:"
|
||||||
while IFS= read -r entry; do
|
while IFS= read -r entry; do
|
||||||
echo " - $entry"
|
echo " - $entry"
|
||||||
done <<< "$removed_entries"
|
done <<<"$removed_entries"
|
||||||
echo ""
|
echo ""
|
||||||
echo "This is NOT allowed. The only way to unblock sites is to:"
|
echo "This is NOT allowed. The only way to unblock sites is to:"
|
||||||
echo ""
|
echo ""
|
||||||
echo " 1. Manually edit /etc/hosts (requires removing chattr protection)"
|
echo " 1. Manually edit /etc/hosts (requires removing chattr protection)"
|
||||||
echo " 2. Delete the state file /etc/hosts.custom-entries.state"
|
echo " 2. Delete the state file /etc/hosts.custom-entries.state"
|
||||||
echo " (also protected with chattr)"
|
echo " (also protected with chattr)"
|
||||||
echo ""
|
echo ""
|
||||||
echo "These manual steps are intentionally difficult to prevent"
|
echo "These manual steps are intentionally difficult to prevent"
|
||||||
echo "impulsive unblocking. If you really need to unblock something,"
|
echo "impulsive unblocking. If you really need to unblock something,"
|
||||||
echo "you'll have to work for it."
|
echo "you'll have to work for it."
|
||||||
echo ""
|
echo ""
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# Run the protection check
|
# Run the protection check
|
||||||
if ! check_custom_entries_protection; then
|
if ! check_custom_entries_protection; then
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Enable systemd-resolved
|
# Enable systemd-resolved
|
||||||
sudo systemctl enable systemd-resolved
|
sudo systemctl enable systemd-resolved
|
||||||
|
|
||||||
# Remove all attributes from /etc/hosts to allow modifications
|
# Remove all attributes from /etc/hosts to allow modifications
|
||||||
sudo chattr -i -a /etc/hosts 2> /dev/null || true
|
sudo chattr -i -a /etc/hosts 2>/dev/null || true
|
||||||
|
|
||||||
# Source and local cache configuration
|
# Source and local cache configuration
|
||||||
URL="https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/fakenews-gambling-porn-social/hosts"
|
URL="https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/fakenews-gambling-porn-social/hosts"
|
||||||
@ -177,33 +177,33 @@ LOCAL_CACHE="/etc/hosts.stevenblack"
|
|||||||
|
|
||||||
# Helpers
|
# Helpers
|
||||||
extract_date_epoch_from_file() {
|
extract_date_epoch_from_file() {
|
||||||
# Grep "# Date:" line and convert to epoch seconds (UTC)
|
# Grep "# Date:" line and convert to epoch seconds (UTC)
|
||||||
local f="$1"
|
local f="$1"
|
||||||
local line
|
local line
|
||||||
line=$(grep -m1 '^# Date:' "$f" 2> /dev/null | sed -E 's/^# Date:[[:space:]]*(.*)[[:space:]]*\(UTC\).*/\1 UTC/')
|
line=$(grep -m1 '^# Date:' "$f" 2>/dev/null | sed -E 's/^# Date:[[:space:]]*(.*)[[:space:]]*\(UTC\).*/\1 UTC/')
|
||||||
if [[ -n $line ]]; then
|
if [[ -n $line ]]; then
|
||||||
date -u -d "$line" +%s 2> /dev/null || echo ""
|
date -u -d "$line" +%s 2>/dev/null || echo ""
|
||||||
else
|
else
|
||||||
echo ""
|
echo ""
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
fetch_remote_header() {
|
fetch_remote_header() {
|
||||||
# Try to fetch only the first ~4KB using HTTP Range; fallback to piping to head
|
# Try to fetch only the first ~4KB using HTTP Range; fallback to piping to head
|
||||||
local out="$1"
|
local out="$1"
|
||||||
if curl -LfsS --max-time 10 -H 'Range: bytes=0-4095' "$URL" -o "$out"; then
|
if curl -LfsS --max-time 10 -H 'Range: bytes=0-4095' "$URL" -o "$out"; then
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
# Fallback – may download more, but we only keep first lines
|
# Fallback – may download more, but we only keep first lines
|
||||||
if curl -LfsS --max-time 10 "$URL" | head -n 20 > "$out"; then
|
if curl -LfsS --max-time 10 "$URL" | head -n 20 >"$out"; then
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
download_remote_full_to() {
|
download_remote_full_to() {
|
||||||
local out="$1"
|
local out="$1"
|
||||||
curl -LfsS "$URL" -o "$out"
|
curl -LfsS "$URL" -o "$out"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Decide whether to use cache or update
|
# Decide whether to use cache or update
|
||||||
@ -212,47 +212,47 @@ trap 'rm -f "$TMP_REMOTE_HEAD"' EXIT
|
|||||||
|
|
||||||
REMOTE_AVAILABLE=0
|
REMOTE_AVAILABLE=0
|
||||||
if fetch_remote_header "$TMP_REMOTE_HEAD"; then
|
if fetch_remote_header "$TMP_REMOTE_HEAD"; then
|
||||||
REMOTE_AVAILABLE=1
|
REMOTE_AVAILABLE=1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
NEED_UPDATE=0
|
NEED_UPDATE=0
|
||||||
|
|
||||||
if [[ -f $LOCAL_CACHE ]]; then
|
if [[ -f $LOCAL_CACHE ]]; then
|
||||||
local_epoch=$(extract_date_epoch_from_file "$LOCAL_CACHE")
|
local_epoch=$(extract_date_epoch_from_file "$LOCAL_CACHE")
|
||||||
else
|
else
|
||||||
local_epoch=""
|
local_epoch=""
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ $REMOTE_AVAILABLE -eq 1 ]]; then
|
if [[ $REMOTE_AVAILABLE -eq 1 ]]; then
|
||||||
remote_epoch=$(extract_date_epoch_from_file "$TMP_REMOTE_HEAD")
|
remote_epoch=$(extract_date_epoch_from_file "$TMP_REMOTE_HEAD")
|
||||||
if [[ -n $local_epoch && -n $remote_epoch && $local_epoch -ge $remote_epoch ]]; then
|
if [[ -n $local_epoch && -n $remote_epoch && $local_epoch -ge $remote_epoch ]]; then
|
||||||
echo "Using cached StevenBlack hosts (up-to-date)."
|
echo "Using cached StevenBlack hosts (up-to-date)."
|
||||||
else
|
else
|
||||||
echo "Cached version is missing or outdated; downloading latest StevenBlack hosts..."
|
echo "Cached version is missing or outdated; downloading latest StevenBlack hosts..."
|
||||||
NEED_UPDATE=1
|
NEED_UPDATE=1
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
if [[ -f $LOCAL_CACHE ]]; then
|
if [[ -f $LOCAL_CACHE ]]; then
|
||||||
echo "No internet; using cached StevenBlack hosts."
|
echo "No internet; using cached StevenBlack hosts."
|
||||||
else
|
else
|
||||||
echo "Error: No internet and no cached StevenBlack hosts found." >&2
|
echo "Error: No internet and no cached StevenBlack hosts found." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Ensure we have a fresh cache if needed
|
# Ensure we have a fresh cache if needed
|
||||||
if [[ $NEED_UPDATE -eq 1 ]]; then
|
if [[ $NEED_UPDATE -eq 1 ]]; then
|
||||||
TMP_DL=$(mktemp)
|
TMP_DL=$(mktemp)
|
||||||
if download_remote_full_to "$TMP_DL"; then
|
if download_remote_full_to "$TMP_DL"; then
|
||||||
# Save raw upstream to cache
|
# Save raw upstream to cache
|
||||||
sudo mv "$TMP_DL" "$LOCAL_CACHE"
|
sudo mv "$TMP_DL" "$LOCAL_CACHE"
|
||||||
sudo chmod 644 "$LOCAL_CACHE"
|
sudo chmod 644 "$LOCAL_CACHE"
|
||||||
echo "Saved latest StevenBlack hosts to cache: $LOCAL_CACHE"
|
echo "Saved latest StevenBlack hosts to cache: $LOCAL_CACHE"
|
||||||
else
|
else
|
||||||
rm -f "$TMP_DL"
|
rm -f "$TMP_DL"
|
||||||
echo "Error: Failed to download latest StevenBlack hosts." >&2
|
echo "Error: Failed to download latest StevenBlack hosts." >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Install the base hosts from cache into /etc/hosts
|
# Install the base hosts from cache into /etc/hosts
|
||||||
@ -272,7 +272,7 @@ sudo sed -i 's/^0\.0\.0\.0 messenger\.com/#0.0.0.0 messenger.com/' /etc/hosts
|
|||||||
|
|
||||||
# Add custom entries for YouTube and Discord
|
# Add custom entries for YouTube and Discord
|
||||||
echo "Adding custom entries for YouTube and Discord..."
|
echo "Adding custom entries for YouTube and Discord..."
|
||||||
tee -a /etc/hosts > /dev/null << 'EOF'
|
tee -a /etc/hosts >/dev/null <<'EOF'
|
||||||
|
|
||||||
# Custom blocking entries
|
# Custom blocking entries
|
||||||
# YouTube
|
# YouTube
|
||||||
@ -295,15 +295,15 @@ tee -a /etc/hosts > /dev/null << 'EOF'
|
|||||||
|
|
||||||
# Steam Store
|
# Steam Store
|
||||||
|
|
||||||
# Discord (selective blocking - media only, voice chat allowed)
|
# Discord - media allowed
|
||||||
0.0.0.0 cdn.discordapp.com
|
# 0.0.0.0 cdn.discordapp.com
|
||||||
0.0.0.0 media.discordapp.net
|
# 0.0.0.0 media.discordapp.net
|
||||||
0.0.0.0 images-ext-1.discordapp.net
|
# 0.0.0.0 images-ext-1.discordapp.net
|
||||||
0.0.0.0 images-ext-2.discordapp.net
|
# 0.0.0.0 images-ext-2.discordapp.net
|
||||||
0.0.0.0 attachments-1.discordapp.net
|
# 0.0.0.0 attachments-1.discordapp.net
|
||||||
0.0.0.0 attachments-2.discordapp.net
|
# 0.0.0.0 attachments-2.discordapp.net
|
||||||
0.0.0.0 tenor.com
|
# 0.0.0.0 tenor.com
|
||||||
0.0.0.0 giphy.com
|
# 0.0.0.0 giphy.com
|
||||||
|
|
||||||
# Food Delivery Services
|
# Food Delivery Services
|
||||||
# Polish services
|
# Polish services
|
||||||
@ -407,20 +407,111 @@ echo "Saving custom entries state for protection mechanism..."
|
|||||||
script_path="$(readlink -f "$0")"
|
script_path="$(readlink -f "$0")"
|
||||||
current_custom_entries=$(extract_custom_entries_from_script "$script_path")
|
current_custom_entries=$(extract_custom_entries_from_script "$script_path")
|
||||||
# Remove immutable from state file if it exists
|
# Remove immutable from state file if it exists
|
||||||
chattr -i "$CUSTOM_ENTRIES_STATE_FILE" 2> /dev/null || true
|
chattr -i "$CUSTOM_ENTRIES_STATE_FILE" 2>/dev/null || true
|
||||||
save_custom_entries_state "$current_custom_entries"
|
save_custom_entries_state "$current_custom_entries"
|
||||||
echo "✅ Custom entries state saved to $CUSTOM_ENTRIES_STATE_FILE"
|
echo "✅ Custom entries state saved to $CUSTOM_ENTRIES_STATE_FILE"
|
||||||
|
|
||||||
# Optionally flush DNS caches
|
# Optionally flush DNS caches
|
||||||
if [[ $FLUSH_DNS -eq 1 ]]; then
|
if [[ $FLUSH_DNS -eq 1 ]]; then
|
||||||
echo "Flushing DNS caches..."
|
echo "Flushing DNS caches..."
|
||||||
sudo systemd-resolve --flush-caches
|
sudo systemd-resolve --flush-caches
|
||||||
sudo systemctl restart NetworkManager.service
|
sudo systemctl restart NetworkManager.service
|
||||||
else
|
else
|
||||||
echo "DNS cache flush skipped (use --flush-dns to enable)."
|
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
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "✅ Installation complete!"
|
echo "✅ Installation complete!"
|
||||||
echo " Custom entries protection is now active."
|
echo " Custom entries protection is now active."
|
||||||
echo " Removing blocked entries from the script will be blocked."
|
echo " Removing blocked entries from the script will be blocked."
|
||||||
|
echo " DNS over HTTPS (DoH) has been disabled in browsers."
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# FORCE BROWSER RESTART TO APPLY DOH CHANGES
|
||||||
|
# ============================================================================
|
||||||
|
# Kill all browser processes so DoH changes take effect immediately
|
||||||
|
echo ""
|
||||||
|
echo "Killing browsers to apply DoH policy changes..."
|
||||||
|
BROWSERS_KILLED=0
|
||||||
|
|
||||||
|
for browser in chrome chromium chromium-browser brave brave-browser firefox firefox-esr thorium vivaldi opera; do
|
||||||
|
if pgrep -x "$browser" &>/dev/null || pgrep -f "/opt/.*/$browser" &>/dev/null; then
|
||||||
|
echo " Killing $browser..."
|
||||||
|
pkill -9 -f "$browser" 2>/dev/null || true
|
||||||
|
BROWSERS_KILLED=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Also kill by common binary paths
|
||||||
|
for pattern in "/opt/google/chrome" "/opt/brave" "/opt/thorium" "/usr/lib/firefox" "/usr/lib/chromium"; do
|
||||||
|
if pgrep -f "$pattern" &>/dev/null; then
|
||||||
|
echo " Killing processes matching $pattern..."
|
||||||
|
pkill -9 -f "$pattern" 2>/dev/null || true
|
||||||
|
BROWSERS_KILLED=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ $BROWSERS_KILLED -eq 1 ]]; then
|
||||||
|
echo ""
|
||||||
|
echo "⚠️ Browsers were killed to apply DNS settings."
|
||||||
|
echo " Reopen your browser - hosts blocking is now enforced."
|
||||||
|
else
|
||||||
|
echo " No browsers were running."
|
||||||
|
fi
|
||||||
|
|||||||
234
scripts/digital_wellbeing/README_COMPULSIVE_BLOCK_LLM.md
Normal file
234
scripts/digital_wellbeing/README_COMPULSIVE_BLOCK_LLM.md
Normal 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
|
||||||
277
scripts/digital_wellbeing/README_MIDNIGHT_SHUTDOWN_LLM.md
Normal file
277
scripts/digital_wellbeing/README_MIDNIGHT_SHUTDOWN_LLM.md
Normal 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)
|
||||||
@ -12,182 +12,298 @@ set -euo pipefail
|
|||||||
# Send desktop notification (inlined from common.sh to avoid dependency issues
|
# Send desktop notification (inlined from common.sh to avoid dependency issues
|
||||||
# when script is installed to /usr/local/bin)
|
# when script is installed to /usr/local/bin)
|
||||||
notify() {
|
notify() {
|
||||||
local title="$1"
|
local title="$1"
|
||||||
local message="$2"
|
local message="$2"
|
||||||
local urgency="${3:-normal}"
|
local urgency="${3:-normal}"
|
||||||
local timeout="${4:-5000}"
|
local timeout="${4:-5000}"
|
||||||
|
|
||||||
if command -v notify-send &> /dev/null; then
|
if command -v notify-send &>/dev/null; then
|
||||||
notify-send -u "$urgency" -t "$timeout" "$title" "$message" 2> /dev/null || true
|
notify-send -u "$urgency" -t "$timeout" "$title" "$message" 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# Configuration
|
# Configuration
|
||||||
STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/compulsive-block"
|
STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/compulsive-block"
|
||||||
LOG_FILE="$STATE_DIR/compulsive-block.log"
|
LOG_FILE="$STATE_DIR/compulsive-block.log"
|
||||||
|
|
||||||
|
# Auto-close timeout in minutes (apps forcefully closed after this)
|
||||||
|
AUTO_CLOSE_TIMEOUT_MINUTES=10
|
||||||
|
# Warning before auto-close (in minutes before timeout)
|
||||||
|
AUTO_CLOSE_WARNING_MINUTES=2
|
||||||
|
|
||||||
# Apps to limit (name -> binary path)
|
# Apps to limit (name -> binary path)
|
||||||
# These are the primary wrapper locations (what the user calls)
|
# These are the primary wrapper locations (what the user calls)
|
||||||
declare -A APPS=(
|
declare -A APPS=(
|
||||||
["beeper"]="/usr/bin/beeper"
|
["beeper"]="/usr/bin/beeper"
|
||||||
["signal-desktop"]="/usr/bin/signal-desktop"
|
["signal-desktop"]="/usr/bin/signal-desktop"
|
||||||
["discord"]="/usr/bin/discord"
|
["discord"]="/usr/bin/discord"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Actual executable paths (the real binaries to exec after wrapper check)
|
# Actual executable paths (the real binaries to exec after wrapper check)
|
||||||
# These are where the real code lives
|
# These are where the real code lives
|
||||||
declare -A REAL_BINARIES=(
|
declare -A REAL_BINARIES=(
|
||||||
["beeper"]="/opt/beeper/beepertexts"
|
["beeper"]="/opt/beeper/beepertexts"
|
||||||
["signal-desktop"]="/usr/lib/signal-desktop/signal-desktop"
|
["signal-desktop"]="/usr/lib/signal-desktop/signal-desktop"
|
||||||
["discord"]="/opt/discord/Discord"
|
["discord"]="/opt/discord/Discord"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Ensure state directory exists
|
# Ensure state directory exists
|
||||||
ensure_state_dir() {
|
ensure_state_dir() {
|
||||||
mkdir -p "$STATE_DIR" 2> /dev/null || true
|
mkdir -p "$STATE_DIR" 2>/dev/null || true
|
||||||
}
|
}
|
||||||
|
|
||||||
# Log message with timestamp
|
# Log message with timestamp
|
||||||
log_message() {
|
log_message() {
|
||||||
local msg
|
local msg
|
||||||
msg="$(date '+%Y-%m-%d %H:%M:%S') - $1"
|
msg="$(date '+%Y-%m-%d %H:%M:%S') - $1"
|
||||||
echo "$msg" >&2
|
echo "$msg" >&2
|
||||||
echo "$msg" >> "$LOG_FILE" 2> /dev/null || true
|
echo "$msg" >>"$LOG_FILE" 2>/dev/null || true
|
||||||
}
|
}
|
||||||
|
|
||||||
# Get current hour key (YYYY-MM-DD-HH format)
|
# Get current hour key (YYYY-MM-DD-HH format)
|
||||||
get_hour_key() {
|
get_hour_key() {
|
||||||
date '+%Y-%m-%d-%H'
|
date '+%Y-%m-%d-%H'
|
||||||
}
|
}
|
||||||
|
|
||||||
# Get state file path for an app
|
# Get state file path for an app
|
||||||
get_state_file() {
|
get_state_file() {
|
||||||
local app="$1"
|
local app="$1"
|
||||||
echo "$STATE_DIR/${app}.lastopen"
|
echo "$STATE_DIR/${app}.lastopen"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Check if app was already opened this hour
|
# Check if app was already opened this hour
|
||||||
was_opened_this_hour() {
|
was_opened_this_hour() {
|
||||||
local app="$1"
|
local app="$1"
|
||||||
local state_file
|
local state_file
|
||||||
state_file=$(get_state_file "$app")
|
state_file=$(get_state_file "$app")
|
||||||
local current_hour
|
local current_hour
|
||||||
current_hour=$(get_hour_key)
|
current_hour=$(get_hour_key)
|
||||||
|
|
||||||
if [[ -f $state_file ]]; then
|
if [[ -f $state_file ]]; then
|
||||||
local last_hour
|
local last_hour
|
||||||
last_hour=$(cat "$state_file" 2> /dev/null || echo "")
|
last_hour=$(cat "$state_file" 2>/dev/null || echo "")
|
||||||
if [[ $last_hour == "$current_hour" ]]; then
|
if [[ $last_hour == "$current_hour" ]]; then
|
||||||
return 0 # Was opened this hour
|
return 0 # Was opened this hour
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
return 1 # Not opened this hour
|
return 1 # Not opened this hour
|
||||||
}
|
}
|
||||||
|
|
||||||
# Record app opening
|
# Record app opening
|
||||||
record_opening() {
|
record_opening() {
|
||||||
local app="$1"
|
local app="$1"
|
||||||
local state_file
|
local state_file
|
||||||
state_file=$(get_state_file "$app")
|
state_file=$(get_state_file "$app")
|
||||||
local current_hour
|
local current_hour
|
||||||
current_hour=$(get_hour_key)
|
current_hour=$(get_hour_key)
|
||||||
|
|
||||||
echo "$current_hour" > "$state_file"
|
echo "$current_hour" >"$state_file"
|
||||||
log_message "ALLOWED: $app opened (first time this hour: $current_hour)"
|
log_message "ALLOWED: $app opened (first time this hour: $current_hour)"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Block app and notify
|
# Block app and notify
|
||||||
block_app() {
|
block_app() {
|
||||||
local app="$1"
|
local app="$1"
|
||||||
local current_hour
|
local current_hour
|
||||||
current_hour=$(get_hour_key)
|
current_hour=$(get_hour_key)
|
||||||
|
|
||||||
log_message "BLOCKED: $app launch prevented (already opened this hour: $current_hour)"
|
log_message "BLOCKED: $app launch prevented (already opened this hour: $current_hour)"
|
||||||
|
|
||||||
# Send notification using common library
|
# Send notification using common library
|
||||||
notify "🚫 $app Blocked" "Already opened this hour. Wait until the next hour." critical 5000
|
notify "🚫 $app Blocked" "Already opened this hour. Wait until the next hour." critical 5000
|
||||||
}
|
}
|
||||||
|
|
||||||
# Get real binary path for an app
|
# Get real binary path for an app
|
||||||
get_real_binary() {
|
get_real_binary() {
|
||||||
local app="$1"
|
local app="$1"
|
||||||
local wrapper_path="${APPS[$app]}"
|
local wrapper_path="${APPS[$app]}"
|
||||||
local real_binary="${REAL_BINARIES[$app]}"
|
local real_binary="${REAL_BINARIES[$app]}"
|
||||||
|
|
||||||
# Check if wrapper is installed (original moved to .orig)
|
# Check if wrapper is installed (original moved to .orig)
|
||||||
if [[ -f "${wrapper_path}.orig" ]]; then
|
if [[ -f "${wrapper_path}.orig" ]]; then
|
||||||
# Wrapper installed, return the actual executable
|
# Wrapper installed, return the actual executable
|
||||||
echo "$real_binary"
|
echo "$real_binary"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
return 1
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get running state file path for an app (tracks PID and start time)
|
||||||
|
get_running_file() {
|
||||||
|
local app="$1"
|
||||||
|
echo "$STATE_DIR/${app}.running"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Clean up stale running state (process no longer running)
|
||||||
|
cleanup_stale_running_state() {
|
||||||
|
local app="$1"
|
||||||
|
local running_file
|
||||||
|
running_file=$(get_running_file "$app")
|
||||||
|
|
||||||
|
if [[ ! -f $running_file ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local pid
|
||||||
|
pid=$(awk '{print $1}' "$running_file" 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
if [[ -z $pid ]]; then
|
||||||
|
rm -f "$running_file"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if process is still running
|
||||||
|
if ! kill -0 "$pid" 2>/dev/null; then
|
||||||
|
log_message "CLEANUP: Stale running state for $app (PID $pid no longer exists)"
|
||||||
|
rm -f "$running_file"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Launch app with auto-close timer
|
||||||
|
launch_with_timer() {
|
||||||
|
local app="$1"
|
||||||
|
local real_binary="$2"
|
||||||
|
shift 2
|
||||||
|
|
||||||
|
local warning_seconds=$(((AUTO_CLOSE_TIMEOUT_MINUTES - AUTO_CLOSE_WARNING_MINUTES) * 60))
|
||||||
|
local running_file
|
||||||
|
running_file=$(get_running_file "$app")
|
||||||
|
|
||||||
|
# Launch the app in background
|
||||||
|
"$real_binary" "$@" &
|
||||||
|
local app_pid=$!
|
||||||
|
|
||||||
|
# Record state
|
||||||
|
echo "$app_pid $(date +%s)" >"$running_file"
|
||||||
|
log_message "LAUNCHED: $app with PID $app_pid (auto-close in ${AUTO_CLOSE_TIMEOUT_MINUTES}m)"
|
||||||
|
|
||||||
|
# Spawn the auto-close daemon in a completely detached subshell
|
||||||
|
(
|
||||||
|
# Detach from terminal
|
||||||
|
exec </dev/null >/dev/null 2>&1
|
||||||
|
|
||||||
|
# Wait for warning time
|
||||||
|
sleep "$warning_seconds"
|
||||||
|
|
||||||
|
# Check if still running before warning
|
||||||
|
if kill -0 "$app_pid" 2>/dev/null; then
|
||||||
|
# Send warning notification
|
||||||
|
notify-send -u critical -t 30000 "⏰ $app Closing Soon" \
|
||||||
|
"Session will end in ${AUTO_CLOSE_WARNING_MINUTES} minutes. Save your work!" 2>/dev/null || true
|
||||||
|
else
|
||||||
|
# Process already exited
|
||||||
|
rm -f "$running_file" 2>/dev/null || true
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Wait remaining time
|
||||||
|
sleep $((AUTO_CLOSE_WARNING_MINUTES * 60))
|
||||||
|
|
||||||
|
# Check if still running
|
||||||
|
if kill -0 "$app_pid" 2>/dev/null; then
|
||||||
|
# Send final notification
|
||||||
|
notify-send -u critical -t 5000 "🚫 $app Session Ended" \
|
||||||
|
"Time's up! Closing $app now." 2>/dev/null || true
|
||||||
|
|
||||||
|
# Graceful kill first
|
||||||
|
kill "$app_pid" 2>/dev/null || true
|
||||||
|
|
||||||
|
# Wait a moment for graceful shutdown
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
# Force kill if still running
|
||||||
|
if kill -0 "$app_pid" 2>/dev/null; then
|
||||||
|
kill -9 "$app_pid" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "$(date '+%Y-%m-%d %H:%M:%S') - AUTO-CLOSED: $app (PID $app_pid) after ${AUTO_CLOSE_TIMEOUT_MINUTES}m" >>"$LOG_FILE" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -f "$running_file" 2>/dev/null || true
|
||||||
|
) &
|
||||||
|
disown
|
||||||
|
|
||||||
|
# Wait for the app to exit (keeps wrapper process alive while app is running)
|
||||||
|
wait "$app_pid" 2>/dev/null || true
|
||||||
|
local exit_code=$?
|
||||||
|
|
||||||
|
# Clean up running state
|
||||||
|
rm -f "$running_file" 2>/dev/null || true
|
||||||
|
|
||||||
|
log_message "EXITED: $app (PID $app_pid) with code $exit_code"
|
||||||
|
return $exit_code
|
||||||
}
|
}
|
||||||
|
|
||||||
# Main wrapper function - called when wrapping app launches
|
# Main wrapper function - called when wrapping app launches
|
||||||
wrapper_main() {
|
wrapper_main() {
|
||||||
local app="$1"
|
local app="$1"
|
||||||
shift
|
shift
|
||||||
|
|
||||||
ensure_state_dir
|
ensure_state_dir
|
||||||
|
|
||||||
local real_binary
|
local real_binary
|
||||||
if ! real_binary=$(get_real_binary "$app"); then
|
if ! real_binary=$(get_real_binary "$app"); then
|
||||||
log_message "ERROR: Real binary not found for $app"
|
log_message "ERROR: Real binary not found for $app"
|
||||||
echo "Error: Real binary for $app not found. Was the installer run?" >&2
|
echo "Error: Real binary for $app not found. Was the installer run?" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if was_opened_this_hour "$app"; then
|
# Clean up stale running state from previous crashes
|
||||||
block_app "$app"
|
cleanup_stale_running_state "$app"
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
record_opening "$app"
|
if was_opened_this_hour "$app"; then
|
||||||
exec "$real_binary" "$@"
|
block_app "$app"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
record_opening "$app"
|
||||||
|
|
||||||
|
# Launch with auto-close timer (replaces direct exec)
|
||||||
|
launch_with_timer "$app" "$real_binary" "$@"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Install wrapper for a specific app
|
# Install wrapper for a specific app
|
||||||
install_wrapper() {
|
install_wrapper() {
|
||||||
local app="$1"
|
local app="$1"
|
||||||
local wrapper_path="${APPS[$app]}"
|
local wrapper_path="${APPS[$app]}"
|
||||||
local real_binary="${REAL_BINARIES[$app]}"
|
local real_binary="${REAL_BINARIES[$app]}"
|
||||||
|
|
||||||
# Check if already wrapped
|
# Check if already wrapped
|
||||||
if [[ -f "${wrapper_path}.orig" ]]; then
|
if [[ -f "${wrapper_path}.orig" ]]; then
|
||||||
echo " ✓ $app already wrapped"
|
echo " ✓ $app already wrapped"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check if wrapper location exists (file or symlink)
|
# Check if wrapper location exists (file or symlink)
|
||||||
if [[ ! -e $wrapper_path && ! -L $wrapper_path ]]; then
|
if [[ ! -e $wrapper_path && ! -L $wrapper_path ]]; then
|
||||||
echo " ⚠ $app not installed ($wrapper_path not found)"
|
echo " ⚠ $app not installed ($wrapper_path not found)"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check if real binary exists
|
# Check if real binary exists
|
||||||
if [[ ! -x $real_binary ]]; then
|
if [[ ! -x $real_binary ]]; then
|
||||||
echo " ⚠ $app real binary not found ($real_binary)"
|
echo " ⚠ $app real binary not found ($real_binary)"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo " Installing wrapper for $app..."
|
echo " Installing wrapper for $app..."
|
||||||
|
|
||||||
# Handle symlinks: save the symlink itself, not the target
|
# Handle symlinks: save the symlink itself, not the target
|
||||||
if [[ -L $wrapper_path ]]; then
|
if [[ -L $wrapper_path ]]; then
|
||||||
local link_target
|
local link_target
|
||||||
link_target=$(readlink "$wrapper_path")
|
link_target=$(readlink "$wrapper_path")
|
||||||
echo " Saving symlink $wrapper_path -> $link_target as ${wrapper_path}.orig"
|
echo " Saving symlink $wrapper_path -> $link_target as ${wrapper_path}.orig"
|
||||||
# Remove symlink and create .orig that stores the link target info
|
# Remove symlink and create .orig that stores the link target info
|
||||||
echo "SYMLINK:$link_target" > "${wrapper_path}.orig"
|
echo "SYMLINK:$link_target" >"${wrapper_path}.orig"
|
||||||
rm "$wrapper_path"
|
rm "$wrapper_path"
|
||||||
else
|
else
|
||||||
echo " Backing up $wrapper_path -> ${wrapper_path}.orig"
|
echo " Backing up $wrapper_path -> ${wrapper_path}.orig"
|
||||||
mv "$wrapper_path" "${wrapper_path}.orig"
|
mv "$wrapper_path" "${wrapper_path}.orig"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo " Creating wrapper at $wrapper_path"
|
echo " Creating wrapper at $wrapper_path"
|
||||||
cat > "$wrapper_path" << WRAPPER_EOF
|
cat >"$wrapper_path" <<WRAPPER_EOF
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Auto-generated wrapper for $app - blocks compulsive opening
|
# Auto-generated wrapper for $app - blocks compulsive opening
|
||||||
# Real binary: $real_binary
|
# Real binary: $real_binary
|
||||||
@ -195,88 +311,88 @@ install_wrapper() {
|
|||||||
exec /usr/local/bin/block-compulsive-opening.sh wrapper "$app" "\$@"
|
exec /usr/local/bin/block-compulsive-opening.sh wrapper "$app" "\$@"
|
||||||
WRAPPER_EOF
|
WRAPPER_EOF
|
||||||
|
|
||||||
chmod +x "$wrapper_path"
|
chmod +x "$wrapper_path"
|
||||||
echo " ✓ $app wrapper installed"
|
echo " ✓ $app wrapper installed"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Uninstall wrapper for a specific app
|
# Uninstall wrapper for a specific app
|
||||||
uninstall_wrapper() {
|
uninstall_wrapper() {
|
||||||
local app="$1"
|
local app="$1"
|
||||||
local wrapper_path="${APPS[$app]}"
|
local wrapper_path="${APPS[$app]}"
|
||||||
|
|
||||||
if [[ ! -f "${wrapper_path}.orig" ]]; then
|
if [[ ! -f "${wrapper_path}.orig" ]]; then
|
||||||
echo " ⚠ $app wrapper not found"
|
echo " ⚠ $app wrapper not found"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo " Removing wrapper for $app..."
|
echo " Removing wrapper for $app..."
|
||||||
rm -f "$wrapper_path"
|
rm -f "$wrapper_path"
|
||||||
|
|
||||||
# Check if it was a symlink (stored as SYMLINK:target in .orig)
|
# Check if it was a symlink (stored as SYMLINK:target in .orig)
|
||||||
local orig_content
|
local orig_content
|
||||||
orig_content=$(cat "${wrapper_path}.orig" 2> /dev/null || echo "")
|
orig_content=$(cat "${wrapper_path}.orig" 2>/dev/null || echo "")
|
||||||
if [[ $orig_content == SYMLINK:* ]]; then
|
if [[ $orig_content == SYMLINK:* ]]; then
|
||||||
local link_target="${orig_content#SYMLINK:}"
|
local link_target="${orig_content#SYMLINK:}"
|
||||||
echo " Restoring symlink $wrapper_path -> $link_target"
|
echo " Restoring symlink $wrapper_path -> $link_target"
|
||||||
ln -s "$link_target" "$wrapper_path"
|
ln -s "$link_target" "$wrapper_path"
|
||||||
rm "${wrapper_path}.orig"
|
rm "${wrapper_path}.orig"
|
||||||
else
|
else
|
||||||
echo " Restoring original file"
|
echo " Restoring original file"
|
||||||
mv "${wrapper_path}.orig" "$wrapper_path"
|
mv "${wrapper_path}.orig" "$wrapper_path"
|
||||||
fi
|
fi
|
||||||
echo " ✓ $app restored"
|
echo " ✓ $app restored"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Install all wrappers
|
# Install all wrappers
|
||||||
install_all() {
|
install_all() {
|
||||||
echo "Installing compulsive opening blockers..."
|
echo "Installing compulsive opening blockers..."
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Install main script to /usr/local/bin
|
# Install main script to /usr/local/bin
|
||||||
local script_path
|
local script_path
|
||||||
script_path="$(readlink -f "$0")"
|
script_path="$(readlink -f "$0")"
|
||||||
local install_path="/usr/local/bin/block-compulsive-opening.sh"
|
local install_path="/usr/local/bin/block-compulsive-opening.sh"
|
||||||
|
|
||||||
if [[ $script_path != "$install_path" ]]; then
|
if [[ $script_path != "$install_path" ]]; then
|
||||||
echo "Installing main script to $install_path..."
|
echo "Installing main script to $install_path..."
|
||||||
cp "$script_path" "$install_path"
|
cp "$script_path" "$install_path"
|
||||||
chmod +x "$install_path"
|
chmod +x "$install_path"
|
||||||
echo "✓ Main script installed"
|
echo "✓ Main script installed"
|
||||||
else
|
else
|
||||||
echo "Main script already at $install_path"
|
echo "Main script already at $install_path"
|
||||||
fi
|
fi
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Install wrappers for each app
|
# Install wrappers for each app
|
||||||
local installed=0
|
local installed=0
|
||||||
for app in "${!APPS[@]}"; do
|
for app in "${!APPS[@]}"; do
|
||||||
if install_wrapper "$app"; then
|
if install_wrapper "$app"; then
|
||||||
((installed++)) || true
|
((installed++)) || true
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "Installation complete. $installed app(s) wrapped."
|
echo "Installation complete. $installed app(s) wrapped."
|
||||||
echo ""
|
echo ""
|
||||||
echo "Each app can now only be opened once per hour."
|
echo "Each app can now only be opened once per hour."
|
||||||
echo "State files stored in: $STATE_DIR"
|
echo "State files stored in: $STATE_DIR"
|
||||||
echo "Logs stored in: $LOG_FILE"
|
echo "Logs stored in: $LOG_FILE"
|
||||||
|
|
||||||
# Install pacman hook to re-wrap after package updates
|
# Install pacman hook to re-wrap after package updates
|
||||||
install_pacman_hook
|
install_pacman_hook
|
||||||
}
|
}
|
||||||
|
|
||||||
# Install pacman hook to re-install wrappers after package updates
|
# Install pacman hook to re-install wrappers after package updates
|
||||||
install_pacman_hook() {
|
install_pacman_hook() {
|
||||||
local hook_dir="/etc/pacman.d/hooks"
|
local hook_dir="/etc/pacman.d/hooks"
|
||||||
local hook_file="$hook_dir/95-compulsive-block-rewrap.hook"
|
local hook_file="$hook_dir/95-compulsive-block-rewrap.hook"
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "Installing pacman hook..."
|
echo "Installing pacman hook..."
|
||||||
|
|
||||||
mkdir -p "$hook_dir"
|
mkdir -p "$hook_dir"
|
||||||
|
|
||||||
cat > "$hook_file" << 'HOOK_EOF'
|
cat >"$hook_file" <<'HOOK_EOF'
|
||||||
[Trigger]
|
[Trigger]
|
||||||
Operation = Upgrade
|
Operation = Upgrade
|
||||||
Operation = Install
|
Operation = Install
|
||||||
@ -291,131 +407,131 @@ When = PostTransaction
|
|||||||
Exec = /usr/local/bin/block-compulsive-opening.sh rewrap-quiet
|
Exec = /usr/local/bin/block-compulsive-opening.sh rewrap-quiet
|
||||||
HOOK_EOF
|
HOOK_EOF
|
||||||
|
|
||||||
chmod 644 "$hook_file"
|
chmod 644 "$hook_file"
|
||||||
echo "✓ Pacman hook installed: $hook_file"
|
echo "✓ Pacman hook installed: $hook_file"
|
||||||
echo " Wrappers will be automatically re-installed after beeper/signal/discord updates"
|
echo " Wrappers will be automatically re-installed after beeper/signal/discord updates"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Uninstall pacman hook
|
# Uninstall pacman hook
|
||||||
uninstall_pacman_hook() {
|
uninstall_pacman_hook() {
|
||||||
local hook_file="/etc/pacman.d/hooks/95-compulsive-block-rewrap.hook"
|
local hook_file="/etc/pacman.d/hooks/95-compulsive-block-rewrap.hook"
|
||||||
if [[ -f $hook_file ]]; then
|
if [[ -f $hook_file ]]; then
|
||||||
rm -f "$hook_file"
|
rm -f "$hook_file"
|
||||||
echo "✓ Pacman hook removed"
|
echo "✓ Pacman hook removed"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# Quietly re-wrap apps (for pacman hook - no interactive output)
|
# Quietly re-wrap apps (for pacman hook - no interactive output)
|
||||||
rewrap_quiet() {
|
rewrap_quiet() {
|
||||||
log_message "REWRAP: Pacman hook triggered, re-installing wrappers"
|
log_message "REWRAP: Pacman hook triggered, re-installing wrappers"
|
||||||
|
|
||||||
for app in "${!APPS[@]}"; do
|
for app in "${!APPS[@]}"; do
|
||||||
local wrapper_path="${APPS[$app]}"
|
local wrapper_path="${APPS[$app]}"
|
||||||
|
|
||||||
# Check if wrapper was overwritten (no longer our wrapper script)
|
# Check if wrapper was overwritten (no longer our wrapper script)
|
||||||
if [[ -f $wrapper_path ]] && ! grep -q "block-compulsive-opening" "$wrapper_path" 2> /dev/null; then
|
if [[ -f $wrapper_path ]] && ! grep -q "block-compulsive-opening" "$wrapper_path" 2>/dev/null; then
|
||||||
# Wrapper was overwritten by package update
|
# Wrapper was overwritten by package update
|
||||||
log_message "REWRAP: $app wrapper was overwritten, re-installing"
|
log_message "REWRAP: $app wrapper was overwritten, re-installing"
|
||||||
|
|
||||||
# Remove old .orig if exists (it's now stale)
|
# Remove old .orig if exists (it's now stale)
|
||||||
rm -f "${wrapper_path}.orig"
|
rm -f "${wrapper_path}.orig"
|
||||||
|
|
||||||
# Re-install wrapper
|
# Re-install wrapper
|
||||||
install_wrapper "$app" >> "$LOG_FILE" 2>&1 || true
|
install_wrapper "$app" >>"$LOG_FILE" 2>&1 || true
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
log_message "REWRAP: Complete"
|
log_message "REWRAP: Complete"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Uninstall all wrappers
|
# Uninstall all wrappers
|
||||||
uninstall_all() {
|
uninstall_all() {
|
||||||
echo "Removing compulsive opening blockers..."
|
echo "Removing compulsive opening blockers..."
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
for app in "${!APPS[@]}"; do
|
for app in "${!APPS[@]}"; do
|
||||||
uninstall_wrapper "$app" || true
|
uninstall_wrapper "$app" || true
|
||||||
done
|
done
|
||||||
|
|
||||||
rm -f "/usr/local/bin/block-compulsive-opening.sh"
|
rm -f "/usr/local/bin/block-compulsive-opening.sh"
|
||||||
|
|
||||||
# Remove pacman hook
|
# Remove pacman hook
|
||||||
uninstall_pacman_hook
|
uninstall_pacman_hook
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "Uninstallation complete."
|
echo "Uninstallation complete."
|
||||||
}
|
}
|
||||||
|
|
||||||
# Show status of all apps
|
# Show status of all apps
|
||||||
show_status() {
|
show_status() {
|
||||||
ensure_state_dir
|
ensure_state_dir
|
||||||
local current_hour
|
local current_hour
|
||||||
current_hour=$(get_hour_key)
|
current_hour=$(get_hour_key)
|
||||||
|
|
||||||
echo "Compulsive Opening Blocker Status"
|
echo "Compulsive Opening Blocker Status"
|
||||||
echo "=================================="
|
echo "=================================="
|
||||||
echo "Current hour: $current_hour"
|
echo "Current hour: $current_hour"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
for app in "${!APPS[@]}"; do
|
for app in "${!APPS[@]}"; do
|
||||||
local state_file
|
local state_file
|
||||||
state_file=$(get_state_file "$app")
|
state_file=$(get_state_file "$app")
|
||||||
local status="not opened this hour"
|
local status="not opened this hour"
|
||||||
local icon="○"
|
local icon="○"
|
||||||
|
|
||||||
if [[ -f $state_file ]]; then
|
if [[ -f $state_file ]]; then
|
||||||
local last_hour
|
local last_hour
|
||||||
last_hour=$(cat "$state_file" 2> /dev/null || echo "")
|
last_hour=$(cat "$state_file" 2>/dev/null || echo "")
|
||||||
if [[ $last_hour == "$current_hour" ]]; then
|
if [[ $last_hour == "$current_hour" ]]; then
|
||||||
status="already opened (blocked until next hour)"
|
status="already opened (blocked until next hour)"
|
||||||
icon="●"
|
icon="●"
|
||||||
else
|
else
|
||||||
status="last opened: $last_hour"
|
status="last opened: $last_hour"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check if wrapped
|
# Check if wrapped
|
||||||
local wrapped="not installed"
|
local wrapped="not installed"
|
||||||
local wrapper_path="${APPS[$app]}"
|
local wrapper_path="${APPS[$app]}"
|
||||||
if [[ -f "${wrapper_path}.orig" ]]; then
|
if [[ -f "${wrapper_path}.orig" ]]; then
|
||||||
wrapped="wrapped"
|
wrapped="wrapped"
|
||||||
elif [[ -f $wrapper_path ]]; then
|
elif [[ -f $wrapper_path ]]; then
|
||||||
wrapped="installed (not wrapped)"
|
wrapped="installed (not wrapped)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
printf " %s %-15s [%s] - %s\n" "$icon" "$app" "$wrapped" "$status"
|
printf " %s %-15s [%s] - %s\n" "$icon" "$app" "$wrapped" "$status"
|
||||||
done
|
done
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "State directory: $STATE_DIR"
|
echo "State directory: $STATE_DIR"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Reset state for an app (allow opening again)
|
# Reset state for an app (allow opening again)
|
||||||
reset_app() {
|
reset_app() {
|
||||||
local app="$1"
|
local app="$1"
|
||||||
local state_file
|
local state_file
|
||||||
state_file=$(get_state_file "$app")
|
state_file=$(get_state_file "$app")
|
||||||
|
|
||||||
if [[ -f $state_file ]]; then
|
if [[ -f $state_file ]]; then
|
||||||
rm -f "$state_file"
|
rm -f "$state_file"
|
||||||
echo "Reset $app - can be opened again this hour"
|
echo "Reset $app - can be opened again this hour"
|
||||||
log_message "RESET: $app state cleared by user"
|
log_message "RESET: $app state cleared by user"
|
||||||
else
|
else
|
||||||
echo "$app was not marked as opened"
|
echo "$app was not marked as opened"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# Clear all state
|
# Clear all state
|
||||||
reset_all() {
|
reset_all() {
|
||||||
ensure_state_dir
|
ensure_state_dir
|
||||||
rm -f "$STATE_DIR"/*.lastopen
|
rm -f "$STATE_DIR"/*.lastopen
|
||||||
echo "All apps reset - can be opened again this hour"
|
echo "All apps reset - can be opened again this hour"
|
||||||
log_message "RESET: All app states cleared by user"
|
log_message "RESET: All app states cleared by user"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Show usage
|
# Show usage
|
||||||
show_usage() {
|
show_usage() {
|
||||||
cat << EOF
|
cat <<EOF
|
||||||
Block Compulsive Opening Script
|
Block Compulsive Opening Script
|
||||||
================================
|
================================
|
||||||
|
|
||||||
@ -447,60 +563,60 @@ EOF
|
|||||||
|
|
||||||
# Main entry point
|
# Main entry point
|
||||||
main() {
|
main() {
|
||||||
case "${1:-help}" in
|
case "${1:-help}" in
|
||||||
install)
|
install)
|
||||||
if [[ $EUID -ne 0 ]]; then
|
if [[ $EUID -ne 0 ]]; then
|
||||||
echo "Error: install requires root privileges"
|
echo "Error: install requires root privileges"
|
||||||
echo "Run: sudo $0 install"
|
echo "Run: sudo $0 install"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
install_all
|
install_all
|
||||||
;;
|
;;
|
||||||
uninstall)
|
uninstall)
|
||||||
if [[ $EUID -ne 0 ]]; then
|
if [[ $EUID -ne 0 ]]; then
|
||||||
echo "Error: uninstall requires root privileges"
|
echo "Error: uninstall requires root privileges"
|
||||||
echo "Run: sudo $0 uninstall"
|
echo "Run: sudo $0 uninstall"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
uninstall_all
|
uninstall_all
|
||||||
;;
|
;;
|
||||||
status)
|
status)
|
||||||
show_status
|
show_status
|
||||||
;;
|
;;
|
||||||
reset)
|
reset)
|
||||||
if [[ -z ${2:-} ]]; then
|
if [[ -z ${2:-} ]]; then
|
||||||
echo "Error: specify app to reset"
|
echo "Error: specify app to reset"
|
||||||
echo "Apps: ${!APPS[*]}"
|
echo "Apps: ${!APPS[*]}"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
reset_app "$2"
|
reset_app "$2"
|
||||||
;;
|
;;
|
||||||
reset-all)
|
reset-all)
|
||||||
reset_all
|
reset_all
|
||||||
;;
|
;;
|
||||||
rewrap-quiet)
|
rewrap-quiet)
|
||||||
# Called by pacman hook - quietly re-wrap apps after package updates
|
# Called by pacman hook - quietly re-wrap apps after package updates
|
||||||
if [[ $EUID -ne 0 ]]; then
|
if [[ $EUID -ne 0 ]]; then
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
rewrap_quiet
|
rewrap_quiet
|
||||||
;;
|
;;
|
||||||
wrapper)
|
wrapper)
|
||||||
if [[ -z ${2:-} ]]; then
|
if [[ -z ${2:-} ]]; then
|
||||||
echo "Error: wrapper requires app name"
|
echo "Error: wrapper requires app name"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
wrapper_main "${@:2}"
|
wrapper_main "${@:2}"
|
||||||
;;
|
;;
|
||||||
help | -h | --help)
|
help | -h | --help)
|
||||||
show_usage
|
show_usage
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
echo "Unknown command: $1"
|
echo "Unknown command: $1"
|
||||||
show_usage
|
show_usage
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
main "$@"
|
main "$@"
|
||||||
|
|||||||
275
scripts/digital_wellbeing/focus_mode_daemon.py
Executable file
275
scripts/digital_wellbeing/focus_mode_daemon.py
Executable 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()
|
||||||
211
scripts/digital_wellbeing/install_focus_mode_daemon.sh
Executable file
211
scripts/digital_wellbeing/install_focus_mode_daemon.sh
Executable 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
|
||||||
282
scripts/digital_wellbeing/pacman/README_FOR_LLM.md
Normal file
282
scripts/digital_wellbeing/pacman/README_FOR_LLM.md
Normal 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
|
||||||
@ -53,4 +53,8 @@ netsurf
|
|||||||
amfora
|
amfora
|
||||||
tartube
|
tartube
|
||||||
youtube
|
youtube
|
||||||
virtualbox
|
# Chrome/Chromium variants
|
||||||
|
google-chrome
|
||||||
|
chromium
|
||||||
|
ungoogled-chromium
|
||||||
|
thorium
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
# Packages matching any of these substrings require a challenge to install.
|
# Packages matching any of these substrings require a challenge to install.
|
||||||
# They will also be uninstalled if found already installed.
|
# They will also be uninstalled if found already installed.
|
||||||
# Lines starting with # are comments.
|
# Lines starting with # are comments.
|
||||||
virtualbox
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
22
scripts/fixes/fix_waifu2x.sh
Executable file
22
scripts/fixes/fix_waifu2x.sh
Executable 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
8
test_results.log
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
==========================================
|
||||||
|
Security Hardening Test Suite
|
||||||
|
==========================================
|
||||||
|
|
||||||
|
Testing components in: /home/kuhy/linux-configuration
|
||||||
|
|
||||||
|
--- HOSTS GUARD ---
|
||||||
|
[0;32m✅ PASS[0m: /etc/hosts is immutable
|
||||||
385
tests/test_security_hardening.sh
Executable file
385
tests/test_security_hardening.sh
Executable 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
|
||||||
Loading…
Reference in New Issue
Block a user