/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "macosroutemonitor.h"

#include <arpa/inet.h>
#include <errno.h>
#include <ifaddrs.h>
#include <net/if.h>
#include <net/if_dl.h>
#include <net/if_types.h>
#include <net/route.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>

#include <QCoreApplication>
#include <QProcess>
#include <QScopeGuard>
#include <QTimer>

#include "leakdetector.h"
#include "logger.h"

namespace {
Logger logger("MacosRouteMonitor");
}  // namespace

MacosRouteMonitor::MacosRouteMonitor(const QString& ifname, QObject* parent)
    : QObject(parent), m_ifname(ifname) {
  MZ_COUNT_CTOR(MacosRouteMonitor);
  logger.debug() << "MacosRouteMonitor created.";

  m_rtsock = socket(PF_ROUTE, SOCK_RAW, 0);
  if (m_rtsock < 0) {
    logger.error() << "Failed to create routing socket:" << strerror(errno);
    return;
  }

  m_ifindex = if_nametoindex(qPrintable(ifname));

  m_notifier = new QSocketNotifier(m_rtsock, QSocketNotifier::Read, this);
  connect(m_notifier, &QSocketNotifier::activated, this,
          &MacosRouteMonitor::rtsockReady);

  // Grab the default routes at startup.
  rtmFetchRoutes(AF_INET);
  rtmFetchRoutes(AF_INET6);
}

MacosRouteMonitor::~MacosRouteMonitor() {
  MZ_COUNT_DTOR(MacosRouteMonitor);
  flushExclusionRoutes();
  if (m_rtsock >= 0) {
    close(m_rtsock);
  }
  logger.debug() << "MacosRouteMonitor destroyed.";
}

// Compare memory against zero.
static int memcmpzero(const void* data, size_t len) {
  const quint8* ptr = static_cast<const quint8*>(data);
  while (len--) {
    if (*ptr++) return 1;
  }
  return 0;
}

void MacosRouteMonitor::handleRtmDelete(const struct rt_msghdr* rtm,
                                        const QByteArray& payload) {
  QList<QByteArray> addrlist = parseAddrList(payload);

  // Ignore routing changes on the tunnel interface.
  if (rtm->rtm_index == m_ifindex) {
    return;
  }

  QStringList list;
#ifdef MZ_DEBUG
  for (auto addr : addrlist) {
    list.append(addrToString(addr));
  }
#endif
  char ifname[IF_NAMESIZE] = "null";
  if (rtm->rtm_index != 0) {
    if_indextoname(rtm->rtm_index, ifname);
  }
  logger.debug() << "Route deleted via" << ifname
                 << QString("addrs(%1):").arg(rtm->rtm_addrs, 0, 16)
                 << list.join(" ");

  // We expect all useful routes to contain a destination, netmask and gateway.
  if (!(rtm->rtm_addrs & RTA_DST) || !(rtm->rtm_addrs & RTA_GATEWAY) ||
      !(rtm->rtm_addrs & RTA_NETMASK) || (addrlist.count() < 3)) {
    return;
  }
  // Ignore interface-scoped routes, we want to find the default route to the
  // internet in the global scope.
  if (rtm->rtm_flags & RTF_IFSCOPE) {
    return;
  }

  // Check for a default route, which should have a netmask of zero.
  const struct sockaddr* sa =
      reinterpret_cast<const struct sockaddr*>(addrlist[2].constData());
  if (sa->sa_family == AF_INET) {
    struct sockaddr_in sin;
    Q_ASSERT(sa->sa_len <= sizeof(sin));
    memset(&sin, 0, sizeof(sin));
    memcpy(&sin, sa, sa->sa_len);
    if (memcmpzero(&sin.sin_addr, sizeof(sin.sin_addr)) != 0) {
      return;
    }
  } else if (sa->sa_family == AF_INET6) {
    struct sockaddr_in6 sin6;
    Q_ASSERT(sa->sa_len <= sizeof(sin6));
    memset(&sin6, 0, sizeof(sin6));
    memcpy(&sin6, sa, sa->sa_len);
    if (memcmpzero(&sin6.sin6_addr, sizeof(sin6.sin6_addr)) != 0) {
      return;
    }
  } else if (sa->sa_family != AF_UNSPEC) {
    // We have sometimes seen the default route reported with AF_UNSPEC.
    return;
  }

  // Clear the default gateway
  const struct sockaddr* dst =
      reinterpret_cast<const struct sockaddr*>(addrlist[0].constData());
  QAbstractSocket::NetworkLayerProtocol protocol;
  if (dst->sa_family == AF_INET) {
    m_defaultGatewayIpv4.clear();
    m_defaultIfindexIpv4 = 0;
    protocol = QAbstractSocket::IPv4Protocol;
  } else if (dst->sa_family == AF_INET6) {
    m_defaultGatewayIpv6.clear();
    m_defaultIfindexIpv6 = 0;
    protocol = QAbstractSocket::IPv6Protocol;
  }

  logger.debug() << "Lost default route via" << ifname
                 << logger.sensitive(addrToString(addrlist[1]));
  for (const IPAddress& prefix : m_exclusionRoutes) {
    if (prefix.address().protocol() == protocol) {
      logger.debug() << "Removing exclusion route to"
                     << prefix.toString();
      rtmSendRoute(RTM_DELETE, prefix, rtm->rtm_index, nullptr);
    }
  }
}

