git: d6cd20cc5c47 - main - netinet6: fix ndp proxying

From: Alexander V. Chernikov <melifaro_at_FreeBSD.org>
Date: Mon, 30 May 2022 10:54:04 UTC
The branch main has been updated by melifaro:

URL: https://cgit.FreeBSD.org/src/commit/?id=d6cd20cc5c475e8bbf257ac1474ff490ae4dcab6

commit d6cd20cc5c475e8bbf257ac1474ff490ae4dcab6
Author:     KUROSAWA Takahiro <takahiro.kurosawa@gmail.com>
AuthorDate: 2022-05-30 07:51:15 +0000
Commit:     Alexander V. Chernikov <melifaro@FreeBSD.org>
CommitDate: 2022-05-30 10:53:33 +0000

    netinet6: fix ndp proxying
    
    We could insert proxy NDP entries by the ndp command, but the host
    with proxy ndp entries had not responded to Neighbor Solicitations.
    Change the following points for proxy NDP to work as expected:
    * join solicited-node multicast addresses for proxy NDP entries
      in order to receive Neighbor Solicitations.
    * look up proxy NDP entries not on the routing table but on the
      link-level address table when receiving Neighbor Solicitations.
    
    Reviewed By: melifaro
    Differential Revision: https://reviews.freebsd.org/D35307
    MFC after:      2 weeks
---
 sys/net/if.c                    |  10 ++
 sys/net/if_llatbl.c             |  48 +++++++++
 sys/net/if_llatbl.h             |  12 ++-
 sys/netinet6/in6.c              | 111 ++++++++++++++++++--
 sys/netinet6/in6_var.h          |   2 +
 sys/netinet6/nd6_nbr.c          |  57 ++++++-----
 tests/sys/netinet6/Makefile     |   3 +-
 tests/sys/netinet6/proxy_ndp.sh | 222 ++++++++++++++++++++++++++++++++++++++++
 8 files changed, 425 insertions(+), 40 deletions(-)

