├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── __test__ ├── certificate.pem ├── doh.test.js ├── dot.test.js ├── key.pem └── util.test.js ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── doh.ts ├── dot.ts ├── index.ts └── util.ts └── tsconfig.json /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Test and Build 2 | 3 | on: ["push" , "pull_request"] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | node-version: [15.x, 17.x, 18.x] 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v3 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - run: npm ci 21 | - run: npm test 22 | - run: npm run build 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "10" 5 | - "11" 6 | - "12" 7 | 8 | install: 9 | - npm install -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Peter Lai 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # doh-js-client 2 | [![Test and Build](https://github.com/sc0Vu/doh-js-client/actions/workflows/ci.yml/badge.svg)](https://github.com/sc0Vu/doh-js-client/actions/workflows/ci.yml) 3 | 4 | DNS-over-HTTPS/DNS-over-TLS client for nodejs, secure your nodejs dns query with modern tls. 5 | 6 | # Install 7 | ```BASH 8 | $ npm install doh-js-client 9 | ``` 10 | 11 | # Usage 12 | 13 | ## DNS over HTTPS (:443) 14 | 15 | 1. Initialize the instance with given provider (google, cloudflare, cleanbrowsing) 16 | ```JS 17 | const DoH = require('doh-js-client').DoH 18 | 19 | let dns = new DoH('google') 20 | ``` 21 | 22 | 2. Resolve dns name 23 | ```JS 24 | dns.resolve('example.com', 'A') 25 | .then(function (record) { 26 | // do something 27 | }) 28 | .catch(function (err) { 29 | // something wrong happened 30 | }) 31 | ``` 32 | 33 | ## DNS over TLS (:583) 34 | 35 | 1. Initialize the instance with given provider (google, cloudflare, cleanbrowsing) 36 | ```JS 37 | const DoT = require('doh-js-client').DoT 38 | 39 | let dns = new DoT('google', privateKeyFilePath, certificateFilePath) 40 | ``` 41 | 42 | 2. Resolve dns name 43 | ```JS 44 | dns.resolve('example.com', 'A') 45 | .then(function (record) { 46 | // do something 47 | }) 48 | .catch(function (err) { 49 | // something wrong happened 50 | }) 51 | ``` 52 | 53 | # Known issue and supported dns type 54 | 55 | 1. Cleanbrowsing doesn't support caa query (return 400). 56 | 57 | 2. Supported dns type: 58 | * A 59 | * AAAA 60 | * CAA 61 | * CNAME 62 | * DS 63 | * DNSKEY 64 | * MX 65 | * NS 66 | * NSEC 67 | * NSEC3 68 | * RRSIG 69 | * SOA 70 | * TXT 71 | 72 | # License 73 | 74 | MIT -------------------------------------------------------------------------------- /__test__/certificate.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDazCCAlOgAwIBAgIUIIMjR3U3ekp42QmfNWv8cdJWmogwDQYJKoZIhvcNAQEL 3 | BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM 4 | GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMDA1MjgwMjU4NTdaFw0yMTA1 5 | MjgwMjU4NTdaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw 6 | HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB 7 | AQUAA4IBDwAwggEKAoIBAQDjZmSU2pMxk+lXCbHYtU1K6flmoE4MmXGLQa5RBqKg 8 | j9ZkdSving6KfFiB/zkSSGhakWVjwvCZ797hd2M5b6fik7pPz/Kq9CcIU1ytCpaP 9 | hMEsBzzJpDUWNLjOzDaPiOLFFiku0VYsrUETukaaz2EvqPRQLS5f+W16E0zrH9zI 10 | drBK6zHNQX/bKF+ho5lKjKP+mmIhIpd3gb3VTUGFpYcaHn0X80Xby6KxmhuPzAFi 11 | 3s7QUsI96E9jedgB0qrDxvhPAlTrT+k0Y5kDAJT4TsEEKNOiDYGth2yg6XuHcnEC 12 | oYtr20mQ1QF4P4cSWYbO+13LSfOLwNxaAedtZ0ogzluFAgMBAAGjUzBRMB0GA1Ud 13 | DgQWBBQltkoPf4NXhJWNMdFNaxwsX1vs2TAfBgNVHSMEGDAWgBQltkoPf4NXhJWN 14 | MdFNaxwsX1vs2TAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQC7 15 | JgEwGkcfEfOawO5c6MN1FQ1M1QwD9DeIfmntuOEa13S7NDxaepcnpefPFCuidQey 16 | joq8h65NEFL33YSlN+Hmmdmm2CovLz0eqZ5h7mTysrhzM/utneyw30hcbleHgUsr 17 | VGSuEiPquwFz6fRAWVHZ2NQU/hISqeSZG3HTTcyxAxsgmGBWoZiWHfA248pbB1uK 18 | RzxzvHdTN1qSsJGU2WzzdVVYsgCy76VpqpajUTYRwKpbtgZBlJvnyh26r8f6k0pW 19 | XicVPGoVDmtJJsh7rz4YQu+7KxvnZ4PATVIq431/BykK80KTd/jPGfvJ+DdpsKXa 20 | XJAFTXsKGgQsGxFLf2q0 21 | -----END CERTIFICATE----- 22 | -------------------------------------------------------------------------------- /__test__/doh.test.js: -------------------------------------------------------------------------------- 1 | const DoH = require('../src/index').DoH 2 | 3 | describe('Test DNS-over-HTTPS client', () => { 4 | let doh = new DoH('google') 5 | let tests = [ 6 | { 7 | domainName: 'www.google.com', 8 | domainType: 'A' 9 | }, { 10 | domainName: 'www.google.com', 11 | domainType: 'AAAA' 12 | }, { 13 | domainName: 'www.google.com', 14 | domainType: 'CNAME' 15 | }, { 16 | domainName: 'www.google.com', 17 | domainType: 'DS' 18 | }, { 19 | domainName: 'www.google.com', 20 | domainType: 'DNSKEY' 21 | }, { 22 | domainName: 'gmail.com', 23 | domainType: 'MX' 24 | }, { 25 | domainName: 'www.google.com', 26 | domainType: 'NS' 27 | }, { 28 | domainName: 'www.google.com', 29 | domainType: 'NSEC' 30 | }, { 31 | domainName: 'www.google.com', 32 | domainType: 'NSEC3' 33 | }, { 34 | domainName: 'www.google.com', 35 | domainType: 'RRSIG' 36 | }, { 37 | domainName: 'www.google.com', 38 | domainType: 'SOA' 39 | }, { 40 | domainName: 'www.google.com', 41 | domainType: 'TXT' 42 | } 43 | // Seems cleanbrowsing doesn't support caa query 44 | // { 45 | // domainName: 'example.com', 46 | // domainType: 'CAA' 47 | // } 48 | ] 49 | 50 | test('initialize doh', () => { 51 | expect(doh.provider).toBe('google') 52 | doh.setProvider('cleanbrowsing') 53 | expect(doh.provider).toBe('cleanbrowsing') 54 | }) 55 | 56 | test('fetch dns over https through google, cloudflare ,cleanbrowsing and quad9', async (done) => { 57 | let totalTests = tests.length 58 | let isOk = true 59 | for (let i=0; i { 4 | let dot = new DoT('google', './__test__/key.pem', './__test__/certificate.pem') 5 | let tests = [ 6 | { 7 | domainName: 'www.google.com', 8 | domainType: 'A' 9 | }, { 10 | domainName: 'www.google.com', 11 | domainType: 'AAAA' 12 | }, { 13 | domainName: 'www.google.com', 14 | domainType: 'CNAME' 15 | }, { 16 | domainName: 'www.google.com', 17 | domainType: 'DS' 18 | }, { 19 | domainName: 'www.google.com', 20 | domainType: 'DNSKEY' 21 | }, { 22 | domainName: 'gmail.com', 23 | domainType: 'MX' 24 | }, { 25 | domainName: 'www.google.com', 26 | domainType: 'NS' 27 | }, { 28 | domainName: 'www.google.com', 29 | domainType: 'NSEC' 30 | }, { 31 | domainName: 'www.google.com', 32 | domainType: 'NSEC3' 33 | }, { 34 | domainName: 'www.google.com', 35 | domainType: 'RRSIG' 36 | }, { 37 | domainName: 'www.google.com', 38 | domainType: 'SOA' 39 | }, { 40 | domainName: 'www.google.com', 41 | domainType: 'TXT' 42 | } 43 | // Seems cleanbrowsing doesn't support caa query 44 | // { 45 | // domainName: 'example.com', 46 | // domainType: 'CAA' 47 | // } 48 | ] 49 | 50 | test('initialize dot', () => { 51 | expect(dot.provider).toBe('google') 52 | dot.setProvider('cleanbrowsing') 53 | expect(dot.provider).toBe('cleanbrowsing') 54 | }) 55 | 56 | test('fetch dns over tls through google, cloudflare, cleanbrowsing and quad9', async (done) => { 57 | let totalTests = tests.length 58 | let isOk = true 59 | for (let i=0; i { 4 | let tests = [ 5 | { 6 | domainType: 'A', 7 | value: 1 8 | }, { 9 | domainType: 'AAAA', 10 | value: 28 11 | }, { 12 | domainType: 'CNAME', 13 | value: 5 14 | }, { 15 | domainType: 'DS', 16 | value: 43 17 | }, { 18 | domainType: 'DNSKEY', 19 | value: 48 20 | }, { 21 | domainType: 'MX', 22 | value: 15 23 | }, { 24 | domainType: 'NS', 25 | value: 2 26 | }, { 27 | domainType: 'NSEC', 28 | value: 47 29 | }, { 30 | domainType: 'NSEC3', 31 | value: 50 32 | }, { 33 | domainType: 'RRSIG', 34 | value: 46 35 | }, { 36 | domainType: 'SOA', 37 | value: 6 38 | }, { 39 | domainType: 'TXT', 40 | value: 16 41 | }, { 42 | domainType: 'CAA', 43 | value: 257 44 | } 45 | ] 46 | 47 | test('test domain type', () => { 48 | let totalTests = tests.length 49 | for (let i=0; i", 17 | "license": "MIT", 18 | "dependencies": { 19 | "native-dns-packet": "^0.1.1", 20 | "xhr2": "^0.2.0" 21 | }, 22 | "devDependencies": { 23 | "@types/jest": "^24.0.15", 24 | "@types/node": "^14.0.6", 25 | "jest": "^24.8.0", 26 | "ts-jest": "^24.0.2", 27 | "typescript": "^3.5.3" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/doh.ts: -------------------------------------------------------------------------------- 1 | import * as XHR2 from 'xhr2' 2 | import * as Packet from 'native-dns-packet' 3 | import * as Util from './util' 4 | 5 | function DoH (provider: string) { 6 | Object.defineProperties(this, { 7 | providers: { 8 | value: { 9 | google: 'https://dns.google/dns-query', 10 | cloudflare: 'https://cloudflare-dns.com/dns-query', 11 | cleanbrowsing: 'https://doh.cleanbrowsing.org/doh/family-filter', 12 | quad9: 'https://dns9.quad9.net/dns-query' 13 | }, 14 | writable: false 15 | } 16 | }) 17 | if (typeof this.providers[provider] === 'undefined') { 18 | throw new Error('We only support these provider: google, cleanbrowsing, cloudflare') 19 | } 20 | this.provider = provider 21 | this.uri = this.providers[this.provider] 22 | } 23 | 24 | DoH.prototype.getProviders = function () { 25 | return Object.keys(this.providers) 26 | } 27 | 28 | DoH.prototype.setProvider = function (provider :string) { 29 | if (this.provider === provider) { 30 | return 31 | } 32 | if (typeof this.providers[provider] === 'undefined') { 33 | throw new Error('We only support these provider: ' + this.getProviders().join(', ')) 34 | } 35 | this.provider = provider 36 | this.uri = this.providers[this.provider] 37 | } 38 | 39 | DoH.prototype.resolve = function (domainName: string, domainType: string) { 40 | let type = Util.getDomainType(domainType) 41 | let dnsPacket = new Packet() 42 | let dnsBuf = Util.newBuffer(128) 43 | dnsPacket.question.push({ 44 | name: domainName, 45 | type: type, 46 | class: 1 47 | }) 48 | Packet.write(dnsBuf, dnsPacket) 49 | 50 | let provider = this.provider 51 | let query = `${this.uri}?dns=${dnsBuf.toString('base64').replace(/=+/, '')}` 52 | return new Promise(function (resolve, reject) { 53 | let xhr = new XHR2() 54 | xhr.open('GET', query, true) 55 | xhr.setRequestHeader('Accept', 'application/dns-message') 56 | xhr.setRequestHeader('Content-type', 'application/dns-message') 57 | xhr.responseType = 'arraybuffer' 58 | xhr.onload = function () { 59 | if (xhr.status === 200 && this.response) { 60 | try { 61 | let dnsResult = Buffer.from(this.response) 62 | let result = Packet.parse(dnsResult) 63 | resolve(result.answer) 64 | } catch (err) { 65 | reject(err) 66 | } 67 | } else { 68 | reject(new Error(`Cannot find the domain, provider: ${provider}, xhr status: ${xhr.status}.`)) 69 | } 70 | } 71 | xhr.onerror = function (err) { 72 | reject(err) 73 | } 74 | xhr.send(null) 75 | }) 76 | } 77 | 78 | module.exports = DoH -------------------------------------------------------------------------------- /src/dot.ts: -------------------------------------------------------------------------------- 1 | import * as tls from 'tls' 2 | import * as fs from 'fs' 3 | import * as Packet from 'native-dns-packet' 4 | import * as Util from './util' 5 | 6 | function DoT (provider: string, keyPath: string, certPath: string) { 7 | Object.defineProperties(this, { 8 | providers: { 9 | value: { 10 | google: 'dns.google', 11 | cloudflare: '1.1.1.1', 12 | cleanbrowsing: '185.228.169.154', 13 | quad9: 'dns.quad9.net' 14 | }, 15 | writable: false 16 | }, 17 | key: { 18 | writable: true 19 | }, 20 | cert: { 21 | writable: true 22 | }, 23 | }) 24 | if (typeof this.providers[provider] === 'undefined') { 25 | throw new Error('We only support these provider: google, cleanbrowsing, cloudflare') 26 | } 27 | const key = fs.readFileSync(keyPath) 28 | const cert = fs.readFileSync(certPath) 29 | this.provider = provider 30 | this.uri = this.providers[this.provider] 31 | this.key = key 32 | this.cert = cert 33 | } 34 | 35 | DoT.prototype.getProviders = function () { 36 | return Object.keys(this.providers) 37 | } 38 | 39 | DoT.prototype.setProvider = function (provider :string) { 40 | if (this.provider === provider) { 41 | return 42 | } 43 | if (typeof this.providers[provider] === 'undefined') { 44 | throw new Error('We only support these provider: ' + this.getProviders().join(', ')) 45 | } 46 | this.provider = provider 47 | this.uri = this.providers[this.provider] 48 | } 49 | 50 | // TODO: refactor socket and provider, multiple query and keepalive connection 51 | DoT.prototype.resolve = function (domainName: string, domainType: string) { 52 | let type = Util.getDomainType(domainType) 53 | let dnsPacket = new Packet() 54 | let dnsBuf = Util.newBuffer(128) 55 | let msgBuf = Util.newBuffer(130) 56 | dnsPacket.question.push({ 57 | name: domainName, 58 | type: type, 59 | class: 1 60 | }) 61 | Packet.write(dnsBuf, dnsPacket) 62 | msgBuf[1] = 128 63 | // copy dns buffer to message buffer 64 | dnsBuf.copy(msgBuf, 2, 0) 65 | 66 | const uri = this.uri 67 | const key = this.key 68 | const cert = this.cert 69 | return new Promise(function (resolve, reject) { 70 | const options = { 71 | key: key, 72 | cert: cert, 73 | // ca: [], 74 | checkServerIdentity: () => { 75 | return null 76 | }, 77 | } 78 | 79 | const socket = tls.connect(853, uri, options, () => { 80 | if (socket.authorized) { 81 | socket.write(msgBuf) 82 | } else { 83 | reject(new Error('socket authorized')) 84 | } 85 | }) 86 | socket.on('data', (data) => { 87 | let sign = data[0] & (1 << 7) 88 | let totalLength = (data & 0xFF) << 8 | data[1] & 0xFF 89 | if (sign) { 90 | totalLength = 0xFFFF0000 | totalLength 91 | } 92 | let result = Packet.parse(data.slice(2)) 93 | resolve(result.answer) 94 | }) 95 | socket.on('error', (err) => { 96 | reject(err) 97 | }) 98 | socket.on('end', () => { 99 | // finish query 100 | }) 101 | }) 102 | } 103 | 104 | module.exports = DoT -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | const DoH = require('./doh') 2 | const DoT = require('./dot') 3 | const Util = require('./util') 4 | module.exports.DoH = DoH 5 | module.exports.DoT = DoT 6 | module.exports.Util = Util -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | export function getDomainType (domainType: string) { 2 | let type: number = 0 3 | switch (domainType.toUpperCase()) { 4 | case 'A': 5 | type = 1 6 | break 7 | case 'AAAA': 8 | type = 28 9 | break 10 | case 'CAA': 11 | type = 257 12 | break 13 | case 'CNAME': 14 | type = 5 15 | break 16 | case 'DS': 17 | type = 43 18 | break 19 | case 'DNSKEY': 20 | type = 48 21 | break 22 | case 'MX': 23 | type = 15 24 | break 25 | case 'NS': 26 | type = 2 27 | break 28 | case 'NSEC': 29 | type = 47 30 | break 31 | case 'NSEC3': 32 | type = 50 33 | break 34 | case 'RRSIG': 35 | type = 46 36 | break 37 | case 'SOA': 38 | type = 6 39 | break 40 | case 'TXT': 41 | type = 16 42 | break 43 | default: 44 | // A 45 | type = 1 46 | break 47 | } 48 | return type 49 | } 50 | 51 | export function newBuffer (length: number) { 52 | let buf 53 | if (Buffer.alloc) { 54 | buf = Buffer.alloc(length) 55 | } else { 56 | buf = new Buffer(length) 57 | } 58 | return buf 59 | } 60 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["es6"], 4 | "outDir": "./dist", 5 | "types": [ 6 | "node" 7 | ] 8 | }, 9 | "include": ["src/*.ts"] 10 | } --------------------------------------------------------------------------------