diff --git a/include/Makefile.am b/include/Makefile.am
index 738f80708ca0..83a0e5209ed7 100644
--- a/include/Makefile.am
+++ b/include/Makefile.am
@@ -3,6 +3,7 @@ SUBDIRS = libnftnl linux
 noinst_HEADERS = internal.h	\
 		 linux_list.h	\
 		 data_reg.h	\
+		 desc.h		\
 		 expr_ops.h	\
 		 obj.h		\
 		 linux_list.h	\
diff --git a/include/desc.h b/include/desc.h
new file mode 100644
index 000000000000..d78af3528118
--- /dev/null
+++ b/include/desc.h
@@ -0,0 +1,19 @@
+#ifndef _LIBNFTNL_DESC_INTERNAL_H_
+#define _LIBNFTNL_DESC_INTERNAL_H_
+
+struct nftnl_expr_desc {
+	uint32_t	etype;
+	uint32_t	byteorder;
+	uint32_t	len;
+
+	struct expr_ops *ops;
+
+	union {
+		struct {
+			uint32_t desc_id;
+			uint32_t type;
+		} payload;
+	};
+};
+
+#endif
diff --git a/include/expr_ops.h b/include/expr_ops.h
index 7a6aa23f9bd1..77e010bf2a7d 100644
--- a/include/expr_ops.h
+++ b/include/expr_ops.h
@@ -2,14 +2,19 @@
 #define _EXPR_OPS_H_
 
 #include <stdint.h>
+#include <libnftnl/desc.h>
 #include "internal.h"
 
 struct nlattr;
 struct nlmsghdr;
 struct nftnl_expr;
