diff mbox series

[ovs-dev,v2] northd, ic: Add hub-spoke options to adv DR routes learned in ovn-ic.

Message ID 20260203202354.228161-1-lucas.vdias@luizalabs.com
State New
Headers show
Series [ovs-dev,v2] northd, ic: Add hub-spoke options to adv DR routes learned in ovn-ic. | expand

Checks

Context Check Description
ovsrobot/apply-robot success apply and check: success
ovsrobot/github-robot-_Build_and_Test success github build: passed
ovsrobot/github-robot-_ovn-kubernetes success github build: passed

Commit Message

Lucas Vargas Dias Feb. 3, 2026, 8:23 p.m. UTC
Consider the scenario where there are 2 AZs using ovn-ic, each AZ has
one LR and its LS. The LRs are interconnected by a transit switch.
On each AZ, we can configure the dynamic routing and exchange routes
via BGP with external BGP speakers.

AZ1
LS1 - LR1 - BGP Speaker1 - Local subnet1
       \
   -----------------------
    transit switch
   -----------------------
AZ2    /
LS2 - LR2 - BGP Speaker2 - Local subnet2

The LR2 will learn the subnet1 prefix via ovn-ic but this prefix will
not be redistributed to BGP Speaker2 and it also happens with LR1.
This scenario uses the network's hub-and-spoke terminology where we can
enable the hub-spoke on the LR for such redistribution.
subnet1 and subnet2 are configured as ic-source-dynamic=true in ovn-ic.
Also, consider the same scenario, but LR1 and LR2 learn the same subnet
for redundancy, hub-spoke option is disabled to not adv the subnet learned
from BGP and advertised in ovn-ic in BGP speakers.

Signed-off-by: Lucas Vargas Dias <lucas.vdias@luizalabs.com>
---
 NEWS                              |  6 ++
 ic/ovn-ic.c                       | 28 +++++++---
 northd/en-advertised-route-sync.c |  3 +
 northd/northd.c                   | 10 +++-
 northd/northd.h                   |  5 +-
 northd/ovn-northd.c               |  1 -
 ovn-nb.xml                        | 21 +++++++
 tests/ovn-ic.at                   | 92 +++++++++++++++++++++++++++++++
 8 files changed, 156 insertions(+), 10 deletions(-)

Comments

Lorenzo Bianconi Feb. 5, 2026, 3:08 p.m. UTC | #1
On Feb 03, Lucas Vargas Dias via dev wrote:
> Consider the scenario where there are 2 AZs using ovn-ic, each AZ has
> one LR and its LS. The LRs are interconnected by a transit switch.
> On each AZ, we can configure the dynamic routing and exchange routes
> via BGP with external BGP speakers.
> 
> AZ1
> LS1 - LR1 - BGP Speaker1 - Local subnet1
>        \
>    -----------------------
>     transit switch
>    -----------------------
> AZ2    /
> LS2 - LR2 - BGP Speaker2 - Local subnet2
> 
> The LR2 will learn the subnet1 prefix via ovn-ic but this prefix will
> not be redistributed to BGP Speaker2 and it also happens with LR1.
> This scenario uses the network's hub-and-spoke terminology where we can
> enable the hub-spoke on the LR for such redistribution.
> subnet1 and subnet2 are configured as ic-source-dynamic=true in ovn-ic.
> Also, consider the same scenario, but LR1 and LR2 learn the same subnet
> for redundancy, hub-spoke option is disabled to not adv the subnet learned
> from BGP and advertised in ovn-ic in BGP speakers.
> 
> Signed-off-by: Lucas Vargas Dias <lucas.vdias@luizalabs.com>

Acked-by: Lorenzo Bianconi <lorenzo.bianconi@redhat.com>

