diff mbox series

bridge: extend BR_ISOLATE to full split-horizon

Message ID 20211209121432.473979-1-equinox@diac24.net (mailing list archive)
State Changes Requested
Delegated to: Netdev Maintainers
Headers show
Series bridge: extend BR_ISOLATE to full split-horizon | expand

Checks

Context Check Description
netdev/tree_selection success Not a local patch

Commit Message

David Lamparter Dec. 9, 2021, 12:14 p.m. UTC
Split-horizon essentially just means being able to create multiple
groups of isolated ports that are isolated within the group, but not
with respect to each other.

The intent is very different, while isolation is a policy feature,
split-horizon is intended to provide functional "multiple member ports
are treated as one for loop avoidance."  But it boils down to the same
thing in the end.

Signed-off-by: David Lamparter <equinox@diac24.net>
Cc: Nikolay Aleksandrov <nikolay@nvidia.com>
Cc: Alexandra Winter <wintera@linux.ibm.com>
---

Alexandra, could you double check my change to the qeth_l2 driver?  I
can't really test it...

Cheers,

David
---
 drivers/s390/net/qeth_l2_main.c | 10 ++++++----
 include/linux/if_bridge.h       |  9 ++++++++-
 include/uapi/linux/if_link.h    |  1 +
 net/bridge/br_if.c              | 12 ++++++++++++
 net/bridge/br_input.c           |  2 +-
 net/bridge/br_netlink.c         | 33 +++++++++++++++++++++++++++++++--
 net/bridge/br_private.h         | 13 ++++++++++---
 net/bridge/br_sysfs_if.c        | 33 ++++++++++++++++++++++++++++++++-
 8 files changed, 101 insertions(+), 12 deletions(-)

Comments

Jakub Kicinski Dec. 9, 2021, 3:42 p.m. UTC | #1
On Thu,  9 Dec 2021 13:14:32 +0100 David Lamparter wrote:
> Split-horizon essentially just means being able to create multiple
> groups of isolated ports that are isolated within the group, but not
> with respect to each other.
> 
> The intent is very different, while isolation is a policy feature,
> split-horizon is intended to provide functional "multiple member ports
> are treated as one for loop avoidance."  But it boils down to the same
> thing in the end.
> 
> Signed-off-by: David Lamparter <equinox@diac24.net>
> Cc: Nikolay Aleksandrov <nikolay@nvidia.com>
> Cc: Alexandra Winter <wintera@linux.ibm.com>

Does not apply to net-next, you'll need to repost even if the code is
good. Please put [PATCH net-next] in the subject.
Nikolay Aleksandrov Dec. 9, 2021, 4:08 p.m. UTC | #2
On 09/12/2021 17:42, Jakub Kicinski wrote:
> On Thu,  9 Dec 2021 13:14:32 +0100 David Lamparter wrote:
>> Split-horizon essentially just means being able to create multiple
>> groups of isolated ports that are isolated within the group, but not
>> with respect to each other.
>>
>> The intent is very different, while isolation is a policy feature,
>> split-horizon is intended to provide functional "multiple member ports
>> are treated as one for loop avoidance."  But it boils down to the same
>> thing in the end.
>>
>> Signed-off-by: David Lamparter <equinox@diac24.net>
>> Cc: Nikolay Aleksandrov <nikolay@nvidia.com>
>> Cc: Alexandra Winter <wintera@linux.ibm.com>
> 
> Does not apply to net-next, you'll need to repost even if the code is
> good. Please put [PATCH net-next] in the subject.
> 

Hi,
For some reason this patch didn't make it to my inbox.. Anyway I was
able to see it now online, a few comments (sorry can't do them inline due
to missing mbox patch):
- please drop the sysfs part, we're not extending sysfs anymore
- split the bridge change from the driver
- drop the /* BR_ISOLATED - previously BIT(16) */ comment
- [IFLA_BRPORT_HORIZON_GROUP] = NLA_POLICY_MIN(NLA_S32, 0), why not just { .type = NLA_U32 } ?
- just forbid having both set (tb[IFLA_BRPORT_ISOLATED] && tb[IFLA_BRPORT_HORIZON_GROUP])
  user-space should use just one of the two, if isolated is set then it overwrites any older
  IFLA_BRPORT_HORIZON_GROUP settings, that should simplify things considerably

Why the limitation (UAPI limited to positive signed int. (recommended ifindex namespace)) ?
You have the full unsigned space available, user-space can use it as it sees fit.
You can just remove the comment about recommended ifindex.

Also please extend the port isolation self-test with a test for a different horizon group.

Thanks,
 Nik
