IRC bouncer
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.

bounce.c 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459
  1. /* Copyright (C) 2019 C. McEnroe <june@causal.agency>
  2. *
  3. * This program is free software: you can redistribute it and/or modify
  4. * it under the terms of the GNU General Public License as published by
  5. * the Free Software Foundation, either version 3 of the License, or
  6. * (at your option) any later version.
  7. *
  8. * This program is distributed in the hope that it will be useful,
  9. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. * GNU General Public License for more details.
  12. *
  13. * You should have received a copy of the GNU General Public License
  14. * along with this program. If not, see <https://www.gnu.org/licenses/>.
  15. */
  16. #include <assert.h>
  17. #include <err.h>
  18. #include <errno.h>
  19. #include <fcntl.h>
  20. #include <getopt.h>
  21. #include <limits.h>
  22. #include <poll.h>
  23. #include <pwd.h>
  24. #include <signal.h>
  25. #include <stdbool.h>
  26. #include <stdio.h>
  27. #include <stdlib.h>
  28. #include <string.h>
  29. #include <strings.h>
  30. #include <sys/file.h>
  31. #include <sys/socket.h>
  32. #include <sys/stat.h>
  33. #include <sysexits.h>
  34. #include <tls.h>
  35. #include <unistd.h>
  36. #ifdef __FreeBSD__
  37. #include <sys/capsicum.h>
  38. #endif
  39. #include "bounce.h"
  40. static void hashPass(void) {
  41. char *pass = getpass("Password: ");
  42. byte rand[12];
  43. arc4random_buf(rand, sizeof(rand));
  44. char salt[3 + BASE64_SIZE(sizeof(rand))] = "$6$";
  45. base64(&salt[3], rand, sizeof(rand));
  46. printf("%s\n", crypt(pass, salt));
  47. }
  48. static void genCert(const char *path) {
  49. const char *name = strrchr(path, '/');
  50. name = (name ? &name[1] : path);
  51. char subj[256];
  52. snprintf(subj, sizeof(subj), "/CN=%.*s", (int)strcspn(name, "."), name);
  53. umask(0066);
  54. execlp(
  55. "openssl", "openssl", "req",
  56. "-x509", "-new", "-newkey", "rsa:4096", "-sha256", "-days", "1000",
  57. "-nodes", "-subj", subj, "-out", path, "-keyout", path,
  58. NULL
  59. );
  60. err(EX_UNAVAILABLE, "openssl");
  61. }
  62. static size_t parseSize(const char *str) {
  63. char *rest;
  64. size_t size = strtoull(str, &rest, 0);
  65. if (*rest) errx(EX_USAGE, "invalid size: %s", str);
  66. return size;
  67. }
  68. static FILE *saveFile;
  69. static void saveSave(void) {
  70. int error = ringSave(saveFile);
  71. if (error) warn("fwrite");
  72. error = fclose(saveFile);
  73. if (error) warn("fclose");
  74. }
  75. static void saveLoad(const char *path) {
  76. umask(0066);
  77. saveFile = fopen(path, "a+");
  78. if (!saveFile) err(EX_CANTCREAT, "%s", path);
  79. int error = flock(fileno(saveFile), LOCK_EX | LOCK_NB);
  80. if (error && errno != EWOULDBLOCK) err(EX_OSERR, "flock");
  81. if (error) errx(EX_CANTCREAT, "lock held by other process: %s", path);
  82. rewind(saveFile);
  83. ringLoad(saveFile);
  84. error = ftruncate(fileno(saveFile), 0);
  85. if (error) err(EX_IOERR, "ftruncate");
  86. atexit(saveSave);
  87. }
  88. struct SplitPath {
  89. int dir;
  90. char *file;
  91. int targetDir;
  92. };
  93. static bool linkTarget(char *target, size_t cap, int dir, const char *file) {
  94. ssize_t len = readlinkat(dir, file, target, cap - 1);
  95. if (len < 0 && errno == EINVAL) return false;
  96. if (len < 0) err(EX_NOINPUT, "%s", file);
  97. target[len] = '\0';
  98. return true;
  99. }
  100. static struct SplitPath splitPath(char *path) {
  101. struct SplitPath split = { .targetDir = -1 };
  102. split.file = strrchr(path, '/');
  103. if (split.file) {
  104. *split.file++ = '\0';
  105. split.dir = open(path, O_DIRECTORY);
  106. } else {
  107. split.file = path;
  108. split.dir = open(".", O_DIRECTORY);
  109. }
  110. if (split.dir < 0) err(EX_NOINPUT, "%s", path);
  111. // Capsicum workaround for certbot "live" symlinks to "../../archive".
  112. char target[PATH_MAX];
  113. if (!linkTarget(target, sizeof(target), split.dir, split.file)) {
  114. return split;
  115. }
  116. char *file = strrchr(target, '/');
  117. if (file) {
  118. *file = '\0';
  119. split.targetDir = openat(split.dir, target, O_DIRECTORY);
  120. if (split.targetDir < 0) err(EX_NOINPUT, "%s", target);
  121. }
  122. return split;
  123. }
  124. static FILE *splitOpen(struct SplitPath split) {
  125. if (split.targetDir >= 0) {
  126. char target[PATH_MAX];
  127. if (!linkTarget(target, sizeof(target), split.dir, split.file)) {
  128. errx(EX_CONFIG, "file is no longer a symlink");
  129. }
  130. split.dir = split.targetDir;
  131. split.file = strrchr(target, '/');
  132. if (!split.file) {
  133. errx(EX_CONFIG, "symlink no longer targets directory");
  134. }
  135. split.file++;
  136. }
  137. int fd = openat(split.dir, split.file, O_RDONLY);
  138. if (fd < 0) err(EX_NOINPUT, "%s", split.file);
  139. FILE *file = fdopen(fd, "r");
  140. if (!file) err(EX_IOERR, "fdopen");
  141. return file;
  142. }
  143. #ifdef __FreeBSD__
  144. static void capLimit(int fd, const cap_rights_t *rights) {
  145. int error = cap_rights_limit(fd, rights);
  146. if (error) err(EX_OSERR, "cap_rights_limit");
  147. }
  148. static void capLimitSplit(struct SplitPath split, const cap_rights_t *rights) {
  149. capLimit(split.dir, rights);
  150. if (split.targetDir >= 0) capLimit(split.targetDir, rights);
  151. }
  152. #endif
  153. static volatile sig_atomic_t signals[NSIG];
  154. static void signalHandler(int signal) {
  155. signals[signal] = 1;
  156. }
  157. static struct {
  158. struct pollfd *fds;
  159. struct Client **clients;
  160. size_t cap, len;
  161. } event;
  162. static void eventAdd(int fd, struct Client *client) {
  163. if (event.len == event.cap) {
  164. event.cap = (event.cap ? event.cap * 2 : 8);
  165. event.fds = realloc(event.fds, sizeof(*event.fds) * event.cap);
  166. if (!event.fds) err(EX_OSERR, "realloc");
  167. event.clients = realloc(
  168. event.clients, sizeof(*event.clients) * event.cap
  169. );
  170. if (!event.clients) err(EX_OSERR, "realloc");
  171. }
  172. event.fds[event.len] = (struct pollfd) { .fd = fd, .events = POLLIN };
  173. event.clients[event.len] = client;
  174. event.len++;
  175. }
  176. static void eventRemove(size_t i) {
  177. close(event.fds[i].fd);
  178. event.len--;
  179. event.fds[i] = event.fds[event.len];
  180. event.clients[i] = event.clients[event.len];
  181. }
  182. int main(int argc, char *argv[]) {
  183. size_t ringSize = 4096;
  184. const char *savePath = NULL;
  185. const char *bindHost = "localhost";
  186. const char *bindPort = "6697";
  187. char bindPath[PATH_MAX] = "";
  188. char certPath[PATH_MAX] = "";
  189. char privPath[PATH_MAX] = "";
  190. bool insecure = false;
  191. const char *clientCert = NULL;
  192. const char *clientPriv = NULL;
  193. const char *host = NULL;
  194. const char *port = "6697";
  195. char *pass = NULL;
  196. bool sasl = false;
  197. char *plain = NULL;
  198. const char *nick = NULL;
  199. const char *user = NULL;
  200. const char *real = NULL;
  201. const char *join = NULL;
  202. const char *quit = "connection reset by purr";
  203. const char *Opts = "!A:C:H:K:NP:Q:U:W:a:c:ef:g:h:j:k:n:p:r:s:u:vw:x";
  204. const struct option LongOpts[] = {
  205. { "insecure", no_argument, NULL, '!' },
  206. { "away", required_argument, NULL, 'A' },
  207. { "cert", required_argument, NULL, 'C' },
  208. { "bind-host", required_argument, NULL, 'H' },
  209. { "priv", required_argument, NULL, 'K' },
  210. { "no-names", no_argument, NULL, 'N' },
  211. { "bind-port", required_argument, NULL, 'P' },
  212. { "quit", required_argument, NULL, 'Q' },
  213. { "bind-path", required_argument, NULL, 'U' },
  214. { "client-pass", required_argument, NULL, 'W' },
  215. { "sasl-plain", required_argument, NULL, 'a' },
  216. { "client-cert", required_argument, NULL, 'c' },
  217. { "sasl-external", no_argument, NULL, 'e' },
  218. { "save", required_argument, NULL, 'f' },
  219. { "host", required_argument, NULL, 'h' },
  220. { "join", required_argument, NULL, 'j' },
  221. { "client-priv", required_argument, NULL, 'k' },
  222. { "nick", required_argument, NULL, 'n' },
  223. { "port", required_argument, NULL, 'p' },
  224. { "real", required_argument, NULL, 'r' },
  225. { "size", required_argument, NULL, 's' },
  226. { "user", required_argument, NULL, 'u' },
  227. { "verbose", no_argument, NULL, 'v' },
  228. { "pass", required_argument, NULL, 'w' },
  229. {0},
  230. };
  231. int opt;
  232. while (0 < (opt = getopt_config(argc, argv, Opts, LongOpts, NULL))) {
  233. switch (opt) {
  234. break; case '!': insecure = true;
  235. break; case 'A': clientAway = optarg;
  236. break; case 'C': strlcpy(certPath, optarg, sizeof(certPath));
  237. break; case 'H': bindHost = optarg;
  238. break; case 'K': strlcpy(privPath, optarg, sizeof(privPath));
  239. break; case 'N': stateNoNames = true;
  240. break; case 'P': bindPort = optarg;
  241. break; case 'Q': quit = optarg;
  242. break; case 'U': strlcpy(bindPath, optarg, sizeof(bindPath));
  243. break; case 'W': clientPass = optarg;
  244. break; case 'a': sasl = true; plain = optarg;
  245. break; case 'c': clientCert = optarg;
  246. break; case 'e': sasl = true;
  247. break; case 'f': savePath = optarg;
  248. break; case 'g': genCert(optarg);
  249. break; case 'h': host = optarg;
  250. break; case 'j': join = optarg;
  251. break; case 'k': clientPriv = optarg;
  252. break; case 'n': nick = optarg;
  253. break; case 'p': port = optarg;
  254. break; case 'r': real = optarg;
  255. break; case 's': ringSize = parseSize(optarg);
  256. break; case 'u': user = optarg;
  257. break; case 'v': verbose = true;
  258. break; case 'w': pass = optarg;
  259. break; case 'x': hashPass(); return EX_OK;
  260. break; default: return EX_USAGE;
  261. }
  262. }
  263. if (bindPath[0]) {
  264. struct stat st;
  265. int error = stat(bindPath, &st);
  266. if (error && errno != ENOENT) err(EX_CANTCREAT, "%s", bindPath);
  267. if (S_ISDIR(st.st_mode)) {
  268. strlcat(bindPath, "/", sizeof(bindPath));
  269. strlcat(bindPath, bindHost, sizeof(bindPath));
  270. }
  271. }
  272. if (!certPath[0]) {
  273. snprintf(
  274. certPath, sizeof(certPath), CERTBOT_PATH "/live/%s/fullchain.pem",
  275. bindHost
  276. );
  277. }
  278. if (!privPath[0]) {
  279. snprintf(
  280. privPath, sizeof(privPath), CERTBOT_PATH "/live/%s/privkey.pem",
  281. bindHost
  282. );
  283. }
  284. if (!host) errx(EX_USAGE, "host required");
  285. if (!nick) {
  286. nick = getenv("USER");
  287. if (!nick) errx(EX_CONFIG, "USER unset");
  288. }
  289. if (!user) user = nick;
  290. if (!real) real = nick;
  291. if (!clientAway) clientAway = "pounced :3";
  292. if (clientPass && clientPass[0] != '$') {
  293. errx(EX_CONFIG, "password must be hashed with -x");
  294. }
  295. ringAlloc(ringSize);
  296. if (savePath) saveLoad(savePath);
  297. struct SplitPath certSplit = splitPath(certPath);
  298. struct SplitPath privSplit = splitPath(privPath);
  299. FILE *cert = splitOpen(certSplit);
  300. FILE *priv = splitOpen(privSplit);
  301. localConfig(cert, priv);
  302. fclose(cert);
  303. fclose(priv);
  304. int bind[8];
  305. size_t binds = bindPath[0]
  306. ? localUnix(bind, ARRAY_LEN(bind), bindPath)
  307. : localBind(bind, ARRAY_LEN(bind), bindHost, bindPort);
  308. serverConfig(insecure, clientCert, clientPriv);
  309. int server = serverConnect(host, port);
  310. #ifdef __FreeBSD__
  311. int error = cap_enter();
  312. if (error) err(EX_OSERR, "cap_enter");
  313. cap_rights_t saveRights, fileRights, sockRights, bindRights;
  314. cap_rights_init(&saveRights, CAP_WRITE);
  315. cap_rights_init(&fileRights, CAP_FCNTL, CAP_FSTAT, CAP_LOOKUP, CAP_READ);
  316. cap_rights_init(&sockRights, CAP_EVENT, CAP_RECV, CAP_SEND, CAP_SETSOCKOPT);
  317. cap_rights_init(&bindRights, CAP_LISTEN, CAP_ACCEPT);
  318. cap_rights_merge(&bindRights, &sockRights);
  319. if (saveFile) capLimit(fileno(saveFile), &saveRights);
  320. capLimitSplit(certSplit, &fileRights);
  321. capLimitSplit(privSplit, &fileRights);
  322. for (size_t i = 0; i < binds; ++i) {
  323. capLimit(bind[i], &bindRights);
  324. }
  325. capLimit(server, &sockRights);
  326. #endif
  327. stateLogin(pass, sasl, plain, nick, user, real);
  328. if (pass) explicit_bzero(pass, strlen(pass));
  329. if (plain) explicit_bzero(plain, strlen(plain));
  330. while (!stateReady()) serverRecv();
  331. serverFormat("AWAY :%s\r\n", clientAway);
  332. if (join) serverFormat("JOIN :%s\r\n", join);
  333. signal(SIGINT, signalHandler);
  334. signal(SIGTERM, signalHandler);
  335. signal(SIGPIPE, SIG_IGN);
  336. signal(SIGINFO, signalHandler);
  337. signal(SIGUSR1, signalHandler);
  338. for (size_t i = 0; i < binds; ++i) {
  339. int error = listen(bind[i], 1);
  340. if (error) err(EX_IOERR, "listen");
  341. eventAdd(bind[i], NULL);
  342. }
  343. eventAdd(server, NULL);
  344. for (;;) {
  345. int nfds = poll(event.fds, event.len, -1);
  346. if (nfds < 0 && errno != EINTR) err(EX_IOERR, "poll");
  347. if (signals[SIGINT] || signals[SIGTERM]) break;
  348. if (signals[SIGINFO]) {
  349. ringInfo();
  350. signals[SIGINFO] = 0;
  351. }
  352. if (signals[SIGUSR1]) {
  353. cert = splitOpen(certSplit);
  354. priv = splitOpen(privSplit);
  355. localConfig(cert, priv);
  356. fclose(cert);
  357. fclose(priv);
  358. signals[SIGUSR1] = 0;
  359. }
  360. if (nfds < 0) continue;
  361. for (size_t i = event.len - 1; i < event.len; --i) {
  362. short revents = event.fds[i].revents;
  363. if (!revents) continue;
  364. if (event.fds[i].fd == server) {
  365. serverRecv();
  366. continue;
  367. }
  368. if (!event.clients[i]) {
  369. int fd;
  370. struct tls *tls = localAccept(&fd, event.fds[i].fd);
  371. int error = tls_handshake(tls);
  372. if (error) {
  373. warnx("tls_handshake: %s", tls_error(tls));
  374. tls_free(tls);
  375. close(fd);
  376. } else {
  377. eventAdd(fd, clientAlloc(tls));
  378. }
  379. continue;
  380. }
  381. struct Client *client = event.clients[i];
  382. if (revents & POLLOUT) clientConsume(client);
  383. if (revents & POLLIN) clientRecv(client);
  384. if (clientError(client) || revents & (POLLHUP | POLLERR)) {
  385. clientFree(client);
  386. eventRemove(i);
  387. }
  388. }
  389. for (size_t i = binds + 1; i < event.len; ++i) {
  390. assert(event.clients[i]);
  391. if (clientDiff(event.clients[i])) {
  392. event.fds[i].events |= POLLOUT;
  393. } else {
  394. event.fds[i].events &= ~POLLOUT;
  395. }
  396. }
  397. }
  398. serverFormat("QUIT :%s\r\n", quit);
  399. for (size_t i = binds + 1; i < event.len; ++i) {
  400. assert(event.clients[i]);
  401. clientFormat(event.clients[i], ":%s QUIT :%s\r\n", stateEcho(), quit);
  402. clientFormat(event.clients[i], "ERROR :Disconnecting\r\n");
  403. clientFree(event.clients[i]);
  404. close(event.fds[i].fd);
  405. }
  406. }