[
  {
    "path": "LICENSE",
    "content": "BSD 2-Clause License\n\nCopyright (c) 2022, Mark Boddington\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n1. Redistributions of source code must retain the above copyright notice, this\n   list of conditions and the following disclaimer.\n\n2. Redistributions in binary form must reproduce the above copyright notice,\n   this list of conditions and the following disclaimer in the documentation\n   and/or other materials provided with the distribution.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "README.md",
    "content": "# NGINX DNS (DNS/DoT/DoH)\n\n> The `v2` branch is migrating to `Buffers` due to NJS deprecating the `String` byte-array functions.\n> Please test and raise issues if you find them. Thank you!\n\nThis repository contains some NJS code, and example configuration files for using NGINX with DNS services.\nNGINX can be used to perform load balancing for DNS (TCP/UDP), and also DNS over TLS (DoT) and DNS over HTTPS (DoH)\n\nNGINX can also be used to provide Global Server Load Balancing (GSLB).\n\nSee the example configuration files in the [examples](examples) folder.\n\n## Setup\nCopy the njs.d folder into /etc/nginx/ and one of the NGINX DoH [examples](examples) to /etc/nginx/nginx.conf\nThe ssl folder contains a test certificate, you will likely want to generate and use your own certificate and update the nginx.conf file accordingly.\n\n## Simple DNS\nNGINX can do simple DNS load balancing, without the need for NJS, using the standard Stream module directives.\n```\nstream {\n\n  # DNS upstream pool.\n  upstream dns {\n    zone dns 64k;\n    server 8.8.8.8:53;\n  }\n\n  # DNS Server. Listens on both TCP and UDP\n  server {\n    listen 53;\n    listen 53 udp;\n    proxy_responses 1;\n    proxy_pass dns;\n  }\n}\n```\n\nHowever 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\nincluded in this repository. \n\nTo 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`.\nFor example:\n```\nstream {\n  js_import /etc/nginx/njs.d/dns/dns.js;\n  js_set $dns_qname dns.get_qname;\n\n  map $dns_qname $upstream_pool {\n    hostnames;\n    *.nginx one;\n    default two;\n  }\n\n  upstream one {\n    ...\n  }\n\n  upstream two {\n    ...\n  }\n\n  server {\n    listen 53 udp;\n    js_preread dns.preread_dns_request;\n    proxy_responses 1;\n    proxy_pass $upstream_pool;\n  }\n\n}\n```\n\n## DNS over TLS (DoT) and DNS over HTTPS (DoH) Gateway\n\nNGINX can act as a DNS(TCP) <-> DNS over TLS (DoT) gateway without any NJS functions. Eg:\n\n```\n  upstream dns {\n    zone dns 64k;\n    server 8.8.8.8:53;\n  }\n\n  upstream dot {\n    zone dot 64k;\n    server 8.8.8.8:853;\n  }\n\n  server {\n    listen 53;\n    listen 853 ssl;\n    ssl_certificate /etc/nginx/ssl/certs/doh.local.pem;\n    ssl_certificate_key /etc/nginx/ssl/private/doh.local.pem;\n    proxy_ssl on;\n    proxy_pass dot;\n  }\n```\nThe 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.\n\nNJS is required if you want to act as a gateway between DoH and DNS/DoT.\nSee the example configuration files in the [examples](examples) folder.\n\nThe full configuration has a HTTP/2 service listening for requests, and does a proxy_pass for requests to /dns-query. \nWe proxy to an internal stream service on port 8053, which uses js_filter to pull out the DNS packet from the HTTP wrapper,\nand forward onto an upstream DNS(TCP) or DoT server.\nThe result is then wrapped back up in a HTTP response and passed to the HTTP/2 service for delivery to the client.\n\nNGINX can log as much or as little as you like, and the NJS allows you to process information in the DNS requests and\nresponses.\n\nSee: [docs/nginx-dns-over-https](docs/nginx-dns-over-https.md) for more information\n\n## NGINX GSLB (work-in-progress)\nUse the nginx-glb.conf file to run an GSLB service.\nCopy the njs.d folder into /etc/nginx/ and the nginx-glb.conf to /etc/nginx/nginx.conf\n\nTODO - Describe the example configuration and how to customise it.\n\n"
  },
  {
    "path": "docs/nginx-dns-over-https.md",
    "content": "## DNS over HTTPS (DoH) Gateway\nUse the nginx-doh.conf file to run a DoH gateway.\nCopy the njs.d and ssl folders into /etc/nginx/ and the nginx-doh.conf to /etc/nginx/nginx.conf\n\n### Simple DNS(TCP) and DNS over TLS (DoT)\nNGINX can act as a DNS(TCP) <-> DNS over TLS (DoT) gateway without any NJS functions. Eg:\n\n```\n  upstream dns {\n    zone dns 64k;\n    server 8.8.8.8:53;\n  }\n\n  upstream dot {\n    zone dot 64k;\n    server 8.8.8.8:853;\n  }\n\n  server {\n    listen 53;\n    listen 853 ssl;\n    ssl_certificate /etc/nginx/ssl/certs/doh.local.pem;\n    ssl_certificate_key /etc/nginx/ssl/private/doh.local.pem;\n    proxy_ssl on;\n    proxy_pass dot;\n  }\n```\n\n### DNS over HTTPS (DoH)\nNJS 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\nfull configuration in nginx-glb.conf.\n\nThe full configuration has a HTTP/2 service listening for requests, and does a proxy_pass for requests to /dns-query.\nWe proxy to an internal stream service on port 8053, which uses js_filter to pull out the DNS packet from the HTTP wrapper,\nand forward onto an upstream DNS(TCP) or DoT server.\nThe result is then wrapped back up in a HTTP response and passed to the HTTP/2 service for delivery to the client.\n\n#### NGINX Stream Server for back-end\nLets look at the stream service first:\n```\n  server {\n    listen 127.0.0.1:8053;\n    js_filter dns.filter_doh_request;\n    proxy_ssl on;\n    proxy_pass dot;\n  }\n```\nWe 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`\nwill find the DNS packet encoded in the request, and forward it on to the upstream DoT service. \n\n#### NGINX HTTP/2 service for the front-end\nTraffic arrives at the stream service via a HTTP/2 server:\n```\n  upstream dohloop {\n    zone dohloop 64k;\n    server 127.0.0.1:8053;\n  }\n\n  proxy_cache_path /var/cache/nginx/doh_cache levels=1:2 keys_zone=doh_cache:10m;\n  server {\n\n    listen 443 ssl http2;\n    ssl_certificate /etc/nginx/ssl/certs/doh.local.pem;\n    ssl_certificate_key /etc/nginx/ssl/private/doh.local.pem;\n\n    proxy_cache_methods GET POST;\n\n    location / {\n      return 404 \"404 Not Found\\n\";\n    }\n\n    location /dns-query {\n      proxy_http_version 1.0;\n      proxy_cache doh_cache;\n      proxy_cache_key $scheme$proxy_host$uri$is_args$args$request_body;\n      proxy_pass http://dohloop;\n    }\n\n  }\n```\nWe listen on the standard HTTPS port for incoming http requests. We return a 404 response to all requests except for those which match our\n`/dns-query` location. All dns-queries are forwarded onto the stream service as HTTP/1.0 requests.\n\n#### NGINX Processing Options\nThe 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\nprocessing (level 2) allows NGINX to gather the necessary intelligence (Resource Record TTLs) to enable a HTTP Content-Cache for the DoH requests.\nAt levels less than 2, we will cache responses, but for just 10 seconds.\n\nChange this setting in the `njs.d/dns/doh.js` file\n```\n/**\n * DNS Decode Level\n * 0: No decoding, minimal processing required to strip packet from HTTP wrapper (fastest)\n * 1: Parse DNS Header and Question. We can log the Question, Class, Type, and Result Code\n * 2: As 1, but also parse answers. We can log the answers, and also cache responses according to TTL.\n * 3: Very Verbose, log everything as above, but also write packet data to error log (slowest)\n**/\nvar $dns_decode_level = 0;\n```\n\n"
  },
  {
    "path": "examples/nginx-dns-routing.conf",
    "content": "user  nginx;\nworker_processes  auto;\n\nload_module modules/ngx_stream_js_module.so;\n\nerror_log  /var/log/nginx/error.log notice;\npid        /var/run/nginx.pid;\n\nevents {\n    worker_connections  1024;\n}\n\nstream {\n\n  # logging\n  log_format  dns   '$remote_addr [$time_local] $protocol \"$dns_qname\" \"$upstream_pool\"';\n  access_log /var/log/nginx/dns-access.log dns;\n\n  # import NJS module\n  js_import /etc/nginx/njs.d/dns/dns.js;\n\n  # NJS function to get the dns_qname, requires a js_preread in the server to populate the variable from the DNS packet\n  js_set $dns_qname dns.get_qname;\n\n  # This maps the qname domain to the DNS server for routing\n  map $dns_qname $upstream_pool {\n    hostnames;\n    *.nginx dnsmasq;\n    *.k8s dnsmasq;\n    default google;\n  }\n\n  # Google upstream\n  upstream google {\n    zone dns 64k;\n    server 8.8.8.8:53;\n  }\n\n  # dnsmasq local upstream\n  upstream dnsmasq {\n    zone dns 64k;\n    server 192.168.64.1:53;\n  }\n\n  # DNS(TCP) Serverr\n  server {\n    listen 53;\n    js_preread dns.preread_dns_request;\n    proxy_pass $upstream_pool;\n  }\n\n  # DNS(UDP) Server\n  server {\n    listen 53 udp;\n    js_preread dns.preread_dns_request;\n    proxy_responses 1;\n    proxy_pass $upstream_pool;\n  }\n\n}\n"
  },
  {
    "path": "examples/nginx-dns-simple.conf",
    "content": "user  nginx;\nworker_processes  auto;\n\nerror_log  /var/log/nginx/error.log notice;\npid        /var/run/nginx.pid;\n\nevents {\n    worker_connections  1024;\n}\n\nstream {\n\n  # DNS upstream pool.\n  upstream dns {\n    zone dns 64k;\n    server 8.8.8.8:53;\n  }\n\n  # DNS Server. Listens on both TCP and UDP\n  server {\n    listen 53;\n    listen 53 udp;\n    proxy_responses 1;\n    proxy_pass dns;\n  }\n}\n"
  },
  {
    "path": "examples/nginx-doh-and-dot-to-dns.conf",
    "content": "user  nginx;\nworker_processes  auto;\n\nload_module modules/ngx_stream_js_module.so;\n\nerror_log  /var/log/nginx/error.log notice;\npid        /var/run/nginx.pid;\n\nevents {\n    worker_connections  1024;\n}\n\nhttp {\n  include       /etc/nginx/mime.types;\n  default_type  application/octet-stream;\n\n  # logging directives\n  log_format  doh   '$remote_addr - $remote_user [$time_local] \"$request\" '\n                    '[ $msec, $request_time, $upstream_response_time $pipe ] '\n                    '$status $body_bytes_sent \"$http_x_forwarded_for\" '\n                    '$upstream_http_x_dns_question $upstream_http_x_dns_type '\n                    '$upstream_http_x_dns_result '\n                    '$upstream_http_x_dns_ttl $upstream_http_x_dns_answers '\n                    '$upstream_cache_status';\n\n  access_log  /var/log/nginx/doh-access.log doh;\n\n  # This upstream connects to a local Stream service which converts HTTP -> DNS\n  upstream dohloop {\n    zone dohloop 64k;\n    server 127.0.0.1:8053;\n    keepalive_timeout 60s;\n    keepalive_requests 100;\n    keepalive 10;\n  }\n\n  # Proxy Cache storage - so we can cache the DoH response from the upstream\n  proxy_cache_path /var/cache/nginx/doh_cache levels=1:2 keys_zone=doh_cache:10m;\n\n  # The DoH server block\n  server {\n  \n    # Listen on standard HTTPS port, and accept HTTP2, with SSL termination\n    listen 443 ssl http2;\n    ssl_certificate /etc/nginx/ssl/certs/doh.local.pem;\n    ssl_certificate_key /etc/nginx/ssl/private/doh.local.pem;\n    ssl_session_cache shared:ssl_cache:10m;\n    ssl_session_timeout 10m;\n\n    # DoH may use GET or POST requests, Cache both\n    proxy_cache_methods GET POST;\n\n    # Return 404 to all responses, except for those using our published DoH URI\n    location / {\n      return 404 \"404 Not Found\\n\";\n    }\n\n    # This is our published DoH URI \n    location /dns-query {\n\n      # Proxy HTTP/1.1, clear the connection header to enable Keep-Alive\n      proxy_http_version 1.1;\n      proxy_set_header Connection \"\";\n\n      # Enable Cache, and set the cache_key to include the request_body\n      proxy_cache doh_cache;\n      proxy_cache_key $scheme$proxy_host$uri$is_args$args$request_body;\n\n      # proxy pass to the dohloop upstream\n      proxy_pass http://dohloop;\n    }\n\n  }\n\n}\n\n# DNS Stream Services\nstream {\n\n  # DNS logging\n  log_format  dns   '$remote_addr [$time_local] $protocol \"$dns_qname\"';\n  access_log /var/log/nginx/dns-access.log dns;\n\n  # Import the NJS module\n  js_import /etc/nginx/njs.d/dns/dns.js;\n\n  # The $dns_qname variable can be populated by preread calls, and can be used for DNS routing\n  js_set $dns_qname dns.get_qname;\n\n  # DNS upstream pool.\n  upstream dns {\n    zone dns 64k;\n    server 8.8.8.8:53;\n  }\n\n  # DNS(TCP) and DNS over TLS (DoT) Server\n  # Terminate DoT and DNS TCP, and proxy onto standard DNS\n  server {\n    listen 53;\n    listen 853 ssl;\n    ssl_certificate /etc/nginx/ssl/certs/doh.local.pem;\n    ssl_certificate_key /etc/nginx/ssl/private/doh.local.pem;\n    js_preread dns.preread_dns_request;\n    proxy_pass dns;\n  }\n\n  # DNS(UDP) Server\n  # DNS UDP proxy onto DNS UDP\n  server {\n    listen 53 udp;\n    proxy_responses 1;\n    js_preread dns.preread_dns_request;\n    proxy_pass dns;\n  }\n\n  # DNS over HTTPS (gateway) Service\n  # Upstream can be either DNS(TCP) or DoT. If upstream is DNS, proxy_ssl should be off.\n  server {\n    listen 127.0.0.1:8053;\n    js_filter dns.filter_doh_request;\n    proxy_pass dns;\n  }\n\n}\n"
  },
  {
    "path": "examples/nginx-doh-and-dot-to-dot.conf",
    "content": "user  nginx;\nworker_processes  auto;\n\nload_module modules/ngx_stream_js_module.so;\n\nerror_log  /var/log/nginx/error.log notice;\npid        /var/run/nginx.pid;\n\nevents {\n    worker_connections  1024;\n}\n\nhttp {\n  include       /etc/nginx/mime.types;\n  default_type  application/octet-stream;\n\n  # logging directives\n  log_format  doh   '$remote_addr - $remote_user [$time_local] \"$request\" '\n                    '[ $msec, $request_time, $upstream_response_time $pipe ] '\n                    '$status $body_bytes_sent \"$http_x_forwarded_for\" '\n                    '$upstream_http_x_dns_question $upstream_http_x_dns_type '\n                    '$upstream_http_x_dns_result '\n                    '$upstream_http_x_dns_ttl $upstream_http_x_dns_answers '\n                    '$upstream_cache_status';\n\n  access_log  /var/log/nginx/doh-access.log doh;\n\n  # This upstream connects to a local Stream service which converts HTTP -> DNS\n  upstream dohloop {\n    zone dohloop 64k;\n    server 127.0.0.1:8053;\n    keepalive_timeout 60s;\n    keepalive_requests 100;\n    keepalive 10;\n  }\n\n  # Proxy Cache storage - so we can cache the DoH response from the upstream\n  proxy_cache_path /var/cache/nginx/doh_cache levels=1:2 keys_zone=doh_cache:10m;\n\n  # The DoH server block\n  server {\n  \n    # Listen on standard HTTPS port, and accept HTTP2, with SSL termination\n    listen 443 ssl http2;\n    ssl_certificate /etc/nginx/ssl/certs/doh.local.pem;\n    ssl_certificate_key /etc/nginx/ssl/private/doh.local.pem;\n    ssl_session_cache shared:ssl_cache:10m;\n    ssl_session_timeout 10m;\n\n    # DoH may use GET or POST requests, Cache both\n    proxy_cache_methods GET POST;\n\n    # Return 404 to all responses, except for those using our published DoH URI\n    location / {\n      return 404 \"404 Not Found\\n\";\n    }\n\n    # This is our published DoH URI \n    location /dns-query {\n\n      # Proxy HTTP/1.1, clear the connection header to enable Keep-Alive\n      proxy_http_version 1.1;\n      proxy_set_header Connection \"\";\n\n      # Enable Cache, and set the cache_key to include the request_body\n      proxy_cache doh_cache;\n      proxy_cache_key $scheme$proxy_host$uri$is_args$args$request_body;\n\n      # proxy pass to the dohloop upstream\n      proxy_pass http://dohloop;\n    }\n\n  }\n\n}\n\n# DNS Stream Services\nstream {\n\n  # DNS logging\n  log_format  dns   '$remote_addr [$time_local] $protocol \"$dns_qname\"';\n  access_log /var/log/nginx/dns-access.log dns;\n\n  # Import the NJS module\n  js_import /etc/nginx/njs.d/dns/dns.js;\n\n  # The $dns_qname variable can be populated by preread calls, and can be used for DNS routing\n  js_set $dns_qname dns.get_qname;\n\n  # DNS over TLS upstream pool\n  upstream dot {\n    zone dot 64k;\n    server 8.8.8.8:853;\n  }\n\n  # DNS(TCP) and DNS over TLS (DoT) Server\n  # Upstream can be either DNS(TCP) or DoT. If upstream is DNS, proxy_ssl should be off.\n  server {\n\n    # DNS TCP\n    listen 53;\n\n    # DNS DoT\n    listen 853 ssl;\n    ssl_certificate /etc/nginx/ssl/certs/doh.local.pem;\n    ssl_certificate_key /etc/nginx/ssl/private/doh.local.pem;\n\n    # This is used to pull out question for logging\n    js_preread dns.preread_dns_request;\n\n    # Enable SSL re-encryption for DoT connection upstream\n    proxy_ssl on;\n    proxy_pass dot;\n  }\n\n  # DNS over HTTPS (gateway) Service\n  # Upstream can be either DNS(TCP) or DoT. If upstream is DNS, proxy_ssl should be off.\n  server {\n    listen 127.0.0.1:8053;\n    js_filter dns.filter_doh_request;\n    proxy_ssl on;\n    proxy_pass dot;\n  }\n\n}\n"
  },
  {
    "path": "examples/nginx-dot-to-dns-simple.conf",
    "content": "user  nginx;\nworker_processes  auto;\n\nload_module modules/ngx_stream_js_module.so;\n\nerror_log  /var/log/nginx/error.log notice;\npid        /var/run/nginx.pid;\n\nevents {\n    worker_connections  1024;\n}\n\n# DNS Stream Services\nstream {\n\n  # DNS upstream pool.\n  upstream dns {\n    zone dns 64k;\n    server 8.8.8.8:53;\n  }\n\n  # DNS(TCP) and DNS over TLS (DoT) Server\n  # Terminates DNS and DoT, then proxies on to standard DNS.\n  server {\n    listen 53;\n    listen 853 ssl;\n    ssl_certificate /etc/nginx/ssl/certs/doh.local.pem;\n    ssl_certificate_key /etc/nginx/ssl/private/doh.local.pem;\n    proxy_pass dns;\n  }\n\n  # DNS(UDP) Server\n  server {\n    listen 53 udp;\n    proxy_responses 1;\n    proxy_pass dns;\n  }\n\n}\n"
  },
  {
    "path": "examples/nginx-dot-to-dot-routing.conf",
    "content": "user  nginx;\nworker_processes  auto;\n\nload_module modules/ngx_stream_js_module.so;\n\nerror_log  /var/log/nginx/error.log notice;\npid        /var/run/nginx.pid;\n\nevents {\n    worker_connections  1024;\n}\n\n# DNS Stream Services\nstream {\n\n  # DNS logging\n  log_format  dns   '$remote_addr [$time_local] $protocol \"$dns_qname\" \"$upstream_pool\"';\n  access_log /var/log/nginx/dns-access.log dns;\n\n  # Import the NJS module\n  js_import /etc/nginx/njs.d/dns/dns.js;\n\n  # The $dns_qname variable will be populated by preread calls, and used for DNS routing\n  js_set $dns_qname dns.get_qname;\n\n  # When doing DNS routing, use $dns_qname to map the questions to the upstream pools.\n  map $dns_qname $upstream_pool {\n    hostnames;\n    *.nginx dnsmasq;\n    *.k8s dnsmasq;\n    default google;\n  }\n\n  # upstream pools (google DoT)\n  upstream google {\n    zone dns 64k;\n    server 8.8.8.8:853;\n  }\n\n  # upstream pools (another DoT)\n  upstream dnsmasq {\n    zone dns 64k;\n    server 192.168.64.1:853;\n  }\n\n  # DNS(TCP) and DNS over TLS (DoT) Server\n  # Upstream can be either DNS(TCP) or DoT. If upstream is DNS, proxy_ssl should be off.\n  server {\n    listen 53;\n    listen 853 ssl;\n    ssl_certificate /etc/nginx/ssl/certs/doh.local.pem;\n    ssl_certificate_key /etc/nginx/ssl/private/doh.local.pem;\n    js_preread dns.preread_dns_request;\n    proxy_ssl on;\n    proxy_pass $upstream_pool;\n  }\n\n}\n"
  },
  {
    "path": "examples/nginx-plus-filtering.conf",
    "content": "#\n# This config shows an example of filtering DNS requests using the Key/value store available in NGINX Plus\n# Push FQDNs into the dns_config key/value zone with a value of \"blocked\" or \"blackhole\" to have them scrubbed from DNS\n# Alternatively push a CSV list of domains as the value to either \"blocked_domains\" or \"blackhole_domains\" to have\n# any requests for records within those zones scrubbed.\n#\n\nuser  nginx;\nworker_processes  auto;\n\nload_module modules/ngx_stream_js_module.so;\n\nerror_log  /var/log/nginx/error.log notice;\npid        /var/run/nginx.pid;\n\nevents {\n    worker_connections  1024;\n}\n\n# DNS Stream Services\nstream {\n\n  # KeyValue store for blocking domains (NGINX Plus only)\n  keyval_zone zone=dns_config:64k state=/etc/nginx/zones/dns_config.zone; \n  keyval \"blocked_domains\" $blocked_domains zone=dns_config;\n  keyval \"blackhole_domains\" $blackhole_domains zone=dns_config;\n  keyval $dns_qname $scrub_action zone=dns_config;\n\n  # DNS logging\n  log_format  dns   '$remote_addr [$time_local] $protocol \"$dns_qname\" \"$upstream_pool\"';\n  access_log /var/log/nginx/dns-access.log dns;\n\n  # Import the NJS module\n  js_import /etc/nginx/njs.d/dns/dns.js;\n\n  # The $dns_qname variable can be populated by preread calls, and can be used for DNS routing\n  js_set $dns_qname dns.get_qname;\n  \n  # The DNS response packet, if we're blocking the domain, this will be set.\n  js_set $dns_response dns.get_response;\n\n  # When doing DNS routing, use $dns_qname to map the questions to the upstream pools.\n  map $dns_qname $upstream {\n    hostnames;\n    *.nginx dnsmasq;\n    *.k8s dnsmasq;\n    default google;\n  }\n\n  # Set upstream to be the pool defined above if dns_response is empty, else pass to the block/blackhole upstream\n  map $dns_response $upstream_pool {\n    \"blocked\" blocked;\n    \"blackhole\" blackhole;\n    default $upstream;\n  }\n\n  # upstream pool for blocked requests (returns nxdomain)\n  upstream blocked {\n    zone blocked 64k;\n    server 127.0.0.1:9953;\n  }\n\n  # upstream pool for blacholed requests (returns 0.0.0.0)\n  upstream blackhole {\n    zone blackhole 64k;\n    server 127.0.0.1:9853;\n  }\n\n  # upstream pools (google DNS)\n  upstream google {\n    zone dns 64k;\n    server 8.8.8.8:53;\n  }\n\n  # upstream pools (another DNS)\n  upstream dnsmasq {\n    zone dns 64k;\n    server 192.168.64.1:5353;\n  }\n\n  # DNS(TCP) and DNS over TLS (DoT) Server\n  # Upstream can be either DNS(TCP) or DoT. If upstream is DNS, proxy_ssl should be off.\n  server {\n    listen 53;\n    listen 853 ssl;\n    ssl_certificate /etc/nginx/ssl/certs/doh.local.pem;\n    ssl_certificate_key /etc/nginx/ssl/private/doh.local.pem;\n    js_preread dns.preread_dns_request;\n    proxy_pass $upstream_pool;\n  }\n\n  # DNS(UDP) Server\n  # Upstream can only be another DNS(UDP) server.\n  server {\n    listen 53 udp;\n    js_preread dns.preread_dns_request;\n    proxy_responses 1;\n    proxy_pass $upstream_pool;\n  }\n\n  # Server for responding to blocked/blackholed responses\n  server {\n    listen 127.0.0.1:9953;\n    listen 127.0.0.1:9853;\n    listen 127.0.0.1:9953 udp;\n    listen 127.0.0.1:9853 udp;\n    js_preread dns.preread_dns_request;\n    return $dns_response;\n  }\n\n}\n"
  },
  {
    "path": "examples/test.conf",
    "content": "user  nginx;\nworker_processes  auto;\n\nload_module modules/ngx_stream_js_module.so;\n\nerror_log  /var/log/nginx/error.log notice;\npid        /var/run/nginx.pid;\n\nevents {\n    worker_connections  1024;\n}\n\nhttp { }\n\n# DNS Stream Services\nstream {\n\n  # DNS logging\n  log_format  dns   '$remote_addr [$time_local] $protocol $status $bytes_sent $bytes_received \"$dns_qname\" \"$upstream_addr\"';\n\n  access_log /var/log/nginx/dns-access.log dns;\n\n  # Import the NJS module\n  js_import /etc/nginx/njs.d/dns/test.js;\n\n  # The $dns_qname variable can be populated by preread calls, and can be used for DNS routing\n  js_set $dns_qname test.get_qname;\n\n  # test server - responds to dns queries\n  server {\n    listen 5553;\n    listen 5553 udp;\n    js_var $test_result;\n    js_preread test.test_dns_encoder;\n    return $test_result;\n  }\n\n  # load balance to test server and parse responses\n  server {\n    listen 5554;\n    listen 5554 udp;\n    proxy_responses 1;\n    js_filter test.test_dns_decoder;\n    proxy_pass 127.0.0.1:5553;\n  }\n\n\n}\n\n"
  },
  {
    "path": "nginx-doh.conf",
    "content": "#\n# This config has bits of DNS/DoT/DoH all over it. See the examples folder for more targeted examples.\n#\n\nuser  nginx;\nworker_processes  auto;\n\nload_module modules/ngx_stream_js_module.so;\n\nerror_log  /var/log/nginx/error.log notice;\npid        /var/run/nginx.pid;\n\nevents {\n    worker_connections  1024;\n}\n\nhttp {\n  include       /etc/nginx/mime.types;\n  default_type  application/octet-stream;\n\n  # logging directives\n  log_format  doh   '$remote_addr - $remote_user [$time_local] \"$request\" '\n                    '[ $msec, $request_time, $upstream_response_time $pipe ] '\n                    '$status $body_bytes_sent \"$http_x_forwarded_for\" '\n                    '$upstream_http_x_dns_question $upstream_http_x_dns_type '\n                    '$upstream_http_x_dns_result '\n                    '$upstream_http_x_dns_ttl $upstream_http_x_dns_answers '\n                    '$upstream_cache_status';\n\n  access_log  /var/log/nginx/doh-access.log doh;\n\n  # This upstream connects to a local Stream service which converts HTTP -> DNS\n  upstream dohloop {\n    zone dohloop 64k;\n    server 127.0.0.1:8053;\n    keepalive_timeout 60s;\n    keepalive_requests 100;\n    keepalive 10;\n  }\n\n  # Proxy Cache storage - so we can cache the DoH response from the upstream\n  proxy_cache_path /var/cache/nginx/doh_cache levels=1:2 keys_zone=doh_cache:10m;\n\n  # The DoH server block\n  server {\n  \n    # Listen on standard HTTPS port, and accept HTTP2, with SSL termination\n    listen 443 ssl http2;\n    ssl_certificate /etc/nginx/ssl/certs/doh.local.pem;\n    ssl_certificate_key /etc/nginx/ssl/private/doh.local.pem;\n    ssl_session_cache shared:ssl_cache:10m;\n    ssl_session_timeout 10m;\n\n    # DoH may use GET or POST requests, Cache both\n    proxy_cache_methods GET POST;\n\n    # Return 404 to all responses, except for those using our published DoH URI\n    location / {\n      return 404 \"404 Not Found\\n\";\n    }\n\n    # This is our published DoH URI \n    location /dns-query {\n\n      # Proxy HTTP/1.1, clear the connection header to enable Keep-Alive\n      proxy_http_version 1.1;\n      proxy_set_header Connection \"\";\n\n      # Enable Cache, and set the cache_key to include the request_body\n      proxy_cache doh_cache;\n      proxy_cache_key $scheme$proxy_host$uri$is_args$args$request_body;\n\n      # proxy pass to the dohloop upstream\n      proxy_pass http://dohloop;\n    }\n\n  }\n\n  # enable API\n  server {\n    listen 8080;\n    location /api {\n      api write=on;\n      allow 127.0.0.1;\n      allow 192.168.64.1;\n      deny all;\n    }\n  }\n\n}\n\n# DNS Stream Services\nstream {\n\n  # KeyValue store for blocking domains (NGINX Plus only)\n  keyval_zone zone=dns_config:64k state=/etc/nginx/zones/dns_config.zone; \n  keyval \"blocked_domains\" $blocked_domains zone=dns_config;\n  keyval \"blackhole_domains\" $blackhole_domains zone=dns_config;\n  keyval $dns_qname $scrub_action zone=dns_config;\n\n  # DNS logging\n  log_format  dns   '$remote_addr [$time_local] $protocol $status $bytes_sent $bytes_received \"$dns_qname\" \"$upstream_addr\"';\n\n  access_log /var/log/nginx/dns-access.log dns;\n\n  # Import the NJS DNS module\n  js_import /etc/nginx/njs.d/dns/dns.js;\n\n  # The $dns_qname variable can be populated by preread calls, and can be used for DNS routing\n  js_set $dns_qname dns.get_qname;\n  \n  # The DNS response packet, if we're blocking the domain, this will be set.\n  js_set $dns_response dns.get_response;\n\n  # When doing DNS routing, use $dns_qname to map the questions to the upstream pools.\n  map $dns_qname $upstream {\n    hostnames;\n    *.nginx dnsmasq;\n    *.k8s dnsmasq;\n    default google;\n  }\n\n  # Set upstream to be the pool defined above if dns_response is empty, else pass to the @block location\n  map $dns_response $upstream_pool {\n    \"blocked\" blocked;\n    \"blackhole\" blackhole;\n    default $upstream;\n  }\n\n  # upstream pool for blocked requests\n  upstream blocked {\n    zone blocked 64k;\n    server 127.0.0.1:9953;\n  }\n\n  upstream blackhole {\n    zone blackhole 64k;\n    server 127.0.0.1:9853;\n  }\n\n  # upstream pools (google DNS)\n  upstream google {\n    zone dns 64k;\n    server 8.8.8.8:53;\n  }\n\n  # upstream pools (another DNS)\n  upstream dnsmasq {\n    zone dns 64k;\n    server 192.168.64.1:5353;\n  }\n\n  # DNS upstream pool.\n  upstream dns {\n    zone dns 64k;\n    server 8.8.8.8:53;\n  }\n\n  # DNS over TLS upstream pool\n  upstream dot {\n    zone dot 64k;\n    server 8.8.8.8:853;\n  }\n\n  # DNS(TCP) and DNS over TLS (DoT) Server\n  # Upstream can be either DNS(TCP) or DoT. If upstream is DNS, proxy_ssl should be off.\n  server {\n    listen 553;\n    listen 853 ssl;\n    ssl_certificate /etc/nginx/ssl/certs/doh.local.pem;\n    ssl_certificate_key /etc/nginx/ssl/private/doh.local.pem;\n    js_preread dns.preread_dns_request;\n    #proxy_ssl on;\n    proxy_pass $upstream_pool;\n  }\n\n  # DNS(UDP) Server\n  # Upstream can only be another DNS(UDP) server.\n  server {\n    listen 553 udp;\n    js_preread dns.preread_dns_request;\n    proxy_responses 1;\n    proxy_pass $upstream_pool;\n  }\n\n  # DNS over HTTPS (gateway) Service\n  # Upstream can be either DNS(TCP) or DoT. If upstream is DNS, proxy_ssl should be off.\n  server {\n    listen 127.0.0.1:8053;\n    js_filter dns.filter_doh_request;\n    proxy_ssl on;\n    proxy_pass dot;\n  }\n\n  # Server for sending blackhole/blocked responses\n  server {\n    listen 127.0.0.1:9953;\n    listen 127.0.0.1:9853;\n    listen 127.0.0.1:9953 udp;\n    listen 127.0.0.1:9853 udp;\n    js_preread dns.preread_dns_request;\n    return $dns_response;\n  }\n\n}\n"
  },
  {
    "path": "nginx-glb.conf",
    "content": "user  nginx;\nworker_processes  auto;\n\nload_module modules/ngx_stream_js_module.so;\nload_module modules/ngx_stream_geoip2_module.so;\n\nerror_log  /var/log/nginx/error.log notice;\npid        /var/run/nginx.pid;\n\nevents {\n    worker_connections  1024;\n}\n\nhttp {\n  include       /etc/nginx/mime.types;\n  default_type  application/octet-stream;\n\n  log_format  main  '$remote_addr - $remote_user [$time_local] \"$request\" '\n                    '$status $body_bytes_sent \"$http_referer\" '\n                    '\"$http_user_agent\" \"$http_x_forwarded_for\"';\n\n  access_log  /var/log/nginx/access.log  main;\n\n  sendfile        on;\n  #tcp_nopush     on;\n\n  keepalive_timeout  65;\n\n  #gzip  on;\n\n  upstream www_netflix_com {\n    zone upstream_www_netflix_com 64k;\n    server 52.49.96.37;    # eu-west-1\n    server 52.51.179.14 backup;\n    server 52.32.190.151;  # us-west-2\n    server 52.41.193.16 backup;\n  }\n\n  match server_ok {\n    status 200-399;\n  }\n\n  server {\n    listen 127.0.0.1:8888;\n    server_name www.netflix.com;\n    set $test \"foobar\";\n    location / {\n      proxy_pass http://www_netflix_com;\n      proxy_set_header Host $host;\n      proxy_http_version 1.1;\n      health_check interval=10 fails=3 passes=3 match=server_ok;\n    }\n  }\n\n  server {\n    listen 127.0.0.1:80;\n    location /api/ {\n      api write=on;\n      allow 127.0.0.1;\n      deny all;\n    }\n  }\n\n}\n\nstream {\n\n  js_include /etc/nginx/njs.d/nginx_stream.js;\n  js_set $glb_response glb_get_response;\n  js_set $edns_subnet glb_get_edns_subnet;\n\n  keyval_zone zone=glb_config:64k state=/etc/nginx/zones/glb_config.zone;\n  keyval \"www_netflix_com\" $www_netflix_com zone=glb_config;\n  keyval \"GLB_USE_EDNS\" $glb_use_edns zone=glb_config;\n\n  # The following keys are needed if s.api() is unavailable\n  keyval \"www_netflix_com_nodes\" $www_netflix_com_nodes zone=glb_config;\n  keyval \"www_netflix_com_geoip_52_49_96_37\" $www_netflix_com_geoip_52_49_96_37 zone=glb_config;\n  keyval \"www_netflix_com_geoip_52_51_179_14\" $www_netflix_com_geoip_52_51_179_14 zone=glb_config;\n  keyval \"www_netflix_com_geoip_52_32_190_151\" $www_netflix_com_geoip_52_32_190_151 zone=glb_config;\n  keyval \"www_netflix_com_geoip_52_41_193_16\" $www_netflix_com_geoip_52_41_193_16 zone=glb_config;\n\n  # set $geoip_source to EDNS if we have one, or the $remote_addr if not\n  map $edns_subnet $geoip_source {\n    \"\" $remote_addr;\n    default $edns_subnet;\n  }\n\n  # get the country, latitude, and longitude from the GeoLite2 City DB\n  geoip2 /etc/geoip/GeoLite2-City.mmdb {\n    $geoip2_country_code default=GB source=$geoip_source country iso_code;\n    $geoip2_latitude default=51.52830 source=$geoip_source location latitude;\n    $geoip2_longitude default=0.0000 source=$geoip_source location longitude;\n  }\n\n  # process the DNS request\n  server {\n    listen 192.168.64.20:53 udp reuseport;\n    js_preread glb_process_request;\n    return $glb_response;\n  }\n\n}\n\n"
  },
  {
    "path": "njs.d/dns/dns.js",
    "content": "import dns from \"libdns.js\";\nexport default {get_qname, get_response, preread_doh_request, preread_dns_request, filter_doh_request};\n\n/**\n * DNS Decode Level\n * 0: No decoding, minimal processing required to strip packet from HTTP wrapper (fastest)\n * 1: Parse DNS Header and Question. We can log the Question, Class, Type, and Result Code\n * 2: As 1, but also parse answers. We can log the answers, and also cache responses in HTTP Content-Cache\n * 3: Very Verbose, log everything as above, but also write packet data to error log (slowest)\n**/\nvar dns_decode_level = 3;\n\n/**\n * DNS Debug Level\n * Specify the decoding level at which we should log packet data to the error log.\n * Default is level 3 (max decoding)\n**/\nvar dns_debug_level = 3;\n\n/**\n * DNS Question Load Balancing\n * Set this to true, if you want to pick the upstream pool based on the DNS Question.\n * Doing so will disable HTTP KeepAlives for DoH so that we can create a new socket for each query\n**/\nvar dns_question_balancing = false;\n\n// The DNS Question name\nvar dns_name = Buffer.alloc(0);\n\nfunction get_qname(s) {\n  return dns_name;\n}\n\n// The Optional DNS response, this is set when we want to block a specific domain\nvar dns_response = Buffer.alloc(0);\n\nfunction get_response(s) {\n  return dns_response.toString();\n}\n\n// Encode the given number to two bytes (16 bit)\nfunction to_bytes( number ) {\n  return Buffer.from( [ ((number>>8) & 0xff), (number & 0xff) ] );\n}\n\nfunction debug(s, msg) {\n  if ( dns_decode_level >= dns_debug_level ) {\n    s.warn(msg);\n  }\n}\n\nfunction process_doh_request(s, decode, scrub) {\n  s.on(\"upstream\", function(data,flags) {\n    if ( data.length == 0 ) {\n      return;\n    }\n    var dataString = data.toString('utf8');\n    const lines = dataString.split(\"\\r\\n\");\n    var bytes;\n    var packet;\n    if(lines[0].startsWith(\"GET\")) {\n      var line = lines[0];\n      var path = line.split(\" \")[1]\n      var params = path.split(\"?\")[1]\n      var qs = params.split(\"&\");\n      qs.some( param => {\n        if (param.startsWith(\"dns=\") ) {\n          bytes = Buffer.from(param.slice(4), \"base64url\");\n          return true;\n        }\n        return false;\n      });\n    }\n\n    if(lines[0].startsWith(\"POST\")) {\n      const index = lines.findIndex(line=>{\n        if(line.length == 0) {\n          return true;\n        }\n      })\n      if(index>0 && lines.length >= index + 1){\n        bytes = Buffer.from(lines[index + 1]);\n      }\n    }\n\n    if (bytes) {\n      debug(s, \"process_doh_request: DNS Req: \" + bytes.toString('hex') );\n      if (decode) {\n        packet = dns.parse_packet(bytes);\n        debug(s, \"process_doh_request: DNS Req ID: \" + packet.id );\n        dns.parse_question(packet);\n        debug(s,\"process_doh_request: DNS Req Name: \" + packet.question.name);\n        dns_name = packet.question.name;\n      }\n      if (scrub) {\n        domain_scrub(s, bytes, packet);\n        s.done();\n      } else {\n        s.send( to_bytes(bytes.length) );\n        s.send( bytes, {flush: true} );\n      }\n    } else {\n      if ( ! scrub) {\n        debug(s, \"process_doh_request: DNS Req: \" + line.toString() );\n        s.send(\"\");\n        data = \"\";\n      }\n    }\n  });\n}\n\nfunction process_dns_request(s, decode, scrub) {\n   s.on(\"upstream\", function(bytes,flags) {\n    if ( bytes.length == 0 ) {\n      return;\n    }\n    var packet;\n    if (bytes) {\n      if (s.variables.protocol == \"TCP\") {\n        // Drop the TCP length field\n        bytes = bytes.slice(2);\n      }\n      debug(s, \"process_dns_request: DNS Req: \" + bytes.toString('hex') );\n      if (decode) {\n        packet = dns.parse_packet(bytes);\n        debug(s, \"process_dns_request: DNS Req ID: \" + packet.id );\n        dns.parse_question(packet);\n        debug(s,\"process_dns_request: DNS Req Name: \" + packet.question.name);\n        dns_name = packet.question.name;\n      }\n      if (scrub) {\n        domain_scrub(s, bytes, packet);\n        s.done();\n      } else {\n        if (s.variables.protocol == \"TCP\") {\n          s.send( to_bytes(bytes.length) );\n        }\n        s.send( bytes, {flush: true} );\n      }\n    }\n  });\n}\n\nfunction domain_scrub(s, data, packet) {\n  var found = false;\n  if ( s.variables.server_port == 9953 ) {\n    dns_response = dns.shortcut_nxdomain(data, packet);\n    if (s.variables.protocol == \"TCP\" ) {\n      dns_response = Buffer.concat( [ to_bytes( dns_response.length ), dns_response ]);\n    }\n    debug(s,\"Scrubbed: Response: \" + dns_response.toString('hex') );\n  } else if ( s.variables.server_port == 9853 ) {\n    var answers = [];\n    if ( packet.question.type == dns.dns_type.A ) {\n      answers.push( {name: packet.question.name, type: dns.dns_type.A, class: dns.dns_class.IN, ttl: 300, rdata: \"0.0.0.0\" } );\n    } else if ( packet.question.type == dns.dns_type.AAAA ) {\n      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\" } );\n    }\n    dns_response = dns.shortcut_response(data, packet, answers);\n    if (s.variables.protocol == \"TCP\" ) {\n      dns_response = Buffer.concat( [ to_bytes( dns_response.length ), dns_response ]);\n    }\n    debug(s,\"Scrubbed: Response: \" + dns_response.toString('hex') );\n  } else {\n    debug(s,\"Scrubbing: Check: Name: \" + packet.question.name );\n    if ( s.variables.scrub_action ) {\n      debug(s, \"Scrubbing: Check: EXACT MATCH: Name: \" + packet.question.name + \", Action: \" + s.variables.scrub_action );\n      dns_response = s.variables.scrub_action;\n      return;\n    } else {\n      [\"blocked\", \"blackhole\"].forEach( function( list ) {\n        if(found) { return };\n        var blocked = s.variables[ list + \"_domains\" ];\n        if ( blocked ) {\n          blocked = blocked.split(',');\n          blocked.forEach( function( domain ) {\n            if (packet.question.name.endsWith( domain )) {\n              debug(s,\"Scrubbing: Check: LISTED: Name: \" + packet.question.name + \", Action: \" + list );\n              dns_response = list;\n              found = true;\n              return;\n            }\n          });\n        }\n      });\n      if(found) { return };\n    }\n    debug(s,\"Scrubbing: Check: NOT FOUND: Name: \" + packet.question.name);\n  }\n}\n\nfunction preread_dns_request(s) {\n  process_dns_request(s, true, true);\n}\n\nfunction preread_doh_request(s) {\n  process_doh_request(s, true, true);\n}\n\nfunction filter_doh_request(s) {\n\n  if ( dns_decode_level >= 3 ) {\n    process_doh_request(s, true, false);\n  } else {\n    process_doh_request(s, false, false);\n  }\n\n  s.on(\"downstream\", function(data, flags) {\n    if ( data.length == 0 ) {\n      return;\n    }\n    // Drop the TCP length field\n    data = data.slice(2);\n\n    debug(s, \"DNS Res: \" + data.toString('hex') );\n    var packet;\n    var answers = \"\";\n    var cache_time = 10;\n    if ( dns_question_balancing ) {\n      s.send(\"HTTP/1.1 200\\r\\nConnection: Close\\r\\nContent-Type: application/dns-message\\r\\nContent-Length:\" + data.length + \"\\r\\n\");\n    } else {\n      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\");\n    }\n\n    if ( dns_decode_level > 0 ) {\n      packet = dns.parse_packet(data);\n      dns.parse_question(packet);\n      dns_name = packet.question.name;\n      s.send(\"X-DNS-Question: \" + dns_name + \"\\r\\n\");\n      s.send(\"X-DNS-Type: \" + dns.dns_type.value[packet.question.type] + \"\\r\\n\");\n      s.send(\"X-DNS-Result: \" + dns.dns_codes.value[packet.codes & 0x0f] + \"\\r\\n\");\n\n      if ( dns_decode_level > 1 ) {\n        if ( dns_decode_level == 2  ) {\n          dns.parse_answers(packet, 2);\n        } else if ( dns_decode_level > 2 ) {\n          dns.parse_complete(packet, 2);\n        }\n        //debug(s, \"DNS Res Answers: \" + JSON.stringify( Object.entries(packet.answers)) );\n        if ( \"min_ttl\" in packet ) {\n          cache_time = packet.min_ttl;\n          s.send(\"X-DNS-TTL: \" + packet.min_ttl + \"\\r\\n\");\n        }\n\n        if ( packet.an > 0 ) {\n          packet.answers.forEach( function(r) { answers += \"[\" + dns.dns_type.value[r.type] + \":\" + r.rdata + \"],\" })\n          answers.slice(0,-1);\n        } else {\n          answers = \"[]\";\n        }\n        s.send(\"X-DNS-Answers: \" +  answers + \"\\r\\n\");\n      }\n      debug(s, \"DNS Res Packet: \" + JSON.stringify( Object.entries(packet)) );\n    }\n\n    var d = new Date( Date.now() + (cache_time*1000) ).toUTCString();\n    if ( ! d.includes(\",\") ) {\n      d = d.split(\" \")\n      d = [d[0] + ',', d[2], d[1], d[3], d[4], d[5]].join(\" \");\n    }\n    s.send(\"Cache-Control: public, max-age=\" + cache_time + \"\\r\\n\" );\n    s.send(\"Expires: \" + d + \"\\r\\n\" );\n\n    s.send(\"\\r\\n\");\n    s.send( data, {flush: true} );\n    if ( dns_question_balancing ) {\n      s.done();\n    }\n  });\n}\n\n"
  },
  {
    "path": "njs.d/dns/glb.js",
    "content": "/**\n\n  BEGIN GLB Functions\n\n**/\n\nimport dns from \"libdns.js\";\nexport default {get_response, get_edns_subnet, process_request};\n\n// Any encoded response packets for NGINX to send back go here \nvar glb_res_packet = String.bytesFrom([]);\n\n// Client subnet gets stored in the variable if we have one\nvar glb_edns_subnet = String.bytesFrom([]);\n\n// Function for js_set to use in order to pick up the glb_res_packet above\nfunction get_response(s) {\n  return glb_res_packet;\n}\n\n// Function to get the EDNS subnet\nfunction get_edns_subnet(s) {\n  return glb_edns_subnet;\n}\n\n// Process a DNS request and generate a response packet, saving it into glb_res_packet\nfunction process_request(s) {\n  s.on(\"upload\", function(data,flags) {\n    s.warn( \"Received: \" + data.toString('hex') );\n    var packet = dns.parse_packet(data);\n    var glb_use_edns = new Boolean(parseInt(s.variables.glb_use_edns));\n    s.warn( \"ID: \" + packet.id );\n    s.warn( \"QD: \" + packet.qd );\n    s.warn( \"AR: \" + packet.ar );\n    if ( packet.qd == 1 ) {\n      dns.parse_question(packet);\n      s.warn(\"Name: \" + packet.question.name);\n\n      // Decode additional records, most clients will send an EDNS (OPT) to increase payload size\n      // and for EDNS Client Subnet, Cookies, etc.\n      if ( packet.ar > 0 ) {\n        // only decode if EDNS is enabled\n        s.warn( \"USE EDNS: \" + glb_use_edns );\n        if ( glb_use_edns ) {\n          dns.parse_complete(packet,1);\n          if ( \"edns\" in packet ) {\n            if ( packet.edns.opts.csubnet ) {\n              s.warn( \"EDNS Subnet: \" + packet.edns.opts.csubnet.subnet );\n              glb_edns_subnet = packet.edns.opts.csubnet.subnet;\n            }\n          }\n        }\n      }\n\n      // Check if we're doing GLB for the given name\n      var config = glb_get_config( packet, \"\", s );\n      if ( ! Array.isArray(config) ) {\n        s.warn(\"Failed to get config for: \" + packet.question.name );\n        glb_res_packet = glb_failure(packet, dns.dns_codes.NXDOMAIN );\n        s.warn( \"Sending: \" + glb_res_packet.toString('hex') );\n        s.done();\n        return;\n      }\n\n      // GSLB this muther\n      var nodes = glb_get_nodes( packet, config, s );\n      if ( ! Array.isArray(nodes) ) {\n        s.warn(\"Failed to get any nodes for: \" + packet.question.name );\n        glb_res_packet = glb_failure(packet, dns.dns_codes.SERVFAIL );\n        s.warn( \"Sending: \" + glb_res_packet.toString('hex') );\n        s.done();\n        return;\n      }\n\n      // Build an array of answers from the nodes\n      var answers = [];\n      if ( config[1] == \"active\" ) {\n        nodes.forEach( function(node) {\n          answers.push( {name: packet.question.name, type: dns.dns_type.A, class: dns.dns_class.IN, ttl: config[2], rdata: node} );\n        });\n      } else if ( config[1] == \"random\" ) {\n        var node = nodes[Math.floor(Math.random()*nodes.length)];\n        answers.push( {name: packet.question.name, type: dns.dns_type.A, class: dns.dns_class.IN, ttl: config[2], rdata: node} );\n      } else if ( config[1] == \"geoip\" ) {\n        var distance=99999999;\n        var closest = [];\n        var client_ip, client_lat, client_lon;\n        /**if ( glb_edns_subnet ) {\n          client_lat = s.variables.edns_latitude;\n          client_lon = s.variables.edns_longitude;\n          client_ip = glb_edns_subnet;\n        } else { **/\n          client_lat = s.variables.geoip2_latitude;\n          client_lon = s.variables.geoip2_longitude;\n          client_ip = s.variables.geoip_source;\n        //}\n        s.warn( \"Client: \" + client_ip + \", Lat: \" + client_lat );\n        s.warn( \"Client: \" + client_ip + \", Lon: \" + client_lon );\n        for (var i=0; i< nodes.length; i++ ) {\n          var suffix = \"_geoip_\" + nodes[i].replace(/\\./g, '_');\n          var node_location = glb_get_config( packet, suffix, s )\n          if ( ! node_location ) {\n            s.warn( \"GEO location missing. Please add GEOIP key for node: \" + nodes[i] );\n            continue;\n          }\n          var nd = glb_calc_distance( client_lon, client_lat,\n                                      node_location[1], node_location[0]);\n          s.warn( \"Distance to: \" + nodes[i] + \" - \" + nd );\n          if ( nd < distance ) {\n            closest = [ nodes[i] ];\n            distance = nd;\n          } else if ( nd == distance ) {\n            closest.push( nodes[i] );\n          }\n        }\n        closest.forEach( function(node) {\n          answers.push( {name: packet.question.name, type: dns.dns_type.A, class: dns.dns_class.IN, ttl: config[2], rdata: node} );\n        });\n      } else {\n        s.warn(\"Unknown LB Algorithm: '\" + config[1] + \"' for: \" + packet.question.name );\n        glb_res_packet = glb_failure(packet, dns.dns_codes.SERVFAIL );\n        s.warn( \"Sending: \" + glb_res_packet.toString('hex') );\n        s.done();\n        return;\n      }\n\n      // Shortcut - copy data from request\n      glb_res_packet = dns.shortcut_response(data, packet, answers);\n\n      // The long way, decode/encode\n      //var response = dns.gen_response_packet( packet, packet.question, answers, [], [] );\n      //glb_res_packet = dns.encode_packet( response );\n\n      s.warn( \"Sending: \" + glb_res_packet.toString('hex') );\n      s.done();\n    }\n  });\n}\n\nfunction glb_failure(packet, code) {\n  var failed = dns.gen_new_packet( packet.id, packet.flags, packet.codes);\n  failed.question = packet.question;\n  failed.qd = 1;\n  failed.codes |= code;\n  failed.flags |= dns.dns_flags.QR;\n  return dns.encode_packet( failed );\n}\n  \nfunction glb_get_config( packet, suffix,  s) {\n  var key = packet.question.name.replace(/\\./g, '_') + suffix;\n  var uri = '/4/stream/keyvals/glb_config';\n  var config;\n  if ( njs.version.slice(0,3) >= 0.9 ) {\n    // future functionality\n    var db = s.api( uri );\n    config = db.read(key);\n  } else {\n    config = s.variables[ key ];\n  }\n  if ( config ) {\n    config = config.split(',');\n  }\n  return config;\n}\n\nfunction glb_get_nodes( packet, config, s ) {\n  var key = packet.question.name.replace(/\\./g, '_');\n  var uri = \"/4/\" + config[0] + \"/upstreams/\" + key;\n  var nodes;\n  if ( njs.version.slice(0,3) >= 0.9 ) {\n    var db = s.api( uri );\n    var json = db.read(key);\n    nodes = glb_process_upstream_status( json, config );\n  } else {\n    // No API, so try _nodes list\n    nodes = s.variables[ key + \"_nodes\" ];\n    nodes = nodes.split(',');\n  }\n  return nodes;\n}\n\nfunction glb_process_upstream_status( json, config ) {\n  // TODO process upstream peers\n  var primary = [];\n  var backup = [];\n}\n\n/**\n * Calculate distance between two GPS locations.\n * Thanks to: https://www.barattalo.it/coding/decimal-degrees-conversion-and-distance-of-two-points-on-google-map/\n**/\nfunction glb_calc_distance(lat1,lon1,lat2,lon2) {\n    var R = 6371; // km (change this constant to get miles)\n    var dLat = (lat2-lat1) * Math.PI / 180;\n    var dLon = (lon2-lon1) * Math.PI / 180;\n    var a = Math.sin(dLat/2) * Math.sin(dLat/2) +\n        Math.cos(lat1 * Math.PI / 180 ) * Math.cos(lat2 * Math.PI / 180 ) *\n        Math.sin(dLon/2) * Math.sin(dLon/2);\n    var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));\n    var d = R * c;\n    if (d>1) return Math.round(d);\n    else if (d<=1) return Math.round(d*1000)+\"m\";\n    return d;\n}\n\n"
  },
  {
    "path": "njs.d/dns/libdns.js",
    "content": "/**\n\n  BEGIN DNS Functions\n\n**/\n\nexport default {dns_type, dns_class, dns_flags, dns_codes,\n                parse_packet, parse_question, parse_answers, \n                parse_complete, parse_resource_record,\n                shortcut_response, shortcut_nxdomain,\n                gen_new_packet, gen_response_packet, encode_packet}\n\n// DNS Types\nvar dns_type = Object.freeze({\n  A:     1,\n  NS:    2,\n  CNAME: 5,\n  SOA:   6,\n  PTR:   12,\n  MX:    15,\n  TXT:   16,\n  AAAA:  28,\n  SRV:   33,\n  OPT:   41,\n  HTTPS: 65,\n  AXFR:  252,\n  ANY:   255,\n  value: { 1:\"A\", 2:\"NS\", 5:\"CNAME\", 6:\"SOA\", 12:\"PTR\", 15:\"MX\", 16:\"TXT\",\n           28:\"AAAA\", 33:\"SRV\", 41:\"OPT\", 65:\"HTTPS\", 252:\"AXFR\", 255:\"ANY\" }\n});\n\n// DNS Classes\nvar dns_class = Object.freeze({\n  IN: 1,\n  CS: 2,\n  CH: 3,\n  HS: 4,\n  value: { 1:\"IN\", 2:\"CS\", 3:\"CH\", 4:\"HS\" }\n});\n\n// DNS flags (made up of QR, Opcode (4bits), AA, TrunCation, Recursion Desired)\nvar dns_flags = Object.freeze({\n  QR: 0x80,\n  AA: 0x4,\n  TC: 0x2,\n  RD: 0x1\n});\n\n// DNS Codes (made up of RA (Recursion Available), Zero (3bits), Response Code (4bits))\nvar dns_codes = Object.freeze({\n  RA:       0x80,\n  Z:        0x70,\n  //RCODE:    0xf,\n  NOERROR:  0x0,\n  FORMERR:  0x1,\n  SERVFAIL: 0x2,\n  NXDOMAIN: 0x3,\n  NOTIMPL:  0x4,\n  REFUSED:  0x5,\n  value: { 0x80:\"RA\", 0x70:\"Z\", 0x0:\"NOERROR\", 0x1:\"FORMERR\", 0x2:\"SERVFAIL\", 0x3:\"NXDOMAIN\", 0x4:\"NOTIMPL\", 0x5:\"REFUSED\" }\n});\n\n// Encode the given number to two bytes (16 bit)\nfunction to_bytes( number ) {\n  return Buffer.from( [ ((number>>8) & 0xff), (number & 0xff) ] );\n}\n\n// Encode the given number to 4 bytes (32 bit)\nfunction to_bytes32( number ) {\n  return Buffer.from( [ (number>>24)&0xff, (number>>16)&0xff, (number>>8)&0xff, number&0xff ] );\n}\n\n// Create a new empty DNS packet structure\nfunction gen_new_packet(id, flags, codes) {\n  var dns_packet = { id: id, flags: flags, codes: codes, qd: 0, an: 0, ns: 0, ar: 0,\n    question: {},\n    answers: [],\n    authority: [],\n    additional: []\n  };\n  return dns_packet;\n}\n\n/** Create a new response packet suitable as a reply to the given request\n *  You should also supply some answers, authority and/or additional records\n *  in arrays to populate the various sections.\n**/\nfunction gen_response_packet( request, question, answers, authority, additional ) {\n  var response = gen_new_packet(request.id, request.flags, request.codes);\n  response.flags |= dns_flags.AA + dns_flags.QR;\n  response.codes |= dns_codes.RA;\n  if ( question == null ) {\n    response.qd = 0;\n  } else {\n    response.qd = 1;\n    response.question = request.question;\n  }\n  answers.forEach( function(answer) {\n    response.an++;\n    response.answers.push( answer );\n  });\n  return response;\n}\n\n/** Encode the provided packet, converting it from the javascript object structure into a bytestring\n *  Returns a bytestring suitable for dropping into a UDP packet, or returning to NGINX\n**/\nfunction encode_packet( packet ) {\n  var encoded = Buffer.from( to_bytes( packet.id ) );\n  encoded = Buffer.concat( [ encoded, Buffer.from([ packet.flags ])] );\n  encoded = Buffer.concat( [ encoded, Buffer.from([ packet.codes ])] );\n  encoded = Buffer.concat( [ encoded, Buffer.from( to_bytes( packet.qd ))] ); // Questions\n  encoded = Buffer.concat( [ encoded, Buffer.from( to_bytes( packet.answers.length ))] ); // Answers\n  encoded = Buffer.concat( [ encoded, Buffer.from( to_bytes( packet.authority.length ))] ); // Authority\n  encoded = Buffer.concat( [ encoded, Buffer.from( to_bytes( packet.additional.length ))] ); // Additional\n  encoded = Buffer.concat( [ encoded, encode_question(packet) ]);\n  packet.answers.forEach( function(answer) {\n    encoded = Buffer.concat( [ encoded, gen_resource_record(packet, answer.name, answer.type, answer.class, answer.ttl, answer.rdata) ]);\n  });\n  packet.authority.forEach( function(auth) {\n    encoded = Buffer.concat( [ encoded, gen_resource_record(packet, auth.name, auth.type, auth.class, auth.ttl, auth.rdata)] );\n  });\n  packet.additional.forEach( function(adtnl) {\n    encoded = Buffer.concat( [ encoded, gen_resource_record(packet, adtnl.name, adtnl.type, adtnl.class, adtnl.ttl, adtnl.rdata)] );\n  });\n  return encoded;\n}\n\n/** Don't mess about. This is a shortcut for responding to DNS Queries. We copy the question out of the query\n *  and cannibalise the original request to generate our response.\n**/\nfunction shortcut_response(data, packet, answers) {\n  var response = Buffer.alloc(0);\n  response = Buffer.concat( [ response, data.slice(0,2) ] );\n  response = Buffer.concat( [ response, Buffer.from([ (packet.flags |= dns_flags.AA | dns_flags.QR) ])] );\n  response = Buffer.concat( [ response, Buffer.from([ (packet.codes |= dns_codes.RA) ])] );\n  // append counts: qd, answer count, 0 auths, 0 additional\n  response = Buffer.concat( [ response, Buffer.from([ 0x00, 0x01 ]), Buffer.from( to_bytes(answers.length)), Buffer.from( [0x0, 0x0, 0x0, 0x0 ]) ] );\n  response = Buffer.concat( [ response, data.slice(12, packet.question.qend ) ] );\n  answers.forEach( function(answer) {\n    response = Buffer.concat( [ response, gen_resource_record(packet, answer.name, answer.type, answer.class, answer.ttl, answer.rdata) ]);\n  });\n  return response;\n}\n\nfunction shortcut_nxdomain(data, packet) {\n  var response = Buffer.alloc(0);\n  response = Buffer.concat( [ response, data.slice(0,2) ] );\n  response = Buffer.concat( [ response, Buffer.from([ (packet.flags |= dns_flags.AA | dns_flags.QR) ])] );\n  response = Buffer.concat( [ response, Buffer.from([ (packet.codes |= dns_codes.NXDOMAIN | dns_codes.RA) ])] );\n  // append counts: qd, answer count, 0 auths, 0 additional\n  response = Buffer.concat( [ response, Buffer.from([ 0x00, 0x01, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0 ]) ] );\n  response = Buffer.concat( [ response, data.slice(12, packet.question.qend ) ] );\n  return response;\n}\n\n/** Encode a question object into a bytestring suitable for use in a UDP packet\n**/\nfunction encode_question(packet) {\n    var encoded = Buffer.from( encode_label(packet.question.name) );\n    encoded = Buffer.concat( [ encoded, Buffer.from(to_bytes(packet.question.type)), Buffer.from(to_bytes(packet.question.class)) ] );\n    return encoded;\n}\n\n/**\n *  Parse an incoming request bytestring into a DNS packet object. This function decodes the first 12 bytes of the headers.\n *  You will probably want to call parse_question() next.\n**/\nfunction parse_packet(data) {\n  var packet = { id: data.readUInt16BE(0), flags: data[2], codes: data[3], min_ttl: 2147483647,\n    qd: data.readUInt16BE(4), an: data.readUInt16BE(6), ns: data.readUInt16BE(8),\n    ar: data.readUInt16BE(10), data: data.slice(12), question: [], answers:[], authority: [], additional: [], offset: 0 };\n  return packet;\n}\n\n/**\n *  Parse the question section of a DNS request packet, adds the QNAME, QTYPE, and QCLASS to the packet object, and stores the\n *  offset in the packet for processing any further sections.\n**/\nfunction parse_question(packet) {\n\n  /** QNAME, QTYPE, QCLASS **/\n\n  var name = parse_label(packet);\n  packet.question = { name: name, type: packet.data.readUInt16BE(packet.offset), \n                      class: packet.data.readUInt16BE(packet.offset+2), qend: packet.offset + 16 };\n  packet.offset += 4;\n  if ( packet.qd != 1 ) {\n    return false;\n  }\n  return true;\n}\n\nfunction parse_answers(packet, decode_level) {\n  \n  // Process the question section if necessary\n  if ( packet.question.length == 0 ) {\n    parse_question(packet);\n  }\n\n  // Process answers\n  if ( packet.an > 0 && packet.answers.length == 0 ) {\n    packet.answers = parse_section(packet, packet.an, decode_level);\n  }\n\n  // If we didn't have any ttls in the packet, then cache for 5 minutes.\n  if (packet.min_ttl == 2147483647) {\n    packet.min_ttl = 300;\n  }\n\n}\n\n\n// Parse all sections of the packet\nfunction parse_complete(packet, decode_level) {\n\n  // Process the question section if necessary\n  if ( packet.question.length == 0 ) {\n    parse_question(packet);\n  }\n\n  // Process answers\n  if ( packet.an > 0 && packet.answers.length == 0 ) {\n    packet.answers = parse_section(packet, packet.an, decode_level);\n  }\n\n  // Process authority\n  if ( packet.ns > 0 && packet.authority.length == 0) {\n    packet.authority = parse_section(packet, packet.ns, decode_level);\n  }\n\n  // Process Additional\n  if ( packet.ar > 0 && packet.additional.length == 0) {\n    packet.additional = parse_section(packet, packet.ar, decode_level);\n  }\n\n  // If we didn't have any ttls in the packet, then cache for 5 minutes.\n  if (packet.min_ttl == 2147483647) {\n    packet.min_ttl = 300;\n  }\n\n}\n\nfunction parse_section(packet, recs, decode_level) {\n  var rrs = [];\n  for (var i=0; i<recs; i++) {\n    var rec = parse_resource_record(packet, decode_level);\n    rrs.push(rec);\n    if ( rec.ttl < packet.min_ttl ) {\n      packet.min_ttl = rec.ttl;\n    }\n  }\n  return rrs;\n}\n\nfunction parse_label(packet) {\n\n  var name = \"\";\n  var compressed = false;\n  var pos = packet.offset;\n  for ( ; pos < packet.data.length; ) {\n    var length = packet.data[pos];\n    if (length == 0) {\n      // null label, name is finished\n      pos++;\n      break;\n    } else if ( length == 192 ) {\n      // compression pointer\n      if ( compressed ) {\n        pos++;\n      } else {\n        packet.offset = ++pos + 1;\n      }\n      pos = packet.data[pos];;\n      if ( pos < 12 ) {\n        // This shouldn't be possible, the header is 12 bytes so a compression pointer can't be less than 12\n        //s.warn(\"DNS Error - parse_label encountered impossible compression pointer\");\n        break;\n      } else {\n        pos = pos - 12;\n        compressed = true;\n      }\n    } else if ( length > 63 ) {\n      // Invalid DNS name, individual labels are limited to 63 bytes.\n        //s.warn(\"DNS Error - parse_label encountered invaliad DNS name\");\n        break;\n    } else {\n      name += packet.data.slice(++pos, pos+length) + \".\"; \n      pos += length;\n    }\n  }\n\n  if ( ! compressed ) {\n    packet.offset = pos\n  }\n\n  name = name.slice(0,-1);\n  return name;\n}\n \n/** TODO Check sizes on resources/packets\nlabels          63 octets or less\nnames           255 octets or less\nTTL             positive values of a signed 32 bit number.\nUDP messages    512 octets or less\n**/\n\nfunction encode_label( name ) {\n\n  var data = Buffer.alloc(0);\n  name.split('.').forEach( function(part){\n    data = Buffer.concat( [ data, Buffer.from([ part.length ]), Buffer.from(part) ] );\n  });\n  data = Buffer.concat( [data, Buffer.from([0]) ]);\n  return data;\n\n}\n\nfunction gen_resource_record(packet, name, type, clss, ttl, rdata) {\n\n  /**\n    NAME\n    TYPE (2 octets)\n    CLASS (2 octects)\n    TTL 32bit signed int\n    RDLength 16bit int length of RDATA\n    RDATA variable length string\n  **/\n\n  var resource\n  var record = \"\";\n\n  if ( name == packet.question.name ) {\n    // The name matches the query, set a compression pointer.\n    resource = Buffer.from([192, 12]);\n  } else {\n    // gen labels for the name\n    resource = encode_label(name);\n  }\n  \n  resource = Buffer.concat( [ resource, Buffer.from([ type & 0xff00, type & 0xff ]) ]);\n  switch(type) {\n    case dns_type.A:\n      record = encode_arpa_v4(rdata);\n      break;\n    case dns_type.AAAA:\n      record = encode_arpa_v6(rdata);\n      break;\n    case dns_type.NS:\n      record = encode_label(rdata);\n      break;\n    case dns_type.CNAME:\n      record = encode_label(rdata);\n      break;\n    case dns_type.SOA:\n      record = encode_soa_record(rdata);\n      break;\n    case dns_type.SRV:\n      record = encode_srv_record(rdata);\n      break;\n    case dns_type.MX:\n      record = encode_mx_record(rdata);\n      break;\n    case dns_type.TXT:\n      record = encode_txt_record(rdata);\n      break;\n    default:\n      //TODO Barf\n  }\n\n  switch(clss) {\n    case dns_class.IN:\n      resource = Buffer.concat([ resource, Buffer.from( [ 0, 1 ] )]);\n      break;\n    default:\n      //TODO Barf\n      resource = Buffer.concat([ resource, Buffer.from( [ 99, 99 ] )]);\n  }\n\n  resource = Buffer.concat( [ resource, Buffer.from(to_bytes32(ttl)) ] );\n  resource = Buffer.concat( [ resource, Buffer.from(to_bytes( record.length )) ] );\n  resource = Buffer.concat( [ resource, Buffer.from(record) ] );\n  return resource;\n}\n\n// Process resource records, to a varying depth dictated by decode_level\n// decode_level {0: name+type, 1: name+type+class+ttl, 2: everything}\nfunction parse_resource_record(packet, decode_level) {\n\n  /**\n    NAME\n    TYPE (2 octets)\n    CLASS (2 octects)\n    TTL 32bit signed int\n    RDLength 16bit int length of RDATA\n    RDATA variable length string\n  **/\n\n  var resource = {}\n  resource.name = parse_label(packet);\n  resource.type = packet.data.readUInt16BE(packet.offset);\n  packet.offset += 2;\n\n  if ( decode_level > 0 ) {\n    if (resource.type == dns_type.OPT ) {\n      // EDNS\n      parse_edns_options(packet);\n    } else {\n      resource.class = packet.data.readUInt16BE(packet.offset);\n      resource.ttl = packet.data.readUInt32BE(packet.offset+2);\n      resource.rdlength = packet.data.readUInt16BE(packet.offset+6);\n      packet.offset +=8;\n      if ( decode_level == 1 ) {\n        resource.rdata = packet.data.slice(packet.offset, packet.offset + resource.rdlength);\n        packet.offset += resource.rdlength;\n      } else {\n        switch(resource.type) {\n          case dns_type.A:\n            resource.rdata = parse_arpa_v4(packet, resource);\n            break;\n          case dns_type.AAAA:\n            resource.rdata = parse_arpa_v6(packet, resource);\n            break;\n          case dns_type.NS:\n            resource.rdata = parse_label(packet);\n            break;\n          case dns_type.CNAME:\n            resource.rdata = parse_label(packet);\n            break;\n          case dns_type.SOA:\n            resource.rdata = parse_soa_record(packet);\n            break;\n          case dns_type.SRV:\n            resource.rdata = parse_srv_record(packet);\n            break;\n          case dns_type.MX:\n            resource.rdata = parse_mx_record(packet);\n            break;\n          case dns_type.TXT:\n            resource.rdata = parse_txt_record(packet, resource.rdlength);\n            break;\n          default:\n            resource.rdata = packet.data.slice(packet.offset, packet.offset + resource.rdlength);\n            packet.offset += resource.rdlength;\n        }\n      }\n    }\n  }\n  return resource;\n}\n\nfunction encode_arpa_v4( ipv4 ) {\n  var rdata = Buffer.alloc(4);\n  var index = 0;\n  ipv4.split('\\.').forEach( function(octet) {\n    rdata[index++] = octet;\n  });\n  return rdata;\n}\n\nfunction parse_arpa_v4(packet) {\n  var octet = [0,0,0,0];\n  for (var i=0; i< 4 ; i++ ) {\n    octet[i] = packet.data[packet.offset++];\n  }\n  return octet.join(\".\");\n}\n\nfunction encode_arpa_v6( ipv6 ) {\n  var rdata = Buffer.alloc(0);\n  ipv6.split(':').forEach( function(segment) {\n    rdata = Buffer.concat( [ rdata, Buffer.from( segment[0] + segment[1], 'hex') ] );\n    rdata = Buffer.concat( [ rdata, Buffer.from( segment[2] + segment[3], 'hex') ] );\n  });\n  return rdata;\n}\n\nfunction parse_arpa_v6(packet) {\n  var ipv6 = \"\";\n  for (var i=0; i<8; i++ ) {\n    ipv6 += packet.data.toString('hex', packet.offset++, ++packet.offset) + \":\";\n  }\n  return ipv6.slice(0,-1);\n}\n\nfunction encode_txt_record( text_array ) {\n  var rdata = Buffer.alloc(0);\n  text_array.forEach( function(text) {\n    var tl = text.length;\n    if ( tl > 255 ) {\n      for (var i=0 ; i < tl ; i++ ) {\n        var len = (tl > (i+255)) ? 255 : tl - i;\n        rdata = Buffer.concat( [ rdata, Buffer.from([len]), Buffer.from(text.slice(i,i+len)) ] );\n        i += len;\n      }\n    } else { \n      rdata = Buffer.concat( [ rdata, Buffer.from([tl]), Buffer.from(text) ] );\n    }\n  });\n  return rdata;\n}\n\nfunction parse_txt_record(packet, length) {\n  var txt = [];\n  var pos = 0;\n  while ( pos < length ) {\n    var tl = packet.data[packet.offset++];\n    txt.push( packet.data.toString('utf8', packet.offset, packet.offset + tl));\n    pos += tl + 1;\n    packet.offset += tl;\n  }\n  return txt;\n}\n\nfunction encode_mx_record( mx ) {\n  var rdata = Buffer.alloc(0);\n  rdata += to_bytes( mx.priority );\n  rdata += encode_label( mx.exchange );\n  return rdata;\n}\n\nfunction parse_mx_record(packet) {\n  var mx = {};\n  mx.priority = packet.data.readUInt16BE(packet.offset);\n  packet.offset += 2;\n  mx.exchange = parse_label(packet);\n  return mx;\n}\n\nfunction encode_srv_record( srv ) {\n  var rdata = Buffer.alloc(6)\n  rdata.writeInt16BE( srv.priority, 0 );\n  rdata.writeInt16BE( srv.weight, 2 );\n  rdata.writeInt16BE( srv.port, 4 );\n  rdata = Buffer.concat( [ rdata, encode_label( srv.target ) ]);\n  ngx.log( ngx.WARN, rdata.toString('hex'));\n  return rdata;\n}\n\nfunction parse_srv_record(packet) {\n  var srv = {};\n  srv.priority = packet.data.readUInt16BE(packet.offset);\n  srv.weight = packet.data.readUInt16BE(packet.offset+2);\n  srv.port = packet.data.readUInt16BE(packet.offset+4);\n  packet.offset += 6;\n  srv.target = parse_label(packet);\n  return srv;\n}\n\nfunction encode_soa_record( soa ) {\n  var rdata = Buffer.concat([ encode_label(soa.primary), encode_label(soa.mailbox) ]);\n  rdata = Buffer.concat( [ rdata, Buffer.from(to_bytes32(soa.serial)), Buffer.from(to_bytes32(soa.refresh)), \n          Buffer.from(to_bytes32(soa.retry)), Buffer.from(to_bytes32(soa.expire)), Buffer.from(to_bytes32(soa.minTTL)) ]);\n  return rdata;\n}\n\nfunction parse_soa_record(packet) {\n  var soa = {};\n  soa.primary = parse_label(packet);\n  soa.mailbox = parse_label(packet);\n  soa.serial  = packet.data.readUInt32BE(packet.offset);\n  soa.refresh = packet.data.readUInt32BE(packet.offset+=4);\n  soa.retry   = packet.data.readUInt32BE(packet.offset+=4);\n  soa.expire  = packet.data.readUInt32BE(packet.offset+=4);\n  soa.minTTL  = packet.data.readUInt32BE(packet.offset+=4);\n  packet.offset +=4;\n  return soa;\n}\n\nfunction parse_edns_options(packet) {\n\n  packet.edns = {}\n  packet.edns.opts = {}\n  packet.edns.size = packet.data.readUInt16BE(packet.offset);\n  packet.edns.rcode = packet.data[packet.offset+2];\n  packet.edns.version = packet.data[packet.offset+3];\n  packet.edns.z = packet.data.readUInt16BE(packet.offset+4);\n  packet.edns.rdlength = packet.data.readUInt16BE(packet.offset+6);\n  packet.offset += 8;\n\n  var end = packet.offset + packet.edns.rdlength;\n  for ( ; packet.offset < end ; ) {\n    var opcode = packet.data.readUInt16BE(packet.offset);\n    var oplength = packet.data.readUInt16BE(packet.offset+2);\n    packet.offset += 4;\n    if ( opcode == 8 ) {\n      //client subnet\n      packet.edns.opts.csubnet = {}\n      packet.edns.opts.csubnet.family = packet.data.readUInt16BE(packet.offset);\n      packet.edns.opts.csubnet.netmask = packet.data[packet.offset+2];\n      packet.edns.opts.csubnet.scope = packet.data[packet.offset+3];\n      packet.offset += 4;\n      if ( packet.edns.opts.csubnet.family == 1 ) {\n        // IPv4\n        var octet = [0,0,0,0];\n        for (var i=4; i< oplength ; i++ ) {\n          octet[i-4] = packet.data[packet.offset++];\n        }\n        packet.edns.opts.csubnet.subnet = octet.join(\".\");\n        break;\n      } else {\n        // We don't support IPv6 yet.\n        packet.edns.opts = {}\n        break;\n      }\n    } else {\n      // We only look for CSUBNET... Not interested in anything else at this time.\n      packet.offset += oplength;\n    }\n  }\n\n}\n\n\n"
  },
  {
    "path": "njs.d/dns/test.js",
    "content": "\nimport dns from \"libdns.js\";\nexport default {get_qname, test_dns_encoder, test_dns_decoder};\n\n/**\n * DNS Decode Level\n * 0: No decoding, minimal processing required to strip packet from HTTP wrapper (fastest)\n * 1: Parse DNS Header and Question. We can log the Question, Class, Type, and Result Code\n * 2: As 1, but also parse answers. We can log the answers, and also cache responses in HTTP Content-Cache\n * 3: Very Verbose, log everything as above, but also write packet data to error log (slowest)\n**/\nvar dns_decode_level = 3;\n\n/**\n * DNS Debug Level\n * Specify the decoding level at which we should log packet data to the error log.\n * Default is level 3 (max decoding)\n**/\nvar dns_debug_level = 3;\n\n// The DNS Question name\nvar dns_name = Buffer.alloc(0);\n\nfunction get_qname(s) {\n  return dns_name;\n}\n\n// Encode the given number to two bytes (16 bit)\nfunction to_bytes( number ) {\n  return Buffer.from( [ ((number>>8) & 0xff), (number & 0xff) ] );\n}\n\nfunction debug(s, msg) {\n  if ( dns_decode_level >= dns_debug_level ) {\n    s.warn(msg);\n  }\n}\n\nfunction test_dns_encoder(s) {\n  s.on(\"upstream\", function(data,flags) {\n    var packet;\n    var test_result = Buffer.alloc(0);\n    if ( data.length == 0 ) {\n      return;\n    }\n    if (data) {\n      if (s.variables.protocol == \"TCP\") {\n        // Drop the TCP length field\n        data = data.slice(2);\n      }\n      debug(s, \"test_dns: DNS Encoder Req: \" + data.toString('hex') );\n      packet = dns.parse_packet(data);\n      dns.parse_question(packet);\n      dns_name = packet.question.name;\n      debug(s, \"test_dns: DNS Encoder Request Packet: \" + JSON.stringify( Object.entries(packet)) );\n      test_result = test_dns_responder(s, data, packet);\n      delete packet.data; // remove the data buffer before printing\n      debug(s, \"test_dns: DNS Encoder Response Packet: \" + JSON.stringify( Object.entries(packet)) );\n      debug(s, \"test_dns: DNS Encoder Res: \" + test_result.toString('hex') );\n      s.variables.test_result = test_result;\n      s.done();\n    }\n  });\n}\n\n\nfunction test_dns_decoder(s) {\n  s.on(\"downstream\", function(data,flags) {\n    var packet;\n    var test_result = Buffer.alloc(0);\n    if ( data.length == 0 ) {\n      return;\n    }\n    if (data) {\n      if (s.variables.protocol == \"TCP\") {\n        // Drop the TCP length field\n        data = data.slice(2);\n      }\n      debug(s, \"test_dns: DNS Decoder Res: \" + data.toString('hex') );\n      packet = dns.parse_packet(data);\n      dns.parse_question(packet);\n      dns_name = packet.question.name;\n      dns.parse_complete(packet, 2);\n      delete packet.data; // remove the data buffer before printing\n      debug(s, \"test_dns: DNS Decoder Response Packet: \" + JSON.stringify( Object.entries(packet)) );\n      if (s.variables.protocol == \"TCP\") {\n        s.send( to_bytes(data.length) );\n      }\n      s.send( data, {flush: true} );\n    }\n  });\n}\n\n/**\n *  Function to perform testing of DNS packet generation for various DNS types\n *  Any domain ending bar.com will use the shortcut_response path \n *  Any domains ending baz.com will use shortcut_nxdomain path\n *  All other queries will return an appropriate set of DNS records.\n**/\nfunction test_dns_responder(s, data, packet) {\n  var answers = [];\n  var test_result;\n\n  if ( packet.question.type == dns.dns_type.A || packet.question.type == dns.dns_type.ANY ) {\n    answers.push( {name: packet.question.name, type: dns.dns_type.A, class: dns.dns_class.IN, ttl: 300, rdata: \"10.2.3.4\" } );\n  } else if ( packet.question.type == dns.dns_type.AAAA ) {\n    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\" } );\n  } else if ( packet.question.type == dns.dns_type.CNAME ) {\n    answers.push( {name: packet.question.name, type: dns.dns_type.CNAME, class: dns.dns_class.IN, ttl: 300, rdata: \"www.foo.bar.baz\" } );\n  } else if ( packet.question.type == dns.dns_type.NS ) {\n    answers.push( {name: packet.question.name, type: dns.dns_type.NS, class: dns.dns_class.IN, ttl: 300, rdata: \"ns1.foo.bar.baz\" } );\n    answers.push( {name: packet.question.name, type: dns.dns_type.NS, class: dns.dns_class.IN, ttl: 300, rdata: \"ns2.foo.bar.baz\" } );\n  } else if ( packet.question.type == dns.dns_type.TXT ) {\n    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\"] } );\n  } else if ( packet.question.type == dns.dns_type.MX ) {\n    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\"} } );\n    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\"} } );\n  } else if ( packet.question.type == dns.dns_type.SRV ) {\n    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\"} } );\n  } else if ( packet.question.type == dns.dns_type.SOA ) {\n    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} } );\n  }\n\n  if ( packet.question.name.toString().endsWith(\"bar.com\") ) {\n    test_result = dns.shortcut_response(data, packet, answers);\n  } else if ( packet.question.name.toString().endsWith(\"baz.com\") ) {\n    test_result = dns.shortcut_nxdomain(data, packet);\n  } else {\n    packet.flags |= dns.dns_flags.AA | dns.dns_flags.QR;\n    packet.codes |= dns.dns_codes.RA;\n    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} });\n    packet.additional.push( {name: packet.question.name, type: dns.dns_type.NS, class: dns.dns_class.IN, ttl: 300, rdata: \"ns1.foo.bar.baz\" } );\n    packet.additional.push( {name: packet.question.name, type: dns.dns_type.NS, class: dns.dns_class.IN, ttl: 300, rdata: \"ns2.foo.bar.baz\" } );\n       packet.answers = answers;\n    test_result = dns.encode_packet(packet);\n  }\n  if (s.variables.protocol == \"TCP\" ) {\n    test_result = Buffer.concat( [ to_bytes( test_result.length ), test_result ]);\n  }\n  return test_result;\n}\n\n"
  },
  {
    "path": "ssl/certs/doh.local.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIID0TCCArmgAwIBAgIUOdQrJG61Cs5p1PRwIOYzUClAZZkwDQYJKoZIhvcNAQEL\nBQAweDELMAkGA1UEBhMCR0IxEjAQBgNVBAgMCUNhbWJyaWRnZTESMBAGA1UEBwwJ\nQ2FtYnJpZGdlMRIwEAYDVQQKDAlOR0lOWCBJbmMxGTAXBgNVBAsMEE5vdyBhIHBh\ncnQgb2YgRjUxEjAQBgNVBAMMCWRvaC5sb2NhbDAeFw0xOTA5MjgxNDU2MzNaFw0y\nMDA5MjcxNDU2MzNaMHgxCzAJBgNVBAYTAkdCMRIwEAYDVQQIDAlDYW1icmlkZ2Ux\nEjAQBgNVBAcMCUNhbWJyaWRnZTESMBAGA1UECgwJTkdJTlggSW5jMRkwFwYDVQQL\nDBBOb3cgYSBwYXJ0IG9mIEY1MRIwEAYDVQQDDAlkb2gubG9jYWwwggEiMA0GCSqG\nSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDSnQpcd9PElrimhx1Absbf4SafKPpM+7Nh\nEVBFJ5Emtxksz1tUsi1mXEQsf9sCKeURvrzoUwyUkkF4Frks14L/+GCXEpCJSrga\nNhhSO6QR0xZ26jXFqwwsE3QkW6URNGZ5IEecI+2JAUiMxhdmO9oEPvRzDmyDoUTT\ndmt6+y0NahrU47OP88yie0Jt1+Mh18U/RQKRUYZz1L4oHV1sujuemDbF7xSkguvV\nEhUMF+316HQNPndrZRVIYjfMUT32qnvlnOKzgB4mNh8biRLekwsPplFuU5vhnUxR\n5pDw4JzNT5Mis8I8+ULUkKKK3wF7Ih3Wp6vMgq/i6CvqcYA/n8LzAgMBAAGjUzBR\nMB0GA1UdDgQWBBSFF5wbFm0N3FXIu14wGAfHIIEDyjAfBgNVHSMEGDAWgBSFF5wb\nFm0N3FXIu14wGAfHIIEDyjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUA\nA4IBAQCHIjdUCNKHnk06jyajT8rKMdjpJQkv6P2nFr0Sf0/1ZtftK+dgA5O3HJmY\naPJVGZlfdOWavYT3i+OrLpSVCwGoCt/V1rSgw6E9zfEarsVdtiZzd9h/HhvOdDGd\nSE1EUJveIoe46DpdeD+pSz068+1WKK7UahArupsXjlcaoCVp7uvLTacP9NPMP6jp\naiOotgZUxHEoelseEgyFGDOyP32OTZv8vGQFTaNLS3zKP8iZNNmfwX1pirY5TJzP\nHcbKgT8aki9+U64vkjoUrvpA8y4U8b7NgKFowkLl7rbHxNqZmstq+YI13RwgNcYG\nkEbvwju0TuQ52TgpKkA3D5fiWhSZ\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "ssl/private/doh.local.pem",
    "content": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDSnQpcd9PElrim\nhx1Absbf4SafKPpM+7NhEVBFJ5Emtxksz1tUsi1mXEQsf9sCKeURvrzoUwyUkkF4\nFrks14L/+GCXEpCJSrgaNhhSO6QR0xZ26jXFqwwsE3QkW6URNGZ5IEecI+2JAUiM\nxhdmO9oEPvRzDmyDoUTTdmt6+y0NahrU47OP88yie0Jt1+Mh18U/RQKRUYZz1L4o\nHV1sujuemDbF7xSkguvVEhUMF+316HQNPndrZRVIYjfMUT32qnvlnOKzgB4mNh8b\niRLekwsPplFuU5vhnUxR5pDw4JzNT5Mis8I8+ULUkKKK3wF7Ih3Wp6vMgq/i6Cvq\ncYA/n8LzAgMBAAECggEABDnYcmCJJEGt9NFzOc6/ONDIuJrW4uKOB92UEb8of3Ff\nFPIYMAvfM1WYnJf4KgPzL7b3DWZVM0n3/FPgZVDxtPcj4QQjWE3igcwiEsxVj3H/\n2mT6rTuwY9YEF5KrLjwx7i5CoZRq+LvI2+JBp/B9gGZO+1wHu2BqBCA1KeOOVN2J\n2FDLt32sMLrY4QQqFefYGk3gdzD06qyjRbpSiymaDOfK7D1xr6iNqGZHOl8eu8I4\nzHBVwuE9L3qPGY+bH1rTWSBrV/hIJSKhXom4PbNz23KpfrgpGJ+yP8hspEUgtGiH\n0Et9o59zKju77sLkZvhLOQSy8Y/yCpn9qNLqoNl8QQKBgQDs4lXCYkaQNMErGYpf\n2+T7fRyuRpCedmoM70p1EmwtLi2ufIWPWPr+7NpvIdmXZb02rzE5gMMAROmC0c7b\n9n7jqBMN8LjdG2yeyl0a1rxxE/eMrYqZ/SIEaxUnam7B50Re767/uUL8Uq2UOuDi\n9e1g6LzvXixM7m1JpS4pkrDkYQKBgQDjm/5DqmOD/cfLbpNhxnLiQkJEHt553+mh\nxXWaxugBGjRyFlXfaRNAoJa5D7+VRc+iFNPF9CwYYMV93JYbVzNwiR5HJ7GfYRKZ\n+01NN5gAZwsOdhiqhkxjc9PcP9JrcGnGC9RMUtSvscwNPh3R2DKPHpnz3O96yzPG\nqZqpEcDn0wKBgQCdX0RgLk/4r8OBMaeXRYwbc6PhN+oODFcqHrMVkdaiMWKR4BIP\nCKs/PvVjDVb0WNfag4stS5jBDgcgLOjDg0ALWHbINRtrcTO5TnGKSgzJBt3X7Nb+\ntIer7cQQ+ol4cn8enxdgtqCE5xyANJmAzqcUUaprT+IYffHHEmDXp6ezIQKBgQCL\n+3prPzWpDcF8+eqmrZgmUz3SC3IkXnOfzINBx6cUVnt+1wHFPyhaDOnlsyvOsHq8\nYjbEfiFIdOvBNpMTCZRXV91JQb5aGSeJkCbAoLpZNQZ1xGfzKFl+qNPZl17gOOi0\npr3Qmvi3fY/TbSqFzoN5xgZFFtIqISMcwV6fMI4FhQKBgGLoMW/OK69zAJpwsVV1\nAjVfXzEY3UtK8RVWspl+bOKPArIKvtR4aTvcT2kkYMxjXM+0wm7dfQq1a3gvyv/K\nd0mhr/Sst7OZuMcWH7xwM5ZmvPWnEMYh55BNThKZ7gdJ6+SFNOWNDCzLao1sC+uX\nGpBWCMEPmsLEVsth20BRtr90\n-----END PRIVATE KEY-----\n"
  }
]