diff --git a/contrib/format.sh b/contrib/format.sh index 5b5f5a495..785d9ba54 100755 --- a/contrib/format.sh +++ b/contrib/format.sh @@ -18,7 +18,7 @@ fi cd "$(dirname $0)/../" if [ "$1" = "verify" ] ; then - if [ $($binary --output-replacements-xml $(find jni daemon llarp include pybind | grep -E '\.([hc](pp)?|mm)$' | grep -v '\#') | grep '' | wc -l) -ne 0 ] ; then + if [ $($binary --output-replacements-xml $(find jni daemon llarp include pybind | grep -E '\.([hc](pp)?|mm?)$' | grep -v '\#') | grep '' | wc -l) -ne 0 ] ; then exit 1 fi else diff --git a/contrib/macos/sign.sh.in b/contrib/macos/sign.sh.in index af3d18d55..6ebf0859a 100755 --- a/contrib/macos/sign.sh.in +++ b/contrib/macos/sign.sh.in @@ -3,9 +3,6 @@ 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" -codesign --verbose=4 --force -s "@CODESIGN_APPEX@" \ - --entitlements "@PROJECT_SOURCE_DIR@/contrib/macos/lokinet-dnsproxy.entitlements.plist" \ - --deep --strict --timestamp --options=runtime "@SIGN_TARGET@/Contents/PlugIns/lokinet-dnsproxy.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" \ diff --git a/daemon/CMakeLists.txt b/daemon/CMakeLists.txt index 65fd29fe1..febc7314a 100644 --- a/daemon/CMakeLists.txt +++ b/daemon/CMakeLists.txt @@ -88,7 +88,6 @@ if(APPLE) $/Contents/Resources/bootstrap.signed COMMAND mkdir -p $/Contents/PlugIns COMMAND cp -a $ $/Contents/PlugIns/ - COMMAND cp -a $ $/Contents/PlugIns/ COMMAND ${CMAKE_COMMAND} -E copy_if_different ${PROJECT_SOURCE_DIR}/contrib/macos/lokinet.provisionprofile $/Contents/embedded.provisionprofile ) @@ -114,14 +113,14 @@ if(APPLE) @ONLY) add_custom_target( sign - DEPENDS "${PROJECT_BINARY_DIR}/sign.sh" lokinet lokinet-extension lokinet-dnsproxy + DEPENDS "${PROJECT_BINARY_DIR}/sign.sh" lokinet lokinet-extension COMMAND "${PROJECT_BINARY_DIR}/sign.sh" ) else() message(WARNING "Not codesigning: CODESIGN_APP (=${CODESIGN_APP}) and/or CODESIGN_APPEX (=${CODESIGN_APPEX}) are not set") add_custom_target( sign - DEPENDS lokinet lokinet-extension lokinet-dnsproxy + DEPENDS lokinet lokinet-extension COMMAND "true") endif() endif() diff --git a/llarp/apple/CMakeLists.txt b/llarp/apple/CMakeLists.txt index 0fdea9a41..8344e90a1 100644 --- a/llarp/apple/CMakeLists.txt +++ b/llarp/apple/CMakeLists.txt @@ -12,16 +12,26 @@ find_library(FOUNDATION Foundation REQUIRED) find_library(NETEXT NetworkExtension REQUIRED) find_library(COREFOUNDATION CoreFoundation REQUIRED) -target_sources(lokinet-util PRIVATE apple_logger.mm) +target_sources(lokinet-util PRIVATE apple_logger.cpp) target_link_libraries(lokinet-util PUBLIC ${FOUNDATION}) -add_executable(lokinet-extension MACOSX_BUNDLE framework.mm) -add_executable(lokinet-dnsproxy MACOSX_BUNDLE dnsproxy.mm) -target_link_libraries(lokinet-extension PUBLIC +target_sources(lokinet-platform PRIVATE vpn_interface.cpp context_wrapper.cpp) + +add_executable(lokinet-extension MACOSX_BUNDLE + PacketTunnelProvider.m + ) +target_link_libraries(lokinet-extension PRIVATE liblokinet ${COREFOUNDATION} ${NETEXT}) -target_link_libraries(lokinet-dnsproxy PUBLIC + +# Not sure what -fapplication-extension does, but XCode puts it in so... +# -fobjc-arc enables automatic reference counting for objective-C code +# -e _NSExtensionMain because the appex has that instead of a `main` function entry point, of course. +target_compile_options(lokinet-extension PRIVATE -fapplication-extension -fobjc-arc) +target_link_options(lokinet-extension PRIVATE -fapplication-extension -e _NSExtensionMain) + +target_link_libraries(lokinet-extension PUBLIC liblokinet ${COREFOUNDATION} ${NETEXT}) @@ -33,29 +43,8 @@ set_target_properties(lokinet-extension PROPERTIES XCODE_PRODUCT_TYPE com.apple.product-type.app-extension ) -# Not sure what -fapplication-extension does, but XCode puts it in so... -# -e _NSExtensionMain because it has that instead of a `main` function entry point, of course. -target_link_options(lokinet-extension PRIVATE -fapplication-extension -e _NSExtensionMain) -target_compile_options(lokinet-extension PRIVATE -fapplication-extension -fobjc-arc) - 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 ) - -set_target_properties(lokinet-dnsproxy PROPERTIES - BUNDLE TRUE - BUNDLE_EXTENSION appex - MACOSX_BUNDLE_INFO_PLIST ${PROJECT_SOURCE_DIR}/contrib/macos/LokinetDNSProxy.Info.plist.in - XCODE_PRODUCT_TYPE com.apple.product-type.app-extension - ) - -target_link_options(lokinet-dnsproxy PRIVATE -fapplication-extension -e _NSExtensionMain) -target_compile_options(lokinet-dnsproxy PRIVATE -fapplication-extension -fobjc-arc) - -add_custom_command(TARGET lokinet-dnsproxy - POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different ${PROJECT_SOURCE_DIR}/contrib/macos/lokinet-dnsproxy.provisionprofile - $/Contents/embedded.provisionprofile - ) diff --git a/llarp/apple/PacketTunnelProvider.m b/llarp/apple/PacketTunnelProvider.m new file mode 100644 index 000000000..b203095f9 --- /dev/null +++ b/llarp/apple/PacketTunnelProvider.m @@ -0,0 +1,134 @@ +#include +#include +#include "context_wrapper.h" + +NSString* error_domain = @"com.loki-project.lokinet"; + +@interface LLARPPacketTunnel : NEPacketTunnelProvider +{ + void* lokinet; +} + +- (void)startTunnelWithOptions:(NSDictionary*)options + completionHandler:(void (^)(NSError* error))completionHandler; + +- (void)stopTunnelWithReason:(NEProviderStopReason)reason + completionHandler:(void (^)(void))completionHandler; + +- (void)handleAppMessage:(NSData*)messageData + completionHandler:(void (^)(NSData* responseData))completionHandler; + +- (void)readPackets; + +@end + +void nslogger(const char* msg) { NSLog(@"%s", msg); } + +void packet_writer(int af, const void* data, size_t size, void* ctx) { + if (ctx == nil || data == nil) + return; + + NSData* buf = [NSData dataWithBytesNoCopy:(void*)data length:size freeWhenDone:NO]; + LLARPPacketTunnel* t = (__bridge LLARPPacketTunnel*) ctx; + NEPacket* packet = [[NEPacket alloc] initWithData:buf protocolFamily: af]; + [t.packetFlow writePacketObjects:@[packet]]; +} + +void start_packet_reader(void* ctx) { + if (ctx == nil) + return; + + LLARPPacketTunnel* t = (__bridge LLARPPacketTunnel*) ctx; + [t readPackets]; +} + +@implementation LLARPPacketTunnel + +- (void)readPackets +{ + [self.packetFlow readPacketObjectsWithCompletionHandler: ^(NSArray* packets) { + if (lokinet == nil) + return; + for (NEPacket* p in packets) { + llarp_apple_incoming(lokinet, p.data.bytes, p.data.length); + } + [self readPackets]; + }]; +} + +- (void)startTunnelWithOptions:(NSDictionary*)options + completionHandler:(void (^)(NSError*))completionHandler +{ + char ip_buf[16]; + char mask_buf[16]; + char dns_buf[16]; + + NSString* default_bootstrap = [[NSBundle mainBundle] pathForResource:@"bootstrap" ofType:@"signed"]; + + lokinet = llarp_apple_init(nslogger, NSHomeDirectory().UTF8String, default_bootstrap.UTF8String, ip_buf, mask_buf, dns_buf); + if (!lokinet) { + NSError *init_failure = [NSError errorWithDomain:error_domain code:500 userInfo:@{@"Error": @"Failed to initialize lokinet"}]; + NSLog(@"%@", [init_failure localizedDescription]); + return completionHandler(init_failure); + } + + NSString* ip = [[NSString alloc] initWithUTF8String:ip_buf]; + NSString* mask = [[NSString alloc] initWithUTF8String:mask_buf]; + NSString* dnsaddr = [[NSString alloc] initWithUTF8String:dns_buf]; + + NEPacketTunnelNetworkSettings* settings = + [[NEPacketTunnelNetworkSettings alloc] initWithTunnelRemoteAddress:@"127.0.0.1"]; + NEDNSSettings* dns = [[NEDNSSettings alloc] initWithServers:@[dnsaddr]]; + dns.domainName = @"localhost.loki"; + // In theory, matchDomains is supposed to be set to DNS suffixes that we resolve. This seems + // highly unreliable, though: often it just doesn't work at all (perhaps only if we make ourselves + // the default route?), and even when it does work, it seems there are secret reasons that some + // domains (such as instagram.com) still won't work because there's some magic sauce in the OS + // that Apple engineers don't want to disclose ("This is what I expected, actually. Although I + // will not comment on what I believe is happening here", from + // https://developer.apple.com/forums/thread/685410). + // + // So the documentation sucks and the feature doesn't appear to work, so as much as it would be + // nice to capture only .loki and .snode when not in exit mode, we can't, so capture everything + // and use our default upstream. + dns.matchDomains = @[@""]; + dns.matchDomainsNoSearch = true; + dns.searchDomains = @[]; + NEIPv4Settings* ipv4 = [[NEIPv4Settings alloc] initWithAddresses:@[ip] + subnetMasks:@[mask]]; + settings.IPv4Settings = ipv4; + settings.DNSSettings = dns; + [self setTunnelNetworkSettings:settings completionHandler:^(NSError* err) { + if (err) { + NSLog(@"Failed to configure lokinet tunnel: %@", err); + return completionHandler(err); + } + + int start_ret = llarp_apple_start(lokinet, packet_writer, start_packet_reader, (__bridge void*) self); + if (start_ret != 0) { + NSError *start_failure = [NSError errorWithDomain:error_domain code:start_ret userInfo:@{@"Error": @"Failed to start lokinet"}]; + NSLog(@"%@", start_failure); + lokinet = nil; + return completionHandler(start_failure); + } + completionHandler(nil); + }]; +} + +- (void)stopTunnelWithReason:(NEProviderStopReason)reason + completionHandler:(void (^)(void))completionHandler +{ + if (lokinet) { + llarp_apple_shutdown(lokinet); + lokinet = nil; + } + completionHandler(); +} + +- (void)handleAppMessage:(NSData*)messageData + completionHandler:(void (^)(NSData* responseData))completionHandler +{ + NSData* response = [NSData dataWithBytesNoCopy:"ok" length:3 freeWhenDone:NO]; + completionHandler(response); +} +@end diff --git a/llarp/apple/PacketTunnelProvider.swift b/llarp/apple/PacketTunnelProvider.swift new file mode 100644 index 000000000..1cee28bed --- /dev/null +++ b/llarp/apple/PacketTunnelProvider.swift @@ -0,0 +1,129 @@ +/// Lokinet network extension swift glue layer + +import NetworkExtension + +enum LokinetError: Error { + case runtimeError(String) +} + +func packet_writer(af: Int32, data: UnsafeRawPointer?, size: Int, ctx: UnsafeMutableRawPointer?) { + if ctx == nil || data == nil { + return + } + let tunnel = ctx!.assumingMemoryBound(to: PacketTunnelProvider.self).pointee + + let packet = NEPacket( + data: Data(bytes: data!, count: 1), + protocolFamily: sa_family_t(af)) + tunnel.packetFlow.writePacketObjects([packet]) +} + + +func start_packet_reader(ctx: UnsafeMutableRawPointer?) { + let tunnel = ctx?.assumingMemoryBound(to: PacketTunnelProvider.self).pointee + tunnel?.readHandler() +} + + +class PacketTunnelProvider: NEPacketTunnelProvider { + + var lokinet: UnsafeMutableRawPointer? + + func readHandler() { + packetFlow.readPacketObjects() { (packets: [NEPacket]) in + if self.lokinet == nil { + return + } + + for p in packets { + p.data.withUnsafeBytes { (buf: UnsafeRawBufferPointer) -> Void in + llarp_apple_incoming(self.lokinet, buf.baseAddress!, buf.count) + } + } + + self.readHandler() + } + } + + override func startTunnel(options: [String : NSObject]?, completionHandler: @escaping (Error?) -> Void) { + + var ip_buf = Array(repeating: 0, count: 16) + var mask_buf = Array(repeating: 0, count: 16) + var dns_buf = Array(repeating: 0, count: 16) + + lokinet = llarp_apple_init(NSHomeDirectory(), &ip_buf, &mask_buf, &dns_buf) + if lokinet == nil { + NSLog("Lokinet initialization failed!") + completionHandler(LokinetError.runtimeError("Lokinet initialization failed")) + return + } + + let ip = String(cString: ip_buf) + let mask = String(cString: mask_buf) + let dns_addr = String(cString: dns_buf) + + NSLog("Lokinet configured with address %s/%s, dns %s", ip, mask, dns_addr) + + let settings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: "127.0.0.1") + settings.ipv4Settings = NEIPv4Settings(addresses: [ip], subnetMasks: [mask]) + let dns = NEDNSSettings(servers: [dns_addr]) + dns.domainName = "localhost.loki" + dns.matchDomains = ["snode", "loki"] + dns.matchDomainsNoSearch = true + dns.searchDomains = [] + settings.dnsSettings = dns + + let condition = NSCondition() + condition.lock() + defer { condition.unlock() } + + var system_error: Error? + + setTunnelNetworkSettings(settings) { error in + system_error = error + condition.signal() + } + + condition.wait() + + if let error = system_error { + NSLog("Failed to set up tunnel: %s", error.localizedDescription) + lokinet = nil + completionHandler(error) + return + } + + var myself = self + let start_ret = llarp_apple_start(lokinet, packet_writer, start_packet_reader, &myself) + if start_ret != 0 { + NSLog("Lokinet failed to start!") + lokinet = nil + completionHandler(LokinetError.runtimeError("Lokinet failed to start")) + return + } + completionHandler(nil) + } + + override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) { + if lokinet != nil { + llarp_apple_shutdown(lokinet) + lokinet = nil + } + completionHandler() + } + + override func handleAppMessage(_ messageData: Data, completionHandler: ((Data?) -> Void)?) { + if let handler = completionHandler { + handler(messageData) + } + } + + override func sleep(completionHandler: @escaping () -> Void) { + // FIXME - do we need to kick lokinet here? + completionHandler() + } + + override func wake() { + // FIXME - do we need to kick lokinet here? + } +} diff --git a/llarp/apple/apple_logger.mm b/llarp/apple/apple_logger.cpp similarity index 53% rename from llarp/apple/apple_logger.mm rename to llarp/apple/apple_logger.cpp index 52b544e58..4a41c78cf 100644 --- a/llarp/apple/apple_logger.mm +++ b/llarp/apple/apple_logger.cpp @@ -1,9 +1,6 @@ #include "apple_logger.hpp" -#include -#include - -namespace llarp +namespace llarp::apple { void NSLogStream::PreLog( @@ -22,15 +19,7 @@ namespace llarp void NSLogStream::Print(LogLevel, const char*, const std::string& msg) { - const char* msg_ptr = msg.c_str(); - const char* msg_fmt = "%s"; - NSString* fmt = [[NSString alloc] initWithUTF8String:msg_ptr]; - NSString* str = [[NSString alloc] initWithUTF8String:msg_fmt]; - NSLog(fmt, str); + ns_logger(msg.c_str()); } - void - NSLogStream::PostLog(std::stringstream&) const - {} - -} // namespace llarp +} // namespace llarp::apple diff --git a/llarp/apple/apple_logger.hpp b/llarp/apple/apple_logger.hpp index 1546392d1..b97810c76 100644 --- a/llarp/apple/apple_logger.hpp +++ b/llarp/apple/apple_logger.hpp @@ -1,11 +1,17 @@ #pragma once +#include #include -namespace llarp +namespace llarp::apple { struct NSLogStream : public ILogStream { + using ns_logger_callback = void (*)(const char* log_this); + + NSLogStream(ns_logger_callback logger) : ns_logger{logger} + {} + void PreLog( std::stringstream& s, @@ -18,13 +24,17 @@ namespace llarp Print(LogLevel lvl, const char* tag, const std::string& msg) override; void - PostLog(std::stringstream& ss) const override; + PostLog(std::stringstream& ss) const override + {} - virtual void + void ImmediateFlush() override {} void Tick(llarp_time_t) override {} + + private: + ns_logger_callback ns_logger; }; -} // namespace llarp +} // namespace llarp::apple diff --git a/llarp/apple/context.hpp b/llarp/apple/context.hpp new file mode 100644 index 000000000..e25090262 --- /dev/null +++ b/llarp/apple/context.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include +#include "vpn_platform.hpp" + +namespace llarp::apple +{ + struct Context : public llarp::Context + { + std::shared_ptr + makeVPNPlatform() override + { + return std::make_shared(*this, m_PacketWriter, m_OnReadable); + } + + // Callbacks that must be set for packet handling *before* calling Setup/Configure/Run; the main + // point of these is to get passed through to VPNInterface, which will be called during setup, + // after construction. + VPNInterface::packet_write_callback m_PacketWriter; + VPNInterface::on_readable_callback m_OnReadable; + }; + +} // namespace llarp::apple diff --git a/llarp/apple/context_wrapper.cpp b/llarp/apple/context_wrapper.cpp new file mode 100644 index 000000000..bd781fa89 --- /dev/null +++ b/llarp/apple/context_wrapper.cpp @@ -0,0 +1,162 @@ +#include +#include +#include +#include +#include +#include +#include "vpn_interface.hpp" +#include "context_wrapper.h" +#include "context.hpp" +#include "apple_logger.hpp" + +namespace +{ + // The default 127.0.0.1:53 won't work (because we run unprivileged) so remap it to this (unless + // specifically overridden to something else in the config): + const llarp::SockAddr DefaultDNSBind{"127.0.0.1:1153"}; + + struct instance_data + { + llarp::apple::Context context; + std::thread runner; + + std::weak_ptr iface; + }; + +} // namespace + +void* +llarp_apple_init( + ns_logger_callback ns_logger, + const char* config_dir_, + const char* default_bootstrap, + char* ip, + char* netmask, + char* dns) +{ + llarp::LogContext::Instance().logStream = std::make_unique(ns_logger); + + try + { + auto config_dir = fs::u8path(config_dir_); + auto config = std::make_shared(config_dir); + std::optional config_path = config_dir / "lokinet.ini"; + if (!fs::exists(*config_path)) + config_path.reset(); + config->Load(config_path); + + // If no range is specified then go look for a free one, set that in the config, and then return + // it to the caller via the char* parameters. + auto& range = config->network.m_ifaddr; + if (!range.addr.h) + { + if (auto maybe = llarp::FindFreeRange()) + range = *maybe; + else + throw std::runtime_error{"Could not find any free IP range"}; + } + auto addr = llarp::net::TruncateV6(range.addr).ToString(); + auto mask = llarp::net::TruncateV6(range.netmask_bits).ToString(); + if (addr.size() > 15 || mask.size() > 15) + throw std::runtime_error{"Unexpected non-IPv4 tunnel range configured"}; + std::strcpy(ip, addr.c_str()); + std::strcpy(netmask, mask.c_str()); + // XXX possibly DNS needs to be the .0 instead of the .1 because mac reasons? + std::strcpy(dns, addr.c_str()); + + // The default DNS bind setting just isn't something we can use as a non-root network extension + // so remap the default value to a high port unless explicitly set to something else. + if (config->dns.m_bind == llarp::SockAddr{"127.0.0.1:53"}) + config->dns.m_bind = DefaultDNSBind; + + // If no explicit bootstrap then set the system default one included with the app bundle + if (config->bootstrap.files.empty()) + config->bootstrap.files.push_back(fs::u8path(default_bootstrap)); + + auto inst = std::make_unique(); + inst->context.Configure(std::move(config)); + return inst.release(); + } + catch (const std::exception& e) + { + LogError("Failed to initialize lokinet from config: ", e.what()); + } + return nullptr; +} + +int +llarp_apple_start( + void* lokinet, + packet_writer_callback packet_writer, + start_reading_callback start_reading, + void* callback_context) +{ + auto* inst = static_cast(lokinet); + inst->context.m_PacketWriter = [inst, packet_writer, callback_context]( + int af_family, void* data, size_t size) { + packet_writer(af_family, data, size, callback_context); + return true; + }; + + inst->context.m_OnReadable = + [inst, start_reading, callback_context](llarp::apple::VPNInterface& iface) { + inst->iface = iface.weak_from_this(); + start_reading(callback_context); + }; + + std::promise result; + inst->runner = std::thread{[inst, &result] { + const llarp::RuntimeOptions opts{}; + try + { + inst->context.Setup(opts); + } + catch (...) + { + result.set_exception(std::current_exception()); + return; + } + result.set_value(); + inst->context.Run(opts); + }}; + + try + { + result.get_future().get(); + } + catch (const std::exception& e) + { + LogError("Failed to initialize lokinet: ", e.what()); + return -1; + } + + return 0; +} + +int +llarp_apple_incoming(void* lokinet, const void* bytes, size_t size) +{ + auto& inst = *static_cast(lokinet); + + auto iface = inst.iface.lock(); + if (!iface) + return -2; + + llarp_buffer_t buf{static_cast(bytes), size}; + if (iface->OfferReadPacket(buf)) + return 0; + + LogError("invalid IP packet: ", llarp::buffer_printer(buf)); + return -1; +} + +void +llarp_apple_shutdown(void* lokinet) +{ + auto* inst = static_cast(lokinet); + + inst->context.CloseAsync(); + inst->context.Wait(); + inst->runner.join(); + delete inst; +} diff --git a/llarp/apple/context_wrapper.h b/llarp/apple/context_wrapper.h new file mode 100644 index 000000000..6522d641a --- /dev/null +++ b/llarp/apple/context_wrapper.h @@ -0,0 +1,94 @@ +#pragma once + +// C-linkage wrappers for interacting with a lokinet context, so that we can call them from Swift +// code (which currently doesn't support C++ interoperability at all). + +#ifdef __cplusplus +extern "C" +{ +#endif + +#include +#include + + /// C callback function for us to invoke when we need to write a packet + typedef void(packet_writer_callback)(int af, const void* data, size_t size, void* ctx); + + /// C callback function to invoke once we are ready to start receiving packets + typedef void(start_reading_callback)(void* ctx); + + /// C callback that bridges things into NSLog + typedef void(ns_logger_callback)(const char* msg); + + /// Initializes a lokinet instance by initializing various objects and loading the configuration + /// (if {config_dir}/lokinet.ini exists). Does not actually start lokinet (call llarp_apple_start + /// for that). + /// + /// Returns NULL if there was a problem initializing/loading the configuration, otherwise returns + /// an opaque void pointer that should be passed into the other llarp_apple_* functions. + /// + /// \param logger a logger callback that we pass log messages to to relay them (i.e. via NSLog). + /// + /// \param config_dir the lokinet configuration directory where lokinet.ini can be and the various + /// other lokinet state files go. + /// + /// \param default_bootstrap the path to the default bootstrap.signed included in installation, + /// which will be used if no explicit bootstrap is set in the config file. + /// + /// \param ip - char buffer where we will write the primary tunnel IP address as a string such as + /// "172.16.0.0". Will write up to 16 characters (including the null terminator). This will be + /// the tunnel IP from the lokinet.ini, if it exists and specifies a range, otherwise we'll + /// configure lokinet to use a currently-unused range and return that. + /// + /// \param netmask the tunnel netmask as a string such as "255.255.0.0". Will write up to 16 + /// characters (including the null terminator). + /// + /// \param dns the DNS address that should be configured to query lokinet, as a string such as + /// "172.16.0.1". Will write up to 16 characters (including the null terminator). + void* + llarp_apple_init( + ns_logger_callback ns_logger, + const char* config_dir, + const char* default_bootstrap, + char* ip, + char* netmask, + char* dns); + + /// Starts the lokinet instance in a new thread. + /// + /// \param packet_writer C function callback that will be called when we need to write a packet to + /// the packet tunnel. Will be passed AF_INET or AF_INET6, a void pointer to the data, the size + /// of the data in bytes, and the opaque callback_context pointer. + /// + /// \param start_reading C function callback that will be called when lokinet is setup and ready + /// to start receiving packets from the packet tunnel. This should set up the read handler to + /// deliver packets via llarp_apple_incoming. This is called with a single argument of the opaque + /// callback_context pointer. + /// + /// \param callback_context Opaque pointer that is passed into the packet_writer and start_reading + /// callback, intended to allow context to be passed through to the callbacks. This code does + /// nothing with this pointer aside from passing it through to callbacks. + /// + /// \returns 0 on succesful startup, -1 on failure. + int + llarp_apple_start( + void* lokinet, + packet_writer_callback packet_writer, + start_reading_callback start_reading, + void* callback_context); + + /// Called to deliver an incoming packet from the apple layer into lokinet; returns 0 on success, + /// -1 if the packet could not be parsed, -2 if there is no current active VPNInterface associated + /// with the lokinet (which generally means llarp_apple_start wasn't called or failed, or lokinet + /// is in the process of shutting down). + int + llarp_apple_incoming(void* lokinet, const void* bytes, size_t size); + + /// Stops a lokinet instance created with `llarp_apple_initialize`. This waits for lokinet to + /// shut down and rejoins the thread. After this call the given pointer is no longer valid. + void + llarp_apple_shutdown(void* lokinet); + +#ifdef __cplusplus +} // extern "C" +#endif diff --git a/llarp/apple/vpn_interface.cpp b/llarp/apple/vpn_interface.cpp new file mode 100644 index 000000000..079e24aa9 --- /dev/null +++ b/llarp/apple/vpn_interface.cpp @@ -0,0 +1,52 @@ + +#include "vpn_interface.hpp" +#include "context.hpp" + +namespace llarp::apple +{ + VPNInterface::VPNInterface( + Context& ctx, packet_write_callback packet_writer, on_readable_callback on_readable) + : m_PacketWriter{std::move(packet_writer)}, m_OnReadable{std::move(on_readable)} + { + ctx.loop->call_soon([this] { m_OnReadable(*this); }); + } + + bool + VPNInterface::OfferReadPacket(const llarp_buffer_t& buf) + { + llarp::net::IPPacket pkt; + if (!pkt.Load(buf)) + return false; + m_ReadQueue.tryPushBack(std::move(pkt)); + return true; + } + + int + VPNInterface::PollFD() const + { + return -1; + } + + std::string + VPNInterface::IfName() const + { + return ""; + } + + net::IPPacket + VPNInterface::ReadNextPacket() + { + net::IPPacket pkt{}; + if (not m_ReadQueue.empty()) + pkt = m_ReadQueue.popFront(); + return pkt; + } + + bool + VPNInterface::WritePacket(net::IPPacket pkt) + { + int af_family = pkt.IsV6() ? AF_INET6 : AF_INET; + return m_PacketWriter(af_family, pkt.buf, pkt.sz); + } + +} // namespace llarp::apple diff --git a/llarp/apple/vpn_interface.hpp b/llarp/apple/vpn_interface.hpp new file mode 100644 index 000000000..c1dff8dbf --- /dev/null +++ b/llarp/apple/vpn_interface.hpp @@ -0,0 +1,51 @@ +#pragma once + +#include +#include +#include +#include + +namespace llarp::apple +{ + struct Context; + + class VPNInterface final : public vpn::NetworkInterface, + public std::enable_shared_from_this + { + public: + using packet_write_callback = std::function; + using on_readable_callback = std::function; + + explicit VPNInterface( + Context& ctx, packet_write_callback packet_writer, on_readable_callback on_readable); + + // Method to call when a packet has arrived to deliver the packet to lokinet + bool + OfferReadPacket(const llarp_buffer_t& buf); + + int + PollFD() const override; + + std::string + IfName() const override; + + net::IPPacket + ReadNextPacket() override; + + bool + WritePacket(net::IPPacket pkt) override; + + private: + // Function for us to call when we have a packet to emit. Should return true if the packet was + // handed off to the OS successfully. + packet_write_callback m_PacketWriter; + + // Called when we are ready to start reading packets + on_readable_callback m_OnReadable; + + static inline constexpr auto PacketQueueSize = 1024; + + thread::Queue m_ReadQueue{PacketQueueSize}; + }; + +} // namespace llarp::apple diff --git a/llarp/apple/vpn_platform.hpp b/llarp/apple/vpn_platform.hpp new file mode 100644 index 000000000..72018b509 --- /dev/null +++ b/llarp/apple/vpn_platform.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include +#include "vpn_interface.hpp" + +namespace llarp::apple +{ + class VPNPlatform final : public vpn::Platform + { + public: + explicit VPNPlatform( + Context& ctx, + VPNInterface::packet_write_callback packet_writer, + VPNInterface::on_readable_callback on_readable) + : m_Context{ctx} + , m_PacketWriter{std::move(packet_writer)} + , m_OnReadable{std::move(on_readable)} + {} + + std::shared_ptr ObtainInterface(vpn::InterfaceInfo) override + { + return std::make_shared(m_Context, m_PacketWriter, m_OnReadable); + } + + private: + Context& m_Context; + VPNInterface::packet_write_callback m_PacketWriter; + VPNInterface::on_readable_callback m_OnReadable; + }; + +} // namespace llarp::apple