summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--include/uapi/linux/if_link.h2
-rw-r--r--net/bridge/br_fdb.c42
-rw-r--r--net/bridge/br_netlink.c17
-rw-r--r--net/bridge/br_private.h4
-rw-r--r--tools/testing/selftests/net/forwarding/Makefile3
-rwxr-xr-xtools/testing/selftests/net/forwarding/bridge_fdb_learning_limit.sh283
6 files changed, 344 insertions, 7 deletions
diff --git a/include/uapi/linux/if_link.h b/include/uapi/linux/if_link.h
index fac351a93aed..9f8a3da0f14f 100644
--- a/include/uapi/linux/if_link.h
+++ b/include/uapi/linux/if_link.h
@@ -510,6 +510,8 @@ enum {
IFLA_BR_VLAN_STATS_PER_PORT,
IFLA_BR_MULTI_BOOLOPT,
IFLA_BR_MCAST_QUERIER_STATE,
+ IFLA_BR_FDB_N_LEARNED,
+ IFLA_BR_FDB_MAX_LEARNED,
__IFLA_BR_MAX,
};
diff --git a/net/bridge/br_fdb.c b/net/bridge/br_fdb.c
index a98ad763b368..c622de5eccd0 100644
--- a/net/bridge/br_fdb.c
+++ b/net/bridge/br_fdb.c
@@ -329,11 +329,18 @@ static void fdb_delete(struct net_bridge *br, struct net_bridge_fdb_entry *f,
hlist_del_init_rcu(&f->fdb_node);
rhashtable_remove_fast(&br->fdb_hash_tbl, &f->rhnode,
br_fdb_rht_params);
+ if (test_and_clear_bit(BR_FDB_DYNAMIC_LEARNED, &f->flags))
+ atomic_dec(&br->fdb_n_learned);
fdb_notify(br, f, RTM_DELNEIGH, swdev_notify);
call_rcu(&f->rcu, fdb_rcu_free);
}
-/* Delete a local entry if no other port had the same address. */
+/* Delete a local entry if no other port had the same address.
+ *
+ * This function should only be called on entries with BR_FDB_LOCAL set,
+ * so even with BR_FDB_ADDED_BY_USER cleared we never need to increase
+ * the accounting for dynamically learned entries again.
+ */
static void fdb_delete_local(struct net_bridge *br,
const struct net_bridge_port *p,
struct net_bridge_fdb_entry *f)
@@ -388,9 +395,20 @@ static struct net_bridge_fdb_entry *fdb_create(struct net_bridge *br,
__u16 vid,
unsigned long flags)
{
+ bool learned = !test_bit(BR_FDB_ADDED_BY_USER, &flags) &&
+ !test_bit(BR_FDB_LOCAL, &flags);
+ u32 max_learned = READ_ONCE(br->fdb_max_learned);
struct net_bridge_fdb_entry *fdb;
int err;
+ if (likely(learned)) {
+ int n_learned = atomic_read(&br->fdb_n_learned);
+
+ if (unlikely(max_learned && n_learned >= max_learned))
+ return NULL;
+ __set_bit(BR_FDB_DYNAMIC_LEARNED, &flags);
+ }
+
fdb = kmem_cache_alloc(br_fdb_cache, GFP_ATOMIC);
if (!fdb)
return NULL;
@@ -407,6 +425,9 @@ static struct net_bridge_fdb_entry *fdb_create(struct net_bridge *br,
return NULL;
}
+ if (likely(learned))
+ atomic_inc(&br->fdb_n_learned);
+
hlist_add_head_rcu(&fdb->fdb_node, &br->fdb_list);
return fdb;
@@ -912,8 +933,12 @@ void br_fdb_update(struct net_bridge *br, struct net_bridge_port *source,
clear_bit(BR_FDB_LOCKED, &fdb->flags);
}
- if (unlikely(test_bit(BR_FDB_ADDED_BY_USER, &flags)))
+ if (unlikely(test_bit(BR_FDB_ADDED_BY_USER, &flags))) {
set_bit(BR_FDB_ADDED_BY_USER, &fdb->flags);
+ if (test_and_clear_bit(BR_FDB_DYNAMIC_LEARNED,
+ &fdb->flags))
+ atomic_dec(&br->fdb_n_learned);
+ }
if (unlikely(fdb_modified)) {
trace_br_fdb_update(br, source, addr, vid, flags);
fdb_notify(br, fdb, RTM_NEWNEIGH, true);
@@ -1075,7 +1100,8 @@ static int fdb_add_entry(struct net_bridge *br, struct net_bridge_port *source,
if (!(flags & NLM_F_CREATE))
return -ENOENT;
- fdb = fdb_create(br, source, addr, vid, 0);
+ fdb = fdb_create(br, source, addr, vid,
+ BIT(BR_FDB_ADDED_BY_USER));
if (!fdb)
return -ENOMEM;
@@ -1088,6 +1114,10 @@ static int fdb_add_entry(struct net_bridge *br, struct net_bridge_port *source,
WRITE_ONCE(fdb->dst, source);
modified = true;
}
+
+ set_bit(BR_FDB_ADDED_BY_USER, &fdb->flags);
+ if (test_and_clear_bit(BR_FDB_DYNAMIC_LEARNED, &fdb->flags))
+ atomic_dec(&br->fdb_n_learned);
}
if (fdb_to_nud(br, fdb) != state) {
@@ -1119,8 +1149,6 @@ static int fdb_add_entry(struct net_bridge *br, struct net_bridge_port *source,
if (fdb_handle_notify(fdb, notify))
modified = true;
- set_bit(BR_FDB_ADDED_BY_USER, &fdb->flags);
-
fdb->used = jiffies;
if (modified) {
if (refresh)
@@ -1464,6 +1492,10 @@ int br_fdb_external_learn_add(struct net_bridge *br, struct net_bridge_port *p,
if (!p)
set_bit(BR_FDB_LOCAL, &fdb->flags);
+ if ((swdev_notify || !p) &&
+ test_and_clear_bit(BR_FDB_DYNAMIC_LEARNED, &fdb->flags))
+ atomic_dec(&br->fdb_n_learned);
+
if (modified)
fdb_notify(br, fdb, RTM_NEWNEIGH, swdev_notify);
}
diff --git a/net/bridge/br_netlink.c b/net/bridge/br_netlink.c
index 10f0d33d8ccf..5ad4abfcb7ba 100644
--- a/net/bridge/br_netlink.c
+++ b/net/bridge/br_netlink.c
@@ -1229,6 +1229,8 @@ static size_t br_port_get_slave_size(const struct net_device *brdev,
}
static const struct nla_policy br_policy[IFLA_BR_MAX + 1] = {
+ [IFLA_BR_UNSPEC] = { .strict_start_type =
+ IFLA_BR_FDB_N_LEARNED },
[IFLA_BR_FORWARD_DELAY] = { .type = NLA_U32 },
[IFLA_BR_HELLO_TIME] = { .type = NLA_U32 },
[IFLA_BR_MAX_AGE] = { .type = NLA_U32 },
@@ -1265,6 +1267,8 @@ static const struct nla_policy br_policy[IFLA_BR_MAX + 1] = {
[IFLA_BR_VLAN_STATS_PER_PORT] = { .type = NLA_U8 },
[IFLA_BR_MULTI_BOOLOPT] =
NLA_POLICY_EXACT_LEN(sizeof(struct br_boolopt_multi)),
+ [IFLA_BR_FDB_N_LEARNED] = { .type = NLA_REJECT },
+ [IFLA_BR_FDB_MAX_LEARNED] = { .type = NLA_U32 },
};
static int br_changelink(struct net_device *brdev, struct nlattr *tb[],
@@ -1539,6 +1543,12 @@ static int br_changelink(struct net_device *brdev, struct nlattr *tb[],
return err;
}
+ if (data[IFLA_BR_FDB_MAX_LEARNED]) {
+ u32 val = nla_get_u32(data[IFLA_BR_FDB_MAX_LEARNED]);
+
+ WRITE_ONCE(br->fdb_max_learned, val);
+ }
+
return 0;
}
@@ -1593,6 +1603,8 @@ static size_t br_get_size(const struct net_device *brdev)
nla_total_size_64bit(sizeof(u64)) + /* IFLA_BR_TOPOLOGY_CHANGE_TIMER */
nla_total_size_64bit(sizeof(u64)) + /* IFLA_BR_GC_TIMER */
nla_total_size(ETH_ALEN) + /* IFLA_BR_GROUP_ADDR */
+ nla_total_size(sizeof(u32)) + /* IFLA_BR_FDB_N_LEARNED */
+ nla_total_size(sizeof(u32)) + /* IFLA_BR_FDB_MAX_LEARNED */
#ifdef CONFIG_BRIDGE_IGMP_SNOOPING
nla_total_size(sizeof(u8)) + /* IFLA_BR_MCAST_ROUTER */
nla_total_size(sizeof(u8)) + /* IFLA_BR_MCAST_SNOOPING */
@@ -1668,7 +1680,10 @@ static int br_fill_info(struct sk_buff *skb, const struct net_device *brdev)
nla_put_u8(skb, IFLA_BR_TOPOLOGY_CHANGE_DETECTED,
br->topology_change_detected) ||
nla_put(skb, IFLA_BR_GROUP_ADDR, ETH_ALEN, br->group_addr) ||
- nla_put(skb, IFLA_BR_MULTI_BOOLOPT, sizeof(bm), &bm))
+ nla_put(skb, IFLA_BR_MULTI_BOOLOPT, sizeof(bm), &bm) ||
+ nla_put_u32(skb, IFLA_BR_FDB_N_LEARNED,
+ atomic_read(&br->fdb_n_learned)) ||
+ nla_put_u32(skb, IFLA_BR_FDB_MAX_LEARNED, br->fdb_max_learned))
return -EMSGSIZE;
#ifdef CONFIG_BRIDGE_VLAN_FILTERING
diff --git a/net/bridge/br_private.h b/net/bridge/br_private.h
index cbbe35278459..27a7a06660f3 100644
--- a/net/bridge/br_private.h
+++ b/net/bridge/br_private.h
@@ -274,6 +274,7 @@ enum {
BR_FDB_NOTIFY,
BR_FDB_NOTIFY_INACTIVE,
BR_FDB_LOCKED,
+ BR_FDB_DYNAMIC_LEARNED,
};
struct net_bridge_fdb_key {
@@ -555,6 +556,9 @@ struct net_bridge {
struct kobject *ifobj;
u32 auto_cnt;
+ atomic_t fdb_n_learned;
+ u32 fdb_max_learned;
+
#ifdef CONFIG_NET_SWITCHDEV
/* Counter used to make sure that hardware domains get unique
* identifiers in case a bridge spans multiple switchdev instances.
diff --git a/tools/testing/selftests/net/forwarding/Makefile b/tools/testing/selftests/net/forwarding/Makefile
index 74e754e266c3..df593b7b3e6b 100644
--- a/tools/testing/selftests/net/forwarding/Makefile
+++ b/tools/testing/selftests/net/forwarding/Makefile
@@ -1,6 +1,7 @@
# SPDX-License-Identifier: GPL-2.0+ OR MIT
-TEST_PROGS = bridge_igmp.sh \
+TEST_PROGS = bridge_fdb_learning_limit.sh \
+ bridge_igmp.sh \
bridge_locked_port.sh \
bridge_mdb.sh \
bridge_mdb_host.sh \
diff --git a/tools/testing/selftests/net/forwarding/bridge_fdb_learning_limit.sh b/tools/testing/selftests/net/forwarding/bridge_fdb_learning_limit.sh
new file mode 100755
index 000000000000..0760a34b7114
--- /dev/null
+++ b/tools/testing/selftests/net/forwarding/bridge_fdb_learning_limit.sh
@@ -0,0 +1,283 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+
+# ShellCheck incorrectly believes that most of the code here is unreachable
+# because it's invoked by variable name following ALL_TESTS.
+#
+# shellcheck disable=SC2317
+
+ALL_TESTS="check_accounting check_limit"
+NUM_NETIFS=6
+source lib.sh
+
+TEST_MAC_BASE=de:ad:be:ef:42:
+
+NUM_PKTS=16
+FDB_LIMIT=8
+
+FDB_TYPES=(
+ # name is counted? overrides learned?
+ 'learned 1 0'
+ 'static 0 1'
+ 'user 0 1'
+ 'extern_learn 0 1'
+ 'local 0 1'
+)
+
+mac()
+{
+ printf "${TEST_MAC_BASE}%02x" "$1"
+}
+
+H1_DEFAULT_MAC=$(mac 42)
+
+switch_create()
+{
+ ip link add dev br0 type bridge
+
+ ip link set dev "$swp1" master br0
+ ip link set dev "$swp2" master br0
+ # swp3 is used to add local MACs, so do not add it to the bridge yet.
+
+ # swp2 is only used for replying when learning on swp1, its MAC should not be learned.
+ ip link set dev "$swp2" type bridge_slave learning off
+
+ ip link set dev br0 up
+
+ ip link set dev "$swp1" up
+ ip link set dev "$swp2" up
+ ip link set dev "$swp3" up
+}
+
+switch_destroy()
+{
+ ip link set dev "$swp3" down
+ ip link set dev "$swp2" down
+ ip link set dev "$swp1" down
+
+ ip link del dev br0
+}
+
+h_create()
+{
+ ip link set "$h1" addr "$H1_DEFAULT_MAC"
+
+ simple_if_init "$h1" 192.0.2.1/24
+ simple_if_init "$h2" 192.0.2.2/24
+}
+
+h_destroy()
+{
+ simple_if_fini "$h1" 192.0.2.1/24
+ simple_if_fini "$h2" 192.0.2.2/24
+}
+
+setup_prepare()
+{
+ h1=${NETIFS[p1]}
+ swp1=${NETIFS[p2]}
+
+ h2=${NETIFS[p3]}
+ swp2=${NETIFS[p4]}
+
+ swp3=${NETIFS[p6]}
+
+ vrf_prepare
+
+ h_create
+
+ switch_create
+}
+
+cleanup()
+{
+ pre_cleanup
+
+ switch_destroy
+
+ h_destroy
+
+ vrf_cleanup
+}
+
+fdb_get_n_learned()
+{
+ ip -d -j link show dev br0 type bridge | \
+ jq '.[]["linkinfo"]["info_data"]["fdb_n_learned"]'
+}
+
+fdb_get_n_mac()
+{
+ local mac=${1}
+
+ bridge -j fdb show br br0 | \
+ jq "map(select(.mac == \"${mac}\" and (has(\"vlan\") | not))) | length"
+}
+
+fdb_fill_learned()
+{
+ local i
+
+ for i in $(seq 1 "$NUM_PKTS"); do
+ fdb_add learned "$(mac "$i")"
+ done
+}
+
+fdb_reset()
+{
+ bridge fdb flush dev br0
+
+ # Keep the default MAC address of h1 in the table. We set it to a different one when
+ # testing dynamic learning.
+ bridge fdb add "$H1_DEFAULT_MAC" dev "$swp1" master static use
+}
+
+fdb_add()
+{
+ local type=$1 mac=$2
+
+ case "$type" in
+ learned)
+ ip link set "$h1" addr "$mac"
+ # Wait for a reply so we implicitly wait until after the forwarding
+ # code finished and the FDB entry was created.
+ PING_COUNT=1 ping_do "$h1" 192.0.2.2
+ check_err $? "Failed to ping another bridge port"
+ ip link set "$h1" addr "$H1_DEFAULT_MAC"
+ ;;
+ local)
+ ip link set dev "$swp3" addr "$mac" && ip link set "$swp3" master br0
+ ;;
+ static)
+ bridge fdb replace "$mac" dev "$swp1" master static
+ ;;
+ user)
+ bridge fdb replace "$mac" dev "$swp1" master static use
+ ;;
+ extern_learn)
+ bridge fdb replace "$mac" dev "$swp1" master extern_learn
+ ;;
+ esac
+
+ check_err $? "Failed to add a FDB entry of type ${type}"
+}
+
+fdb_del()
+{
+ local type=$1 mac=$2
+
+ case "$type" in
+ local)
+ ip link set "$swp3" nomaster
+ ;;
+ *)
+ bridge fdb del "$mac" dev "$swp1" master
+ ;;
+ esac
+
+ check_err $? "Failed to remove a FDB entry of type ${type}"
+}
+
+check_accounting_one_type()
+{
+ local type=$1 is_counted=$2 overrides_learned=$3
+ shift 3
+ RET=0
+
+ fdb_reset
+ fdb_add "$type" "$(mac 0)"
+ learned=$(fdb_get_n_learned)
+ [ "$learned" -ne "$is_counted" ]
+ check_fail $? "Inserted FDB type ${type}: Expected the count ${is_counted}, but got ${learned}"
+
+ fdb_del "$type" "$(mac 0)"
+ learned=$(fdb_get_n_learned)
+ [ "$learned" -ne 0 ]
+ check_fail $? "Removed FDB type ${type}: Expected the count 0, but got ${learned}"
+
+ if [ "$overrides_learned" -eq 1 ]; then
+ fdb_reset
+ fdb_add learned "$(mac 0)"
+ fdb_add "$type" "$(mac 0)"
+ learned=$(fdb_get_n_learned)
+ [ "$learned" -ne "$is_counted" ]
+ check_fail $? "Set a learned entry to FDB type ${type}: Expected the count ${is_counted}, but got ${learned}"
+ fdb_del "$type" "$(mac 0)"
+ fi
+
+ log_test "FDB accounting interacting with FDB type ${type}"
+}
+
+check_accounting()
+{
+ local type_args learned
+ RET=0
+
+ fdb_reset
+ learned=$(fdb_get_n_learned)
+ [ "$learned" -ne 0 ]
+ check_fail $? "Flushed the FDB table: Expected the count 0, but got ${learned}"
+
+ fdb_fill_learned
+ sleep 1
+
+ learned=$(fdb_get_n_learned)
+ [ "$learned" -ne "$NUM_PKTS" ]
+ check_fail $? "Filled the FDB table: Expected the count ${NUM_PKTS}, but got ${learned}"
+
+ log_test "FDB accounting"
+
+ for type_args in "${FDB_TYPES[@]}"; do
+ # This is intentional use of word splitting.
+ # shellcheck disable=SC2086
+ check_accounting_one_type $type_args
+ done
+}
+
+check_limit_one_type()
+{
+ local type=$1 is_counted=$2
+ local n_mac expected=$((1 - is_counted))
+ RET=0
+
+ fdb_reset
+ fdb_fill_learned
+
+ fdb_add "$type" "$(mac 0)"
+ n_mac=$(fdb_get_n_mac "$(mac 0)")
+ [ "$n_mac" -ne "$expected" ]
+ check_fail $? "Inserted FDB type ${type} at limit: Expected the count ${expected}, but got ${n_mac}"
+
+ log_test "FDB limits interacting with FDB type ${type}"
+}
+
+check_limit()
+{
+ local learned
+ RET=0
+
+ ip link set br0 type bridge fdb_max_learned "$FDB_LIMIT"
+
+ fdb_reset
+ fdb_fill_learned
+
+ learned=$(fdb_get_n_learned)
+ [ "$learned" -ne "$FDB_LIMIT" ]
+ check_fail $? "Filled the limited FDB table: Expected the count ${FDB_LIMIT}, but got ${learned}"
+
+ log_test "FDB limits"
+
+ for type_args in "${FDB_TYPES[@]}"; do
+ # This is intentional use of word splitting.
+ # shellcheck disable=SC2086
+ check_limit_one_type $type_args
+ done
+}
+
+trap cleanup EXIT
+
+setup_prepare
+
+tests_run
+
+exit $EXIT_STATUS