> ---
>  NEWS                              |  6 ++
>  ic/ovn-ic.c                       | 28 +++++++---
>  northd/en-advertised-route-sync.c |  3 +
>  northd/northd.c                   | 10 +++-
>  northd/northd.h                   |  5 +-
>  northd/ovn-northd.c               |  1 -
>  ovn-nb.xml                        | 21 +++++++
>  tests/ovn-ic.at                   | 92 +++++++++++++++++++++++++++++++
>  8 files changed, 156 insertions(+), 10 deletions(-)
> 
> diff --git a/NEWS b/NEWS
> index 2a2b5e12d..6002820f3 100644
> --- a/NEWS
> +++ b/NEWS
> @@ -40,6 +40,8 @@ Post v25.09.0
>       * Add the "options:dynamic-routing-no-learning" to Logical Routers ports.
>         If set to true, router port will not learn routes and will forget
>         learned routes. This option has priority over its router counterpart.
> +     * Add support for hub-and-spoke propagation via the "hub-spoke" option
> +       in dynamic-routing-redistribute settings.
>     - Add support for Network Function insertion in OVN with stateful traffic
>       redirection capability in Logical Switch datapath. The feature introduces
>       three new NB database tables:
> @@ -98,6 +100,10 @@ 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 the external_ids:ic-source-dynamic key for
> +     Logical_Router_Static_Route to indicate whether a learned OVN-IC route
> +     originated from dynamic routing sources in the advertising availability
> +     zone.
>  
>  OVN v25.09.0 - xxx xx xxxx
>  --------------------------
> diff --git a/ic/ovn-ic.c b/ic/ovn-ic.c
> index fd5ecefb3..e9fff2d4d 100644
> --- a/ic/ovn-ic.c
> +++ b/ic/ovn-ic.c
> @@ -1283,6 +1283,7 @@ struct ic_route_info {
>      struct in6_addr prefix;
>      unsigned int plen;
>      struct in6_addr nexthop;
> +    bool is_src_dynamic;
>      const char *origin;
>      const char *route_table;
>      const char *route_tag;
> @@ -1554,7 +1555,7 @@ add_to_routes_ad(struct hmap *routes_ad, const struct in6_addr prefix,
>                   const struct nbrec_logical_router_static_route *nb_route,
>                   const struct nbrec_logical_router *nb_lr,
>                   const struct nbrec_load_balancer *nb_lb,
> -                 const char *route_tag)
> +                 const char *route_tag, bool is_src_dynamic)
>  {
>      ovs_assert(nb_route || nb_lrp || nb_lb || nb_lr);
>  
> @@ -1573,6 +1574,7 @@ add_to_routes_ad(struct hmap *routes_ad, const struct in6_addr prefix,
>          ic_route->nb_route = nb_route;
>          ic_route->origin = origin;
>          ic_route->route_table = route_table;
> +        ic_route->is_src_dynamic = is_src_dynamic;
>          ic_route->nb_lrp = nb_lrp;
>          ic_route->nb_lr = nb_lr;
>          ic_route->nb_lb = nb_lb;
> @@ -1649,7 +1651,7 @@ add_static_to_routes_ad(
>  
>      add_to_routes_ad(routes_ad, prefix, plen, nexthop, ROUTE_ORIGIN_STATIC,
>                       nb_route->route_table, NULL, nb_route, nb_lr,
> -                     NULL, route_tag);
> +                     NULL, route_tag, false);
>  }
>  
>  static void
> @@ -1659,7 +1661,8 @@ add_network_to_routes_ad(struct hmap *routes_ad, const char *network,
>                           const struct smap *nb_options,
>                           const struct nbrec_logical_router *nb_lr,
>                           const char *route_tag,
> -                         const struct nbrec_logical_router_port *ts_lrp)
> +                         const struct nbrec_logical_router_port *ts_lrp,
> +                         bool is_src_dynamic)
>  {
>      struct in6_addr prefix, nexthop;
>      unsigned int plen;
> @@ -1711,7 +1714,8 @@ add_network_to_routes_ad(struct hmap *routes_ad, const char *network,
>  
>      /* directly-connected routes go to <main> route table */
>      add_to_routes_ad(routes_ad, prefix, plen, nexthop, ROUTE_ORIGIN_CONNECTED,
> -                     NULL, nb_lrp, NULL, nb_lr, NULL, route_tag);
> +                     NULL, nb_lrp, NULL, nb_lr, NULL, route_tag,
> +                     is_src_dynamic);
>  }
>  
>  static void
> @@ -1769,7 +1773,7 @@ add_lb_vip_to_routes_ad(struct hmap *routes_ad, const char *vip_key,
>  
>      /* Lb vip routes go to <main> route table */
>      add_to_routes_ad(routes_ad, vip_ip, plen, nexthop, ROUTE_ORIGIN_LB,
> -                     NULL, NULL, NULL, nb_lr, nb_lb, route_tag);
> +                     NULL, NULL, NULL, nb_lr, nb_lb, route_tag, false);
>  out:
>      free(vip_str);
>  }
> @@ -2187,6 +2191,12 @@ sync_learned_routes(struct ic_context *ctx,
>                  nbrec_logical_router_static_route_update_options_setkey(
>                      nb_route, "origin", isb_route->origin);
>                  free(uuid_s);
> +                bool is_src_dynamic = smap_get_bool(&isb_route->external_ids,
> +                    "ic-source-dynamic", false);
> +                char *ic_source_dynamic_str = is_src_dynamic ?
> +                    "true" : "false";
> +                nbrec_logical_router_static_route_update_external_ids_setkey(
> +                    nb_route, "ic-source-dynamic", ic_source_dynamic_str);
>                  nbrec_logical_router_update_static_routes_addvalue(ic_lr->lr,
>                      nb_route);
>              }
> @@ -2243,6 +2253,10 @@ ad_route_sync_external_ids(const struct ic_route_info *route_adv,
>                                                       "ic-route-tag");
>          }
>      }
> +
> +    char *ic_src_dynamic_str = route_adv->is_src_dynamic ? "true" : "false";
> +    icsbrec_route_update_external_ids_setkey(isb_route, "ic-source-dynamic",
> +                                             ic_src_dynamic_str);
>  }
>  
>  /* Sync routes from routes_ad to IC-SB. */
> @@ -2372,7 +2386,7 @@ build_ts_routes_to_adv(struct ic_context *ctx,
>                  add_network_to_routes_ad(routes_ad, lrp->networks[j], lrp,
>                                           ts_port_addrs,
>                                           &nb_global->options,
> -                                         lr, route_tag, ts_lrp);
> +                                         lr, route_tag, ts_lrp, false);
>              }
>          } else {
>              /* The router port of the TS port is ignored. */
> @@ -2427,7 +2441,7 @@ build_ts_routes_to_adv(struct ic_context *ctx,
>          add_network_to_routes_ad(routes_ad, sb_route->ip_prefix, NULL,
>                                   ts_port_addrs,
>                                   &nb_global->options,
> -                                 lr, route_tag, ts_lrp);
> +                                 lr, route_tag, ts_lrp, true);
>      }
>      sbrec_learned_route_index_destroy_row(filter);
>  }
> diff --git a/northd/en-advertised-route-sync.c b/northd/en-advertised-route-sync.c
> index be771391d..be046769f 100644
> --- a/northd/en-advertised-route-sync.c
> +++ b/northd/en-advertised-route-sync.c
> @@ -675,6 +675,8 @@ should_advertise_route(const struct uuidset *host_route_lrps,
>          return drr_mode_NAT_is_set(drr);
>      case ROUTE_SOURCE_LB:
>          return drr_mode_LB_is_set(drr);
> +    case ROUTE_SOURCE_IC_DYNAMIC:
> +        return drr_mode_IC_DYNAMIC_is_set(drr);
>      case ROUTE_SOURCE_LEARNED:
>          OVS_NOT_REACHED();
>      default:
> @@ -745,6 +747,7 @@ advertise_route_track_od(struct advertised_route_sync_data *data,
>                             &tracked_op->od->nbr->header_.uuid);
>          }
>          break;
> +    case ROUTE_SOURCE_IC_DYNAMIC:
>      case ROUTE_SOURCE_CONNECTED:
>      case ROUTE_SOURCE_STATIC:
>          break;
> diff --git a/northd/northd.c b/northd/northd.c
> index b4bb4ba6d..fbb75533c 100644
> --- a/northd/northd.c
> +++ b/northd/northd.c
> @@ -870,6 +870,10 @@ parse_dynamic_routing_redistribute(
>              out |= DRRM_LB;
>              continue;
>          }
> +        if (!strcmp(token, "hub-spoke")) {
> +            out |= DRRM_IC_DYNAMIC;
> +            continue;
> +        }
>          static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
>          VLOG_WARN_RL(&rl,
>                       "unknown dynamic-routing-redistribute option '%s' on %s",
> @@ -12034,7 +12038,10 @@ parsed_routes_add_static(const struct ovn_datapath *od,
>      enum route_source source;
>      if (!strcmp(smap_get_def(&route->options, "origin", ""),
>                  ROUTE_ORIGIN_CONNECTED)) {
> -        source = ROUTE_SOURCE_CONNECTED;
> +        bool ic_src_dynamic = smap_get_bool(&route->external_ids,
> +                                            "ic-source-dynamic", false);
> +        source = ic_src_dynamic ?
> +                 ROUTE_SOURCE_IC_DYNAMIC : ROUTE_SOURCE_CONNECTED;
>      } else {
>          source = ROUTE_SOURCE_STATIC;
>      }
> @@ -12129,6 +12136,7 @@ route_source_to_offset(enum route_source source)
>  {
>      switch (source) {
>      case ROUTE_SOURCE_CONNECTED:
> +    case ROUTE_SOURCE_IC_DYNAMIC:
>          return ROUTE_PRIO_OFFSET_CONNECTED;
>      case ROUTE_SOURCE_STATIC:
>          return ROUTE_PRIO_OFFSET_STATIC;
> diff --git a/northd/northd.h b/northd/northd.h
> index eb5c15f34..e6ed2cc3e 100644
> --- a/northd/northd.h
> +++ b/northd/northd.h
> @@ -372,7 +372,8 @@ struct mcast_port_info {
>      DRR_MODE(CONNECTED_AS_HOST, 1) \
>      DRR_MODE(STATIC,            2) \
>      DRR_MODE(NAT,               3) \
> -    DRR_MODE(LB,                4)
> +    DRR_MODE(LB,                4) \
> +    DRR_MODE(IC_DYNAMIC,        5)
>  
>  enum dynamic_routing_redistribute_mode_bits {
>  #define DRR_MODE(PROTOCOL, BIT) DRRM_##PROTOCOL##_BIT = BIT,
> @@ -826,6 +827,8 @@ enum route_source {
>      ROUTE_SOURCE_NAT,
>      /* The route is derived from a LB's VIP. */
>      ROUTE_SOURCE_LB,
> +    /* The route is derived from an ovn-controller and advertised to IC. */
> +    ROUTE_SOURCE_IC_DYNAMIC,
>  };
>  
>  struct parsed_route {
> diff --git a/northd/ovn-northd.c b/northd/ovn-northd.c
> index 7d7568c6f..bc3969053 100644
> --- a/northd/ovn-northd.c
> +++ b/northd/ovn-northd.c
> @@ -905,7 +905,6 @@ main(int argc, char *argv[])
>          &nbrec_load_balancer_col_external_ids,
>          &nbrec_load_balancer_health_check_col_external_ids,
>          &nbrec_logical_router_policy_col_external_ids,
> -        &nbrec_logical_router_static_route_col_external_ids,
>          &nbrec_meter_col_external_ids,
>          &nbrec_meter_band_col_external_ids,
>          &nbrec_mirror_col_external_ids,
> diff --git a/ovn-nb.xml b/ovn-nb.xml
> index 1acbf202b..5f9b47491 100644
> --- a/ovn-nb.xml
> +++ b/ovn-nb.xml
> @@ -3374,6 +3374,13 @@ or
>            Logical Switch.
>          </p>
>  
> +        <p>
> +          If <code>hub-spoke</code> is in the list then northd will synchronize
> +          dynamic routes learned through OVN-IC from other routers into the
> +          <ref table="Advertised_Route" db="OVN_Southbound"/> table, enabling
> +          hub-and-spoke propagation.
> +        </p>
> +
>          <p>
>            This value can be overwritten on a per LRP basis using
>            <ref column="options" key="dynamic-routing-redistribute"
> @@ -4461,6 +4468,13 @@ or
>            via shared Logical Switch.
>          </p>
>  
> +        <p>
> +          If <code>hub-spoke</code> is in the list then northd will synchronize
> +          dynamic routes learned through OVN-IC from other routers into the
> +          <ref table="Advertised_Route" db="OVN_Southbound"/> table, enabling
> +          hub-and-spoke propagation.
> +        </p>
> +
>          <p>
>            If not set the value from <ref column="options"
>            key="dynamic-routing-redistribute" table="Logical_Router"/> on the
> @@ -4823,6 +4837,13 @@ or
>        database.
>      </column>
>  
> +    <column name="external_ids" key="ic-source-dynamic">
> +      <code>ovn-ic</code> populates this key for routes learned from
> +      <ref db="OVN_IC_Southbound"/>. The value is <code>true</code> if the
> +      learned route originated from dynamic sources (e.g. learned routes)
> +      in the advertising availability zone, otherwise <code>false</code>.
> +    </column>
> +
>      <group title="Common Columns">
>        <column name="external_ids">
>          See <em>External IDs</em> at the beginning of this document.
> diff --git a/tests/ovn-ic.at b/tests/ovn-ic.at
> index 370a755be..a62d02da0 100644
> --- a/tests/ovn-ic.at
> +++ b/tests/ovn-ic.at
> @@ -4586,3 +4586,95 @@ OVN_CLEANUP_IC([az1], [az2])
>  
>  AT_CLEANUP
>  ])
> +
> +
> +OVN_FOR_EACH_NORTHD([
> +AT_SETUP([ovn-ic -- Check ovn-ic adv and learn from SB Learned Route - hub and spoke mode])
> +
> +ovn_init_ic_db
> +
> +for i in 1 2; do
> +    ovn_start az$i
> +    ovn_as az$i
> +
> +    # Enable route learning at AZ level
> +    check ovn-nbctl set nb_global . options:ic-route-learn=true
> +    # Enable route advertising at AZ level
> +    check ovn-nbctl set nb_global . options:ic-route-adv=true
> +done
> +
> +# Create new transit switches and LRs. Test topology is next:
> +#
> +#
> +# logical router (lr11) - transit switch (ts11) - logical router (lr12)
> +#
> +#
> +
> +# Create lr11, lr12 and ts11 and connect them
> +for i in 1 2; do
> +    ovn_as az$i
> +
> +    lr=lr1$i
> +    check ovn-nbctl lr-add $lr
> +
> +    ts=ts11
> +    check ovn-ic-nbctl --wait=sb --may-exist ts-add $ts
> +
> +    lrp=lrp-$lr-$ts
> +    lsp=lsp-$ts-$lr
> +    # Create LRP and connect to TS
> +    check ovn-nbctl lrp-add $lr $lrp aa:aa:aa:aa:a1:0$i 169.254.101.$i/24
> +    check ovn-nbctl lsp-add-router-port $ts $lsp $lrp
> +done
> +
> +# Create directly-connected route in lr12
> +check ovn_as az2 ovn-nbctl lrp-add lr12 lrp-lr12 aa:aa:aa:aa:bb:01 "192.168.0.1/24"
> +OVS_WAIT_FOR_OUTPUT([ovn_as az1 ovn-nbctl lr-route-list lr11 | grep 192.168 |
> +             grep learned | awk '{print $1, $2}' | sort ], [0], [dnl
> +192.168.0.0/24 169.254.101.2
> +])
> +
> +ovn_as az2
> +check ovn-nbctl --wait=sb set Logical_Router lr12 option:dynamic-routing=true \
> +    option:dynamic-routing-redistribute="connected,static"
> +check ovn_as az2 ovn-nbctl --wait=sb lrp-add lr12 lr12-dr1 00:00:00:00:ff:01 10.0.0.1/24
> +dr1=$(fetch_column port_binding _uuid logical_port=lr12-dr1)
> +datapath=$(fetch_column datapath_binding _uuid external_ids:name=lr12)
> +
> +check_uuid ovn-sbctl create Learned_Route \
> +    datapath=$datapath                    \
> +    logical_port=$dr1                     \
> +    ip_prefix=192.168.1.0/24              \
> +    nexthop=10.0.0.20
> +
> +# Check Learned_Route adv in ovn-ic
> +OVS_WAIT_FOR_OUTPUT([ovn_as az1 ovn-nbctl lr-route-list lr11 |
> +             grep learned | awk '{print $1, $2}' | sort ], [0], [dnl
> +10.0.0.0/24 169.254.101.2
> +192.168.0.0/24 169.254.101.2
> +192.168.1.0/24 169.254.101.2
> +])
> +
> +ovn_as az1
> +check ovn-nbctl --wait=sb set Logical_Router lr11 option:dynamic-routing=true \
> +    option:dynamic-routing-redistribute="connected,static"
> +# Advertise just 10.0.0.0/24 and 192.168.0.0/24 routes
> +check_row_count Advertised_Route 1 ip_prefix=192.168.0.0/24
> +check_row_count Advertised_Route 1 ip_prefix=10.0.0.0/24
> +check_row_count Advertised_Route 0 ip_prefix=192.168.1.0/24
> +
> +
> +ovn_as az1
> +check ovn-nbctl --wait=sb set Logical_Router lr11 \
> +    option:dynamic-routing-redistribute="connected,static,hub-spoke"
> +# Advertise just 10.0.0.0/24, 192.168.0.0/24 and 192.168.1.0/24 routes
> +# Route 192.168.1.0/24 is learned by DR from other logical routes (lr12)
> +# And lr12 Advertised it in ovn-ic. Hub-spoke option enable re-routing in lr11
> +check_row_count Advertised_Route 1 ip_prefix=192.168.0.0/24
> +check_row_count Advertised_Route 1 ip_prefix=10.0.0.0/24
> +check_row_count Advertised_Route 1 ip_prefix=192.168.1.0/24
> +
> +OVN_CLEANUP_IC([az1], [az2])
> +
> +AT_CLEANUP
> +])
> -- 
> 2.43.0
> 
> 
> -- 
> 
> 
> 
> 
> _'Esta mensagem é direcionada apenas para os endereços constantes no 
> cabeçalho inicial. Se você não está listado nos endereços constantes no 
> cabeçalho, pedimos-lhe que desconsidere completamente o conteúdo dessa 
> mensagem e cuja cópia, encaminhamento e/ou execução das ações citadas estão 
> imediatamente anuladas e proibidas'._
> 
> 
> * **'Apesar do Magazine Luiza tomar 
> todas as precauções razoáveis para assegurar que nenhum vírus esteja 
> presente nesse e-mail, a empresa não poderá aceitar a responsabilidade por 
> quaisquer perdas ou danos causados por esse e-mail ou por seus anexos'.*
> 
> 
> 
> _______________________________________________
> dev mailing list
> dev@openvswitch.org
> https://mail.openvswitch.org/mailman/listinfo/ovs-dev
>
diff mbox series

Patch

diff --git a/NEWS b/NEWS
index 2a2b5e12d..6002820f3 100644
--- a/NEWS
+++ b/NEWS
@@ -40,6 +40,8 @@  Post v25.09.0
      * Add the "options:dynamic-routing-no-learning" to Logical Routers ports.
        If set to true, router port will not learn routes and will forget
        learned routes. This option has priority over its router counterpart.
+     * Add support for hub-and-spoke propagation via the "hub-spoke" option
+       in dynamic-routing-redistribute settings.
    - Add support for Network Function insertion in OVN with stateful traffic
      redirection capability in Logical Switch datapath. The feature introduces
      three new NB database tables:
@@ -98,6 +100,10 @@  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 the external_ids:ic-source-dynamic key for
+     Logical_Router_Static_Route to indicate whether a learned OVN-IC route
+     originated from dynamic routing sources in the advertising availability
+     zone.
 
 OVN v25.09.0 - xxx xx xxxx
 --------------------------
diff --git a/ic/ovn-ic.c b/ic/ovn-ic.c
index fd5ecefb3..e9fff2d4d 100644
--- a/ic/ovn-ic.c
+++ b/ic/ovn-ic.c
@@ -1283,6 +1283,7 @@  struct ic_route_info {
     struct in6_addr prefix;
     unsigned int plen;
     struct in6_addr nexthop;
+    bool is_src_dynamic;
     const char *origin;
     const char *route_table;
     const char *route_tag;
@@ -1554,7 +1555,7 @@  add_to_routes_ad(struct hmap *routes_ad, const struct in6_addr prefix,
                  const struct nbrec_logical_router_static_route *nb_route,
                  const struct nbrec_logical_router *nb_lr,
                  const struct nbrec_load_balancer *nb_lb,
-                 const char *route_tag)
+                 const char *route_tag, bool is_src_dynamic)
 {
     ovs_assert(nb_route || nb_lrp || nb_lb || nb_lr);
 
@@ -1573,6 +1574,7 @@  add_to_routes_ad(struct hmap *routes_ad, const struct in6_addr prefix,
         ic_route->nb_route = nb_route;
         ic_route->origin = origin;
         ic_route->route_table = route_table;
+        ic_route->is_src_dynamic = is_src_dynamic;
         ic_route->nb_lrp = nb_lrp;
         ic_route->nb_lr = nb_lr;
         ic_route->nb_lb = nb_lb;
@@ -1649,7 +1651,7 @@  add_static_to_routes_ad(
 
     add_to_routes_ad(routes_ad, prefix, plen, nexthop, ROUTE_ORIGIN_STATIC,
                      nb_route->route_table, NULL, nb_route, nb_lr,
-                     NULL, route_tag);
+                     NULL, route_tag, false);
 }
 
 static void
@@ -1659,7 +1661,8 @@  add_network_to_routes_ad(struct hmap *routes_ad, const char *network,
                          const struct smap *nb_options,
                          const struct nbrec_logical_router *nb_lr,
                          const char *route_tag,
-                         const struct nbrec_logical_router_port *ts_lrp)
+                         const struct nbrec_logical_router_port *ts_lrp,
+                         bool is_src_dynamic)
 {
     struct in6_addr prefix, nexthop;
     unsigned int plen;
@@ -1711,7 +1714,8 @@  add_network_to_routes_ad(struct hmap *routes_ad, const char *network,
 
     /* directly-connected routes go to <main> route table */
     add_to_routes_ad(routes_ad, prefix, plen, nexthop, ROUTE_ORIGIN_CONNECTED,
-                     NULL, nb_lrp, NULL, nb_lr, NULL, route_tag);
+                     NULL, nb_lrp, NULL, nb_lr, NULL, route_tag,
+                     is_src_dynamic);
 }
 
 static void
@@ -1769,7 +1773,7 @@  add_lb_vip_to_routes_ad(struct hmap *routes_ad, const char *vip_key,
 
     /* Lb vip routes go to <main> route table */
     add_to_routes_ad(routes_ad, vip_ip, plen, nexthop, ROUTE_ORIGIN_LB,
-                     NULL, NULL, NULL, nb_lr, nb_lb, route_tag);
+                     NULL, NULL, NULL, nb_lr, nb_lb, route_tag, false);
 out:
     free(vip_str);
 }
@@ -2187,6 +2191,12 @@  sync_learned_routes(struct ic_context *ctx,
                 nbrec_logical_router_static_route_update_options_setkey(
                     nb_route, "origin", isb_route->origin);
                 free(uuid_s);
+                bool is_src_dynamic = smap_get_bool(&isb_route->external_ids,
+                    "ic-source-dynamic", false);
+                char *ic_source_dynamic_str = is_src_dynamic ?
+                    "true" : "false";
+                nbrec_logical_router_static_route_update_external_ids_setkey(
+                    nb_route, "ic-source-dynamic", ic_source_dynamic_str);
                 nbrec_logical_router_update_static_routes_addvalue(ic_lr->lr,
                     nb_route);
             }
@@ -2243,6 +2253,10 @@  ad_route_sync_external_ids(const struct ic_route_info *route_adv,
                                                      "ic-route-tag");
         }
     }
+
+    char *ic_src_dynamic_str = route_adv->is_src_dynamic ? "true" : "false";
+    icsbrec_route_update_external_ids_setkey(isb_route, "ic-source-dynamic",
+                                             ic_src_dynamic_str);
 }
 
 /* Sync routes from routes_ad to IC-SB. */
@@ -2372,7 +2386,7 @@  build_ts_routes_to_adv(struct ic_context *ctx,
                 add_network_to_routes_ad(routes_ad, lrp->networks[j], lrp,
                                          ts_port_addrs,
                                          &nb_global->options,
-                                         lr, route_tag, ts_lrp);
+                                         lr, route_tag, ts_lrp, false);
             }
         } else {
             /* The router port of the TS port is ignored. */
@@ -2427,7 +2441,7 @@  build_ts_routes_to_adv(struct ic_context *ctx,
         add_network_to_routes_ad(routes_ad, sb_route->ip_prefix, NULL,
                                  ts_port_addrs,
                                  &nb_global->options,
-                                 lr, route_tag, ts_lrp);
+                                 lr, route_tag, ts_lrp, true);
     }
     sbrec_learned_route_index_destroy_row(filter);
 }
