/* This file is part of NSsync Copyright (C) 2011-2017 Sergey Poznyakoff NSsync is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3, or (at your option) any later version. NSsync is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with NSsync. If not, see . */ #include "nssync.h" #include #include #include "runcap.h" #include #include #include #include int lint_mode; int dry_run_mode; int preprocess_only; int debug_level; char *config_file = SYSCONFDIR "/nssync.conf"; char *slave_status_file; char *pidfile; int force; char *tempdir; char *reload_command = "/usr/sbin/rndc reload"; struct json_value *error_list; struct json_value *changed_zones; int check_ns; int server_mode; struct grecs_sockaddr *server_addr; unsigned periodic_timeout = 3600; unsigned delay_timeout = 30; int syslog_facility = LOG_DAEMON; char *syslog_tag; int foreground; int syslog_option; #include "cmdline.h" void start_syslog(void) { grecs_log_to_stderr = 0; openlog(program_name, LOG_PID, syslog_facility); } void vdiag(int prio, const char *fmt, va_list ap) { if (grecs_log_to_stderr) { fprintf(stderr, "%s: ", program_name); vfprintf(stderr, fmt, ap); fputc('\n', stderr); } else { vsyslog(prio, fmt, ap); } } void verror(const char *fmt, va_list ap) { vdiag(LOG_ERR, fmt, ap); } void error(const char *fmt, ...) { va_list ap; va_start(ap, fmt); verror(fmt, ap); va_end(ap); } void info_printf(const char *fmt, ...) { va_list ap; va_start(ap, fmt); vdiag(LOG_INFO, fmt, ap); va_end(ap); } void debug_printf(const char *fmt, ...) { va_list ap; va_start(ap, fmt); vdiag(LOG_DEBUG, fmt, ap); va_end(ap); } void dlz_error(char const *zone, char const *fmt, ...) { struct json_value *obj; char *str = NULL; size_t sz = 0; va_list ap; va_start(ap, fmt); grecs_vasprintf(&str, &sz, fmt, ap); va_end(ap); obj = json_new_object(); json_object_set(obj, "message", json_new_string(str)); free(str); if (zone) json_object_set(obj, "zone", json_new_string(zone)); json_array_append(error_list, obj); } void dlz_success(char const *zone) { json_array_append(changed_zones, json_new_string(zone)); } /* Create the directory DIR, eventually creating all intermediate directories starting from DIR + BASELEN. */ int create_hierarchy(char *dir, size_t baselen) { int rc; struct stat st; char *p; if (stat(dir, &st) == 0) { if (!S_ISDIR(st.st_mode)) { error("component %s is not a directory", dir); return 1; } return 0; } else if (errno != ENOENT) { error("cannot stat file %s: %s", dir, strerror(errno)); return 1; } p = strrchr(dir, '/'); if (p) { if (p - dir + 1 < baselen) { error("base directory %s does not exist", dir); return 1; } *p = 0; } rc = create_hierarchy(dir, baselen); if (rc == 0) { if (p) *p = '/'; if (mkdir(dir, 0744)) { error("cannot create directory %s: %s", dir, strerror(errno)); rc = 1; } } return rc; } static int trycreate(const char *file_name) { int rc = 1; char *dir_name = grecs_strdup(file_name); char *p = strrchr(dir_name, '/'); if (p) { size_t len = strlen(bind_working_dir); if (strncmp(dir_name, bind_working_dir, len) == 0) { *p = 0; rc = create_hierarchy(dir_name, len); } } free(dir_name); return rc; } int copy_file(const char *file_name, FILE *infile, const char *dst_file) { int out_fd; struct stat st; int rc; char *buf; size_t bufsize; size_t fsize; if (dry_run_mode) return 0; if (fstat(fileno(infile), &st)) { error("cannot stat source file %s: %s", file_name, strerror(errno)); return 1; } out_fd = open(dst_file, O_WRONLY|O_TRUNC|O_CREAT, 0640); if (out_fd == -1) { if (errno == ENOENT) { if (trycreate(dst_file)) return 1; out_fd = open(dst_file, O_WRONLY|O_TRUNC|O_CREAT, 0640); } if (out_fd == -1) { error("cannot create destination file %s: %s", dst_file, strerror(errno)); return 1; } } buf = NULL; fsize = st.st_size; for (bufsize = fsize; bufsize > 0 && (buf = malloc (bufsize)) == NULL; bufsize /= 2) ; if (bufsize == 0) grecs_alloc_die (); rc = 0; fseeko(infile, 0, SEEK_SET); while (fsize > 0) { size_t rest; size_t rdbytes; rest = fsize > bufsize ? bufsize : fsize; rdbytes = fread(buf, 1, rest, infile); if (rdbytes == -1) { error("unexpected error reading %s: %s", file_name, strerror(errno)); rc = 1; break; } rest = write(out_fd, buf, rdbytes); if (rest == -1) { error("unexpected error writing to %s: %s", dst_file, strerror(errno)); rc = 1; break; } else if (rest != rdbytes) { error("short write on %s", dst_file); rc = 1; } fsize -= rdbytes; } free(buf); close(out_fd); if (rc) unlink(dst_file); return rc; } /* Move FILE to DST_FILE. If they reside on different devices, use copy_file + unlink. */ int move_file(const char *file, const char *dst_file) { int rc = 0; rc = rename(file, dst_file); if (rc && errno == ENOENT) { if (trycreate(dst_file)) return 1; rc = rename(file, dst_file); } if (rc) { if (errno == EXDEV) { FILE *fp = fopen(file, "r"); if (!fp) { error("cannot open %s for reading: %s", file, strerror(errno)); return 1; } rc = copy_file (file, fp, dst_file); fclose(fp); if (rc) { error("cannot copy %s to %s: %s", file, dst_file, strerror(errno)); rc = 1; } else if (unlink(file)) { error("cannot unlink %s: %s", file, strerror(errno)); } } else { error("cannot move %s to %s: %s", file, dst_file, strerror(errno)); rc = 1; } } return rc; } /* Return 0 if files are the same, 1 if they differ and -1 if in trouble */ static int filecmp(FILE *fp1, FILE *fp2) { int c1, c2; while (1) { c1 = fgetc(fp1); c2 = fgetc(fp2); if (c1 == c2) { if (c1 == EOF) break; continue; } return 1; } return 0; } int compare(const char *file1, const char *file2) { int rc; FILE *fp1, *fp2; struct stat st1, st2; if (access(file2, F_OK) && errno == ENOENT) { debug(2,("destination file %s does not exist", file2)); return 1; } if (stat(file1, &st1) == 0 && stat(file2, &st2) == 0) { if (st1.st_dev == st2.st_dev && st1.st_ino == st2.st_ino) return 0; else if (st1.st_size != st2.st_size) return 1; } fp1 = fopen(file1, "r"); if (!fp1) { error("can't open file \"%s\": %s", file1, strerror(errno)); return -1; } fp2 = fopen(file2, "r"); if (!fp2) { error("can't open file \"%s\": %s", file2, strerror(errno)); return -1; } rc = filecmp(fp1, fp2); fclose(fp1); fclose(fp2); return rc; } static void synchronize(struct nssync *sp) { mode_t um; size_t size = 0; debug(1, ("processing %s", sp->tag)); if (grecs_asprintf(&sp->temp_file_name, &size, "%s/%s.tmp", tempdir, sp->tag)) grecs_alloc_die(); um = umask(077); sp->fp = fopen(sp->temp_file_name, "w+"); umask(um); if (!sp->fp) { error("cannot open %s: %s", sp->temp_file_name, strerror(errno)); return; } format_zones(sp); if (!dry_run_mode) unlink(sp->temp_file_name); /* Cleanup */ free(sp->temp_file_name); sp->temp_file_name = NULL; } int check_slave_status(void) { FILE *fp; char *saved_file = NULL; char *saved_off; char *buf = NULL; size_t size = 0; char *sql_file, *sql_off; if (!slave_status_file) return 0; if (sql_get_slave_status(&sql_file, &sql_off)) { unlink(slave_status_file); return 0; } fp = fopen(slave_status_file, "r"); if (fp) { if (grecs_getline(&buf, &size, fp) > 0) { buf[strlen(buf)-1] = 0; saved_file = buf; saved_off = strchr(saved_file, ' '); if (saved_off) *saved_off++ = 0; } fclose(fp); } if (!force && saved_file && saved_off && strcmp(sql_file, saved_file) == 0 && strcmp(sql_off, saved_off) == 0) { debug(1, ("slave status hasn't changed: nothing to do")); return 1; } fp = fopen(slave_status_file, "w"); if (fp) { fprintf(fp, "%s %s\n", sql_file, sql_off); fclose(fp); } free(sql_file); free(sql_off); return 0; } void remove_pidfile() { if (pidfile && unlink(pidfile)) error("cannot unlink pidfile `%s': %s", pidfile, strerror(errno)); } void create_pidfile(void) { FILE *fp; if (!pidfile) return; fp = fopen(pidfile, "w"); if (!fp) { error("cannot create pidfile `%s': %s", pidfile, strerror(errno)); exit(EX_TEMPFAIL); } fprintf(fp, "%lu", (unsigned long) getpid()); fclose(fp); } /* Check whether pidfile NAME exists and if so, whether its PID is still active. Exit if it is. */ void check_pidfile() { unsigned long pid; FILE *fp; if (!pidfile) return; fp = fopen(pidfile, "r"); if (fp) { if (fscanf(fp, "%lu", &pid) != 1) { error("cannot get pid from pidfile `%s'", pidfile); } else { if (kill(pid, 0) == 0) { error("%s appears to run with pid %lu. " "If it does not, remove `%s' and retry.", program_name, pid, pidfile); exit(EX_TEMPFAIL); } } fclose(fp); if (unlink(pidfile)) { error("cannot unlink pidfile `%s': %s", pidfile, strerror(errno)); exit(EX_NOPERM); } } else if (errno != ENOENT) { error("cannot open pidfile `%s': %s", pidfile, strerror(errno)); exit(EX_TEMPFAIL); } } struct linemon_closure { char const *prefix; }; static void linemon(const char *ptr, size_t len, void *data) { struct linemon_closure *clos = data; if (ptr[len-1] != '\n') error("[%s]: %*.*s\\", clos->prefix, (int)len, (int)len, ptr); else error("[%s]: %*.*s", clos->prefix, (int)(len - 1), (int)(len - 1), ptr); } int dns_reload(void) { int c; struct runcap rc; char *argv[4]; struct linemon_closure closure[2]; debug(1,("about to run %s", reload_command)); if (dry_run_mode) return 0; argv[0] = "/bin/sh"; argv[1] = "-c"; argv[2] = reload_command; argv[3] = NULL; closure[0].prefix = "STDOUT"; rc.rc_cap[RUNCAP_STDOUT].sc_linemon = linemon; rc.rc_cap[RUNCAP_STDOUT].sc_monarg = &closure[0]; closure[1].prefix = "STDERR"; rc.rc_cap[RUNCAP_STDERR].sc_linemon = linemon; rc.rc_cap[RUNCAP_STDERR].sc_monarg = &closure[1]; rc.rc_timeout = 10; /* FIXME */ rc.rc_argv = argv; c = runcap(&rc, RCF_STDOUT_LINEMON | RCF_STDERR_LINEMON | RCF_TIMEOUT); if (c) { error("can't run \"%s\": %s", reload_command, strerror(errno)); } else if (WIFEXITED(rc.rc_status)) { int status = WEXITSTATUS(rc.rc_status); if (status) { error("command \"%s\" returned %d", reload_command, status); c = 1; } } else if (WIFSIGNALED(rc.rc_status)) { error("command \"%s\" terminated on signal %d", reload_command, WTERMSIG(rc.rc_status)); c = 1; } else if (WIFSTOPPED(rc.rc_status)) { error("command \"%s\" got stopped", reload_command); c = 1; } else { error("command \"%s\" terminated with unrecognized status %d", reload_command, rc.rc_status); c = 1; } runcap_free(&rc); return c; } int nssync(struct json_value **ret_json) { struct grecs_list_entry *ep; int res = 1; changed_zones = json_new_array(); error_list = json_new_array(); if (check_slave_status()) res = 0; else { sql_connect(); for (ep = synclist->head; ep; ep = ep->next) synchronize(ep->data); if (json_array_size(error_list) == 0) { for (ep = synclist->head; ep; ep = ep->next) flush_zone_list(ep->data); filetab_clear(); if (json_array_size(error_list) == 0) { if (json_array_size(changed_zones) > 0) { res = dns_reload(); if (res) dlz_error(NULL, "reload failed"); } else res = 0; } } } if (ret_json) { struct timeval tv; struct timezone tz; struct json_value *json; json = json_new_object(); json_object_set(json, "success", json_new_bool(!res)); if (json_array_size(changed_zones) > 0) json_object_set(json, "zones", changed_zones); if (json_array_size(error_list) > 0) json_object_set(json, "errors", error_list); gettimeofday(&tv, &tz); tv.tv_sec += tz.tz_minuteswest * 60; json_object_set(json, "timestamp", json_new_number(tv.tv_sec)); *ret_json = json; } else { json_value_free(error_list); json_value_free(changed_zones); } error_list = NULL; changed_zones = NULL; return res; } /* Signal handing */ void sigfatal(int sig) { remove_pidfile(); _exit(0); } void sigign(int sig) { } void set_signal(int sig, void (*sighandler)(int)) { struct sigaction act; act.sa_handler = sighandler; sigemptyset(&act.sa_mask); act.sa_flags = SA_RESTART; sigaction(sig, &act, NULL); } void set_signals(int *sigv, void (*sighandler)(int)) { int i; for (i = 0; sigv[i]; i++) set_signal(sigv[i], sighandler); } int fatal_signals[] = { SIGHUP, SIGINT, SIGQUIT, SIGTERM, 0 }; int single_run(void) { if (syslog_option) start_syslog(); runas(); debug(1,("start up")); create_pidfile(); if (nssync(NULL)) { error("exiting due to errors"); return EX_UNAVAILABLE; } return 0; } int main(int argc, char **argv) { int rc; config_init(); parse_options(argc, argv); if (preprocess_only) exit(grecs_preproc_run(config_file, grecs_preprocessor) ? EX_CONFIG : 0); config_parse(); check_pidfile(); if (check_ns) get_host_addresses(); if (server_mode) { set_signals(fatal_signals, sigign); rc = nssync_server(); } else { set_signals(fatal_signals, sigfatal); rc = single_run(); } debug(1,("shut down")); remove_pidfile(); return rc; }