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