Commit 6ce71687 authored by David S. Miller's avatar David S. Miller

Merge branch 'locked-bridge-ports'

Hans Schultz says:

====================
Add support for locked bridge ports (for 802.1X)

This series starts by adding support for SA filtering to the bridge,
which is then allowed to be offloaded to switchdev devices. Furthermore
an offloading implementation is supplied for the mv88e6xxx driver.

Public Local Area Networks are often deployed such that there is a
risk of unauthorized or unattended clients getting access to the LAN.
To prevent such access we introduce SA filtering, such that ports
designated as secure ports are set in locked mode, so that only
authorized source MAC addresses are given access by adding them to
the bridges forwarding database. Incoming packets with source MAC
addresses that are not in the forwarding database of the bridge are
discarded. It is then the task of user space daemons to populate the
bridge's forwarding database with static entries of authorized entities.

The most common approach is to use the IEEE 802.1X protocol to take
care of the authorization of allowed users to gain access by opening
for the source address of the authorized host.

With the current use of the bridge parameter in hostapd, there is
a limitation in using this for IEEE 802.1X port authentication. It
depends on hostapd attaching the port on which it has a successful
authentication to the bridge, but that only allows for a single
authentication per port. This patch set allows for the use of
IEEE 802.1X port authentication in a more general network context with
multiple 802.1X aware hosts behind a single port as depicted, which is
a commonly used commercial use-case, as it is only the number of
available entries in the forwarding database that limits the number of
authenticated clients.

      +--------------------------------+
      |                                |
      |      Bridge/Authenticator      |
      |                                |
      +-------------+------------------+
       802.1X port  |
                    |
                    |
             +------+-------+
             |              |
             |  Hub/Switch  |
             |              |
             +-+----------+-+
               |          |
            +--+--+    +--+--+
            |     |    |     |
    Hosts   |  a  |    |  b  |   . . .
            |     |    |     |
            +-----+    +-----+

The 802.1X standard involves three different components, a Supplicant
(Host), an Authenticator (Network Access Point) and an Authentication
Server which is typically a Radius server. This patch set thus enables
the bridge module together with an authenticator application to serve
as an Authenticator on designated ports.

For the bridge to become an IEEE 802.1X Authenticator, a solution using
hostapd with the bridge driver can be found at
https://github.com/westermo/hostapd/tree/bridge_driver .

