llarp/vpn/apple.hpp

Namespaces

Name
llarp
[crypto.hpp]
llarp::vpn
llarp::vpn::detail
apple_route_manager

Classes

Name
class llarp::vpn::AppleInterface
class llarp::vpn::AppleRouteManager
class llarp::vpn::ApplePlatform

Source code

#pragma once

#include "platform.hpp"
#include "common.hpp"
#include <llarp/util/bits.hpp>
#include <llarp/util/fd.hpp>
#include <llarp/util/logging.hpp>
#include <llarp/util/non_blocking.hpp>
#include <llarp/util/thread/threading.hpp>

#include <sys/kern_control.h>
#include <sys/sys_domain.h>
#include <sys/kern_event.h>

#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <sys/sysctl.h>
#include <sys/param.h>
#include <sys/uio.h>

#include <arpa/inet.h>
#include <net/if.h>
#include <net/if_var.h>
#include <net/if_types.h>
#include <net/route.h>
#include <netinet/if_ether.h>
#include <netinet/in.h>

#include <fcntl.h>
#include <ifaddrs.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <algorithm>
#include <variant>
#include <vector>

namespace llarp::vpn
{

  namespace
  {
    static void
    Exec(const std::string& cmd, bool must_succeed = true)
    {
      static auto logcat = log::Cat("vpn.apple");
      log::info(logcat, "exec: {}", cmd);
      if (int ret = ::system(cmd.c_str()); ret != 0 and must_succeed)
        throw std::runtime_error{"command failed (" + std::to_string(ret) + "): " + cmd};
    }
  }  // namespace

  // The lo0 host route for the tun address must be removed when the daemon
  // exits, but the AppleInterface object is retained by event-loop handles
  // beyond context teardown, so its destructor does not reliably run before
  // process exit.  Track added routes and flush them via atexit(3); the
  // handler must not log, as logging may already be torn down by then.
  namespace detail
  {
    util::Mutex g_routes_mutex;

    inline std::vector<std::string>&
    _Routes()
    {
      // Deliberately heap-allocated and never freed: atexit handlers and
      // static destructors run interleaved in reverse registration order, so
      // a plain static vector could be destroyed before RouteFlush() runs and it
      // would then iterate a dead object.  A leaked vector has no destructor
      // to register, making Flush() safe no matter when it fires.
      static auto* routes = new std::vector<std::string>();
      return *routes;
    }

    template <typename Visit_t>
    void
    WithRoutes(Visit_t&& visit)
    {
      util::Lock lock{g_routes_mutex};
      visit(_Routes());
    }

    inline void
    RouteFlush()
    {
      WithRoutes([](auto& routes) {
        for (const auto& ip : routes)
        {
          Exec("/sbin/route -n delete -host " + ip + " -interface lo0", false);
        }
        routes.clear();
      });
    }

    inline void
    RouteTrack(std::string ip)
    {
      static bool registered = (::atexit(&RouteFlush), true);
      (void)registered;
      WithRoutes([ip = std::move(ip)](auto& routes) mutable { routes.push_back(std::move(ip)); });
    }
  }  // namespace detail

  // UTUN_OPT_IFNAME from <net/if_utun.h>, hardcoded so we don't depend on that
  // header existing in older SDKs.
  inline constexpr int apple_utun_opt_ifname = 2;

  class AppleInterface : public NetworkInterface
  {
    std::unique_ptr<util::FD> m_FD;

    friend class AppleRouteManager;

