From d0bf55f32b407fa153dd5d6e41ccefcbb4072611 Mon Sep 17 00:00:00 2001
From: Guus Sliepen <guus@meshlink.io>
Date: Sun, 13 Jun 2021 23:58:54 +0200
Subject: [PATCH] Remove graph, edges and communication via UDP.

---
 src/Makefile.am         |    4 -
 src/connection.h        |    2 -
 src/devtools.c          |  204 --------
 src/devtools.h          |   55 --
 src/discovery.c         | 1063 ---------------------------------------
 src/discovery.h         |   30 --
 src/edge.c              |  115 -----
 src/edge.h              |   50 --
 src/graph.c             |  231 ---------
 src/graph.h             |   25 -
 src/mdns.c              |  441 ----------------
 src/mdns.h              |   14 -
 src/meshlink-tiny++.h   |   21 -
 src/meshlink-tiny.h     |   21 -
 src/meshlink.c          |  160 +-----
 src/meshlink.sym        |    4 -
 src/meshlink_internal.h |   22 -
 src/net.c               |   29 +-
 src/net.h               |    1 -
 src/net_packet.c        |  473 +----------------
 src/net_setup.c         |  111 +---
 src/net_socket.c        |   56 ---
 src/node.c              |   51 --
 src/node.h              |   10 -
 src/protocol.h          |    3 -
 src/protocol_auth.c     |   42 +-
 src/protocol_edge.c     |   17 +-
 src/protocol_key.c      |   41 +-
 test/basic.c            |   26 -
 test/basicpp.cpp        |    6 -
 test/channels-aio-fd.c  |    3 -
 test/channels-fork.c    |    4 -
 test/channels-udp.c     |    2 -
 test/duplicate.c        |    1 -
 test/get-all-nodes.c    |   93 ----
 test/import-export.c    |    5 -
 test/invite-join.c      |    6 -
 test/netns_utils.c      |    2 -
 test/storage-policy.c   |   12 -
 test/trio.c             |    7 -
 test/trio2.c            |    7 -
 test/utils.c            |    6 -
 42 files changed, 47 insertions(+), 3429 deletions(-)
 delete mode 100644 src/discovery.c
 delete mode 100644 src/discovery.h
 delete mode 100644 src/edge.c
 delete mode 100644 src/edge.h
 delete mode 100644 src/graph.c
 delete mode 100644 src/graph.h
 delete mode 100644 src/mdns.c
 delete mode 100644 src/mdns.h

diff --git a/src/Makefile.am b/src/Makefile.am
index 8d62492..51a42e9 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -42,19 +42,15 @@ libmeshlink_tiny_la_SOURCES = \
 	conf.c conf.h \
 	connection.c connection.h \
 	crypto.c crypto.h \
-	discovery.c discovery.h \
 	dropin.c dropin.h \
 	ecdh.h \
 	ecdsa.h \
 	ecdsagen.h \
-	edge.c edge.h \
 	event.c event.h \
-	graph.c graph.h \
 	hash.c hash.h \
 	have.h \
 	list.c list.h \
 	logger.c logger.h \
-	mdns.c mdns.h \
 	meshlink.c meshlink-tiny.h meshlink.sym \
 	meshlink_internal.h \
 	meshlink_queue.h \
diff --git a/src/connection.h b/src/connection.h
index b5ccaed..248f806 100644
--- a/src/connection.h
+++ b/src/connection.h
@@ -44,7 +44,6 @@ typedef struct connection_status_t {
 } connection_status_t;
 
 #include "ecdsa.h"
-#include "edge.h"
 #include "net.h"
 #include "node.h"
 #include "submesh.h"
