summaryrefslogtreecommitdiffhomepage
path: root/applications/luci-app-sshtunnel
diff options
context:
space:
mode:
authorSergey Ponomarev <stokito@gmail.com>2023-12-04 20:27:53 +0200
committerGitHub <noreply@github.com>2023-12-04 19:27:53 +0100
commit7d14746ae88a83163a7e34daae70b264285bbe56 (patch)
tree177683e67b8558a259f63ed19d5fb0043b14a1fc /applications/luci-app-sshtunnel
parent9d746c75f4023bf3c4bcfe77eaa394fbf0188d95 (diff)
New app: luci-app-sshtunnel for SSH tunnels (#6424)
* luci-app-sshtunnel: SSH tunnels The app helps to configure SSH tunnels. You can also generate an SSH key and see known hosts. Signed-off-by: Sergey Ponomarev <stokito@gmail.com>
Diffstat (limited to 'applications/luci-app-sshtunnel')
-rw-r--r--applications/luci-app-sshtunnel/Makefile15
-rw-r--r--applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_hosts.js64
-rw-r--r--applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_keys.js131
-rw-r--r--applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_servers.js147
-rw-r--r--applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js164
-rw-r--r--applications/luci-app-sshtunnel/po/ru/sshtunnel.po303
-rw-r--r--applications/luci-app-sshtunnel/po/templates/sshtunnel.pot284
-rw-r--r--applications/luci-app-sshtunnel/root/usr/share/luci/menu.d/luci-app-sshtunnel.json45
-rw-r--r--applications/luci-app-sshtunnel/root/usr/share/rpcd/acl.d/luci-app-sshtunnel.json20
9 files changed, 1173 insertions, 0 deletions
diff --git a/applications/luci-app-sshtunnel/Makefile b/applications/luci-app-sshtunnel/Makefile
new file mode 100644
index 0000000000..4298b6107f
--- /dev/null
+++ b/applications/luci-app-sshtunnel/Makefile
@@ -0,0 +1,15 @@
+# See /LICENSE for more information.
+# This is free software, licensed under the Apache License, Version 2.0 .
+#
+include $(TOPDIR)/rules.mk
+
+LUCI_TITLE:=LuCI support for SSH Tunnels (sshtunnel package)
+
+PKG_MAINTAINER:=Sergey Ponomarev <stokito@gmail.com>
+LUCI_DEPENDS:=+luci-base +sshtunnel
+PKG_VERSION:=1.0.0
+PKG_RELEASE:=1
+
+include ../../luci.mk
+
+# call BuildPackage - OpenWrt buildroot signature
diff --git a/applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_hosts.js b/applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_hosts.js
new file mode 100644
index 0000000000..b82eae2617
--- /dev/null
+++ b/applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_hosts.js
@@ -0,0 +1,64 @@
+'use strict';
+'require form';
+'require fs';
+'require ui';
+'require view';
+
+
+return view.extend({
+ load: function () {
+ return Promise.all([
+ fs.lines('/root/.ssh/known_hosts'),
+ ]);
+ },
+
+ render: function (data) {
+ var knownHosts = data[0];
+
+ var m, s, o;
+
+ m = new form.Map('sshtunnel', _('SSH Tunnels'),
+ _('This configures <a %s>SSH Tunnels</a>')
+ .format('href="https://openwrt.org/docs/guide-user/services/ssh/sshtunnel"')
+ );
+
+ s = m.section(form.GridSection, '_known_hosts');
+ s.render = L.bind(_renderKnownHosts, this, knownHosts);
+
+ return m.render();
+ },
+});
+
+function _renderKnownHosts(knownHosts) {
+ var table = E('table', {'class': 'table cbi-section-table', 'id': 'known_hosts'}, [
+ E('tr', {'class': 'tr table-titles'}, [
+ E('th', {'class': 'th'}, _('Hostname')),
+ E('th', {'class': 'th'}, _('Public Key')),
+ ])
+ ]);
+
+ var rows = _splitKnownHosts(knownHosts);
+ cbi_update_table(table, rows);
+
+ return E('div', {'class': 'cbi-section cbi-tblsection'}, [
+ E('h3', _('Known hosts ')),
+ E('div', {'class': 'cbi-section-descr'},
+ _('Keys of SSH servers found in %s.').format('<code>/root/.ssh/known_hosts</code>')
+ ),
+ table
+ ]);
+}
+
+function _splitKnownHosts(knownHosts) {
+ var knownHostsMap = [];
+ for (var i = 0; i < knownHosts.length; i++) {
+ var sp = knownHosts[i].indexOf(' ');
+ if (sp < 0) {
+ continue;
+ }
+ var hostname = knownHosts[i].substring(0, sp);
+ var pub = knownHosts[i].substring(sp + 1);
+ knownHostsMap.push([hostname, '<small><code>' + pub + '</code></small>']);
+ }
+ return knownHostsMap;
+}
diff --git a/applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_keys.js b/applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_keys.js
new file mode 100644
index 0000000000..6c83454b0d
--- /dev/null
+++ b/applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_keys.js
@@ -0,0 +1,131 @@
+'use strict';
+'require form';
+'require fs';
+'require ui';
+'require view';
+
+var allSshKeys = {};
+var hasSshKeygen = false;
+
+return view.extend({
+ load: function () {
+ return L.resolveDefault(fs.list('/root/.ssh/'), []).then(function (entries) {
+ var tasks = [
+ L.resolveDefault(fs.stat('/usr/bin/ssh-keygen'), {}),
+ ];
+ // read pub keys
+ for (var i = 0; i < entries.length; i++) {
+ if (entries[i].type === 'file' && entries[i].name.match(/\.pub$/)) {
+ tasks.push(Promise.resolve(entries[i].name));
+ tasks.push(fs.lines('/root/.ssh/' + entries[i].name));
+ }
+ }
+ return Promise.all(tasks);
+ });
+ },
+
+ render: function (data) {
+ hasSshKeygen = data[0].type === 'file';
+ var sshKeys = _splitSshKeys(data.splice(1));
+
+ var m, s, o;
+
+ m = new form.Map('sshtunnel', _('SSH Tunnels'),
+ _('This configures <a %s>SSH Tunnels</a>')
+ .format('href="https://openwrt.org/docs/guide-user/services/ssh/sshtunnel"')
+ );
+
+ s = m.section(form.GridSection, '_keys');
+ s.render = L.bind(_renderSshKeys, this, sshKeys);
+
+ return m.render();
+ },
+});
+
+function _splitSshKeys(sshFiles) {
+ var sshKeys = {};
+ for (var i = 0; i < sshFiles.length; i++) {
+ var sshPubKeyName = sshFiles[i];
+ var sshKeyName = sshPubKeyName.substring(0, sshPubKeyName.length - 4);
+ i++;
+ var sshPub = sshFiles[i];
+ sshKeys[sshKeyName] = '<small><code>' + sshPub + '</code></small>';
+ }
+ allSshKeys = sshKeys;
+ return sshKeys;
+}
+
+function _renderSshKeys(sshKeys) {
+ var table = E('table', {'class': 'table cbi-section-table', 'id': 'keys_table'}, [
+ E('tr', {'class': 'tr table-titles'}, [
+ E('th', {'class': 'th'}, _('Name')),
+ E('th', {'class': 'th'}, _('Public Key')),
+ ])
+ ]);
+
+ var rows = Object.entries(sshKeys);
+ cbi_update_table(table, rows, null);
+
+ var keyGenBtn = E('div', {}, [
+ E('form', {
+ 'submit': _handleKeyGenSubmit,
+ }, [
+ E('label', {}, _('Generate a new key') + ': '),
+ E('span', {'class': 'control-group'}, [
+ E('input', {
+ 'type': 'text',
+ 'name': 'keyName',
+ 'value': 'id_ed25519',
+ 'pattern': '^[a-zA-Z][a-zA-Z0-9_\.]+',
+ 'required': 'required',
+ 'maxsize': '35',
+ 'autocomplete': 'off',
+ }),
+ E('button', {
+ 'id': 'btnGenerateKey',
+ 'type': 'submit',
+ 'class': 'btn cbi-button cbi-button-action',
+ }, [_('Generate')])
+ ])
+ ])
+ ]);
+ return E('div', {'class': 'cbi-section cbi-tblsection'}, [
+ E('h3', _('SSH Keys')),
+ E('div', {'class': 'cbi-section-descr'},
+ _('Add the pub key to %s or %s.')
+ .format('<code>/root/.ssh/authorized_keys</code>', '<code>/etc/dropbear/authorized_keys</code>') + ' ' +
+ _('In LuCI you can do that with <a %s>System / Administration / SSH-Keys</a>')
+ .format('href="/cgi-bin/luci/admin/system/admin/sshkeys"')
+ ),
+ keyGenBtn, table
+ ]);
+}
+
+function _handleKeyGenSubmit(event) {
+ event.preventDefault();
+ var keyName = document.querySelector('input[name="keyName"]').value;
+ if (allSshKeys[keyName]) {
+ document.body.scrollTop = document.documentElement.scrollTop = 0;
+ ui.addNotification(null, E('p', _('A key with that name already exists.'), 'error'));
+ return false;
+ }
+
+ let command = '/usr/bin/ssh-keygen';
+ let commandArgs = ['-t', 'ed25519', '-q', '-N', '', '-f', '/root/.ssh/' + keyName];
+ if (!hasSshKeygen) {
+ command = '/usr/bin/dropbearkey';
+ commandArgs = ['-t', 'ed25519', '-f', '/root/.ssh/' + keyName];
+ }
+ fs.exec(command, commandArgs).then(function (res) {
+ if (res.code === 0) {
+ // refresh the page to see the new key
+ location.reload();
+ } else {
+ throw new Error(res.stdout + ' ' + res.stderr);
+ }
+ }).catch(function (e) {
+ document.body.scrollTop = document.documentElement.scrollTop = 0;
+ ui.addNotification(null, E('p', _('Unable to generate a key: %s').format(e.message)), 'error');
+ });
+ return false;
+}
diff --git a/applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_servers.js b/applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_servers.js
new file mode 100644
index 0000000000..5aa33e6df8
--- /dev/null
+++ b/applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_servers.js
@@ -0,0 +1,147 @@
+'use strict';
+'require form';
+'require fs';
+'require uci';
+'require ui';
+'require view';
+
+var allSshKeys = {};
+
+return view.extend({
+ load: function () {
+ return L.resolveDefault(fs.list('/root/.ssh/'), []).then(function (entries) {
+ var tasks = [];
+ for (var i = 0; i < entries.length; i++) {
+ if (entries[i].type === 'file' && entries[i].name.match(/\.pub$/)) {
+ tasks.push(Promise.resolve(entries[i].name));
+ }
+ }
+ return Promise.all(tasks);
+ });
+ },
+
+ render: function (data) {
+ var sshKeys = _splitSshKeys(data);
+ if (sshKeys.length === 0) {
+ ui.addNotification(null, E('p', _('No SSH keys found, <a %s>generate a new one</a>').format('href="./ssh_keys"')), 'warning');
+ }
+
+ var m, s, o;
+
+ m = new form.Map('sshtunnel', _('SSH Tunnels'),
+ _('This configures <a %s>SSH Tunnels</a>')
+ .format('href="https://openwrt.org/docs/guide-user/services/ssh/sshtunnel"')
+ );
+
+ s = m.section(form.GridSection, 'server', _('Servers'));
+ s.anonymous = false;
+ s.addremove = true;
+ s.nodescriptions = true;
+
+ o = s.tab('general', _('General Settings'));
+ o = s.tab('advanced', _('Advanced Settings'));
+
+ o = s.taboption('general', form.Value, 'hostname', _('Hostname'));
+ o.placeholder = 'example.com';
+ o.datatype = 'host';
+ o.rmempty = false;
+
+ o = s.taboption('general', form.Value, 'port', _('Port'));
+ o.placeholder = '22';
+ o.datatype = 'port';
+
+ o = s.taboption('general', form.Value, 'user', _('User'));
+ o.default = 'root';
+
+ o = s.taboption('general', form.ListValue, 'IdentityFile', _('Key file'),
+ _('Private key file with authentication identity. ' +
+ 'See <em>ssh_config IdentityFile</em>')
+ );
+ o.value('');
+ Object.keys(sshKeys).forEach(function (keyName) {
+ o.value('/root/.ssh/' + keyName, keyName);
+ });
+ o.optional = true;
+
+
+ o = s.taboption('advanced', form.ListValue, 'LogLevel', _('Log level'), 'See <em>ssh_config LogLevel</em>');
+ o.value('QUIET', 'QUIET');
+ o.value('FATAL', 'FATAL');
+ o.value('ERROR', 'ERROR');
+ o.value('INFO', 'INFO');
+ o.value('VERBOSE', 'VERBOSE');
+ o.value('DEBUG', 'DEBUG');
+ o.value('DEBUG2', 'DEBUG2');
+ o.value('DEBUG3', 'DEBUG3');
+ o.default = 'INFO';
+ o.modalonly = true;
+
+ o = s.taboption('advanced', form.ListValue, 'Compression', _('Use compression'),
+ _('Compression may be useful on slow connections. ' +
+ 'See <em>ssh_config Compression</em>')
+ );
+ o.value('yes', _('Yes'));
+ o.value('no', _('No'));
+ o.default = 'no';
+ o.modalonly = true;
+
+ o = s.taboption('advanced', form.Value, 'retrydelay', _('Retry delay'),
+ _('Delay after a connection failure before trying to reconnect.')
+ );
+ o.placeholder = '10';
+ o.default = '10';
+ o.datatype = 'uinteger';
+ o.optional = true;
+ o.modalonly = true;
+
+ o = s.taboption('advanced', form.Value, 'ServerAliveCountMax', _('Server alive count max'),
+ _('The number of server alive messages which may be sent before SSH disconnects from the server. ' +
+ 'See <em>ssh_config ServerAliveCountMax</em>')
+ );
+ o.placeholder = '3';
+ o.datatype = 'uinteger';
+ o.optional = true;
+ o.modalonly = true;
+
+ o = s.taboption('advanced', form.Value, 'ServerAliveInterval', _('Server alive interval'),
+ _('Keep-alive interval (seconds). ' +
+ 'See <em>ssh_config ServerAliveInterval</em>')
+ );
+ o.optional = true;
+ o.default = '60';
+ o.datatype = 'uinteger';
+ o.modalonly = true;
+
+ o = s.taboption('advanced', form.ListValue, 'CheckHostIP', _('Check host IP'),
+ _('Check the host IP address in the <code>known_hosts</code> file. ' +
+ 'This allows ssh to detect whether a host key changed due to DNS spoofing. ' +
+ 'See <em>ssh_config CheckHostIP</em>')
+ );
+ o.value('yes', _('Yes'));
+ o.value('no', _('No'));
+ o.default = 'no';
+ o.modalonly = true;
+
+ o = s.taboption('advanced', form.ListValue, 'StrictHostKeyChecking', _('Strict host key checking'),
+ _('Refuse to connect to hosts whose host key has changed. ' +
+ 'See <em>ssh_config StrictHostKeyChecking</em>'));
+ o.value('accept-new', _('Accept new and check if not changed'));
+ o.value('yes', _('Yes'));
+ o.value('no', _('No'));
+ o.default = 'accept-new';
+ o.modalonly = true;
+
+ return m.render();
+ },
+});
+
+function _splitSshKeys(sshFiles) {
+ var sshKeys = {};
+ for (var i = 0; i < sshFiles.length; i++) {
+ var sshPubKeyName = sshFiles[i];
+ var sshKeyName = sshPubKeyName.substring(0, sshPubKeyName.length - 4);
+ sshKeys[sshKeyName] = '';
+ }
+ allSshKeys = sshKeys;
+ return sshKeys;
+}
diff --git a/applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js b/applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js
new file mode 100644
index 0000000000..34fceab2f0
--- /dev/null
+++ b/applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js
@@ -0,0 +1,164 @@
+'use strict';
+'require form';
+'require fs';
+'require uci';
+'require ui';
+'require view';
+
+return view.extend({
+ load: function () {
+ return Promise.all([
+ uci.load('sshtunnel'),
+ ]);
+ },
+
+ render: function (data) {
+ var m, s, o;
+
+ m = new form.Map('sshtunnel', _('SSH Tunnels'),
+ _('This configures <a %s>SSH Tunnels</a>')
+ .format('href="https://openwrt.org/docs/guide-user/services/ssh/sshtunnel"')
+ );
+
+ s = m.section(form.GridSection, 'tunnelR', _('Remote Tunnels'),
+ _('Forward a port on the remote host to a service on the local host.')
+ );
+ s.anonymous = true;
+ s.addremove = true;
+ s.nodescriptions = true;
+
+ o = s.option(form.Flag, 'enabled', _('Enabled'));
+ o.default = '1';
+
+ o = _addServerOption(s);
+
+ o = s.option(form.Value, 'remoteaddress', _('Remote address'),
+ _('Bind IP address e.g. <code>192.168.1.1</code> or hostname e.g. <code>localhost</code>.') + '<br/>' +
+ _('<code>*</code> means to listen all interfaces <b>including public</b>.')
+ );
+ o.datatype = 'or(host, "*")';
+ o.default = '*';
+ o.rmempty = false;
+
+ o = s.option(form.Value, 'remoteport', _('Remote port'));
+ o.placeholder = '80';
+ o.datatype = 'port';
+ o.rmempty = false;
+
+ o = s.option(form.Value, 'localaddress', _('Local address'),
+ _('Bind IP address e.g. <code>192.168.1.1</code> or hostname e.g. <code>localhost</code>.')
+ );
+ o.datatype = 'host';
+ o.default = 'localhost';
+ o.rmempty = false;
+
+ o = s.option(form.Value, 'localport', _('Local port'));
+ o.datatype = 'port';
+ o.placeholder = '80';
+ o.rmempty = false;
+
+
+ s = m.section(form.GridSection, 'tunnelL', _('Local Tunnels'),
+ _('Forward a port on the local host to a service on the remote host.')
+ );
+ s.anonymous = true;
+ s.addremove = true;
+ s.nodescriptions = true;
+
+ o = s.option(form.Flag, 'enabled', _('Enabled'));
+ o.default = '1';
+
+ o = _addServerOption(s);
+
+ o = s.option(form.Value, 'localaddress', _('Local address'),
+ _('Bind IP address e.g. <code>192.168.1.1</code> or hostname e.g. <code>localhost</code>.') + '<br/>' +
+ _('<code>*</code> means to listen all interfaces <b>including public</b>.')
+ );
+ o.datatype = 'or(host, "*")';
+ o.placeholder = '192.168.1.1'; // not the default * public iface because a user must explicitly configure it
+ o.rmempty = false;
+
+ o = s.option(form.Value, 'localport', _('Local port'));
+ o.datatype = 'port';
+ o.placeholder = '80';
+ o.rmempty = false;
+
+ o = s.option(form.Value, 'remoteaddress', _('Remote address'),
+ _('Bind IP address e.g. <code>192.168.1.1</code> or hostname e.g. <code>localhost</code>.')
+ );
+ o.datatype = 'host';
+ o.default = 'localhost';
+ o.rmempty = false;
+
+ o = s.option(form.Value, 'remoteport', _('Remote port'));
+ o.datatype = 'port';
+ o.default = '80';
+ o.rmempty = false;
+
+
+ s = m.section(form.GridSection, 'tunnelD', _('Dynamic Tunnels'),
+ _('SOCKS proxy via remote host.')
+ );
+ s.anonymous = true;
+ s.addremove = true;
+ s.nodescriptions = true;
+
+ o = s.option(form.Flag, 'enabled', _('Enabled'));
+ o.default = '1';
+
+ o = _addServerOption(s);
+
+ o = s.option(form.Value, 'localaddress', _('Local address'),
+ _('Bind IP address e.g. <code>192.168.1.1</code> or hostname e.g. <code>localhost</code>.') + '<br/>' +
+ _('<code>*</code> means to listen all interfaces <b>including public</b>.')
+ );
+ o.datatype = 'or(host, "*")';
+ o.placeholder = '192.168.1.1'; // not the default * public iface because a user must explicitly configure it
+ o.rmempty = false;
+
+ o = s.option(form.Value, 'localport', _('Local port'));
+ o.datatype = 'port';
+ o.default = '1080';
+ o.rmempty = false;
+
+
+ s = m.section(form.GridSection, 'tunnelW', _('VPN Tunnels'),
+ _('Configure TUN/TAP devices for VPN tunnels.')
+ );
+ s.anonymous = true;
+ s.addremove = true;
+ s.nodescriptions = true;
+
+ o = s.option(form.Flag, 'enabled', _('Enabled'));
+ o.default = '1';
+
+ o = _addServerOption(s);
+
+ o = s.option(form.ListValue, 'vpntype', _('VPN type'));
+ o.value('point-to-point', 'TUN (point-to-point)');
+ o.value('ethernet', 'TAP (ethernet)');
+ o.default = 'point-to-point';
+
+ o = s.option(form.Value, 'localdev', _('Local dev'));
+ o.default = 'any';
+ o.datatype = 'or("any", min(0))';
+ o.rmempty = false;
+
+ o = s.option(form.Value, 'remotedev', _('Remote dev'));
+ o.default = 'any';
+ o.datatype = 'or("any", min(0))';
+ o.rmempty = false;
+
+ return m.render();
+ },
+});
+
+function _addServerOption(s) {
+ var o = s.option(form.ListValue, 'server', _('Server'));
+ o.datatype = 'uciname';
+ o.rmempty = false;
+ uci.sections('sshtunnel', 'server', function (s, sectionName) {
+ o.value(sectionName, s.hostname ? '%s (%s)'.format(sectionName, s.hostname) : sectionName);
+ });
+ return o;
+}
diff --git a/applications/luci-app-sshtunnel/po/ru/sshtunnel.po b/applications/luci-app-sshtunnel/po/ru/sshtunnel.po
new file mode 100644
index 0000000000..71f7cff442
--- /dev/null
+++ b/applications/luci-app-sshtunnel/po/ru/sshtunnel.po
@@ -0,0 +1,303 @@
+msgid ""
+msgstr "Content-Type: text/plain; charset=UTF-8"
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js:37
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js:75
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js:113
+msgid "<code>*</code> means to listen all interfaces <b>including public</b>."
+msgstr ""
+"<code>*</code> значит принимать соединения на всех интерфейсах <b>включая "
+"публичные</b>"
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_keys.js:109
+msgid "A key with that name already exists."
+msgstr "Ключ с таким именем уже существует."
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_servers.js:128
+msgid "Accept new and check if not changed"
+msgstr "Принимать новый и проверять что не изменился"
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_keys.js:95
+msgid "Add the pub key to %s or %s."
+msgstr "Добавьте этот публичный ключ в %s или в %s."
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_servers.js:42
+msgid "Advanced Settings"
+msgstr "Расширенные настройки"
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js:36
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js:49
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js:74
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js:87
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js:112
+msgid ""
+"Bind IP address e.g. <code>192.168.1.1</code> or hostname e.g. "
+"<code>localhost</code>."
+msgstr ""
+"Принимающий соединения IP адрес н.п. <code>192.168.1.1</code> или имя хоста "
+"н.п. <code>localhost</code>"
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_servers.js:115
+msgid "Check host IP"
+msgstr "Проверять IP хоста"
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_servers.js:116
+msgid ""
+"Check the host IP address in the <code>known_hosts</code> file. This allows "
+"ssh to detect whether a host key changed due to DNS spoofing. See "
+"<em>ssh_config CheckHostIP</em>"
+msgstr ""
+"Проверять IP-адрес хоста в файле <code>known_hosts</code>. Это позволяет ssh "
+"определить, изменился ли ключ хоста из-за спуфинга DNS."
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_servers.js:80
+msgid ""
+"Compression may be useful on slow connections. See <em>ssh_config "
+"Compression</em>"
+msgstr ""
+"Сжатие может быть полезным на медленном соединении. См. <em>ssh_config "
+"Compression</em>"
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js:126
+msgid "Configure TUN/TAP devices for VPN tunnels."
+msgstr "Конифгурация устройств TUN/TAP VPN-туннелей."
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_servers.js:89
+msgid "Delay after a connection failure before trying to reconnect."
+msgstr "Задержка перед переподключением"
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js:99
+msgid "Dynamic Tunnels"
+msgstr "Динамические туннели"
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js:30
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js:68
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js:106
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js:132
+msgid "Enabled"
+msgstr "Включен"
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js:62
+msgid "Forward a port on the local host to a service on the remote host."
+msgstr "Перенаправить порт с локального хоста на сервис на удалённом хосте."
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js:24
+msgid "Forward a port on the remote host to a service on the local host."
+msgstr "Перенаправить порт с удалённого хоста на сервис на локальном хосте."
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_servers.js:41
+msgid "General Settings"
+msgstr "Общие настройки"
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_keys.js:88
+msgid "Generate"
+msgstr "Сгенерировать"
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_keys.js:73
+msgid "Generate a new key"
+msgstr "Сгенерировать новый ключ"
+
+#: applications/luci-app-sshtunnel/root/usr/share/rpcd/acl.d/luci-app-sshtunnel.json:3
+msgid "Grant UCI access for luci-app-sshtunnel"
+msgstr "Предоставить UCI доступ для luci-app-sshtunnel"
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_hosts.js:35
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_servers.js:44
+msgid "Hostname"
+msgstr "Имя хоста"
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_keys.js:97
+msgid ""
+"In LuCI you can do that with <a %s>System / Administration / SSH-Keys</a>"
+msgstr ""
+"В LuCI вы можете это сделать через <a %s>Система / Администрирование / SSH "
+"ключи</a>"
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_servers.js:107
+msgid ""
+"Keep-alive interval (seconds). See <em>ssh_config ServerAliveInterval</em>"
+msgstr ""
+"Интервал проверки подключения (секунды). См. See <em>ssh_config "
+"ServerAliveInterval</em>"
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_servers.js:56
+msgid "Key file"
+msgstr "Файл ключа"
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_hosts.js:46
+msgid "Keys of SSH servers found in %s."
+msgstr "Ключи SSH уже известных серверов из %s."
+
+#: applications/luci-app-sshtunnel/root/usr/share/luci/menu.d/luci-app-sshtunnel.json:38
+msgid "Known Hosts"
+msgstr "Знакомые хосты"
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_hosts.js:44
+msgid "Known hosts"
+msgstr "Знакомые хосты"
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js:61
+msgid "Local Tunnels"
+msgstr "Локальные туннели"
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js:48
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js:73
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js:111
+msgid "Local address"
+msgstr "Локальный адрес"
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js:142
+msgid "Local dev"
+msgstr "Локальное устройство dev"
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js:55
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js:81
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js:119
+msgid "Local port"
+msgstr "Локальный порт"
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_servers.js:67
+msgid "Log level"
+msgstr "Уровень логирования"
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_keys.js:61
+msgid "Name"
+msgstr "Название"
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_servers.js:84
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_servers.js:121
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_servers.js:130
+msgid "No"
+msgstr "Нет"
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_servers.js:26
+msgid "No SSH keys found, <a %s>generate a new one</a>"
+msgstr ""
+"Нет ни одного ключа, пожалуйста <a href=\"./ssh_keys\">сгенерируйте новый</"
+"a>."
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_servers.js:49
+msgid "Port"
+msgstr "Порт"
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_servers.js:57
+msgid ""
+"Private key file with authentication identity. See <em>ssh_config "
+"IdentityFile</em>"
+msgstr ""
+"Файл приватного ключа для авторизации. См. <em>ssh_config IdentityFile</em>"
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_hosts.js:36
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_keys.js:62
+msgid "Public Key"
+msgstr "Публичный ключ"
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_servers.js:126
+msgid ""
+"Refuse to connect to hosts whose host key has changed. See <em>ssh_config "
+"StrictHostKeyChecking</em>"
+msgstr ""
+"Отказывать в подключении к хосту если его ключ поменялся. См. <em>ssh_config "
+"StrictHostKeyChecking</em>"
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js:23
+msgid "Remote Tunnels"
+msgstr "Удалённые туннели"
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js:35
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js:86
+msgid "Remote address"
+msgstr "Удалённый адресс"
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js:147
+msgid "Remote dev"
+msgstr "Удалённое устройство dev"
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js:43
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js:93
+msgid "Remote port"
+msgstr "Удалённый порт"
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_servers.js:88
+msgid "Retry delay"
+msgstr "Задержка попытки"
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js:100
+msgid "SOCKS proxy via remote host."
+msgstr "SOCKS прокси через удалённый хост."
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_keys.js:93
+#: applications/luci-app-sshtunnel/root/usr/share/luci/menu.d/luci-app-sshtunnel.json:14
+msgid "SSH Keys"
+msgstr "SSH Ключи"
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_hosts.js:20
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_keys.js:33
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_servers.js:31
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js:18
+#: applications/luci-app-sshtunnel/root/usr/share/luci/menu.d/luci-app-sshtunnel.json:3
+#: applications/luci-app-sshtunnel/root/usr/share/luci/menu.d/luci-app-sshtunnel.json:30
+msgid "SSH Tunnels"
+msgstr "SSH туннели"
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js:157
+msgid "Server"
+msgstr "Сервер"
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_servers.js:97
+msgid "Server alive count max"
+msgstr "Попыток проверки соединения"
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_servers.js:106
+msgid "Server alive interval"
+msgstr "Интервал проверки соединения"
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_servers.js:36
+#: applications/luci-app-sshtunnel/root/usr/share/luci/menu.d/luci-app-sshtunnel.json:22
+msgid "Servers"
+msgstr "Серверы"
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_servers.js:125
+msgid "Strict host key checking"
+msgstr "Строгая проверка ключа хоста"
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_servers.js:98
+msgid ""
+"The number of server alive messages which may be sent before SSH disconnects "
+"from the server. See <em>ssh_config ServerAliveCountMax</em>"
+msgstr ""
+"Сколько проверочных сообщений на сервер отправить прежде чем отключиться.См. "
+"See <em>ssh_config ServerAliveCountMax</em>"
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_hosts.js:21
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_keys.js:34
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_servers.js:32
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js:19
+msgid "This configures <a %s>SSH Tunnels</a>"
+msgstr "Настройка <a %s>SSH туннелей</a>."
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_keys.js:128
+msgid "Unable to generate a key: %s"
+msgstr "Ошибка при генерации ключа: %s"
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_servers.js:79
+msgid "Use compression"
+msgstr "Использовать сжатие"
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_servers.js:53
+msgid "User"
+msgstr "Пользователь"
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js:125
+msgid "VPN Tunnels"
+msgstr "VPN туннели"
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js:137
+msgid "VPN type"
+msgstr "Тип VPN"
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_servers.js:83
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_servers.js:120
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_servers.js:129
+msgid "Yes"
+msgstr "Да"
diff --git a/applications/luci-app-sshtunnel/po/templates/sshtunnel.pot b/applications/luci-app-sshtunnel/po/templates/sshtunnel.pot
new file mode 100644
index 0000000000..9da1f07a7f
--- /dev/null
+++ b/applications/luci-app-sshtunnel/po/templates/sshtunnel.pot
@@ -0,0 +1,284 @@
+msgid ""
+msgstr "Content-Type: text/plain; charset=UTF-8"
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js:37
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js:75
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js:113
+msgid "<code>*</code> means to listen all interfaces <b>including public</b>."
+msgstr ""
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_keys.js:109
+msgid "A key with that name already exists."
+msgstr ""
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_servers.js:128
+msgid "Accept new and check if not changed"
+msgstr ""
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_keys.js:95
+msgid "Add the pub key to %s or %s."
+msgstr ""
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_servers.js:42
+msgid "Advanced Settings"
+msgstr ""
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js:36
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js:49
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js:74
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js:87
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js:112
+msgid ""
+"Bind IP address e.g. <code>192.168.1.1</code> or hostname e.g. "
+"<code>localhost</code>."
+msgstr ""
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_servers.js:115
+msgid "Check host IP"
+msgstr ""
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_servers.js:116
+msgid ""
+"Check the host IP address in the <code>known_hosts</code> file. This allows "
+"ssh to detect whether a host key changed due to DNS spoofing. See "
+"<em>ssh_config CheckHostIP</em>"
+msgstr ""
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_servers.js:80
+msgid ""
+"Compression may be useful on slow connections. See <em>ssh_config "
+"Compression</em>"
+msgstr ""
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js:126
+msgid "Configure TUN/TAP devices for VPN tunnels."
+msgstr ""
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_servers.js:89
+msgid "Delay after a connection failure before trying to reconnect."
+msgstr ""
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js:99
+msgid "Dynamic Tunnels"
+msgstr ""
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js:30
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js:68
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js:106
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js:132
+msgid "Enabled"
+msgstr ""
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js:62
+msgid "Forward a port on the local host to a service on the remote host."
+msgstr ""
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js:24
+msgid "Forward a port on the remote host to a service on the local host."
+msgstr ""
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_servers.js:41
+msgid "General Settings"
+msgstr ""
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_keys.js:88
+msgid "Generate"
+msgstr ""
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_keys.js:73
+msgid "Generate a new key"
+msgstr ""
+
+#: applications/luci-app-sshtunnel/root/usr/share/rpcd/acl.d/luci-app-sshtunnel.json:3
+msgid "Grant UCI access for luci-app-sshtunnel"
+msgstr ""
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_hosts.js:35
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_servers.js:44
+msgid "Hostname"
+msgstr ""
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_keys.js:97
+msgid ""
+"In LuCI you can do that with <a %s>System / Administration / SSH-Keys</a>"
+msgstr ""
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_servers.js:107
+msgid ""
+"Keep-alive interval (seconds). See <em>ssh_config ServerAliveInterval</em>"
+msgstr ""
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_servers.js:56
+msgid "Key file"
+msgstr ""
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_hosts.js:46
+msgid "Keys of SSH servers found in %s."
+msgstr ""
+
+#: applications/luci-app-sshtunnel/root/usr/share/luci/menu.d/luci-app-sshtunnel.json:38
+msgid "Known Hosts"
+msgstr ""
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_hosts.js:44
+msgid "Known hosts"
+msgstr ""
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js:61
+msgid "Local Tunnels"
+msgstr ""
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js:48
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js:73
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js:111
+msgid "Local address"
+msgstr ""
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js:142
+msgid "Local dev"
+msgstr ""
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js:55
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js:81
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js:119
+msgid "Local port"
+msgstr ""
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_servers.js:67
+msgid "Log level"
+msgstr ""
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_keys.js:61
+msgid "Name"
+msgstr ""
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_servers.js:84
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_servers.js:121
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_servers.js:130
+msgid "No"
+msgstr ""
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_servers.js:26
+msgid "No SSH keys found, <a %s>generate a new one</a>"
+msgstr ""
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_servers.js:49
+msgid "Port"
+msgstr ""
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_servers.js:57
+msgid ""
+"Private key file with authentication identity. See <em>ssh_config "
+"IdentityFile</em>"
+msgstr ""
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_hosts.js:36
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_keys.js:62
+msgid "Public Key"
+msgstr ""
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_servers.js:126
+msgid ""
+"Refuse to connect to hosts whose host key has changed. See <em>ssh_config "
+"StrictHostKeyChecking</em>"
+msgstr ""
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js:23
+msgid "Remote Tunnels"
+msgstr ""
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js:35
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js:86
+msgid "Remote address"
+msgstr ""
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js:147
+msgid "Remote dev"
+msgstr ""
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js:43
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js:93
+msgid "Remote port"
+msgstr ""
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_servers.js:88
+msgid "Retry delay"
+msgstr ""
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js:100
+msgid "SOCKS proxy via remote host."
+msgstr ""
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_keys.js:93
+#: applications/luci-app-sshtunnel/root/usr/share/luci/menu.d/luci-app-sshtunnel.json:14
+msgid "SSH Keys"
+msgstr ""
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_hosts.js:20
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_keys.js:33
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_servers.js:31
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js:18
+#: applications/luci-app-sshtunnel/root/usr/share/luci/menu.d/luci-app-sshtunnel.json:3
+#: applications/luci-app-sshtunnel/root/usr/share/luci/menu.d/luci-app-sshtunnel.json:30
+msgid "SSH Tunnels"
+msgstr ""
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js:157
+msgid "Server"
+msgstr ""
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_servers.js:97
+msgid "Server alive count max"
+msgstr ""
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_servers.js:106
+msgid "Server alive interval"
+msgstr ""
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_servers.js:36
+#: applications/luci-app-sshtunnel/root/usr/share/luci/menu.d/luci-app-sshtunnel.json:22
+msgid "Servers"
+msgstr ""
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_servers.js:125
+msgid "Strict host key checking"
+msgstr ""
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_servers.js:98
+msgid ""
+"The number of server alive messages which may be sent before SSH disconnects "
+"from the server. See <em>ssh_config ServerAliveCountMax</em>"
+msgstr ""
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_hosts.js:21
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_keys.js:34
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_servers.js:32
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js:19
+msgid "This configures <a %s>SSH Tunnels</a>"
+msgstr ""
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_keys.js:128
+msgid "Unable to generate a key: %s"
+msgstr ""
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_servers.js:79
+msgid "Use compression"
+msgstr ""
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_servers.js:53
+msgid "User"
+msgstr ""
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js:125
+msgid "VPN Tunnels"
+msgstr ""
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_tunnels.js:137
+msgid "VPN type"
+msgstr ""
+
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_servers.js:83
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_servers.js:120
+#: applications/luci-app-sshtunnel/htdocs/luci-static/resources/view/sshtunnel/ssh_servers.js:129
+msgid "Yes"
+msgstr ""
diff --git a/applications/luci-app-sshtunnel/root/usr/share/luci/menu.d/luci-app-sshtunnel.json b/applications/luci-app-sshtunnel/root/usr/share/luci/menu.d/luci-app-sshtunnel.json
new file mode 100644
index 0000000000..b32c53ad49
--- /dev/null
+++ b/applications/luci-app-sshtunnel/root/usr/share/luci/menu.d/luci-app-sshtunnel.json
@@ -0,0 +1,45 @@
+{
+ "admin/services/sshtunnel": {
+ "title": "SSH Tunnels",
+ "order": 50,
+ "action": {
+ "type": "alias",
+ "path": "admin/services/sshtunnel/ssh_tunnels"
+ },
+ "depends": {
+ "acl": [ "luci-app-sshtunnel" ]
+ }
+ },
+ "admin/services/sshtunnel/ssh_keys": {
+ "title": "SSH Keys",
+ "order": 10,
+ "action": {
+ "type": "view",
+ "path": "sshtunnel/ssh_keys"
+ }
+ },
+ "admin/services/sshtunnel/ssh_servers": {
+ "title": "Servers",
+ "order": 20,
+ "action": {
+ "type": "view",
+ "path": "sshtunnel/ssh_servers"
+ }
+ },
+ "admin/services/sshtunnel/ssh_tunnels": {
+ "title": "SSH Tunnels",
+ "order": 30,
+ "action": {
+ "type": "view",
+ "path": "sshtunnel/ssh_tunnels"
+ }
+ },
+ "admin/services/sshtunnel/ssh_hosts": {
+ "title": "Known Hosts",
+ "order": 40,
+ "action": {
+ "type": "view",
+ "path": "sshtunnel/ssh_hosts"
+ }
+ }
+}
diff --git a/applications/luci-app-sshtunnel/root/usr/share/rpcd/acl.d/luci-app-sshtunnel.json b/applications/luci-app-sshtunnel/root/usr/share/rpcd/acl.d/luci-app-sshtunnel.json
new file mode 100644
index 0000000000..5dfb1405c7
--- /dev/null
+++ b/applications/luci-app-sshtunnel/root/usr/share/rpcd/acl.d/luci-app-sshtunnel.json
@@ -0,0 +1,20 @@
+{
+ "luci-app-sshtunnel": {
+ "description": "Grant UCI access for luci-app-sshtunnel",
+ "read": {
+ "file": {
+ "/root/.ssh/": [ "list" ],
+ "/root/.ssh/*.pub": [ "read" ],
+ "/root/.ssh/known_hosts": [ "read" ],
+ "/usr/bin/ssh-keygen": [ "list" ],
+ "/usr/bin/ssh-keygen *": [ "exec" ],
+ "/usr/bin/dropbearkey": [ "list" ],
+ "/usr/bin/dropbearkey *": [ "exec" ]
+ },
+ "uci": [ "sshtunnel" ]
+ },
+ "write": {
+ "uci": [ "sshtunnel" ]
+ }
+ }
+}