Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 131 additions & 0 deletions examples/nginx-doh-and-dot-to-dns-inject.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# inject CSUBNET into dns queries (useful to pass client identification i.e. while DoT reverse-proxying pihole)

user www-data;
worker_processes 1;
#worker_processes auto;

pid /run/nginx.pid;
error_log /var/log/nginx/dns-error.log notice;

include /etc/nginx/modules-enabled/*.conf;


events {
worker_connections 1024;
}

stream {

# DNS logging
log_format dns '$remote_addr [$time_local] $protocol $status $bytes_sent $bytes_received "$dns_qname" "$dns_client"';

access_log /var/log/nginx/dns-access.log dns;

# Import the NJS module
js_import /etc/nginx/njs.d/dns/dns.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;

# The $dns_client variable can be populated by preread calls, and can be used for DNS routing
js_set $dns_client dns.get_client;


### DNS over TLS reverse proxy ###

# DoT endpoint
server {
# listen to DNS over TLS
listen 853 ssl;

# SSL keys
ssl_certificate /etc/letsencrypt/live/pihole/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/pihole/privkey.pem; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot

# SSL settings
#ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; # Dropping SSLv3, ref: POODLE
ssl_protocols TLSv1.3;
ssl_prefer_server_ciphers on;

# listen to DNS over TCP
#listen 53;
# listen to DNS over UDP
#listen 53 udp;
# treat single udp packets (enable for UDP)
#proxy_responses 1;

# handle DoT request
js_preread dns.preread_dns_request;

# Upstream can be either DNS(TCP) or DoT.
proxy_pass 127.0.0.1:53;
# If upstream is DNS, proxy_ssl should be off.
#proxy_ssl on;
}


### DNS over HTTPS reverse proxy ###

server {
# dohloop
listen unix:/tmp/dohloop.socket;
#listen 127.0.0.1:44353;
# handle DoH request
js_filter dns.filter_doh_request;

# Upstream can be either DNS(TCP) or DoT.
proxy_pass 127.0.0.1:53;
# If upstream is DNS, proxy_ssl should be off.
#proxy_ssl on;
}

}

http {
# DoH endpoint
server {
# listen to DNS over HTTPS
listen 443 ssl http2;
# listen to DNS over plain HTTP
#listen 80 http2;
# DoH requires HTTP/2
#http2 on; # nginx>1.25

# SSL keys
ssl_certificate /etc/letsencrypt/live/pihole/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/pihole/privkey.pem; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot

# SSL settings
#ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; # Dropping SSLv3, ref: POODLE
ssl_protocols TLSv1.3;
ssl_prefer_server_ciphers on;

# Return 404 to all responses, except for those using our published DoH URI
location / {
return 404 "404 Not Found\n";
}

location /dns-query {
# log settings off in favor of stream access log on stream
access_log off;

# Proxy HTTP/1.1
proxy_http_version 1.1;
# clear the connection header to enable Keep-Alive
proxy_set_header Connection "";
# pass client address
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# proxy to dohloop
proxy_pass http://unix:/tmp/dohloop.socket;
#proxy_pass http://127.0.0.1:44353;
}

location /version {
js_import /etc/nginx/njs.d/utils.js;
js_content utils.version;
}
}
}

85 changes: 68 additions & 17 deletions njs.d/dns/dns.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import dns from "libdns.js";
export default {get_qname, get_response, preread_doh_request, preread_dns_request, filter_doh_request};
export default {get_qname, get_client, get_response, preread_doh_request, preread_dns_request, filter_doh_request};

/**
* DNS Decode Level
Expand All @@ -17,6 +17,14 @@ var dns_decode_level = 3;
**/
var dns_debug_level = 3;

