diff --git a/admin/index_m.html b/admin/index_m.html index 0d9ae58..3bfce8a 100644 --- a/admin/index_m.html +++ b/admin/index_m.html @@ -21,6 +21,7 @@ let hosts = []; let names = []; + let configFiles = []; // the function loadSettings has to exist ... async function load(settings, onChange) { @@ -47,11 +48,13 @@ // Set default language hosts = settings.hosts || []; names = settings.names || []; + configFiles = settings.configFiles || []; onChange(false); await decryptTableFields('tableHosts', hosts, (data)=> { values2table('hosts', data, onChange); }); values2table('names', names, onChange); + values2table('configFiles', configFiles, onChange); } function save(callback) { @@ -71,6 +74,7 @@ // Get edited table obj.hosts = table2values('hosts'); obj.names = table2values('names'); + obj.configFiles = table2values('configFiles'); encryptTableFields('tableHosts', obj.hosts); callback(obj); } @@ -128,6 +132,7 @@ @@ -156,11 +161,11 @@ - - + + - - + + @@ -197,29 +202,34 @@ +
+
+
+

You need to list all your WireGuard config files here (with full path) to use the peer reactivation feature. For more information please see the readme file on github.

+

Example: host: Testhost (same as NAME on main page), Interface: wg0, config file: /etc/wireguard/wg0.conf

