chiark / gitweb /
Makefile.am: Stupid workaround for new Automake pettiness.
[distorted-backup] / rfreezefs.c
1 /* -*-c-*-
2  *
3  * Freeze a file system under remote control
4  *
5  * (c) 2011 Mark Wooding
6  */
7
8 /*----- Licensing notice --------------------------------------------------*
9  *
10  * This file is part of the distorted.org.uk backup suite.
11  *
12  * distorted-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  * distorted-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 distorted-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
71 static 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
94 struct 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
106 enum {
107 #define ENUM(tok) T_##tok,
108   TOKENS(ENUM)
109 #undef ENUM
110   T_LIMIT
111 };
112
113 enum {
114 #define MASK(tok) TF_##tok = 1u << T_##tok,
115   TOKENS(MASK)
116 #undef ENUM
117   TF_ALL = (1u << T_LIMIT) - 1u
118 };
119
120 static struct token toktab[] = {
121 #define INIT(tok) { #tok },
122   TOKENS(INIT)
123 #undef INIT
124   { 0 }
125 };
126
127 static 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
160 struct 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
167 static void tokmatch_init(struct tokmatch *tm)
168   { tm->tf = TF_ALL; tm->o = 0; tm->f = 0; }
169
170 static 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
201 static 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
216 struct 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
224 static int *fs;                         /* File descriptors for targets */
225 static char **fsname;                   /* File system names */
226 static size_t nfs;                      /* Number of descriptors */
227
228 /*----- Cleanup -----------------------------------------------------------*/
229
230 #define EOM ((char *)0)
231 static 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
249 static 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
270 static void cleanup(void) { partial_cleanup(nfs); }
271
272 static int sigcatch[] = {
273   SIGINT, SIGQUIT, SIGTERM, SIGHUP, SIGALRM,
274   SIGILL, SIGSEGV, SIGBUS, SIGFPE, SIGABRT
275 };
276
277 static 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
293 static void version(FILE *fp) { pquis(fp, "$, version " VERSION "\n"); }
294 static void usage(FILE *fp)
295   { pquis(fp, "Usage: $ [-n] [-a ADDR] [-p LOPORT[-HIPORT]] FILSYS ...\n"); }
296
297 static void help(FILE *fp)
298 {
299   version(fp); putc('\n', fp);
300   usage(fp);
301   fputs("\n\
302 Freezes a filesystem temporarily, with some measure of safety.\n\
303 \n\
304 The program listens for connections on a TCP port, and prints a line\n\
305 \n\
306         PORT COOKIE\n\
307 \n\
308 to standard output.  You must connect to this PORT and send the COOKIE\n\
309 followed by a newline within a short period of time.  The filesystems\n\
310 will then be frozen, and `OK' written to the connection.  In order to\n\
311 keep the file system frozen, you must keep the connection open, and\n\
312 feed data into it.  If the connection closes, or no data is received\n\
313 within a set period of time, or the program receives one of a variety\n\
314 of signals or otherwise becomes unhappy, the filesystems are thawed again.\n\
315 \n\
316 Options:\n\
317 \n\
318 -h, --help                      Print this help text.\n\
319 -v, --version                   Print the program version number.\n\
320 -u, --usage                     Print a short usage message.\n\
321 \n\
322 -a, --address=ADDR              Listen only on ADDR.\n\
323 -n, --not-really                Don't really freeze or thaw filesystems.\n\
324 -p, --port-range=LO[-HI]        Select a port number between LO and HI.\n\
325                                   If HI is omitted, choose only LO.\n\
326 ", fp);
327 }
328
329 /*----- Main program ------------------------------------------------------*/
330
331 int main(int argc, char *argv[])
332 {
333   char buf[256];
334   int loport = -1, hiport = -1;
335   int sk, fd, maxfd;
336   struct sockaddr_in sin;
337   socklen_t sasz;
338   struct hostent *h;
339   const char *p, *q;
340   struct timeval now, when, delta;
341   struct client *clients = 0, *c, **cc;
342   const struct token *t;
343   struct tokmatch tm;
344   fd_set fdin;
345   int i;
346   ssize_t n;
347   unsigned f = 0;
348 #define f_bogus 0x01u
349 #define f_notreally 0x02u
350
351   ego(argv[0]);
352   sub_init();
353
354   /* --- Partially initialize the socket address --- */
355
356   sin.sin_family = AF_INET;
357   sin.sin_addr.s_addr = INADDR_ANY;
358   sin.sin_port = 0;
359
360   /* --- Parse the command line --- */
361
362   for (;;) {
363     static struct option opts[] = {
364       { "help",         0,              0,      'h' },
365       { "version",      0,              0,      'v' },
366       { "usage",        0,              0,      'u' },
367       { "address",      OPTF_ARGREQ,    0,      'a' },
368       { "not-really",   0,              0,      'n' },
369       { "port-range",   OPTF_ARGREQ,    0,      'p' },
370       { 0,              0,              0,      0 }
371     };
372
373     if ((i = mdwopt(argc, argv, "hvua:np:", opts, 0, 0, 0)) < 0) break;
374     switch (i) {
375       case 'h': help(stdout); exit(0);
376       case 'v': version(stdout); exit(0);
377       case 'u': usage(stdout); exit(0);
378       case 'a':
379         if ((h = gethostbyname(optarg)) == 0) {
380           die(1, "failed to resolve address `%s': %s",
381               optarg, hstrerror(h_errno));
382         }
383         if (h->h_addrtype != AF_INET)
384           die(1, "unexpected address type resolving `%s'", optarg);
385         assert(h->h_length == sizeof(sin.sin_addr));
386         memcpy(&sin.sin_addr, h->h_addr, sizeof(sin.sin_addr));
387         break;
388       case 'n': f |= f_notreally; break;
389       case 'p':
390         if ((p = strchr(optarg, '-')) == 0)
391           loport = hiport = getuint(optarg, 0);
392         else {
393           loport = getuint(optarg, p);
394           hiport = getuint(p + 1, 0);
395         }
396         break;
397       default: f |= f_bogus; break;
398     }
399   }
400   if (f & f_bogus) { usage(stderr); exit(1); }
401   if (optind >= argc) { usage(stderr); exit(1); }
402
403   /* --- Open the file systems --- */
404
405   nfs = argc - optind;
406   fsname = &argv[optind];
407   fs = xmalloc(nfs*sizeof(*fs));
408   for (i = 0; i < nfs; i++) {
409     if ((fs[i] = open(fsname[i], O_RDONLY)) < 0)
410       die(2, "open (%s): %s", fsname[i], strerror(errno));
411   }
412
413   if (f & f_notreally) {
414     for (i = 0; i < nfs; i++) {
415       close(fs[i]);
416       fs[i] = -1;
417     }
418   }
419
420   /* --- Generate random tokens --- */
421
422   inittoks();
423
424   /* --- Create the listening socket --- */
425
426   if ((sk = socket(PF_INET, SOCK_STREAM, 0)) < 0)
427     die(2, "socket: %s", strerror(errno));
428   i = 1;
429   if (setsockopt(sk, SOL_SOCKET, SO_REUSEADDR, &i, sizeof(i)))
430     die(2, "setsockopt (reuseaddr): %s", strerror(errno));
431   if (fdflags(sk, O_NONBLOCK, O_NONBLOCK, FD_CLOEXEC, FD_CLOEXEC))
432     die(2, "fdflags: %s", strerror(errno));
433   if (loport < 0 || loport == hiport) {
434     if (loport >= 0) sin.sin_port = htons(loport);
435     if (bind(sk, (struct sockaddr *)&sin, sizeof(sin)))
436       die(2, "bind: %s", strerror(errno));
437   } else if (hiport != loport) {
438     for (i = loport; i <= hiport; i++) {
439       sin.sin_port = htons(i);
440       if (bind(sk, (struct sockaddr *)&sin, sizeof(sin)) >= 0) break;
441       else if (errno != EADDRINUSE)
442         die(2, "bind: %s", strerror(errno));
443     }
444     if (i > hiport) die(2, "bind: all ports in use");
445   }
446   if (listen(sk, 5)) die(2, "listen: %s", strerror(errno));
447
448   /* --- Tell the caller how to connect to us, and start the timer --- */
449
450   sasz = sizeof(sin);
451   if (getsockname(sk, (struct sockaddr *)&sin, &sasz))
452     die(2, "getsockname (listen): %s", strerror(errno));
453   printf("PORT %d\n", ntohs(sin.sin_port));
454   for (t = toktab; t->label; t++)
455     printf("TOKEN %s %s\n", t->label, t->tok);
456   printf("READY\n");
457   if (fflush(stdout) || ferror(stdout))
458     die(2, "write (stdout, rubric): %s", strerror(errno));
459   gettimeofday(&now, 0); TV_ADDL(&when, &now, TO_CONNECT, 0);
460
461   /* --- Collect incoming connections, and check for the cookie --- *
462    *
463    * This is the tricky part.
464    */
465
466   for (;;) {
467     FD_ZERO(&fdin);
468     FD_SET(sk, &fdin);
469     maxfd = sk;
470     for (c = clients; c; c = c->next) {
471       FD_SET(c->fd, &fdin);
472       if (c->fd > maxfd) maxfd = c->fd;
473     }
474     TV_SUB(&delta, &when, &now);
475     if (select(maxfd + 1, &fdin, 0, 0, &delta) < 0)
476       die(2, "select (accept): %s", strerror(errno));
477     gettimeofday(&now, 0);
478
479     if (TV_CMP(&now, >=, &when)) die(3, "timeout (accept)");
480
481     if (FD_ISSET(sk, &fdin)) {
482       sasz = sizeof(sin);
483       fd = accept(sk, (struct sockaddr *)&sin, &sasz);
484       if (fd >= 0) {
485         if (fdflags(fd, O_NONBLOCK, O_NONBLOCK, FD_CLOEXEC, FD_CLOEXEC) < 0)
486           die(2, "fdflags: %s", strerror(errno));
487         c = CREATE(struct client);
488         c->next = clients; c->fd = fd; tokmatch_init(&c->tm);
489         clients = c;
490       }
491 #ifdef DEBUG
492       else if (errno != EAGAIN)
493         moan("accept: %s", strerror(errno));
494 #endif
495     }
496
497     for (cc = &clients; *cc;) {
498       c = *cc;
499       if (!FD_ISSET(c->fd, &fdin)) goto next_client;
500       n = read(c->fd, buf, sizeof(buf));
501       if (!n) goto disconn;
502       else if (n < 0) {
503         if (errno == EAGAIN) goto next_client;
504         D( moan("read (client; auth): %s", strerror(errno)); )
505         goto disconn;
506       } else {
507         for (p = buf, q = p + n; p < q; p++) {
508           switch (tokmatch_update(&c->tm, *p)) {
509             case 0: break;
510             case TF_FREEZE: goto connected;
511             default:
512               D( moan("bad token from client"); )
513               goto disconn;
514           }
515         }
516       }
517
518     next_client:
519       cc = &c->next;
520       continue;
521
522     disconn:
523       close(c->fd);
524       *cc = c->next;
525       DESTROY(c);
526       continue;
527     }
528   }
529
530 connected:
531   close(sk); sk = fd;
532   while (clients) {
533     if (clients->fd != sk) close(clients->fd);
534     c = clients->next;
535     DESTROY(clients);
536     clients = c;
537   }
538
539   /* --- Establish signal handlers --- *
540    *
541    * Hopefully this will prevent bad things happening if we have an accident.
542    */
543
544   for (i = 0; i < sizeof(sigcatch)/sizeof(sigcatch[0]); i++) {
545     if (signal(sigcatch[i], sigmumble) == SIG_ERR)
546       die(2, "signal (%d): %s", i, strerror(errno));
547   }
548   atexit(cleanup);
549
550   /* --- Prevent the OOM killer from clobbering us --- */
551
552   if ((fd = open("/proc/self/oom_adj", O_WRONLY)) < 0 ||
553       write(fd, "-17\n", 4) < 4 ||
554       close(fd))
555     die(2, "set oom_adj: %s", strerror(errno));
556
557   /* --- Actually freeze the filesystem --- */
558
559   for (i = 0; i < nfs; i++) {
560     if (fs[i] == -1)
561       moan("not really freezing %s", fsname[i]);
562     else {
563       if (ioctl(fs[i], FIFREEZE, 0) < 0) {
564         partial_cleanup(i);
565         die(2, "ioctl (freeze %s): %s", fsname[i], strerror(errno));
566       }
567     }
568   }
569   if (writetok(T_FROZEN, sk)) {
570     cleanup();
571     die(2, "write (frozen): %s", strerror(errno));
572   }
573
574   /* --- Now wait for the other end to detach --- */
575
576   tokmatch_init(&tm);
577   TV_ADDL(&when, &now, TO_KEEPALIVE, 0);
578   for (p++; p < q; p++) {
579     switch (tokmatch_update(&tm, *p)) {
580       case 0: break;
581       case TF_KEEPALIVE: tokmatch_init(&tm); break;
582       case TF_THAW: goto done;
583       default: cleanup(); die(3, "unknown token (keepalive)");
584     }
585   }
586   for (;;) {
587     FD_ZERO(&fdin);
588     FD_SET(sk, &fdin);
589     TV_SUB(&delta, &when, &now);
590     if (select(sk + 1, &fdin, 0, 0, &delta) < 0) {
591       cleanup();
592       die(2, "select (keepalive): %s", strerror(errno));
593     }
594
595     gettimeofday(&now, 0);
596     if (TV_CMP(&now, >, &when)) {
597       cleanup(); die(3, "timeout (keepalive)");
598     }
599     if (FD_ISSET(sk, &fdin)) {
600       n = read(sk, buf, sizeof(buf));
601       if (!n) { cleanup(); die(3, "end-of-file (keepalive)"); }
602       else if (n < 0) {
603         if (errno == EAGAIN) ;
604         else {
605           cleanup();
606           die(2, "read (client, keepalive): %s", strerror(errno));
607         }
608       } else {
609         for (p = buf, q = p + n; p < q; p++) {
610           switch (tokmatch_update(&tm, *p)) {
611             case 0: break;
612             case TF_KEEPALIVE:
613               TV_ADDL(&when, &now, TO_KEEPALIVE, 0);
614               tokmatch_init(&tm);
615               break;
616             case TF_THAW:
617               goto done;
618             default:
619               cleanup();
620               die(3, "unknown token (keepalive)");
621           }
622         }
623       }
624     }
625   }
626
627 done:
628   cleanup();
629   if (writetok(T_THAWED, sk))
630     die(2, "write (thaw): %s", strerror(errno));
631   close(sk);
632   return (0);
633 }
634
635 /*----- That's all, folks -------------------------------------------------*/