   public:
    AppleInterface(InterfaceInfo info)
        : NetworkInterface{std::move(info)}
        , m_FD{std::make_unique<util::FD>(::socket(PF_SYSTEM, SOCK_DGRAM, SYSPROTO_CONTROL))}
    {
      if (m_FD->fd() == -1)
        throw std::invalid_argument{"cannot open control socket: " + std::string{strerror(errno)}};

      ctl_info cinfo{};
      const std::string apple_utun = "com.apple.net.utun_control";
      std::copy_n(apple_utun.c_str(), apple_utun.size(), cinfo.ctl_name);

      {
        vpn::IOCTL ioc{*m_FD};
        ioc.ioctl(CTLIOCGINFO, &cinfo);
      }
      sockaddr_ctl addr{};
      addr.sc_id = cinfo.ctl_id;

      addr.sc_len = sizeof(addr);
      addr.sc_family = AF_SYSTEM;
      addr.ss_sysaddr = AF_SYS_CONTROL;
      addr.sc_unit = 0;  // kernel picks the first free utunN

      if (::connect(m_FD->fd(), (sockaddr*)&addr, sizeof(addr)) < 0)
      {
        m_FD.reset();
        throw std::runtime_error{
            "cannot connect to control socket address: " + std::string{strerror(errno)}};
      }
      uint32_t namesz = IFNAMSIZ;
      std::array<char, IFNAMSIZ + 1> name{};
      if (::getsockopt(m_FD->fd(), SYSPROTO_CONTROL, apple_utun_opt_ifname, name.data(), &namesz)
          < 0)
      {
        m_FD.reset();
        throw std::runtime_error{
            "cannot query for interface name: " + std::string{strerror(errno)}};
      }

      // make underlying file descriptor non blocking
      llarp::util::NonBlocking{*m_FD};

      auto& m_IfName = m_Info.ifname;
      m_IfName = name.data();
      m_Info.index = if_nametoindex(m_IfName.c_str());
      for (const auto& ifaddr : m_Info.addrs)
      {
        if (ifaddr.fam == AF_INET)
        {
          const huint32_t addr = net::ToHost(net::TruncateV6(ifaddr.range.addr));
          const huint32_t netmask = net::ToHost(net::TruncateV6(ifaddr.range.netmask_bits));
          const huint32_t daddr = addr & netmask;
          // utun is point-to-point only; use the OpenVPN-style "topology subnet"
          // trick: /32 p2p pair to the network base address, then route the whole
          // range at the interface.
          Exec(
              "/sbin/ifconfig " + m_IfName + " " + addr.ToString() + " " + daddr.ToString()
              + " mtu " + std::to_string(m_Info.mtu) + " netmask 255.255.255.255 up");
          Exec(
              "/sbin/route -n add -net " + daddr.ToString() + " -netmask " + netmask.ToString()
              + " -interface " + m_IfName);
          // our own tun address is local, macOS wants it via loopback:
          Exec("/sbin/route -n add -host " + addr.ToString() + " -interface lo0");
          detail::RouteTrack(addr.ToString());
        }
        else if (ifaddr.fam == AF_INET6)
        {
          const auto prefixlen = bits::count_bits(ifaddr.range.netmask_bits);
          Exec(
              "/sbin/ifconfig " + m_IfName + " inet6 " + ifaddr.range.addr.ToString()
              + " prefixlen " + std::to_string(prefixlen) + " up");
          Exec(
              "/sbin/route -n add -inet6 -net " + ifaddr.range.addr.ToString() + " -prefixlen "
                  + std::to_string(prefixlen) + " -interface " + m_IfName,
              false);
        }
      }
    }

    ~AppleInterface() override
    {
      // Normally unreachable before process exit (see apple_route_cleanup);
      // when it does run, clean up here and de-register so the atexit flush
      // does not delete the same routes twice.
      // WithRoutes aquires mutex.
      detail::WithRoutes([&](auto& routes) {
        for (const auto& ifaddr : m_Info.addrs)
        {
          if (ifaddr.fam == AF_INET)
          {
            const auto ip = net::TruncateV6(ifaddr.range.addr).ToString();
            Exec("/sbin/route -n delete -host " + ip + " -interface lo0", false);
            routes.erase(std::remove(routes.begin(), routes.end(), ip), routes.end());
          }
        }
      });
    }

    int
    PollFD() const override
    {
      return m_FD->fd();
    }

    net::IPPacket
    ReadNextPacket() override
    {
      constexpr int uintsize = sizeof(unsigned int);
      net::IPPacket pkt{net::IPPacket::MaxSize};

      // Each utun datagram is prefixed with a 4-byte address family header.
      unsigned int pktinfo = 0;
      std::array<iovec, 2> vecs = {iovec{&pktinfo, uintsize}, iovec{pkt.data(), pkt.size()}};
      auto sz = ::readv(m_FD->fd(), vecs.data(), vecs.size());
      if (sz >= uintsize)
      {
        pkt.truncate(sz - uintsize);  // shrink to actual size
      }
      else if (sz < 0)
      {
        if (errno == EAGAIN || errno == EWOULDBLOCK)
        {
          pkt.truncate(0);
          errno = 0;
        }
        else
          throw std::error_code{errno, std::system_category()};
      }
      else  // 0 <= sz < uintsize: short read of the AF header; drop it
      {
        pkt.truncate(0);
      }
      return pkt;
    }

