Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

sys/net/nanocoap: Add CoAP over TCP support #21048

Draft
wants to merge 22 commits into
base: master
Choose a base branch
from
Draft
Changes from 1 commit
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
748784a
sys/include/net/coap.h: Add COAP_PAYLOAD_MARKER_SIZE
maribu Jan 23, 2025
a053b4e
sys/net/nanocoap: improve doc for coap_build_reply
maribu Jan 22, 2025
82efb1d
sys/net/nanocoap: Make APIs (more) transport agnostic
maribu Oct 15, 2024
4c28d8a
sys/net/gcoap: update users of deprecated nanocoap APIs
maribu Oct 15, 2024
e57ad85
sys/net/gcoap_forward_proxy: update users of deprecated nanocoap APIs
maribu Oct 15, 2024
f252274
sys/net/gcoap_dns: update use of deprecated nanocoap APIs
maribu Oct 15, 2024
0da5769
sys/net/CoRD: update use of deprecated nanocoap APIs
maribu Oct 15, 2024
cff5395
examples,tests: update users of deprecated nanocoap APIs
maribu Oct 15, 2024
03ac453
examples/gcoap: upgrade users of deprecated nanocoap APIs
maribu Oct 15, 2024
70242e1
sys/net/sock_util: Do not depend on network stack
maribu Feb 12, 2025
3769723
sys/net/nanocoap: implement CoAP over TCP
maribu Oct 17, 2024
778d1dc
tests/unittests: add tests for nanocoap_tcp
maribu Nov 7, 2024
4772aaa
tests/net/nanocoap_cli: Add TCP support
maribu Nov 11, 2024
b18a8de
examples/nanocoap_server: add CoAP over TCP support
maribu Nov 26, 2024
3b8ebe1
pkg/lwip: Add DEBUG output to lwip_sock_tcp()
maribu Dec 16, 2024
8970951
sys/net/nanocoap: Implement CoAP over WebSocket
maribu Dec 18, 2024
3ea294d
examples/nanocoap_server: add nanocoap_ws support
maribu Dec 23, 2024
68dcf2f
tests/net/nanocoap_cli: Add support for nanocoap_ws
maribu Dec 23, 2024
cd393f8
sys/net: add nanocoap_proxy
maribu Jan 13, 2025
d547646
examples/nanocoap_reverse_proxy: Example reverse proxy
maribu Jan 13, 2025
7ba29e3
dist/tools/coap-yolo: Add WebSocket2UDP proxy
maribu Jan 13, 2025
d536e43
net/nanocoap: Send separate response from server socket
maribu Feb 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
sys/net: add nanocoap_proxy
This implements a trivial reverse proxy that can translate between
different transports for CoAP.

The proxy is relatively limited in that each proxy instance can only
forward one request at a time, reducing throughput and increasing
latency significantly. It also strips the forwarded reply of any CoAP
options (or sends an error if the response contained critical options).
maribu committed Mar 4, 2025
commit cd393f81b5834c2e94ee9a8be6550fc8abb2be23
6 changes: 6 additions & 0 deletions sys/Makefile.dep
Original file line number Diff line number Diff line change
@@ -526,6 +526,12 @@ ifneq (,$(filter nanocoap_dtls,$(USEMODULE)))
USEPKG += tinydtls
endif

ifneq (,$(filter nanocoap_proxy,$(USEMODULE)))
USEMODULE += nanocoap_server_separate
USEMODULE += nanocoap_sock
USEMODULE += tiny_strerror
endif

ifneq (,$(filter nanocoap_server_auto_init,$(USEMODULE)))
USEMODULE += nanocoap_server
endif
128 changes: 128 additions & 0 deletions sys/include/net/nanocoap_proxy.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/*
* Copyright (C) 2025 ML!PA Consulting GmbH
*
* 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.
*/
#ifndef NET_NANOCOAP_PROXY_H
#define NET_NANOCOAP_PROXY_H

