💋 Sync 2026-09-07 03:05:06
Merge-upstream / merge (push) Canceled after 0s

This commit is contained in:
github-actions[bot]
2026-09-07 03:05:06 +08:00
parent 95ded25b90
commit 6348b68a14
22 changed files with 428 additions and 1918 deletions
+6 -29
View File
@@ -1,39 +1,16 @@
# Copyright (C) 2020 Lienol <lawlienol@gmail.com>
#
# Copyright (C) 2006-2017 OpenWrt.org
# Copyright (C) 2022-2026 sirpdboy <herboy2008@gmail.com>
# This is free software, licensed under the GNU General Public License v2.
# See /LICENSE for more information.
# This is free software, licensed under the GNU General Public License v3.
#
include $(TOPDIR)/rules.mk
THEME_NAME:=timecontrol
PKG_NAME:=luci-app-$(THEME_NAME)
PKG_LICENSE:=Apache-2.0
LUCI_TITLE:=LuCI support for timecontrol for nftables
LUCI_DESCRIPTION:=LuCI support for Easy timecontrol for nftables(Internet time control).
LUCI_DEPENDS:=+bc +nftables +bash +conntrack
LUCI_TITLE:=LuCI support for Time Control
LUCI_DEPENDS:=+luci-base @(PACKAGE_firewall||PACKAGE_firewall4)
LUCI_PKGARCH:=all
PKG_VERSION:=3.2.4
PKG_RELEASE:=4
PKG_MAINTAINER:=sirpdboy <herboy2008@gmail.com>
define Build/Compile
endef
define Package/$(PKG_NAME)/postinst
#!/bin/sh
rm -f /tmp/luci-*
endef
define Package/$(PKG_NAME)/conffiles
/etc/config/timecontrol
endef
PKG_VERSION:=1.1
PKG_RELEASE:=5
include $(TOPDIR)/feeds/luci/luci.mk
# call BuildPackage - OpenWrt buildroot signature
@@ -1,335 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
/*
* Copyright (C) 2022-2026 sirpdboy <herboy2008@gmail.com>
*/
'use strict';
'require view';
'require fs';
'require ui';
'require uci';
'require form';
'require poll';
'require rpc';
'require network';
function checkTimeControlProcess() {
return fs.exec('/bin/ps', ['w']).then(function(res) {
if (res.code !== 0) {
return { running: false, pid: null };
}
var lines = res.stdout.split('\n');
var running = false;
var pid = null;
for (var i = 0; i < lines.length; i++) {
var line = lines[i];
if (line.includes('timecontrolctrl')) {
running = true;
// 提取PID
var match = line.match(/^\s*(\d+)/);
if (match) {
pid = match[1];
}
break;
}
}
return { running: running, pid: null };
}).catch(function() {
return { running: false, pid: null };
});
}
function renderServiceStatus(isRunning, pid) {
var statusText = isRunning ? _('RUNNING') : _('NOT RUNNING');
var color = isRunning ? 'green' : 'red';
var icon = isRunning ? '✓' : '✗';
var statusHtml = String.format(
'<em><span style="color:%s">%s <strong>%s %s</strong></span></em>',
color, icon, _('TimeControl Service'), statusText
);
if (isRunning && pid) {
statusHtml += ' <small>(PID: ' + pid + ')</small>';
}
return statusHtml;
}
function getHostList() {
return L.resolveDefault(network.getHostHints(), [])
.then(function(hosts) {
var hostList = [];
if (hosts && hosts.length > 0) {
hosts.forEach(function(host) {
if (host.ipv4 && host.mac) {
hostList.push({
ipv4: host.ipv4,
mac: host.mac,
name: host.name || '',
ipv6: host.ipv6 || ''
});
}
});
}
return hostList;
})
.catch(function() {
return [];
});
}
var cbiRichListValue = form.ListValue.extend({
renderWidget: function(section_id, option_index, cfgvalue) {
var choices = this.transformChoices();
var widget = new ui.Dropdown((cfgvalue != null) ? cfgvalue : this.default, choices, {
id: this.cbid(section_id),
sort: this.keylist,
optional: true,
select_placeholder: this.select_placeholder || this.placeholder,
custom_placeholder: this.custom_placeholder || this.placeholder,
validate: L.bind(this.validate, this, section_id),
disabled: (this.readonly != null) ? this.readonly : this.map.readonly
});
return widget.render();
},
value: function(value, title, description) {
if (description) {
form.ListValue.prototype.value.call(this, value, E([], [
E('span', { 'class': 'hide-open' }, [title]),
E('div', { 'class': 'hide-close', 'style': 'min-width:25vw' }, [
E('strong', [title]),
E('br'),
E('span', { 'style': 'white-space:normal' }, description)
])
]));
} else {
form.ListValue.prototype.value.call(this, value, title);
}
}
});
return view.extend({
load: function() {
return Promise.all([
uci.load('timecontrol'),
network.getHostHints()
]);
},
render: function(data) {
var m, s, o;
let hosts = data[1]?.hosts;
m = new form.Map('timecontrol', _('Internet Time Control'),
_('Users can limit their internet usage time through MAC and IP, with available IP ranges such as 192.168.110.00 to 192.168.10.200') + '<br/>' +
_('黑名单模式时间控制方式:') + '<br/>' +
_('1. 时间段控制: 指定的机器在设定时间段内可以上网,其他时间不能上网') + '<br/>' +
_('2. 允许上机时长: 指定的机器上线后可以上网指定时长,超过时长后不能上网') + '<br/>' +
_('3. 组合控制: 在时间段内+时长限制(在允许的时间段内限制上网时长)') + '<br/>' +
_('Suggested feedback:') + ' <a href="https://github.com/sirpdboy/luci-app-timecontrol.git" target="_blank">GitHub @timecontrol</a>');
s = m.section(form.TypedSection);
s.anonymous = true;
s.render = function() {
var statusView = E('p', { id: 'service_status' },
'<span class="spinning"> </span> ' + _('Checking service status...'));
checkTimeControlProcess()
.then(function(res) {
var status = renderServiceStatus(res.running, res.pid);
statusView.innerHTML = status;
})
.catch(function(err) {
statusView.innerHTML = '<span style="color:orange">⚠ ' +
_('Status check failed') + '</span>';
console.error('Status check error:', err);
});
poll.add(function() {
return checkTimeControlProcess()
.then(function(res) {
var status = renderServiceStatus(res.running, res.pid);
statusView.innerHTML = status;
})
.catch(function(err) {
statusView.innerHTML = '<span style="color:orange">⚠ ' +
_('Status check failed') + '</span>';
console.error('Status check error:', err);
});
}, 5);
poll.start();
return E('div', { class: 'cbi-section', id: 'status_bar' }, [
statusView,
E('div', { 'style': 'text-align: right; font-style: italic;' }, [
E('span', {}, [
_('© github '),
E('a', {
'href': 'https://github.com/sirpdboy',
'target': '_blank',
'style': 'text-decoration: none;'
}, 'by sirpdboy')
])
])
]);
};
s = m.section(form.TypedSection, 'timecontrol');
s.anonymous = true;
s.addremove = false;
o = s.option(cbiRichListValue, 'list_type', _('Control Mode'),
_('blacklist: Block the networking of the target address, whitelist: Only allow networking for the target address and block all other addresses.'));
o.rmempty = false;
o.value('blacklist', _('Blacklist'));
// o.value('whitelist', _('Whitelist'));
o.default = 'blacklist';
o = s.option(cbiRichListValue, 'chain', _('Control Intensity'),
_('Pay attention to strong control: machines under control will not be able to connect to the software router backend!'));
o.value('forward', _('Ordinary forward control'));
o.value('input', _('Strong input control'));
o.default = 'forward';
o.rmempty = false;
var s = m.section(form.TableSection, 'device', _('Device Rules'));
s.addremove = true;
s.anonymous = true;
s.sortable = false;
o = s.option(form.Value, 'comment', _('Comment'));
o.optional = true;
o.placeholder = _('Description');
o = s.option(form.Flag, 'enable', _('Enabled'));
o.rmempty = false;
o.default = '1';
o = s.option(form.Value, 'mac', _('IP/MAC Address'));
o.rmempty = false;
if (hosts) {
var hostOptions = {};
Object.keys(hosts).forEach(function(mac) {
var host = hosts[mac];
var name = host.name || _(' ');
var ips = L.toArray(host.ipaddrs || host.ipv4 || []);
if (ips.length > 0) {
ips.forEach(function(ip) {
var macDisplay = 'MAC: %s (%s - %s)'.format(mac,ip, name);
hostOptions['mac:' + mac] = macDisplay;
var ipDisplay = 'IP: %s (%s - %s)'.format(ip, mac, name);
hostOptions['ip:' + ip] = ipDisplay;
});
}
});
var sortedKeys = Object.keys(hostOptions).sort(function(a, b) {
return hostOptions[a].localeCompare(hostOptions[b]);
});
sortedKeys.forEach(function(key) {
if (key.startsWith('ip:')) {
o.value(key.substring(3), hostOptions[key]);
}
});
sortedKeys.forEach(function(key) {
if (key.startsWith('mac:')) {
o.value(key.substring(4), hostOptions[key]);
}
});
}
// 时间控制方式选择
o = s.option(cbiRichListValue, 'time_mode', _('Time Control Mode'));
o.value('period', _('Time Period Control (allow in period)'));
o.value('duration', _('Allow Duration Control (allow limited time)'));
o.value('combined', _('Combined Control (allow in period + limit duration)'));
o.default = 'period';
o.rmempty = false;
o.onchange = function(ev, mode) {
var row = this.map.findElement('id', this.cbid(this.section_id));
if (row) {
// 显示/隐藏相关字段
var startTime = row.querySelector('[data-field="timestart"]');
var endTime = row.querySelector('[data-field="timeend"]');
var duration = row.querySelector('[data-field="duration"]');
var useDuration = row.querySelector('[data-field="use_duration"]');
var resetCycle = row.querySelector('[data-field="reset_cycle"]');
if (startTime) startTime.parentElement.style.display =
(mode === 'period' || mode === 'combined') ? '' : 'none';
if (endTime) endTime.parentElement.style.display =
(mode === 'period' || mode === 'combined') ? '' : 'none';
if (duration) duration.parentElement.style.display =
(mode === 'duration' || mode === 'combined') ? '' : 'none';
if (useDuration) useDuration.parentElement.style.display =
(mode === 'combined') ? '' : 'none';
if (resetCycle) resetCycle.parentElement.style.display =
(mode === 'duration' || mode === 'combined') ? '' : 'none';
}
};
// 时间段控制字段
o = s.option(form.Value, 'timestart', _('Allow Start Time'));
o.placeholder = '00:00';
o.default = '00:00';
o.depends({ 'time_mode': 'period', '!contains': true });
o.depends({ 'time_mode': 'combined', '!contains': true });
o = s.option(form.Value, 'timeend', _('Allow End Time'));
o.placeholder = '00:00';
o.default = '00:00';
o.depends({ 'time_mode': 'period', '!contains': true });
o.depends({ 'time_mode': 'combined', '!contains': true });
// 持续时间控制字段
o = s.option(form.Value, 'duration', _('Allowed Duration (minutes)'));
o.placeholder = '60';
o.default = '60';
o.datatype = 'min(1)';
o.depends({ 'time_mode': 'duration', '!contains': true });
o.depends({ 'time_mode': 'combined', '!contains': true });
o.description = _('设备上线后允许上网的分钟数,超过后将被禁止上网');
// 重置周期
o = s.option(cbiRichListValue, 'reset_cycle', _('Reset Cycle'));
o.value('daily', _('Daily Reset'));
o.value('weekly', _('Weekly Reset'));
o.value('monthly', _('Monthly Reset'));
o.value('never', _('Never Reset (until manual reset)'));
o.default = 'daily';
o.depends({ 'time_mode': 'duration', '!contains': true });
o.depends({ 'time_mode': 'combined', '!contains': true });
o.description = _('时长重置周期');
// 组合控制:是否在时间段内启用时长限制
o = s.option(form.Flag, 'use_duration', _('Enable Duration Limit in Period'));
o.default = '0';
o.depends({ 'time_mode': 'combined', '!contains': true });
o.description = _('在允许的时间段内限制上网时长');
o = s.option(form.Value, 'week', _('Week Day (1~7)'));
o.value('0', _('Everyday'));
o.value('1', _('Monday'));
o.value('2', _('Tuesday'));
o.value('3', _('Wednesday'));
o.value('4', _('Thursday'));
o.value('5', _('Friday'));
o.value('6', _('Saturday'));
o.value('7', _('Sunday'));
o.value('1,2,3,4,5', _('Workday'));
o.value('6,7', _('Rest Day'));
o.default = '0';
o.rmempty = false;
o.description = _('允许上网的星期');
return m.render();
}
});
@@ -1,238 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
/*
* Copyright (C) 2022-2026 sirpdboy <herboy2008@gmail.com>
*/
'use strict';
'require dom';
'require fs';
'require poll';
'require uci';
'require view';
'require form';
return view.extend({
render: function () {
var css = `
#log_textarea pre {
padding: 10px; /* 内边距 */
border-bottom: 1px solid #ddd; /* 边框颜色 */
font-size: small;
line-height: 1.3; /* 行高 */
white-space: pre-wrap;
word-wrap: break-word;
overflow-y: auto;
}
.cbi-section small {
margin-left: 1rem;
font-size: small;
}
.log-container {
display: flex;
flex-direction: column;
max-height: 1200px;
overflow-y: auto;
border-radius: 3px;
margin-top: 10px;
padding: 5px;
}
.log-line {
padding: 3px 0;
font-family: monospace;
font-size: 12px;
line-height: 1.4;
}
.log-line:last-child {
border-bottom: none;
}
.log-timestamp {
margin-right: 10px;
}
`;
var log_container = E('div', { 'class': 'log-container', 'id': 'log_container' },
E('img', {
'src': L.resource(['icons/loading.gif']),
'alt': _('Loading...'),
'style': 'vertical-align:middle'
}, _('Collecting data ...'))
);
var log_path = '/var/log/timecontrol.log';
var lastLogContent = '';
var lastScrollTop = 0;
var isScrolledToTop = true;
// 解析日志行的时间戳,用于排序
function parseLogTimestamp(logLine) {
// 假设日志格式为: [2024-01-01 12:00:00] INFO: some message
var timestampMatch = logLine.match(/^\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\]/);
if (timestampMatch) {
return new Date(timestampMatch[1]).getTime();
}
return Date.now();
}
function reverseLogLines(logContent) {
if (!logContent || logContent.trim() === '') {
return logContent;
}
var lines = logContent.split('\n');
lines = lines.filter(function(line) {
return line.trim() !== '';
});
lines.sort(function(a, b) {
var timeA = parseLogTimestamp(a);
var timeB = parseLogTimestamp(b);
return timeB - timeA; // 降序排列
});
return lines.join('\n');
}
function formatLogLines(logContent, isNewContent) {
if (!logContent || logContent.trim() === '') {
return E('div', { 'class': 'log-line' }, _('Log is clean.'));
}
var lines = logContent.split('\n');
var formattedLines = [];
for (var i = 0; i < lines.length; i++) {
var line = lines[i].trim();
if (line === '') continue;
var timestampMatch = line.match(/^\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\]/);
var timestampSpan = null;
var messageSpan = null;
var lineClass = 'log-line';
if (timestampMatch) {
timestampSpan = E('span', {
'class': 'log-timestamp',
'title': timestampMatch[1]
}, timestampMatch[0] + ' ');
messageSpan = E('span', {}, line.substring(timestampMatch[0].length + 1));
} else {
messageSpan = E('span', {}, line);
}
var lineDiv = E('div', { 'class': lineClass }, [
timestampSpan,
messageSpan
].filter(function(el) { return el !== null; }));
formattedLines.push(lineDiv);
}
return E('div', {}, formattedLines);
}
var clear_log_button = E('div', {}, [
E('button', {
'class': 'cbi-button cbi-button-remove',
'click': function (ev) {
ev.preventDefault();
var button = ev.target;
button.disabled = true;
button.textContent = _('Clear Logs...');
fs.exec_direct('/usr/libexec/timecontrol-call', ['clear_log'])
.then(function () {
button.textContent = _('Logs cleared successfully!');
button.disabled = false;
button.textContent = _('Clear Logs');
// 立即刷新日志显示框
var logContent = _('Log is clean.');
lastLogContent = logContent;
dom.content(log_container, formatLogLines(logContent, false));
isScrolledToTop = true; // 清空日志后,保持在顶部
})
.catch(function () {
button.textContent = _('Failed to clear log.');
button.disabled = false;
button.textContent = _('Clear Logs');
});
}
}, _('Clear Logs'))
]);
log_container.addEventListener('scroll', function() {
lastScrollTop = this.scrollTop;
isScrolledToTop = this.scrollTop <= 1;
});
poll.add(L.bind(function () {
return fs.read_direct(log_path, 'text')
.then(function (res) {
var logContent = res.trim();
if (logContent === '') {
logContent = _('Log is clean.');
}
// 检查内容是否有变化
if (logContent !== lastLogContent) {
var isNewContent = lastLogContent !== '' && lastLogContent !== _('Log is clean.');
var reversedLog = reverseLogLines(logContent);
// 格式化为HTML
var formattedLog = formatLogLines(reversedLog, isNewContent);
var prevScrollHeight = log_container.scrollHeight;
var prevScrollTop = log_container.scrollTop;
dom.content(log_container, formattedLog);
lastLogContent = logContent;
if (isScrolledToTop || isNewContent) {
log_container.scrollTop = 0;
} else {
var newScrollHeight = log_container.scrollHeight;
var heightDiff = newScrollHeight - prevScrollHeight;
log_container.scrollTop = prevScrollTop + heightDiff;
}
}
}).catch(function (err) {
var logContent;
if (err.toString().includes('NotFoundError')) {
logContent = _('Log file does not exist.');
} else {
logContent = _('Unknown error: %s').format(err);
}
if (logContent !== lastLogContent) {
dom.content(log_container, formatLogLines(logContent, false));
lastLogContent = logContent;
}
});
}));
// 启动轮询
poll.start();
return E('div', { 'class': 'cbi-map' }, [
E('style', [css]),
E('div', { 'class': 'cbi-section' }, [
clear_log_button,
log_container,
E('small', {}, _('Refresh every 5 seconds.').format(L.env.pollinterval)),
E('div', { 'class': 'cbi-section-actions cbi-section-actions-right' })
]),
E('div', { 'style': 'text-align: right; font-style: italic;' }, [
E('span', {}, [
_('© github '),
E('a', {
'href': 'https://github.com/sirpdboy',
'target': '_blank',
'style': 'text-decoration: none;'
}, 'by sirpdboy')
])
])
]);
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});
@@ -0,0 +1,19 @@
module("luci.controller.timecontrol", package.seeall)
function index()
if not nixio.fs.access("/etc/config/timecontrol") then return end
entry({"admin", "control"}, firstchild(), "Control", 44).dependent = false
local page = entry({"admin", "control", "timecontrol"}, cbi("timecontrol"), _("Internet Time Control"))
page.order = 10
page.dependent = true
page.acl_depends = { "luci-app-timecontrol" }
entry({"admin", "control", "timecontrol", "status"}, call("status")).leaf = true
end
function status()
local e = {}
e.status = luci.sys.call("/etc/init.d/timecontrol status >/dev/null 2>&1") == 0
luci.http.prepare_content("application/json")
luci.http.write_json(e)
end
@@ -0,0 +1,62 @@
local o = require "luci.sys"
local a, t, e
a = Map("timecontrol", translate("Internet Time Control"))
a.template = "timecontrol/index"
t = a:section(TypedSection, "basic")
t.anonymous = true
e = t:option(DummyValue, "timecontrol_status", translate("Status"))
e.template = "timecontrol/timecontrol"
e.value = translate("Collecting data...")
e = t:option(Flag, "enable", translate("Enabled"))
e.rmempty = false
t = a:section(TypedSection, "macbind", translate("Client Settings"))
t.template = "cbi/tblsection"
t.anonymous = true
t.addremove = true
e = t:option(Flag, "enable", translate("Enabled"))
e.rmempty = false
e = t:option(Value, "macaddr", "MAC")
e.rmempty = true
o.net.mac_hints(function(t, a) e:value(t, "%s (%s)" % {t, a}) end)
e = t:option(Value, "timeon", translate("No Internet start time"))
e.default = "00:00"
e.optional = false
e = t:option(Value, "timeoff", translate("No Internet end time"))
e.default = "23:59"
e.optional = false
e = t:option(Flag, "z1", translate("Monday"))
e.rmempty = true
e = t:option(Flag, "z2", translate("Tuesday"))
e.rmempty = true
e = t:option(Flag, "z3", translate("Wednesday"))
e.rmempty = true
e = t:option(Flag, "z4", translate("Thursday"))
e.rmempty = true
e = t:option(Flag, "z5", translate("Friday"))
e.rmempty = true
e = t:option(Flag, "z6", translate("Saturday"))
e.rmempty = true
e = t:option(Flag, "z7", translate("Sunday"))
e.rmempty = true
a.apply_on_parse = true
a.on_after_apply = function(self)
luci.sys.call("/etc/init.d/timecontrol reload >/dev/null 2>&1")
end
return a
@@ -0,0 +1,12 @@
<% include("cbi/map") %>
<script type="text/javascript">//<![CDATA[
XHR.poll(2, '<%=luci.dispatcher.build_url("admin", "control", "timecontrol", "status")%>', null,
function (x, result) {
var status = document.getElementsByClassName('timecontrol_status')[0];
status.setAttribute("style", "font-weight:bold;");
status.setAttribute("color", result.status ? "green" : "red");
status.innerHTML = result.status ? '<%=translate("RUNNING")%>' : '<%=translate("NOT RUNNING")%>';
}
)
//]]>
</script>
@@ -0,0 +1,3 @@
<%+cbi/valueheader%>
<font class="timecontrol_status"><%=pcdata(self:cfgvalue(section) or self.default or "")%></font>
<%+cbi/valuefooter%>
+15 -97
View File
@@ -1,123 +1,41 @@
msgid ""
msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
msgid "Control"
msgstr "管控"
msgid "Time Control"
msgstr "时间控制"
msgid "Internet Time Control"
msgstr "上网时间控制"
msgid "Users can limit their internet usage time through MAC and IP, with available IP ranges such as 192.168.110.00 to 192.168.10.200"
msgstr "用户可以通过MAC和IP限制其上网时间,可用IP范围如192.168.110.00至192.168.10.200"
msgid "Suggested feedback:"
msgstr "问题反馈:"
msgid "Checking service status..."
msgstr "正在检查服务状态..."
msgid "Status check failed"
msgstr "状态检查失败"
msgid "RUNNING"
msgstr "运行中"
msgid "NOT RUNNING"
msgstr "未运行"
msgid "TimeControl Service"
msgstr "时间控制服务"
msgid "Control Mode"
msgstr "控制模式"
msgid "blacklist: Block the networking of the target address, whitelist: Only allow networking for the target address and block all other addresses."
msgstr "黑名单:阻断目标地址的上网;白名单:仅允许目标地址上网,阻断其他所有地址。"
msgid "Blacklist"
msgstr "黑名单"
msgid "Whitelist"
msgstr "白名单"
msgid "Control Intensity"
msgstr "控制强度"
msgid "Pay attention to strong control: machines under control will not be able to connect to the software router backend!"
msgstr "注意强控制:被控制的机器将无法连接软件路由器后台!"
msgid "Ordinary forward control"
msgstr "普通管制"
msgid "Strong input control"
msgstr "强力管制"
msgid "Device Rules"
msgstr "设备规则"
msgid "Comment"
msgstr "备注"
msgid "Description"
msgstr "描述"
msgid "Status"
msgstr "状态"
msgid "Enabled"
msgstr "启用"
msgid "IP/MAC Address"
msgstr "IP/MAC地址"
msgid "Client Settings"
msgstr "客户端设置"
msgid "192.168.10.100 or 00:11:22:33:44:55"
msgstr "192.168.10.100 或 00:11:22:33:44:55"
msgid "No Internet start time"
msgstr "禁止上网开始时间"
msgid "-- Please select or enter manually --"
msgstr "-- 请选择或手动输入 --"
msgid "Start Control Time"
msgstr "控制开始时间"
msgid "00:00"
msgstr "00:00"
msgid "Stop Control Time"
msgstr "控制结束时间"
msgid "Week Day (1~7)"
msgstr "星期(1~7"
msgid "Everyday"
msgstr "每天"
msgid "No Internet end time"
msgstr "取消禁止上网时间"
msgid "Monday"
msgstr "星期一"
msgstr "一"
msgid "Tuesday"
msgstr "星期二"
msgstr "二"
msgid "Wednesday"
msgstr "星期三"
msgstr "三"
msgid "Thursday"
msgstr "星期四"
msgstr "四"
msgid "Friday"
msgstr "星期五"
msgstr "五"
msgid "Saturday"
msgstr "星期六"
msgstr "六"
msgid "Sunday"
msgstr "星期日"
msgid "Workday"
msgstr "工作日"
msgid "Rest Day"
msgstr "休息日"
msgid "© github "
msgstr "© 作者 "
msgstr "日"
@@ -1,21 +1,3 @@
config timecontrol
option enabled '0'
option control_mode 'blacklist'
option list_type 'blacklist'
option chain 'input'
config device
option timestart '00:00'
option week '0'
option timeend '23:55'
option mac ''
config basic
option enable '0'
config device
option mac '192.168.10.10/24'
option timestart '00:00'
option timeend '00:00'
option week '0'
option enable '0'
+214 -35
View File
@@ -1,51 +1,230 @@
#!/bin/sh /etc/rc.common
#
# Copyright (C) 2022-2026 sirpdboy herboy2008@gmail.com
#
START=99
USE_PROCD=1
STOP=10
NAME=timecontrol
LOCK="/var/lock/$NAME.lock"
EXTRA_COMMANDS="status"
EXTRA_HELP=" status Check if timecontrol rules are active\n"
start_instance() {
procd_open_instance
procd_set_param command /usr/bin/timecontrolctrl
procd_set_param respawn
procd_set_param stderr 1
procd_close_instance
. /lib/functions.sh
TABLE="timecontrol"
CHAIN="TIMECONTROL"
firewall_backend() {
if command -v fw4 >/dev/null 2>&1 && command -v nft >/dev/null 2>&1; then
echo nft
else
echo iptables
fi
}
_timecontrol_start() {
if [ "$(grep -c 'option enable .1.' /etc/config/$NAME 2>/dev/null)" -gt "0" ]; then
touch $LOCK
timecontrol start
sleep 2
start_instance
else
stop_service
fi
have_ip6tables() {
command -v ip6tables >/dev/null 2>&1
}
start_service(){
[ -f $LOCK ] && exit
_timecontrol_start
rm -f $LOCK
valid_mac() {
printf '%s\n' "$1" | grep -Eq '^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$'
}
service_triggers() {
procd_add_reload_trigger 'timecontrol'
valid_time() {
printf '%s\n' "$1" | grep -Eq '^([01][0-9]|2[0-3]):[0-5][0-9]$'
}
stop_service(){
kill -9 $(busybox ps -w | grep 'timecontrolctrl' | grep -v 'grep' | awk '{print $1}') >/dev/null 2>&1
killall timecontrolctrl 2>/dev/null
rm -f $LOCK 2>/dev/null
timecontrol stop
add_nft_range() {
local macaddr="$1"
local timeon="$2"
local timeoff="$3"
local weekdays="$4"
nft -f - <<-EOF
add rule inet $TABLE forward ether saddr $macaddr meta day { $weekdays } meta hour "$timeon"-"$timeoff" counter drop
EOF
}
reload_service() {
restart
add_nft_rule() {
local macaddr="$1"
local timeon="$2"
local timeoff="$3"
local weekdays="$4"
local weekdays_next="$5"
if [ "$timeon" \< "$timeoff" ] || [ "$timeon" = "$timeoff" ]; then
add_nft_range "$macaddr" "$timeon" "$timeoff" "$weekdays"
else
# Range spans midnight: block until 23:59:59 on the selected
# days, then from 00:00 until timeoff on the following days.
add_nft_range "$macaddr" "$timeon" "23:59:59" "$weekdays"
add_nft_range "$macaddr" "00:00" "$timeoff" "$weekdays_next"
fi
}
add_ipt_range() {
local cmd="$1"
local macaddr="$2"
local timeon="$3"
local timeoff="$4"
local weekdays="$5"
"$cmd" -w -t filter -A "$CHAIN" -m mac --mac-source "$macaddr" \
-m time --kerneltz --timestart "$timeon" --timestop "$timeoff" \
--weekdays "$weekdays" -j DROP
}
add_ipt_rule() {
local macaddr="$1"
local timeon="$2"
local timeoff="$3"
local weekdays="$4"
local weekdays_next="$5"
local cmd
# Mirror every rule into ip6tables as well, otherwise IPv6 traffic
# would bypass the time control completely.
for cmd in iptables ip6tables; do
command -v "$cmd" >/dev/null 2>&1 || continue
if [ "$timeon" \< "$timeoff" ] || [ "$timeon" = "$timeoff" ]; then
add_ipt_range "$cmd" "$macaddr" "$timeon" "$timeoff" "$weekdays"
else
# Range spans midnight: block until 23:59:59 on the
# selected days, then from 00:00 until timeoff on the
# following days.
add_ipt_range "$cmd" "$macaddr" "$timeon" "23:59:59" "$weekdays"
add_ipt_range "$cmd" "$macaddr" "00:00" "$timeoff" "$weekdays_next"
fi
done
}
load_rule() {
local section="$1"
local enabled macaddr timeon timeoff
local z1 z2 z3 z4 z5 z6 z7
local ipt_days nft_days ipt_days_next nft_days_next
config_get_bool enabled "$section" enable 0
[ "$enabled" -eq 1 ] || return 0
config_get macaddr "$section" macaddr
config_get timeon "$section" timeon
config_get timeoff "$section" timeoff
valid_mac "$macaddr" && valid_time "$timeon" && valid_time "$timeoff" || {
logger -t timecontrol "Ignoring invalid rule in section $section"
return 0
}
config_get_bool z1 "$section" z1 0
config_get_bool z2 "$section" z2 0
config_get_bool z3 "$section" z3 0
config_get_bool z4 "$section" z4 0
config_get_bool z5 "$section" z5 0
config_get_bool z6 "$section" z6 0
config_get_bool z7 "$section" z7 0
# The *_next lists hold each selected weekday shifted by one day;
# they apply to the after-midnight part of ranges spanning midnight.
[ "$z1" -eq 1 ] && { append ipt_days Mon ,; append ipt_days_next Tue ,; append nft_days monday ,; append nft_days_next tuesday ,; }
[ "$z2" -eq 1 ] && { append ipt_days Tue ,; append ipt_days_next Wed ,; append nft_days tuesday ,; append nft_days_next wednesday ,; }
[ "$z3" -eq 1 ] && { append ipt_days Wed ,; append ipt_days_next Thu ,; append nft_days wednesday ,; append nft_days_next thursday ,; }
[ "$z4" -eq 1 ] && { append ipt_days Thu ,; append ipt_days_next Fri ,; append nft_days thursday ,; append nft_days_next friday ,; }
[ "$z5" -eq 1 ] && { append ipt_days Fri ,; append ipt_days_next Sat ,; append nft_days friday ,; append nft_days_next saturday ,; }
[ "$z6" -eq 1 ] && { append ipt_days Sat ,; append ipt_days_next Sun ,; append nft_days saturday ,; append nft_days_next sunday ,; }
[ "$z7" -eq 1 ] && { append ipt_days Sun ,; append ipt_days_next Mon ,; append nft_days sunday ,; append nft_days_next monday ,; }
[ -n "$ipt_days" ] || return 0
if [ "$BACKEND" = nft ]; then
add_nft_rule "$macaddr" "$timeon" "$timeoff" "$nft_days" "$nft_days_next"
else
add_ipt_rule "$macaddr" "$timeon" "$timeoff" "$ipt_days" "$ipt_days_next"
fi
}
load_basic() {
config_get_bool ENABLED "$1" enable 0
}
start_nft() {
nft -f - <<-EOF
table inet $TABLE {
chain forward {
type filter hook forward priority -1; policy accept;
}
}
EOF
# Flush fw4's flowtable so that connections already on the fast path
# (which bypasses this forward hook) are forced back to the slow path
# where our DROP rules can reach them. Non-blocked devices will
# re-offload within seconds; the disruption is minimal.
nft flush flowtable inet fw4 flowtable_ft 2>/dev/null
}
start_iptables() {
iptables -w -t filter -N "$CHAIN" || return 1
iptables -w -t filter -I FORWARD 1 -j "$CHAIN"
if have_ip6tables; then
ip6tables -w -t filter -N "$CHAIN" || return 1
ip6tables -w -t filter -I FORWARD 1 -j "$CHAIN"
else
logger -t timecontrol "ip6tables not found; IPv6 traffic will not be controlled"
fi
}
stop_nft() {
command -v nft >/dev/null 2>&1 && nft delete table inet "$TABLE" 2>/dev/null
return 0
}
stop_ipt_family() {
local cmd="$1"
command -v "$cmd" >/dev/null 2>&1 || return 0
while "$cmd" -w -t filter -C FORWARD -j "$CHAIN" 2>/dev/null; do
"$cmd" -w -t filter -D FORWARD -j "$CHAIN" 2>/dev/null || break
done
"$cmd" -w -t filter -F "$CHAIN" 2>/dev/null
"$cmd" -w -t filter -X "$CHAIN" 2>/dev/null
}
stop_iptables() {
stop_ipt_family iptables
stop_ipt_family ip6tables
}
start() {
config_load timecontrol
ENABLED=0
config_foreach load_basic basic
[ "$ENABLED" -eq 1 ] || return 0
stop_nft
stop_iptables
BACKEND="$(firewall_backend)"
mkdir -p /var/etc
printf '%s\n' "/etc/init.d/timecontrol reload" > /var/etc/timecontrol.include
if [ "$BACKEND" = nft ]; then
start_nft || return 1
else
start_iptables || return 1
fi
config_foreach load_rule macbind
}
stop() {
stop_nft
stop_iptables
}
reload() {
stop
start
}
status() {
if [ "$(firewall_backend)" = nft ]; then
nft list table inet "$TABLE" >/dev/null 2>&1
else
iptables -w -t filter -S "$CHAIN" >/dev/null 2>&1
fi
}
@@ -0,0 +1,38 @@
#!/bin/sh
[ ! -f "/usr/share/ucitrack/luci-app-timecontrol.json" ] && {
cat > /usr/share/ucitrack/luci-app-timecontrol.json << EEOF
{
"config": "timecontrol",
"init": "timecontrol"
}
EEOF
}
uci -q batch <<-EOF >/dev/null
delete firewall.timecontrol
EOF
if ! command -v fw4 >/dev/null 2>&1; then
uci -q batch <<-EOF >/dev/null
set firewall.timecontrol=include
set firewall.timecontrol.type=script
set firewall.timecontrol.path=/var/etc/timecontrol.include
set firewall.timecontrol.reload=1
EOF
install -d /var/etc
printf '%s\n' "/etc/init.d/timecontrol reload" > /var/etc/timecontrol.include
fi
uci -q commit firewall
[ -f "/etc/config/ucitrack" ] && {
uci -q batch <<-EOF >/dev/null
delete ucitrack.@timecontrol[-1]
add ucitrack timecontrol
set ucitrack.@timecontrol[-1].init=timecontrol
commit ucitrack
EOF
}
rm -rf /tmp/luci-*cache
exit 0
@@ -1,22 +0,0 @@
#!/bin/sh
[ ! -f "/usr/share/ucitrack/luci-app-timecontrol.json" ] && {
cat > /usr/share/ucitrack/luci-app-timecontrol.json << EEOF
{
"config": "timecontrol",
"init": "timecontrol"
}
EEOF
}
chmod +x /etc/init.d/timecontrol /usr/bin/timecontrol* /usr/libexec/timecontrol-call
uci -q batch <<-EOF >/dev/null
delete ucitrack.@timecontrol[-1]
add ucitrack timecontrol
set ucitrack.@timecontrol[-1].init=timecontrol
commit ucitrack
EOF
[ -s /etc/config/timecontrol ] || echo "config timecontrol" > /etc/config/timecontrol
/etc/init.d/rpcd restart
rm -f /tmp/luci-indexcache
exit 0
@@ -1,600 +0,0 @@
#!/bin/bash
# Copyright (C) 2006 OpenWrt.org
# Copyright 2022-2026 sirpdboy <herboy2008@gmail.com>
crrun=$1
crid=$2
NAME=timecontrol
DEBUG=1 # 开启调试
config_t_get() {
local index=${3:-0}
local ret=$(uci -q get "${NAME}.@${1}[${index}].${2}")
echo "${ret:-$4}"
}
LOG_FILE="/var/log/timecontrol.log"
IDLIST="/var/$NAME.idlist"
bin_nft=$(which nft 2>/dev/null)
bin_iptables=$(which iptables 2>/dev/null)
bin_ip6tables=$(which ip6tables 2>/dev/null)
bin_conntrack=$(which conntrack 2>/dev/null)
nftables_ver=0
iptables_ver=0
# 获取配置
chain=$(config_t_get timecontrol chain 0 "forward")
list_type=$(config_t_get timecontrol list_type 0 "blacklist")
if [ "$chain" = "input" ]; then
StrongCHAIN=1
else
StrongCHAIN=0
fi
dbg() {
if [ "$DEBUG" -eq 1 ]; then
local d="$(date '+%Y-%m-%d %H:%M:%S')"
echo "[$d] FW-DEBUG: $@" >> "$LOG_FILE"
echo "FW-DEBUG: $@"
fi
}
info() {
local d="$(date '+%Y-%m-%d %H:%M:%S')"
echo "[$d] FW-INFO: $@" >> "$LOG_FILE"
echo "FW-INFO: $@"
}
# 地址解析函数 - 修复格式问题
parse_target() {
local target="$1"
# 去除空格
target=$(echo "${target}" | xargs)
# dbg "解析目标地址: $target"
# IPv4单个地址
if echo "$target" | grep -qE '^([0-9]{1,3}\.){3}[0-9]{1,3}$'; then
local octets=(${target//./ })
local valid=1
for octet in "${octets[@]}"; do
if [ "$octet" -gt 255 ] || [ "$octet" -lt 0 ]; then
valid=0
break
fi
done
[ "$valid" -eq 1 ] && {
echo "ipv4:single:$target"
return 0
}
# IPv4范围
elif echo "$target" | grep -qE '^([0-9]{1,3}\.){3}[0-9]{1,3}-([0-9]{1,3}\.){3}[0-9]{1,3}$'; then
local start_ip=${target%-*}
local end_ip=${target#*-}
echo "ipv4:range:$start_ip-$end_ip"
return 0
# CIDR
elif echo "$target" | grep -qE '^([0-9]{1,3}\.){3}[0-9]{1,3}/[0-9]{1,2}$'; then
local ip=${target%/*}
local mask=${target#*/}
[ "$mask" -le 32 ] && [ "$mask" -ge 0 ] && {
echo "ipv4:cidr:$target"
return 0
}
# MAC地址
elif echo "$target" | grep -qE '^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$'; then
echo "mac:single:$(echo "$target" | tr '[:upper:]' '[:lower:]')"
return 0
# IPv6地址
elif echo "$target" | grep -qE '^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$'; then
echo "ipv6:single:$target"
return 0
# IPv6 CIDR
elif echo "$target" | grep -qE '^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}/[0-9]{1,3}$'; then
local ipv6=${target%/*}
local mask=${target#*/}
[ "$mask" -le 128 ] && [ "$mask" -ge 0 ] && {
echo "ipv6:cidr:$target"
return 0
}
fi
dbg "无法解析地址: $target"
return 1
}
# 清理现有连接
flush_connections() {
local target="$1"
[ -x "$bin_conntrack" ] || {
dbg "conntrack不可用"
return
}
local parsed_result=$(parse_target "$target")
[ $? -eq 0 ] || {
dbg "无法解析地址用于清理连接: $target"
return
}
IFS=':' read -r type subtype value <<< "$parsed_result"
dbg "清理连接: type=$type, value=$value"
case "$type" in
"ipv4")
$bin_conntrack -D -s "$value" 2>/dev/null && dbg "清理源连接: $value"
$bin_conntrack -D -d "$value" 2>/dev/null && dbg "清理目标连接: $value"
;;
"mac")
# MAC地址需要先转换为IP
if [ -f "/proc/net/arp" ]; then
local ip_addr=$(grep -i "$value" /proc/net/arp 2>/dev/null | awk '{print $1}' | head -1)
if [ -n "$ip_addr" ]; then
$bin_conntrack -D -s "$ip_addr" 2>/dev/null && dbg "清理MAC源连接: $value -> $ip_addr"
$bin_conntrack -D -d "$ip_addr" 2>/dev/null && dbg "清理MAC目标连接: $value -> $ip_addr"
fi
fi
;;
esac
}
# 检查防火墙工具
check_firewall_tool() {
if [ -x "$bin_nft" ]; then
nftables_ver=1
dbg "检测到nftables: $bin_nft"
elif [ -x "$bin_iptables" ] && [ -x "$bin_ip6tables" ]; then
iptables_ver=1
dbg "检测到iptables: $bin_iptables, $bin_ip6tables"
else
info "错误: 未找到可用的防火墙工具"
return 1
fi
return 0
}
# 初始化防火墙
init_firewall() {
check_firewall_tool || return 1
info "初始化防火墙规则 (模式: $list_type, 强度: $chain)"
if [ -n "$nftables_ver" ]; then
# 使用nftables
dbg "初始化nftables"
# 删除可能存在的旧表
nft delete table inet timecontrol 2>/dev/null
sleep 1
# 创建新表
nft add table inet timecontrol
nft add chain inet timecontrol forward "{ type filter hook forward priority -100; policy accept; }"
# 创建黑名单集合
nft add set inet timecontrol blacklist "{ type ipv4_addr; flags interval; }"
nft add set inet timecontrol blacklist6 "{ type ipv6_addr; flags interval; }"
nft add set inet timecontrol blacklist_mac "{ type ether_addr; }"
# 添加规则(黑名单模式:匹配到就DROP)
nft add rule inet timecontrol forward ip saddr @blacklist drop
nft add rule inet timecontrol forward ip6 saddr @blacklist6 drop
nft add rule inet timecontrol forward ether saddr @blacklist_mac drop
# 强控制模式
if [ "$StrongCHAIN" -eq 1 ]; then
nft add chain inet timecontrol input "{ type filter hook input priority -100; policy accept; }"
nft add rule inet timecontrol input ip saddr @blacklist drop
nft add rule inet timecontrol input ip6 saddr @blacklist6 drop
nft add rule inet timecontrol input ether saddr @blacklist_mac drop
dbg "已启用强控制模式 (INPUT链)"
fi
info "nftables初始化完成"
elif [ -n "$iptables_ver" ]; then
# 使用iptables
dbg "初始化iptables"
# 创建ipset(如果不存在)
ipset create timecontrol_blacklist hash:net 2>/dev/null || {
ipset flush timecontrol_blacklist
dbg "已存在的ipset timecontrol_blacklist已清空"
}
ipset create timecontrol_blacklist6 hash:net family inet6 2>/dev/null || {
ipset flush timecontrol_blacklist6
dbg "已存在的ipset timecontrol_blacklist6已清空"
}
# 删除可能存在的旧规则
iptables -D FORWARD -m set --match-set timecontrol_blacklist src -j DROP 2>/dev/null
ip6tables -D FORWARD -m set --match-set timecontrol_blacklist6 src -j DROP 2>/dev/null
# 添加新规则(黑名单模式)
iptables -I FORWARD -m set --match-set timecontrol_blacklist src -j DROP
ip6tables -I FORWARD -m set --match-set timecontrol_blacklist6 src -j DROP
dbg "已添加FORWARD规则"
# 强控制模式
if [ "$StrongCHAIN" -eq 1 ]; then
iptables -D INPUT -m set --match-set timecontrol_blacklist src -j DROP 2>/dev/null
ip6tables -D INPUT -m set --match-set timecontrol_blacklist6 src -j DROP 2>/dev/null
iptables -I INPUT -m set --match-set timecontrol_blacklist src -j DROP
ip6tables -I INPUT -m set --match-set timecontrol_blacklist6 src -j DROP
dbg "已启用强控制模式 (INPUT链)"
fi
info "iptables初始化完成"
fi
return 0
}
# 停止防火墙规则
stop_firewall() {
info "停止防火墙规则"
if [ -n "$nftables_ver" ]; then
nft delete table inet timecontrol 2>/dev/null && info "nftables规则已删除"
fi
if [ -n "$iptables_ver" ]; then
# 删除iptables规则
iptables -D FORWARD -m set --match-set timecontrol_blacklist src -j DROP 2>/dev/null
iptables -D INPUT -m set --match-set timecontrol_blacklist src -j DROP 2>/dev/null
ip6tables -D FORWARD -m set --match-set timecontrol_blacklist6 src -j DROP 2>/dev/null
ip6tables -D INPUT -m set --match-set timecontrol_blacklist6 src -j DROP 2>/dev/null
# 删除ipset
ipset destroy timecontrol_blacklist 2>/dev/null
ipset destroy timecontrol_blacklist6 2>/dev/null
info "iptables规则已删除"
fi
# 清理ID列表
rm -f "$IDLIST"
}
# 添加设备到防火墙
add_device() {
local id="$1"
local target=$(config_t_get device mac "$id")
[ -z "$target" ] && {
dbg "添加设备失败: ID $id 的目标地址为空"
return
}
local comment=$(config_t_get device comment "$id" "设备$id")
info "添加设备到防火墙: $comment ($target)"
local parsed_result=$(parse_target "$target")
if [ $? -ne 0 ]; then
info "添加失败: 无法解析地址 $target"
return
fi
IFS=':' read -r type subtype value <<< "$parsed_result"
dbg "解析结果: type=$type, subtype=$subtype, value=$value"
if [ -n "$nftables_ver" ]; then
# nftables处理
case "$type" in
"ipv4")
nft add element inet timecontrol blacklist "{ $value }" 2>&1 | while read line; do dbg "nft: $line"; done
dbg "已添加到nftables黑名单(IPv4): $value"
;;
"ipv6")
nft add element inet timecontrol blacklist6 "{ $value }" 2>&1 | while read line; do dbg "nft: $line"; done
dbg "已添加到nftables黑名单(IPv6): $value"
;;
"mac")
nft add element inet timecontrol blacklist_mac "{ $value }" 2>&1 | while read line; do dbg "nft: $line"; done
dbg "已添加到nftables黑名单(MAC): $value"
;;
esac
elif [ -n "$iptables_ver" ]; then
# iptables处理
case "$type" in
"ipv4")
ipset add timecontrol_blacklist "$value" 2>&1 | while read line; do dbg "ipset: $line"; done
dbg "已添加到ipset黑名单(IPv4): $value"
;;
"ipv6")
ipset add timecontrol_blacklist6 "$value" 2>&1 | while read line; do dbg "ipset: $line"; done
dbg "已添加到ipset黑名单(IPv6): $value"
;;
"mac")
# iptables不支持MAC地址直接过滤,记录日志
info "警告: iptables不支持MAC地址过滤,设备 $target 可能无法被阻止"
;;
esac
fi
# 强控制模式清理连接
if [ "$StrongCHAIN" -eq 1 ]; then
dbg "强控制模式,清理现有连接"
flush_connections "$target"
fi
# 验证规则
verify_firewall_rule "$target"
}
# 验证防火墙规则
verify_firewall_rule() {
local target="$1"
dbg "验证防火墙规则: $target"
if [ -n "$nftables_ver" ]; then
nft list table inet timecontrol 2>/dev/null | grep -q "$target" && {
dbg "验证成功: $target 在nftables规则中"
return 0
}
elif [ -n "$iptables_ver" ]; then
ipset test timecontrol_blacklist "$target" 2>/dev/null && {
dbg "验证成功: $target 在ipset中"
return 0
}
fi
dbg "验证失败: $target 不在防火墙规则中"
return 1
}
# 从防火墙移除设备
del_device() {
local id="$1"
local target=$(config_t_get device mac "$id")
[ -z "$target" ] && {
dbg "移除设备失败: ID $id 的目标地址为空"
return
}
local comment=$(config_t_get device comment "$id" "设备$id")
info "从防火墙移除设备: $comment ($target)"
local parsed_result=$(parse_target "$target")
[ $? -eq 0 ] || {
info "移除失败: 无法解析地址 $target"
return
}
IFS=':' read -r type subtype value <<< "$parsed_result"
if [ -n "$nftables_ver" ]; then
case "$type" in
"ipv4")
nft delete element inet timecontrol blacklist "{ $value }" 2>/dev/null
dbg "已从nftables移除(IPv4): $value"
;;
"ipv6")
nft delete element inet timecontrol blacklist6 "{ $value }" 2>/dev/null
dbg "已从nftables移除(IPv6): $value"
;;
"mac")
nft delete element inet timecontrol blacklist_mac "{ $value }" 2>/dev/null
dbg "已从nftables移除(MAC): $value"
;;
esac
elif [ -n "$iptables_ver" ]; then
case "$type" in
"ipv4")
ipset del timecontrol_blacklist "$value" 2>/dev/null
dbg "已从ipset移除(IPv4): $value"
;;
"ipv6")
ipset del timecontrol_blacklist6 "$value" 2>/dev/null
dbg "已从ipset移除(IPv6): $value"
;;
esac
fi
}
# 显示防火墙状态
show_firewall_status() {
echo ""
echo "防火墙状态:"
echo "控制模式: $list_type"
echo "控制强度: $chain $( [ "$StrongCHAIN" -eq 1 ] && echo "(强控制)" )"
echo ""
if [ -n "$nftables_ver" ]; then
echo "nftables规则:"
nft list table inet timecontrol 2>/dev/null || echo " 未找到timecontrol表"
elif [ -n "$iptables_ver" ]; then
echo "iptables规则:"
echo "FORWARD链:"
iptables -L FORWARD -n | grep -i timecontrol || echo " 未找到timecontrol规则"
ip6tables -L FORWARD -n | grep -i timecontrol || echo " 未找到IPv6 timecontrol规则"
if [ "$StrongCHAIN" -eq 1 ]; then
echo ""
echo "INPUT链:"
iptables -L INPUT -n | grep -i timecontrol || echo " 未找到timecontrol规则"
ip6tables -L INPUT -n | grep -i timecontrol || echo " 未找到IPv6 timecontrol规则"
fi
echo ""
echo "ipset内容:"
ipset list timecontrol_blacklist 2>/dev/null | head -20 || echo " timecontrol_blacklist未找到"
echo ""
ipset list timecontrol_blacklist6 2>/dev/null | head -20 || echo " timecontrol_blacklist6未找到"
fi
}
# 诊断函数
diagnose() {
echo ""
echo "=== 时间控制系统诊断 ==="
echo ""
# 检查服务
echo "1. 服务状态:"
if ps | grep -q "timecontrolctrl"; then
echo " ✓ timecontrolctrl 正在运行"
else
echo " ✗ timecontrolctrl 未运行"
fi
# 检查配置文件
echo ""
echo "2. 配置文件:"
if [ -f "/etc/config/timecontrol" ]; then
echo " ✓ 配置文件存在"
uci show timecontrol 2>/dev/null | grep -c "device" | while read count; do
echo " 配置了 $count 个设备"
done
else
echo " ✗ 配置文件不存在"
fi
# 检查防火墙工具
echo ""
echo "3. 防火墙工具:"
if [ -x "$bin_nft" ]; then
echo " ✓ nftables: $bin_nft"
echo " 版本: $($bin_nft --version 2>/dev/null | head -1)"
elif [ -x "$bin_iptables" ]; then
echo " ✓ iptables: $bin_iptables"
echo " 版本: $($bin_iptables --version 2>/dev/null | head -1)"
else
echo " ✗ 未找到防火墙工具"
fi
# 显示当前规则
show_firewall_status
# 检查ID列表
echo ""
echo "4. 当前控制列表:"
if [ -f "$IDLIST" ] && [ -s "$IDLIST" ]; then
echo " 当前禁止的设备:"
cat "$IDLIST" | sed 's/!//g' | while read id; do
local target=$(config_t_get device mac "$id")
local comment=$(config_t_get device comment "$id" "设备$id")
echo " ID$id: $comment ($target)"
done
else
echo " 当前没有设备被禁止"
fi
echo ""
echo "=== 诊断完成 ==="
}
# 主命令处理
case "$crrun" in
"start")
info "启动时间控制"
stop_firewall
init_firewall
if [ $? -eq 0 ]; then
info "时间控制启动成功"
show_firewall_status
else
info "时间控制启动失败"
fi
;;
"stop")
info "停止时间控制"
stop_firewall
info "时间控制已停止"
;;
"add")
[ -z "$crid" ] && {
echo "错误: 需要指定设备ID"
exit 1
}
info "添加设备控制: ID=$crid"
add_device "$crid"
show_firewall_status
;;
"del")
[ -z "$crid" ] && {
echo "错误: 需要指定设备ID"
exit 1
}
info "移除设备控制: ID=$crid"
del_device "$crid"
show_firewall_status
;;
"status")
show_firewall_status
;;
"diagnose")
diagnose
;;
"test")
# 测试地址解析
echo "测试地址解析:"
for test in "192.168.1.100" "192.168.1.0/24" "00:11:22:33:44:55" "invalid"; do
echo -n "$test: "
if parse_target "$test" >/dev/null; then
echo "✓ 有效"
parse_target "$test"
else
echo "✗ 无效"
fi
done
;;
"flush")
# 清理所有连接
info "清理所有连接"
if [ -x "$bin_conntrack" ]; then
$bin_conntrack -F
info "连接已清理"
else
info "conntrack不可用"
fi
;;
"help"|"")
echo "时间控制系统命令工具"
echo ""
echo "用法: $0 {start|stop|add <id>|del <id>|status|diagnose|test|flush|help}"
echo ""
echo "命令说明:"
echo " start - 初始化防火墙规则"
echo " stop - 停止并清理所有防火墙规则"
echo " add <id> - 添加设备到控制列表"
echo " del <id> - 从控制列表移除设备"
echo " status - 显示防火墙状态"
echo " diagnose - 系统诊断"
echo " test - 测试地址解析"
echo " flush - 清理所有网络连接"
echo " help - 显示此帮助信息"
;;
*)
echo "错误: 未知命令 '$crrun'"
echo "使用: $0 help 查看帮助"
exit 1
;;
esac
@@ -1,117 +0,0 @@
#!/bin/bash
# 时间控制日志查看工具
NAME=timecontrol
LOG_FILE="/var/log/$NAME.log"
STATUS_LOG="/var/lib/$NAME/status.log"
CONNECTION_LOG="/var/lib/$NAME/connections.log"
show_realtime_log() {
echo "正在显示实时日志,按 Ctrl+C 退出..."
echo ""
tail -f "$LOG_FILE" | while read line; do
# 高亮显示重要信息
if echo "$line" | grep -q "STATUS-CHANGE\|TIME_EXCEEDED\|RESET"; then
echo -e "\033[1;31m$line\033[0m" # 红色显示重要变更
elif echo "$line" | grep -q "ALLOW_ACCESS\|解除限制"; then
echo -e "\033[1;32m$line\033[0m" # 绿色显示允许访问
elif echo "$line" | grep -q "BLOCK_ACCESS\|添加限制"; then
echo -e "\033[1;33m$line\033[0m" # 黄色显示禁止访问
else
echo "$line"
fi
done
}
show_status_log() {
echo "最近状态变更记录:"
echo "────────────────────────────────────────────────────────────────────"
if [ -f "$STATUS_LOG" ]; then
tail -n 20 "$STATUS_LOG" | while read line; do
local time=$(echo "$line" | cut -d']' -f1 | sed 's/\[//')
local message=$(echo "$line" | cut -d']' -f2-)
printf "%-20s %s\n" "$time" "$message"
done
else
echo "暂无状态记录"
fi
}
show_connection_log() {
echo "设备连接记录:"
echo "────────────────────────────────────────────────────────────────────"
echo "时间 设备 状态"
echo "────────────────────────────────────────────────────────────────────"
if [ -f "$CONNECTION_LOG" ]; then
tail -n 20 "$CONNECTION_LOG" | while read line; do
local timestamp=$(echo "$line" | cut -d',' -f1)
local target=$(echo "$line" | cut -d',' -f2)
local action=$(echo "$line" | cut -d',' -f3)
local time_str=$(date -d "@$timestamp" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || echo "$timestamp")
local action_text=""
case "$action" in
"connect") action_text="上线" ;;
"disconnect") action_text="下线" ;;
*) action_text="$action" ;;
esac
printf "%-20s %-18s %s\n" "$time_str" "$target" "$action_text"
done
else
echo "暂无连接记录"
fi
}
show_summary() {
local summary_file="/tmp/timecontrol_status.txt"
if [ -f "$summary_file" ]; then
cat "$summary_file"
else
echo "状态摘要文件不存在,正在生成..."
timecontrol status
fi
}
show_help() {
echo "时间控制日志查看工具"
echo ""
echo "用法: timecontrol-log {realtime|status|connections|summary|help}"
echo ""
echo "命令:"
echo " realtime - 实时显示日志(彩色高亮)"
echo " status - 显示状态变更记录"
echo " connections - 显示设备连接记录"
echo " summary - 显示状态摘要"
echo " help - 显示此帮助"
echo ""
echo "示例:"
echo " timecontrol-log realtime # 实时监控"
echo " timecontrol-log status # 查看状态变更"
}
case "$1" in
"realtime")
show_realtime_log
;;
"status")
show_status_log
;;
"connections")
show_connection_log
;;
"summary")
show_summary
;;
"help"|"")
show_help
;;
*)
echo "未知命令: $1"
show_help
exit 1
;;
esac
@@ -1,335 +0,0 @@
#!/bin/sh
# Copyright (C) 2006 OpenWrt.org
# Copyright 2022-2026 sirpdboy <herboy2008@gmail.com>
NAME=timecontrol
LOG_FILE="/var/log/timecontrol.log"
DEBUG=1
# 时长数据库目录
DURATION_DIR="/var/lib/timecontrol"
DURATION_DB="$DURATION_DIR/duration.db"
CONNECTION_LOG="$DURATION_DIR/connections.log"
# 状态文件
IDLIST="/var/$NAME.idlist"
STATUS_DB="$DURATION_DIR/status.db"
# 初始化目录
init_dirs() {
mkdir -p "$DURATION_DIR"
touch "$DURATION_DB" "$CONNECTION_LOG" "$STATUS_DB" 2>/dev/null
}
# 日志函数
dbg() {
[ "$DEBUG" -eq 1 ] && {
local d="$(date '+%Y-%m-%d %H:%M:%S')"
echo "[$d] CTRL-DEBUG: $@" >> "$LOG_FILE"
}
}
info() {
local d="$(date '+%Y-%m-%d %H:%M:%S')"
echo "[$d] CTRL-INFO: $@" >> "$LOG_FILE"
}
# 配置文件读取
config_t_get() {
local index=${3:-0}
local ret=$(uci -q get "${NAME}.@${1}[${index}].${2}")
echo "${ret:=${4}}"
}
# 获取启用设备
get_enabled_devices() {
uci show $NAME 2>/dev/null | grep "enable='1'" | grep "device" | grep -oE '\[.*?\]' | grep -o '[0-9]' | sort -n
}
# 时间检查函数
is_time_in_range() {
local start_time=$1
local end_time=$2
local current_time=$(date +%H:%M)
if [ "$start_time" = "$end_time" ]; then
return 0
elif [ "$start_time" \< "$end_time" ]; then
[ "$current_time" \> "$start_time" ] && [ "$current_time" \< "$end_time" ] && return 0
else
[ "$current_time" \> "$start_time" ] || [ "$current_time" \< "$end_time" ] && return 0
fi
return 1
}
is_weekday_in_range() {
local configured_weekdays=$1
local current_weekday=$(date +%u)
[ "$configured_weekdays" = "0" ] && return 0
for ww in $(echo $configured_weekdays | sed 's/,/ /g'); do
[ "$current_weekday" = "$ww" ] && return 0
done
return 1
}
# 时长管理
record_connection_time() {
local target="$1"
local action="$2"
local timestamp=$(date +%s)
echo "$timestamp,$target,$action" >> "$CONNECTION_LOG"
if [ "$action" = "connect" ]; then
if ! grep -q "^$target," "$DURATION_DB" 2>/dev/null; then
echo "$target,$timestamp,0,0" >> "$DURATION_DB"
dbg "初始化时长: $target"
fi
elif [ "$action" = "disconnect" ]; then
if grep -q "^$target," "$DURATION_DB" 2>/dev/null; then
local last_connect=$(grep "^$target," "$DURATION_DB" | cut -d',' -f2)
local total_used=$(grep "^$target," "$DURATION_DB" | cut -d',' -f3)
local last_reset=$(grep "^$target," "$DURATION_DB" | cut -d',' -f4)
local session_duration=$((timestamp - last_connect))
local new_total=$((total_used + session_duration))
sed -i "/^$target,/d" "$DURATION_DB"
echo "$target,$timestamp,$new_total,$last_reset" >> "$DURATION_DB"
fi
fi
}
get_connection_time() {
local target="$1"
local current_time=$(date +%s)
if grep -q "^$target," "$DURATION_DB" 2>/dev/null; then
local last_connect=$(grep "^$target," "$DURATION_DB" | cut -d',' -f2)
local total_used=$(grep "^$target," "$DURATION_DB" | cut -d',' -f3)
if tail -n 5 "$CONNECTION_LOG" 2>/dev/null | grep -q "^[0-9]*,$target,connect$"; then
local session_duration=$((current_time - last_connect))
total_used=$((total_used + session_duration))
fi
echo "$total_used"
else
echo "0"
fi
}
# 重置检查
should_reset_duration() {
local target="$1"
local reset_cycle="$2"
local last_reset_file="$DURATION_DIR/last_reset_$target"
[ ! -f "$last_reset_file" ] && return 0
local last_reset=$(cat "$last_reset_file" 2>/dev/null)
local current_time=$(date +%s)
case "$reset_cycle" in
"daily")
local last_date=$(date -d "@$last_reset" +%Y%m%d 2>/dev/null || echo "0")
local current_date=$(date +%Y%m%d)
[ "$last_date" != "$current_date" ]
;;
"weekly")
local last_week=$(date -d "@$last_reset" +%Y%W 2>/dev/null || echo "0")
local current_week=$(date +%Y%W)
[ "$last_week" != "$current_week" ]
;;
"monthly")
local last_month=$(date -d "@$last_reset" +%Y%m 2>/dev/null || echo "0")
local current_month=$(date +%Y%m)
[ "$last_month" != "$current_month" ]
;;
*)
false
;;
esac
}
reset_duration_counter() {
local target="$1"
local reset_cycle="$2"
local current_time=$(date +%s)
if grep -q "^$target," "$DURATION_DB" 2>/dev/null; then
sed -i "/^$target,/d" "$DURATION_DB"
fi
echo "$target,$current_time,0,$current_time" >> "$DURATION_DB"
echo "$current_time" > "$DURATION_DIR/last_reset_$target"
info "重置时长: $target (周期: $reset_cycle)"
}
# 主检查函数
check_device_control() {
local id="$1"
local target=$(config_t_get device mac "$id")
local time_mode=$(config_t_get device time_mode "$id" "period")
local weekdays=$(config_t_get device week "$id" "0")
# 检查星期
is_weekday_in_range "$weekdays" || {
dbg "星期不允许: $target"
return 1
}
case "$time_mode" in
"period")
local start_time=$(config_t_get device timestart "$id" "00:00")
local end_time=$(config_t_get device timeend "$id" "00:00")
is_time_in_range "$start_time" "$end_time" || {
dbg "时间段外: $target ($start_time-$end_time)"
return 1
}
dbg "时间段内: $target"
return 0
;;
"duration")
local duration=$(config_t_get device duration "$id" "60")
local reset_cycle=$(config_t_get device reset_cycle "$id" "daily")
# 重置检查
if should_reset_duration "$target" "$reset_cycle"; then
reset_duration_counter "$target" "$reset_cycle"
fi
# 记录连接
local last_action=$(grep ",$target," "$CONNECTION_LOG" 2>/dev/null | tail -1 | cut -d',' -f3)
if [ "$last_action" != "connect" ]; then
record_connection_time "$target" "connect"
fi
# 检查时长
local used_seconds=$(get_connection_time "$target")
local used_minutes=$((used_seconds / 60))
local max_minutes=$duration
dbg "时长检查: $target 已用=${used_minutes}分钟, 限制=${max_minutes}分钟"
if [ "$used_minutes" -ge "$max_minutes" ]; then
dbg "已超时: $target"
return 1
fi
dbg "未超时: $target"
return 0
;;
"combined")
local start_time=$(config_t_get device timestart "$id" "00:00")
local end_time=$(config_t_get device timeend "$id" "00:00")
local use_duration=$(config_t_get device use_duration "$id" "0")
# 时间段检查
is_time_in_range "$start_time" "$end_time" || {
dbg "时间段外: $target"
return 1
}
# 时长检查
if [ "$use_duration" = "1" ]; then
local duration=$(config_t_get device duration "$id" "60")
local reset_cycle=$(config_t_get device reset_cycle "$id" "daily")
if should_reset_duration "$target" "$reset_cycle"; then
reset_duration_counter "$target" "$reset_cycle"
fi
local last_action=$(grep ",$target," "$CONNECTION_LOG" 2>/dev/null | tail -1 | cut -d',' -f3)
if [ "$last_action" != "connect" ]; then
record_connection_time "$target" "connect"
fi
local used_seconds=$(get_connection_time "$target")
local used_minutes=$((used_seconds / 60))
local max_minutes=$duration
if [ "$used_minutes" -ge "$max_minutes" ]; then
dbg "时间段内但已超时: $target"
return 1
fi
fi
dbg "时间段内允许: $target"
return 0
;;
*)
# 默认时间段控制
local start_time=$(config_t_get device timestart "$id" "00:00")
local end_time=$(config_t_get device timeend "$id" "00:00")
is_time_in_range "$start_time" "$end_time"
return $?
;;
esac
}
# 更新设备状态
update_device_status() {
local id="$1"
local should_allow="$2" # 0=允许, 1=禁止
local target=$(config_t_get device mac "$id")
local comment=$(config_t_get device comment "$id" "设备$id")
# 检查当前状态
local current_blocked=0
if [ -f "$IDLIST" ] && grep -q "!${id}!" "$IDLIST" 2>/dev/null; then
current_blocked=1
fi
dbg "设备状态: $target, 应该允许=$should_allow, 当前阻止=$current_blocked"
if [ "$should_allow" -eq 0 ]; then
# 应该允许
if [ "$current_blocked" -eq 1 ]; then
dbg "解除阻止: $target"
timecontrol del "$id"
sed -i "/!$id!/d" "$IDLIST" 2>/dev/null
info "允许上网: $comment ($target)"
record_connection_time "$target" "disconnect"
fi
else
# 应该阻止
if [ "$current_blocked" -eq 0 ]; then
dbg "添加阻止: $target"
timecontrol add "$id"
if ! grep -q "!$id!" "$IDLIST" 2>/dev/null; then
echo "!$id!" >> "$IDLIST"
fi
info "阻止上网: $comment ($target)"
record_connection_time "$target" "disconnect"
fi
fi
}
# 主处理循环
main_loop() {
info "时间控制守护进程启动"
init_dirs
while :; do
dbg "开始检查设备"
[ `uci show $NAME 2>/dev/null | grep "enable='1'" | grep "device" | grep -oE '\[.*?\]' | grep -o '[0-9]' | sort -n | wc -l` eq 0 ] && timecontrol stop && break
for id in $(get_enabled_devices); do
if check_device_control "$id"; then
update_device_status "$id" 0 # 允许
else
update_device_status "$id" 1 # 阻止
fi
done
sleep 60
done
}
# 启动
main_loop
@@ -1,32 +0,0 @@
#!/bin/sh
#
# Copyright (C) 2025 sirpdboy herboy2008@gmail.com https://github.com/sirpdboy/luci-app-timecontrol
#
logfile="/var/log/timecontrol.log"
lang=$(uci get luci.main.lang 2>/dev/null)
if [ -z "$lang" ] || [[ "$lang" == "auto" ]]; then
lang=$(echo "${LANG:-${LANGUAGE:-${LC_ALL:-${LC_MESSAGES:-zh_cn}}}}" | awk -F'[ .@]' '{print tolower($1)}' | sed 's/-/_/' 2>/dev/null)
fi
translate() {
# 处理特殊字符
local lua_script=$(cat <<LUA
require "luci.i18n".setlanguage("$lang")
print(require "luci.i18n".translate([==[$1]==]))
LUA
)
lua -e "$lua_script"
}
if [ "$1" == "clear_log" ]; then
# 清空日志
>"${logfile}"
elif [ "$1" == "child" ]; then
shift
command_name=$1
shift
"$command_name" "$@"
fi
@@ -1,32 +0,0 @@
{
"admin/control/timecontrol": {
"title": "Time Control",
"order": 10,
"action": {
"type": "firstchild"
},
"acl": [ "read" ],
"depends": {
"acl": [ "luci-app-timecontrol" ]
},
"recurse": true
},
"admin/control/timecontrol/basic": {
"title": "Time Control",
"order": 10,
"action": {
"type": "view",
"path": "timecontrol/basic"
},
"acl": [ "read" ]
},
"admin/control/timecontrol/log": {
"title": "Log",
"order": 40,
"action": {
"type": "view",
"path": "timecontrol/log"
},
"acl": [ "read" ]
}
}
@@ -1,21 +1,11 @@
{
"luci-app-timecontrol": {
"description": "Grant UCI Internet time control for luci-app-timecontrol",
"read": {
"ubus": {
"file": ["exec", "list", "stat", "read"],
"uci": [ "*" ],
"timecontrol": ["*"]
}
},
"write": {
"ubus": {
"timecontrol": ["*"],
"file": ["write"],
"uci": ["*"]
}
}
}
}
"luci-app-timecontrol": {
"description": "Grant UCI access for luci-app-timecontrol",
"read": {
"uci": [ "timecontrol" ]
},
"write": {
"uci": [ "timecontrol" ]
}
}
}
@@ -0,0 +1,4 @@
{
"config": "timecontrol",
"init": "timecontrol"
}
+1 -1
View File
@@ -10,7 +10,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=mptcp
PKG_VERSION:=6.1
PKG_RELEASE:=12
PKG_RELEASE:=13
PKG_MAINTAINER:=Ycarus (Yannick Chabanois) <ycarus@zugaina.org>
PKG_BUILD_DIR := $(BUILD_DIR)/$(PKG_NAME)
+20 -4
View File
@@ -5,18 +5,34 @@
/etc/init.d/mptcp enabled || exit 0
if [ "$ACTION" = ifupdate -a -n "$IFUPDATE_ADDRESSES" ] && [ -n "$(uci -q get network.$INTERFACE.multipath)" ] && [ "$(uci -q get network.$INTERFACE.multipath)" != "off" ]; then
# A DHCPv6 companion "<intf>_6" (wizard IPv6 SLAAC/DHCPv6 option on a DHCP
# WAN, #4329) shares its parent's device and is kept at multipath=off so it
# never counts as a WAN of its own (status page, MPTCP pages, trackers).
# Its events still matter for the shared device: a new SLAAC address must
# refresh the MPTCP endpoints, and its ifup must re-run the parent's IPv6
# routing (mptcp reload reads the gateway from "<intf>_6" status), so the
# multipath decision is keyed on the parent interface instead.
MP_INTERFACE="$INTERFACE"
case "$INTERFACE" in
*_6)
if [ "$(uci -q get network.$INTERFACE.proto)" = "dhcpv6" ] && [ -n "$(uci -q get network.${INTERFACE%_6})" ]; then
MP_INTERFACE="${INTERFACE%_6}"
fi
;;
esac
MULTIPATH="$(uci -q get network.$MP_INTERFACE.multipath)"
if [ "$ACTION" = ifupdate -a -n "$IFUPDATE_ADDRESSES" ] && [ -n "$MULTIPATH" ] && [ "$MULTIPATH" != "off" ]; then
logger -t "mptcp" "New IP ($IFUPDATE_ADDRESSES) for $INTERFACE ($DEVICE)"
multipath $DEVICE off 2>&1 >/dev/null || exit 0
multipath $DEVICE on 2>&1 >/dev/null || exit 0
elif [ "$ACTION" = ifupdate ] && [ -n "$(uci -q get network.$INTERFACE.multipath)" ] && [ "$(uci -q get network.$INTERFACE.multipath)" != "off" ]; then
elif [ "$ACTION" = ifupdate ] && [ -n "$MULTIPATH" ] && [ "$MULTIPATH" != "off" ]; then
logger -t "mptcp" "Update of $INTERFACE ($DEVICE)"
multipath $DEVICE off 2>&1 >/dev/null || exit 0
multipath $DEVICE on 2>&1 >/dev/null || exit 0
elif [ "$ACTION" = ifup -o "$ACTION" = iflink -o "$ACTION" = link-up ] && [ -z "$(echo $DEVICE | grep oip | grep gre)" ] && [ -n "$(uci -q get network.$INTERFACE.multipath)" ] && [ "$(uci -q get network.$INTERFACE.multipath)" != "off" ]; then
elif [ "$ACTION" = ifup -o "$ACTION" = iflink -o "$ACTION" = link-up ] && [ -z "$(echo $DEVICE | grep oip | grep gre)" ] && [ -n "$MULTIPATH" ] && [ "$MULTIPATH" != "off" ]; then
logger -t "mptcp" "Reloading mptcp config due to $ACTION of $INTERFACE ($DEVICE)"
/etc/init.d/mptcp reload "$DEVICE" >/dev/null || exit 0
elif [ "$ACTION" = ifdown -o "$ACTION" = link-down ]; then
multipath $DEVICE off 2>&1 >/dev/null || exit 0
fi
+23 -2
View File
@@ -190,6 +190,27 @@ interface_max_metric() {
esac
}
# sqm-scripts (cake / htb+fq_codel / hfsc set up by /usr/lib/sqm) and
# qos-scripts own the root qdisc of the devices they shape. Replacing it with
# fq below silently deleted the egress shaper on every "mptcp reload <device>"
# (tracker status change, IP change, ifup hotplug) while SQM's state file still
# claimed it was running, so SQM never re-applied it until its next restart:
# LuCI and the wizard showed SQM enabled, tc showed plain fq (#4329).
# Leave the root qdisc alone whenever another shaper manages this device.
_root_qdisc_managed_elsewhere() {
local dev="$1" config="$2" state_dir sec
[ -n "$dev" ] || return 1
state_dir="$(. /etc/sqm/sqm.conf 2>/dev/null; echo "${SQM_STATE_DIR:-/var/run/sqm}")"
[ -f "${state_dir}/${dev}.state" ] && return 0
# an enabled sqm queue on this device: named after the interface by the
# wizard, or an anonymous section created from the LuCI SQM page
for sec in $(uci -q show sqm 2>/dev/null | sed -n "s/^sqm\.\([^.]*\)\.interface='${dev}'\$/\1/p"); do
[ "$(uci -q get sqm.${sec}.enabled)" = "1" ] && return 0
done
[ "$(uci -q get qos.${config}.enabled)" = "1" ] && return 0
return 1
}
interface_multipath_settings() {
local mode iface proto metric ip4table qdisc
local config="$1"
@@ -433,7 +454,7 @@ interface_multipath_settings() {
#ifconfig $iface txqueuelen 1000 > /dev/null 2>&1
ip link set dev $iface txqueuelen 1000 > /dev/null 2>&1
fi
tc qdisc replace dev $iface root ${qdisc:-fq} > /dev/null 2>&1
_root_qdisc_managed_elsewhere "$iface" "$config" || tc qdisc replace dev $iface root ${qdisc:-fq} > /dev/null 2>&1
fi
if [ -z "$gateway" ] && [ -n "$network" ]; then
if [ "$uci_route" = "1" ]; then
@@ -469,7 +490,7 @@ interface_multipath_settings() {
#ifconfig $iface txqueuelen 1000 > /dev/null 2>&1
ip link set dev $iface txqueuelen 1000 > /dev/null 2>&1
fi
tc qdisc replace dev $iface root ${qdisc:-fq} > /dev/null 2>&1
_root_qdisc_managed_elsewhere "$iface" "$config" || tc qdisc replace dev $iface root ${qdisc:-fq} > /dev/null 2>&1
fi
if [ "$(uci -q get openmptcprouter.settings.disable_ipv6)" != "1" ] && [ "$config" != "omr6in4" ]; then
# IPv6 Updates: