chiark / gitweb /
Initial commit.
[rsync-backup] / rfreezefs.c
CommitLineData
f6b4ffdc
MW
1/* -*-c-*-
2 *
3 * Freeze a file system under remote control
4 *
5 * (c) 2012 Mark Wooding
6 */
7
8/*----- Licensing notice --------------------------------------------------*
9 *
10 * This file is part of the `rsync-backup' program.
11 *
12 * rsync-backup is free software; you can redistribute it and/or modify
13 * it under the terms of the GNU General Public License as published by
14 * the Free Software Foundation; either version 2 of the License, or
15 * (at your option) any later version.
16 *
17 * rsync-backup is distributed in the hope that it will be useful,
18 * but WITHOUT ANY WARRANTY; without even the implied warranty of
19 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 * GNU General Public License for more details.
21 *
22 * You should have received a copy of the GNU General Public License along
23 * with rsync-backup; if not, write to the Free Software Foundation,
24 * Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
25 */
26
27/*----- Header files ------------------------------------------------------*/
28
29#include <assert.h>
30#include <errno.h>
31#include <limits.h>
32#include <signal.h>
33#include <stdarg.h>
34#include <stdio.h>
35#include <string.h>
36#include <stdlib.h>
37#include <time.h>
38
39#include <sys/types.h>
40#include <sys/time.h>
41#include <sys/select.h>
42#include <unistd.h>
43#include <fcntl.h>
44#include <sys/ioctl.h>
45
46#include <linux/fs.h>
47
48#include <sys/socket.h>
49#include <arpa/inet.h>
50#include <netinet/in.h>
51#include <netdb.h>
52
53#include <mLib/alloc.h>
54#include <mLib/dstr.h>
55#include <mLib/base64.h>
56#include <mLib/fdflags.h>
57#include <mLib/mdwopt.h>
58#include <mLib/quis.h>
59#include <mLib/report.h>
60#include <mLib/sub.h>
61#include <mLib/tv.h>
62
63/*----- Magic constants ---------------------------------------------------*/
64
65#define COOKIESZ 16 /* Size of authentication cookie */
66#define TO_CONNECT 30 /* Timeout for incoming connection */
67#define TO_KEEPALIVE 60 /* Timeout between keepalives */
68
69/*----- Utility functions -------------------------------------------------*/
70
71static int getuint(const char *p, const char *q)
72{
73 unsigned long i;
74 int e = errno;
75 char *qq;
76
77 if (!q) q = p + strlen(p);
78 errno = 0;
79 i = strtoul(p, &qq, 0);
80 if (errno || qq < q || i > INT_MAX)
81 die(1, "invalid integer `%s'", p);
82 errno = e;
83 return ((int)i);
84}
85
86#ifdef DEBUG
87# define D(x) x
88#else
89# define D(x)
90#endif
91
92/*----- Token management --------------------------------------------------*/
93
94struct token {
95 const char *label;
96 char tok[(COOKIESZ + 2)*4/3 + 1];
97};
98
99#define TOKENS(_) \
100 _(FREEZE) \
101 _(FROZEN) \
102 _(KEEPALIVE) \
103 _(THAW) \
104 _(THAWED)
105
106enum {
107#define ENUM(tok) T_##tok,
108 TOKENS(ENUM)
109#undef ENUM
110 T_LIMIT
111};
112
113enum {
114#define MASK(tok) TF_##tok = 1u << T_##tok,
115 TOKENS(MASK)
116#undef ENUM
117 TF_ALL = (1u << T_LIMIT) - 1u
118};
119
120static struct token toktab[] = {
121#define INIT(tok) { #tok },
122 TOKENS(INIT)
123#undef INIT
124 { 0 }
125};
126
127static void inittoks(void)
128{
129 static struct token *t, *tt;
130 unsigned char buf[COOKIESZ];
131 int fd;
132 ssize_t n;
133 base64_ctx bc;
134 dstr d = DSTR_INIT;
135
136 if ((fd = open("/dev/urandom", O_RDONLY)) < 0)
137 die(2, "open (urandom): %s", strerror(errno));
138
139 for (t = toktab; t->label; t++) {
140 again:
141 n = read(fd, buf, COOKIESZ);
142 if (n < 0) die(2, "read (urandom): %s", strerror(errno));
143 else if (n < COOKIESZ) die(2, "read (urandom): short read");
144 base64_init(&bc);
145 base64_encode(&bc, buf, COOKIESZ, &d);
146 base64_encode(&bc, 0, 0, &d);
147 dstr_putz(&d);
148
149 for (tt = toktab; tt < t; tt++) {
150 if (strcmp(d.buf, tt->tok) == 0)
151 goto again;
152 }
153
154 assert(d.len < sizeof(t->tok));
155 memcpy(t->tok, d.buf, d.len + 1);
156 dstr_reset(&d);
157 }
158}
159
160struct tokmatch {
161 unsigned tf; /* Possible token matches */
162 size_t o; /* Offset into token string */
163 unsigned f; /* Flags */
164#define TMF_CR 1u /* Seen trailing carriage-return */
165};
166
167static void tokmatch_init(struct tokmatch *tm)
168 { tm->tf = TF_ALL; tm->o = 0; tm->f = 0; }
169
170static int tokmatch_update(struct tokmatch *tm, int ch)
171{
172 const struct token *t;
173 unsigned tf;
174
175 switch (ch) {
176 case '\n':
177 for (t = toktab, tf = 1; t->label; t++, tf <<= 1) {
178 if ((tm->tf & tf) && !t->tok[tm->o])
179 return (tf);
180 }
181 return (-1);
182 case '\r':
183 for (t = toktab, tf = 1; t->label; t++, tf <<= 1) {
184 if ((tm->tf & tf) && !t->tok[tm->o] && !(tm->f & TMF_CR))
185 tm->f |= TMF_CR;
186 else
187 tm->tf &= ~tf;
188 }
189 break;
190 default:
191 for (t = toktab, tf = 1; t->label; t++, tf <<= 1) {
192 if ((tm->tf & tf) && ch != t->tok[tm->o])
193 tm->tf &= ~tf;
194 }
195 tm->o++;
196 break;
197 }
198 return (0);
199}
200
201static int writetok(unsigned i, int fd)
202{
203 static const char nl = '\n';
204 const struct token *t = &toktab[i];
205 size_t n = strlen(t->tok);
206
207 errno = EIO;
208 if (write(fd, t->tok, n) < n ||
209 write(fd, &nl, 1) < 1)
210 return (-1);
211 return (0);
212}
213
214/*----- Data structures ---------------------------------------------------*/
215
216struct client {
217 struct client *next; /* Links in the client chain */
218 int fd; /* File descriptor for socket */
219 struct tokmatch tm; /* Token matching context */
220};
221
222/*----- Static variables --------------------------------------------------*/
223
224static int *fs; /* File descriptors for targets */
225static char **fsname; /* File system names */
226static size_t nfs; /* Number of descriptors */
227
228/*----- Cleanup -----------------------------------------------------------*/
229
230#define EOM ((char *)0)
231static void emerg(const char *msg,...)
232{
233 va_list ap;
234
235#define MSG(m) \
236 do { const char *m_ = m; if (write(2, m_, strlen(m_))); } while (0)
237
238 va_start(ap, msg);
239 MSG(QUIS); MSG(": ");
240 do {
241 MSG(msg);
242 msg = va_arg(ap, const char *);
243 } while (msg != EOM);
244 MSG("\n");
245
246#undef MSG
247}
248
249static void partial_cleanup(size_t n)
250{
251 int i;
252 int bad = 0;
253
254 for (i = 0; i < nfs; i++) {
255 if (fs[i] == -1)
256 emerg("not really thawing ", fsname[i], EOM);
257 else if (fs[i] != -2) {
258 if (ioctl(fs[i], FITHAW, 0)) {
259 emerg("VERY BAD! failed to thaw ",
260 fsname[i], ": ", strerror(errno), EOM);
261 bad = 1;
262 }
263 close(fs[i]);
264 }
265 fs[i] = -2;
266 }
267 if (bad) _exit(112);
268}
269
270static void cleanup(void) { partial_cleanup(nfs); }
271
272static int sigcatch[] = {
273 SIGINT, SIGQUIT, SIGTERM, SIGHUP, SIGALRM,
274 SIGILL, SIGSEGV, SIGBUS, SIGFPE, SIGABRT
275};
276
277static void sigmumble(int sig)
278{
279 sigset_t ss;
280
281 cleanup();
282 emerg(strsignal(sig), 0);
283
284 signal(sig, SIG_DFL);
285 sigemptyset(&ss); sigaddset(&ss, sig);
286 sigprocmask(SIG_UNBLOCK, &ss, 0);
287 raise(sig);
288 _exit(4);
289}
290
291/*----- Help functions ----------------------------------------------------*/
292
293static void version(FILE *fp)
294 { pquis(fp, "$, " PACKAGE " version " VERSION "\n"); }
295static void usage(FILE *fp)
296 { pquis(fp, "Usage: $ [-n] [-a ADDR] [-p LOPORT[-HIPORT]] FILSYS ...\n"); }
297
298static void help(FILE *fp)
299{
300 version(fp); putc('\n', fp);
301 usage(fp);
302 fputs("\n\
303Freezes a filesystem temporarily, with some measure of safety.\n\
304\n\
305The program listens for connections on a TCP port, and prints a line\n\
306\n\
307 PORT COOKIE\n\
308\n\
309to standard output. You must connect to this PORT and send the COOKIE\n\
310followed by a newline within a short period of time. The filesystems\n\
311will then be frozen, and `OK' written to the connection. In order to\n\
312keep the file system frozen, you must keep the connection open, and\n\
313feed data into it. If the connection closes, or no data is received\n\
314within a set period of time, or the program receives one of a variety\n\
315of signals or otherwise becomes unhappy, the filesystems are thawed again.\n\
316\n\
317Options:\n\
318\n\
319-h, --help Print this help text.\n\
320-v, --version Print the program version number.\n\
321-u, --usage Print a short usage message.\n\
322\n\
323-a, --address=ADDR Listen only on ADDR.\n\
324-n, --not-really Don't really freeze or thaw filesystems.\n\
325-p, --port-range=LO[-HI] Select a port number between LO and HI.\n\
326 If HI is omitted, choose only LO.\n\
327", fp);
328}
329
330/*----- Main program ------------------------------------------------------*/
331
332int main(int argc, char *argv[])
333{
334 char buf[256];
335 int loport = -1, hiport = -1;
336 int sk, fd, maxfd;
337 struct sockaddr_in sin;
338 socklen_t sasz;
339 struct hostent *h;
340 const char *p, *q;
341 struct timeval now, when, delta;
342 struct client *clients = 0, *c, **cc;
343 const struct token *t;
344 struct tokmatch tm;
345 fd_set fdin;
346 int i;
347 ssize_t n;
348 unsigned f = 0;
349#define f_bogus 0x01u
350#define f_notreally 0x02u
351
352 ego(argv[0]);
353 sub_init();
354
355 /* --- Partially initialize the socket address --- */
356
357 sin.sin_family = AF_INET;
358 sin.sin_addr.s_addr = INADDR_ANY;
359 sin.sin_port = 0;
360
361 /* --- Parse the command line --- */
362
363 for (;;) {
364 static struct option opts[] = {
365 { "help", 0, 0, 'h' },
366 { "version", 0, 0, 'v' },
367 { "usage", 0, 0, 'u' },
368 { "address", OPTF_ARGREQ, 0, 'a' },
369 { "not-really", 0, 0, 'n' },
370 { "port-range", OPTF_ARGREQ, 0, 'p' },
371 { 0, 0, 0, 0 }
372 };
373
374 if ((i = mdwopt(argc, argv, "hvua:np:", opts, 0, 0, 0)) < 0) break;
375 switch (i) {
376 case 'h': help(stdout); exit(0);
377 case 'v': version(stdout); exit(0);
378 case 'u': usage(stdout); exit(0);
379 case 'a':
380 if ((h = gethostbyname(optarg)) == 0) {
381 die(1, "failed to resolve address `%s': %s",
382 optarg, hstrerror(h_errno));
383 }
384 if (h->h_addrtype != AF_INET)
385 die(1, "unexpected address type resolving `%s'", optarg);
386 assert(h->h_length == sizeof(sin.sin_addr));
387 memcpy(&sin.sin_addr, h->h_addr, sizeof(sin.sin_addr));
388 break;
389 case 'n': f |= f_notreally; break;
390 case 'p':
391 if ((p = strchr(optarg, '-')) == 0)
392 loport = hiport = getuint(optarg, 0);
393 else {
394 loport = getuint(optarg, p);
395 hiport = getuint(p + 1, 0);
396 }
397 break;
398 default: f |= f_bogus; break;
399 }
400 }
401 if (f & f_bogus) { usage(stderr); exit(1); }
402 if (optind >= argc) { usage(stderr); exit(1); }
403
404 /* --- Open the file systems --- */
405
406 nfs = argc - optind;
407 fsname = &argv[optind];
408 fs = xmalloc(nfs*sizeof(*fs));
409 for (i = 0; i < nfs; i++) {
410 if ((fs[i] = open(fsname[i], O_RDONLY)) < 0)
411 die(2, "open (%s): %s", fsname[i], strerror(errno));
412 }
413
414 if (f & f_notreally) {
415 for (i = 0; i < nfs; i++) {
416 close(fs[i]);
417 fs[i] = -1;
418 }
419 }
420
421 /* --- Generate random tokens --- */
422
423 inittoks();
424
425 /* --- Create the listening socket --- */
426
427 if ((sk = socket(PF_INET, SOCK_STREAM, 0)) < 0)
428 die(2, "socket: %s", strerror(errno));
429 i = 1;
430 if (setsockopt(sk, SOL_SOCKET, SO_REUSEADDR, &i, sizeof(i)))
431 die(2, "setsockopt (reuseaddr): %s", strerror(errno));
432 if (fdflags(sk, O_NONBLOCK, O_NONBLOCK, FD_CLOEXEC, FD_CLOEXEC))
433 die(2, "fdflags: %s", strerror(errno));
434 if (loport < 0 || loport == hiport) {
435 if (loport >= 0) sin.sin_port = htons(loport);
436 if (bind(sk, (struct sockaddr *)&sin, sizeof(sin)))
437 die(2, "bind: %s", strerror(errno));
438 } else if (hiport != loport) {
439 for (i = loport; i <= hiport; i++) {
440 sin.sin_port = htons(i);
441 if (bind(sk, (struct sockaddr *)&sin, sizeof(sin)) >= 0) break;
442 else if (errno != EADDRINUSE)
443 die(2, "bind: %s", strerror(errno));
444 }
445 if (i > hiport) die(2, "bind: all ports in use");
446 }
447 if (listen(sk, 5)) die(2, "listen: %s", strerror(errno));
448
449 /* --- Tell the caller how to connect to us, and start the timer --- */
450
451 sasz = sizeof(sin);
452 if (getsockname(sk, (struct sockaddr *)&sin, &sasz))
453 die(2, "getsockname (listen): %s", strerror(errno));
454 printf("PORT %d\n", ntohs(sin.sin_port));
455 for (t = toktab; t->label; t++)
456 printf("TOKEN %s %s\n", t->label, t->tok);
457 printf("READY\n");
458 if (fflush(stdout) || ferror(stdout))
459 die(2, "write (stdout, rubric): %s", strerror(errno));
460 gettimeofday(&now, 0); TV_ADDL(&when, &now, TO_CONNECT, 0);
461
462 /* --- Collect incoming connections, and check for the cookie --- *
463 *
464 * This is the tricky part.
465 */
466
467 for (;;) {
468 FD_ZERO(&fdin);
469 FD_SET(sk, &fdin);
470 maxfd = sk;
471 for (c = clients; c; c = c->next) {
472 FD_SET(c->fd, &fdin);
473 if (c->fd > maxfd) maxfd = c->fd;
474 }
475 TV_SUB(&delta, &when, &now);
476 if (select(maxfd + 1, &fdin, 0, 0, &delta) < 0)
477 die(2, "select (accept): %s", strerror(errno));
478 gettimeofday(&now, 0);
479
480 if (TV_CMP(&now, >=, &when)) die(3, "timeout (accept)");
481
482 if (FD_ISSET(sk, &fdin)) {
483 sasz = sizeof(sin);
484 fd = accept(sk, (struct sockaddr *)&sin, &sasz);
485 if (fd >= 0) {
486 if (fdflags(fd, O_NONBLOCK, O_NONBLOCK, FD_CLOEXEC, FD_CLOEXEC) < 0)
487 die(2, "fdflags: %s", strerror(errno));
488 c = CREATE(struct client);
489 c->next = clients; c->fd = fd; tokmatch_init(&c->tm);
490 clients = c;
491 }
492#ifdef DEBUG
493 else if (errno != EAGAIN)
494 moan("accept: %s", strerror(errno));
495#endif
496 }
497
498 for (cc = &clients; *cc;) {
499 c = *cc;
500 if (!FD_ISSET(c->fd, &fdin)) goto next_client;
501 n = read(c->fd, buf, sizeof(buf));
502 if (!n) goto disconn;
503 else if (n < 0) {
504 if (errno == EAGAIN) goto next_client;
505 D( moan("read (client; auth): %s", strerror(errno)); )
506 goto disconn;
507 } else {
508 for (p = buf, q = p + n; p < q; p++) {
509 switch (tokmatch_update(&c->tm, *p)) {
510 case 0: break;
511 case TF_FREEZE: goto connected;
512 default:
513 D( moan("bad token from client"); )
514 goto disconn;
515 }
516 }
517 }
518
519 next_client:
520 cc = &c->next;
521 continue;
522
523 disconn:
524 close(c->fd);
525 *cc = c->next;
526 DESTROY(c);
527 continue;
528 }
529 }
530
531connected:
532 close(sk); sk = c->fd;
533 while (clients) {
534 if (clients->fd != sk) close(clients->fd);
535 c = clients->next;
536 DESTROY(clients);
537 clients = c;
538 }
539
540 /* --- Establish signal handlers --- *
541 *
542 * Hopefully this will prevent bad things happening if we have an accident.
543 */
544
545 for (i = 0; i < sizeof(sigcatch)/sizeof(sigcatch[0]); i++) {
546 if (signal(sigcatch[i], sigmumble) == SIG_ERR)
547 die(2, "signal (%d): %s", i, strerror(errno));
548 }
549 atexit(cleanup);
550
551 /* --- Prevent the OOM killer from clobbering us --- */
552
553 if ((fd = open("/proc/self/oom_adj", O_WRONLY)) < 0 ||
554 write(fd, "-17\n", 4) < 4 ||
555 close(fd))
556 die(2, "set oom_adj: %s", strerror(errno));
557
558 /* --- Actually freeze the filesystem --- */
559
560 for (i = 0; i < nfs; i++) {
561 if (fs[i] == -1)
562 moan("not really freezing %s", fsname[i]);
563 else {
564 if (ioctl(fs[i], FIFREEZE, 0) < 0) {
565 partial_cleanup(i);
566 die(2, "ioctl (freeze %s): %s", fsname[i], strerror(errno));
567 }
568 }
569 }
570 if (writetok(T_FROZEN, sk)) {
571 cleanup();
572 die(2, "write (frozen): %s", strerror(errno));
573 }
574
575 /* --- Now wait for the other end to detach --- */
576
577 tokmatch_init(&tm);
578 TV_ADDL(&when, &now, TO_KEEPALIVE, 0);
579 for (p++; p < q; p++) {
580 switch (tokmatch_update(&tm, *p)) {
581 case 0: break;
582 case TF_KEEPALIVE: tokmatch_init(&tm); break;
583 case TF_THAW: goto done;
584 default: cleanup(); die(3, "unknown token (keepalive)");
585 }
586 }
587 for (;;) {
588 FD_ZERO(&fdin);
589 FD_SET(sk, &fdin);
590 TV_SUB(&delta, &when, &now);
591 if (select(sk + 1, &fdin, 0, 0, &delta) < 0) {
592 cleanup();
593 die(2, "select (keepalive): %s", strerror(errno));
594 }
595
596 gettimeofday(&now, 0);
597 if (TV_CMP(&now, >, &when)) {
598 cleanup(); die(3, "timeout (keepalive)");
599 }
600 if (FD_ISSET(sk, &fdin)) {
601 n = read(sk, buf, sizeof(buf));
602 if (!n) { cleanup(); die(3, "end-of-file (keepalive)"); }
603 else if (n < 0) {
604 if (errno == EAGAIN) ;
605 else {
606 cleanup();
607 die(2, "read (client, keepalive): %s", strerror(errno));
608 }
609 } else {
610 for (p = buf, q = p + n; p < q; p++) {
611 switch (tokmatch_update(&tm, *p)) {
612 case 0: break;
613 case TF_KEEPALIVE:
614 TV_ADDL(&when, &now, TO_KEEPALIVE, 0);
615 tokmatch_init(&tm);
616 break;
617 case TF_THAW:
618 goto done;
619 default:
620 cleanup();
621 die(3, "unknown token (keepalive)");
622 }
623 }
624 }
625 }
626 }
627
628done:
629 cleanup();
630 if (writetok(T_THAWED, sk))
631 die(2, "write (thaw): %s", strerror(errno));
632 close(sk);
633 return (0);
634}
635
636/*----- That's all, folks -------------------------------------------------*/