diff --git a/Makefile.dep b/Makefile.dep
index 4c55e2e275e8de5234096ae957276bdc1e3d158f..e083323e1b0e67d82efda6c7a4107840c9560b60 100644
--- a/Makefile.dep
+++ b/Makefile.dep
@@ -768,6 +768,21 @@ ifneq (,$(filter rdcli_simple,$(USEMODULE)))
   USEMODULE += fmt
 endif
 
+ifneq (,$(filter rdcli_standalone,$(USEMODULE)))
+  USEMODULE += rdcli
+  USEMODULE += xtimer
+endif
+
+ifneq (,$(filter rdcli,$(USEMODULE)))
+  USEMODULE += rdcli_common
+  USEMODULE += core_thread_flags
+  USEMODULE += gcoap
+  USEMODULE += fmt
+  ifneq (,$(filter shell_commands,$(USEMODULE)))
+    USEMODULE += sock_util
+  endif
+endif
+
 ifneq (,$(filter rdcli_common,$(USEMODULE)))
   USEMODULE += fmt
   USEMODULE += gcoap
diff --git a/makefiles/pseudomodules.inc.mk b/makefiles/pseudomodules.inc.mk
index c57bff7d0dda09d8f5dd3226bf75b646eb7231bd..c5e37daaefed1663c5a830e0bec86284fa812465 100644
--- a/makefiles/pseudomodules.inc.mk
+++ b/makefiles/pseudomodules.inc.mk
@@ -65,6 +65,7 @@ PSEUDOMODULES += pktqueue
 PSEUDOMODULES += printf_float
 PSEUDOMODULES += prng
 PSEUDOMODULES += prng_%
+PSEUDOMODULES += rdcli_standalone
 PSEUDOMODULES += rdcli_simple_standalone
 PSEUDOMODULES += saul_adc
 PSEUDOMODULES += saul_default
diff --git a/sys/Makefile b/sys/Makefile
index 77e8832dc65ddbcea6df64ba3955b7a572c163b9..9d63db742c4f4c5108e8b7cff43d02e15120a2c9 100644
--- a/sys/Makefile
+++ b/sys/Makefile
@@ -133,6 +133,10 @@ endif
 ifneq (,$(filter rdcli_simple,$(USEMODULE)))
     DIRS += net/application_layer/rdcli_simple
 endif
+ifneq (,$(filter rdcli,$(USEMODULE)))
+    DIRS += net/application_layer/rdcli
+endif
+
 
 DIRS += $(dir $(wildcard $(addsuffix /Makefile, $(USEMODULE))))
 
