#!/usr/bin/python3 # -*- mode: python -*- # # This file is part of Plinth. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . # """ Configuration helper for OpenVPN server. """ import argparse import os import subprocess from plinth import action_utils KEYS_DIRECTORY = '/etc/openvpn/freedombox-keys' DH_KEY = '/etc/openvpn/freedombox-keys/dh4096.pem' SERVER_CONFIGURATION_PATH = '/etc/openvpn/freedombox.conf' CA_CERTIFICATE_PATH = KEYS_DIRECTORY + '/ca.crt' USER_CERTIFICATE_PATH = KEYS_DIRECTORY + '/{username}.crt' USER_KEY_PATH = KEYS_DIRECTORY + '/{username}.key' SERVER_CONFIGURATION = ''' port 1194 proto udp dev tun ca /etc/openvpn/freedombox-keys/ca.crt cert /etc/openvpn/freedombox-keys/server.crt key /etc/openvpn/freedombox-keys/server.key dh /etc/openvpn/freedombox-keys/dh4096.pem server 10.91.0.0 255.255.255.0 keepalive 10 120 cipher AES-256-CBC comp-lzo verb 3 ''' CLIENT_CONFIGURATION = ''' client remote {remote} 1194 proto udp dev tun nobind remote-cert-tls server cipher AES-256-CBC comp-lzo redirect-gateway verb 3 {ca} {cert} {key}''' CERTIFICATE_CONFIGURATION = { 'KEY_CONFIG': '/usr/share/easy-rsa/openssl-1.0.0.cnf', 'KEY_DIR': KEYS_DIRECTORY, 'OPENSSL': 'openssl', 'KEY_SIZE': '4096', 'CA_EXPIRE': '3650', 'KEY_EXPIRE': '3650', 'KEY_COUNTRY': 'US', 'KEY_PROVINCE': 'NY', 'KEY_CITY': 'New York', 'KEY_ORG': 'FreedomBox', 'KEY_EMAIL': 'me@freedombox', 'KEY_OU': 'Home', 'KEY_NAME': 'FreedomBox' } COMMON_ARGS = {'env': CERTIFICATE_CONFIGURATION, 'cwd': KEYS_DIRECTORY} def parse_arguments(): """Return parsed command line arguments as dictionary.""" parser = argparse.ArgumentParser() subparsers = parser.add_subparsers(dest='subcommand', help='Sub command') subparsers.add_parser('is-setup', help='Return whether setup is completed') subparsers.add_parser('setup', help='Setup OpenVPN server configuration') get_profile = subparsers.add_parser( 'get-profile', help='Return the OpenVPN profile of a user') get_profile.add_argument('username', help='User to get profile for') get_profile.add_argument('remote_server', help='The server name for the user to connect') return parser.parse_args() def subcommand_is_setup(_): """Return whether setup is complete.""" print('true' if os.path.isfile(DH_KEY) else 'false') def subcommand_setup(_): """Setup configuration, CA and certificates.""" _create_server_config() _create_certificates() _setup_firewall() action_utils.service_enable('openvpn@freedombox') action_utils.service_restart('openvpn@freedombox') def _create_server_config(): """Write server configuration.""" if os.path.exists(SERVER_CONFIGURATION_PATH): return with open(SERVER_CONFIGURATION_PATH, 'w') as file_handle: file_handle.write(SERVER_CONFIGURATION) def _setup_firewall(): """Add TUN device to internal zone in firewalld.""" subprocess.call(['firewall-cmd', '--zone', 'internal', '--add-interface', 'tun+']) subprocess.call(['firewall-cmd', '--permanent', '--zone', 'internal', '--add-interface', 'tun+']) def _create_certificates(): """Generate CA and server certificates.""" try: os.mkdir(KEYS_DIRECTORY, 0o700) except FileExistsError: pass subprocess.check_call(['/usr/share/easy-rsa/clean-all'], **COMMON_ARGS) subprocess.check_call(['/usr/share/easy-rsa/pkitool', '--initca'], **COMMON_ARGS) subprocess.check_call(['/usr/share/easy-rsa/pkitool', '--server', 'server'], **COMMON_ARGS) subprocess.check_call(['/usr/share/easy-rsa/build-dh'], **COMMON_ARGS) def subcommand_get_profile(arguments): """Return the profile for a user.""" username = arguments.username remote_server = arguments.remote_server if username == 'ca' or username == 'server': raise Exception('Invalid username') user_certificate = USER_CERTIFICATE_PATH.format(username=username) user_key = USER_KEY_PATH.format(username=username) if not os.path.isfile(user_certificate) or not os.path.isfile(user_key): subprocess.check_call(['/usr/share/easy-rsa/pkitool', username], **COMMON_ARGS) user_certificate_string = _read_file(user_certificate) user_key_string = _read_file(user_key) ca_string = _read_file(CA_CERTIFICATE_PATH) profile = CLIENT_CONFIGURATION.format( ca=ca_string, cert=user_certificate_string, key=user_key_string, remote=remote_server) print(profile) def _read_file(filename): """Return the entire contens of a file as string.""" with open(filename, 'r') as file_handle: return ''.join(file_handle.readlines()) def main(): """Parse arguments and perform all duties.""" arguments = parse_arguments() subcommand = arguments.subcommand.replace('-', '_') subcommand_method = globals()['subcommand_' + subcommand] subcommand_method(arguments) if __name__ == '__main__': main()