void MacosRouteMonitor::handleRtmUpdate(const struct rt_msghdr* rtm,
                                        const QByteArray& payload) {
  QList<QByteArray> addrlist = parseAddrList(payload);
  int ifindex = rtm->rtm_index;
  char ifname[IF_NAMESIZE] = "null";

  // We expect all useful routes to contain a destination, netmask and gateway.
  if (!(rtm->rtm_addrs & RTA_DST) || !(rtm->rtm_addrs & RTA_GATEWAY) ||
      !(rtm->rtm_addrs & RTA_NETMASK) || (addrlist.count() < 3)) {
    return;
  }
  // Ignore interface-scoped routes, we want to find the default route to the
  // internet in the global scope.
  if (rtm->rtm_flags & RTF_IFSCOPE) {
    return;
  }
  // Ignore route changes that we caused, or routes on the tunnel interface.
  if (rtm->rtm_index == m_ifindex) {
    return;
  }
  if ((rtm->rtm_pid == getpid()) && (rtm->rtm_type != RTM_GET)) {
    return;
  }

  // Special case: If RTA_IFP is set, then we should get the interface index
  // from the address list instead of rtm_index.
  if (rtm->rtm_addrs & RTA_IFP) {
    int addridx = 0;
    for (int mask = 1; mask < RTA_IFP; mask <<= 1) {
      if (rtm->rtm_addrs & mask) {
        addridx++;
      }
    }
    if (addridx >= addrlist.count()) {
      return;
    }
    const char* sdl_data = addrlist[addridx].constData();
    const struct sockaddr_dl* sdl =
        reinterpret_cast<const struct sockaddr_dl*>(sdl_data);
    if (sdl->sdl_family == AF_LINK) {
      ifindex = sdl->sdl_index;
    }
  }

  // Log relevant updates to the routing table.
  QStringList list;
#ifdef MZ_DEBUG
  for (auto addr : addrlist) {
    list.append(addrToString(addr));
  }
#endif
  if_indextoname(ifindex, ifname);
  logger.debug() << "Route update via" << ifname
                 << QString("addrs(%1):").arg(rtm->rtm_addrs, 0, 16)
                 << list.join(" ");

  // Check for a default route, which should have a netmask of zero.
  const struct sockaddr* sa =
      reinterpret_cast<const struct sockaddr*>(addrlist[2].constData());
  if (sa->sa_family == AF_INET) {
    struct sockaddr_in sin;
    Q_ASSERT(sa->sa_len <= sizeof(sin));
    memset(&sin, 0, sizeof(sin));
    memcpy(&sin, sa, sa->sa_len);
    if (memcmpzero(&sin.sin_addr, sizeof(sin.sin_addr)) != 0) {
      return;
    }
  } else if (sa->sa_family == AF_INET6) {
    struct sockaddr_in6 sin6;
    Q_ASSERT(sa->sa_len <= sizeof(sin6));
    memset(&sin6, 0, sizeof(sin6));
    memcpy(&sin6, sa, sa->sa_len);
    if (memcmpzero(&sin6.sin6_addr, sizeof(sin6.sin6_addr)) != 0) {
      return;
    }
  } else if (sa->sa_family != AF_UNSPEC) {
    // The default route sometimes sets a netmask of AF_UNSPEC.
    return;
  }

  // Determine if this is the IPv4 or IPv6 default route.
  const struct sockaddr* dst =
      reinterpret_cast<const struct sockaddr*>(addrlist[0].constData());
  QAbstractSocket::NetworkLayerProtocol protocol;
  int rtm_type = RTM_ADD;
  if (dst->sa_family == AF_INET) {
    if (m_defaultIfindexIpv4 != 0) {
      rtm_type = RTM_CHANGE;
    }
    m_defaultGatewayIpv4 = addrlist[1];
    m_defaultIfindexIpv4 = ifindex;
    protocol = QAbstractSocket::IPv4Protocol;
  } else if (dst->sa_family == AF_INET6) {
    if (m_defaultIfindexIpv6 != 0) {
      rtm_type = RTM_CHANGE;
    }
    m_defaultGatewayIpv6 = addrlist[1];
    m_defaultIfindexIpv6 = ifindex;
    protocol = QAbstractSocket::IPv6Protocol;
  } else {
    return;
  }

  // Update the exclusion routes with the new default route.
  logger.debug() << "Updating default route via" << ifname
                 << addrToString(addrlist[1]);
  for (const IPAddress& prefix : m_exclusionRoutes) {
    if (prefix.address().protocol() == protocol) {
      logger.debug() << "Updating exclusion route to"
                     << prefix.toString();
      rtmSendRoute(rtm_type, prefix, ifindex, addrlist[1].constData());
    }
  }
}

