]> git.meshlink.io Git - meshlink/commitdiff
Add support for black/whitelisting by name, and forgetting nodes.
authorGuus Sliepen <guus@meshlink.io>
Mon, 11 Nov 2019 21:49:05 +0000 (22:49 +0100)
committerGuus Sliepen <guus@meshlink.io>
Mon, 11 Nov 2019 21:49:05 +0000 (22:49 +0100)
src/conf.c
src/conf.h
src/meshlink++.h
src/meshlink.c
src/meshlink.h
src/meshlink.sym
test/Makefile.am
test/blacklist.c [new file with mode: 0644]

index 9a1387126e8263d29b42bb0605be746718133bb4..212b7c4ac7ae53d832bfe9e54e89344fa36b982e 100644 (file)
@@ -736,6 +736,27 @@ bool config_write(meshlink_handle_t *mesh, const char *conf_subdir, const char *
        return true;
 }
 
+/// Delete a host configuration file.
+bool config_delete(meshlink_handle_t *mesh, const char *conf_subdir, const char *name) {
+       assert(conf_subdir);
+       assert(name);
+
+       if(!mesh->confbase) {
+               return true;
+       }
+
+       char path[PATH_MAX];
+       make_host_path(mesh, conf_subdir, name, path, sizeof(path));
+
+       if(unlink(path) && errno != ENOENT) {
+               logger(mesh, MESHLINK_ERROR, "Failed to unlink `%s': %s", path, strerror(errno));
+               meshlink_errno = MESHLINK_ESTORAGE;
+               return false;
+       }
+
+       return true;
+}
+
 /// Read the main configuration file.
 bool main_config_read(meshlink_handle_t *mesh, const char *conf_subdir, config_t *config, void *key) {
        assert(conf_subdir);
index f3d1e358b0f2376c79cc6da4d717e0c8719f8b55..2f79834ac7a0e09dc2c4b581306f919a0b9e823a 100644 (file)
@@ -51,6 +51,7 @@ extern bool main_config_write(struct meshlink_handle *mesh, const char *conf_sub
 extern bool config_exists(struct meshlink_handle *mesh, const char *conf_subdir, const char *name) __attribute__((__warn_unused_result__));
 extern bool config_read(struct meshlink_handle *mesh, const char *conf_subdir, const char *name, struct config_t *, void *key) __attribute__((__warn_unused_result__));
 extern bool config_write(struct meshlink_handle *mesh, const char *conf_subdir, const char *name, const struct config_t *, void *key) __attribute__((__warn_unused_result__));
+extern bool config_delete(struct meshlink_handle *mesh, const char *conf_subdir, const char *name) __attribute__((__warn_unused_result__));
 extern bool config_scan_all(struct meshlink_handle *mesh, const char *conf_subdir, const char *conf_type, config_scan_action_t action, void *arg) __attribute__((__warn_unused_result__));
 
 extern bool invitation_read(struct meshlink_handle *mesh, const char *conf_subdir, const char *name, struct config_t *, void *key) __attribute__((__warn_unused_result__));
index 980e5b12c5d4591da998015105c5ee4bb6dae5cd..74ec06e5a5bd37f80f8e9af866be525dd183cf2e 100644 (file)
@@ -658,6 +658,29 @@ public:
                return meshlink_import(handle, data);
        }
 
+       /// Forget any information about a node.
+       /** This function allows the local node to forget any information it has about a node,
+        *  and if possible will remove any data it has stored on disk about the node.
+        *
+        *  Any open channels to this node must be closed before calling this function.
+        *
+        *  After this call returns, the node handle is invalid and may no longer be used, regardless
+        *  of the return value of this call.
+        *
+        *  Note that this function does not prevent MeshLink from actually forgetting about a node,
+        *  or re-learning information about a node at a later point in time. It is merely a hint that
+        *  the application does not care about this node anymore and that any resources kept could be
+        *  cleaned up.
+        *
+        *  \memberof meshlink_node
+        *  @param node         A pointer to a struct meshlink_node describing the node to be forgotten.
+        *
+        *  @return             This function returns true if all currently known data about the node has been forgotten, false otherwise.
+        */
+       bool forget_node(node *node) {
+               return meshlink_forget_node(handle, node);
+       }
+
        /// Blacklist a node from the mesh.
        /** This function causes the local node to blacklist another node.
         *  The local node will drop any existing connections to that node,
@@ -671,6 +694,21 @@ public:
                return meshlink_blacklist(handle, node);
        }
 
+       /// Blacklist a node from the mesh by name.
+       /** This function causes the local node to blacklist another node by name.
+        *  The local node will drop any existing connections to that node,
+        *  and will not send data to it nor accept any data received from it any more.
+        *
+        *  If no node by the given name is known, it is created.
+        *
+        *  @param name         The name of the node to blacklist.
+        *
+        *  @return             This function returns true if the node has been blacklisted, false otherwise.
+        */
+       bool blacklist_by_name(const char *name) {
+               return meshlink_blacklist_by_name(handle, name);
+       }
+
        /// Whitelist a node on the mesh.
        /** This function causes the local node to whitelist another node.
         *  The local node will allow connections to and from that node,
@@ -684,6 +722,23 @@ public:
                return meshlink_whitelist(handle, node);
        }
 
+       /// Whitelist a node on the mesh by name.
+       /** This function causes the local node to whitelist a node by name.
+        *  The local node will allow connections to and from that node,
+        *  and will send data to it and accept any data received from it.
+        *
+        *  If no node by the given name is known, it is created.
+        *  This is useful if new nodes are blacklisted by default.
+        *
+        *  \memberof meshlink_node
+        *  @param node         A pointer to a struct meshlink_node describing the node to be whitelisted.
+        *
+        *  @return             This function returns true if the node has been whitelisted, false otherwise.
+        */
+       bool whitelist_by_name(const char *name) {
+               return meshlink_whitelist_by_name(handle, name);
+       }
+
        /// Set the poll callback.
        /** This functions sets the callback that is called whenever data can be sent to another node.
         *  The callback is run in MeshLink's own thread.
index 75458fe6940ed221059e39b29035a2f84de6ccc8..575895125fc67ace16e61344907b1b54f5ba62b9 100644 (file)
@@ -1904,17 +1904,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) {
@@ -1975,8 +1975,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;
                }
        }
 
@@ -1992,7 +1992,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;
                        }
                }
@@ -2837,27 +2837,15 @@ bool meshlink_import(meshlink_handle_t *mesh, const char *data) {
        return true;
 }
 
-bool meshlink_blacklist(meshlink_handle_t *mesh, meshlink_node_t *node) {
-       if(!mesh || !node) {
-               meshlink_errno = MESHLINK_EINVAL;
-               return false;
-       }
-
-       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 false;
        }
 
        if(n->status.blacklisted) {
-               logger(mesh, MESHLINK_DEBUG, "Node %s already blacklisted\n", node->name);
-               pthread_mutex_unlock(&mesh->mutex);
+               logger(mesh, MESHLINK_DEBUG, "Node %s already blacklisted\n", n->name);
                return true;
        }
 
@@ -2880,38 +2868,71 @@ bool meshlink_blacklist(meshlink_handle_t *mesh, meshlink_node_t *node) {
        n->mtuprobes = 0;
        n->status.udp_confirmed = false;
 
-       if(!node_write_config(mesh, n)) {
-               pthread_mutex_unlock(&mesh->mutex);
+       /* 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);
+       }
+
+       return node_write_config(mesh, n) && config_sync(mesh, "current");
+}
+
+bool meshlink_blacklist(meshlink_handle_t *mesh, meshlink_node_t *node) {
+       if(!mesh || !node) {
+               meshlink_errno = MESHLINK_EINVAL;
                return false;
        }
 
-       logger(mesh, MESHLINK_DEBUG, "Blacklisted %s.\n", node->name);
+       pthread_mutex_lock(&mesh->mutex);
+
+       if(!blacklist(mesh, (node_t *)node)) {
+               pthread_mutex_unlock(&mesh->mutex);
+               return false;
+       }
 
        pthread_mutex_unlock(&mesh->mutex);
 
-       return config_sync(mesh, "current");
+       logger(mesh, MESHLINK_DEBUG, "Blacklisted %s.\n", node->name);
+       return true;
 }
 
-bool meshlink_whitelist(meshlink_handle_t *mesh, meshlink_node_t *node) {
-       if(!mesh || !node) {
+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 = (node_t *)node;
+       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 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", node->name);
+               logger(mesh, MESHLINK_ERROR, "%s whitelisting itself?\n", n->name);
                meshlink_errno = MESHLINK_EINVAL;
-               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, "Node %s was already whitelisted\n", n->name);
                return true;
        }
 
@@ -2921,22 +2942,115 @@ bool meshlink_whitelist(meshlink_handle_t *mesh, meshlink_node_t *node) {
                update_node_status(mesh, n);
        }
 
-       if(!node_write_config(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);
+
        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);
 
-       return config_sync(mesh, "current");
+       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.
  */
index dd08436d78fd982957612f59896eed48960712b8..3298e7c492c90dc0aff1878d24ec5ded9c6e6168 100644 (file)
@@ -999,6 +999,28 @@ extern char *meshlink_export(struct meshlink_handle *mesh) __attribute__((__warn
  */
 extern bool meshlink_import(struct meshlink_handle *mesh, const char *data) __attribute__((__warn_unused_result__));
 
+/// Forget any information about a node.
+/** This function allows the local node to forget any information it has about a node,
+ *  and if possible will remove any data it has stored on disk about the node.
+ *
+ *  Any open channels to this node must be closed before calling this function.
+ *
+ *  After this call returns, the node handle is invalid and may no longer be used, regardless
+ *  of the return value of this call.
+ *
+ *  Note that this function does not prevent MeshLink from actually forgetting about a node,
+ *  or re-learning information about a node at a later point in time. It is merely a hint that
+ *  the application does not care about this node anymore and that any resources kept could be
+ *  cleaned up.
+ *
+ *  \memberof meshlink_node
+ *  @param mesh         A handle which represents an instance of MeshLink.
+ *  @param node         A pointer to a struct meshlink_node describing the node to be forgotten.
+ *
+ *  @return             This function returns true if all currently known data about the node has been forgotten, false otherwise.
+ */
+extern bool meshlink_forget_node(struct meshlink_handle *mesh, struct meshlink_node *node);
+
 /// Blacklist a node from the mesh.
 /** This function causes the local node to blacklist another node.
  *  The local node will drop any existing connections to that node,
@@ -1012,6 +1034,21 @@ extern bool meshlink_import(struct meshlink_handle *mesh, const char *data) __at
  */
 extern bool meshlink_blacklist(struct meshlink_handle *mesh, struct meshlink_node *node) __attribute__((__warn_unused_result__));
 
+/// Blacklist a node from the mesh by name.
+/** This function causes the local node to blacklist another node by name.
+ *  The local node will drop any existing connections to that node,
+ *  and will not send data to it nor accept any data received from it any more.
+ *
+ *  If no node by the given name is known, it is created.
+ *
+ *  \memberof meshlink_node
+ *  @param mesh         A handle which represents an instance of MeshLink.
+ *  @param name         The name of the node to blacklist.
+ *
+ *  @return             This function returns true if the node has been blacklisted, false otherwise.
+ */
+extern bool meshlink_blacklist_by_name(struct meshlink_handle *mesh, const char *name) __attribute__((__warn_unused_result__));
+
 /// Whitelist a node on the mesh.
 /** This function causes the local node to whitelist a previously blacklisted node.
  *  The local node will allow connections to and from that node,
@@ -1025,6 +1062,22 @@ extern bool meshlink_blacklist(struct meshlink_handle *mesh, struct meshlink_nod
  */
 extern bool meshlink_whitelist(struct meshlink_handle *mesh, struct meshlink_node *node) __attribute__((__warn_unused_result__));
 
+/// Whitelist a node on the mesh by name.
+/** This function causes the local node to whitelist a node by name.
+ *  The local node will allow connections to and from that node,
+ *  and will send data to it and accept any data received from it.
+ *
+ *  If no node by the given name is known, it is created.
+ *  This is useful if new nodes are blacklisted by default.
+ *
+ *  \memberof meshlink_node
+ *  @param mesh         A handle which represents an instance of MeshLink.
+ *  @param node         A pointer to a struct meshlink_node describing the node to be whitelisted.
+ *
+ *  @return             This function returns true if the node has been whitelisted, false otherwise.
+ */
+extern bool meshlink_whitelist_by_name(struct meshlink_handle *mesh, const char *name) __attribute__((__warn_unused_result__));
+
 /// Set whether new nodes are blacklisted by default.
 /** This function sets the blacklist behaviour for newly discovered nodes.
  *  If set to true, new nodes will be automatically blacklisted.
index cf38e94a74b0ab43546732b224b9f308cd0ef989..da5f0bc4738ed0a224df6881df489ed802bba0c5 100644 (file)
@@ -9,6 +9,7 @@ devtool_trybind_probe
 meshlink_add_address
 meshlink_add_external_address
 meshlink_blacklist
+meshlink_blacklist_by_name
 meshlink_channel_aio_fd_receive
 meshlink_channel_aio_fd_send
 meshlink_channel_aio_receive
@@ -27,6 +28,7 @@ meshlink_enable_discovery
 meshlink_encrypted_key_rotate
 meshlink_errno
 meshlink_export
+meshlink_forget_node
 meshlink_get_all_nodes
 meshlink_get_all_nodes_by_dev_class
 meshlink_get_all_nodes_by_submesh
@@ -79,3 +81,4 @@ meshlink_strerror
 meshlink_submesh_open
 meshlink_verify
 meshlink_whitelist
+meshlink_whitelist_by_name
index c4e6466f48c14a74ff3c339d3bc425ce7c8b2cc0..1a0f871050c8c92dcfab941e2ee870580505ce09 100644 (file)
@@ -1,6 +1,7 @@
 TESTS = \
        basic \
        basicpp \
+       blacklist \
        channels \
        channels-aio \
        channels-aio-fd \
@@ -30,6 +31,7 @@ AM_LDFLAGS = $(PTHREAD_LIBS)
 check_PROGRAMS = \
        basic \
        basicpp \
+       blacklist \
        channels \
        channels-aio \
        channels-aio-fd \
@@ -58,6 +60,9 @@ basic_LDADD = ../src/libmeshlink.la
 basicpp_SOURCES = basicpp.cpp utils.c utils.h
 basicpp_LDADD = ../src/libmeshlink.la
 
+blacklist_SOURCES = blacklist.c utils.c utils.h
+blacklist_LDADD = ../src/libmeshlink.la
+
 channels_SOURCES = channels.c utils.c utils.h
 channels_LDADD = ../src/libmeshlink.la
 
diff --git a/test/blacklist.c b/test/blacklist.c
new file mode 100644 (file)
index 0000000..d504460
--- /dev/null
@@ -0,0 +1,180 @@
+#define _GNU_SOURCE
+
+#ifdef NDEBUG
+#undef NDEBUG
+#endif
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <errno.h>
+#include <assert.h>
+#include <sys/time.h>
+
+#include "meshlink.h"
+#include "devtools.h"
+#include "utils.h"
+
+static struct sync_flag bar_connected;
+static struct sync_flag bar_disconnected;
+static struct sync_flag baz_connected;
+
+static void foo_status_cb(meshlink_handle_t *mesh, meshlink_node_t *node, bool reachable) {
+       (void)mesh;
+       (void)reachable;
+
+       if(!strcmp(node->name, "bar")) {
+               if(reachable) {
+                       set_sync_flag(&bar_connected, true);
+               } else {
+                       set_sync_flag(&bar_disconnected, true);
+               }
+       }
+}
+
+static void baz_status_cb(meshlink_handle_t *mesh, meshlink_node_t *node, bool reachable) {
+       (void)mesh;
+       (void)reachable;
+
+       if(!strcmp(node->name, "bar")) {
+               if(reachable) {
+                       set_sync_flag(&baz_connected, true);
+               }
+       }
+}
+
+int main() {
+       meshlink_set_log_cb(NULL, MESHLINK_DEBUG, log_cb);
+
+       // Create three instances.
+
+       const char *name[3] = {"foo", "bar", "baz"};
+       meshlink_handle_t *mesh[3];
+       char *data[3];
+
+       for(int i = 0; i < 3; i++) {
+               char *path = NULL;
+               assert(asprintf(&path, "blacklist_conf.%d", i) != -1 && path);
+
+               assert(meshlink_destroy(path));
+               mesh[i] = meshlink_open(path, name[i], "trio", DEV_CLASS_BACKBONE);
+               assert(mesh[i]);
+               free(path);
+
+               assert(meshlink_add_address(mesh[i], "localhost"));
+
+               data[i] = meshlink_export(mesh[i]);
+               assert(data[i]);
+
+               // Enable default blacklist on all nodes.
+               meshlink_set_default_blacklist(mesh[i], true);
+       }
+
+       // The first node knows the two other nodes.
+
+       for(int i = 1; i < 3; i++) {
+               assert(meshlink_import(mesh[i], data[0]));
+               assert(meshlink_import(mesh[0], data[i]));
+
+               assert(meshlink_get_node(mesh[i], name[0]));
+               assert(meshlink_get_node(mesh[0], name[i]));
+
+       }
+
+       for(int i = 0; i < 3; i++) {
+               free(data[i]);
+       }
+
+       // Second and third node should not know each other yet.
+
+       assert(!meshlink_get_node(mesh[1], name[2]));
+       assert(!meshlink_get_node(mesh[2], name[1]));
+
+       // Whitelisting and blacklisting by name should work.
+
+       assert(meshlink_whitelist_by_name(mesh[0], "quux"));
+       assert(meshlink_blacklist_by_name(mesh[0], "xyzzy"));
+
+       // Since these nodes now exist we should be able to forget them.
+
+       assert(meshlink_forget_node(mesh[0], meshlink_get_node(mesh[0], "quux")));
+
+       // Start the nodes.
+
+       meshlink_set_node_status_cb(mesh[0], foo_status_cb);
+       meshlink_set_node_status_cb(mesh[2], baz_status_cb);
+
+       for(int i = 0; i < 3; i++) {
+               assert(meshlink_start(mesh[i]));
+       }
+
+       // Wait for them to connect.
+
+       assert(wait_sync_flag(&bar_connected, 5));
+
+       // Blacklist bar
+
+       set_sync_flag(&bar_disconnected, false);
+       assert(meshlink_blacklist(mesh[0], meshlink_get_node(mesh[0], name[1])));
+       assert(wait_sync_flag(&bar_disconnected, 5));
+
+       // Whitelist bar
+
+       set_sync_flag(&bar_connected, false);
+       assert(meshlink_whitelist(mesh[0], meshlink_get_node(mesh[0], name[1])));
+       assert(wait_sync_flag(&bar_connected, 15));
+
+       // Bar should not connect to baz
+
+       assert(wait_sync_flag(&baz_connected, 5) == false);
+
+       // But it should know about baz by now
+
+       meshlink_node_t *bar = meshlink_get_node(mesh[2], "bar");
+       meshlink_node_t *baz = meshlink_get_node(mesh[1], "baz");
+       assert(bar);
+       assert(baz);
+
+       // Have bar and baz whitelist each other
+
+       assert(meshlink_whitelist(mesh[1], baz));
+       assert(meshlink_whitelist(mesh[2], bar));
+
+       // They should connect to each other
+
+       assert(wait_sync_flag(&baz_connected, 15));
+
+       // Trying to forget an active node should fail.
+
+       assert(!meshlink_forget_node(mesh[1], baz));
+
+       // We need to re-acquire the handle to baz
+
+       baz = meshlink_get_node(mesh[1], "baz");
+       assert(baz);
+
+       // Stop the mesh.
+
+       for(int i = 0; i < 3; i++) {
+               meshlink_stop(mesh[i]);
+       }
+
+       // Forgetting a node should work now.
+
+       assert(meshlink_forget_node(mesh[1], baz));
+
+       // Clean up.
+
+       for(int i = 0; i < 3; i++) {
+               meshlink_close(mesh[i]);
+       }
+
+       // Check that foo has a config file for xyzzy but not quux
+       assert(access("blacklist_conf.0/current/hosts/xyzzy", F_OK) == 0);
+       assert(access("blacklist_conf.0/current/hosts/quux", F_OK) != 0 && errno == ENOENT);
+
+       // Check that bar has no config file for baz
+       assert(access("blacklist_conf.2/current/hosts/bar", F_OK) == 0);
+       assert(access("blacklist_conf.1/current/hosts/baz", F_OK) != 0 && errno == ENOENT);
+}