Commit b1e97826 authored by Julien Muchembled's avatar Julien Muchembled


parent b3cb14da
......@@ -33,6 +33,7 @@ request.
--port port
The port on which the server will listen
Default: 80
--db path
Path to the server Database file. A new DB file will be created
......@@ -72,10 +72,10 @@ Generic options
file ( since it is an optioin, it must be alone on the line ).
--ip address port proto
Specify connection information to be advertised to other nodes.
address MUST be a ipv4 address since as of now openvpn does not
support ipv6 addresses.
Proto should be either udp or tcp-client
IP address advertised to other nodes. Special values:
- upnp: force autoconfiguration via UPnP
- any: ask peers our IP
Default: ask peers if UPnP fails
-l, ``--log`` `directory`
Path to the directory used for log files. Will create one file
......@@ -103,6 +103,12 @@ Generic options
Babel options
Specify a file to write our process id to (option -I of Babel).
Babel verbosity (option -d of Babel).
-i, ``--interface`` `interface`
Give one interface name for each use of the argument. The interface
will be used to detect other nodes on the local network.
......@@ -135,13 +141,11 @@ Tunnel & Peers options
Default : 3600 ( 1 hour )
--pp port proto
Port and protocol used by the openvpn server(s). Start one openvpn
server for each couple port/protocol specified.
Additionally, if no external configuration is given in the command
line, re6stnet will attempt to forward a port with upnp for each
couple port/proto given.
Protocols should be either udp or tcp-server.
Default : (1194, udp), (1194, tcp-server)
Port and protocol to be announced to other peers, ordered by
preference. For each protocol, start one openvpn server on the
first given port.
Protocols: udp, tcp
Default : --pp 1194 udp --pp 1194 tcp
--tunnel-refresh duration
Interval in seconds between two tunnel refresh. Refreshing tunnels
#!/usr/bin/env python
import argparse, random, select, smtplib, sqlite3, string, socket
import random, select, smtplib, sqlite3, string, socket
import subprocess, time, threading, traceback, errno, logging, os, xmlrpclib
from SimpleXMLRPCServer import SimpleXMLRPCServer, SimpleXMLRPCRequestHandler
from email.mime.text import MIMEText
......@@ -15,18 +15,18 @@ SOL_IPV6 = 41
class RequestHandler(SimpleXMLRPCRequestHandler):
def address_string(self):
# Workaround for
return self.client_address[0]
def _dispatch(self, method, params):
logging.debug('%s%r', method, params)
return self.server._dispatch(method, (self,) + params)
class SimpleXMLRPCServer4(SimpleXMLRPCServer):
allow_reuse_address = True
def address_string(self):
# Workaround for
return self.client_address[0]
class SimpleXMLRPCServer6(SimpleXMLRPCServer4):
......@@ -48,10 +48,10 @@ class main(object):
# Command line parsing
parser = argparse.ArgumentParser(
parser = utils.ArgParser(fromfile_prefix_chars='@',
description='Peer discovery http server for re6stnet')
_ = parser.add_argument
_('--port', required=True, type=int, help='Port of the host server')
_('--port', type=int, default=80, help='Port of the host server')
_('--db', required=True,
help='Path to database file')
_('--ca', required=True,
......@@ -76,21 +76,21 @@ class main(object):
address text not null,
date integer default (strftime('%s','now')))""")
self.db.execute("CREATE INDEX IF NOT EXISTS peers_ping ON peers(date)")
self.db.execute("""CREATE TABLE IF NOT EXISTS tokens (
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)""")
self.db.execute("""CREATE TABLE vpn (
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 vpn already exists':
if e.args[0] != 'table cert already exists':
raise RuntimeError
self.db.execute("INSERT INTO vpn VALUES ('',null,null)")
self.db.execute("INSERT INTO cert VALUES ('',null,null)")
# Loading certificates
with open( as f:
......@@ -124,7 +124,7 @@ class main(object):
token = ''.join(random.sample(string.ascii_lowercase, 8))
# Updating database
self.db.execute("INSERT INTO tokens VALUES (?,?,?,?)", (token, email, 16, int(time.time())))
self.db.execute("INSERT INTO token VALUES (?,?,?,?)", (token, email, 16, int(time.time())))
except sqlite3.IntegrityError:
......@@ -143,18 +143,18 @@ class main(object):
max_len = 128 - len(
assert 0 < prefix_len <= max_len
prefix, = self.db.execute("""SELECT prefix FROM vpn WHERE length(prefix) <= ? AND cert is null
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('There are no more free /%s prefix available' % (prefix_len,))
while len(prefix) < prefix_len:
self.db.execute("UPDATE vpn SET prefix = ? WHERE prefix = ?", (prefix + '1', prefix))
self.db.execute("UPDATE cert SET prefix = ? WHERE prefix = ?", (prefix + '1', prefix))
prefix += '0'
self.db.execute("INSERT INTO vpn VALUES (?,null,null)", (prefix,))
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 vpn SET cert = 'reserved' WHERE prefix = ?", (prefix,))
self.db.execute("UPDATE cert SET cert = 'reserved' WHERE prefix = ?", (prefix,))
return self._getPrefix(prefix_len)
def requestCertificate(self, handler, token, cert_req):
......@@ -162,11 +162,11 @@ class main(object):
req = crypto.load_certificate_request(crypto.FILETYPE_PEM, cert_req)
with self.db:
token, email, prefix_len, _ = self.db.execute("SELECT * FROM tokens WHERE token = ?", (token,)).next()
token, email, prefix_len, _ = self.db.execute("SELECT * FROM token WHERE token = ?", (token,)).next()
except StopIteration:
logging.exception('Bad token (%s) in request' % (token,))
self.db.execute("DELETE FROM tokens WHERE token = ?", (token,))
self.db.execute("DELETE FROM token WHERE token = ?", (token,))
# Get a new prefix
prefix = self._getPrefix(prefix_len)
......@@ -185,7 +185,7 @@ class main(object):
cert = crypto.dump_certificate(crypto.FILETYPE_PEM, cert)
# Insert certificate into db
self.db.execute("UPDATE vpn SET email = ?, cert = ? WHERE prefix = ?", (email, cert, prefix))
self.db.execute("UPDATE cert SET email = ?, cert = ? WHERE prefix = ?", (email, cert, prefix))
return cert
......@@ -199,16 +199,14 @@ class main(object):
return 'http://[%s]:%u' % (self.config.private, self.config.port)
def getBootstrapPeer(self, handler, client_prefix):
cert, = self.db.execute("SELECT cert FROM vpn WHERE prefix = ?",
cert, = self.db.execute("SELECT cert FROM cert WHERE prefix = ?",
logging.trace('Getting bootpeer info...')
prefix, address = self.db.execute("""SELECT prefix, address FROM peers
WHERE prefix != ? ORDER BY random() LIMIT 1""", (client_prefix,)).next()
except StopIteration:'No peer to send for bootstrap')
logging.trace('Gotten bootpeer info from db')
r, w = os.pipe()
threading.Thread(target=os.write, args=(w, cert)).start()
......@@ -221,12 +219,11 @@ class main(object):
def declare(self, handler, address):
print "declaring new node"
client_address, _, _, _ = handler.client_address
client_ip = utils.binFromIp(client_address)
if client_ip.startswith(
prefix = client_ip[len(]
prefix, = self.db.execute("SELECT prefix FROM vpn WHERE prefix <= ? ORDER BY prefix DESC LIMIT 1", (prefix,)).next()
prefix, = self.db.execute("SELECT prefix FROM cert WHERE prefix <= ? ORDER BY prefix DESC LIMIT 1", (prefix,)).next()
self.db.execute("INSERT OR REPLACE INTO peers (prefix, address) VALUES (?,?)", (prefix, address))
return True
......@@ -10,7 +10,7 @@ class PeerManager:
# internal ip = temp arg/attribute
def __init__(self, db_path, registry, key_path, refresh_time, address,
internal_ip, prefix, manual, pp, db_size):
internal_ip, prefix, ip_changed, db_size):
self._refresh_time = refresh_time
self.address = address
self._internal_ip = internal_ip
......@@ -18,8 +18,7 @@ class PeerManager:
self.db_size = db_size
self._registry = registry
self._key_path = key_path
self._pp = pp
self._manual = manual
self._ip_changed = ip_changed
self.tunnel_manager = None
self.sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
......@@ -53,7 +52,15 @@ class PeerManager:
a, = self._db.execute("SELECT value FROM config WHERE name='registry'").next()
except StopIteration:
proxy = xmlrpclib.ServerProxy(registry)
a = proxy.getPrivateAddress()
retry = 1
while True:
a = proxy.getPrivateAddress()
except socket.error, e:
retry = min(60, retry * 2)
self._db.execute("INSERT INTO config VALUES ('registry',?)", (a,))
self._proxy = xmlrpclib.ServerProxy(a)
logging.debug('Database prepared')
......@@ -97,7 +104,7 @@ class PeerManager:
self.next_refresh = time.time() + self._refresh_time
logging.debug('Info sent')
logging.warning("Warning : couldn't send ip, unknown external config. retrying in 30s")
logging.warning("Could not send ip, unknown external config. retrying in 30s")
def getUnusedPeers(self, peer_count):
for populate in self._bootstrap, bool:
......@@ -107,7 +114,7 @@ class PeerManager:
if peer_list:
return peer_list
logging.warning('Cannot find any new peers')
logging.warning('Can not find any new peer')
return []
def _bootstrap(self):
......@@ -152,22 +159,19 @@ class PeerManager:
self.whitelist(utils.binFromSubnet(arg))'%s has disconnected' % (arg,))
elif script_type == 'route-up':
if not self._manual:
external_ip = arg
new_address = list([external_ip, port, proto]
for port, proto, _ in self._pp)
if self.address != new_address:
self.address = new_address'Received new external ip : %s'
% (external_ip,))
if self._ip_changed:
address = self._ip_changed(arg)
if self.address != address:
self.address = address'Received new external ip : %s', arg)
except socket.error, e:
logging.debug('socket.error : %s' % e)"""Connection to server failed while declaring external infos""")"Connection to server failed while"
" declaring external infos (%s)", e)
logging.debug('Unknow message recieved from the openvpn pipe : %s'
% msg)"Unknown message received from the openvpn pipe: %s",
def readSocket(self):
msg = self.socket_file.readline()
import errno
import os
import subprocess
import logging
import utils
verbose = 0
here = os.path.realpath(os.path.dirname(__file__))
ovpn_server = os.path.join(here, 'ovpn-server')
ovpn_client = os.path.join(here, 'ovpn-client')
def openvpn(hello_interval, encrypt, *args, **kw):
def openvpn(iface, hello_interval, encrypt, *args, **kw):
args = ['openvpn',
'--dev-type', 'tap',
'--dev', iface,
'--script-security', '2',
'--user', 'nobody',
'--ping-exit', str(4 * hello_interval),
'--group', 'nogroup',
#'--user', 'nobody', '--group', 'nogroup',
] + list(args)
if not encrypt:
args.extend(['--cipher', 'none'])
logging.trace('%s' % (args,))
return subprocess.Popen(args, **kw)
logging.trace('%r', args)
fd =, '%s.log' % iface),
return subprocess.Popen(args, stdout=fd, stderr=subprocess.STDOUT, **kw)
def server(server_ip, ip_length, max_clients, dh_path, pipe_fd, port, proto, hello_interval, encrypt, *args, **kw):
def server(iface, server_ip, ip_length, max_clients, dh_path, pipe_fd, port, proto, hello_interval, encrypt, *args, **kw):
logging.debug('Starting server...')
if server_ip != '':
if server_ip:
script_up = '%s %s/%u' % (ovpn_server, server_ip, 64)
script_up = '%s none' % (ovpn_server)
return openvpn(hello_interval, encrypt,
return openvpn(iface, hello_interval, encrypt,
'--mode', 'server',
'--up', script_up,
......@@ -40,11 +45,11 @@ def server(server_ip, ip_length, max_clients, dh_path, pipe_fd, port, proto, hel
'--dh', dh_path,
'--max-clients', str(max_clients),
'--port', str(port),
'--proto', proto,
'--proto', 'tcp-server' if proto == 'tcp' else proto,
*args, **kw)
def client(server_address, pipe_fd, hello_interval, encrypt, *args, **kw):
def client(iface, server_address, pipe_fd, hello_interval, encrypt, *args, **kw):
logging.debug('Starting client...')
remote = ['--nobind',
......@@ -52,18 +57,17 @@ def client(server_address, pipe_fd, hello_interval, encrypt, *args, **kw):
'--route-up', ovpn_client + ' ' + str(pipe_fd)]
for ip, port, proto in utils.address_list(server_address):
if proto == 'tcp-server':
proto = 'tcp-client'
remote += '--remote', ip, port, proto
remote += '--remote', ip, port, \
'tcp-client' if proto == 'tcp' else proto
except ValueError, e:
logging.warning('Error "%s" in unpacking address %s for openvpn client'
% (e, server_address,))
remote += args
return openvpn(hello_interval, encrypt, *remote, **kw)
return openvpn(iface, hello_interval, encrypt, *remote, **kw)
def router(network, subnet, subnet_size, interface_list,
wireless, hello_interval, state_path, **kw):
wireless, hello_interval, verbose, pidfile, state_path, **kw):'Starting babel...')
args = ['babeld',
'-C', 'redistribute local ip %s/%s le %s' % (subnet, subnet_size, subnet_size),
......@@ -86,8 +90,18 @@ def router(network, subnet, subnet_size, interface_list,
'-S', state_path,
if pidfile:
args += '-I', pidfile
# WKRD: babeld fails to start if pidfile already exists
pidfile = '/var/run/'
except OSError, e:
if e.errno != errno.ENOENT:
if wireless:
args = args + interface_list
logging.trace('%s' % args)
logging.trace('%r', args)
return subprocess.Popen(args, **kw)
......@@ -6,19 +6,13 @@ import utils
# Be carfull the refresh interval should let the routes be established
log = None
class Connection:
def __init__(self, address, write_pipe, hello, iface, prefix, encrypt,
self.process = plib.client(address, write_pipe, hello, encrypt, '--dev', iface,
're6stnet.client.%s.log' % (prefix,)),
os.O_WRONLY | os.O_CREAT | os.O_TRUNC),
self.process = plib.client(iface, address, write_pipe, hello, encrypt,
self.iface = iface
self.routes = 0
self._prefix = prefix
......@@ -61,10 +55,9 @@ class TunnelManager:
for i in xrange(1, self._client_count + 1))
def refresh(self):'Checking the tunnels...')
logging.debug('Checking tunnels...')
if self._next_tunnel_refresh < time.time():'Refreshing the tunnels...')
self._next_tunnel_refresh = time.time() + self._refresh_time
......@@ -84,12 +77,12 @@ class TunnelManager:
self._client_count + self._refresh_count)]:
def _kill(self, prefix):
def _kill(self, prefix, kill=False):'Killing the connection with %s/%u...'
% (hex(int(prefix, 2))[2:], len(prefix)))
connection = self._connection_dict.pop(prefix)
getattr(connection.process, 'kill' if kill else 'terminate')()
except OSError:
# If the process is already exited
......@@ -144,7 +137,7 @@ class TunnelManager:
if iface in self._iface_list and self._net_len < subnet_size < 128:
prefix = ip[self._net_len:subnet_size]
logging.debug('A route to %s has been discovered on the LAN'
% (hex(int(prefix), 2)[2:]))
% hex(int(prefix, 2))[2:])
self._peer_db.blacklist(prefix, 0)
......@@ -163,7 +156,7 @@ class TunnelManager:
def killAll(self):
for prefix in self._connection_dict.keys():
self._kill(prefix, True)
def checkIncomingTunnel(self, prefix):
if prefix in self._connection_dict:
......@@ -3,57 +3,48 @@ import logging
import time
class NoUPnPDevice(Exception):
def __init__(self):
def __str__(self):
return 'No upnp device found'
class Forwarder:
def __init__(self):
self._u = miniupnpc.UPnP()
self._u.discoverdelay = 200
self._rules = []
raise NoUPnPDevice
self._external_ip = self._u.externalipaddress()
self.next_refresh = time.time()
def AddRule(self, local_port, proto):
def addRule(self, local_port, proto):
# Init parameters
external_port = 1000
if proto == 'udp':
upnp_proto = 'UDP'
elif proto == 'tcp-server':
upnp_proto = 'TCP'
else:'Unknown protocol : %s' % proto)
raise RuntimeError
external_port = 1023
desc = 're6stnet openvpn %s server' % proto
proto = proto.upper()
lanaddr = self._u.lanaddr
# Choose a free port
while True:
while self._u.getspecificportmapping(external_port,
upnp_proto) != None:
external_port += 1
if external_port == 65536:
return None
# Make the redirection
if self._u.addportmapping(external_port, upnp_proto, self._u.lanaddr,
int(local_port), 're6stnet openvpn server', ''):
logging.debug('Forwarding %s:%s to %s:%s' % (self._external_ip,
external_port, self._u.lanaddr, local_port))
self._rules.append((external_port, int(local_port), upnp_proto))
return (self._external_ip, str(external_port), proto)
external_port += 1
if external_port > 65535:
raise Exception('Failed to redirect %u/%s via UPnP'
% (local_port, proto))
if not self._u.getspecificportmapping(external_port, proto):
args = external_port, proto, lanaddr, local_port, desc, ''
except Exception, e:
if str(e) != 'ConflictInMappingEntry':
logging.debug('Forwarding %s:%s to %s:%s', self._external_ip,
external_port, self._u.lanaddr, local_port)
return self._external_ip, external_port
def refresh(self):
logging.debug('Refreshing port forwarding')
for external_port, local_port, proto in self._rules:
self._u.addportmapping(external_port, proto, self._u.lanaddr,
local_port, 're6stnet openvpn server', '')
for args in self._rules:
self.next_refresh = time.time() + 500
def clear(self):
for args in self._rules:
self._u.deleteportmapping(args[0], args[1])
del self.rules[:]
......@@ -6,11 +6,22 @@ logging_levels = logging.WARNING, logging.INFO, logging.DEBUG, 5
def setupLog(log_level):
format='%(asctime)s : %(message)s',
format='%(asctime)s %(levelname)-9s %(message)s',
datefmt='%d-%m-%Y %H:%M:%S')
logging.addLevelName(5, 'TRACE')
logging.trace = lambda *args, **kw: logging.log(5, *args, **kw)
class ArgParser(argparse.ArgumentParser):
def convert_arg_line_to_args(self, arg_line):
arg_line = arg_line.split('#')[0].rstrip()
if arg_line:
if arg_line.startswith('@'):
yield arg_line
for arg in ('--' + arg_line.lstrip('--')).split():
if arg.strip():
yield arg
def binFromIp(ip):
ip1, ip2 = struct.unpack('>QQ', socket.inet_pton(socket.AF_INET6, ip))
#!/usr/bin/env python
import os, sys, select, time, socket
import argparse, subprocess, sqlite3, logging, traceback
from argparse import ArgumentParser
from re6st import plib, utils, db, upnpigd, tunnel
class ArgParser(ArgumentParser):
def convert_arg_line_to_args(self, arg_line):
arg_line = arg_line.split('#')[0].rstrip()
if arg_line:
if arg_line.startswith('@'):
yield arg_line
for arg in ('--' + arg_line.lstrip('--')).split():
if arg.strip():
yield arg
import atexit, os, sys, select, socket, time
import argparse, signal, subprocess, sqlite3, logging, traceback
from re6st import plib, utils, db, tunnel
def ovpnArgs(optional_args, ca_path, cert_path, key_path):
# Treat openvpn arguments
......@@ -32,13 +17,13 @@ def ovpnArgs(optional_args, ca_path, cert_path, key_path):
def getConfig():
parser = ArgParser(fromfile_prefix_chars='@',
parser = utils.ArgParser(fromfile_prefix_chars='@',
description='Resilient virtual private network application')
_ = parser.add_argument
# General Configuration options
_('--ip', default=None, dest='address', action='append', nargs=3,
help='Ip address, port and protocol advertised to other vpn nodes')
help='IP address advertised to other nodes')
_('--registry', required=True,
help="HTTP URL of the discovery peer server,"
" with public host (default port: 80)")
......@@ -49,11 +34,15 @@ def getConfig():
_('-s', '--state', default='/var/lib/re6stnet',
help='Path to re6stnet state directory')
_('-v', '--verbose', default=0, type=int,
help='Defines the verbose level')
help='Log level of re6st itself')
_('-i', '--interface', action='append', dest='iface_list', default=[],
help='Extra interface for LAN discovery')
# Routing algorithm options
help='Specify a file to write our process id to')
_('--babel-verb', default=0,
help='Babel verbosity')
_('--hello', type=int, default=15,
help='Hello interval for babel, in seconds')
_('-w', '--wireless', action='store_true',
......@@ -87,11 +76,6 @@ def getConfig():
def main():
# Get arguments
config = getConfig()
if not config.pp:
config.pp = [['1194', 'udp'], ['1194', 'tcp-server']]
config.pp = list((port, proto, 're6stnet-%s' % proto)
for port, proto in config.pp)
manual = bool(config.address)
network = utils.networkFromCa(
internal_ip, prefix = utils.ipFromCert(network, config.cert)
openvpn_args = ovpnArgs(config.openvpn_args,, config.cert,
......@@ -104,76 +88,76 @@ def main():
logging.trace("Configuration :\n%s" % config)
# Set global variables
tunnel.log = config.log
plib.verbose = config.verbose
plib.log = tunnel.log = config.log
# Create and open read_only pipe to get server events'Creating pipe for server events...')
r_pipe, write_pipe = os.pipe()
read_pipe = os.fdopen(r_pipe)
logging.debug('Pipe created')
signal.signal(signal.SIGHUP, lambda *args: sys.exit(-1))
# Init db and tunnels
forwarder = None
if manual:'Detected manual external configuration')
for c, s in ('udp', 'udp'), ('tcp-client', 'tcp-server'):
if len(list(x for x in config.address if x[2] == c)) \
< len(list(x for x in config.pp if x[1] == s)):
logging.warning("""Beware: in manual configuration, you
declared less external configurations regarding
protocol %s/%s than you gave internal server
configurations""" % (c, s))
address = []
if config.pp:
pp = [(int(port), proto) for port, proto in config.pp]
pp = (1194, 'udp'), (1194, 'tcp')
ip_changed = lambda ip: [(ip, str(port), proto) for port, proto in pp]
forwarder = None
if config.ip == 'upnp' or not config.ip:'Attempting automatic configuration via UPnP...')
forwarder = upnpigd.Forwarder()
config.address = []
for port, proto, _ in config.pp:
ext = forwarder.AddRule(port, proto)
if ext:
except upnpigd.NoUPnPDevice:'No upnp device found')
from re6st.upnpigd import Forwarder
forwarder = Forwarder()
except Exception, e:
if config.ip:
raise"%s: assume we are not NATed", e)
for port, proto in pp:
ip, port = forwarder.addRule(port, proto)
address.append((ip, str(port), proto))
elif config.ip != 'any':
address = ip_changed(config.ip)
if address:
ip_changed = None
peer_db = db.PeerManager(db_path, config.registry, config.key,
config.peers_db_refresh, config.address, internal_ip, prefix,
manual, config.pp, 200)
config.peers_db_refresh, address, internal_ip, prefix,
ip_changed, 200)
tunnel_manager = tunnel.TunnelManager(write_pipe, peer_db, openvpn_args,
config.hello, config.tunnel_refresh, config.connection_count,
config.iface_list, network, prefix, 2, config.encrypt)
peer_db.tunnel_manager = tunnel_manager
# Launch routing protocol. WARNING : you have to be root to start babeld
server_tunnels = {}
for x in pp:
server_tunnels.setdefault('re6stnet-' + x[1], x)
interface_list = list(tunnel_manager.free_interface_set) \
+ config.iface_list + list(iface
for _, _, iface in config.pp)
+ config.iface_list + server_tunnels.keys()
subnet = utils.ipFromBin((network + prefix).ljust(128, '0'))
router = plib.router(network, subnet, len(prefix) + len(network), interface_list,