void MacosRouteMonitor::handleIfaceInfo(const struct if_msghdr* ifm,
                                        const QByteArray& payload) {
  QStringList list;

  if (ifm->ifm_index != if_nametoindex(qPrintable(m_ifname))) {
    return;
  }
  m_ifflags = ifm->ifm_flags;

#ifdef MZ_DEBUG
  QList<QByteArray> addrlist = parseAddrList(payload);
  for (auto addr : addrlist) {
    list.append(addrToString(addr));
  }
#else
  Q_UNUSED(payload);
#endif
  logger.debug() << "Interface" << ifm->ifm_index
                 << "chagned flags:" << ifm->ifm_flags
                 << QString("addrs(%1):").arg(ifm->ifm_addrs, 0, 16)
                 << list.join(" ");
}

void MacosRouteMonitor::rtsockReady() {
  char buf[1024];
  ssize_t len = recv(m_rtsock, buf, sizeof(buf), MSG_DONTWAIT);
  if (len <= 0) {
    return;
  }

#ifndef RTMSG_NEXT
#  define RTMSG_NEXT(_rtm_) \
    (struct rt_msghdr*)((char*)(_rtm_) + (_rtm_)->rtm_msglen)
#endif

  struct rt_msghdr* rtm = reinterpret_cast<struct rt_msghdr*>(buf);
  struct rt_msghdr* end = reinterpret_cast<struct rt_msghdr*>(&buf[len]);
  while (rtm < end) {
    // Ensure the message fits within the buffer
    if (RTMSG_NEXT(rtm) > end) {
      logger.debug() << "Routing message overflowed with length"
                     << rtm->rtm_msglen;
      break;
    }

    // Handle the routing message.
    QByteArray message((char*)rtm, rtm->rtm_msglen);
    switch (rtm->rtm_type) {
      case RTM_ADD:
        message.remove(0, sizeof(struct rt_msghdr));
        handleRtmUpdate(rtm, message);
        break;
      case RTM_DELETE:
        message.remove(0, sizeof(struct rt_msghdr));
        handleRtmDelete(rtm, message);
        break;
      case RTM_CHANGE:
        message.remove(0, sizeof(struct rt_msghdr));
        handleRtmUpdate(rtm, message);
        break;
      case RTM_GET:
        message.remove(0, sizeof(struct rt_msghdr));
        handleRtmUpdate(rtm, message);
        break;
      case RTM_IFINFO:
        message.remove(0, sizeof(struct if_msghdr));
        handleIfaceInfo((struct if_msghdr*)rtm, message);
        break;
      default:
        break;
    }

    rtm = RTMSG_NEXT(rtm);
  }
}

