├── .gitignore ├── README.md ├── geoip └── .gitignore ├── jsonrpc.js ├── package.json └── server.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PowerDNS-GeoDNS 2 | 3 | An experimental GeoDNS implementation in node for PowerDNS, using the Remote backend and a unix socket. 4 | 5 | # Usage 6 | Requirements: 7 | 8 | * PowerDNS 3.2 or newer with the Remote and MySQL backends 9 | * node.js (Possibly 0.10, but I use 0.12) 10 | * A system which supports Unix sockets 11 | * A Maxmind GeoIP City Database (free or paid, The easiest place to put this is in ./geoip, as the default points to geoip/GeoIP2-City.mmdb) 12 | 13 | Installation: 14 | 15 | 1. Install PowerDNS, MySQL, and the required backends 16 | 2. Install node and get a copy of this program, using npm install to install the dependencies (git clone works well for this) 17 | 3. Start the process, optionally using Supervisor. 18 | 4. Restart PowerDNS and run a dig command targeting your server to see if it works. 19 | 5. Create a record in SQL, see the section below for record formats. 20 | 21 | # Records 22 | This uses Knexjs to query your MySQL database for records. Right now, the records are pretty strict and can easily be messed up, this'll be changed in the future. 23 | 24 | Create a record with type "DYNA" and the standard settings for an A record, but the contents should be "map: map name", where map name is the record we'll add next. 25 | 26 | Add a record with type "MAP", the name being the same as the dyna record's content, and the contents your ip addresses, each separated by a comma (no spaces). 27 | 28 | Structure (It only requires domain_id, name, type, content, and ttl): 29 | 30 | (`id`, `domain_id`, `name`, `type`, `content`, `ttl`, `prio`, `change_date`, `disabled`, `ordername`, `auth`) 31 | 32 | Example DYNA: 33 | 34 | (NULL, 1, 'geo.example.com', 'DYNA', 'map: geo', 86400, 0, 0, 0, NULL, 1); 35 | 36 | Example MAP: 37 | 38 | (NULL, 1, 'geo', 'MAP', '8.8.8.8,4.4.2.2,8.8.4.4', 0, 0, 0, 0, NULL, 1); 39 | 40 | 41 | # Supervisor 42 | It's best to keep the process running using Supervisord. An example config is as follows. 43 | 44 | [program:geodns] 45 | autostart=true 46 | autorestart=true 47 | process_name=%(program_name)s 48 | user=pdns 49 | command=node server.js 50 | directory=/usr/local/geodns 51 | stderr_logfile = /var/log/supervisord/geodns-stderr.log 52 | stdout_logfile = /var/log/supervisord/geodns-stdout.log 53 | 54 | # Issues 55 | I'm not sure how easy this will be to understand, and it's quite finicky about the setup as are most dns related things. I can't exactly help you -------------------------------------------------------------------------------- /geoip/.gitignore: -------------------------------------------------------------------------------- 1 | *.mmdb -------------------------------------------------------------------------------- /jsonrpc.js: -------------------------------------------------------------------------------- 1 | var net = require('net'), 2 | fs = require('fs'), 3 | util = require('util'), 4 | EventEmitter = require("events").EventEmitter; 5 | 6 | var oldUMask = process.umask(0000); 7 | 8 | function UnixJSONRPC(path) { 9 | if (fs.existsSync(path)) { 10 | fs.unlinkSync(path); 11 | } 12 | 13 | var self = this; 14 | 15 | var server = this.server = net.createServer(function(socket) { 16 | console.log("[Server] Client connected"); 17 | 18 | var buffer = ''; 19 | 20 | socket.respond = function(data) { 21 | this.write(JSON.stringify(data) + "\n"); 22 | }; 23 | 24 | socket.on('data', function(data) { 25 | buffer += data.toString('utf8'); 26 | 27 | var index = -1; 28 | 29 | do { 30 | index = buffer.indexOf("\n"); 31 | 32 | if (index !== -1) { 33 | var str = buffer.substring(0, index); 34 | 35 | self.handleData(socket, JSON.parse(str)); 36 | 37 | buffer = buffer.substring(index + 1); 38 | } 39 | } while (index !== -1); 40 | }); 41 | 42 | socket.on('end', function() { 43 | }); 44 | }); 45 | 46 | server.listen(path, function() { 47 | process.umask(oldUMask); 48 | console.log('Listening.'); 49 | }); 50 | 51 | EventEmitter(this); 52 | } 53 | 54 | util.inherits(UnixJSONRPC, EventEmitter); 55 | 56 | UnixJSONRPC.prototype.handleData = function(socket, obj) { 57 | if (!this.emit(obj.method, obj.parameters, UnixJSONRPC.prototype.write.bind(this, socket))) { 58 | this.write(socket, { result : false }); 59 | } 60 | }; 61 | 62 | UnixJSONRPC.prototype.write = function(socket, obj) { 63 | socket.write(JSON.stringify(obj) + "\n"); 64 | }; 65 | 66 | module.exports = UnixJSONRPC; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pdns-geodns", 3 | "version": "1.0.0", 4 | "description": "GeoDNS for PowerDNS via Remote backend", 5 | "main": "server.js", 6 | "dependencies": { 7 | "async": "^1.2.1", 8 | "geolib": "^2.0.17", 9 | "ip": "^0.3.3", 10 | "knex": "^0.8.6", 11 | "maxmind-db-reader": "^0.1.1", 12 | "mkdirp": "^0.5.1", 13 | "mysql": "^2.7.0", 14 | "optimist": "^0.6.1" 15 | }, 16 | "devDependencies": {}, 17 | "scripts": { 18 | "test": "echo \"Error: no test specified\" && exit 1", 19 | "start": "node server.js" 20 | }, 21 | "author": "Nikki", 22 | "license": "ISC", 23 | "repository": "github:nikkiii/powerdns-geodns" 24 | } 25 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var JsonRPC = require('./jsonrpc'), 2 | async = require('async'), 3 | fs = require('fs'), 4 | path = require('path'), 5 | mmdbreader = require('maxmind-db-reader'), 6 | geolib = require('geolib'), 7 | ip = require('ip'), 8 | mkdirp = require('mkdirp'); 9 | 10 | var opt = require('optimist') 11 | .boolean('help') 12 | .usage('Usage: $0') 13 | .options('maxminddb', { 14 | alias : 'd', 15 | describe : 'MaxMind Database Path', 16 | default : 'geoip/GeoIP2-City.mmdb' 17 | }) 18 | .options('pdnscfg', { 19 | alias : 'c', 20 | describe : 'PowerDNS Config (File which contains mysql settings)', 21 | default : '/etc/powerdns/pdns.d/pdns.local.gmysql.conf' 22 | }) 23 | .options('socket', { 24 | alias : 's', 25 | describe : 'Unix Socket Path (The parent directory will be created for you)', 26 | default : '/var/run/geodns/geodns.sock' 27 | }); 28 | 29 | var argv = opt.argv; 30 | 31 | if (argv.help) { 32 | opt.showHelp(); 33 | return; 34 | } 35 | 36 | var geoip = mmdbreader.openSync(argv.maxminddb), 37 | geoipCache = {}; 38 | 39 | var cfg = loadPdnsConfig(argv.pdnscfg); 40 | 41 | var knex = require('knex')({ 42 | client: 'mysql', 43 | connection: { 44 | host : cfg['gmysql-host'], 45 | user : cfg['gmysql-user'], 46 | password : cfg['gmysql-password'], 47 | database : cfg['gmysql-dbname'] 48 | } 49 | }); 50 | 51 | mkdirp.sync(path.dirname(argv.socket), { 52 | mode : 0777 53 | }); 54 | 55 | var server = new JsonRPC(argv.socket); 56 | 57 | server.on('initialize', function(data, respond) { 58 | respond({ result : true }); 59 | }); 60 | 61 | server.on('lookup', function (data, respond) { 62 | if (data.qtype == 'ANY' || data.qtype == 'A') { 63 | var dyna = false; 64 | knex('records').where({ 65 | name: data.qname, 66 | type: 'DYNA' 67 | }).first('name', 'content', 'domain_id', 'ttl') 68 | .then(function(row) { 69 | if (!row) { 70 | respond({"result":false}); 71 | return false; 72 | } 73 | dyna = row; 74 | return knex('records').where({ 75 | name: row.content.substring(5), 76 | type: 'MAP', 77 | domain_id: row.domain_id 78 | }).first('content'); 79 | }) 80 | .then(function(record) { 81 | if (!record) { 82 | respond({"result":false}); 83 | return false; 84 | } 85 | var addr = data['real-remote']; 86 | 87 | addr = addr.substring(0, addr.indexOf('/')); 88 | 89 | var servers = record.content.split(","); 90 | 91 | geoip.getGeoData(addr, function(err, geodata) { 92 | if (err || !geodata || !('location' in geodata)) { 93 | respond({ 94 | "result": [{ 95 | "qtype": "A", 96 | "qname": record.content, 97 | "content": servers[0], 98 | "ttl": 60 99 | }] 100 | }); 101 | return; 102 | } 103 | 104 | var user = geodata.location; 105 | 106 | async.sortBy(servers, function(server, callback) { 107 | if (server in geoipCache) { 108 | callback(null, geolib.getDistance(user, geoipCache[server])); 109 | return; 110 | } 111 | geoip.getGeoData(server, function(err, geodata) { 112 | geoipCache[server] = geodata.location; 113 | callback(null, geolib.getDistance(user, geodata.location)); 114 | }); 115 | }, function(err, results) { 116 | var closest = results[0]; 117 | 118 | // TODO scopeMask to allow more accurate responses and caching 119 | respond({ 120 | "result": [{ 121 | "qtype": "A", 122 | "qname": dyna.name, 123 | "content": closest, 124 | "ttl": dyna.ttl, 125 | "scopeMask": 24 126 | }] 127 | }); 128 | }); 129 | }); 130 | }); 131 | } else { 132 | respond({"result":false}); 133 | } 134 | }); 135 | 136 | function loadPdnsConfig(file) { 137 | var file = fs.readFileSync(file), 138 | cfg = {}; 139 | 140 | var re = /(.*?)\+?=(.*)/g; 141 | 142 | while (match = re.exec(file)) { 143 | cfg[match[1]] = match[2]; 144 | } 145 | 146 | return cfg; 147 | } --------------------------------------------------------------------------------