├── .github ├── FUNDING.yml ├── dependabot.yaml └── workflows │ └── release.yml ├── .gitignore ├── .goreleaser.yml ├── LICENSE ├── Makefile ├── README.md ├── TODO.md ├── cli.Dockerfile ├── cmd └── doggo │ ├── cli.go │ ├── completions.go │ ├── help.go │ └── parse.go ├── config-api-sample.toml ├── docs ├── .gitignore ├── .vscode │ ├── extensions.json │ └── launch.json ├── README.md ├── astro.config.mjs ├── package.json ├── public │ └── favicon.svg ├── src │ ├── assets │ │ ├── custom.css │ │ └── doggo.png │ ├── content │ │ ├── config.ts │ │ └── docs │ │ │ ├── features │ │ │ ├── any.md │ │ │ ├── ip.md │ │ │ ├── multiple.md │ │ │ ├── output.md │ │ │ ├── reverse.md │ │ │ ├── shell.md │ │ │ └── tweaks.md │ │ │ ├── guide │ │ │ ├── examples.md │ │ │ └── reference.md │ │ │ ├── index.mdx │ │ │ ├── intro │ │ │ └── installation.md │ │ │ ├── resolvers │ │ │ ├── classic.md │ │ │ ├── dnscrypt.md │ │ │ ├── doh.md │ │ │ ├── dot.md │ │ │ ├── quic.md │ │ │ └── system.md │ │ │ └── usage.md │ └── env.d.ts ├── tsconfig.json └── yarn.lock ├── go.mod ├── go.sum ├── install.sh ├── internal └── app │ ├── app.go │ ├── globalping.go │ ├── nameservers.go │ ├── output.go │ └── questions.go ├── pkg ├── config │ ├── config.go │ ├── config_unix.go │ └── config_windows.go ├── models │ └── models.go ├── resolvers │ ├── classic.go │ ├── common.go │ ├── dnscrypt.go │ ├── doh.go │ ├── doq.go │ ├── resolver.go │ └── utils.go └── utils │ └── logger.go ├── web.Dockerfile ├── web ├── api.go ├── assets │ ├── main.js │ └── style.css ├── config.go ├── handlers.go └── index.html └── www ├── api └── api.md └── static ├── doggo.png └── help.png /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [mr-karan] 2 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | open-pull-requests-limit: 10 8 | - package-ecosystem: "github-actions" 9 | directory: "/" 10 | schedule: 11 | interval: "monthly" 12 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: goreleaser 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*" 7 | 8 | env: 9 | REGISTRY: ghcr.io 10 | 11 | jobs: 12 | goreleaser: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | - name: Set up Go 20 | uses: actions/setup-go@v5 21 | with: 22 | go-version: "1.22" 23 | - name: Log in to the Container registry 24 | uses: docker/login-action@v3 25 | with: 26 | registry: ${{ env.REGISTRY }} 27 | username: ${{ github.repository_owner }} 28 | password: ${{ secrets.GITHUB_TOKEN }} 29 | 30 | - name: Cache node modules 31 | uses: actions/cache@v4 32 | with: 33 | path: ./docs/node_modules 34 | key: ${{ runner.os }}-npm-${{ hashFiles('**/docs/package-lock.json') }} 35 | restore-keys: | 36 | ${{ runner.os }}-npm- 37 | 38 | - name: Install dependencies 39 | run: | 40 | cd docs 41 | yarn install 42 | 43 | - name: Build 44 | run: | 45 | cd docs 46 | yarn build 47 | 48 | - name: Run GoReleaser 49 | uses: goreleaser/goreleaser-action@v6 50 | with: 51 | version: latest 52 | args: release --clean 53 | env: 54 | DOCKER_CLI_EXPERIMENTAL: enabled 55 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | dist/ 8 | 9 | # Test binary, built with `go test -c` 10 | *.test 11 | 12 | # Output of the go coverage tool, specifically when used with LiteIDE 13 | *.out 14 | 15 | # Dependency directories (remove the comment below to include it) 16 | vendor/ 17 | 18 | # App Specific 19 | .env 20 | config.yml 21 | .DS_Store 22 | bin/* 23 | node_modules -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | env: 4 | - GO111MODULE=on 5 | - CGO_ENABLED=0 6 | 7 | before: 8 | hooks: 9 | - go mod tidy 10 | 11 | builds: 12 | - binary: doggo 13 | id: cli 14 | ldflags: 15 | - -s -w -X "main.buildVersion={{ .Tag }} ({{ .ShortCommit }} {{ .Date }})" 16 | main: ./cmd/doggo/ 17 | goos: 18 | - linux 19 | - darwin 20 | - windows 21 | - freebsd 22 | - openbsd 23 | - netbsd 24 | goarch: 25 | - amd64 26 | - arm64 27 | - "386" 28 | - arm 29 | goarm: 30 | - "7" 31 | ignore: 32 | - goos: windows 33 | goarch: arm64 34 | - goos: windows 35 | goarm: "7" 36 | 37 | - binary: doggo-web.bin 38 | id: web 39 | goos: 40 | - linux 41 | goarch: 42 | - amd64 43 | ldflags: 44 | - -s -w -X "main.buildVersion={{ .Tag }} ({{ .ShortCommit }} {{ .Date }})" 45 | main: ./web/ 46 | hooks: 47 | pre: sh -c 'cd ./docs/ && yarn && yarn build' 48 | 49 | archives: 50 | - id: cli 51 | builds: 52 | - cli 53 | format_overrides: 54 | - goos: windows 55 | format: zip 56 | name_template: >- 57 | {{ .ProjectName }}_ 58 | {{- .Version }}_ 59 | {{- title .Os }}_ 60 | {{- if eq .Arch "amd64" }}x86_64 61 | {{- else if eq .Arch "386" }}i386 62 | {{- else }}{{ .Arch }}{{ end }} 63 | wrap_in_directory: true 64 | files: 65 | - README.md 66 | - LICENSE 67 | 68 | - id: web 69 | builds: 70 | - web 71 | wrap_in_directory: true 72 | files: 73 | - README.md 74 | - LICENSE 75 | - config-api-sample.toml 76 | - src: "docs/dist/*" 77 | dst: "docs" 78 | name_template: "{{ .ProjectName }}_web_{{ .Version }}_{{ .Os }}_{{ .Arch }}" 79 | 80 | changelog: 81 | sort: asc 82 | use: github 83 | filters: 84 | exclude: 85 | - "^docs:" 86 | - "^test:" 87 | groups: 88 | - title: "New Features" 89 | regexp: "^.*feat[(\\w)]*:+.*$" 90 | order: 0 91 | - title: "Bug fixes" 92 | regexp: "^.*fix[(\\w)]*:+.*$" 93 | order: 10 94 | - title: Others 95 | order: 999 96 | 97 | dockers: 98 | - image_templates: 99 | - "ghcr.io/mr-karan/doggo:{{ .Tag }}" 100 | - "ghcr.io/mr-karan/doggo:latest" 101 | goarch: amd64 102 | ids: 103 | - cli 104 | dockerfile: cli.Dockerfile 105 | use: buildx 106 | build_flag_templates: 107 | - "--build-arg" 108 | - "ARCH=amd64" 109 | - --platform=linux/amd64 110 | - --label=org.opencontainers.image.title={{ .ProjectName }} 111 | - --label=org.opencontainers.image.source={{ .GitURL }} 112 | - --label=org.opencontainers.image.version=v{{ .Version }} 113 | - --label=org.opencontainers.image.created={{ .Date }} 114 | - --label=org.opencontainers.image.revision={{ .FullCommit }} 115 | 116 | - image_templates: 117 | - "ghcr.io/mr-karan/doggo:{{ .Tag }}-arm64v8" 118 | - "ghcr.io/mr-karan/doggo:latest-arm64v8" 119 | ids: 120 | - cli 121 | goarch: arm64 122 | dockerfile: cli.Dockerfile 123 | build_flag_templates: 124 | - "--build-arg" 125 | - "ARCH=arm64v8" 126 | - --platform=linux/arm64 127 | - --label=org.opencontainers.image.title={{ .ProjectName }} 128 | - --label=org.opencontainers.image.source={{ .GitURL }} 129 | - --label=org.opencontainers.image.version=v{{ .Version }} 130 | - --label=org.opencontainers.image.created={{ .Date }} 131 | - --label=org.opencontainers.image.revision={{ .FullCommit }} 132 | 133 | - image_templates: 134 | - "ghcr.io/mr-karan/doggo-web:{{ .Tag }}" 135 | - "ghcr.io/mr-karan/doggo-web:latest" 136 | ids: 137 | - web 138 | goarch: amd64 139 | dockerfile: web.Dockerfile 140 | use: buildx 141 | extra_files: 142 | - config-api-sample.toml 143 | - docs/dist/ 144 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CLI_BIN := ./bin/doggo.bin 2 | WEB_BIN := ./bin/doggo-web.bin 3 | 4 | HASH := $(shell git rev-parse --short HEAD) 5 | BUILD_DATE := $(shell date '+%Y-%m-%d %H:%M:%S') 6 | VERSION := ${HASH} 7 | 8 | .PHONY: build-cli 9 | build-cli: 10 | go build -o ${CLI_BIN} -ldflags="-X 'main.buildVersion=${VERSION}' -X 'main.buildDate=${BUILD_DATE}'" ./cmd/doggo/ 11 | 12 | .PHONY: build-web 13 | build-web: 14 | go build -o ${WEB_BIN} -ldflags="-X 'main.buildVersion=${VERSION}' -X 'main.buildDate=${BUILD_DATE}'" ./web/ 15 | 16 | .PHONY: run-cli 17 | run-cli: build-cli ## Build and Execute the CLI binary after the build step. 18 | ${CLI_BIN} 19 | 20 | .PHONY: run-web 21 | run-web: build-web ## Build and Execute the API binary after the build step. 22 | ${WEB_BIN} --config config-api-sample.toml 23 | 24 | .PHONY: clean 25 | clean: 26 | go clean 27 | - rm -rf ./bin/ 28 | 29 | .PHONY: lint 30 | lint: 31 | golangci-lint run 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |
3 |

4 |

doggo

5 |

6 | 🐶 Command-line DNS client for humans 7 |
8 |

9 |

10 | Web Interface 11 | · 12 | Documentation 13 |

14 | doggo CLI usage 15 |

16 | 17 | --- 18 | 19 | **doggo** is a modern command-line DNS client (like _dig_) written in Golang. It outputs information in a neat concise manner and supports protocols like DoH, DoT, DoQ, and DNSCrypt as well. 20 | 21 | It's totally inspired by [dog](https://github.com/ogham/dog/) which is written in Rust. I wanted to add some features to it but since I don't know Rust, I found it as a nice opportunity to experiment with writing a DNS Client from scratch in `Go` myself. Hence the name `dog` + `go` => **doggo**. 22 | 23 | ## Installation 24 | 25 | ### Easy Install (Recommended) 26 | 27 | ```shell 28 | curl -sS https://raw.githubusercontent.com/mr-karan/doggo/main/install.sh | sh 29 | ``` 30 | 31 | ### Package Managers 32 | 33 | - Homebrew: `brew install doggo` 34 | - MacPorts (macOS): `port install doggo` 35 | - Arch Linux: `yay -S doggo-bin` 36 | - Scoop (Windows): `scoop install doggo` 37 | - Eget: `eget mr-karan/doggo` 38 | 39 | ### Binary Install 40 | 41 | You can download pre-compiled binaries for various operating systems and architectures from the [Releases](https://github.com/mr-karan/doggo/releases) page. 42 | 43 | ### Go Install 44 | 45 | If you have Go installed on your system, you can use the `go install` command: 46 | 47 | ```shell 48 | go install github.com/mr-karan/doggo/cmd/doggo@latest 49 | ``` 50 | 51 | The binary will be available at `$GOPATH/bin/doggo`. 52 | 53 | ### Docker 54 | 55 | ```shell 56 | docker pull ghcr.io/mr-karan/doggo:latest 57 | docker run --rm ghcr.io/mr-karan/doggo:latest example.com 58 | ``` 59 | 60 | For more installation options, including binary downloads and Docker images, please refer to the [full installation guide](https://doggo.mrkaran.dev/docs/intro/installation/). 61 | 62 | ## Quick Start 63 | 64 | Here are some quick examples to get you started with doggo: 65 | 66 | ```shell 67 | # Simple DNS lookup 68 | doggo example.com 69 | 70 | # Query MX records using a specific nameserver 71 | doggo MX github.com @9.9.9.9 72 | 73 | # Use DNS over HTTPS 74 | doggo example.com @https://cloudflare-dns.com/dns-query 75 | 76 | # JSON output for scripting 77 | doggo example.com --json | jq '.responses[0].answers[].address' 78 | 79 | # Reverse DNS lookup 80 | doggo --reverse 8.8.8.8 --short 81 | 82 | # Using Globalping 83 | doggo example.com --gp-from Germany,Japan --gp-limit 2 84 | ``` 85 | 86 | ## Features 87 | 88 | - Human-readable output with color-coded and tabular format 89 | - JSON output support for easy scripting and parsing 90 | - Multiple transport protocols: DoH, DoT, DoQ, TCP, UDP, DNSCrypt 91 | - Support for `ndots` and `search` configurations 92 | - Multiple resolver support with customizable query strategies 93 | - IPv4 and IPv6 support 94 | - Web interface available 95 | - Shell completions for `zsh` and `fish` 96 | - Reverse DNS lookups 97 | - Flexible query options including various DNS flags 98 | - Debug mode for troubleshooting 99 | - Response time measurement 100 | - Cross-platform support 101 | 102 | ## Documentation 103 | 104 | For comprehensive documentation, including detailed usage instructions, configuration options, and advanced features, please visit our [official documentation site](https://doggo.mrkaran.dev/docs/). 105 | 106 | ## Sponsorship 107 | 108 | If you find doggo useful and would like to support its development, please consider becoming a sponsor. Your support helps maintain and improve this open-source project. 109 | 110 | [![GitHub Sponsors](https://img.shields.io/github/sponsors/mr-karan?style=for-the-badge&logo=github)](https://github.com/sponsors/mr-karan) 111 | 112 | Every contribution, no matter how small, is greatly appreciated and helps keep this project alive and growing. Thank you for your support! 🐶❤️ 113 | 114 | ## License 115 | 116 | This project is licensed under the [MIT License](./LICENSE). 117 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # todo 2 | 3 | ## Resolver 4 | - [x] Create a DNS Resolver struct 5 | - [x] Add methods to initialise the config, set defaults 6 | - [x] Add a resolve method 7 | - [x] Make it separate from Hub 8 | - [x] Parse output into separate fields 9 | - [x] Test IPv6/IPv4 only options 10 | - [x] Add DOH support 11 | - [x] Add DOT support 12 | - [x] Add DNS protocol on TCP mode support. 13 | - [x] Change lookup method. 14 | - [x] Major records supported 15 | - [x] Support multiple resolvers 16 | - [x] Take multiple transport options and initialise resolvers accordingly. 17 | - [x] Add timeout support 18 | - [x] Support SOA/NXDOMAIN 19 | 20 | ## CLI Features 21 | - [x] `ndots` support 22 | - [x] `search list` support 23 | - [x] JSON output 24 | - [x] Colorized output 25 | - [x] Table output 26 | - [x] Parsing options free-form 27 | 28 | ## CLI Grunt 29 | - [x] Query args 30 | - [x] Neatly package them to load args in different functions 31 | - [x] Upper case is not mandatory for query type/classes 32 | - [x] Output 33 | - [x] Custom Help Text 34 | - [x] Add examples 35 | - [x] Colorize 36 | - [x] Add different commands 37 | - [x] Add client transport options 38 | - [x] Fix an issue while loading free form args, where the same records are being added twice 39 | - [x] Remove urfave/cli in favour of `pflag + koanf` 40 | - [x] Flags - Remove unneeded ones 41 | 42 | ## Documentation 43 | - [x] README 44 | - [x] Usage 45 | - [x] Installation 46 | - [x] Features 47 | 48 | 49 | ## Release Checklist 50 | - [x] Goreleaser 51 | - [x] Snap 52 | - [x] Docker 53 | --- 54 | # Future Release 55 | 56 | - [x] Support obscure protocol tweaks in `dig` 57 | - [x] Support more DNS Record Types 58 | - [x] Shell completions 59 | - [x] zsh 60 | - [x] fish 61 | - [x] Homebrew - Goreleaser 62 | - [x] Add `dig +short` short output 63 | - [x] Add `--strategy` for picking nameservers. 64 | - [x] Separate Authority/Answer in JSON output. 65 | - [x] Error on NXDomain (Related upstream [bug](https://github.com/miekg/dns/issues/1198)) 66 | - [x] Reverse Lookup (dig -x) 67 | - [x] Shell completion proper 68 | - [x] CLI docs 69 | - [x] Merge those as misc docs or usage 70 | - [x] Example Guide 71 | 72 | - [ ] Add tests for Resolvers. 73 | - [ ] Add tests for CLI Output. 74 | - [ ] Add support for `dig +trace` like functionality. 75 | - [ ] Explore `dig.rc` kinda file 76 | -------------------------------------------------------------------------------- /cli.Dockerfile: -------------------------------------------------------------------------------- 1 | # Dockerfile 2 | ARG ARCH 3 | FROM ${ARCH}/alpine 4 | COPY doggo /usr/bin/doggo 5 | ENTRYPOINT ["/usr/bin/doggo"] -------------------------------------------------------------------------------- /cmd/doggo/cli.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "log/slog" 8 | "math" 9 | "os" 10 | "sync" 11 | "time" 12 | 13 | "github.com/jsdelivr/globalping-cli/globalping" 14 | "github.com/knadh/koanf/providers/posflag" 15 | "github.com/knadh/koanf/v2" 16 | "github.com/mr-karan/doggo/internal/app" 17 | "github.com/mr-karan/doggo/pkg/resolvers" 18 | "github.com/mr-karan/doggo/pkg/utils" 19 | flag "github.com/spf13/pflag" 20 | ) 21 | 22 | var ( 23 | buildVersion = "unknown" 24 | buildDate = "unknown" 25 | k = koanf.New(".") 26 | ) 27 | 28 | func main() { 29 | if len(os.Args) > 1 && os.Args[1] == "completions" { 30 | completionsCommand() 31 | return 32 | } 33 | 34 | cfg, err := loadConfig() 35 | if err != nil { 36 | fmt.Printf("Error loading configuration: %v\n", err) 37 | os.Exit(1) 38 | } 39 | 40 | if cfg.showVersion { 41 | fmt.Printf("%s - %s\n", buildVersion, buildDate) 42 | os.Exit(0) 43 | } 44 | 45 | logger := utils.InitLogger(cfg.debug) 46 | app := initializeApp(logger, cfg) 47 | 48 | if app.QueryFlags.GPFrom != "" { 49 | res, err := app.GlobalpingMeasurement() 50 | if err != nil { 51 | logger.Error("Error fetching globalping measurement", "error", err) 52 | os.Exit(2) 53 | } 54 | if app.QueryFlags.ShowJSON { 55 | err = app.OutputGlobalpingJSON(res) 56 | } else if app.QueryFlags.ShortOutput { 57 | err = app.OutputGlobalpingShort(res) 58 | } else { 59 | err = app.OutputGlobalping(res) 60 | } 61 | if err != nil { 62 | logger.Error("Error outputting globalping measurement", "error", err) 63 | os.Exit(2) 64 | } 65 | return 66 | } 67 | 68 | if cfg.reverseLookup { 69 | app.ReverseLookup() 70 | } 71 | 72 | app.LoadFallbacks() 73 | app.PrepareQuestions() 74 | 75 | if err := app.LoadNameservers(); err != nil { 76 | logger.Error("Error loading nameservers", "error", err) 77 | os.Exit(2) 78 | } 79 | 80 | resolvers, err := loadResolvers(app, cfg) 81 | if err != nil { 82 | logger.Error("Error loading resolvers", "error", err) 83 | os.Exit(2) 84 | } 85 | app.Resolvers = resolvers 86 | 87 | if len(app.QueryFlags.QNames) == 0 { 88 | cfg.flagSet.Usage() 89 | os.Exit(0) 90 | } 91 | 92 | responses, errors := performLookup(app, cfg) 93 | outputResults(app, responses, errors) 94 | } 95 | 96 | type config struct { 97 | flagSet *flag.FlagSet 98 | showVersion bool 99 | debug bool 100 | reverseLookup bool 101 | timeout time.Duration 102 | queryFlags resolvers.QueryFlags 103 | outputJSON bool 104 | showTime bool 105 | useColor bool 106 | } 107 | 108 | func loadConfig() (*config, error) { 109 | cfg := &config{} 110 | cfg.flagSet = setupFlags() 111 | 112 | if err := parseAndLoadFlags(cfg.flagSet); err != nil { 113 | return nil, fmt.Errorf("error parsing or loading flags: %w", err) 114 | } 115 | 116 | cfg.showVersion = k.Bool("version") 117 | cfg.debug = k.Bool("debug") 118 | cfg.reverseLookup = k.Bool("reverse") 119 | cfg.timeout = k.Duration("timeout") 120 | cfg.outputJSON = k.Bool("json") 121 | cfg.showTime = k.Bool("time") 122 | cfg.useColor = k.Bool("color") 123 | 124 | cfg.queryFlags = resolvers.QueryFlags{ 125 | AA: k.Bool("aa"), 126 | AD: k.Bool("ad"), 127 | CD: k.Bool("cd"), 128 | RD: k.Bool("rd"), 129 | Z: k.Bool("z"), 130 | DO: k.Bool("do"), 131 | } 132 | 133 | return cfg, nil 134 | } 135 | 136 | func setupFlags() *flag.FlagSet { 137 | f := flag.NewFlagSet("config", flag.ContinueOnError) 138 | f.Usage = renderCustomHelp 139 | 140 | f.StringSliceP("query", "q", []string{}, "Domain name to query") 141 | f.StringSliceP("type", "t", []string{}, "Type of DNS record to be queried (A, AAAA, MX etc)") 142 | f.StringSliceP("class", "c", []string{}, "Network class of the DNS record to be queried (IN, CH, HS etc)") 143 | f.StringSliceP("nameserver", "n", []string{}, "Address of the nameserver to send packets to") 144 | f.BoolP("reverse", "x", false, "Performs a DNS Lookup for an IPv4 or IPv6 address") 145 | 146 | f.String("gp-from", "", "Probe locations as a comma-separated list") 147 | f.Int("gp-limit", 1, "Limit the number of probes to use") 148 | 149 | f.DurationP("timeout", "T", 5*time.Second, "Sets the timeout for a query") 150 | f.Bool("search", true, "Use the search list provided in resolv.conf") 151 | f.Int("ndots", -1, "Specify the ndots parameter") 152 | f.BoolP("ipv4", "4", false, "Use IPv4 only") 153 | f.BoolP("ipv6", "6", false, "Use IPv6 only") 154 | f.String("strategy", "all", "Strategy to query nameservers in resolv.conf file") 155 | f.String("tls-hostname", "", "Hostname for certificate verification") 156 | f.Bool("skip-hostname-verification", false, "Skip TLS Hostname Verification") 157 | 158 | f.Bool("any", false, "Query all supported DNS record types") 159 | 160 | f.BoolP("json", "J", false, "Set the output format as JSON") 161 | f.Bool("short", false, "Short output format") 162 | f.Bool("time", false, "Display how long the response took") 163 | f.Bool("color", true, "Show colored output") 164 | f.Bool("debug", false, "Enable debug mode") 165 | 166 | // Add flags for DNS query options 167 | f.Bool("aa", false, "Set Authoritative Answer flag") 168 | f.Bool("ad", false, "Set Authenticated Data flag") 169 | f.Bool("cd", false, "Set Checking Disabled flag") 170 | f.Bool("rd", true, "Set Recursion Desired flag (default: true)") 171 | f.Bool("z", false, "Set Z flag (reserved for future use)") 172 | f.Bool("do", false, "Set DNSSEC OK flag") 173 | 174 | f.Bool("version", false, "Show version of doggo") 175 | 176 | return f 177 | } 178 | 179 | func parseAndLoadFlags(f *flag.FlagSet) error { 180 | if err := f.Parse(os.Args[1:]); err != nil { 181 | return fmt.Errorf("error parsing flags: %w", err) 182 | } 183 | if err := k.Load(posflag.Provider(f, ".", k), nil); err != nil { 184 | return fmt.Errorf("error loading flags: %w", err) 185 | } 186 | return nil 187 | } 188 | 189 | func initializeApp(logger *slog.Logger, cfg *config) *app.App { 190 | gpConfig := globalping.Config{ 191 | APIURL: "https://api.globalping.io/v1", 192 | AuthURL: "https://auth.globalping.io", 193 | UserAgent: fmt.Sprintf("doggo/%s (https://github.com/mr-karan/doggo)", buildVersion), 194 | } 195 | gpToken := os.Getenv("GLOBALPING_TOKEN") 196 | if gpToken != "" { 197 | gpConfig.AuthToken = &globalping.Token{ 198 | AccessToken: gpToken, 199 | Expiry: time.Now().Add(math.MaxInt64), 200 | } 201 | } 202 | globlpingClient := globalping.NewClient(gpConfig) 203 | 204 | app := app.New(logger, globlpingClient, buildVersion) 205 | 206 | if err := k.Unmarshal("", &app.QueryFlags); err != nil { 207 | logger.Error("Error loading args", "error", err) 208 | os.Exit(2) 209 | } 210 | 211 | loadNameservers(&app, cfg.flagSet.Args()) 212 | return &app 213 | } 214 | 215 | func loadNameservers(app *app.App, args []string) { 216 | flagNameservers := k.Strings("nameserver") 217 | unparsedNameservers, qt, qc, qn := loadUnparsedArgs(args) 218 | 219 | if len(flagNameservers) > 0 { 220 | app.QueryFlags.Nameservers = flagNameservers 221 | } else { 222 | app.QueryFlags.Nameservers = unparsedNameservers 223 | } 224 | 225 | app.QueryFlags.QTypes = append(app.QueryFlags.QTypes, qt...) 226 | app.QueryFlags.QClasses = append(app.QueryFlags.QClasses, qc...) 227 | app.QueryFlags.QNames = append(app.QueryFlags.QNames, qn...) 228 | } 229 | 230 | func loadResolvers(app *app.App, cfg *config) ([]resolvers.Resolver, error) { 231 | return resolvers.LoadResolvers(resolvers.Options{ 232 | Nameservers: app.Nameservers, 233 | UseIPv4: app.QueryFlags.UseIPv4, 234 | UseIPv6: app.QueryFlags.UseIPv6, 235 | SearchList: app.ResolverOpts.SearchList, 236 | Ndots: app.ResolverOpts.Ndots, 237 | Timeout: cfg.timeout, 238 | Logger: app.Logger, 239 | Strategy: app.QueryFlags.Strategy, 240 | InsecureSkipVerify: app.QueryFlags.InsecureSkipVerify, 241 | TLSHostname: app.QueryFlags.TLSHostname, 242 | }) 243 | } 244 | 245 | func performLookup(app *app.App, cfg *config) ([]resolvers.Response, []error) { 246 | ctx, cancel := context.WithTimeout(context.Background(), cfg.timeout) 247 | defer cancel() 248 | 249 | var ( 250 | wg sync.WaitGroup 251 | mu sync.Mutex 252 | allResponses []resolvers.Response 253 | allErrors []error 254 | ) 255 | 256 | for _, resolver := range app.Resolvers { 257 | wg.Add(1) 258 | go func(r resolvers.Resolver) { 259 | defer wg.Done() 260 | responses, err := r.Lookup(ctx, app.Questions, cfg.queryFlags) 261 | mu.Lock() 262 | if err != nil { 263 | allErrors = append(allErrors, err) 264 | } else { 265 | allResponses = append(allResponses, responses...) 266 | } 267 | mu.Unlock() 268 | }(resolver) 269 | } 270 | 271 | wg.Wait() 272 | return allResponses, allErrors 273 | } 274 | 275 | func outputResults(app *app.App, responses []resolvers.Response, responseErrors []error) { 276 | if app.QueryFlags.ShowJSON { 277 | outputJSON(app.Logger, responses, responseErrors) 278 | } else { 279 | if len(responseErrors) > 0 { 280 | app.Logger.Error("Error looking up DNS records", "error", responseErrors[0]) 281 | os.Exit(9) 282 | } 283 | app.Output(responses) 284 | } 285 | } 286 | 287 | func outputJSON(logger *slog.Logger, responses []resolvers.Response, responseErrors []error) { 288 | jsonOutput := struct { 289 | Responses []resolvers.Response `json:"responses,omitempty"` 290 | Error string `json:"error,omitempty"` 291 | }{ 292 | Responses: responses, 293 | } 294 | 295 | if len(responseErrors) > 0 { 296 | jsonOutput.Error = responseErrors[0].Error() 297 | } 298 | 299 | jsonData, err := json.MarshalIndent(jsonOutput, "", " ") 300 | if err != nil { 301 | logger.Error("Error marshaling JSON") 302 | os.Exit(1) 303 | } 304 | fmt.Println(string(jsonData)) 305 | } 306 | -------------------------------------------------------------------------------- /cmd/doggo/completions.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | ) 7 | 8 | var ( 9 | bashCompletion = ` 10 | _doggo() { 11 | local cur prev opts 12 | COMPREPLY=() 13 | cur="${COMP_WORDS[COMP_CWORD]}" 14 | prev="${COMP_WORDS[COMP_CWORD-1]}" 15 | 16 | opts="-v --version -h --help -q --query -t --type -n --nameserver -c --class -r --reverse --strategy --ndots --search --timeout -4 --ipv4 -6 --ipv6 --tls-hostname --skip-hostname-verification -J --json --short --color --debug --time --gp-from --gp-limit" 17 | 18 | case "${prev}" in 19 | -t|--type) 20 | COMPREPLY=( $(compgen -W "A AAAA CAA CNAME HINFO MX NS PTR SOA SRV TXT" -- ${cur}) ) 21 | return 0 22 | ;; 23 | -c|--class) 24 | COMPREPLY=( $(compgen -W "IN CH HS" -- ${cur}) ) 25 | return 0 26 | ;; 27 | -n|--nameserver) 28 | COMPREPLY=( $(compgen -A hostname -- ${cur}) ) 29 | return 0 30 | ;; 31 | --strategy) 32 | COMPREPLY=( $(compgen -W "all random first" -- ${cur}) ) 33 | return 0 34 | ;; 35 | --search|--color) 36 | COMPREPLY=( $(compgen -W "true false" -- ${cur}) ) 37 | return 0 38 | ;; 39 | esac 40 | 41 | if [[ ${cur} == -* ]]; then 42 | COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) 43 | else 44 | COMPREPLY=( $(compgen -A hostname -- ${cur}) ) 45 | fi 46 | } 47 | 48 | complete -F _doggo doggo 49 | ` 50 | 51 | zshCompletion = `#compdef doggo 52 | 53 | _doggo() { 54 | local -a commands 55 | commands=( 56 | 'completions:Generate shell completion scripts' 57 | ) 58 | 59 | _arguments -C \ 60 | '(-v --version)'{-v,--version}'[Show version of doggo]' \ 61 | '(-h --help)'{-h,--help}'[Show list of command-line options]' \ 62 | '(-q --query)'{-q,--query}'[Hostname to query the DNS records for]:hostname:_hosts' \ 63 | '(-t --type)'{-t,--type}'[Type of the DNS Record]:record type:(A AAAA CAA CNAME HINFO MX NS PTR SOA SRV TXT)' \ 64 | '(-n --nameserver)'{-n,--nameserver}'[Address of a specific nameserver to send queries to]:nameserver:_hosts' \ 65 | '(-c --class)'{-c,--class}'[Network class of the DNS record being queried]:network class:(IN CH HS)' \ 66 | '(-r --reverse)'{-r,--reverse}'[Performs a DNS Lookup for an IPv4 or IPv6 address]' \ 67 | '--strategy[Strategy to query nameserver listed in etc/resolv.conf]:strategy:(all random first)' \ 68 | '--ndots[Number of required dots in hostname to assume FQDN]:number of dots' \ 69 | '--search[Use the search list defined in resolv.conf]:setting:(true false)' \ 70 | '--timeout[Timeout (in seconds) for the resolver to return a response]:seconds' \ 71 | '(-4 --ipv4)'{-4,--ipv4}'[Use IPv4 only]' \ 72 | '(-6 --ipv6)'{-6,--ipv6}'[Use IPv6 only]' \ 73 | '--tls-hostname[Hostname used for verification of certificate incase the provided DoT nameserver is an IP]:hostname:_hosts' \ 74 | '--skip-hostname-verification[Skip TLS hostname verification in case of DoT lookups]' \ 75 | '(-J --json)'{-J,--json}'[Format the output as JSON]' \ 76 | '--short[Shows only the response section in the output]' \ 77 | '--color[Colored output]:setting:(true false)' \ 78 | '--debug[Enable debug logging]' \ 79 | '--time[Shows how long the response took from the server]' \ 80 | '--gp-from[Query using Globalping API from a specific location]' \ 81 | '--gp-limit[Limit the number of probes to use from Globalping]' \ 82 | '*:hostname:_hosts' \ 83 | && ret=0 84 | 85 | case $state in 86 | (commands) 87 | _describe -t commands 'doggo commands' commands && ret=0 88 | ;; 89 | esac 90 | 91 | return ret 92 | } 93 | 94 | _doggo 95 | ` 96 | 97 | fishCompletion = ` 98 | function __fish_doggo_no_subcommand 99 | set cmd (commandline -opc) 100 | if [ (count $cmd) -eq 1 ] 101 | return 0 102 | end 103 | return 1 104 | end 105 | 106 | # Meta options 107 | complete -c doggo -n '__fish_doggo_no_subcommand' -l 'version' -d "Show version of doggo" 108 | complete -c doggo -n '__fish_doggo_no_subcommand' -l 'help' -d "Show list of command-line options" 109 | 110 | # Query options 111 | complete -c doggo -n '__fish_doggo_no_subcommand' -s 'q' -l 'query' -d "Hostname to query the DNS records for" -x -a "(__fish_print_hostnames)" 112 | complete -c doggo -n '__fish_doggo_no_subcommand' -s 't' -l 'type' -d "Type of the DNS Record" -x -a "A AAAA CAA CNAME HINFO MX NS PTR SOA SRV TXT" 113 | complete -c doggo -n '__fish_doggo_no_subcommand' -s 'n' -l 'nameserver' -d "Address of a specific nameserver to send queries to" -x -a "(__fish_print_hostnames)" 114 | complete -c doggo -n '__fish_doggo_no_subcommand' -s 'c' -l 'class' -d "Network class of the DNS record being queried" -x -a "IN CH HS" 115 | complete -c doggo -n '__fish_doggo_no_subcommand' -s 'r' -l 'reverse' -d "Performs a DNS Lookup for an IPv4 or IPv6 address" 116 | 117 | # Resolver options 118 | complete -c doggo -n '__fish_doggo_no_subcommand' -l 'strategy' -d "Strategy to query nameserver listed in etc/resolv.conf" -x -a "all random first" 119 | complete -c doggo -n '__fish_doggo_no_subcommand' -l 'ndots' -d "Specify ndots parameter" 120 | complete -c doggo -n '__fish_doggo_no_subcommand' -l 'search' -d "Use the search list defined in resolv.conf" -x -a "true false" 121 | complete -c doggo -n '__fish_doggo_no_subcommand' -l 'timeout' -d "Specify timeout (in seconds) for the resolver to return a response" 122 | complete -c doggo -n '__fish_doggo_no_subcommand' -s '4' -l 'ipv4' -d "Use IPv4 only" 123 | complete -c doggo -n '__fish_doggo_no_subcommand' -s '6' -l 'ipv6' -d "Use IPv6 only" 124 | 125 | # Output options 126 | complete -c doggo -n '__fish_doggo_no_subcommand' -s 'J' -l 'json' -d "Format the output as JSON" 127 | complete -c doggo -n '__fish_doggo_no_subcommand' -l 'short' -d "Shows only the response section in the output" 128 | complete -c doggo -n '__fish_doggo_no_subcommand' -l 'color' -d "Colored output" -x -a "true false" 129 | complete -c doggo -n '__fish_doggo_no_subcommand' -l 'debug' -d "Enable debug logging" 130 | complete -c doggo -n '__fish_doggo_no_subcommand' -l 'time' -d "Shows how long the response took from the server" 131 | 132 | # TLS options 133 | complete -c doggo -n '__fish_doggo_no_subcommand' -l 'tls-hostname' -d "Hostname for certificate verification" -x -a "(__fish_print_hostnames)" 134 | complete -c doggo -n '__fish_doggo_no_subcommand' -l 'skip-hostname-verification' -d "Skip TLS hostname verification in case of DoT lookups" 135 | 136 | # Globalping options 137 | complete -c doggo -n '__fish_doggo_no_subcommand' -l 'gp-from' -d "Query using Globalping API from a specific location" 138 | complete -c doggo -n '__fish_doggo_no_subcommand' -l 'gp-limit' -d "Limit the number of probes to use from Globalping" 139 | 140 | # Completions command 141 | complete -c doggo -n '__fish_doggo_no_subcommand' -a completions -d "Generate shell completion scripts" 142 | complete -c doggo -n '__fish_seen_subcommand_from completions' -a "bash zsh fish" -d "Shell type" 143 | ` 144 | ) 145 | 146 | func completionsCommand() { 147 | if len(os.Args) < 3 { 148 | fmt.Println("Usage: doggo completions [bash|zsh|fish]") 149 | os.Exit(1) 150 | } 151 | 152 | shell := os.Args[2] 153 | switch shell { 154 | case "bash": 155 | fmt.Println(bashCompletion) 156 | case "zsh": 157 | fmt.Println(zshCompletion) 158 | case "fish": 159 | fmt.Println(fishCompletion) 160 | default: 161 | fmt.Printf("Unsupported shell: %s\n", shell) 162 | os.Exit(1) 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /cmd/doggo/help.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "text/template" 6 | 7 | "github.com/fatih/color" 8 | ) 9 | 10 | // appHelpTextTemplate is the text/template to customise the Help output. 11 | var appHelpTextTemplate = `{{ "NAME" | color "" "heading" }}: 12 | {{ .Name | color "green" "bold" }} 🐶 {{ .Description }} 13 | 14 | {{ "USAGE" | color "" "heading" }}: 15 | {{ .Name | color "green" "bold" }} [--] {{ "[query options]" | color "yellow" "" }} {{ "[arguments...]" | color "cyan" "" }} 16 | 17 | {{ "VERSION" | color "" "heading" }}: 18 | {{ .Version | color "red" "" }} - {{ .Date | color "red" "" }} 19 | 20 | {{ "EXAMPLES" | color "" "heading" }}: 21 | {{- range $example := .Examples }} 22 | {{ $.Name | color "green" "bold" }} {{ printf "%-40s" $example.Command | color "cyan" "" }}{{ $example.Description }} 23 | {{- end }} 24 | 25 | {{ "FREE FORM ARGUMENTS" | color "" "heading" }}: 26 | Supply hostnames, query types, and classes without flags. Example: 27 | {{ .Name | color "green" "bold" }} {{ "mrkaran.dev A @1.1.1.1" | color "cyan" "" }} 28 | 29 | {{ "TRANSPORT OPTIONS" | color "" "heading" }}: 30 | Specify the protocol with a URL-type scheme. 31 | UDP is used if no scheme is specified. 32 | 33 | {{- range $opt := .TransportOptions }} 34 | {{ printf "%-12s" $opt.Scheme | color "yellow" "" }}{{ printf "%-68s" $opt.Example }}{{ $opt.Description | color "cyan" "" }} 35 | {{- end }} 36 | 37 | {{ "SUBCOMMANDS" | color "" "heading" }}: 38 | {{- range $opt := .Subcommands }} 39 | {{ printf "%-30s" $opt.Flag | color "yellow" "" }}{{ $opt.Description }} 40 | {{- end }} 41 | 42 | {{ "QUERY OPTIONS" | color "" "heading" }}: 43 | {{- range $opt := .QueryOptions }} 44 | {{ printf "%-30s" $opt.Flag | color "yellow" "" }}{{ $opt.Description }} 45 | {{- end }} 46 | 47 | {{ "RESOLVER OPTIONS" | color "" "heading" }}: 48 | {{- range $opt := .ResolverOptions }} 49 | {{ printf "%-30s" $opt.Flag | color "yellow" "" }}{{ $opt.Description }} 50 | {{- end }} 51 | 52 | {{ "QUERY FLAGS" | color "" "heading" }}: 53 | {{- range $flag := .QueryFlags }} 54 | {{ printf "%-30s" $flag.Flag | color "yellow" "" }}{{ $flag.Description }} 55 | {{- end }} 56 | 57 | {{ "OUTPUT OPTIONS" | color "" "heading" }}: 58 | {{- range $opt := .OutputOptions }} 59 | {{ printf "%-30s" $opt.Flag | color "yellow" "" }}{{ $opt.Description }} 60 | {{- end }} 61 | 62 | {{ "GLOBALPING OPTIONS" | color "" "heading" }}: 63 | {{- range $opt := .GlobalPingOptions }} 64 | {{ printf "%-30s" $opt.Flag | color "yellow" "" }}{{ $opt.Description }} 65 | {{- end }} 66 | ` 67 | 68 | func renderCustomHelp() { 69 | type Option struct { 70 | Flag string 71 | Description string 72 | } 73 | 74 | type Example struct { 75 | Command string 76 | Description string 77 | } 78 | 79 | type TransportOption struct { 80 | Scheme string 81 | Example string 82 | Description string 83 | } 84 | 85 | helpTmplVars := map[string]interface{}{ 86 | "Name": "doggo", 87 | "Description": "DNS Client for Humans", 88 | "Version": buildVersion, 89 | "Date": buildDate, 90 | "Examples": []Example{ 91 | {"mrkaran.dev", "Query a domain using defaults."}, 92 | {"mrkaran.dev CNAME", "Query for a CNAME record."}, 93 | {"mrkaran.dev MX @9.9.9.9", "Uses a custom DNS resolver."}, 94 | {"-q mrkaran.dev -t MX -n 1.1.1.1", "Using named arguments."}, 95 | {"mrkaran.dev --aa --ad", "Query with Authoritative Answer and Authenticated Data flags set."}, 96 | {"mrkaran.dev --cd --do", "Query with Checking Disabled and DNSSEC OK flags set."}, 97 | {"mrkaran.dev --gp-from Germany", "Query using Globalping API from a specific location."}, 98 | }, 99 | "TransportOptions": []TransportOption{ 100 | {"@udp://", "eg: @1.1.1.1", "initiates a UDP query to 1.1.1.1:53."}, 101 | {"@tcp://", "eg: @tcp://1.1.1.1", "initiates a TCP query to 1.1.1.1:53."}, 102 | {"@https://", "eg: @https://cloudflare-dns.com/dns-query", "initiates a DOH query to Cloudflare via DoH."}, 103 | {"@tls://", "eg: @tls://1.1.1.1", "initiates a DoT query to 1.1.1.1:853."}, 104 | {"@sdns://", "initiates a DNSCrypt or DoH query using a DNS stamp.", ""}, 105 | {"@quic://", "initiates a DOQ query.", ""}, 106 | }, 107 | "Subcommands": []Option{ 108 | {"completions [bash|zsh|fish]", "Generate the shell completion script for the specified shell."}, 109 | }, 110 | "QueryOptions": []Option{ 111 | {"-q, --query=HOSTNAME", "Hostname to query the DNS records for (eg mrkaran.dev)."}, 112 | {"-t, --type=TYPE", "Type of the DNS Record (A, MX, NS etc)."}, 113 | {"-n, --nameserver=ADDR", "Address of a specific nameserver to send queries to (9.9.9.9, 8.8.8.8 etc)."}, 114 | {"-c, --class=CLASS", "Network class of the DNS record (IN, CH, HS etc)."}, 115 | {"-x, --reverse", "Performs a DNS Lookup for an IPv4 or IPv6 address. Sets the query type and class to PTR and IN respectively."}, 116 | {"--any", "Query all supported DNS record types (A, AAAA, CNAME, MX, NS, PTR, SOA, SRV, TXT, CAA)."}, 117 | }, 118 | "ResolverOptions": []Option{ 119 | {"--strategy=STRATEGY", "Specify strategy to query nameserver listed in etc/resolv.conf. (all, random, first)."}, 120 | {"--ndots=INT", "Specify ndots parameter. Takes value from /etc/resolv.conf if using the system namesever or 1 otherwise."}, 121 | {"--search", "Use the search list defined in resolv.conf. Defaults to true. Set --search=false to disable search list."}, 122 | {"--timeout=DURATION", "Specify timeout for the resolver to return a response (e.g., 5s, 400ms, 1m)."}, 123 | {"-4, --ipv4", "Use IPv4 only."}, 124 | {"-6, --ipv6", "Use IPv6 only."}, 125 | {"--tls-hostname=HOSTNAME", "Provide a hostname for verification of the certificate if the provided DoT nameserver is an IP."}, 126 | {"--skip-hostname-verification", "Skip TLS Hostname Verification in case of DOT Lookups."}, 127 | }, 128 | "QueryFlags": []Option{ 129 | {"--aa", "Set Authoritative Answer flag."}, 130 | {"--ad", "Set Authenticated Data flag."}, 131 | {"--cd", "Set Checking Disabled flag."}, 132 | {"--rd", "Set Recursion Desired flag (default: true)."}, 133 | {"--z", "Set Z flag (reserved for future use)."}, 134 | {"--do", "Set DNSSEC OK flag."}, 135 | }, 136 | "OutputOptions": []Option{ 137 | {"-J, --json", "Format the output as JSON."}, 138 | {"--short", "Short output format. Shows only the response section."}, 139 | {"--color", "Defaults to true. Set --color=false to disable colored output."}, 140 | {"--debug", "Enable debug logging."}, 141 | {"--time", "Shows how long the response took from the server."}, 142 | }, 143 | "GlobalPingOptions": []Option{ 144 | {"--gp-from=Germany", "Query using Globalping API from a specific location."}, 145 | {"--gp-limit=INT", "Limit the number of probes to use from Globalping."}, 146 | }, 147 | } 148 | 149 | tmpl, err := template.New("help").Funcs(template.FuncMap{ 150 | "color": func(clr string, format string, str string) string { 151 | formatter := color.New() 152 | switch clr { 153 | case "yellow": 154 | formatter = formatter.Add(color.FgYellow) 155 | case "red": 156 | formatter = formatter.Add(color.FgRed) 157 | case "cyan": 158 | formatter = formatter.Add(color.FgCyan) 159 | case "green": 160 | formatter = formatter.Add(color.FgGreen) 161 | } 162 | switch format { 163 | case "bold": 164 | formatter = formatter.Add(color.Bold) 165 | case "underline": 166 | formatter = formatter.Add(color.Underline) 167 | case "heading": 168 | formatter = formatter.Add(color.Bold, color.Underline) 169 | } 170 | return formatter.SprintFunc()(str) 171 | }, 172 | }).Parse(appHelpTextTemplate) 173 | if err != nil { 174 | panic(err) 175 | } 176 | err = tmpl.Execute(color.Output, helpTmplVars) 177 | if err != nil { 178 | panic(err) 179 | } 180 | os.Exit(0) 181 | } 182 | -------------------------------------------------------------------------------- /cmd/doggo/parse.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/miekg/dns" 7 | ) 8 | 9 | // loadUnparsedArgs tries to parse all the arguments 10 | // which are unparsed by `flag` library. These arguments don't have any specific 11 | // order so we have to deduce based on the pattern of argument. 12 | // For eg, a nameserver must always begin with `@`. In this 13 | // pattern we deduce the arguments and append it to the 14 | // list of internal query flags. 15 | // In case an argument isn't able to fit in any of the existing 16 | // pattern it is considered to be a "hostname". 17 | // Eg of unparsed argument: `dig mrkaran.dev @1.1.1.1 AAAA` 18 | // where `@1.1.1.1` and `AAAA` are "unparsed" args. 19 | // Returns a list of nameserver, queryTypes, queryClasses, queryNames. 20 | func loadUnparsedArgs(args []string) ([]string, []string, []string, []string) { 21 | var nameservers, queryTypes, queryClasses, queryNames []string 22 | for _, arg := range args { 23 | if strings.HasPrefix(arg, "@") { 24 | nameservers = append(nameservers, strings.TrimPrefix(arg, "@")) 25 | } else if qt, ok := dns.StringToType[strings.ToUpper(arg)]; ok { 26 | queryTypes = append(queryTypes, dns.TypeToString[qt]) 27 | } else if qc, ok := dns.StringToClass[strings.ToUpper(arg)]; ok { 28 | queryClasses = append(queryClasses, dns.ClassToString[qc]) 29 | } else { 30 | queryNames = append(queryNames, arg) 31 | } 32 | } 33 | return nameservers, queryTypes, queryClasses, queryNames 34 | } 35 | -------------------------------------------------------------------------------- /config-api-sample.toml: -------------------------------------------------------------------------------- 1 | [server] 2 | address = ":8080" 3 | name = "doggo-api" 4 | # WARNING If these timeouts are less than 1s, 5 | # the server connection breaks. 6 | read_timeout=7000 7 | write_timeout=7000 8 | keepalive_timeout=5000 9 | max_body_size=10000 -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | # build output 2 | dist/ 3 | # generated types 4 | .astro/ 5 | 6 | # dependencies 7 | node_modules/ 8 | 9 | # logs 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | pnpm-debug.log* 14 | 15 | 16 | # environment variables 17 | .env 18 | .env.production 19 | 20 | # macOS-specific files 21 | .DS_Store 22 | -------------------------------------------------------------------------------- /docs/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["astro-build.astro-vscode"], 3 | "unwantedRecommendations": [] 4 | } 5 | -------------------------------------------------------------------------------- /docs/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "command": "./node_modules/.bin/astro dev", 6 | "name": "Development server", 7 | "request": "launch", 8 | "type": "node-terminal" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Starlight Starter Kit: Basics 2 | 3 | [![Built with Starlight](https://astro.badg.es/v2/built-with-starlight/tiny.svg)](https://starlight.astro.build) 4 | 5 | ``` 6 | npm create astro@latest -- --template starlight 7 | ``` 8 | 9 | [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/withastro/starlight/tree/main/examples/basics) 10 | [![Open with CodeSandbox](https://assets.codesandbox.io/github/button-edit-lime.svg)](https://codesandbox.io/p/sandbox/github/withastro/starlight/tree/main/examples/basics) 11 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fwithastro%2Fstarlight%2Ftree%2Fmain%2Fexamples%2Fbasics&project-name=my-starlight-docs&repository-name=my-starlight-docs) 12 | 13 | > 🧑‍🚀 **Seasoned astronaut?** Delete this file. Have fun! 14 | 15 | ## 🚀 Project Structure 16 | 17 | Inside of your Astro + Starlight project, you'll see the following folders and files: 18 | 19 | ``` 20 | . 21 | ├── public/ 22 | ├── src/ 23 | │ ├── assets/ 24 | │ ├── content/ 25 | │ │ ├── docs/ 26 | │ │ └── config.ts 27 | │ └── env.d.ts 28 | ├── astro.config.mjs 29 | ├── package.json 30 | └── tsconfig.json 31 | ``` 32 | 33 | Starlight looks for `.md` or `.mdx` files in the `src/content/docs/` directory. Each file is exposed as a route based on its file name. 34 | 35 | Images can be added to `src/assets/` and embedded in Markdown with a relative link. 36 | 37 | Static assets, like favicons, can be placed in the `public/` directory. 38 | 39 | ## 🧞 Commands 40 | 41 | All commands are run from the root of the project, from a terminal: 42 | 43 | | Command | Action | 44 | | :------------------------ | :----------------------------------------------- | 45 | | `npm install` | Installs dependencies | 46 | | `npm run dev` | Starts local dev server at `localhost:4321` | 47 | | `npm run build` | Build your production site to `./dist/` | 48 | | `npm run preview` | Preview your build locally, before deploying | 49 | | `npm run astro ...` | Run CLI commands like `astro add`, `astro check` | 50 | | `npm run astro -- --help` | Get help using the Astro CLI | 51 | 52 | ## 👀 Want to learn more? 53 | 54 | Check out [Starlight’s docs](https://starlight.astro.build/), read [the Astro documentation](https://docs.astro.build), or jump into the [Astro Discord server](https://astro.build/chat). 55 | -------------------------------------------------------------------------------- /docs/astro.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "astro/config"; 2 | import starlight from "@astrojs/starlight"; 3 | import { defineConfig, passthroughImageService } from "astro/config"; 4 | 5 | // https://astro.build/config 6 | export default defineConfig({ 7 | base: "/docs", 8 | site: "https://doggo.mrkaran.dev/docs/", 9 | image: { 10 | service: passthroughImageService(), 11 | }, 12 | integrations: [ 13 | starlight({ 14 | title: "Doggo", 15 | customCss: ["./src/assets/custom.css"], 16 | social: { 17 | github: "https://github.com/mr-karan/doggo", 18 | }, 19 | sidebar: [ 20 | { 21 | label: "Introduction", 22 | items: [{ label: "Installation", link: "/intro/installation" }], 23 | }, 24 | { 25 | label: "Usage Guide", 26 | items: [ 27 | { label: "Examples", link: "/guide/examples" }, 28 | { label: "CLI Reference", link: "/guide/reference" }, 29 | ], 30 | }, 31 | { 32 | label: "Resolvers", 33 | items: [ 34 | { label: "Classic (UDP and TCP)", link: "/resolvers/classic" }, 35 | { label: "System", link: "/resolvers/system" }, 36 | { label: "DNS over HTTPS (DoH)", link: "/resolvers/doh" }, 37 | { label: "DNS over TLS (DoT)", link: "/resolvers/dot" }, 38 | { label: "DNSCrypt", link: "/resolvers/dnscrypt" }, 39 | { label: "DNS over HTTPS (DoQ)", link: "/resolvers/quic" }, 40 | ], 41 | }, 42 | { 43 | label: "Features", 44 | items: [ 45 | { label: "Output Formats", link: "/features/output" }, 46 | { label: "Multiple Resolvers", link: "/features/multiple" }, 47 | { label: "IPv4 and IPv6", link: "/features/ip" }, 48 | { label: "Reverse IP Lookups", link: "/features/reverse" }, 49 | { label: "Protocol Tweaks", link: "/features/tweaks" }, 50 | { label: "Shell Completions", link: "/features/shell" }, 51 | { label: "Common Record Types", link: "/features/any" }, 52 | ], 53 | }, 54 | ], 55 | }), 56 | ], 57 | }); 58 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "type": "module", 4 | "version": "0.0.1", 5 | "scripts": { 6 | "dev": "astro dev", 7 | "start": "astro dev", 8 | "build": "astro build", 9 | "preview": "astro preview", 10 | "astro": "astro" 11 | }, 12 | "dependencies": { 13 | "@astrojs/starlight": "^0.24.4", 14 | "astro": "^4.10.2", 15 | "sharp": "^0.33.4" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /docs/public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 28 | 29 | -------------------------------------------------------------------------------- /docs/src/assets/custom.css: -------------------------------------------------------------------------------- 1 | .hero { 2 | padding-block: unset; 3 | } -------------------------------------------------------------------------------- /docs/src/assets/doggo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mr-karan/doggo/16ba284a136354fb4f11fd4d566db9b4364e9a32/docs/src/assets/doggo.png -------------------------------------------------------------------------------- /docs/src/content/config.ts: -------------------------------------------------------------------------------- 1 | import { defineCollection } from 'astro:content'; 2 | import { docsSchema } from '@astrojs/starlight/schema'; 3 | 4 | export const collections = { 5 | docs: defineCollection({ schema: docsSchema() }), 6 | }; 7 | -------------------------------------------------------------------------------- /docs/src/content/docs/features/any.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Common record types 3 | description: Learn how to query multiple DNS record types simultaneously 4 | --- 5 | 6 | The `--any` flag in Doggo allows you to query all supported DNS record types for a given domain in a single command. This can be particularly useful when you want to get a comprehensive view of a domain's DNS configuration without running multiple queries. 7 | 8 | ## Syntax 9 | 10 | ```bash 11 | doggo [domain] --any 12 | ``` 13 | 14 | ## Supported Record Types 15 | 16 | When you use the `--any` flag, Doggo will query for the following common DNS record types: 17 | 18 | - A (IPv4 address) 19 | - AAAA (IPv6 address) 20 | - CNAME (Canonical name) 21 | - MX (Mail exchange) 22 | - NS (Name server) 23 | - PTR (Pointer) 24 | - SOA (Start of authority) 25 | - SRV (Service) 26 | - TXT (Text) 27 | - CAA (Certification Authority Authorization) 28 | 29 | ## Example Usage 30 | 31 | ```bash 32 | doggo mrkaran.dev --any 33 | ``` 34 | 35 | ## Example Output 36 | 37 | ```bash 38 | $ doggo example.com --any 39 | NAME TYPE CLASS TTL ADDRESS NAMESERVER 40 | example.com. A IN 3401s 93.184.215.14 127.0.0.53:53 41 | example.com. AAAA IN 3600s 2606:2800:21f:cb07:6820:80da:af6b:8b2c 127.0.0.53:53 42 | example.com. MX IN 77316s 0 . 127.0.0.53:53 43 | example.com. NS IN 86400s a.iana-servers.net. 127.0.0.53:53 44 | example.com. NS IN 86400s b.iana-servers.net. 127.0.0.53:53 45 | example.com. SOA IN 3600s ns.icann.org. 127.0.0.53:53 46 | noc.dns.icann.org. 2024041841 47 | 7200 3600 1209600 3600 48 | example.com. TXT IN 86400s "v=spf1 -all" 127.0.0.53:53 49 | example.com. TXT IN 86400s "wgyf8z8cgvm2qmxpnbnldrcltvk4xqfn" 127.0.0.53:53 50 | ``` 51 | 52 | ## Considerations 53 | 54 | - The `--any` query may take longer to complete compared to querying a single record type, as it's fetching multiple record types. 55 | -------------------------------------------------------------------------------- /docs/src/content/docs/features/ip.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: IPv4 and IPv6 Support 3 | description: Learn how Doggo handles both IPv4 and IPv6 DNS queries 4 | --- 5 | 6 | Doggo provides support for both IPv4 and IPv6, allowing you to perform DNS queries over either protocol. 7 | 8 | ### Default Behavior 9 | 10 | By default, Doggo will query for A (IPv4) records only. This means it will return only IPv4 addresses when querying a domain without specifying a record type. 11 | 12 | ```bash 13 | $ doggo mrkaran.dev 14 | NAME TYPE CLASS TTL ADDRESS NAMESERVER 15 | mrkaran.dev. A IN 300s 104.21.7.168 127.0.0.53:53 16 | mrkaran.dev. A IN 300s 172.67.187.239 127.0.0.53:53 17 | ``` 18 | 19 | ### Querying for IPv6 (AAAA) Records 20 | 21 | To query for IPv6 addresses, you need to explicitly request AAAA records: 22 | 23 | ```bash 24 | $ doggo AAAA mrkaran.dev 25 | NAME TYPE CLASS TTL ADDRESS NAMESERVER 26 | mrkaran.dev. AAAA IN 300s 2606:4700:3030::ac43:bbef 127.0.0.53:53 27 | mrkaran.dev. AAAA IN 300s 2606:4700:3035::6815:7a8 127.0.0.53:53 28 | ``` 29 | 30 | ### Querying for Both IPv4 and IPv6 31 | 32 | To get both IPv4 and IPv6 addresses, you can specify both A and AAAA record types: 33 | 34 | ```bash 35 | $ doggo A AAAA mrkaran.dev 36 | NAME TYPE CLASS TTL ADDRESS NAMESERVER 37 | mrkaran.dev. A IN 204s 104.21.7.168 127.0.0.53:53 38 | mrkaran.dev. A IN 204s 172.67.187.239 127.0.0.53:53 39 | mrkaran.dev. AAAA IN 284s 2606:4700:3035::6815:7a8 127.0.0.53:53 40 | mrkaran.dev. AAAA IN 284s 2606:4700:3030::ac43:bbef 127.0.0.53:53 41 | ``` 42 | 43 | ### Forcing IPv4 or IPv6 44 | 45 | You can force Doggo to use only IPv4 or IPv6 with the `-4` and `-6` flags respectively: 46 | 47 | #### IPv4 Only (Default behavior) 48 | 49 | ```bash 50 | $ doggo -4 mrkaran.dev 51 | ``` 52 | 53 | #### IPv6 Only 54 | 55 | ```bash 56 | $ doggo -6 mrkaran.dev 57 | ``` 58 | -------------------------------------------------------------------------------- /docs/src/content/docs/features/multiple.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Multiple Resolvers 3 | description: Learn how to use multiple DNS resolvers simultaneously with Doggo 4 | --- 5 | 6 | Doggo supports querying multiple DNS resolvers simultaneously, allowing you to compare responses or use different resolvers for different purposes. 7 | 8 | ### Using Multiple Resolvers 9 | 10 | To use multiple resolvers, simply specify them in your command: 11 | 12 | ```bash 13 | $ doggo mrkaran.dev @1.1.1.1 @8.8.8.8 @9.9.9.9 14 | ``` 15 | 16 | This will query the domain `mrkaran.dev` using Cloudflare (1.1.1.1), Google (8.8.8.8), and Quad9 (9.9.9.9) DNS servers. 17 | 18 | ### Mixing Resolver Types 19 | 20 | You can mix different types of resolvers in a single query: 21 | 22 | ```bash 23 | doggo mrkaran.dev @1.1.1.1 @https://dns.google/dns-query @tls://9.9.9.9 24 | ``` 25 | 26 | This command uses a standard DNS resolver (1.1.1.1), a DoH resolver (Google), and a DoT resolver (Quad9). 27 | -------------------------------------------------------------------------------- /docs/src/content/docs/features/output.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Output Formats 3 | description: Learn about Doggo's various output formats including colored, JSON, and short outputs 4 | --- 5 | 6 | Doggo provides flexible output formats to suit different use cases, from human-readable colored output to machine-parsable JSON. 7 | 8 | ### Colored Output 9 | 10 | By default, Doggo uses a colored, tabular format for easy readability. 11 | 12 | ```bash 13 | doggo mrkaran.dev 14 | NAME TYPE CLASS TTL ADDRESS NAMESERVER 15 | mrkaran.dev. A IN 300s 104.21.7.168 127.0.0.53:53 16 | mrkaran.dev. A IN 300s 172.67.187.239 127.0.0.53:53 17 | ``` 18 | 19 | ```bash 20 | doggo mrkaran.dev --gp-from Europe,Asia --gp-limit 2 21 | LOCATION NAME TYPE CLASS TTL ADDRESS NAMESERVER 22 | Vienna, AT, EU, EDIS GmbH 23 | (AS57169) 24 | mrkaran.dev. A IN 300s 104.21.7.168 private 25 | mrkaran.dev. A IN 300s 172.67.187.239 private 26 | Tokyo, JP, AS, Tencent 27 | Building, Kejizhongyi Avenue 28 | (AS132203) 29 | mrkaran.dev. A IN 300s 104.21.7.168 private 30 | mrkaran.dev. A IN 300s 172.67.187.239 private 31 | ``` 32 | 33 | To disable colored output, use the `--color=false` flag: 34 | 35 | ```bash 36 | doggo mrkaran.dev --color=false 37 | ``` 38 | 39 | ### JSON Output 40 | 41 | For scripting and programmatic use, Doggo supports JSON output using the `--json` or `-J` flag: 42 | 43 | ```bash 44 | doggo internetfreedom.in --json | jq 45 | ``` 46 | 47 | ```json 48 | { 49 | "responses": { 50 | "answers": [ 51 | { 52 | "name": "internetfreedom.in.", 53 | "type": "A", 54 | "class": "IN", 55 | "ttl": "22s", 56 | "address": "104.27.158.96", 57 | "rtt": "37ms", 58 | "nameserver": "127.0.0.1:53" 59 | } 60 | // ... more entries ... 61 | ], 62 | "queries": [ 63 | { 64 | "name": "internetfreedom.in.", 65 | "type": "A", 66 | "class": "IN" 67 | } 68 | ] 69 | } 70 | } 71 | ``` 72 | 73 | ```bash 74 | doggo mrkaran.dev --gp-from Europe,Asia --gp-limit 2 --json | jq 75 | ``` 76 | 77 | ```json 78 | { 79 | "responses": [ 80 | { 81 | "location": "Groningen, NL, EU, Google LLC (AS396982)", 82 | "answers": [ 83 | { 84 | "name": "mrkaran.dev.", 85 | "type": "A", 86 | "class": "IN", 87 | "ttl": "300s", 88 | "address": "172.67.187.239", 89 | "status": "", 90 | "rtt": "", 91 | "nameserver": "private" 92 | } 93 | // ... more entries ... 94 | ] 95 | }, 96 | { 97 | "location": "Jakarta, ID, AS, Zenlayer Inc (AS21859)", 98 | "answers": [ 99 | { 100 | "name": "mrkaran.dev.", 101 | "type": "A", 102 | "class": "IN", 103 | "ttl": "300s", 104 | "address": "172.67.187.239", 105 | "status": "", 106 | "rtt": "", 107 | "nameserver": "private" 108 | } 109 | // ... more entries ... 110 | ] 111 | } 112 | ] 113 | } 114 | ``` 115 | 116 | ### Short Output 117 | 118 | For a more concise view, use the `--short` flag to show only the response section: 119 | 120 | ```bash 121 | doggo mrkaran.dev --short 122 | 104.21.7.168 123 | 172.67.187.239 124 | ``` 125 | 126 | ```bash 127 | doggo mrkaran.dev --gp-from Europe,Asia --gp-limit 2 --short 128 | Frankfurt, DE, EU, WIBO Baltic UAB (AS59939) 129 | 104.21.7.168 130 | 172.67.187.239 131 | Saratov, RU, AS, LLC "SMART CENTER" (AS48763) 132 | 172.67.187.239 133 | 104.21.7.168 134 | ``` 135 | -------------------------------------------------------------------------------- /docs/src/content/docs/features/reverse.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Reverse IP Lookups 3 | description: Learn how to perform reverse IP lookups with Doggo 4 | --- 5 | 6 | Doggo supports reverse IP lookups, allowing you to find the domain name associated with a given IP address. This feature is particularly useful for network diagnostics, security analysis, and understanding the ownership of IP addresses. 7 | 8 | ### Performing a Reverse IP Lookup 9 | 10 | To perform a reverse IP lookup, use the `--reverse` flag followed by the IP address: 11 | 12 | ```bash 13 | doggo --reverse 8.8.4.4 14 | ``` 15 | 16 | ### Short Output Format 17 | 18 | You can combine the reverse lookup with the `--short` flag to get a concise output: 19 | 20 | ```bash 21 | doggo --reverse 8.8.4.4 --short 22 | dns.google. 23 | ``` 24 | 25 | This command returns only the domain name associated with the IP address, without any additional information. 26 | 27 | ### Full Output Format 28 | 29 | Without the `--short` flag, Doggo will provide more detailed information: 30 | 31 | ```bash 32 | $ doggo --reverse 8.8.4.4 33 | NAME TYPE CLASS TTL ADDRESS NAMESERVER 34 | 4.4.8.8.in-addr.arpa. PTR IN 21599s dns.google. 127.0.0.53:53 35 | ``` 36 | 37 | ### IPv6 Support 38 | 39 | Reverse IP lookups also work with IPv6 addresses: 40 | 41 | ```bash 42 | $ doggo --reverse 2001:4860:4860::8888 --short 43 | dns.google. 44 | ``` 45 | -------------------------------------------------------------------------------- /docs/src/content/docs/features/shell.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Shell Completions 3 | description: Learn how to use Doggo's shell completions for zsh and fish 4 | --- 5 | 6 | Doggo provides shell completions for `zsh` and `fish`, enhancing your command-line experience with auto-completion for commands and options. 7 | 8 | ### Bash Completions 9 | 10 | To enable Bash completions for Doggo: 11 | 12 | 1. Generate the completion script: 13 | 14 | ```bash 15 | doggo completions bash > doggo_completion.bash 16 | ``` 17 | 18 | 2. Source the generated file in your `.bashrc` or `.bash_profile`: 19 | 20 | ```bash 21 | echo "source ~/path/to/doggo_completion.bash" >> ~/.bashrc 22 | ``` 23 | 24 | Replace `~/path/to/` with the actual path where you saved the completion script. 25 | 26 | 3. Restart your shell or run `source ~/.bashrc`. 27 | 28 | 29 | ### Zsh Completions 30 | 31 | To enable Zsh completions for Doggo: 32 | 33 | 1. Ensure that shell completions are enabled in your `.zshrc` file: 34 | 35 | ```zsh 36 | autoload -U compinit 37 | compinit 38 | ``` 39 | 40 | 2. Generate the completion script and add it to your Zsh functions path: 41 | 42 | ```bash 43 | doggo completions zsh > "${fpath[1]}/_doggo" 44 | ``` 45 | 46 | 3. Restart your shell or run `source ~/.zshrc`. 47 | 48 | Now you can use Tab to auto-complete Doggo commands and options in Zsh. 49 | 50 | ### Fish Completions 51 | 52 | To enable Fish completions for Doggo: 53 | 54 | 1. Generate the completion script and save it to the Fish completions directory: 55 | 56 | ```bash 57 | $ doggo completions fish > ~/.config/fish/completions/doggo.fish 58 | ``` 59 | 60 | 2. Restart your Fish shell or run `source ~/.config/fish/config.fish`. 61 | 62 | You can now use Tab to auto-complete Doggo commands and options in Fish. 63 | 64 | ### Using Completions 65 | 66 | With completions enabled, you can: 67 | 68 | - Auto-complete command-line flags (e.g., `doggo --`) 69 | - Auto-complete DNS record types (e.g., `doggo -t `) 70 | - Auto-complete subcommands and options 71 | -------------------------------------------------------------------------------- /docs/src/content/docs/features/tweaks.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Protocol Tweaks 3 | description: Learn how to fine-tune DNS queries with Doggo's protocol tweaks 4 | --- 5 | 6 | Doggo provides several options to tweak the DNS protocol parameters, allowing for fine-grained control over your queries. 7 | 8 | ### Query Flags 9 | 10 | Doggo supports setting various DNS query flags: 11 | 12 | ``` 13 | --aa Set Authoritative Answer flag 14 | --ad Set Authenticated Data flag 15 | --cd Set Checking Disabled flag 16 | --rd Set Recursion Desired flag (default: true) 17 | --z Set Z flag (reserved for future use) 18 | --do Set DNSSEC OK flag 19 | ``` 20 | 21 | ### Examples 22 | 23 | 1. Request an authoritative answer: 24 | ```bash 25 | doggo example.com --aa 26 | ``` 27 | 28 | 2. Request DNSSEC data: 29 | ```bash 30 | doggo example.com --do 31 | ``` 32 | 33 | 3. Disable recursive querying: 34 | ```bash 35 | doggo example.com --rd=false 36 | ``` 37 | -------------------------------------------------------------------------------- /docs/src/content/docs/guide/examples.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Usage Examples 3 | description: Practical examples showcasing the versatility and power of Doggo DNS client 4 | --- 5 | 6 | These examples showcase how to combine different features for powerful DNS querying. 7 | 8 | ## Basic Queries 9 | 10 | 1. Simple A record lookup: 11 | 12 | ```bash 13 | doggo example.com 14 | ``` 15 | 16 | 2. Query for a specific record type: 17 | 18 | ```bash 19 | doggo AAAA example.com 20 | ``` 21 | 22 | 3. Query multiple record types simultaneously: 23 | 24 | ```bash 25 | doggo A AAAA MX example.com 26 | ``` 27 | 28 | 4. Query using Globalping API from a specific location: 29 | ```bash 30 | doggo example.com --gp-from Germany 31 | ``` 32 | 33 | ### Using Different Resolvers 34 | 35 | 4. Query using a specific DNS resolver: 36 | 37 | ```bash 38 | doggo example.com @1.1.1.1 39 | ``` 40 | 41 | 5. Use DNS-over-HTTPS (DoH): 42 | 43 | ```bash 44 | doggo example.com @https://cloudflare-dns.com/dns-query 45 | ``` 46 | 47 | 6. Use DNS-over-TLS (DoT): 48 | 49 | ```bash 50 | doggo example.com @tls://1.1.1.1 51 | ``` 52 | 53 | 7. Query multiple resolvers and compare results: 54 | 55 | ```bash 56 | doggo example.com @1.1.1.1 @8.8.8.8 @9.9.9.9 57 | ``` 58 | 59 | 8. Using Globalping API 60 | ```bash 61 | doggo example.com @1.1.1.1 --gp-from Germany 62 | ``` 63 | 64 | ### Advanced Queries 65 | 66 | 8. Perform a reverse DNS lookup: 67 | 68 | ```bash 69 | doggo --reverse 8.8.8.8 70 | ``` 71 | 72 | 9. Set query flags for DNSSEC validation: 73 | 74 | ```bash 75 | doggo example.com --do --cd 76 | ``` 77 | 78 | 10. Use the short output format for concise results: 79 | 80 | ```bash 81 | doggo example.com --short 82 | ``` 83 | 84 | 11. Show query timing information: 85 | ```bash 86 | doggo example.com --time 87 | ``` 88 | 89 | ### Combining Flags 90 | 91 | 12. Perform a reverse lookup with short output and custom resolver: 92 | 93 | ```bash 94 | doggo --reverse 8.8.8.8 --short @1.1.1.1 95 | ``` 96 | 97 | 13. Query for MX records using DoH with JSON output: 98 | 99 | ```bash 100 | doggo MX example.com @https://dns.google/dns-query --json 101 | ``` 102 | 103 | 14. Use IPv6 only with a specific timeout and DNSSEC checking: 104 | ```bash 105 | doggo AAAA example.com -6 --timeout 3s --do 106 | ``` 107 | 108 | ## Scripting and Automation 109 | 110 | 16. Use JSON output for easy parsing in scripts: 111 | 112 | ```bash 113 | doggo example.com --json | jq '.responses[0].answers[].address' 114 | ``` 115 | 116 | 17. Batch query multiple domains from a file: 117 | 118 | ```bash 119 | cat domains.txt | xargs -I {} doggo {} --short 120 | ``` 121 | 122 | 18. Find all nameservers for a domain and its parent domains: 123 | 124 | ```bash 125 | doggo NS example.com example.com. com. . --short 126 | ``` 127 | 128 | 19. Extract all MX records and their priorities: 129 | 130 | ```bash 131 | doggo MX gmail.com --json | jq -r '.responses[0].answers[] | "\(.address) \(.preference)"' 132 | ``` 133 | 134 | 20. Count the number of IPv6 addresses for a domain: 135 | ```bash 136 | doggo AAAA example.com --json | jq '.responses[0].answers | length' 137 | ``` 138 | 139 | ## Troubleshooting and Debugging 140 | 141 | 21. Enable debug logging for verbose output: 142 | 143 | ```bash 144 | doggo example.com --debug 145 | ``` 146 | 147 | 22. Compare responses with and without EDNS Client Subnet: 148 | 149 | ```bash 150 | doggo example.com @8.8.8.8 151 | doggo example.com @8.8.8.8 --z 152 | ``` 153 | 154 | 23. Test DNSSEC validation: 155 | 156 | ```bash 157 | doggo rsasecured.net --do @8.8.8.8 158 | ``` 159 | 160 | This example uses a domain known to be DNSSEC-signed. The `--do` flag sets the DNSSEC OK bit. 161 | 162 | Note: DNSSEC validation can be complex and depends on various factors: 163 | 164 | - The domain must be properly DNSSEC-signed 165 | - The resolver must support DNSSEC 166 | - The resolver must be configured to perform DNSSEC validation 167 | 168 | If you don't see DNSSEC-related information in the output, try using a resolver known to support DNSSEC, like 8.8.8.8 (Google) or 9.9.9.9 (Quad9). 169 | 170 | 24. Compare responses with and without EDNS Client Subnet: 171 | 172 | ```bash 173 | doggo example.com @8.8.8.8 174 | doggo example.com @8.8.8.8 --z 175 | ``` 176 | 177 | 25. Check for DNSSEC records (DNSKEY, DS, RRSIG): 178 | 179 | ```bash 180 | doggo DNSKEY example.com @8.8.8.8 181 | doggo DS example.com @8.8.8.8 182 | doggo RRSIG example.com @8.8.8.8 183 | ``` 184 | 185 | 26. Verify DNSSEC chain of trust: 186 | ```bash 187 | doggo example.com --type=A --do --cd=false @8.8.8.8 188 | ``` 189 | -------------------------------------------------------------------------------- /docs/src/content/docs/guide/reference.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: CLI Reference Guide 3 | description: Comprehensive guide to all command-line options and flags for Doggo DNS client 4 | --- 5 | 6 | This guide provides a comprehensive list of all command-line options and flags available in Doggo. 7 | 8 | ## Basic Syntax 9 | 10 | ``` 11 | doggo [--] [query options] [arguments...] 12 | ``` 13 | 14 | ## Query Options 15 | 16 | | Option | Description | 17 | | ----------------------- | ---------------------------------------------------------------------------- | 18 | | `-q, --query=HOSTNAME` | Hostname to query the DNS records for (e.g., example.com) | 19 | | `-t, --type=TYPE` | Type of the DNS Record (A, MX, NS, etc.) | 20 | | `-n, --nameserver=ADDR` | Address of a specific nameserver to send queries to (e.g., 9.9.9.9, 8.8.8.8) | 21 | | `-c, --class=CLASS` | Network class of the DNS record (IN, CH, HS, etc.) | 22 | | `-x, --reverse` | Performs a reverse DNS lookup for an IPv4 or IPv6 address | 23 | 24 | ## Resolver Options 25 | 26 | | Option | Description | 27 | | ------------------------------ | --------------------------------------------------------------------------- | 28 | | `--strategy=STRATEGY` | Specify strategy to query nameservers (all, random, first) | 29 | | `--ndots=INT` | Specify ndots parameter | 30 | | `--search` | Use the search list defined in resolv.conf (default: true) | 31 | | `--timeout=DURATION` | Specify timeout for the resolver to return a response (e.g., 5s, 400ms, 1m) | 32 | | `-4, --ipv4` | Use IPv4 only | 33 | | `-6, --ipv6` | Use IPv6 only | 34 | | `--tls-hostname=HOSTNAME` | Provide a hostname for TLS certificate verification | 35 | | `--skip-hostname-verification` | Skip TLS Hostname Verification for DoT lookups | 36 | 37 | ## Query Flags 38 | 39 | | Flag | Description | 40 | | ------ | ------------------------------------------ | 41 | | `--aa` | Set Authoritative Answer flag | 42 | | `--ad` | Set Authenticated Data flag | 43 | | `--cd` | Set Checking Disabled flag | 44 | | `--rd` | Set Recursion Desired flag (default: true) | 45 | | `--z` | Set Z flag (reserved for future use) | 46 | | `--do` | Set DNSSEC OK flag | 47 | 48 | ## Output Options 49 | 50 | | Option | Description | 51 | | ------------ | ----------------------------------------------------- | 52 | | `-J, --json` | Format the output as JSON | 53 | | `--short` | Short output format (shows only the response section) | 54 | | `--color` | Enable/disable colored output (default: true) | 55 | | `--debug` | Enable debug logging | 56 | | `--time` | Show query response time | 57 | 58 | ## Transport Options 59 | 60 | Specify the protocol with a URL-type scheme. UDP is used if no scheme is specified. 61 | 62 | | Scheme | Description | Example | 63 | | ----------- | ------------------------------- | --------------------------------------- | 64 | | `@udp://` | UDP query | `@1.1.1.1` | 65 | | `@tcp://` | TCP query | `@tcp://1.1.1.1` | 66 | | `@https://` | DNS over HTTPS (DoH) | `@https://cloudflare-dns.com/dns-query` | 67 | | `@tls://` | DNS over TLS (DoT) | `@tls://1.1.1.1` | 68 | | `@sdns://` | DNSCrypt or DoH using DNS stamp | `@sdns://...` | 69 | | `@quic://` | DNS over QUIC | `@quic://dns.adguard.com` | 70 | 71 | ## Globalping API Options 72 | 73 | | Option | Description | Example | 74 | | ------------ | ---------------------------------- | ----------------------- | 75 | | `--gp-from` | Specify the location to query from | `--gp-from Europe,Asia` | 76 | | `--gp-limit` | Limit the number of probes to use | `--gp-limit 5` | 77 | 78 | ## Examples 79 | 80 | 1. Query a domain using defaults: 81 | 82 | ``` 83 | doggo example.com 84 | ``` 85 | 86 | 2. Query for a CNAME record: 87 | 88 | ``` 89 | doggo example.com CNAME 90 | ``` 91 | 92 | 3. Use a custom DNS resolver: 93 | 94 | ``` 95 | doggo example.com MX @9.9.9.9 96 | ``` 97 | 98 | 4. Using named arguments: 99 | 100 | ``` 101 | doggo -q example.com -t MX -n 1.1.1.1 102 | ``` 103 | 104 | 5. Query with specific flags: 105 | 106 | ``` 107 | doggo example.com --aa --ad 108 | ``` 109 | 110 | 6. Query using Globalping API from a specific location: 111 | ``` 112 | doggo example.com --gp-from Europe,Asia --gp-limit 5 113 | ``` 114 | 115 | For more detailed usage examples, refer to the [Examples](/guide/examples) section. 116 | -------------------------------------------------------------------------------- /docs/src/content/docs/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Doggo 3 | description: The Friendly DNS Client 4 | hero: 5 | tagline: Command-line DNS client for humans 6 | actions: 7 | - text: Visit Demo 8 | link: https://doggo.mrkaran.dev/ 9 | icon: right-arrow 10 | variant: primary 11 | --- 12 | 13 | ![](../../assets/doggo.png) 14 | 15 | ## Features 16 | 17 | - Human-readable output with color-coded and tabular format 18 | - JSON output support for easy scripting and parsing 19 | - Multiple transport protocols: 20 | - DNS over HTTPS (DoH) 21 | - DNS over TLS (DoT) 22 | - DNS over QUIC (DoQ) 23 | - DNS over TCP 24 | - DNS over UDP 25 | - DNSCrypt 26 | - Support for `ndots` and `search` configurations from `resolv.conf` or command-line arguments 27 | - Multiple resolver support with customizable query strategies 28 | - IPv4 and IPv6 support 29 | - Web interface available at [doggo.mrkaran.dev](https://doggo.mrkaran.dev) 30 | - Shell completions for `zsh` and `fish` 31 | - Reverse DNS lookups 32 | - Flexible query options including various DNS flags (AA, AD, CD, DO, etc.) 33 | - Debug mode for troubleshooting 34 | - Response time measurement 35 | - Cross-platform support (Linux, macOS, Windows, FreeBSD, NetBSD) 36 | 37 | ## Sponsor 38 | 39 | If you find Doggo useful and would like to support its development, please consider becoming a sponsor on GitHub. Your support helps maintain and improve this open-source project. 40 | By sponsoring, you're not just supporting the development of Doggo, but also encouraging the creation and maintenance of free, open-source software that benefits the entire community. Every contribution, no matter how small, is greatly appreciated and helps keep this project alive and growing. 41 | 42 | [Become a GitHub Sponsor](https://github.com/sponsors/mr-karan) 43 | 44 | Thank you for your support! 🐶❤️ 45 | -------------------------------------------------------------------------------- /docs/src/content/docs/intro/installation.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Installation 3 | description: Learn how to install Doggo, a modern command-line DNS client for humans 4 | --- 5 | 6 | Doggo can be installed using various methods. Choose the one that best suits your needs and system configuration. 7 | 8 | ### Easy Install (Recommended) 9 | 10 | The easiest way to install Doggo is by using the installation script: 11 | 12 | ```shell 13 | curl -sS https://raw.githubusercontent.com/mr-karan/doggo/main/install.sh | sh 14 | ``` 15 | 16 | This script will automatically download and install the latest version of Doggo for your system. 17 | 18 | ### Binary Installation 19 | 20 | You can download pre-compiled binaries for various operating systems and architectures from the [Releases](https://github.com/mr-karan/doggo/releases) section of the GitHub repository. 21 | 22 | ### Docker 23 | 24 | Doggo is available as a Docker image hosted on GitHub Container Registry (ghcr.io). It supports both x86 and ARM architectures. 25 | 26 | To pull the latest image: 27 | 28 | ```shell 29 | docker pull ghcr.io/mr-karan/doggo:latest 30 | ``` 31 | 32 | To run Doggo using Docker: 33 | 34 | ```shell 35 | docker run --rm ghcr.io/mr-karan/doggo:latest mrkaran.dev @1.1.1.1 MX 36 | ``` 37 | 38 | ### Package Managers 39 | 40 | #### Homebrew (macOS and Linux) 41 | 42 | Install via [Homebrew](https://brew.sh/): 43 | 44 | ```bash 45 | brew install doggo 46 | ``` 47 | 48 | #### Arch Linux 49 | 50 | Install using an AUR helper like `yay`: 51 | 52 | ```bash 53 | yay -S doggo-bin 54 | ``` 55 | 56 | #### Scoop (Windows) 57 | 58 | Install via [Scoop](https://scoop.sh/): 59 | 60 | ```bash 61 | scoop install doggo 62 | ``` 63 | 64 | ### From Source 65 | 66 | To install Doggo from source, you need to have Go installed on your system. 67 | 68 | ```bash 69 | go install github.com/mr-karan/doggo/cmd/doggo@latest 70 | ``` 71 | 72 | The binary will be available at `$GOPATH/bin/doggo`. 73 | 74 | After installation, you can verify the installation by running `doggo` in your terminal. For usage examples and command-line arguments, refer to the [Usage](/usage) section of the documentation. 75 | -------------------------------------------------------------------------------- /docs/src/content/docs/resolvers/classic.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Classic Resolver 3 | description: Understanding and using Doggo's classic DNS resolver functionality 4 | --- 5 | 6 | Doggo's classic resolver supports traditional DNS queries over UDP and TCP protocols. This is the default mode of operation and is compatible with standard DNS servers. 7 | 8 | ### Using the Classic Resolver 9 | 10 | By default, `doggo` uses the classic resolver when no specific protocol is specified. 11 | 12 | ```bash 13 | doggo mrkaran.dev 14 | ``` 15 | 16 | You can explicitly use UDP or TCP by prefixing the nameserver with `@udp://` or `@tcp://` respectively. 17 | 18 | #### UDP 19 | 20 | ```bash 21 | doggo mrkaran.dev @udp://1.1.1.1 22 | ``` 23 | 24 | #### TCP 25 | 26 | ```bash 27 | doggo mrkaran.dev @tcp://8.8.8.8 28 | ``` 29 | -------------------------------------------------------------------------------- /docs/src/content/docs/resolvers/dnscrypt.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: DNSCrypt Resolver 3 | description: Enhance your DNS privacy and security with Doggo's DNSCrypt support 4 | --- 5 | 6 | Doggo supports DNSCrypt, a protocol that authenticates communications between a DNS client and a DNS resolver. It prevents DNS spoofing and provides confidentiality for DNS queries. 7 | 8 | ### Using DNSCrypt 9 | 10 | To use DNSCrypt, you need to provide a DNS stamp prefixed with `@sdns://`: 11 | 12 | ```bash 13 | doggo mrkaran.dev @sdns://AgcAAAAAAAAABzEuMC4wLjEAEmRucy5jbG91ZGZsYXJlLmNvbQovZG5zLXF1ZXJ5 14 | ``` 15 | 16 | This command initiates a DNSCrypt (or DoH) resolver using its DNS stamp. 17 | 18 | ### DNS Stamps 19 | 20 | DNS stamps are compact, encoded strings that contain all the necessary information to connect to a DNSCrypt or DoH server. They include: 21 | 22 | - The protocol used (DNSCrypt or DoH) 23 | - The server's address and port 24 | - The provider's public key 25 | - The provider name 26 | 27 | The stamp in the example above is for Cloudflare's DNS service. 28 | 29 | ### Benefits of Using DNSCrypt 30 | 31 | - Authenticates the DNS resolver, preventing DNS spoofing attacks 32 | - Encrypts DNS queries and responses, enhancing privacy 33 | - Supports features like DNS-based blocklists and custom DNS rules 34 | 35 | ### Public DNSCrypt Resolvers 36 | 37 | You can find a list of public DNSCrypt resolvers at [dnscrypt.info](https://dnscrypt.info/public-servers). Each resolver will have its own DNS stamp that you can use with Doggo. 38 | 39 | ### Considerations When Using DNSCrypt 40 | 41 | - Requires trust in the DNSCrypt provider, as they can see your DNS queries. 42 | - May introduce slight latency compared to classic DNS resolver. 43 | - Not all DNS providers support DNSCrypt. 44 | -------------------------------------------------------------------------------- /docs/src/content/docs/resolvers/doh.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: DNS over HTTPS (DoH) 3 | description: Secure your DNS queries using Doggo's DNS over HTTPS feature 4 | --- 5 | 6 | Doggo supports DNS over HTTPS (DoH), which encrypts DNS queries and responses, enhancing privacy and security by preventing eavesdropping and manipulation of DNS traffic. 7 | 8 | ### Using DoH with Doggo 9 | 10 | To use DoH, specify a DoH server URL prefixed with `@https://`: 11 | 12 | ```bash 13 | doggo mrkaran.dev @https://cloudflare-dns.com/dns-query 14 | ``` 15 | 16 | ### Popular DoH Providers 17 | 18 | Doggo works with various DoH providers. Here are some popular options: 19 | 20 | 1. Cloudflare: `@https://cloudflare-dns.com/dns-query` 21 | 2. Google: `@https://dns.google/dns-query` 22 | 3. Quad9: `@https://dns.quad9.net/dns-query` 23 | 24 | ### Benefits of Using DoH 25 | 26 | - Encrypts DNS traffic, improving privacy 27 | - Helps bypass DNS-based content filters 28 | - Can improve DNS security by preventing DNS spoofing attacks 29 | 30 | ### Considerations When Using DoH 31 | 32 | - May introduce slight latency compared to classic DNS 33 | - Some network administrators may not approve of DoH use, as it bypasses local DNS controls 34 | -------------------------------------------------------------------------------- /docs/src/content/docs/resolvers/dot.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: DNS over TLS (DoT) 3 | description: Secure your DNS queries using Doggo's DNS over TLS feature 4 | --- 5 | 6 | Doggo supports DNS over TLS (DoT), which provides encryption for DNS queries, enhancing privacy and security by protecting DNS traffic from interception and tampering. 7 | 8 | ### Using DoT with Doggo 9 | 10 | To use DoT, specify a DoT server address prefixed with `@tls://`: 11 | 12 | ```bash 13 | doggo example.com @tls://1.1.1.1 14 | ``` 15 | 16 | ### Popular DoT Providers 17 | 18 | Doggo works with various DoT providers. Here are some popular options: 19 | 20 | 1. Cloudflare: `@tls://1.1.1.1` 21 | 2. Google: `@tls://8.8.8.8` 22 | 3. Quad9: `@tls://9.9.9.9` 23 | 24 | ### Benefits of Using DoT 25 | 26 | - Encrypts DNS traffic, improving privacy 27 | - Helps prevent DNS spoofing and man-in-the-middle attacks 28 | - Compatible with most network configurations that allow outbound connections on port 853 29 | 30 | ### Considerations When Using DoT 31 | 32 | - May introduce slight latency compared to classic DNS 33 | - Requires trust in the DoT provider, as they can see your DNS queries 34 | 35 | ### Advanced DoT Usage 36 | 37 | For DNS over TLS (DoT), Doggo provides additional options: 38 | 39 | ``` 40 | --tls-hostname=HOSTNAME Provide a hostname for certificate verification if the DoT nameserver is an IP. 41 | --skip-hostname-verification Skip TLS Hostname Verification for DoT Lookups. 42 | ``` 43 | 44 | #### Specify a custom TLS hostname: 45 | ```bash 46 | doggo example.com @tls://1.1.1.1 --tls-hostname=cloudflare-dns.com 47 | ``` 48 | 49 | #### Skip hostname verification (use with caution): 50 | ```bash 51 | doggo example.com @tls://1.1.1.1 --skip-hostname-verification 52 | ``` 53 | -------------------------------------------------------------------------------- /docs/src/content/docs/resolvers/quic.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: DNS over QUIC (DoQ) 3 | description: Leverage the speed and security of QUIC protocol for DNS queries with Doggo 4 | --- 5 | 6 | Doggo supports DNS over QUIC (DoQ), a relatively new protocol that enhances security through data encryption and improves internet performance by utilizing QUIC. QUIC, or Quick UDP Internet Connections, is a network protocol developed by Google. It reduces latency and speeds up data transmission compared to the traditional TCP protocol. 7 | 8 | ### Using DoQ with Doggo 9 | 10 | To use DoQ, specify a DoQ server URL prefixed with `@quic://`: 11 | 12 | ```bash 13 | doggo mrkaran.dev @quic://dns.adguard.com 14 | ``` 15 | 16 | ### Available DoQ Providers 17 | 18 | As DoQ is a relatively new protocol, fewer providers currently support it compared to DoH or DoT. Here are some known DoQ providers: 19 | 20 | 1. AdGuard: `@quic://dns.adguard.com` 21 | 2. Cloudflare: `@quic://cloudflare-dns.com` 22 | 23 | ### Benefits of Using DoQ 24 | 25 | - Reduces connection establishment time compared to TCP-based protocols 26 | - Improves performance on unreliable networks 27 | -------------------------------------------------------------------------------- /docs/src/content/docs/resolvers/system.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: System Resolver 3 | description: Learn how Doggo interacts with system resolver settings and how to configure resolver behavior 4 | --- 5 | 6 | Doggo interacts with your system's DNS resolver configuration and provides options to customize this behavior. This page explains how Doggo handles `ndots`, `search` domains, and resolver strategies. 7 | 8 | ## Reading from /etc/resolv.conf 9 | 10 | By default, Doggo reads configuration from your system's `/etc/resolv.conf` file. This includes: 11 | 12 | - List of nameservers 13 | - The `ndots` value 14 | - Search domains 15 | 16 | ## ndots Configuration 17 | 18 | The `ndots` option sets the threshold for the number of dots that must appear in a name before an initial absolute query will be made. 19 | 20 | - When using the system nameserver, Doggo reads the `ndots` value from `/etc/resolv.conf`. 21 | - If not using the system nameserver, it defaults to 1. 22 | - You can override this with the `--ndots` flag: 23 | 24 | ```bash 25 | $ doggo example --ndots=2 26 | ``` 27 | 28 | This affects how Doggo handles non-fully qualified domain names. 29 | 30 | ## Search Configuration 31 | 32 | The search configuration allows Doggo to append domain names to queries that are not fully qualified. 33 | 34 | - By default, Doggo uses the search list defined in `resolv.conf`. 35 | - You can disable this behavior with `--search=false`: 36 | 37 | ```bash 38 | $ doggo example --search=false 39 | ``` 40 | 41 | - When search is enabled and a query is not fully qualified, Doggo will try appending domains from the search list. 42 | 43 | ## Resolver Strategy 44 | 45 | The resolver strategy determines how Doggo uses the nameservers listed in `/etc/resolv.conf`. You can specify a strategy using the `--strategy` flag: 46 | 47 | ```bash 48 | $ doggo example.com --strategy=first 49 | ``` 50 | 51 | Available strategies: 52 | 53 | - `all` (default): Use all nameservers listed in `/etc/resolv.conf`. 54 | - `first`: Use only the first nameserver in the list. 55 | - `random`: Randomly choose one nameserver from the list for each query. This can help distribute the load across multiple nameservers. 56 | 57 | ## Command-line Options 58 | 59 | ```bash 60 | --ndots=INT Specify ndots parameter. Takes value from /etc/resolv.conf if using the system nameserver or 1 otherwise. 61 | --search Use the search list defined in resolv.conf. Defaults to true. Set --search=false to disable search list. 62 | --strategy=STRATEGY Specify strategy to query nameservers listed in /etc/resolv.conf. Options: all, first, random. Defaults to all. 63 | --timeout=DURATION Set the timeout for resolver responses (e.g., 5s, 400ms, 1m). 64 | ``` 65 | 66 | ## Examples 67 | 68 | 1. Use system resolver with default settings: 69 | ```bash 70 | doggo example.com 71 | ``` 72 | 73 | 2. Use system resolver but change ndots and disable search: 74 | ```bash 75 | doggo example --ndots=2 --search=false 76 | ``` 77 | 78 | 3. Use system resolver with 'first' strategy and custom timeout: 79 | ```bash 80 | doggo example.com --strategy=first --timeout=2s 81 | ``` 82 | 83 | 4. Override system resolver and use specific nameservers: 84 | ```bash 85 | doggo example.com @1.1.1.1 @8.8.8.8 86 | ``` 87 | Note: When specifying nameservers directly, the system resolver configuration (including strategy) is not used. 88 | 89 | You can find more examples at [Examples](/guide/examples) section. -------------------------------------------------------------------------------- /docs/src/content/docs/usage.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Classic Resolver 3 | description: Understanding and using Doggo's classic DNS resolver functionality 4 | --- -------------------------------------------------------------------------------- /docs/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/strict" 3 | } 4 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mr-karan/doggo 2 | 3 | go 1.22.6 4 | 5 | toolchain go1.23.1 6 | 7 | require ( 8 | github.com/ameshkov/dnscrypt/v2 v2.3.0 9 | github.com/ameshkov/dnsstamps v1.0.3 10 | github.com/fatih/color v1.17.0 11 | github.com/go-chi/chi/v5 v5.1.0 12 | github.com/jsdelivr/globalping-cli v1.3.1-0.20240925142143-6b5f0951f8e1 13 | github.com/knadh/koanf/parsers/toml v0.1.0 14 | github.com/knadh/koanf/providers/env v1.0.0 15 | github.com/knadh/koanf/providers/file v1.1.0 16 | github.com/knadh/koanf/providers/posflag v0.1.0 17 | github.com/knadh/koanf/v2 v2.1.1 18 | github.com/miekg/dns v1.1.62 19 | github.com/olekukonko/tablewriter v0.0.5 20 | github.com/quic-go/quic-go v0.47.0 21 | github.com/spf13/pflag v1.0.5 22 | golang.org/x/sys v0.25.0 23 | ) 24 | 25 | require ( 26 | github.com/AdguardTeam/golibs v0.27.0 // indirect 27 | github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect 28 | github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 // indirect 29 | github.com/andybalholm/brotli v1.1.0 // indirect 30 | github.com/fsnotify/fsnotify v1.7.0 // indirect 31 | github.com/go-task/slim-sprig/v3 v3.0.0 // indirect 32 | github.com/go-viper/mapstructure/v2 v2.2.1 // indirect 33 | github.com/google/pprof v0.0.0-20240929191954-255acd752d31 // indirect 34 | github.com/knadh/koanf/maps v0.1.1 // indirect 35 | github.com/mattn/go-colorable v0.1.13 // indirect 36 | github.com/mattn/go-isatty v0.0.20 // indirect 37 | github.com/mattn/go-runewidth v0.0.16 // indirect 38 | github.com/mitchellh/copystructure v1.2.0 // indirect 39 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 40 | github.com/onsi/ginkgo/v2 v2.20.2 // indirect 41 | github.com/pelletier/go-toml v1.9.5 // indirect 42 | github.com/rivo/uniseg v0.4.7 // indirect 43 | go.uber.org/mock v0.4.0 // indirect 44 | golang.org/x/crypto v0.27.0 // indirect 45 | golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect 46 | golang.org/x/mod v0.21.0 // indirect 47 | golang.org/x/net v0.29.0 // indirect 48 | golang.org/x/sync v0.8.0 // indirect 49 | golang.org/x/tools v0.25.0 // indirect 50 | ) 51 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/AdguardTeam/golibs v0.27.0 h1:YxCFK6HBGp/ZXp3bv5uei+oLH12UfIYB8u2rh1B6nnU= 2 | github.com/AdguardTeam/golibs v0.27.0/go.mod h1:iWdjXPCwmK2g2FKIb/OwEPnovSXeMqRhI8FWLxF5oxE= 3 | github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY= 4 | github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= 5 | github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 h1:52m0LGchQBBVqJRyYYufQuIbVqRawmubW3OFGqK1ekw= 6 | github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635/go.mod h1:lmLxL+FV291OopO93Bwf9fQLQeLyt33VJRUg5VJ30us= 7 | github.com/ameshkov/dnscrypt/v2 v2.3.0 h1:pDXDF7eFa6Lw+04C0hoMh8kCAQM8NwUdFEllSP2zNLs= 8 | github.com/ameshkov/dnscrypt/v2 v2.3.0/go.mod h1:N5hDwgx2cNb4Ay7AhvOSKst+eUiOZ/vbKRO9qMpQttE= 9 | github.com/ameshkov/dnsstamps v1.0.3 h1:Srzik+J9mivH1alRACTbys2xOxs0lRH9qnTA7Y1OYVo= 10 | github.com/ameshkov/dnsstamps v1.0.3/go.mod h1:Ii3eUu73dx4Vw5O4wjzmT5+lkCwovjzaEZZ4gKyIH5A= 11 | github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= 12 | github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= 13 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 14 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 15 | github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= 16 | github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= 17 | github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= 18 | github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= 19 | github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= 20 | github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= 21 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 22 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 23 | github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 24 | github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= 25 | github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= 26 | github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= 27 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 28 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 29 | github.com/google/pprof v0.0.0-20240929191954-255acd752d31 h1:LcRdQWywSgfi5jPsYZ1r2avbbs5IQ5wtyhMBCcokyo4= 30 | github.com/google/pprof v0.0.0-20240929191954-255acd752d31/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= 31 | github.com/jsdelivr/globalping-cli v1.3.1-0.20240925142143-6b5f0951f8e1 h1:UrEC+iF/FHS/5UTJZJXOkm8y7wct2sqhNftJ2WQN7WI= 32 | github.com/jsdelivr/globalping-cli v1.3.1-0.20240925142143-6b5f0951f8e1/go.mod h1:2+lO4/xYSauKsf+pZ62bro1c4StxDO3cYcrLx4jsYmI= 33 | github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs= 34 | github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= 35 | github.com/knadh/koanf/parsers/toml v0.1.0 h1:S2hLqS4TgWZYj4/7mI5m1CQQcWurxUz6ODgOub/6LCI= 36 | github.com/knadh/koanf/parsers/toml v0.1.0/go.mod h1:yUprhq6eo3GbyVXFFMdbfZSo928ksS+uo0FFqNMnO18= 37 | github.com/knadh/koanf/providers/env v1.0.0 h1:ufePaI9BnWH+ajuxGGiJ8pdTG0uLEUWC7/HDDPGLah0= 38 | github.com/knadh/koanf/providers/env v1.0.0/go.mod h1:mzFyRZueYhb37oPmC1HAv/oGEEuyvJDA98r3XAa8Gak= 39 | github.com/knadh/koanf/providers/file v1.1.0 h1:MTjA+gRrVl1zqgetEAIaXHqYje0XSosxSiMD4/7kz0o= 40 | github.com/knadh/koanf/providers/file v1.1.0/go.mod h1:/faSBcv2mxPVjFrXck95qeoyoZ5myJ6uxN8OOVNJJCI= 41 | github.com/knadh/koanf/providers/posflag v0.1.0 h1:mKJlLrKPcAP7Ootf4pBZWJ6J+4wHYujwipe7Ie3qW6U= 42 | github.com/knadh/koanf/providers/posflag v0.1.0/go.mod h1:SYg03v/t8ISBNrMBRMlojH8OsKowbkXV7giIbBVgbz0= 43 | github.com/knadh/koanf/v2 v2.1.1 h1:/R8eXqasSTsmDCsAyYj+81Wteg8AqrV9CP6gvsTsOmM= 44 | github.com/knadh/koanf/v2 v2.1.1/go.mod h1:4mnTRbZCK+ALuBXHZMjDfG9y714L7TykVnZkXbMU3Es= 45 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 46 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 47 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 48 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 49 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 50 | github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 51 | github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= 52 | github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 53 | github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ= 54 | github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ= 55 | github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= 56 | github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= 57 | github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= 58 | github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 59 | github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= 60 | github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= 61 | github.com/onsi/ginkgo/v2 v2.20.2 h1:7NVCeyIWROIAheY21RLS+3j2bb52W0W82tkberYytp4= 62 | github.com/onsi/ginkgo/v2 v2.20.2/go.mod h1:K9gyxPIlb+aIvnZ8bd9Ak+YP18w3APlR+5coaZoE2ag= 63 | github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= 64 | github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= 65 | github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= 66 | github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= 67 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 68 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 69 | github.com/quic-go/quic-go v0.47.0 h1:yXs3v7r2bm1wmPTYNLKAAJTHMYkPEsfYJmTazXrCZ7Y= 70 | github.com/quic-go/quic-go v0.47.0/go.mod h1:3bCapYsJvXGZcipOHuu7plYtaV6tnF+z7wIFsU0WK9E= 71 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 72 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 73 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 74 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 75 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 76 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 77 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 78 | go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= 79 | go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= 80 | golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= 81 | golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= 82 | golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= 83 | golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= 84 | golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= 85 | golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= 86 | golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= 87 | golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= 88 | golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= 89 | golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 90 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 91 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 92 | golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= 93 | golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 94 | golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= 95 | golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 96 | golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= 97 | golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 98 | golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE= 99 | golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg= 100 | google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= 101 | google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 102 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 103 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 104 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | set -eu 4 | printf '\n' 5 | 6 | BOLD="$(tput bold 2>/dev/null || printf '')" 7 | GREY="$(tput setaf 0 2>/dev/null || printf '')" 8 | GREEN="$(tput setaf 2 2>/dev/null || printf '')" 9 | YELLOW="$(tput setaf 3 2>/dev/null || printf '')" 10 | BLUE="$(tput setaf 4 2>/dev/null || printf '')" 11 | RED="$(tput setaf 1 2>/dev/null || printf '')" 12 | NO_COLOR="$(tput sgr0 2>/dev/null || printf '')" 13 | 14 | info() { 15 | printf '%s\n' "${BOLD}${GREY}>${NO_COLOR} $*" 16 | } 17 | 18 | warn() { 19 | printf '%s\n' "${YELLOW}! $*${NO_COLOR}" 20 | } 21 | 22 | error() { 23 | printf '%s\n' "${RED}x $*${NO_COLOR}" >&2 24 | } 25 | 26 | completed() { 27 | printf '%s\n' "${GREEN}✓${NO_COLOR} $*" 28 | } 29 | 30 | has() { 31 | command -v "$1" 1>/dev/null 2>&1 32 | } 33 | 34 | SUPPORTED_TARGETS="Linux_x86_64 Linux_arm64 Windows_x86_64 Darwin_x86_64 Darwin_arm64" 35 | 36 | get_latest_release() { 37 | curl --silent "https://api.github.com/repos/mr-karan/doggo/releases/latest" | 38 | grep '"tag_name":' | 39 | sed -E 's/.*"([^"]+)".*/\1/' 40 | } 41 | 42 | detect_platform() { 43 | platform="$(uname -s)" 44 | case "${platform}" in 45 | Linux*) platform="Linux" ;; 46 | Darwin*) platform="Darwin" ;; 47 | MINGW*|MSYS*|CYGWIN*) platform="Windows" ;; 48 | *) 49 | error "Unsupported platform: ${platform}" 50 | exit 1 51 | ;; 52 | esac 53 | printf '%s' "${platform}" 54 | } 55 | 56 | detect_arch() { 57 | arch="$(uname -m)" 58 | case "${arch}" in 59 | x86_64) arch="x86_64" ;; 60 | aarch64|arm64) arch="arm64" ;; 61 | *) 62 | error "Unsupported architecture: ${arch}" 63 | exit 1 64 | ;; 65 | esac 66 | printf '%s' "${arch}" 67 | } 68 | 69 | download_and_install() { 70 | version="$1" 71 | platform="$2" 72 | arch="$3" 73 | 74 | # Remove 'v' prefix from version for filename 75 | version_no_v="${version#v}" 76 | if [ "${platform}" = "Windows" ]; then 77 | filename="doggo_${version_no_v}_${platform}_${arch}.zip" 78 | else 79 | filename="doggo_${version_no_v}_${platform}_${arch}.tar.gz" 80 | fi 81 | url="https://github.com/mr-karan/doggo/releases/download/${version}/${filename}" 82 | 83 | info "Downloading doggo ${version} for ${platform}_${arch}..." 84 | info "Download URL: ${url}" 85 | 86 | if has curl; then 87 | if ! curl -sSL "${url}" -o "${filename}"; then 88 | error "Failed to download ${filename}" 89 | error "Curl output:" 90 | curl -SL "${url}" 91 | exit 1 92 | fi 93 | elif has wget; then 94 | if ! wget -q "${url}" -O "${filename}"; then 95 | error "Failed to download ${filename}" 96 | error "Wget output:" 97 | wget "${url}" 98 | exit 1 99 | fi 100 | else 101 | error "Neither curl nor wget found. Please install one of them and try again." 102 | exit 1 103 | fi 104 | 105 | info "Verifying downloaded file..." 106 | if [ "${platform}" = "Windows" ]; then 107 | if ! file "${filename}" | grep -q "Zip archive data"; then 108 | error "Downloaded file is not in zip format. Installation failed." 109 | error "File type:" 110 | file "${filename}" 111 | rm -f "${filename}" 112 | exit 1 113 | fi 114 | else 115 | if ! file "${filename}" | grep -q "gzip compressed data"; then 116 | error "Downloaded file is not in gzip format. Installation failed." 117 | error "File type:" 118 | file "${filename}" 119 | rm -f "${filename}" 120 | exit 1 121 | fi 122 | fi 123 | 124 | info "Extracting ${filename}..." 125 | extract_dir="doggo_extract" 126 | mkdir -p "${extract_dir}" 127 | if [ "${platform}" = "Windows" ]; then 128 | if ! unzip -q "${filename}" -d "${extract_dir}"; then 129 | error "Failed to extract ${filename}" 130 | rm -rf "${filename}" "${extract_dir}" 131 | exit 1 132 | fi 133 | else 134 | if ! tar -xzvf "${filename}" -C "${extract_dir}"; then 135 | error "Failed to extract ${filename}" 136 | rm -rf "${filename}" "${extract_dir}" 137 | exit 1 138 | fi 139 | fi 140 | 141 | info "Installing doggo..." 142 | binary_name="doggo" 143 | if [ "${platform}" = "Windows" ]; then 144 | binary_name="doggo.exe" 145 | fi 146 | 147 | # Find the doggo binary in the extracted directory 148 | binary_path=$(find "${extract_dir}" -name "${binary_name}" -type f) 149 | 150 | if [ -z "${binary_path}" ]; then 151 | error "${binary_name} not found in the extracted files" 152 | error "Extracted files:" 153 | ls -R "${extract_dir}" 154 | rm -rf "${filename}" "${extract_dir}" 155 | exit 1 156 | fi 157 | 158 | chmod +x "${binary_path}" 159 | if ! sudo mv "${binary_path}" /usr/local/bin/doggo; then 160 | error "Failed to move doggo to /usr/local/bin/" 161 | rm -rf "${filename}" "${extract_dir}" 162 | exit 1 163 | fi 164 | 165 | info "Cleaning up..." 166 | rm -rf "${filename}" "${extract_dir}" 167 | 168 | completed "doggo ${version} has been installed to /usr/local/bin/doggo" 169 | } 170 | 171 | main() { 172 | if ! has curl && ! has wget; then 173 | error "Either curl or wget is required to download doggo. Please install one of them and try again." 174 | exit 1 175 | fi 176 | 177 | platform="$(detect_platform)" 178 | arch="$(detect_arch)" 179 | version="$(get_latest_release)" 180 | 181 | info "Latest doggo version: ${version}" 182 | info "Detected platform: ${platform}" 183 | info "Detected architecture: ${arch}" 184 | 185 | target="${platform}_${arch}" 186 | 187 | if ! echo "${SUPPORTED_TARGETS}" | grep -q "${target}"; then 188 | error "Unsupported target: ${target}" 189 | exit 1 190 | fi 191 | 192 | download_and_install "${version}" "${platform}" "${arch}" 193 | 194 | info "You can now use doggo by running 'doggo' in your terminal." 195 | } 196 | 197 | main -------------------------------------------------------------------------------- /internal/app/app.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "log/slog" 5 | 6 | "github.com/jsdelivr/globalping-cli/globalping" 7 | "github.com/miekg/dns" 8 | "github.com/mr-karan/doggo/pkg/models" 9 | "github.com/mr-karan/doggo/pkg/resolvers" 10 | ) 11 | 12 | // App represents the structure for all app wide configuration. 13 | type App struct { 14 | Logger *slog.Logger 15 | Version string 16 | QueryFlags models.QueryFlags 17 | Questions []dns.Question 18 | Resolvers []resolvers.Resolver 19 | ResolverOpts resolvers.Options 20 | Nameservers []models.Nameserver 21 | 22 | globalping globalping.Client 23 | } 24 | 25 | // NewApp initializes an instance of App which holds app wide configuration. 26 | func New( 27 | logger *slog.Logger, 28 | globalping globalping.Client, 29 | buildVersion string, 30 | ) App { 31 | app := App{ 32 | Logger: logger, 33 | Version: buildVersion, 34 | QueryFlags: models.QueryFlags{ 35 | QNames: []string{}, 36 | QTypes: []string{}, 37 | QClasses: []string{}, 38 | Nameservers: []string{}, 39 | }, 40 | Nameservers: []models.Nameserver{}, 41 | globalping: globalping, 42 | } 43 | return app 44 | } 45 | -------------------------------------------------------------------------------- /internal/app/globalping.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "net" 8 | "net/url" 9 | "strconv" 10 | "strings" 11 | "time" 12 | 13 | "github.com/fatih/color" 14 | "github.com/jsdelivr/globalping-cli/globalping" 15 | "github.com/mr-karan/doggo/pkg/resolvers" 16 | "github.com/olekukonko/tablewriter" 17 | ) 18 | 19 | var ( 20 | ErrTargetIPVersionNotAllowed = errors.New("ipVersion is not allowed when target is not a domain") 21 | ErrResolverIPVersionNotAllowed = errors.New("ipVersion is not allowed when resolver is not a domain") 22 | ) 23 | 24 | func (app *App) GlobalpingMeasurement() (*globalping.Measurement, error) { 25 | if len(app.QueryFlags.QNames) > 1 { 26 | return nil, errors.New("only one target is allowed for globalping") 27 | } 28 | if len(app.QueryFlags.QTypes) > 1 { 29 | return nil, errors.New("only one query type is allowed for globalping") 30 | } 31 | 32 | target := app.QueryFlags.QNames[0] 33 | resolver, port, protocol, err := parseGlobalpingResolver(app.QueryFlags.Nameservers) 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | if app.QueryFlags.UseIPv4 || app.QueryFlags.UseIPv6 { 39 | if net.ParseIP(target) != nil { 40 | return nil, ErrTargetIPVersionNotAllowed 41 | } 42 | if resolver != "" && net.ParseIP(resolver) != nil { 43 | return nil, ErrResolverIPVersionNotAllowed 44 | } 45 | } 46 | 47 | o := &globalping.MeasurementCreate{ 48 | Type: "dns", 49 | Target: target, 50 | Limit: app.QueryFlags.GPLimit, 51 | Locations: parseGlobalpingLocations(app.QueryFlags.GPFrom), 52 | Options: &globalping.MeasurementOptions{ 53 | Protocol: protocol, 54 | Port: port, 55 | }, 56 | } 57 | if app.QueryFlags.UseIPv4 { 58 | o.Options.IPVersion = globalping.IPVersion4 59 | } else if app.QueryFlags.UseIPv6 { 60 | o.Options.IPVersion = globalping.IPVersion6 61 | } 62 | if resolver != "" { 63 | o.Options.Resolver = resolver 64 | } 65 | if len(app.QueryFlags.QTypes) > 0 { 66 | o.Options.Query = &globalping.QueryOptions{ 67 | Type: app.QueryFlags.QTypes[0], 68 | } 69 | } 70 | res, err := app.globalping.CreateMeasurement(o) 71 | if err != nil { 72 | return nil, err 73 | } 74 | measurement, err := app.globalping.GetMeasurement(res.ID) 75 | if err != nil { 76 | return nil, err 77 | } 78 | for measurement.Status == globalping.StatusInProgress { 79 | time.Sleep(500 * time.Millisecond) 80 | measurement, err = app.globalping.GetMeasurement(res.ID) 81 | if err != nil { 82 | return nil, err 83 | } 84 | } 85 | 86 | if measurement.Status != globalping.StatusFinished { 87 | return nil, &globalping.MeasurementError{ 88 | Message: "measurement did not complete successfully", 89 | } 90 | } 91 | return measurement, nil 92 | } 93 | 94 | func (app *App) OutputGlobalping(m *globalping.Measurement) error { 95 | // Disables colorized output if user specified. 96 | if !app.QueryFlags.Color { 97 | color.NoColor = true 98 | } 99 | 100 | table := tablewriter.NewWriter(color.Output) 101 | header := []string{"Location", "Name", "Type", "Class", "TTL", "Address", "Nameserver"} 102 | 103 | // Formatting options for the table. 104 | table.SetHeader(header) 105 | table.SetAutoWrapText(true) 106 | table.SetAutoFormatHeaders(true) 107 | table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) 108 | table.SetAlignment(tablewriter.ALIGN_LEFT) 109 | table.SetCenterSeparator("") 110 | table.SetColumnSeparator("") 111 | table.SetRowSeparator("") 112 | table.SetHeaderLine(false) 113 | table.SetBorder(false) 114 | table.SetTablePadding("\t") // pad with tabs 115 | table.SetNoWhiteSpace(true) 116 | 117 | for i := range m.Results { 118 | table.Append([]string{getGlobalPingLocationText(&m.Results[i]), "", "", "", "", "", ""}) 119 | answers, err := globalping.DecodeDNSAnswers(m.Results[i].Result.AnswersRaw) 120 | if err != nil { 121 | return err 122 | } 123 | resolver := m.Results[i].Result.Resolver 124 | for _, ans := range answers { 125 | typOut := getColoredType(ans.Type) 126 | output := []string{"", TerminalColorGreen(ans.Name), typOut, ans.Class, fmt.Sprintf("%ds", ans.TTL), ans.Value, resolver} 127 | table.Append(output) 128 | } 129 | } 130 | table.Render() 131 | return nil 132 | } 133 | 134 | func (app *App) OutputGlobalpingShort(m *globalping.Measurement) error { 135 | for i := range m.Results { 136 | fmt.Printf("%s\n", getGlobalPingLocationText(&m.Results[i])) 137 | answers, err := globalping.DecodeDNSAnswers(m.Results[i].Result.AnswersRaw) 138 | if err != nil { 139 | return err 140 | } 141 | for _, ans := range answers { 142 | fmt.Printf("%s\n", ans.Value) 143 | } 144 | } 145 | return nil 146 | } 147 | 148 | type GlobalpingOutputResponse struct { 149 | Location string `json:"location"` 150 | Answers []resolvers.Answer `json:"answers"` 151 | } 152 | 153 | func (app *App) OutputGlobalpingJSON(m *globalping.Measurement) error { 154 | jsonOutput := struct { 155 | Responses []GlobalpingOutputResponse `json:"responses"` 156 | }{ 157 | Responses: make([]GlobalpingOutputResponse, 0, len(m.Results)), 158 | } 159 | for i := range m.Results { 160 | jsonOutput.Responses = append(jsonOutput.Responses, GlobalpingOutputResponse{}) 161 | jsonOutput.Responses[i].Location = getGlobalPingLocationText(&m.Results[i]) 162 | answers, err := globalping.DecodeDNSAnswers(m.Results[i].Result.AnswersRaw) 163 | if err != nil { 164 | return err 165 | } 166 | resolver := m.Results[i].Result.Resolver 167 | for _, ans := range answers { 168 | jsonOutput.Responses[i].Answers = append(jsonOutput.Responses[i].Answers, resolvers.Answer{ 169 | Name: ans.Name, 170 | Type: ans.Type, 171 | Class: ans.Class, 172 | TTL: fmt.Sprintf("%ds", ans.TTL), 173 | Address: ans.Value, 174 | Nameserver: resolver, 175 | }) 176 | } 177 | } 178 | 179 | // Pretty print with 4 spaces. 180 | res, err := json.MarshalIndent(jsonOutput, "", " ") 181 | if err != nil { 182 | return err 183 | } 184 | fmt.Printf("%s\n", res) 185 | return nil 186 | } 187 | 188 | func parseGlobalpingLocations(from string) []globalping.Locations { 189 | if from == "" { 190 | return []globalping.Locations{ 191 | { 192 | Magic: "world", 193 | }, 194 | } 195 | } 196 | fromArr := strings.Split(from, ",") 197 | locations := make([]globalping.Locations, len(fromArr)) 198 | for i, v := range fromArr { 199 | locations[i] = globalping.Locations{ 200 | Magic: strings.TrimSpace(v), 201 | } 202 | } 203 | return locations 204 | } 205 | 206 | func getGlobalPingLocationText(m *globalping.ProbeMeasurement) string { 207 | state := "" 208 | if m.Probe.State != "" { 209 | state = " (" + m.Probe.State + ")" 210 | } 211 | return m.Probe.City + state + ", " + 212 | m.Probe.Country + ", " + 213 | m.Probe.Continent + ", " + 214 | m.Probe.Network + " " + 215 | "(AS" + fmt.Sprint(m.Probe.ASN) + ")" 216 | } 217 | 218 | // parses the resolver string and returns the hostname, port, and protocol. 219 | func parseGlobalpingResolver(nameservers []string) (string, int, string, error) { 220 | port := 53 221 | protocol := "udp" 222 | if len(nameservers) == 0 { 223 | return "", port, protocol, nil 224 | } 225 | 226 | if len(nameservers) > 1 { 227 | return "", 0, "", errors.New("only one resolver is allowed for globalping") 228 | } 229 | 230 | u, err := url.Parse(nameservers[0]) 231 | if err != nil { 232 | return "", 0, "", err 233 | } 234 | if u.Port() != "" { 235 | port, err = strconv.Atoi(u.Port()) 236 | if err != nil { 237 | return "", 0, "", err 238 | } 239 | } 240 | switch u.Scheme { 241 | case "tcp": 242 | protocol = "tcp" 243 | } 244 | 245 | return u.Hostname(), port, protocol, nil 246 | } 247 | -------------------------------------------------------------------------------- /internal/app/nameservers.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "net" 7 | "net/url" 8 | "strings" 9 | "time" 10 | 11 | "github.com/ameshkov/dnsstamps" 12 | "github.com/mr-karan/doggo/pkg/config" 13 | "github.com/mr-karan/doggo/pkg/models" 14 | ) 15 | 16 | func (app *App) LoadNameservers() error { 17 | app.Logger.Debug("LoadNameservers: Initial nameservers", "nameservers", app.QueryFlags.Nameservers) 18 | 19 | app.Nameservers = []models.Nameserver{} // Clear existing nameservers 20 | 21 | if len(app.QueryFlags.Nameservers) > 0 { 22 | for _, srv := range app.QueryFlags.Nameservers { 23 | ns, err := initNameserver(srv) 24 | if err != nil { 25 | app.Logger.Error("error parsing nameserver", "error", err) 26 | return fmt.Errorf("error parsing nameserver: %s", srv) 27 | } 28 | if ns.Address != "" && ns.Type != "" { 29 | app.Nameservers = append(app.Nameservers, ns) 30 | app.Logger.Debug("Added nameserver", "nameserver", ns) 31 | } 32 | } 33 | } 34 | 35 | // If no nameservers were successfully loaded, fall back to system nameservers 36 | if len(app.Nameservers) == 0 { 37 | return app.loadSystemNameservers() 38 | } 39 | 40 | app.Logger.Debug("LoadNameservers: Final nameservers", "nameservers", app.Nameservers) 41 | return nil 42 | } 43 | 44 | func (app *App) loadSystemNameservers() error { 45 | app.Logger.Debug("No user specified nameservers, falling back to system nameservers") 46 | ns, ndots, search, err := getDefaultServers(app.QueryFlags.Strategy) 47 | if err != nil { 48 | app.Logger.Error("error fetching system default nameserver", "error", err) 49 | return fmt.Errorf("error fetching system default nameserver: %v", err) 50 | } 51 | 52 | if app.ResolverOpts.Ndots == -1 { 53 | app.ResolverOpts.Ndots = ndots 54 | } 55 | 56 | if len(search) > 0 && app.QueryFlags.UseSearchList { 57 | app.ResolverOpts.SearchList = search 58 | } 59 | 60 | app.Nameservers = append(app.Nameservers, ns...) 61 | app.Logger.Debug("Loaded system nameservers", "nameservers", app.Nameservers) 62 | return nil 63 | } 64 | 65 | func initNameserver(n string) (models.Nameserver, error) { 66 | // If the nameserver doesn't have a protocol, assume it's UDP 67 | if !strings.Contains(n, "://") { 68 | n = "udp://" + n 69 | } 70 | 71 | u, err := url.Parse(n) 72 | if err != nil { 73 | return models.Nameserver{}, err 74 | } 75 | 76 | ns := models.Nameserver{ 77 | Type: models.UDPResolver, 78 | Address: getAddressWithDefaultPort(u, models.DefaultUDPPort), 79 | } 80 | 81 | switch u.Scheme { 82 | case "sdns": 83 | return handleSDNS(n) 84 | case "https": 85 | ns.Type = models.DOHResolver 86 | ns.Address = u.String() 87 | case "tls": 88 | ns.Type = models.DOTResolver 89 | ns.Address = getAddressWithDefaultPort(u, models.DefaultTLSPort) 90 | case "tcp": 91 | ns.Type = models.TCPResolver 92 | ns.Address = getAddressWithDefaultPort(u, models.DefaultTCPPort) 93 | case "udp": 94 | ns.Type = models.UDPResolver 95 | ns.Address = getAddressWithDefaultPort(u, models.DefaultUDPPort) 96 | case "quic": 97 | ns.Type = models.DOQResolver 98 | ns.Address = getAddressWithDefaultPort(u, models.DefaultDOQPort) 99 | default: 100 | return ns, fmt.Errorf("unsupported protocol: %s", u.Scheme) 101 | } 102 | 103 | return ns, nil 104 | } 105 | 106 | func getAddressWithDefaultPort(u *url.URL, defaultPort string) string { 107 | host := u.Hostname() 108 | port := u.Port() 109 | if port == "" { 110 | port = defaultPort 111 | } 112 | return net.JoinHostPort(host, port) 113 | } 114 | 115 | func handleSDNS(n string) (models.Nameserver, error) { 116 | stamp, err := dnsstamps.NewServerStampFromString(n) 117 | if err != nil { 118 | return models.Nameserver{}, err 119 | } 120 | 121 | switch stamp.Proto { 122 | case dnsstamps.StampProtoTypeDoH: 123 | address := url.URL{Scheme: "https", Host: stamp.ProviderName, Path: stamp.Path} 124 | return models.Nameserver{ 125 | Type: models.DOHResolver, 126 | Address: address.String(), 127 | }, nil 128 | case dnsstamps.StampProtoTypeDNSCrypt: 129 | return models.Nameserver{ 130 | Type: models.DNSCryptResolver, 131 | Address: n, 132 | }, nil 133 | default: 134 | return models.Nameserver{}, fmt.Errorf("unsupported protocol: %v", stamp.Proto.String()) 135 | } 136 | } 137 | 138 | func getDefaultServers(strategy string) ([]models.Nameserver, int, []string, error) { 139 | // Load nameservers from `/etc/resolv.conf`. 140 | dnsServers, ndots, search, err := config.GetDefaultServers() 141 | if err != nil { 142 | return nil, 0, nil, err 143 | } 144 | servers := make([]models.Nameserver, 0, len(dnsServers)) 145 | 146 | switch strategy { 147 | case "random": 148 | // Create a new local random source and generator. 149 | src := rand.NewSource(time.Now().UnixNano()) 150 | rnd := rand.New(src) 151 | 152 | // Choose a random server from the list. 153 | srv := dnsServers[rnd.Intn(len(dnsServers))] 154 | ns := models.Nameserver{ 155 | Type: models.UDPResolver, 156 | Address: net.JoinHostPort(srv, models.DefaultUDPPort), 157 | } 158 | servers = append(servers, ns) 159 | 160 | case "first": 161 | // Choose the first from the list, always. 162 | srv := dnsServers[0] 163 | ns := models.Nameserver{ 164 | Type: models.UDPResolver, 165 | Address: net.JoinHostPort(srv, models.DefaultUDPPort), 166 | } 167 | servers = append(servers, ns) 168 | 169 | default: 170 | // Default behaviour is to load all nameservers. 171 | for _, s := range dnsServers { 172 | ns := models.Nameserver{ 173 | Type: models.UDPResolver, 174 | Address: net.JoinHostPort(s, models.DefaultUDPPort), 175 | } 176 | servers = append(servers, ns) 177 | } 178 | } 179 | 180 | return servers, ndots, search, nil 181 | } 182 | -------------------------------------------------------------------------------- /internal/app/output.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/fatih/color" 9 | "github.com/miekg/dns" 10 | "github.com/mr-karan/doggo/pkg/resolvers" 11 | "github.com/olekukonko/tablewriter" 12 | ) 13 | 14 | var ( 15 | TerminalColorGreen = color.New(color.FgGreen, color.Bold).SprintFunc() 16 | TerminalColorBlue = color.New(color.FgBlue, color.Bold).SprintFunc() 17 | TerminalColorYellow = color.New(color.FgYellow, color.Bold).SprintFunc() 18 | TerminalColorCyan = color.New(color.FgCyan, color.Bold).SprintFunc() 19 | TerminalColorRed = color.New(color.FgRed, color.Bold).SprintFunc() 20 | TerminalColorMagenta = color.New(color.FgMagenta, color.Bold).SprintFunc() 21 | ) 22 | 23 | func (app *App) outputJSON(rsp []resolvers.Response) { 24 | jsonOutput := struct { 25 | Responses []resolvers.Response `json:"responses"` 26 | }{ 27 | Responses: rsp, 28 | } 29 | 30 | // Pretty print with 4 spaces. 31 | res, err := json.MarshalIndent(jsonOutput, "", " ") 32 | if err != nil { 33 | app.Logger.Error("unable to output data in JSON", "error", err) 34 | os.Exit(-1) 35 | } 36 | fmt.Printf("%s\n", res) 37 | } 38 | 39 | func (app *App) outputShort(rsp []resolvers.Response) { 40 | for _, r := range rsp { 41 | for _, a := range r.Answers { 42 | fmt.Printf("%s\n", a.Address) 43 | } 44 | } 45 | } 46 | 47 | func (app *App) outputTerminal(rsp []resolvers.Response) { 48 | // Disables colorized output if user specified. 49 | if !app.QueryFlags.Color { 50 | color.NoColor = true 51 | } 52 | 53 | // Conditional Time column. 54 | table := tablewriter.NewWriter(color.Output) 55 | header := []string{"Name", "Type", "Class", "TTL", "Address", "Nameserver"} 56 | if app.QueryFlags.DisplayTimeTaken { 57 | header = append(header, "Time Taken") 58 | } 59 | 60 | // Show output in case if it's not 61 | // a NOERROR. 62 | outputStatus := false 63 | for _, r := range rsp { 64 | for _, a := range r.Authorities { 65 | if dns.StringToRcode[a.Status] != dns.RcodeSuccess { 66 | outputStatus = true 67 | } 68 | } 69 | for _, a := range r.Answers { 70 | if dns.StringToRcode[a.Status] != dns.RcodeSuccess { 71 | outputStatus = true 72 | } 73 | } 74 | } 75 | if outputStatus { 76 | header = append(header, "Status") 77 | } 78 | 79 | // Formatting options for the table. 80 | table.SetHeader(header) 81 | table.SetAutoWrapText(true) 82 | table.SetAutoFormatHeaders(true) 83 | table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) 84 | table.SetAlignment(tablewriter.ALIGN_LEFT) 85 | table.SetCenterSeparator("") 86 | table.SetColumnSeparator("") 87 | table.SetRowSeparator("") 88 | table.SetHeaderLine(false) 89 | table.SetBorder(false) 90 | table.SetTablePadding("\t") // pad with tabs 91 | table.SetNoWhiteSpace(true) 92 | 93 | for _, r := range rsp { 94 | for _, ans := range r.Answers { 95 | typOut := getColoredType(ans.Type) 96 | output := []string{TerminalColorGreen(ans.Name), typOut, ans.Class, ans.TTL, ans.Address, ans.Nameserver} 97 | // Print how long it took 98 | if app.QueryFlags.DisplayTimeTaken { 99 | output = append(output, ans.RTT) 100 | } 101 | if outputStatus { 102 | output = append(output, TerminalColorRed(ans.Status)) 103 | } 104 | table.Append(output) 105 | } 106 | for _, auth := range r.Authorities { 107 | var typOut string 108 | switch typ := auth.Type; typ { 109 | case "SOA": 110 | typOut = TerminalColorRed(auth.Type) 111 | default: 112 | typOut = TerminalColorBlue(auth.Type) 113 | } 114 | output := []string{TerminalColorGreen(auth.Name), typOut, auth.Class, auth.TTL, auth.MName, auth.Nameserver} 115 | // Print how long it took 116 | if app.QueryFlags.DisplayTimeTaken { 117 | output = append(output, auth.RTT) 118 | } 119 | if outputStatus { 120 | output = append(output, TerminalColorRed(auth.Status)) 121 | } 122 | table.Append(output) 123 | } 124 | } 125 | table.Render() 126 | } 127 | 128 | func getColoredType(t string) string { 129 | switch t { 130 | case "A": 131 | return TerminalColorBlue(t) 132 | case "AAAA": 133 | return TerminalColorBlue(t) 134 | case "MX": 135 | return TerminalColorMagenta(t) 136 | case "NS": 137 | return TerminalColorCyan(t) 138 | case "CNAME": 139 | return TerminalColorYellow(t) 140 | case "TXT": 141 | return TerminalColorYellow(t) 142 | case "SOA": 143 | return TerminalColorRed(t) 144 | default: 145 | return TerminalColorBlue(t) 146 | } 147 | } 148 | 149 | // Output takes a list of `dns.Answers` and based 150 | // on the output format specified displays the information. 151 | func (app *App) Output(responses []resolvers.Response) { 152 | if app.QueryFlags.ShowJSON { 153 | app.outputJSON(responses) 154 | } else if app.QueryFlags.ShortOutput { 155 | app.outputShort(responses) 156 | } else { 157 | app.outputTerminal(responses) 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /internal/app/questions.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | 7 | "github.com/miekg/dns" 8 | "github.com/mr-karan/doggo/pkg/models" 9 | ) 10 | 11 | // LoadFallbacks sets fallbacks for options 12 | // that are not specified by the user but necessary 13 | // for the resolver. 14 | func (app *App) LoadFallbacks() { 15 | if app.QueryFlags.QueryAny { 16 | app.QueryFlags.QTypes = models.GetCommonRecordTypes() 17 | } else if len(app.QueryFlags.QTypes) == 0 { 18 | app.QueryFlags.QTypes = append(app.QueryFlags.QTypes, "A") 19 | } 20 | if len(app.QueryFlags.QClasses) == 0 { 21 | app.QueryFlags.QClasses = append(app.QueryFlags.QClasses, "IN") 22 | } 23 | } 24 | 25 | // PrepareQuestions takes a list of query names, query types and query classes 26 | // and prepare a question for each combination of the above. 27 | func (app *App) PrepareQuestions() { 28 | for _, n := range app.QueryFlags.QNames { 29 | for _, t := range app.QueryFlags.QTypes { 30 | for _, c := range app.QueryFlags.QClasses { 31 | app.Questions = append(app.Questions, dns.Question{ 32 | Name: n, 33 | Qtype: dns.StringToType[strings.ToUpper(t)], 34 | Qclass: dns.StringToClass[strings.ToUpper(c)], 35 | }) 36 | } 37 | } 38 | } 39 | } 40 | 41 | // ReverseLookup is used to perform a reverse DNS Lookup 42 | // using an IPv4 or IPv6 address. 43 | // Query Type is set to PTR, Query Class is set to IN. 44 | // Query Names must be formatted in in-addr.arpa. or ip6.arpa format. 45 | func (app *App) ReverseLookup() { 46 | app.QueryFlags.QTypes = []string{"PTR"} 47 | app.QueryFlags.QClasses = []string{"IN"} 48 | formattedNames := make([]string, 0, len(app.QueryFlags.QNames)) 49 | 50 | for _, n := range app.QueryFlags.QNames { 51 | addr, err := dns.ReverseAddr(n) 52 | if err != nil { 53 | app.Logger.Error("error formatting address", "error", err) 54 | os.Exit(2) 55 | } 56 | formattedNames = append(formattedNames, addr) 57 | } 58 | app.QueryFlags.QNames = formattedNames 59 | } 60 | -------------------------------------------------------------------------------- /pkg/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "net" 4 | 5 | // the whole `FEC0::/10` prefix is deprecated. 6 | // [RFC 3879]: https://tools.ietf.org/html/rfc3879 7 | func isUnicastLinkLocal(ip net.IP) bool { 8 | return len(ip) == net.IPv6len && ip[0] == 0xfe && ip[1] == 0xc0 9 | } 10 | -------------------------------------------------------------------------------- /pkg/config/config_unix.go: -------------------------------------------------------------------------------- 1 | // +build !windows 2 | 3 | package config 4 | 5 | import ( 6 | "net" 7 | 8 | "github.com/miekg/dns" 9 | ) 10 | 11 | // DefaultResolvConfPath specifies path to default resolv config file on UNIX. 12 | var DefaultResolvConfPath = "/etc/resolv.conf" 13 | 14 | // GetDefaultServers get system default nameserver 15 | func GetDefaultServers() ([]string, int, []string, error) { 16 | // if no nameserver is provided, take it from `resolv.conf` 17 | cfg, err := dns.ClientConfigFromFile(DefaultResolvConfPath) 18 | if err != nil { 19 | return nil, 0, nil, err 20 | } 21 | servers := make([]string, 0) 22 | for _, server := range cfg.Servers { 23 | ip := net.ParseIP(server) 24 | if isUnicastLinkLocal(ip) { 25 | continue 26 | } 27 | servers = append(servers, server) 28 | } 29 | return servers, cfg.Ndots, cfg.Search, nil 30 | } 31 | -------------------------------------------------------------------------------- /pkg/config/config_windows.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "os" 5 | "syscall" 6 | "unsafe" 7 | 8 | "golang.org/x/sys/windows" 9 | ) 10 | 11 | // GAA_FLAG_INCLUDE_GATEWAYS Return the addresses of default gateways. 12 | // This flag is supported on Windows Vista and later. 13 | const GAA_FLAG_INCLUDE_GATEWAYS = 0x00000080 14 | 15 | // IpAdapterWinsServerAddress structure in a linked list of Windows Internet Name Service (WINS) server addresses for the adapter. 16 | type IpAdapterWinsServerAddress struct { 17 | Length uint32 18 | _ uint32 19 | Next *IpAdapterWinsServerAddress 20 | Address windows.SocketAddress 21 | } 22 | 23 | // IpAdapterGatewayAddress structure in a linked list of gateways for the adapter. 24 | type IpAdapterGatewayAddress struct { 25 | Length uint32 26 | _ uint32 27 | Next *IpAdapterGatewayAddress 28 | Address windows.SocketAddress 29 | } 30 | 31 | // IpAdapterAddresses structure is the header node for a linked list of addresses for a particular adapter. 32 | // This structure can simultaneously be used as part of a linked list of IP_ADAPTER_ADDRESSES structures. 33 | type IpAdapterAddresses struct { 34 | Length uint32 35 | IfIndex uint32 36 | Next *IpAdapterAddresses 37 | AdapterName *byte 38 | FirstUnicastAddress *windows.IpAdapterUnicastAddress 39 | FirstAnycastAddress *windows.IpAdapterAnycastAddress 40 | FirstMulticastAddress *windows.IpAdapterMulticastAddress 41 | FirstDnsServerAddress *windows.IpAdapterDnsServerAdapter 42 | DnsSuffix *uint16 43 | Description *uint16 44 | FriendlyName *uint16 45 | PhysicalAddress [syscall.MAX_ADAPTER_ADDRESS_LENGTH]byte 46 | PhysicalAddressLength uint32 47 | Flags uint32 48 | Mtu uint32 49 | IfType uint32 50 | OperStatus uint32 51 | Ipv6IfIndex uint32 52 | ZoneIndices [16]uint32 53 | FirstPrefix *windows.IpAdapterPrefix 54 | /* more fields might be present here. */ 55 | TransmitLinkSpeed uint64 56 | ReceiveLinkSpeed uint64 57 | FirstWinsServerAddress *IpAdapterWinsServerAddress 58 | FirstGatewayAddress *IpAdapterGatewayAddress 59 | } 60 | 61 | func adapterAddresses() ([]*IpAdapterAddresses, error) { 62 | var b []byte 63 | // https://docs.microsoft.com/en-us/windows/win32/api/iphlpapi/nf-iphlpapi-getadaptersaddresses 64 | // #define WORKING_BUFFER_SIZE 15000 65 | l := uint32(15000) 66 | for { 67 | b = make([]byte, l) 68 | err := windows.GetAdaptersAddresses(syscall.AF_UNSPEC, GAA_FLAG_INCLUDE_GATEWAYS|windows.GAA_FLAG_INCLUDE_PREFIX, 0, (*windows.IpAdapterAddresses)(unsafe.Pointer(&b[0])), &l) 69 | if err == nil { 70 | if l == 0 { 71 | return nil, nil 72 | } 73 | break 74 | } 75 | if err.(syscall.Errno) != syscall.ERROR_BUFFER_OVERFLOW { 76 | return nil, os.NewSyscallError("getadaptersaddresses", err) 77 | } 78 | if l <= uint32(len(b)) { 79 | return nil, os.NewSyscallError("getadaptersaddresses", err) 80 | } 81 | } 82 | aas := make([]*IpAdapterAddresses, 0, uintptr(l)/unsafe.Sizeof(IpAdapterAddresses{})) 83 | for aa := (*IpAdapterAddresses)(unsafe.Pointer(&b[0])); aa != nil; aa = aa.Next { 84 | aas = append(aas, aa) 85 | } 86 | return aas, nil 87 | } 88 | 89 | func getDefaultDNSServers() ([]string, error) { 90 | ifs, err := adapterAddresses() 91 | if err != nil { 92 | return nil, err 93 | } 94 | dnsServers := make([]string, 0) 95 | for _, ifi := range ifs { 96 | if ifi.OperStatus != windows.IfOperStatusUp { 97 | continue 98 | } 99 | 100 | if ifi.FirstGatewayAddress == nil { 101 | continue 102 | } 103 | 104 | for dnsServer := ifi.FirstDnsServerAddress; dnsServer != nil; dnsServer = dnsServer.Next { 105 | ip := dnsServer.Address.IP() 106 | if isUnicastLinkLocal(ip) { 107 | continue 108 | } 109 | dnsServers = append(dnsServers, ip.String()) 110 | } 111 | } 112 | return dnsServers, nil 113 | } 114 | 115 | // GetDefaultServers get system default nameserver 116 | func GetDefaultServers() ([]string, int, []string, error) { 117 | // TODO: DNS Suffix 118 | servers, err := getDefaultDNSServers() 119 | return servers, 0, nil, err 120 | } 121 | -------------------------------------------------------------------------------- /pkg/models/models.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "strings" 5 | "time" 6 | ) 7 | 8 | const ( 9 | // DefaultTLSPort specifies the default port for a DNS server connecting over TCP over TLS. 10 | DefaultTLSPort = "853" 11 | // DefaultUDPPort specifies the default port for a DNS server connecting over UDP. 12 | DefaultUDPPort = "53" 13 | // DefaultTCPPort specifies the default port for a DNS server connecting over TCP. 14 | DefaultTCPPort = "53" 15 | // DefaultDOQPort specifies the default port for a DNS server connecting over DNS over QUIC. 16 | DefaultDOQPort = "853" 17 | UDPResolver = "udp" 18 | DOHResolver = "doh" 19 | TCPResolver = "tcp" 20 | DOTResolver = "dot" 21 | DNSCryptResolver = "dnscrypt" 22 | DOQResolver = "doq" 23 | // CommonRecordTypes is a string containing all common DNS record types 24 | CommonRecordTypes = "A AAAA CNAME MX NS PTR SOA SRV TXT CAA" 25 | ) 26 | 27 | // QueryFlags is used store the query params 28 | // supplied by the user. 29 | type QueryFlags struct { 30 | QNames []string `koanf:"query" json:"query"` 31 | QTypes []string `koanf:"type" json:"type"` 32 | QClasses []string `koanf:"class" json:"class"` 33 | Nameservers []string `koanf:"nameservers" json:"nameservers"` 34 | UseIPv4 bool `koanf:"ipv4" json:"ipv4"` 35 | UseIPv6 bool `koanf:"ipv6" json:"ipv6"` 36 | Ndots int `koanf:"ndots" json:"ndots"` 37 | Timeout time.Duration `koanf:"timeout" json:"timeout"` 38 | Color bool `koanf:"color" json:"-"` 39 | DisplayTimeTaken bool `koanf:"time" json:"-"` 40 | ShowJSON bool `koanf:"json" json:"-"` 41 | ShortOutput bool `koanf:"short" short:"-"` 42 | UseSearchList bool `koanf:"search" json:"-"` 43 | ReverseLookup bool `koanf:"reverse" reverse:"-"` 44 | Strategy string `koanf:"strategy" strategy:"-"` 45 | InsecureSkipVerify bool `koanf:"skip-hostname-verification" skip-hostname-verification:"-"` 46 | TLSHostname string `koanf:"tls-hostname" tls-hostname:"-"` 47 | QueryAny bool `koanf:"any" json:"any"` 48 | 49 | // Globalping flags 50 | GPFrom string `koanf:"gp-from" json:"gp-from"` 51 | GPLimit int `koanf:"gp-limit" json:"gp-limit"` 52 | } 53 | 54 | // Nameserver represents the type of Nameserver 55 | // along with the server address. 56 | type Nameserver struct { 57 | Address string 58 | Type string 59 | } 60 | 61 | // GetCommonRecordTypes returns a slice of common DNS record types 62 | func GetCommonRecordTypes() []string { 63 | return strings.Fields(CommonRecordTypes) 64 | } 65 | -------------------------------------------------------------------------------- /pkg/resolvers/classic.go: -------------------------------------------------------------------------------- 1 | package resolvers 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "time" 7 | 8 | "github.com/miekg/dns" 9 | ) 10 | 11 | // ClassicResolver represents the config options for setting up a Resolver. 12 | type ClassicResolver struct { 13 | client *dns.Client 14 | server string 15 | resolverOptions Options 16 | } 17 | 18 | // ClassicResolverOpts holds options for setting up a Classic resolver. 19 | type ClassicResolverOpts struct { 20 | UseTLS bool 21 | UseTCP bool 22 | } 23 | 24 | // NewClassicResolver accepts a list of nameservers and configures a DNS resolver. 25 | func NewClassicResolver(server string, classicOpts ClassicResolverOpts, resolverOpts Options) (Resolver, error) { 26 | net := "udp" 27 | client := &dns.Client{ 28 | Timeout: resolverOpts.Timeout, 29 | Net: "udp", 30 | } 31 | 32 | if classicOpts.UseTCP { 33 | net = "tcp" 34 | } 35 | 36 | if resolverOpts.UseIPv4 { 37 | net = net + "4" 38 | } 39 | if resolverOpts.UseIPv6 { 40 | net = net + "6" 41 | } 42 | 43 | if classicOpts.UseTLS { 44 | net = net + "-tls" 45 | // Provide extra TLS config for doing/skipping hostname verification. 46 | client.TLSConfig = &tls.Config{ 47 | ServerName: resolverOpts.TLSHostname, 48 | InsecureSkipVerify: resolverOpts.InsecureSkipVerify, 49 | } 50 | } 51 | 52 | client.Net = net 53 | 54 | return &ClassicResolver{ 55 | client: client, 56 | server: server, 57 | resolverOptions: resolverOpts, 58 | }, nil 59 | } 60 | 61 | // query takes a dns.Question and sends them to DNS Server. 62 | // It parses the Response from the server in a custom output format. 63 | func (r *ClassicResolver) query(ctx context.Context, question dns.Question, flags QueryFlags) (Response, error) { 64 | var ( 65 | rsp Response 66 | messages = prepareMessages(question, flags, r.resolverOptions.Ndots, r.resolverOptions.SearchList) 67 | ) 68 | for _, msg := range messages { 69 | r.resolverOptions.Logger.Debug("Attempting to resolve", 70 | "domain", msg.Question[0].Name, 71 | "ndots", r.resolverOptions.Ndots, 72 | "nameserver", r.server, 73 | ) 74 | 75 | // Since the library doesn't include tcp.Dial time, 76 | // it's better to not rely on `rtt` provided here and calculate it ourselves. 77 | now := time.Now() 78 | 79 | in, _, err := r.client.ExchangeContext(ctx, &msg, r.server) 80 | if err != nil { 81 | if err == context.Canceled || err == context.DeadlineExceeded { 82 | return rsp, err 83 | } 84 | return rsp, err 85 | } 86 | 87 | // In case the response size exceeds 512 bytes (can happen with lot of TXT records), 88 | // fallback to TCP as with UDP the response is truncated. Fallback mechanism is in-line with `dig`. 89 | if in.Truncated { 90 | switch r.client.Net { 91 | case "udp": 92 | r.client.Net = "tcp" 93 | case "udp4": 94 | r.client.Net = "tcp4" 95 | case "udp6": 96 | r.client.Net = "tcp6" 97 | default: 98 | r.client.Net = "tcp" 99 | } 100 | r.resolverOptions.Logger.Debug("Response truncated; retrying now", "protocol", r.client.Net) 101 | return r.query(ctx, question, flags) 102 | } 103 | 104 | // Pack questions in output. 105 | for _, q := range msg.Question { 106 | ques := Question{ 107 | Name: q.Name, 108 | Class: dns.ClassToString[q.Qclass], 109 | Type: dns.TypeToString[q.Qtype], 110 | } 111 | rsp.Questions = append(rsp.Questions, ques) 112 | } 113 | rtt := time.Since(now) 114 | 115 | // Get the authorities and answers. 116 | output := parseMessage(in, rtt, r.server) 117 | rsp.Authorities = output.Authorities 118 | rsp.Answers = output.Answers 119 | 120 | if len(output.Answers) > 0 { 121 | // Stop iterating the searchlist. 122 | break 123 | } 124 | 125 | // Check if context is done after each iteration 126 | select { 127 | case <-ctx.Done(): 128 | return rsp, ctx.Err() 129 | default: 130 | // Continue to next iteration 131 | } 132 | } 133 | return rsp, nil 134 | } 135 | 136 | // Lookup implements the Resolver interface 137 | func (r *ClassicResolver) Lookup(ctx context.Context, questions []dns.Question, flags QueryFlags) ([]Response, error) { 138 | return ConcurrentLookup(ctx, questions, flags, r.query, r.resolverOptions.Logger) 139 | } 140 | -------------------------------------------------------------------------------- /pkg/resolvers/common.go: -------------------------------------------------------------------------------- 1 | package resolvers 2 | 3 | import ( 4 | "context" 5 | "log/slog" 6 | "sync" 7 | 8 | "github.com/miekg/dns" 9 | ) 10 | 11 | // QueryFunc represents the signature of a query function 12 | type QueryFunc func(ctx context.Context, question dns.Question, flags QueryFlags) (Response, error) 13 | 14 | // ConcurrentLookup performs concurrent DNS lookups 15 | func ConcurrentLookup(ctx context.Context, questions []dns.Question, flags QueryFlags, queryFunc QueryFunc, logger *slog.Logger) ([]Response, error) { 16 | var wg sync.WaitGroup 17 | responses := make([]Response, len(questions)) 18 | errors := make([]error, len(questions)) 19 | done := make(chan struct{}) 20 | 21 | for i, q := range questions { 22 | wg.Add(1) 23 | go func(i int, q dns.Question) { 24 | defer wg.Done() 25 | select { 26 | case <-ctx.Done(): 27 | errors[i] = ctx.Err() 28 | default: 29 | resp, err := queryFunc(ctx, q, flags) 30 | responses[i] = resp 31 | errors[i] = err 32 | } 33 | }(i, q) 34 | } 35 | 36 | go func() { 37 | wg.Wait() 38 | close(done) 39 | }() 40 | 41 | select { 42 | case <-ctx.Done(): 43 | return nil, ctx.Err() 44 | case <-done: 45 | // All goroutines have finished 46 | } 47 | 48 | // Collect non-nil responses and handle errors 49 | var validResponses []Response 50 | for i, resp := range responses { 51 | if errors[i] != nil { 52 | if errors[i] != context.Canceled && errors[i] != context.DeadlineExceeded { 53 | logger.Error("error in lookup", "error", errors[i]) 54 | } 55 | } else { 56 | validResponses = append(validResponses, resp) 57 | } 58 | } 59 | 60 | if len(validResponses) == 0 && ctx.Err() != nil { 61 | return nil, ctx.Err() 62 | } 63 | 64 | return validResponses, nil 65 | } 66 | -------------------------------------------------------------------------------- /pkg/resolvers/dnscrypt.go: -------------------------------------------------------------------------------- 1 | package resolvers 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/ameshkov/dnscrypt/v2" 8 | "github.com/miekg/dns" 9 | ) 10 | 11 | // DNSCryptResolver represents the config options for setting up a Resolver. 12 | type DNSCryptResolver struct { 13 | client *dnscrypt.Client 14 | server string 15 | resolverInfo *dnscrypt.ResolverInfo 16 | resolverOptions Options 17 | } 18 | 19 | // DNSCryptResolverOpts holds options for setting up a DNSCrypt resolver. 20 | type DNSCryptResolverOpts struct { 21 | UseTCP bool 22 | } 23 | 24 | // NewDNSCryptResolver accepts a list of nameservers and configures a DNS resolver. 25 | func NewDNSCryptResolver(server string, dnscryptOpts DNSCryptResolverOpts, resolverOpts Options) (Resolver, error) { 26 | net := "udp" 27 | if dnscryptOpts.UseTCP { 28 | net = "tcp" 29 | } 30 | 31 | client := &dnscrypt.Client{Net: net, Timeout: resolverOpts.Timeout, UDPSize: 4096} 32 | resolverInfo, err := client.Dial(server) 33 | if err != nil { 34 | return nil, err 35 | } 36 | return &DNSCryptResolver{ 37 | client: client, 38 | resolverInfo: resolverInfo, 39 | server: resolverInfo.ServerAddress, 40 | resolverOptions: resolverOpts, 41 | }, nil 42 | } 43 | 44 | // Lookup implements the Resolver interface 45 | func (r *DNSCryptResolver) Lookup(ctx context.Context, questions []dns.Question, flags QueryFlags) ([]Response, error) { 46 | return ConcurrentLookup(ctx, questions, flags, r.query, r.resolverOptions.Logger) 47 | } 48 | 49 | // query performs a single DNS query 50 | func (r *DNSCryptResolver) query(ctx context.Context, question dns.Question, flags QueryFlags) (Response, error) { 51 | var ( 52 | rsp Response 53 | messages = prepareMessages(question, flags, r.resolverOptions.Ndots, r.resolverOptions.SearchList) 54 | ) 55 | for _, msg := range messages { 56 | r.resolverOptions.Logger.Debug("Attempting to resolve", 57 | "domain", msg.Question[0].Name, 58 | "ndots", r.resolverOptions.Ndots, 59 | "nameserver", r.server, 60 | ) 61 | 62 | now := time.Now() 63 | 64 | // Use a channel to handle the result of the Exchange 65 | resultChan := make(chan struct { 66 | resp *dns.Msg 67 | err error 68 | }) 69 | 70 | go func() { 71 | resp, err := r.client.Exchange(&msg, r.resolverInfo) 72 | resultChan <- struct { 73 | resp *dns.Msg 74 | err error 75 | }{resp, err} 76 | }() 77 | 78 | // Wait for either the query to complete or the context to be cancelled 79 | select { 80 | case result := <-resultChan: 81 | if result.err != nil { 82 | return rsp, result.err 83 | } 84 | in := result.resp 85 | rtt := time.Since(now) 86 | 87 | // pack questions in output. 88 | for _, q := range msg.Question { 89 | ques := Question{ 90 | Name: q.Name, 91 | Class: dns.ClassToString[q.Qclass], 92 | Type: dns.TypeToString[q.Qtype], 93 | } 94 | rsp.Questions = append(rsp.Questions, ques) 95 | } 96 | // get the authorities and answers. 97 | output := parseMessage(in, rtt, r.server) 98 | rsp.Authorities = output.Authorities 99 | rsp.Answers = output.Answers 100 | 101 | if len(output.Answers) > 0 { 102 | // stop iterating the searchlist. 103 | return rsp, nil 104 | } 105 | case <-ctx.Done(): 106 | return rsp, ctx.Err() 107 | } 108 | } 109 | return rsp, nil 110 | } 111 | -------------------------------------------------------------------------------- /pkg/resolvers/doh.go: -------------------------------------------------------------------------------- 1 | package resolvers 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "crypto/tls" 7 | "encoding/base64" 8 | "fmt" 9 | "io" 10 | "net/http" 11 | "net/url" 12 | "time" 13 | 14 | "github.com/miekg/dns" 15 | ) 16 | 17 | // DOHResolver represents the config options for setting up a DOH based resolver. 18 | type DOHResolver struct { 19 | client *http.Client 20 | server string 21 | resolverOptions Options 22 | } 23 | 24 | // NewDOHResolver accepts a nameserver address and configures a DOH based resolver. 25 | func NewDOHResolver(server string, resolverOpts Options) (Resolver, error) { 26 | // do basic validation 27 | u, err := url.ParseRequestURI(server) 28 | if err != nil { 29 | return nil, fmt.Errorf("%s is not a valid HTTPS nameserver", server) 30 | } 31 | if u.Scheme != "https" { 32 | return nil, fmt.Errorf("missing https in %s", server) 33 | } 34 | transport := http.DefaultTransport.(*http.Transport).Clone() 35 | transport.TLSClientConfig = &tls.Config{ 36 | ServerName: resolverOpts.TLSHostname, 37 | InsecureSkipVerify: resolverOpts.InsecureSkipVerify, 38 | } 39 | httpClient := &http.Client{ 40 | Timeout: resolverOpts.Timeout, 41 | Transport: transport, 42 | } 43 | return &DOHResolver{ 44 | client: httpClient, 45 | server: server, 46 | resolverOptions: resolverOpts, 47 | }, nil 48 | } 49 | 50 | // query takes a dns.Question and sends them to DNS Server. 51 | // It parses the Response from the server in a custom output format. 52 | func (r *DOHResolver) query(ctx context.Context, question dns.Question, flags QueryFlags) (Response, error) { 53 | var ( 54 | rsp Response 55 | messages = prepareMessages(question, flags, r.resolverOptions.Ndots, r.resolverOptions.SearchList) 56 | ) 57 | 58 | for _, msg := range messages { 59 | r.resolverOptions.Logger.Debug("Attempting to resolve", 60 | "domain", msg.Question[0].Name, 61 | "ndots", r.resolverOptions.Ndots, 62 | "nameserver", r.server, 63 | ) 64 | // get the DNS Message in wire format. 65 | b, err := msg.Pack() 66 | if err != nil { 67 | return rsp, err 68 | } 69 | now := time.Now() 70 | 71 | // Create a new request with the context 72 | req, err := http.NewRequestWithContext(ctx, "POST", r.server, bytes.NewBuffer(b)) 73 | if err != nil { 74 | return rsp, err 75 | } 76 | req.Header.Set("Content-Type", "application/dns-message") 77 | 78 | // Make an HTTP POST request to the DNS server with the DNS message as wire format bytes in the body. 79 | resp, err := r.client.Do(req) 80 | if err != nil { 81 | return rsp, err 82 | } 83 | defer resp.Body.Close() 84 | 85 | if resp.StatusCode == http.StatusMethodNotAllowed { 86 | url, err := url.Parse(r.server) 87 | if err != nil { 88 | return rsp, err 89 | } 90 | url.RawQuery = fmt.Sprintf("dns=%v", base64.RawURLEncoding.EncodeToString(b)) 91 | 92 | req, err = http.NewRequestWithContext(ctx, "GET", url.String(), nil) 93 | if err != nil { 94 | return rsp, err 95 | } 96 | resp, err = r.client.Do(req) 97 | if err != nil { 98 | return rsp, err 99 | } 100 | defer resp.Body.Close() 101 | } 102 | if resp.StatusCode != http.StatusOK { 103 | return rsp, fmt.Errorf("error from nameserver %s", resp.Status) 104 | } 105 | rtt := time.Since(now) 106 | 107 | // if debug, extract the response headers 108 | for header, value := range resp.Header { 109 | r.resolverOptions.Logger.Debug("DOH response header", header, value) 110 | } 111 | 112 | // extract the binary response in DNS Message. 113 | body, err := io.ReadAll(resp.Body) 114 | if err != nil { 115 | return rsp, err 116 | } 117 | 118 | err = msg.Unpack(body) 119 | if err != nil { 120 | return rsp, err 121 | } 122 | // pack questions in output. 123 | for _, q := range msg.Question { 124 | ques := Question{ 125 | Name: q.Name, 126 | Class: dns.ClassToString[q.Qclass], 127 | Type: dns.TypeToString[q.Qtype], 128 | } 129 | rsp.Questions = append(rsp.Questions, ques) 130 | } 131 | // get the authorities and answers. 132 | output := parseMessage(&msg, rtt, r.server) 133 | rsp.Authorities = output.Authorities 134 | rsp.Answers = output.Answers 135 | 136 | if len(output.Answers) > 0 { 137 | // stop iterating the searchlist. 138 | break 139 | } 140 | 141 | // Check if context is done after each iteration 142 | select { 143 | case <-ctx.Done(): 144 | return rsp, ctx.Err() 145 | default: 146 | // Continue to next iteration 147 | } 148 | } 149 | return rsp, nil 150 | } 151 | 152 | // Lookup implements the Resolver interface 153 | func (r *DOHResolver) Lookup(ctx context.Context, questions []dns.Question, flags QueryFlags) ([]Response, error) { 154 | return ConcurrentLookup(ctx, questions, flags, r.query, r.resolverOptions.Logger) 155 | } 156 | -------------------------------------------------------------------------------- /pkg/resolvers/doq.go: -------------------------------------------------------------------------------- 1 | package resolvers 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "encoding/binary" 7 | "fmt" 8 | "io" 9 | "time" 10 | 11 | "github.com/miekg/dns" 12 | "github.com/quic-go/quic-go" 13 | ) 14 | 15 | // DOQResolver represents the config options for setting up a DOQ based resolver. 16 | type DOQResolver struct { 17 | tls *tls.Config 18 | server string 19 | resolverOptions Options 20 | } 21 | 22 | // NewDOQResolver accepts a nameserver address and configures a DOQ based resolver. 23 | func NewDOQResolver(server string, resolverOpts Options) (Resolver, error) { 24 | return &DOQResolver{ 25 | tls: &tls.Config{ 26 | NextProtos: []string{"doq"}, 27 | ServerName: resolverOpts.TLSHostname, 28 | InsecureSkipVerify: resolverOpts.InsecureSkipVerify, 29 | }, 30 | server: server, 31 | resolverOptions: resolverOpts, 32 | }, nil 33 | } 34 | 35 | // Lookup implements the Resolver interface 36 | func (r *DOQResolver) Lookup(ctx context.Context, questions []dns.Question, flags QueryFlags) ([]Response, error) { 37 | return ConcurrentLookup(ctx, questions, flags, r.query, r.resolverOptions.Logger) 38 | } 39 | 40 | // query takes a dns.Question and sends them to DNS Server. 41 | // It parses the Response from the server in a custom output format. 42 | func (r *DOQResolver) query(ctx context.Context, question dns.Question, flags QueryFlags) (Response, error) { 43 | var ( 44 | rsp Response 45 | messages = prepareMessages(question, flags, r.resolverOptions.Ndots, r.resolverOptions.SearchList) 46 | ) 47 | 48 | session, err := quic.DialAddr(ctx, r.server, r.tls, nil) 49 | if err != nil { 50 | return rsp, err 51 | } 52 | defer session.CloseWithError(quic.ApplicationErrorCode(quic.NoError), "") 53 | 54 | for _, msg := range messages { 55 | r.resolverOptions.Logger.Debug("Attempting to resolve", 56 | "domain", msg.Question[0].Name, 57 | "ndots", r.resolverOptions.Ndots, 58 | "nameserver", r.server, 59 | ) 60 | 61 | // ref: https://www.rfc-editor.org/rfc/rfc9250.html#name-dns-message-ids 62 | msg.Id = 0 63 | 64 | // get the DNS Message in wire format. 65 | b, err := msg.Pack() 66 | if err != nil { 67 | return rsp, err 68 | } 69 | now := time.Now() 70 | 71 | stream, err := session.OpenStreamSync(ctx) 72 | if err != nil { 73 | return rsp, err 74 | } 75 | defer stream.Close() 76 | 77 | msgLen := uint16(len(b)) 78 | msgLenBytes := []byte{byte(msgLen >> 8), byte(msgLen & 0xFF)} 79 | if _, err = stream.Write(msgLenBytes); err != nil { 80 | return rsp, err 81 | } 82 | // Make a QUIC request to the DNS server with the DNS message as wire format bytes in the body. 83 | if _, err = stream.Write(b); err != nil { 84 | return rsp, err 85 | } 86 | 87 | // Use a separate context with timeout for reading the response 88 | readCtx, cancel := context.WithTimeout(ctx, r.resolverOptions.Timeout) 89 | defer cancel() 90 | 91 | var buf []byte 92 | errChan := make(chan error, 1) 93 | go func() { 94 | var err error 95 | buf, err = io.ReadAll(stream) 96 | errChan <- err 97 | }() 98 | 99 | select { 100 | case err := <-errChan: 101 | if err != nil { 102 | return rsp, err 103 | } 104 | case <-readCtx.Done(): 105 | return rsp, fmt.Errorf("timeout reading response") 106 | } 107 | 108 | rtt := time.Since(now) 109 | 110 | packetLen := binary.BigEndian.Uint16(buf[:2]) 111 | if packetLen != uint16(len(buf[2:])) { 112 | return rsp, fmt.Errorf("packet length mismatch") 113 | } 114 | if err = msg.Unpack(buf[2:]); err != nil { 115 | return rsp, err 116 | } 117 | // pack questions in output. 118 | for _, q := range msg.Question { 119 | ques := Question{ 120 | Name: q.Name, 121 | Class: dns.ClassToString[q.Qclass], 122 | Type: dns.TypeToString[q.Qtype], 123 | } 124 | rsp.Questions = append(rsp.Questions, ques) 125 | } 126 | // get the authorities and answers. 127 | output := parseMessage(&msg, rtt, r.server) 128 | rsp.Authorities = output.Authorities 129 | rsp.Answers = output.Answers 130 | 131 | if len(output.Answers) > 0 { 132 | // stop iterating the searchlist. 133 | break 134 | } 135 | 136 | // Check if context is done after each iteration 137 | select { 138 | case <-ctx.Done(): 139 | return rsp, ctx.Err() 140 | default: 141 | // Continue to next iteration 142 | } 143 | } 144 | return rsp, nil 145 | } 146 | -------------------------------------------------------------------------------- /pkg/resolvers/resolver.go: -------------------------------------------------------------------------------- 1 | package resolvers 2 | 3 | import ( 4 | "context" 5 | "log/slog" 6 | "time" 7 | 8 | "github.com/miekg/dns" 9 | "github.com/mr-karan/doggo/pkg/models" 10 | ) 11 | 12 | // Options represent a set of common options 13 | // to configure a Resolver. 14 | type Options struct { 15 | Logger *slog.Logger 16 | 17 | Nameservers []models.Nameserver 18 | UseIPv4 bool 19 | UseIPv6 bool 20 | SearchList []string 21 | Ndots int 22 | Timeout time.Duration 23 | Strategy string 24 | InsecureSkipVerify bool 25 | TLSHostname string 26 | } 27 | 28 | // Resolver implements the configuration for a DNS 29 | // Client. Different types of providers can load 30 | // a DNS Resolver satisfying this interface. 31 | type Resolver interface { 32 | Lookup(ctx context.Context, questions []dns.Question, flags QueryFlags) ([]Response, error) 33 | } 34 | 35 | // Response represents a custom output format 36 | // for DNS queries. It wraps metadata about the DNS query 37 | // and the DNS Answer as well. 38 | type Response struct { 39 | Answers []Answer `json:"answers"` 40 | Authorities []Authority `json:"authorities"` 41 | Questions []Question `json:"questions"` 42 | } 43 | 44 | type Question struct { 45 | Name string `json:"name"` 46 | Type string `json:"type"` 47 | Class string `json:"class"` 48 | } 49 | 50 | type Answer struct { 51 | Name string `json:"name"` 52 | Type string `json:"type"` 53 | Class string `json:"class"` 54 | TTL string `json:"ttl"` 55 | Address string `json:"address"` 56 | Status string `json:"status"` 57 | RTT string `json:"rtt"` 58 | Nameserver string `json:"nameserver"` 59 | } 60 | 61 | type Authority struct { 62 | Name string `json:"name"` 63 | Type string `json:"type"` 64 | Class string `json:"class"` 65 | TTL string `json:"ttl"` 66 | MName string `json:"mname"` 67 | Status string `json:"status"` 68 | RTT string `json:"rtt"` 69 | Nameserver string `json:"nameserver"` 70 | } 71 | 72 | // LoadResolvers loads differently configured 73 | // resolvers based on a list of nameserver. 74 | func LoadResolvers(opts Options) ([]Resolver, error) { 75 | // For each nameserver, initialise the correct resolver. 76 | rslvrs := make([]Resolver, 0, len(opts.Nameservers)) 77 | 78 | for _, ns := range opts.Nameservers { 79 | if ns.Type == models.DOHResolver { 80 | opts.Logger.Debug("initiating DOH resolver") 81 | rslvr, err := NewDOHResolver(ns.Address, opts) 82 | if err != nil { 83 | return rslvrs, err 84 | } 85 | rslvrs = append(rslvrs, rslvr) 86 | } 87 | if ns.Type == models.DOTResolver { 88 | opts.Logger.Debug("initiating DOT resolver") 89 | rslvr, err := NewClassicResolver(ns.Address, 90 | ClassicResolverOpts{ 91 | UseTLS: true, 92 | UseTCP: true, 93 | }, opts) 94 | 95 | if err != nil { 96 | return rslvrs, err 97 | } 98 | rslvrs = append(rslvrs, rslvr) 99 | } 100 | if ns.Type == models.TCPResolver { 101 | opts.Logger.Debug("initiating TCP resolver") 102 | rslvr, err := NewClassicResolver(ns.Address, 103 | ClassicResolverOpts{ 104 | UseTLS: false, 105 | UseTCP: true, 106 | }, opts) 107 | if err != nil { 108 | return rslvrs, err 109 | } 110 | rslvrs = append(rslvrs, rslvr) 111 | } 112 | if ns.Type == models.UDPResolver { 113 | opts.Logger.Debug("initiating UDP resolver") 114 | rslvr, err := NewClassicResolver(ns.Address, 115 | ClassicResolverOpts{ 116 | UseTLS: false, 117 | UseTCP: false, 118 | }, opts) 119 | if err != nil { 120 | return rslvrs, err 121 | } 122 | rslvrs = append(rslvrs, rslvr) 123 | } 124 | if ns.Type == models.DNSCryptResolver { 125 | opts.Logger.Debug("initiating DNSCrypt resolver") 126 | rslvr, err := NewDNSCryptResolver(ns.Address, 127 | DNSCryptResolverOpts{ 128 | UseTCP: false, 129 | }, opts) 130 | if err != nil { 131 | return rslvrs, err 132 | } 133 | rslvrs = append(rslvrs, rslvr) 134 | } 135 | if ns.Type == models.DOQResolver { 136 | opts.Logger.Debug("initiating DOQ resolver") 137 | rslvr, err := NewDOQResolver(ns.Address, opts) 138 | if err != nil { 139 | return rslvrs, err 140 | } 141 | rslvrs = append(rslvrs, rslvr) 142 | } 143 | } 144 | return rslvrs, nil 145 | } 146 | -------------------------------------------------------------------------------- /pkg/resolvers/utils.go: -------------------------------------------------------------------------------- 1 | package resolvers 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | "time" 8 | 9 | "github.com/miekg/dns" 10 | ) 11 | 12 | // QueryFlags represents the various DNS query flags 13 | type QueryFlags struct { 14 | AA bool // Authoritative Answer 15 | AD bool // Authenticated Data 16 | CD bool // Checking Disabled 17 | RD bool // Recursion Desired 18 | Z bool // Reserved for future use 19 | DO bool // DNSSEC OK 20 | } 21 | 22 | // prepareMessages takes a DNS Question and returns the 23 | // corresponding DNS messages for the same. 24 | func prepareMessages(q dns.Question, flags QueryFlags, ndots int, searchList []string) []dns.Msg { 25 | var ( 26 | possibleQNames = constructPossibleQuestions(q.Name, ndots, searchList) 27 | messages = make([]dns.Msg, 0, len(possibleQNames)) 28 | ) 29 | 30 | for _, qName := range possibleQNames { 31 | msg := dns.Msg{} 32 | // generate a random id for the transaction. 33 | msg.Id = dns.Id() 34 | 35 | // Set query flags 36 | msg.RecursionDesired = flags.RD 37 | msg.AuthenticatedData = flags.AD 38 | msg.CheckingDisabled = flags.CD 39 | msg.Authoritative = flags.AA 40 | msg.Zero = flags.Z 41 | 42 | if flags.DO { 43 | msg.SetEdns0(4096, flags.DO) 44 | } 45 | 46 | // It's recommended to only send 1 question for 1 DNS message. 47 | msg.Question = []dns.Question{{ 48 | Name: qName, 49 | Qtype: q.Qtype, 50 | Qclass: q.Qclass, 51 | }} 52 | messages = append(messages, msg) 53 | } 54 | 55 | return messages 56 | } 57 | 58 | // NameList returns all of the names that should be queried based on the 59 | // config. It is based off of go's net/dns name building, but it does not 60 | // check the length of the resulting names. 61 | // NOTE: It is taken from `miekg/dns/clientconfig.go: func (c *ClientConfig) NameList` 62 | // and slightly modified. 63 | func constructPossibleQuestions(name string, ndots int, searchList []string) []string { 64 | // if this domain is already fully qualified, no append needed. 65 | if dns.IsFqdn(name) { 66 | return []string{name} 67 | } 68 | 69 | // Check to see if the name has more labels than Ndots. Do this before making 70 | // the domain fully qualified. 71 | hasNdots := dns.CountLabel(name) > ndots 72 | // Make the domain fully qualified. 73 | name = dns.Fqdn(name) 74 | 75 | // Make a list of names based off search. 76 | names := []string{} 77 | 78 | // If name has enough dots, try that first. 79 | if hasNdots { 80 | names = append(names, name) 81 | } 82 | for _, s := range searchList { 83 | names = append(names, dns.Fqdn(name+s)) 84 | } 85 | // If we didn't have enough dots, try after suffixes. 86 | if !hasNdots { 87 | names = append(names, name) 88 | } 89 | return names 90 | } 91 | 92 | // parseMessage takes a `dns.Message` and returns a custom 93 | // Response data struct. 94 | func parseMessage(msg *dns.Msg, rtt time.Duration, server string) Response { 95 | var resp Response 96 | timeTaken := fmt.Sprintf("%dms", rtt.Milliseconds()) 97 | 98 | // Parse Authorities section. 99 | for _, ns := range msg.Ns { 100 | // check for SOA record 101 | soa, ok := ns.(*dns.SOA) 102 | if !ok { 103 | // Currently we only check for SOA in Authority. 104 | // If it's not SOA, skip this message. 105 | continue 106 | } 107 | mname := soa.Ns + " " + soa.Mbox + 108 | " " + strconv.FormatInt(int64(soa.Serial), 10) + 109 | " " + strconv.FormatInt(int64(soa.Refresh), 10) + 110 | " " + strconv.FormatInt(int64(soa.Retry), 10) + 111 | " " + strconv.FormatInt(int64(soa.Expire), 10) + 112 | " " + strconv.FormatInt(int64(soa.Minttl), 10) 113 | h := ns.Header() 114 | name := h.Name 115 | qclass := dns.Class(h.Class).String() 116 | ttl := strconv.FormatInt(int64(h.Ttl), 10) + "s" 117 | qtype := dns.Type(h.Rrtype).String() 118 | auth := Authority{ 119 | Name: name, 120 | Type: qtype, 121 | TTL: ttl, 122 | Class: qclass, 123 | MName: mname, 124 | Nameserver: server, 125 | RTT: timeTaken, 126 | Status: dns.RcodeToString[msg.Rcode], 127 | } 128 | resp.Authorities = append(resp.Authorities, auth) 129 | } 130 | // Parse Answers section. 131 | for _, a := range msg.Answer { 132 | var ( 133 | h = a.Header() 134 | // Source https://github.com/jvns/dns-lookup/blob/main/dns.go#L121. 135 | parts = strings.Split(a.String(), "\t") 136 | ans = Answer{ 137 | Name: h.Name, 138 | Type: dns.Type(h.Rrtype).String(), 139 | TTL: strconv.FormatInt(int64(h.Ttl), 10) + "s", 140 | Class: dns.Class(h.Class).String(), 141 | Address: parts[len(parts)-1], 142 | RTT: timeTaken, 143 | Nameserver: server, 144 | } 145 | ) 146 | 147 | resp.Answers = append(resp.Answers, ans) 148 | } 149 | return resp 150 | } 151 | -------------------------------------------------------------------------------- /pkg/utils/logger.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "log/slog" 5 | "os" 6 | ) 7 | 8 | // InitLogger initializes logger. 9 | func InitLogger(debug bool) *slog.Logger { 10 | lvl := slog.LevelInfo 11 | if debug { 12 | lvl = slog.LevelDebug 13 | } 14 | 15 | lgrOpts := &slog.HandlerOptions{ 16 | Level: lvl, 17 | } 18 | 19 | logger := slog.New(slog.NewTextHandler(os.Stderr, lgrOpts)) 20 | return logger 21 | } 22 | -------------------------------------------------------------------------------- /web.Dockerfile: -------------------------------------------------------------------------------- 1 | # Dockerfile 2 | FROM ubuntu:24.04 3 | # Install ca-certificates 4 | RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/* 5 | WORKDIR /app 6 | RUN ls -alht 7 | COPY doggo-web.bin . 8 | COPY config-api-sample.toml config.toml 9 | COPY docs/dist /app/dist/ 10 | CMD ["./doggo-web.bin"] 11 | -------------------------------------------------------------------------------- /web/api.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "embed" 5 | "io/fs" 6 | "net/http" 7 | "os" 8 | "time" 9 | 10 | "github.com/mr-karan/doggo/internal/app" 11 | "github.com/mr-karan/doggo/pkg/utils" 12 | 13 | "github.com/go-chi/chi/v5" 14 | "github.com/go-chi/chi/v5/middleware" 15 | "github.com/knadh/koanf/v2" 16 | ) 17 | 18 | var ( 19 | ko = koanf.New(".") 20 | // Version and date of the build. This is injected at build-time. 21 | buildVersion = "unknown" 22 | buildDate = "unknown" 23 | //go:embed assets/* 24 | assetsDir embed.FS 25 | //go:embed index.html 26 | html []byte 27 | ) 28 | 29 | func main() { 30 | initConfig() 31 | 32 | logger := utils.InitLogger(ko.Bool("app.debug")) 33 | 34 | // Initialize app. 35 | app := app.New(logger, nil, buildVersion) 36 | 37 | // Register router instance. 38 | r := chi.NewRouter() 39 | 40 | // Register middlewares 41 | r.Use(middleware.RequestID) 42 | r.Use(middleware.RealIP) 43 | r.Use(middleware.Logger) 44 | r.Use(middleware.Recoverer) 45 | 46 | // Frontend Handlers. 47 | assets, _ := fs.Sub(assetsDir, "assets") 48 | r.Get("/assets/*", func(w http.ResponseWriter, r *http.Request) { 49 | fs := http.StripPrefix("/assets/", http.FileServer(http.FS(assets))) 50 | fs.ServeHTTP(w, r) 51 | }) 52 | r.Get("/", func(w http.ResponseWriter, r *http.Request) { 53 | w.Header().Add("Content-Type", "text/html") 54 | w.Write(html) 55 | }) 56 | 57 | // API Handlers. 58 | r.Get("/api/", wrap(app, handleIndexAPI)) 59 | r.Get("/api/ping/", wrap(app, handleHealthCheck)) 60 | r.Post("/api/lookup/", wrap(app, handleLookup)) 61 | 62 | // HTTP Server. 63 | srv := &http.Server{ 64 | Addr: ko.String("server.address"), 65 | Handler: r, 66 | ReadTimeout: ko.Duration("server.read_timeout") * time.Millisecond, 67 | WriteTimeout: ko.Duration("server.write_timeout") * time.Millisecond, 68 | IdleTimeout: ko.Duration("server.keepalive_timeout") * time.Millisecond, 69 | } 70 | 71 | logger.Info("starting server", "address", srv.Addr, "version", buildVersion) 72 | 73 | if err := srv.ListenAndServe(); err != nil { 74 | logger.Error("couldn't start server", "error", err) 75 | os.Exit(1) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /web/assets/main.js: -------------------------------------------------------------------------------- 1 | const $ = document.querySelector.bind(document); 2 | const $new = document.createElement.bind(document); 3 | const $show = (el) => { 4 | el.classList.remove('hidden'); 5 | }; 6 | const $hide = (el) => { 7 | el.classList.add('hidden'); 8 | }; 9 | 10 | const apiURL = '/api/lookup/'; 11 | 12 | (function () { 13 | const fields = ['name', 'address', 'type', 'ttl', 'rtt']; 14 | 15 | // createRow creates a table row with the given cell values. 16 | function createRow(item) { 17 | const tr = $new('tr'); 18 | fields.forEach((f) => { 19 | const td = $new('td'); 20 | td.innerText = item[f]; 21 | td.classList.add(f); 22 | tr.appendChild(td); 23 | }); 24 | return tr; 25 | } 26 | 27 | const handleSubmit = async () => { 28 | const tbody = $('#table tbody'), 29 | tbl = $('#table'); 30 | tbody.innerHTML = ''; 31 | $hide(tbl); 32 | 33 | const q = $('input[name=q]').value.trim(), 34 | typ = $('select[name=type]').value, 35 | addr = $('input[name=address]').value.trim(); 36 | 37 | // Post to the API. 38 | const req = await fetch(apiURL, { 39 | method: 'POST', 40 | headers: { 'Content-Type': 'application/json' }, 41 | body: JSON.stringify({ query: [q,], type: [typ,], nameservers: [addr,] }) 42 | }); 43 | 44 | const res = await req.json(); 45 | 46 | if (res.status != 'success') { 47 | const error = (res && res.message) || response.statusText; 48 | throw(error); 49 | return; 50 | } 51 | 52 | if (res.data[0].answers == null) { 53 | throw('No records found.'); 54 | return; 55 | } 56 | 57 | res.data[0].answers.forEach((item) => { 58 | tbody.appendChild(createRow(item)); 59 | }); 60 | 61 | $show(tbl); 62 | }; 63 | 64 | // Capture the form submit. 65 | $('#form').onsubmit = async (e) => { 66 | e.preventDefault(); 67 | 68 | const msg = $('#message'); 69 | $hide(msg); 70 | 71 | try { 72 | await handleSubmit(); 73 | } catch(e) { 74 | msg.innerText = e.toString(); 75 | $show(msg); 76 | throw e; 77 | } 78 | }; 79 | 80 | // Change the address on ns change. 81 | const ns = $("#ns"), addr = $("#address"); 82 | addr.value = ns.value; 83 | 84 | ns.onchange = (e) => { 85 | addr.value = e.target.value; 86 | if(addr.value === "") { 87 | addr.focus(); 88 | } 89 | }; 90 | })(); -------------------------------------------------------------------------------- /web/assets/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --primary: #4338ca; 3 | --secondary: #333; 4 | } 5 | 6 | * { 7 | box-sizing: border-box; 8 | } 9 | 10 | :focus { 11 | outline: 0; 12 | } 13 | 14 | body { 15 | font-family: "Segoe UI", "Helvetica Neue", Inter, sans-serif; 16 | font-size: 16px; 17 | line-height: 24px; 18 | color: #111; 19 | margin: 0 auto; 20 | } 21 | 22 | h1, h2, h3, h4 { 23 | line-height: 1.3em; 24 | } 25 | 26 | a { 27 | color: var(--primary); 28 | } 29 | a:hover { 30 | color: #111; 31 | text-decoration: none; 32 | } 33 | 34 | input, select, button { 35 | border-radius: 5px; 36 | border: 1px solid #ddd; 37 | font-size: 1.3rem; 38 | padding: 10px 15px; 39 | width: 100%; 40 | } 41 | input:focus, select:focus { 42 | border-color: var(--primary); 43 | } 44 | button { 45 | border-color: var(--primary); 46 | background: var(--primary); 47 | color: #fff; 48 | cursor: pointer; 49 | width: auto; 50 | padding: 10px 30px; 51 | } 52 | button:focus, 53 | button:hover { 54 | border-color: var(--secondary); 55 | background: var(--secondary); 56 | } 57 | 58 | label { 59 | display: block; 60 | padding-bottom: 0.5rem; 61 | } 62 | 63 | .box { 64 | box-shadow: 1px 1px 4px #eee; 65 | border: 1px solid #eee; 66 | padding: 30px; 67 | border-radius: 3px; 68 | } 69 | 70 | .hidden { 71 | display: none !important; 72 | } 73 | 74 | .main { 75 | margin: 60px auto 30px auto; 76 | max-width: 900px; 77 | } 78 | 79 | header { 80 | text-align: center; 81 | font-size: 1.5em; 82 | margin-bottom: 60px; 83 | } 84 | .logo span { 85 | color: var(--primary); 86 | font-weight: 900; 87 | } 88 | 89 | form { 90 | margin-bottom: 45px; 91 | } 92 | 93 | .row { 94 | display: flex; 95 | margin-bottom: 15px; 96 | } 97 | .row .field { 98 | flex: 50%; 99 | } 100 | .row .field:last-child { 101 | margin-left: 30px; 102 | } 103 | 104 | .submit { 105 | text-align: right; 106 | } 107 | .help { 108 | color: #666; 109 | font-size: 0.875em; 110 | } 111 | 112 | #message { 113 | color: #ff3300; 114 | } 115 | 116 | table.box { 117 | width: 100%; 118 | max-width: 100%; 119 | padding: 0; 120 | } 121 | table th { 122 | background: #f9fafb; 123 | color: #666; 124 | font-size: 0.875em; 125 | border-bottom: 1px solid #ddd; 126 | } 127 | table th, tbody td { 128 | padding: 10px 15px; 129 | text-align: left; 130 | } 131 | td.name { 132 | font-weight: bold; 133 | } 134 | th.type, td.type { 135 | text-align: center; 136 | } 137 | td.type { 138 | background: #d1fae5; 139 | color: #065f46; 140 | font-weight: bold; 141 | } 142 | 143 | footer { 144 | margin: 60px 0 0 0; 145 | text-align: center; 146 | } 147 | footer a { 148 | text-decoration: none; 149 | } 150 | 151 | 152 | @media (max-width: 650px) { 153 | .main { 154 | margin: 60px 30px 30px 30px; 155 | } 156 | .box { 157 | box-shadow: none; 158 | border: 0; 159 | padding: 0; 160 | } 161 | 162 | .row { 163 | display: block; 164 | } 165 | .field { 166 | margin: 0 0 20px 0; 167 | } 168 | .row .field:last-child { 169 | margin: 0; 170 | } 171 | .submit button { 172 | width: 100%; 173 | } 174 | 175 | table { 176 | table-layout: fixed; 177 | } 178 | table th { 179 | width: 100%; 180 | } 181 | table tr { 182 | border-bottom: 0; 183 | display: flex; 184 | flex-direction: row; 185 | flex-wrap: wrap; 186 | margin-bottom: 30px; 187 | } 188 | table td { 189 | border: 1px solid #eee; 190 | margin: 0 -1px -1px 0; 191 | position: relative; 192 | width: 100%; 193 | word-wrap:break-word; 194 | } 195 | table th.type, table td.type { 196 | text-align: left; 197 | } 198 | table td span { 199 | display: block; 200 | } 201 | } -------------------------------------------------------------------------------- /web/config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | "github.com/knadh/koanf/parsers/toml" 9 | "github.com/knadh/koanf/providers/env" 10 | "github.com/knadh/koanf/providers/file" 11 | "github.com/knadh/koanf/providers/posflag" 12 | flag "github.com/spf13/pflag" 13 | ) 14 | 15 | // Config is the config given by the user 16 | type Config struct { 17 | HTTPAddr string `koanf:"listen_addr"` 18 | } 19 | 20 | func initConfig() error { 21 | f := flag.NewFlagSet("api", flag.ContinueOnError) 22 | f.Usage = func() { 23 | fmt.Println(f.FlagUsages()) 24 | os.Exit(0) 25 | } 26 | 27 | // Register --config flag. 28 | f.StringSlice("config", []string{"config.toml"}, 29 | "Path to one or more TOML config files to load in order") 30 | 31 | // Register --version flag. 32 | f.Bool("version", false, "Show build version") 33 | f.Parse(os.Args[1:]) 34 | // Display version. 35 | if ok, _ := f.GetBool("version"); ok { 36 | fmt.Println(buildVersion, buildDate) 37 | os.Exit(0) 38 | } 39 | 40 | // Read the config files. 41 | cFiles, _ := f.GetStringSlice("config") 42 | for _, f := range cFiles { 43 | if err := ko.Load(file.Provider(f), toml.Parser()); err != nil { 44 | return fmt.Errorf("error reading file: %w", err) 45 | } 46 | } 47 | // Load environment variables and merge into the loaded config. 48 | if err := ko.Load(env.Provider("DOGGO_API_", ".", func(s string) string { 49 | return strings.Replace(strings.ToLower( 50 | strings.TrimPrefix(s, "DOGGO_API_")), "__", ".", -1) 51 | }), nil); err != nil { 52 | return fmt.Errorf("error loading env config: %w", err) 53 | } 54 | 55 | ko.Load(posflag.Provider(f, ".", ko), nil) 56 | return nil 57 | } 58 | -------------------------------------------------------------------------------- /web/handlers.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | "sync" 10 | "time" 11 | 12 | "github.com/mr-karan/doggo/internal/app" 13 | "github.com/mr-karan/doggo/pkg/models" 14 | "github.com/mr-karan/doggo/pkg/resolvers" 15 | ) 16 | 17 | type httpResp struct { 18 | Status string `json:"status"` 19 | Message string `json:"message,omitempty"` 20 | Data interface{} `json:"data,omitempty"` 21 | } 22 | 23 | func handleIndexAPI(w http.ResponseWriter, r *http.Request) { 24 | var ( 25 | app = r.Context().Value("app").(app.App) 26 | ) 27 | 28 | sendResponse(w, http.StatusOK, fmt.Sprintf("Welcome to Doggo API. Version: %s", app.Version)) 29 | } 30 | 31 | func handleHealthCheck(w http.ResponseWriter, r *http.Request) { 32 | sendResponse(w, http.StatusOK, "PONG") 33 | } 34 | 35 | func handleLookup(w http.ResponseWriter, r *http.Request) { 36 | var ( 37 | app = r.Context().Value("app").(app.App) 38 | ) 39 | 40 | // Read body. 41 | b, err := io.ReadAll(r.Body) 42 | defer r.Body.Close() 43 | if err != nil { 44 | app.Logger.Error("error reading request body", "error", err) 45 | sendErrorResponse(w, "Invalid JSON payload", http.StatusBadRequest, nil) 46 | return 47 | } 48 | // Prepare query flags. 49 | var qFlags models.QueryFlags 50 | if err := json.Unmarshal(b, &qFlags); err != nil { 51 | app.Logger.Error("error unmarshalling payload", "error", err) 52 | sendErrorResponse(w, "Invalid JSON payload", http.StatusBadRequest, nil) 53 | return 54 | } 55 | 56 | app.QueryFlags = qFlags 57 | // Load fallbacks. 58 | app.LoadFallbacks() 59 | 60 | // Load Questions. 61 | app.PrepareQuestions() 62 | 63 | if len(app.Questions) == 0 { 64 | sendErrorResponse(w, "Missing field `query`.", http.StatusBadRequest, nil) 65 | return 66 | } 67 | 68 | // Load Nameservers. 69 | if err := app.LoadNameservers(); err != nil { 70 | app.Logger.Error("error loading nameservers", "error", err) 71 | sendErrorResponse(w, "Error looking up for records.", http.StatusInternalServerError, nil) 72 | return 73 | } 74 | 75 | app.Logger.Debug("Loaded nameservers", "nameservers", app.Nameservers) 76 | 77 | // Load Resolvers. 78 | rslvrs, err := resolvers.LoadResolvers(resolvers.Options{ 79 | Nameservers: app.Nameservers, 80 | UseIPv4: app.QueryFlags.UseIPv4, 81 | UseIPv6: app.QueryFlags.UseIPv6, 82 | SearchList: app.ResolverOpts.SearchList, 83 | Ndots: app.ResolverOpts.Ndots, 84 | Timeout: app.QueryFlags.Timeout * time.Second, 85 | Logger: app.Logger, 86 | }) 87 | if err != nil { 88 | app.Logger.Error("error loading resolver", "error", err) 89 | sendErrorResponse(w, "Error looking up for records.", http.StatusInternalServerError, nil) 90 | return 91 | } 92 | app.Resolvers = rslvrs 93 | 94 | app.Logger.Debug("Loaded resolvers", "resolvers", app.Resolvers) 95 | 96 | queryFlags := resolvers.QueryFlags{ 97 | RD: true, 98 | } 99 | 100 | ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second) 101 | defer cancel() 102 | 103 | var ( 104 | wg sync.WaitGroup 105 | mu sync.Mutex 106 | allResponses []resolvers.Response 107 | allErrors []error 108 | ) 109 | 110 | for _, resolver := range app.Resolvers { 111 | wg.Add(1) 112 | go func(r resolvers.Resolver) { 113 | defer wg.Done() 114 | responses, err := r.Lookup(ctx, app.Questions, queryFlags) 115 | mu.Lock() 116 | if err != nil { 117 | allErrors = append(allErrors, err) 118 | } else { 119 | allResponses = append(allResponses, responses...) 120 | } 121 | mu.Unlock() 122 | }(resolver) 123 | } 124 | 125 | wg.Wait() 126 | 127 | if len(allErrors) > 0 { 128 | app.Logger.Error("errors looking up DNS records", "errors", allErrors) 129 | sendErrorResponse(w, "Error looking up for records.", http.StatusInternalServerError, nil) 130 | return 131 | } 132 | 133 | if len(allResponses) == 0 { 134 | sendErrorResponse(w, "No records found.", http.StatusNotFound, nil) 135 | return 136 | } 137 | 138 | sendResponse(w, http.StatusOK, allResponses) 139 | } 140 | 141 | // wrap is a middleware that wraps HTTP handlers and injects the "app" context. 142 | func wrap(app app.App, next http.HandlerFunc) http.HandlerFunc { 143 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 144 | ctx := context.WithValue(r.Context(), "app", app) 145 | next.ServeHTTP(w, r.WithContext(ctx)) 146 | }) 147 | } 148 | 149 | // sendResponse sends a JSON envelope to the HTTP response. 150 | func sendResponse(w http.ResponseWriter, code int, data interface{}) { 151 | w.Header().Set("Content-Type", "application/json; charset=utf-8") 152 | w.WriteHeader(code) 153 | 154 | out, err := json.Marshal(httpResp{Status: "success", Data: data}) 155 | if err != nil { 156 | sendErrorResponse(w, "Internal Server Error", http.StatusInternalServerError, nil) 157 | return 158 | } 159 | 160 | w.Write(out) 161 | } 162 | 163 | // sendErrorResponse sends a JSON error envelope to the HTTP response. 164 | func sendErrorResponse(w http.ResponseWriter, message string, code int, data interface{}) { 165 | w.Header().Set("Content-Type", "application/json; charset=utf-8") 166 | w.WriteHeader(code) 167 | 168 | resp := httpResp{Status: "error", 169 | Message: message, 170 | Data: data} 171 | out, _ := json.Marshal(resp) 172 | w.Write(out) 173 | } 174 | -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Doggo DNS 5 | 6 | 7 | 8 | 9 | 10 |
11 |
12 |

