--- /dev/null
+/*-
+ * Copyright (c) 2015-2019 Alexandre Fenyo <alex@fenyo.net> - http://www.fenyo.net
+ * 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.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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.
+ */
+
+#include <sys/param.h>
+#include <sys/socket.h>
+#include <sys/kdb.h>
+#include <net/if.h>
+#include <net/pfil.h>
+#include <net/if_var.h>
+#include <net/ethernet.h>
+#include <netinet/in.h>
+#include <netinet/in_pcb.h>
+#include <netinet/icmp6.h>
+#include <netinet/ip6.h>
+#include <netinet6/in6_var.h>
+#include <netinet6/ip6_var.h>
+#include <netinet6/scope6_var.h>
+
+#include "ndpacket.h"
+#include "ndconf.h"
+#include "ndparse.h"
+
+// Reply to neighbor solicitations with a specific neighbor advertisement, in order
+// to let the uplink router send packets to a downlink router, that may or may not
+// be the current host that run ndproxy.
+// The current host, the uplink router and the downlink router must be attached
+// to a common layer-2 link with broadcast multi-access capability.
+
+// Neighbor solicitation messages are multicast to the solicited-node multicast
+// address of the target address. Since we do not know the target address,
+// we can not join the corresponding group. So, to capture the solicitation messages,
+// the uplink interface must be set in permanent promiscuous mode and MLD snooping
+// must be disabled on the switches that share the layer-2 link relative to
+// the uplink interface. Note that MLD snooping must not be disabled entirely on
+// each switch, but only on the corresponding vlan.
+
+// called by pfil_run_hooks() @ ip6_input.c:ip_input()
+
+#ifdef PFIL_VERSION
+pfil_return_t packet(struct mbuf **packet_mp, struct ifnet *packet_ifnet,
+ const int packet_dir, void *packet_arg, struct inpcb *packet_inpcb) {
+#else
+int packet(void *packet_arg, struct mbuf **packet_mp, struct ifnet *packet_ifnet,
+ const int packet_dir, struct inpcb *packet_inpcb) {
+#endif
+ struct mbuf *m = NULL, *mreply = NULL;
+ struct ip6_hdr *ip6, *ip6reply;
+ struct icmp6_hdr *icmp6;
+ struct in6_addr srcaddr, dstaddr;
+ int output_flags = 0;
+ int maxlen, ret, i;
+
+#ifdef DEBUG_NDPROXY
+ // when debuging, increment counter of received packets from the uplink interface
+ ndproxy_conf_count = ++ndproxy_conf_count < 0 ? 1 : ndproxy_conf_count;
+#endif
+
+ if (packet_mp == NULL) {
+ printf("NDPROXY ERROR: no mbuf\n");
+ return 0;
+ }
+ m = *packet_mp;
+
+ // locate start of IPv6 header data
+ ip6 = mtod(m, struct ip6_hdr *);
+ struct in6_addr ip6_src = ip6->ip6_src;
+
+ // handle only packets originating from the uplink interface
+ if (strcmp(if_name(packet_ifnet), ndproxy_conf_str_uplink_interface)) {
+#ifdef DEBUG_NDPROXY
+ printf("NDPROXY DEBUG: packets from uplink interface: %s - %d\n", if_name(packet_ifnet), ndproxy_conf_count);
+#endif
+ return 0;
+ }
+
+ // Handle only packets originating from one of the uplink router addresses.
+ // Note that different source addresses can be choosen from the same uplink router, depending on the packet
+ // that triggered the address resolution process or depending on other external factors.
+ // Here are some cases when it can happen:
+ // - the uplink router may have multiple interfaces;
+ // - there may be multiple uplink routers;
+ // - many routers choose to use a link-local address when sending neighbor solicitations,
+ // but when an administrator of such a router, also having a global address assigned on the same link,
+ // tries to send packets (echo request, for instance) to an on-link destination global address,
+ // the source address of the echo request packet prompting the solicitation may be global-scoped according
+ // to the selection algorithm described in RFC-6724. Therefore, the source address of the Neighbor Solicitation
+ // packet should also be selected in the same global scope, according to RFC-4861 (§7.2.2);
+ // - when the uplink router does not yet know its own address, it must use the unspecified address,
+ // according to RFC-4861.
+ // So, it can not be assumed that an uplink router will always use the same IPv6 address to send
+ // neighbor solicitations. Every assigned addresses to the downlink interface of the uplink router
+ // should then be declared to ndproxy via sysctl (net.inet6.ndproxyconf_uplink_ipv6_addresses).
+ // Since the unsolicited address can be used by many different nodes, another node than the uplink router could
+ // make use of such a source IP. This is why if such a node exists, the unsolicited address should not be
+ // declared in the net.inet6.ndproxyconf_uplink_ipv6_addresses sysctl configuration parameter.
+ for (i = 0; i < ndproxy_conf_uplink_ipv6_naddresses; i++) {
+#ifdef DEBUG_NDPROXY
+ printf("NDPROXY INFO: compare: ");
+ printf_ip6addr(ndproxy_conf_uplink_ipv6_addresses + i, false);
+ printf(" (uplink router address) with ");
+ printf_ip6addr(&ip6_src, false);
+ printf(" (source address)\n");
+#endif
+ if (IN6_ARE_ADDR_EQUAL(ndproxy_conf_uplink_ipv6_addresses + i, &ip6_src)) break;
+ }
+ if (i == ndproxy_conf_uplink_ipv6_naddresses) {
+#ifdef DEBUG_NDPROXY
+ printf("NDPROXY INFO: not from uplink router - from: ");
+ printf_ip6addr(&ip6_src, false);
+ printf(" - %d\n", ndproxy_conf_count);
+#endif
+ return 0;
+ }
+
+#ifdef DEBUG_NDPROXY
+ printf("NDPROXY DEBUG: got packet from uplink router - %d\n", ndproxy_conf_count);
+#endif
+
+ // For security reasons, we explicitely reject neighbor solicitation packets containing any extension header:
+ // such a packet is mainly unattended:
+ // Fragmentation:
+ // According to RFC-6980, IPv6 fragmentation header is forbidden in all neighbor discovery messages.
+ // Hop-by-hop header:
+ // commonly used for jumbograms or for MLD. Should not involve neighbor solicitation packets.
+ // Destination mobility headers:
+ // commonly used for mobility, we do not support these headers.
+ // Routing header:
+ // commonly used for mobility or source routing, we do not support these headers.
+ // AH & ESP headers:
+ // securing the neighbor discovery process is not done with IPsec but with the SEcure Neighbor
+ // Discovery protocol (RFC-3971). We can not support RFC-3971, since proxifying ND packets is
+ // some kind of a spoofing process.
+ // look for an ICMPv6 payload and no IPv6 extension header
+ if (ip6->ip6_nxt != IPPROTO_ICMPV6) return 0;
+#ifdef DEBUG_NDPROXY
+ printf("NDPROXY DEBUG: got ICMPv6 from uplink router - %d\n", ndproxy_conf_count);
+#endif
+
+ // locate start of ICMPv6 header data
+ icmp6 = (struct icmp6_hdr *) ((caddr_t) ip6 + sizeof(struct ip6_hdr));
+
+ // check the checksum
+ const u_int16_t sum = icmp6->icmp6_cksum;
+ icmp6->icmp6_cksum = 0;
+ if (sum != in6_cksum(m, IPPROTO_ICMPV6, sizeof(struct ip6_hdr),
+ m->m_len - sizeof(struct ip6_hdr))) {
+ icmp6->icmp6_cksum = sum;
+ printf("NDPROXY ERROR: bad checksum\n");
+ return 0;
+ }
+ icmp6->icmp6_cksum = sum;
+
+ if (icmp6->icmp6_type != ND_NEIGHBOR_SOLICIT || icmp6->icmp6_code) return 0;
+#ifdef DEBUG_NDPROXY
+ printf("NDPROXY DEBUG: got neighbor solicitation from ");
+ printf_ip6addr(&ip6_src, true);
+ printf("\n");
+#endif
+
+ // create a new mbuf to send a neighbor advertisement
+ // ICMPv6 options are rounded up to 8 bytes alignment
+ maxlen = (sizeof(struct ip6_hdr) + sizeof(struct nd_neighbor_advert) +
+ sizeof(struct nd_opt_hdr) + packet_ifnet->if_addrlen + 7) & ~7;
+ if (max_linkhdr + maxlen > MCLBYTES) {
+ printf("NDPROXY ERROR: reply length > MCLBYTES\n");
+ return 0;
+ }
+ if (max_linkhdr + maxlen > MHLEN)
+ mreply = m_getcl(M_NOWAIT, MT_DATA, M_PKTHDR);
+ else
+ mreply = m_gethdr(M_NOWAIT, MT_DATA);
+ if (mreply == NULL) {
+ printf("NDPROXY ERROR: no more mbufs (ENOBUFS)\n");
+ return 0;
+ }
+
+ // this is a newly created packet
+ mreply->m_pkthdr.rcvif = NULL;
+
+ // packet content:
+ // IPv6 header + ICMPv6 Neighbor Advertisement including target address + target link-layer ICMPv6 address option
+ mreply->m_pkthdr.len = mreply->m_len = (sizeof(struct ip6_hdr) + sizeof(struct nd_neighbor_advert)
+ + sizeof(struct nd_opt_hdr) + packet_ifnet->if_addrlen + 7) & ~7;
+
+ // reserve space for the link-layer header
+ mreply->m_data += max_linkhdr;
+
+ // fill in the destination address we want to reply to
+ struct sockaddr_in6 dst_sa;
+ bzero(&dst_sa, sizeof(struct sockaddr_in6));
+ dst_sa.sin6_family = AF_INET6;
+ dst_sa.sin6_len = sizeof(struct sockaddr_in6);
+ dst_sa.sin6_addr = ip6->ip6_src;
+ if ((ret = in6_setscope(&dst_sa.sin6_addr, packet_ifnet, NULL))) {
+ printf("NDPROXY ERROR: can not set source scope id (err=%d)\n", ret);
+ m_freem(mreply);
+ return 0;
+ }
+
+ // According to RFC-4861 (§7.2.4), "The Target Address of the advertisement is copied from the Target Address
+ // of the solicitation. [...] If the source of the solicitation is the unspecified address, the
+ // node MUST [...] multicast the advertisement to the all-nodes address.".
+ if (!IN6_IS_ADDR_UNSPECIFIED(&ip6_src)) dstaddr = ip6->ip6_src;
+ else {
+ // Check compliance to RFC-4861: "If the IP source address is the unspecified address, the IP
+ // destination address is a solicited-node multicast address.".
+ if (ip6->ip6_dst.s6_addr16[0] == IPV6_ADDR_INT16_MLL &&
+ ip6->ip6_dst.s6_addr32[1] == 0 &&
+ ip6->ip6_dst.s6_addr32[2] == IPV6_ADDR_INT32_ONE &&
+ ip6->ip6_dst.s6_addr8[12] == 0xff) {
+#ifdef DEBUG_NDPROXY
+ printf("NDPROXY DEBUG: unspecified source address and solicited-node multicast destination address\n");
+#endif
+ } else {
+ printf("NDPROXY ERROR: destination address should be a solicited-node multicast address\n");
+ m_freem(mreply);
+ return 0;
+ }
+
+ output_flags |= M_MCAST;
+ dstaddr = in6addr_linklocal_allnodes;
+ }
+ if ((ret = in6_setscope(&dstaddr, packet_ifnet, NULL))) {
+ printf("NDPROXY ERROR: can not set destination scope id (err=%d)\n", ret);
+ m_freem(mreply);
+ return 0;
+ }
+
+ // first, apply the RFC-3484 default address selection algorithm to get a source address for the advertisement packet.
+#if (__FreeBSD_version < 1100000)
+ ret = in6_selectsrc(&dst_sa, NULL, NULL, NULL, NULL, NULL, &srcaddr);
+#else
+ uint32_t _dst_sa_scopeid;
+ struct in6_addr _dst_sa;
+ in6_splitscope(&dst_sa.sin6_addr, &_dst_sa, &_dst_sa_scopeid);
+ ret = in6_selectsrc_addr(RT_DEFAULT_FIB, &_dst_sa,
+ _dst_sa_scopeid, packet_ifnet, &srcaddr, NULL);
+#endif
+ if (ret && (ret != EHOSTUNREACH || in6_addrscope(&ip6_src) == IPV6_ADDR_SCOPE_LINKLOCAL)) {
+ printf("NDPROXY ERROR: can not select a source address to reply (err=%d), source scope is %x\n",
+ ret, in6_addrscope(&ip6_src));
+ m_freem(mreply);
+ return 0;
+ }
+ if (ret) {
+ // secondly, try to reply with a link-local address attached to the receiving interface
+ struct in6_ifaddr *llifaddr = in6ifa_ifpforlinklocal(packet_ifnet, 0);
+ if (llifaddr == NULL)
+ printf("NDPROXY WARNING: no link-local address attached to the receiving interface\n");
+
+#ifdef DEBUG_NDPROXY
+ printf("NDPROXY INFO: no address in requested scope, using a link-local address to reply\n");
+#endif
+ if (llifaddr != NULL) {
+ // use the link-local address
+ srcaddr = (llifaddr->ia_addr).sin6_addr;
+ ifa_free((struct ifaddr *) llifaddr);
+ } else
+ // No link-local address, we may for instance currently be verifying that the link-local stateless
+ // autoconfiguration address is unused.
+ // Then, we temporary use the unspecified address (::).
+ bzero(&srcaddr, sizeof srcaddr);
+
+ // Since we have no source address in the same scope of the destination address of the request packet,
+ // we can not simply reply to the source address of the request packet.
+ // Then we reply to the link-local all nodes multicast address (ff02::1).
+ output_flags |= M_MCAST;
+ dstaddr = in6addr_linklocal_allnodes;
+ if ((ret = in6_setscope(&dstaddr, packet_ifnet, NULL))) {
+ printf("NDPROXY ERROR: can not set destination scope id (err=%d)\n", ret);
+ m_freem(mreply);
+ return 0;
+ }
+ }
+
+#ifdef DEBUG_NDPROXY
+ printf("NDPROXY DEBUG: source address used to reply: "); printf_ip6addr(&srcaddr, true); printf("\n");
+#endif
+
+ struct nd_neighbor_solicit *nd_ns = (struct nd_neighbor_solicit *) (ip6 + 1);
+ struct in6_addr nd_ns_target = nd_ns->nd_ns_target;
+
+ // fill in the IPv6 header
+ ip6reply = mtod(mreply, struct ip6_hdr *);
+ ip6reply->ip6_flow = 0;
+ ip6reply->ip6_vfc &= ~IPV6_VERSION_MASK;
+ ip6reply->ip6_vfc |= IPV6_VERSION;
+ ip6reply->ip6_plen = htons((u_short) (mreply->m_len - sizeof(struct ip6_hdr)));
+ ip6reply->ip6_nxt = IPPROTO_ICMPV6;
+ ip6reply->ip6_hlim = 255;
+ ip6reply->ip6_dst = dstaddr;
+ ip6reply->ip6_src = srcaddr;
+
+ // fill in the ICMPv6 neighbor advertisement header
+ struct nd_neighbor_advert *nd_na = (struct nd_neighbor_advert *) (ip6reply + 1);
+ nd_na->nd_na_type = ND_NEIGHBOR_ADVERT;
+ nd_na->nd_na_code = 0;
+
+ nd_na->nd_na_flags_reserved = 0;
+ // According to RFC-4861 (§7.2.4), "If the source of the solicitation is the unspecified address, the
+ // node MUST set the Solicited flag to zero [...]"
+ if (!IN6_IS_ADDR_UNSPECIFIED(&ip6_src)) nd_na->nd_na_flags_reserved = ND_NA_FLAG_SOLICITED;
+
+ // According to RFC-4861 (§7.2.4), "If the Target Address is either an anycast address or a unicast
+ // address for which the node is providing proxy service, [...] the Override flag SHOULD
+ // be set to zero."
+ // Thus, we do not set the ND_NA_FLAG_OVERRIDE flag in nd_na->nd_na_flags_reserved.
+
+ nd_na->nd_na_flags_reserved |= ND_NA_FLAG_ROUTER;
+
+ // according to RFC-4861 (§7.2.3), the target address can not be a multicast address
+ if (IN6_IS_ADDR_MULTICAST(&nd_ns_target)) {
+ printf("NDPROXY WARNING: rejecting multicast target address\n");
+ m_freem(mreply);
+ return 0;
+ }
+
+ // we send a solicited neighbor advertisement relative to the target contained in the received neighbor solicitation
+ nd_na->nd_na_target = nd_ns->nd_ns_target;
+ struct in6_addr nd_na_target = nd_na->nd_na_target;
+
+ // do not manage packets relative to exception target addresses
+ for (i = 0; i < ndproxy_conf_exception_ipv6_naddresses; i++)
+ if (IN6_ARE_ADDR_EQUAL(ndproxy_conf_exception_ipv6_addresses + i, &nd_na_target)) {
+#ifdef DEBUG_NDPROXY
+ printf("NDPROXY INFO: rejecting target\n");
+#endif
+ m_freem(mreply);
+ return 0;
+ } else {
+#ifdef DEBUG_NDPROXY
+ printf("NDPROXY INFO: accepting target: ");
+ printf_ip6addr(ndproxy_conf_exception_ipv6_addresses + i, false);
+ printf(" - ");
+ printf_ip6addr(&nd_na_target, false);
+ printf("\n");
+#endif
+ }
+
+ // proxy to the downlink router: fill in the target link-layer address option with the MAC downlink router address
+ int optlen = sizeof(struct nd_opt_hdr) + ETHER_ADDR_LEN;
+ struct nd_opt_hdr *nd_opt = (struct nd_opt_hdr *) (nd_na + 1);
+ // roundup to 8 bytes alignment
+ optlen = (optlen + 7) & ~7;
+ bzero((caddr_t) nd_opt, optlen);
+ nd_opt->nd_opt_type = ND_OPT_TARGET_LINKADDR;
+ nd_opt->nd_opt_len = optlen >> 3;
+ bcopy(&ndproxy_conf_downlink_mac_address, (caddr_t) (nd_opt + 1), ETHER_ADDR_LEN);
+#ifdef DEBUG_NDPROXY
+ printf("NDPROXY INFO: mac option: ");
+ printf_macaddr_network_format(&ndproxy_conf_downlink_mac_address);
+ printf("\n");
+#endif
+
+ // compute outgoing packet checksum
+ nd_na->nd_na_cksum = 0;
+ nd_na->nd_na_cksum = in6_cksum(mreply, IPPROTO_ICMPV6, sizeof(struct ip6_hdr),
+ mreply->m_len - sizeof(struct ip6_hdr));
+
+#ifdef DEBUG_NDPROXY
+ struct in6_addr ip6reply_ip6_src = ip6reply->ip6_src;
+ struct in6_addr ip6reply_ip6_dst = ip6reply->ip6_dst;
+ printf("NDPROXY DEBUG: src="); printf_ip6addr(&ip6reply_ip6_src, false); printf(" / ");
+ printf("dst="); printf_ip6addr(&ip6reply_ip6_dst, false); printf("\n");
+#endif
+
+ struct ip6_moptions im6o;
+ if (output_flags & M_MCAST) {
+ bzero(&im6o, sizeof im6o);
+ im6o.im6o_multicast_hlim = 255;
+ im6o.im6o_multicast_loop = false;
+ im6o.im6o_multicast_ifp = NULL;
+ }
+
+ // send router advertisement
+ if ((ret = ip6_output(mreply, NULL, NULL, output_flags, output_flags & M_MCAST ? &im6o : NULL, NULL, NULL))) {
+ printf("NDPROXY DEBUG: can not send packet (err=%d)\n", ret);
+#ifdef DEBUG_NDPROXY
+ kdb_backtrace();
+ return 0;
+#endif
+ } else {
+#ifdef DEBUG_NDPROXY
+ printf("NDPROXY DEBUG: reply sent\n");
+#endif
+ }
+
+#ifndef DEBUG_NDPROXY
+ // when NOT debuging, increment counter for each neighbor advertisement sent
+ ndproxy_conf_count = ++ndproxy_conf_count < 0 ? 1 : ndproxy_conf_count;
+#endif
+
+ // do not process this packet by upper layers to avoid sending another advertissement
+ m_freem(m);
+ *packet_mp = NULL;
+ return 1;
+}