From 7753f41b6476f23aabbdde9e61b33459e0e38df1 Mon Sep 17 00:00:00 2001 From: meliurwen Date: Tue, 28 Dec 2021 00:12:14 +0100 Subject: [PATCH] Switched to nginx for doh --- nginx/Dockerfile | 15 + nginx/root/entrypoint.sh | 13 + nginx/root/etc/nginx/conf.d/realip.conf | 14 + nginx/root/etc/nginx/nginx.template | 137 +++++ nginx/root/etc/nginx/njs.d/dns/dns.js | 295 ++++++++++ nginx/root/etc/nginx/njs.d/dns/glb.js | 205 +++++++ nginx/root/etc/nginx/njs.d/dns/libdns.js | 632 +++++++++++++++++++++ nginx/root/etc/nginx/njs.d/nginx_stream.js | 54 ++ 8 files changed, 1365 insertions(+) create mode 100644 nginx/Dockerfile create mode 100755 nginx/root/entrypoint.sh create mode 100644 nginx/root/etc/nginx/conf.d/realip.conf create mode 100644 nginx/root/etc/nginx/nginx.template create mode 100644 nginx/root/etc/nginx/njs.d/dns/dns.js create mode 100644 nginx/root/etc/nginx/njs.d/dns/glb.js create mode 100644 nginx/root/etc/nginx/njs.d/dns/libdns.js create mode 100644 nginx/root/etc/nginx/njs.d/nginx_stream.js diff --git a/nginx/Dockerfile b/nginx/Dockerfile new file mode 100644 index 0000000..cc0f3df --- /dev/null +++ b/nginx/Dockerfile @@ -0,0 +1,15 @@ +ARG IMAGE +ARG TAG + +FROM ${IMAGE}:${TAG} + +LABEL maintainer="Meliurwen " + +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"] diff --git a/nginx/root/entrypoint.sh b/nginx/root/entrypoint.sh new file mode 100755 index 0000000..5509c3a --- /dev/null +++ b/nginx/root/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;' diff --git a/nginx/root/etc/nginx/conf.d/realip.conf b/nginx/root/etc/nginx/conf.d/realip.conf new file mode 100644 index 0000000..7203bef --- /dev/null +++ b/nginx/root/etc/nginx/conf.d/realip.conf @@ -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; diff --git a/nginx/root/etc/nginx/nginx.template b/nginx/root/etc/nginx/nginx.template new file mode 100644 index 0000000..bb76139 --- /dev/null +++ b/nginx/root/etc/nginx/nginx.template @@ -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; + } + +} + diff --git a/nginx/root/etc/nginx/njs.d/dns/dns.js b/nginx/root/etc/nginx/njs.d/dns/dns.js new file mode 100644 index 0000000..54ff1ed --- /dev/null +++ b/nginx/root/etc/nginx/njs.d/dns/dns.js @@ -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') ); +} diff --git a/nginx/root/etc/nginx/njs.d/dns/glb.js b/nginx/root/etc/nginx/njs.d/dns/glb.js new file mode 100644 index 0000000..416256a --- /dev/null +++ b/nginx/root/etc/nginx/njs.d/dns/glb.js @@ -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; +} + diff --git a/nginx/root/etc/nginx/njs.d/dns/libdns.js b/nginx/root/etc/nginx/njs.d/dns/libdns.js new file mode 100644 index 0000000..a555e91 --- /dev/null +++ b/nginx/root/etc/nginx/njs.d/dns/libdns.js @@ -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 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; + } + } + +} + + diff --git a/nginx/root/etc/nginx/njs.d/nginx_stream.js b/nginx/root/etc/nginx/njs.d/nginx_stream.js new file mode 100644 index 0000000..b181dbc --- /dev/null +++ b/nginx/root/etc/nginx/njs.d/nginx_stream.js @@ -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); +//} +