/**
* DNS Inject Client
* Set this to true if you want to inject the client address into the DNS query.
* Doing so will inject the client IP address as CSUBNET in EDNS(0).
* With this you can track clients on your DNS server and selectively blocklist.
* Some DNS servers like pihole feature client specific blocklists out of the box.
**/
var dns_inject_client = false;
/**
* DNS Question Load Balancing
* Set this to true, if you want to pick the upstream pool based on the DNS Question.
Expand All @@ -27,10 +35,18 @@ var dns_question_balancing = false;
// The DNS Question name
var dns_name = Buffer.alloc(0);

// The DNS client address
var dns_client_ip;


function get_qname(s) {
return dns_name;
}

function get_client(s) {
return dns_client_ip;
}

// The Optional DNS response, this is set when we want to block a specific domain
var dns_response = Buffer.alloc(0);

Expand All @@ -54,15 +70,12 @@ function process_doh_request(s, decode, scrub) {
if ( data.length == 0 ) {
return;
}
var dataString = data.toString('utf8');
const lines = dataString.split("\r\n");
var bytes;
var packet;
if(lines[0].startsWith("GET")) {
var line = lines[0];
var path = line.split(" ")[1]
var params = path.split("?")[1]
var qs = params.split("&");
if(data.toString('utf8', 0, 3) == "GET") {
const path = data.toString('utf8', 4, data.indexOf(' ', 4));
const params = path.split("?")[1]
const qs = params.split("&");
qs.some( param => {
if (param.startsWith("dns=") ) {
bytes = Buffer.from(param.slice(4), "base64url");
Expand All @@ -72,15 +85,8 @@ function process_doh_request(s, decode, scrub) {
});
}

if(lines[0].startsWith("POST")) {
const index = lines.findIndex(line=>{
if(line.length == 0) {
return true;
}
})
if(index>0 && lines.length >= index + 1){
bytes = Buffer.from(lines[index + 1]);
}
if(data.toString('utf8', 0, 4) == "POST") {
bytes = data.slice(data.indexOf('\r\n\r\n') + 4);
}

if (bytes) {
Expand All @@ -92,6 +98,15 @@ function process_doh_request(s, decode, scrub) {
debug(s,"process_doh_request: DNS Req Name: " + packet.question.name);
dns_name = packet.question.name;
}
if (dns_inject_client) {
packet = packet ?? dns.parse_packet(bytes);
const index_xforwardedfor = data.indexOf('\r\nX-Forwarded-For: ') + 19;
dns_client_ip = data.toString(
'utf8', index_xforwardedfor, data.indexOf('\r\n', index_xforwardedfor)
).split(',')[0];
debug(s, "process_doh_request: DNS Client IP: \"" + dns_client_ip + "\"");
bytes = inject_client(s, bytes, packet, dns_client_ip);
}
if (scrub) {
domain_scrub(s, bytes, packet);
s.done();
Expand Down Expand Up @@ -128,6 +143,12 @@ function process_dns_request(s, decode, scrub) {
debug(s,"process_dns_request: DNS Req Name: " + packet.question.name);
dns_name = packet.question.name;
}
if (dns_inject_client) {
packet = packet ?? dns.parse_packet(bytes);
dns_client_ip = s.variables.remote_addr;
debug(s, "process_dns_request: DNS Client IP: \"" + dns_client_ip + "\"");
bytes = inject_client(s, bytes, packet, dns_client_ip);
}
if (scrub) {
domain_scrub(s, bytes, packet);
s.done();
Expand Down Expand Up @@ -269,3 +290,33 @@ function filter_doh_request(s) {
});
}

/**
* Inject client address as CSUBNET to DNS query
**/
function inject_client(s, data, packet, client_ip) {

dns.parse_complete(packet, 2);

packet.edns = packet.edns ?? { opts: {} };

const has_csubnet = 'csubnet' in packet.edns.opts;
if ( has_csubnet ) {
dns_client_ip = packet.edns.opts.csubnet.subnet + " via " + dns_client_ip;
debug(s, "inject client: query already has CSUBNET: " + packet.edns.opts.csubnet.subnet + "/" + packet.edns.opts.csubnet.netmask);
} else {
const is_ipv4 = dns.is_ipv4(client_ip);
debug(s, "inject_client: injecting client address: " + client_ip);
packet.edns.opts.csubnet = {
family: (is_ipv4 ? "1" : "2"),
netmask: (is_ipv4 ? "32" : "128"),
scope: 0,
subnet: client_ip
};
data = dns.encode_packet(packet);
debug(s, "inject_client: DNS Request Packet: " + JSON.stringify( Object.entries(packet)) );
debug(s, "inject_client: DNS Req: " + data.toString('hex') );
}

return data;
}

Loading