@@ -770,6 +770,36 @@ RouterStaticRoute(router, key, dsts) :-
},
var dsts = set_singleton(RouteDst{nexthop, src_ip, port, ecmp_symmetric_reply}).
+/* compute route-route pairs for nexthop = "discard" routes */
+function is_discard_route(nexthop: string): bool {
+ match (nexthop) {
+ "discard" -> true,
+ _ -> false
+ }
+}
+relation &DiscardRoute(lrsr: nb::Logical_Router_Static_Route,
+ key: route_key,
+ nexthop: string)
+&DiscardRoute(.lrsr = lrsr,
+ .key = RouteKey{policy, ip_prefix, plen},
+ .nexthop = lrsr.nexthop) :-
+ lrsr in nb::Logical_Router_Static_Route(),
+ var policy = route_policy_from_string(lrsr.policy),
+ Some{(var ip_prefix, var plen)} = ip46_parse_cidr(lrsr.ip_prefix),
+ is_discard_route(lrsr.nexthop).
+
+relation RouterDiscardRoute_(
+ router : Ref<Router>,
+ key : route_key,
+ nexthop : string)
+
+RouterDiscardRoute_(.router = router,
+ .key = route.key,
+ .nexthop = route.nexthop) :-
+ router in &Router(.lr = nb::Logical_Router{.static_routes = routes}),
+ var route_id = FlatMap(routes),
+ route in &DiscardRoute(.lrsr = nb::Logical_Router_Static_Route{._uuid = route_id}).
+
Warning[message] :-
RouterStaticRoute_(.router = router, .key = key, .nexthop = nexthop),
not RouterStaticRoute(.router = router, .key = key),
@@ -7938,6 +7938,7 @@ struct parsed_route {
uint32_t hash;
const struct nbrec_logical_router_static_route *route;
bool ecmp_symmetric_reply;
+ bool is_discard_route;
};
static uint32_t
@@ -7957,20 +7958,23 @@ parsed_routes_add(struct ovs_list *routes,
/* Verify that the next hop is an IP address with an all-ones mask. */
struct in6_addr nexthop;
unsigned int plen;
- if (!ip46_parse_cidr(route->nexthop, &nexthop, &plen)) {
- static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
- VLOG_WARN_RL(&rl, "bad 'nexthop' %s in static route"
- UUID_FMT, route->nexthop,
- UUID_ARGS(&route->header_.uuid));
- return NULL;
- }
- if ((IN6_IS_ADDR_V4MAPPED(&nexthop) && plen != 32) ||
- (!IN6_IS_ADDR_V4MAPPED(&nexthop) && plen != 128)) {
- static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
- VLOG_WARN_RL(&rl, "bad next hop mask %s in static route"
- UUID_FMT, route->nexthop,
- UUID_ARGS(&route->header_.uuid));
- return NULL;
+ bool is_discard_route = !strcmp(route->nexthop, "discard");
+ if (!is_discard_route) {
+ if (!ip46_parse_cidr(route->nexthop, &nexthop, &plen)) {
+ static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
+ VLOG_WARN_RL(&rl, "bad 'nexthop' %s in static route"
+ UUID_FMT, route->nexthop,
+ UUID_ARGS(&route->header_.uuid));
+ return NULL;
+ }
+ if ((IN6_IS_ADDR_V4MAPPED(&nexthop) && plen != 32) ||
+ (!IN6_IS_ADDR_V4MAPPED(&nexthop) && plen != 128)) {
+ static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
+ VLOG_WARN_RL(&rl, "bad next hop mask %s in static route"
+ UUID_FMT, route->nexthop,
+ UUID_ARGS(&route->header_.uuid));
+ return NULL;
+ }
}
/* Parse ip_prefix */
@@ -7984,13 +7988,15 @@ parsed_routes_add(struct ovs_list *routes,
}
/* Verify that ip_prefix and nexthop have same address familiy. */
- if (IN6_IS_ADDR_V4MAPPED(&prefix) != IN6_IS_ADDR_V4MAPPED(&nexthop)) {
- static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
- VLOG_WARN_RL(&rl, "Address family doesn't match between 'ip_prefix' %s"
- " and 'nexthop' %s in static route"UUID_FMT,
- route->ip_prefix, route->nexthop,
- UUID_ARGS(&route->header_.uuid));
- return NULL;
+ if (!is_discard_route) {
+ if (IN6_IS_ADDR_V4MAPPED(&prefix) != IN6_IS_ADDR_V4MAPPED(&nexthop)) {
+ static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1);
+ VLOG_WARN_RL(&rl, "Address family doesn't match between 'ip_prefix'"
+ " %s and 'nexthop' %s in static route"UUID_FMT,
+ route->ip_prefix, route->nexthop,
+ UUID_ARGS(&route->header_.uuid));
+ return NULL;
+ }
}
const struct nbrec_bfd *nb_bt = route->bfd;
@@ -8021,6 +8027,7 @@ parsed_routes_add(struct ovs_list *routes,
pr->route = route;
pr->ecmp_symmetric_reply = smap_get_bool(&route->options,
"ecmp_symmetric_reply", false);
+ pr->is_discard_route = is_discard_route;
ovs_list_insert(routes, &pr->list_node);
return pr;
}
@@ -8433,10 +8440,11 @@ build_ecmp_route_flow(struct hmap *lflows, struct ovn_datapath *od,
}
static void
-add_route(struct hmap *lflows, const struct ovn_port *op,
- const char *lrp_addr_s, const char *network_s, int plen,
- const char *gateway, bool is_src_route,
- const struct ovsdb_idl_row *stage_hint)
+add_route(struct hmap *lflows, struct ovn_datapath *od,
+ const struct ovn_port *op, const char *lrp_addr_s,
+ const char *network_s, int plen, const char *gateway,
+ bool is_src_route, const struct ovsdb_idl_row *stage_hint,
+ bool is_discard_route)
{
bool is_ipv4 = strchr(network_s, '.') ? true : false;
struct ds match = DS_EMPTY_INITIALIZER;
@@ -8455,30 +8463,34 @@ add_route(struct hmap *lflows, const struct ovn_port *op,
&match, &priority);
struct ds common_actions = DS_EMPTY_INITIALIZER;
- ds_put_format(&common_actions, REG_ECMP_GROUP_ID" = 0; %s = ",
- is_ipv4 ? REG_NEXT_HOP_IPV4 : REG_NEXT_HOP_IPV6);
- if (gateway) {
- ds_put_cstr(&common_actions, gateway);
- } else {
- ds_put_format(&common_actions, "ip%s.dst", is_ipv4 ? "4" : "6");
- }
- ds_put_format(&common_actions, "; "
- "%s = %s; "
- "eth.src = %s; "
- "outport = %s; "
- "flags.loopback = 1; "
- "next;",
- is_ipv4 ? REG_SRC_IPV4 : REG_SRC_IPV6,
- lrp_addr_s,
- op->lrp_networks.ea_s,
- op->json_key);
struct ds actions = DS_EMPTY_INITIALIZER;
- ds_put_format(&actions, "ip.ttl--; %s", ds_cstr(&common_actions));
+ if (is_discard_route) {
+ ds_put_format(&actions, "drop;");
+ } else {
+ ds_put_format(&common_actions, REG_ECMP_GROUP_ID" = 0; %s = ",
+ is_ipv4 ? REG_NEXT_HOP_IPV4 : REG_NEXT_HOP_IPV6);
+ if (gateway) {
+ ds_put_cstr(&common_actions, gateway);
+ } else {
+ ds_put_format(&common_actions, "ip%s.dst", is_ipv4 ? "4" : "6");
+ }
+ ds_put_format(&common_actions, "; "
+ "%s = %s; "
+ "eth.src = %s; "
+ "outport = %s; "
+ "flags.loopback = 1; "
+ "next;",
+ is_ipv4 ? REG_SRC_IPV4 : REG_SRC_IPV6,
+ lrp_addr_s,
+ op->lrp_networks.ea_s,
+ op->json_key);
+ ds_put_format(&actions, "ip.ttl--; %s", ds_cstr(&common_actions));
+ }
- ovn_lflow_add_with_hint(lflows, op->od, S_ROUTER_IN_IP_ROUTING, priority,
+ ovn_lflow_add_with_hint(lflows, od, S_ROUTER_IN_IP_ROUTING, priority,
ds_cstr(&match), ds_cstr(&actions),
stage_hint);
- if (op->has_bfd) {
+ if (op && op->has_bfd) {
ds_put_format(&match, " && udp.dst == 3784");
ovn_lflow_add_with_hint(lflows, op->od, S_ROUTER_IN_IP_ROUTING,
priority + 1, ds_cstr(&match),
@@ -8500,16 +8512,18 @@ build_static_route_flow(struct hmap *lflows, struct ovn_datapath *od,
const struct nbrec_logical_router_static_route *route = route_->route;
/* Find the outgoing port. */
- if (!find_static_route_outport(od, ports, route,
- IN6_IS_ADDR_V4MAPPED(&route_->prefix),
- &lrp_addr_s, &out_port)) {
- return;
+ if (!route_->is_discard_route) {
+ if (!find_static_route_outport(od, ports, route,
+ IN6_IS_ADDR_V4MAPPED(&route_->prefix),
+ &lrp_addr_s, &out_port)) {
+ return;
+ }
}
char *prefix_s = build_route_prefix_s(&route_->prefix, route_->plen);
- add_route(lflows, out_port, lrp_addr_s, prefix_s, route_->plen,
- route->nexthop, route_->is_src_route,
- &route->header_);
+ add_route(lflows, route_->is_discard_route ? od : out_port->od, out_port,
+ lrp_addr_s, prefix_s, route_->plen, route->nexthop,
+ route_->is_src_route, &route->header_, route_->is_discard_route);
free(prefix_s);
}
@@ -9735,17 +9749,17 @@ build_ip_routing_flows_for_lrouter_port(
if (op->nbrp) {
for (int i = 0; i < op->lrp_networks.n_ipv4_addrs; i++) {
- add_route(lflows, op, op->lrp_networks.ipv4_addrs[i].addr_s,
+ add_route(lflows, op->od, op, op->lrp_networks.ipv4_addrs[i].addr_s,
op->lrp_networks.ipv4_addrs[i].network_s,
op->lrp_networks.ipv4_addrs[i].plen, NULL, false,
- &op->nbrp->header_);
+ &op->nbrp->header_, false);
}
for (int i = 0; i < op->lrp_networks.n_ipv6_addrs; i++) {
- add_route(lflows, op, op->lrp_networks.ipv6_addrs[i].addr_s,
+ add_route(lflows, op->od, op, op->lrp_networks.ipv6_addrs[i].addr_s,
op->lrp_networks.ipv6_addrs[i].network_s,
op->lrp_networks.ipv6_addrs[i].plen, NULL, false,
- &op->nbrp->header_);
+ &op->nbrp->header_, false);
}
}
}
@@ -6450,6 +6450,16 @@ for (Route(.port = port,
}
}
+/* Install drop routes for all the static routes with nexthop = "discard" */
+Flow(.logical_datapath = router.lr._uuid,
+ .stage = router_stage(IN, IP_ROUTING),
+ .priority = priority as integer,
+ .__match = ip_match,
+ .actions = "drop;",
+ .external_ids = map_empty()) :-
+ r in RouterDiscardRoute_(.router = router, .key = key, .nexthop = nexthop),
+ (var ip_match, var priority) = build_route_match(r.key).
+
/* Logical router ingress table IP_ROUTING & IP_ROUTING_ECMP: IP Routing.
*
* A packet that arrives at this table is an IP packet that should be
@@ -2649,7 +2649,9 @@
<column name="nexthop">
<p>
Nexthop IP address for this route. Nexthop IP address should be the IP
- address of a connected router port or the IP address of a logical port.
+ address of a connected router port or the IP address of a logical port
+ or can be set to <code>discard</code> for dropping packets which match
+ the given route.
</p>
</column>
@@ -1467,18 +1467,38 @@ AT_CHECK([ovn-nbctl lr-route-add lr0 10.0.0.111/24 11.0.0.1/24], [1], [],
AT_CHECK([ovn-nbctl lr-route-add lr0 2001:0db8:1::/64 2001:0db8:0:f103::1/64], [1], [],
[ovn-nbctl: bad IPv6 nexthop argument: 2001:0db8:0:f103::1/64
])
+AT_CHECK([ovn-nbctl --ecmp lr-route-add lr0 20.0.0.0/24 discard], [1], [],
+ [ovn-nbctl: ecmp is not valid for discard routes.
+])
+AT_CHECK([ovn-nbctl --ecmp-symmetric-reply lr-route-add lr0 20.0.0.0/24 discard], [1], [],
+ [ovn-nbctl: ecmp is not valid for discard routes.
+])
+AT_CHECK([ovn-nbctl --bfd lr-route-add lr0 20.0.0.0/24 discard lp0], [1], [],
+ [ovn-nbctl: bfd dst_ip cannot be discard.
+])
+AT_CHECK([ovn-nbctl lr-route-add lr0 20.0.0.0/24 discard lp0], [1], [],
+ [ovn-nbctl: outport is not valid for discard routes.
+])
AT_CHECK([ovn-nbctl --may-exist lr-route-add lr0 10.0.0.111/24 11.0.0.1])
AT_CHECK([ovn-nbctl --policy=src-ip lr-route-add lr0 9.16.1.0/24 11.0.0.1])
dnl Add a route with existed prefix but different policy (src-ip)
AT_CHECK([ovn-nbctl --policy=src-ip lr-route-add lr0 10.0.0.0/24 11.0.0.2])
+AT_CHECK([ovn-nbctl lr-route-add lr0 20.0.0.0/24 discard])
+AT_CHECK([ovn-nbctl --policy=src-ip lr-route-add lr0 20.0.0.0/24 discard])
+AT_CHECK([ovn-nbctl --ecmp --policy=src-ip lr-route-add lr0 20.0.0.0/24 11.0.0.1], [1], [],
+ [ovn-nbctl: discard nexthop for the same ECMP route exists.
+])
+
AT_CHECK([ovn-nbctl lr-route-list lr0], [0], [dnl
IPv4 Routes
10.0.0.0/24 11.0.0.1 dst-ip
10.0.1.0/24 11.0.1.1 dst-ip lp0
+ 20.0.0.0/24 discard dst-ip
9.16.1.0/24 11.0.0.1 src-ip
10.0.0.0/24 11.0.0.2 src-ip
+ 20.0.0.0/24 discard src-ip
0.0.0.0/0 192.168.0.1 dst-ip
])
@@ -1487,11 +1507,16 @@ AT_CHECK([ovn-nbctl lr-route-list lr0], [0], [dnl
IPv4 Routes
10.0.0.0/24 11.0.0.1 dst-ip lp1
10.0.1.0/24 11.0.1.1 dst-ip lp0
+ 20.0.0.0/24 discard dst-ip
9.16.1.0/24 11.0.0.1 src-ip
10.0.0.0/24 11.0.0.2 src-ip
+ 20.0.0.0/24 discard src-ip
0.0.0.0/0 192.168.0.1 dst-ip
])
+AT_CHECK([ovn-nbctl --policy=src-ip lr-route-del lr0 20.0.0.0/24])
+AT_CHECK([ovn-nbctl lr-route-del lr0 20.0.0.0/24 discard])
+
dnl Delete non-existent prefix
AT_CHECK([ovn-nbctl lr-route-del lr0 10.0.2.1/24], [1], [],
[ovn-nbctl: no matching route: policy 'any', prefix '10.0.2.0/24', nexthop 'any', output_port 'any'.
@@ -25216,3 +25216,97 @@ AT_CHECK([cat hv2_offlows_table72.txt | grep -v NXST], [1], [dnl
OVN_CLEANUP([hv1], [hv2])
AT_CLEANUP
])
+
+OVN_FOR_EACH_NORTHD([
+AT_SETUP([ovn -- Static route with discard nexthop])
+ovn_start
+
+# Logical network:
+# Logical Router lr1 has Logical Switch sw1 (10.0.0.0/24) connected to it. sw1 has one
+# switch port sw1-ls1 (10.0.0.100)
+
+ovn-nbctl lr-add lr1
+
+ovn-nbctl ls-add sw1
+
+# Connect sw1 to lr1
+ovn-nbctl lrp-add lr1 lr1-sw1 00:00:01:01:02:03 10.0.0.1/24
+ovn-nbctl lsp-add sw1 sw1-lr1
+ovn-nbctl lsp-set-type sw1-lr1 router
+ovn-nbctl lsp-set-addresses sw1-lr1 router
+ovn-nbctl lsp-set-options sw1-lr1 router-port=lr1-sw1
+
+# Create logical port sw1-lp1 in sw1
+ovn-nbctl lsp-add sw1 sw1-lp1 \
+-- lsp-set-addresses sw1-lp1 "00:00:04:01:02:03 10.0.0.100"
+
+# Create one hypervisor and create OVS ports corresponding to logical ports.
+net_add n1
+
+sim_add hv1
+as hv1
+ovs-vsctl add-br br-phys
+ovn_attach n1 br-phys 192.168.0.1
+
+ovs-vsctl -- add-port br-int hv1-vif1 -- \
+ set interface hv1-vif1 external-ids:iface-id=sw1-lp1 \
+ options:tx_pcap=hv1/vif1-tx.pcap \
+ options:rxq_pcap=hv1/vif1-rx.pcap \
+ ofport-request=1
+
+# Install static routes to drop traffic
+ovn-nbctl lr-route-add lr1 20.0.0.0/24 discard
+
+# Allow some time for ovn-controller to catch up.
+ovn-nbctl --wait=hv sync
+
+# Check logical flows for drop rule
+AT_CHECK([ovn-sbctl dump-flows | grep lr_in_ip_routing | grep "20.0.0.0/24" | \
+ grep drop | wc -l], [0], [dnl
+1
+])
+
+# Send packet.
+packet="inport==\"sw1-lp1\" && eth.src==00:00:04:01:02:03 &&
+ eth.dst==00:00:01:01:02:03 && ip4 && ip.ttl==64 &&
+ ip4.src==10.0.0.100 && ip4.dst==20.0.0.200 &&
+ udp && udp.src==53 && udp.dst==4369"
+
+as hv1 ovs-appctl -t ovn-controller inject-pkt "$packet"
+
+# Check if packet hit the drop rule
+AT_CHECK([ovs-ofctl dump-flows br-int | grep "nw_dst=20.0.0.0/24" | \
+ grep "actions=drop" | grep "n_packets=1" | wc -l], [0], [dnl
+1
+])
+
+# Remove destination match and add discard route with source match
+ovn-nbctl lr-route-del lr1 20.0.0.0/24
+ovn-nbctl --policy="src-ip" lr-route-add lr1 10.0.0.0/24 discard
+
+# Allow some time for ovn-controller to catch up.
+ovn-nbctl --wait=hv sync
+
+# Check logical flows for drop rule
+AT_CHECK([ovn-sbctl dump-flows | grep lr_in_ip_routing | grep "10.0.0.0/24" | \
+ grep drop | wc -l], [0], [dnl
+1
+])
+
+# Send packet.
+packet="inport==\"sw1-lp1\" && eth.src==00:00:04:01:02:03 &&
+ eth.dst==00:00:01:01:02:03 && ip4 && ip.ttl==64 &&
+ ip4.src==10.0.0.100 && ip4.dst==20.0.0.200 &&
+ udp && udp.src==53 && udp.dst==4369"
+
+as hv1 ovs-appctl -t ovn-controller inject-pkt "$packet"
+
+# Check if packet hit the drop rule
+AT_CHECK([ovs-ofctl dump-flows br-int "nw_src=10.0.0.0/24" | \
+ grep "actions=drop" | grep "n_packets=1" | wc -l], [0], [dnl
+1
+])
+
+OVN_CLEANUP([hv1])
+AT_CLEANUP
+])
\ No newline at end of file
@@ -673,7 +673,8 @@
logical port. If <var>port</var> is specified, packets that
match this route will be sent out that port. When
<var>port</var> is omitted, OVN infers the output port based
- on <var>nexthop</var>.
+ on <var>nexthop</var>. Nexthop can be set to <var>discard</var>
+ for dropping packets which match the given route.
</p>
<p>
@@ -3972,6 +3972,7 @@ nbctl_lr_route_add(struct ctl_context *ctx)
const char *policy = shash_find_data(&ctx->options, "--policy");
bool is_src_route = false;
+
if (policy) {
if (!strcmp(policy, "src-ip")) {
is_src_route = true;
@@ -3992,13 +3993,18 @@ nbctl_lr_route_add(struct ctl_context *ctx)
return;
}
- next_hop = v6_prefix
- ? normalize_ipv6_addr_str(ctx->argv[3])
- : normalize_ipv4_addr_str(ctx->argv[3]);
- if (!next_hop) {
- ctl_error(ctx, "bad %s nexthop argument: %s",
- v6_prefix ? "IPv6" : "IPv4", ctx->argv[3]);
- goto cleanup;
+ bool is_discard_route = !strcmp(ctx->argv[3], "discard");
+ if (is_discard_route) {
+ next_hop = xasprintf("discard");
+ } else {
+ next_hop = v6_prefix
+ ? normalize_ipv6_addr_str(ctx->argv[3])
+ : normalize_ipv4_addr_str(ctx->argv[3]);
+ if (!next_hop) {
+ ctl_error(ctx, "bad %s nexthop argument: %s",
+ v6_prefix ? "IPv6" : "IPv4", ctx->argv[3]);
+ goto cleanup;
+ }
}
struct shash_node *bfd = shash_find(&ctx->options, "--bfd");
@@ -4031,6 +4037,25 @@ nbctl_lr_route_add(struct ctl_context *ctx)
ecmp_symmetric_reply;
struct nbrec_logical_router_static_route *route =
nbctl_lr_get_route(lr, prefix, next_hop, is_src_route, ecmp);
+
+ /* Validations for nexthop = "discard" */
+ if (is_discard_route) {
+ if (ecmp) {
+ ctl_error(ctx, "ecmp is not valid for discard routes.");
+ goto cleanup;
+ }
+ if (bfd) {
+ ctl_error(ctx, "bfd dst_ip cannot be discard.");
+ goto cleanup;
+ }
+ if (ctx->argc == 5) {
+ if (is_discard_route) {
+ ctl_error(ctx, "outport is not valid for discard routes.");
+ goto cleanup;
+ }
+ }
+ }
+
if (!ecmp) {
if (route) {
if (!may_exist) {
@@ -4072,6 +4097,13 @@ nbctl_lr_route_add(struct ctl_context *ctx)
goto cleanup;
}
+ struct nbrec_logical_router_static_route *discard_route =
+ nbctl_lr_get_route(lr, prefix, "discard", is_src_route, true);
+ if (discard_route) {
+ ctl_error(ctx, "discard nexthop for the same ECMP route exists.");
+ goto cleanup;
+ }
+
route = nbrec_logical_router_static_route_insert(ctx->txn);
nbrec_logical_router_static_route_set_ip_prefix(route, prefix);
nbrec_logical_router_static_route_set_nexthop(route, next_hop);
@@ -4146,10 +4178,14 @@ nbctl_lr_route_del(struct ctl_context *ctx)
char *nexthop = NULL;
if (ctx->argc >= 4) {
- nexthop = normalize_prefix_str(ctx->argv[3]);
- if (!nexthop) {
- ctl_error(ctx, "bad nexthop argument: %s", ctx->argv[3]);
- return;
+ if (!strcmp(ctx->argv[3], "discard")) {
+ nexthop = xasprintf("discard");
+ } else {
+ nexthop = normalize_prefix_str(ctx->argv[3]);
+ if (!nexthop) {
+ ctl_error(ctx, "bad nexthop argument: %s", ctx->argv[3]);
+ return;
+ }
}
}
@@ -4190,8 +4226,9 @@ nbctl_lr_route_del(struct ctl_context *ctx)
/* Compare nexthop, if specified. */
if (nexthop) {
- char *rt_nexthop =
- normalize_prefix_str(lr->static_routes[i]->nexthop);
+ char *rt_nexthop = !strcmp(lr->static_routes[i]->nexthop, "discard")
+ ? xasprintf("discard")
+ : normalize_prefix_str(lr->static_routes[i]->nexthop);
if (!rt_nexthop) {
/* Ignore existing nexthop we couldn't parse. */
continue;
@@ -5579,7 +5616,9 @@ print_route(const struct nbrec_logical_router_static_route *route,
{
char *prefix = normalize_prefix_str(route->ip_prefix);
- char *next_hop = normalize_prefix_str(route->nexthop);
+ char *next_hop = !strcmp(route->nexthop, "discard")
+ ? xasprintf("discard")
+ : normalize_prefix_str(route->nexthop);
ds_put_format(s, "%25s %25s", prefix, next_hop);
free(prefix);
free(next_hop);