Skip to content

Commit

Permalink
Add gateway randomization
Browse files Browse the repository at this point in the history
  • Loading branch information
dirtyhillbilly committed Jan 18, 2025
1 parent f1f7e1c commit 39fe2a2
Show file tree
Hide file tree
Showing 2 changed files with 103 additions and 17 deletions.
12 changes: 12 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 @@ -75,8 +83,12 @@ 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
108 changes: 91 additions & 17 deletions riseup_vpn_configurator/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
import logging
import os
import pwd
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
Expand Down Expand Up @@ -222,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 @@ -245,25 +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

if filter.location and gw.location != filter.location:
# gateway is not in the wanted location
continue

candidates.append(gw)

def get_server_info() -> Optional[dict]:
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'],
'extra_config': config.get('extra_config', "")
}
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

0 comments on commit 39fe2a2

Please sign in to comment.