diff --git a/sys/include/net/rdcli.h b/sys/include/net/rdcli.h
new file mode 100644
index 0000000000000000000000000000000000000000..f9b2a049f52e246c46637b0fbae174a04169c7c4
--- /dev/null
+++ b/sys/include/net/rdcli.h
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2017-2018 Freie Universität Berlin
+ *
+ * This file is subject to the terms and conditions of the GNU Lesser
+ * General Public License v2.1. See the file LICENSE in the top level
+ * directory for more details.
+ */
+
+/**
+ * @defgroup    net_rdcli CoRE RD Endpoint Library
+ * @ingroup     net
+ * @brief       Library for using RIOT as CoRE Resource Directory endpoint
+ *
+ * This module implements a CoRE Resource Directory endpoint library, that
+ * allows RIOT nodes to register themselves with resource directories.
+ * It implements the standard endpoint functionality as defined in
+ * draft-ietf-core-resource-directory-15.
+ * @see https://tools.ietf.org/html/draft-ietf-core-resource-directory-15
+ *
+ * @note        As the name of this library (`rdcli`) can be misleading in
+ *              context of the RD draft (endpoint vs client), this library
+ *              will most likely undergo a name change in the near future...
+ *
+ * # Design Decisions
+ * - all operations provided by this module are fully synchronous, meaning that
+ *   the functions will block until an operation is successful or will time out
+ * - the implementation limits the client to be registered with a single RD at
+ *   any point in time
+ *
+ * @{
+ *
+ * @file
+ * @brief       CoRE Resource Directory endpoint interface
+ *
+ * @author      Hauke Petersen <hauke.petersen@fu-berlin.de>
+ */
+
+#ifndef NET_RDCLI_H
+#define NET_RDCLI_H
+
+#include "net/sock/udp.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @brief   Return values and error codes used by this module
+ */
+enum {
+    RDCLI_OK        =  0,   /**< everything went as expected */
+    RDCLI_TIMEOUT   = -1,   /**< no response from the network */
+    RDCLI_ERR       = -2,   /**< internal error or invalid reply */
+    RDCLI_NORD      = -3,   /**< not connected to an RD */
+    RDCLI_OVERFLOW  = -4,   /**< internal buffers can not handle input */
+};
+
+/**
+ * @brief   Discover the registration interface resource of a RD
+ *
+ * @param[in] remote    remote endpoint of the target RD
+ * @param[out] regif    the registration interface is written to this buffer
+ * @param[in] maxlen    size of @p regif
+ *
+ * @return  RDCLI_OK on success
+ * @return  RDCLI_TIMEOUT if the discovery request times out
+ * @return  RDCLI_NORD if addressed endpoint is not a RD
+ * @return  RDCLI_ERR on any other internal error
+ */
+int rdcli_discover_regif(const sock_udp_ep_t *remote,
+                         char *regif, size_t maxlen);
+
+/**
+ * @brief   Initiate the node registration by sending an empty push
+ *
+ * - if registration fails (e.g. timeout), we are not associated with any RD
+ *   anymore (even if we have been before we called rdcli_register)
+ *
+ * @note    In case a multicast address is given, the @p regif parameter MUST be
+ *          NULL. The first RD responding to the request will be chosen and all
+ *          replies from other RD servers are ignored.
+ *
+ * @param[in] remote    remote endpoint of the target RD
+ * @param[in] regif     registration interface resource of the RD, it will be
+ *                      discovered automatically when set to NULL
+ *
+ * @return  RDCLI_OK on success
+ * @return  RDCLI_TIMEOUT on registration timeout
+ * @return  RDCLI_NORD if addressed endpoint is not a RD
+ * @return  RDCLI_OVERFLOW if @p regif does not fit into internal buffer
+ * @return  RDCLI_ERR on any other internal error
+ */
+int rdcli_register(const sock_udp_ep_t *remote, const char *regif);
+
+/**
+ * @brief   Update our current entry at the RD
+ *
+ * @return  RDCLI_OK on success
+ * @return  RDCLI_TIMEOUT if the update request times out
+ * @return  RDCLI_ERR on any other internal error
+ */
+int rdcli_update(void);
+
+/**
+ * @brief   Unregister from a given RD server
+ *
+ * @return  RDCLI_OK on success
+ * @return  RDCLI_TIMEOUT if the remove request times out
+ * @return  RDCLI_ERR on any other internal error
+ */
+int rdcli_remove(void);
+
+/**
+ * @brief   Dump the current RD connection status to STDIO (for debugging)
+ */
+void rdcli_dump_status(void);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* NET_RDCLI_H */
+/** @} */
diff --git a/sys/include/net/rdcli_standalone.h b/sys/include/net/rdcli_standalone.h
new file mode 100644
index 0000000000000000000000000000000000000000..a76041b882c0de1a0f57dc1fd50a241618aceb6e
--- /dev/null
+++ b/sys/include/net/rdcli_standalone.h
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2017-2018 Freie Universität Berlin
+ *
+ * This file is subject to the terms and conditions of the GNU Lesser
+ * General Public License v2.1. See the file LICENSE in the top level
+ * directory for more details.
+ */
+
+/**
+ * @defgroup    net_rdcli_standalone CoRE RD Standalone Extension
+ * @ingroup     net_rdcli
+ * @brief       Run CoRE Resource Directory client in standalone configuration
+ *
+ * This sub-module enables the CoRE RD client to manage is registration state
+ * with a server autonomously by periodically running the update procedure. This
+ * is implemented by running a dedicated thread.
+ *
+ * @{
+ *
+ * @file
+ * @brief       CoRE Resource Directory client standalone extension
+ *
+ * @author      Hauke Petersen <hauke.petersen@fu-berlin.de>
+ */
+
+#ifndef NET_RDCLI_STANDALONE_H
+#define NET_RDCLI_STANDALONE_H
+
+#include "net/sock/udp.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * @brief   Possible types of events triggered by the standalone rdcli module
+ */
+typedef enum {
+    RDCLI_REGISTERED,
+    RDCLI_DEREGISTERED,
+    RDCLI_UPDATED,
+} rdcli_standalone_event_t;
+
+/**
+ * @brief   Callback function signature for RD client state synchronization
+ *
+ * The registered callback function is executed in the context of the dedicated
+ * standalone RD client's thread.
+ *
+ * @param[in] t         type of event
+ */
+typedef void(*rdcli_standalone_cb_t)(rdcli_standalone_event_t event);
+
+/**
+ * @brief   Spawn a new thread that takes care of sending periodic updates to an
+ *          active RD entry
+ *
+ * @note    This function must only be called once (typically during system
+ *          initialization)
+ */
+void rdcli_standalone_run(void);
+
+/**
+ * @brief   Register a callback to be notified about RD client state changes
+ *
+ * Only a single callback can be active at any point in time, so setting a new
+ * callback will override the existing one.
+ *
+ * @pre                     @p cb != NULL
+ *
+ * @param[in] cb            callback to execute on RD client state changes
+ */
+void rdcli_standalone_reg_cb(rdcli_standalone_cb_t cb);
+
+/**
+ * @brief   Signal the rdcli thread about connection status change
+ *
+ * @note    This function should not be called by a user, but it is called from
+ *          withing the rdcli implementation
+ *
+ * @param[in] connected     set to true if we are connected to a RD
+ */
+void rdcli_standalone_signal(bool connected);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* NET_RDCLI_STANDALONE_H */
+/** @} */
diff --git a/sys/net/application_layer/rdcli/Makefile b/sys/net/application_layer/rdcli/Makefile
new file mode 100644
index 0000000000000000000000000000000000000000..3b2e7e88b2c7863e573a6540af83ffafe93aedcb
--- /dev/null
+++ b/sys/net/application_layer/rdcli/Makefile
@@ -0,0 +1,7 @@
+SRC = rdcli.c
+
+ifneq (,$(filter rdcli_standalone,$(USEMODULE)))
+  SRC += rdcli_standalone.c
+endif
+
+include $(RIOTBASE)/Makefile.base
diff --git a/sys/net/application_layer/rdcli/rdcli.c b/sys/net/application_layer/rdcli/rdcli.c
new file mode 100644
index 0000000000000000000000000000000000000000..0742ebcc1c7e56d02a500f78830e920060a1fb8e
--- /dev/null
+++ b/sys/net/application_layer/rdcli/rdcli.c
@@ -0,0 +1,363 @@
+/*
+ * Copyright (C) 2017-2018 Freie Universität Berlin
+ *
+ * This file is subject to the terms and conditions of the GNU Lesser
+ * General Public License v2.1. See the file LICENSE in the top level
+ * directory for more details.
+ */
+
+/**
+ * @ingroup     net_rdcli
+ * @{
+ *
+ * @file
+ * @brief       CoRE Resource Directory client implementation
+ *
+ * @author      Hauke Petersen <hauke.petersen@fu-berlin.de>
+ *
+ * @}
+ */
+
+#include <string.h>
+
+#include "fmt.h"
+#include "mutex.h"
+#include "thread_flags.h"
+
+#include "net/gcoap.h"
+#include "net/ipv6/addr.h"
+#include "net/rdcli.h"
+#include "net/rdcli_common.h"
+#include "net/rdcli_config.h"
+
+#ifdef MODULE_RDCLI_STANDALONE
+#include "net/rdcli_standalone.h"
+#endif
+
+#define ENABLE_DEBUG        (0)
+#include "debug.h"
+
+#define FLAG_SUCCESS        (0x0001)
+#define FLAG_TIMEOUT        (0x0002)
+#define FLAG_ERR            (0x0004)
+#define FLAG_OVERFLOW       (0x0008)
+#define FLAG_MASK           (0x000f)
+
+#define BUFSIZE             (512U)
+
+static char *_regif_buf;
+static size_t _regif_buf_len;
+
+static char _rd_loc[NANOCOAP_URI_MAX];
+static char _rd_regif[NANOCOAP_URI_MAX];
+static sock_udp_ep_t _rd_remote;
+
+static mutex_t _mutex = MUTEX_INIT;
+static volatile thread_t *_waiter;
+
+static uint8_t buf[BUFSIZE];
+
+static void _lock(void)
+{
+    mutex_lock(&_mutex);
+    _waiter = sched_active_thread;
+}
+
+static int _sync(void)
+{
+    thread_flags_t flags = thread_flags_wait_any(FLAG_MASK);
+
+    if (flags & FLAG_ERR) {
+        return RDCLI_ERR;
+    }
+    else if (flags & FLAG_TIMEOUT) {
+        return RDCLI_TIMEOUT;
+    }
+    else if (flags & FLAG_OVERFLOW) {
+        return RDCLI_OVERFLOW;
+    }
+    else {
+        return RDCLI_OK;
+    }
+}
+
+static void _on_register(unsigned req_state, coap_pkt_t* pdu,
+                        sock_udp_ep_t *remote)
+{
+    thread_flags_t flag = FLAG_ERR;
+
+    if ((req_state == GCOAP_MEMO_RESP) &&
+        (pdu->hdr->code == COAP_CODE_CREATED)) {
+        /* read the location header and save the RD details on success */
+        if (coap_get_location_path(pdu, (uint8_t *)_rd_loc,
+                                   sizeof(_rd_loc)) > 0) {
+            memcpy(&_rd_remote, remote, sizeof(_rd_remote));
+            flag = FLAG_SUCCESS;
+        }
+        else {
+            /* reset RD entry */
+            flag = FLAG_OVERFLOW;
+        }
+    }
+    else if (req_state == GCOAP_MEMO_TIMEOUT) {
+        flag = FLAG_TIMEOUT;
+    }
+
+    thread_flags_set((thread_t *)_waiter, flag);
+}
+
+static void _on_update_remove(unsigned req_state, coap_pkt_t *pdu, uint8_t code)
+{
+    thread_flags_t flag = FLAG_ERR;
+
+    if ((req_state == GCOAP_MEMO_RESP) && (pdu->hdr->code == code)) {
+        flag = FLAG_SUCCESS;
+    }
+    else if (req_state == GCOAP_MEMO_TIMEOUT) {
+        flag = FLAG_TIMEOUT;
+    }
+
+    thread_flags_set((thread_t *)_waiter, flag);
+}
+
+static void _on_update(unsigned req_state, coap_pkt_t *pdu, sock_udp_ep_t *remote)
+{
+    (void)remote;
+    _on_update_remove(req_state, pdu, COAP_CODE_CHANGED);
+}
+
+static void _on_remove(unsigned req_state, coap_pkt_t *pdu, sock_udp_ep_t *remote)
+{
+    (void)remote;
+    _on_update_remove(req_state, pdu, COAP_CODE_DELETED);
+}
+
+static int _update_remove(unsigned code, gcoap_resp_handler_t handle)
+{
+    coap_pkt_t pkt;
+
+    if (_rd_loc[0] == 0) {
+        return RDCLI_NORD;
+    }
+
+    /* build CoAP request packet */
+    int res = gcoap_req_init(&pkt, buf, sizeof(buf), code, _rd_loc);
+    if (res < 0) {
+        return RDCLI_ERR;
+    }
+    coap_hdr_set_type(pkt.hdr, COAP_TYPE_CON);
+    ssize_t pkt_len = gcoap_finish(&pkt, 0, COAP_FORMAT_NONE);
+
+    /* send request */
+    gcoap_req_send2(buf, pkt_len, &_rd_remote, handle);
+
+    /* synchronize response */
+    return _sync();
+}
+
+static void _on_discover(unsigned req_state, coap_pkt_t *pdu,
+                         sock_udp_ep_t *remote)
+{
+    thread_flags_t flag = RDCLI_NORD;
+    (void)remote;
+
+    if (req_state == GCOAP_MEMO_RESP) {
+        unsigned ct = coap_get_content_type(pdu);
+        if (ct != COAP_FORMAT_LINK) {
+            goto end;
+        }
+        if (pdu->payload_len == 0) {
+            goto end;
+        }
+        /* do simplified parsing of registration interface location */
+        char *start = (char *)pdu->payload;
+        char *end;
+        char *limit = (char *)(pdu->payload + pdu->payload_len);
+        while ((*start != '<') && (start < limit)) {
+            start++;
+        }
+        if (*start != '<') {
+            goto end;
+        }
+        end = ++start;
+        while ((*end != '>') && (end < limit)) {
+            end++;
+        }
+        if (*end != '>') {
+            goto end;
+        }
+        /* TODO: verify size of interface resource identifier */
+        size_t uri_len = (size_t)(end - start);
+        if (uri_len >= _regif_buf_len) {
+            goto end;
+        }
+        memcpy(_regif_buf, start, uri_len);
+        memset((_regif_buf + uri_len), 0, (_regif_buf_len - uri_len));
+        flag = FLAG_SUCCESS;
+    }
+    else if (req_state == GCOAP_MEMO_TIMEOUT) {
+        flag = FLAG_TIMEOUT;
+    }
+
+end:
+    thread_flags_set((thread_t *)_waiter, flag);
+}
+
+static int _discover_internal(const sock_udp_ep_t *remote,
+                              char *regif, size_t maxlen)
+{
+    coap_pkt_t pkt;
+
+    /* save pointer to result buffer */
+    _regif_buf = regif;
+    _regif_buf_len = maxlen;
+
+    /* do URI discovery for the registration interface */
+    int res = gcoap_req_init(&pkt, buf, sizeof(buf), COAP_METHOD_GET,
+                             "/.well-known/core");
+    if (res < 0) {
+        return RDCLI_ERR;
+    }
+    coap_hdr_set_type(pkt.hdr, COAP_TYPE_CON);
+    gcoap_add_qstring(&pkt, "rt", "core.rd");
+    size_t pkt_len = gcoap_finish(&pkt, 0, COAP_FORMAT_NONE);
+    res = gcoap_req_send2(buf, pkt_len, remote, _on_discover);
+    if (res < 0) {
+        return RDCLI_ERR;
+    }
+    return _sync();
+}
+
+int rdcli_discover_regif(const sock_udp_ep_t *remote, char *regif, size_t maxlen)
+{
+    assert(remote && regif);
+
+    _lock();
+    int res = _discover_internal(remote, regif, maxlen);
+    mutex_unlock(&_mutex);
+    return res;
+}
+
+int rdcli_register(const sock_udp_ep_t *remote, const char *regif)
+{
+    assert(remote);
+
+    int res;
+    ssize_t pkt_len;
+    int retval;
+    coap_pkt_t pkt;
+
+    _lock();
+
+    /* if no registration interface is given, we will need to trigger a URI
+     * discovery for it first (see section 5.2) */
+    if (regif == NULL) {
+        retval = _discover_internal(remote, _rd_regif, sizeof(_rd_regif));
+        if (retval != RDCLI_OK) {
+            goto end;
+        }
+    }
+    else {
+        if (strlen(_rd_regif) >= sizeof(_rd_regif)) {
+            retval = RDCLI_OVERFLOW;
+            goto end;
+        }
+        strncpy(_rd_regif, regif, sizeof(_rd_regif));
+    }
+
+    /* build and send CoAP POST request to the RD's registration interface */
+    res = gcoap_req_init(&pkt, buf, sizeof(buf), COAP_METHOD_POST, _rd_regif);
+    if (res < 0) {
+        retval = RDCLI_ERR;
+        goto end;
+    }
+    /* set some packet options and write query string */
+    coap_hdr_set_type(pkt.hdr, COAP_TYPE_CON);
+    rdcli_common_add_qstring(&pkt);
+
+    /* add the resource description as payload */
+    res = gcoap_get_resource_list(pkt.payload, pkt.payload_len,
+                                  COAP_FORMAT_LINK);
+    if (res < 0) {
+        retval = RDCLI_ERR;
+        goto end;
+    }
+
+    /* finish up the packet */
+    pkt_len = gcoap_finish(&pkt, res, COAP_FORMAT_LINK);
+
+    /* send out the request */
+    res = gcoap_req_send2(buf, pkt_len, remote, _on_register);
+    if (res < 0) {
+        retval = RDCLI_ERR;
+        goto end;
+    }
+    retval = _sync();
+
+end:
+    /* if we encountered any error, we mark the client as not connected */
+    if (retval != RDCLI_OK) {
+        _rd_loc[0] = '\0';
+    }
+#ifdef MODULE_RDCLI_STANDALONE
+    else {
+        rdcli_standalone_signal(true);
+    }
+#endif
+
+    mutex_unlock(&_mutex);
+    return retval;
+}
+
+int rdcli_update(void)
+{
+    _lock();
+    int res = _update_remove(COAP_METHOD_POST, _on_update);
+    if (res != RDCLI_OK) {
+        /* in case we are not able to reach the RD, we drop the association */
+#ifdef MODULE_RDCLI_STANDALONE
+        rdcli_standalone_signal(false);
+#endif
+        _rd_loc[0] = '\0';
+    }
+    mutex_unlock(&_mutex);
+    return res;
+}
+
+int rdcli_remove(void)
+{
+    _lock();
+    if (_rd_loc[0] == '\0') {
+        mutex_unlock(&_mutex);
+        return RDCLI_NORD;
+    }
+#ifdef MODULE_RDCLI_STANDALONE
+    rdcli_standalone_signal(false);
+#endif
+    _update_remove(COAP_METHOD_DELETE, _on_remove);
+    /* we actually do not care about the result, we drop the RD local RD entry
+     * in any case */
+    _rd_loc[0] = '\0';
+    mutex_unlock(&_mutex);
+    return RDCLI_OK;
+}
+
+void rdcli_dump_status(void)
+{
+    puts("CoAP RD connection status:");
+
+    if (_rd_loc[0] == 0) {
+        puts("  --- not registered with any RD ---");
+    }
+    else {
+        /* get address string */
+        char addr[IPV6_ADDR_MAX_STR_LEN];
+        ipv6_addr_to_str(addr, (ipv6_addr_t *)&_rd_remote.addr, sizeof(addr));
+
+        printf("RD address: coap://[%s]:%i\n", addr, (int)_rd_remote.port);
+        printf("   ep name: %s\n", rdcli_common_get_ep());
+        printf("  lifetime: %is\n", (int)RDCLI_LT);
+        printf("    reg if: %s\n", _rd_regif);
+        printf("  location: %s\n", _rd_loc);
+    }
+}
diff --git a/sys/net/application_layer/rdcli/rdcli_standalone.c b/sys/net/application_layer/rdcli/rdcli_standalone.c
new file mode 100644
index 0000000000000000000000000000000000000000..73026d90c9794716e231cd96a594879a44f172a0
--- /dev/null
+++ b/sys/net/application_layer/rdcli/rdcli_standalone.c
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2017-2018 Freie Universität Berlin
+ *
+ * This file is subject to the terms and conditions of the GNU Lesser
+ * General Public License v2.1. See the file LICENSE in the top level
+ * directory for more details.
+ */
+
+/**
+ * @ingroup     net_rdcli_simple
+ * @{
+ *
+ * @file
+ * @brief       Standalone extension for the simple RD registration client
+ *
+ * @author      Hauke Petersen <hauke.petersen@fu-berlin.de>
+ *
+ * @}
+ */
+
+#include <stdint.h>
+
+#include "log.h"
+#include "assert.h"
+#include "thread.h"
+#include "xtimer.h"
+#include "net/rdcli.h"
+#include "net/rdcli_config.h"
+#include "net/rdcli_standalone.h"
+
+#define ENABLE_DEBUG    (0)
+#include "debug.h"
+
+/* stack configuration */
+#define STACKSIZE           (THREAD_STACKSIZE_DEFAULT)
+#define PRIO                (THREAD_PRIORITY_MAIN - 1)
+#define TNAME               "rdcli"
+
+#define UPDATE_TIMEOUT      (0xe537)
+
+#define TIMEOUT_US          ((uint64_t)(RDCLI_UPDATE_INTERVAL * US_PER_SEC))
+
+static char _stack[STACKSIZE];
+
+static xtimer_t _timer;
+static kernel_pid_t _runner_pid;
+static msg_t _msg;
+
+static rdcli_standalone_cb_t _cb = NULL;
+
+static void _set_timer(void)
+{
+    xtimer_set_msg64(&_timer, TIMEOUT_US, &_msg, _runner_pid);
+}
+
+static void _notify(rdcli_standalone_event_t event)
+{
+    if (_cb) {
+        _cb(event);
+    }
+}
+
+static void *_reg_runner(void *arg)
+{
+    (void)arg;
+    msg_t in;
+
+    /* prepare context and message */
+    _runner_pid = thread_getpid();
+    _msg.type = UPDATE_TIMEOUT;
+
+    while (1) {
+        DEBUG("rd stand: waiting for message\n");
+        msg_receive(&in);
+        if (in.type == UPDATE_TIMEOUT) {
+            if (rdcli_update() == RDCLI_OK) {
+                DEBUG("rd stand: update ok\n");
+                _set_timer();
+                _notify(RDCLI_UPDATED);
+            }
+            else {
+                _notify(RDCLI_DEREGISTERED);
+            }
+        }
+    }
+
+    return NULL;    /* should never be reached */
+}
+
+void rdcli_standalone_run(void)
+{
+    thread_create(_stack, sizeof(_stack), PRIO, 0, _reg_runner, NULL, TNAME);
+}
+
+void rdcli_standalone_signal(bool connected)
+{
+    /* clear timer in any case */
+    xtimer_remove(&_timer);
+    /* reset the update timer in case a connection was established or updated */
+    if (connected) {
+        _set_timer();
+        _notify(RDCLI_REGISTERED);
+    } else {
+        _notify(RDCLI_DEREGISTERED);
+    }
+}
+
+void rdcli_standalone_reg_cb(rdcli_standalone_cb_t cb)
+{
+    /* Note: we do not allow re-setting the callback (via passing cb := NULL),
+     *       as this would mean additional complexity for synchronizing the
+     *       value of `_cb` to prevent concurrency issues... */
+    assert(cb);
+    _cb = cb;
+}