1 import json

2 import os

3 import platform

4 import stat

5 import subprocess

6 import tempfile

7 from datetime import datetime, timedelta, timezone

8 from pathlib import Path

9

10 import httpx

11 import structlog

12

13 logger = structlog.get_logger(__name__)

14

15

16 def get_nebula_install_path():

17 os_name = platform.system()

18

19 if os_name == "Darwin":

20 base_path = Path(os.path.expanduser("~/Library/Application Support/nebula"))

21 elif os_name == "Linux":

22 base_path = Path("/opt/nebula")

23 else:

24 raise NotImplementedError(f"Unsupported operating system: {os_name}")

25

26 if not base_path.exists():

27 base_path.mkdir(parents=True, exist_ok=True)

28

29 return base_path

30

31

32 def download_nebula_binaries(mesh_admin_endpoint):

33 os_name = platform.system()

34 if os_name not in ["Linux", "Darwin"]:

35 raise NotImplementedError(

36 f"Unsupported operating system: {os_name}. Only Linux and Darwin are supported."

37 )

38

39 architecture = platform.machine()

40

41 binaries = ["nebula", "nebula-cert"]

42 install_path = get_nebula_install_path()

43

44 for binary in binaries:

45 binary_path = install_path / binary

46

47 if binary_path.exists():

48 logger.info(f"{binary} binary already exists", path=str(binary_path))

49 continue

50

51 url = f"{mesh_admin_endpoint}/api/v1/nebula/download/{os_name}/{architecture}/{binary}"

52 logger.info(f"Downloading {binary}", url=url)

53

54 try:

55 with httpx.stream("GET", url) as response:

56 response.raise_for_status()

57 with open(binary_path, "wb") as f:

58 for chunk in response.iter_bytes():

59 f.write(chunk)

60

61 # Make the binary executable

62 binary_path.chmod(binary_path.stat().st_mode | stat.S_IEXEC)

63 logger.info(f"{binary} downloaded and installed", path=str(binary_path))

64

65 except httpx.HTTPStatusError as e:

66 logger.error(

67 f"Failed to download {binary}", status_code=e.response.status_code

68 )

69 raise RuntimeError(f"Failed to download {binary}: {e}")

70

71 return install_path

72

73

74 def get_nebula_path():

75 return get_nebula_binary_path()

76

77

78 def get_nebula_binary_path():

79 install_path = get_nebula_install_path()

80 return install_path / "nebula"

81

82

83 def get_nebula_cert_binary_path():

84 install_path = get_nebula_install_path()

85 return install_path / "nebula-cert"

86

87

88 def create_keys():

89 with tempfile.TemporaryDirectory() as tmpdir:

90 res = subprocess.run(

91 [

92 get_nebula_cert_binary_path(),

93 "keygen",

94 "-out-key",

95 "private.key",

96 "-out-pub",

97 "public.key",

98 ],

99 capture_output=True,

100 text=True,

101 cwd=tmpdir,

102 )

103 if res.returncode != 0:

104 raise RuntimeError(f"Failed to create keys: {res.stderr}")

105

106 private_key = (Path(tmpdir) / "private.key").read_text()

107 public_key = (Path(tmpdir) / "public.key").read_text()

108

109 return private_key, public_key

110

111

112 def sign_keys(

113 ca_key: str,

114 ca_crt: str,

115 public_key: str,

116 name: str,

117 ip: str,

118 groups: frozenset[str],

119):

120 with tempfile.TemporaryDirectory() as tmpdir:

121 temp_path = Path(tmpdir)

122

123 ca_crt_path = temp_path / "ca.crt"

124 ca_crt_path.write_text(ca_crt)

125 ca_key_path = temp_path / "ca.key"

126 ca_key_path.write_text(ca_key)

127

128 public_key_file = temp_path / "public.key"

129 public_key_file.write_text(public_key)

130

131 command = [

132 str(get_nebula_cert_binary_path()),

133 "sign",

134 "-in-pub",

135 "public.key",

136 "-name",

137 name,

138 "-ip",

139 ip,

140 "-groups",

141 " " + ",".join(groups) + " ",

142 "-out-crt",

143 "public.crt",

144 ]

145 print(f"cd {temp_path}")

146 print(" ".join(command))

147

148 res = subprocess.run(

149 command,

150 capture_output=True,

151 text=True,

152 cwd=tmpdir,

153 )

154 assert res.returncode == 0, res.stderr

155 cert = (Path(tmpdir) / "public.crt").read_text()

156 return cert

157

158

159 def print_ca(cert: str):

160 with tempfile.TemporaryDirectory() as tmpdir:

161 temp_path = Path(tmpdir)

162

163 cert_path = temp_path / "cert.crt"

164 cert_path.write_text(cert)

165

166 command = [

167 get_nebula_cert_binary_path(),

168 "print",

169 "-path",

170 "cert.crt",

171 "-json",

172 ]

173 res = subprocess.run(

174 command,

175 capture_output=True,

176 text=True,

177 cwd=tmpdir,

178 )

179

180 assert res.returncode == 0, res.stderr

181 output = res.stdout

182 json_output = json.loads(output)

183 return json_output

184

185

186 def create_ca(ca_name):

187 with tempfile.TemporaryDirectory() as tmpdir:

188 res = subprocess.run(

189 [str(get_nebula_cert_binary_path()), "ca", "-name", ca_name],

190 capture_output=True,

191 text=True,

192 cwd=tmpdir,

193 )

194 assert res.returncode == 0

195 cert = (Path(tmpdir) / "ca.crt").read_text()

196 key = (Path(tmpdir) / "ca.key").read_text()

197

198 return cert, key

199

200

201 def create_expiration_date(minutes=60) -> int:

202 return int(

203 (

204 datetime.utcnow().replace(tzinfo=timezone.utc) + timedelta(minutes)

205 ).timestamp()

206 )

207

208

209 def get_public_ip():

210 res = httpx.get("https://checkip.amazonaws.com/")

211 assert res.status_code == 200

212 public_ip = res.text.strip()

213 return public_ip

214

215

216 def get_default_config_path() -> Path:

217 os_name = platform.system()

218 if os_name == "Darwin":

219 return Path(os.path.expanduser("~/Library/Application Support/meshadmin"))

220 else:

221 return Path("/etc/meshadmin")