Switched to nginx for doh

master
Meliurwen 3 years ago
parent af961b6f84
commit 7753f41b64
Signed by: meliurwen
GPG Key ID: 818A8B35E9F1CE10
  1. 15
      nginx/Dockerfile
  2. 13
      nginx/root/entrypoint.sh
  3. 14
      nginx/root/etc/nginx/conf.d/realip.conf
  4. 137
      nginx/root/etc/nginx/nginx.template
  5. 295
      nginx/root/etc/nginx/njs.d/dns/dns.js
  6. 205
      nginx/root/etc/nginx/njs.d/dns/glb.js
  7. 632
      nginx/root/etc/nginx/njs.d/dns/libdns.js
  8. 54
      nginx/root/etc/nginx/njs.d/nginx_stream.js

@ -0,0 +1,15 @@
ARG IMAGE
ARG TAG
FROM ${IMAGE}:${TAG}
LABEL maintainer="Meliurwen <meliruwen@gmail.com>"
COPY root/ /
ENV DOH_LISTEN_PORT=8080
ENV DOH_HTTP_PREFIX="/dns-query"
ENV UPSTREAM_DNS_ADDR=unbound
ENV UPSTREAM_DNS_PORT=53
ENTRYPOINT ["/entrypoint.sh"]

@ -0,0 +1,13 @@
#!/bin/sh
# Exit at first error
set -e
# Fill the varibles in default.template and put the result in default.conf
envsubst "`env | awk -F = '{printf \" $$%s\", $$1}'`" < \
/etc/nginx/nginx.template > \
/etc/nginx/nginx.conf
cat /etc/nginx/nginx.conf
nginx -g 'daemon off;'

@ -0,0 +1,14 @@
# Real IP Settings
# This option get user's real ip address
# to be fowared to your service container
# The option 'set_real_ip_from'
# must correspont to your docker network address
set_real_ip_from 172.16.0.0/12;
set_real_ip_from 10.0.0.0/8;
set_real_ip_from 192.168.0.0/16;
# Header for Real IP Address
real_ip_header X-Forwarded-For;
#real_ip_header X-Real-IP;
real_ip_recursive on;