diff --git a/northd/en-advertised-route-sync.c b/northd/en-advertised-route-sync.c
index be771391d..be046769f 100644
--- a/northd/en-advertised-route-sync.c
+++ b/northd/en-advertised-route-sync.c
@@ -675,6 +675,8 @@  should_advertise_route(const struct uuidset *host_route_lrps,
         return drr_mode_NAT_is_set(drr);
     case ROUTE_SOURCE_LB:
         return drr_mode_LB_is_set(drr);
+    case ROUTE_SOURCE_IC_DYNAMIC:
+        return drr_mode_IC_DYNAMIC_is_set(drr);
     case ROUTE_SOURCE_LEARNED:
         OVS_NOT_REACHED();
     default:
@@ -745,6 +747,7 @@  advertise_route_track_od(struct advertised_route_sync_data *data,
                            &tracked_op->od->nbr->header_.uuid);
         }
         break;
+    case ROUTE_SOURCE_IC_DYNAMIC:
     case ROUTE_SOURCE_CONNECTED:
     case ROUTE_SOURCE_STATIC:
         break;
diff --git a/northd/northd.c b/northd/northd.c
index b4bb4ba6d..fbb75533c 100644
--- a/northd/northd.c
+++ b/northd/northd.c
@@ -870,6 +870,10 @@  parse_dynamic_routing_redistribute(
             out |= DRRM_LB;
             continue;
         }
