Routes can carry lightweight tunnel (LWT) encapsulation metadata.  FRR,
for example, attaches the L3 VNI of an EVPN type-5 route as the tunnel id
of an IP encapsulation.

Parse the RTA_ENCAP_TYPE and RTA_ENCAP attributes into a new 'lwt' member
of struct route_data_nexthop, so that users of the route-table library
(such as OVN) can learn them.  All attributes of the LWTUNNEL_ENCAP_IP and
LWTUNNEL_ENCAP_IP6 encapsulations are parsed, and the tunnel id is kept as
the 64 bit value the kernel reports.  Any other encapsulation type only
gets its type recorded.

The kernel keeps the encapsulation per next hop, both for a single path
route and for each next hop of a multipath route, so store it per next hop
here as well.

Assisted-by: Claude Opus 5, Cursor
Signed-off-by: Han Zhou <[email protected]>
---
v2:
- Parse all attributes of the LWTUNNEL_ENCAP_IP/IP6 encapsulations, not
  just the tunnel id (Dumitru).
- Use the linux/lwtunnel.h uapi names instead of locally invented macros,
  with a fallback for pre-4.3 build headers (Dumitru).
- Keep the tunnel id as the 64 bit value the kernel reports and drop the
  VXLAN-specific "vni" naming; it is now a generic 'id' in a new
  struct route_data_lwt_tunnel (Dumitru).
- Store the encapsulation per next hop rather than per route, matching
  where the kernel keeps it.  This removes the "adopt the first VNI seen"
  handling for multipath routes.
- Add IPv6 encap and multipath encap tests, and extend the IPv4 test to
  cover src/ttl/tos (Dumitru).

 configure.ac                 |   1 +
 lib/route-table.c            | 139 +++++++++++++++++++++++++++++++++++
 lib/route-table.h            |  30 ++++++++
 tests/system-route.at        |  50 +++++++++++++
 tests/test-lib-route-table.c |  35 ++++++++-
 5 files changed, 254 insertions(+), 1 deletion(-)

