]>
Commit | Line | Data |
---|---|---|
db9ecf05 | 1 | /* SPDX-License-Identifier: LGPL-2.1-or-later */ |
8041b5ba | 2 | |
54e6f97b LP |
3 | #include <net/if_arp.h> |
4 | ||
1c4baffc | 5 | #include "sd-netlink.h" |
b5efdb8a LP |
6 | |
7 | #include "alloc-util.h" | |
54e6f97b | 8 | #include "fd-util.h" |
e80af1bd | 9 | #include "local-addresses.h" |
cf0fbc49 TA |
10 | #include "macro.h" |
11 | #include "netlink-util.h" | |
760877e9 | 12 | #include "sort-util.h" |
8041b5ba | 13 | |
93bab288 YW |
14 | static int address_compare(const struct local_address *a, const struct local_address *b) { |
15 | int r; | |
5502f0d9 LP |
16 | |
17 | /* Order lowest scope first, IPv4 before IPv6, lowest interface index first */ | |
18 | ||
e9140aff LP |
19 | if (a->family == AF_INET && b->family == AF_INET6) |
20 | return -1; | |
21 | if (a->family == AF_INET6 && b->family == AF_INET) | |
22 | return 1; | |
23 | ||
93bab288 YW |
24 | r = CMP(a->scope, b->scope); |
25 | if (r != 0) | |
26 | return r; | |
5502f0d9 | 27 | |
37359b1c | 28 | r = CMP(a->priority, b->priority); |
93bab288 YW |
29 | if (r != 0) |
30 | return r; | |
5502f0d9 | 31 | |
93bab288 YW |
32 | r = CMP(a->ifindex, b->ifindex); |
33 | if (r != 0) | |
34 | return r; | |
5502f0d9 | 35 | |
00d75e57 | 36 | return memcmp(&a->address, &b->address, FAMILY_ADDRESS_SIZE(a->family)); |
5502f0d9 LP |
37 | } |
38 | ||
54e6f97b LP |
39 | static void suppress_duplicates(struct local_address *list, size_t *n_list) { |
40 | size_t old_size, new_size; | |
41 | ||
42 | /* Removes duplicate entries, assumes the list of addresses is already sorted. Updates in-place. */ | |
43 | ||
44 | if (*n_list < 2) /* list with less than two entries can't have duplicates */ | |
45 | return; | |
46 | ||
47 | old_size = *n_list; | |
48 | new_size = 1; | |
49 | ||
50 | for (size_t i = 1; i < old_size; i++) { | |
51 | ||
52 | if (address_compare(list + i, list + new_size - 1) == 0) | |
53 | continue; | |
54 | ||
55 | list[new_size++] = list[i]; | |
56 | } | |
57 | ||
58 | *n_list = new_size; | |
59 | } | |
60 | ||
61 | int local_addresses( | |
62 | sd_netlink *context, | |
63 | int ifindex, | |
64 | int af, | |
65 | struct local_address **ret) { | |
66 | ||
4afd3348 LP |
67 | _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL, *reply = NULL; |
68 | _cleanup_(sd_netlink_unrefp) sd_netlink *rtnl = NULL; | |
e80af1bd | 69 | _cleanup_free_ struct local_address *list = NULL; |
319a4f4b | 70 | size_t n_list = 0; |
d1ca51b1 | 71 | int r; |
d73c3269 | 72 | |
ee8c4568 | 73 | if (context) |
1c4baffc | 74 | rtnl = sd_netlink_ref(context); |
ee8c4568 | 75 | else { |
1c4baffc | 76 | r = sd_netlink_open(&rtnl); |
ee8c4568 LP |
77 | if (r < 0) |
78 | return r; | |
79 | } | |
8041b5ba | 80 | |
6a28b78f | 81 | r = sd_rtnl_message_new_addr(rtnl, &req, RTM_GETADDR, ifindex, af); |
d1ca51b1 TG |
82 | if (r < 0) |
83 | return r; | |
8041b5ba | 84 | |
24c0f385 | 85 | r = sd_netlink_message_set_request_dump(req, true); |
f318f643 YW |
86 | if (r < 0) |
87 | return r; | |
88 | ||
1c4baffc | 89 | r = sd_netlink_call(rtnl, req, 0, &reply); |
d1ca51b1 TG |
90 | if (r < 0) |
91 | return r; | |
d1ca51b1 | 92 | |
d856e1a7 | 93 | for (sd_netlink_message *m = reply; m; m = sd_netlink_message_next(m)) { |
e80af1bd | 94 | struct local_address *a; |
d1ca51b1 | 95 | unsigned char flags; |
5502f0d9 | 96 | uint16_t type; |
1d050e1e | 97 | int ifi, family; |
d1ca51b1 | 98 | |
1c4baffc | 99 | r = sd_netlink_message_get_errno(m); |
d1ca51b1 TG |
100 | if (r < 0) |
101 | return r; | |
102 | ||
1c4baffc | 103 | r = sd_netlink_message_get_type(m, &type); |
d1ca51b1 TG |
104 | if (r < 0) |
105 | return r; | |
d1ca51b1 | 106 | if (type != RTM_NEWADDR) |
8041b5ba LP |
107 | continue; |
108 | ||
ee8c4568 LP |
109 | r = sd_rtnl_message_addr_get_ifindex(m, &ifi); |
110 | if (r < 0) | |
111 | return r; | |
1d050e1e LP |
112 | if (ifindex > 0 && ifi != ifindex) |
113 | continue; | |
ee8c4568 | 114 | |
1d050e1e LP |
115 | r = sd_rtnl_message_addr_get_family(m, &family); |
116 | if (r < 0) | |
117 | return r; | |
5cb56068 YW |
118 | if (!IN_SET(family, AF_INET, AF_INET6)) |
119 | continue; | |
1d050e1e | 120 | if (af != AF_UNSPEC && af != family) |
ee8c4568 LP |
121 | continue; |
122 | ||
5502f0d9 | 123 | r = sd_rtnl_message_addr_get_flags(m, &flags); |
d1ca51b1 TG |
124 | if (r < 0) |
125 | return r; | |
e90863f2 | 126 | if ((flags & (IFA_F_DEPRECATED|IFA_F_TENTATIVE)) != 0) |
d73c3269 | 127 | continue; |
8041b5ba | 128 | |
319a4f4b | 129 | if (!GREEDY_REALLOC0(list, n_list+1)) |
5502f0d9 LP |
130 | return -ENOMEM; |
131 | ||
132 | a = list + n_list; | |
133 | ||
134 | r = sd_rtnl_message_addr_get_scope(m, &a->scope); | |
d1ca51b1 TG |
135 | if (r < 0) |
136 | return r; | |
8041b5ba | 137 | |
945c2931 | 138 | if (ifindex == 0 && IN_SET(a->scope, RT_SCOPE_HOST, RT_SCOPE_NOWHERE)) |
d73c3269 | 139 | continue; |
8041b5ba | 140 | |
1d050e1e | 141 | switch (family) { |
5502f0d9 | 142 | |
d1ca51b1 | 143 | case AF_INET: |
1c4baffc | 144 | r = sd_netlink_message_read_in_addr(m, IFA_LOCAL, &a->address.in); |
d1ca51b1 | 145 | if (r < 0) { |
1c4baffc | 146 | r = sd_netlink_message_read_in_addr(m, IFA_ADDRESS, &a->address.in); |
d1ca51b1 TG |
147 | if (r < 0) |
148 | continue; | |
149 | } | |
150 | break; | |
5502f0d9 | 151 | |
d1ca51b1 | 152 | case AF_INET6: |
1c4baffc | 153 | r = sd_netlink_message_read_in6_addr(m, IFA_LOCAL, &a->address.in6); |
d1ca51b1 | 154 | if (r < 0) { |
1c4baffc | 155 | r = sd_netlink_message_read_in6_addr(m, IFA_ADDRESS, &a->address.in6); |
d1ca51b1 TG |
156 | if (r < 0) |
157 | continue; | |
158 | } | |
159 | break; | |
5502f0d9 | 160 | |
d1ca51b1 | 161 | default: |
d73c3269 | 162 | continue; |
d73c3269 | 163 | } |
8041b5ba | 164 | |
ee8c4568 | 165 | a->ifindex = ifi; |
1d050e1e | 166 | a->family = family; |
8041b5ba | 167 | |
d1ca51b1 | 168 | n_list++; |
5502f0d9 | 169 | }; |
8041b5ba | 170 | |
a64f6041 YW |
171 | typesafe_qsort(list, n_list, address_compare); |
172 | suppress_duplicates(list, &n_list); | |
173 | ||
174 | if (ret) | |
c3a8c6aa | 175 | *ret = TAKE_PTR(list); |
e9140aff LP |
176 | |
177 | return (int) n_list; | |
178 | } | |
179 | ||
bff94a84 YW |
180 | static int add_local_gateway( |
181 | struct local_address **list, | |
182 | size_t *n_list, | |
bff94a84 YW |
183 | int af, |
184 | int ifindex, | |
37359b1c | 185 | uint32_t priority, |
bff94a84 YW |
186 | const RouteVia *via) { |
187 | ||
188 | assert(list); | |
189 | assert(n_list); | |
bff94a84 YW |
190 | assert(via); |
191 | ||
192 | if (af != AF_UNSPEC && af != via->family) | |
193 | return 0; | |
194 | ||
319a4f4b | 195 | if (!GREEDY_REALLOC(*list, *n_list + 1)) |
bff94a84 YW |
196 | return -ENOMEM; |
197 | ||
198 | (*list)[(*n_list)++] = (struct local_address) { | |
199 | .ifindex = ifindex, | |
37359b1c | 200 | .priority = priority, |
bff94a84 YW |
201 | .family = via->family, |
202 | .address = via->address, | |
203 | }; | |
204 | ||
205 | return 0; | |
206 | } | |
207 | ||
54e6f97b LP |
208 | int local_gateways( |
209 | sd_netlink *context, | |
210 | int ifindex, | |
211 | int af, | |
212 | struct local_address **ret) { | |
213 | ||
4afd3348 LP |
214 | _cleanup_(sd_netlink_message_unrefp) sd_netlink_message *req = NULL, *reply = NULL; |
215 | _cleanup_(sd_netlink_unrefp) sd_netlink *rtnl = NULL; | |
e9140aff | 216 | _cleanup_free_ struct local_address *list = NULL; |
319a4f4b | 217 | size_t n_list = 0; |
e9140aff LP |
218 | int r; |
219 | ||
5cb56068 YW |
220 | /* The RTA_VIA attribute is used only for IPv4 routes with an IPv6 gateway. If IPv4 gateways are |
221 | * requested (af == AF_INET), then we do not return IPv6 gateway addresses. Similary, if IPv6 | |
222 | * gateways are requested (af == AF_INET6), then we do not return gateway addresses for IPv4 routes. | |
223 | * So, the RTA_VIA attribute is only parsed when af == AF_UNSPEC. */ | |
224 | bool allow_via = af == AF_UNSPEC; | |
225 | ||
e9140aff | 226 | if (context) |
1c4baffc | 227 | rtnl = sd_netlink_ref(context); |
e9140aff | 228 | else { |
1c4baffc | 229 | r = sd_netlink_open(&rtnl); |
e9140aff LP |
230 | if (r < 0) |
231 | return r; | |
232 | } | |
233 | ||
1d050e1e | 234 | r = sd_rtnl_message_new_route(rtnl, &req, RTM_GETROUTE, af, RTPROT_UNSPEC); |
e9140aff LP |
235 | if (r < 0) |
236 | return r; | |
237 | ||
3e0eeb8e YW |
238 | r = sd_rtnl_message_route_set_type(req, RTN_UNICAST); |
239 | if (r < 0) | |
240 | return r; | |
241 | ||
242 | r = sd_rtnl_message_route_set_table(req, RT_TABLE_MAIN); | |
243 | if (r < 0) | |
244 | return r; | |
245 | ||
24c0f385 | 246 | r = sd_netlink_message_set_request_dump(req, true); |
e9140aff LP |
247 | if (r < 0) |
248 | return r; | |
249 | ||
1c4baffc | 250 | r = sd_netlink_call(rtnl, req, 0, &reply); |
e9140aff LP |
251 | if (r < 0) |
252 | return r; | |
253 | ||
bff94a84 | 254 | for (sd_netlink_message *m = reply; m; m = sd_netlink_message_next(m)) { |
e9140aff | 255 | uint16_t type; |
d1b014df | 256 | unsigned char dst_len, src_len, table; |
37359b1c | 257 | uint32_t ifi = 0, priority = 0; |
1d050e1e | 258 | int family; |
e9140aff | 259 | |
1c4baffc | 260 | r = sd_netlink_message_get_errno(m); |
e9140aff LP |
261 | if (r < 0) |
262 | return r; | |
263 | ||
1c4baffc | 264 | r = sd_netlink_message_get_type(m, &type); |
e9140aff LP |
265 | if (r < 0) |
266 | return r; | |
e9140aff LP |
267 | if (type != RTM_NEWROUTE) |
268 | continue; | |
269 | ||
a98433c0 | 270 | /* We only care for default routes */ |
584d0d2a | 271 | r = sd_rtnl_message_route_get_dst_prefixlen(m, &dst_len); |
e9140aff LP |
272 | if (r < 0) |
273 | return r; | |
e9140aff LP |
274 | if (dst_len != 0) |
275 | continue; | |
276 | ||
584d0d2a | 277 | r = sd_rtnl_message_route_get_src_prefixlen(m, &src_len); |
a98433c0 LP |
278 | if (r < 0) |
279 | return r; | |
280 | if (src_len != 0) | |
281 | continue; | |
282 | ||
d1b014df LP |
283 | r = sd_rtnl_message_route_get_table(m, &table); |
284 | if (r < 0) | |
285 | return r; | |
286 | if (table != RT_TABLE_MAIN) | |
287 | continue; | |
288 | ||
37359b1c | 289 | r = sd_netlink_message_read_u32(m, RTA_PRIORITY, &priority); |
bff94a84 | 290 | if (r < 0 && r != -ENODATA) |
e9140aff | 291 | return r; |
e9140aff | 292 | |
1d050e1e LP |
293 | r = sd_rtnl_message_route_get_family(m, &family); |
294 | if (r < 0) | |
295 | return r; | |
bff94a84 | 296 | if (!IN_SET(family, AF_INET, AF_INET6)) |
1d050e1e | 297 | continue; |
5cb56068 YW |
298 | if (af != AF_UNSPEC && af != family) |
299 | continue; | |
1d050e1e | 300 | |
bff94a84 YW |
301 | r = sd_netlink_message_read_u32(m, RTA_OIF, &ifi); |
302 | if (r < 0 && r != -ENODATA) | |
303 | return r; | |
304 | if (r >= 0) { | |
305 | if (ifi <= 0) | |
306 | return -EINVAL; | |
307 | if (ifindex > 0 && (int) ifi != ifindex) | |
308 | continue; | |
e9140aff | 309 | |
4019bec8 | 310 | union in_addr_union gateway; |
bff94a84 YW |
311 | r = netlink_message_read_in_addr_union(m, RTA_GATEWAY, family, &gateway); |
312 | if (r < 0 && r != -ENODATA) | |
313 | return r; | |
314 | if (r >= 0) { | |
315 | via.family = family; | |
316 | via.address = gateway; | |
37359b1c | 317 | r = add_local_gateway(&list, &n_list, af, ifi, priority, &via); |
bff94a84 YW |
318 | if (r < 0) |
319 | return r; | |
e9140aff | 320 | |
e9140aff | 321 | continue; |
bff94a84 | 322 | } |
e9140aff | 323 | |
5cb56068 YW |
324 | if (!allow_via) |
325 | continue; | |
326 | ||
bff94a84 | 327 | if (family != AF_INET) |
e9140aff LP |
328 | continue; |
329 | ||
4019bec8 | 330 | RouteVia via; |
bff94a84 YW |
331 | r = sd_netlink_message_read(m, RTA_VIA, sizeof(via), &via); |
332 | if (r < 0 && r != -ENODATA) | |
333 | return r; | |
334 | if (r >= 0) { | |
5cb56068 YW |
335 | if (via.family != AF_INET6) |
336 | return -EBADMSG; | |
337 | ||
37359b1c | 338 | r = add_local_gateway(&list, &n_list, af, ifi, priority, &via); |
bff94a84 YW |
339 | if (r < 0) |
340 | return r; | |
341 | ||
342 | continue; | |
343 | } | |
e9140aff LP |
344 | } |
345 | ||
4019bec8 YW |
346 | size_t rta_len; |
347 | _cleanup_free_ void *rta_multipath = NULL; | |
bff94a84 YW |
348 | r = sd_netlink_message_read_data(m, RTA_MULTIPATH, &rta_len, &rta_multipath); |
349 | if (r < 0 && r != -ENODATA) | |
350 | return r; | |
351 | if (r >= 0) { | |
4019bec8 | 352 | _cleanup_ordered_set_free_free_ OrderedSet *multipath_routes = NULL; |
bff94a84 | 353 | MultipathRoute *mr; |
e9140aff | 354 | |
bff94a84 YW |
355 | r = rtattr_read_nexthop(rta_multipath, rta_len, family, &multipath_routes); |
356 | if (r < 0) | |
357 | return r; | |
e9140aff | 358 | |
bff94a84 YW |
359 | ORDERED_SET_FOREACH(mr, multipath_routes) { |
360 | if (ifindex > 0 && mr->ifindex != ifindex) | |
361 | continue; | |
362 | ||
5cb56068 YW |
363 | if (!allow_via && family != mr->gateway.family) |
364 | continue; | |
365 | ||
37359b1c | 366 | r = add_local_gateway(&list, &n_list, af, ifi, priority, &mr->gateway); |
bff94a84 YW |
367 | if (r < 0) |
368 | return r; | |
369 | } | |
370 | } | |
e9140aff LP |
371 | } |
372 | ||
a64f6041 YW |
373 | typesafe_qsort(list, n_list, address_compare); |
374 | suppress_duplicates(list, &n_list); | |
375 | ||
376 | if (ret) | |
54e6f97b | 377 | *ret = TAKE_PTR(list); |
54e6f97b LP |
378 | |
379 | return (int) n_list; | |
380 | } | |
381 | ||
382 | int local_outbounds( | |
383 | sd_netlink *context, | |
384 | int ifindex, | |
385 | int af, | |
386 | struct local_address **ret) { | |
387 | ||
388 | _cleanup_free_ struct local_address *list = NULL, *gateways = NULL; | |
319a4f4b | 389 | size_t n_list = 0; |
54e6f97b LP |
390 | int r, n_gateways; |
391 | ||
392 | /* Determines our default outbound addresses, i.e. the "primary" local addresses we use to talk to IP | |
393 | * addresses behind the default routes. This is still an address of the local host (i.e. this doesn't | |
394 | * resolve NAT or so), but it's the set of addresses the local IP stack most likely uses to talk to | |
395 | * other hosts. | |
396 | * | |
397 | * This works by connect()ing a SOCK_DGRAM socket to the local gateways, and then reading the IP | |
398 | * address off the socket that was chosen for the routing decision. */ | |
399 | ||
400 | n_gateways = local_gateways(context, ifindex, af, &gateways); | |
401 | if (n_gateways < 0) | |
402 | return n_gateways; | |
403 | if (n_gateways == 0) { | |
404 | /* No gateways? Then we have no outbound addresses either. */ | |
405 | if (ret) | |
406 | *ret = NULL; | |
407 | ||
408 | return 0; | |
409 | } | |
410 | ||
411 | for (int i = 0; i < n_gateways; i++) { | |
254d1313 | 412 | _cleanup_close_ int fd = -EBADF; |
54e6f97b LP |
413 | union sockaddr_union sa; |
414 | socklen_t salen; | |
415 | ||
416 | fd = socket(gateways[i].family, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0); | |
417 | if (fd < 0) | |
418 | return -errno; | |
419 | ||
420 | switch (gateways[i].family) { | |
421 | ||
422 | case AF_INET: | |
423 | sa.in = (struct sockaddr_in) { | |
424 | .sin_family = AF_INET, | |
425 | .sin_addr = gateways[i].address.in, | |
098d42b6 YW |
426 | .sin_port = htobe16(53), /* doesn't really matter which port we pick — |
427 | * we just care about the routing decision */ | |
54e6f97b LP |
428 | }; |
429 | ||
430 | break; | |
431 | ||
432 | case AF_INET6: | |
433 | sa.in6 = (struct sockaddr_in6) { | |
434 | .sin6_family = AF_INET6, | |
435 | .sin6_addr = gateways[i].address.in6, | |
436 | .sin6_port = htobe16(53), | |
437 | .sin6_scope_id = gateways[i].ifindex, | |
438 | }; | |
439 | ||
440 | break; | |
441 | ||
442 | default: | |
04499a70 | 443 | assert_not_reached(); |
54e6f97b LP |
444 | } |
445 | ||
446 | /* So ideally we'd just use IP_UNICAST_IF here to pass the ifindex info to the kernel before | |
447 | * connect()ing, sot that it influences the routing decision. However, on current kernels | |
448 | * IP_UNICAST_IF doesn't actually influence the routing decision for UDP — which I think | |
449 | * should probably just be considered a bug. Once that bug is fixed this is the best API to | |
450 | * use, since it is the most lightweight. */ | |
451 | r = socket_set_unicast_if(fd, gateways[i].family, gateways[i].ifindex); | |
452 | if (r < 0) | |
453 | log_debug_errno(r, "Failed to set unicast interface index %i, ignoring: %m", gateways[i].ifindex); | |
454 | ||
455 | /* We'll also use SO_BINDTOINDEX. This requires CAP_NET_RAW on old kernels, hence there's a | |
456 | * good chance this fails. Since 5.7 this restriction was dropped and the first | |
457 | * SO_BINDTOINDEX on a socket may be done without privileges. This one has the benefit of | |
458 | * really influencing the routing decision, i.e. this one definitely works for us — as long | |
098d42b6 | 459 | * as we have the privileges for it. */ |
54e6f97b LP |
460 | r = socket_bind_to_ifindex(fd, gateways[i].ifindex); |
461 | if (r < 0) | |
462 | log_debug_errno(r, "Failed to bind socket to interface %i, ignoring: %m", gateways[i].ifindex); | |
463 | ||
464 | /* Let's now connect() to the UDP socket, forcing the kernel to make a routing decision and | |
465 | * auto-bind the socket. We ignore failures on this, since that failure might happen for a | |
466 | * multitude of reasons (policy/firewall issues, who knows?) and some of them might be | |
467 | * *after* the routing decision and the auto-binding already took place. If so we can still | |
468 | * make use of the binding and return it. Hence, let's not unnecessarily fail early here: we | |
469 | * can still easily detect if the auto-binding worked or not, by comparing the bound IP | |
098d42b6 | 470 | * address with zero — which we do below. */ |
54e6f97b LP |
471 | if (connect(fd, &sa.sa, SOCKADDR_LEN(sa)) < 0) |
472 | log_debug_errno(errno, "Failed to connect SOCK_DGRAM socket to gateway, ignoring: %m"); | |
473 | ||
474 | /* Let's now read the socket address of the socket. A routing decision should have been | |
475 | * made. Let's verify that and use the data. */ | |
476 | salen = SOCKADDR_LEN(sa); | |
477 | if (getsockname(fd, &sa.sa, &salen) < 0) | |
478 | return -errno; | |
479 | assert(sa.sa.sa_family == gateways[i].family); | |
480 | assert(salen == SOCKADDR_LEN(sa)); | |
481 | ||
482 | switch (gateways[i].family) { | |
483 | ||
484 | case AF_INET: | |
485 | if (in4_addr_is_null(&sa.in.sin_addr)) /* Auto-binding didn't work. :-( */ | |
486 | continue; | |
487 | ||
319a4f4b | 488 | if (!GREEDY_REALLOC(list, n_list+1)) |
54e6f97b LP |
489 | return -ENOMEM; |
490 | ||
491 | list[n_list++] = (struct local_address) { | |
492 | .family = gateways[i].family, | |
493 | .ifindex = gateways[i].ifindex, | |
494 | .address.in = sa.in.sin_addr, | |
495 | }; | |
496 | ||
497 | break; | |
498 | ||
499 | case AF_INET6: | |
500 | if (in6_addr_is_null(&sa.in6.sin6_addr)) | |
501 | continue; | |
502 | ||
319a4f4b | 503 | if (!GREEDY_REALLOC(list, n_list+1)) |
54e6f97b LP |
504 | return -ENOMEM; |
505 | ||
506 | list[n_list++] = (struct local_address) { | |
507 | .family = gateways[i].family, | |
508 | .ifindex = gateways[i].ifindex, | |
509 | .address.in6 = sa.in6.sin6_addr, | |
510 | }; | |
511 | break; | |
512 | ||
513 | default: | |
04499a70 | 514 | assert_not_reached(); |
54e6f97b LP |
515 | } |
516 | } | |
517 | ||
a64f6041 YW |
518 | typesafe_qsort(list, n_list, address_compare); |
519 | suppress_duplicates(list, &n_list); | |
520 | ||
521 | if (ret) | |
c3a8c6aa | 522 | *ret = TAKE_PTR(list); |
d73c3269 | 523 | |
e80af1bd | 524 | return (int) n_list; |
8041b5ba | 525 | } |