+        if (!strcmp(token, "hub-spoke")) {
+            out |= DRRM_IC_DYNAMIC;
+            continue;
+        }
         static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
         VLOG_WARN_RL(&rl,
                      "unknown dynamic-routing-redistribute option '%s' on %s",
@@ -12034,7 +12038,10 @@  parsed_routes_add_static(const struct ovn_datapath *od,
     enum route_source source;
     if (!strcmp(smap_get_def(&route->options, "origin", ""),
                 ROUTE_ORIGIN_CONNECTED)) {
-        source = ROUTE_SOURCE_CONNECTED;
+        bool ic_src_dynamic = smap_get_bool(&route->external_ids,
+                                            "ic-source-dynamic", false);
+        source = ic_src_dynamic ?
+                 ROUTE_SOURCE_IC_DYNAMIC : ROUTE_SOURCE_CONNECTED;
     } else {
         source = ROUTE_SOURCE_STATIC;
     }
@@ -12129,6 +12136,7 @@  route_source_to_offset(enum route_source source)
 {
     switch (source) {
     case ROUTE_SOURCE_CONNECTED:
+    case ROUTE_SOURCE_IC_DYNAMIC:
         return ROUTE_PRIO_OFFSET_CONNECTED;
     case ROUTE_SOURCE_STATIC:
         return ROUTE_PRIO_OFFSET_STATIC;
diff --git a/northd/northd.h b/northd/northd.h
index eb5c15f34..e6ed2cc3e 100644
--- a/northd/northd.h
+++ b/northd/northd.h
@@ -372,7 +372,8 @@  struct mcast_port_info {
     DRR_MODE(CONNECTED_AS_HOST, 1) \
     DRR_MODE(STATIC,            2) \
     DRR_MODE(NAT,               3) \
-    DRR_MODE(LB,                4)
+    DRR_MODE(LB,                4) \
+    DRR_MODE(IC_DYNAMIC,        5)
 
 enum dynamic_routing_redistribute_mode_bits {
 #define DRR_MODE(PROTOCOL, BIT) DRRM_##PROTOCOL##_BIT = BIT,
@@ -826,6 +827,8 @@  enum route_source {
     ROUTE_SOURCE_NAT,
     /* The route is derived from a LB's VIP. */
     ROUTE_SOURCE_LB,
+    /* The route is derived from an ovn-controller and advertised to IC. */
+    ROUTE_SOURCE_IC_DYNAMIC,
 };
 
 struct parsed_route {
diff --git a/northd/ovn-northd.c b/northd/ovn-northd.c
index 7d7568c6f..bc3969053 100644
--- a/northd/ovn-northd.c
+++ b/northd/ovn-northd.c
@@ -905,7 +905,6 @@  main(int argc, char *argv[])
         &nbrec_load_balancer_col_external_ids,
         &nbrec_load_balancer_health_check_col_external_ids,
         &nbrec_logical_router_policy_col_external_ids,
-        &nbrec_logical_router_static_route_col_external_ids,
         &nbrec_meter_col_external_ids,
         &nbrec_meter_band_col_external_ids,
         &nbrec_mirror_col_external_ids,
diff --git a/ovn-nb.xml b/ovn-nb.xml
index 1acbf202b..5f9b47491 100644
--- a/ovn-nb.xml
+++ b/ovn-nb.xml
@@ -3374,6 +3374,13 @@  or
           Logical Switch.
         </p>
 
+        <p>
+          If <code>hub-spoke</code> is in the list then northd will synchronize
+          dynamic routes learned through OVN-IC from other routers into the
+          <ref table="Advertised_Route" db="OVN_Southbound"/> table, enabling
+          hub-and-spoke propagation.
+        </p>
+
         <p>
           This value can be overwritten on a per LRP basis using
           <ref column="options" key="dynamic-routing-redistribute"
@@ -4461,6 +4468,13 @@  or
           via shared Logical Switch.
         </p>
 
+        <p>
+          If <code>hub-spoke</code> is in the list then northd will synchronize
+          dynamic routes learned through OVN-IC from other routers into the
+          <ref table="Advertised_Route" db="OVN_Southbound"/> table, enabling
+          hub-and-spoke propagation.
+        </p>
+
         <p>
           If not set the value from <ref column="options"
           key="dynamic-routing-redistribute" table="Logical_Router"/> on the
@@ -4823,6 +4837,13 @@  or
       database.
     </column>
 
+    <column name="external_ids" key="ic-source-dynamic">
+      <code>ovn-ic</code> populates this key for routes learned from
+      <ref db="OVN_IC_Southbound"/>. The value is <code>true</code> if the
+      learned route originated from dynamic sources (e.g. learned routes)
+      in the advertising availability zone, otherwise <code>false</code>.
+    </column>
+
     <group title="Common Columns">
       <column name="external_ids">
         See <em>External IDs</em> at the beginning of this document.
diff --git a/tests/ovn-ic.at b/tests/ovn-ic.at
index 370a755be..a62d02da0 100644
--- a/tests/ovn-ic.at
+++ b/tests/ovn-ic.at
@@ -4586,3 +4586,95 @@  OVN_CLEANUP_IC([az1], [az2])
 
 AT_CLEANUP
 ])
+
+
+OVN_FOR_EACH_NORTHD([
+AT_SETUP([ovn-ic -- Check ovn-ic adv and learn from SB Learned Route - hub and spoke mode])
+
+ovn_init_ic_db
+
+for i in 1 2; do
+    ovn_start az$i
+    ovn_as az$i
+
+    # Enable route learning at AZ level
+    check ovn-nbctl set nb_global . options:ic-route-learn=true
+    # Enable route advertising at AZ level
+    check ovn-nbctl set nb_global . options:ic-route-adv=true
+done
+
+# Create new transit switches and LRs. Test topology is next:
+#
+#
+# logical router (lr11) - transit switch (ts11) - logical router (lr12)
+#
+#
+
+# Create lr11, lr12 and ts11 and connect them
+for i in 1 2; do
+    ovn_as az$i
+
+    lr=lr1$i
+    check ovn-nbctl lr-add $lr
+
+    ts=ts11
+    check ovn-ic-nbctl --wait=sb --may-exist ts-add $ts
+
+    lrp=lrp-$lr-$ts
+    lsp=lsp-$ts-$lr
+    # Create LRP and connect to TS
+    check ovn-nbctl lrp-add $lr $lrp aa:aa:aa:aa:a1:0$i 169.254.101.$i/24
+    check ovn-nbctl lsp-add-router-port $ts $lsp $lrp
+done
+
+# Create directly-connected route in lr12
+check ovn_as az2 ovn-nbctl lrp-add lr12 lrp-lr12 aa:aa:aa:aa:bb:01 "192.168.0.1/24"
+OVS_WAIT_FOR_OUTPUT([ovn_as az1 ovn-nbctl lr-route-list lr11 | grep 192.168 |
+             grep learned | awk '{print $1, $2}' | sort ], [0], [dnl
+192.168.0.0/24 169.254.101.2
+])
+
+ovn_as az2
+check ovn-nbctl --wait=sb set Logical_Router lr12 option:dynamic-routing=true \
+    option:dynamic-routing-redistribute="connected,static"
+check ovn_as az2 ovn-nbctl --wait=sb lrp-add lr12 lr12-dr1 00:00:00:00:ff:01 10.0.0.1/24
+dr1=$(fetch_column port_binding _uuid logical_port=lr12-dr1)
+datapath=$(fetch_column datapath_binding _uuid external_ids:name=lr12)
+
+check_uuid ovn-sbctl create Learned_Route \
+    datapath=$datapath                    \
+    logical_port=$dr1                     \
+    ip_prefix=192.168.1.0/24              \
+    nexthop=10.0.0.20
+
+# Check Learned_Route adv in ovn-ic
+OVS_WAIT_FOR_OUTPUT([ovn_as az1 ovn-nbctl lr-route-list lr11 |
+             grep learned | awk '{print $1, $2}' | sort ], [0], [dnl
+10.0.0.0/24 169.254.101.2
+192.168.0.0/24 169.254.101.2
+192.168.1.0/24 169.254.101.2
+])
+
+ovn_as az1
+check ovn-nbctl --wait=sb set Logical_Router lr11 option:dynamic-routing=true \
+    option:dynamic-routing-redistribute="connected,static"
+# Advertise just 10.0.0.0/24 and 192.168.0.0/24 routes
+check_row_count Advertised_Route 1 ip_prefix=192.168.0.0/24
+check_row_count Advertised_Route 1 ip_prefix=10.0.0.0/24
+check_row_count Advertised_Route 0 ip_prefix=192.168.1.0/24
+
+
+ovn_as az1
+check ovn-nbctl --wait=sb set Logical_Router lr11 \
+    option:dynamic-routing-redistribute="connected,static,hub-spoke"
+# Advertise just 10.0.0.0/24, 192.168.0.0/24 and 192.168.1.0/24 routes
+# Route 192.168.1.0/24 is learned by DR from other logical routes (lr12)
+# And lr12 Advertised it in ovn-ic. Hub-spoke option enable re-routing in lr11
+check_row_count Advertised_Route 1 ip_prefix=192.168.0.0/24
+check_row_count Advertised_Route 1 ip_prefix=10.0.0.0/24
+check_row_count Advertised_Route 1 ip_prefix=192.168.1.0/24
+
+OVN_CLEANUP_IC([az1], [az2])
+
+AT_CLEANUP
+])