diff --git a/READMEs/README.coding.md b/READMEs/README.coding.md
index e844a8f85b9092276d802a4b1ed76e79e4eafcda..9ac87e6b89924b62e5407c28f69da28d380dffda 100644
--- a/READMEs/README.coding.md
+++ b/READMEs/README.coding.md
@@ -743,7 +743,9 @@ callbacks on the named protocol
 
 starting with LWS_CALLBACK_RAW_ADOPT_FILE.
 
-`protocol-lws-raw-test` plugin provides a method for testing this with
+The minimal example `raw/minimal-raw-file` demonstrates how to use it.
+
+`protocol-lws-raw-test` plugin also provides a method for testing this with
 `libwebsockets-test-server-v2.0`:
 
 The plugin creates a FIFO on your system called "/tmp/lws-test-raw"
@@ -827,6 +829,46 @@ and in another window, connect to it using the test client
 The connection should succeed, and text typed in the netcat window (including a CRLF)
 will be received in the client.
 
+@section rawudp RAW UDP socket integration
+
+Lws provides an api to create, optionally bind, and adopt a RAW UDP
+socket (RAW here means an uninterpreted normal UDP socket, not a
+"raw socket").
+
+```
+LWS_VISIBLE LWS_EXTERN struct lws *
+lws_create_adopt_udp(struct lws_vhost *vhost, int port, int flags,
+		     const char *protocol_name, struct lws *parent_wsi);
+```
+
+`flags` should be `LWS_CAUDP_BIND` if the socket will receive packets.
+
+The callbacks `LWS_CALLBACK_RAW_ADOPT`, `LWS_CALLBACK_RAW_CLOSE`,
+`LWS_CALLBACK_RAW_RX` and `LWS_CALLBACK_RAW_WRITEABLE` apply to the
+wsi.  But UDP is different than TCP in some fundamental ways.
+
+For receiving on a UDP connection, data becomes available at
+`LWS_CALLBACK_RAW_RX` as usual, but because there is no specific
+connection with UDP, it is necessary to also get the source address of
+the data separately, using `struct lws_udp * lws_get_udp(wsi)`.
+You should take a copy of the `struct lws_udp` itself (not the
+pointer) and save it for when you want to write back to that peer.
+
+Writing is also a bit different for UDP.  By default, the system has no
+idea about the receiver state and so asking for a `callback_on_writable()`
+always believes that the socket is writeable... the callback will
+happen next time around the event loop.
+
+With UDP, there is no single "connection".  You need to write with sendto() and
+direct the packets to a specific destination.  To return packets to a
+peer who sent something earlier and you copied his `struct lws_udp`, you
+use the .sa and .salen members as the last two parameters of the sendto().
+
+The kernel may not accept to buffer / write everything you wanted to send.
+So you are responsible to watch the result of sendto() and resend the
+unsent part next time (which may involve adding new protocol headers to
+the remainder depending on what you are doing).
+
 @section ecdh ECDH Support
 
 ECDH Certs are now supported.  Enable the CMake option
diff --git a/lib/context.c b/lib/context.c
index 310cfe39b7b7b35f4a752532d90153a3ba073ceb..eb3109eb3b45385095a14fa2955207352e7358f4 100644
--- a/lib/context.c
+++ b/lib/context.c
@@ -565,6 +565,7 @@ lws_create_vhost(struct lws_context *context,
 #endif
 	struct lws_protocols *lwsp;
 	int m, f = !info->pvo;
+	char buf[20];
 #ifdef LWS_HAVE_GETENV
 	char *p;
 #endif
@@ -721,10 +722,22 @@ lws_create_vhost(struct lws_context *context,
 				vh->name, vh->iface, vh->count_protocols);
 	} else
 #endif
