]> git.meshlink.io Git - meshlink/blobdiff - src/meshlink.c
Don't add duplicates to the list of recently seen addresses.
[meshlink] / src / meshlink.c
index 54187a59ed9673a63b166eab73b88b44dbce85bd..69ab8f244ef1e715e171b2b13fd8ff521fb27dab 100644 (file)
@@ -662,11 +662,11 @@ static bool finalize_join(meshlink_handle_t *mesh, const void *buf, uint16_t len
        }
 
        /* Ensure the configuration directory metadata is on disk */
-       if(!config_sync(mesh, "current")) {
+       if(!config_sync(mesh, "current") || !sync_path(mesh->confbase)) {
                return false;
        }
 
-       sptps_send_record(&(mesh->sptps), 1, ecdsa_get_public_key(mesh->private_key), 32);
+       sptps_send_record(&mesh->sptps, 1, ecdsa_get_public_key(mesh->private_key), 32);
 
        logger(mesh, MESHLINK_DEBUG, "Configuration stored in: %s\n", mesh->confbase);
 
@@ -699,7 +699,7 @@ static bool invitation_receive(void *handle, uint8_t type, const void *msg, uint
 
        switch(type) {
        case SPTPS_HANDSHAKE:
-               return sptps_send_record(&(mesh->sptps), 0, mesh->cookie, sizeof(mesh)->cookie);
+               return sptps_send_record(&mesh->sptps, 0, mesh->cookie, sizeof(mesh)->cookie);
 
        case 0:
                return finalize_join(mesh, msg, len);
@@ -796,6 +796,7 @@ static const char *errstr[] = {
        [MESHLINK_EPEER] = "Error communicating with peer",
        [MESHLINK_ENOTSUP] = "Operation not supported",
        [MESHLINK_EBUSY] = "MeshLink instance already in use",
+       [MESHLINK_EBLACKLISTED] = "Node is blacklisted",
 };
 
 const char *meshlink_strerror(meshlink_errno_t err) {
@@ -867,6 +868,18 @@ static void add_local_addresses(meshlink_handle_t *mesh) {
 }
 
 static bool meshlink_setup(meshlink_handle_t *mesh) {
+       if(!config_destroy(mesh->confbase, "new")) {
+               logger(mesh, MESHLINK_ERROR, "Could not delete configuration in %s/new: %s\n", mesh->confbase, strerror(errno));
+               meshlink_errno = MESHLINK_ESTORAGE;
+               return false;
+       }
+
+       if(!config_destroy(mesh->confbase, "old")) {
+               logger(mesh, MESHLINK_ERROR, "Could not delete configuration in %s/old: %s\n", mesh->confbase, strerror(errno));
+               meshlink_errno = MESHLINK_ESTORAGE;
+               return false;
+       }
+
        if(!config_init(mesh, "current")) {
                logger(mesh, MESHLINK_ERROR, "Could not set up configuration in %s/current: %s\n", mesh->confbase, strerror(errno));
                meshlink_errno = MESHLINK_ESTORAGE;
@@ -889,6 +902,7 @@ static bool meshlink_setup(meshlink_handle_t *mesh) {
        mesh->self->name = xstrdup(mesh->name);
        mesh->self->devclass = mesh->devclass;
        mesh->self->ecdsa = ecdsa_set_public_key(ecdsa_get_public_key(mesh->private_key));
+       mesh->self->session_id = mesh->session_id;
 
        if(!write_main_config_files(mesh)) {
                logger(mesh, MESHLINK_ERROR, "Could not write main config files into %s/current: %s\n", mesh->confbase, strerror(errno));
@@ -901,23 +915,10 @@ static bool meshlink_setup(meshlink_handle_t *mesh) {
                return false;
        }
 
-       if(!main_config_lock(mesh)) {
-               logger(NULL, MESHLINK_ERROR, "Cannot lock main config file\n");
-               meshlink_errno = MESHLINK_ESTORAGE;
-               return false;
-       }
-
        return true;
 }
 
 static bool meshlink_read_config(meshlink_handle_t *mesh) {
-       // Open the configuration file and lock it
-       if(!main_config_lock(mesh)) {
-               logger(NULL, MESHLINK_ERROR, "Cannot lock main config file\n");
-               meshlink_errno = MESHLINK_ESTORAGE;
-               return false;
-       }
-
        config_t config;
 
        if(!main_config_read(mesh, "current", &config, mesh->config_key)) {
@@ -967,6 +968,7 @@ static bool meshlink_read_config(meshlink_handle_t *mesh) {
        mesh->self = new_node();
        mesh->self->name = xstrdup(name);
        mesh->self->devclass = mesh->devclass;
+       mesh->self->session_id = mesh->session_id;
 
        if(!node_read_public_key(mesh, mesh->self)) {
                logger(NULL, MESHLINK_ERROR, "Could not read our host configuration file!");
@@ -1101,14 +1103,11 @@ bool meshlink_encrypted_key_rotate(meshlink_handle_t *mesh, const void *new_key,
 
        devtool_keyrotate_probe(1);
 
-       main_config_unlock(mesh);
-
        // Rename confbase/current/ to confbase/old
 
        if(!config_rename(mesh, "current", "old")) {
                logger(mesh, MESHLINK_ERROR, "Cannot rename %s/current to %s/old\n", mesh->confbase, mesh->confbase);
                meshlink_errno = MESHLINK_ESTORAGE;
-               main_config_lock(mesh);
                pthread_mutex_unlock(&mesh->mutex);
                return false;
        }
@@ -1120,18 +1119,12 @@ bool meshlink_encrypted_key_rotate(meshlink_handle_t *mesh, const void *new_key,
        if(!config_rename(mesh, "new", "current")) {
                logger(mesh, MESHLINK_ERROR, "Cannot rename %s/new to %s/current\n", mesh->confbase, mesh->confbase);
                meshlink_errno = MESHLINK_ESTORAGE;
-               main_config_lock(mesh);
                pthread_mutex_unlock(&mesh->mutex);
                return false;
        }
 
        devtool_keyrotate_probe(3);
 
-       if(!main_config_lock(mesh)) {
-               pthread_mutex_unlock(&mesh->mutex);
-               return false;
-       }
-
        // Cleanup the "old" confbase sub-directory
 
        if(!config_destroy(mesh->confbase, "old")) {
@@ -1288,6 +1281,10 @@ meshlink_handle_t *meshlink_open_ex(const meshlink_open_params_t *params) {
 
        randomize(&mesh->prng_state, sizeof(mesh->prng_state));
 
+       do {
+               randomize(&mesh->session_id, sizeof(mesh->session_id));
+       } while(mesh->session_id == 0);
+
        memcpy(mesh->dev_class_traits, default_class_traits, sizeof(default_class_traits));
 
        if(usingname) {
@@ -1318,6 +1315,12 @@ meshlink_handle_t *meshlink_open_ex(const meshlink_open_params_t *params) {
 
        meshlink_queue_init(&mesh->outpacketqueue);
 
+       // Atomically lock the configuration directory.
+       if(!main_config_lock(mesh)) {
+               meshlink_close(mesh);
+               return NULL;
+       }
+
        // If no configuration exists yet, create it.
 
        if(!meshlink_confbase_exists(mesh)) {
@@ -1370,7 +1373,11 @@ meshlink_handle_t *meshlink_open_ex(const meshlink_open_params_t *params) {
        }
 
        add_local_addresses(mesh);
-       node_write_config(mesh, mesh->self);
+
+       if(!node_write_config(mesh, mesh->self)) {
+               logger(NULL, MESHLINK_ERROR, "Cannot update configuration\n");
+               return NULL;
+       }
 
        idle_set(&mesh->loop, idle, mesh);
 
@@ -1506,6 +1513,8 @@ bool meshlink_start(meshlink_handle_t *mesh) {
 
        pthread_cond_wait(&mesh->cond, &mesh->mutex);
        mesh->threadstarted = true;
+       mesh->self->last_reachable = time(NULL);
+       mesh->self->status.dirty = true;
 
        pthread_mutex_unlock(&mesh->mutex);
        return true;
@@ -1520,6 +1529,11 @@ void meshlink_stop(meshlink_handle_t *mesh) {
        pthread_mutex_lock(&mesh->mutex);
        logger(mesh, MESHLINK_DEBUG, "meshlink_stop called\n");
 
+       if(mesh->self) {
+               mesh->self->last_unreachable = time(NULL);
+               mesh->self->status.dirty = true;
+       }
+
        // Shut down the main thread
        event_loop_stop(&mesh->loop);
 
@@ -1559,12 +1573,11 @@ void meshlink_stop(meshlink_handle_t *mesh) {
 
        exit_outgoings(mesh);
 
-       // Write out any changed node config files
+       // Try to write out any changed node config files, ignore errors at this point.
        if(mesh->nodes) {
                for splay_each(node_t, n, mesh->nodes) {
                        if(n->status.dirty) {
-                               node_write_config(mesh, n);
-                               n->status.dirty = false;
+                               n->status.dirty = !node_write_config(mesh, n);
                        }
                }
        }
@@ -1634,16 +1647,56 @@ bool meshlink_destroy(const char *confbase) {
                return false;
        }
 
-       if(!config_destroy(confbase, "current")) {
-               logger(NULL, MESHLINK_ERROR, "Cannot remove confbase sub-directories %s: %s\n", confbase, strerror(errno));
+       /* Exit early if the confbase directory itself doesn't exist */
+       if(access(confbase, F_OK) && errno == ENOENT) {
+               return true;
+       }
+
+       /* Take the lock the same way meshlink_open() would. */
+       char lockfilename[PATH_MAX];
+       snprintf(lockfilename, sizeof(lockfilename), "%s" SLASH "meshlink.lock", confbase);
+
+       FILE *lockfile = fopen(lockfilename, "w+");
+
+       if(!lockfile) {
+               logger(NULL, MESHLINK_ERROR, "Could not open lock file %s: %s", lockfilename, strerror(errno));
+               meshlink_errno = MESHLINK_ESTORAGE;
                return false;
        }
 
-       config_destroy(confbase, "new");
-       config_destroy(confbase, "old");
+#ifdef FD_CLOEXEC
+       fcntl(fileno(lockfile), F_SETFD, FD_CLOEXEC);
+#endif
+
+#ifdef HAVE_MINGW
+       // TODO: use _locking()?
+#else
+
+       if(flock(fileno(lockfile), LOCK_EX | LOCK_NB) != 0) {
+               logger(NULL, MESHLINK_ERROR, "Configuration directory %s still in use\n", lockfilename);
+               fclose(lockfile);
+               meshlink_errno = MESHLINK_EBUSY;
+               return false;
+       }
 
-       if(rmdir(confbase) && errno != ENOENT) {
-               logger(NULL, MESHLINK_ERROR, "Cannot remove directory %s: %s\n", confbase, strerror(errno));
+#endif
+
+       if(!config_destroy(confbase, "current") || !config_destroy(confbase, "new") || !config_destroy(confbase, "old")) {
+               logger(NULL, MESHLINK_ERROR, "Cannot remove sub-directories in %s: %s\n", confbase, strerror(errno));
+               return false;
+       }
+
+       if(unlink(lockfilename)) {
+               logger(NULL, MESHLINK_ERROR, "Cannot remove lock file %s: %s\n", lockfilename, strerror(errno));
+               fclose(lockfile);
+               meshlink_errno = MESHLINK_ESTORAGE;
+               return false;
+       }
+
+       fclose(lockfile);
+
+       if(!sync_path(confbase)) {
+               logger(NULL, MESHLINK_ERROR, "Cannot sync directory %s: %s\n", confbase, strerror(errno));
                meshlink_errno = MESHLINK_ESTORAGE;
                return false;
        }
@@ -1751,6 +1804,7 @@ bool meshlink_send(meshlink_handle_t *mesh, meshlink_node_t *destination, const
 
        if(n->status.blacklisted) {
                logger(mesh, MESHLINK_ERROR, "Node %s blacklisted, dropping packet\n", n->name);
+               meshlink_errno = MESHLINK_EBLACKLISTED;
                return false;
        }
 
@@ -1783,7 +1837,7 @@ bool meshlink_send(meshlink_handle_t *mesh, meshlink_node_t *destination, const
        }
 
        // Notify event loop
-       signal_trigger(&(mesh->loop), &(mesh->datafromapp));
+       signal_trigger(&mesh->loop, &mesh->datafromapp);
 
        return true;
 }
@@ -1868,17 +1922,17 @@ meshlink_node_t *meshlink_get_node(meshlink_handle_t *mesh, const char *name) {
                return NULL;
        }
 
-       meshlink_node_t *node = NULL;
+       node_t *n = NULL;
 
        pthread_mutex_lock(&mesh->mutex);
-       node = (meshlink_node_t *)lookup_node(mesh, (char *)name); // TODO: make lookup_node() use const
+       n = lookup_node(mesh, (char *)name); // TODO: make lookup_node() use const
        pthread_mutex_unlock(&mesh->mutex);
 
-       if(!node) {
+       if(!n) {
                meshlink_errno = MESHLINK_ENOENT;
        }
 
-       return node;
+       return (meshlink_node_t *)n;
 }
 
 meshlink_submesh_t *meshlink_get_submesh(meshlink_handle_t *mesh, const char *name) {
@@ -1939,8 +1993,8 @@ static meshlink_node_t **meshlink_get_all_nodes_by_condition(meshlink_handle_t *
        *nmemb = 0;
 
        for splay_each(node_t, n, mesh->nodes) {
-               if(true == search_node(n, condition)) {
-                       *nmemb = *nmemb + 1;
+               if(search_node(n, condition)) {
+                       ++*nmemb;
                }
        }
 
@@ -1956,7 +2010,7 @@ static meshlink_node_t **meshlink_get_all_nodes_by_condition(meshlink_handle_t *
                meshlink_node_t **p = result;
 
                for splay_each(node_t, n, mesh->nodes) {
-                       if(true == search_node(n, condition)) {
+                       if(search_node(n, condition)) {
                                *p++ = (meshlink_node_t *)n;
                        }
                }
@@ -1989,6 +2043,31 @@ static bool search_node_by_submesh(const node_t *node, const void *condition) {
        return false;
 }
 
+struct time_range {
+       time_t start;
+       time_t end;
+};
+
+static bool search_node_by_last_reachable(const node_t *node, const void *condition) {
+       const struct time_range *range = condition;
+       time_t start = node->last_reachable;
+       time_t end = node->last_unreachable;
+
+       if(end < start) {
+               end = time(NULL);
+
+               if(end < start) {
+                       start = end;
+               }
+       }
+
+       if(range->end >= range->start) {
+               return start <= range->end && end >= range->start;
+       } else {
+               return start > range->start || end < range->end;
+       }
+}
+
 meshlink_node_t **meshlink_get_all_nodes_by_dev_class(meshlink_handle_t *mesh, dev_class_t devclass, meshlink_node_t **nodes, size_t *nmemb) {
        if(!mesh || devclass < 0 || devclass >= DEV_CLASS_COUNT || !nmemb) {
                meshlink_errno = MESHLINK_EINVAL;
@@ -2007,6 +2086,17 @@ meshlink_node_t **meshlink_get_all_nodes_by_submesh(meshlink_handle_t *mesh, mes
        return meshlink_get_all_nodes_by_condition(mesh, submesh, nodes, nmemb, search_node_by_submesh);
 }
 
+meshlink_node_t **meshlink_get_all_nodes_by_last_reachable(meshlink_handle_t *mesh, time_t start, time_t end, meshlink_node_t **nodes, size_t *nmemb) {
+       if(!mesh || !nmemb) {
+               meshlink_errno = MESHLINK_EINVAL;
+               return NULL;
+       }
+
+       struct time_range range = {start, end};
+
+       return meshlink_get_all_nodes_by_condition(mesh, &range, nodes, nmemb, search_node_by_last_reachable);
+}
+
 dev_class_t meshlink_get_node_dev_class(meshlink_handle_t *mesh, meshlink_node_t *node) {
        if(!mesh || !node) {
                meshlink_errno = MESHLINK_EINVAL;
@@ -2136,11 +2226,15 @@ bool meshlink_set_canonical_address(meshlink_handle_t *mesh, meshlink_node_t *no
        node_t *n = (node_t *)node;
        free(n->canonical_address);
        n->canonical_address = canonical_address;
-       node_write_config(mesh, n);
+
+       if(!node_write_config(mesh, n)) {
+               pthread_mutex_unlock(&mesh->mutex);
+               return false;
+       }
 
        pthread_mutex_unlock(&mesh->mutex);
 
-       return true;
+       return config_sync(mesh, "current");
 }
 
 bool meshlink_add_address(meshlink_handle_t *mesh, const char *address) {
@@ -2221,6 +2315,7 @@ bool meshlink_set_port(meshlink_handle_t *mesh, int port) {
        mesh->self = new_node();
        mesh->self->name = xstrdup(mesh->name);
        mesh->self->devclass = mesh->devclass;
+       mesh->self->session_id = mesh->session_id;
        xasprintf(&mesh->myport, "%d", port);
 
        if(!node_read_public_key(mesh, mesh->self)) {
@@ -2659,7 +2754,7 @@ char *meshlink_export(meshlink_handle_t *mesh) {
 
        uint32_t count = 0;
 
-       for(uint32_t i = 0; i < 5; i++) {
+       for(uint32_t i = 0; i < MAX_RECENT; i++) {
                if(mesh->self->recent[i].sa.sa_family) {
                        count++;
                } else {
@@ -2673,6 +2768,9 @@ char *meshlink_export(meshlink_handle_t *mesh) {
                packmsg_add_sockaddr(&out, &mesh->self->recent[i]);
        }
 
+       packmsg_add_int64(&out, 0);
+       packmsg_add_int64(&out, 0);
+
        pthread_mutex_unlock(&mesh->mutex);
 
        if(!packmsg_output_ok(&out)) {
@@ -2771,7 +2869,11 @@ bool meshlink_import(meshlink_handle_t *mesh, const char *data) {
                        break;
                }
 
-               config_write(mesh, "current", n->name, &config, mesh->config_key);
+               if(!config_write(mesh, "current", n->name, &config, mesh->config_key)) {
+                       free_node(n);
+                       return false;
+               }
+
                node_add(mesh, n);
        }
 
@@ -2792,40 +2894,26 @@ bool meshlink_import(meshlink_handle_t *mesh, const char *data) {
        return true;
 }
 
-void meshlink_blacklist(meshlink_handle_t *mesh, meshlink_node_t *node) {
-       if(!mesh || !node) {
-               meshlink_errno = MESHLINK_EINVAL;
-               return;
-       }
-
-       pthread_mutex_lock(&mesh->mutex);
-
-       node_t *n;
-       n = (node_t *)node;
-
+static bool blacklist(meshlink_handle_t *mesh, node_t *n) {
        if(n == mesh->self) {
-               logger(mesh, MESHLINK_ERROR, "%s blacklisting itself?\n", node->name);
+               logger(mesh, MESHLINK_ERROR, "%s blacklisting itself?\n", n->name);
                meshlink_errno = MESHLINK_EINVAL;
-               pthread_mutex_unlock(&mesh->mutex);
-               return;
+               return false;
        }
 
        if(n->status.blacklisted) {
-               logger(mesh, MESHLINK_DEBUG, "Node %s already blacklisted\n", node->name);
-               pthread_mutex_unlock(&mesh->mutex);
-               return;
+               logger(mesh, MESHLINK_DEBUG, "Node %s already blacklisted\n", n->name);
+               return true;
        }
 
        n->status.blacklisted = true;
-       node_write_config(mesh, n);
-       config_sync(mesh, "current");
 
-       logger(mesh, MESHLINK_DEBUG, "Blacklisted %s.\n", node->name);
-
-       //Immediately terminate any connections we have with the blacklisted node
+       /* Immediately shut down any connections we have with the blacklisted node.
+        * We can't call terminate_connection(), because we might be called from a callback function.
+        */
        for list_each(connection_t, c, mesh->connections) {
                if(c->node == n) {
-                       terminate_connection(mesh, c, c->status.active);
+                       shutdown(c->socket, SHUT_RDWR);
                }
        }
 
@@ -2837,46 +2925,189 @@ void meshlink_blacklist(meshlink_handle_t *mesh, meshlink_node_t *node) {
        n->mtuprobes = 0;
        n->status.udp_confirmed = false;
 
-       if(n->status.reachable) {
-               update_node_status(mesh, n);
+       /* Graph updates will suppress status updates for blacklisted nodes, so we need to
+        * manually call the status callback if necessary.
+        */
+       if(n->status.reachable && mesh->node_status_cb) {
+               mesh->node_status_cb(mesh, (meshlink_node_t *)n, false);
        }
 
-       pthread_mutex_unlock(&mesh->mutex);
+       return node_write_config(mesh, n) && config_sync(mesh, "current");
 }
 
-void meshlink_whitelist(meshlink_handle_t *mesh, meshlink_node_t *node) {
+bool meshlink_blacklist(meshlink_handle_t *mesh, meshlink_node_t *node) {
        if(!mesh || !node) {
                meshlink_errno = MESHLINK_EINVAL;
-               return;
+               return false;
        }
 
        pthread_mutex_lock(&mesh->mutex);
 
-       node_t *n = (node_t *)node;
+       if(!blacklist(mesh, (node_t *)node)) {
+               pthread_mutex_unlock(&mesh->mutex);
+               return false;
+       }
 
-       if(!n->status.blacklisted) {
-               logger(mesh, MESHLINK_DEBUG, "Node %s was already whitelisted\n", node->name);
+       pthread_mutex_unlock(&mesh->mutex);
+
+       logger(mesh, MESHLINK_DEBUG, "Blacklisted %s.\n", node->name);
+       return true;
+}
+
+bool meshlink_blacklist_by_name(meshlink_handle_t *mesh, const char *name) {
+       if(!mesh || !name) {
                meshlink_errno = MESHLINK_EINVAL;
+               return false;
+       }
+
+       pthread_mutex_lock(&mesh->mutex);
+
+       node_t *n = lookup_node(mesh, (char *)name);
+
+       if(!n) {
+               n = new_node();
+               n->name = xstrdup(name);
+               node_add(mesh, n);
+       }
+
+       if(!blacklist(mesh, (node_t *)n)) {
                pthread_mutex_unlock(&mesh->mutex);
-               return;
+               return false;
+       }
+
+       pthread_mutex_unlock(&mesh->mutex);
+
+       logger(mesh, MESHLINK_DEBUG, "Blacklisted %s.\n", name);
+       return true;
+}
+
+static bool whitelist(meshlink_handle_t *mesh, node_t *n) {
+       if(n == mesh->self) {
+               logger(mesh, MESHLINK_ERROR, "%s whitelisting itself?\n", n->name);
+               meshlink_errno = MESHLINK_EINVAL;
+               return false;
+       }
+
+       if(!n->status.blacklisted) {
+               logger(mesh, MESHLINK_DEBUG, "Node %s was already whitelisted\n", n->name);
+               return true;
        }
 
        n->status.blacklisted = false;
-       node_write_config(mesh, n);
-       config_sync(mesh, "current");
 
        if(n->status.reachable) {
                update_node_status(mesh, n);
        }
 
+       return node_write_config(mesh, n) && config_sync(mesh, "current");
+}
+
+bool meshlink_whitelist(meshlink_handle_t *mesh, meshlink_node_t *node) {
+       if(!mesh || !node) {
+               meshlink_errno = MESHLINK_EINVAL;
+               return false;
+       }
+
+       pthread_mutex_lock(&mesh->mutex);
+
+       if(!whitelist(mesh, (node_t *)node)) {
+               pthread_mutex_unlock(&mesh->mutex);
+               return false;
+       }
+
        pthread_mutex_unlock(&mesh->mutex);
-       return;
+
+       logger(mesh, MESHLINK_DEBUG, "Whitelisted %s.\n", node->name);
+       return true;
+}
+
+bool meshlink_whitelist_by_name(meshlink_handle_t *mesh, const char *name) {
+       if(!mesh || !name) {
+               meshlink_errno = MESHLINK_EINVAL;
+               return false;
+       }
+
+       pthread_mutex_lock(&mesh->mutex);
+
+       node_t *n = lookup_node(mesh, (char *)name);
+
+       if(!n) {
+               n = new_node();
+               n->name = xstrdup(name);
+               node_add(mesh, n);
+       }
+
+       if(!whitelist(mesh, (node_t *)n)) {
+               pthread_mutex_unlock(&mesh->mutex);
+               return false;
+       }
+
+       pthread_mutex_unlock(&mesh->mutex);
+
+       logger(mesh, MESHLINK_DEBUG, "Whitelisted %s.\n", name);
+       return true;
 }
 
 void meshlink_set_default_blacklist(meshlink_handle_t *mesh, bool blacklist) {
        mesh->default_blacklist = blacklist;
 }
 
+bool meshlink_forget_node(meshlink_handle_t *mesh, meshlink_node_t *node) {
+       if(!mesh || !node) {
+               meshlink_errno = MESHLINK_EINVAL;
+               return false;
+       }
+
+       node_t *n = (node_t *)node;
+
+       pthread_mutex_lock(&mesh->mutex);
+
+       /* Check that the node is not reachable */
+       if(n->status.reachable || n->connection) {
+               pthread_mutex_unlock(&mesh->mutex);
+               logger(mesh, MESHLINK_WARNING, "Could not forget %s: still reachable", n->name);
+               return false;
+       }
+
+       /* Check that we don't have any active UTCP connections */
+       if(n->utcp && utcp_is_active(n->utcp)) {
+               pthread_mutex_unlock(&mesh->mutex);
+               logger(mesh, MESHLINK_WARNING, "Could not forget %s: active UTCP connections", n->name);
+               return false;
+       }
+
+       /* Check that we have no active connections to this node */
+       for list_each(connection_t, c, mesh->connections) {
+               if(c->node == n) {
+                       pthread_mutex_unlock(&mesh->mutex);
+                       logger(mesh, MESHLINK_WARNING, "Could not forget %s: active connection", n->name);
+                       return false;
+               }
+       }
+
+       /* Remove any pending outgoings to this node */
+       if(mesh->outgoings) {
+               for list_each(outgoing_t, outgoing, mesh->outgoings) {
+                       if(outgoing->node == n) {
+                               list_delete_node(mesh->outgoings, node);
+                       }
+               }
+       }
+
+       /* Delete the config file for this node */
+       if(!config_delete(mesh, "current", n->name)) {
+               pthread_mutex_unlock(&mesh->mutex);
+               return false;
+       }
+
+       /* Delete the node struct and any remaining edges referencing this node */
+       node_del(mesh, n);
+
+       pthread_mutex_unlock(&mesh->mutex);
+
+       return config_sync(mesh, "current");
+}
+
 /* Hint that a hostname may be found at an address
  * See header file for detailed comment.
  */
@@ -2889,9 +3120,12 @@ void meshlink_hint_address(meshlink_handle_t *mesh, meshlink_node_t *node, const
        pthread_mutex_lock(&mesh->mutex);
 
        node_t *n = (node_t *)node;
-       memmove(n->recent + 1, n->recent, 4 * sizeof(*n->recent));
-       memcpy(n->recent, addr, SALEN(*addr));
-       node_write_config(mesh, n);
+
+       if(node_add_recent_address(mesh, n, (sockaddr_t *)addr)) {
+               if(!node_write_config(mesh, n)) {
+                       logger(mesh, MESHLINK_DEBUG, "Could not update %s\n", n->name);
+               }
+       }
 
        pthread_mutex_unlock(&mesh->mutex);
        // @TODO do we want to fire off a connection attempt right away?
@@ -3150,7 +3384,7 @@ void meshlink_set_channel_rcvbuf(meshlink_handle_t *mesh, meshlink_channel_t *ch
 }
 
 meshlink_channel_t *meshlink_channel_open_ex(meshlink_handle_t *mesh, meshlink_node_t *node, uint16_t port, meshlink_channel_receive_cb_t cb, const void *data, size_t len, uint32_t flags) {
-       if(data || len) {
+       if(data && len) {
                abort();        // TODO: handle non-NULL data
        }
 
@@ -3176,6 +3410,7 @@ meshlink_channel_t *meshlink_channel_open_ex(meshlink_handle_t *mesh, meshlink_n
 
        if(n->status.blacklisted) {
                logger(mesh, MESHLINK_ERROR, "Cannot open a channel with blacklisted node\n");
+               meshlink_errno = MESHLINK_EBLACKLISTED;
                pthread_mutex_unlock(&mesh->mutex);
                return NULL;
        }
@@ -3183,6 +3418,11 @@ meshlink_channel_t *meshlink_channel_open_ex(meshlink_handle_t *mesh, meshlink_n
        meshlink_channel_t *channel = xzalloc(sizeof(*channel));
        channel->node = n;
        channel->receive_cb = cb;
+
+       if(data && !len) {
+               channel->priv = (void *)data;
+       }
+
        channel->c = utcp_connect_ex(n->utcp, port, channel_recv, channel, flags);
 
        pthread_mutex_unlock(&mesh->mutex);