diff --git a/.drone.jsonnet b/.drone.jsonnet index 89cfddacb..e33d98612 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -197,12 +197,7 @@ local mac_builder(name, // basic system headers. WTF apple: 'export SDKROOT="$(xcrun --sdk macosx --show-sdk-path)"', 'ulimit -n 1024', // because macos sets ulimit to 256 for some reason yeah idk - 'mkdir build', - 'cd build', - 'cmake .. -DCMAKE_CXX_FLAGS=-fcolor-diagnostics -DCMAKE_BUILD_TYPE='+build_type+' ' + - (if werror then '-DWARNINGS_AS_ERRORS=ON ' else '') + cmake_extra, - 'VERBOSE=1 make -j' + jobs, - './test/testAll --use-colour yes', + './contrib/mac.sh' ] + extra_cmds, } ] @@ -280,9 +275,4 @@ local mac_builder(name, // Macos builds: mac_builder('macOS (Release)'), mac_builder('macOS (Debug)', build_type='Debug'), - mac_builder('macOS (Static)', cmake_extra='-DBUILD_STATIC_DEPS=ON -DBUILD_SHARED_LIBS=OFF -DSTATIC_LINK=ON -DDOWNLOAD_SODIUM=FORCE -DDOWNLOAD_CURL=FORCE -DDOWNLOAD_UV=FORCE', - extra_cmds=[ - '../contrib/ci/drone-check-static-libs.sh', - '../contrib/ci/drone-static-upload.sh' - ]), ] diff --git a/.swift-version b/.swift-version new file mode 100644 index 000000000..8ae03c119 --- /dev/null +++ b/.swift-version @@ -0,0 +1 @@ +5.4.2 diff --git a/CMakeLists.txt b/CMakeLists.txt index 8f982142c..bb2983c51 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,9 +5,14 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # Has to be set before `project()`, and ignored on non-macos: set(CMAKE_OSX_DEPLOYMENT_TARGET 10.12 CACHE STRING "macOS deployment target (Apple clang only)") +set(LANGS C CXX) +if(APPLE) + set(LANGS ${LANGS} OBJC Swift) +endif() + find_program(CCACHE_PROGRAM ccache) if(CCACHE_PROGRAM) - foreach(lang C CXX) + foreach(lang ${LANGS}) if(NOT DEFINED CMAKE_${lang}_COMPILER_LAUNCHER AND NOT CMAKE_${lang}_COMPILER MATCHES ".*/ccache") message(STATUS "Enabling ccache for ${lang}") set(CMAKE_${lang}_COMPILER_LAUNCHER ${CCACHE_PROGRAM} CACHE STRING "") @@ -15,10 +20,17 @@ if(CCACHE_PROGRAM) endforeach() endif() + project(lokinet VERSION 0.9.5 DESCRIPTION "lokinet - IP packet onion router" - LANGUAGES C CXX) + LANGUAGES ${LANGS}) + +if(APPLE) + # Apple build number: must be incremented to submit a new build for the same lokinet version, + # should be reset to 0 when the lokinet version increments. + set(LOKINET_APPLE_BUILD 0) +endif() set(RELEASE_MOTTO "A Series of Tubes" CACHE STRING "Release motto") @@ -29,10 +41,8 @@ if(RELEASE_MOTTO AND CMAKE_BUILD_TYPE MATCHES "[Rr][Ee][Ll][Ee][Aa][Ss][Ee]") add_definitions(-DLLARP_RELEASE_MOTTO="${RELEASE_MOTTO}") endif() - list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/cmake") - # Core options option(USE_AVX2 "enable avx2 code" OFF) option(USE_NETNS "enable networking namespace support. Linux only" OFF) @@ -94,14 +104,6 @@ endif() add_definitions(-D${CMAKE_SYSTEM_NAME}) -if(MSVC_VERSION) - enable_language(ASM_MASM) - list(APPEND CMAKE_ASM_MASM_SOURCE_FILE_EXTENSIONS s) - add_definitions(-D_WIN32_WINNT=0x0600 -DNOMINMAX -DSODIUM_STATIC) -else() - enable_language(ASM) -endif() - include(cmake/solaris.cmake) include(cmake/win32.cmake) @@ -184,8 +186,12 @@ else() endif() -# this is messing with release builds -add_compile_options(-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0) +if(NOT APPLE) + add_compile_options(-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0 -Wall -Wextra -Wno-unknown-pragmas -Wno-unused-function -Wno-deprecated-declarations -Werror=vla) + if (CMAKE_CXX_COMPILER_ID MATCHES "Clang") + add_compile_options(-Wno-unknown-warning-option) + endif() +endif() if (NOT CMAKE_SYSTEM_NAME MATCHES "Linux" AND SHADOW) message( FATAL_ERROR "shadow-framework is Linux only" ) @@ -213,17 +219,6 @@ if(TRACY_ROOT) endif() -if (CMAKE_CXX_COMPILER_ID MATCHES "Clang") - add_compile_options(-Wno-unknown-warning-option) -endif() - -if (NOT MSVC_VERSION) - add_compile_options(-Wall -Wextra -Wno-unknown-pragmas) - # vla are evil - add_compile_options(-Werror=vla) - add_compile_options(-Wno-unused-function -Wno-deprecated-declarations) -endif() - include(cmake/coverage.cmake) # these vars are set by the cmake toolchain spec @@ -231,18 +226,20 @@ if (WOW64_CROSS_COMPILE OR WIN64_CROSS_COMPILE) include(cmake/cross_compile.cmake) endif() -if(NATIVE_BUILD) - if(CMAKE_SYSTEM_PROCESSOR STREQUAL ppc64le) - add_compile_options(-mcpu=native -mtune=native) - else() - add_compile_options(-march=native -mtune=native) - endif() -elseif(NOT NON_PC_TARGET) - if (USE_AVX2) - add_compile_options(-march=haswell -mtune=haswell -mfpmath=sse) - else() - # Public binary releases - add_compile_options(-march=nocona -mtune=haswell -mfpmath=sse) +if(NOT APPLE) + if(NATIVE_BUILD) + if(CMAKE_SYSTEM_PROCESSOR STREQUAL ppc64le) + add_compile_options(-mcpu=native -mtune=native) + else() + add_compile_options(-march=native -mtune=native) + endif() + elseif(NOT NON_PC_TARGET) + if (USE_AVX2) + add_compile_options(-march=haswell -mtune=haswell -mfpmath=sse) + else() + # Public binary releases + add_compile_options(-march=nocona -mtune=haswell -mfpmath=sse) + endif() endif() endif() @@ -332,12 +329,11 @@ endif() add_subdirectory(crypto) add_subdirectory(llarp) add_subdirectory(daemon) + + if(WITH_HIVE) add_subdirectory(pybind) endif() - - - if (NOT SHADOW) if(WITH_TESTS OR WITH_HIVE) add_subdirectory(test) @@ -361,6 +357,6 @@ if(NOT TARGET uninstall) endif() -if(BUILD_PACKAGE) +if(BUILD_PACKAGE AND NOT APPLE) include(cmake/installer.cmake) endif() diff --git a/cmake/installer.cmake b/cmake/installer.cmake index 789a88ee4..b8c04e563 100644 --- a/cmake/installer.cmake +++ b/cmake/installer.cmake @@ -7,10 +7,7 @@ if(WIN32) include(cmake/win32_installer_deps.cmake) endif() -if(APPLE) - include(cmake/macos_installer_deps.cmake) -endif() - # This must always be last! include(CPack) + diff --git a/cmake/macos_installer_deps.cmake b/cmake/macos_installer_deps.cmake deleted file mode 100644 index a56995a73..000000000 --- a/cmake/macos_installer_deps.cmake +++ /dev/null @@ -1,113 +0,0 @@ -# macos specific cpack stuff goes here - -# Here we build lokinet-network-control-panel into 'lokinet-gui.app' in "extra/" where a postinstall -# script will then move it to /Applications/. - -set(LOKINET_GUI_REPO "https://github.com/oxen-io/loki-network-control-panel.git" - CACHE STRING "Can be set to override the default lokinet-gui git repository") -set(LOKINET_GUI_CHECKOUT "origin/master" - CACHE STRING "Can be set to specify a particular branch or tag to build from LOKINET_GUI_REPO") -set(MACOS_SIGN_APP "" # FIXME: it doesn't use a Apple Distribution key because WTF knows. - CACHE STRING "enable codesigning of the stuff inside the .app and the lokinet binary -- use a 'Apple Distribution' key (or description) from `security find-identity -v`") -set(MACOS_SIGN_PKG "" - CACHE STRING "enable codesigning of the .pkg -- use a 'Developer ID Installer' key (or description) from `security find-identity -v`") -set(MACOS_NOTARIZE_USER "" - CACHE STRING "set macos notarization username; can also set it in ~/.notarization.cmake") -set(MACOS_NOTARIZE_PASS "" - CACHE STRING "set macos notarization password; can also set it in ~/.notarization.cmake") -set(MACOS_NOTARIZE_ASC "" - CACHE STRING "set macos notarization asc provider; can also set it in ~/.notarization.cmake") - -include(ExternalProject) - -message(STATUS "Building UninstallLokinet.app") - -ExternalProject_Add(lokinet-uninstaller - SOURCE_DIR ${CMAKE_SOURCE_DIR}/contrib/macos/uninstaller - CMAKE_ARGS -DCMAKE_INSTALL_PREFIX=${PROJECT_BINARY_DIR} -DMACOS_SIGN=${MACOS_SIGN_APP} - -DCMAKE_PREFIX_PATH=${CMAKE_PREFIX_PATH} -) - -message(STATUS "Building LokinetGUI.app from ${LOKINET_GUI_REPO} @ ${LOKINET_GUI_CHECKOUT}") - -if(NOT BUILD_STATIC_DEPS) - message(FATAL_ERROR "Building an installer on macos requires -DBUILD_STATIC_DEPS=ON") -endif() - - - -ExternalProject_Add(lokinet-gui - DEPENDS oxenmq::oxenmq - GIT_REPOSITORY "${LOKINET_GUI_REPO}" - GIT_TAG "${LOKINET_GUI_CHECKOUT}" - CMAKE_ARGS -DMACOS_APP=ON -DCMAKE_INSTALL_PREFIX=${PROJECT_BINARY_DIR} -DMACOS_SIGN=${MACOS_SIGN_APP} - -DCMAKE_PREFIX_PATH=${CMAKE_PREFIX_PATH} -DBUILD_SHARED_LIBS=OFF - "-DOXENMQ_LIBRARIES=$$$$$" - "-DOXENMQ_INCLUDE_DIRS=$" - ) - -install(PROGRAMS ${CMAKE_SOURCE_DIR}/contrib/macos/lokinet_uninstall.sh - DESTINATION "bin/" - COMPONENT lokinet) - -install(DIRECTORY ${PROJECT_BINARY_DIR}/LokinetGUI.app - DESTINATION "../../Applications/Lokinet" - USE_SOURCE_PERMISSIONS - COMPONENT gui - PATTERN "*" - ) - -install(DIRECTORY ${PROJECT_BINARY_DIR}/UninstallLokinet.app - DESTINATION "../../Applications/Lokinet" - USE_SOURCE_PERMISSIONS - COMPONENT gui - PATTERN "*" - ) - -# copy files that will be later moved by the postinstall script to proper locations -install(FILES ${CMAKE_SOURCE_DIR}/contrib/macos/lokinet_macos_daemon_script.sh - ${CMAKE_SOURCE_DIR}/contrib/macos/network.loki.lokinet.daemon.plist - ${CMAKE_SOURCE_DIR}/contrib/macos/lokinet-newsyslog.conf - DESTINATION "extra/" - COMPONENT lokinet) - -set(CPACK_COMPONENTS_ALL lokinet gui) - -set(CPACK_COMPONENT_LOKINET_DISPLAY_NAME "Lokinet Service") -set(CPACK_COMPONENT_LOKINET_DESCRIPTION "Main Lokinet runtime service, managed by Launchd") - -set(CPACK_COMPONENT_GUI_DISPLAY_NAME "Lokinet GUI") -set(CPACK_COMPONENT_GUI_DESCRIPTION "Small GUI which provides stats and limited runtime control of the Lokinet service. Resides in the system tray.") - -set(CPACK_GENERATOR "productbuild") -set(CPACK_PACKAGING_INSTALL_PREFIX "/opt/lokinet") -set(CPACK_PREINSTALL_LOKINET_SCRIPT ${CMAKE_SOURCE_DIR}/contrib/macos/preinstall) -set(CPACK_POSTFLIGHT_LOKINET_SCRIPT ${CMAKE_SOURCE_DIR}/contrib/macos/postinstall) - -set(CPACK_RESOURCE_FILE_LICENSE "${PROJECT_SOURCE_DIR}/LICENSE.txt") - -set(CPACK_PRODUCTBUILD_IDENTITY_NAME "${MACOS_SIGN_PKG}") - -if(MACOS_SIGN_APP) - add_custom_target(sign ALL - echo "Signing lokinet and lokinet-vpn binaries" - COMMAND codesign -s "${MACOS_SIGN_APP}" --strict --options runtime --force -vvv $ $ - DEPENDS lokinet lokinet-vpn - ) -endif() - -if(MACOS_SIGN_APP AND MACOS_SIGN_PKG) - if(NOT MACOS_NOTARIZE_USER) - if(EXISTS "$ENV{HOME}/.notarization.cmake") - include("$ENV{HOME}/.notarization.cmake") - endif() - endif() - if(MACOS_NOTARIZE_USER AND MACOS_NOTARIZE_PASS AND MACOS_NOTARIZE_ASC) - message(STATUS "'notarization' target enabled") - configure_file(${CMAKE_SOURCE_DIR}/contrib/macos/notarize.py.in ${CMAKE_CURRENT_BINARY_DIR}/contrib/notarize.py ESCAPE_QUOTES @ONLY) - file(COPY ${CMAKE_CURRENT_BINARY_DIR}/contrib/notarize.py DESTINATION ${PROJECT_BINARY_DIR} FILE_PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE) - add_custom_target(notarize ./notarize.py) - else() - message(WARNING "Not enable 'notarization' target: signing is enabled but notarization info not provided. Create ~/.notarization.cmake or set cmake parameters directly") - endif() -endif() diff --git a/contrib/format.sh b/contrib/format.sh index f83c27436..e514590bd 100755 --- a/contrib/format.sh +++ b/contrib/format.sh @@ -3,6 +3,9 @@ CLANG_FORMAT_DESIRED_VERSION=11 binary=$(which clang-format-$CLANG_FORMAT_DESIRED_VERSION 2>/dev/null) +if [ $? -ne 0 ]; then + binary=$(which clang-format-mp-$CLANG_FORMAT_DESIRED_VERSION 2>/dev/null) +fi if [ $? -ne 0 ]; then binary=$(which clang-format 2>/dev/null) if [ $? -ne 0 ]; then @@ -18,7 +21,23 @@ fi cd "$(dirname $0)/../" if [ "$1" = "verify" ] ; then - exit $($binary --output-replacements-xml $(find jni daemon llarp include pybind | grep -E '\.[hc](pp)?$' | grep -v '\#') | grep '' | wc -l) + if [ $($binary --output-replacements-xml $(find jni daemon llarp include pybind | grep -E '\.([hc](pp)?|mm?)$' | grep -v '\#') | grep '' | wc -l) -ne 0 ] ; then + exit 1 + fi else - $binary -i $(find jni daemon llarp include pybind | grep -E '\.[hc](pp)?$' | grep -v '\#') &> /dev/null + $binary -i $(find jni daemon llarp include pybind | grep -E '\.([hc](pp)?|mm)$' | grep -v '\#') &> /dev/null +fi + +swift_format=$(which swiftformat 2>/dev/null) +if [ $? -eq 0 ]; then + if [ "$1" = "verify" ] ; then + for f in $(find daemon | grep -E '\.swift$' | grep -v '\#') ; do + if [ $($swift_format --quiet --dryrun < "$f" | diff "$f" - | wc -l) -ne 0 ] ; then + exit 1 + fi + done + else + $swift_format --quiet $(find daemon | grep -E '\.swift$' | grep -v '\#') + fi + fi diff --git a/contrib/lokinet.svg b/contrib/lokinet.svg new file mode 100644 index 000000000..896e74bbb --- /dev/null +++ b/contrib/lokinet.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + diff --git a/contrib/mac.sh b/contrib/mac.sh new file mode 100755 index 000000000..4719b40d2 --- /dev/null +++ b/contrib/mac.sh @@ -0,0 +1,39 @@ +#!/bin/bash +# +# Build the shit on mac +# +# You will generally need to add: -DCODESIGN_APP=... to make this work, and (unless you are a +# lokinet team member) will need to pay Apple money for your own team ID and arse around with +# provisioning profiles. See macos/README.txt. +# + +set -e +set +x +if ! [ -f LICENSE.txt ] || ! [ -d llarp ]; then + echo "You need to run this as ./contrib/mac.sh from the top-level lokinet project directory" +fi + +mkdir -p build-mac +cd build-mac +cmake \ + -G Ninja \ + -DBUILD_STATIC_DEPS=ON \ + -DBUILD_PACKAGE=ON \ + -DBUILD_SHARED_LIBS=OFF \ + -DBUILD_TESTING=OFF \ + -DBUILD_LIBLOKINET=OFF \ + -DWITH_TESTS=OFF \ + -DNATIVE_BUILD=OFF \ + -DSTATIC_LINK=ON \ + -DWITH_SYSTEMD=OFF \ + -DFORCE_OXENMQ_SUBMODULE=ON \ + -DSUBMODULE_CHECK=OFF \ + -DWITH_LTO=ON \ + -DCMAKE_BUILD_TYPE=Release \ + "$@" \ + .. +ninja sign + +echo -e "Build complete, your app is here:\n" +ls -lad $(pwd)/daemon/lokinet.app +echo "" diff --git a/contrib/macos/Info.plist.in b/contrib/macos/Info.plist.in new file mode 100644 index 000000000..9311f2404 --- /dev/null +++ b/contrib/macos/Info.plist.in @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + Lokinet + CFBundleExecutable + MacOS/lokinet + CFBundleIdentifier + com.loki-project.lokinet + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + lokinet + CFBundlePackageType + XPC! + CFBundleShortVersionString + @lokinet_VERSION@ + CFBundleVersion + @lokinet_VERSION@.@LOKINET_APPLE_BUILD@ + + diff --git a/contrib/macos/LokinetExtension.Info.plist.in b/contrib/macos/LokinetExtension.Info.plist.in new file mode 100644 index 000000000..80afb1b94 --- /dev/null +++ b/contrib/macos/LokinetExtension.Info.plist.in @@ -0,0 +1,40 @@ + + + + + CFBundleDisplayName + Lokinet + + CFBundleExecutable + lokinet-extension + + CFBundleIdentifier + com.loki-project.lokinet.network-extension + + CFBundleInfoDictionaryVersion + 6.0 + + CFBundlePackageType + XPC! + + CFBundleName + lokinet + + CFBundleVersion + @lokinet_VERSION@ + + ITSAppUsesNonExemptEncryption + + + LSMinimumSystemVersion + 11.0 + + NSExtension + + NSExtensionPointIdentifier + com.apple.networkextension.packet-tunnel + NSExtensionPrincipalClass + LLARPPacketTunnel + + + diff --git a/contrib/macos/README.txt b/contrib/macos/README.txt new file mode 100644 index 000000000..9880ecc3c --- /dev/null +++ b/contrib/macos/README.txt @@ -0,0 +1,38 @@ +This directory contains the magical incantations and random voodoo symbols needed to coax an Apple +build. There's no reason builds have to be this stupid, except that Apple wants to funnel everyone +into the no-CI, no-help, undocumented, non-toy-apps-need-not-apply modern Apple culture. + +This is disgusting. + +But it gets worse. + +The following two files, in particular, are the very worst manifestations of this already toxic +Apple cancer: they are required for proper permissions to run on macOS, are undocumented, and can +only be regenerated through the entirely closed source Apple Developer backend, for which you have +to pay money first to get a team account (a personal account will not work), and they lock the +resulting binaries to only run on individually selected Apple computers selected at the time the +profile is provisioned (with no ability to allow it to run anywhere). + + lokinet.provisionprofile + lokinet-extension.provisionprofile + +This is actively hostile to open source development, but that is nothing new for Apple. + +In order to make things work, you'll have to replace these provisioning profiles with your own +(after paying Apple for the privilege of developing on their platform, of course) and change all the +team/application/bundle IDs to reference your own team, matching the provisioning profiles. The +provisioning profiles must be a "macOS Development" provisioning profile, and must include the +signing keys and the authorized devices on which you want to run it. (The profiles bundled in this +repository contains the lokinet team's "Apple Development" keys associated with the Oxen project, +and mac dev boxes. This is *useless* for anyone else). + +Also take note that you *must not* put a development build `lokinet.app` inside /Applications +because if you do, it won't work because *on top* of the ridiculous signing and entitlement bullshit +that Apple makes you jump through, the rules *also* differ for binaries placed in /Applications +versus binaries placed elsewhere, but like everything else here, it is entirely undocumented. + +If you are reading this to try to build Lokinet for yourself for an Apple operating system and +simultaneously care about open source, privacy, or freedom then you, my friend, are a walking +contradiction: you are trying to get Lokinet to work on a platform that actively despises open +source, privacy, and freedom. Even Windows is a better choice in all of these categories than +Apple. diff --git a/contrib/macos/lokinet-extension.entitlements.plist b/contrib/macos/lokinet-extension.entitlements.plist new file mode 100644 index 000000000..8233a7926 --- /dev/null +++ b/contrib/macos/lokinet-extension.entitlements.plist @@ -0,0 +1,29 @@ + + + + + com.apple.application-identifier + SUQ8J2PCT7.com.loki-project.lokinet.network-extension + + com.apple.developer.networking.networkextension + + packet-tunnel-provider + + + com.apple.developer.team-identifier + SUQ8J2PCT7 + + com.apple.security.app-sandbox + + + com.apple.security.get-task-allow + + + com.apple.security.network.client + + + com.apple.security.network.server + + + + diff --git a/contrib/macos/lokinet-extension.provisionprofile b/contrib/macos/lokinet-extension.provisionprofile new file mode 100644 index 000000000..71f066bda Binary files /dev/null and b/contrib/macos/lokinet-extension.provisionprofile differ diff --git a/contrib/macos/lokinet.entitlements.plist b/contrib/macos/lokinet.entitlements.plist new file mode 100644 index 000000000..3869f5b04 --- /dev/null +++ b/contrib/macos/lokinet.entitlements.plist @@ -0,0 +1,31 @@ + + + + + com.apple.application-identifier + SUQ8J2PCT7.com.loki-project.lokinet + + com.apple.developer.networking.networkextension + + packet-tunnel-provider + dns-proxy + dns-settings + + + com.apple.developer.team-identifier + SUQ8J2PCT7 + + com.apple.security.app-sandbox + + + com.apple.security.get-task-allow + + + com.apple.security.network.client + + + com.apple.security.network.server + + + + diff --git a/contrib/macos/lokinet.provisionprofile b/contrib/macos/lokinet.provisionprofile new file mode 100644 index 000000000..f740cd98a Binary files /dev/null and b/contrib/macos/lokinet.provisionprofile differ diff --git a/contrib/macos/uninstaller/mk-icns.sh b/contrib/macos/mk-icns.sh similarity index 100% rename from contrib/macos/uninstaller/mk-icns.sh rename to contrib/macos/mk-icns.sh diff --git a/contrib/macos/sign.sh.in b/contrib/macos/sign.sh.in new file mode 100755 index 000000000..6ebf0859a --- /dev/null +++ b/contrib/macos/sign.sh.in @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +set -e +codesign --verbose=4 --force -s "@CODESIGN_APPEX@" \ + --entitlements "@PROJECT_SOURCE_DIR@/contrib/macos/lokinet-extension.entitlements.plist" \ + --deep --strict --timestamp --options=runtime "@SIGN_TARGET@/Contents/PlugIns/lokinet-extension.appex" +for file in "@SIGN_TARGET@/Contents/MacOS/lokinet" "@SIGN_TARGET@" ; do + codesign --verbose=4 --force -s "@CODESIGN_APP@" \ + --entitlements "@PROJECT_SOURCE_DIR@/contrib/macos/lokinet.entitlements.plist" \ + --deep --strict --timestamp --options=runtime "$file" +done diff --git a/contrib/windows.sh b/contrib/windows.sh index c1224afe1..5d6bf399c 100755 --- a/contrib/windows.sh +++ b/contrib/windows.sh @@ -1,4 +1,9 @@ #!/bin/bash +# +# helper script for me for when i cross compile for windows +# t. jeff +# + set -e set +x mkdir -p build-windows diff --git a/daemon/CMakeLists.txt b/daemon/CMakeLists.txt index 7c6e8a45b..e2e725d74 100644 --- a/daemon/CMakeLists.txt +++ b/daemon/CMakeLists.txt @@ -1,7 +1,14 @@ -add_executable(lokinet lokinet.cpp) + add_executable(lokinet-vpn lokinet-vpn.cpp) -add_executable(lokinet-bootstrap lokinet-bootstrap.cpp) -enable_lto(lokinet lokinet-vpn lokinet-bootstrap) +if(APPLE) + add_executable(lokinet lokinet.swift) + enable_lto(lokinet) +else() + add_executable(lokinet lokinet.cpp) + add_executable(lokinet-bootstrap lokinet-bootstrap.cpp) + enable_lto(lokinet lokinet-vpn lokinet-bootstrap) +endif() + if(TRACY_ROOT) target_sources(lokinet PRIVATE ${TRACY_ROOT}/TracyClient.cpp) @@ -23,14 +30,21 @@ if(CMAKE_SYSTEM_NAME MATCHES "Linux") endif() endif() -target_link_libraries(lokinet-bootstrap PUBLIC cpr::cpr) -if(NOT WIN32) - find_package(OpenSSL REQUIRED) - # because debian sid's curl doesn't link against openssl for some godawful cursed reason - target_link_libraries(lokinet-bootstrap PUBLIC OpenSSL::SSL OpenSSL::Crypto) +if(NOT APPLE) + target_link_libraries(lokinet-bootstrap PUBLIC cpr::cpr) + if(NOT WIN32) + find_package(OpenSSL REQUIRED) + # because debian sid's curl doesn't link against openssl for some godawful cursed reason + target_link_libraries(lokinet-bootstrap PUBLIC OpenSSL::SSL OpenSSL::Crypto) + endif() endif() -foreach(exe lokinet lokinet-vpn lokinet-bootstrap) +set(exetargets lokinet lokinet-vpn) +if(NOT APPLE) + list(APPEND exetargets lokinet-bootstrap) +endif() + +foreach(exe ${exetargets}) if(WIN32 AND NOT MSVC_VERSION) target_sources(${exe} PRIVATE ../llarp/win32/version.rc) target_link_libraries(${exe} PRIVATE -static-libstdc++ -static-libgcc --static -Wl,--pic-executable,-e,mainCRTStartup,--subsystem,console:5.00) @@ -38,18 +52,77 @@ foreach(exe lokinet lokinet-vpn lokinet-bootstrap) elseif(CMAKE_SYSTEM_NAME MATCHES "FreeBSD") target_link_directories(${exe} PRIVATE /usr/local/lib) endif() - target_link_libraries(${exe} PRIVATE liblokinet) + target_link_libraries(${exe} PUBLIC liblokinet) if(WITH_JEMALLOC) target_link_libraries(${exe} PUBLIC jemalloc) endif() - target_include_directories(${exe} PRIVATE ${CMAKE_SOURCE_DIR}) + target_include_directories(${exe} PUBLIC "${PROJECT_SOURCE_DIR}") target_compile_definitions(${exe} PRIVATE -DVERSIONTAG=${GIT_VERSION_REAL}) add_log_tag(${exe}) if(should_install) - install(TARGETS ${exe} RUNTIME DESTINATION bin COMPONENT lokinet) + if(APPLE) + install(TARGETS ${exe} BUNDLE DESTINATION "${PROJECT_BINARY_DIR}" COMPONENT lokinet) + else() + install(TARGETS ${exe} RUNTIME DESTINATION bin COMPONENT lokinet) + endif() endif() endforeach() +if(APPLE) + + set(CODESIGN_APP "" CACHE STRING "codesign the macos app using this key identity") + set(CODESIGN_APPEX "${CODESIGN_APP}" CACHE STRING "codesign the internal extension using this key identity; defaults to CODESIGN_APP if empty") + + set(mac_icon ${CMAKE_CURRENT_BINARY_DIR}/lokinet.icns) + add_custom_command(OUTPUT ${mac_icon} + COMMAND ${PROJECT_SOURCE_DIR}/contrib/macos/mk-icns.sh ${PROJECT_SOURCE_DIR}/contrib/lokinet.svg ${mac_icon} + DEPENDS ${PROJECT_SOURCE_DIR}/contrib/lokinet.svg ${PROJECT_SOURCE_DIR}/contrib/macos/mk-icns.sh) + add_custom_target(icons DEPENDS ${mac_icon}) + add_dependencies(lokinet icons lokinet-extension) + file(DOWNLOAD "https://seed.lokinet.org/lokinet.signed" ${CMAKE_CURRENT_BINARY_DIR}/bootstrap.signed) + add_custom_command(TARGET lokinet + POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different ${CMAKE_CURRENT_BINARY_DIR}/bootstrap.signed + $/Contents/Resources/bootstrap.signed + COMMAND mkdir -p $/Contents/PlugIns + COMMAND cp -a $ $/Contents/PlugIns/ + COMMAND ${CMAKE_COMMAND} -E copy_if_different ${PROJECT_SOURCE_DIR}/contrib/macos/lokinet.provisionprofile + $/Contents/embedded.provisionprofile + ) + + set_target_properties(lokinet + PROPERTIES + MACOSX_BUNDLE TRUE + MACOSX_BUNDLE_INFO_STRING "Lokinet IP Packet Onion Router" + MACOSX_BUNDLE_BUNDLE_NAME "Lokinet" + MACOSX_BUNDLE_BUNDLE_VERSION "${lokinet_VERSION}" + MACOSX_BUNDLE_LONG_VERSION_STRING "${lokinet_VERSION}" + MACOSX_BUNDLE_SHORT_VERSION_STRING "${lokinet_VERSION_MAJOR}.${lokinet_VERSION_MINOR}" + MACOSX_BUNDLE_GUI_IDENTIFIER "com.loki-project.lokinet" + MACOSX_BUNDLE_INFO_PLIST "${PROJECT_SOURCE_DIR}/contrib/macos/Info.plist.in" + MACOSX_BUNDLE_ICON_FILE "${mac_icon}" + MACOSX_BUNDLE_COPYRIGHT "© 2021, The Oxen Project") + if (CODESIGN_APP AND CODESIGN_APPEX) + message(STATUS "codesigning with ${CODESIGN_APP} (app) ${CODESIGN_APPEX} (appex)") + set(SIGN_TARGET "${CMAKE_CURRENT_BINARY_DIR}/lokinet.app") + configure_file( + "${PROJECT_SOURCE_DIR}/contrib/macos/sign.sh.in" + "${PROJECT_BINARY_DIR}/sign.sh" + @ONLY) + add_custom_target( + sign + DEPENDS "${PROJECT_BINARY_DIR}/sign.sh" lokinet lokinet-extension + COMMAND "${PROJECT_BINARY_DIR}/sign.sh" + ) + else() + message(WARNING "Not codesigning: CODESIGN_APP (=${CODESIGN_APP}) and/or CODESIGN_APPEX (=${CODESIGN_APPEX}) are not set") + add_custom_target( + sign + DEPENDS lokinet lokinet-extension + COMMAND "true") + endif() +endif() + if(SETCAP) install(CODE "execute_process(COMMAND ${SETCAP} cap_net_admin,cap_net_bind_service=+eip ${CMAKE_INSTALL_PREFIX}/bin/lokinet)") endif() diff --git a/daemon/lokinet.swift b/daemon/lokinet.swift new file mode 100644 index 000000000..4af96bac9 --- /dev/null +++ b/daemon/lokinet.swift @@ -0,0 +1,100 @@ +import AppKit +import Foundation +import NetworkExtension + +let app = NSApplication.shared + +class LokinetMain: NSObject, NSApplicationDelegate { + var vpnManager = NETunnelProviderManager() + let lokinetComponent = "com.loki-project.lokinet.network-extension" + + func applicationDidFinishLaunching(_: Notification) { + setupVPNJizz() + } + + func bail() { + app.terminate(self) + } + + func setupVPNJizz() { + NSLog("Starting up lokinet") + NETunnelProviderManager.loadAllFromPreferences { [self] (savedManagers: [NETunnelProviderManager]?, error: Error?) in + if let error = error { + NSLog(error.localizedDescription) + bail() + return + } + + if let savedManagers = savedManagers { + for manager in savedManagers { + if (manager.protocolConfiguration as? NETunnelProviderProtocol)?.providerBundleIdentifier == self.lokinetComponent { + NSLog("%@", manager) + NSLog("Found saved VPN Manager") + self.vpnManager = manager + } + } + } + let providerProtocol = NETunnelProviderProtocol() + providerProtocol.serverAddress = "loki.loki" // Needs to be set to some non-null dummy value + providerProtocol.username = "anonymous" + providerProtocol.providerBundleIdentifier = self.lokinetComponent + providerProtocol.enforceRoutes = true + // macos seems to have trouble when this is true, and reports are that this breaks and + // doesn't do what it says on the tin in the first place. Needs more testing. + providerProtocol.includeAllNetworks = false + self.vpnManager.protocolConfiguration = providerProtocol + self.vpnManager.isEnabled = true + // self.vpnManager.isOnDemandEnabled = true + self.vpnManager.localizedDescription = "lokinet" + self.vpnManager.saveToPreferences(completionHandler: { error -> Void in + if error != nil { + NSLog("Error saving to preferences") + NSLog(error!.localizedDescription) + bail() + } else { + self.vpnManager.loadFromPreferences(completionHandler: { error in + if error != nil { + NSLog("Error loading from preferences") + NSLog(error!.localizedDescription) + bail() + } else { + do { + NSLog("Trying to start") + self.initializeConnectionObserver() + try self.vpnManager.connection.startVPNTunnel() + } catch let error as NSError { + NSLog(error.localizedDescription) + bail() + } catch { + NSLog("There was a fatal error") + bail() + } + } + }) + } + }) + } + } + + func initializeConnectionObserver() { + NotificationCenter.default.addObserver(forName: NSNotification.Name.NEVPNStatusDidChange, object: vpnManager.connection, queue: OperationQueue.main) { _ -> Void in + if self.vpnManager.connection.status == .invalid { + NSLog("VPN configuration is invalid") + } else if self.vpnManager.connection.status == .disconnected { + NSLog("VPN is disconnected.") + } else if self.vpnManager.connection.status == .connecting { + NSLog("VPN is connecting...") + } else if self.vpnManager.connection.status == .reasserting { + NSLog("VPN is reasserting...") + } else if self.vpnManager.connection.status == .disconnecting { + NSLog("VPN is disconnecting...") + } else if self.vpnManager.connection.status == .connected { + NSLog("VPN Connected") + } + } + } +} + +let delegate = LokinetMain() +app.delegate = delegate +app.run() diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt index 9634f0036..f574f4193 100644 --- a/external/CMakeLists.txt +++ b/external/CMakeLists.txt @@ -50,6 +50,16 @@ add_library(uvw INTERFACE) target_include_directories(uvw INTERFACE uvw/src) target_link_libraries(uvw INTERFACE libuv) + + +# We don't need any of these as we don't use the ssl crypto helper code at all: +set(ENABLE_GNUTLS OFF CACHE BOOL "Disable gnutls for ngtcp2") +set(ENABLE_OPENSSL OFF CACHE BOOL "Disable openssl for ngtcp2") +set(ENABLE_BORINGSSL OFF CACHE BOOL "Disable boringssl for ngtcp2") + +add_definitions(-D_GNU_SOURCE) +add_subdirectory(ngtcp2 EXCLUDE_FROM_ALL) + # cpr configuration. Ideally we'd just do this via add_subdirectory, but cpr's cmake requires # 3.15+, and we target lower than that (and this is fairly simple to build). @@ -75,11 +85,3 @@ target_link_libraries(cpr PUBLIC CURL::libcurl) target_include_directories(cpr PUBLIC cpr/include) target_compile_definitions(cpr PUBLIC CPR_CURL_NOSIGNAL) add_library(cpr::cpr ALIAS cpr) - -# We don't need any of these as we don't use the ssl crypto helper code at all: -set(ENABLE_GNUTLS OFF CACHE BOOL "Disable gnutls for ngtcp2") -set(ENABLE_OPENSSL OFF CACHE BOOL "Disable openssl for ngtcp2") -set(ENABLE_BORINGSSL OFF CACHE BOOL "Disable boringssl for ngtcp2") - -add_definitions(-D_GNU_SOURCE) -add_subdirectory(ngtcp2 EXCLUDE_FROM_ALL) diff --git a/include/llarp.hpp b/include/llarp.hpp index e333e2495..5838d3569 100644 --- a/include/llarp.hpp +++ b/include/llarp.hpp @@ -120,7 +120,6 @@ namespace llarp std::unique_ptr> closeWaiter; }; - } // namespace llarp #endif diff --git a/llarp/CMakeLists.txt b/llarp/CMakeLists.txt index 3ae31ae4e..30e90b7be 100644 --- a/llarp/CMakeLists.txt +++ b/llarp/CMakeLists.txt @@ -1,7 +1,6 @@ include(Version) add_library(lokinet-util - STATIC ${CMAKE_CURRENT_BINARY_DIR}/constants/version.cpp util/bencode.cpp util/buffer.cpp @@ -23,8 +22,9 @@ add_library(lokinet-util util/str.cpp util/thread/queue_manager.cpp util/thread/threading.cpp - util/time.cpp -) + util/time.cpp) + + add_dependencies(lokinet-util genversion) target_include_directories(lokinet-util PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} ${PROJECT_SOURCE_DIR}/include ${PROJECT_SOURCE_DIR}) @@ -262,6 +262,10 @@ if(BUILD_LIBLOKINET) add_log_tag(lokinet-shared) endif() +if(APPLE) + add_subdirectory(apple) +endif() + foreach(lokinet_lib liblokinet lokinet-platform lokinet-util lokinet-cryptography) add_log_tag(${lokinet_lib}) endforeach() diff --git a/llarp/apple/CMakeLists.txt b/llarp/apple/CMakeLists.txt new file mode 100644 index 000000000..58a54727a --- /dev/null +++ b/llarp/apple/CMakeLists.txt @@ -0,0 +1,52 @@ + +# 3.13+ so that we can add link libraries to parent targets +cmake_minimum_required(VERSION 3.13) + +if (BUILD_SHARED_LIBS OR NOT BUILD_STATIC_DEPS OR NOT STATIC_LINK) + message(FATAL_ERROR "macOS builds require a full static build; perhaps use the contrib/macos.sh script to build?") +endif() + +# god made apple so that man may suffer + +find_library(FOUNDATION Foundation REQUIRED) +find_library(NETEXT NetworkExtension REQUIRED) +find_library(COREFOUNDATION CoreFoundation REQUIRED) + +target_sources(lokinet-util PRIVATE apple_logger.cpp) +target_link_libraries(lokinet-util PUBLIC ${FOUNDATION}) + +target_sources(lokinet-platform PRIVATE vpn_platform.cpp vpn_interface.cpp route_manager.cpp context_wrapper.cpp) + +add_executable(lokinet-extension MACOSX_BUNDLE + PacketTunnelProvider.m + DNSTrampoline.m + ) +enable_lto(lokinet-extension) +target_link_libraries(lokinet-extension PRIVATE + liblokinet + ${COREFOUNDATION} + ${NETEXT}) + +# Not sure what -fapplication-extension does, but XCode puts it in so... +# -fobjc-arc enables automatic reference counting for objective-C code +# -e _NSExtensionMain because the appex has that instead of a `main` function entry point, of course. +target_compile_options(lokinet-extension PRIVATE -fapplication-extension -fobjc-arc) +target_link_options(lokinet-extension PRIVATE -fapplication-extension -e _NSExtensionMain) + +target_link_libraries(lokinet-extension PUBLIC + liblokinet + ${COREFOUNDATION} + ${NETEXT}) + +set_target_properties(lokinet-extension PROPERTIES + BUNDLE TRUE + BUNDLE_EXTENSION appex + MACOSX_BUNDLE_INFO_PLIST ${PROJECT_SOURCE_DIR}/contrib/macos/LokinetExtension.Info.plist.in + XCODE_PRODUCT_TYPE com.apple.product-type.app-extension + ) + +add_custom_command(TARGET lokinet-extension + POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different ${PROJECT_SOURCE_DIR}/contrib/macos/lokinet-extension.provisionprofile + $/Contents/embedded.provisionprofile + ) diff --git a/llarp/apple/DNSTrampoline.h b/llarp/apple/DNSTrampoline.h new file mode 100644 index 000000000..4935d43c8 --- /dev/null +++ b/llarp/apple/DNSTrampoline.h @@ -0,0 +1,51 @@ +#pragma once +#include +#include + +extern NSString* error_domain; + +/** + * "Trampoline" class that listens for UDP DNS packets on port 1053 coming from lokinet's embedded + * libunbound (when exit mode is enabled), wraps them via NetworkExtension's crappy UDP API, then + * sends responses back to libunbound to be parsed/etc. This class knows nothing about DNS, it is + * basically just a UDP packet forwarder. + * + * So for a lokinet configuration of "upstream=1.1.1.1", when exit mode is OFF: + * - DNS requests go to TUNNELIP:53, get sent to libunbound, which forwards them (directly) to the + * upstream DNS server(s). + * With exit mode ON: + * - DNS requests go to TUNNELIP:53, get send to libunbound, which forwards them to 127.0.0.1:1053, + * which encapsulates them in Apple's god awful crap, then (on a response) sends them back to + * libunbound. + * (This assumes a non-lokinet DNS; .loki and .snode get handled before either of these). + */ +@interface LLARPDNSTrampoline : NSObject +{ + // The socket libunbound talks with: + uv_udp_t request_socket; + // The reply address. This is a bit hacky: we configure libunbound to just use single address + // (rather than a range) so that we don't have to worry about tracking different reply addresses. + @public + struct sockaddr reply_addr; + // UDP "session" aimed at the upstream DNS + @public + NWUDPSession* upstream; + // Apple docs say writes could take time *and* the crappy Apple datagram write methods aren't + // callable again until the previous write finishes. Deal with this garbage API by queuing + // everything than using a uv_async to process the queue. + @public + int write_ready; + @public + NSMutableArray* pending_writes; + uv_async_t write_trigger; +} +- (void)startWithUpstreamDns:(NWUDPSession*)dns + listenPort:(uint16_t)listenPort + uvLoop:(uv_loop_t*)loop + completionHandler:(void (^)(NSError* error))completionHandler; + +- (void)flushWrites; + +- (void)dealloc; + +@end diff --git a/llarp/apple/DNSTrampoline.m b/llarp/apple/DNSTrampoline.m new file mode 100644 index 000000000..0a78a13e2 --- /dev/null +++ b/llarp/apple/DNSTrampoline.m @@ -0,0 +1,136 @@ +#include "DNSTrampoline.h" +#include + +NSString* error_domain = @"com.loki-project.lokinet"; + + +// Receiving an incoming packet, presumably from libunbound. NB: this is called from the libuv +// event loop. +static void on_request(uv_udp_t* socket, ssize_t nread, const uv_buf_t* buf, const struct sockaddr* addr, unsigned flags) { + if (nread < 0) { + NSLog(@"Read error: %s", uv_strerror(nread)); + free(buf->base); + return; + } + + if (nread == 0 || !addr) { + if (buf) + free(buf->base); + return; + } + + LLARPDNSTrampoline* t = (__bridge LLARPDNSTrampoline*) socket->data; + + // We configure libunbound to use just one single port so we'll just send replies to the last port + // to talk to us. (And we're only listening on localhost in the first place). + t->reply_addr = *addr; + + // NSData takes care of calling free(buf->base) for us with this constructor: + [t->pending_writes addObject:[NSData dataWithBytesNoCopy:buf->base length:nread]]; + + [t flushWrites]; +} + +static void on_sent(uv_udp_send_t* req, int status) { + NSArray* datagrams = (__bridge_transfer NSArray*) req->data; + free(req); +} + +// NB: called from the libuv event loop (so we don't have to worry about the above and this one +// running at once from different threads). +static void write_flusher(uv_async_t* async) { + LLARPDNSTrampoline* t = (__bridge LLARPDNSTrampoline*) async->data; + if (t->pending_writes.count == 0) + return; + + NSArray* data = [NSArray arrayWithArray:t->pending_writes]; + [t->pending_writes removeAllObjects]; + __weak LLARPDNSTrampoline* weakSelf = t; + [t->upstream writeMultipleDatagrams:data completionHandler: ^(NSError* error) + { + if (error) + NSLog(@"Failed to send request to upstream DNS: %@", error); + + // Trigger another flush in case anything built up while Apple was doing its things. Just + // call it unconditionally (rather than checking the queue) because this handler is probably + // running in some other thread. + [weakSelf flushWrites]; + } + ]; +} + + +static void alloc_buffer(uv_handle_t* handle, size_t suggested_size, uv_buf_t* buf) { + buf->base = malloc(suggested_size); + buf->len = suggested_size; +} + +@implementation LLARPDNSTrampoline + +- (void)startWithUpstreamDns:(NWUDPSession*) dns + listenPort:(uint16_t) listenPort + uvLoop:(uv_loop_t*) loop + completionHandler:(void (^)(NSError* error))completionHandler +{ + pending_writes = [[NSMutableArray alloc] init]; + write_trigger.data = (__bridge void*) self; + uv_async_init(loop, &write_trigger, write_flusher); + + request_socket.data = (__bridge void*) self; + uv_udp_init(loop, &request_socket); + struct sockaddr_in recv_addr; + uv_ip4_addr("127.0.0.1", listenPort, &recv_addr); + int ret = uv_udp_bind(&request_socket, (const struct sockaddr*) &recv_addr, UV_UDP_REUSEADDR); + if (ret < 0) { + NSString* errstr = [NSString stringWithFormat:@"Failed to start DNS trampoline: %s", uv_strerror(ret)]; + NSError *err = [NSError errorWithDomain:error_domain code:ret userInfo:@{@"Error": errstr}]; + NSLog(@"%@", err); + return completionHandler(err); + } + uv_udp_recv_start(&request_socket, alloc_buffer, on_request); + + NSLog(@"Starting DNS trampoline"); + + upstream = dns; + __weak LLARPDNSTrampoline* weakSelf = self; + [upstream setReadHandler: ^(NSArray* datagrams, NSError* error) { + // Reading a reply back from the UDP socket used to talk to upstream + if (error) { + NSLog(@"Reader handler failed: %@", error); + return; + } + LLARPDNSTrampoline* strongSelf = weakSelf; + if (!strongSelf || datagrams.count == 0) + return; + + uv_buf_t* buffers = malloc(datagrams.count * sizeof(uv_buf_t)); + size_t buf_count = 0; + for (NSData* packet in datagrams) { + buffers[buf_count].base = (void*) packet.bytes; + buffers[buf_count].len = packet.length; + buf_count++; + } + uv_udp_send_t* uvsend = malloc(sizeof(uv_udp_send_t)); + uvsend->data = (__bridge_retained void*) datagrams; + int ret = uv_udp_send(uvsend, &strongSelf->request_socket, buffers, buf_count, &strongSelf->reply_addr, on_sent); + free(buffers); + if (ret < 0) + NSLog(@"Error returning DNS responses to unbound: %s", uv_strerror(ret)); + } maxDatagrams:NSUIntegerMax]; + + completionHandler(nil); +} + +- (void)flushWrites +{ + uv_async_send(&write_trigger); +} + +- (void) dealloc +{ + NSLog(@"Stopping DNS trampoline"); + uv_close((uv_handle_t*) &request_socket, NULL); + uv_close((uv_handle_t*) &write_trigger, NULL); +} + +@end diff --git a/llarp/apple/PacketTunnelProvider.m b/llarp/apple/PacketTunnelProvider.m new file mode 100644 index 000000000..b340e56cb --- /dev/null +++ b/llarp/apple/PacketTunnelProvider.m @@ -0,0 +1,310 @@ +#include +#include +#include "context_wrapper.h" +#include "DNSTrampoline.h" + +@interface LLARPPacketTunnel : NEPacketTunnelProvider +{ + void* lokinet; + @public NEPacketTunnelNetworkSettings* settings; + @public NEIPv4Route* tun_route4; + @public NEIPv6Route* tun_route6; + LLARPDNSTrampoline* dns_tramp; +} + +- (void)startTunnelWithOptions:(NSDictionary*)options + completionHandler:(void (^)(NSError* error))completionHandler; + +- (void)stopTunnelWithReason:(NEProviderStopReason)reason + completionHandler:(void (^)(void))completionHandler; + +- (void)handleAppMessage:(NSData*)messageData + completionHandler:(void (^)(NSData* responseData))completionHandler; + +- (void)readPackets; + +- (void)updateNetworkSettings; + +@end + +static void nslogger(const char* msg) { NSLog(@"%s", msg); } + +static void packet_writer(int af, const void* data, size_t size, void* ctx) { + if (ctx == nil || data == nil) + return; + + NSData* buf = [NSData dataWithBytesNoCopy:(void*)data length:size freeWhenDone:NO]; + LLARPPacketTunnel* t = (__bridge LLARPPacketTunnel*) ctx; + NEPacket* packet = [[NEPacket alloc] initWithData:buf protocolFamily: af]; + [t.packetFlow writePacketObjects:@[packet]]; +} + +static void start_packet_reader(void* ctx) { + if (ctx == nil) + return; + + LLARPPacketTunnel* t = (__bridge LLARPPacketTunnel*) ctx; + [t readPackets]; +} + +static void add_ipv4_route(const char* addr, const char* netmask, void* ctx) { + NEIPv4Route* route = [[NEIPv4Route alloc] + initWithDestinationAddress: [NSString stringWithUTF8String:addr] + subnetMask: [NSString stringWithUTF8String:netmask]]; + + LLARPPacketTunnel* t = (__bridge LLARPPacketTunnel*) ctx; + for (NEIPv4Route* r in t->settings.IPv4Settings.includedRoutes) + if ([r.destinationAddress isEqualToString:route.destinationAddress] && + [r.destinationSubnetMask isEqualToString:route.destinationSubnetMask]) + return; // Already in the settings, nothing to add. + + t->settings.IPv4Settings.includedRoutes = + [t->settings.IPv4Settings.includedRoutes arrayByAddingObject:route]; + + [t updateNetworkSettings]; +} + +static void del_ipv4_route(const char* addr, const char* netmask, void* ctx) { + NEIPv4Route* route = [[NEIPv4Route alloc] + initWithDestinationAddress: [NSString stringWithUTF8String:addr] + subnetMask: [NSString stringWithUTF8String:netmask]]; + + LLARPPacketTunnel* t = (__bridge LLARPPacketTunnel*) ctx; + NSMutableArray* routes = [NSMutableArray arrayWithArray:t->settings.IPv4Settings.includedRoutes]; + for (int i = 0; i < routes.count; i++) { + if ([routes[i].destinationAddress isEqualToString:route.destinationAddress] && + [routes[i].destinationSubnetMask isEqualToString:route.destinationSubnetMask]) { + [routes removeObjectAtIndex:i]; + i--; + } + } + + if (routes.count != t->settings.IPv4Settings.includedRoutes.count) { + t->settings.IPv4Settings.includedRoutes = routes; + [t updateNetworkSettings]; + } +} + +static void add_ipv6_route(const char* addr, int prefix, void* ctx) { + NEIPv6Route* route = [[NEIPv6Route alloc] + initWithDestinationAddress: [NSString stringWithUTF8String:addr] + networkPrefixLength: [NSNumber numberWithInt:prefix]]; + + LLARPPacketTunnel* t = (__bridge LLARPPacketTunnel*) ctx; + for (NEIPv6Route* r in t->settings.IPv6Settings.includedRoutes) + if ([r.destinationAddress isEqualToString:route.destinationAddress] && + [r.destinationNetworkPrefixLength isEqualToNumber:route.destinationNetworkPrefixLength]) + return; // Already in the settings, nothing to add. + + t->settings.IPv6Settings.includedRoutes = + [t->settings.IPv6Settings.includedRoutes arrayByAddingObject:route]; + + [t updateNetworkSettings]; +} + +static void del_ipv6_route(const char* addr, int prefix, void* ctx) { + NEIPv6Route* route = [[NEIPv6Route alloc] + initWithDestinationAddress: [NSString stringWithUTF8String:addr] + networkPrefixLength: [NSNumber numberWithInt:prefix]]; + + LLARPPacketTunnel* t = (__bridge LLARPPacketTunnel*) ctx; + NSMutableArray* routes = [NSMutableArray arrayWithArray:t->settings.IPv6Settings.includedRoutes]; + for (int i = 0; i < routes.count; i++) { + if ([routes[i].destinationAddress isEqualToString:route.destinationAddress] && + [routes[i].destinationNetworkPrefixLength isEqualToNumber:route.destinationNetworkPrefixLength]) { + [routes removeObjectAtIndex:i]; + i--; + } + } + + if (routes.count != t->settings.IPv6Settings.includedRoutes.count) { + t->settings.IPv6Settings.includedRoutes = routes; + [t updateNetworkSettings]; + } +} + +static void add_default_route(void* ctx) { + LLARPPacketTunnel* t = (__bridge LLARPPacketTunnel*) ctx; + + t->settings.IPv4Settings.includedRoutes = @[NEIPv4Route.defaultRoute]; + t->settings.IPv6Settings.includedRoutes = @[NEIPv6Route.defaultRoute]; + + [t updateNetworkSettings]; +} + +static void del_default_route(void* ctx) { + LLARPPacketTunnel* t = (__bridge LLARPPacketTunnel*) ctx; + + t->settings.IPv4Settings.includedRoutes = @[t->tun_route4]; + t->settings.IPv6Settings.includedRoutes = @[t->tun_route6]; + + [t updateNetworkSettings]; +} + +@implementation LLARPPacketTunnel + +- (void)readPackets +{ + [self.packetFlow readPacketObjectsWithCompletionHandler: ^(NSArray* packets) { + if (lokinet == nil) + return; + for (NEPacket* p in packets) { + llarp_apple_incoming(lokinet, p.data.bytes, p.data.length); + } + [self readPackets]; + }]; +} + +- (void)startTunnelWithOptions:(NSDictionary*)options + completionHandler:(void (^)(NSError*))completionHandler +{ + NSString* default_bootstrap = [NSBundle.mainBundle pathForResource:@"bootstrap" ofType:@"signed"]; + NSString* home = NSHomeDirectory(); + + llarp_apple_config conf = { + .config_dir = home.UTF8String, + .default_bootstrap = default_bootstrap.UTF8String, + .ns_logger = nslogger, + .packet_writer = packet_writer, + .start_reading = start_packet_reader, + .route_callbacks = { + .add_ipv4_route = add_ipv4_route, + .del_ipv4_route = del_ipv4_route, + .add_ipv6_route = add_ipv6_route, + .del_ipv6_route = del_ipv6_route, + .add_default_route = add_default_route, + .del_default_route = del_default_route + }, + }; + + lokinet = llarp_apple_init(&conf); + if (!lokinet) { + NSError *init_failure = [NSError errorWithDomain:error_domain code:500 userInfo:@{@"Error": @"Failed to initialize lokinet"}]; + NSLog(@"%@", [init_failure localizedDescription]); + return completionHandler(init_failure); + } + + NSString* ip = [NSString stringWithUTF8String:conf.tunnel_ipv4_ip]; + NSString* mask = [NSString stringWithUTF8String:conf.tunnel_ipv4_netmask]; + + // We don't have a fixed address so just stick some bogus value here: + settings = [[NEPacketTunnelNetworkSettings alloc] initWithTunnelRemoteAddress:@"127.3.2.1"]; + + NEDNSSettings* dns = [[NEDNSSettings alloc] initWithServers:@[ip]]; + dns.domainName = @"localhost.loki"; + dns.matchDomains = @[@""]; + // In theory, matchDomains is supposed to be set to DNS suffixes that we resolve. This seems + // highly unreliable, though: often it just doesn't work at all (perhaps only if we make ourselves + // the default route?), and even when it does work, it seems there are secret reasons that some + // domains (such as instagram.com) still won't work because there's some magic sauce in the OS + // that Apple engineers don't want to disclose ("This is what I expected, actually. Although I + // will not comment on what I believe is happening here", from + // https://developer.apple.com/forums/thread/685410). + // + // So the documentation sucks and the feature doesn't appear to work, so as much as it would be + // nice to capture only .loki and .snode when not in exit mode, we can't, so capture everything + // and use our default upstream. + dns.matchDomains = @[@""]; + dns.matchDomainsNoSearch = true; + dns.searchDomains = @[]; + settings.DNSSettings = dns; + + NWHostEndpoint* upstreamdns_ep; + if (strlen(conf.upstream_dns)) + upstreamdns_ep = [NWHostEndpoint endpointWithHostname:[NSString stringWithUTF8String:conf.upstream_dns] port:@(conf.upstream_dns_port).stringValue]; + + NEIPv4Settings* ipv4 = [[NEIPv4Settings alloc] initWithAddresses:@[ip] + subnetMasks:@[mask]]; + tun_route4 = [[NEIPv4Route alloc] initWithDestinationAddress:ip subnetMask: mask]; + ipv4.includedRoutes = @[tun_route4]; + settings.IPv4Settings = ipv4; + + NSString* ip6 = [NSString stringWithUTF8String:conf.tunnel_ipv6_ip]; + NSNumber* ip6_prefix = [NSNumber numberWithUnsignedInt:conf.tunnel_ipv6_prefix]; + NEIPv6Settings* ipv6 = [[NEIPv6Settings alloc] initWithAddresses:@[ip6] + networkPrefixLengths:@[ip6_prefix]]; + tun_route6 = [[NEIPv6Route alloc] initWithDestinationAddress:ip6 + networkPrefixLength:ip6_prefix]; + ipv6.includedRoutes = @[tun_route6]; + settings.IPv6Settings = ipv6; + + __weak LLARPPacketTunnel* weakSelf = self; + [self setTunnelNetworkSettings:settings completionHandler:^(NSError* err) { + if (err) { + NSLog(@"Failed to configure lokinet tunnel: %@", err); + return completionHandler(err); + } + LLARPPacketTunnel* strongSelf = weakSelf; + if (!strongSelf) + return completionHandler(nil); + + int start_ret = llarp_apple_start(strongSelf->lokinet, (__bridge void*) strongSelf); + if (start_ret != 0) { + NSError *start_failure = [NSError errorWithDomain:error_domain code:start_ret userInfo:@{@"Error": @"Failed to start lokinet"}]; + NSLog(@"%@", start_failure); + lokinet = nil; + return completionHandler(start_failure); + } + + NSLog(@"Starting DNS exit mode trampoline to %@ on 127.0.0.1:%d", upstreamdns_ep, dns_trampoline_port); + NWUDPSession* upstreamdns = [strongSelf createUDPSessionThroughTunnelToEndpoint:upstreamdns_ep fromEndpoint:nil]; + strongSelf->dns_tramp = [LLARPDNSTrampoline alloc]; + [strongSelf->dns_tramp + startWithUpstreamDns:upstreamdns + listenPort:dns_trampoline_port + uvLoop:llarp_apple_get_uv_loop(strongSelf->lokinet) + completionHandler:^(NSError* error) { + if (error) + NSLog(@"Error starting dns trampoline: %@", error); + return completionHandler(error); + }]; + }]; +} + +- (void)stopTunnelWithReason:(NEProviderStopReason)reason + completionHandler:(void (^)(void))completionHandler +{ + if (lokinet) { + llarp_apple_shutdown(lokinet); + lokinet = nil; + } + completionHandler(); +} + +- (void)handleAppMessage:(NSData*)messageData + completionHandler:(void (^)(NSData* responseData))completionHandler +{ + NSData* response = [NSData dataWithBytesNoCopy:"ok" length:3 freeWhenDone:NO]; + completionHandler(response); +} + +- (void)updateNetworkSettings +{ + self.reasserting = YES; + __weak LLARPPacketTunnel* weakSelf = self; + // Apple documentation says that setting network settings to nil isn't required before setting it + // to a new value. Apple lies: both end up with a routing table that looks exactly the same (from + // both `netstat -rn` and from everything that happens in `route -n monitor`), but if we don't + // call with nil first then everything fails to route to either lokinet *and* clearnet through the + // exit, so there is apparently some special magic internal Apple state that actually *does* + // require the tunnel settings being reset with nil first. + // + // Thanks for the accurate documentation, Apple. + // + [self setTunnelNetworkSettings:nil completionHandler:^(NSError* err) { + if (err) + NSLog(@"Failed to clear lokinet tunnel settings: %@", err); + LLARPPacketTunnel* strongSelf = weakSelf; + if (strongSelf) { + [weakSelf setTunnelNetworkSettings:strongSelf->settings completionHandler:^(NSError* err) { + LLARPPacketTunnel* strongSelf = weakSelf; + if (strongSelf) + strongSelf.reasserting = NO; + if (err) + NSLog(@"Failed to reconfigure lokinet tunnel settings: %@", err); + }]; + } + }]; +} + +@end diff --git a/llarp/apple/apple_logger.cpp b/llarp/apple/apple_logger.cpp new file mode 100644 index 000000000..4a41c78cf --- /dev/null +++ b/llarp/apple/apple_logger.cpp @@ -0,0 +1,25 @@ +#include "apple_logger.hpp" + +namespace llarp::apple +{ + void + NSLogStream::PreLog( + std::stringstream& ss, + LogLevel lvl, + const char* fname, + int lineno, + const std::string& nodename) const + { + ss << "[" << LogLevelToString(lvl) << "] "; + ss << "[" << nodename << "]" + << "(" << thread_id_string() << ") " << log_timestamp() << " " << fname << ":" << lineno + << "\t"; + } + + void + NSLogStream::Print(LogLevel, const char*, const std::string& msg) + { + ns_logger(msg.c_str()); + } + +} // namespace llarp::apple diff --git a/llarp/apple/apple_logger.hpp b/llarp/apple/apple_logger.hpp new file mode 100644 index 000000000..b97810c76 --- /dev/null +++ b/llarp/apple/apple_logger.hpp @@ -0,0 +1,40 @@ +#pragma once + +#include +#include + +namespace llarp::apple +{ + struct NSLogStream : public ILogStream + { + using ns_logger_callback = void (*)(const char* log_this); + + NSLogStream(ns_logger_callback logger) : ns_logger{logger} + {} + + void + PreLog( + std::stringstream& s, + LogLevel lvl, + const char* fname, + int lineno, + const std::string& nodename) const override; + + void + Print(LogLevel lvl, const char* tag, const std::string& msg) override; + + void + PostLog(std::stringstream& ss) const override + {} + + void + ImmediateFlush() override + {} + + void Tick(llarp_time_t) override + {} + + private: + ns_logger_callback ns_logger; + }; +} // namespace llarp::apple diff --git a/llarp/apple/context.hpp b/llarp/apple/context.hpp new file mode 100644 index 000000000..4fc808874 --- /dev/null +++ b/llarp/apple/context.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include +#include "vpn_platform.hpp" +#include "route_manager.hpp" + +namespace llarp::apple +{ + struct Context : public llarp::Context + { + std::shared_ptr + makeVPNPlatform() override + { + return std::make_shared( + *this, m_PacketWriter, m_OnReadable, route_callbacks, callback_context); + } + + // Callbacks that must be set for packet handling *before* calling Setup/Configure/Run; the main + // point of these is to get passed through to VPNInterface, which will be called during setup, + // after construction. + VPNInterface::packet_write_callback m_PacketWriter; + VPNInterface::on_readable_callback m_OnReadable; + llarp_route_callbacks route_callbacks{}; + void* callback_context = nullptr; + }; + +} // namespace llarp::apple diff --git a/llarp/apple/context_wrapper.cpp b/llarp/apple/context_wrapper.cpp new file mode 100644 index 000000000..09b45ba9a --- /dev/null +++ b/llarp/apple/context_wrapper.cpp @@ -0,0 +1,194 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include "vpn_interface.hpp" +#include "context_wrapper.h" +#include "context.hpp" +#include "apple_logger.hpp" + +namespace +{ + // The default 127.0.0.1:53 won't work (because we run unprivileged) so remap it to this (unless + // specifically overridden to something else in the config): + const llarp::SockAddr DefaultDNSBind{"127.0.0.1:1153"}; + + struct instance_data + { + llarp::apple::Context context; + std::thread runner; + packet_writer_callback packet_writer; + start_reading_callback start_reading; + + std::weak_ptr iface; + }; + +} // namespace + +const uint16_t dns_trampoline_port = 1053; + +void* +llarp_apple_init(llarp_apple_config* appleconf) +{ + llarp::LogContext::Instance().logStream = + std::make_unique(appleconf->ns_logger); + + try + { + auto config_dir = fs::u8path(appleconf->config_dir); + auto config = std::make_shared(config_dir); + fs::path config_path = config_dir / "lokinet.ini"; + if (!fs::exists(config_path)) + llarp::ensureConfig(config_dir, config_path, /*overwrite=*/false, /*router=*/false); + config->Load(config_path); + + // If no range is specified then go look for a free one, set that in the config, and then return + // it to the caller via the char* parameters. + auto& range = config->network.m_ifaddr; + if (!range.addr.h) + { + if (auto maybe = llarp::FindFreeRange()) + range = *maybe; + else + throw std::runtime_error{"Could not find any free IP range"}; + } + auto addr = llarp::net::TruncateV6(range.addr).ToString(); + auto mask = llarp::net::TruncateV6(range.netmask_bits).ToString(); + if (addr.size() > 15 || mask.size() > 15) + throw std::runtime_error{"Unexpected non-IPv4 tunnel range configured"}; + std::strncpy(appleconf->tunnel_ipv4_ip, addr.c_str(), sizeof(appleconf->tunnel_ipv4_ip)); + std::strncpy( + appleconf->tunnel_ipv4_netmask, mask.c_str(), sizeof(appleconf->tunnel_ipv4_netmask)); + + // TODO: in the future we want to do this properly with our pubkey (see issue #1705), but that's + // going to take a bit more work because we currently can't *get* the (usually) ephemeral pubkey + // at this stage of lokinet configuration. So for now we just stick our IPv4 address into it + // until #1705 gets implemented. + llarp::huint128_t ipv6{ + llarp::uint128_t{0xfd2e'6c6f'6b69'0000, llarp::net::TruncateV6(range.addr).h}}; + std::strncpy( + appleconf->tunnel_ipv6_ip, ipv6.ToString().c_str(), sizeof(appleconf->tunnel_ipv6_ip)); + appleconf->tunnel_ipv6_prefix = 48; + + appleconf->upstream_dns[0] = '\0'; + for (auto& upstream : config->dns.m_upstreamDNS) + { + if (upstream.isIPv4()) + { + std::strcpy(appleconf->upstream_dns, upstream.hostString().c_str()); + appleconf->upstream_dns_port = upstream.getPort(); + break; + } + } + + // The default DNS bind setting just isn't something we can use as a non-root network extension + // so remap the default value to a high port unless explicitly set to something else. + if (config->dns.m_bind == llarp::SockAddr{"127.0.0.1:53"}) + config->dns.m_bind = DefaultDNSBind; + + // If no explicit bootstrap then set the system default one included with the app bundle + if (config->bootstrap.files.empty()) + config->bootstrap.files.push_back(fs::u8path(appleconf->default_bootstrap)); + + auto inst = std::make_unique(); + inst->context.Configure(std::move(config)); + inst->context.route_callbacks = appleconf->route_callbacks; + + inst->packet_writer = appleconf->packet_writer; + inst->start_reading = appleconf->start_reading; + + return inst.release(); + } + catch (const std::exception& e) + { + LogError("Failed to initialize lokinet from config: ", e.what()); + } + return nullptr; +} + +int +llarp_apple_start(void* lokinet, void* callback_context) +{ + auto* inst = static_cast(lokinet); + + inst->context.callback_context = callback_context; + + inst->context.m_PacketWriter = [inst, callback_context](int af_family, void* data, size_t size) { + inst->packet_writer(af_family, data, size, callback_context); + return true; + }; + + inst->context.m_OnReadable = [inst, callback_context](llarp::apple::VPNInterface& iface) { + inst->iface = iface.weak_from_this(); + inst->start_reading(callback_context); + }; + + std::promise result; + inst->runner = std::thread{[inst, &result] { + const llarp::RuntimeOptions opts{}; + try + { + inst->context.Setup(opts); + } + catch (...) + { + result.set_exception(std::current_exception()); + return; + } + result.set_value(); + inst->context.Run(opts); + }}; + + try + { + result.get_future().get(); + } + catch (const std::exception& e) + { + LogError("Failed to initialize lokinet: ", e.what()); + return -1; + } + + return 0; +} + +uv_loop_t* +llarp_apple_get_uv_loop(void* lokinet) +{ + auto& inst = *static_cast(lokinet); + auto uvw = inst.context.loop->MaybeGetUVWLoop(); + assert(uvw); + return uvw->raw(); +} + +int +llarp_apple_incoming(void* lokinet, const void* bytes, size_t size) +{ + auto& inst = *static_cast(lokinet); + + auto iface = inst.iface.lock(); + if (!iface) + return -2; + + llarp_buffer_t buf{static_cast(bytes), size}; + if (iface->OfferReadPacket(buf)) + return 0; + + LogError("invalid IP packet: ", llarp::buffer_printer(buf)); + return -1; +} + +void +llarp_apple_shutdown(void* lokinet) +{ + auto* inst = static_cast(lokinet); + + inst->context.CloseAsync(); + inst->context.Wait(); + inst->runner.join(); + delete inst; +} diff --git a/llarp/apple/context_wrapper.h b/llarp/apple/context_wrapper.h new file mode 100644 index 000000000..37c8a5c7b --- /dev/null +++ b/llarp/apple/context_wrapper.h @@ -0,0 +1,152 @@ +#pragma once + +// C-linkage wrappers for interacting with a lokinet context, so that we can call them from Swift +// code (which currently doesn't support C++ interoperability at all). + +#ifdef __cplusplus +extern "C" +{ +#endif + +#include +#include +#include + + // Port (on localhost) for our DNS trampoline for bouncing DNS requests through the exit route + // when in exit mode. + extern const uint16_t dns_trampoline_port; + + /// C callback function for us to invoke when we need to write a packet + typedef void (*packet_writer_callback)(int af, const void* data, size_t size, void* ctx); + + /// C callback function to invoke once we are ready to start receiving packets + typedef void (*start_reading_callback)(void* ctx); + + /// C callback that bridges things into NSLog + typedef void (*ns_logger_callback)(const char* msg); + + /// C callbacks to add/remove specific and default routes to the tunnel + typedef void (*llarp_route_ipv4_callback)(const char* addr, const char* netmask, void* ctx); + typedef void (*llarp_route_ipv6_callback)(const char* addr, int prefix, void* ctx); + typedef void (*llarp_default_route_callback)(void* ctx); + typedef struct llarp_route_callbacks + { + /// Callback invoked to set up an IPv4 range that should be routed through the tunnel + /// interface. Called with the address and netmask. + llarp_route_ipv4_callback add_ipv4_route; + + /// Callback invoked to set the tunnel as the default IPv4 route. + llarp_default_route_callback add_ipv4_default_route; + + /// Callback invoked to remove a specific range from the tunnel IPv4 routes. Called with the + /// address and netmask. + llarp_route_ipv4_callback del_ipv4_route; + + /// Callback invoked to set up an IPv6 range that should be routed through the tunnel + /// interface. Called with the address and netmask. + llarp_route_ipv6_callback add_ipv6_route; + + /// Callback invoked to remove a specific range from the tunnel IPv6 routes. Called with the + /// address and netmask. + llarp_route_ipv6_callback del_ipv6_route; + + /// Callback invoked to set the tunnel as the default IPv4/IPv6 route. + llarp_default_route_callback add_default_route; + + /// Callback invoked to remove the tunnel as the default IPv4/IPv6 route. + llarp_default_route_callback del_default_route; + } llarp_route_callbacks; + + /// Pack of crap to be passed into llarp_apple_init to initialize + typedef struct llarp_apple_config + { + /// lokinet configuration directory, expected to be the application-specific "home" directory, + /// which is where state files are stored and the lokinet.ini will be loaded (or created if it + /// doesn't exist). + const char* config_dir; + /// path to the default bootstrap.signed file included in installation, which will be used by + /// default when no specific bootstrap is in the config file. + const char* default_bootstrap; + /// llarp_apple_init writes the IP address for the primary tunnel IP address here, + /// null-terminated. + char tunnel_ipv4_ip[INET_ADDRSTRLEN]; + /// llarp_apple_init writes the netmask of the tunnel address here, null-terminated. + char tunnel_ipv4_netmask[INET_ADDRSTRLEN]; + /// Writes the IPv6 address for the tunnel here, null-terminated. + char tunnel_ipv6_ip[INET6_ADDRSTRLEN]; + /// IPv6 address prefix. + uint16_t tunnel_ipv6_prefix; + + /// The first upstream DNS server's IPv4 address the OS should use when in exit mode. + /// (Currently on mac in exit mode we only support querying the first such configured server). + char upstream_dns[INET_ADDRSTRLEN]; + uint16_t upstream_dns_port; + + /// \defgroup callbacks Callbacks + /// Callbacks we invoke for various operations that require glue into the Apple network + /// extension APIs. All of these except for ns_logger are passed the pointer provided to + /// llarp_apple_start when invoked. + /// @{ + + /// simple wrapper around NSLog for lokinet message logging + ns_logger_callback ns_logger; + + /// C function callback that will be called when we need to write a packet to the packet + /// tunnel. Will be passed AF_INET or AF_INET6, a void pointer to the data, and the size of + /// the data in bytes. + packet_writer_callback packet_writer; + + /// C function callback that will be called when lokinet is setup and ready to start receiving + /// packets from the packet tunnel. This should set up the read handler to deliver packets + /// via llarp_apple_incoming. + start_reading_callback start_reading; + + /// Callbacks invoked to add/remove routes to the tunnel. + llarp_route_callbacks route_callbacks; + + /// @} + } llarp_apple_config; + + /// Initializes a lokinet instance by initializing various objects and loading the configuration + /// (if /lokinet.ini exists). Does not actually start lokinet (call llarp_apple_start + /// for that). + /// + /// Returns NULL if there was a problem initializing/loading the configuration, otherwise returns + /// an opaque void pointer that should be passed into the other llarp_apple_* functions. + /// + /// \param config pointer to a llarp_apple_config where we get the various settings needed + /// and return the ip/mask/dns fields needed for the tunnel. + void* + llarp_apple_init(llarp_apple_config* config); + + /// Starts the lokinet instance in a new thread. + /// + /// \param lokinet the void pointer returned by llarp_apple_init + /// + /// \param callback_context Opaque pointer that is passed into the various callbacks provided to + /// llarp_apple_init. This code does nothing with this pointer aside from passing it through to + /// callbacks. + /// + /// \returns 0 on succesful startup, -1 on failure. + int + llarp_apple_start(void* lokinet, void* callback_context); + + /// Returns a pointer to the uv event loop. Must have called llarp_apple_start already. + uv_loop_t* + llarp_apple_get_uv_loop(void* lokinet); + + /// Called to deliver an incoming packet from the apple layer into lokinet; returns 0 on success, + /// -1 if the packet could not be parsed, -2 if there is no current active VPNInterface associated + /// with the lokinet (which generally means llarp_apple_start wasn't called or failed, or lokinet + /// is in the process of shutting down). + int + llarp_apple_incoming(void* lokinet, const void* bytes, size_t size); + + /// Stops a lokinet instance created with `llarp_apple_initialize`. This waits for lokinet to + /// shut down and rejoins the thread. After this call the given pointer is no longer valid. + void + llarp_apple_shutdown(void* lokinet); + +#ifdef __cplusplus +} // extern "C" +#endif diff --git a/llarp/apple/route_manager.cpp b/llarp/apple/route_manager.cpp new file mode 100644 index 000000000..0bf170577 --- /dev/null +++ b/llarp/apple/route_manager.cpp @@ -0,0 +1,101 @@ +#include "route_manager.hpp" +#include +#include +#include +#include + +namespace llarp::apple +{ + void + RouteManager::check_trampoline(bool enable) + { + if (trampoline_active == enable) + return; + auto router = context.router; + if (!router) + { + LogError("Cannot reconfigure to use DNS trampoline: no router"); + return; + } + + std::shared_ptr tun; + router->hiddenServiceContext().ForEachService([&tun](const auto& name, const auto ep) { + tun = std::dynamic_pointer_cast(ep); + return !tun; + }); + + if (!tun) + { + LogError("Cannot reconfigure to use DNS trampoline: no tun endpoint found (!?)"); + return; + } + + if (enable) + saved_upstream_dns = + tun->ReconfigureDNS({SockAddr{127, 0, 0, 1, huint16_t{dns_trampoline_port}}}); + else + tun->ReconfigureDNS(std::move(saved_upstream_dns)); + trampoline_active = enable; + } + + void RouteManager::AddDefaultRouteViaInterface(std::string) + { + check_trampoline(true); + if (callback_context and route_callbacks.add_default_route) + route_callbacks.add_default_route(callback_context); + } + + void RouteManager::DelDefaultRouteViaInterface(std::string) + { + check_trampoline(false); + if (callback_context and route_callbacks.del_default_route) + route_callbacks.del_default_route(callback_context); + } + + void + RouteManager::AddRouteViaInterface(vpn::NetworkInterface&, IPRange range) + { + check_trampoline(true); + if (callback_context) + { + if (range.IsV4()) + { + if (route_callbacks.add_ipv4_route) + route_callbacks.add_ipv4_route( + range.BaseAddressString().c_str(), + net::TruncateV6(range.netmask_bits).ToString().c_str(), + callback_context); + } + else + { + if (route_callbacks.add_ipv6_route) + route_callbacks.add_ipv6_route( + range.BaseAddressString().c_str(), range.HostmaskBits(), callback_context); + } + } + } + + void + RouteManager::DelRouteViaInterface(vpn::NetworkInterface&, IPRange range) + { + check_trampoline(false); + if (callback_context) + { + if (range.IsV4()) + { + if (route_callbacks.del_ipv4_route) + route_callbacks.del_ipv4_route( + range.BaseAddressString().c_str(), + net::TruncateV6(range.netmask_bits).ToString().c_str(), + callback_context); + } + else + { + if (route_callbacks.del_ipv6_route) + route_callbacks.del_ipv6_route( + range.BaseAddressString().c_str(), range.HostmaskBits(), callback_context); + } + } + } + +} // namespace llarp::apple diff --git a/llarp/apple/route_manager.hpp b/llarp/apple/route_manager.hpp new file mode 100644 index 000000000..cb8bb3f1b --- /dev/null +++ b/llarp/apple/route_manager.hpp @@ -0,0 +1,59 @@ +#pragma once + +#include +#include +#include "context_wrapper.h" + +namespace llarp::apple +{ + class RouteManager final : public llarp::vpn::IRouteManager + { + public: + RouteManager(llarp::Context& ctx, llarp_route_callbacks rcs, void* callback_context) + : context{ctx}, route_callbacks{std::move(rcs)}, callback_context{callback_context} + {} + + /// These are called for poking route holes, but we don't have to do that at all on macos + /// because the appex isn't subject to its own rules. + void + AddRoute(IPVariant_t ip, IPVariant_t gateway) override + {} + + void + DelRoute(IPVariant_t ip, IPVariant_t gateway) override + {} + + void + AddDefaultRouteViaInterface(std::string ifname) override; + + void + DelDefaultRouteViaInterface(std::string ifname) override; + + void + AddRouteViaInterface(vpn::NetworkInterface& vpn, IPRange range) override; + + void + DelRouteViaInterface(vpn::NetworkInterface& vpn, IPRange range) override; + + virtual std::vector + GetGatewaysNotOnInterface(std::string ifname) override + { + // We can't get this on mac from our sandbox, but we don't actually need it because we + // ignore the gateway for AddRoute/DelRoute anyway, so just return a zero IP. + std::vector ret; + ret.push_back(huint32_t{0}); + return ret; + } + + private: + llarp::Context& context; + bool trampoline_active = false; + std::vector saved_upstream_dns; + void + check_trampoline(bool enable); + + void* callback_context = nullptr; + llarp_route_callbacks route_callbacks; + }; + +} // namespace llarp::apple diff --git a/llarp/apple/vpn_interface.cpp b/llarp/apple/vpn_interface.cpp new file mode 100644 index 000000000..079e24aa9 --- /dev/null +++ b/llarp/apple/vpn_interface.cpp @@ -0,0 +1,52 @@ + +#include "vpn_interface.hpp" +#include "context.hpp" + +namespace llarp::apple +{ + VPNInterface::VPNInterface( + Context& ctx, packet_write_callback packet_writer, on_readable_callback on_readable) + : m_PacketWriter{std::move(packet_writer)}, m_OnReadable{std::move(on_readable)} + { + ctx.loop->call_soon([this] { m_OnReadable(*this); }); + } + + bool + VPNInterface::OfferReadPacket(const llarp_buffer_t& buf) + { + llarp::net::IPPacket pkt; + if (!pkt.Load(buf)) + return false; + m_ReadQueue.tryPushBack(std::move(pkt)); + return true; + } + + int + VPNInterface::PollFD() const + { + return -1; + } + + std::string + VPNInterface::IfName() const + { + return ""; + } + + net::IPPacket + VPNInterface::ReadNextPacket() + { + net::IPPacket pkt{}; + if (not m_ReadQueue.empty()) + pkt = m_ReadQueue.popFront(); + return pkt; + } + + bool + VPNInterface::WritePacket(net::IPPacket pkt) + { + int af_family = pkt.IsV6() ? AF_INET6 : AF_INET; + return m_PacketWriter(af_family, pkt.buf, pkt.sz); + } + +} // namespace llarp::apple diff --git a/llarp/apple/vpn_interface.hpp b/llarp/apple/vpn_interface.hpp new file mode 100644 index 000000000..c1dff8dbf --- /dev/null +++ b/llarp/apple/vpn_interface.hpp @@ -0,0 +1,51 @@ +#pragma once + +#include +#include +#include +#include + +namespace llarp::apple +{ + struct Context; + + class VPNInterface final : public vpn::NetworkInterface, + public std::enable_shared_from_this + { + public: + using packet_write_callback = std::function; + using on_readable_callback = std::function; + + explicit VPNInterface( + Context& ctx, packet_write_callback packet_writer, on_readable_callback on_readable); + + // Method to call when a packet has arrived to deliver the packet to lokinet + bool + OfferReadPacket(const llarp_buffer_t& buf); + + int + PollFD() const override; + + std::string + IfName() const override; + + net::IPPacket + ReadNextPacket() override; + + bool + WritePacket(net::IPPacket pkt) override; + + private: + // Function for us to call when we have a packet to emit. Should return true if the packet was + // handed off to the OS successfully. + packet_write_callback m_PacketWriter; + + // Called when we are ready to start reading packets + on_readable_callback m_OnReadable; + + static inline constexpr auto PacketQueueSize = 1024; + + thread::Queue m_ReadQueue{PacketQueueSize}; + }; + +} // namespace llarp::apple diff --git a/llarp/apple/vpn_platform.cpp b/llarp/apple/vpn_platform.cpp new file mode 100644 index 000000000..b11c0b05b --- /dev/null +++ b/llarp/apple/vpn_platform.cpp @@ -0,0 +1,22 @@ +#include "vpn_platform.hpp" +#include "context.hpp" + +namespace llarp::apple +{ + VPNPlatform::VPNPlatform( + Context& ctx, + VPNInterface::packet_write_callback packet_writer, + VPNInterface::on_readable_callback on_readable, + llarp_route_callbacks route_callbacks, + void* callback_context) + : m_Context{ctx} + , m_RouteManager{ctx, std::move(route_callbacks), callback_context} + , m_PacketWriter{std::move(packet_writer)} + , m_OnReadable{std::move(on_readable)} + {} + + std::shared_ptr VPNPlatform::ObtainInterface(vpn::InterfaceInfo) + { + return std::make_shared(m_Context, m_PacketWriter, m_OnReadable); + } +} // namespace llarp::apple diff --git a/llarp/apple/vpn_platform.hpp b/llarp/apple/vpn_platform.hpp new file mode 100644 index 000000000..04ce75646 --- /dev/null +++ b/llarp/apple/vpn_platform.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include +#include "vpn_interface.hpp" +#include "route_manager.hpp" + +namespace llarp::apple +{ + class VPNPlatform final : public vpn::Platform + { + public: + explicit VPNPlatform( + Context& ctx, + VPNInterface::packet_write_callback packet_writer, + VPNInterface::on_readable_callback on_readable, + llarp_route_callbacks route_callbacks, + void* callback_context); + + std::shared_ptr ObtainInterface(vpn::InterfaceInfo) override; + + vpn::IRouteManager& + RouteManager() override + { + return m_RouteManager; + } + + private: + Context& m_Context; + apple::RouteManager m_RouteManager; + VPNInterface::packet_write_callback m_PacketWriter; + VPNInterface::on_readable_callback m_OnReadable; + }; + +} // namespace llarp::apple diff --git a/llarp/context.cpp b/llarp/context.cpp index 9bf260ce7..59a54dc28 100644 --- a/llarp/context.cpp +++ b/llarp/context.cpp @@ -138,8 +138,8 @@ namespace llarp if (IsStopping()) return; - if (CallSafe(std::bind(&Context::HandleSignal, this, SIGTERM))) - closeWaiter = std::make_unique>(); + loop->call([this]() { HandleSignal(SIGTERM); }); + closeWaiter = std::make_unique>(); } bool diff --git a/llarp/dns/message.cpp b/llarp/dns/message.cpp index 99ffed763..f10e9b9c6 100644 --- a/llarp/dns/message.cpp +++ b/llarp/dns/message.cpp @@ -48,6 +48,12 @@ namespace llarp return true; } + util::StatusObject + MessageHeader::ToJSON() const + { + return util::StatusObject{}; + } + Message::Message(Message&& other) : hdr_id(std::move(other.hdr_id)) , hdr_fields(std::move(other.hdr_fields)) @@ -74,6 +80,11 @@ namespace llarp additional.resize(size_t(hdr.ar_count)); } + Message::Message(const Question& question) : hdr_id{0}, hdr_fields{} + { + questions.emplace_back(question); + } + bool Message::Encode(llarp_buffer_t* buf) const { @@ -122,6 +133,22 @@ namespace llarp return true; } + util::StatusObject + Message::ToJSON() const + { + std::vector ques; + std::vector ans; + for (const auto& q : questions) + { + ques.push_back(q.ToJSON()); + } + for (const auto& a : answers) + { + ans.push_back(a.ToJSON()); + } + return util::StatusObject{{"questions", ques}, {"answers", ans}}; + } + OwnedBuffer Message::ToBuffer() const { diff --git a/llarp/dns/message.hpp b/llarp/dns/message.hpp index b7d4571b1..937c37d97 100644 --- a/llarp/dns/message.hpp +++ b/llarp/dns/message.hpp @@ -33,6 +33,9 @@ namespace llarp bool Decode(llarp_buffer_t* buf) override; + util::StatusObject + ToJSON() const override; + bool operator==(const MessageHeader& other) const { @@ -44,11 +47,15 @@ namespace llarp struct Message : public Serialize { - Message(const MessageHeader& hdr); + explicit Message(const MessageHeader& hdr); + explicit Message(const Question& question); Message(Message&& other); Message(const Message& other); + util::StatusObject + ToJSON() const override; + void AddNXReply(RR_TTL_t ttl = 1); diff --git a/llarp/dns/question.cpp b/llarp/dns/question.cpp index 7ec066c30..a8c60c260 100644 --- a/llarp/dns/question.cpp +++ b/llarp/dns/question.cpp @@ -3,6 +3,7 @@ #include #include #include +#include "dns.hpp" namespace llarp { @@ -17,6 +18,13 @@ namespace llarp : qname(other.qname), qtype(other.qtype), qclass(other.qclass) {} + Question::Question(std::string name, QType_t type) + : qname{std::move(name)}, qtype{type}, qclass{qClassIN} + { + if (qname.empty()) + throw std::invalid_argument{"qname cannot be empty"}; + } + bool Question::Encode(llarp_buffer_t* buf) const { @@ -48,6 +56,12 @@ namespace llarp return true; } + util::StatusObject + Question::ToJSON() const + { + return util::StatusObject{{"qname", qname}, {"qtype", qtype}, {"qclass", qclass}}; + } + bool Question::IsName(const std::string& other) const { diff --git a/llarp/dns/question.hpp b/llarp/dns/question.hpp index f2fc00076..5434bbcdd 100644 --- a/llarp/dns/question.hpp +++ b/llarp/dns/question.hpp @@ -14,6 +14,9 @@ namespace llarp struct Question : public Serialize { Question() = default; + + explicit Question(std::string name, QType_t type); + Question(Question&& other); Question(const Question& other); bool @@ -58,6 +61,9 @@ namespace llarp /// determine if we are using this TLD bool HasTLD(const std::string& tld) const; + + util::StatusObject + ToJSON() const override; }; inline std::ostream& diff --git a/llarp/dns/rr.cpp b/llarp/dns/rr.cpp index 3fac87e9b..82ed20470 100644 --- a/llarp/dns/rr.cpp +++ b/llarp/dns/rr.cpp @@ -24,6 +24,14 @@ namespace llarp , rData(std::move(other.rData)) {} + ResourceRecord::ResourceRecord(Name_t name, RRType_t type, RR_RData_t data) + : rr_name{std::move(name)} + , rr_type{type} + , rr_class{qClassIN} + , ttl{1} + , rData{std::move(data)} + {} + bool ResourceRecord::Encode(llarp_buffer_t* buf) const { @@ -77,6 +85,17 @@ namespace llarp return true; } + util::StatusObject + ResourceRecord::ToJSON() const + { + return util::StatusObject{ + {"name", rr_name}, + {"type", rr_type}, + {"class", rr_class}, + {"ttl", ttl}, + {"rdata", std::string{reinterpret_cast(rData.data()), rData.size()}}}; + } + std::ostream& ResourceRecord::print(std::ostream& stream, int level, int spaces) const { diff --git a/llarp/dns/rr.hpp b/llarp/dns/rr.hpp index 0b50235da..e9fa72c27 100644 --- a/llarp/dns/rr.hpp +++ b/llarp/dns/rr.hpp @@ -22,12 +22,17 @@ namespace llarp ResourceRecord(const ResourceRecord& other); ResourceRecord(ResourceRecord&& other); + explicit ResourceRecord(Name_t name, RRType_t type, RR_RData_t rdata); + bool Encode(llarp_buffer_t* buf) const override; bool Decode(llarp_buffer_t* buf) override; + util::StatusObject + ToJSON() const override; + std::ostream& print(std::ostream& stream, int level, int spaces) const; diff --git a/llarp/dns/serialize.hpp b/llarp/dns/serialize.hpp index 8834de04c..94388362a 100644 --- a/llarp/dns/serialize.hpp +++ b/llarp/dns/serialize.hpp @@ -1,7 +1,7 @@ #pragma once #include - +#include #include namespace llarp @@ -20,6 +20,10 @@ namespace llarp /// decode entity from buffer virtual bool Decode(llarp_buffer_t* buf) = 0; + + /// convert this whatever into json + virtual util::StatusObject + ToJSON() const = 0; }; bool diff --git a/llarp/dns/server.cpp b/llarp/dns/server.cpp index 5c8d6b4ca..d5b3b6b4b 100644 --- a/llarp/dns/server.cpp +++ b/llarp/dns/server.cpp @@ -70,6 +70,7 @@ namespace llarp::dns m_UnboundResolver = std::make_shared(m_Loop, std::move(replyFunc), std::move(failFunc)); + m_Resolvers.clear(); if (not m_UnboundResolver->Init()) { llarp::LogError("Failed to initialize upstream DNS resolver."); @@ -102,9 +103,15 @@ namespace llarp::dns llarp::LogError("dns reply failed"); } + bool + PacketHandler::IsUpstreamResolver(const SockAddr& to, [[maybe_unused]] const SockAddr& from) const + { + return m_Resolvers.count(to); + } + bool PacketHandler::ShouldHandlePacket( - const SockAddr& to, [[maybe_unused]] const SockAddr& from, llarp_buffer_t buf) const + const SockAddr& to, const SockAddr& from, llarp_buffer_t buf) const { MessageHeader hdr; if (not hdr.Decode(&buf)) @@ -120,12 +127,11 @@ namespace llarp::dns if (m_QueryHandler and m_QueryHandler->ShouldHookDNSMessage(msg)) return true; - - if (m_Resolvers.find(to) != m_Resolvers.end()) - { - return false; - } - return true; + // If this request is going to an upstream resolver then we want to let it through (i.e. don't + // handle it), and so want to return false. If we have something else then we want to + // intercept it to route it through our caching libunbound server (which then redirects the + // request to the lokinet-configured upstream, if not cached). + return !IsUpstreamResolver(to, from); } void diff --git a/llarp/dns/server.hpp b/llarp/dns/server.hpp index 21f45ed54..025ec8ef6 100644 --- a/llarp/dns/server.hpp +++ b/llarp/dns/server.hpp @@ -56,6 +56,11 @@ namespace llarp virtual void SendServerMessageBufferTo(const SockAddr& to, const SockAddr& from, llarp_buffer_t buf) = 0; + // Returns true if this packet is something that looks like it's going to an upstream + // resolver, i.e. matches a configured resolver. + virtual bool + IsUpstreamResolver(const SockAddr& to, const SockAddr& from) const; + private: void HandleUpstreamFailure(const SockAddr& from, const SockAddr& to, Message msg); diff --git a/llarp/dns/unbound_resolver.cpp b/llarp/dns/unbound_resolver.cpp index a6e747523..50c56f5fc 100644 --- a/llarp/dns/unbound_resolver.cpp +++ b/llarp/dns/unbound_resolver.cpp @@ -5,6 +5,8 @@ #include #include +#include + namespace llarp::dns { struct PendingUnboundLookup @@ -140,7 +142,8 @@ namespace llarp::dns UnboundResolver::AddUpstreamResolver(const SockAddr& upstreamResolver) { std::stringstream ss; - ss << upstreamResolver.hostString(); + auto hoststr = upstreamResolver.hostString(); + ss << hoststr; if (const auto port = upstreamResolver.getPort(); port != 53) ss << "@" << port; @@ -151,6 +154,25 @@ namespace llarp::dns Reset(); return false; } + +#ifdef __APPLE__ + // On Apple, we configure a localhost resolver to trampoline requests through the tunnel to the + // actual upstream (because the network extension itself cannot route through the tunnel using + // normal sockets but instead we "get" to use Apple's interfaces, hurray). + if (hoststr == "127.0.0.1") + { + // Not at all clear why this is needed but without it we get "send failed: Can't assign + // requested address" when unbound tries to connect to the localhost address using a source + // address of 0.0.0.0. Yay apple. + ub_ctx_set_option(unboundContext, "outgoing-interface:", hoststr.c_str()); + + // The trampoline expects just a single source port (and sends everything back to it) + ub_ctx_set_option(unboundContext, "outgoing-range:", "1"); + ub_ctx_set_option(unboundContext, "outgoing-port-avoid:", "0-65535"); + ub_ctx_set_option(unboundContext, "outgoing-port-permit:", "1253"); + } +#endif + return true; } diff --git a/llarp/dns/unbound_resolver.hpp b/llarp/dns/unbound_resolver.hpp index 5c67c3ed4..4d79569ce 100644 --- a/llarp/dns/unbound_resolver.hpp +++ b/llarp/dns/unbound_resolver.hpp @@ -1,6 +1,5 @@ #pragma once -#include #include #include #include @@ -17,6 +16,12 @@ #include #endif +extern "C" +{ + struct ub_ctx; + struct ub_result; +} + namespace llarp::dns { using ReplyFunction = diff --git a/llarp/ev/ev_libuv.cpp b/llarp/ev/ev_libuv.cpp index f9ce3c4b4..8f38750c1 100644 --- a/llarp/ev/ev_libuv.cpp +++ b/llarp/ev/ev_libuv.cpp @@ -244,7 +244,7 @@ namespace llarp::uv std::shared_ptr netif, std::function handler) { -#ifndef _WIN32 +#ifdef __linux__ using event_t = uvw::PollEvent; auto handle = m_Impl->resource(netif->PollFD()); #else @@ -264,7 +264,7 @@ namespace llarp::uv } }); -#ifndef _WIN32 +#ifdef __linux__ handle->start(uvw::PollHandle::Event::READABLE); #else handle->start(); diff --git a/llarp/handlers/tun.cpp b/llarp/handlers/tun.cpp index 81a02fad3..8150ba154 100644 --- a/llarp/handlers/tun.cpp +++ b/llarp/handlers/tun.cpp @@ -35,8 +35,8 @@ namespace llarp namespace handlers { // Intercepts DNS IP packets going to an IP on the tun interface; this is currently used on - // Android where binding to a DNS port (i.e. via llarp::dns::Proxy) isn't possible because of OS - // restrictions, but a tun interface *is* available. + // Android and macOS where binding to a DNS port (i.e. via llarp::dns::Proxy) isn't possible + // because of OS restrictions, but a tun interface *is* available. class DnsInterceptor : public dns::PacketHandler { public: @@ -61,6 +61,27 @@ namespace llarp m_Endpoint->HandleWriteIPPacket( pkt.ConstBuffer(), net::ExpandV4(from.asIPv4()), net::ExpandV4(to.asIPv4()), 0); } + +#ifdef ANDROID + bool + IsUpstreamResolver(const SockAddr&, const SockAddr&) const override + { + return true; + } +#endif + +#ifdef __APPLE__ + // DNS on Apple is a bit weird because in order for the NetworkExtension itself to send data + // through the tunnel we have to proxy DNS requests through Apple APIs (and so our actual + // upstream DNS won't be set in our resolvers, which is why the vanilla IsUpstreamResolver + // won't work for us. However when active the mac also only queries the main tunnel IP for + // DNS, so we consider anything else to be upstream-bound DNS to let it through the tunnel. + bool + IsUpstreamResolver(const SockAddr& to, const SockAddr& from) const override + { + return to.asIPv6() != m_Endpoint->GetIfAddr(); + } +#endif }; TunEndpoint::TunEndpoint(AbstractRouter* r, service::Context* parent) @@ -74,7 +95,7 @@ namespace llarp }); m_PacketRouter = std::make_unique( [this](net::IPPacket pkt) { HandleGotUserPacket(std::move(pkt)); }); -#ifdef ANDROID +#if defined(ANDROID) || defined(__APPLE__) m_Resolver = std::make_shared(r, this); m_PacketRouter->AddUDPHandler(huint16_t{53}, [&](net::IPPacket pkt) { const size_t ip_header_size = (pkt.Header()->ihl * 4); @@ -136,6 +157,17 @@ namespace llarp m_Resolver->Restart(); } + std::vector + TunEndpoint::ReconfigureDNS(std::vector servers) + { + std::swap(m_UpstreamResolvers, servers); + m_Resolver->Stop(); + if (!m_Resolver->Start( + m_LocalResolverAddr.createSockAddr(), m_UpstreamResolvers, m_hostfiles)) + llarp::LogError(Name(), " failed to reconfigure DNS server"); + return servers; + } + bool TunEndpoint::Configure(const NetworkConfig& conf, const DnsConfig& dnsConf) { @@ -909,14 +941,17 @@ namespace llarp LogError(Name(), " failed to add network interface"); return false; } - +#ifdef __APPLE__ + m_OurIPv6 = llarp::huint128_t{ + llarp::uint128_t{0xfd2e'6c6f'6b69'0000, llarp::net::TruncateV6(m_OurRange.addr).h}}; +#else const auto maybe = GetInterfaceIPv6Address(m_IfName); if (maybe.has_value()) { m_OurIPv6 = *maybe; LogInfo(Name(), " has ipv6 address ", m_OurIPv6); } - +#endif Router()->loop()->add_ticker([this] { Flush(); }); // Attempt to register DNS on the interface diff --git a/llarp/handlers/tun.hpp b/llarp/handlers/tun.hpp index 4be9ba909..6ea44b794 100644 --- a/llarp/handlers/tun.hpp +++ b/llarp/handlers/tun.hpp @@ -43,6 +43,11 @@ namespace llarp void Thaw() override; + // Reconfigures DNS servers and restarts libunbound with the new servers. Returns the old set + // of configured dns servers. + std::vector + ReconfigureDNS(std::vector servers); + bool Configure(const NetworkConfig& conf, const DnsConfig& dnsConf) override; diff --git a/llarp/rpc/rpc_server.cpp b/llarp/rpc/rpc_server.cpp index 38045d6c4..8e880a187 100644 --- a/llarp/rpc/rpc_server.cpp +++ b/llarp/rpc/rpc_server.cpp @@ -10,6 +10,7 @@ #include #include #include +#include namespace llarp::rpc { @@ -561,6 +562,47 @@ namespace llarp::rpc }); }); }) + .add_request_command( + "dns_query", + [&](oxenmq::Message& msg) { + HandleJSONRequest(msg, [r = m_Router](nlohmann::json obj, ReplyFunction_t reply) { + std::string endpoint{"default"}; + if (const auto itr = obj.find("endpoint"); itr != obj.end()) + { + endpoint = itr->get(); + } + std::string qname{}; + dns::QType_t qtype = dns::qTypeA; + if (const auto itr = obj.find("qname"); itr != obj.end()) + { + qname = itr->get(); + } + + if (const auto itr = obj.find("qtype"); itr != obj.end()) + { + qtype = itr->get(); + } + + dns::Message msg{dns::Question{qname, qtype}}; + + if (auto ep_ptr = (GetEndpointByName(r, endpoint))) + { + if (auto ep = reinterpret_cast(ep_ptr.get())) + { + if (ep->ShouldHookDNSMessage(msg)) + { + ep->HandleHookedDNSMessage(std::move(msg), [reply](dns::Message msg) { + reply(CreateJSONResponse(msg.ToJSON())); + }); + return; + } + } + reply(CreateJSONError("dns query not accepted by endpoint")); + return; + } + reply(CreateJSONError("no such endpoint for dns query")); + }); + }) .add_request_command("config", [&](oxenmq::Message& msg) { HandleJSONRequest(msg, [r = m_Router](nlohmann::json obj, ReplyFunction_t reply) { { diff --git a/llarp/service/endpoint.hpp b/llarp/service/endpoint.hpp index 0fd8c625c..cfb5a2ef9 100644 --- a/llarp/service/endpoint.hpp +++ b/llarp/service/endpoint.hpp @@ -11,7 +11,6 @@ #include "identity.hpp" #include "pendingbuffer.hpp" #include "protocol.hpp" -#include "quic/server.hpp" #include "sendcontext.hpp" #include "service/protocol_type.hpp" #include "session.hpp" diff --git a/llarp/vpn/apple.hpp b/llarp/vpn/apple.hpp deleted file mode 100644 index f20c50ce3..000000000 --- a/llarp/vpn/apple.hpp +++ /dev/null @@ -1,173 +0,0 @@ -#pragma once - -#include -#include "common.hpp" - -#include -#include -#include - -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -namespace llarp::vpn -{ - class AppleInterface : public NetworkInterface - { - const int m_FD; - const InterfaceInfo m_Info; - std::string m_IfName; - - static void - Exec(std::string cmd) - { - LogDebug(cmd); - system(cmd.c_str()); - } - - public: - AppleInterface(InterfaceInfo info) - : m_FD{::socket(PF_SYSTEM, SOCK_DGRAM, SYSPROTO_CONTROL)}, m_Info{std::move(info)} - { - if (m_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); - if (::ioctl(m_FD, CTLIOCGINFO, &cinfo) < 0) - { - ::close(m_FD); - throw std::runtime_error{"ioctl CTLIOCGINFO call failed: " + std::string{strerror(errno)}}; - } - 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; - - if (connect(m_FD, (sockaddr*)&addr, sizeof(addr)) < 0) - { - ::close(m_FD); - throw std::runtime_error{ - "cannot connect to control socket address: " + std::string{strerror(errno)}}; - } - uint32_t namesz = IFNAMSIZ; - char name[IFNAMSIZ + 1]{}; - if (getsockopt(m_FD, SYSPROTO_CONTROL, 2, name, &namesz) < 0) - { - ::close(m_FD); - throw std::runtime_error{ - "cannot query for interface name: " + std::string{strerror(errno)}}; - } - m_IfName = name; - for (const auto& ifaddr : m_Info.addrs) - { - if (ifaddr.fam == AF_INET) - { - const huint32_t addr = net::TruncateV6(ifaddr.range.addr); - const huint32_t netmask = net::TruncateV6(ifaddr.range.netmask_bits); - const huint32_t daddr = addr & netmask; - Exec( - "/sbin/ifconfig " + m_IfName + " " + addr.ToString() + " " + daddr.ToString() - + " mtu 1500 netmask 255.255.255.255 up"); - Exec( - "/sbin/route add " + daddr.ToString() + " -netmask " + netmask.ToString() - + " -interface " + m_IfName); - Exec("/sbin/route add " + addr.ToString() + " -interface lo0"); - } - else if (ifaddr.fam == AF_INET6) - { - Exec("/sbin/ifconfig " + m_IfName + " inet6 " + ifaddr.range.ToString()); - } - } - } - - ~AppleInterface() - { - ::close(m_FD); - } - - std::string - IfName() const override - { - return m_IfName; - } - - int - PollFD() const override - { - return m_FD; - } - - net::IPPacket - ReadNextPacket() override - { - constexpr int uintsize = sizeof(unsigned int); - net::IPPacket pkt{}; - unsigned int pktinfo = 0; - const struct iovec vecs[2] = { - {.iov_base = &pktinfo, .iov_len = uintsize}, - {.iov_base = pkt.buf, .iov_len = sizeof(pkt.buf)}}; - int sz = readv(m_FD, vecs, 2); - if (sz >= uintsize) - pkt.sz = sz - uintsize; - else if (sz >= 0 || errno == EAGAIN || errno == EWOULDBLOCK) - pkt.sz = 0; - else - throw std::error_code{errno, std::system_category()}; - return pkt; - } - - bool - WritePacket(net::IPPacket pkt) override - { - static unsigned int af4 = htonl(AF_INET); - static unsigned int af6 = htonl(AF_INET6); - - const struct iovec vecs[2] = { - {.iov_base = pkt.IsV6() ? &af6 : &af4, .iov_len = sizeof(unsigned int)}, - {.iov_base = pkt.buf, .iov_len = pkt.sz}}; - - ssize_t n = writev(m_FD, vecs, 2); - if (n >= (int)sizeof(unsigned int)) - { - n -= sizeof(unsigned int); - return static_cast(n) == pkt.sz; - } - return false; - } - }; - - class ApplePlatform : public Platform - { - public: - std::shared_ptr - ObtainInterface(InterfaceInfo info) override - { - return std::make_shared(std::move(info)); - } - }; -} // namespace llarp::vpn diff --git a/llarp/vpn/platform.cpp b/llarp/vpn/platform.cpp index 0784ba110..341e28fcb 100644 --- a/llarp/vpn/platform.cpp +++ b/llarp/vpn/platform.cpp @@ -1,3 +1,6 @@ + +#include + #ifdef _WIN32 #include "win32.hpp" #endif @@ -8,9 +11,8 @@ #include "linux.hpp" #endif #endif -#ifdef __APPLE__ -#include "apple.hpp" -#endif + +#include namespace llarp::vpn { @@ -30,7 +32,7 @@ namespace llarp::vpn #endif #endif #ifdef __APPLE__ - plat = std::make_shared(); + throw std::runtime_error{"not supported"}; #endif return plat; } diff --git a/readme.md b/readme.md index b0b08e446..42b339e53 100644 --- a/readme.md +++ b/readme.md @@ -62,14 +62,7 @@ alternatively you can build from source, make sure you have cmake, libuv and xco $ git clone --recursive https://github.com/oxen-io/lokinet $ cd lokinet - $ mkdir build - $ cd build - $ cmake .. -DBUILD_STATIC_DEPS=ON -DBUILD_SHARED_LIBS=OFF -DSTATIC_LINK=ON - $ make -j$(sysctl -n hw.ncpu) - -install: - - $ sudo make install + $ ./contrib/mac.sh -DCODESIGN_KEY='insert your key identity here' -DCODESIGN_TEAM_ID='team id here' ### Windows