🤞 Sync 2026-11-10 21:15:30

This commit is contained in:
github-actions[bot] committed 2026-11-10 21:15:30 +08:00
1 parent 42de8331af
commit 4a38dae8ff
27 files changed
+1281 -235

No files matched your search

@@ -0,0 +1,168 @@
// SPDX-License-Identifier: Apache-2.0
'use strict';
'require form';
'require uci';
'require ui';
'require view';
return view.extend({
load: function () {
return uci.load('v2raya');
},
render: function () {
var m, s, o;
m = new form.Map('v2raya', _('v2rayA'),
_('Settings for the v2rayA service. Save & Apply restarts the service so the new values take effect. v2rayA 2.5.x uses only its bundled v2raya_core; Xray-core and sing-box are not supported backends.'));
s = m.section(form.NamedSection, 'config', 'v2raya');
s.tab('general', _('General'));
s.tab('proxy', _('Transparent proxy'),
_('The transparent proxy mode, routing rules and nodes are configured in the v2rayA web interface. These options control core start-up and hooks.'));
s.tab('logging', _('Logging'));
s.tab('advanced', _('Advanced'));
o = s.taboption('general', form.Flag, 'enabled', _('Enable service'));
o.default = '0';
o.rmempty = false;
o.description = _('Start v2rayA at boot. You can also start or stop it from the overview page.');
o = s.taboption('general', form.Value, 'address', _('Listening address'));
o.placeholder = '0.0.0.0:2017';
o.rmempty = false;
o.validate = function (section_id, value) {
if (!value)
return _('An address is required.');
if (!/^(\[[0-9a-fA-F:]+\]|[A-Za-z0-9._-]+):[0-9]{1,5}$/.test(value))
return _('Enter an address and port, for example 0.0.0.0:2017 or [::]:2017.');
return true;
};
o.description = _('The web interface listens on this address. Use 127.0.0.1:2017 to keep it off the LAN.');
o = s.taboption('general', form.Value, 'baseurl', _('Base URL'));
o.placeholder = '/';
o.description = _('URL prefix when v2rayA runs behind a reverse proxy.');
o.validate = function (section_id, value) {
if (value && value.charAt(0) !== '/')
return _('The base URL must start with /.');
return true;
};
o = s.taboption('general', form.ListValue, 'ipv6_support', _('IPv6 support'));
o.value('auto', _('Auto'));
o.value('on', _('On'));
o.value('off', _('Off'));
o.default = 'auto';
o.description = _('Make sure IPv6 connectivity works before forcing it on.');
o = s.taboption('general', form.ListValue, 'nftables_support', _('nftables support'));
o.value('auto', _('Auto'));
o.value('on', _('On'));
o.value('off', _('Off'));
o.default = 'auto';
o.description = _('nftables is the default firewall backend on OpenWrt 22.03 and later.');
o = s.taboption('proxy', form.Value, 'core_startup_timeout', _('Core startup timeout'));
o.datatype = 'uinteger';
o.placeholder = '15';
o.default = '15';
o.description = _('Seconds to wait for v2raya_core to start. Raise this on slow MIPS routers.');
o = s.taboption('proxy', form.Flag, 'redirect_respect_bound_device', _('Respect bound devices in redirect mode'));
o.default = '0';
o.description = _('Let sockets bound to a device bypass the proxy. Requires Linux 5.14 or later and cgroup v2.');
o = s.taboption('proxy', form.Value, 'transparent_hook', _('Transparent proxy hook'));
o.placeholder = '/usr/bin/example-hook';
o.description = _('Executable called around transparent proxy setup and teardown.');
o = s.taboption('proxy', form.Value, 'core_hook', _('Core life-cycle hook'));
o.placeholder = '/usr/bin/example-hook';
o.description = _('Executable called around v2raya_core start and stop.');
o = s.taboption('proxy', form.Value, 'plugin_manager', _('Plugin manager'));
o.placeholder = '/usr/bin/example-plugin-manager';
o.description = _('Executable used to parse protocols that v2rayA does not know natively.');
o = s.taboption('logging', form.ListValue, 'log_level', _('Log level'));
o.value('trace', _('Trace'));
o.value('debug', _('Debug'));
o.value('info', _('Info'));
o.value('warn', _('Warning'));
o.value('error', _('Error'));
o.default = 'warn';
o.description = _('Warning keeps flash writes down. Use Info while debugging.');
o = s.taboption('logging', form.Value, 'log_max_days', _('Keep logs for'));
o.datatype = 'uinteger';
o.placeholder = '1';
o.default = '1';
o.description = _('Number of days to keep rotated log files.');
o = s.taboption('logging', form.Flag, 'log_disable_color', _('Disable colours in the log'));
o.default = '1';
o.description = _('The log file is plain text, so colours are not useful there.');
o = s.taboption('logging', form.Flag, 'log_disable_timestamp', _('Disable timestamps in the log'));
o.default = '0';
o.description = _('Only relevant when v2rayA writes to syslog or journald.');
o = s.taboption('advanced', form.Value, 'v2ray_assetsdir', _('Rule data directory'));
o.placeholder = '/usr/share/v2raya';
o.default = '/usr/share/v2raya';
o.description = _('Directory containing geoip.dat and geosite.dat. Leave empty for auto-detection.');
o = s.taboption('advanced', form.Value, 'v2ray_bin', _('Core binary (v2raya_core only)'));
o.placeholder = _('Auto-detect');
o.description = _('Path to v2raya_core. Leave empty to use the binary next to v2raya or in PATH. Only the matching v2raya_core is supported; Xray-core and sing-box use incompatible configuration formats.');
o = s.taboption('advanced', form.Value, 'v2ray_confdir', _('Extra configuration directory'));
o.placeholder = _('None');
o.description = _('Additional v2ray configuration files merged into the generated configuration.');
o = s.taboption('advanced', form.Value, 'gomemlimit', _('Go memory limit'));
o.placeholder = '64MiB';
o.default = '64MiB';
o.description = _('Soft memory limit for both v2raya and v2raya_core (GOMEMLIMIT). Use 0 to disable, or 96MiB on a larger router.');
o = s.taboption('advanced', form.Value, 'gomaxprocs', _('Go scheduler threads'));
o.datatype = 'uinteger';
o.placeholder = '2';
o.default = '2';
o.description = _('GOMAXPROCS. 0 uses every CPU core; 2 is enough for most routers.');
o = s.taboption('advanced', form.Value, 'gogc', _('Go GC percent'));
o.datatype = 'uinteger';
o.placeholder = '0';
o.default = '0';
o.description = _('GOGC. 0 keeps the Go default; lower values use less memory and more CPU.');
o = s.taboption('advanced', form.Value, 'nofile', _('Open file limit'));
o.datatype = 'uinteger';
o.placeholder = '16384';
o.default = '16384';
o.description = _('Maximum open file descriptors. 16384 is plenty for a home router and bounds a connection flood.');
return m.render();
},
handleSaveApply: function (ev, mode) {
/*
* Applying the UCI delta emits a config.change event; the init
* script's procd reload trigger restarts the service with the new
* values. Restarting here as well would stop and start it twice.
*/
return this.handleSave(ev).then(function () {
return uci.apply();
}).then(function () {
ui.addNotification(null, E('p', {},
_('Settings applied. The service is restarting.')), 'info');
}).catch(function (err) {
ui.addNotification(null, E('p', {},
_('The configuration was not applied: %s').format(err)), 'danger');
});
}
});
@@ -0,0 +1,76 @@
// SPDX-License-Identifier: Apache-2.0
'use strict';
'require dom';
'require fs';
'require poll';
'require ui';
'require view';
var LOG_PATH = '/var/log/v2raya/v2raya.log';
var MAX_LINES = 500;
return view.extend({
render: function () {
var logEl = E('pre', {
id: 'v2raya-log',
style: 'white-space:pre-wrap;word-break:break-all;max-height:65vh;overflow:auto;padding:8px;margin:0;background:rgba(0,0,0,0.04);border-radius:4px'
}, _('Loading…'));
var refresh = function () {
return L.resolveDefault(fs.read_direct(LOG_PATH, 'text'), null).then(function (text) {
if (text === null) {
dom.content(logEl, _('The log file does not exist yet. Start the service and try again.'));
return;
}
var lines = text.replace(/\s+$/, '').split('\n');
if (lines.length > MAX_LINES)
lines = lines.slice(-MAX_LINES);
var atBottom = (logEl.scrollTop + logEl.clientHeight + 20) >= logEl.scrollHeight;
dom.content(logEl, lines.join('\n') || _('The log is empty.'));
if (atBottom)
logEl.scrollTop = logEl.scrollHeight;
}).catch(function (err) {
dom.content(logEl, _('Unable to read the log: %s').format(err));
});
};
var refreshBtn = E('button', { class: 'btn cbi-button cbi-button-action' }, _('Refresh'));
var clearBtn = E('button', { class: 'btn cbi-button cbi-button-reset' }, _('Clear log'));
refreshBtn.addEventListener('click', function () { refresh(); });
clearBtn.addEventListener('click', function () {
if (!confirm(_('Clear the v2rayA log file?')))
return;
L.resolveDefault(fs.write(LOG_PATH, ''), null).then(function (res) {
if (res === null)
ui.addNotification(null, E('p', {}, _('Unable to clear the log file.')), 'danger');
else
ui.addNotification(null, E('p', {}, _('Log file cleared.')), 'info');
refresh();
});
});
refresh();
poll.add(refresh, 5);
return E('div', { class: 'cbi-map' }, [
E('h2', {}, _('v2rayA log')),
E('div', { class: 'cbi-map-descr' },
_('Showing the last %d lines of %s. Refreshes every %d seconds.')
.format(MAX_LINES, LOG_PATH, L.env.pollinterval || 5)),
E('div', { class: 'cbi-section' }, [
logEl,
E('div', { style: 'margin-top:8px' }, [ refreshBtn, ' ', clearBtn ])
])
]);
},
handleSave: null,
handleSaveApply: null,
handleReset: null
});
@@ -0,0 +1,280 @@
// SPDX-License-Identifier: Apache-2.0
'use strict';
'require view';
'require fs';
'require poll';
'require rpc';
'require uci';
'require ui';
var callServiceList = rpc.declare({
object: 'service',
method: 'list',
params: [ 'name' ],
expect: { '': {} }
});
function serviceRunning() {
return L.resolveDefault(callServiceList('v2raya'), {}).then(function (res) {
var inst = res && res.v2raya && res.v2raya.instances && res.v2raya.instances.v2raya;
return !!(inst && inst.running);
});
}
function commandVersion(command, args) {
return L.resolveDefault(fs.exec(command, args || []), null).then(function (res) {
if (!res || res.code !== 0 || !res.stdout)
return null;
var line = res.stdout.trim().split('\n')[0].trim();
return line || null;
});
}
function detectBackends() {
return L.resolveDefault(fs.exec('/usr/libexec/v2raya-detect', []), null).then(function (res) {
var info = {};
if (!res || res.code !== 0)
return info;
(res.stdout || '').split('\n').forEach(function (line) {
var pos = line.indexOf('=');
if (pos > 0)
info[line.substring(0, pos)] = line.substring(pos + 1);
});
return info;
});
}
function backendRow(label, path, version, packageFlag, note) {
var field = [];
if (path) {
field.push(E('strong', {}, _('Detected')));
field.push(' — ', E('code', {}, path));
if (version)
field.push(E('div', { class: 'cbi-section-descr' }, version));
}
else {
field.push(E('span', { style: 'color:#777' }, _('Not installed')));
}
if (packageFlag === '1')
field.push(E('div', { class: 'cbi-section-descr' }, _('Package installed')));
if (note)
field.push(E('div', { class: 'cbi-section-descr' }, note));
return E('div', { class: 'cbi-value' }, [
E('label', { class: 'cbi-value-title' }, label),
E('div', { class: 'cbi-value-field' }, field)
]);
}
function parseListenAddress(address) {
var match = String(address || '').match(/^\[?([^\]]*)\]?:(\d+)$/);
var host = match ? match[1] : '';
var port = match ? match[2] : '2017';
if (!host || host === '0.0.0.0' || host === '::')
host = window.location.hostname;
if (host.indexOf(':') !== -1)
host = '[' + host + ']';
return { host: host, port: port };
}
function renderStatus(running) {
var el = document.getElementById('v2raya-status');
if (!el)
return;
el.textContent = running ? _('Running') : _('Stopped');
el.style.color = running ? '#2e7d32' : '#c62828';
}
function setBusy(button, busy) {
if (!button)
return;
button.disabled = busy;
}
function runAction(action, button) {
setBusy(button, true);
return L.resolveDefault(fs.exec('/etc/init.d/v2raya', [ action ]), null).then(function (res) {
setBusy(button, false);
if (!res || res.code !== 0) {
ui.addNotification(null, E('p', {},
(res && (res.stderr || res.stdout)) || _('The service command failed.')), 'danger');
return;
}
ui.addNotification(null, E('p', {}, _('Service action "%s" completed.').format(action)), 'info');
return serviceRunning().then(renderStatus);
}).catch(function (err) {
setBusy(button, false);
ui.addNotification(null, E('p', {}, _('Unable to run the service command: %s').format(err)), 'danger');
});
}
function startService(button) {
uci.set('v2raya', 'config', 'enabled', '1');
return L.resolveDefault(uci.save(), null).then(function () {
return runAction('start', button);
}).then(function () {
/*
* Persist enabled=1 so the service also starts after a reboot.
* The resulting config.change event may cause one more restart;
* that is expected after an apply and the page polls the state.
*/
return L.resolveDefault(uci.apply(), null);
});
}
return view.extend({
load: function () {
return Promise.all([
uci.load('v2raya'),
commandVersion('/usr/bin/v2raya', [ '--version' ]),
commandVersion('/usr/bin/v2raya_core', [ 'version' ]),
detectBackends()
]);
},
render: function (data) {
var address = uci.get('v2raya', 'config', 'address') || '0.0.0.0:2017';
var baseurl = uci.get('v2raya', 'config', 'baseurl') || '/';
if (baseurl === '')
baseurl = '/';
var listen = parseListenAddress(address);
var weburl = 'http://' + listen.host + ':' + listen.port +
(baseurl === '/' ? '/' : '/' + baseurl.replace(/^\/+|\/+$/g, '') + '/');
var serviceVersion = data[1] || _('not detected');
var coreVersion = data[2] || _('not detected');
var backends = data[3] || {};
var detectAvailable = Object.keys(backends).length > 0;
var enabledServices = (backends.services_enabled || '').split(',').filter(function (name) {
return name && name !== 'v2raya';
});
var backendChildren = [
E('h3', {}, _('Detected cores and proxy services')),
E('p', { class: 'cbi-section-descr' },
_('v2rayA 2.5.x runs only its bundled v2raya_core. Existing Xray-core, v2ray and sing-box installations are detected here so they are not duplicated or replaced; they are never used as the v2rayA backend.'))
];
if (detectAvailable) {
backendChildren.push(
backendRow(_('v2raya_core (required)'), backends.core_path, backends.core_version, null,
_('This is the only core v2rayA 2.5.x supports.')),
backendRow(_('Xray-core'), backends.xray_path, backends.xray_version, backends.xray_package,
_('Left untouched. Do not point the core binary setting at this.')),
backendRow(_('v2ray-core'), backends.v2ray_path, backends.v2ray_version, backends.v2ray_package,
_('Left untouched. Do not point the core binary setting at this.')),
backendRow(_('sing-box'), backends.singbox_path, backends.singbox_version, backends.singbox_package,
_('Left untouched. sing-box uses a different configuration format and cannot run v2rayA configs.')),
E('div', { class: 'cbi-value' }, [
E('label', { class: 'cbi-value-title' }, _('Rule data')),
E('div', { class: 'cbi-value-field' },
(backends.geoip_present === '1' && backends.geosite_present === '1')
? _('geoip.dat and geosite.dat are installed')
: _('Missing. v2rayA downloads the data on first start and needs internet access; install v2raya-geoip and v2raya-geosite for offline use.'))
]),
E('div', { class: 'cbi-value' }, [
E('label', { class: 'cbi-value-title' }, _('Other proxy services')),
E('div', { class: 'cbi-value-field' },
backends.services_present ? backends.services_present.split(',').join(', ') : _('None detected'))
])
);
}
else {
backendChildren.push(E('p', { class: 'cbi-section-descr' },
_('Core and service detection is unavailable. The installed v2raya package does not provide the detection helper; version 2.5.x and later install it.')));
}
if (detectAvailable && !backends.core_path)
backendChildren.push(E('div', { class: 'alert-message warning' },
_('v2raya_core is missing. Install the v2raya-core package.')));
if (detectAvailable && backends.old_v2raya_package === '1')
backendChildren.push(E('div', { class: 'alert-message warning' },
_('An older v2raya package is still installed. Remove it to avoid duplicate services.')));
if (detectAvailable && enabledServices.length)
backendChildren.push(E('div', { class: 'alert-message warning' },
_('These proxy services are enabled at boot: %s. Run only one transparent-proxy stack, otherwise duplicate firewall rules will conflict.').format(enabledServices.join(', '))));
var status = E('span', { id: 'v2raya-status', style: 'font-weight:bold' }, _('Checking…'));
var startBtn = E('button', { class: 'btn cbi-button cbi-button-apply' }, _('Start'));
var stopBtn = E('button', { class: 'btn cbi-button cbi-button-reset' }, _('Stop'));
var restartBtn = E('button', { class: 'btn cbi-button cbi-button-action' }, _('Restart'));
startBtn.addEventListener('click', function () { startService(startBtn); });
stopBtn.addEventListener('click', function () { runAction('stop', stopBtn); });
restartBtn.addEventListener('click', function () { runAction('restart', restartBtn); });
var refresh = function () {
return serviceRunning().then(renderStatus);
};
refresh();
poll.add(refresh, 5);
return E('div', { class: 'cbi-map' }, [
E('h2', {}, _('v2rayA')),
E('div', { class: 'cbi-map-descr' }, [
_('A web client for its own Xray-based core, with transparent proxy support.'),
' ',
E('a', { href: weburl, target: '_blank', rel: 'noreferrer noopener' }, weburl)
]),
E('div', { class: 'cbi-section' }, [
E('div', { class: 'cbi-value' }, [
E('label', { class: 'cbi-value-title' }, _('Service')),
E('div', { class: 'cbi-value-field' }, status)
]),
E('div', { class: 'cbi-value' }, [
E('label', { class: 'cbi-value-title' }, _('v2rayA version')),
E('div', { class: 'cbi-value-field' }, serviceVersion)
]),
E('div', { class: 'cbi-value' }, [
E('label', { class: 'cbi-value-title' }, _('v2raya_core version')),
E('div', { class: 'cbi-value-field' }, coreVersion)
]),
E('div', { class: 'cbi-value' }, [
E('label', { class: 'cbi-value-title' }, _('Listening address')),
E('div', { class: 'cbi-value-field' }, address)
]),
E('div', { class: 'cbi-value' }, [
E('label', { class: 'cbi-value-title' }, _('Web interface')),
E('div', { class: 'cbi-value-field' },
E('a', { href: weburl, target: '_blank', rel: 'noreferrer noopener' }, _('Open v2rayA')))
])
]),
E('div', { class: 'cbi-section' }, backendChildren),
E('div', { class: 'cbi-section' }, [
E('div', { class: 'cbi-value' }, [
E('label', { class: 'cbi-value-title' }, _('Actions')),
E('div', { class: 'cbi-value-field' }, [ startBtn, ' ', stopBtn, ' ', restartBtn ])
]),
E('p', { class: 'cbi-section-descr' },
_('These buttons control the background service. Connecting the proxy is done in the v2rayA web UI with the Connect button.'))
]),
E('div', { class: 'cbi-section' }, [
E('p', { class: 'cbi-section-descr' },
_('This independent, community-run packaging project is not affiliated with the upstream v2rayA authors. It is AI-assisted and human-maintained.'))
])
]);
},
handleSave: null,
handleSaveApply: null,
handleReset: null
});