2022-07-11 18:14:31

by Jaehee Park

[permalink] [raw]
Subject: [PATCH net-next 0/3] net: ipv4/ipv6: new option to accept garp/untracked na only if in-network

The first patch adds an option to learn a neighbor from garp only if
the src ip is in the same subnet of addresses configured on the
interface. The option has been added to arp_accept in ipv4.

The same feature has been added to ndisc (patch 2). For ipv6, the
subnet filtering knob is an extension of the accept_untracked_na
option introduced in these patches:
https://lore.kernel.org/all/[email protected]/t/
https://lore.kernel.org/netdev/[email protected]/T/

The third patch contains selftests for testing the different options
for accepting arp and neighbor advertisements.

Jaehee Park (3):
net: ipv4: new arp_accept option to accept garp only if in-network
net: ipv6: new accept_untracked_na option to accept na only if
in-network
selftests: net: arp_ndisc_untracked_subnets: test for arp_accept and
accept_untracked_na

Documentation/networking/ip-sysctl.rst | 48 +--
include/linux/inetdevice.h | 2 +-
net/ipv4/arp.c | 24 +-
net/ipv6/addrconf.c | 2 +-
net/ipv6/ndisc.c | 29 +-
tools/testing/selftests/net/Makefile | 1 +
.../net/arp_ndisc_untracked_subnets.sh | 281 ++++++++++++++++++
7 files changed, 360 insertions(+), 27 deletions(-)
create mode 100755 tools/testing/selftests/net/arp_ndisc_untracked_subnets.sh

--
2.30.2


2022-07-11 18:20:48

by Jaehee Park

[permalink] [raw]
Subject: [PATCH net-next 1/3] net: ipv4: new arp_accept option to accept garp only if in-network

In many deployments, we want the option to not learn a neighbor from
garp if the src ip is not in the subnet of addresses configured on the
interface. net.ipv4.arp_accept sysctl is currently used to control
creation of a neigh from a received garp packet. This patch adds a
new option '2' to net.ipv4.arp_accept which extends option '1' by
including the subnet check.

Signed-off-by: Jaehee Park <[email protected]>
Suggested-by: Roopa Prabhu <[email protected]>
---
Documentation/networking/ip-sysctl.rst | 4 +++-
include/linux/inetdevice.h | 2 +-
net/ipv4/arp.c | 24 ++++++++++++++++++++++--
3 files changed, 26 insertions(+), 4 deletions(-)

diff --git a/Documentation/networking/ip-sysctl.rst b/Documentation/networking/ip-sysctl.rst
index 4c8bbf5acfd1..599373601a2b 100644
--- a/Documentation/networking/ip-sysctl.rst
+++ b/Documentation/networking/ip-sysctl.rst
@@ -1633,12 +1633,14 @@ arp_notify - BOOLEAN
or hardware address changes.
== ==========================================================

-arp_accept - BOOLEAN
+arp_accept - INTEGER
Define behavior for gratuitous ARP frames who's IP is not
already present in the ARP table:

- 0 - don't create new entries in the ARP table
- 1 - create new entries in the ARP table
+ - 2 - create new entries only if src ip is in the same subnet as
+ the configured address on the received interface

Both replies and requests type gratuitous arp will trigger the
ARP table to be updated, if this setting is on.
diff --git a/include/linux/inetdevice.h b/include/linux/inetdevice.h
index ead323243e7b..ddb27fc0ee8c 100644
--- a/include/linux/inetdevice.h
+++ b/include/linux/inetdevice.h
@@ -131,7 +131,7 @@ static inline void ipv4_devconf_setall(struct in_device *in_dev)
IN_DEV_ORCONF((in_dev), IGNORE_ROUTES_WITH_LINKDOWN)

#define IN_DEV_ARPFILTER(in_dev) IN_DEV_ORCONF((in_dev), ARPFILTER)
-#define IN_DEV_ARP_ACCEPT(in_dev) IN_DEV_ORCONF((in_dev), ARP_ACCEPT)
+#define IN_DEV_ARP_ACCEPT(in_dev) IN_DEV_MAXCONF((in_dev), ARP_ACCEPT)
#define IN_DEV_ARP_ANNOUNCE(in_dev) IN_DEV_MAXCONF((in_dev), ARP_ANNOUNCE)
#define IN_DEV_ARP_IGNORE(in_dev) IN_DEV_MAXCONF((in_dev), ARP_IGNORE)
#define IN_DEV_ARP_NOTIFY(in_dev) IN_DEV_MAXCONF((in_dev), ARP_NOTIFY)
diff --git a/net/ipv4/arp.c b/net/ipv4/arp.c
index af2f12ffc9ca..5eedb042c50b 100644
--- a/net/ipv4/arp.c
+++ b/net/ipv4/arp.c
@@ -429,6 +429,26 @@ static int arp_ignore(struct in_device *in_dev, __be32 sip, __be32 tip)
return !inet_confirm_addr(net, in_dev, sip, tip, scope);
}

