Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions launch_unbound_nmcontrol.sh
Original file line number Diff line number Diff line change
@@ -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
60 changes: 36 additions & 24 deletions nmcontrol.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
import ConfigParser

app = {}
def main():

def init():
# init app config
global app
app['conf'] = ConfigParser.SafeConfigParser()
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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':
Expand All @@ -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
Expand All @@ -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:
Expand All @@ -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()
75 changes: 75 additions & 0 deletions nmcontrol_unbound.py
Original file line number Diff line number Diff line change
@@ -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.")
25 changes: 24 additions & 1 deletion plugin/pluginDns.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, '<domain>', 'Get the method (if any) used to delegate control of the domain (ns, translate, or alias)'],
'getIp4': [1, 1, '<domain>', 'Get a list of IPv4 for the domain'],
'getIp6': [1, 1, '<domain>', 'Get a list of IPv6 for the domain'],
'getIp4Offline': [1, 1, '<domain>', 'Get a list of IPv4 for the domain, without generating network traffic'],
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -174,7 +184,20 @@ def getNs(self, domain):

@plugin.public
def getTranslate(self, domain):
return self._getRecordForRPC(domain, 'getTranslate')
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 result[0]

@plugin.public
def getOnion(self, domain):
Expand Down
1 change: 1 addition & 0 deletions plugin/pluginNamespaceDomain.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
57 changes: 57 additions & 0 deletions unbound_nmcontrol.conf
Original file line number Diff line number Diff line change
@@ -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"