diff --git a/Makefile.am b/Makefile.am
index 6a4d7083919e..a412734fffd1 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -22,7 +22,7 @@ endif
 if ETHTOOL_ENABLE_NETLINK
 ethtool_SOURCES += \
 		  netlink/netlink.c netlink/netlink.h netlink/extapi.h \
-		  netlink/strset.c netlink/strset.h \
+		  netlink/strset.c netlink/strset.h netlink/monitor.c \
 		  uapi/linux/ethtool_netlink.h \
 		  uapi/linux/netlink.h uapi/linux/genetlink.h
 ethtool_CFLAGS += @MNL_CFLAGS@
diff --git a/ethtool.c b/ethtool.c
index e8044a6af76c..b7f40d2f3826 100644
--- a/ethtool.c
+++ b/ethtool.c
@@ -5294,6 +5294,9 @@ static int show_usage(struct cmd_context *ctx)
 		if (args[i].opthelp)
 			fputs(args[i].opthelp, stdout);
 	}
+#ifdef ETHTOOL_ENABLE_NETLINK
+	monitor_usage();
+#endif
 
 	return 0;
 }
@@ -5335,6 +5338,20 @@ int main(int argc, char **argp)
 	argp++;
 	argc--;
 
+#ifdef ETHTOOL_ENABLE_NETLINK
+	if (*argp && !strcmp(*argp, "--monitor")) {
+		if (netlink_init(&ctx)) {
+			fprintf(stderr,
+				"Option --monitor is only available with netlink.\n");
+			return 1;
+		} else {
+			ctx.argp = ++argp;
+			ctx.argc = --argc;
+			return nl_monitor(&ctx);
+		}
+	}
+#endif
+
 	/* First argument must be either a valid option or a device
 	 * name to get settings for (which we don't expect to begin
 	 * with '-').
diff --git a/netlink/extapi.h b/netlink/extapi.h
index 05ab083d9b9c..827e3732888a 100644
--- a/netlink/extapi.h
+++ b/netlink/extapi.h
@@ -13,4 +13,8 @@ struct nl_context;
 int netlink_init(struct cmd_context *ctx);
 int netlink_done(struct cmd_context *ctx);
 
+int nl_monitor(struct cmd_context *ctx);
+
+void monitor_usage();
+
 #endif /* ETHTOOL_EXTAPI_H__ */