+static int arp_accept(struct in_device *in_dev, __be32 sip)
+{
+ struct net *net = dev_net(in_dev->dev);
+ int scope = RT_SCOPE_LINK;
+
+ switch (IN_DEV_ARP_ACCEPT(in_dev)) {
+ case 0: /* don't create new entries from garp */
+ return 0;
+ case 1: /* create new entries from garp */
+ return 1;
+ case 2: /*
+ * create garp only if sip is in the same subnet
+ * as an address configured on the incoming interface
+ */
+ return inet_confirm_addr(net, in_dev, sip, 0, scope) ? 1 : 0;
+ default:
+ return 0;
+ }
+}
+
static int arp_filter(__be32 sip, __be32 tip, struct net_device *dev)
{
struct rtable *rt;
@@ -868,12 +888,12 @@ static int arp_process(struct net *net, struct sock *sk, struct sk_buff *skb)
n = __neigh_lookup(&arp_tbl, &sip, dev, 0);

addr_type = -1;
- if (n || IN_DEV_ARP_ACCEPT(in_dev)) {
+ if (n || arp_accept(in_dev, sip)) {
is_garp = arp_is_garp(net, dev, &addr_type, arp->ar_op,
sip, tip, sha, tha);
}

- if (IN_DEV_ARP_ACCEPT(in_dev)) {
+ if (arp_accept(in_dev, sip)) {
/* Unsolicited ARP is not accepted by default.
It is possible, that this option should be enabled for some
devices (strip is candidate)
--
2.30.2

2022-07-11 18:32:23

by Jaehee Park

[permalink] [raw]
Subject: [PATCH net-next 3/3] selftests: net: arp_ndisc_untracked_subnets: test for arp_accept and accept_untracked_na

ipv4 arp_accept has a new option '2' to create new neighbor entries
only if the src ip is in the subnet of configured address on the
interface. This selftest tests all options in arp_accept.

ipv6 has a sysctl endpoint, accept_untracked_na, that defines the
behavior for accepting untracked neighbor advertisements. A new option
similar to that of arp_accept for learning only from the same subnet is
added to accept_untracked_na. This selftest tests this new feature.

Signed-off-by: Jaehee Park <[email protected]>
Suggested-by: Roopa Prabhu <[email protected]>
---
tools/testing/selftests/net/Makefile | 1 +
.../net/arp_ndisc_untracked_subnets.sh | 281 ++++++++++++++++++
2 files changed, 282 insertions(+)
create mode 100755 tools/testing/selftests/net/arp_ndisc_untracked_subnets.sh

diff --git a/tools/testing/selftests/net/Makefile b/tools/testing/selftests/net/Makefile
index ddad703ace34..9c2e9e303c37 100644
--- a/tools/testing/selftests/net/Makefile
+++ b/tools/testing/selftests/net/Makefile
@@ -38,6 +38,7 @@ TEST_PROGS += srv6_end_dt6_l3vpn_test.sh
TEST_PROGS += vrf_strict_mode_test.sh
TEST_PROGS += arp_ndisc_evict_nocarrier.sh
TEST_PROGS += ndisc_unsolicited_na_test.sh
+TEST_PROGS += arp_ndisc_untracked_subnets.sh
TEST_PROGS += stress_reuseport_listen.sh
TEST_PROGS_EXTENDED := in_netns.sh setup_loopback.sh setup_veth.sh
TEST_PROGS_EXTENDED += toeplitz_client.sh toeplitz.sh
diff --git a/tools/testing/selftests/net/arp_ndisc_untracked_subnets.sh b/tools/testing/selftests/net/arp_ndisc_untracked_subnets.sh
new file mode 100755
index 000000000000..57a14b4e26f2
--- /dev/null
+++ b/tools/testing/selftests/net/arp_ndisc_untracked_subnets.sh
@@ -0,0 +1,281 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+#
+# 2 namespaces: one host and one router. Use arping from the host to send a
+# garp to the router. Router accepts or ignores based on its arp_accept
+# configuration.
+
+TESTS="arp ndisc"
+
+ROUTER_NS="ns-router"
+ROUTER_NS_V6="ns-router-v6"
+ROUTER_INTF="veth-router"
+ROUTER_ADDR="10.0.10.1"
+ROUTER_ADDR_V6="2001:db8:abcd:0012::1"
+
+HOST_NS="ns-host"
+HOST_NS_V6="ns-host-v6"
+HOST_INTF="veth-host"
+HOST_ADDR="10.0.10.2"
+HOST_ADDR_V6="2001:db8:abcd:0012::2"
+
+SUBNET_WIDTH=24
+SUBNET_WIDTH_V6=64
+
+cleanup() {
+ ip netns del ${HOST_NS}
+ ip netns del ${ROUTER_NS}
+}
+
+cleanup_v6() {
+ ip netns del ${HOST_NS_V6}
+ ip netns del ${ROUTER_NS_V6}
+}
+
+setup() {
+ local arp_accept=$1
+
+ # setup two namespaces
+ ip netns add ${ROUTER_NS}
+ ip netns add ${HOST_NS}
+
+ # setup interfaces veth0 and veth1, which are pairs in separate
+ # namespaces. veth0 is veth-router, veth1 is veth-host.
+ # first, setup the inteface's link to the namespace
+ # then, set the interface "up"
+ ip netns exec ${ROUTER_NS} ip link add name ${ROUTER_INTF} \
+ type veth peer name ${HOST_INTF}
+
+ ip netns exec ${ROUTER_NS} ip link set dev ${ROUTER_INTF} up
+ ip netns exec ${ROUTER_NS} ip link set dev ${HOST_INTF} netns ${HOST_NS}
+
+ ip netns exec ${HOST_NS} ip link set dev ${HOST_INTF} up
+ ip netns exec ${ROUTER_NS} ip addr add ${ROUTER_ADDR}/${SUBNET_WIDTH} \
+ dev ${ROUTER_INTF}
+
+ ip netns exec ${HOST_NS} ip addr add ${HOST_ADDR}/${SUBNET_WIDTH} \
+ dev ${HOST_INTF}
+ ip netns exec ${HOST_NS} ip route add default via ${HOST_ADDR} \
+ dev ${HOST_INTF}
+ ip netns exec ${ROUTER_NS} ip route add default via ${ROUTER_ADDR} \
+ dev ${ROUTER_INTF}
+
+ ROUTER_CONF=net.ipv4.conf.${ROUTER_INTF}
+ ip netns exec ${ROUTER_NS} sysctl -w \
+ ${ROUTER_CONF}.arp_accept=${arp_accept} >/dev/null 2>&1
+}
+
+setup_v6() {
+ local accept_untracked_na=$1
+
+ # setup two namespaces
+ ip netns add ${ROUTER_NS_V6}
+ ip netns add ${HOST_NS_V6}
+
+ # setup interfaces veth0 and veth1, which are pairs in separate
+ # namespaces. veth0 is veth-router, veth1 is veth-host.
+ # first, setup the inteface's link to the namespace
+ # then, set the interface "up"
+ ip -6 -netns ${ROUTER_NS_V6} link add name ${ROUTER_INTF} \
+ type veth peer name ${HOST_INTF}
+
+ ip -6 -netns ${ROUTER_NS_V6} link set dev ${ROUTER_INTF} up
+ ip -6 -netns ${ROUTER_NS_V6} link set dev ${HOST_INTF} netns \
+ ${HOST_NS_V6}
+
+ ip -6 -netns ${HOST_NS_V6} link set dev ${HOST_INTF} up
+ ip -6 -netns ${ROUTER_NS_V6} addr add \
+ ${ROUTER_ADDR_V6}/${SUBNET_WIDTH_V6} dev ${ROUTER_INTF} nodad
+
+ HOST_CONF=net.ipv6.conf.${HOST_INTF}
+ ip netns exec ${HOST_NS_V6} sysctl -qw ${HOST_CONF}.ndisc_notify=1
+ ip netns exec ${HOST_NS_V6} sysctl -qw ${HOST_CONF}.disable_ipv6=0
+ ip -6 -netns ${HOST_NS_V6} addr add ${HOST_ADDR_V6}/${SUBNET_WIDTH_V6} \
+ dev ${HOST_INTF}
+
+ ROUTER_CONF=net.ipv6.conf.${ROUTER_INTF}
+
+ ip netns exec ${ROUTER_NS_V6} sysctl -w \
+ ${ROUTER_CONF}.forwarding=1 >/dev/null 2>&1
+ ip netns exec ${ROUTER_NS_V6} sysctl -w \
+ ${ROUTER_CONF}.drop_unsolicited_na=0 >/dev/null 2>&1
+ ip netns exec ${ROUTER_NS_V6} sysctl -w \
+ ${ROUTER_CONF}.accept_untracked_na=${accept_untracked_na} \
+ >/dev/null 2>&1
+}
+
+verify_arp() {
+ local arp_accept=$1
+ local same_subnet=$2
+
+ # If no entries, there's an error, so stderr would not be 0.
+ neigh_show_output=$(ip netns exec ${ROUTER_NS} ip neigh get \
+ ${HOST_ADDR} dev ${ROUTER_INTF} 2>/dev/null)
+
+ if [ ${arp_accept} -eq 1 ]; then
+ # Neighbor entries expected.
+ [[ ${neigh_show_output} ]]
+ elif [ ${arp_accept} -eq 2 ]; then
+ if [ ${same_subnet} -eq 1 ]; then
+ # Neighbor entries expected.
+ [[ ${neigh_show_output} ]]
+ else
+ [[ -z ${neigh_show_output} ]]
+ fi
+ else
+ [[ -z ${neigh_show_output} ]]
+ fi
+ }
+
+arp_test_gratuitous() {
+ local arp_accept=$1
+ local same_subnet=$2
+
+ if [ ${arp_accept} -eq 2 ]; then
+ test_msg=("test_arp: "
+ "accept_arp=$1 "
+ "same_subnet=$2")
+ if [ ${same_subnet} -eq 0 ]; then
+ HOST_ADDR=10.0.11.3
+ else
+ HOST_ADDR=10.0.10.3
+ fi
+ else
+ test_msg=("test_arp: "
+ "accept_arp=$1")
+ fi
+ # supply arp_accept option to setup which sets it in sysctl
+ setup ${arp_accept}
+ ip netns exec ${HOST_NS} arping -A -U ${HOST_ADDR} -c1 2>&1 >/dev/null
+ verify_arp $1 $2
+
+ if [ $? -eq 0 ]; then
+ printf " TEST: %-60s [ OK ]\n" "${test_msg[*]}"
+ else
+ printf " TEST: %-60s [FAIL]\n" "${test_msg[*]}"
+ fi
+ cleanup
+}
+
+arp_test_gratuitous_combinations() {
+ arp_test_gratuitous 0
+ arp_test_gratuitous 1
+ arp_test_gratuitous 2 0 # second entry indicates subnet or not
+ arp_test_gratuitous 2 1
+}
+
+cleanup_tcpdump() {
+ set -e
+ [[ ! -z ${tcpdump_stdout} ]] && rm -f ${tcpdump_stdout}
+ [[ ! -z ${tcpdump_stderr} ]] && rm -f ${tcpdump_stderr}
+ tcpdump_stdout=
+ tcpdump_stderr=
+ set +e
+}
+
+start_tcpdump() {
+ set -e
+ tcpdump_stdout=`mktemp`
+ tcpdump_stderr=`mktemp`
+ ip netns exec ${ROUTER_NS_V6} timeout 15s \
+ tcpdump --immediate-mode -tpni ${ROUTER_INTF} -c 1 \
+ "icmp6 && icmp6[0] == 136 && src ${HOST_ADDR_V6}" \
+ > ${tcpdump_stdout} 2> /dev/null
+ set +e
+}
+
+verify_ndisc() {
+ local accept_untracked_na=$1
+ local same_subnet=$2
+
+ neigh_show_output=$(ip -6 -netns ${ROUTER_NS_V6} neigh show \
+ to ${HOST_ADDR_V6} dev ${ROUTER_INTF} nud stale)
+
+ if [ ${accept_untracked_na} -eq 1 ]; then
+ # Neighbour entry expected to be present for 011 case
+ [[ ${neigh_show_output} ]]
+ elif [ ${accept_untracked_na} -eq 2 ]; then
+ if [ ${same_subnet} -eq 1 ]; then
+ [[ ${neigh_show_output} ]]
+ else
+ [[ -z ${neigh_show_output} ]]
+ fi
+ else
+ # Neighbour entry expected to be absent for all other cases
+ [[ -z ${neigh_show_output} ]]
+ fi
+}
+
+ndisc_test_untracked_advertisements() {
+ # HOST_ADDR_V6="2001:db8:91::3"
+ # ROUTER_ADDR_V6="2001:db8:91::1"
+ test_msg=("test_ndisc: "
+ "accept_untracked_na=$1")
+
+ local accept_untracked_na=$1
+ local same_subnet=$2
+ if [ ${accept_untracked_na} -eq 2 ]; then
+ test_msg=("test_ndisc: "
+ "accept_untracked_na=$1 "
+ "same_subnet=$2")
+ if [ ${same_subnet} -eq 0 ]; then
+ # not same subnet
+ HOST_ADDR_V6=2000:db8:abcd:0013::4
+ else
+ HOST_ADDR_V6=2001:db8:abcd:0012::3
+ fi
+ fi
+ setup_v6 $1 $2
+ start_tcpdump
+ verify_ndisc $1 $2
+
+ if [ $? -eq 0 ]; then
+ printf " TEST: %-60s [ OK ]\n" "${test_msg[*]}"
+ else
+ printf " TEST: %-60s [FAIL]\n" "${test_msg[*]}"
+ fi
+
+ cleanup_tcpdump
+ cleanup_v6
+}
+
+ndisc_test_untracked_combinations() {
+ ndisc_test_untracked_advertisements 0
+ ndisc_test_untracked_advertisements 1
+ ndisc_test_untracked_advertisements 2 0
+ ndisc_test_untracked_advertisements 2 1
+}
+
+################################################################################
+# usage
+
+usage()
+{
+ cat <<EOF
+usage: ${0##*/} OPTS
+
+ -t <test> Test(s) to run (default: all)
+ (options: $TESTS)
+EOF
+}
+
+################################################################################
+# main
+
+while getopts ":t:h" opt; do
+ case $opt in
+ t) TESTS=$OPTARG;;
+ h) usage; exit 0;;
+ *) usage; exit 1;;
+ esac
+done
+
+for t in $TESTS
+do
+ case $t in
+ arp_test_gratuitous_combinations|arp) arp_test_gratuitous_combinations;;
+ ndisc_test_untracked_combinations|ndisc) \
+ ndisc_test_untracked_combinations;;
+ help) echo "Test names: $TESTS"; exit 0;;
+esac
+done
--
2.30.2

