From 0aed18b092f2edfefc52998c265680f71ae31ccb Mon Sep 17 00:00:00 2001 From: JeremyRand Date: Thu, 9 Apr 2015 03:29:20 -0500 Subject: [PATCH 1/6] Added delegation type detection to DNS plugin. --- plugin/pluginDns.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/plugin/pluginDns.py b/plugin/pluginDns.py index 05f6aad..32b7ba3 100644 --- a/plugin/pluginDns.py +++ b/plugin/pluginDns.py @@ -51,6 +51,7 @@ class pluginDns(plugin.PluginThread): #'resolver': ['Forward standard requests to', '8.8.8.8,8.8.4.4'], } helps = { + 'getDelegateType': [1, 1, '', 'Get the method (if any) used to delegate control of the domain (ns, translate, or alias)'], 'getIp4': [1, 1, '', 'Get a list of IPv4 for the domain'], 'getIp6': [1, 1, '', 'Get a list of IPv6 for the domain'], 'getIp4Offline': [1, 1, '', 'Get a list of IPv4 for the domain, without generating network traffic'], @@ -126,6 +127,15 @@ def _getRecordForRPC(self, domain, recType): return result.toJsonForRPC() + @plugin.public + def getDelegateType(self, domain): + if len(json.loads(self.getNs(domain))) > 0: + return "ns" + if len(json.loads(self.getTranslate(domain))) > 0: + return "translate" + # TODO: process alias + return None + @plugin.public def getIp4(self, domain): result = self.getIp4Offline(domain) @@ -145,6 +155,11 @@ def getIp4(self, domain): @plugin.public def getIp4Offline(self, domain): + delegate = self.getDelegateType(domain) + + if delegate != None: + return json.dumps([delegate]) + return self._getRecordForRPC(domain, 'getIp4') @plugin.public @@ -166,6 +181,11 @@ def getIp6(self, domain): @plugin.public def getIp6Offline(self, domain): + delegate = self.getDelegateType(domain) + + if delegate != None: + return json.dumps([delegate]) + return self._getRecordForRPC(domain, 'getIp6') @plugin.public @@ -174,6 +194,12 @@ def getNs(self, domain): @plugin.public def getTranslate(self, domain): + + delegate = self.getDelegateType(domain) + + if delegate != None: + return json.dumps([]) + return self._getRecordForRPC(domain, 'getTranslate') @plugin.public From 6f12156b7b4983aaf288c2abd50fc00ae0141f82 Mon Sep 17 00:00:00 2001 From: JeremyRand Date: Thu, 9 Apr 2015 03:33:03 -0500 Subject: [PATCH 2/6] NMControl can now be run as a guest to other programs. --- nmcontrol.py | 60 +++++++++++++++++++++++++++++++--------------------- 1 file changed, 36 insertions(+), 24 deletions(-) diff --git a/nmcontrol.py b/nmcontrol.py index 2f380fe..91399ec 100755 --- a/nmcontrol.py +++ b/nmcontrol.py @@ -9,7 +9,8 @@ import ConfigParser app = {} -def main(): + +def init(): # init app config global app app['conf'] = ConfigParser.SafeConfigParser() @@ -24,10 +25,11 @@ def main(): # add conf path import platformDep path = os.path.join(platformDep.getNmcontrolDir(), 'conf') + os.sep - for argv in sys.argv: - if argv.startswith("--confdir=") or argv.startswith("--main.confdir="): - path = argv.split("=")[1] - path = os.path.realpath(path) + os.sep + if __name__=='__main__': + for argv in sys.argv: + if argv.startswith("--confdir=") or argv.startswith("--main.confdir="): + path = argv.split("=")[1] + path = os.path.realpath(path) + os.sep app['path']['conf'] = path import common @@ -40,9 +42,10 @@ def main(): app['debug'] = False # debug mode - for argv in sys.argv: - if argv in ['--debug=1','--main.debug=1']: - app['debug'] = True + if __name__=='__main__': + for argv in sys.argv: + if argv in ['--debug=1','--main.debug=1']: + app['debug'] = True # init modules import re @@ -71,20 +74,23 @@ def main(): print "Exception when loading "+modType, module, ":", e # parse command line options - # Note: There should not be plugins and services with the same name - (options, app['args']) = app['parser'].parse_args() - for option, value in vars(options).items(): - if value is not None: - tmp = option.split('.') - if len(tmp) == 1: - app['plugins']['main'].conf[tmp[0]] = value - else: - module = tmp[0] - tmp.remove(module) - if module in app['plugins']: - app['plugins'][module].conf['.'.join(tmp)] = value - if module in app['services']: - app['services'][module].conf['.'.join(tmp)] = value + # Note: There should not be plugins and services with the same name + if __name__=='__main__': + (options, app['args']) = app['parser'].parse_args() + for option, value in vars(options).items(): + if value is not None: + tmp = option.split('.') + if len(tmp) == 1: + app['plugins']['main'].conf[tmp[0]] = value + else: + module = tmp[0] + tmp.remove(module) + if module in app['plugins']: + app['plugins'][module].conf['.'.join(tmp)] = value + if module in app['services']: + app['services'][module].conf['.'.join(tmp)] = value + else: + app['args'] = [] ###### Act as client : send rpc request ###### if len(app['args']) > 0 and app['args'][0] != 'start': @@ -98,11 +104,13 @@ def main(): print data['result']['reply'] if app['debug'] and data['result']['prints']: print "LOG:", data['result']['prints'] if app['args'][0] != 'restart': - return + sys.exit(0) # daemon mode if os.name == "nt": # MS Windows print "Daemon mode not possible on MS Windows." + elif __name__ != '__main__': + print "Not daemonizing since we're being hosted by another program." elif int(app['plugins']['main'].conf['daemon']) == 1: print "Entering background mode" import daemonize @@ -123,6 +131,8 @@ def main(): if app['plugins'][plugin].__dict__.has_key("criticalStartException") and app['plugins'][plugin].criticalStartException: raise Exception(app['plugins'][plugin].criticalStartException) +def main(): + global app #services_started = [] #for service in app['services']: # if app['services'][service].running: @@ -135,9 +145,11 @@ def main(): except (KeyboardInterrupt, SystemExit): print '\n! Received keyboard interrupt, quitting threads.\n' +def shutdown(): # stop main program app['plugins']['main'].stop() - if __name__=='__main__': + init() main() + shutdown() From a9f7aad11c09c9221749c7317efe5722fa3ce952 Mon Sep 17 00:00:00 2001 From: JeremyRand Date: Wed, 8 Apr 2015 17:34:23 -0500 Subject: [PATCH 3/6] Fixed infinite loop in getTranslate caused by getDelegateType. --- plugin/pluginDns.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/plugin/pluginDns.py b/plugin/pluginDns.py index 32b7ba3..ac75015 100644 --- a/plugin/pluginDns.py +++ b/plugin/pluginDns.py @@ -194,10 +194,7 @@ def getNs(self, domain): @plugin.public def getTranslate(self, domain): - - delegate = self.getDelegateType(domain) - - if delegate != None: + if len(json.loads(self.getNs(domain))) > 0: return json.dumps([]) return self._getRecordForRPC(domain, 'getTranslate') From 8e323eebcfe4941a91b85d0fd4cd5072a2cc3bfd Mon Sep 17 00:00:00 2001 From: JeremyRand Date: Thu, 9 Apr 2015 02:50:28 -0500 Subject: [PATCH 4/6] Removed delegation code from some dns methods; this is better placed in a more central location. --- plugin/pluginDns.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/plugin/pluginDns.py b/plugin/pluginDns.py index ac75015..04807d6 100644 --- a/plugin/pluginDns.py +++ b/plugin/pluginDns.py @@ -155,11 +155,6 @@ def getIp4(self, domain): @plugin.public def getIp4Offline(self, domain): - delegate = self.getDelegateType(domain) - - if delegate != None: - return json.dumps([delegate]) - return self._getRecordForRPC(domain, 'getIp4') @plugin.public @@ -181,11 +176,6 @@ def getIp6(self, domain): @plugin.public def getIp6Offline(self, domain): - delegate = self.getDelegateType(domain) - - if delegate != None: - return json.dumps([delegate]) - return self._getRecordForRPC(domain, 'getIp6') @plugin.public @@ -194,10 +184,20 @@ def getNs(self, domain): @plugin.public def getTranslate(self, domain): - if len(json.loads(self.getNs(domain))) > 0: - return json.dumps([]) + result = json.loads(self._getRecordForRPC(domain, 'getTranslate')) + + if len(result) == 0: + return None + + return result[0] + + def getAlias(self, domain): + result = json.loads(self._getRecordForRPC(domain, 'getAlias')) + + if len(result) == 0: + return None - return self._getRecordForRPC(domain, 'getTranslate') + return result[0] @plugin.public def getOnion(self, domain): From a9ee9ed2b92167ca7f2243ece35732d1562216e2 Mon Sep 17 00:00:00 2001 From: JeremyRand Date: Thu, 9 Apr 2015 02:51:20 -0500 Subject: [PATCH 5/6] Added getAlias (so far nonworking) to pluginNamespaceDomain's supportedMethods. --- plugin/pluginNamespaceDomain.py | 1 + 1 file changed, 1 insertion(+) diff --git a/plugin/pluginNamespaceDomain.py b/plugin/pluginNamespaceDomain.py index ffebe4c..788b996 100644 --- a/plugin/pluginNamespaceDomain.py +++ b/plugin/pluginNamespaceDomain.py @@ -18,6 +18,7 @@ class pluginNamespaceDomain(plugin.PluginThread): 'getIp4' : 'ip', 'getIp6' : 'ip6', 'getNs' : 'ns', + 'getAlias' : 'alias', # broken because _expandSelectedRecord interferes 'getTranslate' : 'translate', 'getOnion' : 'tor', 'getI2p' : 'i2p', From fc0d1c03a1ef4854080d0000263ea5b63475cd8f Mon Sep 17 00:00:00 2001 From: JeremyRand Date: Thu, 9 Apr 2015 03:10:25 -0500 Subject: [PATCH 6/6] Initial version of an Unbound module. Successfully tested, but not production-ready at all. --- launch_unbound_nmcontrol.sh | 10 +++++ nmcontrol_unbound.py | 75 +++++++++++++++++++++++++++++++++++++ unbound_nmcontrol.conf | 57 ++++++++++++++++++++++++++++ 3 files changed, 142 insertions(+) create mode 100755 launch_unbound_nmcontrol.sh create mode 100755 nmcontrol_unbound.py create mode 100644 unbound_nmcontrol.conf diff --git a/launch_unbound_nmcontrol.sh b/launch_unbound_nmcontrol.sh new file mode 100755 index 0000000..d23e964 --- /dev/null +++ b/launch_unbound_nmcontrol.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +# Note: right now you must run this as root. Yes, this sucks. +# Don't use it in a production environment until that is fixed. + +# to set this up on Fedora, as root: +# echo "dns=dnsmasq">>/etc/NetworkManager/NetworkManager.conf +# echo "server=/bit/127.0.0.2">/etc/NetworkManager/dnsmasq.d/bit-tld + +unbound -c ./unbound_nmcontrol.conf diff --git a/nmcontrol_unbound.py b/nmcontrol_unbound.py new file mode 100755 index 0000000..7bb4f1a --- /dev/null +++ b/nmcontrol_unbound.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python2 + +import nmcontrol +import json + +blockchain_ttl = 60 + +def init(id, cfg): + log_info("pythonmod: init called, module id is %d port: %d script: %s" % (id, cfg.port, cfg.python_script)) + return True + +def deinit(id): + log_info("pythonmod: deinit called, module id is %d" % id) + return True + +def inform_super(id, qstate, superqstate, qdata): + return True + +def operate(id, event, qstate, qdata): + if (event == MODULE_EVENT_NEW) or (event == MODULE_EVENT_PASS): + if (qstate.qinfo.qname_str.endswith(".bit.")): #query name ends with localdomain + #create instance of DNS message (packet) with given parameters + #msg = DNSMessage(qstate.qinfo.qname_str, RR_TYPE_A, RR_CLASS_IN, PKT_QR | PKT_RA | PKT_AA) + msg = DNSMessage(qstate.qinfo.qname_str, RR_TYPE_A, RR_CLASS_IN, PKT_QR | PKT_AA) + #append RR + + ### TODO: detect nonexistent .bit domain and return NXDOMAIN + + # Process NS + addrs = json.loads(nmcontrol.app['plugins']['dns'].getNs(qstate.qinfo.qname_str[:-1])) + for addr in addrs: + msg.authority.append("%s %d IN NS %s" % (qstate.qinfo.qname_str, blockchain_ttl, str(addr)) ) + + # Process A + if (qstate.qinfo.qtype == RR_TYPE_A) or (qstate.qinfo.qtype == RR_TYPE_ANY): + addrs = json.loads(nmcontrol.app['plugins']['dns'].getIp4(qstate.qinfo.qname_str[:-1])) + + for addr in addrs: + msg.answer.append("%s %d IN A %s" % (qstate.qinfo.qname_str, blockchain_ttl, str(addr)) ) + + # Process AAAA + if (qstate.qinfo.qtype == RR_TYPE_AAAA) or (qstate.qinfo.qtype == RR_TYPE_ANY): + addrs = json.loads(nmcontrol.app['plugins']['dns'].getIp6(qstate.qinfo.qname_str[:-1])) + + for addr in addrs: + msg.answer.append("%s %d IN AAAA %s" % (qstate.qinfo.qname_str, blockchain_ttl, str(addr)) ) + + #set qstate.return_msg + if not msg.set_return_msg(qstate): + qstate.ext_state[id] = MODULE_ERROR + return True + + # Namecoin blockchain results are secure by default + qstate.return_msg.rep.security = 2 + + qstate.return_rcode = RCODE_NOERROR + qstate.ext_state[id] = MODULE_FINISHED + return True + else: + #pass the query to validator + qstate.ext_state[id] = MODULE_WAIT_MODULE + return True + + if event == MODULE_EVENT_MODDONE: + log_info("pythonmod: iterator module done") + qstate.ext_state[id] = MODULE_FINISHED + return True + + log_err("pythonmod: bad event") + qstate.ext_state[id] = MODULE_ERROR + return True + +nmcontrol.init() + +log_info("pythonmod: script loaded.") \ No newline at end of file diff --git a/unbound_nmcontrol.conf b/unbound_nmcontrol.conf new file mode 100644 index 0000000..877b948 --- /dev/null +++ b/unbound_nmcontrol.conf @@ -0,0 +1,57 @@ +server: + # module configuration of the server. A string with identifiers + # separated by spaces. "iterator" or "validator iterator" + module-config: "python validator iterator" + + # if given, a chroot(2) is done to the given directory. + # i.e. you can chroot to the working directory, for example, + # for extra security, but make sure all files are in that directory. + # + # If chroot is enabled, you should pass the configfile (from the + # commandline) as a full path from the original root. After the + # chroot has been performed the now defunct portion of the config + # file path is removed to be able to reread the config after a reload. + # + # All other file paths (working dir, logfile, roothints, and + # key files) can be specified in several ways: + # o as an absolute path relative to the new root. + # o as a relative path to the working directory. + # o as an absolute path relative to the original root. + # In the last case the path is adjusted to remove the unused portion. + # + # The pid file can be absolute and outside of the chroot, it is + # written just prior to performing the chroot and dropping permissions. + # + # Additionally, unbound may need to access /dev/random (for entropy). + # How to do this is specific to your OS. + # + # If you give "" no chroot is performed. The path must not end in a /. + chroot: "" + + # if given, user privileges are dropped (after binding port), + # and the given username is assumed. Default is user "unbound". + # If you give "" no privileges are dropped. + username: "" + + # the working directory. The relative files in this config are + # relative to this directory. If you give "" the working directory + # is not changed. + directory: "" + + # Detach from the terminal, run in background, "yes" or "no". + do-daemonize: no + + # specify the interfaces to answer queries from by ip-address. + # The default is to listen to localhost (127.0.0.1 and ::1). + # specify 0.0.0.0 and ::0 to bind to all available interfaces. + # specify every interface on a new 'interface:' labelled line. + # The listen interfaces are not changed on reload, only on restart. + interface: 127.0.0.2 + +## Python config section. To enable: +## o use --with-pythonmodule to configure before compiling. +## o list python in the module-config string (above) to enable. +## o and give a python-script to run. +python: + # Script file to load + python-script: "nmcontrol_unbound.py" \ No newline at end of file