-	lwsl_notice("Creating Vhost '%s' port %d, %d protocols, IPv6 %s\n",
-			vh->name, info->port, vh->count_protocols,
-			LWS_IPV6_ENABLED(vh) ? "on" : "off");
-
+	{
+		switch(info->port) {
+		case CONTEXT_PORT_NO_LISTEN:
+			strcpy(buf, "(serving disabled)");
+			break;
+		case CONTEXT_PORT_NO_LISTEN_SERVER:
+			strcpy(buf, "(no listener)");
+			break;
+		default:
+			lws_snprintf(buf, sizeof(buf), "port %u", info->port);
+			break;
+		}
+		lwsl_notice("Creating Vhost '%s' %s, %d protocols, IPv6 %s\n",
+				vh->name, buf, vh->count_protocols,
+				LWS_IPV6_ENABLED(vh) ? "on" : "off");
+	}
 	mounts = info->mounts;
 	while (mounts) {
 		(void)mount_protocols[0];
diff --git a/lib/libwebsockets.c b/lib/libwebsockets.c
index 9ba3461cd232f9b0be228ccdd96e931cf2dc3b1d..fc822a65818483895bbde541ba75de909d9b7b41 100644
--- a/lib/libwebsockets.c
+++ b/lib/libwebsockets.c
@@ -80,6 +80,7 @@ __lws_free_wsi(struct lws *wsi)
 	lws_free_set_NULL(wsi->rxflow_buffer);
 	lws_free_set_NULL(wsi->trunc_alloc);
 	lws_free_set_NULL(wsi->ws);
+	lws_free_set_NULL(wsi->udp);
 
 	/* we may not have an ah, but may be on the waiting list... */
 	lwsl_info("ah det due to close\n");
@@ -1330,6 +1331,12 @@ lws_protocol_get(struct lws *wsi)
 	return wsi->protocol;
 }
 
+LWS_VISIBLE const struct lws_udp *
+lws_get_udp(const struct lws *wsi)
+{
+	return wsi->udp;
+}
+
 LWS_VISIBLE struct lws *
 lws_get_network_wsi(struct lws *wsi)
 {
@@ -2694,12 +2701,71 @@ lws_get_addr_scope(const char *ipaddr)
 }
 #endif
 
