public inbox for development@lists.ipfire.org
 help / color / mirror / Atom feed
From: Jon Murphy <jon.murphy@ipfire.org>
To: development@lists.ipfire.org
Subject: [PATCH] RPZ: update code to include WEBGUI and additional languages
Date: Thu, 06 Feb 2025 10:35:22 -0600	[thread overview]
Message-ID: <20250206163522.2363178-1-jon.murphy@ipfire.org> (raw)

[-- Attachment #1: Type: text/plain, Size: 89678 bytes --]

What is it?
Response Policy Zone (RPZ) is a mechanism to define local policies in a
standardized way and load those policies from external sources.
Bottom line: RPZ allows admins to easily block access to websites via DNS lookup.

RPZ can block websites via categories.  Examples include: fake websites, annoying
pop-up ads, newly registered domains, DoH bypass sites, bad "host" services,
maliscious top level domains (e.g., *.zip, *.mov), piracy, gambling, pornography,
and more.  RPZ lists come from various RPZ providers and their available
catagories.

This RPZ add-on enables the RPZ functionality by adding a couple lines in a
configuration file.  This add-on simply adds configuration files and adds
scripts (config, metrics and sleep) to make RPZ easier for the admin to use.

The RPZ scripts include additional languages: German, Spanish, French, Turkish,
and Italian.

RPZ itself was release in 2010 and has been part of the IPFire build since ~2015.

Why is it needed?  What is its value?

 - The RPZ concept places this filtering into IPFire, our internet access
gateway, which is (should be) solely used as DNS source of the internal network.

 - As most sites use HTTPS it makes it difficult to filter traffic with URL
Filter without also properly configuring conventional (non-transparent)
mode on the proxy.  RPZ is a nice replacement for the URL Filter.

 - No need to install and maintain an additional device like PiHole or AdBlock
browser extensions on multiple user devices.

 - This is an additional layer of protection for users. Less worry someone will
click on something that gets them into trouble. And, saying this with emphasis,
the ability to do it in one place!

 - Blocked sites save on unneeded traffic and can lessen the threat of malware
in advertisements

 - Logging allows the admin to see the site blocked and take actions

 - RPZ will be used at the home, home-office (work from home), schools,
ministerial, and at the office.  Device counts are small (2-6) to medium (~80)
to mediam-large (200+).

 - RPZ can block ads, popups, phishing, scammers, spyware, malware, annoying
popups, NSFW links, DOH servers, and the usual internet trash.

------------------------------

Change Log for RPZ add-on

rpz-1.0.0-18 on 2025-02-05
 - Build for approval & release as IPFire add-on

---

rpz-beta-0.1.18-18.ipfire on 2025-02-01
rpz.cgi:
 - new feature: added a mod key to force a unbound restart

rpz-config and rpz-make:
 - new feature: added action for unbound restart `rpz-config unbound-restart`

rpz-metrics:
 - simple reformatting
 - rename far right column from "last update" to "last download"

---

rpz-beta-0.1.17-17.ipfire on 2024-12-09
rpz-make
 - bug fix: corrected validation regex for wildcards like: `*.domain.com`

---

rpz-beta-0.1.16-16.ipfire on 2024-11-18
rpz-make
 - new feature: updated validation regex
 - bug fix: moved validation to beginning of process.  Now we validate before
creating config files.

rpz.cgi:
 - new feature: use CSS color variables of the main ipfire theme
 - bug fix: empty zonefile remarks were stored as “undef” and caused a warning
 - bug fix: HTML textarea removes the first empty line in a custom list
 - thank you Leo!

---

rpz-beta-0.1.15-15.ipfire on 2024-11-04
rpz.cgi:
 - new feature: added new language file for Turkish (thank you Peppe)

rpz-make
 - bug fix: corrected empty allow/block list issue.  An empty allow/block list
will now remove contents of allow/block.rpz files and remove unneeded
allow/block.conf file.  (thank you iptom)

---

rpz-beta-0.1.14-14.ipfire on  2024-10-29
rpz-config:
 - bug fix: correct missing rpz extension. `rpz-config list` displayed URL
incorrectly (thank you Bernhard)

rpz.cgi:
 - bug fix: remove extra `"` in language files (thank you Bernhard)
 - new feature: slightly dim "apply" button when not enabled

---

rpz-beta-0.1.13-13.ipfire on 2024-10-27
 - skipped

---

rpz-beta-0.1.12-12.ipfire on 2024-10-21
rpz.cgi:
 - new feature: added new language file for French  (thank you gw-ipfire)

---

rpz-beta-0.1.11-11.ipfire on 2024-10-18
rpz.cgi:
- new feature: added new language file for Italian (thank you umberto)
- new feature: added new language file for Spanish (thank you Roberto)

---

rpz-beta-0.1.10-10.ipfire on 2024-10-15
rpz-make:
 - bug fix: corrected validation error for a custom list entry (thank you siosios)
   - e.g., `*.cloudflare-dns.com`

install.sh:
 - bug fix: add chown to correct user created files

update.sh:
 - bug fix: add chown to correct user created files (thank you siosios)

---

rpz-beta-0.1.9-9.ipfire on 2024-10-08
rpz.cgi:
 - new feature: added new language file for German (thank you Leo)
 - bug fix: add missing "rpz exitcode 110"
 - bug fix: corrected missing RPZ menu item at menu > IPFire

---

rpz-beta-0.1.8-8.ipfire on 2024-10-04
 - skipped

---

rpz-beta-0.1.7-7.ipfire on 2024-10-03
All:
 - new feature: includes beta version numbers for pakfire package,
instead of only `rpz-1.0.0-1.ipfire`, for each release.

rpz.cgi:
 - new feature: added new WebGUI at `rpz.cgi`
    - a BIG thank you to Leo Hofmann for all of his work creating the webgui!!
 - bug fix: corrected missing RPZ menu item at menu > IPFire

rpz-make:
 - new feature: validate entries in allowlist and blocklist
 - new feature: add "no-reload" option for WebGUI

rpz-metrics:
 - new feature: info can be sorted by name, by hit count, by line count, by
"enabled" list or all lists

backups:
 - bug fix: include all files in `/var/ipfire/dns/rpz` directory in backup

update.sh:
 - bug fix: corrected ownership for `/var/ipfire/dns/rpz` directory during an
update

Build:
 - bug fix: `block.rpz.conf` and `block.rpz` from build.  Files to be created
by `rpz-make`

WebGUI and German language file
Contribution-by: Leo-Andres Hofmann <hofmann(a)leo-andres.de>

Spanish language file
Contribution-by: Roberto Peña

Italian language file
Contribution-by: Umberto Parma

French language file
Contribution-by: gw-ipfire

Turkish language file
Contribution-by: Peppe Tech

Contribution-by: Bernhard Bitsch <bbitsch(a)ipfire.org>
Contribution-by: Erik Kapfer <erik.kapfer(a)ipfire.org>
Signed-off-by: Jon Murphy <jon.murphy(a)ipfire.org
---
 config/backup/includes/rpz                 |   4 +
 config/cfgroot/manualpages                 |   1 +
 config/menu/EX-rpz.menu                    |   6 +
 config/rootfiles/common/configroot         |   1 +
 config/rootfiles/common/web-user-interface |   1 +
 config/rootfiles/packages/rpz              |  20 +
 config/rpz/00-rpz.conf                     |  10 +
 config/rpz/rpz-config                      | 130 +++
 config/rpz/rpz-functions                   |  85 ++
 config/rpz/rpz-make                        | 203 +++++
 config/rpz/rpz-metrics                     | 170 ++++
 config/rpz/rpz-sleep                       |  58 ++
 config/rpz/rpz.de.pl                       |  30 +
 config/rpz/rpz.en.pl                       |  30 +
 config/rpz/rpz.es.pl                       |  30 +
 config/rpz/rpz.fr.pl                       |  30 +
 config/rpz/rpz.it.pl                       |  30 +
 config/rpz/rpz.tr.pl                       |  30 +
 html/cgi-bin/rpz.cgi                       | 923 +++++++++++++++++++++
 lfs/rpz                                    |  96 +++
 make.sh                                    |   3 +-
 src/paks/rpz/install.sh                    |  36 +
 src/paks/rpz/uninstall.sh                  |  38 +
 src/paks/rpz/update.sh                     |  52 ++
 24 files changed, 2016 insertions(+), 1 deletion(-)
 create mode 100644 config/backup/includes/rpz
 create mode 100644 config/menu/EX-rpz.menu
 create mode 100644 config/rootfiles/packages/rpz
 create mode 100644 config/rpz/00-rpz.conf
 create mode 100644 config/rpz/rpz-config
 create mode 100644 config/rpz/rpz-functions
 create mode 100644 config/rpz/rpz-make
 create mode 100755 config/rpz/rpz-metrics
 create mode 100755 config/rpz/rpz-sleep
 create mode 100644 config/rpz/rpz.de.pl
 create mode 100644 config/rpz/rpz.en.pl
 create mode 100644 config/rpz/rpz.es.pl
 create mode 100644 config/rpz/rpz.fr.pl
 create mode 100644 config/rpz/rpz.it.pl
 create mode 100644 config/rpz/rpz.tr.pl
 create mode 100644 html/cgi-bin/rpz.cgi
 create mode 100644 lfs/rpz
 create mode 100644 src/paks/rpz/install.sh
 create mode 100644 src/paks/rpz/uninstall.sh
 create mode 100644 src/paks/rpz/update.sh

diff --git a/config/backup/includes/rpz b/config/backup/includes/rpz
new file mode 100644
index 000000000..36513e494
--- /dev/null
+++ b/config/backup/includes/rpz
@@ -0,0 +1,4 @@
+/var/ipfire/dns/rpz/*
+/etc/unbound/zonefiles/allow.rpz
+/etc/unbound/zonefiles/block.rpz
+/etc/unbound/local.d/*rpz.conf
diff --git a/config/cfgroot/manualpages b/config/cfgroot/manualpages
index 1f7e01efc..d3a48c633 100644
--- a/config/cfgroot/manualpages
+++ b/config/cfgroot/manualpages
@@ -70,6 +70,7 @@ pakfire.cgi=configuration/ipfire/pakfire
 wlanap.cgi=addons/wireless
 tor.cgi=addons/tor
 samba.cgi=addons/samba
+rpz.cgi=addons/rpz
 
 #	Logs menu
 logs.cgi/summary.dat=configuration/logs/summary
diff --git a/config/menu/EX-rpz.menu b/config/menu/EX-rpz.menu
new file mode 100644
index 000000000..2f4daf410
--- /dev/null
+++ b/config/menu/EX-rpz.menu
@@ -0,0 +1,6 @@
+$subipfire->{'20.rpz'} = {
+    'caption' => $Lang::tr{'rpz'},
+    'uri' => '/cgi-bin/rpz.cgi',
+    'title' => "RPZ",
+    'enabled' => 1,
+};
diff --git a/config/rootfiles/common/configroot b/config/rootfiles/common/configroot
index 9839eee45..b30d6aae4 100644
--- a/config/rootfiles/common/configroot
+++ b/config/rootfiles/common/configroot
@@ -120,6 +120,7 @@ var/ipfire/menu.d/70-log.menu
 #var/ipfire/menu.d/EX-apcupsd.menu
 #var/ipfire/menu.d/EX-guardian.menu
 #var/ipfire/menu.d/EX-mympd.menu
+#var/ipfire/menu.d/EX-rpz.menu
 #var/ipfire/menu.d/EX-samba.menu
 #var/ipfire/menu.d/EX-tor.menu
 #var/ipfire/menu.d/EX-transmission.menu
diff --git a/config/rootfiles/common/web-user-interface b/config/rootfiles/common/web-user-interface
index 816241dae..e00464076 100644
--- a/config/rootfiles/common/web-user-interface
+++ b/config/rootfiles/common/web-user-interface
@@ -69,6 +69,7 @@ srv/web/ipfire/cgi-bin/proxy.cgi
 srv/web/ipfire/cgi-bin/qos.cgi
 srv/web/ipfire/cgi-bin/remote.cgi
 srv/web/ipfire/cgi-bin/routing.cgi
+#srv/web/ipfire/cgi-bin/rpz.cgi
 #srv/web/ipfire/cgi-bin/samba.cgi
 srv/web/ipfire/cgi-bin/services.cgi
 srv/web/ipfire/cgi-bin/shutdown.cgi
diff --git a/config/rootfiles/packages/rpz b/config/rootfiles/packages/rpz
new file mode 100644
index 000000000..1c8663049
--- /dev/null
+++ b/config/rootfiles/packages/rpz
@@ -0,0 +1,20 @@
+etc/unbound/local.d/00-rpz.conf
+etc/unbound/zonefiles
+etc/unbound/zonefiles/allow.rpz
+usr/sbin/rpz-config
+usr/sbin/rpz-functions
+usr/sbin/rpz-make
+usr/sbin/rpz-metrics
+usr/sbin/rpz-sleep
+var/ipfire/addon-lang/rpz.de.pl
+var/ipfire/addon-lang/rpz.en.pl
+var/ipfire/addon-lang/rpz.es.pl
+var/ipfire/addon-lang/rpz.fr.pl
+var/ipfire/addon-lang/rpz.it.pl
+var/ipfire/addon-lang/rpz.tr.pl
+var/ipfire/backup/addons/includes/rpz
+var/ipfire/dns/rpz
+var/ipfire/dns/rpz/allowlist
+var/ipfire/dns/rpz/blocklist
+var/ipfire/menu.d/EX-rpz.menu
+srv/web/ipfire/cgi-bin/rpz.cgi
diff --git a/config/rpz/00-rpz.conf b/config/rpz/00-rpz.conf
new file mode 100644
index 000000000..f005a4f2e
--- /dev/null
+++ b/config/rpz/00-rpz.conf
@@ -0,0 +1,10 @@
+server:
+    module-config: "respip validator iterator"
+
+rpz:
+    name:                    allow.rpz
+    zonefile:                /etc/unbound/zonefiles/allow.rpz
+    rpz-action-override:     passthru
+    rpz-log:                 yes
+    rpz-log-name:            allow
+    rpz-signal-nxdomain-ra:  yes
diff --git a/config/rpz/rpz-config b/config/rpz/rpz-config
new file mode 100644
index 000000000..c72d50f9b
--- /dev/null
+++ b/config/rpz/rpz-config
@@ -0,0 +1,130 @@
+#!/bin/bash
+###############################################################################
+#                                                                             #
+#  IPFire.org - A linux based firewall                                        #
+#  Copyright (C) 2024-2025  IPFire Team  <info(a)ipfire.org>                    #
+#                                                                             #
+#  This program 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 3 of the License, or          #
+#  (at your option) any later version.                                        #
+#                                                                             #
+#  This program is distributed in the hope that it will be useful,            #
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of             #
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the              #
+#  GNU General Public License for more details.                               #
+#                                                                             #
+#  You should have received a copy of the GNU General Public License          #
+#  along with this program.  If not, see <http://www.gnu.org/licenses/>.      #
+#                                                                             #
+###############################################################################
+
+version="2025-01-11 - v44"
+
+###############     Functions     ###############
+
+source /usr/sbin/rpz-functions
+
+###############       Main        ###############
+
+tagName="unbound"
+
+rpzAction="${1}"                    #  input RPZ action
+rpzName="${2}"                      #  input RPZ name
+rpzURL="${3}"                       #  input RPZ URL
+rpzOption1="${4}"                   #  input RPZ option #1
+rpzOption2="${5}"                   #  input RPZ option #2
+
+rpzConfig="/etc/unbound/local.d/${rpzName}.rpz.conf"    #  output zone conf file
+rpzFile="/etc/unbound/zonefiles/${rpzName}.rpz"         #  output for RPZ file
+
+rpzLog="yes"                        #  log default is yes
+ucReload="yes"                      #  reload default is yes
+
+while [[ $# -gt 0 ]] ; do
+    case "$1" in
+        --no-log )      rpzLog="no"   ;;
+        --no-reload )   ucReload="no" ; checkConf="no" ;;
+    esac
+    shift       # Shift after checking all the cases to get next option
+done
+
+case "${rpzAction}" in
+    #  add new rpz list
+    add )
+        check_name "${rpzName}"             #  is this a valid name?
+        #  does this config already exist?  If yes, then exit
+        if [[ -f "${rpzConfig}" ]] ; then
+            msg_log "error: rpz: duplicate - ${rpzConfig} already exists. exit"
+            exit 104
+        fi
+
+        #  is this a valid URL?
+        regex='^https://[-[:alnum:]\+&@#/%?=~_|!:,.;]*[-[:alnum:]\+&@#/%=~_|]'
+        if ! [[ "${rpzURL}" =~ $regex ]] ; then
+            msg_log "error: rpz: the URL is not valid: \"${rpzURL}\". exit."
+            exit 105
+        fi
+
+        #  create the zone config file
+        {
+        echo "rpz:"
+        echo "    name:                   ${rpzName}.rpz"
+        echo "    zonefile:               ${rpzFile}"
+        echo "    url:                    ${rpzURL}"
+        echo "    rpz-action-override:    nxdomain"
+        echo "    rpz-log:                ${rpzLog}"
+        echo "    rpz-log-name:           ${rpzName}"
+        echo "    rpz-signal-nxdomain-ra: yes"
+        } > "${rpzConfig}"
+
+        #  set-up zonefile
+        #    create an empty rpz file if it does not exist
+        if [[ ! -f "${rpzFile}" ]] ; then
+            touch "${rpzFile}"
+            #  unbound requires these settings for rpz files
+            set_permissions "${rpzFile}" "${rpzConfig}"
+        fi
+        ;;
+
+    #  trash config file & rpz file
+    remove )
+        if ! [[ -f "${rpzConfig}" ]] ; then
+            msg_log "error: rpz: cannot remove ${rpzConfig}, does not exist. exit"
+            exit 106
+        fi
+
+        msg_log "info: rpz: remove config file & rpz file \"${rpzName}\""
+        rm "${rpzConfig}"
+        rm "${rpzFile}"
+        ;;
+
+    reload )
+        check_unbound_conf "${checkConf}"
+        ;;
+
+    list )
+        awk -F':' '/^\s*name:/{ gsub(/[[:blank:]]|\.rpz/, "",$2) ; NAME=$2 } \
+            /^\s*url:/{ gsub(/[[:blank:]]/, "") ; print NAME"="$2":"$3} '  \
+            /etc/unbound/local.d/*rpz.conf
+        exit
+        ;;
+
+    unbound-restart )
+        check_unbound_conf "${checkConf}"
+        unbound_restart
+        exit
+        ;;
+
+    * )
+        msg_log "error: rpz: missing or incorrect parameter"
+        printf "Usage:   $(basename "$0") <ACTION> <NAME> <URL> <OPTION> <OPTION>\n"
+        printf "Version: ${version}\n"
+        exit 108
+        ;;
+
+esac
+
+unbound_control_reload "${ucReload}"
+
+exit
diff --git a/config/rpz/rpz-functions b/config/rpz/rpz-functions
new file mode 100644
index 000000000..ace1d2690
--- /dev/null
+++ b/config/rpz/rpz-functions
@@ -0,0 +1,85 @@
+#!/bin/bash
+###############################################################################
+#                                                                             #
+#  IPFire.org - A linux based firewall                                        #
+#  Copyright (C) 2024  IPFire Team  <info(a)ipfire.org>                         #
+#                                                                             #
+#  This program 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 3 of the License, or          #
+#  (at your option) any later version.                                        #
+#                                                                             #
+#  This program is distributed in the hope that it will be useful,            #
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of             #
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the              #
+#  GNU General Public License for more details.                               #
+#                                                                             #
+#  You should have received a copy of the GNU General Public License          #
+#  along with this program.  If not, see <http://www.gnu.org/licenses/>.      #
+#                                                                             #
+###############################################################################
+
+version="2024-12-10 - v02"
+
+###############     Functions     ###############
+
+msg_log () {
+    logger --tag "${tagName}" "$*"
+    if tty --silent ; then
+        echo "${tagName}:" "$*"
+    fi
+}
+
+#  check for a valid name
+check_name () {
+    local theName="${1}"
+
+    regex='^[a-zA-Z0-9_]+$'             # no dash or plus, alpha numeric only
+    regex1='^(allow|block)$'            # allow and block are reserved NAMEs
+    if [[ ! "${theName}" =~ $regex ]] || [[ "${theName}" =~ $regex1 ]] ; then
+        msg_log "error: rpz: the NAME is not valid: \"${theName}\". exit."
+        exit 101
+    fi
+}
+
+set_permissions () {
+    chown nobody:nobody "$@"
+    chmod 644 "$@"
+}
+
+check_unbound_conf () {
+    local thecheckConf="${1:-yes}"      #   check config default is yes
+
+    #  check the above config files
+    if [[ "${thecheckConf}" == yes ]] ; then
+        msg_log "info: rpz: check for errors with \"unbound-checkconf\""
+
+        if ! unbound-checkconf ; then
+            msg_log "error: rpz: unbound-checkconf found invalid configuration."
+            msg_log \
+              "error: rpz: In Terminal run the command \"unbound-checkconf\" for more information. exit."
+            exit 102
+        fi
+    fi
+}
+
+unbound_control_reload () {
+    local theReload="${1:-yes}"     #   reload default is yes
+
+    if [[ "${theReload}" == yes ]] ; then
+        #  reload due to the changes
+        msg_log  "info: rpz: run \"unbound-control reload\""
+
+        if ! unbound-control reload ; then
+            msg_log "error: rpz: unbound-control reload. exit."
+            exit 109
+        fi
+    fi
+}
+
+unbound_restart () {
+    #  restart due to the changes
+    msg_log  "info: rpz: run \"unbound restart\""
+
+    /usr/local/bin/unboundctrl restart
+}
diff --git a/config/rpz/rpz-make b/config/rpz/rpz-make
new file mode 100644
index 000000000..927d55170
--- /dev/null
+++ b/config/rpz/rpz-make
@@ -0,0 +1,203 @@
+#!/bin/bash
+###############################################################################
+#                                                                             #
+#  IPFire.org - A linux based firewall                                        #
+#  Copyright (C) 2024-2025  IPFire Team  <info(a)ipfire.org>                    #
+#                                                                             #
+#  This program 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 3 of the License, or          #
+#  (at your option) any later version.                                        #
+#                                                                             #
+#  This program is distributed in the hope that it will be useful,            #
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of             #
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the              #
+#  GNU General Public License for more details.                               #
+#                                                                             #
+#  You should have received a copy of the GNU General Public License          #
+#  along with this program.  If not, see <http://www.gnu.org/licenses/>.      #
+#                                                                             #
+###############################################################################
+
+version="2025-01-11 - v14"
+
+###############     Functions     ###############
+
+source /usr/sbin/rpz-functions
+
+#   create the config file for allow
+make_allow_config () {
+    local theLog="${1:-yes}"                            #  log default ON
+    local theConfig="/etc/unbound/local.d/00-rpz.conf"  #  output zone conf file
+    local theList="/var/ipfire/dns/rpz/allowlist"       #  input custom list of domains
+
+    msg_log "info: rpz: make config file \"00-rpz.conf\""
+
+    echo "server:
+    module-config: \"respip validator iterator\"" > "${theConfig}"
+
+    #  does allow list exist?
+    if [[ -s "${theList}" ]] && grep -q . "${theList}" ; then
+
+    echo "rpz:
+    name:                   allow.rpz
+    zonefile:               /etc/unbound/zonefiles/allow.rpz
+    rpz-action-override:    passthru
+    rpz-log:                ${theLog}
+    rpz-log-name:           allow
+    rpz-signal-nxdomain-ra: yes" >> "${theConfig}"
+
+    fi
+
+    #  set-up zonefile - unbound requires these settings for rpz files
+    set_permissions "${theConfig}"
+}
+
+#   create the config file for block
+make_block_config () {
+    local theLog="${1:-yes}"                                #  log default ON
+    local theConfig="/etc/unbound/local.d/block.rpz.conf"   #  output zone conf file
+    local theList="/var/ipfire/dns/rpz/blocklist"           #  input custom list of domains
+
+    msg_log "info: rpz: make config file \"block.rpz.conf\""
+
+    #  does block list exist?
+    if [[ -s "${theList}" ]] && grep -q . "${theList}" ; then
+
+    echo "rpz:
+    name:                   block.rpz
+    zonefile:               /etc/unbound/zonefiles/block.rpz
+    rpz-action-override:    nxdomain
+    rpz-log:                ${theLog}
+    rpz-log-name:           block
+    rpz-signal-nxdomain-ra: yes" > "${theConfig}"
+
+        #  set-up zonefile - unbound requires these settings for rpz files
+        set_permissions "${theConfig}"
+    else
+        #   no - trash the config file
+        rm --verbose /etc/unbound/local.d/block.rpz.conf
+    fi
+}
+
+#   create an RPZ file for allow or block
+make_rpz_file () {
+    local theName="${1}"        #   allow or block
+    local theAction='.'         # the default is nxdomain or block
+    local actionList
+
+    local theList="/var/ipfire/dns/rpz/${theName}list"         #  input custom list of domains
+    local theZonefile="/etc/unbound/zonefiles/${theName}.rpz"  #  output file for RPZ
+
+    #  does a list exist?
+    if [[ -s "${theList}" ]] && grep -q . "${theList}" ; then
+
+        # for allow set to passthru
+        [[ "${theName}" == allow ]] && theAction='rpz-passthru.'
+
+        #  drop any extra "blanks" and add "CNAME <RPZ action>." to each line
+        actionList=$( awk '{$1=$1};1' "${theList}" |
+            sed "/^[^;].*[[:alnum:]]/ s|$|  CNAME   ${theAction}|" )
+
+        msg_log "info: rpz: create zonefile for \"${theName}list\""
+
+echo "; Name:             ${theName} list
+; Last modified:    $(date "+%Y-%m-%d at %H.%M.%S %Z")
+;
+;   domains with actions list
+;
+${actionList}" > "${theZonefile}"
+
+        #  set-up zonefile - unbound requires these settings for rpz files
+        set_permissions "${theZonefile}"
+        #  set-up allow/block list files
+        set_permissions "${theList}"
+    else
+        msg_log "info: rpz: the ${theList} is empty."
+
+        rm --verbose "${theZonefile}"   # trash the RPZ file
+    fi
+}
+
+#   check if allow/block list is valid
+validate_list () {
+    local theName="${1}"        #   allow or block
+    local theList="/var/ipfire/dns/rpz/${theName}list"  #  input custom list of domains
+
+	#   remove good:
+	#    - properly formated domain names with or without leading wildcard
+	#    - properly formated top level domain (TLD) names with wildcard
+	#    - blank lines and comment lines
+	#   remaining lines are considered "bad"
+    bad_lines=$( sed --regexp-extended  \
+        '/^(\*\.)?([a-zA-Z0-9](([a-zA-Z0-9\-]){0,61}[a-zA-Z0-9])?\.)+([a-zA-Z]{2,}|xn--[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])$/d ;
+         /^(\*\.)([a-z]{2,61}|xn--[a-z0-9]{1,60})$/d ;
+         /^$/d ; /^;/d' "${theList}" )
+
+    if [[ ! -z "${bad_lines}" ]] ; then
+        msg_log "error: rpz: invalid line(s) in ${theList}."
+        printf "%s\n" "bad line(s): ${bad_lines}"
+        exit 110
+    fi
+}
+
+
+###############       Main        ###############
+
+tagName="unbound"
+
+rpzName="${1}"                      #  input RPZ name
+
+rpzLog="yes"                        #  log default is yes
+ucReload="yes"                      #  reload default is yes
+
+while [[ $# -gt 0 ]] ; do
+    case "$1" in
+        --no-log )      rpzLog="no"   ;;
+        --no-reload )   ucReload="no" ;  checkConf="no" ;;
+    esac
+    shift       # Shift after checking all the cases to get next option
+done
+
+case "${rpzName}" in
+    #  make a new allow or block rpz file
+
+    allow )
+        validate_list 'allow'           #   is the allowlist valid?
+        make_allow_config "${rpzLog}"
+        make_rpz_file 'allow'
+        ;;
+
+    allowblock )
+        validate_list 'allow'           #   is the list valid?
+        make_allow_config "${rpzLog}"
+        make_rpz_file 'allow'
+        ;&
+
+    block )
+        validate_list 'block'           #   is the blocklist valid?
+        make_block_config "${rpzLog}"
+        make_rpz_file 'block'
+        ;;
+
+    reload )
+        check_unbound_conf "${checkConf}"
+        ;;
+
+    unbound-restart )
+        check_unbound_conf "${checkConf}"
+		unbound_restart
+		exit
+        ;;
+
+    * )
+        msg_log "error: rpz: missing or incorrect parameter"
+        printf "Usage:   $(basename "$0") <NAME> <OPTION> <OPTION>\n"
+        printf "Version: ${version}\n"
+        exit 108
+        ;;
+esac
+
+unbound_control_reload "${ucReload}"
+
+exit
diff --git a/config/rpz/rpz-metrics b/config/rpz/rpz-metrics
new file mode 100755
index 000000000..4d43e1629
--- /dev/null
+++ b/config/rpz/rpz-metrics
@@ -0,0 +1,170 @@
+#!/bin/bash
+###############################################################################
+#                                                                             #
+#  IPFire.org - A linux based firewall                                        #
+#  Copyright (C) 2024  IPFire Team  <info(a)ipfire.org>                         #
+#                                                                             #
+#  This program 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 3 of the License, or          #
+#  (at your option) any later version.                                        #
+#                                                                             #
+#  This program is distributed in the hope that it will be useful,            #
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of             #
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the              #
+#  GNU General Public License for more details.                               #
+#                                                                             #
+#  You should have received a copy of the GNU General Public License          #
+#  along with this program.  If not, see <http://www.gnu.org/licenses/>.      #
+#                                                                             #
+###############################################################################
+
+version="2025-01-20 - v25"
+
+###############       Main        ###############
+
+weeks="2"                   #   default to two message logs
+sortBy="name"               #   default "by name"
+rpzActive="enabled"         #   default "enabled only"
+
+while [[ $# -gt 0 ]] ; do
+    case "$1" in
+        --by-names | --by-name | name )				sortBy="name"   ;;
+
+        --by-hits | --by-hit | hits | hit )     	sortBy="hit"    ;;
+
+        --by-lines | --by-line | lines | line )    	sortBy="line"   ;;
+
+        --by-effect )								sortBy="effect"   ;;
+
+        --enabled-only )            				rpzActive="enabled" ;;
+
+        --active-all | --all | all )      			rpzActive="all"     ;;
+
+        [0-9] | [0-9][0-9] )        				weeks=$1        ;;
+    esac
+    shift       # Shift after checking all the cases to get next option
+done
+
+#  get the list of message logs for N weeks
+messageLogs=$( find /var/log/messages* -type f | sort --version-sort |
+    head -"${weeks}" )
+
+#  get the list of RPZ names & counts from the message log(s)
+rpzNameCount=$( for logf in ${messageLogs} ; do
+    zgrep --text --fixed-strings 'info: rpz: applied' "${logf}" |
+      awk '$10 ~ /\[\w*]/ { print $10 }' ;
+    done | sort | uniq --count )
+
+#  flip results and remove brackets `[` and `]`
+rpzNameCount=$( echo "${rpzNameCount}" |
+    awk '{ print $2, $1 }' |
+    sed --regexp-extended 's|^\[(.*)\]|\1|' )
+
+#  grab only names
+rpzNames=$( echo "${rpzNameCount}" | awk '{ print $1 }' )
+
+#  get list of RPZ files
+rpzFileList=$( find /etc/unbound/zonefiles -type f -iname "*.rpz" )
+
+#  get basename of those files
+rpzBaseNames=$( echo "${rpzFileList}" |
+    sed 's|/etc/unbound/zonefiles/||g ; s|\.rpz||g ;' )
+
+#  add to rpzNames
+rpzNames="${rpzNames}"$'\n'"${rpzBaseNames}"
+
+#  drop duplicate names
+rpzNames=$( echo "${rpzNames}" | sort --unique  )
+
+#  get line count for each RPZ
+lineCount=$( echo "${rpzFileList}" | xargs wc -l )
+
+#  get comment line count and blank line count for each RPZ
+commentCount=$( echo "${rpzFileList}" | xargs grep --count -e "^$" -e "^;" )
+
+#  get modified date each RPZ
+modDateList=$( echo "${rpzFileList}" | xargs stat -c '%.10y  %n' )
+
+ucListAuthZones=$( unbound-control list_auth_zones )
+
+#  get width of RPZ names
+pWidth=$( echo "${rpzNames}" | awk '{ print $1"   " }' | wc -L )
+pFormat="%-${pWidth}s %-8s %-8s %8s %12s %12s\n"
+
+#  print title line
+printf "${pFormat}" "name" "hits" "active" "lines" " hits/line" "last download"
+printf -- "--------------"
+
+theResults=""
+totalLines=0
+totalHits=0
+while read -r theName
+do
+    printf -- "--"        #   pretend progress bar
+
+    #  is this RPZ list active?
+    theActive="disabled"
+    if grep --quiet "^${theName}\.rpz" <<< "${ucListAuthZones}"
+    then
+        theActive="enabled"
+    else
+        [[ "${rpzActive}" == enabled ]] && continue
+    fi
+
+    #  get hit count
+    theHits="0"
+    if output=$( grep "^${theName}\s" <<< "${rpzNameCount}" ) ; then
+        theHits=$( echo "${output}" | awk '{ print $2 }' )
+        totalHits=$(( totalHits + theHits ))
+    fi
+
+    #  get line count
+    theLines="n/a"
+    hitsPerLine="0"
+    if output=$( grep --fixed-strings "/${theName}.rpz" <<< "${lineCount}" ) ; then
+        theLines=$( echo "${output}" | awk '{ print $1 }' )
+        totalLines=$(( totalLines + theLines ))
+
+        if [[ "${theLines}" -gt 2 ]] ; then
+            hitsPerLine=$(( 100 * theHits / theLines ))
+        fi
+    fi
+
+    #  get modification date
+    theModDate="n/a"
+    if output=$( grep --fixed-strings "/${theName}.rpz" <<< "${modDateList}" ) ; then
+        theModDate=$( echo "${output}" | awk '{ print $1 }' )
+    fi
+
+    #  add to results list
+    theResults+="${theName} ${theHits} ${theActive} ${theLines} ${hitsPerLine} ${theModDate}"$'\n'
+
+done <<< "${rpzNames}"
+
+case "${sortBy}" in
+    #  sort by "active" then by "name"
+    name) sortArg=(-k3,3r -k1,1)			;;
+
+    #  sort by "active" then by "hits" then by "name"
+    hit)  sortArg=(-k3,3r -k2,2nr -k1,1)	;;
+
+    #  sort by "active" then by "lines" then by "name"
+    line) sortArg=(-k3,3r -k4,4nr -k1,1)	;;
+
+    #  sort by "active" then by "effect" then by "name"
+    effect) sortArg=(-k3,3r -k5,5nr -k1,1)	;;
+esac
+
+printf -- "--------------\n"
+#  remove blank lines, sort, print as columns
+echo "${theResults}" |
+    awk '!/^[[:space:]]*$/' |
+    sort "${sortArg[@]}"    |
+    awk --assign=width="${pWidth}" \
+        '{ printf "%-*s %-8s %-8s %8s %10s %% %12s\n", width, $1, $2, $3, $4, $5, $6 }'
+
+printf "${pFormat}" "" "=======" "" "========" "" ""
+printf "${pFormat}" "Totals -->" "${totalHits}" "" "${totalLines}" "" ""
+
+exit
diff --git a/config/rpz/rpz-sleep b/config/rpz/rpz-sleep
new file mode 100755
index 000000000..dd3603599
--- /dev/null
+++ b/config/rpz/rpz-sleep
@@ -0,0 +1,58 @@
+#!/bin/bash
+###############################################################################
+#                                                                             #
+#  IPFire.org - A linux based firewall                                        #
+#  Copyright (C) 2024  IPFire Team  <info(a)ipfire.org>                         #
+#                                                                             #
+#  This program 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 3 of the License, or          #
+#  (at your option) any later version.                                        #
+#                                                                             #
+#  This program is distributed in the hope that it will be useful,            #
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of             #
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the              #
+#  GNU General Public License for more details.                               #
+#                                                                             #
+#  You should have received a copy of the GNU General Public License          #
+#  along with this program.  If not, see <http://www.gnu.org/licenses/>.      #
+#                                                                             #
+###############################################################################
+
+version="2024-08-16"        # v05
+
+###############     Functions     ###############
+
+#   send message to message log
+msg_log () {
+    logger --tag "${tagName}" "$*"
+    if tty --silent ; then
+        echo "${tagName}:" "$*"
+    fi
+}
+
+###############       Main        ###############
+
+tagName="unbound"
+
+sleepTime="${1:-5m}"            #  default to sleep for 5m (5 minutes)
+
+zoneList=$( unbound-control list_auth_zones | awk '{print $1}' )
+
+for zone in ${zoneList} ; do
+    printf "disable ${zone}\t"
+    unbound-control rpz_disable "${zone}"
+done
+
+msg_log "info: rpz: disabled all zones for ${sleepTime}"
+
+sleep "${sleepTime}"
+
+for zone in ${zoneList} ; do
+    printf "enable ${zone}\t"
+    unbound-control rpz_enable "${zone}"
+done
+
+msg_log "info: rpz: enabled all zones"
+
+exit
diff --git a/config/rpz/rpz.de.pl b/config/rpz/rpz.de.pl
new file mode 100644
index 000000000..3770c6bb0
--- /dev/null
+++ b/config/rpz/rpz.de.pl
@@ -0,0 +1,30 @@
+#  Added for Response Policy Zone (RPZ) add-on
+%tr = (%tr,
+'rpz' => 'Response Policy Zones (RPZ)',
+'rpz apply' => 'Übernehmen',
+'rpz cl allow enable' => 'Benutzerdefinierte Allowlist aktivieren:',
+'rpz cl allow info' => 'Zugelassene Domains (eine pro Zeile)<br>Beispiel: domain.com, *.domain.com',
+'rpz cl allow' => 'Benutzerdefinierte Allowlist',
+'rpz cl block enable' => 'Benutzerdefinierte Blocklist aktivieren:',
+'rpz cl block info' => 'Gesperrte Domains (eine pro Zeile)<br>Beispiel: domain.com, *.domain.com',
+'rpz cl block' => 'Benutzerdefinierte Blocklist',
+'rpz cl' => 'Benutzerdefinierte Listen',
+'rpz exitcode 101' => 'Der Name enthält unzulässige Zeichen',
+'rpz exitcode 102' => 'unbound-checkconf hat eine fehlerhafte Konfiguration ermittelt. Führen Sie das Kommando unbound-checkconf auf der Konsole aus, um weitere Informationen zu erhalten.',
+'rpz exitcode 103' => 'Die benutzerdefinierte Allow-/Blocklist ist leer',
+'rpz exitcode 104' => 'Ein Eintrag mit identischem Namen existiert bereits',
+'rpz exitcode 105' => 'Die URL ist ungültig',
+'rpz exitcode 106' => 'Eintrag kann nicht entfernt werden, der Name existiert nicht',
+'rpz exitcode 107' => 'Der Name ist ungültig - nur "allow" oder "block" möglich',
+'rpz exitcode 108' => 'Fehlende oder inkorrekte Parameter',
+'rpz exitcode 109' => 'unbound-control reload ist fehlgeschlagen',
+'rpz exitcode 110' => 'Die benutzerdefinierte Allow-/Blocklist enthält unzulässige Einträge',
+'rpz exitcode 201' => 'Die Anmerkung enthält unzulässige Zeichen',
+'rpz exitcode 202' => 'Ungültiger Eintrag in der benutzerdefinierten Allowlist, Zeile ',
+'rpz exitcode 203' => 'Ungültiger Eintrag in der benutzerdefinierten Blocklist, Zeile ',
+'rpz exitcode 204' => 'Ausgewählter Eintrag existiert nicht: ',
+'rpz zf editor' => 'Zonendatei-Eintrag bearbeiten',
+'rpz zf imported' => '(importiert aus rpz-config)',
+'rpz zf remark info' => 'Erlaubte Zeichen sind a-z, A-Z, 0-9 und Unterstriche',
+'rpz zf' => 'Zonendateien',
+);
diff --git a/config/rpz/rpz.en.pl b/config/rpz/rpz.en.pl
new file mode 100644
index 000000000..0720a8940
--- /dev/null
+++ b/config/rpz/rpz.en.pl
@@ -0,0 +1,30 @@
+#  Added for Response Policy Zone (RPZ) add-on
+%tr = (%tr,
+'rpz' => 'Response Policy Zones (RPZ)',
+'rpz apply' => 'Apply',
+'rpz cl allow enable' => 'Enable custom allowlist:',
+'rpz cl allow info' => 'Allowed domains (one per line)<br>Example: domain.com, *.domain.com',
+'rpz cl allow' => 'Custom allowlist',
+'rpz cl block enable' => 'Enable custom blocklist:',
+'rpz cl block info' => 'Blocked domains (one per line)<br>Example: domain.com, *.domain.com',
+'rpz cl block' => 'Custom blocklist',
+'rpz cl' => 'Custom lists',
+'rpz exitcode 101' => 'the NAME is not valid',
+'rpz exitcode 102' => 'unbound-checkconf found invalid configuration. In the Terminal run the command unbound-checkconf for more information',
+'rpz exitcode 103' => 'the allow/block list is empty',
+'rpz exitcode 104' => 'duplicate - NAME already exists',
+'rpz exitcode 105' => 'the URL is not valid',
+'rpz exitcode 106' => 'cannot remove the NAME does not exist',
+'rpz exitcode 107' => 'the NAME is not valid - "allow" or "block" only',
+'rpz exitcode 108' => 'missing or incorrect parameter',
+'rpz exitcode 109' => 'unbound-control reload failed',
+'rpz exitcode 110' => 'custom Allowlist/Blocklist contains invalid entries',
+'rpz exitcode 201' => 'the REMARK is not valid',
+'rpz exitcode 202' => 'invalid entry in allowlist, line ',
+'rpz exitcode 203' => 'invalid entry in blocklist, line ',
+'rpz exitcode 204' => 'Selected entry does not exist: ',
+'rpz zf editor' => 'Edit zonefiles entry',
+'rpz zf imported' => '(imported from rpz-config)',
+'rpz zf remark info' => 'Valid characters are a-z, A-Z, 0-9 and underscore.',
+'rpz zf' => 'Zonefiles',
+);
diff --git a/config/rpz/rpz.es.pl b/config/rpz/rpz.es.pl
new file mode 100644
index 000000000..98628e4aa
--- /dev/null
+++ b/config/rpz/rpz.es.pl
@@ -0,0 +1,30 @@
+#  Added for Response Policy Zone (RPZ) add-on
+%tr = (%tr,
+'rpz' => 'Zonas de política de respuesta (RPZ)',
+'rpz apply' => 'Aplicar',
+'rpz cl allow enable' => 'Habilitar la lista blanca personalizada:',
+'rpz cl allow info' => 'Dominio permitido (uno por línea)<br>Ejemplo: domain.com, *.domain.com',
+'rpz cl allow' => 'Lista blanca personalizada',
+'rpz cl block enable' => 'Habilitar la lista negra personalizada:',
+'rpz cl block info' => 'Dominio bloqueado (uno por línea)<br>Ejemplo: domain.com, *.domain.com',
+'rpz cl block' => 'Lista negra personalizada',
+'rpz cl' => 'Lista personalizada',
+'rpz exitcode 101' => 'El NOMBRE no es válido',
+'rpz exitcode 102' => 'unbound-checkconf ha encontrado una configuración no válida. Desde Terminal, ejecute el comando unbound-checkconf para mayor información',
+'rpz exitcode 103' => 'La lista de permitidos/bloqueados está vacía',
+'rpz exitcode 104' => 'duplicado - NOMBRE ya existe',
+'rpz exitcode 105' => 'la URL no es válida',
+'rpz exitcode 106' => 'no es posible eliminar el NOMBRE que no existe',
+'rpz exitcode 107' => 'el NOMBRE no es válido - sólo "permitir" o "bloquear"',
+'rpz exitcode 108' => 'parámetro faltante o incorrecto',
+'rpz exitcode 109' => 'Error al recargar unbound-control',
+'rpz exitcode 110' => 'la Lista blanca/Lista negra personalizada contiene entradas no válidas',
+'rpz exitcode 201' => 'el COMENTARIO no es válido',
+'rpz exitcode 202' => 'entrada no válida en la lista blanca, línea ',
+'rpz exitcode 203' => 'entrada no válida en la lista negra, línea ',
+'rpz exitcode 204' => 'La entrada seleccionada no existe: ',
+'rpz zf editor' => 'Editar la entrada de archivos de zona',
+'rpz zf imported' => '(importado de rpz-config)',
+'rpz zf remark info' => 'Los caracteres válidos son a-z, A-Z, 0-9 y guión bajo',
+'rpz zf' => 'Archivos de zona',
+);
diff --git a/config/rpz/rpz.fr.pl b/config/rpz/rpz.fr.pl
new file mode 100644
index 000000000..f35f3c2d0
--- /dev/null
+++ b/config/rpz/rpz.fr.pl
@@ -0,0 +1,30 @@
+#  Added for Response Policy Zone (RPZ) add-on
+%tr = (%tr,
+'rpz' => 'Response Policy Zones (RPZ)',
+'rpz apply' => 'Appliquer',
+'rpz cl allow enable' => 'Activer la liste d\'autorisations personnalisée:',
+'rpz cl allow info' => 'Domaines autorisés (un par ligne)<br>Example: domain.com, *.domain.com',
+'rpz cl allow' => 'Liste d\'autorisations personnalisée',
+'rpz cl block enable' => 'Activer la liste de blocage personnalisée:',
+'rpz cl block info' => 'Domaines bloqués (un par ligne)<br>Example: domain.com, *.domain.com',
+'rpz cl block' => 'liste de blocage personnalisé',
+'rpz cl' => 'Listes personnalisées',
+'rpz exitcode 101' => 'le NOM n\'est pas valide',
+'rpz exitcode 102' => 'unbound-checkconf configuration non valide trouvée. Dans le terminal, exécutez la commande unbound-checkconf pour plus d\'informations',
+'rpz exitcode 103' => 'la liste autoriser/bloquer est vide',
+'rpz exitcode 104' => 'le NOM existe déjà',
+'rpz exitcode 105' => 'L\'URL n\'est pas valide',
+'rpz exitcode 106' => 'impossible de supprimer le NOM n\'existe pas',
+'rpz exitcode 107' => 'le NOM n\'est pas valide - « autoriser » ou « bloquer » seulement',
+'rpz exitcode 108' => 'paramètre manquant ou incorrect',
+'rpz exitcode 109' => 'unbound-control rechargement échoué',
+'rpz exitcode 110' => 'la liste autoriser/bloquer contient des entrées non valides',
+'rpz exitcode 201' => 'la REMARQUE n\'est pas valable',
+'rpz exitcode 202' => 'entrée non valide dans la liste d\'autorisation, ligne ',
+'rpz exitcode 203' => 'entrée non valide dans la liste de blocs, ligne ',
+'rpz exitcode 204' => 'L\'entrée sélectionnée n\'existe pas: ',
+'rpz zf editor' => 'Modifier l\'entrée Fichiers Zone',
+'rpz zf imported' => '(importé de rpz-config)',
+'rpz zf remark info' => 'Les caractères valides sont a-z, A-Z, 0-9 et soulignement.',
+'rpz zf' => 'Fichiers Zone',
+);
diff --git a/config/rpz/rpz.it.pl b/config/rpz/rpz.it.pl
new file mode 100644
index 000000000..ee81605c9
--- /dev/null
+++ b/config/rpz/rpz.it.pl
@@ -0,0 +1,30 @@
+#  Added for Response Policy Zone (RPZ) add-on
+%tr = (%tr,
+'rpz' => 'Response Policy Zones (RPZ)',
+'rpz apply' => 'Applica',
+'rpz cl allow enable' => 'Abilita la Whitelist personalizzata:',
+'rpz cl allow info' => 'Domini consentiti (uno per riga)<br>Esempio: domain.com, *.domain.com',
+'rpz cl allow' => 'Whitelist personalizzata',
+'rpz cl block enable' => 'Abilita la Blacklist personalizzata:',
+'rpz cl block info' => 'Domini bloccati (uno per riga)<br>Esempio: domain.com, *.domain.com',
+'rpz cl block' => 'Blacklist personalizzata',
+'rpz cl' => 'Liste personalizzate',
+'rpz exitcode 101' => 'il NOME non è valido',
+'rpz exitcode 102' => 'unbound-checkconf ha trovato una configurazione non valida. Dal Terminale esegui il comando unbound-checkconf per maggiori informazioni',
+'rpz exitcode 103' => 'l\'elenco consentiti/bloccati è vuoto',
+'rpz exitcode 104' => 'duplicato - NAME esiste di già',
+'rpz exitcode 105' => 'l\'URL non è valido',
+'rpz exitcode 106' => 'non è possibile rimuovere il NOME non esiste',
+'rpz exitcode 107' => 'il NOME non è valido - solo "consenti" o "blocca"',
+'rpz exitcode 108' => 'parametro mancante o non corretto',
+'rpz exitcode 109' => 'ricaricamento del controllo non associato non riuscito',
+'rpz exitcode 110' => 'la Whitelist/Blacklist personalizzata contiene voci non valide',
+'rpz exitcode 201' => 'l"OSSERVAZIONE non è valida',
+'rpz exitcode 202' => 'voce non valida nella Whitelist, riga ',
+'rpz exitcode 203' => 'voce non valida nella Blacklist, riga ',
+'rpz exitcode 204' => 'La voce selezionata non esiste: ',
+'rpz zf editor' => 'Modifica la voce dei file di zona',
+'rpz zf imported' => '(importato da rpz-config)',
+'rpz zf remark info' => 'I caratteri validi sono a-z, A-Z, 0-9 e trattino basso',
+'rpz zf' => 'Zonefiles',
+);
diff --git a/config/rpz/rpz.tr.pl b/config/rpz/rpz.tr.pl
new file mode 100644
index 000000000..00226e192
--- /dev/null
+++ b/config/rpz/rpz.tr.pl
@@ -0,0 +1,30 @@
+#  I�in eklendi Ayriyeten Response Policy Zone (RPZ)
+%tr = (%tr,
+'rpz' => 'Response Policy Zones (RPZ)',
+'rpz apply' => 'Uygulamak',
+'rpz cl allow enable' => '�zel Etkinlestir allowlist:',
+'rpz cl allow info' => 'Izin verilmis domains (satir basina bir)<br>�rnegin: domain.com, *.domain.com',
+'rpz cl allow' => 'Etkinlestir allowlist',
+'rpz cl block enable' => '�zel Etkinlestir blocklist:',
+'rpz cl block info' => 'Engellenmis domains (satir basina bir)<br>�rnegin: domain.com, *.domain.com',
+'rpz cl block' => 'Engellenmis blocklist',
+'rpz cl' => 'Engellenmis lists',
+'rpz exitcode 101' => 'NAME ge�erli degil',
+'rpz exitcode 102' => 'unbound-checkconf ge�ersiz yapilandirma bulundu. Terminalde daha fazla bilgi i�in Unbound-checkConf komutunu �alistirin',
+'rpz exitcode 103' => 'allow/block liste bos',
+'rpz exitcode 104' => 'kopyalamak - NAME zaten var',
+'rpz exitcode 105' => 'URL ge�erli degil',
+'rpz exitcode 106' => '�ikarilamiyor NAME yok',
+'rpz exitcode 107' => 'NAME ge�erli degil - "allow" veya "block" yalniz',
+'rpz exitcode 108' => 'Parametre eksik veya yanlis',
+'rpz exitcode 109' => 'unbound-control basarisiz',
+'rpz exitcode 110' => 'Engellenmis Allowlist/Blocklist ge�ersiz girisler i�erir',
+'rpz exitcode 201' => 'REMARK ge�erli degil',
+'rpz exitcode 202' => 'Ge�ersiz giris allowlist, line ',
+'rpz exitcode 203' => 'Ge�ersiz giris blocklist, line ',
+'rpz exitcode 204' => 'Se�ilen giris yok: ',
+'rpz zf editor' => 'yazimlamak zonefiles giris',
+'rpz zf imported' => '(ithal edildi rpz-config)',
+'rpz zf remark info' => 'Ge�erli karakterler a-z, A-Z, 0-9ve alt�st.',
+'rpz zf' => 'Zonefiles',
+);
diff --git a/html/cgi-bin/rpz.cgi b/html/cgi-bin/rpz.cgi
new file mode 100644
index 000000000..a821c92ac
--- /dev/null
+++ b/html/cgi-bin/rpz.cgi
@@ -0,0 +1,923 @@
+#!/usr/bin/perl
+###############################################################################
+#                                                                             #
+# IPFire.org - A linux based firewall                                         #
+# Copyright (C) 2005-2024  IPFire Team  <info(a)ipfire.org>                     #
+#                                                                             #
+# This program 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 3 of the License, or           #
+# (at your option) any later version.                                         #
+#                                                                             #
+# This program is distributed in the hope that it will be useful,             #
+# but WITHOUT ANY WARRANTY; without even the implied warranty of              #
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the               #
+# GNU General Public License for more details.                                #
+#                                                                             #
+# You should have received a copy of the GNU General Public License           #
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.       #
+#                                                                             #
+###############################################################################
+
+use strict;
+use Scalar::Util qw(looks_like_number);
+
+# debugging
+#use warnings;
+#use CGI::Carp 'fatalsToBrowser';
+#use Data::Dumper;
+
+require '/var/ipfire/general-functions.pl';
+require "${General::swroot}/lang.pl";
+require "${General::swroot}/header.pl";
+
+###--- Extra HTML ---###
+my $extraHead = <<END
+<style>
+	/* alternating row background */
+	.tbl tr:nth-child(2n+2) {
+		background-color: var(--color-light-grey);
+	}
+	.tbl tr:nth-child(2n+3) {
+		background-color: var(--color-grey);
+	}
+	/* text styles */
+	.tbl th:not(:last-child) {
+		text-align: left;
+	}
+	div.right {
+		text-align: right;
+		margin-top: 0.5em;
+	}
+	/* customlist input */
+	textarea.domainlist {
+		margin: 0.5em 0;
+		resize: vertical;
+		min-height: 10em;
+		overflow: auto;
+		white-space: pre;
+	}
+	button[type=submit]:disabled {
+		opacity: 0.6;
+	}
+</style>
+END
+;
+###--- End of extra HTML ---###
+
+
+### Settings ###
+
+# Request DNS service reload after configuration change
+my $RPZ_RELOAD_FLAG = "${General::swroot}/dns/rpz/reload.flag";
+
+# Configuration file for all available zonefiles
+# Format: index, name (unique), enabled (on/off), URL, remark
+my $ZONEFILES_CONF = "${General::swroot}/dns/rpz/zonefiles.conf";
+
+# Configuration file for custom lists
+# IDs: 0=allowlist, 1=blocklist, 2=options (allow/block enabled)
+my $CUSTOMLISTS_CONF = "${General::swroot}/dns/rpz/customlists.conf";
+
+# Export custom lists to rpz-config
+my $RPZ_ALLOWLIST = "${General::swroot}/dns/rpz/allowlist";
+my $RPZ_BLOCKLIST = "${General::swroot}/dns/rpz/blocklist";
+
+
+### Preparation ###
+
+# Create missing config files
+unless(-f $ZONEFILES_CONF) { &General::system('touch', "$ZONEFILES_CONF"); }
+unless(-f $CUSTOMLISTS_CONF) { &General::system('touch', "$CUSTOMLISTS_CONF"); }
+
+
+## Global gui data
+my $errormessage = "";
+
+## Global configuration data
+my %zonefiles = ();
+my %customlists = ();
+&_zonefiles_load();
+&_customlists_load();
+
+## Global CGI form data
+my %cgiparams = ();
+&Header::getcgihash(\%cgiparams);
+
+my $action = $cgiparams{'ACTION'} // 'NONE';
+my $action_key = $cgiparams{'KEY'} // ''; # entry being edited, empty = none/new
+
+
+###--- Process form actions ---###
+
+# Zonefiles action: Check whether the requested entry exists
+if((substr($action, 0, 3) eq 'ZF_') && ($action_key)) {
+	unless(defined $zonefiles{$action_key}) {
+		$errormessage = &_rpz_error_tr(204, $action_key);
+		$action = 'NONE';
+	}
+}
+
+## Perform actions
+if($action eq 'ZF_SAVE') {				## Save new or modified zonefiles entry
+	if(&_action_zf_save()) {
+		$action = 'NONE'; # success, return to main page
+		&_http_prg_redirect();
+	} else {
+		$action = 'ZF_EDIT'; # error occured, keep editing
+	}
+
+} elsif($action eq 'ZF_TOGGLE') {		## Toggle on/off
+	if(&_action_zf_toggle()) {
+		$action = 'NONE';
+		&_http_prg_redirect();
+	}
+
+} elsif($action eq 'ZF_REMOVE') {		## Remove entry
+	if(&_action_zf_remove()) {
+		$action = 'NONE';
+		&_http_prg_redirect();
+	}
+
+} elsif($action eq 'CL_SAVE') {			## Save custom lists
+	if(&_action_cl_save()) {
+		$action = 'NONE';
+		&_http_prg_redirect();
+	}
+
+} elsif($action eq 'RPZ_RELOAD') {		## Reload dns configuration
+	if(&_action_rpz_reload()) {
+		$action = 'NONE';
+		&_http_prg_redirect();
+	}
+
+} elsif($action eq 'UNB_RESTART') {		## Restart unbound service
+	if(&_action_unb_restart()) {
+		$action = 'NONE';
+		&_http_prg_redirect();
+	}
+
+}
+
+
+###--- Start GUI ---###
+
+## Start http output
+&Header::showhttpheaders();
+
+# Start HTML
+&Header::openpage($Lang::tr{'rpz'}, 1, $extraHead);
+&Header::openbigbox('100%', 'left', '');
+
+# Show error messages
+if($errormessage) {
+	&_print_message($errormessage);
+}
+
+# Handle zonefile add/edit mode
+if($action eq "ZF_EDIT") {
+	&_print_zonefile_editor();
+
+	# Finalize page and exit cleanly
+	&Header::closebigbox();
+	&Header::closepage();
+	exit(0);
+}
+
+# Show gui elements
+&_print_zonefiles();
+&_print_customlists();
+&_print_gui_extras();
+
+&Header::closebigbox();
+&Header::closepage();
+
+###--- End of GUI ---###
+
+
+###--- Internal configuration file functions ---###
+
+# Load all available zonefiles from rpz-config and the internal configuration
+sub _zonefiles_load {
+	# Clean start
+	%zonefiles = ();
+
+	# Source 1: Get the currently enabled zonefiles from rpz-config (expected format [name]=[URL])
+	my @enabled_files = &General::system_output('/usr/sbin/rpz-config', 'list');
+
+	foreach my $row (@enabled_files) {
+		chomp($row);
+
+		# Use regex instead of split() to skip non-matching lines
+		next unless($row =~ /^(\w+)=(.+)$/);
+		my ($name, $url) = ($1, $2);
+
+		# Unique names are already guaranteed by rpz-config
+		if(&_rpz_validate_zonefile($name, $url, '', 0) == 0) {
+			# Populate global data hash, mark all found entries as enabled
+			my %entry = ('enabled' => 'on',
+				'url' => $url,
+				'remark' => $Lang::tr{'rpz zf imported'});
+
+			$zonefiles{$name} = \%entry;
+		}
+	}
+
+	# Source 2: Get additional data and disabled entries from configuration file
+	my %configured_files = ();
+	&General::readhasharray($ZONEFILES_CONF, \%configured_files);
+
+	foreach my $row (values (%configured_files)) {
+		my ($name, $enabled, $url, $remark) = @$row;
+		$remark //= "";
+
+		next unless($name);
+
+		# Check whether this row belongs to an entry already imported from rpz-config
+		if(defined $zonefiles{$name}) {
+			# Existing entry, only merge additional data
+			$zonefiles{$name}{'remark'} = $remark;
+		} else {
+			# Skip entry if it is marked as enabled but not found by rpz-config. It was then deleted manually
+			if($enabled ne 'on') {
+				# Populate global data hash
+				my %entry = ('enabled' => 'off',
+					'url' => $url // "",
+					'remark' => $remark);
+
+				$zonefiles{$name} = \%entry;
+			}
+		}
+	}
+}
+
+# Save internal zonefiles configuration
+sub _zonefiles_save_conf {
+	my $index = 0;
+	my %export = ();
+
+	# Loop trough all zonefiles and create "hasharray" type export
+	foreach my $name (keys %zonefiles) {
+		my @entry = ($name,
+			$zonefiles{$name}{'enabled'},
+			$zonefiles{$name}{'url'},
+			$zonefiles{$name}{'remark'});
+
+		$export{$index++} = \@entry;
+	}
+
+	&General::writehasharray($ZONEFILES_CONF, \%export);
+}
+
+# Load custom lists from rpz-config and the internal configuration
+sub _customlists_load {
+	# Clean start
+	%customlists = ();
+
+	# Load configuration file
+	my %lists_conf = ();
+	&General::readhasharray($CUSTOMLISTS_CONF, \%lists_conf);
+
+	# Get list options, enabled by default to start import
+	$customlists{'allow'}{'enabled'} = $lists_conf{2}[0] // 'on';
+	$customlists{'block'}{'enabled'} = $lists_conf{2}[1] // 'on';
+
+	# Import enabled list from rpz-config, otherwise retrieve stored or empty list from configuration file
+	if($customlists{'allow'}{'enabled'} eq 'on') {
+		&_customlist_import('allow', $RPZ_ALLOWLIST);
+	} else {
+		$customlists{'allow'}{'list'} = $lists_conf{0} // [];
+	}
+	if($customlists{'block'}{'enabled'} eq 'on') {
+		&_customlist_import('block', $RPZ_BLOCKLIST);
+	} else {
+		$customlists{'block'}{'list'} = $lists_conf{1} // [];
+	}
+}
+
+# Save internal custom lists configuration
+sub _customlists_save_conf {
+	my %export = ();
+
+	# Match IDs with import function
+	$export{0} = $customlists{'allow'}{'list'};
+	$export{1} = $customlists{'block'}{'list'};
+	$export{2} = [$customlists{'allow'}{'enabled'}, $customlists{'block'}{'enabled'}];
+
+	&General::writehasharray($CUSTOMLISTS_CONF, \%export);
+}
+
+# Import a custom list from plain file, returns empty list if file is missing
+sub _customlist_import {
+	my ($listname, $filename) = @_;
+	my @list = ();
+
+	# File exists, load and check all lines
+	if(-f $filename) {
+		open(my $FH, '<', $filename) or die "Can't read $filename: $!";
+		while(my $line = <$FH>) {
+			chomp($line);
+			push(@list, $line);
+		}
+		close($FH);
+
+		# Clean up imported data
+		&_rpz_validate_customlist(\@list, 1);
+	}
+
+	$customlists{$listname}{'list'} = \@list;
+}
+
+# Export a custom list to plain file or clear file if list is disabled
+sub _customlist_export {
+	my ($listname, $filename) = @_;
+	return unless(defined $customlists{$listname});
+
+	# Write enabled domain list to file, otherwise save empty file
+	open(my $FH, '>', $filename) or die "Can't write $filename: $!";
+
+	if($customlists{$listname}{'enabled'} eq 'on') {
+		foreach my $line (@{$customlists{$listname}{'list'}}) {
+			print $FH "$line\n";
+		}
+	} else {
+		print $FH "; Note: This list is currently disabled by $ENV{'SCRIPT_NAME'}\n";
+	}
+
+	close($FH);
+}
+
+
+###--- Internal gui functions ---###
+
+# Show simple message box
+sub _print_message {
+	my ($message, $title) = @_;
+	$title ||= $Lang::tr{'error messages'};
+
+	&Header::openbox('100%', 'left', $title);
+	print "<span>$message</span>";
+	&Header::closebox();
+}
+
+# Show all zone files and related gui elements
+sub _print_zonefiles {
+	&Header::openbox('100%', 'left', $Lang::tr{'rpz zf'});
+
+	print <<END
+<table class="tbl" width="100%">
+	<tr>
+		<th>$Lang::tr{'name'}</th>
+		<th>URL</th>
+		<th>$Lang::tr{'remark'}</th>
+		<th colspan="3">$Lang::tr{'action'}</th>
+	</tr>
+END
+;
+
+	# Sort zonefiles by name and loop trough all entries
+	foreach my $name (sort keys %zonefiles) {
+
+		# Toggle button label translation
+		my $toggle_tr = ($zonefiles{$name}{'enabled'} eq 'on') ? $Lang::tr{'click to disable'} : $Lang::tr{'click to enable'};
+
+		print <<END
+	<tr>
+		<td>$name</td>
+		<td>$zonefiles{$name}{'url'}</td>
+		<td>$zonefiles{$name}{'remark'}</td>
+
+		<td align="center" width="5%">
+			<form method="post" action="$ENV{'SCRIPT_NAME'}">
+				<input type="hidden" name="KEY" value="$name">
+				<input type="hidden" name="ACTION" value="ZF_TOGGLE">
+				<input type="image" src="/images/$zonefiles{$name}{'enabled'}.gif" title="$toggle_tr" alt="$toggle_tr">
+			</form>
+		</td>
+		<td align="center" width="5%">
+			<form method="post" action="$ENV{'SCRIPT_NAME'}">
+				<input type="hidden" name="KEY" value="$name">
+				<input type="hidden" name="ACTION" value="ZF_EDIT">
+				<input type="image" src="/images/edit.gif" title="$Lang::tr{'edit'}" alt="$Lang::tr{'edit'}">
+			</form>
+		</td>
+		<td align="center" width="5%">
+			<form method="post" action="$ENV{'SCRIPT_NAME'}">
+				<input type="hidden" name="KEY" value="$name">
+				<input type="hidden" name="ACTION" value="ZF_REMOVE">
+				<input type="image" src="/images/delete.gif" title="$Lang::tr{'remove'}" alt="$Lang::tr{'remove'}">
+			</form>
+		</td>
+	</tr>
+END
+;
+	}
+
+	# Disable reload button if not needed
+	my $reload_state = &_rpz_needs_reload() ? "" : " disabled";
+
+	print <<END
+</table>
+
+<div class="right">
+	<form method="post" action="$ENV{'SCRIPT_NAME'}">
+		<input type="hidden" name="KEY" value="">
+		<button type="submit" name="ACTION" value="ZF_EDIT">$Lang::tr{'add'}</button>
+		<button type="submit" name="ACTION" value="RPZ_RELOAD" class="commit"$reload_state>$Lang::tr{'rpz apply'}</button>
+	</form>
+</div>
+END
+;
+
+	&Header::closebox();
+}
+
+# Show zonefiles entry editor
+sub _print_zonefile_editor {
+
+	# Key specified: Edit existing entry
+	if(($action_key) && (defined $zonefiles{$action_key})) {
+		# Load data to be edited, but don't override already present values (allows user to edit after error)
+		$cgiparams{'ZF_NAME'} //= $action_key;
+		$cgiparams{'ZF_URL'} //= $zonefiles{$action_key}{'url'};
+		$cgiparams{'ZF_REMARK'} //= $zonefiles{$action_key}{'remark'};
+	}
+
+	# Fallback to empty form
+	$cgiparams{'ZF_NAME'} //= "";
+	$cgiparams{'ZF_URL'} //= "";
+	$cgiparams{'ZF_REMARK'} //= "";
+
+	&Header::openbox('100%', 'left', $Lang::tr{'rpz zf editor'});
+
+	print <<END
+<form method="post" action="$ENV{'SCRIPT_NAME'}">
+<input type="hidden" name="KEY" value="$action_key">
+<table width="100%">
+	<tr>
+		<td width="20%">$Lang::tr{'name'}:&nbsp;<img src="/blob.gif" alt="*"></td>
+		<td><input type="text" name="ZF_NAME" value="$cgiparams{'ZF_NAME'}" size="40" maxlength="32" title="$Lang::tr{'rpz zf remark info'}" pattern="[a-zA-Z0-9_]{1,32}" required></td>
+	</tr>
+	<tr>
+		<td width="20%">URL:&nbsp;<img src="/blob.gif" alt="*"></td>
+		<td><input type="url" name="ZF_URL" value="$cgiparams{'ZF_URL'}" size="40" maxlength="128" required></td>
+	</tr>
+	<tr>
+		<td width="20%">$Lang::tr{'remark'}:</td>
+		<td><input type="text" name="ZF_REMARK" value="$cgiparams{'ZF_REMARK'}" size="40" maxlength="32"></td>
+	</tr>
+	<tr>
+		<td colspan="2"><hr></td>
+	</tr>
+	<tr>
+		<td width="55%"><img src="/blob.gif" alt="*">&nbsp;$Lang::tr{'required field'}</td>
+		<td align="right"><button type="submit" name="ACTION" value="ZF_SAVE">$Lang::tr{'save'}</button></td>
+	</tr>
+</table>
+</form>
+
+<div class="right">
+	<form method="post" action="$ENV{'SCRIPT_NAME'}">
+		<button type="submit" name="ACTION" value="NONE">$Lang::tr{'back'}</button>
+	</form>
+</div>
+END
+;
+
+	&Header::closebox();
+}
+
+# Show custom allow/block files and related gui elements
+sub _print_customlists {
+
+	# Load lists from config, unless they are currently being edited
+	if($action ne 'CL_SAVE') {
+		$cgiparams{'ALLOW_LIST'} = join("\n", @{$customlists{'allow'}{'list'}});
+		$cgiparams{'BLOCK_LIST'} = join("\n", @{$customlists{'block'}{'list'}});
+
+		$cgiparams{'ALLOW_ENABLED'} = ($customlists{'allow'}{'enabled'} eq 'on') ? 'on' : undef;
+		$cgiparams{'BLOCK_ENABLED'} = ($customlists{'block'}{'enabled'} eq 'on') ? 'on' : undef;
+	}
+
+	# Fallback to empty form
+	$cgiparams{'ALLOW_LIST'} //= "";
+	$cgiparams{'BLOCK_LIST'} //= "";
+
+	# HTML checkboxes, unchecked = no or undef value in POST data
+	my %checked = ();
+	$checked{'ALLOW_ENABLED'} = (defined $cgiparams{'ALLOW_ENABLED'}) ? " checked" : "";
+	$checked{'BLOCK_ENABLED'} = (defined $cgiparams{'BLOCK_ENABLED'}) ? " checked" : "";
+
+	# Disable reload button if not needed
+	my $reload_state = &_rpz_needs_reload() ? "" : " disabled";
+
+	&Header::openbox('100%', 'left', $Lang::tr{'rpz cl'});
+
+	print <<END
+<form method="post" action="$ENV{'SCRIPT_NAME'}">
+<table width="100%">
+	<tr>
+		<td colspan="2"><b>$Lang::tr{'rpz cl allow'}</b><br>$Lang::tr{'rpz cl allow info'}</td>
+		<td colspan="2"><b>$Lang::tr{'rpz cl block'}</b><br>$Lang::tr{'rpz cl block info'}</td>
+	</tr>
+	<tr>
+		<td colspan="2"><textarea name="ALLOW_LIST" class="domainlist" cols="45">
+$cgiparams{'ALLOW_LIST'}</textarea></td>
+		<td colspan="2"><textarea name="BLOCK_LIST" class="domainlist" cols="45">
+$cgiparams{'BLOCK_LIST'}</textarea></td>
+	</tr>
+	<tr>
+		<td><label for="allow_enabled">$Lang::tr{'rpz cl allow enable'}</label></td>
+		<td width="15%"><input type="checkbox" name="ALLOW_ENABLED" id="allow_enabled"$checked{'ALLOW_ENABLED'}></td>
+		<td><label for="block_enabled">$Lang::tr{'rpz cl block enable'}</label></td>
+		<td width="15%"><input type="checkbox" name="BLOCK_ENABLED" id="block_enabled"$checked{'BLOCK_ENABLED'}></td>
+	</tr>
+	<tr>
+		<td colspan="4"><hr></td>
+	</tr>
+	<tr>
+		<td align="right" colspan="4">
+			<button type="submit" name="ACTION" value="CL_SAVE">$Lang::tr{'save'}</button>
+			<button type="submit" name="ACTION" value="RPZ_RELOAD" class="commit"$reload_state>$Lang::tr{'rpz apply'}</button>
+		</td>
+</table>
+</form>
+END
+;
+
+	&Header::closebox();
+}
+
+# Output javascript and extra gui elements
+sub _print_gui_extras {
+
+	# Apply/Restart button modifier key handler
+	if(&_rpz_needs_reload()) {
+		print <<END
+<script>
+	// Commit modifier key handler
+	(function(jq, document) {
+		var keyEventsOn = false;	// Keyboard events attached
+		var keyModify = false;		// Modifier key pressed
+		var mouseHover = false;		// Mouse over commit button
+		var btnModified = false;	// Button modified to "Restart"
+
+		// Document-level key events, enable only while cursor is over button
+		function attachKeyEvents() {
+			if(keyEventsOn) {
+				return;
+			}
+			keyEventsOn = true;
+
+			jq(document).on("keydown.rpz", function(event) {
+				if((!keyModify) && event.shiftKey) {
+					keyModify = true;
+					handleModify();
+				}
+			});
+			jq(document).on("keyup.rpz", function(event) {
+				if(keyModify && (!event.shiftKey)) {
+					keyModify = false;
+					handleModify();
+				}
+			});
+		}
+		function removeKeyEvents() {
+			keyModify = false;
+			if(keyEventsOn) {
+				jq(document).off("keydown.rpz keyup.rpz");
+				keyEventsOn = false;
+			}
+		}
+
+		// Attach mouse hover events to commit buttons
+		function attachMouseEvents() {
+			jq("button.commit").on("mouseenter", function(event) {
+				if(!mouseHover) {
+					mouseHover = true;
+					attachKeyEvents();
+					// Handle already pressed key
+					keyModify = !!(event.shiftKey);
+					handleModify();
+				}
+			});
+
+			// Cursor moved away: Disable key listener to minimize events
+			jq("button.commit").on("mouseleave", function() {
+				if(mouseHover) {
+					mouseHover = false;
+					removeKeyEvents();
+					handleModify();
+				}
+			});
+		}
+
+		// Modify commit button
+		function handleModify() {
+			let modify = mouseHover && keyModify;
+			if(btnModified != modify) {
+				if(modify) {
+					jq("button.commit").text("$Lang::tr{'restart'}").val("UNB_RESTART");
+				} else {
+					jq("button.commit").text("$Lang::tr{'rpz apply'}").val("RPZ_RELOAD");
+				}
+				btnModified = modify;
+			}
+		}
+
+		// jQuery DOM ready
+		jq(function() {
+			attachMouseEvents();
+		});
+	})(jQuery, document);
+</script>
+END
+;
+	} # End of modifier key handler
+
+}
+
+
+###--- Internal action processing functions ---###
+
+# Toggle zonefile on/off
+sub _action_zf_toggle {
+	return unless(defined $zonefiles{$action_key});
+
+	my $result = 0;
+	my $enabled = $zonefiles{$action_key}{'enabled'};
+
+	# Perform toggle action
+	if($enabled eq 'on') {
+		$enabled = 'off';
+		$result = &General::system('/usr/sbin/rpz-config', 'remove', $action_key, '--no-reload');
+	} else {
+		$enabled = 'on';
+		$result = &General::system('/usr/sbin/rpz-config', 'add', $action_key, $zonefiles{$action_key}{'url'}, '--no-reload');
+	}
+
+	# Check for errors, request service reload on success
+	return unless &_rpz_check_result($result, 1);
+
+	# Save changes
+	$zonefiles{$action_key}{'enabled'} = $enabled;
+	&_zonefiles_save_conf();
+
+	return 1;
+}
+
+# Remove zonefile
+sub _action_zf_remove {
+	return unless(defined $zonefiles{$action_key});
+
+	# Remove from rpz-config if currently active
+	if($zonefiles{$action_key}{'enabled'} eq 'on') {
+		my $result = &General::system('/usr/sbin/rpz-config', 'remove', $action_key, '--no-reload');
+
+		# Check for errors, request service reload on success
+		return unless &_rpz_check_result($result, 1);
+	}
+
+	# Remove from data hash and save changes
+	delete $zonefiles{$action_key};
+	&_zonefiles_save_conf();
+
+	# Clear action_key, as the entry is now removed entirely
+	$action_key = "";
+
+	return 1;
+}
+
+# Create or update zonefile entry
+# Returns undef if gui needs to stay in editor mode
+sub _action_zf_save {
+	my $result = 0;
+
+	my $name = $cgiparams{'ZF_NAME'} // "";
+	my $url = $cgiparams{'ZF_URL'} // "";
+	my $remark = $cgiparams{'ZF_REMARK'} // "";
+	my $enabled = 'on'; # Enable new entries by default
+
+	# Note on variables:
+	# name = unique key, will be used to address the entry
+	# action_key = name of the entry being edited, empty for new entry
+
+	# Only check for unique name if it changed
+	# (this also checks new entries because the action_key is empty in this case)
+	$result = &_rpz_validate_zonefile($name, $url, $remark, (lc($name) ne lc($action_key)));
+	return unless &_rpz_check_result($result, 0);
+
+	# Edit existing entry: Determine what was changed
+	if(($action_key) && (defined $zonefiles{$action_key})) {
+		# Name und URL remain unchanged, only save remark and finish
+		if(($name eq $action_key) && ($url eq $zonefiles{$action_key}{'url'})) {
+			$zonefiles{$action_key}{'remark'} = $remark;
+			&_zonefiles_save_conf();
+
+			return 1;
+		}
+
+		# Entry was changed and needs to be recreated, preserve status
+		$enabled = $zonefiles{$action_key}{'enabled'};
+
+		# Remove from rpz-config
+		return unless &_action_zf_remove();
+	}
+
+	# Add new entry to rpz-config
+	if($enabled eq 'on') {
+		$result = &General::system('/usr/sbin/rpz-config', 'add', $name, $url, '--no-reload');
+
+		# Check for errors, request service reload on success
+		return unless &_rpz_check_result($result, 1);
+	}
+
+	# Add to global data hash and save changes
+	my %entry = ('enabled' => $enabled,
+		'url' => $url,
+		'remark' => $remark);
+
+	$zonefiles{$name} = \%entry;
+	&_zonefiles_save_conf();
+
+	return 1;
+}
+
+# Save custom lists
+sub _action_cl_save {
+	return unless((defined $cgiparams{'ALLOW_LIST'}) && (defined $cgiparams{'BLOCK_LIST'}));
+
+	my $result = 0;
+
+	my @allowlist = split(/\R/, $cgiparams{'ALLOW_LIST'});
+	my @blocklist = split(/\R/, $cgiparams{'BLOCK_LIST'});
+
+	# Validate lists
+	$result = &_rpz_validate_customlist(\@allowlist);
+	if($result != 0) {
+		$errormessage = &_rpz_error_tr(202, $result);
+		return;
+	}
+	$result = &_rpz_validate_customlist(\@blocklist);
+	if($result != 0) {
+		$errormessage = &_rpz_error_tr(203, $result);
+		return;
+	}
+
+	# Add to global data hash and save changes
+	$customlists{'allow'}{'list'} = \@allowlist;
+	$customlists{'block'}{'list'} = \@blocklist;
+	$customlists{'allow'}{'enabled'} = (defined $cgiparams{'ALLOW_ENABLED'}) ? 'on' : 'off';
+	$customlists{'block'}{'enabled'} = (defined $cgiparams{'BLOCK_ENABLED'}) ? 'on' : 'off';
+
+	&_customlists_save_conf();
+	&_customlist_export('allow', $RPZ_ALLOWLIST);
+	&_customlist_export('block', $RPZ_BLOCKLIST);
+
+	# Make new lists, request service reload on success
+	$result = &General::system('/usr/sbin/rpz-make', 'allowblock', '--no-reload');
+	return unless &_rpz_check_result($result, 1);
+
+	return 1;
+}
+
+# Trigger rpz-config reload
+sub _action_rpz_reload {
+	return 1 unless &_rpz_needs_reload();
+
+	# Immediately clear flag to prevent multiple reloads
+	if(-f $RPZ_RELOAD_FLAG) {
+		unlink($RPZ_RELOAD_FLAG) or die "Can't remove $RPZ_RELOAD_FLAG: $!";
+	}
+
+	# Perform reload, recreate reload flag on error to enable retry
+	my $result = &General::system('/usr/sbin/rpz-config', 'reload');
+	if(not &_rpz_check_result($result, 0)) {
+		&General::system('touch', "$RPZ_RELOAD_FLAG");
+		return;
+	}
+
+	return 1;
+}
+
+# Trigger unbound restart
+sub _action_unb_restart {
+	return 1 unless &_rpz_needs_reload();
+
+	# Immediately clear flag to prevent multiple restarts
+	if(-f $RPZ_RELOAD_FLAG) {
+		unlink($RPZ_RELOAD_FLAG) or die "Can't remove $RPZ_RELOAD_FLAG: $!";
+	}
+
+	# Perform restart, unboundctrl always exits zero
+	&General::system('/usr/local/bin/unboundctrl', 'restart');
+
+	return 1;
+}
+
+
+###--- Internal rpz-config functions ---###
+
+# Translate rpz-config exitcodes and messages
+# 100-199: rpz-config, 200-299: webgui
+sub _rpz_error_tr {
+	my ($error, $append) = @_;
+	$append //= '';
+
+	# Translate numeric exit codes
+	if(looks_like_number($error)) {
+		if(defined $Lang::tr{"rpz exitcode $error"}) {
+			$error = $Lang::tr{"rpz exitcode $error"};
+		}
+	}
+
+	return "RPZ $Lang::tr{'error'}: $error" . &Header::escape($append);
+}
+
+# Check result of rpz-config system call, request reload on success
+sub _rpz_check_result {
+	my ($result, $request_reload) = @_;
+	$request_reload //= 0;
+
+	# exitcode 0 = success
+	if($result != 0) {
+		$errormessage = &_rpz_error_tr($result);
+		return;
+	}
+
+	# Set reload flag
+	if($request_reload) {
+		&General::system('touch', "$RPZ_RELOAD_FLAG");
+	}
+
+	return 1;
+}
+
+# Test whether reload flag is set
+sub _rpz_needs_reload {
+	return (-f $RPZ_RELOAD_FLAG);
+}
+
+# Validate a zonefile entry, returns rpz-config exitcode on failure. Use _rpz_check_result to verify.
+# unique = check for unique name
+sub _rpz_validate_zonefile {
+	my ($name, $url, $remark, $unique) = @_;
+	$unique //= 1;
+
+	unless($name =~ /^[a-zA-Z0-9_]{1,32}$/) {
+		return 101;
+	}
+	unless($url =~ /^[\w+\.:;\/\\&@#%?=\-~|!]{1,128}$/) {
+		return 105;
+	}
+	unless($remark =~ /^[\w \-()\.:;*\/\\?!&=]{0,32}$/) {
+		return 201;
+	}
+
+	# Check against already existing names
+	if($unique) {
+		foreach my $existing (keys %zonefiles) {
+			if(lc($name) eq lc($existing)) {
+				return 104;
+			}
+		}
+	}
+
+	return 0;
+}
+
+# Validate a custom list, returns number of rejected line on failure. Check for non-zero results.
+# listref = array reference, cleanup = remove invalid entries instead of returning an error
+sub _rpz_validate_customlist {
+	my ($listref, $cleanup) = @_;
+	$cleanup //= 0;
+
+	foreach my $index (reverse 0..$#{$listref}) {
+		my $row = @$listref[$index];
+		next unless($row); # Skip/allow empty lines
+
+		# Reject/remove everything besides wildcard domains and remarks
+		if((not &General::validwildcarddomainname($row)) && (not $row =~ /^;[\w \-()\.:;*\/\\?!&=]*$/)) {
+			unless($cleanup) {
+				# +1 for user friendly line number and to ensure non-zero exitcode
+				return $index + 1;
+			}
+
+			# Remove current row
+			splice(@$listref, $index, 1);
+		}
+	}
+
+	return 0;
+}
+
+
+###--- Internal misc functions ---###
+
+# Send HTTP 303 redirect headers for post/request/get pattern
+# (Must be sent before calling &Header::showhttpheaders())
+sub _http_prg_redirect {
+	my $location = "https://$ENV{'SERVER_NAME'}:$ENV{'SERVER_PORT'}$ENV{'SCRIPT_NAME'}";
+	print "Status: 303 See Other\n";
+	print "Location: $location\n";
+}
diff --git a/lfs/rpz b/lfs/rpz
new file mode 100644
index 000000000..7ddbc38e5
--- /dev/null
+++ b/lfs/rpz
@@ -0,0 +1,96 @@
+###############################################################################
+#                                                                             #
+# IPFire.org - A linux based firewall                                         #
+# Copyright (C) 2024  IPFire Team  <info(a)ipfire.org>                          #
+#                                                                             #
+# This program 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 3 of the License, or           #
+# (at your option) any later version.                                         #
+#                                                                             #
+# This program is distributed in the hope that it will be useful,             #
+# but WITHOUT ANY WARRANTY; without even the implied warranty of              #
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the               #
+# GNU General Public License for more details.                                #
+#                                                                             #
+# You should have received a copy of the GNU General Public License           #
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.       #
+#                                                                             #
+###############################################################################
+
+###############################################################################
+#  Definitions
+###############################################################################
+
+include Config
+
+SUMMARY    = response policy zone - RPZ reputation system for unbound DNS
+
+VER        = 1.0.0
+
+THISAPP    = rpz-$(VER)
+DIR_APP    = $(DIR_SRC)/$(THISAPP)
+TARGET     = $(DIR_INFO)/$(THISAPP)
+
+PROG       = rpz
+PAK_VER    = 18
+
+DEPS       =
+
+SERVICES   =
+
+###############################################################################
+# Top-level Rules
+###############################################################################
+
+install : $(TARGET)
+
+check :
+
+download :
+
+b2 :
+
+dist:
+	@$(PAK)
+
+###############################################################################
+# Installation Details
+###############################################################################
+
+$(TARGET) :
+	@$(PREBUILD)
+	@rm -rf $(DIR_APP)
+
+	#  RPZ scripts
+	install --verbose --mode=755 \
+	  $(DIR_CONF)/rpz/{rpz-config,rpz-metrics,rpz-sleep,rpz-make,rpz-functions} \
+	  --target-directory=/usr/sbin
+
+	#  RPZ config files
+	mkdir -pv /etc/unbound/local.d
+	install --verbose --mode=644 --owner=nobody --group=nobody \
+	  $(DIR_CONF)/rpz/00-rpz.conf \
+	  --target-directory=/etc/unbound/local.d
+	chown --verbose --recursive nobody:nobody /etc/unbound/local.d
+
+	#  RPZ custom list files for allow and block
+	mkdir -pv /var/ipfire/dns/rpz
+	touch /var/ipfire/dns/rpz/{allowlist,blocklist}
+	chown --verbose --recursive nobody:nobody /var/ipfire/dns/rpz
+
+	#  RPZ zone files
+	#   create empty RPZ config file to avoid a unbound config error
+	mkdir -pv /etc/unbound/zonefiles
+	touch /etc/unbound/zonefiles/allow.rpz
+	chown --verbose --recursive nobody:nobody /etc/unbound/zonefiles
+
+	# Install addon-specific language-files
+	install --verbose --mode=004 $(DIR_CONF)/rpz/rpz.*.pl \
+	  --target-directory=/var/ipfire/addon-lang
+
+	# Install backup definition
+	cp -vf $(DIR_CONF)/backup/includes/rpz /var/ipfire/backup/addons/includes/rpz
+
+	@rm -rf $(DIR_APP)
+	@$(POSTBUILD)
diff --git a/make.sh b/make.sh
index 827ea9e77..a77535b13 100755
--- a/make.sh
+++ b/make.sh
@@ -390,7 +390,7 @@ prepareenv() {
 		if [ "${free_space}" -lt "${required_space}" ]; then
 			# Add any consumed space
 			while read -r consumed_space path; do
-				(( free_space += consumed_space / 1024 / 1024 )) 
+				(( free_space += consumed_space / 1024 / 1024 ))
 			done <<< "$(du --summarize --bytes "${BUILD_DIR}" "${IMAGES_DIR}" "${LOG_DIR}" 2>/dev/null)"
 		fi
 
@@ -2087,6 +2087,7 @@ build_system() {
 	lfsmake2 btrfs-progs
 	lfsmake2 inotify-tools
 	lfsmake2 grub-btrfs
+	lfsmake2 rpz
 
 	lfsmake2 linux
 	lfsmake2 rtl8812au
diff --git a/src/paks/rpz/install.sh b/src/paks/rpz/install.sh
new file mode 100644
index 000000000..ef99bf742
--- /dev/null
+++ b/src/paks/rpz/install.sh
@@ -0,0 +1,36 @@
+#!/bin/bash
+###############################################################################
+#                                                                             #
+#  IPFire.org - A linux based firewall                                        #
+#  Copyright (C) 2024  IPFire Team  <info(a)ipfire.org>                         #
+#                                                                             #
+#  This program 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 3 of the License, or          #
+#  (at your option) any later version.                                        #
+#                                                                             #
+#  This program is distributed in the hope that it will be useful,            #
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of             #
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the              #
+#  GNU General Public License for more details.                               #
+#                                                                             #
+#  You should have received a copy of the GNU General Public License          #
+#  along with this program.  If not, see <http://www.gnu.org/licenses/>.      #
+#                                                                             #
+###############################################################################
+#
+. /opt/pakfire/lib/functions.sh
+extract_files
+restore_backup ${NAME}
+
+#	fix user created files
+chown --verbose --recursive nobody:nobody \
+	/var/ipfire/dns/rpz    \
+	/etc/unbound/zonefiles \
+	/etc/unbound/local.d
+
+# Update Language cache
+/usr/local/bin/update-lang-cache
+
+#  restart unbound to load config file
+/etc/init.d/unbound restart
diff --git a/src/paks/rpz/uninstall.sh b/src/paks/rpz/uninstall.sh
new file mode 100644
index 000000000..e11427df3
--- /dev/null
+++ b/src/paks/rpz/uninstall.sh
@@ -0,0 +1,38 @@
+#!/bin/bash
+###############################################################################
+#                                                                             #
+#  IPFire.org - A linux based firewall                                        #
+#  Copyright (C) 2024  IPFire Team  <info(a)ipfire.org>                         #
+#                                                                             #
+#  This program 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 3 of the License, or          #
+#  (at your option) any later version.                                        #
+#                                                                             #
+#  This program is distributed in the hope that it will be useful,            #
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of             #
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the              #
+#  GNU General Public License for more details.                               #
+#                                                                             #
+#  You should have received a copy of the GNU General Public License          #
+#  along with this program.  If not, see <http://www.gnu.org/licenses/>.      #
+#                                                                             #
+###############################################################################
+#
+. /opt/pakfire/lib/functions.sh
+
+#  stop unbound to delete RPZ conf file
+/etc/init.d/unbound stop
+
+make_backup ${NAME}
+remove_files
+
+#  delete rpz config files.  Otherwise unbound will throw error:
+#    "[1723428668] unbound-control[17117:0] error: connect: Connection refused for 127.0.0.1 port 8953"
+/bin/rm --verbose --force /etc/unbound/local.d/*.rpz.conf
+
+# Update Language cache
+/usr/local/bin/update-lang-cache
+
+#  start unbound to load unbound config file
+/etc/init.d/unbound start
diff --git a/src/paks/rpz/update.sh b/src/paks/rpz/update.sh
new file mode 100644
index 000000000..9bc340bc6
--- /dev/null
+++ b/src/paks/rpz/update.sh
@@ -0,0 +1,52 @@
+#!/bin/bash
+###############################################################################
+#                                                                             #
+#  IPFire.org - A linux based firewall                                        #
+#  Copyright (C) 2024  IPFire Team  <info(a)ipfire.org>                         #
+#                                                                             #
+#  This program 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 3 of the License, or          #
+#  (at your option) any later version.                                        #
+#                                                                             #
+#  This program is distributed in the hope that it will be useful,            #
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of             #
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the              #
+#  GNU General Public License for more details.                               #
+#                                                                             #
+#  You should have received a copy of the GNU General Public License          #
+#  along with this program.  If not, see <http://www.gnu.org/licenses/>.      #
+#                                                                             #
+###############################################################################
+#
+. /opt/pakfire/lib/functions.sh
+
+#  from update.sh
+extract_backup_includes
+
+#  stop unbound to delete RPZ conf file
+/etc/init.d/unbound stop
+
+#  from uninstall.sh
+make_backup ${NAME}
+remove_files
+
+#  delete rpz config files.  Otherwise unbound will throw error:
+#    "unbound-control[nn:0] error: connect: Connection refused for 127.0.0.1 port 8953"
+/bin/rm --verbose --force /etc/unbound/local.d/*.rpz.conf
+
+#  from install.sh
+extract_files
+restore_backup ${NAME}
+
+#	fix user created files
+chown --verbose --recursive nobody:nobody \
+	/var/ipfire/dns/rpz    \
+	/etc/unbound/zonefiles \
+	/etc/unbound/local.d
+
+# Update Language cache
+/usr/local/bin/update-lang-cache
+
+#  restart unbound to load config files
+/etc/init.d/unbound start
-- 
2.39.5


             reply	other threads:[~2025-02-06 16:35 UTC|newest]

Thread overview: 30+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2025-02-06 16:35 Jon Murphy [this message]
2025-02-06 19:35 ` Bernhard Bitsch
2025-02-06 20:13 ` Michael Tremer
2025-02-08 18:41   ` jon
2025-02-08 19:27     ` Michael Tremer
     [not found]       ` <C28A0D7E-6C16-4F6F-9366-A8498F40631E@ipfire.org>
2025-02-14 12:07         ` Michael Tremer
2025-02-14 12:58           ` Bernhard Bitsch
2025-02-14 13:52             ` Michael Tremer
2025-02-14 14:16               ` Bernhard Bitsch
2025-03-01 10:18           ` Adolf Belka
     [not found]             ` <3BF29525-C9F4-4FD2-834D-FBE791E99E8C@ipfire.org>
2025-03-02 10:51               ` Adolf Belka
2025-03-10 17:47                 ` jon
2025-03-16 17:00         ` Jon Murphy
2025-03-17 10:35           ` Michael Tremer
2025-03-19  2:58             ` Jon Murphy
2025-03-19 10:35               ` Michael Tremer
2025-03-19 18:22                 ` Jon Murphy
     [not found]                 ` <afcb2a99-1281-43e3-bd3d-d915024683f6@ipfire.org>
2025-03-20 16:26                   ` Michael Tremer
2025-03-24  0:00                     ` Re[2]: " Jon Murphy
2025-03-24 10:17                       ` Michael Tremer
2025-03-24 13:33                         ` Bernhard Bitsch
2025-03-24 14:25                           ` Michael Tremer
2025-03-24 14:33                             ` Re[2]: " Jon Murphy
2025-03-24 14:36                               ` Michael Tremer
2025-03-24 14:38                                 ` Re[2]: " Jon Murphy
2025-03-24 14:40                                   ` Michael Tremer
2025-03-24 14:42                                     ` Re[2]: " Jon Murphy
2025-03-24 14:43                                       ` Michael Tremer
2025-03-24 14:49                                 ` Bernhard Bitsch
2025-03-24 14:41                             ` Bernhard Bitsch

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20250206163522.2363178-1-jon.murphy@ipfire.org \
    --to=jon.murphy@ipfire.org \
    --cc=development@lists.ipfire.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox