@@ -9,7 +9,14 @@ set -euo pipefail
99readonly UNBOUND_PORT=5335
1010readonly NETALERTX_PORT=20211
1111readonly PYTHON_SUITE_PORT=8090
12+ readonly NETALERTX_IMAGE=" jokobsk/netalertx:latest"
13+ readonly SUITE_API_KEY_ENV=" ${SUITE_API_KEY:- } "
14+ readonly SUITE_LOG_LEVEL_ENV=" ${SUITE_LOG_LEVEL:- INFO} "
15+ readonly INSTALL_USER=${SUDO_USER:- $(whoami)}
1216readonly PROJECT_DIR=" $( pwd) "
17+ readonly RESOLV_CONF=" /etc/resolv.conf"
18+ readonly RESOLV_CONF_BACKUP=" /etc/resolv.conf.pi-hole-installer.bak"
19+ readonly -a FALLBACK_RESOLVERS=(" 1.1.1.1" " 9.9.9.9" )
1320
1421# 🎨 Colors
1522RED=' \033[0;31m' ; GREEN=' \033[0;32m' ; YELLOW=' \033[1;33m' ; BLUE=' \033[0;34m' ; NC=' \033[0m'
@@ -22,7 +29,24 @@ error() { echo -e "${RED}[✗]${NC} $*"; }
2229step () { echo -e " \n${YELLOW} [STEP]${NC} $* " ; }
2330
2431# 🛡️ Error handler
25- trap ' error "Installation failed. See logs above."' ERR
32+ trap ' error "Installation failed. See logs above."; exit 1' ERR
33+
34+ # ---------------------------------------------------------------------------
35+ # 🧪 Helpers
36+ write_resolv_conf () {
37+ local file=" $1 " ; shift
38+ : > " $file "
39+ for resolver in " $@ " ; do
40+ printf ' nameserver %s\n' " $resolver " >> " $file "
41+ done
42+ }
43+
44+ extract_env_value () {
45+ local key=" $1 " file=" $2 "
46+ if [[ -f " $file " ]]; then
47+ grep -E " ^${key} =" " $file " | tail -n1 | cut -d= -f2-
48+ fi
49+ }
2650
2751# ---------------------------------------------------------------------------
2852# 🔍 System checks
@@ -34,24 +58,72 @@ check_system() {
3458 success " System checks passed"
3559}
3660
61+ # 🧰 Ubuntu resolver handling (Port 53)
62+ handle_systemd_resolved () {
63+ step " Checking systemd-resolved on Ubuntu (port 53)"
64+ if [[ -r /etc/os-release ]]; then
65+ # shellcheck disable=SC1091
66+ . /etc/os-release
67+ if [[ " ${ID:- } " == ubuntu* || " ${ID_LIKE:- } " == * ubuntu* ]]; then
68+ if systemctl list-unit-files | grep -q ' ^systemd-resolved\.service' ; then
69+ if systemctl is-active --quiet systemd-resolved; then
70+ warn " systemd-resolved is active; stopping to free port 53"
71+ systemctl stop systemd-resolved || true
72+ fi
73+ systemctl disable systemd-resolved || true
74+ if [[ -L $RESOLV_CONF ]]; then
75+ warn " $RESOLV_CONF is a symlink; replacing with static resolver"
76+ if [[ ! -e $RESOLV_CONF_BACKUP ]]; then
77+ mv -f " $RESOLV_CONF " " $RESOLV_CONF_BACKUP "
78+ else
79+ rm -f " $RESOLV_CONF "
80+ fi
81+ fi
82+ write_resolv_conf " $RESOLV_CONF " " ${FALLBACK_RESOLVERS[@]} "
83+ fi
84+ fi
85+ fi
86+ success " Resolver prepared with external fallbacks"
87+ }
88+
89+ finalize_resolver_configuration () {
90+ log " Pointing system resolver to Pi-hole"
91+ local resolvers=(" 127.0.0.1" " ${FALLBACK_RESOLVERS[@]} " )
92+ write_resolv_conf " $RESOLV_CONF " " ${resolvers[@]} "
93+ success " System resolver now prefers Pi-hole on 127.0.0.1"
94+ }
95+
96+ # 🔌 Port conflicts
3797check_ports () {
3898 step " Checking ports"
3999 local ports=($UNBOUND_PORT $NETALERTX_PORT $PYTHON_SUITE_PORT 53)
40- for port in " ${ports[@]} " ; do
41- if ss -tuln | grep -q " :$port " ; then
42- warn " Port $port already in use"
43- fi
44- done
100+ if command -v ss > /dev/null; then
101+ for port in " ${ports[@]} " ; do
102+ if ss -tuln | grep -q " :$port " ; then
103+ warn " Port $port already in use"
104+ fi
105+ done
106+ elif command -v netstat > /dev/null; then
107+ for port in " ${ports[@]} " ; do
108+ if netstat -tuln | grep -q " :$port " ; then
109+ warn " Port $port already in use"
110+ fi
111+ done
112+ else
113+ warn " Neither ss nor netstat available; skipping port checks"
114+ fi
45115}
46116
47117# 📦 Packages
48118install_packages () {
49119 step " Installing system packages"
50120 apt-get update -qq
51- python3 python3-venv python3-pip git docker.io openssl systemd sqlite3
121+ apt-get install -y unbound ca-certificates curl dnsutils \
122+ python3 python3-venv python3-pip git docker.io openssl systemd sqlite3 iproute2
52123 success " System packages installed"
53124}
54125
126+ # 🔐 Unbound config
55127configure_unbound () {
56128 step " Configuring Unbound"
57129 install -d -m 0755 /var/lib/unbound
@@ -74,6 +146,7 @@ server:
74146 cache-max-ttl: 86400
75147 trust-anchor-file: /var/lib/unbound/root.key
76148 root-hints: /var/lib/unbound/root.hints
149+ tls-cert-bundle: /etc/ssl/certs/ca-certificates.crt
77150
78151forward-zone:
79152 name: "."
@@ -100,6 +173,7 @@ install_pihole() {
100173 sed -i " s/^PIHOLE_DNS_1=.*/PIHOLE_DNS_1=127.0.0.1#$UNBOUND_PORT /" /etc/pihole/setupVars.conf
101174 sed -i " s/^PIHOLE_DNS_2=.*/PIHOLE_DNS_2=/" /etc/pihole/setupVars.conf
102175 pihole restartdns
176+ finalize_resolver_configuration
103177 success " Pi-hole configured with Unbound"
104178}
105179
@@ -122,12 +196,33 @@ install_netalertx() {
122196setup_python_suite () {
123197 step " Setting up Python suite"
124198 cd " $PROJECT_DIR "
199+ # Ensure data directory exists and is writable by service user
200+ install -d -m 0755 " $PROJECT_DIR /data"
201+ chown -R " $INSTALL_USER :$INSTALL_USER " " $PROJECT_DIR /data"
202+
125203 [[ -d .venv ]] || sudo -u " $INSTALL_USER " python3 -m venv .venv
126204 sudo -u " $INSTALL_USER " .venv/bin/pip install -U pip
127205 sudo -u " $INSTALL_USER " .venv/bin/pip install -r requirements.txt
128206 sudo -u " $INSTALL_USER " .venv/bin/python scripts/bootstrap.py || true
129207
130- cat > /etc/systemd/system/pihole-suite.service << EOF
208+ local env_file=" $PROJECT_DIR /.env"
209+ local suite_api_key=" $SUITE_API_KEY_ENV "
210+ if [[ -z " $suite_api_key " ]]; then
211+ suite_api_key=" $( extract_env_value " SUITE_API_KEY" " $env_file " ) "
212+ fi
213+ if [[ -z " $suite_api_key " ]]; then
214+ suite_api_key=" $( openssl rand -hex 16) "
215+ fi
216+
217+ cat > " $env_file " << ENV
218+ SUITE_API_KEY=$suite_api_key
219+ SUITE_PORT=$PYTHON_SUITE_PORT
220+ SUITE_DATA_DIR=$PROJECT_DIR /data
221+ SUITE_LOG_LEVEL=$SUITE_LOG_LEVEL_ENV
222+ ENV
223+ chown " $INSTALL_USER :$INSTALL_USER " " $env_file "
224+
225+ cat > /etc/systemd/system/pihole-suite.service << SERVICE_EOF
131226[Unit]
132227Description=Pi-hole Suite (API + monitoring)
133228After=network.target pihole-FTL.service
@@ -147,18 +242,16 @@ RestartSec=5
147242NoNewPrivileges=yes
148243ProtectSystem=strict
149244ProtectHome=yes
245+ ProtectKernelTunables=yes
246+ ProtectKernelModules=yes
247+ ProtectControlGroups=yes
150248PrivateTmp=yes
249+ ReadWritePaths=$PROJECT_DIR $PROJECT_DIR /data
151250
152251[Install]
153252WantedBy=multi-user.target
154- EOF
253+ SERVICE_EOF
155254
156- cat > .env << ENV
157- SUITE_API_KEY=$SUITE_API_KEY
158- SUITE_PORT=$PYTHON_SUITE_PORT
159- SUITE_DATA_DIR=$PROJECT_DIR /data
160- SUITE_LOG_LEVEL=${SUITE_LOG_LEVEL:- INFO}
161- ENV
162255 systemctl daemon-reload
163256 systemctl enable --now pihole-suite.service
164257 success " Python suite running on :$PYTHON_SUITE_PORT "
168261run_health_checks () {
169262 step " Running health checks"
170263 dig +short @127.0.0.1 -p $UNBOUND_PORT example.com | grep -q " ." && success " Unbound OK" || error " Unbound FAIL"
264+ pihole status | grep -iEq " blocking.+enabled|enabled" && success " Pi-hole OK" || warn " Pi-hole status unclear"
171265 docker ps | grep -q netalertx && success " NetAlertX OK" || warn " NetAlertX missing"
172266 systemctl is-active --quiet pihole-suite && success " Python suite OK" || warn " Python suite not active"
173267}
@@ -179,7 +273,7 @@ show_summary() {
179273 echo " Pi-hole admin: http://$( hostname -I | awk ' {print $1}' ) /admin"
180274 echo " NetAlertX: http://$( hostname -I | awk ' {print $1}' ) :$NETALERTX_PORT "
181275 echo " Python Suite: http://127.0.0.1:$PYTHON_SUITE_PORT "
182- echo " API Key: $SUITE_API_KEY "
276+ echo " API Key: $( extract_env_value " SUITE_API_KEY" " $PROJECT_DIR /.env " ) "
183277}
184278
185279# 🚀 Main
0 commit comments