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'` 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..0fcc79b --- /dev/null +++ b/vpn-proxy/app/middleware/cidr.py @@ -0,0 +1,41 @@ +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 __init__(self, get_response): + # One-time configuration & initialization, when the web server starts. + self.get_response = get_response + + self.cidrs = [ + netaddr.IPNetwork(cidr) for cidr in settings.SOURCE_CIDRS + ] + + 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() 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 b0185fa..2c282cf 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..ac244bd 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 @@ -44,7 +57,8 @@ 'app', ] -MIDDLEWARE_CLASSES = [ +MIDDLEWARE = [ + 'app.middleware.cidr.CidrMiddleware', 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware',