From 3a94c357fc08524b876eac4096ac71d6fc26248c Mon Sep 17 00:00:00 2001 From: Christos Pollalis Date: Fri, 6 Oct 2017 04:02:06 +0300 Subject: [PATCH 1/4] Install missing dependencies and fix path to requirements.txt --- scripts/install.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/install.sh b/scripts/install.sh index bcdb278..0531c32 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -36,10 +36,11 @@ set -ex apt-get update -q apt-get install -yq --no-install-recommends \ - python python-pip openvpn uwsgi uwsgi-plugin-python + python python-pip openvpn uwsgi uwsgi-plugin-python \ + python-dev build-essential pip install -U pip -pip install -r $DIR/vpn-proxy/requirements.txt +pip install -r $DIR/requirements.txt echo "VPN_SERVER_REMOTE_ADDRESS = \"$VPN_IP\"" > $DIR/vpn-proxy/conf.d/0000-vpn-ip.py SOURCE_CIDRS=`echo "$SOURCE_CIDRS" | sed 's/ /", "/g'` From d7b865c9cb6d4b2cea5a264216988fe5dcf3dab4 Mon Sep 17 00:00:00 2001 From: Christos Pollalis Date: Fri, 6 Oct 2017 15:01:04 +0300 Subject: [PATCH 2/4] A couple of small improvements * Stop using django's reserved ALLOWED_HOSTS settings variable to avoid unexpected denial of service * Use variables from project.settings directly, without re-declaring them as module-level variables * Update the docstring of app.models:choose_ip --- vpn-proxy/app/models.py | 47 +++++++++++++++++------------------ vpn-proxy/app/tunnels.py | 9 +++---- vpn-proxy/project/settings.py | 19 +++++++++++--- 3 files changed, 43 insertions(+), 32 deletions(-) diff --git a/vpn-proxy/app/models.py b/vpn-proxy/app/models.py index c301b75..4632520 100644 --- a/vpn-proxy/app/models.py +++ b/vpn-proxy/app/models.py @@ -14,38 +14,37 @@ from .tunnels import get_conf, get_client_conf, get_client_script from .tunnels import add_iptables, del_iptables, add_fwmark, del_fwmark -IFACE_PREFIX = settings.IFACE_PREFIX -SERVER_PORT_START = settings.SERVER_PORT_START + PORT_ALLOC_START, PORT_ALLOC_STOP = settings.PORT_ALLOC_RANGE -ALLOWED_VPN_ADDRESSES = settings.ALLOWED_HOSTS -EXCLUDED_VPN_ADDRESSES = settings.EXCLUDED_HOSTS + log = logging.getLogger(__name__) -def choose_ip(routable_cidrs, excluded_cidrs=[], - reserved_cidrs=EXCLUDED_VPN_ADDRESSES, client_addr=''): - """ - Find available IP addresses for both sides of a VPN Tunnel (client & - server) based on a list of available_cidrs. This method iterates over the - CIDRs contained in ALLOWED_VPN_ADDRESSES, while excluding the lists of - routable_cidrs, excluded_cidrs, and reserved_cidrs. - :param routable_cidrs: the CIDRs that are to be routed over the - particular VPN Tunnel - :param excluded_cidrs: an optional CIDRs list provided by the end user - in order to be excluded from the address allocation process - :param reserved_cidrs: CIDRs reserved for local usage - :param client_addr: since the client IP is allocated first, the client_addr - is used to attempt to pick an adjacent IP address for the server side. - Alternatively, this is an empty string +def choose_ip(routable_cidrs, excluded_cidrs=[], client_addr=''): + """Find available IP addresses for both sides of a VPN Tunnel. + + This method iterates over the settings.ALLOWED_CIDRS list in order to + allocate available IP address to both the client and server side of a + VPN tunnel. CIDRs that belong to the lists of settings.RESERVED_CIDRS, + `routable_cidrs`, and `excluded_cidrs` are excluded from the allocation + process. + + :param routable_cidrs: the CIDRs that are to be routed over a VPN tunnel + :param excluded_cidrs: an optional list of CIDRs to be excluded from the + address allocation process + :param client_addr: the `client_addr` is used to attempt to pick an + adjacent IP address for the server side + :return: a private IP address + """ - exc_nets = routable_cidrs + excluded_cidrs + reserved_cidrs + exc_nets = routable_cidrs + excluded_cidrs + settings.RESERVED_CIDRS # make sure the exc_nets list does not contain any empty strings exc_nets = [exc_net for exc_net in exc_nets if exc_net] # a list of unique, non-overlapping supernets (to be excluded) exc_nets = IPSet(exc_nets).iter_cidrs() - for network in ALLOWED_VPN_ADDRESSES: + for network in settings.ALLOWED_CIDRS: available_cidrs = IPSet(IPNetwork(network)) for exc_net in exc_nets: available_cidrs.remove(exc_net) @@ -145,16 +144,16 @@ class Tunnel(BaseModel): client = models.GenericIPAddressField(protocol='IPv4', validators=[check_ip]) key = models.TextField(default=gen_key, blank=False, unique=True) - protocol = models.CharField(max_length=3, default='udp', + protocol = models.CharField(max_length=3, default='udp', choices=[('udp', 'UDP'), ('tcp', 'TCP')]) @property def name(self): - return '%s%s' % (IFACE_PREFIX, self.id) + return '%s%s' % (settings.IFACE_PREFIX, self.id) @property def port(self): - return (SERVER_PORT_START + self.id - 1) if self.id else None + return (settings.SERVER_PORT_START + self.id - 1) if self.id else None @property def rtable(self): diff --git a/vpn-proxy/app/tunnels.py b/vpn-proxy/app/tunnels.py index 704a06a..ee8f546 100644 --- a/vpn-proxy/app/tunnels.py +++ b/vpn-proxy/app/tunnels.py @@ -8,8 +8,7 @@ SOURCE_CIDRS = ','.join(settings.SOURCE_CIDRS) -REMOTE_IP = settings.VPN_SERVER_REMOTE_ADDRESS -IN_IFACE = settings.IN_IFACE + log = logging.getLogger(__name__) @@ -233,7 +232,7 @@ def get_conf(tunnel): def get_client_conf(tunnel): - return '\n'.join(['remote %s' % REMOTE_IP, + return '\n'.join(['remote %s' % settings.VPN_SERVER_REMOTE_ADDRESS, 'dev %s' % tunnel.name, 'dev-type tun', 'port %s' % tunnel.port, @@ -299,14 +298,14 @@ def check_iptables(forwarding, job='-C', rule=''): MASQUERADE packets routed via the virtual interface""" mangle_rule = ['iptables', '-w', '-t', 'mangle', job, 'PREROUTING', '-p', 'tcp', - '-i', str(IN_IFACE), + '-i', str(settings.IN_IFACE), '-s', str(SOURCE_CIDRS), '--destination-port', str(forwarding.loc_port), '-j', 'MARK', '--set-mark', str(forwarding.tunnel.id)] nat_rule = ['iptables', '-w', '-t', 'nat', job, 'PREROUTING', '-p', 'tcp', - '-i', str(IN_IFACE), + '-i', str(settings.IN_IFACE), '-s', str(SOURCE_CIDRS), '--destination-port', str(forwarding.loc_port), '-j', 'DNAT', '--to-destination', str(forwarding.destination)] diff --git a/vpn-proxy/project/settings.py b/vpn-proxy/project/settings.py index 3f9a466..b76b39c 100644 --- a/vpn-proxy/project/settings.py +++ b/vpn-proxy/project/settings.py @@ -24,12 +24,25 @@ # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True -# IP addressing settings -ALLOWED_HOSTS = ['10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16'] -EXCLUDED_HOSTS = [] +# A list of client IPs and hostnames the application may serve +# See https://docs.djangoproject.com/en/1.11/ref/settings/#allowed-hosts +ALLOWED_HOSTS = ['*'] + +# IP address allocation range +ALLOWED_CIDRS = ['10.0.0.0/8', '172.17.0.0/12', '192.168.0.0/16'] +RESERVED_CIDRS = [] + +# The minimum port number OpenVPN may listen on for incoming connections SERVER_PORT_START = 1195 + +# The range of available ports to be allocated on the server-side in order +# to proxy requests over VPN PORT_ALLOC_RANGE = (5000, 10000) + +# VPN interface name prefix IFACE_PREFIX = 'vpn-tun' + +# The interface that may accept proxying requests IN_IFACE = 'eth0' # Application definition From c7a1ec8baba9f328083b9bc6f78d36c565dadec0 Mon Sep 17 00:00:00 2001 From: Christos Pollalis Date: Fri, 6 Oct 2017 16:44:03 +0300 Subject: [PATCH 3/4] Add CidrMiddleware The CidrMiddleware extends the idea behind settings.ALLOWED_HOSTS in order to allow filtering to take place based on IP ranges instead of only distinct IP addresses --- vpn-proxy/app/middleware/__init__.py | 0 vpn-proxy/app/middleware/cidr.py | 38 ++++++++++++++++++++++++++++ vpn-proxy/project/settings.py | 1 + 3 files changed, 39 insertions(+) create mode 100644 vpn-proxy/app/middleware/__init__.py create mode 100644 vpn-proxy/app/middleware/cidr.py diff --git a/vpn-proxy/app/middleware/__init__.py b/vpn-proxy/app/middleware/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/vpn-proxy/app/middleware/cidr.py b/vpn-proxy/app/middleware/cidr.py new file mode 100644 index 0000000..b762911 --- /dev/null +++ b/vpn-proxy/app/middleware/cidr.py @@ -0,0 +1,38 @@ +import netaddr +import logging + +from django.http import Http404 +from django.conf import settings + + +log = logging.getLogger(__name__) + + +class CidrMiddleware(object): + """A middleware that filters requests based on their origin. + + This middleware plays the role of settings.ALLOWED_HOSTS, while it also + allows filtering to take place based on ranges of IP addresses given in + CIDR notation. + + The host's IP address is verified against the list of networks provided + in settings.SOURCE_CIDRS. In case of no match, an HTTP 404 status code + is returned. + + See https://docs.djangoproject.com/en/1.11/ref/settings/#allowed-hosts + + """ + + def process_request(self, request): + + # Get the HTTP host from the request headers. + host = request.META['REMOTE_ADDR'] + + host = netaddr.IPAddress(host) + cidrs = [netaddr.IPNetwork(cidr) for cidr in settings.SOURCE_CIDRS] + + for cidr in cidrs: + if host in cidr: + return + log.critical('Connection attempt from unauthorized source %s', host) + raise Http404 diff --git a/vpn-proxy/project/settings.py b/vpn-proxy/project/settings.py index b76b39c..a0e9ab1 100644 --- a/vpn-proxy/project/settings.py +++ b/vpn-proxy/project/settings.py @@ -58,6 +58,7 @@ ] MIDDLEWARE_CLASSES = [ + 'app.middleware.cidr.CidrMiddleware', 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', From 9ea6c90da689cbca636c2ffe861d6b516184b0e3 Mon Sep 17 00:00:00 2001 From: Christos Pollalis Date: Fri, 6 Oct 2017 22:15:54 +0300 Subject: [PATCH 4/4] Update old-style CidrMiddleware According to https://docs.djangoproject.com/en/1.11/topics/http/middleware/#upgrading-pre-django-1-10-style-middleware --- vpn-proxy/app/middleware/cidr.py | 23 +++++++++++++---------- vpn-proxy/project/settings.py | 2 +- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/vpn-proxy/app/middleware/cidr.py b/vpn-proxy/app/middleware/cidr.py index b762911..0fcc79b 100644 --- a/vpn-proxy/app/middleware/cidr.py +++ b/vpn-proxy/app/middleware/cidr.py @@ -23,16 +23,19 @@ class CidrMiddleware(object): """ - def process_request(self, request): + def __init__(self, get_response): + # One-time configuration & initialization, when the web server starts. + self.get_response = get_response - # Get the HTTP host from the request headers. - host = request.META['REMOTE_ADDR'] - - host = netaddr.IPAddress(host) - cidrs = [netaddr.IPNetwork(cidr) for cidr in settings.SOURCE_CIDRS] + self.cidrs = [ + netaddr.IPNetwork(cidr) for cidr in settings.SOURCE_CIDRS + ] - for cidr in cidrs: - if host in cidr: - return + def __call__(self, request): + # Get the source IP address from the request headers. + host = request.META['REMOTE_ADDR'] + for cidr in self.cidrs: + if netaddr.IPAddress(host) in cidr: + return self.get_response(request) log.critical('Connection attempt from unauthorized source %s', host) - raise Http404 + raise Http404() diff --git a/vpn-proxy/project/settings.py b/vpn-proxy/project/settings.py index a0e9ab1..ac244bd 100644 --- a/vpn-proxy/project/settings.py +++ b/vpn-proxy/project/settings.py @@ -57,7 +57,7 @@ 'app', ] -MIDDLEWARE_CLASSES = [ +MIDDLEWARE = [ 'app.middleware.cidr.CidrMiddleware', 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',