diff --git a/configure.ac b/configure.ac
index db233fad453d..7f2713dea9bd 100644
--- a/configure.ac
+++ b/configure.ac
@@ -115,6 +115,7 @@ AC_CHECK_MEMBERS([struct sockaddr_in6.sin6_scope_id], [], 
[],
 AC_CHECK_FUNCS([mlockall strnlen getloadavg statvfs getmntent_r sendmmsg 
clock_gettime])
 AC_CHECK_HEADERS([mntent.h sys/statvfs.h linux/types.h linux/if_ether.h])
 AC_CHECK_HEADERS([linux/net_namespace.h stdatomic.h bits/floatn-common.h])
+AC_CHECK_HEADERS([linux/lwtunnel.h])
 AC_CHECK_HEADERS([net/if_mib.h], [], [], [[#include <sys/types.h>
 #include <net/if.h>]])
 
diff --git a/lib/route-table.c b/lib/route-table.c
index 2a13a5cc7d93..34083a18baa7 100644
--- a/lib/route-table.c
+++ b/lib/route-table.c
@@ -25,8 +25,12 @@
 #include <sys/socket.h>
 #include <linux/fib_rules.h>
 #include <linux/rtnetlink.h>
+#ifdef HAVE_LINUX_LWTUNNEL_H
+#include <linux/lwtunnel.h>
+#endif
 #include <net/if.h>
 
+#include "byte-order.h"
 #include "coverage.h"
 #include "hash.h"
 #include "netdev.h"
@@ -48,6 +52,21 @@
 #define FRA_SUPPRESS_PREFIXLEN 14 /* Linux 3.12 */
 #define FRA_TABLE 15 /* Linux 2.6.19 */
 #define FRA_PROTOCOL 21 /* Linux 4.17 */
+#define RTA_ENCAP_TYPE 21 /* Linux 4.3 */
+#define RTA_ENCAP 22 /* Linux 4.3 */
+
+/* The subset of linux/lwtunnel.h that we use, for builds against headers
+ * older than Linux 4.3, which is where that header was introduced. */
+#ifndef HAVE_LINUX_LWTUNNEL_H
+enum { LWTUNNEL_ENCAP_NONE = 0,
+       LWTUNNEL_ENCAP_IP = 2,
+       LWTUNNEL_ENCAP_IP6 = 4 };
+enum { LWTUNNEL_IP_ID = 1, LWTUNNEL_IP_DST = 2, LWTUNNEL_IP_SRC = 3,
+       LWTUNNEL_IP_TTL = 4, LWTUNNEL_IP_TOS = 5, LWTUNNEL_IP_FLAGS = 6 };
+enum { LWTUNNEL_IP6_ID = 1, LWTUNNEL_IP6_DST = 2, LWTUNNEL_IP6_SRC = 3,
+       LWTUNNEL_IP6_HOPLIMIT = 4, LWTUNNEL_IP6_TC = 5,
+       LWTUNNEL_IP6_FLAGS = 6 };
+#endif
 
 /* Linux 4.1 added RTA_VIA. */
 #ifndef HAVE_RTA_VIA
@@ -431,6 +450,114 @@ rule_parse(struct ofpbuf *buf, void *change_)
     return ipv4 ? RTNLGRP_IPV4_RULE : RTNLGRP_IPV6_RULE;
 }
 
+/* Returns a string describing 'encap_type', which is expected to be one of
+ * the LWTUNNEL_ENCAP_* values.  Encapsulation types whose attributes this
+ * module does not parse are reported as "unsupported". */
+const char *
+route_data_lwt_encap_type_to_string(uint16_t encap_type)
+{
+    switch (encap_type) {
+    case LWTUNNEL_ENCAP_NONE: return "none";
+    case LWTUNNEL_ENCAP_IP: return "ip";
+    case LWTUNNEL_ENCAP_IP6: return "ip6";
+    default: return "unsupported";
+    }
+}
+
+/* Stores the tunnel endpoint in 'attr' into 'addr', as an IPv4-mapped address
+ * if 'ipv4'. */
+static void
+route_table_parse_lwt_addr__(const struct nlattr *attr, bool ipv4,
+                             struct in6_addr *addr)
+{
+    if (ipv4) {
+        in6_addr_set_mapped_ipv4(addr, nl_attr_get_be32(attr));
+    } else {
+        *addr = nl_attr_get_in6_addr(attr);
+    }
+}
+
+/* Parses the nested lightweight tunnel encapsulation attributes in 'encap'
+ * into 'lwt', whose 'encap_type' must already be set.  Encapsulation types
+ * other than LWTUNNEL_ENCAP_IP and LWTUNNEL_ENCAP_IP6 are left with only
+ * 'encap_type' filled in.
+ *
+ * Returns true on success, false on a parse error. */
+static bool
+route_table_parse_lwt_tunnel__(const struct nlattr *encap,
+                               struct route_data_lwt_tunnel *lwt)
+{
+    /* The LWTUNNEL_IP_* and LWTUNNEL_IP6_* attributes are numbered
+     * identically and only differ in the size of the tunnel endpoints, so the
+     * IPv4 names are used to index both policies. */
+    static const struct nl_policy policy[] = {
+        [LWTUNNEL_IP_ID] = { .type = NL_A_BE64, .optional = true },
+        [LWTUNNEL_IP_DST] = { .type = NL_A_BE32, .optional = true },
+        [LWTUNNEL_IP_SRC] = { .type = NL_A_BE32, .optional = true },
+        [LWTUNNEL_IP_TTL] = { .type = NL_A_U8, .optional = true },
+        [LWTUNNEL_IP_TOS] = { .type = NL_A_U8, .optional = true },
+        [LWTUNNEL_IP_FLAGS] = { .type = NL_A_BE16, .optional = true },
+    };
+
+    static const struct nl_policy policy6[] = {
+        [LWTUNNEL_IP6_ID] = { .type = NL_A_BE64, .optional = true },
+        [LWTUNNEL_IP6_DST] = { .type = NL_A_IPV6, .optional = true },
+        [LWTUNNEL_IP6_SRC] = { .type = NL_A_IPV6, .optional = true },
+        [LWTUNNEL_IP6_HOPLIMIT] = { .type = NL_A_U8, .optional = true },
+        [LWTUNNEL_IP6_TC] = { .type = NL_A_U8, .optional = true },
+        [LWTUNNEL_IP6_FLAGS] = { .type = NL_A_BE16, .optional = true },
+    };
+
+    struct nlattr *attrs[ARRAY_SIZE(policy)];
+    bool ipv4;
+
+    BUILD_ASSERT(ARRAY_SIZE(policy) == ARRAY_SIZE(policy6));
+
+    /* The casts avoid warnings about comparing the two distinct enums. */
+    BUILD_ASSERT((int) LWTUNNEL_IP_ID == (int) LWTUNNEL_IP6_ID
+                 && (int) LWTUNNEL_IP_DST == (int) LWTUNNEL_IP6_DST
+                 && (int) LWTUNNEL_IP_SRC == (int) LWTUNNEL_IP6_SRC
+                 && (int) LWTUNNEL_IP_TTL == (int) LWTUNNEL_IP6_HOPLIMIT
+                 && (int) LWTUNNEL_IP_TOS == (int) LWTUNNEL_IP6_TC
+                 && (int) LWTUNNEL_IP_FLAGS == (int) LWTUNNEL_IP6_FLAGS);
+
+    if (lwt->encap_type == LWTUNNEL_ENCAP_IP) {
+        ipv4 = true;
+    } else if (lwt->encap_type == LWTUNNEL_ENCAP_IP6) {
+        ipv4 = false;
+    } else {
+        return true;
+    }
+
+    if (!nl_parse_nested(encap, ipv4 ? policy : policy6, attrs,
+                         ARRAY_SIZE(attrs))) {
+        VLOG_DBG_RL(&rl, "received unparseable lwtunnel encap attributes");
+        return false;
+    }
+
+    if (attrs[LWTUNNEL_IP_ID]) {
+        lwt->id = ntohll(nl_attr_get_be64(attrs[LWTUNNEL_IP_ID]));
+        lwt->id_present = true;
+    }
+    if (attrs[LWTUNNEL_IP_DST]) {
+        route_table_parse_lwt_addr__(attrs[LWTUNNEL_IP_DST], ipv4, &lwt->dst);
+    }
+    if (attrs[LWTUNNEL_IP_SRC]) {
+        route_table_parse_lwt_addr__(attrs[LWTUNNEL_IP_SRC], ipv4, &lwt->src);
+    }
+    if (attrs[LWTUNNEL_IP_TTL]) {
+        lwt->ttl = nl_attr_get_u8(attrs[LWTUNNEL_IP_TTL]);
+    }
+    if (attrs[LWTUNNEL_IP_TOS]) {
+        lwt->tos = nl_attr_get_u8(attrs[LWTUNNEL_IP_TOS]);
+    }
+    if (attrs[LWTUNNEL_IP_FLAGS]) {
+        lwt->flags = ntohs(nl_attr_get_be16(attrs[LWTUNNEL_IP_FLAGS]));
+    }
+
+    return true;
+}
+
 /* Returns true if the given route requires nexthop information (output
  * interface, nexthop IP, ...).  Returns false for special route types
  * that don't need this information. */
@@ -468,6 +595,8 @@ route_table_parse__(struct ofpbuf *buf, size_t ofs,
         [RTA_PRIORITY] = { .type = NL_A_U32, .optional = true },
         [RTA_VIA] = { .type = NL_A_RTA_VIA, .optional = true },
         [RTA_MULTIPATH] = { .type = NL_A_NESTED, .optional = true },
+        [RTA_ENCAP_TYPE] = { .type = NL_A_U16, .optional = true },
+        [RTA_ENCAP] = { .type = NL_A_NESTED, .optional = true },
     };
 
     static const struct nl_policy policy6[] = {
@@ -480,6 +609,8 @@ route_table_parse__(struct ofpbuf *buf, size_t ofs,
         [RTA_PRIORITY] = { .type = NL_A_U32, .optional = true },
         [RTA_VIA] = { .type = NL_A_RTA_VIA, .optional = true },
         [RTA_MULTIPATH] = { .type = NL_A_NESTED, .optional = true },
+        [RTA_ENCAP_TYPE] = { .type = NL_A_U16, .optional = true },
+        [RTA_ENCAP] = { .type = NL_A_NESTED, .optional = true },
     };
 
     struct nlattr *attrs[ARRAY_SIZE(policy)];
@@ -585,6 +716,14 @@ route_table_parse__(struct ofpbuf *buf, size_t ofs,
         if (attrs[RTA_PRIORITY]) {
             change->rd.rta_priority = nl_attr_get_u32(attrs[RTA_PRIORITY]);
         }
+        if (attrs[RTA_ENCAP_TYPE]) {
+            rdnh->lwt.encap_type = nl_attr_get_u16(attrs[RTA_ENCAP_TYPE]);
+            if (attrs[RTA_ENCAP]
+                && !route_table_parse_lwt_tunnel__(attrs[RTA_ENCAP],
+                                                   &rdnh->lwt)) {
+                goto error_out;
+            }
+        }
         if (attrs[RTA_VIA]) {
             const struct rtvia *rtvia = nl_attr_get(attrs[RTA_VIA]);
             ovs_be32 addr;
diff --git a/lib/route-table.h b/lib/route-table.h
index b49fbb14ebe0..6b5dfba98aa8 100644
--- a/lib/route-table.h
+++ b/lib/route-table.h
@@ -99,6 +99,31 @@
  * netlink notifier and so on, the functions in this module are thread safe.
  */
 
+/* Lightweight tunnel (LWT) encapsulation of a next hop, as extracted from the
+ * RTA_ENCAP_TYPE and RTA_ENCAP Netlink attributes.
+ *
+ * Only the LWTUNNEL_ENCAP_IP and LWTUNNEL_ENCAP_IP6 encapsulations are
+ * understood; for any other encapsulation type only 'encap_type' is set and
+ * the remaining members are left zeroed. */
+struct route_data_lwt_tunnel {
+    /* LWTUNNEL_ENCAP_* value, LWTUNNEL_ENCAP_NONE if the next hop has no
+     * encapsulation.  Use route_data_lwt_encap_type_to_string() to format. */
+    uint16_t encap_type;
+
+    /* A tunnel id of 0 is valid, so 'id' is meaningful only if 'id_present'
+     * is true. */
+    bool id_present;
+    uint64_t id;                 /* Tunnel id, e.g. a VXLAN VNI. */
+
+    /* Tunnel endpoints, IPv4-mapped for LWTUNNEL_ENCAP_IP.  0 if missing. */
+    struct in6_addr dst;
+    struct in6_addr src;
+
+    uint8_t ttl;                 /* Hop limit for IP6.  0 if missing. */
+    uint8_t tos;                 /* Traffic class for IP6.  0 if missing. */
+    uint16_t flags;              /* TUNNEL_* flags.  0 if missing. */
+};
+
 /* Information about a next hop stored in a linked list with base in struct
  * route_data.  Please refer to comment in struct route_data for details. */
 struct route_data_nexthop {
@@ -107,6 +132,10 @@ struct route_data_nexthop {
     sa_family_t family;
     struct in6_addr addr;
     char ifname[IFNAMSIZ]; /* Interface name. */
+
+    /* Encapsulation to apply when forwarding to this next hop.  The kernel
+     * keeps this per next hop, both for single path and multipath routes. */
+    struct route_data_lwt_tunnel lwt;
 };
 
 struct route_data {
@@ -181,4 +210,5 @@ bool route_table_dump_one_table(uint32_t id, sa_family_t 
family,
                                 void *aux);
 int route_table_parse(struct ofpbuf *, void *change);
 void route_data_destroy(struct route_data *);
+const char *route_data_lwt_encap_type_to_string(uint16_t encap_type);
 #endif /* route-table.h */
diff --git a/tests/system-route.at b/tests/system-route.at
index a074c51f9fd0..4b6ac56ba969 100644
--- a/tests/system-route.at
+++ b/tests/system-route.at
@@ -331,6 +331,56 @@ AT_CHECK([ovstest test-lib-route-table-dump | \
 192.168.10.12/32 rtm_protocol: RTPROT_BOOT
 ])
 
+dnl A route without encapsulation reports nothing on its next hop.
+AT_CHECK([ovstest test-lib-route-table-dump | \
+          grep '192.168.10.12.*nexthop'], [0], [dnl
+    192.168.10.12/32 nexthop family: AF_INET addr: 10.0.0.18 ifname: p1-route
+])
+
+dnl Add a route with an IP lightweight-tunnel (LWT) encapsulation.  Its
+dnl attributes are parsed into the next hop's lwt member.  A tunnel id is used
+dnl by, for example, EVPN type-5 routes to carry an L3 VNI.
+AT_CHECK([ip route add 192.168.10.13/32 encap ip id 5000 dst 10.0.0.19 \
+          src 10.0.0.17 ttl 10 tos 8 dev p1-route via 10.0.0.18], [0],
+         [stdout])
+AT_CHECK([ovstest test-lib-route-table-dump | \
+          grep '192.168.10.13.*nexthop'], [0], [dnl
+    192.168.10.13/32 nexthop family: AF_INET addr: 10.0.0.18 ifname: p1-route 
encap: ip id: 5000 dst: 10.0.0.19 src: 10.0.0.17 ttl: 10 tos: 8 flags: 0x0
+])
+
+dnl The kernel keeps the encapsulation per next hop, so each next hop of a
+dnl multipath route can have its own.
+AT_CHECK([ip route add 192.168.10.14/32 \
+          nexthop encap ip id 100 dst 10.0.0.20 dev p1-route via 10.0.0.18 \
+          nexthop encap ip id 200 dst 10.0.0.21 dev p1-route via 10.0.0.19],
+         [0], [stdout])
+AT_CHECK([ovstest test-lib-route-table-dump | \
+          grep '192.168.10.14.*nexthop' | sort], [0], [dnl
+    192.168.10.14/32 nexthop family: AF_INET addr: 10.0.0.18 ifname: p1-route 
encap: ip id: 100 dst: 10.0.0.20 src: 0.0.0.0 ttl: 0 tos: 0 flags: 0x0
+    192.168.10.14/32 nexthop family: AF_INET addr: 10.0.0.19 ifname: p1-route 
encap: ip id: 200 dst: 10.0.0.21 src: 0.0.0.0 ttl: 0 tos: 0 flags: 0x0
+])
+
+AT_CLEANUP
+
+AT_SETUP([route-table - lwtunnel encap attributes - ipv6])
+AT_KEYWORDS([route])
+
+dnl Create tap ports.
+AT_CHECK([ip tuntap add name p1-route mode tap])
+AT_CHECK([ip link set p1-route up])
+on_exit 'ip link del p1-route'
+
+AT_CHECK([ip -6 addr add fc00:db8:dead::10/64 dev p1-route], [0], [stdout])
+
+dnl Add a route with an IP6 lightweight-tunnel (LWT) encapsulation.
+AT_CHECK([ip -6 route add fc00:db8:cafe::/64 encap ip6 id 5000 \
+          dst fc00:db8:dead::19 src fc00:db8:dead::10 hoplimit 10 tc 8 \
+          dev p1-route via fc00:db8:dead::1], [0], [stdout])
+AT_CHECK([ovstest test-lib-route-table-dump | \
+          grep 'fc00:db8:cafe::.*nexthop'], [0], [dnl
+    fc00:db8:cafe::/64 nexthop family: AF_INET6 addr: fc00:db8:dead::1 ifname: 
p1-route encap: ip6 id: 5000 dst: fc00:db8:dead::19 src: fc00:db8:dead::10 ttl: 
10 tos: 8 flags: 0x0
+])
+
 AT_CLEANUP
 
 dnl Checks that OVS ignores unsupported routing rules.
diff --git a/tests/test-lib-route-table.c b/tests/test-lib-route-table.c
index f99f056c8ddc..69e568c40925 100644
--- a/tests/test-lib-route-table.c
+++ b/tests/test-lib-route-table.c
@@ -70,6 +70,37 @@ rt_table_name(uint32_t id)
            tid;
 }
 
+/* Prints the lightweight tunnel encapsulation of a next hop, if any.  Nothing
+ * is printed for a next hop without encapsulation, so that the expected
+ * output of tests that do not exercise encapsulation stays unchanged. */
+static void
+print_lwt_tunnel(const struct route_data_lwt_tunnel *lwt)
+{
+    struct ds addr = DS_EMPTY_INITIALIZER;
+
+    if (!lwt->encap_type) {
+        return;
+    }
+
+    printf(" encap: %s",
+           route_data_lwt_encap_type_to_string(lwt->encap_type));
+    if (lwt->id_present) {
+        printf(" id: %"PRIu64, lwt->id);
+    }
+
+    ipv6_format_mapped(&lwt->dst, &addr);
+    printf(" dst: %s", ds_cstr(&addr));
+
+    ds_clear(&addr);
+    ipv6_format_mapped(&lwt->src, &addr);
+    printf(" src: %s", ds_cstr(&addr));
+
+    printf(" ttl: %"PRIu8" tos: %"PRIu8" flags: 0x%"PRIx16,
+           lwt->ttl, lwt->tos, lwt->flags);
+
+    ds_destroy(&addr);
+}
+
 static void
 test_lib_route_table_handle_msg(const struct route_table_msg *change,
                                 void *data OVS_UNUSED,
@@ -95,13 +126,15 @@ test_lib_route_table_handle_msg(const struct 
route_table_msg *change,
     LIST_FOR_EACH (rdnh, nexthop_node, &rd->nexthops) {
         ds_clear(&nexthop_addr);
         ipv6_format_mapped(&rdnh->addr, &nexthop_addr);
-        printf("    %s/%u nexthop family: %s addr: %s ifname: %s\n",
+        printf("    %s/%u nexthop family: %s addr: %s ifname: %s",
                ds_cstr(&rta_dst), rd->rtm_dst_len,
                rdnh->family == AF_INET ? "AF_INET" :
                rdnh->family == AF_INET6 ? "AF_INET6" :
                "UNKNOWN",
                ds_cstr(&nexthop_addr),
                rdnh->ifname);
+        print_lwt_tunnel(&rdnh->lwt);
+        printf("\n");
     }
 
     ds_destroy(&nexthop_addr);
-- 
2.38.1

_______________________________________________
dev mailing list
[email protected]
https://mail.openvswitch.org/mailman/listinfo/ovs-dev

Reply via email to