+#if !defined(LWS_NO_SERVER)
+
+LWS_EXTERN struct lws *
+lws_create_adopt_udp(struct lws_vhost *vhost, int port, int flags,
+		     const char *protocol_name, struct lws *parent_wsi)
+{
+	lws_sock_file_fd_type sock;
+	struct addrinfo h, *r, *rp;
+	struct lws *wsi = NULL;
+	char buf[16];
+	int n;
+
+	memset(&h, 0, sizeof(h));
+	h.ai_family = AF_UNSPEC;    /* Allow IPv4 or IPv6 */
+	h.ai_socktype = SOCK_DGRAM;
+	h.ai_protocol = IPPROTO_UDP;
+	h.ai_flags = AI_PASSIVE | AI_ADDRCONFIG;
+
+	lws_snprintf(buf, sizeof(buf), "%u", port);
+	n = getaddrinfo(NULL, buf, &h, &r);
+	if (n) {
+		lwsl_info("%s: getaddrinfo error: %s\n", __func__,
+			  gai_strerror(n));
+		goto bail;
+	}
+
+	for (rp = r; rp; rp = rp->ai_next) {
+		sock.sockfd = socket(rp->ai_family, rp->ai_socktype,
+				     rp->ai_protocol);
+		if (sock.sockfd >= 0)
+			break;
+	}
+	if (!rp) {
+		lwsl_err("%s: unable to create INET socket\n", __func__);
+		goto bail1;
+	}
+
+	if ((flags & LWS_CAUDP_BIND) &&
+	    bind(sock.sockfd, rp->ai_addr, rp->ai_addrlen) ==-1) {
+		lwsl_err("%s: bind failed\n", __func__);
+		goto bail2;
+	}
+
+	wsi = lws_adopt_descriptor_vhost(vhost, LWS_ADOPT_RAW_SOCKET_UDP, sock,
+				        protocol_name, parent_wsi);
+	if (!wsi)
+		lwsl_err("%s: udp adoption failed\n", __func__);
+
+bail2:
+	if (!wsi)
+		close(sock.sockfd);
+bail1:
+	freeaddrinfo(r);
+
+bail:
+	return wsi;
+}
+
+#endif
+
 LWS_EXTERN void
 lws_restart_ws_ping_pong_timer(struct lws *wsi)
 {
-	if (!wsi->context->ws_ping_pong_interval)
-		return;
-	if (!lws_state_is_ws(wsi->state))
+	if (!wsi->context->ws_ping_pong_interval ||
+	    !lws_state_is_ws(wsi->state))
 		return;
 
 	wsi->ws->time_next_ping_check = (time_t)lws_now_secs();
diff --git a/lib/libwebsockets.h b/lib/libwebsockets.h
index 210ff562579d8e64f5b83a2024aee9f4d4830582..a03928d6b92a6968efafb02238bcd9df2037e1da 100644
--- a/lib/libwebsockets.h
+++ b/lib/libwebsockets.h
@@ -4555,6 +4555,7 @@ enum pending_timeout {
 	PENDING_TIMEOUT_KILLED_BY_PARENT			= 23,
 	PENDING_TIMEOUT_CLOSE_SEND				= 24,
 	PENDING_TIMEOUT_HOLDING_AH				= 25,
+	PENDING_TIMEOUT_UDP_IDLE				= 26,
 
 	/****** add new things just above ---^ ******/
 
@@ -4998,7 +4999,7 @@ lws_callback_http_dummy(struct lws *wsi, enum lws_callback_reasons reason,
 /**
  * lws_get_socket_fd() - returns the socket file descriptor
  *
- * You will not need this unless you are doing something special
+ * This is needed to use sendto() on UDP raw sockets
  *
  * \param wsi:	Websocket connection instance
  */
@@ -5151,8 +5152,10 @@ typedef enum {
 	LWS_ADOPT_ALLOW_SSL = 4,	/* flag: if set requires LWS_ADOPT_SOCKET */
 	LWS_ADOPT_WS_PARENTIO = 8,	/* flag: ws mode parent handles IO
 					 *   if given must be only flag
-					 *   wsi put directly into ws mode
-					 */
+					 *   wsi put directly into ws mode */
+	LWS_ADOPT_FLAG_UDP = 16,	/* flag: socket is UDP */
+
+	LWS_ADOPT_RAW_SOCKET_UDP = LWS_ADOPT_SOCKET | LWS_ADOPT_FLAG_UDP,
 } lws_adoption_type;
 
 typedef union {
@@ -5160,6 +5163,14 @@ typedef union {
 	lws_filefd_type filefd;
 } lws_sock_file_fd_type;
 
+struct lws_udp {
+	struct sockaddr sa;
+	socklen_t salen;
+
+	struct sockaddr sa_pending;
+	socklen_t salen_pending;
+};
+
 /*
 * lws_adopt_descriptor_vhost() - adopt foreign socket or file descriptor
 * if socket descriptor, should already have been accepted from listen socket
@@ -5236,6 +5247,24 @@ lws_adopt_socket_readbuf(struct lws_context *context, lws_sockfd_type accept_fd,
 LWS_VISIBLE LWS_EXTERN struct lws *
 lws_adopt_socket_vhost_readbuf(struct lws_vhost *vhost, lws_sockfd_type accept_fd,
                                const char *readbuf, size_t len);
+
+#define LWS_CAUDP_BIND 1
+
+/**
+ * lws_create_adopt_udp() - create, bind and adopt a UDP socket
+ *
+ * \param vhost:	 lws vhost
+ * \param port:		 UDP port to bind to, -1 means unbound
+ * \param flags:	 0 or LWS_CAUDP_NO_BIND
+ * \param protocol_name: Name of protocol on vhost to bind wsi to
+ * \param parent_wsi:	 NULL or parent wsi new wsi will be a child of
+ *
+ * Either returns new wsi bound to accept_fd, or closes accept_fd and
+ * returns NULL, having cleaned up any new wsi pieces.
+ * */
+LWS_VISIBLE LWS_EXTERN struct lws *
+lws_create_adopt_udp(struct lws_vhost *vhost, int port, int flags,
+		     const char *protocol_name, struct lws *parent_wsi);
 ///@}
 
 /** \defgroup net Network related helper APIs
@@ -5632,6 +5661,16 @@ lws_get_parent(const struct lws *wsi);
 LWS_VISIBLE LWS_EXTERN struct lws * LWS_WARN_UNUSED_RESULT
 lws_get_child(const struct lws *wsi);
 
+/**
+ * lws_get_udp() - get wsi's udp struct
+ *
+ * \param wsi: lws connection
+ *
+ * Returns NULL or pointer to the wsi's UDP-specific information
+ */
+LWS_VISIBLE LWS_EXTERN const struct lws_udp * LWS_WARN_UNUSED_RESULT
+lws_get_udp(const struct lws *wsi);
+
 /**
  * lws_parent_carries_io() - mark wsi as needing to send messages via parent
  *
diff --git a/lib/output.c b/lib/output.c
index 31ccb3786224598a9747d460102b1f591bc081d4..be3a4e734839ddbf156bb914598c45ac2642c2fd 100644
--- a/lib/output.c
+++ b/lib/output.c
@@ -198,6 +198,12 @@ handle_truncated_send:
 	wsi->trunc_len = (unsigned int)(real_len - n);
 	memcpy(wsi->trunc_alloc, buf + n, real_len - n);
 
+	if (lws_wsi_is_udp(wsi)) {
+		/* stash original destination for fulfilling UDP partials */
+		wsi->udp->sa_pending = wsi->udp->sa;
+		wsi->udp->salen_pending = wsi->udp->salen;
+	}
+
 	/* since something buffered, force it to get another chance to send */
 	lws_callback_on_writable(wsi);
 
@@ -858,12 +864,19 @@ lws_ssl_capable_read_no_ssl(struct lws *wsi, unsigned char *buf, int len)
 
 	lws_stats_atomic_bump(context, pt, LWSSTATS_C_API_READ, 1);
 
-	n = recv(wsi->desc.sockfd, (char *)buf, len, 0);
+	if (lws_wsi_is_udp(wsi)) {
+		wsi->udp->salen = sizeof(wsi->udp->sa);
+		n = recvfrom(wsi->desc.sockfd, (char *)buf, len, 0,
+			     &wsi->udp->sa, &wsi->udp->salen);
+	} else
+		n = recv(wsi->desc.sockfd, (char *)buf, len, 0);
+
 	if (n >= 0) {
 		if (wsi->vhost)
 			wsi->vhost->conn_stats.rx += n;
 		lws_stats_atomic_bump(context, pt, LWSSTATS_B_READ, n);
 		lws_restart_ws_ping_pong_timer(wsi);
+
 		return n;
 	}
 #if LWS_POSIX
@@ -882,7 +895,13 @@ lws_ssl_capable_write_no_ssl(struct lws *wsi, unsigned char *buf, int len)
 	int n = 0;
 
 #if LWS_POSIX
-	n = send(wsi->desc.sockfd, (char *)buf, len, MSG_NOSIGNAL);
+	if (lws_wsi_is_udp(wsi)) {
+		if (wsi->trunc_len)
+			n = sendto(wsi->desc.sockfd, buf, len, 0, &wsi->udp->sa_pending, wsi->udp->salen_pending);
+		else
+			n = sendto(wsi->desc.sockfd, buf, len, 0, &wsi->udp->sa, wsi->udp->salen);
+	} else
+		n = send(wsi->desc.sockfd, (char *)buf, len, MSG_NOSIGNAL);
 //	lwsl_info("%s: sent len %d result %d", __func__, len, n);
 	if (n >= 0)
 		return n;
diff --git a/lib/private-libwebsockets.h b/lib/private-libwebsockets.h
index 02b22a2d4245f6ecbf452b010b4d15e7ebaae154..d6604a9b8b65c75c9bddfb8a303b23ef9345f7d1 100644
--- a/lib/private-libwebsockets.h
+++ b/lib/private-libwebsockets.h
@@ -1840,6 +1840,8 @@ struct lws_access_log {
 };
 #endif
 
+#define lws_wsi_is_udp(___wsi) (!!___wsi->udp)
+
 struct lws {
 	/* structs */
 
@@ -1881,6 +1883,7 @@ struct lws {
 #endif
 	struct allocated_headers *ah;
 	struct lws *ah_wait_list;
+	struct lws_udp *udp;
 	unsigned char *preamble_rx;
 #ifndef LWS_NO_CLIENT
 	struct client_info_stash *stash;
diff --git a/lib/server/peer-limits.c b/lib/server/peer-limits.c
index 4e8b3aba2cdd5dbd245094bc8f9d0356be7e158e..707454fe02ec81bc8d0595f8eeb3a75794fc0f8d 100644
--- a/lib/server/peer-limits.c
+++ b/lib/server/peer-limits.c
@@ -70,10 +70,9 @@ lws_get_or_create_peer(struct lws_vhost *vhost, lws_sockfd_type sockfd)
 	}
 #endif
 	rlen = sizeof(addr);
-	if (getpeername(sockfd, (struct sockaddr*)&addr, &rlen)) {
-		lwsl_notice("%s: getpeername failed\n", __func__);
+	if (getpeername(sockfd, (struct sockaddr*)&addr, &rlen))
+		/* eg, udp doesn't have to have a peer */
 		return NULL;
-	}
 
 	if (af == AF_INET) {
 		struct sockaddr_in *s = (struct sockaddr_in *)&addr;
@@ -111,6 +110,7 @@ lws_get_or_create_peer(struct lws_vhost *vhost, lws_sockfd_type sockfd)
 	peer = lws_zalloc(sizeof(*peer), "peer");
 	if (!peer) {
 		lws_context_unlock(context); /* === */
+		lwsl_err("%s: OOM for new peer\n", __func__);
 		return NULL;
 	}
 
diff --git a/lib/server/server.c b/lib/server/server.c
index 25d4607323ee361a5334340a288d2f53238d437f..3fa7602044e83a7d7c8b7ab99bc9a29583fa70ef 100644
--- a/lib/server/server.c
+++ b/lib/server/server.c
@@ -2012,11 +2012,7 @@ lws_adopt_descriptor_vhost(struct lws_vhost *vh, lws_adoption_type type,
 	if (type & LWS_ADOPT_SOCKET && !(type & LWS_ADOPT_WS_PARENTIO)) {
 		peer = lws_get_or_create_peer(vh, fd.sockfd);
 
-		if (!peer) {
-			lwsl_err("OOM creating peer\n");
-			return NULL;
-		}
-		if (context->ip_limit_wsi &&
+		if (peer && context->ip_limit_wsi &&
 		    peer->count_wsi >= context->ip_limit_wsi) {
 			lwsl_notice("Peer reached wsi limit %d\n",
 					context->ip_limit_wsi);
@@ -2092,6 +2088,13 @@ lws_adopt_descriptor_vhost(struct lws_vhost *vh, lws_adoption_type type,
 		lwsl_debug("%s: new wsi %p, sockfd %d\n", __func__, new_wsi,
 			   (int)(lws_intptr_t)fd.sockfd);
 
+		if (type & LWS_ADOPT_FLAG_UDP)
+			/*
+			 * these can be >128 bytes, so just alloc for UDP
+			 */
+			new_wsi->udp = lws_malloc(sizeof(*new_wsi->udp),
+						     "udp struct");
+
 		if (type & LWS_ADOPT_HTTP)
 			/* the transport is accepted...
 			 * give him time to negotiate */