diff --git a/sys/net/if.c b/sys/net/if.c
index c50cc2d291e2..8f463d0bfa58 100644
--- a/sys/net/if.c
+++ b/sys/net/if.c
@@ -1017,6 +1017,16 @@ if_purgeaddrs(struct ifnet *ifp)
 {
 	struct ifaddr *ifa;
 
+#ifdef INET6
+	/*
+	 * Need to leave multicast addresses of proxy NDP llentries
+	 * before in6_purgeifaddr() because the llentries are keys
+	 * for in6_multi objects of proxy NDP entries.
+	 * in6_purgeifaddr()s clean up llentries including proxy NDPs
+	 * then we would lose the keys if they are called earlier.
+	 */
+	in6_purge_proxy_ndp(ifp);
+#endif
 	while (1) {
 		struct epoch_tracker et;
 
diff --git a/sys/net/if_llatbl.c b/sys/net/if_llatbl.c
index 1e7229ba95ca..b7ea4bc8e4ea 100644
--- a/sys/net/if_llatbl.c
+++ b/sys/net/if_llatbl.c
@@ -730,6 +730,51 @@ lltable_prefix_free(int af, struct sockaddr *addr, struct sockaddr *mask,
 	LLTABLE_LIST_RUNLOCK();
 }
 
+/*
+ * Delete llentries that func() returns true.
+ */
+struct lle_match_data {
+	struct llentries dchain;
+	llt_match_cb_t *func;
+	void *farg;
+};
+
+static int
+lltable_delete_conditional_cb(struct lltable *llt, struct llentry *lle,
+    void *farg)
+{
+	struct lle_match_data *lmd;
+
+	lmd = (struct lle_match_data *)farg;
+	if (lmd->func(llt, lle, lmd->farg)) {
+		LLE_WLOCK(lle);
+		CK_LIST_INSERT_HEAD(&lmd->dchain, lle, lle_chain);
+	}
+
+	return (0);
+}
+
+void
+lltable_delete_conditional(struct lltable *llt, llt_match_cb_t *func,
+    void *farg)
+{
+	struct llentry *lle, *next;
+	struct lle_match_data lmd;
+
+	bzero(&lmd, sizeof(lmd));
+	CK_LIST_INIT(&lmd.dchain);
+	lmd.func = func;
+	lmd.farg = farg;
+
+	IF_AFDATA_WLOCK(llt->llt_ifp);
+	lltable_foreach_lle(llt, lltable_delete_conditional_cb, &lmd);
+	llentries_unlink(llt, &lmd.dchain);
+	IF_AFDATA_WUNLOCK(llt->llt_ifp);
+
+	CK_LIST_FOREACH_SAFE(lle, &lmd.dchain, lle_chain, next)
+		llt->llt_delete_entry(llt, lle);
+}
+
 struct lltable *
 lltable_allocate_htbl(uint32_t hsize)
 {
@@ -955,6 +1000,9 @@ lla_rt_output(struct rt_msghdr *rtm, struct rt_addrinfo *info)
 			lltable_unlink_entry(llt, lle_tmp);
 		}
 		lltable_link_entry(llt, lle);
+		if ((lle->la_flags & LLE_PUB) != 0 &&
+		    (llt->llt_flags & LLT_ADDEDPROXY) == 0)
+			llt->llt_flags |= LLT_ADDEDPROXY;
 		IF_AFDATA_WUNLOCK(ifp);
 
 		if (lle_tmp != NULL) {
diff --git a/sys/net/if_llatbl.h b/sys/net/if_llatbl.h
index a290fb2349aa..5c5dd7a9e08f 100644
--- a/sys/net/if_llatbl.h
+++ b/sys/net/if_llatbl.h
@@ -164,11 +164,13 @@ typedef void (llt_post_resolved_t)(struct lltable *, struct llentry *);
 
 typedef int (llt_foreach_cb_t)(struct lltable *, struct llentry *, void *);
 typedef int (llt_foreach_entry_t)(struct lltable *, llt_foreach_cb_t *, void *);
+typedef bool (llt_match_cb_t)(struct lltable *, struct llentry *, void *);
 
 struct lltable {
 	SLIST_ENTRY(lltable)	llt_link;
 	sa_family_t		llt_af;
-	uint8_t			llt_spare[3];
+	uint8_t			llt_flags;
+	uint8_t			llt_spare[2];
 	int			llt_hsize;
 	int			llt_entries;
 	int			llt_maxentries;
@@ -194,6 +196,11 @@ struct lltable {
 
 MALLOC_DECLARE(M_LLTABLE);
 
+/*
+ * LLtable flags
+ */
+#define	LLT_ADDEDPROXY	0x01	/* added a proxy llentry */
+
 /*
  * LLentry flags
  */
@@ -258,6 +265,9 @@ bool lltable_acquire_wlock(struct ifnet *ifp, struct llentry *lle);
 
 int lltable_foreach_lle(struct lltable *llt, llt_foreach_cb_t *f,
     void *farg);
+void lltable_delete_conditional(struct lltable *llt, llt_match_cb_t *func,
+    void *farg);
+
 /*
  * Generic link layer address lookup function.
  */
diff --git a/sys/netinet6/in6.c b/sys/netinet6/in6.c
index a39f7734e0ba..857e05c0f112 100644
--- a/sys/netinet6/in6.c
+++ b/sys/netinet6/in6.c
@@ -162,6 +162,9 @@ static int in6_update_ifa_internal(struct ifnet *, struct in6_aliasreq *,
 static int in6_broadcast_ifa(struct ifnet *, struct in6_aliasreq *,
     struct in6_ifaddr *, int);
 
+static void in6_join_proxy_ndp_mc(struct ifnet *, const struct in6_addr *);
+static void in6_leave_proxy_ndp_mc(struct ifnet *, const struct in6_addr *);
+
 #define ifa2ia6(ifa)	((struct in6_ifaddr *)(ifa))
 #define ia62ifa(ia6)	(&((ia6)->ia_ifa))
 
@@ -741,6 +744,26 @@ in6_joingroup_legacy(struct ifnet *ifp, const struct in6_addr *mcaddr,
 
 	return (imm);
 }
+
+static int
+in6_solicited_node_maddr(struct in6_addr *maddr,
+    struct ifnet *ifp, const struct in6_addr *base)
+{
+	int error;
+
+	bzero(maddr, sizeof(struct in6_addr));
+	maddr->s6_addr32[0] = IPV6_ADDR_INT32_MLL;
+	maddr->s6_addr32[2] = htonl(1);
+	maddr->s6_addr32[3] = base->s6_addr32[3];
+	maddr->s6_addr8[12] = 0xff;
+	if ((error = in6_setscope(maddr, ifp, NULL)) != 0) {
+		/* XXX: should not happen */
+		log(LOG_ERR, "%s: in6_setscope failed\n", __func__);
+	}
+
+	return error;
+}
+
 /*
  * Join necessary multicast groups.  Factored out from in6_update_ifa().
  * This entire work should only be done once, for the default FIB.
@@ -757,16 +780,9 @@ in6_update_ifa_join_mc(struct ifnet *ifp, struct in6_aliasreq *ifra,
 	KASSERT(in6m_sol != NULL, ("%s: in6m_sol is NULL", __func__));
 
 	/* Join solicited multicast addr for new host id. */
-	bzero(&mltaddr, sizeof(struct in6_addr));
-	mltaddr.s6_addr32[0] = IPV6_ADDR_INT32_MLL;
-	mltaddr.s6_addr32[2] = htonl(1);
-	mltaddr.s6_addr32[3] = ifra->ifra_addr.sin6_addr.s6_addr32[3];
-	mltaddr.s6_addr8[12] = 0xff;
-	if ((error = in6_setscope(&mltaddr, ifp, NULL)) != 0) {
-		/* XXX: should not happen */
-		log(LOG_ERR, "%s: in6_setscope failed\n", __func__);
+	if ((error = in6_solicited_node_maddr(&mltaddr, ifp,
+	    &ifra->ifra_addr.sin6_addr)) != 0)
 		goto cleanup;
-	}
 	delay = error = 0;
 	if ((flags & IN6_IFAUPDATE_DADDELAY)) {
 		/*
@@ -2285,6 +2301,10 @@ in6_lltable_delete_entry(struct lltable *llt, struct llentry *lle)
 {
 
 	lle->la_flags |= LLE_DELETED;
+
+	/* Leave the solicited multicast group. */
+	if ((lle->la_flags & LLE_PUB) != 0)
+		in6_leave_proxy_ndp_mc(llt->llt_ifp, &lle->r_l3addr.addr6);
 	EVENTHANDLER_INVOKE(lle_event, lle, LLENTRY_DELETED);
 #ifdef DIAGNOSTIC
 	log(LOG_INFO, "ifaddr cache = %p is deleted\n", lle);
@@ -2463,7 +2483,9 @@ in6_lltable_dump_entry(struct lltable *llt, struct llentry *lle,
 static void
 in6_lltable_post_resolved(struct lltable *llt, struct llentry *lle)
 {
-	/* Handle proxy NDP entries (not yet). */
+	/* Join the solicited multicast group for dst. */
+	if ((lle->la_flags & LLE_PUB) == LLE_PUB)
+		in6_join_proxy_ndp_mc(llt->llt_ifp, &lle->r_l3addr.addr6);
 }
 
 static struct lltable *
@@ -2621,3 +2643,72 @@ in6_sin_2_v4mapsin6_in_sock(struct sockaddr **nam)
 	free(*nam, M_SONAME);
 	*nam = (struct sockaddr *)sin6_p;
 }
+
+/*
+ * Join/leave the solicited multicast groups for proxy NDP entries.
+ */
+static void
+in6_join_proxy_ndp_mc(struct ifnet *ifp, const struct in6_addr *dst)
+{
+	struct in6_multi *inm;
+	struct in6_addr mltaddr;
+	char ip6buf[INET6_ADDRSTRLEN];
+	int error;
+
+	if (in6_solicited_node_maddr(&mltaddr, ifp, dst) != 0)
+		return;	/* error logged in in6_solicited_node_maddr. */
+
+	error = in6_joingroup(ifp, &mltaddr, NULL, &inm, 0);
+	if (error != 0) {
+		nd6log((LOG_WARNING,
+		    "%s: in6_joingroup failed for %s on %s (errno=%d)\n",
+		    __func__, ip6_sprintf(ip6buf, &mltaddr), if_name(ifp),
+		    error));
+	}
+}
+
+static void
+in6_leave_proxy_ndp_mc(struct ifnet *ifp, const struct in6_addr *dst)
+{
+	struct epoch_tracker et;
+	struct in6_multi *inm;
+	struct in6_addr mltaddr;
+	char ip6buf[INET6_ADDRSTRLEN];
+
+	if (in6_solicited_node_maddr(&mltaddr, ifp, dst) != 0)
+		return;	/* error logged in in6_solicited_node_maddr. */
+
+	NET_EPOCH_ENTER(et);
+	inm = in6m_lookup(ifp, &mltaddr);
+	NET_EPOCH_EXIT(et);
+	if (inm != NULL)
+		in6_leavegroup(inm, NULL);
+	else
+		nd6log((LOG_WARNING, "%s: in6m_lookup failed for %s on %s\n",
+		    __func__, ip6_sprintf(ip6buf, &mltaddr), if_name(ifp)));
+}
+
+static bool
+in6_lle_match_pub(struct lltable *llt, struct llentry *lle, void *farg)
+{
+	return ((lle->la_flags & LLE_PUB) != 0);
+}
+
+void
+in6_purge_proxy_ndp(struct ifnet *ifp)
+{
+	struct lltable *llt;
+	bool need_purge;
+
+	llt = LLTABLE6(ifp);
+	IF_AFDATA_WLOCK(ifp);
+	need_purge = ((llt->llt_flags & LLT_ADDEDPROXY) != 0);
+	IF_AFDATA_WUNLOCK(ifp);
+
+	/*
+	 * Ever added proxy ndp entries, leave solicited node multicast
+	 * before deleting the llentry.
+	 */
+	if (need_purge)
+		lltable_delete_conditional(llt, in6_lle_match_pub, NULL);
+}
diff --git a/sys/netinet6/in6_var.h b/sys/netinet6/in6_var.h
index d3da832d2b90..1c8e78edd46c 100644
--- a/sys/netinet6/in6_var.h
+++ b/sys/netinet6/in6_var.h
@@ -916,6 +916,8 @@ int	in6_is_addr_deprecated(struct sockaddr_in6 *);
 int	in6_src_ioctl(u_long, caddr_t);
 
 void	in6_newaddrmsg(struct in6_ifaddr *, int);
+
+void	in6_purge_proxy_ndp(struct ifnet *);
 /*
  * Extended API for IPv6 FIB support.
  */
diff --git a/sys/netinet6/nd6_nbr.c b/sys/netinet6/nd6_nbr.c
index 3a56964f8eb3..7877a7bc91b1 100644
--- a/sys/netinet6/nd6_nbr.c
+++ b/sys/netinet6/nd6_nbr.c
@@ -96,6 +96,9 @@ static void nd6_na_output_fib(struct ifnet *, const struct in6_addr *,
 static void nd6_ns_output_fib(struct ifnet *, const struct in6_addr *,
     const struct in6_addr *, const struct in6_addr *, uint8_t *, u_int);
 
+static struct ifaddr *nd6_proxy_fill_sdl(struct ifnet *,
+    const struct in6_addr *, struct sockaddr_dl *);
+
 VNET_DEFINE_STATIC(int, dad_enhanced) = 1;
 #define	V_dad_enhanced			VNET(dad_enhanced)
 
@@ -255,34 +258,8 @@ nd6_ns_input(struct mbuf *m, int off, int icmp6len)
 	/* (2) check. */
 	proxy = 0;
 	if (ifa == NULL) {
-		struct sockaddr_dl rt_gateway;
-		struct rt_addrinfo info;
-		struct sockaddr_in6 dst6;
-
-		bzero(&dst6, sizeof(dst6));
-		dst6.sin6_len = sizeof(struct sockaddr_in6);
-		dst6.sin6_family = AF_INET6;
-		dst6.sin6_addr = taddr6;
-
-		bzero(&rt_gateway, sizeof(rt_gateway));
-		rt_gateway.sdl_len = sizeof(rt_gateway);
-		bzero(&info, sizeof(info));
-		info.rti_info[RTAX_GATEWAY] = (struct sockaddr *)&rt_gateway;
-
-		if (rib_lookup_info(ifp->if_fib, (struct sockaddr *)&dst6,
-		    0, 0, &info) == 0) {
-			if ((info.rti_flags & RTF_ANNOUNCE) != 0 &&
-			    rt_gateway.sdl_family == AF_LINK) {
-				/*
-				 * proxy NDP for single entry
-				 */
-				proxydl = *SDL(&rt_gateway);
-				ifa = (struct ifaddr *)in6ifa_ifpforlinklocal(
-				    ifp, IN6_IFF_NOTREADY|IN6_IFF_ANYCAST);
-				if (ifa)
-					proxy = 1;
-			}
-		}
+		if ((ifa = nd6_proxy_fill_sdl(ifp, &taddr6, &proxydl)) != NULL)
+			proxy = 1;
 	}
 	if (ifa == NULL) {
 		/*
@@ -386,6 +363,30 @@ nd6_ns_input(struct mbuf *m, int off, int icmp6len)
 	m_freem(m);
 }
 
+static struct ifaddr *
+nd6_proxy_fill_sdl(struct ifnet *ifp, const struct in6_addr *taddr6,
+    struct sockaddr_dl *sdl)
+{
+	struct ifaddr *ifa;
+	struct llentry *ln;
+
+	ifa = NULL;
+	ln = nd6_lookup(taddr6, LLE_SF(AF_INET6, 0), ifp);
+	if (ln == NULL)
+		return (ifa);
+	if ((ln->la_flags & (LLE_PUB | LLE_VALID)) == (LLE_PUB | LLE_VALID)) {
+		link_init_sdl(ifp, (struct sockaddr *)sdl, ifp->if_type);
+		sdl->sdl_alen = ifp->if_addrlen;
+		bcopy(ln->ll_addr, &sdl->sdl_data, ifp->if_addrlen);
+		LLE_RUNLOCK(ln);
+		ifa = (struct ifaddr *)in6ifa_ifpforlinklocal(ifp,
+		    IN6_IFF_NOTREADY|IN6_IFF_ANYCAST);
+	} else
+		LLE_RUNLOCK(ln);
+
+	return (ifa);
+}
+
 /*
  * Output a Neighbor Solicitation Message. Caller specifies:
  *	- ICMP6 header source IP6 address
diff --git a/tests/sys/netinet6/Makefile b/tests/sys/netinet6/Makefile
index dfae9f698ec4..a4b6aa553b1d 100644
--- a/tests/sys/netinet6/Makefile
+++ b/tests/sys/netinet6/Makefile
@@ -15,7 +15,8 @@ ATF_TESTS_SH=			\
 				output6 \
 				lpm6 \
 				fibs6 \
-				ndp
+				ndp \
+				proxy_ndp
 TEST_METADATA.output6+=	required_programs="python"
 
 ${PACKAGE}FILES+=		exthdr.py
diff --git a/tests/sys/netinet6/proxy_ndp.sh b/tests/sys/netinet6/proxy_ndp.sh
new file mode 100755
index 000000000000..64b9d0d45b06
--- /dev/null
+++ b/tests/sys/netinet6/proxy_ndp.sh
@@ -0,0 +1,222 @@
+#!/usr/bin/env atf-sh
+#-
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright (c) 2022 KUROSAWA Takahiro <takahiro.kurosawa@gmail.com>
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+#    notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+#    notice, this list of conditions and the following disclaimer in the
+#    documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+# SUCH DAMAGE.
+#
+# $FreeBSD$
+#
+
+. $(atf_get_srcdir)/../common/vnet.subr
+
+atf_test_case "pndp_add_gu_success" "cleanup"
+pndp_add_gu_success_head() {
+	atf_set descr 'Test proxy ndp record addition'
+	atf_set require.user root
+}
+
+pndp_add_gu_success_body() {
+
+	vnet_init
+
+	jname="v6t-pndp_add_success"
+
+	epair0=$(vnet_mkepair)
+
+	vnet_mkjail ${jname} ${epair0}a
+	jexec ${jname} ndp -i ${epair0}a -- -disabled
+	jexec ${jname} ifconfig ${epair0}a up
+
+	jexec ${jname} ifconfig ${epair0}a inet6 2001:db8::1/64
+	proxy_mac=`jexec ${jname} ifconfig ${epair0}a ether | awk '$1~/ether/{print$2}'`
+
+	# wait for DAD to complete
+	while [ `jexec ${jname} ifconfig | grep inet6 | grep -c tentative` != "0" ]; do
+		sleep 0.1
+	done
+
+	atf_check jexec ${jname} ndp -s 2001:db8::2 ${proxy_mac} proxy
+	while [ `jexec ${jname} ifmcstat | grep -c undefined` != "0" ]; do
+		sleep 0.1
+	done
+
+	# checking the output of ndp -an is covered by ndp.sh.
+	# we check the output of ifmcstat output here.
+	t=`jexec ${jname} ifmcstat -i ${epair0}a -f inet6 | grep -A1 'group ff02::1:ff00:2'`
+	atf_check -o match:'mcast-macaddr 33:33:ff:00:00:02' echo $t
+}
+
+pndp_add_gu_success_cleanup() {
+	vnet_cleanup
+}
+
+atf_test_case "pndp_del_gu_success" "cleanup"
+pndp_del_gu_success_head() {
+	atf_set descr 'Test proxy ndp record deletion'
+	atf_set require.user root
+}
+
+pndp_del_gu_success_body() {
+
+	vnet_init
+
+	jname="v6t-pndp_del_gu_success"
+
+	epair0=$(vnet_mkepair)
+
+	vnet_mkjail ${jname} ${epair0}a
+
+	jexec ${jname} ndp -i ${epair0}a -- -disabled
+	jexec ${jname} ifconfig ${epair0}a up
+
+	jexec ${jname} ifconfig ${epair0}a inet6 2001:db8::1/64
+	proxy_mac=`jexec ${jname} ifconfig ${epair0}a ether | awk '$1~/ether/{print$2}'`
+
+	# wait for DAD to complete
+	while [ `jexec ${jname} ifconfig | grep inet6 | grep -c tentative` != "0" ]; do
+		sleep 0.1
+	done
+
+	atf_check jexec ${jname} ndp -s 2001:db8::2 ${proxy_mac} proxy
+	while [ `jexec ${jname} ifmcstat | grep -c undefined` != "0" ]; do
+		sleep 0.1
+	done
+	jexec ${jname} ping -c1 -t1 2001:db8::2
+
+	atf_check -o match:"2001:db8::2 \(2001:db8::2\) deleted" jexec ${jname} ndp -nd 2001:db8::2
+	while [ `jexec ${jname} ifmcstat | grep -c undefined` != "0" ]; do
+		sleep 0.1
+	done
+	atf_check \
+	    -o not-match:'group ff02::1:ff00:2' \
+	    -o not-match:'mcast-macaddr 33:33:ff:00:00:02' \
+	    jexec ${jname} ifmcstat -i ${epair0}a -f inet6
+}
+
+pndp_del_gu_success_cleanup() {
+	vnet_cleanup
+}
+
+atf_test_case "pndp_ifdestroy_success" "cleanup"
+pndp_ifdetroy_success_head() {
+	atf_set descr 'Test interface destruction with proxy ndp'
+	atf_set require.user root
+}
+
+pndp_ifdestroy_success_body() {
+
+	vnet_init
+
+	jname="v6t-pndp_ifdestroy_success"
+
+	epair0=$(vnet_mkepair)
+
+	vnet_mkjail ${jname} ${epair0}a
+
+	jexec ${jname} ndp -i ${epair0}a -- -disabled
+	jexec ${jname} ifconfig ${epair0}a up
+
+	jexec ${jname} ifconfig ${epair0}a inet6 2001:db8::1/64
+	proxy_mac=`jexec ${jname} ifconfig ${epair0}a ether | awk '$1~/ether/{print$2}'`
+
+	# wait for DAD to complete
+	while [ `jexec ${jname} ifconfig | grep inet6 | grep -c tentative` != "0" ]; do
+		sleep 0.1
+	done
+
+	atf_check jexec ${jname} ndp -s 2001:db8::2 ${proxy_mac} proxy
+	while [ `jexec ${jname} ifmcstat | grep -c undefined` != "0" ]; do
+		sleep 0.1
+	done
+
+	atf_check jexec ${jname} ifconfig ${epair0}a destroy
+}
+
+pndp_ifdestroy_success_cleanup() {
+	vnet_cleanup
+}
+
+atf_test_case "pndp_neighbor_advert" "cleanup"
+pndp_neighbor_advert_head() {
+	atf_set descr 'Test Neighbor Advertisement for proxy ndp'
+	atf_set require.user root
+}
+
+pndp_neighbor_advert_body() {
+
+	vnet_init
+
+	jname_a="v6t-pndp_neighbor_advert_a"	# NA sender (w/proxy ndp entry)
+	jname_b="v6t-pndp_neighbor_advert_b"	# NA receiver (checker)
+	proxy_addr="2001:db8::aaaa"
+
+	epair0=$(vnet_mkepair)
+
+	vnet_mkjail ${jname_a} ${epair0}a
+	jexec ${jname_a} ndp -i ${epair0}a -- -disabled
+	jexec ${jname_a} ifconfig ${epair0}a up
+	jexec ${jname_a} ifconfig ${epair0}a inet6 2001:db8::1/64
+	proxy_mac=`jexec ${jname_a} ifconfig ${epair0}a ether | awk '$1~/ether/{print$2}'`
+	# wait for DAD to complete
+	while [ `jexec ${jname_a} ifconfig | grep inet6 | grep -c tentative` != "0" ]; do
+		sleep 0.1
+	done
+	atf_check jexec ${jname_a} ndp -s ${proxy_addr} ${proxy_mac} proxy
+	while [ `jexec ${jname_a} ifmcstat | grep -c undefined` != "0" ]; do
+		sleep 0.1
+	done
+
+	vnet_mkjail ${jname_b} ${epair0}b
+	jexec ${jname_b} ndp -i ${epair0}b -- -disabled
+	jexec ${jname_b} ifconfig ${epair0}b up
+	jexec ${jname_b} ifconfig ${epair0}b inet6 2001:db8::2/64
+	# wait for DAD to complete
+	while [ `jexec ${jname_b} ifconfig | grep inet6 | grep -c tentative` != "0" ]; do
+		sleep 0.1
+	done
+
+	jexec ${jname_b} ndp -nc
+	# jname_b sends a NS before ICMPv6 Echo Request for the proxy address.
+	# jname_a responds with a NA resolving the proxy address.
+	# Then there must be a NDP entry of the proxy address in jname_b.
+	jexec ${jname_b} ping -c1 -t1 ${proxy_addr}
+	atf_check -o match:"${proxy_addr} +${proxy_mac} +${epair0}b" \
+	    jexec ${jname_b} ndp -an
+}
+
+pndp_neighbor_advert_cleanup() {
+	vnet_cleanup
+}
+
+atf_init_test_cases()
+{
+
+	atf_add_test_case "pndp_add_gu_success"
+	atf_add_test_case "pndp_del_gu_success"
+	atf_add_test_case "pndp_ifdestroy_success"
+	atf_add_test_case "pndp_neighbor_advert"
+}
+
+# end
+