void MacosRouteMonitor::rtmAppendAddr(struct rt_msghdr* rtm, size_t maxlen,
                                      int rtaddr, const void* sa) {
  size_t sa_len = static_cast<const struct sockaddr*>(sa)->sa_len;
  Q_ASSERT((rtm->rtm_addrs & rtaddr) == 0);
  if ((rtm->rtm_msglen + sa_len) > maxlen) {
    return;
  }

  memcpy((char*)rtm + rtm->rtm_msglen, sa, sa_len);
  rtm->rtm_addrs |= rtaddr;
  rtm->rtm_msglen += sa_len;
  if (rtm->rtm_msglen % sizeof(uint32_t)) {
    rtm->rtm_msglen += sizeof(uint32_t) - (rtm->rtm_msglen % sizeof(uint32_t));
  }
}

bool MacosRouteMonitor::rtmSendRoute(int action, const IPAddress& prefix,
                                     unsigned int ifindex, const void* gateway,
                                     int flags) {
  constexpr size_t rtm_max_size = sizeof(struct rt_msghdr) +
                                  sizeof(struct sockaddr_in6) * 2 +
                                  sizeof(struct sockaddr_storage);
  char buf[rtm_max_size] = {0};
  struct rt_msghdr* rtm = reinterpret_cast<struct rt_msghdr*>(buf);

  rtm->rtm_msglen = sizeof(struct rt_msghdr);
  rtm->rtm_version = RTM_VERSION;
  rtm->rtm_type = action;
  rtm->rtm_index = ifindex;
  rtm->rtm_flags = flags | RTF_STATIC | RTF_UP;
  rtm->rtm_addrs = 0;
  rtm->rtm_pid = 0;
  rtm->rtm_seq = m_rtseq++;
  rtm->rtm_errno = 0;
  rtm->rtm_inits = 0;
  memset(&rtm->rtm_rmx, 0, sizeof(rtm->rtm_rmx));

  // Append RTA_DST
  if (prefix.address().protocol() == QAbstractSocket::IPv6Protocol) {
    struct sockaddr_in6 sin6;
    Q_IPV6ADDR dst = prefix.address().toIPv6Address();
    memset(&sin6, 0, sizeof(sin6));
    sin6.sin6_family = AF_INET6;
    sin6.sin6_len = sizeof(sin6);
    memcpy(&sin6.sin6_addr, &dst, 16);
    rtmAppendAddr(rtm, rtm_max_size, RTA_DST, &sin6);
  } else {
    struct sockaddr_in sin;
    quint32 dst = prefix.address().toIPv4Address();
    memset(&sin, 0, sizeof(sin));
    sin.sin_family = AF_INET;
    sin.sin_len = sizeof(sin);
    sin.sin_addr.s_addr = htonl(dst);
    rtmAppendAddr(rtm, rtm_max_size, RTA_DST, &sin);
  }

  // Append RTA_GATEWAY
  if (gateway != nullptr) {
    int family = static_cast<const struct sockaddr*>(gateway)->sa_family;
    if ((family == AF_INET) || (family == AF_INET6)) {
      rtm->rtm_flags |= RTF_GATEWAY;
    }
    rtmAppendAddr(rtm, rtm_max_size, RTA_GATEWAY, gateway);
  }

  // Append RTA_NETMASK
  unsigned int plen = prefix.prefixLength();
  if (prefix.address().protocol() == QAbstractSocket::IPv6Protocol) {
    struct sockaddr_in6 sin6;
    memset(&sin6, 0, sizeof(sin6));
    sin6.sin6_family = AF_INET6;
    sin6.sin6_len = sizeof(sin6);
    memset(&sin6.sin6_addr.s6_addr, 0xff, plen / 8);
    if (plen % 8) {
      sin6.sin6_addr.s6_addr[plen / 8] = 0xFF ^ (0xFF >> (plen % 8));
    }
    rtmAppendAddr(rtm, rtm_max_size, RTA_NETMASK, &sin6);
  } else if (prefix.address().protocol() == QAbstractSocket::IPv4Protocol) {
    struct sockaddr_in sin;
    memset(&sin, 0, sizeof(sin));
    sin.sin_family = AF_INET;
    sin.sin_len = sizeof(struct sockaddr_in);
    sin.sin_addr.s_addr = 0xffffffff;
    if (plen < 32) {
      sin.sin_addr.s_addr ^= htonl(0xffffffff >> plen);
    }
    rtmAppendAddr(rtm, rtm_max_size, RTA_NETMASK, &sin);
  }

  // Send the routing message to the kernel.
  int len = write(m_rtsock, rtm, rtm->rtm_msglen);
  if (len == rtm->rtm_msglen) {
    return true;
  }
  if ((action == RTM_ADD) && (errno == EEXIST)) {
    return true;
  }
  if ((action == RTM_DELETE) && (errno == ESRCH)) {
    return true;
  }
  logger.warning() << "Failed to send routing message:" << strerror(errno);
  return false;
}

