Bind9 rpz transfer not work

config bind9 zone slave of ipfire rpz , but zone not download (no cache file saved).

other slave zones trasfer is ok so should be no problem with permission..

//example

zone “ads.rpz.ipfire.org” in { file “ads.rpz.ipfire.org.cache”; type slave; masters {81.3.27.55;}; ixfr-from-differences yes; };

// log ( strip zone name to ads.xxx due not allow 2 links for new user)

2026-04-13T00:55:53.351239+07:00 runes named[2183944]: zone ads.xxx/IN: Transfer started. 2026-04-13T00:55:53.544006+07:00 runes named[2183944]: transfer of ‘ads.xxx/IN’ from 81.3.27.55#53: connected using 81.3.27.55#53 2026-04-13T00:55:54.526570+07:00 runes named[2183944]: transfer of ‘ads.xxx/IN’ from 81.3.27.55#53: failed while receiving responses: end of file 2026-04-13T00:55:54.526795+07:00 runes named[2183944]: transfer of ‘ads.xxx/IN’ from 81.3.27.55#53: Transfer status: end of file 2026-04-13T00:55:54.526926+07:00 runes named[2183944]: transfer of ‘ads.xxx IN’ from 81.3.27.55#53: Transfer completed: 8 messages, 3747 records, 131262 bytes, 0.982 secs (133668 bytes/sec) (serial 1775984405)

Hello Densin,

this looks like the connection is just being interrupted, or that BIND has downloaded some data that it cannot understand. I have not personally tested BIND myself, but I don’t see any reason why IPFire DBL should not work in it.

What you can do as a test from the same machine is to run a manual zone transfer like so:

dig @xfr.dbl.ipfire.org ads.rpz.ipfire.org AXFR

That should show you the entire zone and at least confirm that there is not a connectivity problem.

…… long output ….

fine.bursthealth.com.au.ads.rpz.ipfire.org. 60 IN CNAME . *.fine.bursthealth.com.au.ads.rpz.ipfire.org. 60 IN CNAME .
;; communications error to 81.3.27.55#53: end of file
;; no servers could be reached

tested by two server in diffrent datacenter , same output.

This seems to be running just fine for me:

...
*.adservice.google.co.zw.ads.rpz.ipfire.org. 60 IN CNAME .
stbg.stanbicbank.co.zw.ads.rpz.ipfire.org. 60 IN CNAME .
*.stbg.stanbicbank.co.zw.ads.rpz.ipfire.org. 60 IN CNAME .
ads.rpz.ipfire.org.     60      IN      SOA     primary.dbl.ipfire.org. hostmaster.ipfire.org. 1776081003 3600 600 3600000 60
;; Query time: 2296 msec
;; SERVER: 81.3.27.55#53(81.3.27.55)
;; WHEN: Mon Apr 13 16:52:16 BST 2026
;; XFR size: 281840 records (messages 645, bytes 10581144)

You might have a middleware on your network that is thinking that a DNS TCP connection might only be allowed to send a couple of bytes until it is being cut off?

Alternatively you can fetch the zone using TLS over port 853 on the same server, but Unbound does not support this, yet.

after test too much I got blocked to 81.3.27.55

so I vibe script instead.

port it here may help others.

#!/bin/bash

#Ex: sudo -u bind DblDownloader.sh -q porn phishing gambling malware

#License GPL-v2

#Vibe coding by densin@linux.in.th buddy with gemini

QUIET=false
RPZPATH=“/etc/bind/zones.rpz”
#RPZPATH=“zones.rpz” #debug
BLOCKLIST=() # can add default zone herer