Nikolay Aleksandrov Dec. 9, 2021, 4:23 p.m. UTC | #3
On 09/12/2021 18:08, Nikolay Aleksandrov wrote:
> On 09/12/2021 17:42, Jakub Kicinski wrote:
>> On Thu,  9 Dec 2021 13:14:32 +0100 David Lamparter wrote:
>>> Split-horizon essentially just means being able to create multiple
>>> groups of isolated ports that are isolated within the group, but not
>>> with respect to each other.
>>>
>>> The intent is very different, while isolation is a policy feature,
>>> split-horizon is intended to provide functional "multiple member ports
>>> are treated as one for loop avoidance."  But it boils down to the same
>>> thing in the end.
>>>
>>> Signed-off-by: David Lamparter <equinox@diac24.net>
>>> Cc: Nikolay Aleksandrov <nikolay@nvidia.com>
>>> Cc: Alexandra Winter <wintera@linux.ibm.com>
>>
>> Does not apply to net-next, you'll need to repost even if the code is
>> good. Please put [PATCH net-next] in the subject.
>>
> 
> Hi,
> For some reason this patch didn't make it to my inbox.. Anyway I was
> able to see it now online, a few comments (sorry can't do them inline due
> to missing mbox patch):
> - please drop the sysfs part, we're not extending sysfs anymore
> - split the bridge change from the driver
> - drop the /* BR_ISOLATED - previously BIT(16) */ comment
> - [IFLA_BRPORT_HORIZON_GROUP] = NLA_POLICY_MIN(NLA_S32, 0), why not just { .type = NLA_U32 } ?

> - just forbid having both set (tb[IFLA_BRPORT_ISOLATED] && tb[IFLA_BRPORT_HORIZON_GROUP])
>   user-space should use just one of the two, if isolated is set then it overwrites any older
>   IFLA_BRPORT_HORIZON_GROUP settings, that should simplify things considerably

Actually they'll have to be exported together always... that's unfortunate. I get
why you need the extra netlink logic, I think we'll have to keep it.
Alexandra Winter Dec. 9, 2021, 4:23 p.m. UTC | #4
On 09.12.21 17:08, Nikolay Aleksandrov wrote:
> On 09/12/2021 17:42, Jakub Kicinski wrote:
>> On Thu,  9 Dec 2021 13:14:32 +0100 David Lamparter wrote:
>>> Split-horizon essentially just means being able to create multiple
>>> groups of isolated ports that are isolated within the group, but not
>>> with respect to each other.
>>>
>>> The intent is very different, while isolation is a policy feature,
>>> split-horizon is intended to provide functional "multiple member ports
>>> are treated as one for loop avoidance."  But it boils down to the same
>>> thing in the end.
>>>
>>> Signed-off-by: David Lamparter <equinox@diac24.net>
>>> Cc: Nikolay Aleksandrov <nikolay@nvidia.com>
>>> Cc: Alexandra Winter <wintera@linux.ibm.com>
>>
>> Does not apply to net-next, you'll need to repost even if the code is
>> good. Please put [PATCH net-next] in the subject.
>>
> 
> Hi,
> For some reason this patch didn't make it to my inbox.. Anyway I was
> able to see it now online, a few comments (sorry can't do them inline due
> to missing mbox patch):
> - please drop the sysfs part, we're not extending sysfs anymore
> - split the bridge change from the driver
> - drop the /* BR_ISOLATED - previously BIT(16) */ comment
> - [IFLA_BRPORT_HORIZON_GROUP] = NLA_POLICY_MIN(NLA_S32, 0), why not just { .type = NLA_U32 } ?
> - just forbid having both set (tb[IFLA_BRPORT_ISOLATED] && tb[IFLA_BRPORT_HORIZON_GROUP])
>   user-space should use just one of the two, if isolated is set then it overwrites any older
>   IFLA_BRPORT_HORIZON_GROUP settings, that should simplify things considerably
Yes, please keep it compatible with userspace setting IFLA_BRPORT_ISOLATED only.
> 
> Why the limitation (UAPI limited to positive signed int. (recommended ifindex namespace)) ?
> You have the full unsigned space available, user-space can use it as it sees fit.
> You can just remove the comment about recommended ifindex.
> 
> Also please extend the port isolation self-test with a test for a different horizon group.
> 
> Thanks,
>  Nik
> 
> 
> 
>
Nikolay Aleksandrov Dec. 9, 2021, 4:57 p.m. UTC | #5
On 09/12/2021 18:23, Nikolay Aleksandrov wrote:
> On 09/12/2021 18:08, Nikolay Aleksandrov wrote:
>> On 09/12/2021 17:42, Jakub Kicinski wrote:
>>> On Thu,  9 Dec 2021 13:14:32 +0100 David Lamparter wrote:
>>>> Split-horizon essentially just means being able to create multiple
>>>> groups of isolated ports that are isolated within the group, but not
>>>> with respect to each other.
>>>>
>>>> The intent is very different, while isolation is a policy feature,
>>>> split-horizon is intended to provide functional "multiple member ports
>>>> are treated as one for loop avoidance."  But it boils down to the same
>>>> thing in the end.
>>>>
>>>> Signed-off-by: David Lamparter <equinox@diac24.net>
>>>> Cc: Nikolay Aleksandrov <nikolay@nvidia.com>
>>>> Cc: Alexandra Winter <wintera@linux.ibm.com>
>>>
>>> Does not apply to net-next, you'll need to repost even if the code is
>>> good. Please put [PATCH net-next] in the subject.
>>>
>>
>> Hi,
>> For some reason this patch didn't make it to my inbox.. Anyway I was
>> able to see it now online, a few comments (sorry can't do them inline due
>> to missing mbox patch):
>> - please drop the sysfs part, we're not extending sysfs anymore
>> - split the bridge change from the driver
>> - drop the /* BR_ISOLATED - previously BIT(16) */ comment
>> - [IFLA_BRPORT_HORIZON_GROUP] = NLA_POLICY_MIN(NLA_S32, 0), why not just { .type = NLA_U32 } ?
> 
>> - just forbid having both set (tb[IFLA_BRPORT_ISOLATED] && tb[IFLA_BRPORT_HORIZON_GROUP])
>>   user-space should use just one of the two, if isolated is set then it overwrites any older
>>   IFLA_BRPORT_HORIZON_GROUP settings, that should simplify things considerably
> 
> Actually they'll have to be exported together always... that's unfortunate. I get
> why you need the extra netlink logic, I think we'll have to keep it.
So one relatively simple way to drop most of the logic is to have BRPORT_HORIZON_GROUP's
attribute get set after ISOLATED so it always overwrites it, which is similar to the current
situation but if you set only ISOLATED later it will function as intended (i.e. drop the logic
for horizon_group when using the old attr). Still will have to disallow ISOLATED = 0/HORIZON_GROUP >0
and ISOLATED=1/HORIZON_GROUP=0 though as these don't make sense.

