Commit d7a4d73f authored by Julien Muchembled's avatar Julien Muchembled

New option to prevent tunnelling accross borders of listed countries

parent 6b45d7ea
......@@ -10,7 +10,7 @@ Package: re6stnet
Architecture: all
Depends: ${misc:Depends}, ${python:Depends}, python-pkg-resources, python-openssl (>= 0.13), openvpn (>= 2.3), babeld (= 1.6.2-nxd1), iproute2 | iproute, openssl
Recommends: ${python:Recommends}, logrotate
Suggests: ndisc6
Suggests: ${python:Suggests}, ndisc6
Conflicts: re6st-node
Replaces: re6st-node
Description: resilient, scalable, IPv6 network application
......@@ -31,6 +31,9 @@ SCRIPTNAME=/etc/init.d/$NAME
#
do_start()
{
if [ -r $CONFDIR/GeoLite2-Country.mmdb ]
then export GEOIP2_MMDB=$CONFDIR/GeoLite2-Country.mmdb
fi
# Return
# 0 if daemon has been started
# 1 if daemon was already running
......
......@@ -15,7 +15,7 @@ include debian/common.mk
override_dh_python2:
sed -i /^miniupnpc$$/d `find $(TMP)/usr -name requires.txt`
dh_python2 --recommends=miniupnpc
dh_python2 --recommends=miniupnpc --suggests=geoip2
override_dh_auto_clean:
make clean
......
import json, logging, os, sqlite3, socket, subprocess, sys, time, zlib
from itertools import chain
from .registry import RegistryClient
from . import utils, version, x509
class Cache(object):
crl = ()
def __init__(self, db_path, registry, cert, db_size=200):
self._prefix = cert.prefix
self._db_size = db_size
......@@ -25,7 +24,8 @@ class Cache(object):
peer TEXT PRIMARY KEY NOT NULL,
try INTEGER NOT NULL DEFAULT 0)""")
q("CREATE INDEX volatile.stat_try ON stat(try)")
q("INSERT INTO volatile.stat (peer) SELECT prefix FROM peer")
q("INSERT INTO volatile.stat (peer)"
" SELECT prefix FROM peer WHERE prefix")
self._db.commit()
self._loadConfig(self._selectConfig(q))
try:
......@@ -71,10 +71,16 @@ class Cache(object):
def _loadConfig(self, config):
cls = self.__class__
logging.debug("Loading network parameters:")
self.crl = self.same_country = ()
for k, v in config:
if k == 'crl':
v = set(json.loads(v))
elif hasattr(cls, k):
if k == 'crl': # BBB
k = 'crl:json'
if k.endswith(':json'):
k = k[:-5]
v = json.loads(v)
if k == 'crl':
v = set(v)
if hasattr(cls, k):
continue
setattr(self, k, v)
logging.debug("- %s: %r", k, v)
......@@ -83,13 +89,20 @@ class Cache(object):
logging.info("Getting new network parameters from registry...")
try:
# TODO: When possible, the registry should be queried via the re6st.
config = json.loads(zlib.decompress(
x = json.loads(zlib.decompress(
self._registry.getNetworkConfig(self._prefix)))
base64 = config.pop('', ())
config = dict((str(k), v.decode('base64') if k in base64 else
str(v) if type(v) is unicode else v)
for k, v in config.iteritems())
config['crl'] = json.dumps(config['crl'])
base64 = x.pop('', ())
config = {}
for k, v in x.iteritems():
k = str(k)
if k in base64:
v = v.decode('base64')
elif type(v) is unicode:
v = str(v)
elif isinstance(v, (list, dict)):
k += ':json'
v = json.dumps(v)
config[k] = v
except socket.error, e:
logging.warning(e)
return
......@@ -186,6 +199,22 @@ class Cache(object):
(prefix,)).fetchone()
return r and r[0]
@property
def my_address(self):
for x, in self._db.execute("SELECT address FROM peer WHERE NOT prefix"):
return x
return ''
@my_address.setter
def my_address(self, *args):
with self._db as db:
db.execute("INSERT OR REPLACE INTO peer VALUES ('', ?)", args)
@my_address.deleter
def my_address(self):
with self._db as db:
db.execute("DELETE FROM peer WHERE NOT prefix")
# Exclude our own address from results in case it is there, which may
# happen if a node change its certificate without clearing the cache.
# IOW, one should probably always put our own address there.
......
......@@ -5,7 +5,7 @@ from SocketServer import ThreadingTCPServer
from urlparse import parse_qsl
if 're6st' not in sys.modules:
sys.path[0] = os.path.dirname(os.path.dirname(sys.path[0]))
from re6st import ctl, registry, utils, version
from re6st import registry, utils, version
# 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 3650 -out ca.crt
......@@ -132,6 +132,8 @@ def main():
help="Interval in seconds between two tunnel refresh: the worst"
" tunnel is closed if the number of client tunnels has reached"
" its maximum number (client-count).")
_('--same-country', action='append', metavar="CODE",
help="prevent tunnelling accross borders of listed countries")
config = parser.parse_args()
......
......@@ -76,11 +76,11 @@ class RegistryServer(object):
"email TEXT NOT NULL",
"prefix_len INTEGER NOT NULL",
"date INTEGER NOT NULL")
if utils.sqliteCreateTable(self.db, "cert",
utils.sqliteCreateTable(self.db, "cert",
"prefix TEXT PRIMARY KEY NOT NULL",
"email TEXT",
"cert TEXT"):
self.db.execute("INSERT INTO cert VALUES ('',null,null)")
"cert TEXT")
self.db.execute("INSERT OR IGNORE INTO cert VALUES ('',null,null)")
utils.sqliteCreateTable(self.db, "crl",
"serial INTEGER PRIMARY KEY NOT NULL",
# Expiration date of revoked certificate.
......@@ -122,6 +122,8 @@ class RegistryServer(object):
}
if self.config.ipv4:
kw['ipv4'], kw['ipv4_sublen'] = self.config.ipv4
if self.config.same_country:
kw['same_country'] = self.config.same_country
for x in ('client_count', 'encrypt', 'hello',
'max_clients', 'min_protocol', 'tunnel_refresh'):
kw[x] = getattr(self.config, x)
......@@ -138,7 +140,7 @@ class RegistryServer(object):
# Example to avoid all nodes to restart at the same time:
# kw['delay_restart'] = 600 * random.random()
kw['version'] = self.version.encode('base64')
self.network_config = zlib.compress(json.dumps(kw))
self.network_config = zlib.compress(json.dumps(kw), 9)
# The 3 first bits code the number of bytes.
def encodeVersion(self, version):
......
import errno, json, logging, os, random
import socket, subprocess, struct, time, weakref
import errno, json, logging, os, random, socket
import subprocess, struct, sys, time, weakref
from collections import defaultdict, deque
from bisect import bisect, insort
from OpenSSL import crypto
......@@ -7,6 +7,27 @@ from . import ctl, plib, rina, utils, version, x509
PORT = 326
family_dict = {
socket.AF_INET: 'IPv4',
socket.AF_INET6: 'IPv6',
}
proto_dict = {
'tcp4': (socket.AF_INET, socket.SOL_TCP),
'udp4': (socket.AF_INET, socket.SOL_UDP),
'tcp6': (socket.AF_INET6, socket.SOL_TCP),
'udp6': (socket.AF_INET6, socket.SOL_UDP),
}
proto_dict['tcp'] = proto_dict['tcp4']
proto_dict['udp'] = proto_dict['udp4']
def resolve(ip, port, proto):
try:
family, proto = proto_dict[proto]
except KeyError:
return None, ()
return family, (x[-1][0]
for x in socket.getaddrinfo(ip, port, family, 0, proto))
class MultiGatewayManager(dict):
......@@ -172,6 +193,7 @@ class BaseTunnelManager(object):
NEED_RESTART = frozenset(('babel_default', 'encrypt', 'hello',
'ipv4', 'ipv4_sublen'))
_geoiplookup = None
_forward = None
_next_rina = True
......@@ -188,6 +210,31 @@ class BaseTunnelManager(object):
address_dict = defaultdict(list)
for family, address in address:
address_dict[family] += address
if any(address_dict.itervalues()):
del cache.my_address
else:
for address in utils.parse_address(cache.my_address):
try:
proto = proto_dict[address[2]]
except KeyError:
continue
address_dict[proto[0]].append(address)
db = os.getenv('GEOIP2_MMDB')
if db:
from geoip2 import database, errors
country = database.Reader(db).country
def geoiplookup(ip):
try:
return country(ip).country.iso_code
except errors.AddressNotFoundError:
return
self._geoiplookup = geoiplookup
self._country = {}
for address in address_dict.itervalues():
self._updateCountry(address)
elif cache.same_country:
sys.exit("Can not respect 'same_country' network configuration"
" (GEOIP2_MMDB not set)")
self._address = dict((family, utils.dump_address(address))
for family, address in address_dict.iteritems()
if address)
......@@ -589,11 +636,23 @@ class BaseTunnelManager(object):
'\7%s (%s)' % (msg, os.uname()[2]))
break
def _updateCountry(self, address):
for address in address:
family, ip = resolve(*address)
for ip in ip:
country = self._geoiplookup(ip)
if country:
if self._country.get(family) != country:
self._country[family] = country
logging.info('%s country: %s',
family_dict[family], country)
return
class TunnelManager(BaseTunnelManager):
NEED_RESTART = BaseTunnelManager.NEED_RESTART.union((
'client_count', 'max_clients', 'tunnel_refresh'))
'client_count', 'max_clients', 'same_country', 'tunnel_refresh'))
def __init__(self, control_socket, cache, cert, openvpn_args,
timeout, client_count, iface_list, address, ip_changed,
......@@ -780,16 +839,35 @@ class TunnelManager(BaseTunnelManager):
if prefix in self._served or prefix in self._connection_dict:
return False
assert prefix != self._prefix, self.__dict__
address = [x for x in utils.parse_address(address)
if x[2] not in self._disable_proto]
address_list = []
same_country = self.cache.same_country
for x in utils.parse_address(address):
if x[2] in self._disable_proto:
continue
if same_country:
family, ip = resolve(*x)
my_country = self._country.get(family)
if my_country:
for ip in ip:
country = self._geoiplookup(ip)
if country and (country != my_country
if my_country in same_country else
country in same_country):
logging.debug('Do not tunnel to %s (%s -> %s)',
ip, my_country, country)
else:
address_list.append((ip, x[1], x[2]))
continue
address_list.append(x)
self.cache.connecting(prefix, 1)
if not address:
if not address_list:
return False
logging.info('Establishing a connection with %u/%u',
int(prefix, 2), len(prefix))
with utils.exit:
iface = self._getFreeInterface(prefix)
self._connection_dict[prefix] = c = Connection(self, address, iface, prefix)
self._connection_dict[prefix] = c = Connection(
self, address_list, iface, prefix)
if self._gateway_manager is not None:
for ip in c:
self._gateway_manager.add(ip, True)
......@@ -917,6 +995,9 @@ class TunnelManager(BaseTunnelManager):
family, address = self._ip_changed(ip)
if address:
self._address[family] = utils.dump_address(address)
if self._geoiplookup:
self._updateCountry(address)
self.cache.my_address = ';'.join(self._address.itervalues())
def broadcastNewVersion(self):
self._babel_dump_new_version()
......
......@@ -32,7 +32,7 @@ if dirty:
# they are intended to the network admin.
# Only 'protocol' is important and it must be increased whenever they would be
# a wish to force an update of nodes.
protocol = 3
protocol = 4
min_protocol = 1
if __name__ == "__main__":
......
......@@ -93,6 +93,9 @@ setup(
# BBB: use MANIFEST.in only so that egg_info works with very old setuptools
include_package_data = True,
install_requires = ['pyOpenSSL >= 0.13', 'miniupnpc'],
extras_require = {
'geoip': ['geoip2'],
},
#dependency_links = [
# "http://miniupnp.free.fr/files/download.php?file=miniupnpc-1.7.20120714.tar.gz#egg=miniupnpc-1.7",
# ],
......
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