From 6e088a08d3434b5fc3c3a4052c22d7dc5e332505 Mon Sep 17 00:00:00 2001 From: wgapi Cloud9 Date: Thu, 21 Oct 2021 18:29:34 -0600 Subject: [PATCH] #15 Set domains in nameserver --- Dockerfile | 1 + app/add.js | 19 ++++- app/del.js | 202 +++++++++++++++++++++++++------------------- includes/helpers.js | 165 ++++++++++++++++++++++-------------- index.js | 6 +- 5 files changed, 235 insertions(+), 158 deletions(-) diff --git a/Dockerfile b/Dockerfile index c87e7b8..cceeae4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,5 @@ FROM node:16-bullseye +RUN apt install -y dnsutils WORKDIR /app COPY package*.json ./ RUN npm install diff --git a/app/add.js b/app/add.js index dbe4e1e..7dbb864 100644 --- a/app/add.js +++ b/app/add.js @@ -16,8 +16,9 @@ let axios; (async()=>{ ca: await fs.readFile(env.CA_CERT_FILE), }), }) - } catch (err) { console.log(err) } + } catch (err) { console.error(err) } })() +const dns_key = `hmac-sha512:wgapi-${env.LOCAL_SERVER}:${env.DNS_KEY}` module.exports = async (req, res) => { const new_hostname = req.query['name'] @@ -116,7 +117,19 @@ AllowedIPs = ${ipv4_addr}/32, ${ipv6_addr}/128` } } - //TODO: Nameserver config + // Update nameserver + const domain = `${new_hostname}.${user.name}.${env.TLD}.` + try { + await helper.nsUpdate(dns_key, env.DNS_MASTER, +`update add ${domain} ${env.DNS_TTL} A ${ipv4_addr} +update add ${domain} ${env.DNS_TTL} AAAA ${ipv6_addr} +update add *.${domain} ${env.DNS_TTL} CNAME ${domain}`) + } + catch (err) { + console.error(`Failed to add ns record:\n${err}`) + return res.sendStatus(500) + } + finally { console.log(`Updated nameserver to add ${domain}.`) } // Generate user config const listen_port = Math.floor(50000 + Math.random() * 10000) @@ -130,6 +143,6 @@ ${client_peers.join('\n')}` // Send config to user res.setHeader('content-type', 'text/plain') - res.send(config) + return res.send(config) } \ No newline at end of file diff --git a/app/del.js b/app/del.js index fb7af68..80f60fd 100644 --- a/app/del.js +++ b/app/del.js @@ -18,6 +18,7 @@ let axios; (async()=>{ }) } catch (err) { console.log(err) } })() +const dns_key = `hmac-sha512:wgapi-${env.LOCAL_SERVER}:${env.DNS_KEY}` module.exports = async (req, res) => { console.log(`Received request from ${req.requester} to delete ${JSON.stringify(req.query)}`) @@ -27,95 +28,118 @@ module.exports = async (req, res) => { catch (err) { console.error(`Failed to get user from ${req.requester}`) return res.sendStatus(err) - } console.log(`${req.requester} must be ${user.name}`) - - // Check user token - if (req.query['token']!==helper.getToken(req.requester)) { - console.log(`Invalid token from ${req.requester}: ${req.query['token']}`) - return res.sendStatus(403) - } - - // Load wg.conf and search for peers - let config_file - let peer_pubkey - try { config_file = await fs.readFile(env.WG_CONFIG_FILE) } - catch (err) { console.error(err); return res.sendStatus(500) } - const config = config_file.toString() - const peer = config.split('\n\n') - .filter( (paragraph) => { - return paragraph.includes('[Peer]') - }).filter( (peer) => { - // .filter() doesn't support async so use then/catch in this block - if (req.query['name']) { - return peer.includes(`[Peer] # ${req.query['name']}.`) - } else if (req.query['pubkey']) { - peer_pubkey = req.query['pubkey'] - return peer.includes(`PublicKey = ${req.query['pubkey']}`) - } else if (req.query['psk']) { - return peer.includes(`PresharedKey = ${req.query['psk']}`) - } else if (req.query['ip']) { - return peer.split('\n').some( (line) => ( - line.includes('AllowedIPs') && - line.includes(` ${req.query['ip']}/`) - ) ) - } else if (req.query['privkey']) { - wg.getPubkeyFromPrivkey(req.query['privkey']) - .then((pubkey) => { - peer_pubkey = pubkey - return peer.includes(`PublicKey = ${pubkey}`) - }) - .catch((err) => { - console.error(`Failed to generate public key from private key during delete request\n`,err) - return res.sendStatus(500) - }) - } else { - console.error(`${req.requester} sent delete request without specifying a peer`) - return res.sendStatus(400) - } - })[0] - if (peer===undefined) { - console.log(`No peer found for delete request from ${req.requester}`) - return res.sendStatus(404) - } - - // Parse peer - const peer_lines = peer.split('\n') - const peer_name = peer_lines - .filter( (line) => line.includes('[Peer] # ') )[0] - .split(' # ')[1] - if (peer_pubkey===undefined) { - peer_pubkey = peer_lines - .filter( (line) => line.includes('PublicKey = ') )[0] - .split(' = ')[1] - } - - // Delete from config - console.log(`Deleting ${peer_name}`); try { - await fs.writeFile(env.WG_CONFIG_FILE, - config.replace(`\n\n${peer}`,'') - .replace('\n\n\n','\n\n') - ) - } catch (err) { - console.error(`Failed to delete ${peer_name}:\n`,err) - return res.sendStatus(500) - } - - // Inform other servers - for (const server of env.SERVERS) { - if (server.host!==env.LOCAL_SERVER) { - try { - console.log(`Informing ${server.host} to delete ${peer_name}`) - await axios.post(`${server.admin_endpoint}/del?secret=${server.secret}`, peer_pubkey, { - headers: {'Content-Type': 'text/plain'}, - }) - } catch (err) { - console.error(`Failed to inform ${server.host} to delete ${peer_name}:\n\n`,err) - return res.sendStatus(500) - } + } finally { + console.log(`${req.requester} must be ${user.name}`) + + // Check user token + if (req.query['token']!==helper.getToken(req.requester)) { + console.log(`Invalid token from ${req.requester}: ${req.query['token']}`) + return res.sendStatus(403) } + + // Load wg.conf + let config_file + try { config_file = await fs.readFile(env.WG_CONFIG_FILE) } + catch (err) { console.error(err); return res.sendStatus(500) } + finally { + + // Search for peer + let peer_pubkey + const config = config_file.toString() + const peer = config.split('\n\n') + .filter( (paragraph) => { + return paragraph.includes('[Peer]') + }).filter( (peer) => { + if (req.query['name']) { + return peer.includes(`[Peer] # ${req.query['name']}.`) + } else if (req.query['pubkey']) { + peer_pubkey = req.query['pubkey'] + return peer.includes(`PublicKey = ${req.query['pubkey']}`) + } else if (req.query['psk']) { + return peer.includes(`PresharedKey = ${req.query['psk']}`) + } else if (req.query['ip']) { + return peer.split('\n').some( (line) => ( + line.includes('AllowedIPs') && + line.includes(` ${req.query['ip']}/`) + ) ) + } else if (req.query['privkey']) { + // .filter() doesn't support async so use then/catch in this block + wg.getPubkeyFromPrivkey(req.query['privkey']) + .then((pubkey) => { + peer_pubkey = pubkey + return peer.includes(`PublicKey = ${pubkey}`) + }) + .catch((err) => { + console.error(`Failed to generate public key from private key during delete request\n`,err) + return res.sendStatus(500) + }) + } else { + console.error(`${req.requester} sent delete request without specifying a peer`) + return res.sendStatus(400) + } + })[0] + if (peer===undefined) { + console.log(`No peer found for delete request from ${req.requester}`) + return res.sendStatus(404) + } + + // Parse peer + const peer_lines = peer.split('\n') + const peer_name = peer_lines + .filter( (line) => line.includes('[Peer] # ') )[0] + .split(' # ')[1] + if (peer_pubkey===undefined) { + peer_pubkey = peer_lines + .filter( (line) => line.includes('PublicKey = ') )[0] + .split(' = ')[1] + } + + // Delete from local wg config + console.log(`Deleting ${peer_name}`); try { + await fs.writeFile(env.WG_CONFIG_FILE, + config.replace(`\n\n${peer}`,'') + .replace('\n\n\n','\n\n') + ) + } catch (err) { + console.error(`Failed to delete ${peer_name}:\n`,err) + return res.sendStatus(500) + } finally { + + // Inform other servers + for (const server of env.SERVERS) { + if (server.host!==env.LOCAL_SERVER) { + try { + console.log(`Informing ${server.host} to delete ${peer_name}`) + await axios.post(`${server.admin_endpoint}/del?secret=${server.secret}`, peer_pubkey, { + headers: {'Content-Type': 'text/plain'}, + }) + } catch (err) { + console.error(`Failed to inform ${server.host} to delete ${peer_name}:\n\n`,err) + return res.sendStatus(500) + } + } + } + + // Delete domains from nameserver + try { + await helper.nsUpdate(dns_key, env.DNS_MASTER, +`update delete ${peer_name}. A +update delete ${peer_name}. AAAA +update delete *.${peer_name}. CNAME`) + } + catch (err) { + console.error(`Failed to delete ns record:\n${err}`) + return res.sendStatus(500) + } + finally { console.log(`Updated nameserver to delete ${peer_name}.`) } + + // Inform user that delete was successful + res.sendStatus(200) + + } + + } + } - - // Inform user that delete was successful - res.sendStatus(200) - + } \ No newline at end of file diff --git a/includes/helpers.js b/includes/helpers.js index 10cd3f4..27079ca 100644 --- a/includes/helpers.js +++ b/includes/helpers.js @@ -2,80 +2,115 @@ const crypto = require('crypto') const env = require(process.argv[2]||'../env/env.json') const fs = require('fs').promises +const spawn = require('child_process').spawn + let tokens = {} module.exports = { - getUserFromIp: async (ip) => new Promise( async (resolve, reject) => { + getUserFromIp: (ip) => + new Promise( async (resolve, reject) => { - // Get subnet (number) - let subnet - if (ip.includes(env.IPV4_NET)) subnet = ip.split('.').slice(-2,-1)[0] - else if (ip.includes(env.IPV6_NET)) subnet = ip.split(':').slice(-2,-1)[0] - else console.log(`Received request from ${ip}, which does not appear to be from the network.`) - - // Read wg.conf file for this user's other devices - let userpeers; try { - userpeers = (await fs.readFile(env.WG_CONFIG_FILE)).toString() - .split('\n\n').filter( (paragraph) => { - return paragraph.includes('[Peer]') - }).filter( (peer) => { - return peer.includes(`${env.IPV4_NET}.${subnet}`) || peer.includes(`${env.IPV6_NET}:${subnet}`) - }) - } catch (err) { console.log(err) } - let found_usernames = [] - let found_hosts = [] - for (const userpeer of userpeers) { - let userpeer_obj = {} - for (const line of userpeer.split('\n')) { - if (line.includes('[Peer] # ')) { - const domain = line.split(' # ')[1].split('.') - userpeer_obj.name = domain[0] - found_usernames.push(domain[1]) - } - else if (line.includes('AllowedIPs = ')) { - const ips = line.split('=')[1].split(', ') - userpeer_obj.ipv4 = ips.filter( (ip) => ip.includes(env.IPV4_NET) )[0].trim() - userpeer_obj.ipv6 = ips.filter( (ip) => ip.includes(env.IPV6_NET) )[0].trim() + // Get subnet (number) + let subnet + if (ip.includes(env.IPV4_NET)) subnet = ip.split('.').slice(-2,-1)[0] + else if (ip.includes(env.IPV6_NET)) subnet = ip.split(':').slice(-2,-1)[0] + else console.log(`Received request from ${ip}, which does not appear to be from the network.`) + + // Read wg.conf file for this user's other devices + let userpeers; try { + userpeers = (await fs.readFile(env.WG_CONFIG_FILE)).toString() + .split('\n\n').filter( (paragraph) => { + return paragraph.includes('[Peer]') + }).filter( (peer) => { + return peer.includes(`${env.IPV4_NET}.${subnet}`) || peer.includes(`${env.IPV6_NET}:${subnet}`) + }) + } catch (err) { console.log(err) } + let found_usernames = [] + let found_hosts = [] + for (const userpeer of userpeers) { + let userpeer_obj = {} + for (const line of userpeer.split('\n')) { + if (line.includes('[Peer] # ')) { + const domain = line.split(' # ')[1].split('.') + userpeer_obj.name = domain[0] + found_usernames.push(domain[1]) + } + else if (line.includes('AllowedIPs = ')) { + const ips = line.split('=')[1].split(', ') + userpeer_obj.ipv4 = ips.filter( (ip) => ip.includes(env.IPV4_NET) )[0].trim() + userpeer_obj.ipv6 = ips.filter( (ip) => ip.includes(env.IPV6_NET) )[0].trim() + } } + found_hosts.push(userpeer_obj) + } + + // Check that all IP addresses are in correct subnet or error out + if (!found_hosts.every((host) => + host.ipv4.includes(`${env.IPV4_NET}.${subnet}.`) && + host.ipv6.includes(`${env.IPV6_NET}:${subnet}:`) + )) { + console.error( + `Found unmatching IP address subnets for ${ip}: \ + ${found_hosts.map( (host) => [host.ipv4,host.ipv6] )}` + ); reject(500) + // Check that the ip is "on the list" + // Should never get here because this ip can't access this IP! + } else if (found_usernames.length ===0) { + console.error(`Received request from ${ip} not in wg.conf!`) + reject(403) + // Check that all usernames are the same correct or error out + // https://stackoverflow.com/a/35568895 + } else if (!found_usernames.every( (v,i,r) => v === r[0] )) { + console.error(`Found unmatching usernames for ${ip}: ${found_usernames.toString()}`) + reject(500) + } else { + resolve({ + name: found_usernames[0], + subnet: subnet, + peers: found_hosts, + }) } - found_hosts.push(userpeer_obj) - } - - // Check that all IP addresses are in correct subnet or error out - if (!found_hosts.every((host) => - host.ipv4.includes(`${env.IPV4_NET}.${subnet}.`) && - host.ipv6.includes(`${env.IPV6_NET}:${subnet}:`) - )) { - console.error( - `Found unmatching IP address subnets for ${ip}: \ - ${found_hosts.map( (host) => [host.ipv4,host.ipv6] )}` - ); reject(500) - // Check that the ip is "on the list" - // Should never get here because this ip can't access this IP! - } else if (found_usernames.length ===0) { - console.error(`Received request from ${ip} not in wg.conf!`) - reject(403) - // Check that all usernames are the same correct or error out - // https://stackoverflow.com/a/35568895 - } else if (!found_usernames.every( (v,i,r) => v === r[0] )) { - console.error(`Found unmatching usernames for ${ip}: ${found_usernames.toString()}`) - reject(500) - } else { - resolve({ - name: found_usernames[0], - subnet: subnet, - peers: found_hosts, - }) - } }), - setToken: async (ip) => new Promise ( async (resolve, reject) => { - try { - tokens[ip] = await crypto.randomBytes(40).toString('hex') - } catch (err) { reject(err) } - resolve(tokens[ip]) - }), getToken: (ip) => tokens[ip], + setToken: (ip) => + new Promise ( async (resolve, reject) => { + try { + tokens[ip] = await crypto.randomBytes(40).toString('hex') + } catch (err) { reject(err) } + resolve(tokens[ip]) + }), + + nsUpdate: (key, server, payload) => + new Promise( (resolve, reject) => { + try { + + let nsupdate = spawn('nsupdate', ['-y', key]) + + // Collect output + let errors = '' + nsupdate.stdout.on('data', (data) => { + console.log(`nsupdate stdout: ${data}`) + }) + nsupdate.stderr.on('data', (data) => { + console.error(`nsupdate stderr: ${data}`) + errors += data + }) + + // Send data + nsupdate.stdin.write(`server ${server}\n${payload}\nsend\nquit`) + nsupdate.stdin.end() + + // Handle exit + nsupdate.on('error', (err) => { reject(err) }) + nsupdate.on('exit', (status) => { + if (status===0) reject(errors) + else resolve() + }) + + // Something went wrong with the spawn? + } catch (err) { reject(err) } + }), } \ No newline at end of file diff --git a/index.js b/index.js index 1545e4c..3ed368f 100644 --- a/index.js +++ b/index.js @@ -8,6 +8,8 @@ const mw = require('./includes/middleware.js') const express = require('express') const app = express() const admin = express() +const helper = require('./includes/helpers') +const dns_key = `hmac-sha512:wgapi-${env.LOCAL_SERVER}:${env.DNS_KEY}` app .use(mw.getRequester) @@ -16,6 +18,7 @@ app .get('/add', mw.getDnsServers, require('./app/add.js')) .get('/del', require('./app/del.js')) .listen(env.PORT) + admin .use(mw.getAdminRequester) .use(mw.allowServers) @@ -23,4 +26,5 @@ admin .post('/add', require('./admin/add.js')) .post('/del', require('./admin/del.js')) .listen(env.ADMIN_PORT) -console.log('Server started') + +console.log('Server started') \ No newline at end of file