e.g.:
if (tb[IFLA_BRPORT_ISOLATED])
	p->horizon_group = !!nla_get_u8(tb[IFLA_BRPORT_ISOLATED]);
if (tb[IFLA_BRPORT_HORIZON_GROUP])
	p->horizon_group = nla_get_u32(tb[IFLA_BRPORT_HORIZON_GROUP]);



This will be also in line with how they're exported (ISOLATED = 1 and HORIZON_GROUP >0).
David Lamparter Dec. 10, 2021, 4:49 p.m. UTC | #6
Thanks all for the feedback!
(rolled up several mails below)

On Thu, Dec 09, 2021 at 07:42:04AM -0800, Jakub Kicinski wrote:
> Does not apply to net-next, you'll need to repost even if the code is
> good. Please put [PATCH net-next] in the subject.

Apologies, I think I skipped a step in my rebase somewhere.

On Thu, Dec 09, 2021 at 06:08:03PM +0200, Nikolay Aleksandrov wrote:
> - please drop the sysfs part, we're not extending sysfs anymore

ACK (will remove the "horizon_group" file, change to "isolated" remains)

> - split the bridge change from the driver

ACK - I wasn't sure if it's OK if the intermediate doesn't compile due
to deleted BR_ISOLATED?  Or should I make 3 patches with only the 3rd
removing the flag definition?

> - drop the /* BR_ISOLATED - previously BIT(16) */ comment

Should I move up the other bits below it or just leave a hole at 16?

> - [IFLA_BRPORT_HORIZON_GROUP] = NLA_POLICY_MIN(NLA_S32, 0), why not just { .type = NLA_U32 } ?
>
> Why the limitation (UAPI limited to positive signed int. (recommended ifindex namespace)) ?
> You have the full unsigned space available, user-space can use it as it sees fit.
> You can just remove the comment about recommended ifindex.

No problem, I guess I'm overthinking this a bit.  Using the ifindex is
just a "good way" of avoiding confusion if multiple things in userspace
try to configure this.  Kernel really doesn't care.

> Also please extend the port isolation self-test with a test for a different horizon group.

ACK

