re6st-registry 12.2 KB
Newer Older
1
#!/usr/bin/env python
2 3 4
import errno, logging, mailbox, os, random, select
import smtplib, socket, sqlite3, string, subprocess, sys
import threading, time, traceback, xmlrpclib
5
from collections import deque
Guillaume Bury's avatar
Guillaume Bury committed
6
from SimpleXMLRPCServer import SimpleXMLRPCServer, SimpleXMLRPCRequestHandler
Guillaume Bury's avatar
Guillaume Bury committed
7
from email.mime.text import MIMEText
8
from OpenSSL import crypto
9
from re6st import tunnel, utils
10

11 12 13 14 15 16 17
# 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

IPV6_V6ONLY = 26
SOL_IPV6 = 41


Guillaume Bury's avatar
Guillaume Bury committed
18 19
class RequestHandler(SimpleXMLRPCRequestHandler):

Julien Muchembled's avatar
Julien Muchembled committed
20 21 22 23
    def address_string(self):
        # Workaround for http://bugs.python.org/issue6085
        return self.client_address[0]

Guillaume Bury's avatar
Guillaume Bury committed
24
    def _dispatch(self, method, params):
Julien Muchembled's avatar
Julien Muchembled committed
25
        logging.debug('%s%r', method, params)
Guillaume Bury's avatar
Guillaume Bury committed
26 27
        return self.server._dispatch(method, (self,) + params)

28 29 30 31
class SimpleXMLRPCServer4(SimpleXMLRPCServer):

    allow_reuse_address = True

32

33 34 35 36 37 38 39 40
class SimpleXMLRPCServer6(SimpleXMLRPCServer4):

    address_family = socket.AF_INET6

    def server_bind(self):
        self.socket.setsockopt(SOL_IPV6, IPV6_V6ONLY, 1)
        SimpleXMLRPCServer4.server_bind(self)

41

42 43 44
class main(object):

    def __init__(self):
Guillaume Bury's avatar
Guillaume Bury committed
45
        self.cert_duration = 365 * 86400
Ulysse Beaugnon's avatar
Ulysse Beaugnon committed
46
        self.time_out = 45000
47
        self.refresh_interval = 600
48
        self.last_refresh = time.time()
Guillaume Bury's avatar
Guillaume Bury committed
49

Guillaume Bury's avatar
Guillaume Bury committed
50

51
        # Command line parsing
Julien Muchembled's avatar
Julien Muchembled committed
52
        parser = utils.ArgParser(fromfile_prefix_chars='@',
Julien Muchembled's avatar
Julien Muchembled committed
53 54
            description="re6stnet registry used to bootstrap nodes"
                        " and deliver certificates.")
55
        _ = parser.add_argument
Julien Muchembled's avatar
Julien Muchembled committed
56 57 58 59 60 61 62 63 64 65
        _('--port', type=int, default=80,
            help="Port on which the server will listen.")
        _('-4', dest='bind4', default='0.0.0.0',
            help="Bind server to this IPv4.")
        _('-6', dest='bind6', default='::',
            help="Bind server to this IPv6.")
        _('--db', default='/var/lib/re6stnet/registry.db',
            help="Path to SQLite database file. It is automatically initialized"
                 " if the file does not exist.")
        _('--ca', required=True, help=parser._ca_help)
66
        _('--key', required=True,
Julien Muchembled's avatar
Julien Muchembled committed
67
                help="CA private key in .pem format.")
68
        _('--mailhost', required=True,
Julien Muchembled's avatar
Julien Muchembled committed
69 70 71
                help="SMTP host to send confirmation emails. For debugging"
                     " purpose, it can also be an absolute or existing path to"
                     " a mailbox file")
72
        _('--private',
Julien Muchembled's avatar
Julien Muchembled committed
73 74
                help="re6stnet IP of the node on which runs the registry."
                     " Required for normal operation.")