/**
* @defgroup net_nanocoap_proxy nanocoap proxy implementation
* @ingroup net
* @brief A trivial reverse proxy on top of nanocoap
*
* @{
*
* @file
* @brief nanocoap reverse proxy
*
* @author Marian Buschsieweke <marian.buschsieweke@posteo.net>
*/

#include <stdint.h>
#include <unistd.h>

#include "bitfield.h"
#include "event.h"
#include "net/nanocoap.h"
#include "net/nanocoap_sock.h"

#ifdef __cplusplus
extern "C" {
#endif
#ifndef CONFIG_NANOCOAP_RPROXY_PARALLEL_FORWARDS
/**
* @brief Number of requests to handle in parallel
*
* @note The forwards will currently be forwarded one at a time, but a higher
* value will allow accepting more requests that come in roughly at the
* same time.
*/
# define CONFIG_NANOCOAP_RPROXY_PARALLEL_FORWARDS 1
#endif

/**
* @brief Canonical name of the NanoCoAP reverse proxy context
*/
typedef struct nanocoap_rproxy_ctx nanocoap_rproxy_ctx_t;

/**
* @brief Context for a forwarded request/reply
*/
typedef struct {
event_t ev; /**< Event used to execute the forwarding */
nanocoap_rproxy_ctx_t *proxy; /**< The proxy this belongs to */
/**
* @brief The name of the endpoint to forward to
*
* @note This is needed to translate e.g. Location-Path options in the
* reply into the corresponding proxy-path
*
* @details `54 == strlen("[0000:0000:0000:0000:0000:ffff:192.168.100.228]:65535") + 1`
*/
char ep[54];
nanocoap_sock_t client; /**< Client socket to use to send this request */
nanocoap_server_response_ctx_t response_ctx; /**< response ctx to use to reply */
coap_pkt_t req; /**< Request for forward */
uint8_t buf[256]; /**< Buffer used to store the request to forward */
} nanocoap_rproxy_forward_ctx_t;

/**
* @brief Contents of @ref nanocoap_rproxy_ctx_t
*/
struct nanocoap_rproxy_ctx {
event_queue_t *evq; /**< Event queue that handles the forwards */
/**
* @brief Request forwarding contexts
*/
nanocoap_rproxy_forward_ctx_t forwards[CONFIG_NANOCOAP_RPROXY_PARALLEL_FORWARDS];
/**
* @brief Bookkeeping for @ref nanocoap_rproxy_ctx_t::forwards
*/
BITFIELD(forwards_used, CONFIG_NANOCOAP_RPROXY_PARALLEL_FORWARDS);
/**
* @brief URI-Scheme to use when forwarding
*/
char scheme[16];
/**
* @brief Target endpoint (in the format that @ref nanocoap_sock_url_connect() can parse)
*
* @note Set this to `NULL` to parse the endpoint from request URI instead
*/
const char *target_ep;
};

/**
* @brief nanocoap resource handler implementing the proxy
*
* Usage:
*
* ```C
* static nanocoap_rproxy_ctx_t _udp_proxy = {
* .evq = EVENT_PRIO_MEDIUM,
* .scheme = "coap://"
* };
*
* NANOCOAP_RESOURCE(udp_proxy) {
* .path = "/udp",
* .methods = COAP_GET | COAP_PUT | COAP_POST | COAP_DELETE | COAP_MATCH_SUBTREE,
* .handler= nanocoap_rproxy_handler,
* .context = &_udp_proxy,
* };
* ```
*
* @param[in] pkt Received packet to forward
* @param[out] buf Buffer to write the response to
* @param[in] len Length of @p buf in bytes
* @param[in] ctx Request ctx
*/
ssize_t nanocoap_rproxy_handler(coap_pkt_t *pkt, uint8_t *buf, size_t len,
coap_request_ctx_t *ctx);

#ifdef __cplusplus
}
#endif
/** @} */
#endif /* NET_NANOCOAP_PROXY_H */
410 changes: 410 additions & 0 deletions sys/net/application_layer/nanocoap/proxy.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,410 @@
/*
* Copyright (C) 2025 ML!PA Consulting GmbH
*
* 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_nanocoap_proxy
* @{
*
* @file
* @brief A trivial reverse proxy on top of nanocoap
*
* @author Marian Buschsieweke <marian.buschsieweke@posteo.net>
*
* @}
*/

