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