    bool
    WritePacket(net::IPPacket pkt) override
    {
      static unsigned int af4 = htonl(AF_INET);
      static unsigned int af6 = htonl(AF_INET6);
      const void* af_ptr =
          pkt.IsV6() ? static_cast<const void*>(&af6) : static_cast<const void*>(&af4);
      size_t af_len = sizeof(unsigned int);

      std::array<iovec, 2> vecs = {
          iovec{const_cast<void*>(af_ptr), af_len},
          iovec{const_cast<byte_t*>(pkt.data()), pkt.size()}};
      ssize_t n = ::writev(m_FD->fd(), vecs.data(), vecs.size());
      if (n >= static_cast<ssize_t>(af_len))
      {
        n -= af_len;
        return static_cast<size_t>(n) == pkt.size();
      }
      return false;
    }
  };

  class AppleRouteManager : public IRouteManager
  {
    static std::string
    FamilyFlag(const net::ipaddr_t& ip)
    {
      if (std::holds_alternative<net::ipv6addr_t>(ip))
        return "-inet6 ";
      return "";
    }

   public:
    AppleRouteManager() = default;
    ~AppleRouteManager() override = default;

    // Add a first-hop hole-poke route to a specific IP via a physical gateway
    void
    AddRoute(net::ipaddr_t ip, net::ipaddr_t gateway) override
    {
      Exec(
          "/sbin/route -n add " + FamilyFlag(ip) + "-host " + llarp::net::ToString(ip) + " "
          + llarp::net::ToString(gateway));
    }

    void
    DelRoute(net::ipaddr_t ip, net::ipaddr_t gateway) override
    {
      Exec(
          "/sbin/route -n delete " + FamilyFlag(ip) + "-host " + llarp::net::ToString(ip) + " "
              + llarp::net::ToString(gateway),
          false);
    }

    // Capture all traffic at the tun interface WITHOUT touching the existing
    // default route: two /1 routes (plus four /2s for IPv6) are more specific
    // than "default", so the original gateway stays in the table for the
    // hole-poked first-hop routes added above.
    void
    AddDefaultRouteViaInterface(NetworkInterface& vpn) override
    {
      const auto& ifname = vpn.Info().ifname;
      for (const auto* range : {"0.0.0.0/1", "128.0.0.0/1"})
        Exec(std::string{"/sbin/route -n add -net "} + range + " -interface " + ifname);

      bool have_v6 = false;
      for (const auto& addr : vpn.Info().addrs)
        have_v6 |= (addr.fam == AF_INET6);
      if (have_v6)
        for (const auto* base : {"::", "4000::", "8000::", "c000::"})
          Exec(
              std::string{"/sbin/route -n add -inet6 -net "} + base + " -prefixlen 2 -interface "
                  + ifname,
              false);
    }

    void
    DelDefaultRouteViaInterface(NetworkInterface& vpn) override
    {
      const auto& ifname = vpn.Info().ifname;
      for (const auto* range : {"0.0.0.0/1", "128.0.0.0/1"})
        Exec(std::string{"/sbin/route -n delete -net "} + range + " -interface " + ifname, false);

      for (const auto* base : {"::", "4000::", "8000::", "c000::"})
        Exec(
            std::string{"/sbin/route -n delete -inet6 -net "} + base + " -prefixlen 2 -interface "
                + ifname,
            false);
    }

    // Add a route for a subnet via the VPN interface
    void
    AddRouteViaInterface(NetworkInterface& vpn, IPRange range) override
    {
      const auto& ifname = vpn.Info().ifname;
      if (range.IsV4())
        Exec(
            "/sbin/route -n add -net " + net::TruncateV6(range.addr).ToString() + " -netmask "
            + range.NetmaskString() + " -interface " + ifname);
      else
        Exec(
            "/sbin/route -n add -inet6 -net " + range.addr.ToString() + " -prefixlen "
            + std::to_string(bits::count_bits(range.netmask_bits)) + " -interface " + ifname);
    }

