diff --git a/doc/guides/nics/features/udp.ini b/doc/guides/nics/features/udp.ini
new file mode 100644
index 000000000000..dfc39204dacf
--- /dev/null
+++ b/doc/guides/nics/features/udp.ini
@@ -0,0 +1,10 @@
+;
+; Supported features of the 'udp' network poll mode driver.
+;
+; Refer to default.ini for the full list of available PMD features.
+;
+[Features]
+Basic stats          = Y
+Stats per queue      = Y
+Multiprocess aware   = Y
+Scattered Rx         = P
diff --git a/doc/guides/nics/udp.rst b/doc/guides/nics/udp.rst
new file mode 100644
index 000000000000..7b86f5e273e9
--- /dev/null
+++ b/doc/guides/nics/udp.rst
@@ -0,0 +1,30 @@
+..  SPDX-License-Identifier: BSD-3-Clause
+    Copyright(c) 2018 Intel Corporation.
+
+UDP Poll Mode Driver
+====================
+
+UDP Poll Mode Driver (PMD) is a basic virtual driver useful for testing.
+It provides a simple bare encapsulation of Ethernet frames in a UDP
+socket.  This is useful since the test driver application can
+easily open a local UDP socket and interact with a DPDK application.
+This can even be done inside a VM or container in automated test setup.
+
+
+Driver Configuration
+--------------------
+
+The driver is a virtual device configured with the --vdev option.
+The device name must start with the net_udp prefix follwed by numbers
+or letters The name is unique for each device. Each device can have
+multiple stream options and multiple devices can be used.
+Multiple device definitions can be arranged using multiple --vdev.
+
+Both local and remote address must be specified. Both IPv4 and IPv6
+are supported examples:
+
+.. code-block:: console
+
+   ./<build_dir>/app/dpdk-testpmd -l 0-3 -n 4 \
+       --vdev 'net_udp0,local=127.0.0.1:9000,remote=192.0.2.1:9000',
+       --vdev 'net_udp1,local=[:0],9000,remote=[2001:DB8::1]:9000'
diff --git a/drivers/net/meson.build b/drivers/net/meson.build
index 35bfa78dee66..36f2d9ed9b96 100644
--- a/drivers/net/meson.build
+++ b/drivers/net/meson.build
@@ -56,6 +56,7 @@ drivers = [
         'tap',
         'thunderx',
         'txgbe',
+        'udp',
         'vdev_netvsc',
         'vhost',
         'virtio',
diff --git a/drivers/net/udp/meson.build b/drivers/net/udp/meson.build
new file mode 100644
index 000000000000..e7bfd843f4b2
--- /dev/null
+++ b/drivers/net/udp/meson.build
@@ -0,0 +1,11 @@
+# SPDX-License-Identifier: BSD-3-Clause
+
+if is_windows
+    build = false
+    reason = 'not supported on Windows'
+    subdir_done()
+endif
+
+sources = files('rte_eth_udp.c')
+
+pmd_supports_disable_iova_as_pa = true
diff --git a/drivers/net/udp/rte_eth_udp.c b/drivers/net/udp/rte_eth_udp.c
new file mode 100644
index 000000000000..8ce65721b3ec
--- /dev/null
+++ b/drivers/net/udp/rte_eth_udp.c
@@ -0,0 +1,728 @@
+/* SPDX-License-Identifier: BSD-3-Clause
+ *
+ * Copyright (c) 2022 Microsoft Corp.
+ * All rights reserved.
+ */
+
+#include <assert.h>
+#include <errno.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <sys/socket.h>
+#include <netinet/in.h>
+#include <arpa/inet.h>
+#include <sys/uio.h>
+#include <unistd.h>
+#include <netdb.h>
+#include <string.h>
+
+#include <rte_bus_vdev.h>
+#include <rte_common.h>
+#include <rte_ethdev.h>
+#include <ethdev_vdev.h>
+#include <ethdev_driver.h>
+#include <bus_vdev_driver.h>
+#include <rte_ether.h>
+#include <rte_kvargs.h>
+#include <rte_log.h>
+#include <rte_mbuf.h>
+
+/* Which strings are valid kvargs for this driver */
+#define ETH_UDP_LOCAL_ARG	"local"
+#define ETH_UDP_REMOTE_ARG	"remote"
+
+static int eth_udp_logtype;
+#define PMD_LOG(level, fmt, args...) \
+	rte_log(RTE_LOG_ ## level, eth_udp_logtype, \
+		"%s(): " fmt "\n", __func__, ##args)
+
+struct pmd_internals;
+
+struct udp_queue {
+	struct rte_mempool *mb_pool;
+	struct pmd_internals *internals;
+	uint16_t queue_id;
+	int sock_fd;
+
+	uint64_t pkts;
+	uint64_t bytes;
+	uint64_t nobufs;
+};
+
+struct pmd_internals {
+	uint16_t port_id;
+	int sock_fd;
+
+	struct sockaddr_storage remote;
+	struct sockaddr_storage local;
+	struct rte_ether_addr eth_addr;
+
+	struct udp_queue rx_queues[RTE_MAX_QUEUES_PER_PORT];
+	struct udp_queue tx_queues[RTE_MAX_QUEUES_PER_PORT];
+};
+
+static struct rte_eth_link pmd_link = {
+	.link_speed = RTE_ETH_SPEED_NUM_10G,
+	.link_duplex = RTE_ETH_LINK_FULL_DUPLEX,
+	.link_status = RTE_ETH_LINK_DOWN,
+	.link_autoneg = RTE_ETH_LINK_FIXED,
+};
+
+static int
+parse_ipv6_address(const char *value, struct sockaddr_in6 *sin6)
+{
+	char *str = strdupa(value);
+	char *endp;
+
+	++str;	 /* skip leading '[' */
+	endp = strchr(str, ']');
+	if (endp == NULL) {
+		PMD_LOG(ERR, "missing closing ]");
+		return -EINVAL;
+	}
+
+	*endp++ = '\0';
+	sin6->sin6_family = AF_INET6;
+
+	if (inet_pton(AF_INET6, ++str, &sin6->sin6_addr) != 1) {
+		PMD_LOG(ERR, "invalid ipv6 address '%s'", str);
+		return -EINVAL;
+	}
+
+	/* Handle [ff80::1]:999 as address and port */
+	if (*endp == ':') {
+		sin6->sin6_port = htons(strtoul(endp + 1, NULL, 0));
+	} else if (*endp != '\0') {
+		PMD_LOG(ERR, "incorrect ipv6 port syntax");
+		return -EINVAL;
+	}
+	return 0;
+}
+
+static int
+parse_ipv4_address(const char *value, struct sockaddr_in *sin)
+{
+	char *str = strdupa(value);
+	char *endp;
+
+	endp = strchr(str, ':');
+	if (endp)
+		*endp++ = '\0';
+
+	memset(sin, 0, sizeof(*sin));
+	sin->sin_family = AF_INET;
+
+	if (inet_pton(AF_INET, str, &sin->sin_addr) != 1) {
+		PMD_LOG(ERR, "invalid ipv4 address '%s'", str);
+		return -EINVAL;
+	}
+
+	if (endp != NULL)
+		sin->sin_port = htons(strtoul(endp, NULL, 0));
+
+	return 0;
+}
+
+/* Addresses are given on Kvargs as:
+ *   127.0.0.1:9000
+ *   [::1]:9000
+ */
+static int
+get_address_arg(const char *key, const char *value, void *sarg)
+{
+	if (value == NULL)
+		return -EINVAL;
+
+	PMD_LOG(DEBUG, "%s='%s'", key, value);
+
+	if (*value == '[')
+		return parse_ipv6_address(value, sarg);
+	else
+		return parse_ipv4_address(value, sarg);
+}
+
+/* Helper function to determine how many mbufs are needed per packet  */
+static uint16_t
+eth_mbuf_per_pkt(uint16_t port_id,
+		 struct rte_mempool *mb_pool)
+{
+	const struct rte_eth_dev *dev = &rte_eth_devices[port_id];
+	uint16_t buf_size = rte_pktmbuf_data_room_size(mb_pool);
+
+	return (dev->data->mtu + buf_size - 1) / buf_size;
+}
+
+
+/*
+ * Receive packets from socket into mbufs.
+ *
+ * In order to handle multiple packets at a time and scattered receive
+ * this allocates the worst case number of buffers.
+ *
+ * If out of memory, or socket gives error returns 0.
+ */
+static uint16_t
+eth_udp_rx(void *queue, struct rte_mbuf **pkts, uint16_t nb_pkts)
+{
+	struct udp_queue *udp_q = queue;
+	uint16_t port_id = udp_q->internals->port_id;
+	struct rte_mempool *mpool = udp_q->mb_pool;
+	unsigned int segs_per_pkt = eth_mbuf_per_pkt(port_id, mpool);
+	unsigned int num_segs = nb_pkts * segs_per_pkt;
+	struct rte_mbuf *bufs[num_segs];
+	struct iovec iovecs[num_segs];
+	struct mmsghdr msgs[nb_pkts];
+	unsigned int seg_idx = 0, nb_iovs = 0;
+	uint64_t num_rx_bytes = 0;
+	int ret;
+
+	/* Allocate worst case number of buffers to be used. */
+	if (rte_pktmbuf_alloc_bulk(mpool, bufs, num_segs) != 0) {
+		PMD_LOG(ERR, "alloc mbuf failed");
+		++udp_q->nobufs;
+		return 0;
+	}
+
+	/* Initialize the multi-packet headers and link the mbufs per packet */
+	memset(msgs, 0, sizeof(msgs));
+	for (uint16_t i = 0; i < nb_pkts; i++) {
+		msgs[i].msg_hdr.msg_iov    = &iovecs[nb_iovs];
+		msgs[i].msg_hdr.msg_iovlen = segs_per_pkt;
+
+		for (unsigned int n = 0; n < segs_per_pkt; n++, nb_iovs++) {
+			struct rte_mbuf *mb = bufs[nb_iovs];
+
+			iovecs[nb_iovs].iov_base = rte_pktmbuf_mtod(mb, void *);
+			iovecs[nb_iovs].iov_len = rte_pktmbuf_tailroom(mb);
+		}
+	}
+	assert(nb_iovs == num_segs);
+
+	ret = recvmmsg(udp_q->sock_fd, msgs, nb_pkts, 0, NULL);
+	if (ret < 0) {
+		if (!(errno == EWOULDBLOCK || errno == EINTR))
+			PMD_LOG(ERR, "recv failed: %s", strerror(errno));
+
+		rte_pktmbuf_free_bulk(bufs, num_segs);
+		return 0;
+	}
+	PMD_LOG(DEBUG, "recvmmsg returned %d", ret);
+
+	/* Adjust mbuf length and segments based on result. */
+	for (int i = 0; i < ret; i++) {
+		struct rte_mbuf **top = &pkts[i];
+		struct rte_mbuf *m0, *mb;
+		unsigned int unfilled;
+		size_t len;
+
+		/* Number of bytes in this packet */
+		len = msgs[i].msg_len;
+		num_rx_bytes += len;
+
+		m0 = mb = bufs[seg_idx];
+		m0->pkt_len = len;
+		m0->port = port_id;
+		m0->nb_segs = 0;
+
+		while (len > 0) {
+			mb->data_len  = RTE_MIN(len,
+						rte_pktmbuf_tailroom(mb));
+			len -= mb->data_len;
+			*top = mb;
+			top = &mb->next;
+
+			++m0->nb_segs;
+			mb = bufs[++seg_idx];
+
+		}
+		*top = NULL;
+
+		/* Drop rest of chain */
+		unfilled = segs_per_pkt - m0->nb_segs;
+		if (unfilled > 0) {
+			rte_pktmbuf_free_bulk(bufs + seg_idx, unfilled);
+			seg_idx += unfilled;
+		}
+	}
+
+	udp_q->pkts += ret;
+	udp_q->bytes += num_rx_bytes;
+
+	/* Free any unused buffers */
+	if (seg_idx < num_segs)
+		rte_pktmbuf_free_bulk(bufs + seg_idx, num_segs - seg_idx);
+
+	return ret;
+}
+
+/*
+ * Send mbufs over UDP socket.
+ */
+static uint16_t
+eth_udp_tx(void *queue, struct rte_mbuf **bufs, uint16_t nb_bufs)
+{
+	struct udp_queue *udp_q = queue;
+	struct iovec iovecs[nb_bufs * RTE_MBUF_MAX_NB_SEGS];
+	struct mmsghdr msgs[nb_bufs];
+	unsigned int iov_iter;
+	int ret;
+
+	memset(msgs, 0, sizeof(msgs));
+	iov_iter = 0;
+	for (uint16_t i = 0; i < nb_bufs; i++) {
+		struct rte_mbuf *mb = bufs[i];
+		unsigned int nsegs = mb->nb_segs;
+
+		msgs[i].msg_hdr.msg_iov    = &iovecs[iov_iter];
+		msgs[i].msg_hdr.msg_iovlen = nsegs;
+
+		for (unsigned int n = 0; n < nsegs; n++) {
+			iovecs[iov_iter].iov_base = rte_pktmbuf_mtod(mb, void *);
+			iovecs[iov_iter].iov_len = rte_pktmbuf_tailroom(mb);
+			iov_iter++;
+			mb = mb->next;
+		}
+		assert(mb == NULL);
+	}
+
+	ret = sendmmsg(udp_q->sock_fd, msgs, nb_bufs, 0);
+	if (ret < 0) {
+		if (!(errno == EWOULDBLOCK || errno == EINTR))
+			PMD_LOG(ERR, "sendmmsg failed: %s", strerror(errno));
+		ret = 0;
+	} else {
+		uint64_t num_tx_bytes = 0;
+
+		for (int i = 0; i < ret; i++)
+			num_tx_bytes += msgs[i].msg_len;
+
+		udp_q->pkts += ret;
+		udp_q->bytes += num_tx_bytes;
+	}
+
+	if (ret < nb_bufs)
+		rte_pktmbuf_free_bulk(bufs + ret, nb_bufs - ret);
+
+	return ret;
+}
+
+static int
+eth_dev_configure(struct rte_eth_dev *dev __rte_unused)
+{
+	return 0;
+}
+
+static int
+eth_dev_start(struct rte_eth_dev *dev)
+{
+	dev->data->dev_link.link_status = RTE_ETH_LINK_UP;
+	return 0;
+}
+
+static int
+eth_dev_stop(struct rte_eth_dev *dev)
+{
+	struct pmd_internals *internal = dev->data->dev_private;
+	unsigned int i;
+
+
+	for (i = 0; i < dev->data->nb_tx_queues; i++)
+		internal->tx_queues[i].sock_fd = -1;
+
+	for (i = 0; i < dev->data->nb_rx_queues; i++) {
+		struct udp_queue *rxq = &internal->rx_queues[i];
+
+		close(rxq->sock_fd);
+		rxq->sock_fd = -1;
+	}
+
+	dev->data->dev_link.link_status = RTE_ETH_LINK_DOWN;
+	return 0;
+}
+
+static int create_socket(struct pmd_internals *internals)
+{
+	socklen_t addrlen;
+	int family, sock_fd, on = 1;
+
+	family = internals->local.ss_family;
+	sock_fd = socket(family, SOCK_DGRAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0);
+	if (sock_fd < 0) {
+		PMD_LOG(ERR, "socket(): failed %s", strerror(errno));
+		return -1;
+	}
+
+	if (setsockopt(sock_fd, SOL_SOCKET, SO_REUSEPORT, &on, sizeof(on)) < 0) {
+		PMD_LOG(ERR, "setsockopt(SO_REUSEPORT): failed %s", strerror(errno));
+		goto fail;
+	}
+
+	if (family == AF_INET6)
+		addrlen = sizeof(struct sockaddr_in6);
+	else
+		addrlen = sizeof(struct sockaddr_in);
+
+
+	/* if address family is not set, then local address not specified */
+	if (bind(sock_fd, (struct sockaddr *)&internals->local, addrlen) < 0) {
+		PMD_LOG(ERR, "bind: failed %s", strerror(errno));
+		goto fail;
+	}
+
+	if (connect(sock_fd, (struct sockaddr *)&internals->remote, addrlen) < 0) {
+		PMD_LOG(ERR, "connect: failed %s", strerror(errno));
+		goto fail;
+	}
+
+	/* Get actual local family to reuse same address */
+	addrlen = sizeof(internals->local);
+	if (getsockname(sock_fd, (struct sockaddr *)&internals->local, &addrlen) < 0) {
+		PMD_LOG(ERR, "getsockname failed %s", strerror(errno));
+		goto fail;
+	}
+
+	addrlen = sizeof(internals->remote);
+	if (getpeername(sock_fd, (struct sockaddr *)&internals->remote, &addrlen) < 0) {
+		PMD_LOG(ERR, "getsockname failed %s", strerror(errno));
+		goto fail;
+	}
+
+	return sock_fd;
+
+fail:
+	close(sock_fd);
+	return -1;
+}
+
+static int
+eth_rx_queue_setup(struct rte_eth_dev *dev, uint16_t rx_queue_id,
+		uint16_t nb_rx_desc __rte_unused,
+		unsigned int socket_id __rte_unused,
+		const struct rte_eth_rxconf *rx_conf __rte_unused,
+		struct rte_mempool *mb_pool)
+{
+	struct pmd_internals *internals = dev->data->dev_private;
+	struct udp_queue *rx_q = &internals->rx_queues[rx_queue_id];
+
+	dev->data->rx_queues[rx_queue_id] = rx_q;
+	rx_q->internals = internals;
+	rx_q->queue_id = rx_queue_id;
+	rx_q->mb_pool = mb_pool;
+
+	if (rx_queue_id == 0)
+		rx_q->sock_fd = internals->sock_fd;
+	else
+		rx_q->sock_fd = create_socket(internals);
+
+	return (rx_q->sock_fd < 0) ? -1 : 0;
+}
+
+static void
+eth_rx_queue_release(struct rte_eth_dev *dev, uint16_t rx_queue_id)
+{
+	struct pmd_internals *internals = dev->data->dev_private;
+	struct udp_queue *rx_q = &internals->rx_queues[rx_queue_id];
+
+	if (rx_q->queue_id > 0)
+		close(rx_q->sock_fd);
+
+	rx_q->sock_fd = -1;
+}
+
+static int
+eth_tx_queue_setup(struct rte_eth_dev *dev, uint16_t tx_queue_id,
+		uint16_t nb_tx_desc __rte_unused,
+		unsigned int socket_id __rte_unused,
+		const struct rte_eth_txconf *tx_conf __rte_unused)
+{
+	struct pmd_internals *internals = dev->data->dev_private;
+	struct udp_queue *tx_q = &internals->tx_queues[tx_queue_id];
+
+	dev->data->tx_queues[tx_queue_id] = tx_q;
+	tx_q->queue_id = tx_queue_id;
+	tx_q->internals = internals;
+	tx_q->sock_fd = internals->sock_fd;
+
+	return 0;
+}
+
+static void
+eth_tx_queue_release(struct rte_eth_dev *dev, uint16_t tx_queue_id)
+{
+	struct pmd_internals *internals = dev->data->dev_private;
+	struct udp_queue *tx_q = &internals->tx_queues[tx_queue_id];
+
+	tx_q->sock_fd = -1;
+}
+
+static int
+eth_mtu_set(struct rte_eth_dev *dev __rte_unused, uint16_t mtu __rte_unused)
+{
+	return 0;
+}
+
+static int
+eth_dev_info(struct rte_eth_dev *dev __rte_unused,
+	     struct rte_eth_dev_info *dev_info)
+{
+	dev_info->max_mac_addrs = 1;
+	dev_info->max_rx_pktlen = UINT16_MAX;
+	dev_info->max_rx_queues = RTE_MAX_QUEUES_PER_PORT;
+	dev_info->max_tx_queues = RTE_MAX_QUEUES_PER_PORT;
+	dev_info->min_rx_bufsize = 0;
+	dev_info->tx_offload_capa = RTE_ETH_TX_OFFLOAD_MULTI_SEGS;
+	dev_info->rx_offload_capa = RTE_ETH_RX_OFFLOAD_SCATTER;
+
+	return 0;
+}
+
+static int
+eth_link_update(struct rte_eth_dev *dev __rte_unused,
+		int wait_to_complete __rte_unused)
+{
+	return 0;
+}
+
+static int
+eth_mac_address_set(__rte_unused struct rte_eth_dev *dev,
+		    __rte_unused struct rte_ether_addr *addr)
+{
+	return 0;
+}
+
+static int
+eth_stats_get(struct rte_eth_dev *dev, struct rte_eth_stats *stats)
+{
+	const struct pmd_internals *internal = dev->data->dev_private;
+	unsigned int i, num_stats;
+
+	num_stats = RTE_MIN((unsigned)RTE_ETHDEV_QUEUE_STAT_CNTRS,
+			    dev->data->nb_rx_queues);
+	for (i = 0; i < num_stats; i++) {
+		const struct udp_queue *q = &internal->rx_queues[i];
+
+		stats->q_ipackets[i] = q->pkts;
+		stats->ipackets += q->pkts;
+		stats->q_ibytes[i] += q->bytes;
+		stats->ibytes += q->bytes;
+		stats->rx_nombuf += q->nobufs;
+	}
+
+	num_stats = RTE_MIN((unsigned)RTE_ETHDEV_QUEUE_STAT_CNTRS,
+			    dev->data->nb_tx_queues);
+	for (i = 0; i < num_stats; i++) {
+		const struct udp_queue *q = &internal->tx_queues[i];
+
+		stats->q_opackets[i] = q->pkts;
+		stats->opackets += q->pkts;
+		stats->q_obytes[i] += q->bytes;
+		stats->obytes += q->bytes;
+	}
+
+	return 0;
+}
+
+static int
+eth_stats_reset(struct rte_eth_dev *dev)
+{
+	struct pmd_internals *internal = dev->data->dev_private;
+	unsigned int i;
+
+	for (i = 0; i < RTE_DIM(internal->rx_queues); i++) {
+		struct udp_queue *q = &internal->rx_queues[i];
+
+		q->pkts = 0;
+		q->bytes = 0;
+		q->nobufs = 0;
+	}
+
+
+	for (i = 0; i < RTE_DIM(internal->tx_queues); i++) {
+		struct udp_queue *q = &internal->tx_queues[i];
+
+		q->pkts = 0;
+		q->bytes = 0;
+		q->nobufs = 0;
+	}
+
+	return 0;
+}
+
+static const struct eth_dev_ops ops = {
+	.dev_start = eth_dev_start,
+	.dev_stop = eth_dev_stop,
+	.dev_configure = eth_dev_configure,
+	.dev_infos_get = eth_dev_info,
+	.rx_queue_setup = eth_rx_queue_setup,
+	.tx_queue_setup = eth_tx_queue_setup,
+	.rx_queue_release = eth_rx_queue_release,
+	.tx_queue_release = eth_tx_queue_release,
+	.mtu_set = eth_mtu_set,
+	.link_update = eth_link_update,
+	.mac_addr_set = eth_mac_address_set,
+	.stats_get = eth_stats_get,
+	.stats_reset = eth_stats_reset,
+};
+
+static int
+parse_parameters(struct pmd_internals *internals, const char *params)
+{
+	static const char * const valid_args[] = {
+		"local", "remote", NULL
+	};
+	struct rte_kvargs *kvlist;
+	int ret;
+
+	if (params == NULL && params[0] == '\0')
+		return 0;
+
+	PMD_LOG(INFO, "parameters \"%s\"", params);
+	kvlist = rte_kvargs_parse(params, valid_args);
+	if (kvlist == NULL)
+		return -1;
+
+	ret = rte_kvargs_process(kvlist, ETH_UDP_LOCAL_ARG,
+				 &get_address_arg, &internals->local);
+	if (ret < 0)
+		goto out;
+
+	ret = rte_kvargs_process(kvlist, ETH_UDP_REMOTE_ARG,
+				 &get_address_arg, &internals->remote);
+
+out:
+	rte_kvargs_free(kvlist);
+	return ret;
+}
+
+static int
+validate_parameters(struct pmd_internals *internals)
+{
+	int family = internals->remote.ss_family;
+
+	if (family == AF_UNSPEC) {
+		PMD_LOG(ERR, "remote address required");
+		return -EINVAL;
+	}
+
+	/*
+	 * if no local address is specified,
+	 * then use same port and  and let kernel choose.
+	 */
+	if (internals->local.ss_family == AF_UNSPEC) {
+		internals->local.ss_family = family;
+	} else if (internals->local.ss_family != family) {
+		PMD_LOG(ERR, "Local and remote address family differ");
+		return -EINVAL;
+	}
+
+	return 0;
+}
+
+static int
+rte_pmd_udp_probe(struct rte_vdev_device *dev)
+{
+	struct pmd_internals *internals;
+	struct rte_eth_dev *eth_dev;
+	struct rte_eth_dev_data *data;
+	const char *name;
+	int ret;
+
+	name = rte_vdev_device_name(dev);
+
+	PMD_LOG(INFO, "Initializing pmd_udp for %s", name);
+
+	if (rte_eal_process_type() == RTE_PROC_SECONDARY) {
+		PMD_LOG(ERR, "Secondary not supported");
+		return -ENOTSUP;
+	}
+
+	eth_dev = rte_eth_vdev_allocate(dev, sizeof(*internals));
+	if (!eth_dev)
+		return -ENOMEM;
+
+	internals = eth_dev->data->dev_private;
+	internals->port_id = eth_dev->data->port_id;
+
+	ret = parse_parameters(internals, rte_vdev_device_args(dev));
+	if (ret < 0)
+		goto fail;
+
+	ret = validate_parameters(internals);
+	if (ret < 0)
+		goto fail;
+
+	/*
+	 * Note: first socket is used for transmit and for
+	 * receive queue 0.
+	 */
+	internals->sock_fd = create_socket(internals);
+	if (internals->sock_fd < 0) {
+		ret = errno ? -errno : -EINVAL;
+		goto fail;
+	}
+
+	rte_eth_random_addr(internals->eth_addr.addr_bytes);
+
+	data = eth_dev->data;
+	data->dev_link = pmd_link;
+	data->mac_addrs = &internals->eth_addr;
+	data->promiscuous = 1;
+	data->all_multicast = 1;
+
+	eth_dev->dev_ops = &ops;
+	eth_dev->rx_pkt_burst = eth_udp_rx;
+	eth_dev->tx_pkt_burst = eth_udp_tx;
+
+	rte_eth_dev_probing_finish(eth_dev);
+
+fail:
+	if (ret != 0) {
+		eth_dev->data->mac_addrs = NULL;
+		rte_eth_dev_release_port(eth_dev);
+	}
+
+	return ret;
+}
+
+static int
+rte_pmd_udp_remove(struct rte_vdev_device *dev)
+{
+	struct rte_eth_dev *eth_dev = NULL;
+	const char *name;
+
+	name = rte_vdev_device_name(dev);
+
+	PMD_LOG(INFO, "Closing udp ethdev %s", name);
+
+	/* find the ethdev entry */
+	eth_dev = rte_eth_dev_allocated(name);
+	if (eth_dev == NULL)
+		return -1;
+
+	/* mac_addrs must not be freed alone because part of dev_private */
+	if (rte_eal_process_type() == RTE_PROC_PRIMARY)
+		eth_dev->data->mac_addrs = NULL;
+
+	rte_eth_dev_release_port(eth_dev);
+
+	return 0;
+}
+
+static struct rte_vdev_driver pmd_udp_drv = {
+	.probe = rte_pmd_udp_probe,
+	.remove = rte_pmd_udp_remove,
+};
+
+RTE_PMD_REGISTER_VDEV(net_udp, pmd_udp_drv);
+RTE_PMD_REGISTER_ALIAS(net_udp, eth_udp);
+RTE_PMD_REGISTER_PARAM_STRING(net_udp,
+	"local=<string>"
+	"remote=<string>");
+
+RTE_INIT(eth_udp_init_log)
+{
+	eth_udp_logtype = rte_log_register("pmd.net.udp");
+	if (eth_udp_logtype >= 0)
+		rte_log_set_level(eth_udp_logtype, RTE_LOG_NOTICE);
+}
diff --git a/drivers/net/udp/version.map b/drivers/net/udp/version.map
new file mode 100644
index 000000000000..78c3585d7c6b
--- /dev/null
+++ b/drivers/net/udp/version.map
@@ -0,0 +1,3 @@
+DPDK_23 {
+	local: *;
+};