download_new() {
local BASE_FILE=“$1”
local URL=“$2”

if [ -z "$BASE_FILE" ] || [ -z "$URL" ]; then
    $QUIET || echo "Usage: download_new <base_filename> <url>"
    return 1
fi

# Automatically append .new to the designated output file
local OUTPUT_FILE="${BASE_FILE}.new"

# --- Step 0: Date Freshness Check ---
# Check the server against your existing BASE_FILE (e.g., db.example.com)
if [ -f "$BASE_FILE" ]; then
    local REMOTE_LAST_MODIFIED=$(curl -sIL "$URL" | grep -i '^last-modified:' | tail -n 1 | cut -d: -f2- | tr -d '\r')

    if [ -n "$REMOTE_LAST_MODIFIED" ]; then
        local REMOTE_EPOCH=$(date -d "$REMOTE_LAST_MODIFIED" +%s 2>/dev/null)
        local LOCAL_EPOCH=$(date -r "$BASE_FILE" +%s 2>/dev/null)

        if [ -n "$REMOTE_EPOCH" ] && [ -n "$LOCAL_EPOCH" ] && [ "$LOCAL_EPOCH" -ge "$REMOTE_EPOCH" ]; then
            $QUIET || echo "⏭️  SKIPPED: Local file '$BASE_FILE' is already up-to-date."
            return 0
        fi
    fi
fi

$QUIET || echo "Downloading to $OUTPUT_FILE..."

# --- Step 1: Download ---
local HEADER_FILE=$(mktemp)
local HTTP_STATUS=$(curl -sL -w "%{http_code}" -D "$HEADER_FILE" -o "$OUTPUT_FILE" "$URL")

# --- Step 2: HTTP Status Check ---
if [ "$HTTP_STATUS" -ne 200 ]; then
    $QUIET || echo "❌ FAILED: HTTP $HTTP_STATUS"
    [ -f "$OUTPUT_FILE" ] && rm "$OUTPUT_FILE"
    rm "$HEADER_FILE"
    return 1
fi

# --- Step 3: Size Check ---
local EXPECTED_SIZE=$(grep -i '^content-length:' "$HEADER_FILE" | awk '{print $2}' | tr -d '\r' | tail -n 1)
local ACTUAL_SIZE=$(wc -c < "$OUTPUT_FILE" | tr -d ' ')
rm "$HEADER_FILE"

if [ -n "$EXPECTED_SIZE" ]; then
    if [ "$ACTUAL_SIZE" -eq "$EXPECTED_SIZE" ]; then
        $QUIET || echo "✅ SUCCESS: $OUTPUT_FILE ($ACTUAL_SIZE bytes)"
        return 0
    else
        $QUIET || echo "❌ ERROR: Size mismatch. Expected $EXPECTED_SIZE, got $ACTUAL_SIZE. Removing $OUTPUT_FILE."
        [ -f "$OUTPUT_FILE" ] && rm "$OUTPUT_FILE"
        return 2
    fi
else
    $QUIET || echo "⚠️  FINISHED: $OUTPUT_FILE ($ACTUAL_SIZE bytes) - No size header to verify."
    return 0
fi

}

zonefilter() {
local ZONE_NAME=“$1”
local ZONE_FILE=“$2”

# Input Validation
if [ -z "$ZONE_NAME" ] || [ -z "$ZONE_FILE" ]; then
    $QUIET || echo "Usage: zonefilter <zone_name> <zone_file>"
    return 1
fi

if [ ! -f "$ZONE_FILE" ]; then
    $QUIET || echo "❌ ERROR: File '$ZONE_FILE' does not exist."
    return 1
fi

# Run check based on global QUIET variable
if $QUIET; then
    named-checkzone "$ZONE_NAME" "$ZONE_FILE" &> /dev/null
    local EXIT_CODE=$?
else
    echo "Filtering zone '$ZONE_NAME' via named-checkzone..."
    named-checkzone "$ZONE_NAME" "$ZONE_FILE"
    local EXIT_CODE=$?
fi

# Handle errors by deleting the file
if [ $EXIT_CODE -ne 0 ]; then
    $QUIET || echo "❌ ERROR detected in '$ZONE_FILE'. Deleting error file."
    [ -f "$ZONE_FILE" ] && rm "$ZONE_FILE"
    return 3
fi

$QUIET || echo "✅ PASSED: '$ZONE_FILE' is a valid zone."
return 0

}

replacefile() {
local SRC_FILE=“$1”
local DST_FILE=“$2”

# Input Validation
if [ -z "$SRC_FILE" ] || [ -z "$DST_FILE" ]; then
    $QUIET || echo "Usage: replacefile <src_file> <dst_file>"
    return 1
fi

# Check if source file exists before doing anything
if [ -f "$SRC_FILE" ]; then

    # Move source to destination (overwriting destination if it exists)
    if mv "$SRC_FILE" "$DST_FILE"; then
        return 0
    else
        return 1
    fi
else
    # If src doesn't exist, we do nothing and return a distinct exit code (e.g., 4)
    return 4
fi

}

while [[ $# -gt 0 ]]; do
case $1 in
-h|–help)
echo “IpfireDblDownloader.sh [-q] <blocklist … >”
shift
exit 1
;;
-q|–quiet)
QUIET=true
shift
;;
*)
BLOCKLIST+=(“$1”)
shift
;;
esac
done

#mkdir -p ${RPZPATH}
if [ ! -d “$RPZPATH” ]; then
echo “Error: Directory ‘$RPZPATH’ does not exist.” >&2
echo "Create Directory or change RPZPATH in script "
echo " make sure about permission and bind config "
exit 1
fi

for BLOCK in “${BLOCKLIST[@]}”; do
download_new ${RPZPATH}“/”$BLOCK".rpz" https://dbl.ipfire.org/lists/${BLOCK}/rpz.zone
zonefilter ${BLOCK}“.rpz.ipfire.org” ${RPZPATH}“/”$BLOCK".rpz.new"
replacefile ${RPZPATH}“/”$BLOCK".rpz.new" ${RPZPATH}“/”$BLOCK".rpz"
done
rndc reload

######################## end ####################

We don’t currently run any rate limiting on the XFR over DNS.

But the script that you are posting is an incredibly poorly designed workaround for what you want to achieve. It sends various HTTP requests and on HTTP we actually do have rate limiting to avoid people pulling the same zone data over and over again.