diff --git a/parental-control/Makefile b/parental-control/Makefile index 9fb394347..93a13a605 100644 --- a/parental-control/Makefile +++ b/parental-control/Makefile @@ -5,13 +5,13 @@ include $(TOPDIR)/rules.mk PKG_NAME:=parental-control -PKG_VERSION:=1.0.4 +PKG_VERSION:=1.1.0 LOCAL_DEV:=0 ifneq ($(LOCAL_DEV),1) PKG_SOURCE_PROTO:=git PKG_SOURCE_URL:=https://dev.iopsys.eu/network/parental-control.git -PKG_SOURCE_VERSION:=eea7793e26b52f45f4e47e849894ac3f8cdc3747 +PKG_SOURCE_VERSION:=6a70cb9e0a9d92952610589915b85ae91841cb7b PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION)-$(PKG_SOURCE_VERSION).tar.gz PKG_MIRROR_HASH:=skip endif @@ -59,8 +59,10 @@ endef endif define Package/parental-control/install + $(INSTALL_DIR) $(1)/etc/parentalcontrol $(INSTALL_DIR) $(1)/lib/parentalcontrol $(INSTALL_DATA) ./files/lib/parentalcontrol/parentalcontrol.sh $(1)/lib/parentalcontrol/ + $(INSTALL_BIN) ./files/lib/parentalcontrol/sync_bundles.sh $(1)/lib/parentalcontrol/ $(INSTALL_DIR) $(1)/etc $(INSTALL_DATA) ./files/etc/firewall.parentalcontrol $(1)/etc/ @@ -78,11 +80,13 @@ define Package/parental-control/install $(INSTALL_DATA) ./files/etc/uci-defaults/95-firewall_parentalcontrol.ucidefaults $(1)/etc/uci-defaults/ $(INSTALL_DATA) ./files/etc/uci-defaults/95-migrate_urlfilter.ucidefaults $(1)/etc/uci-defaults/ + $(INSTALL_DIR) $(1)/lib/upgrade/keep.d + $(INSTALL_DATA) ./files/lib/upgrade/keep.d/parentalcontrol $(1)/lib/upgrade/keep.d/parentalcontrol + $(BBFDM_REGISTER_SERVICES) -v ${VENDOR_PREFIX} ./bbfdm_service.json $(1) parentalcontrol ifeq ($(CONFIG_PARENTAL_CONTROL_INCLUDE_URLFILTER_BUNDLES),y) - $(INSTALL_DIR) $(1)/etc/parental-control - $(INSTALL_DATA) ./files/etc/parental-control/urlbundles.tar.xz $(1)/etc/parental-control/ + $(INSTALL_DATA) ./files/etc/parentalcontrol/urlbundles.tar.xz $(1)/etc/parentalcontrol/ endif endef diff --git a/parental-control/files/etc/config/parentalcontrol b/parental-control/files/etc/config/parentalcontrol index fd30de201..250ca7533 100644 --- a/parental-control/files/etc/config/parentalcontrol +++ b/parental-control/files/etc/config/parentalcontrol @@ -1,3 +1,93 @@ config globals 'globals' - option enable '0' - option loglevel '3' + option enable '0' + option loglevel '3' + +config urlbundle 'urlbundle_1' + option enable '0' + option name 'Abuse' + option download_url 'https://blocklistproject.github.io/Lists/alt-version/abuse-nl.txt' + +config urlbundle 'urlbundle_2' + option enable '0' + option name 'Ads' + option download_url 'https://blocklistproject.github.io/Lists/alt-version/ads-nl.txt' + +config urlbundle 'urlbundle_3' + option enable '0' + option name 'Crypto' + option download_url 'https://blocklistproject.github.io/Lists/alt-version/crypto-nl.txt' + +config urlbundle 'urlbundle_4' + option enable '1' + option name 'Drugs' + option download_url 'https://blocklistproject.github.io/Lists/alt-version/drugs-nl.txt' + +config urlbundle 'urlbundle_5' + option enable '0' + option name 'Everything else' + option download_url 'https://blocklistproject.github.io/Lists/alt-version/everything-nl.txt' + +config urlbundle 'urlbundle_6' + option enable '1' + option name 'Facebook/Instagram' + option download_url 'https://blocklistproject.github.io/Lists/alt-version/facebook-nl.txt' + +config urlbundle 'urlbundle_7' + option enable '1' + option name 'Fraud' + option download_url 'https://blocklistproject.github.io/Lists/alt-version/fraud-nl.txt' + +config urlbundle 'urlbundle_8' + option enable '1' + option name 'Gambling' + option download_url 'https://blocklistproject.github.io/Lists/alt-version/gambling-nl.txt' + +config urlbundle 'urlbundle_9' + option enable '0' + option name 'Malware' + option download_url 'https://blocklistproject.github.io/Lists/alt-version/malware-nl.txt' + +config urlbundle 'urlbundle_10' + option enable '1' + option name 'Phishing' + option download_url 'https://blocklistproject.github.io/Lists/alt-version/phishing-nl.txt' + +config urlbundle 'urlbundle_11' + option enable '1' + option name 'Piracy' + option download_url 'https://blocklistproject.github.io/Lists/alt-version/piracy-nl.txt' + +config urlbundle 'urlbundle_12' + option enable '0' + option name 'Porn' + option download_url 'https://blocklistproject.github.io/Lists/alt-version/porn-nl.txt' + +config urlbundle 'urlbundle_13' + option enable '1' + option name 'Ransomware' + option download_url 'https://blocklistproject.github.io/Lists/alt-version/ransomware-nl.txt' + +config urlbundle 'urlbundle_14' + option enable '0' + option name 'Redirect' + option download_url 'https://blocklistproject.github.io/Lists/alt-version/redirect-nl.txt' + +config urlbundle 'urlbundle_15' + option enable '1' + option name 'Scam' + option download_url 'https://blocklistproject.github.io/Lists/alt-version/scam-nl.txt' + +config urlbundle 'urlbundle_16' + option enable '0' + option name 'TikTok' + option download_url 'https://blocklistproject.github.io/Lists/alt-version/tiktok-nl.txt' + +config urlbundle 'urlbundle_17' + option enable '0' + option name 'Torrent' + option download_url 'https://blocklistproject.github.io/Lists/alt-version/torrent-nl.txt' + +config urlbundle 'urlbundle_18' + option enable '0' + option name 'Tracking' + option download_url 'https://blocklistproject.github.io/Lists/alt-version/tracking-nl.txt' diff --git a/parental-control/files/etc/init.d/parentalcontrol b/parental-control/files/etc/init.d/parentalcontrol index 9d0713083..17bba8b7c 100755 --- a/parental-control/files/etc/init.d/parentalcontrol +++ b/parental-control/files/etc/init.d/parentalcontrol @@ -11,7 +11,8 @@ PROG=/usr/sbin/urlfilter validate_global_section() { uci_validate_section parentalcontrol globals globals \ 'enable:bool:1' \ - 'loglevel:uinteger:3' + 'loglevel:uinteger:3' \ + 'bundle_path:string' } remove_fw_rules() { @@ -47,19 +48,48 @@ configure_fw_rules() { add_internet_schedule_rules } +copy_dhcp_leases() { + src="/tmp/dhcp.leases" + dest="/etc/parentalcontrol/dhcp.leases" + dest_dir="/etc/parentalcontrol/" + + # Ensure the destination directory exists + mkdir -p "$dest_dir" || { logger -p err "Failed to create directory $dest_dir."; return 1; } + + # Check if the source file exists and is not empty + if [ -s "$src" ]; then + # Compare the content of the source and destination + if ! cmp -s "$src" "$dest"; then + # Use atomic copy to prevent partial writes + tmp_dest="${dest}.tmp" + cp "$src" "$tmp_dest" && mv "$tmp_dest" "$dest" + fi + fi +} + start_service() { - local enable loglevel + local enable loglevel bundle_path config_load parentalcontrol validate_global_section + [ -n "${bundle_path}" ] && mkdir -p ${bundle_path} + # add default bundles process_default_bundles # add firewall rules configure_fw_rules - procd_open_instance parentalcontrol_dm - procd_set_param command ${PROG} + # if the router is, for example, upgraded and then it boots up + # then /tmp/dhcp.leases will be empty until clients try to get a lease, + # in that case, hostnames will not be processed by the daemon, + # for this we copy /tmp/dhcp.leases to /etc/parentalcontrol/dhcp.leases + # which will be persistent acrros reboots and upgrade where settings are kept + # and will be used as a backup in case /tmp/dhcp.leases is empty + copy_dhcp_leases + + procd_open_instance "parentalcontrol_dm" + procd_set_param command nice -n 10 "${PROG}" # Lower priority procd_append_param command -l ${loglevel} procd_set_param respawn procd_close_instance @@ -69,6 +99,7 @@ stop_service() { # remove default bundles remove_default_bundles remove_fw_rules + copy_dhcp_leases } reload_service() { @@ -78,6 +109,7 @@ reload_service() { start else configure_fw_rules + copy_dhcp_leases ubus send parentalcontrol.reload fi } diff --git a/parental-control/files/etc/parental-control/urlbundles.tar.xz b/parental-control/files/etc/parentalcontrol/urlbundles.tar.xz similarity index 100% rename from parental-control/files/etc/parental-control/urlbundles.tar.xz rename to parental-control/files/etc/parentalcontrol/urlbundles.tar.xz diff --git a/parental-control/files/lib/parentalcontrol/parentalcontrol.sh b/parental-control/files/lib/parentalcontrol/parentalcontrol.sh index 927397f16..6db1eb3fc 100644 --- a/parental-control/files/lib/parentalcontrol/parentalcontrol.sh +++ b/parental-control/files/lib/parentalcontrol/parentalcontrol.sh @@ -13,8 +13,8 @@ IP_RULE="" ACL_FILE="" parentalcontrol_ipv4_forward="" parentalcontrol_ipv6_forward="" -default_bundle_dir="/tmp/urlfilter/default/" -bundle_archive="/etc/parental-control/urlbundles.tar.xz" +default_bundle_dir="/tmp/parentalcontrol/default/" +bundle_archive="/etc/parentalcontrol/urlbundles.tar.xz" log() { echo "$*" |logger -t urlfilter.init -p debug diff --git a/parental-control/files/lib/parentalcontrol/sync_bundles.sh b/parental-control/files/lib/parentalcontrol/sync_bundles.sh new file mode 100644 index 000000000..6e7fc24b8 --- /dev/null +++ b/parental-control/files/lib/parentalcontrol/sync_bundles.sh @@ -0,0 +1,282 @@ +#!/bin/sh + +. /lib/functions.sh + +LOCKFILE="/tmp/sync_bundles.lock" + +# this script handles syncing bundles +# if its a remote file, then it would be downloaded and placed in bundle_dir +bundle_path="$(uci -q get parentalcontrol.globals.bundle_path)" +if [ -z "${bundle_path}" ]; then + bundle_path="/tmp/parentalcontrol" +fi + +stringstore_dir="${bundle_path}/stringstore" +bundle_dir="${bundle_path}/urlbundles" +bundle_sizes="${bundle_path}/bundle_sizes" + +# Ensure required directories and files exist +initialize_environment() { + mkdir -p "$bundle_dir" + mkdir -p "$stringstore_dir" + [ ! -f "$bundle_sizes" ] && touch "$bundle_sizes" +} + +# Function to sanitize URLs to avoid code injection and ensure safety +sanitize_url() { + local raw_url="$1" + echo "$raw_url" | sed 's/[^a-zA-Z0-9_.:/?-]//g' +} + +update_bundle_file_from_url() { + local download_url="$1" + local bundle_file_name="$2" + local bundle_file_size="$3" + local bundle_name="$4" + local file_name="$5" + local available_memory + + available_memory=$(df "$bundle_dir" | tail -n 1 | awk '{print $(NF-2)}') # Available memory in 1K blocks + local needed_blocks=$((bundle_file_size / 1024)) # Convert bundle_file_size to 1K blocks + local max_size=$((10 * 1024 * 1024)) # 10MB in bytes + + if [ "$available_memory" -le "$needed_blocks" ]; then + logger -p info "Error: Not enough disk space for bundle: ${bundle_name}" + return 1 + fi + + if [ "$bundle_file_size" -gt "$max_size" ]; then + logger -p info "update_bundle_file_from_url: Error: File size for ${bundle_name} exceeds 10MB" + return 1 + fi + + # Determine file path + local file_path + if echo "$download_url" | grep -q "^file://"; then + file_path=${download_url#file://} + else + # Random delay (0-5s) before starting the download + local delay=$((RANDOM % 6)) + logger -p info "update_bundle_file_from_url: Waiting ${delay}s before downloading..." + sleep "$delay" + + # Retry logic with exponential backoff + local temp_file="${bundle_dir}/tmp_${file_name}" + local attempt=1 + local success=0 + while [ $attempt -le 3 ]; do + wget -q -O "$temp_file" "$download_url" + if [ $? -eq 0 ]; then + success=1 + break + else + logger -p info "update_bundle_file_from_url: Download failed. Retrying $attempt ..." + local backoff=$(( (2 ** attempt) + (RANDOM % 3) )) # Exponential backoff + 0-2s jitter + sleep "$backoff" + fi + attempt=$(( attempt+1 )) + done + + if [ $success -ne 1 ]; then + logger -p info "update_bundle_file_from_url: Failed to download bundle: ${bundle_name}" + rm -f "$temp_file" + return 1 + fi + file_path="$temp_file" + fi + + # Handle compressed files + local final_path="${bundle_dir}/${bundle_file_name}" + if [[ "$file_path" =~ \.xz$ ]]; then + if ! xz -dc "$file_path" > "$final_path"; then + logger -p info "update_bundle_file_from_url: Decompression failed." + rm -f "$final_path" + rm -f "$file_path" + return 1 + fi + + rm -f "$file_path" + elif [[ "$file_path" =~ \.gz$ ]]; then + if ! gzip -dc "$file_path" > "$final_path"; then + logger -p info "update_bundle_file_from_url: Decompression failed." + rm -f "$final_path" + rm -f "$file_path" + return 1 + fi + + rm -f "$file_path" + else + mv "$file_path" "$final_path" + fi + + # file would have lines of the format: 0.0.0.0 www.facebook.com + # so we keep only the url part and remove duplicates + local processed_final_path="${final_path}_urls" + awk '{print $NF}' "$final_path" | sort -u > "$processed_final_path" + + # delete unprocessed file + rm -rf "$final_path" + + # Update the bundle size and send ubus event + echo "$bundle_file_name $bundle_file_size" >> "$bundle_sizes" + ubus send "parentalcontrol.bundle.update" "{\"bundle_file_path\":\"${processed_final_path}\",\"bundle_name\":\"${bundle_name}\"}" + + return 0 +} + +handle_download_url() { + local raw_download_url="$1" + local bundle_name="$2" + + local sanitized_url + sanitized_url=$(sanitize_url "$raw_download_url") + + local file_name="${sanitized_url##*/}" # Get everything after the last '/' + + local bundle_file_name="${file_name}.urlbundle" + local unprocessed_file=0 + local file_path="${sanitized_url#file://}" + + if echo "$sanitized_url" | grep -qE "^https?://|^file://"; then + local previous_bundle_size + previous_bundle_size=$(grep "^${bundle_file_name} " "$bundle_sizes" | awk '{print $2}') + + # If the URL is HTTP, fetch the file size + local bundle_file_size + if echo "$sanitized_url" | grep -qE "^https?://"; then + bundle_file_size="$(curl -I "$sanitized_url" 2>&1 | grep -i 'content-length' | cut -d: -f2 | xargs)" + [ -z "$bundle_file_size" ] && bundle_file_size=0 + else + # If it's a file:// URL, get the file size from the filesystem + bundle_file_size=$(ls -l "$file_path" 2>/dev/null | awk '{print $5}') + [ -z "$bundle_file_size" ] && bundle_file_size=0 + fi + + if [ -n "$previous_bundle_size" ] && [ "$bundle_file_size" -eq "$previous_bundle_size" ]; then + return + fi + + if echo "$sanitized_url" | grep -q "^file://" && ! echo "$sanitized_url" | grep -Eq "\.(xz|gz)$"; then + # the file is not processed and hence not moved if it is a local uncompressed file + sed -i "/^${bundle_file_name} /d" "$bundle_sizes" + echo "$bundle_file_name $bundle_file_size" >> "$bundle_sizes" + ubus send "parentalcontrol.bundle.update" "{\"bundle_file_path\":\"${file_path}\",\"bundle_name\":\"${bundle_name}\"}" + return + fi + + # Remove existing entries + if [ -n "$previous_bundle_size" ]; then + sed -i "/^${bundle_file_name} /d" "$bundle_sizes" + rm -f "${bundle_dir}/${bundle_file_name}" + fi + + update_bundle_file_from_url "$sanitized_url" "$bundle_file_name" "$bundle_file_size" "$bundle_name" "$file_name" + else + logger -p info "Error: Unsupported URL format for ${bundle_file_name}" + fi +} + +cleanup_bundle_files() { + local dir="$1" + [ -d "$dir" ] || return 1 + + # Collect all download_url entries using config_foreach + local urls="" + get_download_url() { + local section="$1" + config_get url "$section" download_url + config_get_bool enable "$1" enable 0 + + if [ "${enable}" -eq 0 ]; then + # bundle is disabled + return 0 + fi + url="${url#file://}" + url="${url#https://}" + url="${url#http://}" + + url="${url##*/}" # Get everything after the last '/' + urls="$urls $url" + } + + config_load parentalcontrol + config_foreach get_download_url urlbundle + + # Loop through all files in the directory + for file in "$dir"/*; do + [ -f "$file" ] || continue # Skip non-files + + # Remove the suffix after the last dot + base_name="$(basename "$file")" + name="${base_name%.*}" # Removes the last dot and suffix + + # Check if the name is present in the collected urls + if ! echo "$urls" | grep -q "$name"; then + rm -f "$file" + sed -i "/^${name} /d" "$bundle_sizes" + fi + done +} + +# Main handler for all profile URL bundles +handle_filter_for_bundles() { + ubus -t 20 wait_for bbfdm.parentalcontrol + + if [ "$?" -ne 0 ]; then + logger -p error "bbfdm.parentalcontrol object not found" + return + fi + + initialize_environment + + cleanup_bundle_files "$bundle_dir" + cleanup_bundle_files "$stringstore_dir" + + config_load parentalcontrol + + config_get_bool enable globals enable 0 + if [ "${enable}" -eq 0 ]; then + # Parental control is disabled + return 0 + fi + + local profile enable bundles bundle_name download_url + + check_bundle_exists() { + config_get name "$1" name + config_get_bool enable "$1" enable 0 + config_get download_url "$1" download_url + + if [ "${enable}" -eq 0 ]; then + # bundle is disabled + return 0 + fi + + if [ "$name" = "$2" ]; then + handle_download_url "$download_url" "$name" + fi + } + + handle_bundle_from_profile() { + local bundle_name="$1" + + config_foreach check_bundle_exists urlbundle "$bundle_name" + } + + handle_profile() { + config_get_bool enable "$1" enable 0 + [ "$enable" -ne 1 ] && return + + config_list_foreach "$1" profile_urlbundle handle_bundle_from_profile + } + + config_foreach handle_profile profile_urlfilter +} + + +# Open file descriptor 200 for locking +exec 200>"$LOCKFILE" +# Try to acquire an exclusive lock; exit if another instance is running +flock -n 200 || { logger -p info "sync_bundles.sh is already running, exiting."; exit 1; } + +handle_filter_for_bundles diff --git a/parental-control/files/lib/upgrade/keep.d/parentalcontrol b/parental-control/files/lib/upgrade/keep.d/parentalcontrol new file mode 100644 index 000000000..770ada5a1 --- /dev/null +++ b/parental-control/files/lib/upgrade/keep.d/parentalcontrol @@ -0,0 +1 @@ +/etc/parentalcontrol/dhcp.leases