Add the "fsck" command to the CLI.

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.
This commit is contained in:
Guus Sliepen 2015-01-15 22:57:56 +01:00
parent a95e182d9c
commit 268e3ffca7
7 changed files with 547 additions and 4 deletions

View 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:

View 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 ==================================================================

View 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 \

486
src/fsck.c Normal file
View file

@ -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;
}

26
src/fsck.h Normal file
View file

@ -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

View 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},
};

View 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];