macOS system extension support

Adds support for building Lokinet as a system extension, and fixes
various problems in the macos implementation found during development of
the system extension support.
pull/1942/head
Jason Rhinelander 2 years ago
parent 61d7ff3787
commit 09372994bb

@ -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()

@ -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()

@ -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)

@ -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()

@ -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 ""

@ -1,24 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDisplayName</key>
<string>Lokinet</string>
<key>CFBundleExecutable</key>
<string>MacOS/lokinet</string>
<key>CFBundleIdentifier</key>
<string>com.loki-project.lokinet</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>lokinet</string>
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleShortVersionString</key>
<string>@lokinet_VERSION@</string>
<key>CFBundleVersion</key>
<string>@lokinet_VERSION@.@LOKINET_APPLE_BUILD@</string>
</dict>
</plist>

@ -1,40 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDisplayName</key>
<string>Lokinet</string>
<key>CFBundleExecutable</key>
<string>lokinet-extension</string>
<key>CFBundleIdentifier</key>
<string>com.loki-project.lokinet.network-extension</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundlePackageType</key>
<string>XPC!</string>
<key>CFBundleName</key>
<string>lokinet</string>
<key>CFBundleVersion</key>
<string>@lokinet_VERSION@</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSMinimumSystemVersion</key>
<string>11.0</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.networkextension.packet-tunnel</string>
<key>NSExtensionPrincipalClass</key>
<string>LLARPPacketTunnel</string>
</dict>
</dict>
</plist>

@ -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.

@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDisplayName</key>
<string>Lokinet Network Extension</string>
<key>CFBundleExecutable</key>
<string>org.lokinet.network-extension</string>
<key>CFBundleIdentifier</key>
<string>org.lokinet.network-extension</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundlePackageType</key>
<string>SYSX</string>
<key>CFBundleName</key>
<string>org.lokinet.network-extension</string>
<key>CFBundleVersion</key>
<string>@lokinet_VERSION@.@LOKINET_APPLE_BUILD@</string>
<key>CFBundleShortVersionString</key>
<string>@lokinet_VERSION@</string>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>MacOSX</string>
</array>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSMinimumSystemVersion</key>
<string>10.15</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2022 The Oxen Project, licensed under GPLv3-or-later</string>
<key>NSSystemExtensionUsageDescription</key>
<string>Provides Lokinet Network connectivity.</string>
<key>NetworkExtension</key>
<dict>
<key>NEMachServiceName</key>
<string>SUQ8J2PCT7.org.lokinet.network-extension</string>
<key>NEProviderClasses</key>
<dict>
<key>com.apple.networkextension.packet-tunnel</key>
<string>LLARPPacketTunnel</string>
<key>com.apple.networkextension.dns-proxy</key>
<string>LLARPDNSProxy</string>
</dict>
</dict>
</dict>
</plist>

@ -3,11 +3,12 @@
<plist version="1.0">
<dict>
<key>com.apple.application-identifier</key>
<string>SUQ8J2PCT7.com.loki-project.lokinet.network-extension</string>
<string>SUQ8J2PCT7.org.lokinet.network-extension</string>
<key>com.apple.developer.networking.networkextension</key>
<array>
<string>packet-tunnel-provider-systemextension</string>
<string>packet-tunnel-provider</string>
<string>dns-proxy</string>
</array>
<key>com.apple.developer.team-identifier</key>
@ -16,9 +17,6 @@
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.get-task-allow</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>

