You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

vpngen-cli.py 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447
  1. #! /usr/bin/env python3
  2. from __future__ import print_function
  3. import argparse
  4. import json
  5. import os
  6. import sys
  7. import glob
  8. import json
  9. import os
  10. import os.path
  11. import re
  12. import shutil
  13. from enum import Enum
  14. from subprocess import call
  15. class VpnGenError(Enum):
  16. Success = 0,
  17. VpnAlreadyExists = 1,
  18. VpnDoesNotExists = 2,
  19. ClientAlreadyExists = 3,
  20. ClientDoesNotExists = 4
  21. class VpnGen:
  22. default_config_base_dir = ""
  23. default_config_file = ""
  24. default_client_config_file = ""
  25. ovpn_config_path = ""
  26. def __init__(self, default_config_path, ovpn_config_path):
  27. self.default_config_base_dir = os.path.abspath(default_config_path)
  28. self.default_config_file = "%s.conf" % self.default_config_base_dir
  29. self.default_client_config_file = "%s%sclients%sclient.conf" % (self.default_config_base_dir, os.sep, os.sep)
  30. self.ovpn_config_path = os.path.abspath(ovpn_config_path)
  31. def f7(self, seq):
  32. seen = set()
  33. seen_add = seen.add
  34. return [x for x in seq if not (x in seen or seen_add(x))]
  35. def get_vpn_vars(self):
  36. with open(self.default_config_file, "r") as f:
  37. default_config = f.read()
  38. variables = re.findall('\$\{([^}]+)}', default_config)
  39. variables += ["KEY_COUNTRY", "KEY_PROVINCE", "KEY_CITY", "KEY_ORG", "KEY_EMAIL"]
  40. variables = self.f7(variables)
  41. return variables
  42. def get_client_vars(self, vpn_name):
  43. default_client_config_path = self.get_client_default_config_path(vpn_name)
  44. if not os.path.exists(default_client_config_path):
  45. return None
  46. with open(default_client_config_path, "r") as f:
  47. default_config = f.read()
  48. variables = re.findall('\$\{([^}]+)}', default_config)
  49. variables = self.f7(variables)
  50. vpn_variables = self.get_vpn_vars()
  51. real_variables = []
  52. for var in variables:
  53. if var not in vpn_variables and var != "client":
  54. real_variables.append(var)
  55. return real_variables
  56. def get_base_dir(self, vpn_name):
  57. return "%s%s%s%s" % (self.ovpn_config_path, os.sep, vpn_name, os.sep)
  58. def get_config_path(self, vpn_name):
  59. return "%s%s%s.conf" % (self.ovpn_config_path, os.sep, vpn_name)
  60. def get_vpn_variables_path(self, vpn_name):
  61. base_dir = self.get_base_dir(vpn_name)
  62. return "%svpngen.json" % base_dir
  63. def get_easy_rsa_dir(self, vpn_name):
  64. base_dir = self.get_base_dir(vpn_name)
  65. return "%seasy-rsa%s" % (base_dir, os.sep)
  66. def get_easy_rsa_key_dir(self, vpn_name):
  67. easyrsadir = self.get_easy_rsa_dir(vpn_name)
  68. return "%skeys%s" % (easyrsadir, os.sep)
  69. def get_pkitool_path(self, vpn_name):
  70. easyrsadir = self.get_easy_rsa_dir(vpn_name)
  71. return "%spkitool" % easyrsadir
  72. def get_client_default_config_path(self, vpn_name):
  73. base_dir = self.get_base_dir(vpn_name)
  74. return "%s%sclients%sclient.conf" % (base_dir, os.sep, os.sep)
  75. def get_client_dir(self, vpn_name, client_name):
  76. base_dir = self.get_base_dir(vpn_name)
  77. return "%sclients%s%s-%s%s" % (base_dir, os.sep, client_name, vpn_name, os.sep)
  78. def get_client_config_path(self, vpn_name, client_name):
  79. client_dir = self.get_client_dir(vpn_name, client_name)
  80. return "%s%s-%s.conf" % (client_dir, client_name, vpn_name)
  81. def get_client_variables_path(self, vpn_name, client_name):
  82. client_dir = self.get_client_dir(vpn_name, client_name)
  83. return "%svpngen.json" % client_dir
  84. def get_client_generated_files_paths(self, vpn_name, client_name):
  85. keys_dir = self.get_easy_rsa_key_dir(vpn_name,)
  86. return [
  87. "%s%s.crt" % (keys_dir, client_name),
  88. "%s%s.key" % (keys_dir, client_name)
  89. ]
  90. def get_client_tarball_path(self, vpn_name, client_name):
  91. base_dir = self.get_base_dir(vpn_name)
  92. return "%sclients%s%s-%s.tar.bz2" % (base_dir, os.sep, client_name, vpn_name)
  93. def get_server_needed_files_paths(self, vpn_name):
  94. keys_dir = self.get_easy_rsa_key_dir(vpn_name)
  95. return [
  96. "%sca.crt" % keys_dir,
  97. "%sta.key" % keys_dir
  98. ]
  99. def get_client_misc_files_paths(self, vpn_name):
  100. base_dir = self.get_base_dir(vpn_name)
  101. return glob.glob("%smisc-files%s*" % (base_dir, os.sep))
  102. def get_all_needed_files_paths(self, vpn_name, client_name):
  103. return self.get_client_generated_files_paths(vpn_name, client_name) +\
  104. self.get_server_needed_files_paths(vpn_name) +\
  105. self.get_client_misc_files_paths(vpn_name)
  106. def get_server_variables(self, vpn_name):
  107. with open(self.get_vpn_variables_path(vpn_name), "r") as f:
  108. return json.load(f)['variables']
  109. def get_client_variables(self, vpn_name, client_name):
  110. with open(self.get_client_variables_path(vpn_name, client_name), "r") as f:
  111. return json.load(f)['variables']
  112. def get_client_list(self, vpn_name):
  113. base_dir = self.get_base_dir(vpn_name)
  114. files_paths = glob.glob("%sclients%s*" % (base_dir, os.sep))
  115. files_names = list(map(lambda file_path: os.path.basename(file_path), files_paths))
  116. clients = []
  117. for file_name in files_names:
  118. if file_name != 'client.conf' and not file_name.endswith(".tar.bz2"):
  119. clients.append(file_name[0:len(file_name) - len(vpn_name) - 1])
  120. return clients
  121. def setup_vars(self, vpn_name, variables):
  122. os.environ["KEY_COUNTRY"] = variables['KEY_COUNTRY']
  123. os.environ["KEY_PROVINCE"] = variables['KEY_PROVINCE']
  124. os.environ["KEY_CITY"] = variables['KEY_CITY']
  125. os.environ["KEY_ORG"] = variables['KEY_ORG']
  126. os.environ["KEY_OU"] = variables['KEY_ORG']
  127. os.environ["KEY_CN"] = variables['KEY_ORG']
  128. os.environ["KEY_NAME"] = variables['KEY_ORG']
  129. os.environ["KEY_EMAIL"] = variables['KEY_EMAIL']
  130. os.environ["KEY_SIZE"] = variables['KEY_SIZE']
  131. os.environ["CA_EXPIRE"] = variables['CA_EXPIRE']
  132. os.environ["KEY_EXPIRE"] = variables['KEY_EXPIRE']
  133. self.setup_vars_openssl(vpn_name)
  134. def setup_vars_openssl(self, vpn_name):
  135. easyrsadir = self.get_easy_rsa_dir(vpn_name)
  136. os.environ["EASY_RSA"] = easyrsadir
  137. os.environ["OPENSSL"] = "openssl"
  138. os.environ["PKCS11TOOL"] = "pkcs11-tool"
  139. os.environ["GREP"] = "grep"
  140. os.environ["KEY_CONFIG"] = "%s%s" % (easyrsadir, "openssl.cnf")
  141. os.environ["KEY_DIR"] = "%s%s" % (easyrsadir, "keys")
  142. os.environ["PKCS11_MODULE_PATH"] = "dummy"
  143. os.environ["PKCS11_PIN"] = "dummy"
  144. def create_vpn(self, vpn_name, variables):
  145. base_dir = self.get_base_dir(vpn_name)
  146. conf_file = self.get_config_path(vpn_name)
  147. conf_vpngen_file = self.get_vpn_variables_path(vpn_name)
  148. if os.path.exists(base_dir) or os.path.exists(conf_file):
  149. return VpnGenError.VpnAlreadyExists
  150. with open(self.default_config_file, "r") as f:
  151. default_config = f.read()
  152. variables['name'] = vpn_name
  153. for variable in variables:
  154. default_config = default_config.replace("${%s}" % variable, variables[variable])
  155. os.makedirs(base_dir)
  156. with open(conf_file, "w") as f:
  157. f.write(default_config)
  158. os.rmdir(base_dir)
  159. shutil.copytree(self.default_config_base_dir, base_dir)
  160. curdir = os.curdir
  161. easyrsadir = self.get_easy_rsa_dir(vpn_name)
  162. pkitool = self.get_pkitool_path(vpn_name)
  163. os.chdir(easyrsadir)
  164. self.setup_vars(vpn_name, variables)
  165. call([".%sclean-all" % os.sep])
  166. call([pkitool, "--initca", "-batch"])
  167. call([pkitool, "--server", "server", "-batch"])
  168. call([".%sbuild-dh" % os.sep])
  169. call(["openssl", "ca", "-gencrl",
  170. "-keyfile", "keys%sca.key" % os.sep,
  171. "-cert", "keys%sca.crt" % os.sep,
  172. "-out", "keys%scrl.pem" % os.sep,
  173. "-config", "openssl.cnf"])
  174. del os.environ["KEY_OU"]
  175. del os.environ["KEY_CN"]
  176. del os.environ["KEY_NAME"]
  177. call(["openvpn", "--genkey", "--secret", "keys%sta.key" % os.sep])
  178. with open(conf_vpngen_file, "w") as f:
  179. json.dump({'variables': variables}, f, indent=4, separators=(',', ': '))
  180. os.chdir(curdir)
  181. return VpnGenError.Success
  182. def remove_vpn(self, vpn_name):
  183. base_dir = self.get_base_dir(vpn_name)
  184. conf_file = self.get_config_path(vpn_name)
  185. if not os.path.exists(base_dir) and not os.path.exists(conf_file):
  186. return VpnGenError.VpnDoesNotExists
  187. os.remove(conf_file)
  188. shutil.rmtree(base_dir)
  189. return VpnGenError.Success
  190. def create_client(self, vpn_name, client_name, variables):
  191. base_dir = self.get_base_dir(vpn_name)
  192. if not os.path.exists(base_dir):
  193. return VpnGenError.VpnDoesNotExists
  194. client_dir = self.get_client_dir(vpn_name, client_name)
  195. if os.path.exists(client_dir):
  196. return VpnGenError.ClientAlreadyExists
  197. curdir = os.curdir
  198. easyrsadir = self.get_easy_rsa_dir(vpn_name)
  199. pkitool = self.get_pkitool_path(vpn_name)
  200. os.chdir(easyrsadir)
  201. self.setup_vars(vpn_name, variables)
  202. os.environ["KEY_CN"] = client_name
  203. os.environ["KEY_NAME"] = client_name
  204. call([pkitool, client_name])
  205. os.chdir(curdir)
  206. os.makedirs(client_dir)
  207. return self.rebuild_client(vpn_name, client_name, variables)
  208. def remove_client(self, vpn_name, client_name):
  209. base_dir = self.get_base_dir(vpn_name)
  210. if not os.path.exists(base_dir):
  211. return VpnGenError.VpnDoesNotExists
  212. client_dir = self.get_client_dir(vpn_name, client_name)
  213. if not os.path.exists(client_dir):
  214. return VpnGenError.ClientDoesNotExists
  215. self.setup_vars_openssl(vpn_name)
  216. curdir = os.curdir
  217. easyrsadir = self.get_easy_rsa_dir(vpn_name)
  218. os.chdir(easyrsadir)
  219. call(["./revoke-full", client_name])
  220. os.chdir(curdir)
  221. return VpnGenError.Success
  222. def rebuild_client(self, vpn_name, client_name, variables):
  223. variables["client"] = client_name
  224. client_dir = self.get_client_dir(vpn_name, client_name)
  225. client_conf_file = self.get_client_config_path(vpn_name, client_name)
  226. client_default_config_path = self.get_client_default_config_path(vpn_name)
  227. with open(client_default_config_path, "r") as f:
  228. client_default_config = f.read()
  229. for variable in variables:
  230. client_default_config = client_default_config.replace("${%s}" % variable, variables[variable])
  231. files_names = glob.glob("%s%s*" % (client_dir, os.sep))
  232. for file_name in files_names:
  233. os.remove(file_name)
  234. with open(client_conf_file, "w") as f:
  235. f.write(client_default_config)
  236. files_paths = self.get_all_needed_files_paths(vpn_name, client_name)
  237. for file_path in files_paths:
  238. dest = "%s%s-%s-%s" % (client_dir, client_name, vpn_name, os.path.basename(file_path))
  239. shutil.copy(file_path, dest)
  240. split = os.path.splitext(client_conf_file)
  241. client_ovpn_file = "%s.ovpn" % split[0]
  242. shutil.copy(client_conf_file, client_ovpn_file)
  243. files_names = glob.glob("%s%s*" % (client_dir, os.sep))
  244. files_names = list(map(lambda file_path: os.path.basename(file_path), files_names))
  245. call(["tar", "cfj", self.get_client_tarball_path(vpn_name, client_name),
  246. "-C", client_dir] + files_names)
  247. client_variables = {}
  248. for variable in self.get_client_vars(vpn_name):
  249. client_variables[variable] = variables[variable]
  250. with open(self.get_client_variables_path(vpn_name, client_name), "w") as f:
  251. json.dump({'variables': client_variables}, f, indent=4, separators=(',', ': '))
  252. return VpnGenError.Success
  253. def eprint(*args, **kwargs):
  254. print(*args, file=sys.stderr, **kwargs)
  255. def create_variables(variables, defaults):
  256. variables_set = defaults.copy()
  257. for variable in variables:
  258. if variable == 'name' or variable == 'client':
  259. continue
  260. default = variables_set[variable] if variable in variables_set else ''
  261. print("Enter a value for '%s' [%s]: " % (variable, default), end='', flush=True)
  262. value = sys.stdin.readline()[:-1]
  263. if value != '' or variable not in variables_set:
  264. variables_set[variable] = value
  265. return variables_set
  266. def main():
  267. parser = argparse.ArgumentParser(description='Manage OpenVPN VPNs')
  268. parser.add_argument('--vpn', help='The VPN to use', required=True)
  269. parser.add_argument('--config', dest='config', default='/etc/vpngen/vpngen.json', help='Configuration file path')
  270. parser.add_argument('--create', help='Create a VPN', action='store_true')
  271. parser.add_argument('--remove', help='Remove a VPN', action='store_true')
  272. parser.add_argument('--create-client', help='Create a client for the VPN', metavar='CLIENT')
  273. parser.add_argument('--remove-client', help='Remove a client for the VPN', metavar='CLIENT')
  274. parser.add_argument('--rebuild-client', help='Rebuild a client configuration', metavar='CLIENT')
  275. parser.add_argument('--rebuild-clients', help='Rebuild clients configurations', action='store_true')
  276. args = parser.parse_args()
  277. with open(args.config, "r") as f:
  278. config = json.load(f)
  279. vpn_name = config['vpnPrefix'] + args.vpn + config['vpnSuffix']
  280. if args.create_client is not None:
  281. client_name = args.create_client
  282. elif args.remove_client is not None:
  283. client_name = args.remove_client
  284. elif args.rebuild_client is not None:
  285. client_name = args.rebuild_client
  286. else:
  287. client_name = None
  288. if client_name is not None:
  289. client_name = config['clientPrefix'] + client_name + config['clientSuffix']
  290. vpng = VpnGen(config['defaultConfigPath'], config['ovpnConfigPath'])
  291. config_path = vpng.get_vpn_variables_path(vpn_name)
  292. if os.path.exists(config_path):
  293. with open(config_path, "r") as f:
  294. data = json.load(f)
  295. config['defaults'].update(data['variables'])
  296. if args.create:
  297. default_variables = config['defaults'].copy()
  298. variables = create_variables(vpng.get_vpn_vars(), default_variables)
  299. res = vpng.create_vpn(vpn_name, variables)
  300. if res == VpnGenError.Success:
  301. print("VPN %s created successfully" % vpn_name)
  302. else:
  303. eprint("Failed to create VPN %s: %s" % (vpn_name, res))
  304. exit(1)
  305. elif args.remove:
  306. res = vpng.remove_vpn(vpn_name)
  307. if res == VpnGenError.Success:
  308. print("VPN %s removed successfully" % vpn_name)
  309. else:
  310. eprint("Failed to remove VPN %s: %s" % (vpn_name, res))
  311. exit(1)
  312. elif args.create_client:
  313. default_variables = config['defaults'].copy()
  314. default_variables.update(vpng.get_server_variables(vpn_name))
  315. variables = create_variables(vpng.get_client_vars(vpn_name), default_variables)
  316. if variables is None:
  317. res = VpnGenError.VpnDoesNotExists
  318. else:
  319. res = vpng.create_client(vpn_name, client_name, variables)
  320. if res == VpnGenError.Success:
  321. print("Client %s created successfully on VPN %s" % (client_name, vpn_name))
  322. else:
  323. eprint("Failed to create client %s on VPN %s: %s" % (client_name, vpn_name, res))
  324. exit(1)
  325. elif args.remove_client:
  326. res = vpng.remove_client(vpn_name, client_name)
  327. if res == VpnGenError.Success:
  328. print("Client %s removed successfully on VPN %s" % (client_name, vpn_name))
  329. else:
  330. eprint("Failed to remove client %s on VPN %s: %s" % (client_name, vpn_name, res))
  331. exit(1)
  332. elif args.rebuild_client:
  333. default_variables = config['defaults'].copy()
  334. default_variables.update(vpng.get_server_variables(vpn_name))
  335. default_variables.update(vpng.get_client_variables(vpn_name, client_name))
  336. res = vpng.rebuild_client(vpn_name, client_name, default_variables)
  337. if res == VpnGenError.Success:
  338. print("Client %s configuration rebuilt successfully on VPN %s" % (client_name, vpn_name))
  339. else:
  340. eprint("Failed to rebuild client %s configuration on VPN %s: %s" % (client_name, vpn_name, res))
  341. exit(1)
  342. elif args.rebuild_clients:
  343. default_variables = config['defaults'].copy()
  344. default_variables.update(vpng.get_server_variables(vpn_name))
  345. for client_name in vpng.get_client_list(vpn_name):
  346. variables = default_variables.copy()
  347. variables.update(vpng.get_client_variables(vpn_name, client_name))
  348. res = vpng.rebuild_client(vpn_name, client_name, variables)
  349. if res == VpnGenError.Success:
  350. print("Client %s configuration rebuilt successfully on VPN %s" % (client_name, vpn_name))
  351. else:
  352. eprint("Failed to rebuild client %s configuration on VPN %s: %s" % (client_name, vpn_name, res))
  353. exit(1)
  354. main()