diff --git a/include/net/ip.h b/include/net/ip.h
index 5b317c9f4470..12cd3971f4cf 100644
--- a/include/net/ip.h
+++ b/include/net/ip.h
@@ -98,7 +98,7 @@ static inline void ipcm_init_sk(struct ipcm_cookie *ipcm,
 #define PKTINFO_SKB_CB(skb) ((struct in_pktinfo *)((skb)->cb))
 
 /* return enslaved device index if relevant */
-static inline int inet_sdif(struct sk_buff *skb)
+static inline int inet_sdif(const struct sk_buff *skb)
 {
 #if IS_ENABLED(CONFIG_NET_L3_MASTER_DEV)
 	if (skb && ipv4_l3mdev_skb(IPCB(skb)->flags))
diff --git a/include/uapi/linux/netfilter/xt_slavedev.h b/include/uapi/linux/netfilter/xt_slavedev.h
new file mode 100644
index 000000000000..d35785b04c4b
--- /dev/null
+++ b/include/uapi/linux/netfilter/xt_slavedev.h
@@ -0,0 +1,18 @@
+/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
+#ifndef _UAPI_XT_SLAVEDEV_H
+#define _UAPI_XT_SLAVEDEV_H
+
+#include <linux/types.h>
+#include <linux/if.h>
+
+#define XT_SLAVEDEV_IN_INV	0x01	/* invert interface match */
+#define XT_SLAVEDEV_IN_STRICT	0x02	/* require iif to be enslaved */
+#define XT_SLAVEDEV_MASK	(0x04 - 1)
+
+struct xt_slavedev_info {
+	char iniface[IFNAMSIZ];
+	unsigned char iniface_mask[IFNAMSIZ];
+	__u8 flags;
+};
+
+#endif /* _UAPI_XT_SLAVEDEV_H */
diff --git a/net/netfilter/Kconfig b/net/netfilter/Kconfig
index 91efae88e8c2..a259192bdb2e 100644
--- a/net/netfilter/Kconfig
+++ b/net/netfilter/Kconfig
@@ -1530,6 +1530,18 @@ config NETFILTER_XT_MATCH_SCTP
 	  If you want to compile it as a module, say M here and read
 	  <file:Documentation/kbuild/modules.rst>.  If unsure, say `N'.
 
+config NETFILTER_XT_MATCH_SLAVEDEV
+	tristate '"slavedev" match support'
+	depends on NET_L3_MASTER_DEV
+	depends on NETFILTER_ADVANCED
+	help
+	  Slavedev packet matching matches against the input slave interface
+	  if the IP packet arrived on a layer 3 master device. It allows
+	  matching against the input interface if that interface is
+	  associated to a VRF master device.
+
+	  To compile it as a module, choose M here.  If unsure, say N.
+
 config NETFILTER_XT_MATCH_SOCKET
 	tristate '"socket" match support'
 	depends on NETFILTER_XTABLES
diff --git a/net/netfilter/Makefile b/net/netfilter/Makefile
index 5e9b2eb24349..9650d1d5216c 100644
--- a/net/netfilter/Makefile
+++ b/net/netfilter/Makefile
@@ -197,6 +197,7 @@ obj-$(CONFIG_NETFILTER_XT_MATCH_RATEEST) += xt_rateest.o
 obj-$(CONFIG_NETFILTER_XT_MATCH_REALM) += xt_realm.o
 obj-$(CONFIG_NETFILTER_XT_MATCH_RECENT) += xt_recent.o
 obj-$(CONFIG_NETFILTER_XT_MATCH_SCTP) += xt_sctp.o
+obj-$(CONFIG_NETFILTER_XT_MATCH_SLAVEDEV) += xt_slavedev.o
 obj-$(CONFIG_NETFILTER_XT_MATCH_SOCKET) += xt_socket.o
 obj-$(CONFIG_NETFILTER_XT_MATCH_STATE) += xt_state.o
 obj-$(CONFIG_NETFILTER_XT_MATCH_STATISTIC) += xt_statistic.o
diff --git a/net/netfilter/xt_slavedev.c b/net/netfilter/xt_slavedev.c
new file mode 100644
index 000000000000..a53466dfb10b
--- /dev/null
+++ b/net/netfilter/xt_slavedev.c
@@ -0,0 +1,80 @@
+// SPDX-License-Identifier: GPL-2.0-only
+
+#include <net/ip.h>
+#include <net/ipv6.h>
+#include <linux/module.h>
+#include <linux/netfilter/x_tables.h>
+#include <uapi/linux/netfilter/xt_slavedev.h>
+
+static bool
+slavedev_mt(const struct sk_buff *skb, struct xt_action_param *par)
+{
+	const struct xt_slavedev_info *info = par->matchinfo;
+	struct net_device *sd;
+	unsigned long ret = 1;
+	int sdif = 0;
+
+	switch (xt_family(par)) {
+	case NFPROTO_IPV4:
+		sdif = inet_sdif(skb);
+		break;
+	case NFPROTO_IPV6:
+		sdif = inet6_sdif(skb);
+		break;
+	}
+	if (sdif) {
+		sd = dev_get_by_index_rcu(xt_net(par), sdif);
+		if (sd)
+			ret = ifname_compare_aligned(sd->name, info->iniface,
+						     info->iniface_mask);
+	} else if (!(info->flags & XT_SLAVEDEV_IN_STRICT) && xt_in(par)) {
+		ret = ifname_compare_aligned(xt_inname(par), info->iniface,
+					     info->iniface_mask);
+	}
+
+	if (!ret ^ !(info->flags & XT_SLAVEDEV_IN_INV))
+		return false;
+
+	return true;
+}
+
+static int slavedev_mt_check(const struct xt_mtchk_param *par)
+{
+	const struct xt_slavedev_info *info = par->matchinfo;
+
+	if (info->flags & ~XT_SLAVEDEV_MASK)
+		return -EINVAL;
+
+	return 0;
+}
+
+static struct xt_match slavedev_mt_reg __read_mostly = {
+	.name		= "slavedev",
+	.revision	= 0,
+	.family		= NFPROTO_UNSPEC,
+	.checkentry	= slavedev_mt_check,
+	.match		= slavedev_mt,
+	.matchsize	= sizeof(struct xt_slavedev_info),
+	.hooks		= (1 << NF_INET_LOCAL_IN) |
+			  (1 << NF_INET_FORWARD),
+	.me		= THIS_MODULE,
+};
+
+static int __init slavedev_mt_init(void)
+{
+	return xt_register_match(&slavedev_mt_reg);
+}
+
+static void __exit slavedev_mt_exit(void)
+{
+	xt_unregister_match(&slavedev_mt_reg);
+}
+
+module_init(slavedev_mt_init);
+module_exit(slavedev_mt_exit);
+
+MODULE_LICENSE("GPL");
+MODULE_AUTHOR("Martin Willi <martin@strongswan.org>");
+MODULE_DESCRIPTION("Xtables: L3master input slave device match");
+MODULE_ALIAS("ipt_slavedev");
+MODULE_ALIAS("ip6t_slavedev");