> - just forbid having both set (tb[IFLA_BRPORT_ISOLATED] && tb[IFLA_BRPORT_HORIZON_GROUP])
>   user-space should use just one of the two, if isolated is set then it overwrites any older
>   IFLA_BRPORT_HORIZON_GROUP settings, that should simplify things considerably

So a bit of a hidden question here is which way "BR_ISOLATED" maps -
both "ISOLATED :=: group >= 1" and "ISOLATED :=: group == 1" are
possible.  But regardless of which way it's defined, there's to some
degree a risk of "old" tools accidentally wrecking the horizon group
setting.  That's why I ended up making BR_ISOLATE=1 not change the
horizon group if it's nonzero already...

On Thu, Dec 09, 2021 at 05:23:35PM +0100, Alexandra Winter wrote:
> Yes, please keep it compatible with userspace setting IFLA_BRPORT_ISOLATED only.

On Thu, Dec 09, 2021 at 06:23:25PM +0200, Nikolay Aleksandrov wrote:
> Actually they'll have to be exported together always... that's unfortunate. I get
> why you need the extra netlink logic, I think we'll have to keep it.

Yeah - we can't remove BR_ISOLATED from netlink GETs to keep compat, so
we need to accept it in SETs too in order to not break userspace handing
it right back in 1:1 for a get(-modify)-set...

On Thu, Dec 09, 2021 at 06:57:23PM +0200, Nikolay Aleksandrov wrote:
> So one relatively simple way to drop most of the logic is to have BRPORT_HORIZON_GROUP's
> attribute get set after ISOLATED so it always overwrites it, which is similar to the current
> situation but if you set only ISOLATED later it will function as intended (i.e. drop the logic
> for horizon_group when using the old attr). Still will have to disallow ISOLATED = 0/HORIZON_GROUP >0
> and ISOLATED=1/HORIZON_GROUP=0 though as these don't make sense.
>
> e.g.:
> if (tb[IFLA_BRPORT_ISOLATED])
> 	p->horizon_group = !!nla_get_u8(tb[IFLA_BRPORT_ISOLATED]);
> if (tb[IFLA_BRPORT_HORIZON_GROUP])
> 	p->horizon_group = nla_get_u32(tb[IFLA_BRPORT_HORIZON_GROUP]);
>
> This will be also in line with how they're exported (ISOLATED = 1 and HORIZON_GROUP >0).

This boils down to the same logic as is currently in the patch, except
it's written the other way around and with an else, i.e.

if (tb[IFLA_BRPORT_HORIZON_GROUP])
	p->horizon_group = ...
else if (tb[IFLA_BRPORT_ISOLATED])
	p->horizon_group = ...

With the else it seems more obvious to me, to avoid someone in the
future missing the fact that the 2 settings interact - so I would
preferably keep it like this.


Cheers,

-David
Alexandra Winter Dec. 10, 2021, 4:53 p.m. UTC | #7
On 09.12.21 13:14, David Lamparter wrote:
> Split-horizon essentially just means being able to create multiple
> groups of isolated ports that are isolated within the group, but not
> with respect to each other.
> 
> The intent is very different, while isolation is a policy feature,
> split-horizon is intended to provide functional "multiple member ports
> are treated as one for loop avoidance."  But it boils down to the same
> thing in the end.
> 
> Signed-off-by: David Lamparter <equinox@diac24.net>
> Cc: Nikolay Aleksandrov <nikolay@nvidia.com>
> Cc: Alexandra Winter <wintera@linux.ibm.com>
> ---
> 
> Alexandra, could you double check my change to the qeth_l2 driver?  I
> can't really test it...
> 
> Cheers,
> 
> David
> ---
Reviewed and tested for s390/qeth. Looks good to me, see 2 comments below.
Kind regards 
Alexandra