bool MacosRouteMonitor::rtmFetchRoutes(int family) {
  constexpr size_t rtm_max_size =
      sizeof(struct rt_msghdr) + sizeof(struct sockaddr_storage) * 2;
  char buf[rtm_max_size] = {0};
  struct rt_msghdr* rtm = reinterpret_cast<struct rt_msghdr*>(buf);

  rtm->rtm_msglen = sizeof(struct rt_msghdr);
  rtm->rtm_version = RTM_VERSION;
  rtm->rtm_type = RTM_GET;
  rtm->rtm_flags = RTF_UP | RTF_GATEWAY;
  rtm->rtm_addrs = 0;
  rtm->rtm_pid = 0;
  rtm->rtm_seq = m_rtseq++;
  rtm->rtm_errno = 0;
  rtm->rtm_inits = 0;
  memset(&rtm->rtm_rmx, 0, sizeof(rtm->rtm_rmx));

  if (family == AF_INET) {
    struct sockaddr_in sin;
    memset(&sin, 0, sizeof(struct sockaddr_in));
    sin.sin_family = AF_INET;
    sin.sin_len = sizeof(struct sockaddr_in);
    rtmAppendAddr(rtm, rtm_max_size, RTA_DST, &sin);
    rtmAppendAddr(rtm, rtm_max_size, RTA_NETMASK, &sin);
  } else if (family == AF_INET6) {
    struct sockaddr_in6 sin6;
    memset(&sin6, 0, sizeof(struct sockaddr_in6));
    sin6.sin6_family = AF_INET6;
    sin6.sin6_len = sizeof(struct sockaddr_in6);
    rtmAppendAddr(rtm, rtm_max_size, RTA_DST, &sin6);
    rtmAppendAddr(rtm, rtm_max_size, RTA_NETMASK, &sin6);
  } else {
    logger.warning() << "Unsupported address family";
    return false;
  }

  // Send the routing message into the kernel.
  int len = write(m_rtsock, rtm, rtm->rtm_msglen);
  if (len == rtm->rtm_msglen) {
    return true;
  }
  logger.warning() << "Failed to request routing table:" << strerror(errno);
  return false;
}

bool MacosRouteMonitor::insertRoute(const IPAddress& prefix, int flags) {
  struct sockaddr_dl datalink;
  memset(&datalink, 0, sizeof(datalink));
  datalink.sdl_family = AF_LINK;
  datalink.sdl_len = offsetof(struct sockaddr_dl, sdl_data) + m_ifname.length();
  datalink.sdl_index = m_ifindex;
  datalink.sdl_type = IFT_OTHER;
  datalink.sdl_nlen = m_ifname.length();
  datalink.sdl_alen = 0;
  datalink.sdl_slen = 0;
  memcpy(&datalink.sdl_data, qPrintable(m_ifname), datalink.sdl_nlen);

  return rtmSendRoute(RTM_ADD, prefix, m_ifindex, &datalink, flags);
}

bool MacosRouteMonitor::deleteRoute(const IPAddress& prefix, int flags) {
  return rtmSendRoute(RTM_DELETE, prefix, m_ifindex, nullptr, flags);
}

