Hi Rosemarie,

On 3/11/25 10:29 PM, Rosemarie O'Riorden wrote:
> When a gateway router has a load balancer configured, the option
> lb_force_snat_ip=router_ip can be set so that OVN SNATs load balanced
> packets to the logical router's egress interface's IP address, that is, the
> port chosen as "outport" in the lr_in_ip_routing stage.
> 
> However, this was only designed to work when one network was configured
> on the logical router outport. When multiple networks were configured,
> OVN's behavior was to simply choose the lexicographically first IP address for
> SNAT. This often lead to an incorrect address being used for SNAT.
> 
> To fix this, two main components have been added:
>  1. A new flag, flags.network_id. It is 4 bits and stores an index.
>  2. A new stage in the router ingress pipeline, lr_in_network_id.

The discussion we had with Aleksandra and Ilya here reminded me that we
should increment OVN_INTERNAL_MINOR_VER every time we add new logical
stages:

https://mail.openvswitch.org/pipermail/ovs-dev/2025-March/421992.html

> 
> Now in the stage lr_in_network_id, OVN generates flows that assign
> flags.network_id with an index, which is chosen by looping through the 
> networks
> on the port, and assigning flags.network_id = i. flags.network_id is then
> matched on later in lr_out_snat and the network at that index will be chosen
> for SNAT.
> 
> However, if there are more than 16 networks, flags.network_id will be 0
> for networks 17 and up. Then, for those networks, the first
> lexicographical network will be chosen for SNAT.
> 
> There is also a lower priority flow with the old behavior to make upgrades
> smooth.
> 
> Two tests have been added to verify that:
>  1. The correct network is chosen for SNAT.
>  2. The new and updated flows with flags.network_id are correct.
> 
> And tests that were broken by this new behavior have been updated.
> 
> Reported-at: https://issues.redhat.com/browse/FDP-871
> Reported-at: 
> https://mail.openvswitch.org/pipermail/ovs-dev/2024-October/417717.html
> Signed-off-by: Rosemarie O'Riorden <rosema...@redhat.com>
> ---
> v4:
> - Change "op->lrp_networks.ipv4_addrs" to "op->lrp_networks.ipv6_addrs" in
>   error message for IPv6
> - Add flow with old behavior only if the router port has at least one IPv4
>   address, or 2 IPv6 addresses (2 because LLA is always there).
> - Add ovn-northd.at test to cover case when LRP has just a single IPv6 address
>   (LLA) and no IPv4. Make sure priority-105 flow is not added.
> - Fixed tests where flows were affected by changes
> v3:
> - Fix "16 bits" typo, flags.network_id 4 bits (fits 16 networks)
> - Replace inconsistent comments (networks "17 and up" get network_id=0)
> - Define OVN_MAX_NETWORK_ID macro, use instead of hardcoding 15 or 16
> - Use OVN_MAX_NETWORK_ID while defining MLF_NETWORK_ID
> - Fix incorrect expression resulting in wrong hex value
> - Remove extra quotations around port name, resulting in double quotes and 
> thus
>   invalid flows
> - Make priority-100 flow (with old behavior) 105 instead
> - Remove "flags.force_snat_for_lb == 1" from match in new stage, set 
> network_id
>   for all IP packets instead
> - Change function name for new stage since it's no longer restricted to "force
>   snat for lb" case
> - Update affected test flows
> - Fix comment that was too long, improve wording
> - Replace "routerip" with "router_ip"
> v2:
> - Improve wording of commit message and add missing explanation
> - Add Reported-at tag
> - Correct hex value for MLF_NETWORK_ID in include/ovn/logical-fields.h
> - In northd/northd.c:
>   - Change behavior to set flags.network_id=0 for networks 16 and up
>   - Add flow with old behavior for consistency during upgrades
>   - Fix redundant loop conditions
>   - Remove code I accidentally included in v1
>   - Fix indentation in multiple spots
>   - Remove incorrect changes in build_lrouter_nat_defrag_and_lb()
> - Fix documentation, add new stage
> - Move end-to-end test from ovn-northd.at to ovn.at
> - Add check before all ovn and ovs commands in test
> - Remove unnecessary section included in test
> - Updated all flows in tests that were affected by new changes
> - Added more IP addresses to test to test behavior with >16 networks
> ---
>  include/ovn/logical-fields.h |  12 ++-
>  lib/logical-fields.c         |   3 +
>  northd/northd.c              | 184 +++++++++++++++++++++++++++++------
>  northd/northd.h              |   3 +-
>  northd/ovn-northd.8.xml      |  59 +++++++++--
>  tests/ovn-northd.at          | 146 ++++++++++++++++++++++++---
>  tests/ovn.at                 |  71 ++++++++++++++
>  7 files changed, 424 insertions(+), 54 deletions(-)
> 
> diff --git a/include/ovn/logical-fields.h b/include/ovn/logical-fields.h
> index 196ac9dd8..b9446a042 100644
> --- a/include/ovn/logical-fields.h
> +++ b/include/ovn/logical-fields.h
> @@ -71,6 +71,10 @@ enum ovn_controller_event {
>  #define MFF_LOG_LB_AFF_MATCH_PORT_OLD        MFF_REG8
>  #define MFF_LOG_LB_AFF_MATCH_LS_IP6_ADDR_OLD MFF_XXREG0
>  
> +/* Maximum number of networks supported by 4-bit flags.network_id. */
> +#define OVN_MAX_NETWORK_ID \
> +    ((1 << (MLF_NETWORK_ID_END_BIT - MLF_NETWORK_ID_START_BIT + 1)) - 1)
> +
>  void ovn_init_symtab(struct shash *symtab);
>  
>  /* MFF_LOG_FLAGS_REG bit assignments */
> @@ -97,6 +101,8 @@ enum mff_log_flags_bits {
>      MLF_FROM_CTRL_BIT = 19,
>      MLF_UNSNAT_NEW_BIT = 20,
>      MLF_UNSNAT_NOT_TRACKED_BIT = 21,
> +    MLF_NETWORK_ID_START_BIT = 28,
> +    MLF_NETWORK_ID_END_BIT = 31,
>  };
>  
>  /* MFF_LOG_FLAGS_REG flag assignments */
> @@ -159,7 +165,11 @@ enum mff_log_flags {
>      MLF_UNSNAT_NEW = (1 << MLF_UNSNAT_NEW_BIT),
>  
>      /* Indicate that the packet didn't go through unSNAT. */
> -    MLF_UNSNAT_NOT_TRACKED = (1 << MLF_UNSNAT_NOT_TRACKED_BIT)
> +    MLF_UNSNAT_NOT_TRACKED = (1 << MLF_UNSNAT_NOT_TRACKED_BIT),
> +
> +    /* Assign network ID to packet to choose correct network for snat when
> +     * lb_force_snat_ip=router_ip. */
> +    MLF_NETWORK_ID = (OVN_MAX_NETWORK_ID << MLF_NETWORK_ID_START_BIT),
>  };
>  
>  /* OVN logical fields
> diff --git a/lib/logical-fields.c b/lib/logical-fields.c
> index ed287f42b..db2d08ada 100644
> --- a/lib/logical-fields.c
> +++ b/lib/logical-fields.c
> @@ -147,6 +147,9 @@ ovn_init_symtab(struct shash *symtab)
>               MLF_UNSNAT_NOT_TRACKED_BIT);
>      expr_symtab_add_subfield(symtab, "flags.unsnat_not_tracked", NULL,
>                               flags_str);
> +    snprintf(flags_str, sizeof flags_str, "flags[%d..%d]",
> +             MLF_NETWORK_ID_START_BIT, MLF_NETWORK_ID_END_BIT);
> +    expr_symtab_add_subfield(symtab, "flags.network_id", NULL, flags_str);
>  
>      snprintf(flags_str, sizeof flags_str, "flags[%d]", MLF_FROM_CTRL_BIT);
>      expr_symtab_add_subfield(symtab, "flags.from_ctrl", NULL, flags_str);
> diff --git a/northd/northd.c b/northd/northd.c
> index 1d3e132d4..c94f83e32 100644
> --- a/northd/northd.c
> +++ b/northd/northd.c
> @@ -12985,73 +12985,118 @@ build_lrouter_force_snat_flows_op(struct ovn_port 
> *op,
>                                    struct ds *match, struct ds *actions,
>                                    struct lflow_ref *lflow_ref)
>  {
> +    size_t network_id;
>      ovs_assert(op->nbrp);
>      if (!op->peer || !lrnat_rec->lb_force_snat_router_ip) {
>          return;
>      }
>  
> -    if (op->lrp_networks.n_ipv4_addrs) {
> +    for (size_t i = 0; i < op->lrp_networks.n_ipv4_addrs; i++) {
>          ds_clear(match);
>          ds_clear(actions);
>  
>          ds_put_format(match, "inport == %s && ip4.dst == %s",
> -                      op->json_key, op->lrp_networks.ipv4_addrs[0].addr_s);
> +                      op->json_key, op->lrp_networks.ipv4_addrs[i].addr_s);
>          ovn_lflow_add(lflows, op->od, S_ROUTER_IN_UNSNAT, 110,
>                        ds_cstr(match), "ct_snat;", lflow_ref);
>  
>          ds_clear(match);
>  
> +        /* Since flags.network_id is 4 bits, assign flags.network_id = 0 for
> +         * networks 17 and up. */
> +        if (i > OVN_MAX_NETWORK_ID) {
> +            network_id = 0;
> +            static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
> +            VLOG_WARN_RL(&rl, "Logical router port %s already has the max of 
> "
> +                              "%d networks configured, so for network "
> +                              "\"%s/%d\" the first IP [%s] will be 
> considered "
> +                              "as SNAT for load balancer.", op->json_key,
> +                              OVN_MAX_NETWORK_ID + 1,
> +                              op->lrp_networks.ipv4_addrs[i].addr_s,
> +                              op->lrp_networks.ipv4_addrs[i].plen,
> +                              op->lrp_networks.ipv4_addrs[0].addr_s);
> +        } else {
> +            network_id = i;
> +        }
> +
>          /* Higher priority rules to force SNAT with the router port ip.
>           * This only takes effect when the packet has already been
>           * load balanced once. */
> -        ds_put_format(match, "flags.force_snat_for_lb == 1 && ip4 && "
> -                      "outport == %s", op->json_key);
> +        ds_put_format(match, "flags.force_snat_for_lb == 1 && "
> +                      "flags.network_id == %"PRIuSIZE" && ip4 && "
> +                      "outport == %s", network_id, op->json_key);
>          ds_put_format(actions, "ct_snat(%s);",
> -                      op->lrp_networks.ipv4_addrs[0].addr_s);
> +                      op->lrp_networks.ipv4_addrs[network_id].addr_s);
>          ovn_lflow_add(lflows, op->od, S_ROUTER_OUT_SNAT, 110,
>                        ds_cstr(match), ds_cstr(actions),
>                        lflow_ref);
> -        if (op->lrp_networks.n_ipv4_addrs > 1) {
> -            static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
> -            VLOG_WARN_RL(&rl, "Logical router port %s is configured with "
> -                              "multiple IPv4 addresses.  Only the first "
> -                              "IP [%s] is considered as SNAT for load "
> -                              "balancer", op->json_key,
> -                              op->lrp_networks.ipv4_addrs[0].addr_s);
> -        }
>      }
>  
>      /* op->lrp_networks.ipv6_addrs will always have LLA and that will be
> -     * last in the list. So add the flows only if n_ipv6_addrs > 1. */
> -    if (op->lrp_networks.n_ipv6_addrs > 1) {
> +     * last in the list. So loop to add flows n_ipv6_addrs - 1 times. */
> +    for (size_t i = 0; i < op->lrp_networks.n_ipv6_addrs - 1; i++) {
>          ds_clear(match);
>          ds_clear(actions);
>  
>          ds_put_format(match, "inport == %s && ip6.dst == %s",
> -                      op->json_key, op->lrp_networks.ipv6_addrs[0].addr_s);
> +                      op->json_key, op->lrp_networks.ipv6_addrs[i].addr_s);
>          ovn_lflow_add(lflows, op->od, S_ROUTER_IN_UNSNAT, 110,
>                        ds_cstr(match), "ct_snat;", lflow_ref);
> -
>          ds_clear(match);
>  
> +        /* Since flags.network_id is 4 bits, assign flags.network_id = 0 for
> +         * networks 17 and up. */
> +        if (i > OVN_MAX_NETWORK_ID) {
> +            network_id = 0;
> +            static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
> +            VLOG_WARN_RL(&rl, "Logical router port %s already has the max of 
> "
> +                              "%d networks configured, so for network "
> +                              "\"%s/%d\" the first IP [%s] will be 
> considered "
> +                              "as SNAT for load balancer.", op->json_key,
> +                              OVN_MAX_NETWORK_ID + 1,
> +                              op->lrp_networks.ipv6_addrs[i].addr_s,
> +                              op->lrp_networks.ipv6_addrs[i].plen,
> +                              op->lrp_networks.ipv6_addrs[0].addr_s);
> +        } else {
> +            network_id = i;
> +        }
> +
>          /* Higher priority rules to force SNAT with the router port ip.
>           * This only takes effect when the packet has already been
>           * load balanced once. */
> -        ds_put_format(match, "flags.force_snat_for_lb == 1 && ip6 && "
> -                      "outport == %s", op->json_key);
> +        ds_put_format(match, "flags.force_snat_for_lb == 1 && "
> +                      "flags.network_id == %"PRIuSIZE" && ip6 && "
> +                      "outport == %s", network_id, op->json_key);
>          ds_put_format(actions, "ct_snat(%s);",
> -                      op->lrp_networks.ipv6_addrs[0].addr_s);
> +                      op->lrp_networks.ipv6_addrs[network_id].addr_s);
>          ovn_lflow_add(lflows, op->od, S_ROUTER_OUT_SNAT, 110,
>                        ds_cstr(match), ds_cstr(actions),
>                        lflow_ref);
> -        if (op->lrp_networks.n_ipv6_addrs > 2) {
> -            static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
> -            VLOG_WARN_RL(&rl, "Logical router port %s is configured with "
> -                              "multiple IPv6 addresses.  Only the first "
> -                              "IP [%s] is considered as SNAT for load "
> -                              "balancer", op->json_key,
> -                              op->lrp_networks.ipv6_addrs[0].addr_s);
> -        }
> +    }
> +
> +    /* This lower-priority flow matches the old behavior for if northd is
> +     * upgraded before controller and flags.network_id is not recognized. */
> +    if (op->lrp_networks.n_ipv4_addrs > 0) {
> +        ds_clear(match);
> +        ds_clear(actions);
> +        ds_put_format(match, "flags.force_snat_for_lb == 1 && ip4 && "
> +                      "outport == %s", op->json_key);
> +        ds_put_format(actions, "ct_snat(%s);",
> +                      op->lrp_networks.ipv4_addrs[0].addr_s);
> +        ovn_lflow_add(lflows, op->od, S_ROUTER_OUT_SNAT, 105,
> +                      ds_cstr(match), ds_cstr(actions), lflow_ref);
> +    }

