I am working on a script that would run PiHole directly on an asus-merlin router without needing the extra hardware of the pi.
Requirements
an asus router with asus-merlin and entware installed
Here is an overview of what the script does
- create a second virtual bridge device with it's own ip
- install all necessary entware dependencies
- configure dnsmasq to log queries
- configure lighttpd and bind to the above bridge
- checkout required pihole files
- make necessary fixes to run entirely in /opt
- lots of router specific tweeks and workarounds
Here's the script...
#!/bin/sh
# Pi-hole: A black hole for Internet advertisements
# (c) 2015, 2016 by Jacob Salmela
# Network-wide ad blocking via your Raspberry Pi
# http://pi-hole.net
# Installs Pi-hole
#
# Pi-hole is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
# piholeDebug.sh is not installed. It does not work because dnsmasq is in wierd places
# updateDashboard.sh is not installed. To update pihole and dashboard use this script.
# setupLCD.sh is not installed
#Set this to an IP different than your router and not in your dhcp range
IPHOLE="192.168.1.254"
#Set this to your tz
TZ="America/Los_Angeles"
spinner()
{
local pid=$1
local delay="1s"
local spinstr='/-\|'
while [ "$(ps | awk '{print $1}' | grep "$pid")" ]; do
local temp=${spinstr#?}
printf " [%c] " "$spinstr"
local spinstr=${temp}${spinstr%"$temp"}
sleep ${delay}
printf "\b\b\b\b\b\b"
done
printf " \b\b\b\b"
}
installDependencies() {
echo ":::"
echo "::: Installing Dependencies"
PIHOLE_DEPS=(
bc bash curl git git-http sed rsync perl coreutils-mktemp coreutils-truncate net-tools-hostname
php5-fastcgi php5-mod-json php5-mod-openssl php5-mod-session
lighttpd lighttpd-mod-fastcgi lighttpd-mod-access lighttpd-mod-accesslog lighttpd-mod-expire
lighttpd-mod-compress lighttpd-mod-redirect lighttpd-mod-rewrite lighttpd-mod-setenv
)
for i in "${PIHOLE_DEPS[@]}"; do
opkg install "$i"
done
echo "!!! done."
}
createBridge() {
echo ":::"
echo "::: Creating Bridge Interface"
FILE=/jffs/scripts/services-start
touch "$FILE"
chmod +x "$FILE"
grep -q "$IPHOLE" "$FILE" || echo '
#Setup bridge for PiHole
ifconfig br0:1 '$IPHOLE' netmask 255.255.255.0 up
' >> "$FILE"
source "$FILE"
echo "!!! done."
}
setupPhpTZ() {
echo ":::"
echo "::: Setup php.ini with correct TZ..."
#TZ=$(cut -f1 -d "," /opt/etc/TZ)
sed -i "s|.*date.timezone.*|date.timezone = $TZ|" /opt/etc/php.ini
echo "!!! done."
}
setupDnsmasq() {
echo ":::"
echo "::: Creating dnsmasq configuration"
FILE=/jffs/configs/dnsmasq.conf.add
touch "$FILE"
grep -q "pihole" "$FILE" || echo '
# Set dnsmasq configs for PiHole
log-queries
log-async
log-facility=/opt/var/log/pihole.log
addn-hosts=/opt/etc/pihole/gravity.list
' >> "$FILE"
service restart_dnsmasq >> /dev/null
echo "!!! done."
}
setupLighttpd() {
echo ":::"
echo "::: Creating lighttpd configuration"
#Setup cache dir for compress
mkdir -p /tmp/lighttpd/compress
sed -i 's|cache_dir|"/tmp/lighttpd"|g' /opt/etc/lighttpd/conf.d/30-compress.conf
FILE=/opt/etc/lighttpd/conf.d/40-pihole.conf
touch "$FILE"
grep -q "pihole" "$FILE" || echo '
server.bind = "'$IPHOLE'"
server.error-handler-404 = "pihole/index.html"
accesslog.filename = "/opt/var/log/lighttpd/access.log"
accesslog.format = "%{%s}t|%V|%r|%s|%b"
fastcgi.server = (
".php" =>
( "localhost" =>
( "socket" => "/tmp/php-fcgi.sock",
"bin-path" => "/opt/bin/php-fcgi",
"max-procs" => 1,
"bin-environment" =>
( "PHP_FCGI_CHILDREN" => "2",
"PHP_FCGI_MAX_REQUESTS" => "1000"
)
)
)
)
# If the URL starts with /admin, it is the Web interface
$HTTP["url"] =~ "^/admin/" {
# Create a response header for debugging using curl -I
setenv.add-response-header = (
"X-Pi-hole" => "The Pi-hole Web interface is working!",
"X-Frame-Options" => "DENY"
)
}
# If the URL does not start with /admin, then it is a query for an ad domain
$HTTP["url"] =~ "^(?!/admin)/.*" {
# Create a response header for debugging using curl -I
setenv.add-response-header = ( "X-Pi-hole" => "A black hole for Internet advertisements." )
# rewrite only js requests
url.rewrite = ("(.*).js" => "pihole/index.js")
}
$HTTP["host"] =~ "ads.hulu.com|ads-v-darwin.hulu.com|ads-e-darwin.hulu.com" {
url.redirect = ( "^/published/(.*)" => "http://192.168.1.1:8200/MediaItems/pi-hole.mov")
}
' >> "$FILE"
/opt/etc/init.d/S80lighttpd restart
echo "!!! done."
}
webInterfaceGitUrl="https://github.com/pi-hole/AdminLTE.git"
webInterfaceDir="/opt/etc/.pihole_admin"
piholeGitUrl="https://github.com/pi-hole/pi-hole.git"
piholeFilesDir="/opt/etc/.pihole"
getGitFiles() {
# Setup git repos for base files and web admin
echo ":::"
echo "::: Checking for existing base files..."
if is_repo ${piholeFilesDir}; then
make_repo ${piholeFilesDir} ${piholeGitUrl}
else
update_repo ${piholeFilesDir}
fi
echo ":::"
echo "::: Checking for existing web interface..."
if is_repo ${webInterfaceDir}; then
make_repo ${webInterfaceDir} ${webInterfaceGitUrl}
else
update_repo ${webInterfaceDir}
fi
}
is_repo() {
# If the directory does not have a .git folder it is not a repo
echo -n "::: Checking $1 is a repo..."
if [ -d "$1/.git" ]; then
echo " OK!"
return 1
fi
echo " not found!!"
return 0
}
make_repo() {
# Remove the non-repod interface and clone the interface
echo -n "::: Cloning $2 into $1..."
rm -rf "$1"
git clone -q "$2" "$1" > /dev/null & spinner $!
echo " done!"
}
update_repo() {
# Pull the latest commits
echo -n "::: Updating repo in $1..."
cd "$1" || exit
git pull -q > /dev/null & spinner $!
echo " done!"
}
installScripts() {
# Install the scripts from /opt/etc/.pihole to their various locations
echo ":::"
echo "::: Installing scripts to /opt/pihole..."
mkdir -p /opt/pihole
cp /opt/etc/.pihole/pihole /opt/pihole/pihole
cp /opt/etc/.pihole/gravity.sh /opt/pihole/gravity.sh
cp /opt/etc/.pihole/advanced/Scripts/chronometer.sh /opt/pihole/chronometer.sh
cp /opt/etc/.pihole/advanced/Scripts/whitelist.sh /opt/pihole/whitelist.sh
cp /opt/etc/.pihole/advanced/Scripts/blacklist.sh /opt/pihole/blacklist.sh
cp /opt/etc/.pihole/advanced/Scripts/whitelist.sh /opt/pihole/version.sh
#make everything executable
chmod +x /opt/pihole/*.sh
#everything in /etc is actually in /opt/etc
sed -i 's|/etc|/opt/etc|g' /opt/pihole/*
#everything in /var is actually in /opt/var
sed -i 's|/var|/opt/var|g' /opt/pihole/*
#everything in /var/www/html is actually in /opt/share/www
sed -i 's|/var/www/html|/opt/share/www|g' /opt/pihole/*
#bash is in /opt/bin/bash
sed -i 's|/bin/bash|/opt/bin/bash|g' /opt/pihole/*
#in the gravity.sh script don't run gravity_reload function. It doesn't work with our dnsmasq setup
sed -i 's|^gravity_reload|s|^#|' /opt/pihole/gravity.sh
#instead just restart dnsmasq
echo 'service restart_dnsmasq' >> gravity.sh
#remove functionality from pihole that does not work correctly
sed -i 's|-ud, updateDashboard|s|^#|' /opt/pihole/pihole
sed -i 's|-up, updatePihole|s|^#|' /opt/pihole/pihole
sed -i 's|-s, setupLCD|s|^#|' /opt/pihole/pihole
sed -i 's|-d, debug|s|^#|' /opt/pihole/pihole
#create a working piholeLogflush.sh
#instead of just wiping the log completely keep the last hour of data
#then we will set a cron to run this every 5 minutes (should be fine, grep is plenty fast)
echo '
#!/usr/bin/env bash
echo -n "::: Flushing hour old data from /var/log/pihole.log ..."
grep "^$(date -d -1hour +'%b %d %H:%M:%S')" /opt/var/log/pihole.log > /opt/var/log/pihole.tmp
mv /opt/var/log/pihole.tmp /opt/var/log/pihole.log
echo "... done!"
' > /opt/pihole/piholeLogFlush.sh
#link pihole to something in our path
ln -sf /opt/pihole/pihole /opt/usr/sbin/pihole
echo "!!! done."
}
installAdmin() {
echo ":::"
echo -n "::: Installing Admin to /opt/share/www/admin..."
mkdir -p /opt/share/www/admin
rsync -a --exclude=".git*" /opt/etc/.pihole_admin/ /opt/share/www/admin/ > /dev/null & spinner $!
#everything in /etc is actually in /opt/etc
find /opt/share/www/admin/ -type f -exec sed -i -e 's|/etc|/opt/etc|g' {} \; > /dev/null & spinner $!
#everything in /var is actually in /opt/var
find /opt/share/www/admin/ -type f -exec sed -i -e 's|/var|/opt/var|g' {} \; > /dev/null & spinner $!
#fix bug in script data.php
sed -i '|function getAllQueries() {|a\\$status = "" ' /opt/share/www/admin/data.php
#enable php debug
echo 'error_reporting = -1\ndisplay_errors = On\nhtml_errors = On\n' >> /opt/share/www/admin/.user.ini
echo "!!! done."
}
installPiholeMov() {
echo ":::"
echo -n "::: Installing pi-hole movie..."
curl -s -o /opt/pihole/pi-hole.mov http://jacobsalmela.com/wp-content/uploads/2014/10/pi-hole.mov > /dev/null & spinner $!
echo " done."
}
createPiholeIpFile() {
echo ":::"
echo "::: Create PiHole Ip file..."
mkdir -p /opt/etc/pihole
echo "$IPHOLE" > /opt/etc/pihole/piholeIP
echo "!!! done."
}
createDummyHostnameFile() {
echo ":::"
echo "::: Create dummy Host file..."
echo "pi.hole" > /opt/etc/hostname
echo "!!! done."
}
createLogFile() {
# Create logfiles if necessary
echo ":::"
echo -n "::: Creating log file and changing owner to nobody..."
if [ ! -f /opt/var/log/pihole.log ]; then
touch /opt/var/log/pihole.log
chmod 640 /opt/var/log/pihole.log
chown nobody:root /var/log/pihole.log
echo " done!"
else
echo " already exists!"
fi
}
installPiholeWeb() {
# Install the web interface
echo ":::"
echo "::: Installing pihole custom index page..."
mkdir -p /opt/share/www/pihole
cp /opt/etc/.pihole/advanced/index.* /opt/share/www/pihole/.
echo "!!! done"
}
installCron() {
echo ":::"
echo "::: Installing Cron Jobs"
FILE=/jffs/scripts/init-start
touch "$FILE"
chmod +x "$FILE"
grep -q "pihole" "$FILE" || echo '
# Pi-hole: Update the ad sources once a week on Sunday at 01:59
cru a UpdateGravity "59 1 * * 7 /opt/pihole/pihole updateGravity"
# Pi-hole: Flush the log every 5 minutes, keep an hours worth of data
# Log gets too big for our puny memory
cru a FlushLog "*/5 * * * * /opt/pihole/pihole flush"
' >> "$FILE"
echo "!!! done."
}
runGravity() {
# Rub gravity.sh to build blacklists
echo ":::"
echo "::: Preparing to run gravity.sh to refresh hosts..."
if ls /opt/etc/pihole/list* 1> /dev/null 2>&1; then
echo "::: Cleaning up previous install (preserving whitelist/blacklist)"
rm /opt/etc/pihole/list.*
fi
echo "::: Running gravity.sh"
/opt/pihole/gravity.sh
}
installPiHole() {
installDependencies
createBridge
setupPhpTZ
setupDnsmasq
setupLighttpd
getGitFiles
installScripts
installAdmin
installPiholeMov
createPiholeIpFile
createDummyHostnameFile
createLogFile
installPiholeWeb
installCron
runGravity
echo "::: View the web interface at http://pi.hole/admin or http://$IP/admin"
}
updatePihole() {
installDependencies
getGitFiles
installScripts
installAdmin
installPiholeMov
installPiholeWeb
runGravity
}
function helpFunc {
echo "::: Install PiHole!"
echo ":::"
echo "::: Options:"
echo "::: -i, install"
echo "::: -u, update"
exit 1
}
if [[ $# = 0 ]]; then
helpFunc
fi
# Handle redirecting to specific functions based on arguments
case "$1" in
"-i" | "install" ) installPiHole;;
"-u" | "install" ) updatePiHole;;
* ) helpFunc;;
esac
Here are some notes on what works and what doesn't
- piholeDebug.sh does not work because dnsmasq is in wierd places
- the dns log is set to just age off stuff older than an hour instead of completely flushing the log. I have to keep the log at only an hour because these routers just can't handle the memory required to process huge logs. This might get better if they move the admin to use a db as has been discussed before.