create_new_wireguard_account 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. #!/usr/bin/env python3
  2. import configparser
  3. import datetime
  4. import os
  5. import smtplib
  6. import subprocess
  7. import sys
  8. import tempfile
  9. import textwrap
  10. from email.mime.multipart import MIMEMultipart
  11. from email.mime.application import MIMEApplication
  12. from email.mime.text import MIMEText
  13. class Email:
  14. """
  15. Not really necessary, but I keep on forgetting most of an email arguments
  16. and I'm tired of debugging this class of error... Too bad we don't have any
  17. record type in this language.
  18. PS: I hate python.
  19. """
  20. def __init__(self, username, passwd, from_addr, to_addr, server, port):
  21. self.username = username
  22. self.passwd = passwd
  23. self.from_addr = from_addr
  24. self.to_addr = to_addr
  25. self.server = server
  26. self.port = port
  27. class Config:
  28. """
  29. Same as Email
  30. """
  31. def __init__(self, config):
  32. self.smtp_user = config['smtp']['username']
  33. self.smtp_pass = config['smtp']['password']
  34. self.smtp_server = config['smtp']['server']
  35. self.smtp_port = int(config['smtp']['port'])
  36. self.smtp_from = config['smtp']['from']
  37. self.wg_service = config['wireguard-service']['name']
  38. self.pubkey = config['wireguard-service']['pubkey']
  39. self.endpoint = config['wireguard-service']['endpoint']
  40. def _run_cmd(cmd):
  41. print("> $ %s" % (cmd))
  42. subprocess.run(cmd, shell=True, check=True)
  43. def check_env ():
  44. """
  45. Checks if wireguard is correctly installed, the script is run as
  46. root and the member id is correct.
  47. """
  48. wgInstalled = os.system("wg")
  49. if os.geteuid() != 0:
  50. print("On a besoin des droits root pour créer un accès VPN.")
  51. print("Relancez la commande préfixée d'un sudo.")
  52. raise Exception("Got root?")
  53. if wgInstalled != 0:
  54. print("Wireguard ne semble pas être installé sur votre système.")
  55. print("Il faudrait installer wireguard-tools et wireguard-dkms avant"
  56. " d'utiliser ce script")
  57. raise Exception("Install wg")
  58. member_email = input("EMail du nouveau membre: ")
  59. member_id = int(input("Numéro d'adhérant du nouveau membre: "))
  60. if member_id < 2 or member_id > 253:
  61. print("On est parti du principe que les IDs des membres commencent à 1.")
  62. print("Ah, aussi, pour le moment, on est aussi partis du principe que ça "
  63. "s'arrête à 253.")
  64. print("Si on a plus de 253 membres, premièrement, FÉLICITATIONS venant "
  65. "du Félix du passé. Par contre, il faut repenser l'adressage des "
  66. "IPs du VPN maintenant :( Ranson du succès j'immagine.")
  67. raise Exception("Wrong member_id")
  68. return (member_email, member_id)
  69. def gen_wg_keys (temp_dir):
  70. """
  71. Generates both the private and the public wireguard key of the new member.
  72. """
  73. pubkey_path = os.path.join(temp_dir, "pub.key")
  74. privkey_path = os.path.join(temp_dir, "priv.key")
  75. psk_path = os.path.join(temp_dir, "psk.key")
  76. _run_cmd("wg genkey | tee %s | wg pubkey > %s" % (privkey_path, pubkey_path))
  77. _run_cmd("wg genpsk > %s" % (psk_path))
  78. return (privkey_path, pubkey_path, psk_path)
  79. def is_duplicate_entry_wg_conf (member_id, config_file):
  80. """
  81. Look for a potential wireguard duplicate entry.
  82. Note: wg config format is not really a init file because of the multiple
  83. init entries. Hence, we cannot use a proper parser to to the check. We'll
  84. only try to pattern match the ip addr.
  85. """
  86. with open(config_file, "r") as wg_conf_file:
  87. return ("10.0.0.{}".format(member_id) in wg_conf_file.read())
  88. def update_wg_config (member_id, config_file, pubkey_path, psk_path):
  89. """
  90. Generate the wireguard peer entry for this new member.
  91. """
  92. wg_new_peer = textwrap.dedent('''
  93. [Peer]
  94. PublicKey = pubksubs
  95. PresharedKey = psksubs
  96. AllowedIPs = 10.0.0.{0}/32, fd00::{0}/128
  97. ''').format(member_id)
  98. with open(config_file, "a") as wg_conf_file:
  99. wg_conf_file.write(wg_new_peer)
  100. _run_cmd('sed -i "s|pubksubs|"$(cat %s)"|g" %s' % (pubkey_path, config_file))
  101. _run_cmd('sed -i "s|psksubs|"$(cat %s)"|g" %s' % (psk_path, config_file))
  102. def generate_wg_quick_client_config(peer_priv_key, member_id,
  103. server_pub_key, psk, server_endpoint):
  104. return textwrap.dedent('''\
  105. [Interface]
  106. PrivateKey = {0}
  107. Address = 10.0.0.{1}/24, fd00::{1}/64
  108. SaveConfig = false
  109. DNS = 80.67.169.12, 80.67.169.40, 2001:910:800::12, 2001:910:800::40
  110. [Peer]
  111. PublicKey = {2}
  112. PresharedKey = {3}
  113. AllowedIPs = 0.0.0.0/0, ::/0
  114. Endpoint = {4}
  115. ''').format(peer_priv_key.strip(), member_id, server_pub_key, psk.strip(),\
  116. server_endpoint)
  117. def send_email(email, wg_client_config):
  118. """
  119. Send the private key by email.
  120. email:
  121. - username
  122. - passwd
  123. - from_addr
  124. - to_addr
  125. - server
  126. """
  127. from_addr = 'bureau@baionet.fr'
  128. password = email.passwd
  129. msg = MIMEMultipart()
  130. msg['Subject'] = "Votre acces VPN Baionet"
  131. msg['Date'] = str(datetime.datetime.now())
  132. msg['From'] = email.from_addr
  133. msg['To'] = email.to_addr
  134. body = textwrap.dedent('''
  135. blahblah, cf le wiki blahblahblah
  136. ''')
  137. part = MIMEApplication(
  138. wg_client_config.encode("utf-8"),
  139. Name="wg0.conf")
  140. part['Content-Disposition'] = 'attachment; filename=wg0.conf'
  141. msg.attach(part)
  142. username = email.username
  143. server = smtplib.SMTP(email.server, email.port)
  144. server.ehlo()
  145. server.starttls()
  146. server.login(email.username, email.passwd)
  147. server.sendmail(email.from_addr, email.to_addr, msg.as_string())
  148. server.quit()
  149. print("[+] E-Mail envoyé à %s." % email.to_addr)
  150. # Main Function
  151. # =============
  152. # *************
  153. # =============
  154. # 1- Parse email/member id.
  155. # 2- Génère les clés wireguard.
  156. # 3- Crée/déploie la configuration wireguard.
  157. # 5- Envoie la clé à l'utilisateur (email/manuellement)
  158. if __name__ == '__main__':
  159. cp = configparser.ConfigParser()
  160. config_file = "/etc/wireguard/wg0.conf"
  161. cp.read('/etc/wireguard/wg-create-account.ini')
  162. config = Config(cp)
  163. (member_email, member_id) = check_env()
  164. with tempfile.TemporaryDirectory() as temp_dir:
  165. try:
  166. print("[+] Génération des clés wireguard")
  167. (privkey_path, pubkey_path, psk_path) = gen_wg_keys(temp_dir)
  168. print("[+] Envoi de la clé privée au nouveau membre")
  169. print("Deux solutions ici:")
  170. print("1- On envoie la configuration du nouveau membre en ligne. (automatique)")
  171. print("2- On utilise une autre méthode pour passer la configuration au nouveau membre. (manuel)")
  172. print("")
  173. print("Suivant votre modèle de menace, envoyer la clé privée par e-mail peut ou peut ne pas être une bonne idée.")
  174. use_email = input("Envoyer la configuration (contenant la clé privée) par email? (O/n)")
  175. if use_email.strip().lower() != "n" :
  176. with open(privkey_path, "r") as pkh:
  177. peer_privkey = pkh.read()
  178. with open(psk_path, "r") as pskh:
  179. peer_psk = pskh.read()
  180. email = Email(config.smtp_user, config.smtp_pass, config.smtp_from,\
  181. member_email, config.smtp_server, config.smtp_port)
  182. send_email(email, generate_wg_quick_client_config(peer_privkey, member_id, config.pubkey,\
  183. peer_psk, config.endpoint))
  184. else:
  185. print("Mode utilisateur avancé")
  186. print("=======================")
  187. print("À vous de vous débrouiller pour passer les clés/config à l'utilisateur")
  188. print("Clé privée: %s" % (privkey_path))
  189. print("Clé pré-partagée (psk): %s" % (psk_path))
  190. print("Clé publique (psk): %s" % (pubkey_path))
  191. input("Appuyez sur entrée pour continuer (les clés privées seront détruites): ")
  192. except Exception as e:
  193. print("ERREUR: erreur lors de la génération/transfert de la clé:")
  194. print(e)
  195. print("Si vous ne comprenez pas le problème, transférez le message d'erreur complet à la liste "
  196. "de diffusion technique.")
  197. print("Pas de panique: on n'a pas touché à la configuration du serveur de wireguard, "
  198. "rien n'a cassé.")
  199. sys.exit(1)
  200. try:
  201. print("[+] Modification de la configuration wireguard")
  202. if not is_duplicate_entry_wg_conf(member_id, config_file):
  203. update_wg_config(member_id, config_file, pubkey_path, psk_path)
  204. else:
  205. print("Le membre {} semble déja avoir un compte VPN.".format(member_id))
  206. print("Veuillez contacter la liste de diffusion technique "
  207. " si son compte necessite une ré-activation.")
  208. sys.exit(1)
  209. print("[+] Chargement de la nouvelle interface réseau")
  210. _run_cmd("systemctl restart %s" % (config.wg_service))
  211. print("[+] Nettoyage des clés")
  212. _run_cmd("shred -u %s %s %s" % (privkey_path, pubkey_path, psk_path))
  213. print("[+] COMPTE CRÉE AVEC SUCCÈS")
  214. except Exception as e:
  215. print("ERREUR CRITIQUE: attention, la configuration wireguard est cassée, il y a probablement urgence.")
  216. print(e)
  217. print("Si vous ne savez pas quoi faire pour régler le problème, contactez en urgence la "
  218. "liste de diffusion technique en joignant le message d'erreur ci-dessus.")
  219. print("Je le répète: le serveur VPN est probablement cassé, c'est une urgence.")
  220. sys.exit(1)