🎈 Sync 2026-09-09 01:06:03

This commit is contained in:
github-actions[bot]
2026-09-09 01:06:03 +08:00
parent b6b067e0ad
commit 302b45db1b
35 changed files with 5887 additions and 0 deletions
+60
View File
@@ -0,0 +1,60 @@
#
# Copyright (C) 2026 HAProxy Manager contributors
#
# This is free software, licensed under the MIT License.
#
include $(TOPDIR)/rules.mk
PKG_VERSION:=0.6.1
PKG_RELEASE:=1
PKG_LICENSE:=MIT
PKG_MAINTAINER:=Philipp Shklyaev <job@umk0.ru>
LUCI_MAINTAINER:=$(PKG_MAINTAINER)
LUCI_TITLE:=LuCI HAProxy Manager
LUCI_DESCRIPTION:=Manage Web and TCP HAProxy services, firewall access, and recovery points.
LUCI_DEPENDS:=+luci-base +rpcd +rpcd-mod-file +haproxy
LUCI_PKGARCH:=all
define Package/luci-app-haproxy-manager/conffiles
/etc/config/haproxy_manager
endef
define Package/luci-app-haproxy-manager/postinst
#!/bin/sh
[ -n "$${IPKG_INSTROOT}" ] || {
/usr/libexec/haproxy-manager/migrate >/dev/null 2>&1 || logger -t haproxy-manager "Configuration migration failed"
rm -f /tmp/luci-indexcache.* 2>/dev/null || true
rm -rf /tmp/luci-modulecache/ 2>/dev/null || true
/etc/init.d/rpcd reload >/dev/null 2>&1 || true
/usr/libexec/haproxy-manager/recover package install >/dev/null 2>&1 &
}
exit 0
endef
define Package/luci-app-haproxy-manager/prerm
#!/bin/sh
case "$${PKG_UPGRADE:-0}:$${1:-}" in
1:*|*:upgrade) exit 0 ;;
esac
[ -n "$${IPKG_INSTROOT}" ] || {
/usr/libexec/haproxy-manager/uninstall >/dev/null 2>&1 ||
logger -t haproxy-manager "Package runtime cleanup failed"
}
exit 0
endef
define Package/luci-app-haproxy-manager/postrm
#!/bin/sh
[ -n "$${IPKG_INSTROOT}" ] || {
rm -f /tmp/luci-indexcache.* 2>/dev/null || true
rm -rf /tmp/luci-modulecache/ 2>/dev/null || true
/etc/init.d/rpcd reload >/dev/null 2>&1 || true
}
exit 0
endef
include $(TOPDIR)/feeds/luci/luci.mk
# call BuildPackage - OpenWrt buildroot signature
@@ -0,0 +1,305 @@
.hm-toolbar {
display: flex;
align-items: center;
gap: 0.75rem;
margin: 0 0 1.25rem;
}
.hm-filter {
appearance: none;
box-sizing: border-box;
width: min(100%, 28rem);
min-height: 2.5rem;
padding: 0.5rem 0.75rem;
border: 1px solid var(--border-color-medium, rgba(128, 128, 128, 0.45));
border-radius: 4px;
outline: 0;
background: var(--background-color-high, rgba(128, 128, 128, 0.06));
color: inherit;
font: inherit;
}
.hm-filter:focus {
border-color: var(--primary-color-medium, currentColor);
box-shadow: 0 0 0 2px rgba(128, 128, 128, 0.18);
}
.hm-route-count {
white-space: nowrap;
opacity: 0.72;
}
.hm-endpoint {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.5rem;
min-width: 0;
}
.hm-endpoint-value,
.hm-destination {
overflow-wrap: anywhere;
}
.hm-port-summary {
opacity: 0.72;
white-space: nowrap;
}
.hm-port-list {
display: flex;
flex-wrap: wrap;
gap: 0.35rem;
}
.hm-port-list code,
.hm-destination code {
white-space: nowrap;
}
.hm-destination {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.hm-badge,
.hm-state {
display: inline-flex;
align-items: center;
width: max-content;
min-height: 1.6rem;
padding: 0 0.45rem;
border: 1px solid rgba(128, 128, 128, 0.45);
border-radius: 4px;
font-size: 0.82em;
font-weight: 600;
line-height: 1.2;
white-space: nowrap;
}
.hm-state-off {
opacity: 0.6;
}
.hm-state-danger {
border-color: var(--error-color, #b94a48);
color: var(--error-color, #b94a48);
}
.hm-filter-empty {
margin-top: 1rem;
}
.cbi-section-table-row[hidden],
.hm-filter-empty[hidden] {
display: none !important;
}
.hm-route-name {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.5rem;
min-width: 0;
}
.hm-service-cell {
display: flex;
align-items: flex-start;
flex-direction: column;
gap: 0.4rem;
}
.hm-mobile-only {
display: none;
}
.hm-actions {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
flex-wrap: wrap;
}
.hm-notification {
margin: 0;
white-space: pre-wrap;
word-break: break-word;
}
.hm-incident-report {
box-sizing: border-box;
max-height: min(60vh, 36rem);
margin: 1rem 0;
overflow: auto;
white-space: pre-wrap;
word-break: break-word;
}
.hm-inline-alert {
margin-bottom: 1rem;
}
.hm-status-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 1px;
margin: 1.25rem 0 2rem;
border: 1px solid rgba(128, 128, 128, 0.35);
border-radius: 4px;
overflow: hidden;
}
.hm-status-item {
min-width: 0;
padding: 1rem;
background: var(--background-color-low, rgba(128, 128, 128, 0.08));
}
.hm-status-label {
display: block;
margin-bottom: 0.35rem;
font-size: 0.85em;
opacity: 0.72;
}
.hm-status-value {
display: block;
overflow-wrap: anywhere;
font-size: 1.05em;
font-weight: 600;
}
.hm-listener-table code {
white-space: nowrap;
}
.hm-section-heading {
margin-top: 2rem;
}
.hm-section-title-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
}
.hm-section-title-row h3 {
margin: 0;
}
.hm-section-heading > p {
margin: 0.25rem 0 0.75rem;
}
.hm-backup-latest {
margin-left: 0.5rem;
}
.hm-backup-table .cbi-section-actions {
text-align: right;
}
.hm-raw-editor {
box-sizing: border-box;
width: 100%;
min-height: 32rem;
font-family: monospace;
line-height: 1.45;
tab-size: 2;
}
@media (max-width: 900px) {
.hm-status-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 600px) {
.hm-toolbar {
align-items: stretch;
flex-direction: column;
}
.hm-filter {
width: 100%;
}
.hm-status-grid {
grid-template-columns: 1fr;
}
.hm-section-title-row {
align-items: stretch;
flex-direction: column;
}
.hm-section-title-row .cbi-button {
width: 100%;
}
.hm-actions > .btn,
.hm-actions > .cbi-button {
flex: 1 1 100%;
}
.hm-mobile-only {
display: inline-flex;
}
.cbi-section-table-row > .td[data-name="_status"] {
display: none !important;
}
.hm-route-table .cbi-section-table-row[data-sid] {
display: grid !important;
grid-template-columns: minmax(0, 1fr) auto;
gap: 0.75rem 1rem;
padding: 1rem !important;
}
.hm-route-table .cbi-section-table-row[data-sid] > .td {
display: block;
box-sizing: border-box;
width: auto !important;
min-width: 0 !important;
padding: 0 !important;
border: 0 !important;
}
.hm-route-table .cbi-section-table-row[data-sid] > .td::before {
display: none !important;
content: none !important;
}
.hm-route-table .cbi-section-table-row[data-sid] > .td[data-name="_service"],
.hm-route-table .cbi-section-table-row[data-sid] > .td[data-name="_endpoint"] {
grid-column: 1 / -1;
}
.hm-route-table .cbi-section-table-row[data-sid] > .td[data-name="_destination"] {
align-self: center;
grid-column: 1 / -1;
}
.hm-route-table .cbi-section-table-row[data-sid] > .cbi-section-actions {
align-self: center;
grid-column: 1 / -1;
}
.hm-route-table .cbi-section-table-row[data-sid] > .td[data-name="_firewall"] {
grid-column: 1 / -1;
}
.hm-route-table .cbi-section-table-row[data-sid] > .cbi-section-actions > div {
display: flex;
gap: 0.35rem;
}
.hm-raw-editor {
min-height: 24rem;
}
}
@@ -0,0 +1,97 @@
'use strict';
'require baseclass';
'require fs';
'require rpc';
'require ui';
var callUciCommit = rpc.declare({
object: 'uci',
method: 'commit',
params: [ 'config' ],
reject: true
});
var APPLY_LOCK_BUSY = 75;
function recoveryId(output) {
var path = String(output || '').trim();
var id = path.split('/').pop();
if (!/^\d{8}-\d{6}$/.test(id))
throw new Error(_('Unable to create a recovery point.'));
return id;
}
return baseclass.extend({
ensureStyles: function() {
if (document.querySelector('link[data-haproxy-manager-style]'))
return;
document.head.appendChild(E('link', {
'rel': 'stylesheet',
'href': L.resource('haproxy-manager/style.css'),
'data-haproxy-manager-style': '1'
}));
},
notify: function(message, level) {
ui.addNotification(null, E('pre', { 'class': 'hm-notification' }, String(message || '').trim()), level || 'info');
},
notifyError: function(error) {
this.notify(error && error.message ? error.message : String(error), 'danger');
},
exec: function(path, args) {
return fs.exec(path, args || []).then(function(result) {
if (!result.code)
return result;
var message = String(result.stderr || result.stdout ||
_('Command failed with code %d.').format(result.code)).trim();
var error = new Error(message);
error.code = result.code;
error.stdout = result.stdout;
error.stderr = result.stderr;
throw error;
});
},
commitAndApply: function() {
var backupId;
var committed = false;
return this.exec('/usr/libexec/haproxy-manager/backup', []).then(function(result) {
backupId = recoveryId(result.stdout);
return callUciCommit('haproxy_manager');
}).then(function() {
committed = true;
return this.exec('/usr/libexec/haproxy-manager/apply', [ '--backup', backupId ]);
}.bind(this)).catch(function(error) {
if (!committed || !backupId || error.code !== APPLY_LOCK_BUSY)
throw error;
return this.exec('/usr/libexec/haproxy-manager/rollback', [ backupId ]).catch(function() {
return null;
}).then(function() {
throw error;
});
}.bind(this));
},
saveAndApply: function(map) {
return map.save(null, true).then(function() {
return this.commitAndApply();
}.bind(this));
},
notifyApplied: function() {
this.notify(_('Changes saved and applied.'), 'info');
},
reloadAfterApply: function(delay) {
window.setTimeout(function() {
window.location.reload();
}, delay || 2500);
}
});
@@ -0,0 +1,88 @@
'use strict';
/* global hmUi */
'require view';
'require fs';
'require ui';
'require haproxy-manager.ui as hmUi';
return view.extend({
load: function() {
return Promise.all([
fs.read('/etc/haproxy.cfg').catch(function() {
return '';
})
]);
},
applyRaw: function(textarea) {
ui.showModal(_('Apply raw configuration?'), [
E('p', _('The file will be validated and backed up before HAProxy restarts.')),
E('div', { 'class': 'right' }, [
E('button', {
'class': 'btn cbi-button',
'type': 'button',
'click': ui.hideModal
}, _('Cancel')),
' ',
E('button', {
'class': 'btn cbi-button cbi-button-apply',
'type': 'button',
'click': ui.createHandlerFn(this, function() {
return fs.write('/tmp/haproxy-manager-raw.cfg', textarea.value).then(function() {
return hmUi.exec('/usr/libexec/haproxy-manager/apply-raw-file', [ '/tmp/haproxy-manager-raw.cfg' ]);
}).then(function(r) {
ui.hideModal();
hmUi.notify(r.stdout || _('Applied'), 'info');
}).catch(function(err) {
ui.hideModal();
hmUi.notifyError(err);
});
})
}, _('Apply'))
])
]);
},
render: function(data) {
var cfg = data[0];
var textarea = E('textarea', {
'id': 'haproxy-raw-config',
'class': 'cbi-input-textarea hm-raw-editor',
'spellcheck': 'false',
'aria-label': _('Raw HAProxy configuration')
}, cfg || '');
hmUi.ensureStyles();
return E('div', { 'class': 'cbi-map' }, [
E('h2', _('Raw HAProxy Config')),
E('div', { 'class': 'cbi-map-descr' }, _('Expert editor for /etc/haproxy.cfg.')),
E('div', { 'class': 'alert-message warning' }, _('Generated routes can overwrite manual changes to this file.')),
textarea,
E('div', { 'class': 'cbi-page-actions hm-actions' }, [
E('button', {
'class': 'btn cbi-button cbi-button-action',
'type': 'button',
'click': ui.createHandlerFn(this, function() {
return fs.write('/tmp/haproxy-manager-raw.cfg', textarea.value).then(function() {
return hmUi.exec('/usr/libexec/haproxy-manager/validate', [ '/tmp/haproxy-manager-raw.cfg' ]);
}).then(function(r) {
hmUi.notify(r.stdout || _('Config is valid'), 'info');
}).catch(function(err) {
hmUi.notifyError(err);
});
})
}, _('Validate')),
E('button', {
'class': 'btn cbi-button cbi-button-apply',
'type': 'button',
'click': ui.createHandlerFn(this, function() { this.applyRaw(textarea); })
}, _('Apply raw config'))
])
]);
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});
@@ -0,0 +1,464 @@
'use strict';
/* global hmUi */
'require view';
'require dom';
'require form';
'require ui';
'require haproxy-manager.ui as hmUi';
function listValue(value) {
if (Array.isArray(value))
return value;
if (value == null || value === '')
return [];
return String(value).trim().split(/\s+/);
}
function routeKind(map, sectionId) {
return map.data.get('haproxy_manager', sectionId, 'kind') ||
map.data.get('haproxy_manager', sectionId, 'protocol') || 'web';
}
function kindLabel(kind) {
switch (kind) {
case 'web': return _('Web');
case 'ssh': return _('SSH');
case 'rdp': return _('Remote Desktop');
case 'custom': return _('Custom TCP');
case 'http': return _('HTTP only');
case 'https': return _('HTTPS only');
case 'both': return _('Legacy Web');
default: return _('TCP');
}
}
function portMapIsValid(value) {
var match = String(value || '').match(/^(\d+):(\d+)$/);
return !!match && +match[1] >= 1 && +match[1] <= 65535 && +match[2] >= 1 && +match[2] <= 65535;
}
function parseFirewall(output) {
var result = { ports: {}, conflicts: [], conflict_count: '0', enabled: '0', policy: '', managed_rule: '0' };
String(output || '').split(/\r?\n/).forEach(function(line) {
var fields = line.split('\t');
if (fields[0] == 'port')
result.ports[fields[1]] = true;
else if (fields[0] == 'conflict')
result.conflicts.push({ section: fields[1], name: fields[2], ports: fields[3] });
else if (fields[0] == 'conflicts')
result.conflict_count = fields[1] || '0';
else if (fields[0])
result[fields[0]] = fields.slice(1).join('\t');
});
return result;
}
function specContains(spec, port) {
return String(spec || '').split(/[\s,]+/).some(function(token) {
var range = token.split('-').map(Number);
return range.length == 1 ? range[0] == port : port >= range[0] && port <= range[1];
});
}
function routePorts(map, sectionId, kind) {
var main = function(option, fallback) {
return map.data.get('haproxy_manager', 'main', option) || fallback;
};
switch (kind) {
case 'web':
return [
map.data.get('haproxy_manager', sectionId, 'web_http') == '0' ? null : main('http_port', '80'),
map.data.get('haproxy_manager', sectionId, 'web_https') == '0' ? null : main('https_port', '443')
].filter(Boolean);
case 'ssh':
return [map.data.get('haproxy_manager', sectionId, 'ssh_listen_port') || '22'];
case 'rdp':
return [map.data.get('haproxy_manager', sectionId, 'rdp_listen_port') || '3389'];
case 'custom':
return listValue(map.data.get('haproxy_manager', sectionId, 'port_map')).map(function(item) {
return item.split(':')[0];
});
case 'http': return [main('http_port', '80')];
case 'https': return [main('https_port', '443')];
case 'both': return [main('http_port', '80'), main('https_port', '443')];
default:
return [map.data.get('haproxy_manager', sectionId, 'listen_port') ||
map.data.get('haproxy_manager', sectionId, 'backend_port')].filter(Boolean);
}
}
return view.extend({
load: function() {
return Promise.all([
hmUi.exec('/usr/libexec/haproxy-manager/firewall-plan', []).catch(function() {
return { stdout: '' };
})
]);
},
render: function(data) {
var m, s, o;
var firewall = parseFirewall(data[0].stdout);
hmUi.ensureStyles();
m = new form.Map('haproxy_manager', _('HAProxy Services'),
_('Publish Web, SSH, Remote Desktop, and custom TCP services from one place.'));
s = m.section(form.GridSection, 'route', _('Services'));
s.anonymous = true;
s.addremove = true;
s.sortable = true;
s.nodescriptions = true;
s.addbtntitle = _('Add service');
o = s.option(form.Flag, 'enabled', _('Enabled'));
o.default = '1';
o.modalonly = true;
o = s.option(form.Value, 'name', _('Service name'));
o.placeholder = _('My service');
o.rmempty = true;
o.modalonly = true;
o = s.option(form.ListValue, 'kind', _('Service type'));
o.value('web', _('Web (HTTP + HTTPS)'));
o.value('ssh', _('SSH'));
o.value('rdp', _('Remote Desktop'));
o.value('custom', _('Custom TCP'));
o.default = 'web';
o.rmempty = false;
o.modalonly = true;
o = s.option(form.Value, 'host', _('Domain'));
o.placeholder = 'example.org';
o.datatype = 'hostname';
o.rmempty = false;
o.modalonly = true;
o.depends('kind', 'web');
o = s.option(form.Value, 'backend_host', _('Destination host'));
o.placeholder = '192.0.2.10';
o.datatype = 'or(ipaddr,hostname)';
o.rmempty = false;
o.modalonly = true;
o = s.option(form.Flag, 'web_http', _('Publish HTTP'));
o.default = '1';
o.modalonly = true;
o.depends('kind', 'web');
o = s.option(form.Value, 'backend_http_port', _('Destination HTTP port'));
o.datatype = 'port';
o.default = '80';
o.rmempty = false;
o.modalonly = true;
o.depends({ kind: 'web', web_http: '1' });
o = s.option(form.Flag, 'web_https', _('Publish HTTPS'));
o.default = '1';
o.modalonly = true;
o.depends('kind', 'web');
o = s.option(form.Value, 'backend_https_port', _('Destination HTTPS port'));
o.datatype = 'port';
o.default = '443';
o.rmempty = false;
o.modalonly = true;
o.depends({ kind: 'web', web_https: '1' });
o = s.option(form.Value, 'ssh_listen_port', _('Public SSH port'));
o.datatype = 'port';
o.default = '22';
o.rmempty = false;
o.modalonly = true;
o.depends('kind', 'ssh');
o = s.option(form.Value, 'ssh_backend_port', _('Destination SSH port'));
o.datatype = 'port';
o.default = '22';
o.rmempty = false;
o.modalonly = true;
o.depends('kind', 'ssh');
o = s.option(form.Value, 'rdp_listen_port', _('Public RDP port'));
o.datatype = 'port';
o.default = '3389';
o.rmempty = false;
o.modalonly = true;
o.depends('kind', 'rdp');
o = s.option(form.Value, 'rdp_backend_port', _('Destination RDP port'));
o.datatype = 'port';
o.default = '3389';
o.rmempty = false;
o.modalonly = true;
o.depends('kind', 'rdp');
o = s.option(form.DynamicList, 'port_map', _('TCP port mappings'));
o.placeholder = '8443:443';
o.rmempty = false;
o.modalonly = true;
o.depends('kind', 'custom');
o.validate = function(sectionId, value) {
return portMapIsValid(value) || _('Use the public:destination format, for example 8443:443.');
};
o = s.option(form.DummyValue, '_status', _('Status'));
o.modalonly = false;
o.textvalue = function(sectionId) {
var enabled = m.data.get('haproxy_manager', sectionId, 'enabled') != '0';
return E('span', {
'class': 'hm-state %s'.format(enabled ? 'hm-state-on' : 'hm-state-off')
}, enabled ? _('On') : _('Off'));
};
o = s.option(form.DummyValue, '_service', _('Service'));
o.modalonly = false;
o.textvalue = function(sectionId) {
var kind = routeKind(m, sectionId);
var name = m.data.get('haproxy_manager', sectionId, 'name') ||
m.data.get('haproxy_manager', sectionId, 'host') || kindLabel(kind);
var enabled = m.data.get('haproxy_manager', sectionId, 'enabled') != '0';
return E('div', { 'class': 'hm-service-cell' }, [
E('div', { 'class': 'hm-route-name' }, [
E('strong', name),
E('span', {
'class': 'hm-state hm-mobile-only %s'.format(enabled ? 'hm-state-on' : 'hm-state-off')
}, enabled ? _('On') : _('Off'))
]),
E('span', { 'class': 'hm-badge' }, kindLabel(kind))
]);
};
o = s.option(form.DummyValue, '_endpoint', _('Public endpoint'));
o.modalonly = false;
o.textvalue = function(sectionId) {
var kind = routeKind(m, sectionId);
var ports = routePorts(m, sectionId, kind);
var host = m.data.get('haproxy_manager', sectionId, 'host');
if (kind == 'web' || kind == 'http' || kind == 'https' || kind == 'both') {
return E('div', { 'class': 'hm-endpoint' }, [
E('span', { 'class': 'hm-endpoint-value' }, host || _('Not set')),
E('span', { 'class': 'hm-port-summary' }, ports.map(function(port) { return ':' + port; }).join(' + '))
]);
}
return E('div', { 'class': 'hm-port-list' }, ports.map(function(port) {
return E('code', ':' + port);
}));
};
o = s.option(form.DummyValue, '_destination', _('Destination'));
o.modalonly = false;
o.textvalue = function(sectionId) {
var kind = routeKind(m, sectionId);
var host = m.data.get('haproxy_manager', sectionId, 'backend_host') || _('Not set');
var mappings = [];
if (kind == 'web') {
if (m.data.get('haproxy_manager', sectionId, 'web_http') != '0')
mappings.push(_('HTTP') + ' :' + (m.data.get('haproxy_manager', sectionId, 'backend_http_port') || '80'));
if (m.data.get('haproxy_manager', sectionId, 'web_https') != '0')
mappings.push(_('HTTPS') + ' :' + (m.data.get('haproxy_manager', sectionId, 'backend_https_port') || '443'));
}
else if (kind == 'ssh')
mappings.push(':' + (m.data.get('haproxy_manager', sectionId, 'ssh_backend_port') || '22'));
else if (kind == 'rdp')
mappings.push(':' + (m.data.get('haproxy_manager', sectionId, 'rdp_backend_port') || '3389'));
else if (kind == 'custom')
mappings = listValue(m.data.get('haproxy_manager', sectionId, 'port_map')).map(function(item) { return ':' + item.split(':')[1]; });
else
mappings.push(':' + (m.data.get('haproxy_manager', sectionId, 'backend_port') || ''));
return E('div', { 'class': 'hm-destination' }, [
E('code', host),
E('span', mappings.join(' / '))
]);
};
o = s.option(form.DummyValue, '_firewall', _('Firewall'));
o.modalonly = false;
o.textvalue = function(sectionId) {
var kind = routeKind(m, sectionId);
var ports = routePorts(m, sectionId, kind).map(Number);
var conflict = firewall.conflicts.some(function(item) {
return ports.some(function(port) { return specContains(item.ports, port); });
});
var state = firewall.enabled != '1' ? _('Manual') : conflict ? _('Conflict') :
(firewall.managed_rule == '1' || firewall.policy == 'ACCEPT' ? _('Ready') : _('Pending'));
return E('span', {
'class': 'hm-state %s'.format(conflict ? 'hm-state-danger' : firewall.enabled == '1' ? 'hm-state-on' : 'hm-state-off')
}, state);
};
function applied(reload) {
hmUi.notifyApplied();
if (reload)
hmUi.reloadAfterApply();
}
function applyMap(reload) {
return hmUi.saveAndApply(m).then(function() {
applied(reload);
}).catch(function(error) {
hmUi.notifyError(error);
});
}
var baseHandleDrop = s.handleDrop;
var baseHandleTouchEnd = s.handleTouchEnd;
s.handleModalSave = function(modalMap, ev) {
var mapNode = this.getActiveModalMap();
var activeMap = dom.findClassInstance(mapNode);
return activeMap.save(null, true).then(function() {
return hmUi.commitAndApply();
}).then(function() {
return this.handleModalCancel(modalMap, ev, true);
}.bind(this)).then(function() {
applied(true);
}).catch(function(error) {
hmUi.notifyError(error);
});
};
s.handleRemove = function(sectionId) {
ui.showModal(_('Delete service?'), [
E('p', _('The service will be removed from HAProxy and the firewall immediately.')),
E('div', { 'class': 'right' }, [
E('button', {
'class': 'btn cbi-button',
'click': ui.hideModal
}, _('Cancel')),
' ',
E('button', {
'class': 'btn cbi-button cbi-button-negative important',
'click': ui.createHandlerFn(this, function() {
ui.hideModal();
m.data.remove('haproxy_manager', sectionId);
return applyMap(true);
})
}, _('Delete and apply'))
])
]);
};
function applyReorder() {
window.setTimeout(function() {
applyMap(true);
}, 100);
}
s.handleDrop = function(ev) {
var shouldApply = !!(ev.currentTarget && ev.currentTarget.matches &&
ev.currentTarget.matches('.drag-over-above, .drag-over-below'));
var result = baseHandleDrop.call(this, ev);
if (shouldApply)
applyReorder();
return result;
};
s.handleTouchEnd = function(ev) {
var row = ev.target && ev.target.closest ? ev.target.closest('.tr') : null;
var shouldApply = !!(document.querySelector('.touchsort-element') && row &&
row.parentNode.querySelector('.drag-over-above, .drag-over-below'));
var result = baseHandleTouchEnd.call(this, ev);
if (shouldApply)
applyReorder();
return result;
};
return m.render().then(function(node) {
var filterInput = E('input', {
'id': 'haproxy-route-filter',
'class': 'cbi-input-text hm-filter',
'type': 'search',
'placeholder': _('Filter services'),
'aria-label': _('Filter services')
});
var countNode = E('span', { 'class': 'hm-route-count' });
var emptyNode = E('p', {
'class': 'alert-message notice hm-filter-empty',
'hidden': ''
}, _('No matching services'));
var toolbar = E('div', { 'class': 'hm-toolbar' }, [ filterInput, countNode ]);
var section = node.querySelector('.cbi-section');
function updateFilter() {
var query = filterInput.value.trim().toLowerCase();
var rows = node.querySelectorAll('.cbi-section-table-row[data-sid]');
var visible = 0;
for (var i = 0; i < rows.length; i++) {
var match = !query || rows[i].textContent.toLowerCase().indexOf(query) > -1;
rows[i].hidden = !match;
visible += match ? 1 : 0;
}
countNode.textContent = _('%d services').format(visible);
emptyNode.hidden = visible > 0 || !query;
}
filterInput.addEventListener('input', updateFilter);
if (section) {
var table = section.querySelector('.table');
if (table)
table.classList.add('hm-route-table');
section.insertBefore(toolbar, table || section.firstChild);
section.appendChild(emptyNode);
}
if (firewall.conflicts.length) {
node.insertBefore(E('div', { 'class': 'alert-message warning hm-inline-alert' }, [
E('strong', _('Firewall conflict detected.')),
' ',
E('a', { 'href': L.url('admin/services/haproxy-manager/settings') }, _('Review firewall settings'))
]), node.firstChild.nextSibling);
}
node.appendChild(E('div', { 'class': 'cbi-page-actions hm-actions' }, [
E('button', {
'class': 'btn cbi-button cbi-button-action',
'type': 'button',
'click': ui.createHandlerFn(this, function() {
return hmUi.exec('/usr/libexec/haproxy-manager/generate', [ '/tmp/haproxy-manager-preview.cfg' ]).then(function() {
return hmUi.exec('/usr/libexec/haproxy-manager/validate', [ '/tmp/haproxy-manager-preview.cfg' ]);
}).then(function() {
return hmUi.exec('/usr/libexec/haproxy-manager/firewall-plan', []);
}).then(function(r) {
var plan = parseFirewall(r.stdout);
hmUi.notify(plan.conflicts.length ?
_('Configuration is valid. Firewall conflicts: %d').format(plan.conflicts.length) :
_('Configuration and firewall are ready.'), plan.conflicts.length ? 'warning' : 'info');
}).catch(function(err) {
hmUi.notifyError(err);
});
})
}, _('Check configuration')),
E('button', {
'class': 'btn cbi-button cbi-button-apply',
'type': 'button',
'click': ui.createHandlerFn(this, function() {
return applyMap(true);
})
}, _('Synchronize now'))
]));
updateFilter();
return node;
}.bind(this));
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});
@@ -0,0 +1,136 @@
'use strict';
/* global hmUi */
'require view';
'require form';
'require ui';
'require haproxy-manager.ui as hmUi';
return view.extend({
render: function() {
var m, s, o;
hmUi.ensureStyles();
m = new form.Map('haproxy_manager', _('HAProxy Settings'),
_('Configure shared Web entry ports, router access, and firewall automation.'));
s = m.section(form.NamedSection, 'main', 'settings', _('HAProxy service'));
s.anonymous = true;
o = s.option(form.Flag, 'enabled', _('Enable managed configuration'));
o.default = '0';
o = s.option(form.Value, 'wan_interface', _('WAN network interface'));
o.default = 'wan';
o.datatype = 'uciname';
o.rmempty = false;
o.description = _('The selected logical interface is monitored for address changes. In multi-WAN setups, select the interface that receives public connections.');
o = s.option(form.Value, 'wan_bind_ip', _('WAN bind address'));
o.placeholder = _('Automatic');
o.default = 'auto';
o.datatype = 'or(ipaddr,hostname)';
o.rmempty = false;
s = m.section(form.NamedSection, 'main', 'settings', _('Web entry ports'));
s.anonymous = true;
s.description = _('Every Web service shares these public ports. SSH, Remote Desktop, and Custom TCP services use the ports configured on each service.');
o = s.option(form.Value, 'http_port', _('Public HTTP port'));
o.datatype = 'port';
o.default = '80';
o.rmempty = false;
o = s.option(form.Value, 'https_port', _('Public HTTPS port'));
o.datatype = 'port';
o.default = '443';
o.rmempty = false;
s = m.section(form.NamedSection, 'main', 'settings', _('Router access'));
s.anonymous = true;
o = s.option(form.Flag, 'manage_uhttpd_bind', _('Keep LuCI on the LAN address'));
o.default = '0';
o.description = _('Recommended when HAProxy uses public ports 80 or 443. LuCI remains available from the local network.');
o = s.option(form.Value, 'lan_bind_ip', _('LuCI LAN address'));
o.placeholder = _('Automatic');
o.default = 'auto';
o.datatype = 'or(ipaddr,hostname)';
o.depends('manage_uhttpd_bind', '1');
s = m.section(form.NamedSection, 'main', 'settings', _('Firewall automation'));
s.anonymous = true;
o = s.option(form.Flag, 'manage_firewall', _('Open HAProxy ports on WAN automatically'));
o.default = '0';
o.description = _('Creates and updates only firewall rules owned by HAProxy Manager.');
o = s.option(form.Value, 'firewall_zone', _('WAN firewall zone'));
o.default = 'wan';
o.rmempty = false;
o.depends('manage_firewall', '1');
o = s.option(form.ListValue, 'firewall_conflict_mode', _('Existing port-forward conflicts'));
o.value('warn', _('Stop and show conflicts'));
o.value('disable', _('Disable conflicting forwards during apply'));
o.default = 'warn';
o.rmempty = false;
o.depends('manage_firewall', '1');
s = m.section(form.NamedSection, 'main', 'settings', _('Recovery'));
s.anonymous = true;
s.description = _('A restorable snapshot is created before migration and every apply.');
o = s.option(form.Flag, 'auto_recover', _('Recover HAProxy after networking becomes ready'));
o.default = '1';
o.description = _('Reconciles generated listeners after WAN address changes, retries a stopped service, and retains diagnostics for the seven latest incidents. Raw configurations are never overwritten.');
o = s.option(form.Flag, 'webhook_enabled', _('Send recovery webhook notifications'));
o.default = '0';
o.description = _('Sends only the incident identifier, result, action, interface, and reason. Configuration, addresses, and diagnostic logs are never included.');
o = s.option(form.Value, 'webhook_url', _('Webhook URL'));
o.password = true;
o.rmempty = false;
o.depends('webhook_enabled', '1');
o.validate = function(sectionId, value) {
return /^https?:\/\/[^/\s]+(?:\/\S*)?$/.test(value || '') || _('Enter an HTTP or HTTPS URL without spaces.');
};
return m.render().then(function(node) {
var recoverySection = node.querySelectorAll('.cbi-section');
var lastSection = recoverySection.length ? recoverySection[recoverySection.length - 1] : null;
if (lastSection) {
lastSection.appendChild(E('p', [
E('a', {
'class': 'btn cbi-button',
'href': L.url('admin/services/haproxy-manager/status') + '#recovery'
}, _('Open recovery'))
]));
}
node.appendChild(E('div', { 'class': 'cbi-page-actions hm-actions' }, [
E('button', {
'class': 'btn cbi-button cbi-button-apply',
'type': 'button',
'click': ui.createHandlerFn(this, function() {
return hmUi.saveAndApply(m).then(function() {
hmUi.notifyApplied();
hmUi.reloadAfterApply();
}).catch(function(err) {
hmUi.notifyError(err);
});
})
}, _('Save settings'))
]));
return node;
}.bind(this));
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});
@@ -0,0 +1,294 @@
'use strict';
/* global hmUi */
'require view';
'require ui';
'require haproxy-manager.ui as hmUi';
function parseStatus(output) {
var status = { listeners: [] };
String(output || '').split(/\r?\n/).forEach(function(line) {
var fields = line.split('\t');
if (fields[0] == 'listener') {
status.listeners.push({
protocol: fields[1] || '',
address: fields[2] || '',
process: fields.slice(3).join('\t') || ''
});
}
else if (fields[0])
status[fields[0]] = fields.slice(1).join('\t');
});
return status;
}
function parseBackups(output) {
return String(output || '').split(/\r?\n/).map(function(line) {
var fields = line.split('\t');
return fields[0] == 'backup' ? { id: fields[1], latest: fields[2] == '1', files: fields[3] } : null;
}).filter(Boolean);
}
function parseIncidents(output) {
return String(output || '').split(/\r?\n/).map(function(line) {
var fields = line.split('\t');
return fields[0] == 'incident' ? {
id: fields[1],
latest: fields[2] == '1',
result: fields[3] || '',
action: fields[4] || '',
interface: fields[5] || '',
reason: fields[6] || ''
} : null;
}).filter(Boolean);
}
function parseFirewall(output) {
var result = { conflicts: '0', enabled: '0', policy: '' };
String(output || '').split(/\r?\n/).forEach(function(line) {
var fields = line.split('\t');
if (fields[0] && fields[0] != 'conflict' && fields[0] != 'port')
result[fields[0]] = fields.slice(1).join('\t');
});
return result;
}
function backupDate(id) {
var match = String(id || '').match(/^(\d{4})(\d{2})(\d{2})-(\d{2})(\d{2})(\d{2})$/);
return match ? '%s-%s-%s %s:%s:%s'.format(match[1], match[2], match[3], match[4], match[5], match[6]) : id;
}
function statusItem(label, value) {
return E('div', { 'class': 'hm-status-item' }, [
E('span', { 'class': 'hm-status-label' }, label),
E('span', { 'class': 'hm-status-value' }, value)
]);
}
return view.extend({
load: function() {
return Promise.all([
hmUi.exec('/usr/libexec/haproxy-manager/status', []).catch(function(err) {
return { stdout: '', stderr: err.message || String(err), code: 1 };
}),
hmUi.exec('/usr/libexec/haproxy-manager/backups', []).catch(function() { return { stdout: '' }; }),
hmUi.exec('/usr/libexec/haproxy-manager/firewall-plan', []).catch(function() { return { stdout: '' }; }),
hmUi.exec('/usr/libexec/haproxy-manager/incidents', []).catch(function() { return { stdout: '' }; })
]);
},
showIncident: function(incidentId) {
return hmUi.exec('/usr/libexec/haproxy-manager/incident', [ incidentId ]).then(function(result) {
ui.showModal(_('Incident diagnostics'), [
E('p', _('Diagnostic report for %s.').format(backupDate(incidentId))),
E('pre', { 'class': 'hm-incident-report' }, result.stdout || _('Not available')),
E('div', { 'class': 'right' }, E('button', {
'class': 'btn cbi-button cbi-button-action',
'type': 'button',
'click': ui.hideModal
}, _('Close')))
]);
}).catch(function(err) {
hmUi.notifyError(err);
});
},
showRestore: function(backupId) {
ui.showModal(_('Restore configuration?'), [
E('p', _('HAProxy, firewall, and LuCI settings will be restored from %s.').format(backupDate(backupId))),
E('div', { 'class': 'right' }, [
E('button', {
'class': 'btn cbi-button',
'type': 'button',
'click': ui.hideModal
}, _('Cancel')),
' ',
E('button', {
'class': 'btn cbi-button cbi-button-negative important',
'type': 'button',
'click': ui.createHandlerFn(this, function() {
return hmUi.exec('/usr/libexec/haproxy-manager/rollback', [ backupId ]).then(function(r) {
ui.hideModal();
hmUi.notify(r.stdout || _('Restored'), 'info');
hmUi.reloadAfterApply(3500);
}).catch(function(err) {
ui.hideModal();
hmUi.notifyError(err);
});
})
}, _('Restore'))
])
]);
},
render: function(data) {
var res = data[0];
var status = parseStatus(res.stdout);
var backups = parseBackups(data[1].stdout);
var firewall = parseFirewall(data[2].stdout);
var incidents = parseIncidents(data[3].stdout);
var running = status.service == 'running';
var autoRecovery = status.auto_recovery != '0';
var recoveryResults = {
'recovered': _('Recovered'),
'failed': _('Failed'),
'invalid-config': _('Invalid configuration'),
'backup-failed': _('Recovery point failed'),
'detected': _('In progress')
};
recoveryResults.reconciled = _('WAN address reconciled');
recoveryResults['rolled-back'] = _('Failed, configuration restored');
recoveryResults['rollback-failed'] = _('Failed, restore also failed');
recoveryResults['address-unavailable'] = _('WAN address unavailable');
recoveryResults['invalid-generated-config'] = _('Generated configuration is invalid');
recoveryResults['install-failed'] = _('Configuration installation failed');
var lastRecovery = status.last_incident ? '%s - %s%s'.format(
backupDate(status.last_incident),
recoveryResults[status.last_incident_result] || status.last_incident_result,
status.last_incident_interface ? ' (%s)'.format(status.last_incident_interface) : ''
) : _('No incidents recorded');
var firewallText = firewall.enabled != '1' ? _('Manual') : +firewall.conflicts > 0 ?
_('%d conflicts').format(+firewall.conflicts) : _('Managed');
var modes = {
generated: _('Generated routes'),
raw: _('Raw configuration'),
none: _('Not managed')
};
var listeners = status.listeners.map(function(listener) {
return E('tr', { 'class': 'tr' }, [
E('td', { 'class': 'td', 'data-title': _('Protocol') }, listener.protocol),
E('td', { 'class': 'td', 'data-title': _('Address') }, E('code', listener.address)),
E('td', { 'class': 'td', 'data-title': _('Process') }, listener.process)
]);
});
var backupRows = backups.map(function(backup) {
return E('tr', { 'class': 'tr' }, [
E('td', { 'class': 'td', 'data-title': _('Created') }, [
E('span', backupDate(backup.id)),
backup.latest ? E('span', { 'class': 'hm-badge hm-backup-latest' }, _('Latest')) : ''
]),
E('td', { 'class': 'td', 'data-title': _('Contents') }, _('%d configuration files').format(+backup.files || 0)),
E('td', { 'class': 'td cbi-section-actions' }, E('button', {
'class': 'btn cbi-button cbi-button-action',
'type': 'button',
'click': ui.createHandlerFn(this, function() { this.showRestore(backup.id); })
}, _('Restore')))
]);
}.bind(this));
var incidentRows = incidents.map(function(incident) {
var result = recoveryResults[incident.result] || incident.result || _('Not available');
var context = [ incident.action, incident.interface ].filter(Boolean).join(' / ');
return E('tr', { 'class': 'tr' }, [
E('td', { 'class': 'td', 'data-title': _('Created') }, [
E('span', backupDate(incident.id)),
incident.latest ? E('span', { 'class': 'hm-badge hm-backup-latest' }, _('Latest')) : ''
]),
E('td', { 'class': 'td', 'data-title': _('Result') }, result),
E('td', { 'class': 'td', 'data-title': _('Trigger') }, context || incident.reason || '-'),
E('td', { 'class': 'td cbi-section-actions' }, E('button', {
'class': 'btn cbi-button cbi-button-action',
'type': 'button',
'click': ui.createHandlerFn(this, function() { return this.showIncident(incident.id); })
}, _('View')))
]);
}.bind(this));
hmUi.ensureStyles();
if (!listeners.length) {
listeners.push(E('tr', { 'class': 'tr placeholder' }, [
E('td', { 'class': 'td', 'colspan': '3' }, _('No HAProxy or LuCI listeners detected.'))
]));
}
if (!backupRows.length) {
backupRows.push(E('tr', { 'class': 'tr placeholder' }, [
E('td', { 'class': 'td', 'colspan': '3' }, _('No recovery points yet.'))
]));
}
if (!incidentRows.length) {
incidentRows.push(E('tr', { 'class': 'tr placeholder' }, [
E('td', { 'class': 'td', 'colspan': '4' }, _('No incidents recorded'))
]));
}
return E('div', { 'class': 'cbi-map' }, [
E('h2', _('HAProxy Status')),
E('div', { 'class': 'cbi-map-descr' }, _('Service health, active listeners, and recovery points.')),
res.code && res.stderr ? E('div', { 'class': 'alert-message error' }, res.stderr) : '',
E('div', { 'class': 'hm-status-grid' }, [
statusItem(_('Service'), E('span', {
'class': 'hm-state %s'.format(running ? 'hm-state-on' : 'hm-state-off')
}, running ? _('Running') : _('Stopped'))),
statusItem(_('WAN address'), status.wan_ip || _('Not set')),
statusItem(_('HAProxy version'), status.version || _('Not available')),
statusItem(_('Automatic recovery'), E('span', {
'class': 'hm-state %s'.format(autoRecovery ? 'hm-state-on' : 'hm-state-off')
}, autoRecovery ? _('Enabled') : _('Disabled'))),
statusItem(_('Configuration mode'), modes[status.active_mode] || status.active_mode || modes.none),
statusItem(_('Last recovery'), lastRecovery),
statusItem(_('Firewall'), E('span', {
'class': 'hm-state %s'.format(+firewall.conflicts > 0 ? 'hm-state-danger' : firewall.enabled == '1' ? 'hm-state-on' : 'hm-state-off')
}, firewallText))
]),
E('h3', _('Active listeners')),
E('table', { 'class': 'table hm-listener-table' }, [
E('thead', {}, [ E('tr', { 'class': 'tr table-titles' }, [
E('th', { 'class': 'th' }, _('Protocol')),
E('th', { 'class': 'th' }, _('Address')),
E('th', { 'class': 'th' }, _('Process'))
]) ]),
E('tbody', {}, listeners)
]),
E('div', { 'class': 'hm-section-heading' }, [
E('h3', _('Recovery incidents')),
E('p', { 'class': 'cbi-section-descr' }, _('The seven latest automatic recovery reports are retained on the router.'))
]),
E('table', { 'class': 'table hm-incident-table' }, [
E('thead', {}, [ E('tr', { 'class': 'tr table-titles' }, [
E('th', { 'class': 'th' }, _('Created')),
E('th', { 'class': 'th' }, _('Result')),
E('th', { 'class': 'th' }, _('Trigger')),
E('th', { 'class': 'th' })
]) ]),
E('tbody', {}, incidentRows)
]),
E('div', { 'id': 'recovery', 'class': 'hm-section-heading' }, [
E('div', { 'class': 'hm-section-title-row' }, [
E('h3', _('Recovery points')),
E('button', {
'class': 'btn cbi-button cbi-button-add',
'type': 'button',
'click': ui.createHandlerFn(this, function() {
return hmUi.exec('/usr/libexec/haproxy-manager/backup', []).then(function() {
window.location.reload();
}).catch(function(err) { hmUi.notifyError(err); });
})
}, _('Create recovery point'))
]),
E('p', { 'class': 'cbi-section-descr' }, _('Restore HAProxy, firewall, and LuCI settings from a previous snapshot.'))
]),
E('table', { 'class': 'table hm-backup-table' }, [
E('thead', {}, [ E('tr', { 'class': 'tr table-titles' }, [
E('th', { 'class': 'th' }, _('Created')),
E('th', { 'class': 'th' }, _('Contents')),
E('th', { 'class': 'th' })
]) ]),
E('tbody', {}, backupRows)
]),
E('div', { 'class': 'cbi-page-actions hm-actions' }, [
E('button', {
'class': 'btn cbi-button cbi-button-action',
'type': 'button',
'click': function() { window.location.reload(); }
}, _('Refresh'))
])
]);
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});
@@ -0,0 +1,499 @@
msgid ""
msgstr ""
"Language: es\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
msgid "Command failed with code %d."
msgstr "El comando terminó con el código %d."
msgid "%d configuration files"
msgstr "%d archivos de configuración"
msgid "%d conflicts"
msgstr "%d conflictos"
msgid "%d services"
msgstr "%d servicios"
msgid "A restorable snapshot is created before migration and every apply."
msgstr "Se crea un punto de restauración antes de la migración y de cada aplicación."
msgid "Active listeners"
msgstr "Escuchas activas"
msgid "Add service"
msgstr "Añadir servicio"
msgid "Address"
msgstr "Dirección"
msgid "Applied"
msgstr "Aplicado"
msgid "Apply"
msgstr "Aplicar"
msgid "Apply changes"
msgstr "Aplicar cambios"
msgid "Apply raw config"
msgstr "Aplicar configuración"
msgid "Apply raw configuration?"
msgstr "¿Aplicar la configuración manual?"
msgid "Automatic"
msgstr "Automático"
msgid "Cancel"
msgstr "Cancelar"
msgid "Check configuration"
msgstr "Comprobar configuración"
msgid "Config is valid"
msgstr "La configuración es válida"
msgid "Configuration and firewall are ready."
msgstr "La configuración y el cortafuegos están listos."
msgid "Configuration is valid. Firewall conflicts: %d"
msgstr "La configuración es válida. Conflictos del cortafuegos: %d"
msgid "Configure shared Web entry ports, router access, and firewall automation."
msgstr "Configure los puertos Web compartidos, el acceso al router y la automatización del cortafuegos."
msgid "Conflict"
msgstr "Conflicto"
msgid "Contents"
msgstr "Contenido"
msgid "Create recovery point"
msgstr "Crear punto de restauración"
msgid "Created"
msgstr "Creado"
msgid "Creates and updates only firewall rules owned by HAProxy Manager."
msgstr "Crea y actualiza únicamente las reglas del cortafuegos gestionadas por HAProxy Manager."
msgid "Custom TCP"
msgstr "TCP personalizado"
msgid "Destination"
msgstr "Destino"
msgid "Destination host"
msgstr "Host de destino"
msgid "Destination HTTP port"
msgstr "Puerto HTTP de destino"
msgid "Destination HTTPS port"
msgstr "Puerto HTTPS de destino"
msgid "Destination RDP port"
msgstr "Puerto RDP de destino"
msgid "Destination SSH port"
msgstr "Puerto SSH de destino"
msgid "Disable conflicting forwards during apply"
msgstr "Desactivar redirecciones conflictivas al aplicar"
msgid "Domain"
msgstr "Dominio"
msgid "Enable managed configuration"
msgstr "Activar configuración administrada"
msgid "Enabled"
msgstr "Activado"
msgid "Every Web service shares these public ports. SSH, Remote Desktop, and Custom TCP services use the ports configured on each service."
msgstr "Todos los servicios Web comparten estos puertos públicos. SSH, Escritorio remoto y TCP personalizado usan los puertos configurados en cada servicio."
msgid "Existing port-forward conflicts"
msgstr "Conflictos con redirecciones existentes"
msgid "Expert editor for /etc/haproxy.cfg."
msgstr "Editor avanzado de /etc/haproxy.cfg."
msgid "Filter services"
msgstr "Filtrar servicios"
msgid "Firewall"
msgstr "Cortafuegos"
msgid "Firewall automation"
msgstr "Automatización del cortafuegos"
msgid "Firewall conflict detected."
msgstr "Se detectó un conflicto del cortafuegos."
msgid "Generated routes can overwrite manual changes to this file."
msgstr "Las rutas generadas pueden sobrescribir los cambios manuales de este archivo."
msgid "Grant access to LuCI HAProxy Manager"
msgstr "Conceder acceso a LuCI HAProxy Manager"
msgid "HAProxy"
msgstr "HAProxy"
msgid "HAProxy service"
msgstr "Servicio HAProxy"
msgid "HAProxy Services"
msgstr "Servicios HAProxy"
msgid "HAProxy Settings"
msgstr "Ajustes de HAProxy"
msgid "HAProxy Status"
msgstr "Estado de HAProxy"
msgid "HAProxy version"
msgstr "Versión de HAProxy"
msgid "HAProxy, firewall, and LuCI settings will be restored from %s."
msgstr "La configuración de HAProxy, del cortafuegos y de LuCI se restaurará desde %s."
msgid "HTTP"
msgstr "HTTP"
msgid "HTTP only"
msgstr "Solo HTTP"
msgid "HTTPS"
msgstr "HTTPS"
msgid "HTTPS only"
msgstr "Solo HTTPS"
msgid "Keep LuCI on the LAN address"
msgstr "Mantener LuCI en la dirección LAN"
msgid "Latest"
msgstr "Más reciente"
msgid "Legacy Web"
msgstr "Web heredado"
msgid "LuCI LAN address"
msgstr "Dirección LAN de LuCI"
msgid "Managed"
msgstr "Gestionado"
msgid "Manual"
msgstr "Manual"
msgid "My service"
msgstr "Mi servicio"
msgid "No HAProxy or LuCI listeners detected."
msgstr "No se detectaron escuchas de HAProxy o LuCI."
msgid "No matching services"
msgstr "No hay servicios coincidentes"
msgid "No recovery points yet."
msgstr "Aún no hay puntos de restauración."
msgid "Not available"
msgstr "No disponible"
msgid "Not set"
msgstr "Sin definir"
msgid "Off"
msgstr "Desactivado"
msgid "On"
msgstr "Activado"
msgid "Open HAProxy ports on WAN automatically"
msgstr "Abrir automáticamente los puertos de HAProxy en WAN"
msgid "Open recovery"
msgstr "Abrir recuperación"
msgid "Pending"
msgstr "Pendiente"
msgid "Process"
msgstr "Proceso"
msgid "Protocol"
msgstr "Protocolo"
msgid "Public endpoint"
msgstr "Punto de entrada público"
msgid "Public HTTP port"
msgstr "Puerto HTTP público"
msgid "Public HTTPS port"
msgstr "Puerto HTTPS público"
msgid "Public RDP port"
msgstr "Puerto RDP público"
msgid "Public SSH port"
msgstr "Puerto SSH público"
msgid "Publish HTTP"
msgstr "Publicar HTTP"
msgid "Publish HTTPS"
msgstr "Publicar HTTPS"
msgid "Publish Web, SSH, Remote Desktop, and custom TCP services from one place."
msgstr "Publique servicios Web, SSH, Escritorio remoto y TCP personalizado desde un solo lugar."
msgid "Raw Config"
msgstr "Configuración"
msgid "Raw HAProxy Config"
msgstr "Configuración de HAProxy"
msgid "Raw HAProxy configuration"
msgstr "Configuración de HAProxy"
msgid "Ready"
msgstr "Listo"
msgid "Recommended when HAProxy uses public ports 80 or 443. LuCI remains available from the local network."
msgstr "Recomendado cuando HAProxy usa los puertos públicos 80 o 443. LuCI seguirá disponible desde la red local."
msgid "Recovery"
msgstr "Recuperación"
msgid "Recovery points"
msgstr "Puntos de restauración"
msgid "Refresh"
msgstr "Actualizar"
msgid "Remote Desktop"
msgstr "Escritorio remoto"
msgid "Restore"
msgstr "Restaurar"
msgid "Restore configuration?"
msgstr "¿Restaurar la configuración?"
msgid "Restore HAProxy, firewall, and LuCI settings from a previous snapshot."
msgstr "Restaure la configuración de HAProxy, del cortafuegos y de LuCI desde una instantánea anterior."
msgid "Restored"
msgstr "Restaurado"
msgid "Review firewall settings"
msgstr "Revisar ajustes del cortafuegos"
msgid "Router access"
msgstr "Acceso al router"
msgid "Running"
msgstr "En ejecución"
msgid "Save and apply"
msgstr "Guardar y aplicar"
msgid "Save settings"
msgstr "Guardar ajustes"
msgid "Service"
msgstr "Servicio"
msgid "Service health, active listeners, and recovery points."
msgstr "Estado del servicio, listeners activos y puntos de restauración."
msgid "Service name"
msgstr "Nombre del servicio"
msgid "Service type"
msgstr "Tipo de servicio"
msgid "Services"
msgstr "Servicios"
msgid "Settings"
msgstr "Ajustes"
msgid "Settings saved"
msgstr "Ajustes guardados"
msgid "SSH"
msgstr "SSH"
msgid "Status"
msgstr "Estado"
msgid "Stop and show conflicts"
msgstr "Detener y mostrar conflictos"
msgid "Stopped"
msgstr "Detenido"
msgid "TCP"
msgstr "TCP"
msgid "TCP port mappings"
msgstr "Asignaciones de puertos TCP"
msgid "The file will be validated and backed up before HAProxy restarts."
msgstr "El archivo se validará y guardará antes de reiniciar HAProxy."
msgid "Use the public:destination format, for example 8443:443."
msgstr "Use el formato público:destino, por ejemplo 8443:443."
msgid "Validate"
msgstr "Validar"
msgid "WAN address"
msgstr "Dirección WAN"
msgid "WAN bind address"
msgstr "Dirección de enlace WAN"
msgid "WAN firewall zone"
msgstr "Zona WAN del cortafuegos"
msgid "Web"
msgstr "Web"
msgid "Web (HTTP + HTTPS)"
msgstr "Web (HTTP + HTTPS)"
msgid "Web entry ports"
msgstr "Puertos de entrada Web"
msgid "Changes saved and applied."
msgstr "Cambios guardados y aplicados."
msgid "Delete and apply"
msgstr "Eliminar y aplicar"
msgid "Delete service?"
msgstr "¿Eliminar el servicio?"
msgid "Synchronize now"
msgstr "Sincronizar ahora"
msgid "The service will be removed from HAProxy and the firewall immediately."
msgstr "El servicio se eliminará inmediatamente de HAProxy y del cortafuegos."
msgid "Unable to create a recovery point."
msgstr "No se pudo crear un punto de recuperación."
msgid "Automatic recovery"
msgstr "Recuperación automática"
msgid "Disabled"
msgstr "Desactivada"
msgid "Failed"
msgstr "Falló"
msgid "If HAProxy stops before a listener address exists, retry it after a network interface comes up and retain diagnostics for the seven latest incidents."
msgstr "Si HAProxy se detiene antes de que exista una dirección de escucha, reintentar tras activarse una interfaz de red y conservar el diagnóstico de los siete incidentes más recientes."
msgid "In progress"
msgstr "En curso"
msgid "Invalid configuration"
msgstr "Configuración no válida"
msgid "Last recovery"
msgstr "Última recuperación"
msgid "No incidents recorded"
msgstr "No se registraron incidentes"
msgid "Recover HAProxy after networking becomes ready"
msgstr "Recuperar HAProxy cuando la red esté lista"
msgid "Recovered"
msgstr "Recuperado"
msgid "Recovery point failed"
msgstr "Error al crear el punto de recuperación"
msgid "WAN network interface"
msgstr "Interfaz de red WAN"
msgid "Close"
msgstr "Cerrar"
msgid "Configuration mode"
msgstr "Modo de configuración"
msgid "Diagnostic report for %s."
msgstr "Informe de diagnóstico de %s."
msgid "Enter an HTTP or HTTPS URL without spaces."
msgstr "Introduzca una URL HTTP o HTTPS sin espacios."
msgid "Failed, configuration restored"
msgstr "Error; se restauró la configuración"
msgid "Failed, restore also failed"
msgstr "Error; la restauración también falló"
msgid "Generated routes"
msgstr "Rutas generadas"
msgid "Incident diagnostics"
msgstr "Diagnóstico del incidente"
msgid "Not managed"
msgstr "No gestionado"
msgid "Raw configuration"
msgstr "Configuración manual"
msgid "Reconciles generated listeners after WAN address changes, retries a stopped service, and retains diagnostics for the seven latest incidents. Raw configurations are never overwritten."
msgstr "Sincroniza los listeners generados tras cambios de la dirección WAN, vuelve a iniciar un servicio detenido y conserva el diagnóstico de los siete incidentes más recientes. Las configuraciones manuales nunca se sobrescriben."
msgid "Recovery incidents"
msgstr "Incidentes de recuperación"
msgid "Result"
msgstr "Resultado"
msgid "Send recovery webhook notifications"
msgstr "Enviar notificaciones webhook de recuperación"
msgid "Sends only the incident identifier, result, action, interface, and reason. Configuration, addresses, and diagnostic logs are never included."
msgstr "Solo envía el identificador del incidente, el resultado, la acción, la interfaz y el motivo. Nunca incluye la configuración, las direcciones ni los registros de diagnóstico."
msgid "The selected logical interface is monitored for address changes. In multi-WAN setups, select the interface that receives public connections."
msgstr "Se supervisan los cambios de dirección de la interfaz lógica seleccionada. En configuraciones multi-WAN, seleccione la interfaz que recibe conexiones públicas."
msgid "The seven latest automatic recovery reports are retained on the router."
msgstr "El router conserva los siete informes de recuperación automática más recientes."
msgid "Trigger"
msgstr "Desencadenante"
msgid "View"
msgstr "Ver"
msgid "WAN address reconciled"
msgstr "Dirección WAN sincronizada"
msgid "WAN address unavailable"
msgstr "Dirección WAN no disponible"
msgid "Webhook URL"
msgstr "URL del webhook"
msgid "Generated configuration is invalid"
msgstr "La configuración generada no es válida"
msgid "Configuration installation failed"
msgstr "No se pudo instalar la configuración"
@@ -0,0 +1,499 @@
msgid ""
msgstr ""
"Language: ja\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
msgid "Command failed with code %d."
msgstr "コマンドはコード %d で終了しました。"
msgid "%d configuration files"
msgstr "%d 個の設定ファイル"
msgid "%d conflicts"
msgstr "%d 件の競合"
msgid "%d services"
msgstr "%d 個のサービス"
msgid "A restorable snapshot is created before migration and every apply."
msgstr "移行前および変更を適用するたびに復元ポイントを作成します。"
msgid "Active listeners"
msgstr "有効なリスナー"
msgid "Add service"
msgstr "サービスを追加"
msgid "Address"
msgstr "アドレス"
msgid "Applied"
msgstr "適用しました"
msgid "Apply"
msgstr "適用"
msgid "Apply changes"
msgstr "変更を適用"
msgid "Apply raw config"
msgstr "設定を適用"
msgid "Apply raw configuration?"
msgstr "手動設定を適用しますか?"
msgid "Automatic"
msgstr "自動"
msgid "Cancel"
msgstr "キャンセル"
msgid "Check configuration"
msgstr "設定を確認"
msgid "Config is valid"
msgstr "設定は有効です"
msgid "Configuration and firewall are ready."
msgstr "設定とファイアウォールの準備が完了しました。"
msgid "Configuration is valid. Firewall conflicts: %d"
msgstr "設定は有効です。ファイアウォールの競合: %d"
msgid "Configure shared Web entry ports, router access, and firewall automation."
msgstr "共有 Web ポート、ルーターへのアクセス、ファイアウォールの自動化を設定します。"
msgid "Conflict"
msgstr "競合"
msgid "Contents"
msgstr "内容"
msgid "Create recovery point"
msgstr "復元ポイントを作成"
msgid "Created"
msgstr "作成日時"
msgid "Creates and updates only firewall rules owned by HAProxy Manager."
msgstr "HAProxy Manager が所有するファイアウォールルールのみを作成、更新します。"
msgid "Custom TCP"
msgstr "カスタム TCP"
msgid "Destination"
msgstr "転送先"
msgid "Destination host"
msgstr "転送先ホスト"
msgid "Destination HTTP port"
msgstr "転送先 HTTP ポート"
msgid "Destination HTTPS port"
msgstr "転送先 HTTPS ポート"
msgid "Destination RDP port"
msgstr "転送先 RDP ポート"
msgid "Destination SSH port"
msgstr "転送先 SSH ポート"
msgid "Disable conflicting forwards during apply"
msgstr "適用時に競合する転送を無効化"
msgid "Domain"
msgstr "ドメイン"
msgid "Enable managed configuration"
msgstr "管理対象の設定を有効化"
msgid "Enabled"
msgstr "有効"
msgid "Every Web service shares these public ports. SSH, Remote Desktop, and Custom TCP services use the ports configured on each service."
msgstr "すべての Web サービスはこれらの公開ポートを共有します。SSH、リモートデスクトップ、カスタム TCP は各サービスに設定したポートを使用します。"
msgid "Existing port-forward conflicts"
msgstr "既存のポート転送との競合"
msgid "Expert editor for /etc/haproxy.cfg."
msgstr "/etc/haproxy.cfg の上級者向けエディターです。"
msgid "Filter services"
msgstr "サービスを絞り込み"
msgid "Firewall"
msgstr "ファイアウォール"
msgid "Firewall automation"
msgstr "ファイアウォールの自動化"
msgid "Firewall conflict detected."
msgstr "ファイアウォールの競合を検出しました。"
msgid "Generated routes can overwrite manual changes to this file."
msgstr "生成されたルートにより、このファイルの手動変更が上書きされる場合があります。"
msgid "Grant access to LuCI HAProxy Manager"
msgstr "LuCI HAProxy Manager へのアクセスを許可"
msgid "HAProxy"
msgstr "HAProxy"
msgid "HAProxy service"
msgstr "HAProxy サービス"
msgid "HAProxy Services"
msgstr "HAProxy サービス"
msgid "HAProxy Settings"
msgstr "HAProxy 設定"
msgid "HAProxy Status"
msgstr "HAProxy ステータス"
msgid "HAProxy version"
msgstr "HAProxy バージョン"
msgid "HAProxy, firewall, and LuCI settings will be restored from %s."
msgstr "HAProxy、ファイアウォール、LuCI の設定を %s から復元します。"
msgid "HTTP"
msgstr "HTTP"
msgid "HTTP only"
msgstr "HTTP のみ"
msgid "HTTPS"
msgstr "HTTPS"
msgid "HTTPS only"
msgstr "HTTPS のみ"
msgid "Keep LuCI on the LAN address"
msgstr "LuCI を LAN アドレスに限定"
msgid "Latest"
msgstr "最新"
msgid "Legacy Web"
msgstr "旧形式 Web"
msgid "LuCI LAN address"
msgstr "LuCI LAN アドレス"
msgid "Managed"
msgstr "管理中"
msgid "Manual"
msgstr "手動"
msgid "My service"
msgstr "マイサービス"
msgid "No HAProxy or LuCI listeners detected."
msgstr "HAProxy または LuCI のリスナーが見つかりません。"
msgid "No matching services"
msgstr "一致するサービスはありません"
msgid "No recovery points yet."
msgstr "復元ポイントはまだありません。"
msgid "Not available"
msgstr "利用不可"
msgid "Not set"
msgstr "未設定"
msgid "Off"
msgstr "オフ"
msgid "On"
msgstr "オン"
msgid "Open HAProxy ports on WAN automatically"
msgstr "WAN の HAProxy ポートを自動的に開く"
msgid "Open recovery"
msgstr "復元を開く"
msgid "Pending"
msgstr "適用待ち"
msgid "Process"
msgstr "プロセス"
msgid "Protocol"
msgstr "プロトコル"
msgid "Public endpoint"
msgstr "公開エンドポイント"
msgid "Public HTTP port"
msgstr "公開 HTTP ポート"
msgid "Public HTTPS port"
msgstr "公開 HTTPS ポート"
msgid "Public RDP port"
msgstr "公開 RDP ポート"
msgid "Public SSH port"
msgstr "公開 SSH ポート"
msgid "Publish HTTP"
msgstr "HTTP を公開"
msgid "Publish HTTPS"
msgstr "HTTPS を公開"
msgid "Publish Web, SSH, Remote Desktop, and custom TCP services from one place."
msgstr "Web、SSH、リモートデスクトップ、カスタム TCP サービスを一か所から公開します。"
msgid "Raw Config"
msgstr "生設定"
msgid "Raw HAProxy Config"
msgstr "HAProxy 生設定"
msgid "Raw HAProxy configuration"
msgstr "HAProxy 生設定"
msgid "Ready"
msgstr "準備完了"
msgid "Recommended when HAProxy uses public ports 80 or 443. LuCI remains available from the local network."
msgstr "HAProxy が公開ポート 80 または 443 を使用する場合に推奨します。LuCI はローカルネットワークから利用できます。"
msgid "Recovery"
msgstr "復元"
msgid "Recovery points"
msgstr "復元ポイント"
msgid "Refresh"
msgstr "更新"
msgid "Remote Desktop"
msgstr "リモートデスクトップ"
msgid "Restore"
msgstr "復元"
msgid "Restore configuration?"
msgstr "設定を復元しますか?"
msgid "Restore HAProxy, firewall, and LuCI settings from a previous snapshot."
msgstr "以前のスナップショットから HAProxy、ファイアウォール、LuCI の設定を復元します。"
msgid "Restored"
msgstr "復元しました"
msgid "Review firewall settings"
msgstr "ファイアウォール設定を確認"
msgid "Router access"
msgstr "ルーターへのアクセス"
msgid "Running"
msgstr "実行中"
msgid "Save and apply"
msgstr "保存して適用"
msgid "Save settings"
msgstr "設定を保存"
msgid "Service"
msgstr "サービス"
msgid "Service health, active listeners, and recovery points."
msgstr "サービス状態、待受ポート、復元ポイントを表示します。"
msgid "Service name"
msgstr "サービス名"
msgid "Service type"
msgstr "サービス種別"
msgid "Services"
msgstr "サービス"
msgid "Settings"
msgstr "設定"
msgid "Settings saved"
msgstr "設定を保存しました"
msgid "SSH"
msgstr "SSH"
msgid "Status"
msgstr "ステータス"
msgid "Stop and show conflicts"
msgstr "停止して競合を表示"
msgid "Stopped"
msgstr "停止中"
msgid "TCP"
msgstr "TCP"
msgid "TCP port mappings"
msgstr "TCP ポートマッピング"
msgid "The file will be validated and backed up before HAProxy restarts."
msgstr "HAProxy の再起動前にファイルを検証し、バックアップします。"
msgid "Use the public:destination format, for example 8443:443."
msgstr "公開:転送先 の形式を使用してください。例: 8443:443"
msgid "Validate"
msgstr "検証"
msgid "WAN address"
msgstr "WAN アドレス"
msgid "WAN bind address"
msgstr "WAN バインドアドレス"
msgid "WAN firewall zone"
msgstr "WAN ファイアウォールゾーン"
msgid "Web"
msgstr "Web"
msgid "Web (HTTP + HTTPS)"
msgstr "Web (HTTP + HTTPS)"
msgid "Web entry ports"
msgstr "Web 公開ポート"
msgid "Changes saved and applied."
msgstr "変更を保存して適用しました。"
msgid "Delete and apply"
msgstr "削除して適用"
msgid "Delete service?"
msgstr "サービスを削除しますか?"
msgid "Synchronize now"
msgstr "今すぐ同期"
msgid "The service will be removed from HAProxy and the firewall immediately."
msgstr "サービスは HAProxy とファイアウォールから直ちに削除されます。"
msgid "Unable to create a recovery point."
msgstr "復元ポイントを作成できません。"
msgid "Automatic recovery"
msgstr "自動復旧"
msgid "Disabled"
msgstr "無効"
msgid "Failed"
msgstr "失敗"
msgid "If HAProxy stops before a listener address exists, retry it after a network interface comes up and retain diagnostics for the seven latest incidents."
msgstr "リスナーアドレスが利用可能になる前に HAProxy が停止した場合、ネットワークインターフェースの起動後に再試行し、最新 7 件の診断を保存します。"
msgid "In progress"
msgstr "進行中"
msgid "Invalid configuration"
msgstr "無効な設定"
msgid "Last recovery"
msgstr "最終復旧"
msgid "No incidents recorded"
msgstr "インシデントは記録されていません"
msgid "Recover HAProxy after networking becomes ready"
msgstr "ネットワーク準備完了後に HAProxy を復旧"
msgid "Recovered"
msgstr "復旧済み"
msgid "Recovery point failed"
msgstr "復旧ポイントの作成に失敗"
msgid "WAN network interface"
msgstr "WAN ネットワークインターフェース"
msgid "Close"
msgstr "閉じる"
msgid "Configuration mode"
msgstr "設定モード"
msgid "Diagnostic report for %s."
msgstr "%s の診断レポートです。"
msgid "Enter an HTTP or HTTPS URL without spaces."
msgstr "空白を含まない HTTP または HTTPS URL を入力してください。"
msgid "Failed, configuration restored"
msgstr "失敗したため設定を復元しました"
msgid "Failed, restore also failed"
msgstr "失敗し、復元にも失敗しました"
msgid "Generated routes"
msgstr "生成されたルート"
msgid "Incident diagnostics"
msgstr "インシデント診断"
msgid "Not managed"
msgstr "管理対象外"
msgid "Raw configuration"
msgstr "手動設定"
msgid "Reconciles generated listeners after WAN address changes, retries a stopped service, and retains diagnostics for the seven latest incidents. Raw configurations are never overwritten."
msgstr "WAN アドレスの変更後に生成済みリスナーを同期し、停止したサービスを再試行して、直近 7 件のインシデント診断を保持します。手動設定は上書きされません。"
msgid "Recovery incidents"
msgstr "復旧インシデント"
msgid "Result"
msgstr "結果"
msgid "Send recovery webhook notifications"
msgstr "復旧 Webhook 通知を送信"
msgid "Sends only the incident identifier, result, action, interface, and reason. Configuration, addresses, and diagnostic logs are never included."
msgstr "インシデント ID、結果、アクション、インターフェース、理由のみを送信します。設定、アドレス、診断ログは含まれません。"
msgid "The selected logical interface is monitored for address changes. In multi-WAN setups, select the interface that receives public connections."
msgstr "選択した論理インターフェースのアドレス変更を監視します。マルチ WAN 構成では、外部接続を受信するインターフェースを選択してください。"
msgid "The seven latest automatic recovery reports are retained on the router."
msgstr "直近 7 件の自動復旧レポートがルーターに保持されます。"
msgid "Trigger"
msgstr "トリガー"
msgid "View"
msgstr "表示"
msgid "WAN address reconciled"
msgstr "WAN アドレスを同期しました"
msgid "WAN address unavailable"
msgstr "WAN アドレスを利用できません"
msgid "Webhook URL"
msgstr "Webhook URL"
msgid "Generated configuration is invalid"
msgstr "生成された設定が無効です"
msgid "Configuration installation failed"
msgstr "設定のインストールに失敗しました"
@@ -0,0 +1,499 @@
msgid ""
msgstr ""
"Language: ko\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
msgid "Command failed with code %d."
msgstr "명령이 코드 %d(으)로 종료되었습니다."
msgid "%d configuration files"
msgstr "설정 파일 %d개"
msgid "%d conflicts"
msgstr "충돌 %d개"
msgid "%d services"
msgstr "서비스 %d개"
msgid "A restorable snapshot is created before migration and every apply."
msgstr "마이그레이션 전과 변경 사항을 적용할 때마다 복원 지점을 만듭니다."
msgid "Active listeners"
msgstr "활성 리스너"
msgid "Add service"
msgstr "서비스 추가"
msgid "Address"
msgstr "주소"
msgid "Applied"
msgstr "적용됨"
msgid "Apply"
msgstr "적용"
msgid "Apply changes"
msgstr "변경 사항 적용"
msgid "Apply raw config"
msgstr "설정 적용"
msgid "Apply raw configuration?"
msgstr "수동 설정을 적용하시겠습니까?"
msgid "Automatic"
msgstr "자동"
msgid "Cancel"
msgstr "취소"
msgid "Check configuration"
msgstr "설정 확인"
msgid "Config is valid"
msgstr "설정이 유효합니다"
msgid "Configuration and firewall are ready."
msgstr "설정과 방화벽이 준비되었습니다."
msgid "Configuration is valid. Firewall conflicts: %d"
msgstr "설정이 유효합니다. 방화벽 충돌: %d"
msgid "Configure shared Web entry ports, router access, and firewall automation."
msgstr "공유 Web 포트, 라우터 접근 및 방화벽 자동화를 설정합니다."
msgid "Conflict"
msgstr "충돌"
msgid "Contents"
msgstr "내용"
msgid "Create recovery point"
msgstr "복원 지점 만들기"
msgid "Created"
msgstr "생성 시각"
msgid "Creates and updates only firewall rules owned by HAProxy Manager."
msgstr "HAProxy Manager가 소유한 방화벽 규칙만 생성하고 업데이트합니다."
msgid "Custom TCP"
msgstr "사용자 지정 TCP"
msgid "Destination"
msgstr "대상"
msgid "Destination host"
msgstr "대상 호스트"
msgid "Destination HTTP port"
msgstr "대상 HTTP 포트"
msgid "Destination HTTPS port"
msgstr "대상 HTTPS 포트"
msgid "Destination RDP port"
msgstr "대상 RDP 포트"
msgid "Destination SSH port"
msgstr "대상 SSH 포트"
msgid "Disable conflicting forwards during apply"
msgstr "적용 시 충돌하는 포트 포워딩 비활성화"
msgid "Domain"
msgstr "도메인"
msgid "Enable managed configuration"
msgstr "관리 설정 활성화"
msgid "Enabled"
msgstr "활성화"
msgid "Every Web service shares these public ports. SSH, Remote Desktop, and Custom TCP services use the ports configured on each service."
msgstr "모든 Web 서비스는 이 공개 포트를 공유합니다. SSH, 원격 데스크톱 및 사용자 지정 TCP 서비스는 각 서비스에 설정된 포트를 사용합니다."
msgid "Existing port-forward conflicts"
msgstr "기존 포트 포워딩 충돌"
msgid "Expert editor for /etc/haproxy.cfg."
msgstr "/etc/haproxy.cfg 고급 편집기입니다."
msgid "Filter services"
msgstr "서비스 필터"
msgid "Firewall"
msgstr "방화벽"
msgid "Firewall automation"
msgstr "방화벽 자동화"
msgid "Firewall conflict detected."
msgstr "방화벽 충돌이 감지되었습니다."
msgid "Generated routes can overwrite manual changes to this file."
msgstr "생성된 경로가 이 파일의 수동 변경 내용을 덮어쓸 수 있습니다."
msgid "Grant access to LuCI HAProxy Manager"
msgstr "LuCI HAProxy Manager에 대한 접근 권한 부여"
msgid "HAProxy"
msgstr "HAProxy"
msgid "HAProxy service"
msgstr "HAProxy 서비스"
msgid "HAProxy Services"
msgstr "HAProxy 서비스"
msgid "HAProxy Settings"
msgstr "HAProxy 설정"
msgid "HAProxy Status"
msgstr "HAProxy 상태"
msgid "HAProxy version"
msgstr "HAProxy 버전"
msgid "HAProxy, firewall, and LuCI settings will be restored from %s."
msgstr "HAProxy, 방화벽 및 LuCI 설정을 %s에서 복원합니다."
msgid "HTTP"
msgstr "HTTP"
msgid "HTTP only"
msgstr "HTTP만"
msgid "HTTPS"
msgstr "HTTPS"
msgid "HTTPS only"
msgstr "HTTPS만"
msgid "Keep LuCI on the LAN address"
msgstr "LuCI를 LAN 주소에 유지"
msgid "Latest"
msgstr "최신"
msgid "Legacy Web"
msgstr "기존 Web"
msgid "LuCI LAN address"
msgstr "LuCI LAN 주소"
msgid "Managed"
msgstr "관리됨"
msgid "Manual"
msgstr "수동"
msgid "My service"
msgstr "내 서비스"
msgid "No HAProxy or LuCI listeners detected."
msgstr "HAProxy 또는 LuCI 리스너가 감지되지 않았습니다."
msgid "No matching services"
msgstr "일치하는 서비스가 없습니다"
msgid "No recovery points yet."
msgstr "아직 복원 지점이 없습니다."
msgid "Not available"
msgstr "사용할 수 없음"
msgid "Not set"
msgstr "설정되지 않음"
msgid "Off"
msgstr "꺼짐"
msgid "On"
msgstr "켜짐"
msgid "Open HAProxy ports on WAN automatically"
msgstr "WAN에서 HAProxy 포트를 자동으로 열기"
msgid "Open recovery"
msgstr "복원 열기"
msgid "Pending"
msgstr "적용 대기"
msgid "Process"
msgstr "프로세스"
msgid "Protocol"
msgstr "프로토콜"
msgid "Public endpoint"
msgstr "공개 엔드포인트"
msgid "Public HTTP port"
msgstr "공개 HTTP 포트"
msgid "Public HTTPS port"
msgstr "공개 HTTPS 포트"
msgid "Public RDP port"
msgstr "공개 RDP 포트"
msgid "Public SSH port"
msgstr "공개 SSH 포트"
msgid "Publish HTTP"
msgstr "HTTP 공개"
msgid "Publish HTTPS"
msgstr "HTTPS 공개"
msgid "Publish Web, SSH, Remote Desktop, and custom TCP services from one place."
msgstr "Web, SSH, 원격 데스크톱 및 사용자 지정 TCP 서비스를 한 곳에서 공개합니다."
msgid "Raw Config"
msgstr "원시 설정"
msgid "Raw HAProxy Config"
msgstr "HAProxy 원시 설정"
msgid "Raw HAProxy configuration"
msgstr "HAProxy 원시 설정"
msgid "Ready"
msgstr "준비됨"
msgid "Recommended when HAProxy uses public ports 80 or 443. LuCI remains available from the local network."
msgstr "HAProxy가 공개 포트 80 또는 443을 사용할 때 권장합니다. LuCI는 로컬 네트워크에서 계속 사용할 수 있습니다."
msgid "Recovery"
msgstr "복원"
msgid "Recovery points"
msgstr "복원 지점"
msgid "Refresh"
msgstr "새로 고침"
msgid "Remote Desktop"
msgstr "원격 데스크톱"
msgid "Restore"
msgstr "복원"
msgid "Restore configuration?"
msgstr "설정을 복원하시겠습니까?"
msgid "Restore HAProxy, firewall, and LuCI settings from a previous snapshot."
msgstr "이전 스냅샷에서 HAProxy, 방화벽 및 LuCI 설정을 복원합니다."
msgid "Restored"
msgstr "복원됨"
msgid "Review firewall settings"
msgstr "방화벽 설정 검토"
msgid "Router access"
msgstr "라우터 접근"
msgid "Running"
msgstr "실행 중"
msgid "Save and apply"
msgstr "저장 후 적용"
msgid "Save settings"
msgstr "설정 저장"
msgid "Service"
msgstr "서비스"
msgid "Service health, active listeners, and recovery points."
msgstr "서비스 상태, 활성 리스너 및 복원 지점입니다."
msgid "Service name"
msgstr "서비스 이름"
msgid "Service type"
msgstr "서비스 유형"
msgid "Services"
msgstr "서비스"
msgid "Settings"
msgstr "설정"
msgid "Settings saved"
msgstr "설정이 저장되었습니다"
msgid "SSH"
msgstr "SSH"
msgid "Status"
msgstr "상태"
msgid "Stop and show conflicts"
msgstr "중지하고 충돌 표시"
msgid "Stopped"
msgstr "중지됨"
msgid "TCP"
msgstr "TCP"
msgid "TCP port mappings"
msgstr "TCP 포트 매핑"
msgid "The file will be validated and backed up before HAProxy restarts."
msgstr "HAProxy를 다시 시작하기 전에 파일을 검증하고 백업합니다."
msgid "Use the public:destination format, for example 8443:443."
msgstr "공개:대상 형식을 사용하십시오. 예: 8443:443"
msgid "Validate"
msgstr "검증"
msgid "WAN address"
msgstr "WAN 주소"
msgid "WAN bind address"
msgstr "WAN 바인드 주소"
msgid "WAN firewall zone"
msgstr "WAN 방화벽 영역"
msgid "Web"
msgstr "Web"
msgid "Web (HTTP + HTTPS)"
msgstr "Web (HTTP + HTTPS)"
msgid "Web entry ports"
msgstr "Web 공개 포트"
msgid "Changes saved and applied."
msgstr "변경 사항이 저장되고 적용되었습니다."
msgid "Delete and apply"
msgstr "삭제 후 적용"
msgid "Delete service?"
msgstr "서비스를 삭제하시겠습니까?"
msgid "Synchronize now"
msgstr "지금 동기화"
msgid "The service will be removed from HAProxy and the firewall immediately."
msgstr "서비스가 HAProxy와 방화벽에서 즉시 제거됩니다."
msgid "Unable to create a recovery point."
msgstr "복구 지점을 만들 수 없습니다."
msgid "Automatic recovery"
msgstr "자동 복구"
msgid "Disabled"
msgstr "비활성화"
msgid "Failed"
msgstr "실패"
msgid "If HAProxy stops before a listener address exists, retry it after a network interface comes up and retain diagnostics for the seven latest incidents."
msgstr "리스너 주소가 준비되기 전에 HAProxy가 중지되면 네트워크 인터페이스가 올라온 뒤 다시 시작하고 최근 7개 장애의 진단 정보를 보관합니다."
msgid "In progress"
msgstr "진행 중"
msgid "Invalid configuration"
msgstr "잘못된 구성"
msgid "Last recovery"
msgstr "마지막 복구"
msgid "No incidents recorded"
msgstr "기록된 장애 없음"
msgid "Recover HAProxy after networking becomes ready"
msgstr "네트워크 준비 후 HAProxy 복구"
msgid "Recovered"
msgstr "복구됨"
msgid "Recovery point failed"
msgstr "복구 지점 생성 실패"
msgid "WAN network interface"
msgstr "WAN 네트워크 인터페이스"
msgid "Close"
msgstr "닫기"
msgid "Configuration mode"
msgstr "구성 모드"
msgid "Diagnostic report for %s."
msgstr "%s 진단 보고서입니다."
msgid "Enter an HTTP or HTTPS URL without spaces."
msgstr "공백 없이 HTTP 또는 HTTPS URL을 입력하십시오."
msgid "Failed, configuration restored"
msgstr "실패하여 구성을 복원함"
msgid "Failed, restore also failed"
msgstr "실패했으며 복원도 실패함"
msgid "Generated routes"
msgstr "생성된 경로"
msgid "Incident diagnostics"
msgstr "장애 진단"
msgid "Not managed"
msgstr "관리되지 않음"
msgid "Raw configuration"
msgstr "수동 구성"
msgid "Reconciles generated listeners after WAN address changes, retries a stopped service, and retains diagnostics for the seven latest incidents. Raw configurations are never overwritten."
msgstr "WAN 주소 변경 후 생성된 리스너를 동기화하고 중지된 서비스를 다시 시작하며 최근 7개 장애의 진단을 보관합니다. 수동 구성은 절대 덮어쓰지 않습니다."
msgid "Recovery incidents"
msgstr "복구 장애"
msgid "Result"
msgstr "결과"
msgid "Send recovery webhook notifications"
msgstr "복구 웹훅 알림 보내기"
msgid "Sends only the incident identifier, result, action, interface, and reason. Configuration, addresses, and diagnostic logs are never included."
msgstr "장애 식별자, 결과, 작업, 인터페이스 및 원인만 전송합니다. 구성, 주소 및 진단 로그는 포함하지 않습니다."
msgid "The selected logical interface is monitored for address changes. In multi-WAN setups, select the interface that receives public connections."
msgstr "선택한 논리 인터페이스의 주소 변경을 감시합니다. 다중 WAN 구성에서는 외부 연결을 수신하는 인터페이스를 선택하십시오."
msgid "The seven latest automatic recovery reports are retained on the router."
msgstr "최근 7개의 자동 복구 보고서가 라우터에 보관됩니다."
msgid "Trigger"
msgstr "트리거"
msgid "View"
msgstr "보기"
msgid "WAN address reconciled"
msgstr "WAN 주소 동기화됨"
msgid "WAN address unavailable"
msgstr "WAN 주소를 사용할 수 없음"
msgid "Webhook URL"
msgstr "웹훅 URL"
msgid "Generated configuration is invalid"
msgstr "생성된 구성이 유효하지 않음"
msgid "Configuration installation failed"
msgstr "구성을 설치하지 못함"
@@ -0,0 +1,499 @@
msgid ""
msgstr ""
"Language: ru\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
msgid "Command failed with code %d."
msgstr "Команда завершилась с кодом %d."
msgid "%d configuration files"
msgstr "%d файлов конфигурации"
msgid "%d conflicts"
msgstr "%d конфликтов"
msgid "%d services"
msgstr "%d сервисов"
msgid "A restorable snapshot is created before migration and every apply."
msgstr "Перед миграцией и каждым применением создаётся точка восстановления."
msgid "Active listeners"
msgstr "Активные слушатели"
msgid "Add service"
msgstr "Добавить сервис"
msgid "Address"
msgstr "Адрес"
msgid "Applied"
msgstr "Применено"
msgid "Apply"
msgstr "Применить"
msgid "Apply changes"
msgstr "Применить изменения"
msgid "Apply raw config"
msgstr "Применить конфиг"
msgid "Apply raw configuration?"
msgstr "Применить конфигурацию вручную?"
msgid "Automatic"
msgstr "Автоматически"
msgid "Cancel"
msgstr "Отмена"
msgid "Check configuration"
msgstr "Проверить конфигурацию"
msgid "Config is valid"
msgstr "Конфиг корректен"
msgid "Configuration and firewall are ready."
msgstr "Конфигурация и межсетевой экран готовы."
msgid "Configuration is valid. Firewall conflicts: %d"
msgstr "Конфигурация корректна. Конфликтов межсетевого экрана: %d"
msgid "Configure shared Web entry ports, router access, and firewall automation."
msgstr "Настройка общих Web-портов, доступа к роутеру и автоматизации межсетевого экрана."
msgid "Conflict"
msgstr "Конфликт"
msgid "Contents"
msgstr "Содержимое"
msgid "Create recovery point"
msgstr "Создать точку восстановления"
msgid "Created"
msgstr "Создана"
msgid "Creates and updates only firewall rules owned by HAProxy Manager."
msgstr "Создаёт и обновляет только правила межсетевого экрана, принадлежащие HAProxy Manager."
msgid "Custom TCP"
msgstr "Пользовательский TCP"
msgid "Destination"
msgstr "Назначение"
msgid "Destination host"
msgstr "Внутренний хост"
msgid "Destination HTTP port"
msgstr "Внутренний HTTP-порт"
msgid "Destination HTTPS port"
msgstr "Внутренний HTTPS-порт"
msgid "Destination RDP port"
msgstr "Внутренний RDP-порт"
msgid "Destination SSH port"
msgstr "Внутренний SSH-порт"
msgid "Disable conflicting forwards during apply"
msgstr "Отключать конфликтующие пробросы при применении"
msgid "Domain"
msgstr "Домен"
msgid "Enable managed configuration"
msgstr "Включить управляемую конфигурацию"
msgid "Enabled"
msgstr "Включено"
msgid "Every Web service shares these public ports. SSH, Remote Desktop, and Custom TCP services use the ports configured on each service."
msgstr "Все Web-сервисы используют эти общие внешние порты. SSH, удалённый рабочий стол и пользовательские TCP-сервисы используют порты из своих настроек."
msgid "Existing port-forward conflicts"
msgstr "Конфликты с существующими пробросами"
msgid "Expert editor for /etc/haproxy.cfg."
msgstr "Экспертный редактор /etc/haproxy.cfg."
msgid "Filter services"
msgstr "Фильтр сервисов"
msgid "Firewall"
msgstr "Межсетевой экран"
msgid "Firewall automation"
msgstr "Автоматизация межсетевого экрана"
msgid "Firewall conflict detected."
msgstr "Обнаружен конфликт межсетевого экрана."
msgid "Generated routes can overwrite manual changes to this file."
msgstr "Сгенерированные маршруты могут перезаписать ручные изменения этого файла."
msgid "Grant access to LuCI HAProxy Manager"
msgstr "Предоставить доступ к LuCI HAProxy Manager"
msgid "HAProxy"
msgstr "HAProxy"
msgid "HAProxy service"
msgstr "Служба HAProxy"
msgid "HAProxy Services"
msgstr "Сервисы HAProxy"
msgid "HAProxy Settings"
msgstr "Настройки HAProxy"
msgid "HAProxy Status"
msgstr "Статус HAProxy"
msgid "HAProxy version"
msgstr "Версия HAProxy"
msgid "HAProxy, firewall, and LuCI settings will be restored from %s."
msgstr "Настройки HAProxy, межсетевого экрана и LuCI будут восстановлены из точки %s."
msgid "HTTP"
msgstr "HTTP"
msgid "HTTP only"
msgstr "Только HTTP"
msgid "HTTPS"
msgstr "HTTPS"
msgid "HTTPS only"
msgstr "Только HTTPS"
msgid "Keep LuCI on the LAN address"
msgstr "Оставить LuCI на LAN-адресе"
msgid "Latest"
msgstr "Последняя"
msgid "Legacy Web"
msgstr "Устаревший Web"
msgid "LuCI LAN address"
msgstr "LAN-адрес LuCI"
msgid "Managed"
msgstr "Управляется"
msgid "Manual"
msgstr "Вручную"
msgid "My service"
msgstr "Мой сервис"
msgid "No HAProxy or LuCI listeners detected."
msgstr "Слушатели HAProxy или LuCI не обнаружены."
msgid "No matching services"
msgstr "Подходящих сервисов нет"
msgid "No recovery points yet."
msgstr "Точек восстановления пока нет."
msgid "Not available"
msgstr "Недоступно"
msgid "Not set"
msgstr "Не задано"
msgid "Off"
msgstr "Выкл"
msgid "On"
msgstr "Вкл"
msgid "Open HAProxy ports on WAN automatically"
msgstr "Автоматически открывать порты HAProxy на WAN"
msgid "Open recovery"
msgstr "Открыть восстановление"
msgid "Pending"
msgstr "Ожидает применения"
msgid "Process"
msgstr "Процесс"
msgid "Protocol"
msgstr "Протокол"
msgid "Public endpoint"
msgstr "Внешний адрес"
msgid "Public HTTP port"
msgstr "Внешний HTTP-порт"
msgid "Public HTTPS port"
msgstr "Внешний HTTPS-порт"
msgid "Public RDP port"
msgstr "Внешний RDP-порт"
msgid "Public SSH port"
msgstr "Внешний SSH-порт"
msgid "Publish HTTP"
msgstr "Публиковать HTTP"
msgid "Publish HTTPS"
msgstr "Публиковать HTTPS"
msgid "Publish Web, SSH, Remote Desktop, and custom TCP services from one place."
msgstr "Публикация Web, SSH, удалённого рабочего стола и пользовательских TCP-сервисов в одном месте."
msgid "Raw Config"
msgstr "Конфиг"
msgid "Raw HAProxy Config"
msgstr "Конфиг HAProxy"
msgid "Raw HAProxy configuration"
msgstr "Конфигурация HAProxy"
msgid "Ready"
msgstr "Готово"
msgid "Recommended when HAProxy uses public ports 80 or 443. LuCI remains available from the local network."
msgstr "Рекомендуется, когда HAProxy использует внешние порты 80 или 443. LuCI останется доступен из локальной сети."
msgid "Recovery"
msgstr "Восстановление"
msgid "Recovery points"
msgstr "Точки восстановления"
msgid "Refresh"
msgstr "Обновить"
msgid "Remote Desktop"
msgstr "Удалённый рабочий стол"
msgid "Restore"
msgstr "Восстановить"
msgid "Restore configuration?"
msgstr "Восстановить конфигурацию?"
msgid "Restore HAProxy, firewall, and LuCI settings from a previous snapshot."
msgstr "Восстановление настроек HAProxy, межсетевого экрана и LuCI из предыдущего снимка."
msgid "Restored"
msgstr "Восстановлено"
msgid "Review firewall settings"
msgstr "Проверить настройки межсетевого экрана"
msgid "Router access"
msgstr "Доступ к роутеру"
msgid "Running"
msgstr "Работает"
msgid "Save and apply"
msgstr "Сохранить и применить"
msgid "Save settings"
msgstr "Сохранить настройки"
msgid "Service"
msgstr "Сервис"
msgid "Service health, active listeners, and recovery points."
msgstr "Состояние службы, активные слушатели и точки восстановления."
msgid "Service name"
msgstr "Название сервиса"
msgid "Service type"
msgstr "Тип сервиса"
msgid "Services"
msgstr "Сервисы"
msgid "Settings"
msgstr "Настройки"
msgid "Settings saved"
msgstr "Настройки сохранены"
msgid "SSH"
msgstr "SSH"
msgid "Status"
msgstr "Статус"
msgid "Stop and show conflicts"
msgstr "Остановиться и показать конфликты"
msgid "Stopped"
msgstr "Остановлен"
msgid "TCP"
msgstr "TCP"
msgid "TCP port mappings"
msgstr "Соответствия TCP-портов"
msgid "The file will be validated and backed up before HAProxy restarts."
msgstr "Перед перезапуском HAProxy файл будет проверен и сохранён в бэкап."
msgid "Use the public:destination format, for example 8443:443."
msgstr "Используйте формат внешний:внутренний, например 8443:443."
msgid "Validate"
msgstr "Проверить"
msgid "WAN address"
msgstr "WAN-адрес"
msgid "WAN bind address"
msgstr "Адрес привязки WAN"
msgid "WAN firewall zone"
msgstr "WAN-зона межсетевого экрана"
msgid "Web"
msgstr "Web"
msgid "Web (HTTP + HTTPS)"
msgstr "Web (HTTP + HTTPS)"
msgid "Web entry ports"
msgstr "Внешние Web-порты"
msgid "Changes saved and applied."
msgstr "Изменения сохранены и применены."
msgid "Delete and apply"
msgstr "Удалить и применить"
msgid "Delete service?"
msgstr "Удалить сервис?"
msgid "Synchronize now"
msgstr "Синхронизировать"
msgid "The service will be removed from HAProxy and the firewall immediately."
msgstr "Сервис будет немедленно удалён из HAProxy и межсетевого экрана."
msgid "Unable to create a recovery point."
msgstr "Не удалось создать точку восстановления."
msgid "Automatic recovery"
msgstr "Автоматическое восстановление"
msgid "Disabled"
msgstr "Отключено"
msgid "Failed"
msgstr "Не удалось"
msgid "If HAProxy stops before a listener address exists, retry it after a network interface comes up and retain diagnostics for the seven latest incidents."
msgstr "Если HAProxy остановился до появления адреса слушателя, повторить запуск после поднятия сетевого интерфейса и сохранить диагностику семи последних инцидентов."
msgid "In progress"
msgstr "Выполняется"
msgid "Invalid configuration"
msgstr "Некорректная конфигурация"
msgid "Last recovery"
msgstr "Последнее восстановление"
msgid "No incidents recorded"
msgstr "Срабатываний не зафиксировано"
msgid "Recover HAProxy after networking becomes ready"
msgstr "Автоматически запускать HAProxy после готовности сети"
msgid "Recovered"
msgstr "Восстановлено"
msgid "Recovery point failed"
msgstr "Не удалось создать точку восстановления"
msgid "WAN network interface"
msgstr "Сетевой интерфейс WAN"
msgid "Close"
msgstr "Закрыть"
msgid "Configuration mode"
msgstr "Режим конфигурации"
msgid "Diagnostic report for %s."
msgstr "Диагностический отчёт для %s."
msgid "Enter an HTTP or HTTPS URL without spaces."
msgstr "Введите HTTP- или HTTPS-адрес без пробелов."
msgid "Failed, configuration restored"
msgstr "Ошибка, конфигурация восстановлена"
msgid "Failed, restore also failed"
msgstr "Ошибка, восстановление также не удалось"
msgid "Generated routes"
msgstr "Сгенерированные маршруты"
msgid "Incident diagnostics"
msgstr "Диагностика инцидента"
msgid "Not managed"
msgstr "Не управляется"
msgid "Raw configuration"
msgstr "Пользовательская конфигурация"
msgid "Reconciles generated listeners after WAN address changes, retries a stopped service, and retains diagnostics for the seven latest incidents. Raw configurations are never overwritten."
msgstr "Синхронизирует сгенерированные слушатели после смены WAN-адреса, повторно запускает остановленную службу и хранит диагностику семи последних инцидентов. Пользовательские конфигурации никогда не перезаписываются."
msgid "Recovery incidents"
msgstr "Инциденты восстановления"
msgid "Result"
msgstr "Результат"
msgid "Send recovery webhook notifications"
msgstr "Отправлять webhook-уведомления о восстановлении"
msgid "Sends only the incident identifier, result, action, interface, and reason. Configuration, addresses, and diagnostic logs are never included."
msgstr "Отправляются только идентификатор инцидента, результат, действие, интерфейс и причина. Конфигурация, адреса и диагностические журналы никогда не передаются."
msgid "The selected logical interface is monitored for address changes. In multi-WAN setups, select the interface that receives public connections."
msgstr "Выбранный логический интерфейс отслеживается на предмет смены адреса. В конфигурациях с несколькими WAN выберите интерфейс, принимающий публичные подключения."
msgid "The seven latest automatic recovery reports are retained on the router."
msgstr "На маршрутизаторе хранятся семь последних отчётов автоматического восстановления."
msgid "Trigger"
msgstr "Событие"
msgid "View"
msgstr "Просмотреть"
msgid "WAN address reconciled"
msgstr "WAN-адрес синхронизирован"
msgid "WAN address unavailable"
msgstr "WAN-адрес недоступен"
msgid "Webhook URL"
msgstr "URL webhook"
msgid "Generated configuration is invalid"
msgstr "Сгенерированная конфигурация недействительна"
msgid "Configuration installation failed"
msgstr "Не удалось установить конфигурацию"
+1
View File
@@ -0,0 +1 @@
zh_Hans
@@ -0,0 +1,499 @@
msgid ""
msgstr ""
"Language: zh_Hans\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
msgid "Command failed with code %d."
msgstr "命令以代码 %d 退出。"
msgid "%d configuration files"
msgstr "%d 个配置文件"
msgid "%d conflicts"
msgstr "%d 个冲突"
msgid "%d services"
msgstr "%d 个服务"
msgid "A restorable snapshot is created before migration and every apply."
msgstr "迁移前和每次应用更改时都会创建可恢复的快照。"
msgid "Active listeners"
msgstr "活动监听器"
msgid "Add service"
msgstr "添加服务"
msgid "Address"
msgstr "地址"
msgid "Applied"
msgstr "已应用"
msgid "Apply"
msgstr "应用"
msgid "Apply changes"
msgstr "应用更改"
msgid "Apply raw config"
msgstr "应用配置"
msgid "Apply raw configuration?"
msgstr "应用手动配置?"
msgid "Automatic"
msgstr "自动"
msgid "Cancel"
msgstr "取消"
msgid "Check configuration"
msgstr "检查配置"
msgid "Config is valid"
msgstr "配置有效"
msgid "Configuration and firewall are ready."
msgstr "配置和防火墙已就绪。"
msgid "Configuration is valid. Firewall conflicts: %d"
msgstr "配置有效。防火墙冲突:%d"
msgid "Configure shared Web entry ports, router access, and firewall automation."
msgstr "配置共享 Web 入口端口、路由器访问和防火墙自动化。"
msgid "Conflict"
msgstr "冲突"
msgid "Contents"
msgstr "内容"
msgid "Create recovery point"
msgstr "创建恢复点"
msgid "Created"
msgstr "创建时间"
msgid "Creates and updates only firewall rules owned by HAProxy Manager."
msgstr "仅创建和更新由 HAProxy Manager 管理的防火墙规则。"
msgid "Custom TCP"
msgstr "自定义 TCP"
msgid "Destination"
msgstr "目标"
msgid "Destination host"
msgstr "目标主机"
msgid "Destination HTTP port"
msgstr "目标 HTTP 端口"
msgid "Destination HTTPS port"
msgstr "目标 HTTPS 端口"
msgid "Destination RDP port"
msgstr "目标 RDP 端口"
msgid "Destination SSH port"
msgstr "目标 SSH 端口"
msgid "Disable conflicting forwards during apply"
msgstr "应用时禁用冲突的端口转发"
msgid "Domain"
msgstr "域名"
msgid "Enable managed configuration"
msgstr "启用托管配置"
msgid "Enabled"
msgstr "已启用"
msgid "Every Web service shares these public ports. SSH, Remote Desktop, and Custom TCP services use the ports configured on each service."
msgstr "所有 Web 服务共享这些公网端口。SSH、远程桌面和自定义 TCP 服务使用各自配置的端口。"
msgid "Existing port-forward conflicts"
msgstr "现有端口转发冲突"
msgid "Expert editor for /etc/haproxy.cfg."
msgstr "/etc/haproxy.cfg 高级编辑器。"
msgid "Filter services"
msgstr "筛选服务"
msgid "Firewall"
msgstr "防火墙"
msgid "Firewall automation"
msgstr "防火墙自动化"
msgid "Firewall conflict detected."
msgstr "检测到防火墙冲突。"
msgid "Generated routes can overwrite manual changes to this file."
msgstr "生成路由可能会覆盖对此文件的手动修改。"
msgid "Grant access to LuCI HAProxy Manager"
msgstr "授予访问 LuCI HAProxy Manager 的权限"
msgid "HAProxy"
msgstr "HAProxy"
msgid "HAProxy service"
msgstr "HAProxy 服务"
msgid "HAProxy Services"
msgstr "HAProxy 服务"
msgid "HAProxy Settings"
msgstr "HAProxy 设置"
msgid "HAProxy Status"
msgstr "HAProxy 状态"
msgid "HAProxy version"
msgstr "HAProxy 版本"
msgid "HAProxy, firewall, and LuCI settings will be restored from %s."
msgstr "将从 %s 恢复 HAProxy、防火墙和 LuCI 设置。"
msgid "HTTP"
msgstr "HTTP"
msgid "HTTP only"
msgstr "仅 HTTP"
msgid "HTTPS"
msgstr "HTTPS"
msgid "HTTPS only"
msgstr "仅 HTTPS"
msgid "Keep LuCI on the LAN address"
msgstr "让 LuCI 仅使用 LAN 地址"
msgid "Latest"
msgstr "最新"
msgid "Legacy Web"
msgstr "旧版 Web"
msgid "LuCI LAN address"
msgstr "LuCI LAN 地址"
msgid "Managed"
msgstr "已管理"
msgid "Manual"
msgstr "手动"
msgid "My service"
msgstr "我的服务"
msgid "No HAProxy or LuCI listeners detected."
msgstr "未检测到 HAProxy 或 LuCI 监听器。"
msgid "No matching services"
msgstr "没有匹配的服务"
msgid "No recovery points yet."
msgstr "还没有恢复点。"
msgid "Not available"
msgstr "不可用"
msgid "Not set"
msgstr "未设置"
msgid "Off"
msgstr "关闭"
msgid "On"
msgstr "开启"
msgid "Open HAProxy ports on WAN automatically"
msgstr "自动在 WAN 上开放 HAProxy 端口"
msgid "Open recovery"
msgstr "打开恢复"
msgid "Pending"
msgstr "等待应用"
msgid "Process"
msgstr "进程"
msgid "Protocol"
msgstr "协议"
msgid "Public endpoint"
msgstr "公共入口"
msgid "Public HTTP port"
msgstr "公网 HTTP 端口"
msgid "Public HTTPS port"
msgstr "公网 HTTPS 端口"
msgid "Public RDP port"
msgstr "公网 RDP 端口"
msgid "Public SSH port"
msgstr "公网 SSH 端口"
msgid "Publish HTTP"
msgstr "发布 HTTP"
msgid "Publish HTTPS"
msgstr "发布 HTTPS"
msgid "Publish Web, SSH, Remote Desktop, and custom TCP services from one place."
msgstr "在一个界面中发布 Web、SSH、远程桌面和自定义 TCP 服务。"
msgid "Raw Config"
msgstr "原始配置"
msgid "Raw HAProxy Config"
msgstr "HAProxy 原始配置"
msgid "Raw HAProxy configuration"
msgstr "HAProxy 原始配置"
msgid "Ready"
msgstr "就绪"
msgid "Recommended when HAProxy uses public ports 80 or 443. LuCI remains available from the local network."
msgstr "当 HAProxy 使用公网端口 80 或 443 时建议启用。LuCI 仍可从本地网络访问。"
msgid "Recovery"
msgstr "恢复"
msgid "Recovery points"
msgstr "恢复点"
msgid "Refresh"
msgstr "刷新"
msgid "Remote Desktop"
msgstr "远程桌面"
msgid "Restore"
msgstr "恢复"
msgid "Restore configuration?"
msgstr "恢复配置?"
msgid "Restore HAProxy, firewall, and LuCI settings from a previous snapshot."
msgstr "从以前的快照恢复 HAProxy、防火墙和 LuCI 设置。"
msgid "Restored"
msgstr "已恢复"
msgid "Review firewall settings"
msgstr "检查防火墙设置"
msgid "Router access"
msgstr "路由器访问"
msgid "Running"
msgstr "运行中"
msgid "Save and apply"
msgstr "保存并应用"
msgid "Save settings"
msgstr "保存设置"
msgid "Service"
msgstr "服务"
msgid "Service health, active listeners, and recovery points."
msgstr "服务状态、活动监听器和恢复点。"
msgid "Service name"
msgstr "服务名称"
msgid "Service type"
msgstr "服务类型"
msgid "Services"
msgstr "服务"
msgid "Settings"
msgstr "设置"
msgid "Settings saved"
msgstr "设置已保存"
msgid "SSH"
msgstr "SSH"
msgid "Status"
msgstr "状态"
msgid "Stop and show conflicts"
msgstr "停止并显示冲突"
msgid "Stopped"
msgstr "已停止"
msgid "TCP"
msgstr "TCP"
msgid "TCP port mappings"
msgstr "TCP 端口映射"
msgid "The file will be validated and backed up before HAProxy restarts."
msgstr "HAProxy 重启前将验证并备份该文件。"
msgid "Use the public:destination format, for example 8443:443."
msgstr "请使用 公网:目标 格式,例如 8443:443。"
msgid "Validate"
msgstr "验证"
msgid "WAN address"
msgstr "WAN 地址"
msgid "WAN bind address"
msgstr "WAN 绑定地址"
msgid "WAN firewall zone"
msgstr "WAN 防火墙区域"
msgid "Web"
msgstr "Web"
msgid "Web (HTTP + HTTPS)"
msgstr "WebHTTP + HTTPS"
msgid "Web entry ports"
msgstr "Web 入口端口"
msgid "Changes saved and applied."
msgstr "更改已保存并应用。"
msgid "Delete and apply"
msgstr "删除并应用"
msgid "Delete service?"
msgstr "删除服务?"
msgid "Synchronize now"
msgstr "立即同步"
msgid "The service will be removed from HAProxy and the firewall immediately."
msgstr "该服务将立即从 HAProxy 和防火墙中移除。"
msgid "Unable to create a recovery point."
msgstr "无法创建恢复点。"
msgid "Automatic recovery"
msgstr "自动恢复"
msgid "Disabled"
msgstr "已禁用"
msgid "Failed"
msgstr "失败"
msgid "If HAProxy stops before a listener address exists, retry it after a network interface comes up and retain diagnostics for the seven latest incidents."
msgstr "如果 HAProxy 在监听地址可用前停止,则在网络接口启动后重试,并保留最近七次事件的诊断信息。"
msgid "In progress"
msgstr "进行中"
msgid "Invalid configuration"
msgstr "配置无效"
msgid "Last recovery"
msgstr "最近一次恢复"
msgid "No incidents recorded"
msgstr "尚无事件记录"
msgid "Recover HAProxy after networking becomes ready"
msgstr "网络就绪后恢复 HAProxy"
msgid "Recovered"
msgstr "已恢复"
msgid "Recovery point failed"
msgstr "恢复点创建失败"
msgid "WAN network interface"
msgstr "WAN 网络接口"
msgid "Close"
msgstr "关闭"
msgid "Configuration mode"
msgstr "配置模式"
msgid "Diagnostic report for %s."
msgstr "%s 的诊断报告。"
msgid "Enter an HTTP or HTTPS URL without spaces."
msgstr "请输入不含空格的 HTTP 或 HTTPS URL。"
msgid "Failed, configuration restored"
msgstr "失败,已恢复配置"
msgid "Failed, restore also failed"
msgstr "失败,恢复操作也失败"
msgid "Generated routes"
msgstr "生成的路由"
msgid "Incident diagnostics"
msgstr "事件诊断"
msgid "Not managed"
msgstr "未管理"
msgid "Raw configuration"
msgstr "手动配置"
msgid "Reconciles generated listeners after WAN address changes, retries a stopped service, and retains diagnostics for the seven latest incidents. Raw configurations are never overwritten."
msgstr "WAN 地址变化后同步生成的监听器,重试已停止的服务,并保留最近七次事件的诊断信息。手动配置绝不会被覆盖。"
msgid "Recovery incidents"
msgstr "恢复事件"
msgid "Result"
msgstr "结果"
msgid "Send recovery webhook notifications"
msgstr "发送恢复 Webhook 通知"
msgid "Sends only the incident identifier, result, action, interface, and reason. Configuration, addresses, and diagnostic logs are never included."
msgstr "仅发送事件标识符、结果、操作、接口和原因。绝不会包含配置、地址和诊断日志。"
msgid "The selected logical interface is monitored for address changes. In multi-WAN setups, select the interface that receives public connections."
msgstr "监控所选逻辑接口的地址变化。在多 WAN 配置中,请选择接收公网连接的接口。"
msgid "The seven latest automatic recovery reports are retained on the router."
msgstr "路由器会保留最近七份自动恢复报告。"
msgid "Trigger"
msgstr "触发条件"
msgid "View"
msgstr "查看"
msgid "WAN address reconciled"
msgstr "WAN 地址已同步"
msgid "WAN address unavailable"
msgstr "WAN 地址不可用"
msgid "Webhook URL"
msgstr "Webhook URL"
msgid "Generated configuration is invalid"
msgstr "生成的配置无效"
msgid "Configuration installation failed"
msgstr "配置安装失败"
@@ -0,0 +1,17 @@
config settings 'main'
option schema_version '3'
option enabled '0'
option active_mode 'none'
option auto_recover '1'
option manage_uhttpd_bind '0'
option manage_firewall '0'
option firewall_zone 'wan'
option firewall_conflict_mode 'warn'
option lan_bind_ip 'auto'
option wan_interface 'wan'
option wan_bind_ip 'auto'
option http_port '80'
option https_port '443'
option backup_dir '/root/haproxy-manager-backups'
option webhook_enabled '0'
option webhook_url ''
@@ -0,0 +1,11 @@
#!/bin/sh
case "${ACTION:-}" in
ifup|ifupdate) ;;
*) exit 0 ;;
esac
[ "${INTERFACE:-}" = "$(uci -q get haproxy_manager.main.wan_interface 2>/dev/null || echo wan)" ] || exit 0
[ -x /usr/libexec/haproxy-manager/recover ] || exit 0
/usr/libexec/haproxy-manager/recover "$ACTION" "${INTERFACE:-unknown}" >/dev/null 2>&1 &
exit 0
@@ -0,0 +1,180 @@
#!/bin/sh
set -eu
. /usr/libexec/haproxy-manager/common.sh
umask 077
LOCK_DIR=/var/lock/haproxy-manager.apply
LOCK_BUSY_EXIT=75
TMP_CFG="/tmp/haproxy-manager-apply.$$"
BACKUP_ID=""
APPLIED=0
UHTTPD_CHANGED=0
if [ "${1:-}" = --backup ]; then
BACKUP_ID="${2:-}"
[ "$#" -eq 2 ] || { echo "Usage: apply [--backup YYYYMMDD-HHMMSS]" >&2; exit 1; }
is_valid_backup_id "$BACKUP_ID" || { echo "Invalid backup identifier" >&2; exit 1; }
elif [ "$#" -ne 0 ]; then
echo "Usage: apply [--backup YYYYMMDD-HHMMSS]" >&2
exit 1
fi
operation_lock_acquire "$LOCK_DIR" || {
echo "Another HAProxy Manager operation is already running." >&2
exit "$LOCK_BUSY_EXIT"
}
cleanup_temp() {
rm -f "$TMP_CFG"
}
release_lock() {
operation_lock_release "$LOCK_DIR"
}
cleanup() {
cleanup_temp
release_lock
}
restore_on_error() {
status=$?
trap - EXIT
if [ "$APPLIED" != 1 ] && [ -n "$BACKUP_ID" ]; then
if [ "$UHTTPD_CHANGED" = 1 ]; then
cleanup_temp
(
sleep 2
if /usr/libexec/haproxy-manager/rollback --sync "$BACKUP_ID" >/dev/null 2>&1; then
logger -t haproxy-manager "Apply failed; restored $BACKUP_ID"
else
logger -t haproxy-manager "Apply failed; could not restore $BACKUP_ID"
fi
release_lock
) >/dev/null 2>&1 &
operation_lock_set_pid "$LOCK_DIR" "$!"
exit "$status"
fi
if /usr/libexec/haproxy-manager/rollback --sync "$BACKUP_ID" >/dev/null 2>&1; then
logger -t haproxy-manager "Apply failed; restored $BACKUP_ID"
else
logger -t haproxy-manager "Apply failed; could not restore $BACKUP_ID"
fi
fi
cleanup
exit "$status"
}
trap restore_on_error EXIT
if [ -n "$BACKUP_ID" ]; then
[ -d "$(backup_dir)/$BACKUP_ID" ] || { echo "Backup not found: $BACKUP_ID" >&2; exit 1; }
else
BACKUP_ID="$(basename "$(/usr/libexec/haproxy-manager/backup)")"
fi
/usr/libexec/haproxy-manager/migrate >/dev/null
defer_disabled_reload() {
APPLIED=1
trap - EXIT
cleanup_temp
(
sleep 2
if /etc/init.d/uhttpd reload >/dev/null 2>&1; then
logger -t haproxy-manager "LuCI listener bindings restored"
else
if /usr/libexec/haproxy-manager/rollback --sync "$BACKUP_ID" >/dev/null 2>&1; then
logger -t haproxy-manager "LuCI reload failed; restored $BACKUP_ID"
else
logger -t haproxy-manager "LuCI reload failed; could not restore $BACKUP_ID"
fi
fi
release_lock
) >/dev/null 2>&1 &
operation_lock_set_pid "$LOCK_DIR" "$!"
}
if [ "$(uci_get main enabled 0)" != 1 ]; then
MODE="$(active_mode)"
restore_uhttpd_bindings
[ "$UHTTPD_CHANGED" != 1 ] || uci commit uhttpd
/usr/libexec/haproxy-manager/firewall-sync --disable >/dev/null
case "$MODE" in
generated|raw)
/etc/init.d/haproxy stop >/dev/null 2>&1 || true
/etc/init.d/haproxy disable >/dev/null 2>&1 || true
uci set "$CONFIG.main.active_mode=none"
uci commit "$CONFIG"
;;
esac
if [ "$UHTTPD_CHANGED" = 1 ]; then
defer_disabled_reload
echo "Managed HAProxy service disabled. LuCI bindings will be restored shortly. Backup: $BACKUP_ID"
exit 0
fi
APPLIED=1
trap - EXIT
cleanup
echo "Managed HAProxy service disabled. Backup: $BACKUP_ID"
exit 0
fi
/usr/libexec/haproxy-manager/generate "$TMP_CFG" >/dev/null
/usr/libexec/haproxy-manager/validate "$TMP_CFG"
if [ "$(uci_get main manage_firewall 0)" = 1 ]; then
/usr/libexec/haproxy-manager/firewall-sync --check >/dev/null
fi
MANAGE_UHTTPD="$(uci_get main manage_uhttpd_bind 0)"
LAN_BIND_IP="$(lan_ip || true)"
if [ "$MANAGE_UHTTPD" = 1 ]; then
[ -n "$LAN_BIND_IP" ] || { echo "Cannot detect LAN IPv4 address. Set the LuCI LAN address manually." >&2; exit 1; }
is_valid_ip_or_host "$LAN_BIND_IP" || { echo "Invalid LuCI LAN address: $LAN_BIND_IP" >&2; exit 1; }
capture_uhttpd_bindings
set_uhttpd_list listen_http "$LAN_BIND_IP:80"
set_uhttpd_list listen_https "$LAN_BIND_IP:443"
else
restore_uhttpd_bindings
fi
[ "$UHTTPD_CHANGED" != 1 ] || uci commit uhttpd
/usr/libexec/haproxy-manager/firewall-sync >/dev/null
atomic_install_file "$TMP_CFG" "$HAPROXY_CFG"
uci set "$CONFIG.main.active_mode=generated"
uci commit "$CONFIG"
/etc/init.d/haproxy enable >/dev/null 2>&1 || true
if [ "$UHTTPD_CHANGED" = 1 ]; then
APPLIED=1
trap - EXIT
cleanup_temp
(
sleep 2
if /etc/init.d/uhttpd reload >/dev/null 2>&1 && /etc/init.d/haproxy restart >/dev/null 2>&1; then
logger -t haproxy-manager "LuCI and HAProxy reloaded successfully"
else
if /usr/libexec/haproxy-manager/rollback --sync "$BACKUP_ID" >/dev/null 2>&1; then
logger -t haproxy-manager "Service reload failed; restored $BACKUP_ID"
else
logger -t haproxy-manager "Service reload failed; could not restore $BACKUP_ID"
fi
fi
release_lock
) >/dev/null 2>&1 &
operation_lock_set_pid "$LOCK_DIR" "$!"
echo "Applied successfully. LuCI and HAProxy will reload shortly. Backup: $BACKUP_ID"
exit 0
fi
/etc/init.d/haproxy restart
APPLIED=1
trap - EXIT
cleanup
echo "Applied successfully. Backup: $BACKUP_ID"
@@ -0,0 +1,45 @@
#!/bin/sh
set -eu
. /usr/libexec/haproxy-manager/common.sh
umask 077
SRC="${1:-}"
is_safe_temp_path "$SRC" || { echo "Refusing unsafe raw config path: $SRC" >&2; exit 1; }
[ -f "$SRC" ] || { echo "Raw config file not found: $SRC" >&2; exit 1; }
LOCK_DIR=/var/lock/haproxy-manager.apply
operation_lock_acquire "$LOCK_DIR" || { echo "Another HAProxy Manager operation is already running." >&2; exit 1; }
TMP_RAW="/tmp/haproxy-manager-raw-apply.$$"
BACKUP_ID=""
APPLIED=0
restore_on_error() {
status=$?
trap - EXIT
if [ "$APPLIED" != 1 ] && [ -n "$BACKUP_ID" ]; then
/usr/libexec/haproxy-manager/rollback --sync "$BACKUP_ID" >/dev/null 2>&1 || true
logger -t haproxy-manager "Raw apply failed; restored $BACKUP_ID"
fi
rm -f "$TMP_RAW" "$SRC"
operation_lock_release "$LOCK_DIR"
exit "$status"
}
trap restore_on_error EXIT
cp "$SRC" "$TMP_RAW"
rm -f "$SRC"
/usr/libexec/haproxy-manager/validate "$TMP_RAW"
BACKUP_ID="$(basename "$(/usr/libexec/haproxy-manager/backup)")"
atomic_install_file "$TMP_RAW" "$HAPROXY_CFG"
uci set "$CONFIG.main.active_mode=raw"
uci commit "$CONFIG"
/etc/init.d/haproxy enable >/dev/null 2>&1 || true
/etc/init.d/haproxy restart
APPLIED=1
trap - EXIT
rm -f "$TMP_RAW" "$SRC"
operation_lock_release "$LOCK_DIR"
echo "Raw HAProxy config applied. Backup: $BACKUP_ID"
@@ -0,0 +1,59 @@
#!/bin/sh
set -eu
. /usr/libexec/haproxy-manager/common.sh
REQUESTED_BASE="$(backup_dir)"
STAMP="$(date +%Y%m%d-%H%M%S)"
umask 077
mkdir -p "$REQUESTED_BASE"
BASE="$(resolve_storage_dir "$REQUESTED_BASE")" || {
echo "Recovery point directory resolves outside /root or /mnt" >&2
exit 1
}
chmod 700 "$BASE" 2>/dev/null || true
DIR="$BASE/$STAMP"
attempt=1
while ! mkdir "$DIR" 2>/dev/null; do
[ "$attempt" -lt 5 ] || {
echo "Cannot create recovery point in $BASE after $attempt attempts" >&2
exit 1
}
sleep 1
attempt=$((attempt + 1))
STAMP="$(date +%Y%m%d-%H%M%S)"
DIR="$BASE/$STAMP"
done
chmod 700 "$DIR" 2>/dev/null || true
[ -f /etc/config/firewall ] && cp /etc/config/firewall "$DIR/firewall"
[ -f /etc/config/uhttpd ] && cp /etc/config/uhttpd "$DIR/uhttpd"
[ -f /etc/config/haproxy_manager ] && cp /etc/config/haproxy_manager "$DIR/haproxy_manager"
[ -f "$HAPROXY_CFG" ] && cp "$HAPROXY_CFG" "$DIR/haproxy.cfg"
{
if /etc/init.d/haproxy enabled >/dev/null 2>&1; then
printf 'enabled=1\n'
else
printf 'enabled=0\n'
fi
if /etc/init.d/haproxy status >/dev/null 2>&1; then
printf 'running=1\n'
else
printf 'running=0\n'
fi
} > "$DIR/haproxy.state"
printf '%s\n' "$STAMP" > "$BASE/LAST"
count=0
for old in $(find "$BASE" -maxdepth 1 -type d -name '[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-[0-9][0-9][0-9][0-9][0-9][0-9]' 2>/dev/null | sort -r); do
count=$((count + 1))
[ "$count" -le "$BACKUP_LIMIT" ] && continue
case "$old" in
"$BASE"/[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-[0-9][0-9][0-9][0-9][0-9][0-9])
rm -f "$old"/*
rmdir "$old"
;;
esac
done
printf '%s\n' "$DIR"
@@ -0,0 +1,22 @@
#!/bin/sh
. /usr/libexec/haproxy-manager/common.sh
BASE="$(backup_dir)"
[ ! -d "$BASE" ] || BASE="$(resolve_storage_dir "$BASE")" || exit 1
LAST=""
[ ! -r "$BASE/LAST" ] || LAST="$(basename "$(cat "$BASE/LAST")")"
count=0
for dir in $(find "$BASE" -maxdepth 1 -type d -name '[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-[0-9][0-9][0-9][0-9][0-9][0-9]' 2>/dev/null | sort -r); do
[ -d "$dir" ] || continue
id="$(basename "$dir")"
current=0
[ "$id" != "$LAST" ] || current=1
files=0
for file in firewall uhttpd haproxy_manager haproxy.cfg; do
[ ! -f "$dir/$file" ] || files=$((files + 1))
done
printf 'backup\t%s\t%s\t%s\n' "$id" "$current" "$files"
count=$((count + 1))
[ "$count" -lt "$BACKUP_LIMIT" ] || break
done
@@ -0,0 +1,361 @@
#!/bin/sh
CONFIG=haproxy_manager
HAPROXY_CFG=/etc/haproxy.cfg
TMP_CFG=/tmp/haproxy-manager-generated.cfg
BACKUP_LIMIT=7
INCIDENT_LIMIT=7
FIREWALL_RULE_NAME='HAProxy Manager: WAN listeners'
FIREWALL_DISABLED_PREFIX='HAProxy Manager disabled: '
uci_get() {
local section="$1"
local option="$2"
local default="${3:-}"
uci -q get "$CONFIG.$section.$option" 2>/dev/null || printf '%s\n' "$default"
}
backup_dir() {
local dir
dir="$(uci_get main backup_dir /root/haproxy-manager-backups)"
is_safe_storage_dir "$dir" || {
echo "Recovery point directory must be a plain path under /root or /mnt: $dir" >&2
return 1
}
printf '%s\n' "$dir"
}
incident_dir() {
local dir
dir="$(uci_get main incident_dir /root/haproxy-manager-incidents)"
is_safe_storage_dir "$dir" || {
echo "Incident directory must be a plain path under /root or /mnt: $dir" >&2
return 1
}
printf '%s\n' "$dir"
}
is_safe_storage_dir() {
case "$1" in
/root/*|/mnt/*) ;;
*) return 1 ;;
esac
case "/${1#/}/" in
*/../*|*/./*|*[!A-Za-z0-9_./-]*) return 1 ;;
esac
return 0
}
resolve_storage_dir() {
local dir="$1" resolved
[ -d "$dir" ] || return 1
resolved="$(cd "$dir" 2>/dev/null && pwd -P)" || return 1
is_safe_storage_dir "$resolved" || return 1
printf '%s\n' "$resolved"
}
is_safe_temp_path() {
local path="$1" name
name="${path#/tmp/}"
[ "$path" = "/tmp/$name" ] || return 1
case "$name" in
haproxy-manager-[A-Za-z0-9._-]*) return 0 ;;
*) return 1 ;;
esac
}
operation_lock_acquire() {
local lock_dir="$1" lock_pid
if mkdir "$lock_dir" 2>/dev/null; then
printf '%s\n' "$$" > "$lock_dir/pid"
return 0
fi
lock_pid="$(cat "$lock_dir/pid" 2>/dev/null || true)"
case "$lock_pid" in
''|*[!0-9]*) lock_pid=0 ;;
esac
if [ "$lock_pid" -gt 1 ] 2>/dev/null && kill -0 "$lock_pid" 2>/dev/null; then
return 1
fi
rm -f "$lock_dir/pid"
rmdir "$lock_dir" 2>/dev/null || return 1
mkdir "$lock_dir" 2>/dev/null || return 1
printf '%s\n' "$$" > "$lock_dir/pid"
}
operation_lock_set_pid() {
printf '%s\n' "$2" > "$1/pid"
}
operation_lock_release() {
rm -f "$1/pid"
rmdir "$1" 2>/dev/null || true
}
atomic_install_file() {
local source="$1" destination="$2" temporary mode
[ -f "$source" ] || return 1
temporary="$destination.haproxy-manager.$$"
rm -f "$temporary"
if ! cp "$source" "$temporary"; then
rm -f "$temporary"
return 1
fi
mode="$(stat -c '%a' "$destination" 2>/dev/null || stat -c '%a' "$source" 2>/dev/null || echo 600)"
chmod "$mode" "$temporary" || { rm -f "$temporary"; return 1; }
mv -f "$temporary" "$destination" || { rm -f "$temporary"; return 1; }
}
active_mode() {
local mode
mode="$(uci_get main active_mode)"
case "$mode" in
generated|raw|none) printf '%s\n' "$mode" ;;
*)
if [ "$(uci_get main enabled 0)" = 1 ] &&
grep -q '^# Generated by luci-app-haproxy-manager\.' "$HAPROXY_CFG" 2>/dev/null; then
printf 'generated\n'
else
printf 'none\n'
fi
;;
esac
}
wan_interface() {
local interface
interface="$(uci_get main wan_interface wan)"
case "$interface" in
''|*[!A-Za-z0-9_.-]*) return 1 ;;
esac
printf '%s\n' "$interface"
}
wan_ip() {
local configured interface
configured="$(uci_get main wan_bind_ip auto)"
if [ "$configured" != auto ] && [ -n "$configured" ]; then
printf '%s\n' "$configured"
return 0
fi
interface="$(wan_interface)" || return 1
ubus call "network.interface.$interface" status 2>/dev/null \
| jsonfilter -e '@["ipv4-address"][0].address' 2>/dev/null \
| grep -m1 .
}
lan_ip() {
local configured
configured="$(uci_get main lan_bind_ip auto)"
if [ "$configured" != auto ] && [ -n "$configured" ]; then
printf '%s\n' "$configured"
return 0
fi
ubus call network.interface.lan status 2>/dev/null \
| jsonfilter -e '@["ipv4-address"][0].address' 2>/dev/null \
| grep -m1 .
}
route_name() {
printf '%s' "$1" | tr '.:-' '___' | tr -cd 'A-Za-z0-9_'
}
is_valid_host() {
case "$1" in
''|[!A-Za-z0-9]*|*[!A-Za-z0-9]) return 1 ;;
*[!A-Za-z0-9.-]*) return 1 ;;
esac
return 0
}
is_valid_ip_or_host() {
case "$1" in
''|*[!A-Za-z0-9_.:-]*) return 1 ;;
esac
return 0
}
is_valid_backup_id() {
case "$1" in
[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-[0-9][0-9][0-9][0-9][0-9][0-9]) return 0 ;;
*) return 1 ;;
esac
}
backend_address() {
local host="$1"
local port="$2"
case "$host" in
*:*) printf '[%s]:%s\n' "$host" "$port" ;;
*) printf '%s:%s\n' "$host" "$port" ;;
esac
}
is_valid_port() {
local port="$1"
[ "$port" -ge 1 ] 2>/dev/null && [ "$port" -le 65535 ] 2>/dev/null
}
route_sections() {
uci -q show "$CONFIG" \
| sed -n "s/^$CONFIG\\.\\(@route\\[[0-9][0-9]*\\]\\)=route/\\1/p"
}
route_kind() {
local section="$1"
local kind
kind="$(uci_get "$section" kind)"
[ -n "$kind" ] || kind="$(uci_get "$section" protocol http)"
printf '%s\n' "$kind"
}
route_port_maps() {
local section="$1"
local kind="$2"
local public_port backend_port
case "$kind" in
ssh)
public_port="$(uci_get "$section" ssh_listen_port 22)"
backend_port="$(uci_get "$section" ssh_backend_port 22)"
printf '%s:%s\n' "$public_port" "$backend_port"
;;
rdp)
public_port="$(uci_get "$section" rdp_listen_port 3389)"
backend_port="$(uci_get "$section" rdp_backend_port 3389)"
printf '%s:%s\n' "$public_port" "$backend_port"
;;
custom)
for map in $(uci_get "$section" port_map); do
printf '%s\n' "$map"
done
;;
tcp)
public_port="$(uci_get "$section" listen_port "$(uci_get "$section" backend_port)")"
backend_port="$(uci_get "$section" backend_port)"
printf '%s:%s\n' "$public_port" "$backend_port"
;;
esac
}
is_valid_port_map() {
local map="$1"
local public_port backend_port
case "$map" in
*:*:*) return 1 ;;
*:*) ;;
*) return 1 ;;
esac
public_port="${map%%:*}"
backend_port="${map#*:}"
is_valid_port "$public_port" && is_valid_port "$backend_port"
}
required_ports() {
local section enabled kind http_enabled https_enabled map
local http_port https_port
http_port="$(uci_get main http_port 80)"
https_port="$(uci_get main https_port 443)"
[ "$(uci_get main enabled 0)" = 1 ] || return 0
for section in $(route_sections); do
enabled="$(uci_get "$section" enabled 1)"
[ "$enabled" = 1 ] || continue
kind="$(route_kind "$section")"
case "$kind" in
web)
http_enabled="$(uci_get "$section" web_http 1)"
https_enabled="$(uci_get "$section" web_https 1)"
[ "$http_enabled" != 1 ] || printf '%s\n' "$http_port"
[ "$https_enabled" != 1 ] || printf '%s\n' "$https_port"
;;
http)
printf '%s\n' "$http_port"
;;
https)
printf '%s\n' "$https_port"
;;
both)
printf '%s\n%s\n' "$http_port" "$https_port"
;;
ssh|rdp|custom|tcp)
for map in $(route_port_maps "$section" "$kind"); do
printf '%s\n' "${map%%:*}"
done
;;
esac
done | sort -nu
}
port_spec_contains() {
local spec="$1"
local wanted="$2"
local token first last
for token in $(printf '%s' "$spec" | tr ',' ' '); do
case "$token" in
*-*)
first="${token%%-*}"
last="${token#*-}"
[ "$wanted" -ge "$first" ] 2>/dev/null && [ "$wanted" -le "$last" ] 2>/dev/null && return 0
;;
*)
[ "$token" = "$wanted" ] && return 0
;;
esac
done
return 1
}
set_uhttpd_list() {
local option values current address
option="$1"
values="$2"
current="$(uci -q get "uhttpd.main.$option" 2>/dev/null || true)"
[ "$current" = "$values" ] && return 0
uci -q delete "uhttpd.main.$option" || true
for address in $values; do
uci add_list "uhttpd.main.$option=$address"
done
UHTTPD_CHANGED=1
}
capture_uhttpd_bindings() {
local saved_http saved_https address
[ "$(uci_get main uhttpd_bind_owned 0)" != 1 ] || return 0
saved_http="$(uci -q get uhttpd.main.listen_http 2>/dev/null || true)"
saved_https="$(uci -q get uhttpd.main.listen_https 2>/dev/null || true)"
uci -q delete "$CONFIG.main.uhttpd_saved_listen_http" || true
uci -q delete "$CONFIG.main.uhttpd_saved_listen_https" || true
for address in $saved_http; do
uci add_list "$CONFIG.main.uhttpd_saved_listen_http=$address"
done
for address in $saved_https; do
uci add_list "$CONFIG.main.uhttpd_saved_listen_https=$address"
done
uci set "$CONFIG.main.uhttpd_bind_owned=1"
uci commit "$CONFIG"
}
restore_uhttpd_bindings() {
[ "$(uci_get main uhttpd_bind_owned 0)" = 1 ] || return 0
set_uhttpd_list listen_http "$(uci_get main uhttpd_saved_listen_http)"
set_uhttpd_list listen_https "$(uci_get main uhttpd_saved_listen_https)"
uci -q delete "$CONFIG.main.uhttpd_saved_listen_http" || true
uci -q delete "$CONFIG.main.uhttpd_saved_listen_https" || true
uci -q delete "$CONFIG.main.uhttpd_bind_owned" || true
uci commit "$CONFIG"
}
@@ -0,0 +1,65 @@
#!/bin/sh
. /usr/libexec/haproxy-manager/common.sh
ZONE="$(uci_get main firewall_zone wan)"
MANAGED="$(uci_get main manage_firewall 0)"
MODE="$(uci_get main firewall_conflict_mode warn)"
PORTS="$(required_ports)"
WAN_POLICY=unknown
HAS_MANAGED_RULE=0
CONFLICTS=0
for zone in $(uci -q show firewall | sed -n 's/^firewall\.\(@zone\[[0-9][0-9]*\]\)=zone/\1/p'); do
[ "$(uci -q get "firewall.$zone.name")" = "$ZONE" ] || continue
WAN_POLICY="$(uci -q get "firewall.$zone.input" 2>/dev/null || echo REJECT)"
break
done
for rule in $(uci -q show firewall | sed -n 's/^firewall\.\(@rule\[[0-9][0-9]*\]\)=rule/\1/p'); do
name="$(uci -q get "firewall.$rule.name" 2>/dev/null || true)"
[ "$name" = "$FIREWALL_RULE_NAME" ] || continue
HAS_MANAGED_RULE=1
break
done
printf 'enabled\t%s\n' "$MANAGED"
printf 'zone\t%s\n' "$ZONE"
printf 'policy\t%s\n' "$WAN_POLICY"
printf 'mode\t%s\n' "$MODE"
printf 'managed_rule\t%s\n' "$HAS_MANAGED_RULE"
for port in $PORTS; do
printf 'port\t%s\n' "$port"
done
for redirect in $(uci -q show firewall | sed -n 's/^firewall\.\(@redirect\[[0-9][0-9]*\]\)=redirect/\1/p'); do
redirect_enabled="$(uci -q get "firewall.$redirect.enabled" 2>/dev/null || echo 1)"
redirect_name="$(uci -q get "firewall.$redirect.name" 2>/dev/null || true)"
if [ "$redirect_enabled" != 1 ]; then
case "$redirect_name" in
"$FIREWALL_DISABLED_PREFIX"*)
[ "$MANAGED" = 1 ] && [ "$MODE" != disable ] || continue
;;
*) continue ;;
esac
fi
[ "$(uci -q get "firewall.$redirect.src" 2>/dev/null || true)" = "$ZONE" ] || continue
proto="$(uci -q get "firewall.$redirect.proto" 2>/dev/null || echo 'tcp udp')"
case " $proto " in *' tcp '*|*' tcpudp '*) ;; *) continue ;; esac
spec="$(uci -q get "firewall.$redirect.src_dport" 2>/dev/null || true)"
[ -n "$spec" ] || continue
for port in $PORTS; do
port_spec_contains "$spec" "$port" || continue
name="${redirect_name:-$redirect}"
dest_ip="$(uci -q get "firewall.$redirect.dest_ip" 2>/dev/null || true)"
dest_port="$(uci -q get "firewall.$redirect.dest_port" 2>/dev/null || true)"
printf 'conflict\t%s\t%s\t%s\t%s\t%s\n' "$redirect" "$name" "$spec" "$dest_ip" "$dest_port"
CONFLICTS=$((CONFLICTS + 1))
break
done
done
printf 'conflicts\t%s\n' "$CONFLICTS"
@@ -0,0 +1,106 @@
#!/bin/sh
set -eu
. /usr/libexec/haproxy-manager/common.sh
umask 077
CHECK_ONLY=0
FORCE_DISABLED=0
case "${1:-}" in
'') ;;
--check) CHECK_ONLY=1 ;;
--disable) FORCE_DISABLED=1 ;;
*) echo "Usage: firewall-sync [--check|--disable]" >&2; exit 1 ;;
esac
ZONE="$(uci_get main firewall_zone wan)"
MODE="$(uci_get main firewall_conflict_mode warn)"
MANAGED="$(uci_get main manage_firewall 0)"
[ "$FORCE_DISABLED" != 1 ] || MANAGED=0
PLAN=/tmp/haproxy-manager-firewall-plan.$$
/usr/libexec/haproxy-manager/firewall-plan > "$PLAN"
trap 'rm -f "$PLAN"' EXIT
CONFLICTS="$(sed -n 's/^conflicts[[:space:]]*//p' "$PLAN")"
if [ "$MANAGED" = 1 ] && [ "${CONFLICTS:-0}" -gt 0 ] && [ "$MODE" != disable ]; then
echo "Enabled firewall port forwards conflict with HAProxy listeners:" >&2
awk -F '\t' '$1 == "conflict" { printf "- %s (WAN %s -> %s:%s)\n", $3, $4, $5, $6 }' "$PLAN" >&2
echo "Choose automatic conflict resolution in Settings or disable the redirects manually." >&2
exit 3
fi
[ "$CHECK_ONLY" != 1 ] || { cat "$PLAN"; exit 0; }
PORTS=""
[ "$MANAGED" != 1 ] || PORTS="$(sed -n 's/^port[[:space:]]*//p' "$PLAN")"
CHANGED=0
for redirect in $(uci -q show firewall | sed -n 's/^firewall\.\(@redirect\[[0-9][0-9]*\]\)=redirect/\1/p'); do
name="$(uci -q get "firewall.$redirect.name" 2>/dev/null || true)"
case "$name" in
"$FIREWALL_DISABLED_PREFIX"*) managed_disabled=1 ;;
*) managed_disabled=0 ;;
esac
[ "$managed_disabled" = 1 ] || continue
spec="$(uci -q get "firewall.$redirect.src_dport" 2>/dev/null || true)"
still_needed=0
for port in $PORTS; do
port_spec_contains "$spec" "$port" || continue
still_needed=1
break
done
[ "$still_needed" = 1 ] && continue
uci set "firewall.$redirect.enabled=1"
CHANGED=1
name="${name#"$FIREWALL_DISABLED_PREFIX"}"
if [ -n "$name" ]; then
uci set "firewall.$redirect.name=$name"
else
uci -q delete "firewall.$redirect.name" || true
fi
done
if [ "$MANAGED" = 1 ] && [ "$MODE" = disable ]; then
for redirect in $(awk -F '\t' '$1 == "conflict" { print $2 }' "$PLAN"); do
[ -n "$redirect" ] || continue
name="$(uci -q get "firewall.$redirect.name" 2>/dev/null || true)"
case "$name" in
"$FIREWALL_DISABLED_PREFIX"*) ;;
*) uci set "firewall.$redirect.name=$FIREWALL_DISABLED_PREFIX$name" ;;
esac
uci set "firewall.$redirect.enabled=0"
CHANGED=1
done
fi
for index in $(uci -q show firewall | sed -n 's/^firewall\.@rule\[\([0-9][0-9]*\)\]=rule/\1/p' | sort -rn); do
rule="@rule[$index]"
name="$(uci -q get "firewall.$rule.name" 2>/dev/null || true)"
if [ "$name" = "$FIREWALL_RULE_NAME" ]; then
uci -q delete "firewall.$rule" || true
CHANGED=1
fi
done
if [ -n "$PORTS" ]; then
rule="$(uci add firewall rule)"
uci set "firewall.$rule.name=$FIREWALL_RULE_NAME"
uci set "firewall.$rule.src=$ZONE"
uci set "firewall.$rule.proto=tcp"
uci set "firewall.$rule.target=ACCEPT"
uci set "firewall.$rule.family=ipv4"
for port in $PORTS; do
uci add_list "firewall.$rule.dest_port=$port"
done
CHANGED=1
fi
if [ "$CHANGED" = 1 ]; then
uci commit firewall
/etc/init.d/firewall reload
fi
if [ "$MANAGED" = 1 ]; then
echo "Firewall rules synchronized"
else
echo "Firewall automation disabled; managed rules removed"
fi
@@ -0,0 +1,234 @@
#!/bin/sh
set -eu
. /usr/libexec/haproxy-manager/common.sh
umask 077
OUT="${1:-$TMP_CFG}"
is_safe_temp_path "$OUT" || { echo "Refusing unsafe generated config path: $OUT" >&2; exit 1; }
[ ! -d "$OUT" ] || { echo "Refusing generated config directory: $OUT" >&2; exit 1; }
WORK_OUT="$OUT.new.$$"
ENABLED="$(uci_get main enabled 0)"
HTTP_PORT="$(uci_get main http_port 80)"
HTTPS_PORT="$(uci_get main https_port 443)"
WAN_IP="$(wan_ip || true)"
if [ "$ENABLED" != 1 ]; then
echo "Managed HAProxy configuration is disabled." >&2
exit 2
fi
is_valid_port "$HTTP_PORT" || { echo "Invalid public HTTP port: $HTTP_PORT" >&2; exit 1; }
is_valid_port "$HTTPS_PORT" || { echo "Invalid public HTTPS port: $HTTPS_PORT" >&2; exit 1; }
if [ -z "$WAN_IP" ]; then
echo "Cannot detect WAN IPv4 address. Set the WAN bind address manually." >&2
exit 1
fi
is_valid_ip_or_host "$WAN_IP" || { echo "Invalid WAN bind address: $WAN_IP" >&2; exit 1; }
TMP_HTTP_RULES=/tmp/haproxy-manager-http-rules.$$
TMP_HTTP_BACKENDS=/tmp/haproxy-manager-http-backends.$$
TMP_TLS_RULES=/tmp/haproxy-manager-tls-rules.$$
TMP_TLS_BACKENDS=/tmp/haproxy-manager-tls-backends.$$
TMP_TCP_FRONTENDS=/tmp/haproxy-manager-tcp-frontends.$$
TMP_PORTS=/tmp/haproxy-manager-listen-ports.$$
: > "$TMP_HTTP_RULES"
: > "$TMP_HTTP_BACKENDS"
: > "$TMP_TLS_RULES"
: > "$TMP_TLS_BACKENDS"
: > "$TMP_TCP_FRONTENDS"
: > "$TMP_PORTS"
trap 'rm -f "$WORK_OUT" "$TMP_HTTP_RULES" "$TMP_HTTP_BACKENDS" "$TMP_TLS_RULES" "$TMP_TLS_BACKENDS" "$TMP_TCP_FRONTENDS" "$TMP_PORTS"' EXIT
HAS_HTTP=0
HAS_HTTPS=0
HAS_TCP=0
claim_port() {
local port="$1"
local owner="$2"
local existing
if grep -q "^${port}[[:space:]]" "$TMP_PORTS"; then
existing="$(sed -n "s/^${port}[[:space:]]*//p" "$TMP_PORTS" | head -1)"
echo "Public TCP port $port conflicts with ${existing:-another service}." >&2
exit 1
fi
printf '%s\t%s\n' "$port" "$owner" >> "$TMP_PORTS"
}
add_http_route() {
local name="$1" host="$2" backend_host="$3" backend_port="$4"
local address
address="$(backend_address "$backend_host" "$backend_port")"
if [ "$HAS_HTTP" != 1 ]; then
claim_port "$HTTP_PORT" "the shared Web HTTP listener"
HAS_HTTP=1
fi
{
printf ' acl host_%s hdr(host),host_only -i %s\n' "$name" "$host"
printf ' use_backend be_http_%s if host_%s\n' "$name" "$name"
} >> "$TMP_HTTP_RULES"
{
printf 'backend be_http_%s\n' "$name"
printf ' mode http\n'
printf ' option httpchk GET /\n'
printf ' server srv1 %s check\n\n' "$address"
} >> "$TMP_HTTP_BACKENDS"
}
add_https_route() {
local name="$1" host="$2" backend_host="$3" backend_port="$4"
local address
address="$(backend_address "$backend_host" "$backend_port")"
if [ "$HAS_HTTPS" != 1 ]; then
claim_port "$HTTPS_PORT" "the shared Web HTTPS listener"
HAS_HTTPS=1
fi
printf ' use_backend be_tls_%s if { req.ssl_sni -i %s }\n' "$name" "$host" >> "$TMP_TLS_RULES"
{
printf 'backend be_tls_%s\n' "$name"
printf ' mode tcp\n'
printf ' server srv1 %s check\n\n' "$address"
} >> "$TMP_TLS_BACKENDS"
}
add_tcp_route() {
local name="$1" label="$2" backend_host="$3" public_port="$4" backend_port="$5"
local address bind_address
address="$(backend_address "$backend_host" "$backend_port")"
bind_address="$(backend_address "$WAN_IP" "$public_port")"
claim_port "$public_port" "service $label"
HAS_TCP=1
{
printf 'frontend fe_tcp_%s\n' "$name"
printf ' bind %s\n' "$bind_address"
printf ' mode tcp\n'
printf ' default_backend be_tcp_%s\n\n' "$name"
printf 'backend be_tcp_%s\n' "$name"
printf ' mode tcp\n'
printf ' server srv1 %s check\n\n' "$address"
} >> "$TMP_TCP_FRONTENDS"
}
for section in $(route_sections); do
enabled="$(uci_get "$section" enabled 1)"
[ "$enabled" = 1 ] || continue
kind="$(route_kind "$section")"
label="$(uci_get "$section" name)"
host="$(uci_get "$section" host)"
backend_host="$(uci_get "$section" backend_host)"
[ -n "$label" ] || label="$host"
[ -n "$label" ] || label="$kind"
name="$(route_name "${label}_${section}")"
is_valid_ip_or_host "$backend_host" || { echo "Invalid backend host for $label: $backend_host" >&2; exit 1; }
case "$kind" in
web)
is_valid_host "$host" || { echo "Invalid or missing domain for Web service $label: $host" >&2; exit 1; }
http_enabled="$(uci_get "$section" web_http 1)"
https_enabled="$(uci_get "$section" web_https 1)"
[ "$http_enabled" = 1 ] || [ "$https_enabled" = 1 ] || { echo "Web service $label has no enabled protocol." >&2; exit 1; }
if [ "$http_enabled" = 1 ]; then
backend_port="$(uci_get "$section" backend_http_port 80)"
is_valid_port "$backend_port" || { echo "Invalid HTTP backend port for $label: $backend_port" >&2; exit 1; }
add_http_route "$name" "$host" "$backend_host" "$backend_port"
fi
if [ "$https_enabled" = 1 ]; then
backend_port="$(uci_get "$section" backend_https_port 443)"
is_valid_port "$backend_port" || { echo "Invalid HTTPS backend port for $label: $backend_port" >&2; exit 1; }
add_https_route "$name" "$host" "$backend_host" "$backend_port"
fi
;;
http|https|both)
is_valid_host "$host" || { echo "Invalid or missing domain for legacy service $label: $host" >&2; exit 1; }
backend_port="$(uci_get "$section" backend_port)"
is_valid_port "$backend_port" || { echo "Invalid backend port for $label: $backend_port" >&2; exit 1; }
case "$kind" in http|both) add_http_route "$name" "$host" "$backend_host" "$backend_port" ;; esac
case "$kind" in https|both) add_https_route "$name" "$host" "$backend_host" "$backend_port" ;; esac
;;
ssh|rdp|custom|tcp)
map_index=0
maps="$(route_port_maps "$section" "$kind")"
[ -n "$maps" ] || { echo "No TCP port mappings configured for $label." >&2; exit 1; }
for map in $maps; do
is_valid_port_map "$map" || { echo "Invalid TCP port mapping for $label: $map" >&2; exit 1; }
public_port="${map%%:*}"
backend_port="${map#*:}"
map_index=$((map_index + 1))
add_tcp_route "${name}_${map_index}" "$label" "$backend_host" "$public_port" "$backend_port"
done
;;
*)
echo "Invalid service type for $label: $kind" >&2
exit 1
;;
esac
done
if [ "$HAS_HTTP" != 1 ] && [ "$HAS_HTTPS" != 1 ] && [ "$HAS_TCP" != 1 ]; then
echo "No enabled services define a HAProxy listener." >&2
exit 2
fi
{
printf '# Generated by luci-app-haproxy-manager. Manual changes may be overwritten.\n'
printf 'global\n'
printf ' log /dev/log local0\n'
printf ' maxconn 2048\n'
printf ' daemon\n\n'
printf 'defaults\n'
printf ' log global\n'
printf ' timeout connect 5s\n'
printf ' timeout client 60s\n'
printf ' timeout server 60s\n'
printf ' option dontlognull\n\n'
if [ "$HAS_HTTP" = 1 ]; then
http_bind="$(backend_address "$WAN_IP" "$HTTP_PORT")"
printf 'frontend fe_web_http\n'
printf ' bind %s\n' "$http_bind"
printf ' mode http\n'
printf ' option httplog\n'
cat "$TMP_HTTP_RULES"
printf ' default_backend be_not_found\n\n'
cat "$TMP_HTTP_BACKENDS"
fi
if [ "$HAS_HTTPS" = 1 ]; then
https_bind="$(backend_address "$WAN_IP" "$HTTPS_PORT")"
printf 'frontend fe_web_https\n'
printf ' bind %s\n' "$https_bind"
printf ' mode tcp\n'
printf ' tcp-request inspect-delay 5s\n'
printf ' tcp-request content accept if { req.ssl_hello_type 1 }\n'
cat "$TMP_TLS_RULES"
printf ' default_backend be_tls_not_found\n\n'
cat "$TMP_TLS_BACKENDS"
fi
cat "$TMP_TCP_FRONTENDS"
printf 'backend be_not_found\n'
printf ' mode http\n'
printf ' http-request return status 404 content-type text/plain string "No HAProxy route matched this host.\\n"\n\n'
printf 'backend be_tls_not_found\n'
printf ' mode tcp\n'
printf ' tcp-request content reject\n'
} > "$WORK_OUT"
# Replace the path itself so a stale symlink cannot redirect this root write.
rm -f "$OUT"
mv "$WORK_OUT" "$OUT"
printf '%s\n' "$OUT"
@@ -0,0 +1,13 @@
#!/bin/sh
set -eu
. /usr/libexec/haproxy-manager/common.sh
ID="${1:-}"
is_valid_backup_id "$ID" || { echo "Invalid incident identifier" >&2; exit 1; }
BASE="$(incident_dir)"
[ ! -d "$BASE" ] || BASE="$(resolve_storage_dir "$BASE")" || exit 1
FILE="$BASE/$ID/diagnostics.log"
[ -f "$FILE" ] || { echo "Incident report not found: $ID" >&2; exit 1; }
# Keep the RPC response bounded even if a platform command produced excessive output.
tail -c 65536 "$FILE"
@@ -0,0 +1,28 @@
#!/bin/sh
. /usr/libexec/haproxy-manager/common.sh
BASE="$(incident_dir)"
[ ! -d "$BASE" ] || BASE="$(resolve_storage_dir "$BASE")" || exit 1
LAST=""
[ ! -r "$BASE/LAST" ] || LAST="$(basename "$(sed -n '1p' "$BASE/LAST")")"
safe_field() {
printf '%s' "$1" | tr '\t\r\n' ' '
}
count=0
for dir in $(find "$BASE" -maxdepth 1 -type d -name '[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-[0-9][0-9][0-9][0-9][0-9][0-9]' 2>/dev/null | sort -r); do
[ -d "$dir" ] || continue
id="$(basename "$dir")"
latest=0
[ "$id" != "$LAST" ] || latest=1
result="$(sed -n '1p' "$dir/result" 2>/dev/null || echo detected)"
action="$(sed -n 's/^action=//p' "$dir/meta" 2>/dev/null | head -1)"
interface="$(sed -n 's/^interface=//p' "$dir/meta" 2>/dev/null | head -1)"
reason="$(sed -n 's/^reason=//p' "$dir/meta" 2>/dev/null | head -1)"
printf 'incident\t%s\t%s\t%s\t%s\t%s\t%s\n' \
"$id" "$latest" "$(safe_field "$result")" "$(safe_field "$action")" \
"$(safe_field "$interface")" "$(safe_field "$reason")"
count=$((count + 1))
[ "$count" -lt "$INCIDENT_LIMIT" ] || break
done
@@ -0,0 +1,150 @@
#!/bin/sh
set -eu
. /usr/libexec/haproxy-manager/common.sh
migrate_firewall_markers() {
local section marker name changed
changed=0
for section in $(uci -q show firewall | sed -n 's/^firewall\.\(@redirect\[[0-9][0-9]*\]\)=redirect/\1/p'); do
marker="$(uci -q get "firewall.$section.hm_disabled_by_haproxy_manager" 2>/dev/null || true)"
[ -n "$marker" ] || continue
if [ "$marker" = 1 ]; then
name="$(uci -q get "firewall.$section.name" 2>/dev/null || true)"
case "$name" in
"$FIREWALL_DISABLED_PREFIX"*) ;;
*) uci set "firewall.$section.name=$FIREWALL_DISABLED_PREFIX$name" ;;
esac
fi
uci -q delete "firewall.$section.hm_disabled_by_haproxy_manager" || true
changed=1
done
for section in $(uci -q show firewall | sed -n 's/^firewall\.\(@rule\[[0-9][0-9]*\]\)=rule/\1/p'); do
marker="$(uci -q get "firewall.$section.hm_managed" 2>/dev/null || true)"
[ -n "$marker" ] || continue
[ "$marker" != 1 ] || uci set "firewall.$section.name=$FIREWALL_RULE_NAME"
uci -q delete "firewall.$section.hm_managed" || true
changed=1
done
[ "$changed" != 1 ] || uci commit firewall
}
migrate_firewall_markers
SCHEMA_VERSION="$(uci_get main schema_version 1)"
MIGRATED=0
if [ "$SCHEMA_VERSION" -lt 2 ] 2>/dev/null; then
SECTIONS="$(route_sections)"
[ -z "$SECTIONS" ] || /usr/libexec/haproxy-manager/backup >/dev/null
CONSUMED=/tmp/haproxy-manager-migrate-consumed.$$
: > "$CONSUMED"
trap 'rm -f "$CONSUMED"' EXIT
is_consumed() {
grep -qx "$1" "$CONSUMED"
}
to_web() {
local section="$1" http_enabled="$2" https_enabled="$3" http_backend="$4" https_backend="$5"
uci set "$CONFIG.$section.kind=web"
uci set "$CONFIG.$section.web_http=$http_enabled"
uci set "$CONFIG.$section.web_https=$https_enabled"
[ "$http_enabled" != 1 ] || uci set "$CONFIG.$section.backend_http_port=$http_backend"
[ "$https_enabled" != 1 ] || uci set "$CONFIG.$section.backend_https_port=$https_backend"
uci -q delete "$CONFIG.$section.protocol" || true
uci -q delete "$CONFIG.$section.listen_port" || true
uci -q delete "$CONFIG.$section.backend_port" || true
}
clean_web_name() {
local section="$1" host="$2" name clean
name="$(uci_get "$section" name)"
clean="$name"
case "$name" in
"$host 80"|"$host:80"|"$host HTTP"|"$host http") clean="$host" ;;
*" HTTP") clean="${name% HTTP}" ;;
*" http") clean="${name% http}" ;;
esac
[ -z "$clean" ] || [ "$clean" = "$name" ] || uci set "$CONFIG.$section.name=$clean"
}
for section in $SECTIONS; do
protocol="$(uci_get "$section" protocol)"
case "$protocol" in
http)
host="$(uci_get "$section" host)"
backend_host="$(uci_get "$section" backend_host)"
enabled="$(uci_get "$section" enabled 1)"
http_backend="$(uci_get "$section" backend_port 80)"
match=""
for candidate in $SECTIONS; do
is_consumed "$candidate" && continue
[ "$(uci_get "$candidate" protocol)" = https ] || continue
[ "$(uci_get "$candidate" host)" = "$host" ] || continue
[ "$(uci_get "$candidate" backend_host)" = "$backend_host" ] || continue
[ "$(uci_get "$candidate" enabled 1)" = "$enabled" ] || continue
match="$candidate"
break
done
if [ -n "$match" ]; then
clean_web_name "$section" "$host"
to_web "$section" 1 1 "$http_backend" "$(uci_get "$match" backend_port 443)"
printf '%s\n' "$match" >> "$CONSUMED"
else
clean_web_name "$section" "$host"
to_web "$section" 1 0 "$http_backend" 443
fi
;;
both)
backend="$(uci_get "$section" backend_port 80)"
clean_web_name "$section" "$(uci_get "$section" host)"
to_web "$section" 1 1 "$backend" "$backend"
;;
tcp)
public_port="$(uci_get "$section" listen_port "$(uci_get "$section" backend_port)")"
backend_port="$(uci_get "$section" backend_port)"
uci set "$CONFIG.$section.kind=custom"
uci -q delete "$CONFIG.$section.port_map" || true
uci add_list "$CONFIG.$section.port_map=$public_port:$backend_port"
uci -q delete "$CONFIG.$section.protocol" || true
uci -q delete "$CONFIG.$section.listen_port" || true
uci -q delete "$CONFIG.$section.backend_port" || true
;;
esac
done
for section in $SECTIONS; do
is_consumed "$section" && continue
[ "$(uci_get "$section" protocol)" = https ] || continue
to_web "$section" 0 1 80 "$(uci_get "$section" backend_port 443)"
done
sed -n 's/^@route\[\([0-9][0-9]*\)\]$/\1/p' "$CONSUMED" | sort -rn | while read -r index; do
uci -q delete "$CONFIG.@route[$index]" || true
done
MIGRATED=1
fi
if [ "$SCHEMA_VERSION" -lt 3 ] 2>/dev/null; then
if [ "$(uci_get main enabled 0)" = 1 ] &&
grep -q '^# Generated by luci-app-haproxy-manager\.' "$HAPROXY_CFG" 2>/dev/null; then
uci set "$CONFIG.main.active_mode=generated"
else
uci set "$CONFIG.main.active_mode=none"
fi
uci set "$CONFIG.main.schema_version=3"
uci commit "$CONFIG"
MIGRATED=1
fi
[ "$MIGRATED" != 1 ] || echo "Configuration schema upgraded to version 3"
@@ -0,0 +1,44 @@
#!/bin/sh
set -eu
. /usr/libexec/haproxy-manager/common.sh
[ "$(uci_get main webhook_enabled 0)" = 1 ] || exit 0
URL="$(uci_get main webhook_url)"
case "$URL" in
http://*) AUTHORITY="${URL#http://}" ;;
https://*) AUTHORITY="${URL#https://}" ;;
*) echo "Webhook URL must use HTTP or HTTPS" >&2; exit 1 ;;
esac
[ -n "${AUTHORITY%%/*}" ] || { echo "Webhook URL must include a host" >&2; exit 1; }
case "$URL" in
*[![:graph:]]*) echo "Webhook URL contains invalid characters" >&2; exit 1 ;;
esac
if command -v uclient-fetch >/dev/null 2>&1; then
FETCHER=uclient-fetch
elif command -v wget >/dev/null 2>&1; then
FETCHER=wget
else
echo "A compatible wget implementation is required for webhook delivery" >&2
exit 1
fi
[ -r /usr/share/libubox/jshn.sh ] || { echo "jshn.sh is not available" >&2; exit 1; }
INCIDENT_ID="$(printf '%s' "${1:-unknown}" | tr -cd '0-9-')"
RESULT="$(printf '%s' "${2:-unknown}" | tr -cd 'A-Za-z0-9_.:-')"
ACTION="$(printf '%s' "${3:-unknown}" | tr -cd 'A-Za-z0-9_.:-')"
INTERFACE="$(printf '%s' "${4:-unknown}" | tr -cd 'A-Za-z0-9_.:-')"
REASON="$(printf '%s' "${5:-unknown}" | tr -cd 'A-Za-z0-9_.:-')"
. /usr/share/libubox/jshn.sh
json_init
json_add_string event haproxy-manager-recovery
json_add_string incident "$INCIDENT_ID"
json_add_string result "$RESULT"
json_add_string action "$ACTION"
json_add_string interface "$INTERFACE"
json_add_string reason "$REASON"
PAYLOAD="$(json_dump)"
"$FETCHER" -q -T 10 -O /dev/null \
--header='Content-Type: application/json' \
--post-data="$PAYLOAD" "$URL"
@@ -0,0 +1,4 @@
#!/bin/sh
. /usr/libexec/haproxy-manager/common.sh
required_ports
@@ -0,0 +1,259 @@
#!/bin/sh
set -eu
. /usr/libexec/haproxy-manager/common.sh
umask 077
ACTION="$(printf '%s' "${1:-network}" | tr -cd 'A-Za-z0-9_.:-')"
INTERFACE="$(printf '%s' "${2:-unknown}" | tr -cd 'A-Za-z0-9_.:-')"
RECOVER_LOCK=/var/lock/haproxy-manager.recover
APPLY_LOCK=/var/lock/haproxy-manager.apply
APPLY_LOCK_HELD=0
TMP_CFG="/tmp/haproxy-manager-recover.$$"
INCIDENT=""
BASE=""
DIAGNOSTICS=/dev/null
[ -n "$ACTION" ] || ACTION=network
[ -n "$INTERFACE" ] || INTERFACE=unknown
[ "$(uci_get main auto_recover 1)" = 1 ] || exit 0
[ "$(uci_get main enabled 0)" = 1 ] || exit 0
RECONCILE_GENERATED=0
case "$ACTION" in
ifup|ifupdate)
[ "$INTERFACE" = "$(wan_interface)" ] || exit 0
RECONCILE_GENERATED=1
;;
esac
[ -x /etc/init.d/haproxy ] || exit 0
/etc/init.d/haproxy enabled >/dev/null 2>&1 || exit 0
MODE="$(active_mode)"
case "$MODE" in
generated|raw) ;;
*) exit 0 ;;
esac
cleanup() {
rm -f "$TMP_CFG"
[ "$APPLY_LOCK_HELD" != 1 ] || operation_lock_release "$APPLY_LOCK"
operation_lock_release "$RECOVER_LOCK"
}
trim_incidents() {
local count old
count=0
for old in $(find "$BASE" -maxdepth 1 -type d -name '[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-[0-9][0-9][0-9][0-9][0-9][0-9]' 2>/dev/null | sort -r); do
count=$((count + 1))
[ "$count" -le "$INCIDENT_LIMIT" ] && continue
case "$old" in
"$BASE"/[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-[0-9][0-9][0-9][0-9][0-9][0-9])
rm -f "$old/meta" "$old/result" "$old/diagnostics.log"
rmdir "$old" 2>/dev/null || true
;;
esac
done
}
create_incident() {
local reason="$1" id attempt target_ip requested_base
requested_base="$(incident_dir)" || return 1
umask 077
mkdir -p "$requested_base" || {
logger -t haproxy-manager "Cannot create incident report directory: $requested_base"
return 1
}
BASE="$(resolve_storage_dir "$requested_base")" || {
logger -t haproxy-manager "Incident directory resolves outside /root or /mnt"
return 1
}
id="$(date +%Y%m%d-%H%M%S)"
INCIDENT="$BASE/$id"
attempt=1
while ! mkdir "$INCIDENT" 2>/dev/null; do
[ "$attempt" -lt 5 ] || {
logger -t haproxy-manager "Cannot create incident report in $BASE"
return 1
}
sleep 1
attempt=$((attempt + 1))
id="$(date +%Y%m%d-%H%M%S)"
INCIDENT="$BASE/$id"
done
chmod 700 "$BASE" "$INCIDENT" 2>/dev/null || true
target_ip="$(wan_ip 2>/dev/null || echo unknown)"
cat > "$INCIDENT/meta" <<EOF || return 1
created=$id
action=$ACTION
interface=$INTERFACE
reason=$reason
target_ip=$target_ip
EOF
printf 'detected\n' > "$INCIDENT/result" || return 1
printf '%s\n' "$id" > "$BASE/LAST" || return 1
trim_incidents
DIAGNOSTICS="$INCIDENT/diagnostics.log"
if [ "$reason" = wan-address-change ]; then
{
printf 'HAProxy Manager WAN listener reconciliation\n'
printf 'Incident: %s\nAction: %s\nInterface: %s\nReason: %s\nTarget WAN address: %s\n' \
"$id" "$ACTION" "$INTERFACE" "$reason" "$target_ip"
} > "$DIAGNOSTICS" 2>&1
return 0
fi
{
printf 'HAProxy Manager automatic recovery\n'
printf 'Incident: %s\nAction: %s\nInterface: %s\nReason: %s\nTarget WAN address: %s\n\n' \
"$id" "$ACTION" "$INTERFACE" "$reason" "$target_ip"
printf '== Time and uptime ==\n'
date || true
uptime || true
printf '\n== Memory ==\n'
free || true
printf '\n== IPv4 addresses ==\n'
ip -4 addr show || true
printf '\n== HAProxy procd state ==\n'
ubus call service list '{"name":"haproxy","verbose":true}' || true
printf '\n== Network interfaces ==\n'
ubus call network.interface dump || true
printf '\n== Recent HAProxy, procd, and OOM messages ==\n'
logread | grep -iE 'haproxy|procd.*instance|out of memory|oom|killed process' | tail -n 160 || true
printf '\n== Kernel OOM messages ==\n'
dmesg | grep -Ei 'oom|out of memory|killed process|haproxy' | tail -n 80 || true
} > "$DIAGNOSTICS" 2>&1
}
set_result() {
local result="$1" id reason
if [ -z "$INCIDENT" ] || [ ! -d "$INCIDENT" ]; then
logger -t haproxy-manager "Recovery result without incident report: $result"
return 0
fi
if ! printf '%s\n' "$result" > "$INCIDENT/result"; then
logger -t haproxy-manager "Could not update incident report with result: $result"
INCIDENT=""
DIAGNOSTICS=/dev/null
return 0
fi
id="${INCIDENT##*/}"
reason="$(sed -n 's/^reason=//p' "$INCIDENT/meta" 2>/dev/null | head -1)"
(
if ! /usr/libexec/haproxy-manager/notify "$id" "$result" "$ACTION" "$INTERFACE" "$reason"; then
logger -t haproxy-manager "Webhook notification failed for incident $id"
fi
) >/dev/null 2>&1 &
}
operation_lock_acquire "$RECOVER_LOCK" || exit 0
trap cleanup EXIT
# Let netifd finish installing addresses and routes before comparing listeners.
sleep 2
operation_lock_acquire "$APPLY_LOCK" || exit 0
APPLY_LOCK_HELD=1
CONFIG_CHANGED=0
GENERATION_FAILED=0
VALIDATION_FAILED=0
if [ "$RECONCILE_GENERATED" = 1 ] && [ "$MODE" = generated ] &&
[ "$(uci_get main wan_bind_ip auto)" = auto ]; then
if /usr/libexec/haproxy-manager/generate "$TMP_CFG" >/dev/null 2>&1; then
if /usr/libexec/haproxy-manager/validate "$TMP_CFG" >/dev/null 2>&1; then
cmp -s "$TMP_CFG" "$HAPROXY_CFG" || CONFIG_CHANGED=1
else
VALIDATION_FAILED=1
fi
else
GENERATION_FAILED=1
fi
fi
if [ "$GENERATION_FAILED" != 1 ] && [ "$VALIDATION_FAILED" != 1 ] &&
[ "$CONFIG_CHANGED" != 1 ] && /etc/init.d/haproxy status >/dev/null 2>&1; then
exit 0
fi
if [ "$GENERATION_FAILED" = 1 ]; then
REASON=wan-address-unavailable
elif [ "$VALIDATION_FAILED" = 1 ]; then
REASON=invalid-generated-config
elif [ "$CONFIG_CHANGED" = 1 ]; then
REASON=wan-address-change
else
REASON=service-stopped
fi
if ! create_incident "$REASON"; then
logger -t haproxy-manager "Incident report could not be stored; recovery will continue"
INCIDENT=""
DIAGNOSTICS=/dev/null
fi
logger -t haproxy-manager "HAProxy recovery started after $ACTION on $INTERFACE ($REASON)"
if [ "$GENERATION_FAILED" = 1 ]; then
set_result address-unavailable
logger -t haproxy-manager "Automatic recovery stopped: WAN address is unavailable"
exit 1
fi
if [ "$VALIDATION_FAILED" = 1 ]; then
set_result invalid-generated-config
logger -t haproxy-manager "Automatic recovery stopped: generated configuration is invalid"
exit 1
fi
if [ "$CONFIG_CHANGED" = 1 ]; then
BACKUP_PATH=""
if ! BACKUP_PATH="$(/usr/libexec/haproxy-manager/backup 2>> "$DIAGNOSTICS")"; then
set_result backup-failed
logger -t haproxy-manager "Automatic recovery stopped: snapshot creation failed"
exit 1
fi
printf '\nRecovery point: %s\n' "${BACKUP_PATH##*/}" >> "$DIAGNOSTICS" || true
if ! atomic_install_file "$TMP_CFG" "$HAPROXY_CFG"; then
set_result install-failed
logger -t haproxy-manager "Automatic recovery stopped: HAProxy configuration could not be installed"
exit 1
fi
if /etc/init.d/haproxy restart >> "$DIAGNOSTICS" 2>&1 &&
/etc/init.d/haproxy status >/dev/null 2>&1; then
set_result reconciled
logger -t haproxy-manager "HAProxy listeners reconciled with the current WAN address"
exit 0
fi
if /usr/libexec/haproxy-manager/rollback --sync "${BACKUP_PATH##*/}" >> "$DIAGNOSTICS" 2>&1; then
set_result rolled-back
logger -t haproxy-manager "WAN listener reconciliation failed and was rolled back"
else
set_result rollback-failed
logger -t haproxy-manager "WAN listener reconciliation and rollback both failed"
fi
exit 1
fi
if ! /etc/init.d/haproxy check >> "$DIAGNOSTICS" 2>&1; then
set_result invalid-config
logger -t haproxy-manager "Automatic recovery stopped: HAProxy configuration is invalid"
exit 1
fi
attempt=1
while [ "$attempt" -le 5 ]; do
printf '\n== Start attempt %s ==\n' "$attempt" >> "$DIAGNOSTICS" || true
/etc/init.d/haproxy restart >> "$DIAGNOSTICS" 2>&1 || true
sleep $((attempt + 1))
if /etc/init.d/haproxy status >/dev/null 2>&1; then
set_result recovered
{
printf '\n== Recovered listeners ==\n'
ps w | grep '[h]aproxy' || true
netstat -lntp 2>/dev/null | grep haproxy || true
} >> "$DIAGNOSTICS" 2>&1
logger -t haproxy-manager "HAProxy recovered after $attempt attempt(s)"
exit 0
fi
attempt=$((attempt + 1))
done
set_result failed
logger -t haproxy-manager "HAProxy automatic recovery failed after 5 attempts"
exit 1
@@ -0,0 +1,124 @@
#!/bin/sh
set -eu
. /usr/libexec/haproxy-manager/common.sh
SYNC=0
LOCK_DIR=/var/lock/haproxy-manager.apply
LOCK_HELD=0
if [ "${1:-}" = --sync ]; then
[ "$#" -eq 2 ] || { echo "Usage: rollback [--sync] [last|YYYYMMDD-HHMMSS]" >&2; exit 1; }
SYNC=1
TARGET="$2"
else
[ "$#" -le 1 ] || { echo "Usage: rollback [--sync] [last|YYYYMMDD-HHMMSS]" >&2; exit 1; }
TARGET="${1:-last}"
fi
release_lock() {
[ "$LOCK_HELD" != 1 ] || operation_lock_release "$LOCK_DIR"
}
if [ "$SYNC" != 1 ]; then
attempt=0
while ! operation_lock_acquire "$LOCK_DIR"; do
[ "$attempt" -lt 15 ] || { echo "Another HAProxy Manager operation is still running." >&2; exit 1; }
attempt=$((attempt + 1))
sleep 1
done
LOCK_HELD=1
trap 'status=$?; release_lock; exit "$status"' EXIT
fi
BASE="$(backup_dir)"
[ ! -d "$BASE" ] || BASE="$(resolve_storage_dir "$BASE")" || {
echo "Recovery point directory resolves outside /root or /mnt" >&2
exit 1
}
if [ "$TARGET" = last ]; then
[ -f "$BASE/LAST" ] || { echo "No LAST backup marker found" >&2; exit 1; }
TARGET="$(basename "$(cat "$BASE/LAST")")"
fi
is_valid_backup_id "$TARGET" || { echo "Invalid backup identifier" >&2; exit 1; }
TARGET_DIR="$BASE/$TARGET"
[ -d "$TARGET_DIR" ] || { echo "Backup not found: $TARGET" >&2; exit 1; }
firewall_changed=0
uhttpd_changed=0
state_failed=0
if [ -f "$TARGET_DIR/firewall" ] && ! cmp -s "$TARGET_DIR/firewall" /etc/config/firewall; then
atomic_install_file "$TARGET_DIR/firewall" /etc/config/firewall
firewall_changed=1
fi
if [ -f "$TARGET_DIR/uhttpd" ] && ! cmp -s "$TARGET_DIR/uhttpd" /etc/config/uhttpd; then
atomic_install_file "$TARGET_DIR/uhttpd" /etc/config/uhttpd
uhttpd_changed=1
fi
[ -f "$TARGET_DIR/haproxy_manager" ] && atomic_install_file "$TARGET_DIR/haproxy_manager" /etc/config/haproxy_manager
[ -f "$TARGET_DIR/haproxy.cfg" ] && atomic_install_file "$TARGET_DIR/haproxy.cfg" "$HAPROXY_CFG"
if [ "$firewall_changed" = 1 ] && ! /etc/init.d/firewall restart >/dev/null 2>&1; then
logger -t haproxy-manager "Firewall restart failed while restoring $TARGET"
state_failed=1
fi
haproxy_enabled=1
haproxy_running=1
if [ -f "$TARGET_DIR/haproxy.state" ]; then
haproxy_enabled="$(sed -n 's/^enabled=//p' "$TARGET_DIR/haproxy.state" | head -1)"
haproxy_running="$(sed -n 's/^running=//p' "$TARGET_DIR/haproxy.state" | head -1)"
fi
if [ "$haproxy_enabled" = 1 ]; then
if ! /etc/init.d/haproxy enable >/dev/null 2>&1; then
logger -t haproxy-manager "Could not enable HAProxy while restoring $TARGET"
state_failed=1
fi
else
if ! /etc/init.d/haproxy disable >/dev/null 2>&1; then
logger -t haproxy-manager "Could not disable HAProxy while restoring $TARGET"
state_failed=1
fi
fi
restore_services() {
local failed="$state_failed"
if [ "$uhttpd_changed" = 1 ] && ! /etc/init.d/uhttpd restart >/dev/null 2>&1; then
logger -t haproxy-manager "LuCI restart failed while restoring $TARGET"
failed=1
fi
if [ "$haproxy_running" = 1 ]; then
if ! /etc/init.d/haproxy restart >/dev/null 2>&1 ||
! /etc/init.d/haproxy status >/dev/null 2>&1; then
logger -t haproxy-manager "HAProxy restart failed while restoring $TARGET"
failed=1
fi
else
if ! /etc/init.d/haproxy stop >/dev/null 2>&1 ||
/etc/init.d/haproxy status >/dev/null 2>&1; then
logger -t haproxy-manager "HAProxy stop failed while restoring $TARGET"
failed=1
fi
fi
return "$failed"
}
if [ "$SYNC" = 1 ]; then
restore_services || exit 1
else
LOCK_HELD=0
trap - EXIT
(
sleep 2
if ! restore_services; then
logger -t haproxy-manager "Service restoration failed for recovery point $TARGET"
fi
operation_lock_release "$LOCK_DIR"
) >/dev/null 2>&1 &
operation_lock_set_pid "$LOCK_DIR" "$!"
fi
echo "Restored backup $TARGET. Affected services will reload shortly."
@@ -0,0 +1,56 @@
#!/bin/sh
. /usr/libexec/haproxy-manager/common.sh
enabled="$(uci_get main enabled 0)"
wan="$(wan_ip 2>/dev/null || echo unknown)"
version="not-available"
service="stopped"
backup_count=0
auto_recovery="$(uci_get main auto_recover 1)"
mode="$(active_mode)"
last_incident=""
last_incident_result="none"
last_incident_interface=""
if command -v haproxy >/dev/null 2>&1; then
version="$(haproxy -v 2>/dev/null | sed -n '1s/^HAProxy version \([^ ]*\).*/\1/p')"
[ -n "$version" ] || version="unknown"
fi
if [ -x /etc/init.d/haproxy ] && /etc/init.d/haproxy status >/dev/null 2>&1; then
service="running"
fi
backup_count="$(/usr/libexec/haproxy-manager/backups 2>/dev/null | wc -l | tr -d ' ')"
INCIDENT_BASE="$(incident_dir)"
if [ -d "$INCIDENT_BASE" ]; then
INCIDENT_BASE="$(resolve_storage_dir "$INCIDENT_BASE")" || INCIDENT_BASE=""
fi
if [ -f "$INCIDENT_BASE/LAST" ]; then
last_incident="$(sed -n '1p' "$INCIDENT_BASE/LAST")"
if is_valid_backup_id "$last_incident" && [ -d "$INCIDENT_BASE/$last_incident" ]; then
last_incident_result="$(sed -n '1p' "$INCIDENT_BASE/$last_incident/result" 2>/dev/null || echo detected)"
last_incident_interface="$(sed -n 's/^interface=//p' "$INCIDENT_BASE/$last_incident/meta" 2>/dev/null | head -1)"
else
last_incident=""
last_incident_result="none"
fi
fi
printf 'enabled\t%s\n' "$enabled"
printf 'wan_ip\t%s\n' "$wan"
printf 'version\t%s\n' "$version"
printf 'service\t%s\n' "$service"
printf 'backup_count\t%s\n' "$backup_count"
printf 'auto_recovery\t%s\n' "$auto_recovery"
printf 'active_mode\t%s\n' "$mode"
printf 'last_incident\t%s\n' "$last_incident"
printf 'last_incident_result\t%s\n' "$last_incident_result"
printf 'last_incident_interface\t%s\n' "$last_incident_interface"
netstat -lntp 2>/dev/null | awk '
NR > 2 && $6 == "LISTEN" && ($7 ~ /haproxy/ || $7 ~ /uhttpd/) {
printf "listener\t%s\t%s\t%s\n", $1, $4, $7
}
'
@@ -0,0 +1,70 @@
#!/bin/sh
set -u
. /usr/libexec/haproxy-manager/common.sh
LOCK_DIR=/var/lock/haproxy-manager.apply
LOCK_HELD=0
UHTTPD_CHANGED=0
MODE="$(active_mode)"
FAILED=0
attempt=0
while ! operation_lock_acquire "$LOCK_DIR"; do
[ "$attempt" -lt 15 ] || {
logger -t haproxy-manager "Package removal could not acquire the operation lock"
exit 1
}
attempt=$((attempt + 1))
sleep 1
done
LOCK_HELD=1
trap '[ "$LOCK_HELD" != 1 ] || operation_lock_release "$LOCK_DIR"' EXIT
/usr/libexec/haproxy-manager/backup >/dev/null 2>&1 ||
logger -t haproxy-manager "Could not create a final recovery point before package removal"
if ! restore_uhttpd_bindings; then
logger -t haproxy-manager "Could not restore the original LuCI listener bindings"
FAILED=1
fi
if [ "$UHTTPD_CHANGED" = 1 ] && ! uci commit uhttpd; then
logger -t haproxy-manager "Could not commit the restored LuCI listener bindings"
FAILED=1
fi
if ! /usr/libexec/haproxy-manager/firewall-sync --disable >/dev/null 2>&1; then
logger -t haproxy-manager "Could not remove owned firewall state"
FAILED=1
fi
case "$MODE" in
generated|raw)
if ! /etc/init.d/haproxy stop >/dev/null 2>&1 ||
/etc/init.d/haproxy status >/dev/null 2>&1; then
logger -t haproxy-manager "Could not stop HAProxy during package removal"
FAILED=1
fi
if ! /etc/init.d/haproxy disable >/dev/null 2>&1; then
logger -t haproxy-manager "Could not disable HAProxy during package removal"
FAILED=1
fi
;;
esac
if [ "$UHTTPD_CHANGED" = 1 ]; then
/etc/init.d/uhttpd restart >/dev/null 2>&1 ||
{ logger -t haproxy-manager "Could not restart LuCI after restoring its listeners"; FAILED=1; }
fi
if [ "$FAILED" != 0 ]; then
logger -t haproxy-manager "Package runtime cleanup is incomplete; removal was stopped"
exit 1
fi
uci set "$CONFIG.main.active_mode=none"
uci commit "$CONFIG"
LOCK_HELD=0
operation_lock_release "$LOCK_DIR"
logger -t haproxy-manager "Removed package-owned HAProxy, firewall, and LuCI runtime state"
exit 0
@@ -0,0 +1,14 @@
#!/bin/sh
set -eu
. /usr/libexec/haproxy-manager/common.sh
CFG="${1:-/etc/haproxy.cfg}"
if [ "$CFG" != "$HAPROXY_CFG" ]; then
is_safe_temp_path "$CFG" || { echo "Refusing unsafe HAProxy config path: $CFG" >&2; exit 1; }
fi
if ! command -v haproxy >/dev/null 2>&1; then
echo "haproxy binary is not installed" >&2
exit 1
fi
haproxy -c -f "$CFG"
@@ -0,0 +1,49 @@
{
"admin/services/haproxy-manager": {
"title": "HAProxy",
"i18n": "haproxy-manager",
"order": 60,
"action": {
"type": "firstchild"
},
"depends": {
"acl": [ "luci-app-haproxy-manager" ]
}
},
"admin/services/haproxy-manager/status": {
"title": "Status",
"i18n": "haproxy-manager",
"order": 20,
"action": {
"type": "view",
"path": "haproxy-manager/status"
}
},
"admin/services/haproxy-manager/routes": {
"title": "Services",
"i18n": "haproxy-manager",
"order": 10,
"action": {
"type": "view",
"path": "haproxy-manager/routes"
}
},
"admin/services/haproxy-manager/settings": {
"title": "Settings",
"i18n": "haproxy-manager",
"order": 30,
"action": {
"type": "view",
"path": "haproxy-manager/settings"
}
},
"admin/services/haproxy-manager/raw": {
"title": "Raw Config",
"i18n": "haproxy-manager",
"order": 40,
"action": {
"type": "view",
"path": "haproxy-manager/raw"
}
}
}
@@ -0,0 +1,36 @@
{
"luci-app-haproxy-manager": {
"description": "Grant access to LuCI HAProxy Manager",
"read": {
"uci": [ "haproxy_manager" ],
"file": {
"/etc/haproxy.cfg": [ "read" ],
"/tmp/haproxy-manager-*": [ "read" ]
},
"ubus": {
"file": [ "read", "stat" ]
}
},
"write": {
"uci": [ "haproxy_manager" ],
"file": {
"/usr/libexec/haproxy-manager/status": [ "exec" ],
"/usr/libexec/haproxy-manager/generate": [ "exec" ],
"/usr/libexec/haproxy-manager/apply": [ "exec" ],
"/usr/libexec/haproxy-manager/backup": [ "exec" ],
"/usr/libexec/haproxy-manager/backups": [ "exec" ],
"/usr/libexec/haproxy-manager/incidents": [ "exec" ],
"/usr/libexec/haproxy-manager/incident": [ "exec" ],
"/usr/libexec/haproxy-manager/firewall-plan": [ "exec" ],
"/usr/libexec/haproxy-manager/rollback": [ "exec" ],
"/usr/libexec/haproxy-manager/validate": [ "exec" ],
"/usr/libexec/haproxy-manager/apply-raw-file": [ "exec" ],
"/tmp/haproxy-manager-*": [ "write" ]
},
"ubus": {
"file": [ "exec", "write" ],
"uci": [ "commit" ]
}
}
}
}