Add the "fsck" command to the CLI.
authorGuus Sliepen <guus@tinc-vpn.org>
Thu, 15 Jan 2015 21:57:56 +0000 (22:57 +0100)
committerGuus Sliepen <guus@tinc-vpn.org>
Thu, 15 Jan 2015 22:06:38 +0000 (23:06 +0100)
This will report possible problems in the configuration files, and in
some cases offers to fix them.

The code is far from perfect yet. It expects keys to be in their default
locations, it doesn't check for Public/PrivateKey[File] statemetns yet.
It also does not correctly handle Ed25519 public keys yet.

doc/tinc.8.in
doc/tinc.texi
src/Makefile.am
src/fsck.c [new file with mode: 0644]
src/fsck.h [new file with mode: 0644]
src/tincctl.c
src/tincctl.h

index bb56386..aa4e833 100644 (file)
@@ -215,6 +215,14 @@ If
 .Ar netname
 is given, switch to that network.
 Otherwise, display a list of all networks for which configuration files exist.
+.It fsck
+This will check the configuration files for possible problems,
+such as unsafe file permissions, missing executable bit on script,
+unknown and obsolete configuration variables, wrong public and/or private keys, and so on.
+.Pp
+When problems are found, this will be printed on a line with WARNING or ERROR in front of it.
+Most problems must be corrected by the user itself, however in some cases (like file permissions and missing public keys),
+tinc will ask if it should fix the problem.
 .El
 .Sh EXAMPLES
 Examples of some commands:
index 427fbea..2f4949e 100644 (file)
@@ -2436,11 +2436,21 @@ Dump VPN traffic going through the local tinc node in pcap-savefile format to st
 from where it can be redirected to a file or piped through a program that can parse it directly,
 such as tcpdump.
 
-@cindex network [@var{netname}]
-@item network
+@cindex network
+@item network [@var{netname}]
 If @var{netname} is given, switch to that network.
 Otherwise, display a list of all networks for which configuration files exist.
 
+@cindex fsck
+@item fsck
+This will check the configuration files for possible problems,
+such as unsafe file permissions, missing executable bit on script,
+unknown and obsolete configuration variables, wrong public and/or private keys, and so on.
+
+When problems are found, this will be printed on a line with WARNING or ERROR in front of it.
+Most problems must be corrected by the user itself, however in some cases (like file permissions and missing public keys),
+tinc will ask if it should fix the problem.
+
 @end table
 
 @c ==================================================================
index ea53ea2..beb71cd 100644 (file)
@@ -101,6 +101,7 @@ tinc_SOURCES = \
        dropin.c dropin.h \
        getopt.c getopt.h \
        getopt1.c \
+       fsck.c fsck.h \
        info.c info.h \
        invitation.c invitation.h \
        list.c list.h \
