Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat randomize gateways #11

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,16 @@ Default config file `/etc/riseup-vpn.yaml`
---
# /etc/riseup-vpn.yaml

# if given, use it as gateway
server: vpn07-par.riseup.net

# if `server` is not set, randomly pick any server from `location`
# location: Seattle

# openvpn protocol to use. If not set, randomly pick any protocol supported by server
protocol: udp

# openvpn port to use. If not set, randomly pick any port supported by server
port: 53

# excluded_routes: list servcies that should not be routed over VPN
Expand All @@ -69,9 +77,18 @@ excluded_routes:
# os user/group
user: openvpn
group: openvpn

# configuration verbatim to append to openvpn configuraion
extra_config: |
# disable logs
verb: 0
```

`server`, `protocol` and `port` are optional. If not given, `--generate-config` will randomly pick missing parameters.

# How to use it
[![asciicast](https://asciinema.org/a/559611.svg)](https://asciinema.org/a/559611)

# Installation (as a dev)

We need to run the tool with root permissions (for example to write to /etc). Running the tests also need root privileges (because we use chown). Therefore, I recommend running the dev environment also as root user.
Expand Down
139 changes: 108 additions & 31 deletions riseup_vpn_configurator/__init__.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,27 @@
#!/usr/bin/env python3
import sys
import os
import logging
import argparse
import grp
import json
import yaml
import subprocess
import logging
import os
import pwd
import grp
from jinja2 import Template
import random
import shutil
import socket
import subprocess
import sys
from dataclasses import dataclass
from ipaddress import ip_network
from pathlib import Path
from typing import NoReturn, Optional

import psutil
import requests
from ipaddress import ip_network
from pyasn1_modules import pem, rfc2459
import yaml
from icmplib import ICMPLibError, ping
from jinja2 import Template
from pyasn1.codec.der import decoder
import psutil
from icmplib import ping, ICMPLibError
import shutil
import socket
from typing import Optional, NoReturn
from pyasn1_modules import pem, rfc2459

FORMAT = "%(levelname)s: %(message)s"
logging.basicConfig(format=FORMAT, level=logging.INFO)
Expand Down Expand Up @@ -221,15 +224,17 @@ def check_config_file() -> None:
logging.error(f"Could not parse config file {config_file}")
print_default_config(1)

for c in ("server", "protocol", "port", "excluded_routes"):
# check mandatory parameters
for c in ("excluded_routes",):
if c not in y.keys():
logging.error(f"Error checking configuration file ({config_file}): '{c}' not specified")
sys.exit(1)

if y["protocol"] not in ("tcp", "udp"):
if "protocol" in y and y["protocol"] not in ("tcp", "udp"):
logging.error(f"Error checking configuration file ({config_file}): 'protocol' must be one of the values tcp|udp (specified was '{y['protocol']}')")
sys.exit(1)
if not str(y["port"]).isnumeric():

if "port" in y and not str(y["port"]).isnumeric():
logging.error(f"Error checking configuration file ({config_file}): 'port' must be numeric (specified was '{y['port']}')")
sys.exit(1)

Expand All @@ -244,24 +249,95 @@ def check_config_file() -> None:
sys.exit(1)
logging.info("Configuration file: OK")

@dataclass
class Gateway:
host: Optional[str]
protocols: list[str]
ports: list[int]
ip_address: Optional[str]
location: Optional[str]

@classmethod
def from_json(cls, gw_item: dict):
for transport in gw_item.get('capabilities',{}).get('transport', {}):
if transport.get('type') == 'openvpn':
protocols = transport.get('protocols', [])
ports = [int(p) for p in transport.get('ports', [])]
break
return cls(host=gw_item.get('host'),
protocols=protocols,
ports=ports,
ip_address=gw_item.get('ip_address'),
location=gw_item.get('location'))

def filter_gateways(gateways: dict, filter: Gateway) -> list:
"""Find gateways that match the given filter."""
candidates = []
for gw_item in gateways:
gw = Gateway.from_json(gw_item)

if filter.host and gw.host == filter.host:
# force given gateway
return [gw]

if filter.protocols:
if not [proto for proto in filter.protocols if proto in gw.protocols]:
# gateway does not support any wanted protocols
continue
else:
# force given protocol
gw.protocols = filter.protocols

if filter.ports:
if not [port for port in filter.ports if port in gw.ports]:
# gateway does not support any wanted ports
continue
else:
# force given port
gw.ports = filter.ports

def get_server_info() -> Optional[dict]:
if filter.location and gw.location != filter.location:
# gateway is not in the wanted location
continue

candidates.append(gw)

return candidates

def get_server_info() -> dict:
with open(config_file) as f:
config = yaml.safe_load(f)
with open(gateway_json) as f:
j = json.load(f)
gateways = j['gateways']
for gw in gateways:
if gw['host'] == config['server']:
return {
'hostname': gw['host'],
'ip_address': gw['ip_address'],
'proto': config['protocol'],
'port': config['port'],
'location': gw['location'],
}
logging.error(f"Gateway '{config['server']}' not found in gateway list. Please check with --list")
sys.exit(1)

protocol = config.get('protocol')
port = config.get('port')
filter = Gateway(host=config.get('server'),
protocols=[protocol] if protocol else None,
ports=[int(port)] if port else None,
location=config.get('location'),
ip_address=None)

gateways = filter_gateways(j['gateways'], filter)
if not gateways:
logging.error("Can't find any gateway matching critera. Please check with --list.")
sys.exit(1)

# pick a gateway that match filters
gw = random.choice(gateways)

# pick one protocol and port
protocol = random.choice(gw.protocols)
port = random.choice(gw.ports)

return {
'hostname': gw.host,
'ip_address': gw.ip_address,
'proto': protocol,
'port': port,
'location': gw.location,
'extra_config': config.get('extra_config', "")
}


def generate_configuration() -> None:
Expand Down Expand Up @@ -303,7 +379,8 @@ def check_file_exists(file: Path) -> None:

ca {{ ca_cert_file }}
cert {{ cert_file }}
key {{ key_file }}"""
key {{ key_file }}
{{ server_info['extra_config'] }}"""

server_info = get_server_info()
excluded_routes = get_excluded_routes()
Expand Down
5 changes: 5 additions & 0 deletions riseup_vpn_configurator/riseup-vpn.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,8 @@ excluded_routes:
# os user/group
user: openvpn
group: openvpn

# configuration verbatim to append to openvpn configuraion
extra_config: |
# disable logs
verb: 0