On 2/6/26 10:54 AM, Ales Musil via dev wrote:
> The RFC defines a Virtual Router Redundancy Protocol [0], in order
> for that protocol to work the workload might "spoof" MAC address
> within ARP or ND request/response. This wasn't allowed as the port
> security is specifically designed against spoofing and checks if
> the port security MAC address is the same for source of ARP/ND
> and the inner source/target address. To make the port security
> compliant add a special literal which when specified will allow
> user to add any/all MAC addresses defined by VRRPv3. The traffic
> from and to those additional MAC addresses will be allowed as
> well as permutations of ARP/ND inner MACs combined with the
> physical MAC as a source.
> 
> [0] https://datatracker.ietf.org/doc/html/rfc9568
> Reported-at: https://issues.redhat.com/browse/FDP-2979
> Signed-off-by: Ales Musil <[email protected]>
> ---
> v4: Rebase on top of latest main.
>     Update the RFC url.
>     Add Jacob's ack.
> v5: Rebase on top of latest main.
>     Address nits pointed out by Dumitru.
>     Add extra test for invalid VRRPv3 MAC.
>     Update the wording in documentation.
>     Remove acks as the code changed.
>     Do not populate flows for physical MAC when VRRP is specified.

Thanks, Ales!  Functionally this version looks good to me for the
most part, see some small comments below.

> ---
>  NEWS               |   3 +
>  controller/lflow.c | 932 +++++++++++++++++++++++++++++----------------
>  ovn-nb.xml         |  55 +++
>  tests/ovn.at       | 779 +++++++++++++++++++++++++++++++++++++
>  4 files changed, 1446 insertions(+), 323 deletions(-)
> 
> diff --git a/NEWS b/NEWS
> index 2a2b5e12d..7c7458224 100644
> --- a/NEWS
> +++ b/NEWS
> @@ -98,6 +98,9 @@ Post v25.09.0
>       reserving an unused IP from the backend's subnet. This change allows
>       using LRP IPs directly, eliminating the need to reserve additional IPs
>       per backend port.
> +   - Add support for special port_security prefix "VRRPv3". This prefix 
> allows
> +     CMS to specify one physical MAC and multiple VRRPv3 MAC addresses.
> +     The VRRPv3 MAC also accepts masked format.

nit: It feels like this record doesn't contain enough info to start using
the feature, which is fine, but it also doesn't contain enough info to
explain what it is for.

Maybe replace the last two sentences with something like "This prefix
allows CMS to allow all required traffic for a VRRPv3 virtual router
behind LSP.  See <man page reference> for more details." ?

