Hello tech@, I have been developing a new daemon for OpenBSD that fills in a gap in the multicast protocol support for network edges. More specifically I'm talking about a multicast proxy. I'm sending this e-mail to share the daemon code and see if there is interest in such.
The mcast-proxy is a less featured multicast routing daemon that is mostly used on equipments that face client networks (end users). It is mainly used when you don't need a full multicast routing daemon (like dvmrpd, mrouted or pim), but you want to use your networks resources efficiently. This implementation has the following features: * Support IPv4 (IGMPv1/v2) multicast proxy * Support IPv6 (MLDv1) multicast proxy * Privilege dropping (runs as user) * chroot jailing The development of this daemon brought improvements to the IPv6 multicast stack, like: * Initial MP support Now IPv6 multicast routing code uses the art routing table to store the multicast routes. This also means you can see your multicast routes in route(8). * Support multiple rdomains The interfaces mif (multicast interface) are now domain specific, so you can have mif ids duplicated on different rdomains. * Fixed a few problems in MLD code that prevented some client/server functionality Note: the daemon is not yet pledge()d as there is no support for MRT(6)_* setsockopt() calls. Note 2: IPv6 multicast proxy requires an OpenBSD -current, because of the recent kernel changes and netstat(8). --- To run multicast routing protocols in your machines you have to configure the following settings: * Allow multicast routing: # rcctl enable multicast * (IPv4 only) allow IGMP packets. To allow IP options you have to configure your PF traffic pass rule to accept IP options. Example: change 'pass' to 'pass allow-opts'. * Add a multicast route (if the default doesn't exist or is not correct) IPv4: route add 224/8 192.168.0.1 IPv6: route add ff00::/8 fe80::fce1:baff:fed0:2001%vio1 * In case you are using the default route for multicast you might need to specify an alternate multicast source. By default mcast-proxy only accepts multicast traffic from the same network of your interface. Example: em0 has IPv6 address: 2001:db8::100, but the multicast traffic comes from 2001:db9::10. The mcast-proxy.conf: ... interface em0 { source 2001:db9::/64 upstream } ... The same applies for IPv4. --- How to build it: * Save this e-mail (e.g. /tmp/mail) * Create a new directory (e.g. mkdir /tmp/mcast-proxy) * Apply the diff in this email (e.g. cd/tmp/mcast-proxy; patch -p0 -i /tmp/mail) * Build it (e.g. cd /tmp/mcast-proxy; make obj; make) * Run it (e.g. /tmp/mcast-proxy/obj/mcast-proxy) Reading the man pages: * The daemon man page: cd /tmp/mcast-proxy; mandoc mcast-proxy.8 | less * The configuration man page: cd /tmp/mcast-proxy; mandoc mcast-proxy.conf.5 | less --- The daemon code is split in the following file hierarchy: * mcast-proxy.c: all IGMP/MLD related packet parsing * mrt.c: the multicast routing table on userland * kroute.c: all kernel interactions * util.c: misc functions that did not fit the other files Here is the daemon code: diff --git Makefile Makefile new file mode 100644 index 0000000..d99eaed --- /dev/null +++ Makefile @@ -0,0 +1,14 @@ +# $OpenBSD:$ + +SRCS = mcast-proxy.c kroute.c log.c mrt.c parse.y util.c +PROG = mcast-proxy +MAN = mcast-proxy.8 mcast-proxy.conf.5 + +CFLAGS += -I${.CURDIR} +CFLAGS += -Wall -Wextra -Wshadow +CFLAGS += -Wmissing-prototypes -Wmissing-declarations +CFLAGS += -Wstrict-prototypes -Wpointer-arith -Wsign-compare +DPADD = ${LIBEVENT} +LDADD = -levent + +.include <bsd.prog.mk> diff --git kroute.c kroute.c new file mode 100644 index 0000000..af32091 --- /dev/null +++ kroute.c @@ -0,0 +1,1251 @@ +/* $OpenBSD:$ */ + +/* + * Copyright (c) 2017 Rafael Zalamena <rzalam...@openbsd.org> + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include <sys/param.h> +#include <sys/sysctl.h> +#include <sys/socket.h> + +#include <net/if_dl.h> +#include <net/route.h> +#include <netinet/in.h> +#include <netinet/ip_mroute.h> +#include <netinet6/ip6_mroute.h> + +#include <errno.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include "mcast-proxy.h" + +#define MAX_RTSOCK_BUF (128 * 1024) + +int bad_addr_v4(struct in_addr); +int bad_addr_v6(struct in6_addr *); +int iacmp(struct intf_addr *, struct intf_addr *); + +int vif4_nextvidx(void); +int vif6_nextvidx(void); + +void if_announce(struct if_announcemsghdr *); +void if_update(unsigned short, int, struct if_data *, + struct sockaddr_dl *sdl); +void if_newaddr(unsigned short, struct sockaddr *, struct sockaddr *); +void if_deladdr(unsigned short, struct sockaddr *, struct sockaddr *); +void get_rtaddrs(int, struct sockaddr *, struct sockaddr **); +void rtmsg_process(const uint8_t *, size_t); + +struct in6_addr in6_allrouters = IN6ADDR_LINKLOCAL_ALLROUTERS_INIT; + +int vindex; +int vindex6; +int rtsd_rcvbuf; + +void +assert_mcastforward(void) +{ + int mforward = 0; + size_t mforwardlen = sizeof(mforward); + int mib[4]; + + if (!ic.ic_ipv4) + goto skip_v4mforwarding; + + mib[0] = CTL_NET; + mib[1] = PF_INET; + mib[2] = IPPROTO_IP; + mib[3] = IPCTL_MFORWARDING; + if (sysctl(mib, nitems(mib), &mforward, &mforwardlen, NULL, 0) == -1) + fatal("sysctl IPv4 IPCTL_MFORWARDING"); + + if (!mforward) + fatalx("%s: IPv4 multicast forwarding is disabled", + __func__); + + skip_v4mforwarding: + if (!ic.ic_ipv6) + return; + + mib[0] = CTL_NET; + mib[1] = PF_INET6; + mib[2] = IPPROTO_IPV6; + mib[3] = IPV6CTL_MFORWARDING; + if (sysctl(mib, nitems(mib), &mforward, &mforwardlen, NULL, 0) == -1) + fatal("sysctl IPv6 IPCTL_MFORWARDING"); + + if (!mforward) + fatalx("%s: IPv6 multicast forwarding is disabled", + __func__); +} + +int +open_igmp_socket(void) +{ + int sd, v; + uint8_t ttl = 1, loop = 0; + + sd = socket(AF_INET, SOCK_RAW | SOCK_CLOEXEC, IPPROTO_IGMP); + if (sd == -1) { + log_warn("%s: socket", __func__); + return -1; + } + + /* Initialize the multicast routing socket. */ + v = 1; + if (setsockopt(sd, IPPROTO_IP, MRT_INIT, &v, sizeof(v)) == -1) { + log_warn("%s: setsockopt MRT_INIT", __func__); + close(sd); + return -1; + } + + /* Include IP header on packets. */ + v = 1; + if (setsockopt(sd, IPPROTO_IP, IP_HDRINCL, &v, sizeof(v)) == -1) { + log_warn("%s: setsockopt IP_HDRINCL", __func__); + close(sd); + return -1; + } + + /* Use TTL of 1 to send multicast packets. */ + if (setsockopt(sd, IPPROTO_IP, IP_MULTICAST_TTL, &ttl, + sizeof(ttl)) == -1) { + log_warn("%s: setsockopt IP_MULTICAST_TTL", __func__); + close(sd); + return -1; + } + + /* Don't send multicast packets to loopback. */ + if (setsockopt(sd, IPPROTO_IP, IP_MULTICAST_LOOP, &loop, + sizeof(loop)) == -1) { + log_warn("%s: setsockopt IP_MULTICAST_LOOP", __func__); + close(sd); + return -1; + } + + return sd; +} + +int +close_igmp_socket(int sd) +{ + if (sd == -1) + return 0; + + if (setsockopt(sd, IPPROTO_IP, MRT_DONE, NULL, 0) == -1) { + log_warn("%s: setsockopt MRT_DONE", __func__); + return -1; + } + + if (close(sd) == -1) { + log_warn("%s: close", __func__); + return -1; + } + + return 0; +} + +int +open_mld_socket(void) +{ + int sd, v; + unsigned int ttl = 1; + + sd = socket(AF_INET6, SOCK_RAW | SOCK_CLOEXEC, IPPROTO_ICMPV6); + if (sd == -1) { + log_warn("%s: socket", __func__); + return -1; + } + + /* Initialize the multicast routing socket. */ + v = 1; + if (setsockopt(sd, IPPROTO_IPV6, MRT6_INIT, &v, sizeof(v)) == -1) { + log_warn("%s: setsockopt MRT6_INIT", __func__); + close(sd); + return -1; + } + + /* Include IP header on packets. */ + v = 1; + if (setsockopt(sd, IPPROTO_IPV6, IPV6_RECVPKTINFO, &v, + sizeof(v)) == -1) { + log_warn("%s: setsockopt IPV6_RECVPKTINFO", __func__); + close(sd); + return -1; + } + + /* Use TTL of 1 to send multicast packets. */ + if (setsockopt(sd, IPPROTO_IPV6, IPV6_MULTICAST_HOPS, &ttl, + sizeof(ttl)) == -1) { + log_warn("%s: setsockopt IPV6_MULTICAST_HOPS", __func__); + close(sd); + return -1; + } + + return sd; +} + +int +close_mld_socket(int sd) +{ + if (sd == -1) + return 0; + + if (setsockopt(sd, IPPROTO_IPV6, MRT6_DONE, NULL, 0) == -1) { + log_warn("%s: setsockopt MRT6_DONE", __func__); + return -1; + } + + if (close(sd) == -1) { + log_warn("%s: close", __func__); + return -1; + } + + return 0; +} + +int +igmp_setif(struct intf_data *id) +{ + struct intf_addr *ia; + struct in_addr any; + + if (id == NULL) { + memset(&any, 0, sizeof(any)); + if (setsockopt(igmpsd, IPPROTO_IP, IP_MULTICAST_IF, + &any, sizeof(any)) == -1) { + log_warn("%s: setsockopt IP_MULTICAST_IF default", + __func__); + return -1; + } + return 0; + } + + ia = intf_primaryv4(id); + if (ia == NULL) + return -1; + + if (setsockopt(igmpsd, IPPROTO_IP, IP_MULTICAST_IF, + &ia->ia_addr.v4, sizeof(ia->ia_addr.v4)) == -1) { + log_warn("%s: setsockopt IP_MULTICAST_IF %s", + __func__, id->id_name); + return -1; + } + + return 0; +} + +int +vif_register(struct intf_data *id) +{ + int error = 0; + + if (id->id_vindex == INVALID_VINDEX) + error |= vif4_register(id); + if (id->id_vindex6 == INVALID_VINDEX) + error |= vif6_register(id); + + return error; +} + +int +vif_unregister(struct intf_data *id) +{ + int error = 0; + + if (id->id_vindex != INVALID_VINDEX) + error |= vif4_unregister(id); + if (id->id_vindex != INVALID_VINDEX) + error |= vif6_unregister(id); + + return error; +} + +int +vif4_nextvidx(void) +{ + struct intf_data *id; + int vidx; + + for (vidx = 0; vidx < MAXMIFS; vidx++) { + SLIST_FOREACH(id, &iflist, id_entry) { + if (vidx == id->id_vindex) + break; + } + if (id != NULL) + continue; + + return vidx; + } + + return -1; +} + +int +vif4_register(struct intf_data *id) +{ + struct intf_addr *ia; + struct vifctl vifc; + int vidx; + + /* Don't allow registration if not selected. */ + if (!id->id_mv4) + return 0; + + /* Already registered. */ + if (id->id_vindex != INVALID_VINDEX) + return 0; + + ia = intf_primaryv4(id); + if (ia == NULL) + return -1; + + memset(&vifc, 0, sizeof(vifc)); + vifc.vifc_flags = 0; + vifc.vifc_threshold = id->id_ttl; + vifc.vifc_rate_limit = 0; + vifc.vifc_lcl_addr = ia->ia_addr.v4; + vifc.vifc_rmt_addr.s_addr = INADDR_ANY; + + vidx = vif4_nextvidx(); + if (vidx == -1) { + log_warnx("%s: no more virtual interfaces available", + __func__); + return -1; + } + + vifc.vifc_vifi = id->id_vindex = vidx; + log_debug("%s: %s (vindex %d) threshold %d rate %d address %s", + __func__, id->id_name, id->id_vindex, id->id_ttl, 0, + addr4tostr(&ia->ia_addr.v4)); + + if (setsockopt(igmpsd, IPPROTO_IP, MRT_ADD_VIF, &vifc, + sizeof(vifc)) == -1) { + id->id_vindex = INVALID_VINDEX; + log_warn("%s: setsockopt MRT_ADD_VIF", __func__); + return -1; + } + + return 0; +} + +int +vif4_unregister(struct intf_data *id) +{ + struct intf_addr *ia; + struct vifctl vifc; + + /* Don't allow registration if not selected. */ + if (!id->id_mv4) + return 0; + + /* Already unregistered. */ + if (id->id_vindex == INVALID_VINDEX) + return 0; + + ia = intf_primaryv4(id); + if (ia == NULL) + return -1; + + memset(&vifc, 0, sizeof(vifc)); + vifc.vifc_flags = 0; + vifc.vifc_vifi = id->id_vindex; + vifc.vifc_threshold = id->id_ttl; + vifc.vifc_rate_limit = 0; + vifc.vifc_lcl_addr = ia->ia_addr.v4; + vifc.vifc_rmt_addr.s_addr = INADDR_ANY; + + log_debug("%s: %s (%d) threshold %d rate %d address %s", + __func__, id->id_name, id->id_vindex, id->id_ttl, 0, + addr4tostr(&ia->ia_addr.v4)); + + if (setsockopt(igmpsd, IPPROTO_IP, MRT_DEL_VIF, &vifc, + sizeof(vifc)) == -1) { + log_warn("%s: setsockopt MRT_DEL_VIF", __func__); + return -1; + } + + id->id_vindex = INVALID_VINDEX; + + return 0; +} + +int +vif6_nextvidx(void) +{ + struct intf_data *id; + int vidx; + + for (vidx = 0; vidx < MAXMIFS; vidx++) { + SLIST_FOREACH(id, &iflist, id_entry) { + if (vidx == id->id_vindex6) + break; + } + if (id != NULL) + continue; + + return vidx; + } + + return -1; +} + +int +vif6_register(struct intf_data *id) +{ + struct mif6ctl mif6c; + int vidx; + + /* Don't allow registration if not selected. */ + if (!id->id_mv6) + return 0; + + /* Already registered. */ + if (id->id_vindex6 != INVALID_VINDEX) + return 0; + + memset(&mif6c, 0, sizeof(mif6c)); + mif6c.mif6c_pifi = id->id_index; + + vidx = vif6_nextvidx(); + if (vidx == -1) { + log_warnx("%s: no more virtual interfaces available", + __func__); + return -1; + } + + id->id_vindex6 = mif6c.mif6c_mifi = vidx; + log_debug("%s: %s (vindex %d) rate %d", + __func__, id->id_name, id->id_vindex6, 0); + + if (setsockopt(mldsd, IPPROTO_IPV6, MRT6_ADD_MIF, &mif6c, + sizeof(mif6c)) == -1) { + id->id_vindex6 = INVALID_VINDEX; + log_warn("%s: setsockopt MRT6_ADD_MIF", __func__); + return -1; + } + + return 0; +} + +int +vif6_unregister(struct intf_data *id) +{ + struct mif6ctl mif6c; + + /* Don't allow registration if not selected. */ + if (!id->id_mv6) + return 0; + + /* Already unregistered. */ + if (id->id_vindex6 == INVALID_VINDEX) + return 0; + + memset(&mif6c, 0, sizeof(mif6c)); + mif6c.mif6c_pifi = id->id_index; + + log_debug("%s: %s (vindex %d) rate %d", + __func__, id->id_name, id->id_vindex6, 0); + + if (setsockopt(mldsd, IPPROTO_IPV6, MRT6_DEL_MIF, &mif6c, + sizeof(mif6c)) == -1) { + log_warn("%s: setsockopt MRT6_DEL_MIF", __func__); + return -1; + } + + id->id_vindex6 = INVALID_VINDEX; + + return 0; +} + +int +mcast_join(struct intf_data *id, struct sockaddr_storage *ss) +{ + int error = 0; + + if (ss == NULL) { + error |= mcast4_join(id, NULL); + error |= mcast6_join(id, NULL); + } else { + switch (ss->ss_family) { + case AF_INET: + error = mcast4_join(id, &sstosin(ss)->sin_addr); + break; + case AF_INET6: + error = mcast6_join(id, &sstosin6(ss)->sin6_addr); + break; + + default: + log_debug("%s: invalid protocol %d", + __func__, ss->ss_family); + error = -1; + } + } + + return error; +} + +int +mcast_leave(struct intf_data *id, struct sockaddr_storage *ss) +{ + int error = 0; + + if (ss == NULL) { + error |= mcast4_leave(id, NULL); + error |= mcast6_leave(id, NULL); + } else { + switch (ss->ss_family) { + case AF_INET: + error = mcast4_leave(id, &sstosin(ss)->sin_addr); + break; + case AF_INET6: + error = mcast6_leave(id, &sstosin6(ss)->sin6_addr); + break; + + default: + log_debug("%s: invalid protocol %d", + __func__, ss->ss_family); + error = -1; + } + } + + return error; +} + +int +mcast4_join(struct intf_data *id, struct in_addr *in) +{ + struct intf_addr *ia; + struct ip_mreq imr; + + /* IPv4 is disabled in this interface. */ + if (!id->id_mv4) + return 0; + + ia = intf_primaryv4(id); + if (ia == NULL) + return -1; + + if (in == NULL) + log_debug("%s: %s (%d) address %s group all_routers", + __func__, id->id_name, id->id_vindex, + addr4tostr(&ia->ia_addr.v4)); + else + log_debug("%s: %s (%d) address %s group %s", + __func__, id->id_name, id->id_vindex, + addr4tostr(&ia->ia_addr.v4), addr4tostr(in)); + + imr.imr_multiaddr.s_addr = (in == NULL) ? + htonl(INADDR_ALLROUTERS_GROUP) : in->s_addr; + imr.imr_interface = ia->ia_addr.v4; + if (setsockopt(igmpsd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &imr, + sizeof(imr)) == -1) { + log_debug("%s: setsockopt IP_ADD_MEMBERSHIP: %s", + __func__, strerror(errno)); + return -1; + } + + return 0; +} + +int +mcast4_leave(struct intf_data *id, struct in_addr *in) +{ + struct intf_addr *ia; + struct ip_mreq imr; + + /* IPv4 is disabled in this interface. */ + if (!id->id_mv4) + return 0; + + ia = intf_primaryv4(id); + if (ia == NULL) + return -1; + + if (in == NULL) + log_debug("%s: %s (%d) address %s group all_routers", + __func__, id->id_name, id->id_vindex, + addr4tostr(&ia->ia_addr.v4)); + else + log_debug("%s: %s (%d) address %s group %s", + __func__, id->id_name, id->id_vindex, + addr4tostr(&ia->ia_addr.v4), addr4tostr(in)); + + imr.imr_multiaddr.s_addr = (in == NULL) ? + htonl(INADDR_ALLROUTERS_GROUP) : in->s_addr; + imr.imr_interface = ia->ia_addr.v4; + if (setsockopt(igmpsd, IPPROTO_IP, IP_DROP_MEMBERSHIP, &imr, + sizeof(imr)) == -1) { + log_debug("%s: setsockopt IP_DROP_MEMBERSHIP: %s", + __func__, strerror(errno)); + return -1; + } + + return 0; +} + +int +mcast6_join(struct intf_data *id, struct in6_addr *in6) +{ + struct ipv6_mreq ipv6mr; + + /* IPv6 is disabled in this interface. */ + if (!id->id_mv6) + return 0; + + if (in6 == NULL) + log_debug("%s: %s (%d) group all_routers", + __func__, id->id_name, id->id_vindex6); + else + log_debug("%s: %s (%d) group %s", + __func__, id->id_name, id->id_vindex6, addr6tostr(in6)); + + ipv6mr.ipv6mr_multiaddr = (in6 == NULL) ? in6_allrouters : *in6; + ipv6mr.ipv6mr_interface = id->id_index; + if (setsockopt(mldsd, IPPROTO_IPV6, IPV6_JOIN_GROUP, &ipv6mr, + sizeof(ipv6mr)) == -1) { + log_debug("%s: setsockopt IPV6_JOIN_GROUP: %s", + __func__, strerror(errno)); + return -1; + } + + return 0; +} + +int +mcast6_leave(struct intf_data *id, struct in6_addr *in6) +{ + struct ipv6_mreq ipv6mr; + + /* IPv6 is disabled in this interface. */ + if (!id->id_mv6) + return 0; + + if (in6 == NULL) + log_debug("%s: %s (%d) group all_routers", + __func__, id->id_name, id->id_vindex6); + else + log_debug("%s: %s (%d) group %s", + __func__, id->id_name, id->id_vindex6, addr6tostr(in6)); + + ipv6mr.ipv6mr_multiaddr = (in6 == NULL) ? in6_allrouters : *in6; + ipv6mr.ipv6mr_interface = id->id_index; + if (setsockopt(mldsd, IPPROTO_IPV6, IPV6_LEAVE_GROUP, &ipv6mr, + sizeof(ipv6mr)) == -1) { + log_warn("%s: setsockopt IPV6_LEAVE_GROUP: %s", + __func__, strerror(errno)); + return -1; + } + + return 0; +} + +int +mcast_addroute(unsigned short pvidx, union uaddr *origin, + union uaddr *group, struct molist *molist) +{ + struct intf_data *id; + struct multicast_origin *mo; + struct mfcctl mfcc; + unsigned short vidx; + + memset(&mfcc, 0, sizeof(mfcc)); + mfcc.mfcc_origin = origin->v4; + mfcc.mfcc_mcastgrp = group->v4; + mfcc.mfcc_parent = pvidx; + LIST_FOREACH(mo, molist, mo_entry) { + id = mo->mo_id; + + /* Don't set upstream interface TTL. */ + if (id == upstreamif) + continue; + + vidx = id->id_vindex; + if (vidx > MAXVIFS) + continue; + + mfcc.mfcc_ttls[vidx] = id->id_ttl; + } + + log_debug("%s: add route origin %s group %s parent %d", + __func__, addr4tostr(&origin->v4), addr4tostr(&group->v4), + pvidx); + + LIST_FOREACH(mo, molist, mo_entry) { + id = mo->mo_id; + vidx = id->id_vindex; + if (vidx > MAXVIFS) + continue; + + if (mfcc.mfcc_ttls[vidx]) + log_debug(" vif %s (%d) ttl %d", + id->id_name, vidx, mfcc.mfcc_ttls[vidx]); + else + log_debug(" vif %s (%d) disabled", + id->id_name, vidx); + } + + + if (setsockopt(igmpsd, IPPROTO_IP, MRT_ADD_MFC, &mfcc, + sizeof(mfcc)) == -1) { + log_warn("%s: setsockopt MRT_ADD_MFC", __func__); + return -1; + } + + return 0; +} + +int +mcast_addroute6(unsigned short pvidx, union uaddr *origin, + union uaddr *group, struct molist *molist) +{ + struct intf_data *id; + struct multicast_origin *mo; + struct mf6cctl mf6cc; + unsigned short vidx; + + memset(&mf6cc, 0, sizeof(mf6cc)); + mf6cc.mf6cc_parent = pvidx; + mf6cc.mf6cc_origin.sin6_family = AF_INET6; + mf6cc.mf6cc_origin.sin6_addr = origin->v6; + mf6cc.mf6cc_origin.sin6_len = sizeof(mf6cc.mf6cc_origin); + mf6cc.mf6cc_mcastgrp.sin6_family = AF_INET6; + mf6cc.mf6cc_mcastgrp.sin6_addr = group->v6; + mf6cc.mf6cc_mcastgrp.sin6_len = sizeof(mf6cc.mf6cc_mcastgrp); + LIST_FOREACH(mo, molist, mo_entry) { + id = mo->mo_id; + + /* Don't set upstream interface. */ + if (id == upstreamif) + continue; + + vidx = id->id_vindex6; + if (vidx > MAXMIFS) + continue; + + IF_SET(vidx, &mf6cc.mf6cc_ifset); + } + + log_debug("%s: add route origin %s group %s parent %d", + __func__, addr6tostr(&origin->v6), addr6tostr(&group->v6), + pvidx); + + LIST_FOREACH(mo, molist, mo_entry) { + id = mo->mo_id; + vidx = id->id_vindex6; + if (vidx > MAXMIFS) + continue; + + if (IF_ISSET(vidx, &mf6cc.mf6cc_ifset)) + log_debug(" mif %s (%d)", + id->id_name, vidx); + else + log_debug(" mif %s (%d) disabled", + id->id_name, vidx); + } + + + if (setsockopt(mldsd, IPPROTO_IPV6, MRT6_ADD_MFC, &mf6cc, + sizeof(mf6cc)) == -1) { + log_warn("%s: setsockopt MRT6_ADD_MFC", __func__); + return -1; + } + + return 0; +} + +int +mcast_delroute(unsigned short pvidx, union uaddr *origin, + union uaddr *group) +{ + struct mfcctl mfcc; + + memset(&mfcc, 0, sizeof(mfcc)); + mfcc.mfcc_origin = origin->v4; + mfcc.mfcc_mcastgrp = group->v4; + mfcc.mfcc_parent = pvidx; + + log_debug("%s: del route origin %s group %s parent %d", + __func__, addr4tostr(&origin->v4), addr4tostr(&group->v4), + pvidx); + + if (setsockopt(igmpsd, IPPROTO_IP, MRT_DEL_MFC, &mfcc, + sizeof(mfcc)) == -1) { + log_warn("%s: setsockopt MRT_DEL_MFC", __func__); + return -1; + } + + return 0; +} + +int +mcast_delroute6(unsigned short pvidx, union uaddr *origin, + union uaddr *group) +{ + struct mf6cctl mf6cc; + + memset(&mf6cc, 0, sizeof(mf6cc)); + mf6cc.mf6cc_parent = pvidx; + mf6cc.mf6cc_origin.sin6_addr = origin->v6; + mf6cc.mf6cc_mcastgrp.sin6_addr = group->v6; + + log_debug("%s: del route origin %s group %s parent %d", + __func__, addr6tostr(&origin->v6), addr6tostr(&group->v6), + pvidx); + + if (setsockopt(mldsd, IPPROTO_IPV6, MRT6_DEL_MFC, &mf6cc, + sizeof(mf6cc)) == -1) { + log_warn("%s: setsockopt MRT_DEL6_MFC", __func__); + return -1; + } + + return 0; +} + +void +intf_dispatch(int sd, __unused short ev, __unused void *arg) +{ + ssize_t n; + static uint8_t *buf; + + if (buf == NULL) { + buf = malloc(rtsd_rcvbuf); + if (buf == NULL) + fatal("%s: malloc", __func__); + } + + n = read(sd, buf, rtsd_rcvbuf); + if (n == -1) { + if (errno == EAGAIN || errno == EWOULDBLOCK || + errno == EINTR) + return; + + log_warn("%s: read", __func__); + return; + } + if (n == 0) + fatalx("%s: routing socket closed", __func__); + + rtmsg_process(buf, n); +} + +int +intf_init(void) +{ + size_t len; + int mib[6]; + uint8_t *buf; + int sd, opt, rcvbuf, defrcvbuf; + socklen_t optlen; + + mib[0] = CTL_NET; + mib[1] = PF_ROUTE; + mib[2] = 0; + mib[3] = 0; /* wildcard */ + mib[4] = NET_RT_IFLIST; + mib[5] = 0; + + if (sysctl(mib, 6, NULL, &len, NULL, 0) == -1) + fatal("%s: sysctl", __func__); + if ((buf = malloc(len)) == NULL) + fatal("%s: malloc", __func__); + if (sysctl(mib, 6, buf, &len, NULL, 0) == -1) { + free(buf); + fatal("%s: sysctl", __func__); + } + + rtmsg_process(buf, len); + free(buf); + + sd = socket(AF_ROUTE, SOCK_RAW | SOCK_CLOEXEC | SOCK_NONBLOCK, 0); + if (sd == -1) + fatal("%s: socket", __func__); + + opt = 0; + if (setsockopt(sd, SOL_SOCKET, SO_USELOOPBACK, + &opt, sizeof(opt)) == -1) + fatal("%s: setsockopt SO_USELOOPBACK", __func__); + + /* Increase the receive buffer. */ + rcvbuf = MAX_RTSOCK_BUF; + optlen = sizeof(rcvbuf); + if (getsockopt(sd, SOL_SOCKET, SO_RCVBUF, + &defrcvbuf, &optlen) == -1) + log_warn("%s: getsockopt SO_RCVBUF", __func__); + else + for (; rcvbuf > defrcvbuf && + setsockopt(sd, SOL_SOCKET, SO_RCVBUF, + &rcvbuf, sizeof(rcvbuf)) == -1 && errno == ENOBUFS; + rcvbuf /= 2) + continue; + + rtsd_rcvbuf = rcvbuf; + + return (sd); +} + +void +if_announce(struct if_announcemsghdr *ifan) +{ + struct intf_data *id; + + if (ifan->ifan_what == IFAN_DEPARTURE) { + log_debug("%s departure: %s", __func__, ifan->ifan_name); + + id = intf_lookupbyname(ifan->ifan_name); + if (id == NULL) + return; + + id->id_enabled = 0; + id->id_vindex = INVALID_VINDEX; + id->id_vindex6 = INVALID_VINDEX; + return; + } else + log_debug("%s arrival: %s", __func__, ifan->ifan_name); + + id = intf_lookupbyname(ifan->ifan_name); + if (id == NULL) { + id = id_insert(ifan->ifan_index); + if (id == NULL) + return; + } + + id->id_index = ifan->ifan_index; + strlcpy(id->id_name, ifan->ifan_name, sizeof(id->id_name)); +} + +void +if_update(unsigned short ifindex, int flags, struct if_data *ifd, + struct sockaddr_dl *sdl) +{ + struct intf_data *id; + size_t sdllen = 0; + char ifname[IFNAMSIZ]; + + /* Don't install loopback interfaces. */ + if ((flags & IFF_LOOPBACK) == IFF_LOOPBACK) + return; + /* Don't install non multicast interfaces. */ + if ((flags & IFF_MULTICAST) != IFF_MULTICAST) + return; + + /* Check for sdl and copy interface name. */ + if (sdl == NULL || sdl->sdl_family != AF_LINK) + goto insert_interface; + + sdllen = (sdl->sdl_nlen >= sizeof(id->id_name)) ? + (sizeof(id->id_name) - 1) : sdl->sdl_nlen; + + memcpy(ifname, sdl->sdl_data, sdllen); + ifname[sdllen] = 0; + + log_debug("%s: if %s (%d)", __func__, ifname, ifindex); + + id = intf_lookupbyname(ifname); + if (id == NULL) { + insert_interface: + id = id_insert(ifindex); + if (id == NULL) + return; + } + + id->id_enabled = (flags & IFF_UP) && + LINK_STATE_IS_UP(ifd->ifi_link_state); + id->id_index = ifindex; + id->id_flags = flags; + id->id_rdomain = ifd->ifi_rdomain; + if (sdllen > 0) + strlcpy(id->id_name, ifname, sizeof(id->id_name)); +} + +int +bad_addr_v4(struct in_addr addr) +{ + uint32_t a = ntohl(addr.s_addr); + + if (((a >> IN_CLASSA_NSHIFT) == 0) || + ((a >> IN_CLASSA_NSHIFT) == IN_LOOPBACKNET) || + IN_MULTICAST(a) || IN_BADCLASS(a)) + return (1); + + return (0); +} + +int +bad_addr_v6(struct in6_addr *addr) +{ + if (IN6_IS_ADDR_UNSPECIFIED(addr) || + IN6_IS_ADDR_LOOPBACK(addr) || + IN6_IS_ADDR_MULTICAST(addr) || + IN6_IS_ADDR_SITELOCAL(addr) || + IN6_IS_ADDR_V4MAPPED(addr) || + IN6_IS_ADDR_V4COMPAT(addr)) + return (1); + + return (0); +} + +void +if_newaddr(unsigned short ifindex, struct sockaddr *ifa, struct sockaddr *mask) +{ + struct intf_data *id; + struct intf_addr *ia; + struct sockaddr_in *ifa4, *mask4; + struct sockaddr_in6 *ifa6, *mask6; + int newaddr; + + if (ifa == NULL) + return; + + id = intf_lookupbyindex(ifindex); + if (id == NULL) { + log_debug("%s: corresponding if %d not found", + __func__, ifindex); + return; + } + + switch (ifa->sa_family) { + case AF_INET: + ifa4 = (struct sockaddr_in *) ifa; + mask4 = (struct sockaddr_in *) mask; + + /* filter out unwanted addresses */ + if (bad_addr_v4(ifa4->sin_addr)) + return; + + ia = calloc(1, sizeof(*ia)); + if (ia == NULL) + fatal("%s: calloc", __func__); + + ia->ia_addr.v4 = ifa4->sin_addr; + if (mask4) + ia->ia_prefixlen = + mask2prefixlen(mask4->sin_addr.s_addr); + + log_debug("%s: if %s (%d): %s (prefixlen %d)", + __func__, id->id_name, id->id_index, + addr4tostr(&ifa4->sin_addr), ia->ia_prefixlen); + break; + case AF_INET6: + ifa6 = (struct sockaddr_in6 *) ifa; + mask6 = (struct sockaddr_in6 *) mask; + + /* We only care about link-local and global-scope. */ + if (bad_addr_v6(&ifa6->sin6_addr)) + return; + + ia = calloc(1, sizeof(*ia)); + if (ia == NULL) + fatal("%s: calloc", __func__); + + ia->ia_addr.v6 = ifa6->sin6_addr; + if (mask6) + ia->ia_prefixlen = mask2prefixlen6(mask6); + + log_debug("%s: if %s (%d): %s (prefixlen %d)", + __func__, id->id_name, id->id_index, + addr6tostr(&ifa6->sin6_addr), ia->ia_prefixlen); + break; + default: + return; + } + + newaddr = (intf_primaryv4(id) == NULL); + + ia->ia_af = ifa->sa_family; + ia_inserttail(&id->id_ialist, ia); + + /* + * Register interface if it is a new primary address in a + * enabled interface. + */ + if (newaddr && id->id_dir != IDIR_DISABLE) { + vif_register(id); + if (id->id_dir == IDIR_DOWNSTREAM) + mcast_join(id, NULL); + } +} + +int +iacmp(struct intf_addr *ia, struct intf_addr *ian) +{ + if (ia->ia_af > ian->ia_af) + return -1; + + return memcmp(&ia->ia_addr, &ian->ia_addr, (ia->ia_af == AF_INET) ? + sizeof(ia->ia_addr.v4) : sizeof(ia->ia_addr.v6)); +} + +void +if_deladdr(unsigned short ifindex, struct sockaddr *ifa, struct sockaddr *mask) +{ + struct intf_data *id; + struct intf_addr iac, *ia; + struct sockaddr_in *ifa4, *mask4; + struct sockaddr_in6 *ifa6, *mask6; + int regagain = 0; + + if (ifa == NULL) + return; + + id = intf_lookupbyindex(ifindex); + if (id == NULL) { + log_debug("%s: corresponding if %d not found", + __func__, ifindex); + return; + } + + memset(&iac, 0, sizeof(iac)); + iac.ia_af = ifa->sa_family; + switch (ifa->sa_family) { + case AF_INET: + ifa4 = (struct sockaddr_in *) ifa; + mask4 = (struct sockaddr_in *) mask; + + /* filter out unwanted addresses */ + if (bad_addr_v4(ifa4->sin_addr)) + return; + + iac.ia_addr.v4 = ifa4->sin_addr; + if (mask4) + iac.ia_prefixlen = + mask2prefixlen(mask4->sin_addr.s_addr); + + log_debug("%s: if %s (%d): %s (prefixlen %d)", + __func__, id->id_name, id->id_index, + addr4tostr(&ifa4->sin_addr), iac.ia_prefixlen); + break; + case AF_INET6: + ifa6 = (struct sockaddr_in6 *) ifa; + mask6 = (struct sockaddr_in6 *) mask; + + /* We only care about link-local and global-scope. */ + if (bad_addr_v6(&ifa6->sin6_addr)) + return; + + iac.ia_addr.v6 = ifa6->sin6_addr; + if (mask6) + iac.ia_prefixlen = mask2prefixlen6(mask6); + + log_debug("%s: if %s (%d): %s (prefixlen %d)", + __func__, id->id_name, id->id_index, + addr6tostr(&ifa6->sin6_addr), iac.ia_prefixlen); + break; + default: + return; + } + + SLIST_FOREACH(ia, &id->id_ialist, ia_entry) { + if (ia->ia_af != iac.ia_af || + ia->ia_prefixlen != iac.ia_prefixlen || + iacmp(ia, &iac)) + continue; + + /* + * Unregister the interface if this is a primary + * address, then check for new primary address. + */ + if (ia->ia_af == AF_INET && ia == intf_primaryv4(id)) { + vif4_unregister(id); + if (intf_primaryv4(id) != NULL) + regagain = 1; + } + + SLIST_REMOVE(&id->id_ialist, ia, intf_addr, ia_entry); + free(ia); + + /* Re-register if there is a new primary address. */ + if (regagain) + vif4_register(id); + return; + } +} + +#define ROUNDUP(a) \ + (((a) & (sizeof(long) - 1)) ? (1 + ((a) | (sizeof(long) - 1))) : (a)) + +void +get_rtaddrs(int addrs, struct sockaddr *sa, struct sockaddr **rti_info) +{ + int i; + + for (i = 0; i < RTAX_MAX; i++) { + if (addrs & (1 << i)) { + rti_info[i] = sa; + sa = (struct sockaddr *)((char *)(sa) + + ROUNDUP(sa->sa_len)); + } else + rti_info[i] = NULL; + } +} + +void +rtmsg_process(const uint8_t *buf, size_t len) +{ + struct rt_msghdr *rtm; + struct if_msghdr ifm; + struct ifa_msghdr *ifam; + struct sockaddr *sa, *rti_info[RTAX_MAX]; + size_t offset; + const uint8_t *next; + + for (offset = 0; offset < len; offset += rtm->rtm_msglen) { + next = buf + offset; + rtm = (struct rt_msghdr *)next; + if (len < offset + sizeof(unsigned short) || + len < offset + rtm->rtm_msglen) + fatalx("%s: partial RTM in buffer", __func__); + if (rtm->rtm_version != RTM_VERSION) + continue; + + sa = (struct sockaddr *)(next + rtm->rtm_hdrlen); + get_rtaddrs(rtm->rtm_addrs, sa, rti_info); + + switch (rtm->rtm_type) { + case RTM_IFINFO: + memcpy(&ifm, next, sizeof(ifm)); + if_update(ifm.ifm_index, ifm.ifm_flags, &ifm.ifm_data, + (struct sockaddr_dl *)rti_info[RTAX_IFP]); + break; + case RTM_NEWADDR: + ifam = (struct ifa_msghdr *)rtm; + if ((ifam->ifam_addrs & (RTA_NETMASK | RTA_IFA | + RTA_BRD)) == 0) + break; + + if_newaddr(ifam->ifam_index, + (struct sockaddr *)rti_info[RTAX_IFA], + (struct sockaddr *)rti_info[RTAX_NETMASK]); + break; + case RTM_DELADDR: + ifam = (struct ifa_msghdr *)rtm; + if ((ifam->ifam_addrs & (RTA_NETMASK | RTA_IFA | + RTA_BRD)) == 0) + break; + + if_deladdr(ifam->ifam_index, + (struct sockaddr *)rti_info[RTAX_IFA], + (struct sockaddr *)rti_info[RTAX_NETMASK]); + break; + case RTM_IFANNOUNCE: + if_announce((struct if_announcemsghdr *)next); + break; + default: + break; + } + } +} diff --git log.c log.c new file mode 100644 index 0000000..9a509fa --- /dev/null +++ log.c @@ -0,0 +1,218 @@ +/* $OpenBSD:$ */ + +/* + * Copyright (c) 2003, 2004 Henning Brauer <henn...@openbsd.org> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include <stdio.h> +#include <stdlib.h> +#include <stdarg.h> +#include <string.h> +#include <syslog.h> +#include <errno.h> +#include <time.h> + +static int debug; +static int verbose; +const char *log_procname; + +void log_init(int, int); +void log_procinit(const char *); +void log_setverbose(int); +int log_getverbose(void); +void log_warn(const char *, ...) + __attribute__((__format__ (printf, 1, 2))); +void log_warnx(const char *, ...) + __attribute__((__format__ (printf, 1, 2))); +void log_info(const char *, ...) + __attribute__((__format__ (printf, 1, 2))); +void log_debug(const char *, ...) + __attribute__((__format__ (printf, 1, 2))); +void logit(int, const char *, ...) + __attribute__((__format__ (printf, 2, 3))); +void vlog(int, const char *, va_list) + __attribute__((__format__ (printf, 2, 0))); +__dead void fatal(const char *, ...) + __attribute__((__format__ (printf, 1, 2))); +__dead void fatalx(const char *, ...) + __attribute__((__format__ (printf, 1, 2))); + +void +log_init(int n_debug, int facility) +{ + extern char *__progname; + + debug = n_debug; + verbose = n_debug; + log_procinit(__progname); + + if (!debug) + openlog(__progname, LOG_PID | LOG_NDELAY, facility); + + tzset(); +} + +void +log_procinit(const char *procname) +{ + if (procname != NULL) + log_procname = procname; +} + +void +log_setverbose(int v) +{ + verbose = v; +} + +int +log_getverbose(void) +{ + return (verbose); +} + +void +logit(int pri, const char *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + vlog(pri, fmt, ap); + va_end(ap); +} + +void +vlog(int pri, const char *fmt, va_list ap) +{ + char *nfmt; + int saved_errno = errno; + + if (debug) { + /* best effort in out of mem situations */ + if (asprintf(&nfmt, "%s\n", fmt) == -1) { + vfprintf(stderr, fmt, ap); + fprintf(stderr, "\n"); + } else { + vfprintf(stderr, nfmt, ap); + free(nfmt); + } + fflush(stderr); + } else + vsyslog(pri, fmt, ap); + + errno = saved_errno; +} + +void +log_warn(const char *emsg, ...) +{ + char *nfmt; + va_list ap; + int saved_errno = errno; + + /* best effort to even work in out of memory situations */ + if (emsg == NULL) + logit(LOG_ERR, "%s", strerror(saved_errno)); + else { + va_start(ap, emsg); + + if (asprintf(&nfmt, "%s: %s", emsg, + strerror(saved_errno)) == -1) { + /* we tried it... */ + vlog(LOG_ERR, emsg, ap); + logit(LOG_ERR, "%s", strerror(saved_errno)); + } else { + vlog(LOG_ERR, nfmt, ap); + free(nfmt); + } + va_end(ap); + } + + errno = saved_errno; +} + +void +log_warnx(const char *emsg, ...) +{ + va_list ap; + + va_start(ap, emsg); + vlog(LOG_ERR, emsg, ap); + va_end(ap); +} + +void +log_info(const char *emsg, ...) +{ + va_list ap; + + va_start(ap, emsg); + vlog(LOG_INFO, emsg, ap); + va_end(ap); +} + +void +log_debug(const char *emsg, ...) +{ + va_list ap; + + if (verbose > 1) { + va_start(ap, emsg); + vlog(LOG_DEBUG, emsg, ap); + va_end(ap); + } +} + +static void +vfatalc(int code, const char *emsg, va_list ap) +{ + static char s[BUFSIZ]; + const char *sep; + + if (emsg != NULL) { + (void)vsnprintf(s, sizeof(s), emsg, ap); + sep = ": "; + } else { + s[0] = '\0'; + sep = ""; + } + if (code) + logit(LOG_CRIT, "%s: %s%s%s", + log_procname, s, sep, strerror(code)); + else + logit(LOG_CRIT, "%s%s%s", log_procname, sep, s); +} + +void +fatal(const char *emsg, ...) +{ + va_list ap; + + va_start(ap, emsg); + vfatalc(errno, emsg, ap); + va_end(ap); + exit(1); +} + +void +fatalx(const char *emsg, ...) +{ + va_list ap; + + va_start(ap, emsg); + vfatalc(0, emsg, ap); + va_end(ap); + exit(1); +} diff --git log.h log.h new file mode 100644 index 0000000..b684405 --- /dev/null +++ log.h @@ -0,0 +1,48 @@ +/* $OpenBSD:$ */ + +/* + * Copyright (c) 2003, 2004 Henning Brauer <henn...@openbsd.org> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef LOG_H +#define LOG_H + +#include <sys/cdefs.h> + +#include <stdarg.h> +#include <syslog.h> + +void log_init(int, int); +void log_procinit(const char *); +void log_setverbose(int); +int log_getverbose(void); +void log_warn(const char *, ...) + __attribute__((__format__ (printf, 1, 2))); +void log_warnx(const char *, ...) + __attribute__((__format__ (printf, 1, 2))); +void log_info(const char *, ...) + __attribute__((__format__ (printf, 1, 2))); +void log_debug(const char *, ...) + __attribute__((__format__ (printf, 1, 2))); +void logit(int, const char *, ...) + __attribute__((__format__ (printf, 2, 3))); +void vlog(int, const char *, va_list) + __attribute__((__format__ (printf, 2, 0))); +__dead void fatal(const char *, ...) + __attribute__((__format__ (printf, 1, 2))); +__dead void fatalx(const char *, ...) + __attribute__((__format__ (printf, 1, 2))); + +#endif /* LOG_H */ diff --git mcast-proxy.8 mcast-proxy.8 new file mode 100644 index 0000000..cddeab0 --- /dev/null +++ mcast-proxy.8 @@ -0,0 +1,113 @@ +.\" $OpenBSD:$ +.\" +.\" Copyright (c) 2017 Rafael Zalamena <rzalam...@openbsd.org> +.\" +.\" Permission to use, copy, modify, and/or distribute this software for any +.\" purpose with or without fee is hereby granted, provided that the above +.\" copyright notice and this permission notice appear in all copies. +.\" +.\" THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +.\" WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +.\" MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +.\" ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +.\" WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +.\" ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +.\" OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +.\" +.Dd $Mdocdate$ +.Dt MCAST-PROXY 8 +.Os +.Sh NAME +.Nm mcast-proxy +.Nd Multicast Proxy +.Sh SYNOPSIS +.Nm +.Op Fl dnv +.Op Fl D Ar macro Ns = Ns Ar value +.Op Fl f Ar file +.Sh DESCRIPTION +.Nm +is a multicast proxy implementation for the Internet Group Management +Protocol (IGMP) and Multicast Listener Discovery (MLD) protocols. +It is used on networks that face the client to control the multicast +traffic based on the interest of the local network and to reduce the +load by filtering unneeded multicast traffic. +.Pp +The options are as follows: +.Bl -tag -width Ds +.It Fl D Ar macro Ns = Ns Ar value +Define +.Ar macro +to be set to +.Ar value +on the command line. +Overrides the definition of +.Ar macro +in the configuration file. +.It Fl d +Do not daemonize. +If this option is specified, +.Nm +will run in the foreground and log to +.Em stderr . +.It Fl f Ar file +Specify an alternative configuration file. +.It Fl n +Only check the configuration file for validity. +.It Fl v +Produce more verbose output. +.El +.Sh FILES +.Bl -tag -width "/etc/mcast-proxy.confXX" +.It Pa /etc/mcast-proxy.conf +Default +.Nm +configuration file. +.El +.Sh SEE ALSO +.Xr multicast 4 , +.Xr mcast-proxy.conf 5 +.Sh STANDARDS +.Rs +.%A S. Deering +.%D August 1989 +.%R RFC 1112 +.%T Host Extensions for IP Multicasting +.Re +.Pp +.Rs +.%A W. Fenner +.%D November 1997 +.%R RFC 2236 +.%T Internet Group Management Protocol, Version 2 +.Re +.Pp +.Rs +.%A M. Christensen +.%A Thrane & Thrane +.%A K. Kimball +.%A F. Solensky +.%D May 2006 +.%R RFC 4541 +.%T Considerations for Internet Group Management Protocol (IGMP) and Multicast Listener Discovery (MLD) Snooping Switches +.Re +.Pp +.Rs +.%A B. Fenner +.%A H. He +.%A B. Haberman +.%A H. Sandick +.%D August 2006 +.%R RFC 4605 +.%T Internet Group Management Protocol (IGMP) / Multicast Listener Discovery (MLD)-Based Multicast Forwarding ("IGMP/MLD Proxying") +.Re +.Sh HISTORY +The +.Nm +program first appeared in +.Ox 6.2 . +.Sh AUTHORS +.An -nosplit +.Nm +was written by +.An Rafael Zalamena Aq Mt rzalam...@openbsd.org diff --git mcast-proxy.c mcast-proxy.c new file mode 100644 index 0000000..ee92532 --- /dev/null +++ mcast-proxy.c @@ -0,0 +1,846 @@ +/* $OpenBSD:$ */ + +/* + * Copyright (c) 2017 Rafael Zalamena <rzalam...@openbsd.org> + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include <arpa/inet.h> + +#include <sys/time.h> + +#include <netinet/in.h> +#include <netinet/ip.h> +#include <netinet/ip6.h> +#include <netinet/icmp6.h> +#include <netinet/igmp.h> + +#include <pwd.h> +#include <signal.h> +#include <stdlib.h> +#include <stdio.h> +#include <string.h> +#include <unistd.h> + +#include "mcast-proxy.h" + +__dead void usage(void); +__dead void daemon_shutdown(void); +void sighandler(int, short, void *); +void config_setdefaults(void); + +int mcast_mquery4(struct intf_data *, struct in_addr *, struct in_addr *); +int mcast_mquery6(struct intf_data *, struct in6_addr *, struct in6_addr *); +int build_packet(uint8_t *, size_t *, struct intf_data *, struct in_addr *, + struct in_addr *, uint8_t, uint8_t); +int build_packet6(uint8_t *, size_t *, struct intf_data *, + struct in6_addr *, uint8_t, uint8_t); +int kernel_parse(uint8_t *, size_t); +int kernel_parsev6(uint8_t *, size_t); +struct igmp *igmp_parse(uint8_t *, size_t *, struct sockaddr_storage *); +const char *igmptypetostr(uint16_t); +void intf_setup(void); +void send_generalmquery(int, short, void *); +void igmp_recv(int, short, void *); +const char *mldtypetostr(uint16_t); +int mld_parse(struct intf_data *, struct sockaddr_storage *, uint8_t *, + size_t); +void mld_recv(int, short, void *); + +struct iflist iflist; +struct intf_data *upstreamif; +int igmpsd = -1; +int mldsd = -1; + +const char *config_file = "/etc/mcast-proxy.conf"; +struct igmpproxy_conf ic; + +int +main(int argc, char *argv[]) +{ + struct passwd *pw; + int verbose = 0, daemonize = 1, noaction = 0; + int ch, intfsd; + struct timeval qtv; + struct event igmpev, mldev, intfev, qtimerev; + struct event hupev, termev, intev; + + config_setdefaults(); + + /* Load all system interfaces and get their information. */ + intfsd = intf_init(); + + /* Initiate with verbose logging. */ + log_init(1, LOG_DAEMON); + log_setverbose(1); + + while ((ch = getopt(argc, argv, "f:D:dnv")) != -1) { + switch (ch) { + case 'D': + if (cmdline_symset(optarg) < 0) + log_warnx("could not parse macro definition %s", + optarg); + break; + case 'd': + daemonize = 0; + break; + case 'f': + config_file = optarg; + break; + case 'n': + noaction = 1; + break; + case 'v': + verbose = 2; + break; + default: + usage(); + break; + } + } + + if (parse_config(config_file) == -1) + fatalx("configuration failed"); + + if (noaction) + exit(0); + + /* Assert that we can run multicast forwarding. */ + assert_mcastforward(); + + /* Create the IGMP socket. */ + if (ic.ic_ipv4) + igmpsd = open_igmp_socket(); + if (ic.ic_ipv6) + mldsd = open_mld_socket(); + + /* Drop privileges. */ + pw = getpwnam(IGMP_PROXY_USER); + if (pw == NULL) + fatal("getpwnam"); + + if (chroot(pw->pw_dir) == -1) + fatal("chroot"); + if (chdir("/") == -1) + fatal("chdir"); + + if (setgroups(1, &pw->pw_gid) || + setresgid(pw->pw_gid, pw->pw_gid, pw->pw_gid) || + setresuid(pw->pw_uid, pw->pw_uid, pw->pw_uid)) + fatal("privilege drop"); + + /* Use the configured logging verbosity. */ + log_init(!daemonize, LOG_DAEMON); + log_setverbose(verbose); + + if (daemonize) + daemon(0, 0); + + log_info("startup"); + + /* Initialize libevent. */ + event_init(); + + /* Install signal handlers. */ + signal_set(&hupev, SIGHUP, sighandler, NULL); + signal_set(&intev, SIGINT, sighandler, NULL); + signal_set(&termev, SIGTERM, sighandler, NULL); + signal_add(&hupev, NULL); + signal_add(&intev, NULL); + signal_add(&termev, NULL); + signal(SIGPIPE, SIG_IGN); + + event_set(&igmpev, igmpsd, EV_READ | EV_PERSIST, + igmp_recv, NULL); + event_add(&igmpev, NULL); + event_set(&mldev, mldsd, EV_READ | EV_PERSIST, + mld_recv, NULL); + event_add(&mldev, NULL); + event_set(&intfev, intfsd, EV_READ | EV_PERSIST, + intf_dispatch, NULL); + event_add(&intfev, NULL); + + qtv.tv_sec = IGMP_STARTUP_QUERY_INTERVAL; + qtv.tv_usec = 0; + evtimer_set(&qtimerev, send_generalmquery, &qtimerev); + evtimer_add(&qtimerev, &qtv); + + /* Initialize interfaces IGMP reception. */ + intf_setup(); + +#if 0 + if (pledge("stdio inet", NULL) != 0) + fatal("pledge"); +#endif + + /* Send the startup query. */ + send_generalmquery(0, 0, &qtimerev); + + event_dispatch(); + + daemon_shutdown(); + + return 0; +} + +__dead void +usage(void) +{ + extern const char *__progname; + + fprintf(stderr, "%s: [-dnv] [-D macro=value] [-f config]\n", + __progname); + + exit(1); +} + +__dead void +daemon_shutdown(void) +{ + struct intf_data *id; + int error = 0; + + /* Clean up routes to make sure no interface references exist. */ + mrt_cleanup(); + upstreamif = NULL; + + /* Remove all interfaces. */ + while (!SLIST_EMPTY(&iflist)) { + id = SLIST_FIRST(&iflist); + id_free(id); + } + + /* Close multicast sockets. */ + error |= close_igmp_socket(igmpsd); + error |= close_mld_socket(mldsd); + igmpsd = -1; + mldsd = -1; + + exit(error != 0); +} + +void +sighandler(int sig, __unused short ev, __unused void *arg) +{ + switch (sig) { + case SIGHUP: + /* FALLTHROUGH */ + case SIGTERM: + case SIGINT: + log_info("received signal %d", sig); + daemon_shutdown(); + break; + } +} + +void +config_setdefaults(void) +{ + ic.ic_ipv4 = 1; + ic.ic_ipv6 = 0; +} + +const char * +igmptypetostr(uint16_t type) +{ + switch (type) { + case IGMP_HOST_MEMBERSHIP_QUERY: + return "MEMBERSHIP_QUERY"; + case IGMP_v1_HOST_MEMBERSHIP_REPORT: + return "MEMBERSHIP_REPORT_V1"; + case IGMP_v2_HOST_MEMBERSHIP_REPORT: + return "MEMBERSHIP_REPORT_V2"; + case IGMP_HOST_LEAVE_MESSAGE: + return "LEAVE"; + + default: + return "unknown"; + } +} + +int +build_packet(uint8_t *p, size_t *plen, struct intf_data *id, + struct in_addr *dst, struct in_addr *grp, uint8_t type, uint8_t code) +{ + struct ip *ip = (struct ip *)p; + struct intf_addr *ia; + struct igmp *igmp; + uint8_t hlen; + + *plen = 0; + + if ((ia = intf_primaryv4(id)) == NULL) { + log_debug("%s doesn't have an address", id->id_name); + return -1; + } + + memset(ip, 0, sizeof(*ip)); + hlen = sizeof(*ip) >> 2; + ip->ip_hl = hlen; + ip->ip_v = IPVERSION; + ip->ip_tos = IPTOS_PREC_INTERNETCONTROL; + ip->ip_ttl = IPDEFTTL; + ip->ip_p = IPPROTO_IGMP; + ip->ip_src = ia->ia_addr.v4; + ip->ip_dst = *dst; + *plen = hlen << 2; + + igmp = (struct igmp *)(p + sizeof(*ip)); + igmp->igmp_type = type; + igmp->igmp_code = code; + igmp->igmp_cksum = 0; + igmp->igmp_group = *grp; + *plen += sizeof(*igmp); + + /* Calculate the IP checksum. */ + ip->ip_len = htons(*plen); + ip->ip_sum = wrapsum(checksum((uint8_t *)ip, hlen, 0)); + + /* Calculate the IGMP checksum. */ + igmp->igmp_cksum = wrapsum(checksum((uint8_t *)igmp, + sizeof(*igmp), 0)); + + return 0; +} + +int +mcast_mquery4(struct intf_data *id, struct in_addr *dst, struct in_addr *grp) +{ + struct intf_addr *ia; + size_t blen; + ssize_t bsent; + struct sockaddr_storage to; + uint8_t b[2048]; + + if ((ia = intf_primaryv4(id)) == NULL) { + log_debug("%s doesn't have an address", id->id_name); + return -1; + } + + blen = sizeof(b); + if (build_packet(b, &blen, id, dst, grp, + IGMP_HOST_MEMBERSHIP_QUERY, IGMP_QUERY_INTERVAL) == -1) { + log_debug("%s: packet build failed", __func__); + return -1; + } + + igmp_setif(id); + + to.ss_family = AF_INET; + to.ss_len = sizeof(struct sockaddr_in); + sstosin(&to)->sin_addr = *dst; + if ((bsent = sendto(igmpsd, b, blen, 0, (struct sockaddr *)&to, + to.ss_len)) == -1) { + log_warn("send IGMP %s (via %s) to %s", + addr4tostr(&ia->ia_addr.v4), id->id_name, addrtostr(&to)); + return -1; + } + + igmp_setif(NULL); + + log_debug("%s (%s) -> %s IGMP MEMBERSHIP_QUERY %ld bytes", + addr4tostr(&ia->ia_addr.v4), id->id_name, addrtostr(&to), + bsent); + + return 0; +} + +int +build_packet6(uint8_t *p, size_t *plen, struct intf_data *id, + struct in6_addr *grp, uint8_t type, uint8_t code) +{ + struct intf_addr *ia; + struct mld_hdr *mld; + + *plen = 0; + + if ((ia = intf_ipv6linklayer(id)) == NULL) { + log_debug("%s doesn't have an address", id->id_name); + return -1; + } + + mld = (struct mld_hdr *)p; + mld->mld_type = type; + mld->mld_code = code; + mld->mld_cksum = 0; + mld->mld_maxdelay = 0; + mld->mld_reserved = 0; + mld->mld_addr = *grp; + *plen += sizeof(*mld); + + return 0; +} + +int +mcast_mquery6(struct intf_data *id, struct in6_addr *dst, + struct in6_addr *grp) +{ + struct intf_addr *ia; + struct cmsghdr *cmsg; + struct in6_pktinfo *ipi6; + size_t blen; + ssize_t bsent; + struct msghdr msg; + struct sockaddr_storage to; + struct iovec iov[1]; + uint8_t b[2048]; + uint8_t cmsgbuf[ + CMSG_SPACE(sizeof(struct in6_pktinfo)) + ]; + + if ((ia = intf_ipv6linklayer(id)) == NULL) { + log_debug("%s doesn't have an address", id->id_name); + return -1; + } + + blen = sizeof(b); + if (build_packet6(b, &blen, id, grp, MLD_LISTENER_QUERY, 0) == -1) { + log_debug("%s: packet build failed", __func__); + return -1; + } + + to.ss_family = AF_INET6; + to.ss_len = sizeof(struct sockaddr_in6); + sstosin6(&to)->sin6_addr = *dst; + + /* Populate msghdr. */ + memset(&msg, 0, sizeof(msg)); + iov[0].iov_base = b; + iov[0].iov_len = blen; + msg.msg_iov = iov; + msg.msg_iovlen = 1; + msg.msg_name = &to; + msg.msg_namelen = sizeof(struct sockaddr_in6); + + /* Populate msghdr parameters. */ + memset(cmsgbuf, 0, sizeof(cmsgbuf)); + msg.msg_control = cmsgbuf; + msg.msg_controllen = sizeof(cmsgbuf); + + /* Use the IPV6_PKTINFO to select the interface. */ + cmsg = (struct cmsghdr *)CMSG_FIRSTHDR(&msg); + cmsg->cmsg_len = CMSG_LEN(sizeof(*ipi6)); + cmsg->cmsg_level = IPPROTO_IPV6; + cmsg->cmsg_type = IPV6_PKTINFO; + + /* Set output interface */ + ipi6 = (struct in6_pktinfo *)CMSG_DATA(cmsg); + ipi6->ipi6_ifindex = id->id_index; + + if ((bsent = sendmsg(mldsd, &msg, 0)) == -1) { + log_warn("MLD %s (via %s) to %s", + addr6tostr(&ia->ia_addr.v6), id->id_name, addrtostr(&to)); + return -1; + } + + log_debug("%s (%s) -> %s MLD MEMBERSHIP_QUERY %ld bytes", + addr6tostr(&ia->ia_addr.v6), id->id_name, addrtostr(&to), + bsent); + + return 0; +} + +int +kernel_parse(uint8_t *p, size_t plen) +{ + struct intf_addr *ia; + struct intf_data *id; + struct ip *ip = (struct ip *)p; + size_t hlen; + + /* Sanity check: do we have enough data to work with? */ + if (plen < sizeof(*ip)) { + log_debug("%s: insufficient packet size", __func__); + return 0; + } + + /* Validate upstream interface current state. */ + if (upstreamif == NULL) { + log_debug("%s: no upstream interface", __func__); + return 0; + } + if ((ia = intf_primaryv4(upstreamif)) == NULL) { + log_debug("%s: no upstream interface address", __func__); + return 0; + } + + /* IP header validations. */ + if (ip->ip_v != IPVERSION) { + log_debug("%s: wrong IP version", __func__); + return 0; + } + hlen = ip->ip_hl << 2; + if (hlen < sizeof(*ip)) { + log_debug("%s: wrong IP header length", __func__); + return 0; + } + if ((ip->ip_off & IP_OFFMASK) != 0) { + log_debug("%s: fragmented packet", __func__); + return 0; + } + if (ip->ip_ttl == 0) { + log_debug("%s: invalid TTL", __func__); + return 0; + } + if (ip->ip_src.s_addr == INADDR_ANY || + ip->ip_dst.s_addr == INADDR_ANY) { + log_debug("%s: invalid packet addresses", __func__); + return 0; + } + + /* We only handle kernel messages here. */ + if (ip->ip_p != IPPROTO_IP) + return -1; + + id = intf_lookupbyaddr4(ip->ip_src.s_addr); + if (id == NULL || !id->id_enabled) { + log_debug("%s: no interface matches origin", __func__); + return 0; + } + + mrt_insert4(MV_IGMPV3, id, &ip->ip_src, &ip->ip_dst); + + return 0; +} + +struct igmp * +igmp_parse(uint8_t *p, size_t *plen, struct sockaddr_storage *src) +{ + struct ip *ip = (struct ip *)p; + size_t hlen, ptotal; + uint16_t cksum; + + if (ip->ip_p != IPPROTO_IGMP) { + log_debug("%s: expected IGMP message, got %d", + __func__, ip->ip_p); + return NULL; + } + + hlen = ip->ip_hl << 2; + + ptotal = ntohs(ip->ip_len); + if (*plen != ptotal) { + log_debug("%s: IP header length different than packet " + "(%ld vs %ld)", __func__, ptotal, *plen); + return 0; + } + + cksum = wrapsum(checksum((uint8_t *)ip, hlen, 0)); + if (cksum != 0) { + log_debug("%s: IP checksum is invalid", __func__); + return NULL; + } + + cksum = wrapsum(checksum((uint8_t *)ip, *plen, 0)); + if (cksum != 0) { + log_debug("%s: IGMP invalid checksum", __func__); + return NULL; + } + + log_debug("IGMP (IPv%d) %s -> %s %ld bytes", + ip->ip_v, addr4tostr(&ip->ip_src), addr4tostr(&ip->ip_dst), + *plen); + + /* Return the source address and update the remaining size. */ + memset(src, 0, sizeof(*src)); + src->ss_family = AF_INET; + src->ss_len = sizeof(struct sockaddr_in); + sstosin(src)->sin_addr = ip->ip_src; + + *plen -= hlen; + + return ((struct igmp *)(p + hlen)); +} + +void +igmp_recv(int fd, __unused short ev, __unused void *arg) +{ + struct igmp *igmp; + struct intf_data *id; + ssize_t rlen; + struct sockaddr_storage src; + uint8_t p[2048]; + + if ((rlen = recv(fd, p, sizeof(p), 0)) == -1) { + log_warn("%s: recv", __func__); + return; + } + /* Check for kernel messages and do IP header validations. */ + if (kernel_parse(p, rlen) == 0 || + (igmp = igmp_parse(p, &rlen, &src)) == NULL) + return; + + /* Handle the IGMP packet. */ + if ((size_t)rlen < sizeof(*igmp)) { + log_debug("%s: IGMP packet too short", __func__); + return; + } + + log_debug(" %s: code %d group %s", + igmptypetostr(igmp->igmp_type), igmp->igmp_code, + addr4tostr(&igmp->igmp_group)); + + /* Sanity check: group is always multicast address. */ + if (!IN_MULTICAST(ntohl(igmp->igmp_group.s_addr))) { + log_debug("%s: group is not a multicast address", + __func__); + return; + } + + /* Determine from which interface this packet came from. */ + id = intf_lookupbyaddr4(sstosin(&src)->sin_addr.s_addr); + if (id == NULL || !id->id_enabled) { + log_debug("%s: no interface matches origin", __func__); + return; + } + + /* Don't receive commands from upstream interface. */ + if (id == upstreamif) { + log_debug("%s: ignoring host command on upstream interface", + __func__); + return; + } + + switch (igmp->igmp_type) { + case IGMP_HOST_MEMBERSHIP_QUERY: + break; + case IGMP_v1_HOST_MEMBERSHIP_REPORT: + mrt_insert4(MV_IGMPV1, id, &sstosin(&src)->sin_addr, + &igmp->igmp_group); + break; + case IGMP_v2_HOST_MEMBERSHIP_REPORT: + mrt_insert4(MV_IGMPV2, id, &sstosin(&src)->sin_addr, + &igmp->igmp_group); + break; + case IGMP_HOST_LEAVE_MESSAGE: + mrt_remove4(id, &sstosin(&src)->sin_addr, &igmp->igmp_group); + break; + } +} + +const char * +mldtypetostr(uint16_t type) +{ + switch (type) { + case MLD_LISTENER_QUERY: + return "LISTENER_QUERY"; + case MLD_LISTENER_REPORT: + return "LISTENER_REPORT"; + case MLD_LISTENER_DONE: + return "LISTENER_DONE"; + + default: + return "unknown"; + } +} + +int +kernel_parsev6(uint8_t *p, size_t plen) +{ + struct ip6_hdr *ip6 = (struct ip6_hdr *)p; + struct intf_data *id; + + /* Sanity checks: + * - packet size (ipv6 header) + * - multicast destination + */ + if (plen < sizeof(*ip6)) { + log_debug("%s: packet too small for IPv6 header", __func__); + return -1; + } + if (!IN6_IS_ADDR_MULTICAST(&ip6->ip6_dst)) { + log_debug("%s: not a multicast packet", __func__); + return -1; + } + + id = intf_lookupbyaddr6(&ip6->ip6_src); + if (id == NULL || !id->id_enabled) { + log_debug("%s: no input interface for %s", + __func__, addr6tostr(&ip6->ip6_src)); + return -1; + } + + log_debug("IPv6 %s (%s) -> %s", + addr6tostr(&ip6->ip6_src), id->id_name, + addr6tostr(&ip6->ip6_dst)); + + mrt_insert6(MV_IGMPV3, id, &ip6->ip6_src, &ip6->ip6_dst); + + return 0; +} + +int +mld_parse(struct intf_data *id, struct sockaddr_storage *src, + uint8_t *p, size_t plen) +{ + struct mld_hdr *mld = (struct mld_hdr *)p; + + if (plen < sizeof(*mld)) { + log_debug("%s: packet too small", __func__); + return -1; + } + + log_debug("MLD %s %s -> %s", mldtypetostr(mld->mld_type), + addrtostr(src), addr6tostr(&mld->mld_addr)); + + switch (mld->mld_type) { + case MLD_LISTENER_QUERY: + break; + case MLD_LISTENER_REPORT: + mrt_insert6(MV_IGMPV2, id, &sstosin6(src)->sin6_addr, + &mld->mld_addr); + break; + case MLD_LISTENER_DONE: + mrt_remove6(id, &sstosin6(src)->sin6_addr, &mld->mld_addr); + break; + + default: + log_debug("%s: invalid MLD type %d", + __func__, mld->mld_type); + break; + } + + return 0; +} + +void +mld_recv(int sd, __unused short ev, __unused void *arg) +{ + struct in6_pktinfo *ipi6 = NULL; + struct intf_data *id; + struct cmsghdr *cmsg; + ssize_t rlen; + struct msghdr msg; + struct iovec iov[1]; + struct sockaddr_storage ss; + uint8_t iovbuf[2048]; + uint8_t cmsgbuf[ + CMSG_SPACE(sizeof(*ipi6)) + ]; + + iov[0].iov_base = iovbuf; + iov[0].iov_len = sizeof(iovbuf); + + memset(&msg, 0, sizeof(msg)); + msg.msg_iov = iov; + msg.msg_iovlen = 1; + msg.msg_control = cmsgbuf; + msg.msg_controllen = sizeof(cmsgbuf); + msg.msg_name = &ss; + msg.msg_namelen = sizeof(ss); + if ((rlen = recvmsg(sd, &msg, 0)) == -1) { + log_warn("%s: recvmsg", __func__); + return; + } + + /* Sanity check: is this IPv6? */ + if (ss.ss_family != AF_INET6) { + log_debug("%s: received non IPv6 packet", __func__); + return; + } + + /* Find out input interface. */ + for (cmsg = (struct cmsghdr *)CMSG_FIRSTHDR(&msg); cmsg; + cmsg = (struct cmsghdr *)CMSG_NXTHDR(&msg, cmsg)) { + if (cmsg->cmsg_level != IPPROTO_IPV6) + continue; + + switch (cmsg->cmsg_type) { + case IPV6_PKTINFO: + ipi6 = (struct in6_pktinfo *)CMSG_DATA(cmsg); + break; + } + } + /* Kernel messages from the routing socket don't have PKTINFO. */ + if (ipi6 == NULL) { + kernel_parsev6(iovbuf, rlen); + return; + } + + /* Deal with packets coming from the network. */ + id = intf_lookupbyindex(ipi6->ipi6_ifindex); + if (id == NULL || !id->id_enabled) { + log_debug("%s: no input interface for %s", + __func__, addrtostr(&ss)); + return; + } + + /* Don't receive commands from upstream interface. */ + if (id == upstreamif) { + log_debug("%s: ignoring host on upstream interface", + __func__); + return; + } + + mld_parse(id, &ss, iovbuf, rlen); +} + +void +send_generalmquery(__unused int sd, short ev, void *arg) +{ + struct event *qtimerev = (struct event *)arg; + struct intf_data *id; + struct timeval qtv = { IGMP_QUERY_INTERVAL, 0 }; + struct in_addr allhostsgrp, zerogrp; + struct in6_addr allhostsgrp6 = + IN6ADDR_LINKLOCAL_ALLNODES_INIT; + struct in6_addr zerogrp6 = IN6ADDR_ANY_INIT; + + allhostsgrp.s_addr = htonl(INADDR_ALLHOSTS_GROUP); + zerogrp.s_addr = 0; + + SLIST_FOREACH(id, &iflist, id_entry) { + /* Only join downstream interfaces. */ + if (id->id_dir != IDIR_DOWNSTREAM) + continue; + + if (id->id_mv4) + mcast_mquery4(id, &allhostsgrp, &zerogrp); + if (id->id_mv6) + mcast_mquery6(id, &allhostsgrp6, &zerogrp6); + } + + /* Only start timers if not called manually. */ + if ((ev & EV_TIMEOUT) == EV_TIMEOUT) { + evtimer_add(qtimerev, &qtv); + mrt_querytimeradd(); + } +} + +void +intf_setup(void) +{ + struct intf_data *id; + + SLIST_FOREACH(id, &iflist, id_entry) { + /* Disable IPv4 multicast if disabled globally. */ + if (ic.ic_ipv4 == 0) + id->id_mv4 = 0; + /* Disable IPv6 multicast if disabled globally. */ + if (ic.ic_ipv6 == 0) + id->id_mv6 = 0; + + if (id->id_dir == IDIR_DISABLE) + continue; + + /* Register all enabled interfaces. */ + vif_register(id); + + if (id->id_dir != IDIR_DOWNSTREAM) + continue; + + /* Only join downstream interfaces. */ + mcast_join(id, NULL); + } +} diff --git mcast-proxy.conf.5 mcast-proxy.conf.5 new file mode 100644 index 0000000..1400eef --- /dev/null +++ mcast-proxy.conf.5 @@ -0,0 +1,138 @@ +.\" $OpenBSD:$ +.\" +.\" Copyright (c) 2017 Rafael Zalamena <rzalam...@openbsd.org> +.\" +.\" Permission to use, copy, modify, and distribute this software for any +.\" purpose with or without fee is hereby granted, provided that the above +.\" copyright notice and this permission notice appear in all copies. +.\" +.\" THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +.\" WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +.\" MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +.\" ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +.\" WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +.\" ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +.\" OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +.\" +.Dd $Mdocdate$ +.Dt MCAST-PROXY.CONF 5 +.Os +.Sh NAME +.Nm mcast-proxy.conf +.Nd Multicast Proxy configuration file +.Sh DESCRIPTION +The +.Xr mcast-proxy 8 +daemon implements IGMP/MLD proxy for multicast routing. +.Sh SECTIONS +The +.Nm +config file is divided into three main sections. +.Bl -tag -width xxxx +.It Sy Macros +User-defined variables may be defined and used later, simplifying the +configuration file. +.It Sy Global Configuration +Global settings for +.Xr mcast-proxy 8 . +Allows the configuration of globally supported Internet Protocols +versions: IPv4 and/or IPv6. +.It Sy Interfaces Configuration +Interface-specific parameters. +.El +.Pp +Argument names not beginning with a letter, digit, or underscore +must be quoted. +.Pp +Additional configuration files can be included with the +.Ic include +keyword, for example: +.Bd -literal -offset indent +include "/etc/mcast-proxy.sub.conf" +.Ed +.Sh MACROS +Macros can be defined that will later be expanded in context. +Macro names must start with a letter, digit, or underscore, +and may contain any of those characters. +Macro names may not be reserved words (for example, +.Ic upstreamif , +.Ic interface , +or +.Ic default-threshold ) . +Macros are not expanded inside quotes. +.Pp +For example: +.Bd -literal -offset indent +upstreamif="em0" +default_threshold="1" +interface $upstreamif { + threshold $default_threshold + upstream +} +.Ed +.Sh GLOBAL CONFIGURATION +Here are the settings that can be set globally: +.Bl -tag -width Ds +.It Ic ipv4 Pq Ic yes Ns | Ns Ic no +Determines if the mcast-proxy will be enabled for IPv4. +This setting is enabled by default. +.It Ic ipv6 Pq Ic yes Ns | Ns Ic no +Determines if MLD-proxy will be enabled for IPv6. +This setting is disabled by default. +.El +.Sh INTERFACES CONFIGURATION +This section will describe the interface multicast configuration +options. +An interface is specified by its name. +.Bd -literal -offset indent +interface em0 { + ... +} +.Ed +.Pp +Interface-specific parameters are listed below. +.Bl -tag -width Ds +.It Ic ipv4 Pq Ic yes Ns | Ns Ic no +Enables or disables IPv4 support in this interface. +The default value is inherited from the global configuration. +.It Ic ipv6 Pq Ic yes Ns | Ns Ic no +Enables or disables IPv6 support in this interface. +The default value is inherited from the global configuration. +.It Ic threshold Ar number +Specify the minimum TTL required in the incoming packets to be +forwarded (IPv4 only). The default value is 1. +.It Ic source Ar network Ns / Ns Ar prefix +Specify an alternate network to receive multicast from. +By default only multicast traffic coming from the same network of the +interface will be allowed. +.It Pq Ic disabled Ns | Ns Ic downstream Ns | Ns Ic upstream +Configure the interface role in the multicast proxying setup. +.Ar disabled +will disable the interface participation, +.Ar downstream +mark client facing interfaces and +.Ar upstream +mark the interface which will receive the multicast traffic. +.Pp +By default all interfaces are +.Ar disabled . +.El +.Sh FILES +.Bl -tag -width "/etc/mcast-proxy.conf" -compact +.It Pa /etc/mcast-proxy.conf +.Xr mcast-proxy 8 +configuration file +.El +.Sh SEE ALSO +.Xr mcast-proxy 8 , +.Xr rc.conf.local 8 +.Sh HISTORY +The +.Nm +file format first appeared in +.Ox 6.2 . +.Sh AUTHORS +The +.Xr mcast-proxy 8 +program was written by +.An Rafael Zalamena Aq Mt rzalam...@openbsd.org . diff --git mcast-proxy.h mcast-proxy.h new file mode 100644 index 0000000..eac6ac5 --- /dev/null +++ mcast-proxy.h @@ -0,0 +1,214 @@ +/* $OpenBSD:$ */ + +/* + * Copyright (c) 2017 Rafael Zalamena <rzalam...@openbsd.org> + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef IGMP_PROXY_H +#define IGMP_PROXY_H + +#include <arpa/inet.h> +#include <sys/queue.h> +#include <sys/socket.h> +#include <sys/types.h> + +#include <netinet/in.h> +#include <net/if.h> + +#include <event.h> + +#include "log.h" + +#define IGMP_PROXY_USER "_dhcp" + +/* RFC 2236 section 8: value definitions. */ +#define IGMP_QUERY_INTERVAL 125 /* 125 seconds. */ +#define IGMP_RESPONSE_INTERVAL 10 /* 10 seconds. */ +#define IGMP_ROBUSTNESS_DEFVALUE 2 +#define IGMP_STARTUP_QUERY_INTERVAL (IGMP_QUERY_INTERVAL * 0.25) +/* + * RFC 2236 Section 8.4: Group membership interval. + * Group membership interval is composed by the following formula: + * (Robustness * Query_Interval) + Query_Response_Interval. + */ +#define IGMP_GROUP_MEMBERSHIP_INTERVAL(r, q) \ + (((r) * (q)) + IGMP_RESPONSE_INTERVAL) + +/* Signalize invalid virtual/multicast interface index. */ +#define INVALID_VINDEX ((uint16_t)-1) + +/* Interface direction configuration values. */ +enum intf_direction { + IDIR_DISABLE = 0, + IDIR_DOWNSTREAM, + IDIR_UPSTREAM, +}; + +enum mr_version { + MV_UNKNOWN, + MV_IGMPV1, + MV_IGMPV2, /* or MLDv1. */ + MV_IGMPV3, /* or MLDv2. */ +}; + +union uaddr { + struct in_addr v4; + struct in6_addr v6; +}; + +struct intf_addr { + SLIST_ENTRY(intf_addr) ia_entry; + int ia_af; + union uaddr ia_addr; + uint8_t ia_prefixlen; +}; +SLIST_HEAD(ialist, intf_addr); + +struct intf_data { + SLIST_ENTRY(intf_data) id_entry; + + /* Interface status. */ + int id_enabled; + /* Interface name. */ + char id_name[IFNAMSIZ]; + /* Interface index. */ + unsigned int id_index; + /* Interface rdomain. */ + unsigned int id_rdomain; + /* Interface flags. */ + unsigned int id_flags; + /* Interface IPv4 list. */ + struct ialist id_ialist; + /* Interface alternative networks. */ + struct ialist id_altnetlist; + + /* Multicast configurations. */ + + /* Virtual interface index. */ + uint16_t id_vindex; + /* Virtual IPv6 interface index. */ + uint16_t id_vindex6; + /* Interface direction configuration. */ + enum intf_direction id_dir; + /* Acceptable TTL threshold. */ + uint8_t id_ttl; + /* Use IPv4 multicast. */ + int id_mv4; + /* Use IPv6 multicast. */ + int id_mv6; +}; +SLIST_HEAD(iflist, intf_data); + +struct multicast_origin { + LIST_ENTRY(multicast_origin) mo_entry; + int mo_alive; + int mo_af; + struct intf_data *mo_id; + union uaddr mo_addr; +}; +LIST_HEAD(molist, multicast_origin); + +struct igmpproxy_conf { + int ic_ipv4; + int ic_ipv6; +}; + +/* igmp-proxy.c */ +extern struct intf_data *upstreamif; +extern struct iflist iflist; +extern int igmpsd; +extern int mldsd; +extern struct igmpproxy_conf ic; + +/* kroute.c */ +void assert_mcastforward(void); +int intf_init(void); +int igmp_setif(struct intf_data *); +int vif_register(struct intf_data *); +int vif_unregister(struct intf_data *); +int vif4_register(struct intf_data *); +int vif4_unregister(struct intf_data *); +int vif6_register(struct intf_data *); +int vif6_unregister(struct intf_data *); +void intf_dispatch(int, short, void *); +void intf_load(void); +int open_igmp_socket(void); +int close_igmp_socket(int); +int open_mld_socket(void); +int close_mld_socket(int); +int mcast_join(struct intf_data *, struct sockaddr_storage *); +int mcast_leave(struct intf_data *, struct sockaddr_storage *); +int mcast4_join(struct intf_data *, struct in_addr *); +int mcast4_leave(struct intf_data *, struct in_addr *); +int mcast6_join(struct intf_data *, struct in6_addr *); +int mcast6_leave(struct intf_data *, struct in6_addr *); +int mcast_addroute(unsigned short, union uaddr *, union uaddr *, + struct molist *); +int mcast_addroute6(unsigned short, union uaddr *, union uaddr *, + struct molist *); +int mcast_delroute(unsigned short, union uaddr *, union uaddr *); +int mcast_delroute6(unsigned short, union uaddr *, union uaddr *); + +/* util.c */ +const char *addrtostr(struct sockaddr_storage *); +const char *addr4tostr(struct in_addr *); +const char *addr6tostr(struct in6_addr *); +int id_matchaddr4(struct intf_data *, uint32_t); +int id_matchaddr6(struct intf_data *, struct in6_addr *); +uint16_t checksum(uint8_t *, uint16_t, uint32_t); +uint16_t wrapsum(uint16_t); +struct intf_data *id_insert(unsigned short); +struct intf_data *id_new(void); +void id_free(struct intf_data *); +void ia_inserttail(struct ialist *, struct intf_addr *); +struct intf_data *intf_lookupbyname(const char *); +struct intf_data *intf_lookupbyaddr4(uint32_t); +struct intf_data *intf_lookupbyaddr6(struct in6_addr *); +struct intf_data *intf_lookupbyindex(unsigned short); +struct intf_addr *intf_primaryv4(struct intf_data *); +struct intf_addr *intf_ipv6linklayer(struct intf_data *); +uint8_t mask2prefixlen(in_addr_t); +uint8_t mask2prefixlen6(struct sockaddr_in6 *); +in_addr_t prefixlen2mask(uint8_t); +void applymask(int, union uaddr *, const union uaddr *, int); + +/* mrt.c */ +void mrt_querytimeradd(void); +struct multicast_route *mrt_insert4(enum mr_version, struct intf_data *, + struct in_addr *, struct in_addr *); +struct multicast_route *mrt_insert6(enum mr_version, struct intf_data *, + struct in6_addr *, struct in6_addr *); +void mrt_remove4(struct intf_data *, struct in_addr *, struct in_addr *); +void mrt_remove6(struct intf_data *, struct in6_addr *, struct in6_addr *); +void mrt_cleanup(void); + +/* parse.y */ +int cmdline_symset(const char *); +int parse_config(const char *); + +/* Helpers */ +static inline struct sockaddr_in * +sstosin(struct sockaddr_storage *ss) +{ + return (struct sockaddr_in *)ss; +} + +static inline struct sockaddr_in6 * +sstosin6(struct sockaddr_storage *ss) +{ + return (struct sockaddr_in6 *)ss; +} + +#endif /* IGMP_PROXY_H */ diff --git mrt.c mrt.c new file mode 100644 index 0000000..df80b75 --- /dev/null +++ mrt.c @@ -0,0 +1,577 @@ +/* $OpenBSD:$ */ + +/* + * Copyright (c) 2017 Rafael Zalamena <rzalam...@openbsd.org> + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include <sys/tree.h> + +#include <stdlib.h> +#include <string.h> + +#include "mcast-proxy.h" + +enum mr_state { + MS_NOTJOINED, + MS_JOINED, +}; + +struct multicast_route { + RB_ENTRY(multicast_route) mr_entry; + + enum mr_state mr_state; + enum mr_version mr_version; + int mr_af; + union uaddr mr_group; + struct event mr_timer; + /* Version timer. */ + struct event mr_vtimer; + /* Lowest version recorded during the version timer. */ + enum mr_version mr_lowestversion; + struct intf_data *mr_upstream; + + /* Origin list. */ + struct molist mr_molist; +}; +RB_HEAD(mrtree, multicast_route) mrtree = RB_INITIALIZER(&mrtree); + +struct multicast_origin *mo_lookup(struct molist *, struct intf_data *, + union uaddr *); +struct multicast_origin *mrt_addorigin(struct multicast_route *, + struct intf_data *,union uaddr *); +void _mrt_delorigin(struct multicast_route *, struct multicast_origin *); +void mrt_delorigin(struct multicast_route *, struct intf_data *, + union uaddr *); + +void mrt_timeradd(struct event *); +void mrt_timer(int, short, void *); +void mrt_vtimeradd(struct multicast_route *); +void mrt_vtimer(int, short, void *); +struct multicast_route *mrt_new(void); +void mrt_free(struct multicast_route *); +struct multicast_route *mrt_find4(struct in_addr *); +struct multicast_route *mrt_find6(struct in6_addr *); +int mrcmp(struct multicast_route *, struct multicast_route *); +RB_PROTOTYPE(mrtree, multicast_route, mr_entry, mrcmp); +void mrt_nextstate(struct multicast_route *); + +struct multicast_origin * +mo_lookup(struct molist *molist, struct intf_data *id, union uaddr *addr) +{ + struct multicast_origin *mo; + size_t addrsize; + + LIST_FOREACH(mo, molist, mo_entry) { + addrsize = (mo->mo_af == AF_INET) ? + sizeof(addr->v4) : sizeof(addr->v6); + if (id != NULL && id != mo->mo_id) + continue; + if (memcmp(addr, &mo->mo_addr, addrsize) != 0) + continue; + + return mo; + } + + return NULL; +} + +struct multicast_origin * +mrt_addorigin(struct multicast_route *mr, struct intf_data *id, + union uaddr *addr) +{ + struct multicast_origin *mo; + + mo = mo_lookup(&mr->mr_molist, id, addr); + if (mo != NULL) { + /* Update the kernel routes in case they have expired. */ + if (mr->mr_upstream != NULL) { + if (mo->mo_af == AF_INET) + mcast_addroute(mr->mr_upstream->id_vindex, + addr, &mr->mr_group, &mr->mr_molist); + else + mcast_addroute6(mr->mr_upstream->id_vindex6, + addr, &mr->mr_group, &mr->mr_molist); + } + mo->mo_alive = 1; + return mo; + } + + mo = calloc(1, sizeof(*mo)); + if (mo == NULL) { + log_warn("%s: calloc", __func__); + return NULL; + } + + LIST_INSERT_HEAD(&mr->mr_molist, mo, mo_entry); + + mo->mo_alive = 1; + mo->mo_id = id; + mo->mo_af = mr->mr_af; + mo->mo_addr = *addr; + if (id == upstreamif || mr->mr_upstream) { + if (mr->mr_upstream == NULL) + mr->mr_upstream = upstreamif; + + if (mo->mo_af == AF_INET) + mcast_addroute(mr->mr_upstream->id_vindex, addr, + &mr->mr_group, &mr->mr_molist); + else + mcast_addroute6(mr->mr_upstream->id_vindex6, addr, + &mr->mr_group, &mr->mr_molist); + } + + return mo; +} + +void +_mrt_delorigin(struct multicast_route *mr, struct multicast_origin *mo) +{ + LIST_REMOVE(mo, mo_entry); + + if (mr->mr_upstream != NULL) { + /* + * If this was the last item of the group list we can + * uninstall the whole group, otherwise update the + * installed routes with the current origins. + */ + if (LIST_EMPTY(&mr->mr_molist)) { + if (mo->mo_af == AF_INET) + mcast_delroute(mr->mr_upstream->id_vindex, + &mo->mo_addr, &mr->mr_group); + else + mcast_delroute6(mr->mr_upstream->id_vindex6, + &mo->mo_addr, &mr->mr_group); + } else { + if (mo->mo_af == AF_INET) + mcast_addroute(mr->mr_upstream->id_vindex, + &mo->mo_addr, &mr->mr_group, + &mr->mr_molist); + else + mcast_addroute6(mr->mr_upstream->id_vindex6, + &mo->mo_addr, &mr->mr_group, + &mr->mr_molist); + } + } + + free(mo); +} + +void +mrt_delorigin(struct multicast_route *mr, struct intf_data *id, + union uaddr *addr) +{ + struct multicast_origin *mo; + + mo = mo_lookup(&mr->mr_molist, id, addr); + if (mo == NULL) + return; + + _mrt_delorigin(mr, mo); +} + +void +mrt_timeradd(struct event *ev) +{ + unsigned long total = IGMP_GROUP_MEMBERSHIP_INTERVAL( + IGMP_ROBUSTNESS_DEFVALUE, IGMP_RESPONSE_INTERVAL); + struct timeval tv; + + if (evtimer_pending(ev, &tv)) + evtimer_del(ev); + + tv.tv_sec = total; + tv.tv_usec = 0; + evtimer_add(ev, &tv); +} + +void +mrt_querytimeradd(void) +{ + struct multicast_route *mr; + + /* Activate all group expire timers. */ + RB_FOREACH(mr, mrtree, &mrtree) { + mrt_timeradd(&mr->mr_timer); + } +} + +void +mrt_vtimeradd(struct multicast_route *mr) +{ + mrt_timeradd(&mr->mr_vtimer); +} + +void +mrt_timer(__unused int sd, __unused short ev, void *arg) +{ + struct multicast_route *mr = arg; + struct multicast_origin *mo, *mon; + + if (mr->mr_af == AF_INET) + log_debug("%s: group %s timer expired", + __func__, addr4tostr(&mr->mr_group.v4)); + else + log_debug("%s: group %s timer expired", + __func__, addr6tostr(&mr->mr_group.v6)); + + /* Remove origins that did not respond. */ + LIST_FOREACH_SAFE(mo, &mr->mr_molist, mo_entry, mon) { + if (mo->mo_alive) { + /* Mark as dead until next update. */ + mo->mo_alive = 0; + continue; + } + + _mrt_delorigin(mr, mo); + } + + mrt_nextstate(mr); + + /* Remove the group if there is no more origins. */ + if (LIST_EMPTY(&mr->mr_molist)) + mrt_free(mr); +} + +void +mrt_vtimer(__unused int sd, __unused short ev, void *arg) +{ + struct multicast_route *mr = arg; + + if (mr->mr_af == AF_INET) + log_debug("%s: group %s version timer expired", + __func__, addr4tostr(&mr->mr_group.v4)); + else + log_debug("%s: group %s version timer expired", + __func__, addr6tostr(&mr->mr_group.v6)); + + mrt_vtimeradd(mr); + + /* + * Apply the RFC 2236 section 5 and RFC 4541 section 2.1.1 sub + * item 1: the IGMPv2 is the most compatible version of the + * protocol. + * + * This is the default fallback version. + */ + if (mr->mr_version == MV_IGMPV2) + return; + + /* + * If we are on a 'special' version, reset the lowest value and + * expect another report with a version different than v2. If no + * new reports with different version comes in, assume that + * there are no more to enter a compatibility mode. + */ + mr->mr_version = mr->mr_lowestversion; + mr->mr_lowestversion = MV_IGMPV2; +} + +struct multicast_route * +mrt_new(void) +{ + struct multicast_route *mr; + + mr = calloc(1, sizeof(*mr)); + if (mr == NULL) { + log_warn("%s: calloc", __func__); + return NULL; + } + + mr->mr_state = MS_NOTJOINED; + mr->mr_version = MV_IGMPV3; + mr->mr_lowestversion = MV_IGMPV3; + LIST_INIT(&mr->mr_molist); + + evtimer_set(&mr->mr_timer, mrt_timer, mr); + evtimer_set(&mr->mr_vtimer, mrt_vtimer, mr); + mrt_timeradd(&mr->mr_timer); + mrt_timeradd(&mr->mr_vtimer); + + return mr; +} + +void +mrt_free(struct multicast_route *mr) +{ + struct multicast_origin *mo; + struct timeval tv; + struct sockaddr_storage ss; + + if (evtimer_pending(&mr->mr_timer, &tv)) + evtimer_del(&mr->mr_timer); + + if (evtimer_pending(&mr->mr_vtimer, &tv)) + evtimer_del(&mr->mr_vtimer); + + while (!LIST_EMPTY(&mr->mr_molist)) { + mo = LIST_FIRST(&mr->mr_molist); + LIST_REMOVE(mo, mo_entry); + _mrt_delorigin(mr, mo); + } + + ss.ss_family = mr->mr_af; + if (ss.ss_family == AF_INET) + sstosin(&ss)->sin_addr = mr->mr_group.v4; + else + sstosin6(&ss)->sin6_addr = mr->mr_group.v6; + + log_debug("%s: remove group %s", __func__, addrtostr(&ss)); + + RB_REMOVE(mrtree, &mrtree, mr); + + free(mr); +} + +void +mrt_cleanup(void) +{ + struct multicast_route *mr; + + while (!RB_EMPTY(&mrtree)) { + mr = RB_ROOT(&mrtree); + mrt_free(mr); + } +} + +struct multicast_route * +mrt_find4(struct in_addr *in) +{ + struct multicast_route key; + + memset(&key, 0, sizeof(key)); + key.mr_af = AF_INET; + key.mr_group.v4 = *in; + return RB_FIND(mrtree, &mrtree, &key); +} + +struct multicast_route * +mrt_insert4(enum mr_version mv, struct intf_data *id, + struct in_addr *origin, struct in_addr *group) +{ + struct multicast_route *mr, *mrn; + union uaddr uorigin; + + /* Sanity check: only use multicast groups. */ + if (!IN_MULTICAST(ntohl(group->s_addr))) { + log_debug("%s(%s, %s): not multicast group", + __func__, id->id_name, addr4tostr(group)); + return NULL; + } + + /* Try to find it, if it exists just add the new origin. */ + mr = mrt_find4(group); + if (mr != NULL) + goto add_origin; + + /* Otherwise create one and insert. */ + mr = mrt_new(); + if (mr == NULL) + return NULL; + + mr->mr_af = AF_INET; + mr->mr_group.v4 = *group; + mrn = RB_INSERT(mrtree, &mrtree, mr); + if (mrn != NULL) { + mrt_free(mr); + mr = mrn; + } + + add_origin: + /* + * Always use the lowest version immediately, otherwise wait the + * query timeout before switching. See mrt_vtimer() for more + * details. + */ + if (mr->mr_version > mv) + mr->mr_version = mv; + if (mr->mr_lowestversion > mv) + mr->mr_lowestversion = mv; + + uorigin.v4 = *origin; + mrt_addorigin(mr, id, &uorigin); + + mrt_nextstate(mr); + + return mr; +} + +void +mrt_remove4(struct intf_data *id, struct in_addr *origin, + struct in_addr *group) +{ + struct multicast_route *mr; + union uaddr uorigin; + + mr = mrt_find4(group); + if (mr == NULL) + return; + + /* IGMPv1 compatibility mode does not accept fast-leave. */ + if (mr->mr_version == MV_IGMPV1) + return; + + uorigin.v4 = *origin; + mrt_delorigin(mr, id, &uorigin); + mrt_nextstate(mr); + if (LIST_EMPTY(&mr->mr_molist)) + mrt_free(mr); +} + +struct multicast_route * +mrt_find6(struct in6_addr *in6) +{ + struct multicast_route key; + + memset(&key, 0, sizeof(key)); + key.mr_af = AF_INET6; + key.mr_group.v6 = *in6; + return RB_FIND(mrtree, &mrtree, &key); +} + +struct multicast_route * +mrt_insert6(enum mr_version mv, struct intf_data *id, + struct in6_addr *origin, struct in6_addr *group) +{ + struct multicast_route *mr, *mrn; + union uaddr uorigin; + + /* Sanity check: only use multicast groups. */ + if (!IN6_IS_ADDR_MULTICAST(group)) { + log_debug("%s(%s, %s): not multicast group", + __func__, id->id_name, addr6tostr(group)); + return NULL; + } + + /* Try to find it, if it exists just add the new origin. */ + mr = mrt_find6(group); + if (mr != NULL) + goto add_origin; + + /* Otherwise create one and insert. */ + mr = mrt_new(); + if (mr == NULL) + return NULL; + + mr->mr_af = AF_INET6; + mr->mr_group.v6 = *group; + mrn = RB_INSERT(mrtree, &mrtree, mr); + if (mrn != NULL) { + mrt_free(mr); + mr = mrn; + } + + add_origin: + /* + * Always use the lowest version immediately, otherwise wait the + * query timeout before switching. See mrt_vtimer() for more + * details. + */ + if (mr->mr_version > mv) + mr->mr_version = mv; + if (mr->mr_lowestversion > mv) + mr->mr_lowestversion = mv; + + uorigin.v6 = *origin; + mrt_addorigin(mr, id, &uorigin); + + mrt_nextstate(mr); + + return mr; +} + +void +mrt_remove6(struct intf_data *id, struct in6_addr *origin, + struct in6_addr *group) +{ + struct multicast_route *mr; + union uaddr uorigin; + + mr = mrt_find6(group); + if (mr == NULL) + return; + + uorigin.v6 = *origin; + mrt_delorigin(mr, id, &uorigin); + mrt_nextstate(mr); + if (LIST_EMPTY(&mr->mr_molist)) + mrt_free(mr); +} + +void +mrt_nextstate(struct multicast_route *mr) +{ + struct sockaddr_storage ss; + + if (mr->mr_upstream == NULL) { + log_debug("%s: no upstream interface", __func__); + return; + } + + ss.ss_family = mr->mr_af; + switch (ss.ss_family) { + case AF_INET: + sstosin(&ss)->sin_addr = mr->mr_group.v4; + break; + case AF_INET6: + sstosin6(&ss)->sin6_addr = mr->mr_group.v6; + break; + default: + fatalx("%s: unknown family %d", + __func__, ss.ss_family); + } + + switch (mr->mr_state) { + case MS_NOTJOINED: + /* Don't join if there is no interest. */ + if (LIST_EMPTY(&mr->mr_molist)) + return; + + mcast_join(mr->mr_upstream, &ss); + mr->mr_state = MS_JOINED; + break; + + case MS_JOINED: + /* Don't leave if there is still peers. */ + if (!LIST_EMPTY(&mr->mr_molist)) + return; + + mcast_leave(mr->mr_upstream, &ss); + mr->mr_state = MS_NOTJOINED; + break; + + default: + log_debug("%s: invalid state %d", + __func__, mr->mr_state); + break; + } +} + +RB_GENERATE(mrtree, multicast_route, mr_entry, mrcmp); + +int +mrcmp(struct multicast_route *mr1, struct multicast_route *mr2) +{ + size_t addrsize; + + if (mr1->mr_af > mr2->mr_af) + return 1; + else if (mr1->mr_af < mr2->mr_af) + return -1; + + addrsize = (mr1->mr_af == AF_INET) ? + sizeof(mr1->mr_group.v4) : sizeof(mr1->mr_group.v6); + + return memcmp(&mr1->mr_group, &mr2->mr_group, addrsize); +} diff --git parse.y parse.y new file mode 100644 index 0000000..ca6865c --- /dev/null +++ parse.y @@ -0,0 +1,720 @@ +/* $OpenBSD:$ */ + +/* + * Copyright (c) 2017 Rafael Zalamena <rzalam...@openbsd.org> + * Copyright (c) 2015 Renato Westphal <ren...@openbsd.org> + * Copyright (c) 2004, 2005 Esben Norby <no...@openbsd.org> + * Copyright (c) 2004 Ryan McBride <mcbr...@openbsd.org> + * Copyright (c) 2002, 2003, 2004 Henning Brauer <henn...@openbsd.org> + * Copyright (c) 2001 Markus Friedl. All rights reserved. + * Copyright (c) 2001 Daniel Hartmeier. All rights reserved. + * Copyright (c) 2001 Theo de Raadt. All rights reserved. + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +%{ +#include <arpa/inet.h> + +#include <sys/limits.h> +#include <sys/queue.h> +#include <sys/stat.h> + +#include <ctype.h> +#include <err.h> +#include <stdarg.h> +#include <stdint.h> +#include <stdio.h> +#include <unistd.h> + +#include "mcast-proxy.h" + +struct file { + TAILQ_ENTRY(file) entry; + FILE *stream; + char *name; + int lineno; + int errors; +}; +TAILQ_HEAD(files, file); + +struct sym { + TAILQ_ENTRY(sym) entry; + int used; + int persist; + char *nam; + char *val; +}; +TAILQ_HEAD(symhead, sym); + +typedef struct { + union { + int64_t number; + char *string; + } v; + int lineno; +} YYSTYPE; + +#define MAXPUSHBACK 128 + +static int yyerror(const char *, ...) + __attribute__((__format__ (printf, 1, 2))) + __attribute__((__nonnull__ (1))); +static int kw_cmp(const void *, const void *); +static int lookup(char *); +static int lgetc(int); +static int lungetc(int); +static int findeol(void); +static int yylex(void); +static int check_file_secrecy(int, const char *); +static struct file *pushfile(const char *, int); +static int popfile(void); +static int yyparse(void); +static int symset(const char *, const char *, int); +static char *symget(const char *); + +static struct file *file, *topfile; +static struct files files = TAILQ_HEAD_INITIALIZER(files); +static struct symhead symhead = TAILQ_HEAD_INITIALIZER(symhead); +static int errors; + +static unsigned char *parsebuf; +static int parseindex; +static unsigned char pushback_buffer[MAXPUSHBACK]; +static int pushback_index; + +struct intf_data *cid; + +%} + +%token IPV4 IPV6 INTERFACE DISABLE DOWNSTREAM SOURCE UPSTREAM THRESHOLD +%token INCLUDE YES NO +%token ERROR +%token <v.string> STRING +%token <v.number> NUMBER +%type <v.number> yesno +%type <v.string> string + +%% + +grammar : /* empty */ + | grammar '\n' + | grammar conf_opt '\n' + | grammar include '\n' + | grammar varset '\n' + | grammar error '\n' { file->errors++; } + ; + +conf_opt : INTERFACE STRING { + cid = intf_lookupbyname($2); + if (cid == NULL) { + cid = id_new(); + if (cid == NULL) + fatal("%s:%d: calloc", + file->name, yylval.lineno); + if (strlcpy(cid->id_name, $2, + sizeof(cid->id_name)) >= sizeof(cid->id_name)) + fatalx("%s:%d: interface name too long", + file->name, yylval.lineno); + } + + cid->id_mv4 = ic.ic_ipv4; + cid->id_mv6 = ic.ic_ipv6; + } intf_block + | global_ip + ; + +global_ip : IPV4 yesno { ic.ic_ipv4 = $2; } + | IPV6 yesno { ic.ic_ipv6 = $2; } + ; + +intf_block : '{' optnl intf_opts '}' + | '{' optnl '}' + ; + +intf_opts : intf_opt nl intf_opts + | intf_opt optnl + ; + +intf_opt : THRESHOLD NUMBER { + if ($2 < 1 || $2 > 255) + fatalx("%s:%d: invalid threshold value: %llu", + file->name, yylval.lineno, $2); + + cid->id_ttl = $2; + } + | SOURCE STRING { + struct intf_addr *ia; + char *prefixp; + const char *errp; + + prefixp = strchr($2, '/'); + if (prefixp == NULL) + fatalx("%s:%d: failed to find prefix", + file->name, yylval.lineno); + + *prefixp = 0; + prefixp++; + if (*prefixp == 0) + fatalx("%s:%d: empty prefix", + file->name, yylval.lineno); + + ia = calloc(1, sizeof(*ia)); + if (ia == NULL) + fatal("%s:%d: calloc", + file->name, yylval.lineno); + + if (inet_pton(AF_INET, $2, &ia->ia_addr) != 1) { + if (inet_pton(AF_INET6, $2, &ia->ia_addr) != 1) { + fatalx("%s:%d: invalid address '%s'", + file->name, yylval.lineno, $2); + } else + ia->ia_af = AF_INET6; + } else + ia->ia_af = AF_INET; + + ia->ia_prefixlen = strtonum(prefixp, 0, 128, &errp); + if (errp != NULL) + fatalx("%s:%d: invalid prefix length: %s", + file->name, yylval.lineno, errp); + if (ia->ia_af == AF_INET && ia->ia_prefixlen > 32) + fatalx("%s:%d: invalid prefix length", + file->name, yylval.lineno); + else if (ia->ia_af == AF_INET6 && ia->ia_prefixlen > 128) + fatalx("%s:%d: invalid prefix length", + file->name, yylval.lineno); + + SLIST_INSERT_HEAD(&cid->id_altnetlist, ia, ia_entry); + } + | UPSTREAM { + if (upstreamif != NULL) + fatalx("%s:%d: it is not possible to have " + "multiple upstream interfaces.", + file->name, yylval.lineno); + + upstreamif = cid; + cid->id_dir = IDIR_UPSTREAM; + } + | DOWNSTREAM { cid->id_dir = IDIR_DOWNSTREAM; } + | DISABLE { cid->id_dir = IDIR_DISABLE; } + | IPV4 yesno { cid->id_mv4 = $2; } + | IPV6 yesno { cid->id_mv6 = $2; } + ; + +include : INCLUDE STRING { + struct file *nfile; + + if ((nfile = pushfile($2, 1)) == NULL) { + yyerror("failed to include file %s", $2); + free($2); + YYERROR; + } + free($2); + + file = nfile; + lungetc('\n'); + } + ; + +varset : STRING '=' string { + const char *s = $1; + while (*s++) { + if (isspace((unsigned char)*s)) { + yyerror("macro name cannot contain " + "whitespace"); + YYERROR; + } + } + if (symset($1, $3, 0) == -1) + fatal("cannot store variable"); + free($1); + free($3); + } + ; + +string : string STRING { + if (asprintf(&$$, "%s %s", $1, $2) == -1) { + free($1); + free($2); + yyerror("string: asprintf"); + YYERROR; + } + free($1); + free($2); + } + | STRING + ; + +optnl : '\n' optnl + | + ; + +nl : '\n' optnl + ; + +yesno : YES { $$ = 1; } + | NO { $$ = 0; } + ; + +%% + +struct keywords { + const char *k_name; + int k_val; +}; + +static int +yyerror(const char *fmt, ...) +{ + va_list ap; + char *msg; + + file->errors++; + va_start(ap, fmt); + if (vasprintf(&msg, fmt, ap) == -1) + fatalx("yyerror vasprintf"); + va_end(ap); + logit(LOG_CRIT, "%s:%d: %s", file->name, yylval.lineno, msg); + free(msg); + return (0); +} + +static int +kw_cmp(const void *k, const void *e) +{ + return (strcmp(k, ((const struct keywords *)e)->k_name)); +} + +static int +lookup(char *s) +{ + /* this has to be sorted always */ + static const struct keywords keywords[] = { + {"disabled", DISABLE}, + {"downstream", DOWNSTREAM}, + {"include", INCLUDE}, + {"interface", INTERFACE}, + {"ipv4", IPV4}, + {"ipv6", IPV6}, + {"no", NO}, + {"source", SOURCE}, + {"threshold", THRESHOLD}, + {"upstream", UPSTREAM}, + {"yes", YES}, + }; + const struct keywords *p; + + p = bsearch(s, keywords, sizeof(keywords)/sizeof(keywords[0]), + sizeof(keywords[0]), kw_cmp); + + if (p) + return (p->k_val); + else + return (STRING); +} + +static int +lgetc(int quotec) +{ + int c, next; + + if (parsebuf) { + /* Read character from the parsebuffer instead of input. */ + if (parseindex >= 0) { + c = parsebuf[parseindex++]; + if (c != '\0') + return (c); + parsebuf = NULL; + } else + parseindex++; + } + + if (pushback_index) + return (pushback_buffer[--pushback_index]); + + if (quotec) { + if ((c = getc(file->stream)) == EOF) { + yyerror("reached end of file while parsing " + "quoted string"); + if (file == topfile || popfile() == EOF) + return (EOF); + return (quotec); + } + return (c); + } + + while ((c = getc(file->stream)) == '\\') { + next = getc(file->stream); + if (next != '\n') { + c = next; + break; + } + yylval.lineno = file->lineno; + file->lineno++; + } + + while (c == EOF) { + if (file == topfile || popfile() == EOF) + return (EOF); + c = getc(file->stream); + } + return (c); +} + +static int +lungetc(int c) +{ + if (c == EOF) + return (EOF); + if (parsebuf) { + parseindex--; + if (parseindex >= 0) + return (c); + } + if (pushback_index < MAXPUSHBACK-1) + return (pushback_buffer[pushback_index++] = c); + else + return (EOF); +} + +static int +findeol(void) +{ + int c; + + parsebuf = NULL; + + /* skip to either EOF or the first real EOL */ + while (1) { + if (pushback_index) + c = pushback_buffer[--pushback_index]; + else + c = lgetc(0); + if (c == '\n') { + file->lineno++; + break; + } + if (c == EOF) + break; + } + return (ERROR); +} + +static int +yylex(void) +{ + unsigned char buf[8096]; + unsigned char *p, *val; + int quotec, next, c; + int token; + + top: + p = buf; + while ((c = lgetc(0)) == ' ' || c == '\t') + ; /* nothing */ + + yylval.lineno = file->lineno; + if (c == '#') + while ((c = lgetc(0)) != '\n' && c != EOF) + ; /* nothing */ + if (c == '$' && parsebuf == NULL) { + while (1) { + if ((c = lgetc(0)) == EOF) + return (0); + + if (p + 1 >= buf + sizeof(buf) - 1) { + yyerror("string too long"); + return (findeol()); + } + if (isalnum(c) || c == '_') { + *p++ = c; + continue; + } + *p = '\0'; + lungetc(c); + break; + } + val = symget(buf); + if (val == NULL) { + yyerror("macro '%s' not defined", buf); + return (findeol()); + } + parsebuf = val; + parseindex = 0; + goto top; + } + + switch (c) { + case '\'': + case '"': + quotec = c; + while (1) { + if ((c = lgetc(quotec)) == EOF) + return (0); + if (c == '\n') { + file->lineno++; + continue; + } else if (c == '\\') { + if ((next = lgetc(quotec)) == EOF) + return (0); + if (next == quotec || c == ' ' || c == '\t') + c = next; + else if (next == '\n') { + file->lineno++; + continue; + } else + lungetc(next); + } else if (c == quotec) { + *p = '\0'; + break; + } else if (c == '\0') { + yyerror("syntax error"); + return (findeol()); + } + if (p + 1 >= buf + sizeof(buf) - 1) { + yyerror("string too long"); + return (findeol()); + } + *p++ = c; + } + yylval.v.string = strdup(buf); + if (yylval.v.string == NULL) + err(1, "yylex: strdup"); + return (STRING); + } + +#define allowed_to_end_number(x) \ + (isspace(x) || x == ')' || x ==',' || x == '/' || x == '}' || x == '=') + + if (c == '-' || isdigit(c)) { + do { + *p++ = c; + if ((unsigned)(p-buf) >= sizeof(buf)) { + yyerror("string too long"); + return (findeol()); + } + } while ((c = lgetc(0)) != EOF && isdigit(c)); + lungetc(c); + if (p == buf + 1 && buf[0] == '-') + goto nodigits; + if (c == EOF || allowed_to_end_number(c)) { + const char *errstr = NULL; + + *p = '\0'; + yylval.v.number = strtonum(buf, LLONG_MIN, + LLONG_MAX, &errstr); + if (errstr) { + yyerror("\"%s\" invalid number: %s", + buf, errstr); + return (findeol()); + } + return (NUMBER); + } else { +nodigits: + while (p > buf + 1) + lungetc(*--p); + c = *--p; + if (c == '-') + return (c); + } + } + +#define allowed_in_string(x) \ + (isalnum(x) || (ispunct(x) && x != '(' && x != ')' && \ + x != '{' && x != '}' && \ + x != '!' && x != '=' && x != '#' && \ + x != ',')) + + if (isalnum(c) || c == ':' || c == '_') { + do { + *p++ = c; + if ((unsigned)(p-buf) >= sizeof(buf)) { + yyerror("string too long"); + return (findeol()); + } + } while ((c = lgetc(0)) != EOF && (allowed_in_string(c))); + lungetc(c); + *p = '\0'; + if ((token = lookup(buf)) == STRING) + if ((yylval.v.string = strdup(buf)) == NULL) + err(1, "yylex: strdup"); + return (token); + } + if (c == '\n') { + yylval.lineno = file->lineno; + file->lineno++; + } + if (c == EOF) + return (0); + return (c); +} + +static int +check_file_secrecy(int fd, const char *fname) +{ + struct stat st; + + if (fstat(fd, &st)) { + log_warn("cannot stat %s", fname); + return (-1); + } + if (st.st_uid != 0 && st.st_uid != getuid()) { + log_warnx("%s: owner not root or current user", fname); + return (-1); + } + if (st.st_mode & (S_IWGRP | S_IXGRP | S_IRWXO)) { + log_warnx("%s: group writable or world read/writable", fname); + return (-1); + } + return (0); +} + +static struct file * +pushfile(const char *name, int secret) +{ + struct file *nfile; + + if ((nfile = calloc(1, sizeof(struct file))) == NULL) { + log_warn("calloc"); + return (NULL); + } + if ((nfile->name = strdup(name)) == NULL) { + log_warn("strdup"); + free(nfile); + return (NULL); + } + if ((nfile->stream = fopen(nfile->name, "r")) == NULL) { + log_warn("%s", nfile->name); + free(nfile->name); + free(nfile); + return (NULL); + } else if (secret && + check_file_secrecy(fileno(nfile->stream), nfile->name)) { + fclose(nfile->stream); + free(nfile->name); + free(nfile); + return (NULL); + } + nfile->lineno = 1; + TAILQ_INSERT_TAIL(&files, nfile, entry); + return (nfile); +} + +static int +popfile(void) +{ + struct file *prev; + + if ((prev = TAILQ_PREV(file, files, entry)) != NULL) + prev->errors += file->errors; + + TAILQ_REMOVE(&files, file, entry); + fclose(file->stream); + free(file->name); + free(file); + file = prev; + return (file ? 0 : EOF); +} + +static int +symset(const char *nam, const char *val, int persist) +{ + struct sym *sym; + + TAILQ_FOREACH(sym, &symhead, entry) { + if (strcmp(nam, sym->nam) == 0) + break; + } + + if (sym != NULL) { + if (sym->persist == 1) + return (0); + else { + free(sym->nam); + free(sym->val); + TAILQ_REMOVE(&symhead, sym, entry); + free(sym); + } + } + if ((sym = calloc(1, sizeof(*sym))) == NULL) + return (-1); + + sym->nam = strdup(nam); + if (sym->nam == NULL) { + free(sym); + return (-1); + } + sym->val = strdup(val); + if (sym->val == NULL) { + free(sym->nam); + free(sym); + return (-1); + } + sym->used = 0; + sym->persist = persist; + TAILQ_INSERT_TAIL(&symhead, sym, entry); + return (0); +} + +int +cmdline_symset(const char *s) +{ + char *sym, *val; + int ret; + size_t len; + + if ((val = strrchr(s, '=')) == NULL) + return (-1); + + len = strlen(s) - strlen(val) + 1; + if ((sym = malloc(len)) == NULL) + errx(1, "cmdline_symset: malloc"); + + strlcpy(sym, s, len); + + ret = symset(sym, val + 1, 1); + free(sym); + + return (ret); +} + +static char * +symget(const char *nam) +{ + struct sym *sym; + + TAILQ_FOREACH(sym, &symhead, entry) { + if (strcmp(nam, sym->nam) == 0) { + sym->used = 1; + return (sym->val); + } + } + return (NULL); +} + +int +parse_config(const char *filename) +{ + if ((file = pushfile(filename, 0)) == NULL) + return -1; + + topfile = file; + + yyparse(); + errors = file->errors; + popfile(); + if (errors) + return -1; + + return 0; +} diff --git util.c util.c new file mode 100644 index 0000000..4594f8a --- /dev/null +++ util.c @@ -0,0 +1,474 @@ +/* $OpenBSD:$ */ + +/* + * Copyright (c) 2017 Rafael Zalamena <rzalam...@openbsd.org> + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include <arpa/inet.h> + +#include <stdlib.h> +#include <string.h> + +#include "mcast-proxy.h" + +const char * +addrtostr(struct sockaddr_storage *ss) +{ + struct sockaddr_in *sin; + struct sockaddr_in6 *sin6; + static char buf[4][128]; + static unsigned int bufpos = 0; + + bufpos = (bufpos + 1) % 4; + + switch (ss->ss_family) { + case AF_INET: + sin = sstosin(ss); + inet_ntop(AF_INET, &sin->sin_addr, buf[bufpos], + sizeof(buf[bufpos])); + return buf[bufpos]; + case AF_INET6: + sin6 = sstosin6(ss); + buf[bufpos][0] = '['; + inet_ntop(AF_INET6, &sin6->sin6_addr, &buf[bufpos][1], + sizeof(buf[bufpos])); + strlcat(buf[bufpos], "]", sizeof(buf[bufpos])); + return buf[bufpos]; + + default: + return "unknown"; + } +} + +const char * +addr4tostr(struct in_addr *addr) +{ + struct sockaddr_storage ss; + + memset(&ss, 0, sizeof(ss)); + ss.ss_family = AF_INET; + ss.ss_len = sizeof(struct sockaddr_in); + sstosin(&ss)->sin_addr = *addr; + + return addrtostr(&ss); +} + +const char * +addr6tostr(struct in6_addr *addr) +{ + struct sockaddr_storage ss; + + memset(&ss, 0, sizeof(ss)); + ss.ss_family = AF_INET6; + ss.ss_len = sizeof(struct sockaddr_in6); + memcpy(&sstosin6(&ss)->sin6_addr, addr, sizeof(*addr)); + + return addrtostr(&ss); +} + +int +id_matchaddr4(struct intf_data *id, uint32_t addr) +{ + struct intf_addr *ia; + union uaddr addrorg, addrtgt, naddr; + + naddr.v4.s_addr = addr; + + /* Check for address in interface address list. */ + SLIST_FOREACH(ia, &id->id_ialist, ia_entry) { + if (ia->ia_af != AF_INET) + continue; + + applymask(AF_INET, &addrtgt, &naddr, ia->ia_prefixlen); + applymask(AF_INET, &addrorg, &ia->ia_addr, ia->ia_prefixlen); + if (memcmp(&addrorg, &addrtgt, sizeof(addrorg.v4)) == 0) + return 1; + } + + /* Check for address in the subnet address list. */ + SLIST_FOREACH(ia, &id->id_altnetlist, ia_entry) { + if (ia->ia_af != AF_INET) + continue; + + applymask(AF_INET, &addrtgt, &naddr, ia->ia_prefixlen); + applymask(AF_INET, &addrorg, &ia->ia_addr, ia->ia_prefixlen); + if (memcmp(&addrorg, &addrtgt, sizeof(addrorg.v4)) == 0) + return 1; + } + + return 0; +} + +int +id_matchaddr6(struct intf_data *id, struct in6_addr *addr) +{ + struct intf_addr *ia; + union uaddr addrorg, addrtgt, naddr; + + naddr.v6 = *addr; + + /* Check for address in interface address list. */ + SLIST_FOREACH(ia, &id->id_ialist, ia_entry) { + if (ia->ia_af != AF_INET6) + continue; + + applymask(AF_INET6, &addrtgt, &naddr, ia->ia_prefixlen); + applymask(AF_INET6, &addrorg, &ia->ia_addr, ia->ia_prefixlen); + if (memcmp(&addrorg, &addrtgt, sizeof(addrorg.v6)) == 0) + return 1; + } + + /* Check for address in the subnet address list. */ + SLIST_FOREACH(ia, &id->id_altnetlist, ia_entry) { + if (ia->ia_af != AF_INET6) + continue; + + applymask(AF_INET6, &addrtgt, &naddr, ia->ia_prefixlen); + applymask(AF_INET6, &addrorg, &ia->ia_addr, ia->ia_prefixlen); + if (memcmp(&addrorg, &addrtgt, sizeof(addrorg.v6)) == 0) + return 1; + } + + return 0; +} + +struct intf_data * +intf_lookupbyname(const char *ifname) +{ + struct intf_data *id; + + SLIST_FOREACH(id, &iflist, id_entry) { + if (strcmp(id->id_name, ifname) == 0) + return id; + } + + return NULL; +} + +struct intf_data * +intf_lookupbyindex(unsigned short index) +{ + struct intf_data *id; + + SLIST_FOREACH(id, &iflist, id_entry) { + if (id->id_index == index) + return id; + } + + return NULL; +} + +struct intf_data * +intf_lookupbyaddr4(uint32_t addr) +{ + struct intf_data *id; + + SLIST_FOREACH(id, &iflist, id_entry) { + if (id_matchaddr4(id, addr)) + return id; + } + + return NULL; +} + +struct intf_data * +intf_lookupbyaddr6(struct in6_addr *addr) +{ + struct intf_data *id; + + SLIST_FOREACH(id, &iflist, id_entry) { + if (id_matchaddr6(id, addr)) + return id; + } + + return NULL; +} + +struct intf_addr * +intf_primaryv4(struct intf_data *id) +{ + struct intf_addr *ia; + + SLIST_FOREACH(ia, &id->id_ialist, ia_entry) { + if (ia->ia_af != AF_INET) + continue; + + return ia; + } + + return NULL; +} + +struct intf_addr * +intf_ipv6linklayer(struct intf_data *id) +{ + struct intf_addr *ia; + + SLIST_FOREACH(ia, &id->id_ialist, ia_entry) { + if (ia->ia_af != AF_INET6) + continue; + if (!IN6_IS_ADDR_LINKLOCAL(&ia->ia_addr.v6)) + continue; + + return ia; + } + + return NULL; +} + +void +ia_inserttail(struct ialist *ial, struct intf_addr *ia) +{ + struct intf_addr *ian; + + SLIST_FOREACH(ian, ial, ia_entry) { + if (SLIST_NEXT(ian, ia_entry) == NULL) + break; + } + if (ian != NULL) + SLIST_INSERT_AFTER(ian, ia, ia_entry); + else + SLIST_INSERT_HEAD(ial, ia, ia_entry); +} + +struct intf_data * +id_new(void) +{ + struct intf_data *id; + + id = calloc(1, sizeof(*id)); + if (id == NULL) { + log_warn("%s: calloc", __func__); + return NULL; + } + + /* Default minimum TTL threshold. */ + id->id_ttl = 1; + + id->id_index = (unsigned short)-1; + id->id_vindex = INVALID_VINDEX; + id->id_vindex6 = INVALID_VINDEX; + SLIST_INSERT_HEAD(&iflist, id, id_entry); + + return id; +} + +struct intf_data * +id_insert(unsigned short index) +{ + struct intf_data *id; + + id = intf_lookupbyindex(index); + if (id != NULL) + return id; + + id = id_new(); + if (id == NULL) + return NULL; + + id->id_index = index; + + return id; +} + +void +id_free(struct intf_data *id) +{ + struct intf_addr *ia; + + if (id == NULL) + return; + + while (!SLIST_EMPTY(&id->id_ialist)) { + ia = SLIST_FIRST(&id->id_ialist); + SLIST_REMOVE(&id->id_ialist, ia, intf_addr, ia_entry); + free(ia); + } + while (!SLIST_EMPTY(&id->id_altnetlist)) { + ia = SLIST_FIRST(&id->id_altnetlist); + SLIST_REMOVE(&id->id_altnetlist, ia, intf_addr, ia_entry); + free(ia); + } + + SLIST_REMOVE(&iflist, id, intf_data, id_entry); + free(id); +} + +uint8_t +mask2prefixlen(in_addr_t ina) +{ + if (ina == 0) + return (0); + else + return (33 - ffs(ntohl(ina))); +} + +uint8_t +mask2prefixlen6(struct sockaddr_in6 *sa_in6) +{ + uint8_t l = 0, *ap, *ep; + + /* + * sin6_len is the size of the sockaddr so substract the offset of + * the possibly truncated sin6_addr struct. + */ + ap = (uint8_t *)&sa_in6->sin6_addr; + ep = (uint8_t *)sa_in6 + sa_in6->sin6_len; + for (; ap < ep; ap++) { + /* this "beauty" is adopted from sbin/route/show.c ... */ + switch (*ap) { + case 0xff: + l += 8; + break; + case 0xfe: + l += 7; + return (l); + case 0xfc: + l += 6; + return (l); + case 0xf8: + l += 5; + return (l); + case 0xf0: + l += 4; + return (l); + case 0xe0: + l += 3; + return (l); + case 0xc0: + l += 2; + return (l); + case 0x80: + l += 1; + return (l); + case 0x00: + return (l); + default: + fatalx("%s: non contiguous inet6 netmask", __func__); + } + } + + return (l); +} + +in_addr_t +prefixlen2mask(uint8_t prefixlen) +{ + if (prefixlen == 0) + return (0); + + return (htonl(0xffffffff << (32 - prefixlen))); +} + +void +applymask(int af, union uaddr *dest, const union uaddr *src, + int prefixlen) +{ + struct in6_addr mask; + int i; + + switch (af) { + case AF_INET: + dest->v4.s_addr = src->v4.s_addr & prefixlen2mask(prefixlen); + break; + case AF_INET6: + memset(&mask, 0, sizeof(mask)); + for (i = 0; i < prefixlen / 8; i++) + mask.s6_addr[i] = 0xff; + i = prefixlen % 8; + if (i) + mask.s6_addr[prefixlen / 8] = 0xff00 >> i; + + for (i = 0; i < 16; i++) + dest->v6.s6_addr[i] = src->v6.s6_addr[i] & + mask.s6_addr[i]; + break; + default: + fatalx("%s: unknown address family", __func__); + } +} + +/* Packet assembly code, originally contributed by Archie Cobbs. */ + +/* + * Copyright (c) 1995, 1996, 1999 The Internet Software Consortium. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of The Internet Software Consortium nor the names + * of its contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE INTERNET SOFTWARE CONSORTIUM AND + * CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE INTERNET SOFTWARE CONSORTIUM OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF + * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * + * This software has been written for the Internet Software Consortium + * by Ted Lemon <mel...@fugue.com> in cooperation with Vixie + * Enterprises. To learn more about the Internet Software Consortium, + * see ``http://www.vix.com/isc''. To learn more about Vixie + * Enterprises, see ``http://www.vix.com''. + */ + +uint16_t +checksum(uint8_t *buf, uint16_t nbytes, uint32_t sum) +{ + unsigned int i; + + /* Checksum all the pairs of bytes first... */ + for (i = 0; i < (nbytes & ~1U); i += 2) { + sum += (u_int16_t)ntohs(*((u_int16_t *)(buf + i))); + if (sum > 0xFFFF) + sum -= 0xFFFF; + } + + /* + * If there's a single byte left over, checksum it, too. + * Network byte order is big-endian, so the remaining byte is + * the high byte. + */ + if (i < nbytes) { + sum += buf[i] << 8; + if (sum > 0xFFFF) + sum -= 0xFFFF; + } + + return sum; +} + +uint16_t +wrapsum(uint16_t sum) +{ + sum = ~sum & 0xFFFF; + return htons(sum); +}