2022-07-11 23:52:50

by Jaehee Park

[permalink] [raw]
Subject: Re: [PATCH net-next 0/3] net: ipv4/ipv6: new option to accept garp/untracked na only if in-network

On Mon, Jul 11, 2022 at 10:51 AM Jaehee Park <[email protected]> wrote:
>
> The first patch adds an option to learn a neighbor from garp only if
> the src ip is in the same subnet of addresses configured on the
> interface. The option has been added to arp_accept in ipv4.
>
> The same feature has been added to ndisc (patch 2). For ipv6, the
> subnet filtering knob is an extension of the accept_untracked_na
> option introduced in these patches:
> https://lore.kernel.org/all/[email protected]/t/
> https://lore.kernel.org/netdev/[email protected]/T/
>
> The third patch contains selftests for testing the different options
> for accepting arp and neighbor advertisements.
>
> Jaehee Park (3):
> net: ipv4: new arp_accept option to accept garp only if in-network
> net: ipv6: new accept_untracked_na option to accept na only if
> in-network
> selftests: net: arp_ndisc_untracked_subnets: test for arp_accept and
> accept_untracked_na
>
> Documentation/networking/ip-sysctl.rst | 48 +--
> include/linux/inetdevice.h | 2 +-
> net/ipv4/arp.c | 24 +-
> net/ipv6/addrconf.c | 2 +-
> net/ipv6/ndisc.c | 29 +-
> tools/testing/selftests/net/Makefile | 1 +
> .../net/arp_ndisc_untracked_subnets.sh | 281 ++++++++++++++++++
> 7 files changed, 360 insertions(+), 27 deletions(-)
> create mode 100755 tools/testing/selftests/net/arp_ndisc_untracked_subnets.sh
>
> --
> 2.30.2
>


I forgot a few cleanups. I will post a v2 soon.
Sorry about that!

Thanks,
Jaehee