parent
af961b6f84
commit
7753f41b64
@ -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…
Reference in new issue