diff --git a/.drone.jsonnet b/.drone.jsonnet index 436a039ee..64669f76e 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -22,6 +22,7 @@ local debian_pipeline(name, image, cmake_extra='', extra_cmds=[], jobs=6, + tests=true, loki_repo=false, allow_fail=false) = { kind: 'pipeline', @@ -55,10 +56,12 @@ local debian_pipeline(name, image, 'cmake .. -DWITH_SETCAP=OFF -DCMAKE_CXX_FLAGS=-fdiagnostics-color=always -DCMAKE_BUILD_TYPE='+build_type+' ' + (if werror then '-DWARNINGS_AS_ERRORS=ON ' else '') + '-DWITH_LTO=' + (if lto then 'ON ' else 'OFF ') + + (if tests then '' else '-DWITH_TESTS=OFF ') + cmake_extra, 'VERBOSE=1 make -j' + jobs, - '../contrib/ci/drone-gdb.sh ./test/testAll --use-colour yes', - ] + extra_cmds, + ] + + (if tests then ['../contrib/ci/drone-gdb.sh ./test/testAll --use-colour yes'] else []) + + extra_cmds, } ], }; @@ -76,7 +79,13 @@ local apk_builder(name, image, extra_cmds=[], allow_fail=false, jobs=6) = { [if allow_fail then "failure"]: "ignore", environment: { SSH_KEY: { from_secret: "SSH_KEY" }, ANDROID: "android" }, commands: [ - 'VERBOSE=1 JOBS='+jobs+' NDK=/usr/lib/android-ndk ./contrib/android.sh' + 'VERBOSE=1 JOBS='+jobs+' NDK=/usr/lib/android-ndk ./contrib/android.sh', + 'git clone https://github.com/majestrate/lokinet-mobile', + 'cp -av lokinet-jni-*/* lokinet-mobile/lokinet_lib/android/src/main/jniLibs/', + 'cd lokinet-mobile', + 'flutter build apk --debug', + 'cd ..', + 'cp lokinet-mobile/build/app/outputs/apk/debug/app-debug.apk lokinet.apk' ] + extra_cmds } ] @@ -191,12 +200,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, } ] @@ -252,7 +256,7 @@ local mac_builder(name, ]), // Static build (on bionic) which gets uploaded to builds.lokinet.dev: - debian_pipeline("Static (bionic amd64)", docker_base+'ubuntu-bionic', deps='g++-8 python3-dev automake libtool', lto=true, + debian_pipeline("Static (bionic amd64)", docker_base+'ubuntu-bionic', deps='g++-8 python3-dev automake libtool', lto=true, tests=false, cmake_extra='-DBUILD_STATIC_DEPS=ON -DBUILD_SHARED_LIBS=OFF -DSTATIC_LINK=ON -DCMAKE_C_COMPILER=gcc-8 -DCMAKE_CXX_COMPILER=g++-8 ' + '-DCMAKE_CXX_FLAGS="-march=x86-64 -mtune=haswell" -DCMAKE_C_FLAGS="-march=x86-64 -mtune=haswell" -DNATIVE_BUILD=OFF ' + '-DWITH_SYSTEMD=OFF', @@ -274,9 +278,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..3077b523d 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 + VERSION 0.9.6 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/StaticBuild.cmake b/cmake/StaticBuild.cmake index 783ae11d1..adf00bc0f 100644 --- a/cmake/StaticBuild.cmake +++ b/cmake/StaticBuild.cmake @@ -5,10 +5,10 @@ set(LOCAL_MIRROR "" CACHE STRING "local mirror path/URL for lib downloads") -set(OPENSSL_VERSION 1.1.1k CACHE STRING "openssl version") +set(OPENSSL_VERSION 1.1.1l CACHE STRING "openssl version") set(OPENSSL_MIRROR ${LOCAL_MIRROR} https://www.openssl.org/source CACHE STRING "openssl download mirror(s)") set(OPENSSL_SOURCE openssl-${OPENSSL_VERSION}.tar.gz) -set(OPENSSL_HASH SHA256=892a0875b9872acd04a9fde79b1f943075d5ea162415de3047c327df33fbaee5 +set(OPENSSL_HASH SHA256=0b7a3e5e59c34827fe0c3a74b7ec8baef302b98fa80088d7f9153aa16fa76bd1 CACHE STRING "openssl source hash") set(EXPAT_VERSION 2.3.0 CACHE STRING "expat version") 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/bencode-dump.py b/contrib/bencode-dump.py index 1c1f55499..8b5b8e642 100755 --- a/contrib/bencode-dump.py +++ b/contrib/bencode-dump.py @@ -3,11 +3,13 @@ import sys import pprint -if len(sys.argv) != 2 or sys.argv[1].startswith('-'): +if len(sys.argv) == 1 or (len(sys.argv) == 2 and sys.argv[1] == '-'): + f = sys.stdin.buffer +elif len(sys.argv) != 2 or sys.argv[1].startswith('-'): print("Usage: {} FILE -- dumps a bencoded file".format(sys.argv[0]), file=sys.stderr) sys.exit(1) - -f = open(sys.argv[1], 'rb') +else: + f = open(sys.argv[1], 'rb') class HexPrinter(): def __init__(self, data): diff --git a/contrib/ci/drone-static-upload.sh b/contrib/ci/drone-static-upload.sh index bdbd57561..c5d35bfaa 100755 --- a/contrib/ci/drone-static-upload.sh +++ b/contrib/ci/drone-static-upload.sh @@ -39,11 +39,10 @@ if [ -e build-windows ]; then # zipit up yo archive="$base.zip" zip -r "$archive" "$base" -elif [ -e build-android ] ; then +elif [ -e lokinet.apk ] ; then # android af ngl - cp -av lokinet-jni-* "$base" - archive="$base.tar.xz" - tar cJvf "$archive" "$base" + archive="$base.apk" + cp -av lokinet.apk "$archive" else cp -av daemon/lokinet daemon/lokinet-vpn daemon/lokinet-bootstrap "$base" # tar dat shiz up yo 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-bootstrap.cpp b/daemon/lokinet-bootstrap.cpp index 7e8a4305a..435ec6767 100644 --- a/daemon/lokinet-bootstrap.cpp +++ b/daemon/lokinet-bootstrap.cpp @@ -9,6 +9,7 @@ #include #include +#include #ifndef _WIN32 #include @@ -28,6 +29,26 @@ namespace std::cout << msg << std::endl; return 1; } + + int + print_help(std::string exe) + { + std::cout << R"(Lokinet bootstrap.signed fetchy program thing + +Downloads the initial bootstrap.signed for lokinet into a local file from a +default or user defined server via the reachable network. + +Usage: )" << exe + << R"( [bootstrap_url [output_file]] + +bootstrap_url can be specified as a full URL, or a special named value +("mainnet" or "testnet") to download from the pre-defined mainnet or testnet +bootstrap URLs. + +)"; + return 0; + } + } // namespace int @@ -42,6 +63,15 @@ main(int argc, char* argv[]) std::string bootstrap_url = bootstrap_urls.at("lokinet"); fs::path outputfile{llarp::GetDefaultBootstrap()}; + const std::unordered_set help_args = {"-h", "--help"}; + + for (int idx = 1; idx < argc; idx++) + { + const std::string arg{argv[idx]}; + if (help_args.count(arg)) + return print_help(argv[0]); + } + if (argc > 1) { if (auto itr = bootstrap_urls.find(argv[1]); itr != bootstrap_urls.end()) @@ -57,7 +87,7 @@ main(int argc, char* argv[]) { outputfile = fs::path{argv[2]}; } - + std::cout << "fetching " << bootstrap_url << std::endl; cpr::Response resp = #ifdef _WIN32 cpr::Get( @@ -79,6 +109,7 @@ main(int argc, char* argv[]) { try { + std::cout << "writing bootstrap file to: " << outputfile << std::endl; fs::ofstream ofs{outputfile, std::ios::binary}; ofs.exceptions(fs::ofstream::failbit); ofs << data; 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 d1aed3353..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}) @@ -52,7 +52,6 @@ add_library(lokinet-platform net/ip_range.cpp net/net.cpp net/net_int.cpp - net/route.cpp net/sock_addr.cpp vpn/packet_router.cpp vpn/platform.cpp @@ -263,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/config/config.cpp b/llarp/config/config.cpp index bef3e521f..4e1b4c80e 100644 --- a/llarp/config/config.cpp +++ b/llarp/config/config.cpp @@ -3,7 +3,6 @@ #include "config/definition.hpp" #include "ini.hpp" -#include #include #include #include @@ -711,6 +710,8 @@ namespace llarp // Default, but if we get any upstream (including upstream=, i.e. empty string) we clear it constexpr Default DefaultUpstreamDNS{"1.1.1.1"}; m_upstreamDNS.emplace_back(DefaultUpstreamDNS.val); + if (!m_upstreamDNS.back().getPort()) + m_upstreamDNS.back().setPort(53); conf.defineOption( "dns", @@ -798,7 +799,7 @@ namespace llarp { info.interface = std::string{name}; - std::vector splits = split(value, ','); + std::vector splits = split(value, ","); for (std::string_view str : splits) { int asNum = std::atoi(str.data()); diff --git a/llarp/constants/defaults.hpp b/llarp/constants/defaults.hpp deleted file mode 100644 index 884fdf1bf..000000000 --- a/llarp/constants/defaults.hpp +++ /dev/null @@ -1,18 +0,0 @@ -#pragma once - -#ifndef DEFAULT_RESOLVER_US -#define DEFAULT_RESOLVER_US "1.1.1.1" -#endif -#ifndef DEFAULT_RESOLVER_EU -#define DEFAULT_RESOLVER_EU "1.1.1.1" -#endif -#ifndef DEFAULT_RESOLVER_AU -#define DEFAULT_RESOLVER_AU "1.1.1.1" -#endif - -#ifndef DEFAULT_LOKINET_USER -#define DEFAULT_LOKINET_USER "lokinet" -#endif -#ifndef DEFAULT_LOKINET_GROUP -#define DEFAULT_LOKINET_GROUP "lokinet" -#endif 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 22155ec4c..d5b3b6b4b 100644 --- a/llarp/dns/server.cpp +++ b/llarp/dns/server.cpp @@ -58,9 +58,9 @@ namespace llarp::dns return true; auto failFunc = [self = weak_from_this()]( - const SockAddr& from, const SockAddr& to, Message msg) { + const SockAddr& to, const SockAddr& from, Message msg) { if (auto this_ptr = self.lock()) - this_ptr->SendServerMessageBufferTo(from, to, msg.ToBuffer()); + this_ptr->SendServerMessageBufferTo(to, from, msg.ToBuffer()); }; auto replyFunc = [self = weak_from_this()](auto&&... args) { @@ -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."); @@ -95,15 +96,22 @@ namespace llarp::dns } void - Proxy::SendServerMessageBufferTo(const SockAddr&, const SockAddr& to, llarp_buffer_t buf) + Proxy::SendServerMessageBufferTo( + const SockAddr& to, [[maybe_unused]] const SockAddr& from, llarp_buffer_t buf) { if (!m_Server->send(to, buf)) 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)) @@ -119,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 @@ -157,7 +164,7 @@ namespace llarp::dns // yea it is, let's turn off DoH because god is dead. msg.AddNXReply(); // press F to pay respects - SendServerMessageBufferTo(resolver, from, msg.ToBuffer()); + SendServerMessageBufferTo(from, resolver, msg.ToBuffer()); return; } } @@ -165,7 +172,7 @@ namespace llarp::dns if (m_QueryHandler && m_QueryHandler->ShouldHookDNSMessage(msg)) { auto reply = [self = shared_from_this(), to = from, resolver](dns::Message msg) { - self->SendServerMessageBufferTo(resolver, to, msg.ToBuffer()); + self->SendServerMessageBufferTo(to, resolver, msg.ToBuffer()); }; if (!m_QueryHandler->HandleHookedDNSMessage(std::move(msg), reply)) { @@ -177,7 +184,7 @@ namespace llarp::dns // no upstream resolvers // let's serv fail it msg.AddServFail(); - SendServerMessageBufferTo(resolver, from, msg.ToBuffer()); + SendServerMessageBufferTo(from, resolver, msg.ToBuffer()); } else { diff --git a/llarp/dns/server.hpp b/llarp/dns/server.hpp index e778b2e22..025ec8ef6 100644 --- a/llarp/dns/server.hpp +++ b/llarp/dns/server.hpp @@ -54,7 +54,12 @@ namespace llarp protected: virtual void - SendServerMessageBufferTo(const SockAddr& from, const SockAddr& to, llarp_buffer_t buf) = 0; + 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 @@ -84,7 +89,7 @@ namespace llarp protected: void SendServerMessageBufferTo( - const SockAddr& from, const SockAddr& to, llarp_buffer_t buf) override; + const SockAddr& to, const SockAddr& from, llarp_buffer_t buf) override; private: std::shared_ptr m_Server; diff --git a/llarp/dns/unbound_resolver.cpp b/llarp/dns/unbound_resolver.cpp index 90ebbbcd3..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 @@ -25,11 +27,18 @@ namespace llarp::dns UnboundResolver::Reset() { started = false; - if (runner) +#ifdef _WIN32 + if (runner.joinable()) + { + runner.join(); + } +#else + if (udp) { - runner->join(); - runner.reset(); + udp->close(); } + udp.reset(); +#endif if (unboundContext) { ub_ctx_delete(unboundContext); @@ -37,12 +46,16 @@ namespace llarp::dns unboundContext = nullptr; } - UnboundResolver::UnboundResolver(EventLoop_ptr loop, ReplyFunction reply, FailFunction fail) - : unboundContext(nullptr) - , started(false) - , replyFunc(loop->make_caller(std::move(reply))) - , failFunc(loop->make_caller(std::move(fail))) - {} + UnboundResolver::UnboundResolver(EventLoop_ptr _loop, ReplyFunction reply, FailFunction fail) + : unboundContext{nullptr} + , started{false} + , replyFunc{_loop->make_caller(std::move(reply))} + , failFunc{_loop->make_caller(std::move(fail))} + { +#ifndef _WIN32 + loop = _loop->MaybeGetUVWLoop(); +#endif + } // static callback void @@ -58,7 +71,7 @@ namespace llarp::dns { Message& msg = lookup->msg; msg.AddServFail(); - this_ptr->failFunc(lookup->resolverAddr, lookup->askerAddr, msg); + this_ptr->failFunc(lookup->askerAddr, lookup->resolverAddr, msg); ub_resolve_free(result); return; } @@ -73,7 +86,7 @@ namespace llarp::dns buf.cur = buf.base; hdr.Encode(&buf); - this_ptr->replyFunc(lookup->resolverAddr, lookup->askerAddr, std::move(pkt)); + this_ptr->replyFunc(lookup->askerAddr, lookup->resolverAddr, std::move(pkt)); ub_resolve_free(result); } @@ -94,14 +107,33 @@ namespace llarp::dns } ub_ctx_async(unboundContext, 1); - runner = std::make_unique([&]() { +#ifdef _WIN32 + runner = std::thread{[&]() { while (started) { if (unboundContext) ub_wait(unboundContext); std::this_thread::sleep_for(25ms); } - }); + if (unboundContext) + ub_process(unboundContext); + }}; +#else + if (auto loop_ptr = loop.lock()) + { + udp = loop_ptr->resource(ub_fd(unboundContext)); + udp->on([ptr = weak_from_this()](auto&, auto&) { + if (auto self = ptr.lock()) + { + if (self->unboundContext) + { + ub_process(self->unboundContext); + } + } + }); + udp->start(uvw::PollHandle::Event::READABLE); + } +#endif started = true; return true; } @@ -110,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; @@ -121,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; } @@ -145,7 +197,7 @@ namespace llarp::dns if (not unboundContext) { msg.AddServFail(); - failFunc(to, from, std::move(msg)); + failFunc(from, to, std::move(msg)); return; } @@ -163,7 +215,7 @@ namespace llarp::dns if (err != 0) { msg.AddServFail(); - failFunc(to, from, std::move(msg)); + failFunc(from, to, std::move(msg)); return; } } diff --git a/llarp/dns/unbound_resolver.hpp b/llarp/dns/unbound_resolver.hpp index e1861ab88..4d79569ce 100644 --- a/llarp/dns/unbound_resolver.hpp +++ b/llarp/dns/unbound_resolver.hpp @@ -1,6 +1,5 @@ #pragma once -#include #include #include #include @@ -13,14 +12,22 @@ #ifdef _WIN32 #include +#else +#include #endif +extern "C" +{ + struct ub_ctx; + struct ub_result; +} + namespace llarp::dns { using ReplyFunction = - std::function; + std::function; using FailFunction = - std::function; + std::function; class UnboundResolver : public std::enable_shared_from_this { @@ -28,11 +35,16 @@ namespace llarp::dns ub_ctx* unboundContext; std::atomic started; - std::unique_ptr runner; + +#ifdef _WIN32 + std::thread runner; +#else + std::weak_ptr loop; + std::shared_ptr udp; +#endif ReplyFunction replyFunc; FailFunction failFunc; - void Reset(); 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/ev/vpn.hpp b/llarp/ev/vpn.hpp index 6f5c3c540..c3346fcdd 100644 --- a/llarp/ev/vpn.hpp +++ b/llarp/ev/vpn.hpp @@ -4,6 +4,8 @@ #include #include +#include + namespace llarp { struct Context; @@ -59,6 +61,44 @@ namespace llarp::vpn WritePacket(net::IPPacket pkt) = 0; }; + class IRouteManager + { + public: + using IPVariant_t = std::variant; + + IRouteManager() = default; + IRouteManager(const IRouteManager&) = delete; + IRouteManager(IRouteManager&&) = delete; + virtual ~IRouteManager() = default; + + virtual void + AddRoute(IPVariant_t ip, IPVariant_t gateway) = 0; + + virtual void + DelRoute(IPVariant_t ip, IPVariant_t gateway) = 0; + + virtual void + AddDefaultRouteViaInterface(std::string ifname) = 0; + + virtual void + DelDefaultRouteViaInterface(std::string ifname) = 0; + + virtual void + AddRouteViaInterface(NetworkInterface& vpn, IPRange range) = 0; + + virtual void + DelRouteViaInterface(NetworkInterface& vpn, IPRange range) = 0; + + virtual std::vector + GetGatewaysNotOnInterface(std::string ifname) = 0; + + virtual void + AddBlackhole(){}; + + virtual void + DelBlackhole(){}; + }; + /// a vpn platform /// responsible for obtaining vpn interfaces class Platform @@ -73,6 +113,10 @@ namespace llarp::vpn /// blocks until ready, throws on error virtual std::shared_ptr ObtainInterface(InterfaceInfo info) = 0; + + /// get owned ip route manager for managing routing table + virtual IRouteManager& + RouteManager() = 0; }; /// create native vpn platform diff --git a/llarp/handlers/tun.cpp b/llarp/handlers/tun.cpp index 4a34df612..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); @@ -87,8 +108,8 @@ namespace llarp OwnedBuffer buf{pkt.sz - (8 + ip_header_size)}; std::copy_n(ptr + 8, buf.sz, buf.buf.get()); - if (m_Resolver->ShouldHandlePacket(laddr, raddr, buf)) - m_Resolver->HandlePacket(laddr, raddr, buf); + if (m_Resolver->ShouldHandlePacket(raddr, laddr, buf)) + m_Resolver->HandlePacket(raddr, laddr, buf); else HandleGotUserPacket(std::move(pkt)); }); @@ -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 @@ -974,6 +1009,7 @@ namespace llarp TunEndpoint::Stop() { // save address map if applicable +#ifndef ANDROID if (m_PersistAddrMapFile) { const auto& file = *m_PersistAddrMapFile; @@ -994,6 +1030,7 @@ namespace llarp maybe->write(data.data(), data.size()); } } +#endif if (m_Resolver) m_Resolver->Stop(); return llarp::service::Endpoint::Stop(); 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/net/route.cpp b/llarp/net/route.cpp deleted file mode 100644 index 051d86fae..000000000 --- a/llarp/net/route.cpp +++ /dev/null @@ -1,561 +0,0 @@ -#include "route.hpp" - -#ifdef __linux__ -#include -#include -#include -#include -#include -#ifndef ANDROID -#include -#include -#endif -#include "net.hpp" -#include -#include -#endif -#ifdef __APPLE__ -#include "net.hpp" -#include -#endif -#ifdef _WIN32 -#include -#include -#include -#include -#include -#include -#include -#include -#include "net_int.hpp" -#include "ip.hpp" -#endif - -#include -#include -#include - -namespace llarp::net -{ -#ifndef __linux__ - void - Execute(std::string cmd) - { - LogInfo(cmd); - system(cmd.c_str()); - } -#endif - -#ifdef __linux__ -#ifndef ANDROID - - enum class GatewayMode - { - eFirstHop, - eLowerDefault, - eUpperDefault - }; - - struct NLSocket - { - NLSocket() : fd(socket(AF_NETLINK, SOCK_DGRAM, NETLINK_ROUTE)) - { - if (fd == -1) - throw std::runtime_error("failed to make netlink socket"); - } - - ~NLSocket() - { - if (fd != -1) - close(fd); - } - - const int fd; - }; - - /* Helper structure for ip address data and attributes */ - typedef struct - { - unsigned char family; - unsigned char bitlen; - unsigned char data[sizeof(struct in6_addr)]; - } _inet_addr; - - /* */ - -#define NLMSG_TAIL(nmsg) ((struct rtattr*)(((intptr_t)(nmsg)) + NLMSG_ALIGN((nmsg)->nlmsg_len))) - - /* Add new data to rtattr */ - int - rtattr_add(struct nlmsghdr* n, unsigned int maxlen, int type, const void* data, int alen) - { - int len = RTA_LENGTH(alen); - struct rtattr* rta; - - if (NLMSG_ALIGN(n->nlmsg_len) + RTA_ALIGN(len) > maxlen) - { - fprintf(stderr, "rtattr_add error: message exceeded bound of %d\n", maxlen); - return -1; - } - - rta = NLMSG_TAIL(n); - rta->rta_type = type; - rta->rta_len = len; - - if (alen) - { - memcpy(RTA_DATA(rta), data, alen); - } - - n->nlmsg_len = NLMSG_ALIGN(n->nlmsg_len) + RTA_ALIGN(len); - - return 0; - } - - int - do_route( - int sock, - int cmd, - int flags, - const _inet_addr* dst, - const _inet_addr* gw, - GatewayMode mode, - int if_idx) - { - struct - { - struct nlmsghdr n; - struct rtmsg r; - char buf[4096]; - } nl_request{}; - - /* Initialize request structure */ - nl_request.n.nlmsg_len = NLMSG_LENGTH(sizeof(struct rtmsg)); - nl_request.n.nlmsg_flags = NLM_F_REQUEST | flags; - nl_request.n.nlmsg_type = cmd; - nl_request.n.nlmsg_pid = getpid(); - nl_request.r.rtm_family = dst->family; - nl_request.r.rtm_table = RT_TABLE_MAIN; - if (if_idx) - { - nl_request.r.rtm_scope = RT_SCOPE_LINK; - } - else - { - nl_request.r.rtm_scope = RT_SCOPE_NOWHERE; - } - /* Set additional flags if NOT deleting route */ - if (cmd != RTM_DELROUTE) - { - nl_request.r.rtm_protocol = RTPROT_BOOT; - nl_request.r.rtm_type = RTN_UNICAST; - } - - nl_request.r.rtm_family = dst->family; - nl_request.r.rtm_dst_len = dst->bitlen; - nl_request.r.rtm_scope = 0; - - /* Set gateway */ - if (gw->bitlen != 0 and dst->family == AF_INET) - { - rtattr_add(&nl_request.n, sizeof(nl_request), RTA_GATEWAY, &gw->data, gw->bitlen / 8); - } - nl_request.r.rtm_family = gw->family; - if (mode == GatewayMode::eFirstHop) - { - rtattr_add( - &nl_request.n, sizeof(nl_request), /*RTA_NEWDST*/ RTA_DST, &dst->data, dst->bitlen / 8); - /* Set interface */ - rtattr_add(&nl_request.n, sizeof(nl_request), RTA_OIF, &if_idx, sizeof(int)); - } - if (mode == GatewayMode::eUpperDefault) - { - if (dst->family == AF_INET) - { - rtattr_add( - &nl_request.n, - sizeof(nl_request), - /*RTA_NEWDST*/ RTA_DST, - &dst->data, - sizeof(uint32_t)); - } - else - { - rtattr_add(&nl_request.n, sizeof(nl_request), RTA_OIF, &if_idx, sizeof(int)); - rtattr_add( - &nl_request.n, - sizeof(nl_request), - /*RTA_NEWDST*/ RTA_DST, - &dst->data, - sizeof(in6_addr)); - } - } - /* Send message to the netlink */ - return send(sock, &nl_request, sizeof(nl_request), 0); - } - - int - read_addr(const char* addr, _inet_addr* res, int bitlen = 32) - { - if (strchr(addr, ':')) - { - res->family = AF_INET6; - res->bitlen = bitlen; - } - else - { - res->family = AF_INET; - res->bitlen = bitlen; - } - return inet_pton(res->family, addr, res->data); - } - -#endif -#endif - -#ifdef _WIN32 - - std::wstring - get_win_sys_path() - { - wchar_t win_sys_path[MAX_PATH] = {0}; - const wchar_t* default_sys_path = L"C:\\Windows\\system32"; - - if (!GetSystemDirectoryW(win_sys_path, _countof(win_sys_path))) - { - wcsncpy(win_sys_path, default_sys_path, _countof(win_sys_path)); - win_sys_path[_countof(win_sys_path) - 1] = L'\0'; - } - return win_sys_path; - } - - std::string - RouteCommand() - { - std::wstring wcmd = get_win_sys_path() + L"\\route.exe"; - - using convert_type = std::codecvt_utf8; - std::wstring_convert converter; - return converter.to_bytes(wcmd); - } - - std::string - NetshCommand() - { - std::wstring wcmd = get_win_sys_path() + L"\\netsh.exe"; - - using convert_type = std::codecvt_utf8; - std::wstring_convert converter; - return converter.to_bytes(wcmd); - } - - template - void - ForEachWIN32Interface(Visit visit) - { -#define MALLOC(x) HeapAlloc(GetProcessHeap(), 0, (x)) -#define FREE(x) HeapFree(GetProcessHeap(), 0, (x)) - MIB_IPFORWARDTABLE* pIpForwardTable; - DWORD dwSize = 0; - DWORD dwRetVal = 0; - - pIpForwardTable = (MIB_IPFORWARDTABLE*)MALLOC(sizeof(MIB_IPFORWARDTABLE)); - if (pIpForwardTable == nullptr) - return; - - if (GetIpForwardTable(pIpForwardTable, &dwSize, 0) == ERROR_INSUFFICIENT_BUFFER) - { - FREE(pIpForwardTable); - pIpForwardTable = (MIB_IPFORWARDTABLE*)MALLOC(dwSize); - if (pIpForwardTable == nullptr) - { - return; - } - } - - if ((dwRetVal = GetIpForwardTable(pIpForwardTable, &dwSize, 0)) == NO_ERROR) - { - for (int i = 0; i < (int)pIpForwardTable->dwNumEntries; i++) - { - visit(&pIpForwardTable->table[i]); - } - } - FREE(pIpForwardTable); -#undef MALLOC -#undef FREE - } - - std::optional - GetInterfaceIndex(huint32_t ip) - { - std::optional ret = std::nullopt; - ForEachWIN32Interface([&ret, n = ToNet(ip)](auto* iface) { - if (ret.has_value()) - return; - if (iface->dwForwardNextHop == n.n) - { - ret = iface->dwForwardIfIndex; - } - }); - return ret; - } - -#endif - - void - AddRoute(std::string ip, std::string gateway) - { - LogInfo("Add route: ", ip, " via ", gateway); -#ifdef __linux__ -#ifndef ANDROID - NLSocket sock; - int if_idx = 0; - _inet_addr to_addr{}; - _inet_addr gw_addr{}; - int nl_cmd = RTM_NEWROUTE; - int nl_flags = NLM_F_CREATE | NLM_F_EXCL; - read_addr(gateway.c_str(), &gw_addr); - read_addr(ip.c_str(), &to_addr); - do_route(sock.fd, nl_cmd, nl_flags, &to_addr, &gw_addr, GatewayMode::eFirstHop, if_idx); -#endif -#else - std::stringstream ss; -#if _WIN32 - ss << RouteCommand() << " ADD " << ip << " MASK 255.255.255.255 " << gateway << " METRIC 2"; -#elif __APPLE__ - ss << "/sbin/route -n add -host " << ip << " " << gateway; -#else -#error unsupported platform -#endif - Execute(ss.str()); -#endif - } - - void - DelRoute(std::string ip, std::string gateway) - { - LogInfo("Delete route: ", ip, " via ", gateway); -#ifdef __linux__ -#ifndef ANDROID - NLSocket sock; - int if_idx = 0; - _inet_addr to_addr{}; - _inet_addr gw_addr{}; - int nl_cmd = RTM_DELROUTE; - int nl_flags = 0; - read_addr(gateway.c_str(), &gw_addr); - read_addr(ip.c_str(), &to_addr); - do_route(sock.fd, nl_cmd, nl_flags, &to_addr, &gw_addr, GatewayMode::eFirstHop, if_idx); -#endif -#else - std::stringstream ss; -#if _WIN32 - ss << RouteCommand() << " DELETE " << ip << " MASK 255.255.255.255 " << gateway << " METRIC 2"; -#elif __APPLE__ - ss << "/sbin/route -n delete -host " << ip << " " << gateway; -#else -#error unsupported platform -#endif - Execute(ss.str()); -#endif - } - - void - AddDefaultRouteViaInterface(std::string ifname) - { - LogInfo("Add default route via ", ifname); -#ifdef __linux__ -#ifndef ANDROID - NLSocket sock; - int if_idx = if_nametoindex(ifname.c_str()); - _inet_addr to_addr{}; - _inet_addr gw_addr{}; - const auto maybe = GetInterfaceAddr(ifname); - if (not maybe.has_value()) - throw std::runtime_error("we dont have our own net interface?"); - int nl_cmd = RTM_NEWROUTE; - int nl_flags = NLM_F_CREATE | NLM_F_EXCL; - const auto host = maybe->asIPv4().ToString(); - read_addr(host.c_str(), &gw_addr); - read_addr("0.0.0.0", &to_addr, 1); - do_route(sock.fd, nl_cmd, nl_flags, &to_addr, &gw_addr, GatewayMode::eLowerDefault, if_idx); - read_addr("128.0.0.0", &to_addr, 1); - do_route(sock.fd, nl_cmd, nl_flags, &to_addr, &gw_addr, GatewayMode::eUpperDefault, if_idx); - const auto maybeInt = GetInterfaceIPv6Address(ifname); - if (maybeInt.has_value()) - { - const auto host = maybeInt->ToString(); - LogInfo("add v6 route via ", host); - read_addr(host.c_str(), &gw_addr, 128); - read_addr("::", &to_addr, 2); - do_route(sock.fd, nl_cmd, nl_flags, &to_addr, &gw_addr, GatewayMode::eUpperDefault, if_idx); - read_addr("4000::", &to_addr, 2); - do_route(sock.fd, nl_cmd, nl_flags, &to_addr, &gw_addr, GatewayMode::eUpperDefault, if_idx); - read_addr("8000::", &to_addr, 2); - do_route(sock.fd, nl_cmd, nl_flags, &to_addr, &gw_addr, GatewayMode::eUpperDefault, if_idx); - read_addr("c000::", &to_addr, 2); - do_route(sock.fd, nl_cmd, nl_flags, &to_addr, &gw_addr, GatewayMode::eUpperDefault, if_idx); - } -#endif -#elif _WIN32 - // poke hole for loopback bacause god is dead on windows - Execute(RouteCommand() + " ADD 127.0.0.0 MASK 255.0.0.0 0.0.0.0"); - - huint32_t ip{}; - ip.FromString(ifname); - const auto ipv6 = net::ExpandV4Lan(ip); - - Execute(RouteCommand() + " ADD ::/2 " + ipv6.ToString()); - Execute(RouteCommand() + " ADD 4000::/2 " + ipv6.ToString()); - Execute(RouteCommand() + " ADD 8000::/2 " + ipv6.ToString()); - Execute(RouteCommand() + " ADD c000::/2 " + ipv6.ToString()); - ifname.back()++; - Execute(RouteCommand() + " ADD 0.0.0.0 MASK 128.0.0.0 " + ifname); - Execute(RouteCommand() + " ADD 128.0.0.0 MASK 128.0.0.0 " + ifname); - -#elif __APPLE__ - Execute("/sbin/route -n add -cloning -net 0.0.0.0 -netmask 128.0.0.0 -interface " + ifname); - Execute("/sbin/route -n add -cloning -net 128.0.0.0 -netmask 128.0.0.0 -interface " + ifname); - - Execute("/sbin/route -n add -inet6 -net ::/2 -interface " + ifname); - Execute("/sbin/route -n add -inet6 -net 4000::/2 -interface " + ifname); - Execute("/sbin/route -n add -inet6 -net 8000::/2 -interface " + ifname); - Execute("/sbin/route -n add -inet6 -net c000::/2 -interface " + ifname); -#else -#error unsupported platform -#endif - } - - void - DelDefaultRouteViaInterface(std::string ifname) - { - LogInfo("Remove default route via ", ifname); -#ifdef __linux__ -#ifndef ANDROID - NLSocket sock; - int if_idx = if_nametoindex(ifname.c_str()); - _inet_addr to_addr{}; - _inet_addr gw_addr{}; - const auto maybe = GetInterfaceAddr(ifname); - - if (not maybe.has_value()) - throw std::runtime_error("we dont have our own net interface?"); - int nl_cmd = RTM_DELROUTE; - int nl_flags = 0; - const auto host = maybe->asIPv4().ToString(); - read_addr(host.c_str(), &gw_addr); - read_addr("0.0.0.0", &to_addr, 1); - do_route(sock.fd, nl_cmd, nl_flags, &to_addr, &gw_addr, GatewayMode::eLowerDefault, if_idx); - read_addr("128.0.0.0", &to_addr, 1); - do_route(sock.fd, nl_cmd, nl_flags, &to_addr, &gw_addr, GatewayMode::eUpperDefault, if_idx); - - const auto maybeInt = GetInterfaceIPv6Address(ifname); - if (maybeInt.has_value()) - { - const auto host = maybeInt->ToString(); - LogInfo("del v6 route via ", host); - read_addr(host.c_str(), &gw_addr, 128); - read_addr("::", &to_addr, 2); - do_route(sock.fd, nl_cmd, nl_flags, &to_addr, &gw_addr, GatewayMode::eUpperDefault, if_idx); - read_addr("4000::", &to_addr, 2); - do_route(sock.fd, nl_cmd, nl_flags, &to_addr, &gw_addr, GatewayMode::eUpperDefault, if_idx); - read_addr("8000::", &to_addr, 2); - do_route(sock.fd, nl_cmd, nl_flags, &to_addr, &gw_addr, GatewayMode::eUpperDefault, if_idx); - read_addr("c000::", &to_addr, 2); - do_route(sock.fd, nl_cmd, nl_flags, &to_addr, &gw_addr, GatewayMode::eUpperDefault, if_idx); - } -#endif -#elif _WIN32 - huint32_t ip{}; - ip.FromString(ifname); - const auto ipv6 = net::ExpandV4Lan(ip); - - Execute(RouteCommand() + " DELETE ::/2 " + ipv6.ToString()); - Execute(RouteCommand() + " DELETE 4000::/2 " + ipv6.ToString()); - Execute(RouteCommand() + " DELETE 8000::/2 " + ipv6.ToString()); - Execute(RouteCommand() + " DELETE c000::/2 " + ipv6.ToString()); - ifname.back()++; - Execute(RouteCommand() + " DELETE 0.0.0.0 MASK 128.0.0.0 " + ifname); - Execute(RouteCommand() + " DELETE 128.0.0.0 MASK 128.0.0.0 " + ifname); - Execute(RouteCommand() + " DELETE 127.0.0.0 MASK 255.0.0.0 0.0.0.0"); -#elif __APPLE__ - Execute("/sbin/route -n delete -cloning -net 0.0.0.0 -netmask 128.0.0.0 -interface " + ifname); - Execute( - "/sbin/route -n delete -cloning -net 128.0.0.0 -netmask 128.0.0.0 -interface " + ifname); - - Execute("/sbin/route -n delete -inet6 -net ::/2 -interface " + ifname); - Execute("/sbin/route -n delete -inet6 -net 4000::/2 -interface " + ifname); - Execute("/sbin/route -n delete -inet6 -net 8000::/2 -interface " + ifname); - Execute("/sbin/route -n delete -inet6 -net c000::/2 -interface " + ifname); - -#else -#error unsupported platform -#endif - } - - std::vector - GetGatewaysNotOnInterface(std::string ifname) - { - std::vector gateways; -#ifdef __linux__ -#ifdef ANDROID -#else - std::ifstream inf("/proc/net/route"); - for (std::string line; std::getline(inf, line);) - { - const auto parts = split(line, '\t'); - if (parts[1].find_first_not_of('0') == std::string::npos and parts[0] != ifname) - { - const auto& ip = parts[2]; - if ((ip.size() == sizeof(uint32_t) * 2) and oxenmq::is_hex(ip)) - { - huint32_t x{}; - oxenmq::from_hex(ip.begin(), ip.end(), reinterpret_cast(&x.h)); - gateways.emplace_back(x.ToString()); - } - } - } -#endif - return gateways; -#elif _WIN32 - ForEachWIN32Interface([&](auto w32interface) { - struct in_addr gateway, interface_addr; - gateway.S_un.S_addr = (u_long)w32interface->dwForwardDest; - interface_addr.S_un.S_addr = (u_long)w32interface->dwForwardNextHop; - std::string interface_name{inet_ntoa(interface_addr)}; - if ((!gateway.S_un.S_addr) and interface_name != ifname) - { - llarp::LogTrace( - "Win32 find gateway: Adding gateway (", interface_name, ") to list of gateways."); - gateways.push_back(std::move(interface_name)); - } - }); - return gateways; -#elif __APPLE__ - LogDebug("get gateways not on ", ifname); - // mac os is so godawful man - FILE* p = popen("/usr/sbin/netstat -rn -f inet", "r"); - if (p == nullptr) - { - return gateways; - } - char* line = nullptr; - size_t len = 0; - ssize_t read = 0; - while ((read = getline(&line, &len, p)) != -1) - { - const std::string line_str(line, len); - const auto parts = llarp::split_any(line_str, " "sv, true); - if (parts[0] == "default" and parts[3] != ifname) - { - gateways.emplace_back(parts[1]); - } - } - pclose(p); - return gateways; -#else -#error unsupported platform -#endif - } - -} // namespace llarp::net diff --git a/llarp/net/route.hpp b/llarp/net/route.hpp deleted file mode 100644 index 6dccbbf23..000000000 --- a/llarp/net/route.hpp +++ /dev/null @@ -1,28 +0,0 @@ -#pragma once - -#include -#include - -namespace llarp::net -{ - /// get every ip address that is a gateway that isn't owned by interface with name ifname - std::vector - GetGatewaysNotOnInterface(std::string ifname); - - /// add route to ipaddr via gateway ip - void - AddRoute(std::string ipaddr, std::string gateway); - - /// delete route to ipaddr via gateway ip - void - DelRoute(std::string ipaddr, std::string gateway); - - /// add default route via interface with name ifname - void - AddDefaultRouteViaInterface(std::string ifname); - - /// delete default route via interface with name ifname - void - DelDefaultRouteViaInterface(std::string ifname); - -} // namespace llarp::net diff --git a/llarp/net/sock_addr.cpp b/llarp/net/sock_addr.cpp index 2fd6ddf5a..a09a88527 100644 --- a/llarp/net/sock_addr.cpp +++ b/llarp/net/sock_addr.cpp @@ -244,7 +244,7 @@ namespace llarp // NOTE: this potentially involves multiple memory allocations, // reimplement without split() if it is performance bottleneck - auto splits = split(str, ':'); + auto splits = split(str, ":"); // TODO: having ":port" at the end makes this ambiguous with IPv6 // come up with a strategy for implementing @@ -260,7 +260,7 @@ namespace llarp assert(splits.size() > 0); // splits[0] should be dot-separated IPv4 - auto ipSplits = split(splits[0], '.'); + auto ipSplits = split(splits[0], "."); if (ipSplits.size() != 4) throw std::invalid_argument(stringify(str, " is not a valid IPv4 address")); @@ -299,24 +299,17 @@ namespace llarp SockAddr::hostString() const { std::string str; - + char buf[INET6_ADDRSTRLEN] = {0x0}; if (isIPv4()) { // handle IPv4 mapped addrs - constexpr auto MaxIPv4PlusPortStringSize = 22; - str.reserve(MaxIPv4PlusPortStringSize); - char buf[128] = {0x0}; inet_ntop(AF_INET, &m_addr4.sin_addr.s_addr, buf, sizeof(buf)); - str.append(buf); + str = buf; } else { - constexpr auto MaxIPv6PlusPortStringSize = 128; - str.reserve(MaxIPv6PlusPortStringSize); - - char buf[128] = {0x0}; inet_ntop(AF_INET6, &m_addr.sin6_addr.s6_addr, buf, sizeof(buf)); - + str.reserve(std::strlen(buf) + 2); str.append("["); str.append(buf); str.append("]"); diff --git a/llarp/router/i_gossiper.hpp b/llarp/router/i_gossiper.hpp index 6df477de0..86954930f 100644 --- a/llarp/router/i_gossiper.hpp +++ b/llarp/router/i_gossiper.hpp @@ -3,6 +3,9 @@ namespace llarp { + /// The maximum number of peers we will flood a gossiped RC to when propagating an RC + constexpr size_t MaxGossipPeers = 20; + struct I_RCGossiper { virtual ~I_RCGossiper() = default; diff --git a/llarp/router/rc_gossiper.cpp b/llarp/router/rc_gossiper.cpp index e9f839cc1..7eb8aa88a 100644 --- a/llarp/router/rc_gossiper.cpp +++ b/llarp/router/rc_gossiper.cpp @@ -76,17 +76,37 @@ namespace llarp DHTImmediateMessage gossip; gossip.msgs.emplace_back(new dht::GotRouterMessage(dht::Key_t{}, 0, {rc}, false)); - // send it to everyone + std::vector gossipTo; + + // select peers to gossip to + m_LinkManager->ForEachPeer( + [&](const ILinkSession* peerSession, bool) { + // ensure connected session + if (not(peerSession && peerSession->IsEstablished())) + return; + // check if public router + const auto other_rc = peerSession->GetRemoteRC(); + if (not other_rc.IsPublicRouter()) + return; + gossipTo.emplace_back(other_rc.pubkey); + }, + true); + + std::unordered_set keys; + // grab the keys we want to use + std::sample( + gossipTo.begin(), gossipTo.end(), std::inserter(keys, keys.end()), MaxGossipPeers, CSRNG{}); + m_LinkManager->ForEachPeer([&](ILinkSession* peerSession) { - // ensure connected session if (not(peerSession && peerSession->IsEstablished())) return; - // check if public router - const auto other_rc = peerSession->GetRemoteRC(); - if (not other_rc.IsPublicRouter()) + + // exclude from gossip as we have not selected to use it + if (keys.count(peerSession->GetPubKey()) == 0) return; + // encode message - ILinkSession::Message_t msg; + ILinkSession::Message_t msg{}; msg.resize(MAX_LINK_MSG_SIZE / 2); llarp_buffer_t buf(msg); if (not gossip.BEncode(&buf)) diff --git a/llarp/router/route_poker.cpp b/llarp/router/route_poker.cpp index 29818e840..9017125bc 100644 --- a/llarp/router/route_poker.cpp +++ b/llarp/router/route_poker.cpp @@ -1,7 +1,6 @@ #include "route_poker.hpp" #include "abstractrouter.hpp" #include "net/sock_addr.hpp" -#include #include #include @@ -30,13 +29,15 @@ namespace llarp void RoutePoker::DisableRoute(huint32_t ip, huint32_t gateway) { - net::DelRoute(ip.ToString(), gateway.ToString()); + vpn::IRouteManager& route = m_Router->GetVPNPlatform()->RouteManager(); + route.DelRoute(ip, gateway); } void RoutePoker::EnableRoute(huint32_t ip, huint32_t gateway) { - net::AddRoute(ip.ToString(), gateway.ToString()); + vpn::IRouteManager& route = m_Router->GetVPNPlatform()->RouteManager(); + route.AddRoute(ip, gateway); } void @@ -86,11 +87,13 @@ namespace llarp RoutePoker::~RoutePoker() { + vpn::IRouteManager& route = m_Router->GetVPNPlatform()->RouteManager(); for (const auto& [ip, gateway] : m_PokedRoutes) { if (gateway.h) - net::DelRoute(ip.ToString(), gateway.ToString()); + route.DelRoute(ip, gateway); } + route.DelBlackhole(); } std::optional @@ -100,14 +103,17 @@ namespace llarp throw std::runtime_error("Attempting to use RoutePoker before calling Init"); const auto ep = m_Router->hiddenServiceContext().GetDefault(); - const auto gateways = net::GetGatewaysNotOnInterface(ep->GetIfName()); + vpn::IRouteManager& route = m_Router->GetVPNPlatform()->RouteManager(); + const auto gateways = route.GetGatewaysNotOnInterface(ep->GetIfName()); if (gateways.empty()) { return std::nullopt; } - huint32_t addr{}; - addr.FromString(gateways[0]); - return addr; + if (auto* ptr = std::get_if(&gateways[0])) + { + return huint32_t{*ptr}; + } + return std::nullopt; } void @@ -185,13 +191,17 @@ namespace llarp void RoutePoker::Up() { + vpn::IRouteManager& route = m_Router->GetVPNPlatform()->RouteManager(); + + // black hole all routes by default + route.AddBlackhole(); // explicit route pokes for first hops m_Router->ForEachPeer( [&](auto session, auto) mutable { AddRoute(session->GetRemoteEndpoint().asIPv4()); }, false); // add default route const auto ep = m_Router->hiddenServiceContext().GetDefault(); - net::AddDefaultRouteViaInterface(ep->GetIfName()); + route.AddDefaultRouteViaInterface(ep->GetIfName()); } void @@ -203,7 +213,11 @@ namespace llarp false); // remove default route const auto ep = m_Router->hiddenServiceContext().GetDefault(); - net::DelDefaultRouteViaInterface(ep->GetIfName()); + vpn::IRouteManager& route = m_Router->GetVPNPlatform()->RouteManager(); + + route.DelDefaultRouteViaInterface(ep->GetIfName()); + // delete route blackhole + route.DelBlackhole(); } } // namespace llarp diff --git a/llarp/router/router.cpp b/llarp/router/router.cpp index d5333ba9f..4fa608afc 100644 --- a/llarp/router/router.cpp +++ b/llarp/router/router.cpp @@ -12,7 +12,6 @@ #include #include #include -#include #include #include #include diff --git a/llarp/rpc/endpoint_rpc.cpp b/llarp/rpc/endpoint_rpc.cpp index 8f59576c6..302f14d65 100644 --- a/llarp/rpc/endpoint_rpc.cpp +++ b/llarp/rpc/endpoint_rpc.cpp @@ -22,7 +22,7 @@ namespace llarp::rpc if (m_AuthURL.empty() or m_AuthMethod.empty()) return; m_LMQ->connect_remote( - m_AuthURL, + oxenmq::address{m_AuthURL}, [self = shared_from_this()](oxenmq::ConnectionID c) { self->m_Conn = std::move(c); LogInfo("connected to endpoint auth server via ", *self->m_Conn); diff --git a/llarp/rpc/rpc_server.cpp b/llarp/rpc/rpc_server.cpp index ac4dac512..8e880a187 100644 --- a/llarp/rpc/rpc_server.cpp +++ b/llarp/rpc/rpc_server.cpp @@ -4,13 +4,13 @@ #include #include #include -#include #include #include #include #include #include #include +#include namespace llarp::rpc { @@ -562,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/util/str.cpp b/llarp/util/str.cpp index a87cfd9ed..512d5c55b 100644 --- a/llarp/util/str.cpp +++ b/llarp/util/str.cpp @@ -67,35 +67,6 @@ namespace llarp return str; } - std::vector - split(const std::string_view str, char delimiter) - { - std::vector splits; - const auto str_size = str.size(); - size_t last = 0; - size_t next = 0; - while (last < str_size and next < std::string_view::npos) - { - next = str.find_first_of(delimiter, last); - if (next > last) - { - splits.push_back(str.substr(last, next - last)); - - last = next; - - // advance to next non-delimiter - while (str[last] == delimiter and last < str_size) - last++; - } - else - { - break; - } - } - - return splits; - } - using namespace std::literals; std::vector diff --git a/llarp/util/str.hpp b/llarp/util/str.hpp index ab6e4588d..2fdebe838 100644 --- a/llarp/util/str.hpp +++ b/llarp/util/str.hpp @@ -35,14 +35,6 @@ namespace llarp return o.str(); } - /// Split a string on a given delimiter - // - /// @param str is the string to split - /// @param delimiter is the character to split on - /// @return a vector of std::string_views with the split words, excluding the delimeter - std::vector - split(const std::string_view str, char delimiter); - using namespace std::literals; /// Returns true if the first string is equal to the second string, compared case-insensitively. diff --git a/llarp/vpn/android.hpp b/llarp/vpn/android.hpp index b2c3ef5b3..c9792a009 100644 --- a/llarp/vpn/android.hpp +++ b/llarp/vpn/android.hpp @@ -60,9 +60,32 @@ namespace llarp::vpn } }; + class AndroidRouteManager : public IRouteManager + { + void AddRoute(IPVariant_t, IPVariant_t) override{}; + + void DelRoute(IPVariant_t, IPVariant_t) override{}; + + void AddDefaultRouteViaInterface(std::string) override{}; + + void DelDefaultRouteViaInterface(std::string) override{}; + + void + AddRouteViaInterface(NetworkInterface&, IPRange) override{}; + + void + DelRouteViaInterface(NetworkInterface&, IPRange) override{}; + + std::vector GetGatewaysNotOnInterface(std::string) override + { + return std::vector{}; + }; + }; + class AndroidPlatform : public Platform { const int fd; + AndroidRouteManager _routeManager{}; public: AndroidPlatform(llarp::Context* ctx) : fd(ctx->androidFD) @@ -73,6 +96,11 @@ namespace llarp::vpn { return std::make_shared(std::move(info), fd); } + IRouteManager& + RouteManager() override + { + return _routeManager; + } }; } // namespace llarp::vpn 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/linux.hpp b/llarp/vpn/linux.hpp index 8d67a6256..5401ef10a 100644 --- a/llarp/vpn/linux.hpp +++ b/llarp/vpn/linux.hpp @@ -6,9 +6,19 @@ #include #include #include "common.hpp" -#include +#include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include + namespace llarp::vpn { struct in6_ifreq @@ -121,14 +131,334 @@ namespace llarp::vpn } }; + class LinuxRouteManager : public IRouteManager + { + const int fd; + + enum class GatewayMode + { + eFirstHop, + eLowerDefault, + eUpperDefault + }; + + struct NLRequest + { + nlmsghdr n; + rtmsg r; + char buf[4096]; + + void + AddData(int type, const void* data, int alen) + { +#define NLMSG_TAIL(nmsg) ((struct rtattr*)(((intptr_t)(nmsg)) + NLMSG_ALIGN((nmsg)->nlmsg_len))) + + int len = RTA_LENGTH(alen); + rtattr* rta; + if (NLMSG_ALIGN(n.nlmsg_len) + RTA_ALIGN(len) > sizeof(*this)) + { + throw std::length_error{"nlrequest add data overflow"}; + } + rta = NLMSG_TAIL(&n); + rta->rta_type = type; + rta->rta_len = len; + if (alen) + { + memcpy(RTA_DATA(rta), data, alen); + } + n.nlmsg_len = NLMSG_ALIGN(n.nlmsg_len) + RTA_ALIGN(len); +#undef NLMSG_TAIL + } + }; + + /* Helper structure for ip address data and attributes */ + struct _inet_addr + { + unsigned char family; + unsigned char bitlen; + unsigned char data[sizeof(struct in6_addr)]; + + _inet_addr(huint32_t addr, size_t bits = 32) + { + family = AF_INET; + bitlen = bits; + htobe32buf(data, addr.h); + } + + _inet_addr(huint128_t addr, size_t bits = 128) + { + family = AF_INET6; + bitlen = bits; + const nuint128_t net = ToNet(addr); + std::memcpy(data, &net, 16); + } + }; + + void + Blackhole(int cmd, int flags, int af) + { + NLRequest nl_request{}; + /* Initialize request structure */ + nl_request.n.nlmsg_len = NLMSG_LENGTH(sizeof(struct rtmsg)); + nl_request.n.nlmsg_flags = NLM_F_REQUEST | flags; + nl_request.n.nlmsg_type = cmd; + nl_request.n.nlmsg_pid = getpid(); + nl_request.r.rtm_family = af; + nl_request.r.rtm_table = RT_TABLE_LOCAL; + nl_request.r.rtm_type = RTN_BLACKHOLE; + nl_request.r.rtm_scope = RT_SCOPE_UNIVERSE; + if (af == AF_INET) + { + uint32_t addr{}; + nl_request.AddData(RTA_DST, &addr, sizeof(addr)); + } + else + { + uint128_t addr{}; + nl_request.AddData(RTA_DST, &addr, sizeof(addr)); + } + send(fd, &nl_request, sizeof(nl_request), 0); + } + + void + Route( + int cmd, + int flags, + const _inet_addr& dst, + const _inet_addr& gw, + GatewayMode mode, + int if_idx) + { + NLRequest nl_request{}; + + /* Initialize request structure */ + nl_request.n.nlmsg_len = NLMSG_LENGTH(sizeof(struct rtmsg)); + nl_request.n.nlmsg_flags = NLM_F_REQUEST | flags; + nl_request.n.nlmsg_type = cmd; + nl_request.n.nlmsg_pid = getpid(); + nl_request.r.rtm_family = dst.family; + nl_request.r.rtm_table = RT_TABLE_MAIN; + if (if_idx) + { + nl_request.r.rtm_scope = RT_SCOPE_LINK; + } + else + { + nl_request.r.rtm_scope = RT_SCOPE_NOWHERE; + } + /* Set additional flags if NOT deleting route */ + if (cmd != RTM_DELROUTE) + { + nl_request.r.rtm_protocol = RTPROT_BOOT; + nl_request.r.rtm_type = RTN_UNICAST; + } + + nl_request.r.rtm_family = dst.family; + nl_request.r.rtm_dst_len = dst.bitlen; + nl_request.r.rtm_scope = 0; + + /* Set gateway */ + if (gw.bitlen != 0 and dst.family == AF_INET) + { + nl_request.AddData(RTA_GATEWAY, &gw.data, gw.bitlen / 8); + } + nl_request.r.rtm_family = gw.family; + if (mode == GatewayMode::eFirstHop) + { + nl_request.AddData(RTA_DST, &dst.data, dst.bitlen / 8); + /* Set interface */ + nl_request.AddData(RTA_OIF, &if_idx, sizeof(int)); + } + if (mode == GatewayMode::eUpperDefault) + { + if (dst.family == AF_INET) + { + nl_request.AddData(RTA_DST, &dst.data, sizeof(uint32_t)); + } + else + { + nl_request.AddData(RTA_OIF, &if_idx, sizeof(int)); + nl_request.AddData(RTA_DST, &dst.data, sizeof(in6_addr)); + } + } + /* Send message to the netlink */ + send(fd, &nl_request, sizeof(nl_request), 0); + } + + void + DefaultRouteViaInterface(std::string ifname, int cmd, int flags) + { + int if_idx = if_nametoindex(ifname.c_str()); + const auto maybe = GetInterfaceAddr(ifname); + if (not maybe) + throw std::runtime_error{"we dont have our own network interface?"}; + + const _inet_addr gateway{maybe->asIPv4()}; + const _inet_addr lower{ipaddr_ipv4_bits(0, 0, 0, 0), 1}; + const _inet_addr upper{ipaddr_ipv4_bits(128, 0, 0, 0), 1}; + + Route(cmd, flags, lower, gateway, GatewayMode::eLowerDefault, if_idx); + Route(cmd, flags, upper, gateway, GatewayMode::eUpperDefault, if_idx); + + if (const auto maybe6 = GetInterfaceIPv6Address(ifname)) + { + const _inet_addr gateway6{*maybe6, 128}; + for (const std::string str : {"::", "4000::", "8000::", "c000::"}) + { + huint128_t _hole{}; + _hole.FromString(str); + const _inet_addr hole6{_hole, 2}; + Route(cmd, flags, hole6, gateway6, GatewayMode::eUpperDefault, if_idx); + } + } + } + + void + RouteViaInterface(int cmd, int flags, std::string ifname, IPRange range) + { + int if_idx = if_nametoindex(ifname.c_str()); + if (range.IsV4()) + { + const auto maybe = GetInterfaceAddr(ifname); + if (not maybe) + throw std::runtime_error{"we dont have our own network interface?"}; + + const _inet_addr gateway{maybe->asIPv4()}; + + const _inet_addr addr{ + net::TruncateV6(range.addr), bits::count_bits(net::TruncateV6(range.netmask_bits))}; + + Route(cmd, flags, addr, gateway, GatewayMode::eUpperDefault, if_idx); + } + else + { + const auto maybe = GetInterfaceIPv6Address(ifname); + if (not maybe) + throw std::runtime_error{"we dont have our own network interface?"}; + const _inet_addr gateway{*maybe, 128}; + const _inet_addr addr{range.addr, bits::count_bits(range.netmask_bits)}; + Route(cmd, flags, addr, gateway, GatewayMode::eUpperDefault, if_idx); + } + } + + void + Route(int cmd, int flags, IPVariant_t ip, IPVariant_t gateway) + { + // do bullshit double std::visit because lol variants + std::visit( + [gateway, cmd, flags, this](auto&& ip) { + const _inet_addr toAddr{ip}; + std::visit( + [toAddr, cmd, flags, this](auto&& gateway) { + const _inet_addr gwAddr{gateway}; + Route(cmd, flags, toAddr, gwAddr, GatewayMode::eFirstHop, 0); + }, + gateway); + }, + ip); + } + + public: + LinuxRouteManager() : fd{socket(AF_NETLINK, SOCK_DGRAM, NETLINK_ROUTE)} + { + if (fd == -1) + throw std::runtime_error{"failed to make netlink socket"}; + } + + ~LinuxRouteManager() + { + close(fd); + } + + void + AddRoute(IPVariant_t ip, IPVariant_t gateway) override + { + Route(RTM_NEWROUTE, NLM_F_CREATE | NLM_F_EXCL, ip, gateway); + } + + void + DelRoute(IPVariant_t ip, IPVariant_t gateway) override + { + Route(RTM_DELROUTE, 0, ip, gateway); + } + + void + AddDefaultRouteViaInterface(std::string ifname) override + { + DefaultRouteViaInterface(ifname, RTM_NEWROUTE, NLM_F_CREATE | NLM_F_EXCL); + } + + void + DelDefaultRouteViaInterface(std::string ifname) override + { + DefaultRouteViaInterface(ifname, RTM_DELROUTE, 0); + } + + void + AddRouteViaInterface(NetworkInterface& vpn, IPRange range) override + { + RouteViaInterface(RTM_NEWROUTE, NLM_F_CREATE | NLM_F_EXCL, vpn.IfName(), range); + } + + void + DelRouteViaInterface(NetworkInterface& vpn, IPRange range) override + { + RouteViaInterface(RTM_DELROUTE, 0, vpn.IfName(), range); + } + + std::vector + GetGatewaysNotOnInterface(std::string ifname) override + { + std::vector gateways{}; + std::ifstream inf{"/proc/net/route"}; + for (std::string line; std::getline(inf, line);) + { + const auto parts = split(line, "\t"); + if (parts[1].find_first_not_of('0') == std::string::npos and parts[0] != ifname) + { + const auto& ip = parts[2]; + if ((ip.size() == sizeof(uint32_t) * 2) and oxenmq::is_hex(ip)) + { + huint32_t x{}; + oxenmq::from_hex(ip.begin(), ip.end(), reinterpret_cast(&x.h)); + gateways.emplace_back(x); + } + } + } + return gateways; + } + + void + AddBlackhole() override + { + Blackhole(RTM_NEWROUTE, NLM_F_CREATE | NLM_F_EXCL, AF_INET); + Blackhole(RTM_NEWROUTE, NLM_F_CREATE | NLM_F_EXCL, AF_INET6); + } + + void + DelBlackhole() override + { + Blackhole(RTM_DELROUTE, 0, AF_INET); + Blackhole(RTM_DELROUTE, 0, AF_INET6); + } + }; + class LinuxPlatform : public Platform { + LinuxRouteManager _routeManager{}; + public: std::shared_ptr ObtainInterface(InterfaceInfo info) override { return std::make_shared(std::move(info)); }; + + IRouteManager& + RouteManager() override + { + return _routeManager; + } }; } // 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/llarp/vpn/win32.hpp b/llarp/vpn/win32.hpp index 405cfabe1..06e2c8297 100644 --- a/llarp/vpn/win32.hpp +++ b/llarp/vpn/win32.hpp @@ -117,6 +117,57 @@ namespace llarp::vpn return deviceid; } + template + void + ForEachWIN32Interface(Visit visit) + { +#define MALLOC(x) HeapAlloc(GetProcessHeap(), 0, (x)) +#define FREE(x) HeapFree(GetProcessHeap(), 0, (x)) + MIB_IPFORWARDTABLE* pIpForwardTable; + DWORD dwSize = 0; + DWORD dwRetVal = 0; + + pIpForwardTable = (MIB_IPFORWARDTABLE*)MALLOC(sizeof(MIB_IPFORWARDTABLE)); + if (pIpForwardTable == nullptr) + return; + + if (GetIpForwardTable(pIpForwardTable, &dwSize, 0) == ERROR_INSUFFICIENT_BUFFER) + { + FREE(pIpForwardTable); + pIpForwardTable = (MIB_IPFORWARDTABLE*)MALLOC(dwSize); + if (pIpForwardTable == nullptr) + { + return; + } + } + + if ((dwRetVal = GetIpForwardTable(pIpForwardTable, &dwSize, 0)) == NO_ERROR) + { + for (int i = 0; i < (int)pIpForwardTable->dwNumEntries; i++) + { + visit(&pIpForwardTable->table[i]); + } + } + FREE(pIpForwardTable); +#undef MALLOC +#undef FREE + } + + std::optional + GetInterfaceIndex(huint32_t ip) + { + std::optional ret = std::nullopt; + ForEachWIN32Interface([&ret, n = ToNet(ip)](auto* iface) { + if (ret.has_value()) + return; + if (iface->dwForwardNextHop == n.n) + { + ret = iface->dwForwardIfIndex; + } + }); + return ret; + } + class Win32Interface final : public NetworkInterface { std::atomic m_Run; @@ -124,7 +175,7 @@ namespace llarp::vpn std::vector m_Threads; thread::Queue m_ReadQueue; - const InterfaceInfo m_Info; + InterfaceInfo m_Info; static std::wstring get_win_sys_path() @@ -159,6 +210,16 @@ namespace llarp::vpn } public: + static std::string + RouteCommand() + { + std::wstring wcmd = get_win_sys_path() + L"\\route.exe"; + + using convert_type = std::codecvt_utf8; + std::wstring_convert converter; + return converter.to_bytes(wcmd); + } + Win32Interface(InterfaceInfo info) : m_ReadQueue{1024}, m_Info{std::move(info)} { DWORD len; @@ -213,6 +274,7 @@ namespace llarp::vpn IPADDR sock[3]{}; const nuint32_t addr = xhtonl(net::TruncateV6(ifaddr.range.addr)); ip = net::TruncateV6(ifaddr.range.addr); + m_Info.ifname = ip.ToString(); const nuint32_t mask = xhtonl(net::TruncateV6(ifaddr.range.netmask_bits)); LogInfo("address ", addr, " netmask ", mask); sock[0] = addr.n; @@ -306,7 +368,7 @@ namespace llarp::vpn { if (ifaddr.fam == AF_INET6) { - const auto maybe = net::GetInterfaceIndex(ip); + const auto maybe = GetInterfaceIndex(ip); if (maybe.has_value()) { NetSH( @@ -348,7 +410,7 @@ namespace llarp::vpn std::string IfName() const override { - return ""; + return m_Info.ifname; } void @@ -433,8 +495,138 @@ namespace llarp::vpn } }; + class Win32RouteManager : public IRouteManager + { + void + Execute(std::string cmd) const + { + LogInfo(cmd); + ::system(cmd.c_str()); + } + + static std::string + RouteCommand() + { + return Win32Interface::RouteCommand(); + } + + void + Route(IPVariant_t ip, IPVariant_t gateway, std::string cmd) + { + std::stringstream ss; + std::string ip_str; + std::string gateway_str; + + std::visit([&ip_str](auto&& ip) { ip_str = ip.ToString(); }, ip); + std::visit([&gateway_str](auto&& gateway) { gateway_str = gateway.ToString(); }, gateway); + + ss << RouteCommand() << " " << cmd << " " << ip_str << " MASK 255.255.255.255 " << gateway_str + << " METRIC 2"; + Execute(ss.str()); + } + + void + DefaultRouteViaInterface(std::string ifname, std::string cmd) + { + // poke hole for loopback bacause god is dead on windows + Execute(RouteCommand() + " " + cmd + " 127.0.0.0 MASK 255.0.0.0 0.0.0.0"); + + huint32_t ip{}; + ip.FromString(ifname); + const auto ipv6 = net::ExpandV4Lan(ip); + + Execute(RouteCommand() + " " + cmd + " ::/2 " + ipv6.ToString()); + Execute(RouteCommand() + " " + cmd + " 4000::/2 " + ipv6.ToString()); + Execute(RouteCommand() + " " + cmd + " 8000::/2 " + ipv6.ToString()); + Execute(RouteCommand() + " " + cmd + " c000::/2 " + ipv6.ToString()); + + ifname.back()++; + Execute(RouteCommand() + " " + cmd + " 0.0.0.0 MASK 128.0.0.0 " + ifname); + Execute(RouteCommand() + " " + cmd + " 128.0.0.0 MASK 128.0.0.0 " + ifname); + } + + void + RouteViaInterface(std::string ifname, IPRange range, std::string cmd) + { + if (range.IsV4()) + { + const huint32_t addr4 = net::TruncateV6(range.addr); + const huint32_t mask4 = net::TruncateV6(range.netmask_bits); + Execute( + RouteCommand() + " " + cmd + " " + addr4.ToString() + " MASK " + mask4.ToString() + " " + + ifname); + } + else + { + Execute( + RouteCommand() + " " + cmd + range.addr.ToString() + " MASK " + + range.netmask_bits.ToString() + " " + ifname); + } + } + + public: + void + AddRoute(IPVariant_t ip, IPVariant_t gateway) override + { + Route(ip, gateway, "ADD"); + } + + void + DelRoute(IPVariant_t ip, IPVariant_t gateway) override + { + Route(ip, gateway, "DELETE"); + } + + void + AddRouteViaInterface(NetworkInterface& vpn, IPRange range) override + { + RouteViaInterface(vpn.IfName(), range, "ADD"); + } + + void + DelRouteViaInterface(NetworkInterface& vpn, IPRange range) override + { + RouteViaInterface(vpn.IfName(), range, "DELETE"); + } + + std::vector + GetGatewaysNotOnInterface(std::string ifname) override + { + std::vector gateways; + ForEachWIN32Interface([&](auto w32interface) { + struct in_addr gateway, interface_addr; + gateway.S_un.S_addr = (u_long)w32interface->dwForwardDest; + interface_addr.S_un.S_addr = (u_long)w32interface->dwForwardNextHop; + std::string interface_name{inet_ntoa(interface_addr)}; + if ((!gateway.S_un.S_addr) and interface_name != ifname) + { + llarp::LogTrace( + "Win32 find gateway: Adding gateway (", interface_name, ") to list of gateways."); + huint32_t x{}; + if (x.FromString(interface_name)) + gateways.push_back(x); + } + }); + return gateways; + } + + void + AddDefaultRouteViaInterface(std::string ifname) override + { + DefaultRouteViaInterface(ifname, "ADD"); + } + + void + DelDefaultRouteViaInterface(std::string ifname) override + { + DefaultRouteViaInterface(ifname, "DELETE"); + } + }; + class Win32Platform : public Platform { + Win32RouteManager _routeManager{}; + public: std::shared_ptr ObtainInterface(InterfaceInfo info) override @@ -443,6 +635,11 @@ namespace llarp::vpn netif->Start(); return netif; }; + IRouteManager& + RouteManager() override + { + return _routeManager; + } }; } // namespace llarp::vpn diff --git a/lokinet-bootstrap b/lokinet-bootstrap deleted file mode 100755 index 683173b69..000000000 --- a/lokinet-bootstrap +++ /dev/null @@ -1,65 +0,0 @@ -#!/usr/bin/env bash -# this shell script will be replaced by a proper program in the future (probably) -# -# from https://stackoverflow.com/questions/5947742/how-to-change-the-output-color-of-echo-in-linux -RED='\033[0;31m' -GREEN='\033[0;32m' -NC='\033[0m' # No Color - -set -e - -helpme= -default_mainnet=https://seed.lokinet.org/lokinet.signed -default_testnet=https://seed.lokinet.org/testnet.signed -default_dest="$HOME/.lokinet/bootstrap.signed" - -if [ "$#" -gt 2 ]; then - helpme=y -fi - -if [ -z "$1" ] || [ "$1" == "mainnet" ] || [ "$1" == "lokinet" ]; then - url="${default_mainnet}" -elif [ "$1" == "testnet" ]; then - url="${default_testnet}" -elif [[ "$1" = -* ]]; then - helpme=y -else - url="$1" -fi - -if [[ "$2" = -* ]]; then - helpme=y -elif [ -n "$2" ]; then - dest="$2" -else - dest="$default_dest" -fi - -if [ -n "$helpme" ]; then - echo "Usage: $0 [URL [DEST]] -- download bootstrap file from URL (default: lokinet) and save to DEST (default: $default_dest)." - echo "URL can be a full URL, or else 'lokinet' or 'testnet' to use the default lokinet/testnet seed URL. 'mainnet' can be used" - echo "as an alias for 'lokinet'." - exit 1 -fi - -destdir="$(dirname $dest)" -if [ ! -d "$destdir" ]; then - mkdir "$destdir" -fi - -echo "downloading $url" - -# use temp file to not overrwrite existing bootstrap file on fail -#tmp=mktemp -tmp=/tmp/bootstrap.tmp - -# MacOS does not have wget without homebrew but does have curl -# Rick also had indicated most BSDs have curl too -if curl -fsSL "$url" >"$tmp"; then - mv "$tmp" "$dest" - echo -e "${GREEN}lokinet successfully bootstrapped${NC}" -else - echo -e "${RED}failed to download bootstrap from $url${NC}" - rm -f "$tmp" - exit 1 -fi diff --git a/readme.md b/readme.md index 69ddd90c4..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 @@ -153,12 +146,17 @@ to configure as relay: ## Running on Linux -**DO NOT RUN AS ROOT**, run as normal user. This requires the binary to have the proper setcaps set by `make install` on the binary. +**DO NOT RUN AS ROOT**, run as normal user. to run, after you create default config: $ lokinet +This requires the binary to have the proper capabilities which is usually set by `make install` on the binary. If you have errors regarding permissions to open a new interface this can be resolved using: + + $ sudo setcap cap_net_admin,cap_net_bind_service=+eip /usr/local/bin/lokinet + + ## Running on macOS/UNIX/BSD **YOU HAVE TO RUN AS ROOT**, run using sudo. Elevated privileges are needed to create the virtual tunnel interface. diff --git a/test/net/test_sock_addr.cpp b/test/net/test_sock_addr.cpp index 9102a96ed..c0b666745 100644 --- a/test/net/test_sock_addr.cpp +++ b/test/net/test_sock_addr.cpp @@ -43,9 +43,9 @@ TEST_CASE("SockAddr fromString", "[SockAddr]") CHECK_THROWS_WITH(llarp::SockAddr("1.2.3"), "1.2.3 is not a valid IPv4 address"); - CHECK_THROWS_WITH(llarp::SockAddr("1.2.3."), "1.2.3. is not a valid IPv4 address"); + CHECK_THROWS_WITH(llarp::SockAddr("1.2.3."), "1.2.3. contains invalid numeric value"); - CHECK_THROWS_WITH(llarp::SockAddr(".1.2.3"), ".1.2.3 is not a valid IPv4 address"); + CHECK_THROWS_WITH(llarp::SockAddr(".1.2.3"), ".1.2.3 contains invalid numeric value"); CHECK_THROWS_WITH(llarp::SockAddr("1.2.3.4.5"), "1.2.3.4.5 is not a valid IPv4 address"); diff --git a/test/util/test_llarp_util_str.cpp b/test/util/test_llarp_util_str.cpp index b7cd15fba..b53173660 100644 --- a/test/util/test_llarp_util_str.cpp +++ b/test/util/test_llarp_util_str.cpp @@ -91,7 +91,7 @@ TEST_CASE("neither true nor false string values", "[str][nottruefalse]") { } TEST_CASE("split strings with multiple matches", "[str]") { - auto splits = llarp::split("this is a test", ' '); + auto splits = llarp::split("this is a test", " "); REQUIRE(splits.size() == 4); REQUIRE(splits[0] == "this"); REQUIRE(splits[1] == "is"); @@ -100,13 +100,13 @@ TEST_CASE("split strings with multiple matches", "[str]") { } TEST_CASE("split strings with single match", "[str]") { - auto splits = llarp::split("uno", ';'); + auto splits = llarp::split("uno", ";"); REQUIRE(splits.size() == 1); REQUIRE(splits[0] == "uno"); } -TEST_CASE("split strings with consecutive delimiters", "[str]") { - auto splits = llarp::split("a o e u", ' '); +TEST_CASE("split_any strings with consecutive delimiters", "[str]") { + auto splits = llarp::split_any("a o e u", " "); REQUIRE(splits.size() == 4); REQUIRE(splits[0] == "a"); REQUIRE(splits[1] == "o"); @@ -115,14 +115,35 @@ TEST_CASE("split strings with consecutive delimiters", "[str]") { } TEST_CASE("split delimiter-only string", "[str]") { - auto splits = llarp::split(" ", ' '); - REQUIRE(splits.size() == 0); + { + auto splits = llarp::split(" ", " "); + REQUIRE(splits.size() == 5); + } + + { + auto splits = llarp::split_any(" ", " "); + REQUIRE(splits.size() == 2); + } + + { + auto splits = llarp::split(" ", " ", true); + REQUIRE(splits.size() == 0); + } - splits = llarp::split(" ", ' '); - REQUIRE(splits.size() == 0); + { + auto splits = llarp::split_any(" ", " ", true); + REQUIRE(splits.size() == 0); + } } TEST_CASE("split empty string", "[str]") { - auto splits = llarp::split("", ' '); - REQUIRE(splits.size() == 0); + { + auto splits = llarp::split("", " "); + REQUIRE(splits.size() == 1); + } + + { + auto splits = llarp::split("", " ", true); + REQUIRE(splits.size() == 0); + } }