Nit: I'd add an empty line here.

> +    /* op->lrp_networks.ipv6_addrs will always have LLA, so only add flow if
> +     * there is more than 1. */
> +    if (op->lrp_networks.n_ipv6_addrs > 1) {
> +        ds_clear(match);
> +        ds_clear(actions);
> +        ds_put_format(match, "flags.force_snat_for_lb == 1 && ip6 && "
> +                      "outport == %s", op->json_key);
> +        ds_put_format(actions, "ct_snat(%s);",
> +                      op->lrp_networks.ipv6_addrs[0].addr_s);
> +        ovn_lflow_add(lflows, op->od, S_ROUTER_OUT_SNAT, 105,
> +                      ds_cstr(match), ds_cstr(actions), lflow_ref);
>      }
>  }
>  
> @@ -14930,6 +14975,87 @@ build_arp_request_flows_for_lrouter(
>                    lflow_ref);
>  }
>  
> +static void
> +build_lrouter_network_id_flows(
> +            struct ovn_datapath *od, struct lflow_table *lflows,
> +            struct ds *match, struct ds *actions, struct lflow_ref 
> *lflow_ref)

Nit: most other functions around this one that have multi-line argument
lists use 8 spaces as indent.

> +{
> +    const struct ovn_port *op;
> +    size_t network_id;
> +    HMAP_FOR_EACH (op, dp_node, &od->ports) {
> +        for (size_t i = 0; i < op->lrp_networks.n_ipv4_addrs; i++) {
> +            /* Since flags.network_id is 4 bits and can store 16 network IDs,
> +             * assign a value of 0 for networks 17 and up. */
> +            if (i > OVN_MAX_NETWORK_ID) {
> +                network_id = 0;
> +                static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 
> 5);
> +                VLOG_WARN_RL(&rl, "Logical router port %s already has the 
> max "
> +                                  "of %d networks configured, so network "
> +                                  "\"%s/%d\" is assigned "
> +                                  "flags.network_id = 0.", op->json_key,
> +                                  OVN_MAX_NETWORK_ID + 1,
> +                                  op->lrp_networks.ipv4_addrs[i].addr_s,
> +                                  op->lrp_networks.ipv4_addrs[i].plen);
> +            } else {
> +                network_id = i;
> +            }
> +
> +            ds_clear(match);
> +            ds_clear(actions);
> +
> +            ds_put_format(match, "outport == %s && "
> +                          REG_NEXT_HOP_IPV4 " == %s/%d", op->json_key,
> +                          op->lrp_networks.ipv4_addrs[i].addr_s,
> +                          op->lrp_networks.ipv4_addrs[i].plen);
> +
> +            ds_put_format(actions, "flags.network_id = %"PRIuSIZE"; ",
> +                          network_id);
> +            ds_put_format(actions, "next;");
> +
> +            ovn_lflow_add(lflows, op->od, S_ROUTER_IN_NETWORK_ID, 110,
> +                          ds_cstr(match), ds_cstr(actions),
> +                          lflow_ref);
> +        }
> +
> +        /* op->lrp_networks.ipv6_addrs will always have LLA and that will be
> +         * last in the list. So add the flows only if n_ipv6_addrs > 1, and
> +         * loop n_ipv6_addrs - 1 times. */
> +        for (size_t i = 0; i < op->lrp_networks.n_ipv6_addrs - 1; i++) {
> +            /* Since flags.network_id is 4 bits and can store 16 network IDs,
> +             * assign a value of 0 for networks 17 and up. */
> +            if (i > OVN_MAX_NETWORK_ID) {
> +                network_id = 0;
> +                static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 
> 5);
> +                VLOG_WARN_RL(&rl, "Logical router port %s already has the 
> max "
> +                                  "of %d networks configured, so network "
> +                                  "\"%s/%d\" is assigned "
> +                                  "flags.network_id = 0.", op->json_key,
> +                                  OVN_MAX_NETWORK_ID + 1,
> +                                  op->lrp_networks.ipv6_addrs[i].addr_s,
> +                                  op->lrp_networks.ipv6_addrs[i].plen);
> +            } else {
> +                network_id = i;
> +            }
> +
> +            ds_clear(match);
> +            ds_clear(actions);
> +
> +            ds_put_format(match, "outport == %s && "
> +                          REG_NEXT_HOP_IPV6 " == %s/%d", op->json_key,
> +                          op->lrp_networks.ipv6_addrs[i].addr_s,
> +                          op->lrp_networks.ipv6_addrs[i].plen);
> +
> +            ds_put_format(actions, "flags.network_id = %"PRIuSIZE"; ", i);
> +            ds_put_format(actions, "next;");
> +
> +            ovn_lflow_add(lflows, op->od, S_ROUTER_IN_NETWORK_ID, 110,
> +                          ds_cstr(match), ds_cstr(actions), lflow_ref);
> +        }
> +    }
> +    ovn_lflow_add(lflows, od, S_ROUTER_IN_NETWORK_ID, 0,
> +                  "1", "next;", lflow_ref);
> +}
> +
>  /* Logical router egress table DELIVERY: Delivery (priority 100-110).
>   *
>   * Priority 100 rules deliver packets to enabled logical ports.
> @@ -17328,6 +17454,8 @@ build_lswitch_and_lrouter_iterate_by_lr(struct 
> ovn_datapath *od,
>                                          &lsi->actions,
>                                          lsi->meter_groups,
>                                          NULL);
> +    build_lrouter_network_id_flows(od, lsi->lflows, &lsi->match,
> +                                         &lsi->actions, NULL);

Nit: indentation.

>      build_misc_local_traffic_drop_flows_for_lrouter(od, lsi->lflows, NULL);
>  
>      build_lr_nat_defrag_and_lb_default_flows(od, lsi->lflows, NULL);
> diff --git a/northd/northd.h b/northd/northd.h
> index 1a7afe902..388bac6df 100644
> --- a/northd/northd.h
> +++ b/northd/northd.h
> @@ -547,7 +547,8 @@ enum ovn_stage {
>      PIPELINE_STAGE(ROUTER, IN,  CHK_PKT_LEN,     22, "lr_in_chk_pkt_len")    
>  \
>      PIPELINE_STAGE(ROUTER, IN,  LARGER_PKTS,     23, "lr_in_larger_pkts")    
>  \
>      PIPELINE_STAGE(ROUTER, IN,  GW_REDIRECT,     24, "lr_in_gw_redirect")    
>  \
> -    PIPELINE_STAGE(ROUTER, IN,  ARP_REQUEST,     25, "lr_in_arp_request")    
>  \
> +    PIPELINE_STAGE(ROUTER, IN,  NETWORK_ID,      25, "lr_in_network_id")     
>  \
> +    PIPELINE_STAGE(ROUTER, IN,  ARP_REQUEST,     26, "lr_in_arp_request")    
>  \
>                                                                        \
>      /* Logical router egress stages. */                               \
>      PIPELINE_STAGE(ROUTER, OUT, CHECK_DNAT_LOCAL,   0,                       
> \
> diff --git a/northd/ovn-northd.8.xml b/northd/ovn-northd.8.xml
> index 155ba8a49..5dd0a004b 100644
> --- a/northd/ovn-northd.8.xml
> +++ b/northd/ovn-northd.8.xml
> @@ -5066,7 +5066,44 @@ icmp6 {
>        </li>
>      </ul>
>  
> -   <h3>Ingress Table 25: ARP Request</h3>
> +   <h3>Ingress Table 25: Network ID</h3>
> +
> +    <p>
> +      This table generates flows that set flags.network_id. It holds the
> +      following flows:
> +    </p>
> +
> +    <ul>
> +      <li>
> +        <p>
> +          A priority-110 flow for IPv4 packets with match <code>outport ==
> +          <var>P</var> &amp;&amp; REG_NEXT_HOP_IPV4 ==
> +          <var>I</var>/<var>C</var></code>, and actions 
> <code>flags.network_id
> +          = <var>N</var>; next;</code>.
> +        </p>
> +
> +        <p>
> +          Where <var>P</var> is the outport, <var>I</var> is the next-hop IP,
> +          <var>C</var> is the next-hop network CIDR, and <var>N</var> is the
> +          network id (index).
> +        </p>
> +
> +        <p>
> +        <code>flags.network_id</code> is 4 bits, and thus only 16 networks 
> can
> +        be indexed. If the number of networks is greater than 16, networks 17
> +        and up will have the actions <code>flags.network_id = 0; next;</code>
> +        and only the first lexicographical IP will be considered for SNAT for
> +        those networks.
> +        </p>
> +      </li>
> +
> +      <li>
> +        Catch-all: A priority-0 flow with match <code>1</code> has
> +        actions <code>next;</code>.
> +      </li>
> +    </ul>
> +
> +   <h3>Ingress Table 26: ARP Request</h3>
>  
>      <p>
>        In the common case where the Ethernet destination has been resolved, 
> this
> @@ -5330,18 +5367,22 @@ nd_ns {
>            table="Logical_Router"/>:lb_force_snat_ip=router_ip), then for
>            each logical router port <var>P</var> attached to the Gateway
>            router, a priority-110 flow matches
> -          <code>flags.force_snat_for_lb == 1 &amp;&amp; outport == 
> <var>P</var>
> -          </code> with an action <code>ct_snat(<var>R</var>);</code>
> -          where <var>R</var> is the IP configured on the router port.
> -          If <code>R</code> is an IPv4 address then the match will also
> -          include <code>ip4</code> and if it is an IPv6 address, then the
> -          match will also include <code>ip6</code>.
> +          <code>flags.force_snat_for_lb == 1 &amp;&amp; flags.network_id ==
> +          <var>N</var> &amp;&amp; outport == <var>P</var></code>, where
> +          <var>N</var> is the network index, with an action
> +          <code>ct_snat(<var>R</var>);</code> where <var>R</var> is the IP
> +          configured on the router port. If <code>R</code> is an IPv4 address
> +          then the match will also include <code>ip4</code> and if it is an
> +          IPv6 address, then the match will also include <code>ip6</code>.
> +          <var>N</var>, the network index, will be 0 for networks 17 and up.
>          </p>
>  
>          <p>
>            If the logical router port <var>P</var> is configured with multiple
> -          IPv4 and multiple IPv6 addresses, only the first IPv4 and first 
> IPv6
> -          address is considered.
> +          IPv4 and multiple IPv6 addresses, the IPv4 and IPv6 address within
> +          the same network as the next-hop will be chosen. However, if there
> +          are more than 16 networks configured, the first lexicographical IP
> +          will be considered for SNAT for networks 17 and up.
>          </p>
>        </li>
>  
> diff --git a/tests/ovn-northd.at b/tests/ovn-northd.at
> index cfaba19bf..6b981fd89 100644
> --- a/tests/ovn-northd.at
> +++ b/tests/ovn-northd.at
> @@ -4493,9 +4493,12 @@ AT_CHECK([grep "lr_in_dnat" lr0flows | 
> ovn_strip_lflows], [0], [dnl
>  
>  AT_CHECK([grep "lr_out_snat" lr0flows | ovn_strip_lflows], [0], [dnl
>    table=??(lr_out_snat        ), priority=0    , match=(1), action=(next;)
> -  table=??(lr_out_snat        ), priority=110  , 
> match=(flags.force_snat_for_lb == 1 && ip4 && outport == "lr0-public"), 
> action=(ct_snat(172.168.0.100);)
> -  table=??(lr_out_snat        ), priority=110  , 
> match=(flags.force_snat_for_lb == 1 && ip4 && outport == "lr0-sw0"), 
> action=(ct_snat(10.0.0.1);)
> -  table=??(lr_out_snat        ), priority=110  , 
> match=(flags.force_snat_for_lb == 1 && ip4 && outport == "lr0-sw1"), 
> action=(ct_snat(20.0.0.1);)
> +  table=??(lr_out_snat        ), priority=105  , 
> match=(flags.force_snat_for_lb == 1 && ip4 && outport == "lr0-public"), 
> action=(ct_snat(172.168.0.100);)
> +  table=??(lr_out_snat        ), priority=105  , 
> match=(flags.force_snat_for_lb == 1 && ip4 && outport == "lr0-sw0"), 
> action=(ct_snat(10.0.0.1);)
> +  table=??(lr_out_snat        ), priority=105  , 
> match=(flags.force_snat_for_lb == 1 && ip4 && outport == "lr0-sw1"), 
> action=(ct_snat(20.0.0.1);)
> +  table=??(lr_out_snat        ), priority=110  , 
> match=(flags.force_snat_for_lb == 1 && flags.network_id == 0 && ip4 && 
> outport == "lr0-public"), action=(ct_snat(172.168.0.100);)
> +  table=??(lr_out_snat        ), priority=110  , 
> match=(flags.force_snat_for_lb == 1 && flags.network_id == 0 && ip4 && 
> outport == "lr0-sw0"), action=(ct_snat(10.0.0.1);)
> +  table=??(lr_out_snat        ), priority=110  , 
> match=(flags.force_snat_for_lb == 1 && flags.network_id == 0 && ip4 && 
> outport == "lr0-sw1"), action=(ct_snat(20.0.0.1);)
>    table=??(lr_out_snat        ), priority=120  , match=(nd_ns), 
> action=(next;)
>  ])
>  
> @@ -4558,10 +4561,14 @@ AT_CHECK([grep "lr_in_dnat" lr0flows | 
> ovn_strip_lflows], [0], [dnl
>  
>  AT_CHECK([grep "lr_out_snat" lr0flows | ovn_strip_lflows], [0], [dnl
>    table=??(lr_out_snat        ), priority=0    , match=(1), action=(next;)
> -  table=??(lr_out_snat        ), priority=110  , 
> match=(flags.force_snat_for_lb == 1 && ip4 && outport == "lr0-public"), 
> action=(ct_snat(172.168.0.100);)
> -  table=??(lr_out_snat        ), priority=110  , 
> match=(flags.force_snat_for_lb == 1 && ip4 && outport == "lr0-sw0"), 
> action=(ct_snat(10.0.0.1);)
> -  table=??(lr_out_snat        ), priority=110  , 
> match=(flags.force_snat_for_lb == 1 && ip4 && outport == "lr0-sw1"), 
> action=(ct_snat(20.0.0.1);)
> -  table=??(lr_out_snat        ), priority=110  , 
> match=(flags.force_snat_for_lb == 1 && ip6 && outport == "lr0-sw1"), 
> action=(ct_snat(bef0::1);)
> +  table=??(lr_out_snat        ), priority=105  , 
> match=(flags.force_snat_for_lb == 1 && ip4 && outport == "lr0-public"), 
> action=(ct_snat(172.168.0.100);)
> +  table=??(lr_out_snat        ), priority=105  , 
> match=(flags.force_snat_for_lb == 1 && ip4 && outport == "lr0-sw0"), 
> action=(ct_snat(10.0.0.1);)
> +  table=??(lr_out_snat        ), priority=105  , 
> match=(flags.force_snat_for_lb == 1 && ip4 && outport == "lr0-sw1"), 
> action=(ct_snat(20.0.0.1);)
> +  table=??(lr_out_snat        ), priority=105  , 
> match=(flags.force_snat_for_lb == 1 && ip6 && outport == "lr0-sw1"), 
> action=(ct_snat(bef0::1);)
> +  table=??(lr_out_snat        ), priority=110  , 
> match=(flags.force_snat_for_lb == 1 && flags.network_id == 0 && ip4 && 
> outport == "lr0-public"), action=(ct_snat(172.168.0.100);)
> +  table=??(lr_out_snat        ), priority=110  , 
> match=(flags.force_snat_for_lb == 1 && flags.network_id == 0 && ip4 && 
> outport == "lr0-sw0"), action=(ct_snat(10.0.0.1);)
> +  table=??(lr_out_snat        ), priority=110  , 
> match=(flags.force_snat_for_lb == 1 && flags.network_id == 0 && ip4 && 
> outport == "lr0-sw1"), action=(ct_snat(20.0.0.1);)
> +  table=??(lr_out_snat        ), priority=110  , 
> match=(flags.force_snat_for_lb == 1 && flags.network_id == 0 && ip6 && 
> outport == "lr0-sw1"), action=(ct_snat(bef0::1);)
>    table=??(lr_out_snat        ), priority=120  , match=(nd_ns), 
> action=(next;)
>  ])
>  
> @@ -6268,8 +6275,10 @@ AT_CHECK([grep "lr_out_post_undnat" lr0flows | 
> ovn_strip_lflows], [0], [dnl
>  
>  AT_CHECK([grep "lr_out_snat" lr0flows | ovn_strip_lflows], [0], [dnl
>    table=??(lr_out_snat        ), priority=0    , match=(1), action=(next;)
> -  table=??(lr_out_snat        ), priority=110  , 
> match=(flags.force_snat_for_lb == 1 && ip4 && outport == "lr0-public"), 
> action=(ct_snat(172.168.0.10);)
> -  table=??(lr_out_snat        ), priority=110  , 
> match=(flags.force_snat_for_lb == 1 && ip4 && outport == "lr0-sw0"), 
> action=(ct_snat(10.0.0.1);)
> +  table=??(lr_out_snat        ), priority=105  , 
> match=(flags.force_snat_for_lb == 1 && ip4 && outport == "lr0-public"), 
> action=(ct_snat(172.168.0.10);)
> +  table=??(lr_out_snat        ), priority=105  , 
> match=(flags.force_snat_for_lb == 1 && ip4 && outport == "lr0-sw0"), 
> action=(ct_snat(10.0.0.1);)
> +  table=??(lr_out_snat        ), priority=110  , 
> match=(flags.force_snat_for_lb == 1 && flags.network_id == 0 && ip4 && 
> outport == "lr0-public"), action=(ct_snat(172.168.0.10);)
> +  table=??(lr_out_snat        ), priority=110  , 
> match=(flags.force_snat_for_lb == 1 && flags.network_id == 0 && ip4 && 
> outport == "lr0-sw0"), action=(ct_snat(10.0.0.1);)
>    table=??(lr_out_snat        ), priority=120  , match=(nd_ns), 
> action=(next;)
>    table=??(lr_out_snat        ), priority=25   , match=(ip && ip4.src == 
> 10.0.0.0/24 && (!ct.trk || !ct.rpl)), action=(ct_snat(172.168.0.10);)
>    table=??(lr_out_snat        ), priority=33   , match=(ip && ip4.src == 
> 10.0.0.10 && (!ct.trk || !ct.rpl)), action=(ct_snat(172.168.0.30);)
> @@ -6334,8 +6343,10 @@ AT_CHECK([grep "lr_out_post_undnat" lr0flows | 
> ovn_strip_lflows], [0], [dnl
>  
>  AT_CHECK([grep "lr_out_snat" lr0flows | ovn_strip_lflows], [0], [dnl
>    table=??(lr_out_snat        ), priority=0    , match=(1), action=(next;)
> -  table=??(lr_out_snat        ), priority=110  , 
> match=(flags.force_snat_for_lb == 1 && ip4 && outport == "lr0-public"), 
> action=(ct_snat(172.168.0.10);)
> -  table=??(lr_out_snat        ), priority=110  , 
> match=(flags.force_snat_for_lb == 1 && ip4 && outport == "lr0-sw0"), 
> action=(ct_snat(10.0.0.1);)
> +  table=??(lr_out_snat        ), priority=105  , 
> match=(flags.force_snat_for_lb == 1 && ip4 && outport == "lr0-public"), 
> action=(ct_snat(172.168.0.10);)
> +  table=??(lr_out_snat        ), priority=105  , 
> match=(flags.force_snat_for_lb == 1 && ip4 && outport == "lr0-sw0"), 
> action=(ct_snat(10.0.0.1);)
> +  table=??(lr_out_snat        ), priority=110  , 
> match=(flags.force_snat_for_lb == 1 && flags.network_id == 0 && ip4 && 
> outport == "lr0-public"), action=(ct_snat(172.168.0.10);)
> +  table=??(lr_out_snat        ), priority=110  , 
> match=(flags.force_snat_for_lb == 1 && flags.network_id == 0 && ip4 && 
> outport == "lr0-sw0"), action=(ct_snat(10.0.0.1);)
>    table=??(lr_out_snat        ), priority=120  , match=(nd_ns), 
> action=(next;)
>    table=??(lr_out_snat        ), priority=25   , match=(ip && ip4.src == 
> 10.0.0.0/24 && (!ct.trk || !ct.rpl)), action=(ct_snat(172.168.0.10);)
>    table=??(lr_out_snat        ), priority=33   , match=(ip && ip4.src == 
> 10.0.0.10 && (!ct.trk || !ct.rpl)), action=(ct_snat(172.168.0.30);)
> @@ -6412,10 +6423,14 @@ AT_CHECK([grep "lr_out_post_undnat" lr0flows | 
> ovn_strip_lflows], [0], [dnl
>  
>  AT_CHECK([grep "lr_out_snat" lr0flows | ovn_strip_lflows], [0], [dnl
>    table=??(lr_out_snat        ), priority=0    , match=(1), action=(next;)
> -  table=??(lr_out_snat        ), priority=110  , 
> match=(flags.force_snat_for_lb == 1 && ip4 && outport == "lr0-public"), 
> action=(ct_snat(172.168.0.10);)
> -  table=??(lr_out_snat        ), priority=110  , 
> match=(flags.force_snat_for_lb == 1 && ip4 && outport == "lr0-sw0"), 
> action=(ct_snat(10.0.0.1);)
> -  table=??(lr_out_snat        ), priority=110  , 
> match=(flags.force_snat_for_lb == 1 && ip6 && outport == "lr0-public"), 
> action=(ct_snat(def0::10);)
> -  table=??(lr_out_snat        ), priority=110  , 
> match=(flags.force_snat_for_lb == 1 && ip6 && outport == "lr0-sw0"), 
> action=(ct_snat(aef0::1);)
> +  table=??(lr_out_snat        ), priority=105  , 
> match=(flags.force_snat_for_lb == 1 && ip4 && outport == "lr0-public"), 
> action=(ct_snat(172.168.0.10);)
> +  table=??(lr_out_snat        ), priority=105  , 
> match=(flags.force_snat_for_lb == 1 && ip4 && outport == "lr0-sw0"), 
> action=(ct_snat(10.0.0.1);)
> +  table=??(lr_out_snat        ), priority=105  , 
> match=(flags.force_snat_for_lb == 1 && ip6 && outport == "lr0-public"), 
> action=(ct_snat(def0::10);)
> +  table=??(lr_out_snat        ), priority=105  , 
> match=(flags.force_snat_for_lb == 1 && ip6 && outport == "lr0-sw0"), 
> action=(ct_snat(aef0::1);)
> +  table=??(lr_out_snat        ), priority=110  , 
> match=(flags.force_snat_for_lb == 1 && flags.network_id == 0 && ip4 && 
> outport == "lr0-public"), action=(ct_snat(172.168.0.10);)
> +  table=??(lr_out_snat        ), priority=110  , 
> match=(flags.force_snat_for_lb == 1 && flags.network_id == 0 && ip4 && 
> outport == "lr0-sw0"), action=(ct_snat(10.0.0.1);)
> +  table=??(lr_out_snat        ), priority=110  , 
> match=(flags.force_snat_for_lb == 1 && flags.network_id == 0 && ip6 && 
> outport == "lr0-public"), action=(ct_snat(def0::10);)
> +  table=??(lr_out_snat        ), priority=110  , 
> match=(flags.force_snat_for_lb == 1 && flags.network_id == 0 && ip6 && 
> outport == "lr0-sw0"), action=(ct_snat(aef0::1);)
>    table=??(lr_out_snat        ), priority=120  , match=(nd_ns), 
> action=(next;)
>    table=??(lr_out_snat        ), priority=25   , match=(ip && ip4.src == 
> 10.0.0.0/24 && (!ct.trk || !ct.rpl)), action=(ct_snat(172.168.0.10);)
>    table=??(lr_out_snat        ), priority=33   , match=(ip && ip4.src == 
> 10.0.0.10 && (!ct.trk || !ct.rpl)), action=(ct_snat(172.168.0.30);)
> @@ -16748,3 +16763,104 @@ check grep -q "Bad configuration: The peer of the 
> switch port 'ls-lrp1' (LRP pee
>  
>  AT_CLEANUP
>  ])
> +
> +AT_SETUP([lb_force_snat_ip=router_ip generate flags.network_id flows])

This should be wrapped in OVN_FOR_EACH_NORTHD_NO_HV.

> +ovn_start
> +
> +check ovn-nbctl lr-add lr
> +check ovn-nbctl set logical_router lr options:chassis=hv1
> +check ovn-nbctl set logical_router lr options:lb_force_snat_ip=router_ip
> +check ovn-nbctl lrp-add lr lrp-client 02:00:00:00:00:02 1.1.1.1/24 ff01::01
> +check ovn-nbctl lrp-add lr lrp-server 02:00:00:00:00:03 1.1.2.1/24 
> 7.7.7.1/24 \
> +            8.8.8.1/24 1.2.1.1/24 1.2.2.1/24 3.3.3.1/24 4.4.4.1/24 
> 5.5.5.1/24 \
> +            6.6.6.1/24 6.2.1.1/24 6.3.1.1/24 6.4.1.1/24 6.5.1.1/24 
> 6.6.1.1/24 \
> +                                  6.7.1.1/24 6.8.1.1/24 6.9.1.1/24 
> 7.2.1.1/24 \
> +                                                   ff01::02 ff01::03 ff01::06
> +check ovn-nbctl ls-add ls-client
> +check ovn-nbctl ls-add ls-server
> +check ovn-nbctl lsp-add ls-client lsp-client-router
> +check ovn-nbctl lsp-set-type lsp-client-router router
> +check ovn-nbctl lsp-add ls-server lsp-server-router
> +check ovn-nbctl lsp-set-type lsp-server-router router
> +check ovn-nbctl set logical_switch_port lsp-client-router 
> options:router-port=lrp-client
> +check ovn-nbctl set logical_switch_port lsp-server-router 
> options:router-port=lrp-server
> +check ovn-nbctl lsp-add ls-client client
> +check ovn-nbctl lsp-add ls-server server
> +check ovn-nbctl lsp-set-addresses client "02:00:00:00:00:01 1.1.1.10"
> +check ovn-nbctl lsp-set-addresses server "02:00:00:00:00:04 2.2.2.10"
> +Andover, Massachusettscheck ovn-nbctl lsp-set-addresses lsp-client-router 
> router

Hmmm, Andover, Massachusetts. :)

> +check ovn-nbctl lsp-set-addresses lsp-server-router router
> +check ovn-nbctl lb-add lb 42.42.42.42:80 2.2.2.10:80 udp
> +check ovn-nbctl lr-lb-add lr lb
> +check ovn-nbctl --wait=hv sync

Nit: this can be --wait=sb.

> +
> +ovn-sbctl dump-flows lr > lrflows
> +AT_CAPTURE_FILE([lrflows])
> +
> +AT_CHECK([grep -E flags.network_id lrflows | ovn_strip_lflows], [0], [dnl
> +  table=??(lr_in_network_id   ), priority=110  , match=(outport == 
> "lrp-client" && reg0 == 1.1.1.1/24), action=(flags.network_id = 0; next;)
> +  table=??(lr_in_network_id   ), priority=110  , match=(outport == 
> "lrp-client" && xxreg0 == ff01::1/128), action=(flags.network_id = 0; next;)
> +  table=??(lr_in_network_id   ), priority=110  , match=(outport == 
> "lrp-server" && reg0 == 1.1.2.1/24), action=(flags.network_id = 0; next;)
> +  table=??(lr_in_network_id   ), priority=110  , match=(outport == 
> "lrp-server" && reg0 == 1.2.1.1/24), action=(flags.network_id = 1; next;)
> +  table=??(lr_in_network_id   ), priority=110  , match=(outport == 
> "lrp-server" && reg0 == 1.2.2.1/24), action=(flags.network_id = 2; next;)
> +  table=??(lr_in_network_id   ), priority=110  , match=(outport == 
> "lrp-server" && reg0 == 3.3.3.1/24), action=(flags.network_id = 3; next;)
> +  table=??(lr_in_network_id   ), priority=110  , match=(outport == 
> "lrp-server" && reg0 == 4.4.4.1/24), action=(flags.network_id = 4; next;)
> +  table=??(lr_in_network_id   ), priority=110  , match=(outport == 
> "lrp-server" && reg0 == 5.5.5.1/24), action=(flags.network_id = 5; next;)
> +  table=??(lr_in_network_id   ), priority=110  , match=(outport == 
> "lrp-server" && reg0 == 6.2.1.1/24), action=(flags.network_id = 6; next;)
> +  table=??(lr_in_network_id   ), priority=110  , match=(outport == 
> "lrp-server" && reg0 == 6.3.1.1/24), action=(flags.network_id = 7; next;)
> +  table=??(lr_in_network_id   ), priority=110  , match=(outport == 
> "lrp-server" && reg0 == 6.4.1.1/24), action=(flags.network_id = 8; next;)
> +  table=??(lr_in_network_id   ), priority=110  , match=(outport == 
> "lrp-server" && reg0 == 6.5.1.1/24), action=(flags.network_id = 9; next;)
> +  table=??(lr_in_network_id   ), priority=110  , match=(outport == 
> "lrp-server" && reg0 == 6.6.1.1/24), action=(flags.network_id = 10; next;)
> +  table=??(lr_in_network_id   ), priority=110  , match=(outport == 
> "lrp-server" && reg0 == 6.6.6.1/24), action=(flags.network_id = 11; next;)
> +  table=??(lr_in_network_id   ), priority=110  , match=(outport == 
> "lrp-server" && reg0 == 6.7.1.1/24), action=(flags.network_id = 12; next;)
> +  table=??(lr_in_network_id   ), priority=110  , match=(outport == 
> "lrp-server" && reg0 == 6.8.1.1/24), action=(flags.network_id = 13; next;)
> +  table=??(lr_in_network_id   ), priority=110  , match=(outport == 
> "lrp-server" && reg0 == 6.9.1.1/24), action=(flags.network_id = 14; next;)
> +  table=??(lr_in_network_id   ), priority=110  , match=(outport == 
> "lrp-server" && reg0 == 7.2.1.1/24), action=(flags.network_id = 15; next;)
> +  table=??(lr_in_network_id   ), priority=110  , match=(outport == 
> "lrp-server" && reg0 == 7.7.7.1/24), action=(flags.network_id = 0; next;)
> +  table=??(lr_in_network_id   ), priority=110  , match=(outport == 
> "lrp-server" && reg0 == 8.8.8.1/24), action=(flags.network_id = 0; next;)
> +  table=??(lr_in_network_id   ), priority=110  , match=(outport == 
> "lrp-server" && xxreg0 == ff01::2/128), action=(flags.network_id = 0; next;)
> +  table=??(lr_in_network_id   ), priority=110  , match=(outport == 
> "lrp-server" && xxreg0 == ff01::3/128), action=(flags.network_id = 1; next;)
> +  table=??(lr_in_network_id   ), priority=110  , match=(outport == 
> "lrp-server" && xxreg0 == ff01::6/128), action=(flags.network_id = 2; next;)
> +  table=??(lr_out_snat        ), priority=110  , 
> match=(flags.force_snat_for_lb == 1 && flags.network_id == 0 && ip4 && 
> outport == "lrp-client"), action=(ct_snat(1.1.1.1);)
> +  table=??(lr_out_snat        ), priority=110  , 
> match=(flags.force_snat_for_lb == 1 && flags.network_id == 0 && ip4 && 
> outport == "lrp-server"), action=(ct_snat(1.1.2.1);)
> +  table=??(lr_out_snat        ), priority=110  , 
> match=(flags.force_snat_for_lb == 1 && flags.network_id == 0 && ip6 && 
> outport == "lrp-client"), action=(ct_snat(ff01::1);)
> +  table=??(lr_out_snat        ), priority=110  , 
> match=(flags.force_snat_for_lb == 1 && flags.network_id == 0 && ip6 && 
> outport == "lrp-server"), action=(ct_snat(ff01::2);)
> +  table=??(lr_out_snat        ), priority=110  , 
> match=(flags.force_snat_for_lb == 1 && flags.network_id == 1 && ip4 && 
> outport == "lrp-server"), action=(ct_snat(1.2.1.1);)
> +  table=??(lr_out_snat        ), priority=110  , 
> match=(flags.force_snat_for_lb == 1 && flags.network_id == 1 && ip6 && 
> outport == "lrp-server"), action=(ct_snat(ff01::3);)
> +  table=??(lr_out_snat        ), priority=110  , 
> match=(flags.force_snat_for_lb == 1 && flags.network_id == 10 && ip4 && 
> outport == "lrp-server"), action=(ct_snat(6.6.1.1);)
> +  table=??(lr_out_snat        ), priority=110  , 
> match=(flags.force_snat_for_lb == 1 && flags.network_id == 11 && ip4 && 
> outport == "lrp-server"), action=(ct_snat(6.6.6.1);)
> +  table=??(lr_out_snat        ), priority=110  , 
> match=(flags.force_snat_for_lb == 1 && flags.network_id == 12 && ip4 && 
> outport == "lrp-server"), action=(ct_snat(6.7.1.1);)
> +  table=??(lr_out_snat        ), priority=110  , 
> match=(flags.force_snat_for_lb == 1 && flags.network_id == 13 && ip4 && 
> outport == "lrp-server"), action=(ct_snat(6.8.1.1);)
> +  table=??(lr_out_snat        ), priority=110  , 
> match=(flags.force_snat_for_lb == 1 && flags.network_id == 14 && ip4 && 
> outport == "lrp-server"), action=(ct_snat(6.9.1.1);)
> +  table=??(lr_out_snat        ), priority=110  , 
> match=(flags.force_snat_for_lb == 1 && flags.network_id == 15 && ip4 && 
> outport == "lrp-server"), action=(ct_snat(7.2.1.1);)
> +  table=??(lr_out_snat        ), priority=110  , 
> match=(flags.force_snat_for_lb == 1 && flags.network_id == 2 && ip4 && 
> outport == "lrp-server"), action=(ct_snat(1.2.2.1);)
> +  table=??(lr_out_snat        ), priority=110  , 
> match=(flags.force_snat_for_lb == 1 && flags.network_id == 2 && ip6 && 
> outport == "lrp-server"), action=(ct_snat(ff01::6);)
> +  table=??(lr_out_snat        ), priority=110  , 
> match=(flags.force_snat_for_lb == 1 && flags.network_id == 3 && ip4 && 
> outport == "lrp-server"), action=(ct_snat(3.3.3.1);)
> +  table=??(lr_out_snat        ), priority=110  , 
> match=(flags.force_snat_for_lb == 1 && flags.network_id == 4 && ip4 && 
> outport == "lrp-server"), action=(ct_snat(4.4.4.1);)
> +  table=??(lr_out_snat        ), priority=110  , 
> match=(flags.force_snat_for_lb == 1 && flags.network_id == 5 && ip4 && 
> outport == "lrp-server"), action=(ct_snat(5.5.5.1);)
> +  table=??(lr_out_snat        ), priority=110  , 
> match=(flags.force_snat_for_lb == 1 && flags.network_id == 6 && ip4 && 
> outport == "lrp-server"), action=(ct_snat(6.2.1.1);)
> +  table=??(lr_out_snat        ), priority=110  , 
> match=(flags.force_snat_for_lb == 1 && flags.network_id == 7 && ip4 && 
> outport == "lrp-server"), action=(ct_snat(6.3.1.1);)
> +  table=??(lr_out_snat        ), priority=110  , 
> match=(flags.force_snat_for_lb == 1 && flags.network_id == 8 && ip4 && 
> outport == "lrp-server"), action=(ct_snat(6.4.1.1);)
> +  table=??(lr_out_snat        ), priority=110  , 
> match=(flags.force_snat_for_lb == 1 && flags.network_id == 9 && ip4 && 
> outport == "lrp-server"), action=(ct_snat(6.5.1.1);)
> +])
> +AT_CLEANUP
> +
> +AT_SETUP([lb_force_snat_ip=router_ip one IPv6 address])

This should be wrapped in OVN_FOR_EACH_NORTHD_NO_HV.

> +ovn_start
> +
> +check ovn-nbctl ls-add sw0
> +check ovn-nbctl lr-add lr0
> +check ovn-nbctl set logical_router lr0 options:chassis=hv1
> +check ovn-nbctl set logical_router lr0 options:lb_force_snat_ip=router_ip
> +check ovn-nbctl lrp-add lr0 lrp0 02:00:00:00:00:01
> +check ovn-nbctl lsp-add sw0 lrp0-attachment
> +check ovn-nbctl lsp-set-type lrp0-attachment router
> +check ovn-nbctl lsp-set-addresses lrp0-attachment 02:00:00:00:00:01
> +check ovn-nbctl lsp-set-options lrp0-attachment router-port=lrp0
> +check ovn-nbctl --wait=hv sync

Nit: this could be just --wait=sb.

> +
> +ovn-sbctl dump-flows lr0 > lrflows
> +AT_CAPTURE_FILE([lrflows])
> +AT_CHECK([grep -E "flags.force_snat_for_lb == 1" lrflows | grep priority=105 
> | ovn_strip_lflows], [0], [dnl

"-E" is extended regex, we don't use any here.

I would change this to:
AT_CHECK([! ovn_strip_lflows < lrflows | grep priority=105 | grep -q 
"flags.force_snat_for_lb == 1"])


> +])

For completeness I think we should check what happens when we
add an explicit IPv6 address.  Something like:

check ovn-nbctl lrp-del lrp0 -- lrp-add lr0 lrp0 02:00:00:00:00:01 4242::4242/64
check ovn-nbctl --wait=sb sync
ovn-sbctl dump-flows lr0 > lrflows
AT_CAPTURE_FILE([lrflows])
AT_CHECK([ovn_strip_lflows < lrflows | grep priority=105 | grep -c 
"flags.force_snat_for_lb == 1"], [0], [dnl
1
])

> +AT_CLEANUP
> diff --git a/tests/ovn.at b/tests/ovn.at
> index d8c06cd73..4156cc376 100644
> --- a/tests/ovn.at
> +++ b/tests/ovn.at
> @@ -42163,3 +42163,74 @@ wait_row_count ACL_ID 0
>  
>  AT_CLEANUP
>  ])
> +
> +AT_SETUP([lb_force_snat_ip=router_ip select correct network for snat])

This should be wrapped in OVN_FOR_EACH_NORTHD.

> +AT_SKIP_IF([test $HAVE_SCAPY = no])
> +ovn_start
> +
> +check ovn-nbctl lr-add lr
> +check ovn-nbctl set logical_router lr options:chassis=hv1
> +check ovn-nbctl set logical_router lr options:lb_force_snat_ip=router_ip
> +check ovn-nbctl lrp-add lr lrp-client 02:00:00:00:00:02 1.1.1.1/24
> +check ovn-nbctl lrp-add lr lrp-server 02:00:00:00:00:03 1.1.2.1/24 
> 1.2.1.1/24 \
> +            1.1.3.1/24 2.2.2.1/24 7.7.7.1/24 8.8.8.1/24 6.7.1.1/24 
> 6.8.1.1/24 \
> +            8.8.8.1/24 1.2.1.1/24 1.2.2.1/24 3.3.3.1/24 4.4.4.1/24 
> 5.5.5.1/24 \
> +            6.6.6.1/24 6.2.1.1/24 6.3.1.1/24 6.4.1.1/24 6.5.1.1/24 6.6.1.1/24

This adds 20 networks, out of which 2 are duplicated (8.8.8.1/24 and 
1.2.1.1/24),
so we could add a check that a warning is generated for the last 2.

> +check ovn-nbctl ls-add ls-client
> +check ovn-nbctl ls-add ls-server
> +check ovn-nbctl lsp-add ls-client lsp-client-router
> +check ovn-nbctl lsp-set-type lsp-client-router router
> +check ovn-nbctl lsp-add ls-server lsp-server-router
> +check ovn-nbctl lsp-set-type lsp-server-router router
> +check ovn-nbctl set logical_switch_port lsp-client-router 
> options:router-port=lrp-client
> +check ovn-nbctl set logical_switch_port lsp-server-router 
> options:router-port=lrp-server
> +check ovn-nbctl lsp-add ls-client client
> +check ovn-nbctl lsp-add ls-server server
> +check ovn-nbctl lsp-set-addresses client "02:00:00:00:00:01 1.1.1.10"
> +check ovn-nbctl lsp-set-addresses server "02:00:00:00:00:04 2.2.2.10"
> +check ovn-nbctl lsp-set-addresses lsp-client-router router
> +check ovn-nbctl lsp-set-addresses lsp-server-router router
> +check ovn-nbctl lb-add lb 42.42.42.42:80 2.2.2.10:80 udp
> +check ovn-nbctl lr-lb-add lr lb
> +
> +# Create a hypervisor and create OVS ports corresponding to logical ports.
> +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 ovs-vsctl -- add-port br-int hv1-vif1 -- \
> +    set interface hv1-vif1 external-ids:iface-id=client \
> +    options:tx_pcap=hv1/client-tx.pcap \
> +    options:rxq_pcap=hv1/client-rx.pcap
> +
> +check ovs-vsctl -- add-port br-int hv1-vif2 -- \
> +    set interface hv1-vif2 external-ids:iface-id=server \
> +    options:tx_pcap=hv1/server-tx.pcap \
> +    options:rxq_pcap=hv1/server-rx.pcap
> +
> +wait_for_ports_up
> +check ovn-nbctl --wait=hv sync
> +
> +tx_src_mac="02:00:00:00:00:01"
> +tx_dst_mac="02:00:00:00:00:02"
> +tx_src_ip=1.1.1.10
> +tx_dst_ip=42.42.42.42
> +request=$(fmt_pkt "Ether(dst='${tx_dst_mac}', src='${tx_src_mac}')/ \
> +                  IP(src='${tx_src_ip}', dst='${tx_dst_ip}')/ \
> +                  UDP(sport=20001, dport=80)")

Nit: I'd indent these under Ether.

> +
> +check as hv1 ovs-appctl netdev-dummy/receive hv1-vif1 $request
> +
> +rx_src_mac="02:00:00:00:00:03"
> +rx_dst_mac="02:00:00:00:00:04"
> +rx_src_ip=2.2.2.1
> +rx_dst_ip=2.2.2.10
> +expected=$(fmt_pkt "Ether(dst='${rx_dst_mac}', src='${rx_src_mac}')/ \
> +                  IP(src='${rx_src_ip}', dst='${rx_dst_ip}', ttl=0x3F)/ \
> +                  UDP(sport=20001, dport=80)")

Nit: I'd indent these under Ether.

> +
> +echo $expected > expected
> +OVN_CHECK_PACKETS([hv1/server-tx.pcap], [expected])
> +AT_CLEANUP

I could squash in the following incremental diff that takes care of the
review comments before applying the patch to main.  I can also take care
of updating the commit message as you suggested in your other reply.
What do you think?

diff --git a/lib/ovn-util.c b/lib/ovn-util.c
index c65b36bb5f..032b16ff38 100644
--- a/lib/ovn-util.c
+++ b/lib/ovn-util.c
@@ -897,7 +897,7 @@ ip_address_and_port_from_lb_key(const char *key, char 
**ip_address,
  *
  * This value is also used to handle some backward compatibility during
  * upgrading. It should never decrease or rewind. */
-#define OVN_INTERNAL_MINOR_VER 6
+#define OVN_INTERNAL_MINOR_VER 7
 
 /* Returns the OVN version. The caller must free the returned value. */
 char *
diff --git a/northd/northd.c b/northd/northd.c
index c94f83e328..cd35204b90 100644
--- a/northd/northd.c
+++ b/northd/northd.c
@@ -13086,6 +13086,7 @@ build_lrouter_force_snat_flows_op(struct ovn_port *op,
         ovn_lflow_add(lflows, op->od, S_ROUTER_OUT_SNAT, 105,
                       ds_cstr(match), ds_cstr(actions), lflow_ref);
     }
+
     /* op->lrp_networks.ipv6_addrs will always have LLA, so only add flow if
      * there is more than 1. */
     if (op->lrp_networks.n_ipv6_addrs > 1) {
@@ -14977,8 +14978,8 @@ build_arp_request_flows_for_lrouter(
 
 static void
 build_lrouter_network_id_flows(
-            struct ovn_datapath *od, struct lflow_table *lflows,
-            struct ds *match, struct ds *actions, struct lflow_ref *lflow_ref)
+        struct ovn_datapath *od, struct lflow_table *lflows,
+        struct ds *match, struct ds *actions, struct lflow_ref *lflow_ref)
 {
     const struct ovn_port *op;
     size_t network_id;
@@ -17455,7 +17456,7 @@ build_lswitch_and_lrouter_iterate_by_lr(struct 
ovn_datapath *od,
                                         lsi->meter_groups,
                                         NULL);
     build_lrouter_network_id_flows(od, lsi->lflows, &lsi->match,
-                                         &lsi->actions, NULL);
+                                   &lsi->actions, NULL);
     build_misc_local_traffic_drop_flows_for_lrouter(od, lsi->lflows, NULL);
 
     build_lr_nat_defrag_and_lb_default_flows(od, lsi->lflows, NULL);
diff --git a/tests/ovn-northd.at b/tests/ovn-northd.at
index 6b981fd890..2e11c2922f 100644
--- a/tests/ovn-northd.at
+++ b/tests/ovn-northd.at
@@ -16764,6 +16764,7 @@ check grep -q "Bad configuration: The peer of the 
switch port 'ls-lrp1' (LRP pee
 AT_CLEANUP
 ])
 
+OVN_FOR_EACH_NORTHD_NO_HV([
 AT_SETUP([lb_force_snat_ip=router_ip generate flags.network_id flows])
 ovn_start
 
@@ -16788,11 +16789,11 @@ check ovn-nbctl lsp-add ls-client client
 check ovn-nbctl lsp-add ls-server server
 check ovn-nbctl lsp-set-addresses client "02:00:00:00:00:01 1.1.1.10"
 check ovn-nbctl lsp-set-addresses server "02:00:00:00:00:04 2.2.2.10"
-Andover, Massachusettscheck ovn-nbctl lsp-set-addresses lsp-client-router 
router
+check ovn-nbctl lsp-set-addresses lsp-client-router router
 check ovn-nbctl lsp-set-addresses lsp-server-router router
 check ovn-nbctl lb-add lb 42.42.42.42:80 2.2.2.10:80 udp
 check ovn-nbctl lr-lb-add lr lb
-check ovn-nbctl --wait=hv sync
+check ovn-nbctl --wait=sb sync
 
 ovn-sbctl dump-flows lr > lrflows
 AT_CAPTURE_FILE([lrflows])
@@ -16844,7 +16845,9 @@ AT_CHECK([grep -E flags.network_id lrflows | 
ovn_strip_lflows], [0], [dnl
   table=??(lr_out_snat        ), priority=110  , 
match=(flags.force_snat_for_lb == 1 && flags.network_id == 9 && ip4 && outport 
== "lrp-server"), action=(ct_snat(6.5.1.1);)
 ])
 AT_CLEANUP
+])
 
+OVN_FOR_EACH_NORTHD_NO_HV([
 AT_SETUP([lb_force_snat_ip=router_ip one IPv6 address])
 ovn_start
 
@@ -16857,10 +16860,19 @@ check ovn-nbctl lsp-add sw0 lrp0-attachment
 check ovn-nbctl lsp-set-type lrp0-attachment router
 check ovn-nbctl lsp-set-addresses lrp0-attachment 02:00:00:00:00:01
 check ovn-nbctl lsp-set-options lrp0-attachment router-port=lrp0
-check ovn-nbctl --wait=hv sync
+check ovn-nbctl --wait=sb sync
 
 ovn-sbctl dump-flows lr0 > lrflows
 AT_CAPTURE_FILE([lrflows])
-AT_CHECK([grep -E "flags.force_snat_for_lb == 1" lrflows | grep priority=105 | 
ovn_strip_lflows], [0], [dnl
+AT_CHECK([! ovn_strip_lflows < lrflows | grep priority=105 | grep -q 
"flags.force_snat_for_lb == 1"])
+
+check ovn-nbctl lrp-del lrp0 -- lrp-add lr0 lrp0 02:00:00:00:00:01 
4242::4242/64
+check ovn-nbctl --wait=sb sync
+ovn-sbctl dump-flows lr0 > lrflows
+AT_CAPTURE_FILE([lrflows])
+AT_CHECK([ovn_strip_lflows < lrflows | grep priority=105 | grep -c 
"flags.force_snat_for_lb == 1"], [0], [dnl
+1
 ])
+
 AT_CLEANUP
+])
diff --git a/tests/ovn.at b/tests/ovn.at
index 4156cc3767..f1600a2393 100644
--- a/tests/ovn.at
+++ b/tests/ovn.at
@@ -42164,6 +42164,7 @@ wait_row_count ACL_ID 0
 AT_CLEANUP
 ])
 
+OVN_FOR_EACH_NORTHD([
 AT_SETUP([lb_force_snat_ip=router_ip select correct network for snat])
 AT_SKIP_IF([test $HAVE_SCAPY = no])
 ovn_start
@@ -42174,8 +42175,8 @@ check ovn-nbctl set logical_router lr 
options:lb_force_snat_ip=router_ip
 check ovn-nbctl lrp-add lr lrp-client 02:00:00:00:00:02 1.1.1.1/24
 check ovn-nbctl lrp-add lr lrp-server 02:00:00:00:00:03 1.1.2.1/24 1.2.1.1/24 \
             1.1.3.1/24 2.2.2.1/24 7.7.7.1/24 8.8.8.1/24 6.7.1.1/24 6.8.1.1/24 \
-            8.8.8.1/24 1.2.1.1/24 1.2.2.1/24 3.3.3.1/24 4.4.4.1/24 5.5.5.1/24 \
-            6.6.6.1/24 6.2.1.1/24 6.3.1.1/24 6.4.1.1/24 6.5.1.1/24 6.6.1.1/24
+            1.2.2.1/24 3.3.3.1/24 4.4.4.1/24 5.5.5.1/24 6.6.6.1/24 6.2.1.1/24 \
+            6.3.1.1/24 6.4.1.1/24 6.5.1.1/24 6.6.1.1/24
 check ovn-nbctl ls-add ls-client
 check ovn-nbctl ls-add ls-server
 check ovn-nbctl lsp-add ls-client lsp-client-router
@@ -42213,13 +42214,17 @@ check ovs-vsctl -- add-port br-int hv1-vif2 -- \
 wait_for_ports_up
 check ovn-nbctl --wait=hv sync
 
+check grep -q \
+    'Logical router port "lrp-server" already has the max of 16 networks 
configured' \
+    northd/ovn-northd.log
+
 tx_src_mac="02:00:00:00:00:01"
 tx_dst_mac="02:00:00:00:00:02"
 tx_src_ip=1.1.1.10
 tx_dst_ip=42.42.42.42
 request=$(fmt_pkt "Ether(dst='${tx_dst_mac}', src='${tx_src_mac}')/ \
-                  IP(src='${tx_src_ip}', dst='${tx_dst_ip}')/ \
-                  UDP(sport=20001, dport=80)")
+                   IP(src='${tx_src_ip}', dst='${tx_dst_ip}')/ \
+                   UDP(sport=20001, dport=80)")
 
 check as hv1 ovs-appctl netdev-dummy/receive hv1-vif1 $request
 
@@ -42228,9 +42233,10 @@ rx_dst_mac="02:00:00:00:00:04"
 rx_src_ip=2.2.2.1
 rx_dst_ip=2.2.2.10
 expected=$(fmt_pkt "Ether(dst='${rx_dst_mac}', src='${rx_src_mac}')/ \
-                  IP(src='${rx_src_ip}', dst='${rx_dst_ip}', ttl=0x3F)/ \
-                  UDP(sport=20001, dport=80)")
+                    IP(src='${rx_src_ip}', dst='${rx_dst_ip}', ttl=0x3F)/ \
+                    UDP(sport=20001, dport=80)")
 
 echo $expected > expected
 OVN_CHECK_PACKETS([hv1/server-tx.pcap], [expected])
 AT_CLEANUP
+])
--

Regards,
Dumitru

_______________________________________________
dev mailing list
d...@openvswitch.org
https://mail.openvswitch.org/mailman/listinfo/ovs-dev

Reply via email to