Neale Ranns | cbe25aa | 2019-09-30 10:53:31 +0000 | [diff] [blame] | 1 | /* |
| 2 | * l2/l2_arp_term.c: IP v4 ARP L2 BD termination |
| 3 | * |
| 4 | * Copyright (c) 2010 Cisco and/or its affiliates. |
| 5 | * Licensed under the Apache License, Version 2.0 (the "License"); |
| 6 | * you may not use this file except in compliance with the License. |
| 7 | * You may obtain a copy of the License at: |
| 8 | * |
| 9 | * http://www.apache.org/licenses/LICENSE-2.0 |
| 10 | * |
| 11 | * Unless required by applicable law or agreed to in writing, software |
| 12 | * distributed under the License is distributed on an "AS IS" BASIS, |
| 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 14 | * See the License for the specific language governing permissions and |
| 15 | * limitations under the License. |
| 16 | */ |
| 17 | |
| 18 | #include <vlibmemory/api.h> |
| 19 | |
| 20 | #include <vnet/l2/l2_arp_term.h> |
| 21 | #include <vnet/l2/l2_input.h> |
| 22 | #include <vnet/l2/feat_bitmap.h> |
| 23 | |
| 24 | #include <vnet/ip/ip4_packet.h> |
Neale Ranns | e403113 | 2020-10-26 13:00:06 +0000 | [diff] [blame^] | 25 | #include <vnet/ip/ip6_packet.h> |
| 26 | #include <vnet/ip/icmp6.h> |
| 27 | #include <vnet/ip/ip6.h> |
| 28 | #include <vnet/ip/format.h> |
Neale Ranns | cbe25aa | 2019-09-30 10:53:31 +0000 | [diff] [blame] | 29 | #include <vnet/ethernet/arp_packet.h> |
| 30 | |
| 31 | static const u8 vrrp_prefix[] = { 0x00, 0x00, 0x5E, 0x00, 0x01 }; |
| 32 | |
| 33 | l2_arp_term_main_t l2_arp_term_main; |
| 34 | |
| 35 | /* |
| 36 | * ARP/ND Termination in a L2 Bridge Domain based on IP4/IP6 to MAC |
| 37 | * hash tables mac_by_ip4 and mac_by_ip6 for each BD. |
| 38 | */ |
| 39 | typedef enum |
| 40 | { |
| 41 | ARP_TERM_NEXT_L2_OUTPUT, |
| 42 | ARP_TERM_NEXT_DROP, |
| 43 | ARP_TERM_N_NEXT, |
| 44 | } arp_term_next_t; |
| 45 | |
| 46 | u32 arp_term_next_node_index[32]; |
| 47 | |
| 48 | typedef struct |
| 49 | { |
| 50 | u8 packet_data[64]; |
| 51 | } ethernet_arp_input_trace_t; |
| 52 | |
| 53 | #define foreach_ethernet_arp_error \ |
| 54 | _ (replies_sent, "ARP replies sent") \ |
| 55 | _ (l2_type_not_ethernet, "L2 type not ethernet") \ |
| 56 | _ (l3_type_not_ip4, "L3 type not IP4") \ |
| 57 | _ (l3_src_address_not_local, "IP4 source address not local to subnet") \ |
| 58 | _ (l3_dst_address_not_local, "IP4 destination address not local to subnet") \ |
| 59 | _ (l3_dst_address_unset, "IP4 destination address is unset") \ |
| 60 | _ (l3_src_address_is_local, "IP4 source address matches local interface") \ |
| 61 | _ (l3_src_address_learned, "ARP request IP4 source address learned") \ |
| 62 | _ (replies_received, "ARP replies received") \ |
| 63 | _ (opcode_not_request, "ARP opcode not request") \ |
| 64 | _ (proxy_arp_replies_sent, "Proxy ARP replies sent") \ |
| 65 | _ (l2_address_mismatch, "ARP hw addr does not match L2 frame src addr") \ |
| 66 | _ (gratuitous_arp, "ARP probe or announcement dropped") \ |
| 67 | _ (interface_no_table, "Interface is not mapped to an IP table") \ |
| 68 | _ (interface_not_ip_enabled, "Interface is not IP enabled") \ |
| 69 | _ (unnumbered_mismatch, "RX interface is unnumbered to different subnet") \ |
| 70 | |
| 71 | typedef enum |
| 72 | { |
| 73 | #define _(sym,string) ETHERNET_ARP_ERROR_##sym, |
| 74 | foreach_ethernet_arp_error |
| 75 | #undef _ |
| 76 | ETHERNET_ARP_N_ERROR, |
| 77 | } ethernet_arp_reply_error_t; |
| 78 | |
| 79 | static char *ethernet_arp_error_strings[] = { |
| 80 | #define _(sym,string) string, |
| 81 | foreach_ethernet_arp_error |
| 82 | #undef _ |
| 83 | }; |
| 84 | |
| 85 | static u8 * |
| 86 | format_arp_term_input_trace (u8 * s, va_list * va) |
| 87 | { |
| 88 | CLIB_UNUSED (vlib_main_t * vm) = va_arg (*va, vlib_main_t *); |
| 89 | CLIB_UNUSED (vlib_node_t * node) = va_arg (*va, vlib_node_t *); |
| 90 | ethernet_arp_input_trace_t *t = va_arg (*va, ethernet_arp_input_trace_t *); |
| 91 | |
| 92 | /* arp-term trace data saved is either arp or ip6/icmp6 packet: |
| 93 | - for arp, the 1st 16-bit field is hw type of value of 0x0001. |
| 94 | - for ip6, the first nibble has value of 6. */ |
| 95 | s = format (s, "%U", t->packet_data[0] == 0 ? |
| 96 | format_ethernet_arp_header : format_ip6_header, |
| 97 | t->packet_data, sizeof (t->packet_data)); |
| 98 | |
| 99 | return s; |
| 100 | } |
| 101 | |
| 102 | void |
| 103 | l2_arp_term_set_publisher_node (bool on) |
| 104 | { |
| 105 | l2_arp_term_main_t *l2am = &l2_arp_term_main; |
| 106 | |
| 107 | l2am->publish = on; |
| 108 | } |
| 109 | |
| 110 | static int |
| 111 | l2_arp_term_publish (l2_arp_term_publish_event_t * ctx) |
| 112 | { |
| 113 | l2_arp_term_main_t *l2am = &l2_arp_term_main; |
| 114 | |
| 115 | vec_add1 (l2am->publish_events, *ctx); |
| 116 | |
| 117 | vlib_process_signal_event (vlib_get_main (), |
| 118 | l2_arp_term_process_node.index, |
| 119 | L2_ARP_TERM_EVENT_PUBLISH, 0); |
| 120 | |
| 121 | return 0; |
| 122 | } |
| 123 | |
| 124 | static inline void |
| 125 | l2_arp_term_publish_v4_dp (u32 sw_if_index, |
| 126 | const ethernet_arp_ip4_over_ethernet_address_t * a) |
| 127 | { |
| 128 | l2_arp_term_main_t *l2am = &l2_arp_term_main; |
| 129 | |
| 130 | if (!l2am->publish) |
| 131 | return; |
| 132 | |
| 133 | l2_arp_term_publish_event_t args = { |
| 134 | .sw_if_index = sw_if_index, |
| 135 | .type = IP46_TYPE_IP4, |
| 136 | .ip.ip4 = a->ip4, |
| 137 | .mac = a->mac, |
| 138 | }; |
| 139 | |
| 140 | vl_api_rpc_call_main_thread (l2_arp_term_publish, (u8 *) & args, |
| 141 | sizeof (args)); |
| 142 | } |
| 143 | |
| 144 | static inline void |
| 145 | l2_arp_term_publish_v6_dp (u32 sw_if_index, |
| 146 | const ip6_address_t * addr, |
| 147 | const mac_address_t * mac) |
| 148 | { |
| 149 | l2_arp_term_main_t *l2am = &l2_arp_term_main; |
| 150 | |
| 151 | if (!l2am->publish) |
| 152 | return; |
| 153 | |
| 154 | l2_arp_term_publish_event_t args = { |
| 155 | .sw_if_index = sw_if_index, |
| 156 | .type = IP46_TYPE_IP6, |
| 157 | .ip.ip6 = *addr, |
| 158 | .mac = *mac, |
| 159 | }; |
| 160 | |
| 161 | vl_api_rpc_call_main_thread (l2_arp_term_publish, (u8 *) & args, |
| 162 | sizeof (args)); |
| 163 | } |
| 164 | |
| 165 | static inline int |
| 166 | vnet_ip6_nd_term (vlib_main_t * vm, |
| 167 | vlib_node_runtime_t * node, |
| 168 | vlib_buffer_t * p0, |
| 169 | ethernet_header_t * eth, |
| 170 | ip6_header_t * ip, u32 sw_if_index, u16 bd_index) |
| 171 | { |
| 172 | icmp6_neighbor_solicitation_or_advertisement_header_t *ndh; |
| 173 | mac_address_t mac; |
| 174 | |
| 175 | mac_address_from_bytes (&mac, eth->src_address); |
| 176 | ndh = ip6_next_header (ip); |
| 177 | if (ndh->icmp.type != ICMP6_neighbor_solicitation && |
| 178 | ndh->icmp.type != ICMP6_neighbor_advertisement) |
| 179 | return 0; |
| 180 | |
| 181 | if (PREDICT_FALSE ((node->flags & VLIB_NODE_FLAG_TRACE) && |
| 182 | (p0->flags & VLIB_BUFFER_IS_TRACED))) |
| 183 | { |
| 184 | u8 *t0 = vlib_add_trace (vm, node, p0, |
| 185 | sizeof (icmp6_input_trace_t)); |
| 186 | clib_memcpy (t0, ip, sizeof (icmp6_input_trace_t)); |
| 187 | } |
| 188 | |
| 189 | /* Check if anyone want ND events for L2 BDs */ |
| 190 | if (PREDICT_FALSE (!ip6_address_is_link_local_unicast (&ip->src_address))) |
| 191 | { |
| 192 | l2_arp_term_publish_v6_dp (sw_if_index, &ip->src_address, &mac); |
| 193 | } |
| 194 | |
| 195 | /* Check if MAC entry exsist for solicited target IP */ |
| 196 | if (ndh->icmp.type == ICMP6_neighbor_solicitation) |
| 197 | { |
| 198 | icmp6_neighbor_discovery_ethernet_link_layer_address_option_t *opt; |
| 199 | l2_bridge_domain_t *bd_config; |
| 200 | u8 *macp; |
| 201 | |
| 202 | opt = (void *) (ndh + 1); |
| 203 | if ((opt->header.type != |
| 204 | ICMP6_NEIGHBOR_DISCOVERY_OPTION_source_link_layer_address) || |
| 205 | (opt->header.n_data_u64s != 1)) |
| 206 | return 0; /* source link layer address option not present */ |
| 207 | |
| 208 | bd_config = vec_elt_at_index (l2input_main.bd_configs, bd_index); |
| 209 | macp = |
| 210 | (u8 *) hash_get_mem (bd_config->mac_by_ip6, &ndh->target_address); |
| 211 | if (macp) |
| 212 | { /* found ip-mac entry, generate eighbor advertisement response */ |
| 213 | int bogus_length; |
| 214 | vlib_node_runtime_t *error_node = |
| 215 | vlib_node_get_runtime (vm, ip6_icmp_input_node.index); |
| 216 | ip->dst_address = ip->src_address; |
| 217 | ip->src_address = ndh->target_address; |
| 218 | ip->hop_limit = 255; |
| 219 | opt->header.type = |
| 220 | ICMP6_NEIGHBOR_DISCOVERY_OPTION_target_link_layer_address; |
| 221 | clib_memcpy (opt->ethernet_address, macp, 6); |
| 222 | ndh->icmp.type = ICMP6_neighbor_advertisement; |
| 223 | ndh->advertisement_flags = clib_host_to_net_u32 |
| 224 | (ICMP6_NEIGHBOR_ADVERTISEMENT_FLAG_SOLICITED | |
| 225 | ICMP6_NEIGHBOR_ADVERTISEMENT_FLAG_OVERRIDE); |
| 226 | ndh->icmp.checksum = 0; |
| 227 | ndh->icmp.checksum = |
| 228 | ip6_tcp_udp_icmp_compute_checksum (vm, p0, ip, &bogus_length); |
| 229 | clib_memcpy (eth->dst_address, eth->src_address, 6); |
| 230 | clib_memcpy (eth->src_address, macp, 6); |
| 231 | vlib_error_count (vm, error_node->node_index, |
| 232 | ICMP6_ERROR_NEIGHBOR_ADVERTISEMENTS_TX, 1); |
| 233 | return 1; |
| 234 | } |
| 235 | } |
| 236 | |
| 237 | return 0; |
| 238 | |
| 239 | } |
| 240 | |
| 241 | static uword |
| 242 | arp_term_l2bd (vlib_main_t * vm, |
| 243 | vlib_node_runtime_t * node, vlib_frame_t * frame) |
| 244 | { |
| 245 | l2input_main_t *l2im = &l2input_main; |
| 246 | u32 n_left_from, next_index, *from, *to_next; |
| 247 | u32 n_replies_sent = 0; |
| 248 | u16 last_bd_index = ~0; |
| 249 | l2_bridge_domain_t *last_bd_config = 0; |
| 250 | l2_input_config_t *cfg0; |
| 251 | |
| 252 | from = vlib_frame_vector_args (frame); |
| 253 | n_left_from = frame->n_vectors; |
| 254 | next_index = node->cached_next_index; |
| 255 | |
| 256 | while (n_left_from > 0) |
| 257 | { |
| 258 | u32 n_left_to_next; |
| 259 | |
| 260 | vlib_get_next_frame (vm, node, next_index, to_next, n_left_to_next); |
| 261 | |
| 262 | while (n_left_from > 0 && n_left_to_next > 0) |
| 263 | { |
| 264 | vlib_buffer_t *p0; |
| 265 | ethernet_header_t *eth0; |
| 266 | ethernet_arp_header_t *arp0; |
| 267 | ip6_header_t *iph0; |
| 268 | u8 *l3h0; |
| 269 | u32 pi0, error0, next0, sw_if_index0; |
| 270 | u16 ethertype0; |
| 271 | u16 bd_index0; |
| 272 | u32 ip0; |
| 273 | u8 *macp0; |
| 274 | |
| 275 | pi0 = from[0]; |
| 276 | to_next[0] = pi0; |
| 277 | from += 1; |
| 278 | to_next += 1; |
| 279 | n_left_from -= 1; |
| 280 | n_left_to_next -= 1; |
| 281 | |
| 282 | p0 = vlib_get_buffer (vm, pi0); |
| 283 | // Terminate only local (SHG == 0) ARP |
| 284 | if (vnet_buffer (p0)->l2.shg != 0) |
| 285 | goto next_l2_feature; |
| 286 | |
| 287 | eth0 = vlib_buffer_get_current (p0); |
| 288 | l3h0 = (u8 *) eth0 + vnet_buffer (p0)->l2.l2_len; |
| 289 | ethertype0 = clib_net_to_host_u16 (*(u16 *) (l3h0 - 2)); |
| 290 | arp0 = (ethernet_arp_header_t *) l3h0; |
| 291 | |
| 292 | if (ethertype0 != ETHERNET_TYPE_ARP) |
| 293 | goto check_ip6_nd; |
| 294 | |
| 295 | if ((arp0->opcode != |
| 296 | clib_host_to_net_u16 (ETHERNET_ARP_OPCODE_request)) && |
| 297 | (arp0->opcode != |
| 298 | clib_host_to_net_u16 (ETHERNET_ARP_OPCODE_reply))) |
| 299 | goto check_ip6_nd; |
| 300 | |
| 301 | /* Must be ARP request/reply packet here */ |
| 302 | if (PREDICT_FALSE ((node->flags & VLIB_NODE_FLAG_TRACE) && |
| 303 | (p0->flags & VLIB_BUFFER_IS_TRACED))) |
| 304 | { |
| 305 | u8 *t0 = vlib_add_trace (vm, node, p0, |
| 306 | sizeof (ethernet_arp_input_trace_t)); |
| 307 | clib_memcpy_fast (t0, l3h0, |
| 308 | sizeof (ethernet_arp_input_trace_t)); |
| 309 | } |
| 310 | |
| 311 | error0 = 0; |
| 312 | error0 = |
| 313 | (arp0->l2_type != |
| 314 | clib_net_to_host_u16 (ETHERNET_ARP_HARDWARE_TYPE_ethernet) |
| 315 | ? ETHERNET_ARP_ERROR_l2_type_not_ethernet : error0); |
| 316 | error0 = |
| 317 | (arp0->l3_type != |
| 318 | clib_net_to_host_u16 (ETHERNET_TYPE_IP4) ? |
| 319 | ETHERNET_ARP_ERROR_l3_type_not_ip4 : error0); |
| 320 | |
| 321 | sw_if_index0 = vnet_buffer (p0)->sw_if_index[VLIB_RX]; |
| 322 | |
| 323 | if (error0) |
| 324 | goto drop; |
| 325 | |
| 326 | /* Trash ARP packets whose ARP-level source addresses do not |
| 327 | match, or if requester address is mcast */ |
| 328 | if (PREDICT_FALSE |
| 329 | (!ethernet_mac_address_equal (eth0->src_address, |
| 330 | arp0->ip4_over_ethernet[0]. |
| 331 | mac.bytes)) |
| 332 | || ethernet_address_cast (arp0->ip4_over_ethernet[0].mac.bytes)) |
| 333 | { |
| 334 | /* VRRP virtual MAC may be different to SMAC in ARP reply */ |
| 335 | if (!ethernet_mac_address_equal |
| 336 | (arp0->ip4_over_ethernet[0].mac.bytes, vrrp_prefix)) |
| 337 | { |
| 338 | error0 = ETHERNET_ARP_ERROR_l2_address_mismatch; |
| 339 | goto drop; |
| 340 | } |
| 341 | } |
| 342 | if (PREDICT_FALSE |
| 343 | (ip4_address_is_multicast (&arp0->ip4_over_ethernet[0].ip4))) |
| 344 | { |
| 345 | error0 = ETHERNET_ARP_ERROR_l3_src_address_not_local; |
| 346 | goto drop; |
| 347 | } |
| 348 | |
| 349 | /* Check if anyone want ARP request events for L2 BDs */ |
| 350 | l2_arp_term_publish_v4_dp (sw_if_index0, |
| 351 | &arp0->ip4_over_ethernet[0]); |
| 352 | |
| 353 | /* lookup BD mac_by_ip4 hash table for MAC entry */ |
| 354 | ip0 = arp0->ip4_over_ethernet[1].ip4.as_u32; |
| 355 | bd_index0 = vnet_buffer (p0)->l2.bd_index; |
| 356 | if (PREDICT_FALSE ((bd_index0 != last_bd_index) |
| 357 | || (last_bd_index == (u16) ~ 0))) |
| 358 | { |
| 359 | last_bd_index = bd_index0; |
| 360 | last_bd_config = vec_elt_at_index (l2im->bd_configs, bd_index0); |
| 361 | } |
| 362 | macp0 = (u8 *) hash_get (last_bd_config->mac_by_ip4, ip0); |
| 363 | |
| 364 | if (PREDICT_FALSE (!macp0)) |
| 365 | goto next_l2_feature; /* MAC not found */ |
| 366 | if (PREDICT_FALSE (arp0->ip4_over_ethernet[0].ip4.as_u32 == |
| 367 | arp0->ip4_over_ethernet[1].ip4.as_u32)) |
| 368 | goto next_l2_feature; /* GARP */ |
| 369 | |
| 370 | /* MAC found, send ARP reply - |
| 371 | Convert ARP request packet to ARP reply */ |
| 372 | arp0->opcode = clib_host_to_net_u16 (ETHERNET_ARP_OPCODE_reply); |
| 373 | arp0->ip4_over_ethernet[1] = arp0->ip4_over_ethernet[0]; |
| 374 | arp0->ip4_over_ethernet[0].ip4.as_u32 = ip0; |
| 375 | mac_address_from_bytes (&arp0->ip4_over_ethernet[0].mac, macp0); |
| 376 | clib_memcpy_fast (eth0->dst_address, eth0->src_address, 6); |
| 377 | clib_memcpy_fast (eth0->src_address, macp0, 6); |
| 378 | n_replies_sent += 1; |
| 379 | |
| 380 | output_response: |
| 381 | /* For BVI, need to use l2-fwd node to send ARP reply as |
| 382 | l2-output node cannot output packet to BVI properly */ |
| 383 | cfg0 = vec_elt_at_index (l2im->configs, sw_if_index0); |
Neale Ranns | 47a3d99 | 2020-09-29 15:38:51 +0000 | [diff] [blame] | 384 | if (PREDICT_FALSE (l2_input_is_bvi (cfg0))) |
Neale Ranns | cbe25aa | 2019-09-30 10:53:31 +0000 | [diff] [blame] | 385 | { |
| 386 | vnet_buffer (p0)->l2.feature_bitmap |= L2INPUT_FEAT_FWD; |
| 387 | vnet_buffer (p0)->sw_if_index[VLIB_RX] = 0; |
| 388 | goto next_l2_feature; |
| 389 | } |
| 390 | |
| 391 | /* Send ARP/ND reply back out input interface through l2-output */ |
| 392 | vnet_buffer (p0)->sw_if_index[VLIB_TX] = sw_if_index0; |
| 393 | next0 = ARP_TERM_NEXT_L2_OUTPUT; |
| 394 | vlib_validate_buffer_enqueue_x1 (vm, node, next_index, |
| 395 | to_next, n_left_to_next, pi0, |
| 396 | next0); |
| 397 | continue; |
| 398 | |
| 399 | check_ip6_nd: |
| 400 | /* IP6 ND event notification or solicitation handling to generate |
| 401 | local response instead of flooding */ |
| 402 | iph0 = (ip6_header_t *) l3h0; |
| 403 | if (PREDICT_FALSE (ethertype0 == ETHERNET_TYPE_IP6 && |
| 404 | iph0->protocol == IP_PROTOCOL_ICMP6 && |
| 405 | !ip6_address_is_unspecified |
| 406 | (&iph0->src_address))) |
| 407 | { |
| 408 | sw_if_index0 = vnet_buffer (p0)->sw_if_index[VLIB_RX]; |
| 409 | if (vnet_ip6_nd_term |
| 410 | (vm, node, p0, eth0, iph0, sw_if_index0, |
| 411 | vnet_buffer (p0)->l2.bd_index)) |
| 412 | goto output_response; |
| 413 | } |
| 414 | |
| 415 | next_l2_feature: |
| 416 | { |
| 417 | next0 = vnet_l2_feature_next (p0, arp_term_next_node_index, |
| 418 | L2INPUT_FEAT_ARP_TERM); |
| 419 | vlib_validate_buffer_enqueue_x1 (vm, node, next_index, |
| 420 | to_next, n_left_to_next, |
| 421 | pi0, next0); |
| 422 | continue; |
| 423 | } |
| 424 | |
| 425 | drop: |
| 426 | if (0 == arp0->ip4_over_ethernet[0].ip4.as_u32 || |
| 427 | (arp0->ip4_over_ethernet[0].ip4.as_u32 == |
| 428 | arp0->ip4_over_ethernet[1].ip4.as_u32)) |
| 429 | { |
| 430 | error0 = ETHERNET_ARP_ERROR_gratuitous_arp; |
| 431 | } |
| 432 | next0 = ARP_TERM_NEXT_DROP; |
| 433 | p0->error = node->errors[error0]; |
| 434 | |
| 435 | vlib_validate_buffer_enqueue_x1 (vm, node, next_index, |
| 436 | to_next, n_left_to_next, pi0, |
| 437 | next0); |
| 438 | } |
| 439 | |
| 440 | vlib_put_next_frame (vm, node, next_index, n_left_to_next); |
| 441 | } |
| 442 | |
| 443 | vlib_error_count (vm, node->node_index, |
| 444 | ETHERNET_ARP_ERROR_replies_sent, n_replies_sent); |
| 445 | return frame->n_vectors; |
| 446 | } |
| 447 | |
| 448 | /* *INDENT-OFF* */ |
| 449 | VLIB_REGISTER_NODE (arp_term_l2bd_node, static) = { |
| 450 | .function = arp_term_l2bd, |
| 451 | .name = "arp-term-l2bd", |
| 452 | .vector_size = sizeof (u32), |
| 453 | .n_errors = ETHERNET_ARP_N_ERROR, |
| 454 | .error_strings = ethernet_arp_error_strings, |
| 455 | .n_next_nodes = ARP_TERM_N_NEXT, |
| 456 | .next_nodes = { |
| 457 | [ARP_TERM_NEXT_L2_OUTPUT] = "l2-output", |
| 458 | [ARP_TERM_NEXT_DROP] = "error-drop", |
| 459 | }, |
| 460 | .format_buffer = format_ethernet_arp_header, |
| 461 | .format_trace = format_arp_term_input_trace, |
| 462 | }; |
| 463 | /* *INDENT-ON* */ |
| 464 | |
| 465 | clib_error_t * |
| 466 | arp_term_init (vlib_main_t * vm) |
| 467 | { |
| 468 | // Initialize the feature next-node indexes |
| 469 | feat_bitmap_init_next_nodes (vm, |
| 470 | arp_term_l2bd_node.index, |
| 471 | L2INPUT_N_FEAT, |
| 472 | l2input_get_feat_names (), |
| 473 | arp_term_next_node_index); |
| 474 | return 0; |
| 475 | } |
| 476 | |
| 477 | VLIB_INIT_FUNCTION (arp_term_init); |
| 478 | |
| 479 | /* |
| 480 | * fd.io coding-style-patch-verification: ON |
| 481 | * |
| 482 | * Local Variables: |
| 483 | * eval: (c-set-style "gnu") |
| 484 | * End: |
| 485 | */ |