#include <stdlib.h>
#include <string.h>

#include "bitfield.h"
#include "container.h"
#include "event.h"
#include "fmt.h"
#include "log.h"
#include "net/nanocoap.h"
#include "net/nanocoap_proxy.h"
#include "random.h"
#include "tiny_strerror.h"

#define ENABLE_DEBUG 0
#include "debug.h"

#ifndef CONFIG_NANOCOAP_PROXY_RESPONSE_HEADER_MAX_SIZE
# define CONFIG_NANOCOAP_PROXY_RESPONSE_HEADER_MAX_SIZE 32
#endif

#ifndef CONFIG_NANOCOAP_PROXY_REQUEST_PACKET_TYPE
# define CONFIG_NANOCOAP_PROXY_REQUEST_PACKET_TYPE COAP_TYPE_NON
#endif

static int _request_done_cb(void *arg, coap_pkt_t *pkt)
{
const char *err_buf_overflown_msg = "resp hdr overflown";
nanocoap_rproxy_forward_ctx_t *conn = arg;
uint8_t rbuf[CONFIG_NANOCOAP_PROXY_RESPONSE_HEADER_MAX_SIZE];

ssize_t hdr_len = nanocoap_server_build_separate(&conn->response_ctx,
rbuf, sizeof(rbuf),
coap_get_code_raw(pkt),
COAP_TYPE_NON, random_uint32());
if (hdr_len < 0) {
return hdr_len;
}

uint8_t *pktpos = rbuf + hdr_len;
uint8_t *pktend = rbuf + sizeof(rbuf);
uint16_t lastonum = 0;
for (unsigned i = 0; i < pkt->options_len; i++) {
bool forward_option = false;
uint16_t onum = pkt->options[i].opt_num;
switch (onum) {
/* convert URI-path: trim off prefix uses to identify target endpoint */
case COAP_OPT_BLOCK1:
case COAP_OPT_BLOCK2:
/* Block1 and Block2 are critical, so we cannot silently elide them.
* The are also unsafe to forward, so normally we would give up
* rather than forwarding them as opaque. But, yolo */
forward_option = true;
break;
default:
if (onum & 0x2) {
/* Option is not safe for forwarding. If it is critical, we
* give up. Otherwise we just drop the option and continue. */
if (onum & 0x1) {
/* option is critical, we cannot just elide it */
DEBUG("[reverse proxy] Critical Option %u in response not handled --> 5.00\n", (unsigned)onum);
char str_onum[8];
return nanocoap_server_send_separate(&conn->response_ctx,
COAP_CODE_INTERNAL_SERVER_ERROR,
COAP_TYPE_NON, str_onum,
fmt_u16_dec(str_onum, onum));
}
}
else {
forward_option = true;
}
break;
}

if (forward_option) {
/* option is safe for forwarding, we copy-paste it */
coap_optpos_t pos = pkt->options[i];
uint8_t *optval;
size_t optlen = coap_opt_get_next(pkt, &pos, &optval, false);
/* worst case option header is 5 bytes + option length */
if (pktpos + 5 + optlen > pktend) {
/* option (potentially) overflows buffer */
DEBUG_PUTS("[reverse proxy] Buffer too small for CoAP Option --> 5.00");
return nanocoap_server_send_separate(&conn->response_ctx,
COAP_CODE_INTERNAL_SERVER_ERROR,
COAP_TYPE_NON, err_buf_overflown_msg,
strlen(err_buf_overflown_msg));
}
pktpos += coap_put_option(pktpos, lastonum, onum, optval, optlen);
lastonum = onum;
}
}

if (pkt->payload_len) {
if (pktpos + 1 > pktend) {
return nanocoap_server_send_separate(&conn->response_ctx,
COAP_CODE_INTERNAL_SERVER_ERROR,
COAP_TYPE_NON, err_buf_overflown_msg,
strlen(err_buf_overflown_msg));
}
*pktpos++ = 0xff;
}

iolist_t data = {
.iol_base = pkt->payload,
.iol_len = pkt->payload_len,
};

iolist_t head = {
.iol_next = &data,
.iol_base = rbuf,
.iol_len = (uintptr_t)pktpos - (uintptr_t)rbuf,
};

DEBUG_PUTS("[reverse proxy] Forwarding reply");
return nanocoap_server_sendv_separate(&conn->response_ctx, &head);
}

