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