123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673 |
- /*
- * Copyright (C) 2014 John Crispin <blogic@openwrt.org>
- * Copyright (C) 2014 Felix Fietkau <nbd@openwrt.org>
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU Lesser General Public License version 2.1
- * as published by the Free Software Foundation
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- */
- #define _GNU_SOURCE
- #include <sys/socket.h>
- #include <sys/ioctl.h>
- #include <sys/types.h>
- #include <sys/stat.h>
- #include <sys/utsname.h>
- #include <net/if.h>
- #include <netinet/in.h>
- #include <arpa/inet.h>
- #include <sys/types.h>
- #include <ifaddrs.h>
- #include <stdlib.h>
- #include <string.h>
- #include <unistd.h>
- #include <stdio.h>
- #include <errno.h>
- #include <libubox/usock.h>
- #include <libubox/uloop.h>
- #include <libubox/avl-cmp.h>
- #include <libubox/utils.h>
- #include "cache.h"
- #include "interface.h"
- #include "util.h"
- #include "dns.h"
- #include "announce.h"
- #include "service.h"
- static struct uloop_fd ufd[] = {
- [SOCK_UC_IPV4] = { .fd = -1 },
- [SOCK_UC_IPV6] = { .fd = -1 },
- [SOCK_MC_IPV4] = { .fd = -1 },
- [SOCK_MC_IPV6] = { .fd = -1 },
- };
- static int
- interface_send_packet4(struct interface *iface, struct sockaddr_in *to, struct iovec *iov, int iov_len)
- {
- static size_t cmsg_data[( CMSG_SPACE(sizeof(struct in_pktinfo)) / sizeof(size_t)) + 1];
- static struct sockaddr_in a = {};
- static struct msghdr m = {
- .msg_name = (struct sockaddr *) &a,
- .msg_namelen = sizeof(a),
- .msg_control = cmsg_data,
- .msg_controllen = CMSG_LEN(sizeof(struct in_pktinfo)),
- };
- struct in_pktinfo *pkti;
- struct cmsghdr *cmsg;
- int fd;
- a.sin_family = AF_INET;
- a.sin_port = htons(MCAST_PORT);
- m.msg_iov = iov;
- m.msg_iovlen = iov_len;
- memset(cmsg_data, 0, sizeof(cmsg_data));
- cmsg = CMSG_FIRSTHDR(&m);
- cmsg->cmsg_len = m.msg_controllen;
- cmsg->cmsg_level = IPPROTO_IP;
- cmsg->cmsg_type = IP_PKTINFO;
- pkti = (struct in_pktinfo*) CMSG_DATA(cmsg);
- pkti->ipi_ifindex = iface->ifindex;
- fd = ufd[iface->type].fd;
- if (interface_multicast(iface)) {
- a.sin_addr.s_addr = inet_addr(MCAST_ADDR);
- if (to)
- fprintf(stderr, "Ignoring IPv4 address for multicast interface\n");
- } else {
- a.sin_addr.s_addr = to->sin_addr.s_addr;
- }
- return sendmsg(fd, &m, 0);
- }
- static int
- interface_send_packet6(struct interface *iface, struct sockaddr_in6 *to, struct iovec *iov, int iov_len)
- {
- static size_t cmsg_data[( CMSG_SPACE(sizeof(struct in6_pktinfo)) / sizeof(size_t)) + 1];
- static struct sockaddr_in6 a = {};
- static struct msghdr m = {
- .msg_name = (struct sockaddr *) &a,
- .msg_namelen = sizeof(a),
- .msg_control = cmsg_data,
- .msg_controllen = CMSG_LEN(sizeof(struct in6_pktinfo)),
- };
- struct in6_pktinfo *pkti;
- struct cmsghdr *cmsg;
- int fd;
- a.sin6_family = AF_INET6;
- a.sin6_port = htons(MCAST_PORT);
- a.sin6_scope_id = iface->ifindex;
- m.msg_iov = iov;
- m.msg_iovlen = iov_len;
- memset(cmsg_data, 0, sizeof(cmsg_data));
- cmsg = CMSG_FIRSTHDR(&m);
- cmsg->cmsg_len = m.msg_controllen;
- cmsg->cmsg_level = IPPROTO_IPV6;
- cmsg->cmsg_type = IPV6_PKTINFO;
- pkti = (struct in6_pktinfo*) CMSG_DATA(cmsg);
- pkti->ipi6_ifindex = iface->ifindex;
- fd = ufd[iface->type].fd;
- if (interface_multicast(iface)) {
- inet_pton(AF_INET6, MCAST_ADDR6, &a.sin6_addr);
- if (to)
- fprintf(stderr, "Ignoring IPv6 address for multicast interface\n");
- } else {
- a.sin6_addr = to->sin6_addr;
- }
- return sendmsg(fd, &m, 0);
- }
- int
- interface_send_packet(struct interface *iface, struct sockaddr *to, struct iovec *iov, int iov_len)
- {
- if (!interface_multicast(iface) && !to) {
- fprintf(stderr, "No IP address specified for unicast interface\n");
- errno = EINVAL;
- return -1;
- }
- if (debug > 1) {
- fprintf(stderr, "TX ipv%d: %s\n", interface_ipv6(iface) ? 6 : 4, iface->name);
- fprintf(stderr, " multicast: %d\n", interface_multicast(iface));
- }
- if (interface_ipv6(iface))
- return interface_send_packet6(iface, (struct sockaddr_in6 *)to, iov, iov_len);
- return interface_send_packet4(iface, (struct sockaddr_in *)to, iov, iov_len);
- }
- static struct interface *interface_lookup(unsigned int ifindex, enum umdns_socket_type type)
- {
- struct interface *iface;
- vlist_for_each_element(&interfaces, iface, node)
- if (iface->ifindex == ifindex && iface->type == type)
- return iface;
- return NULL;
- }
- static void interface_free(struct interface *iface)
- {
- announce_free(iface);
- free(iface->addrs.v4);
- free(iface);
- }
- static int
- interface_valid_src(void *ip1, void *mask, void *ip2, int len)
- {
- uint8_t *i1 = ip1;
- uint8_t *i2 = ip2;
- uint8_t *m = mask;
- int i;
- if (cfg_no_subnet)
- return 0;
- for (i = 0; i < len; i++, i1++, i2++, m++) {
- if ((*i1 & *m) != (*i2 & *m))
- return -1;
- }
- return 0;
- }
- static void
- read_socket4(struct uloop_fd *u, unsigned int events)
- {
- enum umdns_socket_type type = (enum umdns_socket_type)(u - ufd);
- struct interface *iface;
- static uint8_t buffer[8 * 1024];
- struct iovec iov[1];
- char cmsg[CMSG_SPACE(sizeof(struct in_pktinfo)) + CMSG_SPACE(sizeof(int)) + 1];
- struct cmsghdr *cmsgptr;
- struct msghdr msg;
- socklen_t len;
- struct sockaddr_in from;
- int flags = 0;
- uint8_t ttl = 0;
- struct in_pktinfo *inp = NULL;
- bool valid_src = false;
- if (u->eof) {
- uloop_end();
- return;
- }
- iov[0].iov_base = buffer;
- iov[0].iov_len = sizeof(buffer);
- memset(&msg, 0, sizeof(msg));
- msg.msg_name = (struct sockaddr *) &from;
- msg.msg_namelen = sizeof(struct sockaddr_in);
- msg.msg_iov = iov;
- msg.msg_iovlen = 1;
- msg.msg_control = &cmsg;
- msg.msg_controllen = sizeof(cmsg);
- len = recvmsg(u->fd, &msg, flags);
- if (len == -1) {
- perror("read failed");
- return;
- }
- for (cmsgptr = CMSG_FIRSTHDR(&msg); cmsgptr != NULL; cmsgptr = CMSG_NXTHDR(&msg, cmsgptr)) {
- void *c = CMSG_DATA(cmsgptr);
- switch (cmsgptr->cmsg_type) {
- case IP_PKTINFO:
- inp = ((struct in_pktinfo *) c);
- break;
- case IP_TTL:
- ttl = (uint8_t) *((int *) c);
- break;
- default:
- fprintf(stderr, "unknown cmsg %x\n", cmsgptr->cmsg_type);
- return;
- }
- }
- if (!inp)
- return;
- iface = interface_lookup(inp->ipi_ifindex, type);
- if (!iface)
- return;
- if (debug > 1) {
- char buf[256];
- fprintf(stderr, "RX ipv4: %s\n", iface->name);
- fprintf(stderr, " multicast: %d\n", interface_multicast(iface));
- inet_ntop(AF_INET, &from.sin_addr, buf, 256);
- fprintf(stderr, " src %s:%d\n", buf, ntohs(from.sin_port));
- inet_ntop(AF_INET, &inp->ipi_spec_dst, buf, 256);
- fprintf(stderr, " dst %s\n", buf);
- inet_ntop(AF_INET, &inp->ipi_addr, buf, 256);
- fprintf(stderr, " real %s\n", buf);
- fprintf(stderr, " ttl %u\n", ttl);
- }
- for (size_t i = 0; i < iface->addrs.n_addr; i++) {
- if (!interface_valid_src((void *)&iface->addrs.v4[i].addr,
- (void *)&iface->addrs.v4[i].mask,
- (void *) &from.sin_addr, 4)) {
- valid_src = true;
- break;
- }
- }
- if (!valid_src)
- return;
- dns_handle_packet(iface, (struct sockaddr *) &from, ntohs(from.sin_port), buffer, len);
- }
- static void
- read_socket6(struct uloop_fd *u, unsigned int events)
- {
- enum umdns_socket_type type = (enum umdns_socket_type)(u - ufd);
- struct interface *iface;
- static uint8_t buffer[8 * 1024];
- struct iovec iov[1];
- char cmsg6[CMSG_SPACE(sizeof(struct in6_pktinfo)) + CMSG_SPACE(sizeof(int)) + 1];
- struct cmsghdr *cmsgptr;
- struct msghdr msg;
- socklen_t len;
- struct sockaddr_in6 from;
- int flags = 0;
- int ttl = 0;
- struct in6_pktinfo *inp = NULL;
- bool valid_src = false;
- if (u->eof) {
- uloop_end();
- return;
- }
- iov[0].iov_base = buffer;
- iov[0].iov_len = sizeof(buffer);
- memset(&msg, 0, sizeof(msg));
- msg.msg_name = (struct sockaddr *) &from;
- msg.msg_namelen = sizeof(struct sockaddr_in6);
- msg.msg_iov = iov;
- msg.msg_iovlen = 1;
- msg.msg_control = &cmsg6;
- msg.msg_controllen = sizeof(cmsg6);
- len = recvmsg(u->fd, &msg, flags);
- if (len == -1) {
- perror("read failed");
- return;
- }
- for (cmsgptr = CMSG_FIRSTHDR(&msg); cmsgptr != NULL; cmsgptr = CMSG_NXTHDR(&msg, cmsgptr)) {
- void *c = CMSG_DATA(cmsgptr);
- switch (cmsgptr->cmsg_type) {
- case IPV6_PKTINFO:
- inp = ((struct in6_pktinfo *) c);
- break;
- case IPV6_HOPLIMIT:
- ttl = (uint8_t) *((int *) c);
- break;
- default:
- fprintf(stderr, "unknown cmsg %x\n", cmsgptr->cmsg_type);
- return;
- }
- }
- if (!inp)
- return;
- iface = interface_lookup(inp->ipi6_ifindex, type);
- if (!iface)
- return;
- if (debug > 1) {
- char buf[256];
- fprintf(stderr, "RX ipv6: %s\n", iface->name);
- fprintf(stderr, " multicast: %d\n", interface_multicast(iface));
- inet_ntop(AF_INET6, &from.sin6_addr, buf, 256);
- fprintf(stderr, " src %s:%d\n", buf, ntohs(from.sin6_port));
- inet_ntop(AF_INET6, &inp->ipi6_addr, buf, 256);
- fprintf(stderr, " dst %s\n", buf);
- fprintf(stderr, " ttl %u\n", ttl);
- }
- for (size_t i = 0; i < iface->addrs.n_addr; i++) {
- if (!interface_valid_src((void *)&iface->addrs.v6[i].addr,
- (void *)&iface->addrs.v6[i].mask,
- (void *)&from.sin6_addr, 6)) {
- valid_src = true;
- break;
- }
- }
- if (!valid_src)
- return;
- dns_handle_packet(iface, (struct sockaddr *) &from, ntohs(from.sin6_port), buffer, len);
- }
- static int
- interface_mcast_setup4(struct interface *iface)
- {
- struct ip_mreqn mreq;
- struct sockaddr_in sa = {};
- int fd = ufd[SOCK_MC_IPV4].fd;
- sa.sin_family = AF_INET;
- sa.sin_port = htons(MCAST_PORT);
- inet_pton(AF_INET, MCAST_ADDR, &sa.sin_addr);
- memset(&mreq, 0, sizeof(mreq));
- mreq.imr_multiaddr = sa.sin_addr;
- mreq.imr_ifindex = iface->ifindex;
- mreq.imr_address.s_addr = iface->addrs.v4[0].addr.s_addr;
- /* Some network drivers have issues with dropping membership of
- * mcast groups when the iface is down, but don't allow rejoining
- * when it comes back up. This is an ugly workaround
- * -- this was copied from avahi --
- */
- setsockopt(fd, IPPROTO_IP, IP_DROP_MEMBERSHIP, &mreq, sizeof(mreq));
- setsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq));
- return 0;
- }
- static int
- interface_mcast_setup6(struct interface *iface)
- {
- struct ipv6_mreq mreq;
- struct sockaddr_in6 sa = {};
- int fd = ufd[SOCK_MC_IPV6].fd;
- sa.sin6_family = AF_INET6;
- sa.sin6_port = htons(MCAST_PORT);
- inet_pton(AF_INET6, MCAST_ADDR6, &sa.sin6_addr);
- memset(&mreq, 0, sizeof(mreq));
- mreq.ipv6mr_multiaddr = sa.sin6_addr;
- mreq.ipv6mr_interface = iface->ifindex;
- setsockopt(fd, IPPROTO_IPV6, IPV6_LEAVE_GROUP, &mreq, sizeof(mreq));
- setsockopt(fd, IPPROTO_IPV6, IPV6_ADD_MEMBERSHIP, &mreq, sizeof(mreq));
- return 0;
- }
- static void interface_start(struct interface *iface)
- {
- if (iface->type & SOCKTYPE_BIT_UNICAST)
- return;
- if (iface->type & SOCKTYPE_BIT_IPV6)
- interface_mcast_setup6(iface);
- else
- interface_mcast_setup4(iface);
- dns_send_question(iface, NULL, C_DNS_SD, TYPE_PTR, 0);
- announce_init(iface);
- }
- static bool
- iface_equal(struct interface *if_old, struct interface *if_new)
- {
- size_t addr_size;
- if (if_old->ifindex != if_new->ifindex ||
- if_old->addrs.n_addr != if_new->addrs.n_addr)
- return false;
- if (if_old->type & SOCKTYPE_BIT_IPV6)
- addr_size = sizeof(*if_old->addrs.v6);
- else
- addr_size = sizeof(*if_old->addrs.v4);
- addr_size *= if_old->addrs.n_addr;
- if (memcmp(if_old->addrs.v4, if_new->addrs.v4, addr_size) != 0)
- return false;
- return true;
- }
- static void
- iface_update_cb(struct vlist_tree *tree, struct vlist_node *node_new,
- struct vlist_node *node_old)
- {
- struct interface *if_old = container_of_safe(node_old, struct interface, node);
- struct interface *if_new = container_of_safe(node_new, struct interface, node);
- if (if_old && if_new) {
- if (!iface_equal(if_old, if_new))
- cache_cleanup(if_old);
- free(if_old->addrs.v4);
- if_old->addrs = if_new->addrs;
- if_old->ifindex = if_new->ifindex;
- free(if_new);
- return;
- }
- if (if_old)
- interface_free(if_old);
- if (if_new)
- interface_start(if_new);
- }
- static int interface_init_socket(enum umdns_socket_type type)
- {
- struct sockaddr_in6 local6 = {
- .sin6_family = AF_INET6
- };
- struct sockaddr_in local = {
- .sin_family = AF_INET
- };
- uint8_t ttl = 255;
- int ittl = 255;
- int yes = 1;
- int no = 0;
- int fd;
- int af = (type & SOCKTYPE_BIT_IPV6) ? AF_INET6 : AF_INET;
- if (ufd[type].fd >= 0)
- return 0;
- ufd[type].fd = fd = socket(af, SOCK_DGRAM, 0);
- if (fd < 0)
- return -1;
- setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
- #ifdef SO_REUSEPORT
- setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &yes, sizeof(yes));
- #endif
- switch (type) {
- case SOCK_UC_IPV4:
- case SOCK_UC_IPV6:
- break;
- case SOCK_MC_IPV4:
- setsockopt(fd, IPPROTO_IP, IP_MULTICAST_TTL, &ttl, sizeof(ttl));
- setsockopt(fd, IPPROTO_IP, IP_TTL, &ittl, sizeof(ittl));
- setsockopt(fd, IPPROTO_IP, IP_MULTICAST_LOOP, &no, sizeof(no));
- local.sin_port = htons(MCAST_PORT);
- break;
- case SOCK_MC_IPV6:
- setsockopt(fd, IPPROTO_IPV6, IPV6_MULTICAST_HOPS, &ttl, sizeof(ttl));
- setsockopt(fd, IPPROTO_IPV6, IPV6_UNICAST_HOPS, &ttl, sizeof(ttl));
- setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &yes, sizeof(yes));
- setsockopt(fd, IPPROTO_IPV6, IPV6_MULTICAST_LOOP, &no, sizeof(no));
- local6.sin6_port = htons(MCAST_PORT);
- break;
- }
- if (type & SOCKTYPE_BIT_IPV6) {
- ufd[type].cb = read_socket6;
- if (bind(fd, (struct sockaddr *)&local6, sizeof(local6)) < 0)
- goto error;
- setsockopt(fd, IPPROTO_IPV6, IPV6_RECVPKTINFO, &yes, sizeof(yes));
- setsockopt(fd, IPPROTO_IPV6, IPV6_RECVHOPLIMIT, &yes, sizeof(yes));
- } else {
- ufd[type].cb = read_socket4;
- if (bind(fd, (struct sockaddr *)&local, sizeof(local)) < 0)
- goto error;
- setsockopt(fd, IPPROTO_IP, IP_PKTINFO, &yes, sizeof(yes));
- setsockopt(fd, IPPROTO_IP, IP_RECVTTL, &yes, sizeof(yes));
- }
- uloop_fd_add(&ufd[type], ULOOP_READ);
- return 0;
- error:
- close(ufd[type].fd);
- return -1;
- }
- static void
- __interface_add(const char *name, enum umdns_socket_type type,
- struct interface_addr_list *list)
- {
- struct interface *iface;
- unsigned int ifindex;
- char *id_buf;
- if (interface_init_socket(type))
- goto error;
- ifindex = if_nametoindex(name);
- if (!ifindex)
- goto error;
- iface = calloc_a(sizeof(*iface),
- &id_buf, strlen(name) + 3);
- sprintf(id_buf, "%d_%s", type, name);
- iface->name = id_buf + 2;
- iface->ifindex = ifindex;
- iface->type = type;
- iface->addrs = *list;
- vlist_add(&interfaces, &iface->node, id_buf);
- return;
- error:
- free(list->v4);
- }
- int interface_add(const char *name)
- {
- struct ifaddrs *ifap, *ifa;
- struct interface_addr_list addr4 = {}, addr6 = {};
- getifaddrs(&ifap);
- for (ifa = ifap; ifa; ifa = ifa->ifa_next) {
- if (strcmp(ifa->ifa_name, name))
- continue;
- if (ifa->ifa_addr->sa_family == AF_INET) {
- struct sockaddr_in *sin;
- if (cfg_proto && (cfg_proto != 4))
- continue;
- addr4.v4 = realloc(addr4.v4, (addr4.n_addr + 1) * sizeof(*addr4.v4));
- sin = (struct sockaddr_in *) ifa->ifa_addr;
- addr4.v4[addr4.n_addr].addr = sin->sin_addr;
- sin = (struct sockaddr_in *) ifa->ifa_netmask;
- addr4.v4[addr4.n_addr++].mask = sin->sin_addr;
- }
- if (ifa->ifa_addr->sa_family == AF_INET6) {
- uint8_t ll_prefix[] = {0xfe, 0x80 };
- struct sockaddr_in6 *sin6;
- if (cfg_proto && (cfg_proto != 6))
- continue;
- sin6 = (struct sockaddr_in6 *) ifa->ifa_addr;
- if (memcmp(&sin6->sin6_addr, &ll_prefix, 2))
- continue;
- addr6.v6 = realloc(addr6.v6, (addr6.n_addr + 1) * sizeof(*addr6.v6));
- sin6 = (struct sockaddr_in6 *) ifa->ifa_addr;
- addr6.v6[addr6.n_addr].addr = sin6->sin6_addr;
- sin6 = (struct sockaddr_in6 *) ifa->ifa_netmask;
- addr6.v6[addr6.n_addr++].mask = sin6->sin6_addr;
- }
- }
- freeifaddrs(ifap);
- if (addr4.n_addr) {
- size_t addr_size = addr4.n_addr * sizeof(*addr4.v4);
- void *addr_dup = malloc(addr_size);
- memcpy(addr_dup, addr4.v4, addr_size);
- __interface_add(name, SOCK_UC_IPV4, &addr4);
- addr4.v4 = addr_dup;
- __interface_add(name, SOCK_MC_IPV4, &addr4);
- }
- if (addr6.n_addr) {
- size_t addr_size = addr6.n_addr * sizeof(*addr6.v6);
- void *addr_dup = malloc(addr_size);
- memcpy(addr_dup, addr6.v6, addr_size);
- __interface_add(name, SOCK_UC_IPV6, &addr6);
- addr6.v6 = addr_dup;
- __interface_add(name, SOCK_MC_IPV6, &addr6);
- }
- return !addr4.n_addr && !addr6.n_addr;
- }
- void interface_shutdown(void)
- {
- struct interface *iface;
- vlist_for_each_element(&interfaces, iface, node)
- if (interface_multicast(iface)) {
- dns_reply_a(iface, NULL, 0);
- service_announce_services(iface, NULL, 0);
- }
- for (size_t i = 0; i < ARRAY_SIZE(ufd); i++) {
- uloop_fd_delete(&ufd[i]);
- close(ufd[i].fd);
- ufd[i].fd = -1;
- }
- }
- struct interface *interface_get(const char *name, enum umdns_socket_type type)
- {
- char id_buf[32];
- snprintf(id_buf, sizeof(id_buf), "%d_%s", type, name);
- struct interface *iface = vlist_find(&interfaces, id_buf, iface, node);
- return iface;
- }
- VLIST_TREE(interfaces, avl_strcmp, iface_update_cb, true, false);
|