+
+
+
+
+ add + Click here to add a new config file +
+
NameName Host addressUserPasswordUserPassword sudo Docker Poll interval
+ + + + + + + + + +
Host nameInterface nameconfig file
+ + + + - - \ No newline at end of file diff --git a/main.js b/main.js index faf59de..a847b02 100644 --- a/main.js +++ b/main.js @@ -9,28 +9,18 @@ const utils = require('@iobroker/adapter-core'); // Load your modules here, e.g.: -const {Client} = require('ssh2'); -const timeOuts=[]; -let adapter=null; +const {Client} = require('ssh2'); +const knownPeers = []; +const timeOuts = []; +let adapter = null; +let secret = ''; -/** - * Opens an ssh connection to the given host, executes the wg-json command and returns the output data of that command. - * - * @param {string} hostname symbolic name of the host - * @param {string} hostaddress IP address of the host - * @param {string} user username which is used to connect to the host - * @param {string} pass password for the user - * @param {boolean} sudo indicator whether sudo should be used - * @param {boolean} docker indicator whether sudo should be used - * @returns {Promise} returns a json structure when successful or an error message - */ -async function getWireguardInfos(hostname, hostaddress, user, pass, sudo, docker) { - adapter.log.info(`Retrieving WireGuard status from host [${hostname}] on address [${hostaddress}]`); - return new Promise(function(resolve, reject) { + +async function execCommand(hostaddress, user, pass, command){ + return new Promise((resolve, reject) => { + adapter.log.debug(`Executing command [${command}] on host ${hostaddress}.`); const conn = new Client(); - let command = docker ? 'docker exec -it wireguard /usr/bin/wg show all dump' : 'wg show all dump'; - command = sudo ? 'sudo ' + command : command; conn.on('ready', () => { adapter.log.debug('ssh client :: authenticated'); adapter.log.debug(`Executing command: [${command}]`); @@ -56,12 +46,79 @@ async function getWireguardInfos(hostname, hostaddress, user, pass, sudo, docker conn.connect({ host: hostaddress, port: 22, - username: user, - password: pass + username: adapter.decrypt(secret, user), + password: adapter.decrypt(secret, pass) }); }); } + +/** + * Opens an ssh connection to the given host, executes the wg-json command and returns the output data of that command. + * + * @param {string} hostname symbolic name of the host + * @param {string} hostaddress IP address of the host + * @param {string} user username which is used to connect to the host + * @param {string} pass password for the user + * @param {boolean} sudo indicator whether sudo should be used + * @param {boolean} docker indicator whether sudo should be used + * @returns {Promise} returns a json structure when successful or an error message + */ +async function getWireguardInfos(hostname, hostaddress, user, pass, sudo, docker) { + adapter.log.debug(`Retrieving WireGuard status of host [${hostname}] on address [${hostaddress}]`); + let command = docker ? 'docker exec -it wireguard /usr/bin/wg show all dump' : 'wg show all dump'; + command = sudo ? 'sudo ' + command : command; + return new Promise(function(resolve, reject) { + execCommand(hostaddress, user, pass, command) + .then((result) => { + resolve(result); + }) + .catch((error) => { + reject(error); + }); + }); +} + +function getExtendedCommand(command, hostaddress){ + for(let i=0; i < adapter.config.hosts.length; i++){ + if (adapter.config.hosts[i].hostaddress === hostaddress){ + command = adapter.config.hosts[i].docker? 'docker exec -it wireguard /usr/bin/'+command : command; + command = adapter.config.hosts[i].sudo? 'sudo '+command : command; + return command; + } else { + throw new Error(`Command couldn't be extended: ${command}`); + } + } +} + +function suspendPeer(hostaddress, path, user, pass, iFace, peer){ + adapter.log.info(`Suspending peer [${peer}] of interface ${iFace} on host ${hostaddress}.`); + // adapter.log.info(`knownPeers [${knownPeers}].`); + return new Promise(function(resolve, reject) { + const command = getExtendedCommand(`wg set ${iFace} peer ${peer} remove`, hostaddress); + execCommand(hostaddress, user, pass, command) + .then((result) => { + adapter.setState(path+'.connected', false, true); + adapter.setState(path+'.isSuspended', true, true); + resolve(result); + }) + .catch((error) => { + reject(error); + }); + }); +} + +async function restorePeers(hostaddress, user, pass, iFace, configFile){ + const command = getExtendedCommand(`wg syncconf ${iFace} ${configFile}`, hostaddress); + execCommand(hostaddress, user, pass, command) + .then((result) => { + return result; + }) + .catch((error) => { + throw new Error(error); + }); +} + /** * parses the commandline output of the wg show all dump command and parses it into a json structure * @@ -80,16 +137,16 @@ async function parseWireguardInfosToJson(wgRawData){ for ( let i=0; i{ for (let i=0; i < devices.length; i++) { @@ -315,12 +408,52 @@ class Wireguard extends utils.Adapter { name: 'wireguard', }); this.on('ready', this.onReady.bind(this)); - // this.on('stateChange', this.onStateChange.bind(this)); + this.on('stateChange', this.onStateChange.bind(this)); // this.on('objectChange', this.onObjectChange.bind(this)); // this.on('message', this.onMessage.bind(this)); this.on('unload', this.onUnload.bind(this)); } + async onStateChange(id, state){ + if (state) { + // The state was changed + // this.log.info(`state ${id} changed: ${state.val} (ack = ${state.ack})`); + if (!state.ack) { + // manual change / request + // wireguard.0.ganymed-wg0.peers.kBTx8Hyd6V51XSzI2fhzR0Wngfhwg3cAJNBvSevCi3Q=.suspend_Peer + let hostaddress = ''; + let user = ''; + let pass = ''; + let configFile = ''; + const iFace = id.split('.', 3).pop().split('-').pop(); + const peer = id.split('.', 5).pop(); + const searchHost = id.split('.', 3).pop().split('-', 1).pop(); + for (let host=0; host < this.config.hosts.length; host++) { + if (this.config.hosts[host].name === searchHost) { + hostaddress = this.config.hosts[host].hostaddress; + user = this.config.hosts[host].user; + pass = this.config.hosts[host].password; + break; + } + } + if ('suspend_Peer' === id.split('.').pop()){ + adapter.log.info(`suspending peer on interface ${iFace}`); + await suspendPeer(hostaddress, id.split('.', 5).join('.'), user, pass, iFace, peer); + } else if ('restore_all_Peers' === id.split('.').pop()){ + adapter.log.info(`restoring all peers for interface ${iFace} on host ${searchHost}`); + for (let i=0; i < this.config.configFiles.length; i++) { + adapter.log.info(`Config: iFace=${this.config.configFiles[i].iFace}, host=${this.config.configFiles[i].hostName}`); + if ((this.config.configFiles[i].hostName === searchHost) && (this.config.configFiles[i].iFace === iFace) ){ + configFile = this.config.configFiles[i].configFile; + break; + } + } + await restorePeers(hostaddress, user, pass, iFace, configFile); + } + } + } + } + /** * Is called when databases are connected and adapter received configuration. */ @@ -330,8 +463,7 @@ class Wireguard extends utils.Adapter { // Initialize your adapter here adapter = this; // preserve adapter reference to address functions etc. correctly later const settings = this.config; - let secret; - adapter.getForeignObject('system.config', (err, obj) => { + this.getForeignObject('system.config', (err, obj) => { if (obj && obj.native && obj.native.secret) { secret = obj.native.secret; } else { @@ -345,9 +477,8 @@ class Wireguard extends utils.Adapter { } try{ for (let host=0; host < settings.hosts.length; host++) { - this.log.debug(JSON.stringify(settings.hosts[host])); timeOuts.push(setInterval(async function pollHost() { - await getWireguardInfos(settings.hosts[host].name, settings.hosts[host].hostaddress, adapter.decrypt(secret, settings.hosts[host].user), adapter.decrypt(secret, settings.hosts[host].password), settings.hosts[host].sudo, settings.hosts[host].docker) + await getWireguardInfos(settings.hosts[host].name, settings.hosts[host].hostaddress, settings.hosts[host].user, settings.hosts[host].password, settings.hosts[host].sudo, settings.hosts[host].docker) .then(async (wgInfos)=> { await parseWireguardInfosToJson(wgInfos) .then(async (wgJson)=>{