diff --git a/Makefile b/Makefile
index 5b040415a12b..e64c65992585 100644
--- a/Makefile
+++ b/Makefile
@@ -55,7 +55,7 @@ WFLAGS += -Wmissing-declarations -Wold-style-definition -Wformat=2
 CFLAGS := $(WFLAGS) $(CCOPTS) -I../include -I../include/uapi $(DEFINES) $(CFLAGS)
 YACCFLAGS = -d -t -v
 
-SUBDIRS=lib ip tc bridge misc netem genl tipc devlink rdma man
+SUBDIRS=lib ip tc bridge misc netem genl tipc devlink rdma dcb man
 
 LIBNETLINK=../lib/libutil.a ../lib/libnetlink.a
 LDLIBS += $(LIBNETLINK)
diff --git a/dcb/Makefile b/dcb/Makefile
new file mode 100644
index 000000000000..9966c8f0bfa4
--- /dev/null
+++ b/dcb/Makefile
@@ -0,0 +1,24 @@
+# SPDX-License-Identifier: GPL-2.0
+include ../config.mk
+
+TARGETS :=
+
+ifeq ($(HAVE_MNL),y)
+
+DCBOBJ = dcb.o
+TARGETS += dcb
+
+endif
+
+all: $(TARGETS) $(LIBS)
+
+dcb: $(DCBOBJ) $(LIBNETLINK)
+	$(QUIET_LINK)$(CC) $^ $(LDFLAGS) $(LDLIBS) -o $@
+
+install: all
+	for i in $(TARGETS); \
+	do install -m 0755 $$i $(DESTDIR)$(SBINDIR); \
+	done
+
+clean:
+	rm -f $(DCBOBJ) $(TARGETS)
diff --git a/dcb/dcb.c b/dcb/dcb.c
new file mode 100644
index 000000000000..5346f0ec4179
--- /dev/null
+++ b/dcb/dcb.c
@@ -0,0 +1,414 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+#include <stdio.h>
+#include <linux/dcbnl.h>
+#include <libmnl/libmnl.h>
+#include <getopt.h>
+
+#include "dcb.h"
+#include "mnl_utils.h"
+#include "namespace.h"
+#include "utils.h"
+#include "version.h"
+
+static int dcb_init(struct dcb *dcb)
+{
+	dcb->buf = malloc(MNL_SOCKET_BUFFER_SIZE);
+	if (dcb->buf == NULL) {
+		perror("Netlink buffer allocation");
+		return -1;
+	}
+
+	dcb->nl = mnlu_socket_open(NETLINK_ROUTE);
+	if (dcb->nl == NULL) {
+		perror("Open netlink socket");
+		goto err_socket_open;
+	}
+
+	new_json_obj_plain(dcb->json_output);
+	return 0;
+
+err_socket_open:
+	free(dcb->buf);
+	return -1;
+}
+
+static void dcb_fini(struct dcb *dcb)
+{
+	delete_json_obj_plain();
+	mnl_socket_close(dcb->nl);
+}
+
+static struct dcb *dcb_alloc(void)
+{
+	struct dcb *dcb;
+
+	dcb = calloc(1, sizeof(*dcb));
+	if (!dcb)
+		return NULL;
+	return dcb;
+}
+
+static void dcb_free(struct dcb *dcb)
+{
+	free(dcb);
+}
+
+struct dcb_get_attribute {
+	struct dcb *dcb;
+	int attr;
+	void *data;
+	size_t data_len;
+};
+
+static int dcb_get_attribute_attr_ieee_cb(const struct nlattr *attr, void *data)
+{
+	struct dcb_get_attribute *ga = data;
+	uint16_t len;
+
+	if (mnl_attr_get_type(attr) != ga->attr)
+		return MNL_CB_OK;
+
+	len = mnl_attr_get_payload_len(attr);
+	if (len != ga->data_len) {
+		fprintf(stderr, "Wrong len %d, expected %zd\n", len, ga->data_len);
+		return MNL_CB_ERROR;
+	}
+
+	memcpy(ga->data, mnl_attr_get_payload(attr), ga->data_len);
+	return MNL_CB_STOP;
+}
+
+static int dcb_get_attribute_attr_cb(const struct nlattr *attr, void *data)
+{
+	if (mnl_attr_get_type(attr) != DCB_ATTR_IEEE)
+		return MNL_CB_OK;
+
+	return mnl_attr_parse_nested(attr, dcb_get_attribute_attr_ieee_cb, data);
+}
+
+static int dcb_get_attribute_cb(const struct nlmsghdr *nlh, void *data)
+{
+	return mnl_attr_parse(nlh, sizeof(struct dcbmsg), dcb_get_attribute_attr_cb, data);
+}
+
+static int dcb_set_attribute_attr_cb(const struct nlattr *attr, void *data)
+{
+	uint16_t len;
+	uint8_t err;
+
+	if (mnl_attr_get_type(attr) != DCB_ATTR_IEEE)
+		return MNL_CB_OK;
+
+	len = mnl_attr_get_payload_len(attr);
+	if (len != 1) {
+		fprintf(stderr, "Response attribute expected to have size 1, not %d\n", len);
+		return MNL_CB_ERROR;
+	}
+
+	err = mnl_attr_get_u8(attr);
+	if (err) {
+		fprintf(stderr, "Error when attempting to set attribute: %s\n",
+			strerror(err));
+		return MNL_CB_ERROR;
+	}
+
+	return MNL_CB_STOP;
+}
+
+static int dcb_set_attribute_cb(const struct nlmsghdr *nlh, void *data)
+{
+	return mnl_attr_parse(nlh, sizeof(struct dcbmsg), dcb_set_attribute_attr_cb, data);
+}
+
+static int dcb_talk(struct dcb *dcb, struct nlmsghdr *nlh, mnl_cb_t cb, void *data)
+{
+	int ret;
+
+	ret = mnl_socket_sendto(dcb->nl, nlh, nlh->nlmsg_len);
+	if (ret < 0) {
+		perror("mnl_socket_sendto");
+		return -1;
+	}
+
+	return mnlu_socket_recv_run(dcb->nl, nlh->nlmsg_seq, dcb->buf, MNL_SOCKET_BUFFER_SIZE,
+				    cb, data);
+}
+
+static struct nlmsghdr *dcb_prepare(struct dcb *dcb, const char *dev,
+				    uint32_t nlmsg_type, uint8_t dcb_cmd)
+{
+	struct dcbmsg dcbm = {
+		.cmd = dcb_cmd,
+	};
+	struct nlmsghdr *nlh;
+
+	nlh = mnlu_msg_prepare(dcb->buf, nlmsg_type, NLM_F_REQUEST, &dcbm, sizeof(dcbm));
+	mnl_attr_put_strz(nlh, DCB_ATTR_IFNAME, dev);
+	return nlh;
+}
+
+int dcb_get_attribute(struct dcb *dcb, const char *dev, int attr, void *data, size_t data_len)
+{
+	struct dcb_get_attribute ga;
+	struct nlmsghdr *nlh;
+	int ret;
+
+	nlh = dcb_prepare(dcb, dev, RTM_GETDCB, DCB_CMD_IEEE_GET);
+
+	ga = (struct dcb_get_attribute) {
+		.dcb = dcb,
+		.attr = attr,
+		.data = data,
+		.data_len = data_len,
+	};
+	ret = dcb_talk(dcb, nlh, dcb_get_attribute_cb, &ga);
+	if (ret) {
+		perror("Attribute read");
+		return ret;
+	}
+	return 0;
+}
+
+int dcb_set_attribute(struct dcb *dcb, const char *dev, int attr, const void *data, size_t data_len)
+{
+	struct nlmsghdr *nlh;
+	struct nlattr *nest;
+	int ret;
+
+	nlh = dcb_prepare(dcb, dev, RTM_GETDCB, DCB_CMD_IEEE_SET);
+
+	nest = mnl_attr_nest_start(nlh, DCB_ATTR_IEEE);
+	mnl_attr_put(nlh, attr, data_len, data);
+	mnl_attr_nest_end(nlh, nest);
+
+	ret = dcb_talk(dcb, nlh, dcb_set_attribute_cb, NULL);
+	if (ret) {
+		perror("Attribute write");
+		return ret;
+	}
+	return 0;
+}
+
+void dcb_print_array_u8(const __u8 *array, size_t size)
+{
+	SPRINT_BUF(b);
+	size_t i;
+
+	for (i = 0; i < size; i++) {
+		snprintf(b, sizeof(b), "%zd:%%d ", i);
+		print_uint(PRINT_ANY, NULL, b, array[i]);
+	}
+}
+
+void dcb_print_array_kw(const __u8 *array, size_t array_size,
+			const char *const kw[], size_t kw_size)
+{
+	SPRINT_BUF(b);
+	size_t i;
+
+	for (i = 0; i < array_size; i++) {
+		__u8 emt = array[i];
+
+		snprintf(b, sizeof(b), "%zd:%%s ", i);
+		if (emt < kw_size && kw[emt])
+			print_string(PRINT_ANY, NULL, b, kw[emt]);
+		else
+			print_string(PRINT_ANY, NULL, b, "???");
+	}
+}
+
+void dcb_print_named_array(const char *json_name, const char *fp_name,
+			   const __u8 *array, size_t size,
+			   void (*print_array)(const __u8 *, size_t))
+{
+	open_json_array(PRINT_JSON, json_name);
+	print_string(PRINT_FP, NULL, "%s ", fp_name);
+	print_array(array, size);
+	close_json_array(PRINT_JSON, json_name);
+}
+
+int dcb_parse_mapping(const char *what_key, __u32 key, __u32 max_key,
+		      const char *what_value, __u32 value, __u32 max_value,
+		      void (*set_array)(__u32 index, __u32 value, void *data),
+		      void *set_array_data)
+{
+	bool is_all = key == (__u32) -1;
+
+	if (!is_all && key > max_key) {
+		fprintf(stderr, "In %s:%s mapping, %s is expected to be 0..%d\n",
+			what_key, what_value, what_key, max_key);
+		return -EINVAL;
+	}
+
+	if (value > max_value) {
+		fprintf(stderr, "In %s:%s mapping, %s is expected to be 0..%d\n",
+			what_key, what_value, what_value, max_value);
+		return -EINVAL;
+	}
+
+	if (is_all) {
+		for (key = 0; key < max_key; key++)
+			set_array(key, value, set_array_data);
+	} else {
+		set_array(key, value, set_array_data);
+	}
+
+	return 0;
+}
+
+void dcb_set_u8(__u32 key, __u32 value, void *data)
+{
+	__u8 *array = data;
+
+	array[key] = value;
+}
+
+int dcb_cmd_parse_dev(struct dcb *dcb, int argc, char **argv,
+		      int (*and_then)(struct dcb *dcb, const char *dev,
+				      int argc, char **argv),
+		      void (*help)(void))
+{
+	const char *dev;
+
+	if (!argc || matches(*argv, "help") == 0) {
+		help();
+		return 0;
+	} else if (matches(*argv, "dev") == 0) {
+		NEXT_ARG();
+		dev = *argv;
+		if (check_ifname(dev)) {
+			invarg("not a valid ifname", *argv);
+			return -EINVAL;
+		}
+		NEXT_ARG_FWD();
+		return and_then(dcb, dev, argc, argv);
+	} else {
+		fprintf(stderr, "Expected `dev DEV', not `%s'", *argv);
+		help();
+		return -EINVAL;
+	}
+}
+
+static void dcb_help(void)
+{
+	fprintf(stderr,
+		"Usage: dcb [ OPTIONS ] OBJECT { COMMAND | help }\n"
+		"       dcb [ -f | --force ] { -b | --batch } filename [ -N | --Netns ] netnsname\n"
+		"where  OBJECT :=\n"
+		"       OPTIONS := [ -V | --Version | -j | --json | -p | --pretty | -v | --verbose ]\n");
+}
+
+static int dcb_cmd(struct dcb *dcb, int argc, char **argv)
+{
+	if (!argc || matches(*argv, "help") == 0) {
+		dcb_help();
+		return 0;
+	}
+
+	fprintf(stderr, "Object \"%s\" is unknown\n", *argv);
+	return -ENOENT;
+}
+
+static int dcb_batch_cmd(int argc, char *argv[], void *data)
+{
+	struct dcb *dcb = data;
+
+	return dcb_cmd(dcb, argc, argv);
+}
+
+static int dcb_batch(struct dcb *dcb, const char *name, bool force)
+{
+	return do_batch(name, force, dcb_batch_cmd, dcb);
+}
+
+int main(int argc, char **argv)
+{
+	static const struct option long_options[] = {
+		{ "Version",		no_argument,		NULL, 'V' },
+		{ "force",		no_argument,		NULL, 'f' },
+		{ "batch",		required_argument,	NULL, 'b' },
+		{ "json",		no_argument,		NULL, 'j' },
+		{ "pretty",		no_argument,		NULL, 'p' },
+		{ "Netns",		required_argument,	NULL, 'N' },
+		{ "help",		no_argument,		NULL, 'h' },
+		{ NULL, 0, NULL, 0 }
+	};
+	const char *batch_file = NULL;
+	bool force = false;
+	struct dcb *dcb;
+	int opt;
+	int err;
+	int ret;
+
+	dcb = dcb_alloc();
+	if (!dcb) {
+		fprintf(stderr, "Failed to allocate memory for dcb\n");
+		return EXIT_FAILURE;
+	}
+
+	while ((opt = getopt_long(argc, argv, "b:c::fhjnpvN:V",
+				  long_options, NULL)) >= 0) {
+
+		switch (opt) {
+		case 'V':
+			printf("dcb utility, iproute2-%s\n", version);
+			ret = EXIT_SUCCESS;
+			goto dcb_free;
+		case 'f':
+			force = true;
+			break;
+		case 'b':
+			batch_file = optarg;
+			break;
+		case 'j':
+			dcb->json_output = true;
+			break;
+		case 'p':
+			pretty = true;
+			break;
+		case 'N':
+			if (netns_switch(optarg)) {
+				ret = EXIT_FAILURE;
+				goto dcb_free;
+			}
+			break;
+		case 'h':
+			dcb_help();
+			return 0;
+		default:
+			fprintf(stderr, "Unknown option.\n");
+			dcb_help();
+			ret = EXIT_FAILURE;
+			goto dcb_free;
+		}
+	}
+
+	argc -= optind;
+	argv += optind;
+
+	err = dcb_init(dcb);
+	if (err) {
+		ret = EXIT_FAILURE;
+		goto dcb_free;
+	}
+
+	if (batch_file)
+		err = dcb_batch(dcb, batch_file, force);
+	else
+		err = dcb_cmd(dcb, argc, argv);
+
+	if (err) {
+		ret = EXIT_FAILURE;
+		goto dcb_fini;
+	}
+
+	ret = EXIT_SUCCESS;
+
+dcb_fini:
+	dcb_fini(dcb);
+dcb_free:
+	dcb_free(dcb);
+
+	return ret;
+}
diff --git a/dcb/dcb.h b/dcb/dcb.h
new file mode 100644
index 000000000000..a09d102a7fc6
--- /dev/null
+++ b/dcb/dcb.h
@@ -0,0 +1,39 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#ifndef __DCB_H__
+#define __DCB_H__ 1
+
+#include <stdbool.h>
+#include <stddef.h>
+
+/* dcb.c */
+
+struct dcb {
+	char *buf;
+	struct mnl_socket *nl;
+	bool json_output;
+};
+
+int dcb_parse_mapping(const char *what_key, __u32 key, __u32 max_key,
+		      const char *what_value, __u32 value, __u32 max_value,
+		      void (*set_array)(__u32 index, __u32 value, void *data),
+		      void *set_array_data);
+int dcb_cmd_parse_dev(struct dcb *dcb, int argc, char **argv,
+		      int (*and_then)(struct dcb *dcb, const char *dev,
+				      int argc, char **argv),
+		      void (*help)(void));
+
+void dcb_set_u8(__u32 key, __u32 value, void *data);
+
+int dcb_get_attribute(struct dcb *dcb, const char *dev, int attr,
+		      void *data, size_t data_len);
+int dcb_set_attribute(struct dcb *dcb, const char *dev, int attr,
+		      const void *data, size_t data_len);
+
+void dcb_print_named_array(const char *json_name, const char *fp_name,
+			   const __u8 *array, size_t size,
+			   void (*print_array)(const __u8 *, size_t));
+void dcb_print_array_u8(const __u8 *array, size_t size);
+void dcb_print_array_kw(const __u8 *array, size_t array_size,
+			const char *const kw[], size_t kw_size);
+
+#endif /* __DCB_H__ */
diff --git a/man/man8/dcb.8 b/man/man8/dcb.8
new file mode 100644
index 000000000000..19433bfb906d
--- /dev/null
+++ b/man/man8/dcb.8
@@ -0,0 +1,103 @@
+.TH DCB 8 "19 October 2020" "iproute2" "Linux"
+.SH NAME
+dcb \- show / manipulate DCB (Data Center Bridging) settings
+.SH SYNOPSIS
+.sp
+.ad l
+.in +8
+
+.ti -8
+.B dcb
+.RB "[ " -force " ] "
+.BI "-batch " filename
+.sp
+
+.ti -8
+.B dcb
+.RI "[ " OPTIONS " ] "
+.B help
+.sp
+
+.SH OPTIONS
+
+.TP
+.BR "\-V" , " --Version"
+Print the version of the
+.B dcb
+utility and exit.
+
+.TP
+.BR "\-b", " --batch " <FILENAME>
+Read commands from provided file or standard input and invoke them. First
+failure will cause termination of dcb.
+
+.TP
+.BR "\-f", " --force"
+Don't terminate dcb on errors in batch mode. If there were any errors during
+execution of the commands, the application return code will be non zero.
+
+.TP
+.BR "\-j" , " --json"
+Generate JSON output.
+
+.TP
+.BR "\-p" , " --pretty"
+When combined with -j generate a pretty JSON output.
+
+.SH OBJECTS
+
+.SH COMMANDS
+
+A \fICOMMAND\fR specifies the action to perform on the object. The set of
+possible actions depends on the object type. As a rule, it is possible to
+.B show
+objects and to invoke topical
+.B help,
+which prints a list of available commands and argument syntax conventions.
+
+.SH ARRAY PARAMETERS
+
+Like commands, specification of parameters is in the domain of individual
+objects (and their commands) as well. However, much of the DCB interface
+revolves around arrays of fixed size that specify one value per some key, such
+as per traffic class or per priority. There is therefore a single syntax for
+adjusting elements of these arrays. It consists of a series of
+\fIKEY\fB:\fIVALUE\fR pairs, where the meaning of the individual keys and values
+depends on the parameter.
+
+The elements are evaluated in order from left to right, and the latter ones
+override the earlier ones. The elements that are not specified on the command
+line are queried from the kernel and their current value is retained.
+
+As an example, take a made-up parameter tc-juju, which can be set to charm
+traffic in a given TC with either good luck or bad luck. \fIKEY\fR can therefore
+be 0..7 (as is usual for TC numbers in DCB), and \fIVALUE\fR either of
+\fBnone\fR, \fBgood\fR, and \fBbad\fR. An example of changing a juju value of
+TCs 0 and 7, while leaving all other intact, would then be:
+
+.P
+# dcb foo set dev eth0 tc-juju 0:good 7:bad
+
+A special key, \fBall\fR, is recognized which sets the same value to all array
+elements. This can be combined with the usual single-element syntax. E.g. in the
+following, the juju of all keys is set to \fBnone\fR, except 0 and 7, which have
+other values:
+
+.P
+# dcb foo set dev eth0 tc-juju all:none 0:good 7:bad
+
+.SH EXIT STATUS
+Exit status is 0 if command was successful or a positive integer upon failure.
+
+.SH SEE ALSO
+.BR dcb-ets (8)
+.br
+
+.SH REPORTING BUGS
+Report any bugs to the Network Developers mailing list
+.B <netdev@vger.kernel.org>
+where the development and maintenance is primarily done.
+You do not have to be subscribed to the list to send a message there.
+
+.SH AUTHOR
+Petr Machata <me@pmachata.org>
