avf: flow support enhancement
[vpp.git] / src / plugins / avf / flow.c
1 /*
2  *------------------------------------------------------------------
3  * Copyright (c) 2020 Intel and/or its affiliates.
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at:
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  *------------------------------------------------------------------
16  */
17
18 #include <stdbool.h>
19 #include <vlib/vlib.h>
20 #include <vppinfra/ring.h>
21 #include <vlib/unix/unix.h>
22 #include <vlib/pci/pci.h>
23 #include <vnet/ethernet/ethernet.h>
24
25 #include <avf/avf.h>
26 #include <avf/avf_advanced_flow.h>
27
28 #define FLOW_IS_ETHERNET_CLASS(f) (f->type == VNET_FLOW_TYPE_ETHERNET)
29
30 #define FLOW_IS_IPV4_CLASS(f)                                                 \
31   ((f->type == VNET_FLOW_TYPE_IP4) ||                                         \
32    (f->type == VNET_FLOW_TYPE_IP4_N_TUPLE) ||                                 \
33    (f->type == VNET_FLOW_TYPE_IP4_N_TUPLE_TAGGED) ||                          \
34    (f->type == VNET_FLOW_TYPE_IP4_VXLAN) ||                                   \
35    (f->type == VNET_FLOW_TYPE_IP4_GTPC) ||                                    \
36    (f->type == VNET_FLOW_TYPE_IP4_GTPU) ||                                    \
37    (f->type == VNET_FLOW_TYPE_IP4_L2TPV3OIP) ||                               \
38    (f->type == VNET_FLOW_TYPE_IP4_IPSEC_ESP) ||                               \
39    (f->type == VNET_FLOW_TYPE_IP4_IPSEC_AH))
40
41 #define FLOW_IS_IPV6_CLASS(f)                                                 \
42   ((f->type == VNET_FLOW_TYPE_IP6) ||                                         \
43    (f->type == VNET_FLOW_TYPE_IP6_N_TUPLE) ||                                 \
44    (f->type == VNET_FLOW_TYPE_IP6_N_TUPLE_TAGGED) ||                          \
45    (f->type == VNET_FLOW_TYPE_IP6_VXLAN))
46
47 /* check if flow is L3 type */
48 #define FLOW_IS_L3_TYPE(f)                                                    \
49   ((f->type == VNET_FLOW_TYPE_IP4) || (f->type == VNET_FLOW_TYPE_IP6))
50
51 /* check if flow is L4 type */
52 #define FLOW_IS_L4_TYPE(f)                                                    \
53   ((f->type == VNET_FLOW_TYPE_IP4_N_TUPLE) ||                                 \
54    (f->type == VNET_FLOW_TYPE_IP6_N_TUPLE) ||                                 \
55    (f->type == VNET_FLOW_TYPE_IP4_N_TUPLE_TAGGED) ||                          \
56    (f->type == VNET_FLOW_TYPE_IP6_N_TUPLE_TAGGED))
57
58 /* check if flow is L4 tunnel type */
59 #define FLOW_IS_L4_TUNNEL_TYPE(f)                                             \
60   ((f->type == VNET_FLOW_TYPE_IP4_VXLAN) ||                                   \
61    (f->type == VNET_FLOW_TYPE_IP6_VXLAN) ||                                   \
62    (f->type == VNET_FLOW_TYPE_IP4_GTPC) ||                                    \
63    (f->type == VNET_FLOW_TYPE_IP4_GTPU))
64
65 int
66 avf_fdir_vc_op_callback (void *vc_hdl, enum virthnl_adv_ops vc_op, void *in,
67                          u32 in_len, void *out, u32 out_len)
68 {
69   u32 dev_instance = *(u32 *) vc_hdl;
70   avf_device_t *ad = avf_get_device (dev_instance);
71   clib_error_t *err = 0;
72   int is_add;
73
74   if (vc_op >= VIRTCHNL_ADV_OP_MAX)
75     {
76       return -1;
77     }
78
79   switch (vc_op)
80     {
81     case VIRTCHNL_ADV_OP_ADD_FDIR_FILTER:
82       is_add = 1;
83       break;
84     case VIRTCHNL_ADV_OP_DEL_FDIR_FILTER:
85       is_add = 0;
86       break;
87     default:
88       avf_log_err (ad, "unsupported avf virtual channel opcode %u\n",
89                    (u32) vc_op);
90       return -1;
91     }
92
93   err = avf_program_flow (dev_instance, is_add, in, in_len, out, out_len);
94   if (err != 0)
95     {
96       avf_log_err (ad, "avf fdir program failed: %U", format_clib_error, err);
97       clib_error_free (err);
98       return -1;
99     }
100
101   avf_log_debug (ad, "avf fdir program success");
102   return 0;
103 }
104
105 static int
106 avf_flow_add (u32 dev_instance, vnet_flow_t *f, avf_flow_entry_t *fe)
107 {
108   avf_device_t *ad = avf_get_device (dev_instance);
109   int rv = 0;
110   int ret = 0;
111   u16 src_port = 0, dst_port = 0;
112   u16 src_port_mask = 0, dst_port_mask = 0;
113   u8 protocol = IP_PROTOCOL_RESERVED;
114   bool fate = false;
115   struct avf_flow_error error;
116
117   int layer = 0;
118   int action_count = 0;
119
120   struct avf_fdir_vc_ctx vc_ctx;
121   struct avf_fdir_conf *filter;
122   struct avf_flow_item avf_items[VIRTCHNL_MAX_NUM_PROTO_HDRS];
123   struct avf_flow_action avf_actions[VIRTCHNL_MAX_NUM_ACTIONS];
124
125   struct avf_ipv4_hdr ip4_spec = {}, ip4_mask = {};
126   struct avf_ipv6_hdr ip6_spec = {}, ip6_mask = {};
127   struct avf_tcp_hdr tcp_spec = {}, tcp_mask = {};
128   struct avf_udp_hdr udp_spec = {}, udp_mask = {};
129   struct avf_gtp_hdr gtp_spec = {}, gtp_mask = {};
130   struct avf_l2tpv3oip_hdr l2tpv3_spec = {}, l2tpv3_mask = {};
131   struct avf_esp_hdr esp_spec = {}, esp_mask = {};
132   struct avf_esp_hdr ah_spec = {}, ah_mask = {};
133
134   struct avf_flow_action_queue act_q = {};
135   struct avf_flow_action_mark act_msk = {};
136
137   enum
138   {
139     FLOW_UNKNOWN_CLASS,
140     FLOW_ETHERNET_CLASS,
141     FLOW_IPV4_CLASS,
142     FLOW_IPV6_CLASS,
143   } flow_class = FLOW_UNKNOWN_CLASS;
144
145   if (FLOW_IS_ETHERNET_CLASS (f))
146     flow_class = FLOW_ETHERNET_CLASS;
147   else if (FLOW_IS_IPV4_CLASS (f))
148     flow_class = FLOW_IPV4_CLASS;
149   else if (FLOW_IS_IPV6_CLASS (f))
150     flow_class = FLOW_IPV6_CLASS;
151   else
152     return VNET_FLOW_ERROR_NOT_SUPPORTED;
153
154   ret = avf_fdir_rcfg_create (&filter, 0, ad->vsi_id, ad->n_rx_queues);
155   if (ret)
156     {
157       rv = VNET_FLOW_ERROR_INTERNAL;
158       goto done;
159     }
160
161   /* init a virtual channel context */
162   vc_ctx.vc_hdl = &dev_instance;
163   vc_ctx.vc_op = avf_fdir_vc_op_callback;
164
165   clib_memset (avf_items, 0, sizeof (avf_actions));
166   clib_memset (avf_actions, 0, sizeof (avf_actions));
167
168   /* Ethernet Layer */
169   avf_items[layer].type = VIRTCHNL_PROTO_HDR_ETH;
170   avf_items[layer].spec = NULL;
171   avf_items[layer].mask = NULL;
172   layer++;
173
174   if (flow_class == FLOW_IPV4_CLASS)
175     {
176       vnet_flow_ip4_t *ip4_ptr = &f->ip4;
177
178       /* IPv4 Layer */
179       avf_items[layer].type = VIRTCHNL_PROTO_HDR_IPV4;
180       avf_items[layer].spec = &ip4_spec;
181       avf_items[layer].mask = &ip4_mask;
182       layer++;
183
184       // memset (&ip4_spec, 0, sizeof (ip4_spec));
185       // memset (&ip4_mask, 0, sizeof (ip4_mask));
186
187       if ((!ip4_ptr->src_addr.mask.as_u32) &&
188           (!ip4_ptr->dst_addr.mask.as_u32) && (!ip4_ptr->protocol.mask))
189         {
190           ;
191         }
192       else
193         {
194           ip4_spec.src_addr = ip4_ptr->src_addr.addr.as_u32;
195           ip4_mask.src_addr = ip4_ptr->src_addr.mask.as_u32;
196
197           ip4_spec.dst_addr = ip4_ptr->dst_addr.addr.as_u32;
198           ip4_mask.dst_addr = ip4_ptr->dst_addr.mask.as_u32;
199
200           ip4_spec.next_proto_id = ip4_ptr->protocol.prot;
201           ip4_mask.next_proto_id = ip4_ptr->protocol.mask;
202         }
203
204       if (FLOW_IS_L4_TYPE (f) || FLOW_IS_L4_TUNNEL_TYPE (f))
205         {
206           vnet_flow_ip4_n_tuple_t *ip4_n_ptr = &f->ip4_n_tuple;
207
208           src_port = ip4_n_ptr->src_port.port;
209           dst_port = ip4_n_ptr->dst_port.port;
210           src_port_mask = ip4_n_ptr->src_port.mask;
211           dst_port_mask = ip4_n_ptr->dst_port.mask;
212         }
213
214       protocol = ip4_ptr->protocol.prot;
215     }
216   else if (flow_class == FLOW_IPV6_CLASS)
217     {
218       vnet_flow_ip6_t *ip6_ptr = &f->ip6;
219
220       /* IPv6 Layer */
221       avf_items[layer].type = VIRTCHNL_PROTO_HDR_IPV4;
222       avf_items[layer].spec = &ip6_spec;
223       avf_items[layer].mask = &ip6_mask;
224       layer++;
225
226       // memset (&ip6_spec, 0, sizeof (ip6_spec));
227       // memset (&ip6_mask, 0, sizeof (ip6_mask));
228
229       if ((ip6_address_is_zero (&ip6_ptr->src_addr.mask)) &&
230           (ip6_address_is_zero (&ip6_ptr->dst_addr.mask)) &&
231           (!ip6_ptr->protocol.mask))
232         {
233           ;
234         }
235       else
236         {
237           clib_memcpy (ip6_spec.src_addr, &ip6_ptr->src_addr.addr,
238                        ARRAY_LEN (ip6_ptr->src_addr.addr.as_u8));
239           clib_memcpy (ip6_mask.src_addr, &ip6_ptr->src_addr.mask,
240                        ARRAY_LEN (ip6_ptr->src_addr.mask.as_u8));
241           clib_memcpy (ip6_spec.dst_addr, &ip6_ptr->dst_addr.addr,
242                        ARRAY_LEN (ip6_ptr->dst_addr.addr.as_u8));
243           clib_memcpy (ip6_mask.dst_addr, &ip6_ptr->dst_addr.mask,
244                        ARRAY_LEN (ip6_ptr->dst_addr.mask.as_u8));
245           ip6_spec.proto = ip6_ptr->protocol.prot;
246           ip6_mask.proto = ip6_ptr->protocol.mask;
247         }
248
249       if (FLOW_IS_L4_TYPE (f) || FLOW_IS_L4_TUNNEL_TYPE (f))
250         {
251           vnet_flow_ip6_n_tuple_t *ip6_n_ptr = &f->ip6_n_tuple;
252
253           src_port = ip6_n_ptr->src_port.port;
254           dst_port = ip6_n_ptr->dst_port.port;
255           src_port_mask = ip6_n_ptr->src_port.mask;
256           dst_port_mask = ip6_n_ptr->dst_port.mask;
257         }
258
259       protocol = ip6_ptr->protocol.prot;
260     }
261
262   if (FLOW_IS_L3_TYPE (f))
263     goto pattern_end;
264
265   /* Layer 4 */
266   switch (protocol)
267     {
268     case IP_PROTOCOL_L2TP:
269       avf_items[layer].type = VIRTCHNL_PROTO_HDR_L2TPV3;
270       avf_items[layer].spec = &l2tpv3_spec;
271       avf_items[layer].mask = &l2tpv3_mask;
272       layer++;
273
274       // memset (&l2tpv3_spec, 0, sizeof (l2tpv3_spec));
275       // memset (&l2tpv3_mask, 0, sizeof (l2tpv3_mask));
276
277       vnet_flow_ip4_l2tpv3oip_t *l2tph = &f->ip4_l2tpv3oip;
278       l2tpv3_spec.session_id = clib_host_to_net_u32 (l2tph->session_id);
279       l2tpv3_mask.session_id = ~0;
280       break;
281
282     case IP_PROTOCOL_IPSEC_ESP:
283       avf_items[layer].type = VIRTCHNL_PROTO_HDR_ESP;
284       avf_items[layer].spec = &esp_spec;
285       avf_items[layer].mask = &esp_mask;
286       layer++;
287
288       // memset (&esp_spec, 0, sizeof (esp_spec));
289       // memset (&esp_mask, 0, sizeof (esp_mask));
290
291       vnet_flow_ip4_ipsec_esp_t *esph = &f->ip4_ipsec_esp;
292       esp_spec.spi = clib_host_to_net_u32 (esph->spi);
293       esp_mask.spi = ~0;
294       break;
295
296     case IP_PROTOCOL_IPSEC_AH:
297       avf_items[layer].type = VIRTCHNL_PROTO_HDR_AH;
298       avf_items[layer].spec = &ah_spec;
299       avf_items[layer].mask = &ah_mask;
300       layer++;
301
302       // memset (&ah_spec, 0, sizeof (ah_spec));
303       // memset (&ah_mask, 0, sizeof (ah_mask));
304
305       vnet_flow_ip4_ipsec_ah_t *ah = &f->ip4_ipsec_ah;
306       ah_spec.spi = clib_host_to_net_u32 (ah->spi);
307       ah_mask.spi = ~0;
308       break;
309
310     case IP_PROTOCOL_TCP:
311       avf_items[layer].type = VIRTCHNL_PROTO_HDR_TCP;
312       avf_items[layer].spec = &tcp_spec;
313       avf_items[layer].mask = &tcp_mask;
314       layer++;
315
316       // memset (&tcp_spec, 0, sizeof (tcp_spec));
317       // memset (&tcp_mask, 0, sizeof (tcp_mask));
318
319       if (src_port_mask)
320         {
321           tcp_spec.src_port = clib_host_to_net_u16 (src_port);
322           tcp_mask.src_port = clib_host_to_net_u16 (src_port_mask);
323         }
324       if (dst_port_mask)
325         {
326           tcp_spec.dst_port = clib_host_to_net_u16 (dst_port);
327           tcp_mask.dst_port = clib_host_to_net_u16 (dst_port_mask);
328         }
329       break;
330
331     case IP_PROTOCOL_UDP:
332       avf_items[layer].type = VIRTCHNL_PROTO_HDR_UDP;
333       avf_items[layer].spec = &udp_spec;
334       avf_items[layer].mask = &udp_mask;
335       layer++;
336
337       // memset (&udp_spec, 0, sizeof (udp_spec));
338       // memset (&udp_mask, 0, sizeof (udp_mask));
339
340       if (src_port_mask)
341         {
342           udp_spec.src_port = clib_host_to_net_u16 (src_port);
343           udp_mask.src_port = clib_host_to_net_u16 (src_port_mask);
344         }
345       if (dst_port_mask)
346         {
347           udp_spec.dst_port = clib_host_to_net_u16 (dst_port);
348           udp_mask.dst_port = clib_host_to_net_u16 (dst_port_mask);
349         }
350
351       /* handle the UDP tunnels */
352       if (f->type == VNET_FLOW_TYPE_IP4_GTPU)
353         {
354           avf_items[layer].type = VIRTCHNL_PROTO_HDR_GTPU_IP;
355           avf_items[layer].spec = &gtp_spec;
356           avf_items[layer].mask = &gtp_mask;
357           layer++;
358
359           // memset (&gtp_spec, 0, sizeof (gtp_spec));
360           // memset (&gtp_mask, 0, sizeof (gtp_mask));
361
362           vnet_flow_ip4_gtpu_t *gu = &f->ip4_gtpu;
363           gtp_spec.teid = clib_host_to_net_u32 (gu->teid);
364           gtp_mask.teid = ~0;
365         }
366       break;
367
368     default:
369       rv = VNET_FLOW_ERROR_NOT_SUPPORTED;
370       goto done;
371     }
372
373 pattern_end:
374   /* pattern end flag  */
375   avf_items[layer].type = VIRTCHNL_PROTO_HDR_NONE;
376   ret = avf_fdir_parse_pattern (filter, avf_items, &error);
377   if (ret)
378     {
379       avf_log_err (ad, "avf fdir parse pattern failed: %s", error.message);
380       rv = VNET_FLOW_ERROR_NOT_SUPPORTED;
381       goto done;
382     }
383
384   /* Action */
385   /* Only one 'fate' can be assigned */
386   if (f->actions & VNET_FLOW_ACTION_REDIRECT_TO_QUEUE)
387     {
388       avf_actions[action_count].type = VIRTCHNL_ACTION_QUEUE;
389       avf_actions[action_count].conf = &act_q;
390
391       act_q.index = f->redirect_queue;
392       fate = true;
393       action_count++;
394     }
395
396   if (f->actions & VNET_FLOW_ACTION_DROP)
397     {
398       avf_actions[action_count].type = VIRTCHNL_ACTION_DROP;
399       avf_actions[action_count].conf = NULL;
400
401       if (fate == true)
402         {
403           rv = VNET_FLOW_ERROR_INTERNAL;
404           goto done;
405         }
406       else
407         fate = true;
408
409       action_count++;
410     }
411
412   if (fate == false)
413     {
414       avf_actions[action_count].type = VIRTCHNL_ACTION_PASSTHRU;
415       avf_actions[action_count].conf = NULL;
416
417       fate = true;
418       action_count++;
419     }
420
421   if (f->actions & VNET_FLOW_ACTION_MARK)
422     {
423       avf_actions[action_count].type = VIRTCHNL_ACTION_MARK;
424       avf_actions[action_count].conf = &act_msk;
425       action_count++;
426
427       act_msk.id = fe->mark;
428     }
429
430   /* action end flag */
431   avf_actions[action_count].type = VIRTCHNL_ACTION_NONE;
432
433   /* parse action */
434   ret = avf_fdir_parse_action (avf_actions, filter, &error);
435   if (ret)
436     {
437       avf_log_err (ad, "avf fdir parse action failed: %s", error.message);
438       rv = VNET_FLOW_ERROR_NOT_SUPPORTED;
439       goto done;
440     }
441
442   /* create flow rule, save rule */
443   ret = avf_fdir_rule_create (&vc_ctx, filter);
444
445   if (ret)
446     {
447       avf_log_err (ad, "avf fdir rule create failed: %s",
448                    avf_fdir_prgm_error_decode (ret));
449       rv = VNET_FLOW_ERROR_INTERNAL;
450       goto done;
451     }
452   else
453     {
454       fe->rcfg = filter;
455     }
456 done:
457
458   return rv;
459 }
460
461 int
462 avf_flow_ops_fn (vnet_main_t *vm, vnet_flow_dev_op_t op, u32 dev_instance,
463                  u32 flow_index, uword *private_data)
464 {
465   vnet_flow_t *flow = vnet_get_flow (flow_index);
466   avf_device_t *ad = avf_get_device (dev_instance);
467   avf_flow_entry_t *fe = NULL;
468   avf_flow_lookup_entry_t *fle = NULL;
469   int rv = 0;
470
471   if ((ad->feature_bitmap & VIRTCHNL_VF_OFFLOAD_FDIR_PF) == 0)
472     {
473       rv = VNET_FLOW_ERROR_NOT_SUPPORTED;
474       goto done;
475     }
476
477   if (op == VNET_FLOW_DEV_OP_ADD_FLOW)
478     {
479       pool_get (ad->flow_entries, fe);
480       fe->flow_index = flow->index;
481
482       /* if we need to mark packets, assign one mark */
483       if (flow->actions &
484           (VNET_FLOW_ACTION_MARK | VNET_FLOW_ACTION_REDIRECT_TO_NODE |
485            VNET_FLOW_ACTION_BUFFER_ADVANCE))
486         {
487           /* reserve slot 0 */
488           if (ad->flow_lookup_entries == 0)
489             pool_get_aligned (ad->flow_lookup_entries, fle,
490                               CLIB_CACHE_LINE_BYTES);
491           pool_get_aligned (ad->flow_lookup_entries, fle,
492                             CLIB_CACHE_LINE_BYTES);
493           fe->mark = fle - ad->flow_lookup_entries;
494
495           /* install entry in the lookup table */
496           clib_memset (fle, -1, sizeof (*fle));
497           if (flow->actions & VNET_FLOW_ACTION_MARK)
498             fle->flow_id = flow->mark_flow_id;
499           if (flow->actions & VNET_FLOW_ACTION_REDIRECT_TO_NODE)
500             fle->next_index = flow->redirect_device_input_next_index;
501           if (flow->actions & VNET_FLOW_ACTION_BUFFER_ADVANCE)
502             fle->buffer_advance = flow->buffer_advance;
503
504           if ((ad->flags & AVF_DEVICE_F_RX_FLOW_OFFLOAD) == 0)
505             {
506               ad->flags |= AVF_DEVICE_F_RX_FLOW_OFFLOAD;
507             }
508         }
509       else
510         fe->mark = 0;
511
512       switch (flow->type)
513         {
514         case VNET_FLOW_TYPE_IP4:
515         case VNET_FLOW_TYPE_IP6:
516         case VNET_FLOW_TYPE_IP4_N_TUPLE:
517         case VNET_FLOW_TYPE_IP6_N_TUPLE:
518         case VNET_FLOW_TYPE_IP4_VXLAN:
519         case VNET_FLOW_TYPE_IP4_GTPU:
520         case VNET_FLOW_TYPE_IP4_L2TPV3OIP:
521         case VNET_FLOW_TYPE_IP4_IPSEC_ESP:
522         case VNET_FLOW_TYPE_IP4_IPSEC_AH:
523           if ((rv = avf_flow_add (dev_instance, flow, fe)))
524             goto done;
525           break;
526         default:
527           rv = VNET_FLOW_ERROR_NOT_SUPPORTED;
528           goto done;
529         }
530
531       *private_data = fe - ad->flow_entries;
532     }
533   else if (op == VNET_FLOW_DEV_OP_DEL_FLOW)
534     {
535       fe = vec_elt_at_index (ad->flow_entries, *private_data);
536
537       struct avf_fdir_vc_ctx ctx;
538       ctx.vc_hdl = &dev_instance;
539       ctx.vc_op = avf_fdir_vc_op_callback;
540
541       rv = avf_fdir_rule_destroy (&ctx, fe->rcfg);
542       if (rv)
543         return VNET_FLOW_ERROR_INTERNAL;
544
545       if (fe->mark)
546         {
547           fle = pool_elt_at_index (ad->flow_lookup_entries, fe->mark);
548           clib_memset (fle, -1, sizeof (*fle));
549           pool_put_index (ad->flow_lookup_entries, fe->mark);
550         }
551
552       (void) avf_fdir_rcfg_destroy (fe->rcfg);
553       clib_memset (fe, 0, sizeof (*fe));
554       pool_put (ad->flow_entries, fe);
555       goto disable_rx_offload;
556     }
557   else
558     return VNET_FLOW_ERROR_NOT_SUPPORTED;
559
560 done:
561   if (rv)
562     {
563       if (fe)
564         {
565           clib_memset (fe, 0, sizeof (*fe));
566           pool_put (ad->flow_entries, fe);
567         }
568
569       if (fle)
570         {
571           clib_memset (fle, -1, sizeof (*fle));
572           pool_put (ad->flow_lookup_entries, fle);
573         }
574     }
575 disable_rx_offload:
576   if ((ad->flags & AVF_DEVICE_F_RX_FLOW_OFFLOAD) != 0 &&
577       pool_elts (ad->flow_entries) == 0)
578     {
579       ad->flags &= ~AVF_DEVICE_F_RX_FLOW_OFFLOAD;
580     }
581
582   return rv;
583 }
584
585 /*
586  * fd.io coding-style-patch-verification: ON
587  *
588  * Local Variables:
589  * eval: (c-set-style "gnu")
590  * End:
591  */