Commit f2e11d86 authored by Julien Muchembled's avatar Julien Muchembled

INCOMPATIBLE: change registry protocol

- authenticated communications with registered clients
- XML-RPC is dropped
- multi-threaded server
parent 746b369c
...@@ -278,8 +278,9 @@ if len(sys.argv) > 1: ...@@ -278,8 +278,9 @@ if len(sys.argv) > 1:
elif self.path == '/tunnel.html': elif self.path == '/tunnel.html':
other = 'route' other = 'route'
gv = registry.Popen(('python', '-c', r"""if 1: gv = registry.Popen(('python', '-c', r"""if 1:
import math, xmlrpclib import math
g = xmlrpclib.ServerProxy('http://localhost/').topology() from re6st.registry import RegistryClient
g = eval(RegistryClient('http://localhost/').topology())
print 'digraph {' print 'digraph {'
a = 2 * math.pi / len(g) a = 2 * math.pi / len(g)
z = 4 z = 4
...@@ -293,7 +294,7 @@ if len(sys.argv) > 1: ...@@ -293,7 +294,7 @@ if len(sys.argv) > 1:
for p in p or (): for p in p or ():
print '"%s" -> "%s";' % (n, title(p)) print '"%s" -> "%s";' % (n, title(p))
print '}' print '}'
"""), stdout=subprocess.PIPE).communicate()[0] """), stdout=subprocess.PIPE, cwd="..").communicate()[0]
if gv: if gv:
svg = subprocess.Popen(('neato', '-Tsvg'), svg = subprocess.Popen(('neato', '-Tsvg'),
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
......
#!/usr/bin/python #!/usr/bin/python
import argparse, atexit, errno, os, subprocess, sqlite3, sys, xmlrpclib import argparse, atexit, errno, os, subprocess, sqlite3, sys
from OpenSSL import crypto from OpenSSL import crypto
from re6st import utils from re6st import registry, utils
def create(path, text=None, mode=0666): def create(path, text=None, mode=0666):
fd = os.open(path, os.O_CREAT | os.O_WRONLY | os.O_TRUNC, mode) fd = os.open(path, os.O_CREAT | os.O_WRONLY | os.O_TRUNC, mode)
...@@ -10,6 +10,9 @@ def create(path, text=None, mode=0666): ...@@ -10,6 +10,9 @@ def create(path, text=None, mode=0666):
finally: finally:
os.close(fd) os.close(fd)
def loadCert(pem):
return crypto.load_certificate(crypto.FILETYPE_PEM, pem)
def main(): def main():
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description="Setup script for re6stnet.", description="Setup script for re6stnet.",
...@@ -47,11 +50,11 @@ def main(): ...@@ -47,11 +50,11 @@ def main():
dh_path = 'dh2048.pem' dh_path = 'dh2048.pem'
# Establish connection with server # Establish connection with server
s = xmlrpclib.ServerProxy(config.registry, allow_none=True) s = registry.RegistryClient(config.registry)
# Get CA # Get CA
ca = s.getCa() ca = s.getCa()
network = utils.networkFromCa(ca) network = utils.networkFromCa(loadCert(ca))
if config.is_needed: if config.is_needed:
route, err = subprocess.Popen(('ip', '-6', '-o', 'route', 'get', route, err = subprocess.Popen(('ip', '-6', '-o', 'route', 'get',
utils.ipFromBin(network)), utils.ipFromBin(network)),
...@@ -72,7 +75,7 @@ def main(): ...@@ -72,7 +75,7 @@ def main():
req = crypto.X509Req() req = crypto.X509Req()
try: try:
with open(cert_path) as f: with open(cert_path) as f:
cert = crypto.load_certificate(crypto.FILETYPE_PEM, f.read()) cert = loadCert(f.read())
components = dict(cert.get_subject().get_components()) components = dict(cert.get_subject().get_components())
components.pop('CN', None) components.pop('CN', None)
except IOError, e: except IOError, e:
...@@ -157,7 +160,7 @@ dh %s ...@@ -157,7 +160,7 @@ dh %s
""" % (config.registry, ca_path, cert_path, key_path, dh_path)) """ % (config.registry, ca_path, cert_path, key_path, dh_path))
print "Sample configuration file created." print "Sample configuration file created."
cn = utils.subnetFromCert(cert) cn = utils.subnetFromCert(loadCert(cert))
subnet = network + utils.binFromSubnet(cn) subnet = network + utils.binFromSubnet(cn)
print "Your subnet: %s/%u (CN=%s)" \ print "Your subnet: %s/%u (CN=%s)" \
% (utils.ipFromBin(subnet), len(subnet), cn) % (utils.ipFromBin(subnet), len(subnet), cn)
......
#!/usr/bin/python #!/usr/bin/python
import errno, logging, mailbox, os, random, select import errno, httplib, logging, select, socket
import smtplib, socket, sqlite3, string, subprocess, sys from BaseHTTPServer import BaseHTTPRequestHandler
import threading, time, traceback, xmlrpclib from SocketServer import ThreadingTCPServer
from collections import deque from urlparse import parse_qsl
from SimpleXMLRPCServer import SimpleXMLRPCServer, SimpleXMLRPCRequestHandler from re6st import registry, utils
from email.mime.text import MIMEText
from OpenSSL import crypto
from re6st import tunnel, utils
# To generate server ca and key with serial for 2001:db8:42::/48 # To generate server ca and key with serial for 2001:db8:42::/48
# openssl req -nodes -new -x509 -key ca.key -set_serial 0x120010db80042 -days 365 -out ca.crt # openssl req -nodes -new -x509 -key ca.key -set_serial 0x120010db80042 -days 3650 -out ca.crt
# TODO: There is currently no mechanism so that client automatically get
# a renewed CA certificate.
IPV6_V6ONLY = 26 IPV6_V6ONLY = 26
SOL_IPV6 = 41 SOL_IPV6 = 41
class RequestHandler(SimpleXMLRPCRequestHandler): class RequestHandler(BaseHTTPRequestHandler):
def address_string(self): def address_string(self):
# Workaround for http://bugs.python.org/issue6085 # Workaround for http://bugs.python.org/issue6085
return self.client_address[0] return self.client_address[0]
def _dispatch(self, method, params): def do_GET(self):
logging.debug('%s%r', method, params) try:
return self.server._dispatch(method, (self,) + params) try:
path, query = self.path.split('?', 1)
except ValueError:
path = self.path
query = {}
else:
query = dict(parse_qsl(query, keep_blank_values=1,
strict_parsing=1))
_, path = path.split('/')
if not _ and path[0] != '_':
return self.server._handle_request(self, path, query)
except Exception:
logging.info(self.requestline, exc_info=1)
self.send_error(httplib.BAD_REQUEST)
def log_error(*args):
pass
class SimpleXMLRPCServer4(SimpleXMLRPCServer): class HTTPServer4(ThreadingTCPServer):
allow_reuse_address = True allow_reuse_address = True
daemon_threads = True
class SimpleXMLRPCServer6(SimpleXMLRPCServer4): class HTTPServer6(HTTPServer4):
address_family = socket.AF_INET6 address_family = socket.AF_INET6
def server_bind(self): def server_bind(self):
self.socket.setsockopt(SOL_IPV6, IPV6_V6ONLY, 1) self.socket.setsockopt(SOL_IPV6, IPV6_V6ONLY, 1)
SimpleXMLRPCServer4.server_bind(self) HTTPServer4.server_bind(self)
class main(object): def main():
parser = utils.ArgParser(fromfile_prefix_chars='@',
def __init__(self): description="re6stnet registry used to bootstrap nodes"
self.cert_duration = 365 * 86400 " and deliver certificates.")
self.time_out = 45000 _ = parser.add_argument
self.refresh_interval = 600 _('--port', type=int, default=80,
self.last_refresh = time.time() help="Port on which the server will listen.")
_('-4', dest='bind4', default='0.0.0.0',
help="Bind server to this IPv4.")
# Command line parsing _('-6', dest='bind6', default='::',
parser = utils.ArgParser(fromfile_prefix_chars='@', help="Bind server to this IPv6.")
description="re6stnet registry used to bootstrap nodes" _('--db', default='/var/lib/re6stnet/registry.db',
" and deliver certificates.") help="Path to SQLite database file. It is automatically initialized"
_ = parser.add_argument " if the file does not exist.")
_('--port', type=int, default=80, _('--ca', required=True, help=parser._ca_help)
help="Port on which the server will listen.") _('--key', required=True,
_('-4', dest='bind4', default='0.0.0.0', help="CA private key in .pem format.")
help="Bind server to this IPv4.") _('--mailhost', required=True,
_('-6', dest='bind6', default='::', help="SMTP host to send confirmation emails. For debugging"
help="Bind server to this IPv6.") " purpose, it can also be an absolute or existing path to"
_('--db', default='/var/lib/re6stnet/registry.db', " a mailbox file")
help="Path to SQLite database file. It is automatically initialized" _('--private',
" if the file does not exist.") help="re6stnet IP of the node on which runs the registry."
_('--ca', required=True, help=parser._ca_help) " Required for normal operation.")
_('--key', required=True, _('--prefix-length', default=16, type=int,
help="CA private key in .pem format.") help="Default length of allocated prefixes.")
_('--mailhost', required=True, _('--anonymous-prefix-length', type=int,
help="SMTP host to send confirmation emails. For debugging" help="Length of allocated anonymous prefixes."
" purpose, it can also be an absolute or existing path to" " If 0 or unset, registration by email is required")
" a mailbox file") _('-l', '--logfile', default='/var/log/re6stnet/registry.log',
_('--private', help="Path to logging file.")
help="re6stnet IP of the node on which runs the registry." _('-v', '--verbose', default=1, type=int,
" Required for normal operation.") help="Log level. 0 disables logging."
_('--prefix-length', default=16, type=int, " Use SIGUSR1 to reopen log.")
help="Default length of allocated prefixes.") config = parser.parse_args()
_('--anonymous-prefix-length', type=int,
help="Length of allocated anonymous prefixes." utils.setupLog(config.verbose, config.logfile)
" If 0 or unset, registration by email is required")
_('-l', '--logfile', default='/var/log/re6stnet/registry.log', server = registry.RegistryServer(config)
help="Path to logging file.") def requestHandler(request, client_address, _):
_('-v', '--verbose', default=1, type=int, RequestHandler(request, client_address, server)
help="Log level. 0 disables logging."
" Use SIGUSR1 to reopen log.") server_list = []
self.config = parser.parse_args() if config.bind4:
server_list.append(HTTPServer4((config.bind4, config.port),
utils.setupLog(self.config.verbose, self.config.logfile) requestHandler))
if config.bind6:
if self.config.private: server_list.append(HTTPServer6((config.bind6, config.port),
self.sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) requestHandler))
else: if server_list:
logging.warning('You have declared no private address' empty_list = []
', either this is the first start, or you should'
'check you configuration')
# Database initializing
utils.makedirs(os.path.dirname(self.config.db))
self.db = sqlite3.connect(self.config.db, isolation_level=None)
self.db.execute("""CREATE TABLE IF NOT EXISTS token (
token text primary key not null,
email text not null,
prefix_len integer not null,
date integer not null)""")
try:
self.db.execute("""CREATE TABLE cert (
prefix text primary key not null,
email text,
cert text)""")
except sqlite3.OperationalError, e:
if e.args[0] != 'table cert already exists':
raise RuntimeError
else:
self.db.execute("INSERT INTO cert VALUES ('',null,null)")
# Loading certificates
with open(self.config.ca) as f:
self.ca = crypto.load_certificate(crypto.FILETYPE_PEM, f.read())
with open(self.config.key) as f:
self.key = crypto.load_privatekey(crypto.FILETYPE_PEM, f.read())
# Get vpn network prefix
self.network = bin(self.ca.get_serial_number())[3:]
logging.info("Network: %s/%u", utils.ipFromBin(self.network),
len(self.network))
self._email = self.ca.get_subject().emailAddress
# Starting server
server_list = []
if self.config.bind4:
server4 = SimpleXMLRPCServer4((self.config.bind4, self.config.port),
requestHandler=RequestHandler, allow_none=True)
server4.register_instance(self)
server_list.append(server4)
if self.config.bind6:
server6 = SimpleXMLRPCServer6((self.config.bind6, self.config.port),
requestHandler=RequestHandler, allow_none=True)
server6.register_instance(self)
server_list.append(server6)
if len(server_list) == 1:
server_list[0].serve_forever()
else:
while True:
try:
r = select.select(server_list[:], [], [])[0]
except select.error as e:
if e.args[0] != errno.EINTR:
raise
else:
for r in r:
r._handle_request_noblock()
def requestToken(self, handler, email):
while True: while True:
# Generating token
token = ''.join(random.sample(string.ascii_lowercase, 8))
args = token, email, self.config.prefix_length, int(time.time())
# Updating database
try:
self.db.execute("INSERT INTO token VALUES (?,?,?,?)", args)
break
except sqlite3.IntegrityError:
pass
# Creating and sending email
msg = MIMEText('Hello, your token to join re6st network is: %s\n'
% token)
msg['Subject'] = '[re6stnet] Token Request'
if self._email:
msg['From'] = self._email
msg['To'] = email
if os.path.isabs(self.config.mailhost) or \
os.path.isfile(self.config.mailhost):
m = mailbox.mbox(self.config.mailhost)
try: try:
m.add(msg) r = select.select(server_list[:], empty_list, empty_list)[0]
finally: except select.error as e:
m.close() if e.args[0] != errno.EINTR:
else: raise
s = smtplib.SMTP(self.config.mailhost) else:
s.sendmail(self._email, email, msg.as_string()) for r in r:
s.quit() r._handle_request_noblock()
def _getPrefix(self, prefix_len):
max_len = 128 - len(self.network)
assert 0 < prefix_len <= max_len
try:
prefix, = self.db.execute("""SELECT prefix FROM cert WHERE length(prefix) <= ? AND cert is null
ORDER BY length(prefix) DESC""", (prefix_len,)).next()
except StopIteration:
logging.error('No more free /%u prefix available', prefix_len)
raise
while len(prefix) < prefix_len:
self.db.execute("UPDATE cert SET prefix = ? WHERE prefix = ?", (prefix + '1', prefix))
prefix += '0'
self.db.execute("INSERT INTO cert VALUES (?,null,null)", (prefix,))
if len(prefix) < max_len or '1' in prefix:
return prefix
self.db.execute("UPDATE cert SET cert = 'reserved' WHERE prefix = ?", (prefix,))
return self._getPrefix(prefix_len)
def requestCertificate(self, handler, token, cert_req):
try:
req = crypto.load_certificate_request(crypto.FILETYPE_PEM, cert_req)
with self.db:
if token is None:
prefix_len = self.config.anonymous_prefix_length
if not prefix_len:
return
email = None
else:
try:
token, email, prefix_len, _ = self.db.execute(
"SELECT * FROM token WHERE token = ?",
(token,)).next()
except StopIteration:
return
self.db.execute("DELETE FROM token WHERE token = ?", (token,))
# Get a new prefix
prefix = self._getPrefix(prefix_len)
# Create certificate
cert = crypto.X509()
cert.set_serial_number(0) # required for libssl < 1.0
cert.gmtime_adj_notBefore(0)
cert.gmtime_adj_notAfter(self.cert_duration)
cert.set_issuer(self.ca.get_subject())
subject = req.get_subject()
subject.CN = "%u/%u" % (int(prefix, 2), prefix_len)
cert.set_subject(subject)
cert.set_pubkey(req.get_pubkey())
cert.sign(self.key, 'sha1')
cert = crypto.dump_certificate(crypto.FILETYPE_PEM, cert)
# Insert certificate into db
self.db.execute("UPDATE cert SET email = ?, cert = ? WHERE prefix = ?", (email, cert, prefix))
return cert
except Exception:
f = traceback.format_exception(*sys.exc_info())
logging.error('%s%s', f.pop(), ''.join(f))
raise
def getCa(self, handler):
return crypto.dump_certificate(crypto.FILETYPE_PEM, self.ca)
def getPrivateAddress(self, handler):
return self.config.private
def getBootstrapPeer(self, handler, client_prefix):
cert, = self.db.execute("SELECT cert FROM cert WHERE prefix = ?",
(client_prefix,)).next()
address = self.config.private, tunnel.PORT
self.sock.sendto('\2', address)
peer = None
while select.select([self.sock], [], [], peer is None)[0]:
msg = self.sock.recv(1<<16)
if msg[0] == '\1':
try:
peer = msg[1:].split('\n')[-2]
except IndexError:
peer = ''
if peer is None:
raise EnvironmentError("Timeout while querying [%s]:%u" % address)
if not peer or peer.split()[0] == client_prefix:
raise LookupError("No bootstrap peer found")
logging.info("Sending bootstrap peer: %s", peer)
r, w = os.pipe()
try:
threading.Thread(target=os.write, args=(w, cert)).start()
p = subprocess.Popen(('openssl', 'rsautl', '-encrypt', '-certin', '-inkey', '/proc/self/fd/%u' % r),
stdin=subprocess.PIPE, stdout=subprocess.PIPE)
return xmlrpclib.Binary(p.communicate(peer)[0])
finally:
os.close(r)
os.close(w)
def topology(self, handler):
if handler.client_address[0] in ('127.0.0.1', '::'):
is_registry = utils.binFromIp(self.config.private
)[len(self.network):].startswith
peers = deque('%u/%u' % (int(x, 2), len(x))
for x, in self.db.execute("SELECT prefix FROM cert")
if is_registry(x))
assert len(peers) == 1
cookie = hex(random.randint(0, 1<<32))[2:]
graph = dict.fromkeys(peers)
asked = 0
while True:
r, w, _ = select.select([self.sock],
[self.sock] if peers else [], [], 1)
if r:
answer = self.sock.recv(1<<16)
if answer[0] == '\xfe':
answer = answer[1:].split('\n')[:-1]
if len(answer) >= 3 and answer[0] == cookie:
x = answer[3:]
assert answer[1] not in x, (answer, graph)
graph[answer[1]] = x[:int(answer[2])]
x = set(x).difference(graph)
peers += x
graph.update(dict.fromkeys(x))
if w:
x = utils.binFromSubnet(peers.popleft())
x = utils.ipFromBin(self.network + x)
try:
self.sock.sendto('\xff%s\n' % cookie, (x, tunnel.PORT))
except socket.error:
pass
elif not r:
break
return graph
if __name__ == "__main__": if __name__ == "__main__":
main() main()
import logging, sqlite3, socket, subprocess, xmlrpclib, time import logging, sqlite3, socket, subprocess, time
from urllib import splittype, splithost, splitport from . import utils
import utils
class PeerDB(object): class PeerDB(object):
...@@ -10,7 +9,7 @@ class PeerDB(object): ...@@ -10,7 +9,7 @@ class PeerDB(object):
self._prefix = prefix self._prefix = prefix
self._db_size = db_size self._db_size = db_size
self._key_path = key_path self._key_path = key_path
self._proxy = xmlrpclib.ServerProxy(registry) self._registry = registry
logging.info('Initialize cache ...') logging.info('Initialize cache ...')
self._db = sqlite3.connect(db_path, isolation_level=None) self._db = sqlite3.connect(db_path, isolation_level=None)
...@@ -37,7 +36,7 @@ class PeerDB(object): ...@@ -37,7 +36,7 @@ class PeerDB(object):
retry = 1 retry = 1
while True: while True:
try: try:
a = self._proxy.getPrivateAddress() a = self._registry.getPrivateAddress(self._prefix)
break break
except socket.error, e: except socket.error, e:
logging.warning(e) logging.warning(e)
...@@ -83,16 +82,9 @@ class PeerDB(object): ...@@ -83,16 +82,9 @@ class PeerDB(object):
def getBootstrapPeer(self): def getBootstrapPeer(self):
logging.info('Getting Boot peer...') logging.info('Getting Boot peer...')
try: try:
bootpeer = self._proxy.getBootstrapPeer(self._prefix).data bootpeer = self._registry.getBootstrapPeer(self._prefix)
p = subprocess.Popen( prefix, address = utils.decrypt(self._key_path, bootpeer).split()
('openssl', 'rsautl', '-decrypt', '-inkey', self._key_path), except (socket.error, subprocess.CalledProcessError, ValueError), e:
stdin=subprocess.PIPE, stdout=subprocess.PIPE)
out, err = p.communicate(bootpeer)
if p.returncode:
raise subprocess.CalledProcessError(p.returncode, err)
prefix, address = out.split()
except (socket.error, xmlrpclib.Fault, subprocess.CalledProcessError,
ValueError), e:
logging.warning('Failed to bootstrap (%s)', e) logging.warning('Failed to bootstrap (%s)', e)
else: else:
if prefix != self._prefix: if prefix != self._prefix:
......
import base64, hmac, hashlib, httplib, inspect, logging, mailbox, os, random
import select, smtplib, socket, sqlite3, string, struct, threading, time
from collections import deque
from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
from email.mime.text import MIMEText
from OpenSSL import crypto
from urllib import splittype, splithost, splitport, urlencode
from . import tunnel, utils
HMAC_HEADER = "Re6stHMAC"
class getcallargs(type):
def __init__(cls, name, bases, d):
type.__init__(cls, name, bases, d)
for n, f in d.iteritems():
if n[0] == '_':
continue
try:
args, varargs, varkw, defaults = inspect.getargspec(f)
except TypeError:
continue
if varargs or varkw or defaults:
continue
f.getcallargs = eval("lambda %s: locals()" % ','.join(args[1:]))
class RegistryServer(object):
__metaclass__ = getcallargs
def __init__(self, config):
self.config = config
self.cert_duration = 365 * 86400
self.lock = threading.Lock()
self.sessions = {}
if self.config.private:
self.sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
else:
logging.warning('You have declared no private address'
', either this is the first start, or you should'
'check you configuration')
# Database initializing
utils.makedirs(os.path.dirname(self.config.db))
self.db = sqlite3.connect(self.config.db, isolation_level=None,
check_same_thread=False)
self.db.execute("""CREATE TABLE IF NOT EXISTS token (
token text primary key not null,
email text not null,
prefix_len integer not null,
date integer not null)""")
try:
self.db.execute("""CREATE TABLE cert (
prefix text primary key not null,
email text,
cert text)""")
except sqlite3.OperationalError, e:
if e.args[0] != 'table cert already exists':
raise RuntimeError
else:
self.db.execute("INSERT INTO cert VALUES ('',null,null)")
# Loading certificates
with open(self.config.ca) as f:
self.ca = crypto.load_certificate(crypto.FILETYPE_PEM, f.read())
with open(self.config.key) as f:
self.key = crypto.load_privatekey(crypto.FILETYPE_PEM, f.read())
# Get vpn network prefix
self.network = utils.networkFromCa(self.ca)
logging.info("Network: %s/%u", utils.ipFromBin(self.network),
len(self.network))
self._email = self.ca.get_subject().emailAddress
def _handle_request(self, request, method, kw):
m = getattr(self, method)
if method in ('topology',) and \
request.client_address[0] not in ('127.0.0.1', '::'):
return request.send_error(httplib.FORBIDDEN)
key = m.getcallargs(**kw).get('cn')
if key:
h = base64.b64decode(request.headers[HMAC_HEADER])
with self.lock:
session = self.sessions[key]
for key in session:
if h == hmac.HMAC(key, request.path, hashlib.sha1).digest():
break
else:
raise Exception("Wrong HMAC")
key = hashlib.sha1(key).digest()
session[:] = hashlib.sha1(key).digest(),
try:
result = m(**kw)
except:
logging.warning(request.requestline, exc_info=1)
return request.send_error(httplib.INTERNAL_SERVER_ERROR)
if result:
request.send_response(httplib.OK)
request.send_header("Content-Length", str(len(result)))
else:
request.send_response(httplib.NO_CONTENT)
if key:
request.send_header(HMAC_HEADER, base64.b64encode(
hmac.HMAC(key, result, hashlib.sha1).digest()))
request.end_headers()
if result:
request.wfile.write(result)
def hello(self, client_prefix):
with self.lock:
cert = self._getCert(client_prefix)
key = hashlib.sha1(struct.pack('Q',
random.getrandbits(64))).digest()
self.sessions.setdefault(client_prefix, [])[1:] = key,
key = utils.encrypt(cert, key)
sign = crypto.sign(self.key, key, 'sha1')
assert len(key) == len(sign)
return key + sign
def _getCert(self, client_prefix):
assert self.lock.locked()
return self.db.execute("SELECT cert FROM cert WHERE prefix = ?",
(client_prefix,)).next()[0]
def requestToken(self, email):
while True:
# Generating token
token = ''.join(random.sample(string.ascii_lowercase, 8))
args = token, email, self.config.prefix_length, int(time.time())
# Updating database
try:
self.db.execute("INSERT INTO token VALUES (?,?,?,?)", args)
break
except sqlite3.IntegrityError:
pass
# Creating and sending email
msg = MIMEText('Hello, your token to join re6st network is: %s\n'
% token)
msg['Subject'] = '[re6stnet] Token Request'
if self._email:
msg['From'] = self._email
msg['To'] = email
if os.path.isabs(self.config.mailhost) or \
os.path.isfile(self.config.mailhost):
with self.lock:
m = mailbox.mbox(self.config.mailhost)
try:
m.add(msg)
finally:
m.close()
else:
s = smtplib.SMTP(self.config.mailhost)
s.sendmail(self._email, email, msg.as_string())
s.quit()
def _getPrefix(self, prefix_len):
max_len = 128 - len(self.network)
assert 0 < prefix_len <= max_len
try:
prefix, = self.db.execute("""SELECT prefix FROM cert WHERE length(prefix) <= ? AND cert is null
ORDER BY length(prefix) DESC""", (prefix_len,)).next()
except StopIteration:
logging.error('No more free /%u prefix available', prefix_len)
raise
while len(prefix) < prefix_len:
self.db.execute("UPDATE cert SET prefix = ? WHERE prefix = ?", (prefix + '1', prefix))
prefix += '0'
self.db.execute("INSERT INTO cert VALUES (?,null,null)", (prefix,))
if len(prefix) < max_len or '1' in prefix:
return prefix
self.db.execute("UPDATE cert SET cert = 'reserved' WHERE prefix = ?", (prefix,))
return self._getPrefix(prefix_len)
def requestCertificate(self, token, req):
req = crypto.load_certificate_request(crypto.FILETYPE_PEM, req)
with self.lock:
with self.db:
if token is None:
prefix_len = self.config.anonymous_prefix_length
if not prefix_len:
return
email = None
else:
try:
token, email, prefix_len, _ = self.db.execute(
"SELECT * FROM token WHERE token = ?",
(token,)).next()
except StopIteration:
return
self.db.execute("DELETE FROM token WHERE token = ?", (token,))
# Get a new prefix
prefix = self._getPrefix(prefix_len)
# Create certificate
cert = crypto.X509()
cert.set_serial_number(0) # required for libssl < 1.0
cert.gmtime_adj_notBefore(0)
cert.gmtime_adj_notAfter(self.cert_duration)
cert.set_issuer(self.ca.get_subject())
subject = req.get_subject()
subject.CN = "%u/%u" % (int(prefix, 2), prefix_len)
cert.set_subject(subject)
cert.set_pubkey(req.get_pubkey())
cert.sign(self.key, 'sha1')
cert = crypto.dump_certificate(crypto.FILETYPE_PEM, cert)
# Insert certificate into db
self.db.execute("UPDATE cert SET email = ?, cert = ? WHERE prefix = ?", (email, cert, prefix))
return cert
def getCa(self):
return crypto.dump_certificate(crypto.FILETYPE_PEM, self.ca)
def getPrivateAddress(self, cn):
return self.config.private
def getBootstrapPeer(self, cn):
with self.lock:
cert = self._getCert(cn)
address = self.config.private, tunnel.PORT
self.sock.sendto('\2', address)
peer = None
while select.select([self.sock], [], [], peer is None)[0]:
msg = self.sock.recv(1<<16)
if msg[0] == '\1':
try:
peer = msg[1:].split('\n')[-2]
except IndexError:
peer = ''
if peer is None:
raise EnvironmentError("Timeout while querying [%s]:%u" % address)
if not peer or peer.split()[0] == cn:
raise LookupError("No bootstrap peer found")
logging.info("Sending bootstrap peer: %s", peer)
return utils.encrypt(cert, peer)
def topology(self):
with self.lock:
is_registry = utils.binFromIp(self.config.private
)[len(self.network):].startswith
peers = deque('%u/%u' % (int(x, 2), len(x))
for x, in self.db.execute("SELECT prefix FROM cert")
if is_registry(x))
assert len(peers) == 1
cookie = hex(random.randint(0, 1<<32))[2:]
graph = dict.fromkeys(peers)
asked = 0
while True:
r, w, _ = select.select([self.sock],
[self.sock] if peers else [], [], 1)
if r:
answer = self.sock.recv(1<<16)
if answer[0] == '\xfe':
answer = answer[1:].split('\n')[:-1]
if len(answer) >= 3 and answer[0] == cookie:
x = answer[3:]
assert answer[1] not in x, (answer, graph)
graph[answer[1]] = x[:int(answer[2])]
x = set(x).difference(graph)
peers += x
graph.update(dict.fromkeys(x))
if w:
x = utils.binFromSubnet(peers.popleft())
x = utils.ipFromBin(self.network + x)
try:
self.sock.sendto('\xff%s\n' % cookie, (x, tunnel.PORT))
except socket.error:
pass
elif not r:
break
return repr(graph)
class RegistryClient(object):
_hmac = None
def __init__(self, url, key_path=None, ca=None, auto_close=True):
self.key_path = key_path
self.ca = ca
self.auto_close = auto_close
scheme, host = splittype(url)
host, path = splithost(host)
host, port = splitport(host)
self._conn = dict(http=httplib.HTTPConnection,
https=httplib.HTTPSConnection,
)[scheme](host, port)
self._path = path.rstrip('/')
def __getattr__(self, name):
getcallargs = getattr(RegistryServer, name).getcallargs
def rpc(*args, **kw):
kw = getcallargs(*args, **kw)
query = '/' + name
if kw:
if any(type(v) is not str for v in kw.itervalues()):
raise TypeError
query += '?' + urlencode(kw)
url = self._path + query
client_prefix = kw.get('cn')
retry = True
try:
while retry:
if client_prefix:
key = self._hmac
if not key:
retry = False
h = self.hello(client_prefix)
n = len(h) // 2
crypto.verify(self.ca, h[n:], h[:n], 'sha1')
key = utils.decrypt(self.key_path, h[:n])
h = hmac.HMAC(key, query, hashlib.sha1).digest()
key = hashlib.sha1(key).digest()
self._hmac = hashlib.sha1(key).digest()
else:
retry = False
self._conn.putrequest('GET', url, skip_accept_encoding=1)
if client_prefix:
self._conn.putheader(HMAC_HEADER, base64.b64encode(h))
self._conn.endheaders()
response = self._conn.getresponse()
body = response.read()
if response.status in (httplib.OK, httplib.NO_CONTENT) and (
not client_prefix or
hmac.HMAC(key, body, hashlib.sha1).digest() ==
base64.b64decode(response.msg[HMAC_HEADER])):
if self.auto_close and name != 'hello':
self._conn.close()
return body
if client_prefix:
self._hmac = None
except Exception:
logging.info(url, exc_info=1)
else:
logging.info('%s\nUnexpected response %s %s',
url, response.status, response.reason)
self._conn.close()
setattr(self, name, rpc)
return rpc
import argparse, errno, logging, os, shlex, signal, socket import argparse, errno, logging, os, shlex, signal, socket
import struct, subprocess, textwrap, threading, time import struct, subprocess, textwrap, threading, time
from OpenSSL import crypto
logging_levels = logging.WARNING, logging.INFO, logging.DEBUG, 5 logging_levels = logging.WARNING, logging.INFO, logging.DEBUG, 5
...@@ -128,11 +127,9 @@ def ipFromBin(ip, suffix=''): ...@@ -128,11 +127,9 @@ def ipFromBin(ip, suffix=''):
struct.pack('>QQ', int(ip[:64], 2), int(ip[64:], 2))) struct.pack('>QQ', int(ip[:64], 2), int(ip[64:], 2)))
def networkFromCa(ca): def networkFromCa(ca):
ca = crypto.load_certificate(crypto.FILETYPE_PEM, ca)
return bin(ca.get_serial_number())[3:] return bin(ca.get_serial_number())[3:]
def subnetFromCert(cert): def subnetFromCert(cert):
cert = crypto.load_certificate(crypto.FILETYPE_PEM, cert)
return cert.get_subject().CN return cert.get_subject().CN
def dump_address(address): def dump_address(address):
...@@ -150,3 +147,27 @@ def parse_address(address_list): ...@@ -150,3 +147,27 @@ def parse_address(address_list):
def binFromSubnet(subnet): def binFromSubnet(subnet):
p, l = subnet.split('/') p, l = subnet.split('/')
return bin(int(p))[2:].rjust(int(l), '0') return bin(int(p))[2:].rjust(int(l), '0')
def decrypt(key_path, data):
p = subprocess.Popen(
('openssl', 'rsautl', '-decrypt', '-inkey', key_path),
stdin=subprocess.PIPE, stdout=subprocess.PIPE)
out, err = p.communicate(data)
if p.returncode:
raise subprocess.CalledProcessError(p.returncode, err)
return out
def encrypt(cert, data):
r, w = os.pipe()
try:
threading.Thread(target=os.write, args=(w, cert)).start()
p = subprocess.Popen(('openssl', 'rsautl', '-encrypt', '-certin',
'-inkey', '/proc/self/fd/%u' % r),
stdin=subprocess.PIPE, stdout=subprocess.PIPE)
out, err = p.communicate(data)
finally:
os.close(r)
os.close(w)
if p.returncode:
raise subprocess.CalledProcessError(p.returncode, err)
return out
...@@ -2,7 +2,9 @@ ...@@ -2,7 +2,9 @@
import atexit, errno, logging, os, select, signal import atexit, errno, logging, os, select, signal
import sqlite3, subprocess, sys, time, traceback import sqlite3, subprocess, sys, time, traceback
from collections import deque from collections import deque
from re6st import plib, utils, db, tunnel from OpenSSL import crypto
from re6st import db, plib, tunnel, utils
from re6st.registry import RegistryClient
def getConfig(): def getConfig():
...@@ -115,9 +117,11 @@ def main(): ...@@ -115,9 +117,11 @@ def main():
# Get arguments # Get arguments
config = getConfig() config = getConfig()
with open(config.ca) as f: with open(config.ca) as f:
network = utils.networkFromCa(f.read()) ca = crypto.load_certificate(crypto.FILETYPE_PEM, f.read())
with open(config.cert) as f: with open(config.cert) as f:
prefix = utils.binFromSubnet(utils.subnetFromCert(f.read())) cert = crypto.load_certificate(crypto.FILETYPE_PEM, f.read())
network = utils.networkFromCa(ca)
prefix = utils.binFromSubnet(utils.subnetFromCert(cert))
config.openvpn_args += ( config.openvpn_args += (
'--ca', config.ca, '--ca', config.ca,
'--cert', config.cert, '--cert', config.cert,
...@@ -228,7 +232,8 @@ def main(): ...@@ -228,7 +232,8 @@ def main():
# Create and open read_only pipe to get server events # Create and open read_only pipe to get server events
r_pipe, write_pipe = os.pipe() r_pipe, write_pipe = os.pipe()
read_pipe = os.fdopen(r_pipe) read_pipe = os.fdopen(r_pipe)
peer_db = db.PeerDB(db_path, config.registry, config.key, prefix) registry = RegistryClient(config.registry, config.key, ca)
peer_db = db.PeerDB(db_path, registry, config.key, prefix)
tunnel_manager = tunnel.TunnelManager(write_pipe, peer_db, tunnel_manager = tunnel.TunnelManager(write_pipe, peer_db,
config.openvpn_args, timeout, config.tunnel_refresh, config.openvpn_args, timeout, config.tunnel_refresh,
config.client_count, config.iface_list, network, prefix, config.client_count, config.iface_list, network, prefix,
......
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