+struct nftnl_expr_desc;
+struct nftnl_udata;
+struct nftnl_udata_buf;
 
 struct expr_ops {
 	const char *name;
+	enum nftnl_expr_desc_type type;
 	uint32_t alloc_len;
 	int	max_attr;
 	void	(*init)(const struct nftnl_expr *e);
@@ -19,9 +24,15 @@ struct expr_ops {
 	int 	(*parse)(struct nftnl_expr *e, struct nlattr *attr);
 	void	(*build)(struct nlmsghdr *nlh, const struct nftnl_expr *e);
 	int	(*snprintf)(char *buf, size_t len, uint32_t flags, const struct nftnl_expr *e);
+	struct {
+		int	(*set)(struct nftnl_expr_desc *uexpr, uint8_t type, const void *data, uint32_t data_len);
+		int	(*build)(struct nftnl_udata_buf *ud, const struct nftnl_expr_desc *uexpr);
+		int	(*parse)(const struct nftnl_udata *attr, struct nftnl_expr_desc *uexpr);
+	} desc;
 };
 
 struct expr_ops *nftnl_expr_ops_lookup(const char *name);
+struct expr_ops *nftnl_expr_ops_lookup_by_type(uint32_t type);
 
 #define nftnl_expr_data(ops) (void *)ops->data
 
diff --git a/include/internal.h b/include/internal.h
index 1f96731589c0..7b058aa2ced8 100644
--- a/include/internal.h
+++ b/include/internal.h
@@ -12,5 +12,6 @@
 #include "expr.h"
 #include "expr_ops.h"
 #include "rule.h"
+#include "desc.h"
 
 #endif /* _LIBNFTNL_INTERNAL_H_ */
diff --git a/include/libnftnl/Makefile.am b/include/libnftnl/Makefile.am
index d846a574f438..3d072ed8642b 100644
--- a/include/libnftnl/Makefile.am
+++ b/include/libnftnl/Makefile.am
@@ -1,4 +1,5 @@
 pkginclude_HEADERS = batch.h		\
+		     desc.h		\
 		     table.h		\
 		     trace.h		\
 		     chain.h		\
diff --git a/include/libnftnl/desc.h b/include/libnftnl/desc.h
new file mode 100644
index 000000000000..1202eb9ddf79
--- /dev/null
+++ b/include/libnftnl/desc.h
@@ -0,0 +1,57 @@
+#ifndef _LIBNFTNL_DESC_H_
+#define _LIBNFTNL_DESC_H_
+
+#include <stdio.h>
+#include <stdint.h>
+#include <stdbool.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* from include/expression.h in libnftables */
+enum nftnl_expr_desc_type {
+	NFTNL_EXPR_UNSPEC	= 0,
+	NFTNL_EXPR_PAYLOAD	= 7,
+	NFTNL_EXPR_EXTHDR,
+	NFTNL_EXPR_META,
+	NFTNL_EXPR_SOCKET,
+	NFTNL_EXPR_OSF,
+	NFTNL_EXPR_CT,
+	NFTNL_EXPR_RT		= 25,
+};
+
+#define NFTNL_DESC_EXPR_BASE		16
+
+enum nftnl_expr_desc_types {
+	NFTNL_DESC_EXPR_TYPE,
+	NFTNL_DESC_EXPR_BYTEORDER,
+	NFTNL_DESC_EXPR_LEN,
+	NFTNL_DESC_EXPR_DATA		= NFTNL_DESC_EXPR_BASE,
+	__NFTNL_DESC_EXPR_MAX
+};
+#define NFTNL_DESC_EXPR_MAX (__NFTNL_DESC_EXPR_MAX - 1)
+
+enum nftnl_expr_desc_payload_types {
+	NFTNL_DESC_PAYLOAD_DESC		= NFTNL_DESC_EXPR_BASE,
+	NFTNL_DESC_PAYLOAD_TYPE
+};
+
+struct nftnl_expr_desc;
+struct nftnl_udata_buf;
+struct nftnl_udata;
+
+struct nftnl_expr_desc *nftnl_expr_desc_alloc(void);
+void nftnl_expr_desc_free(struct nftnl_expr_desc *dexpr);
+int nftnl_expr_desc_set(struct nftnl_expr_desc *dexpr, uint8_t type,
+			const void *data, uint32_t data_len);
+int nftnl_expr_desc_build(struct nftnl_udata_buf *udbuf,
+			  const struct nftnl_expr_desc *dexpr);
+int nftnl_expr_desc_parse(const struct nftnl_udata *attr,
+			  struct nftnl_expr_desc *dexpr);
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif /* _LIBNFTNL_DESC_H_ */
diff --git a/src/Makefile.am b/src/Makefile.am
index c3b0ab974bd2..06e1567ffd13 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -7,6 +7,7 @@ libnftnl_la_LDFLAGS = -Wl,--version-script=$(srcdir)/libnftnl.map	\
 
 libnftnl_la_SOURCES = utils.c		\
 		      batch.c		\
+		      desc.c		\
 		      flowtable.c	\
 		      common.c		\
 		      gen.c		\
diff --git a/src/desc.c b/src/desc.c
new file mode 100644
index 000000000000..d8566ba2ee38
--- /dev/null
+++ b/src/desc.c
@@ -0,0 +1,142 @@
+/*
+ * (C) 2022 by Pablo Neira Ayuso <pablo@netfilter.org>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ */
+#include "internal.h"
+
+#include <time.h>
+#include <endian.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+#include <netinet/in.h>
+
+#include <libmnl/libmnl.h>
+#include <linux/netfilter/nfnetlink.h>
+#include <linux/netfilter/nf_tables.h>
+
+#include <libnftnl/udata.h>
+#include <libnftnl/desc.h>
+
+EXPORT_SYMBOL(nftnl_expr_desc_alloc);
+struct nftnl_expr_desc *nftnl_expr_desc_alloc(void)
+{
+	return calloc(1, sizeof(struct nftnl_expr_desc));
+}
+
+EXPORT_SYMBOL(nftnl_expr_desc_free);
+void nftnl_expr_desc_free(struct nftnl_expr_desc *dexpr)
+{
+	free(dexpr);
+}
+
+EXPORT_SYMBOL(nftnl_expr_desc_set);
+int nftnl_expr_desc_set(struct nftnl_expr_desc *dexpr, uint8_t type,
+			const void *data, uint32_t data_len)
+{
+	int err = 0;
+
+	switch (type) {
+	case NFTNL_DESC_EXPR_TYPE:
+		memcpy(&dexpr->etype, data, data_len);
+		dexpr->ops = nftnl_expr_ops_lookup_by_type(dexpr->etype);
+		break;
+	case NFTNL_DESC_EXPR_BYTEORDER:
+		memcpy(&dexpr->byteorder, data, data_len);
+		break;
+	case NFTNL_DESC_EXPR_LEN:
+		memcpy(&dexpr->len, data, data_len);
+		break;
+	case NFTNL_DESC_EXPR_DATA:
+		if (dexpr->ops && dexpr->ops->desc.set)
+			err = dexpr->ops->desc.set(dexpr, type, data, data_len);
+		break;
+	default:
+		err = -1;
+		break;
+	}
+
+	return err;
+}
+
+#define NFTNL_UDATA_EXPR_TYPE		0
+#define NFTNL_UDATA_EXPR_BYTEORDER	1
+#define NFTNL_UDATA_EXPR_LEN		2
+#define NFTNL_UDATA_EXPR_DATA		3
+#define NFTNL_UDATA_EXPR_MAX		4
+
+EXPORT_SYMBOL(nftnl_expr_desc_build);
+int nftnl_expr_desc_build(struct nftnl_udata_buf *udbuf,
+			  const struct nftnl_expr_desc *dexpr)
+{
+	struct nftnl_udata *nest;
+	int err = 0;
+
+	nftnl_udata_put_u32(udbuf, NFTNL_UDATA_EXPR_TYPE, dexpr->etype);
+	nftnl_udata_put_u32(udbuf, NFTNL_UDATA_EXPR_BYTEORDER, dexpr->byteorder);
+	nftnl_udata_put_u32(udbuf, NFTNL_UDATA_EXPR_LEN, dexpr->len);
+	if (dexpr->ops && dexpr->ops->desc.build) {
+		nest = nftnl_udata_nest_start(udbuf, NFTNL_UDATA_EXPR_DATA);
+		err = dexpr->ops->desc.build(udbuf, dexpr);
+		nftnl_udata_nest_end(udbuf, nest);
+	}
+
+	return err;
+}
+
+static int nftnl_expr_desc_parse_nested(const struct nftnl_udata *attr, void *data)
+{
+	const struct nftnl_udata **ud = data;
+	uint8_t type = nftnl_udata_type(attr);
+	uint8_t len = nftnl_udata_len(attr);
+
+	switch (type) {
+	case NFTNL_UDATA_EXPR_TYPE:
+	case NFTNL_UDATA_EXPR_BYTEORDER:
+	case NFTNL_UDATA_EXPR_LEN:
+		if (len != sizeof(uint32_t))
+			return -1;
+		break;
+	case NFTNL_UDATA_EXPR_DATA:
+		break;
+	default:
+	        return 0;
+	}
+
+	ud[type] = attr;
+	return 0;
+}
+
+EXPORT_SYMBOL(nftnl_expr_desc_parse);
+int nftnl_expr_desc_parse(const struct nftnl_udata *attr,
+			  struct nftnl_expr_desc *dexpr)
+{
+	const struct nftnl_udata *ud[NFTNL_UDATA_EXPR_MAX + 1] = {};
+	int err;
+
+	err = nftnl_udata_parse(nftnl_udata_get(attr), nftnl_udata_len(attr),
+				nftnl_expr_desc_parse_nested, ud);
+	if (err < 0)
+		return -1;
+
+	err = 0;
+	if (ud[NFTNL_UDATA_EXPR_TYPE]) {
+		dexpr->etype = nftnl_udata_get_u32(ud[NFTNL_UDATA_EXPR_TYPE]);
+		dexpr->ops = nftnl_expr_ops_lookup_by_type(dexpr->etype);
+	}
+	if (ud[NFTNL_UDATA_EXPR_BYTEORDER])
+		dexpr->byteorder = nftnl_udata_get_u32(ud[NFTNL_UDATA_EXPR_BYTEORDER]);
+	if (ud[NFTNL_UDATA_EXPR_LEN])
+		dexpr->len = nftnl_udata_get_u32(ud[NFTNL_UDATA_EXPR_LEN]);
+	if (ud[NFTNL_UDATA_EXPR_DATA]) {
+		if (dexpr->ops && dexpr->ops->desc.parse)
+			err = dexpr->ops->desc.parse(ud[NFTNL_UDATA_EXPR_DATA], dexpr);
+	}
+
+	return err;
+}
diff --git a/src/expr/payload.c b/src/expr/payload.c
index 82747ec8994f..afa0f2451155 100644
--- a/src/expr/payload.c
+++ b/src/expr/payload.c
@@ -23,6 +23,7 @@
 
 #include <libnftnl/expr.h>
 #include <libnftnl/rule.h>
+#include <libnftnl/udata.h>
 
 struct nftnl_expr_payload {
 	enum nft_registers	sreg;
@@ -251,8 +252,83 @@ nftnl_expr_payload_snprintf(char *buf, size_t len,
 				payload->offset, payload->dreg);
 }
 
+static int nftnl_expr_payload_desc_set(struct nftnl_expr_desc *dexpr,
+				       uint8_t type, const void *data,
+				       uint32_t data_len)
+{
+	switch (type) {
+	case NFTNL_DESC_PAYLOAD_DESC:
+		memcpy(&dexpr->payload.desc_id, data, sizeof(dexpr->payload.desc_id));
+		break;
+	case NFTNL_DESC_PAYLOAD_TYPE:
+		memcpy(&dexpr->payload.type, data, sizeof(dexpr->payload.type));
+		break;
+	default:
+		return -1;
+	}
+
+	return 0;
+}
+
+#define NFTNL_UDATA_PAYLOAD_DESC 0
+#define NFTNL_UDATA_PAYLOAD_TYPE 1
+#define NFTNL_UDATA_PAYLOAD_MAX 2
+
+static int nftnl_expr_payload_desc_build(struct nftnl_udata_buf *udbuf,
+					 const struct nftnl_expr_desc *dexpr)
+{
+	nftnl_udata_put_u32(udbuf, NFTNL_UDATA_PAYLOAD_DESC,
+			    dexpr->payload.desc_id);
+	nftnl_udata_put_u32(udbuf, NFTNL_UDATA_PAYLOAD_TYPE,
+			    dexpr->payload.type);
+
+	return 0;
+}
+
+static int payload_parse_udata(const struct nftnl_udata *attr, void *data)
+{
+	const struct nftnl_udata **ud = data;
+	uint8_t type = nftnl_udata_type(attr);
+	uint8_t len = nftnl_udata_len(attr);
+
+	switch (type) {
+	case NFTNL_UDATA_PAYLOAD_DESC:
+	case NFTNL_UDATA_PAYLOAD_TYPE:
+		if (len != sizeof(uint32_t))
+			return -1;
+		break;
+	default:
+		return 0;
+	}
+
+	ud[type] = attr;
+	return 0;
+}
+
+static int nftnl_expr_payload_desc_parse(const struct nftnl_udata *attr,
+					 struct nftnl_expr_desc *dexpr)
+{
+	const struct nftnl_udata *ud[NFTNL_UDATA_PAYLOAD_MAX + 1] = {};
+	int err;
+
+	err = nftnl_udata_parse(nftnl_udata_get(attr), nftnl_udata_len(attr),
+				payload_parse_udata, ud);
+	if (err < 0)
+		return -1;
+
+	if (!ud[NFTNL_UDATA_PAYLOAD_DESC] ||
+	    !ud[NFTNL_UDATA_PAYLOAD_TYPE])
+		return -1;
+
+	dexpr->payload.desc_id = nftnl_udata_get_u32(ud[NFTNL_UDATA_PAYLOAD_DESC]);
+	dexpr->payload.type = nftnl_udata_get_u32(ud[NFTNL_UDATA_PAYLOAD_TYPE]);
+
+	return 0;
+}
+
 struct expr_ops expr_ops_payload = {
 	.name		= "payload",
+	.type		= NFTNL_EXPR_PAYLOAD,
 	.alloc_len	= sizeof(struct nftnl_expr_payload),
 	.max_attr	= NFTA_PAYLOAD_MAX,
 	.set		= nftnl_expr_payload_set,
@@ -260,4 +336,9 @@ struct expr_ops expr_ops_payload = {
 	.parse		= nftnl_expr_payload_parse,
 	.build		= nftnl_expr_payload_build,
 	.snprintf	= nftnl_expr_payload_snprintf,
+	.desc		= {
+		.set	= nftnl_expr_payload_desc_set,
+		.build	= nftnl_expr_payload_desc_build,
+		.parse	= nftnl_expr_payload_desc_parse,
+	},
 };
diff --git a/src/expr_ops.c b/src/expr_ops.c
index 7248e4f98b0a..383414a42619 100644
--- a/src/expr_ops.c
+++ b/src/expr_ops.c
@@ -102,3 +102,16 @@ struct expr_ops *nftnl_expr_ops_lookup(const char *name)
 	}
 	return NULL;
 }
+
+struct expr_ops *nftnl_expr_ops_lookup_by_type(uint32_t type)
+{
+	int i = 0;
+
+	while (expr_ops[i] != NULL) {
+		if (expr_ops[i]->type == type)
+			return expr_ops[i];
+
+		i++;
+	}
+	return NULL;
+}
