feat(parental): добавить поддержку фильтрации как по IP, так и по MAC-адресам
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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 ]);
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user