diff mbox series

[ovs-dev,v4,02/11] route-table: Introduce multi-table route lookup.

Message ID 20251027124259.3395209-3-dchumak@nvidia.com
State Changes Requested
Delegated to: Ilya Maximets
Headers show
Series ovs-router: Multi-table routing infrastructure. | 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-_Build_and_Test success github build: passed

Commit Message

Dima Chumak Oct. 27, 2025, 12:42 p.m. UTC
Introduce support for route lookup across several routing tables
following a priority from highest to lowest.

To properly prioritize route lookup order, the single router table in
OVS is now split into three tables: local, main and default.
Corresponding routing rules are created by default, where local table
has the highest priority and main and default tables have two lowest
priorities respectively. All non-standard tables can be added with a
priority in between local and main.

All routing tables are created by reading the Routing Policy Database
(RPDB) from the kernel, on systems that support it, and importing only
those tables which are referenced in the RPDB rules with a table lookup
action. The table IDs and rule priority are copied from the kernel RPDB
as is.

Current implementation only supports RPDB rules with a source address
selector, in form of '[not] from IP' match.

Signed-off-by: Dima Chumak <dchumak@nvidia.com>
---
 lib/netdev-dummy.c           |   4 +-
 lib/ovs-router.c             | 223 ++++++++++++++++++++++++------
 lib/ovs-router.h             |   7 +
 lib/packets.c                |  19 +++
 lib/packets.h                |   2 +
 lib/route-table.c            | 256 ++++++++++++++++++++++++++++++++---
 lib/route-table.h            |  22 ++-
 tests/ovs-router.at          |  10 --
 tests/test-lib-route-table.c |   5 +-
 9 files changed, 469 insertions(+), 79 deletions(-)

Comments

