Repository: TuxInvader/nginx-dns Branch: master Commit: 29ee0d7e45e4 Files: 19 Total size: 75.2 KB Directory structure: gitextract_h3c4240l/ ├── LICENSE ├── README.md ├── docs/ │ └── nginx-dns-over-https.md ├── examples/ │ ├── nginx-dns-routing.conf │ ├── nginx-dns-simple.conf │ ├── nginx-doh-and-dot-to-dns.conf │ ├── nginx-doh-and-dot-to-dot.conf │ ├── nginx-dot-to-dns-simple.conf │ ├── nginx-dot-to-dot-routing.conf │ ├── nginx-plus-filtering.conf │ └── test.conf ├── nginx-doh.conf ├── nginx-glb.conf ├── njs.d/ │ └── dns/ │ ├── dns.js │ ├── glb.js │ ├── libdns.js │ └── test.js └── ssl/ ├── certs/ │ └── doh.local.pem └── private/ └── doh.local.pem ================================================ FILE CONTENTS ================================================ ================================================ FILE: LICENSE ================================================ BSD 2-Clause License Copyright (c) 2022, Mark Boddington All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: README.md ================================================ # NGINX DNS (DNS/DoT/DoH) > The `v2` branch is migrating to `Buffers` due to NJS deprecating the `String` byte-array functions. > Please test and raise issues if you find them. Thank you! This repository contains some NJS code, and example configuration files for using NGINX with DNS services. NGINX can be used to perform load balancing for DNS (TCP/UDP), and also DNS over TLS (DoT) and DNS over HTTPS (DoH) NGINX can also be used to provide Global Server Load Balancing (GSLB). See the example configuration files in the [examples](examples) folder. ## Setup Copy the njs.d folder into /etc/nginx/ and one of the NGINX DoH [examples](examples) to /etc/nginx/nginx.conf The ssl folder contains a test certificate, you will likely want to generate and use your own certificate and update the nginx.conf file accordingly. ## Simple DNS NGINX can do simple DNS load balancing, without the need for NJS, using the standard Stream module directives. ``` stream { # DNS upstream pool. upstream dns { zone dns 64k; server 8.8.8.8:53; } # DNS Server. Listens on both TCP and UDP server { listen 53; listen 53 udp; proxy_responses 1; proxy_pass dns; } } ``` However if you want to carry out layer 7 inspection of the DNS traffic for logging or routing purposes, then you will need to use the NJS module included in this repository. To perform DNS routing, you need to make a `js_preread` function call in the server context, and use a `js_set` function with a `map`. For example: ``` stream { js_import /etc/nginx/njs.d/dns/dns.js; js_set $dns_qname dns.get_qname; map $dns_qname $upstream_pool { hostnames; *.nginx one; default two; } upstream one { ... } upstream two { ... } server { listen 53 udp; js_preread dns.preread_dns_request; proxy_responses 1; proxy_pass $upstream_pool; } } ``` ## DNS over TLS (DoT) and DNS over HTTPS (DoH) Gateway NGINX can act as a DNS(TCP) <-> DNS over TLS (DoT) gateway without any NJS functions. Eg: ``` upstream dns { zone dns 64k; server 8.8.8.8:53; } upstream dot { zone dot 64k; server 8.8.8.8:853; } server { listen 53; listen 853 ssl; ssl_certificate /etc/nginx/ssl/certs/doh.local.pem; ssl_certificate_key /etc/nginx/ssl/private/doh.local.pem; proxy_ssl on; proxy_pass dot; } ``` The above example will accpet DNS and DoT requests, and forward them to a DoT upstream. If your upstream is DNS, and you want to terminate DoT on NGINX, then remove the `proxy_ssl on;` directive, and change the `proxy_pass` directive to use the standard DNS upstream. NJS is required if you want to act as a gateway between DoH and DNS/DoT. See the example configuration files in the [examples](examples) folder. The full configuration has a HTTP/2 service listening for requests, and does a proxy_pass for requests to /dns-query. We proxy to an internal stream service on port 8053, which uses js_filter to pull out the DNS packet from the HTTP wrapper, and forward onto an upstream DNS(TCP) or DoT server. The result is then wrapped back up in a HTTP response and passed to the HTTP/2 service for delivery to the client. NGINX can log as much or as little as you like, and the NJS allows you to process information in the DNS requests and responses. See: [docs/nginx-dns-over-https](docs/nginx-dns-over-https.md) for more information ## NGINX GSLB (work-in-progress) Use the nginx-glb.conf file to run an GSLB service. Copy the njs.d folder into /etc/nginx/ and the nginx-glb.conf to /etc/nginx/nginx.conf TODO - Describe the example configuration and how to customise it. ================================================ FILE: docs/nginx-dns-over-https.md ================================================ ## DNS over HTTPS (DoH) Gateway Use the nginx-doh.conf file to run a DoH gateway. Copy the njs.d and ssl folders into /etc/nginx/ and the nginx-doh.conf to /etc/nginx/nginx.conf ### Simple DNS(TCP) and DNS over TLS (DoT) NGINX can act as a DNS(TCP) <-> DNS over TLS (DoT) gateway without any NJS functions. Eg: ``` upstream dns { zone dns 64k; server 8.8.8.8:53; } upstream dot { zone dot 64k; server 8.8.8.8:853; } server { listen 53; listen 853 ssl; ssl_certificate /etc/nginx/ssl/certs/doh.local.pem; ssl_certificate_key /etc/nginx/ssl/private/doh.local.pem; proxy_ssl on; proxy_pass dot; } ``` ### DNS over HTTPS (DoH) NJS is required if you want to act as a gateway between DoH and DNS/DoT. In this case we need some NJS code, and the full configuration in nginx-glb.conf. The full configuration has a HTTP/2 service listening for requests, and does a proxy_pass for requests to /dns-query. We proxy to an internal stream service on port 8053, which uses js_filter to pull out the DNS packet from the HTTP wrapper, and forward onto an upstream DNS(TCP) or DoT server. The result is then wrapped back up in a HTTP response and passed to the HTTP/2 service for delivery to the client. #### NGINX Stream Server for back-end Lets look at the stream service first: ``` server { listen 127.0.0.1:8053; js_filter dns.filter_doh_request; proxy_ssl on; proxy_pass dot; } ``` We listen on the loopback interface on port 8053. HTTP/1.0 requests will be passed to us from the NGINX http service. The `js_filter` will find the DNS packet encoded in the request, and forward it on to the upstream DoT service. #### NGINX HTTP/2 service for the front-end Traffic arrives at the stream service via a HTTP/2 server: ``` upstream dohloop { zone dohloop 64k; server 127.0.0.1:8053; } proxy_cache_path /var/cache/nginx/doh_cache levels=1:2 keys_zone=doh_cache:10m; server { listen 443 ssl http2; ssl_certificate /etc/nginx/ssl/certs/doh.local.pem; ssl_certificate_key /etc/nginx/ssl/private/doh.local.pem; proxy_cache_methods GET POST; location / { return 404 "404 Not Found\n"; } location /dns-query { proxy_http_version 1.0; proxy_cache doh_cache; proxy_cache_key $scheme$proxy_host$uri$is_args$args$request_body; proxy_pass http://dohloop; } } ``` We listen on the standard HTTPS port for incoming http requests. We return a 404 response to all requests except for those which match our `/dns-query` location. All dns-queries are forwarded onto the stream service as HTTP/1.0 requests. #### NGINX Processing Options The NJS code can perform varying degress of processing on the DNS packets. The fastest for DNS is to do no processing (level 0), but enabling some processing (level 2) allows NGINX to gather the necessary intelligence (Resource Record TTLs) to enable a HTTP Content-Cache for the DoH requests. At levels less than 2, we will cache responses, but for just 10 seconds. Change this setting in the `njs.d/dns/doh.js` file ``` /** * 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 according to TTL. * 3: Very Verbose, log everything as above, but also write packet data to error log (slowest) **/ var $dns_decode_level = 0; ``` ================================================ FILE: examples/nginx-dns-routing.conf ================================================ 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; } stream { # logging log_format dns '$remote_addr [$time_local] $protocol "$dns_qname" "$upstream_pool"'; access_log /var/log/nginx/dns-access.log dns; # import NJS module js_import /etc/nginx/njs.d/dns/dns.js; # NJS function to get the dns_qname, requires a js_preread in the server to populate the variable from the DNS packet js_set $dns_qname dns.get_qname; # This maps the qname domain to the DNS server for routing map $dns_qname $upstream_pool { hostnames; *.nginx dnsmasq; *.k8s dnsmasq; default google; } # Google upstream upstream google { zone dns 64k; server 8.8.8.8:53; } # dnsmasq local upstream upstream dnsmasq { zone dns 64k; server 192.168.64.1:53; } # DNS(TCP) Serverr server { listen 53; js_preread dns.preread_dns_request; proxy_pass $upstream_pool; } # DNS(UDP) Server server { listen 53 udp; js_preread dns.preread_dns_request; proxy_responses 1; proxy_pass $upstream_pool; } } ================================================ FILE: examples/nginx-dns-simple.conf ================================================ user nginx; worker_processes auto; error_log /var/log/nginx/error.log notice; pid /var/run/nginx.pid; events { worker_connections 1024; } stream { # DNS upstream pool. upstream dns { zone dns 64k; server 8.8.8.8:53; } # DNS Server. Listens on both TCP and UDP server { listen 53; listen 53 udp; proxy_responses 1; proxy_pass dns; } } ================================================ FILE: examples/nginx-doh-and-dot-to-dns.conf ================================================ 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; # The DoH server block server { # Listen on standard HTTPS port, and accept HTTP2, with SSL termination listen 443 ssl http2; ssl_certificate /etc/nginx/ssl/certs/doh.local.pem; ssl_certificate_key /etc/nginx/ssl/private/doh.local.pem; ssl_session_cache shared:ssl_cache:10m; ssl_session_timeout 10m; # 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 /dns-query { # 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; } } } # 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; # 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; # DNS upstream pool. upstream dns { zone dns 64k; server 8.8.8.8:53; } # DNS(TCP) and DNS over TLS (DoT) Server # Terminate DoT and DNS TCP, and proxy onto standard DNS server { listen 53; listen 853 ssl; ssl_certificate /etc/nginx/ssl/certs/doh.local.pem; ssl_certificate_key /etc/nginx/ssl/private/doh.local.pem; 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 # Upstream can be either DNS(TCP) or DoT. If upstream is DNS, proxy_ssl should be off. server { listen 127.0.0.1:8053; js_filter dns.filter_doh_request; proxy_pass dns; } } ================================================ FILE: examples/nginx-doh-and-dot-to-dot.conf ================================================ 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; # The DoH server block server { # Listen on standard HTTPS port, and accept HTTP2, with SSL termination listen 443 ssl http2; ssl_certificate /etc/nginx/ssl/certs/doh.local.pem; ssl_certificate_key /etc/nginx/ssl/private/doh.local.pem; ssl_session_cache shared:ssl_cache:10m; ssl_session_timeout 10m; # 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 /dns-query { # 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; } } } # 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; # 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; # DNS over TLS upstream pool upstream dot { zone dot 64k; server 8.8.8.8:853; } # DNS(TCP) and DNS over TLS (DoT) Server # Upstream can be either DNS(TCP) or DoT. If upstream is DNS, proxy_ssl should be off. server { # DNS TCP listen 53; # DNS DoT listen 853 ssl; ssl_certificate /etc/nginx/ssl/certs/doh.local.pem; ssl_certificate_key /etc/nginx/ssl/private/doh.local.pem; # This is used to pull out question for logging js_preread dns.preread_dns_request; # Enable SSL re-encryption for DoT connection upstream proxy_ssl on; proxy_pass dot; } # DNS over HTTPS (gateway) Service # Upstream can be either DNS(TCP) or DoT. If upstream is DNS, proxy_ssl should be off. server { listen 127.0.0.1:8053; js_filter dns.filter_doh_request; proxy_ssl on; proxy_pass dot; } } ================================================ FILE: examples/nginx-dot-to-dns-simple.conf ================================================ 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; } # DNS Stream Services stream { # DNS upstream pool. upstream dns { zone dns 64k; server 8.8.8.8:53; } # DNS(TCP) and DNS over TLS (DoT) Server # Terminates DNS and DoT, then proxies on to standard DNS. server { listen 53; listen 853 ssl; ssl_certificate /etc/nginx/ssl/certs/doh.local.pem; ssl_certificate_key /etc/nginx/ssl/private/doh.local.pem; proxy_pass dns; } # DNS(UDP) Server server { listen 53 udp; proxy_responses 1; proxy_pass dns; } } ================================================ FILE: examples/nginx-dot-to-dot-routing.conf ================================================ 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; } # DNS Stream Services stream { # DNS logging log_format dns '$remote_addr [$time_local] $protocol "$dns_qname" "$upstream_pool"'; 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 will be populated by preread calls, and used for DNS routing js_set $dns_qname dns.get_qname; # When doing DNS routing, use $dns_qname to map the questions to the upstream pools. map $dns_qname $upstream_pool { hostnames; *.nginx dnsmasq; *.k8s dnsmasq; default google; } # upstream pools (google DoT) upstream google { zone dns 64k; server 8.8.8.8:853; } # upstream pools (another DoT) upstream dnsmasq { zone dns 64k; server 192.168.64.1:853; } # DNS(TCP) and DNS over TLS (DoT) Server # Upstream can be either DNS(TCP) or DoT. If upstream is DNS, proxy_ssl should be off. server { listen 53; listen 853 ssl; ssl_certificate /etc/nginx/ssl/certs/doh.local.pem; ssl_certificate_key /etc/nginx/ssl/private/doh.local.pem; js_preread dns.preread_dns_request; proxy_ssl on; proxy_pass $upstream_pool; } } ================================================ FILE: examples/nginx-plus-filtering.conf ================================================ # # This config shows an example of filtering DNS requests using the Key/value store available in NGINX Plus # Push FQDNs into the dns_config key/value zone with a value of "blocked" or "blackhole" to have them scrubbed from DNS # Alternatively push a CSV list of domains as the value to either "blocked_domains" or "blackhole_domains" to have # any requests for records within those zones scrubbed. # 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; } # DNS Stream Services stream { # KeyValue store for blocking domains (NGINX Plus only) keyval_zone zone=dns_config:64k state=/etc/nginx/zones/dns_config.zone; keyval "blocked_domains" $blocked_domains zone=dns_config; keyval "blackhole_domains" $blackhole_domains zone=dns_config; keyval $dns_qname $scrub_action zone=dns_config; # DNS logging log_format dns '$remote_addr [$time_local] $protocol "$dns_qname" "$upstream_pool"'; 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 response packet, if we're blocking the domain, this will be set. js_set $dns_response dns.get_response; # When doing DNS routing, use $dns_qname to map the questions to the upstream pools. map $dns_qname $upstream { hostnames; *.nginx dnsmasq; *.k8s dnsmasq; default google; } # Set upstream to be the pool defined above if dns_response is empty, else pass to the block/blackhole upstream map $dns_response $upstream_pool { "blocked" blocked; "blackhole" blackhole; default $upstream; } # upstream pool for blocked requests (returns nxdomain) upstream blocked { zone blocked 64k; server 127.0.0.1:9953; } # upstream pool for blacholed requests (returns 0.0.0.0) upstream blackhole { zone blackhole 64k; server 127.0.0.1:9853; } # upstream pools (google DNS) upstream google { zone dns 64k; server 8.8.8.8:53; } # upstream pools (another DNS) upstream dnsmasq { zone dns 64k; server 192.168.64.1:5353; } # DNS(TCP) and DNS over TLS (DoT) Server # Upstream can be either DNS(TCP) or DoT. If upstream is DNS, proxy_ssl should be off. server { listen 53; listen 853 ssl; ssl_certificate /etc/nginx/ssl/certs/doh.local.pem; ssl_certificate_key /etc/nginx/ssl/private/doh.local.pem; js_preread dns.preread_dns_request; proxy_pass $upstream_pool; } # DNS(UDP) Server # Upstream can only be another DNS(UDP) server. server { listen 53 udp; js_preread dns.preread_dns_request; proxy_responses 1; proxy_pass $upstream_pool; } # Server for responding to blocked/blackholed responses server { listen 127.0.0.1:9953; listen 127.0.0.1:9853; listen 127.0.0.1:9953 udp; listen 127.0.0.1:9853 udp; js_preread dns.preread_dns_request; return $dns_response; } } ================================================ FILE: examples/test.conf ================================================ 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 { } # DNS Stream Services stream { # DNS logging log_format dns '$remote_addr [$time_local] $protocol $status $bytes_sent $bytes_received "$dns_qname" "$upstream_addr"'; access_log /var/log/nginx/dns-access.log dns; # Import the NJS module js_import /etc/nginx/njs.d/dns/test.js; # The $dns_qname variable can be populated by preread calls, and can be used for DNS routing js_set $dns_qname test.get_qname; # test server - responds to dns queries server { listen 5553; listen 5553 udp; js_var $test_result; js_preread test.test_dns_encoder; return $test_result; } # load balance to test server and parse responses server { listen 5554; listen 5554 udp; proxy_responses 1; js_filter test.test_dns_decoder; proxy_pass 127.0.0.1:5553; } } ================================================ FILE: nginx-doh.conf ================================================ # # This config has bits of DNS/DoT/DoH all over it. See the examples folder for more targeted examples. # 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; # The DoH server block server { # Listen on standard HTTPS port, and accept HTTP2, with SSL termination listen 443 ssl http2; ssl_certificate /etc/nginx/ssl/certs/doh.local.pem; ssl_certificate_key /etc/nginx/ssl/private/doh.local.pem; ssl_session_cache shared:ssl_cache:10m; ssl_session_timeout 10m; # 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 /dns-query { # 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; } } # enable API server { listen 8080; location /api { api write=on; allow 127.0.0.1; allow 192.168.64.1; deny all; } } } # DNS Stream Services stream { # KeyValue store for blocking domains (NGINX Plus only) keyval_zone zone=dns_config:64k state=/etc/nginx/zones/dns_config.zone; keyval "blocked_domains" $blocked_domains zone=dns_config; keyval "blackhole_domains" $blackhole_domains zone=dns_config; keyval $dns_qname $scrub_action zone=dns_config; # DNS logging log_format dns '$remote_addr [$time_local] $protocol $status $bytes_sent $bytes_received "$dns_qname" "$upstream_addr"'; access_log /var/log/nginx/dns-access.log dns; # Import the NJS DNS 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 response packet, if we're blocking the domain, this will be set. js_set $dns_response dns.get_response; # When doing DNS routing, use $dns_qname to map the questions to the upstream pools. map $dns_qname $upstream { hostnames; *.nginx dnsmasq; *.k8s dnsmasq; default google; } # Set upstream to be the pool defined above if dns_response is empty, else pass to the @block location map $dns_response $upstream_pool { "blocked" blocked; "blackhole" blackhole; default $upstream; } # upstream pool for blocked requests upstream blocked { zone blocked 64k; server 127.0.0.1:9953; } upstream blackhole { zone blackhole 64k; server 127.0.0.1:9853; } # upstream pools (google DNS) upstream google { zone dns 64k; server 8.8.8.8:53; } # upstream pools (another DNS) upstream dnsmasq { zone dns 64k; server 192.168.64.1:5353; } # DNS upstream pool. upstream dns { zone dns 64k; server 8.8.8.8:53; } # DNS over TLS upstream pool upstream dot { zone dot 64k; server 8.8.8.8:853; } # DNS(TCP) and DNS over TLS (DoT) Server # Upstream can be either DNS(TCP) or DoT. If upstream is DNS, proxy_ssl should be off. server { listen 553; listen 853 ssl; ssl_certificate /etc/nginx/ssl/certs/doh.local.pem; ssl_certificate_key /etc/nginx/ssl/private/doh.local.pem; js_preread dns.preread_dns_request; #proxy_ssl on; proxy_pass $upstream_pool; } # DNS(UDP) Server # Upstream can only be another DNS(UDP) server. server { listen 553 udp; js_preread dns.preread_dns_request; proxy_responses 1; proxy_pass $upstream_pool; } # DNS over HTTPS (gateway) Service # Upstream can be either DNS(TCP) or DoT. If upstream is DNS, proxy_ssl should be off. server { listen 127.0.0.1:8053; js_filter dns.filter_doh_request; proxy_ssl on; proxy_pass dot; } # Server for sending blackhole/blocked responses server { listen 127.0.0.1:9953; listen 127.0.0.1:9853; listen 127.0.0.1:9953 udp; listen 127.0.0.1:9853 udp; js_preread dns.preread_dns_request; return $dns_response; } } ================================================ FILE: nginx-glb.conf ================================================ user nginx; worker_processes auto; load_module modules/ngx_stream_js_module.so; load_module modules/ngx_stream_geoip2_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; log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; access_log /var/log/nginx/access.log main; sendfile on; #tcp_nopush on; keepalive_timeout 65; #gzip on; upstream www_netflix_com { zone upstream_www_netflix_com 64k; server 52.49.96.37; # eu-west-1 server 52.51.179.14 backup; server 52.32.190.151; # us-west-2 server 52.41.193.16 backup; } match server_ok { status 200-399; } server { listen 127.0.0.1:8888; server_name www.netflix.com; set $test "foobar"; location / { proxy_pass http://www_netflix_com; proxy_set_header Host $host; proxy_http_version 1.1; health_check interval=10 fails=3 passes=3 match=server_ok; } } server { listen 127.0.0.1:80; location /api/ { api write=on; allow 127.0.0.1; deny all; } } } stream { js_include /etc/nginx/njs.d/nginx_stream.js; js_set $glb_response glb_get_response; js_set $edns_subnet glb_get_edns_subnet; keyval_zone zone=glb_config:64k state=/etc/nginx/zones/glb_config.zone; keyval "www_netflix_com" $www_netflix_com zone=glb_config; keyval "GLB_USE_EDNS" $glb_use_edns zone=glb_config; # The following keys are needed if s.api() is unavailable keyval "www_netflix_com_nodes" $www_netflix_com_nodes zone=glb_config; keyval "www_netflix_com_geoip_52_49_96_37" $www_netflix_com_geoip_52_49_96_37 zone=glb_config; keyval "www_netflix_com_geoip_52_51_179_14" $www_netflix_com_geoip_52_51_179_14 zone=glb_config; keyval "www_netflix_com_geoip_52_32_190_151" $www_netflix_com_geoip_52_32_190_151 zone=glb_config; keyval "www_netflix_com_geoip_52_41_193_16" $www_netflix_com_geoip_52_41_193_16 zone=glb_config; # set $geoip_source to EDNS if we have one, or the $remote_addr if not map $edns_subnet $geoip_source { "" $remote_addr; default $edns_subnet; } # get the country, latitude, and longitude from the GeoLite2 City DB geoip2 /etc/geoip/GeoLite2-City.mmdb { $geoip2_country_code default=GB source=$geoip_source country iso_code; $geoip2_latitude default=51.52830 source=$geoip_source location latitude; $geoip2_longitude default=0.0000 source=$geoip_source location longitude; } # process the DNS request server { listen 192.168.64.20:53 udp reuseport; js_preread glb_process_request; return $glb_response; } } ================================================ FILE: njs.d/dns/dns.js ================================================ 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 = 3; /** * DNS Debug Level * Specify the decoding level at which we should log packet data to the error log. * Default is level 3 (max decoding) **/ var dns_debug_level = 3; /** * 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 = Buffer.alloc(0); 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 = Buffer.alloc(0); function get_response(s) { return dns_response.toString(); } // Encode the given number to two bytes (16 bit) function to_bytes( number ) { return Buffer.from( [ ((number>>8) & 0xff), (number & 0xff) ] ); } function debug(s, msg) { if ( dns_decode_level >= dns_debug_level ) { s.warn(msg); } } function process_doh_request(s, decode, scrub) { s.on("upstream", function(data,flags) { 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("&"); qs.some( param => { if (param.startsWith("dns=") ) { bytes = Buffer.from(param.slice(4), "base64url"); return true; } return false; }); } 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 (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("upstream", 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 = Buffer.concat( [ 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 = Buffer.concat( [ 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("downstream", 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.rdata + "]," }) 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(); } }); } ================================================ FILE: njs.d/dns/glb.js ================================================ /** 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; } ================================================ FILE: njs.d/dns/libdns.js ================================================ /** 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, HTTPS: 65, AXFR: 252, ANY: 255, value: { 1:"A", 2:"NS", 5:"CNAME", 6:"SOA", 12:"PTR", 15:"MX", 16:"TXT", 28:"AAAA", 33:"SRV", 41:"OPT", 65:"HTTPS", 252:"AXFR", 255:"ANY" } }); // 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" } }); // Encode the given number to two bytes (16 bit) function to_bytes( number ) { return Buffer.from( [ ((number>>8) & 0xff), (number & 0xff) ] ); } // Encode the given number to 4 bytes (32 bit) function to_bytes32( number ) { return Buffer.from( [ (number>>24)&0xff, (number>>16)&0xff, (number>>8)&0xff, number&0xff ] ); } // 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 = Buffer.from( to_bytes( packet.id ) ); encoded = Buffer.concat( [ encoded, Buffer.from([ packet.flags ])] ); encoded = Buffer.concat( [ encoded, Buffer.from([ packet.codes ])] ); encoded = Buffer.concat( [ encoded, Buffer.from( to_bytes( packet.qd ))] ); // Questions encoded = Buffer.concat( [ encoded, Buffer.from( to_bytes( packet.answers.length ))] ); // Answers encoded = Buffer.concat( [ encoded, Buffer.from( to_bytes( packet.authority.length ))] ); // Authority encoded = Buffer.concat( [ encoded, Buffer.from( to_bytes( packet.additional.length ))] ); // Additional encoded = Buffer.concat( [ encoded, encode_question(packet) ]); packet.answers.forEach( function(answer) { encoded = Buffer.concat( [ encoded, gen_resource_record(packet, answer.name, answer.type, answer.class, answer.ttl, answer.rdata) ]); }); packet.authority.forEach( function(auth) { encoded = Buffer.concat( [ encoded, gen_resource_record(packet, auth.name, auth.type, auth.class, auth.ttl, auth.rdata)] ); }); packet.additional.forEach( function(adtnl) { encoded = Buffer.concat( [ encoded, gen_resource_record(packet, adtnl.name, adtnl.type, adtnl.class, adtnl.ttl, adtnl.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 = Buffer.alloc(0); response = Buffer.concat( [ response, data.slice(0,2) ] ); response = Buffer.concat( [ response, Buffer.from([ (packet.flags |= dns_flags.AA | dns_flags.QR) ])] ); response = Buffer.concat( [ response, Buffer.from([ (packet.codes |= dns_codes.RA) ])] ); // append counts: qd, answer count, 0 auths, 0 additional response = Buffer.concat( [ response, Buffer.from([ 0x00, 0x01 ]), Buffer.from( to_bytes(answers.length)), Buffer.from( [0x0, 0x0, 0x0, 0x0 ]) ] ); response = Buffer.concat( [ response, data.slice(12, packet.question.qend ) ] ); answers.forEach( function(answer) { response = Buffer.concat( [ response, gen_resource_record(packet, answer.name, answer.type, answer.class, answer.ttl, answer.rdata) ]); }); return response; } function shortcut_nxdomain(data, packet) { var response = Buffer.alloc(0); response = Buffer.concat( [ response, data.slice(0,2) ] ); response = Buffer.concat( [ response, Buffer.from([ (packet.flags |= dns_flags.AA | dns_flags.QR) ])] ); response = Buffer.concat( [ response, Buffer.from([ (packet.codes |= dns_codes.NXDOMAIN | dns_codes.RA) ])] ); // append counts: qd, answer count, 0 auths, 0 additional response = Buffer.concat( [ response, Buffer.from([ 0x00, 0x01, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0 ]) ] ); response = Buffer.concat( [ 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 = Buffer.from( encode_label(packet.question.name) ); encoded = Buffer.concat( [ encoded, Buffer.from(to_bytes(packet.question.type)), Buffer.from(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: data.readUInt16BE(0), flags: data[2], codes: data[3], min_ttl: 2147483647, qd: data.readUInt16BE(4), an: data.readUInt16BE(6), ns: data.readUInt16BE(8), ar: data.readUInt16BE(10), 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: packet.data.readUInt16BE(packet.offset), class: packet.data.readUInt16BE(packet.offset+2), qend: packet.offset + 16 }; packet.offset += 4; if ( packet.qd != 1 ) { return false; } return true; } function parse_answers(packet, decode_level) { // Process the question section if necessary if ( packet.question.length == 0 ) { parse_question(packet); } // Process answers if ( packet.an > 0 && packet.answers.length == 0 ) { packet.answers = parse_section(packet, packet.an, decode_level); } // If we didn't have any ttls in the packet, then cache for 5 minutes. if (packet.min_ttl == 2147483647) { packet.min_ttl = 300; } } // Parse all sections of the packet function parse_complete(packet, decode_level) { // Process the question section if necessary if ( packet.question.length == 0 ) { parse_question(packet); } // Process answers if ( packet.an > 0 && packet.answers.length == 0 ) { packet.answers = parse_section(packet, packet.an, decode_level); } // Process authority if ( packet.ns > 0 && packet.authority.length == 0) { packet.authority = parse_section(packet, packet.ns, decode_level); } // Process Additional if ( packet.ar > 0 && packet.additional.length == 0) { packet.additional = parse_section(packet, packet.ar, decode_level); } // If we didn't have any ttls in the packet, then cache for 5 minutes. if (packet.min_ttl == 2147483647) { packet.min_ttl = 300; } } function parse_section(packet, recs, decode_level) { var rrs = []; for (var i=0; i 63 ) { // Invalid DNS name, individual labels are limited to 63 bytes. //s.warn("DNS Error - parse_label encountered invaliad DNS name"); break; } else { name += packet.data.slice(++pos, pos+length) + "."; pos += length; } } if ( ! compressed ) { packet.offset = pos } name = name.slice(0,-1); return name; } /** TODO Check sizes on resources/packets labels 63 octets or less names 255 octets or less TTL positive values of a signed 32 bit number. UDP messages 512 octets or less **/ function encode_label( name ) { var data = Buffer.alloc(0); name.split('.').forEach( function(part){ data = Buffer.concat( [ data, Buffer.from([ part.length ]), Buffer.from(part) ] ); }); data = Buffer.concat( [data, Buffer.from([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 = Buffer.from([192, 12]); } else { // gen labels for the name resource = encode_label(name); } resource = Buffer.concat( [ resource, Buffer.from([ 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 = Buffer.concat([ resource, Buffer.from( [ 0, 1 ] )]); break; default: //TODO Barf resource = Buffer.concat([ resource, Buffer.from( [ 99, 99 ] )]); } resource = Buffer.concat( [ resource, Buffer.from(to_bytes32(ttl)) ] ); resource = Buffer.concat( [ resource, Buffer.from(to_bytes( record.length )) ] ); resource = Buffer.concat( [ resource, Buffer.from(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 = packet.data.readUInt16BE(packet.offset); packet.offset += 2; if ( decode_level > 0 ) { if (resource.type == dns_type.OPT ) { // EDNS parse_edns_options(packet); } else { resource.class = packet.data.readUInt16BE(packet.offset); resource.ttl = packet.data.readUInt32BE(packet.offset+2); resource.rdlength = packet.data.readUInt16BE(packet.offset+6); packet.offset +=8; if ( decode_level == 1 ) { resource.rdata = packet.data.slice(packet.offset, packet.offset + resource.rdlength); packet.offset += resource.rdlength; } else { switch(resource.type) { case dns_type.A: resource.rdata = parse_arpa_v4(packet, resource); break; case dns_type.AAAA: resource.rdata = parse_arpa_v6(packet, resource); break; case dns_type.NS: resource.rdata = parse_label(packet); break; case dns_type.CNAME: resource.rdata = parse_label(packet); break; case dns_type.SOA: resource.rdata = parse_soa_record(packet); break; case dns_type.SRV: resource.rdata = parse_srv_record(packet); break; case dns_type.MX: resource.rdata = parse_mx_record(packet); break; case dns_type.TXT: resource.rdata = parse_txt_record(packet, resource.rdlength); break; default: resource.rdata = packet.data.slice(packet.offset, packet.offset + resource.rdlength); packet.offset += resource.rdlength; } } } } return resource; } function encode_arpa_v4( ipv4 ) { var rdata = Buffer.alloc(4); var index = 0; ipv4.split('\.').forEach( function(octet) { rdata[index++] = octet; }); return rdata; } function parse_arpa_v4(packet) { var octet = [0,0,0,0]; for (var i=0; i< 4 ; i++ ) { octet[i] = packet.data[packet.offset++]; } return octet.join("."); } function encode_arpa_v6( ipv6 ) { var rdata = Buffer.alloc(0); ipv6.split(':').forEach( function(segment) { rdata = Buffer.concat( [ rdata, Buffer.from( segment[0] + segment[1], 'hex') ] ); rdata = Buffer.concat( [ rdata, Buffer.from( segment[2] + segment[3], 'hex') ] ); }); return rdata; } function parse_arpa_v6(packet) { var ipv6 = ""; for (var i=0; i<8; i++ ) { ipv6 += packet.data.toString('hex', packet.offset++, ++packet.offset) + ":"; } return ipv6.slice(0,-1); } function encode_txt_record( text_array ) { var rdata = Buffer.alloc(0); 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 = Buffer.concat( [ rdata, Buffer.from([len]), Buffer.from(text.slice(i,i+len)) ] ); i += len; } } else { rdata = Buffer.concat( [ rdata, Buffer.from([tl]), Buffer.from(text) ] ); } }); return rdata; } function parse_txt_record(packet, length) { var txt = []; var pos = 0; while ( pos < length ) { var tl = packet.data[packet.offset++]; txt.push( packet.data.toString('utf8', packet.offset, packet.offset + tl)); pos += tl + 1; packet.offset += tl; } return txt; } function encode_mx_record( mx ) { var rdata = Buffer.alloc(0); rdata += to_bytes( mx.priority ); rdata += encode_label( mx.exchange ); return rdata; } function parse_mx_record(packet) { var mx = {}; mx.priority = packet.data.readUInt16BE(packet.offset); packet.offset += 2; mx.exchange = parse_label(packet); return mx; } function encode_srv_record( srv ) { var rdata = Buffer.alloc(6) rdata.writeInt16BE( srv.priority, 0 ); rdata.writeInt16BE( srv.weight, 2 ); rdata.writeInt16BE( srv.port, 4 ); rdata = Buffer.concat( [ rdata, encode_label( srv.target ) ]); ngx.log( ngx.WARN, rdata.toString('hex')); return rdata; } function parse_srv_record(packet) { var srv = {}; srv.priority = packet.data.readUInt16BE(packet.offset); srv.weight = packet.data.readUInt16BE(packet.offset+2); srv.port = packet.data.readUInt16BE(packet.offset+4); packet.offset += 6; srv.target = parse_label(packet); return srv; } function encode_soa_record( soa ) { var rdata = Buffer.concat([ encode_label(soa.primary), encode_label(soa.mailbox) ]); rdata = Buffer.concat( [ rdata, Buffer.from(to_bytes32(soa.serial)), Buffer.from(to_bytes32(soa.refresh)), Buffer.from(to_bytes32(soa.retry)), Buffer.from(to_bytes32(soa.expire)), Buffer.from(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 = packet.data.readUInt32BE(packet.offset); soa.refresh = packet.data.readUInt32BE(packet.offset+=4); soa.retry = packet.data.readUInt32BE(packet.offset+=4); soa.expire = packet.data.readUInt32BE(packet.offset+=4); soa.minTTL = packet.data.readUInt32BE(packet.offset+=4); packet.offset +=4; return soa; } function parse_edns_options(packet) { packet.edns = {} packet.edns.opts = {} packet.edns.size = packet.data.readUInt16BE(packet.offset); packet.edns.rcode = packet.data[packet.offset+2]; packet.edns.version = packet.data[packet.offset+3]; packet.edns.z = packet.data.readUInt16BE(packet.offset+4); packet.edns.rdlength = packet.data.readUInt16BE(packet.offset+6); packet.offset += 8; var end = packet.offset + packet.edns.rdlength; for ( ; packet.offset < end ; ) { var opcode = packet.data.readUInt16BE(packet.offset); var oplength = packet.data.readUInt16BE(packet.offset+2); packet.offset += 4; if ( opcode == 8 ) { //client subnet packet.edns.opts.csubnet = {} packet.edns.opts.csubnet.family = packet.data.readUInt16BE(packet.offset); packet.edns.opts.csubnet.netmask = packet.data[packet.offset+2]; packet.edns.opts.csubnet.scope = packet.data[packet.offset+3]; packet.offset += 4; 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[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; } } } ================================================ FILE: njs.d/dns/test.js ================================================ import dns from "libdns.js"; export default {get_qname, test_dns_encoder, test_dns_decoder}; /** * 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 = 3; /** * DNS Debug Level * Specify the decoding level at which we should log packet data to the error log. * Default is level 3 (max decoding) **/ var dns_debug_level = 3; // The DNS Question name var dns_name = Buffer.alloc(0); function get_qname(s) { return dns_name; } // Encode the given number to two bytes (16 bit) function to_bytes( number ) { return Buffer.from( [ ((number>>8) & 0xff), (number & 0xff) ] ); } function debug(s, msg) { if ( dns_decode_level >= dns_debug_level ) { s.warn(msg); } } function test_dns_encoder(s) { s.on("upstream", function(data,flags) { var packet; var test_result = Buffer.alloc(0); if ( data.length == 0 ) { return; } if (data) { if (s.variables.protocol == "TCP") { // Drop the TCP length field data = data.slice(2); } debug(s, "test_dns: DNS Encoder Req: " + data.toString('hex') ); packet = dns.parse_packet(data); dns.parse_question(packet); dns_name = packet.question.name; debug(s, "test_dns: DNS Encoder Request Packet: " + JSON.stringify( Object.entries(packet)) ); test_result = test_dns_responder(s, data, packet); delete packet.data; // remove the data buffer before printing debug(s, "test_dns: DNS Encoder Response Packet: " + JSON.stringify( Object.entries(packet)) ); debug(s, "test_dns: DNS Encoder Res: " + test_result.toString('hex') ); s.variables.test_result = test_result; s.done(); } }); } function test_dns_decoder(s) { s.on("downstream", function(data,flags) { var packet; var test_result = Buffer.alloc(0); if ( data.length == 0 ) { return; } if (data) { if (s.variables.protocol == "TCP") { // Drop the TCP length field data = data.slice(2); } debug(s, "test_dns: DNS Decoder Res: " + data.toString('hex') ); packet = dns.parse_packet(data); dns.parse_question(packet); dns_name = packet.question.name; dns.parse_complete(packet, 2); delete packet.data; // remove the data buffer before printing debug(s, "test_dns: DNS Decoder Response Packet: " + JSON.stringify( Object.entries(packet)) ); if (s.variables.protocol == "TCP") { s.send( to_bytes(data.length) ); } s.send( data, {flush: true} ); } }); } /** * Function to perform testing of DNS packet generation for various DNS types * Any domain ending bar.com will use the shortcut_response path * Any domains ending baz.com will use shortcut_nxdomain path * All other queries will return an appropriate set of DNS records. **/ function test_dns_responder(s, data, packet) { var answers = []; var test_result; if ( packet.question.type == dns.dns_type.A || packet.question.type == dns.dns_type.ANY ) { 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.toString().endsWith("bar.com") ) { test_result = dns.shortcut_response(data, packet, answers); } else if ( packet.question.name.toString().endsWith("baz.com") ) { test_result = dns.shortcut_nxdomain(data, packet); } 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" } ); packet.answers = answers; test_result = dns.encode_packet(packet); } if (s.variables.protocol == "TCP" ) { test_result = Buffer.concat( [ to_bytes( test_result.length ), test_result ]); } return test_result; } ================================================ FILE: ssl/certs/doh.local.pem ================================================ -----BEGIN CERTIFICATE----- MIID0TCCArmgAwIBAgIUOdQrJG61Cs5p1PRwIOYzUClAZZkwDQYJKoZIhvcNAQEL BQAweDELMAkGA1UEBhMCR0IxEjAQBgNVBAgMCUNhbWJyaWRnZTESMBAGA1UEBwwJ Q2FtYnJpZGdlMRIwEAYDVQQKDAlOR0lOWCBJbmMxGTAXBgNVBAsMEE5vdyBhIHBh cnQgb2YgRjUxEjAQBgNVBAMMCWRvaC5sb2NhbDAeFw0xOTA5MjgxNDU2MzNaFw0y MDA5MjcxNDU2MzNaMHgxCzAJBgNVBAYTAkdCMRIwEAYDVQQIDAlDYW1icmlkZ2Ux EjAQBgNVBAcMCUNhbWJyaWRnZTESMBAGA1UECgwJTkdJTlggSW5jMRkwFwYDVQQL DBBOb3cgYSBwYXJ0IG9mIEY1MRIwEAYDVQQDDAlkb2gubG9jYWwwggEiMA0GCSqG SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDSnQpcd9PElrimhx1Absbf4SafKPpM+7Nh EVBFJ5Emtxksz1tUsi1mXEQsf9sCKeURvrzoUwyUkkF4Frks14L/+GCXEpCJSrga NhhSO6QR0xZ26jXFqwwsE3QkW6URNGZ5IEecI+2JAUiMxhdmO9oEPvRzDmyDoUTT dmt6+y0NahrU47OP88yie0Jt1+Mh18U/RQKRUYZz1L4oHV1sujuemDbF7xSkguvV EhUMF+316HQNPndrZRVIYjfMUT32qnvlnOKzgB4mNh8biRLekwsPplFuU5vhnUxR 5pDw4JzNT5Mis8I8+ULUkKKK3wF7Ih3Wp6vMgq/i6CvqcYA/n8LzAgMBAAGjUzBR MB0GA1UdDgQWBBSFF5wbFm0N3FXIu14wGAfHIIEDyjAfBgNVHSMEGDAWgBSFF5wb Fm0N3FXIu14wGAfHIIEDyjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUA A4IBAQCHIjdUCNKHnk06jyajT8rKMdjpJQkv6P2nFr0Sf0/1ZtftK+dgA5O3HJmY aPJVGZlfdOWavYT3i+OrLpSVCwGoCt/V1rSgw6E9zfEarsVdtiZzd9h/HhvOdDGd SE1EUJveIoe46DpdeD+pSz068+1WKK7UahArupsXjlcaoCVp7uvLTacP9NPMP6jp aiOotgZUxHEoelseEgyFGDOyP32OTZv8vGQFTaNLS3zKP8iZNNmfwX1pirY5TJzP HcbKgT8aki9+U64vkjoUrvpA8y4U8b7NgKFowkLl7rbHxNqZmstq+YI13RwgNcYG kEbvwju0TuQ52TgpKkA3D5fiWhSZ -----END CERTIFICATE----- ================================================ FILE: ssl/private/doh.local.pem ================================================ -----BEGIN PRIVATE KEY----- MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDSnQpcd9PElrim hx1Absbf4SafKPpM+7NhEVBFJ5Emtxksz1tUsi1mXEQsf9sCKeURvrzoUwyUkkF4 Frks14L/+GCXEpCJSrgaNhhSO6QR0xZ26jXFqwwsE3QkW6URNGZ5IEecI+2JAUiM xhdmO9oEPvRzDmyDoUTTdmt6+y0NahrU47OP88yie0Jt1+Mh18U/RQKRUYZz1L4o HV1sujuemDbF7xSkguvVEhUMF+316HQNPndrZRVIYjfMUT32qnvlnOKzgB4mNh8b iRLekwsPplFuU5vhnUxR5pDw4JzNT5Mis8I8+ULUkKKK3wF7Ih3Wp6vMgq/i6Cvq cYA/n8LzAgMBAAECggEABDnYcmCJJEGt9NFzOc6/ONDIuJrW4uKOB92UEb8of3Ff FPIYMAvfM1WYnJf4KgPzL7b3DWZVM0n3/FPgZVDxtPcj4QQjWE3igcwiEsxVj3H/ 2mT6rTuwY9YEF5KrLjwx7i5CoZRq+LvI2+JBp/B9gGZO+1wHu2BqBCA1KeOOVN2J 2FDLt32sMLrY4QQqFefYGk3gdzD06qyjRbpSiymaDOfK7D1xr6iNqGZHOl8eu8I4 zHBVwuE9L3qPGY+bH1rTWSBrV/hIJSKhXom4PbNz23KpfrgpGJ+yP8hspEUgtGiH 0Et9o59zKju77sLkZvhLOQSy8Y/yCpn9qNLqoNl8QQKBgQDs4lXCYkaQNMErGYpf 2+T7fRyuRpCedmoM70p1EmwtLi2ufIWPWPr+7NpvIdmXZb02rzE5gMMAROmC0c7b 9n7jqBMN8LjdG2yeyl0a1rxxE/eMrYqZ/SIEaxUnam7B50Re767/uUL8Uq2UOuDi 9e1g6LzvXixM7m1JpS4pkrDkYQKBgQDjm/5DqmOD/cfLbpNhxnLiQkJEHt553+mh xXWaxugBGjRyFlXfaRNAoJa5D7+VRc+iFNPF9CwYYMV93JYbVzNwiR5HJ7GfYRKZ +01NN5gAZwsOdhiqhkxjc9PcP9JrcGnGC9RMUtSvscwNPh3R2DKPHpnz3O96yzPG qZqpEcDn0wKBgQCdX0RgLk/4r8OBMaeXRYwbc6PhN+oODFcqHrMVkdaiMWKR4BIP CKs/PvVjDVb0WNfag4stS5jBDgcgLOjDg0ALWHbINRtrcTO5TnGKSgzJBt3X7Nb+ tIer7cQQ+ol4cn8enxdgtqCE5xyANJmAzqcUUaprT+IYffHHEmDXp6ezIQKBgQCL +3prPzWpDcF8+eqmrZgmUz3SC3IkXnOfzINBx6cUVnt+1wHFPyhaDOnlsyvOsHq8 YjbEfiFIdOvBNpMTCZRXV91JQb5aGSeJkCbAoLpZNQZ1xGfzKFl+qNPZl17gOOi0 pr3Qmvi3fY/TbSqFzoN5xgZFFtIqISMcwV6fMI4FhQKBgGLoMW/OK69zAJpwsVV1 AjVfXzEY3UtK8RVWspl+bOKPArIKvtR4aTvcT2kkYMxjXM+0wm7dfQq1a3gvyv/K d0mhr/Sst7OZuMcWH7xwM5ZmvPWnEMYh55BNThKZ7gdJ6+SFNOWNDCzLao1sC+uX GpBWCMEPmsLEVsth20BRtr90 -----END PRIVATE KEY-----