@@ -71,7 +70,6 @@ typedef struct connection_t {
 
 	struct outgoing_t *outgoing;    /* used to keep track of outgoing connections */
 
-	struct edge_t *edge;            /* edge associated with this connection */
 	struct submesh_t *submesh;      /* his submesh handle if available in invitation file */
 
 	// Only used during authentication
diff --git a/src/devtools.c b/src/devtools.c
index 2a98e59..d14d1d8 100644
--- a/src/devtools.c
+++ b/src/devtools.c
@@ -55,210 +55,6 @@ void (*devtool_set_inviter_commits_first)(bool inviter_commited_first) = inviter
 void (*devtool_adns_resolve_probe)(void) = nop_probe;
 void (*devtool_sptps_renewal_probe)(meshlink_node_t *node) = sptps_renewal_nop_probe;
 
-/* Return an array of edges in the current network graph.
- * Data captures the current state and will not be updated.
- * Caller must deallocate data when done.
- */
-devtool_edge_t *devtool_get_all_edges(meshlink_handle_t *mesh, devtool_edge_t *edges, size_t *nmemb) {
-	if(!mesh || !nmemb || (*nmemb && !edges)) {
-		meshlink_errno = MESHLINK_EINVAL;
-		return NULL;
-	}
-
-	if(pthread_mutex_lock(&mesh->mutex) != 0) {
-		abort();
-	}
-
-	devtool_edge_t *result = NULL;
-	unsigned int result_size = 0;
-
-	result_size = mesh->edges->count / 2;
-
-	// if result is smaller than edges, we have to dealloc all the excess devtool_edge_t
-	if((size_t)result_size > *nmemb) {
-		result = xrealloc(edges, result_size * sizeof(*result));
-	} else {
-		result = edges;
-	}
-
-	if(result) {
-		devtool_edge_t *p = result;
-		unsigned int n = 0;
-
-		for splay_each(edge_t, e, mesh->edges) {
-			// skip edges that do not represent a two-directional connection
-			if(!e->reverse || e->reverse->to != e->from) {
-				continue;
-			}
-
-			// don't count edges twice
-			if(e->to < e->from) {
-				continue;
-			}
-
-			assert(n < result_size);
-
-			p->from = (meshlink_node_t *)e->from;
-			p->to = (meshlink_node_t *)e->to;
-			p->address = e->address.storage;
-			p->weight = e->weight;
-
-			n++;
-			p++;
-		}
-
-		// shrink result to the actual amount of memory used
-		result = xrealloc(result, n * sizeof(*result));
-		*nmemb = n;
-	} else {
-		*nmemb = 0;
-		meshlink_errno = MESHLINK_ENOMEM;
-	}
-
-	pthread_mutex_unlock(&mesh->mutex);
-
-	return result;
-}
-
-static bool fstrwrite(const char *str, FILE *stream) {
-	assert(stream);
-
-	size_t len = strlen(str);
-
-	if(fwrite((void *)str, 1, len, stream) != len) {
-		return false;
-	}
-
-	return true;
-}
-
-bool devtool_export_json_all_edges_state(meshlink_handle_t *mesh, FILE *stream) {
-	assert(stream);
-
-	bool result = true;
-
-	if(pthread_mutex_lock(&mesh->mutex) != 0) {
-		abort();
-	}
-
-	// export edges and nodes
-	size_t node_count = 0;
-	size_t edge_count = 0;
-
-	meshlink_node_t **nodes = meshlink_get_all_nodes(mesh, NULL, &node_count);
-	devtool_edge_t *edges = devtool_get_all_edges(mesh, NULL, &edge_count);
-
-	if((!nodes && node_count != 0) || (!edges && edge_count != 0)) {
-		goto fail;
-	}
-
-	// export begin
-	if(!fstrwrite("{\n", stream)) {
-		goto fail;
-	}
-
-	// export nodes
-	if(!fstrwrite("\t\"nodes\": {\n", stream)) {
-		goto fail;
-	}
-
-	char buf[16];
-
-	for(size_t i = 0; i < node_count; ++i) {
-		if(!fstrwrite("\t\t\"", stream) || !fstrwrite(((node_t *)nodes[i])->name, stream) || !fstrwrite("\": {\n", stream)) {
-			goto fail;
-		}
-
-		if(!fstrwrite("\t\t\t\"name\": \"", stream) || !fstrwrite(((node_t *)nodes[i])->name, stream) || !fstrwrite("\",\n", stream)) {
-			goto fail;
-		}
-
-		snprintf(buf, sizeof(buf), "%d", ((node_t *)nodes[i])->devclass);
-
-		if(!fstrwrite("\t\t\t\"devclass\": ", stream) || !fstrwrite(buf, stream) || !fstrwrite("\n", stream)) {
-			goto fail;
-		}
-
-		if(!fstrwrite((i + 1) != node_count ? "\t\t},\n" : "\t\t}\n", stream)) {
-			goto fail;
-		}
-	}
-
-	if(!fstrwrite("\t},\n", stream)) {
-		goto fail;
-	}
-
-	// export edges
-
-	if(!fstrwrite("\t\"edges\": {\n", stream)) {
-		goto fail;
-	}
-
-	for(size_t i = 0; i < edge_count; ++i) {
-		if(!fstrwrite("\t\t\"", stream) || !fstrwrite(edges[i].from->name, stream) || !fstrwrite("_to_", stream) || !fstrwrite(edges[i].to->name, stream) || !fstrwrite("\": {\n", stream)) {
-			goto fail;
-		}
-
-		if(!fstrwrite("\t\t\t\"from\": \"", stream) || !fstrwrite(edges[i].from->name, stream) || !fstrwrite("\",\n", stream)) {
-			goto fail;
-		}
-
-		if(!fstrwrite("\t\t\t\"to\": \"", stream) || !fstrwrite(edges[i].to->name, stream) || !fstrwrite("\",\n", stream)) {
-			goto fail;
-		}
-
-		char *host = NULL, *port = NULL, *address = NULL;
-		sockaddr2str((const sockaddr_t *)&edges[i].address, &host, &port);
-
-		if(host && port) {
-			xasprintf(&address, "{ \"host\": \"%s\", \"port\": %s }", host, port);
-		}
-
-		free(host);
-		free(port);
-
-		if(!fstrwrite("\t\t\t\"address\": ", stream) || !fstrwrite(address ? address : "null", stream) || !fstrwrite(",\n", stream)) {
-			free(address);
-			goto fail;
-		}
-
-		free(address);
-
-		snprintf(buf, sizeof(buf), "%d", edges[i].weight);
-
-		if(!fstrwrite("\t\t\t\"weight\": ", stream) || !fstrwrite(buf, stream) || !fstrwrite("\n", stream)) {
-			goto fail;
-		}
-
-		if(!fstrwrite((i + 1) != edge_count ? "\t\t},\n" : "\t\t}\n", stream)) {
-			goto fail;
-		}
-	}
-
-	if(!fstrwrite("\t}\n", stream)) {
-		goto fail;
-	}
-
-	// DONE!
-
-	if(!fstrwrite("}", stream)) {
-		goto fail;
-	}
-
-	goto done;
-
-fail:
-	result = false;
-
-done:
-	free(nodes);
-	free(edges);
-
-	pthread_mutex_unlock(&mesh->mutex);
-
-	return result;
-}
-
 void devtool_get_node_status(meshlink_handle_t *mesh, meshlink_node_t *node, devtool_node_status_t *status) {
 	if(!mesh || !node || !status) {
 		meshlink_errno = MESHLINK_EINVAL;
diff --git a/src/devtools.h b/src/devtools.h
index 786e290..c9335cf 100644
--- a/src/devtools.h
+++ b/src/devtools.h
@@ -26,61 +26,6 @@
  *  Applications should not depend on any of these functions for their normal operation.
  */
 
-/// An edge in the MeshLink network.
-typedef struct devtool_edge devtool_edge_t;
-
-/// An edge in the MeshLink network.
-struct devtool_edge {
-	struct meshlink_node *from;     ///< Pointer to a node. Node memory is
-	//   owned by meshlink and should not be
-	//   deallocated. Node contents may be
-	//   changed by meshlink.
-	struct meshlink_node *to;       ///< Pointer to a node. Node memory is
-	//   owned by meshlink and should not be
-	//   deallocated. Node contents may be
-	//   changed by meshlink.
-	struct sockaddr_storage address;///< The address information associated
-	//   with this edge.
-	int weight;                     ///< Weight assigned to this edge.
-};
-
-/// Get a list of edges.
-/** This function returns an array with copies of all known bidirectional edges.
- *  The edges are copied to capture the mesh state at call time, since edges
- *  mutate frequently. The nodes pointed to within the devtool_edge_t type
- *  are not copies; these are the same pointers that one would get from a call
- *  to meshlink_get_all_nodes().
- *
- *  @param mesh         A handle which represents an instance of MeshLink.
- *  @param edges        A pointer to a previously allocated array of
- *                      devtool_edge_t, or NULL in which case MeshLink will
- *                      allocate a new array.
- *                      The application is allowed to call free() on the array whenever it wishes.
- *                      The pointers in the devtool_edge_t elements are valid until
- *                      meshlink_close() is called.
- *  @param nmemb        A pointer to a variable holding the number of elements that
- *                      are stored in the array. In case the @a edges @a
- *                      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 devtool_edge_t elements,
- *                      or NULL in case of an error.
- *                      If the @a edges @a argument was not NULL, then the
- *                      return value can be either the same value or a different
- *                      value. If the new values is NULL, then the old array
- *                      will have been freed by Meshlink.
- */
-devtool_edge_t *devtool_get_all_edges(meshlink_handle_t *mesh, devtool_edge_t *edges, size_t *nmemb);
-
-/// Export a list of edges to a file in JSON format.
-/*  @param mesh         A handle which represents an instance of MeshLink.
- *  @param FILE         An open file descriptor to which a JSON representation of the edges will be written.
- *
- *  @return             True in case of success, false otherwise.
- */
-bool devtool_export_json_all_edges_state(meshlink_handle_t *mesh, FILE *stream);
-
 /// The status of a node.
 typedef struct devtool_node_status devtool_node_status_t;
 
diff --git a/src/discovery.c b/src/discovery.c
deleted file mode 100644
index 032b559..0000000
--- a/src/discovery.c
+++ /dev/null
@@ -1,1063 +0,0 @@
-/*
-  discovery.c -- local network discovery
-  Copyright (C) 2014-2021 Guus Sliepen <guus@meshlink.io>
-
-  This program is free software; you can redistribute it and/or modify
-  it under the terms of the GNU General Public License as published by
-  the Free Software Foundation; either version 2 of the License, or
-  (at your option) any later version.
-
-  This program is distributed in the hope that it will be useful,
-  but WITHOUT ANY WARRANTY; without even the implied warranty of
-  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-  GNU General Public License for more details.
-
-  You should have received a copy of the GNU General Public License along
-  with this program; if not, write to the Free Software Foundation, Inc.,
-  51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
-*/
-
-#define __APPLE_USE_RFC_3542
-#include "system.h"
-
-#if defined(__APPLE__)
-#include <CoreFoundation/CoreFoundation.h>
-#include <CoreFoundation/CFArray.h>
-#include <CoreFoundation/CFString.h>
-#include <SystemConfiguration/SystemConfiguration.h>
-#include <net/if.h>
-#include <netinet/in.h>
-#elif defined(__unix) && !defined(__linux)
-#include <net/if.h>
-#include <net/route.h>
-#include <netinet/in.h>
-#elif defined(__linux)
-#include <asm/types.h>
-#include <net/if.h>
-#include <linux/if_link.h>
-#include <linux/netlink.h>
-#include <linux/rtnetlink.h>
-#endif
-
-#include "mdns.h"
-#include "meshlink_internal.h"
-#include "event.h"
-#include "discovery.h"
-#include "sockaddr.h"
-#include "logger.h"
-#include "netutl.h"
-#include "node.h"
-#include "connection.h"
-#include "utils.h"
-#include "xalloc.h"
-
-#define MESHLINK_MDNS_SERVICE_TYPE "_%s._tcp"
-#define MESHLINK_MDNS_NAME_KEY "name"
-#define MESHLINK_MDNS_FINGERPRINT_KEY "fingerprint"
-
-#ifndef MSG_NOSIGNAL
-#define MSG_NOSIGNAL 0
-#endif
-
-#ifndef IPV6_ADD_MEMBERSHIP
-#define IPV6_ADD_MEMBERSHIP IPV6_JOIN_GROUP
-#endif
-
-#ifndef IPV6_DROP_MEMBERSHIP
-#define IPV6_DROP_MEMBERSHIP IPV6_LEAVE_GROUP
-#endif
-
-static const sockaddr_t mdns_address_ipv4 = {
-	.in.sin_family = AF_INET,
-	.in.sin_addr.s_addr = 0xfb0000e0,
-	.in.sin_port = 0xe914,
-};
-
-static const sockaddr_t mdns_address_ipv6 = {
-	.in6.sin6_family = AF_INET6,
-	.in6.sin6_addr.s6_addr[0x0] = 0xff,
-	.in6.sin6_addr.s6_addr[0x1] = 0x02,
-	.in6.sin6_addr.s6_addr[0xf] = 0xfb,
-	.in6.sin6_port = 0xe914,
-};
-
-typedef struct discovery_address {
-	int index;
-	bool up;
-	sockaddr_t address;
-	time_t last_response_sent;
-} discovery_address_t;
-
-static int iface_compare(const void *va, const void *vb) {
-	const int *a = va;
-	const int *b = vb;
-	return *a - *b;
-}
-
-static int address_compare(const void *va, const void *vb) {
-	const discovery_address_t *a = va;
-	const discovery_address_t *b = vb;
-
-	if(a->index != b->index) {
-		return a->index - b->index;
-	}
-
-	return sockaddrcmp_noport(&a->address, &b->address);
-}
-
-static void send_mdns_packet_ipv4(meshlink_handle_t *mesh, int fd, int index, const sockaddr_t *src, const sockaddr_t *dest, void *data, size_t len) {
-#ifdef IP_MULTICAST_IF
-	struct ip_mreqn mreq = {
-		.imr_address = src->in.sin_addr,
-		.imr_ifindex = index,
-	};
-
-	if(setsockopt(fd, IPPROTO_IP, IP_MULTICAST_IF, &mreq, sizeof(mreq)) != 0) {
-		logger(mesh, MESHLINK_ERROR, "Could not set outgoing multicast interface on IPv4 socket");
-		return;
-	}
-
-#endif
-
-#ifdef IP_PKTINFO
-	struct iovec iov  = {
-		.iov_base = data,
-		.iov_len = len,
-	};
-
-	struct in_pktinfo pkti = {
-		.ipi_ifindex = index,
-		.ipi_addr = src->in.sin_addr,
-	};
-
-	union {
-		char buf[CMSG_SPACE(sizeof(pkti))];
-		struct cmsghdr align;
-	} u;
-
-	memset(&u, 0, sizeof(u));
-
-	struct msghdr msg = {
-		.msg_name = (struct sockaddr *) &dest->sa,
-		.msg_namelen = SALEN(dest->sa),
-		.msg_iov = &iov,
-		.msg_iovlen = 1,
-		.msg_control = u.buf,
-		.msg_controllen = sizeof(u.buf),
-	};
-
-	struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
-	cmsg->cmsg_level = IPPROTO_IP;
-	cmsg->cmsg_type = IP_PKTINFO;
-	cmsg->cmsg_len = CMSG_LEN(sizeof(pkti));
-	memcpy(CMSG_DATA(cmsg), &pkti, sizeof(pkti));
-
-	// Send the packet
-	ssize_t result = sendmsg(fd, &msg, MSG_DONTWAIT | MSG_NOSIGNAL);
-#else
-	(void)index;
-	(void)src;
-
-	// Send the packet
-	ssize_t result = sendto(fd, data, len, MSG_DONTWAIT | MSG_NOSIGNAL, &dest->sa, SALEN(dest->sa));
-#endif
-
-	if(result <= 0) {
-		logger(mesh, MESHLINK_ERROR, "Error sending multicast packet: %s", strerror(errno));
-	}
-}
-
-static void send_mdns_packet_ipv6(meshlink_handle_t *mesh, int fd, int index, const sockaddr_t *src, const sockaddr_t *dest, void *data, size_t len) {
-#ifdef IPV6_MULTICAST_IF
-
-	if(setsockopt(fd, IPPROTO_IPV6, IPV6_MULTICAST_IF, &index, sizeof(index)) != 0) {
-		logger(mesh, MESHLINK_ERROR, "Could not set outgoing multicast interface on IPv6 socket");
-		return;
-	}
-
-#endif
-
-#ifdef IPV6_PKTINFO
-	struct iovec iov  = {
-		.iov_base = data,
-		.iov_len = len,
-	};
-
-	struct in6_pktinfo pkti = {
-		.ipi6_ifindex = index,
-		.ipi6_addr = src->in6.sin6_addr,
-	};
-
-	union {
-		char buf[CMSG_SPACE(sizeof(pkti))];
-		struct cmsghdr align;
-	} u;
-
-	memset(&u, 0, sizeof u);
-
-	struct msghdr msg = {
-		.msg_name = (struct sockaddr *) &dest->sa,
-		.msg_namelen = SALEN(dest->sa),
-		.msg_iov = &iov,
-		.msg_iovlen = 1,
-		.msg_control = u.buf,
-		.msg_controllen = CMSG_LEN(sizeof(pkti)),
-	};
-
-	struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
-	cmsg->cmsg_level = IPPROTO_IPV6;
-	cmsg->cmsg_type = IPV6_PKTINFO;
-	cmsg->cmsg_len = CMSG_LEN(sizeof(pkti));
-	memcpy(CMSG_DATA(cmsg), &pkti, sizeof(pkti));
-
-	// Send the packet
-	ssize_t result = sendmsg(fd, &msg, MSG_DONTWAIT | MSG_NOSIGNAL);
-#else
-	(void)index;
-	(void)src;
-
-	// Send the packet
-	ssize_t result = sendto(fd, data, len, MSG_DONTWAIT | MSG_NOSIGNAL, &dest->sa, SALEN(dest->sa));
-#endif
-
-	if(result <= 0) {
-		logger(mesh, MESHLINK_ERROR, "Error sending multicast packet: %s", strerror(errno));
-	}
-}
-
-static void send_mdns_packet(meshlink_handle_t *mesh, discovery_address_t *addr, bool response) {
-	// Configure the socket to send the packet to the right interface
-	uint8_t msg[1024];
-	size_t msg_size;
-
-	if(response) {
-		char *fingerprint = meshlink_get_fingerprint(mesh, (meshlink_node_t *)mesh->self);
-		static const char *keys[] = {MESHLINK_MDNS_NAME_KEY, MESHLINK_MDNS_FINGERPRINT_KEY};
-		const char *values[] = {mesh->name, fingerprint};
-		msg_size = prepare_response(msg, sizeof msg, fingerprint, mesh->appname, "tcp", atoi(mesh->myport), 2, keys, values);
-		free(fingerprint);
-		addr->last_response_sent = mesh->loop.now.tv_sec;
-	} else {
-		msg_size = prepare_request(msg, sizeof msg, mesh->appname, "tcp");
-	}
-
-	switch(addr->address.sa.sa_family) {
-	case AF_INET:
-		send_mdns_packet_ipv4(mesh, mesh->discovery.sockets[0].fd, addr->index, &addr->address, &mdns_address_ipv4, msg, msg_size);
-		break;
-
-	case AF_INET6:
-		send_mdns_packet_ipv6(mesh, mesh->discovery.sockets[1].fd, addr->index, &addr->address, &mdns_address_ipv6, msg, msg_size);
-		break;
-
-	default:
-		break;
-	}
-}
-
-static void mdns_io_handler(event_loop_t *loop, void *data, int flags) {
-	(void)flags;
-	meshlink_handle_t *mesh = loop->data;
-	io_t *io = data;
-	uint8_t buf[1024];
-	sockaddr_t sa;
-	socklen_t sl = sizeof(sa);
-
-#ifdef IP_PKTINFO
-	struct iovec iov  = {
-		.iov_base = buf,
-		.iov_len = sizeof(buf),
-	};
-
-	union {
-		char buf[1024];
-		struct cmsghdr align;
-	} u;
-
-	struct msghdr msg = {
-		.msg_name = &sa,
-		.msg_namelen = sl,
-		.msg_iov = &iov,
-		.msg_iovlen = 1,
-		.msg_control = u.buf,
-		.msg_controllen = sizeof(u.buf),
-	};
-
-	ssize_t len = recvmsg(io->fd, &msg, MSG_DONTWAIT | MSG_NOSIGNAL);
-
-	sl = msg.msg_namelen;
-#else
-	ssize_t len = recvfrom(io->fd, buf, sizeof(buf), MSG_DONTWAIT, &sa.sa, &sl);
-#endif
-
-	if(len == -1) {
-		if(!sockwouldblock(errno)) {
-			logger(mesh, MESHLINK_ERROR, "Error reading from mDNS discovery socket: %s", strerror(errno));
-			io_set(loop, io, 0);
-		}
-
-		return;
-	}
-
-	char *name = NULL;
-	uint16_t port = 0;
-	const char *keys[2] = {MESHLINK_MDNS_NAME_KEY, MESHLINK_MDNS_FINGERPRINT_KEY};
-	char *values[2] = {NULL, NULL};
-
-	if(parse_response(buf, len, &name, mesh->appname, "tcp", &port, 2, keys, values)) {
-		node_t *n = (node_t *)meshlink_get_node(mesh, values[0]);
-
-		if(n) {
-			if(n != mesh->self) {
-				logger(mesh, MESHLINK_INFO, "Node %s discovered on the local network.\n", n->name);
-			}
-
-			switch(sa.sa.sa_family) {
-			case AF_INET:
-				sa.in.sin_port = htons(port);
-				break;
-
-			case AF_INET6:
-				sa.in6.sin6_port = htons(port);
-				break;
-
-			default:
-				logger(mesh, MESHLINK_WARNING, "Could not resolve node %s to a known address family type.\n", n->name);
-				sa.sa.sa_family = AF_UNKNOWN;
-				break;
-			}
-
-			if(sa.sa.sa_family != AF_UNKNOWN) {
-				n->catta_address = sa;
-				n->last_connect_try = 0;
-				node_add_recent_address(mesh, n, &sa);
-
-				if(n->connection) {
-					n->connection->last_ping_time = -3600;
-				}
-
-				for list_each(outgoing_t, outgoing, mesh->outgoings) {
-					if(outgoing->node != n) {
-						continue;
-					}
-
-					outgoing->timeout = 0;
-
-					if(outgoing->ev.cb) {
-						timeout_set(&mesh->loop, &outgoing->ev, &(struct timespec) {
-							0, 0
-						});
-					}
-				}
-			}
-		}
-	} else if(parse_request(buf, len, mesh->appname, "tcp")) {
-		char *fingerprint = meshlink_get_fingerprint(mesh, (meshlink_node_t *)mesh->self);
-		const char *response_values[] = {mesh->name, fingerprint};
-		size_t size = prepare_response(buf, sizeof(buf), fingerprint, mesh->appname, "tcp", atoi(mesh->myport), 2, keys, response_values);
-
-		int index = -1;
-		int family = AF_UNSPEC;
-
-		for(struct cmsghdr *cmptr = CMSG_FIRSTHDR(&msg); cmptr != NULL; cmptr = CMSG_NXTHDR(&msg, cmptr)) {
-#ifdef IP_PKTINFO
-
-			if(cmptr->cmsg_level == IPPROTO_IP && cmptr->cmsg_type == IP_PKTINFO) {
-				struct in_pktinfo *pktinfo = (struct in_pktinfo *) CMSG_DATA(cmptr);
-				index = pktinfo->ipi_ifindex;
-				family = AF_INET;
-				break;
-			} else
-#endif
-#ifdef IPV6_PKTINFO
-				if(cmptr->cmsg_level == IPPROTO_IPV6 && cmptr->cmsg_type == IPV6_PKTINFO) {
-					struct in6_pktinfo *pktinfo = (struct in6_pktinfo *) CMSG_DATA(cmptr);
-					index = pktinfo->ipi6_ifindex;
-					family = AF_INET6;
-					break;
-				}
-
-#endif
-		}
-
-		if(index) {
-			// Send a multicast response back using all addresses matching the interface
-			for(int i = 0; i < mesh->discovery.address_count; i++) {
-				discovery_address_t *p = &mesh->discovery.addresses[i];
-
-				if(p->index == index && p->address.sa.sa_family == family) {
-					send_mdns_packet(mesh, p, true);
-				}
-			}
-		} else {
-			// Send a unicast response back
-			sendto(io->fd, buf, size, MSG_DONTWAIT | MSG_NOSIGNAL, &sa.sa, sl);
-		}
-
-		free(fingerprint);
-	}
-
-	free(name);
-
-	for(int i = 0; i < 2; i++) {
-		free(values[i]);
-	}
-}
-
-static void iface_up(meshlink_handle_t *mesh, int index) {
-	int *p = bsearch(&index, mesh->discovery.ifaces, mesh->discovery.iface_count, sizeof(*p), iface_compare);
-
-	if(p) {
-		return;
-	}
-
-	mesh->discovery.ifaces = xrealloc(mesh->discovery.ifaces, ++mesh->discovery.iface_count * sizeof(*p));
-	mesh->discovery.ifaces[mesh->discovery.iface_count - 1] = index;
-	qsort(mesh->discovery.ifaces, mesh->discovery.iface_count, sizeof(*p), iface_compare);
-
-	// Add multicast membership
-	struct ip_mreqn mreq4 = {
-		.imr_multiaddr = mdns_address_ipv4.in.sin_addr,
-		.imr_ifindex = index,
-	};
-
-	setsockopt(mesh->discovery.sockets[0].fd, IPPROTO_IP, IP_DROP_MEMBERSHIP, &mreq4, sizeof(mreq4));
-	setsockopt(mesh->discovery.sockets[0].fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq4, sizeof(mreq4));
-
-	struct ipv6_mreq mreq6 = {
-		.ipv6mr_multiaddr = mdns_address_ipv6.in6.sin6_addr,
-		.ipv6mr_interface = index,
-	};
-
-	setsockopt(mesh->discovery.sockets[1].fd, IPPROTO_IPV6, IPV6_DROP_MEMBERSHIP, &mreq6, sizeof(mreq6));
-	setsockopt(mesh->discovery.sockets[1].fd, IPPROTO_IPV6, IPV6_ADD_MEMBERSHIP, &mreq6, sizeof(mreq6));
-
-	// Send an announcement for all addresses associated with this interface
-	for(int i = 0; i < mesh->discovery.address_count; i++) {
-		if(mesh->discovery.addresses[i].index == index) {
-			send_mdns_packet(mesh, &mesh->discovery.addresses[i], false);
-		}
-	}
-
-	handle_network_change(mesh, true);
-}
-
-static void iface_down(meshlink_handle_t *mesh, int index) {
-	int *p = bsearch(&index, mesh->discovery.ifaces, mesh->discovery.iface_count, sizeof(*p), iface_compare);
-
-	if(!p) {
-		return;
-	}
-
-	// Drop multicast membership
-	struct ip_mreqn mreq4 = {
-		.imr_multiaddr = mdns_address_ipv4.in.sin_addr,
-		.imr_ifindex = index,
-	};
-	setsockopt(mesh->discovery.sockets[0].fd, IPPROTO_IP, IP_DROP_MEMBERSHIP, &mreq4, sizeof(mreq4));
-
-	struct ipv6_mreq mreq6 = {
-		.ipv6mr_multiaddr = mdns_address_ipv6.in6.sin6_addr,
-		.ipv6mr_interface = index,
-	};
-	setsockopt(mesh->discovery.sockets[1].fd, IPPROTO_IPV6, IPV6_DROP_MEMBERSHIP, &mreq6, sizeof(mreq6));
-
-	memmove(p, p + 1, (mesh->discovery.ifaces + --mesh->discovery.iface_count - p) * sizeof(*p));
-
-	handle_network_change(mesh, mesh->discovery.iface_count);
-}
-
-static void addr_add(meshlink_handle_t *mesh, const discovery_address_t *addr) {
-	discovery_address_t *p = bsearch(addr, mesh->discovery.addresses, mesh->discovery.address_count, sizeof(*p), address_compare);
-
-	if(p) {
-		return;
-	}
-
-	bool up = bsearch(&addr->index, mesh->discovery.ifaces, mesh->discovery.iface_count, sizeof(int), iface_compare);
-
-	mesh->discovery.addresses = xrealloc(mesh->discovery.addresses, ++mesh->discovery.address_count * sizeof(*p));
-	mesh->discovery.addresses[mesh->discovery.address_count - 1] = *addr;
-	mesh->discovery.addresses[mesh->discovery.address_count - 1].up = up;
-
-	if(up) {
-		send_mdns_packet(mesh, &mesh->discovery.addresses[mesh->discovery.address_count - 1], false);
-	}
-
-	qsort(mesh->discovery.addresses, mesh->discovery.address_count, sizeof(*p), address_compare);
-}
-
-static void addr_del(meshlink_handle_t *mesh, const discovery_address_t *addr) {
-	discovery_address_t *p = bsearch(addr, mesh->discovery.addresses, mesh->discovery.address_count, sizeof(*p), address_compare);
-
-	if(!p) {
-		return;
-	}
-
-	memmove(p, p + 1, (mesh->discovery.addresses + --mesh->discovery.address_count - p) * sizeof(*p));
-}
-
-void scan_ifaddrs(meshlink_handle_t *mesh) {
-#ifdef HAVE_GETIFADDRS
-	struct ifaddrs *ifa = NULL;
-
-	if(getifaddrs(&ifa) == -1) {
-		logger(mesh, MESHLINK_ERROR, "Could not get list of interface addresses: %s", strerror(errno));
-		return;
-	}
-
-	// Check for interfaces being removed
-	for(int i = 0; i < mesh->discovery.iface_count;) {
-		bool found = false;
-
-		for(struct ifaddrs *ifap = ifa; ifap; ifap = ifap->ifa_next) {
-			if(!ifap->ifa_name) {
-				continue;
-			}
-
-			int index = if_nametoindex(ifap->ifa_name);
-
-			if(mesh->discovery.ifaces[i] == index) {
-				found = true;
-				break;
-			}
-		}
-
-		if(!found) {
-			iface_down(mesh, mesh->discovery.ifaces[i]);
-		} else {
-			i++;
-		}
-	}
-
-	// Check for addresses being removed
-	for(int i = 0; i < mesh->discovery.address_count;) {
-		discovery_address_t *p = &mesh->discovery.addresses[i];
-		bool found = false;
-
-		for(struct ifaddrs *ifap = ifa; ifap; ifap = ifap->ifa_next) {
-			if(!ifap->ifa_name || !ifap->ifa_addr) {
-				continue;
-			}
-
-			int index = if_nametoindex(ifap->ifa_name);
-
-			if(p->index == index && sockaddrcmp_noport(&p->address, (sockaddr_t *)ifap->ifa_addr) == 0) {
-				found = true;
-				break;
-			}
-		}
-
-		if(!found) {
-			(void)addr_del;
-			memmove(p, p + 1, (mesh->discovery.addresses + --mesh->discovery.address_count - p) * sizeof(*p));
-		} else {
-			i++;
-		}
-	}
-
-	// Check for interfaces state changes and addresses going up
-	for(struct ifaddrs *ifap = ifa; ifap; ifap = ifap->ifa_next) {
-		if(!ifap->ifa_name) {
-			continue;
-		}
-
-		int index = if_nametoindex(ifap->ifa_name);
-
-		if(ifap->ifa_flags & IFF_UP && ifap->ifa_flags & IFF_MULTICAST && !(ifap->ifa_flags & IFF_LOOPBACK)) {
-			iface_up(mesh, index);
-		} else {
-			iface_down(mesh, index);
-		}
-
-		if(!ifap->ifa_addr) {
-			continue;
-		}
-
-		discovery_address_t addr  = {
-			.index = index,
-		};
-
-		sockaddr_t *sa = (sockaddr_t *)ifap->ifa_addr;
-
-		if(sa->sa.sa_family == AF_INET) {
-			memcpy(&addr.address.in, &sa->in, sizeof(sa->in));
-			addr.address.in.sin_port = ntohs(5353);
-		} else if(sa->sa.sa_family == AF_INET6) {
-			memcpy(&addr.address.in6, &sa->in6, sizeof(sa->in6));
-			addr.address.in6.sin6_port = ntohs(5353);
-		} else {
-			addr.address.sa.sa_family = AF_UNKNOWN;
-		}
-
-		if(addr.address.sa.sa_family != AF_UNKNOWN) {
-			addr_add(mesh, &addr);
-		}
-	}
-
-	freeifaddrs(ifa);
-#else
-	(void)mesh;
-#endif
-}
-
-#if defined(__linux)
-static void netlink_getlink(int fd) {
-	static const struct {
-		struct nlmsghdr nlm;
-		struct ifinfomsg ifi;
-	} msg = {
-		.nlm.nlmsg_len = NLMSG_LENGTH(sizeof(msg.ifi)),
-		.nlm.nlmsg_type = RTM_GETLINK,
-		.nlm.nlmsg_flags = NLM_F_DUMP | NLM_F_REQUEST,
-		.nlm.nlmsg_seq = 1,
-		.ifi.ifi_family = AF_UNSPEC,
-	};
-	send(fd, &msg, msg.nlm.nlmsg_len, 0);
-}
-
-static void netlink_getaddr(int fd) {
-	static const struct {
-		struct nlmsghdr nlm;
-		struct ifaddrmsg ifa;
-	} msg = {
-		.nlm.nlmsg_len = NLMSG_LENGTH(sizeof(msg.ifa)),
-		.nlm.nlmsg_type = RTM_GETADDR,
-		.nlm.nlmsg_flags = NLM_F_DUMP | NLM_F_REQUEST,
-		.nlm.nlmsg_seq = 2,
-		.ifa.ifa_family = AF_UNSPEC,
-	};
-	send(fd, &msg, msg.nlm.nlmsg_len, 0);
-}
-
-static void netlink_parse_link(meshlink_handle_t *mesh, const struct nlmsghdr *nlm) {
-	const struct ifinfomsg *ifi = (const struct ifinfomsg *)(nlm + 1);
-
-	if(ifi->ifi_flags & IFF_UP && ifi->ifi_flags & IFF_MULTICAST) {
-		iface_up(mesh, ifi->ifi_index);
-	} else {
-		iface_down(mesh, ifi->ifi_index);
-	}
-}
-
-static void netlink_parse_addr(meshlink_handle_t *mesh, const struct nlmsghdr *nlm) {
-	const struct ifaddrmsg *ifa = (const struct ifaddrmsg *)(nlm + 1);
-	const uint8_t *ptr = (const uint8_t *)(ifa + 1);
-	size_t len = nlm->nlmsg_len - (ptr - (const uint8_t *)nlm);
-
-	while(len >= sizeof(struct rtattr)) {
-		const struct rtattr *rta = (const struct rtattr *)ptr;
-
-		if(rta->rta_len <= 0 || rta->rta_len > len) {
-			break;
-		}
-
-		if(rta->rta_type == IFA_ADDRESS) {
-			discovery_address_t addr  = {
-				.index = ifa->ifa_index,
-			};
-
-			if(rta->rta_len == 8) {
-				addr.address.sa.sa_family = AF_INET;
-				memcpy(&addr.address.in.sin_addr, ptr + 4, 4);
-				addr.address.in.sin_port = ntohs(5353);
-			} else if(rta->rta_len == 20) {
-				addr.address.sa.sa_family = AF_INET6;
-				memcpy(&addr.address.in6.sin6_addr, ptr + 4, 16);
-				addr.address.in6.sin6_port = ntohs(5353);
-				addr.address.in6.sin6_scope_id = ifa->ifa_index;
-			} else {
-				addr.address.sa.sa_family = AF_UNKNOWN;
-			}
-
-			if(addr.address.sa.sa_family != AF_UNKNOWN) {
-				if(nlm->nlmsg_type == RTM_NEWADDR) {
-					addr_add(mesh, &addr);
-				} else {
-					addr_del(mesh, &addr);
-				}
-			}
-		}
-
-		unsigned short rta_len = (rta->rta_len + 3) & ~3;
-		ptr += rta_len;
-		len -= rta_len;
-	}
-}
-
-static void netlink_parse(meshlink_handle_t *mesh, const void *data, size_t len) {
-	const uint8_t *ptr = data;
-
-	while(len >= sizeof(struct nlmsghdr)) {
-		const struct nlmsghdr *nlm = (const struct nlmsghdr *)ptr;
-
-		if(nlm->nlmsg_len > len) {
-			break;
-		}
-
-		switch(nlm->nlmsg_type) {
-		case RTM_NEWLINK:
-		case RTM_DELLINK:
-			netlink_parse_link(mesh, nlm);
-			break;
-
-		case RTM_NEWADDR:
-		case RTM_DELADDR:
-			netlink_parse_addr(mesh, nlm);
-		}
-
-		ptr += nlm->nlmsg_len;
-		len -= nlm->nlmsg_len;
-	}
-}
-
-static void netlink_io_handler(event_loop_t *loop, void *data, int flags) {
-	(void)flags;
-	(void)data;
-	meshlink_handle_t *mesh = loop->data;
-
-	struct {
-		struct nlmsghdr nlm;
-		char data[16384];
-	} msg;
-
-	while(true) {
-		ssize_t result = recv(mesh->discovery.pfroute_io.fd, &msg, sizeof(msg), MSG_DONTWAIT);
-
-		if(result <= 0) {
-			if(result == 0 || errno == EAGAIN || errno == EINTR) {
-				break;
-			}
-
-			logger(mesh, MESHLINK_ERROR, "Reading from Netlink socket failed: %s\n", strerror(errno));
-			io_set(loop, &mesh->discovery.pfroute_io, 0);
-		}
-
-		if((size_t)result < sizeof(msg.nlm)) {
-			logger(mesh, MESHLINK_ERROR, "Invalid Netlink message\n");
-			break;
-		}
-
-		if(msg.nlm.nlmsg_type == NLMSG_DONE) {
-			if(msg.nlm.nlmsg_seq == 1) {
-				// We just got the result of GETLINK, now send GETADDR.
-				netlink_getaddr(mesh->discovery.pfroute_io.fd);
-			}
-		} else {
-			netlink_parse(mesh, &msg, result);
-
-			if(loop->now.tv_sec > mesh->discovery.last_update + 5) {
-				mesh->discovery.last_update = loop->now.tv_sec;
-				handle_network_change(mesh, true);
-			}
-		}
-	}
-}
-#elif defined(__APPLE__)
-static void reachability_change_callback(SCNetworkReachabilityRef reachability, SCNetworkReachabilityFlags flags, void *info) {
-	(void)reachability;
-	(void)flags;
-
-	meshlink_handle_t *mesh = info;
-
-	pthread_mutex_lock(&mesh->mutex);
-
-	scan_ifaddrs(mesh);
-
-	if(mesh->loop.now.tv_sec > mesh->discovery.last_update + 5) {
-		logger(mesh, MESHLINK_INFO, "Network change detected");
-		mesh->discovery.last_update = mesh->loop.now.tv_sec;
-		handle_network_change(mesh, true);
-	}
-
-	pthread_mutex_unlock(&mesh->mutex);
-}
-
-static void *network_change_handler(void *arg) {
-	meshlink_handle_t *mesh = arg;
-
-	mesh->discovery.runloop = CFRunLoopGetCurrent();
-
-	SCNetworkReachabilityRef reach_v4 = SCNetworkReachabilityCreateWithName(NULL, "93.184.216.34");
-	SCNetworkReachabilityRef reach_v6 = SCNetworkReachabilityCreateWithName(NULL, "2606:2800:220:1:248:1893:25c8:1946");
-
-	SCNetworkReachabilityContext context;
-	memset(&context, 0, sizeof(context));
-	context.info = mesh;
-
-	if(reach_v4) {
-		SCNetworkReachabilitySetCallback(reach_v4, reachability_change_callback, &context);
-		SCNetworkReachabilityScheduleWithRunLoop(reach_v4, mesh->discovery.runloop, kCFRunLoopDefaultMode);
-	} else {
-		logger(mesh, MESHLINK_ERROR, "Could not create IPv4 network reachability watcher");
-	}
-
-	if(reach_v6) {
-		SCNetworkReachabilitySetCallback(reach_v6, reachability_change_callback, &context);
-		SCNetworkReachabilityScheduleWithRunLoop(reach_v6, mesh->discovery.runloop, kCFRunLoopDefaultMode);
-	} else {
-		logger(mesh, MESHLINK_ERROR, "Could not create IPv6 network reachability watcher");
-	}
-
-	CFRunLoopRun();
-
-	mesh->discovery.runloop = NULL;
-
-	if(reach_v4) {
-		SCNetworkReachabilityUnscheduleFromRunLoop(reach_v4, mesh->discovery.runloop, kCFRunLoopDefaultMode);
-		CFRelease(reach_v4);
-	}
-
-	if(reach_v6) {
-		SCNetworkReachabilityUnscheduleFromRunLoop(reach_v6, mesh->discovery.runloop, kCFRunLoopDefaultMode);
-		CFRelease(reach_v6);
-	}
-
-	return NULL;
-}
-#elif defined(RTM_NEWADDR)
-static void pfroute_parse_iface(meshlink_handle_t *mesh, const struct rt_msghdr *rtm) {
-	const struct if_msghdr *ifm = (const struct if_msghdr *)rtm;
-
-	if(ifm->ifm_flags & IFF_UP && ifm->ifm_flags & IFF_MULTICAST && !(ifm->ifm_flags & IFF_LOOPBACK)) {
-		iface_up(mesh, ifm->ifm_index);
-	} else {
-		iface_down(mesh, ifm->ifm_index);
-	}
-}
-
-static void pfroute_parse_addr(meshlink_handle_t *mesh, const struct rt_msghdr *rtm) {
-	const struct ifa_msghdr *ifam = (const struct ifa_msghdr *)rtm;
-	const char *p = (const char *)(ifam + 1);
-
-	for(unsigned int i = 1; i; i <<= 1) {
-		if(!(ifam->ifam_addrs & i)) {
-			continue;
-		}
-
-		const sockaddr_t *sa = (const sockaddr_t *)p;
-
-		if(i == RTA_IFA) {
-			discovery_address_t addr = {
-				.index = ifam->ifam_index,
-			};
-
-			if(sa->sa.sa_family == AF_INET) {
-				addr.address.in = sa->in;
-				addr.address.in.sin_port = ntohs(5353);
-			} else if(sa->sa.sa_family == AF_INET6) {
-				addr.address.in6 = sa->in6;
-				addr.address.in6.sin6_port = ntohs(5353);
-			} else {
-				addr.address.sa.sa_family = AF_UNKNOWN;
-			}
-
-			if(addr.address.sa.sa_family != AF_UNKNOWN) {
-				if(ifam->ifam_type == RTM_NEWADDR) {
-					addr_add(mesh, &addr);
-				} else {
-					addr_del(mesh, &addr);
-				}
-			}
-
-			break;
-		}
-
-		size_t len = (sa->sa.sa_len + 3) & ~3;
-		p += len;
-	}
-}
-
-static void pfroute_io_handler(event_loop_t *loop, void *data, int flags) {
-	(void)flags;
-	(void)data;
-	meshlink_handle_t *mesh = loop->data;
-
-	struct {
-		struct rt_msghdr rtm;
-		char data[2048];
-	} msg;
-
-	while(true) {
-		msg.rtm.rtm_version = 0;
-		ssize_t result = recv(mesh->discovery.pfroute_io.fd, &msg, sizeof(msg), MSG_DONTWAIT);
-
-		if(result <= 0) {
-			if(result == 0 || errno == EAGAIN || errno == EINTR) {
-				break;
-			}
-
-			logger(mesh, MESHLINK_ERROR, "Reading from PFROUTE socket failed: %s\n", strerror(errno));
-			io_set(loop, &mesh->discovery.pfroute_io, 0);
-		}
-
-		if(msg.rtm.rtm_version != RTM_VERSION) {
-			logger(mesh, MESHLINK_ERROR, "Invalid PFROUTE message version\n");
-			break;
-		}
-
-		switch(msg.rtm.rtm_type) {
-		case RTM_IFINFO:
-			pfroute_parse_iface(mesh, &msg.rtm);
-			break;
-
-		case RTM_NEWADDR:
-		case RTM_DELADDR:
-			pfroute_parse_addr(mesh, &msg.rtm);
-			break;
-
-		default:
-			break;
-		}
-	}
-}
-#endif
-
-bool discovery_start(meshlink_handle_t *mesh) {
-	logger(mesh, MESHLINK_DEBUG, "discovery_start called\n");
-
-	assert(mesh);
-
-	// Set up multicast sockets for mDNS
-	static const int one = 1;
-	static const int ttl = 255;
-	static const uint8_t one8 = 1;
-	static const uint8_t ttl8 = 255;
-
-	int fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
-
-	if(fd == -1) {
-		logger(mesh, MESHLINK_ERROR, "Error creating IPv4 socket: %s", strerror(errno));
-	} else {
-
-		sockaddr_t sa4 = {
-			.in.sin_family = AF_INET,
-			.in.sin_port = ntohs(5353),
-		};
-		setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
-#ifdef SO_REUSEPORT
-		setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &one, sizeof(one));
-#endif
-#ifdef IP_PKTINFO
-		setsockopt(fd, IPPROTO_IP, IP_PKTINFO, &one, sizeof(one));
-#endif
-		setsockopt(fd, IPPROTO_IP, IP_MULTICAST_LOOP, &one8, sizeof(one8));
-		setsockopt(fd, IPPROTO_IP, IP_MULTICAST_TTL, &ttl8, sizeof(ttl8));
-
-		if(bind(fd, &sa4.sa, SALEN(sa4.sa)) == -1) {
-			logger(mesh, MESHLINK_ERROR, "Error binding to IPv4 multicast socket: %s", strerror(errno));
-		} else {
-			io_add(&mesh->loop, &mesh->discovery.sockets[0], mdns_io_handler, &mesh->discovery.sockets[0], fd, IO_READ);
-		}
-	}
-
-	fd = socket(AF_INET6, SOCK_DGRAM, IPPROTO_UDP);
-
-	if(fd == -1) {
-		logger(mesh, MESHLINK_ERROR, "Error creating IPv6 socket: %s", strerror(errno));
-	} else {
-		sockaddr_t sa6 = {
-			.in6.sin6_family = AF_INET6,
-			.in6.sin6_port = ntohs(5353),
-		};
-
-		setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
-#ifdef SO_REUSEPORT
-		setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &one, sizeof(one));
-#endif
-#ifdef IPV6_RECVPKTINFO
-		setsockopt(fd, IPPROTO_IPV6, IPV6_RECVPKTINFO, &one, sizeof(one));
-#endif
-		setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &one, sizeof(one));
-		setsockopt(fd, IPPROTO_IPV6, IPV6_MULTICAST_LOOP, &one, sizeof(one));
-		setsockopt(fd, IPPROTO_IPV6, IPV6_MULTICAST_HOPS, &ttl, sizeof(ttl));
-		setsockopt(fd, IPPROTO_IPV6, IPV6_UNICAST_HOPS, &ttl, sizeof(ttl));
-
-		if(bind(fd, &sa6.sa, SALEN(sa6.sa)) == -1) {
-			logger(mesh, MESHLINK_ERROR, "Error binding to IPv6 multicast socket: %s", strerror(errno));
-		} else {
-			io_add(&mesh->loop, &mesh->discovery.sockets[1], mdns_io_handler, &mesh->discovery.sockets[1], fd, IO_READ);
-		}
-	}
-
-#if defined(__linux)
-	int sock = socket(AF_NETLINK, SOCK_DGRAM, NETLINK_ROUTE);
-
-	if(sock != -1) {
-		struct sockaddr_nl sa;
-		memset(&sa, 0, sizeof(sa));
-		sa.nl_family = AF_NETLINK;
-		sa.nl_groups = RTMGRP_LINK | RTMGRP_IPV4_IFADDR | RTMGRP_IPV6_IFADDR;
-
-		if(bind(sock, (struct sockaddr *)&sa, sizeof(sa)) != -1) {
-			io_add(&mesh->loop, &mesh->discovery.pfroute_io, netlink_io_handler, NULL, sock, IO_READ);
-			netlink_getlink(sock);
-		} else {
-			logger(mesh, MESHLINK_WARNING, "Could not bind AF_NETLINK socket: %s", strerror(errno));
-			scan_ifaddrs(mesh);
-		}
-	} else {
-		logger(mesh, MESHLINK_WARNING, "Could not open AF_NETLINK socket: %s", strerror(errno));
-		scan_ifaddrs(mesh);
-	}
-
-#elif defined(__APPLE__)
-	pthread_create(&mesh->discovery.thread, NULL, network_change_handler, mesh);
-	// TODO: Do we need to wait for the thread to start succesfully?
-	scan_ifaddrs(mesh);
-#elif defined(RTM_NEWADDR)
-	int sock = socket(PF_ROUTE, SOCK_RAW, AF_UNSPEC);
-
-	if(sock != -1) {
-		io_add(&mesh->loop, &mesh->discovery.pfroute_io, pfroute_io_handler, NULL, sock, IO_READ);
-	} else {
-		logger(mesh, MESHLINK_WARNING, "Could not open PF_ROUTE socket: %s", strerror(errno));
-	}
-
-	scan_ifaddrs(mesh);
-#endif
-
-	return true;
-}
-
-void discovery_stop(meshlink_handle_t *mesh) {
-	logger(mesh, MESHLINK_DEBUG, "discovery_stop called\n");
-
-	assert(mesh);
-
-	free(mesh->discovery.ifaces);
-	free(mesh->discovery.addresses);
-	mesh->discovery.ifaces = NULL;
-	mesh->discovery.addresses = NULL;
-	mesh->discovery.iface_count = 0;
-	mesh->discovery.address_count = 0;
-
-#if defined(__APPLE__)
-
-	if(mesh->discovery.runloop) {
-		CFRunLoopStop(mesh->discovery.runloop);
-		pthread_join(mesh->discovery.thread, NULL);
-	}
-
-#endif
-
-	if(mesh->discovery.pfroute_io.cb) {
-		close(mesh->discovery.pfroute_io.fd);
-		io_del(&mesh->loop, &mesh->discovery.pfroute_io);
-	}
-
-	for(int i = 0; i < 2; i++) {
-		if(mesh->discovery.sockets[i].cb) {
-			close(mesh->discovery.sockets[i].fd);
-			io_del(&mesh->loop, &mesh->discovery.sockets[i]);
-		}
-	}
-}
-
-void discovery_refresh(meshlink_handle_t *mesh) {
-	for(int i = 0; i < mesh->discovery.address_count; i++) {
-		if(mesh->discovery.addresses[i].up) {
-			send_mdns_packet(mesh, &mesh->discovery.addresses[i], false);
-		}
-	}
-}
diff --git a/src/discovery.h b/src/discovery.h
deleted file mode 100644
index 03c4312..0000000
--- a/src/discovery.h
+++ /dev/null
@@ -1,30 +0,0 @@
-#ifndef MESHLINK_DISCOVERY_H
-#define MESHLINK_DISCOVERY_H
-
-/*
-    discovery.h -- header for discovery.c
-    Copyright (C) 2014-2021 Guus Sliepen <guus@meshlink.io>
-
-    This program is free software; you can redistribute it and/or modify
-    it under the terms of the GNU General Public License as published by
-    the Free Software Foundation; either version 2 of the License, or
-    (at your option) any later version.
-
-    This program is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU General Public License for more details.
-
-    You should have received a copy of the GNU General Public License along
-    with this program; if not, write to the Free Software Foundation, Inc.,
-    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
-*/
-
-#include <stdbool.h>
-
-bool discovery_start(meshlink_handle_t *mesh);
-void discovery_stop(meshlink_handle_t *mesh);
-void discovery_refresh(meshlink_handle_t *mesh);
-void scan_ifaddrs(meshlink_handle_t *mesh);
-
-#endif
diff --git a/src/edge.c b/src/edge.c
deleted file mode 100644
index afe9cfe..0000000
--- a/src/edge.c
+++ /dev/null
@@ -1,115 +0,0 @@
-/*
-    edge.c -- edge tree management
-    Copyright (C) 2014 Guus Sliepen <guus@meshlink.io>
-
-    This program is free software; you can redistribute it and/or modify
-    it under the terms of the GNU General Public License as published by
-    the Free Software Foundation; either version 2 of the License, or
-    (at your option) any later version.
-
-    This program is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU General Public License for more details.
-
-    You should have received a copy of the GNU General Public License along
-    with this program; if not, write to the Free Software Foundation, Inc.,
-    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
-*/
-
-#include "system.h"
-
-#include "splay_tree.h"
-#include "edge.h"
-#include "logger.h"
-#include "meshlink_internal.h"
-#include "netutl.h"
-#include "node.h"
-#include "utils.h"
-#include "xalloc.h"
-
-static int edge_compare(const edge_t *a, const edge_t *b) {
-	return strcmp(a->to->name, b->to->name);
-}
-
-static int edge_weight_compare(const edge_t *a, const edge_t *b) {
-	int result;
-
-	result = a->weight - b->weight;
-
-	if(result) {
-		return result;
-	}
-
-	result = strcmp(a->from->name, b->from->name);
-
-	if(result) {
-		return result;
-	}
-
-	return strcmp(a->to->name, b->to->name);
-}
-
-void init_edges(meshlink_handle_t *mesh) {
-	mesh->edges = splay_alloc_tree((splay_compare_t) edge_weight_compare, NULL);
-}
-
-splay_tree_t *new_edge_tree(void) {
-	return splay_alloc_tree((splay_compare_t) edge_compare, (splay_action_t) free_edge);
-}
-
-void free_edge_tree(splay_tree_t *edge_tree) {
-	splay_delete_tree(edge_tree);
-}
-
-void exit_edges(meshlink_handle_t *mesh) {
-	if(mesh->edges) {
-		splay_delete_tree(mesh->edges);
-	}
-
-	mesh->edges = NULL;
-}
-
-/* Creation and deletion of connection elements */
-
-edge_t *new_edge(void) {
-	return xzalloc(sizeof(edge_t));
-}
-
-void free_edge(edge_t *e) {
-	sockaddrfree(&e->address);
-
-	free(e);
-}
-
-void edge_add(meshlink_handle_t *mesh, edge_t *e) {
-	splay_insert(mesh->edges, e);
-	splay_insert(e->from->edge_tree, e);
-
-	e->reverse = lookup_edge(e->to, e->from);
-
-	if(e->reverse) {
-		e->reverse->reverse = e;
-	}
-}
-
-void edge_del(meshlink_handle_t *mesh, edge_t *e) {
-	if(e->reverse) {
-		e->reverse->reverse = NULL;
-	}
-
-	splay_delete(mesh->edges, e);
-	splay_delete(e->from->edge_tree, e);
-}
-
-edge_t *lookup_edge(node_t *from, node_t *to) {
-	assert(from);
-	assert(to);
-
-	edge_t v;
-
-	v.from = from;
-	v.to = to;
-
-	return splay_search(from->edge_tree, &v);
-}
diff --git a/src/edge.h b/src/edge.h
deleted file mode 100644
index d5ec117..0000000
--- a/src/edge.h
+++ /dev/null
@@ -1,50 +0,0 @@
-#ifndef MESHLINK_EDGE_H
-#define MESHLINK_EDGE_H
-
-/*
-    edge.h -- header for edge.c
-    Copyright (C) 2014, 2017 Guus Sliepen <guus@meshlink.io>
-
-    This program is free software; you can redistribute it and/or modify
-    it under the terms of the GNU General Public License as published by
-    the Free Software Foundation; either version 2 of the License, or
-    (at your option) any later version.
-
-    This program is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU General Public License for more details.
-
-    You should have received a copy of the GNU General Public License along
-    with this program; if not, write to the Free Software Foundation, Inc.,
-    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
-*/
-
-#include "splay_tree.h"
-#include "connection.h"
-#include "net.h"
-#include "node.h"
-
-typedef struct edge_t {
-	struct node_t *from;
-	struct node_t *to;
-	sockaddr_t address;
-
-	struct connection_t *connection;        /* connection associated with this edge, if available */
-	struct edge_t *reverse;                 /* edge in the opposite direction, if available */
-
-	int weight;                             /* weight of this edge */
-	uint32_t session_id;                     /* the session_id of the from node */
-} edge_t;
-
-void init_edges(struct meshlink_handle *mesh);
-void exit_edges(struct meshlink_handle *mesh);
-edge_t *new_edge(void) __attribute__((__malloc__));
-void free_edge(edge_t *);
-struct splay_tree_t *new_edge_tree(void) __attribute__((__malloc__));
-void free_edge_tree(struct splay_tree_t *);
-void edge_add(struct meshlink_handle *mesh, edge_t *);
-void edge_del(struct meshlink_handle *mesh, edge_t *);
-edge_t *lookup_edge(struct node_t *, struct node_t *) __attribute__((__warn_unused_result__));
-
-#endif
diff --git a/src/graph.c b/src/graph.c
deleted file mode 100644
index 9a2bfb1..0000000
--- a/src/graph.c
+++ /dev/null
@@ -1,231 +0,0 @@
-/*
-    graph.c -- graph algorithms
-    Copyright (C) 2014 Guus Sliepen <guus@meshlink.io>
-
-    This program is free software; you can redistribute it and/or modify
-    it under the terms of the GNU General Public License as published by
-    the Free Software Foundation; either version 2 of the License, or
-    (at your option) any later version.
-
-    This program is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU General Public License for more details.
-
-    You should have received a copy of the GNU General Public License along
-    with this program; if not, write to the Free Software Foundation, Inc.,
-    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
-*/
-
-/* We need to generate two trees from the graph:
-
-   1. A minimum spanning tree for broadcasts,
-   2. A single-source shortest path tree for unicasts.
-
-   Actually, the first one alone would suffice but would make unicast packets
-   take longer routes than necessary.
-
-   For the MST algorithm we can choose from Prim's or Kruskal's. I personally
-   favour Kruskal's, because we make an extra AVL tree of edges sorted on
-   weights (metric). That tree only has to be updated when an edge is added or
-   removed, and during the MST algorithm we just have go linearly through that
-   tree, adding safe edges until #edges = #nodes - 1. The implementation here
-   however is not so fast, because I tried to avoid having to make a forest and
-   merge trees.
-
-   For the SSSP algorithm Dijkstra's seems to be a nice choice. Currently a
-   simple breadth-first search is presented here.
-
-   The SSSP algorithm will also be used to determine whether nodes are
-   reachable from the source. It will also set the correct destination address
-   and port of a node if possible.
-*/
-
-#include "system.h"
-
-#include "connection.h"
-#include "edge.h"
-#include "graph.h"
-#include "list.h"
-#include "logger.h"
-#include "meshlink_internal.h"
-#include "netutl.h"
-#include "node.h"
-#include "protocol.h"
-#include "utils.h"
-#include "xalloc.h"
-#include "graph.h"
-
-/* Implementation of a simple breadth-first search algorithm.
-   Running time: O(E)
-*/
-
-static void sssp_bfs(meshlink_handle_t *mesh) {
-	list_t *todo_list = list_alloc(NULL);
-
-	/* Clear visited status on nodes */
-
-	for splay_each(node_t, n, mesh->nodes) {
-		n->status.visited = false;
-		n->distance = -1;
-	}
-
-	/* Begin with mesh->self */
-
-	mesh->self->status.visited = mesh->threadstarted;
-	mesh->self->nexthop = mesh->self;
-	mesh->self->prevedge = NULL;
-	mesh->self->distance = 0;
-	list_insert_head(todo_list, mesh->self);
-
-	/* Loop while todo_list is filled */
-
-	for list_each(node_t, n, todo_list) {                   /* "n" is the node from which we start */
-		logger(mesh, MESHLINK_DEBUG, " Examining edges from %s", n->name);
-
-		if(n->distance < 0) {
-			abort();
-		}
-
-		for splay_each(edge_t, e, n->edge_tree) {       /* "e" is the edge connected to "from" */
-			if(!e->reverse) {
-				continue;
-			}
-
-			/* Situation:
-
-			           /
-			          /
-			   ----->(n)---e-->(e->to)
-			          \
-			           \
-
-			   Where e is an edge, (n) and (e->to) are nodes.
-			   n->address is set to the e->address of the edge left of n to n.
-			   We are currently examining the edge e right of n from n:
-
-			   - If edge e provides for better reachability of e->to, update
-			     e->to and (re)add it to the todo_list to (re)examine the reachability
-			     of nodes behind it.
-			 */
-
-			if(e->to->status.visited
-			                && (e->to->distance != n->distance + 1 || e->weight >= e->to->prevedge->weight)) {
-				continue;
-			}
-
-			e->to->status.visited = true;
-			e->to->nexthop = (n->nexthop == mesh->self) ? e->to : n->nexthop;
-			e->to->prevedge = e;
-			e->to->distance = n->distance + 1;
-
-			if(!e->to->status.reachable || (e->to->address.sa.sa_family == AF_UNSPEC && e->address.sa.sa_family != AF_UNKNOWN)) {
-				update_node_udp(mesh, e->to, &e->address);
-			}
-
-			list_insert_tail(todo_list, e->to);
-		}
-
-		list_next = list_node->next; /* Because the list_insert_tail() above could have added something extra for us! */
-		list_delete_node(todo_list, list_node);
-	}
-
-	list_free(todo_list);
-}
-
-static void check_reachability(meshlink_handle_t *mesh) {
-	/* Check reachability status. */
-
-	int reachable = -1; /* Don't count ourself */
-
-	for splay_each(node_t, n, mesh->nodes) {
-		if(n->status.visited) {
-			reachable++;
-		}
-
-		/* Check for nodes that have changed session_id */
-		if(n->status.visited && n->prevedge && n->prevedge->reverse->session_id != n->session_id) {
-			logger(mesh, MESHLINK_DEBUG, "Node %s has a new session ID", n->name);
-
-			n->session_id = n->prevedge->reverse->session_id;
-
-			if(n->utcp) {
-				utcp_reset_all_connections(n->utcp);
-			}
-
-			n->status.validkey = false;
-			sptps_stop(&n->sptps);
-			n->status.waitingforkey = false;
-			n->last_req_key = -3600;
-
-			n->status.udp_confirmed = false;
-			n->maxmtu = MTU;
-			n->minmtu = 0;
-			n->mtuprobes = 0;
-
-			timeout_del(&mesh->loop, &n->mtutimeout);
-		}
-
-		if(n->status.visited != n->status.reachable) {
-			n->status.reachable = !n->status.reachable;
-			n->status.dirty = true;
-
-			if(!n->status.blacklisted) {
-				if(n->status.reachable) {
-					logger(mesh, MESHLINK_DEBUG, "Node %s became reachable", n->name);
-					bool first_time_reachable = !n->last_reachable;
-					n->last_reachable = time(NULL);
-
-					if(first_time_reachable) {
-						if(!node_write_config(mesh, n, false)) {
-							logger(mesh, MESHLINK_WARNING, "Could not write host config file for node %s!\n", n->name);
-
-						}
-					}
-				} else {
-					logger(mesh, MESHLINK_DEBUG, "Node %s became unreachable", n->name);
-					n->last_unreachable = time(NULL);
-				}
-			}
-
-			n->status.udp_confirmed = false;
-			n->maxmtu = MTU;
-			n->minmtu = 0;
-			n->mtuprobes = 0;
-
-			timeout_del(&mesh->loop, &n->mtutimeout);
-
-			if(!n->status.blacklisted) {
-				update_node_status(mesh, n);
-			}
-
-			if(!n->status.reachable) {
-				update_node_udp(mesh, n, NULL);
-				n->status.broadcast = false;
-			}
-
-			if(n->utcp) {
-				utcp_offline(n->utcp, !n->status.reachable);
-			}
-		}
-	}
-
-	if(mesh->reachable != reachable) {
-		if(!reachable) {
-			mesh->last_unreachable = mesh->loop.now.tv_sec;
-
-			if(mesh->threadstarted && mesh->periodictimer.cb) {
-				timeout_set(&mesh->loop, &mesh->periodictimer, &(struct timespec) {
-					0, prng(mesh, TIMER_FUDGE)
-				});
-			}
-		}
-
-		mesh->reachable = reachable;
-	}
-}
-
-void graph(meshlink_handle_t *mesh) {
-	sssp_bfs(mesh);
-	check_reachability(mesh);
-}
diff --git a/src/graph.h b/src/graph.h
deleted file mode 100644
index 1775732..0000000
--- a/src/graph.h
+++ /dev/null
@@ -1,25 +0,0 @@
-#ifndef MESHLINK_GRAPH_H
-#define MESHLINK_GRAPH_H
-
-/*
-    graph.h -- header for graph.c
-    Copyright (C) 2014, 2017 Guus Sliepen <guus@meshlink.io>
-
-    This program is free software; you can redistribute it and/or modify
-    it under the terms of the GNU General Public License as published by
-    the Free Software Foundation; either version 2 of the License, or
-    (at your option) any later version.
-
-    This program is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU General Public License for more details.
-
-    You should have received a copy of the GNU General Public License along
-    with this program; if not, write to the Free Software Foundation, Inc.,
-    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
-*/
-
-void graph(struct meshlink_handle *mesh);
-
-#endif
diff --git a/src/mdns.c b/src/mdns.c
deleted file mode 100644
index e92a833..0000000
--- a/src/mdns.c
+++ /dev/null
@@ -1,441 +0,0 @@
-// SPDX-FileCopyrightText: 2020 Guus Sliepen <guus@meshlink.io>
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-#include "system.h"
-
-#include <stddef.h>
-
-#include "mdns.h"
-#include "xalloc.h"
-
-// Creating a buffer
-
-typedef struct {
-	uint8_t *ptr;
-	ptrdiff_t len;
-} buf_t;
-
-static void buf_add(buf_t *buf, const void *data, uint32_t len) {
-	if(buf->len >= len) {
-		memcpy(buf->ptr, data, len);
-		buf->ptr += len;
-		buf->len -= len;
-	} else {
-		buf->len = -1;
-	}
-}
-
-static void buf_add_uint8(buf_t *buf, uint8_t val) {
-	if(buf->len >= 1) {
-		buf->ptr[0] = val;
-		buf->ptr++;
-		buf->len--;
-	} else {
-		buf->len = -1;
-	}
-}
-
-static void buf_add_uint16(buf_t *buf, uint16_t val) {
-	uint16_t nval = htons(val);
-	buf_add(buf, &nval, sizeof(nval));
-}
-
-static void buf_add_uint32(buf_t *buf, uint32_t val) {
-	uint32_t nval = htonl(val);
-	buf_add(buf, &nval, sizeof(nval));
-}
-
-static void buf_add_label(buf_t *buf, const char *str) {
-	size_t len = strlen(str);
-
-	if(len < 256) {
-		buf_add_uint8(buf, len);
-		buf_add(buf, str, len);
-	} else {
-		buf->len = -1;
-	}
-}
-
-static void buf_add_ulabel(buf_t *buf, const char *str) {
-	size_t len = strlen(str);
-
-	if(len + 1 < 256) {
-		buf_add_uint8(buf, len + 1);
-		buf_add_uint8(buf, '_');
-		buf_add(buf, str, len);
-	} else {
-		buf->len = -1;
-	}
-}
-
-static void buf_add_kvp(buf_t *buf, const char *key, const char *val) {
-	size_t key_len = strlen(key);
-	size_t val_len = strlen(val);
-
-	if(key_len + val_len + 1 < 256) {
-		buf_add_uint8(buf, key_len + val_len + 1);
-		buf_add(buf, key, key_len);
-		buf_add_uint8(buf, '=');
-		buf_add(buf, val, val_len);
-	} else {
-		buf->len = -1;
-	}
-}
-
-static uint8_t *buf_len_start(buf_t *buf) {
-	if(buf->len < 2) {
-		buf->len = -1;
-		return NULL;
-	} else {
-		uint8_t *ptr = buf->ptr;
-		buf->ptr += 2;
-		buf->len -= 2;
-		return ptr;
-	}
-}
-
-static void buf_len_end(buf_t *buf, uint8_t *ptr) {
-	if(buf->len < 0) {
-		return;
-	}
-
-	uint16_t len = htons(buf->ptr - ptr - 2);
-	memcpy(ptr, &len, sizeof(len));
-}
-
-// Functions reading a buffer
-
-typedef struct {
-	const uint8_t *ptr;
-	ptrdiff_t len;
-} cbuf_t;
-
-static void buf_check(cbuf_t *buf, const void *data, uint32_t len) {
-	if(buf->len >= len && !memcmp(buf->ptr, data, len)) {
-		buf->ptr += len;
-		buf->len -= len;
-	} else {
-		buf->len = -1;
-	}
-}
-
-static void buf_check_uint8(cbuf_t *buf, uint8_t val) {
-	if(buf->len >= 1 && buf->ptr[0] == val) {
-		buf->ptr++;
-		buf->len--;
-	} else {
-		buf->len = -1;
-	}
-}
-
-static void buf_check_uint16(cbuf_t *buf, uint16_t val) {
-	uint16_t nval = htons(val);
-	buf_check(buf, &nval, sizeof(nval));
-}
-
-static uint16_t buf_get_uint16(cbuf_t *buf) {
-	uint16_t nval;
-
-	if(buf->len >= 2) {
-		memcpy(&nval, buf->ptr, 2);
-		buf->ptr += 2;
-		buf->len -= 2;
-		return ntohs(nval);
-	} else {
-		buf->len = -1;
-		return 0;
-	}
-}
-
-static void buf_check_uint32(cbuf_t *buf, uint32_t val) {
-	uint32_t nval = htonl(val);
-	buf_check(buf, &nval, sizeof(nval));
-}
-
-static void buf_check_label(cbuf_t *buf, const char *str) {
-	size_t len = strlen(str);
-
-	if(len < 256) {
-		buf_check_uint8(buf, len);
-		buf_check(buf, str, len);
-	} else {
-		buf->len = -1;
-	}
-}
-
-static char *buf_get_label(cbuf_t *buf) {
-	if(buf->len < 1) {
-		buf->len = -1;
-		return NULL;
-	}
-
-	uint8_t len = buf->ptr[0];
-	buf->ptr++;
-	buf->len--;
-
-	if(buf->len < len) {
-		buf->len = -1;
-		return NULL;
-	}
-
-	char *label = xmalloc(len + 1);
-	memcpy(label, buf->ptr, len);
-	label[len] = 0;
-	buf->ptr += len;
-	buf->len -= len;
-	return label;
-}
-
-static void buf_check_ulabel(cbuf_t *buf, const char *str) {
-	size_t len = strlen(str);
-
-	if(len + 1 < 256) {
-		buf_check_uint8(buf, len + 1);
-		buf_check_uint8(buf, '_');
-		buf_check(buf, str, len);
-	} else {
-		buf->len = -1;
-	}
-}
-
-static void buf_get_kvp(cbuf_t *buf, const char *key, char **val) {
-	char *kvp = buf_get_label(buf);
-
-	if(buf->len == -1) {
-		return;
-	}
-
-	char *split = strchr(kvp, '=');
-
-	if(!split) {
-		buf->len = -1;
-		return;
-	}
-
-	*split++ = 0;
-
-	if(strcmp(kvp, key)) {
-		buf->len = -1;
-		return;
-	}
-
-	memmove(kvp, split, strlen(split) + 1);
-	*val = kvp;
-}
-
-static const uint8_t *buf_check_len_start(cbuf_t *buf) {
-	if(buf->len < 2) {
-		buf->len = -1;
-		return NULL;
-	} else {
-		const uint8_t *ptr = buf->ptr;
-		buf->ptr += 2;
-		buf->len -= 2;
-		return ptr;
-	}
-}
-
-static void buf_check_len_end(cbuf_t *buf, const uint8_t *ptr) {
-	if(buf->len < 0) {
-		return;
-	}
-
-	uint16_t len = htons(buf->ptr - ptr - 2);
-
-	if(memcmp(ptr, &len, sizeof(len))) {
-		buf->len = -1;
-	}
-}
-
-size_t prepare_request(void *vdata, size_t size, const char *protocol, const char *transport) {
-	uint8_t *data = vdata;
-	buf_t buf = {data, size};
-
-	// Header
-	buf_add_uint16(&buf, 0); // TX ID
-	buf_add_uint16(&buf, 0); // flags
-	buf_add_uint16(&buf, 1); // 1 question
-	buf_add_uint16(&buf, 0); // 0 answer RR
-	buf_add_uint16(&buf, 0); // 0 authority RRs
-	buf_add_uint16(&buf, 0); // 0 additional RR
-
-	// Question section: _protocol._transport.local PTR IN
-	buf_add_ulabel(&buf, protocol);
-	buf_add_ulabel(&buf, transport);
-	buf_add_label(&buf, "local");
-	buf_add_uint8(&buf, 0);
-	buf_add_uint16(&buf, 0xc); // PTR
-	buf_add_uint16(&buf, 0x1); // IN
-
-	// Done.
-	if(buf.len < 0) {
-		return 0;
-	} else {
-		return buf.ptr - data;
-	}
-}
-
-bool parse_request(const void *vdata, size_t size, const char *protocol, const char *transport) {
-	const uint8_t *data = vdata;
-	cbuf_t buf = {data, size};
-
-	// Header
-	buf_get_uint16(&buf); // TX ID
-	buf_check_uint16(&buf, 0); // flags
-	buf_check_uint16(&buf, 1); // 1 question
-	buf_get_uint16(&buf); // ? answer RR
-	buf_get_uint16(&buf); // ? authority RRs
-	buf_get_uint16(&buf); // ? additional RR
-
-	if(buf.len == -1) {
-		return false;
-	}
-
-	// Question section: _protocol._transport.local PTR IN
-	buf_check_ulabel(&buf, protocol);
-	buf_check_ulabel(&buf, transport);
-	buf_check_label(&buf, "local");
-	buf_check_uint8(&buf, 0);
-	buf_check_uint16(&buf, 0xc); // PTR
-	buf_check_uint16(&buf, 0x1); // IN
-
-	if(buf.len == -1) {
-		return false;
-	}
-
-	// Done.
-	return buf.len != -1;
-}
-
-size_t prepare_response(void *vdata, size_t size, const char *name, const char *protocol, const char *transport, uint16_t port, int nkeys, const char **keys, const char **values) {
-	uint8_t *data = vdata;
-	buf_t buf = {data, size};
-
-	// Header
-	buf_add_uint16(&buf, 0); // TX ID
-	buf_add_uint16(&buf, 0x8400); // flags
-	buf_add_uint16(&buf, 0); // 1 question
-	buf_add_uint16(&buf, 3); // 1 answer RR
-	buf_add_uint16(&buf, 0); // 0 authority RRs
-	buf_add_uint16(&buf, 0); // 1 additional RR
-
-	// Add the TXT record: _protocol._transport local TXT IN 3600 name._protocol._transport key=value...
-	uint16_t full_name = buf.ptr - data; // remember start of full name
-	buf_add_label(&buf, name);
-	uint16_t protocol_offset = buf.ptr - data; // remember start of _protocol
-	buf_add_ulabel(&buf, protocol);
-	buf_add_ulabel(&buf, transport);
-	uint16_t local_offset = buf.ptr - data; // remember start of local
-	buf_add_label(&buf, "local");
-	buf_add_uint8(&buf, 0);
-	buf_add_uint16(&buf, 0x10); // TXT
-	buf_add_uint16(&buf, 0x1); // IN
-	buf_add_uint32(&buf, 3600); // TTL
-
-	uint8_t *len_ptr = buf_len_start(&buf);
-
-	for(int i = 0; i < nkeys; i++) {
-		buf_add_kvp(&buf, keys[i], values[i]);
-	}
-
-	buf_len_end(&buf, len_ptr);
-
-	// Add the PTR record: _protocol._transport.local PTR IN 3600 name._protocol._transport.local
-	buf_add_uint16(&buf, 0xc000 | protocol_offset);
-	buf_add_uint16(&buf, 0xc); // PTR
-	buf_add_uint16(&buf, 0x8001); // IN (flush)
-	buf_add_uint32(&buf, 3600); // TTL
-	len_ptr = buf_len_start(&buf);
-	buf_add_uint16(&buf, 0xc000 | full_name);
-	buf_len_end(&buf, len_ptr);
-
-	// Add the SRV record: name._protocol._transport.local SRV IN 120 0 0 port name.local
-	buf_add_uint16(&buf, 0xc000 | full_name);
-	buf_add_uint16(&buf, 0x21); // SRV
-	buf_add_uint16(&buf, 0x8001); // IN (flush)
-	buf_add_uint32(&buf, 120); // TTL
-	len_ptr = buf_len_start(&buf);
-	buf_add_uint16(&buf, 0); // priority
-	buf_add_uint16(&buf, 0); // weight
-	buf_add_uint16(&buf, port); // port
-	buf_add_label(&buf, name);
-	buf_add_uint16(&buf, 0xc000 | local_offset);
-	buf_len_end(&buf, len_ptr);
-
-	// Done.
-	if(buf.len < 0) {
-		return 0;
-	} else {
-		return buf.ptr - data;
-	}
-}
-
-bool parse_response(const void *vdata, size_t size, char **name, const char *protocol, const char *transport, uint16_t *port, int nkeys, const char **keys, char **values) {
-	const uint8_t *data = vdata;
-	cbuf_t buf = {data, size};
-
-	// Header
-	buf_check_uint16(&buf, 0); // TX ID
-	buf_check_uint16(&buf, 0x8400); // flags
-	buf_check_uint16(&buf, 0); // 0 question
-	buf_check_uint16(&buf, 3); // 1 answer RR
-	buf_check_uint16(&buf, 0); // 0 authority RRs
-	buf_check_uint16(&buf, 0); // 0 checkitional RR
-
-	if(buf.len == -1) {
-		return false;
-	}
-
-	// Check the TXT record: _protocol._transport local TXT IN 3600 name._protocol._transport key=value...
-	uint16_t full_name = buf.ptr - data; // remember start of full name
-	*name = buf_get_label(&buf);
-	uint16_t protocol_offset = buf.ptr - data; // remember start of _protocol
-	buf_check_ulabel(&buf, protocol);
-	buf_check_ulabel(&buf, transport);
-	uint16_t local_offset = buf.ptr - data; // remember start of local
-	buf_check_label(&buf, "local");
-	buf_check_uint8(&buf, 0);
-	buf_check_uint16(&buf, 0x10); // TXT
-	buf_check_uint16(&buf, 0x1); // IN
-	buf_check_uint32(&buf, 3600); // TTL
-	const uint8_t *len_ptr = buf_check_len_start(&buf);
-
-	for(int i = 0; i < nkeys; i++) {
-		buf_get_kvp(&buf, keys[i], &values[i]);
-	}
-
-	buf_check_len_end(&buf, len_ptr);
-
-	if(buf.len == -1) {
-		return false;
-	}
-
-	// Check the PTR record: _protocol._transport.local PTR IN 3600 name._protocol._transport.local
-	buf_check_uint16(&buf, 0xc000 | protocol_offset);
-	buf_check_uint16(&buf, 0xc); // PTR
-	buf_check_uint16(&buf, 0x8001); // IN (flush)
-	buf_check_uint32(&buf, 3600); // TTL
-	len_ptr = buf_check_len_start(&buf);
-	buf_check_uint16(&buf, 0xc000 | full_name);
-	buf_check_len_end(&buf, len_ptr);
-
-	if(buf.len == -1) {
-		return false;
-	}
-
-	// Check the SRV record: name._protocol._transport.local SRV IN 120 0 0 port name.local
-	buf_check_uint16(&buf, 0xc000 | full_name);
-	buf_check_uint16(&buf, 0x21); // SRV
-	buf_check_uint16(&buf, 0x8001); // IN (flush)
-	buf_check_uint32(&buf, 120); // TTL
-	len_ptr = buf_check_len_start(&buf);
-	buf_check_uint16(&buf, 0); // priority
-	buf_check_uint16(&buf, 0); // weight
-	*port = buf_get_uint16(&buf); // port
-	buf_check_label(&buf, *name);
-	buf_check_uint16(&buf, 0xc000 | local_offset);
-	buf_check_len_end(&buf, len_ptr);
-
-	// Done.
-	return buf.len == 0;
-}
diff --git a/src/mdns.h b/src/mdns.h
deleted file mode 100644
index 6029158..0000000
--- a/src/mdns.h
+++ /dev/null
@@ -1,14 +0,0 @@
-#pragma once
-
-// SPDX-FileCopyrightText: 2020 Guus Sliepen <guus@meshlink.io>
-// SPDX-License-Identifier: GPL-2.0-or-later
-
-#include <stdint.h>
-#include <unistd.h>
-
-size_t prepare_packet(void *buf, size_t size, const char *name, const char *protocol, const char *transport, uint16_t port, int nkeys, const char **keys, const char **values, bool response);
-size_t prepare_request(void *buf, size_t size, const char *protocol, const char *transport);
-size_t prepare_response(void *buf, size_t size, const char *name, const char *protocol, const char *transport, uint16_t port, int nkeys, const char **keys, const char **values);
-bool parse_response(const void *buf, size_t size, char **name, const char *protocol, const char *transport, uint16_t *port, int nkeys, const char **keys, char **values);
-bool parse_request(const void *buf, size_t size, const char *protocol, const char *transport);
-
diff --git a/src/meshlink-tiny++.h b/src/meshlink-tiny++.h
index b9e7bb7..fbe0b29 100644
--- a/src/meshlink-tiny++.h
+++ b/src/meshlink-tiny++.h
@@ -1017,16 +1017,6 @@ public:
 		return meshlink_channel_get_mss(handle, channel);
 	};
 
