From 65b4a0d62cf6e2c09e6844ac83f9c7c4fd975298 Mon Sep 17 00:00:00 2001 From: wgapi Cloud9 Date: Mon, 18 Oct 2021 17:17:45 -0600 Subject: [PATCH] Refactoring and added /list (#11) --- admin/del.js | 5 +- app/add.js | 185 +++++++++++++++++--------------------------- app/del.js | 5 +- app/list.js | 22 ++++++ includes/helpers.js | 71 +++++++++++++++++ index.js | 2 + 6 files changed, 172 insertions(+), 118 deletions(-) create mode 100644 app/list.js create mode 100644 includes/helpers.js diff --git a/admin/del.js b/admin/del.js index 169a6e2..42a4e56 100644 --- a/admin/del.js +++ b/admin/del.js @@ -23,7 +23,10 @@ module.exports = async (req, res) => { return peer.includes(`PublicKey = ${req.body}`) }) try { - await fs.writeFile(env.WG_CONFIG_FILE, config.replace(`\n\n${peer}`,'')) + 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 config from ${env.WG_CONFIG_FILE}:\n`,err) res.sendStatus(500) diff --git a/app/add.js b/app/add.js index 1badd4b..fad55eb 100644 --- a/app/add.js +++ b/app/add.js @@ -4,6 +4,7 @@ */ const env = require(process.argv[2]||'../env/env.json') +const helper = require('../includes/helpers') const fs = require('fs').promises const wg = require('../includes/wireguard') const https = require('https') @@ -19,140 +20,93 @@ let axios; (async()=>{ })() module.exports = async (req, res) => { - const hostname = req.query['name'] - if (!hostname) { + const new_hostname = req.query['name'] + if (!new_hostname) { console.log(`New peer request from ${req.requester} didn't provide a hostname`) res.sendStatus(400); return - } - console.log(`New peer request from ${req.requester} for ${hostname}`) + } else console.log(`New peer request from ${req.requester} for ${new_hostname}`) - // Determine user subnet - let subnet - if (req.requester.includes(env.IPV4_NET)) subnet = req.requester.split('.').slice(-2,-1)[0] - else if (req.requester.includes(env.IPV6_NET)) subnet = req.requester.split(':').slice(-2,-1)[0] - else console.log(`Received add request from ${req.requester}, 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_ipv6s = [] - let found_ipv4s = [] - for (const userpeer of userpeers) { - for (const line of userpeer.split('\n')) { - if (line.includes('[Peer]')) { - // Check if host exists - if (line.includes(`# ${hostname}.`)) { - console.log(`Host already exists for ${hostname}`) - res.sendStatus(409); return - } - found_usernames.push(line.split('.').slice(-2,-1)[0]) - } - else if (line.includes('AllowedIPs')) { - const ips = line.split('=')[1].split(',') - found_ipv4s.push(ips.filter( (ip) => ip.includes(env.IPV4_NET) )[0].trim()) - found_ipv6s.push(ips.filter( (ip) => ip.includes(env.IPV6_NET) )[0].trim()) - } - } + // Get user from IP + let user; try { user = await helper.getUserFromIp(req.requester) } + catch (err) { + console.error(`Failed to get user from ${req.requester}`) + res.sendStatus(err); return + } console.log(`${req.requester} must be ${user.name}`) + + // Check if new peer already exists + if (user.peers.map((peer) => peer.name).includes(new_hostname)) { + console.log(`Host already exists for ${new_hostname}.${user.name}.${env.TLD}`) + res.sendStatus(409); return } - // Check that all IP addresses are in correct subnet or error out - if (!found_ipv4s.every((found_ipv4) => - found_ipv4.toString().includes(`${env.IPV4_NET}.${subnet}.`))) { - console.log(`Found unmatching IPv4 address subnets for ${req.requester}: ${found_ipv4s}`) - res.sendStatus(500); return - } else if (!found_ipv6s.every((found_ipv6) => - found_ipv6.toString().includes(`${env.IPV6_NET}:${subnet}:`))) { - console.log(`Found unmatching IPv6 address subnets for ${req.requester}: ${found_ipv6s}`) - res.sendStatus(500); return - - // Check that the req.requester is "on the list" - // Should never get here because this req.requester can't access this IP! - } else if (found_usernames.length ===0) { - console.log(`Received request from ${req.requester} not in wg.conf!`) - res.sendStatus(403); return - // Check that all usernames are correct or error out - // https://stackoverflow.com/a/35568895 - } else if (!found_usernames.every( (v,i,r) => v === r[0] )) { - console.log(`Found unmatching usernames for ${req.requester}: ${found_usernames.toString()}`) - res.sendStatus(500); return - - // Everything looks good! Proceed - } else { - const username = found_usernames[0] - console.log(`${req.requester} must be ${username}`) - - // Find next available host part in config - const used_ipv4_hosts = found_ipv4s.map((found_ipv4) => + + // Find next available host part + const used_ipv4_hosts = user.peers + .map((host) => host.ipv4).map((found_ipv4) => found_ipv4.toString().split('.')[3].split('/')[0]) - const used_ipv6_hosts = found_ipv6s.map((found_ipv6) => + const used_ipv6_hosts = user.peers + .map(() => host.ipv6).map((found_ipv6) => found_ipv6.toString().split(':')[3].split('/')[0]) - let host = 1 - while ([...used_ipv4_hosts,...used_ipv6_hosts].includes(host.toString())) host++ - - // Create IP Addresses and keys - const ipv4_addr = `${env.IPV4_NET}.${subnet}.${host}` - const ipv6_addr = `${env.IPV6_NET}:${subnet}:${host}` - let keypair; try { - keypair = await wg.generateKeypair() + let host = 1 + while ([...used_ipv4_hosts,...used_ipv6_hosts].includes(host.toString())) host++ + + // Create IP Addresses and keys + const ipv4_addr = `${env.IPV4_NET}.${user.subnet}.${host}` + const ipv6_addr = `${env.IPV6_NET}:${user.subnet}:${host}` + let keypair; try { + keypair = await wg.generateKeypair() + } catch (err) { + console.log(err) + } + + // Peer with each server + let client_peers = [] + for (const server of env.SERVERS) { + let psk; try { + psk = await wg.generatePSK() } catch (err) { console.log(err) } - - // Peer with each server - let client_peers = [] - for (const server of env.SERVERS) { - let psk; try { - psk = await wg.generatePSK() - } catch (err) { - console.log(err) - } - // Add server to client as [Peer] - const allowed_ipv4s = `${server.ipv4}/${(server.host===env.LOCAL_SERVER)?env.IPV4_CIDR:'32'}` - const allowed_ipv6s = `${server.ipv6}/${(server.host===env.LOCAL_SERVER)?env.IPV6_CIDR:'128'}` - - client_peers.push(` + // Add server to client as [Peer] + const allowed_ipv4s = `${server.ipv4}/${(server.host===env.LOCAL_SERVER)?env.IPV4_CIDR:'32'}` + const allowed_ipv6s = `${server.ipv6}/${(server.host===env.LOCAL_SERVER)?env.IPV6_CIDR:'128'}` + + client_peers.push(` [Peer] # ${server.host}.${env.TLD} PublicKey = ${server.pubkey} PresharedKey = ${psk} AllowedIPs = ${allowed_ipv4s}, ${allowed_ipv6s} Endpoint = ${server.endpoint} PersistentKeepAlive = 25`) - // Add client to server as [Peer] - const server_config = `\n -[Peer] # ${hostname}.${username}.${env.TLD} + // Add client to server as [Peer] + const server_config = `\n +[Peer] # ${new_hostname}.${user.name}.${env.TLD} PublicKey = ${keypair[0]} PresharedKey = ${psk} AllowedIPs = ${ipv4_addr}/32, ${ipv6_addr}/128` - if (server.host===env.LOCAL_SERVER) { - // Add server_config to wg0.conf - try { await fs.appendFile(env.WG_CONFIG_FILE, server_config) } - catch (err) { console.error(err); return} - } else { - // Send config to other server - console.log(`Sending config to ${server.host}.gf4`) - try { - await axios.post(`${server.admin_endpoint}/add`, server_config, { - headers: {'Content-Type': 'text/plain'}, - }) - } catch (err) { - if (err.message==='Request failed with status code 403') { - console.error(`Received 403 from ${server.admin_endpoint}/add`) - } else console.error(err) - } + if (server.host===env.LOCAL_SERVER) { + // Add server_config to wg0.conf + try { await fs.appendFile(env.WG_CONFIG_FILE, server_config) } + catch (err) { console.error(err); return} + } else { + // Send config to other server + console.log(`Sending config to ${server.host}.gf4`) + try { + await axios.post(`${server.admin_endpoint}/add`, server_config, { + headers: {'Content-Type': 'text/plain'}, + }) + } catch (err) { + if (err.message==='Request failed with status code 403') { + console.error(`Received 403 from ${server.admin_endpoint}/add`) + } else console.error(err) } } + } - //TODO: Nameserver config + //TODO: Nameserver config - // Generate config - const listen_port = Math.floor(50000 + Math.random() * 10000) - const config = `[Interface] + // Generate config + const listen_port = Math.floor(50000 + Math.random() * 10000) + const config = `[Interface] PrivateKey = ${keypair[1]} Address = ${ipv4_addr}/${env.IPV4_CIDR}, ${ipv6_addr}/${env.IPV6_CIDR} DNS = ${res.locals.DNS_SERVERS_STRING} @@ -160,9 +114,8 @@ ListenPort = ${listen_port} PostUp = resolvectl domain ${env.TLD} ${env.TLD} ${client_peers.join('\n')}` - // Send config to user - res.setHeader('content-type', 'text/plain') - res.send(config) - } + // Send config to user + res.setHeader('content-type', 'text/plain') + res.send(config) } \ No newline at end of file diff --git a/app/del.js b/app/del.js index 8640866..beff171 100644 --- a/app/del.js +++ b/app/del.js @@ -77,7 +77,10 @@ module.exports = async (req, res) => { // Delete from config console.log(`Deleting ${peer_name}`); try { - await fs.writeFile(env.WG_CONFIG_FILE, config.replace(`\n\n${peer}`,'')) + 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) res.sendStatus(500); return diff --git a/app/list.js b/app/list.js new file mode 100644 index 0000000..6bf88ba --- /dev/null +++ b/app/list.js @@ -0,0 +1,22 @@ +'use strict' +/* app/list.js + * route for a client to add a peer +*/ + +const helper = require('../includes/helpers') + +module.exports = async (req, res) => { + console.log(`${req.requester} loaded dashboard`) + + // Get user from IP + let user; try { user = await helper.getUserFromIp(req.requester) } + catch (err) { + console.error(`Failed to get user from ${req.requester}`) + res.sendStatus(err); return + } console.log(`${req.requester} must be ${user.name}`) + + // Send user + res.setHeader('content-type', 'text/json') + res.send(user) + +} \ No newline at end of file diff --git a/includes/helpers.js b/includes/helpers.js new file mode 100644 index 0000000..cda47d4 --- /dev/null +++ b/includes/helpers.js @@ -0,0 +1,71 @@ +'use strict' +const env = require(process.argv[2]||'../env/env.json') +const fs = require('fs').promises + +module.exports = { + + getUserFromIp: async (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() + } + } + 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, + }) + } + }), + +} \ No newline at end of file diff --git a/index.js b/index.js index b2abaea..1545e4c 100644 --- a/index.js +++ b/index.js @@ -11,6 +11,8 @@ const admin = express() app .use(mw.getRequester) + .get('/', (req, res) => res.redirect('/list')) + .get('/list', require('./app/list.js')) .get('/add', mw.getDnsServers, require('./app/add.js')) .get('/del', require('./app/del.js')) .listen(env.PORT)