diff options
| author | repliqa <sarzilhossain@proton.me> | 2025-07-23 14:06:15 +0600 |
|---|---|---|
| committer | repliqa <sarzilhossain@proton.me> | 2025-07-23 14:06:15 +0600 |
| commit | 69acb7a82a68eeb439e55b994281056df52c81b1 (patch) | |
| tree | 7c6a53694e11511a3014470c213255a503f9c95e /library | |
Diffstat (limited to 'library')
| -rw-r--r-- | library/README.md | 33 | ||||
| -rw-r--r-- | library/format_output.py | 39 | ||||
| -rw-r--r-- | library/hysteria.py | 66 | ||||
| -rw-r--r-- | library/ocserv.py | 80 | ||||
| -rw-r--r-- | library/sshvpn.py | 76 | ||||
| -rw-r--r-- | library/user_expiration.py | 48 | ||||
| -rw-r--r-- | library/xray.py | 104 |
7 files changed, 446 insertions, 0 deletions
diff --git a/library/README.md b/library/README.md new file mode 100644 index 00000000..43a1d318 --- /dev/null +++ b/library/README.md @@ -0,0 +1,33 @@ +# Custom Modules +## Table of Contents + - [Description](#description) + - [Protocols](#protocols) + - [ocserv.py](#ocserv.py) + - [xray.py](#xray.py) + - [sshvpn.py](#sshvpn.py) + - [hysteria.py](#hysteria.py) + +## Description +Custom modules for user management for different protcols. Each module takes a list of users as input, writes to configuration or password file, returns a list of usernames and passwords that are printed at the end of playbook run. + + ## Protocols +### xray.py +Description: User management module for xray (vless, vmess, trojan) +Input Parameters: +- users - all_users + vless_users/vmess_users/trojan_users +- protocol - vless/vmess/trojan + +### ocserv.py +Description: User management module for ocserv +Input Parameters: +- users - all_users + ocserv_users + +### hysteria.py +Description: User management module for hysteria +Input Parameters: +- users - all_users + hysteria_users + +### sshvpn.py +Description: User management module for sshvpn +Input Parameters: +- users - all_users + sshvpn_users diff --git a/library/format_output.py b/library/format_output.py new file mode 100644 index 00000000..6a50e7ce --- /dev/null +++ b/library/format_output.py @@ -0,0 +1,39 @@ +#!/usr/local/bin/python3 + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +from ansible.module_utils.basic import AnsibleModule +import json, shlex, os +from datetime import datetime + +EXPIRE_USER_JSON_PATH = "/var/reactance/.user_expiration.json" + +def run_module(): + changed = False + module = AnsibleModule( + argument_spec=dict( + users = dict(type='list', required=True) + ), + supports_check_mode=True + ) + + user_pass_list = module.params["users"] + msg = """ +######################### +#### CHANGED USERS #### + """ + for protocol in user_pass_list: + msg += f"## {protocol.key}" + proto_user_pass_dict = protocol.values() + for user in proto_user_pass_dict.keys(): + msg += f"# {user}: {proto_user_pass_dict[user]}" + + msg += "#########################" + module.exit_json(changed=changed, msg=msg) + +def main(): + run_module() + +if __name__ == "__main__": + main() diff --git a/library/hysteria.py b/library/hysteria.py new file mode 100644 index 00000000..99f67871 --- /dev/null +++ b/library/hysteria.py @@ -0,0 +1,66 @@ +#!/usr/local/bin/python3 + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +from ansible.module_utils.basic import AnsibleModule +import json, shlex, os +from datetime import datetime + +HYSTERIA_CONFIG_FILE = "/var/reactance/hysteria/etc/config.json" +SALAMANDER_PASSWD_FILE = "/var/reactance/hysteria/salamander_password" + +def exec_shell(cmd, module): + rc, stdout, stderr= module.run_command(cmd, environ_update={'TERM': 'dumb'}) + if rc != 0: + module.fail_json(stderr) + return stdout.rstrip() + +def hysteria_get_users(): + with open(HYSTERIA_CONFIG_FILE, "r") as f: + hysteria_config_dict = json.loads(f.read()) + previous_users = hysteria_config_dict["auth"]["userpass"] + return previous_users, hysteria_config_dict + +def hysteria_user_control(update_password, module): + previous_users, hysteria_config_dict = hysteria_get_users() + selected_users = set(update_password.keys()) + user_pass_dict = {} + new_users_dict = {} + for user in selected_users: + if user in previous_users and not update_password[user]: + user_pass_dict[user] = {"hysteria": previous_users[user]} + else: + user_pass_dict[user] = {"hysteria": exec_shell("openssl rand -hex 32", module)} + new_users_dict[user] = user_pass_dict[user] + + with open(HYSTERIA_CONFIG_FILE, "w") as f: + hysteria_config_dict["auth"]["userpass"] = user_pass_dict + f.write(json.dumps(hysteria_config_dict, indent=1)) + + return new_users_dict + +def run_module(): + module = AnsibleModule( + argument_spec=dict( + users = dict(type='list', required=True) + ), + supports_check_mode=True + ) + users = module.params["users"] + update_password = {} + + for user in users: + if 'regen' in user.keys() and user['regen']: + update_password[user['user']] = True + else: + update_password[user['user']] = False + + user_pass_dict = hysteria_user_control(update_password, module) + module.exit_json(changed=True, msg=user_pass_dict) + +def main(): + run_module() + +if __name__ == "__main__": + main() diff --git a/library/ocserv.py b/library/ocserv.py new file mode 100644 index 00000000..5ff51adf --- /dev/null +++ b/library/ocserv.py @@ -0,0 +1,80 @@ +#!/usr/local/bin/python3 + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +from ansible.module_utils.basic import AnsibleModule +import json, shlex, os +from datetime import datetime + + +OCSERV_CERTS_DIR = "/var/reactance/ocserv/certs" + +def exec_shell(cmd, module): + rc, stdout, stderr= module.run_command(cmd, environ_update={'TERM': 'dumb'}, use_unsafe_shell=True) + if rc != 0: + module.fail_json(stderr) + return stdout.rstrip() + +def ocserv_get_users(): + previous_users = [".".join(i.split('.')[:-1]) for i in os.listdir(OCSERV_CERTS_DIR) if i.endswith(".p12")] + return previous_users + +def ocserv_user_control(update_password, module): + previous_users = ocserv_get_users() + selected_users = set(update_password.keys()) + new_users_dict = {} + + # Remove users not in group_vars + for user in previous_users: + if user not in selected_users or not update_password[user]: + # new code goes here - remove user, update crl + exec_shell(f"cat {OCSERV_CERTS_DIR}/{user}-cert.pem > {OCSERV_CERTS_DIR}/revoked.pem", module) + exec_shell(f"certtool --generate-crl --load-ca-privkey {OCSERV_CERTS_DIR}/ca-key.pem --load-ca-certificate {OCSERV_CERTS_DIR}/ca-cert.pem --load-certificate {OCSERV_CERTS_DIR}/revoked.pem --template {OCSERV_CERTS_DIR}/crl.tmpl --outfile {OCSERV_CERTS_DIR}/crl.pem", module) + exec_shell(f"rm {OCSERV_CERTS_DIR}/{user}-cert.pem {OCSERV_CERTS_DIR}/{user}-key.pem {OCSERV_CERTS_DIR}/{user}.p12", module) + + # Add new users or update password of existing users + for user in selected_users: + if user not in previous_users or update_password[user]: + # new code goes here - generate template, certs + user_template_contents = f""" +dn = "cn={user},UID={user}" +expiration_days = -1 +signing_key +tls_www_client + """ + user_template_file = os.path.join(OCSERV_CERTS_DIR, f"{user}.tmpl") + with open(user_template_file, "w") as f: + f.write(user_template_contents) + exec_shell(f"certtool --generate-privkey --outfile {OCSERV_CERTS_DIR}/{user}-key.pem", module) + exec_shell(f"certtool --generate-certificate --load-privkey {OCSERV_CERTS_DIR}/{user}-key.pem --load-ca-certificate {OCSERV_CERTS_DIR}/ca-cert.pem --load-ca-privkey {OCSERV_CERTS_DIR}/ca-key.pem --template {OCSERV_CERTS_DIR}/{user}.tmpl --outfile {OCSERV_CERTS_DIR}/{user}-cert.pem", module) + exec_shell(f"certtool --to-p12 --load-privkey {OCSERV_CERTS_DIR}/{user}-key.pem --pkcs-cipher 3des-pkcs12 --load-certificate {OCSERV_CERTS_DIR}/{user}-cert.pem --outfile {OCSERV_CERTS_DIR}/{user}.p12 --password {user} --p12-name {user} --outder", module) + exec_shell(f"rm {user_template_file}", module) + new_users_dict[user] = {"ocserv": []} # a hack + + return new_users_dict + +def run_module(): + module = AnsibleModule( + argument_spec=dict( + users = dict(type='list', required=True) + ), + supports_check_mode=True + ) + users = module.params["users"] + update_password = {} + + for user in users: + if 'regen' in user.keys() and user['regen']: + update_password[user['user']] = True + else: + update_password[user['user']] = False + + new_users_dict = ocserv_user_control(update_password, module) + module.exit_json(changed=True, msg=new_users_dict) + +def main(): + run_module() + +if __name__ == "__main__": + main() diff --git a/library/sshvpn.py b/library/sshvpn.py new file mode 100644 index 00000000..42c1e60d --- /dev/null +++ b/library/sshvpn.py @@ -0,0 +1,76 @@ +#!/usr/local/bin/python3 + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +from ansible.module_utils.basic import AnsibleModule +import json, shlex, os +from datetime import datetime + +SSH_ROOT = "/var/reactance/sshvpn/.ssh" +AUTHORIZED_KEYS = os.path.join(SSH_ROOT, "authorized_keys") + +def exec_shell(cmd, module): + # use_unsafe_shell=True so ansible doesn't remove | + rc, stdout, stderr= module.run_command(cmd, environ_update={'TERM': 'dumb'}, use_unsafe_shell=True) + if rc != 0: + module.fail_json(stderr) + return stdout.rstrip() + +def sshvpn_get_users(): + previous_users = [".".join(i.split('.')[:-1]) for i in os.listdir(SSH_ROOT) if i.endswith(".pub")] + return previous_users + +def sshvpn_update_users(update_password, module): + previous_users = sshvpn_get_users() + new_users_dict = {} + + # Remove users not in new group_vars + for user in previous_users: + if user not in update_password.keys(): + exec_shell(f"rm {SSH_ROOT}/{user} {SSH_ROOT}/{user}.pub", module) + + # Update keys for new users or regenerate keys for old users + for user in update_password.keys(): + if user not in previous_users or update_password[user]: + exec_shell(f"yes | ssh-keygen -q -t ed25519 -C {user} -N \'\' -f \'{SSH_ROOT}/{user}\'", module) + with open(f"{SSH_ROOT}/{user}", "r") as privkey: + new_users_dict[user] = {"sshvpn": privkey.read()} + + # Overwrite existing authorized_keys file + users_pubkeys = [i for i in os.listdir(SSH_ROOT) if i.endswith(".pub")] + with open(AUTHORIZED_KEYS, "w") as f: + for user_pubkey in users_pubkeys: + user_pubkey_file = os.path.join(SSH_ROOT, user_pubkey) + with open(user_pubkey_file, "r") as pkey: + f.write(pkey.read()) + + # kill running sessions + exec_shell(f"pkill -u sshvpn &>/dev/null", module) + + return new_users_dict + +def run_module(): + module = AnsibleModule( + argument_spec=dict( + users = dict(type='list', required=True) + ), + supports_check_mode=True + ) + users = module.params["users"] + update_password = {} + + for user in users: + if 'regen' in user.keys() and user['regen']: + update_password[user['user']] = True + else: + update_password[user['user']] = False + + new_users_dict = sshvpn_update_users(update_password, module) + module.exit_json(changed=True, msg=new_users_dict) + +def main(): + run_module() + +if __name__ == "__main__": + main() diff --git a/library/user_expiration.py b/library/user_expiration.py new file mode 100644 index 00000000..2bf88e8f --- /dev/null +++ b/library/user_expiration.py @@ -0,0 +1,48 @@ +#!/usr/local/bin/python3 + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +from ansible.module_utils.basic import AnsibleModule +import json, shlex, os +from datetime import datetime + +EXPIRE_USER_JSON_PATH = "/var/reactance/.user_expiration.json" + +def run_module(): + changed = False + module = AnsibleModule( + argument_spec=dict( + users = dict(type='list', required=True) + ), + supports_check_mode=True + ) + + users = module.params["users"] + + user_expire_dict = {} + if os.path.exists(EXPIRE_USER_JSON_PATH): + with open(EXPIRE_USER_JSON_PATH, 'r') as f: + user_expire_dict = json.loads(f.read()) + + for user in users: + if 'expire' in user.keys(): + changed = True + time = str(datetime(*[int(i) for i in user['expire'].split('-')]).timestamp()) + if time not in user_expire_dict.keys(): + user_expire_entry = set() # To make sure we don't have duplicates + else: + user_expire_entry = set(user_expire_dict[time]) + user_expire_entry.add(user['user']) + user_expire_dict[time] = list(user_expire_entry) # JSON can't work with sets + + with open(EXPIRE_USER_JSON_PATH, 'w') as f: + f.write(json.dumps(user_expire_dict, indent=1)) + + module.exit_json(changed=changed) + +def main(): + run_module() + +if __name__ == "__main__": + main() diff --git a/library/xray.py b/library/xray.py new file mode 100644 index 00000000..ff2d0357 --- /dev/null +++ b/library/xray.py @@ -0,0 +1,104 @@ +#!/usr/local/bin/python3 + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +from ansible.module_utils.basic import AnsibleModule +import json, shlex, os +from datetime import datetime + +XRAY_CONFIG_PATH = "/var/reactance/xray/etc/config.json" +VISION_PUBKEY_FILE = "/var/reactance/xray/public_key" + +def exec_shell(cmd, module): + rc, stdout, stderr = module.run_command(cmd, environ_update={'TERM': 'dumb'}) + if rc != 0: + module.fail_json(stderr) + return stdout.rstrip() +def xray_gen_password(protocol, module): + login_method = {'vmess': 'id', 'vless': 'id', 'trojan': 'password'}[protocol] + return login_method, exec_shell({'trojan': 'openssl rand -hex 32', 'vless': '/var/reactance/xray/bin/xray uuid', 'vmess': '/var/reactance/xray/bin/xray uuid'}[protocol], module) + +def xray_get_users(protocol): + with open(XRAY_CONFIG_PATH, "r") as f: + xray_config_dict = json.loads(f.read()) + inbounds = xray_config_dict["inbounds"] + protos_users = {} + for inbound in inbounds: + if inbound['protocol'] == protocol: + protos_users[inbound['protocol']] = [j['email'] for j in inbound['settings']['clients']] + return protos_users, xray_config_dict + +def xray_user_control(update_password, protocol, address, service_port, public_key, module): + previous_users, xray_config_dict = xray_get_users(protocol) + user_pass_list = [] + all_users_dict = {} + new_users_dict = {} + + # search through all inbound protocools + for i, inbound in enumerate(xray_config_dict['inbounds']): + if inbound['protocol'] == protocol: + previous_users_dict = inbound['settings']['clients'] + previous_users_list = [i['email'] for i in previous_users_dict] + selected_users = set(update_password.keys()) + + # keep old passwords + for user in previous_users_dict: + if user['email'] in selected_users and not update_password[user['email']]: + user_pass_list.append(user) + all_users_dict[user['email']] = user[{'vmess': 'id', 'vless': 'id', 'trojan': 'password'}[protocol]] + + # generate new passwords + for user in selected_users: + if user not in all_users_dict.keys(): + login_method, xray_password = xray_gen_password(protocol, module) + new_user = { 'email': user, login_method: xray_password } + xray_url = f"{protocol}://{ xray_password }@{ address }:{ service_port }?security=reality&sni=behindthename.com&fp=chrome&pbk={ public_key }" + all_users_dict[user] = {protocol: xray_password} + if protocol in ["vless", "vmess"]: + new_user["flow"] = "xtls-rprx-vision" + xray_url += "&flow=xtls-rprx-vision" + xray_url += f"#{protocol}_{user}" + user_pass_list.append(new_user) + new_users_dict[user] = {protocol: xray_url} + + xray_config_dict['inbounds'][i]['settings']['clients'] = user_pass_list + + with open(XRAY_CONFIG_PATH, "w") as f: + f.write(json.dumps(xray_config_dict, indent=1)) + + return new_users_dict + +def run_module(): + module = AnsibleModule( + argument_spec=dict( + users = dict(type='list', required=True), + protocol = dict(type='str', required=True), + address = dict(type='str', required=True), + service_port = dict(type='int', required=True), + public_key = dict(type='str', required=True) + ), + supports_check_mode=True + ) + + users = module.params["users"] + protocol = module.params["protocol"] + address = module.params["address"] + service_port = module.params["service_port"] + public_key = module.params["public_key"] + update_password = {} + + for user in users: + if 'regen' in user.keys() and user['regen']: + update_password[user['user']] = True + else: + update_password[user['user']] = False + + new_users_dict = xray_user_control(update_password, protocol, address, service_port, public_key, module) + module.exit_json(changed=True, msg=new_users_dict) + +def main(): + run_module() + +if __name__ == "__main__": + main() |
