mirror of https://github.com/oxen-io/lokinet
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
parent
fd759914b6
commit
f00e78c1a3
@ -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
|
Loading…
Reference in New Issue