diff --git a/CMakeLists.txt b/CMakeLists.txt index 028c5a333..eca295599 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,7 +3,7 @@ cmake_minimum_required(VERSION 3.10) # bionic's cmake version 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(CMAKE_OSX_DEPLOYMENT_TARGET 10.15 CACHE STRING "macOS deployment target (Apple clang only)") option(BUILD_DAEMON "build lokinet daemon and associated utils" ON) @@ -311,6 +311,6 @@ if(NOT TARGET uninstall) endif() -if(BUILD_PACKAGE AND NOT APPLE) +if(BUILD_PACKAGE) include(cmake/installer.cmake) endif() diff --git a/cmake/StaticBuild.cmake b/cmake/StaticBuild.cmake index 3ddbe9757..db39eff78 100644 --- a/cmake/StaticBuild.cmake +++ b/cmake/StaticBuild.cmake @@ -351,6 +351,15 @@ set_target_properties(libzmq PROPERTIES INTERFACE_LINK_LIBRARIES "${libzmq_link_libs}" INTERFACE_COMPILE_DEFINITIONS "ZMQ_STATIC") + +# +# +# +# Everything that follows is *only* for lokinet-bootstrap (i.e. if adding new deps put them *above* +# this). +# +# +# if(NOT WITH_BOOTSTRAP) return() endif() diff --git a/cmake/check_for_std_filesystem.cmake b/cmake/check_for_std_filesystem.cmake index aef0448ec..84248d07e 100644 --- a/cmake/check_for_std_filesystem.cmake +++ b/cmake/check_for_std_filesystem.cmake @@ -44,6 +44,7 @@ if(filesystem_is_good EQUAL 1) else() # Probably broken AF macos message(STATUS "std::filesystem is not available, apparently this compiler isn't C++17 compliant; falling back to ghc::filesystem") + set(GHC_FILESYSTEM_WITH_INSTALL OFF CACHE INTERNAL "") add_subdirectory(external/ghc-filesystem) target_link_libraries(filesystem INTERFACE ghc_filesystem) target_compile_definitions(filesystem INTERFACE USE_GHC_FILESYSTEM) diff --git a/cmake/installer.cmake b/cmake/installer.cmake index bdd4958fc..60e39f69b 100644 --- a/cmake/installer.cmake +++ b/cmake/installer.cmake @@ -5,6 +5,8 @@ set(CPACK_RESOURCE_FILE_LICENSE "${PROJECT_SOURCE_DIR}/LICENSE") if(WIN32) include(cmake/win32_installer_deps.cmake) +elseif(APPLE) + set(CPACK_GENERATOR DragNDrop;ZIP) endif() diff --git a/contrib/mac.sh b/contrib/mac.sh index 0ddcbe3ff..855f6bcc5 100755 --- a/contrib/mac.sh +++ b/contrib/mac.sh @@ -18,16 +18,9 @@ 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 \ "$@" \ @@ -35,5 +28,5 @@ cmake \ ninja sign echo -e "Build complete, your app is here:\n" -ls -lad $(pwd)/daemon/lokinet.app +ls -lad $(pwd)/Lokinet.app echo "" diff --git a/contrib/macos/Info.plist.in b/contrib/macos/Info.plist.in deleted file mode 100644 index 9311f2404..000000000 --- a/contrib/macos/Info.plist.in +++ /dev/null @@ -1,24 +0,0 @@ - - - - - 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 deleted file mode 100644 index 80afb1b94..000000000 --- a/contrib/macos/LokinetExtension.Info.plist.in +++ /dev/null @@ -1,40 +0,0 @@ - - - - - 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 deleted file mode 100644 index 9880ecc3c..000000000 --- a/contrib/macos/README.txt +++ /dev/null @@ -1,38 +0,0 @@ -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.Info.plist.in b/contrib/macos/lokinet-extension.Info.plist.in new file mode 100644 index 000000000..7647393ee --- /dev/null +++ b/contrib/macos/lokinet-extension.Info.plist.in @@ -0,0 +1,64 @@ + + + + + CFBundleDevelopmentRegion + en + + CFBundleDisplayName + Lokinet Network Extension + + CFBundleExecutable + org.lokinet.network-extension + + CFBundleIdentifier + org.lokinet.network-extension + + CFBundleInfoDictionaryVersion + 6.0 + + CFBundlePackageType + SYSX + + CFBundleName + org.lokinet.network-extension + + CFBundleVersion + @lokinet_VERSION@.@LOKINET_APPLE_BUILD@ + + CFBundleShortVersionString + @lokinet_VERSION@ + + CFBundleSupportedPlatforms + + MacOSX + + + ITSAppUsesNonExemptEncryption + + + LSMinimumSystemVersion + 10.15 + + NSHumanReadableCopyright + Copyright © 2022 The Oxen Project, licensed under GPLv3-or-later + + NSSystemExtensionUsageDescription + Provides Lokinet Network connectivity. + + NetworkExtension + + NEMachServiceName + SUQ8J2PCT7.org.lokinet.network-extension + + NEProviderClasses + + com.apple.networkextension.packet-tunnel + LLARPPacketTunnel + + com.apple.networkextension.dns-proxy + LLARPDNSProxy + + + + diff --git a/contrib/macos/lokinet-extension.provisionprofile b/contrib/macos/lokinet-extension.dev.provisionprofile similarity index 79% rename from contrib/macos/lokinet-extension.provisionprofile rename to contrib/macos/lokinet-extension.dev.provisionprofile index 33605eba6..c7a1b3269 100644 Binary files a/contrib/macos/lokinet-extension.provisionprofile and b/contrib/macos/lokinet-extension.dev.provisionprofile differ diff --git a/contrib/macos/lokinet-extension.entitlements.plist b/contrib/macos/lokinet-extension.plugin.entitlements.plist similarity index 75% rename from contrib/macos/lokinet-extension.entitlements.plist rename to contrib/macos/lokinet-extension.plugin.entitlements.plist index 56c32bb55..b8baadbc7 100644 --- a/contrib/macos/lokinet-extension.entitlements.plist +++ b/contrib/macos/lokinet-extension.plugin.entitlements.plist @@ -3,11 +3,12 @@ com.apple.application-identifier - SUQ8J2PCT7.com.loki-project.lokinet.network-extension + SUQ8J2PCT7.org.lokinet.network-extension com.apple.developer.networking.networkextension - packet-tunnel-provider-systemextension + packet-tunnel-provider + dns-proxy com.apple.developer.team-identifier @@ -16,9 +17,6 @@ com.apple.security.app-sandbox - com.apple.security.get-task-allow - - com.apple.security.network.client diff --git a/contrib/macos/lokinet-extension.release.provisionprofile b/contrib/macos/lokinet-extension.release.provisionprofile new file mode 100644 index 000000000..1eaefd12e Binary files /dev/null and b/contrib/macos/lokinet-extension.release.provisionprofile differ diff --git a/contrib/macos/lokinet.entitlements.plist b/contrib/macos/lokinet-extension.sysext.entitlements.plist similarity index 60% rename from contrib/macos/lokinet.entitlements.plist rename to contrib/macos/lokinet-extension.sysext.entitlements.plist index 040256232..26086f9fe 100644 --- a/contrib/macos/lokinet.entitlements.plist +++ b/contrib/macos/lokinet-extension.sysext.entitlements.plist @@ -3,13 +3,12 @@ com.apple.application-identifier - SUQ8J2PCT7.com.loki-project.lokinet + SUQ8J2PCT7.org.lokinet.network-extension com.apple.developer.networking.networkextension packet-tunnel-provider-systemextension - dns-proxy-systemextension - dns-settings + dns-proxy-systemextension com.apple.developer.team-identifier @@ -18,13 +17,15 @@ com.apple.security.app-sandbox - com.apple.security.get-task-allow - + com.apple.security.application-groups + + SUQ8J2PCT7.org.lokinet + - com.apple.security.network.client + com.apple.security.network.client - - com.apple.security.network.server + + com.apple.security.network.server diff --git a/contrib/macos/lokinet.Info.plist.in b/contrib/macos/lokinet.Info.plist.in new file mode 100644 index 000000000..c03953cde --- /dev/null +++ b/contrib/macos/lokinet.Info.plist.in @@ -0,0 +1,39 @@ + + + + + CFBundleDevelopmentRegion + en + + CFBundleDisplayName + Lokinet + + CFBundleExecutable + Lokinet + + CFBundleIdentifier + org.lokinet + + CFBundleInfoDictionaryVersion + 6.0 + + CFBundleName + Lokinet + + CFBundlePackageType + APPL + + CFBundleShortVersionString + @lokinet_VERSION@ + + CFBundleVersion + @lokinet_VERSION@.@LOKINET_APPLE_BUILD@ + + LSMinimumSystemVersion + 10.15 + + NSHumanReadableCopyright + Copyright © 2022 The Oxen Project, licensed under GPLv3-or-later + + + diff --git a/contrib/macos/lokinet.provisionprofile b/contrib/macos/lokinet.dev.provisionprofile similarity index 80% rename from contrib/macos/lokinet.provisionprofile rename to contrib/macos/lokinet.dev.provisionprofile index 9fbcd4f7c..e15cccff4 100644 Binary files a/contrib/macos/lokinet.provisionprofile and b/contrib/macos/lokinet.dev.provisionprofile differ diff --git a/contrib/macos/lokinet.plugin.entitlements.plist b/contrib/macos/lokinet.plugin.entitlements.plist new file mode 100644 index 000000000..7c172b9e0 --- /dev/null +++ b/contrib/macos/lokinet.plugin.entitlements.plist @@ -0,0 +1,28 @@ + + + + + com.apple.application-identifier + SUQ8J2PCT7.org.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.network.client + + + com.apple.security.network.server + + + + diff --git a/contrib/macos/lokinet.release.provisionprofile b/contrib/macos/lokinet.release.provisionprofile new file mode 100644 index 000000000..6aaeead39 Binary files /dev/null and b/contrib/macos/lokinet.release.provisionprofile differ diff --git a/contrib/macos/lokinet.sysext.entitlements.plist b/contrib/macos/lokinet.sysext.entitlements.plist new file mode 100644 index 000000000..4c08d92f5 --- /dev/null +++ b/contrib/macos/lokinet.sysext.entitlements.plist @@ -0,0 +1,36 @@ + + + + + com.apple.application-identifier + SUQ8J2PCT7.org.lokinet + + com.apple.developer.networking.networkextension + + packet-tunnel-provider-systemextension + dns-proxy-systemextension + dns-settings + + + com.apple.developer.team-identifier + SUQ8J2PCT7 + + com.apple.developer.system-extension.install + + + com.apple.security.app-sandbox + + + com.apple.security.application-groups + + SUQ8J2PCT7.org.lokinet + + + com.apple.security.network.client + + + com.apple.security.network.server + + + + diff --git a/contrib/macos/notarize.py.in b/contrib/macos/notarize.py.in old mode 100644 new mode 100755 index e042bface..5752e8b55 --- a/contrib/macos/notarize.py.in +++ b/contrib/macos/notarize.py.in @@ -4,21 +4,47 @@ import sys import plistlib import subprocess import time +import os +import os.path + +def bold_red(x): + return "\x1b[31;1m" + x + "\x1b[0m" + +if not @notarize_py_is_sysext@: + print(bold_red("\nUnable to notarize: this lokinet is not built as a system extension\n"), file=sys.stderr) + sys.exit(1) + +if not all(("@MACOS_NOTARIZE_USER@", "@MACOS_NOTARIZE_PASS@", "@MACOS_NOTARIZE_ASC@")): + print(bold_red("\nUnable to notarize: one or more required notarization variable not set; see contrib/macos/README.txt\n") + + " Called with -DMACOS_NOTARIZE_USER=@MACOS_NOTARIZE_USER@\n" + " -DMACOS_NOTARIZE_PASS=@MACOS_NOTARIZE_PASS@\n" + " -DMACOS_NOTARIZE_ASC=@MACOS_NOTARIZE_ASC@\n", + file=sys.stderr) + sys.exit(1) + +os.chdir("@PROJECT_BINARY_DIR@") +app = "Lokinet.app" +zipfile = "Lokinet.app.notarize.zip" +print(f"Creating lokinet.app.notarize.zip from lokinet.app") +if os.path.exists(zipfile): + os.remove(zipfile) +subprocess.run(['zip', '-r', zipfile, app]) -pkg = "lokinet-@PROJECT_VERSION@-Darwin.pkg" userpass = ('--username', "@MACOS_NOTARIZE_USER@", '--password', "@MACOS_NOTARIZE_PASS@") -print("Submitting {} for notarization; this may take a minute...".format(pkg)) +print("Submitting {} for notarization; this may take a minute...".format(zipfile)) started = time.time() -result = subprocess.run([ +command = [ 'xcrun', 'altool', '--notarize-app', - '--primary-bundle-id', 'org.lokinet.lokinet.pkg.@PROJECT_VERSION@', + '--primary-bundle-id', 'org.lokinet.@PROJECT_VERSION@', *userpass, '--asc-provider', "@MACOS_NOTARIZE_ASC@", - '--file', pkg, + '--file', zipfile, '--output-format', 'xml' - ], stdout=subprocess.PIPE) + ] +print(command) +result = subprocess.run(command, stdout=subprocess.PIPE) data = plistlib.loads(result.stdout) if 'success-message' not in data or 'notarization-upload' not in data or 'RequestUUID' not in data['notarization-upload']: @@ -70,7 +96,12 @@ print("\n") if not success: sys.exit(42) -print("Stapling {}".format(pkg)) -result = subprocess.run(['xcrun', 'stapler', 'staple', pkg]) +if os.path.exists(zipfile): + os.remove(zipfile) + +print("Stapling {}...".format(app), end='') +result = subprocess.run(['xcrun', 'stapler', 'staple', app]) result.check_returncode() + +print(" success.\n") diff --git a/contrib/macos/sign.sh.in b/contrib/macos/sign.sh.in index 6ebf0859a..a949f1ec5 100755 --- a/contrib/macos/sign.sh.in +++ b/contrib/macos/sign.sh.in @@ -1,10 +1,25 @@ #!/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" + +if [ -z "@CODESIGN" ]; then + echo "Cannot codesign: this build was not configured with codesigning" >&2 + exit 1 +fi + +for ext in systemextension appex; do + netext="@lokinet_ext_dir@/org.lokinet.network-extension.$ext" + if [ -e "@SIGN_TARGET@/$netext" ]; then + echo -e "\n\e[33;1mSigning $netext...\e[0m\n" >&2 + codesign --verbose=4 --force -s "@CODESIGN_ID@" \ + --entitlements "@PROJECT_SOURCE_DIR@/contrib/macos/lokinet-extension.@LOKINET_ENTITLEMENTS_TYPE@.entitlements.plist" \ + --deep --strict --timestamp --options=runtime "@SIGN_TARGET@/$netext" + fi +done + +for sub in "/Contents/MacOS/Lokinet" "" ; do + echo -e "\n\e[33;1mSigning $(basename @SIGN_TARGET@)$sub...\e[0m\n" >&2 + codesign --verbose=4 --force -s "@CODESIGN_ID@" \ + --entitlements "@PROJECT_SOURCE_DIR@/contrib/macos/lokinet.@LOKINET_ENTITLEMENTS_TYPE@.entitlements.plist" \ + --deep --strict --timestamp --options=runtime "@SIGN_TARGET@$sub" done diff --git a/daemon/CMakeLists.txt b/daemon/CMakeLists.txt index 50999b080..93e93162a 100644 --- a/daemon/CMakeLists.txt +++ b/daemon/CMakeLists.txt @@ -1,14 +1,18 @@ -add_executable(lokinet-vpn lokinet-vpn.cpp) +set(exetargets lokinet) + if(APPLE) add_executable(lokinet lokinet.swift) - enable_lto(lokinet) else() add_executable(lokinet lokinet.cpp) - enable_lto(lokinet lokinet-vpn) endif() +add_executable(lokinet-vpn lokinet-vpn.cpp) +enable_lto(lokinet lokinet-vpn) +list(APPEND exetargets lokinet-vpn) + if(WITH_BOOTSTRAP) add_executable(lokinet-bootstrap lokinet-bootstrap.cpp) + list(APPEND exetargets lokinet-bootstrap) enable_lto(lokinet-bootstrap) endif() @@ -42,11 +46,6 @@ if(WITH_BOOTSTRAP) endif() endif() -set(exetargets lokinet lokinet-vpn) -if(WITH_BOOTSTRAP) - list(APPEND exetargets lokinet-bootstrap) -endif() - foreach(exe ${exetargets}) if(WIN32 AND NOT MSVC_VERSION) target_sources(${exe} PRIVATE ${CMAKE_BINARY_DIR}/${exe}.rc) @@ -57,10 +56,13 @@ foreach(exe ${exetargets}) endif() target_link_libraries(${exe} PUBLIC liblokinet) target_include_directories(${exe} PUBLIC "${PROJECT_SOURCE_DIR}") - target_compile_definitions(${exe} PRIVATE -DVERSIONTAG=${GIT_VERSION_REAL}) + add_log_tag(${exe}) if(should_install) if(APPLE) - install(TARGETS ${exe} BUNDLE DESTINATION "${PROJECT_BINARY_DIR}" COMPONENT lokinet) + install(TARGETS ${exe} + BUNDLE DESTINATION "${PROJECT_BINARY_DIR}" + RUNTIME DESTINATION "." + COMPONENT lokinet) else() install(TARGETS ${exe} RUNTIME DESTINATION bin COMPONENT lokinet) endif() @@ -68,10 +70,77 @@ foreach(exe ${exetargets}) endforeach() if(APPLE) - - set(CODESIGN_APP "" CACHE STRING "codesign the macos app using this key identity") - set(CODESIGN_EXT "${CODESIGN_APP}" CACHE STRING "codesign the internal extension using this key identity; defaults to CODESIGN_APP if empty") + option(MACOS_SYSTEM_EXTENSION + "Build the network extension as a system extension rather than a plugin. This must be ON for non-app store release builds, and must be OFF for dev builds and Mac App Store distribution builds" + OFF) option(CODESIGN "codesign the resulting app and extension" ON) + set(CODESIGN_ID "" CACHE STRING "codesign the macos app using this key identity; if empty we'll try to guess") + set(default_profile_type "dev") + if(MACOS_SYSTEM_EXTENSION) + set(default_profile_type "release") + endif() + set(CODESIGN_PROFILE "${PROJECT_SOURCE_DIR}/contrib/macos/lokinet.${default_profile_type}.provisionprofile" CACHE FILEPATH + "Path to a .provisionprofile to use for the main app") + set(CODESIGN_EXT_PROFILE "${PROJECT_SOURCE_DIR}/contrib/macos/lokinet-extension.${default_profile_type}.provisionprofile" CACHE FILEPATH + "Path to a .provisionprofile to use for the extension") + + if(CODESIGN AND NOT CODESIGN_ID) + if(MACOS_SYSTEM_EXTENSION) + set(codesign_cert_pattern "Developer ID Application") + else() + set(codesign_cert_pattern "Apple Development") + endif() + execute_process( + COMMAND security find-identity -v -p codesigning + COMMAND sed -n "s/^ *[0-9][0-9]*) *\\([A-F0-9]\\{40\\}\\) *\"\\(${codesign_cert_pattern}.*\\)\"\$/\\1 \\2/p" + RESULT_VARIABLE find_id_exit_code + OUTPUT_VARIABLE find_id_output) + if(NOT find_id_exit_code EQUAL 0) + message(FATAL_ERROR "Finding signing identities with security find-identity failed; try specifying an id using -DCODESIGN_ID=...") + endif() + + string(REGEX MATCHALL "(^|\n)[0-9A-F]+" find_id_sign_id "${find_id_output}") + if(NOT find_id_sign_id) + message(FATAL_ERROR "Did not find any \"${codesign_cert_pattern}\" identity; try specifying an id using -DCODESIGN_ID=...") + endif() + if (find_id_sign_id MATCHES ";") + message(FATAL_ERROR "Found multiple \"${codesign_cert_pattern}\" identities:\n${find_id_output}\nSpecify an identify using -DCODESIGN_ID=...") + endif() + set(CODESIGN_ID "${find_id_sign_id}" CACHE STRING "" FORCE) + endif() + + if(CODESIGN) + message(STATUS "Codesigning using ${CODESIGN_ID}") + else() + message(WARNING "Codesigning disabled; the resulting build will not run on most macOS systems") + endif() + + if(MACOS_SYSTEM_EXTENSION) + set(lokinet_ext_dir Contents/Library/SystemExtensions) + target_compile_definitions(lokinet PRIVATE MACOS_SYSTEM_EXTENSION) + if (NOT MACOS_NOTARIZE_USER AND NOT MACOS_NOTARIZE_PASS AND NOT MACOS_NOTARIZE_ASC AND EXISTS "$ENV{HOME}/.notarization.cmake") + message(STATUS "Loading notarization info from ~/.notarization.cmake") + include("$ENV{HOME}/.notarization.cmake") + endif() + if (MACOS_NOTARIZE_USER AND MACOS_NOTARIZE_PASS AND MACOS_NOTARIZE_ASC) + message(STATUS "Enabling notarization with account ${MACOS_NOTARIZE_ASC}/${MACOS_NOTARIZE_USER}") + else() + message(WARNING "You have not set one or more of MACOS_NOTARIZE_USER, MACOS_NOTARIZE_PASS, MACOS_NOTARIZE_ASC: notarization will fail; see contrib/macos/README.txt") + endif() + else() + set(lokinet_ext_dir Contents/PlugIns) + endif() + + foreach(var CODESIGN_PROFILE CODESIGN_EXT_PROFILE) + if(NOT ${var}) + message(WARNING "Missing a ${var} provisioning profile, and not building a system extension: Apple will most likely log an uninformative error message to the system log and then kill harmless kittens if you try to run the result") + endif() + if(NOT EXISTS "${${var}}") + message(FATAL_ERROR "Provisioning profile ${${var}} does not exist; fix your -D${var} path") + endif() + endforeach() + message(STATUS "Using ${CODESIGN_PROFILE} provisioning profile") + message(STATUS "Using ${CODESIGN_EXT_PROFILE} extension provisioning profile") set(mac_icon ${CMAKE_CURRENT_BINARY_DIR}/lokinet.icns) add_custom_command(OUTPUT ${mac_icon} @@ -79,37 +148,50 @@ if(APPLE) 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) + set(post_build_pp) + if(CODESIGN AND CODESIGN_PROFILE) + set(post_build_pp COMMAND ${CMAKE_COMMAND} -E copy_if_different ${CODESIGN_PROFILE} + $/Contents/embedded.provisionprofile) + endif() + add_custom_command(TARGET lokinet POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different ${PROJECT_SOURCE_DIR}/contrib/bootstrap/mainnet.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 - ) + COMMAND mkdir -p $/${lokinet_ext_dir} + COMMAND cp -a $ $/${lokinet_ext_dir} + ${post_build_pp} + ) set_target_properties(lokinet PROPERTIES + OUTPUT_NAME Lokinet 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_GUI_IDENTIFIER "org.lokinet" + MACOSX_BUNDLE_INFO_PLIST "${PROJECT_SOURCE_DIR}/contrib/macos/lokinet.Info.plist.in" MACOSX_BUNDLE_ICON_FILE "${mac_icon}" - MACOSX_BUNDLE_COPYRIGHT "© 2021, The Oxen Project") + MACOSX_BUNDLE_COPYRIGHT "© 2022, The Oxen Project" + RUNTIME_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}" + ) + if(NOT CODESIGN) message(STATUS "codesigning disabled") add_custom_target( sign DEPENDS lokinet lokinet-extension COMMAND "true") - elseif (CODESIGN_APP AND CODESIGN_EXT) - message(STATUS "codesigning with ${CODESIGN_APP} (app) ${CODESIGN_EXT} (appex)") - set(SIGN_TARGET "${CMAKE_CURRENT_BINARY_DIR}/lokinet.app") + elseif(CODESIGN) + set(SIGN_TARGET "${PROJECT_BINARY_DIR}/lokinet.app") + if(MACOS_SYSTEM_EXTENSION) + set(LOKINET_ENTITLEMENTS_TYPE sysext) + else() + set(LOKINET_ENTITLEMENTS_TYPE plugin) + endif() configure_file( "${PROJECT_SOURCE_DIR}/contrib/macos/sign.sh.in" "${PROJECT_BINARY_DIR}/sign.sh" @@ -119,6 +201,25 @@ if(APPLE) DEPENDS "${PROJECT_BINARY_DIR}/sign.sh" lokinet lokinet-extension COMMAND "${PROJECT_BINARY_DIR}/sign.sh" ) + + if(NOT (MACOS_NOTARIZE_USER AND MACOS_NOTARIZE_PASS AND MACOS_NOTARIZE_ASC)) + message(WARNING "You have not set one or more of MACOS_NOTARIZE_USER, MACOS_NOTARIZE_PASS, MACOS_NOTARIZE_ASC: notarization disabled") + endif() + if (MACOS_SYSTEM_EXTENSION) + set(notarize_py_is_sysext True) + else() + set(notarize_py_is_sysext False) + endif() + configure_file( + "${PROJECT_SOURCE_DIR}/contrib/macos/notarize.py.in" + "${PROJECT_BINARY_DIR}/notarize.py" + @ONLY) + add_custom_target( + notarize + DEPENDS "${PROJECT_BINARY_DIR}/notarize.py" sign + COMMAND "${PROJECT_BINARY_DIR}/notarize.py" + ) + else() message(FATAL_ERROR "CODESIGN_APP (=${CODESIGN_APP}) and/or CODESIGN_EXT (=${CODESIGN_EXT}) are not set. To disable code signing use -DCODESIGN=OFF") endif() diff --git a/daemon/lokinet.swift b/daemon/lokinet.swift index 86277cb88..1d8e0a106 100644 --- a/daemon/lokinet.swift +++ b/daemon/lokinet.swift @@ -1,6 +1,7 @@ import AppKit import Foundation import NetworkExtension +import SystemExtensions let app = NSApplication.shared @@ -11,14 +12,13 @@ let HELP_STRING = "usage: lokinet [--start|--stop]" class LokinetMain: NSObject, NSApplicationDelegate { var vpnManager = NETunnelProviderManager() - let lokinetComponent = "com.loki-project.lokinet.network-extension" - var mode = "" + var mode = START + let netextBundleId = "org.lokinet.network-extension" func applicationDidFinishLaunching(_: Notification) { if self.mode == START { - setupVPNTunnel() - } - else if self.mode == STOP { + startNetworkExtension() + } else if self.mode == STOP { tearDownVPNTunnel() } else { self.result(msg: HELP_STRING) @@ -46,7 +46,7 @@ class LokinetMain: NSObject, NSApplicationDelegate { if let savedManagers = savedManagers { for manager in savedManagers { - if (manager.protocolConfiguration as? NETunnelProviderProtocol)?.providerBundleIdentifier == self.lokinetComponent { + if (manager.protocolConfiguration as? NETunnelProviderProtocol)?.providerBundleIdentifier == self.netextBundleId { manager.isEnabled = false self.result(msg: "Lokinet Down") return @@ -57,8 +57,21 @@ class LokinetMain: NSObject, NSApplicationDelegate { } } + func startNetworkExtension() { +#if MACOS_SYSTEM_EXTENSION + NSLog("Loading Lokinet network extension") + // Start by activating the system extension + let activationRequest = OSSystemExtensionRequest.activationRequest(forExtensionWithIdentifier: netextBundleId, queue: .main) + activationRequest.delegate = self + OSSystemExtensionManager.shared.submitRequest(activationRequest) +#else + setupVPNTunnel() +#endif + } + func setupVPNTunnel() { - NSLog("Starting up Lokinet") + + NSLog("Starting up Lokinet tunnel") NETunnelProviderManager.loadAllFromPreferences { [self] (savedManagers: [NETunnelProviderManager]?, error: Error?) in if let error = error { self.result(msg: error.localizedDescription) @@ -67,7 +80,7 @@ class LokinetMain: NSObject, NSApplicationDelegate { if let savedManagers = savedManagers { for manager in savedManagers { - if (manager.protocolConfiguration as? NETunnelProviderProtocol)?.providerBundleIdentifier == self.lokinetComponent { + if (manager.protocolConfiguration as? NETunnelProviderProtocol)?.providerBundleIdentifier == self.netextBundleId { NSLog("Found saved VPN Manager") self.vpnManager = manager } @@ -76,7 +89,7 @@ class LokinetMain: NSObject, NSApplicationDelegate { 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.providerBundleIdentifier = self.netextBundleId 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. @@ -130,11 +143,42 @@ class LokinetMain: NSObject, NSApplicationDelegate { } } +#if MACOS_SYSTEM_EXTENSION + +extension LokinetMain: OSSystemExtensionRequestDelegate { + + func request(_ request: OSSystemExtensionRequest, didFinishWithResult result: OSSystemExtensionRequest.Result) { + guard result == .completed else { + NSLog("Unexpected result %d for system extension request", result.rawValue) + return + } + NSLog("Lokinet system extension loaded") + setupVPNTunnel() + } + + func request(_ request: OSSystemExtensionRequest, didFailWithError error: Error) { + NSLog("System extension request failed: %@", error.localizedDescription) + } + + func requestNeedsUserApproval(_ request: OSSystemExtensionRequest) { + NSLog("Extension %@ requires user approval", request.identifier) + } + + func request(_ request: OSSystemExtensionRequest, + actionForReplacingExtension existing: OSSystemExtensionProperties, + withExtension extension: OSSystemExtensionProperties) -> OSSystemExtensionRequest.ReplacementAction { + NSLog("Replacing extension %@ version %@ with version %@", request.identifier, existing.bundleShortVersion, `extension`.bundleShortVersion) + return .replace + } +} + +#endif + let args = CommandLine.arguments -if args.count > 1 { +if args.count <= 2 { let delegate = LokinetMain() - delegate.mode = args[1] + delegate.mode = args.count > 1 ? args[1] : START app.delegate = delegate app.run() } else { diff --git a/docs/macos-signing.txt b/docs/macos-signing.txt index 9e6f2ba07..9e323da8e 100644 --- a/docs/macos-signing.txt +++ b/docs/macos-signing.txt @@ -1,73 +1,123 @@ -Codesigning and notarization on macOS +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. -This is painful. Thankfully most of the pain is now in CMake and a python script. +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. -To build, codesign, and notarized and installer package, CMake needs to be invoked with: +This is disgusting. - cd build - rm -rf * # optional but recommended - cmake .. -DBUILD_PACKAGE=ON -DDOWNLOAD_SODIUM=ON -DMACOS_SIGN_APP=ABC123... -DMACOS_SIGN_PKG=DEF456... +But it gets worse. -where the ABC123... key is a "Developer ID Installer" key and PKG key is a "Developer ID -Application" key. You have to go through a bunch of pain, pay Apple money, and then read a bunch of -poorly written documentation that doesn't help very much to create these and get them working. But once you have them -set up in Keychain, you should be able to list your keys with: +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). - security find-identity -v + lokinet.dev.provisionprofile + lokinet-extension.dev.provisionprofile -and you should see (at least) one "Developer ID Installer: ..." and one "Developer ID Application: -...". You need both for reasons that only Apple knows. The former is used to sign the installer -.pkg, and the latter is used to sign everything *inside* the .pkg, and you can't use the same key -for both because Apple designed code signing by marketing committee rather than ask any actual -competent software developers how code signing should work. +This is actively hostile to open source development, but that is nothing new for Apple. -Either way, these two values can be specified either by hex value or description string that -`security find-identity -v` spits out. +There are also release provisioning profiles -You also need to set up the notarization parameters; these can either be specified directly on the -cmake command line by adding: + lokinet.release.provisionprofile + lokinet-extension.release.provisionprofile - -DMACOS_NOTARIZE_ASC=XYZ123 -DMACOS_NOTARIZE_USER=me@example.com -DMACOS_NOTARIZE_PASS=@keychain:codesigning-password +These ones allow distribution of the app, but only if notarized, and again require notarization plus +signing by a (paid) Apple developer account. -or, more simply, by putting them inside a `~/.notarization.cmake` file that will be included if it -exists (and the MACOS_SIGN_* variables are set) -- see below. +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 dev +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). -These three values here are: +For release builds, you still need a provisioning profile, but it must be a "Distribution: Developer +ID" provisioning profile, and are tied to a (paid) Developer ID. The ones in the repository are +attached to the Oxen Project Developer ID and are useless to anyone else. -MACOS_NOTARIZE_ASC: +Once you have that in place, you need to build and sign the package using a certificate matching +your provisioning profile before your Apple system will allow it to run. (That's right, your $2000 +box won't let you run programs you build from source on it unless you also subscribe to a $100/year +Apple developer account). -Organization-specific unique value; this is printed inside (brackets) when you run: `security -find-identity -v`: +Okay, so now that you have paid Apple more money for the privilege of using your own computer, +here's how you make a signed lokinet app: - 1) 1C75DDBF884DEF3D5927C3F29BB7FC5ADAE2E1B3 "Apple Development: me@example.com (ABC123XYZ9)" +1) Decide which type of build you are doing: a lokinet system extension, or an app extension. The + former must be signed and notarized and will only work when placed in the /Applications folder, + but will not work as a dev build and cannot be distributed outside the Mac App Store. The latter + is usable as a dev build, but still requires a signature and Apple-provided provisioningprofile + listing the limited number of devices on which it is allowed to run. -MACOS_NOTARIZE_USER: + For system extension builds you want to add the -DMACOS_SYSTEM_EXTENSION=ON flag to cmake. -Your Apple Developer login. +2) Figure out the certificate to use for signing and make sure you have it installed. For a + distributable system extension build you need a "Developer ID Application" key and certificate, + issued by your paid developer.apple.com account. For dev builds you need a "Apple Development" + certificate. -MACOS_NOTARIZE_PASS: + In most cases you don't need to specify these; the default cmake script will figure them out. + (If it can't, e.g. because you have multiple of the right type installed, it will error with the + keys it found). -This should be an app-specific password created for signing on the Apple Developer website. You -*can* specify it directly, but it is much better to use the magic `@keychain:blah` value, where -'blah' is a password name recorded in Keychain. To get that in place you run: + To be explicit, use `security find-identity -v` to list your keys, then list the key identity + with -DCODESIGN_ID=..... - export HISTFILE='' # for bash: you don't want to store this in your history - xcrun altool --store-password-in-keychain-item "NOTARIZE_PASSWORD" -u "user" -p "password" +3) If you are doing a system extension build you will need to provide notarization login information by adding: -where NOTARIZE_PASSWORD is just some name for the password (I called it 'blah' or -'codesigning-password' above), and the "user" and "password" are replaced with your actual Apple -Developer account device-specific login credentials. + -DMACOS_NOTARIZE_ASC=XYZ123 -DMACOS_NOTARIZE_USER=me@example.com -DMACOS_NOTARIZE_PASS=@keychain:codesigning-password -Optionally, put these last three inside a `~/.notarization.cmake` file: + a) The first value (XYZ123) needs to be the organization-specific unique value, and is printed in + brackets in the certificate description. For example: - set(MACOS_NOTARIZE_USER "jagerman@jagerman.com") - set(MACOS_NOTARIZE_PASS "@keychain:codesigning-password") - set(MACOS_NOTARIZE_ASC "SUQ8J2PCT7") + 15095CD1E6AF441ABC69BDC52EE186A18200A49F "Developer ID Application: Some Developer (ABC123XYZ9)" -Then, finally, you can build the package from the build directory with: + would require ABC123XYZ9 for this field. - make package -j4 # or whatever -j makes you happy - make notarize + b) The USER field is your Apple Developer login e-mail address. -The former builds and signs the package, the latter submits it for notarization. This can take a -few minutes; the script polls Apple's server until it is finished passing or failing notarization. + c) The PASS field is a keychain reference holding your "Application-Specific Password". To set + up such a password for your account, consult Apple documentation. Once you have it, load it + into your keychain via: + + export HISTFILE='' # Don't want to store this in the shell history + xcrun altool --store-password-in-keychain-item "codesigning-password" -u "user" -p "password" + + You can change "codesigning-password" to whatever you want (just make sure it agrees with the + -DMACOS_NOTARIZE_PASS option you build with). "user" and "password" should be your developer + account device-specific login credentials provided by Apple. + + To make your life easier, stash these settings into a `~/.notarization.cmake` file inside your + home directory; if you have not specified them in the build, and this file exists, lokinet's + cmake will load it: + + set(MACOS_NOTARIZE_USER "me@example.com") + set(MACOS_NOTARIZE_PASS "@keychain:codesigning-password") + set(MACOS_NOTARIZE_ASC "ABC123XYZ9") + +4) Build and sign the package; there is a script `contrib/mac.sh` that can help (extra cmake options + you need can be appended to the end), or you can build yourself in a build directory. See the + script for the other cmake options that are typically needed. Note that `-G Ninja` (as well as a + working ninja builder) are required. + + If you get an error `errSecInternalComponent` this is Apple's highly descriptive way of telling + you that you need to unlock your keychain, which you can do by running `security unlock`. + + If doing it yourself, `ninja sign` will build and then sign the app. + + If you need to also notarize (e.g. for a system extension build) run `./notarize.py` from the + build directory (or alternatively `ninja notarize`, but the former gives you status output while + it runs). + +5) Packaging the app: you want to use `-DBUILD_PACKAGE=ON` when configuring with cmake and then, + once all signing and notarization is complete, run `cpack` which will give you a .dmg and a .zip + containing the release. diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt index 0ea284a78..6514f72a1 100644 --- a/external/CMakeLists.txt +++ b/external/CMakeLists.txt @@ -41,7 +41,7 @@ endif() macro(system_or_submodule BIGNAME smallname pkgconf subdir) option(FORCE_${BIGNAME}_SUBMODULE "force using ${smallname} submodule" OFF) - if(NOT STATIC AND NOT FORCE_${BIGNAME}_SUBMODULE) + if(NOT BUILD_STATIC_DEPS AND NOT FORCE_${BIGNAME}_SUBMODULE AND NOT FORCE_ALL_SUBMODULES) pkg_check_modules(${BIGNAME} ${pkgconf} IMPORTED_TARGET) endif() if(${BIGNAME}_FOUND) @@ -64,6 +64,7 @@ endmacro() system_or_submodule(OXENC oxenc liboxenc>=1.0.3 oxen-encoding) system_or_submodule(OXENMQ oxenmq liboxenmq>=1.2.12 oxen-mq) set(JSON_BuildTests OFF CACHE INTERNAL "") +set(JSON_Install OFF CACHE INTERNAL "") system_or_submodule(NLOHMANN nlohmann_json nlohmann_json>=3.7.0 nlohmann) if (STATIC OR FORCE_SPDLOG_SUBMODULE OR FORCE_FMT_SUBMODULE) diff --git a/llarp/CMakeLists.txt b/llarp/CMakeLists.txt index 0782add06..c6dd5e6f6 100644 --- a/llarp/CMakeLists.txt +++ b/llarp/CMakeLists.txt @@ -252,7 +252,7 @@ if(BUILD_LIBLOKINET) if(WIN32) target_link_libraries(lokinet-shared PUBLIC ws2_32 iphlpapi -fstack-protector) install(TARGETS lokinet-shared DESTINATION bin COMPONENT liblokinet) - else() + elseif(NOT APPLE) install(TARGETS lokinet-shared LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} COMPONENT liblokinet) endif() endif() diff --git a/llarp/apple/CMakeLists.txt b/llarp/apple/CMakeLists.txt index ad18d9920..30b984653 100644 --- a/llarp/apple/CMakeLists.txt +++ b/llarp/apple/CMakeLists.txt @@ -25,26 +25,37 @@ target_link_libraries(lokinet-extension PRIVATE ${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_compile_options(lokinet-extension PRIVATE -fobjc-arc) +if(MACOS_SYSTEM_EXTENSION) + target_compile_definitions(lokinet-extension PRIVATE MACOS_SYSTEM_EXTENSION) + target_compile_definitions(lokinet-util PUBLIC MACOS_SYSTEM_EXTENSION) +else() + target_link_options(lokinet-extension PRIVATE -e _NSExtensionMain) +endif() -target_link_libraries(lokinet-extension PUBLIC - liblokinet - ${COREFOUNDATION} - ${NETEXT}) +if(MACOS_SYSTEM_EXTENSION) + set(bundle_ext systemextension) + set(product_type com.apple.product-type.system-extension) +else() + set(bundle_ext appex) + set(product_type com.apple.product-type.app-extension) +endif() 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 + BUNDLE_EXTENSION ${bundle_ext} + OUTPUT_NAME org.lokinet.network-extension + MACOSX_BUNDLE_INFO_PLIST ${PROJECT_SOURCE_DIR}/contrib/macos/lokinet-extension.Info.plist.in + XCODE_PRODUCT_TYPE ${product_type} ) -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 +if(CODESIGN AND CODESIGN_EXT_PROFILE) + add_custom_command(TARGET lokinet-extension + POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + ${CODESIGN_EXT_PROFILE} + $/Contents/embedded.provisionprofile ) +endif() diff --git a/llarp/apple/DNSTrampoline.h b/llarp/apple/DNSTrampoline.h index 4935d43c8..117d567b2 100644 --- a/llarp/apple/DNSTrampoline.h +++ b/llarp/apple/DNSTrampoline.h @@ -5,18 +5,19 @@ 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. + * "Trampoline" class that listens for UDP DNS packets when we have exit mode enabled. These arrive + * on localhost: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, but + * using Apple magic reinvented wheel wrappers that are oh so wonderful like everything Apple. * * 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). + * - DNS requests go unbound either to 127.0.0.1:53 directly (system extension) or bounced through + * TUNNELIP:53 (app extension), 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. + * - DNS requests go to unbound, as above, and unbound 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 to be delivered back to the requestor. * (This assumes a non-lokinet DNS; .loki and .snode get handled before either of these). */ @interface LLARPDNSTrampoline : NSObject @@ -40,6 +41,7 @@ extern NSString* error_domain; uv_async_t write_trigger; } - (void)startWithUpstreamDns:(NWUDPSession*)dns + listenIp:(NSString*)listenIp listenPort:(uint16_t)listenPort uvLoop:(uv_loop_t*)loop completionHandler:(void (^)(NSError* error))completionHandler; diff --git a/llarp/apple/DNSTrampoline.m b/llarp/apple/DNSTrampoline.m index 0a78a13e2..cbbe211a3 100644 --- a/llarp/apple/DNSTrampoline.m +++ b/llarp/apple/DNSTrampoline.m @@ -1,7 +1,7 @@ #include "DNSTrampoline.h" #include -NSString* error_domain = @"com.loki-project.lokinet"; +NSString* error_domain = @"org.lokinet"; // Receiving an incoming packet, presumably from libunbound. NB: this is called from the libuv @@ -68,10 +68,12 @@ static void alloc_buffer(uv_handle_t* handle, size_t suggested_size, uv_buf_t* b @implementation LLARPDNSTrampoline - (void)startWithUpstreamDns:(NWUDPSession*) dns + listenIp:(NSString*) listenIp listenPort:(uint16_t) listenPort uvLoop:(uv_loop_t*) loop completionHandler:(void (^)(NSError* error))completionHandler { + NSLog(@"Setting up trampoline"); pending_writes = [[NSMutableArray alloc] init]; write_trigger.data = (__bridge void*) self; uv_async_init(loop, &write_trigger, write_flusher); @@ -79,7 +81,7 @@ static void alloc_buffer(uv_handle_t* handle, size_t suggested_size, uv_buf_t* b 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); + uv_ip4_addr(listenIp.UTF8String, 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)]; diff --git a/llarp/apple/PacketTunnelProvider.m b/llarp/apple/PacketTunnelProvider.m index b340e56cb..0dac9953f 100644 --- a/llarp/apple/PacketTunnelProvider.m +++ b/llarp/apple/PacketTunnelProvider.m @@ -3,9 +3,12 @@ #include "context_wrapper.h" #include "DNSTrampoline.h" +#define LLARP_APPLE_PACKET_BUF_SIZE 64 + @interface LLARPPacketTunnel : NEPacketTunnelProvider { void* lokinet; + llarp_incoming_packet packet_buf[LLARP_APPLE_PACKET_BUF_SIZE]; @public NEPacketTunnelNetworkSettings* settings; @public NEIPv4Route* tun_route4; @public NEIPv6Route* tun_route6; @@ -35,8 +38,8 @@ static void packet_writer(int af, const void* data, size_t size, void* ctx) { 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]]; + [t.packetFlow writePackets:@[buf] + withProtocols:@[[NSNumber numberWithInt:af]]]; } static void start_packet_reader(void* ctx) { @@ -48,6 +51,7 @@ static void start_packet_reader(void* ctx) { } static void add_ipv4_route(const char* addr, const char* netmask, void* ctx) { + NSLog(@"Adding IPv4 route %s:%s to packet tunnel", addr, netmask); NEIPv4Route* route = [[NEIPv4Route alloc] initWithDestinationAddress: [NSString stringWithUTF8String:addr] subnetMask: [NSString stringWithUTF8String:netmask]]; @@ -65,6 +69,7 @@ static void add_ipv4_route(const char* addr, const char* netmask, void* ctx) { } static void del_ipv4_route(const char* addr, const char* netmask, void* ctx) { + NSLog(@"Removing IPv4 route %s:%s to packet tunnel", addr, netmask); NEIPv4Route* route = [[NEIPv4Route alloc] initWithDestinationAddress: [NSString stringWithUTF8String:addr] subnetMask: [NSString stringWithUTF8String:netmask]]; @@ -124,6 +129,7 @@ static void del_ipv6_route(const char* addr, int prefix, void* ctx) { } static void add_default_route(void* ctx) { + NSLog(@"Making the tunnel the default route"); LLARPPacketTunnel* t = (__bridge LLARPPacketTunnel*) ctx; t->settings.IPv4Settings.includedRoutes = @[NEIPv4Route.defaultRoute]; @@ -133,6 +139,7 @@ static void add_default_route(void* ctx) { } static void del_default_route(void* ctx) { + NSLog(@"Removing default route from tunnel"); LLARPPacketTunnel* t = (__bridge LLARPPacketTunnel*) ctx; t->settings.IPv4Settings.includedRoutes = @[t->tun_route4]; @@ -148,9 +155,21 @@ static void del_default_route(void* ctx) { [self.packetFlow readPacketObjectsWithCompletionHandler: ^(NSArray* packets) { if (lokinet == nil) return; + + size_t size = 0; for (NEPacket* p in packets) { - llarp_apple_incoming(lokinet, p.data.bytes, p.data.length); + packet_buf[size].bytes = p.data.bytes; + packet_buf[size].size = p.data.length; + size++; + if (size >= LLARP_APPLE_PACKET_BUF_SIZE) + { + llarp_apple_incoming(lokinet, packet_buf, size); + size = 0; + } } + if (size > 0) + llarp_apple_incoming(lokinet, packet_buf, size); + [self readPackets]; }]; } @@ -190,7 +209,10 @@ static void del_default_route(void* ctx) { // 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]]; + NSString* dns_ip = [NSString stringWithUTF8String:conf.dns_bind_ip]; + + NSLog(@"setting dns to %@", dns_ip); + NEDNSSettings* dns = [[NEDNSSettings alloc] initWithServers:@[dns_ip]]; dns.domainName = @"localhost.loki"; dns.matchDomains = @[@""]; // In theory, matchDomains is supposed to be set to DNS suffixes that we resolve. This seems @@ -246,11 +268,13 @@ static void del_default_route(void* ctx) { return completionHandler(start_failure); } - NSLog(@"Starting DNS exit mode trampoline to %@ on 127.0.0.1:%d", upstreamdns_ep, dns_trampoline_port); + NSString* dns_tramp_ip = @"127.0.0.1"; + NSLog(@"Starting DNS exit mode trampoline to %@ on %@:%d", upstreamdns_ep, dns_tramp_ip, dns_trampoline_port); NWUDPSession* upstreamdns = [strongSelf createUDPSessionThroughTunnelToEndpoint:upstreamdns_ep fromEndpoint:nil]; strongSelf->dns_tramp = [LLARPDNSTrampoline alloc]; [strongSelf->dns_tramp startWithUpstreamDns:upstreamdns + listenIp:dns_tramp_ip listenPort:dns_trampoline_port uvLoop:llarp_apple_get_uv_loop(strongSelf->lokinet) completionHandler:^(NSError* error) { @@ -258,7 +282,7 @@ static void del_default_route(void* ctx) { NSLog(@"Error starting dns trampoline: %@", error); return completionHandler(error); }]; - }]; + }]; } - (void)stopTunnelWithReason:(NEProviderStopReason)reason @@ -308,3 +332,12 @@ static void del_default_route(void* ctx) { } @end + +#ifdef MACOS_SYSTEM_EXTENSION + +int main() { + [NEProvider startSystemExtensionMode]; + dispatch_main(); +} + +#endif diff --git a/llarp/apple/context_wrapper.cpp b/llarp/apple/context_wrapper.cpp index f2a904251..4fc85c804 100644 --- a/llarp/apple/context_wrapper.cpp +++ b/llarp/apple/context_wrapper.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -14,10 +15,6 @@ 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; @@ -30,7 +27,8 @@ namespace } // namespace -const uint16_t dns_trampoline_port = 1053; +// Expose this with C linkage so that objective-c can use it +extern "C" const uint16_t dns_trampoline_port = llarp::apple::dns_trampoline_port; void* llarp_apple_init(llarp_apple_config* appleconf) @@ -89,10 +87,12 @@ llarp_apple_init(llarp_apple_config* appleconf) } } - // 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; +#ifdef MACOS_SYSTEM_EXTENSION + std::strncpy( + appleconf->dns_bind_ip, + config->dns.m_bind.hostString().c_str(), + sizeof(appleconf->dns_bind_ip)); +#endif // If no explicit bootstrap then set the system default one included with the app bundle if (config->bootstrap.files.empty()) @@ -170,20 +170,26 @@ llarp_apple_get_uv_loop(void* lokinet) } int -llarp_apple_incoming(void* lokinet, const void* bytes, size_t size) +llarp_apple_incoming(void* lokinet, const llarp_incoming_packet* packets, size_t size) { auto& inst = *static_cast(lokinet); auto iface = inst.iface.lock(); if (!iface) - return -2; + return -1; - llarp_buffer_t buf{static_cast(bytes), size}; - if (iface->OfferReadPacket(buf)) - return 0; + int count = 0; + for (size_t i = 0; i < size; i++) + { + llarp_buffer_t buf{static_cast(packets[i].bytes), packets[i].size}; + if (iface->OfferReadPacket(buf)) + count++; + else + llarp::LogError("invalid IP packet: ", llarp::buffer_printer(buf)); + } - llarp::LogError("invalid IP packet: ", llarp::buffer_printer(buf)); - return -1; + iface->MaybeWakeUpperLayers(); + return count; } void diff --git a/llarp/apple/context_wrapper.h b/llarp/apple/context_wrapper.h index 37c8a5c7b..1f09a46d3 100644 --- a/llarp/apple/context_wrapper.h +++ b/llarp/apple/context_wrapper.h @@ -82,6 +82,12 @@ extern "C" char upstream_dns[INET_ADDRSTRLEN]; uint16_t upstream_dns_port; +#ifdef MACOS_SYSTEM_EXTENSION + /// DNS bind IP; llarp_apple_init writes the lokinet config value here so that we know (in Apple + /// API code) what to set DNS to when lokinet gets turned on. Null terminated. + char dns_bind_ip[INET_ADDRSTRLEN]; +#endif + /// \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 @@ -135,12 +141,23 @@ extern "C" 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). + /// Struct of packet data; a C array of tests gets passed to llarp_apple_incoming + typedef struct llarp_incoming_packet + { + const void* bytes; + size_t size; + } llarp_incoming_packet; + + /// Called to deliver one or more incoming packets from the apple layer into lokinet. Takes a C + /// array of `llarp_incoming_packets` with pointers/sizes set to the individual new packets that + /// have arrived. + /// + /// Returns the number of valid packets on success (which can be less than the number of provided + /// packets, if some failed to parse), or -1 if there is no current active VPNInterface associated + /// with the lokinet instance (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); + llarp_apple_incoming(void* lokinet, const llarp_incoming_packet* packets, 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. diff --git a/llarp/config/config.cpp b/llarp/config/config.cpp index 90be0b1ee..7fc78746d 100644 --- a/llarp/config/config.cpp +++ b/llarp/config/config.cpp @@ -775,11 +775,7 @@ namespace llarp // Most non-linux platforms have loopback as 127.0.0.1/32, but linux uses 127.0.0.1/8 so that we // can bind to other 127.* IPs to avoid conflicting with something else that may be listening on // 127.0.0.1:53. -#ifdef __linux__ - constexpr Default DefaultDNSBind{"127.3.2.1:53"}; -#else - constexpr Default DefaultDNSBind{"127.0.0.1:53"}; -#endif + constexpr Default DefaultDNSBind{platform::is_linux ? "127.3.2.1:53" : "127.0.0.1:53"}; // Default, but if we get any upstream (including upstream=, i.e. empty string) we clear it constexpr Default DefaultUpstreamDNS{"9.9.9.10"}; diff --git a/llarp/constants/apple.hpp b/llarp/constants/apple.hpp new file mode 100644 index 000000000..46a94f9bc --- /dev/null +++ b/llarp/constants/apple.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include + +namespace llarp::apple +{ + /// Localhost port on macOS where we proxy DNS requests *through* the tunnel, because without + /// calling into special snowflake Apple network APIs an extension's network connections all go + /// around the tunnel, even when the tunnel is (supposedly) the default route. + inline constexpr std::uint16_t dns_trampoline_port = 1053; + + /// We query the above trampoline from unbound with this fixed source port (so that the trampoline + /// is simplified by not having to track different ports for different requests). + inline constexpr std::uint16_t dns_trampoline_source_port = 1054; +} // namespace llarp::apple diff --git a/llarp/dns/unbound_resolver.cpp b/llarp/dns/unbound_resolver.cpp index a65728285..cc1fd3d87 100644 --- a/llarp/dns/unbound_resolver.cpp +++ b/llarp/dns/unbound_resolver.cpp @@ -1,6 +1,8 @@ #include "unbound_resolver.hpp" #include "server.hpp" +#include +#include #include #include #include @@ -144,37 +146,51 @@ namespace llarp::dns bool UnboundResolver::AddUpstreamResolver(const SockAddr& upstreamResolver) { - std::stringstream ss; - auto hoststr = upstreamResolver.hostString(); - ss << hoststr; + const auto hoststr = upstreamResolver.hostString(); + std::string upstream = hoststr; - if (const auto port = upstreamResolver.getPort(); port != 53) - ss << "@" << port; + const auto port = upstreamResolver.getPort(); + if (port != 53) + { + upstream += '@'; + upstream += std::to_string(port); + } - const auto str = ss.str(); - if (ub_ctx_set_fwd(unboundContext, str.c_str()) != 0) + LogError("Adding upstream resolver ", upstream); + if (ub_ctx_set_fwd(unboundContext, upstream.c_str()) != 0) { 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") + if constexpr (platform::is_apple) { - // 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"); + // On Apple, when we turn on exit mode, we can't directly connect to upstream from here + // because, from within the network extension, macOS ignores setting the tunnel as the default + // route and would leak all DNS; instead we have to bounce things through the objective C + // trampoline code so that it can call into Apple's special snowflake API to set up a socket + // that has the magic Apple snowflake sauce added on top so that it actually routes through + // the tunnel instead of around it. + // + // This behaviour is all carefully and explicitly documented by Apple with plenty of examples + // and other exposition, of course, just like all of their wonderful new APIs to reinvent + // standard unix interfaces. + if (hoststr == "127.0.0.1" && port == apple::dns_trampoline_port) + { + // 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:", "127.0.0.1"); + + // 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:", + std::to_string(apple::dns_trampoline_source_port).c_str()); + } } -#endif return true; } diff --git a/llarp/handlers/tun.cpp b/llarp/handlers/tun.cpp index e14e37c06..776b2edb1 100644 --- a/llarp/handlers/tun.cpp +++ b/llarp/handlers/tun.cpp @@ -72,9 +72,10 @@ namespace llarp #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. + // upstream DNS won't be set in our resolvers, which is why the vanilla IsUpstreamResolver, + // above, 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 { @@ -88,7 +89,7 @@ namespace llarp { m_PacketRouter = std::make_unique( [this](net::IPPacket pkt) { HandleGotUserPacket(std::move(pkt)); }); -#if defined(ANDROID) || defined(__APPLE__) +#if defined(ANDROID) || (defined(__APPLE__) && !defined(MACOS_SYSTEM_EXTENSION)) 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);