]> git.meshlink.io Git - meshlink/commitdiff
Add meshlink_get_all_nodes_by_last_reachable().
authorGuus Sliepen <guus@meshlink.io>
Sun, 1 Dec 2019 22:56:10 +0000 (23:56 +0100)
committerGuus Sliepen <guus@meshlink.io>
Sun, 1 Dec 2019 23:32:04 +0000 (00:32 +0100)
MeshLink now keeps track of when a node was last reachable. This can be
used by an application to detect nodes that were never reachable or which
have not been reachable for a certain amount of time.

src/graph.c
src/meshlink.c
src/meshlink.h
src/meshlink.sym
src/meshlink_internal.h
src/net_setup.c
src/node.h
test/Makefile.am
test/get-all-nodes.c [new file with mode: 0644]

index d40087d8dc1c65f680c9f55b403f52c3acfc110f..5afa7c32526509a31b610e8153d26c4c41a87829 100644 (file)
@@ -165,12 +165,14 @@ static void check_reachability(meshlink_handle_t *mesh) {
 
                if(n->status.visited != n->status.reachable) {
                        n->status.reachable = !n->status.reachable;
 
                if(n->status.visited != n->status.reachable) {
                        n->status.reachable = !n->status.reachable;
-                       n->last_state_change = mesh->loop.now.tv_sec;
+                       n->status.dirty = true;
 
                        if(n->status.reachable) {
                                logger(mesh, MESHLINK_DEBUG, "Node %s became reachable", n->name);
 
                        if(n->status.reachable) {
                                logger(mesh, MESHLINK_DEBUG, "Node %s became reachable", n->name);
+                               n->last_reachable = mesh->loop.now.tv_sec;
                        } else {
                                logger(mesh, MESHLINK_DEBUG, "Node %s became unreachable", n->name);
                        } else {
                                logger(mesh, MESHLINK_DEBUG, "Node %s became unreachable", n->name);
+                               n->last_unreachable = mesh->loop.now.tv_sec;
                        }
 
                        /* TODO: only clear status.validkey if node is unreachable? */
                        }
 
                        /* TODO: only clear status.validkey if node is unreachable? */
index f30fcdf3ea680d21fd52289161b36aa3e4575be7..88a4c31991a9a340e32edde80523b6686b62b354 100644 (file)
@@ -1501,6 +1501,8 @@ bool meshlink_start(meshlink_handle_t *mesh) {
 
        pthread_cond_wait(&mesh->cond, &mesh->mutex);
        mesh->threadstarted = true;
 
        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;
 
        pthread_mutex_unlock(&mesh->mutex);
        return true;
@@ -1515,6 +1517,11 @@ void meshlink_stop(meshlink_handle_t *mesh) {
        pthread_mutex_lock(&mesh->mutex);
        logger(mesh, MESHLINK_DEBUG, "meshlink_stop called\n");
 
        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);
 
        // Shut down the main thread
        event_loop_stop(&mesh->loop);
 
@@ -2025,6 +2032,31 @@ static bool search_node_by_submesh(const node_t *node, const void *condition) {
        return false;
 }
 
        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;
 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;
@@ -2043,6 +2075,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);
 }
 
        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;
 dev_class_t meshlink_get_node_dev_class(meshlink_handle_t *mesh, meshlink_node_t *node) {
        if(!mesh || !node) {
                meshlink_errno = MESHLINK_EINVAL;
@@ -2714,6 +2757,9 @@ char *meshlink_export(meshlink_handle_t *mesh) {
                packmsg_add_sockaddr(&out, &mesh->self->recent[i]);
        }
 
                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)) {
        pthread_mutex_unlock(&mesh->mutex);
 
        if(!packmsg_output_ok(&out)) {
index 3298e7c492c90dc0aff1878d24ec5ded9c6e6168..98a34bd651d72f9a25a9a6b6aadc4eb9b2b49bc9 100644 (file)
@@ -712,6 +712,30 @@ extern struct meshlink_node **meshlink_get_all_nodes_by_dev_class(struct meshlin
  */
 extern struct meshlink_node **meshlink_get_all_nodes_by_submesh(struct meshlink_handle *mesh, struct meshlink_submesh *submesh, struct meshlink_node **nodes, size_t *nmemb) __attribute__((__warn_unused_result__));
 
  */
 extern struct meshlink_node **meshlink_get_all_nodes_by_submesh(struct meshlink_handle *mesh, struct meshlink_submesh *submesh, struct meshlink_node **nodes, size_t *nmemb) __attribute__((__warn_unused_result__));
 
+/// Get the list of all nodes by time they were last reachable.
+/** This function returns a list with handles for all the nodes whose last known reachability time overlaps with the given time range.
+ *  If the range includes 0, it will count nodes that were never online.
+ *  If start is bigger than end, the result will be inverted.
+ *
+ *  \memberof meshlink_handle
+ *  @param mesh         A handle which represents an instance of MeshLink.
+ *  @param start        Start time.
+ *  @param end          End time.
+ *  @param nodes        A pointer to a previously allocated array of pointers to struct meshlink_node, or NULL in which case MeshLink will allocate a new array.
+ *                      The application can supply an array it allocated itself with malloc, or the return value from the previous call to this function (which is the preferred way).
+ *                      The application is allowed to call free() on the array whenever it wishes.
+ *                      The pointers in the array are valid until meshlink_close() is called.
+ *  @param nmemb        A pointer to a variable holding the number of nodes that were reachable within the period given by @a start and @a end.
+ *                      In case the @a nodes argument is not NULL, MeshLink might call realloc() on the array to change its size.
+ *                      The contents of this variable will be changed to reflect the new size of the array.
+ *
+ *  @return             A pointer to an array containing pointers to all known nodes that were reachable within the period given by @a start and @a end.
+ *                      If the @a nodes argument was not NULL, then the return value can either be the same value or a different value.
+ *                      If it is a new value, the old value of @a nodes should not be used anymore.
+ *                      If the new value is NULL, then the old array will have been freed by MeshLink.
+ */
+extern struct meshlink_node **meshlink_get_all_nodes_by_last_reachable(struct meshlink_handle *mesh, time_t start, time_t end, struct meshlink_node **nodes, size_t *nmemb) __attribute__((__warn_unused_result__));
+
 /// Get the node's device class.
 /** This function returns the device class of the given node.
  *
 /// Get the node's device class.
 /** This function returns the device class of the given node.
  *
index da5f0bc4738ed0a224df6881df489ed802bba0c5..674a260e1b18c16dd852c19f0b88015b4f862175 100644 (file)
@@ -31,6 +31,7 @@ meshlink_export
 meshlink_forget_node
 meshlink_get_all_nodes
 meshlink_get_all_nodes_by_dev_class
 meshlink_forget_node
 meshlink_get_all_nodes
 meshlink_get_all_nodes_by_dev_class
+meshlink_get_all_nodes_by_last_reachable
 meshlink_get_all_nodes_by_submesh
 meshlink_get_external_address
 meshlink_get_external_address_for_family
 meshlink_get_all_nodes_by_submesh
 meshlink_get_external_address
 meshlink_get_external_address_for_family
index 5f87632c6c7742da92e0cc5620c3ac7259bbb9cc..15edba9ab9a1bf1c3452635c888dac1abe8bb9c7 100644 (file)
@@ -42,8 +42,8 @@ static const char meshlink_invitation_label[] = "MeshLink invitation";
 static const char meshlink_tcp_label[] = "MeshLink TCP";
 static const char meshlink_udp_label[] = "MeshLink UDP";
 
 static const char meshlink_tcp_label[] = "MeshLink TCP";
 static const char meshlink_udp_label[] = "MeshLink UDP";
 
-#define MESHLINK_CONFIG_VERSION 1
-#define MESHLINK_INVITATION_VERSION 1
+#define MESHLINK_CONFIG_VERSION 2
+#define MESHLINK_INVITATION_VERSION 2
 
 struct CattaServer;
 struct CattaSServiceBrowser;
 
 struct CattaServer;
 struct CattaSServiceBrowser;
index ec16b400873f15742ac2e998b287797771ad4c87..8c8ef6ed453809d05af4e24fad10627e8b5c5681 100644 (file)
@@ -116,6 +116,15 @@ bool node_read_public_key(meshlink_handle_t *mesh, node_t *n) {
                }
        }
 
                }
        }
 
+       time_t last_reachable = packmsg_get_int64(&in);
+       time_t last_unreachable = packmsg_get_int64(&in);
+
+       if(!n->last_reachable) {
+               n->last_reachable = last_reachable;
+       }
+
+       if(!n->last_unreachable) {
+               n->last_unreachable = last_unreachable;
        }
 
        config_free(&config);
        }
 
        config_free(&config);
@@ -192,6 +201,9 @@ bool node_read_from_config(meshlink_handle_t *mesh, node_t *n, const config_t *c
                }
        }
 
                }
        }
 
+       n->last_reachable = packmsg_get_int64(&in);
+       n->last_unreachable = packmsg_get_int64(&in);
+
        return packmsg_done(&in);
 }
 
        return packmsg_done(&in);
 }
 
@@ -233,6 +245,9 @@ bool node_write_config(meshlink_handle_t *mesh, node_t *n) {
                packmsg_add_sockaddr(&out, &n->recent[i]);
        }
 
                packmsg_add_sockaddr(&out, &n->recent[i]);
        }
 
+       packmsg_add_int64(&out, n->last_reachable);
+       packmsg_add_int64(&out, n->last_unreachable);
+
        if(!packmsg_output_ok(&out)) {
                meshlink_errno = MESHLINK_EINTERNAL;
                return false;
        if(!packmsg_output_ok(&out)) {
                meshlink_errno = MESHLINK_EINTERNAL;
                return false;
@@ -245,6 +260,8 @@ bool node_write_config(meshlink_handle_t *mesh, node_t *n) {
                return false;
        }
 
                return false;
        }
 
+
+
        return true;
 }
 
        return true;
 }
 
@@ -398,7 +415,6 @@ bool setup_myself(meshlink_handle_t *mesh) {
 
        mesh->self->nexthop = mesh->self;
        mesh->self->status.reachable = true;
 
        mesh->self->nexthop = mesh->self;
        mesh->self->status.reachable = true;
-       mesh->self->last_state_change = mesh->loop.now.tv_sec;
 
        node_add(mesh, mesh->self);
 
 
        node_add(mesh, mesh->self);
 
index 1a2fdca0059e489ff102bdf2a4faf764c2d98d01..19e31fac9aaa31ee75363e3fe6df4d18528c82f0 100644 (file)
@@ -76,7 +76,6 @@ typedef struct node_t {
        struct meshlink_handle *mesh;           /* The mesh this node belongs to */
        struct submesh_t *submesh;              /* Nodes Sub-Mesh Handle*/
 
        struct meshlink_handle *mesh;           /* The mesh this node belongs to */
        struct submesh_t *submesh;              /* Nodes Sub-Mesh Handle*/
 
-       time_t last_state_change;
        time_t last_req_key;
 
        struct ecdsa *ecdsa;                    /* His public ECDSA key */
        time_t last_req_key;
 
        struct ecdsa *ecdsa;                    /* His public ECDSA key */
@@ -89,6 +88,9 @@ typedef struct node_t {
        sockaddr_t recent[MAX_RECENT];                   /* Recently seen addresses */
 
        // Graph-related member variables
        sockaddr_t recent[MAX_RECENT];                   /* Recently seen addresses */
 
        // Graph-related member variables
+       time_t last_reachable;
+       time_t last_unreachable;
+
        int distance;
        struct node_t *nexthop;                 /* nearest node from us to him */
        struct edge_t *prevedge;                /* nearest node from him to us */
        int distance;
        struct node_t *nexthop;                 /* nearest node from us to him */
        struct edge_t *prevedge;                /* nearest node from him to us */
index 1a0f871050c8c92dcfab941e2ee870580505ce09..f631ea505c6a9d500233fcabb506b789591e1881 100644 (file)
@@ -13,6 +13,7 @@ TESTS = \
        duplicate \
        encrypted \
        ephemeral \
        duplicate \
        encrypted \
        ephemeral \
+       get-all-nodes \
        import-export \
        invite-join \
        sign-verify \
        import-export \
        invite-join \
        sign-verify \
@@ -44,6 +45,7 @@ check_PROGRAMS = \
        echo-fork \
        encrypted \
        ephemeral \
        echo-fork \
        encrypted \
        ephemeral \
+       get-all-nodes \
        import-export \
        invite-join \
        sign-verify \
        import-export \
        invite-join \
        sign-verify \
@@ -99,6 +101,9 @@ encrypted_LDADD = ../src/libmeshlink.la
 ephemeral_SOURCES = ephemeral.c utils.c utils.h
 ephemeral_LDADD = ../src/libmeshlink.la
 
 ephemeral_SOURCES = ephemeral.c utils.c utils.h
 ephemeral_LDADD = ../src/libmeshlink.la
 
+get_all_nodes_SOURCES = get-all-nodes.c utils.c utils.h
+get_all_nodes_LDADD = ../src/libmeshlink.la
+
 import_export_SOURCES = import-export.c utils.c utils.h
 import_export_LDADD = ../src/libmeshlink.la
 
 import_export_SOURCES = import-export.c utils.c utils.h
 import_export_LDADD = ../src/libmeshlink.la
 
diff --git a/test/get-all-nodes.c b/test/get-all-nodes.c
new file mode 100644 (file)
index 0000000..5dc4af1
--- /dev/null
@@ -0,0 +1,216 @@
+#ifdef NDEBUG
+#undef NDEBUG
+#endif
+
+#include <stdio.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/time.h>
+#include <time.h>
+#include <assert.h>
+
+#include "meshlink.h"
+#include "utils.h"
+
+struct sync_flag bar_reachable;
+
+void status_cb(meshlink_handle_t *mesh, meshlink_node_t *node, bool reachable) {
+       (void)mesh;
+
+       if(reachable && !strcmp(node->name, "bar")) {
+               set_sync_flag(&bar_reachable, true);
+       }
+}
+
+int main() {
+       struct meshlink_node **nodes = NULL;
+       size_t nnodes = 0;
+
+       meshlink_set_log_cb(NULL, MESHLINK_DEBUG, log_cb);
+
+       // Open new meshlink instances.
+
+       assert(meshlink_destroy("get_all_nodes_conf.1"));
+       assert(meshlink_destroy("get_all_nodes_conf.2"));
+       assert(meshlink_destroy("get_all_nodes_conf.3"));
+
+       meshlink_handle_t *mesh[3];
+       mesh[0] = meshlink_open("get_all_nodes_conf.1", "foo", "import-export", DEV_CLASS_BACKBONE);
+       assert(mesh[0]);
+
+       mesh[1] = meshlink_open("get_all_nodes_conf.2", "bar", "import-export", DEV_CLASS_STATIONARY);
+       assert(mesh[1]);
+
+       mesh[2] = meshlink_open("get_all_nodes_conf.3", "baz", "get-all-nodes", DEV_CLASS_STATIONARY);
+       assert(mesh[2]);
+
+       // Check that we only know about ourself.
+
+       nodes = meshlink_get_all_nodes(mesh[0], nodes, &nnodes);
+       assert(nnodes == 1);
+       assert(nodes[0] == meshlink_get_self(mesh[0]));
+
+       // We should never have been online.
+
+       nodes = meshlink_get_all_nodes_by_last_reachable(mesh[0], 0, -1, nodes, &nnodes);
+       assert(nnodes == 0);
+
+       nodes = meshlink_get_all_nodes_by_last_reachable(mesh[0], 0, 0, nodes, &nnodes);
+       assert(nnodes == 1);
+       assert(nodes[0] == meshlink_get_self(mesh[0]));
+
+       // Let nodes know about each other.
+
+       for(int i = 0; i < 3; i++) {
+               meshlink_enable_discovery(mesh[i], false);
+               assert(meshlink_add_address(mesh[i], "localhost"));
+               char *data = meshlink_export(mesh[i]);
+               assert(data);
+
+               for(int j = 0; j < 3; j++) {
+                       if(i == j) {
+                               continue;
+                       }
+
+                       assert(meshlink_import(mesh[j], data));
+               }
+       }
+
+       // We should know about all nodes now, and their device class.
+
+       nodes = meshlink_get_all_nodes(mesh[0], nodes, &nnodes);
+       assert(nnodes == 3);
+
+       nodes = meshlink_get_all_nodes_by_dev_class(mesh[0], DEV_CLASS_BACKBONE, nodes, &nnodes);
+       assert(nnodes == 1);
+       assert(nodes[0] == meshlink_get_self(mesh[0]));
+
+       nodes = meshlink_get_all_nodes_by_dev_class(mesh[0], DEV_CLASS_STATIONARY, nodes, &nnodes);
+       assert(nnodes == 2);
+
+       // But no node should have been online.
+
+       nodes = meshlink_get_all_nodes_by_last_reachable(mesh[0], 0, -1, nodes, &nnodes);
+       assert(nnodes == 0);
+
+       nodes = meshlink_get_all_nodes_by_last_reachable(mesh[0], 0, 0, nodes, &nnodes);
+       assert(nnodes == 3);
+
+       // Start foo.
+
+       time_t foo_started = time(NULL);
+       assert(meshlink_start(mesh[0]));
+
+       nodes = meshlink_get_all_nodes_by_last_reachable(mesh[0], 0, -1, nodes, &nnodes);
+       assert(nnodes == 1);
+       assert(nodes[0] == meshlink_get_self(mesh[0]));
+
+       nodes = meshlink_get_all_nodes_by_last_reachable(mesh[0], 0, 0, nodes, &nnodes);
+       assert(nnodes == 2);
+
+       nodes = meshlink_get_all_nodes_by_last_reachable(mesh[0], foo_started - 1, -1, nodes, &nnodes);
+       assert(nnodes == 1);
+       assert(nodes[0] == meshlink_get_self(mesh[0]));
+
+       nodes = meshlink_get_all_nodes_by_last_reachable(mesh[0], 1, foo_started - 1, nodes, &nnodes);
+       assert(nnodes == 0);
+
+       nodes = meshlink_get_all_nodes_by_last_reachable(mesh[0], 1, foo_started + 1, nodes, &nnodes);
+       assert(nnodes == 1);
+       assert(nodes[0] == meshlink_get_self(mesh[0]));
+
+       // Start bar and wait for it to connect.
+
+       meshlink_set_node_status_cb(mesh[0], status_cb);
+
+       sleep(2);
+       assert(meshlink_start(mesh[1]));
+       assert(wait_sync_flag(&bar_reachable, 20));
+       time_t bar_started = time(NULL);
+
+       // Validate time ranges.
+
+       nodes = meshlink_get_all_nodes_by_last_reachable(mesh[0], 0, -1, nodes, &nnodes);
+       assert(nnodes == 2);
+
+       nodes = meshlink_get_all_nodes_by_last_reachable(mesh[0], 0, 0, nodes, &nnodes);
+       assert(nnodes == 1);
+       assert(nodes[0] == meshlink_get_node(mesh[0], "baz"));
+
+       nodes = meshlink_get_all_nodes_by_last_reachable(mesh[0], 1, foo_started + 1, nodes, &nnodes);
+       assert(nnodes == 1);
+       assert(nodes[0] == meshlink_get_self(mesh[0]));
+
+       nodes = meshlink_get_all_nodes_by_last_reachable(mesh[0], bar_started, bar_started, nodes, &nnodes);
+       assert(nnodes == 2);
+       assert(nodes[0] == meshlink_get_node(mesh[0], "bar"));
+       assert(nodes[1] == meshlink_get_self(mesh[0]));
+
+       // Stop bar.
+
+       meshlink_stop(mesh[1]);
+       sleep(2);
+       time_t bar_stopped = time(NULL);
+
+       // Validate we can see when bar was reachable.
+
+       nodes = meshlink_get_all_nodes_by_last_reachable(mesh[0], bar_stopped, bar_stopped, nodes, &nnodes);
+       assert(nnodes == 1);
+       assert(nodes[0] == meshlink_get_self(mesh[0]));
+
+       nodes = meshlink_get_all_nodes_by_last_reachable(mesh[0], bar_started, bar_started, nodes, &nnodes);
+       assert(nnodes == 2);
+       assert(nodes[0] == meshlink_get_node(mesh[0], "bar"));
+       assert(nodes[1] == meshlink_get_self(mesh[0]));
+
+       // Close and restart foo, check that it remembers correctly.
+
+       meshlink_close(mesh[0]);
+       sleep(2);
+       time_t foo_stopped = time(NULL);
+       mesh[0] = meshlink_open("get_all_nodes_conf.1", "foo", "import-export", DEV_CLASS_BACKBONE);
+       assert(mesh[0]);
+
+       nodes = meshlink_get_all_nodes(mesh[0], nodes, &nnodes);
+       assert(nnodes == 3);
+
+       nodes = meshlink_get_all_nodes_by_dev_class(mesh[0], DEV_CLASS_BACKBONE, nodes, &nnodes);
+       assert(nnodes == 1);
+       assert(nodes[0] == meshlink_get_self(mesh[0]));
+
+       nodes = meshlink_get_all_nodes_by_dev_class(mesh[0], DEV_CLASS_STATIONARY, nodes, &nnodes);
+       assert(nnodes == 2);
+
+       nodes = meshlink_get_all_nodes_by_last_reachable(mesh[0], 0, 0, nodes, &nnodes);
+       assert(nnodes == 1);
+       assert(nodes[0] == meshlink_get_node(mesh[0], "baz"));
+
+       nodes = meshlink_get_all_nodes_by_last_reachable(mesh[0], 0, -1, nodes, &nnodes);
+       assert(nnodes == 2);
+
+       nodes = meshlink_get_all_nodes_by_last_reachable(mesh[0], 1, foo_started - 1, nodes, &nnodes);
+       assert(nnodes == 0);
+
+       nodes = meshlink_get_all_nodes_by_last_reachable(mesh[0], 1, foo_started + 1, nodes, &nnodes);
+       assert(nnodes == 1);
+       assert(nodes[0] == meshlink_get_self(mesh[0]));
+
+       nodes = meshlink_get_all_nodes_by_last_reachable(mesh[0], bar_started, bar_started, nodes, &nnodes);
+       assert(nnodes == 2);
+       assert(nodes[0] == meshlink_get_node(mesh[0], "bar"));
+       assert(nodes[1] == meshlink_get_self(mesh[0]));
+
+       nodes = meshlink_get_all_nodes_by_last_reachable(mesh[0], bar_stopped, bar_stopped, nodes, &nnodes);
+       assert(nnodes == 1);
+       assert(nodes[0] == meshlink_get_self(mesh[0]));
+
+       nodes = meshlink_get_all_nodes_by_last_reachable(mesh[0], foo_stopped, -1, nodes, &nnodes);
+       assert(nnodes == 0);
+
+       // Clean up.
+
+       for(int i = 0; i < 3; i++) {
+               meshlink_close(mesh[i]);
+       }
+}