    void
    DelRouteViaInterface(NetworkInterface& vpn, IPRange range) override
    {
      const auto& ifname = vpn.Info().ifname;
      if (range.IsV4())
        Exec(
            "/sbin/route -n delete -net " + net::TruncateV6(range.addr).ToString() + " -netmask "
                + range.NetmaskString() + " -interface " + ifname,
            false);
      else
        Exec(
            "/sbin/route -n delete -inet6 -net " + range.addr.ToString() + " -prefixlen "
                + std::to_string(bits::count_bits(range.netmask_bits)) + " -interface " + ifname,
            false);
    }

    // Enumerate default-route gateways that do NOT live on the tun interface,
    // by dumping the routing table via the classic BSD sysctl interface.  The
    // route poker uses this to find the physical gateway for hole-poking.
    std::vector<net::ipaddr_t>
    GetGatewaysNotOnInterface(NetworkInterface& vpn) override
    {
      std::vector<net::ipaddr_t> gateways;
      const unsigned int tun_index = if_nametoindex(vpn.Info().ifname.c_str());

      int mib[6] = {CTL_NET, PF_ROUTE, 0, 0 /* all families */, NET_RT_FLAGS, RTF_GATEWAY};
      size_t needed = 0;
      if (::sysctl(mib, 6, nullptr, &needed, nullptr, 0) != 0)
        return gateways;
      std::vector<char> buf;
      buf.resize(needed);
      if (::sysctl(mib, 6, buf.data(), &needed, nullptr, 0) != 0)
        return gateways;

      // routing socket sockaddrs are packed with 4-byte alignment; sa_len == 0
      // still occupies one alignment unit
      constexpr auto align = sizeof(uint32_t);
      const auto sa_size = [](const sockaddr* sa) {
        return sa->sa_len ? ((sa->sa_len + align - 1) & ~(align - 1)) : align;
      };

      for (char* ptr = buf.data(); ptr + sizeof(rt_msghdr) <= buf.data() + needed;)
      {
        auto* rtm = reinterpret_cast<rt_msghdr*>(ptr);
        if (rtm->rtm_msglen == 0)
          break;
        char* addrs = ptr + sizeof(rt_msghdr);
        ptr += rtm->rtm_msglen;

        if (rtm->rtm_version != RTM_VERSION)
          continue;
        if ((rtm->rtm_flags & (RTF_UP | RTF_GATEWAY)) != (RTF_UP | RTF_GATEWAY))
          continue;
        if (rtm->rtm_index == tun_index)
          continue;  // route lives on our own interface
        if ((rtm->rtm_addrs & (RTA_DST | RTA_GATEWAY)) != (RTA_DST | RTA_GATEWAY))
          continue;

        const sockaddr* dst = nullptr;
        const sockaddr* gw = nullptr;
        char* sa_ptr = addrs;
        for (int i = 0; i < RTAX_MAX; i++)
        {
          if (not(rtm->rtm_addrs & (1 << i)))
            continue;
          auto* sa = reinterpret_cast<const sockaddr*>(sa_ptr);
          if (i == RTAX_DST)
            dst = sa;
          else if (i == RTAX_GATEWAY)
            gw = sa;
          sa_ptr += sa_size(sa);
        }
        if (not dst or not gw)
          continue;

        // only default routes (dst 0.0.0.0); the poker only consumes IPv4
        if (dst->sa_family != AF_INET or gw->sa_family != AF_INET)
          continue;
        if (reinterpret_cast<const sockaddr_in*>(dst)->sin_addr.s_addr != 0)
          continue;

        gateways.emplace_back(
            net::ipv4addr_t{reinterpret_cast<const sockaddr_in*>(gw)->sin_addr.s_addr});
      }
      return gateways;
    }
  };

  class ApplePlatform : public Platform
  {
    AppleRouteManager _routeManager{};

   public:
    std::shared_ptr<NetworkInterface>
    ObtainInterface(InterfaceInfo info, AbstractRouter*) override
    {
      return std::static_pointer_cast<NetworkInterface>(
          std::make_shared<AppleInterface>(std::move(info)));
    };

    IRouteManager&
    RouteManager() override
    {
      return _routeManager;
    }
  };
}  // namespace llarp::vpn

Updated on 2026-08-23 at 15:46:34 +0000