diff --git a/llarp/apple/CMakeLists.txt b/llarp/apple/CMakeLists.txt index 69a356020..32c4f989b 100644 --- a/llarp/apple/CMakeLists.txt +++ b/llarp/apple/CMakeLists.txt @@ -19,6 +19,7 @@ target_sources(lokinet-platform PRIVATE vpn_interface.cpp route_manager.cpp cont add_executable(lokinet-extension MACOSX_BUNDLE PacketTunnelProvider.m + DNSTrampoline.m ) target_link_libraries(lokinet-extension PRIVATE liblokinet diff --git a/llarp/apple/DNSTrampoline.h b/llarp/apple/DNSTrampoline.h new file mode 100644 index 000000000..9e3446d76 --- /dev/null +++ b/llarp/apple/DNSTrampoline.h @@ -0,0 +1,48 @@ +#pragma once +#include +#include + +extern NSString* error_domain; + +/** + * "Trampoline" class that listens for UDP DNS packets on port 1053 coming from lokinet's embedded + * libunbound (when exit mode is enabled), wraps them via NetworkExtension's crappy UDP API, then + * sends responses back to libunbound to be parsed/etc. This class knows nothing about DNS, it is + * basically just a UDP packet forwarder. + * + * So for a lokinet configuration of "upstream=1.1.1.1", when exit mode is OFF: + * - DNS requests go to TUNNELIP:53, get sent to libunbound, which forwards them (directly) to the + * upstream DNS server(s). + * With exit mode ON: + * - DNS requests go to TUNNELIP:53, get send to libunbound, which forwards them to 127.0.0.1:1053, + * which encapsulates them in Apple's god awful crap, then (on a response) sends them back to + * libunbound. + * (This assumes a non-lokinet DNS; .loki and .snode get handled before either of these). + */ +@interface LLARPDNSTrampoline : NSObject +{ + // The socket libunbound talks with: + uv_udp_t request_socket; + // The reply address. This is a bit hacky: we configure libunbound to just use single address + // (rather than a range) so that we don't have to worry about tracking different reply addresses. + @public struct sockaddr reply_addr; + // UDP "session" aimed at the upstream DNS + @public NWUDPSession* upstream; + // Apple docs say writes could take time *and* the crappy Apple datagram write methods aren't + // callable again until the previous write finishes. Deal with this garbage API by queuing + // everything than using a uv_async to process the queue. + @public int write_ready; + @public NSMutableArray* pending_writes; + uv_async_t write_trigger; +} +- (void)startWithUpstreamDns:(NWUDPSession*) dns + listenPort:(uint16_t) listenPort + uvLoop:(uv_loop_t*) loop + completionHandler:(void (^)(NSError* error))completionHandler; + +- (void)flushWrites; + +- (void)dealloc; + +@end + diff --git a/llarp/apple/DNSTrampoline.m b/llarp/apple/DNSTrampoline.m new file mode 100644 index 000000000..b0aad0121 --- /dev/null +++ b/llarp/apple/DNSTrampoline.m @@ -0,0 +1,136 @@ +#include "DNSTrampoline.h" +#include + +NSString* error_domain = @"com.loki-project.lokinet"; + + +// Receiving an incoming packet, presumably from libunbound. NB: this is called from the libuv +// event loop. +static void on_request(uv_udp_t* socket, ssize_t nread, const uv_buf_t* buf, const struct sockaddr* addr, unsigned flags) { + if (nread < 0) { + NSLog(@"Read error: %s", uv_strerror(nread)); + free(buf->base); + return; + } + + if (nread == 0 || !addr) { + if (buf) + free(buf->base); + return; + } + + LLARPDNSTrampoline* t = (__bridge LLARPDNSTrampoline*) socket->data; + + // We configure libunbound to use just one single port so we'll just send replies to the last port + // to talk to us. (And we're only listening on localhost in the first place). + t->reply_addr = *addr; + + // NSData takes care of calling free(buf->base) for us with this constructor: + [t->pending_writes addObject:[NSData dataWithBytesNoCopy:buf->base length:nread]]; + + [t flushWrites]; +} + +static void on_sent(uv_udp_send_t* req, int status) { + NSArray* datagrams = (__bridge_transfer NSArray*) req->data; + free(req); +} + +// NB: called from the libuv event loop (so we don't have to worry about the above and this one +// running at once from different threads). +static void write_flusher(uv_async_t* async) { + LLARPDNSTrampoline* t = (__bridge LLARPDNSTrampoline*) async->data; + if (t->pending_writes.count == 0) + return; + + NSArray* data = [NSArray arrayWithArray:t->pending_writes]; + [t->pending_writes removeAllObjects]; + __weak LLARPDNSTrampoline* weakSelf = t; + [t->upstream writeMultipleDatagrams:data completionHandler: ^(NSError* error) + { + if (error) + NSLog(@"Failed to send request to upstream DNS: %@", error); + + // Trigger another flush in case anything built up while Apple was doing its things. Just + // call it unconditionally (rather than checking the queue) because this handler is probably + // running in some other thread. + [weakSelf flushWrites]; + } + ]; +} + + +static void alloc_buffer(uv_handle_t* handle, size_t suggested_size, uv_buf_t* buf) { + buf->base = malloc(suggested_size); + buf->len = suggested_size; +} + +@implementation LLARPDNSTrampoline + +- (void)startWithUpstreamDns:(NWUDPSession*) dns + listenPort:(uint16_t) listenPort + uvLoop:(uv_loop_t*) loop + completionHandler:(void (^)(NSError* error))completionHandler +{ + pending_writes = [[NSMutableArray alloc] init]; + write_trigger.data = (__bridge void*) self; + uv_async_init(loop, &write_trigger, write_flusher); + + request_socket.data = (__bridge void*) self; + uv_udp_init(loop, &request_socket); + struct sockaddr_in recv_addr; + uv_ip4_addr("127.0.0.1", listenPort, &recv_addr); + int ret = uv_udp_bind(&request_socket, (const struct sockaddr*) &recv_addr, UV_UDP_REUSEADDR); + if (ret < 0) { + NSString* errstr = [NSString stringWithFormat:@"Failed to start DNS trampoline: %s", uv_strerror(ret)]; + NSError *err = [NSError errorWithDomain:error_domain code:ret userInfo:@{@"Error": errstr}]; + NSLog(@"%@", err); + return completionHandler(err); + } + uv_udp_recv_start(&request_socket, alloc_buffer, on_request); + + NSLog(@"Starting DNS trampoline"); + + upstream = dns; + __weak LLARPDNSTrampoline* weakSelf = self; + [upstream setReadHandler: ^(NSArray* datagrams, NSError* error) { + // Reading a reply back from the UDP socket used to talk to upstream + if (error) { + NSLog(@"Reader handler failed: %@", error); + return; + } + LLARPDNSTrampoline* strongSelf = weakSelf; + if (!strongSelf || datagrams.count == 0) + return; + + uv_buf_t* buffers = malloc(datagrams.count * sizeof(uv_buf_t)); + size_t buf_count = 0; + for (NSData* packet in datagrams) { + buffers[buf_count].base = (void*) packet.bytes; + buffers[buf_count].len = packet.length; + buf_count++; + } + uv_udp_send_t* uvsend = malloc(sizeof(uv_udp_send_t)); + uvsend->data = (__bridge_retained void*) datagrams; + int ret = uv_udp_send(uvsend, &strongSelf->request_socket, buffers, buf_count, &strongSelf->reply_addr, on_sent); + free(buffers); + if (ret < 0) + NSLog(@"Error returning DNS responses to unbound: %s", uv_strerror(ret)); + } maxDatagrams:NSUIntegerMax]; + + completionHandler(nil); +} + +- (void)flushWrites +{ + uv_async_send(&write_trigger); +} + +- (void) dealloc +{ + NSLog(@"Shutting down DNS trampoline"); + uv_close((uv_handle_t*) &request_socket, NULL); + uv_close((uv_handle_t*) &write_trigger, NULL); +} + +@end diff --git a/llarp/apple/PacketTunnelProvider.m b/llarp/apple/PacketTunnelProvider.m index 26e35953c..a8fcf2845 100644 --- a/llarp/apple/PacketTunnelProvider.m +++ b/llarp/apple/PacketTunnelProvider.m @@ -1,14 +1,18 @@ #include #include #include "context_wrapper.h" +#include "DNSTrampoline.h" -NSString* error_domain = @"com.loki-project.lokinet"; +// Port (on localhost) for our DNS trampoline for bouncing DNS requests through the exit route when +// in exit mode. +const uint16_t dns_trampoline_port = 1053; @interface LLARPPacketTunnel : NEPacketTunnelProvider { void* lokinet; @public NEPacketTunnelNetworkSettings* settings; @public NEIPv4Route* tun_route4; + LLARPDNSTrampoline* dns_tramp; } - (void)startTunnelWithOptions:(NSDictionary*)options @@ -26,9 +30,9 @@ NSString* error_domain = @"com.loki-project.lokinet"; @end -void nslogger(const char* msg) { NSLog(@"%s", msg); } +static void nslogger(const char* msg) { NSLog(@"%s", msg); } -void packet_writer(int af, const void* data, size_t size, void* ctx) { +static void packet_writer(int af, const void* data, size_t size, void* ctx) { if (ctx == nil || data == nil) return; @@ -38,7 +42,7 @@ void packet_writer(int af, const void* data, size_t size, void* ctx) { [t.packetFlow writePacketObjects:@[packet]]; } -void start_packet_reader(void* ctx) { +static void start_packet_reader(void* ctx) { if (ctx == nil) return; @@ -46,7 +50,7 @@ void start_packet_reader(void* ctx) { [t readPackets]; } -void add_ipv4_route(const char* addr, const char* netmask, void* ctx) { +static void add_ipv4_route(const char* addr, const char* netmask, void* ctx) { NEIPv4Route* route = [[NEIPv4Route alloc] initWithDestinationAddress: [NSString stringWithUTF8String:addr] subnetMask: [NSString stringWithUTF8String:netmask]]; @@ -63,7 +67,7 @@ void add_ipv4_route(const char* addr, const char* netmask, void* ctx) { [t updateNetworkSettings]; } -void del_ipv4_route(const char* addr, const char* netmask, void* ctx) { +static void del_ipv4_route(const char* addr, const char* netmask, void* ctx) { NEIPv4Route* route = [[NEIPv4Route alloc] initWithDestinationAddress: [NSString stringWithUTF8String:addr] subnetMask: [NSString stringWithUTF8String:netmask]]; @@ -84,7 +88,7 @@ void del_ipv4_route(const char* addr, const char* netmask, void* ctx) { } } -void add_ipv6_route(const char* addr, int prefix, void* ctx) { +static void add_ipv6_route(const char* addr, int prefix, void* ctx) { NEIPv6Route* route = [[NEIPv6Route alloc] initWithDestinationAddress: [NSString stringWithUTF8String:addr] networkPrefixLength: [NSNumber numberWithInt:prefix]]; @@ -100,7 +104,8 @@ void add_ipv6_route(const char* addr, int prefix, void* ctx) { [t updateNetworkSettings]; } -void del_ipv6_route(const char* addr, int prefix, void* ctx) { + +static void del_ipv6_route(const char* addr, int prefix, void* ctx) { NEIPv6Route* route = [[NEIPv6Route alloc] initWithDestinationAddress: [NSString stringWithUTF8String:addr] networkPrefixLength: [NSNumber numberWithInt:prefix]]; @@ -120,7 +125,8 @@ void del_ipv6_route(const char* addr, int prefix, void* ctx) { [t updateNetworkSettings]; } } -void add_default_route(void* ctx) { + +static void add_default_route(void* ctx) { LLARPPacketTunnel* t = (__bridge LLARPPacketTunnel*) ctx; t->settings.IPv4Settings.includedRoutes = @[NEIPv4Route.defaultRoute]; @@ -128,7 +134,8 @@ void add_default_route(void* ctx) { [t updateNetworkSettings]; } -void del_default_route(void* ctx) { + +static void del_default_route(void* ctx) { LLARPPacketTunnel* t = (__bridge LLARPPacketTunnel*) ctx; t->settings.IPv4Settings.includedRoutes = @[t->tun_route4]; @@ -182,13 +189,13 @@ void del_default_route(void* ctx) { NSString* ip = [NSString stringWithUTF8String:conf.tunnel_ipv4_ip]; NSString* mask = [NSString stringWithUTF8String:conf.tunnel_ipv4_netmask]; - NSString* dnsaddr = [NSString stringWithUTF8String:conf.tunnel_dns]; // 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:@[dnsaddr]]; + NEDNSSettings* dns = [[NEDNSSettings alloc] initWithServers:@[ip]]; dns.domainName = @"localhost.loki"; + dns.matchDomains = @[@""]; // In theory, matchDomains is supposed to be set to DNS suffixes that we resolve. This seems // highly unreliable, though: often it just doesn't work at all (perhaps only if we make ourselves // the default route?), and even when it does work, it seems there are secret reasons that some @@ -203,6 +210,11 @@ void del_default_route(void* ctx) { dns.matchDomains = @[@""]; dns.matchDomainsNoSearch = true; dns.searchDomains = @[]; + + NWHostEndpoint* upstreamdns_ep; + if (strlen(conf.upstream_dns)) + upstreamdns_ep = [NWHostEndpoint endpointWithHostname:[NSString stringWithUTF8String:conf.upstream_dns] port:@(conf.upstream_dns_port).stringValue]; + NEIPv4Settings* ipv4 = [[NEIPv4Settings alloc] initWithAddresses:@[ip] subnetMasks:@[mask]]; tun_route4 = [[NEIPv4Route alloc] initWithDestinationAddress:ip subnetMask: mask]; @@ -226,7 +238,18 @@ void del_default_route(void* ctx) { lokinet = nil; return completionHandler(start_failure); } - completionHandler(nil); + + NWUDPSession* upstreamdns = [strongSelf createUDPSessionThroughTunnelToEndpoint:upstreamdns_ep fromEndpoint:nil]; + strongSelf->dns_tramp = [LLARPDNSTrampoline alloc]; + [strongSelf->dns_tramp + startWithUpstreamDns:upstreamdns + listenPort:dns_trampoline_port + uvLoop:llarp_apple_get_uv_loop(strongSelf->lokinet) + completionHandler:^(NSError* error) { + if (error) + NSLog(@"Error starting dns trampoline: %@", error); + return completionHandler(error); + }]; }]; } diff --git a/llarp/apple/context_wrapper.cpp b/llarp/apple/context_wrapper.cpp index 8d66911d0..84d98e67c 100644 --- a/llarp/apple/context_wrapper.cpp +++ b/llarp/apple/context_wrapper.cpp @@ -1,9 +1,11 @@ #include #include +#include #include #include #include #include +#include #include "vpn_interface.hpp" #include "context_wrapper.h" #include "context.hpp" @@ -57,7 +59,15 @@ llarp_apple_init(llarp_apple_config* appleconf) throw std::runtime_error{"Unexpected non-IPv4 tunnel range configured"}; std::strcpy(appleconf->tunnel_ipv4_ip, addr.c_str()); std::strcpy(appleconf->tunnel_ipv4_netmask, mask.c_str()); - std::strcpy(appleconf->tunnel_dns, addr.c_str()); + + appleconf->upstream_dns[0] = '\0'; + for (auto& upstream : config->dns.m_upstreamDNS) { + if (upstream.isIPv4()) { + std::strcpy(appleconf->upstream_dns, upstream.hostString().c_str()); + appleconf->upstream_dns_port = upstream.getPort(); + break; + } + } // The default DNS bind setting just isn't something we can use as a non-root network extension // so remap the default value to a high port unless explicitly set to something else. @@ -135,6 +145,15 @@ llarp_apple_start( return 0; } +uv_loop_t* +llarp_apple_get_uv_loop(void* lokinet) +{ + auto& inst = *static_cast(lokinet); + auto uvw = inst.context.loop->MaybeGetUVWLoop(); + assert(uvw); + return uvw->raw(); +} + int llarp_apple_incoming(void* lokinet, const void* bytes, size_t size) { diff --git a/llarp/apple/context_wrapper.h b/llarp/apple/context_wrapper.h index 279dd7cf1..425986015 100644 --- a/llarp/apple/context_wrapper.h +++ b/llarp/apple/context_wrapper.h @@ -10,6 +10,7 @@ extern "C" #include #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); @@ -66,8 +67,10 @@ extern "C" char tunnel_ipv4_ip[16]; /// llarp_apple_init writes the netmask of the tunnel address here, null-terminated. char tunnel_ipv4_netmask[16]; - /// The DNS server IPv4 address the OS should use. Null-terminated. - char tunnel_dns[16]; + /// The first upstream DNS server's IPv4 address the OS should use when in exit mode. + /// (Currently on mac in exit mode we only support querying the first such configured server). + char upstream_dns[16]; + uint16_t upstream_dns_port; /// \defgroup callbacks Callbacks /// Callbacks we invoke for various operations that require glue into the Apple network @@ -119,6 +122,10 @@ extern "C" int llarp_apple_start(void* lokinet, void* callback_context); + /// Returns a pointer to the uv event loop. Must have called llarp_apple_start already. + uv_loop_t* + llarp_apple_get_uv_loop(void* lokinet); + /// Called to deliver an incoming packet from the apple layer into lokinet; returns 0 on success, /// -1 if the packet could not be parsed, -2 if there is no current active VPNInterface associated /// with the lokinet (which generally means llarp_apple_start wasn't called or failed, or lokinet