diff --git a/src/fsck.c b/src/fsck.c
new file mode 100644 (file)
index 0000000..8df95ec
--- /dev/null
@@ -0,0 +1,486 @@
+/*
+    fsck.c -- Check the configuration files for problems
+    Copyright (C) 2014 Guus Sliepen <guus@tinc-vpn.org>
+
+    This program 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 2 of the License, or
+    (at your option) any later version.
+
+    This program 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 this program; if not, write to the Free Software Foundation, Inc.,
+    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+*/
+
+#include "system.h"
+
+#include "crypto.h"
+#include "ecdsa.h"
+#include "ecdsagen.h"
+#include "fsck.h"
+#include "names.h"
+#ifndef DISABLE_LEGACY
+#include "rsa.h"
+#include "rsagen.h"
+#endif
+#include "tincctl.h"
+#include "utils.h"
+
+static bool ask_fix(void) {
+       if(force)
+               return true;
+       if(!tty)
+               return false;
+again:
+       fprintf(stderr, "Fix y/n? ");
+       char buf[1024];
+       if(!fgets(buf, sizeof buf, stdin)) {
+               tty = false;
+               return false;
+       }
+       if(buf[0] == 'y' || buf[0] == 'Y')
+               return true;
+       if(buf[0] == 'n' || buf[0] == 'N')
+               return false;
+       goto again;
+}
+
+static void print_tinc_cmd(const char *argv0, const char *format, ...) {
+       if(confbasegiven)
+               fprintf(stderr, "%s -c %s ", argv0, confbase);
+       else if(netname)
+               fprintf(stderr, "%s -n %s ", argv0, netname);
+       else
+               fprintf(stderr, "%s ", argv0);
+       va_list va;
+       va_start(va, format);
+       vfprintf(stderr, format, va);
+       va_end(va);
+       fputc('\n', stderr);
+}
+
+static int strtailcmp(const char *str, const char *tail) {
+       size_t slen = strlen(str);
+       size_t tlen = strlen(tail);
+       if(tlen > slen)
+               return -1;
+       return memcmp(str + slen - tlen, tail, tlen);
+}
+
+static void check_conffile(const char *fname, bool server) {
+       FILE *f = fopen(fname, "r");
+       if(!f) {
+               fprintf(stderr, "ERROR: cannot read %s: %s\n", fname, strerror(errno));
+               return;
+       }
+
+       char line[2048];
+       int lineno = 0;
+       bool skip = false;
+       const int maxvariables = 50;
+       int count[maxvariables];
+       memset(count, 0, sizeof count);
+
+       while(fgets(line, sizeof line, f)) {
+               if(skip) {
+                       if(!strncmp(line, "-----END", 8))
+                               skip = false;
+                       continue;
+               } else {
+                       if(!strncmp(line, "-----BEGIN", 10)) {
+                               skip = true;
+                               continue;
+                       }
+               }
+
+               int len;
+               char *variable, *value, *eol;
+               variable = value = line;
+
+               lineno++;
+
+               eol = line + strlen(line);
+               while(strchr("\t \r\n", *--eol))
+                       *eol = '\0';
+
+               if(!line[0] || line[0] == '#')
+                       continue;
+
+               len = strcspn(value, "\t =");
+               value += len;
+               value += strspn(value, "\t ");
+               if(*value == '=') {
+                       value++;
+                       value += strspn(value, "\t ");
+               }
+               variable[len] = '\0';
+
+               bool found = false;
+
+               for(int i = 0; variables[i].name; i++) {
+                       if(strcasecmp(variables[i].name, variable))
+                               continue;
+
+                       found = true;
+
+                       if(variables[i].type & VAR_OBSOLETE) {
+                               fprintf(stderr, "WARNING: obsolete variable %s in %s line %d\n", variable, fname, lineno);
+                       }
+
+                       if(i < maxvariables)
+                               count[i]++;
+               }
+
+               if(!found)
+                       fprintf(stderr, "WARNING: unknown variable %s in %s line %d\n", variable, fname, lineno);
+
+               if(!*value)
+                       fprintf(stderr, "ERROR: no value for variable %s in %s line %d\n", variable, fname, lineno);
+       }
+
+       for(int i = 0; variables[i].name && i < maxvariables; i++) {
+               if(count[i] > 1 && !(variables[i].type & VAR_MULTIPLE))
+                       fprintf(stderr, "WARNING: multiple instances of variable %s in %s\n", variables[i].name, fname);
+       }
+
+       if(ferror(f))
+               fprintf(stderr, "ERROR: while reading %s: %s\n", fname, strerror(errno));
+
+       fclose(f);
+}
+
+int fsck(const char *argv0) {
+       uid_t uid = getuid();
+
+       // Check that tinc.conf is readable.
+
+       if(access(tinc_conf, R_OK)) {
+               fprintf(stderr, "ERROR: cannot read %s: %s\n", tinc_conf, strerror(errno));
+               if(errno == ENOENT) {
+                       fprintf(stderr, "No tinc configuration found. Create a new one with:\n\n");
+                       print_tinc_cmd(argv0, "init");
+               } else if(errno == EACCES) {
+                       if(uid != 0)
+                               fprintf(stderr, "You are currently not running tinc as root. Use sudo?\n");
+                       else
+                               fprintf(stderr, "Check the permissions of each component of the path %s.\n", tinc_conf);
+               }
+               return 1;
+       }
+
+       char *name = get_my_name(true);
+       if(!name) {
+               fprintf(stderr, "ERROR: tinc cannot run without a valid Name.\n");
+               return 1;
+       }
+
+       // Check for private keys.
+       // TODO: use RSAPrivateKeyFile and Ed25519PrivateKeyFile variables if present.
+
+       struct stat st;
+       char fname[PATH_MAX];
+       char dname[PATH_MAX];
+
+#ifndef DISABLE_LEGACY
+       rsa_t *rsa_priv = NULL;
+       snprintf(fname, sizeof fname, "%s/rsa_key.priv", confbase);
+
+       if(stat(fname, &st)) {
+               if(errno != ENOENT) {
+                       // Something is seriously wrong here. If we can access the directory with tinc.conf in it, we should certainly be able to stat() an existing file.
+                       fprintf(stderr, "ERROR: cannot read %s: %s\n", fname, strerror(errno));
+                       fprintf(stderr, "Please correct this error.\n");
+                       return 1;
+               }
+       } else {
+               FILE *f = fopen(fname, "r");
+               if(!f) {
+                       fprintf(stderr, "ERROR: could not open %s: %s\n", fname, strerror(errno));
+                       return 1;
+               }
+               rsa_priv = rsa_read_pem_private_key(f);
+               fclose(f);
+               if(!rsa_priv) {
+                       fprintf(stderr, "ERROR: No key or unusable key found in %s.\n", fname);
+                       fprintf(stderr, "You can generate a new RSA key with:\n\n");
+                       print_tinc_cmd(argv0, "generate-rsa-keys");
+                       return 1;
+               }
+
+               if(st.st_mode & 077) {
+                       fprintf(stderr, "WARNING: unsafe file permissions on %s.\n", fname);
+                       if(st.st_uid != uid) {
+                               fprintf(stderr, "You are not running %s as the same uid as %s.\n", argv0, fname);
+                       } else if(ask_fix()) {
+                               if(chmod(fname, st.st_mode & ~077))
+                                       fprintf(stderr, "ERROR: could not change permissions of %s: %s\n", fname, strerror(errno));
+                               else
+                                       fprintf(stderr, "Fixed permissions of %s.\n", fname);
+                       }
+               }
+       }
+#endif
+
+       ecdsa_t *ecdsa_priv = NULL;
+       snprintf(fname, sizeof fname, "%s/ed25519_key.priv", confbase);
+
+       if(stat(fname, &st)) {
+               if(errno != ENOENT) {
+                       // Something is seriously wrong here. If we can access the directory with tinc.conf in it, we should certainly be able to stat() an existing file.
+                       fprintf(stderr, "ERROR: cannot read %s: %s\n", fname, strerror(errno));
+                       fprintf(stderr, "Please correct this error.\n");
+                       return 1;
+               }
+       } else {
+               FILE *f = fopen(fname, "r");
+               if(!f) {
+                       fprintf(stderr, "ERROR: could not open %s: %s\n", fname, strerror(errno));
+                       return 1;
+               }
+               ecdsa_priv = ecdsa_read_pem_private_key(f);
+               fclose(f);
+               if(!ecdsa_priv) {
+                       fprintf(stderr, "ERROR: No key or unusable key found in %s.\n", fname);
+                       fprintf(stderr, "You can generate a new Ed25519 key with:\n\n");
+                       print_tinc_cmd(argv0, "generate-ed25519-keys");
+                       return 1;
+               }
+
+               if(st.st_mode & 077) {
+                       fprintf(stderr, "WARNING: unsafe file permissions on %s.\n", fname);
+                       if(st.st_uid != uid) {
+                               fprintf(stderr, "You are not running %s as the same uid as %s.\n", argv0, fname);
+                       } else if(ask_fix()) {
+                               if(chmod(fname, st.st_mode & ~077))
+                                       fprintf(stderr, "ERROR: could not change permissions of %s: %s\n", fname, strerror(errno));
+                               else
+                                       fprintf(stderr, "Fixed permissions of %s.\n", fname);
+                       }
+               }
+       }
+
+#ifdef DISABLE_LEGACY
+       if(!ecdsa_priv) {
+               fprintf(stderr, "ERROR: No Ed25519 private key found.\n");
+#else
+       if(!rsa_priv && !ecdsa_priv) {
+               fprintf(stderr, "ERROR: Neither RSA or Ed25519 private key found.\n");
+#endif
+               fprintf(stderr, "You can generate new keys with:\n\n");
+               print_tinc_cmd(argv0, "generate-keys");
+               return 1;
+       }
+
+       // Check for public keys.
+       // TODO: use RSAPublicKeyFile and Ed25519PublicKeyFile variables if present.
+
+       snprintf(fname, sizeof fname, "%s/hosts/%s", confbase, name);
+       if(access(fname, R_OK))
+               fprintf(stderr, "WARNING: cannot read %s\n", fname);
+
+       FILE *f;
+
+#ifndef DISABLE_LEGACY
+       rsa_t *rsa_pub = NULL;
+
+       f = fopen(fname, "r");
+       if(f)
+               rsa_pub = rsa_read_pem_public_key(f);
+       fclose(f);
+
+       if(rsa_priv) {
+               if(!rsa_pub) {
+                       fprintf(stderr, "WARNING: No (usable) public RSA key found.\n");
+                       if(ask_fix()) {
+                               FILE *f = fopen(fname, "a");
+                               if(f) {
+                                       if(rsa_write_pem_public_key(rsa_priv, f))
+                                               fprintf(stderr, "Wrote RSA public key to %s.\n", fname);
+                                       else
+                                               fprintf(stderr, "ERROR: could not write RSA public key to %s.\n", fname);
+                                       fclose(f);
+                               } else {
+                                       fprintf(stderr, "ERROR: could not append to %s: %s\n", fname, strerror(errno));
+                               }
+                       }
+               } else {
+                       // TODO: suggest remedies
+                       size_t len = rsa_size(rsa_priv);
+                       if(len != rsa_size(rsa_pub)) {
+                               fprintf(stderr, "ERROR: public and private RSA keys do not match.\n");
+                               return 1;
+                       }
+                       char buf1[len], buf2[len], buf3[len];
+                       randomize(buf1, sizeof buf1);
+                       buf1[0] &= 0x7f;
+                       memset(buf2, 0, sizeof buf2);
+                       memset(buf3, 0, sizeof buf2);
+                       if(!rsa_public_encrypt(rsa_pub, buf1, sizeof buf1, buf2)) {
+                               fprintf(stderr, "ERROR: public RSA key does not work.\n");
+                               return 1;
+                       }
+                       if(!rsa_private_decrypt(rsa_priv, buf2, sizeof buf2, buf3)) {
+                               fprintf(stderr, "ERROR: private RSA key does not work.\n");
+                               return 1;
+                       }
+                       if(memcmp(buf1, buf3, sizeof buf1)) {
+                               fprintf(stderr, "ERROR: public and private RSA keys do not match.\n");
+                               return 1;
+                       }
+               }
+       } else {
+               if(rsa_pub)
+                       fprintf(stderr, "WARNING: A public RSA key was found but no private key is known.\n");
+       }
+#endif
+       //
+       // TODO: this should read the Ed25519PublicKey config variable instead.
+       ecdsa_t *ecdsa_pub = NULL;
+
+       f = fopen(fname, "r");
+       if(f)
+               ecdsa_pub = ecdsa_read_pem_public_key(f);
+       fclose(f);
+
+       if(ecdsa_priv) {
+               if(!ecdsa_pub) {
+                       fprintf(stderr, "WARNING: No (usable) public Ed25519 key found.\n");
+                       if(ask_fix()) {
+                               FILE *f = fopen(fname, "a");
+                               if(f) {
+                                       if(ecdsa_write_pem_public_key(ecdsa_priv, f))
+                                               fprintf(stderr, "Wrote Ed25519 public key to %s.\n", fname);
+                                       else
+                                               fprintf(stderr, "ERROR: could not write Ed25519 public key to %s.\n", fname);
+                                       fclose(f);
+                               } else {
+                                       fprintf(stderr, "ERROR: could not append to %s: %s\n", fname, strerror(errno));
+                               }
+                       }
+               } else {
+                       // TODO: suggest remedies
+                       char *key1 = ecdsa_get_base64_public_key(ecdsa_pub);
+                       if(!key1) {
+                               fprintf(stderr, "ERROR: public Ed25519 key does not work.\n");
+                               return 1;
+                       }
+                       char *key2 = ecdsa_get_base64_public_key(ecdsa_priv);
+                       if(!key2) {
+                               free(key1);
+                               fprintf(stderr, "ERROR: private Ed25519 key does not work.\n");
+                               return 1;
+                       }
+                       int result = strcmp(key1, key2);
+                       free(key1);
+                       free(key2);
+                       if(result) {
+                               fprintf(stderr, "ERROR: public and private Ed25519 keys do not match.\n");
+                               return 1;
+                       }
+               }
+       } else {
+               if(ecdsa_pub)
+                       fprintf(stderr, "WARNING: A public Ed25519 key was found but no private key is known.\n");
+       }
+
+       // Check whether scripts are executable
+
+       struct dirent *ent;
+       DIR *dir = opendir(confbase);
+       if(!dir) {
+               fprintf(stderr, "ERROR: cannot read directory %s: %s\n", confbase, strerror(errno));
+               return 1;
+       }
+
+       while((ent = readdir(dir))) {
+               if(strtailcmp(ent->d_name, "-up") && strtailcmp(ent->d_name, "-down"))
+                       continue;
+
+               strncpy(fname, ent->d_name, sizeof fname);
+               char *dash = strrchr(fname, '-');
+               if(!dash)
+                       continue;
+               *dash = 0;
+
+               if(strcmp(fname, "tinc") && strcmp(fname, "host") && strcmp(fname, "subnet")) {
+                       static bool explained = false;
+                       fprintf(stderr, "WARNING: Unknown script %s" SLASH "%s found.\n", confbase, ent->d_name);
+                       if(!explained) {
+                               fprintf(stderr, "The only scripts in %s executed by tinc are:\n", confbase);
+                               fprintf(stderr, "tinc-up, tinc-down, host-up, host-down, subnet-up and subnet-down.\n");
+                               explained = true;
+                       }
+                       continue;
+               }
+
+               snprintf(fname, sizeof fname, "%s" SLASH "%s", confbase, ent->d_name);
+               if(access(fname, R_OK | X_OK)) {
+                       if(errno != EACCES) {
+                               fprintf(stderr, "ERROR: cannot access %s: %s\n", fname, strerror(errno));
+                               continue;
+                       }
+                       fprintf(stderr, "WARNING: cannot read and execute %s: %s\n", fname, strerror(errno));
+                       if(ask_fix()) {
+                               if(chmod(fname, 0755))
+                                       fprintf(stderr, "ERROR: cannot change permissions on %s: %s\n", fname, strerror(errno));
+                       }
+               }
+       }
+       closedir(dir);
+
+       snprintf(dname, sizeof dname, "%s" SLASH "hosts", confbase);
+       dir = opendir(dname);
+       if(!dir) {
+               fprintf(stderr, "ERROR: cannot read directory %s: %s\n", dname, strerror(errno));
+               return 1;
+       }
+
+       while((ent = readdir(dir))) {
+               if(strtailcmp(ent->d_name, "-up") && strtailcmp(ent->d_name, "-down"))
+                       continue;
+
+               strncpy(fname, ent->d_name, sizeof fname);
+               char *dash = strrchr(fname, '-');
+               if(!dash)
+                       continue;
+               *dash = 0;
+
+               snprintf(fname, sizeof fname, "%s" SLASH "hosts" SLASH "%s", confbase, ent->d_name);
+               if(access(fname, R_OK | X_OK)) {
+                       if(errno != EACCES) {
+                               fprintf(stderr, "ERROR: cannot access %s: %s\n", fname, strerror(errno));
+                               continue;
+                       }
+                       fprintf(stderr, "WARNING: cannot read and execute %s: %s\n", fname, strerror(errno));
+                       if(ask_fix()) {
+                               if(chmod(fname, 0755))
+                                       fprintf(stderr, "ERROR: cannot change permissions on %s: %s\n", fname, strerror(errno));
+                       }
+               }
+       }
+       closedir(dir);
+       
+       // Check for obsolete / unsafe / unknown configuration variables.
+
+       check_conffile(tinc_conf, true);
+
+       dir = opendir(dname);
+       if(dir) {
+               while((ent = readdir(dir))) {
+                       if(!check_id(ent->d_name))
+                               continue;
+
+                       snprintf(fname, sizeof fname, "%s" SLASH "hosts" SLASH "%s", confbase, ent->d_name);
+                       check_conffile(fname, false);
+               }
+               closedir(dir);
+       }
+
+       return 0;
+}
+
diff --git a/src/fsck.h b/src/fsck.h
new file mode 100644 (file)
index 0000000..51e4f55
--- /dev/null
@@ -0,0 +1,26 @@
+/*
+    fsck.h -- header for fsck.c.
+    Copyright (C) 2012 Guus Sliepen <guus@tinc-vpn.org>
+
+    This program 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 2 of the License, or
+    (at your option) any later version.
+
+    This program 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 this program; if not, write to the Free Software Foundation, Inc.,
+    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+*/
+
+#ifndef __TINC_FSCK_H__
+#define __TINC_FSCK_H__
+
+extern int fsck(const char *argv0);
+
+#endif
+
index 29c5715..04fbdd5 100644 (file)
@@ -31,6 +31,7 @@
 #include "control_common.h"
 #include "crypto.h"
 #include "ecdsagen.h"
+#include "fsck.h"
 #include "info.h"
 #include "invitation.h"
 #include "names.h"
@@ -66,7 +67,7 @@ char line[4096];
 static int code;
 static int req;
 static int result;
-static bool force = false;
+bool force = false;
 bool tty = true;
 bool confbasegiven = false;
 bool netnamegiven = false;
@@ -150,6 +151,7 @@ static void usage(bool status) {
                                "  invite NODE [...]          Generate an invitation for NODE\n"
                                "  join INVITATION            Join a VPN using an INVITIATION\n"
                                "  network [NETNAME]          List all known networks, or switch to the one named NETNAME.\n"
+                               "  fsck                       Check the configuration files for problems.\n"
                                "\n");
                printf("Report bugs to tinc@tinc-vpn.org.\n");
        }
