#15 Set domains in nameserver

master
wgapi Cloud9 2021-10-21 18:29:34 -06:00
parent 6cc02cc860
commit 6e088a08d3
5 changed files with 235 additions and 158 deletions

View File

@ -1,4 +1,5 @@
FROM node:16-bullseye
RUN apt install -y dnsutils
WORKDIR /app
COPY package*.json ./
RUN npm install

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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) }
}),
}

View File

@ -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')