feat(parental): добавить поддержку фильтрации как по IP, так и по MAC-адресам

This commit is contained in:
Dushnilin
2026-08-17 21:52:28 +03:00
parent 7a9c87622e
commit e695da1fb7
5 changed files with 80 additions and 33 deletions
@@ -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()
@@ -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 '<span style="opacity:0.5;">' + _("All devices") + "</span>";
}
const badges = ips
.map(
(ip) =>
'<span class="badge" style="background:#2a3441;color:#63b3ed;padding:3px 8px;border-radius:6px;font-size:11px;font-family:monospace;margin-right:4px;font-weight:600;display:inline-block;margin-bottom:2px;">' +
ip +
"</span>",
)
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 (
`<span class="badge" style="background:${bg};color:${color};padding:3px 8px;border-radius:6px;font-size:11px;font-family:monospace;margin-right:4px;font-weight:600;display:inline-block;margin-bottom:2px;">` +
icon +
dev +
"</span>"
);
})
.join("");
return "<div>" + badges + "</div>";
};
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,
+3 -2
View File
@@ -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");
+8 -5
View File
@@ -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 ]);
+12
View File
@@ -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"