summaryrefslogtreecommitdiff
path: root/src/opentracker.c
diff options
context:
space:
mode:
authorDirk Engling <erdgeist@erdgeist.org>2026-11-02 20:50:24 +0100
committerDirk Engling <erdgeist@erdgeist.org>2026-11-02 21:14:02 +0100
commitad541c67761b8aa6da1566802c347cac4018d0c2 (patch)
treea9166fa9d349911d69a6948e53b05344dcc348a0 /src/opentracker.c
parentc679ff8a3b1418a5960cc49ba6458a0007df54fa (diff)
Move the build sources into src/
Diffstat (limited to 'src/opentracker.c')
-rw-r--r--src/opentracker.c443
1 files changed, 443 insertions, 0 deletions
diff --git a/src/opentracker.c b/src/opentracker.c
new file mode 100644
index 0000000..14b817e
--- /dev/null
+++ b/src/opentracker.c
@@ -0,0 +1,443 @@
1/* This software was written by Dirk Engling <erdgeist@erdgeist.org>
2 It is considered beerware. Prost. Skol. Cheers or whatever.
3 Some of the stuff below is stolen from Fefes example libowfat httpd. */
4
5/* System */
6#include <ctype.h>
7#include <errno.h>
8#include <limits.h>
9#include <pthread.h>
10#include <pwd.h>
11#include <signal.h>
12#include <stdint.h>
13#include <stdio.h>
14#include <stdlib.h>
15#include <string.h>
16#include <sys/socket.h>
17#include <unistd.h>
18#ifdef WANT_SYSLOGS
19#include <syslog.h>
20#endif
21
22/* Opentracker */
23#include "ot_accesslist.h"
24#include "ot_config.h"
25#include "ot_hex.h"
26#include "ot_http.h"
27#include "ot_ip.h"
28#include "ot_livesync.h"
29#include "ot_mutex.h"
30#include "ot_workqueue.h"
31#include "ot_random.h"
32#include "ot_scan.h"
33#include "ot_event.h"
34#include "ot_fdstate.h"
35#include "ot_socket.h"
36#include "ot_stats.h"
37#include "ot_tcp.h"
38#include "ot_udp.h"
39#include "ot_worker.h"
40#include "trackerlogic.h"
41
42/* Globals */
43uint32_t g_tracker_id;
44_Atomic int g_opentracker_running = 1;
45int g_self_pipe[2];
46
47static void panic(const char *routine) __attribute__((noreturn));
48static void panic(const char *routine) {
49 fprintf(stderr, "%s: %s\n", routine, strerror(errno));
50 exit(111);
51}
52
53/* Say we are stopping, wake the loop, and let main do the work on its own
54 stack. Everything other than SIGINT falls through and does nothing, which
55 is what OT_SIGNAL_WAKE wants: returning from the read is all it is for. */
56static void signal_handler(int s) {
57 if (s == SIGINT) {
58 const char byte = 0;
59 int saved = errno;
60
61 /* Any new interrupt signal quits the application */
62 signal(SIGINT, SIG_DFL);
63
64 /* Tell all other threads to not acquire any new lock on a bucket
65 but cancel their operations and return */
66 g_opentracker_running = 0;
67
68 (void)!write(g_self_pipe[1], &byte, 1);
69 errno = saved;
70 }
71}
72
73static void defaul_signal_handlers(void) {
74 sigset_t signal_mask;
75 sigemptyset(&signal_mask);
76 sigaddset(&signal_mask, SIGPIPE);
77 sigaddset(&signal_mask, SIGHUP);
78 sigaddset(&signal_mask, SIGINT);
79 sigaddset(&signal_mask, SIGALRM);
80 pthread_sigmask(SIG_BLOCK, &signal_mask, NULL);
81}
82
83static void install_signal_handlers(void) {
84 struct sigaction sa;
85 sigset_t signal_mask;
86 sigemptyset(&signal_mask);
87
88 sa.sa_handler = signal_handler;
89 sigemptyset(&sa.sa_mask);
90 sa.sa_flags = 0;
91 if ((sigaction(SIGINT, &sa, NULL) == -1) || (sigaction(SIGALRM, &sa, NULL) == -1) ||
92 (sigaction(OT_SIGNAL_WAKE, &sa, NULL) == -1))
93 panic("install_signal_handlers");
94
95 sigaddset(&signal_mask, SIGINT);
96 pthread_sigmask(SIG_UNBLOCK, &signal_mask, NULL);
97}
98
99
100/* How many ready descriptors one wait hands back at a time */
101#define OT_EVENT_BATCH 512
102
103static void *server_mainloop(void *args) {
104 struct ot_workstruct ws;
105 time_t next_timeout_check = time_future_seconds(OT_CLIENT_TIMEOUT_CHECKINTERVAL);
106 struct iovec *iovector;
107 size_t iovec_entries;
108 int is_partial;
109
110 (void)args;
111
112 if (workstruct_init(&ws) == -1)
113 panic("Initializing worker failed");
114
115 while (g_opentracker_running) {
116 ot_event ready[OT_EVENT_BATCH];
117 int64_t sock;
118 int count, i;
119
120 count = event_wait(ready, OT_EVENT_BATCH, OT_CLIENT_TIMEOUT_CHECKINTERVAL * 1000);
121
122 for (i = 0; i < count; ++i) {
123 const void *cookie;
124
125 sock = ready[i].fd;
126 /* A descriptor closed earlier in this same batch may already have been
127 handed out again by accept, so an event is only ours if the
128 generation it was registered under is still the one in place */
129 if (fdstate_generation((int)sock) != ready[i].generation)
130 continue;
131 cookie = fdstate_cookie((int)sock);
132
133 if ((intptr_t)cookie == FLAG_TCP)
134 tcp_accept(sock, &ws);
135 else if ((intptr_t)cookie == FLAG_UDP)
136 handle_udp6(sock, &ws);
137 else if ((intptr_t)cookie == FLAG_SELFPIPE)
138 while (read((int)sock, ws.inbuf, G_INBUF_SIZE) > 0)
139 ;
140 else if (fdstate_claim((int)sock, ready[i].generation)) {
141 if (ready[i].what & EVENT_IN)
142 tcp_read(sock, &ws);
143 /* The read may have closed it, so look again before writing */
144 if ((ready[i].what & EVENT_OUT) && fdstate_generation((int)sock) == ready[i].generation)
145 tcp_write(sock);
146 if (fdstate_generation((int)sock) == ready[i].generation)
147 fdstate_release((int)sock);
148 }
149 }
150
151 while ((sock = workqueue_popresult(&iovec_entries, &iovector, &is_partial)) != -1)
152 http_sendiovecdata(sock, &ws, iovec_entries, iovector, is_partial);
153
154 if (time_now_seconds() > next_timeout_check) {
155 int cursor = 0;
156
157 while ((sock = fdstate_next_expired(time_now_seconds(), &cursor)) != -1)
158 http_close(sock);
159 next_timeout_check = time_future_seconds(OT_CLIENT_TIMEOUT_CHECKINTERVAL);
160 }
161
162 livesync_ticker();
163 }
164
165 /* This work struct is about to go out of scope, so hand its tally over
166 before anything can read it there */
167 workstruct_deinit(&ws);
168 return 0;
169}
170
171
172/* A group of sockets sharing one address only spreads datagrams if every
173 member asked for it before binding, the first one included, so whether
174 this entry will have workers has to be known here. */
175static socket_sharing listen_sharing_for(const ot_listen *entry) {
176 if (entry->proto != FLAG_MCA && entry->workers > 1 && socket_can_balance())
177 return SOCKET_BALANCED;
178 return entry->shared ? SOCKET_SHARED : SOCKET_EXCLUSIVE;
179}
180
181static void listen_bind_one(ot_listen *entry) {
182 socket_sharing sharing = listen_sharing_for(entry);
183
184#ifdef _DEBUG
185 {
186 const char *protos[] = {"TCP", "UDP", "UDP mcast"};
187 char _debug[512];
188 int off = snprintf(_debug, sizeof(_debug), "Binding socket type %s to address [", protos[entry->proto]);
189 off += ip_format(_debug + off, entry->ip);
190 snprintf(_debug + off, sizeof(_debug) - off, "]:%d...", entry->port);
191 fputs(_debug, stderr);
192 }
193#endif
194
195#ifdef WANT_SYNC_LIVE
196 if (entry->proto == FLAG_MCA) {
197 livesync_bind_mcast(entry->ip, entry->port);
198#ifdef _DEBUG
199 fputs(" success.\n", stderr);
200#endif
201 return;
202 }
203#endif
204
205 if (entry->proto == FLAG_TCP) {
206 if ((entry->sock = socket_tcp_listen(entry->ip, entry->port, sharing)) == -1)
207 panic("socket_tcp_listen");
208 socket_defer_accept((int)entry->sock);
209 } else {
210 if ((entry->sock = socket_udp_bind(entry->ip, entry->port, sharing)) == -1)
211 panic("socket_udp_bind");
212 }
213
214 if (event_add((int)entry->sock, EVENT_IN, fdstate_open(entry->sock, (void *)entry->proto, 0)) == -1)
215 panic("event_add");
216
217#ifdef _DEBUG
218 fputs(" success.\n", stderr);
219#endif
220}
221
222/* Bound in the order the entries were given.
223 Note: On macOS SO_REUSEPORT delivers everything to the socket that bound last. */
224static void listen_bind_all(void) {
225 size_t count, i;
226 ot_listen *entries = config_listen_entries(&count);
227
228 for (i = 0; i < count; ++i)
229 listen_bind_one(entries + i);
230}
231
232/* The socket the event loop was using is passed to the first worker.
233 If start up tests show that the kernel can spread connections over
234 a group bound to one address, every worker binds one of its own.
235 If not, all workers call accept on the first. */
236static void listen_start_tcp_workers(void) {
237 size_t count, i;
238 ot_listen *entries = config_listen_entries(&count);
239
240 for (i = 0; i < count; ++i) {
241 ot_listen *entry = entries + i;
242 int balance = listen_sharing_for(entry) == SOCKET_BALANCED;
243 int worker;
244
245 if (entry->proto != FLAG_TCP || entry->workers <= 0)
246 continue;
247
248 event_del((int)entry->sock);
249 tcp_worker_install(entry->sock);
250
251 for (worker = 1; worker < entry->workers; ++worker) {
252 int64_t sock = entry->sock;
253
254 if (balance) {
255 sock = socket_tcp_listen(entry->ip, entry->port, SOCKET_BALANCED);
256 if (sock == -1) {
257 fprintf(stderr, "Warning: could not bind another socket for tcp worker %d: %s. Sharing the first instead.\n", worker,
258 strerror(errno));
259 sock = entry->sock;
260 }
261 }
262 tcp_worker_install(sock);
263 }
264 }
265}
266
267static void listen_start_udp_workers(void) {
268 size_t count, i;
269 ot_listen *entries = config_listen_entries(&count);
270
271 udp_init();
272
273 for (i = 0; i < count; ++i) {
274 ot_listen *entry = entries + i;
275 int balance = listen_sharing_for(entry) == SOCKET_BALANCED;
276 int worker;
277
278 if (entry->proto != FLAG_UDP || entry->workers <= 0)
279 continue;
280
281 event_del((int)entry->sock);
282 udp_worker_install(entry->sock);
283
284 for (worker = 1; worker < entry->workers; ++worker) {
285 int64_t sock = entry->sock;
286
287 if (balance) {
288 sock = socket_udp_bind(entry->ip, entry->port, SOCKET_BALANCED);
289 if (sock == -1) {
290 fprintf(stderr, "Warning: could not bind another socket for udp worker %d: %s. Sharing the first instead.\n", worker, strerror(errno));
291 sock = entry->sock;
292 }
293 }
294 udp_worker_install(sock);
295 }
296 }
297}
298
299void load_state(const char *const state_filename) {
300 FILE *state_filehandle;
301 char inbuf[512];
302 ot_hash infohash;
303 uint64_t base, downcount;
304 int consumed;
305
306 state_filehandle = fopen(state_filename, "r");
307
308 if (state_filehandle == NULL) {
309 fprintf(stderr, "Warning: Can't open config file: %s.", state_filename);
310 return;
311 }
312
313 /* We do ignore anything that is not of the form "^[:xdigit:]:\d+:\d+" */
314 while (fgets(inbuf, sizeof(inbuf), state_filehandle)) {
315 size_t i;
316
317 if (!hex_to_hash(infohash, inbuf))
318 continue;
319 i = 2 * sizeof(ot_hash);
320
321 if (inbuf[i++] != ':' || !(consumed = scan_uint(inbuf + i, &base, UINT64_MAX)))
322 continue;
323 i += consumed;
324 if (inbuf[i++] != ':' || !(consumed = scan_uint(inbuf + i, &downcount, UINT64_MAX)))
325 continue;
326 add_torrent_from_saved_state(infohash, base, downcount);
327 }
328
329 fclose(state_filehandle);
330}
331
332int drop_privileges(const char *const serveruser, const char *const serverdir) {
333 struct passwd *pws = NULL;
334
335#ifdef _DEBUG
336 if (!geteuid())
337 fprintf(stderr, "Dropping to user %s.\n", serveruser);
338 if (serverdir)
339 fprintf(stderr, "ch%s'ing to directory %s.\n", geteuid() ? "dir" : "root", serverdir);
340#endif
341
342 /* Grab pws entry before chrooting */
343 pws = getpwnam(serveruser);
344 endpwent();
345
346 if (geteuid() == 0) {
347 /* Running as root: chroot and drop privileges */
348 if (serverdir && chroot(serverdir)) {
349 fprintf(stderr, "Could not chroot to %s, because: %s\n", serverdir, strerror(errno));
350 return -1;
351 }
352
353 if (chdir("/"))
354 panic("chdir() failed after chrooting: ");
355
356 /* If we can't find server user, revert to nobody's default uid */
357 if (!pws) {
358 fprintf(stderr, "Warning: Could not get password entry for %s. Reverting to uid -2.\n", serveruser);
359 if (setegid((gid_t)-2) || setgid((gid_t)-2) || setuid((uid_t)-2) || seteuid((uid_t)-2))
360 panic("Could not set uid to value -2");
361 } else {
362 if (setegid(pws->pw_gid) || setgid(pws->pw_gid) || setuid(pws->pw_uid) || seteuid(pws->pw_uid))
363 panic("Could not set uid to specified value");
364 }
365
366 if (geteuid() == 0 || getegid() == 0)
367 panic("Still running with root privileges?!");
368 } else {
369 /* Normal user, just chdir() */
370 if (serverdir && chdir(serverdir)) {
371 fprintf(stderr, "Could not chroot to %s, because: %s\n", serverdir, strerror(errno));
372 return -1;
373 }
374 }
375
376 return 0;
377}
378
379/* Maintain our copy of the clock. time() on BSDs is very expensive. */
380int main(int argc, char **argv) {
381
382 random_init();
383 if (fdstate_init() == -1)
384 panic("fdstate_init");
385 if (event_init() == -1)
386 panic("event_init");
387 config_parse(argc, argv);
388
389 listen_bind_all();
390
391 defaul_signal_handlers();
392
393#ifdef WANT_SYSLOGS
394 openlog("opentracker", 0, LOG_USER);
395 setlogmask(LOG_UPTO(LOG_INFO));
396#endif
397
398 if (drop_privileges(g_serveruser ? g_serveruser : "nobody", g_serverdir) == -1)
399 panic("drop_privileges failed, exiting. Last error");
400
401
402 /* Create our self pipe which allows us to interrupt mainloops
403 wait in case some data is available to send out */
404 if (pipe(g_self_pipe) == -1)
405 panic("selfpipe failed: ");
406 /* Both ends have to be non blocking: the loop drains the read end until it
407 is empty, and a worker nudging a full pipe has already made its point */
408 socket_set_blocking(g_self_pipe[0], 0);
409 socket_set_blocking(g_self_pipe[1], 0);
410 if (event_add(g_self_pipe[0], EVENT_IN, fdstate_open(g_self_pipe[0], (void *)FLAG_SELFPIPE, 0)) == -1)
411 panic("selfpipe event_add");
412
413 /* Init all sub systems. This call may fail with an exit() */
414 trackerlogic_init();
415
416#ifdef _DEBUG_RANDOMTORRENTS
417 fprintf(stderr, "DEBUG: Generating %d random peers on random torrents. This may take a while. (Setting RANDOMTORRENTS in trackerlogic.h)\n", RANDOMTORRENTS);
418 trackerlogic_add_random_torrents(RANDOMTORRENTS);
419 fprintf(stderr, "... done.\n");
420#endif
421
422 if (g_statefile)
423 load_state(g_statefile);
424
425 install_signal_handlers();
426
427 listen_start_tcp_workers();
428 listen_start_udp_workers();
429
430 server_mainloop(0);
431 worker_stop_all();
432
433 /* No thread is serving any more, so the teardown can run where a debugger
434 and the sanitizers can follow it */
435 trackerlogic_deinit();
436#ifdef WANT_SYSLOGS
437 closelog();
438#endif
439
440 event_deinit();
441 fdstate_deinit();
442 return 0;
443}