v46.1-ui-mt76: Argon theme + hardened WG tunnel + Link Health dashboard
基于 v46 正式版(mt76)只恢复 UI 层: - luci-theme-argon 2.4.3: local-background-wins 登录页 + bg1.jpg fallback - luci-app-argon-config: 去 ui.changes.apply, ACL mutator 移 write - luci-app-wgtunnel: rpcd ucode 后端(get/status/prepare/apply/rollback/reconnect), JSONMap 前端, 60s 一次性 token, 快照回滚, 无全局 network ACL - luci-app-tr3000-status: 只读 rpcd ucode + 5s 轮询仪表盘 - tools/: audit_ui_packages.py + install_preview.sh + rollback_watchdog.sh - docs/: 开发经历与翻车记录 + 固件哈希记录 固件本体(含烤入 WG 私钥/PSK)不入 git, 仅 K 盘保存. kernel 成员与 v46 byte-identical; 尚未刷机.
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_LICENSE:=GPL-3.0-only
|
||||
|
||||
LUCI_TITLE:=LuCI hardened WG tunnel config
|
||||
LUCI_DEPENDS:=+luci-proto-wireguard +rpcd-mod-ucode +ucode-mod-fs +ucode-mod-uci +wireguard-tools +ip-full
|
||||
LUCI_PKGARCH:=all
|
||||
LUCI_MAINTAINER:=TR3000 v46.1 UI
|
||||
|
||||
include $(TOPDIR)/feeds/luci/luci.mk
|
||||
|
||||
# call BuildPackage - OpenWrt buildroot signature
|
||||
+175
@@ -0,0 +1,175 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require rpc';
|
||||
'require form';
|
||||
'require ui';
|
||||
'require poll';
|
||||
'require dom';
|
||||
|
||||
var callGet = rpc.declare({ object: 'luci.wgtunnel', method: 'get', expect: { '': {} } });
|
||||
var callStatus = rpc.declare({ object: 'luci.wgtunnel', method: 'status', expect: { '': {} } });
|
||||
var callPrepare = rpc.declare({ object: 'luci.wgtunnel', method: 'prepare', params: [ 'input' ], expect: { '': {} } });
|
||||
var callApply = rpc.declare({ object: 'luci.wgtunnel', method: 'apply', params: [ 'input' ], expect: { '': {} } });
|
||||
var callReconnect = rpc.declare({ object: 'luci.wgtunnel', method: 'reconnect', expect: { '': {} } });
|
||||
var callRollback = rpc.declare({ object: 'luci.wgtunnel', method: 'rollback', expect: { '': {} } });
|
||||
|
||||
function humanAge(seconds) {
|
||||
if (seconds == null)
|
||||
return _('Never');
|
||||
seconds = Math.max(0, Number(seconds));
|
||||
if (seconds < 60)
|
||||
return _('%d seconds ago').format(seconds);
|
||||
if (seconds < 3600)
|
||||
return _('%d minutes ago').format(Math.floor(seconds / 60));
|
||||
return _('%d hours ago').format(Math.floor(seconds / 3600));
|
||||
}
|
||||
|
||||
function statusText(data) {
|
||||
data = data || {};
|
||||
var rt = data.runtime || {};
|
||||
return '%s · %s · wg0:%s route:%s vxlan:%s'.format(data.state || _('unknown'), humanAge(data.handshake_age_seconds), rt.wg0 ? 'ok' : 'fail', rt.route ? 'ok' : 'fail', rt.vxlan ? 'ok' : 'fail');
|
||||
}
|
||||
|
||||
function listValue(value) {
|
||||
return Array.isArray(value) ? value : (value ? [ value ] : []);
|
||||
}
|
||||
|
||||
function keyPlaceholder(configured) {
|
||||
return configured ? _('<configured; leave blank to keep>') : _('<paste complete key>');
|
||||
}
|
||||
|
||||
return view.extend({
|
||||
load: function() {
|
||||
return Promise.all([
|
||||
L.resolveDefault(callGet(), {}),
|
||||
L.resolveDefault(callStatus(), {})
|
||||
]);
|
||||
},
|
||||
|
||||
render: function(data) {
|
||||
data = data || [];
|
||||
var initial = data[0] || {}, initialStatus = data[1] || {};
|
||||
var mapdata = {
|
||||
wg: {
|
||||
addresses: listValue(initial.addresses),
|
||||
endpoint_host: initial.endpoint_host || '',
|
||||
endpoint_port: initial.endpoint_port || '',
|
||||
allowed_ips: listValue(initial.allowed_ips),
|
||||
private_key: '',
|
||||
public_key: '',
|
||||
preshared_key: ''
|
||||
}
|
||||
};
|
||||
var m = new form.JSONMap(mapdata, _('WG 隧道'), _('只编辑 wg0 到办公室的 WireGuard 参数。密钥不会从路由器读回浏览器;留空表示保持原值。'));
|
||||
var s = m.section(form.NamedSection, 'wg', _('办公室隧道'));
|
||||
s.addremove = false;
|
||||
|
||||
var o = s.option(form.DynamicList, 'addresses', _('本机隧道地址'));
|
||||
o.placeholder = initial.required_address || '10.99.0.2/32';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.Value, 'endpoint_host', _('办公室入口地址'));
|
||||
o.datatype = 'host';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.Value, 'endpoint_port', _('办公室入口端口'));
|
||||
o.datatype = 'port';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.DynamicList, 'allowed_ips', _('允许的隧道 IP'));
|
||||
o.placeholder = initial.required_allowed_ip || '10.99.0.1/32';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.Value, 'private_key', _('本机私钥'));
|
||||
o.password = true;
|
||||
o.placeholder = keyPlaceholder(initial.private_key_configured);
|
||||
o.rmempty = true;
|
||||
|
||||
o = s.option(form.Value, 'public_key', _('办公室公钥'));
|
||||
o.password = true;
|
||||
o.placeholder = keyPlaceholder(initial.public_key_configured);
|
||||
o.rmempty = true;
|
||||
|
||||
o = s.option(form.Value, 'preshared_key', _('预共享密钥'));
|
||||
o.password = true;
|
||||
o.placeholder = keyPlaceholder(initial.preshared_key_configured);
|
||||
o.rmempty = true;
|
||||
|
||||
return m.render().then(L.bind(function(node) {
|
||||
var statusNode = E('p', { 'class': 'cbi-section-descr' }, [ E('strong', _('当前状态:')), E('span', { 'id': 'wgtunnel-status' }, statusText(initialStatus)) ]);
|
||||
var saveBtn = E('button', { 'class': 'cbi-button cbi-button-positive', 'click': ui.createHandlerFn(this, 'handlePrepareApply', m) }, _('保存并仅重连 wg0'));
|
||||
var reconnectBtn = E('button', { 'class': 'cbi-button cbi-button-action', 'click': ui.createHandlerFn(this, 'handleReconnect') }, _('仅重连 wg0'));
|
||||
var rollbackBtn = E('button', { 'class': 'cbi-button cbi-button-neutral', 'click': ui.createHandlerFn(this, 'handleRollback') }, _('回滚上次快照'));
|
||||
node.appendChild(E('hr'));
|
||||
node.appendChild(statusNode);
|
||||
node.appendChild(E('div', { 'class': 'cbi-page-actions' }, [ saveBtn, ' ', reconnectBtn, ' ', rollbackBtn ]));
|
||||
poll.add(L.bind(function() {
|
||||
return L.resolveDefault(callStatus(), {}).then(function(next) {
|
||||
var el = document.getElementById('wgtunnel-status');
|
||||
if (el)
|
||||
dom.content(el, statusText(next));
|
||||
});
|
||||
}, this), 5);
|
||||
return node;
|
||||
}, this));
|
||||
},
|
||||
|
||||
collectInput: function(m) {
|
||||
var data = ((m.data || {}).wg || {});
|
||||
return {
|
||||
addresses: listValue(data.addresses),
|
||||
endpoint_host: data.endpoint_host || '',
|
||||
endpoint_port: data.endpoint_port || '',
|
||||
allowed_ips: listValue(data.allowed_ips),
|
||||
private_key: data.private_key || '',
|
||||
public_key: data.public_key || '',
|
||||
preshared_key: data.preshared_key || ''
|
||||
};
|
||||
},
|
||||
|
||||
handlePrepareApply: function(ev, m) {
|
||||
return m.save(null, true).then(L.bind(function() {
|
||||
return callPrepare(this.collectInput(m));
|
||||
}, this)).then(function(prepared) {
|
||||
if (!prepared.ok)
|
||||
throw new Error(prepared.message || prepared.code || _('Prepare failed'));
|
||||
var rows = (prepared.diff || []).map(function(d) { return E('li', {}, [ d.field, ': ', String(d.before), ' → ', String(d.after) ]); });
|
||||
if (!rows.length)
|
||||
rows.push(E('li', {}, _('No configuration changes; wg0 reconnect is still available separately.')));
|
||||
return ui.showModal(_('确认 WG 配置变更'), [
|
||||
E('p', _('将只写入 network.wg0 / network.wgpeer 的允许字段,并且只重连 wg0。不会重启 LAN、Wi-Fi 或防火墙。')),
|
||||
E('ul', rows),
|
||||
E('div', { 'class': 'right' }, [
|
||||
E('button', { 'class': 'cbi-button', 'click': ui.hideModal }, _('Cancel')), ' ',
|
||||
E('button', { 'class': 'cbi-button cbi-button-positive', 'click': function() {
|
||||
ui.hideModal();
|
||||
return callApply({ token: prepared.token, confirm: true }).then(function(result) {
|
||||
if (result.ok)
|
||||
ui.addNotification(null, E('p', _('WG 配置已保存,wg0/VXLAN 检查通过。')), 'info');
|
||||
else
|
||||
ui.addNotification(null, E('p', _('应用失败,已尝试回滚:%s').format(result.code || 'unknown')), 'danger');
|
||||
});
|
||||
} }, _('Apply'))
|
||||
])
|
||||
]);
|
||||
}).catch(function(e) {
|
||||
ui.addNotification(null, E('p', _('WG 操作失败:%s').format(e.message || e)), 'danger');
|
||||
});
|
||||
},
|
||||
|
||||
handleReconnect: function() {
|
||||
return L.resolveDefault(callReconnect(), {}).then(function(result) {
|
||||
ui.addNotification(null, E('p', result.ok ? _('wg0 已重连。') : _('wg0 重连失败。')), result.ok ? 'info' : 'danger');
|
||||
});
|
||||
},
|
||||
|
||||
handleRollback: function() {
|
||||
return L.resolveDefault(callRollback(), {}).then(function(result) {
|
||||
ui.addNotification(null, E('p', result.ok ? _('已恢复上次 WG 配置快照并重连 wg0。') : _('没有可用快照或回滚失败。')), result.ok ? 'info' : 'danger');
|
||||
});
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
});
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"admin/network/wgtunnel": {
|
||||
"title": "WG 隧道",
|
||||
"order": 35,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "wgtunnel"
|
||||
},
|
||||
"depends": {
|
||||
"acl": [ "luci-app-wgtunnel" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"luci-app-wgtunnel": {
|
||||
"description": "Manage only the TR3000 wg0 office tunnel through luci.wgtunnel",
|
||||
"read": {
|
||||
"ubus": {
|
||||
"luci.wgtunnel": [ "get", "status" ]
|
||||
}
|
||||
},
|
||||
"write": {
|
||||
"ubus": {
|
||||
"luci.wgtunnel": [ "prepare", "apply", "rollback", "reconnect" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+367
@@ -0,0 +1,367 @@
|
||||
#!/usr/bin/ucode
|
||||
/* SPDX-License-Identifier: GPL-3.0-only */
|
||||
'use strict';
|
||||
|
||||
import { access, chmod, mkdir, popen, readfile, stat, unlink, writefile } from 'fs';
|
||||
import { cursor } from 'uci';
|
||||
|
||||
const IFACE = 'wg0';
|
||||
const PEER = 'wgpeer';
|
||||
const STATE_DIR = '/tmp/luci-wgtunnel';
|
||||
const TOKEN_FILE = STATE_DIR + '/prepare.json';
|
||||
const SNAPSHOT = STATE_DIR + '/network.backup';
|
||||
const LOCK = STATE_DIR + '/apply.lock';
|
||||
const REQUIRED_ADDRESS = '10.99.0.2/32';
|
||||
const REQUIRED_ALLOWED = '10.99.0.1/32';
|
||||
|
||||
let uci = null;
|
||||
|
||||
function sh(command) {
|
||||
let fd = popen(command, 'r');
|
||||
if (!fd)
|
||||
return { ok: false, out: '' };
|
||||
let out = fd.read('all') || '';
|
||||
return { ok: fd.close() == 0, out };
|
||||
}
|
||||
|
||||
function read_text(path) {
|
||||
let value = readfile(path);
|
||||
return value == null ? null : trim(value);
|
||||
}
|
||||
|
||||
function uptime() {
|
||||
let value = read_text('/proc/uptime') || '0';
|
||||
return +split(value, /\s+/)[0];
|
||||
}
|
||||
|
||||
function ensure_state_dir() {
|
||||
mkdir(STATE_DIR, 0700);
|
||||
chmod(STATE_DIR, 0700);
|
||||
}
|
||||
|
||||
function fail(code, message) {
|
||||
return { ok: false, code, message: message || code };
|
||||
}
|
||||
|
||||
function arr(value) {
|
||||
if (value == null)
|
||||
return [];
|
||||
return type(value) == 'array' ? value : [ value ];
|
||||
}
|
||||
|
||||
function uniq(values) {
|
||||
let result = [];
|
||||
for (let value in arr(values)) {
|
||||
value = trim(`${value}`);
|
||||
if (value != '' && index(result, value) < 0)
|
||||
push(result, value);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function clean_scalar(value) {
|
||||
value = trim(`${value || ''}`);
|
||||
return match(value, /[\x00-\x1f\x7f]/) ? null : value;
|
||||
}
|
||||
|
||||
function ip4_to_int(ip) {
|
||||
let parts = split(ip, '.');
|
||||
if (length(parts) != 4)
|
||||
return null;
|
||||
let n = 0;
|
||||
for (let part in parts) {
|
||||
if (!match(part, /^[0-9]+$/))
|
||||
return null;
|
||||
let v = +part;
|
||||
if (v < 0 || v > 255)
|
||||
return null;
|
||||
n = n * 256 + v;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
function parse_cidr(value) {
|
||||
value = clean_scalar(value);
|
||||
if (value == null)
|
||||
return null;
|
||||
let m = match(value, /^([0-9]{1,3}(?:\.[0-9]{1,3}){3})\/([0-9]|[12][0-9]|3[0-2])$/);
|
||||
if (!m)
|
||||
return null;
|
||||
let ip = ip4_to_int(m[1]);
|
||||
let prefix = +m[2];
|
||||
if (ip == null)
|
||||
return null;
|
||||
return { text: `${m[1]}/${prefix}`, ip, prefix };
|
||||
}
|
||||
|
||||
function reject_bad_cidr(value, field) {
|
||||
let cidr = parse_cidr(value);
|
||||
if (cidr == null)
|
||||
return fail('INVALID_CIDR', `${field}: ${value}`);
|
||||
if (cidr.ip == 0 || cidr.prefix == 0)
|
||||
return fail('DEFAULT_ROUTE_REJECTED', field);
|
||||
let first = floor(cidr.ip / 16777216);
|
||||
if (first >= 224 || first == 127 || first == 169)
|
||||
return fail('RESERVED_RANGE_REJECTED', field);
|
||||
let ip = cidr.text;
|
||||
if (match(ip, /^192\.168\./) || match(ip, /^10\.(?!99\.)/) || match(ip, /^172\.(1[6-9]|2[0-9]|3[0-1])\./))
|
||||
return fail('LAN_WAN_OVERLAP_REJECTED', field);
|
||||
return null;
|
||||
}
|
||||
|
||||
function validate_cidr_list(values, required, field) {
|
||||
values = uniq(values);
|
||||
if (index(values, required) < 0)
|
||||
push(values, required);
|
||||
for (let value in values) {
|
||||
let err = reject_bad_cidr(value, field);
|
||||
if (err)
|
||||
return err;
|
||||
}
|
||||
return { ok: true, values };
|
||||
}
|
||||
|
||||
function valid_port(value) {
|
||||
value = clean_scalar(value);
|
||||
return value != null && match(value, /^[0-9]+$/) && +value > 0 && +value <= 65535;
|
||||
}
|
||||
|
||||
function valid_host(value) {
|
||||
value = clean_scalar(value);
|
||||
return value != null && value != '' && length(value) <= 253 && match(value, /^[A-Za-z0-9_.:-]+$/) && !match(value, /^-/) && !match(value, /\.\./);
|
||||
}
|
||||
|
||||
function valid_wg_key(value) {
|
||||
value = clean_scalar(value);
|
||||
if (value == null || value == '')
|
||||
return true;
|
||||
if (!match(value, /^[A-Za-z0-9+/]{43}=$/))
|
||||
return false;
|
||||
let decoded = b64dec(value);
|
||||
return decoded != null && length(decoded) == 32 && b64enc(decoded) == value;
|
||||
}
|
||||
|
||||
function section_exists(name, type_name) {
|
||||
return uci.get('network', name) != null && (type_name == null || uci.get('network', name, '.type') == type_name);
|
||||
}
|
||||
|
||||
function generation() {
|
||||
let s = stat('/etc/config/network');
|
||||
return s == null ? 'missing' : `${s.mtime || 0}:${s.size || 0}`;
|
||||
}
|
||||
|
||||
function read_config() {
|
||||
let addresses = uniq(uci.get('network', IFACE, 'addresses'));
|
||||
let allowed = uniq(uci.get('network', PEER, 'allowed_ips'));
|
||||
return {
|
||||
interface: IFACE,
|
||||
peer_section: PEER,
|
||||
addresses,
|
||||
private_key_configured: !!uci.get('network', IFACE, 'private_key'),
|
||||
endpoint_host: uci.get('network', PEER, 'endpoint_host') || '',
|
||||
endpoint_port: uci.get('network', PEER, 'endpoint_port') || '',
|
||||
public_key_configured: !!uci.get('network', PEER, 'public_key'),
|
||||
preshared_key_configured: !!uci.get('network', PEER, 'preshared_key'),
|
||||
allowed_ips: allowed,
|
||||
required_address: REQUIRED_ADDRESS,
|
||||
required_allowed_ip: REQUIRED_ALLOWED,
|
||||
generation: generation()
|
||||
};
|
||||
}
|
||||
|
||||
function normalize_input(input) {
|
||||
input ||= {};
|
||||
let addresses = validate_cidr_list(input.addresses, REQUIRED_ADDRESS, 'addresses');
|
||||
if (!addresses.ok)
|
||||
return addresses;
|
||||
let allowed = validate_cidr_list(input.allowed_ips, REQUIRED_ALLOWED, 'allowed_ips');
|
||||
if (!allowed.ok)
|
||||
return allowed;
|
||||
let endpoint_host = clean_scalar(input.endpoint_host);
|
||||
let endpoint_port = clean_scalar(input.endpoint_port);
|
||||
if (!valid_host(endpoint_host))
|
||||
return fail('INVALID_ENDPOINT_HOST');
|
||||
if (!valid_port(endpoint_port))
|
||||
return fail('INVALID_ENDPOINT_PORT');
|
||||
for (let name in [ 'private_key', 'public_key', 'preshared_key' ])
|
||||
if (!valid_wg_key(input[name]))
|
||||
return fail('INVALID_WG_KEY', name);
|
||||
return { ok: true, values: {
|
||||
addresses: addresses.values,
|
||||
endpoint_host,
|
||||
endpoint_port,
|
||||
allowed_ips: allowed.values,
|
||||
private_key: clean_scalar(input.private_key) || null,
|
||||
public_key: clean_scalar(input.public_key) || null,
|
||||
preshared_key: clean_scalar(input.preshared_key) || null
|
||||
} };
|
||||
}
|
||||
|
||||
function diff_values(current, next) {
|
||||
let diff = [];
|
||||
function add(field, before, after, secret) {
|
||||
if (json(before) != json(after))
|
||||
push(diff, { field, before: secret ? (before ? '<configured>' : '<empty>') : before, after: secret ? (after ? '<new value>' : '<unchanged>') : after, secret: !!secret });
|
||||
}
|
||||
add('network.wg0.addresses', current.addresses, next.addresses, false);
|
||||
add('network.wgpeer.endpoint_host', current.endpoint_host, next.endpoint_host, false);
|
||||
add('network.wgpeer.endpoint_port', current.endpoint_port, next.endpoint_port, false);
|
||||
add('network.wgpeer.allowed_ips', current.allowed_ips, next.allowed_ips, false);
|
||||
add('network.wg0.private_key', current.private_key_configured, next.private_key != null, true);
|
||||
add('network.wgpeer.public_key', current.public_key_configured, next.public_key != null, true);
|
||||
add('network.wgpeer.preshared_key', current.preshared_key_configured, next.preshared_key != null, true);
|
||||
return diff;
|
||||
}
|
||||
|
||||
function write_token(state) {
|
||||
ensure_state_dir();
|
||||
writefile(TOKEN_FILE, json(state));
|
||||
chmod(TOKEN_FILE, 0600);
|
||||
}
|
||||
|
||||
function read_token() {
|
||||
let text = read_text(TOKEN_FILE);
|
||||
return text == null ? null : json(text);
|
||||
}
|
||||
|
||||
function delete_token() {
|
||||
unlink(TOKEN_FILE);
|
||||
}
|
||||
|
||||
function set_list(section, option, values) {
|
||||
uci.delete('network', section, option);
|
||||
for (let value in values)
|
||||
uci.add_list('network', section, option, value);
|
||||
}
|
||||
|
||||
function apply_values(values) {
|
||||
set_list(IFACE, 'addresses', values.addresses);
|
||||
uci.set('network', PEER, 'endpoint_host', values.endpoint_host);
|
||||
uci.set('network', PEER, 'endpoint_port', values.endpoint_port);
|
||||
set_list(PEER, 'allowed_ips', values.allowed_ips);
|
||||
if (values.private_key != null)
|
||||
uci.set('network', IFACE, 'private_key', values.private_key);
|
||||
if (values.public_key != null)
|
||||
uci.set('network', PEER, 'public_key', values.public_key);
|
||||
if (values.preshared_key != null)
|
||||
uci.set('network', PEER, 'preshared_key', values.preshared_key);
|
||||
uci.commit('network');
|
||||
}
|
||||
|
||||
function snapshot() {
|
||||
ensure_state_dir();
|
||||
let copied = sh(`cp /etc/config/network '${SNAPSHOT}' && chmod 0600 '${SNAPSHOT}'`);
|
||||
return copied.ok;
|
||||
}
|
||||
|
||||
function restore_snapshot() {
|
||||
if (!access(SNAPSHOT))
|
||||
return false;
|
||||
let restored = sh(`cp '${SNAPSHOT}' /etc/config/network && chmod 0600 /etc/config/network`);
|
||||
return restored.ok;
|
||||
}
|
||||
|
||||
function reconnect_wg0() {
|
||||
return sh('/sbin/ifdown wg0 >/dev/null 2>&1; /sbin/ifup wg0 >/dev/null 2>&1').ok;
|
||||
}
|
||||
|
||||
function check_runtime() {
|
||||
let link = sh('/usr/sbin/ip link show dev wg0 >/dev/null 2>&1');
|
||||
let route = sh('/usr/sbin/ip route show 10.99.0.1/32 2>/dev/null');
|
||||
let vxlan = sh('/usr/sbin/ip -d link show dev vxlan0 2>/dev/null');
|
||||
let ok_route = route.ok && index(route.out, 'dev wg0') >= 0;
|
||||
let ok_vxlan = vxlan.ok && index(vxlan.out, 'vxlan id 10') >= 0 && index(vxlan.out, 'dstport 4789') >= 0 && index(vxlan.out, 'nolearning') >= 0 && index(vxlan.out, 'master br-lan') >= 0;
|
||||
return { wg0: link.ok, route: ok_route, vxlan: ok_vxlan, ok: link.ok && ok_route && ok_vxlan };
|
||||
}
|
||||
|
||||
function status() {
|
||||
let hs = sh('/usr/bin/wg show wg0 latest-handshakes 2>/dev/null');
|
||||
let latest = null, peers = 0;
|
||||
if (hs.ok) {
|
||||
for (let line in split(trim(hs.out), /\n/)) {
|
||||
let fields = split(line, /\t/);
|
||||
if (length(fields) >= 2) {
|
||||
peers++;
|
||||
let t = +fields[1];
|
||||
if (t > (latest || 0))
|
||||
latest = t;
|
||||
}
|
||||
}
|
||||
}
|
||||
let now = timelocal(localtime());
|
||||
let age = latest == null || latest == 0 ? null : max(0, now - latest);
|
||||
let runtime = check_runtime();
|
||||
return { available: hs.ok, peers, handshake_age_seconds: age, runtime, state: !hs.ok ? 'unavailable' : (peers == 0 ? 'no_peer' : (age == null ? 'never_handshaken' : (age <= 180 ? 'healthy' : 'stale'))) };
|
||||
}
|
||||
|
||||
const methods = {
|
||||
get: { call: function() {
|
||||
uci = cursor();
|
||||
return read_config();
|
||||
} },
|
||||
status: { call: function() {
|
||||
return status();
|
||||
} },
|
||||
prepare: { call: function(input) {
|
||||
uci = cursor();
|
||||
if (!section_exists(IFACE, 'interface') || !section_exists(PEER, 'wireguard_wg0'))
|
||||
return fail('REQUIRED_SECTION_MISSING');
|
||||
let normalized = normalize_input(input || {});
|
||||
if (!normalized.ok)
|
||||
return normalized;
|
||||
let current = read_config();
|
||||
let diff = diff_values(current, normalized.values);
|
||||
let token = trim(readfile('/proc/sys/kernel/random/uuid') || `${uptime()}`);
|
||||
write_token({ token, expires: uptime() + 60, generation: current.generation, values: normalized.values });
|
||||
return { ok: true, token, expires_in: 60, diff, requires_confirm: true };
|
||||
} },
|
||||
apply: { call: function(input) {
|
||||
uci = cursor();
|
||||
input ||= {};
|
||||
let state = read_token();
|
||||
if (state == null || input.token != state.token || input.confirm !== true)
|
||||
return fail('INVALID_TOKEN');
|
||||
if (uptime() > state.expires)
|
||||
return fail('TOKEN_EXPIRED');
|
||||
if (generation() != state.generation)
|
||||
return fail('CONFIG_GENERATION_CHANGED');
|
||||
if (!sh(`mkdir '${LOCK}' 2>/dev/null`).ok)
|
||||
return fail('APPLY_LOCKED');
|
||||
let runtime = null;
|
||||
try {
|
||||
delete_token();
|
||||
if (!snapshot()) {
|
||||
sh(`rmdir '${LOCK}' 2>/dev/null`);
|
||||
return fail('SNAPSHOT_FAILED');
|
||||
}
|
||||
apply_values(state.values);
|
||||
reconnect_wg0();
|
||||
runtime = check_runtime();
|
||||
if (!runtime.ok) {
|
||||
restore_snapshot();
|
||||
reconnect_wg0();
|
||||
sh(`rmdir '${LOCK}' 2>/dev/null`);
|
||||
return { ok: false, code: 'RUNTIME_CHECK_FAILED_ROLLED_BACK', runtime };
|
||||
}
|
||||
sh(`rmdir '${LOCK}' 2>/dev/null`);
|
||||
return { ok: true, runtime };
|
||||
} catch (e) {
|
||||
restore_snapshot();
|
||||
reconnect_wg0();
|
||||
sh(`rmdir '${LOCK}' 2>/dev/null`);
|
||||
return fail('APPLY_EXCEPTION_ROLLED_BACK');
|
||||
}
|
||||
} },
|
||||
rollback: { call: function() {
|
||||
let ok = restore_snapshot();
|
||||
if (ok)
|
||||
reconnect_wg0();
|
||||
return { ok, runtime: check_runtime() };
|
||||
} },
|
||||
reconnect: { call: function() {
|
||||
let ok = reconnect_wg0();
|
||||
return { ok, runtime: check_runtime() };
|
||||
} }
|
||||
};
|
||||
|
||||
return { 'luci.wgtunnel': methods };
|
||||
Reference in New Issue
Block a user