2 econf.c -- configuration code
3 Copyright (C) 2018 Guus Sliepen <guus@meshlink.io>
5 This program is free software; you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation; either version 2 of the License, or
8 (at your option) any later version.
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
15 You should have received a copy of the GNU General Public License along
16 with this program; if not, write to the Free Software Foundation, Inc.,
17 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
23 #include <sys/types.h>
29 #include "meshlink_internal.h"
34 static bool sync_path(const char *pathname) {
37 int fd = open(pathname, O_RDONLY);
40 logger(NULL, MESHLINK_ERROR, "Failed to open %s: %s\n", pathname, strerror(errno));
41 meshlink_errno = MESHLINK_ESTORAGE;
46 logger(NULL, MESHLINK_ERROR, "Failed to sync %s: %s\n", pathname, strerror(errno));
48 meshlink_errno = MESHLINK_ESTORAGE;
53 logger(NULL, MESHLINK_ERROR, "Failed to close %s: %s\n", pathname, strerror(errno));
55 meshlink_errno = MESHLINK_ESTORAGE;
62 static bool invalidate_config_file(meshlink_handle_t *mesh, const char *name, size_t len) {
64 config_t empty_config = {NULL, 0};
65 return config_store(mesh, name, &empty_config);
68 /// Wipe an existing configuration directory
69 bool config_destroy(const struct meshlink_open_params *params) {
70 if(!params->confbase) {
74 FILE *lockfile = NULL;
76 if(!params->load_cb) {
77 /* Exit early if the confbase directory itself doesn't exist */
78 if(access(params->confbase, F_OK) && errno == ENOENT) {
82 /* Take the lock the same way meshlink_open() would. */
83 lockfile = fopen(params->lock_filename, "w+");
86 logger(NULL, MESHLINK_ERROR, "Could not open lock file %s: %s", params->lock_filename, strerror(errno));
87 meshlink_errno = MESHLINK_ESTORAGE;
92 fcntl(fileno(lockfile), F_SETFD, FD_CLOEXEC);
96 // TODO: use _locking()?
99 if(flock(fileno(lockfile), LOCK_EX | LOCK_NB) != 0) {
100 logger(NULL, MESHLINK_ERROR, "Configuration directory %s still in use\n", params->lock_filename);
102 meshlink_errno = MESHLINK_EBUSY;
110 meshlink_handle_t tmp_mesh;
111 memset(&tmp_mesh, 0, sizeof tmp_mesh);
113 tmp_mesh.confbase = params->confbase;
114 tmp_mesh.name = params->name;
115 tmp_mesh.load_cb = params->load_cb;
116 tmp_mesh.store_cb = params->store_cb;
117 tmp_mesh.ls_cb = params->ls_cb;
119 if(!config_ls(&tmp_mesh, invalidate_config_file)) {
120 logger(NULL, MESHLINK_ERROR, "Cannot remove configuration files\n");
122 meshlink_errno = MESHLINK_ESTORAGE;
127 if(!params->load_cb) {
128 if(unlink(params->lock_filename) && errno != ENOENT) {
129 logger(NULL, MESHLINK_ERROR, "Cannot remove lock file %s: %s\n", params->lock_filename, strerror(errno));
131 meshlink_errno = MESHLINK_ESTORAGE;
137 if(!sync_path(params->confbase)) {
138 logger(NULL, MESHLINK_ERROR, "Cannot sync directory %s: %s\n", params->confbase, strerror(errno));
139 meshlink_errno = MESHLINK_ESTORAGE;
143 rmdir(params->confbase);
149 /// Read a blob of data.
150 static bool load(meshlink_handle_t *mesh, const char *key, void *data, size_t *len) {
151 logger(mesh, MESHLINK_DEBUG, "load(%s, %p, %zu)", key ? key : "(null)", data, *len);
154 if(!mesh->load_cb(mesh, key, data, len)) {
155 logger(mesh, MESHLINK_ERROR, "Failed to open `%s'\n", key);
156 meshlink_errno = MESHLINK_ESTORAGE;
163 char filename[PATH_MAX];
164 snprintf(filename, sizeof(filename), "%s/%s", mesh->confbase, key);
166 FILE *f = fopen(filename, "r");
169 logger(mesh, MESHLINK_ERROR, "Failed to open `%s': %s\n", filename, strerror(errno));
170 meshlink_errno = MESHLINK_ESTORAGE;
176 if(fseek(f, 0, SEEK_END) || (actual_len = ftell(f)) <= 0 || fseek(f, 0, SEEK_SET)) {
177 logger(mesh, MESHLINK_ERROR, "Cannot get config file size: %s\n", strerror(errno));
178 meshlink_errno = MESHLINK_ESTORAGE;
183 size_t todo = (size_t)actual_len < *len ? (size_t)actual_len : *len;
191 if(fread(data, todo, 1, f) != 1) {
192 logger(mesh, MESHLINK_ERROR, "Cannot read config file: %s\n", strerror(errno));
193 meshlink_errno = MESHLINK_ESTORAGE;
199 logger(mesh, MESHLINK_ERROR, "Failed to close `%s': %s", filename, strerror(errno));
200 meshlink_errno = MESHLINK_ESTORAGE;
207 /// Store a blob of data.
208 static bool store(meshlink_handle_t *mesh, const char *key, const void *data, size_t len) {
209 logger(mesh, MESHLINK_DEBUG, "store(%s, %p, %zu)", key ? key : "(null)", data, len);
212 if(!mesh->store_cb(mesh, key, data, len)) {
213 logger(mesh, MESHLINK_ERROR, "Cannot write config file: %s", strerror(errno));
214 meshlink_errno = MESHLINK_ESTORAGE;
221 char filename[PATH_MAX];
222 snprintf(filename, sizeof(filename), "%s" SLASH "%s", mesh->confbase, key);
225 if(unlink(filename) && errno != ENOENT) {
226 logger(mesh, MESHLINK_ERROR, "Failed to remove `%s': %s", filename, strerror(errno));
227 meshlink_errno = MESHLINK_ESTORAGE;
234 char tmp_filename[PATH_MAX];
235 snprintf(tmp_filename, sizeof(tmp_filename), "%s" SLASH "%s.tmp", mesh->confbase, key);
237 FILE *f = fopen(tmp_filename, "w");
240 logger(mesh, MESHLINK_ERROR, "Failed to open `%s': %s", tmp_filename, strerror(errno));
241 meshlink_errno = MESHLINK_ESTORAGE;
245 if(fwrite(data, len, 1, f) != 1) {
246 logger(mesh, MESHLINK_ERROR, "Cannot write config file: %s", strerror(errno));
247 meshlink_errno = MESHLINK_ESTORAGE;
253 logger(mesh, MESHLINK_ERROR, "Failed to flush file: %s", strerror(errno));
254 meshlink_errno = MESHLINK_ESTORAGE;
259 if(fsync(fileno(f))) {
260 logger(mesh, MESHLINK_ERROR, "Failed to sync file: %s\n", strerror(errno));
261 meshlink_errno = MESHLINK_ESTORAGE;
267 logger(mesh, MESHLINK_ERROR, "Failed to close `%s': %s", filename, strerror(errno));
268 meshlink_errno = MESHLINK_ESTORAGE;
272 if(rename(tmp_filename, filename)) {
273 logger(mesh, MESHLINK_ERROR, "Failed to rename `%s' to `%s': %s", tmp_filename, filename, strerror(errno));
274 meshlink_errno = MESHLINK_ESTORAGE;
281 /// Read a configuration file, decrypting it if necessary.
282 bool config_load(meshlink_handle_t *mesh, const char *name, config_t *config) {
284 uint8_t *buf = xmalloc(buflen);
287 if(!load(mesh, name, buf, &len)) {
291 buf = xrealloc(buf, len);
296 if(!load(mesh, name, (void **)&buf, &len) || len != buflen) {
297 meshlink_errno = MESHLINK_ESTORAGE;
302 if(mesh->config_key) {
304 logger(mesh, MESHLINK_ERROR, "Cannot decrypt config file\n");
305 meshlink_errno = MESHLINK_ESTORAGE;
310 size_t decrypted_len = len - 12 - 16;
311 uint8_t *decrypted = xmalloc(decrypted_len);
313 chacha_poly1305_ctx_t *ctx = chacha_poly1305_init();
314 chacha_poly1305_set_key(ctx, mesh->config_key);
316 if(chacha_poly1305_decrypt_iv96(ctx, buf, buf + 12, len - 12, decrypted, &decrypted_len)) {
317 chacha_poly1305_exit(ctx);
322 logger(mesh, MESHLINK_ERROR, "Cannot decrypt config file\n");
323 meshlink_errno = MESHLINK_ESTORAGE;
324 chacha_poly1305_exit(ctx);
337 bool config_exists(meshlink_handle_t *mesh, const char *name) {
340 return load(mesh, name, NULL, &len) && len;
343 /// Write a configuration file, encrypting it if necessary.
344 bool config_store(meshlink_handle_t *mesh, const char *name, const config_t *config) {
345 if(mesh->config_key) {
346 size_t encrypted_len = config->len + 16; // length of encrypted data
347 uint8_t encrypted[12 + encrypted_len]; // store sequence number at the start
349 randomize(encrypted, 12);
350 chacha_poly1305_ctx_t *ctx = chacha_poly1305_init();
351 chacha_poly1305_set_key(ctx, mesh->config_key);
353 if(!chacha_poly1305_encrypt_iv96(ctx, encrypted, config->buf, config->len, encrypted + 12, &encrypted_len)) {
354 logger(mesh, MESHLINK_ERROR, "Cannot encrypt config file\n");
355 meshlink_errno = MESHLINK_ESTORAGE;
356 chacha_poly1305_exit(ctx);
360 chacha_poly1305_exit(ctx);
362 return store(mesh, name, encrypted, 12 + encrypted_len);
365 return store(mesh, name, config->buf, config->len);
368 /// Free resources of a loaded configuration file.
369 void config_free(config_t *config) {
370 assert(!config->len || config->buf);
372 free((uint8_t *)config->buf);
377 bool config_ls(meshlink_handle_t *mesh, config_scan_action_t action) {
378 logger(mesh, MESHLINK_DEBUG, "ls(%p)", (void *)(intptr_t)action);
380 if(!mesh->confbase) {
385 return mesh->ls_cb(mesh, action);
391 dir = opendir(mesh->confbase);
394 logger(mesh, MESHLINK_ERROR, "Could not open %s: %s", mesh->confbase, strerror(errno));
395 meshlink_errno = MESHLINK_ESTORAGE;
399 while((ent = readdir(dir))) {
400 if(ent->d_name[0] == '.') {
404 if(!action(mesh, ent->d_name, 0)) {
414 /// Re-encrypt a configuration file.
415 static bool change_key(meshlink_handle_t *mesh, const char *name, size_t len) {
419 if(!config_load(mesh, name, &config)) {
423 size_t name_len = strlen(name);
424 char new_name[name_len + 3];
425 memcpy(new_name, name, name_len);
427 if(!strcmp(name, "meshlink.conf")) {
428 // Update meshlink.conf in-place
429 new_name[name_len] = 0;
431 memcpy(new_name + name_len, ".r", 3);
434 void *orig_key = mesh->config_key;
435 mesh->config_key = mesh->ls_priv;
436 bool result = config_store(mesh, new_name, &config);
437 mesh->config_key = orig_key;
442 extern bool (*devtool_keyrotate_probe)(int stage);
444 static bool change_node_key(meshlink_handle_t *mesh, const char *name, size_t len) {
445 /* Skip the main config and lock files */
446 if(!strcmp(name, "meshlink.conf") || !strcmp(name, "meshlink.lock")) {
450 /* Skip any already rotated file */
451 int namelen = strlen(name);
453 if(namelen >= 2 && !strcmp(name + namelen - 2, ".r")) {
457 if(!devtool_keyrotate_probe(0)) {
461 return change_key(mesh, name, len);
464 static bool cleanup_old_file(meshlink_handle_t *mesh, const char *name, size_t len) {
466 size_t name_len = strlen(name);
468 if(name_len < 3 || strcmp(name + name_len - 2, ".r")) {
474 if(!config_load(mesh, name, &config)) {
475 store(mesh, name, NULL, 0);
479 char new_name[name_len - 1];
480 memcpy(new_name, name, name_len - 2);
481 new_name[name_len - 2] = '\0';
483 return config_store(mesh, new_name, &config) && store(mesh, name, NULL, 0);
486 bool config_cleanup_old_files(meshlink_handle_t *mesh) {
487 return config_ls(mesh, cleanup_old_file);
490 bool config_change_key(meshlink_handle_t *mesh, void *new_key) {
491 mesh->ls_priv = new_key;
493 if(!config_ls(mesh, change_node_key)) {
497 if(!devtool_keyrotate_probe(1)) {
501 if(!change_key(mesh, "meshlink.conf", 0)) {
505 free(mesh->config_key);
506 mesh->config_key = new_key;
508 if(!devtool_keyrotate_probe(2)) {
512 config_cleanup_old_files(mesh);
514 devtool_keyrotate_probe(3);
519 /// Migrate old format configuration directory
520 static bool config_migrate(meshlink_handle_t *mesh) {
523 char new_path[PATH_MAX];
525 // Check if there we need to migrate
527 snprintf(path, sizeof path, "%s/meshlink.conf", mesh->confbase);
529 if(access(path, F_OK) == 0) {
533 snprintf(path, sizeof path, "%s/current/meshlink.conf", mesh->confbase);
535 if(access(path, F_OK) == -1) {
539 // Migrate host config files
544 snprintf(base, sizeof base, "%s/current/hosts", mesh->confbase);
548 logger(NULL, MESHLINK_ERROR, "Failed to migrate %s to %s: %s", base, mesh->confbase, strerror(errno));
552 while((ent = readdir(dir))) {
553 if(ent->d_name[0] == '.') {
557 if(!check_id(ent->d_name)) {
561 snprintf(path, sizeof path, "%s/current/hosts/%s", mesh->confbase, ent->d_name);
562 snprintf(new_path, sizeof new_path, "%s/%s", mesh->confbase, ent->d_name);
564 if(rename(path, new_path) == -1) {
565 logger(NULL, MESHLINK_ERROR, "Failed to migrate %s to %s: %s", path, new_path, strerror(errno));
573 // Migrate invitation files
575 snprintf(base, sizeof base, "%s/current/invitations", mesh->confbase);
579 logger(NULL, MESHLINK_ERROR, "Failed to migrate %s to %s: %s", base, mesh->confbase, strerror(errno));
583 while((ent = readdir(dir))) {
584 if(ent->d_name[0] == '.') {
588 snprintf(path, sizeof path, "%s/current/invitations/%s", mesh->confbase, ent->d_name);
589 snprintf(new_path, sizeof new_path, "%s/%s.inv", mesh->confbase, ent->d_name);
591 if(rename(path, new_path) == -1) {
592 logger(NULL, MESHLINK_ERROR, "Failed to migrate %s to %s: %s", path, new_path, strerror(errno));
600 // Migrate meshlink.conf
602 snprintf(path, sizeof path, "%s/current/meshlink.conf", mesh->confbase);
603 snprintf(new_path, sizeof new_path, "%s/meshlink.conf", mesh->confbase);
605 if(rename(path, new_path) == -1) {
606 logger(NULL, MESHLINK_ERROR, "Failed to migrate %s to %s: %s", path, new_path, strerror(errno));
610 // Remove directories that should now be empty
612 snprintf(base, sizeof base, "%s/current/hosts", mesh->confbase);
614 snprintf(base, sizeof base, "%s/current/invitations", mesh->confbase);
616 snprintf(base, sizeof base, "%s/current", mesh->confbase);
625 /// Initialize the configuration directory
626 bool config_init(meshlink_handle_t *mesh, const struct meshlink_open_params *params) {
627 if(!mesh->confbase) {
632 if(mkdir(mesh->confbase, 0700) && errno != EEXIST) {
633 logger(NULL, MESHLINK_ERROR, "Cannot create configuration directory %s: %s", mesh->confbase, strerror(errno));
634 meshlink_close(mesh);
635 meshlink_errno = MESHLINK_ESTORAGE;
639 mesh->lockfile = fopen(params->lock_filename, "w+");
641 if(!mesh->lockfile) {
642 logger(NULL, MESHLINK_ERROR, "Cannot not open %s: %s\n", params->lock_filename, strerror(errno));
643 meshlink_errno = MESHLINK_ESTORAGE;
648 fcntl(fileno(mesh->lockfile), F_SETFD, FD_CLOEXEC);
653 if(flock(fileno(mesh->lockfile), LOCK_EX | LOCK_NB) != 0) {
654 logger(NULL, MESHLINK_ERROR, "Cannot lock %s: %s\n", params->lock_filename, strerror(errno));
655 fclose(mesh->lockfile);
656 mesh->lockfile = NULL;
657 meshlink_errno = MESHLINK_EBUSY;
663 if(!config_migrate(mesh)) {
671 void config_exit(meshlink_handle_t *mesh) {
673 fclose(mesh->lockfile);
674 mesh->lockfile = NULL;
678 static void make_invitation_path(const char *name, char *path, size_t len) {
683 snprintf(path, len, "%s.inv", name);
686 /// Read an invitation file from the confbase sub-directory, and immediately delete it.
687 bool invitation_read(meshlink_handle_t *mesh, const char *name, config_t *config) {
691 char invitation_name[PATH_MAX];
692 make_invitation_path(name, invitation_name, sizeof(invitation_name));
694 if(!config_load(mesh, invitation_name, config)) {
695 logger(mesh, MESHLINK_ERROR, "Could not read invitation file %s\n", invitation_name);
699 // Make sure the file is deleted so it cannot be reused
700 if(!invalidate_config_file(mesh, invitation_name, 0)) {
701 logger(mesh, MESHLINK_ERROR, "Could not delete invitation file %s\n", invitation_name);
709 /// Write an invitation file.
710 bool invitation_write(meshlink_handle_t *mesh, const char *name, const config_t *config) {
714 char invitation_name[PATH_MAX];
715 make_invitation_path(name, invitation_name, sizeof(invitation_name));
717 if(!config_store(mesh, invitation_name, config)) {
718 logger(mesh, MESHLINK_ERROR, "Could not write invitation file %s\n", invitation_name);
727 const char *node_name;
731 static bool purge_cb(meshlink_handle_t *mesh, const char *name, size_t len) {
733 purge_info_t *info = mesh->ls_priv;
734 size_t namelen = strlen(name);
736 // Skip anything that is not an invitation
737 if(namelen < 4 || strcmp(name + namelen - 4, ".inv") != 0) {
743 if(!config_load(mesh, name, &config)) {
744 logger(mesh, MESHLINK_ERROR, "Could not read invitation file %s\n", name);
745 // Purge anything we can't read
746 invalidate_config_file(mesh, name, 0);
751 packmsg_input_t in = {config.buf, config.len};
752 uint32_t version = packmsg_get_uint32(&in);
753 time_t timestamp = packmsg_get_int64(&in);
755 if(!packmsg_input_ok(&in) || version != MESHLINK_INVITATION_VERSION) {
756 logger(mesh, MESHLINK_ERROR, "Invalid invitation file %s\n", name);
757 invalidate_config_file(mesh, name, 0);
761 if(info->node_name) {
762 char *node_name = packmsg_get_str_dup(&in);
764 if(!node_name || strcmp(node_name, info->node_name) == 0) {
765 invalidate_config_file(mesh, name, 0);
770 } else if(timestamp < info->deadline) {
771 invalidate_config_file(mesh, name, 0);
775 config_free(&config);
779 /// Purge old invitation files
780 size_t invitation_purge_old(meshlink_handle_t *mesh, time_t deadline) {
781 purge_info_t info = {deadline, NULL, 0};
782 mesh->ls_priv = &info;
784 if(!config_ls(mesh, purge_cb)) {
785 /* Ignore any failure */
791 /// Purge invitations for the given node
792 size_t invitation_purge_node(meshlink_handle_t *mesh, const char *node_name) {
793 purge_info_t info = {0, node_name, 0};
794 mesh->ls_priv = &info;
796 if(!config_ls(mesh, purge_cb)) {
797 /* Ignore any failure */