]> git.ipfire.org Git - people/ms/dnsmasq.git/blame - src/radv.c
add general flag param to iface_enumerate IPv6 callback
[people/ms/dnsmasq.git] / src / radv.c
CommitLineData
c5ad4e79
SK
1/* dnsmasq is Copyright (c) 2000-2012 Simon Kelley
2
3 This program is free software; you can redistribute it and/or modify
4 it under the terms of the GNU General Public License as published by
5 the Free Software Foundation; version 2 dated June, 1991, or
6 (at your option) version 3 dated 29 June, 2007.
7
8 This program is distributed in the hope that it will be useful,
9 but WITHOUT ANY WARRANTY; without even the implied warranty of
10 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 GNU General Public License for more details.
12
13 You should have received a copy of the GNU General Public License
14 along with this program. If not, see <http://www.gnu.org/licenses/>.
15*/
16
17
843c96b4 18/* NB. This code may be called during a DHCPv4 or transaction which is in ping-wait
c5ad4e79 19 It therefore cannot use any DHCP buffer resources except outpacket, which is
843c96b4
SK
20 not used by DHCPv4 code. This code may also be called when DHCP 4 or 6 isn't
21 active, so we ensure that outpacket is allocated here too */
c5ad4e79
SK
22
23#include "dnsmasq.h"
c5ad4e79
SK
24
25#ifdef HAVE_DHCP6
26
22d904db
SK
27#include <netinet/icmp6.h>
28
c5ad4e79 29struct ra_param {
1f776932 30 time_t now;
30cd9666 31 int ind, managed, other, found_context, first;
c5ad4e79 32 char *if_name;
18f0fb05 33 struct dhcp_netid *tags;
c5ad4e79
SK
34 struct in6_addr link_local;
35};
36
37struct search_param {
38 time_t now; int iface;
39};
40
1f776932 41static void send_ra(time_t now, int iface, char *iface_name, struct in6_addr *dest);
c5ad4e79 42static int add_prefixes(struct in6_addr *local, int prefix,
bad7b875 43 int scope, int if_index, int flags,
1f776932 44 int preferred, int valid, void *vparam);
c5ad4e79 45static int iface_search(struct in6_addr *local, int prefix,
bad7b875 46 int scope, int if_index, int flags,
1f776932 47 int prefered, int valid, void *vparam);
c5ad4e79
SK
48static int add_lla(int index, unsigned int type, char *mac, size_t maclen, void *parm);
49
c5379c1a 50static int hop_limit;
c5ad4e79
SK
51
52void ra_init(time_t now)
53{
c5ad4e79
SK
54 struct icmp6_filter filter;
55 int fd;
0e88d53f 56#if defined(IPV6_TCLASS) && defined(IPTOS_CLASS_CS6)
c5ad4e79
SK
57 int class = IPTOS_CLASS_CS6;
58#endif
59 int val = 255; /* radvd uses this value */
7b6dd880 60 socklen_t len = sizeof(int);
353ae4d2
SK
61 struct dhcp_context *context;
62
843c96b4
SK
63 /* ensure this is around even if we're not doing DHCPv6 */
64 expand_buf(&daemon->outpacket, sizeof(struct dhcp_packet));
353ae4d2
SK
65
66 /* See if we're guessing SLAAC addresses, if so we need to recieve ping replies */
1f776932 67 for (context = daemon->dhcp6; context; context = context->next)
353ae4d2
SK
68 if ((context->flags & CONTEXT_RA_NAME))
69 break;
70
c5ad4e79
SK
71 ICMP6_FILTER_SETBLOCKALL(&filter);
72 ICMP6_FILTER_SETPASS(ND_ROUTER_SOLICIT, &filter);
353ae4d2
SK
73 if (context)
74 ICMP6_FILTER_SETPASS(ICMP6_ECHO_REPLY, &filter);
c5ad4e79
SK
75
76 if ((fd = socket(PF_INET6, SOCK_RAW, IPPROTO_ICMPV6)) == -1 ||
c5379c1a 77 getsockopt(fd, IPPROTO_IPV6, IPV6_UNICAST_HOPS, &hop_limit, &len) ||
0e88d53f 78#if defined(IPV6_TCLASS) && defined(IPTOS_CLASS_CS6)
c5ad4e79
SK
79 setsockopt(fd, IPPROTO_IPV6, IPV6_TCLASS, &class, sizeof(class)) == -1 ||
80#endif
81 !fix_fd(fd) ||
82 !set_ipv6pktinfo(fd) ||
83 setsockopt(fd, IPPROTO_IPV6, IPV6_UNICAST_HOPS, &val, sizeof(val)) ||
84 setsockopt(fd, IPPROTO_IPV6, IPV6_MULTICAST_HOPS, &val, sizeof(val)) ||
85 setsockopt(fd, IPPROTO_ICMPV6, ICMP6_FILTER, &filter, sizeof(filter)) == -1)
86 die (_("cannot create ICMPv6 socket: %s"), NULL, EC_BADNET);
87
88 daemon->icmp6fd = fd;
89
353ae4d2 90 ra_start_unsolicted(now, NULL);
c5ad4e79
SK
91}
92
353ae4d2 93void ra_start_unsolicted(time_t now, struct dhcp_context *context)
c5ad4e79 94{
353ae4d2 95 /* init timers so that we do ra's for some/all soon. some ra_times will end up zeroed
c5ad4e79
SK
96 if it's not appropriate to advertise those contexts.
97 This gets re-called on a netlink route-change to re-do the advertisement
98 and pick up new interfaces */
6e3dba3f 99
353ae4d2 100 if (context)
1b75c1e6 101 context->ra_short_period_start = context->ra_time = now;
353ae4d2 102 else
1f776932 103 for (context = daemon->dhcp6; context; context = context->next)
6e3dba3f 104 if (!(context->flags & CONTEXT_TEMPLATE))
1b75c1e6
SK
105 {
106 context->ra_time = now + (rand16()/13000); /* range 0 - 5 */
107 /* re-do frequently for a minute or so, in case the first gets lost. */
108 context->ra_short_period_start = now;
109 }
c5ad4e79
SK
110}
111
1f776932 112void icmp6_packet(time_t now)
c5ad4e79
SK
113{
114 char interface[IF_NAMESIZE+1];
115 ssize_t sz;
116 int if_index = 0;
117 struct cmsghdr *cmptr;
118 struct msghdr msg;
119 union {
120 struct cmsghdr align; /* this ensures alignment */
121 char control6[CMSG_SPACE(sizeof(struct in6_pktinfo))];
122 } control_u;
123 struct sockaddr_in6 from;
5ef33279 124 unsigned char *packet;
c5ad4e79 125 struct iname *tmp;
c5ad4e79
SK
126
127 /* Note: use outpacket for input buffer */
128 msg.msg_control = control_u.control6;
129 msg.msg_controllen = sizeof(control_u);
130 msg.msg_flags = 0;
131 msg.msg_name = &from;
132 msg.msg_namelen = sizeof(from);
133 msg.msg_iov = &daemon->outpacket;
134 msg.msg_iovlen = 1;
135
136 if ((sz = recv_dhcp_packet(daemon->icmp6fd, &msg)) == -1 || sz < 8)
137 return;
5ef33279
SK
138
139 packet = (unsigned char *)daemon->outpacket.iov_base;
c5ad4e79
SK
140
141 for (cmptr = CMSG_FIRSTHDR(&msg); cmptr; cmptr = CMSG_NXTHDR(&msg, cmptr))
142 if (cmptr->cmsg_level == IPPROTO_IPV6 && cmptr->cmsg_type == daemon->v6pktinfo)
143 {
144 union {
145 unsigned char *c;
146 struct in6_pktinfo *p;
147 } p;
148 p.c = CMSG_DATA(cmptr);
149
150 if_index = p.p->ipi6_ifindex;
151 }
152
153 if (!indextoname(daemon->icmp6fd, if_index, interface))
154 return;
155
4f7b304f 156 if (!iface_check(AF_LOCAL, NULL, interface, NULL))
c5ad4e79
SK
157 return;
158
159 for (tmp = daemon->dhcp_except; tmp; tmp = tmp->next)
160 if (tmp->name && (strcmp(tmp->name, interface) == 0))
161 return;
162
8bc4cece 163 if (packet[1] != 0)
353ae4d2 164 return;
8bc4cece 165
5ef33279
SK
166 if (packet[0] == ICMP6_ECHO_REPLY)
167 lease_ping_reply(&from.sin6_addr, packet, interface);
168 else if (packet[0] == ND_ROUTER_SOLICIT)
c5ad4e79 169 {
5ef33279
SK
170 char *mac = "";
171
172 /* look for link-layer address option for logging */
173 if (sz >= 16 && packet[8] == ICMP6_OPT_SOURCE_MAC && (packet[9] * 8) + 8 <= sz)
174 {
175 print_mac(daemon->namebuff, &packet[10], (packet[9] * 8) - 2);
176 mac = daemon->namebuff;
177 }
178
179 my_syslog(MS_DHCP | LOG_INFO, "RTR-SOLICIT(%s) %s", interface, mac);
f632e567 180 /* source address may not be valid in solicit request. */
1f776932 181 send_ra(now, if_index, interface, !IN6_IS_ADDR_UNSPECIFIED(&from.sin6_addr) ? &from.sin6_addr : NULL);
c5ad4e79 182 }
c5ad4e79
SK
183}
184
1f776932 185static void send_ra(time_t now, int iface, char *iface_name, struct in6_addr *dest)
c5ad4e79
SK
186{
187 struct ra_packet *ra;
188 struct ra_param parm;
189 struct ifreq ifr;
190 struct sockaddr_in6 addr;
191 struct dhcp_context *context;
18f0fb05
SK
192 struct dhcp_netid iface_id;
193 struct dhcp_opt *opt_cfg;
194 int done_dns = 0;
195
c5ad4e79
SK
196 save_counter(0);
197 ra = expand(sizeof(struct ra_packet));
198
353ae4d2 199 ra->type = ND_ROUTER_ADVERT;
c5ad4e79 200 ra->code = 0;
c5379c1a 201 ra->hop_limit = hop_limit;
884a6dfe 202 ra->flags = 0x00;
c5ad4e79
SK
203 ra->lifetime = htons(1800); /* AdvDefaultLifetime*/
204 ra->reachable_time = 0;
205 ra->retrans_time = 0;
206
207 parm.ind = iface;
208 parm.managed = 0;
30cd9666 209 parm.other = 0;
c5ad4e79
SK
210 parm.found_context = 0;
211 parm.if_name = iface_name;
212 parm.first = 1;
1f776932
SK
213 parm.now = now;
214
18f0fb05
SK
215 /* set tag with name == interface */
216 iface_id.net = iface_name;
217 iface_id.next = NULL;
218 parm.tags = &iface_id;
c5ad4e79 219
1f776932 220 for (context = daemon->dhcp6; context; context = context->next)
18f0fb05
SK
221 {
222 context->flags &= ~CONTEXT_RA_DONE;
223 context->netid.next = &context->netid;
224 }
225
c5ad4e79
SK
226 if (!iface_enumerate(AF_INET6, &parm, add_prefixes) ||
227 !parm.found_context)
228 return;
229
230 strncpy(ifr.ifr_name, iface_name, IF_NAMESIZE);
231
232 if (ioctl(daemon->icmp6fd, SIOCGIFMTU, &ifr) != -1)
233 {
234 put_opt6_char(ICMP6_OPT_MTU);
235 put_opt6_char(1);
236 put_opt6_short(0);
237 put_opt6_long(ifr.ifr_mtu);
238 }
239
240 iface_enumerate(AF_LOCAL, &iface, add_lla);
18f0fb05
SK
241
242 /* RDNSS, RFC 6106, use relevant DHCP6 options */
243 (void)option_filter(parm.tags, NULL, daemon->dhcp_opts6);
c5ad4e79 244
18f0fb05
SK
245 for (opt_cfg = daemon->dhcp_opts6; opt_cfg; opt_cfg = opt_cfg->next)
246 {
247 int i;
248
249 /* netids match and not encapsulated? */
250 if (!(opt_cfg->flags & DHOPT_TAGOK))
251 continue;
252
253 if (opt_cfg->opt == OPTION6_DNS_SERVER)
254 {
255 struct in6_addr *a = (struct in6_addr *)opt_cfg->val;
c5ad4e79 256
18f0fb05
SK
257 done_dns = 1;
258 if (opt_cfg->len == 0)
259 continue;
260
261 put_opt6_char(ICMP6_OPT_RDNSS);
262 put_opt6_char((opt_cfg->len/8) + 1);
263 put_opt6_short(0);
264 put_opt6_long(1800); /* lifetime - twice RA retransmit */
265 /* zero means "self" */
266 for (i = 0; i < opt_cfg->len; i += IN6ADDRSZ, a++)
267 if (IN6_IS_ADDR_UNSPECIFIED(a))
268 put_opt6(&parm.link_local, IN6ADDRSZ);
269 else
270 put_opt6(a, IN6ADDRSZ);
271 }
272
273 if (opt_cfg->opt == OPTION6_DOMAIN_SEARCH && opt_cfg->len != 0)
274 {
275 int len = ((opt_cfg->len+7)/8);
276
277 put_opt6_char(ICMP6_OPT_DNSSL);
278 put_opt6_char(len + 1);
279 put_opt6_short(0);
280 put_opt6_long(1800); /* lifetime - twice RA retransmit */
281 put_opt6(opt_cfg->val, opt_cfg->len);
282
283 /* pad */
284 for (i = opt_cfg->len; i < len * 8; i++)
285 put_opt6_char(0);
286 }
287 }
288
289 if (!done_dns)
290 {
291 /* default == us. */
292 put_opt6_char(ICMP6_OPT_RDNSS);
293 put_opt6_char(3);
294 put_opt6_short(0);
295 put_opt6_long(1800); /* lifetime - twice RA retransmit */
296 put_opt6(&parm.link_local, IN6ADDRSZ);
297 }
c5ad4e79
SK
298
299 /* set managed bits unless we're providing only RA on this link */
300 if (parm.managed)
30cd9666
SK
301 ra->flags |= 0x80; /* M flag, managed, */
302 if (parm.other)
303 ra->flags |= 0x40; /* O flag, other */
304
c5ad4e79
SK
305 /* decide where we're sending */
306 memset(&addr, 0, sizeof(addr));
22d904db
SK
307#ifdef HAVE_SOCKADDR_SA_LEN
308 addr.sin6_len = sizeof(struct sockaddr_in6);
309#endif
c5ad4e79
SK
310 addr.sin6_family = AF_INET6;
311 addr.sin6_port = htons(IPPROTO_ICMPV6);
312 if (dest)
313 {
353ae4d2 314 addr.sin6_addr = *dest;
c5ad4e79
SK
315 if (IN6_IS_ADDR_LINKLOCAL(dest) ||
316 IN6_IS_ADDR_MC_LINKLOCAL(dest))
317 addr.sin6_scope_id = iface;
318 }
319 else
f632e567 320 inet_pton(AF_INET6, ALL_NODES, &addr.sin6_addr);
22d904db 321
c5ad4e79 322 send_from(daemon->icmp6fd, 0, daemon->outpacket.iov_base, save_counter(0),
50303b19 323 (union mysockaddr *)&addr, (struct all_addr *)&parm.link_local, iface);
c5ad4e79
SK
324
325}
326
327static int add_prefixes(struct in6_addr *local, int prefix,
bad7b875 328 int scope, int if_index, int flags,
1f776932 329 int preferred, int valid, void *vparam)
c5ad4e79 330{
c5ad4e79 331 struct ra_param *param = vparam;
c5ad4e79
SK
332
333 (void)scope; /* warning */
bad7b875 334 (void)flags;
1f776932
SK
335 (void)preferred;
336 (void)valid;
c5ad4e79
SK
337
338 if (if_index == param->ind)
339 {
340 if (IN6_IS_ADDR_LINKLOCAL(local))
341 param->link_local = *local;
342 else if (!IN6_IS_ADDR_LOOPBACK(local) &&
c5ad4e79
SK
343 !IN6_IS_ADDR_MULTICAST(local))
344 {
1e02a859 345 int do_prefix = 0;
c8257540
SK
346 int do_slaac = 0;
347 int deprecate = 0;
1f776932 348 int found_constructed = 0;
c8257540 349 unsigned int time = 0xffffffff;
1f776932 350 int calc_valid = 0, calc_preferred = 0;
1e02a859
SK
351 struct dhcp_context *context;
352
1f776932 353 for (context = daemon->dhcp6; context; context = context->next)
6e3dba3f
SK
354 if (!(context->flags & CONTEXT_TEMPLATE) &&
355 prefix == context->prefix &&
c5ad4e79
SK
356 is_same_net6(local, &context->start6, prefix) &&
357 is_same_net6(local, &context->end6, prefix))
358 {
30cd9666
SK
359 if ((context->flags &
360 (CONTEXT_RA_ONLY | CONTEXT_RA_NAME | CONTEXT_RA_STATELESS)))
361 {
362 do_slaac = 1;
4723d49d 363 if (context->flags & CONTEXT_DHCP)
05e92e5a
SK
364 {
365 param->other = 1;
366 if (!(context->flags & CONTEXT_RA_STATELESS))
367 param->managed = 1;
368 }
30cd9666 369 }
884a6dfe 370 else
0010b474
SK
371 {
372 /* don't do RA for non-ra-only unless --enable-ra is set */
373 if (!option_bool(OPT_RA))
374 continue;
375 param->managed = 1;
30cd9666 376 param->other = 1;
0010b474 377 }
1f776932
SK
378
379 if (context->flags & CONTEXT_CONSTRUCTED)
380 {
381 found_constructed = 1;
382 calc_valid = context->valid;
383 calc_preferred = context->preferred;
384 if (context->valid != -1)
385 calc_valid -= (int)param->now;
386 if (context->preferred != -1)
387 calc_preferred -= (int)param->now;
388 }
389
c8257540
SK
390 /* find floor time */
391 if (time > context->lease_time)
392 time = context->lease_time;
c5ad4e79 393
c8257540
SK
394 if (context->flags & CONTEXT_DEPRECATE)
395 deprecate = 1;
396
18f0fb05
SK
397 /* collect dhcp-range tags */
398 if (context->netid.next == &context->netid && context->netid.net)
399 {
400 context->netid.next = param->tags;
401 param->tags = &context->netid;
402 }
403
5ae34bf3
SK
404 /* subsequent prefixes on the same interface
405 and subsequent instances of this prefix don't need timers.
406 Be careful not to find the same prefix twice with different
407 addresses. */
1e02a859
SK
408 if (!(context->flags & CONTEXT_RA_DONE))
409 {
5ae34bf3
SK
410 if (!param->first)
411 context->ra_time = 0;
1e02a859
SK
412 context->flags |= CONTEXT_RA_DONE;
413 do_prefix = 1;
414 }
5ae34bf3
SK
415
416 param->first = 0;
417 param->found_context = 1;
c5ad4e79 418 }
1f776932
SK
419
420 if (!found_constructed)
421 {
422 calc_valid = time;
423 calc_preferred = deprecate ? 0 : time;
424 }
1e02a859
SK
425
426 if (do_prefix)
c8257540 427 {
1e02a859
SK
428 struct prefix_opt *opt;
429
430 if ((opt = expand(sizeof(struct prefix_opt))))
431 {
5ef33279
SK
432 /* zero net part of address */
433 setaddr6part(local, addr6part(local) & ~((prefix == 64) ? (u64)-1LL : (1LLU << (128 - prefix)) - 1LLU));
434
1e02a859
SK
435 /* lifetimes must be min 2 hrs, by RFC 2462 */
436 if (time < 7200)
437 time = 7200;
438
439 opt->type = ICMP6_OPT_PREFIX;
440 opt->len = 4;
441 opt->prefix_len = prefix;
fd05f127
SK
442 /* autonomous only if we're not doing dhcp, always set "on-link" */
443 opt->flags = do_slaac ? 0xC0 : 0x80;
1f776932
SK
444 opt->valid_lifetime = htonl(calc_valid);
445 opt->preferred_lifetime = htonl(calc_preferred);
5ef33279 446 opt->reserved = 0;
1e02a859 447 opt->prefix = *local;
1e02a859 448
5ef33279 449 inet_ntop(AF_INET6, local, daemon->addrbuff, ADDRSTRLEN);
1e02a859
SK
450 my_syslog(MS_DHCP | LOG_INFO, "RTR-ADVERT(%s) %s", param->if_name, daemon->addrbuff);
451 }
c8257540 452 }
c5ad4e79
SK
453 }
454 }
455 return 1;
456}
457
458static int add_lla(int index, unsigned int type, char *mac, size_t maclen, void *parm)
459{
460 (void)type;
461
462 if (index == *((int *)parm))
463 {
464 /* size is in units of 8 octets and includes type and length (2 bytes)
465 add 7 to round up */
466 int len = (maclen + 9) >> 3;
467 unsigned char *p = expand(len << 3);
468 memset(p, 0, len << 3);
469 *p++ = ICMP6_OPT_SOURCE_MAC;
470 *p++ = len;
471 memcpy(p, mac, maclen);
472
473 return 0;
474 }
475
476 return 1;
477}
478
479time_t periodic_ra(time_t now)
480{
481 struct search_param param;
482 struct dhcp_context *context;
483 time_t next_event;
484 char interface[IF_NAMESIZE+1];
485
486 param.now = now;
29d28dda 487 param.iface = 0;
c5ad4e79
SK
488
489 while (1)
490 {
491 /* find overdue events, and time of first future event */
1f776932 492 for (next_event = 0, context = daemon->dhcp6; context; context = context->next)
c5ad4e79
SK
493 if (context->ra_time != 0)
494 {
7dbe9814 495 if (difftime(context->ra_time, now) <= 0.0)
c5ad4e79
SK
496 break; /* overdue */
497
7dbe9814
SK
498 if (next_event == 0 || difftime(next_event, context->ra_time) > 0.0)
499 next_event = context->ra_time;
c5ad4e79
SK
500 }
501
502 /* none overdue */
503 if (!context)
504 break;
505
506 /* There's a context overdue, but we can't find an interface
507 associated with it, because it's for a subnet we dont
508 have an interface on. Probably we're doing DHCP on
509 a remote subnet via a relay. Zero the timer, since we won't
510 ever be able to send ra's and satistfy it. */
511 if (iface_enumerate(AF_INET6, &param, iface_search))
512 context->ra_time = 0;
29d28dda
SK
513 else if (param.iface != 0 &&
514 indextoname(daemon->icmp6fd, param.iface, interface) &&
f7fe3627 515 iface_check(AF_LOCAL, NULL, interface, NULL))
421594f8
SK
516 {
517 struct iname *tmp;
518 for (tmp = daemon->dhcp_except; tmp; tmp = tmp->next)
519 if (tmp->name && (strcmp(tmp->name, interface) == 0))
520 break;
521 if (!tmp)
1f776932 522 send_ra(now, param.iface, interface, NULL);
421594f8
SK
523 }
524 }
c5ad4e79
SK
525 return next_event;
526}
421594f8 527
c5ad4e79 528static int iface_search(struct in6_addr *local, int prefix,
bad7b875 529 int scope, int if_index, int flags,
1f776932 530 int preferred, int valid, void *vparam)
c5ad4e79
SK
531{
532 struct search_param *param = vparam;
741c2952 533 struct dhcp_context *context;
c5ad4e79
SK
534
535 (void)scope;
1f776932
SK
536 (void)preferred;
537 (void)valid;
c5ad4e79 538
1f776932 539 for (context = daemon->dhcp6; context; context = context->next)
6e3dba3f
SK
540 if (!(context->flags & CONTEXT_TEMPLATE) &&
541 prefix == context->prefix &&
c5ad4e79 542 is_same_net6(local, &context->start6, prefix) &&
6e3dba3f
SK
543 is_same_net6(local, &context->end6, prefix) &&
544 context->ra_time != 0 &&
545 difftime(context->ra_time, param->now) <= 0.0)
546 {
547 /* found an interface that's overdue for RA determine new
548 timeout value and arrange for RA to be sent unless interface is
549 still doing DAD.*/
550
bad7b875 551 if (!(flags & IFACE_TENTATIVE))
6e3dba3f
SK
552 param->iface = if_index;
553
1b75c1e6 554 if (difftime(param->now, context->ra_short_period_start) < 60.0)
6e3dba3f
SK
555 /* range 5 - 20 */
556 context->ra_time = param->now + 5 + (rand16()/4400);
557 else
558 /* range 450 - 600 */
559 context->ra_time = param->now + 450 + (rand16()/440);
560
561 /* zero timers for other contexts on the same subnet, so they don't timeout
562 independently */
563 for (context = context->next; context; context = context->next)
564 if (prefix == context->prefix &&
565 is_same_net6(local, &context->start6, prefix) &&
566 is_same_net6(local, &context->end6, prefix))
567 context->ra_time = 0;
568
569 return 0; /* found, abort */
570 }
c5ad4e79
SK
571
572 return 1; /* keep searching */
573}
574
801ca9a7 575
c5ad4e79 576#endif