@ -0,0 +1,137 @@
user nginx;
worker_processes auto;
load_module modules/ngx_stream_js_module.so;
error_log /var/log/nginx/error.log notice;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# logging directives
log_format doh '$remote_addr - $remote_user [$time_local] "$request" '
'[ $msec, $request_time, $upstream_response_time $pipe ] '
'$status $body_bytes_sent "$http_x_forwarded_for" '
'$upstream_http_x_dns_question $upstream_http_x_dns_type '
'$upstream_http_x_dns_result '
'$upstream_http_x_dns_ttl $upstream_http_x_dns_answers '
'$upstream_cache_status';
access_log /var/log/nginx/doh-access.log doh;
# This upstream connects to a local Stream service which converts HTTP -> DNS
upstream dohloop {
zone dohloop 64k;
server 127.0.0.1:8053;
keepalive_timeout 60s;
keepalive_requests 100;
keepalive 10;
}
# Proxy Cache storage - so we can cache the DoH response from the upstream
proxy_cache_path /var/cache/nginx/doh_cache levels=1:2 keys_zone=doh_cache:10m;
# Apply fix for very long server names
server_names_hash_bucket_size 128;
# Security Headers
add_header Content-Security-Policy "default-src 'none'; script-src 'self' 'unsafe-inline'; img-src 'self'; style-src 'self' 'unsafe-inline'; font-src 'self'; object-src 'none'; media-src 'self' blob:; worker-src 'self' blob:; base-uri 'self'; form-action 'self'; frame-ancestors 'self'; connect-src 'self' https://*.twimg.com; manifest-src 'self'";
add_header X-Content-Type-Options nosniff;
add_header X-Frame-Options DENY;
add_header X-XSS-Protection "1; mode=block";
add_header 'Referrer-Policy' 'strict-origin';
# Proxy
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_redirect off;
server_tokens off;
# The DoH server block
server {
# Listen on standard HTTP port
listen ${DOH_LISTEN_PORT};
# DoH may use GET or POST requests, Cache both
proxy_cache_methods GET POST;
# Return 404 to all responses, except for those using our published DoH URI
location / {
return 404 "404 Not Found\n";
}
# This is our published DoH URI
location ${DOH_HTTP_PREFIX} {
# Proxy HTTP/1.1, clear the connection header to enable Keep-Alive
proxy_http_version 1.1;
proxy_set_header Connection "";
# Enable Cache, and set the cache_key to include the request_body
proxy_cache doh_cache;
proxy_cache_key $scheme$proxy_host$uri$is_args$args$request_body;
# proxy pass to the dohloop upstream
proxy_pass http://dohloop;
}
}
include /etc/nginx/conf.d/*.conf;
}
# DNS Stream Services
stream {
# DNS logging
log_format dns '$remote_addr [$time_local] $protocol "$dns_qname"';
access_log /var/log/nginx/dns-access.log dns;
# Include the NJS module
js_include /etc/nginx/njs.d/nginx_stream.js;
# The $dns_qname variable can be populated by preread calls, and can be used for DNS routing
js_set $dns_qname dns_get_qname;
# DNS upstream pool.
upstream dns {
zone dns 64k;
server ${UPSTREAM_DNS_ADDR}:${UPSTREAM_DNS_PORT};
}
# DNS(TCP)
server {
listen 53;
js_preread dns_preread_dns_request;
proxy_pass dns;
}
# DNS(UDP) Server
# DNS UDP proxy onto DNS UDP
server {
listen 53 udp;
proxy_responses 1;
js_preread dns_preread_dns_request;
proxy_pass dns;
}
# DNS over HTTPS (gateway) Service
server {
listen 127.0.0.1:8053;
js_filter dns_filter_doh_request;
proxy_pass dns;
}
}

@ -0,0 +1,295 @@
import dns from "libdns.js";
export default {get_qname, get_response, preread_doh_request, preread_dns_request, filter_doh_request};
/**
* DNS Decode Level
* 0: No decoding, minimal processing required to strip packet from HTTP wrapper (fastest)
* 1: Parse DNS Header and Question. We can log the Question, Class, Type, and Result Code
* 2: As 1, but also parse answers. We can log the answers, and also cache responses in HTTP Content-Cache
* 3: Very Verbose, log everything as above, but also write packet data to error log (slowest)
**/
var dns_decode_level = 2;
/**
* DNS Question Load Balancing
* Set this to true, if you want to pick the upstream pool based on the DNS Question.
* Doing so will disable HTTP KeepAlives for DoH so that we can create a new socket for each query
**/
var dns_question_balancing = false;
// The DNS Question name
var dns_name = String.bytesFrom([]);
function get_qname(s) {
return dns_name;
}
// The Optional DNS response, this is set when we want to block a specific domain
var dns_response = String.bytesFrom([]);
function get_response(s) {
return dns_response.toString();
}
// Encode the given number to two bytes (16 bit)
function to_bytes( number ) {
return String.fromCodePoint( ((number>>8) & 0xff), (number & 0xff) ).toBytes();
}
function debug(s, msg) {
if ( dns_decode_level >= 3 ) {
s.warn(msg);
}
}
function process_doh_request(s, decode, scrub) {
s.on("upload", function(data,flags) {
if ( data.length == 0 ) {
return;
}
data.split("\r\n").forEach( function(line) {
var bytes;
var packet;
if ( line.toString('hex').startsWith( '0000') ) {
bytes = line;
} else if ( line.toString().startsWith("GET /dns-query?") ) {
var qs = line.slice("GET /dns-query?".length, line.length - " HTTP/1.1".length)
qs = qs.split("&");
debug(s, "process_doh_request: QS Params: " + qs );
qs.some( param => {
if ( param.startsWith("dns=") ) {
bytes = String.bytesFrom(param.slice(4), "base64url");
return true;
}
return false;
});
}
if (bytes) {
debug(s, "process_doh_request: DNS Req: " + bytes.toString('hex') );
if (decode) {
packet = dns.parse_packet(bytes);
debug(s, "process_doh_request: DNS Req ID: " + packet.id );
dns.parse_question(packet);
debug(s,"process_doh_request: DNS Req Name: " + packet.question.name);
dns_name = packet.question.name;
}
if (scrub) {
domain_scrub(s, bytes, packet);
s.done();
} else {
s.send( to_bytes(bytes.length) );
s.send( bytes, {flush: true} );
}
} else {
if ( ! scrub) {
debug(s, "process_doh_request: DNS Req: " + line.toString() );
s.send("");
data = "";
}
}
});
});
}
function process_dns_request(s, decode, scrub) {
s.on("upload", function(bytes,flags) {
if ( bytes.length == 0 ) {
return;
}
var packet;
if (bytes) {
if (s.variables.protocol == "TCP") {
// Drop the TCP length field
bytes = bytes.slice(2);
}
debug(s, "process_dns_request: DNS Req: " + bytes.toString('hex') );
if (decode) {
packet = dns.parse_packet(bytes);
debug(s, "process_dns_request: DNS Req ID: " + packet.id );
dns.parse_question(packet);
debug(s,"process_dns_request: DNS Req Name: " + packet.question.name);
dns_name = packet.question.name;
}
if (scrub) {
domain_scrub(s, bytes, packet);
s.done();
} else {
if (s.variables.protocol == "TCP") {
s.send( to_bytes(bytes.length) );
}
s.send( bytes, {flush: true} );
}
}
});
}
function domain_scrub(s, data, packet) {
var found = false;
if ( s.variables.server_port == 9953 ) {
dns_response = dns.shortcut_nxdomain(data, packet);
if (s.variables.protocol == "TCP" ) {
dns_response = to_bytes( dns_response.length ) + dns_response;
}
debug(s,"Scrubbed: Response: " + dns_response.toString('hex') );
} else if ( s.variables.server_port == 9853 ) {
var answers = [];
if ( packet.question.type == dns.dns_type.A ) {
answers.push( {name: packet.question.name, type: dns.dns_type.A, class: dns.dns_class.IN, ttl: 300, rdata: "0.0.0.0" } );
} else if ( packet.question.type == dns.dns_type.AAAA ) {
answers.push( {name: packet.question.name, type: dns.dns_type.AAAA, class: dns.dns_class.IN, ttl: 300, rdata: "0000:0000:0000:0000:0000:0000:0000:0000" } );
}
dns_response = dns.shortcut_response(data, packet, answers);
if (s.variables.protocol == "TCP" ) {
dns_response = to_bytes( dns_response.length ) + dns_response;
}
debug(s,"Scrubbed: Response: " + dns_response.toString('hex') );
} else {
debug(s,"Scrubbing: Check: Name: " + packet.question.name );
if ( s.variables.scrub_action ) {
debug(s, "Scrubbing: Check: EXACT MATCH: Name: " + packet.question.name + ", Action: " + s.variables.scrub_action );
dns_response = s.variables.scrub_action;
return;
} else {
["blocked", "blackhole"].forEach( function( list ) {
if(found) { return };
var blocked = s.variables[ list + "_domains" ];
if ( blocked ) {
blocked = blocked.split(',');
blocked.forEach( function( domain ) {
if (packet.question.name.endsWith( domain )) {
debug(s,"Scrubbing: Check: LISTED: Name: " + packet.question.name + ", Action: " + list );
dns_response = list;
found = true;
return;
}
});
}
});
if(found) { return };
}
debug(s,"Scrubbing: Check: NOT FOUND: Name: " + packet.question.name);
}
}
function preread_dns_request(s) {
process_dns_request(s, true, true);
}
function preread_doh_request(s) {
process_doh_request(s, true, true);
}
function filter_doh_request(s) {
if ( dns_decode_level >= 3 ) {
process_doh_request(s, true, false);
} else {
process_doh_request(s, false, false);
}
s.on("download", function(data, flags) {
if ( data.length == 0 ) {
return;
}
// Drop the TCP length field
data = data.slice(2);
debug(s, "DNS Res: " + data.toString('hex') );
var packet;
var answers = "";
var cache_time = 10;
if ( dns_question_balancing ) {
s.send("HTTP/1.1 200\r\nConnection: Close\r\nContent-Type: application/dns-message\r\nContent-Length:" + data.length + "\r\n");
} else {
s.send("HTTP/1.1 200\r\nConnection: Keep-Alive\r\nKeep-Alive: timeout=60, max=1000\r\nContent-Type: application/dns-message\r\nContent-Length:" + data.length + "\r\n");
}
if ( dns_decode_level > 0 ) {
packet = dns.parse_packet(data);
dns.parse_question(packet);
dns_name = packet.question.name;
s.send("X-DNS-Question: " + dns_name + "\r\n");
s.send("X-DNS-Type: " + dns.dns_type.value[packet.question.type] + "\r\n");
s.send("X-DNS-Result: " + dns.dns_codes.value[packet.codes & 0x0f] + "\r\n");
if ( dns_decode_level > 1 ) {
if ( dns_decode_level == 2 ) {
dns.parse_answers(packet, 2);
} else if ( dns_decode_level > 2 ) {
dns.parse_complete(packet, 2);
}
debug(s, "DNS Res Answers: " + JSON.stringify( Object.entries(packet.answers)) );
if ( "min_ttl" in packet ) {
cache_time = packet.min_ttl;
s.send("X-DNS-TTL: " + packet.min_ttl + "\r\n");
}
if ( packet.an > 0 ) {
packet.answers.forEach( function(r) { answers += "[" + dns.dns_type.value[r.type] + ":" + r.data + "]," })
answers.slice(0,-1);
} else {
answers = "[]";
}
s.send("X-DNS-Answers: " + answers + "\r\n");
}
debug(s, "DNS Res Packet: " + JSON.stringify( Object.entries(packet)) );
}
var d = new Date( Date.now() + (cache_time*1000) ).toUTCString();
if ( ! d.includes(",") ) {
d = d.split(" ")
d = [d[0] + ',', d[2], d[1], d[3], d[4], d[5]].join(" ");
}
s.send("Cache-Control: public, max-age=" + cache_time + "\r\n" );
s.send("Expires: " + d + "\r\n" );
s.send("\r\n");
s.send( data, {flush: true} );
if ( dns_question_balancing ) {
s.done();
}
});
}
/**
* Function to perform testing of DNS packet generation for various DNS types
**/
function test_dns_responder(s, data, packet) {
debug(s,"Testing: DNS Req Name: " + packet.question.name);
var answers = [];
if ( packet.question.type == dns.dns_type.A ) {
answers.push( {name: packet.question.name, type: dns.dns_type.A, class: dns.dns_class.IN, ttl: 300, rdata: "10.2.3.4" } );
} else if ( packet.question.type == dns.dns_type.AAAA ) {
answers.push( {name: packet.question.name, type: dns.dns_type.AAAA, class: dns.dns_class.IN, ttl: 300, rdata: "fe80:0002:0003:0004:0005:0006:0007:0008" } );
} else if ( packet.question.type == dns.dns_type.CNAME ) {
answers.push( {name: packet.question.name, type: dns.dns_type.CNAME, class: dns.dns_class.IN, ttl: 300, rdata: "www.foo.bar.baz" } );
} else if ( packet.question.type == dns.dns_type.NS ) {
answers.push( {name: packet.question.name, type: dns.dns_type.NS, class: dns.dns_class.IN, ttl: 300, rdata: "ns1.foo.bar.baz" } );
answers.push( {name: packet.question.name, type: dns.dns_type.NS, class: dns.dns_class.IN, ttl: 300, rdata: "ns2.foo.bar.baz" } );
} else if ( packet.question.type == dns.dns_type.TXT ) {
answers.push( {name: packet.question.name, type: dns.dns_type.TXT, class: dns.dns_class.IN, ttl: 300, rdata: ["ns1.foo.bar.baz","1aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "1AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA1234567890"] } );
} else if ( packet.question.type == dns.dns_type.MX ) {
answers.push( {name: packet.question.name, type: dns.dns_type.MX, class: dns.dns_class.IN, ttl: 300, rdata: { priority: 1, exchange: "mx1.foo.com"} } );
answers.push( {name: packet.question.name, type: dns.dns_type.MX, class: dns.dns_class.IN, ttl: 300, rdata: { priority: 10, exchange: "mx2.foo.com"} } );
} else if ( packet.question.type == dns.dns_type.SRV ) {
answers.push( {name: packet.question.name, type: dns.dns_type.SRV, class: dns.dns_class.IN, ttl: 300, rdata: { priority: 1, weight: 10, port: 443, target: "server1.foo.com"} } );
} else if ( packet.question.type == dns.dns_type.SOA ) {
answers.push( {name: packet.question.name, type: dns.dns_type.SOA, class: dns.dns_class.IN, ttl: 300, rdata: { primary: "ns1.foo.com", mailbox: "mb.nginx.com", serial: 2019102801, refresh: 1800, retry: 3600, expire: 826483, minTTL:300} } );
}
if ( packet.question.name.endsWith("bar.com") ) {
dns_response = dns.shortcut_response(data, packet, answers);
} else {
packet.flags |= dns.dns_flags.AA | dns.dns_flags.QR;
packet.codes |= dns.dns_codes.RA;
packet.authority.push( {name: packet.question.name, type: dns.dns_type.SOA, class: dns.dns_class.IN, ttl: 300, rdata: { primary: "ns1.foo.com", mailbox: "mb.nginx.com", serial: 2019102801, refresh: 1800, retry: 3600, expire: 826483, minTTL:300} });
packet.additional.push( {name: packet.question.name, type: dns.dns_type.NS, class: dns.dns_class.IN, ttl: 300, rdata: "ns1.foo.bar.baz" } );
packet.additional.push( {name: packet.question.name, type: dns.dns_type.NS, class: dns.dns_class.IN, ttl: 300, rdata: "ns2.foo.bar.baz" } );
dns_response = dns.encode_packet(packet);
}
if (s.variables.protocol == "TCP" ) {
dns_response = to_bytes( dns_response.length ) + dns_response;
}
debug(s,"Testing: Response: " + dns_response.toString('hex') );
}

@ -0,0 +1,205 @@
/**
BEGIN GLB Functions
**/
import dns from "libdns.js";
export default {get_response, get_edns_subnet, process_request};
// Any encoded response packets for NGINX to send back go here
var glb_res_packet = String.bytesFrom([]);
// Client subnet gets stored in the variable if we have one
var glb_edns_subnet = String.bytesFrom([]);
// Function for js_set to use in order to pick up the glb_res_packet above
function get_response(s) {
return glb_res_packet;
}
// Function to get the EDNS subnet
function get_edns_subnet(s) {
return glb_edns_subnet;
}
// Process a DNS request and generate a response packet, saving it into glb_res_packet
function process_request(s) {
s.on("upload", function(data,flags) {
s.warn( "Received: " + data.toString('hex') );
var packet = dns.parse_packet(data);
var glb_use_edns = new Boolean(parseInt(s.variables.glb_use_edns));
s.warn( "ID: " + packet.id );
s.warn( "QD: " + packet.qd );
s.warn( "AR: " + packet.ar );
if ( packet.qd == 1 ) {
dns.parse_question(packet);
s.warn("Name: " + packet.question.name);
// Decode additional records, most clients will send an EDNS (OPT) to increase payload size
// and for EDNS Client Subnet, Cookies, etc.
if ( packet.ar > 0 ) {
// only decode if EDNS is enabled
s.warn( "USE EDNS: " + glb_use_edns );
if ( glb_use_edns ) {
dns.parse_complete(packet,1);
if ( "edns" in packet ) {
if ( packet.edns.opts.csubnet ) {
s.warn( "EDNS Subnet: " + packet.edns.opts.csubnet.subnet );
glb_edns_subnet = packet.edns.opts.csubnet.subnet;
}
}
}
}
// Check if we're doing GLB for the given name
var config = glb_get_config( packet, "", s );
if ( ! Array.isArray(config) ) {
s.warn("Failed to get config for: " + packet.question.name );
glb_res_packet = glb_failure(packet, dns.dns_codes.NXDOMAIN );
s.warn( "Sending: " + glb_res_packet.toString('hex') );
s.done();
return;
}
// GSLB this muther
var nodes = glb_get_nodes( packet, config, s );
if ( ! Array.isArray(nodes) ) {
s.warn("Failed to get any nodes for: " + packet.question.name );
glb_res_packet = glb_failure(packet, dns.dns_codes.SERVFAIL );
s.warn( "Sending: " + glb_res_packet.toString('hex') );
s.done();
return;
}
// Build an array of answers from the nodes
var answers = [];
if ( config[1] == "active" ) {
nodes.forEach( function(node) {
answers.push( {name: packet.question.name, type: dns.dns_type.A, class: dns.dns_class.IN, ttl: config[2], rdata: node} );
});
} else if ( config[1] == "random" ) {
var node = nodes[Math.floor(Math.random()*nodes.length)];
answers.push( {name: packet.question.name, type: dns.dns_type.A, class: dns.dns_class.IN, ttl: config[2], rdata: node} );
} else if ( config[1] == "geoip" ) {
var distance=99999999;
var closest = [];
var client_ip, client_lat, client_lon;
/**if ( glb_edns_subnet ) {
client_lat = s.variables.edns_latitude;
client_lon = s.variables.edns_longitude;
client_ip = glb_edns_subnet;
} else { **/
client_lat = s.variables.geoip2_latitude;
client_lon = s.variables.geoip2_longitude;
client_ip = s.variables.geoip_source;
//}
s.warn( "Client: " + client_ip + ", Lat: " + client_lat );
s.warn( "Client: " + client_ip + ", Lon: " + client_lon );
for (var i=0; i< nodes.length; i++ ) {
var suffix = "_geoip_" + nodes[i].replace(/\./g, '_');
var node_location = glb_get_config( packet, suffix, s )
if ( ! node_location ) {
s.warn( "GEO location missing. Please add GEOIP key for node: " + nodes[i] );
continue;
}
var nd = glb_calc_distance( client_lon, client_lat,
node_location[1], node_location[0]);
s.warn( "Distance to: " + nodes[i] + " - " + nd );
if ( nd < distance ) {
closest = [ nodes[i] ];
distance = nd;
} else if ( nd == distance ) {
closest.push( nodes[i] );
}
}
closest.forEach( function(node) {
answers.push( {name: packet.question.name, type: dns.dns_type.A, class: dns.dns_class.IN, ttl: config[2], rdata: node} );
});
} else {
s.warn("Unknown LB Algorithm: '" + config[1] + "' for: " + packet.question.name );
glb_res_packet = glb_failure(packet, dns.dns_codes.SERVFAIL );
s.warn( "Sending: " + glb_res_packet.toString('hex') );
s.done();
return;
}
// Shortcut - copy data from request
glb_res_packet = dns.shortcut_response(data, packet, answers);
// The long way, decode/encode
//var response = dns.gen_response_packet( packet, packet.question, answers, [], [] );
//glb_res_packet = dns.encode_packet( response );
s.warn( "Sending: " + glb_res_packet.toString('hex') );
s.done();
}
});
}
function glb_failure(packet, code) {
var failed = dns.gen_new_packet( packet.id, packet.flags, packet.codes);
failed.question = packet.question;
failed.qd = 1;
failed.codes |= code;
failed.flags |= dns.dns_flags.QR;
return dns.encode_packet( failed );
}
function glb_get_config( packet, suffix, s) {
var key = packet.question.name.replace(/\./g, '_') + suffix;
var uri = '/4/stream/keyvals/glb_config';
var config;
if ( njs.version.slice(0,3) >= 0.9 ) {
// future functionality
var db = s.api( uri );
config = db.read(key);
} else {
config = s.variables[ key ];
}
if ( config ) {
config = config.split(',');
}
return config;
}
function glb_get_nodes( packet, config, s ) {
var key = packet.question.name.replace(/\./g, '_');
var uri = "/4/" + config[0] + "/upstreams/" + key;
var nodes;
if ( njs.version.slice(0,3) >= 0.9 ) {
var db = s.api( uri );
var json = db.read(key);
nodes = glb_process_upstream_status( json, config );
} else {
// No API, so try _nodes list
nodes = s.variables[ key + "_nodes" ];
nodes = nodes.split(',');
}
return nodes;
}
function glb_process_upstream_status( json, config ) {
// TODO process upstream peers
var primary = [];
var backup = [];
}
/**
* Calculate distance between two GPS locations.
* Thanks to: https://www.barattalo.it/coding/decimal-degrees-conversion-and-distance-of-two-points-on-google-map/
**/
function glb_calc_distance(lat1,lon1,lat2,lon2) {
var R = 6371; // km (change this constant to get miles)
var dLat = (lat2-lat1) * Math.PI / 180;
var dLon = (lon2-lon1) * Math.PI / 180;
var a = Math.sin(dLat/2) * Math.sin(dLat/2) +
Math.cos(lat1 * Math.PI / 180 ) * Math.cos(lat2 * Math.PI / 180 ) *
Math.sin(dLon/2) * Math.sin(dLon/2);
var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
var d = R * c;
if (d>1) return Math.round(d);
else if (d<=1) return Math.round(d*1000)+"m";
return d;
}

@ -0,0 +1,632 @@
/**
BEGIN DNS Functions
**/
export default {dns_type, dns_class, dns_flags, dns_codes,
parse_packet, parse_question, parse_answers,
parse_complete, parse_resource_record,
shortcut_response, shortcut_nxdomain,
gen_new_packet, gen_response_packet, encode_packet}
// DNS Types
var dns_type = Object.freeze({
A: 1,
NS: 2,
CNAME: 5,
SOA: 6,
PTR: 12,
MX: 15,
TXT: 16,
AAAA: 28,
SRV: 33,
OPT: 41,
AXFR: 252,
value: { 1:"A", 2:"NS", 5:"CNAME", 6:"SOA", 12:"PTR", 15:"MX", 16:"TXT",
28:"AAAA", 33:"SRV", 41:"OPT", 252:"AXFR" }
});
// DNS Classes
var dns_class = Object.freeze({
IN: 1,
CS: 2,
CH: 3,
HS: 4,
value: { 1:"IN", 2:"CS", 3:"CH", 4:"HS" }
});
// DNS flags (made up of QR, Opcode (4bits), AA, TrunCation, Recursion Desired)
var dns_flags = Object.freeze({
QR: 0x80,
AA: 0x4,
TC: 0x2,
RD: 0x1
});
// DNS Codes (made up of RA (Recursion Available), Zero (3bits), Response Code (4bits))
var dns_codes = Object.freeze({
RA: 0x80,
Z: 0x70,
//RCODE: 0xf,
NOERROR: 0x0,
FORMERR: 0x1,
SERVFAIL: 0x2,
NXDOMAIN: 0x3,
NOTIMPL: 0x4,
REFUSED: 0x5,
value: { 0x80:"RA", 0x70:"Z", 0x0:"NOERROR", 0x1:"FORMERR", 0x2:"SERVFAIL", 0x3:"NXDOMAIN", 0x4:"NOTIMPL", 0x5:"REFUSED" }
});
// Convert two bytes in a packet to a 16bit int
function to_int(A, B) {
return (((A & 0xFF) << 8) | (B & 0xFF));
}
// Convert four bytes in a packet to a 32bit int
function to_int32(A, B, C, D) {
return ( ((A & 0xFF) << 24) | ((B & 0xFF) << 16) | ((C & 0xFF) << 8) | (D & 0xFF) );
}
// Encode the given number to two bytes (16 bit)
function to_bytes( number ) {
return String.fromCodePoint( ((number>>8) & 0xff), (number & 0xff) ).toBytes();
}
// Encode the given number to 4 bytes (32 bit)
function to_bytes32( number ) {
return String.fromCodePoint( (number>>24)&0xff, (number>>16)&0xff, (number>>8)&0xff, number&0xff ).toBytes();
}
// Create a new empty DNS packet structure
function gen_new_packet(id, flags, codes) {
var dns_packet = { id: id, flags: flags, codes: codes, qd: 0, an: 0, ns: 0, ar: 0,
question: {},
answers: [],
authority: [],
additional: []
};
return dns_packet;
}
/** Create a new response packet suitable as a reply to the given request
* You should also supply some answers, authority and/or additional records
* in arrays to populate the various sections.
**/
function gen_response_packet( request, question, answers, authority, additional ) {
var response = gen_new_packet(request.id, request.flags, request.codes);
response.flags |= dns_flags.AA + dns_flags.QR;
response.codes |= dns_codes.RA;
if ( question == null ) {
response.qd = 0;
} else {
response.qd = 1;
response.question = request.question;
}
answers.forEach( function(answer) {
response.an++;
response.answers.push( answer );
});
return response;
}
/** Encode the provided packet, converting it from the javascript object structure into a bytestring
* Returns a bytestring suitable for dropping into a UDP packet, or returning to NGINX
**/
function encode_packet( packet ) {
var encoded = to_bytes( packet.id );
encoded += String.fromCodePoint( packet.flags ).toBytes();
encoded += String.fromCodePoint( packet.codes ).toBytes();
encoded += to_bytes( packet.qd ); // Questions
encoded += to_bytes( packet.answers.length ); // Answers
encoded += to_bytes( packet.authority.length ); // Authority
encoded += to_bytes( packet.additional.length ); // Additional
encoded += encode_question(packet);
packet.answers.forEach( function(answer) {
encoded += gen_resource_record(packet, answer.name, answer.type, answer.class, answer.ttl, answer.rdata);
});
packet.authority.forEach( function(rec) {
encoded += gen_resource_record(packet, rec.name, rec.type, rec.class, rec.ttl, rec.rdata);
});
packet.additional.forEach( function(rec) {
encoded += gen_resource_record(packet, rec.name, rec.type, rec.class, rec.ttl, rec.rdata);
});
return encoded;
}
/** Don't mess about. This is a shortcut for responding to DNS Queries. We copy the question out of the query
* and cannibalise the original request to generate our response.
**/
function shortcut_response(data, packet, answers) {
var response = String.bytesFrom([]);
response += data.slice(0, 2);
response += String.fromCodePoint( packet.flags |= dns_flags.AA | dns_flags.QR ).toBytes();
response += String.fromCodePoint( packet.codes |= dns_codes.RA ).toBytes();
response += to_bytes( 1 ); // Questions
response += to_bytes( answers.length ); // Answers
response += to_bytes( 0 ); // Authority
response += to_bytes( 0 ); // Additional
response += data.slice(12, packet.question.qend );
answers.forEach( function(answer) {
response += gen_resource_record(packet, answer.name, answer.type, answer.class, answer.ttl, answer.rdata);
});
return response;
}
function shortcut_nxdomain(data, packet) {
var response = String.bytesFrom([]);
response += data.slice(0,2);
response += String.fromCodePoint( packet.flags |= dns_flags.AA | dns_flags.QR ).toBytes();
response += String.fromCodePoint( packet.codes |= dns_codes.NXDOMAIN | dns_codes.RA ).toBytes();
response += to_bytes( 1 ); // Questions
response += to_bytes( 0 ); // Answers
response += to_bytes( 0 ); // Authority
response += to_bytes( 0 ); // Additional
response += data.slice(12, packet.question.qend );
return response;
}
/** Encode a question object into a bytestring suitable for use in a UDP packet
**/
function encode_question(packet) {
var encoded = encode_label(packet.question.name);
encoded += to_bytes(packet.question.type);
encoded += to_bytes(packet.question.class);
return encoded;
}
/**
* Parse an incoming request bytestring into a DNS packet object. This function decodes the first 12 bytes of the headers.
* You will probably want to call parse_question() next.
**/
function parse_packet(data) {
var packet = { id: to_int(data.codePointAt(0), data.codePointAt(1)), flags: data.codePointAt(2), codes: data.codePointAt(3), min_ttl: 2147483647,
qd: to_int(data.codePointAt(4), data.codePointAt(5)), an: to_int(data.codePointAt(6), data.codePointAt(7)), ns: to_int(data.codePointAt(8), data.codePointAt(9)),
ar: to_int(data.codePointAt(10), data.codePointAt(11)), data: data.slice(12), question: [], answers:[], authority: [], additional: [], offset: 0 };
return packet;
}
/**
* Parse the question section of a DNS request packet, adds the QNAME, QTYPE, and QCLASS to the packet object, and stores the
* offset in the packet for processing any further sections.
**/
function parse_question(packet) {
/** QNAME, QTYPE, QCLASS **/
var name = parse_label(packet);
packet.question = { name: name, type: to_int(packet.data.codePointAt(packet.offset++), packet.data.codePointAt(packet.offset++)),
class: to_int(packet.data.codePointAt(packet.offset++), packet.data.codePointAt(packet.offset++)), qend: packet.offset + 12 };
if ( packet.qd != 1 ) {
return false;
}
return true;
}
function parse_answers(packet, decode_level) {
// Process the question section if necessary
if ( packet.question.length == 0 ) {
parse_question(packet);
}
// Process answers
if ( packet.an > 0 && packet.answers.length == 0 ) {
packet.answers = parse_section(packet, packet.an, decode_level);
}
// If we didn't have any ttls in the packet, then cache for 5 minutes.
if (packet.min_ttl == 2147483647) {
packet.min_ttl = 300;
}
}
// Parse all sections of the packet
function parse_complete(packet, decode_level) {
// Process the question section if necessary
if ( packet.question.length == 0 ) {
parse_question(packet);
}
// Process answers
if ( packet.an > 0 && packet.answers.length == 0 ) {
packet.answers = parse_section(packet, packet.an, decode_level);
}
// Process authority
if ( packet.ns > 0 && packet.authority.length == 0) {
packet.authority = parse_section(packet, packet.ns, decode_level);
}
// Process Additional
if ( packet.ar > 0 && packet.additional.length == 0) {
packet.additional = parse_section(packet, packet.ar, decode_level);
}
// If we didn't have any ttls in the packet, then cache for 5 minutes.
if (packet.min_ttl == 2147483647) {
packet.min_ttl = 300;
}
}
function parse_section(packet, recs, decode_level) {
var rrs = [];
for (var i=0; i<recs; i++) {
var rec = parse_resource_record(packet, decode_level);
rrs.push(rec);
if ( rec.ttl < packet.min_ttl ) {
packet.min_ttl = rec.ttl;
}
}
return rrs;
}
function parse_label(packet) {
var name = "";
var compressed = false;
var pos = packet.offset;
for ( ; pos < packet.data.length; ) {
var length = packet.data.codePointAt(pos);
if (length == 0) {
// null label, name is finished
pos++;
break;
} else if ( length == 192 ) {
// compression pointer
if ( compressed ) {
pos++;
} else {
packet.offset = ++pos + 1;
}
pos = packet.data.codePointAt(pos);;
if ( pos < 12 ) {
// This shouldn't be possible, the header is 12 bytes so a compression pointer can't be less than 12
//s.warn("DNS Error - parse_label encountered impossible compression pointer");
break;
} else {
pos = pos - 12;
compressed = true;
}
} else if ( length > 63 ) {
// Invalid DNS name, individual labels are limited to 63 bytes.
//s.warn("DNS Error - parse_label encountered invaliad DNS name");
break;
} else {
name += packet.data.slice(++pos, pos+length) + ".";
pos += length;
}
}
if ( ! compressed ) {
packet.offset = pos
}
name = name.slice(0,-1);
return name;
}
/** TODO Check sizes on resources/packets
labels 63 octets or less
names 255 octets or less
TTL positive values of a signed 32 bit number.
UDP messages 512 octets or less
**/
function encode_label( name ) {
var data = String.bytesFrom([]);
name.split('.').forEach( function(part){
data += String.fromCodePoint(part.length);
data += part;
});
data += String.fromCodePoint(0);
return data;
}
function gen_resource_record(packet, name, type, clss, ttl, rdata) {
/**
NAME
TYPE (2 octets)
CLASS (2 octects)
TTL 32bit signed int
RDLength 16bit int length of RDATA
RDATA variable length string
**/
var resource = "";
var record = "";
if ( name == packet.question.name ) {
// The name matches the query, set a compression pointer.
resource += String.fromCodePoint(192, 12).toBytes();
} else {
// gen labels for the name
resource += encode_label(name);
}
resource += String.fromCodePoint(type & 0xff00, type & 0xff);
switch(type) {
case dns_type.A:
record = encode_arpa_v4(rdata);
break;
case dns_type.AAAA:
record = encode_arpa_v6(rdata);
break;
case dns_type.NS:
record = encode_label(rdata);
break;
case dns_type.CNAME:
record = encode_label(rdata);
break;
case dns_type.SOA:
record = encode_soa_record(rdata);
break;
case dns_type.SRV:
record = encode_srv_record(rdata);
break;
case dns_type.MX:
record = encode_mx_record(rdata);
break;
case dns_type.TXT:
record = encode_txt_record(rdata);
break;
default:
//TODO Barf
}
switch(clss) {
case dns_class.IN:
resource += String.fromCodePoint(0,1).toBytes();
break;
default:
//TODO Barf
resource += String.fromCodePoint(99,99).toBytes();
}
resource += to_bytes32(ttl);
resource += to_bytes( record.length );
resource += record;
return resource;
}
// Process resource records, to a varying depth dictated by decode_level
// decode_level {0: name+type, 1: name+type+class+ttl, 2: everything}
function parse_resource_record(packet, decode_level) {
/**
NAME
TYPE (2 octets)
CLASS (2 octects)
TTL 32bit signed int
RDLength 16bit int length of RDATA
RDATA variable length string
**/
var resource = {}
resource.name = parse_label(packet);
resource.type = to_int(packet.data.codePointAt(packet.offset++), packet.data.codePointAt(packet.offset++));
if ( decode_level > 0 ) {
if (resource.type == dns_type.OPT ) {
// EDNS
parse_edns_options(packet);
} else {
resource.class = to_int(packet.data.codePointAt(packet.offset++), packet.data.codePointAt(packet.offset++));
resource.ttl = to_int32(packet.data.codePointAt(packet.offset++), packet.data.codePointAt(packet.offset++),
packet.data.codePointAt(packet.offset++), packet.data.codePointAt(packet.offset++));
resource.rdlength = to_int(packet.data.codePointAt(packet.offset++), packet.data.codePointAt(packet.offset++));
if ( decode_level == 1 ) {
resource.data = packet.data.slice(packet.offset, packet.offset + resource.rdlength);
packet.offset += resource.rdlength;
} else {
switch(resource.type) {
case dns_type.A:
resource.data = parse_arpa_v4(packet, resource);
break;
case dns_type.AAAA:
resource.data = parse_arpa_v6(packet, resource);
break;
case dns_type.NS:
resource.data = parse_label(packet);
break;
case dns_type.CNAME:
resource.data = parse_label(packet);
break;
case dns_type.SOA:
resource.data = parse_soa_record(packet);
break;
case dns_type.SRV:
resource.data = parse_srv_record(packet);
break;
case dns_type.MX:
resource.data = parse_mx_record(packet);
break;
case dns_type.TXT:
resource.data = parse_txt_record(packet, resource.rdlength);
break;
default:
resource.data = packet.data.slice(packet.offset, packet.offset + resource.rdlength);
packet.offset += resource.rdlength;
}
}
}
}
return resource;
}
function encode_arpa_v4( ipv4 ) {
var rdata = "";
ipv4.split('\.').forEach( function(octet) {
rdata += String.fromCodePoint( octet ).toBytes();
});
return rdata;
}
function parse_arpa_v4(packet) {
var octet = [0,0,0,0];
for (var i=0; i< 4 ; i++ ) {
octet[i] = packet.data.codePointAt(packet.offset++);
}
return octet.join(".");
}
function encode_arpa_v6( ipv6 ) {
var rdata = "";
ipv6.split(':').forEach( function(segment) {
rdata += String.bytesFrom(segment[0] + segment[1], 'hex');
rdata += String.bytesFrom(segment[2] + segment[3], 'hex');
});
return rdata;
}
function parse_arpa_v6(packet) {
var ipv6 = "";
for (var i=0; i<8; i++ ) {
var a = packet.data.charCodeAt(packet.offset++).toString(16);
var b = packet.data.charCodeAt(packet.offset++).toString(16);
ipv6 += a + b + ":";
}
return ipv6.slice(0,-1);
}
function encode_txt_record( text_array ) {
var rdata = String.bytesFrom([]);
text_array.forEach( function(text) {
var tl = text.length;
if ( tl > 255 ) {
for (var i=0 ; i < tl ; i++ ) {
var len = (tl > (i+255)) ? 255 : tl - i;
rdata += String.fromCodePoint(len).toBytes();
rdata += text.slice(i,i+len);
i += len;
}
} else {
rdata += String.fromCodePoint(tl).toBytes();
rdata += text;
}
});
return rdata;
}
function parse_txt_record(packet, length) {
var txt = [];
var pos = 0;
while ( pos < length ) {
var tl = packet.data.codePointAt(packet.offset++);
txt.push( packet.data.slice(packet.offset, packet.offset + tl));
pos += tl + 1;
packet.offset += tl;
}
return txt;
}
function encode_mx_record( mx ) {
var rdata = String.bytesFrom([]);
rdata += to_bytes( mx.priority );
rdata += encode_label( mx.exchange );
return rdata;
}
function parse_mx_record(packet) {
var mx = {};
mx.priority = to_int(packet.data.codePointAt(packet.offset++), packet.data.codePointAt(packet.offset++));
mx.exchange = parse_label(packet);
return mx;
}
function encode_srv_record( srv ) {
var rdata = String.bytesFrom([]);
rdata += to_bytes( srv.priority );
rdata += to_bytes( srv.weight );
rdata += to_bytes( srv.port );
rdata += encode_label( srv.target );
return rdata;
}
function parse_srv_record(packet) {
var srv = {};
srv.priority = to_int(packet.data.codePointAt(packet.offset++), packet.data.codePointAt(packet.offset++));
srv.weight = to_int(packet.data.codePointAt(packet.offset++), packet.data.codePointAt(packet.offset++));
srv.port = to_int(packet.data.codePointAt(packet.offset++), packet.data.codePointAt(packet.offset++));
srv.target = parse_label(packet);
return srv;
}
function encode_soa_record( soa ) {
var rdata = String.bytesFrom([]);
rdata += encode_label(soa.primary);
rdata += encode_label(soa.mailbox);
rdata += to_bytes32(soa.serial);
rdata += to_bytes32(soa.refresh);
rdata += to_bytes32(soa.retry);
rdata += to_bytes32(soa.expire);
rdata += to_bytes32(soa.minTTL);
return rdata;
}
function parse_soa_record(packet) {
var soa = {};
soa.primary = parse_label(packet);
soa.mailbox = parse_label(packet);
soa.serial = to_int32(packet.data.codePointAt(packet.offset++), packet.data.codePointAt(packet.offset++),
packet.data.codePointAt(packet.offset++), packet.data.codePointAt(packet.offset++));
soa.refresh = to_int32(packet.data.codePointAt(packet.offset++), packet.data.codePointAt(packet.offset++),
packet.data.codePointAt(packet.offset++), packet.data.codePointAt(packet.offset++));
soa.retry = to_int32(packet.data.codePointAt(packet.offset++), packet.data.codePointAt(packet.offset++),
packet.data.codePointAt(packet.offset++), packet.data.codePointAt(packet.offset++));
soa.expire = to_int32(packet.data.codePointAt(packet.offset++), packet.data.codePointAt(packet.offset++),
packet.data.codePointAt(packet.offset++), packet.data.codePointAt(packet.offset++));
soa.minTTL = to_int32(packet.data.codePointAt(packet.offset++), packet.data.codePointAt(packet.offset++),
packet.data.codePointAt(packet.offset++), packet.data.codePointAt(packet.offset++));
return soa;
}
function parse_edns_options(packet) {
packet.edns = {}
packet.edns.opts = {}
packet.edns.size = to_int(packet.data.codePointAt(packet.offset++), packet.data.codePointAt(packet.offset++));
packet.edns.rcode = packet.data.codePointAt(packet.offset++);
packet.edns.version = packet.data.codePointAt(packet.offset++);
packet.edns.z = to_int(packet.data.codePointAt(packet.offset++), packet.data.codePointAt(packet.offset++));
packet.edns.rdlength = to_int(packet.data.codePointAt(packet.offset++), packet.data.codePointAt(packet.offset++));
var end = packet.offset + packet.edns.rdlength;
for ( ; packet.offset < end ; ) {
var opcode = to_int(packet.data.codePointAt(packet.offset++), packet.data.codePointAt(packet.offset++));
var oplength = to_int(packet.data.codePointAt(packet.offset++), packet.data.codePointAt(packet.offset++));
if ( opcode == 8 ) {
//client subnet
packet.edns.opts.csubnet = {}
packet.edns.opts.csubnet.family = to_int(packet.data.codePointAt(packet.offset++), packet.data.codePointAt(packet.offset++));
packet.edns.opts.csubnet.netmask = packet.data.codePointAt(packet.offset++);
packet.edns.opts.csubnet.scope = packet.data.codePointAt(packet.offset++);
if ( packet.edns.opts.csubnet.family == 1 ) {
// IPv4
var octet = [0,0,0,0];
for (var i=4; i< oplength ; i++ ) {
octet[i-4] = packet.data.codePointAt(packet.offset++);
}
packet.edns.opts.csubnet.subnet = octet.join(".");
break;
} else {
// We don't support IPv6 yet.
packet.edns.opts = {}
break;
}
} else {
// We only look for CSUBNET... Not interested in anything else at this time.
packet.offset += oplength;
}
}
}

@ -0,0 +1,54 @@
import glb from './dns/glb.js';
import dns from './dns/dns.js';
/**
* GLB Functions
**/
// GLB return the response packet to js_set
function glb_get_response(s) {
return glb.get_response(s);
}
// GLB setup the on(upload) callback to process DNS packets
function glb_process_request(s) {
return glb.process_request(s);
}
// GLB return the EDNS subnet to the js_set call for GEOIP2
function glb_get_edns_subnet(s) {
return glb.get_edns_subnet(s);
}
/**
* DNS Functions
**/
// DNS over HTTPS gateway - use as js_filter
function dns_filter_doh_request(s) {
return dns.filter_doh_request(s);
}
function dns_preread_doh_request(s) {
return dns.preread_doh_request(s);
}
function dns_preread_dns_request(s) {
return dns.preread_dns_request(s);
}
// Return the DNS Question
function dns_get_qname(s) {
return dns.get_qname(s);
}
// return the DNS Response, if we want to override (block) the domain
function dns_get_response(s) {
return dns.get_response(s);
}
//function dns_filter_udp_request(s) {
// return dns.filter_udp_request(s);
//}
Loading…
Cancel
Save