-	/// Enable or disable zeroconf discovery of local peers
-	/** This controls whether zeroconf discovery using the Catta library will be
-	 *  enabled to search for peers on the local network. By default, it is enabled.
-	 *
-	 *  @param enable  Set to true to enable discovery, false to disable.
-	 */
-	void enable_discovery(bool enable = true) {
-		meshlink_enable_discovery(handle, enable);
-	}
-
 	/// Inform MeshLink that the local network configuration might have changed
 	/** This is intended to be used when there is no way for MeshLink to get notifications of local network changes.
 	 *  It forces MeshLink to scan all network interfaces for changes in up/down status and new/removed addresses,
@@ -1080,17 +1070,6 @@ public:
 		meshlink_set_inviter_commits_first(handle, inviter_commits_first);
 	}
 
-	/// Set the URL used to discover the host's external address
-	/** For generating invitation URLs, MeshLink can look up the externally visible address of the local node.
-	 *  It does so by querying an external service. By default, this is http://meshlink.io/host.cgi.
-	 *  Only URLs starting with http:// are supported.
-	 *
-	 *  @param url   The URL to use for external address queries, or NULL to revert back to the default URL.
-	 */
-	void set_external_address_discovery_url(const char *url) {
-		meshlink_set_external_address_discovery_url(handle, url);
-	}
-
 private:
 	// non-copyable:
 	mesh(const mesh &) /* TODO: C++11: = delete */;