>  drivers/s390/net/qeth_l2_main.c | 10 ++++++----
>  include/linux/if_bridge.h       |  9 ++++++++-
>  include/uapi/linux/if_link.h    |  1 +
>  net/bridge/br_if.c              | 12 ++++++++++++
>  net/bridge/br_input.c           |  2 +-
>  net/bridge/br_netlink.c         | 33 +++++++++++++++++++++++++++++++--
>  net/bridge/br_private.h         | 13 ++++++++++---
>  net/bridge/br_sysfs_if.c        | 33 ++++++++++++++++++++++++++++++++-
>  8 files changed, 101 insertions(+), 12 deletions(-)
> 
> diff --git a/drivers/s390/net/qeth_l2_main.c b/drivers/s390/net/qeth_l2_main.c
> index 303461d70af3..405d36757c22 100644
> --- a/drivers/s390/net/qeth_l2_main.c
> +++ b/drivers/s390/net/qeth_l2_main.c
> @@ -729,8 +729,8 @@ static bool qeth_l2_must_learn(struct net_device *netdev,
>  	priv = netdev_priv(netdev);
>  	return (netdev != dstdev &&
>  		(priv->brport_features & BR_LEARNING_SYNC) &&
> -		!(br_port_flag_is_set(netdev, BR_ISOLATED) &&
> -		  br_port_flag_is_set(dstdev, BR_ISOLATED)) &&
> +		!(br_port_horizon_group(netdev) != 0 &&
> +		  br_port_horizon_group(netdev) == br_port_horizon_group(dstdev)) &&
WARNING: line length of 84 exceeds 80 columns

>  		(netdev->netdev_ops == &qeth_l2_iqd_netdev_ops ||
>  		 netdev->netdev_ops == &qeth_l2_osa_netdev_ops));
>  }
> @@ -757,6 +757,7 @@ static void qeth_l2_br2dev_worker(struct work_struct *work)
>  	struct net_device *lowerdev;
>  	struct list_head *iter;
>  	int err = 0;
> +	u32 horizon_group;
reverse Christmas tree!
>  
>  	kfree(br2dev_event_work);
>  	QETH_CARD_TEXT_(card, 4, "b2dw%04lx", event);
> @@ -770,12 +771,13 @@ static void qeth_l2_br2dev_worker(struct work_struct *work)
>  	if (!qeth_l2_must_learn(lsyncdev, dstdev))
>  		goto unlock;
>  
> -	if (br_port_flag_is_set(lsyncdev, BR_ISOLATED)) {
> +	horizon_group = br_port_horizon_group(lsyncdev);
> +	if (horizon_group) {
>  		/* Update lsyncdev and its isolated sibling(s): */
>  		iter = &brdev->adj_list.lower;
>  		lowerdev = netdev_next_lower_dev_rcu(brdev, &iter);
>  		while (lowerdev) {
> -			if (br_port_flag_is_set(lowerdev, BR_ISOLATED)) {
> +			if (br_port_horizon_group(lowerdev) == horizon_group) {
>  				switch (event) {
>  				case SWITCHDEV_FDB_ADD_TO_DEVICE:
>  					err = dev_uc_add(lowerdev, addr);

---snip---
David Lamparter Dec. 10, 2021, 4:57 p.m. UTC | #8
On Fri, Dec 10, 2021 at 05:53:17PM +0100, Alexandra Winter wrote:
> On 09.12.21 13:14, David Lamparter wrote:
> > Alexandra, could you double check my change to the qeth_l2 driver?  I
> > can't really test it...
>
> Reviewed and tested for s390/qeth. Looks good to me, see 2 comments below.

Awesome, Thanks a lot!

I'll fix your comments together with the other bits Nikolay pointed out
& send a v2 in a few days or so.

Cheers,


-David
diff mbox series

Patch

diff --git a/drivers/s390/net/qeth_l2_main.c b/drivers/s390/net/qeth_l2_main.c
index 303461d70af3..405d36757c22 100644
--- a/drivers/s390/net/qeth_l2_main.c
+++ b/drivers/s390/net/qeth_l2_main.c
@@ -729,8 +729,8 @@  static bool qeth_l2_must_learn(struct net_device *netdev,
 	priv = netdev_priv(netdev);
 	return (netdev != dstdev &&
 		(priv->brport_features & BR_LEARNING_SYNC) &&
-		!(br_port_flag_is_set(netdev, BR_ISOLATED) &&
-		  br_port_flag_is_set(dstdev, BR_ISOLATED)) &&
+		!(br_port_horizon_group(netdev) != 0 &&
+		  br_port_horizon_group(netdev) == br_port_horizon_group(dstdev)) &&
 		(netdev->netdev_ops == &qeth_l2_iqd_netdev_ops ||
 		 netdev->netdev_ops == &qeth_l2_osa_netdev_ops));
 }
@@ -757,6 +757,7 @@  static void qeth_l2_br2dev_worker(struct work_struct *work)
 	struct net_device *lowerdev;
 	struct list_head *iter;
 	int err = 0;
+	u32 horizon_group;
 
 	kfree(br2dev_event_work);
 	QETH_CARD_TEXT_(card, 4, "b2dw%04lx", event);
@@ -770,12 +771,13 @@  static void qeth_l2_br2dev_worker(struct work_struct *work)
 	if (!qeth_l2_must_learn(lsyncdev, dstdev))
 		goto unlock;
 
-	if (br_port_flag_is_set(lsyncdev, BR_ISOLATED)) {
+	horizon_group = br_port_horizon_group(lsyncdev);
+	if (horizon_group) {
 		/* Update lsyncdev and its isolated sibling(s): */
 		iter = &brdev->adj_list.lower;
 		lowerdev = netdev_next_lower_dev_rcu(brdev, &iter);
 		while (lowerdev) {
-			if (br_port_flag_is_set(lowerdev, BR_ISOLATED)) {
+			if (br_port_horizon_group(lowerdev) == horizon_group) {
 				switch (event) {
 				case SWITCHDEV_FDB_ADD_TO_DEVICE:
 					err = dev_uc_add(lowerdev, addr);
diff --git a/include/linux/if_bridge.h b/include/linux/if_bridge.h
index 509e18c7e740..a9738cb40066 100644
--- a/include/linux/if_bridge.h
+++ b/include/linux/if_bridge.h
@@ -53,7 +53,7 @@  struct br_ip_list {
 #define BR_VLAN_TUNNEL		BIT(13)
 #define BR_BCAST_FLOOD		BIT(14)
 #define BR_NEIGH_SUPPRESS	BIT(15)
-#define BR_ISOLATED		BIT(16)
+/* BR_ISOLATED - previously BIT(16) */
 #define BR_MRP_AWARE		BIT(17)
 #define BR_MRP_LOST_CONT	BIT(18)
 #define BR_MRP_LOST_IN_CONT	BIT(19)
@@ -158,6 +158,7 @@  struct net_device *br_fdb_find_port(const struct net_device *br_dev,
 				    __u16 vid);
 void br_fdb_clear_offload(const struct net_device *dev, u16 vid);
 bool br_port_flag_is_set(const struct net_device *dev, unsigned long flag);
+u32 br_port_horizon_group(const struct net_device *dev);
 u8 br_port_get_stp_state(const struct net_device *dev);
 clock_t br_get_ageing_time(const struct net_device *br_dev);
 #else
@@ -179,6 +180,12 @@  br_port_flag_is_set(const struct net_device *dev, unsigned long flag)
 	return false;
 }
 
+static inline u32
+br_port_horizon_group(const struct net_device *dev)
+{
+	return 0;
+}
+
 static inline u8 br_port_get_stp_state(const struct net_device *dev)
 {
 	return BR_STATE_DISABLED;
diff --git a/include/uapi/linux/if_link.h b/include/uapi/linux/if_link.h
index 4ac53b30b6dc..d3a65ae33a62 100644
--- a/include/uapi/linux/if_link.h
+++ b/include/uapi/linux/if_link.h
@@ -536,6 +536,7 @@  enum {
 	IFLA_BRPORT_MRP_IN_OPEN,
 	IFLA_BRPORT_MCAST_EHT_HOSTS_LIMIT,
 	IFLA_BRPORT_MCAST_EHT_HOSTS_CNT,
+	IFLA_BRPORT_HORIZON_GROUP,	/* split-horizon (isolation) index */
 	__IFLA_BRPORT_MAX
 };
 #define IFLA_BRPORT_MAX (__IFLA_BRPORT_MAX - 1)
diff --git a/net/bridge/br_if.c b/net/bridge/br_if.c
index a52ad81596b7..837d8e2fd191 100644
--- a/net/bridge/br_if.c
+++ b/net/bridge/br_if.c
@@ -784,3 +784,15 @@  bool br_port_flag_is_set(const struct net_device *dev, unsigned long flag)
 	return p->flags & flag;
 }
 EXPORT_SYMBOL_GPL(br_port_flag_is_set);
+
+u32 br_port_horizon_group(const struct net_device *dev)
+{
+	struct net_bridge_port *p;
+
+	p = br_port_get_rtnl_rcu(dev);
+	if (!p)
+		return 0;
+
+	return p->horizon_group;
+}
+EXPORT_SYMBOL_GPL(br_port_horizon_group);
diff --git a/net/bridge/br_input.c b/net/bridge/br_input.c
index b50382f957c1..b0d71d01bc02 100644
--- a/net/bridge/br_input.c
+++ b/net/bridge/br_input.c
@@ -112,7 +112,7 @@  int br_handle_frame_finish(struct net *net, struct sock *sk, struct sk_buff *skb
 		goto drop;
 
 	BR_INPUT_SKB_CB(skb)->brdev = br->dev;
-	BR_INPUT_SKB_CB(skb)->src_port_isolated = !!(p->flags & BR_ISOLATED);
+	BR_INPUT_SKB_CB(skb)->src_horizon_group = p->horizon_group;
 
 	if (IS_ENABLED(CONFIG_INET) &&
 	    (skb->protocol == htons(ETH_P_ARP) ||
diff --git a/net/bridge/br_netlink.c b/net/bridge/br_netlink.c
index 0c8b5f1a15bc..eb139a73cbbf 100644
--- a/net/bridge/br_netlink.c
+++ b/net/bridge/br_netlink.c
@@ -203,6 +203,7 @@  static inline size_t br_port_info_size(void)
 		+ nla_total_size(sizeof(u8))	/* IFLA_BRPORT_MRP_IN_OPEN */
 		+ nla_total_size(sizeof(u32))	/* IFLA_BRPORT_MCAST_EHT_HOSTS_LIMIT */
 		+ nla_total_size(sizeof(u32))	/* IFLA_BRPORT_MCAST_EHT_HOSTS_CNT */
+		+ nla_total_size(sizeof(u32))	/* IFLA_BRPORT_HORIZON_GROUP */
 		+ 0;
 }
 
@@ -269,7 +270,8 @@  static int br_port_fill_attrs(struct sk_buff *skb,
 							  BR_MRP_LOST_CONT)) ||
 	    nla_put_u8(skb, IFLA_BRPORT_MRP_IN_OPEN,
 		       !!(p->flags & BR_MRP_LOST_IN_CONT)) ||
-	    nla_put_u8(skb, IFLA_BRPORT_ISOLATED, !!(p->flags & BR_ISOLATED)))
+	    nla_put_u8(skb, IFLA_BRPORT_ISOLATED, !!p->horizon_group) ||
+	    nla_put_u32(skb, IFLA_BRPORT_HORIZON_GROUP, p->horizon_group))
 		return -EMSGSIZE;
 
 	timerval = br_timer_value(&p->message_age_timer);
@@ -829,6 +831,7 @@  static const struct nla_policy br_port_policy[IFLA_BRPORT_MAX + 1] = {
 	[IFLA_BRPORT_ISOLATED]	= { .type = NLA_U8 },
 	[IFLA_BRPORT_BACKUP_PORT] = { .type = NLA_U32 },
 	[IFLA_BRPORT_MCAST_EHT_HOSTS_LIMIT] = { .type = NLA_U32 },
+	[IFLA_BRPORT_HORIZON_GROUP] = NLA_POLICY_MIN(NLA_S32, 0),
 };
 
 /* Change the state of the port and notify spanning tree */
@@ -874,6 +877,21 @@  static int br_setport(struct net_bridge_port *p, struct nlattr *tb[],
 	bool br_vlan_tunnel_old;
 	int err;
 
+	if (tb[IFLA_BRPORT_ISOLATED] && tb[IFLA_BRPORT_HORIZON_GROUP]) {
+		if (nla_get_u8(tb[IFLA_BRPORT_ISOLATED]) &&
+		    nla_get_u32(tb[IFLA_BRPORT_HORIZON_GROUP]) == 0) {
+			NL_SET_ERR_MSG(extack,
+				       "HORIZON_GROUP must be non-zero for ISOLATED flag");
+			return -EINVAL;
+		}
+		if (!nla_get_u8(tb[IFLA_BRPORT_ISOLATED]) &&
+		    nla_get_u32(tb[IFLA_BRPORT_HORIZON_GROUP]) != 0) {
+			NL_SET_ERR_MSG(extack,
+				       "ISOLATED cannot be unset with nonzero HORIZON_GROUP");
+			return -EINVAL;
+		}
+	}
+
 	old_flags = p->flags;
 	br_vlan_tunnel_old = (old_flags & BR_VLAN_TUNNEL) ? true : false;
 
@@ -892,7 +910,6 @@  static int br_setport(struct net_bridge_port *p, struct nlattr *tb[],
 	br_set_port_flag(p, tb, IFLA_BRPORT_PROXYARP_WIFI, BR_PROXYARP_WIFI);
 	br_set_port_flag(p, tb, IFLA_BRPORT_VLAN_TUNNEL, BR_VLAN_TUNNEL);
 	br_set_port_flag(p, tb, IFLA_BRPORT_NEIGH_SUPPRESS, BR_NEIGH_SUPPRESS);
-	br_set_port_flag(p, tb, IFLA_BRPORT_ISOLATED, BR_ISOLATED);
 
 	changed_mask = old_flags ^ p->flags;
 
@@ -973,6 +990,18 @@  static int br_setport(struct net_bridge_port *p, struct nlattr *tb[],
 			return err;
 	}
 
+	if (tb[IFLA_BRPORT_HORIZON_GROUP]) {
+		p->horizon_group = nla_get_u32(tb[IFLA_BRPORT_HORIZON_GROUP]);
+	} else if (tb[IFLA_BRPORT_ISOLATED]) {
+		u8 isolated = nla_get_u8(tb[IFLA_BRPORT_ISOLATED]);
+
+		/* don't trample other values set by HORIZON_GROUP */
+		if (isolated && p->horizon_group == 0)
+			p->horizon_group = 1;
+		else if (!isolated)
+			p->horizon_group = 0;
+	}
+
 	return 0;
 }
 
diff --git a/net/bridge/br_private.h b/net/bridge/br_private.h
index af2b3512d86c..7916f0aa39de 100644
--- a/net/bridge/br_private.h
+++ b/net/bridge/br_private.h
@@ -353,6 +353,12 @@  struct net_bridge_port {
 #endif
 	struct net_bridge_port		__rcu *backup_port;
 
+	/* Ports with the *same* non-zero horizon_group are isolated from
+	 * each other.  Zero or *differing* horizon_group forwards normally.
+	 * UAPI limited to positive signed int. (recommended ifindex namespace)
+	 */
+	u32				horizon_group;
+
 	/* STP */
 	u8				priority;
 	u8				state;
@@ -539,13 +545,14 @@  struct net_bridge {
 struct br_input_skb_cb {
 	struct net_device *brdev;
 
+	u32 src_horizon_group;
+
 	u16 frag_max_size;
 #ifdef CONFIG_BRIDGE_IGMP_SNOOPING
 	u8 igmp;
 	u8 mrouters_only:1;
 #endif
 	u8 proxyarp_replied:1;
-	u8 src_port_isolated:1;
 #ifdef CONFIG_BRIDGE_VLAN_FILTERING
 	u8 vlan_filtered:1;
 #endif
@@ -811,8 +818,8 @@  void br_flood(struct net_bridge *br, struct sk_buff *skb,
 static inline bool br_skb_isolated(const struct net_bridge_port *to,
 				   const struct sk_buff *skb)
 {
-	return BR_INPUT_SKB_CB(skb)->src_port_isolated &&
-	       (to->flags & BR_ISOLATED);
+	return BR_INPUT_SKB_CB(skb)->src_horizon_group &&
+		BR_INPUT_SKB_CB(skb)->src_horizon_group == to->horizon_group;
 }
 
 /* br_if.c */
diff --git a/net/bridge/br_sysfs_if.c b/net/bridge/br_sysfs_if.c
index 07fa76080512..0344315b6f37 100644
--- a/net/bridge/br_sysfs_if.c
+++ b/net/bridge/br_sysfs_if.c
@@ -239,7 +239,37 @@  BRPORT_ATTR_FLAG(proxyarp_wifi, BR_PROXYARP_WIFI);
 BRPORT_ATTR_FLAG(multicast_flood, BR_MCAST_FLOOD);
 BRPORT_ATTR_FLAG(broadcast_flood, BR_BCAST_FLOOD);
 BRPORT_ATTR_FLAG(neigh_suppress, BR_NEIGH_SUPPRESS);
-BRPORT_ATTR_FLAG(isolated, BR_ISOLATED);
+
+static ssize_t show_isolated(struct net_bridge_port *p, char *buf)
+{
+	return sprintf(buf, "%u\n", !!p->horizon_group);
+}
+
+static int br_set_isolated(struct net_bridge_port *p, unsigned long val)
+{
+	if (val == 0)
+		p->horizon_group = 0;
+	else if (p->horizon_group == 0)
+		p->horizon_group = 1;
+	return 0;
+}
+static BRPORT_ATTR(isolated, 0644, show_isolated, br_set_isolated);
+
+static ssize_t show_horizon_group(struct net_bridge_port *p, char *buf)
+{
+	return sprintf(buf, "%u\n", p->horizon_group);
+}
+
+static int br_set_horizon_group(struct net_bridge_port *p, unsigned long val)
+{
+	if (val > INT_MAX)
+		return -EINVAL;
+
+	p->horizon_group = val;
+	return 0;
+}
+static BRPORT_ATTR(horizon_group, 0644, show_horizon_group,
+		   br_set_horizon_group);
 
 #ifdef CONFIG_BRIDGE_IGMP_SNOOPING
 static ssize_t show_multicast_router(struct net_bridge_port *p, char *buf)
@@ -292,6 +322,7 @@  static const struct brport_attribute *brport_attrs[] = {
 	&brport_attr_group_fwd_mask,
 	&brport_attr_neigh_suppress,
 	&brport_attr_isolated,
+	&brport_attr_horizon_group,
 	&brport_attr_backup_port,
 	NULL
 };