🐤 Sync 2026-10-03 00:12:36

This commit is contained in:
github-actions[bot] committed 2026-10-03 00:12:36 +08:00
1 parent ce3df55b24
commit cb1d978497
58 files changed
+1627 -1018

No files matched your search

+2 -2
View File
@@ -9,7 +9,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-linkback
PKG_VERSION:=1.0.0
PKG_RELEASE:=1
PKG_RELEASE:=2
PKG_MAINTAINER:=Dante
LUCI_TITLE:=LuCI support for Multi-WAN LinkBack Failover/Failback
@@ -17,7 +17,7 @@ LUCI_DEPENDS:=+luci-base +ucode +ucode-mod-fs +ucode-mod-uci +ucode-mod-ubus +li
LUCI_PKGARCH:=all
define Package/$(PKG_NAME)/postinst
#!/sh
#!/bin/sh
[ -n "$${IPKG_INSTROOT}" ] || {
/etc/init.d/rpcd restart
}
@@ -49,10 +49,10 @@ return view.extend({
var container = E('div', { 'class': 'cbi-map' }, [
E('h2', {}, _('LinkBack 链路守护') + ' - ' + _('Overview')),
E('div', { 'class': 'cbi-map-descr' }, _('Real-time health state, priority mapping, and service control for Multi-WAN default routing metrics.'))
E('div', { 'class': 'cbi-map-descr' }, _('Real-time health state, priority mapping, and service control for Multi-WAN / Multi-Gateway failover.'))
]);
// --- Service Status Section (Aligned with lucky style) ---
// --- Service Status Section ---
var stateText = _('Collecting...');
var stateColor = '#999';
@@ -84,15 +84,19 @@ return view.extend({
click: ui.createHandlerFn(this, 'handleServiceAction', 'restart')
}, _('Restart'))
])
]),
E('tr', { 'class': 'tr' }, [
E('td', { 'class': 'td left', 'width': '33%' }, _('Working Mode')),
E('td', { 'class': 'td left', 'id': 'linkback-service-mode' }, '-')
])
])
])
]);
container.appendChild(statusSection);
// --- Link Status Section (Aligned with lucky style) ---
// --- Link Status Section ---
var linkSection = E('fieldset', { 'class': 'cbi-section' }, [
E('legend', {}, _('Link Status')),
E('legend', {}, _('Link / Gateway Status')),
E('div', { 'class': 'cbi-section-node' }, [
E('div', { id: 'linkback-status-box' }, [
E('p', { 'class': 'spinning' }, _('Loading real-time link status...'))
@@ -131,6 +135,16 @@ return view.extend({
}
}
// Update mode indicator
var modeEl = document.getElementById('linkback-service-mode');
if (modeEl && status) {
if (status.mode === 'multi_gw') {
modeEl.textContent = _('单wan多网关模式') + ' (' + _('wan网络接口') + ': ' + (status.interface || 'wan') + ')';
} else {
modeEl.textContent = _('多wan口模式');
}
}
var restartBtn = document.getElementById('linkback-restart-btn');
if (restartBtn) {
if (status && status.links && status.links.length > 0) {
@@ -143,7 +157,7 @@ return view.extend({
if (!status || !status.links || status.links.length === 0) {
dom.content(box, [
E('p', { style: 'color:#f44336; font-weight:bold; padding:20px;' },
_('LinkBack daemon is not running or no links are configured. Please check Settings page.'))
_('LinkBack daemon is not running or no targets are configured. Please check Settings page.'))
]);
return;
}
@@ -154,7 +168,7 @@ return view.extend({
var table = E('table', { 'class': 'table cbi-section-table' }, [
E('tr', { 'class': 'tr table-titles' }, [
E('th', { 'class': 'th', 'style': 'text-align:center; width:5%;' }, '#'),
E('th', { 'class': 'th', 'style': 'text-align:left;' }, _('Link Name')),
E('th', { 'class': 'th', 'style': 'text-align:left;' }, _('Target / Interface')),
E('th', { 'class': 'th', 'style': 'text-align:left;' }, _('Status')),
E('th', { 'class': 'th', 'style': 'text-align:left;' }, _('Routing Attributes')),
E('th', { 'class': 'th', 'style': 'text-align:left;' }, _('Latency Details'))
@@ -8,42 +8,70 @@ return view.extend({
load: function() {
return Promise.all([
uci.load('linkback'),
uci.load('network')
uci.load('network'),
uci.load('firewall').catch(function() { return {}; })
]);
},
// Comprehensive validation: check all conditions required for the service
// to be safely enabled. Returns an error message string, or null if OK.
_validateServiceConfig: function() {
var global_sec = uci.sections('linkback', 'global')[0] || {};
var mode = global_sec.mode || 'multi_wan';
var link_sections = uci.sections('linkback', 'link') || [];
// Rule 1: At least 2 interfaces
// Rule 1: At least 2 targets
if (link_sections.length < 2) {
return _('Cannot enable service: At least 2 monitored WAN interfaces must be configured for failover switcher.');
return _('Cannot enable service: At least 2 monitored links/gateways must be configured for failover switcher.');
}
// Rule 2: ALL interfaces must have a health check configured
if (mode === 'multi_gw') {
var iface = global_sec.interface || 'wan';
if (!iface) {
return _('Cannot enable service: WAN interface must be configured in Multi-Gateway mode.');
}
}
// Rule 2: ALL targets must have a health check configured
var priorities = {};
var gateways = {};
var names = {};
for (var i = 0; i < link_sections.length; i++) {
var s_id = link_sections[i]['.name'];
var iface_name = uci.get('linkback', s_id, 'name') || s_id;
var target_name = uci.get('linkback', s_id, 'name') || s_id;
var gw = uci.get('linkback', s_id, 'gateway');
var prio = uci.get('linkback', s_id, 'priority') || '1';
var has_check = uci.get('linkback', s_id, 'ping_targets') ||
uci.get('linkback', s_id, 'dns_server') ||
uci.get('linkback', s_id, 'tcp_target');
if (!has_check) {
return _('Cannot enable service: Interface "%s" has no health check configured.').format(iface_name);
return _('Cannot enable service: Target "%s" has no health check configured.').format(target_name);
}
}
// Rule 3: No duplicate priorities
var priorities = {};
for (var i = 0; i < link_sections.length; i++) {
var s_id = link_sections[i]['.name'];
var iface_name = uci.get('linkback', s_id, 'name') || s_id;
var prio = uci.get('linkback', s_id, 'priority') || '1';
// Priority uniqueness
if (priorities[prio]) {
return _('Cannot enable service: Interfaces "%s" and "%s" have the same priority %s.').format(priorities[prio], iface_name, prio);
return _('Cannot enable service: Targets "%s" and "%s" have the same priority %s.').format(priorities[prio], target_name, prio);
}
priorities[prio] = target_name;
// Mode specific checks
if (mode === 'multi_gw') {
var ipPattern = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
if (!gw || !ipPattern.test(gw)) {
return _('Cannot enable service: Target "%s" has an invalid or missing Gateway IP.').format(target_name);
}
if (gateways[gw]) {
return _('Cannot enable service: Targets "%s" and "%s" have the same Gateway IP %s.').format(gateways[gw], target_name, gw);
}
gateways[gw] = target_name;
} else {
if (names[target_name]) {
return _('Cannot enable service: Interface "%s" is configured multiple times.').format(target_name);
}
names[target_name] = true;
}
priorities[prio] = iface_name;
}
return null;
@@ -53,11 +81,48 @@ return view.extend({
var m, s, o;
var self = this;
var getActiveMode = function() {
var global_sec = uci.sections('linkback', 'global')[0] || {};
var sid = global_sec['.name'] || '@global[0]';
var sel = document.querySelector('[name="cbid.linkback.' + sid + '.mode"]') ||
document.querySelector('select[id*="mode"], select[name*="mode"]');
if (sel && sel.value) {
return sel.value;
}
return uci.get('linkback', sid, 'mode') || 'multi_wan';
};
var current_mode = getActiveMode();
// 智能提取 firewall 中 wan 区域关联的所有网络接口
var wan_interfaces = {};
var has_wan_zone = false;
uci.sections('firewall', 'zone').forEach(function(sec) {
if (sec.name === 'wan') {
has_wan_zone = true;
var nets = sec.network;
if (Array.isArray(nets)) {
nets.forEach(function(n) { if (n) wan_interfaces[n] = true; });
} else if (typeof(nets) === 'string') {
nets.split(/\s+/).forEach(function(n) { if (n) wan_interfaces[n] = true; });
}
}
});
// 若未配置 wan 区域,则安全回退至系统非回环非 lan 接口
if (!has_wan_zone || Object.keys(wan_interfaces).length === 0) {
uci.sections('network', 'interface').forEach(function(sec) {
var n = sec['.name'];
if (n !== 'loopback' && n !== 'lan') {
wan_interfaces[n] = true;
}
});
}
// 智能判断当前是否为中文环境,并提供实时校验的翻译 fallback
var is_zh = (_('Base Metric') === '默认跃点' || _('Base Metric') === '默认跃点 (Metric)');
var t_priority_empty = is_zh ? '优先级不能为空。' : _('Priority must not be empty.');
var t_priority_conflict = is_zh ? '优先级 %s 与接口 "%s" 冲突。优先级必须是唯一的。' : _('Priority %s conflicts with interface "%s". Priorities must be unique.');
var t_priority_conflict = is_zh ? '优先级 %s 与其他目标冲突。优先级必须是唯一的。' : _('Priority %s conflicts with another target. Priorities must be unique.');
// Helper function to expand table column controls and eliminate right empty space
var makeTableColumnExpand = function(opt, width) {
@@ -81,17 +146,16 @@ return view.extend({
m = new form.Map('linkback',
_('LinkBack 链路守护') + ' - ' + _('Settings'),
_('Configure Multi-WAN failover service, health check parameters, and monitored link interfaces.'));
_('Configure Multi-WAN / Multi-Gateway failover service, shared health check parameters, and monitored targets.'));
// --- Global Settings Section ---
s = m.section(form.TypedSection, 'global', _('Global Settings'));
s.anonymous = true;
// Enable switch (总开关) - must be the first option
// 1. Enable switch
o = s.option(form.Flag, 'enabled', _('Enable Service'),
_('Master switch to enable or disable the LinkBack failover daemon.'));
o.rmempty = false;
// Intercept at UCI write level to validate before enabling.
o.write = function(section_id, value) {
if (value === '1') {
var err = self._validateServiceConfig();
@@ -104,27 +168,94 @@ return view.extend({
uci.set('linkback', section_id, 'enabled', value);
};
// --- Monitored Links Section ---
s = m.section(form.GridSection, 'link', _('Monitored WAN Interfaces'),
_('Add and prioritize your WAN interfaces. Lower priority number means higher preference (e.g., 1 = primary, 2 = backup).'));
// 2. Working Mode
o = s.option(form.ListValue, 'mode', _('Working Mode'),
_('Choose failover mode: Multi-WAN interface failover or Single-WAN multi-gateway redundancy.'));
o.value('multi_wan', _('多wan口模式'));
o.value('multi_gw', _('单wan多网关模式'));
o.default = 'multi_wan';
o.rmempty = false;
o.onchange = function(ev, section_id, value) {
uci.set('linkback', section_id, 'mode', value);
var links = uci.sections('linkback', 'link') || [];
if (links.length > 0) {
if (value === 'multi_gw') {
ui.addNotification(null, E('p',
_('已切换为单wan多网关模式。请确保监控目标列表中配置了有效的网关 IP。')
), 'info');
} else {
ui.addNotification(null, E('p',
_('已切换为多wan口模式。请确保监控目标列表中选择了正确的 WAN 接口。')
), 'info');
}
}
};
// 3. WAN Interface in Multi-Gateway mode
o = s.option(form.ListValue, 'interface', _('wan网络接口'),
_('选择存在多个wan网关的接口'));
o.default = 'wan';
o.rmempty = false;
o.depends('mode', 'multi_gw');
Object.keys(wan_interfaces).forEach(function(iface) {
o.value(iface);
});
// 4. Global Shared Health Check & Failover Parameters (共用调度超时)
o = s.option(form.Value, 'check_interval', _('Check Interval (s)'),
_('Shared interval in seconds between health check cycles for all targets.'));
o.datatype = 'uinteger';
o.default = '5';
o.rmempty = false;
o = s.option(form.Value, 'check_timeout', _('Check Timeout (s)'),
_('Shared maximum wait time in seconds for each probe (1s recommended to prevent blocking).'));
o.datatype = 'uinteger';
o.default = '1';
o.rmempty = false;
o = s.option(form.Value, 'recovery_delay', _('Recovery Delay'),
_('Number of consecutive successful checks required before marking link as recovered (failback anti-flap).'));
o.datatype = 'uinteger';
o.default = '3';
o.rmempty = false;
o = s.option(form.Value, 'failover_delay', _('Failover Delay'),
_('Number of consecutive failed checks required before marking link as faulted (failover anti-flap).'));
o.datatype = 'uinteger';
o.default = '2';
o.rmempty = false;
// --- Monitored Targets Section ---
s = m.section(form.GridSection, 'link', _('Monitored Targets'),
_('Configure monitored WAN interfaces or next-hop gateways with custom probe targets and priorities (1 = primary, 2 = backup).'));
s.anonymous = true;
s.addremove = true;
// Custom dynamic Modal title for gorgeous UX
// Custom dynamic Modal title
s.modaltitle = function(section_id) {
var parent_title = _('LinkBack 链路守护') + ' - ' + _('Settings');
var is_new = (this.map.addedSection === section_id) || !uci.get('linkback', section_id, 'name');
if (is_new) {
return parent_title + ' - ' + _('Add Monitored Interface');
var mode = getActiveMode();
var is_new = (this.map.addedSection === section_id) ||
(mode === 'multi_gw' ? !uci.get('linkback', section_id, 'gateway') : !uci.get('linkback', section_id, 'name'));
if (mode === 'multi_gw') {
if (is_new) {
return parent_title + ' - ' + _('Add Monitored Gateway');
} else {
var gw = uci.get('linkback', section_id, 'gateway') || uci.get('linkback', section_id, 'name') || section_id;
return parent_title + ' - ' + _('Edit Monitored Gateway') + ' (' + gw + ')';
}
} else {
var name = uci.get('linkback', section_id, 'name') || section_id;
return parent_title + ' - ' + _('Edit Monitored Interface') + ' (' + name + ')';
if (is_new) {
return parent_title + ' - ' + _('Add Monitored WAN Interface');
} else {
var iface = uci.get('linkback', section_id, 'name') || section_id;
return parent_title + ' - ' + _('Edit Monitored WAN Interface') + ' (' + iface + ')';
}
}
};
// When service is already enabled and user modifies/deletes links,
// re-validate on the GridSection level. If the resulting config is invalid,
// show a warning and auto-disable the service to maintain safety.
var origRemove = s.handleRemove;
s.handleRemove = function(section_id, ev) {
return origRemove.apply(this, arguments).then(function() {
@@ -141,78 +272,147 @@ return view.extend({
});
};
// 1. Enabled (Enabled as the first column)
// 1. Enabled
o = s.option(form.Flag, 'enabled', _('Enabled'));
o.default = '1';
o.rmempty = false;
makeTableColumnExpand(o, '8%');
// 2. Interface name - dynamic dropdown (No description for main column)
o = s.option(form.ListValue, 'name', _('Interface'));
o.rmempty = false;
// Once-off load of all network interfaces safely avoiding race conditions
var network_interfaces = {};
uci.sections('network', 'interface').forEach(function(sec) {
var n = sec['.name'];
if (n !== 'loopback' && n !== 'lan') {
network_interfaces[n] = true;
o.value(n);
}
});
// Safeguard to show already configured interfaces even if they were deleted from network config
uci.sections('linkback', 'link').forEach(function(sec) {
if (sec.name && !network_interfaces[sec.name]) {
o.value(sec.name, _('%s (configured)').format(sec.name));
}
});
// 动态隐藏已被配置的接口:通过在 renderWidget 中进行局部变量过滤与实例化 ui.Select,
// 完全不修改全局的 this.keylist 和 this.keyvalues,保证 100% 线程安全和无副作用
o.renderWidget = function(section_id, option_index, cfgvalue) {
// 找出已经被其他 Section 选中的网卡(已配置接口名)
var used_names = {};
uci.sections('linkback', 'link').forEach(function(sec) {
if (sec['.name'] !== section_id && sec.name) {
used_names[sec.name] = true;
// 2. Monitored Target Display (Table only)
o = s.option(form.DummyValue, '_target_disp', _('Monitored Target'));
o.modalonly = false;
o.cfgvalue = function(section_id) {
var gw = uci.get('linkback', section_id, 'gateway');
var name = uci.get('linkback', section_id, 'name');
var mode = getActiveMode();
if (mode === 'multi_gw' || gw) {
if (gw) {
if (name && name !== gw) {
return gw + ' (' + name + ')';
}
return gw;
}
});
return name ? (_('Missing Gateway IP: %s').format(name)) : _('Unset');
} else {
return name || _('Unselected');
}
};
makeTableColumnExpand(o, '24%');
// 动态生成仅针对当前渲染 section_id 的 choices 和 keylist
var filtered_choices = {};
var filtered_keylist = [];
if (Array.isArray(this.keylist)) {
for (var i = 0; i < this.keylist.length; i++) {
var key = this.keylist[i];
if (!used_names[key]) {
filtered_keylist.push(key);
filtered_choices[key] = this.vallist[i];
// 3a. Gateway IP (Multi-GW mode only, Modal only)
var o_gw = s.option(form.Value, 'gateway', _('Gateway IP'),
_('Next-hop IPv4 address of this gateway (e.g. 192.168.1.254).'));
o_gw.datatype = 'ip4addr';
o_gw.modalonly = true;
o_gw.placeholder = '192.168.1.254';
var origRenderGw = o_gw.render;
o_gw.render = function(option_index, section_id, in_table) {
if (getActiveMode() !== 'multi_gw') {
return Promise.resolve(E('div', { 'style': 'display: none !important;' }));
}
return origRenderGw.call(this, option_index, section_id, in_table);
};
o_gw.validate = function(section_id, value) {
if (getActiveMode() !== 'multi_gw') {
return true;
}
if (!value || value.trim() === '') {
return _('Gateway IP is required.');
}
var val = value.trim();
var self_opt = this;
var conflict = false;
uci.sections('linkback', 'link').forEach(function(sec) {
var sid = sec['.name'];
if (sid !== section_id) {
var other_gw = self_opt.formvalue(sid) || uci.get('linkback', sid, 'gateway');
if (other_gw && other_gw === val) {
conflict = true;
}
}
}
// 判断当前是编辑已有配置还是添加新配置
// 如果在 UCI 中已经保存了 name,则是编辑状态,此时网卡不可编辑
var is_edit = !!uci.get('linkback', section_id, 'name');
// 直接在局部变量中实例化 ui.Select,避免任何时序与引用问题
var widget = new ui.Select((cfgvalue != null) ? cfgvalue : this.default, filtered_choices, {
id: this.cbid(section_id),
size: this.size,
sort: filtered_keylist,
widget: this.widget,
optional: this.optional,
orientation: this.orientation,
placeholder: this.placeholder,
validate: (typeof(this.getValidator) === 'function') ? this.getValidator(section_id) : (this.validate ? this.validate.bind(this, section_id) : null),
disabled: is_edit ? true : ((this.readonly != null) ? this.readonly : this.map.readonly)
});
return widget.render();
if (conflict) {
return _('Gateway IP %s is already used by another link.').format(val);
}
return true;
};
o_gw.write = function(section_id, value) {
if (getActiveMode() === 'multi_gw') {
if (value != null && value.trim() !== '') {
uci.set('linkback', section_id, 'gateway', value.trim());
} else {
uci.remove('linkback', section_id, 'gateway');
}
} else {
uci.remove('linkback', section_id, 'gateway');
}
};
o.validate = function(section_id, value) {
// 3b. Gateway Alias (Multi-GW mode only, Modal only)
var o_alias = s.option(form.Value, 'name_alias', _('Gateway Alias (Optional)'),
_('Descriptive alias for this gateway (e.g. Bypass_GW, Main_Router). If empty, Gateway IP will be used.'));
o_alias.modalonly = true;
o_alias.placeholder = 'Bypass_GW';
var origRenderAlias = o_alias.render;
o_alias.render = function(option_index, section_id, in_table) {
if (getActiveMode() !== 'multi_gw') {
return Promise.resolve(E('div', { 'style': 'display: none !important;' }));
}
return origRenderAlias.call(this, option_index, section_id, in_table);
};
o_alias.cfgvalue = function(section_id) {
if (getActiveMode() === 'multi_gw') {
var gw = uci.get('linkback', section_id, 'gateway');
var name = uci.get('linkback', section_id, 'name');
if (name && name !== gw) {
return name;
}
}
return '';
};
o_alias.write = function(section_id, value) {
if (getActiveMode() === 'multi_gw') {
var gw = uci.get('linkback', section_id, 'gateway');
if (!value || value.trim() === '') {
uci.set('linkback', section_id, 'name', gw || section_id);
} else {
uci.set('linkback', section_id, 'name', value.trim());
}
}
};
// 3c. WAN Interface (Multi-WAN mode only, Modal only)
var o_iface = s.option(form.ListValue, 'name_iface', _('WAN Interface'),
_('Logical interface from firewall WAN zone.'));
o_iface.modalonly = true;
Object.keys(wan_interfaces).forEach(function(iface) {
o_iface.value(iface);
});
uci.sections('linkback', 'link').forEach(function(sec) {
if (sec.name && !wan_interfaces[sec.name]) {
o_iface.value(sec.name, _('%s (configured)').format(sec.name));
}
});
var origRenderIface = o_iface.render;
o_iface.render = function(option_index, section_id, in_table) {
if (getActiveMode() !== 'multi_wan') {
return Promise.resolve(E('div', { 'style': 'display: none !important;' }));
}
return origRenderIface.call(this, option_index, section_id, in_table);
};
o_iface.cfgvalue = function(section_id) {
if (getActiveMode() === 'multi_wan') {
return uci.get('linkback', section_id, 'name');
}
return null;
};
o_iface.validate = function(section_id, value) {
if (getActiveMode() !== 'multi_wan') {
return true;
}
if (!value) {
return _('WAN Interface is required.');
}
var added = false;
uci.sections('linkback', 'link').forEach(function(sec) {
if (sec['.name'] !== section_id && sec.name === value) {
@@ -224,11 +424,16 @@ return view.extend({
}
return true;
};
o_iface.write = function(section_id, value) {
if (getActiveMode() === 'multi_wan') {
uci.set('linkback', section_id, 'name', value);
uci.remove('linkback', section_id, 'gateway');
}
};
makeTableColumnExpand(o, '25%');
// 3. Priority (No description for main column)
o = s.option(form.Value, 'priority', _('Priority'));
// 3. Priority
o = s.option(form.Value, 'priority', _('Priority'),
_('Lower number indicates higher priority (e.g. 1 = Primary, 2 = Backup).'));
o.datatype = 'uinteger';
o.default = '1';
o.rmempty = false;
@@ -238,7 +443,6 @@ return view.extend({
}
var self_opt = this;
var has_conflict = false;
var conflicting_iface = '';
uci.sections('linkback', 'link').forEach(function(sec) {
var sid = sec['.name'];
if (sid !== section_id) {
@@ -248,16 +452,15 @@ return view.extend({
}
if (other_val != null && other_val !== '' && String(other_val) === String(value)) {
has_conflict = true;
conflicting_iface = uci.get('linkback', sid, 'name') || sid;
}
}
});
if (has_conflict) {
return t_priority_conflict.format(value, conflicting_iface);
return t_priority_conflict.format(value);
}
return true;
};
makeTableColumnExpand(o, '15%');
makeTableColumnExpand(o, '12%');
// 4. Metric (Read-only, generated from priority * 10)
var metric_title = _('Base Metric');
@@ -273,7 +476,7 @@ return view.extend({
}
return prio_val * 10;
};
makeTableColumnExpand(o, '15%');
makeTableColumnExpand(o, '12%');
// 5. Dummy display option for Check Type in main Grid table (read-only)
o = s.option(form.DummyValue, 'check_type_disp', _('Check Type'));
@@ -287,10 +490,11 @@ return view.extend({
return _('TCP Probe');
return _('-- Not Configured --');
};
makeTableColumnExpand(o, '20%');
makeTableColumnExpand(o, '18%');
// 6. Check Type Dropdown (Virtual field, Modal only to avoid Save & Apply side-effects)
o = s.option(form.ListValue, 'check_type', _('Check Type'));
// 6. Check Type Dropdown (Virtual field, Modal only)
o = s.option(form.ListValue, 'check_type', _('Check Type'),
_('Health probe method for this specific target.'));
o.value('ping', _('Ping Probe'));
o.value('dns', _('DNS Probe'));
o.value('tcp', _('TCP Probe'));
@@ -298,7 +502,6 @@ return view.extend({
o.rmempty = false;
o.modalonly = true;
// Derive the check type from which probe target is configured in UCI.
o.cfgvalue = function(section_id) {
if (uci.get('linkback', section_id, 'ping_targets'))
return 'ping';
@@ -306,22 +509,19 @@ return view.extend({
return 'dns';
if (uci.get('linkback', section_id, 'tcp_target'))
return 'tcp';
return 'ping'; // Default to ping if not configured
return 'ping';
};
// Only write when the check type actually CHANGES from the current state.
o.write = function(section_id, value) {
var current = uci.get('linkback', section_id, 'ping_targets') ? 'ping' :
(uci.get('linkback', section_id, 'dns_server') ? 'dns' :
(uci.get('linkback', section_id, 'tcp_target') ? 'tcp' : ''));
var next = (value == null) ? 'ping' : String(value);
// No change - do nothing
if (next === current) {
return;
}
// Clear old type's fields and any legacy weights/thresholds
uci.remove('linkback', section_id, 'weight_threshold');
uci.remove('linkback', section_id, 'ping_weight');
uci.remove('linkback', section_id, 'dns_weight');
@@ -340,14 +540,15 @@ return view.extend({
}
};
// 7. Ping Probe Parameters
// 7. Custom Ping Probe Parameters
o = s.option(form.Value, 'ping_targets', _('Ping Targets'),
_('Comma-separated list of IPs to ping (e.g., 223.5.5.5,8.8.8.8).'));
o.rmempty = true;
_('Custom IP list to ping through this link/gateway (comma-separated, e.g. 223.5.5.5,119.29.29.29).'));
o.default = '223.5.5.5,119.29.29.29';
o.rmempty = false;
o.modalonly = true;
o.depends('check_type', 'ping');
o.validate = function(section_id, value) {
if (!value) return true;
if (!value) return _('Ping Targets is required.');
var ips = value.replace(/\s+/g, '').split(',');
for (var i = 0; i < ips.length; i++) {
var ipPattern = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
@@ -366,64 +567,38 @@ return view.extend({
}
};
// 7. DNS Probe Parameters
// 8. Custom DNS Probe Parameters
o = s.option(form.Value, 'dns_server', _('DNS Server'),
_('DNS server IP for UDP query probe (e.g., 119.29.29.29).'));
_('DNS server IP for UDP query probe (e.g. 119.29.29.29).'));
o.datatype = 'ip4addr';
o.rmempty = true;
o.rmempty = false;
o.modalonly = true;
o.depends('check_type', 'dns');
o = s.option(form.Value, 'dns_domain', _('DNS Domain'),
_('Domain name to resolve for DNS probe (e.g., www.baidu.com).'));
o.rmempty = true;
_('Domain name to resolve for DNS probe (e.g. www.baidu.com).'));
o.default = 'www.baidu.com';
o.rmempty = false;
o.modalonly = true;
o.depends('check_type', 'dns');
// 8. TCP Probe Parameters
// 9. Custom TCP Probe Parameters
o = s.option(form.Value, 'tcp_target', _('TCP Target'),
_('Target IP for TCP handshake probe.'));
o.datatype = 'ip4addr';
o.rmempty = true;
o.rmempty = false;
o.modalonly = true;
o.depends('check_type', 'tcp');
o = s.option(form.Value, 'tcp_port', _('TCP Port'),
_('Target port for TCP handshake probe.'));
_('Target port for TCP handshake probe (e.g. 80 or 443).'));
o.datatype = 'port';
o.rmempty = true;
o.default = '80';
o.rmempty = false;
o.modalonly = true;
o.depends('check_type', 'tcp');
// 9. Individual health check options (Modal only)
o = s.option(form.Value, 'check_interval', _('Check Interval (s)'),
_('Time in seconds between each health check cycle for this link.'));
o.datatype = 'uinteger';
o.default = '5';
o.rmempty = false;
o.modalonly = true;
o = s.option(form.Value, 'check_timeout', _('Check Timeout (s)'),
_('Maximum wait time in seconds for each individual check probe for this link.'));
o.datatype = 'uinteger';
o.default = '3';
o.rmempty = false;
o.modalonly = true;
o = s.option(form.Value, 'recovery_delay', _('Recovery Delay'),
_('Number of consecutive successful checks required before marking this link as healthy (failback anti-flap).'));
o.datatype = 'uinteger';
o.default = '3';
o.rmempty = false;
o.modalonly = true;
o = s.option(form.Value, 'failover_delay', _('Failover Delay'),
_('Number of consecutive failed checks required before marking this link as faulted (failover anti-flap).'));
o.datatype = 'uinteger';
o.default = '2';
o.rmempty = false;
o.modalonly = true;
// 提示:超时与防抖延迟由全局配置统一管理,子项完全无需重复配置
return m.render();
}
});
+158 -126
View File
@@ -1,5 +1,5 @@
msgid ""
msgstr "Content-Type: text/plain; charset=UTF-8"
msgstr "Content-Type: text/plain; charset=UTF-8\n"
msgid "LinkBack 链路守护"
msgstr "网关切换"
@@ -16,6 +16,12 @@ msgstr "日志"
msgid "Real-time health state, priority mapping, and service control for Multi-WAN default routing metrics."
msgstr "用于 Multi-WAN 默认路由 Metric 的实时健康状态、优先级映射及服务控制。"
msgid "Real-time health state, priority mapping, and service control for Multi-WAN / Multi-Gateway failover."
msgstr "用于多 WAN / 多网关容灾切换的实时健康状态、优先级映射与服务控制。"
msgid "Configure Multi-WAN / Multi-Gateway failover service, shared health check parameters, and monitored targets."
msgstr "配置多 WAN / 多网关容灾切换服务、全局共用健康检测参数与监控目标。"
msgid "Service Status"
msgstr "服务状态"
@@ -37,6 +43,9 @@ msgstr "停止"
msgid "Link Status"
msgstr "链路状态"
msgid "Link / Gateway Status"
msgstr "链路 / 网关状态"
msgid "Loading real-time link status..."
msgstr "正在加载实时链路状态..."
@@ -49,18 +58,93 @@ msgstr "已停止"
msgid "LinkBack daemon is not running or no links are configured. Please check Settings page."
msgstr "网关切换守护进程未运行或未配置任何链路。请检查设置页面。"
msgid "主选链路 / Active"
msgstr "主选网关 / 活跃中"
msgid "LinkBack daemon is not running or no targets are configured. Please check Settings page."
msgstr "网关切换守护进程未运行或未配置任何监控目标。请检查设置页面。"
msgid "次选链路 / Standby"
msgstr "备选网关 / 待命"
msgid "Working Mode"
msgstr "工作模式"
msgid "备选链路 / Faulted"
msgstr "故障网关 / 已断开"
msgid "多wan口模式"
msgstr "多wan口模式"
msgid "单wan多网关模式"
msgstr "单wan多网关模式"
msgid "wan网络接口"
msgstr "wan网络接口"
msgid "选择存在多个wan网关的接口"
msgstr "选择存在多个wan网关的接口"
msgid "Choose failover mode: Multi-WAN interface failover or Single-WAN multi-gateway redundancy."
msgstr "选择容灾模式:多 WAN 接口链路容灾,或单 WAN 多网关主备容灾。"
msgid "Monitored Gateways"
msgstr "监控网关列表"
msgid "Monitored WAN Interfaces"
msgstr "监控 WAN 接口列表"
msgid "Add next-hop gateways with custom probe targets and priorities (1 = primary, 2 = backup)."
msgstr "添加下一跳网关并配置自定义探测目标与优先级 (1 = 主选,2 = 备选)。"
msgid "Add WAN interfaces from firewall zone with custom probe targets and priorities (1 = primary, 2 = backup)."
msgstr "添加来自防火墙区域的 WAN 接口并配置自定义探测目标与优先级 (1 = 主选,2 = 备选)。"
msgid "WAN Interface"
msgstr "WAN 接口"
msgid "Logical interface from firewall WAN zone."
msgstr "来自防火墙 WAN 区域的逻辑接口。"
msgid "Gateway Alias (Optional)"
msgstr "网关别名 (可选)"
msgid "Descriptive alias for this gateway (e.g. Bypass_GW, Main_Router). If empty, Gateway IP will be used."
msgstr "该网关的自定义别名 (例如:旁路由主网关、光猫备用网关)。若留空则自动使用网关 IP。"
msgid "Gateway IP"
msgstr "网关 IP"
msgid "Next-hop IPv4 address of this gateway (e.g. 192.168.1.254)."
msgstr "该网关的下一跳 IPv4 地址 (例如:192.168.1.254)。"
msgid "Gateway IP is required."
msgstr "网关 IP 不能为空。"
msgid "Gateway IP %s is already used by another link."
msgstr "网关 IP %s 已被其他目标使用,必须唯一。"
msgid "Add Monitored Gateway"
msgstr "添加监控网关"
msgid "Edit Monitored Gateway"
msgstr "编辑监控网关"
msgid "Add Monitored WAN Interface"
msgstr "添加监控 WAN 接口"
msgid "Edit Monitored WAN Interface"
msgstr "编辑监控 WAN 接口"
msgid "Target / Interface"
msgstr "监控目标 / 接口"
msgid "Running State"
msgstr "运行状态"
msgid "Routing Attributes"
msgstr "路由属性"
msgid "Latency Details"
msgstr "时延明细"
msgid "Priority"
msgstr "优先级"
msgid "Lower number indicates higher priority (e.g. 1 = Primary, 2 = Backup)."
msgstr "数值越小代表优先级越高 (例如:1 = 主选,2 = 备选)。"
msgid "Metric"
msgstr "路由跃点 (Metric)"
@@ -85,9 +169,6 @@ msgstr "服务操作执行失败。"
msgid "RPC error: %s"
msgstr "RPC 错误: %s"
msgid "Configure Multi-WAN failover service, health check parameters, and monitored link interfaces."
msgstr "配置多 WAN 故障切换服务、健康度检查参数及被监控的网关接口。"
msgid "Global Settings"
msgstr "全局设置"
@@ -100,39 +181,30 @@ msgstr "启用或禁用网关切换服务的全局总开关。"
msgid "Check Interval (s)"
msgstr "检测间隔 (秒)"
msgid "Time in seconds between each health check cycle."
msgstr "每个健康度检测周期之间的时间间隔。"
msgid "Shared interval in seconds between health check cycles for all targets."
msgstr "所有链路与网关共用的健康检测周期时间 (秒)。"
msgid "Check Timeout (s)"
msgstr "检测超时 (秒)"
msgid "Maximum wait time in seconds for each individual check probe."
msgstr "每个检测探针的最大等待超时时间。"
msgid "Shared maximum wait time in seconds for each probe (1s recommended to prevent blocking)."
msgstr "所有探针共用的最大等待超时时间 (推荐 1 秒以杜绝网络阻塞)。"
msgid "Recovery Delay"
msgstr "恢复判定延迟"
msgid "Number of consecutive successful checks required before marking a faulted link as healthy again (failback anti-flap)."
msgid "Number of consecutive successful checks required before marking link as recovered (failback anti-flap)."
msgstr "故障网关恢复健康所需连续检测成功的次数 (防抖动回切)。"
msgid "Failover Delay"
msgstr "故障判定延迟"
msgid "Number of consecutive failed checks required before marking a healthy link as faulted (failover anti-flap)."
msgid "Number of consecutive failed checks required before marking link as faulted (failover anti-flap)."
msgstr "健康网关判定为故障所需连续检测失败的次数 (防抖动切换)。"
msgid "Monitored WAN Interfaces"
msgstr "监控网关接口"
msgid "Add and prioritize your WAN interfaces. Lower priority number means higher preference (e.g., 1 = primary, 2 = backup)."
msgstr "添加并配置您的广域网网关接口优先级。数值越小代表优先级越高 (例如:1 = 主网关,2 = 备用网关)。"
msgid "Interface"
msgstr "网络接口"
msgid "Select a logical WAN interface from Network settings."
msgstr "从系统网络配置中选择一个逻辑 WAN 接口。"
msgid "Enabled"
msgstr "已启用"
@@ -143,29 +215,29 @@ msgid "Default route metric when this link is healthy (e.g., wan=10, wan2=20)."
msgstr "当该接口健康时应用的默认路由 Metric 跃点数值 (例如:wan=10, wan2=20)。"
msgid "Ping Targets"
msgstr "Ping 检测目标"
msgstr "Ping 探测目标"
msgid "Space-separated list of IPs to ping (e.g., 223.5.5.5 8.8.8.8)."
msgstr "通过空格分隔的 Ping 目标 IP 地址列表 (例如:223.5.5.5 8.8.8.8)。"
msgid "Custom IP list to ping through this link/gateway (comma-separated, e.g. 223.5.5.5,119.29.29.29)."
msgstr "通过该链路/网关探测的目标 IP 列表 (逗号分隔,例如:223.5.5.5,119.29.29.29)。"
msgid "Ping Weight"
msgstr "Ping 权重"
msgid "Ping Targets is required."
msgstr "Ping 探测目标 IP 不能为空。"
msgid "Invalid IP address: \"%s\""
msgstr "无效的 IP 地址: \"%s\""
msgid "DNS Server"
msgstr "DNS 服务器"
msgid "DNS server IP for UDP query probe (e.g., 119.29.29.29)."
msgid "DNS server IP for UDP query probe (e.g. 119.29.29.29)."
msgstr "用于 UDP 解析探测的 DNS 服务器 IP (例如:119.29.29.29)。"
msgid "DNS Domain"
msgstr "DNS 解析域名"
msgid "Domain name to resolve for DNS probe (e.g., www.baidu.com)."
msgid "Domain name to resolve for DNS probe (e.g. www.baidu.com)."
msgstr "用于 DNS 探针解析的测试目标域名 (例如:www.baidu.com)。"
msgid "DNS Weight"
msgstr "DNS 权重"
msgid "TCP Target"
msgstr "TCP 检测目标"
@@ -175,17 +247,8 @@ msgstr "用于 TCP 三次握手探测的目标主机 IP 地址。"
msgid "TCP Port"
msgstr "TCP 检测端口"
msgid "Target port for TCP handshake probe."
msgstr "用于 TCP 探测的测试目标端口号。"
msgid "TCP Weight"
msgstr "TCP 权重"
msgid "Health Threshold"
msgstr "健康门槛"
msgid "Combined weight score required to mark this link healthy (e.g., if threshold is 2, any 2 successful checks suffice)."
msgstr "判定该接口健康所需的各指标合并通过总分值 (例如:门槛为 2,任意两项检测成功即可)。"
msgid "Target port for TCP handshake probe (e.g. 80 or 443)."
msgstr "用于 TCP 探测的测试目标端口号 (例如:80 或 443)。"
msgid "Loading logs..."
msgstr "正在加载运行日志..."
@@ -199,71 +262,32 @@ msgstr "系统日志"
msgid "No logs available."
msgstr "暂无可用运行日志。"
msgid "Enable Ping Probe"
msgstr "启用 Ping 检测"
msgid "Enable DNS Probe"
msgstr "启用 DNS 检测"
msgid "Enable TCP Probe"
msgstr "启用 TCP 检测"
msgid "Select a logical WAN interface."
msgstr "选择逻辑 WAN 接口。"
msgid "Routing priority (lower = higher preference)."
msgstr "路由优先级(数值越小优先级越高)。"
msgid "Default route metric when healthy."
msgstr "接口健康时的默认路由跃点 (Metric) 数值。"
msgid "Combined weight score required to mark this link healthy."
msgstr "判定该接口健康所需的各检测指标成功权重分之和。"
msgid "Collecting..."
msgstr "正在收集数据..."
msgid "Running State"
msgstr "运行状态"
msgid "Check Type"
msgstr "检测类型"
msgid "Time in seconds between each health check cycle for this link."
msgstr "该链路每次健康度检测周期之间的时间间隔 (秒)。"
msgid "Health probe method for this specific target."
msgstr "该目标采用的健康探测方式。"
msgid "Maximum wait time in seconds for each individual check probe for this link."
msgstr "该链路每个检测探针的最大等待超时时间 (秒)。"
msgid "Cannot enable service: At least 2 monitored links/gateways must be configured for failover switcher."
msgstr "无法启用服务: 容灾切换至少需要配置 2 个监控链路或网关。"
msgid "Number of consecutive successful checks required before marking this link as healthy (failback anti-flap)."
msgstr "判定该链路恢复健康所需连续检测成功的次数 (防抖动回切)。"
msgid "Cannot enable service: WAN interface must be configured in Multi-Gateway mode."
msgstr "无法启用服务: 单wan多网关模式下必须指定wan网络接口。"
msgid "Number of consecutive failed checks required before marking this link as faulted (failover anti-flap)."
msgstr "判定该链路发生故障所需连续检测失败的次数 (防抖动切换)。"
msgid "Cannot enable service: Target \"%s\" has no health check configured."
msgstr "无法启用服务: 监控目标「%s」未配置健康检测策略。"
msgid "Add Monitored Interface"
msgstr "添加监控接口"
msgid "Cannot enable service: Targets \"%s\" and \"%s\" have the same priority %s."
msgstr "无法启用服务: 目标「%s」和「%s」的路由优先级均为 %s,不允许重复。"
msgid "Edit Monitored Interface"
msgstr "编辑监控接口"
msgid "Cannot enable service: Target \"%s\" has an invalid or missing Gateway IP."
msgstr "无法启用服务: 目标「%s」缺少有效的网关 IP。"
msgid "Cannot enable service: No monitored WAN interfaces are configured."
msgstr "无法启用服务: 未配置任何监控的 WAN 接口。"
msgid "Cannot enable service: Targets \"%s\" and \"%s\" have the same Gateway IP %s."
msgstr "无法启用服务: 目标「%s」和「%s」具有相同的网关 IP %s,不允许重复。"
msgid "Cannot enable service: At least one interface must have a configured check type (Ping, DNS, or TCP)."
msgstr "无法启用服务: 至少需要为一个接口配置有效的检测类型 (Ping、DNS 或 TCP)。"
msgid "-- Not Configured --"
msgstr "-- 未配置 --"
msgid "Cannot enable service: At least 2 monitored WAN interfaces must be configured for failover switcher."
msgstr "无法启用服务: 链路备份切换至少需要配置 2 个监控的 WAN 接口。"
msgid "Cannot enable service: Interface \"%s\" has no health check configured."
msgstr "无法启用服务: 接口「%s」未配置健康检测策略。"
msgid "Cannot enable service: Interfaces \"%s\" and \"%s\" have the same priority %s."
msgstr "无法启用服务: 接口「%s」和「%s」的路由优先级均为 %s,不允许重复。"
msgid "Cannot enable service: Interface \"%s\" is configured multiple times."
msgstr "无法启用服务: 接口「%s」被重复配置。"
msgid "Service has been auto-disabled because the configuration is no longer valid: "
msgstr "由于配置不再合法,服务已被自动禁用: "
@@ -286,37 +310,45 @@ msgstr "该接口已被配置,请选择其他接口。"
msgid "Priority must not be empty."
msgstr "优先级不能为空。"
msgid "Priority %s conflicts with interface \"%s\". Priorities must be unique."
msgstr "优先级 %s 与接口 \"%s\" 冲突。优先级必须是唯一的。"
msgid "Metric must not be empty."
msgstr "默认跃点不能为空。"
msgid "Metric %s conflicts with interface \"%s\". Metrics must be unique."
msgstr "跃点数 %s 与接口 \"%s\" 冲突。跃点数必须是唯一的。"
msgid "Link Name"
msgstr "链路名称"
msgid "Status"
msgstr "状态"
msgid "Routing Attributes"
msgstr "路由属性"
msgid "Latency Details"
msgstr "时延详情"
msgid "Priority %s conflicts with another target. Priorities must be unique."
msgstr "优先级 %s 与其他目标冲突。优先级必须是唯一的。"
msgid "Active"
msgstr "活跃"
msgstr "主选活跃"
msgid "Standby"
msgstr "备选"
msgstr "备选待命"
msgid "Faulted"
msgstr "故障"
msgstr "故障离线"
msgid "Comma-separated list of IPs to ping (e.g., 223.5.5.5,8.8.8.8)."
msgstr "通过逗号分隔的 Ping 目标 IP 地址列表 (例如:223.5.5.5,8.8.8.8)。"
msgid "-- Not Configured --"
msgstr "-- 未配置 --"
msgid "Monitored Targets"
msgstr "监控目标列表"
msgid "Configure monitored WAN interfaces or next-hop gateways with custom probe targets and priorities (1 = primary, 2 = backup)."
msgstr "配置监控的 WAN 接口或下一跳网关,并设置自定义探测目标与优先级 (1 = 主选,2 = 备选)。"
msgid "Monitored Target"
msgstr "监控目标"
msgid "Missing Gateway IP: %s"
msgstr "缺少网关 IP: %s"
msgid "Unset"
msgstr "未设置"
msgid "Unselected"
msgstr "未选择"
msgid "WAN Interface is required."
msgstr "WAN 网络接口不能为空。"
msgid "已切换为单wan多网关模式。请确保监控目标列表中配置了有效的网关 IP。"
msgstr "已切换为单wan多网关模式。请确保监控目标列表中配置了有效的网关 IP。"
msgid "已切换为多wan口模式。请确保监控目标列表中选择了正确的 WAN 接口。"
msgstr "已切换为多wan口模式。请确保监控目标列表中选择了正确的 WAN 接口。"
@@ -2,7 +2,7 @@
"luci-app-linkback": {
"description": "ACL for LinkBack Multi-WAN Failover application",
"read": {
"uci": [ "linkback", "network" ],
"uci": [ "linkback", "network", "firewall" ],
"ubus": {
"luci.linkback": [ "get_status", "get_log" ],
"network.interface": [ "dump" ],
@@ -22,6 +22,8 @@ const methods = {
if (!status) {
status = {
enabled: false,
mode: "multi_wan",
interface: "wan",
check_interval: 5,
active_link: "none",
links: []