#15 Set domains in nameserver
parent
6cc02cc860
commit
6e088a08d3
|
@ -1,4 +1,5 @@
|
|||
FROM node:16-bullseye
|
||||
RUN apt install -y dnsutils
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm install
|
||||
|
|
19
app/add.js
19
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)
|
||||
|
||||
}
|
202
app/del.js
202
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)
|
||||
|
||||
|
||||
}
|
|
@ -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) }
|
||||
}),
|
||||
|
||||
}
|
6
index.js
6
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')
|
Loading…
Reference in New Issue