Doggo DNS

13 |
14 | 15 |
16 |
17 |
18 | 19 | 20 |
21 |
22 | 23 | 36 |
37 |
38 |
39 |
40 | 41 | 48 |
49 |
50 | 51 | 53 |

54 | To use different protocols like DOH, DOT etc. refer to the instructions 55 | here. 56 |

57 |
58 |
59 |
60 |

61 |
62 | 63 |
64 |
65 |
66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 90 | 91 | 92 | 98 |
99 | 100 | 101 | -------------------------------------------------------------------------------- /www/api/api.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | ``` 4 | curl --request POST \ 5 | --url http://localhost:8080/lookup/ \ 6 | --header 'Content-Type: application/json' \ 7 | --data '{ 8 | "query": ["mrkaran.dev"], 9 | "type": ["A"], 10 | "class": ["IN"], 11 | "nameservers": ["9.9.9.9"] 12 | }' 13 | ``` 14 | -------------------------------------------------------------------------------- /www/static/doggo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mr-karan/doggo/16ba284a136354fb4f11fd4d566db9b4364e9a32/www/static/doggo.png -------------------------------------------------------------------------------- /www/static/help.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mr-karan/doggo/16ba284a136354fb4f11fd4d566db9b4364e9a32/www/static/help.png --------------------------------------------------------------------------------