@ -3,13 +3,12 @@
<plist version="1.0">
<dict>
<key>com.apple.application-identifier</key>
<string>SUQ8J2PCT7.com.loki-project.lokinet</string>
<string>SUQ8J2PCT7.org.lokinet.network-extension</string>
<key>com.apple.developer.networking.networkextension</key>
<array>
<string>packet-tunnel-provider-systemextension</string>
<string>dns-proxy-systemextension</string>
<string>dns-settings</string>
<string>dns-proxy-systemextension</string>
</array>
<key>com.apple.developer.team-identifier</key>
@ -18,13 +17,15 @@
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.get-task-allow</key>
<true/>
<key>com.apple.security.application-groups</key>
<array>
<string>SUQ8J2PCT7.org.lokinet</string>
</array>
<key>com.apple.security.network.client</key>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key>
<key>com.apple.security.network.server</key>
<true/>
</dict>

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDisplayName</key>
<string>Lokinet</string>
<key>CFBundleExecutable</key>
<string>Lokinet</string>
<key>CFBundleIdentifier</key>
<string>org.lokinet</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>Lokinet</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>@lokinet_VERSION@</string>
<key>CFBundleVersion</key>
<string>@lokinet_VERSION@.@LOKINET_APPLE_BUILD@</string>
<key>LSMinimumSystemVersion</key>
<string>10.15</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2022 The Oxen Project, licensed under GPLv3-or-later</string>
</dict>
</plist>

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.application-identifier</key>
<string>SUQ8J2PCT7.org.lokinet</string>
<key>com.apple.developer.networking.networkextension</key>
<array>
<string>packet-tunnel-provider</string>
<string>dns-proxy</string>
<string>dns-settings</string>
</array>
<key>com.apple.developer.team-identifier</key>
<string>SUQ8J2PCT7</string>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
</dict>
</plist>

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.application-identifier</key>
<string>SUQ8J2PCT7.org.lokinet</string>
<key>com.apple.developer.networking.networkextension</key>
<array>
<string>packet-tunnel-provider-systemextension</string>
<string>dns-proxy-systemextension</string>
<string>dns-settings</string>
</array>
<key>com.apple.developer.team-identifier</key>
<string>SUQ8J2PCT7</string>
<key>com.apple.developer.system-extension.install</key>
<true/>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.application-groups</key>
<array>
<string>SUQ8J2PCT7.org.lokinet</string>
</array>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
</dict>
</plist>

@ -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")

@ -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

@ -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}
$<TARGET_BUNDLE_DIR:lokinet>/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
$<TARGET_BUNDLE_DIR:lokinet-extension>/Contents/Resources/bootstrap.signed
COMMAND mkdir -p $<TARGET_BUNDLE_DIR:lokinet>/Contents/PlugIns
COMMAND cp -a $<TARGET_BUNDLE_DIR:lokinet-extension> $<TARGET_BUNDLE_DIR:lokinet>/Contents/PlugIns/
COMMAND ${CMAKE_COMMAND} -E copy_if_different ${PROJECT_SOURCE_DIR}/contrib/macos/lokinet.provisionprofile
$<TARGET_BUNDLE_DIR:lokinet>/Contents/embedded.provisionprofile
)
COMMAND mkdir -p $<TARGET_BUNDLE_DIR:lokinet>/${lokinet_ext_dir}
COMMAND cp -a $<TARGET_BUNDLE_DIR:lokinet-extension> $<TARGET_BUNDLE_DIR:lokinet>/${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()

@ -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 {

@ -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.

@ -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)

@ -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()

@ -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
$<TARGET_BUNDLE_DIR:lokinet-extension>/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}
$<TARGET_BUNDLE_DIR:lokinet-extension>/Contents/embedded.provisionprofile
)
endif()

@ -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;

@ -1,7 +1,7 @@
#include "DNSTrampoline.h"
#include <uv.h>
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<NSData*> 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)];

@ -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<NEPacket*>* 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

@ -3,6 +3,7 @@
#include <cassert>
#include <llarp/net/ip_packet.hpp>
#include <llarp/config/config.hpp>
#include <llarp/constants/apple.hpp>
#include <llarp/util/fs.hpp>
#include <uvw/loop.h>
#include <llarp/util/logging.hpp>
@ -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<instance_data*>(lokinet);
auto iface = inst.iface.lock();
if (!iface)
return -2;
return -1;
llarp_buffer_t buf{static_cast<const uint8_t*>(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<const uint8_t*>(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

@ -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.

@ -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"};

@ -0,0 +1,15 @@
#pragma once
#include <cstdint>
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

@ -1,6 +1,8 @@
#include "unbound_resolver.hpp"
#include "server.hpp"
#include <llarp/constants/apple.hpp>
#include <llarp/constants/platform.hpp>
#include <llarp/util/buffer.hpp>
#include <sstream>
#include <llarp/util/str.hpp>
@ -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;
}

@ -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<vpn::PacketRouter>(
[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<DnsInterceptor>(r, this);
m_PacketRouter->AddUDPHandler(huint16_t{53}, [&](net::IPPacket pkt) {
const size_t ip_header_size = (pkt.Header()->ihl * 4);

Loading…
Cancel
Save