static void _disconnect(nanocoap_rproxy_forward_ctx_t *conn)
{
nanocoap_sock_close(&conn->client);
bf_unset(conn->proxy->forwards_used, index_of(conn->proxy->forwards, conn));

}

static void _forward_request_handler(event_t *ev)
{
nanocoap_rproxy_forward_ctx_t *conn = container_of(ev, nanocoap_rproxy_forward_ctx_t, ev);
DEBUG_PUTS("[reverse proxy] Forwarding request ...");
ssize_t err = nanocoap_sock_request_cb(&conn->client, &conn->req, _request_done_cb, conn);
DEBUG("[reverse proxy] Forwarded request: %s\n", (err < 0) ? tiny_strerror(err) : "OK");
if (err < 0) {
const char *errmsg = tiny_strerror(err);
nanocoap_server_send_separate(&conn->response_ctx,
COAP_CODE_INTERNAL_SERVER_ERROR,
COAP_TYPE_NON, errmsg, strlen(errmsg));
}
_disconnect(conn);
}

static bool _is_duplicate(nanocoap_rproxy_ctx_t *ctx, coap_pkt_t *pkt, const coap_request_ctx_t *req)
{
for (unsigned i = 0; i < CONFIG_NANOCOAP_RPROXY_PARALLEL_FORWARDS; i++) {
if (bf_isset(ctx->forwards_used, i)) {
if (nanocoap_is_duplicate_in_separate_ctx(&ctx->forwards[i].response_ctx, pkt, req)) {
return true;
}
}
}

return false;
}

static int _connect(nanocoap_rproxy_forward_ctx_t **dest, nanocoap_rproxy_ctx_t *ctx,
const char *ep, size_t ep_len)
{
if (ep_len >= sizeof(ctx->forwards[0].ep)) {
return -EINVAL;
}

unsigned idx_free = CONFIG_NANOCOAP_RPROXY_PARALLEL_FORWARDS;
for (unsigned i = 0; i < CONFIG_NANOCOAP_RPROXY_PARALLEL_FORWARDS; i++) {
if (!bf_isset(ctx->forwards_used, i)) {
idx_free = i;
break;
}
}

if (idx_free >= CONFIG_NANOCOAP_RPROXY_PARALLEL_FORWARDS) {
return -EAGAIN;
}

char uri[CONFIG_NANOCOAP_URI_MAX];
size_t scheme_len = strlen(ctx->scheme);
if (scheme_len + ep_len + 1 > sizeof(uri)) {
/* not enough space in uri to write scheme + ep + terminating zero byte */
return -EOVERFLOW;
}
char *pos = uri;
memcpy(pos, ctx->scheme, scheme_len);
pos += scheme_len;
memcpy(pos, ep, ep_len);
pos += ep_len;
*pos = '\0';

nanocoap_rproxy_forward_ctx_t *conn = &ctx->forwards[idx_free];

int err = nanocoap_sock_url_connect(uri, &conn->client);
if (err) {
LOG_WARNING("Reverse proxy: Failed to connect to \"%s\"\n", uri);
return -EHOSTUNREACH;
}

DEBUG("[reverse proxy] Connected to \"%s\"\n", uri);

bf_set(ctx->forwards_used, idx_free);
memcpy(conn->ep, ep, ep_len);
conn->ep[ep_len] = '\0';
conn->proxy = ctx;

*dest = conn;
return 0;
}

