From 928191dd286806ef255fb2fb22b0df2653e7ccb7 Mon Sep 17 00:00:00 2001 From: arlophoenix Date: Mon, 27 Jul 2026 22:06:18 +1200 Subject: [PATCH 1/2] Add a Block Creality Cloud Telemetry option The stock telemetry agent (alchemistp) uploads printer configs and Klipper logs to *.cxswyjy.com, which the crealitycloud.com-only blocklists in circulation do not cover at all. This adds a menu option that blackholes the 16 known Creality cloud hosts in /etc/hosts and drops outbound MQTT on tcp 1883/8883 to anything off the LAN. The rules are re-applied by an init script because the K1 2025 rootfs is a volatile RAM filesystem, so /etc/hosts reverts to stock on every boot, and iptables rules never survive a reboot on any model. It is a blocklist, not a firewall: the OUTPUT policy is untouched and nothing else is filtered, so the cloud relay options the helper installs keep working. iptables is missing entirely on the K1 and Ender-3 V3 series, so the port rules are skipped there and only the hosts blocklist is applied. --- files/services/S995cloudblock | 184 ++++++++++++++++++ scripts/block_creality_cloud.sh | 78 ++++++++ scripts/menu/10SE/customize_menu_10SE.sh | 15 ++ scripts/menu/10SE/info_menu_10SE.sh | 1 + scripts/menu/3KE/customize_menu_3KE.sh | 15 ++ scripts/menu/3KE/info_menu_3KE.sh | 1 + scripts/menu/3V3/customize_menu_3V3.sh | 15 ++ scripts/menu/3V3/info_menu_3V3.sh | 1 + scripts/menu/E5M/customize_menu_E5M.sh | 15 ++ scripts/menu/E5M/info_menu_E5M.sh | 1 + scripts/menu/K1/customize_menu_K1.sh | 15 ++ scripts/menu/K1/info_menu_K1.sh | 1 + .../menu/K1_2025/customize_menu_K1C_2025.sh | 15 ++ scripts/menu/K1_2025/info_menu_K1C_2025.sh | 1 + scripts/paths.sh | 4 + 15 files changed, 362 insertions(+) create mode 100755 files/services/S995cloudblock create mode 100644 scripts/block_creality_cloud.sh diff --git a/files/services/S995cloudblock b/files/services/S995cloudblock new file mode 100755 index 0000000..d2ecb33 --- /dev/null +++ b/files/services/S995cloudblock @@ -0,0 +1,184 @@ +#!/bin/sh +# +# Block Creality Cloud Telemetry +# Credit: C0DEbrained +# +# Blackholes the hosts the stock Creality daemons use for telemetry, remote +# access and OTA checks, and drops outbound MQTT to anything off the LAN. +# +# Re-applied at every boot on purpose: iptables rules never survive a reboot on +# any model, and on the K1 2025 the rootfs is a volatile RAM filesystem, so +# /etc/hosts reverts to stock at every start. +# +# This is a blocklist, not a firewall. The OUTPUT policy is left untouched and +# nothing else is filtered, so the cloud relay options the helper installs +# (OctoEverywhere, Obico, GuppyFLO, SimplyPrint, OctoApp, Mobileraker) keep +# working. + +HOSTS="/etc/hosts" +MARKER="# Creality Helper Script - Block Creality Cloud Telemetry" +MQTT_PORTS="1883 8883" + +# Hosts found in the stock vectorp and onyxp binaries, in alchemistp's +# config.json, and on the wire during a print. +BLACKHOLE=" +api.crealitycloud.com +mqtt.crealitycloud.com +cxsw-cdn.crealitycloud.com +model-cdn.crealitycloud.com +admin-pre.crealitycloud.com +pre-tb-iot.crealitycloud.com +www.crealitycloud.com +api.crealitycloud.cn +mqtt.crealitycloud.cn +admin-pre.crealitycloud.cn +api-dev.crealitycloud.cn +www.crealitycloud.cn +c-smart.cxswyjy.com +c-smart-cn-local.cxswyjy.com +devdata.cxswyjy.com +www.creality.com +" + +# The K1 and Ender-3 V3 series ship without an iptables binary even though the +# kernel module is loaded, so every rule below is optional. The hosts blocklist +# is applied either way. +have_iptables() { + command -v iptables > /dev/null 2>&1 && iptables -L OUTPUT -n > /dev/null 2>&1 +} + +# Directly attached subnets, so a local MQTT broker stays reachable. Never +# guess: no route, no LAN exemption. +lan_networks() { + ip route 2>/dev/null | awk '/proto kernel/ && /src/ { print $1 }' +} + +# ACCEPT rules are inserted at the top so they always precede the DROP rules, +# even if the network came up after a first pass added the drops alone. +accept_rule() { + iptables -C "$@" 2>/dev/null || iptables -I "$@" +} + +drop_rule() { + iptables -C "$@" 2>/dev/null || iptables -A "$@" +} + +delete_rule() { + while iptables -C "$@" 2>/dev/null; do + iptables -D "$@" 2>/dev/null || break + done +} + +# Appending to a file whose last line has no newline would corrupt that line. +hosts_end_with_newline() { + [ -s "$HOSTS" ] || return 0 + [ -z "$(tail -c 1 "$HOSTS")" ] || printf '\n' >> "$HOSTS" +} + +# Only ever matches the lines this script writes, so an entry a user added by +# hand for the same host is left alone on removal. +host_pattern() { + echo "^0\.0\.0\.0[[:space:]][[:space:]]*$(echo "$1" | sed 's/\./\\./g')\$" +} + +hosts_block() { + local host + hosts_end_with_newline + grep -q "^${MARKER}\$" "$HOSTS" 2>/dev/null || printf '%s\n' "$MARKER" >> "$HOSTS" + for host in $BLACKHOLE; do + grep -q "$(host_pattern "$host")" "$HOSTS" 2>/dev/null || printf '0.0.0.0 %s\n' "$host" >> "$HOSTS" + done +} + +hosts_unblock() { + local host + [ -f "$HOSTS" ] || return 0 + for host in $BLACKHOLE; do + sed -i "/$(host_pattern "$host")/d" "$HOSTS" + done + sed -i "/^${MARKER}\$/d" "$HOSTS" +} + +# At boot this can run before DHCP has finished, which would add the drops with +# no LAN exemption and leave a local MQTT broker unreachable until the next +# boot. Wait briefly, but never hold up a printer that has no network at all. +wait_for_lan() { + local waited=0 + while [ -z "$(lan_networks)" ] && [ "$waited" -lt 5 ]; do + sleep 1 + waited=$((waited + 1)) + done +} + +mqtt_block() { + local lan + local port + have_iptables || return 0 + wait_for_lan + for port in $MQTT_PORTS; do + # 127.0.0.1 is not in the LAN CIDR, and a blackholed host resolves to + # 0.0.0.0, which the kernel routes to loopback. Without this the drop below + # silently swallows that traffic instead of letting it fail fast with a + # RST. Every rule is scoped to an MQTT port so that removing it can never + # take out a broader rule another tool owns. + accept_rule OUTPUT -o lo -p tcp --dport "$port" -j ACCEPT + for lan in $(lan_networks); do + accept_rule OUTPUT -p tcp -d "$lan" --dport "$port" -j ACCEPT + done + # vectorp carries hardcoded broker IPs (47.114.48.45:1883 and + # 120.55.101.240:1883) that bypass DNS entirely, so the port rules are what + # actually stops it. + drop_rule OUTPUT -p tcp --dport "$port" -j DROP + done +} + +mqtt_unblock() { + local lan + local port + have_iptables || return 0 + for port in $MQTT_PORTS; do + delete_rule OUTPUT -p tcp --dport "$port" -j DROP + for lan in $(lan_networks); do + delete_rule OUTPUT -p tcp -d "$lan" --dport "$port" -j ACCEPT + done + delete_rule OUTPUT -o lo -p tcp --dport "$port" -j ACCEPT + done +} + +case "$1" in + start) + echo "Blocking Creality cloud telemetry..." + hosts_block + mqtt_block + ;; + stop) + echo "Unblocking Creality cloud telemetry..." + mqtt_unblock + hosts_unblock + ;; + restart|force-reload) + "$0" stop + "$0" start + ;; + status) + blocked=0 + total=0 + for host in $BLACKHOLE; do + total=$((total + 1)) + if grep -q "$(host_pattern "$host")" "$HOSTS" 2>/dev/null; then + blocked=$((blocked + 1)) + fi + done + echo "Hosts blocklist: $blocked of $total entries present" + if have_iptables; then + iptables -L OUTPUT -v -n + else + echo "MQTT port rules: skipped (iptables not available)" + fi + ;; + *) + echo "Usage: $0 {start|stop|restart|force-reload|status}" + exit 1 +esac + +exit 0 diff --git a/scripts/block_creality_cloud.sh b/scripts/block_creality_cloud.sh new file mode 100644 index 0000000..03f6f54 --- /dev/null +++ b/scripts/block_creality_cloud.sh @@ -0,0 +1,78 @@ +#!/bin/sh + +set -e + +function block_creality_cloud_message(){ + top_line + title 'Block Creality Cloud Telemetry' "${yellow}" + inner_line + hr + echo -e " │ ${cyan}This allows to blackhole the Creality cloud hosts used by ${white}│" + echo -e " │ ${cyan}stock daemons for telemetry, remote access and OTA checks, ${white}│" + echo -e " │ ${cyan}and to drop outbound MQTT to anything outside your network. ${white}│" + hr + echo -e " │ ${cyan}Remote access options (OctoEverywhere, Obico, GuppyFLO, ${white}│" + echo -e " │ ${cyan}SimplyPrint, OctoApp, Mobileraker) are not affected. ${white}│" + hr + bottom_line +} + +function install_block_creality_cloud(){ + block_creality_cloud_message + local yn + while true; do + install_msg "Block Creality Cloud Telemetry" yn + case "${yn}" in + Y|y) + echo -e "${white}" + echo -e "Info: Copying service file..." + cp "$CLOUD_BLOCK_SERVICE_URL" "$CLOUD_BLOCK_SERVICE_FILE" + chmod 755 "$CLOUD_BLOCK_SERVICE_FILE" + echo -e "Info: Applying blocklist..." + set +e + "$CLOUD_BLOCK_SERVICE_FILE" start + set -e + ok_msg "Block Creality Cloud Telemetry has been installed successfully!" + if ! command -v iptables > /dev/null 2>&1; then + echo -e " ${darkred}Note: iptables is not available on this printer, MQTT port rules were skipped.${white}" + echo -e " ${darkred}The hosts blocklist is still active.${white}" + echo + fi + echo -e " ${white}Connections already open are not torn down, reboot to apply it fully.${white}" + echo + return;; + N|n) + error_msg "Installation canceled!" + return;; + *) + error_msg "Please select a correct choice!";; + esac + done +} + +function remove_block_creality_cloud(){ + block_creality_cloud_message + local yn + while true; do + remove_msg "Block Creality Cloud Telemetry" yn + case "${yn}" in + Y|y) + echo -e "${white}" + echo -e "Info: Restoring hosts file and removing rules..." + set +e + if [ -f "$CLOUD_BLOCK_SERVICE_FILE" ]; then + "$CLOUD_BLOCK_SERVICE_FILE" stop + fi + set -e + echo -e "Info: Removing file..." + rm -f "$CLOUD_BLOCK_SERVICE_FILE" + ok_msg "Block Creality Cloud Telemetry has been removed successfully!" + return;; + N|n) + error_msg "Deletion canceled!" + return;; + *) + error_msg "Please select a correct choice!";; + esac + done +} diff --git a/scripts/menu/10SE/customize_menu_10SE.sh b/scripts/menu/10SE/customize_menu_10SE.sh index ca339a2..b776b31 100755 --- a/scripts/menu/10SE/customize_menu_10SE.sh +++ b/scripts/menu/10SE/customize_menu_10SE.sh @@ -15,6 +15,9 @@ function customize_menu_ui_10se() { hr menu_option '5' 'Install' 'Creality Dynamic Logos for Fluidd' hr + menu_option '6' 'Install' 'Block Creality Cloud Telemetry' + menu_option '7' 'Remove' 'Block Creality Cloud Telemetry' + hr inner_line hr bottom_menu_option 'b' 'Back to [Main Menu]' "${yellow}" @@ -79,6 +82,18 @@ function customize_menu_10se() { else run "install_creality_dynamic_logos" "customize_menu_ui_10se" fi;; + 6) + if [ -f "$CLOUD_BLOCK_SERVICE_FILE" ]; then + error_msg "Block Creality Cloud Telemetry is already installed!" + else + run "install_block_creality_cloud" "customize_menu_ui_10se" + fi;; + 7) + if [ ! -f "$CLOUD_BLOCK_SERVICE_FILE" ]; then + error_msg "Block Creality Cloud Telemetry is not installed!" + else + run "remove_block_creality_cloud" "customize_menu_ui_10se" + fi;; B|b) clear; main_menu; break;; Q|q) diff --git a/scripts/menu/10SE/info_menu_10SE.sh b/scripts/menu/10SE/info_menu_10SE.sh index f9769cb..a7d0581 100755 --- a/scripts/menu/10SE/info_menu_10SE.sh +++ b/scripts/menu/10SE/info_menu_10SE.sh @@ -66,6 +66,7 @@ function info_menu_ui_10se() { info_line "$(check_file_10se "$CREALITY_WEB_FILE")" 'Creality Web Interface' info_line "$(check_folder_10se "$GUPPY_SCREEN_FOLDER")" 'Guppy Screen' info_line "$(check_file_10se "$FLUIDD_LOGO_FILE")" 'Creality Dynamic Logos for Fluidd' + info_line "$(check_file_10se "$CLOUD_BLOCK_SERVICE_FILE")" 'Block Creality Cloud Telemetry' hr inner_line hr diff --git a/scripts/menu/3KE/customize_menu_3KE.sh b/scripts/menu/3KE/customize_menu_3KE.sh index 5f29bff..d99fdfe 100755 --- a/scripts/menu/3KE/customize_menu_3KE.sh +++ b/scripts/menu/3KE/customize_menu_3KE.sh @@ -15,6 +15,9 @@ function customize_menu_ui_3ke() { hr menu_option '5' 'Install' 'Creality Dynamic Logos for Fluidd' hr + menu_option '6' 'Install' 'Block Creality Cloud Telemetry' + menu_option '7' 'Remove' 'Block Creality Cloud Telemetry' + hr inner_line hr bottom_menu_option 'b' 'Back to [Main Menu]' "${yellow}" @@ -79,6 +82,18 @@ function customize_menu_3ke() { else run "install_creality_dynamic_logos" "customize_menu_ui_3ke" fi;; + 6) + if [ -f "$CLOUD_BLOCK_SERVICE_FILE" ]; then + error_msg "Block Creality Cloud Telemetry is already installed!" + else + run "install_block_creality_cloud" "customize_menu_ui_3ke" + fi;; + 7) + if [ ! -f "$CLOUD_BLOCK_SERVICE_FILE" ]; then + error_msg "Block Creality Cloud Telemetry is not installed!" + else + run "remove_block_creality_cloud" "customize_menu_ui_3ke" + fi;; B|b) clear; main_menu; break;; Q|q) diff --git a/scripts/menu/3KE/info_menu_3KE.sh b/scripts/menu/3KE/info_menu_3KE.sh index 75673ac..503797c 100755 --- a/scripts/menu/3KE/info_menu_3KE.sh +++ b/scripts/menu/3KE/info_menu_3KE.sh @@ -68,6 +68,7 @@ function info_menu_ui_3ke() { info_line "$(check_file_3ke "$CREALITY_WEB_FILE")" 'Creality Web Interface' info_line "$(check_folder_3ke "$GUPPY_SCREEN_FOLDER")" 'Guppy Screen' info_line "$(check_file_3ke "$FLUIDD_LOGO_FILE")" 'Creality Dynamic Logos for Fluidd' + info_line "$(check_file_3ke "$CLOUD_BLOCK_SERVICE_FILE")" 'Block Creality Cloud Telemetry' hr inner_line hr diff --git a/scripts/menu/3V3/customize_menu_3V3.sh b/scripts/menu/3V3/customize_menu_3V3.sh index 295b747..769d4e8 100755 --- a/scripts/menu/3V3/customize_menu_3V3.sh +++ b/scripts/menu/3V3/customize_menu_3V3.sh @@ -15,6 +15,9 @@ function customize_menu_ui_3v3() { hr menu_option '5' 'Install' 'Creality Dynamic Logos for Fluidd' hr + menu_option '6' 'Install' 'Block Creality Cloud Telemetry' + menu_option '7' 'Remove' 'Block Creality Cloud Telemetry' + hr inner_line hr bottom_menu_option 'b' 'Back to [Main Menu]' "${yellow}" @@ -79,6 +82,18 @@ function customize_menu_3v3() { else run "install_creality_dynamic_logos" "customize_menu_ui_3v3" fi;; + 6) + if [ -f "$CLOUD_BLOCK_SERVICE_FILE" ]; then + error_msg "Block Creality Cloud Telemetry is already installed!" + else + run "install_block_creality_cloud" "customize_menu_ui_3v3" + fi;; + 7) + if [ ! -f "$CLOUD_BLOCK_SERVICE_FILE" ]; then + error_msg "Block Creality Cloud Telemetry is not installed!" + else + run "remove_block_creality_cloud" "customize_menu_ui_3v3" + fi;; B|b) clear; main_menu; break;; Q|q) diff --git a/scripts/menu/3V3/info_menu_3V3.sh b/scripts/menu/3V3/info_menu_3V3.sh index b7abe0a..6806e31 100755 --- a/scripts/menu/3V3/info_menu_3V3.sh +++ b/scripts/menu/3V3/info_menu_3V3.sh @@ -70,6 +70,7 @@ function info_menu_ui_3v3() { info_line "$(check_file_3v3 "$CREALITY_WEB_FILE")" 'Creality Web Interface' info_line "$(check_folder_3v3 "$GUPPY_SCREEN_FOLDER")" 'Guppy Screen' info_line "$(check_file_3v3 "$FLUIDD_LOGO_FILE")" 'Creality Dynamic Logos for Fluidd' + info_line "$(check_file_3v3 "$CLOUD_BLOCK_SERVICE_FILE")" 'Block Creality Cloud Telemetry' hr inner_line hr diff --git a/scripts/menu/E5M/customize_menu_E5M.sh b/scripts/menu/E5M/customize_menu_E5M.sh index cbf915f..3930e48 100755 --- a/scripts/menu/E5M/customize_menu_E5M.sh +++ b/scripts/menu/E5M/customize_menu_E5M.sh @@ -15,6 +15,9 @@ function customize_menu_ui_e5m() { hr menu_option '5' 'Install' 'Creality Dynamic Logos for Fluidd' hr + menu_option '6' 'Install' 'Block Creality Cloud Telemetry' + menu_option '7' 'Remove' 'Block Creality Cloud Telemetry' + hr inner_line hr bottom_menu_option 'b' 'Back to [Main Menu]' "${yellow}" @@ -79,6 +82,18 @@ function customize_menu_e5m() { else run "install_creality_dynamic_logos" "customize_menu_ui_e5m" fi;; + 6) + if [ -f "$CLOUD_BLOCK_SERVICE_FILE" ]; then + error_msg "Block Creality Cloud Telemetry is already installed!" + else + run "install_block_creality_cloud" "customize_menu_ui_e5m" + fi;; + 7) + if [ ! -f "$CLOUD_BLOCK_SERVICE_FILE" ]; then + error_msg "Block Creality Cloud Telemetry is not installed!" + else + run "remove_block_creality_cloud" "customize_menu_ui_e5m" + fi;; B|b) clear; main_menu; break;; Q|q) diff --git a/scripts/menu/E5M/info_menu_E5M.sh b/scripts/menu/E5M/info_menu_E5M.sh index b2aaa4f..de1b9ee 100755 --- a/scripts/menu/E5M/info_menu_E5M.sh +++ b/scripts/menu/E5M/info_menu_E5M.sh @@ -74,6 +74,7 @@ function info_menu_ui_e5m() { info_line "$(check_file_e5m "$CREALITY_WEB_FILE")" 'Creality Web Interface' info_line "$(check_folder_e5m "$GUPPY_SCREEN_FOLDER")" 'Guppy Screen' info_line "$(check_file_e5m "$FLUIDD_LOGO_FILE")" 'Creality Dynamic Logos for Fluidd' + info_line "$(check_file_e5m "$CLOUD_BLOCK_SERVICE_FILE")" 'Block Creality Cloud Telemetry' hr inner_line hr diff --git a/scripts/menu/K1/customize_menu_K1.sh b/scripts/menu/K1/customize_menu_K1.sh index ec33b71..9907d1a 100755 --- a/scripts/menu/K1/customize_menu_K1.sh +++ b/scripts/menu/K1/customize_menu_K1.sh @@ -18,6 +18,9 @@ function customize_menu_ui_k1() { hr menu_option '7' 'Install' 'Creality Dynamic Logos for Fluidd' hr + menu_option '8' 'Install' 'Block Creality Cloud Telemetry' + menu_option '9' 'Remove' 'Block Creality Cloud Telemetry' + hr inner_line hr bottom_menu_option 'b' 'Back to [Main Menu]' "${yellow}" @@ -98,6 +101,18 @@ function customize_menu_k1() { else run "install_creality_dynamic_logos" "customize_menu_ui_k1" fi;; + 8) + if [ -f "$CLOUD_BLOCK_SERVICE_FILE" ]; then + error_msg "Block Creality Cloud Telemetry is already installed!" + else + run "install_block_creality_cloud" "customize_menu_ui_k1" + fi;; + 9) + if [ ! -f "$CLOUD_BLOCK_SERVICE_FILE" ]; then + error_msg "Block Creality Cloud Telemetry is not installed!" + else + run "remove_block_creality_cloud" "customize_menu_ui_k1" + fi;; B|b) clear; main_menu; break;; Q|q) diff --git a/scripts/menu/K1/info_menu_K1.sh b/scripts/menu/K1/info_menu_K1.sh index 6639571..f6e3bf3 100755 --- a/scripts/menu/K1/info_menu_K1.sh +++ b/scripts/menu/K1/info_menu_K1.sh @@ -74,6 +74,7 @@ function info_menu_ui_k1() { info_line "$(check_file_k1 "$CREALITY_WEB_FILE")" 'Creality Web Interface' info_line "$(check_folder_k1 "$GUPPY_SCREEN_FOLDER")" 'Guppy Screen' info_line "$(check_file_k1 "$FLUIDD_LOGO_FILE")" 'Creality Dynamic Logos for Fluidd' + info_line "$(check_file_k1 "$CLOUD_BLOCK_SERVICE_FILE")" 'Block Creality Cloud Telemetry' hr inner_line hr diff --git a/scripts/menu/K1_2025/customize_menu_K1C_2025.sh b/scripts/menu/K1_2025/customize_menu_K1C_2025.sh index 83e7601..436d37c 100755 --- a/scripts/menu/K1_2025/customize_menu_K1C_2025.sh +++ b/scripts/menu/K1_2025/customize_menu_K1C_2025.sh @@ -9,6 +9,9 @@ function customize_menu_ui_k1_2025() { hr menu_option '1' 'Install' 'Creality Dynamic Logos for Fluidd' hr + menu_option '2' 'Install' 'Block Creality Cloud Telemetry' + menu_option '3' 'Remove' 'Block Creality Cloud Telemetry' + hr inner_line hr bottom_menu_option 'b' 'Back to [Main Menu]' "${yellow}" @@ -33,6 +36,18 @@ function customize_menu_k1_2025() { else run "install_creality_dynamic_logos" "customize_menu_ui_k1_2025" fi;; + 2) + if [ -f "$CLOUD_BLOCK_SERVICE_FILE" ]; then + error_msg "Block Creality Cloud Telemetry is already installed!" + else + run "install_block_creality_cloud" "customize_menu_ui_k1_2025" + fi;; + 3) + if [ ! -f "$CLOUD_BLOCK_SERVICE_FILE" ]; then + error_msg "Block Creality Cloud Telemetry is not installed!" + else + run "remove_block_creality_cloud" "customize_menu_ui_k1_2025" + fi;; B|b) clear; main_menu; break;; Q|q) diff --git a/scripts/menu/K1_2025/info_menu_K1C_2025.sh b/scripts/menu/K1_2025/info_menu_K1C_2025.sh index aee1326..685e4c7 100755 --- a/scripts/menu/K1_2025/info_menu_K1C_2025.sh +++ b/scripts/menu/K1_2025/info_menu_K1C_2025.sh @@ -84,6 +84,7 @@ function info_menu_ui_k1_2025() { hr subtitle '•CUSTOMIZATION:' info_line "$(check_file_k1_2025 "$FLUIDD_LOGO_FILE")" 'Creality Dynamic Logos for Fluidd' + info_line "$(check_file_k1_2025 "$CLOUD_BLOCK_SERVICE_FILE")" 'Block Creality Cloud Telemetry' hr inner_line hr diff --git a/scripts/paths.sh b/scripts/paths.sh index 06060df..40ef61e 100755 --- a/scripts/paths.sh +++ b/scripts/paths.sh @@ -222,6 +222,10 @@ function set_paths() { GO2RTC_CONFIG_FILE_URL="${HS_FILES}/go2rtc/go2rtc.yaml" GO2RTC_SERVICE_URL="${HS_FILES}/services/S50go2rtc" GO2RTC_SERVICE_FILE="${INITD_FOLDER}/S50go2rtc" + + # Block Creality Cloud Telemetry # + CLOUD_BLOCK_SERVICE_URL="${HS_FILES}/services/S995cloudblock" + CLOUD_BLOCK_SERVICE_FILE="${INITD_FOLDER}/S995cloudblock" } function set_permissions() { From c4751dfa20400fd7c420e78e1d9961b10c34f7a4 Mon Sep 17 00:00:00 2001 From: arlophoenix Date: Tue, 28 Jul 2026 08:19:01 +1200 Subject: [PATCH 2/2] Own the hosts entries and the firewall rules explicitly Review found that both halves of this option identified their own state by matching content, so uninstall could remove things it never created. A user who had already blackholed one of these hosts by hand lost that line, and an identical MQTT rule belonging to another tool was deleted along with ours. The hosts entries now live between BEGIN and END markers and are removed as a range, and the firewall rules live in a CREALITY_CLOUDBLOCK chain that is flushed and deleted on uninstall. Both are rebuilt from scratch on every start, so a changed LAN, a hand-edited entry, or a host list that grew between versions self-heal instead of accumulating. Removing the content matching also removed the need to recompute the LAN at uninstall time, which used to strand ACCEPT rules for a subnet the printer had moved off. The chain is linked with an insert rather than an append, because an OUTPUT chain that already carries a broad ACCEPT would otherwise match before the drops and the block would silently do nothing. Rule failures are no longer swallowed: iptables calls take -w where the build supports it, a present-but-unusable iptables is now distinguished from a model that has none, and start propagates a non-zero exit so the installer stops reporting success after a partial apply. Also pins umask 022 for the same reason S56moonraker_service does, stops requiring "proto kernel" in ip route output, and keeps the guard against a hosts file with no trailing newline, without which the begin marker would join onto the last line and hide the block from the range delete. --- files/services/S995cloudblock | 248 ++++++++++++++++++++------------ scripts/block_creality_cloud.sh | 10 +- 2 files changed, 167 insertions(+), 91 deletions(-) mode change 100644 => 100755 scripts/block_creality_cloud.sh diff --git a/files/services/S995cloudblock b/files/services/S995cloudblock index d2ecb33..e4efbe2 100755 --- a/files/services/S995cloudblock +++ b/files/services/S995cloudblock @@ -11,13 +11,24 @@ # /etc/hosts reverts to stock at every start. # # This is a blocklist, not a firewall. The OUTPUT policy is left untouched and -# nothing else is filtered, so the cloud relay options the helper installs -# (OctoEverywhere, Obico, GuppyFLO, SimplyPrint, OctoApp, Mobileraker) keep +# nothing else is filtered, so the cloud relay options the helper installs keep # working. +# +# Everything written here is owned and delimited: the hosts entries live between +# two markers, the firewall rules live in their own chain. Nothing is matched by +# content, so uninstall cannot remove a hosts line or a firewall rule that +# something else created. + +# A root SSH shell on CrealityOS inherits umask 077 while boot uses 022, so pin +# it here, same reason S56moonraker_service does. +umask 022 HOSTS="/etc/hosts" -MARKER="# Creality Helper Script - Block Creality Cloud Telemetry" +BEGIN_MARKER="# BEGIN Creality Helper Script - Block Creality Cloud Telemetry" +END_MARKER="# END Creality Helper Script - Block Creality Cloud Telemetry" +CHAIN="CREALITY_CLOUDBLOCK" MQTT_PORTS="1883 8883" +LAN_WAIT_SECONDS=5 # Hosts found in the stock vectorp and onyxp binaries, in alchemistp's # config.json, and on the wire during a print. @@ -40,145 +51,202 @@ devdata.cxswyjy.com www.creality.com " -# The K1 and Ender-3 V3 series ship without an iptables binary even though the -# kernel module is loaded, so every rule below is optional. The hosts blocklist -# is applied either way. -have_iptables() { - command -v iptables > /dev/null 2>&1 && iptables -L OUTPUT -n > /dev/null 2>&1 -} - -# Directly attached subnets, so a local MQTT broker stays reachable. Never -# guess: no route, no LAN exemption. -lan_networks() { - ip route 2>/dev/null | awk '/proto kernel/ && /src/ { print $1 }' -} - -# ACCEPT rules are inserted at the top so they always precede the DROP rules, -# even if the network came up after a first pass added the drops alone. -accept_rule() { - iptables -C "$@" 2>/dev/null || iptables -I "$@" -} - -drop_rule() { - iptables -C "$@" 2>/dev/null || iptables -A "$@" -} +IPT="" +IPTABLES_STATE="" -delete_rule() { - while iptables -C "$@" 2>/dev/null; do - iptables -D "$@" 2>/dev/null || break - done +# The K1 and Ender-3 V3 series ship without an iptables binary even though the +# kernel module is loaded, which is a normal state this script tolerates. A +# binary that is present but unusable is a real error and must not be reported +# as "this model has no iptables". +probe_iptables() { + [ -n "$IPTABLES_STATE" ] && return 0 + if ! command -v iptables > /dev/null 2>&1; then + IPTABLES_STATE="absent" + return 0 + fi + # -w serialises against anything else writing the same table. iptables 1.8 + # knows the flag, older builds do not, so fall back rather than fail every + # call. + if iptables -w 5 -L OUTPUT -n > /dev/null 2>&1; then + IPT="iptables -w 5" + IPTABLES_STATE="ok" + elif iptables -L OUTPUT -n > /dev/null 2>&1; then + IPT="iptables" + IPTABLES_STATE="ok" + else + IPTABLES_STATE="error" + fi } -# Appending to a file whose last line has no newline would corrupt that line. -hosts_end_with_newline() { - [ -s "$HOSTS" ] || return 0 - [ -z "$(tail -c 1 "$HOSTS")" ] || printf '\n' >> "$HOSTS" +ipt() { + $IPT "$@" } -# Only ever matches the lines this script writes, so an entry a user added by -# hand for the same host is left alone on removal. -host_pattern() { - echo "^0\.0\.0\.0[[:space:]][[:space:]]*$(echo "$1" | sed 's/\./\\./g')\$" +# Tested with -S rather than -C so this works on iptables builds predating -C. +chain_is_linked() { + ipt -S OUTPUT 2>/dev/null | grep -q -- "-j $CHAIN" } -hosts_block() { - local host - hosts_end_with_newline - grep -q "^${MARKER}\$" "$HOSTS" 2>/dev/null || printf '%s\n' "$MARKER" >> "$HOSTS" - for host in $BLACKHOLE; do - grep -q "$(host_pattern "$host")" "$HOSTS" 2>/dev/null || printf '0.0.0.0 %s\n' "$host" >> "$HOSTS" - done -} - -hosts_unblock() { - local host - [ -f "$HOSTS" ] || return 0 - for host in $BLACKHOLE; do - sed -i "/$(host_pattern "$host")/d" "$HOSTS" - done - sed -i "/^${MARKER}\$/d" "$HOSTS" +# Directly attached subnets, so a local MQTT broker stays reachable. Never +# guess: no route, no LAN exemption. Matched on "src" plus a CIDR in the first +# field rather than on "proto kernel", which not every ip implementation prints. +lan_networks() { + ip route 2>/dev/null | awk '/[[:space:]]src[[:space:]]/ && $1 ~ /\// { print $1 }' } -# At boot this can run before DHCP has finished, which would add the drops with -# no LAN exemption and leave a local MQTT broker unreachable until the next +# At boot this can run before DHCP has finished, which would build the chain +# with no LAN exemption and leave a local MQTT broker unreachable until the next # boot. Wait briefly, but never hold up a printer that has no network at all. wait_for_lan() { local waited=0 - while [ -z "$(lan_networks)" ] && [ "$waited" -lt 5 ]; do + while [ -z "$(lan_networks)" ] && [ "$waited" -lt "$LAN_WAIT_SECONDS" ]; do sleep 1 waited=$((waited + 1)) done } +# Everything outside the managed region, verbatim. A begin marker with no end +# marker deletes to EOF, which is the right recovery from an interrupted write. +hosts_without_block() { + [ -f "$HOSTS" ] || return 0 + sed "/^${BEGIN_MARKER}\$/,/^${END_MARKER}\$/d" "$HOSTS" +} + +# Rewritten in a single pass rather than appended line by line, so there is one +# window in which a concurrent writer could be clobbered instead of seventeen. +# Copied back with cat rather than mv to keep the original inode, mode and owner +# and to leave a symlinked /etc/hosts pointing where it did before. +hosts_write() { + local host + local last + local tmp="${HOSTS}.cloudblock.$$" + hosts_without_block > "$tmp" 2>/dev/null || { rm -f "$tmp"; return 1; } + if [ "$1" = "block" ]; then + # sed preserves a missing final newline, so without this the begin marker + # would be appended onto the end of the last existing line. That corrupts + # the line and, worse, hides the marker from the range delete on uninstall, + # stranding the whole block. Fails closed: if tail -c is unavailable the + # newline goes in anyway, because a blank line is harmless and a joined line + # is not. + if [ -s "$tmp" ]; then + last=$(tail -c 1 "$tmp" 2>/dev/null) || last="x" + [ -z "$last" ] || printf '\n' >> "$tmp" + fi + { + printf '%s\n' "$BEGIN_MARKER" + for host in $BLACKHOLE; do + printf '0.0.0.0 %s\n' "$host" + done + printf '%s\n' "$END_MARKER" + } >> "$tmp" || { rm -f "$tmp"; return 1; } + fi + cat "$tmp" > "$HOSTS" || { rm -f "$tmp"; return 1; } + rm -f "$tmp" +} + +# Rebuilt from scratch on every start, so a changed LAN, a hand-edited entry or +# a host list that grew between versions all self-heal instead of accumulating. mqtt_block() { local lan local port - have_iptables || return 0 + local rc=0 + + probe_iptables + case "$IPTABLES_STATE" in + absent) + return 0 + ;; + error) + echo "Warning: iptables is present but unusable, MQTT port rules skipped." >&2 + return 1 + ;; + esac + wait_for_lan + + ipt -N "$CHAIN" 2>/dev/null + ipt -F "$CHAIN" || rc=1 + for port in $MQTT_PORTS; do - # 127.0.0.1 is not in the LAN CIDR, and a blackholed host resolves to - # 0.0.0.0, which the kernel routes to loopback. Without this the drop below - # silently swallows that traffic instead of letting it fail fast with a - # RST. Every rule is scoped to an MQTT port so that removing it can never - # take out a broader rule another tool owns. - accept_rule OUTPUT -o lo -p tcp --dport "$port" -j ACCEPT + # A blackholed host resolves to 0.0.0.0, which the kernel routes to + # loopback. Without this the drop below swallows that silently instead of + # letting it fail fast with a RST. + ipt -A "$CHAIN" -o lo -p tcp --dport "$port" -j ACCEPT || rc=1 for lan in $(lan_networks); do - accept_rule OUTPUT -p tcp -d "$lan" --dport "$port" -j ACCEPT + ipt -A "$CHAIN" -p tcp -d "$lan" --dport "$port" -j ACCEPT || rc=1 done # vectorp carries hardcoded broker IPs (47.114.48.45:1883 and # 120.55.101.240:1883) that bypass DNS entirely, so the port rules are what # actually stops it. - drop_rule OUTPUT -p tcp --dport "$port" -j DROP + ipt -A "$CHAIN" -p tcp --dport "$port" -j DROP || rc=1 done + + # Inserted, not appended: an OUTPUT chain that already carries a broad ACCEPT + # would otherwise match first and the drops would never be reached. + chain_is_linked || ipt -I OUTPUT -j "$CHAIN" || rc=1 + + [ "$rc" -eq 0 ] || echo "Warning: some MQTT port rules could not be applied." >&2 + return "$rc" } mqtt_unblock() { - local lan - local port - have_iptables || return 0 - for port in $MQTT_PORTS; do - delete_rule OUTPUT -p tcp --dport "$port" -j DROP - for lan in $(lan_networks); do - delete_rule OUTPUT -p tcp -d "$lan" --dport "$port" -j ACCEPT - done - delete_rule OUTPUT -o lo -p tcp --dport "$port" -j ACCEPT + probe_iptables + [ "$IPTABLES_STATE" = "ok" ] || return 0 + + while chain_is_linked; do + ipt -D OUTPUT -j "$CHAIN" 2>/dev/null || break done + ipt -F "$CHAIN" 2>/dev/null + ipt -X "$CHAIN" 2>/dev/null + return 0 } +rc=0 + case "$1" in start) echo "Blocking Creality cloud telemetry..." - hosts_block - mqtt_block + hosts_write block || { echo "Error: could not update $HOSTS." >&2; rc=1; } + mqtt_block || rc=1 ;; stop) echo "Unblocking Creality cloud telemetry..." mqtt_unblock - hosts_unblock + hosts_write unblock || { echo "Error: could not restore $HOSTS." >&2; rc=1; } ;; restart|force-reload) "$0" stop "$0" start + rc=$? ;; status) - blocked=0 - total=0 - for host in $BLACKHOLE; do - total=$((total + 1)) - if grep -q "$(host_pattern "$host")" "$HOSTS" 2>/dev/null; then - blocked=$((blocked + 1)) - fi - done - echo "Hosts blocklist: $blocked of $total entries present" - if have_iptables; then - iptables -L OUTPUT -v -n + if grep -q "^${BEGIN_MARKER}\$" "$HOSTS" 2>/dev/null; then + echo "Hosts blocklist: active ($(sed -n "/^${BEGIN_MARKER}\$/,/^${END_MARKER}\$/p" "$HOSTS" | grep -c '^0\.0\.0\.0') entries)" else - echo "MQTT port rules: skipped (iptables not available)" + echo "Hosts blocklist: inactive" fi + probe_iptables + case "$IPTABLES_STATE" in + absent) + echo "MQTT port rules: skipped (no iptables on this model)" + ;; + error) + echo "MQTT port rules: iptables is present but unusable" + rc=1 + ;; + ok) + if chain_is_linked; then + ipt -L "$CHAIN" -v -n + else + echo "MQTT port rules: chain $CHAIN is not linked into OUTPUT" + fi + ;; + esac ;; *) echo "Usage: $0 {start|stop|restart|force-reload|status}" exit 1 + ;; esac -exit 0 +exit "$rc" diff --git a/scripts/block_creality_cloud.sh b/scripts/block_creality_cloud.sh old mode 100644 new mode 100755 index 03f6f54..e62a449 --- a/scripts/block_creality_cloud.sh +++ b/scripts/block_creality_cloud.sh @@ -29,12 +29,20 @@ function install_block_creality_cloud(){ cp "$CLOUD_BLOCK_SERVICE_URL" "$CLOUD_BLOCK_SERVICE_FILE" chmod 755 "$CLOUD_BLOCK_SERVICE_FILE" echo -e "Info: Applying blocklist..." + local start_rc set +e "$CLOUD_BLOCK_SERVICE_FILE" start + start_rc=$? set -e + if [ "$start_rc" -ne 0 ]; then + error_msg "Block Creality Cloud Telemetry was installed but did not apply cleanly!" + echo -e " ${darkred}Run '$CLOUD_BLOCK_SERVICE_FILE status' to see what is missing.${white}" + echo + return + fi ok_msg "Block Creality Cloud Telemetry has been installed successfully!" if ! command -v iptables > /dev/null 2>&1; then - echo -e " ${darkred}Note: iptables is not available on this printer, MQTT port rules were skipped.${white}" + echo -e " ${darkred}Note: this printer has no iptables, so the MQTT port rules were skipped.${white}" echo -e " ${darkred}The hosts blocklist is still active.${white}" echo fi