iopsys-feed/parental-control/files/lib/parentalcontrol/parentalcontrol.sh

650 lines
19 KiB
Bash

#!/bin/sh
. /lib/functions.sh
day=""
next_days=""
prev_days=""
schedule_added=""
ACCESS_RULE=""
IP_RULE=""
ACL_FILE=""
parentalcontrol_ipv4_forward=""
parentalcontrol_ipv6_forward=""
bundle_path="$(uci -q get parentalcontrol.globals.bundle_path)"
default_bundle_dir="${bundle_path}/default/"
bundle_archive="/etc/parentalcontrol/urlbundles.tar.xz"
log() {
echo "$*" |logger -t urlfilter.init -p debug
}
process_default_bundles() {
if [ -s "$bundle_archive" ]; then
if mkdir -p "$default_bundle_dir"; then
if tar -xJf "$bundle_archive" -C "$default_bundle_dir"; then
log "default bundles placed at $default_bundle_dir"
else
log "default bundles could not be placed at $default_bundle_dir"
fi
else
log "could not create directory: $default_bundle_dir"
fi
else
log "default bundles not available"
fi
}
remove_default_bundles() {
rm -rf "$default_bundle_dir"
}
# Function to calculate UTC time and relative day
get_relative_day() {
local hour="$1"
local offset="$2"
local relative_day="$3"
local utc_hour
# we need to force hours and minutes to be treated as base 10 (decimal)
# otherwise shell will treat, for example, 09 as octal
# hour=$((10#$hour)) does not work on busybox
# so we use another trick
hour=$(expr $hour + 0)
# Extract the sign and the hour part of the offset
local sign=${offset:0:1}
local offset_hour=${offset:1:2}
# Adjust hour based on the offset
if [ "$sign" = "-" ]; then
utc_hour=$((hour + offset_hour))
else
utc_hour=$((hour - offset_hour))
fi
# Handle overflow/underflow of UTC hours to keep within 0-23 range
if [ $utc_hour -lt 0 ]; then
if [ "$relative_day" = "today" ]; then
relative_day="yesterday"
else
relative_day="today"
fi
elif [ $utc_hour -ge 24 ]; then
if [ "$relative_day" = "today" ]; then
relative_day="tomorrow"
else
relative_day="tomorrow"
fi
else
if [ "$relative_day" = "tomorrow" ]; then
relative_day="tomorrow"
else
relative_day="today"
fi
fi
echo "$relative_day"
}
get_next_day() {
local weekday="$1"
case "$weekday" in
"Mon"|"Monday") echo "Tuesday"
;;
"Tue"|"Tuesday") echo "Wednesday"
;;
"Wed"|"Wednesday") echo "Thursday"
;;
"Thu"|"Thursday") echo "Friday"
;;
"Fri"|"Friday") echo "Saturday"
;;
"Sat"|"Saturday") echo "Sunday"
;;
"Sun"|"Sunday") echo "Monday"
;;
esac
}
get_previous_day() {
local weekday="$1"
case "$weekday" in
"Mon"|"Monday") echo "Sunday"
;;
"Tue"|"Tuesday") echo "Monday"
;;
"Wed"|"Wednesday") echo "Tuesday"
;;
"Thu"|"Thursday") echo "Wednesday"
;;
"Fri"|"Friday") echo "Thursday"
;;
"Sat"|"Saturday") echo "Friday"
;;
"Sun"|"Sunday") echo "Saturday"
;;
esac
}
add_access_rule() {
local rule_prefix="$1"
local start_time="$2"
local stop_time="$3"
local weekdays="$4"
local target="$5"
local rule
local start_hm stop_hm
if [ -z "$target" ]; then
return
fi
if [ -n "$weekdays" ]; then
start_hm=$(echo "$start_time" | awk -F: '{ print $1,$2 }' | sed 's/ //')
stop_hm=$(echo "$stop_time" | awk -F: '{ print $1,$2 }' | sed 's/ //')
if [ "$start_hm" = "$stop_hm" ]; then
return
fi
rule_prefix="$rule_prefix -m time --timestart $start_time --timestop $stop_time --weekdays $weekdays"
fi
rule="$rule_prefix -j $target"
echo "iptables -w -A parentalcontrol_forward ${rule}" >> "$ACL_FILE"
echo "ip6tables -w -A parentalcontrol_forward ${rule}" >> "$ACL_FILE"
}
generate_ip_rule() {
local utc_start_relative_day="$1"
local utc_end_relative_day="$2"
local utc_start_time="$3"
local utc_stop_time="$4"
local target="$5"
# Handle the cases based on the relation between utc_start_relative_day and utc_end_relative_day
if [ "$utc_start_relative_day" = "yesterday" ] && [ "$utc_end_relative_day" = "yesterday" ]; then
# Rule for yesterday only
add_access_rule "$IP_RULE" "$utc_start_time" "$utc_stop_time" "$prev_days" "$target"
elif [ "$utc_start_relative_day" = "yesterday" ] && [ "$utc_end_relative_day" = "today" ]; then
# Rule for yesterday to today
add_access_rule "$IP_RULE" "$utc_start_time" "23:59:59" "$prev_days" "$target"
add_access_rule "$IP_RULE" "00:00" "$utc_stop_time" "$day" "$target"
elif [ "$utc_start_relative_day" = "today" ] && [ "$utc_end_relative_day" = "today" ]; then
# Rule for today only
add_access_rule "$IP_RULE" "$utc_start_time" "$utc_stop_time" "$day" "$target"
elif [ "$utc_start_relative_day" = "today" ] && [ "$utc_end_relative_day" = "tomorrow" ]; then
# Rule for today to tomorrow
add_access_rule "$IP_RULE" "$utc_start_time" "23:59:59" "$day" "$target"
add_access_rule "$IP_RULE" "00:00" "$utc_stop_time" "$next_days" "$target"
elif [ "$utc_start_relative_day" = "tomorrow" ] && [ "$utc_end_relative_day" = "tomorrow" ]; then
# Rule for tomorrow only
add_access_rule "$IP_RULE" "$utc_start_time" "$utc_stop_time" "$next_days" "$target"
else
log "Error: Unhandled case"
fi
}
handle_day_list() {
local value=$1
val=$(echo $value | cut -c 1-3)
next_day_val=$(get_next_day $val)
prev_day_val=$(get_previous_day $val)
if [ -z $day ]; then
day="$val"
next_days="$next_day_val"
prev_days="$prev_day_val"
else
day="$day,$val"
next_days="$next_days,$next_day_val"
prev_days="$prev_days,$prev_day_val"
fi
}
handle_schedule() {
local schedule_section="$1"
local type="$2"
local schedule_ref="$3"
local local_start_time local_stop_time duration zone_offset local_start_hh local_stop_hh
local is_enabled
local target
local day_config
local relative_day_end="today"
IP_RULE="$ACCESS_RULE"
day=""
next_days=""
prev_days=""
local all_days="Monday Tuesday Wednesday Thursday Friday Saturday Sunday"
zone_offset=$(date +%z)
config_get_bool is_enabled "$schedule_section" "enable" 0
if [ $is_enabled -eq 0 ]; then
return
fi
if [ "$type" = "profile_bedtime_schedule" ]; then
target="DROP"
config_get local_start_time "$schedule_section" "start_time" "00:00:00"
config_get local_stop_time "$schedule_section" "end_time" "23:59:59"
local_start_hh=$(echo $local_start_time | awk -F: '{ print $1 }')
local_stop_hh=$(echo $local_stop_time | awk -F: '{ print $1 }')
config_get day_config "$schedule_section" "day" "$all_days"
else
if [ "$schedule_ref" != "$schedule_section" ]; then
return
fi
# for access rules to be effective for a schedule, need to add DROP rule
# to block the access outside the defined schedule
# therefore, set flag
if [ "$schedule_added" = "0" ]; then
schedule_added="1"
fi
# internet_access has been updated to be internet_break
# so drop traffic during the schedule, and allow outside the schedule
target="DROP"
config_get local_start_time "$schedule_section" "start_time" "00:00"
config_get duration "$schedule_section" "duration"
local hh=$(echo $local_start_time | awk -F: '{ print $1 }')
local mm=$(echo $local_start_time | awk -F: '{ print $2 }')
local hh_s=`expr $hh \* 3600`
local mm_s=`expr $mm \* 60`
local ss=$(( hh_s + mm_s ))
local_start_hh=$hh
if [ -n "$duration" ]; then
local stop_ss rem_ss mm
stop_ss=$(( ss + duration ))
hh=$(( stop_ss / 3600 ))
rem_ss=$(( stop_ss % 3600 ))
mm=$(( rem_ss / 60 ))
ss=$(( rem_ss % 60 ))
local_stop_time="$hh:$mm:$ss"
local_stop_hh="$hh"
else
# if duration is not specified, then apply rule to end of the day
local_stop_time="23:59:59"
local_stop_hh="23"
fi
config_get day_config "$schedule_section" "day" "$all_days"
fi
IFS=" "
for d in $day_config; do
handle_day_list $d
done
utc_start_time=$(date -u -d @$(date "+%s" -d "$local_start_time") +%H:%M)
utc_start_time="$utc_start_time"
utc_stop_time=$(date -u -d @$(date "+%s" -d "$local_stop_time") +%H:%M)
utc_stop_time="$utc_stop_time"
# Determine whether the local end hour crosses midnight
if [ "$local_start_hh" -gt "$local_stop_hh" ]; then
relative_day_end="tomorrow"
fi
local utc_start_relative_day=$(get_relative_day "$local_start_hh" "$zone_offset" "today")
local utc_end_relative_day=$(get_relative_day "$local_stop_hh" "$zone_offset" "$relative_day_end")
generate_ip_rule "$utc_start_relative_day" "$utc_end_relative_day" "$utc_start_time" "$utc_stop_time" "$target"
}
handle_bedtime() {
local mac_addresses="$1"
local mac
# if mac addresses are present, then we apply the rule for each mac address
# otherwise apply the rule to everybody
for mac in $mac_addresses; do
ACCESS_RULE="-m mac --mac-source $mac"
config_foreach handle_schedule profile_bedtime_schedule "profile_bedtime_schedule" ""
done
}
handle_internet_break() {
local mac_addresses="$1"
local mac
local schedule_ref
config_get schedule_ref "$profile_section" "internet_break_schedule"
for mac in $mac_addresses; do
ACCESS_RULE="-m mac --mac-source $mac"
schedule_added="0"
# check if schedule is defined for this profile/internet_break instance
# and if yes, create rule accordingly
if [ -n "$schedule_ref" ]; then
config_load "schedules"
config_foreach handle_schedule schedule "schedule" "$schedule_ref"
fi
done
}
parse_macs() {
local maclist="$1"
local m mac
for m in $maclist; do
# trim whitespace
mac="$(echo "$m" | tr -d ' \t\r\n')"
# validate format
if echo "$mac" | grep -qE '^[0-9A-Fa-f]{2}(:[0-9A-Fa-f]{2}){5}$'; then
echo "$mac"
else
log "parse_macs(): Invalid MAC in mac list: '$mac'"
fi
done
}
handle_profile() {
local profile_section="$1"
local internet_break_enable bedtime_enable hostlist maclist
config_get_bool internet_break_enable "$profile_section" "internet_break_enable" 0
config_get_bool bedtime_enable "$profile_section" "bedtime_enable" 0
if [ $internet_break_enable -eq 0 ] && [ $bedtime_enable -eq 0 ]; then
return
fi
config_get hostlist "$profile_section" "host"
config_get maclist "$profile_section" "mac"
# If both lists are empty, nothing to do
if [ -z "$hostlist" ] && [ -z "$maclist" ]; then
return
fi
ACCESS_RULE=""
# both uci options contain mac addresses
# one is given directly by the user
# other is resolved by the data model from Hosts.Host object
local mac_addresses="$(parse_macs "${hostlist} ${maclist}" | awk '{ if (NF && !seen[$0]++) { print $0 } }' | tr '\n' ' ')"
# default value of Hosts.AccessControl.{i}.Enable is false,
# so, if not defined in uci as 1, assume 0
if [ $internet_break_enable -gt 0 ]; then
handle_internet_break "${mac_addresses}"
# handle_internet_break may have loaded schedules uci
# so, reload parentalcontrol
config_load "parentalcontrol"
fi
if [ $bedtime_enable -gt 0 ]; then
handle_bedtime "${mac_addresses}"
fi
}
add_internet_schedule_rules() {
ACL_FILE="/tmp/parentalcontrol_access_control/access_control.rules"
rm -f $ACL_FILE
mkdir -p /tmp/parentalcontrol_access_control/
touch $ACL_FILE
echo "iptables -w -F parentalcontrol_forward" >> $ACL_FILE
echo "ip6tables -w -F parentalcontrol_forward" >> $ACL_FILE
parentalcontrol_ipv4_forward=$(iptables -w -t filter --list -n | grep parentalcontrol_forward)
if [ -z "$parentalcontrol_ipv4_forward" ]; then
echo "iptables -w -t filter -N parentalcontrol_forward" >> $ACL_FILE
ret=$?
[ $ret -eq 0 ] && echo "iptables -w -t filter -I FORWARD -j parentalcontrol_forward" >> $ACL_FILE
fi
parentalcontrol_ipv6_forward=$(ip6tables -w -t filter --list -n | grep parentalcontrol_forward)
if [ -z "$parentalcontrol_ipv6_forward" ]; then
echo "ip6tables -w -t filter -N parentalcontrol_forward" >> $ACL_FILE
ret=$?
[ $ret -eq 0 ] && echo "ip6tables -w -t filter -I FORWARD -j parentalcontrol_forward" >> $ACL_FILE
fi
# Load /etc/config/parentalcontrol UCI file
config_load "parentalcontrol"
config_foreach handle_profile "profile"
# apply the rules
sh $ACL_FILE
}
add_iptables_nfqueue_rules() {
local queue_num="$1"
# Check if urlfilter used
if ! uci show parentalcontrol | grep -q profile_urlfilter; then
return
fi
# IPv4
# FORWARD
if ! iptables -w -nL | grep -q "URLFILTER_FORWARD"; then
iptables -w -N URLFILTER_FORWARD
iptables -w -I FORWARD 1 -j URLFILTER_FORWARD
# capture DNS responses (sport 53)
iptables -w -A URLFILTER_FORWARD -p tcp --sport 53 -j NFQUEUE --queue-num $queue_num --queue-bypass
iptables -w -A URLFILTER_FORWARD -p udp --sport 53 -j NFQUEUE --queue-num $queue_num --queue-bypass
# HTTP/HTTPS flows
iptables -w -A URLFILTER_FORWARD -p tcp --match multiport --ports 80,443 -j NFQUEUE --queue-num $queue_num --queue-bypass
iptables -w -A URLFILTER_FORWARD -p udp --match multiport --ports 80,443 -j NFQUEUE --queue-num $queue_num --queue-bypass
fi
# INPUT
if ! iptables -w -nL | grep -q "URLFILTER_INPUT"; then
iptables -w -N URLFILTER_INPUT
iptables -w -I INPUT 1 -j URLFILTER_INPUT
iptables -w -A URLFILTER_INPUT -p tcp --sport 53 ! -i lo -j NFQUEUE --queue-num $queue_num --queue-bypass
iptables -w -A URLFILTER_INPUT -p udp --sport 53 ! -i lo -j NFQUEUE --queue-num $queue_num --queue-bypass
fi
# OUTPUT
if ! iptables -w -nL | grep -q "URLFILTER_OUTPUT"; then
iptables -w -N URLFILTER_OUTPUT
iptables -w -I OUTPUT 1 -j URLFILTER_OUTPUT
iptables -w -A URLFILTER_OUTPUT -p tcp --sport 53 ! -o lo -j NFQUEUE --queue-num $queue_num --queue-bypass
iptables -w -A URLFILTER_OUTPUT -p udp --sport 53 ! -o lo -j NFQUEUE --queue-num $queue_num --queue-bypass
fi
# ebtables bypass for IPv4
ebtables --concurrent -A FORWARD -p ip --ip-protocol 6 --ip-destination-port 443 -j SKIPLOG 2>/dev/null
ebtables --concurrent -A FORWARD -p ip --ip-protocol 6 --ip-source-port 53 -j SKIPLOG 2>/dev/null
ebtables --concurrent -A FORWARD -p ip --ip-protocol 17 --ip-source-port 53 -j SKIPLOG 2>/dev/null
# IPv6
# FORWARD
if ! ip6tables -w -nL | grep -q "URLFILTER_FORWARD6"; then
ip6tables -w -N URLFILTER_FORWARD6
ip6tables -w -I FORWARD 1 -j URLFILTER_FORWARD6
ip6tables -w -A URLFILTER_FORWARD6 -p tcp --sport 53 -j NFQUEUE --queue-num $queue_num --queue-bypass
ip6tables -w -A URLFILTER_FORWARD6 -p udp --sport 53 -j NFQUEUE --queue-num $queue_num --queue-bypass
ip6tables -w -A URLFILTER_FORWARD6 -p tcp --match multiport --ports 80,443 -j NFQUEUE --queue-num $queue_num --queue-bypass
ip6tables -w -A URLFILTER_FORWARD6 -p udp --match multiport --ports 80,443 -j NFQUEUE --queue-num $queue_num --queue-bypass
fi
# INPUT
if ! ip6tables -w -nL | grep -q "URLFILTER_INPUT6"; then
ip6tables -w -N URLFILTER_INPUT6
ip6tables -w -I INPUT 1 -j URLFILTER_INPUT6
ip6tables -w -A URLFILTER_INPUT6 -p tcp --sport 53 ! -i lo -j NFQUEUE --queue-num $queue_num --queue-bypass
ip6tables -w -A URLFILTER_INPUT6 -p udp --sport 53 ! -i lo -j NFQUEUE --queue-num $queue_num --queue-bypass
fi
# OUTPUT
if ! ip6tables -w -nL | grep -q "URLFILTER_OUTPUT6"; then
ip6tables -w -N URLFILTER_OUTPUT6
ip6tables -w -I OUTPUT 1 -j URLFILTER_OUTPUT6
ip6tables -w -A URLFILTER_OUTPUT6 -p tcp --sport 53 ! -o lo -j NFQUEUE --queue-num $queue_num --queue-bypass
ip6tables -w -A URLFILTER_OUTPUT6 -p udp --sport 53 ! -o lo -j NFQUEUE --queue-num $queue_num --queue-bypass
fi
# ebtables bypass for IPv6
ebtables --concurrent -A FORWARD -p ip6 --ip6-protocol 6 --ip6-destination-port 443 -j SKIPLOG 2>/dev/null
ebtables --concurrent -A FORWARD -p ip6 --ip6-protocol 6 --ip6-source-port 53 -j SKIPLOG 2>/dev/null
ebtables --concurrent -A FORWARD -p ip6 --ip6-protocol 17 --ip6-source-port 53 -j SKIPLOG 2>/dev/null
}
remove_iptables_nfqueue_rules() {
# IPv4
for chain in URLFILTER_FORWARD URLFILTER_INPUT URLFILTER_OUTPUT; do
if iptables -w -nL | grep -q "$chain"; then
iptables -w -D FORWARD -j $chain 2>/dev/null
iptables -w -D INPUT -j $chain 2>/dev/null
iptables -w -D OUTPUT -j $chain 2>/dev/null
iptables -w -F $chain
iptables -w -X $chain
fi
done
ebtables --concurrent -D FORWARD -p ip --ip-protocol 6 --ip-destination-port 443 -j SKIPLOG 2>/dev/null
ebtables --concurrent -D FORWARD -p ip --ip-protocol 6 --ip-source-port 53 -j SKIPLOG 2>/dev/null
ebtables --concurrent -D FORWARD -p ip --ip-protocol 17 --ip-source-port 53 -j SKIPLOG 2>/dev/null
# IPv6
for chain in URLFILTER_FORWARD6 URLFILTER_INPUT6 URLFILTER_OUTPUT6; do
if ip6tables -w -nL | grep -q "$chain"; then
ip6tables -w -D FORWARD -j $chain 2>/dev/null
ip6tables -w -D INPUT -j $chain 2>/dev/null
ip6tables -w -D OUTPUT -j $chain 2>/dev/null
ip6tables -w -F $chain
ip6tables -w -X $chain
fi
done
ebtables --concurrent -D FORWARD -p ip6 --ip6-protocol 6 --ip6-destination-port 443 -j SKIPLOG 2>/dev/null
ebtables --concurrent -D FORWARD -p ip6 --ip6-protocol 6 --ip6-source-port 53 -j SKIPLOG 2>/dev/null
ebtables --concurrent -D FORWARD -p ip6 --ip6-protocol 17 --ip6-source-port 53 -j SKIPLOG 2>/dev/null
}
remove_internet_schedule_rules() {
# remove from iptables, if chain exists
if iptables -w -nL FORWARD|grep -iqE "parentalcontrol_forward"; then
iptables -w -t filter -D FORWARD -j parentalcontrol_forward
iptables -w -F parentalcontrol_forward
iptables -w -X parentalcontrol_forward
fi
# remove from ip6tables, if chain exists
if ip6tables -w -nL FORWARD|grep -iqE "parentalcontrol_forward"; then
ip6tables -w -t filter -D FORWARD -j parentalcontrol_forward
ip6tables -w -F parentalcontrol_forward
ip6tables -w -X parentalcontrol_forward
fi
}
get_host_ip_from_mac() {
local mac="$1"
local ip=""
# Validate MAC format
if ! echo "$mac" | grep -qE '^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$'; then
log "get_host_ip_from_mac(): Invalid MAC address format '$mac'"
return 1
fi
# Try to find IP from ARP table
ip="$(cat /proc/net/arp | awk -v mac="$mac" 'tolower($4) == tolower(mac) {print $1; exit}')"
if [ -n "$ip" ]; then
URLFILTER_IPS="${URLFILTER_IPS} ${ip}"
return 0
else
log "get_host_ip_from_mac(): No IP found for MAC $mac in ARP table"
return 1
fi
}
# Global array for resolved IPs
URLFILTER_IPS=""
resolve_profile_hosts() {
local section="$1"
local hostlist maclist h m
config_get hostlist "$section" host
config_get maclist "$section" mac
for h in $hostlist; do
get_host_ip_from_mac "$h"
done
for m in $maclist; do
get_host_ip_from_mac "$m"
done
}
# Main function to collect IPs and delete conntrack entries
flush_conntrack_for_hosts() {
URLFILTER_IPS=""
local count max
config_foreach resolve_profile_hosts profile
URLFILTER_IPS="$(echo "$URLFILTER_IPS" | tr ' ' '\n' | sort -u | xargs)"
for ip in $URLFILTER_IPS; do
count=0
max=1000
while conntrack -D -s "$ip" >/dev/null 2>&1; do
count=$((count+1))
if [ $count -ge $max ]; then
log "Warning: Forced to stop conntrack delete after $max deletions for $ip (possible loop)"
break
fi
done
done
}
OVERRIDE_JSON="/etc/parentalcontrol/urlbundle_override.json"
DM_PLUGIN_PATH="/usr/share/bbfdm/micro_services/parentalcontrol/urlbundle_override.json"
enable_urlfilter_dm() {
if [ -f "${DM_PLUGIN_PATH}" ]; then
rm ${DM_PLUGIN_PATH}
echo "Please restart to apply"
fi
}
disable_urlfilter_dm() {
mkdir -p "$(dirname ${DM_PLUGIN_PATH})"
if [ ! -f "${DM_PLUGIN_PATH}" ]; then
if [ -f "${OVERRIDE_JSON}" ]; then
cp "${OVERRIDE_JSON}" "${DM_PLUGIN_PATH}"
echo "Please restart to apply"
fi
fi
}