The relevant components work transparently in relation to if it is the
bridge module or the offloaded switchcore case that is in use.
====================
Signed-off-by: default avatarDavid S. Miller <davem@davemloft.net>
parents b26ef81c b2b681a4
...@@ -6103,7 +6103,7 @@ static int mv88e6xxx_port_pre_bridge_flags(struct dsa_switch *ds, int port, ...@@ -6103,7 +6103,7 @@ static int mv88e6xxx_port_pre_bridge_flags(struct dsa_switch *ds, int port,
const struct mv88e6xxx_ops *ops; const struct mv88e6xxx_ops *ops;
if (flags.mask & ~(BR_LEARNING | BR_FLOOD | BR_MCAST_FLOOD | if (flags.mask & ~(BR_LEARNING | BR_FLOOD | BR_MCAST_FLOOD |
BR_BCAST_FLOOD)) BR_BCAST_FLOOD | BR_PORT_LOCKED))
return -EINVAL; return -EINVAL;
ops = chip->info->ops; ops = chip->info->ops;
...@@ -6161,6 +6161,13 @@ static int mv88e6xxx_port_bridge_flags(struct dsa_switch *ds, int port, ...@@ -6161,6 +6161,13 @@ static int mv88e6xxx_port_bridge_flags(struct dsa_switch *ds, int port,
goto out; goto out;
} }
if (flags.mask & BR_PORT_LOCKED) {
bool locked = !!(flags.val & BR_PORT_LOCKED);
err = mv88e6xxx_port_set_lock(chip, port, locked);
if (err)
goto out;
}
out: out:
mv88e6xxx_reg_unlock(chip); mv88e6xxx_reg_unlock(chip);
......
...@@ -1234,6 +1234,35 @@ int mv88e6xxx_port_set_mirror(struct mv88e6xxx_chip *chip, int port, ...@@ -1234,6 +1234,35 @@ int mv88e6xxx_port_set_mirror(struct mv88e6xxx_chip *chip, int port,
return err; return err;
} }
int mv88e6xxx_port_set_lock(struct mv88e6xxx_chip *chip, int port,
bool locked)
{
u16 reg;
int err;
err = mv88e6xxx_port_read(chip, port, MV88E6XXX_PORT_CTL0, &reg);
if (err)
return err;
reg &= ~MV88E6XXX_PORT_CTL0_SA_FILT_MASK;
if (locked)
reg |= MV88E6XXX_PORT_CTL0_SA_FILT_DROP_ON_LOCK;
err = mv88e6xxx_port_write(chip, port, MV88E6XXX_PORT_CTL0, reg);
if (err)
return err;
err = mv88e6xxx_port_read(chip, port, MV88E6XXX_PORT_ASSOC_VECTOR, &reg);
if (err)
return err;
reg &= ~MV88E6XXX_PORT_ASSOC_VECTOR_LOCKED_PORT;
if (locked)
reg |= MV88E6XXX_PORT_ASSOC_VECTOR_LOCKED_PORT;
return mv88e6xxx_port_write(chip, port, MV88E6XXX_PORT_ASSOC_VECTOR, reg);
}
int mv88e6xxx_port_set_8021q_mode(struct mv88e6xxx_chip *chip, int port, int mv88e6xxx_port_set_8021q_mode(struct mv88e6xxx_chip *chip, int port,
u16 mode) u16 mode)
{ {
......
...@@ -147,7 +147,11 @@ ...@@ -147,7 +147,11 @@
/* Offset 0x04: Port Control Register */ /* Offset 0x04: Port Control Register */
#define MV88E6XXX_PORT_CTL0 0x04 #define MV88E6XXX_PORT_CTL0 0x04
#define MV88E6XXX_PORT_CTL0_USE_CORE_TAG 0x8000 #define MV88E6XXX_PORT_CTL0_USE_CORE_TAG 0x8000
#define MV88E6XXX_PORT_CTL0_DROP_ON_LOCK 0x4000 #define MV88E6XXX_PORT_CTL0_SA_FILT_MASK 0xc000
#define MV88E6XXX_PORT_CTL0_SA_FILT_DISABLED 0x0000
#define MV88E6XXX_PORT_CTL0_SA_FILT_DROP_ON_LOCK 0x4000
#define MV88E6XXX_PORT_CTL0_SA_FILT_DROP_ON_UNLOCK 0x8000
#define MV88E6XXX_PORT_CTL0_SA_FILT_DROP_ON_CPU 0xc000
#define MV88E6XXX_PORT_CTL0_EGRESS_MODE_MASK 0x3000 #define MV88E6XXX_PORT_CTL0_EGRESS_MODE_MASK 0x3000
#define MV88E6XXX_PORT_CTL0_EGRESS_MODE_UNMODIFIED 0x0000 #define MV88E6XXX_PORT_CTL0_EGRESS_MODE_UNMODIFIED 0x0000
#define MV88E6XXX_PORT_CTL0_EGRESS_MODE_UNTAGGED 0x1000 #define MV88E6XXX_PORT_CTL0_EGRESS_MODE_UNTAGGED 0x1000
...@@ -370,6 +374,9 @@ int mv88e6xxx_port_set_fid(struct mv88e6xxx_chip *chip, int port, u16 fid); ...@@ -370,6 +374,9 @@ int mv88e6xxx_port_set_fid(struct mv88e6xxx_chip *chip, int port, u16 fid);
int mv88e6xxx_port_get_pvid(struct mv88e6xxx_chip *chip, int port, u16 *pvid); int mv88e6xxx_port_get_pvid(struct mv88e6xxx_chip *chip, int port, u16 *pvid);
int mv88e6xxx_port_set_pvid(struct mv88e6xxx_chip *chip, int port, u16 pvid); int mv88e6xxx_port_set_pvid(struct mv88e6xxx_chip *chip, int port, u16 pvid);
int mv88e6xxx_port_set_lock(struct mv88e6xxx_chip *chip, int port,
bool locked);
int mv88e6xxx_port_set_8021q_mode(struct mv88e6xxx_chip *chip, int port, int mv88e6xxx_port_set_8021q_mode(struct mv88e6xxx_chip *chip, int port,
u16 mode); u16 mode);
int mv88e6095_port_tag_remap(struct mv88e6xxx_chip *chip, int port); int mv88e6095_port_tag_remap(struct mv88e6xxx_chip *chip, int port);
......
...@@ -58,6 +58,7 @@ struct br_ip_list { ...@@ -58,6 +58,7 @@ struct br_ip_list {
#define BR_MRP_LOST_CONT BIT(18) #define BR_MRP_LOST_CONT BIT(18)
#define BR_MRP_LOST_IN_CONT BIT(19) #define BR_MRP_LOST_IN_CONT BIT(19)
#define BR_TX_FWD_OFFLOAD BIT(20) #define BR_TX_FWD_OFFLOAD BIT(20)
#define BR_PORT_LOCKED BIT(21)
#define BR_DEFAULT_AGEING_TIME (300 * HZ) #define BR_DEFAULT_AGEING_TIME (300 * HZ)
......
...@@ -537,6 +537,7 @@ enum { ...@@ -537,6 +537,7 @@ enum {
IFLA_BRPORT_MRP_IN_OPEN, IFLA_BRPORT_MRP_IN_OPEN,
IFLA_BRPORT_MCAST_EHT_HOSTS_LIMIT, IFLA_BRPORT_MCAST_EHT_HOSTS_LIMIT,
IFLA_BRPORT_MCAST_EHT_HOSTS_CNT, IFLA_BRPORT_MCAST_EHT_HOSTS_CNT,
IFLA_BRPORT_LOCKED,
__IFLA_BRPORT_MAX __IFLA_BRPORT_MAX
}; };
#define IFLA_BRPORT_MAX (__IFLA_BRPORT_MAX - 1) #define IFLA_BRPORT_MAX (__IFLA_BRPORT_MAX - 1)
......
...@@ -81,6 +81,7 @@ int br_handle_frame_finish(struct net *net, struct sock *sk, struct sk_buff *skb ...@@ -81,6 +81,7 @@ int br_handle_frame_finish(struct net *net, struct sock *sk, struct sk_buff *skb
if (!p || p->state == BR_STATE_DISABLED) if (!p || p->state == BR_STATE_DISABLED)
goto drop; goto drop;
br = p->br;
brmctx = &p->br->multicast_ctx; brmctx = &p->br->multicast_ctx;
pmctx = &p->multicast_ctx; pmctx = &p->multicast_ctx;
state = p->state; state = p->state;
...@@ -88,10 +89,18 @@ int br_handle_frame_finish(struct net *net, struct sock *sk, struct sk_buff *skb ...@@ -88,10 +89,18 @@ int br_handle_frame_finish(struct net *net, struct sock *sk, struct sk_buff *skb
&state, &vlan)) &state, &vlan))
goto out; goto out;
if (p->flags & BR_PORT_LOCKED) {
struct net_bridge_fdb_entry *fdb_src =
br_fdb_find_rcu(br, eth_hdr(skb)->h_source, vid);
if (!fdb_src || READ_ONCE(fdb_src->dst) != p ||
test_bit(BR_FDB_LOCAL, &fdb_src->flags))
goto drop;
}
nbp_switchdev_frame_mark(p, skb); nbp_switchdev_frame_mark(p, skb);
/* insert into forwarding database after filtering to avoid spoofing */ /* insert into forwarding database after filtering to avoid spoofing */
br = p->br;
if (p->flags & BR_LEARNING) if (p->flags & BR_LEARNING)
br_fdb_update(br, p, eth_hdr(skb)->h_source, vid, 0); br_fdb_update(br, p, eth_hdr(skb)->h_source, vid, 0);
......
...@@ -184,6 +184,7 @@ static inline size_t br_port_info_size(void) ...@@ -184,6 +184,7 @@ static inline size_t br_port_info_size(void)
+ nla_total_size(1) /* IFLA_BRPORT_VLAN_TUNNEL */ + nla_total_size(1) /* IFLA_BRPORT_VLAN_TUNNEL */
+ nla_total_size(1) /* IFLA_BRPORT_NEIGH_SUPPRESS */ + nla_total_size(1) /* IFLA_BRPORT_NEIGH_SUPPRESS */
+ nla_total_size(1) /* IFLA_BRPORT_ISOLATED */ + nla_total_size(1) /* IFLA_BRPORT_ISOLATED */
+ nla_total_size(1) /* IFLA_BRPORT_LOCKED */
+ nla_total_size(sizeof(struct ifla_bridge_id)) /* IFLA_BRPORT_ROOT_ID */ + nla_total_size(sizeof(struct ifla_bridge_id)) /* IFLA_BRPORT_ROOT_ID */
+ nla_total_size(sizeof(struct ifla_bridge_id)) /* IFLA_BRPORT_BRIDGE_ID */ + nla_total_size(sizeof(struct ifla_bridge_id)) /* IFLA_BRPORT_BRIDGE_ID */
+ nla_total_size(sizeof(u16)) /* IFLA_BRPORT_DESIGNATED_PORT */ + nla_total_size(sizeof(u16)) /* IFLA_BRPORT_DESIGNATED_PORT */
...@@ -269,7 +270,8 @@ static int br_port_fill_attrs(struct sk_buff *skb, ...@@ -269,7 +270,8 @@ static int br_port_fill_attrs(struct sk_buff *skb,
BR_MRP_LOST_CONT)) || BR_MRP_LOST_CONT)) ||
nla_put_u8(skb, IFLA_BRPORT_MRP_IN_OPEN, nla_put_u8(skb, IFLA_BRPORT_MRP_IN_OPEN,
!!(p->flags & BR_MRP_LOST_IN_CONT)) || !!(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->flags & BR_ISOLATED)) ||
nla_put_u8(skb, IFLA_BRPORT_LOCKED, !!(p->flags & BR_PORT_LOCKED)))
return -EMSGSIZE; return -EMSGSIZE;
timerval = br_timer_value(&p->message_age_timer); timerval = br_timer_value(&p->message_age_timer);
...@@ -827,6 +829,7 @@ static const struct nla_policy br_port_policy[IFLA_BRPORT_MAX + 1] = { ...@@ -827,6 +829,7 @@ static const struct nla_policy br_port_policy[IFLA_BRPORT_MAX + 1] = {
[IFLA_BRPORT_GROUP_FWD_MASK] = { .type = NLA_U16 }, [IFLA_BRPORT_GROUP_FWD_MASK] = { .type = NLA_U16 },
[IFLA_BRPORT_NEIGH_SUPPRESS] = { .type = NLA_U8 }, [IFLA_BRPORT_NEIGH_SUPPRESS] = { .type = NLA_U8 },
[IFLA_BRPORT_ISOLATED] = { .type = NLA_U8 }, [IFLA_BRPORT_ISOLATED] = { .type = NLA_U8 },
[IFLA_BRPORT_LOCKED] = { .type = NLA_U8 },
[IFLA_BRPORT_BACKUP_PORT] = { .type = NLA_U32 }, [IFLA_BRPORT_BACKUP_PORT] = { .type = NLA_U32 },
[IFLA_BRPORT_MCAST_EHT_HOSTS_LIMIT] = { .type = NLA_U32 }, [IFLA_BRPORT_MCAST_EHT_HOSTS_LIMIT] = { .type = NLA_U32 },
}; };
...@@ -893,6 +896,7 @@ static int br_setport(struct net_bridge_port *p, struct nlattr *tb[], ...@@ -893,6 +896,7 @@ static int br_setport(struct net_bridge_port *p, struct nlattr *tb[],
br_set_port_flag(p, tb, IFLA_BRPORT_VLAN_TUNNEL, BR_VLAN_TUNNEL); 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_NEIGH_SUPPRESS, BR_NEIGH_SUPPRESS);
br_set_port_flag(p, tb, IFLA_BRPORT_ISOLATED, BR_ISOLATED); br_set_port_flag(p, tb, IFLA_BRPORT_ISOLATED, BR_ISOLATED);
br_set_port_flag(p, tb, IFLA_BRPORT_LOCKED, BR_PORT_LOCKED);
changed_mask = old_flags ^ p->flags; changed_mask = old_flags ^ p->flags;
......
...@@ -72,7 +72,7 @@ bool nbp_switchdev_allowed_egress(const struct net_bridge_port *p, ...@@ -72,7 +72,7 @@ bool nbp_switchdev_allowed_egress(const struct net_bridge_port *p,
/* Flags that can be offloaded to hardware */ /* Flags that can be offloaded to hardware */
#define BR_PORT_FLAGS_HW_OFFLOAD (BR_LEARNING | BR_FLOOD | \ #define BR_PORT_FLAGS_HW_OFFLOAD (BR_LEARNING | BR_FLOOD | \
BR_MCAST_FLOOD | BR_BCAST_FLOOD) BR_MCAST_FLOOD | BR_BCAST_FLOOD | BR_PORT_LOCKED)
int br_switchdev_set_port_flag(struct net_bridge_port *p, int br_switchdev_set_port_flag(struct net_bridge_port *p,
unsigned long flags, unsigned long flags,
......
...@@ -176,7 +176,7 @@ static int dsa_port_inherit_brport_flags(struct dsa_port *dp, ...@@ -176,7 +176,7 @@ static int dsa_port_inherit_brport_flags(struct dsa_port *dp,
struct netlink_ext_ack *extack) struct netlink_ext_ack *extack)
{ {
const unsigned long mask = BR_LEARNING | BR_FLOOD | BR_MCAST_FLOOD | const unsigned long mask = BR_LEARNING | BR_FLOOD | BR_MCAST_FLOOD |
BR_BCAST_FLOOD; BR_BCAST_FLOOD | BR_PORT_LOCKED;
struct net_device *brport_dev = dsa_port_to_bridge_port(dp); struct net_device *brport_dev = dsa_port_to_bridge_port(dp);
int flag, err; int flag, err;
...@@ -200,7 +200,7 @@ static void dsa_port_clear_brport_flags(struct dsa_port *dp) ...@@ -200,7 +200,7 @@ static void dsa_port_clear_brport_flags(struct dsa_port *dp)
{ {
const unsigned long val = BR_FLOOD | BR_MCAST_FLOOD | BR_BCAST_FLOOD; const unsigned long val = BR_FLOOD | BR_MCAST_FLOOD | BR_BCAST_FLOOD;
const unsigned long mask = BR_LEARNING | BR_FLOOD | BR_MCAST_FLOOD | const unsigned long mask = BR_LEARNING | BR_FLOOD | BR_MCAST_FLOOD |
BR_BCAST_FLOOD; BR_BCAST_FLOOD | BR_PORT_LOCKED;
int flag, err; int flag, err;
for_each_set_bit(flag, &mask, 32) { for_each_set_bit(flag, &mask, 32) {
......
# SPDX-License-Identifier: GPL-2.0+ OR MIT # SPDX-License-Identifier: GPL-2.0+ OR MIT
TEST_PROGS = bridge_igmp.sh \ TEST_PROGS = bridge_igmp.sh \
bridge_locked_port.sh \
bridge_port_isolation.sh \ bridge_port_isolation.sh \
bridge_sticky_fdb.sh \ bridge_sticky_fdb.sh \
bridge_vlan_aware.sh \ bridge_vlan_aware.sh \
......
#!/bin/bash
# SPDX-License-Identifier: GPL-2.0
ALL_TESTS="locked_port_ipv4 locked_port_ipv6 locked_port_vlan"
NUM_NETIFS=4
CHECK_TC="no"
source lib.sh
h1_create()
{
simple_if_init $h1 192.0.2.1/24 2001:db8:1::1/64
vrf_create "vrf-vlan-h1"
ip link set dev vrf-vlan-h1 up
vlan_create $h1 100 vrf-vlan-h1 198.51.100.1/24
}
h1_destroy()
{
vlan_destroy $h1 100
simple_if_fini $h1 192.0.2.1/24 2001:db8:1::1/64
}
h2_create()
{
simple_if_init $h2 192.0.2.2/24 2001:db8:1::2/64
vrf_create "vrf-vlan-h2"
ip link set dev vrf-vlan-h2 up
vlan_create $h2 100 vrf-vlan-h2 198.51.100.2/24
}
h2_destroy()
{
vlan_destroy $h2 100
simple_if_fini $h2 192.0.2.2/24 2001:db8:1::2/64
}
switch_create()
{
ip link add dev br0 type bridge vlan_filtering 1
ip link set dev $swp1 master br0
ip link set dev $swp2 master br0
ip link set dev br0 up
ip link set dev $swp1 up
ip link set dev $swp2 up
bridge link set dev $swp1 learning off
}
switch_destroy()
{
ip link set dev $swp2 down
ip link set dev $swp1 down
ip link del dev br0
}
setup_prepare()
{
h1=${NETIFS[p1]}
swp1=${NETIFS[p2]}
swp2=${NETIFS[p3]}
h2=${NETIFS[p4]}
vrf_prepare
h1_create
h2_create
switch_create
}
cleanup()
{
pre_cleanup
switch_destroy
h2_destroy
h1_destroy
vrf_cleanup
}
locked_port_ipv4()
{
RET=0
check_locked_port_support || return 0
ping_do $h1 192.0.2.2
check_err $? "Ping did not work before locking port"
bridge link set dev $swp1 locked on
ping_do $h1 192.0.2.2
check_fail $? "Ping worked after locking port, but before adding FDB entry"
bridge fdb add `mac_get $h1` dev $swp1 master static
ping_do $h1 192.0.2.2
check_err $? "Ping did not work after locking port and adding FDB entry"
bridge link set dev $swp1 locked off
bridge fdb del `mac_get $h1` dev $swp1 master static
ping_do $h1 192.0.2.2
check_err $? "Ping did not work after unlocking port and removing FDB entry."
log_test "Locked port ipv4"
}
locked_port_vlan()
{
RET=0
check_locked_port_support || return 0
bridge vlan add vid 100 dev $swp1
bridge vlan add vid 100 dev $swp2
ping_do $h1.100 198.51.100.2
check_err $? "Ping through vlan did not work before locking port"
bridge link set dev $swp1 locked on
ping_do $h1.100 198.51.100.2
check_fail $? "Ping through vlan worked after locking port, but before adding FDB entry"
bridge fdb add `mac_get $h1` dev $swp1 vlan 100 master static
ping_do $h1.100 198.51.100.2
check_err $? "Ping through vlan did not work after locking port and adding FDB entry"
bridge link set dev $swp1 locked off
bridge fdb del `mac_get $h1` dev $swp1 vlan 100 master static
ping_do $h1.100 198.51.100.2
check_err $? "Ping through vlan did not work after unlocking port and removing FDB entry"
bridge vlan del vid 100 dev $swp1
bridge vlan del vid 100 dev $swp2
log_test "Locked port vlan"
}
locked_port_ipv6()
{
RET=0
check_locked_port_support || return 0
ping6_do $h1 2001:db8:1::2
check_err $? "Ping6 did not work before locking port"
bridge link set dev $swp1 locked on
ping6_do $h1 2001:db8:1::2
check_fail $? "Ping6 worked after locking port, but before adding FDB entry"
bridge fdb add `mac_get $h1` dev $swp1 master static
ping6_do $h1 2001:db8:1::2
check_err $? "Ping6 did not work after locking port and adding FDB entry"
bridge link set dev $swp1 locked off
bridge fdb del `mac_get $h1` dev $swp1 master static
ping6_do $h1 2001:db8:1::2
check_err $? "Ping6 did not work after unlocking port and removing FDB entry"
log_test "Locked port ipv6"
}
trap cleanup EXIT
setup_prepare
setup_wait
tests_run
exit $EXIT_STATUS
...@@ -126,6 +126,14 @@ check_ethtool_lanes_support() ...@@ -126,6 +126,14 @@ check_ethtool_lanes_support()
fi fi
} }
check_locked_port_support()
{
if ! bridge -d link show | grep -q " locked"; then
echo "SKIP: iproute2 too old; Locked port feature not supported."
return $ksft_skip
fi
}
if [[ "$(id -u)" -ne 0 ]]; then if [[ "$(id -u)" -ne 0 ]]; then
echo "SKIP: need root privileges" echo "SKIP: need root privileges"
exit $ksft_skip exit $ksft_skip
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment