Add DNS trampoline

This runs a DNS listener on localhost:1053 that bounces requests to the
upstream DNS through the tunnel.  The idea here is that, when we turn on
exit mode, we start libunbound bouncing the requests through the
trampoline (since if it makes direct requests they won't go through the
tunnel).

(The actual libunbound configuration is still to follow).
pull/1688/head
Jason Rhinelander 3 years ago committed by Jeff Becker
parent fd759914b6
commit f00e78c1a3
No known key found for this signature in database
GPG Key ID: F357B3B42F6F9B05

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

@ -0,0 +1,48 @@
#pragma once
#include <uv.h>
#include <NetworkExtension/NetworkExtension.h>
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<NSData*>* 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

@ -0,0 +1,136 @@
#include "DNSTrampoline.h"
#include <uv.h>
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<NSData*>* datagrams = (__bridge_transfer NSArray<NSData*>*) 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<NSData*>* data = [NSArray<NSData*> 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<NSData*> 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<NSData*>* 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

@ -1,14 +1,18 @@
#include <Foundation/Foundation.h>
#include <NetworkExtension/NetworkExtension.h>
#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<NSString*, NSObject*>*)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);
}];
}];
}

@ -1,9 +1,11 @@
#include <cstdint>
#include <cstring>
#include <cassert>
#include <llarp/net/ip_packet.hpp>
#include <llarp/config/config.hpp>
#include <llarp/util/fs.hpp>
#include <llarp/util/logging/buffer.hpp>
#include <uvw/loop.h>
#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<instance_data*>(lokinet);
auto uvw = inst.context.loop->MaybeGetUVWLoop();
assert(uvw);
return uvw->raw();
}
int
llarp_apple_incoming(void* lokinet, const void* bytes, size_t size)
{

@ -10,6 +10,7 @@ extern "C"
#include <unistd.h>
#include <sys/socket.h>
#include <uv.h>
/// 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

Loading…
Cancel
Save