75
        _('--prefix-length', default=16,
Julien Muchembled's avatar
Julien Muchembled committed
76
                help="Default length of allocated prefixes.")
77
        _('-l', '--logfile', default='/var/log/re6stnet/registry.log',
Julien Muchembled's avatar
Julien Muchembled committed
78
                help="Path to logging file.")
79
        _('-v', '--verbose', default=1, type=int,
Julien Muchembled's avatar
Julien Muchembled committed
80 81
                help="Log level. 0 disables logging."
                     " Use SIGUSR1 to reopen log.")
82
        self.config = parser.parse_args()
83

84 85
        utils.setupLog(self.config.verbose, self.config.logfile)

86 87 88
        if self.config.private:
            self.sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
        else:
89 90 91 92
            logging.warning('You have declared no private address'
                    ', either this is the first start, or you should'
                    'check you configuration')

93
        # Database initializing
Julien Muchembled's avatar
Julien Muchembled committed
94
        utils.makedirs(os.path.dirname(self.config.db))
95
        self.db = sqlite3.connect(self.config.db, isolation_level=None)
Julien Muchembled's avatar
Julien Muchembled committed
96
        self.db.execute("""CREATE TABLE IF NOT EXISTS token (
97 98
                        token text primary key not null,
                        email text not null,
99
                        prefix_len integer not null,
100
                        date integer not null)""")
101
        try:
Julien Muchembled's avatar
Julien Muchembled committed
102
            self.db.execute("""CREATE TABLE cert (
103 104 105 106
                               prefix text primary key not null,
                               email text,
                               cert text)""")
        except sqlite3.OperationalError, e:
Julien Muchembled's avatar
Julien Muchembled committed
107
            if e.args[0] != 'table cert already exists':
108 109
                raise RuntimeError
        else:
Julien Muchembled's avatar
Julien Muchembled committed
110
            self.db.execute("INSERT INTO cert VALUES ('',null,null)")
111

112
        # Loading certificates
113
        with open(self.config.ca) as f:
114
            self.ca = crypto.load_certificate(crypto.FILETYPE_PEM, f.read())
115
        with open(self.config.key) as f:
116
            self.key = crypto.load_privatekey(crypto.FILETYPE_PEM, f.read())
117
        # Get vpn network prefix
118
        self.network = bin(self.ca.get_serial_number())[3:]
119 120 121
        logging.info("Network: %s/%u", utils.ipFromBin(self.network),
                                       len(self.network))
        self._email = self.ca.get_subject().emailAddress
122 123

        # Starting server
Julien Muchembled's avatar
Julien Muchembled committed
124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147
        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()
148

Guillaume Bury's avatar
Guillaume Bury committed
149
    def requestToken(self, handler, email):
150 151 152
        while True:
            # Generating token
            token = ''.join(random.sample(string.ascii_lowercase, 8))
153
            args = token, email, self.config.prefix_length, int(time.time())
154 155
            # Updating database
            try:
156
                self.db.execute("INSERT INTO token VALUES (?,?,?,?)", args)
157
                break
158
            except sqlite3.IntegrityError:
159 160 161
                pass

        # Creating and sending email
162 163
        msg = MIMEText('Hello, your token to join re6st network is: %s\n'
                       % token)
164
        msg['Subject'] = '[re6stnet] Token Request'
165 166
        if self._email:
            msg['From'] = self._email
167
        msg['To'] = email
168 169 170 171 172 173 174 175 176 177 178
        if os.path.isabs(self.config.mailhost) or \
           os.path.isfile(self.config.mailhost):
            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()
179

Guillaume Bury's avatar
Guillaume Bury committed
180
    def _getPrefix(self, prefix_len):
Guillaume Bury's avatar
Guillaume Bury committed
181 182 183
        max_len = 128 - len(self.network)
        assert 0 < prefix_len <= max_len
        try:
