Line | Count | Source |
1 | | /* |
2 | | * |
3 | | * Authors: |
4 | | * Pedro Roque <roque@di.fc.ul.pt> |
5 | | * Lars Fenneberg <lf@elemental.net> |
6 | | * |
7 | | * This software is Copyright 1996,1997 by the above mentioned author(s), |
8 | | * All Rights Reserved. |
9 | | * |
10 | | * The license which is distributed with this software in the file COPYRIGHT |
11 | | * applies to this software. If your distribution is missing this file, you |
12 | | * may request it from <reubenhwk@gmail.com>. |
13 | | * |
14 | | */ |
15 | | |
16 | | #include "config.h" |
17 | | #include "includes.h" |
18 | | #include "radvd.h" |
19 | | |
20 | | static void process_rs(int sock, struct Interface *, unsigned char *msg, int len, struct sockaddr_in6 *); |
21 | | static void process_ra(struct Interface *, unsigned char *msg, int len, struct sockaddr_in6 *); |
22 | | static int addr_match(struct in6_addr *a1, struct in6_addr *a2, int prefixlen); |
23 | | |
24 | | void process(int sock, struct Interface *interfaces, unsigned char *msg, int len, struct sockaddr_in6 *addr, |
25 | | struct in6_pktinfo *pkt_info, int hoplimit) |
26 | 609 | { |
27 | 609 | char if_namebuf[IF_NAMESIZE] = {""}; |
28 | 609 | char *if_name = if_indextoname(pkt_info->ipi6_ifindex, if_namebuf); |
29 | 609 | if (!if_name) { |
30 | 0 | if_name = "unknown interface"; |
31 | 0 | } |
32 | 609 | dlog(LOG_DEBUG, 4, "%s received a packet", if_name); |
33 | | |
34 | 609 | char addr_str[INET6_ADDRSTRLEN]; |
35 | 609 | addrtostr(&addr->sin6_addr, addr_str, sizeof(addr_str)); |
36 | | |
37 | 609 | if (!pkt_info) { |
38 | 0 | flog(LOG_WARNING, "%s received packet with no pkt_info from %s!", if_name, addr_str); |
39 | 0 | return; |
40 | 0 | } |
41 | | |
42 | | /* |
43 | | * can this happen? |
44 | | */ |
45 | | |
46 | 609 | if (len < sizeof(struct icmp6_hdr)) { |
47 | 0 | flog(LOG_WARNING, "%s received icmpv6 packet with invalid length (%d) from %s", if_name, len, addr_str); |
48 | 0 | return; |
49 | 0 | } |
50 | | |
51 | 609 | struct icmp6_hdr *icmph = (struct icmp6_hdr *)msg; |
52 | | |
53 | 609 | if (icmph->icmp6_type != ND_ROUTER_SOLICIT && icmph->icmp6_type != ND_ROUTER_ADVERT) { |
54 | | /* |
55 | | * We just want to listen to RSs and RAs |
56 | | */ |
57 | | |
58 | 17 | flog(LOG_ERR, "%s icmpv6 filter failed", if_name); |
59 | 17 | return; |
60 | 17 | } |
61 | | |
62 | 592 | if (icmph->icmp6_type == ND_ROUTER_ADVERT) { |
63 | 493 | if (len < sizeof(struct nd_router_advert)) { |
64 | 5 | flog(LOG_WARNING, "%s received icmpv6 RA packet with invalid length (%d) from %s", if_name, len, |
65 | 5 | addr_str); |
66 | 5 | return; |
67 | 5 | } |
68 | | |
69 | 488 | if (!IN6_IS_ADDR_LINKLOCAL(&addr->sin6_addr)) { |
70 | 0 | flog(LOG_WARNING, "%s received icmpv6 RA packet with non-linklocal source address from %s", if_name, |
71 | 0 | addr_str); |
72 | 0 | return; |
73 | 0 | } |
74 | 488 | } |
75 | | |
76 | 587 | if (icmph->icmp6_type == ND_ROUTER_SOLICIT) { |
77 | 99 | if (len < sizeof(struct nd_router_solicit)) { |
78 | 0 | flog(LOG_WARNING, "%s received icmpv6 RS packet with invalid length (%d) from %s", if_name, len, |
79 | 0 | addr_str); |
80 | 0 | return; |
81 | 0 | } |
82 | 99 | } |
83 | | |
84 | 587 | if (icmph->icmp6_code != 0) { |
85 | 8 | flog(LOG_WARNING, "%s received icmpv6 RS/RA packet with invalid code (%d) from %s", if_name, icmph->icmp6_code, |
86 | 8 | addr_str); |
87 | 8 | return; |
88 | 8 | } |
89 | | |
90 | | /* get iface by received if_index */ |
91 | 579 | struct Interface *iface = find_iface_by_index(interfaces, pkt_info->ipi6_ifindex); |
92 | | |
93 | 579 | if (iface == NULL) { |
94 | 0 | dlog(LOG_WARNING, 4, "%s received icmpv6 RS/RA packet on an unknown interface with index %d", if_name, |
95 | 0 | pkt_info->ipi6_ifindex); |
96 | 0 | return; |
97 | 0 | } |
98 | | |
99 | 579 | if (!iface->state_info.ready && (0 != setup_iface(sock, iface))) { |
100 | 0 | flog(LOG_WARNING, "%s received RS or RA on %s but %s is not ready and setup_iface failed", if_name, |
101 | 0 | iface->props.name, iface->props.name); |
102 | 0 | return; |
103 | 0 | } |
104 | | |
105 | 579 | if (hoplimit != 255) { |
106 | 0 | flog(LOG_WARNING, "%s received RS or RA with invalid hoplimit %d from %s", if_name, hoplimit, addr_str); |
107 | 0 | return; |
108 | 0 | } |
109 | | |
110 | 579 | if (icmph->icmp6_type == ND_ROUTER_SOLICIT) { |
111 | 91 | dlog(LOG_DEBUG, 3, "%s received RS from: %s", if_name, addr_str); |
112 | 91 | process_rs(sock, iface, msg, len, addr); |
113 | 488 | } else if (icmph->icmp6_type == ND_ROUTER_ADVERT) { |
114 | 488 | if (0 == memcmp(&addr->sin6_addr, &iface->props.if_addr, sizeof(iface->props.if_addr))) { |
115 | 488 | dlog(LOG_DEBUG, 3, "%s received RA from: %s (myself)", if_name, addr_str); |
116 | 488 | } else { |
117 | 0 | dlog(LOG_DEBUG, 3, "%s received RA from: %s", if_name, addr_str); |
118 | 0 | } |
119 | 488 | process_ra(iface, msg, len, addr); |
120 | 488 | } |
121 | 579 | } |
122 | | |
123 | | static void process_rs(int sock, struct Interface *iface, unsigned char *msg, int len, struct sockaddr_in6 *addr) |
124 | 91 | { |
125 | | /* RFC 7772, section 5.1: |
126 | | * 5.1. Network-Side Recommendations |
127 | | * 1. Router manufacturers SHOULD allow network administrators to |
128 | | * configure the routers to respond to Router Solicitations with |
129 | | * unicast Router Advertisements if: |
130 | | * * The Router Solicitation's source address is not the |
131 | | * unspecified address, and: |
132 | | * * The solicitation contains a valid Source Link-Layer Address |
133 | | * option. |
134 | | * |
135 | | * However, testing shows that many clients do not set the SLLA option: |
136 | | * https://github.com/reubenhwk/radvd/issues/63#issuecomment-287172252 |
137 | | * As of 2017/03/16: |
138 | | * - macOS 10.12.3 sierra - sends SLLA 2 times out of 4 |
139 | | * - iOS 10.2.1 (iPhone 5s) - no SLLA |
140 | | * - Android 7.0 (sony xperia phone) - sends SLLA |
141 | | * - Android 5.1 (nexus 7 tablet) - sends SLLA |
142 | | * - Ubuntu 16.04.2 LTS w/ Network Manager, running 4.9 kernel (dell laptop) - no SLLA |
143 | | * - Windows 10 (dell laptop) - no SLLA |
144 | | * |
145 | | * We decide to ignore the SLLA option for now, and only require the |
146 | | * unspecified address check. Clients that did not set the SLLA option will |
147 | | * trigger a neighbour solicit to the solicited-node address trying to |
148 | | * resolve the link-local address to, this would still be less traffic than |
149 | | * the all-nodes multicast. |
150 | | */ |
151 | 91 | int rfc7772_unicast_response = iface->AdvRASolicitedUnicast && !IN6_IS_ADDR_UNSPECIFIED(&addr->sin6_addr); |
152 | | |
153 | | /* validation */ |
154 | 91 | len -= sizeof(struct nd_router_solicit); |
155 | | |
156 | 91 | uint8_t *opt_str = (uint8_t *)(msg + sizeof(struct nd_router_solicit)); |
157 | | |
158 | 13.5k | while (len > 0) { |
159 | 13.5k | if (len < 2) { |
160 | 2 | flog(LOG_WARNING, "trailing garbage in RS"); |
161 | 2 | return; |
162 | 2 | } |
163 | | |
164 | 13.5k | int const optlen = (opt_str[1] << 3); |
165 | | |
166 | 13.5k | if (optlen == 0) { |
167 | 27 | flog(LOG_WARNING, "zero length option in RS"); |
168 | 27 | return; |
169 | 13.5k | } else if (optlen > len) { |
170 | 37 | flog(LOG_WARNING, "option length greater than total length in RS"); |
171 | 37 | return; |
172 | 37 | } |
173 | | |
174 | 13.4k | if (*opt_str == ND_OPT_SOURCE_LINKADDR && IN6_IS_ADDR_UNSPECIFIED(&addr->sin6_addr)) { |
175 | 0 | flog(LOG_WARNING, |
176 | 0 | "received icmpv6 RS packet with unspecified source address and there is a lladdr option"); |
177 | 0 | return; |
178 | 0 | } |
179 | | |
180 | 13.4k | len -= optlen; |
181 | 13.4k | opt_str += optlen; |
182 | 13.4k | } |
183 | | |
184 | 25 | struct timespec ts; |
185 | 25 | clock_gettime(CLOCK_MONOTONIC, &ts); |
186 | | |
187 | 25 | double const delay = (MAX_RA_DELAY_SECONDS * rand() / (RAND_MAX + 1.0)); |
188 | | |
189 | 25 | if (iface->UnicastOnly || rfc7772_unicast_response) { |
190 | 0 | send_ra_forall(sock, iface, &addr->sin6_addr); |
191 | 25 | } else if (timespecdiff(&ts, &iface->times.last_multicast) / 1000.0 < iface->MinDelayBetweenRAs) { |
192 | | /* last RA was sent only a few moments ago, don't send another immediately. */ |
193 | 0 | double next = iface->MinDelayBetweenRAs - (ts.tv_sec + ts.tv_nsec / 1000000000.0) + |
194 | 0 | (iface->times.last_multicast.tv_sec + iface->times.last_multicast.tv_nsec / 1000000000.0) + delay; |
195 | 0 | dlog(LOG_DEBUG, 5, "%s: rate limiting RA's, rescheduling RA %f seconds from now", iface->props.name, next); |
196 | 0 | reschedule_iface(iface, next); |
197 | 25 | } else { |
198 | | /* no RA sent in a while, send a multicast reply */ |
199 | 25 | send_ra_forall(sock, iface, NULL); |
200 | 25 | double next = rand_between(iface->MinRtrAdvInterval, iface->MaxRtrAdvInterval); |
201 | 25 | reschedule_iface(iface, next); |
202 | 25 | } |
203 | 25 | dlog(LOG_DEBUG, 2, "%s processed an RS", iface->props.name); |
204 | 25 | } |
205 | | |
206 | | /* |
207 | | * check router advertisements according to RFC 4861, 6.2.7 |
208 | | */ |
209 | | static void process_ra(struct Interface *iface, unsigned char *msg, int len, struct sockaddr_in6 *addr) |
210 | 488 | { |
211 | 488 | char addr_str[INET6_ADDRSTRLEN]; |
212 | 488 | addrtostr(&addr->sin6_addr, addr_str, sizeof(addr_str)); |
213 | | |
214 | 488 | struct nd_router_advert *radvert = (struct nd_router_advert *)msg; |
215 | | |
216 | 488 | if ((radvert->nd_ra_curhoplimit && iface->ra_header_info.AdvCurHopLimit) && |
217 | 0 | (radvert->nd_ra_curhoplimit != iface->ra_header_info.AdvCurHopLimit)) { |
218 | 0 | flog(LOG_WARNING, "our AdvCurHopLimit on %s doesn't agree with %s", iface->props.name, addr_str); |
219 | 0 | } |
220 | | |
221 | 488 | if ((radvert->nd_ra_flags_reserved & ND_RA_FLAG_MANAGED) && !iface->ra_header_info.AdvManagedFlag) { |
222 | 145 | flog(LOG_WARNING, "our AdvManagedFlag on %s doesn't agree with %s", iface->props.name, addr_str); |
223 | 145 | } |
224 | | |
225 | 488 | if ((radvert->nd_ra_flags_reserved & ND_RA_FLAG_OTHER) && !iface->ra_header_info.AdvOtherConfigFlag) { |
226 | 129 | flog(LOG_WARNING, "our AdvOtherConfigFlag on %s doesn't agree with %s", iface->props.name, addr_str); |
227 | 129 | } |
228 | | |
229 | | /* note: we don't check the default router preference here, because they're likely different */ |
230 | | |
231 | 488 | if ((radvert->nd_ra_reachable && iface->ra_header_info.AdvReachableTime) && |
232 | 0 | (ntohl(radvert->nd_ra_reachable) != iface->ra_header_info.AdvReachableTime)) { |
233 | 0 | flog(LOG_WARNING, "our AdvReachableTime on %s doesn't agree with %s", iface->props.name, addr_str); |
234 | 0 | } |
235 | | |
236 | 488 | if ((radvert->nd_ra_retransmit && iface->ra_header_info.AdvRetransTimer) && |
237 | 0 | (ntohl(radvert->nd_ra_retransmit) != iface->ra_header_info.AdvRetransTimer)) { |
238 | 0 | flog(LOG_WARNING, "our AdvRetransTimer on %s doesn't agree with %s", iface->props.name, addr_str); |
239 | 0 | } |
240 | | |
241 | 488 | len -= sizeof(struct nd_router_advert); |
242 | | |
243 | 488 | if (len == 0) |
244 | 74 | return; |
245 | | |
246 | 414 | uint8_t *opt_str = (uint8_t *)(msg + sizeof(struct nd_router_advert)); |
247 | | |
248 | 56.0k | while (len > 0) { |
249 | | |
250 | 55.8k | if (len < 2) { |
251 | 15 | flog(LOG_ERR, "trailing garbage in RA on %s from %s", iface->props.name, addr_str); |
252 | 15 | break; |
253 | 15 | } |
254 | | |
255 | 55.8k | int optlen = (opt_str[1] << 3); |
256 | | |
257 | 55.8k | if (optlen == 0) { |
258 | 111 | flog(LOG_ERR, "zero length option in RA on %s from %s", iface->props.name, addr_str); |
259 | 111 | break; |
260 | 55.7k | } else if (optlen > len) { |
261 | 92 | flog(LOG_ERR, "option length (%d) greater than total" |
262 | 92 | " length (%d) in RA on %s from %s", |
263 | 92 | optlen, len, iface->props.name, addr_str); |
264 | 92 | break; |
265 | 92 | } |
266 | | |
267 | 55.6k | switch (*opt_str) { |
268 | 3.81k | case ND_OPT_MTU: { |
269 | 3.81k | struct nd_opt_mtu *mtu = (struct nd_opt_mtu *)opt_str; |
270 | 3.81k | if (len < sizeof(*mtu)) |
271 | 0 | return; |
272 | | |
273 | 3.81k | if (iface->AdvLinkMTU && (ntohl(mtu->nd_opt_mtu_mtu) != iface->AdvLinkMTU)) { |
274 | 0 | flog(LOG_WARNING, "our AdvLinkMTU on %s doesn't agree with %s", iface->props.name, addr_str); |
275 | 0 | } |
276 | 3.81k | break; |
277 | 3.81k | } |
278 | 1.96k | case ND_OPT_PREFIX_INFORMATION: { |
279 | 1.96k | struct nd_opt_prefix_info *pinfo = (struct nd_opt_prefix_info *)opt_str; |
280 | 1.96k | if (len < sizeof(*pinfo)) |
281 | 11 | return; |
282 | 1.95k | int preferred = ntohl(pinfo->nd_opt_pi_preferred_time); |
283 | 1.95k | int valid = ntohl(pinfo->nd_opt_pi_valid_time); |
284 | | |
285 | 1.95k | struct AdvPrefix *prefix = iface->AdvPrefixList; |
286 | 1.95k | while (prefix) { |
287 | 0 | char prefix_str[INET6_ADDRSTRLEN]; |
288 | 0 | if ((prefix->PrefixLen == pinfo->nd_opt_pi_prefix_len) && |
289 | 0 | addr_match(&prefix->Prefix, &pinfo->nd_opt_pi_prefix, prefix->PrefixLen)) { |
290 | 0 | addrtostr(&prefix->Prefix, prefix_str, sizeof(prefix_str)); |
291 | |
|
292 | 0 | if (!prefix->DecrementLifetimesFlag && valid != prefix->AdvValidLifetime) { |
293 | 0 | flog(LOG_WARNING, "our AdvValidLifetime on" |
294 | 0 | " %s for %s doesn't agree with %s", |
295 | 0 | iface->props.name, prefix_str, addr_str); |
296 | 0 | } |
297 | 0 | if (!prefix->DecrementLifetimesFlag && preferred != prefix->AdvPreferredLifetime) { |
298 | 0 | flog(LOG_WARNING, "our AdvPreferredLifetime on" |
299 | 0 | " %s for %s doesn't agree with %s", |
300 | 0 | iface->props.name, prefix_str, addr_str); |
301 | 0 | } |
302 | 0 | } |
303 | |
|
304 | 0 | prefix = prefix->next; |
305 | 0 | } |
306 | 1.95k | break; |
307 | 1.96k | } |
308 | 209 | case ND_OPT_ROUTE_INFORMATION: |
309 | | /* not checked: these will very likely vary a lot */ |
310 | 209 | break; |
311 | 20.2k | case ND_OPT_SOURCE_LINKADDR: |
312 | | /* not checked */ |
313 | 20.2k | break; |
314 | 24.5k | case ND_OPT_TARGET_LINKADDR: |
315 | 24.9k | case ND_OPT_REDIRECTED_HEADER: |
316 | 24.9k | flog(LOG_ERR, "invalid option %d in RA on %s from %s", (int)*opt_str, iface->props.name, addr_str); |
317 | 24.9k | break; |
318 | | /* Mobile IPv6 extensions */ |
319 | 468 | case ND_OPT_RTR_ADV_INTERVAL: |
320 | 712 | case ND_OPT_HOME_AGENT_INFO: |
321 | | /* not checked */ |
322 | 712 | break; |
323 | 1.10k | case ND_OPT_RDNSS_INFORMATION: { |
324 | 1.10k | char rdnss_str[INET6_ADDRSTRLEN]; |
325 | 1.10k | struct nd_opt_rdnss_info_local *rdnssinfo = (struct nd_opt_rdnss_info_local *)opt_str; |
326 | 1.10k | if (len < sizeof(*rdnssinfo)) |
327 | 0 | return; |
328 | | |
329 | | // must be a multiple of 2, in the range of 3..255 |
330 | | // https://datatracker.ietf.org/doc/html/rfc8106#section-5.1 |
331 | 1.10k | if (rdnssinfo->nd_opt_rdnssi_len >= 3 && rdnssinfo->nd_opt_rdnssi_len % 2 == 1) { |
332 | 4.48k | for (int i = 0; i < (rdnssinfo->nd_opt_rdnssi_len - 1) / 2; i++) { |
333 | 4.00k | if (!check_rdnss_presence(iface->AdvRDNSSList, &rdnssinfo->nd_opt_rdnssi_addr[i])) { |
334 | 4.00k | addrtostr(&rdnssinfo->nd_opt_rdnssi_addr[i], rdnss_str, sizeof(rdnss_str)); |
335 | 4.00k | flog(LOG_WARNING, "RDNSS address %s received on %s from %s is not advertised by us", |
336 | 4.00k | rdnss_str, iface->props.name, addr_str); |
337 | 4.00k | } |
338 | 4.00k | } |
339 | 626 | } else { |
340 | 626 | flog(LOG_ERR, "invalid len %i in RDNSS option on %s from %s", |
341 | 626 | rdnssinfo->nd_opt_rdnssi_len, iface->props.name, addr_str); |
342 | 626 | } |
343 | | |
344 | 1.10k | break; |
345 | 1.10k | } |
346 | 999 | case ND_OPT_DNSSL_INFORMATION: { |
347 | 999 | struct nd_opt_dnssl_info_local *dnsslinfo = (struct nd_opt_dnssl_info_local *)opt_str; |
348 | 999 | if (len < sizeof(*dnsslinfo)) |
349 | 0 | return; |
350 | | |
351 | 17.3k | for (int offset = 0; offset < (dnsslinfo->nd_opt_dnssli_len - 1) * 8;) { |
352 | 16.6k | char suffix[256] = {""}; |
353 | 16.6k | if (&dnsslinfo->nd_opt_dnssli_suffixes[offset] - opt_str >= len) |
354 | 0 | return; |
355 | 16.6k | int label_len = dnsslinfo->nd_opt_dnssli_suffixes[offset++]; |
356 | | |
357 | 16.6k | if (label_len == 0) { |
358 | | /* |
359 | | * Ignore empty suffixes. They're |
360 | | * probably just padding... |
361 | | */ |
362 | 2.05k | if (suffix[0] == '\0') |
363 | 2.05k | continue; |
364 | | |
365 | 0 | if (!check_dnssl_presence(iface->AdvDNSSLList, suffix)) { |
366 | 0 | flog(LOG_WARNING, |
367 | 0 | "DNSSL suffix %s received on %s from %s is not advertised by us", suffix, |
368 | 0 | iface->props.name, addr_str); |
369 | 0 | } |
370 | |
|
371 | 0 | suffix[0] = '\0'; |
372 | 0 | continue; |
373 | 2.05k | } |
374 | | |
375 | | /* |
376 | | * 1) must not overflow int: label + 2, offset + label_len |
377 | | * 2) last byte of dnssli_suffix must not overflow opt_str + len |
378 | | */ |
379 | 14.6k | if ((sizeof(suffix) - strlen(suffix)) < (label_len + 2) || label_len >= (INT_MAX - 1) || |
380 | 14.3k | &dnsslinfo->nd_opt_dnssli_suffixes[offset + label_len] - opt_str >= len || |
381 | 14.2k | offset + label_len < offset) { |
382 | 336 | flog(LOG_ERR, "oversized suffix in DNSSL option on %s from %s", iface->props.name, |
383 | 336 | addr_str); |
384 | 336 | break; |
385 | 336 | } |
386 | | |
387 | 14.2k | if (suffix[0] != '\0') |
388 | 0 | strcat(suffix, "."); |
389 | 14.2k | strncat(suffix, (char *)&dnsslinfo->nd_opt_dnssli_suffixes[offset], label_len); |
390 | 14.2k | offset += label_len; |
391 | 14.2k | } |
392 | 999 | break; |
393 | 999 | } |
394 | 999 | case ND_OPT_PREF64: |
395 | | /* not checked */ |
396 | 244 | break; |
397 | 1.50k | default: |
398 | 1.50k | dlog(LOG_DEBUG, 1, "unknown option %d in RA on %s from %s", (int)*opt_str, iface->props.name, addr_str); |
399 | 1.50k | break; |
400 | 55.6k | } |
401 | | |
402 | 55.6k | len -= optlen; |
403 | 55.6k | opt_str += optlen; |
404 | 55.6k | } |
405 | 403 | dlog(LOG_DEBUG, 2, "processed RA on %s", iface->props.name); |
406 | 403 | } |
407 | | |
408 | | static int addr_match(struct in6_addr *a1, struct in6_addr *a2, int prefixlen) |
409 | 0 | { |
410 | 0 | unsigned int pdw = prefixlen >> 0x05; /* num of whole uint32_t in prefix */ |
411 | 0 | if (pdw) { |
412 | 0 | if (memcmp(a1, a2, pdw << 2)) |
413 | 0 | return 0; |
414 | 0 | } |
415 | | |
416 | 0 | unsigned int pbi = prefixlen & 0x1f; /* num of bits in incomplete uint32_t in prefix */ |
417 | 0 | if (pbi) { |
418 | 0 | uint32_t w1 = *((uint32_t *)a1 + pdw); |
419 | 0 | uint32_t w2 = *((uint32_t *)a2 + pdw); |
420 | |
|
421 | 0 | uint32_t mask = htonl(((uint32_t)0xffffffff) << (0x20 - pbi)); |
422 | |
|
423 | 0 | if ((w1 ^ w2) & mask) |
424 | 0 | return 0; |
425 | 0 | } |
426 | | |
427 | 0 | return 1; |
428 | 0 | } |