Ilya Maximets Dec. 9, 2025, 9:51 p.m. UTC | #1
On 10/27/25 1:42 PM, Dima Chumak via dev wrote:
> Introduce support for route lookup across several routing tables
> following a priority from highest to lowest.
> 
> To properly prioritize route lookup order, the single router table in
> OVS is now split into three tables: local, main and default.
> Corresponding routing rules are created by default, where local table
> has the highest priority and main and default tables have two lowest
> priorities respectively. All non-standard tables can be added with a
> priority in between local and main.
> 
> All routing tables are created by reading the Routing Policy Database
> (RPDB) from the kernel, on systems that support it, and importing only
> those tables which are referenced in the RPDB rules with a table lookup
> action. The table IDs and rule priority are copied from the kernel RPDB
> as is.
> 
> Current implementation only supports RPDB rules with a source address
> selector, in form of '[not] from IP' match.
> 
> Signed-off-by: Dima Chumak <dchumak@nvidia.com>
> ---
>  lib/netdev-dummy.c           |   4 +-
>  lib/ovs-router.c             | 223 ++++++++++++++++++++++++------
>  lib/ovs-router.h             |   7 +
>  lib/packets.c                |  19 +++
>  lib/packets.h                |   2 +
>  lib/route-table.c            | 256 ++++++++++++++++++++++++++++++++---
>  lib/route-table.h            |  22 ++-
>  tests/ovs-router.at          |  10 --
>  tests/test-lib-route-table.c |   5 +-
>  9 files changed, 469 insertions(+), 79 deletions(-)
> 
> diff --git a/lib/netdev-dummy.c b/lib/netdev-dummy.c
> index bad86d3c4c76..5e75012cd57a 100644
> --- a/lib/netdev-dummy.c
> +++ b/lib/netdev-dummy.c
> @@ -2223,7 +2223,7 @@ netdev_dummy_ip4addr(struct unixctl_conn *conn, int argc OVS_UNUSED,
>  
>              in6_addr_set_mapped_ipv4(&ip6, ip.s_addr);
>              /* Insert local route entry for the new address. */
> -            ovs_router_force_insert(CLS_MAIN, 0, &ip6, 32 + 96, true,
> +            ovs_router_force_insert(CLS_LOCAL, 0, &ip6, 32 + 96, true,
>                                      argv[1], &in6addr_any, &ip6);
>              /* Insert network route entry for the new address. */
>              ovs_router_force_insert(CLS_MAIN, 0, &ip6, plen + 96, false,
> @@ -2260,7 +2260,7 @@ netdev_dummy_ip6addr(struct unixctl_conn *conn, int argc OVS_UNUSED,
>              netdev_dummy_add_in6(netdev, &ip6, &mask);
>  
>              /* Insert local route entry for the new address. */
> -            ovs_router_force_insert(CLS_MAIN, 0, &ip6, 128, true, argv[1],
> +            ovs_router_force_insert(CLS_LOCAL, 0, &ip6, 128, true, argv[1],
>                                      &in6addr_any, &ip6);
>              /* Insert network route entry for the new address. */
>              ovs_router_force_insert(CLS_MAIN, 0, &ip6, plen, false, argv[1],
> diff --git a/lib/ovs-router.c b/lib/ovs-router.c
> index 70644f7bd4f2..808e8b244c35 100644
> --- a/lib/ovs-router.c
> +++ b/lib/ovs-router.c
> @@ -43,6 +43,7 @@
>  #include "seq.h"
>  #include "ovs-thread.h"
>  #include "route-table.h"
> +#include "pvector.h"
>  #include "tnl-ports.h"
>  #include "unixctl.h"
>  #include "util.h"
> @@ -57,10 +58,20 @@ struct clsmap_node {
>      struct classifier cls;
>  };
>  
> +struct router_rule {
> +    uint32_t prio;
> +    bool invert;
> +    bool ipv4;
> +    uint8_t src_prefix;
> +    struct in6_addr from_addr;
> +    uint32_t lookup_table;
> +};
> +
>  static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
>  
>  static struct ovs_mutex mutex = OVS_MUTEX_INITIALIZER;
>  static struct cmap clsmap = CMAP_INITIALIZER;
> +static struct pvector rules;
>  
>  /* By default, use the system routing table.  For system-independent testing,
>   * the unit tests disable using the system routing table. */
> @@ -162,18 +173,20 @@ ovs_router_lookup(uint32_t mark, const struct in6_addr *ip6_dst,
>                    struct in6_addr *src, struct in6_addr *gw)
>  {
>      struct flow flow = {.ipv6_dst = *ip6_dst, .pkt_mark = mark};
> -    struct classifier *cls_main = cls_find(CLS_MAIN);
> -    const struct cls_rule *cr;
> -
> -    if (!cls_main) {
> -        return false;
> -    }
> +    const struct in6_addr *from_src = src;
> +    const struct cls_rule *cr = NULL;
> +    struct router_rule *rule;
>  
>      if (src && ipv6_addr_is_set(src)) {
> -        const struct cls_rule *cr_src;
>          struct flow flow_src = {.ipv6_dst = *src, .pkt_mark = mark};
> +        struct classifier *cls_local = cls_find(CLS_LOCAL);
> +        const struct cls_rule *cr_src;
> +
> +        if (!cls_local) {
> +            return false;
> +        }
>  
> -        cr_src = classifier_lookup(cls_main, OVS_VERSION_MAX, &flow_src,
> +        cr_src = classifier_lookup(cls_local, OVS_VERSION_MAX, &flow_src,
>                                     NULL, NULL);
>          if (cr_src) {
>              struct ovs_router_entry *p_src = ovs_router_entry_cast(cr_src);
> @@ -185,7 +198,44 @@ ovs_router_lookup(uint32_t mark, const struct in6_addr *ip6_dst,
>          }
>      }
>  
> -    cr = classifier_lookup(cls_main, OVS_VERSION_MAX, &flow, NULL, NULL);
> +    if (!from_src) {
> +        from_src = &in6addr_any;
> +    }
> +
> +    PVECTOR_FOR_EACH (rule, &rules) {
> +        uint8_t plen = rule->ipv4 ? rule->src_prefix + 96 : rule->src_prefix;
> +        bool matched = (IN6_IS_ADDR_V4MAPPED(from_src) ? rule->ipv4 : true) &&
> +                       (!rule->src_prefix ||
> +                        ipv6_addr_equals_masked(&rule->from_addr, from_src,
> +                                                plen));
> +
> +        if (rule->invert) {
> +            matched = !matched;
> +        }

So, if we have a negative ipv6 rule, e.g. "not from=fc08::/64" and we have
an ipv4 source address, we'll get IN6_IS_ADDR_V4MAPPED(from_src) --> true,
rule->ipv4 --> false, so matched = false && (...), which is false.  Then
we check the inversion and suddenly get matched == true and go to lookup
a route in the table.  If somehow that table contains ipv4 routes, we can
find them, which should not be happening.

We should not proceed here if the address family doesn't match the rule,
otherwise all inverted rules will match traffic from a wrong family.

There is also a problem with in6addr_any here, which is set when there is
no source address specified.  This complicates the check as it's not a
ipv4 mapped address, even if the destination we're looking up is ipv4.
This may also lead to a match for a rule from the wrong family.

I'm guessing, this is done this way because we're also skipping the dump
of the ipv6 default rules and we're not creating ipv6 default rules in
the init_standard_rules() either.  So, somehow this code expects that
standard rules are not family specific.  This will cause issues if we try
to avoid lookups for the wrong family.   One more little problem here is
that there is no standard rule for the "default" table for ipv6 in Linux,
only for local and main.  This at least is inconsistent.

To fix that, I think, we actually need to add standard rules for both
families and not skip ones dumped from the kernel.  Then we can filter
and avoid matching rules from a wrong family during the lookup.

> +
> +        if (matched) {
> +            struct classifier *cls = cls_find(rule->lookup_table);
> +
> +            if (!cls) {
> +                /* A rule can be added before the table is created */

nit: Missing period at the end of the sentence.

> +                continue;
> +            }
> +            cr = classifier_lookup(cls, OVS_VERSION_MAX, &flow, NULL,
> +                                   NULL);
> +            if (cr) {
> +                struct ovs_router_entry *p = ovs_router_entry_cast(cr);
> +                /* Avoid matching mapped IPv4 of a packet against default IPv6
> +                 * route entry. Either packet dst is IPv6 or both packet and

nit: Double spaces between sentences.

> +                 * route entry dst are mapped IPv4.
> +                 */
> +                if (!IN6_IS_ADDR_V4MAPPED(ip6_dst) ||
> +                    IN6_IS_ADDR_V4MAPPED(&p->nw_addr)) {
> +                    break;
> +                }
> +            }
> +        }
> +    }
> +

<snip>

> +static int
> +rule_pvec_prio(uint32_t prio)
> +{
> +    /* Invert the priority of a pvector entry to reverse the default sorting
> +     * order (descending) to maintain the standard rules semantic where 0 is
> +     * the highest priority and UINT_MAX is the lowest. The mapping is the

nit: Double spaces.

So, what I'm proposing is something like this:

diff --git a/lib/ovs-router.c b/lib/ovs-router.c
index 99660aa5d..3655451f4 100644
--- a/lib/ovs-router.c
+++ b/lib/ovs-router.c
@@ -200,15 +200,24 @@ ovs_router_lookup(uint32_t mark, const struct in6_addr *ip6_dst,
     }
 
     if (!from_src) {
-        from_src = &in6addr_any;
+        if (IN6_IS_ADDR_V4MAPPED(ip6_dst)) {
+            from_src = &in6addr_v4mapped_any;
+        } else {
+            from_src = &in6addr_any;
+        }
     }
 
     PVECTOR_FOR_EACH (rule, &rules) {
         uint8_t plen = rule->ipv4 ? rule->src_prefix + 96 : rule->src_prefix;
-        bool matched = (IN6_IS_ADDR_V4MAPPED(from_src) ? rule->ipv4 : true) &&
-                       (!rule->src_prefix ||
-                        ipv6_addr_equals_masked(&rule->from_addr, from_src,
-                                                plen));
+        bool matched;
+
+        if ((IN6_IS_ADDR_V4MAPPED(from_src) && !rule->ipv4) ||
+            (!IN6_IS_ADDR_V4MAPPED(from_src) && rule->ipv4)) {
+            continue;
+        }
+
+        matched = (!rule->src_prefix ||
+                   ipv6_addr_equals_masked(&rule->from_addr, from_src, plen));
 
         if (rule->invert) {
             matched = !matched;
@@ -838,9 +847,17 @@ static void
 init_standard_rules(void)
 {
     /* Add default rules using same priorities as Linux kernel does. */
-    ovs_router_rule_add(0, false, 0, &in6addr_any, CLS_LOCAL, true);
-    ovs_router_rule_add(0x7FFE, false, 0, &in6addr_any, CLS_MAIN, true);
-    ovs_router_rule_add(0x7FFF, false, 0, &in6addr_any, CLS_DEFAULT, true);
+    ovs_router_rule_add(0, false, 0,
+                        &in6addr_v4mapped_any, CLS_LOCAL, true);
+    ovs_router_rule_add(0x7FFE, false, 0,
+                        &in6addr_v4mapped_any, CLS_MAIN, true);
+    ovs_router_rule_add(0x7FFF, false, 0,
+                        &in6addr_v4mapped_any, CLS_DEFAULT, true);
+
+    ovs_router_rule_add(0, false, 0,
+                        &in6addr_any, CLS_LOCAL, false);
+    ovs_router_rule_add(0x7FFE, false, 0,
+                        &in6addr_any, CLS_MAIN, false);
 }
 
 static void
diff --git a/lib/packets.c b/lib/packets.c
index eb3f7d837..c4a7c45a9 100644
--- a/lib/packets.c
+++ b/lib/packets.c
@@ -38,6 +38,7 @@
 const struct in6_addr in6addr_exact = IN6ADDR_EXACT_INIT;
 const struct in6_addr in6addr_all_hosts = IN6ADDR_ALL_HOSTS_INIT;
 const struct in6_addr in6addr_all_routers = IN6ADDR_ALL_ROUTERS_INIT;
+const struct in6_addr in6addr_v4mapped_any = IN6ADDR_V4MAPPED_ANY_INIT;
 
 struct in6_addr
 flow_tnl_dst(const struct flow_tnl *tnl)
diff --git a/lib/packets.h b/lib/packets.h
index 9a974f6f8..adae3b71b 100644
--- a/lib/packets.h
+++ b/lib/packets.h
@@ -1174,6 +1174,11 @@ extern const struct in6_addr in6addr_all_routers;
 #define IN6ADDR_ALL_ROUTERS_INIT { { { 0xff,0x02,0x00,0x00,0x00,0x00,0x00,0x00, \
                                        0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x02 } } }
 
+extern const struct in6_addr in6addr_v4mapped_any;
+#define IN6ADDR_V4MAPPED_ANY_INIT \
+    { { { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \
+          0x00, 0x00, 0xff, 0xff, 0x00, 0x00, 0x00, 0x00 } } }
+
 static inline bool ipv6_addr_equals(const struct in6_addr *a,
                                     const struct in6_addr *b)
 {
diff --git a/lib/route-table.c b/lib/route-table.c
index 40be2762c..917469642 100644
--- a/lib/route-table.c
+++ b/lib/route-table.c
@@ -95,7 +95,6 @@ static void route_table_handle_msg(const struct route_table_msg *, void *aux,
 static void route_table_change(struct route_table_msg *, void *aux);
 static void rules_change(const struct route_table_msg *, void *aux);
 static void route_map_clear(void);
-static bool is_standard_table_id(uint32_t table_id);
 
 static void name_table_init(void);
 static void name_table_change(const struct rtnetlink_change *, void *);
@@ -316,7 +315,7 @@ static int
 rule_parse(struct ofpbuf *buf, void *change_)
 {
     struct route_table_msg *change = change_;
-    bool ipv4 = false, from_addr_any = false;
+    bool ipv4 = false;
     bool parsed;
 
     static const struct nl_policy policy[] = {
@@ -389,7 +388,6 @@ rule_parse(struct ofpbuf *buf, void *change_)
             } else {
                 change->rud.from_addr = in6addr_any;
             }
-            from_addr_any = true;
         }
 
         if (attrs[FRA_TABLE]) {
@@ -406,14 +404,6 @@ rule_parse(struct ofpbuf *buf, void *change_)
         return 0;
     }
 
-    /* Skip standard IPv6 rules for local and main tables to avoid duplicates
-     * as they end up being the same as the similar IPv4 rules.
-     */
-    if (!ipv4 && (change->rud.prio == 0 || change->rud.prio == 0x7FFE) &&
-        from_addr_any && is_standard_table_id(change->rud.lookup_table)) {
-        change->relevant = false;
-    }
-
     /* Check if there are any additional attributes that aren't supported
      * currently by OVS rule-based route lookup. */
     if (change->relevant) {
@@ -732,15 +722,6 @@ route_table_parse(struct ofpbuf *buf, void *change)
                                nlmsg, rtm, NULL, change);
 }
 
-static bool
-is_standard_table_id(uint32_t table_id)
-{
-    return !table_id
-           || table_id == RT_TABLE_DEFAULT
-           || table_id == RT_TABLE_MAIN
-           || table_id == RT_TABLE_LOCAL;
-}
-
 static void
 route_table_change(struct route_table_msg *change, void *aux OVS_UNUSED)
 {
---

This should solve the issue of matching on the rule from a wrong family.
But also makes a code a bit easier to understand, IMO.

And there seems to be no tests covering inverted matches, we should add
some.  Including one for the wrong ip family match.

What do you think?  Did I miss something?


On side effect of this change though is that we now get both ipv4 and ipv6
standard rules in the rule/show command and it doesn't look particularly
great.  The 'Cached' vs 'Cached6' makes it understandable, but doesn't make
it look better.  Since we already have the -6 argument for the add/del
commands for the rules, maybe we should add -6 argument to the rules/show?
This will be consistent with the ip command for the kernel as well.

Best regards, Ilya Maximets.
diff mbox series

Patch

diff --git a/lib/netdev-dummy.c b/lib/netdev-dummy.c
index bad86d3c4c76..5e75012cd57a 100644
--- a/lib/netdev-dummy.c
+++ b/lib/netdev-dummy.c
@@ -2223,7 +2223,7 @@  netdev_dummy_ip4addr(struct unixctl_conn *conn, int argc OVS_UNUSED,
 
             in6_addr_set_mapped_ipv4(&ip6, ip.s_addr);
             /* Insert local route entry for the new address. */
-            ovs_router_force_insert(CLS_MAIN, 0, &ip6, 32 + 96, true,
+            ovs_router_force_insert(CLS_LOCAL, 0, &ip6, 32 + 96, true,
                                     argv[1], &in6addr_any, &ip6);
             /* Insert network route entry for the new address. */
             ovs_router_force_insert(CLS_MAIN, 0, &ip6, plen + 96, false,
@@ -2260,7 +2260,7 @@  netdev_dummy_ip6addr(struct unixctl_conn *conn, int argc OVS_UNUSED,
             netdev_dummy_add_in6(netdev, &ip6, &mask);
 
             /* Insert local route entry for the new address. */
-            ovs_router_force_insert(CLS_MAIN, 0, &ip6, 128, true, argv[1],
+            ovs_router_force_insert(CLS_LOCAL, 0, &ip6, 128, true, argv[1],
                                     &in6addr_any, &ip6);
             /* Insert network route entry for the new address. */
             ovs_router_force_insert(CLS_MAIN, 0, &ip6, plen, false, argv[1],
diff --git a/lib/ovs-router.c b/lib/ovs-router.c
index 70644f7bd4f2..808e8b244c35 100644
--- a/lib/ovs-router.c
+++ b/lib/ovs-router.c
@@ -43,6 +43,7 @@ 
 #include "seq.h"
 #include "ovs-thread.h"
 #include "route-table.h"
+#include "pvector.h"
 #include "tnl-ports.h"
 #include "unixctl.h"
 #include "util.h"
@@ -57,10 +58,20 @@  struct clsmap_node {
     struct classifier cls;
 };
 
+struct router_rule {
+    uint32_t prio;
+    bool invert;
+    bool ipv4;
+    uint8_t src_prefix;
+    struct in6_addr from_addr;
+    uint32_t lookup_table;
+};
+
 static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
 
 static struct ovs_mutex mutex = OVS_MUTEX_INITIALIZER;
 static struct cmap clsmap = CMAP_INITIALIZER;
+static struct pvector rules;
 
 /* By default, use the system routing table.  For system-independent testing,
  * the unit tests disable using the system routing table. */
@@ -162,18 +173,20 @@  ovs_router_lookup(uint32_t mark, const struct in6_addr *ip6_dst,
                   struct in6_addr *src, struct in6_addr *gw)
 {
     struct flow flow = {.ipv6_dst = *ip6_dst, .pkt_mark = mark};
-    struct classifier *cls_main = cls_find(CLS_MAIN);
-    const struct cls_rule *cr;
-
-    if (!cls_main) {
-        return false;
-    }
+    const struct in6_addr *from_src = src;
+    const struct cls_rule *cr = NULL;
+    struct router_rule *rule;
 
     if (src && ipv6_addr_is_set(src)) {
-        const struct cls_rule *cr_src;
         struct flow flow_src = {.ipv6_dst = *src, .pkt_mark = mark};
+        struct classifier *cls_local = cls_find(CLS_LOCAL);
+        const struct cls_rule *cr_src;
+
+        if (!cls_local) {
+            return false;
+        }
 
-        cr_src = classifier_lookup(cls_main, OVS_VERSION_MAX, &flow_src,
+        cr_src = classifier_lookup(cls_local, OVS_VERSION_MAX, &flow_src,
                                    NULL, NULL);
         if (cr_src) {
             struct ovs_router_entry *p_src = ovs_router_entry_cast(cr_src);
@@ -185,7 +198,44 @@  ovs_router_lookup(uint32_t mark, const struct in6_addr *ip6_dst,
         }
     }
 
-    cr = classifier_lookup(cls_main, OVS_VERSION_MAX, &flow, NULL, NULL);
+    if (!from_src) {
+        from_src = &in6addr_any;
+    }
+
+    PVECTOR_FOR_EACH (rule, &rules) {
+        uint8_t plen = rule->ipv4 ? rule->src_prefix + 96 : rule->src_prefix;
+        bool matched = (IN6_IS_ADDR_V4MAPPED(from_src) ? rule->ipv4 : true) &&
+                       (!rule->src_prefix ||
+                        ipv6_addr_equals_masked(&rule->from_addr, from_src,
+                                                plen));
+
+        if (rule->invert) {
+            matched = !matched;
+        }
+
+        if (matched) {
+            struct classifier *cls = cls_find(rule->lookup_table);
+
+            if (!cls) {
+                /* A rule can be added before the table is created */
+                continue;
+            }
+            cr = classifier_lookup(cls, OVS_VERSION_MAX, &flow, NULL,
+                                   NULL);
+            if (cr) {
+                struct ovs_router_entry *p = ovs_router_entry_cast(cr);
+                /* Avoid matching mapped IPv4 of a packet against default IPv6
+                 * route entry. Either packet dst is IPv6 or both packet and
+                 * route entry dst are mapped IPv4.
+                 */
+                if (!IN6_IS_ADDR_V4MAPPED(ip6_dst) ||
+                    IN6_IS_ADDR_V4MAPPED(&p->nw_addr)) {
+                    break;
+                }
+            }
+        }
+    }
+
     if (cr) {
         struct ovs_router_entry *p = ovs_router_entry_cast(cr);
 
@@ -652,43 +702,47 @@  out:
 static void
 ovs_router_show_text(struct ds *ds)
 {
+    struct classifier *std_cls[3];
     struct ovs_router_entry *rt;
-    struct classifier *cls_main;
 
-    cls_main = cls_find(CLS_MAIN);
-    if (!cls_main) {
-        return;
-    }
+    std_cls[0] = cls_find(CLS_LOCAL);
+    std_cls[1] = cls_find(CLS_MAIN);
+    std_cls[2] = cls_find(CLS_DEFAULT);
 
     ds_put_format(ds, "Route Table:\n");
-    CLS_FOR_EACH (rt, cr, cls_main) {
-        uint8_t plen;
-        if (rt->priority == rt->plen || rt->local) {
-            ds_put_format(ds, "Cached: ");
-        } else {
-            ds_put_format(ds, "User: ");
-        }
-        ipv6_format_mapped(&rt->nw_addr, ds);
-        plen = rt->plen;
-        if (IN6_IS_ADDR_V4MAPPED(&rt->nw_addr)) {
-            plen -= 96;
-        }
-        ds_put_format(ds, "/%"PRIu8, plen);
-        if (rt->mark) {
-            ds_put_format(ds, " MARK %"PRIu32, rt->mark);
+    for (int i = 0; i < ARRAY_SIZE(std_cls); i++) {
+        if (!std_cls[i]) {
+            continue;
         }
+        CLS_FOR_EACH (rt, cr, std_cls[i]) {
+            uint8_t plen;
+            if (rt->priority == rt->plen || rt->local) {
+                ds_put_format(ds, "Cached: ");
+            } else {
+                ds_put_format(ds, "User: ");
+            }
+            ipv6_format_mapped(&rt->nw_addr, ds);
+            plen = rt->plen;
+            if (IN6_IS_ADDR_V4MAPPED(&rt->nw_addr)) {
+                plen -= 96;
+            }
+            ds_put_format(ds, "/%"PRIu8, plen);
+            if (rt->mark) {
+                ds_put_format(ds, " MARK %"PRIu32, rt->mark);
+            }
 
-        ds_put_format(ds, " dev %s", rt->output_netdev);
-        if (ipv6_addr_is_set(&rt->gw)) {
-            ds_put_format(ds, " GW ");
-            ipv6_format_mapped(&rt->gw, ds);
-        }
-        ds_put_format(ds, " SRC ");
-        ipv6_format_mapped(&rt->src_addr, ds);
-        if (rt->local) {
-            ds_put_format(ds, " local");
+            ds_put_format(ds, " dev %s", rt->output_netdev);
+            if (ipv6_addr_is_set(&rt->gw)) {
+                ds_put_format(ds, " GW ");
+                ipv6_format_mapped(&rt->gw, ds);
+            }
+            ds_put_format(ds, " SRC ");
+            ipv6_format_mapped(&rt->src_addr, ds);
+            if (rt->local) {
+                ds_put_format(ds, " local");
+            }
+            ds_put_format(ds, "\n");
         }
-        ds_put_format(ds, "\n");
     }
 }
 
@@ -779,23 +833,114 @@  ovs_router_flush(bool flush_all)
     ovs_mutex_unlock(&mutex);
 }
 
+static void
+init_standard_rules(void)
+{
+    /* Add default rules using same priorities as Linux kernel does. */
+    ovs_router_rule_add(0, false, 0, &in6addr_any, CLS_LOCAL, true);
+    ovs_router_rule_add(0x7FFE, false, 0, &in6addr_any, CLS_MAIN, true);
+    ovs_router_rule_add(0x7FFF, false, 0, &in6addr_any, CLS_DEFAULT, true);
+}
+
+static void
+rule_destroy_cb(struct router_rule *rule)
+{
+    ovsrcu_postpone(free, rule);
+}
+
+static void
+ovs_router_rules_flush_protected(void)
+{
+    struct router_rule *rule;
+
+    PVECTOR_FOR_EACH (rule, &rules) {
+        pvector_remove(&rules, rule);
+        ovsrcu_postpone(rule_destroy_cb, rule);
+    }
+    pvector_publish(&rules);
+}
+
+void
+ovs_router_rules_flush(void)
+{
+    ovs_mutex_lock(&mutex);
+    ovs_router_rules_flush_protected();
+    ovs_mutex_unlock(&mutex);
+}
+
 static void
 ovs_router_flush_handler(void *aux OVS_UNUSED)
 {
     ovs_mutex_lock(&mutex);
+    ovs_router_rules_flush_protected();
     ovs_router_flush_protected(true);
+    pvector_destroy(&rules);
     ovs_assert(cmap_is_empty(&clsmap));
     cmap_destroy(&clsmap);
     cmap_init(&clsmap);
     ovs_mutex_unlock(&mutex);
 }
 
+bool
+ovs_router_is_referenced(uint32_t table)
+{
+    struct router_rule *rule;
+
+    PVECTOR_FOR_EACH (rule, &rules) {
+        if (rule->lookup_table == table) {
+            return true;
+        }
+    }
+    return false;
+}
+
+static int
+rule_pvec_prio(uint32_t prio)
+{
+    /* Invert the priority of a pvector entry to reverse the default sorting
+     * order (descending) to maintain the standard rules semantic where 0 is
+     * the highest priority and UINT_MAX is the lowest. The mapping is the
+     * following:
+     *
+     *     0        -> INT_MAX
+     *     INT_MAX  -> 0
+     *     UINT_MAX -> INT_MIN
+     */
+    if (prio <= INT_MAX) {
+        return -(INT_MIN + (int) prio + 1);
+    } else {
+        return -((int) (prio - INT_MAX - 1)) - 1;
+    }
+}
+
+void
+ovs_router_rule_add(uint32_t prio, bool invert, uint8_t src_len,
+                    const struct in6_addr *from, uint32_t lookup_table,
+                    bool ipv4)
+{
+    struct router_rule *rule = xzalloc(sizeof *rule);
+
+    rule->prio = prio;
+    rule->invert = invert;
+    rule->src_prefix = src_len;
+    rule->from_addr = *from;
+    rule->lookup_table = lookup_table;
+    rule->ipv4 = ipv4;
+
+    pvector_insert(&rules, rule, rule_pvec_prio(prio));
+    pvector_publish(&rules);
+}
+
 void
 ovs_router_init(void)
 {
     static struct ovsthread_once once = OVSTHREAD_ONCE_INITIALIZER;
 
     if (ovsthread_once_start(&once)) {
+        ovs_mutex_lock(&mutex);
+        pvector_init(&rules);
+        init_standard_rules();
+        ovs_mutex_unlock(&mutex);
         fatal_signal_add_hook(ovs_router_flush_handler, NULL, NULL, true);
         unixctl_command_register("ovs/route/add",
                                  "ip/plen dev [gw] "
diff --git a/lib/ovs-router.h b/lib/ovs-router.h
index f4e6487d3dc6..62d306423ce6 100644
--- a/lib/ovs-router.h
+++ b/lib/ovs-router.h
@@ -27,7 +27,9 @@  extern "C" {
 #endif
 
 enum {
+    CLS_DEFAULT = 253,
     CLS_MAIN = 254,
+    CLS_LOCAL = 255,
     CLS_ALL = UINT32_MAX,
 };
 
@@ -35,6 +37,7 @@  bool ovs_router_lookup(uint32_t mark, const struct in6_addr *ip_dst,
                        char output_netdev[],
                        struct in6_addr *src, struct in6_addr *gw);
 void ovs_router_init(void);
+bool ovs_router_is_referenced(uint32_t table);
 void ovs_router_insert(uint32_t table, uint32_t mark,
                        const struct in6_addr *ip_dst,
                        uint8_t plen, bool local,
@@ -46,7 +49,11 @@  void ovs_router_force_insert(uint32_t table, uint32_t mark,
                              const char output_netdev[],
                              const struct in6_addr *gw,
                              const struct in6_addr *prefsrc);
+void ovs_router_rule_add(uint32_t prio, bool invert, uint8_t src_len,
+                         const struct in6_addr *from, uint32_t lookup_table,
+                         bool ipv4);
 void ovs_router_flush(bool flush_all);
+void ovs_router_rules_flush(void);
 
 void ovs_router_disable_system_routing_table(void);
 
diff --git a/lib/packets.c b/lib/packets.c
index a0bb2ad482f1..eb3f7d837d37 100644
--- a/lib/packets.c
+++ b/lib/packets.c
@@ -1081,6 +1081,25 @@  ipv6_is_cidr(const struct in6_addr *netmask)
     return true;
 }
 
+bool
+ipv6_addr_equals_masked(const struct in6_addr *a, const struct in6_addr *b,
+                        int plen)
+{
+    struct in6_addr mask;
+    struct in6_addr ma;
+    struct in6_addr mb;
+
+    if (plen == 128) {
+        return ipv6_addr_equals(a, b);
+    }
+
+    mask = ipv6_create_mask(plen);
+    ma = ipv6_addr_bitand(a, &mask);
+    mb = ipv6_addr_bitand(b, &mask);
+
+    return ipv6_addr_equals(&ma, &mb);
+}
+
 /* Populates 'b' with an Ethernet II packet headed with the given 'eth_dst',
  * 'eth_src' and 'eth_type' parameters.  A payload of 'size' bytes is allocated
  * in 'b' and returned.  This payload may be populated with appropriate
diff --git a/lib/packets.h b/lib/packets.h
index 6eba07700a69..9a974f6f8dab 100644
--- a/lib/packets.h
+++ b/lib/packets.h
@@ -1608,6 +1608,8 @@  bool ipv6_is_zero(const struct in6_addr *a);
 struct in6_addr ipv6_create_mask(int mask);
 int ipv6_count_cidr_bits(const struct in6_addr *netmask);
 bool ipv6_is_cidr(const struct in6_addr *netmask);
+bool ipv6_addr_equals_masked(const struct in6_addr *a,
+                             const struct in6_addr *b, int plen);
 
 bool ipv6_parse(const char *s, struct in6_addr *ip);
 char *ipv6_parse_masked(const char *s, struct in6_addr *ipv6,
diff --git a/lib/route-table.c b/lib/route-table.c
index 0e77a9c9325e..40be2762c2b4 100644
--- a/lib/route-table.c
+++ b/lib/route-table.c
@@ -23,6 +23,7 @@ 
 #include <netinet/in.h>
 #include <arpa/inet.h>
 #include <sys/socket.h>
+#include <linux/fib_rules.h>
 #include <linux/rtnetlink.h>
 #include <net/if.h>
 
@@ -40,9 +41,13 @@ 
 #include "tnl-ports.h"
 #include "openvswitch/vlog.h"
 
-/* Linux 2.6.36 added RTA_MARK, so define it just in case we're building with
- * old headers.  (We can't test for it with #ifdef because it's an enum.) */
-#define RTA_MARK 16
+/* Below constants were added in different Linux versions, so define them just
+ * in case we're building with old headers. (We can't test for it with #ifdef
+ * because it's an enum.) */
+#define RTA_MARK 16 /* Linux 2.6.36 */
+#define FRA_SUPPRESS_PREFIXLEN 14 /* Linux 3.12 */
+#define FRA_TABLE 15 /* Linux 2.6.19 */
+#define FRA_PROTOCOL 21 /* Linux 4.17 */
 
 /* Linux 4.1 added RTA_VIA. */
 #ifndef HAVE_RTA_VIA
@@ -57,7 +62,9 @@  VLOG_DEFINE_THIS_MODULE(route_table);
 
 COVERAGE_DEFINE(route_table_dump);
 
+BUILD_ASSERT_DECL((enum rt_class_t) CLS_DEFAULT == RT_TABLE_DEFAULT);
 BUILD_ASSERT_DECL((enum rt_class_t) CLS_MAIN == RT_TABLE_MAIN);
+BUILD_ASSERT_DECL((enum rt_class_t) CLS_LOCAL == RT_TABLE_LOCAL);
 
 static struct ovs_mutex route_table_mutex = OVS_MUTEX_INITIALIZER;
 static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 20);
@@ -70,14 +77,25 @@  static struct nln *nln = NULL;
 static struct route_table_msg nln_rtmsg_change;
 static struct nln_notifier *route_notifier = NULL;
 static struct nln_notifier *route6_notifier = NULL;
+static struct nln_notifier *rule_notifier = NULL;
+static struct nln_notifier *rule6_notifier = NULL;
 static struct nln_notifier *name_notifier = NULL;
 
 static bool route_table_valid = false;
+static bool rules_valid = false;
+
+static int route_nln_parse(struct ofpbuf *, void *change);
+
+static void rule_handle_msg(const struct route_table_msg *);
+static int rule_parse(struct ofpbuf *, void *change);
 
 static void route_table_reset(void);
-static void route_table_handle_msg(const struct route_table_msg *, void *aux);
+static void route_table_handle_msg(const struct route_table_msg *, void *aux,
+                                   uint32_t table);
 static void route_table_change(struct route_table_msg *, void *aux);
+static void rules_change(const struct route_table_msg *, void *aux);
 static void route_map_clear(void);
+static bool is_standard_table_id(uint32_t table_id);
 
 static void name_table_init(void);
 static void name_table_change(const struct rtnetlink_change *, void *);
@@ -116,9 +134,11 @@  route_table_init(void)
     ovs_assert(!nln);
     ovs_assert(!route_notifier);
     ovs_assert(!route6_notifier);
+    ovs_assert(!rule_notifier);
+    ovs_assert(!rule6_notifier);
 
     ovs_router_init();
-    nln = nln_create(NETLINK_ROUTE, route_table_parse, &nln_rtmsg_change);
+    nln = nln_create(NETLINK_ROUTE, route_nln_parse, &nln_rtmsg_change);
 
     route_notifier =
         nln_notifier_create(nln, RTNLGRP_IPV4_ROUTE,
@@ -127,6 +147,13 @@  route_table_init(void)
         nln_notifier_create(nln, RTNLGRP_IPV6_ROUTE,
                             (nln_notify_func *) route_table_change, NULL);
 
+    rule_notifier =
+        nln_notifier_create(nln, RTNLGRP_IPV4_RULE,
+                            (nln_notify_func *) rules_change, NULL);
+    rule6_notifier =
+        nln_notifier_create(nln, RTNLGRP_IPV6_RULE,
+                            (nln_notify_func *) rules_change, NULL);
+
     route_table_reset();
     name_table_init();
 
@@ -143,7 +170,7 @@  route_table_run(void)
         rtnetlink_run();
         nln_run(nln);
 
-        if (!route_table_valid) {
+        if (!route_table_valid || !rules_valid) {
             route_table_reset();
         }
     }
@@ -202,7 +229,7 @@  route_table_dump_one_table(uint32_t id,
             if (!(nlmsghdr->nlmsg_flags & NLM_F_DUMP_FILTERED)) {
                 filtered = false;
             }
-            handle_msg_cb(&msg, aux);
+            handle_msg_cb(&msg, aux, id);
             route_data_destroy(&msg.rd);
         }
     }
@@ -213,28 +240,203 @@  route_table_dump_one_table(uint32_t id,
 }
 
 static void
-route_table_reset(void)
+rules_dump(void)
 {
-    uint32_t tables[] = {
-        RT_TABLE_DEFAULT,
-        RT_TABLE_MAIN,
-        RT_TABLE_LOCAL,
-    };
+    uint64_t reply_stub[NL_DUMP_BUFSIZE / 8];
+    struct ofpbuf request, reply, buf;
+    struct fib_rule_hdr *rq_msg;
+    struct nl_dump dump;
+
+    ofpbuf_init(&request, 0);
+
+    nl_msg_put_nlmsghdr(&request, sizeof *rq_msg, RTM_GETRULE, NLM_F_REQUEST);
+
+    rq_msg = ofpbuf_put_zeros(&request, sizeof *rq_msg);
+    rq_msg->family = AF_UNSPEC;
+
+    nl_dump_start(&dump, NETLINK_ROUTE, &request);
+    ofpbuf_uninit(&request);
+
+    ofpbuf_use_stub(&buf, reply_stub, sizeof reply_stub);
+    while (nl_dump_next(&dump, &reply, &buf)) {
+        struct route_table_msg msg;
+
+        if (rule_parse(&reply, &msg)) {
+            rule_handle_msg(&msg);
+        }
+    }
+    ofpbuf_uninit(&buf);
+    nl_dump_done(&dump);
+}
 
+static void
+route_table_reset(void)
+{
     route_map_clear();
     netdev_get_addrs_list_flush();
     route_table_valid = true;
+    rules_valid = true;
     rt_change_seq++;
-
     COVERAGE_INC(route_table_dump);
+    rules_dump();
+}
+
+static void
+rule_handle_msg(const struct route_table_msg *change)
+{
+    if (change->relevant) {
+        const struct rule_data *rd = &change->rud;
+
+        route_table_dump_one_table(rd->lookup_table, route_table_handle_msg,
+                                   NULL);
+        ovs_router_rule_add(rd->prio, rd->invert, rd->src_len, &rd->from_addr,
+                            rd->lookup_table, rd->ipv4);
+    }
+}
+
+static int route_nln_parse(struct ofpbuf *buf, void *change_)
+{
+    const struct nlmsghdr *nlmsg = buf->data;
+
+    if (nlmsg->nlmsg_type == RTM_NEWROUTE ||
+        nlmsg->nlmsg_type == RTM_DELROUTE) {
+        return route_table_parse(buf, change_);
+    } else if (nlmsg->nlmsg_type == RTM_NEWRULE ||
+               nlmsg->nlmsg_type == RTM_DELRULE) {
+        return rule_parse(buf, change_);
+    }
+
+    VLOG_DBG_RL(&rl, "received unsupported rtnetlink route message");
+    return 0;
+}
+
+/* Return RTNLGRP_IPV4_RULE or RTNLGRP_IPV6_RULE on success, 0 on parse
+ * error. */
+static int
+rule_parse(struct ofpbuf *buf, void *change_)
+{
+    struct route_table_msg *change = change_;
+    bool ipv4 = false, from_addr_any = false;
+    bool parsed;
+
+    static const struct nl_policy policy[] = {
+        [FRA_PRIORITY] = { .type = NL_A_U32, .optional = true },
+        [FRA_SRC] = { .type = NL_A_U32, .optional = true },
+        [FRA_TABLE] = { .type = NL_A_U32, .optional = true },
+    };
+
+    static const struct nl_policy policy6[] = {
+        [FRA_PRIORITY] = { .type = NL_A_U32, .optional = true },
+        [FRA_SRC] = { .type = NL_A_IPV6, .optional = true },
+        [FRA_TABLE] = { .type = NL_A_U32, .optional = true },
+    };
+
+    struct nlattr *attrs[ARRAY_SIZE(policy)];
+    const struct fib_rule_hdr *frh;
+
+    frh = ofpbuf_at(buf, NLMSG_HDRLEN, sizeof *frh);
+    if (!frh || frh->action != FR_ACT_TO_TBL || frh->tos || frh->dst_len) {
+        /* Unsupported rule. */
+        return 0;
+    }
+
+    if (frh->family == AF_INET) {
+        parsed = nl_policy_parse(buf, NLMSG_HDRLEN +
+                                 sizeof(struct fib_rule_hdr), policy, attrs,
+                                 ARRAY_SIZE(policy));
+        ipv4 = true;
+    } else if (frh->family == AF_INET6) {
+        parsed = nl_policy_parse(buf, NLMSG_HDRLEN +
+                                 sizeof(struct fib_rule_hdr), policy6, attrs,
+                                 ARRAY_SIZE(policy6));
+    } else {
+        VLOG_DBG_RL(&rl, "received non AF_INET rtnetlink route message");
+        return 0;
+    }
+
+    if (parsed) {
+        const struct nlmsghdr *nlmsg;
+
+        nlmsg = buf->data;
 
-    for (size_t i = 0; i < ARRAY_SIZE(tables); i++) {
-        if (!route_table_dump_one_table(tables[i],
-                                        route_table_handle_msg, NULL)) {
-            /* Got unfiltered reply, no need to dump further. */
-            break;
+        memset(change, 0, sizeof *change);
+        change->relevant = true;
+        change->nlmsg_type = nlmsg->nlmsg_type;
+        change->rud.invert = false;
+        change->rud.src_len = frh->src_len;
+        change->rud.lookup_table = frh->table;
+        change->rud.ipv4 = ipv4;
+
+        if (frh->flags & FIB_RULE_INVERT) {
+            /* Invert the matching of rule selector. */
+            change->rud.invert = true;
         }
+
+        if (attrs[FRA_PRIORITY]) {
+            change->rud.prio = nl_attr_get_u32(attrs[FRA_PRIORITY]);
+        }
+
+        if (attrs[FRA_SRC]) {
+            if (ipv4) {
+                ovs_be32 src = nl_attr_get_be32(attrs[FRA_SRC]);
+                in6_addr_set_mapped_ipv4(&change->rud.from_addr, src);
+            } else {
+                change->rud.from_addr = nl_attr_get_in6_addr(attrs[FRA_SRC]);
+            }
+        } else {
+            if (ipv4) {
+                in6_addr_set_mapped_ipv4(&change->rud.from_addr, 0);
+            } else {
+                change->rud.from_addr = in6addr_any;
+            }
+            from_addr_any = true;
+        }
+
+        if (attrs[FRA_TABLE]) {
+            change->rud.lookup_table = nl_attr_get_u32(attrs[FRA_TABLE]);
+        } else {
+            change->relevant = false;
+        }
+
+        if (change->rud.invert && !change->rud.src_len) {
+            change->relevant = false;
+        }
+    } else {
+        VLOG_DBG_RL(&rl, "received unparseable rtnetlink rule message");
+        return 0;
+    }
+
+    /* Skip standard IPv6 rules for local and main tables to avoid duplicates
+     * as they end up being the same as the similar IPv4 rules.
+     */
+    if (!ipv4 && (change->rud.prio == 0 || change->rud.prio == 0x7FFE) &&
+        from_addr_any && is_standard_table_id(change->rud.lookup_table)) {
+        change->relevant = false;
     }
+
+    /* Check if there are any additional attributes that aren't supported
+     * currently by OVS rule-based route lookup. */
+    if (change->relevant) {
+        size_t offset = NLMSG_HDRLEN + sizeof(struct fib_rule_hdr);
+        struct nlattr *nla;
+        size_t left;
+
+        NL_ATTR_FOR_EACH (nla, left, ofpbuf_at(buf, offset, 0),
+                          buf->size - offset) {
+            uint16_t type = nl_attr_type(nla);
+
+            if ((type > FRA_SRC && type < FRA_PRIORITY) ||
+                (type > FRA_PRIORITY && type < FRA_SUPPRESS_PREFIXLEN) ||
+                (type > FRA_TABLE && type < FRA_PROTOCOL) ||
+                type > FRA_PROTOCOL) {
+                change->relevant = false;
+                break;
+            }
+        }
+    }
+
+    /* Success. */
+    return ipv4 ? RTNLGRP_IPV4_RULE : RTNLGRP_IPV6_RULE;
 }
 
 /* Returns true if the given route requires nexthop information (output
@@ -544,7 +746,7 @@  route_table_change(struct route_table_msg *change, void *aux OVS_UNUSED)
 {
     if (!change
         || (change->relevant
-            && is_standard_table_id(change->rd.rta_table_id))) {
+            && ovs_router_is_referenced(change->rd.rta_table_id))) {
         route_table_valid = false;
     }
     if (change) {
@@ -554,7 +756,7 @@  route_table_change(struct route_table_msg *change, void *aux OVS_UNUSED)
 
 static void
 route_table_handle_msg(const struct route_table_msg *change,
-                       void *aux OVS_UNUSED)
+                       void *aux OVS_UNUSED, uint32_t table)
 {
     if (change->relevant && change->nlmsg_type == RTM_NEWROUTE
             && !ovs_list_is_empty(&change->rd.nexthops)) {
@@ -567,7 +769,7 @@  route_table_handle_msg(const struct route_table_msg *change,
         rdnh = CONTAINER_OF(ovs_list_front(&change->rd.nexthops),
                             const struct route_data_nexthop, nexthop_node);
 
-        ovs_router_insert(CLS_MAIN, rd->rta_mark, &rd->rta_dst,
+        ovs_router_insert(table, rd->rta_mark, &rd->rta_dst,
                           IN6_IS_ADDR_V4MAPPED(&rd->rta_dst)
                           ? rd->rtm_dst_len + 96 : rd->rtm_dst_len,
                           rd->rtn_local, rdnh->ifname, &rdnh->addr,
@@ -575,9 +777,19 @@  route_table_handle_msg(const struct route_table_msg *change,
     }
 }
 
+static void
+rules_change(const struct route_table_msg *change OVS_UNUSED,
+             void *aux OVS_UNUSED)
+{
+    if (!change || change->relevant) {
+        rules_valid = false;
+    }
+}
+
 static void
 route_map_clear(void)
 {
+    ovs_router_rules_flush();
     ovs_router_flush(false);
 }
 
diff --git a/lib/route-table.h b/lib/route-table.h
index b805e84dd64c..b90d431fc950 100644
--- a/lib/route-table.h
+++ b/lib/route-table.h
@@ -143,12 +143,26 @@  struct route_data {
     uint32_t rta_priority;       /* 0 if missing. */
 };
 
+struct rule_data {
+    bool invert;
+    uint32_t prio;
+    uint8_t src_len;
+    struct in6_addr from_addr;
+    uint32_t lookup_table;
+    bool ipv4;
+};
+
 /* A digested version of a route message sent down by the kernel to indicate
- * that a route has changed. */
+ * that a route or a rule has changed. */
 struct route_table_msg {
     bool relevant;        /* Should this message be processed? */
-    uint16_t nlmsg_type;  /* e.g. RTM_NEWROUTE, RTM_DELROUTE. */
-    struct route_data rd; /* Data parsed from this message. */
+    uint16_t nlmsg_type;  /* e.g. RTM_NEWROUTE, RTM_DELROUTE, RTM_NEWRULE,
+                           * RTM_DELRULE. */
+    union {               /* Data parsed from this message, depending on
+                           * nlmsg_type. */
+        struct route_data rd;
+        struct rule_data rud;
+    };
 };
 
 uint64_t route_table_get_change_seq(void);
@@ -160,7 +174,7 @@  bool route_table_fallback_lookup(const struct in6_addr *ip6_dst,
                                  struct in6_addr *gw6);
 
 typedef void route_table_handle_msg_callback(const struct route_table_msg *,
-                                             void *aux);
+                                             void *aux, uint32_t table);
 
 bool route_table_dump_one_table(uint32_t id,
                                 route_table_handle_msg_callback *,
diff --git a/tests/ovs-router.at b/tests/ovs-router.at
index d5f56da786d9..b86d0a1cd37c 100644
--- a/tests/ovs-router.at
+++ b/tests/ovs-router.at
@@ -30,16 +30,6 @@  User: 2.2.2.3/32 MARK 1 dev br0 SRC 2.2.2.2
 ])
 AT_CHECK([ovs-appctl --format=json --pretty ovs/route/show], [0], [dnl
 [[
-  {
-    "dst": "2.2.2.2",
-    "local": true,
-    "nexthops": [
-      {
-        "dev": "br0"}],
-    "prefix": 32,
-    "prefsrc": "2.2.2.2",
-    "priority": 192,
-    "user": false},
   {
     "dst": "2.2.2.3",
     "local": false,
diff --git a/tests/test-lib-route-table.c b/tests/test-lib-route-table.c
index 4ba17826e1f1..740a02a73fb9 100644
--- a/tests/test-lib-route-table.c
+++ b/tests/test-lib-route-table.c
@@ -72,7 +72,8 @@  rt_table_name(uint32_t id)
 
 static void
 test_lib_route_table_handle_msg(const struct route_table_msg *change,
-                                void *data OVS_UNUSED)
+                                void *data OVS_UNUSED,
+                                uint32_t table OVS_UNUSED)
 {
     struct ds nexthop_addr = DS_EMPTY_INITIALIZER;
     struct ds rta_prefsrc = DS_EMPTY_INITIALIZER;
@@ -120,7 +121,7 @@  static void
 test_lib_route_table_change(struct route_table_msg *change,
                             void *aux OVS_UNUSED)
 {
-    test_lib_route_table_handle_msg(change, NULL);
+    test_lib_route_table_handle_msg(change, NULL, 0);
     route_data_destroy(&change->rd);
 }