├── examples ├── nginx-dns-simple.conf ├── nginx-dot-to-dns-simple.conf ├── test.conf ├── nginx-dns-routing.conf ├── nginx-dot-to-dot-routing.conf ├── nginx-plus-filtering.conf ├── nginx-doh-and-dot-to-dns.conf └── nginx-doh-and-dot-to-dot.conf ├── LICENSE ├── ssl ├── certs │ └── doh.local.pem └── private │ └── doh.local.pem ├── nginx-glb.conf ├── docs └── nginx-dns-over-https.md ├── README.md ├── nginx-doh.conf └── njs.d └── dns ├── test.js ├── glb.js ├── dns.js └── libdns.js /examples/nginx-dns-simple.conf: -------------------------------------------------------------------------------- 1 | user nginx; 2 | worker_processes auto; 3 | 4 | error_log /var/log/nginx/error.log notice; 5 | pid /var/run/nginx.pid; 6 | 7 | events { 8 | worker_connections 1024; 9 | } 10 | 11 | stream { 12 | 13 | # DNS upstream pool. 14 | upstream dns { 15 | zone dns 64k; 16 | server 8.8.8.8:53; 17 | } 18 | 19 | # DNS Server. Listens on both TCP and UDP 20 | server { 21 | listen 53; 22 | listen 53 udp; 23 | proxy_responses 1; 24 | proxy_pass dns; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/nginx-dot-to-dns-simple.conf: -------------------------------------------------------------------------------- 1 | user nginx; 2 | worker_processes auto; 3 | 4 | load_module modules/ngx_stream_js_module.so; 5 | 6 | error_log /var/log/nginx/error.log notice; 7 | pid /var/run/nginx.pid; 8 | 9 | events { 10 | worker_connections 1024; 11 | } 12 | 13 | # DNS Stream Services 14 | stream { 15 | 16 | # DNS upstream pool. 17 | upstream dns { 18 | zone dns 64k; 19 | server 8.8.8.8:53; 20 | } 21 | 22 | # DNS(TCP) and DNS over TLS (DoT) Server 23 | # Terminates DNS and DoT, then proxies on to standard DNS. 24 | server { 25 | listen 53; 26 | listen 853 ssl; 27 | ssl_certificate /etc/nginx/ssl/certs/doh.local.pem; 28 | ssl_certificate_key /etc/nginx/ssl/private/doh.local.pem; 29 | proxy_pass dns; 30 | } 31 | 32 | # DNS(UDP) Server 33 | server { 34 | listen 53 udp; 35 | proxy_responses 1; 36 | proxy_pass dns; 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /examples/test.conf: -------------------------------------------------------------------------------- 1 | user nginx; 2 | worker_processes auto; 3 | 4 | load_module modules/ngx_stream_js_module.so; 5 | 6 | error_log /var/log/nginx/error.log notice; 7 | pid /var/run/nginx.pid; 8 | 9 | events { 10 | worker_connections 1024; 11 | } 12 | 13 | http { } 14 | 15 | # DNS Stream Services 16 | stream { 17 | 18 | # DNS logging 19 | log_format dns '$remote_addr [$time_local] $protocol $status $bytes_sent $bytes_received "$dns_qname" "$upstream_addr"'; 20 | 21 | access_log /var/log/nginx/dns-access.log dns; 22 | 23 | # Import the NJS module 24 | js_import /etc/nginx/njs.d/dns/test.js; 25 | 26 | # The $dns_qname variable can be populated by preread calls, and can be used for DNS routing 27 | js_set $dns_qname test.get_qname; 28 | 29 | # test server - responds to dns queries 30 | server { 31 | listen 5553; 32 | listen 5553 udp; 33 | js_var $test_result; 34 | js_preread test.test_dns_encoder; 35 | return $test_result; 36 | } 37 | 38 | # load balance to test server and parse responses 39 | server { 40 | listen 5554; 41 | listen 5554 udp; 42 | proxy_responses 1; 43 | js_filter test.test_dns_decoder; 44 | proxy_pass 127.0.0.1:5553; 45 | } 46 | 47 | 48 | } 49 | 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2022, Mark Boddington 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /ssl/certs/doh.local.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIID0TCCArmgAwIBAgIUOdQrJG61Cs5p1PRwIOYzUClAZZkwDQYJKoZIhvcNAQEL 3 | BQAweDELMAkGA1UEBhMCR0IxEjAQBgNVBAgMCUNhbWJyaWRnZTESMBAGA1UEBwwJ 4 | Q2FtYnJpZGdlMRIwEAYDVQQKDAlOR0lOWCBJbmMxGTAXBgNVBAsMEE5vdyBhIHBh 5 | cnQgb2YgRjUxEjAQBgNVBAMMCWRvaC5sb2NhbDAeFw0xOTA5MjgxNDU2MzNaFw0y 6 | MDA5MjcxNDU2MzNaMHgxCzAJBgNVBAYTAkdCMRIwEAYDVQQIDAlDYW1icmlkZ2Ux 7 | EjAQBgNVBAcMCUNhbWJyaWRnZTESMBAGA1UECgwJTkdJTlggSW5jMRkwFwYDVQQL 8 | DBBOb3cgYSBwYXJ0IG9mIEY1MRIwEAYDVQQDDAlkb2gubG9jYWwwggEiMA0GCSqG 9 | SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDSnQpcd9PElrimhx1Absbf4SafKPpM+7Nh 10 | EVBFJ5Emtxksz1tUsi1mXEQsf9sCKeURvrzoUwyUkkF4Frks14L/+GCXEpCJSrga 11 | NhhSO6QR0xZ26jXFqwwsE3QkW6URNGZ5IEecI+2JAUiMxhdmO9oEPvRzDmyDoUTT 12 | dmt6+y0NahrU47OP88yie0Jt1+Mh18U/RQKRUYZz1L4oHV1sujuemDbF7xSkguvV 13 | EhUMF+316HQNPndrZRVIYjfMUT32qnvlnOKzgB4mNh8biRLekwsPplFuU5vhnUxR 14 | 5pDw4JzNT5Mis8I8+ULUkKKK3wF7Ih3Wp6vMgq/i6CvqcYA/n8LzAgMBAAGjUzBR 15 | MB0GA1UdDgQWBBSFF5wbFm0N3FXIu14wGAfHIIEDyjAfBgNVHSMEGDAWgBSFF5wb 16 | Fm0N3FXIu14wGAfHIIEDyjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUA 17 | A4IBAQCHIjdUCNKHnk06jyajT8rKMdjpJQkv6P2nFr0Sf0/1ZtftK+dgA5O3HJmY 18 | aPJVGZlfdOWavYT3i+OrLpSVCwGoCt/V1rSgw6E9zfEarsVdtiZzd9h/HhvOdDGd 19 | SE1EUJveIoe46DpdeD+pSz068+1WKK7UahArupsXjlcaoCVp7uvLTacP9NPMP6jp 20 | aiOotgZUxHEoelseEgyFGDOyP32OTZv8vGQFTaNLS3zKP8iZNNmfwX1pirY5TJzP 21 | HcbKgT8aki9+U64vkjoUrvpA8y4U8b7NgKFowkLl7rbHxNqZmstq+YI13RwgNcYG 22 | kEbvwju0TuQ52TgpKkA3D5fiWhSZ 23 | -----END CERTIFICATE----- 24 | -------------------------------------------------------------------------------- /examples/nginx-dns-routing.conf: -------------------------------------------------------------------------------- 1 | user nginx; 2 | worker_processes auto; 3 | 4 | load_module modules/ngx_stream_js_module.so; 5 | 6 | error_log /var/log/nginx/error.log notice; 7 | pid /var/run/nginx.pid; 8 | 9 | events { 10 | worker_connections 1024; 11 | } 12 | 13 | stream { 14 | 15 | # logging 16 | log_format dns '$remote_addr [$time_local] $protocol "$dns_qname" "$upstream_pool"'; 17 | access_log /var/log/nginx/dns-access.log dns; 18 | 19 | # import NJS module 20 | js_import /etc/nginx/njs.d/dns/dns.js; 21 | 22 | # NJS function to get the dns_qname, requires a js_preread in the server to populate the variable from the DNS packet 23 | js_set $dns_qname dns.get_qname; 24 | 25 | # This maps the qname domain to the DNS server for routing 26 | map $dns_qname $upstream_pool { 27 | hostnames; 28 | *.nginx dnsmasq; 29 | *.k8s dnsmasq; 30 | default google; 31 | } 32 | 33 | # Google upstream 34 | upstream google { 35 | zone dns 64k; 36 | server 8.8.8.8:53; 37 | } 38 | 39 | # dnsmasq local upstream 40 | upstream dnsmasq { 41 | zone dns 64k; 42 | server 192.168.64.1:53; 43 | } 44 | 45 | # DNS(TCP) Serverr 46 | server { 47 | listen 53; 48 | js_preread dns.preread_dns_request; 49 | proxy_pass $upstream_pool; 50 | } 51 | 52 | # DNS(UDP) Server 53 | server { 54 | listen 53 udp; 55 | js_preread dns.preread_dns_request; 56 | proxy_responses 1; 57 | proxy_pass $upstream_pool; 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /examples/nginx-dot-to-dot-routing.conf: -------------------------------------------------------------------------------- 1 | user nginx; 2 | worker_processes auto; 3 | 4 | load_module modules/ngx_stream_js_module.so; 5 | 6 | error_log /var/log/nginx/error.log notice; 7 | pid /var/run/nginx.pid; 8 | 9 | events { 10 | worker_connections 1024; 11 | } 12 | 13 | # DNS Stream Services 14 | stream { 15 | 16 | # DNS logging 17 | log_format dns '$remote_addr [$time_local] $protocol "$dns_qname" "$upstream_pool"'; 18 | access_log /var/log/nginx/dns-access.log dns; 19 | 20 | # Import the NJS module 21 | js_import /etc/nginx/njs.d/dns/dns.js; 22 | 23 | # The $dns_qname variable will be populated by preread calls, and used for DNS routing 24 | js_set $dns_qname dns.get_qname; 25 | 26 | # When doing DNS routing, use $dns_qname to map the questions to the upstream pools. 27 | map $dns_qname $upstream_pool { 28 | hostnames; 29 | *.nginx dnsmasq; 30 | *.k8s dnsmasq; 31 | default google; 32 | } 33 | 34 | # upstream pools (google DoT) 35 | upstream google { 36 | zone dns 64k; 37 | server 8.8.8.8:853; 38 | } 39 | 40 | # upstream pools (another DoT) 41 | upstream dnsmasq { 42 | zone dns 64k; 43 | server 192.168.64.1:853; 44 | } 45 | 46 | # DNS(TCP) and DNS over TLS (DoT) Server 47 | # Upstream can be either DNS(TCP) or DoT. If upstream is DNS, proxy_ssl should be off. 48 | server { 49 | listen 53; 50 | listen 853 ssl; 51 | ssl_certificate /etc/nginx/ssl/certs/doh.local.pem; 52 | ssl_certificate_key /etc/nginx/ssl/private/doh.local.pem; 53 | js_preread dns.preread_dns_request; 54 | proxy_ssl on; 55 | proxy_pass $upstream_pool; 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /ssl/private/doh.local.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDSnQpcd9PElrim 3 | hx1Absbf4SafKPpM+7NhEVBFJ5Emtxksz1tUsi1mXEQsf9sCKeURvrzoUwyUkkF4 4 | Frks14L/+GCXEpCJSrgaNhhSO6QR0xZ26jXFqwwsE3QkW6URNGZ5IEecI+2JAUiM 5 | xhdmO9oEPvRzDmyDoUTTdmt6+y0NahrU47OP88yie0Jt1+Mh18U/RQKRUYZz1L4o 6 | HV1sujuemDbF7xSkguvVEhUMF+316HQNPndrZRVIYjfMUT32qnvlnOKzgB4mNh8b 7 | iRLekwsPplFuU5vhnUxR5pDw4JzNT5Mis8I8+ULUkKKK3wF7Ih3Wp6vMgq/i6Cvq 8 | cYA/n8LzAgMBAAECggEABDnYcmCJJEGt9NFzOc6/ONDIuJrW4uKOB92UEb8of3Ff 9 | FPIYMAvfM1WYnJf4KgPzL7b3DWZVM0n3/FPgZVDxtPcj4QQjWE3igcwiEsxVj3H/ 10 | 2mT6rTuwY9YEF5KrLjwx7i5CoZRq+LvI2+JBp/B9gGZO+1wHu2BqBCA1KeOOVN2J 11 | 2FDLt32sMLrY4QQqFefYGk3gdzD06qyjRbpSiymaDOfK7D1xr6iNqGZHOl8eu8I4 12 | zHBVwuE9L3qPGY+bH1rTWSBrV/hIJSKhXom4PbNz23KpfrgpGJ+yP8hspEUgtGiH 13 | 0Et9o59zKju77sLkZvhLOQSy8Y/yCpn9qNLqoNl8QQKBgQDs4lXCYkaQNMErGYpf 14 | 2+T7fRyuRpCedmoM70p1EmwtLi2ufIWPWPr+7NpvIdmXZb02rzE5gMMAROmC0c7b 15 | 9n7jqBMN8LjdG2yeyl0a1rxxE/eMrYqZ/SIEaxUnam7B50Re767/uUL8Uq2UOuDi 16 | 9e1g6LzvXixM7m1JpS4pkrDkYQKBgQDjm/5DqmOD/cfLbpNhxnLiQkJEHt553+mh 17 | xXWaxugBGjRyFlXfaRNAoJa5D7+VRc+iFNPF9CwYYMV93JYbVzNwiR5HJ7GfYRKZ 18 | +01NN5gAZwsOdhiqhkxjc9PcP9JrcGnGC9RMUtSvscwNPh3R2DKPHpnz3O96yzPG 19 | qZqpEcDn0wKBgQCdX0RgLk/4r8OBMaeXRYwbc6PhN+oODFcqHrMVkdaiMWKR4BIP 20 | CKs/PvVjDVb0WNfag4stS5jBDgcgLOjDg0ALWHbINRtrcTO5TnGKSgzJBt3X7Nb+ 21 | tIer7cQQ+ol4cn8enxdgtqCE5xyANJmAzqcUUaprT+IYffHHEmDXp6ezIQKBgQCL 22 | +3prPzWpDcF8+eqmrZgmUz3SC3IkXnOfzINBx6cUVnt+1wHFPyhaDOnlsyvOsHq8 23 | YjbEfiFIdOvBNpMTCZRXV91JQb5aGSeJkCbAoLpZNQZ1xGfzKFl+qNPZl17gOOi0 24 | pr3Qmvi3fY/TbSqFzoN5xgZFFtIqISMcwV6fMI4FhQKBgGLoMW/OK69zAJpwsVV1 25 | AjVfXzEY3UtK8RVWspl+bOKPArIKvtR4aTvcT2kkYMxjXM+0wm7dfQq1a3gvyv/K 26 | d0mhr/Sst7OZuMcWH7xwM5ZmvPWnEMYh55BNThKZ7gdJ6+SFNOWNDCzLao1sC+uX 27 | GpBWCMEPmsLEVsth20BRtr90 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /nginx-glb.conf: -------------------------------------------------------------------------------- 1 | user nginx; 2 | worker_processes auto; 3 | 4 | load_module modules/ngx_stream_js_module.so; 5 | load_module modules/ngx_stream_geoip2_module.so; 6 | 7 | error_log /var/log/nginx/error.log notice; 8 | pid /var/run/nginx.pid; 9 | 10 | events { 11 | worker_connections 1024; 12 | } 13 | 14 | http { 15 | include /etc/nginx/mime.types; 16 | default_type application/octet-stream; 17 | 18 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 19 | '$status $body_bytes_sent "$http_referer" ' 20 | '"$http_user_agent" "$http_x_forwarded_for"'; 21 | 22 | access_log /var/log/nginx/access.log main; 23 | 24 | sendfile on; 25 | #tcp_nopush on; 26 | 27 | keepalive_timeout 65; 28 | 29 | #gzip on; 30 | 31 | upstream www_netflix_com { 32 | zone upstream_www_netflix_com 64k; 33 | server 52.49.96.37; # eu-west-1 34 | server 52.51.179.14 backup; 35 | server 52.32.190.151; # us-west-2 36 | server 52.41.193.16 backup; 37 | } 38 | 39 | match server_ok { 40 | status 200-399; 41 | } 42 | 43 | server { 44 | listen 127.0.0.1:8888; 45 | server_name www.netflix.com; 46 | set $test "foobar"; 47 | location / { 48 | proxy_pass http://www_netflix_com; 49 | proxy_set_header Host $host; 50 | proxy_http_version 1.1; 51 | health_check interval=10 fails=3 passes=3 match=server_ok; 52 | } 53 | } 54 | 55 | server { 56 | listen 127.0.0.1:80; 57 | location /api/ { 58 | api write=on; 59 | allow 127.0.0.1; 60 | deny all; 61 | } 62 | } 63 | 64 | } 65 | 66 | stream { 67 | 68 | js_include /etc/nginx/njs.d/nginx_stream.js; 69 | js_set $glb_response glb_get_response; 70 | js_set $edns_subnet glb_get_edns_subnet; 71 | 72 | keyval_zone zone=glb_config:64k state=/etc/nginx/zones/glb_config.zone; 73 | keyval "www_netflix_com" $www_netflix_com zone=glb_config; 74 | keyval "GLB_USE_EDNS" $glb_use_edns zone=glb_config; 75 | 76 | # The following keys are needed if s.api() is unavailable 77 | keyval "www_netflix_com_nodes" $www_netflix_com_nodes zone=glb_config; 78 | keyval "www_netflix_com_geoip_52_49_96_37" $www_netflix_com_geoip_52_49_96_37 zone=glb_config; 79 | keyval "www_netflix_com_geoip_52_51_179_14" $www_netflix_com_geoip_52_51_179_14 zone=glb_config; 80 | keyval "www_netflix_com_geoip_52_32_190_151" $www_netflix_com_geoip_52_32_190_151 zone=glb_config; 81 | keyval "www_netflix_com_geoip_52_41_193_16" $www_netflix_com_geoip_52_41_193_16 zone=glb_config; 82 | 83 | # set $geoip_source to EDNS if we have one, or the $remote_addr if not 84 | map $edns_subnet $geoip_source { 85 | "" $remote_addr; 86 | default $edns_subnet; 87 | } 88 | 89 | # get the country, latitude, and longitude from the GeoLite2 City DB 90 | geoip2 /etc/geoip/GeoLite2-City.mmdb { 91 | $geoip2_country_code default=GB source=$geoip_source country iso_code; 92 | $geoip2_latitude default=51.52830 source=$geoip_source location latitude; 93 | $geoip2_longitude default=0.0000 source=$geoip_source location longitude; 94 | } 95 | 96 | # process the DNS request 97 | server { 98 | listen 192.168.64.20:53 udp reuseport; 99 | js_preread glb_process_request; 100 | return $glb_response; 101 | } 102 | 103 | } 104 | 105 | -------------------------------------------------------------------------------- /examples/nginx-plus-filtering.conf: -------------------------------------------------------------------------------- 1 | # 2 | # This config shows an example of filtering DNS requests using the Key/value store available in NGINX Plus 3 | # Push FQDNs into the dns_config key/value zone with a value of "blocked" or "blackhole" to have them scrubbed from DNS 4 | # Alternatively push a CSV list of domains as the value to either "blocked_domains" or "blackhole_domains" to have 5 | # any requests for records within those zones scrubbed. 6 | # 7 | 8 | user nginx; 9 | worker_processes auto; 10 | 11 | load_module modules/ngx_stream_js_module.so; 12 | 13 | error_log /var/log/nginx/error.log notice; 14 | pid /var/run/nginx.pid; 15 | 16 | events { 17 | worker_connections 1024; 18 | } 19 | 20 | # DNS Stream Services 21 | stream { 22 | 23 | # KeyValue store for blocking domains (NGINX Plus only) 24 | keyval_zone zone=dns_config:64k state=/etc/nginx/zones/dns_config.zone; 25 | keyval "blocked_domains" $blocked_domains zone=dns_config; 26 | keyval "blackhole_domains" $blackhole_domains zone=dns_config; 27 | keyval $dns_qname $scrub_action zone=dns_config; 28 | 29 | # DNS logging 30 | log_format dns '$remote_addr [$time_local] $protocol "$dns_qname" "$upstream_pool"'; 31 | access_log /var/log/nginx/dns-access.log dns; 32 | 33 | # Import the NJS module 34 | js_import /etc/nginx/njs.d/dns/dns.js; 35 | 36 | # The $dns_qname variable can be populated by preread calls, and can be used for DNS routing 37 | js_set $dns_qname dns.get_qname; 38 | 39 | # The DNS response packet, if we're blocking the domain, this will be set. 40 | js_set $dns_response dns.get_response; 41 | 42 | # When doing DNS routing, use $dns_qname to map the questions to the upstream pools. 43 | map $dns_qname $upstream { 44 | hostnames; 45 | *.nginx dnsmasq; 46 | *.k8s dnsmasq; 47 | default google; 48 | } 49 | 50 | # Set upstream to be the pool defined above if dns_response is empty, else pass to the block/blackhole upstream 51 | map $dns_response $upstream_pool { 52 | "blocked" blocked; 53 | "blackhole" blackhole; 54 | default $upstream; 55 | } 56 | 57 | # upstream pool for blocked requests (returns nxdomain) 58 | upstream blocked { 59 | zone blocked 64k; 60 | server 127.0.0.1:9953; 61 | } 62 | 63 | # upstream pool for blacholed requests (returns 0.0.0.0) 64 | upstream blackhole { 65 | zone blackhole 64k; 66 | server 127.0.0.1:9853; 67 | } 68 | 69 | # upstream pools (google DNS) 70 | upstream google { 71 | zone dns 64k; 72 | server 8.8.8.8:53; 73 | } 74 | 75 | # upstream pools (another DNS) 76 | upstream dnsmasq { 77 | zone dns 64k; 78 | server 192.168.64.1:5353; 79 | } 80 | 81 | # DNS(TCP) and DNS over TLS (DoT) Server 82 | # Upstream can be either DNS(TCP) or DoT. If upstream is DNS, proxy_ssl should be off. 83 | server { 84 | listen 53; 85 | listen 853 ssl; 86 | ssl_certificate /etc/nginx/ssl/certs/doh.local.pem; 87 | ssl_certificate_key /etc/nginx/ssl/private/doh.local.pem; 88 | js_preread dns.preread_dns_request; 89 | proxy_pass $upstream_pool; 90 | } 91 | 92 | # DNS(UDP) Server 93 | # Upstream can only be another DNS(UDP) server. 94 | server { 95 | listen 53 udp; 96 | js_preread dns.preread_dns_request; 97 | proxy_responses 1; 98 | proxy_pass $upstream_pool; 99 | } 100 | 101 | # Server for responding to blocked/blackholed responses 102 | server { 103 | listen 127.0.0.1:9953; 104 | listen 127.0.0.1:9853; 105 | listen 127.0.0.1:9953 udp; 106 | listen 127.0.0.1:9853 udp; 107 | js_preread dns.preread_dns_request; 108 | return $dns_response; 109 | } 110 | 111 | } 112 | -------------------------------------------------------------------------------- /docs/nginx-dns-over-https.md: -------------------------------------------------------------------------------- 1 | ## DNS over HTTPS (DoH) Gateway 2 | Use the nginx-doh.conf file to run a DoH gateway. 3 | Copy the njs.d and ssl folders into /etc/nginx/ and the nginx-doh.conf to /etc/nginx/nginx.conf 4 | 5 | ### Simple DNS(TCP) and DNS over TLS (DoT) 6 | NGINX can act as a DNS(TCP) <-> DNS over TLS (DoT) gateway without any NJS functions. Eg: 7 | 8 | ``` 9 | upstream dns { 10 | zone dns 64k; 11 | server 8.8.8.8:53; 12 | } 13 | 14 | upstream dot { 15 | zone dot 64k; 16 | server 8.8.8.8:853; 17 | } 18 | 19 | server { 20 | listen 53; 21 | listen 853 ssl; 22 | ssl_certificate /etc/nginx/ssl/certs/doh.local.pem; 23 | ssl_certificate_key /etc/nginx/ssl/private/doh.local.pem; 24 | proxy_ssl on; 25 | proxy_pass dot; 26 | } 27 | ``` 28 | 29 | ### DNS over HTTPS (DoH) 30 | 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 31 | full configuration in nginx-glb.conf. 32 | 33 | The full configuration has a HTTP/2 service listening for requests, and does a proxy_pass for requests to /dns-query. 34 | We proxy to an internal stream service on port 8053, which uses js_filter to pull out the DNS packet from the HTTP wrapper, 35 | and forward onto an upstream DNS(TCP) or DoT server. 36 | The result is then wrapped back up in a HTTP response and passed to the HTTP/2 service for delivery to the client. 37 | 38 | #### NGINX Stream Server for back-end 39 | Lets look at the stream service first: 40 | ``` 41 | server { 42 | listen 127.0.0.1:8053; 43 | js_filter dns.filter_doh_request; 44 | proxy_ssl on; 45 | proxy_pass dot; 46 | } 47 | ``` 48 | 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` 49 | will find the DNS packet encoded in the request, and forward it on to the upstream DoT service. 50 | 51 | #### NGINX HTTP/2 service for the front-end 52 | Traffic arrives at the stream service via a HTTP/2 server: 53 | ``` 54 | upstream dohloop { 55 | zone dohloop 64k; 56 | server 127.0.0.1:8053; 57 | } 58 | 59 | proxy_cache_path /var/cache/nginx/doh_cache levels=1:2 keys_zone=doh_cache:10m; 60 | server { 61 | 62 | listen 443 ssl http2; 63 | ssl_certificate /etc/nginx/ssl/certs/doh.local.pem; 64 | ssl_certificate_key /etc/nginx/ssl/private/doh.local.pem; 65 | 66 | proxy_cache_methods GET POST; 67 | 68 | location / { 69 | return 404 "404 Not Found\n"; 70 | } 71 | 72 | location /dns-query { 73 | proxy_http_version 1.0; 74 | proxy_cache doh_cache; 75 | proxy_cache_key $scheme$proxy_host$uri$is_args$args$request_body; 76 | proxy_pass http://dohloop; 77 | } 78 | 79 | } 80 | ``` 81 | 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 82 | `/dns-query` location. All dns-queries are forwarded onto the stream service as HTTP/1.0 requests. 83 | 84 | #### NGINX Processing Options 85 | 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 86 | processing (level 2) allows NGINX to gather the necessary intelligence (Resource Record TTLs) to enable a HTTP Content-Cache for the DoH requests. 87 | At levels less than 2, we will cache responses, but for just 10 seconds. 88 | 89 | Change this setting in the `njs.d/dns/doh.js` file 90 | ``` 91 | /** 92 | * DNS Decode Level 93 | * 0: No decoding, minimal processing required to strip packet from HTTP wrapper (fastest) 94 | * 1: Parse DNS Header and Question. We can log the Question, Class, Type, and Result Code 95 | * 2: As 1, but also parse answers. We can log the answers, and also cache responses according to TTL. 96 | * 3: Very Verbose, log everything as above, but also write packet data to error log (slowest) 97 | **/ 98 | var $dns_decode_level = 0; 99 | ``` 100 | 101 | -------------------------------------------------------------------------------- /examples/nginx-doh-and-dot-to-dns.conf: -------------------------------------------------------------------------------- 1 | user nginx; 2 | worker_processes auto; 3 | 4 | load_module modules/ngx_stream_js_module.so; 5 | 6 | error_log /var/log/nginx/error.log notice; 7 | pid /var/run/nginx.pid; 8 | 9 | events { 10 | worker_connections 1024; 11 | } 12 | 13 | http { 14 | include /etc/nginx/mime.types; 15 | default_type application/octet-stream; 16 | 17 | # logging directives 18 | log_format doh '$remote_addr - $remote_user [$time_local] "$request" ' 19 | '[ $msec, $request_time, $upstream_response_time $pipe ] ' 20 | '$status $body_bytes_sent "$http_x_forwarded_for" ' 21 | '$upstream_http_x_dns_question $upstream_http_x_dns_type ' 22 | '$upstream_http_x_dns_result ' 23 | '$upstream_http_x_dns_ttl $upstream_http_x_dns_answers ' 24 | '$upstream_cache_status'; 25 | 26 | access_log /var/log/nginx/doh-access.log doh; 27 | 28 | # This upstream connects to a local Stream service which converts HTTP -> DNS 29 | upstream dohloop { 30 | zone dohloop 64k; 31 | server 127.0.0.1:8053; 32 | keepalive_timeout 60s; 33 | keepalive_requests 100; 34 | keepalive 10; 35 | } 36 | 37 | # Proxy Cache storage - so we can cache the DoH response from the upstream 38 | proxy_cache_path /var/cache/nginx/doh_cache levels=1:2 keys_zone=doh_cache:10m; 39 | 40 | # The DoH server block 41 | server { 42 | 43 | # Listen on standard HTTPS port, and accept HTTP2, with SSL termination 44 | listen 443 ssl http2; 45 | ssl_certificate /etc/nginx/ssl/certs/doh.local.pem; 46 | ssl_certificate_key /etc/nginx/ssl/private/doh.local.pem; 47 | ssl_session_cache shared:ssl_cache:10m; 48 | ssl_session_timeout 10m; 49 | 50 | # DoH may use GET or POST requests, Cache both 51 | proxy_cache_methods GET POST; 52 | 53 | # Return 404 to all responses, except for those using our published DoH URI 54 | location / { 55 | return 404 "404 Not Found\n"; 56 | } 57 | 58 | # This is our published DoH URI 59 | location /dns-query { 60 | 61 | # Proxy HTTP/1.1, clear the connection header to enable Keep-Alive 62 | proxy_http_version 1.1; 63 | proxy_set_header Connection ""; 64 | 65 | # Enable Cache, and set the cache_key to include the request_body 66 | proxy_cache doh_cache; 67 | proxy_cache_key $scheme$proxy_host$uri$is_args$args$request_body; 68 | 69 | # proxy pass to the dohloop upstream 70 | proxy_pass http://dohloop; 71 | } 72 | 73 | } 74 | 75 | } 76 | 77 | # DNS Stream Services 78 | stream { 79 | 80 | # DNS logging 81 | log_format dns '$remote_addr [$time_local] $protocol "$dns_qname"'; 82 | access_log /var/log/nginx/dns-access.log dns; 83 | 84 | # Import the NJS module 85 | js_import /etc/nginx/njs.d/dns/dns.js; 86 | 87 | # The $dns_qname variable can be populated by preread calls, and can be used for DNS routing 88 | js_set $dns_qname dns.get_qname; 89 | 90 | # DNS upstream pool. 91 | upstream dns { 92 | zone dns 64k; 93 | server 8.8.8.8:53; 94 | } 95 | 96 | # DNS(TCP) and DNS over TLS (DoT) Server 97 | # Terminate DoT and DNS TCP, and proxy onto standard DNS 98 | server { 99 | listen 53; 100 | listen 853 ssl; 101 | ssl_certificate /etc/nginx/ssl/certs/doh.local.pem; 102 | ssl_certificate_key /etc/nginx/ssl/private/doh.local.pem; 103 | js_preread dns.preread_dns_request; 104 | proxy_pass dns; 105 | } 106 | 107 | # DNS(UDP) Server 108 | # DNS UDP proxy onto DNS UDP 109 | server { 110 | listen 53 udp; 111 | proxy_responses 1; 112 | js_preread dns.preread_dns_request; 113 | proxy_pass dns; 114 | } 115 | 116 | # DNS over HTTPS (gateway) Service 117 | # Upstream can be either DNS(TCP) or DoT. If upstream is DNS, proxy_ssl should be off. 118 | server { 119 | listen 127.0.0.1:8053; 120 | js_filter dns.filter_doh_request; 121 | proxy_pass dns; 122 | } 123 | 124 | } 125 | -------------------------------------------------------------------------------- /examples/nginx-doh-and-dot-to-dot.conf: -------------------------------------------------------------------------------- 1 | user nginx; 2 | worker_processes auto; 3 | 4 | load_module modules/ngx_stream_js_module.so; 5 | 6 | error_log /var/log/nginx/error.log notice; 7 | pid /var/run/nginx.pid; 8 | 9 | events { 10 | worker_connections 1024; 11 | } 12 | 13 | http { 14 | include /etc/nginx/mime.types; 15 | default_type application/octet-stream; 16 | 17 | # logging directives 18 | log_format doh '$remote_addr - $remote_user [$time_local] "$request" ' 19 | '[ $msec, $request_time, $upstream_response_time $pipe ] ' 20 | '$status $body_bytes_sent "$http_x_forwarded_for" ' 21 | '$upstream_http_x_dns_question $upstream_http_x_dns_type ' 22 | '$upstream_http_x_dns_result ' 23 | '$upstream_http_x_dns_ttl $upstream_http_x_dns_answers ' 24 | '$upstream_cache_status'; 25 | 26 | access_log /var/log/nginx/doh-access.log doh; 27 | 28 | # This upstream connects to a local Stream service which converts HTTP -> DNS 29 | upstream dohloop { 30 | zone dohloop 64k; 31 | server 127.0.0.1:8053; 32 | keepalive_timeout 60s; 33 | keepalive_requests 100; 34 | keepalive 10; 35 | } 36 | 37 | # Proxy Cache storage - so we can cache the DoH response from the upstream 38 | proxy_cache_path /var/cache/nginx/doh_cache levels=1:2 keys_zone=doh_cache:10m; 39 | 40 | # The DoH server block 41 | server { 42 | 43 | # Listen on standard HTTPS port, and accept HTTP2, with SSL termination 44 | listen 443 ssl http2; 45 | ssl_certificate /etc/nginx/ssl/certs/doh.local.pem; 46 | ssl_certificate_key /etc/nginx/ssl/private/doh.local.pem; 47 | ssl_session_cache shared:ssl_cache:10m; 48 | ssl_session_timeout 10m; 49 | 50 | # DoH may use GET or POST requests, Cache both 51 | proxy_cache_methods GET POST; 52 | 53 | # Return 404 to all responses, except for those using our published DoH URI 54 | location / { 55 | return 404 "404 Not Found\n"; 56 | } 57 | 58 | # This is our published DoH URI 59 | location /dns-query { 60 | 61 | # Proxy HTTP/1.1, clear the connection header to enable Keep-Alive 62 | proxy_http_version 1.1; 63 | proxy_set_header Connection ""; 64 | 65 | # Enable Cache, and set the cache_key to include the request_body 66 | proxy_cache doh_cache; 67 | proxy_cache_key $scheme$proxy_host$uri$is_args$args$request_body; 68 | 69 | # proxy pass to the dohloop upstream 70 | proxy_pass http://dohloop; 71 | } 72 | 73 | } 74 | 75 | } 76 | 77 | # DNS Stream Services 78 | stream { 79 | 80 | # DNS logging 81 | log_format dns '$remote_addr [$time_local] $protocol "$dns_qname"'; 82 | access_log /var/log/nginx/dns-access.log dns; 83 | 84 | # Import the NJS module 85 | js_import /etc/nginx/njs.d/dns/dns.js; 86 | 87 | # The $dns_qname variable can be populated by preread calls, and can be used for DNS routing 88 | js_set $dns_qname dns.get_qname; 89 | 90 | # DNS over TLS upstream pool 91 | upstream dot { 92 | zone dot 64k; 93 | server 8.8.8.8:853; 94 | } 95 | 96 | # DNS(TCP) and DNS over TLS (DoT) Server 97 | # Upstream can be either DNS(TCP) or DoT. If upstream is DNS, proxy_ssl should be off. 98 | server { 99 | 100 | # DNS TCP 101 | listen 53; 102 | 103 | # DNS DoT 104 | listen 853 ssl; 105 | ssl_certificate /etc/nginx/ssl/certs/doh.local.pem; 106 | ssl_certificate_key /etc/nginx/ssl/private/doh.local.pem; 107 | 108 | # This is used to pull out question for logging 109 | js_preread dns.preread_dns_request; 110 | 111 | # Enable SSL re-encryption for DoT connection upstream 112 | proxy_ssl on; 113 | proxy_pass dot; 114 | } 115 | 116 | # DNS over HTTPS (gateway) Service 117 | # Upstream can be either DNS(TCP) or DoT. If upstream is DNS, proxy_ssl should be off. 118 | server { 119 | listen 127.0.0.1:8053; 120 | js_filter dns.filter_doh_request; 121 | proxy_ssl on; 122 | proxy_pass dot; 123 | } 124 | 125 | } 126 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NGINX DNS (DNS/DoT/DoH) 2 | 3 | > The `v2` branch is migrating to `Buffers` due to NJS deprecating the `String` byte-array functions. 4 | > Please test and raise issues if you find them. Thank you! 5 | 6 | This repository contains some NJS code, and example configuration files for using NGINX with DNS services. 7 | NGINX can be used to perform load balancing for DNS (TCP/UDP), and also DNS over TLS (DoT) and DNS over HTTPS (DoH) 8 | 9 | NGINX can also be used to provide Global Server Load Balancing (GSLB). 10 | 11 | See the example configuration files in the [examples](examples) folder. 12 | 13 | ## Setup 14 | Copy the njs.d folder into /etc/nginx/ and one of the NGINX DoH [examples](examples) to /etc/nginx/nginx.conf 15 | 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. 16 | 17 | ## Simple DNS 18 | NGINX can do simple DNS load balancing, without the need for NJS, using the standard Stream module directives. 19 | ``` 20 | stream { 21 | 22 | # DNS upstream pool. 23 | upstream dns { 24 | zone dns 64k; 25 | server 8.8.8.8:53; 26 | } 27 | 28 | # DNS Server. Listens on both TCP and UDP 29 | server { 30 | listen 53; 31 | listen 53 udp; 32 | proxy_responses 1; 33 | proxy_pass dns; 34 | } 35 | } 36 | ``` 37 | 38 | 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 39 | included in this repository. 40 | 41 | 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`. 42 | For example: 43 | ``` 44 | stream { 45 | js_import /etc/nginx/njs.d/dns/dns.js; 46 | js_set $dns_qname dns.get_qname; 47 | 48 | map $dns_qname $upstream_pool { 49 | hostnames; 50 | *.nginx one; 51 | default two; 52 | } 53 | 54 | upstream one { 55 | ... 56 | } 57 | 58 | upstream two { 59 | ... 60 | } 61 | 62 | server { 63 | listen 53 udp; 64 | js_preread dns.preread_dns_request; 65 | proxy_responses 1; 66 | proxy_pass $upstream_pool; 67 | } 68 | 69 | } 70 | ``` 71 | 72 | ## DNS over TLS (DoT) and DNS over HTTPS (DoH) Gateway 73 | 74 | NGINX can act as a DNS(TCP) <-> DNS over TLS (DoT) gateway without any NJS functions. Eg: 75 | 76 | ``` 77 | upstream dns { 78 | zone dns 64k; 79 | server 8.8.8.8:53; 80 | } 81 | 82 | upstream dot { 83 | zone dot 64k; 84 | server 8.8.8.8:853; 85 | } 86 | 87 | server { 88 | listen 53; 89 | listen 853 ssl; 90 | ssl_certificate /etc/nginx/ssl/certs/doh.local.pem; 91 | ssl_certificate_key /etc/nginx/ssl/private/doh.local.pem; 92 | proxy_ssl on; 93 | proxy_pass dot; 94 | } 95 | ``` 96 | 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. 97 | 98 | NJS is required if you want to act as a gateway between DoH and DNS/DoT. 99 | See the example configuration files in the [examples](examples) folder. 100 | 101 | The full configuration has a HTTP/2 service listening for requests, and does a proxy_pass for requests to /dns-query. 102 | We proxy to an internal stream service on port 8053, which uses js_filter to pull out the DNS packet from the HTTP wrapper, 103 | and forward onto an upstream DNS(TCP) or DoT server. 104 | The result is then wrapped back up in a HTTP response and passed to the HTTP/2 service for delivery to the client. 105 | 106 | NGINX can log as much or as little as you like, and the NJS allows you to process information in the DNS requests and 107 | responses. 108 | 109 | See: [docs/nginx-dns-over-https](docs/nginx-dns-over-https.md) for more information 110 | 111 | ## NGINX GSLB (work-in-progress) 112 | Use the nginx-glb.conf file to run an GSLB service. 113 | Copy the njs.d folder into /etc/nginx/ and the nginx-glb.conf to /etc/nginx/nginx.conf 114 | 115 | TODO - Describe the example configuration and how to customise it. 116 | 117 | -------------------------------------------------------------------------------- /nginx-doh.conf: -------------------------------------------------------------------------------- 1 | # 2 | # This config has bits of DNS/DoT/DoH all over it. See the examples folder for more targeted examples. 3 | # 4 | 5 | user nginx; 6 | worker_processes auto; 7 | 8 | load_module modules/ngx_stream_js_module.so; 9 | 10 | error_log /var/log/nginx/error.log notice; 11 | pid /var/run/nginx.pid; 12 | 13 | events { 14 | worker_connections 1024; 15 | } 16 | 17 | http { 18 | include /etc/nginx/mime.types; 19 | default_type application/octet-stream; 20 | 21 | # logging directives 22 | log_format doh '$remote_addr - $remote_user [$time_local] "$request" ' 23 | '[ $msec, $request_time, $upstream_response_time $pipe ] ' 24 | '$status $body_bytes_sent "$http_x_forwarded_for" ' 25 | '$upstream_http_x_dns_question $upstream_http_x_dns_type ' 26 | '$upstream_http_x_dns_result ' 27 | '$upstream_http_x_dns_ttl $upstream_http_x_dns_answers ' 28 | '$upstream_cache_status'; 29 | 30 | access_log /var/log/nginx/doh-access.log doh; 31 | 32 | # This upstream connects to a local Stream service which converts HTTP -> DNS 33 | upstream dohloop { 34 | zone dohloop 64k; 35 | server 127.0.0.1:8053; 36 | keepalive_timeout 60s; 37 | keepalive_requests 100; 38 | keepalive 10; 39 | } 40 | 41 | # Proxy Cache storage - so we can cache the DoH response from the upstream 42 | proxy_cache_path /var/cache/nginx/doh_cache levels=1:2 keys_zone=doh_cache:10m; 43 | 44 | # The DoH server block 45 | server { 46 | 47 | # Listen on standard HTTPS port, and accept HTTP2, with SSL termination 48 | listen 443 ssl http2; 49 | ssl_certificate /etc/nginx/ssl/certs/doh.local.pem; 50 | ssl_certificate_key /etc/nginx/ssl/private/doh.local.pem; 51 | ssl_session_cache shared:ssl_cache:10m; 52 | ssl_session_timeout 10m; 53 | 54 | # DoH may use GET or POST requests, Cache both 55 | proxy_cache_methods GET POST; 56 | 57 | # Return 404 to all responses, except for those using our published DoH URI 58 | location / { 59 | return 404 "404 Not Found\n"; 60 | } 61 | 62 | # This is our published DoH URI 63 | location /dns-query { 64 | 65 | # Proxy HTTP/1.1, clear the connection header to enable Keep-Alive 66 | proxy_http_version 1.1; 67 | proxy_set_header Connection ""; 68 | 69 | # Enable Cache, and set the cache_key to include the request_body 70 | proxy_cache doh_cache; 71 | proxy_cache_key $scheme$proxy_host$uri$is_args$args$request_body; 72 | 73 | # proxy pass to the dohloop upstream 74 | proxy_pass http://dohloop; 75 | } 76 | 77 | } 78 | 79 | # enable API 80 | server { 81 | listen 8080; 82 | location /api { 83 | api write=on; 84 | allow 127.0.0.1; 85 | allow 192.168.64.1; 86 | deny all; 87 | } 88 | } 89 | 90 | } 91 | 92 | # DNS Stream Services 93 | stream { 94 | 95 | # KeyValue store for blocking domains (NGINX Plus only) 96 | keyval_zone zone=dns_config:64k state=/etc/nginx/zones/dns_config.zone; 97 | keyval "blocked_domains" $blocked_domains zone=dns_config; 98 | keyval "blackhole_domains" $blackhole_domains zone=dns_config; 99 | keyval $dns_qname $scrub_action zone=dns_config; 100 | 101 | # DNS logging 102 | log_format dns '$remote_addr [$time_local] $protocol $status $bytes_sent $bytes_received "$dns_qname" "$upstream_addr"'; 103 | 104 | access_log /var/log/nginx/dns-access.log dns; 105 | 106 | # Import the NJS DNS module 107 | js_import /etc/nginx/njs.d/dns/dns.js; 108 | 109 | # The $dns_qname variable can be populated by preread calls, and can be used for DNS routing 110 | js_set $dns_qname dns.get_qname; 111 | 112 | # The DNS response packet, if we're blocking the domain, this will be set. 113 | js_set $dns_response dns.get_response; 114 | 115 | # When doing DNS routing, use $dns_qname to map the questions to the upstream pools. 116 | map $dns_qname $upstream { 117 | hostnames; 118 | *.nginx dnsmasq; 119 | *.k8s dnsmasq; 120 | default google; 121 | } 122 | 123 | # Set upstream to be the pool defined above if dns_response is empty, else pass to the @block location 124 | map $dns_response $upstream_pool { 125 | "blocked" blocked; 126 | "blackhole" blackhole; 127 | default $upstream; 128 | } 129 | 130 | # upstream pool for blocked requests 131 | upstream blocked { 132 | zone blocked 64k; 133 | server 127.0.0.1:9953; 134 | } 135 | 136 | upstream blackhole { 137 | zone blackhole 64k; 138 | server 127.0.0.1:9853; 139 | } 140 | 141 | # upstream pools (google DNS) 142 | upstream google { 143 | zone dns 64k; 144 | server 8.8.8.8:53; 145 | } 146 | 147 | # upstream pools (another DNS) 148 | upstream dnsmasq { 149 | zone dns 64k; 150 | server 192.168.64.1:5353; 151 | } 152 | 153 | # DNS upstream pool. 154 | upstream dns { 155 | zone dns 64k; 156 | server 8.8.8.8:53; 157 | } 158 | 159 | # DNS over TLS upstream pool 160 | upstream dot { 161 | zone dot 64k; 162 | server 8.8.8.8:853; 163 | } 164 | 165 | # DNS(TCP) and DNS over TLS (DoT) Server 166 | # Upstream can be either DNS(TCP) or DoT. If upstream is DNS, proxy_ssl should be off. 167 | server { 168 | listen 553; 169 | listen 853 ssl; 170 | ssl_certificate /etc/nginx/ssl/certs/doh.local.pem; 171 | ssl_certificate_key /etc/nginx/ssl/private/doh.local.pem; 172 | js_preread dns.preread_dns_request; 173 | #proxy_ssl on; 174 | proxy_pass $upstream_pool; 175 | } 176 | 177 | # DNS(UDP) Server 178 | # Upstream can only be another DNS(UDP) server. 179 | server { 180 | listen 553 udp; 181 | js_preread dns.preread_dns_request; 182 | proxy_responses 1; 183 | proxy_pass $upstream_pool; 184 | } 185 | 186 | # DNS over HTTPS (gateway) Service 187 | # Upstream can be either DNS(TCP) or DoT. If upstream is DNS, proxy_ssl should be off. 188 | server { 189 | listen 127.0.0.1:8053; 190 | js_filter dns.filter_doh_request; 191 | proxy_ssl on; 192 | proxy_pass dot; 193 | } 194 | 195 | # Server for sending blackhole/blocked responses 196 | server { 197 | listen 127.0.0.1:9953; 198 | listen 127.0.0.1:9853; 199 | listen 127.0.0.1:9953 udp; 200 | listen 127.0.0.1:9853 udp; 201 | js_preread dns.preread_dns_request; 202 | return $dns_response; 203 | } 204 | 205 | } 206 | -------------------------------------------------------------------------------- /njs.d/dns/test.js: -------------------------------------------------------------------------------- 1 | 2 | import dns from "libdns.js"; 3 | export default {get_qname, test_dns_encoder, test_dns_decoder}; 4 | 5 | /** 6 | * DNS Decode Level 7 | * 0: No decoding, minimal processing required to strip packet from HTTP wrapper (fastest) 8 | * 1: Parse DNS Header and Question. We can log the Question, Class, Type, and Result Code 9 | * 2: As 1, but also parse answers. We can log the answers, and also cache responses in HTTP Content-Cache 10 | * 3: Very Verbose, log everything as above, but also write packet data to error log (slowest) 11 | **/ 12 | var dns_decode_level = 3; 13 | 14 | /** 15 | * DNS Debug Level 16 | * Specify the decoding level at which we should log packet data to the error log. 17 | * Default is level 3 (max decoding) 18 | **/ 19 | var dns_debug_level = 3; 20 | 21 | // The DNS Question name 22 | var dns_name = Buffer.alloc(0); 23 | 24 | function get_qname(s) { 25 | return dns_name; 26 | } 27 | 28 | // Encode the given number to two bytes (16 bit) 29 | function to_bytes( number ) { 30 | return Buffer.from( [ ((number>>8) & 0xff), (number & 0xff) ] ); 31 | } 32 | 33 | function debug(s, msg) { 34 | if ( dns_decode_level >= dns_debug_level ) { 35 | s.warn(msg); 36 | } 37 | } 38 | 39 | function test_dns_encoder(s) { 40 | s.on("upstream", function(data,flags) { 41 | var packet; 42 | var test_result = Buffer.alloc(0); 43 | if ( data.length == 0 ) { 44 | return; 45 | } 46 | if (data) { 47 | if (s.variables.protocol == "TCP") { 48 | // Drop the TCP length field 49 | data = data.slice(2); 50 | } 51 | debug(s, "test_dns: DNS Encoder Req: " + data.toString('hex') ); 52 | packet = dns.parse_packet(data); 53 | dns.parse_question(packet); 54 | dns_name = packet.question.name; 55 | debug(s, "test_dns: DNS Encoder Request Packet: " + JSON.stringify( Object.entries(packet)) ); 56 | test_result = test_dns_responder(s, data, packet); 57 | delete packet.data; // remove the data buffer before printing 58 | debug(s, "test_dns: DNS Encoder Response Packet: " + JSON.stringify( Object.entries(packet)) ); 59 | debug(s, "test_dns: DNS Encoder Res: " + test_result.toString('hex') ); 60 | s.variables.test_result = test_result; 61 | s.done(); 62 | } 63 | }); 64 | } 65 | 66 | 67 | function test_dns_decoder(s) { 68 | s.on("downstream", function(data,flags) { 69 | var packet; 70 | var test_result = Buffer.alloc(0); 71 | if ( data.length == 0 ) { 72 | return; 73 | } 74 | if (data) { 75 | if (s.variables.protocol == "TCP") { 76 | // Drop the TCP length field 77 | data = data.slice(2); 78 | } 79 | debug(s, "test_dns: DNS Decoder Res: " + data.toString('hex') ); 80 | packet = dns.parse_packet(data); 81 | dns.parse_question(packet); 82 | dns_name = packet.question.name; 83 | dns.parse_complete(packet, 2); 84 | delete packet.data; // remove the data buffer before printing 85 | debug(s, "test_dns: DNS Decoder Response Packet: " + JSON.stringify( Object.entries(packet)) ); 86 | if (s.variables.protocol == "TCP") { 87 | s.send( to_bytes(data.length) ); 88 | } 89 | s.send( data, {flush: true} ); 90 | } 91 | }); 92 | } 93 | 94 | /** 95 | * Function to perform testing of DNS packet generation for various DNS types 96 | * Any domain ending bar.com will use the shortcut_response path 97 | * Any domains ending baz.com will use shortcut_nxdomain path 98 | * All other queries will return an appropriate set of DNS records. 99 | **/ 100 | function test_dns_responder(s, data, packet) { 101 | var answers = []; 102 | var test_result; 103 | 104 | if ( packet.question.type == dns.dns_type.A || packet.question.type == dns.dns_type.ANY ) { 105 | answers.push( {name: packet.question.name, type: dns.dns_type.A, class: dns.dns_class.IN, ttl: 300, rdata: "10.2.3.4" } ); 106 | } else if ( packet.question.type == dns.dns_type.AAAA ) { 107 | 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" } ); 108 | } else if ( packet.question.type == dns.dns_type.CNAME ) { 109 | answers.push( {name: packet.question.name, type: dns.dns_type.CNAME, class: dns.dns_class.IN, ttl: 300, rdata: "www.foo.bar.baz" } ); 110 | } else if ( packet.question.type == dns.dns_type.NS ) { 111 | answers.push( {name: packet.question.name, type: dns.dns_type.NS, class: dns.dns_class.IN, ttl: 300, rdata: "ns1.foo.bar.baz" } ); 112 | answers.push( {name: packet.question.name, type: dns.dns_type.NS, class: dns.dns_class.IN, ttl: 300, rdata: "ns2.foo.bar.baz" } ); 113 | } else if ( packet.question.type == dns.dns_type.TXT ) { 114 | 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"] } ); 115 | } else if ( packet.question.type == dns.dns_type.MX ) { 116 | 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"} } ); 117 | 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"} } ); 118 | } else if ( packet.question.type == dns.dns_type.SRV ) { 119 | 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"} } ); 120 | } else if ( packet.question.type == dns.dns_type.SOA ) { 121 | 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} } ); 122 | } 123 | 124 | if ( packet.question.name.toString().endsWith("bar.com") ) { 125 | test_result = dns.shortcut_response(data, packet, answers); 126 | } else if ( packet.question.name.toString().endsWith("baz.com") ) { 127 | test_result = dns.shortcut_nxdomain(data, packet); 128 | } else { 129 | packet.flags |= dns.dns_flags.AA | dns.dns_flags.QR; 130 | packet.codes |= dns.dns_codes.RA; 131 | 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} }); 132 | packet.additional.push( {name: packet.question.name, type: dns.dns_type.NS, class: dns.dns_class.IN, ttl: 300, rdata: "ns1.foo.bar.baz" } ); 133 | packet.additional.push( {name: packet.question.name, type: dns.dns_type.NS, class: dns.dns_class.IN, ttl: 300, rdata: "ns2.foo.bar.baz" } ); 134 | packet.answers = answers; 135 | test_result = dns.encode_packet(packet); 136 | } 137 | if (s.variables.protocol == "TCP" ) { 138 | test_result = Buffer.concat( [ to_bytes( test_result.length ), test_result ]); 139 | } 140 | return test_result; 141 | } 142 | 143 | -------------------------------------------------------------------------------- /njs.d/dns/glb.js: -------------------------------------------------------------------------------- 1 | /** 2 | 3 | BEGIN GLB Functions 4 | 5 | **/ 6 | 7 | import dns from "libdns.js"; 8 | export default {get_response, get_edns_subnet, process_request}; 9 | 10 | // Any encoded response packets for NGINX to send back go here 11 | var glb_res_packet = String.bytesFrom([]); 12 | 13 | // Client subnet gets stored in the variable if we have one 14 | var glb_edns_subnet = String.bytesFrom([]); 15 | 16 | // Function for js_set to use in order to pick up the glb_res_packet above 17 | function get_response(s) { 18 | return glb_res_packet; 19 | } 20 | 21 | // Function to get the EDNS subnet 22 | function get_edns_subnet(s) { 23 | return glb_edns_subnet; 24 | } 25 | 26 | // Process a DNS request and generate a response packet, saving it into glb_res_packet 27 | function process_request(s) { 28 | s.on("upload", function(data,flags) { 29 | s.warn( "Received: " + data.toString('hex') ); 30 | var packet = dns.parse_packet(data); 31 | var glb_use_edns = new Boolean(parseInt(s.variables.glb_use_edns)); 32 | s.warn( "ID: " + packet.id ); 33 | s.warn( "QD: " + packet.qd ); 34 | s.warn( "AR: " + packet.ar ); 35 | if ( packet.qd == 1 ) { 36 | dns.parse_question(packet); 37 | s.warn("Name: " + packet.question.name); 38 | 39 | // Decode additional records, most clients will send an EDNS (OPT) to increase payload size 40 | // and for EDNS Client Subnet, Cookies, etc. 41 | if ( packet.ar > 0 ) { 42 | // only decode if EDNS is enabled 43 | s.warn( "USE EDNS: " + glb_use_edns ); 44 | if ( glb_use_edns ) { 45 | dns.parse_complete(packet,1); 46 | if ( "edns" in packet ) { 47 | if ( packet.edns.opts.csubnet ) { 48 | s.warn( "EDNS Subnet: " + packet.edns.opts.csubnet.subnet ); 49 | glb_edns_subnet = packet.edns.opts.csubnet.subnet; 50 | } 51 | } 52 | } 53 | } 54 | 55 | // Check if we're doing GLB for the given name 56 | var config = glb_get_config( packet, "", s ); 57 | if ( ! Array.isArray(config) ) { 58 | s.warn("Failed to get config for: " + packet.question.name ); 59 | glb_res_packet = glb_failure(packet, dns.dns_codes.NXDOMAIN ); 60 | s.warn( "Sending: " + glb_res_packet.toString('hex') ); 61 | s.done(); 62 | return; 63 | } 64 | 65 | // GSLB this muther 66 | var nodes = glb_get_nodes( packet, config, s ); 67 | if ( ! Array.isArray(nodes) ) { 68 | s.warn("Failed to get any nodes for: " + packet.question.name ); 69 | glb_res_packet = glb_failure(packet, dns.dns_codes.SERVFAIL ); 70 | s.warn( "Sending: " + glb_res_packet.toString('hex') ); 71 | s.done(); 72 | return; 73 | } 74 | 75 | // Build an array of answers from the nodes 76 | var answers = []; 77 | if ( config[1] == "active" ) { 78 | nodes.forEach( function(node) { 79 | answers.push( {name: packet.question.name, type: dns.dns_type.A, class: dns.dns_class.IN, ttl: config[2], rdata: node} ); 80 | }); 81 | } else if ( config[1] == "random" ) { 82 | var node = nodes[Math.floor(Math.random()*nodes.length)]; 83 | answers.push( {name: packet.question.name, type: dns.dns_type.A, class: dns.dns_class.IN, ttl: config[2], rdata: node} ); 84 | } else if ( config[1] == "geoip" ) { 85 | var distance=99999999; 86 | var closest = []; 87 | var client_ip, client_lat, client_lon; 88 | /**if ( glb_edns_subnet ) { 89 | client_lat = s.variables.edns_latitude; 90 | client_lon = s.variables.edns_longitude; 91 | client_ip = glb_edns_subnet; 92 | } else { **/ 93 | client_lat = s.variables.geoip2_latitude; 94 | client_lon = s.variables.geoip2_longitude; 95 | client_ip = s.variables.geoip_source; 96 | //} 97 | s.warn( "Client: " + client_ip + ", Lat: " + client_lat ); 98 | s.warn( "Client: " + client_ip + ", Lon: " + client_lon ); 99 | for (var i=0; i< nodes.length; i++ ) { 100 | var suffix = "_geoip_" + nodes[i].replace(/\./g, '_'); 101 | var node_location = glb_get_config( packet, suffix, s ) 102 | if ( ! node_location ) { 103 | s.warn( "GEO location missing. Please add GEOIP key for node: " + nodes[i] ); 104 | continue; 105 | } 106 | var nd = glb_calc_distance( client_lon, client_lat, 107 | node_location[1], node_location[0]); 108 | s.warn( "Distance to: " + nodes[i] + " - " + nd ); 109 | if ( nd < distance ) { 110 | closest = [ nodes[i] ]; 111 | distance = nd; 112 | } else if ( nd == distance ) { 113 | closest.push( nodes[i] ); 114 | } 115 | } 116 | closest.forEach( function(node) { 117 | answers.push( {name: packet.question.name, type: dns.dns_type.A, class: dns.dns_class.IN, ttl: config[2], rdata: node} ); 118 | }); 119 | } else { 120 | s.warn("Unknown LB Algorithm: '" + config[1] + "' for: " + packet.question.name ); 121 | glb_res_packet = glb_failure(packet, dns.dns_codes.SERVFAIL ); 122 | s.warn( "Sending: " + glb_res_packet.toString('hex') ); 123 | s.done(); 124 | return; 125 | } 126 | 127 | // Shortcut - copy data from request 128 | glb_res_packet = dns.shortcut_response(data, packet, answers); 129 | 130 | // The long way, decode/encode 131 | //var response = dns.gen_response_packet( packet, packet.question, answers, [], [] ); 132 | //glb_res_packet = dns.encode_packet( response ); 133 | 134 | s.warn( "Sending: " + glb_res_packet.toString('hex') ); 135 | s.done(); 136 | } 137 | }); 138 | } 139 | 140 | function glb_failure(packet, code) { 141 | var failed = dns.gen_new_packet( packet.id, packet.flags, packet.codes); 142 | failed.question = packet.question; 143 | failed.qd = 1; 144 | failed.codes |= code; 145 | failed.flags |= dns.dns_flags.QR; 146 | return dns.encode_packet( failed ); 147 | } 148 | 149 | function glb_get_config( packet, suffix, s) { 150 | var key = packet.question.name.replace(/\./g, '_') + suffix; 151 | var uri = '/4/stream/keyvals/glb_config'; 152 | var config; 153 | if ( njs.version.slice(0,3) >= 0.9 ) { 154 | // future functionality 155 | var db = s.api( uri ); 156 | config = db.read(key); 157 | } else { 158 | config = s.variables[ key ]; 159 | } 160 | if ( config ) { 161 | config = config.split(','); 162 | } 163 | return config; 164 | } 165 | 166 | function glb_get_nodes( packet, config, s ) { 167 | var key = packet.question.name.replace(/\./g, '_'); 168 | var uri = "/4/" + config[0] + "/upstreams/" + key; 169 | var nodes; 170 | if ( njs.version.slice(0,3) >= 0.9 ) { 171 | var db = s.api( uri ); 172 | var json = db.read(key); 173 | nodes = glb_process_upstream_status( json, config ); 174 | } else { 175 | // No API, so try _nodes list 176 | nodes = s.variables[ key + "_nodes" ]; 177 | nodes = nodes.split(','); 178 | } 179 | return nodes; 180 | } 181 | 182 | function glb_process_upstream_status( json, config ) { 183 | // TODO process upstream peers 184 | var primary = []; 185 | var backup = []; 186 | } 187 | 188 | /** 189 | * Calculate distance between two GPS locations. 190 | * Thanks to: https://www.barattalo.it/coding/decimal-degrees-conversion-and-distance-of-two-points-on-google-map/ 191 | **/ 192 | function glb_calc_distance(lat1,lon1,lat2,lon2) { 193 | var R = 6371; // km (change this constant to get miles) 194 | var dLat = (lat2-lat1) * Math.PI / 180; 195 | var dLon = (lon2-lon1) * Math.PI / 180; 196 | var a = Math.sin(dLat/2) * Math.sin(dLat/2) + 197 | Math.cos(lat1 * Math.PI / 180 ) * Math.cos(lat2 * Math.PI / 180 ) * 198 | Math.sin(dLon/2) * Math.sin(dLon/2); 199 | var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); 200 | var d = R * c; 201 | if (d>1) return Math.round(d); 202 | else if (d<=1) return Math.round(d*1000)+"m"; 203 | return d; 204 | } 205 | 206 | -------------------------------------------------------------------------------- /njs.d/dns/dns.js: -------------------------------------------------------------------------------- 1 | import dns from "libdns.js"; 2 | export default {get_qname, get_response, preread_doh_request, preread_dns_request, filter_doh_request}; 3 | 4 | /** 5 | * DNS Decode Level 6 | * 0: No decoding, minimal processing required to strip packet from HTTP wrapper (fastest) 7 | * 1: Parse DNS Header and Question. We can log the Question, Class, Type, and Result Code 8 | * 2: As 1, but also parse answers. We can log the answers, and also cache responses in HTTP Content-Cache 9 | * 3: Very Verbose, log everything as above, but also write packet data to error log (slowest) 10 | **/ 11 | var dns_decode_level = 3; 12 | 13 | /** 14 | * DNS Debug Level 15 | * Specify the decoding level at which we should log packet data to the error log. 16 | * Default is level 3 (max decoding) 17 | **/ 18 | var dns_debug_level = 3; 19 | 20 | /** 21 | * DNS Question Load Balancing 22 | * Set this to true, if you want to pick the upstream pool based on the DNS Question. 23 | * Doing so will disable HTTP KeepAlives for DoH so that we can create a new socket for each query 24 | **/ 25 | var dns_question_balancing = false; 26 | 27 | // The DNS Question name 28 | var dns_name = Buffer.alloc(0); 29 | 30 | function get_qname(s) { 31 | return dns_name; 32 | } 33 | 34 | // The Optional DNS response, this is set when we want to block a specific domain 35 | var dns_response = Buffer.alloc(0); 36 | 37 | function get_response(s) { 38 | return dns_response.toString(); 39 | } 40 | 41 | // Encode the given number to two bytes (16 bit) 42 | function to_bytes( number ) { 43 | return Buffer.from( [ ((number>>8) & 0xff), (number & 0xff) ] ); 44 | } 45 | 46 | function debug(s, msg) { 47 | if ( dns_decode_level >= dns_debug_level ) { 48 | s.warn(msg); 49 | } 50 | } 51 | 52 | function process_doh_request(s, decode, scrub) { 53 | s.on("upstream", function(data,flags) { 54 | if ( data.length == 0 ) { 55 | return; 56 | } 57 | var dataString = data.toString('utf8'); 58 | const lines = dataString.split("\r\n"); 59 | var bytes; 60 | var packet; 61 | if(lines[0].startsWith("GET")) { 62 | var line = lines[0]; 63 | var path = line.split(" ")[1] 64 | var params = path.split("?")[1] 65 | var qs = params.split("&"); 66 | qs.some( param => { 67 | if (param.startsWith("dns=") ) { 68 | bytes = Buffer.from(param.slice(4), "base64url"); 69 | return true; 70 | } 71 | return false; 72 | }); 73 | } 74 | 75 | if(lines[0].startsWith("POST")) { 76 | const index = lines.findIndex(line=>{ 77 | if(line.length == 0) { 78 | return true; 79 | } 80 | }) 81 | if(index>0 && lines.length >= index + 1){ 82 | bytes = Buffer.from(lines[index + 1]); 83 | } 84 | } 85 | 86 | if (bytes) { 87 | debug(s, "process_doh_request: DNS Req: " + bytes.toString('hex') ); 88 | if (decode) { 89 | packet = dns.parse_packet(bytes); 90 | debug(s, "process_doh_request: DNS Req ID: " + packet.id ); 91 | dns.parse_question(packet); 92 | debug(s,"process_doh_request: DNS Req Name: " + packet.question.name); 93 | dns_name = packet.question.name; 94 | } 95 | if (scrub) { 96 | domain_scrub(s, bytes, packet); 97 | s.done(); 98 | } else { 99 | s.send( to_bytes(bytes.length) ); 100 | s.send( bytes, {flush: true} ); 101 | } 102 | } else { 103 | if ( ! scrub) { 104 | debug(s, "process_doh_request: DNS Req: " + line.toString() ); 105 | s.send(""); 106 | data = ""; 107 | } 108 | } 109 | }); 110 | } 111 | 112 | function process_dns_request(s, decode, scrub) { 113 | s.on("upstream", function(bytes,flags) { 114 | if ( bytes.length == 0 ) { 115 | return; 116 | } 117 | var packet; 118 | if (bytes) { 119 | if (s.variables.protocol == "TCP") { 120 | // Drop the TCP length field 121 | bytes = bytes.slice(2); 122 | } 123 | debug(s, "process_dns_request: DNS Req: " + bytes.toString('hex') ); 124 | if (decode) { 125 | packet = dns.parse_packet(bytes); 126 | debug(s, "process_dns_request: DNS Req ID: " + packet.id ); 127 | dns.parse_question(packet); 128 | debug(s,"process_dns_request: DNS Req Name: " + packet.question.name); 129 | dns_name = packet.question.name; 130 | } 131 | if (scrub) { 132 | domain_scrub(s, bytes, packet); 133 | s.done(); 134 | } else { 135 | if (s.variables.protocol == "TCP") { 136 | s.send( to_bytes(bytes.length) ); 137 | } 138 | s.send( bytes, {flush: true} ); 139 | } 140 | } 141 | }); 142 | } 143 | 144 | function domain_scrub(s, data, packet) { 145 | var found = false; 146 | if ( s.variables.server_port == 9953 ) { 147 | dns_response = dns.shortcut_nxdomain(data, packet); 148 | if (s.variables.protocol == "TCP" ) { 149 | dns_response = Buffer.concat( [ to_bytes( dns_response.length ), dns_response ]); 150 | } 151 | debug(s,"Scrubbed: Response: " + dns_response.toString('hex') ); 152 | } else if ( s.variables.server_port == 9853 ) { 153 | var answers = []; 154 | if ( packet.question.type == dns.dns_type.A ) { 155 | answers.push( {name: packet.question.name, type: dns.dns_type.A, class: dns.dns_class.IN, ttl: 300, rdata: "0.0.0.0" } ); 156 | } else if ( packet.question.type == dns.dns_type.AAAA ) { 157 | 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" } ); 158 | } 159 | dns_response = dns.shortcut_response(data, packet, answers); 160 | if (s.variables.protocol == "TCP" ) { 161 | dns_response = Buffer.concat( [ to_bytes( dns_response.length ), dns_response ]); 162 | } 163 | debug(s,"Scrubbed: Response: " + dns_response.toString('hex') ); 164 | } else { 165 | debug(s,"Scrubbing: Check: Name: " + packet.question.name ); 166 | if ( s.variables.scrub_action ) { 167 | debug(s, "Scrubbing: Check: EXACT MATCH: Name: " + packet.question.name + ", Action: " + s.variables.scrub_action ); 168 | dns_response = s.variables.scrub_action; 169 | return; 170 | } else { 171 | ["blocked", "blackhole"].forEach( function( list ) { 172 | if(found) { return }; 173 | var blocked = s.variables[ list + "_domains" ]; 174 | if ( blocked ) { 175 | blocked = blocked.split(','); 176 | blocked.forEach( function( domain ) { 177 | if (packet.question.name.endsWith( domain )) { 178 | debug(s,"Scrubbing: Check: LISTED: Name: " + packet.question.name + ", Action: " + list ); 179 | dns_response = list; 180 | found = true; 181 | return; 182 | } 183 | }); 184 | } 185 | }); 186 | if(found) { return }; 187 | } 188 | debug(s,"Scrubbing: Check: NOT FOUND: Name: " + packet.question.name); 189 | } 190 | } 191 | 192 | function preread_dns_request(s) { 193 | process_dns_request(s, true, true); 194 | } 195 | 196 | function preread_doh_request(s) { 197 | process_doh_request(s, true, true); 198 | } 199 | 200 | function filter_doh_request(s) { 201 | 202 | if ( dns_decode_level >= 3 ) { 203 | process_doh_request(s, true, false); 204 | } else { 205 | process_doh_request(s, false, false); 206 | } 207 | 208 | s.on("downstream", function(data, flags) { 209 | if ( data.length == 0 ) { 210 | return; 211 | } 212 | // Drop the TCP length field 213 | data = data.slice(2); 214 | 215 | debug(s, "DNS Res: " + data.toString('hex') ); 216 | var packet; 217 | var answers = ""; 218 | var cache_time = 10; 219 | if ( dns_question_balancing ) { 220 | s.send("HTTP/1.1 200\r\nConnection: Close\r\nContent-Type: application/dns-message\r\nContent-Length:" + data.length + "\r\n"); 221 | } else { 222 | 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"); 223 | } 224 | 225 | if ( dns_decode_level > 0 ) { 226 | packet = dns.parse_packet(data); 227 | dns.parse_question(packet); 228 | dns_name = packet.question.name; 229 | s.send("X-DNS-Question: " + dns_name + "\r\n"); 230 | s.send("X-DNS-Type: " + dns.dns_type.value[packet.question.type] + "\r\n"); 231 | s.send("X-DNS-Result: " + dns.dns_codes.value[packet.codes & 0x0f] + "\r\n"); 232 | 233 | if ( dns_decode_level > 1 ) { 234 | if ( dns_decode_level == 2 ) { 235 | dns.parse_answers(packet, 2); 236 | } else if ( dns_decode_level > 2 ) { 237 | dns.parse_complete(packet, 2); 238 | } 239 | //debug(s, "DNS Res Answers: " + JSON.stringify( Object.entries(packet.answers)) ); 240 | if ( "min_ttl" in packet ) { 241 | cache_time = packet.min_ttl; 242 | s.send("X-DNS-TTL: " + packet.min_ttl + "\r\n"); 243 | } 244 | 245 | if ( packet.an > 0 ) { 246 | packet.answers.forEach( function(r) { answers += "[" + dns.dns_type.value[r.type] + ":" + r.rdata + "]," }) 247 | answers.slice(0,-1); 248 | } else { 249 | answers = "[]"; 250 | } 251 | s.send("X-DNS-Answers: " + answers + "\r\n"); 252 | } 253 | debug(s, "DNS Res Packet: " + JSON.stringify( Object.entries(packet)) ); 254 | } 255 | 256 | var d = new Date( Date.now() + (cache_time*1000) ).toUTCString(); 257 | if ( ! d.includes(",") ) { 258 | d = d.split(" ") 259 | d = [d[0] + ',', d[2], d[1], d[3], d[4], d[5]].join(" "); 260 | } 261 | s.send("Cache-Control: public, max-age=" + cache_time + "\r\n" ); 262 | s.send("Expires: " + d + "\r\n" ); 263 | 264 | s.send("\r\n"); 265 | s.send( data, {flush: true} ); 266 | if ( dns_question_balancing ) { 267 | s.done(); 268 | } 269 | }); 270 | } 271 | 272 | -------------------------------------------------------------------------------- /njs.d/dns/libdns.js: -------------------------------------------------------------------------------- 1 | /** 2 | 3 | BEGIN DNS Functions 4 | 5 | **/ 6 | 7 | export default {dns_type, dns_class, dns_flags, dns_codes, 8 | parse_packet, parse_question, parse_answers, 9 | parse_complete, parse_resource_record, 10 | shortcut_response, shortcut_nxdomain, 11 | gen_new_packet, gen_response_packet, encode_packet} 12 | 13 | // DNS Types 14 | var dns_type = Object.freeze({ 15 | A: 1, 16 | NS: 2, 17 | CNAME: 5, 18 | SOA: 6, 19 | PTR: 12, 20 | MX: 15, 21 | TXT: 16, 22 | AAAA: 28, 23 | SRV: 33, 24 | OPT: 41, 25 | HTTPS: 65, 26 | AXFR: 252, 27 | ANY: 255, 28 | value: { 1:"A", 2:"NS", 5:"CNAME", 6:"SOA", 12:"PTR", 15:"MX", 16:"TXT", 29 | 28:"AAAA", 33:"SRV", 41:"OPT", 65:"HTTPS", 252:"AXFR", 255:"ANY" } 30 | }); 31 | 32 | // DNS Classes 33 | var dns_class = Object.freeze({ 34 | IN: 1, 35 | CS: 2, 36 | CH: 3, 37 | HS: 4, 38 | value: { 1:"IN", 2:"CS", 3:"CH", 4:"HS" } 39 | }); 40 | 41 | // DNS flags (made up of QR, Opcode (4bits), AA, TrunCation, Recursion Desired) 42 | var dns_flags = Object.freeze({ 43 | QR: 0x80, 44 | AA: 0x4, 45 | TC: 0x2, 46 | RD: 0x1 47 | }); 48 | 49 | // DNS Codes (made up of RA (Recursion Available), Zero (3bits), Response Code (4bits)) 50 | var dns_codes = Object.freeze({ 51 | RA: 0x80, 52 | Z: 0x70, 53 | //RCODE: 0xf, 54 | NOERROR: 0x0, 55 | FORMERR: 0x1, 56 | SERVFAIL: 0x2, 57 | NXDOMAIN: 0x3, 58 | NOTIMPL: 0x4, 59 | REFUSED: 0x5, 60 | value: { 0x80:"RA", 0x70:"Z", 0x0:"NOERROR", 0x1:"FORMERR", 0x2:"SERVFAIL", 0x3:"NXDOMAIN", 0x4:"NOTIMPL", 0x5:"REFUSED" } 61 | }); 62 | 63 | // Encode the given number to two bytes (16 bit) 64 | function to_bytes( number ) { 65 | return Buffer.from( [ ((number>>8) & 0xff), (number & 0xff) ] ); 66 | } 67 | 68 | // Encode the given number to 4 bytes (32 bit) 69 | function to_bytes32( number ) { 70 | return Buffer.from( [ (number>>24)&0xff, (number>>16)&0xff, (number>>8)&0xff, number&0xff ] ); 71 | } 72 | 73 | // Create a new empty DNS packet structure 74 | function gen_new_packet(id, flags, codes) { 75 | var dns_packet = { id: id, flags: flags, codes: codes, qd: 0, an: 0, ns: 0, ar: 0, 76 | question: {}, 77 | answers: [], 78 | authority: [], 79 | additional: [] 80 | }; 81 | return dns_packet; 82 | } 83 | 84 | /** Create a new response packet suitable as a reply to the given request 85 | * You should also supply some answers, authority and/or additional records 86 | * in arrays to populate the various sections. 87 | **/ 88 | function gen_response_packet( request, question, answers, authority, additional ) { 89 | var response = gen_new_packet(request.id, request.flags, request.codes); 90 | response.flags |= dns_flags.AA + dns_flags.QR; 91 | response.codes |= dns_codes.RA; 92 | if ( question == null ) { 93 | response.qd = 0; 94 | } else { 95 | response.qd = 1; 96 | response.question = request.question; 97 | } 98 | answers.forEach( function(answer) { 99 | response.an++; 100 | response.answers.push( answer ); 101 | }); 102 | return response; 103 | } 104 | 105 | /** Encode the provided packet, converting it from the javascript object structure into a bytestring 106 | * Returns a bytestring suitable for dropping into a UDP packet, or returning to NGINX 107 | **/ 108 | function encode_packet( packet ) { 109 | var encoded = Buffer.from( to_bytes( packet.id ) ); 110 | encoded = Buffer.concat( [ encoded, Buffer.from([ packet.flags ])] ); 111 | encoded = Buffer.concat( [ encoded, Buffer.from([ packet.codes ])] ); 112 | encoded = Buffer.concat( [ encoded, Buffer.from( to_bytes( packet.qd ))] ); // Questions 113 | encoded = Buffer.concat( [ encoded, Buffer.from( to_bytes( packet.answers.length ))] ); // Answers 114 | encoded = Buffer.concat( [ encoded, Buffer.from( to_bytes( packet.authority.length ))] ); // Authority 115 | encoded = Buffer.concat( [ encoded, Buffer.from( to_bytes( packet.additional.length ))] ); // Additional 116 | encoded = Buffer.concat( [ encoded, encode_question(packet) ]); 117 | packet.answers.forEach( function(answer) { 118 | encoded = Buffer.concat( [ encoded, gen_resource_record(packet, answer.name, answer.type, answer.class, answer.ttl, answer.rdata) ]); 119 | }); 120 | packet.authority.forEach( function(auth) { 121 | encoded = Buffer.concat( [ encoded, gen_resource_record(packet, auth.name, auth.type, auth.class, auth.ttl, auth.rdata)] ); 122 | }); 123 | packet.additional.forEach( function(adtnl) { 124 | encoded = Buffer.concat( [ encoded, gen_resource_record(packet, adtnl.name, adtnl.type, adtnl.class, adtnl.ttl, adtnl.rdata)] ); 125 | }); 126 | return encoded; 127 | } 128 | 129 | /** Don't mess about. This is a shortcut for responding to DNS Queries. We copy the question out of the query 130 | * and cannibalise the original request to generate our response. 131 | **/ 132 | function shortcut_response(data, packet, answers) { 133 | var response = Buffer.alloc(0); 134 | response = Buffer.concat( [ response, data.slice(0,2) ] ); 135 | response = Buffer.concat( [ response, Buffer.from([ (packet.flags |= dns_flags.AA | dns_flags.QR) ])] ); 136 | response = Buffer.concat( [ response, Buffer.from([ (packet.codes |= dns_codes.RA) ])] ); 137 | // append counts: qd, answer count, 0 auths, 0 additional 138 | response = Buffer.concat( [ response, Buffer.from([ 0x00, 0x01 ]), Buffer.from( to_bytes(answers.length)), Buffer.from( [0x0, 0x0, 0x0, 0x0 ]) ] ); 139 | response = Buffer.concat( [ response, data.slice(12, packet.question.qend ) ] ); 140 | answers.forEach( function(answer) { 141 | response = Buffer.concat( [ response, gen_resource_record(packet, answer.name, answer.type, answer.class, answer.ttl, answer.rdata) ]); 142 | }); 143 | return response; 144 | } 145 | 146 | function shortcut_nxdomain(data, packet) { 147 | var response = Buffer.alloc(0); 148 | response = Buffer.concat( [ response, data.slice(0,2) ] ); 149 | response = Buffer.concat( [ response, Buffer.from([ (packet.flags |= dns_flags.AA | dns_flags.QR) ])] ); 150 | response = Buffer.concat( [ response, Buffer.from([ (packet.codes |= dns_codes.NXDOMAIN | dns_codes.RA) ])] ); 151 | // append counts: qd, answer count, 0 auths, 0 additional 152 | response = Buffer.concat( [ response, Buffer.from([ 0x00, 0x01, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0 ]) ] ); 153 | response = Buffer.concat( [ response, data.slice(12, packet.question.qend ) ] ); 154 | return response; 155 | } 156 | 157 | /** Encode a question object into a bytestring suitable for use in a UDP packet 158 | **/ 159 | function encode_question(packet) { 160 | var encoded = Buffer.from( encode_label(packet.question.name) ); 161 | encoded = Buffer.concat( [ encoded, Buffer.from(to_bytes(packet.question.type)), Buffer.from(to_bytes(packet.question.class)) ] ); 162 | return encoded; 163 | } 164 | 165 | /** 166 | * Parse an incoming request bytestring into a DNS packet object. This function decodes the first 12 bytes of the headers. 167 | * You will probably want to call parse_question() next. 168 | **/ 169 | function parse_packet(data) { 170 | var packet = { id: data.readUInt16BE(0), flags: data[2], codes: data[3], min_ttl: 2147483647, 171 | qd: data.readUInt16BE(4), an: data.readUInt16BE(6), ns: data.readUInt16BE(8), 172 | ar: data.readUInt16BE(10), data: data.slice(12), question: [], answers:[], authority: [], additional: [], offset: 0 }; 173 | return packet; 174 | } 175 | 176 | /** 177 | * Parse the question section of a DNS request packet, adds the QNAME, QTYPE, and QCLASS to the packet object, and stores the 178 | * offset in the packet for processing any further sections. 179 | **/ 180 | function parse_question(packet) { 181 | 182 | /** QNAME, QTYPE, QCLASS **/ 183 | 184 | var name = parse_label(packet); 185 | packet.question = { name: name, type: packet.data.readUInt16BE(packet.offset), 186 | class: packet.data.readUInt16BE(packet.offset+2), qend: packet.offset + 16 }; 187 | packet.offset += 4; 188 | if ( packet.qd != 1 ) { 189 | return false; 190 | } 191 | return true; 192 | } 193 | 194 | function parse_answers(packet, decode_level) { 195 | 196 | // Process the question section if necessary 197 | if ( packet.question.length == 0 ) { 198 | parse_question(packet); 199 | } 200 | 201 | // Process answers 202 | if ( packet.an > 0 && packet.answers.length == 0 ) { 203 | packet.answers = parse_section(packet, packet.an, decode_level); 204 | } 205 | 206 | // If we didn't have any ttls in the packet, then cache for 5 minutes. 207 | if (packet.min_ttl == 2147483647) { 208 | packet.min_ttl = 300; 209 | } 210 | 211 | } 212 | 213 | 214 | // Parse all sections of the packet 215 | function parse_complete(packet, decode_level) { 216 | 217 | // Process the question section if necessary 218 | if ( packet.question.length == 0 ) { 219 | parse_question(packet); 220 | } 221 | 222 | // Process answers 223 | if ( packet.an > 0 && packet.answers.length == 0 ) { 224 | packet.answers = parse_section(packet, packet.an, decode_level); 225 | } 226 | 227 | // Process authority 228 | if ( packet.ns > 0 && packet.authority.length == 0) { 229 | packet.authority = parse_section(packet, packet.ns, decode_level); 230 | } 231 | 232 | // Process Additional 233 | if ( packet.ar > 0 && packet.additional.length == 0) { 234 | packet.additional = parse_section(packet, packet.ar, decode_level); 235 | } 236 | 237 | // If we didn't have any ttls in the packet, then cache for 5 minutes. 238 | if (packet.min_ttl == 2147483647) { 239 | packet.min_ttl = 300; 240 | } 241 | 242 | } 243 | 244 | function parse_section(packet, recs, decode_level) { 245 | var rrs = []; 246 | for (var i=0; i 63 ) { 284 | // Invalid DNS name, individual labels are limited to 63 bytes. 285 | //s.warn("DNS Error - parse_label encountered invaliad DNS name"); 286 | break; 287 | } else { 288 | name += packet.data.slice(++pos, pos+length) + "."; 289 | pos += length; 290 | } 291 | } 292 | 293 | if ( ! compressed ) { 294 | packet.offset = pos 295 | } 296 | 297 | name = name.slice(0,-1); 298 | return name; 299 | } 300 | 301 | /** TODO Check sizes on resources/packets 302 | labels 63 octets or less 303 | names 255 octets or less 304 | TTL positive values of a signed 32 bit number. 305 | UDP messages 512 octets or less 306 | **/ 307 | 308 | function encode_label( name ) { 309 | 310 | var data = Buffer.alloc(0); 311 | name.split('.').forEach( function(part){ 312 | data = Buffer.concat( [ data, Buffer.from([ part.length ]), Buffer.from(part) ] ); 313 | }); 314 | data = Buffer.concat( [data, Buffer.from([0]) ]); 315 | return data; 316 | 317 | } 318 | 319 | function gen_resource_record(packet, name, type, clss, ttl, rdata) { 320 | 321 | /** 322 | NAME 323 | TYPE (2 octets) 324 | CLASS (2 octects) 325 | TTL 32bit signed int 326 | RDLength 16bit int length of RDATA 327 | RDATA variable length string 328 | **/ 329 | 330 | var resource 331 | var record = ""; 332 | 333 | if ( name == packet.question.name ) { 334 | // The name matches the query, set a compression pointer. 335 | resource = Buffer.from([192, 12]); 336 | } else { 337 | // gen labels for the name 338 | resource = encode_label(name); 339 | } 340 | 341 | resource = Buffer.concat( [ resource, Buffer.from([ type & 0xff00, type & 0xff ]) ]); 342 | switch(type) { 343 | case dns_type.A: 344 | record = encode_arpa_v4(rdata); 345 | break; 346 | case dns_type.AAAA: 347 | record = encode_arpa_v6(rdata); 348 | break; 349 | case dns_type.NS: 350 | record = encode_label(rdata); 351 | break; 352 | case dns_type.CNAME: 353 | record = encode_label(rdata); 354 | break; 355 | case dns_type.SOA: 356 | record = encode_soa_record(rdata); 357 | break; 358 | case dns_type.SRV: 359 | record = encode_srv_record(rdata); 360 | break; 361 | case dns_type.MX: 362 | record = encode_mx_record(rdata); 363 | break; 364 | case dns_type.TXT: 365 | record = encode_txt_record(rdata); 366 | break; 367 | default: 368 | //TODO Barf 369 | } 370 | 371 | switch(clss) { 372 | case dns_class.IN: 373 | resource = Buffer.concat([ resource, Buffer.from( [ 0, 1 ] )]); 374 | break; 375 | default: 376 | //TODO Barf 377 | resource = Buffer.concat([ resource, Buffer.from( [ 99, 99 ] )]); 378 | } 379 | 380 | resource = Buffer.concat( [ resource, Buffer.from(to_bytes32(ttl)) ] ); 381 | resource = Buffer.concat( [ resource, Buffer.from(to_bytes( record.length )) ] ); 382 | resource = Buffer.concat( [ resource, Buffer.from(record) ] ); 383 | return resource; 384 | } 385 | 386 | // Process resource records, to a varying depth dictated by decode_level 387 | // decode_level {0: name+type, 1: name+type+class+ttl, 2: everything} 388 | function parse_resource_record(packet, decode_level) { 389 | 390 | /** 391 | NAME 392 | TYPE (2 octets) 393 | CLASS (2 octects) 394 | TTL 32bit signed int 395 | RDLength 16bit int length of RDATA 396 | RDATA variable length string 397 | **/ 398 | 399 | var resource = {} 400 | resource.name = parse_label(packet); 401 | resource.type = packet.data.readUInt16BE(packet.offset); 402 | packet.offset += 2; 403 | 404 | if ( decode_level > 0 ) { 405 | if (resource.type == dns_type.OPT ) { 406 | // EDNS 407 | parse_edns_options(packet); 408 | } else { 409 | resource.class = packet.data.readUInt16BE(packet.offset); 410 | resource.ttl = packet.data.readUInt32BE(packet.offset+2); 411 | resource.rdlength = packet.data.readUInt16BE(packet.offset+6); 412 | packet.offset +=8; 413 | if ( decode_level == 1 ) { 414 | resource.rdata = packet.data.slice(packet.offset, packet.offset + resource.rdlength); 415 | packet.offset += resource.rdlength; 416 | } else { 417 | switch(resource.type) { 418 | case dns_type.A: 419 | resource.rdata = parse_arpa_v4(packet, resource); 420 | break; 421 | case dns_type.AAAA: 422 | resource.rdata = parse_arpa_v6(packet, resource); 423 | break; 424 | case dns_type.NS: 425 | resource.rdata = parse_label(packet); 426 | break; 427 | case dns_type.CNAME: 428 | resource.rdata = parse_label(packet); 429 | break; 430 | case dns_type.SOA: 431 | resource.rdata = parse_soa_record(packet); 432 | break; 433 | case dns_type.SRV: 434 | resource.rdata = parse_srv_record(packet); 435 | break; 436 | case dns_type.MX: 437 | resource.rdata = parse_mx_record(packet); 438 | break; 439 | case dns_type.TXT: 440 | resource.rdata = parse_txt_record(packet, resource.rdlength); 441 | break; 442 | default: 443 | resource.rdata = packet.data.slice(packet.offset, packet.offset + resource.rdlength); 444 | packet.offset += resource.rdlength; 445 | } 446 | } 447 | } 448 | } 449 | return resource; 450 | } 451 | 452 | function encode_arpa_v4( ipv4 ) { 453 | var rdata = Buffer.alloc(4); 454 | var index = 0; 455 | ipv4.split('\.').forEach( function(octet) { 456 | rdata[index++] = octet; 457 | }); 458 | return rdata; 459 | } 460 | 461 | function parse_arpa_v4(packet) { 462 | var octet = [0,0,0,0]; 463 | for (var i=0; i< 4 ; i++ ) { 464 | octet[i] = packet.data[packet.offset++]; 465 | } 466 | return octet.join("."); 467 | } 468 | 469 | function encode_arpa_v6( ipv6 ) { 470 | var rdata = Buffer.alloc(0); 471 | ipv6.split(':').forEach( function(segment) { 472 | rdata = Buffer.concat( [ rdata, Buffer.from( segment[0] + segment[1], 'hex') ] ); 473 | rdata = Buffer.concat( [ rdata, Buffer.from( segment[2] + segment[3], 'hex') ] ); 474 | }); 475 | return rdata; 476 | } 477 | 478 | function parse_arpa_v6(packet) { 479 | var ipv6 = ""; 480 | for (var i=0; i<8; i++ ) { 481 | ipv6 += packet.data.toString('hex', packet.offset++, ++packet.offset) + ":"; 482 | } 483 | return ipv6.slice(0,-1); 484 | } 485 | 486 | function encode_txt_record( text_array ) { 487 | var rdata = Buffer.alloc(0); 488 | text_array.forEach( function(text) { 489 | var tl = text.length; 490 | if ( tl > 255 ) { 491 | for (var i=0 ; i < tl ; i++ ) { 492 | var len = (tl > (i+255)) ? 255 : tl - i; 493 | rdata = Buffer.concat( [ rdata, Buffer.from([len]), Buffer.from(text.slice(i,i+len)) ] ); 494 | i += len; 495 | } 496 | } else { 497 | rdata = Buffer.concat( [ rdata, Buffer.from([tl]), Buffer.from(text) ] ); 498 | } 499 | }); 500 | return rdata; 501 | } 502 | 503 | function parse_txt_record(packet, length) { 504 | var txt = []; 505 | var pos = 0; 506 | while ( pos < length ) { 507 | var tl = packet.data[packet.offset++]; 508 | txt.push( packet.data.toString('utf8', packet.offset, packet.offset + tl)); 509 | pos += tl + 1; 510 | packet.offset += tl; 511 | } 512 | return txt; 513 | } 514 | 515 | function encode_mx_record( mx ) { 516 | var rdata = Buffer.alloc(0); 517 | rdata += to_bytes( mx.priority ); 518 | rdata += encode_label( mx.exchange ); 519 | return rdata; 520 | } 521 | 522 | function parse_mx_record(packet) { 523 | var mx = {}; 524 | mx.priority = packet.data.readUInt16BE(packet.offset); 525 | packet.offset += 2; 526 | mx.exchange = parse_label(packet); 527 | return mx; 528 | } 529 | 530 | function encode_srv_record( srv ) { 531 | var rdata = Buffer.alloc(6) 532 | rdata.writeInt16BE( srv.priority, 0 ); 533 | rdata.writeInt16BE( srv.weight, 2 ); 534 | rdata.writeInt16BE( srv.port, 4 ); 535 | rdata = Buffer.concat( [ rdata, encode_label( srv.target ) ]); 536 | ngx.log( ngx.WARN, rdata.toString('hex')); 537 | return rdata; 538 | } 539 | 540 | function parse_srv_record(packet) { 541 | var srv = {}; 542 | srv.priority = packet.data.readUInt16BE(packet.offset); 543 | srv.weight = packet.data.readUInt16BE(packet.offset+2); 544 | srv.port = packet.data.readUInt16BE(packet.offset+4); 545 | packet.offset += 6; 546 | srv.target = parse_label(packet); 547 | return srv; 548 | } 549 | 550 | function encode_soa_record( soa ) { 551 | var rdata = Buffer.concat([ encode_label(soa.primary), encode_label(soa.mailbox) ]); 552 | rdata = Buffer.concat( [ rdata, Buffer.from(to_bytes32(soa.serial)), Buffer.from(to_bytes32(soa.refresh)), 553 | Buffer.from(to_bytes32(soa.retry)), Buffer.from(to_bytes32(soa.expire)), Buffer.from(to_bytes32(soa.minTTL)) ]); 554 | return rdata; 555 | } 556 | 557 | function parse_soa_record(packet) { 558 | var soa = {}; 559 | soa.primary = parse_label(packet); 560 | soa.mailbox = parse_label(packet); 561 | soa.serial = packet.data.readUInt32BE(packet.offset); 562 | soa.refresh = packet.data.readUInt32BE(packet.offset+=4); 563 | soa.retry = packet.data.readUInt32BE(packet.offset+=4); 564 | soa.expire = packet.data.readUInt32BE(packet.offset+=4); 565 | soa.minTTL = packet.data.readUInt32BE(packet.offset+=4); 566 | packet.offset +=4; 567 | return soa; 568 | } 569 | 570 | function parse_edns_options(packet) { 571 | 572 | packet.edns = {} 573 | packet.edns.opts = {} 574 | packet.edns.size = packet.data.readUInt16BE(packet.offset); 575 | packet.edns.rcode = packet.data[packet.offset+2]; 576 | packet.edns.version = packet.data[packet.offset+3]; 577 | packet.edns.z = packet.data.readUInt16BE(packet.offset+4); 578 | packet.edns.rdlength = packet.data.readUInt16BE(packet.offset+6); 579 | packet.offset += 8; 580 | 581 | var end = packet.offset + packet.edns.rdlength; 582 | for ( ; packet.offset < end ; ) { 583 | var opcode = packet.data.readUInt16BE(packet.offset); 584 | var oplength = packet.data.readUInt16BE(packet.offset+2); 585 | packet.offset += 4; 586 | if ( opcode == 8 ) { 587 | //client subnet 588 | packet.edns.opts.csubnet = {} 589 | packet.edns.opts.csubnet.family = packet.data.readUInt16BE(packet.offset); 590 | packet.edns.opts.csubnet.netmask = packet.data[packet.offset+2]; 591 | packet.edns.opts.csubnet.scope = packet.data[packet.offset+3]; 592 | packet.offset += 4; 593 | if ( packet.edns.opts.csubnet.family == 1 ) { 594 | // IPv4 595 | var octet = [0,0,0,0]; 596 | for (var i=4; i< oplength ; i++ ) { 597 | octet[i-4] = packet.data[packet.offset++]; 598 | } 599 | packet.edns.opts.csubnet.subnet = octet.join("."); 600 | break; 601 | } else { 602 | // We don't support IPv6 yet. 603 | packet.edns.opts = {} 604 | break; 605 | } 606 | } else { 607 | // We only look for CSUBNET... Not interested in anything else at this time. 608 | packet.offset += oplength; 609 | } 610 | } 611 | 612 | } 613 | 614 | 615 | --------------------------------------------------------------------------------