diff --git a/netlink/monitor.c b/netlink/monitor.c
new file mode 100644
index 000000000000..d7ed562356f1
--- /dev/null
+++ b/netlink/monitor.c
@@ -0,0 +1,218 @@
+#include <errno.h>
+
+#include "../internal.h"
+#include "netlink.h"
+#include "strset.h"
+
+static void monitor_newdev(struct nl_context *nlctx, struct nlattr *evattr)
+{
+	const struct nlattr *tb[ETHA_NEWDEV_MAX + 1] = {};
+	DECLARE_ATTR_TB_INFO(tb);
+	const char *devname;
+	int ret;
+
+	ret = mnl_attr_parse_nested(evattr, attr_cb, &tb_info);
+	if (ret < 0)
+		return;
+	if (!tb[ETHA_NEWDEV_DEV])
+		return;
+	devname = get_dev_name(tb[ETHA_NEWDEV_DEV]);
+	if (!devname)
+		return;
+	printf("New device %s registered.\n", devname);
+
+	ret = init_aux_nlctx(nlctx);
+	if (ret < 0)
+		return;
+	load_perdev_strings(nlctx->aux_nlctx, devname);
+}
+
+static void monitor_deldev(struct nl_context *nlctx, struct nlattr *evattr)
+{
+	const struct nlattr *tb[ETHA_DELDEV_MAX + 1] = {};
+	DECLARE_ATTR_TB_INFO(tb);
+	const char *devname;
+	int ret;
+
+	ret = mnl_attr_parse_nested(evattr, attr_cb, &tb_info);
+	if (ret < 0)
+		return;
+	if (!tb[ETHA_DELDEV_DEV])
+		return;
+	devname = get_dev_name(tb[ETHA_DELDEV_DEV]);
+	if (!devname)
+		return;
+	printf("Device %s unregistered.\n", devname);
+
+	free_perdev_strings(devname);
+}
+
+static int monitor_event_cb(const struct nlmsghdr *nlhdr, void *data)
+{
+	struct nl_context *nlctx = data;
+	struct nlattr *evattr;
+
+	mnl_attr_for_each(evattr, nlhdr, GENL_HDRLEN) {
+		switch(mnl_attr_get_type(evattr)) {
+		case ETHA_EVENT_NEWDEV:
+			monitor_newdev(nlctx, evattr);
+			break;
+		case ETHA_EVENT_DELDEV:
+			monitor_deldev(nlctx, evattr);
+			break;
+		}
+	}
+
+	return MNL_CB_OK;
+}
+
+static struct {
+	uint8_t		cmd;
+	mnl_cb_t	cb;
+} monitor_callbacks[] = {
+	{
+		.cmd	= ETHNL_CMD_EVENT,
+		.cb	= monitor_event_cb,
+	},
+};
+
+static int monitor_any_cb(const struct nlmsghdr *nlhdr, void *data)
+{
+	const struct genlmsghdr *ghdr = (const struct genlmsghdr *)(nlhdr + 1);
+	struct nl_context *nlctx = data;
+	unsigned i;
+
+	if (nlctx->filter_cmd && ghdr->cmd != nlctx->filter_cmd)
+		return MNL_CB_OK;
+
+	for (i = 0; i < MNL_ARRAY_SIZE(monitor_callbacks); i++)
+		if (monitor_callbacks[i].cmd == ghdr->cmd)
+			return monitor_callbacks[i].cb(nlhdr, data);
+
+	return MNL_CB_OK;
+}
+
+struct monitor_option {
+	const char	*pattern;
+	uint8_t		cmd;
+	uint32_t	info_mask;
+};
+
+static struct monitor_option monitor_opts[] = {
+	{
+		.pattern	= "--all",
+		.cmd		= 0,
+	},
+};
+
+static bool pattern_match(const char *s, const char *pattern)
+{
+	const char *opt = pattern;
+	const char *next;
+	int slen = strlen(s);
+	int optlen;
+
+	do {
+		next = opt;
+		while (*next && *next != '|')
+			next++;
+		optlen = next - opt;
+		if (slen == optlen && !strncmp(s, opt, optlen))
+			return true;
+
+		opt = next;
+		if (*opt == '|')
+			opt++;
+	} while (*opt);
+
+	return false;
+}
+
+static int parse_monitor(struct cmd_context *ctx)
+{
+	struct nl_context *nlctx = ctx->nlctx;
+	char **argp = ctx->argp;
+	int argc = ctx->argc;
+	const char *opt = "";
+	unsigned int i;
+
+	if (*argp && argp[0][0] == '-') {
+		opt = *argp;
+		argp++;
+		argc--;
+	}
+	for (i = 0; i < MNL_ARRAY_SIZE(monitor_opts); i++) {
+		if (pattern_match(opt, monitor_opts[i].pattern)) {
+			nlctx->filter_cmd = monitor_opts[i].cmd;
+			nlctx->filter_mask = monitor_opts[i].info_mask;
+			goto opt_found;
+		}
+	}
+	fprintf(stderr, "monitoring for option '%s' not supported\n", *argp);
+	return -1;
+
+opt_found:
+	if (*argp && strcmp(*argp, WILDCARD_DEVNAME))
+		ctx->devname = *argp;
+	return 0;
+}
+
+int nl_monitor(struct cmd_context *ctx)
+{
+	bool is_dev;
+	struct nl_context *nlctx = ctx->nlctx;
+	uint32_t grpid = nlctx->mon_mcgrp_id;
+	int ret;
+
+	if (!grpid) {
+		fprintf(stderr, "multicast group 'monitor' not found\n");
+		return -EOPNOTSUPP;
+	}
+	if (parse_monitor(ctx) < 0)
+		return 1;
+	is_dev = ctx->devname && strcmp(ctx->devname, WILDCARD_DEVNAME);
+
+	ret = load_global_strings(nlctx);
+	if (ret < 0)
+		return ret;
+	ret = mnl_socket_setsockopt(nlctx->sk, NETLINK_ADD_MEMBERSHIP,
+				    &grpid, sizeof(grpid));
+	if (ret < 0)
+		return ret;
+	ret = load_perdev_strings(nlctx, is_dev ? ctx->devname : NULL);
+	if (ret < 0)
+		return ret;
+
+	nlctx->filter_devname = ctx->devname;
+	nlctx->is_monitor = true;
+	nlctx->port = 0;
+	nlctx->seq = 0;
+
+	fputs("listening...\n", stdout);
+	fflush(stdout);
+	ret = ethnl_process_reply(nlctx, monitor_any_cb);
+	free_perdev_strings(NULL);
+	return ret;
+}
+
+void monitor_usage()
+{
+	const char *p;
+	unsigned i;
+
+	fputs("        ethtool --monitor               Show kernel notifications\n",
+	      stdout);
+	for (i = 0; i < MNL_ARRAY_SIZE(monitor_opts); i++) {
+		if (i > 0)
+			fputs("\n                  | ", stdout);
+		else
+			fputs("                ( ", stdout);
+		for (p = monitor_opts[i].pattern; *p; p++)
+			if (*p == '|')
+				fputs(" | ", stdout);
+			else
+				fputc(*p, stdout);
+	}
+	fputs(" )\n", stdout);
+	fputs("                [ DEVNAME | * ]\n", stdout);
+}
diff --git a/netlink/netlink.c b/netlink/netlink.c
index 0671c4589c72..69f692d2cf04 100644
--- a/netlink/netlink.c
+++ b/netlink/netlink.c
@@ -391,6 +391,31 @@ err:
 
 /* get ethtool family id */
 
