3 * Freeze a file system under remote control
5 * (c) 2011 Mark Wooding
8 /*----- Licensing notice --------------------------------------------------*
10 * This file is part of the distorted.org.uk backup suite.
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.
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.
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.
27 /*----- Header files ------------------------------------------------------*/
39 #include <sys/types.h>
41 #include <sys/select.h>
44 #include <sys/ioctl.h>
48 #include <sys/socket.h>
49 #include <arpa/inet.h>
50 #include <netinet/in.h>
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>
63 /*----- Magic constants ---------------------------------------------------*/
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 */
69 /*----- Utility functions -------------------------------------------------*/
71 static int getuint(const char *p, const char *q)
77 if (!q) q = p + strlen(p);
79 i = strtoul(p, &qq, 0);
80 if (errno || qq < q || i > INT_MAX)
81 die(1, "invalid integer `%s'", p);
92 /*----- Token management --------------------------------------------------*/
96 char tok[(COOKIESZ + 2)*4/3 + 1];
107 #define ENUM(tok) T_##tok,
114 #define MASK(tok) TF_##tok = 1u << T_##tok,
117 TF_ALL = (1u << T_LIMIT) - 1u
120 static struct token toktab[] = {
121 #define INIT(tok) { #tok },
127 static void inittoks(void)
129 static struct token *t, *tt;
130 unsigned char buf[COOKIESZ];
136 if ((fd = open("/dev/urandom", O_RDONLY)) < 0)
137 die(2, "open (urandom): %s", strerror(errno));
139 for (t = toktab; t->label; t++) {
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");
145 base64_encode(&bc, buf, COOKIESZ, &d);
146 base64_encode(&bc, 0, 0, &d);
149 for (tt = toktab; tt < t; tt++) {
150 if (strcmp(d.buf, tt->tok) == 0)
154 assert(d.len < sizeof(t->tok));
155 memcpy(t->tok, d.buf, d.len + 1);
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 */
167 static void tokmatch_init(struct tokmatch *tm)
168 { tm->tf = TF_ALL; tm->o = 0; tm->f = 0; }
170 static int tokmatch_update(struct tokmatch *tm, int ch)
172 const struct token *t;
177 for (t = toktab, tf = 1; t->label; t++, tf <<= 1) {
178 if ((tm->tf & tf) && !t->tok[tm->o])
183 for (t = toktab, tf = 1; t->label; t++, tf <<= 1) {
184 if ((tm->tf & tf) && !t->tok[tm->o] && !(tm->f & TMF_CR))
191 for (t = toktab, tf = 1; t->label; t++, tf <<= 1) {
192 if ((tm->tf & tf) && ch != t->tok[tm->o])
201 static int writetok(unsigned i, int fd)
203 static const char nl = '\n';
204 const struct token *t = &toktab[i];
205 size_t n = strlen(t->tok);
208 if (write(fd, t->tok, n) < n ||
209 write(fd, &nl, 1) < 1)
214 /*----- Data structures ---------------------------------------------------*/
217 struct client *next; /* Links in the client chain */
218 int fd; /* File descriptor for socket */
219 struct tokmatch tm; /* Token matching context */
222 /*----- Static variables --------------------------------------------------*/
224 static int *fs; /* File descriptors for targets */
225 static char **fsname; /* File system names */
226 static size_t nfs; /* Number of descriptors */
228 /*----- Cleanup -----------------------------------------------------------*/
230 #define EOM ((char *)0)
231 static void emerg(const char *msg,...)
236 do { const char *m_ = m; if (write(2, m_, strlen(m_))); } while (0)
239 MSG(QUIS); MSG(": ");
242 msg = va_arg(ap, const char *);
243 } while (msg != EOM);
249 static void partial_cleanup(size_t n)
254 for (i = 0; i < nfs; i++) {
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);
270 static void cleanup(void) { partial_cleanup(nfs); }
272 static int sigcatch[] = {
273 SIGINT, SIGQUIT, SIGTERM, SIGHUP, SIGALRM,
274 SIGILL, SIGSEGV, SIGBUS, SIGFPE, SIGABRT
277 static void sigmumble(int sig)
282 emerg(strsignal(sig), 0);
284 signal(sig, SIG_DFL);
285 sigemptyset(&ss); sigaddset(&ss, sig);
286 sigprocmask(SIG_UNBLOCK, &ss, 0);
291 /*----- Help functions ----------------------------------------------------*/
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"); }
297 static void help(FILE *fp)
299 version(fp); putc('\n', fp);
302 Freezes a filesystem temporarily, with some measure of safety.\n\
304 The program listens for connections on a TCP port, and prints a line\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\
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\
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\
329 /*----- Main program ------------------------------------------------------*/
331 int main(int argc, char *argv[])
334 int loport = -1, hiport = -1;
336 struct sockaddr_in sin;
340 struct timeval now, when, delta;
341 struct client *clients = 0, *c, **cc;
342 const struct token *t;
348 #define f_bogus 0x01u
349 #define f_notreally 0x02u
354 /* --- Partially initialize the socket address --- */
356 sin.sin_family = AF_INET;
357 sin.sin_addr.s_addr = INADDR_ANY;
360 /* --- Parse the command line --- */
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' },
373 if ((i = mdwopt(argc, argv, "hvua:np:", opts, 0, 0, 0)) < 0) break;
375 case 'h': help(stdout); exit(0);
376 case 'v': version(stdout); exit(0);
377 case 'u': usage(stdout); exit(0);
379 if ((h = gethostbyname(optarg)) == 0) {
380 die(1, "failed to resolve address `%s': %s",
381 optarg, hstrerror(h_errno));
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));
388 case 'n': f |= f_notreally; break;
390 if ((p = strchr(optarg, '-')) == 0)
391 loport = hiport = getuint(optarg, 0);
393 loport = getuint(optarg, p);
394 hiport = getuint(p + 1, 0);
397 default: f |= f_bogus; break;
400 if (f & f_bogus) { usage(stderr); exit(1); }
401 if (optind >= argc) { usage(stderr); exit(1); }
403 /* --- Open the file systems --- */
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));
413 if (f & f_notreally) {
414 for (i = 0; i < nfs; i++) {
420 /* --- Generate random tokens --- */
424 /* --- Create the listening socket --- */
426 if ((sk = socket(PF_INET, SOCK_STREAM, 0)) < 0)
427 die(2, "socket: %s", strerror(errno));
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));
444 if (i > hiport) die(2, "bind: all ports in use");
446 if (listen(sk, 5)) die(2, "listen: %s", strerror(errno));
448 /* --- Tell the caller how to connect to us, and start the timer --- */
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);
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);
461 /* --- Collect incoming connections, and check for the cookie --- *
463 * This is the tricky part.
470 for (c = clients; c; c = c->next) {
471 FD_SET(c->fd, &fdin);
472 if (c->fd > maxfd) maxfd = c->fd;
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);
479 if (TV_CMP(&now, >=, &when)) die(3, "timeout (accept)");
481 if (FD_ISSET(sk, &fdin)) {
483 fd = accept(sk, (struct sockaddr *)&sin, &sasz);
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);
492 else if (errno != EAGAIN)
493 moan("accept: %s", strerror(errno));
497 for (cc = &clients; *cc;) {
499 if (!FD_ISSET(c->fd, &fdin)) goto next_client;
500 n = read(c->fd, buf, sizeof(buf));
501 if (!n) goto disconn;
503 if (errno == EAGAIN) goto next_client;
504 D( moan("read (client; auth): %s", strerror(errno)); )
507 for (p = buf, q = p + n; p < q; p++) {
508 switch (tokmatch_update(&c->tm, *p)) {
510 case TF_FREEZE: goto connected;
512 D( moan("bad token from client"); )
533 if (clients->fd != sk) close(clients->fd);
539 /* --- Establish signal handlers --- *
541 * Hopefully this will prevent bad things happening if we have an accident.
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));
550 /* --- Prevent the OOM killer from clobbering us --- */
552 if ((fd = open("/proc/self/oom_adj", O_WRONLY)) < 0 ||
553 write(fd, "-17\n", 4) < 4 ||
555 die(2, "set oom_adj: %s", strerror(errno));
557 /* --- Actually freeze the filesystem --- */
559 for (i = 0; i < nfs; i++) {
561 moan("not really freezing %s", fsname[i]);
563 if (ioctl(fs[i], FIFREEZE, 0) < 0) {
565 die(2, "ioctl (freeze %s): %s", fsname[i], strerror(errno));
569 if (writetok(T_FROZEN, sk)) {
571 die(2, "write (frozen): %s", strerror(errno));
574 /* --- Now wait for the other end to detach --- */
577 TV_ADDL(&when, &now, TO_KEEPALIVE, 0);
578 for (p++; p < q; p++) {
579 switch (tokmatch_update(&tm, *p)) {
581 case TF_KEEPALIVE: tokmatch_init(&tm); break;
582 case TF_THAW: goto done;
583 default: cleanup(); die(3, "unknown token (keepalive)");
589 TV_SUB(&delta, &when, &now);
590 if (select(sk + 1, &fdin, 0, 0, &delta) < 0) {
592 die(2, "select (keepalive): %s", strerror(errno));
595 gettimeofday(&now, 0);
596 if (TV_CMP(&now, >, &when)) {
597 cleanup(); die(3, "timeout (keepalive)");
599 if (FD_ISSET(sk, &fdin)) {
600 n = read(sk, buf, sizeof(buf));
601 if (!n) { cleanup(); die(3, "end-of-file (keepalive)"); }
603 if (errno == EAGAIN) ;
606 die(2, "read (client, keepalive): %s", strerror(errno));
609 for (p = buf, q = p + n; p < q; p++) {
610 switch (tokmatch_update(&tm, *p)) {
613 TV_ADDL(&when, &now, TO_KEEPALIVE, 0);
620 die(3, "unknown token (keepalive)");
629 if (writetok(T_THAWED, sk))
630 die(2, "write (thaw): %s", strerror(errno));
635 /*----- That's all, folks -------------------------------------------------*/