]> git.ipfire.org Git - people/ms/ipfire-2.x.git/commitdiff
Add Fast Flux detection in DNS unbound-python
authorMichael Tremer <michael.tremer@ipfire.org>
Fri, 25 Apr 2025 10:10:42 +0000 (11:10 +0100)
committerMichael Tremer <michael.tremer@ipfire.org>
Fri, 25 Apr 2025 12:34:37 +0000 (14:34 +0200)
This has been implemented because of a request on the forum. Since the
proxy is generally outgoing technology it makes sense to enable this
kind of filtering in DNS.

This patch adds a Python script which processes every query and its
response and extracts all IP addresses from it. Those IP addresses will
then be resolved to their origin AS. If there are more then THRESHOLD
different ASes, the request will be blocked.

The AS lookups will only be performed when there is enough IP addresses
to actually hit the threshold. So there should be next to no performance
impact here except the overhead of the Python module itself.

Signed-off-by: Michael Tremer <michael.tremer@ipfire.org>
16 files changed:
config/rootfiles/common/unbound
config/unbound/fastflux-detection.py [new file with mode: 0644]
config/unbound/unbound.conf
doc/language_issues.en
doc/language_issues.es
doc/language_issues.fr
doc/language_issues.it
doc/language_issues.nl
doc/language_issues.pl
doc/language_issues.ru
doc/language_issues.tr
doc/language_missings
html/cgi-bin/dns.cgi
langs/de/cgi-bin/de.pl
langs/en/cgi-bin/en.pl
lfs/unbound

index 03c956503cfc4fa79e2b06a05f364ffeb72b122e..9ca84d2d9ae8be989eb85dbee0bd05410d603e91 100644 (file)
@@ -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
diff --git a/config/unbound/fastflux-detection.py b/config/unbound/fastflux-detection.py
new file mode 100644 (file)
index 0000000..4ad62ee
--- /dev/null
@@ -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
index 012beab54dc48c70a29b6d9b6969479ec37ef2e6..b1f2871c4acb88639ab64950957b98313910ed42 100644 (file)
@@ -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
index 1c1c546f7e6892db7f217da73c99ac63e7e46557..a5860a1535f0524bd7fe4cd02e834f5c76682936 100644 (file)
@@ -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)
index cf7237435776cf4a13396f2e68b9a56344eec190..2b7937842b4ba9bf7c3a33c331a0e69dec4a156f 100644 (file)
@@ -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
index 702911061d20b6184b20690f4a798b3f42973641..b9703cd4d480800ea3755ab965934fcc0020132e 100644 (file)
@@ -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
index 3d93239afd1eb2fe58e35e24d00ec5332a046bf4..c5297c2419c046340e3646c39c67674d55469dc0 100644 (file)
@@ -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)
index f1090fc337f64bd6b10ec55537a4e166841a1cf4..7f16de518dbe79034b2d528f5663da6108b2f0e2 100644 (file)
@@ -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)
index 1db36fb67f0d67fdbaee3bacc4bfee837c66d021..063045ef3622d86a86af851cb455e9272e2a37e9 100644 (file)
@@ -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)
index 4d29c4f951d0ed22654ff87b0313dabaffb477d4..5c85a2c4006f71351f20e4810f1293a867fb8f85 100644 (file)
@@ -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)
index 2da19f276101dd87823fa684b4c7462c72691cfa..473250445724096852cc56db96b48c14337e469c 100644 (file)
@@ -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)
index 48b98ce74d710f27ace217825216ebd6e8a1ef8f..f5c0949b5a9ea158c58c6ce0ed5ea40acfabfacf 100644 (file)
 < cpu frequency
 < data transfer
 < dhcp fixed ip address in dynamic range
+< dns enable fast flux detection
 < dns servers
 < done
 < downfall gather data sampling
 < bypassed
 < ca name must only contain characters or spaces
 < data transfer
+< dns enable fast flux detection
 < done
 < endpoint
 < endpoint address
 < 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
 < 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
 < 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
 < 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
 < 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
index 0d3b14797a6e930284550fc31693b93a9874f571..8cc39f97c578761de7824a0a118ca9ec0e354f90 100644 (file)
@@ -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'}
index 3ce02b657a9eadf15a8bd38989854bf8a802b2fa..aafc180a6d4a7cf9ff52b12fb17a22d1bbf5dc75 100644 (file)
 '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 />',
index 3e647e6e53dc9937fa9aeac0403a42a16913fa05..fb82fcd0836f946063d453bbe862696adc0665f1 100644 (file)
 '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.',
index 8c79125ecb36b15034ba3870ad7b686389c36d37..00105b6911031c13017d2ad11cd18b00e8e05d38 100644 (file)
@@ -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