def do_install(self, service_opts: UnixServiceOpts, auto_startup: bool = False, auto_connman_dhcp: bool = False): FileHelper.mkdirs(self.opts.vpn_dir.parent) FileHelper.unpack_archive(ClientOpts.get_resource(ClientOpts.VPN_ZIP), self.opts.vpn_dir) FileHelper.mkdirs([self.opts.vpn_dir, self.opts.runtime_dir]) FileHelper.chmod(self.opts.runtime_dir, mode=0o0755) FileHelper.chmod([ os.path.join(self.opts.vpn_dir, p) for p in ('vpnclient', 'vpncmd') ], mode=0o0755) _, cmd = EnvHelper.build_executable_command() svc_opts = self._standard_service_opt(service_opts) self.device.unix_service.create( svc_opts, { '{{WORKING_DIR}}': f'{self.opts.vpn_dir}', '{{VPN_DESC}}': svc_opts.service_name, '{{START_CMD}}': f'{cmd} start --vpn-dir {self.opts.vpn_dir}', '{{STOP_CMD}}': f'{cmd} stop --vpn-dir {self.opts.vpn_dir}' }, auto_startup) self._dump_cache_service(svc_opts) self.device.ip_resolver.add_hook(svc_opts.service_name, { '{{WORKING_DIR}}': f'{self.opts.vpn_dir}', '{{VPN_CLIENT_CLI}}': cmd }) self.device.dns_resolver.create_config(svc_opts.service_name, auto_connman_dhcp) self.storage.empty() self.opts.export_env()
def __dns(vpn_opts: ClientOpts, nic: str, reason: str, new_nameservers: str, old_nameservers: str, debug: bool): logger.info(f'Discover DNS with {reason}::{nic}...') _reason = DHCPReason[reason] if not vpn_opts.is_vpn_nic(nic): logger.warn(f'NIC[{nic}] does not belong to VPN service') sys.exit(0) executor = VPNClientExecutor( vpn_opts, adhoc_task=True).require_install().probe(silent=True, log_lvl=logger.INFO) current = executor.storage.get_current(info=True) if not current: current = executor.storage.find(executor.opts.nic_to_account(nic)) if not current: logger.warn(f'Not found any VPN account') sys.exit(ErrorCode.VPN_ACCOUNT_NOT_FOUND) if executor.opts.nic_to_account(nic) != current.account: logger.warn(f'NIC[{nic}] does not meet current VPN account') sys.exit(ErrorCode.VPN_ACCOUNT_NOT_MATCH) if debug: now = datetime.now().isoformat() FileHelper.write_file( FileHelper.tmp_dir().joinpath('vpn_dns'), append=True, content= f"{now}::{reason}::{nic}::{new_nameservers}::{old_nameservers}\n") executor.device.dns_resolver.resolve(executor.vpn_service, _reason, current.hub, new_nameservers, old_nameservers)
def show(vpn_opts: VpnOpts, version: str, sha: str, show_brand=False, show_license=False, is_json=False): if show_brand and not is_json: brand = FileHelper.read_file_by_line( vpn_opts.get_resource('banner.txt')) brand and print(brand) ver = { "vpn_version": vpn_opts.get_vpn_version(Versions.VPN_VERSION), "cli_version": version, "hash_version": sha } if is_json: print(JsonHelper.to_json(ver)) else: logger.info(f'VPN version : {ver.get("vpn_version")}') logger.info(f'CLI version : {ver.get("cli_version")}') logger.info(f'Hash version: {ver.get("hash_version")}') logger.sep(logger.INFO, 58) if show_license: print( FileHelper.read_file_by_line( vpn_opts.get_resource('LICENSE_BUNDLE.md'), fallback_if_not_exists=''))
def _check_pid(pid_file: str, log_lvl=logger.TRACE) -> int: try: logger.log(log_lvl, f'Read PID file {pid_file}') pid = FileHelper.read_file_by_line(pid_file) pid = int(pid) if pid and pid > 0 and SystemHelper.is_pid_exists(pid): return pid except Exception as _: FileHelper.rm(pid_file) return 0
def remove(self, svc_opts: UnixServiceOpts, force: bool = False): service_fqn = self.to_service_fqn(svc_opts.service_dir, svc_opts.service_name) self.stop(svc_opts.service_name) self.disable(svc_opts.service_name) if force and FileHelper.is_exists(service_fqn): logger.info(f'Remove System service [{svc_opts.service_name}]...') FileHelper.rm(service_fqn) SystemHelper.exec_command("systemctl daemon-reload", silent=True, log_lvl=logger.INFO)
def gen_intermediate_cert(cert_key, private_key, prefix, items, output_opts: OutputOpts, cert_attributes: CertAttributes): """ Generate an Intermediate Signed certificate """ outputs = {} ca_crt, ca_pkey = __load_key(cert_key, private_key) for item in items: outputs[item] = __gen_cert(f'{prefix}.{item}', cert_attributes, ca_crt, ca_pkey) FileHelper.write_file(output_opts.make_file(f"{item}.key"), outputs[item]['private_key']) FileHelper.write_file(output_opts.make_file(f"{item}.crt"), outputs[item]['cert_key']) JsonHelper.dump(output_opts.make_file(f"signed-intermediate-{output_opts.file}.json"), outputs)
def restore_config(self, vpn_service: str, keep_dnsmasq=True): if not keep_dnsmasq: logger.debug( f'Remove dnsmasq vpn hook config [{self._dnsmasq_vpn_hook_cfg}]' ) FileHelper.rm(self._dnsmasq_vpn_hook_cfg) logger.debug( f'Remove dnsmasq vpn config [{self._dnsmasq_vpn_cfg(vpn_service)}]' ) FileHelper.rm(self._dnsmasq_vpn_cfg(vpn_service)) if self._resolver: self._resolver.restore_config(vpn_service, keep_dnsmasq)
def update_hook(self, reason: DHCPReason, priv_root_dns: str, nameservers: list, vpn_nameserver_hook_conf: Path): logger.info( f'Update VPN DNS config file on [{reason.name}][{priv_root_dns}] with nameservers {nameservers}...' ) servers = '\n'.join( [f'server=/{priv_root_dns}/{ns}' for ns in nameservers]) FileHelper.write_file( vpn_nameserver_hook_conf, mode=0o644, content= f'### Generated at [{datetime.now().isoformat()}]\n{servers}\n')
def cleanup_config(self, vpn_service: str, keep_dnsmasq=True): if self.is_connman(): return resolver = self._resolver() if keep_dnsmasq: resolver.reset_hook(self.vpn_hook_cfg) elif FileHelper.is_readable(self.origin_resolv_cfg): logger.info(f'Restore System DNS config file...') FileHelper.backup(self.origin_resolv_cfg, DNSResolver.DNS_SYSTEM_FILE) FileHelper.rm(self.vpn_resolv_cfg) resolver.restore_config(vpn_service, keep_dnsmasq) resolver.restart(_all=not keep_dnsmasq, keep_dnsmasq=keep_dnsmasq)
def change_host_name(hostname: str, log_lvl=logger.DEBUG): prev_regex = re.escape('127.0.1.1') + r'\s+' + re.escape( socket.gethostname()) + r'.*' SystemHelper.exec_command(f'hostnamectl set-hostname {hostname}', log_lvl=logger.down_lvl(log_lvl)) FileHelper.replace_in_file('/etc/hosts', {prev_regex: f'127.0.1.1 {hostname}'}, regex=True) SystemHelper.exec_command(f'hostnamectl', silent=True, log_lvl=log_lvl) logger.sep(level=log_lvl, quantity=20) SystemHelper.exec_command(f'cat /etc/hosts', silent=True, log_lvl=log_lvl) logger.sep(level=log_lvl)
def adapt_dnsmasq(self, origin_resolv_conf: Path, vpn_service: str) -> Optional[Path]: content = FileHelper.read_file_by_line(self.config.main_cfg) resolv = TextHelper.awk(next( iter(TextHelper.grep(content, r'dnsmasq_resolv=.+')), None), sep='=', pos=1) return Path(resolv or self.config.runtime_resolv)
def do_uninstall(self, keep_vpn: bool = True, keep_dnsmasq: bool = True, service_opts: UnixServiceOpts = None, log_lvl: int = logger.INFO): vpn_service = self._standard_service_opt(service_opts).service_name logger.info(f'Uninstall VPN service [{vpn_service}]...') self.do_delete([a.account for a in self.storage.list()], force_stop=True, log_lvl=log_lvl) if not keep_vpn: logger.log(log_lvl, f'Remove VPN Client [{self.opts.vpn_dir}]...') self.device.ip_resolver.remove_hook(vpn_service) self.opts.remove_env() FileHelper.rm(self.opts.vpn_dir) self.device.dns_resolver.cleanup_config(vpn_service, keep_dnsmasq=keep_dnsmasq)
def gen_ssh(users, output_opts: OutputOpts): """ Generate SSH key """ output = {} crypto_backend = crypto_default_backend() for user in users: ssh_key = rsa.generate_private_key(backend=crypto_backend, public_exponent=65537, key_size=4096) private_ssh_key = __serialize_private_key(ssh_key) public_ssh_key = ssh_key.public_key() \ .public_bytes(crypto_serialization.Encoding.OpenSSH, crypto_serialization.PublicFormat.OpenSSH) \ .decode(DEFAULT_ENCODING) output[user] = {'private_ssh_key': private_ssh_key, 'public_ssh_key': public_ssh_key} FileHelper.write_file(output_opts.make_file(user + "_ssh"), private_ssh_key) FileHelper.write_file(output_opts.make_file(user + "_ssh.pub"), public_ssh_key) JsonHelper.dump(output_opts.to_fqn_file(".json"), output)
def gen_root_cert(output_opts: OutputOpts, cert_attributes: CertAttributes): """ Generate Root Certification """ crypto_backend = crypto_default_backend() algorithm = hashes.SHA512() now = datetime.datetime.utcnow() private_key = rsa.generate_private_key(public_exponent=65537, key_size=4096, backend=crypto_backend) subject = issuer = cert_attributes.create_x509_attributes() crt = x509.CertificateBuilder() \ .subject_name(subject).issuer_name(issuer).public_key(private_key.public_key()) \ .serial_number(x509.random_serial_number()).not_valid_before(now) \ .not_valid_after(now + datetime.timedelta(days=cert_attributes.valid_days)) \ .sign(private_key, algorithm, crypto_backend) root_private_key = __serialize_private_key(private_key) root_cert_key = crt.public_bytes(encoding=crypto_serialization.Encoding.PEM).decode(DEFAULT_ENCODING) output = {'private_key': root_private_key, 'cert_key': root_cert_key, 'serial_number': f'{crt.serial_number:0>40X}'} FileHelper.write_file(output_opts.to_fqn_file("key"), root_private_key) FileHelper.write_file(output_opts.to_fqn_file("crt"), root_cert_key) JsonHelper.dump(output_opts.to_fqn_file("json"), output)
def __validate_nameservers(self, reason: DHCPReason, new_ns: str = None, old_ns: str = None) -> Optional[list]: if reason.is_ignore(): return None if reason is DHCPReason.RENEW and new_ns == old_ns and FileHelper.is_readable( self.vpn_hook_cfg): return None nameservers = old_ns if reason.is_unreachable() else new_ns return [ns for ns in nameservers.split(',') if ns][0:2] if nameservers else None
def gen_signed_cert(cert_key, private_key, intermediate_code: str, fn: str, items: list, dump_to_file: bool, seq: bool, quantity: int, start_from: int, prefix: str, length: int, output_opts: OutputOpts, cert_attributes: CertAttributes): """ Generate Signed certificate """ if not seq and not items: raise RuntimeError('Must provide singed certification name') if seq: if quantity <= 0 or start_from <= 0 or length <= 0: raise RuntimeError('Invalid value in sequence mode. All of [quantity, start_from, length] must be > 0') seq_format = f'0{length}d' items = [f'{prefix}{x:{seq_format}}' for x in range(start_from, start_from + quantity)] outputs = {} ca_crt, ca_pkey = __load_key(cert_key, private_key) for item in items: outputs[item] = __gen_cert(f'{item}.{fn}.{intermediate_code}', cert_attributes, ca_crt, ca_pkey) if dump_to_file: FileHelper.write_file(output_opts.make_file(f"{item}.key"), outputs[item]['private_key']) FileHelper.write_file(output_opts.make_file(f"{item}.crt"), outputs[item]['cert_key']) JsonHelper.dump(output_opts.make_file(f"{intermediate_code}-{output_opts.file}.json"), outputs)
def __import(server_opts: ServerOpts, hub_password: str, vpn_opts: ToolOpts, group: str, certs_file: str, output_opts: OutputOpts): executor = VPNAuthExecutor(vpn_opts, server_opts, hub_password) data = JsonHelper.read(certs_file, strict=False) tmp_dir = FileHelper.tmp_dir('vpn_auth') command_file = FileHelper.touch(tmp_dir.joinpath('vpncmd.txt')) vpn_acc = {} for k, v in data.items(): cert_file = tmp_dir.joinpath(f'{k}.cert') FileHelper.write_file(cert_file, v['cert_key']) commands = [ f'CAAdd /{cert_file}', f'UserCreate {k} /GROUP:{group or "none"} /RealName:none /Note:none', f'UserSignedSet {k} /CN:{v["fqdn"]} /SERIAL:{v["serial_number"]}' ] vpn_acc[k] = { 'vpn_server': server_opts.host, 'vpn_port': server_opts.port, 'vpn_hub': server_opts.hub, 'vpn_account': server_opts.hub, 'vpn_auth_type': 'cert', 'vpn_user': k, 'vpn_cert_key': v['cert_key'], 'vpn_private_key': v['private_key'], } FileHelper.write_file(command_file, '\n'.join(commands) + '\n', append=True) executor.exec_command(f'/IN:{command_file}', log_lvl=logger.INFO) logger.sep(logger.INFO) out = output_opts.make_file( f'{server_opts.hub}-{output_opts.to_file("json")}') logger.info(f'Export VPN accounts to {out}...') JsonHelper.dump(out, vpn_acc) logger.done()
def reset_hook(self, vpn_nameserver_hook_conf: Path): logger.info(f'Reset VPN DNS config file...') if FileHelper.is_writable(vpn_nameserver_hook_conf): FileHelper.write_file(vpn_nameserver_hook_conf, mode=0o644, content='') FileHelper.create_symlink(vpn_nameserver_hook_conf, self._dnsmasq_vpn_hook_cfg, force=True) else: FileHelper.rm(self._dnsmasq_vpn_hook_cfg)
def create_config(self, vpn_acc: str, replacements: dict): config_file = self._to_config_file(vpn_acc) logger.log(self.log_lvl, f'Create DHCP client VPN config[{config_file}]...') FileHelper.copy(self.resource_dir.joinpath(self.DHCLIENT_CONFIG_TMPL), config_file, force=True) FileHelper.replace_in_file(config_file, replacements, backup='') FileHelper.chmod(config_file, mode=0o0644)
def create_config(self, vpn_service: str, auto_connman_dhcp: bool): if self.is_connman(): FileHelper.write_file(self.connman_dhcp, str(auto_connman_dhcp)) return if not FileHelper.is_readable(self.origin_resolv_cfg): logger.info( f'Backup System DNS config file to [{self.origin_resolv_cfg}]...' ) FileHelper.backup(DNSResolver.DNS_SYSTEM_FILE, self.origin_resolv_cfg, remove=False) if not FileHelper.is_readable(self.origin_resolv_cfg): logger.error( f'Not found origin DNS config file [{self.origin_resolv_cfg}]') sys.exit(ErrorCode.FILE_CORRUPTED) if not FileHelper.is_readable(self.vpn_hook_cfg): FileHelper.touch(self.vpn_hook_cfg, 0o0644) self._resolver().setup(vpn_service, self.origin_resolv_cfg, self.vpn_resolv_cfg, self.vpn_hook_cfg) self._resolver().restart(_all=True)
def add_hook(self, service_name: str, replacements: dict): exit_hook_file = self._to_hook_file(service_name) logger.log(self.log_lvl, f'Create DHCP client VPN hook[{exit_hook_file}]...') FileHelper.copy(self.resource_dir.joinpath( self.DHCLIENT_EXIT_HOOK_TMPL), exit_hook_file, force=True) FileHelper.replace_in_file(exit_hook_file, replacements, backup='') FileHelper.chmod(exit_hook_file, mode=0o0744)
def restore_config(self, backup_dir: Path, keep_backup: bool): logger.info( f'Restore VPN configuration [{backup_dir}] to [{self.opts.vpn_dir}]...' ) FileHelper.copy(backup_dir.joinpath(self.opts.VPN_CONFIG_FILE), self.opts.config_file, force=True) FileHelper.copy(backup_dir.joinpath(self.opts.RUNTIME_FOLDER), self.opts.runtime_dir, force=True) FileHelper.rm(backup_dir, force=not keep_backup)
def _common_adapt_dnsmasq(self, vpn_service: str): identity = self.config.identity logger.debug( f'Adapt [{identity}] DNS resolver service to compatible with [dnsmasq] and [{vpn_service}]...' ) FileHelper.mkdirs(self.config.config_dir) FileHelper.copy(self.resource_dir.joinpath(f'dnsmasq-{identity}.conf'), self.config.to_fqn_cfg(self.DNSMASQ_TUNED_CFG), True) FileHelper.chmod(self.config.to_fqn_cfg(self.DNSMASQ_TUNED_CFG), mode=0o0644) return self.config.runtime_resolv
def backup_config(self): backup_dir = self.opts.backup_dir() logger.info( f'Backup VPN configuration [{self.opts.vpn_dir}] to [{backup_dir}] ...' ) FileHelper.mkdirs(backup_dir) FileHelper.copy(self.opts.config_file, backup_dir, force=True) FileHelper.copy(self.opts.runtime_dir, backup_dir.joinpath(self.opts.RUNTIME_FOLDER), force=True) default_acc = self.storage.get_default() current_acc = self.storage.get_current() svc_opt = self._standard_service_opt() return default_acc, current_acc, svc_opt, backup_dir
def create(self, svc_opts: UnixServiceOpts, replacements: dict, auto_startup: bool = False): service_fqn = self.to_service_fqn(svc_opts.service_dir, svc_opts.service_name) logger.info( f'Add new service [{svc_opts.service_name}] in [{service_fqn}]...') FileHelper.copy(self.resource_dir.joinpath(Systemd.SERVICE_FILE_TMPL), service_fqn, force=True) FileHelper.replace_in_file(service_fqn, replacements, backup='') FileHelper.chmod(service_fqn, mode=0o0644) SystemHelper.exec_command("systemctl daemon-reload", silent=True, log_lvl=logger.INFO) if auto_startup: self.enable(svc_opts.service_name)
def setup(self, vpn_service: str, origin_resolv_conf: Path, vpn_resolv_conf: Path, vpn_nameserver_hook_conf: Path): if not self._available: logger.error('[dnsmasq] is not yet installed or is corrupted') sys.exit(ErrorCode.MISSING_REQUIREMENT) logger.info('Setup DNS resolver[dnsmasq]...') dnsmasq_vpn_cfg = self._dnsmasq_vpn_cfg(vpn_service) runtime_resolv_cfg = self.adapt_dnsmasq(origin_resolv_conf, vpn_service) dnsmasq_opts = { '{{DNS_RESOLVED_FILE}}': self.__build_dnsmasq_conf('resolv-file', runtime_resolv_cfg), '{{PORT}}': self.__build_dnsmasq_conf('port', self.dnsmasq_options().get('port', None)), '{{CACHE_SIZE}}': self.__build_dnsmasq_conf( 'cache-size', self.dnsmasq_options().get('cache_size', None)) } logger.debug( f'Add [dnsmasq] config for {vpn_service}[{dnsmasq_vpn_cfg}]...') FileHelper.copy(self.resource_dir.joinpath(self.DNSMASQ_CONFIG_TMPL), dnsmasq_vpn_cfg, force=True) FileHelper.replace_in_file(dnsmasq_vpn_cfg, dnsmasq_opts, backup='') FileHelper.chmod(dnsmasq_vpn_cfg, mode=0o0644) logger.debug( f'Symlink [dnsmasq] VPN nameserver runtime configuration [{vpn_nameserver_hook_conf}]...' ) FileHelper.create_symlink(vpn_nameserver_hook_conf, self._dnsmasq_vpn_hook_cfg, force=True) logger.info(f'Generate System DNS config file from VPN service...') FileHelper.write_file(vpn_resolv_conf, self.__dnsmasq_resolv(vpn_service), mode=0o0644) FileHelper.create_symlink(vpn_resolv_conf, DNSResolver.DNS_SYSTEM_FILE, force=True) self.service.enable(self.config.identity)
def remove_hook(self, service_name: str): exit_hook_file = self._to_hook_file(service_name) logger.log(self.log_lvl, f'Remove DHCP client VPN hook[{exit_hook_file}]...') FileHelper.rm(exit_hook_file, force=True)
def _common_remove_dnsmasq(self, vpn_service: str, keep_dnsmasq: bool): if not keep_dnsmasq: cfg = self.config.to_fqn_cfg(self.DNSMASQ_TUNED_CFG) logger.debug( f'Remove [dnsmasq] and [{vpn_service}] plugin[{cfg}]...') FileHelper.rm(cfg)
def is_enable_connman_dhcp(self) -> bool: yes_ = ('true', 't', 'yes', '1') return self.is_connman() and FileHelper.read_file_by_line( self.connman_dhcp, fallback_if_not_exists='0').lower() in yes_
def adapt_dnsmasq(self, origin_resolv_conf: Path, vpn_service: str) -> Optional[Path]: return FileHelper.get_target_link(origin_resolv_conf) or self.config.runtime_resolv if \ FileHelper.is_readable(self.config.runtime_resolv) else origin_resolv_conf