@@ -1,6 +1,7 @@
etc/rc.d/init.d/unbound
#etc/unbound
etc/unbound/dhcp-leases.conf
+etc/unbound/fastflux-detection.py
etc/unbound/forward.conf
etc/unbound/icannbundle.pem
etc/unbound/local.d
new file mode 100644
@@ -0,0 +1,167 @@
+###############################################################################
+# #
+# Pakfire - The IPFire package management system #
+# Copyright (C) 2025 IPFire Development Team #
+# #
+# 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/>. #
+# #
+###############################################################################
+
+import datetime
+import ipaddress
+import location
+import socket
+
+DEFAULT_THRESHOLD = 5
+
+def read_config(path):
+ """
+ Opens the configuration file and reads it line by line
+ """
+ config = {}
+
+ with open(path) as f:
+ for line in f:
+ # Remove any trailing newline
+ line = line.rstrip()
+
+ # Split by key and value
+ key, _, val = line.partition("=")
+
+ # Store the line
+ config[key] = val
+
+ return config
+
+def init(id, cfg):
+ global db
+ global ENABLED
+ global THRESHOLD
+
+ # Read the configuration
+ config = read_config("/var/ipfire/dns/settings")
+
+ # Is this module enabled?
+ ENABLED = config.get("FF_DETECTION", "false") in ("on", "true", "1")
+
+ # Fetch the treshold
+ if ENABLED:
+ threshold = config.get("FF_THRESHOLD", 5)
+
+ try:
+ THRESHOLD = int(threshold)
+ except (TypeError, ValueError):
+ log_warning("Failed to parse Fast Flux threshold '%s'."
+ " Using default of %s" % (threshold, DEFAULT_THRESHOLD))
+ THRESHOLD = DEFAULT_THRESHOLD
+
+ # Open the location database
+ try:
+ db = location.open()
+
+ # Fail if we could not open the database
+ except Exception as e:
+ log_error("Failed to open the location database: %s" % e)
+ return False
+
+ log_info("Opened Location database")
+ log_info(" Database Vendor : %s" % db.vendor)
+ log_info(" Created At : %s" % datetime.datetime.fromtimestamp(db.created_at))
+
+ # Done!
+ log_info("FastFlux detection module loaded")
+
+ return True
+
+def deinit(id):
+ log_info("FastFlux detection module unloaded")
+ return True
+
+def inform_super(id, qstate, superqstate, qdata):
+ return True
+
+def operate(id, event, qstate, qdata):
+ # Execute when everything else is done
+ if event == MODULE_EVENT_MODDONE:
+ # Do nothing if this is not enabled
+ if not ENABLED:
+ qstate.ext_state[id] = MODULE_FINISHED
+ return True
+
+ # Extract the qname
+ qname = qstate.qinfo.qname_str
+
+ # Deny access to the qname?
+ deny = False
+
+ # Extract the response
+ rrset = qstate.return_msg.rep.rrsets
+
+ addrs = set()
+
+ # Find all IP addresses in the response
+ for i in range(qstate.return_msg.rep.rrset_count):
+ rr = rrset[i]
+
+ # Extract the type
+ type = socket.ntohs(rr.rk.type)
+
+ # Only process types A and AAAA
+ if type in (1, 28):
+ for i in range(rr.entry.data.count):
+ payload = rr.entry.data.rr_data[i]
+
+ # Parse the IP address
+ if type == 1:
+ addr = ipaddress.IPv4Address(payload[2:])
+ elif type == 28:
+ addr = ipaddress.IPv6Address(payload[2:])
+
+ addrs.add(addr)
+
+ # Only perform any further action if we have at least as many as threshold IP addresses
+ if len(addrs) >= THRESHOLD:
+ asns = set()
+
+ # Look up the networks for all addresses
+ for addr in addrs:
+ network = db.lookup("%s" % addr)
+
+ # If no network could be found, we add zero to represent an unknown value
+ asns.add(network.asn if network else 0)
+
+ # Check for selective announements
+ if 0 in asns:
+ log_info("Denying access to %s due to suspected selective announcements" % qname)
+ deny = True
+
+ # Check if the threshold was exceeded
+ elif len(asns) >= THRESHOLD:
+ log_info("Denying access to %s due to suspected Fast Flux announcement" % qname)
+ deny = True
+
+ # Return SERVFAIL?
+ # XXX It would be nice to send an extended DNS error here (e.g. BLOCKED), but it
+ # seems that this is currently not supported in the Python module.
+ if deny:
+ qstate.ext_state[id] = MODULE_ERROR
+ return True
+
+ # Otherwise, continue
+ qstate.ext_state[id] = MODULE_FINISHED
+ return True
+
+ # Not handling other events
+ qstate.ext_state[id] = MODULE_WAIT_MODULE
+ return True
@@ -12,6 +12,9 @@ server:
username: "nobody"
do-ip6: no
+ # Load modules
+ module-config: "validator python iterator"
+
# System Tuning
include: "/etc/unbound/tuning.conf"
@@ -68,6 +71,10 @@ server:
# Include any forward zones
include: "/etc/unbound/forward.conf"
+python:
+ # Enable Fast Flux Detection
+ python-script: "/etc/unbound/fastflux-detection.py"
+
remote-control:
control-enable: yes
control-use-cert: no
@@ -611,6 +611,7 @@ WARNING: untranslated string: dnat address = Firewall Interface
WARNING: untranslated string: dns check failed = DNS check failed
WARNING: untranslated string: dns check servers = Check DNS Servers
WARNING: untranslated string: dns configuration = DNS Configuration
+WARNING: untranslated string: dns enable fast flux detection = Fast Flux Detection
WARNING: untranslated string: dns enable safe-search = Enable Safe Search
WARNING: untranslated string: dns enable safe-search youtube = Include YouTube in Safe Search
WARNING: untranslated string: dns forward disable dnssec = Disable DNSSEC (dangerous)
@@ -1015,6 +1015,7 @@ WARNING: untranslated string: ca name must only contain characters and spaces =
WARNING: untranslated string: cpu frequency = CPU frequency
WARNING: untranslated string: data transfer = Data Transfer
WARNING: untranslated string: dhcp fixed ip address in dynamic range = Fixed IP Address in dynamic range
+WARNING: untranslated string: dns enable fast flux detection = Fast Flux Detection
WARNING: untranslated string: dns servers = DNS Servers
WARNING: untranslated string: done = Done
WARNING: untranslated string: downfall gather data sampling = Downfall/Gather Data Sampling
@@ -979,6 +979,7 @@ WARNING: untranslated string: bypassed = Bypassed
WARNING: untranslated string: ca name must only contain characters and spaces = unknown string
WARNING: untranslated string: core notice 3 = available.
WARNING: untranslated string: data transfer = Data Transfer
+WARNING: untranslated string: dns enable fast flux detection = Fast Flux Detection
WARNING: untranslated string: done = Done
WARNING: untranslated string: enable disable client = unknown string
WARNING: untranslated string: enable disable dyndns = unknown string
@@ -1019,6 +1019,7 @@ WARNING: untranslated string: disconnected = Disconnected
WARNING: untranslated string: dl client arch insecure = Download insecure Client Package (zip)
WARNING: untranslated string: dns check servers = Check DNS Servers
WARNING: untranslated string: dns configuration = DNS Configuration
+WARNING: untranslated string: dns enable fast flux detection = Fast Flux Detection
WARNING: untranslated string: dns enable safe-search = Enable Safe Search
WARNING: untranslated string: dns enable safe-search youtube = Include YouTube in Safe Search
WARNING: untranslated string: dns forward disable dnssec = Disable DNSSEC (dangerous)
@@ -1019,6 +1019,7 @@ WARNING: untranslated string: disable = Disable
WARNING: untranslated string: disconnected = Disconnected
WARNING: untranslated string: dns check servers = Check DNS Servers
WARNING: untranslated string: dns configuration = DNS Configuration
+WARNING: untranslated string: dns enable fast flux detection = Fast Flux Detection
WARNING: untranslated string: dns enable safe-search = Enable Safe Search
WARNING: untranslated string: dns enable safe-search youtube = Include YouTube in Safe Search
WARNING: untranslated string: dns forward disable dnssec = Disable DNSSEC (dangerous)
@@ -991,6 +991,7 @@ WARNING: untranslated string: dl client arch insecure = Download insecure Client
WARNING: untranslated string: dnat address = Firewall Interface
WARNING: untranslated string: dns check servers = Check DNS Servers
WARNING: untranslated string: dns configuration = DNS Configuration
+WARNING: untranslated string: dns enable fast flux detection = Fast Flux Detection
WARNING: untranslated string: dns enable safe-search = Enable Safe Search
WARNING: untranslated string: dns enable safe-search youtube = Include YouTube in Safe Search
WARNING: untranslated string: dns forward disable dnssec = Disable DNSSEC (dangerous)
@@ -986,6 +986,7 @@ WARNING: untranslated string: dl client arch insecure = Download insecure Client
WARNING: untranslated string: dnat address = Firewall Interface
WARNING: untranslated string: dns check servers = Check DNS Servers
WARNING: untranslated string: dns configuration = DNS Configuration
+WARNING: untranslated string: dns enable fast flux detection = Fast Flux Detection
WARNING: untranslated string: dns enable safe-search = Enable Safe Search
WARNING: untranslated string: dns enable safe-search youtube = Include YouTube in Safe Search
WARNING: untranslated string: dns forward disable dnssec = Disable DNSSEC (dangerous)
@@ -998,6 +998,7 @@ WARNING: untranslated string: disable = Disable
WARNING: untranslated string: disconnected = Disconnected
WARNING: untranslated string: dns check servers = Check DNS Servers
WARNING: untranslated string: dns configuration = DNS Configuration
+WARNING: untranslated string: dns enable fast flux detection = Fast Flux Detection
WARNING: untranslated string: dns enable safe-search = Enable Safe Search
WARNING: untranslated string: dns enable safe-search youtube = Include YouTube in Safe Search
WARNING: untranslated string: dns forward disable dnssec = Disable DNSSEC (dangerous)
@@ -173,6 +173,7 @@
< cpu frequency
< data transfer
< dhcp fixed ip address in dynamic range
+< dns enable fast flux detection
< dns servers
< done
< downfall gather data sampling
@@ -287,6 +288,7 @@
< bypassed
< ca name must only contain characters or spaces
< data transfer
+< dns enable fast flux detection
< done
< endpoint
< endpoint address
@@ -509,6 +511,7 @@
< dns check servers
< dns configuration
< dns could not add server
+< dns enable fast flux detection
< dns enable safe-search
< dns enable safe-search youtube
< dns forward disable dnssec
@@ -1110,6 +1113,7 @@
< dns check servers
< dns configuration
< dns could not add server
+< dns enable fast flux detection
< dns enable safe-search
< dns enable safe-search youtube
< dns forward disable dnssec
@@ -1807,6 +1811,7 @@
< dns check servers
< dns configuration
< dns could not add server
+< dns enable fast flux detection
< dns enable safe-search
< dns enable safe-search youtube
< dnsforward
@@ -2884,6 +2889,7 @@
< dns check servers
< dns configuration
< dns could not add server
+< dns enable fast flux detection
< dns enable safe-search
< dns enable safe-search youtube
< dnsforward
@@ -3823,6 +3829,7 @@
< dns check servers
< dns configuration
< dns could not add server
+< dns enable fast flux detection
< dns enable safe-search
< dns enable safe-search youtube
< dns forward disable dnssec
@@ -82,6 +82,11 @@ if ($cgiparams{'GENERAL'} eq $Lang::tr{'save'}) {
$cgiparams{'USE_ISP_NAMESERVERS'} = "off";
}
+ # Add value for non-checked checkbox.
+ if ($cgiparams{'FF_DETECTION'} ne "on") {
+ $cgiparams{'FF_DETECTION'} = "off";
+ }
+
# Add value for non-checked checkbox.
if ($cgiparams{'ENABLE_SAFE_SEARCH'} ne "on") {
$cgiparams{'ENABLE_SAFE_SEARCH'} = "off";
@@ -264,6 +269,7 @@ if (($cgiparams{'SERVERS'} eq $Lang::tr{'save'}) || ($cgiparams{'SERVERS'} eq $L
# Hash to store the generic DNS settings.
my %settings = ();
$settings{"ENABLE_SAFE_SEARCH_YOUTUBE"} = "on";
+$settings{"FF_DETECTION"} = "on";
# Read-in general DNS settings.
&General::readhash("$settings_file", \%settings);
@@ -311,6 +317,10 @@ $checked{'USE_ISP_NAMESERVERS'}{'off'} = '';
$checked{'USE_ISP_NAMESERVERS'}{'on'} = '';
$checked{'USE_ISP_NAMESERVERS'}{$settings{'USE_ISP_NAMESERVERS'}} = "checked='checked'";
+$checked{'FF_DETECTION'}{'off'} = '';
+$checked{'FF_DETECTION'}{'on'} = '';
+$checked{'FF_DETECTION'}{$settings{'FF_DETECTION'}} = "checked='checked'";
+
$checked{'ENABLE_SAFE_SEARCH'}{'off'} = '';
$checked{'ENABLE_SAFE_SEARCH'}{'on'} = '';
$checked{'ENABLE_SAFE_SEARCH'}{$settings{'ENABLE_SAFE_SEARCH'}} = "checked='checked'";
@@ -380,6 +390,17 @@ sub show_general_dns_configuration () {
</td>
</tr>
+ <tr>
+ <td width="33%">
+ $Lang::tr{'dns enable fast flux detection'}
+ </td>
+
+ <td>
+ <input type="checkbox" name="FF_DETECTION" $checked{'FF_DETECTION'}{'on'}>
+ </td>
+ </tr>
+
+
<tr>
<td width="33%">
$Lang::tr{'dns enable safe-search'}
@@ -835,6 +835,7 @@
'dns check servers' => 'DNS-Server prüfen',
'dns configuration' => 'DNS-Konfiguration',
'dns desc' => 'Wenn auf Schnittstelle red0 die IP-Adressinformationen über DHCP vom Provider kommen, werden automatisch die DNS-Server-Adressen des Providers gesetzt. Hier können Sie nun diese mit den eigenen DNS-Server-IP-Adressen überschreiben.',
+'dns enable fast flux detection' => 'Fast-Flux-Erkennung',
'dns enable safe-search' => 'Safe Search via DNS aktivieren',
'dns enable safe-search youtube' => 'YouTube in Safe Search einbeziehen',
'dns error 0' => 'Die IP Adresse vom <strong>primären</strong> DNS Server ist nicht gültig, bitte überprüfen Sie Ihre Eingabe!<br />Die eingegebene <strong>sekundären</strong> DNS Server Adresse ist jedoch gültig.<br />',
@@ -880,6 +880,7 @@
'dns configuration' => 'DNS Configuration',
'dns could not add server' => 'Could not add server - Reason:',
'dns desc' => 'If the red0 interface gets the IP address information via DHCP from the provider, the DNS server addresses will be set automatically. Now here you are able to change these DNS server IP addresses with your own ones.',
+'dns enable fast flux detection' => 'Fast Flux Detection',
'dns enable safe-search' => 'Enable Safe Search',
'dns enable safe-search youtube' => 'Include YouTube in Safe Search',
'dns error 0' => 'The IP address of the <strong>primary</strong> DNS server is not valid, please check your entries!<br />The entered <strong>secondary</strong> DNS server address is valid.',
@@ -97,6 +97,10 @@ $(TARGET) : $(patsubst %,$(DIR_DL)/%,$(objects))
touch /etc/unbound/{dhcp-leases,forward}.conf
-mkdir -pv /etc/unbound/local.d
+ # Install Python scripts
+ install -v -m 644 $(DIR_SRC)/config/unbound/fastflux-detection.py \
+ /etc/unbound/fastflux-detection.py
+
# Install root hints
install -v -m 644 $(DIR_SRC)/config/unbound/root.hints \
/etc/unbound/root.hints