Confirmed live: install_frigate()'s fresh-install path overwrote a working, hand-crafted docker-compose.yml (Frigate + mosquitto + frigate-notify) with zero backup, because that file's shape didn't match what frigate.sh's own "existing install" detection knew how to recognize. Every service's own detection is a judgment call about what counts as "already installed" and can miss a real setup built outside this repo's conventions. lib/common.sh gains backup_if_exists(FILE) — copies FILE to FILE.bak.<timestamp> if it exists, no-ops otherwise (including DRY_RUN). Applied before every service's own `cat > docker-compose.yml`/`cat > .env` write across all 60 services that do one (115 call sites), plus a matching standalone-mode stub added to every service's own bootstrap block, same convention already used for port_in_use/find_free_port. This doesn't replace a service's own update/fresh-reinstall detection — it's the safety net underneath it, so a wrong detection costs a .bak file to restore from instead of the original silently disappearing. Also fixes the actual gap that surfaced this: services/frigate.sh's Authelia offer only checked for Authelia installed locally on Frigate's own box, which is never true for a dedicated NVR box with no local Caddy either (the common shape — Caddy lives elsewhere, snippet-generation mode already handles that). Now offers Authelia protection unconditionally and, when Authelia isn't local, asks whether it lives on the same machine as Caddy (still "import authelia", since that's local to wherever Caddy ends up) or on a genuinely separate third machine (the explicit header-pinned forward_auth form, per CLAUDE.md's "forward_auth to a remote Authelia" note, needed because a bare authelia:9091 shortcut only works one hop).
236 lines
9.3 KiB
Bash
236 lines
9.3 KiB
Bash
#!/bin/bash
|
|
# services/watchyourlan.sh — WatchYourLAN network device tracker.
|
|
# Part of the modular post-install system (sourced by setup.sh).
|
|
#
|
|
# Can also be run standalone on any machine:
|
|
# sudo bash watchyourlan.sh
|
|
# (Docker must already be installed when run standalone)
|
|
#
|
|
# Continuously scans the network for connected devices, tracks history,
|
|
# and can alert on new/unknown devices. Uses network_mode: host so it
|
|
# can see the physical network directly (required for ARP scanning).
|
|
|
|
# ── Standalone bootstrap ──────────────────────────────────────────────────────
|
|
# Detected when the script is executed directly rather than sourced by setup.sh.
|
|
# Sets up helpers and globals, then defers execution until after the function
|
|
# definition at the bottom of this file.
|
|
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
|
[[ "$(id -u)" == "0" ]] || { echo "Run with sudo: sudo bash $0"; exit 1; }
|
|
|
|
_SELF_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
_COMMON="$_SELF_DIR/../lib/common.sh"
|
|
|
|
if [[ -f "$_COMMON" ]]; then
|
|
# Full repo present — use the real helpers (picks up ~/docker/.config too)
|
|
# shellcheck source=../lib/common.sh
|
|
source "$_COMMON"
|
|
else
|
|
# One-off copy — inline minimal stubs so the script works without the repo
|
|
log_info() { echo -e "\033[0;34m[INFO]\033[0m $*"; }
|
|
log_success() { echo -e "\033[0;32m[OK]\033[0m $*"; }
|
|
log_warning() { echo -e "\033[1;33m[WARN]\033[0m $*"; }
|
|
log_error() { echo -e "\033[0;31m[ERROR]\033[0m $*" >&2; }
|
|
|
|
require_docker() {
|
|
command -v docker &>/dev/null || {
|
|
log_error "Docker not found. Install it first:"
|
|
log_error " curl -fsSL https://get.docker.com | sudo sh"
|
|
return 1
|
|
}
|
|
docker compose version &>/dev/null || {
|
|
log_error "Docker Compose plugin missing:"
|
|
log_error " sudo apt-get install -y docker-compose-plugin"
|
|
return 1
|
|
}
|
|
}
|
|
|
|
ensure_docker_dir_ownership() {
|
|
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$@" 2>/dev/null || true
|
|
}
|
|
|
|
# Match common.sh's eval-based pattern so local vars in install_* are set correctly
|
|
prompt_text() {
|
|
local _q="$1" _def="$2" _var="$3" _r
|
|
[[ "${UNATTENDED:-false}" == "true" ]] && { eval "$_var='$_def'"; return; }
|
|
read -r -p " $_q " _r
|
|
eval "$_var='${_r:-$_def}'"
|
|
}
|
|
|
|
prompt_yn() {
|
|
local _q="$1" _def="$2" _var="$3" _r
|
|
[[ "${UNATTENDED:-false}" == "true" ]] && { eval "$_var='$_def'"; return; }
|
|
read -r -p " $_q " _r
|
|
eval "$_var='${_r:-$_def}'"
|
|
}
|
|
|
|
write_readme() {
|
|
local _dir="$1"; shift
|
|
mkdir -p "$_dir"
|
|
cat > "$_dir/README.md"
|
|
}
|
|
backup_if_exists() {
|
|
local _file="$1"
|
|
[ -f "$_file" ] || return 0
|
|
cp -p "$_file" "${_file}.bak.$(date +%Y%m%d-%H%M%S)" 2>/dev/null
|
|
}
|
|
fi
|
|
|
|
# Globals — ACTUAL_USER/ACTUAL_HOME must come before DOCKER_DIR
|
|
# ($HOME under sudo is /root, not the real user's home)
|
|
ACTUAL_USER="${ACTUAL_USER:-${SUDO_USER:-$USER}}"
|
|
ACTUAL_HOME="$(getent passwd "$ACTUAL_USER" 2>/dev/null | cut -d: -f6 || echo "${HOME:-/root}")"
|
|
DOCKER_DIR="${DOCKER_DIR:-$ACTUAL_HOME/docker}"
|
|
DRY_RUN="${DRY_RUN:-false}"
|
|
UNATTENDED="${UNATTENDED:-false}"
|
|
SITE_TZ="${SITE_TZ:-$(cat /etc/timezone 2>/dev/null || echo UTC)}"
|
|
SITE_DOMAIN="${SITE_DOMAIN:-example.com}"
|
|
SITE_CADDY_NET="${SITE_CADDY_NET:-caddy_net}"
|
|
CADDY_REMOTE_HOST="${CADDY_REMOTE_HOST:-}"
|
|
|
|
register_service() { :; } # no-op — no wizard to register into
|
|
_RUN_STANDALONE=1
|
|
fi
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
register_service watchyourlan utilities "Network device tracker (WatchYourLAN)" 8840
|
|
|
|
install_watchyourlan() {
|
|
require_docker || return 1
|
|
log_info "Installing WatchYourLAN..."
|
|
local WYL_DIR="$DOCKER_DIR/watchyourlan"
|
|
|
|
if [ "$DRY_RUN" = true ]; then
|
|
echo "[DRY-RUN] Would create $WYL_DIR (watchyourlan_data/)"
|
|
echo "[DRY-RUN] Would deploy aceberg/watchyourlan:latest (network_mode: host)"
|
|
echo "[DRY-RUN] Port 8840 on host, needs network interface name"
|
|
return 0
|
|
fi
|
|
|
|
mkdir -p "$WYL_DIR/watchyourlan_data"
|
|
ensure_docker_dir_ownership "$WYL_DIR"
|
|
cd "$WYL_DIR" || return 1
|
|
|
|
local TZ_VAL="${SITE_TZ:-$(cat /etc/timezone 2>/dev/null || echo UTC)}"
|
|
|
|
# Auto-detect primary network interface
|
|
local DEFAULT_IFACE
|
|
DEFAULT_IFACE=$(ip route show default 2>/dev/null | awk '/default/ {print $5; exit}')
|
|
[ -z "$DEFAULT_IFACE" ] && DEFAULT_IFACE="eth0"
|
|
|
|
echo ""
|
|
echo " WatchYourLAN needs to know which network interface to scan."
|
|
echo " Your detected primary interface: $DEFAULT_IFACE"
|
|
echo ""
|
|
ip link show 2>/dev/null | awk -F: '/^[0-9]+: / && !/lo/ {gsub(/ /,"",$2); print " •", $2}' || true
|
|
echo ""
|
|
local SCAN_IFACE=""
|
|
prompt_text "Network interface to scan:" "$DEFAULT_IFACE" SCAN_IFACE
|
|
[ -z "$SCAN_IFACE" ] && SCAN_IFACE="$DEFAULT_IFACE"
|
|
|
|
local GUI_PORT="8840"
|
|
prompt_text "GUI port [8840]:" "8840" GUI_PORT
|
|
[ -z "$GUI_PORT" ] && GUI_PORT="8840"
|
|
|
|
backup_if_exists docker-compose.yml
|
|
cat > docker-compose.yml << 'WYL_COMPOSE'
|
|
name: watchyourlan
|
|
|
|
services:
|
|
watchyourlan:
|
|
image: aceberg/watchyourlan:latest
|
|
container_name: watchyourlan
|
|
hostname: watchyourlan
|
|
restart: unless-stopped
|
|
network_mode: host
|
|
env_file: .env
|
|
volumes:
|
|
- ./watchyourlan_data:/data
|
|
WYL_COMPOSE
|
|
|
|
backup_if_exists .env
|
|
cat > .env << WYL_ENV
|
|
# ── General ───────────────────────────────────────────────────────────────────
|
|
TZ=$TZ_VAL
|
|
|
|
# ── WatchYourLAN ──────────────────────────────────────────────────────────────
|
|
# Network interface to scan (ARP scanning requires the physical interface)
|
|
IFACE=$SCAN_IFACE
|
|
|
|
# GUI bind address and port (network_mode: host — binds directly to the host)
|
|
GUIIP=0.0.0.0
|
|
GUIPORT=$GUI_PORT
|
|
|
|
# Web UI theme (darkly, cosmo, lumen, sandstone, etc.)
|
|
THEME=darkly
|
|
WYL_ENV
|
|
|
|
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$WYL_DIR"
|
|
log_success "WatchYourLAN configured at $WYL_DIR"
|
|
|
|
# WatchYourLAN uses network_mode: host, so Caddy container-name routing
|
|
# can't reach it via caddy_net regardless of where Caddy runs. Access is
|
|
# directly on host port $GUI_PORT. Mirrors configure_caddy_for_service's
|
|
# own mode resolution (lib/common.sh) purely to word this note correctly.
|
|
local _CADDY_MODE="${CADDY_MODE:-none}"
|
|
[ "$_CADDY_MODE" = "none" ] && [ -d "$DOCKER_DIR/caddy" ] && _CADDY_MODE="local"
|
|
[ "$_CADDY_MODE" = "none" ] && [ -n "${CADDY_REMOTE_HOST:-}" ] && _CADDY_MODE="remote"
|
|
if [ "$_CADDY_MODE" != "none" ]; then
|
|
echo ""
|
|
log_info "Note: WatchYourLAN uses host networking (needed for ARP scanning)."
|
|
log_info "It cannot join caddy_net. To put it behind Caddy, add this block manually:"
|
|
echo ""
|
|
echo " yourdomain.com {"
|
|
echo " reverse_proxy <HOST_IP>:$GUI_PORT"
|
|
echo " }"
|
|
echo ""
|
|
if [ "$_CADDY_MODE" = "remote" ]; then
|
|
echo " where HOST_IP is this server's real network IP (Caddy is on a different box)."
|
|
else
|
|
echo " where HOST_IP is this server's IP on the Docker bridge (usually 172.17.0.1)."
|
|
fi
|
|
fi
|
|
|
|
write_readme "$WYL_DIR" << MD
|
|
# WatchYourLAN — network device tracker
|
|
|
|
Scans the network continuously for connected devices, tracks history,
|
|
and alerts on new or unknown devices joining the network.
|
|
|
|
## Access
|
|
- URL: http://localhost:$GUI_PORT (or http://<server-ip>:$GUI_PORT from LAN)
|
|
|
|
## Scanning interface
|
|
Configured to scan: **$SCAN_IFACE**
|
|
Change \`IFACE\` in .env and restart if you need to scan a different interface.
|
|
|
|
## Network mode note
|
|
WatchYourLAN uses \`network_mode: host\` to see real ARP traffic.
|
|
This means it cannot be added to caddy_net for reverse proxy via container name.
|
|
To put it behind Caddy, use the host's IP directly in the Caddyfile:
|
|
\`reverse_proxy 172.17.0.1:$GUI_PORT\` (adjust IP to your Docker bridge gateway).
|
|
|
|
## Manage
|
|
\`\`\`bash
|
|
cd $WYL_DIR
|
|
docker compose up -d # start
|
|
docker compose down # stop
|
|
docker compose logs -f # logs
|
|
docker compose pull && docker compose up -d # update
|
|
\`\`\`
|
|
MD
|
|
|
|
local START_WYL=""
|
|
prompt_yn "Start WatchYourLAN now? (y/n):" "y" START_WYL
|
|
if [ "$START_WYL" = "y" ] || [ "$START_WYL" = "Y" ]; then
|
|
docker compose up -d \
|
|
&& log_success "WatchYourLAN started" \
|
|
|| log_warning "Failed to start — check: docker compose logs"
|
|
fi
|
|
|
|
echo " Access at: http://localhost:$GUI_PORT"
|
|
echo " Scanning: interface $SCAN_IFACE"
|
|
echo ""
|
|
}
|
|
|
|
[[ "${_RUN_STANDALONE:-0}" == 1 ]] && install_watchyourlan
|