Julien Muchembled's avatar
Julien Muchembled committed
184
            prefix, = self.db.execute("""SELECT prefix FROM cert WHERE length(prefix) <= ? AND cert is null
Guillaume Bury's avatar
Guillaume Bury committed
185 186
                                         ORDER BY length(prefix) DESC""", (prefix_len,)).next()
        except StopIteration:
187
            logging.error('No more free /%u prefix available', prefix_len)
Guillaume Bury's avatar
Guillaume Bury committed
188 189
            raise
        while len(prefix) < prefix_len:
Julien Muchembled's avatar
Julien Muchembled committed
190
            self.db.execute("UPDATE cert SET prefix = ? WHERE prefix = ?", (prefix + '1', prefix))
Guillaume Bury's avatar
Guillaume Bury committed
191
            prefix += '0'
Julien Muchembled's avatar
Julien Muchembled committed
192
            self.db.execute("INSERT INTO cert VALUES (?,null,null)", (prefix,))
Guillaume Bury's avatar
Guillaume Bury committed
193
        if len(prefix) < max_len or '1' in prefix:
Guillaume Bury's avatar
Guillaume Bury committed
194
            return prefix
Julien Muchembled's avatar
Julien Muchembled committed
195
        self.db.execute("UPDATE cert SET cert = 'reserved' WHERE prefix = ?", (prefix,))
Guillaume Bury's avatar
Guillaume Bury committed
196
        return self._getPrefix(prefix_len)
Guillaume Bury's avatar
Guillaume Bury committed
197

Guillaume Bury's avatar
Guillaume Bury committed
198
    def requestCertificate(self, handler, token, cert_req):
199 200 201 202
        try:
            req = crypto.load_certificate_request(crypto.FILETYPE_PEM, cert_req)
            with self.db:
                try:
Julien Muchembled's avatar
Julien Muchembled committed
203
                    token, email, prefix_len, _ = self.db.execute("SELECT * FROM token WHERE token = ?", (token,)).next()
204
                except StopIteration:
Julien Muchembled's avatar
Julien Muchembled committed
205
                    return
Julien Muchembled's avatar
Julien Muchembled committed
206
                self.db.execute("DELETE FROM token WHERE token = ?", (token,))
207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224

                # Get a new prefix
                prefix = self._getPrefix(prefix_len)

                # Create certificate
                cert = crypto.X509()
                #cert.set_serial_number(serial)
                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
Julien Muchembled's avatar
Julien Muchembled committed
225
                self.db.execute("UPDATE cert SET email = ?, cert = ? WHERE prefix = ?", (email, cert, prefix))
226 227

            return cert
228 229 230
        except Exception:
            f = traceback.format_exception(*sys.exc_info())
            logging.error('%s%s', f.pop(), ''.join(f))
231
            raise
232

Guillaume Bury's avatar
Guillaume Bury committed
233
    def getCa(self, handler):
234 235
        return crypto.dump_certificate(crypto.FILETYPE_PEM, self.ca)

236
    def getPrivateAddress(self, handler):
237
        return self.config.private
238 239

    def getBootstrapPeer(self, handler, client_prefix):
Julien Muchembled's avatar
Julien Muchembled committed
240
        cert, = self.db.execute("SELECT cert FROM cert WHERE prefix = ?",
Guillaume Bury's avatar
Guillaume Bury committed
241
                (client_prefix,)).next()
242 243 244 245 246 247 248 249 250 251 252
        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:
253
            raise EnvironmentError("Timeout while querying [%s]:%u" % address)
254 255 256
        if not peer or peer.split()[0] == client_prefix:
            raise LookupError("No bootstrap peer found")
        logging.info("Sending bootstrap peer: %s", peer)
257 258 259 260 261
        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)
262
            return xmlrpclib.Binary(p.communicate(peer)[0])
263 264 265
        finally:
            os.close(r)
            os.close(w)
266

267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302
    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

303 304
if __name__ == "__main__":
    main()