]> git.proxmox.com Git - mirror_frr.git/blob - isisd/fabricd.c
Merge pull request #3371 from donaldsharp/vtysh_ospf_instance
[mirror_frr.git] / isisd / fabricd.c
1 /*
2 * IS-IS Rout(e)ing protocol - OpenFabric extensions
3 *
4 * Copyright (C) 2018 Christian Franke
5 *
6 * This file is part of FreeRangeRouting (FRR)
7 *
8 * FRR is free software; you can redistribute it and/or modify it
9 * under the terms of the GNU General Public License as published by the
10 * Free Software Foundation; either version 2, or (at your option) any
11 * later version.
12 *
13 * FRR is distributed in the hope that it will be useful, but
14 * WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16 * General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License along
19 * with this program; see the file COPYING; if not, write to the Free Software
20 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
21 */
22 #include <zebra.h>
23 #include "isisd/fabricd.h"
24 #include "isisd/isisd.h"
25 #include "isisd/isis_memory.h"
26 #include "isisd/isis_circuit.h"
27 #include "isisd/isis_misc.h"
28 #include "isisd/isis_adjacency.h"
29 #include "isisd/isis_spf.h"
30 #include "isisd/isis_tlvs.h"
31 #include "isisd/isis_lsp.h"
32 #include "isisd/isis_spf_private.h"
33 #include "isisd/isis_tx_queue.h"
34
35 DEFINE_MTYPE_STATIC(ISISD, FABRICD_STATE, "ISIS OpenFabric")
36 DEFINE_MTYPE_STATIC(ISISD, FABRICD_NEIGHBOR, "ISIS OpenFabric Neighbor Entry")
37
38 /* Tracks initial synchronization as per section 2.4
39 *
40 * We declare the sync complete once we have seen at least one
41 * CSNP and there are no more LSPs with SSN or SRM set.
42 */
43 enum fabricd_sync_state {
44 FABRICD_SYNC_PENDING,
45 FABRICD_SYNC_STARTED,
46 FABRICD_SYNC_COMPLETE
47 };
48
49 struct fabricd {
50 struct isis_area *area;
51
52 enum fabricd_sync_state initial_sync_state;
53 time_t initial_sync_start;
54 struct isis_circuit *initial_sync_circuit;
55 struct thread *initial_sync_timeout;
56
57 struct isis_spftree *spftree;
58 struct skiplist *neighbors;
59 struct hash *neighbors_neighbors;
60
61 uint8_t tier;
62 uint8_t tier_config;
63 uint8_t tier_pending;
64 struct thread *tier_calculation_timer;
65 struct thread *tier_set_timer;
66 };
67
68 /* Code related to maintaining the neighbor lists */
69
70 struct neighbor_entry {
71 struct isis_vertex *vertex;
72 bool present;
73 };
74
75 static struct neighbor_entry *neighbor_entry_new(struct isis_vertex *vertex)
76 {
77 struct neighbor_entry *rv = XMALLOC(MTYPE_FABRICD_NEIGHBOR, sizeof(*rv));
78
79 rv->vertex = vertex;
80 return rv;
81 }
82
83 static void neighbor_entry_del(struct neighbor_entry *neighbor)
84 {
85 XFREE(MTYPE_FABRICD_NEIGHBOR, neighbor);
86 }
87
88 static void neighbor_entry_del_void(void *arg)
89 {
90 neighbor_entry_del((struct neighbor_entry *)arg);
91 }
92
93 static void neighbor_lists_clear(struct fabricd *f)
94 {
95 while (!skiplist_empty(f->neighbors))
96 skiplist_delete_first(f->neighbors);
97
98 hash_clean(f->neighbors_neighbors, neighbor_entry_del_void);
99 }
100
101 static unsigned neighbor_entry_hash_key(void *np)
102 {
103 struct neighbor_entry *n = np;
104
105 return jhash(n->vertex->N.id, ISIS_SYS_ID_LEN, 0x55aa5a5a);
106 }
107
108 static bool neighbor_entry_hash_cmp(const void *a, const void *b)
109 {
110 const struct neighbor_entry *na = a, *nb = b;
111
112 return memcmp(na->vertex->N.id, nb->vertex->N.id, ISIS_SYS_ID_LEN) == 0;
113 }
114
115 static int neighbor_entry_list_cmp(void *a, void *b)
116 {
117 struct neighbor_entry *na = a, *nb = b;
118
119 return -memcmp(na->vertex->N.id, nb->vertex->N.id, ISIS_SYS_ID_LEN);
120 }
121
122 static struct neighbor_entry *neighbor_entry_lookup_list(struct skiplist *list,
123 const uint8_t *id)
124 {
125 struct isis_vertex querier;
126 isis_vertex_id_init(&querier, id, VTYPE_NONPSEUDO_TE_IS);
127
128 struct neighbor_entry n = {
129 .vertex = &querier
130 };
131
132 struct neighbor_entry *rv;
133
134 if (skiplist_search(list, &n, (void**)&rv))
135 return NULL;
136
137 if (!rv->present)
138 return NULL;
139
140 return rv;
141 }
142
143 static struct neighbor_entry *neighbor_entry_lookup_hash(struct hash *hash,
144 const uint8_t *id)
145 {
146 struct isis_vertex querier;
147 isis_vertex_id_init(&querier, id, VTYPE_NONPSEUDO_TE_IS);
148
149 struct neighbor_entry n = {
150 .vertex = &querier
151 };
152
153 struct neighbor_entry *rv = hash_lookup(hash, &n);
154
155 if (!rv || !rv->present)
156 return NULL;
157
158 return rv;
159 }
160
161 static void neighbor_lists_update(struct fabricd *f)
162 {
163 neighbor_lists_clear(f);
164
165 struct listnode *node;
166 struct isis_vertex *v;
167
168 for (ALL_QUEUE_ELEMENTS_RO(&f->spftree->paths, node, v)) {
169 if (!v->d_N || !VTYPE_IS(v->type))
170 continue;
171
172 if (v->d_N > 2)
173 break;
174
175 struct neighbor_entry *n = neighbor_entry_new(v);
176 if (v->d_N == 1) {
177 skiplist_insert(f->neighbors, n, n);
178 } else {
179 struct neighbor_entry *inserted;
180 inserted = hash_get(f->neighbors_neighbors, n, hash_alloc_intern);
181 assert(inserted == n);
182 }
183 }
184 }
185
186 struct fabricd *fabricd_new(struct isis_area *area)
187 {
188 struct fabricd *rv = XCALLOC(MTYPE_FABRICD_STATE, sizeof(*rv));
189
190 rv->area = area;
191 rv->initial_sync_state = FABRICD_SYNC_PENDING;
192
193 rv->spftree = isis_spftree_new(area);
194 rv->neighbors = skiplist_new(0, neighbor_entry_list_cmp,
195 neighbor_entry_del_void);
196 rv->neighbors_neighbors = hash_create(neighbor_entry_hash_key,
197 neighbor_entry_hash_cmp,
198 "Fabricd Neighbors");
199
200 rv->tier = rv->tier_config = ISIS_TIER_UNDEFINED;
201 return rv;
202 };
203
204 void fabricd_finish(struct fabricd *f)
205 {
206 if (f->initial_sync_timeout)
207 thread_cancel(f->initial_sync_timeout);
208
209 if (f->tier_calculation_timer)
210 thread_cancel(f->tier_calculation_timer);
211
212 if (f->tier_set_timer)
213 thread_cancel(f->tier_set_timer);
214
215 isis_spftree_del(f->spftree);
216 neighbor_lists_clear(f);
217 skiplist_free(f->neighbors);
218 hash_free(f->neighbors_neighbors);
219 }
220
221 static int fabricd_initial_sync_timeout(struct thread *thread)
222 {
223 struct fabricd *f = THREAD_ARG(thread);
224
225 zlog_info("OpenFabric: Initial synchronization on %s timed out!",
226 f->initial_sync_circuit->interface->name);
227 f->initial_sync_state = FABRICD_SYNC_PENDING;
228 f->initial_sync_circuit = NULL;
229 f->initial_sync_timeout = NULL;
230 return 0;
231 }
232
233 void fabricd_initial_sync_hello(struct isis_circuit *circuit)
234 {
235 struct fabricd *f = circuit->area->fabricd;
236
237 if (!f)
238 return;
239
240 if (f->initial_sync_state > FABRICD_SYNC_PENDING)
241 return;
242
243 f->initial_sync_state = FABRICD_SYNC_STARTED;
244
245 long timeout = 2 * circuit->hello_interval[1] * circuit->hello_multiplier[1];
246
247 f->initial_sync_circuit = circuit;
248 if (f->initial_sync_timeout)
249 return;
250
251 thread_add_timer(master, fabricd_initial_sync_timeout, f,
252 timeout, &f->initial_sync_timeout);
253 f->initial_sync_start = monotime(NULL);
254
255 zlog_info("OpenFabric: Started initial synchronization with %s on %s",
256 sysid_print(circuit->u.p2p.neighbor->sysid),
257 circuit->interface->name);
258 }
259
260 bool fabricd_initial_sync_is_in_progress(struct isis_area *area)
261 {
262 struct fabricd *f = area->fabricd;
263
264 if (!f)
265 return false;
266
267 if (f->initial_sync_state > FABRICD_SYNC_PENDING
268 && f->initial_sync_state < FABRICD_SYNC_COMPLETE)
269 return true;
270
271 return false;
272 }
273
274 bool fabricd_initial_sync_is_complete(struct isis_area *area)
275 {
276 struct fabricd *f = area->fabricd;
277
278 if (!f)
279 return false;
280
281 return f->initial_sync_state == FABRICD_SYNC_COMPLETE;
282 }
283
284 struct isis_circuit *fabricd_initial_sync_circuit(struct isis_area *area)
285 {
286 struct fabricd *f = area->fabricd;
287 if (!f)
288 return NULL;
289
290 return f->initial_sync_circuit;
291 }
292
293 void fabricd_initial_sync_finish(struct isis_area *area)
294 {
295 struct fabricd *f = area->fabricd;
296
297 if (!f)
298 return;
299
300 if (monotime(NULL) - f->initial_sync_start < 5)
301 return;
302
303 zlog_info("OpenFabric: Initial synchronization on %s complete.",
304 f->initial_sync_circuit->interface->name);
305 f->initial_sync_state = FABRICD_SYNC_COMPLETE;
306 f->initial_sync_circuit = NULL;
307 thread_cancel(f->initial_sync_timeout);
308 f->initial_sync_timeout = NULL;
309 }
310
311 static void fabricd_bump_tier_calculation_timer(struct fabricd *f);
312 static void fabricd_set_tier(struct fabricd *f, uint8_t tier);
313
314 static uint8_t fabricd_calculate_fabric_tier(struct isis_area *area)
315 {
316 struct isis_spftree *local_tree = fabricd_spftree(area);
317 struct listnode *node;
318
319 struct isis_vertex *furthest_t0 = NULL,
320 *second_furthest_t0 = NULL;
321
322 struct isis_vertex *v;
323
324 for (ALL_QUEUE_ELEMENTS_RO(&local_tree->paths, node, v)) {
325 struct isis_lsp *lsp = lsp_for_vertex(local_tree, v);
326
327 if (!lsp || !lsp->tlvs
328 || !lsp->tlvs->spine_leaf
329 || !lsp->tlvs->spine_leaf->has_tier
330 || lsp->tlvs->spine_leaf->tier != 0)
331 continue;
332
333 second_furthest_t0 = furthest_t0;
334 furthest_t0 = v;
335 }
336
337 if (!second_furthest_t0) {
338 zlog_info("OpenFabric: Could not find two T0 routers");
339 return ISIS_TIER_UNDEFINED;
340 }
341
342 zlog_info("OpenFabric: Found %s as furthest t0 from local system, dist == %"
343 PRIu32, rawlspid_print(furthest_t0->N.id), furthest_t0->d_N);
344
345 struct isis_spftree *remote_tree =
346 isis_run_hopcount_spf(area, furthest_t0->N.id, NULL);
347
348 struct isis_vertex *furthest_from_remote =
349 isis_vertex_queue_last(&remote_tree->paths);
350
351 if (!furthest_from_remote) {
352 zlog_info("OpenFabric: Found no furthest node in remote spf");
353 isis_spftree_del(remote_tree);
354 return ISIS_TIER_UNDEFINED;
355 } else {
356 zlog_info("OpenFabric: Found %s as furthest from remote dist == %"
357 PRIu32, rawlspid_print(furthest_from_remote->N.id),
358 furthest_from_remote->d_N);
359 }
360
361 int64_t tier = furthest_from_remote->d_N - furthest_t0->d_N;
362 isis_spftree_del(remote_tree);
363
364 if (tier < 0 || tier >= ISIS_TIER_UNDEFINED) {
365 zlog_info("OpenFabric: Calculated tier %" PRId64 " seems implausible",
366 tier);
367 return ISIS_TIER_UNDEFINED;
368 }
369
370 zlog_info("OpenFabric: Calculated %" PRId64 " as tier", tier);
371 return tier;
372 }
373
374 static int fabricd_tier_set_timer(struct thread *thread)
375 {
376 struct fabricd *f = THREAD_ARG(thread);
377 f->tier_set_timer = NULL;
378
379 fabricd_set_tier(f, f->tier_pending);
380 return 0;
381 }
382
383 static int fabricd_tier_calculation_cb(struct thread *thread)
384 {
385 struct fabricd *f = THREAD_ARG(thread);
386 uint8_t tier = ISIS_TIER_UNDEFINED;
387 f->tier_calculation_timer = NULL;
388
389 tier = fabricd_calculate_fabric_tier(f->area);
390 if (tier == ISIS_TIER_UNDEFINED)
391 return 0;
392
393 zlog_info("OpenFabric: Got tier %" PRIu8 " from algorithm. Arming timer.",
394 tier);
395 f->tier_pending = tier;
396 thread_add_timer(master, fabricd_tier_set_timer, f,
397 f->area->lsp_gen_interval[ISIS_LEVEL2 - 1],
398 &f->tier_set_timer);
399
400 return 0;
401 }
402
403 static void fabricd_bump_tier_calculation_timer(struct fabricd *f)
404 {
405 /* Cancel timer if we already know our tier */
406 if (f->tier != ISIS_TIER_UNDEFINED
407 || f->tier_set_timer) {
408 if (f->tier_calculation_timer) {
409 thread_cancel(f->tier_calculation_timer);
410 f->tier_calculation_timer = NULL;
411 }
412 return;
413 }
414
415 /* If we need to calculate the tier, wait some
416 * time for the topology to settle before running
417 * the calculation */
418 if (f->tier_calculation_timer) {
419 thread_cancel(f->tier_calculation_timer);
420 f->tier_calculation_timer = NULL;
421 }
422
423 thread_add_timer(master, fabricd_tier_calculation_cb, f,
424 2 * f->area->lsp_gen_interval[ISIS_LEVEL2 - 1],
425 &f->tier_calculation_timer);
426 }
427
428 static void fabricd_set_tier(struct fabricd *f, uint8_t tier)
429 {
430 if (f->tier == tier)
431 return;
432
433 zlog_info("OpenFabric: Set own tier to %" PRIu8, tier);
434 f->tier = tier;
435
436 fabricd_bump_tier_calculation_timer(f);
437 lsp_regenerate_schedule(f->area, ISIS_LEVEL2, 0);
438 }
439
440 void fabricd_run_spf(struct isis_area *area)
441 {
442 struct fabricd *f = area->fabricd;
443
444 if (!f)
445 return;
446
447 isis_run_hopcount_spf(area, isis->sysid, f->spftree);
448 neighbor_lists_update(f);
449 fabricd_bump_tier_calculation_timer(f);
450 }
451
452 struct isis_spftree *fabricd_spftree(struct isis_area *area)
453 {
454 struct fabricd *f = area->fabricd;
455
456 if (!f)
457 return NULL;
458
459 return f->spftree;
460 }
461
462 void fabricd_configure_tier(struct isis_area *area, uint8_t tier)
463 {
464 struct fabricd *f = area->fabricd;
465
466 if (!f || f->tier_config == tier)
467 return;
468
469 f->tier_config = tier;
470 fabricd_set_tier(f, tier);
471 }
472
473 uint8_t fabricd_tier(struct isis_area *area)
474 {
475 struct fabricd *f = area->fabricd;
476
477 if (!f)
478 return ISIS_TIER_UNDEFINED;
479
480 return f->tier;
481 }
482
483 int fabricd_write_settings(struct isis_area *area, struct vty *vty)
484 {
485 struct fabricd *f = area->fabricd;
486 int written = 0;
487
488 if (!f)
489 return written;
490
491 if (f->tier_config != ISIS_TIER_UNDEFINED) {
492 vty_out(vty, " fabric-tier %" PRIu8 "\n", f->tier_config);
493 written++;
494 }
495
496 return written;
497 }
498
499 static void move_to_dnr(struct isis_lsp *lsp, struct neighbor_entry *n)
500 {
501 struct isis_adjacency *adj = listnode_head(n->vertex->Adj_N);
502
503 n->present = false;
504
505 if (isis->debugs & DEBUG_FABRICD_FLOODING) {
506 char buff[PREFIX2STR_BUFFER];
507 zlog_debug("OpenFabric: Adding %s to DNR",
508 vid2string(n->vertex, buff, sizeof(buff)));
509 }
510
511 if (adj) {
512 isis_tx_queue_add(adj->circuit->tx_queue, lsp,
513 TX_LSP_CIRCUIT_SCOPED);
514 }
515 }
516
517 static void move_to_rf(struct isis_lsp *lsp, struct neighbor_entry *n)
518 {
519 struct isis_adjacency *adj = listnode_head(n->vertex->Adj_N);
520
521 n->present = false;
522
523 if (isis->debugs & DEBUG_FABRICD_FLOODING) {
524 char buff[PREFIX2STR_BUFFER];
525 zlog_debug("OpenFabric: Adding %s to RF",
526 vid2string(n->vertex, buff, sizeof(buff)));
527 }
528
529 if (adj) {
530 isis_tx_queue_add(adj->circuit->tx_queue, lsp,
531 TX_LSP_NORMAL);
532 }
533 }
534
535 static void mark_neighbor_as_present(struct hash_backet *backet, void *arg)
536 {
537 struct neighbor_entry *n = backet->data;
538
539 n->present = true;
540 }
541
542 static void handle_firsthops(struct hash_backet *backet, void *arg)
543 {
544 struct isis_lsp *lsp = arg;
545 struct fabricd *f = lsp->area->fabricd;
546 struct isis_vertex *vertex = backet->data;
547
548 struct neighbor_entry *n;
549
550 n = neighbor_entry_lookup_list(f->neighbors, vertex->N.id);
551 if (n) {
552 if (isis->debugs & DEBUG_FABRICD_FLOODING) {
553 char buff[PREFIX2STR_BUFFER];
554 zlog_debug("Removing %s from NL as its in the reverse path",
555 vid2string(vertex, buff, sizeof(buff)));
556 }
557 n->present = false;
558 }
559
560 n = neighbor_entry_lookup_hash(f->neighbors_neighbors, vertex->N.id);
561 if (n) {
562 if (isis->debugs & DEBUG_FABRICD_FLOODING) {
563 char buff[PREFIX2STR_BUFFER];
564 zlog_debug("Removing %s from NN as its in the reverse path",
565 vid2string(vertex, buff, sizeof(buff)));
566 }
567 n->present = false;
568 }
569 }
570
571 void fabricd_lsp_flood(struct isis_lsp *lsp)
572 {
573 struct fabricd *f = lsp->area->fabricd;
574 assert(f);
575
576 void *cursor = NULL;
577 struct neighbor_entry *n;
578
579 if (isis->debugs & DEBUG_FABRICD_FLOODING) {
580 zlog_debug("OpenFabric: Flooding LSP %s",
581 rawlspid_print(lsp->hdr.lsp_id));
582 }
583
584 /* Mark all elements in NL as present and move T0s into DNR */
585 while (!skiplist_next(f->neighbors, NULL, (void **)&n, &cursor)) {
586 n->present = true;
587
588 struct isis_lsp *node_lsp = lsp_for_vertex(f->spftree,
589 n->vertex);
590 if (!node_lsp
591 || !node_lsp->tlvs
592 || !node_lsp->tlvs->spine_leaf
593 || !node_lsp->tlvs->spine_leaf->has_tier
594 || node_lsp->tlvs->spine_leaf->tier != 0) {
595 continue;
596 }
597
598 if (isis->debugs & DEBUG_FABRICD_FLOODING) {
599 zlog_debug("Moving %s to DNR because it's T0",
600 rawlspid_print(node_lsp->hdr.lsp_id));
601 }
602
603 move_to_dnr(lsp, n);
604 }
605
606 /* Mark all elements in NN as present */
607 hash_iterate(f->neighbors_neighbors, mark_neighbor_as_present, NULL);
608
609 struct isis_vertex *originator = isis_find_vertex(&f->spftree->paths,
610 lsp->hdr.lsp_id,
611 VTYPE_NONPSEUDO_TE_IS);
612
613 /* Remove all IS from NL and NN in the shortest path
614 * to the IS that originated the LSP */
615 if (originator)
616 hash_iterate(originator->firsthops, handle_firsthops, lsp);
617
618 /* Iterate over all remaining IS in NL */
619 cursor = NULL;
620 while (!skiplist_next(f->neighbors, NULL, (void **)&n, &cursor)) {
621 if (!n->present)
622 continue;
623
624 struct isis_lsp *nlsp = lsp_for_vertex(f->spftree, n->vertex);
625 if (!nlsp || !nlsp->tlvs) {
626 if (isis->debugs & DEBUG_FABRICD_FLOODING) {
627 char buff[PREFIX2STR_BUFFER];
628 zlog_debug("Moving %s to DNR as it has no LSP",
629 vid2string(n->vertex, buff, sizeof(buff)));
630 }
631
632 move_to_dnr(lsp, n);
633 continue;
634 }
635
636 if (isis->debugs & DEBUG_FABRICD_FLOODING) {
637 char buff[PREFIX2STR_BUFFER];
638 zlog_debug("Considering %s from NL...",
639 vid2string(n->vertex, buff, sizeof(buff)));
640 }
641
642 /* For all neighbors of the NL IS check whether they are present
643 * in NN. If yes, remove from NN and set need_reflood. */
644 bool need_reflood = false;
645 struct isis_extended_reach *er;
646 for (er = (struct isis_extended_reach *)nlsp->tlvs->extended_reach.head;
647 er; er = er->next) {
648 struct neighbor_entry *nn;
649
650 nn = neighbor_entry_lookup_hash(f->neighbors_neighbors,
651 er->id);
652
653 if (nn) {
654 if (isis->debugs & DEBUG_FABRICD_FLOODING) {
655 char buff[PREFIX2STR_BUFFER];
656 zlog_debug("Found neighbor %s in NN, removing it from NN and setting reflood.",
657 vid2string(nn->vertex, buff, sizeof(buff)));
658 }
659
660 nn->present = false;
661 need_reflood = true;
662 }
663 }
664
665 if (need_reflood)
666 move_to_rf(lsp, n);
667 else
668 move_to_dnr(lsp, n);
669 }
670
671 if (isis->debugs & DEBUG_FABRICD_FLOODING) {
672 zlog_debug("OpenFabric: Flooding algorithm complete.");
673 }
674 }
675
676 void fabricd_trigger_csnp(struct isis_area *area)
677 {
678 struct fabricd *f = area->fabricd;
679
680 if (!f)
681 return;
682
683 struct listnode *node;
684 struct isis_circuit *circuit;
685
686 for (ALL_LIST_ELEMENTS_RO(area->circuit_list, node, circuit)) {
687 if (!circuit->t_send_csnp[1])
688 continue;
689
690 thread_cancel(circuit->t_send_csnp[ISIS_LEVEL2 - 1]);
691 thread_add_timer_msec(master, send_l2_csnp, circuit,
692 isis_jitter(500, CSNP_JITTER),
693 &circuit->t_send_csnp[ISIS_LEVEL2 - 1]);
694 }
695 }
696
697 struct list *fabricd_ip_addrs(struct isis_circuit *circuit)
698 {
699 if (circuit->ip_addrs && listcount(circuit->ip_addrs))
700 return circuit->ip_addrs;
701
702 if (!fabricd || !circuit->area || !circuit->area->circuit_list)
703 return NULL;
704
705 struct listnode *node;
706 struct isis_circuit *c;
707
708 for (ALL_LIST_ELEMENTS_RO(circuit->area->circuit_list, node, c)) {
709 if (c->circ_type != CIRCUIT_T_LOOPBACK)
710 continue;
711
712 if (!c->ip_addrs || !listcount(c->ip_addrs))
713 return NULL;
714
715 return c->ip_addrs;
716 }
717
718 return NULL;
719 }