diff --git a/src/meshlink-tiny.h b/src/meshlink-tiny.h
index 8dbe725..0ee3f1a 100644
--- a/src/meshlink-tiny.h
+++ b/src/meshlink-tiny.h
@@ -1544,16 +1544,6 @@ void meshlink_set_node_channel_timeout(struct meshlink_handle *mesh, struct mesh
  */
 void meshlink_hint_address(struct meshlink_handle *mesh, struct meshlink_node *node, const struct sockaddr *addr);
 
-/// Enable or disable zeroconf discovery of local peers
-/** This controls whether zeroconf discovery using the Catta library will be
- *  enabled to search for peers on the local network. By default, it is enabled.
- *
- *  \memberof meshlink_handle
- *  @param mesh    A handle which represents an instance of MeshLink.
- *  @param enable  Set to true to enable discovery, false to disable.
- */
-void meshlink_enable_discovery(struct meshlink_handle *mesh, bool enable);
-
 /// Inform MeshLink that the local network configuration might have changed
 /** This is intended to be used when there is no way for MeshLink to get notifications of local network changes.
  *  It forces MeshLink to scan all network interfaces for changes in up/down status and new/removed addresses,
@@ -1630,17 +1620,6 @@ void meshlink_reset_timers(struct meshlink_handle *mesh);
  */
 void meshlink_set_inviter_commits_first(struct meshlink_handle *mesh, bool inviter_commits_first);
 
-/// Set the URL used to discover the host's external address
-/** For generating invitation URLs, MeshLink can look up the externally visible address of the local node.
- *  It does so by querying an external service. By default, this is http://meshlink.io/host.cgi.
- *  Only URLs starting with http:// are supported.
- *
- *  \memberof meshlink_handle
- *  @param mesh  A handle which represents an instance of MeshLink.
- *  @param url   The URL to use for external address queries, or NULL to revert back to the default URL.
- */
-void meshlink_set_external_address_discovery_url(struct meshlink_handle *mesh, const char *url);
-
 /// Set the scheduling granularity of the application
 /** This should be set to the effective scheduling granularity for the application.
  *  This depends on the scheduling granularity of the operating system, the application's
diff --git a/src/meshlink.c b/src/meshlink.c
index 0d607fe..abec7f4 100644
--- a/src/meshlink.c
+++ b/src/meshlink.c
@@ -37,9 +37,7 @@
 #include "utils.h"
 #include "xalloc.h"
 #include "ed25519/sha512.h"
-#include "discovery.h"
 #include "devtools.h"
-#include "graph.h"
 
 #ifndef MSG_NOSIGNAL
 #define MSG_NOSIGNAL 0
@@ -464,18 +462,7 @@ static bool try_bind(meshlink_handle_t *mesh, int port) {
 			}
 		}
 
-		/* If TCP worked, then we require that UDP works as well. */
-
-		int udp_fd = setup_udp_listen_socket(mesh, aip);
-
-		if(udp_fd == -1) {
-			closesocket(tcp_fd);
-			success = false;
-			break;
-		}
-
 		closesocket(tcp_fd);
-		closesocket(udp_fd);
 		success = true;
 	}
 
