Commit 685d5aba authored by Girish Ramakrishnan's avatar Girish Ramakrishnan

Add DNS server

This adds dnsmasq as an embedded DNS server that learns and unlearns
addresses on the fly.

The server pushes the app's fqdn as the search domain. It also pushes
dnsmasq to be used as the DNS for the connection.

Clients are identified using "devicename.username".

fixes #7
parent cc37daf5
......@@ -9,7 +9,8 @@ RUN echo "deb http://build.openvpn.net/debian/openvpn/stable xenial main" > /etc
## Installing OpenVPN, key-management tool, and iptables
RUN apt-get update -y
RUN apt-get install -y openvpn=2.4.3-* easy-rsa iptables
RUN apt-get install -y openvpn=2.4.3-* easy-rsa iptables dnsmasq && \
rm -rf /var/cache/apt /var/lib/apt/lists
RUN mkdir -p /app/code
WORKDIR /app/code
......@@ -29,8 +30,8 @@ ADD app /app/code/app
# Somehow postinstall is not run automatically when building docker image
RUN npm run postinstall
ADD start.sh server.js openvpn-conf.sh openvpn-on-client-connect.sh openvpn-on-client-disconnect.sh /app/code/
RUN chmod +x start.sh openvpn-conf.sh openvpn-on-client-connect.sh openvpn-on-client-disconnect.sh
ADD start.sh server.js openvpn-conf.sh openvpn-on-client-connect.sh openvpn-on-client-disconnect.sh openvpn-on-learn-address.sh /app/code/
RUN chmod +x start.sh openvpn-conf.sh openvpn-on-client-connect.sh openvpn-on-client-disconnect.sh openvpn-on-learn-address.sh
RUN mkdir -p /app/data
......
......@@ -19,8 +19,8 @@ auth SHA256
# Network
server 10.8.0.0 255.255.255.0
push \"redirect-gateway def1 bypass-dhcp\"
push \"dhcp-option DNS 8.8.4.4\"
push \"dhcp-option DNS 8.8.8.8\"
push \"dhcp-option DNS 10.8.0.1\"
push \"dhcp-option DOMAIN ${APP_DOMAIN}\"
client-to-client
keepalive 10 120
# Security
......@@ -36,4 +36,5 @@ status /run/openvpn-status.log
script-security 2
client-connect /app/code/openvpn-on-client-connect.sh
client-disconnect /app/code/openvpn-on-client-disconnect.sh
learn-address /app/code/openvpn-on-learn-address.sh
"
#!/usr/bin/env bash
set -eu
# the args to this script are operation(add/update/delete), IP, CN
operation="$1"
ip="$2"
cn="${3:-}" # absent for deletes
curl -sS 'http://127.0.0.1:3000/api/onLearnAddress/' -X POST \
--data-urlencode "operation=${operation}" \
--data-urlencode "cn=${cn}" \
--data-urlencode "vpnIp=${ip}" \
--data-urlencode "token@/run/admin-token"
......@@ -86,6 +86,7 @@ app.use('/logout', (req, res) => {
})
router.post('/api/onConnect/', urlEncodedParser, openvpn.onClientConnect)
router.post('/api/onDisconnect/', urlEncodedParser, openvpn.onClientDisconnect)
router.post('/api/onLearnAddress/', urlEncodedParser, openvpn.onLearnAddress)
router.get('/api/list/', isAuthenticated, openvpn.list)
router.put('/api/key/*', isAuthenticated, openvpn.createKey)
router.get('/api/key/*', isAuthenticated, openvpn.getKey)
......
......@@ -9,6 +9,7 @@ const Archiver = require('archiver')
const baseDir = '/app/code/easyrsa'
const keyDir = '/app/data/keys'
const hostsDir = '/run/dnsmasq/hosts'
const env = {
EASY_RSA: baseDir,
......@@ -71,6 +72,7 @@ verb 3
`
const connectedClients = {}
const hostnames = {}
const exec = ({tag, file, args, wantedCode = 0}) => new Promise((resolve, reject) => {
assert.strictEqual(typeof tag, 'string')
......@@ -121,6 +123,7 @@ const readdir = promisify(fs.readdir)
const stat = promisify(fs.stat)
const rm = promisify(fs.unlink)
const readFile = promisify(fs.readFile)
const writeFile = promisify(fs.writeFile)
const ADMIN_TOKEN = fs.readFileSync('/run/admin-token', 'utf8')
......@@ -305,6 +308,35 @@ const onClientDisconnect = (req, res, next) => {
return res.status(200).send({disconnected: {user, deviceName}})
}
const onLearnAddress = (req, res, next) => {
const operation = req.body['operation']
const vpnIp = req.body['vpnIp']
const cn = req.body['cn'] // won't be set for delete
const token = req.body['token']
if (token !== ADMIN_TOKEN) return next(new HttpError(401, 'Unauthorized'))
if (!operation || !vpnIp) return next(new HttpError(409, 'Invalid Request'))
if (!operation.match(/^(add|update|delete)$/)) return next(new HttpError(409, 'Invalid operation'))
if (operation.match(/^(add|update)$/) && !cn) return next(new HttpError(409, 'cn is required'))
if (operation === 'add' || operation === 'update') {
const match = /^([A-Za-z0-9.]+):([A-Za-z0-9\-_]+)$/.exec(cn)
if (!match) return next(new HttpError(409, 'Invalid Request'))
const [, user, deviceName] = match
const hostname = deviceName + '.' + user
hostnames[vpnIp] = `${hostname} ${hostname}.${process.env.APP_DOMAIN}`
} else if (operation === 'delete') {
delete hostnames[vpnIp]
}
const hostsFile = Object.keys(hostnames).map((ip) => `${ip} ${hostnames[ip]}`).join('\n') + '\n'
writeFile(path.join(hostsDir, 'cloudron_hosts'), hostsFile, 'utf8')
.then(() => res.status(200).send({learned: {operation, vpnIp}}))
.catch(error => next(new HttpError(500, error)))
}
const isRunning = () => {
try {
return childProcess.execSync('supervisorctl status openvpn')
......@@ -322,5 +354,6 @@ module.exports = {
revokeKey,
isRunning,
onClientConnect,
onClientDisconnect
onClientDisconnect,
onLearnAddress
}
......@@ -49,6 +49,9 @@ fi
# Add iptables rules for NATing VPN traffic
iptables -t nat -A POSTROUTING -s 10.8.0.0/24 -o eth0 -j MASQUERADE
# Clear all hosts on startup
mkdir -p /run/dnsmasq/hosts
# Fix permissions
chown -R cloudron:cloudron /app/data /tmp /run
......
[program:dnsmasq]
user=root
command=/usr/sbin/dnsmasq --keep-in-foreground --hostsdir=/run/dnsmasq/hosts --user=cloudron
autostart=true
autorestart=true
; https://veithen.github.io/2015/01/08/supervisord-redirecting-stdout.html
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment