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