urlfilter: transform into ParentalControl bbf extension

* config file has been renamed from urlfilter to parentalcontrol
* along with urlfiltering, shell scripts handle internet_access
  and bedtime blocking support, which have been added as
  separate sections for better mapping to the bbf data model
  extension
* uci-default script has been added to handle migration from
  previous to new format
This commit is contained in:
Mohd Husaam Mehdi 2024-09-17 15:53:05 +05:30
parent cc0d1b5910
commit 6f7f14b241
10 changed files with 623 additions and 443 deletions

View file

@ -5,13 +5,13 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=urlfilter
PKG_VERSION:=2.0.1
PKG_VERSION:=2.0.2
LOCAL_DEV:=0
ifneq ($(LOCAL_DEV),1)
PKG_SOURCE_PROTO:=git
PKG_SOURCE_URL:=https://dev.iopsys.eu/network/urlfilter.git
PKG_SOURCE_VERSION:=08044747036259db23d6581fcbaa51750516749e
PKG_SOURCE_VERSION:=e56831c48472c5fa2595f12f02baa4437b6970e4
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION)-$(PKG_SOURCE_VERSION).tar.gz
PKG_MIRROR_HASH:=skip
endif
@ -26,7 +26,7 @@ define Package/urlfilter
SECTION:=utils
CATEGORY:=Utilities
TITLE:=URL filter
DEPENDS:=+libuci +libnetfilter-queue +libnfnetlink +iptables-mod-nfqueue +libpthread +libubox +ubus +conntrack
DEPENDS:=+libuci +libcurl +libnetfilter-queue +libnfnetlink +iptables-mod-nfqueue +libpthread +libubox +ubus +conntrack
endef
define Package/urlfilter/description
@ -38,11 +38,17 @@ TARGET_CFLAGS += \
ifeq ($(LOCAL_DEV),1)
define Build/Prepare
$(CP) -rf ./urlfilter/* $(PKG_BUILD_DIR)/
$(CP) -rf ./urlfilter/* $(PKG_BUILD_DIR)/
endef
endif
define Package/urlfilter/install
$(INSTALL_DIR) $(1)/lib/parentalcontrol
$(INSTALL_DATA) ./files/lib/parentalcontrol/parentalcontrol.sh $(1)/lib/parentalcontrol/
$(INSTALL_DIR) $(1)/etc
$(INSTALL_DATA) ./files/etc/firewall.parentalcontrol $(1)/etc/
$(INSTALL_DIR) $(1)/usr/sbin
$(INSTALL_BIN) $(PKG_BUILD_DIR)/urlfilter $(1)/usr/sbin
@ -50,15 +56,11 @@ define Package/urlfilter/install
$(INSTALL_BIN) ./files/etc/init.d/urlfilter $(1)/etc/init.d/
$(INSTALL_DIR) $(1)/etc/config
$(INSTALL_DATA) ./files/etc/config/urlfilter $(1)/etc/config/
$(INSTALL_DIR) $(1)/etc/
$(INSTALL_DATA) ./files/etc/firewall.urlfilter $(1)/etc/
$(INSTALL_DATA) ./files/etc/config/parentalcontrol $(1)/etc/config/
$(INSTALL_DIR) $(1)/etc/uci-defaults
$(INSTALL_DATA) ./files/etc/uci-defaults/95-urlfilter_add_firewall_rule $(1)/etc/uci-defaults
$(BBFDM_INSTALL_MS_DM) ./files/etc/urlfilter/urlfilter.json $(1) $(PKG_NAME)
$(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/
endef
$(eval $(call BuildPackage,urlfilter))

View file

@ -0,0 +1,41 @@
#!/bin/sh
. /lib/functions.sh
. /lib/parentalcontrol/parentalcontrol.sh
add_iptables_nfqueue_rules() {
iptables -w -nL FORWARD|grep -iqE "NFQUEUE"
if [ "$?" -ne 0 ]; then
# setup netfilter queue 0, use queue bypass so that if no application is
# listening to this queue then traffic is unaffected.
iptables -w -I FORWARD 1 -p tcp --match multiport --ports 80,443,53 -j NFQUEUE --queue-num 0 --queue-bypass
iptables -w -I FORWARD 1 -p udp --match multiport --ports 80,443,53 -j NFQUEUE --queue-num 0 --queue-bypass
iptables -w -I INPUT 1 -p tcp --match multiport --ports 80,443,53 -j NFQUEUE --queue-num 0 --queue-bypass
iptables -w -I INPUT 1 -p udp --match multiport --ports 80,443,53 -j NFQUEUE --queue-num 0 --queue-bypass
# disable acceleration for https packet so that they can be read by urlfilter
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
fi
ip6tables -w -nL FORWARD|grep -iqE "NFQUEUE"
if [ "$?" -ne 0 ]; then
#ip6table rules
ip6tables -w -I FORWARD 1 -p tcp --match multiport --ports 80,443,53 -j NFQUEUE --queue-num 0 --queue-bypass
ip6tables -w -I FORWARD 1 -p udp --match multiport --ports 80,443,53 -j NFQUEUE --queue-num 0 --queue-bypass
ip6tables -w -I INPUT 1 -p tcp --match multiport --ports 80,443,53 -j NFQUEUE --queue-num 0 --queue-bypass
ip6tables -w -I INPUT 1 -p udp --match multiport --ports 80,443,53 -j NFQUEUE --queue-num 0 --queue-bypass
# disable acceleration for https packet so that they can be read by urlfilter
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
fi
}
add_iptables_nfqueue_rules
handle_internet_schedule

View file

@ -1,31 +0,0 @@
iptables -w -nL FORWARD|grep -iqE "NFQUEUE"
if [ "$?" -ne 0 ]; then
# setup netfilter queue 0, use queue bypass so that if no application is
# listening to this queue then traffic is unaffected.
iptables -w -I FORWARD 1 -p tcp --match multiport --ports 80,443,53 -j NFQUEUE --queue-num 0 --queue-bypass
iptables -w -I FORWARD 1 -p udp --match multiport --ports 80,443,53 -j NFQUEUE --queue-num 0 --queue-bypass
iptables -w -I INPUT 1 -p tcp --match multiport --ports 80,443,53 -j NFQUEUE --queue-num 0 --queue-bypass
iptables -w -I INPUT 1 -p udp --match multiport --ports 80,443,53 -j NFQUEUE --queue-num 0 --queue-bypass
# disable acceleration for https packet so that they can be read by urlfilter
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
fi
ip6tables -w -nL FORWARD|grep -iqE "NFQUEUE"
if [ "$?" -ne 0 ]; then
#ip6table rules
ip6tables -w -I FORWARD 1 -p tcp --match multiport --ports 80,443,53 -j NFQUEUE --queue-num 0 --queue-bypass
ip6tables -w -I FORWARD 1 -p udp --match multiport --ports 80,443,53 -j NFQUEUE --queue-num 0 --queue-bypass
ip6tables -w -I INPUT 1 -p tcp --match multiport --ports 80,443,53 -j NFQUEUE --queue-num 0 --queue-bypass
ip6tables -w -I INPUT 1 -p udp --match multiport --ports 80,443,53 -j NFQUEUE --queue-num 0 --queue-bypass
# disable acceleration for https packet so that they can be read by urlfilter
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
fi

View file

@ -4,12 +4,11 @@ START=95
STOP=10
USE_PROCD=1
NAME=urlfilter
PROG=/usr/sbin/urlfilter
start_service() {
if [ "$(uci -q get urlfilter.globals.enable)" == "1" ]; then
if [ "$(uci -q get parentalcontrol.globals.enable)" == "1" ]; then
procd_open_instance urlfilter
procd_set_param command ${PROG}
procd_set_param respawn
@ -21,6 +20,10 @@ start_service() {
sleep 5
conntrack -F
fi
. /lib/parentalcontrol/parentalcontrol.sh
handle_internet_schedule
fi
}
@ -30,5 +33,6 @@ reload_service() {
}
service_triggers() {
procd_add_reload_trigger "urlfilter"
procd_add_reload_trigger "parentalcontrol"
procd_add_reload_trigger "schedules"
}

View file

@ -0,0 +1,11 @@
#!/bin/sh
if [ -f /etc/firewall.parentalcontrol ]; then
uci -q get firewall.parentalcontrol >/dev/null || {
uci -q set firewall.parentalcontrol=include
uci -q set firewall.parentalcontrol.path="/etc/firewall.parentalcontrol"
uci -q set firewall.parentalcontrol.reload=1
}
fi
exit 0

View file

@ -0,0 +1,132 @@
#!/bin/sh
. /lib/functions.sh
# Convert URL filter to parental control format
urlfilter_config="/etc/config/urlfilter"
parentalcontrol_config="/etc/config/parentalcontrol"
schedules_config="/etc/config/schedules"
# this script only needs to work if urlfilter_config was found
if [ ! -s "$urlfilter_config" ]; then
exit 0
fi
# reset parentalcontrol_config
# but schedules might have data other than schedules
# so append to it
rm -f "$parentalcontrol_config"
touch "$parentalcontrol_config"
schedules_enable="$(uci -q get schedules.global.enable)"
# if no schedules config, then add it
if [ ! -s "$schedules_config" ]; then
touch "$schedules_config"
schedules_enable=1
fi
# Parse globals
uci -q batch <<EOF
set parentalcontrol.globals=globals
set parentalcontrol.globals.enable="$(uci -q get urlfilter.globals.enable)"
set schedules.global=global
set schedules.global.enable="$schedules_enable"
EOF
# Function to handle filter sections
handle_filter() {
local section="$1"
local url_list="$2"
local profile_name="$3"
local access="$4"
local profile_name enable start_time duration days filter_profile macaddr_list
config_get filter_profile "$section" profile
# if option profile value and profile name match, then
if [ "$filter_profile" = "$profile_name" ]; then
config_get enable "$section" enable
config_get start_time "$section" start_time
config_get duration "$section" duration
config_get macaddr_list "$section" macaddr
config_get days "$section" day
# Add hosts based on MAC addresses in the filter
if [ -n "$macaddr_list" ]; then
for macaddr in $macaddr_list; do
uci -q add_list parentalcontrol.${profile_name}.host="$macaddr"
done
fi
uci -q set parentalcontrol.f_$filter_profile=profile_urlfilter
uci -q set parentalcontrol.f_$filter_profile.enable="$enable"
uci -q set parentalcontrol.f_$filter_profile.access="$access"
uci -q set parentalcontrol.f_$filter_profile.dm_parent="$profile_name"
# Add URLs one by one as filter_text
for url in $url_list; do
uci -q add_list parentalcontrol.f_$filter_profile.filter_text="$url"
done
# Add schedule if time restrictions exist
if [ -n "$start_time" ] && [ -n "$duration" ] && [ -n "$days" ]; then
local schedule_name
# declare and assign separately to avoid masking return value
schedule_name="$(uci -q add schedules schedule)"
# if adding schedule was successful, then populate it
if [ "$?" -eq 0 ] && [ -n "$schedule_name" ]; then
uci -q set schedules.${schedule_name}=schedule
uci -q set schedules.${schedule_name}.enable="$enable"
uci -q set schedules.${schedule_name}.start="$start_time"
uci -q set schedules.${schedule_name}.duration="$duration"
for day in $days; do
uci -q add_list schedules.${schedule_name}.day="$day"
done
# Link schedule to profile_urlfilter
uci -q set parentalcontrol.f_$filter_profile.profile_urlfilter_schedule="$schedule_name"
fi
fi
fi
}
# Function to handle profile sections
handle_profile() {
local section="$1"
local profile_name whitelist_urls blacklist_urls
config_get profile_name "$section" name
# if name was not set then continue
if [ -z "$profile_name" ]; then
return
fi
config_get whitelist_urls "$section" whitelist_url
config_get blacklist_urls "$section" blacklist_url
# Create the new profile in parentalcontrol
uci -q set parentalcontrol.${profile_name}=profile
uci -q set parentalcontrol.${profile_name}.name="$profile_name"
# Add whitelist/blacklist URLs as filter_text
if [ -n "$whitelist_urls" ]; then
config_foreach handle_filter filter "$whitelist_urls" "$profile_name" 1 # Whitelist access
fi
if [ -n "$blacklist_urls" ]; then
config_foreach handle_filter filter "$blacklist_urls" "$profile_name" 0 # Blacklist access
fi
}
# Load urlfilter UCI config and iterate through profiles and filters
config_load "urlfilter"
config_foreach handle_profile profile
config_foreach handle_filter filter
# Commit changes
uci commit parentalcontrol
uci commit schedules
rm -f "$urlfilter_config"

View file

@ -1,11 +0,0 @@
#!/bin/sh
if uci -q get firewall.urlfilter >/dev/null; then
exit
fi
uci set firewall.urlfilter=include
uci set firewall.urlfilter.reload=1
uci set firewall.urlfilter.path=/etc/firewall.urlfilter
uci commit firewall

View file

@ -1,387 +0,0 @@
{
"json_plugin_version": 2,
"Device.{BBF_VENDOR_PREFIX}URLFilter.": {
"type": "object",
"protocols": [
"cwmp",
"usp"
],
"description": "This object contains the information about URLs to be blocked or allowed to access from specified MAC addresses in given time duration.",
"access": false,
"array": false,
"Enable": {
"type": "boolean",
"read": true,
"write": true,
"protocols": [
"cwmp",
"usp"
],
"description": "Enable or disable URLFiltering on the CPE.",
"mapping": [
{
"type": "uci",
"uci": {
"file": "urlfilter",
"section": {
"name": "globals"
},
"option": {
"name": "enable"
}
}
}
]
},
"GlobalBlacklist": {
"type": "boolean",
"read": true,
"write": true,
"protocols": [
"cwmp",
"usp"
],
"description": "Enable or disable access of the URLs specified in <<param|BlacklistURL>> from all connected devices.",
"mapping": [
{
"type": "uci",
"uci": {
"file": "urlfilter",
"section": {
"name": "globals"
},
"option": {
"name": "global_blacklist"
}
}
}
]
},
"BlacklistURL": {
"type": "string",
"read": true,
"write": true,
"protocols": [
"cwmp",
"usp"
],
"description": "Comma separated list of URLs to be blacklisted from all connected devices.",
"list": {
"datatype": "string"
},
"mapping": [
{
"type": "uci",
"uci": {
"file": "urlfilter",
"section": {
"name": "globals"
},
"list": {
"name": "blacklist_url"
}
}
}
]
},
"ProfileNumberOfEntries": {
"type": "unsignedInt",
"read": true,
"write": false,
"protocols": [
"cwmp",
"usp"
],
"datatype": "unsignedInt",
"description": "<<numentries>>",
"mapping": [
{
"type": "uci",
"uci": {
"file": "urlfilter",
"section": {
"type": "profile"
},
"option": {
"name": "@Count"
}
}
}
]
},
"Device.{BBF_VENDOR_PREFIX}URLFilter.Profile.{i}.": {
"type": "object",
"protocols": [
"cwmp",
"usp"
],
"description": "Table contain details of the blacklist/whitelist profiles.",
"uniqueKeys": [
"Name"
],
"access": true,
"array": true,
"mapping": [
{
"type": "uci",
"uci": {
"file": "urlfilter",
"section": {
"type": "profile"
},
"dmmapfile": "dmmap_urlfilter"
}
}
],
"Alias": {
"type": "string",
"read": true,
"write": false,
"protocols": [
"cwmp",
"usp"
],
"datatype": "string",
"range": [
{
"max": 64
}
],
"flags": [
"Unique",
"Linker"
],
"mapping": [
{
"data": "@Parent",
"type": "uci_sec",
"key": "@Name"
}
]
},
"Name": {
"type": "string",
"read": true,
"write": true,
"protocols": [
"cwmp",
"usp"
],
"description": "Name of the profile. This should be unique for each entry in the table.",
"datatype": "string",
"range": [
{
"max": 64
}
],
"mapping": [
{
"data": "@Parent",
"type": "uci_sec",
"key": "name"
}
]
},
"WhitelistURL": {
"type": "string",
"read": true,
"write": true,
"protocols": [
"cwmp",
"usp"
],
"description": "Comma separated list of URLs which should be allowed to access.",
"list": {
"datatype": "string"
},
"mapping": [
{
"data": "@Parent",
"type": "uci_sec",
"list": "whitelist_url"
}
]
},
"BlacklistURL": {
"type": "string",
"read": true,
"write": true,
"protocols": [
"cwmp",
"usp"
],
"description": "Comma separated list of URLs which should not be allowed to access.",
"list": {
"datatype": "string"
},
"mapping": [
{
"data": "@Parent",
"type": "uci_sec",
"list": "blacklist_url"
}
]
}
},
"FilterNumberOfEntries": {
"type": "unsignedInt",
"read": true,
"write": false,
"protocols": [
"cwmp",
"usp"
],
"datatype": "unsignedInt",
"description": "<<numentries>>",
"mapping": [
{
"type": "uci",
"uci": {
"file": "urlfilter",
"section": {
"type": "filter"
},
"option": {
"name": "@Count"
}
}
}
]
},
"Device.{BBF_VENDOR_PREFIX}URLFilter.Filter.{i}.": {
"type": "object",
"protocols": [
"cwmp",
"usp"
],
"description": "Table contain MAC addresses on which <<object|Profile>> should be applied along with other information like filtering should be applied on which day, the timing information when the filtering should be done etc.",
"access": true,
"array": true,
"mapping": [
{
"type": "uci",
"uci": {
"file": "urlfilter",
"section": {
"type": "filter"
},
"dmmapfile": "dmmap_urlfilter"
}
}
],
"Enable": {
"type": "boolean",
"read": true,
"write": true,
"protocols": [
"cwmp",
"usp"
],
"description": "Enable or disable this filter instance on the CPE.",
"mapping": [
{
"data": "@Parent",
"type": "uci_sec",
"key": "enable"
}
]
},
"Profile": {
"type": "string",
"read": true,
"write": true,
"protocols": [
"cwmp",
"usp"
],
"flags": [
"Reference"
],
"description": "Path of the <<object|Profile>> that should be applied.",
"mapping": [
{
"data": "@Parent",
"type": "uci_sec",
"key": "profile",
"linker_obj": "Device.{BBF_VENDOR_PREFIX}URLFilter.Profile.*.Alias"
}
]
},
"MACAddress": {
"type": "string",
"read": true,
"write": true,
"protocols": [
"cwmp",
"usp"
],
"description": "Comma separated list of MAC addresses for which the filtering should be done.",
"list": {
"datatype": "string"
},
"mapping": [
{
"data": "@Parent",
"type": "uci_sec",
"list": "macaddr"
}
]
},
"Day": {
"type": "string",
"read": true,
"write": true,
"protocols": [
"cwmp",
"usp"
],
"description": "Comma separated list of weekdays. Filtering should be done on the mentioned days only.",
"list": {
"datatype": "string"
},
"mapping": [
{
"data": "@Parent",
"type": "uci_sec",
"list": "day"
}
]
},
"StartTime": {
"type": "string",
"read": true,
"write": true,
"protocols": [
"cwmp",
"usp"
],
"description": "Time when filtering shall start.",
"mapping": [
{
"data": "@Parent",
"type": "uci_sec",
"key": "start_time"
}
]
},
"Duration": {
"type": "string",
"read": true,
"write": true,
"protocols": [
"cwmp",
"usp"
],
"description": "The duration in seconds to filter the URLs from start time.",
"mapping": [
{
"data": "@Parent",
"type": "uci_sec",
"key": "duration"
}
]
}
}
}
}

View file

@ -0,0 +1,419 @@
#!/bin/sh
. /lib/functions.sh
day=""
next_days=""
prev_days=""
schedule_added=""
ACCESS_RULE=""
IP_RULE=""
ACL_FILE=""
parentalcontrol_ipv4_forward=""
parentalcontrol_ipv6_forward=""
# 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
logger -t parental_control "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)
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
config_get_bool is_enabled "$schedule_section" "enable" 0
if [ $is_enabled -eq 0 ]; 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
target="ACCEPT"
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"
}
# Function that parses input for MAC addresses or hostnames
parse_macs_or_hostnames() {
local input="$1"
local lease_file="/tmp/dhcp.leases"
for item in $input; do
case "$item" in
??:??:??:??:??:??)
# It's a MAC address, print it as is
echo "$item"
;;
*)
# Assume it's a hostname and search for its MAC address in the leases file
mac=$(awk -v hostname="$item" '$4 == hostname {print $2}' "$lease_file")
if [ -n "$mac" ]; then
echo "$mac"
fi
;;
esac
done
}
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_access() {
local mac_addresses="$1"
local mac
local access_policy
config_get access_policy "$profile_section" "internet_access_policy"
local schedule_ref
config_get schedule_ref "$profile_section" "internet_access_schedule"
for mac in $mac_addresses; do
ACCESS_RULE="-m mac --mac-source $mac"
# As per Data Model, if access policy is deny, then schedule is to be ignored
# and no access is to be provided for the device
if [ "$access_policy" = "Deny" ]; then
add_access_rule "$ACCESS_RULE" "" "" "" "DROP"
continue # no need to parse schedule
fi
schedule_added="0"
# check if schedule is defined for this profile/internet_access instance
# and if yes, create rule accordingly
if [ -n "$schedule_ref" ]; then
config_load "schedules"
config_foreach handle_schedule schedule "schedule" "$schedule_ref"
fi
# for access rule to work, need to have default drop rule as last rule
if [ "$schedule_added" = "1" ]; then
add_access_rule "$ACCESS_RULE" "" "" "" "DROP"
fi
done
}
handle_profile() {
local profile_section="$1"
local internet_access_enable bedtime_enable hostlist
config_get hostlist "$profile_section" "host"
if [ -z "$hostlist" ]; then
return
fi
ACCESS_RULE=""
# convert hostnames to mac addresses if needed
# and replace newlines with space because it messes up the for loops in
# handle_internet_access and handle_bedtime functions
local mac_addresses="$(parse_macs_or_hostnames "${hostlist}" | tr '\n' ' ')"
# default value of Hosts.AccessControl.{i}.Enable is false,
# so, if not defined in uci as 1, assume 0
config_get_bool internet_access_enable "$profile_section" "internet_access_enable" 0
if [ $internet_access_enable -gt 0 ]; then
handle_internet_access "${mac_addresses}"
# handle_internet_access may have loaded schedules uci
# so, reload parentalcontrol
config_load "parentalcontrol"
fi
config_get_bool bedtime_enable "$profile_section" "bedtime_enable" 0
if [ $bedtime_enable -gt 0 ]; then
handle_bedtime "${mac_addresses}"
fi
}
handle_internet_schedule() {
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 -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 -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
}