diff --git a/ip.h b/ip.h
index b8d4a5b..0742612 100644
--- a/ip.h
+++ b/ip.h
@@ -92,4 +92,13 @@ struct ipv6_opt_hdr {
 
 char *ipv6_l4hdr(const struct pool *p, int idx, size_t offset, uint8_t *proto,
 		 size_t *dlen);
+
+/* IPv6 link-local all-nodes multicast adddress, ff02::1 */
+static const struct in6_addr in6addr_ll_all_nodes = {
+	.s6_addr = {
+		0xff, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
+	},
+};
+
 #endif /* IP_H */
diff --git a/ndp.c b/ndp.c
index 09df8d6..7ee44b2 100644
--- a/ndp.c
+++ b/ndp.c
@@ -372,3 +372,57 @@ int ndp(const struct ctx *c, const struct icmp6hdr *ih,
 
 	return 1;
 }
+
+/* Default interval between unsolicited RAs (seconds) */
+#define DEFAULT_MAX_RTR_ADV_INTERVAL	600	/* RFC 4861, 6.2.1 */
+
+/* Minimum required interval between RAs (seconds) */
+#define MIN_DELAY_BETWEEN_RAS		3	/* RFC 4861, 10 */
+
+static time_t next_ra;
+
+/**
+ * ndp_timer() - Send unsolicited NDP messages if necessary
+ * @c:		Execution context
+ * @now:	Current (monotonic) time
+ */
+void ndp_timer(const struct ctx *c, const struct timespec *now)
+{
+	time_t max_rtr_adv_interval = DEFAULT_MAX_RTR_ADV_INTERVAL;
+	time_t min_rtr_adv_interval, interval;
+
+	if (c->no_ra || now->tv_sec < next_ra)
+		return;
+
+	/* We must advertise before the route's lifetime expires */
+	max_rtr_adv_interval = MIN(max_rtr_adv_interval, RT_LIFETIME - 1);
+
+	/* But we must not go smaller than the minimum delay */
+	max_rtr_adv_interval = MAX(max_rtr_adv_interval, MIN_DELAY_BETWEEN_RAS);
+
+	/* RFC 4861, 6.2.1 */
+	min_rtr_adv_interval = MAX(max_rtr_adv_interval / 3,
+				   MIN_DELAY_BETWEEN_RAS);
+
+	/* As required by RFC 4861, we randomise the interval between
+	 * unsolicited RAs.  This is to prevent multiple routers on a link
+	 * getting synchronised (e.g. after booting a bunch of routers at once)
+	 * and causing flurries of RAs at the same time.
+	 *
+	 * This random doesn't need to be cryptographically strong, so random(3)
+	 * is fine.  Other routers on the link also want to avoid
+	 * synchronisation, and anything malicious has much easier ways to cause
+	 * trouble.
+	 *
+	 * The modulus also makes this not strictly a uniform distribution, but,
+	 * again, it's close enough for our purposes.
+	 */
+	interval = min_rtr_adv_interval +
+		random() % (max_rtr_adv_interval - min_rtr_adv_interval);
+
+	info("NDP: sending unsolicited RA, next in %llds", (long long)interval);
+
+	ndp_ra(c, &in6addr_ll_all_nodes);
+
+	next_ra = now->tv_sec + interval;
+}
diff --git a/ndp.h b/ndp.h
index abe6d02..41c2000 100644
--- a/ndp.h
+++ b/ndp.h
@@ -6,7 +6,10 @@
 #ifndef NDP_H
 #define NDP_H
 
+struct icmp6hdr;
+
 int ndp(const struct ctx *c, const struct icmp6hdr *ih,
 	const struct in6_addr *saddr, const struct pool *p);
+void ndp_timer(const struct ctx *c, const struct timespec *now);
 
 #endif /* NDP_H */
diff --git a/passt.c b/passt.c
index 83b26c5..a51a4e1 100644
--- a/passt.c
+++ b/passt.c
@@ -49,6 +49,7 @@
 #include "arch.h"
 #include "log.h"
 #include "tcp_splice.h"
+#include "ndp.h"
 
 #define EPOLL_EVENTS		8
 
@@ -107,6 +108,8 @@ static void post_handler(struct ctx *c, const struct timespec *now)
 
 	flow_defer_handler(c, now);
 #undef CALL_PROTO_HANDLER
+
+	ndp_timer(c, now);
 }
 
 /**