upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/src/tor.cpp
diff options
context:
space:
mode:
authorNsable-sh <nsable-sh@noreply.git.bitflash.network>2026-08-13 11:54:35 -0300
committerNsable-sh <nsable-sh@noreply.git.bitflash.network>2026-08-13 11:54:35 -0300
commit53cc17d621f55b8904812fae5df90ef089bf5aaf (patch)
tree794d251d6bdf51f0d488469ff050ece1846cdbea /src/tor.cpp
parentc009e7cc0c5093f4c4add741cb9d131d19956871 (diff)
net: add managed Tor hidden service bootstrap
Diffstat (limited to 'src/tor.cpp')
-rw-r--r--src/tor.cpp492
1 files changed, 492 insertions, 0 deletions
diff --git a/src/tor.cpp b/src/tor.cpp
new file mode 100644
index 0000000..30a6f0b
--- /dev/null
+++ b/src/tor.cpp
@@ -0,0 +1,492 @@
1// Copyright (c) 2026 Bitflash developers
2// Distributed under the MIT/X11 software license.
3
4#include "headers_core.h"
5#include "tor.h"
6#include "nostr.h"
7#include "proxy.h"
8
9#include <errno.h>
10
11#ifndef _WIN32
12#include <signal.h>
13#include <sys/wait.h>
14#endif
15
16static CCriticalSection cs_managedTor;
17static bool g_fManagedTor = false;
18static bool g_fManagedTorStopping = false;
19static unsigned short g_managedTorSocksPort = 0;
20static unsigned short g_managedTorP2PPort = 0;
21static std::string g_managedTorPath;
22static std::string g_managedTorDataDir;
23static std::string g_managedTorHiddenServiceDir;
24static std::string g_managedTorOnion;
25static std::string g_managedTorStatus;
26#ifdef _WIN32
27static PROCESS_INFORMATION g_managedTorProcess;
28#else
29static pid_t g_managedTorPid = -1;
30#endif
31
32static std::string PathJoin(const std::string& a, const std::string& b)
33{
34 if (a.empty())
35 return b;
36 char last = a[a.size() - 1];
37 if (last == '/' || last == '\\')
38 return a + b;
39 return a + "/" + b;
40}
41
42static std::string NormalizeTorrcPath(std::string path)
43{
44 for (size_t i = 0; i < path.size(); i++)
45 if (path[i] == '\\')
46 path[i] = '/';
47 return path;
48}
49
50static std::string QuoteTorrcPath(const std::string& path)
51{
52 std::string out = "\"";
53 for (size_t i = 0; i < path.size(); i++)
54 {
55 if (path[i] == '"' || path[i] == '\\')
56 out += '\\';
57 out += path[i];
58 }
59 out += "\"";
60 return out;
61}
62
63static std::string QuoteCommandArg(const std::string& arg)
64{
65 std::string out = "\"";
66 for (size_t i = 0; i < arg.size(); i++)
67 {
68 if (arg[i] == '"')
69 out += '\\';
70 out += arg[i];
71 }
72 out += "\"";
73 return out;
74}
75
76static bool EnsureDir(const std::string& path, std::string& errOut)
77{
78 if (path.empty())
79 {
80 errOut = "directory path is empty";
81 return false;
82 }
83#ifdef _WIN32
84 DWORD attr = GetFileAttributesA(path.c_str());
85 if (attr != INVALID_FILE_ATTRIBUTES)
86 {
87 if (attr & FILE_ATTRIBUTE_DIRECTORY)
88 return true;
89 errOut = strprintf("%s exists and is not a directory", path.c_str());
90 return false;
91 }
92 if (CreateDirectoryA(path.c_str(), NULL))
93 return true;
94 DWORD err = GetLastError();
95 if (err == ERROR_ALREADY_EXISTS)
96 return true;
97 errOut = strprintf("could not create %s (error %lu)", path.c_str(), (unsigned long)err);
98 return false;
99#else
100 struct stat st;
101 if (stat(path.c_str(), &st) == 0)
102 {
103 if (S_ISDIR(st.st_mode))
104 return true;
105 errOut = strprintf("%s exists and is not a directory", path.c_str());
106 return false;
107 }
108 if (mkdir(path.c_str(), 0700) == 0 || errno == EEXIST)
109 return true;
110 errOut = strprintf("could not create %s: %s", path.c_str(), strerror(errno));
111 return false;
112#endif
113}
114
115static bool WriteTextFile(const std::string& path, const std::string& data,
116 std::string& errOut)
117{
118 FILE* f = fopen(path.c_str(), "wb");
119 if (!f)
120 {
121 errOut = strprintf("could not write %s", path.c_str());
122 return false;
123 }
124 bool ok = fwrite(data.data(), 1, data.size(), f) == data.size();
125 if (fclose(f) != 0)
126 ok = false;
127 if (!ok)
128 {
129 errOut = strprintf("could not finish writing %s", path.c_str());
130 return false;
131 }
132 return true;
133}
134
135static bool ReadOneLine(const std::string& path, std::string& out)
136{
137 out.clear();
138 FILE* f = fopen(path.c_str(), "rb");
139 if (!f)
140 return false;
141 char buf[256] = {0};
142 if (!fgets(buf, sizeof(buf), f))
143 {
144 fclose(f);
145 return false;
146 }
147 fclose(f);
148 out = buf;
149 while (!out.empty() && (out[out.size() - 1] == '\n' || out[out.size() - 1] == '\r' ||
150 out[out.size() - 1] == ' ' || out[out.size() - 1] == '\t'))
151 out.erase(out.size() - 1);
152 return !out.empty();
153}
154
155static bool PickLocalPort(unsigned short& portOut, std::string& errOut)
156{
157 portOut = 0;
158 SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
159 if (s == INVALID_SOCKET)
160 {
161 errOut = "could not create local port probe socket";
162 return false;
163 }
164 struct sockaddr_in addr;
165 memset(&addr, 0, sizeof(addr));
166 addr.sin_family = AF_INET;
167 addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
168 addr.sin_port = 0;
169 if (bind(s, (struct sockaddr*)&addr, sizeof(addr)) != 0)
170 {
171 closesocket(s);
172 errOut = "could not bind local port probe socket";
173 return false;
174 }
175 socklen_t len = sizeof(addr);
176 if (getsockname(s, (struct sockaddr*)&addr, &len) != 0)
177 {
178 closesocket(s);
179 errOut = "could not read local port probe socket";
180 return false;
181 }
182 portOut = ntohs(addr.sin_port);
183 closesocket(s);
184 if (portOut == 0)
185 {
186 errOut = "local port probe returned port zero";
187 return false;
188 }
189 return true;
190}
191
192static std::string ExecutableDir()
193{
194#ifdef _WIN32
195 char path[MAX_PATH + 1] = {0};
196 DWORD n = GetModuleFileNameA(NULL, path, MAX_PATH);
197 if (n == 0 || n >= MAX_PATH)
198 return ".";
199 std::string s = path;
200 size_t slash = s.find_last_of("\\/");
201 if (slash == std::string::npos)
202 return ".";
203 return s.substr(0, slash);
204#else
205 char path[4096] = {0};
206 ssize_t n = readlink("/proc/self/exe", path, sizeof(path) - 1);
207 if (n <= 0)
208 return ".";
209 path[n] = 0;
210 std::string s = path;
211 size_t slash = s.find_last_of('/');
212 if (slash == std::string::npos)
213 return ".";
214 return s.substr(0, slash);
215#endif
216}
217
218static bool FileIsExecutableCandidate(const std::string& path)
219{
220#ifdef _WIN32
221 DWORD attr = GetFileAttributesA(path.c_str());
222 return attr != INVALID_FILE_ATTRIBUTES && !(attr & FILE_ATTRIBUTE_DIRECTORY);
223#else
224 return access(path.c_str(), X_OK) == 0;
225#endif
226}
227
228static std::string ResolveTorPath(const std::string& torPathOpt)
229{
230 if (!torPathOpt.empty())
231 return torPathOpt;
232
233 std::vector<std::string> candidates;
234 std::string exeDir = ExecutableDir();
235#ifdef _WIN32
236 candidates.push_back(PathJoin(exeDir, "tor/tor.exe"));
237 candidates.push_back(PathJoin(exeDir, "tor.exe"));
238 candidates.push_back("tor.exe");
239#else
240 candidates.push_back(PathJoin(exeDir, "tor/tor"));
241 candidates.push_back(PathJoin(exeDir, "tor"));
242 candidates.push_back("tor");
243#endif
244 for (size_t i = 0; i < candidates.size(); i++)
245 {
246 if (candidates[i].find('/') == std::string::npos &&
247 candidates[i].find('\\') == std::string::npos)
248 return candidates[i]; // Let the OS search PATH.
249 if (FileIsExecutableCandidate(candidates[i]))
250 return candidates[i];
251 }
252#ifdef _WIN32
253 return "tor.exe";
254#else
255 return "tor";
256#endif
257}
258
259std::string BtfBuildManagedTorrcForTest(const std::string& dataDir,
260 const std::string& hiddenServiceDir,
261 unsigned short socksPort,
262 unsigned short p2pPort)
263{
264 std::string torData = NormalizeTorrcPath(dataDir);
265 std::string hsDir = NormalizeTorrcPath(hiddenServiceDir);
266 std::string logFile = NormalizeTorrcPath(PathJoin(dataDir, "tor.log"));
267
268 std::string s;
269 s += "# Generated by Bitflash. Edit only while Bitflash is not managing Tor.\n";
270 s += "DataDirectory " + QuoteTorrcPath(torData) + "\n";
271 s += strprintf("SocksPort 127.0.0.1:%u IsolateSOCKSAuth IsolateClientAddr IsolateDestAddr IsolateDestPort\n",
272 (unsigned)socksPort);
273 s += "HiddenServiceDir " + QuoteTorrcPath(hsDir) + "\n";
274 s += "HiddenServiceVersion 3\n";
275 s += strprintf("HiddenServicePort %u 127.0.0.1:%u\n",
276 (unsigned)p2pPort, (unsigned)p2pPort);
277 s += "Log notice file " + QuoteTorrcPath(logFile) + "\n";
278 return s;
279}
280
281static bool StartTorProcess(const std::string& torPath, const std::string& torrcPath,
282 std::string& errOut)
283{
284#ifdef _WIN32
285 memset(&g_managedTorProcess, 0, sizeof(g_managedTorProcess));
286 STARTUPINFOA si;
287 memset(&si, 0, sizeof(si));
288 si.cb = sizeof(si);
289 std::string cmd = QuoteCommandArg(torPath) + " -f " + QuoteCommandArg(torrcPath);
290 std::vector<char> mutableCmd(cmd.begin(), cmd.end());
291 mutableCmd.push_back(0);
292 if (!CreateProcessA(NULL, &mutableCmd[0], NULL, NULL, FALSE, CREATE_NO_WINDOW,
293 NULL, NULL, &si, &g_managedTorProcess))
294 {
295 errOut = strprintf("could not start Tor process %s (error %lu)",
296 torPath.c_str(), (unsigned long)GetLastError());
297 return false;
298 }
299 return true;
300#else
301 g_managedTorPid = fork();
302 if (g_managedTorPid < 0)
303 {
304 errOut = strprintf("could not fork Tor process: %s", strerror(errno));
305 return false;
306 }
307 if (g_managedTorPid == 0)
308 {
309 execlp(torPath.c_str(), torPath.c_str(), "-f", torrcPath.c_str(), (char*)NULL);
310 _exit(127);
311 }
312 return true;
313#endif
314}
315
316static void ThreadManagedTorHostname(void*)
317{
318 std::string hostnamePath;
319 unsigned short p2pPort = 0;
320 CRITICAL_BLOCK(cs_managedTor)
321 {
322 hostnamePath = PathJoin(g_managedTorHiddenServiceDir, "hostname");
323 p2pPort = g_managedTorP2PPort;
324 }
325
326 for (int i = 0; i < 90 && !fShutdown; i++)
327 {
328 std::string onion;
329 if (ReadOneLine(hostnamePath, onion))
330 {
331 std::string endpoint = strprintf("%s:%u", onion.c_str(), (unsigned)p2pPort);
332 std::string err;
333 if (BtfSetLocalOnionEndpoint(endpoint, err))
334 {
335 CRITICAL_BLOCK(cs_managedTor)
336 {
337 g_managedTorOnion = BtfLocalOnionEndpoint();
338 g_managedTorStatus = strprintf("running, SOCKS5 127.0.0.1:%u, onion %s",
339 (unsigned)g_managedTorSocksPort,
340 g_managedTorOnion.c_str());
341 }
342 printf("Managed Tor hidden service ready: %s\n", BtfLocalOnionEndpoint().c_str());
343 return;
344 }
345 CRITICAL_BLOCK(cs_managedTor)
346 g_managedTorStatus = strprintf("running, but generated onion endpoint was rejected: %s",
347 err.c_str());
348 return;
349 }
350 Sleep(1000);
351 }
352
353 CRITICAL_BLOCK(cs_managedTor)
354 {
355 if (!g_fManagedTorStopping)
356 g_managedTorStatus = strprintf("running, SOCKS5 127.0.0.1:%u, waiting for hidden service hostname",
357 (unsigned)g_managedTorSocksPort);
358 }
359}
360
361bool BtfStartManagedTor(const std::string& torPathOpt, std::string& errOut)
362{
363 CRITICAL_BLOCK(cs_managedTor)
364 {
365 if (g_fManagedTor)
366 {
367 errOut = "managed Tor is already running";
368 return false;
369 }
370 }
371
372 std::string root = PathJoin(GetAppDir(), "managed-tor");
373 std::string dataDir = PathJoin(root, "data");
374 std::string hsDir = PathJoin(root, "onion-service");
375 std::string torrcPath = PathJoin(root, "torrc");
376 if (!EnsureDir(root, errOut) || !EnsureDir(dataDir, errOut) || !EnsureDir(hsDir, errOut))
377 return false;
378
379 unsigned short socksPort = 0;
380 if (!PickLocalPort(socksPort, errOut))
381 return false;
382
383 unsigned short p2pPort = ntohs(nListenPort);
384 std::string torrc = BtfBuildManagedTorrcForTest(dataDir, hsDir, socksPort, p2pPort);
385 if (!WriteTextFile(torrcPath, torrc, errOut))
386 return false;
387
388 std::string torPath = ResolveTorPath(torPathOpt);
389 {
390 CRITICAL_BLOCK(cs_managedTor)
391 {
392 g_fManagedTor = true;
393 g_fManagedTorStopping = false;
394 g_managedTorPath = torPath;
395 g_managedTorDataDir = dataDir;
396 g_managedTorHiddenServiceDir = hsDir;
397 g_managedTorSocksPort = socksPort;
398 g_managedTorP2PPort = p2pPort;
399 g_managedTorOnion.clear();
400 g_managedTorStatus = strprintf("starting %s, SOCKS5 127.0.0.1:%u",
401 torPath.c_str(), (unsigned)socksPort);
402 }
403 }
404
405 if (!StartTorProcess(torPath, torrcPath, errOut))
406 {
407 CRITICAL_BLOCK(cs_managedTor)
408 {
409 g_fManagedTor = false;
410 g_managedTorStatus = strprintf("failed: %s", errOut.c_str());
411 }
412 return false;
413 }
414
415 std::string proxyErr;
416 if (!BtfEnableTorProxy(strprintf("127.0.0.1:%u", (unsigned)socksPort), proxyErr))
417 {
418 BtfStopManagedTor();
419 errOut = strprintf("could not enable managed Tor SOCKS5 proxy: %s", proxyErr.c_str());
420 return false;
421 }
422
423 if (_beginthread(ThreadManagedTorHostname, 0, NULL) == (uintptr_t)-1)
424 {
425 BtfStopManagedTor();
426 errOut = "could not start managed Tor hostname watcher";
427 return false;
428 }
429
430 return true;
431}
432
433void BtfStopManagedTor()
434{
435 bool fWasRunning = false;
436 CRITICAL_BLOCK(cs_managedTor)
437 {
438 fWasRunning = g_fManagedTor;
439 if (!fWasRunning)
440 return;
441 g_fManagedTorStopping = true;
442 g_fManagedTor = false;
443 g_managedTorStatus = "stopping";
444 }
445#ifdef _WIN32
446 if (g_managedTorProcess.hProcess)
447 {
448 TerminateProcess(g_managedTorProcess.hProcess, 0);
449 WaitForSingleObject(g_managedTorProcess.hProcess, 5000);
450 CloseHandle(g_managedTorProcess.hProcess);
451 CloseHandle(g_managedTorProcess.hThread);
452 memset(&g_managedTorProcess, 0, sizeof(g_managedTorProcess));
453 }
454#else
455 if (g_managedTorPid > 0)
456 {
457 kill(g_managedTorPid, SIGTERM);
458 for (int i = 0; i < 50; i++)
459 {
460 int status = 0;
461 pid_t r = waitpid(g_managedTorPid, &status, WNOHANG);
462 if (r == g_managedTorPid)
463 break;
464 Sleep(100);
465 }
466 g_managedTorPid = -1;
467 }
468#endif
469 CRITICAL_BLOCK(cs_managedTor)
470 {
471 g_managedTorStatus = "stopped";
472 g_managedTorOnion.clear();
473 }
474}
475
476bool BtfManagedTorEnabled()
477{
478 CRITICAL_BLOCK(cs_managedTor)
479 return g_fManagedTor;
480 return false;
481}
482
483std::string BtfManagedTorStatus()
484{
485 CRITICAL_BLOCK(cs_managedTor)
486 {
487 if (!g_fManagedTor && g_managedTorStatus.empty())
488 return "disabled";
489 return g_managedTorStatus;
490 }
491 return "disabled";
492}