@@ -656,10 +643,6 @@ static bool finalize_join(join_state_t *state, const void *buf, uint16_t len) {
 			}
 		}
 
-		/* Clear the reachability times, since we ourself have never seen these nodes yet */
-		n->last_reachable = 0;
-		n->last_unreachable = 0;
-
 		if(!node_write_config(mesh, n, true)) {
 			free_node(n);
 			return false;
@@ -1369,7 +1352,6 @@ meshlink_handle_t *meshlink_open_ex(const meshlink_open_params_t *params) {
 
 	mesh->appname = xstrdup(params->appname);
 	mesh->devclass = params->devclass;
-	mesh->discovery.enabled = true;
 	mesh->netns = params->netns;
 	mesh->submeshes = NULL;
 	mesh->log_cb = global_log_cb;
@@ -1545,10 +1527,6 @@ static void *meshlink_main_loop(void *arg) {
 #endif // HAVE_SETNS
 	}
 
-	if(mesh->discovery.enabled) {
-		discovery_start(mesh);
-	}
-
 	if(pthread_mutex_lock(&mesh->mutex) != 0) {
 		abort();
 	}
@@ -1560,11 +1538,6 @@ static void *meshlink_main_loop(void *arg) {
 
 	pthread_mutex_unlock(&mesh->mutex);
 
-	// Stop discovery
-	if(mesh->discovery.enabled) {
-		discovery_stop(mesh);
-	}
-
 	return NULL;
 }
 
@@ -1636,9 +1609,6 @@ bool meshlink_start(meshlink_handle_t *mesh) {
 	pthread_cond_wait(&mesh->cond, &mesh->mutex);
 	mesh->threadstarted = true;
 
-	// Ensure we are considered reachable
-	graph(mesh);
-
 	pthread_mutex_unlock(&mesh->mutex);
 	return true;
 }
@@ -1658,20 +1628,7 @@ void meshlink_stop(meshlink_handle_t *mesh) {
 	// Shut down the main thread
 	event_loop_stop(&mesh->loop);
 
-	// Send ourselves a UDP packet to kick the event loop
-	for(int i = 0; i < mesh->listen_sockets; i++) {
-		sockaddr_t sa;
-		socklen_t salen = sizeof(sa);
-
-		if(getsockname(mesh->listen_socket[i].udp.fd, &sa.sa, &salen) == -1) {
-			logger(mesh, MESHLINK_ERROR, "System call `%s' failed: %s", "getsockname", sockstrerror(sockerrno));
-			continue;
-		}
-
-		if(sendto(mesh->listen_socket[i].udp.fd, "", 1, MSG_NOSIGNAL, &sa.sa, salen) == -1) {
-			logger(mesh, MESHLINK_ERROR, "Could not send a UDP packet to ourself: %s", sockstrerror(sockerrno));
-		}
-	}
+	// TODO: send something to a local socket to kick the event loop
 
 	if(mesh->threadstarted) {
 		// Wait for the main thread to finish
@@ -1701,11 +1658,6 @@ void meshlink_stop(meshlink_handle_t *mesh) {
 	exit_adns(mesh);
 	exit_outgoings(mesh);
 
-	// Ensure we are considered unreachable
-	if(mesh->nodes) {
-		graph(mesh);
-	}
-
 	// 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) {
@@ -2289,26 +2241,6 @@ struct time_range {
 	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;
@@ -2327,17 +2259,6 @@ 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;
@@ -2387,13 +2308,9 @@ bool meshlink_get_node_reachability(struct meshlink_handle *mesh, struct meshlin
 
 	reachable = n->status.reachable && !n->status.blacklisted;
 
-	if(last_reachable) {
-		*last_reachable = n->last_reachable;
-	}
-
-	if(last_unreachable) {
-		*last_unreachable = n->last_unreachable;
-	}
+	// TODO: handle reachable times?
+	(void)last_reachable;
+	(void)last_unreachable;
 
 	pthread_mutex_unlock(&mesh->mutex);
 
@@ -3075,10 +2992,6 @@ bool meshlink_import(meshlink_handle_t *mesh, const char *data) {
 			break;
 		}
 
-		/* Clear the reachability times, since we ourself have never seen these nodes yet */
-		n->last_reachable = 0;
-		n->last_unreachable = 0;
-
 		if(!node_write_config(mesh, n, true)) {
 			free_node(n);
 			free(buf);
@@ -4049,36 +3962,6 @@ void handle_duplicate_node(meshlink_handle_t *mesh, node_t *n) {
 	mesh->node_duplicate_cb(mesh, (meshlink_node_t *)n);
 }
 
-void meshlink_enable_discovery(meshlink_handle_t *mesh, bool enable) {
-	logger(mesh, MESHLINK_DEBUG, "meshlink_enable_discovery(%d)", enable);
-
-	if(!mesh) {
-		meshlink_errno = MESHLINK_EINVAL;
-		return;
-	}
-
-	if(pthread_mutex_lock(&mesh->mutex) != 0) {
-		abort();
-	}
-
-	if(mesh->discovery.enabled == enable) {
-		goto end;
-	}
-
-	if(mesh->threadstarted) {
-		if(enable) {
-			discovery_start(mesh);
-		} else {
-			discovery_stop(mesh);
-		}
-	}
-
-	mesh->discovery.enabled = enable;
-
-end:
-	pthread_mutex_unlock(&mesh->mutex);
-}
-
 void meshlink_hint_network_change(struct meshlink_handle *mesh) {
 	logger(mesh, MESHLINK_DEBUG, "meshlink_hint_network_change()");
 
@@ -4091,15 +3974,6 @@ void meshlink_hint_network_change(struct meshlink_handle *mesh) {
 		abort();
 	}
 
-	if(mesh->discovery.enabled) {
-		scan_ifaddrs(mesh);
-	}
-
-	if(mesh->loop.now.tv_sec > mesh->discovery.last_update + 5) {
-		mesh->discovery.last_update = mesh->loop.now.tv_sec;
-		handle_network_change(mesh, 1);
-	}
-
 	pthread_mutex_unlock(&mesh->mutex);
 }
 
@@ -4180,10 +4054,6 @@ void meshlink_reset_timers(struct meshlink_handle *mesh) {
 
 	handle_network_change(mesh, true);
 
-	if(mesh->discovery.enabled) {
-		discovery_refresh(mesh);
-	}
-
 	pthread_mutex_unlock(&mesh->mutex);
 }
 
@@ -4203,28 +4073,6 @@ void meshlink_set_inviter_commits_first(struct meshlink_handle *mesh, bool invit
 	pthread_mutex_unlock(&mesh->mutex);
 }
 
-void meshlink_set_external_address_discovery_url(struct meshlink_handle *mesh, const char *url) {
-	logger(mesh, MESHLINK_DEBUG, "meshlink_set_external_address_discovery_url(%s)", url ? url : "(null)");
-
-	if(!mesh) {
-		meshlink_errno = EINVAL;
-		return;
-	}
-
-	if(url && (strncmp(url, "http://", 7) || strchr(url, ' '))) {
-		meshlink_errno = EINVAL;
-		return;
-	}
-
-	if(pthread_mutex_lock(&mesh->mutex) != 0) {
-		abort();
-	}
-
-	free(mesh->external_address_url);
-	mesh->external_address_url = url ? xstrdup(url) : NULL;
-	pthread_mutex_unlock(&mesh->mutex);
-}
-
 void meshlink_set_scheduling_granularity(struct meshlink_handle *mesh, long granularity) {
 	logger(mesh, MESHLINK_DEBUG, "meshlink_set_scheduling_granularity(%ld)", granularity);
 
diff --git a/src/meshlink.sym b/src/meshlink.sym
index acb8525..f6cd014 100644
--- a/src/meshlink.sym
+++ b/src/meshlink.sym
@@ -1,6 +1,4 @@
 __emutls_v.meshlink_errno
-devtool_export_json_all_edges_state
-devtool_get_all_edges
 devtool_get_all_submeshes
 devtool_get_node_status
 devtool_keyrotate_probe
@@ -28,7 +26,6 @@ meshlink_clear_canonical_address
 meshlink_close
 meshlink_destroy
 meshlink_destroy_ex
-meshlink_enable_discovery
 meshlink_encrypted_key_rotate
 meshlink_errno
 meshlink_export
@@ -81,7 +78,6 @@ meshlink_set_dev_class_fast_retry_period
 meshlink_set_dev_class_maxtimeout
 meshlink_set_dev_class_timeouts
 meshlink_set_error_cb
-meshlink_set_external_address_discovery_url
 meshlink_set_inviter_commits_first
 meshlink_set_log_cb
 meshlink_set_node_channel_timeout
diff --git a/src/meshlink_internal.h b/src/meshlink_internal.h
index af3a250..1084911 100644
--- a/src/meshlink_internal.h
+++ b/src/meshlink_internal.h
@@ -47,7 +47,6 @@ static const char meshlink_udp_label[] = "MeshLink UDP";
 
 typedef struct listen_socket_t {
 	struct io_t tcp;
-	struct io_t udp;
 	sockaddr_t sa;
 	sockaddr_t broadcast_sa;
 } listen_socket_t;
@@ -100,10 +99,7 @@ struct meshlink_handle {
 	meshlink_queue_t outpacketqueue;
 	signal_t datafromapp;
 
-	hash_t *node_udp_cache;
-
 	struct splay_tree_t *nodes;
-	struct splay_tree_t *edges;
 
 	struct list_t *connections;
 	struct list_t *outgoings;
@@ -148,8 +144,6 @@ struct meshlink_handle {
 
 	dev_class_t devclass;
 
-	int udp_choice;
-
 	dev_class_traits_t dev_class_traits[DEV_CLASS_COUNT];
 
 	int netns;
@@ -168,22 +162,6 @@ struct meshlink_handle {
 	pthread_cond_t cond;
 	bool threadstarted;
 
-	// mDNS discovery
-	struct {
-		bool enabled;
-		io_t pfroute_io;
-		int *ifaces;
-		struct discovery_address *addresses;
-		int iface_count;
-		int address_count;
-		io_t sockets[2];
-		time_t last_update;
-#ifdef __APPLE__
-		pthread_t thread;
-		void *runloop;
-#endif
-	} discovery;
-
 	// ADNS
 	pthread_t adns_thread;
 	pthread_cond_t adns_cond;
diff --git a/src/net.c b/src/net.c
index 35cfc65..60c16cb 100644
--- a/src/net.c
+++ b/src/net.c
@@ -23,7 +23,6 @@
 #include "conf.h"
 #include "connection.h"
 #include "devtools.h"
-#include "graph.h"
 #include "logger.h"
 #include "meshlink_internal.h"
 #include "meta.h"
@@ -47,11 +46,12 @@ static const int default_interval = 60;
 /*
   Terminate a connection:
   - Mark it as inactive
-  - Remove the edge representing this connection
   - Kill it with fire
   - Check if we need to retry making an outgoing connection
 */
 void terminate_connection(meshlink_handle_t *mesh, connection_t *c, bool report) {
+	(void)report;
+
 	if(c->status.active) {
 		logger(mesh, MESHLINK_INFO, "Closing connection with %s", c->name);
 	}
@@ -66,31 +66,6 @@ void terminate_connection(meshlink_handle_t *mesh, connection_t *c, bool report)
 
 	c->status.active = false;
 
-	if(c->edge) {
-		if(report) {
-			send_del_edge(mesh, mesh->everyone, c->edge, 0);
-		}
-
-		edge_del(mesh, c->edge);
-		c->edge = NULL;
-
-		/* Run MST and SSSP algorithms */
-
-		graph(mesh);
-
-		/* If the node is not reachable anymore but we remember it had an edge to us, clean it up */
-
-		if(report && c->node && !c->node->status.reachable) {
-			edge_t *e;
-			e = lookup_edge(c->node, mesh->self);
-
-			if(e) {
-				send_del_edge(mesh, mesh->everyone, e, 0);
-				edge_del(mesh, e);
-			}
-		}
-	}
-
 	outgoing_t *outgoing = c->outgoing;
 	connection_del(mesh, c);
 
diff --git a/src/net.h b/src/net.h
index 9994fac..34bb54c 100644
--- a/src/net.h
+++ b/src/net.h
@@ -89,7 +89,6 @@ void finish_connecting(struct meshlink_handle *mesh, struct connection_t *);
 void do_outgoing_connection(struct meshlink_handle *mesh, struct outgoing_t *);
 void handle_new_meta_connection(struct event_loop_t *loop, void *, int);
 int setup_tcp_listen_socket(struct meshlink_handle *mesh, const struct addrinfo *aip) __attribute__((__warn_unused_result__));
-int setup_udp_listen_socket(struct meshlink_handle *mesh, const struct addrinfo *aip) __attribute__((__warn_unused_result__));
 bool send_sptps_data(void *handle, uint8_t type, const void *data, size_t len);
 bool receive_sptps_record(void *handle, uint8_t type, const void *data, uint16_t len) __attribute__((__warn_unused_result__));
 void send_packet(struct meshlink_handle *mesh, struct node_t *, struct vpn_packet_t *);
diff --git a/src/net_packet.c b/src/net_packet.c
index 02a617d..b6478e4 100644
--- a/src/net_packet.c
+++ b/src/net_packet.c
@@ -22,7 +22,6 @@
 #include "conf.h"
 #include "connection.h"
 #include "crypto.h"
-#include "graph.h"
 #include "logger.h"
 #include "meshlink_internal.h"
 #include "net.h"
@@ -34,202 +33,6 @@
 
 int keylifetime = 0;
 
-static void send_udppacket(meshlink_handle_t *mesh, node_t *, vpn_packet_t *);
-
-#define MAX_SEQNO 1073741824
-
-/* mtuprobes == 1..30: initial discovery, send bursts with 1 second interval
-   mtuprobes ==    31: sleep pinginterval seconds
-   mtuprobes ==    32: send 1 burst, sleep pingtimeout second
-   mtuprobes ==    33: no response from other side, restart PMTU discovery process
-
-   Probes are sent in batches of at least three, with random sizes between the
-   lower and upper boundaries for the MTU thus far discovered.
-
-   After the initial discovery, a fourth packet is added to each batch with a
-   size larger than the currently known PMTU, to test if the PMTU has increased.
-
-   In case local discovery is enabled, another packet is added to each batch,
-   which will be broadcast to the local network.
-
-*/
-
-static void send_mtu_probe_handler(event_loop_t *loop, void *data) {
-	meshlink_handle_t *mesh = loop->data;
-	node_t *n = data;
-	int timeout = 1;
-
-	n->mtuprobes++;
-
-	if(!n->status.reachable || !n->status.validkey) {
-		logger(mesh, MESHLINK_INFO, "Trying to send MTU probe to unreachable or rekeying node %s", n->name);
-		n->mtuprobes = 0;
-		return;
-	}
-
-	if(n->mtuprobes > 32) {
-		if(!n->minmtu) {
-			n->mtuprobes = 31;
-			timeout = mesh->dev_class_traits[n->devclass].pinginterval;
-			goto end;
-		}
-
-		logger(mesh, MESHLINK_INFO, "%s did not respond to UDP ping, restarting PMTU discovery", n->name);
-		n->status.udp_confirmed = false;
-		n->mtuprobes = 1;
-		n->minmtu = 0;
-		n->maxmtu = MTU;
-
-		update_node_pmtu(mesh, n);
-	}
-
-	if(n->mtuprobes >= 10 && n->mtuprobes < 32 && !n->minmtu) {
-		logger(mesh, MESHLINK_INFO, "No response to MTU probes from %s", n->name);
-		n->mtuprobes = 31;
-	}
-
-	if(n->mtuprobes == 30 || (n->mtuprobes < 30 && n->minmtu >= n->maxmtu)) {
-		if(n->minmtu > n->maxmtu) {
-			n->minmtu = n->maxmtu;
-			update_node_pmtu(mesh, n);
-		} else {
-			n->maxmtu = n->minmtu;
-		}
-
-		n->mtu = n->minmtu;
-		logger(mesh, MESHLINK_INFO, "Fixing MTU of %s to %d after %d probes", n->name, n->mtu, n->mtuprobes);
-		n->mtuprobes = 31;
-	}
-
-	if(n->mtuprobes == 31) {
-		if(!n->minmtu && n->status.want_udp && n->nexthop && n->nexthop->connection) {
-			/* Send a dummy ANS_KEY to try to update the reflexive UDP address */
-			send_request(mesh, n->nexthop->connection, NULL, "%d %s %s . -1 -1 -1 0", ANS_KEY, mesh->self->name, n->name);
-			n->status.want_udp = false;
-		}
-
-		timeout = mesh->dev_class_traits[n->devclass].pinginterval;
-		goto end;
-	} else if(n->mtuprobes == 32) {
-		timeout = mesh->dev_class_traits[n->devclass].pingtimeout;
-	}
-
-	for(int i = 0; i < 5; i++) {
-		int len;
-
-		if(i == 0) {
-			if(n->mtuprobes < 30 || n->maxmtu + 8 >= MTU) {
-				continue;
-			}
-
-			len = n->maxmtu + 8;
-		} else if(n->maxmtu <= n->minmtu) {
-			len = n->maxmtu;
-		} else {
-			len = n->minmtu + 1 + prng(mesh, n->maxmtu - n->minmtu);
-		}
-
-		if(len < 64) {
-			len = 64;
-		}
-
-		vpn_packet_t packet;
-		packet.probe = true;
-		memset(packet.data, 0, 14);
-		randomize(packet.data + 14, len - 14);
-		packet.len = len;
-		n->status.broadcast = i >= 4 && n->mtuprobes <= 10 && n->prevedge;
-
-		logger(mesh, MESHLINK_DEBUG, "Sending MTU probe length %d to %s", len, n->name);
-
-		send_udppacket(mesh, n, &packet);
-	}
-
-	n->status.broadcast = false;
-
-end:
-	timeout_set(&mesh->loop, &n->mtutimeout, &(struct timespec) {
-		timeout, prng(mesh, TIMER_FUDGE)
-	});
-}
-
-void send_mtu_probe(meshlink_handle_t *mesh, node_t *n) {
-	timeout_add(&mesh->loop, &n->mtutimeout, send_mtu_probe_handler, n, &(struct timespec) {
-		1, 0
-	});
-	send_mtu_probe_handler(&mesh->loop, n);
-}
-
-static void mtu_probe_h(meshlink_handle_t *mesh, node_t *n, vpn_packet_t *packet, uint16_t len) {
-	if(len < 64) {
-		logger(mesh, MESHLINK_WARNING, "Got too short MTU probe length %d from %s", packet->len, n->name);
-		return;
-	}
-
-	logger(mesh, MESHLINK_DEBUG, "Got MTU probe length %d from %s", packet->len, n->name);
-
-	if(!packet->data[0]) {
-		/* It's a probe request, send back a reply */
-
-		packet->data[0] = 1;
-
-		/* Temporarily set udp_confirmed, so that the reply is sent
-		   back exactly the way it came in. */
-
-		bool udp_confirmed = n->status.udp_confirmed;
-		n->status.udp_confirmed = true;
-		send_udppacket(mesh, n, packet);
-		n->status.udp_confirmed = udp_confirmed;
-	} else {
-		/* It's a valid reply: now we know bidirectional communication
-		   is possible using the address and socket that the reply
-		   packet used. */
-
-		if(!n->status.udp_confirmed) {
-			char *address, *port;
-			sockaddr2str(&n->address, &address, &port);
-
-			if(n->nexthop && n->nexthop->connection) {
-				send_request(mesh, n->nexthop->connection, NULL, "%d %s %s . -1 -1 -1 0 %s %s", ANS_KEY, n->name, n->name, address, port);
-			} else {
-				logger(mesh, MESHLINK_WARNING, "Cannot send reflexive address to %s via %s", n->name, n->nexthop ? n->nexthop->name : n->name);
-			}
-
-			free(address);
-			free(port);
-			n->status.udp_confirmed = true;
-		}
-
-		/* If we haven't established the PMTU yet, restart the discovery process. */
-
-		if(n->mtuprobes > 30) {
-			if(len == n->maxmtu + 8) {
-				logger(mesh, MESHLINK_INFO, "Increase in PMTU to %s detected, restarting PMTU discovery", n->name);
-				n->maxmtu = MTU;
-				n->mtuprobes = 10;
-				return;
-			}
-
-			if(n->minmtu) {
-				n->mtuprobes = 30;
-			} else {
-				n->mtuprobes = 1;
-			}
-		}
-
-		/* If applicable, raise the minimum supported MTU */
-
-		if(len > n->maxmtu) {
-			len = n->maxmtu;
-		}
-
-		if(n->minmtu < len) {
-			n->minmtu = len;
-			update_node_pmtu(mesh, n);
-		}
-	}
-}
-
 /* VPN packet I/O */
 
 static void receive_packet(meshlink_handle_t *mesh, node_t *n, vpn_packet_t *packet) {
@@ -245,33 +48,6 @@ static void receive_packet(meshlink_handle_t *mesh, node_t *n, vpn_packet_t *pac
 	}
 }
 
-static bool try_mac(meshlink_handle_t *mesh, node_t *n, const vpn_packet_t *inpkt) {
-	(void)mesh;
-	return sptps_verify_datagram(&n->sptps, inpkt->data, inpkt->len);
-}
-
-static void receive_udppacket(meshlink_handle_t *mesh, node_t *n, vpn_packet_t *inpkt) {
-	if(!n->status.reachable) {
-		logger(mesh, MESHLINK_ERROR, "Got SPTPS data from unreachable node %s", n->name);
-		return;
-	}
-
-	if(!n->sptps.state) {
-		if(!n->status.waitingforkey) {
-			logger(mesh, MESHLINK_DEBUG, "Got packet from %s but we haven't exchanged keys yet", n->name);
-			send_req_key(mesh, n);
-		} else {
-			logger(mesh, MESHLINK_DEBUG, "Got packet from %s but he hasn't got our key yet", n->name);
-		}
-
-		return;
-	}
-
-	if(!sptps_receive_data(&n->sptps, inpkt->data, inpkt->len)) {
-		logger(mesh, MESHLINK_ERROR, "Could not process SPTPS data from %s: %s", n->name, strerror(errno));
-	}
-}
-
 static void send_sptps_packet(meshlink_handle_t *mesh, node_t *n, vpn_packet_t *origpkt) {
 	if(!n->status.reachable) {
 		logger(mesh, MESHLINK_ERROR, "Trying to send SPTPS data to unreachable node %s", n->name);
@@ -305,107 +81,6 @@ static void send_sptps_packet(meshlink_handle_t *mesh, node_t *n, vpn_packet_t *
 	return;
 }
 
-static void choose_udp_address(meshlink_handle_t *mesh, const node_t *n, const sockaddr_t **sa, int *sock, sockaddr_t *sa_buf) {
-	/* Latest guess */
-	*sa = &n->address;
-	*sock = n->sock;
-
-	/* If the UDP address is confirmed, use it. */
-	if(n->status.udp_confirmed) {
-		return;
-	}
-
-	/* Send every third packet to n->address; that could be set
-	   to the node's reflexive UDP address discovered during key
-	   exchange. */
-
-	if(++mesh->udp_choice >= 3) {
-		mesh->udp_choice = 0;
-		return;
-	}
-
-	/* If we have learned an address via Catta, try this once every batch */
-	if(mesh->udp_choice == 1 && n->catta_address.sa.sa_family != AF_UNSPEC) {
-		*sa = &n->catta_address;
-		goto check_socket;
-	}
-
-	/* Else, if we have a canonical address, try this once every batch */
-	if(mesh->udp_choice == 1 && n->canonical_address) {
-		char *host = xstrdup(n->canonical_address);
-		char *port = strchr(host, ' ');
-
-		if(port) {
-			*port++ = 0;
-			*sa_buf = str2sockaddr_random(mesh, host, port);
-			*sa = sa_buf;
-
-			if(sa_buf->sa.sa_family != AF_UNKNOWN) {
-				free(host);
-				goto check_socket;
-			}
-		}
-
-		free(host);
-	}
-
-	/* Otherwise, address are found in edges to this node.
-	   So we pick a random edge and a random socket. */
-
-	edge_t *candidate = NULL;
-
-	{
-		int i = 0;
-		int j = prng(mesh, n->edge_tree->count);
-
-		for splay_each(edge_t, e, n->edge_tree) {
-			if(i++ == j) {
-				candidate = e->reverse;
-				break;
-			}
-		}
-	}
-
-	if(candidate) {
-		*sa = &candidate->address;
-		*sock = prng(mesh, mesh->listen_sockets);
-	}
-
-check_socket:
-
-	/* Make sure we have a suitable socket for the chosen address */
-	if(mesh->listen_socket[*sock].sa.sa.sa_family != (*sa)->sa.sa_family) {
-		for(int i = 0; i < mesh->listen_sockets; i++) {
-			if(mesh->listen_socket[i].sa.sa.sa_family == (*sa)->sa.sa_family) {
-				*sock = i;
-				break;
-			}
-		}
-	}
-}
-
-static void choose_broadcast_address(meshlink_handle_t *mesh, const node_t *n, const sockaddr_t **sa, int *sock) {
-	*sock = prng(mesh, mesh->listen_sockets);
-	sockaddr_t *broadcast_sa = &mesh->listen_socket[*sock].broadcast_sa;
-
-	if(broadcast_sa->sa.sa_family == AF_INET6) {
-		broadcast_sa->in6.sin6_port = n->prevedge->address.in.sin_port;
-	} else {
-		broadcast_sa->in.sin_port = n->prevedge->address.in.sin_port;
-	}
-
-	*sa = broadcast_sa;
-}
-
-static void send_udppacket(meshlink_handle_t *mesh, node_t *n, vpn_packet_t *origpkt) {
-	if(!n->status.reachable) {
-		logger(mesh, MESHLINK_INFO, "Trying to send UDP packet to unreachable node %s", n->name);
-		return;
-	}
-
-	send_sptps_packet(mesh, n, origpkt);
-}
-
 bool send_sptps_data(void *handle, uint8_t type, const void *data, size_t len) {
 	assert(handle);
 	assert(data);
@@ -419,54 +94,29 @@ bool send_sptps_data(void *handle, uint8_t type, const void *data, size_t len) {
 		return false;
 	}
 
-	/* Send it via TCP if it is a handshake packet, TCPOnly is in use, or this packet is larger than the MTU. */
-
-	if(type >= SPTPS_HANDSHAKE || (type != PKT_PROBE && (len - 21) > to->minmtu)) {
-		char buf[len * 4 / 3 + 5];
-		b64encode(data, buf, len);
 
-		if(!to->nexthop || !to->nexthop->connection) {
-			logger(mesh, MESHLINK_WARNING, "Unable to forward SPTPS packet to %s via %s", to->name, to->nexthop ? to->nexthop->name : to->name);
-			return false;
-		}
-
-		/* If no valid key is known yet, send the packets using ANS_KEY requests,
-		   to ensure we get to learn the reflexive UDP address. */
-		if(!to->status.validkey) {
-			return send_request(mesh, to->nexthop->connection, NULL, "%d %s %s %s -1 -1 -1 %d", ANS_KEY, mesh->self->name, to->name, buf, 0);
-		} else {
-			return send_request(mesh, to->nexthop->connection, NULL, "%d %s %s %d %s", REQ_KEY, mesh->self->name, to->name, REQ_SPTPS, buf);
-		}
+	if(type == PKT_PROBE) {
+		/* Probe packets are not supported. */
+		return false;
 	}
 
-	/* Otherwise, send the packet via UDP */
+	/* Send it via TCP. */
 
-	sockaddr_t sa_buf;
-	const sockaddr_t *sa;
-	int sock;
+	char buf[len * 4 / 3 + 5];
+	b64encode(data, buf, len);
 
-	if(to->status.broadcast) {
-		choose_broadcast_address(mesh, to, &sa, &sock);
-	} else {
-		choose_udp_address(mesh, to, &sa, &sock, &sa_buf);
+	if(!to->nexthop || !to->nexthop->connection) {
+		logger(mesh, MESHLINK_WARNING, "Unable to forward SPTPS packet to %s via %s", to->name, to->nexthop ? to->nexthop->name : to->name);
+		return false;
 	}
 
-	if(sendto(mesh->listen_socket[sock].udp.fd, data, len, 0, &sa->sa, SALEN(sa->sa)) < 0 && !sockwouldblock(sockerrno)) {
-		if(sockmsgsize(sockerrno)) {
-			if(to->maxmtu >= len) {
-				to->maxmtu = len - 1;
-			}
-
-			if(to->mtu >= len) {
-				to->mtu = len - 1;
-			}
-		} else {
-			logger(mesh, MESHLINK_WARNING, "Error sending UDP SPTPS packet to %s: %s", to->name, sockstrerror(sockerrno));
-			return false;
-		}
+	/* If no valid key is known yet, send the packets using ANS_KEY requests,
+	   to ensure we get to learn the reflexive UDP address. */
+	if(!to->status.validkey) {
+		return send_request(mesh, to->nexthop->connection, NULL, "%d %s %s %s -1 -1 -1 %d", ANS_KEY, mesh->self->name, to->name, buf, 0);
+	} else {
+		return send_request(mesh, to->nexthop->connection, NULL, "%d %s %s %d %s", REQ_KEY, mesh->self->name, to->name, REQ_SPTPS, buf);
 	}
-
-	return true;
 }
 
 bool receive_sptps_record(void *handle, uint8_t type, const void *data, uint16_t len) {
@@ -498,11 +148,8 @@ bool receive_sptps_record(void *handle, uint8_t type, const void *data, uint16_t
 	vpn_packet_t inpkt;
 
 	if(type == PKT_PROBE) {
-		inpkt.len = len;
-		inpkt.probe = true;
-		memcpy(inpkt.data, data, len);
-		mtu_probe_h(mesh, from, &inpkt, len);
-		return true;
+		/* We shouldn't receive any UDP probe packets. */
+		return false;
 	} else {
 		inpkt.probe = false;
 	}
@@ -549,89 +196,3 @@ void send_packet(meshlink_handle_t *mesh, node_t *n, vpn_packet_t *packet) {
 	send_sptps_packet(mesh, n, packet);
 	return;
 }
-
-static node_t *try_harder(meshlink_handle_t *mesh, const sockaddr_t *from, const vpn_packet_t *pkt) {
-	node_t *n = NULL;
-	bool hard = false;
-
-	for splay_each(edge_t, e, mesh->edges) {
-		if(!e->to->status.reachable || e->to == mesh->self) {
-			continue;
-		}
-
-		if(sockaddrcmp_noport(from, &e->address)) {
-			if(mesh->last_hard_try == mesh->loop.now.tv_sec) {
-				continue;
-			}
-
-			hard = true;
-		}
-
-		if(!try_mac(mesh, e->to, pkt)) {
-			continue;
-		}
-
-		n = e->to;
-		break;
-	}
-
-	if(hard) {
-		mesh->last_hard_try = mesh->loop.now.tv_sec;
-	}
-
-	return n;
-}
-
-void handle_incoming_vpn_data(event_loop_t *loop, void *data, int flags) {
-	(void)flags;
-	meshlink_handle_t *mesh = loop->data;
-	listen_socket_t *ls = data;
-	vpn_packet_t pkt;
-	char *hostname;
-	sockaddr_t from;
-	socklen_t fromlen = sizeof(from);
-	node_t *n;
-	int len;
-
-	memset(&from, 0, sizeof(from));
-
-	len = recvfrom(ls->udp.fd, pkt.data, MAXSIZE, 0, &from.sa, &fromlen);
-
-	if(len <= 0 || len > MAXSIZE) {
-		if(!sockwouldblock(sockerrno)) {
-			logger(mesh, MESHLINK_ERROR, "Receiving packet failed: %s", sockstrerror(sockerrno));
-		}
-
-		return;
-	}
-
-	pkt.len = len;
-
-	sockaddrunmap(&from); /* Some braindead IPv6 implementations do stupid things. */
-
-	n = lookup_node_udp(mesh, &from);
-
-	if(!n) {
-		n = try_harder(mesh, &from, &pkt);
-
-		if(n) {
-			update_node_udp(mesh, n, &from);
-		} else if(mesh->log_level <= MESHLINK_WARNING) {
-			hostname = sockaddr2hostname(&from);
-			logger(mesh, MESHLINK_WARNING, "Received UDP packet from unknown source %s", hostname);
-			free(hostname);
-			return;
-		} else {
-			return;
-		}
-	}
-
-	if(n->status.blacklisted) {
-		logger(mesh, MESHLINK_WARNING, "Dropping packet from blacklisted node %s", n->name);
-		return;
-	}
-
-	n->sock = ls - mesh->listen_socket;
-
-	receive_udppacket(mesh, n, &pkt);
-}
diff --git a/src/net_setup.c b/src/net_setup.c
index a7a538f..7fd760b 100644
--- a/src/net_setup.c
+++ b/src/net_setup.c
@@ -22,7 +22,6 @@
 #include "conf.h"
 #include "connection.h"
 #include "ecdsa.h"
-#include "graph.h"
 #include "logger.h"
 #include "meshlink_internal.h"
 #include "net.h"
@@ -121,16 +120,8 @@ 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;
-	}
+	packmsg_skip_element(&in); // last_reachable
+	packmsg_skip_element(&in); // last_unreachable
 
 	config_free(&config);
 	return true;
@@ -212,8 +203,8 @@ 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);
+	packmsg_skip_element(&in); // last_reachable
+	packmsg_skip_element(&in); // last_unreachable
 
 	return packmsg_done(&in);
 }
@@ -271,8 +262,8 @@ bool node_write_config(meshlink_handle_t *mesh, node_t *n, bool new_key) {
 		packmsg_add_sockaddr(&out, &n->recent[i]);
 	}
 
-	packmsg_add_int64(&out, n->last_reachable);
-	packmsg_add_int64(&out, n->last_unreachable);
+	packmsg_add_int64(&out, 0); // last_reachable
+	packmsg_add_int64(&out, 0); // last_unreachable
 
 	if(!packmsg_output_ok(&out)) {
 		meshlink_errno = MESHLINK_EINTERNAL;
@@ -394,79 +385,6 @@ int setup_tcp_listen_socket(meshlink_handle_t *mesh, const struct addrinfo *aip)
 	return nfd;
 }
 
-int setup_udp_listen_socket(meshlink_handle_t *mesh, const struct addrinfo *aip) {
-	int nfd = socket(aip->ai_family, SOCK_DGRAM, IPPROTO_UDP);
-
-	if(nfd == -1) {
-		return -1;
-	}
-
-#ifdef FD_CLOEXEC
-	fcntl(nfd, F_SETFD, FD_CLOEXEC);
-#endif
-
-#ifdef O_NONBLOCK
-	int flags = fcntl(nfd, F_GETFL);
-
-	if(fcntl(nfd, F_SETFL, flags | O_NONBLOCK) < 0) {
-		closesocket(nfd);
-		logger(mesh, MESHLINK_ERROR, "System call `%s' failed: %s", "fcntl", strerror(errno));
-		return -1;
-	}
-
-#elif defined(WIN32)
-	unsigned long arg = 1;
-
-	if(ioctlsocket(nfd, FIONBIO, &arg) != 0) {
-		closesocket(nfd);
-		logger(mesh, MESHLINK_ERROR, "Call to `%s' failed: %s", "ioctlsocket", sockstrerror(sockerrno));
-		return -1;
-	}
-
-#endif
-
-	int option = 1;
-	setsockopt(nfd, SOL_SOCKET, SO_REUSEADDR, (void *)&option, sizeof(option));
-	setsockopt(nfd, SOL_SOCKET, SO_BROADCAST, (void *)&option, sizeof(option));
-
-#if defined(IPV6_V6ONLY)
-
-	if(aip->ai_family == AF_INET6) {
-		setsockopt(nfd, IPPROTO_IPV6, IPV6_V6ONLY, (void *)&option, sizeof(option));
-	}
-
-#endif
-
-#if defined(IP_DONTFRAG) && !defined(IP_DONTFRAGMENT)
-#define IP_DONTFRAGMENT IP_DONTFRAG
-#endif
-
-#if defined(IP_MTU_DISCOVER) && defined(IP_PMTUDISC_DO)
-	option = IP_PMTUDISC_DO;
-	setsockopt(nfd, IPPROTO_IP, IP_MTU_DISCOVER, (void *)&option, sizeof(option));
-#elif defined(IP_DONTFRAGMENT)
-	option = 1;
-	setsockopt(nfd, IPPROTO_IP, IP_DONTFRAGMENT, (void *)&option, sizeof(option));
-#endif
-
-	if(aip->ai_family == AF_INET6) {
-#if defined(IPV6_MTU_DISCOVER) && defined(IPV6_PMTUDISC_DO)
-		option = IPV6_PMTUDISC_DO;
-		setsockopt(nfd, IPPROTO_IPV6, IPV6_MTU_DISCOVER, (void *)&option, sizeof(option));
-#elif defined(IPV6_DONTFRAG)
-		option = 1;
-		setsockopt(nfd, IPPROTO_IPV6, IPV6_DONTFRAG, (void *)&option, sizeof(option));
-#endif
-	}
-
-	if(bind(nfd, aip->ai_addr, aip->ai_addrlen)) {
-		closesocket(nfd);
-		return -1;
-	}
-
-	return nfd;
-}
-
 /*
   Add listening sockets.
 */
@@ -523,18 +441,7 @@ static bool add_listen_sockets(meshlink_handle_t *mesh) {
 			}
 		}
 
-		/* If TCP worked, then we require that UDP works as well. */
-
-		int udp_fd = setup_udp_listen_socket(mesh, aip);
-
-		if(udp_fd == -1) {
-			closesocket(tcp_fd);
-			success = false;
-			break;
-		}
-
 		io_add(&mesh->loop, &mesh->listen_socket[mesh->listen_sockets].tcp, handle_new_meta_connection, &mesh->listen_socket[mesh->listen_sockets], tcp_fd, IO_READ);
-		io_add(&mesh->loop, &mesh->listen_socket[mesh->listen_sockets].udp, handle_incoming_vpn_data, &mesh->listen_socket[mesh->listen_sockets], udp_fd, IO_READ);
 
 		if(mesh->log_level <= MESHLINK_INFO) {
 			char *hostname = sockaddr2hostname((sockaddr_t *) aip->ai_addr);
@@ -562,9 +469,7 @@ static bool add_listen_sockets(meshlink_handle_t *mesh) {
 	if(!success) {
 		for(int i = 0; i < mesh->listen_sockets; i++) {
 			io_del(&mesh->loop, &mesh->listen_socket[i].tcp);
-			io_del(&mesh->loop, &mesh->listen_socket[i].udp);
 			closesocket(mesh->listen_socket[i].tcp.fd);
-			closesocket(mesh->listen_socket[i].udp.fd);
 		}
 
 		mesh->listen_sockets = 0;
@@ -626,7 +531,6 @@ bool setup_network(meshlink_handle_t *mesh) {
 	init_connections(mesh);
 	init_submeshes(mesh);
 	init_nodes(mesh);
-	init_edges(mesh);
 	init_requests(mesh);
 
 	if(!setup_myself(mesh)) {
@@ -651,13 +555,10 @@ void close_network_connections(meshlink_handle_t *mesh) {
 
 	for(int i = 0; i < mesh->listen_sockets; i++) {
 		io_del(&mesh->loop, &mesh->listen_socket[i].tcp);
-		io_del(&mesh->loop, &mesh->listen_socket[i].udp);
 		closesocket(mesh->listen_socket[i].tcp.fd);
-		closesocket(mesh->listen_socket[i].udp.fd);
 	}
 
 	exit_requests(mesh);
-	exit_edges(mesh);
 	exit_nodes(mesh);
 	exit_submeshes(mesh);
 	exit_connections(mesh);
diff --git a/src/net_socket.c b/src/net_socket.c
index 4989b6c..29e3725 100644
--- a/src/net_socket.c
+++ b/src/net_socket.c
@@ -174,44 +174,6 @@ static void handle_meta_io(event_loop_t *loop, void *data, int flags) {
 	}
 }
 
-// Find edges pointing to this node, and use them to build a list of unique, known addresses.
-static struct addrinfo *get_known_addresses(node_t *n) {
-	struct addrinfo *ai = NULL;
-
-	for splay_each(edge_t, e, n->edge_tree) {
-		if(!e->reverse) {
-			continue;
-		}
-
-		bool found = false;
-
-		for(struct addrinfo *aip = ai; aip; aip = aip->ai_next) {
-			if(!sockaddrcmp(&e->reverse->address, (sockaddr_t *)aip->ai_addr)) {
-				found = true;
-				break;
-			}
-		}
-
-		if(found) {
-			continue;
-		}
-
-		// Create a new struct addrinfo, and put it at the head of the list.
-		struct addrinfo *nai = xzalloc(sizeof(*nai) + SALEN(e->reverse->address.sa));
-		nai->ai_next = ai;
-		ai = nai;
-
-		ai->ai_family = e->reverse->address.sa.sa_family;
-		ai->ai_socktype = SOCK_STREAM;
-		ai->ai_protocol = IPPROTO_TCP;
-		ai->ai_addrlen = SALEN(e->reverse->address.sa);
-		ai->ai_addr = (struct sockaddr *)(nai + 1);
-		memcpy(ai->ai_addr, &e->reverse->address, ai->ai_addrlen);
-	}
-
-	return ai;
-}
-
 // Build a list of recently seen addresses.
 static struct addrinfo *get_recent_addresses(node_t *n) {
 	struct addrinfo *ai = NULL;
@@ -339,24 +301,6 @@ static bool get_next_outgoing_address(meshlink_handle_t *mesh, outgoing_t *outgo
 			return true;
 		}
 
-		free_known_addresses(outgoing->ai);
-		outgoing->ai = NULL;
-		outgoing->aip = NULL;
-		outgoing->state = OUTGOING_KNOWN;
-	}
-
-	if(outgoing->state == OUTGOING_KNOWN) {
-		if(!outgoing->aip) {
-			outgoing->ai = get_known_addresses(outgoing->node);
-			outgoing->aip = outgoing->ai;
-		} else {
-			outgoing->aip = outgoing->aip->ai_next;
-		}
-
-		if(outgoing->aip) {
-			return true;
-		}
-
 		free_known_addresses(outgoing->ai);
 		outgoing->ai = NULL;
 		outgoing->aip = NULL;
diff --git a/src/node.c b/src/node.c
index b8caed6..160bcb3 100644
--- a/src/node.c
+++ b/src/node.c
@@ -35,26 +35,19 @@ static int node_compare(const node_t *a, const node_t *b) {
 
 void init_nodes(meshlink_handle_t *mesh) {
 	mesh->nodes = splay_alloc_tree((splay_compare_t) node_compare, (splay_action_t) free_node);
-	mesh->node_udp_cache = hash_alloc(0x100, sizeof(sockaddr_t));
 }
 
 void exit_nodes(meshlink_handle_t *mesh) {
-	if(mesh->node_udp_cache) {
-		hash_free(mesh->node_udp_cache);
-	}
-
 	if(mesh->nodes) {
 		splay_delete_tree(mesh->nodes);
 	}
 
-	mesh->node_udp_cache = NULL;
 	mesh->nodes = NULL;
 }
 
 node_t *new_node(void) {
 	node_t *n = xzalloc(sizeof(*n));
 
-	n->edge_tree = new_edge_tree();
 	n->mtu = MTU;
 	n->maxmtu = MTU;
 	n->devclass = DEV_CLASS_UNKNOWN;
@@ -67,10 +60,6 @@ void free_node(node_t *n) {
 
 	utcp_exit(n->utcp);
 
-	if(n->edge_tree) {
-		free_edge_tree(n->edge_tree);
-	}
-
 	sockaddrfree(&n->address);
 
 	ecdsa_free(n->ecdsa);
@@ -93,11 +82,6 @@ void node_add(meshlink_handle_t *mesh, node_t *n) {
 
 void node_del(meshlink_handle_t *mesh, node_t *n) {
 	timeout_del(&mesh->loop, &n->mtutimeout);
-
-	for splay_each(edge_t, e, n->edge_tree) {
-		edge_del(mesh, e);
-	}
-
 	splay_delete(mesh->nodes, n);
 }
 
@@ -110,41 +94,6 @@ node_t *lookup_node(meshlink_handle_t *mesh, const char *name) {
 	return result;
 }
 
-node_t *lookup_node_udp(meshlink_handle_t *mesh, const sockaddr_t *sa) {
-	return hash_search(mesh->node_udp_cache, sa);
-}
-
-void update_node_udp(meshlink_handle_t *mesh, node_t *n, const sockaddr_t *sa) {
-	if(n == mesh->self) {
-		logger(mesh, MESHLINK_WARNING, "Trying to update UDP address of mesh->self!");
-		return;
-	}
-
-	hash_insert(mesh->node_udp_cache, &n->address, NULL);
-
-	if(sa) {
-		n->address = *sa;
-		n->sock = 0;
-
-		for(int i = 0; i < mesh->listen_sockets; i++) {
-			if(mesh->listen_socket[i].sa.sa.sa_family == sa->sa.sa_family) {
-				n->sock = i;
-				break;
-			}
-		}
-
-		hash_insert(mesh->node_udp_cache, sa, n);
-
-		node_add_recent_address(mesh, n, sa);
-
-		if(mesh->log_level <= MESHLINK_DEBUG) {
-			char *hostname = sockaddr2hostname(&n->address);
-			logger(mesh, MESHLINK_DEBUG, "UDP address of %s set to %s", n->name, hostname);
-			free(hostname);
-		}
-	}
-}
-
 bool node_add_recent_address(meshlink_handle_t *mesh, node_t *n, const sockaddr_t *sa) {
 	(void)mesh;
 	bool found = false;
diff --git a/src/node.h b/src/node.h
index 63f3c2c..26d868d 100644
--- a/src/node.h
+++ b/src/node.h
@@ -88,15 +88,7 @@ typedef struct node_t {
 	sockaddr_t recent[MAX_RECENT];          /* Recently seen addresses */
 	sockaddr_t catta_address;               /* Latest address seen by Catta */
 
-	// 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 */
-
-	struct splay_tree_t *edge_tree;         /* Edges with this node as one of the endpoints */
 } node_t;
 
 void init_nodes(struct meshlink_handle *mesh);
@@ -106,8 +98,6 @@ void free_node(node_t *n);
 void node_add(struct meshlink_handle *mesh, node_t *n);
 void node_del(struct meshlink_handle *mesh, node_t *n);
 node_t *lookup_node(struct meshlink_handle *mesh, const char *name) __attribute__((__warn_unused_result__));
-node_t *lookup_node_udp(struct meshlink_handle *mesh, const sockaddr_t *sa) __attribute__((__warn_unused_result__));
-void update_node_udp(struct meshlink_handle *mesh, node_t *n, const sockaddr_t *sa);
 bool node_add_recent_address(struct meshlink_handle *mesh, node_t *n, const sockaddr_t *addr);
 
 #endif
diff --git a/src/protocol.h b/src/protocol.h
index fb1e109..5882513 100644
--- a/src/protocol.h
+++ b/src/protocol.h
@@ -70,7 +70,6 @@ typedef struct past_request_t {
 #define MAX_STRING_SIZE 2049
 #define MAX_STRING "%2048s"
 
-#include "edge.h"
 #include "net.h"
 #include "node.h"
 
@@ -92,8 +91,6 @@ bool send_ack(struct meshlink_handle *mesh, struct connection_t *);
 bool send_error(struct meshlink_handle *mesh, struct connection_t *, request_error_t, const char *);
 bool send_ping(struct meshlink_handle *mesh, struct connection_t *);
 bool send_pong(struct meshlink_handle *mesh, struct connection_t *);
-bool send_add_edge(struct meshlink_handle *mesh, struct connection_t *, const struct edge_t *, int contradictions);
-bool send_del_edge(struct meshlink_handle *mesh, struct connection_t *, const struct edge_t *, int contradictions);
 bool send_req_key(struct meshlink_handle *mesh, struct node_t *);
 bool send_canonical_address(struct meshlink_handle *mesh, struct node_t *);
 
diff --git a/src/protocol_auth.c b/src/protocol_auth.c
index 943040e..e292d58 100644
--- a/src/protocol_auth.c
+++ b/src/protocol_auth.c
@@ -23,8 +23,6 @@
 #include "connection.h"
 #include "devtools.h"
 #include "ecdsa.h"
-#include "edge.h"
-#include "graph.h"
 #include "logger.h"
 #include "meshlink_internal.h"
 #include "meta.h"
@@ -148,16 +146,6 @@ bool send_ack(meshlink_handle_t *mesh, connection_t *c) {
 	return send_request(mesh, c, NULL, "%d %s %d %x", ACK, mesh->myport, mesh->devclass, OPTION_PMTU_DISCOVERY | (PROT_MINOR << 24));
 }
 
-static void send_everything(meshlink_handle_t *mesh, connection_t *c) {
-	/* Send all known subnets and edges */
-
-	for splay_each(node_t, n, mesh->nodes) {
-		for inner_splay_each(edge_t, e, n->edge_tree) {
-			send_add_edge(mesh, c, e, 0);
-		}
-	}
-}
-
 bool ack_h(meshlink_handle_t *mesh, connection_t *c, const char *request) {
 	assert(request);
 	assert(*request);
@@ -200,10 +188,6 @@ bool ack_h(meshlink_handle_t *mesh, connection_t *c, const char *request) {
 				n->connection->outgoing = NULL;
 			}
 
-			/* Remove the edge before terminating the connection, to prevent a graph update. */
-			edge_del(mesh, n->connection->edge);
-			n->connection->edge = NULL;
-
 			terminate_connection(mesh, n->connection, false);
 		}
 	}
@@ -248,31 +232,7 @@ bool ack_h(meshlink_handle_t *mesh, connection_t *c, const char *request) {
 		}
 	}
 
-	/* Send him everything we know */
-
-	send_everything(mesh, c);
-
-	/* Create an edge_t for this connection */
-
-	assert(devclass >= 0 && devclass < DEV_CLASS_COUNT);
-
-	c->edge = new_edge();
-	c->edge->from = mesh->self;
-	c->edge->to = n;
-	sockaddrcpy_setport(&c->edge->address, &c->address, atoi(hisport));
-	c->edge->weight = mesh->dev_class_traits[devclass].edge_weight;
-	c->edge->connection = c;
-
-	node_add_recent_address(mesh, n, &c->address);
-	edge_add(mesh, c->edge);
-
-	/* Notify everyone of the new edge */
-
-	send_add_edge(mesh, mesh->everyone, c->edge, 0);
-
-	/* Run MST and SSSP algorithms */
-
-	graph(mesh);
+	/* TODO: Create an edge_t for this connection, send it */
 
 	/* Request a session key to jump start UDP traffic */
 
diff --git a/src/protocol_edge.c b/src/protocol_edge.c
index aa1a648..16ebfd5 100644
--- a/src/protocol_edge.c
+++ b/src/protocol_edge.c
@@ -21,8 +21,6 @@
 
 #include "conf.h"
 #include "connection.h"
-#include "edge.h"
-#include "graph.h"
 #include "logger.h"
 #include "meshlink_internal.h"
 #include "meta.h"
@@ -34,6 +32,7 @@
 #include "xalloc.h"
 #include "submesh.h"
 
+#if 0
 bool send_add_edge(meshlink_handle_t *mesh, connection_t *c, const edge_t *e, int contradictions) {
 	bool x;
 	char *address, *port;
@@ -82,11 +81,15 @@ bool send_add_edge(meshlink_handle_t *mesh, connection_t *c, const edge_t *e, in
 
 	return x;
 }
+#endif
 
 bool add_edge_h(meshlink_handle_t *mesh, connection_t *c, const char *request) {
 	assert(request);
 	assert(*request);
 
+	(void)mesh;
+	(void)c;
+#if 0
 	edge_t *e;
 	node_t *from, *to;
 	char from_name[MAX_STRING_SIZE];
@@ -255,10 +258,13 @@ bool add_edge_h(meshlink_handle_t *mesh, connection_t *c, const char *request) {
 	/* Tell the rest about the new edge */
 
 	forward_request(mesh, c, s, request);
+#endif
 
+	/* TODO: Check if this is an edge we would own */
 	return true;
 }
 
+#if 0
 bool send_del_edge(meshlink_handle_t *mesh, connection_t *c, const edge_t *e, int contradictions) {
 	submesh_t *s = NULL;
 
@@ -286,11 +292,15 @@ bool send_del_edge(meshlink_handle_t *mesh, connection_t *c, const edge_t *e, in
 	return send_request(mesh, c, s, "%d %x %s %s %d %x", DEL_EDGE, prng(mesh, UINT_MAX),
 	                    e->from->name, e->to->name, contradictions, e->session_id);
 }
+#endif
 
 bool del_edge_h(meshlink_handle_t *mesh, connection_t *c, const char *request) {
 	assert(request);
 	assert(*request);
 
+	(void)mesh;
+	(void)c;
+#if 0
 	edge_t *e;
 	char from_name[MAX_STRING_SIZE];
 	char to_name[MAX_STRING_SIZE];
@@ -380,5 +390,8 @@ bool del_edge_h(meshlink_handle_t *mesh, connection_t *c, const char *request) {
 		}
 	}
 
+#endif
+
+	/* TODO: Check if this is an edge we would own. */
 	return true;
 }
diff --git a/src/protocol_key.c b/src/protocol_key.c
index 25dbc55..bd27b01 100644
--- a/src/protocol_key.c
+++ b/src/protocol_key.c
@@ -411,17 +411,6 @@ bool ans_key_h(meshlink_handle_t *mesh, connection_t *c, const char *request) {
 			return false;
 		}
 
-		/* Append the known UDP address of the from node, if we have a confirmed one */
-		if(!*address && from->status.udp_confirmed && from->address.sa.sa_family != AF_UNSPEC) {
-			char *reflexive_address, *reflexive_port;
-			logger(mesh, MESHLINK_DEBUG, "Appending reflexive UDP address to ANS_KEY from %s to %s", from->name, to->name);
-			sockaddr2str(&from->address, &reflexive_address, &reflexive_port);
-			send_request(mesh, to->nexthop->connection, NULL, "%s %s %s", request, reflexive_address, reflexive_port);
-			free(reflexive_address);
-			free(reflexive_port);
-			return true;
-		}
-
 		return send_request(mesh, to->nexthop->connection, NULL, "%s", request);
 	}
 
@@ -429,29 +418,7 @@ bool ans_key_h(meshlink_handle_t *mesh, connection_t *c, const char *request) {
 
 	if(from == mesh->self) {
 		if(*key == '.' && *address && *port) {
-			logger(mesh, MESHLINK_DEBUG, "Learned our own reflexive UDP address from %s: %s port %s", c->name, address, port);
-
-			/* Inform all other nodes we want to communicate with and which are reachable via this connection */
-			for splay_each(node_t, n, mesh->nodes) {
-				if(n->nexthop != c->node) {
-					continue;
-				}
-
-				if(n->status.udp_confirmed) {
-					continue;
-				}
-
-				if(!n->status.waitingforkey && !n->status.validkey) {
-					continue;
-				}
-
-				if(!n->nexthop->connection) {
-					continue;
-				}
-
-				logger(mesh, MESHLINK_DEBUG, "Forwarding our own reflexive UDP address to %s", n->name);
-				send_request(mesh, c, NULL, "%d %s %s . -1 -1 -1 0 %s %s", ANS_KEY, mesh->self->name, n->name, address, port);
-			}
+			/* Ignore reflexive UDP address */
 		} else {
 			logger(mesh, MESHLINK_WARNING, "Got %s from %s from %s to %s",
 			       "ANS_KEY", c->name, from_name, to_name);
@@ -482,12 +449,8 @@ bool ans_key_h(meshlink_handle_t *mesh, connection_t *c, const char *request) {
 
 	if(from->status.validkey) {
 		if(*address && *port) {
-			logger(mesh, MESHLINK_DEBUG, "Using reflexive UDP address from %s: %s port %s", from->name, address, port);
-			sockaddr_t sa = str2sockaddr(address, port);
-			update_node_udp(mesh, from, &sa);
+			/* Ignore reflexive UDP address */
 		}
-
-		send_mtu_probe(mesh, from);
 	}
 
 	return true;
diff --git a/test/basic.c b/test/basic.c
index e848efd..1fd39ec 100644
--- a/test/basic.c
+++ b/test/basic.c
@@ -43,32 +43,12 @@ int main(void) {
 	assert(self);
 	assert(!strcmp(self->name, "foo"));
 
-	// Check that we are not reachable.
-
-	time_t last_reachable;
-	time_t last_unreachable;
-	assert(!meshlink_get_node_reachability(mesh, self, &last_reachable, &last_unreachable));
-	assert(!last_reachable);
-	assert(!last_unreachable);
-
 	// Start and stop the mesh.
 
 	assert(meshlink_start(mesh));
 
-	// Check that we are now reachable
-
-	assert(meshlink_get_node_reachability(mesh, self, &last_reachable, &last_unreachable));
-	assert(last_reachable);
-	assert(!last_unreachable);
-
 	meshlink_stop(mesh);
 
-	// Check that we are no longer reachable.
-
-	assert(!meshlink_get_node_reachability(mesh, self, &last_reachable, &last_unreachable));
-	assert(last_reachable);
-	assert(last_unreachable);
-
 	// Make sure we can start and stop the mesh again.
 
 	assert(meshlink_start(mesh));
@@ -92,12 +72,6 @@ int main(void) {
 	assert(!strcmp(mesh->name, "foo"));
 	assert(!strcmp(self->name, "foo"));
 
-	// Check that we remembered we were reachable
-
-	assert(!meshlink_get_node_reachability(mesh, self, &last_reachable, &last_unreachable));
-	assert(last_reachable);
-	assert(last_unreachable);
-
 	// Check that the name is ignored now, and that we still are "foo".
 
 	assert(!meshlink_get_node(mesh, "bar"));
diff --git a/test/basicpp.cpp b/test/basicpp.cpp
index bed6946..06e732c 100644
--- a/test/basicpp.cpp
+++ b/test/basicpp.cpp
@@ -24,10 +24,6 @@ int main(void) {
 		assert(self);
 		assert(!strcmp(self->name, "foo"));
 
-		// Disable local discovery.
-
-		mesh.enable_discovery(false);
-
 		// Start and stop the mesh.
 
 		assert(mesh.start());
@@ -55,8 +51,6 @@ int main(void) {
 
 		// Start and stop the mesh.
 
-		mesh.enable_discovery(false);
-
 		assert(mesh.start());
 		mesh.stop();
 	}
diff --git a/test/channels-aio-fd.c b/test/channels-aio-fd.c
index dbfdde6..0917fa8 100644
--- a/test/channels-aio-fd.c
+++ b/test/channels-aio-fd.c
@@ -102,9 +102,6 @@ int main(void) {
 
 	mesh_b->priv = in_infos;
 
-	meshlink_enable_discovery(mesh_a, false);
-	meshlink_enable_discovery(mesh_b, false);
-
 	// Set the callbacks.
 
 	meshlink_set_channel_accept_cb(mesh_b, accept_cb);
diff --git a/test/channels-fork.c b/test/channels-fork.c
index a1089b5..329d131 100644
--- a/test/channels-fork.c
+++ b/test/channels-fork.c
@@ -84,8 +84,6 @@ static int main1(int rfd, int wfd) {
 	meshlink_handle_t *mesh = meshlink_open("channels_fork_conf.1", "foo", "channels-fork", DEV_CLASS_BACKBONE);
 	assert(mesh);
 
-	meshlink_enable_discovery(mesh, false);
-
 	assert(meshlink_set_canonical_address(mesh, meshlink_get_self(mesh), "localhost", NULL));
 
 	char *data = meshlink_export(mesh);
@@ -138,8 +136,6 @@ static int main2(int rfd, int wfd) {
 	meshlink_handle_t *mesh = meshlink_open("channels_fork_conf.2", "bar", "channels-fork", DEV_CLASS_BACKBONE);
 	assert(mesh);
 
-	meshlink_enable_discovery(mesh, false);
-
 	assert(meshlink_set_canonical_address(mesh, meshlink_get_self(mesh), "localhost", NULL));
 
 	char *data = meshlink_export(mesh);
diff --git a/test/channels-udp.c b/test/channels-udp.c
index c268727..820eb54 100644
--- a/test/channels-udp.c
+++ b/test/channels-udp.c
@@ -97,7 +97,6 @@ int main(void) {
 	assert(meshlink_destroy("channels_udp_conf.0"));
 	meshlink_handle_t *server = meshlink_open("channels_udp_conf.0", "server", "channels-udp", DEV_CLASS_BACKBONE);
 	assert(server);
-	meshlink_enable_discovery(server, false);
 	server->priv = channels;
 	meshlink_set_channel_accept_cb(server, accept_cb);
 	assert(meshlink_start(server));
@@ -110,7 +109,6 @@ int main(void) {
 		clients[i].mesh = meshlink_open(dir, names[i], "channels-udp", DEV_CLASS_STATIONARY);
 		assert(clients[i].mesh);
 		clients[i].mesh->priv = &clients[i];
-		meshlink_enable_discovery(clients[i].mesh, false);
 		link_meshlink_pair(server, clients[i].mesh);
 		meshlink_set_node_status_cb(clients[i].mesh, status_cb);
 		assert(meshlink_start(clients[i].mesh));
diff --git a/test/duplicate.c b/test/duplicate.c
index 6e7607c..0dba67f 100644
--- a/test/duplicate.c
+++ b/test/duplicate.c
@@ -36,7 +36,6 @@ int main(void) {
 		assert(mesh[i]);
 
 		assert(meshlink_set_canonical_address(mesh[i], meshlink_get_self(mesh[i]), "localhost", NULL));
-		meshlink_enable_discovery(mesh[i], false);
 
 		meshlink_set_node_duplicate_cb(mesh[i], handle_duplicate);
 	}
diff --git a/test/get-all-nodes.c b/test/get-all-nodes.c
index 2d68c42..1945058 100644
--- a/test/get-all-nodes.c
+++ b/test/get-all-nodes.c
@@ -53,19 +53,9 @@ int main(void) {
 	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_set_canonical_address(mesh[i], meshlink_get_self(mesh[i]), "localhost", NULL));
 		char *data = meshlink_export(mesh[i]);
 		assert(data);
@@ -93,37 +83,11 @@ int main(void) {
 	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);
@@ -133,46 +97,15 @@ int main(void) {
 	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", "get-all_nodes", DEV_CLASS_BACKBONE);
 	assert(mesh[0]);
 
@@ -186,32 +119,6 @@ int main(void) {
 	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++) {
diff --git a/test/import-export.c b/test/import-export.c
index 08343d0..3382fee 100644
--- a/test/import-export.c
+++ b/test/import-export.c
@@ -48,11 +48,6 @@ int main(void) {
 	meshlink_handle_t *mesh2 = meshlink_open("import_export_conf.2", "bar", "import-export", DEV_CLASS_BACKBONE);
 	assert(mesh2);
 
-	// Disable local discovery
-
-	meshlink_enable_discovery(mesh1, false);
-	meshlink_enable_discovery(mesh2, false);
-
 	// Import and export both side's data
 
 	assert(meshlink_set_canonical_address(mesh1, meshlink_get_self(mesh1), "localhost", NULL));
diff --git a/test/invite-join.c b/test/invite-join.c
index 9ae831e..e84ddd8 100644
--- a/test/invite-join.c
+++ b/test/invite-join.c
@@ -72,12 +72,6 @@ int main(void) {
 	meshlink_handle_t *mesh3 = meshlink_open("invite_join_conf.3", "quux", "invite-join", DEV_CLASS_BACKBONE);
 	assert(mesh3);
 
-	// Disable local discovery.
-
-	meshlink_enable_discovery(mesh1, false);
-	meshlink_enable_discovery(mesh2, false);
-	meshlink_enable_discovery(mesh3, false);
-
 	// Have the first instance generate invitations.
 
 	meshlink_set_node_status_cb(mesh1, status_cb);
diff --git a/test/netns_utils.c b/test/netns_utils.c
index 50f7be8..5e0ed21 100644
--- a/test/netns_utils.c
+++ b/test/netns_utils.c
@@ -55,8 +55,6 @@ static void create_peers(peer_config_t *peers, int npeers, const char *prefix) {
 		assert(peers[i].mesh);
 		free(params);
 		free(conf_path);
-
-		meshlink_enable_discovery(peers[i].mesh, false);
 	}
 }
 
diff --git a/test/storage-policy.c b/test/storage-policy.c
index 7f757a3..6885406 100644
--- a/test/storage-policy.c
+++ b/test/storage-policy.c
@@ -25,8 +25,6 @@ int main(void) {
 	mesh2 = meshlink_open("storage-policy_conf.2", "bar", "storage-policy", DEV_CLASS_BACKBONE);
 	assert(mesh1);
 	assert(mesh2);
-	meshlink_enable_discovery(mesh1, false);
-	meshlink_enable_discovery(mesh2, false);
 	meshlink_set_storage_policy(mesh1, MESHLINK_STORAGE_DISABLED);
 	meshlink_set_storage_policy(mesh2, MESHLINK_STORAGE_DISABLED);
 
@@ -56,8 +54,6 @@ int main(void) {
 	mesh2 = meshlink_open("storage-policy_conf.2", "bar", "storage-policy", DEV_CLASS_BACKBONE);
 	assert(mesh1);
 	assert(mesh2);
-	meshlink_enable_discovery(mesh1, false);
-	meshlink_enable_discovery(mesh2, false);
 	meshlink_set_storage_policy(mesh1, MESHLINK_STORAGE_KEYS_ONLY);
 	meshlink_set_storage_policy(mesh2, MESHLINK_STORAGE_KEYS_ONLY);
 
@@ -82,8 +78,6 @@ int main(void) {
 	mesh2 = meshlink_open("storage-policy_conf.2", "bar", "storage-policy", DEV_CLASS_BACKBONE);
 	assert(mesh1);
 	assert(mesh2);
-	meshlink_enable_discovery(mesh1, false);
-	meshlink_enable_discovery(mesh2, false);
 	meshlink_set_storage_policy(mesh1, MESHLINK_STORAGE_KEYS_ONLY);
 	meshlink_set_storage_policy(mesh2, MESHLINK_STORAGE_KEYS_ONLY);
 
@@ -115,8 +109,6 @@ int main(void) {
 	mesh2 = meshlink_open("storage-policy_conf.2", "bar", "storage-policy", DEV_CLASS_BACKBONE);
 	assert(mesh1);
 	assert(mesh2);
-	meshlink_enable_discovery(mesh1, false);
-	meshlink_enable_discovery(mesh2, false);
 	meshlink_set_storage_policy(mesh1, MESHLINK_STORAGE_KEYS_ONLY);
 	meshlink_set_storage_policy(mesh2, MESHLINK_STORAGE_KEYS_ONLY);
 
@@ -141,8 +133,6 @@ int main(void) {
 	mesh2 = meshlink_open("storage-policy_conf.2", "bar", "storage-policy", DEV_CLASS_BACKBONE);
 	assert(mesh1);
 	assert(mesh2);
-	meshlink_enable_discovery(mesh1, false);
-	meshlink_enable_discovery(mesh2, false);
 
 	assert(!meshlink_get_node_reachability(mesh1, meshlink_get_node(mesh1, "bar"), &last_reachable, &last_unreachable));
 	assert(last_reachable);
@@ -156,8 +146,6 @@ int main(void) {
 	mesh2 = meshlink_open("storage-policy_conf.2", "bar", "storage-policy", DEV_CLASS_BACKBONE);
 	assert(mesh1);
 	assert(mesh2);
-	meshlink_enable_discovery(mesh1, false);
-	meshlink_enable_discovery(mesh2, false);
 	meshlink_set_storage_policy(mesh1, MESHLINK_STORAGE_KEYS_ONLY);
 	meshlink_set_storage_policy(mesh2, MESHLINK_STORAGE_KEYS_ONLY);
 
diff --git a/test/trio.c b/test/trio.c
index 292a1a5..5b5a612 100644
--- a/test/trio.c
+++ b/test/trio.c
@@ -119,13 +119,6 @@ int main(void) {
 
 	assert(wait_sync_flag(&received, 15));
 
-	// Check that the second and third node have autoconnected to each other
-
-	devtool_edge_t *edges = NULL;
-	size_t nedges = 0;
-	assert_after((edges = devtool_get_all_edges(mesh[1], edges, &nedges), nedges == 3), 15);
-	free(edges);
-
 	// Stop the first node
 
 	meshlink_stop(mesh[0]);
diff --git a/test/trio2.c b/test/trio2.c
index cc1ce87..c59aa64 100644
--- a/test/trio2.c
+++ b/test/trio2.c
@@ -105,13 +105,6 @@ int main(void) {
 	assert(wait_sync_flag(&bar_learned_baz, 5));
 	assert(wait_sync_flag(&baz_learned_bar, 5));
 
-	// Check that the second and third node autoconnect to each other
-
-	devtool_edge_t *edges = NULL;
-	size_t nedges = 0;
-	assert_after((edges = devtool_get_all_edges(mesh[1], edges, &nedges), nedges == 3), 15);
-	free(edges);
-
 	// Stop the nodes nodes
 
 	for(int i = 0; i < 3; i++) {
diff --git a/test/utils.c b/test/utils.c
index 872060b..27582ad 100644
--- a/test/utils.c
+++ b/test/utils.c
@@ -100,9 +100,6 @@ void open_meshlink_pair(meshlink_handle_t **pa, meshlink_handle_t **pb, const ch
 	free(a_name);
 	free(b_name);
 
-	meshlink_enable_discovery(a, false);
-	meshlink_enable_discovery(b, false);
-
 	link_meshlink_pair(a, b);
 
 	*pa = a;
@@ -120,9 +117,6 @@ void open_meshlink_pair_ephemeral(meshlink_handle_t **pa, meshlink_handle_t **pb
 	assert(a);
 	assert(b);
 
-	meshlink_enable_discovery(a, false);
-	meshlink_enable_discovery(b, false);
-
 	link_meshlink_pair(a, b);
 
 	*pa = a;
-- 
2.39.5