From e695da1fb787e2af93ae4f30487eabe6807ad295 Mon Sep 17 00:00:00 2001 From: Dushnilin Date: Mon, 17 Aug 2026 21:52:28 +0300 Subject: [PATCH] =?UTF-8?q?feat(parental):=20=D0=B4=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=B8=D1=82=D1=8C=20=D0=BF=D0=BE=D0=B4=D0=B4=D0=B5=D1=80?= =?UTF-8?q?=D0=B6=D0=BA=D1=83=20=D1=84=D0=B8=D0=BB=D1=8C=D1=82=D1=80=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D0=B8=20=D0=BA=D0=B0=D0=BA=20=D0=BF=D0=BE=20IP,=20?= =?UTF-8?q?=D1=82=D0=B0=D0=BA=20=D0=B8=20=D0=BF=D0=BE=20MAC-=D0=B0=D0=B4?= =?UTF-8?q?=D1=80=D0=B5=D1=81=D0=B0=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/view/tachyon/local_devices.js | 38 +++++++++++----- .../resources/view/tachyon/parental.js | 45 ++++++++++++------- tachyon/files/usr/lib/config/validator.uc | 5 ++- tachyon/files/usr/lib/nft/apply.uc | 13 +++--- tests/parental_control.sh | 12 +++++ 5 files changed, 80 insertions(+), 33 deletions(-) diff --git a/luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/local_devices.js b/luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/local_devices.js index e1e73d35..0fe2b286 100644 --- a/luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/local_devices.js +++ b/luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/local_devices.js @@ -49,18 +49,28 @@ function normalizeLocalDeviceName(name) { function addLocalDeviceChoice(choices, ip, name, mac) { const normalizedIp = `${ip || ""}`.trim(); + const normalizedMac = `${mac || ""}`.trim().toLowerCase(); const normalizedName = normalizeLocalDeviceName(name); - if (!normalizedIp || !main.validateIP(normalizedIp).valid) { - return; + if (normalizedIp && main.validateIP(normalizedIp).valid) { + if (normalizedName) { + choices[normalizedIp] = `${normalizedName} (${normalizedIp})`; + } else if (normalizedMac) { + choices[normalizedIp] = `${normalizedIp} [${normalizedMac}]`; + } else if (!choices[normalizedIp]) { + choices[normalizedIp] = `${normalizedIp}`; + } } - if (normalizedName) { - choices[normalizedIp] = `${normalizedName} (${normalizedIp})`; - } else if (mac) { - choices[normalizedIp] = `${normalizedIp} [${mac.toLowerCase()}]`; - } else if (!choices[normalizedIp]) { - choices[normalizedIp] = `${normalizedIp}`; + if (normalizedMac && /^([0-9a-f]{2}[:-]){5}[0-9a-f]{2}$/.test(normalizedMac)) { + const formattedMac = normalizedMac.replace(/-/g, ":"); + if (normalizedName) { + choices[formattedMac] = `${normalizedName} [MAC: ${formattedMac}]`; + } else if (normalizedIp) { + choices[formattedMac] = `${normalizedIp} [MAC: ${formattedMac}]`; + } else if (!choices[formattedMac]) { + choices[formattedMac] = `${formattedMac}`; + } } } @@ -191,12 +201,18 @@ function sortLocalDeviceChoiceValues(choices) { }); } -function hasSingleIpValue(values) { - return normalizeOptionValues(values).some( - (value) => main.validateIP(value).valid, +function isIpOrMac(value) { + const val = `${value || ""}`.trim(); + return ( + main.validateIP(val).valid || + /^([0-9a-fA-F]{2}[:-]){5}[0-9a-fA-F]{2}$/.test(val) ); } +function hasSingleIpValue(values) { + return normalizeOptionValues(values).some(isIpOrMac); +} + function preloadLocalDeviceChoicesForValues(values) { return hasSingleIpValue(values) ? loadLocalDeviceChoices() diff --git a/luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/parental.js b/luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/parental.js index 07b57520..09437177 100644 --- a/luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/parental.js +++ b/luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/parental.js @@ -112,6 +112,14 @@ function formatDaysDisplay(rawDays) { return days.map((d) => DAY_SHORT_LABELS[d] || d).join(", "); } +function validateDevice(sectionId, value) { + if (!value) return true; + const str = `${value}`.trim(); + if (main.validateIP(str).valid) return true; + if (/^([0-9a-fA-F]{2}[:-]){5}[0-9a-fA-F]{2}$/.test(str)) return true; + return _("Invalid IP or MAC address. Expected e.g. 192.168.1.150 or AA:BB:CC:DD:EE:FF"); +} + function createParentalContent(section) { // Enabled switch let o = section.option(form.Flag, "enabled", _("Enable")); @@ -121,28 +129,34 @@ function createParentalContent(section) { o.width = "5rem"; // Device column in table - o = section.option(form.DummyValue, "_device_display", _("Device / IP")); + o = section.option(form.DummyValue, "_device_display", _("Device / IP / MAC")); o.rawhtml = true; o.modalonly = false; o.cfgvalue = function (sectionId) { const rawIp = uci.get(UCI_PACKAGE, sectionId, "device_ip"); - const ips = normalizeListValues(rawIp); - if (ips.length === 0) { + const devs = normalizeListValues(rawIp); + if (devs.length === 0) { return '' + _("All devices") + ""; } - const badges = ips - .map( - (ip) => - '' + - ip + - "", - ) + const badges = devs + .map((dev) => { + const isMac = /^([0-9a-fA-F]{2}[:-]){5}[0-9a-fA-F]{2}$/.test(dev); + const bg = isMac ? "#2d3748" : "#2a3441"; + const color = isMac ? "#b794f4" : "#63b3ed"; + const icon = isMac ? "🏷 " : "🖥 "; + return ( + `` + + icon + + dev + + "" + ); + }) .join(""); return "
" + badges + "
"; }; o.textvalue = function (sectionId) { - const ips = normalizeListValues(uci.get(UCI_PACKAGE, sectionId, "device_ip")); - return ips.length === 0 ? _("All devices") : ips.join(", "); + const devs = normalizeListValues(uci.get(UCI_PACKAGE, sectionId, "device_ip")); + return devs.length === 0 ? _("All devices") : devs.join(", "); }; // Target column in table @@ -279,12 +293,13 @@ function createParentalContent(section) { o = section.option( form.DynamicList, "device_ip", - _("Target Devices (IP / Hostname)"), - _("Select LAN devices to apply this schedule to, or enter IP addresses manually."), + _("Target Devices (IP / MAC)"), + _("Select LAN devices to apply this schedule to, or enter IP or MAC addresses manually."), ); o.modalonly = true; o.rmempty = false; - o.placeholder = "192.168.1.150"; + o.placeholder = "192.168.1.150 or AA:BB:CC:DD:EE:FF"; + o.validate = validateDevice; o.renderWidget = function (sectionId, optionIndex, cfgvalue) { return local_devices.createLocalDeviceDynamicListWidget( this, diff --git a/tachyon/files/usr/lib/config/validator.uc b/tachyon/files/usr/lib/config/validator.uc index 2a390756..2a758b8d 100644 --- a/tachyon/files/usr/lib/config/validator.uc +++ b/tachyon/files/usr/lib/config/validator.uc @@ -1754,8 +1754,9 @@ function validate_schedule(schedule, sections) { } for (let ip in raw_ips) { let clean_ip = trim(as_string(ip)); - if (clean_ip != "" && !core_ip.is_ip_or_cidr(clean_ip)) - fail_validation("Schedule '" + name + "' has invalid device IP '" + clean_ip + "'. Aborted."); + let is_mac = match(clean_ip, /^([0-9a-fA-F]{2}[:-]){5}[0-9a-fA-F]{2}$/) != null; + if (clean_ip != "" && !core_ip.is_ip_or_cidr(clean_ip) && !is_mac) + fail_validation("Schedule '" + name + "' has invalid device IP or MAC address '" + clean_ip + "'. Aborted."); } let target = option(schedule, "target", "all"); diff --git a/tachyon/files/usr/lib/nft/apply.uc b/tachyon/files/usr/lib/nft/apply.uc index 2bd16056..d6b23299 100644 --- a/tachyon/files/usr/lib/nft/apply.uc +++ b/tachyon/files/usr/lib/nft/apply.uc @@ -952,14 +952,17 @@ function nft_add_schedule_rules_from_schedules(schedules, sections, table) { } for (let raw_ip in raw_ips) { - let ip_str = trim(as_string(raw_ip)); - if (ip_str == "") continue; - let family = core_ip.ip_family(ip_str); + let dev_str = trim(as_string(raw_ip)); + if (dev_str == "") continue; + let is_mac = match(dev_str, /^([0-9a-fA-F]{2}[:-]){5}[0-9a-fA-F]{2}$/) != null; + let family = is_mac ? 0 : core_ip.ip_family(dev_str); let saddr_key = family == 6 ? "ip6" : "ip"; for (let interval in intervals) { let time_args = [ "meta", "hour", sprintf("\"%s\"-\"%s\"", interval[0], interval[1]) ]; - let base_match = [ saddr_key, "saddr", ip_str ]; + let base_match = is_mac ? + [ "ether", "saddr", lc(replace(dev_str, "-", ":")) ] : + [ saddr_key, "saddr", dev_str ]; append_array(base_match, days_args); append_array(base_match, time_args); @@ -988,7 +991,7 @@ function nft_add_schedule_rules_from_schedules(schedules, sections, table) { nft_add_rule(table, "parental_control", sub_rule); nft_add_rule(table, "parental_forward", sub_rule); } - if (sets.subnets6 && family == 6) { + if (sets.subnets6 && (family == 6 || is_mac)) { let sub6_rule = []; append_array(sub6_rule, base_match); append_array(sub6_rule, [ "ip6", "daddr", "@" + as_string(sets.subnets6), "counter", verdict ]); diff --git a/tests/parental_control.sh b/tests/parental_control.sh index 1b25ae71..579fa558 100644 --- a/tests/parental_control.sh +++ b/tests/parental_control.sh @@ -84,6 +84,15 @@ cat >"$WORK_DIR/schedule_fixture.json" <<'JSON' "start_time": "21:00", "end_time": "07:00", "days": [ "sat", "sun" ] + }, + { + ".name": "mac_rule", + "enabled": "1", + "device_ip": "11:22:33:44:55:66", + "target": "all", + "action": "block", + "start_time": "10:00", + "end_time": "12:00" } ] } @@ -97,6 +106,9 @@ assert_contains "$NFT_LOG" "parental_forward ip saddr 192.168.1.150 meta day { F assert_contains "$NFT_LOG" "parental_forward ip saddr 192.168.1.150 meta day { Friday, Monday, Thursday, Tuesday, Wednesday } meta hour \"00:00:00\"-\"08:00:00\" counter drop" "midnight wrap part 2" assert_contains "$NFT_LOG" "parental_control ip saddr 192.168.1.150 meta day { Friday, Monday, Thursday, Tuesday, Wednesday } meta hour \"22:00:00\"-\"23:59:59\" counter drop" "parental_control drop" +# Verify MAC address rule in NFT_LOG +assert_contains "$NFT_LOG" "parental_forward ether saddr 11:22:33:44:55:66 meta hour \"10:00:00\"-\"12:00:00\" counter drop" "mac ether saddr drop" + # Verify per-section blocking assert_contains "$NFT_LOG" "ip saddr 192.168.1.160 meta day { Saturday, Sunday } meta hour \"21:00:00\"-\"23:59:59\" ip daddr @tachyon_rule_sec_youtube_subnets counter drop" "section youtube subnet drop" assert_contains "$NFT_LOG" "ip saddr 192.168.1.160 meta day { Saturday, Sunday } meta hour \"21:00:00\"-\"23:59:59\" ip daddr @tachyon_rule_sec_discord_subnets counter drop" "section discord subnet drop"