ssize_t nanocoap_rproxy_handler(coap_pkt_t *pkt, uint8_t *buf, size_t len,
coap_request_ctx_t *ctx)
{
nanocoap_rproxy_ctx_t *proxy = coap_request_ctx_get_context(ctx);

if (_is_duplicate(proxy, pkt, ctx)) {
DEBUG_PUTS("[reverse proxy] Got duplicate --> empty ACK (if needed)");
return coap_reply_empty_ack(pkt, buf, len);
}

char uri[CONFIG_NANOCOAP_URI_MAX] = "";
coap_get_uri_path(pkt, (void *)uri);

const char *my_path = coap_request_ctx_get_path(ctx);
/* If this does not hold the subtree matching should not have happened */
assume(strlen(uri) >= strlen(my_path));

const char *uri_remainder = uri + strlen(my_path);
const char *ep = proxy->target_ep;
const char *ep_end;

if (ep) {
/* endpoint provided by the application */
ep_end = ep + strlen(ep);
}
else {
/* endpoint not provided by application --> target to parse from
* the request URI */
ep = uri_remainder;
if (*ep != '/') {
DEBUG_PUTS("[reverse proxy] No endpoint specified --> 4.04");
return coap_reply_simple(pkt, COAP_CODE_404, buf, len, COAP_FORMAT_NONE,
NULL, 0);
}
ep++;
ep_end = strchr(ep, '/');
if (ep_end) {
uri_remainder = ep_end;
}
else {
ep_end = ep + strlen(ep);
}
}

nanocoap_rproxy_forward_ctx_t *conn = NULL;
{
int err = _connect(&conn, proxy, ep, (uintptr_t)ep_end - (uintptr_t)ep);
const char *msg_invalid_ep = "invalid ep";
switch (err) {
case 0:
/* no error */
break;
case -EAGAIN:
DEBUG_PUTS("[reverse proxy] No free slot for connection --> 4.29");
return coap_reply_simple(pkt, COAP_CODE_TOO_MANY_REQUESTS, buf, len,
COAP_FORMAT_NONE, NULL, 0);
case -EINVAL:
DEBUG_PUTS("[reverse proxy] Invalid EP --> 4.00");
return coap_reply_simple(pkt, COAP_CODE_BAD_REQUEST, buf, len,
COAP_FORMAT_NONE, msg_invalid_ep, strlen(msg_invalid_ep));
case -EOVERFLOW:
DEBUG_PUTS("[reverse proxy] URI buffer too small to connect --> 5.00");
return coap_reply_simple(pkt, COAP_CODE_BAD_REQUEST, buf, len,
COAP_FORMAT_NONE, NULL, 0);
case -EHOSTUNREACH:
DEBUG_PUTS("[reverse proxy] Failed to connect --> 4.04");
return coap_reply_simple(pkt, COAP_CODE_404, buf, len,
COAP_FORMAT_NONE, NULL, 0);
default:
DEBUG("[reverse proxy] Unhandled error %d in _connect --> 5.00\n", err);
return coap_reply_simple(pkt, COAP_CODE_BAD_REQUEST, buf, len,
COAP_FORMAT_NONE, NULL, 0);
}
}

if (nanocoap_server_prepare_separate(&conn->response_ctx, pkt, ctx)) {
DEBUG_PUTS("[reverse proxy] Failed to prepare response context --> RST");
/* Send a RST message: We don't support extended tokens here */
return coap_build_reply(pkt, 0, buf, len, 0);
}

ssize_t hdr_len = nanocoap_sock_build_pkt(&conn->client, &conn->req,
conn->buf, sizeof(conn->buf),
CONFIG_NANOCOAP_PROXY_REQUEST_PACKET_TYPE,
coap_get_token(pkt), coap_get_token_len(pkt),
coap_get_code_raw(pkt));
if (hdr_len < 0) {
DEBUG("[reverse proxy] Failed to build req to forward: %" PRIdSIZE " --> 5.00", hdr_len);
_disconnect(conn);
return coap_reply_simple(pkt, COAP_CODE_INTERNAL_SERVER_ERROR,
buf, len, COAP_FORMAT_NONE, NULL, 0);
}

uint8_t *pktpos = conn->buf + hdr_len;
uint8_t *pktend = conn->buf + len;
uint16_t lastonum = 0;
for (unsigned i = 0; i < pkt->options_len; i++) {
bool forward_option = false;
uint16_t onum = pkt->options[i].opt_num;
switch (onum) {
/* convert URI-path: trim off prefix uses to identify target endpoint */
case COAP_OPT_URI_PATH:
if (uri_remainder) {
/* CoAP option header can be 3 bytes: Even if Uri-Path (11)
* would be the first option (Option Delta would be 11), it
* fits into 4 bits. Option Length can be extended (+2 bytes
* in worst case */
if (pktpos + 3 + strlen(uri_remainder) > pktend) {
/* URI-Path (potentially) overflows buffer */
DEBUG_PUTS("[reverse proxy] Buffer too small for URI --> 5.00");
_disconnect(conn);
return coap_reply_simple(pkt, COAP_CODE_INTERNAL_SERVER_ERROR,
buf, len, COAP_FORMAT_NONE, NULL, 0);
}
pktpos += coap_opt_put_uri_pathquery(pktpos, &lastonum, uri_remainder);
}
break;
case COAP_OPT_BLOCK1:
case COAP_OPT_BLOCK2:
/* Block1 and Block2 are critical, so we cannot silently elide them.
* The are also unsafe to forward, so normally we would give up
* rather than forwarding them as opaque. But, yolo */
forward_option = true;
break;
default:
if (onum & 0x2) {
/* Option is not safe for forwarding. If it is critical, we
* give up. Otherwise we just drop the option and continue. */
if (onum & 0x1) {
/* option is critical, we cannot just elide it */
DEBUG("[reverse proxy] Option %u not handled --> 4.02\n", (unsigned)onum);
_disconnect(conn);
char str_onum[8];
return coap_reply_simple(pkt, COAP_CODE_BAD_OPTION,
buf, len, COAP_FORMAT_NONE,
str_onum, fmt_u32_dec(str_onum, onum));

}
}
else {
forward_option = true;
}
break;
}

if (forward_option) {
/* option is safe for forwarding, we copy-paste it */
coap_optpos_t pos = pkt->options[i];
uint8_t *optval;
size_t optlen = coap_opt_get_next(pkt, &pos, &optval, false);
/* worst case option header is 5 bytes + option length */
if (pktpos + 5 + optlen > pktend) {
/* option (potentially) overflows buffer */
_disconnect(conn);
DEBUG_PUTS("[reverse proxy] Buffer too small for CoAP Option --> 5.00");
return coap_reply_simple(pkt, COAP_CODE_INTERNAL_SERVER_ERROR,
buf, len, COAP_FORMAT_NONE, NULL, 0);
}
DEBUG("[reverse proxy] Adding option %u, sized %u\n",
(unsigned)onum, (unsigned)optlen);
pktpos += coap_put_option(pktpos, lastonum, onum, optval, optlen);
lastonum = onum;
}
}

conn->req.payload = pktpos;
conn->req.payload_len = pkt->payload_len;
if (pkt->payload_len) {
if (pktpos + 1 + pkt->payload_len > pktend) {
_disconnect(conn);
DEBUG_PUTS("[reverse proxy] Buffer too small for payload --> 5.00");
return coap_reply_simple(pkt, COAP_CODE_INTERNAL_SERVER_ERROR,
buf, len, COAP_FORMAT_NONE, NULL, 0);
}
*pktpos++ = 0xff; /* payload marker */
conn->req.payload = pktpos;
memcpy(pktpos, pkt->payload, pkt->payload_len);
}

assume(conn->req.payload + conn->req.payload_len <= pktend);
conn->ev.handler = _forward_request_handler;
DEBUG("[reverse proxy] Scheduled forwarding request for URI \"%s\" to %.*s\n",
uri_remainder, (int)((uintptr_t)ep_end - (uintptr_t)ep), ep);
event_post(proxy->evq, &conn->ev);
DEBUG_PUTS("[reverse proxy] Request to forward queued --> ACK (if needed)");
return coap_reply_empty_ack(pkt, buf, len);
}