>  
>  OVN v25.09.0 - xxx xx xxxx
>  --------------------------
> diff --git a/controller/lflow.c b/controller/lflow.c
> index 94fd8807c..904de8ff0 100644
> --- a/controller/lflow.c
> +++ b/controller/lflow.c
> @@ -2340,6 +2340,157 @@ add_port_sec_flows(const struct shash *binding_lports,
>      }
>  }
>  
> +struct masked_ip4_addr {
> +    ovs_be32 addr;
> +    ovs_be32 mask;
> +    ovs_be32 bcast;
> +};
> +
> +struct masked_ip6_addr {
> +    struct in6_addr addr;
> +    struct in6_addr mask;
> +};
> +
> +struct masked_eth_addr {
> +    struct eth_addr addr;
> +    struct eth_addr mask;
> +};
> +
> +struct port_security_addresses {
> +    struct eth_addr phys_addr;
> +    /* Vector of 'struct masked_eth_addr'. */
> +    struct vector vrrp4;
> +    /* Vector of 'struct masked_eth_addr'. */
> +    struct vector vrrp6;
> +    /* Vector of 'struct masked_ip4_addr' .*/
> +    struct vector ip4;
> +    /* Vector of 'struct masked_ip6_addr' .*/
> +    struct vector ip6;
> +};
> +
> +static void
> +port_security_addresses_init(struct port_security_addresses *ps_addr)
> +{
> +    *ps_addr = (struct port_security_addresses) {
> +        .phys_addr = eth_addr_zero,
> +        .vrrp4 = VECTOR_EMPTY_INITIALIZER(struct masked_eth_addr),
> +        .vrrp6 = VECTOR_EMPTY_INITIALIZER(struct masked_eth_addr),
> +        .ip4 = VECTOR_EMPTY_INITIALIZER(struct masked_ip4_addr),
> +        .ip6 = VECTOR_EMPTY_INITIALIZER(struct masked_ip6_addr),
> +    };
> +}
> +
> +static void
> +port_security_addresses_clear(struct port_security_addresses *ps_addr)
> +{
> +    vector_clear(&ps_addr->vrrp4);
> +    vector_clear(&ps_addr->vrrp6);
> +    vector_clear(&ps_addr->ip4);
> +    vector_clear(&ps_addr->ip6);
> +}
> +
> +static void
> +port_security_addresses_destroy(struct port_security_addresses *ps_addr)
> +{
> +    vector_destroy(&ps_addr->vrrp4);
> +    vector_destroy(&ps_addr->vrrp6);
> +    vector_destroy(&ps_addr->ip4);
> +    vector_destroy(&ps_addr->ip6);
> +}
> +
> +static const struct masked_eth_addr maddr_any_vrrp4 = {
> +    .addr = ETH_ADDR_C(00,00,5e,00,01,00),
> +    .mask = ETH_ADDR_C(ff,ff,ff,ff,ff,00)
> +};
> +static const struct masked_eth_addr maddr_any_vrrp6 = {
> +    .addr = ETH_ADDR_C(00,00,5e,00,02,00),
> +    .mask = ETH_ADDR_C(ff,ff,ff,ff,ff,00)

nit: trailing comma?  Other structure initializers have it and ones for this
structure inside the functions as well.

> +};
> +
> +static bool
> +port_security_addresses_add_vrrp_mac(struct port_security_addresses *ps_addr,
> +                                     struct eth_addr mac, unsigned int plen)
> +{
> +    /* Only the last byte contains ID for VRRPv3. */
> +    if (plen < 40) {
> +        return false;
> +    }
> +
> +    /* If the masked portion is non-zero, the host can only use
> +     * the specified MAC address.  If zero, the host is allowed
> +     * to use any MAC address within the mask.

This looks a little strange to me.  I'd not expect 00:00:5e:00:01:42/40 to
only allow 00:00:5e:00:01:42 address.  We should probbaly just fail here
if the masked part is not zero.

The format where the masked part is not zero makes sense for the IPs in the
regular port security record, because it means IP within the subnet + subnet
prefix length, and not the whole subnet.  This kind of configuration is
needed, because we need to create a broadcast flow, and so we have to know
the prefix, even if we want to allow a single IP.

For the VRRP MAC addresses though this doesn't make a lot of sense, we don't
need to create any broadcast flows, we just need to know the subnet.  So,
we either clear the masked bits or fail if they are present.  Failing seems
less confusing from the user's perspective.

WDYT?

Either way this behavior is documented for IPs, but not for MACs.  But it
will be hard to explain in the docs why only the specified IP is used, when
there is no use for the prefix.

> +     */
> +    struct eth_addr mask = eth_addr_create_mask(plen);
> +    struct masked_eth_addr maddr = (struct masked_eth_addr) {

nit: shouldn't need to cast, it can be an initializer.  Compliler probbaly
doesn't care, but semantially it's a copy of an anonymous structure vs
designated initializer.

> +        .addr = mac,
> +        .mask = (eth_addr_to_uint64(mac) & ~eth_addr_to_uint64(mask))
> +              ? eth_addr_exact : mask,

nit: should probably be 2 spaces to the right.

> +    };
> +
> +    /* The exact match on VRRPv3 MAC ending with zero is not allowed, the
> +     * id is starting from 1. */
> +    if (plen == 48) {
> +        if (eth_addr_equals(mac, maddr_any_vrrp4.addr) ||
> +            eth_addr_equals(mac, maddr_any_vrrp6.addr)) {
> +            return false;
> +        }
> +    }
> +
> +    if (eth_addr_equal_except(maddr_any_vrrp4.addr, mac,
> +                              maddr_any_vrrp4.mask)) {
> +        vector_push(&ps_addr->vrrp4, &maddr);
> +        return true;
> +    }
> +
> +    if (eth_addr_equal_except(maddr_any_vrrp6.addr, mac,
> +                              maddr_any_vrrp6.mask)) {
> +        vector_push(&ps_addr->vrrp6, &maddr);
> +        return true;
> +    }
> +
> +    return false;
> +}
> +
> +static bool
> +port_security_addresses_add_ip(struct port_security_addresses *ps_addr,
> +                               struct in6_addr ip, unsigned int plen)
> +{
> +    /* When the netmask is applied, if the host portion is
> +     * non-zero, the host can only use the specified
> +     * address.  If zero, the host is allowed to use any
> +     * address in the subnet. Also add broadcast in the special case
> +     * of matching only the specified address.

nit: Line lengths seem inconsistent.  Also single vs double spaces.

> +     */
> +    if (IN6_IS_ADDR_V4MAPPED(&ip)) {
> +        if (plen > 32) {
> +            return false;
> +        }
> +
> +        ovs_be32 addr = in6_addr_get_mapped_ipv4(&ip);
> +        ovs_be32 mask = be32_prefix_mask(plen);
> +
> +        struct masked_ip4_addr maddr = (struct masked_ip4_addr) {

nit: cast.

> +            .addr = addr,
> +            .mask = (addr & ~mask) ? OVS_BE32_MAX : mask,
> +            .bcast = (addr & ~mask) ? (addr | ~mask) : htonl(0),
> +        };
> +        vector_push(&ps_addr->ip4, &maddr);
> +    } else {
> +        if (plen > 128) {
> +            return false;
> +        }
> +
> +        struct in6_addr mask = ipv6_create_mask(plen);
> +        struct masked_ip6_addr maddr = (struct masked_ip6_addr) {

nit: cast.

> +            .addr = ip,
> +            .mask = !ipv6_addr_is_host_zero(&ip, &mask) ? in6addr_exact : 
> mask,
> +        };
> +        vector_push(&ps_addr->ip6, &maddr);
> +    }
> +
> +    return true;
> +}
> +
>  static void
>  reset_match_for_port_sec_flows(const struct sbrec_port_binding *pb,
>                                 enum mf_field_id reg_id, struct match *match)
> @@ -2446,24 +2597,39 @@ build_in_port_sec_default_flows(const struct 
> sbrec_port_binding *pb,
>                      &pb->header_.uuid);
>  }
>  
> +static void
> +build_out_port_sec_default_flows(const struct sbrec_port_binding *pb,
> +                                struct match *m, struct ofpbuf *ofpacts,
> +                                struct ovn_desired_flow_table *flow_table)
> +{
> +    /* Add the below logical flow equivalent OF rules in 'out_port_sec_nd'
> +     * table.
> +     * priority: 80
> +     * match - "outport == pb->logical_port"
> +     * action - "port_sec_failed = 1;"
> +     * descrption: "Drop all traffic"
> +     */
> +    reset_match_for_port_sec_flows(pb, MFF_LOG_OUTPORT, m);
> +    build_port_sec_deny_action(ofpacts);
> +    ofctrl_add_flow(flow_table, OFTABLE_CHK_OUT_PORT_SEC, 80,
> +                    pb->header_.uuid.parts[0], m, ofpacts,
> +                    &pb->header_.uuid);
> +}
> +
>  static void
>  build_in_port_sec_no_ip_flows(const struct sbrec_port_binding *pb,
> -                              struct lport_addresses *ps_addr,
> +                              struct eth_addr mac, struct eth_addr mask,
>                                struct match *m, struct ofpbuf *ofpacts,
>                                struct ovn_desired_flow_table *flow_table)
>  {
> -    if (ps_addr->n_ipv4_addrs || ps_addr->n_ipv6_addrs) {
> -        return;
> -    }
> -
>      /* Add the below logical flow equivalent OF rules in 'in_port_sec' table.
>       * priority: 90
> -     * match - "inport == pb->logical_port && eth.src == ps_addr.ea"
> +     * match - "inport == pb->logical_port && eth.src == mac/mask"
>       * action - "next;"
>       * description: "Advance the packet for ARP/ND check"
>       */
>      reset_match_for_port_sec_flows(pb, MFF_LOG_INPORT, m);
> -    match_set_dl_src(m, ps_addr->ea);
> +    match_set_dl_src_masked(m, mac, mask);
>      build_port_sec_adv_nd_check(ofpacts);
>      ofctrl_add_flow(flow_table, OFTABLE_CHK_IN_PORT_SEC, 90,
>                      pb->header_.uuid.parts[0], m, ofpacts,
> @@ -2472,43 +2638,26 @@ build_in_port_sec_no_ip_flows(const struct 
> sbrec_port_binding *pb,
>  
>  static void
>  build_in_port_sec_ip4_flows(const struct sbrec_port_binding *pb,
> -                           struct lport_addresses *ps_addr,
> -                           struct match *m, struct ofpbuf *ofpacts,
> -                           struct ovn_desired_flow_table *flow_table)
> +                            struct eth_addr mac, struct eth_addr mask,
> +                            const struct vector *ip4_addrs,
> +                            struct match *m, struct ofpbuf *ofpacts,
> +                            struct ovn_desired_flow_table *flow_table)
>  {
> -    if (!ps_addr->n_ipv4_addrs) {
> -        /* If no IPv4 addresses, then 'pb' is not allowed to send IPv4 
> traffic.
> -         * build_in_port_sec_default_flows() takes care of this scenario. */
> -        return;
> -    }
> -
>      /* Advance all traffic from the port security eth address for ND check. 
> */
>      build_port_sec_allow_action(ofpacts);
> +    reset_match_for_port_sec_flows(pb, MFF_LOG_INPORT, m);
> +    match_set_dl_src_masked(m, mac, mask);
> +    match_set_dl_type(m, htons(ETH_TYPE_IP));
>  
>      /* Add the below logical flow equivalent OF rules in in_port_sec.
>       * priority: 90
> -     * match - "inport == pb->port && eth.src == ps_addr.ea &&
> -     *         ip4.src == {ps_addr.ipv4_addrs}"
> +     * match - "inport == pb->port && eth.src == mac/mask &&
> +     *         ip4.src == {ip4}"
>       * action - "port_sec_failed = 0;"
>       */
> -    for (size_t j = 0; j < ps_addr->n_ipv4_addrs; j++) {
> -        reset_match_for_port_sec_flows(pb, MFF_LOG_INPORT, m);
> -        match_set_dl_src(m, ps_addr->ea);
> -        match_set_dl_type(m, htons(ETH_TYPE_IP));
> -
> -        ovs_be32 mask = ps_addr->ipv4_addrs[j].mask;
> -        /* When the netmask is applied, if the host portion is
> -         * non-zero, the host can only use the specified
> -         * address.  If zero, the host is allowed to use any
> -         * address in the subnet.
> -         */
> -        if (ps_addr->ipv4_addrs[j].plen == 32 ||
> -                ps_addr->ipv4_addrs[j].addr & ~mask) {
> -            match_set_nw_src(m, ps_addr->ipv4_addrs[j].addr);
> -        } else {
> -            match_set_nw_src_masked(m, ps_addr->ipv4_addrs[j].addr, mask);
> -        }
> -
> +    const struct masked_ip4_addr *ip;
> +    VECTOR_FOR_EACH_PTR (ip4_addrs, ip) {
> +        match_set_nw_src_masked(m, ip->addr, ip->mask);
>          ofctrl_add_flow(flow_table, OFTABLE_CHK_IN_PORT_SEC, 90,
>                          pb->header_.uuid.parts[0], m, ofpacts,
>                          &pb->header_.uuid);
> @@ -2516,20 +2665,14 @@ build_in_port_sec_ip4_flows(const struct 
> sbrec_port_binding *pb,
>  
>      /* Add the below logical flow equivalent OF rules in in_port_sec.
>       * priority: 90
> -     * match - "inport == pb->port && eth.src == ps_addr.ea &&
> +     * match - "inport == pb->port && eth.src == mac/mask &&
>       *          ip4.src == 0.0.0.0 && ip4.dst == 255.255.255.255 &&
>       *          udp.src == 67 && udp.dst == 68"
>       * action - "port_sec_failed = 0;"
>       * description: "Allow the DHCP requests."
>       */
> -    reset_match_for_port_sec_flows(pb, MFF_LOG_INPORT, m);
> -    match_set_dl_src(m, ps_addr->ea);
> -    match_set_dl_type(m, htons(ETH_TYPE_IP));
> -
> -    ovs_be32 ip4 = htonl(0);
> -    match_set_nw_src(m, ip4);
> -    ip4 = htonl(0xffffffff);
> -    match_set_nw_dst(m, ip4);
> +    match_set_nw_src(m, htonl(0));
> +    match_set_nw_dst(m, htonl(0xffffffff));
>      match_set_nw_proto(m, IPPROTO_UDP);
>      match_set_tp_src(m, htons(68));
>      match_set_tp_dst(m, htons(67));
> @@ -2542,33 +2685,42 @@ build_in_port_sec_ip4_flows(const struct 
> sbrec_port_binding *pb,
>  /* Adds the OF rules to allow ARP packets in 'in_port_sec_nd' table. */
>  static void
>  build_in_port_sec_arp_flows(const struct sbrec_port_binding *pb,
> -                           struct lport_addresses *ps_addr,
> -                           struct match *m, struct ofpbuf *ofpacts,
> -                           struct ovn_desired_flow_table *flow_table)
> +                            struct eth_addr phys_mac,
> +                            const struct vector *ip4_addrs,
> +                            const struct vector *vrrp4_addrs,
> +                            bool is_vrrp, struct match *m,
> +                            struct ofpbuf *ofpacts,
> +                            struct ovn_desired_flow_table *flow_table)
>  {
> -    if (!ps_addr->n_ipv4_addrs && ps_addr->n_ipv6_addrs) {
> -        /* No ARP is allowed as only IPv6 addresses are configured. */
> -        return;
> -    }
> -
>      build_port_sec_allow_action(ofpacts);
> +    reset_match_for_port_sec_flows(pb, MFF_LOG_INPORT, m);
> +    match_set_dl_src(m, phys_mac);
> +    match_set_dl_type(m, htons(ETH_TYPE_ARP));
>  
> -    if (!ps_addr->n_ipv4_addrs) {
> +    if (vector_is_empty(ip4_addrs)) {
>          /* No IPv4 addresses.
>           * Add the below logical flow equivalent OF rules in 'in_port_sec_nd'
>           * table.
>           * priority: 90
> -         * match - "inport == pb->port && eth.src == ps_addr.ea &&
> -         *          arp && arp.sha == ps_addr.ea"
> +         * match - "inport == pb->port && eth.src == phys_mac &&
> +         *          arp && arp.sha == {phys_mac, vrrp4_addrs}"
>           * action - "port_sec_failed = 0;"
>           */
> -        reset_match_for_port_sec_flows(pb, MFF_LOG_INPORT, m);
> -        match_set_dl_src(m, ps_addr->ea);
> -        match_set_dl_type(m, htons(ETH_TYPE_ARP));
> -        match_set_arp_sha(m, ps_addr->ea);
> -        ofctrl_add_flow(flow_table, OFTABLE_CHK_IN_PORT_SEC_ND, 90,
> -                        pb->header_.uuid.parts[0], m, ofpacts,
> -                        &pb->header_.uuid);
> +
> +        if (!is_vrrp) {
> +            match_set_arp_sha(m, phys_mac);
> +            ofctrl_add_flow(flow_table, OFTABLE_CHK_IN_PORT_SEC_ND, 90,
> +                            pb->header_.uuid.parts[0], m, ofpacts,
> +                            &pb->header_.uuid);
> +        }
> +
> +        struct masked_eth_addr *mmac;
> +        VECTOR_FOR_EACH_PTR (vrrp4_addrs, mmac) {
> +            match_set_arp_sha_masked(m, mmac->addr, mmac->mask);
> +            ofctrl_add_flow(flow_table, OFTABLE_CHK_IN_PORT_SEC_ND, 90,
> +                            pb->header_.uuid.parts[0], m, ofpacts,
> +                            &pb->header_.uuid);
> +        }
>      }
>  
>      /* Add the below logical flow equivalent OF rules in 'in_port_sec_nd'
> @@ -2578,74 +2730,62 @@ build_in_port_sec_arp_flows(const struct 
> sbrec_port_binding *pb,
>       *         arp && arp.sha == ps_addr.ea && arp.spa == 
> {ps_addr.ipv4_addrs}"
>       * action - "port_sec_failed = 0;"
>       */
> -    for (size_t j = 0; j < ps_addr->n_ipv4_addrs; j++) {
> -        reset_match_for_port_sec_flows(pb, MFF_LOG_INPORT, m);
> -        match_set_dl_src(m, ps_addr->ea);
> -        match_set_dl_type(m, htons(ETH_TYPE_ARP));
> -        match_set_arp_sha(m, ps_addr->ea);
> -
> -        ovs_be32 mask = ps_addr->ipv4_addrs[j].mask;
> -        if (ps_addr->ipv4_addrs[j].plen == 32 ||
> -                ps_addr->ipv4_addrs[j].addr & ~mask) {
> -            match_set_nw_src(m, ps_addr->ipv4_addrs[j].addr);
> -        } else {
> -            match_set_nw_src_masked(m, ps_addr->ipv4_addrs[j].addr, mask);
> +    const struct masked_ip4_addr *ip;
> +    VECTOR_FOR_EACH_PTR (ip4_addrs, ip) {
> +        match_set_nw_src_masked(m, ip->addr, ip->mask);
> +
> +        if (!is_vrrp) {
> +            match_set_arp_sha(m, phys_mac);
> +            ofctrl_add_flow(flow_table, OFTABLE_CHK_IN_PORT_SEC_ND, 90,
> +                            pb->header_.uuid.parts[0], m, ofpacts,
> +                            &pb->header_.uuid);
> +        }
> +
> +        struct masked_eth_addr *mmac;
> +        VECTOR_FOR_EACH_PTR (vrrp4_addrs, mmac) {
> +            match_set_arp_sha_masked(m, mmac->addr, mmac->mask);
> +            ofctrl_add_flow(flow_table, OFTABLE_CHK_IN_PORT_SEC_ND, 90,
> +                            pb->header_.uuid.parts[0], m, ofpacts,
> +                            &pb->header_.uuid);
>          }
> -        ofctrl_add_flow(flow_table, OFTABLE_CHK_IN_PORT_SEC_ND, 90,
> -                        pb->header_.uuid.parts[0], m, ofpacts,
> -                        &pb->header_.uuid);
>      }
>  }
>  
>  static void
>  build_in_port_sec_ip6_flows(const struct sbrec_port_binding *pb,
> -                           struct lport_addresses *ps_addr,
> -                           struct match *m, struct ofpbuf *ofpacts,
> -                           struct ovn_desired_flow_table *flow_table)
> +                            struct eth_addr mac, struct eth_addr mask,
> +                            const struct vector *ip6_addrs,
> +                            struct match *m, struct ofpbuf *ofpacts,
> +                            struct ovn_desired_flow_table *flow_table)
>  {
> -    if (!ps_addr->n_ipv6_addrs) {
> -        /* If no IPv6 addresses, then 'pb' is not allowed to send IPv6 
> traffic.
> -         * build_in_port_sec_default_flows() takes care of this scenario. */
> -        return;
> -    }
> -
>      /* Add the below logical flow equivalent OF rules in 'in_port_sec_nd'
>       * table.
>       * priority: 90
> -     * match - "inport == pb->port && eth.src == ps_addr.ea &&
> +     * match - "inport == pb->port && eth.src == mac/mask &&
>       *         ip6.src == {ps_addr.ipv6_addrs, lla}"
>       * action - "next;"
>       * description - Advance the packet for Neighbor Solicit/Adv check.
>       */
>      build_port_sec_adv_nd_check(ofpacts);
> +    reset_match_for_port_sec_flows(pb, MFF_LOG_INPORT, m);
> +    match_set_dl_src_masked(m, mac, mask);
> +    match_set_dl_type(m, htons(ETH_TYPE_IPV6));
>  
> -    for (size_t j = 0; j < ps_addr->n_ipv6_addrs; j++) {
> -        reset_match_for_port_sec_flows(pb, MFF_LOG_INPORT, m);
> -        match_set_dl_src(m, ps_addr->ea);
> -        match_set_dl_type(m, htons(ETH_TYPE_IPV6));
> -
> -        if (ps_addr->ipv6_addrs[j].plen == 128
> -            || !ipv6_addr_is_host_zero(&ps_addr->ipv6_addrs[j].addr,
> -                                        &ps_addr->ipv6_addrs[j].mask)) {
> -            match_set_ipv6_src(m, &ps_addr->ipv6_addrs[j].addr);
> -        } else {
> -            match_set_ipv6_src_masked(m, &ps_addr->ipv6_addrs[j].network,
> -                                        &ps_addr->ipv6_addrs[j].mask);
> -        }
> -
> +    const struct masked_ip6_addr *ip;
> +    VECTOR_FOR_EACH_PTR (ip6_addrs, ip) {
> +        match_set_ipv6_src_masked(m, &ip->addr, &ip->mask);
>          ofctrl_add_flow(flow_table, OFTABLE_CHK_IN_PORT_SEC, 90,
>                          pb->header_.uuid.parts[0], m, ofpacts,
>                          &pb->header_.uuid);
>      }
>  
> -    reset_match_for_port_sec_flows(pb, MFF_LOG_INPORT, m);
> -    match_set_dl_src(m, ps_addr->ea);
> -    match_set_dl_type(m, htons(ETH_TYPE_IPV6));
>  
>      struct in6_addr lla;
> -    in6_generate_lla(ps_addr->ea, &lla);
> -    match_set_ipv6_src(m, &lla);
> +    in6_generate_lla(mac, &lla);
> +    unsigned int plen = 128 - 48 + eth_addr_get_prefix_len(mask);
> +    struct in6_addr lla_mask = ipv6_create_mask(plen);
>  
> +    match_set_ipv6_src_masked(m, &lla, &lla_mask);
>      ofctrl_add_flow(flow_table, OFTABLE_CHK_IN_PORT_SEC, 90,
>                      pb->header_.uuid.parts[0], m, ofpacts,
>                      &pb->header_.uuid);
> @@ -2660,11 +2800,12 @@ build_in_port_sec_ip6_flows(const struct 
> sbrec_port_binding *pb,
>       */
>      build_port_sec_allow_action(ofpacts);
>      match_set_ipv6_src(m, &in6addr_any);
> -    struct in6_addr ip6, mask;
> -    char *err = ipv6_parse_masked("ff02::/16", &ip6, &mask);
> +
> +    struct in6_addr ip6, ip_mask;
> +    char *err = ipv6_parse_masked("ff02::/16", &ip6, &ip_mask);
>      ovs_assert(!err);
>  
> -    match_set_ipv6_dst_masked(m, &ip6, &mask);
> +    match_set_ipv6_dst_masked(m, &ip6, &ip_mask);
>      match_set_nw_proto(m, IPPROTO_ICMPV6);
>      match_set_icmp_type(m, 131);
>      match_set_icmp_code(m, 0);
> @@ -2697,99 +2838,118 @@ build_in_port_sec_ip6_flows(const struct 
> sbrec_port_binding *pb,
>   * 'in_port_sec_nd' table. */
>  static void
>  build_in_port_sec_nd_flows(const struct sbrec_port_binding *pb,
> -                           struct lport_addresses *ps_addr,
> -                           struct match *m, struct ofpbuf *ofpacts,
> +                           struct eth_addr phys_mac,
> +                           const struct vector *ip6_addrs,
> +                           const struct vector *vrrp6_addrs,
> +                           bool is_vrrp, struct match *m,
> +                           struct ofpbuf *ofpacts,
>                             struct ovn_desired_flow_table *flow_table)
>  {
>      build_port_sec_allow_action(ofpacts);
> +    reset_match_for_port_sec_flows(pb, MFF_LOG_INPORT, m);
> +    match_set_dl_src(m, phys_mac);
> +    match_set_dl_type(m, htons(ETH_TYPE_IPV6));
> +    match_set_nw_proto(m, IPPROTO_ICMPV6);
> +    match_set_icmp_code(m, 0);
> +    match_set_nw_ttl(m, 255);
>  
>      /* Add the below logical flow equivalent OF rules in 'in_port_sec_nd'
>       * table.
>       * priority: 90
> -     * match - "inport == pb->port && eth.src == ps_addr.ea &&
> -     *          icmp6 && icmp6.code == 135 && icmp6.type == 0 &&
> -     *          ip6.tll == 255 && nd.sll == {00:00:00:00:00:00, ps_addr.ea}"
> +     * match - "inport == pb->port && eth.src == phys_mac &&
> +     *          icmp6 && icmp6.type == 135 && icmp6.code == 0 &&
> +     *          ip6.tll == 255 &&
> +     *          nd.sll == {00:00:00:00:00:00, phys_mac, vrrp6_addrs}"
>       * action - "port_sec_failed = 0;"
>       */
> -    reset_match_for_port_sec_flows(pb, MFF_LOG_INPORT, m);
> -    match_set_dl_type(m, htons(ETH_TYPE_IPV6));
> -    match_set_nw_proto(m, IPPROTO_ICMPV6);
> -    match_set_dl_src(m, ps_addr->ea);
> -    match_set_nw_ttl(m, 255);
> +
>      match_set_icmp_type(m, 135);
> -    match_set_icmp_code(m, 0);
>  
>      match_set_arp_sha(m, eth_addr_zero);
>      ofctrl_add_flow(flow_table, OFTABLE_CHK_IN_PORT_SEC_ND, 90,
>                      pb->header_.uuid.parts[0], m, ofpacts,
>                      &pb->header_.uuid);
>  
> -    match_set_arp_sha(m, ps_addr->ea);
> -    ofctrl_add_flow(flow_table, OFTABLE_CHK_IN_PORT_SEC_ND, 90,
> -                    pb->header_.uuid.parts[0], m, ofpacts,
> -                    &pb->header_.uuid);
> +    if (!is_vrrp) {
> +        match_set_arp_sha(m, phys_mac);
> +        ofctrl_add_flow(flow_table, OFTABLE_CHK_IN_PORT_SEC_ND, 90,
> +                        pb->header_.uuid.parts[0], m, ofpacts,
> +                        &pb->header_.uuid);
> +    }
>  
> +    struct masked_eth_addr *mmac;
> +    VECTOR_FOR_EACH_PTR (vrrp6_addrs, mmac) {
> +        match_set_arp_sha_masked(m, mmac->addr, mmac->mask);
> +        ofctrl_add_flow(flow_table, OFTABLE_CHK_IN_PORT_SEC_ND, 90,
> +                        pb->header_.uuid.parts[0], m, ofpacts,
> +                        &pb->header_.uuid);
> +    }
> +
> +    match_set_arp_sha_masked(m, eth_addr_zero, eth_addr_zero);
>      match_set_icmp_type(m, 136);
> -    match_set_icmp_code(m, 0);
> -    if (ps_addr->n_ipv6_addrs) {
> +    if (!vector_is_empty(ip6_addrs)) {
>          /* Add the below logical flow equivalent OF rules in 'in_port_sec_nd'
>           * table if IPv6 addresses are configured.
>           * priority: 90
> -         * match - "inport == pb->port && eth.src == ps_addr.ea && icmp6 &&
> -         *          icmp6.code == 136 && icmp6.type == 0 && ip6.tll == 255 &&
> -         *          nd.tll == {00:00:00:00:00:00, ps_addr.ea} &&
> -         *          nd.target == {ps_addr.ipv6_addrs, lla}"
> +         * match - "inport == pb->port && eth.src == phys_mac && icmp6 &&
> +         *          icmp6.type == 136 && icmp6.code == 0 && ip6.tll == 255 &&
> +         *          nd.tll == {00:00:00:00:00:00, phys_mac, vrrp6_addrs} &&
> +         *          nd.target == {lla, ip6_addrs}"
>           * action - "port_sec_failed = 0;"
>           */
>          struct in6_addr lla;
> -        in6_generate_lla(ps_addr->ea, &lla);
> -        match_set_arp_tha(m, eth_addr_zero);
> -
> -        match_set_nd_target(m, &lla);
> -        ofctrl_add_flow(flow_table, OFTABLE_CHK_IN_PORT_SEC_ND, 90,
> -                        pb->header_.uuid.parts[0], m, ofpacts,
> -                        &pb->header_.uuid);
> -        match_set_arp_tha(m, ps_addr->ea);
> +        in6_generate_lla(phys_mac, &lla);
>          match_set_nd_target(m, &lla);
> +
> +        match_set_arp_tha(m, eth_addr_zero);
>          ofctrl_add_flow(flow_table, OFTABLE_CHK_IN_PORT_SEC_ND, 90,
>                          pb->header_.uuid.parts[0], m, ofpacts,
>                          &pb->header_.uuid);
>  
> -        for (size_t j = 0; j < ps_addr->n_ipv6_addrs; j++) {
> -            reset_match_for_port_sec_flows(pb, MFF_LOG_INPORT, m);
> -            match_set_dl_src(m, ps_addr->ea);
> -            match_set_dl_type(m, htons(ETH_TYPE_IPV6));
> -            match_set_nw_proto(m, IPPROTO_ICMPV6);
> -            match_set_nw_ttl(m, 255);
> -            match_set_icmp_type(m, 136);
> -            match_set_icmp_code(m, 0);
> -            match_set_arp_tha(m, eth_addr_zero);
> -
> -            if (ps_addr->ipv6_addrs[j].plen == 128
> -                || !ipv6_addr_is_host_zero(&ps_addr->ipv6_addrs[j].addr,
> -                                            &ps_addr->ipv6_addrs[j].mask)) {
> -                match_set_nd_target(m, &ps_addr->ipv6_addrs[j].addr);
> -            } else {
> -                match_set_nd_target_masked(m, 
> &ps_addr->ipv6_addrs[j].network,
> -                                           &ps_addr->ipv6_addrs[j].mask);
> -            }
> +        if (!is_vrrp) {
> +            match_set_arp_tha(m, phys_mac);
> +            ofctrl_add_flow(flow_table, OFTABLE_CHK_IN_PORT_SEC_ND, 90,
> +                            pb->header_.uuid.parts[0], m, ofpacts,
> +                            &pb->header_.uuid);
> +        }
>  
> +        VECTOR_FOR_EACH_PTR (vrrp6_addrs, mmac) {
> +            match_set_arp_tha_masked(m, mmac->addr, mmac->mask);
>              ofctrl_add_flow(flow_table, OFTABLE_CHK_IN_PORT_SEC_ND, 90,
>                              pb->header_.uuid.parts[0], m, ofpacts,
>                              &pb->header_.uuid);
> +        }
>  
> -            match_set_arp_tha(m, ps_addr->ea);
> +        const struct masked_ip6_addr *ip;
> +        VECTOR_FOR_EACH_PTR (ip6_addrs, ip) {
> +            match_set_nd_target_masked(m, &ip->addr, &ip->mask);
> +
> +            match_set_arp_tha(m, eth_addr_zero);
>              ofctrl_add_flow(flow_table, OFTABLE_CHK_IN_PORT_SEC_ND, 90,
>                              pb->header_.uuid.parts[0], m, ofpacts,
>                              &pb->header_.uuid);
> +
> +            if (!is_vrrp) {
> +                match_set_arp_tha(m, phys_mac);
> +                ofctrl_add_flow(flow_table, OFTABLE_CHK_IN_PORT_SEC_ND, 90,
> +                                pb->header_.uuid.parts[0], m, ofpacts,
> +                                &pb->header_.uuid);
> +            }
> +
> +            VECTOR_FOR_EACH_PTR (vrrp6_addrs, mmac) {
> +                match_set_arp_tha_masked(m, mmac->addr, mmac->mask);
> +                ofctrl_add_flow(flow_table, OFTABLE_CHK_IN_PORT_SEC_ND, 90,
> +                                pb->header_.uuid.parts[0], m, ofpacts,
> +                                &pb->header_.uuid);
> +            }
>          }
>      } else {
>          /* Add the below logical flow equivalent OF rules in 'in_port_sec_nd'
>           * table if no IPv6 addresses are configured.
>           * priority: 90
> -         * match - "inport == pb->port && eth.src == ps_addr.ea && icmp6 &&
> +         * match - "inport == pb->port && eth.src == phys_mac && icmp6 &&
>           *          icmp6.code == 136 && icmp6.type == 0 && ip6.tll == 255 &&
> -         *          nd.tll == {00:00:00:00:00:00, ps_addr.ea}"
> +         *          nd.tll == {00:00:00:00:00:00, phys_mac, vrrp6_addrs}"
>           * action - "port_sec_failed = 0;"
>           */
>          match_set_arp_tha(m, eth_addr_zero);
> @@ -2797,27 +2957,36 @@ build_in_port_sec_nd_flows(const struct 
> sbrec_port_binding *pb,
>                          pb->header_.uuid.parts[0], m, ofpacts,
>                          &pb->header_.uuid);
>  
> -        match_set_arp_tha(m, ps_addr->ea);
> -        ofctrl_add_flow(flow_table, OFTABLE_CHK_IN_PORT_SEC_ND, 90,
> -                        pb->header_.uuid.parts[0], m, ofpacts,
> -                        &pb->header_.uuid);
> +        if (!is_vrrp) {
> +            match_set_arp_tha(m, phys_mac);
> +            ofctrl_add_flow(flow_table, OFTABLE_CHK_IN_PORT_SEC_ND, 90,
> +                            pb->header_.uuid.parts[0], m, ofpacts,
> +                            &pb->header_.uuid);
> +        }
> +
> +        VECTOR_FOR_EACH_PTR (vrrp6_addrs, mmac) {
> +            match_set_arp_tha_masked(m, mmac->addr, mmac->mask);
> +            ofctrl_add_flow(flow_table, OFTABLE_CHK_IN_PORT_SEC_ND, 90,
> +                            pb->header_.uuid.parts[0], m, ofpacts,
> +                            &pb->header_.uuid);
> +        }
>      }
>  }
>  
>  static void
>  build_out_port_sec_no_ip_flows(const struct sbrec_port_binding *pb,
> -                               struct lport_addresses *ps_addr,
> +                               struct eth_addr mac, struct eth_addr mask,
>                                 struct match *m, struct ofpbuf *ofpacts,
>                                 struct ovn_desired_flow_table *flow_table)
>  {
>      /* Add the below logical flow equivalent OF rules in 'out_port_sec' 
> table.
>       * priority: 85
> -     * match - "outport == pb->logical_port && eth.dst == ps_addr.ea"
> +     * match - "outport == pb->logical_port && eth.dst == mac/mask"
>       * action - "port_sec_failed = 0;"
>       * description: "Allow the packet if eth.dst matches."
>       */
>      reset_match_for_port_sec_flows(pb, MFF_LOG_OUTPORT, m);
> -    match_set_dl_dst(m, ps_addr->ea);
> +    match_set_dl_dst_masked(m, mac, mask);
>      build_port_sec_allow_action(ofpacts);
>      ofctrl_add_flow(flow_table, OFTABLE_CHK_OUT_PORT_SEC, 85,
>                      pb->header_.uuid.parts[0], m, ofpacts,
> @@ -2826,97 +2995,70 @@ build_out_port_sec_no_ip_flows(const struct 
> sbrec_port_binding *pb,
>  
>  static void
>  build_out_port_sec_ip4_flows(const struct sbrec_port_binding *pb,
> -                            struct lport_addresses *ps_addr,
> -                            struct match *m, struct ofpbuf *ofpacts,
> -                            struct ovn_desired_flow_table *flow_table)
> +                             struct eth_addr mac, struct eth_addr mask,
> +                             const struct vector *ip4_addrs,
> +                             struct match *m, struct ofpbuf *ofpacts,
> +                             struct ovn_desired_flow_table *flow_table)
>  {
> -    if (!ps_addr->n_ipv4_addrs && !ps_addr->n_ipv6_addrs) {
> -         /* No IPv4 and no IPv6 addresses in the port security.
> -          * Both IPv4 and IPv6 traffic should be delivered to the
> -          * lport. build_out_port_sec_no_ip_flows() takes care of
> -          * adding the required flow(s) to allow. */
> -        return;
> -    }
> +    reset_match_for_port_sec_flows(pb, MFF_LOG_OUTPORT, m);
> +    match_set_dl_dst_masked(m, mac, mask);
> +    match_set_dl_type(m, htons(ETH_TYPE_IP));
>  
>      /* Add the below logical flow equivalent OF rules in 'out_port_sec' 
> table.
>       * priority: 90
> -     * match - "outport == pb->logical_port && eth.dst == ps_addr.ea && ip4"
> +     * match - "outport == pb->logical_port && eth.dst == mac/mask && ip4"
>       * action - "port_sec_failed = 1;"
>       * description: Default drop IPv4 packets.  If IPv4 addresses are
>       *              configured, then higher priority flows are added
>       *              to allow specific IPv4 packets.
>       */
> -    reset_match_for_port_sec_flows(pb, MFF_LOG_OUTPORT, m);
> -    match_set_dl_dst(m, ps_addr->ea);
> -    match_set_dl_type(m, htons(ETH_TYPE_IP));
> +
>      build_port_sec_deny_action(ofpacts);
>      ofctrl_add_flow(flow_table, OFTABLE_CHK_OUT_PORT_SEC, 90,
>                      pb->header_.uuid.parts[0], m, ofpacts,
>                      &pb->header_.uuid);
>  
> -    if (!ps_addr->n_ipv4_addrs) {
> +    if (vector_is_empty(ip4_addrs)) {
>          return;
>      }
>  
> +    build_port_sec_allow_action(ofpacts);
>      /* Add the below logical flow equivalent OF rules in 'out_port_sec' 
> table.
>       * priority: 95
> -     * match - "outport == pb->logical_port && eth.dst == ps_addr.ea &&
> -     *          ip4.dst == {ps_addr.ipv4_addrs, 255.255.255.255, 
> 224.0.0.0/4},"
> +     * match - "outport == pb->logical_port && eth.dst == mac/mask &&
> +     *          ip4.dst == {ip4_addrs, 255.255.255.255, 224.0.0.0/4},"
>       * action - "port_sec_failed = 0;"
>       */
> -    build_port_sec_allow_action(ofpacts);
> -    for (size_t j = 0; j < ps_addr->n_ipv4_addrs; j++) {
> -        reset_match_for_port_sec_flows(pb, MFF_LOG_OUTPORT, m);
> -        match_set_dl_dst(m, ps_addr->ea);
> -        match_set_dl_type(m, htons(ETH_TYPE_IP));
> -        ovs_be32 mask = ps_addr->ipv4_addrs[j].mask;
> -        if (ps_addr->ipv4_addrs[j].plen == 32
> -                || ps_addr->ipv4_addrs[j].addr & ~mask) {
> -
> -            if (ps_addr->ipv4_addrs[j].plen != 32) {
> -                /* Special case to allow bcast traffic.
> -                 * Eg. If ps_addr is 10.0.0.4/24, then add the below flow
> -                 * priority: 95
> -                 * match - "outport == pb->logical_port &&
> -                 *          eth.dst == ps_addr.ea &&
> -                 *          ip4.dst == 10.0.0.255"
> -                 * action - "port_sec_failed = 0;"
> -                 */
> -                ovs_be32 bcast_addr;
> -                ovs_assert(ip_parse(ps_addr->ipv4_addrs[j].bcast_s,
> -                                    &bcast_addr));
> -                match_set_nw_dst(m, bcast_addr);
> -                ofctrl_add_flow(flow_table, OFTABLE_CHK_OUT_PORT_SEC, 95,
> -                                pb->header_.uuid.parts[0], m, ofpacts,
> -                                &pb->header_.uuid);
> -            }
> -
> -            match_set_nw_dst(m, ps_addr->ipv4_addrs[j].addr);
> -        } else {
> -            /* host portion is zero */
> -            match_set_nw_dst_masked(m, ps_addr->ipv4_addrs[j].addr,
> -                                    mask);
> -        }
> -
> +    const struct masked_ip4_addr *ip;
> +    VECTOR_FOR_EACH_PTR (ip4_addrs, ip) {
> +        match_set_nw_dst_masked(m, ip->addr, ip->mask);
>          ofctrl_add_flow(flow_table, OFTABLE_CHK_OUT_PORT_SEC, 95,
>                          pb->header_.uuid.parts[0], m, ofpacts,
>                          &pb->header_.uuid);
> -    }
>  
> -    reset_match_for_port_sec_flows(pb, MFF_LOG_OUTPORT, m);
> -    match_set_dl_dst(m, ps_addr->ea);
> -    match_set_dl_type(m, htons(ETH_TYPE_IP));
> +        if (ip->bcast) {
> +            /* Special case to allow bcast traffic.
> +             * Eg. If address is 10.0.0.4/24, then add the below flow
> +             * priority: 95
> +             * match - "outport == pb->logical_port &&
> +             *          eth.dst == ps_addr.ea &&
> +             *          ip4.dst == 10.0.0.255"
> +             * action - "port_sec_failed = 0;"
> +             */
> +            match_set_nw_dst(m, ip->bcast);
> +            ofctrl_add_flow(flow_table, OFTABLE_CHK_OUT_PORT_SEC, 95,
> +                            pb->header_.uuid.parts[0], m, ofpacts,
> +                            &pb->header_.uuid);
> +        }
> +    }
>  
> -    ovs_be32 ip4 = htonl(0xffffffff);
> -    match_set_nw_dst(m, ip4);
> +    match_set_nw_dst(m, htonl(0xffffffff));
>      ofctrl_add_flow(flow_table, OFTABLE_CHK_OUT_PORT_SEC, 95,
>                      pb->header_.uuid.parts[0], m, ofpacts,
>                      &pb->header_.uuid);
>  
>      /* Allow 224.0.0.0/4 traffic. */
> -    ip4 = htonl(0xe0000000);
> -    ovs_be32 mask = htonl(0xf0000000);
> -    match_set_nw_dst_masked(m, ip4, mask);
> +    match_set_nw_dst_masked(m, htonl(0xe0000000), htonl(0xf0000000));
>      ofctrl_add_flow(flow_table, OFTABLE_CHK_OUT_PORT_SEC, 95,
>                      pb->header_.uuid.parts[0], m, ofpacts,
>                      &pb->header_.uuid);
> @@ -2924,112 +3066,270 @@ build_out_port_sec_ip4_flows(const struct 
> sbrec_port_binding *pb,
>  
>  static void
>  build_out_port_sec_ip6_flows(const struct sbrec_port_binding *pb,
> -                            struct lport_addresses *ps_addr,
> -                            struct match *m, struct ofpbuf *ofpacts,
> -                            struct ovn_desired_flow_table *flow_table)
> +                             struct eth_addr mac, struct eth_addr mask,
> +                             const struct vector *ip6_addrs,
> +                             struct match *m, struct ofpbuf *ofpacts,
> +                             struct ovn_desired_flow_table *flow_table)
>  {
> -    if (!ps_addr->n_ipv4_addrs && !ps_addr->n_ipv6_addrs) {
> -        /* No IPv4 and no IPv6 addresses in the port security.
> -         * Both IPv4 and IPv6 traffic should be delivered to the
> -         * lport. build_out_port_sec_no_ip_flows() takes care of
> -         * adding the required flow(s) to allow. */
> -        return;
> -    }
> +    reset_match_for_port_sec_flows(pb, MFF_LOG_OUTPORT, m);
> +    match_set_dl_dst_masked(m, mac, mask);
> +    match_set_dl_type(m, htons(ETH_TYPE_IPV6));
>  
>      /* Add the below logical flow equivalent OF rules in 'out_port_sec' 
> table.
>       * priority: 90
> -     * match - "outport == pb->logical_port && eth.dst == ps_addr.ea && ip6"
> +     * match - "outport == pb->logical_port && eth.dst == mac/mask && ip6"
>       * action - "port_sec_failed = 1;"
>       * description: Default drop IPv6 packets.  If IPv6 addresses are
>       *              configured, then higher priority flows are added
>       *              to allow specific IPv6 packets.
>       */
> -    reset_match_for_port_sec_flows(pb, MFF_LOG_OUTPORT, m);
> -    match_set_dl_dst(m, ps_addr->ea);
> -    match_set_dl_type(m, htons(ETH_TYPE_IPV6));
>      build_port_sec_deny_action(ofpacts);
>      ofctrl_add_flow(flow_table, OFTABLE_CHK_OUT_PORT_SEC, 90,
>                      pb->header_.uuid.parts[0], m, ofpacts,
>                      &pb->header_.uuid);
>  
> -    if (!ps_addr->n_ipv6_addrs) {
> +    if (vector_is_empty(ip6_addrs)) {
>          return;
>      }
>  
> +    build_port_sec_allow_action(ofpacts);
>      /* Add the below logical flow equivalent OF rules in 'out_port_sec' 
> table.
>       * priority: 95
> -     * match - "outport == pb->logical_port && eth.dst == ps_addr.ea &&
> -     *          ip6.dst == {ps_addr.ipv6_addrs, lla, ff00::/8},"
> +     * match - "outport == pb->logical_port && eth.dst == mac/mask &&
> +     *          ip6.dst == {mac/mask, ip6_addrs, lla, ff00::/8},"
>       * action - "port_sec_failed = 0;"
>       */
> -    build_port_sec_allow_action(ofpacts);
> -    for (size_t j = 0; j < ps_addr->n_ipv6_addrs; j++) {
> -        reset_match_for_port_sec_flows(pb, MFF_LOG_OUTPORT, m);
> -        match_set_dl_dst(m, ps_addr->ea);
> -        match_set_dl_type(m, htons(ETH_TYPE_IPV6));
> -
> -        if (ps_addr->ipv6_addrs[j].plen == 128
> -            || !ipv6_addr_is_host_zero(&ps_addr->ipv6_addrs[j].addr,
> -                                        &ps_addr->ipv6_addrs[j].mask)) {
> -            match_set_ipv6_dst(m, &ps_addr->ipv6_addrs[j].addr);
> -        } else {
> -            match_set_ipv6_dst_masked(m, &ps_addr->ipv6_addrs[j].network,
> -                                      &ps_addr->ipv6_addrs[j].mask);
> -        }
> -
> +    const struct masked_ip6_addr *ip;
> +    VECTOR_FOR_EACH_PTR (ip6_addrs, ip) {
> +        match_set_ipv6_dst_masked(m, &ip->addr, &ip->mask);
>          ofctrl_add_flow(flow_table, OFTABLE_CHK_OUT_PORT_SEC, 95,
>                          pb->header_.uuid.parts[0], m, ofpacts,
>                          &pb->header_.uuid);
>      }
>  
>      struct in6_addr lla;
> -    in6_generate_lla(ps_addr->ea, &lla);
> +    in6_generate_lla(mac, &lla);
> +    unsigned int plen = 128 - 48 + eth_addr_get_prefix_len(mask);
> +    struct in6_addr lla_mask = ipv6_create_mask(plen);
>  
> -    reset_match_for_port_sec_flows(pb, MFF_LOG_OUTPORT, m);
> -    match_set_dl_dst(m, ps_addr->ea);
> -    match_set_dl_type(m, htons(ETH_TYPE_IPV6));
> -    match_set_ipv6_dst(m, &lla);
> +    match_set_ipv6_dst_masked(m, &lla, &lla_mask);
>      ofctrl_add_flow(flow_table, OFTABLE_CHK_OUT_PORT_SEC, 95,
>                      pb->header_.uuid.parts[0], m, ofpacts,
>                      &pb->header_.uuid);
>  
> -    struct in6_addr ip6, mask;
> -    char *err = ipv6_parse_masked("ff00::/8", &ip6, &mask);
> +    struct in6_addr ip6, ip_mask;
> +    char *err = ipv6_parse_masked("ff00::/8", &ip6, &ip_mask);
>      ovs_assert(!err);
>  
> -    match_set_ipv6_dst_masked(m, &ip6, &mask);
> +    match_set_ipv6_dst_masked(m, &ip6, &ip_mask);
>      ofctrl_add_flow(flow_table, OFTABLE_CHK_OUT_PORT_SEC, 95,
>                      pb->header_.uuid.parts[0], m, ofpacts,
>                      &pb->header_.uuid);
>  }
>  
>  static void
> -consider_port_sec_flows(const struct sbrec_port_binding *pb,
> -                        struct ovn_desired_flow_table *flow_table)
> +build_port_sec_entry_flows(const struct sbrec_port_binding *pb,
> +                           const struct port_security_addresses *ps_addr,
> +                           struct match *m, struct ofpbuf *ofpacts,
> +                           struct ovn_desired_flow_table *flow_table)
>  {
> -    if (!pb->n_port_security) {
> -        return;
> +    /* Input no-ip flows. */
> +    if (vector_is_empty(&ps_addr->ip4) && vector_is_empty(&ps_addr->ip6)) {
> +        build_in_port_sec_no_ip_flows(pb, ps_addr->phys_addr, eth_addr_exact,
> +                                      m, ofpacts, flow_table);
>      }
>  
> -    struct lport_addresses *ps_addrs;   /* Port security addresses. */
> -    size_t n_ps_addrs = 0;
> +    /* Input IPv4 flows. */
> +    if (!vector_is_empty(&ps_addr->ip4)) {
> +        build_in_port_sec_ip4_flows(pb, ps_addr->phys_addr, eth_addr_exact,
> +                                    &ps_addr->ip4, m, ofpacts, flow_table);
> +    }
>  
> -    ps_addrs = xmalloc(sizeof *ps_addrs * pb->n_port_security);
> -    for (size_t i = 0; i < pb->n_port_security; i++) {
> -        if (!extract_lsp_addresses(pb->port_security[i],
> -                                    &ps_addrs[n_ps_addrs])) {
> -            static struct vlog_rate_limit rl
> -                = VLOG_RATE_LIMIT_INIT(1, 1);
> -            VLOG_WARN_RL(&rl, "invalid syntax '%s' in port "
> -                         "security. No MAC address found",
> -                         pb->port_security[i]);
> -            continue;
> +    /* Input ARP flows. */
> +    if (!vector_is_empty(&ps_addr->ip4) || vector_is_empty(&ps_addr->ip6)) {
> +        build_in_port_sec_arp_flows(pb, ps_addr->phys_addr, &ps_addr->ip4,
> +                                    &ps_addr->vrrp4, false, m, ofpacts,
> +                                    flow_table);
> +    }
> +
> +    /* Input Ipv6 flows. */
> +    if (!vector_is_empty(&ps_addr->ip6)) {
> +        build_in_port_sec_ip6_flows(pb, ps_addr->phys_addr, eth_addr_exact,
> +                                    &ps_addr->ip6, m, ofpacts, flow_table);
> +    }
> +
> +    /* Input ND flows. */
> +    build_in_port_sec_nd_flows(pb, ps_addr->phys_addr, &ps_addr->ip6,
> +                               &ps_addr->vrrp6, false, m, ofpacts,
> +                               flow_table);
> +
> +    /* Output no-ip flows. */
> +    build_out_port_sec_no_ip_flows(pb, ps_addr->phys_addr, eth_addr_exact,
> +                                    m, ofpacts, flow_table);
> +
> +    /* Output IPv4 flows. */
> +    if (!vector_is_empty(&ps_addr->ip4) || !vector_is_empty(&ps_addr->ip6)) {
> +        build_out_port_sec_ip4_flows(pb, ps_addr->phys_addr, eth_addr_exact,
> +                                     &ps_addr->ip4, m, ofpacts, flow_table);
> +    }
> +
> +    /* Output Ipv6 flows. */
> +    if (!vector_is_empty(&ps_addr->ip4) || !vector_is_empty(&ps_addr->ip6)) {
> +        build_out_port_sec_ip6_flows(pb, ps_addr->phys_addr, eth_addr_exact,
> +                                     &ps_addr->ip6, m, ofpacts, flow_table);
> +    }
> +}
> +
> +static void
> +build_port_sec_entry_vrrp_flows(const struct sbrec_port_binding *pb,
> +                                const struct port_security_addresses 
> *ps_addr,
> +                                struct match *m, struct ofpbuf *ofpacts,
> +                                struct ovn_desired_flow_table *flow_table)
> +{
> +    const struct masked_eth_addr *maddr;
> +
> +    /* Input no-ip flows. */
> +    if (vector_is_empty(&ps_addr->ip4) && vector_is_empty(&ps_addr->ip6)) {
> +        VECTOR_FOR_EACH_PTR (&ps_addr->vrrp4, maddr) {
> +            build_in_port_sec_no_ip_flows(pb, maddr->addr, maddr->mask,
> +                                          m, ofpacts, flow_table);
> +        }
> +
> +        VECTOR_FOR_EACH_PTR (&ps_addr->vrrp6, maddr) {
> +            build_in_port_sec_no_ip_flows(pb, maddr->addr, maddr->mask,
> +                                          m, ofpacts, flow_table);
>          }
> -        n_ps_addrs++;
>      }
>  
> -    if (!n_ps_addrs) {
> -        free(ps_addrs);
> +    /* Input IPv4 flows. */
> +    if (!vector_is_empty(&ps_addr->ip4)) {
> +        VECTOR_FOR_EACH_PTR (&ps_addr->vrrp4, maddr) {
> +            build_in_port_sec_ip4_flows(pb, maddr->addr, maddr->mask,
> +                                        &ps_addr->ip4, m, ofpacts, 
> flow_table);
> +        }
> +    }
> +
> +    /* Input ARP flows. */
> +    if (!vector_is_empty(&ps_addr->ip4) || vector_is_empty(&ps_addr->ip6)) {
> +        build_in_port_sec_arp_flows(pb, ps_addr->phys_addr, &ps_addr->ip4,
> +                                    &ps_addr->vrrp4, true, m, ofpacts,
> +                                    flow_table);
> +    }
> +
> +    /* Input Ipv6 flows. */
> +    if (!vector_is_empty(&ps_addr->ip6)) {
> +        VECTOR_FOR_EACH_PTR (&ps_addr->vrrp6, maddr) {
> +            build_in_port_sec_ip6_flows(pb, maddr->addr, maddr->mask,
> +                                        &ps_addr->ip6, m, ofpacts, 
> flow_table);
> +        }
> +    }
> +
> +    /* Input ND flows. */
> +    build_in_port_sec_nd_flows(pb, ps_addr->phys_addr, &ps_addr->ip6,
> +                               &ps_addr->vrrp6, true, m, ofpacts,
> +                               flow_table);
> +
> +    /* Output no-ip flows. */
> +    VECTOR_FOR_EACH_PTR (&ps_addr->vrrp4, maddr) {
> +        build_out_port_sec_no_ip_flows(pb, maddr->addr, maddr->mask,
> +                                       m, ofpacts, flow_table);
> +    }
> +
> +    VECTOR_FOR_EACH_PTR (&ps_addr->vrrp6, maddr) {
> +        build_out_port_sec_no_ip_flows(pb, maddr->addr, maddr->mask,
> +                                       m, ofpacts, flow_table);
> +    }
> +
> +    /* Output IPv4 flows. */
> +    if (!vector_is_empty(&ps_addr->ip4) || !vector_is_empty(&ps_addr->ip6)) {
> +        VECTOR_FOR_EACH_PTR (&ps_addr->vrrp4, maddr) {
> +            build_out_port_sec_ip4_flows(pb, maddr->addr, maddr->mask,
> +                                         &ps_addr->ip4, m, ofpacts,
> +                                         flow_table);
> +        }
> +    }
> +
> +    /* Output Ipv6 flows. */
> +    if (!vector_is_empty(&ps_addr->ip4) || !vector_is_empty(&ps_addr->ip6)) {
> +        VECTOR_FOR_EACH_PTR (&ps_addr->vrrp6, maddr) {
> +            build_out_port_sec_ip6_flows(pb, maddr->addr, maddr->mask,
> +                                         &ps_addr->ip6, m, ofpacts,
> +                                         flow_table);
> +        }
> +    }
> +}
> +
> +static bool
> +port_security_addresses_parse_entry(const char *entry, const char *lsp,
> +                                    struct port_security_addresses *ps_addr)
> +{
> +    static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 1);
> +
> +    bool vrrpv3 = !strncmp(entry, "VRRPv3", 6);
> +    int n = vrrpv3 ? 7 : 0;
> +
> +    if (!ovs_scan_len(entry, &n, ETH_ADDR_SCAN_FMT,
> +                      ETH_ADDR_SCAN_ARGS(ps_addr->phys_addr))) {
> +        VLOG_WARN_RL(&rl, "invalid syntax '%s' in port security for LSP %s: "
> +                     "No MAC address found", entry, lsp);
> +        return false;
> +    }
> +
> +    bool ok = true;
> +
> +    /* Only MAC address is provided. */
> +    if (!entry[n]) {
> +        goto vrrp_check;
> +    }
> +
> +    char *save_ptr = NULL;
> +    char *tokstr = xstrdup(entry + n);
> +    for (char *token = strtok_r(tokstr, " ", &save_ptr);
> +         token != NULL;
> +         token = strtok_r(NULL, " ", &save_ptr)) {
> +        struct eth_addr mac;
> +        struct in6_addr ip;
> +        unsigned int plen;
> +
> +        if (vrrpv3 && eth_addr_parse_masked(token, &mac, &plen)) {
> +            if (!port_security_addresses_add_vrrp_mac(ps_addr, mac, plen)) {
> +                VLOG_WARN_RL(&rl, "invalid syntax '%s' in port security for"
> +                             " LSP %s: Invalid VRRPv3 MAC", token, lsp);
> +                ok = false;
> +                break;
> +            }
> +        } else if (ip46_parse_cidr(token, &ip, &plen)) {
> +            if (!port_security_addresses_add_ip(ps_addr, ip, plen)) {
> +                VLOG_WARN_RL(&rl, "invalid syntax '%s' in port security for"
> +                             " LSP %s: Invalid IP", token, lsp);
> +                ok = false;
> +                break;
> +            }
> +        } else {
> +            VLOG_WARN_RL(&rl, "invalid syntax '%s' in port security for"
> +                         " LSP %s: Invalid IP or MAC", token, lsp);
> +            ok = false;
> +            break;
> +        }
> +    }
> +
> +    free(tokstr);
> +
> +vrrp_check:
> +    if (vrrpv3 && vector_is_empty(&ps_addr->vrrp4) &&
> +        vector_is_empty(&ps_addr->vrrp6)) {
> +        vector_push(&ps_addr->vrrp4, &maddr_any_vrrp4);
> +        vector_push(&ps_addr->vrrp6, &maddr_any_vrrp6);
> +    }
> +
> +    return ok;
> +}
> +
> +static void
> +consider_port_sec_flows(const struct sbrec_port_binding *pb,
> +                        struct ovn_desired_flow_table *flow_table)
> +{
> +    if (!pb->n_port_security) {
>          return;
>      }
>  
> @@ -3037,48 +3337,34 @@ consider_port_sec_flows(const struct 
> sbrec_port_binding *pb,
>      uint64_t stub[1024 / 8];
>      struct ofpbuf ofpacts = OFPBUF_STUB_INITIALIZER(stub);
>  
> -    build_in_port_sec_default_flows(pb, &match, &ofpacts, flow_table);
> +    bool flows_installed = false;
> +    struct port_security_addresses ps_addr;
> +    port_security_addresses_init(&ps_addr);
>  
> -    for (size_t i = 0; i < n_ps_addrs; i++) {
> -        build_in_port_sec_no_ip_flows(pb, &ps_addrs[i], &match, &ofpacts,
> -                                      flow_table);
> -        build_in_port_sec_ip4_flows(pb, &ps_addrs[i], &match, &ofpacts,
> -                                    flow_table);
> -        build_in_port_sec_arp_flows(pb, &ps_addrs[i], &match, &ofpacts,
> -                                    flow_table);
> -        build_in_port_sec_ip6_flows(pb, &ps_addrs[i], &match, &ofpacts,
> -                                    flow_table);
> -        build_in_port_sec_nd_flows(pb, &ps_addrs[i], &match, &ofpacts,
> -                                   flow_table);
> -    }
> +    for (size_t i = 0; i < pb->n_port_security; i++) {
> +        if (port_security_addresses_parse_entry(pb->port_security[i],
> +                                                pb->logical_port, &ps_addr)) 
> {
> +            if (vector_is_empty(&ps_addr.vrrp4) &&
> +                vector_is_empty(&ps_addr.vrrp6)) {
> +                build_port_sec_entry_flows(pb, &ps_addr, &match,
> +                                           &ofpacts, flow_table);
>  
> -    /* Out port security. */
> +            } else {
> +                build_port_sec_entry_vrrp_flows(pb, &ps_addr, &match,
> +                                                &ofpacts, flow_table);
> +            }
>  
> -    /* Add the below logical flow equivalent OF rules in 'out_port_sec_nd'
> -     * table.
> -     * priority: 80
> -     * match - "outport == pb->logical_port"
> -     * action - "port_sec_failed = 1;"
> -     * descrption: "Drop all traffic"
> -     */
> -    reset_match_for_port_sec_flows(pb, MFF_LOG_OUTPORT, &match);
> -    build_port_sec_deny_action(&ofpacts);
> -    ofctrl_add_flow(flow_table, OFTABLE_CHK_OUT_PORT_SEC, 80,
> -                    pb->header_.uuid.parts[0], &match, &ofpacts,
> -                    &pb->header_.uuid);
> +            flows_installed = true;
> +        }
>  
> -    for (size_t i = 0; i < n_ps_addrs; i++) {
> -        build_out_port_sec_no_ip_flows(pb, &ps_addrs[i], &match, &ofpacts,
> -                                       flow_table);
> -        build_out_port_sec_ip4_flows(pb, &ps_addrs[i], &match, &ofpacts,
> -                                       flow_table);
> -        build_out_port_sec_ip6_flows(pb, &ps_addrs[i], &match, &ofpacts,
> -                                       flow_table);
> +        port_security_addresses_clear(&ps_addr);
>      }
>  
> -    ofpbuf_uninit(&ofpacts);
> -    for (size_t i = 0; i < n_ps_addrs; i++) {
> -        destroy_lport_addresses(&ps_addrs[i]);
> +    if (flows_installed) {
> +        build_in_port_sec_default_flows(pb, &match, &ofpacts, flow_table);
> +        build_out_port_sec_default_flows(pb, &match, &ofpacts, flow_table);
>      }
> -    free(ps_addrs);
> +
> +    ofpbuf_uninit(&ofpacts);
> +    port_security_addresses_destroy(&ps_addr);
>  }
> diff --git a/ovn-nb.xml b/ovn-nb.xml
> index 1acbf202b..a9c68e1a4 100644
> --- a/ovn-nb.xml
> +++ b/ovn-nb.xml
> @@ -2057,6 +2057,21 @@
>            addresses within an element may be space or comma separated.
>          </p>
>  
> +        <p>
> +          This also supports the special prefix "VRRPv3" that allows
> +          specification of single physical MAC and multiple VRRPv3 MAC
> +          addresses. As for non VRRPv3 entries, multiple IP addresses can be
> +          associated with the specified MACs. "VRRPv3" with a single
> +          physical MAC translates to allowing traffic for the whole "VRRPv3"
> +          range of MACs. See more in the examples.
> +
> +          When the port security configuration entry contains the VRRPv3 
> label,
> +          the behavior for physical MAC (the first specified) is different.
> +          The installed flows will allow traffic from to VRRP MACs + IPs.

from/to ?

> +          The physical MAC is there to properly allow ARP/ND with given
> +          VRRP MACs.

Maybe mention here that to allow traffic that is not related to a virtual
router, e.g. IP traffic with a physical MAC as a source, a regular port
security entry should be added separately.

> +        </p>
> +
>          <p>
>            This column is provided as a convenience to cloud management 
> systems,
>            but all of the features that it implements can be implemented as 
> ACLs
> @@ -2102,6 +2117,46 @@
>              255.255.255.255, and any address in 224.0.0.0/4.  The host may 
> not
>              send or receive any IPv6 (including IPv6 Neighbor Discovery) 
> traffic.
>            </dd>
> +
> +          <dt><code>"VRRPv3 &lt;PHYSICAL_MAC&gt;"</code></dt>
> +          <dd>
> +            The host may also send ARP/ND packets with physical
> +            MAC as a source and the inner SHA/TLL/SLL all the VRRPv3
> +            MACs, 00:00:5e:00:01:{VRID}/40 for IPv4 and
> +            00:00:5E:00:02:{VRID}/40 for IPv6.
> +          </dd>
> +
> +          <dt><code>"VRRPv3 &lt;PHYSICAL_MAC&gt;
> +                    &lt;VRRPV3_MACv4_1&gt;/[&lt;MASK1&gt;]
> +                    &lt;VRRPV3_MACv4_N&gt;
> +                    &lt;VRRPV3_MACv6_1&gt;/[&lt;MASK2&gt;]
> +                    &lt;VRRPV3_MACv6_N&gt;"</code></dt>
> +          <dd>
> +            The host may also send ARP/ND packets with physical
> +            MAC as a source and the inner SHA/TLL/SLL all the VRRPv3
> +            MACs, 00:00:5e:00:01:{VRID} for IPv4 and
> +            00:00:5E:00:02:{VRID} for IPv6. The VRRPv3 MACs can be provided
> +            with a mask with prefix between /40 and /48.
> +          </dd>
> +
> +          <dt><code>"VRRPv3 &lt;PHYSICAL_MAC&gt;
> +            &lt;VRRPV3_MACv4_1&gt;/[&lt;MASK1&gt;]
> +            &lt;VRRPV3_MACv4_N&gt;
> +            &lt;VRRPV3_MACv6_1&gt;/[&lt;MASK2&gt;]
> +            &lt;VRRPV3_MACv6_N&gt;
> +            &lt;VRRPV3_IPv4_1&gt;/[&lt;MASK1&gt;]
> +            &lt;VRRPV3_IPv4_N&gt;
> +            &lt;VRRPV3_IPv6_1&gt;/[&lt;MASK2&gt;]
> +            &lt;VRRPV3_IPv6_N&gt;"</code></dt>
> +          <dd>
> +            The host may also send ARP/ND packets with physical
> +            MAC as a source and the inner SHA/TLL/SLL all the VRRPv3
> +            MACs, 00:00:5e:00:01:{VRID} for IPv4 and
> +            00:00:5E:00:02:{VRID} for IPv6. The VRRPv3 MACs can be provided
> +            with a mask with prefix between /40 and /48. The provided IP
> +            addresses further restrict the inner IP for ARP/ND. As well
> +            as traffic to/from given VRRPv3 MACs and the provided IPs.

'and the provided IPs' seems redundant here.

> +          </dd>
>          </dl>
>        </column>
>      </group>
> diff --git a/tests/ovn.at b/tests/ovn.at
> index 961b6ecf7..28f53a491 100644
> --- a/tests/ovn.at
> +++ b/tests/ovn.at
> @@ -44611,3 +44611,782 @@ check ovn-nbctl --wait=hv lsp-set-type down_ext 
> localnet
>  OVN_CLEANUP([hv1],[hv2])
>  AT_CLEANUP
>  ])
> +
> +OVN_FOR_EACH_NORTHD([
> +AT_SETUP([Port security - VRRPv3 ARP/ND])
> +AT_SKIP_IF([test $HAVE_SCAPY = no])
> +ovn_start
> +net_add n1
> +sim_add hv1
> +as hv1
> +check ovs-vsctl add-br br-phys
> +ovn_attach n1 br-phys 192.168.0.1
> +
> +check ovn-nbctl ls-add ls \
> +    -- set logical_switch ls other-config:requested-tnl-key=1
> +
> +check ovn-nbctl lsp-add ls lsp1
> +check ovn-nbctl lsp-set-addresses lsp1 "00:00:00:00:10:01 192.168.10.1 
> fd10::1" \
> +    -- set logical_switch_port lsp1 options:requested-tnl-key=1
> +
> +check ovn-nbctl lsp-add ls lsp2
> +check ovn-nbctl lsp-set-addresses lsp2 "00:00:00:00:10:02 192.168.10.2 
> fd10::2" \
> +    -- set logical_switch_port lsp2 options:requested-tnl-key=2
> +
> +check ovs-vsctl -- add-port br-int vif1 -- \
> +      set interface vif1 external-ids:iface-id=lsp1 \
> +      options:tx_pcap=hv1/vif1-tx.pcap \
> +      options:rxq_pcap=hv1/vif1-rx.pcap
> +
> +check ovs-vsctl -- add-port br-int vif2 -- \
> +      set interface vif2 external-ids:iface-id=lsp2 \
> +      options:tx_pcap=hv1/vif2-tx.pcap \
> +      options:rxq_pcap=hv1/vif2-rx.pcap
> +
> +wait_for_ports_up
> +
> +test_arp() {
> +    local dropped=$1
> +
> +    packet=$(fmt_pkt "
> +        Ether(dst='ff:ff:ff:ff:ff:ff', src='00:00:00:00:10:01') /
> +        ARP(op=1, hwsrc='00:00:5e:00:01:05', hwdst='ff:ff:ff:ff:ff:ff', 
> psrc='192.168.10.1', pdst='192.168.10.2')
> +    ")
> +    check as hv1 ovs-appctl netdev-dummy/receive vif1 $packet
> +
> +    packet=$(fmt_pkt "
> +            Ether(dst='ff:ff:ff:ff:ff:ff', src='00:00:00:00:10:01') /
> +            ARP(op=2, hwsrc='00:00:5e:00:01:05', hwdst='ff:ff:ff:ff:ff:ff', 
> psrc='192.168.10.1', pdst='192.168.10.1')

nit: Indentation still inconsistent here.  Should be 4 spaces to the left.

> +    ")
> +    check as hv1 ovs-appctl netdev-dummy/receive vif1 $packet
> +
> +    if [[ "$dropped" != "yes" ]]; then
> +        echo $packet >> vif2.expected
> +        packet=$(fmt_pkt "
> +            Ether(dst='00:00:00:00:10:01', src='00:00:00:00:10:02') /
> +            ARP(op=2, hwsrc='00:00:00:00:10:02', hwdst='00:00:5e:00:01:05', 
> psrc='192.168.10.2', pdst='192.168.10.1')
> +        ")
> +        echo $packet >> vif1.expected
> +    fi
> +}
> +test_nd() {
> +    local dropped=$1
> +
> +    packet=$(fmt_pkt "
> +        Ether(dst='33:33:ff:00:00:01', src='00:00:00:00:10:01') /
> +        IPv6(src='fd10::1', dst='ff02::1:ff00:2') /
> +        ICMPv6ND_NS(tgt='fd10::2') /
> +        ICMPv6NDOptSrcLLAddr(lladdr='00:00:5e:00:02:05')
> +    ")
> +    check as hv1 ovs-appctl netdev-dummy/receive vif1 $packet
> +
> +    packet=$(fmt_pkt "
> +        Ether(dst='33:33:ff:00:00:01', src='00:00:00:00:10:01') /
> +        IPv6(src='fd10::1', dst='ff02::1:ff00:1') /
> +        ICMPv6ND_NA(tgt='fd10::1') /
> +        ICMPv6NDOptDstLLAddr(lladdr='00:00:5e:00:02:05')
> +    ")
> +    check as hv1 ovs-appctl netdev-dummy/receive vif1 $packet
> +
> +    if [[ "$dropped" != "yes" ]]; then
> +        echo $packet >> vif2.expected
> +
> +        packet=$(fmt_pkt "
> +                Ether(dst='00:00:00:00:10:01', src='00:00:00:00:10:02') /
> +                IPv6(src='fd10::2', dst='fd10::1') /
> +                ICMPv6ND_NA(tgt='fd10::2', R=0, S=1, O=1) /
> +                ICMPv6NDOptDstLLAddr(lladdr='00:00:00:00:10:02')
> +        ")
> +        echo $packet >> vif1.expected
> +    fi
> +}
> +
> +reset_pcap_and_expected() {
> +    reset_pcap_file vif1 hv1/vif1
> +    reset_pcap_file vif2 hv1/vif2
> +
> +    : > vif1.expected
> +    : > vif2.expected
> +}
> +
> +AS_BOX([Without port security])
> +reset_pcap_and_expected
> +
> +test_arp no
> +test_nd no
> +
> +OVN_CHECK_PACKETS([hv1/vif1-tx.pcap], [vif1.expected])
> +OVN_CHECK_PACKETS([hv1/vif2-tx.pcap], [vif2.expected])
> +
> +AT_CHECK([ovs-ofctl dump-flows br-int table=OFTABLE_CHK_IN_PORT_SEC | 
> ofctl_strip_all | sort | grep -v NXST_FLOW], [1])
> +AT_CHECK([ovs-ofctl dump-flows br-int table=OFTABLE_CHK_IN_PORT_SEC_ND | 
> ofctl_strip_all | sort | grep -v NXST_FLOW], [1])
> +AT_CHECK([ovs-ofctl dump-flows br-int table=OFTABLE_CHK_OUT_PORT_SEC | 
> ofctl_strip_all | sort | grep -v NXST_FLOW], [1])
> +
> +AS_BOX([With MAC only port security])
> +reset_pcap_and_expected
> +check ovn-nbctl --wait=hv lsp-set-port-security lsp1 "00:00:00:00:10:01"
> +
> +test_arp yes
> +test_nd yes
> +
> +OVN_CHECK_PACKETS([hv1/vif1-tx.pcap], [vif1.expected])
> +OVN_CHECK_PACKETS([hv1/vif2-tx.pcap], [vif2.expected])
> +
> +AT_CHECK([ovs-ofctl dump-flows br-int table=OFTABLE_CHK_IN_PORT_SEC | 
> ofctl_strip_all | sort | grep -v NXST_FLOW], [0], [dnl
> + table=OFTABLE_CHK_IN_PORT_SEC, priority=80,reg14=0x1,metadata=0x1 
> actions=load:0x1->NXM_NX_REG10[[12]]
> + table=OFTABLE_CHK_IN_PORT_SEC, 
> priority=90,reg14=0x1,metadata=0x1,dl_src=00:00:00:00:10:01 
> actions=resubmit(,OFTABLE_CHK_IN_PORT_SEC_ND)
> + table=OFTABLE_CHK_IN_PORT_SEC, priority=95,arp,reg14=0x1,metadata=0x1 
> actions=resubmit(,OFTABLE_CHK_IN_PORT_SEC_ND)
> +])
> +AT_CHECK([ovs-ofctl dump-flows br-int table=OFTABLE_CHK_IN_PORT_SEC_ND | 
> ofctl_strip_all | sort | grep -v NXST_FLOW], [0], [dnl
> + table=OFTABLE_CHK_IN_PORT_SEC_ND, priority=80,arp,reg14=0x1,metadata=0x1 
> actions=load:0x1->NXM_NX_REG10[[12]]
> + table=OFTABLE_CHK_IN_PORT_SEC_ND, 
> priority=80,icmp6,reg14=0x1,metadata=0x1,nw_ttl=255,icmp_type=135 
> actions=load:0x1->NXM_NX_REG10[[12]]
> + table=OFTABLE_CHK_IN_PORT_SEC_ND, 
> priority=80,icmp6,reg14=0x1,metadata=0x1,nw_ttl=255,icmp_type=136 
> actions=load:0x1->NXM_NX_REG10[[12]]
> + table=OFTABLE_CHK_IN_PORT_SEC_ND, 
> priority=90,arp,reg14=0x1,metadata=0x1,dl_src=00:00:00:00:10:01,arp_sha=00:00:00:00:10:01
>  actions=load:0->NXM_NX_REG10[[12]]
> + table=OFTABLE_CHK_IN_PORT_SEC_ND, 
> priority=90,icmp6,reg14=0x1,metadata=0x1,dl_src=00:00:00:00:10:01,nw_ttl=255,icmp_type=135,icmp_code=0,nd_sll=00:00:00:00:00:00
>  actions=load:0->NXM_NX_REG10[[12]]
> + table=OFTABLE_CHK_IN_PORT_SEC_ND, 
> priority=90,icmp6,reg14=0x1,metadata=0x1,dl_src=00:00:00:00:10:01,nw_ttl=255,icmp_type=135,icmp_code=0,nd_sll=00:00:00:00:10:01
>  actions=load:0->NXM_NX_REG10[[12]]
> + table=OFTABLE_CHK_IN_PORT_SEC_ND, 
> priority=90,icmp6,reg14=0x1,metadata=0x1,dl_src=00:00:00:00:10:01,nw_ttl=255,icmp_type=136,icmp_code=0,nd_tll=00:00:00:00:00:00
>  actions=load:0->NXM_NX_REG10[[12]]
> + table=OFTABLE_CHK_IN_PORT_SEC_ND, 
> priority=90,icmp6,reg14=0x1,metadata=0x1,dl_src=00:00:00:00:10:01,nw_ttl=255,icmp_type=136,icmp_code=0,nd_tll=00:00:00:00:10:01
>  actions=load:0->NXM_NX_REG10[[12]]
> +])
> +AT_CHECK([ovs-ofctl dump-flows br-int table=OFTABLE_CHK_OUT_PORT_SEC | 
> ofctl_strip_all | sort | grep -v NXST_FLOW], [0], [dnl
> + table=OFTABLE_CHK_OUT_PORT_SEC, priority=80,reg15=0x1,metadata=0x1 
> actions=load:0x1->NXM_NX_REG10[[12]]
> + table=OFTABLE_CHK_OUT_PORT_SEC, 
> priority=85,reg15=0x1,metadata=0x1,dl_dst=00:00:00:00:10:01 
> actions=load:0->NXM_NX_REG10[[12]]
> +])
> +
> +AS_BOX([With MAC + IP port security])
> +reset_pcap_and_expected
> +check ovn-nbctl --wait=hv lsp-set-port-security lsp1 "00:00:00:00:10:01 
> 192.168.10.1 fd10::1"
> +
> +test_arp yes
> +test_nd yes
> +
> +OVN_CHECK_PACKETS([hv1/vif1-tx.pcap], [vif1.expected])
> +OVN_CHECK_PACKETS([hv1/vif2-tx.pcap], [vif2.expected])
> +
> +AT_CHECK([ovs-ofctl dump-flows br-int table=OFTABLE_CHK_IN_PORT_SEC | 
> ofctl_strip_all | sort | grep -v NXST_FLOW], [0], [dnl
> + table=OFTABLE_CHK_IN_PORT_SEC, priority=80,reg14=0x1,metadata=0x1 
> actions=load:0x1->NXM_NX_REG10[[12]]
> + table=OFTABLE_CHK_IN_PORT_SEC, 
> priority=90,icmp6,reg14=0x1,metadata=0x1,dl_src=00:00:00:00:10:01,ipv6_src=::,ipv6_dst=ff02::/16,icmp_type=131,icmp_code=0
>  actions=load:0->NXM_NX_REG10[[12]]
> + table=OFTABLE_CHK_IN_PORT_SEC, 
> priority=90,icmp6,reg14=0x1,metadata=0x1,dl_src=00:00:00:00:10:01,ipv6_src=::,ipv6_dst=ff02::/16,icmp_type=135,icmp_code=0
>  actions=resubmit(,OFTABLE_CHK_IN_PORT_SEC_ND)
> + table=OFTABLE_CHK_IN_PORT_SEC, 
> priority=90,icmp6,reg14=0x1,metadata=0x1,dl_src=00:00:00:00:10:01,ipv6_src=::,ipv6_dst=ff02::/16,icmp_type=143,icmp_code=0
>  actions=load:0->NXM_NX_REG10[[12]]
> + table=OFTABLE_CHK_IN_PORT_SEC, 
> priority=90,ip,reg14=0x1,metadata=0x1,dl_src=00:00:00:00:10:01,nw_src=192.168.10.1
>  actions=load:0->NXM_NX_REG10[[12]]
> + table=OFTABLE_CHK_IN_PORT_SEC, 
> priority=90,ipv6,reg14=0x1,metadata=0x1,dl_src=00:00:00:00:10:01,ipv6_src=fd10::1
>  actions=resubmit(,OFTABLE_CHK_IN_PORT_SEC_ND)
> + table=OFTABLE_CHK_IN_PORT_SEC, 
> priority=90,ipv6,reg14=0x1,metadata=0x1,dl_src=00:00:00:00:10:01,ipv6_src=fe80::200:ff:fe00:1001
>  actions=resubmit(,OFTABLE_CHK_IN_PORT_SEC_ND)
> + table=OFTABLE_CHK_IN_PORT_SEC, 
> priority=90,udp,reg14=0x1,metadata=0x1,dl_src=00:00:00:00:10:01,nw_src=0.0.0.0,nw_dst=255.255.255.255,tp_src=68,tp_dst=67
>  actions=load:0->NXM_NX_REG10[[12]]
> + table=OFTABLE_CHK_IN_PORT_SEC, priority=95,arp,reg14=0x1,metadata=0x1 
> actions=resubmit(,OFTABLE_CHK_IN_PORT_SEC_ND)
> +])
> +AT_CHECK([ovs-ofctl dump-flows br-int table=OFTABLE_CHK_IN_PORT_SEC_ND | 
> ofctl_strip_all | sort | grep -v NXST_FLOW], [0], [dnl
> + table=OFTABLE_CHK_IN_PORT_SEC_ND, priority=80,arp,reg14=0x1,metadata=0x1 
> actions=load:0x1->NXM_NX_REG10[[12]]
> + table=OFTABLE_CHK_IN_PORT_SEC_ND, 
> priority=80,icmp6,reg14=0x1,metadata=0x1,nw_ttl=255,icmp_type=135 
> actions=load:0x1->NXM_NX_REG10[[12]]
> + table=OFTABLE_CHK_IN_PORT_SEC_ND, 
> priority=80,icmp6,reg14=0x1,metadata=0x1,nw_ttl=255,icmp_type=136 
> actions=load:0x1->NXM_NX_REG10[[12]]
> + table=OFTABLE_CHK_IN_PORT_SEC_ND, 
> priority=90,arp,reg14=0x1,metadata=0x1,dl_src=00:00:00:00:10:01,arp_spa=192.168.10.1,arp_sha=00:00:00:00:10:01
>  actions=load:0->NXM_NX_REG10[[12]]
> + table=OFTABLE_CHK_IN_PORT_SEC_ND, 
> priority=90,icmp6,reg14=0x1,metadata=0x1,dl_src=00:00:00:00:10:01,nw_ttl=255,icmp_type=135,icmp_code=0,nd_sll=00:00:00:00:00:00
>  actions=load:0->NXM_NX_REG10[[12]]
> + table=OFTABLE_CHK_IN_PORT_SEC_ND, 
> priority=90,icmp6,reg14=0x1,metadata=0x1,dl_src=00:00:00:00:10:01,nw_ttl=255,icmp_type=135,icmp_code=0,nd_sll=00:00:00:00:10:01
>  actions=load:0->NXM_NX_REG10[[12]]
> + table=OFTABLE_CHK_IN_PORT_SEC_ND, 
> priority=90,icmp6,reg14=0x1,metadata=0x1,dl_src=00:00:00:00:10:01,nw_ttl=255,icmp_type=136,icmp_code=0,nd_target=fd10::1,nd_tll=00:00:00:00:00:00
>  actions=load:0->NXM_NX_REG10[[12]]
> + table=OFTABLE_CHK_IN_PORT_SEC_ND, 
> priority=90,icmp6,reg14=0x1,metadata=0x1,dl_src=00:00:00:00:10:01,nw_ttl=255,icmp_type=136,icmp_code=0,nd_target=fd10::1,nd_tll=00:00:00:00:10:01
>  actions=load:0->NXM_NX_REG10[[12]]
> + table=OFTABLE_CHK_IN_PORT_SEC_ND, 
> priority=90,icmp6,reg14=0x1,metadata=0x1,dl_src=00:00:00:00:10:01,nw_ttl=255,icmp_type=136,icmp_code=0,nd_target=fe80::200:ff:fe00:1001,nd_tll=00:00:00:00:00:00
>  actions=load:0->NXM_NX_REG10[[12]]
> + table=OFTABLE_CHK_IN_PORT_SEC_ND, 
> priority=90,icmp6,reg14=0x1,metadata=0x1,dl_src=00:00:00:00:10:01,nw_ttl=255,icmp_type=136,icmp_code=0,nd_target=fe80::200:ff:fe00:1001,nd_tll=00:00:00:00:10:01
>  actions=load:0->NXM_NX_REG10[[12]]
> +])
> +AT_CHECK([ovs-ofctl dump-flows br-int table=OFTABLE_CHK_OUT_PORT_SEC | 
> ofctl_strip_all | sort | grep -v NXST_FLOW], [0], [dnl
> + table=OFTABLE_CHK_OUT_PORT_SEC, priority=80,reg15=0x1,metadata=0x1 
> actions=load:0x1->NXM_NX_REG10[[12]]
> + table=OFTABLE_CHK_OUT_PORT_SEC, 
> priority=85,reg15=0x1,metadata=0x1,dl_dst=00:00:00:00:10:01 
> actions=load:0->NXM_NX_REG10[[12]]
> + table=OFTABLE_CHK_OUT_PORT_SEC, 
> priority=90,ip,reg15=0x1,metadata=0x1,dl_dst=00:00:00:00:10:01 
> actions=load:0x1->NXM_NX_REG10[[12]]
> + table=OFTABLE_CHK_OUT_PORT_SEC, 
> priority=90,ipv6,reg15=0x1,metadata=0x1,dl_dst=00:00:00:00:10:01 
> actions=load:0x1->NXM_NX_REG10[[12]]
> + table=OFTABLE_CHK_OUT_PORT_SEC, 
> priority=95,ip,reg15=0x1,metadata=0x1,dl_dst=00:00:00:00:10:01,nw_dst=192.168.10.1
>  actions=load:0->NXM_NX_REG10[[12]]
> + table=OFTABLE_CHK_OUT_PORT_SEC, 
> priority=95,ip,reg15=0x1,metadata=0x1,dl_dst=00:00:00:00:10:01,nw_dst=224.0.0.0/4
>  actions=load:0->NXM_NX_REG10[[12]]
> + table=OFTABLE_CHK_OUT_PORT_SEC, 
> priority=95,ip,reg15=0x1,metadata=0x1,dl_dst=00:00:00:00:10:01,nw_dst=255.255.255.255
>  actions=load:0->NXM_NX_REG10[[12]]
> + table=OFTABLE_CHK_OUT_PORT_SEC, 
> priority=95,ipv6,reg15=0x1,metadata=0x1,dl_dst=00:00:00:00:10:01,ipv6_dst=fd10::1
>  actions=load:0->NXM_NX_REG10[[12]]
> + table=OFTABLE_CHK_OUT_PORT_SEC, 
> priority=95,ipv6,reg15=0x1,metadata=0x1,dl_dst=00:00:00:00:10:01,ipv6_dst=fe80::200:ff:fe00:1001
>  actions=load:0->NXM_NX_REG10[[12]]
> + table=OFTABLE_CHK_OUT_PORT_SEC, 
> priority=95,ipv6,reg15=0x1,metadata=0x1,dl_dst=00:00:00:00:10:01,ipv6_dst=ff00::/8
>  actions=load:0->NXM_NX_REG10[[12]]
> +])
> +
> +
> +AS_BOX([With MAC only port security, VRRPv3=any])
> +reset_pcap_and_expected
> +check ovn-nbctl --wait=hv lsp-set-port-security lsp1 "00:00:00:00:10:01" 
> "VRRPv3 00:00:00:00:10:01"

It's good that we have all these combined tests, as that's how the feature
will most likely be used, but we now lost all the coverage for VRRP-only
cases.  We need at least a few, otherwise we're not checking from which
of these entries the flows are coming from.

Best regards, Ilya Maximets.
_______________________________________________
dev mailing list
[email protected]
https://mail.openvswitch.org/mailman/listinfo/ovs-dev

Reply via email to