summaryrefslogtreecommitdiff
path: root/library
diff options
context:
space:
mode:
Diffstat (limited to 'library')
-rw-r--r--library/README.md33
-rw-r--r--library/format_output.py39
-rw-r--r--library/hysteria.py66
-rw-r--r--library/ocserv.py80
-rw-r--r--library/sshvpn.py76
-rw-r--r--library/user_expiration.py48
-rw-r--r--library/xray.py104
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()