bool MacosRouteMonitor::addExclusionRoute(const IPAddress& prefix) {
  logger.debug() << "Adding exclusion route for" << prefix.toString();

  if (m_exclusionRoutes.contains(prefix)) {
    logger.warning() << "Exclusion route already exists";
    return false;
  }
  m_exclusionRoutes.append(prefix);

  // If the default route is known, then updte the routing table immediately.
  if ((prefix.address().protocol() == QAbstractSocket::IPv4Protocol) &&
      (m_defaultIfindexIpv4 != 0) && !m_defaultGatewayIpv4.isEmpty()) {
    return rtmSendRoute(RTM_ADD, prefix, m_defaultIfindexIpv4,
                        m_defaultGatewayIpv4.constData());
  }
  if ((prefix.address().protocol() == QAbstractSocket::IPv6Protocol) &&
      (m_defaultIfindexIpv6 != 0) && !m_defaultGatewayIpv6.isEmpty()) {
    return rtmSendRoute(RTM_ADD, prefix, m_defaultIfindexIpv6,
                        m_defaultGatewayIpv6.constData());
  }

  // Otherwise, the default route isn't known yet. Do nothing.
  return true;
}

bool MacosRouteMonitor::deleteExclusionRoute(const IPAddress& prefix) {
  logger.debug() << "Deleting exclusion route for" << prefix.toString();

  m_exclusionRoutes.removeAll(prefix);
  if (prefix.address().protocol() == QAbstractSocket::IPv4Protocol) {
    return rtmSendRoute(RTM_DELETE, prefix, m_defaultIfindexIpv4, nullptr);
  } else if (prefix.address().protocol() == QAbstractSocket::IPv6Protocol) {
    return rtmSendRoute(RTM_DELETE, prefix, m_defaultIfindexIpv6, nullptr);
  } else {
    return false;
  }
}

void MacosRouteMonitor::flushExclusionRoutes() {
  while (!m_exclusionRoutes.isEmpty()) {
    IPAddress prefix = m_exclusionRoutes.takeFirst();
    if (prefix.address().protocol() == QAbstractSocket::IPv4Protocol) {
      rtmSendRoute(RTM_DELETE, prefix, m_defaultIfindexIpv4, nullptr);
    } else if (prefix.address().protocol() == QAbstractSocket::IPv6Protocol) {
      rtmSendRoute(RTM_DELETE, prefix, m_defaultIfindexIpv6, nullptr);
    }
  }
}

// static
QList<QByteArray> MacosRouteMonitor::parseAddrList(const QByteArray& payload) {
  QList<QByteArray> list;
  int offset = 0;
  constexpr int minlen = offsetof(struct sockaddr, sa_len) + sizeof(u_short);

  while ((offset + minlen) <= payload.length()) {
    struct sockaddr* sa = (struct sockaddr*)(payload.constData() + offset);
    int paddedSize = sa->sa_len;
    if (!paddedSize || (paddedSize % sizeof(uint32_t))) {
      paddedSize += sizeof(uint32_t) - (paddedSize % sizeof(uint32_t));
    }
    if ((offset + paddedSize) > payload.length()) {
      break;
    }
    list.append(payload.mid(offset, paddedSize));
    offset += paddedSize;
  }
  return list;
}

// static
QString MacosRouteMonitor::addrToString(const struct sockaddr* sa) {
  if (sa->sa_family == AF_INET) {
    const struct sockaddr_in* sin = (const struct sockaddr_in*)sa;
    return QString(inet_ntoa(sin->sin_addr));
  }
  if (sa->sa_family == AF_INET6) {
    const struct sockaddr_in6* sin6 = (const struct sockaddr_in6*)sa;
    char buf[INET6_ADDRSTRLEN];
    return QString(inet_ntop(AF_INET6, &sin6->sin6_addr, buf, sizeof(buf)));
  }
  if (sa->sa_family == AF_LINK) {
    const struct sockaddr_dl* sdl = (const struct sockaddr_dl*)sa;
    return QString("link#%1:").arg(sdl->sdl_index) + QString(link_ntoa(sdl));
  }
  if (sa->sa_family == AF_UNSPEC) {
    return QString("unspec");
  }
  return QString("unknown(af=%1)").arg(sa->sa_family);
}

// static
QString MacosRouteMonitor::addrToString(const QByteArray& data) {
  const struct sockaddr* sa = (const struct sockaddr*)data.constData();
  Q_ASSERT(sa->sa_len <= data.length());
  return addrToString(sa);
}
