diff --git a/app/test/Makefile b/app/test/Makefile
index 57930c00b..f621978d7 100644
--- a/app/test/Makefile
+++ b/app/test/Makefile
@@ -230,6 +230,11 @@ SRCS-$(CONFIG_RTE_LIBRTE_BPF) += test_bpf.c
 
 SRCS-$(CONFIG_RTE_LIBRTE_RCU) += test_rcu_qsbr.c test_rcu_qsbr_perf.c
 
+ifeq ($(CONFIG_RTE_LIBRTE_IF_PROXY),y)
+SRCS-y += test_if_proxy.c
+LDLIBS += -lrte_if_proxy
+endif
+
 SRCS-$(CONFIG_RTE_LIBRTE_IPSEC) += test_ipsec.c
 SRCS-$(CONFIG_RTE_LIBRTE_IPSEC) += test_ipsec_sad.c
 ifeq ($(CONFIG_RTE_LIBRTE_IPSEC),y)
diff --git a/app/test/meson.build b/app/test/meson.build
index fb49d804b..2a3b5fef2 100644
--- a/app/test/meson.build
+++ b/app/test/meson.build
@@ -61,6 +61,7 @@ test_sources = files('commands.c',
 	'test_hash_perf.c',
 	'test_hash_readwrite_lf.c',
 	'test_interrupts.c',
+	'test_if_proxy.c',
 	'test_ipsec.c',
 	'test_ipsec_sad.c',
 	'test_kni.c',
diff --git a/app/test/test_if_proxy.c b/app/test/test_if_proxy.c
new file mode 100644
index 000000000..0ecfb79b4
--- /dev/null
+++ b/app/test/test_if_proxy.c
@@ -0,0 +1,431 @@
+/* SPDX-License-Identifier: BSD-3-Clause
+ * Copyright(C) 2020 Marvell International Ltd.
+ */
+
+#include "test.h"
+
+#include <rte_ethdev.h>
+#include <rte_if_proxy.h>
+
+#include <string.h>
+#include <unistd.h>
+#include <signal.h>
+#include <net/if.h>
+#include <arpa/inet.h>
+#include <pthread.h>
+#include <time.h>
+
+static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
+static pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
+
+enum net_op {
+	INITIALIZED	= 1U << 0,
+	LOOP_ROUTE	= 1U << 1,
+	LOOP6_ROUTE	= 1U << 2,
+	LINK_CHANGED	= 1U << 3,
+	MAC_CHANGED	= 1U << 4,
+	MTU_CHANGED	= 1U << 5,
+	ADDR_ADD	= 1U << 6,
+	ADDR_DEL	= 1U << 7,
+	ROUTE_ADD	= 1U << 8,
+	ROUTE_DEL	= 1U << 9,
+	ADDR6_ADD	= 1U << 10,
+	ADDR6_DEL	= 1U << 11,
+	ROUTE6_ADD	= 1U << 12,
+	ROUTE6_DEL	= 1U << 13,
+};
+
+static unsigned int state;
+
+static struct {
+	struct rte_ether_addr mac_addr;
+	uint16_t port_id, mtu;
+	struct in_addr ipv4, route4;
+	struct in6_addr ipv6, route6;
+	uint16_t depth4, depth6;
+	int is_up;
+} net_cfg;
+
+static
+int unlock_notify(unsigned int op)
+{
+	/* the mutex is expected to be locked on entry */
+	RTE_VERIFY(pthread_mutex_trylock(&mutex) == EBUSY);
+	state |= op;
+
+	pthread_mutex_unlock(&mutex);
+	return pthread_cond_signal(&cond);
+}
+
+static
+int wait_for(unsigned int op_mask, unsigned int sec)
+{
+	struct timespec time;
+	int ec = pthread_mutex_trylock(&mutex);
+
+	/* the mutex is expected to be locked on entry */
+	RTE_VERIFY(ec == EBUSY);
+
+	ec = 0;
+	clock_gettime(CLOCK_REALTIME, &time);
+	time.tv_sec += sec;
+
+	while ((state & op_mask) != op_mask && ec == 0)
+		ec = pthread_cond_timedwait(&cond, &mutex, &time);
+
+	return ec;
+}
+
+static
+int expect(unsigned int op_mask, const char *fmt, ...)
+#if __GNUC__
+	__attribute__((format(printf, 2, 3)));
+#endif
+
+static
+int expect(unsigned int op_mask, const char *fmt, ...)
+{
+	char cmd[128];
+	va_list args;
+	int ret;
+
+	state &= ~op_mask;
+	va_start(args, fmt);
+	vsnprintf(cmd, sizeof(cmd), fmt, args);
+	va_end(args);
+	ret = system(cmd);
+	if (ret == 0)
+		/* IPv6 address notifications seem to need that long delay. */
+		return wait_for(op_mask, 2);
+	return ret;
+}
+
+static
+void mac_change(uint16_t port_id, const struct rte_ether_addr *mac)
+{
+	pthread_mutex_lock(&mutex);
+	RTE_VERIFY(port_id == net_cfg.port_id);
+	if (memcmp(mac->addr_bytes, net_cfg.mac_addr.addr_bytes,
+		   RTE_ETHER_ADDR_LEN) == 0) {
+		unlock_notify(MAC_CHANGED);
+		return;
+	}
+	pthread_mutex_unlock(&mutex);
+}
+
+static
+void mtu_change(uint16_t port_id, uint16_t mtu)
+{
+	pthread_mutex_lock(&mutex);
+	RTE_VERIFY(port_id == net_cfg.port_id);
+	if (net_cfg.mtu == mtu) {
+		unlock_notify(MTU_CHANGED);
+		return;
+	}
+	pthread_mutex_unlock(&mutex);
+}
+
+static
+void link_change(uint16_t port_id, int is_up)
+{
+	pthread_mutex_lock(&mutex);
+	RTE_VERIFY(port_id == net_cfg.port_id);
+	if (net_cfg.is_up == is_up) {
+		unlock_notify(LINK_CHANGED);
+		return;
+	}
+	pthread_mutex_unlock(&mutex);
+}
+
+static
+void addr_add(uint16_t port_id, uint32_t ip)
+{
+	pthread_mutex_lock(&mutex);
+	RTE_VERIFY(port_id == net_cfg.port_id);
+	if (net_cfg.ipv4.s_addr == ip) {
+		unlock_notify(ADDR_ADD);
+		return;
+	}
+	pthread_mutex_unlock(&mutex);
+}
+
+static
+void addr_del(uint16_t port_id, uint32_t ip)
+{
+	pthread_mutex_lock(&mutex);
+	RTE_VERIFY(port_id == net_cfg.port_id);
+	if (net_cfg.ipv4.s_addr == ip) {
+		unlock_notify(ADDR_DEL);
+		return;
+	}
+	pthread_mutex_unlock(&mutex);
+}
+
+static
+void addr6_add(uint16_t port_id, const uint8_t *ip)
+{
+	pthread_mutex_lock(&mutex);
+	RTE_VERIFY(port_id == net_cfg.port_id);
+	if (memcmp(ip, net_cfg.ipv6.s6_addr, 16) == 0) {
+		unlock_notify(ADDR6_ADD);
+		return;
+	}
+	pthread_mutex_unlock(&mutex);
+}
+
+static
+void addr6_del(uint16_t port_id __rte_unused, const uint8_t *ip)
+{
+	pthread_mutex_lock(&mutex);
+	RTE_VERIFY(port_id == net_cfg.port_id);
+	if (memcmp(ip, net_cfg.ipv6.s6_addr, 16) == 0) {
+		unlock_notify(ADDR6_DEL);
+		return;
+	}
+	pthread_mutex_unlock(&mutex);
+}
+
+static
+void route_add(uint32_t ip, uint8_t depth)
+{
+	pthread_mutex_lock(&mutex);
+	/* Since we are checking if during initialization we get some routing
+	 * info we need to notify either when we are not initialized or when
+	 * the exact route matches.
+	 */
+	if (!(state & INITIALIZED) ||
+	    (net_cfg.depth4 == depth && net_cfg.route4.s_addr == ip)) {
+		unlock_notify(ROUTE_ADD);
+		return;
+	}
+	pthread_mutex_unlock(&mutex);
+}
+
+static
+void route_del(uint32_t ip, uint8_t depth)
+{
+	pthread_mutex_lock(&mutex);
+	if (net_cfg.depth4 == depth && net_cfg.route4.s_addr == ip) {
+		unlock_notify(ROUTE_DEL);
+		return;
+	}
+	pthread_mutex_unlock(&mutex);
+}
+
+static
+void route6_add(const uint8_t *ip, uint8_t depth)
+{
+	pthread_mutex_lock(&mutex);
+	/* Since we are checking if during initialization we get some routing
+	 * info we need to notify either when we are not initialized or when
+	 * the exact route matches.
+	 */
+	if (!(state & INITIALIZED) ||
+	    (net_cfg.depth6 == depth &&
+	     /* don't check for trailing zeros */
+	     memcmp(ip, net_cfg.route6.s6_addr, depth/8) == 0)) {
+		unlock_notify(ROUTE6_ADD);
+		return;
+	}
+	pthread_mutex_unlock(&mutex);
+}
+
+static
+void route6_del(const uint8_t *ip, uint8_t depth)
+{
+	pthread_mutex_lock(&mutex);
+	if (net_cfg.depth6 == depth &&
+	    /* don't check for trailing zeros */
+	    memcmp(ip, net_cfg.route6.s6_addr, depth/8) == 0) {
+		unlock_notify(ROUTE6_DEL);
+		return;
+	}
+	pthread_mutex_unlock(&mutex);
+}
+
+static
+void cfg_finished(void)
+{
+	pthread_mutex_lock(&mutex);
+	unlock_notify(INITIALIZED);
+}
+
+static
+struct rte_ifpx_callbacks cbs = {
+	.mac_change = mac_change,
+	.mtu_change = mtu_change,
+	.link_change = link_change,
+	.addr_add = addr_add,
+	.addr_del = addr_del,
+	.addr6_add = addr6_add,
+	.addr6_del = addr6_del,
+	.route_add = route_add,
+	.route_del = route_del,
+	.route6_add = route6_add,
+	.route6_del = route6_del,
+	/* lib specific callback */
+	.cfg_finished = cfg_finished,
+};
+
+static int
+test_if_proxy(void)
+{
+	int ec;
+	char buf[INET6_ADDRSTRLEN];
+	const struct rte_ifpx_info *pinfo;
+
+	state = 0;
+	memset(&net_cfg, 0, sizeof(net_cfg));
+	/* Since we are not going to test RX/TX we can just create proxy and
+	 * bind it to itself to test just notification functionality.
+	 */
+	net_cfg.port_id = rte_ifpx_create(RTE_IFPX_DEFAULT);
+	RTE_VERIFY(net_cfg.port_id != RTE_MAX_ETHPORTS);
+	rte_ifpx_port_bind(net_cfg.port_id, net_cfg.port_id);
+	rte_ifpx_callbacks_register(&cbs);
+	rte_ifpx_listen();
+
+	pthread_mutex_lock(&mutex);
+	/* During initialization we should observe IPv4/6 loopback routes. */
+	net_cfg.route4.s_addr = RTE_IPV4(127, 0, 0, 1);
+	net_cfg.depth4 = 32;
+	memcpy(net_cfg.route6.s6_addr, in6addr_loopback.s6_addr, 16);
+	net_cfg.depth6 = 128;
+	ec = wait_for(INITIALIZED | ROUTE_ADD | ROUTE6_ADD, 2);
+	if (ec != 0) {
+		printf("Failed to obtain network configuration\n");
+		goto exit;
+	}
+	pinfo = rte_ifpx_info_get(net_cfg.port_id);
+	RTE_VERIFY(pinfo);
+
+	/* Make sure the link is down. */
+	net_cfg.is_up = 0;
+	ec = expect(LINK_CHANGED, "ip link set dev %s down", pinfo->if_name);
+	RTE_VERIFY(ec == ETIMEDOUT || ec == 0);
+
+	/* Test link up notification. */
+	net_cfg.is_up = 1;
+	ec = expect(LINK_CHANGED, "ip link set dev %s up", pinfo->if_name);
+	if (ec != 0) {
+		printf("Failed to notify about link going up\n");
+		goto exit;
+	}
+
+	/* Test for MAC changes notification. */
+	rte_eth_random_addr(net_cfg.mac_addr.addr_bytes);
+	rte_ether_format_addr(buf, sizeof(buf), &net_cfg.mac_addr);
+	ec = expect(MAC_CHANGED, "ip link set dev %s address %s",
+		    pinfo->if_name, buf);
+	if (ec != 0) {
+		printf("Missing/wrong notification about mac change\n");
+		goto exit;
+	}
+
+	/* Test for MTU changes notification. */
+	net_cfg.mtu = pinfo->mtu + 100;
+	ec = expect(MTU_CHANGED, "ip link set dev %s mtu %d",
+		    pinfo->if_name, net_cfg.mtu);
+	if (ec != 0) {
+		printf("Missing/wrong notification about mtu change\n");
+		goto exit;
+	}
+
+	/* Test for adding of IPv4 address - using address from TEST-2 pool.
+	 * This test is specific to linux netlink behaviour - after adding
+	 * address we get both notification about address being added and new
+	 * route.  So I check both.
+	 */
+	net_cfg.ipv4.s_addr = RTE_IPV4(198, 51, 100, 14);
+	net_cfg.route4.s_addr = net_cfg.ipv4.s_addr;
+	net_cfg.depth4 = 32;
+	ec = expect(ADDR_ADD | ROUTE_ADD, "ip addr add 198.51.100.14 dev %s",
+		    pinfo->if_name);
+	if (ec != 0) {
+		printf("Missing/wrong notifications about IPv4 address add\n");
+		goto exit;
+	}
+
+	/* Test for IPv4 address removal.  See comment above for 'addr add'. */
+	ec = expect(ADDR_DEL | ROUTE_DEL, "ip addr del 198.51.100.14/32 dev %s",
+		    pinfo->if_name);
+	if (ec != 0) {
+		printf("Missing/wrong notifications about IPv4 address del\n");
+		goto exit;
+	}
+
+	/* Test for adding IPv4 route. */
+	net_cfg.route4.s_addr = RTE_IPV4(198, 51, 100, 0);
+	net_cfg.depth4 = 24;
+	ec = expect(ROUTE_ADD, "ip route add 198.51.100.0/24 dev %s",
+		    pinfo->if_name);
+	if (ec != 0) {
+		printf("Missing/wrong notifications about IPv4 route add\n");
+		goto exit;
+	}
+
+	/* Test for IPv4 route removal. */
+	ec = expect(ROUTE_DEL, "ip route del 198.51.100.0/24 dev %s",
+		    pinfo->if_name);
+	if (ec != 0) {
+		printf("Missing/wrong notifications about IPv4 route del\n");
+		goto exit;
+	}
+
+	/* Now the same for IPv6 - with address from "documentation pool". */
+	inet_pton(AF_INET6, "2001:db8::dead:beef", net_cfg.ipv6.s6_addr);
+	/* This is specific to linux netlink behaviour - after adding address
+	 * we get both notification about address being added and new route.
+	 * So I wait for both.
+	 */
+	memcpy(net_cfg.route6.s6_addr, net_cfg.ipv6.s6_addr, 16);
+	net_cfg.depth6 = 128;
+	ec = expect(ADDR6_ADD | ROUTE6_ADD,
+		    "ip addr add 2001:db8::dead:beef dev %s",
+		    pinfo->if_name);
+	if (ec != 0) {
+		printf("Missing/wrong notifications about IPv6 address add\n");
+		goto exit;
+	}
+
+	/* See comment above for 'addr6 add'. */
+	ec = expect(ADDR6_DEL | ROUTE6_DEL,
+		    "ip addr del 2001:db8::dead:beef/128 dev %s",
+		    pinfo->if_name);
+	if (ec != 0) {
+		printf("Missing/wrong notifications about IPv6 address del\n");
+		goto exit;
+	}
+
+	net_cfg.depth6 = 96;
+	ec = expect(ROUTE6_ADD, "ip route add 2001:db8::dead:0/96 dev %s",
+		    pinfo->if_name);
+	if (ec != 0) {
+		printf("Missing/wrong notifications about IPv6 route add\n");
+		goto exit;
+	}
+
+	ec = expect(ROUTE6_DEL, "ip route del 2001:db8::dead:0/96 dev %s",
+		    pinfo->if_name);
+	if (ec != 0) {
+		printf("Missing/wrong notifications about IPv6 route del\n");
+		goto exit;
+	}
+
+	/* Finally put link down and test for notification. */
+	net_cfg.is_up = 0;
+	ec = expect(LINK_CHANGED, "ip link set dev %s down", pinfo->if_name);
+	if (ec != 0) {
+		printf("Failed to notify about link going down\n");
+		goto exit;
+	}
+
+exit:
+	pthread_mutex_unlock(&mutex);
+	rte_ifpx_destroy(net_cfg.port_id);
+	rte_ifpx_close();
+
+	return ec;
+}
+
+REGISTER_TEST_COMMAND(if_proxy_autotest, test_if_proxy)
diff --git a/doc/guides/prog_guide/if_proxy_lib.rst b/doc/guides/prog_guide/if_proxy_lib.rst
new file mode 100644
index 000000000..dc1202cdf
--- /dev/null
+++ b/doc/guides/prog_guide/if_proxy_lib.rst
@@ -0,0 +1,103 @@
+..  SPDX-License-Identifier: BSD-3-Clause
+    Copyright(C) 2019 Marvell International Ltd.
+
+.. _IF_Proxy_Library:
+
+IF Proxy Library
+================
+
+When a network interface is assigned to DPDK it usually disappears from
+the system.
+This way user looses ability to configure it via typical configuration
+tools and is left basically with two options:
+
+  - configure it via command line arguments,
+
+  - add support for live configuration via some IPC mechanism.
+
+The first option is static and the second one requires some work to add
+communication loop (e.g. separate thread listening/communicating on
+a socket).
+
+This library adds a possibility to configure DPDK ports by using normal
+configuration utilities (e.g. from iproute2 suite).
+It requires user to configure additional DPDK ports that are visible to
+the system (such as Tap or KNI - actually any port that has valid
+'if_index' in 'struct rte_eth_dev_info' will do) and designate them as
+a port representor (a proxy) in the system.
+
+Let's see typical intended usage by an example.
+Suppose that you have application that handles traffic on two ports (in
+the white list below).
+
+    ./app -w 00:14.0 -w 00:16.0 --vdev=net_tap0 --vdev=net_tap1
+
+So in addition you configure two proxy ports and in the application code
+you bind them to the "main" ports:
+
+    rte_if_proxy_port_bind(port0, proxy0);
+    rte_if_proxy_port_bind(port1, proxy1);
+
+This binding is a logical one - there is no automatic packet forwarding
+configured.
+This is because library cannot tell upfront what portion of the traffic
+received on ports 0/1 should be redirected to the system via proxies and
+also it does not know how the application is structured (what packet
+processing engines it uses).
+Therefore it is application writer responsibility to include proxy ports
+into its packet processing and forward appropriate packets between
+proxies and ports.
+What the library actually does is that it gets network configuration
+from the system and listens to its changes.
+This information is then matched against 'if_index' of the configured
+proxies (when applicable - routing information is global) and passed to
+the application via set of callbacks that user has to register:
+
+    rte_if_proxy_callbacks_register(&cbs);
+
+Here 'cbs' is a 'struct rte_if_proxy_callbacks' which has following
+members:
+
+    void  (*mac_change)(uint16_t port_id, const struct rte_ether_addr *mac);
+    void  (*mtu_change)(uint16_t port_id, uint16_t mtu);
+    void (*link_change)(uint16_t port_id, int is_up);
+    /* IPv4 addresses are in host order */
+    void    (*addr_add)(uint16_t port_id, uint32_t ip);
+    void    (*addr_del)(uint16_t port_id, uint32_t ip);
+    void   (*addr6_add)(uint16_t port_id, const uint8_t *ip);
+    void   (*addr6_del)(uint16_t port_id, const uint8_t *ip);
+    void   (*route_add)(uint32_t ip, uint8_t depth);
+    void   (*route_del)(uint32_t ip, uint8_t depth);
+    void  (*route6_add)(const uint8_t *ip, uint8_t depth);
+    void  (*route6_del)(const uint8_t *ip, uint8_t depth);
+    /* lib specific callback - called when initial network configuration
+     * query is finished */
+    void (*cfg_finished)(void);
+
+So for example when the user issues command:
+
+    ip link set dev dtap0 mtu 1600
+
+then library will call `mtu_change()` callback with port_id equal to
+'port0' (id of the port bound to this proxy) and 'mtu' equal to 1600
+('dtap0' is the default interface name for 'net_tap0').
+Application can simply use `rte_eth_dev_set_mtu()` as this callback.
+The same way `rte_eth_dev_default_mac_addr_set()` can be used for
+`mac_change()` and `rte_eth_dev_set_link_up/down()` can be used inside
+the callback that does dispatch based on 'is_up' argument.
+
+Please note however that the context in which these callbacks are called
+is most probably different from the one in which packets are handled and
+it is application writer responsibility to use proper synchronization
+mechanisms - if they are needed.
+
+If the application supports IP protocol stack then it can utilize
+callbacks for adding/removing of addresses to the proxies and also
+routing information (note that routing info is not associated with any
+port).
+E.g. application can feed some LPM tables with these addresses and upon
+reception of a packet on some port match this packet against those
+tables to figure out what to do with this packet.
+If the decision is to pass it to the system then it can simply forward
+them to the proxy corresponding to the port on which packet has been
+received by using standard PMD TX interface.
diff --git a/doc/guides/prog_guide/index.rst b/doc/guides/prog_guide/index.rst
index dc4851c57..0a1541f34 100644
--- a/doc/guides/prog_guide/index.rst
+++ b/doc/guides/prog_guide/index.rst
@@ -57,6 +57,7 @@ Programmer's Guide
     metrics_lib
     bpf_lib
     ipsec_lib
+    if_proxy_lib
     source_org
     dev_kit_build_system
     dev_kit_root_make_help
diff --git a/examples/Makefile b/examples/Makefile
index feff79784..5aa9ab431 100644
--- a/examples/Makefile
+++ b/examples/Makefile
@@ -81,6 +81,7 @@ else
 $(info vm_power_manager requires libvirt >= 0.9.3)
 endif
 endif
+DIRS-$(CONFIG_RTE_LIBRTE_IF_PROXY) += if_proxy
 
 DIRS-y += eventdev_pipeline
 
diff --git a/examples/if_proxy/Makefile b/examples/if_proxy/Makefile
new file mode 100644
index 000000000..dd0515fa4
--- /dev/null
+++ b/examples/if_proxy/Makefile
@@ -0,0 +1,58 @@
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright(c) 2019 Marvell International Ltd.
+
+# binary name
+APP = if_proxy
+
+# all source are stored in SRCS-y
+SRCS-y := main.c
+
+# Build using pkg-config variables if possible
+ifeq ($(shell pkg-config --exists libdpdk && echo 0),0)
+
+all: shared
+.PHONY: shared static
+shared: build/$(APP)-shared
+	ln -sf $(APP)-shared build/$(APP)
+static: build/$(APP)-static
+	ln -sf $(APP)-static build/$(APP)
+
+PKGCONF=pkg-config --define-prefix
+
+PC_FILE := $(shell $(PKGCONF) --path libdpdk)
+CFLAGS += -O3 $(shell $(PKGCONF) --cflags libdpdk)
+LDFLAGS_SHARED = $(shell $(PKGCONF) --libs libdpdk)
+LDFLAGS_STATIC = -Wl,-Bstatic $(shell $(PKGCONF) --static --libs libdpdk)
+
+build/$(APP)-shared: $(SRCS-y) Makefile $(PC_FILE) | build
+	$(CC) $(CFLAGS) $(SRCS-y) -o $@ $(LDFLAGS) $(LDFLAGS_SHARED)
+
+build/$(APP)-static: $(SRCS-y) Makefile $(PC_FILE) | build
+	$(CC) $(CFLAGS) $(SRCS-y) -o $@ $(LDFLAGS) $(LDFLAGS_STATIC)
+
+build:
+	@mkdir -p $@
+
+.PHONY: clean
+clean:
+	rm -f build/$(APP) build/$(APP)-static build/$(APP)-shared
+	test -d build && rmdir -p build || true
+
+else # Build using legacy build system
+
+ifeq ($(RTE_SDK),)
+$(error "Please define RTE_SDK environment variable")
+endif
+
+# Default target, detect a build directory, by looking for a path with a .config
+RTE_TARGET ?= $(notdir $(abspath $(dir $(firstword $(wildcard $(RTE_SDK)/*/.config)))))
+
+include $(RTE_SDK)/mk/rte.vars.mk
+
+CFLAGS += -O3
+CFLAGS += -DALLOW_EXPERIMENTAL_API
+CFLAGS += $(WERROR_FLAGS)
+LDLIBS += -lrte_if_proxy -lrte_ethdev -lrte_eal
+
+include $(RTE_SDK)/mk/rte.extapp.mk
+endif
diff --git a/examples/if_proxy/main.c b/examples/if_proxy/main.c
new file mode 100644
index 000000000..2195fb490
--- /dev/null
+++ b/examples/if_proxy/main.c
@@ -0,0 +1,203 @@
+/* SPDX-License-Identifier: BSD-3-Clause
+ * Copyright(C) 2020 Marvell International Ltd.
+ */
+
+#include <rte_if_proxy.h>
+
+#include <string.h>
+#include <unistd.h>
+#include <signal.h>
+#include <arpa/inet.h>
+
+static
+char buf[INET6_ADDRSTRLEN];
+
+static
+uint16_t proxy_id = RTE_MAX_ETHPORTS;
+
+static
+void mac_change(uint16_t port_id, const struct rte_ether_addr *mac)
+{
+	char buf[3*RTE_ETHER_ADDR_LEN];
+
+	rte_ether_format_addr(buf, sizeof(buf), mac);
+	printf("\tmac change for port %u -> %s\n", port_id, buf);
+}
+
+static
+void mtu_change(uint16_t port_id, uint16_t mtu)
+{
+	printf("\tmtu change for port %u -> %u\n", port_id, mtu);
+}
+
+static
+void link_change(uint16_t port_id, int is_up)
+{
+	printf("\tport %u going %s\n", port_id, is_up ? "up" : "down");
+}
+
+static
+void addr_add(uint16_t port_id, uint32_t ip)
+{
+	struct in_addr a = { .s_addr = htonl(ip) };
+
+	printf("\taddress add for port %u -> %s\n", port_id,
+	       inet_ntop(AF_INET, &a, buf, sizeof(buf)));
+}
+
+static
+void addr_del(uint16_t port_id, uint32_t ip)
+{
+	struct in_addr a = { .s_addr = htonl(ip) };
+
+	printf("\taddress del for port %u -> %s\n", port_id,
+	       inet_ntop(AF_INET, &a, buf, sizeof(buf)));
+}
+
+static
+void addr6_add(uint16_t port_id, const uint8_t *ip)
+{
+	struct in6_addr a;
+
+	memcpy(a.s6_addr, ip, 16);
+	printf("\taddress6 add for port %u -> %s\n", port_id,
+	       inet_ntop(AF_INET6, &a, buf, sizeof(buf)));
+}
+
+static
+void addr6_del(uint16_t port_id, const uint8_t *ip)
+{
+	struct in6_addr a;
+
+	memcpy(a.s6_addr, ip, 16);
+	printf("\taddress6 del for port %u -> %s\n", port_id,
+	       inet_ntop(AF_INET6, &a, buf, sizeof(buf)));
+}
+
+static
+void route_add(uint32_t ip, uint8_t depth)
+{
+	struct in_addr a = { .s_addr = htonl(ip) };
+
+	printf("\troute add -> %s/%u\n",
+	       inet_ntop(AF_INET, &a, buf, sizeof(buf)), depth);
+}
+
+static
+void route_del(uint32_t ip, uint8_t depth)
+{
+	struct in_addr a = { .s_addr = htonl(ip) };
+
+	printf("\troute del -> %s/%u\n",
+	       inet_ntop(AF_INET, &a, buf, sizeof(buf)), depth);
+}
+
+static
+void route6_add(const uint8_t *ip, uint8_t depth)
+{
+	struct in6_addr a;
+
+	memcpy(a.s6_addr, ip, 16);
+	printf("\troute6 add -> %s/%u\n",
+	       inet_ntop(AF_INET6, &a, buf, sizeof(buf)), depth);
+}
+
+static
+void route6_del(const uint8_t *ip, uint8_t depth)
+{
+	struct in6_addr a;
+
+	memcpy(a.s6_addr, ip, 16);
+	printf("\troute6 del -> %s/%u\n",
+	       inet_ntop(AF_INET6, &a, buf, sizeof(buf)), depth);
+}
+
+struct rte_ifpx_callbacks cbs = {
+	.mac_change = mac_change,
+	.mtu_change = mtu_change,
+	.link_change = link_change,
+	.addr_add = addr_add,
+	.addr_del = addr_del,
+	.addr6_add = addr6_add,
+	.addr6_del = addr6_del,
+	.route_add = route_add,
+	.route_del = route_del,
+	.route6_add = route6_add,
+	.route6_del = route6_del,
+};
+
+static
+void proxy_bind_change(int sig)
+{
+	uint16_t port;
+	if (sig == SIGUSR1)
+		port = 0;
+	else if (sig == SIGUSR2)
+		port = 1;
+	else
+		return;
+
+	if (port >= rte_eth_dev_count_avail()) {
+		printf("\tNot enough ports allocated!\n");
+		return;
+	}
+
+	if (rte_ifpx_proxy_get(port) == RTE_MAX_ETHPORTS) {
+		printf("\tbinding port %d to proxy\n", port);
+		rte_ifpx_port_bind(port, proxy_id);
+	} else {
+		printf("\tunbinding port %d\n", port);
+		rte_ifpx_port_unbind(port);
+	}
+}
+
+int
+main(int argc, char **argv)
+{
+	int i, sig, nb_ports;
+	sigset_t set;
+
+	/* init EAL */
+	i = rte_eal_init(argc, argv);
+	if (i < 0)
+		rte_exit(EXIT_FAILURE, "Invalid EAL arguments\n");
+	argc -= i;
+	argv += i;
+
+	nb_ports = rte_eth_dev_count_avail();
+	if (nb_ports == 0)
+		rte_exit(EXIT_FAILURE, "No Ethernet ports - bye\n");
+
+	proxy_id = rte_ifpx_create(RTE_IFPX_DEFAULT);
+	if (proxy_id >= RTE_MAX_ETHPORTS) {
+		printf("Failed to create default proxy\n");
+		return -1;
+	}
+	/* Bind all ports to the same proxy. */
+	for (i = 0; i < nb_ports; ++i)
+		rte_ifpx_port_bind(i, proxy_id);
+	rte_ifpx_callbacks_register(&cbs);
+	rte_ifpx_listen();
+
+	/* Since we do not process packets - only listen to net events - we only
+	 * wait for signal either to quit or to change proxy binding.
+	 */
+	signal(SIGUSR1, proxy_bind_change);
+	signal(SIGUSR2, proxy_bind_change);
+
+	sigemptyset(&set);
+	sigaddset(&set, SIGINT);
+	sigprocmask(SIG_BLOCK, &set, NULL);
+	printf("Press ^C to quit\n");
+	do {
+		i = sigwait(&set, &sig);
+	} while (i != 0 && sig != SIGINT);
+
+	RTE_ETH_FOREACH_DEV(i) {
+		printf("\nClosing port %d...\n", i);
+		rte_eth_dev_close(i);
+	}
+	printf("Bye\n");
+
+	return 0;
+}
diff --git a/examples/if_proxy/meson.build b/examples/if_proxy/meson.build
new file mode 100644
index 000000000..5f5826a90
--- /dev/null
+++ b/examples/if_proxy/meson.build
@@ -0,0 +1,12 @@
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright(c) 2019 Marvell International Ltd.
+
+# meson file, for building this example as part of a main DPDK build.
+#
+# To build this example as a standalone application with an already-installed
+# DPDK instance, use 'make'
+
+allow_experimental_apis = true
+sources = files(
+	'main.c'
+)
diff --git a/examples/meson.build b/examples/meson.build
index 1f2b6f516..468ef8a90 100644
--- a/examples/meson.build
+++ b/examples/meson.build
@@ -16,7 +16,7 @@ all_examples = [
 	'eventdev_pipeline',
 	'fips_validation', 'flow_classify',
 	'flow_filtering', 'helloworld',
-	'ioat',
+	'if_proxy', 'ioat',
 	'ip_fragmentation', 'ip_pipeline',
 	'ip_reassembly', 'ipsec-secgw',
 	'ipv4_multicast', 'kni',