@@ -2154,7 +2156,6 @@ static int switch_network(char *name) {
        free(netname);
        netname = strcmp(name, ".") ? xstrdup(name) : NULL;
 
-       make_names();
         xasprintf(&tinc_conf, "%s" SLASH "tinc.conf", confbase);
         xasprintf(&hosts_dir, "%s" SLASH "hosts", confbase);
        xasprintf(&prompt, "%s> ", identname);
@@ -2199,6 +2200,15 @@ static int cmd_network(int argc, char *argv[]) {
        return 0;
 }
 
+static int cmd_fsck(int argc, char *argv[]) {
+       if(argc > 1) {
+               fprintf(stderr, "Too many arguments!\n");
+               return 1;
+       }
+
+       return fsck(orig_argv[0]);
+}
+
 static const struct {
        const char *command;
        int (*function)(int argc, char *argv[]);
@@ -2241,6 +2251,7 @@ static const struct {
        {"invite", cmd_invite},
        {"join", cmd_join},
        {"network", cmd_network},
+       {"fsck", cmd_fsck},
        {NULL, NULL},
 };
 
index e636887..4a917af 100644 (file)
@@ -21,6 +21,7 @@
 #define __TINC_TINCCTL_H__
 
 extern bool tty;
+extern bool force;
 extern char line[4096];
 extern int fd;
 extern char buffer[4096];