Bug: network blocklist is flushed and refilled line by line — the firewall is partly open for minutes on every update (fix included)
-
While investigating hourly API timeouts on
POST /api/v1/network/blocklistI found that every blocklist update leaves the server partly unprotected for several minutes. I measured it, read the code (10.1.5, unchanged in master), and wrote a drop-in replacement forsetblocklist.shthat fixes it. I think this is also the root cause behind several older threads.What happens
src/scripts/setblocklist.shfirst runsipset flush cloudron_blocklistand then re-adds every line with a separateipset addprocess. With a list of ~110,000 entries that is ~110,000 process starts in a row. Until the loop is done, everything not yet re-added is simply not blocked — and the entries at the end of the file come back last.Measured (ipset size sampled every 15 s during a normal update on a 10-core server):
10:59:51 89,645 entries 11:00:21 3,474 <- flushed 11:01:21 28,960 11:02:22 54,211 11:03:22 71,989 11:04:07 89,658 <- complete againSo roughly 4 minutes with on average half the list missing — on a quiet server. When the server is busy the loop takes longer than 10 minutes, the API call times out on the client side, and the hole lasts accordingly. With an hourly sync that is 7 % of the time or more with a partly open firewall.
Two more problems in the same code path
- No serialization.
network.setBlocklist()writesblocklist.txtand startssetblocklist.shwithout any lock. If a second call arrives while the first script is still running (easy when the first one takes 10+ minutes, or when a client gives up and retries), the second script flushes the set in the middle of the first one's loop, while the first one keeps reading a file that has just been rewritten. - Partial apply on error. The script runs with
set -e. If oneipset addfails, the script stops and the rest of the list is never loaded until the next update — while the database says the full list is active. That matches what was described in #10568 ("Blocked IP addresses in Cloudron but still able to access WordPress sites": "the block list never completely applied").
Fix: load into a temporary set and swap atomically
ipsethas exactly the tools for this: build the new list in a temporary set with a singleipset restore(one process instead of 110,000), thenipset swapit with the live set. The swap is atomic, the iptables rules keep pointing at the same set name, and the live set is never empty. Aflockserializes concurrent calls. If any entry is rejected,ipset restorefails as a whole and the old list stays fully active instead of a partial one.Measured on the same server with the same
blocklist.txt(114,330 entries, 24,670 of them IPv6), loaded into separate test sets:ipset restore2.2 seconds,ipset swap3 milliseconds — instead of 4 to 10+ minutes with a partly empty firewall. The resulting set had exactly the same number of entries as the live one.Drop-in replacement for
src/scripts/setblocklist.sh(same input file, same set names, same--check) :#!/bin/bash set -eu -o pipefail if [[ ${EUID} -ne 0 ]]; then echo "This script should be run as root." > /dev/stderr exit 1 fi if [[ $# == 1 && "$1" == "--check" ]]; then echo "OK" exit 0 fi [[ "${BOX_ENV}" == "test" ]] && exit exec 9>/run/cloudron-setblocklist.lock flock 9 # one blocklist update at a time user_firewall_json="/home/yellowtent/platformdata/firewall/blocklist.txt" has_ipv6=0 ipset list -n cloudron_blocklist6 >/dev/null 2>&1 && has_ipv6=1 # kernel may not have ipv6 # same type and maxelem as in cloudron-firewall.sh, otherwise swap refuses ipset destroy cloudron_blocklist_new 2>/dev/null || true ipset create cloudron_blocklist_new hash:net maxelem 262144 if [[ ${has_ipv6} == 1 ]]; then ipset destroy cloudron_blocklist6_new 2>/dev/null || true ipset create cloudron_blocklist6_new hash:net family inet6 maxelem 262144 fi if [[ -f "${user_firewall_json}" ]]; then # without the -n block, any last line without a new line won't be read it! while read -r line || [[ -n "$line" ]]; do [[ -z "${line}" ]] && continue # ignore empty lines [[ "$line" =~ ^#.*$ ]] && continue # ignore lines starting with # if [[ "$line" == *":"* ]]; then if [[ ${has_ipv6} == 1 ]]; then echo "add cloudron_blocklist6_new ${line}"; fi else echo "add cloudron_blocklist_new ${line}" fi done < "${user_firewall_json}" | ipset restore -exist # -exist ignores duplicates fi ipset swap cloudron_blocklist_new cloudron_blocklist ipset destroy cloudron_blocklist_new if [[ ${has_ipv6} == 1 ]]; then ipset swap cloudron_blocklist6_new cloudron_blocklist6 ipset destroy cloudron_blocklist6_new fiBackground — this has come up for years
- #1838 (2020): firewall restart takes 15–20 s — "the time the kernel is taking to add your blocklist to ipset".
- #3154 (2020): country lists cause response timeouts; fixed by removing the API timeout (
req.clearTimeout()), but the loading itself stayed line by line. - #3795 (2020–2024): many users keep blocklists up to date with scripts against the API ("1.5 minutes per API call").
- #6010 (2021): server crash after adding FireHOL level1.
- #10547 / #10568 (2023): "hash is full", and blocked IPs still getting through because the list never completely applied.
- #15536 (2026): body size limit raised to 20 MB — thanks! — which makes large lists possible, and makes the slow, non-atomic apply more visible.
- #15702 (2026): robi on large IP sets; the FSF/reaction approach he quotes is exactly this: export/restore instead of rule by rule, "nearly instantaneous".
A possible next step (not needed for the fix)
With an atomic apply that takes seconds, frequent full updates become cheap. An API to add or remove single entries (without resending the whole list) would be a nice follow-up for tools that react to attacks in real time, but the fix above is the important part: it closes the hole on every update, removes the timeouts and makes concurrent updates safe, without any change to the API, database or UI.
Happy to test a patched version on my servers.
- No serialization.
-
-
G girish has marked this topic as solved
Hello! It looks like you're interested in this conversation, but you don't have an account yet.
Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.
With your input, this post could be even better 💗
Register Login