+static void ethnl_find_monitor_group(struct nl_context *nlctx,
+				     struct nlattr *nest)
+{
+	const struct nlattr *grp_tb[CTRL_ATTR_MCAST_GRP_MAX + 1] = {};
+	DECLARE_ATTR_TB_INFO(grp_tb);
+	struct nlattr *grp_attr;
+	int ret;
+
+	nlctx->mon_mcgrp_id = 0;
+	mnl_attr_for_each_nested(grp_attr, nest) {
+		ret = mnl_attr_parse_nested(grp_attr, attr_cb, &grp_tb_info);
+		if (ret < 0)
+			return;
+		if (!grp_tb[CTRL_ATTR_MCAST_GRP_NAME] ||
+		    !grp_tb[CTRL_ATTR_MCAST_GRP_ID])
+			continue;
+		if (strcmp(mnl_attr_get_str(grp_tb[CTRL_ATTR_MCAST_GRP_NAME]),
+			   ETHTOOL_MCGRP_MONITOR_NAME))
+			continue;
+		nlctx->mon_mcgrp_id =
+			mnl_attr_get_u32(grp_tb[CTRL_ATTR_MCAST_GRP_ID]);
+		return;
+	}
+}
+
 static int ethnl_family_cb(const struct nlmsghdr *nlhdr, void *data)
 {
 	struct nl_context *nlctx = data;
@@ -398,9 +423,13 @@ static int ethnl_family_cb(const struct nlmsghdr *nlhdr, void *data)
 
 	nlctx->ethnl_fam = 0;
 	mnl_attr_for_each(attr, nlhdr, GENL_HDRLEN) {
-		if (mnl_attr_get_type(attr) == CTRL_ATTR_FAMILY_ID) {
+		switch(mnl_attr_get_type(attr)) {
+		case CTRL_ATTR_FAMILY_ID:
 			nlctx->ethnl_fam = mnl_attr_get_u16(attr);
 			break;
+		case CTRL_ATTR_MCAST_GROUPS:
+			ethnl_find_monitor_group(nlctx, attr);
+			break;
 		}
 	}
 
@@ -478,6 +507,7 @@ int __init_aux_nlctx(struct nl_context *nlctx)
 	}
 
 	aux->ethnl_fam = nlctx->ethnl_fam;
+	aux->mon_mcgrp_id = nlctx->mon_mcgrp_id;
 	nlctx->aux_nlctx = aux;
 
 	return 0;
diff --git a/netlink/netlink.h b/netlink/netlink.h
index 547c865ed535..15bf9f3873b0 100644
--- a/netlink/netlink.h
+++ b/netlink/netlink.h
@@ -17,6 +17,7 @@
 
 struct nl_context {
 	int ethnl_fam;
+	uint32_t mon_mcgrp_id;
 	struct mnl_socket *sk;
 	struct nl_context *aux_nlctx;
 	void *cmd_private;
@@ -30,6 +31,10 @@ struct nl_context {
 	const char *devname;
 	bool is_dump;
 	int exit_code;
+	bool is_monitor;
+	uint8_t filter_cmd;
+	uint32_t filter_mask;
+	const char *filter_devname;
 };
 
 struct attr_tb_info {
@@ -144,6 +149,27 @@ static inline void show_u32_yn(const struct nlattr **tb, unsigned int idx,
 		       mnl_attr_get_u32(tb[idx]) ? "yes" : "no");
 }
 
+/* reply content filtering */
+
+static inline bool mask_ok(const struct nl_context *nlctx, uint32_t bits)
+{
+	return !nlctx->filter_mask || (nlctx->filter_mask & bits);
+}
+
+static inline bool dev_ok(const struct nl_context *nlctx)
+{
+	return !nlctx->filter_devname ||
+	       (nlctx->devname &&
+		!strcmp(nlctx->devname, nlctx->filter_devname));
+}
+
+static inline bool show_only(const struct nl_context *nlctx, uint32_t bits)
+{
+	if (nlctx->is_monitor || !nlctx->filter_mask)
+		return false;
+	return nlctx->filter_mask & ~bits;
+}
+
 /* misc */
 
 static inline int init_aux_nlctx(struct nl_context *nlctx)
