├── .gitignore ├── .gitmodules ├── .ignore ├── CHANGELOG.adoc ├── COPYING ├── LICENSES ├── CC-BY-NC-SA-4.0.txt ├── GPL-3.0-or-later.txt └── LicenseRef-mcrae.txt ├── Makefile ├── README ├── README.adoc ├── REUSE.toml ├── _bin └── nft_dnstest_chain.sh ├── _sys ├── Library │ └── LaunchDaemons │ │ └── info.kilabit.rescached.plist ├── etc │ ├── init.d │ │ └── rescached.run │ └── rescached │ │ ├── block.d │ │ ├── .pgl.yoyo.org │ │ ├── .someonewhocares.org │ │ └── .winhelp2002.mvps.org │ │ ├── localhost.pem │ │ ├── localhost.pem.key │ │ └── rescached.cfg └── usr │ ├── lib │ └── systemd │ │ └── system │ │ └── rescached.service │ └── share │ └── man │ ├── man1 │ ├── rescached.1.gz │ └── resolver.1.gz │ └── man5 │ └── rescached.cfg.5.gz ├── _test └── etc │ └── rescached │ ├── block.d │ ├── .a.block │ ├── .b.block │ └── .c.block │ ├── hosts.d │ └── hosts │ └── rescached.cfg ├── _www ├── block.d │ └── index.html ├── doc │ ├── CHANGELOG.adoc │ ├── README.adoc │ ├── benchmark.adoc │ ├── html.tmpl │ ├── images │ │ ├── Screenshot_wui_environment.png │ │ ├── Screenshot_wui_frontpage.png │ │ ├── Screenshot_wui_hosts_blocks.png │ │ ├── Screenshot_wui_hosts_d.png │ │ └── Screenshot_wui_zone_d.png │ ├── index.adoc │ ├── rescached.cfg.adoc │ └── resolver.adoc ├── environment │ └── index.html ├── favicon.png ├── hosts.d │ └── index.html ├── index.css ├── index.html ├── index.js ├── rescached.js └── zone.d │ └── index.html ├── blockd.go ├── blockd_test.go ├── client.go ├── client_test.go ├── cmd ├── rescached │ └── main.go ├── resolver │ ├── doc.go │ ├── main.go │ └── resolver.go └── resolverbench │ └── main.go ├── environment.go ├── environment_test.go ├── go.mod ├── go.sum ├── httpd.go ├── internal └── cmd │ └── www │ └── main.go ├── memfs_generate.go ├── rescached.go ├── rescached_test.go ├── testdata ├── hosts.d │ ├── hosts │ └── hosts.2 ├── master.d │ └── test ├── rescached.cfg ├── rescached.cfg.test.out ├── resolv.conf └── resolv.conf.empty └── zone_record_request.go /.gitignore: -------------------------------------------------------------------------------- 1 | /CHANGELOG.html 2 | /README.html 3 | /TODO 4 | /_bin/darwin_amd64 5 | /_bin/linux_amd64 6 | /_test/etc/rescached/hosts.d/hosts 7 | /_test/etc/rescached/zone.d/ 8 | /_test/var/cache/rescached/ 9 | /_www/doc/*.html 10 | /cover.html 11 | /cover.out 12 | /heap* 13 | /rescached 14 | /resolver 15 | /resolverbench 16 | /testdata/rescached.pid 17 | /testdata/test.pid 18 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "AUR"] 2 | path = _AUR 3 | url = https://aur.archlinux.org/rescached-git.git 4 | -------------------------------------------------------------------------------- /.ignore: -------------------------------------------------------------------------------- 1 | _www/src/build 2 | _www/public/build 3 | -------------------------------------------------------------------------------- /LICENSES/CC-BY-NC-SA-4.0.txt: -------------------------------------------------------------------------------- 1 | Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International 2 | 3 | Creative Commons Corporation (“Creative Commons”) is not a law firm and does not provide legal services or legal advice. Distribution of Creative Commons public licenses does not create a lawyer-client or other relationship. Creative Commons makes its licenses and related information available on an “as-is” basis. Creative Commons gives no warranties regarding its licenses, any material licensed under their terms and conditions, or any related information. Creative Commons disclaims all liability for damages resulting from their use to the fullest extent possible. 4 | 5 | Using Creative Commons Public Licenses 6 | 7 | Creative Commons public licenses provide a standard set of terms and conditions that creators and other rights holders may use to share original works of authorship and other material subject to copyright and certain other rights specified in the public license below. The following considerations are for informational purposes only, are not exhaustive, and do not form part of our licenses. 8 | 9 | Considerations for licensors: Our public licenses are intended for use by those authorized to give the public permission to use material in ways otherwise restricted by copyright and certain other rights. Our licenses are irrevocable. Licensors should read and understand the terms and conditions of the license they choose before applying it. Licensors should also secure all rights necessary before applying our licenses so that the public can reuse the material as expected. Licensors should clearly mark any material not subject to the license. This includes other CC-licensed material, or material used under an exception or limitation to copyright. More considerations for licensors. 10 | 11 | Considerations for the public: By using one of our public licenses, a licensor grants the public permission to use the licensed material under specified terms and conditions. If the licensor’s permission is not necessary for any reason–for example, because of any applicable exception or limitation to copyright–then that use is not regulated by the license. Our licenses grant only permissions under copyright and certain other rights that a licensor has authority to grant. Use of the licensed material may still be restricted for other reasons, including because others have copyright or other rights in the material. A licensor may make special requests, such as asking that all changes be marked or described. Although not required by our licenses, you are encouraged to respect those requests where reasonable. More considerations for the public. 12 | 13 | Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Public License 14 | 15 | By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions. 16 | 17 | Section 1 – Definitions. 18 | 19 | a. Adapted Material means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image. 20 | 21 | b. Adapter's License means the license You apply to Your Copyright and Similar Rights in Your contributions to Adapted Material in accordance with the terms and conditions of this Public License. 22 | 23 | c. BY-NC-SA Compatible License means a license listed at creativecommons.org/compatiblelicenses, approved by Creative Commons as essentially the equivalent of this Public License. 24 | 25 | d. Copyright and Similar Rights means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights. 26 | 27 | e. Effective Technological Measures means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements. 28 | 29 | f. Exceptions and Limitations means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material. 30 | 31 | g. License Elements means the license attributes listed in the name of a Creative Commons Public License. The License Elements of this Public License are Attribution, NonCommercial, and ShareAlike. 32 | 33 | h. Licensed Material means the artistic or literary work, database, or other material to which the Licensor applied this Public License. 34 | 35 | i. Licensed Rights means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license. 36 | 37 | j. Licensor means the individual(s) or entity(ies) granting rights under this Public License. 38 | 39 | k. NonCommercial means not primarily intended for or directed towards commercial advantage or monetary compensation. For purposes of this Public License, the exchange of the Licensed Material for other material subject to Copyright and Similar Rights by digital file-sharing or similar means is NonCommercial provided there is no payment of monetary compensation in connection with the exchange. 40 | 41 | l. Share means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them. 42 | 43 | m. Sui Generis Database Rights means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world. 44 | 45 | n. You means the individual or entity exercising the Licensed Rights under this Public License. Your has a corresponding meaning. 46 | 47 | Section 2 – Scope. 48 | 49 | a. License grant. 50 | 51 | 1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to: 52 | 53 | A. reproduce and Share the Licensed Material, in whole or in part, for NonCommercial purposes only; and 54 | 55 | B. produce, reproduce, and Share Adapted Material for NonCommercial purposes only. 56 | 57 | 2. Exceptions and Limitations. For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions. 58 | 59 | 3. Term. The term of this Public License is specified in Section 6(a). 60 | 61 | 4. Media and formats; technical modifications allowed. The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a)(4) never produces Adapted Material. 62 | 63 | 5. Downstream recipients. 64 | 65 | A. Offer from the Licensor – Licensed Material. Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License. 66 | 67 | B. Additional offer from the Licensor – Adapted Material. Every recipient of Adapted Material from You automatically receives an offer from the Licensor to exercise the Licensed Rights in the Adapted Material under the conditions of the Adapter’s License You apply. 68 | 69 | C. No downstream restrictions. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material. 70 | 71 | 6. No endorsement. Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i). 72 | 73 | b. Other rights. 74 | 75 | 1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise. 76 | 77 | 2. Patent and trademark rights are not licensed under this Public License. 78 | 79 | 3. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties, including when the Licensed Material is used other than for NonCommercial purposes. 80 | 81 | Section 3 – License Conditions. 82 | 83 | Your exercise of the Licensed Rights is expressly made subject to the following conditions. 84 | 85 | a. Attribution. 86 | 87 | 1. If You Share the Licensed Material (including in modified form), You must: 88 | 89 | A. retain the following if it is supplied by the Licensor with the Licensed Material: 90 | 91 | i. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated); 92 | 93 | ii. a copyright notice; 94 | 95 | iii. a notice that refers to this Public License; 96 | 97 | iv. a notice that refers to the disclaimer of warranties; 98 | 99 | v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable; 100 | 101 | B. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and 102 | 103 | C. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License. 104 | 105 | 2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information. 106 | 107 | 3. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable. 108 | 109 | b. ShareAlike.In addition to the conditions in Section 3(a), if You Share Adapted Material You produce, the following conditions also apply. 110 | 111 | 1. The Adapter’s License You apply must be a Creative Commons license with the same License Elements, this version or later, or a BY-NC-SA Compatible License. 112 | 113 | 2. You must include the text of, or the URI or hyperlink to, the Adapter's License You apply. You may satisfy this condition in any reasonable manner based on the medium, means, and context in which You Share Adapted Material. 114 | 115 | 3. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, Adapted Material that restrict exercise of the rights granted under the Adapter's License You apply. 116 | 117 | Section 4 – Sui Generis Database Rights. 118 | 119 | Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material: 120 | 121 | a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database for NonCommercial purposes only; 122 | 123 | b. if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material, including for purposes of Section 3(b); and 124 | 125 | c. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database. 126 | For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights. 127 | 128 | Section 5 – Disclaimer of Warranties and Limitation of Liability. 129 | 130 | a. Unless otherwise separately undertaken by the Licensor, to the extent possible, the Licensor offers the Licensed Material as-is and as-available, and makes no representations or warranties of any kind concerning the Licensed Material, whether express, implied, statutory, or other. This includes, without limitation, warranties of title, merchantability, fitness for a particular purpose, non-infringement, absence of latent or other defects, accuracy, or the presence or absence of errors, whether or not known or discoverable. Where disclaimers of warranties are not allowed in full or in part, this disclaimer may not apply to You. 131 | 132 | b. To the extent possible, in no event will the Licensor be liable to You on any legal theory (including, without limitation, negligence) or otherwise for any direct, special, indirect, incidental, consequential, punitive, exemplary, or other losses, costs, expenses, or damages arising out of this Public License or use of the Licensed Material, even if the Licensor has been advised of the possibility of such losses, costs, expenses, or damages. Where a limitation of liability is not allowed in full or in part, this limitation may not apply to You. 133 | 134 | c. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability. 135 | 136 | Section 6 – Term and Termination. 137 | 138 | a. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically. 139 | 140 | b. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates: 141 | 142 | 1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or 143 | 144 | 2. upon express reinstatement by the Licensor. 145 | 146 | For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License. 147 | 148 | c. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License. 149 | 150 | d. Sections 1, 5, 6, 7, and 8 survive termination of this Public License. 151 | 152 | Section 7 – Other Terms and Conditions. 153 | 154 | a. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed. 155 | 156 | b. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License. 157 | 158 | Section 8 – Interpretation. 159 | 160 | a. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License. 161 | 162 | b. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions. 163 | 164 | c. No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor. 165 | 166 | d. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority. 167 | 168 | Creative Commons is not a party to its public licenses. Notwithstanding, Creative Commons may elect to apply one of its public licenses to material it publishes and in those instances will be considered the “Licensor.” Except for the limited purpose of indicating that material is shared under a Creative Commons public license or as otherwise permitted by the Creative Commons policies published at creativecommons.org/policies, Creative Commons does not authorize the use of the trademark “Creative Commons” or any other trademark or logo of Creative Commons without its prior written consent including, without limitation, in connection with any unauthorized modifications to any of its public licenses or any other arrangements, understandings, or agreements concerning use of licensed material. For the avoidance of doubt, this paragraph does not form part of the public licenses. 169 | 170 | Creative Commons may be contacted at creativecommons.org. 171 | -------------------------------------------------------------------------------- /LICENSES/GPL-3.0-or-later.txt: -------------------------------------------------------------------------------- 1 | ../COPYING -------------------------------------------------------------------------------- /LICENSES/LicenseRef-mcrae.txt: -------------------------------------------------------------------------------- 1 | MCRAE GENERAL PUBLIC LICENSE (version 4.r53) 2 | -------------------------------------------- 3 | This license applies to any work containing a notice placed by the 4 | copyright holder (that would be ME) saying it is uses the McRae 5 | General Public License. "The work" refers to any such work. 6 | 7 | This license stipulates that it is strictly forbidden to redistribute 8 | or use the work in any manner that could possibly be construed as 9 | making anybody any money, or I'll sue you. No! You will NOT do that! 10 | Okee? And if you don't agree with these terms, you can print out the 11 | source to the work and stick it up your ARSE. 12 | 13 | Aye, but otherwise feel free to take "the work" and do what you like. 14 | If you're TOO BLOODY LAZY to do it yourself, I cannae help it. OK? 15 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ## SPDX-FileCopyrightText: 2018 M. Shulhan 2 | ## SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | .FORCE: 5 | 6 | CGO_ENABLED:=$(shell go env CGO_ENABLED) 7 | GOOS:=$(shell go env GOOS) 8 | GOARCH:=$(shell go env GOARCH) 9 | VERSION=$(shell git describe --long | sed 's/\([^-]*-g\)/r\1/;s/-/./g') 10 | 11 | COVER_OUT:=cover.out 12 | COVER_HTML:=cover.html 13 | CPU_PROF:=cpu.prof 14 | MEM_PROF:=mem.prof 15 | DEBUG= 16 | LD_FLAGS= 17 | 18 | RESCACHED_BIN=_bin/$(GOOS)_$(GOARCH)/rescached 19 | RESOLVER_BIN=_bin/$(GOOS)_$(GOARCH)/resolver 20 | RESOLVERBENCH_BIN=_bin/$(GOOS)_$(GOARCH)/resolverbench 21 | 22 | RESCACHED_MAN:=_sys/usr/share/man/man1/rescached.1.gz 23 | RESCACHED_CFG_MAN:=_sys/usr/share/man/man5/rescached.cfg.5.gz 24 | RESOLVER_MAN:=_sys/usr/share/man/man1/resolver.1.gz 25 | 26 | DIR_BIN=/usr/bin 27 | DIR_MAN=/usr/share/man 28 | DIR_RESCACHED=/usr/share/rescached 29 | 30 | ##---- Tasks for testing, linting, and building program. 31 | 32 | .PHONY: all 33 | all: lint test resolver rescached 34 | 35 | .PHONY: build 36 | build: resolver rescached 37 | 38 | ## Build with race detection. 39 | 40 | .PHONY: debug 41 | debug: CGO_ENABLED=1 42 | debug: DEBUG=-race -v 43 | debug: test build 44 | 45 | 46 | .PHONY: resolver 47 | resolver: LD_FLAGS =-X 'main.Usage=$$(go tool doc ./cmd/resolver)' 48 | resolver: LD_FLAGS+=-X 'git.sr.ht/~shulhan/rescached.Version=$(VERSION)' 49 | resolver: 50 | mkdir -p _bin/$(GOOS)_$(GOARCH) 51 | go build $(DEBUG) -ldflags="$(LD_FLAGS)" -o _bin/$(GOOS)_$(GOARCH)/ ./cmd/resolver 52 | 53 | .PHONY: rescached 54 | rescached: LD_FLAGS+=-X 'git.sr.ht/~shulhan/rescached.Version=$(VERSION)' 55 | rescached: 56 | mkdir -p _bin/$(GOOS)_$(GOARCH) 57 | go run ./cmd/rescached embed 58 | go build $(DEBUG) -ldflags="$(LD_FLAGS)" -o _bin/$(GOOS)_$(GOARCH)/ ./cmd/rescached 59 | 60 | 61 | .PHONY: test 62 | test: 63 | go test $(DEBUG) -count=1 -coverprofile=$(COVER_OUT) ./... 64 | go tool cover -html=$(COVER_OUT) -o $(COVER_HTML) 65 | 66 | .PHONY: test.prof 67 | test.prof: 68 | go test $(DEBUG) -count=1 \ 69 | -cpuprofile $(CPU_PROF) \ 70 | -memprofile $(MEM_PROF) ./... 71 | 72 | .PHONY: lint 73 | lint: 74 | -fieldalignment ./... 75 | -shadow ./... 76 | -golangci-lint run \ 77 | --presets bugs,metalinter,performance,unused \ 78 | --disable bodyclose \ 79 | --disable exhaustive \ 80 | --disable musttag \ 81 | ./... 82 | -reuse --suppress-deprecation lint 83 | 84 | 85 | ##---- Cleaning up. 86 | 87 | .PHONY: clean distclean 88 | 89 | distclean: clean 90 | go clean -i ./... 91 | 92 | clean: 93 | rm -f cmd/rescached/memfs.go 94 | rm -f testdata/rescached.pid 95 | rm -f $(COVER_OUT) $(COVER_HTML) 96 | rm -f $(RESCACHED_BIN) $(RESOLVER_BIN) $(RESOLVERBENCH_BIN) 97 | 98 | 99 | ##---- Documentation tasks. 100 | 101 | .PHONY: doc 102 | 103 | doc: $(RESCACHED_MAN) $(RESCACHED_CFG_MAN) $(RESOLVER_MAN) 104 | 105 | $(RESCACHED_MAN): README.adoc 106 | asciidoctor --backend=manpage --destination-dir=_sys/usr/share/man/man1/ $< 107 | gzip -f _sys/usr/share/man/man1/rescached.1 108 | 109 | $(RESCACHED_CFG_MAN): _www/doc/rescached.cfg.adoc 110 | asciidoctor --backend=manpage --destination-dir=_sys/usr/share/man/man5/ $< 111 | gzip -f _sys/usr/share/man/man5/rescached.cfg.5 112 | 113 | $(RESOLVER_MAN): _www/doc/resolver.adoc 114 | asciidoctor --backend=manpage --destination-dir=_sys/usr/share/man/man1/ $< 115 | gzip -f _sys/usr/share/man/man1/resolver.1 116 | 117 | 118 | ##---- Development tasks 119 | 120 | .PHONY: dev 121 | 122 | dev: 123 | go run ./cmd/rescached -dir-base=./_test -config=etc/rescached/rescached.cfg dev 124 | 125 | 126 | ##---- Common tasks for installing and uninstalling program. 127 | 128 | .PHONY: install-common uninstall-common 129 | 130 | install-common: 131 | mkdir -p $(PREFIX)/etc/rescached 132 | mkdir -p $(PREFIX)/etc/rescached/hosts.d 133 | mkdir -p $(PREFIX)/etc/rescached/zone.d 134 | 135 | cp -f _sys/etc/rescached/rescached.cfg $(PREFIX)/etc/rescached/ 136 | cp -f _sys/etc/rescached/localhost.pem $(PREFIX)/etc/rescached/ 137 | cp -f _sys/etc/rescached/localhost.pem.key $(PREFIX)/etc/rescached/ 138 | 139 | mkdir -p $(PREFIX)/etc/rescached/block.d 140 | cp -f _sys/etc/rescached/block.d/.pgl.yoyo.org $(PREFIX)/etc/rescached/block.d/ 141 | cp -f _sys/etc/rescached/block.d/.someonewhocares.org $(PREFIX)/etc/rescached/block.d/ 142 | cp -f _sys/etc/rescached/block.d/.winhelp2002.mvps.org $(PREFIX)/etc/rescached/block.d/ 143 | 144 | mkdir -p $(PREFIX)$(DIR_BIN) 145 | cp -f $(RESCACHED_BIN) $(PREFIX)$(DIR_BIN) 146 | cp -f $(RESOLVER_BIN) $(PREFIX)$(DIR_BIN) 147 | 148 | mkdir -p $(PREFIX)$(DIR_MAN)/man1 149 | mkdir -p $(PREFIX)$(DIR_MAN)/man5 150 | cp $(RESCACHED_MAN) $(PREFIX)$(DIR_MAN)/man1/ 151 | cp $(RESOLVER_MAN) $(PREFIX)$(DIR_MAN)/man1/ 152 | cp $(RESCACHED_CFG_MAN) $(PREFIX)$(DIR_MAN)/man5/ 153 | 154 | mkdir -p $(PREFIX)$(DIR_RESCACHED) 155 | cp COPYING $(PREFIX)$(DIR_RESCACHED) 156 | 157 | 158 | uninstall-common: 159 | rm -f $(PREFIX)$(DIR_RESCACHED)/COPYING 160 | 161 | rm -f $(PREFIX)$(DIR_MAN)/man5/$(RESCACHED_CFG_MAN) 162 | rm -f $(PREFIX)$(DIR_MAN)/man1/$(RESOLVER_MAN) 163 | rm -f $(PREFIX)$(DIR_MAN)/man1/$(RESCACHED_MAN) 164 | 165 | rm -f $(PREFIX)$(DIR_BIN)/resolver 166 | rm -f $(PREFIX)$(DIR_BIN)/rescached 167 | 168 | 169 | ##---- Tasks for installing and uninstalling on GNU/Linux with systemd. 170 | 171 | .PHONY: install deploy uninstall 172 | 173 | install: build install-common 174 | mkdir -p $(PREFIX)/usr/lib/systemd/system 175 | cp _sys/usr/lib/systemd/system/rescached.service $(PREFIX)/usr/lib/systemd/system/ 176 | 177 | uninstall: uninstall-common 178 | systemctl stop rescached 179 | systemctl disable rescached 180 | rm -f /usr/lib/systemd/system/rescached.service 181 | 182 | deploy: build 183 | sudo rsync _bin/$(GOOS)_$(GOARCH)/rescached $(DIR_BIN)/ 184 | sudo rsync _bin/$(GOOS)_$(GOARCH)/resolver $(DIR_BIN)/ 185 | sudo systemctl restart rescached 186 | 187 | 188 | ##---- Tasks for installing and uninstalling service on macOS. 189 | 190 | .PHONY: install-macos deploy-macos uninstall-macos 191 | 192 | install-macos: DIR_BIN=/usr/local/bin 193 | install-macos: DIR_MAN=/usr/local/share/man 194 | install-macos: DIR_RESCACHED=/usr/local/share/rescached 195 | install-macos: build install-common 196 | cp _sys/Library/LaunchDaemons/info.kilabit.rescached.plist /Library/LaunchDaemons/ 197 | 198 | deploy-macos: DIR_BIN=/usr/local/bin 199 | deploy-macos: build 200 | sudo cp _bin/$(GOOS)_$(GOARCH)/rescached $(DIR_BIN)/ 201 | sudo cp _bin/$(GOOS)_$(GOARCH)/resolver $(DIR_BIN)/ 202 | sudo launchctl stop info.kilabit.rescached 203 | 204 | uninstall-macos: DIR_BIN=/usr/local/bin 205 | uninstall-macos: DIR_MAN=/usr/local/share/man 206 | uninstall-macos: DIR_RESCACHED=/usr/local/share/rescached 207 | uninstall-macos: uninstall-common 208 | launchctl stop info.kilabit.rescached 209 | launchctl unload info.kilabit.rescached 210 | rm -f /Library/LaunchDaemons/info.kilabit.rescached.plist 211 | 212 | 213 | ##---- Tasks for deploying to public DNS server. 214 | 215 | .PHONY: deploy-personal-server 216 | 217 | build-linux-amd64: CGO_ENABLED=0 218 | build-linux-amd64: GOOS=linux 219 | build-linux-amd64: GOARCH=amd64 220 | build-linux-amd64: build 221 | 222 | deploy-personal-server: build-linux-amd64 223 | rsync --progress _bin/linux_amd64/rescached personal-server:~/bin/rescached 224 | ssh personal-server "sudo rsync ~/bin/rescached /usr/bin/rescached; sudo systemctl restart rescached.service" 225 | 226 | 227 | #---- Development. 228 | 229 | ## Run web server to test WUI. 230 | 231 | .PHONY: dev.www 232 | dev.www: 233 | go run ./internal/cmd/www 234 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2018 M. Shulhan 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | = RESCACHED(1) 4 | M. Shulhan 5 | 8 February 2022 6 | :doctype: manpage 7 | :mansource: rescached 8 | :manmanual: rescached 9 | :toc: 10 | 11 | 12 | == NAME 13 | 14 | rescached - DNS resolver cache daemon. 15 | 16 | 17 | == SYNOPSIS 18 | 19 | rescached [-config 'rescached.cfg'] 20 | 21 | 22 | == OPTIONS 23 | 24 | `rescached.cfg` is rescached configuration, usually it reside in 25 | /etc/rescached/rescached.cfg. 26 | 27 | 28 | == DESCRIPTION 29 | 30 | `rescached` is a daemon that caching internet name and address on local memory 31 | for speeding up DNS resolution. 32 | 33 | `rescached` is not a reimplementation of DNS server like BIND. 34 | 35 | `rescached` primary goal is only to caching DNS queries and answers, used by 36 | personal or small group of users, to minimize unneeded traffic to outside 37 | network. 38 | 39 | 40 | === FEATURES 41 | 42 | List of current features, 43 | 44 | * Enable to handle request from UDP and TCP connections 45 | * Enable to forward request using UDP or TCP 46 | * Load and serve addresses and host names in `/etc/hosts` 47 | * Load and serve hosts formatted files inside directory 48 | `/etc/rescached/hosts.d/` 49 | * Blocking ads and/or malicious websites through host list in 50 | `/etc/rescached/hosts.d/` 51 | * Support loading and serving zone file format from 52 | `/etc/rescached/zone.d` 53 | * Integration with openresolv 54 | * Support DNS over TLS (DoH) (RFC 7858) 55 | * Support DNS over HTTPS (DoH) (RFC 8484) 56 | 57 | 58 | === BEHIND THE DNS 59 | 60 | When you open a website, let say 'kilabit.info', in a browser, the first thing 61 | that browser do is to translate name address 'kilabit.info' into an internet 62 | address (for example to 18.136.35.199) so browser can make a connection to 63 | 'kilabit.info' server. 64 | 65 | How browser do that? 66 | 67 | First, it will send query to one of DNS server listed in your system 68 | configuration (for example, `/etc/resolv.conf` in Linux). 69 | Then, if your DNS server also "caching" the name that you requested, it will 70 | reply the answer (internet address) directly, if it is not then it will ask 71 | their parent DNS server. 72 | 73 | ---- 74 | +----+ +----------------+ +------------------+ 75 | | PC | <==> | ISP DNS Server | <==> | Other DNS Server | <==> ... 76 | +----+ +----------------+ +------------------+ 77 | ---- 78 | 79 | If you browsing frequently on the same site, hitting the refresh button, 80 | opening another page on the same website, etc; this procedures will always 81 | repeated every times, not including all external links like ads, social media 82 | button, or JavaScript from an other server. 83 | 84 | To make this repetitive procedures less occurred, you can run `rescached` in 85 | your personal computer. 86 | The first time the answer is received in your local computer, `rescached` will 87 | saved it in computer memory and any subsequent request of the same address 88 | will be answered directly by `rescached`. 89 | 90 | ---- 91 | +----+ +----------------+ +------------------+ 92 | | PC | | ISP DNS Server | <==> | Other DNS Server | <==> ... 93 | +----+ +----------------+ +------------------+ 94 | ^^ ^^ 95 | || || 96 | vv || 97 | +-----------+ || 98 | | rescached | <==// 99 | +-----------+ 100 | ---- 101 | 102 | The only request that will be send to your DNS server is the one that does not 103 | already exist in `rescached` cache. 104 | 105 | 106 | === HOW CACHE WORKS 107 | 108 | This section explain the simplified version of how internal program works. 109 | 110 | Each DNS record in cache have the time last accessed field, which defined how 111 | the cache will be ordered in memory. 112 | The last queried host-name will be at the bottom of cache list, and the oldest 113 | queried host-name will at the top of cache list. 114 | 115 | The following table illustrate list of caches in memory, 116 | 117 | ---- 118 | +---------------------+------------------+ 119 | | Accessed At | host-name | 120 | +---------------------+------------------+ 121 | | 2018-01-01 00:00:01 | kilabit.info | 122 | +---------------------+------------------+ 123 | | 2018-01-01 00:00:02 | www.google.com | 124 | +---------------------+------------------+ 125 | | ... | ... | 126 | +---------------------+------------------+ 127 | | 2018-01-01 00:01:00 | www.kilabit.info | 128 | +---------------------+------------------+ 129 | ---- 130 | 131 | Every `cache.prune_delay` (let say every 5 minutes), rescached will try to 132 | pruning old records from cache. 133 | If the accessed-at value of record in cache is less than, 134 | 135 | ---- 136 | current-time + cache.threshold 137 | ---- 138 | 139 | (remember that "cache.threshold" value must be negative) it will remove the 140 | record from cache. 141 | 142 | 143 | == BUILDING 144 | 145 | === PREREQUISITES 146 | 147 | * https://golang.org[Go compiler] 148 | * https://git-scm.com[git, version control system] 149 | * asciidoc, to generate manual pages 150 | * systemd or system V init tool for service on Linux 151 | 152 | === COMPILING 153 | 154 | Steps to compile from source, 155 | 156 | ---- 157 | $ go get -u git.sr.ht/~shulhan/rescached 158 | $ cd ${GOPATH}/src/git.sr.ht/~shulhan/rescached 159 | $ go build ./cmd/rescached 160 | ---- 161 | 162 | The last command will build binary named `rescached` in current directory. 163 | 164 | === INSTALLATION 165 | 166 | After program successfully build, you can install it manually by copying to 167 | system binary directory. 168 | 169 | ==== MANUAL INSTALLATION 170 | 171 | Copy rescached configuration to system directory. 172 | We use directory "/etc/rescached" as configuration directory. 173 | 174 | $ sudo mkdir -p /etc/rescached 175 | $ sudo cp cmd/rescached/rescached.cfg /etc/rescached/ 176 | 177 | Copy rescached program to your system path. 178 | 179 | $ sudo cp -f rescached /usr/bin/ 180 | 181 | Create system startup script. 182 | 183 | If you want your program running each time the system is starting up you can 184 | create a system startup script (or system service). 185 | For OS using systemd, you can see an example for `systemd` service in 186 | `scripts/rescached.service`. 187 | For system using launchd (macOS), you can see an example in 188 | `scripts/info.kilabit.rescached.plist`. 189 | 190 | This step could be different between systems, consult your distribution 191 | wiki, forum, or mailing-list on how to create system startup script. 192 | 193 | ==== AUTOMATIC INSTALLATION ON LINUX 194 | 195 | Automatic installation on Linux require systemd. 196 | Run the following command 197 | 198 | $ sudo make install 199 | 200 | to setup and copies all required files and binaries to system directories. 201 | You can then start the rescached service using systemd, 202 | 203 | $ sudo systemctl start rescached 204 | 205 | ==== AUTOMATIC INSTALLATION ON MACOS 206 | 207 | Run the following command 208 | 209 | $ sudo make install-macos 210 | 211 | to setup and copies all required files and binaries to system directories. 212 | You can then load the rescached service using launchd, 213 | 214 | $ sudo launchctl load info.kilabit.rescached 215 | 216 | 217 | ==== POST INSTALLATION 218 | 219 | * Set your parent DNS server. 220 | + 221 | Edit rescached configuration, `/etc/rescached/rescached.cfg`, change the value 222 | of `parent` based on your preferred DNS server. 223 | 224 | * Set the cache prune delay and threshold 225 | + 226 | Edit rescached configuration, `/etc/rescached/rescached.cfg`, change the value 227 | of `cache.prune_delay` and/or `cache.threshold` to match your needs. 228 | 229 | * Set your system DNS server to point to rescached. 230 | + 231 | -- 232 | In UNIX system, 233 | 234 | $ sudo mv /etc/resolv.conf /etc/resolv.conf.org 235 | $ sudo echo "nameserver 127.0.0.1" > /etc/resolv.conf 236 | -- 237 | 238 | * If you use `systemd`, run `rescached` service by invoking, 239 | + 240 | $ sudo systemctl start rescached.service 241 | + 242 | and if you want `rescached` service to run when system startup, enable it by 243 | invoking, 244 | + 245 | $ sudo systemctl enable rescached.service 246 | 247 | 248 | == CONFIGURATION 249 | 250 | All rescached configuration located in file `/etc/rescached/rescached.cfg`. 251 | See manual page of *rescached.cfg*(5) for more information. 252 | 253 | === ZONE FILE 254 | 255 | Rescached support loading zone file format. 256 | Unlike hosts file format, where each domain name is only mapped to type A 257 | (IPv4 address), in zone file, one can define other type that known to 258 | rescached. 259 | All files defined `zone.d` configuration are considered as zone file and 260 | will be loaded by rescached only if the configuration is not empty. 261 | 262 | Example of zone file, 263 | 264 | ---- 265 | $ORIGIN my-site.vm. 266 | $TTL 3600 267 | 268 | ; resource record (RR) address 269 | @ A 192.168.56.10 270 | 271 | ; resource record alias 272 | dev CNAME @ 273 | 274 | ; resource record address for other sub-domain 275 | staging A 192.168.100.1 276 | 277 | ; resource record address for other absolute domain. 278 | my-site.com A 10.8.0.1 279 | ---- 280 | 281 | Here we defined the variable origin for root domain "my-site.vm." with minimum 282 | time-to-live (TTL) to 3600 seconds. 283 | If no "$ORIGIN" variable is defined, rescached will use the file name as 284 | $ORIGIN's value. 285 | 286 | The "@" character will be replaced with the value of $ORIGIN. 287 | 288 | The first resource record (RR) is defining an IPv4 address for "my-site.vm." 289 | to "192.168.56.10". 290 | 291 | The second RR add an alias for relative subdomain "dev". 292 | Domain name that does not terminated with "." are called relative, and 293 | the origin will be appended to form the absolute domain "dev.my-site.vm". 294 | In this case IP address for "dev.my-site.vm." is equal to "my-site.vm.". 295 | 296 | The third RR define a mapping for another relative subdomain 297 | "staging.my-site.vm." to address "192.168.100.1". 298 | 299 | The last RR define a mapping for absolute domain "my-site.com." to IP 300 | address "10.8.0.1". 301 | 302 | For more information about format of zone file see RFC 1035 section 5. 303 | 304 | 305 | === INTEGRATION WITH OPENRESOLV 306 | 307 | Rescached can detect change on file generated by resolvconf. 308 | To use this feature unset the "file.resolvconf" in configuration file and set 309 | either "dnsmasq_resolv", "pdnsd_resolv", or "unbound_conf" in 310 | "/etc/resolvconf.conf" to point to file referenced in "file.resolvconf". 311 | 312 | For more information see *rescached.cfg*(5). 313 | 314 | 315 | === INTEGRATION WITH DNS OVER HTTPS 316 | 317 | DNS over HTTPS (DoH) is the new protocol to query DNS through HTTPS layer. 318 | Rescached support serving DNS over HTTPS or as client to parent DoH 319 | nameservers. 320 | To enable this feature rescached provided TLS certificate and private key. 321 | 322 | Example configuration in *rescached.cfg*, 323 | 324 | ---- 325 | [dns "server"] 326 | parent = https://kilabit.info/dns-query 327 | tls.certificate = /etc/rescached/localhost.cert.pem 328 | tls.private_key = /etc/rescached/localhost.key.pem 329 | tls.allow_insecure = false 330 | ---- 331 | 332 | If the parent nameserver is using self-signed certificate, you can set 333 | "tls.allow_insecure" to true. 334 | 335 | Using the above configuration, rescached will serve DoH queries on 336 | https://localhost/dns-query on port 443 and UDP queries on port 53. 337 | All queries to both locations will be forwarded to parent nameserver. 338 | 339 | This feature can be tested using Firefox Nightly by updating the configuration 340 | in "about:config" into, 341 | 342 | ---- 343 | network.trr.bootstrapAddress;127.0.0.1 344 | network.trr.mode;3 345 | network.trr.uri;https://localhost/dns-query 346 | ---- 347 | 348 | Since we are using `mode=3`, the `network.trr.bootstrapAddress` is required so 349 | Firefox Nightly can resolve "localhost" to "127.0.0.1". 350 | If you use the provided self-signed certificate, you must import and/or enable 351 | an exception for it manually in Firefox Nightly (for example. by opening 352 | https://localhost/dns-query in new tab and accept security risk). 353 | 354 | To check if DoH works, first, set the `debug` option to `1`, and 355 | restart the rescached. 356 | Open a new terminal and run `sudo journalctl -xf`, to show current system log. 357 | Run Firefox Nightly and open any random website. 358 | At the terminal you will see output from rescached which looks like these, 359 | 360 | ---- 361 | ... rescached[808]: dns: ^ DoH https://kilabit.info/dns-query 41269:&{Name:id.wikipedia.org Type:A} 362 | ... rescached[808]: dns: < UDP 45873:&{Name:id.wikipedia.org Type:AAAA} 363 | ... rescached[808]: dns: + UDP 41269:&{Name:id.wikipedia.org Type:A} 364 | ---- 365 | 366 | If you see number "4" in request line, "< request: 4", thats indicated that 367 | request is from HTTPS connection and its working. 368 | 369 | 370 | == WEB USER INTERFACE 371 | 372 | The rescached service provide a web user interface that can be accessed at 373 | http://127.0.0.1:5380. 374 | 375 | .Screenshot of front page 376 | image:https://raw.githubusercontent.com/shuLhan/rescached-go/master/_www/doc/images/Screenshot_wui_frontpage.png[Screenshot 377 | of rescached front page,320] 378 | 379 | The front page allow user to monitor active caches, query the caches, and 380 | removing the caches. 381 | 382 | .Screenshot of Environment page 383 | image:https://raw.githubusercontent.com/shuLhan/rescached-go/master/_www/doc/images/Screenshot_wui_environment.png[rescached environment page,320] 384 | 385 | The Environment page allow user to modify the rescached configuration on the 386 | fly. 387 | 388 | .Screenshot of Hosts Blocks page 389 | image:https://raw.githubusercontent.com/shuLhan/rescached-go/master/_www/doc/images/Screenshot_wui_hosts_blocks.png[rescached 390 | Hosts Blocks page,320] 391 | 392 | The Hosts Blocks page allow user to enable or disable the external sources of 393 | hosts blocks list. 394 | 395 | .Screenshot of Hosts.d page 396 | image:https://raw.githubusercontent.com/shuLhan/rescached-go/master/_www/doc/images/Screenshot_wui_hosts_d.png[rescached 397 | Hosts.d page,320] 398 | 399 | The Hosts.d page allow user to manage hosts file, creating new hosts file, 400 | create new record, or delete a record. 401 | 402 | .Screenshot of Zone.d page 403 | image:https://raw.githubusercontent.com/shuLhan/rescached-go/master/_www/doc/images/Screenshot_wui_zone_d.png[rescached 404 | Zone.d page,320] 405 | 406 | The Zone.d page allow user manage zone file, creating new zone file, adding or 407 | deleting new resource record in the zone file. 408 | 409 | 410 | == EXIT STATUS 411 | 412 | Upon success, `rescached` will return 0, or 1 otherwise. 413 | 414 | 415 | == FILES 416 | 417 | `/etc/rescached/rescached.cfg`:: The `rescached` main configuration. 418 | This configuration will be read when program started. 419 | 420 | `/usr/share/rescached/COPYING`:: License file for this software. 421 | 422 | `/var/run/rescached.pid`:: File where process ID of rescached will be saved 423 | when running. 424 | 425 | 426 | == NOTES 427 | 428 | This program developed with references to, 429 | 430 | RFC1034:: Domain Names - Concepts and Facilities. 431 | RFC1035:: Domain Names - Implementation and Specification. 432 | RFC1886:: DNS Extensions to support IP version 6. 433 | RFC2782:: A DNS RR for specifying the location of services (DNS SRV) 434 | RFC8484:: DNS Queries over HTTPS (DoH) 435 | 436 | == BUGS 437 | 438 | `rescached` only know specific DNS record type, 439 | 440 | [horizontal] 441 | A:: A host address in IPv4 442 | NS:: An authoritative name server 443 | CNAME:: A canonical name for an alias 444 | SOA:: Start of [a zone of] authority record 445 | MB:: Mail box 446 | MG:: Mail group 447 | NULL:: Placeholders for experimental extensions 448 | WKS:: Record to describe well-known services supported by a host 449 | PTR:: Pointer to a canonical name. 450 | HINFO:: Host information 451 | MINFO:: Mail information 452 | MX:: Mail exchange 453 | TXT:: Text record 454 | AAAA:: A host address in IPv6 455 | SRV:: Service locator 456 | OPT:: This is a "pseudo DNS record type" needed to support EDNS 457 | 458 | `rescached` only run and tested in Linux and macOS system. 459 | Technically, if it can compiled, it will run in any operating system. 460 | 461 | 462 | == AUTHOR 463 | 464 | `rescached` is developed by Shulhan (ms@kilabit.info). 465 | 466 | 467 | == LICENSE 468 | 469 | Copyright 2018, M. Shulhan (ms@kilabit.info). 470 | All rights reserved. 471 | 472 | Use of this source code is governed by a GPL 3.0 license that can be found 473 | in the COPYING file. 474 | 475 | 476 | == LINKS 477 | 478 | The project for this software is available at 479 | https://sr.ht/~shulhan/rescached. 480 | 481 | For request of features and/or bugs report please submitted through web at 482 | https://todo.sr.ht/~shulhan/rescached. 483 | 484 | 485 | == SEE ALSO 486 | 487 | *rescached.cfg*(5) 488 | -------------------------------------------------------------------------------- /README.adoc: -------------------------------------------------------------------------------- 1 | README -------------------------------------------------------------------------------- /REUSE.toml: -------------------------------------------------------------------------------- 1 | ## SPDX-FileCopyrightText: 2024 M. Shulhan 2 | ## 3 | ## SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | version = 1 6 | 7 | [[annotations]] 8 | path = [".gitignore", ".gitmodules", ".ignore", "go.sum"] 9 | SPDX-FileCopyrightText = "2018 Shulhan " 10 | SPDX-License-Identifier = "GPL-3.0-or-later" 11 | 12 | [[annotations]] 13 | path = ["_www/doc/rescached.cfg.5.gz", "_www/doc/resolver.1.gz"] 14 | SPDX-FileCopyrightText = "2020 Shulhan " 15 | SPDX-License-Identifier = "GPL-3.0-or-later" 16 | 17 | [[annotations]] 18 | path = ["_www/doc/images/**", "rescached.1.gz", "testdata/**", "_sys/**" 19 | , "_test/**", "_www/favicon.png"] 20 | SPDX-FileCopyrightText = "2021 Shulhan " 21 | SPDX-License-Identifier = "GPL-3.0-or-later" 22 | 23 | [[annotations]] 24 | path = ["_sys/etc/rescached/block.d/.pgl.yoyo.org"] 25 | SPDX-FileCopyrightText = "McRae " 26 | SPDX-License-Identifier = "LicenseRef-mcrae" 27 | 28 | [[annotations]] 29 | path = ["_sys/etc/rescached/block.d/.winhelp2002.mvps.org"] 30 | SPDX-FileCopyrightText = "WinHelp2002 " 31 | SPDX-License-Identifier = "CC-BY-NC-SA-4.0" 32 | -------------------------------------------------------------------------------- /_bin/nft_dnstest_chain.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | ## SPDX-FileCopyrightText: 2021 M. Shulhan 3 | ## SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | 6 | if [[ "$1" == "flush" ]]; then 7 | echo "nft: delete chain dnstest"; 8 | nft delete chain ip nat dnstest; 9 | exit 0 10 | fi 11 | 12 | ## Forward port 53 to 5350 for testing. 13 | 14 | nft -- add chain ip nat dnstest { type nat hook output priority 0 \; } 15 | nft add rule ip nat dnstest tcp dport 53 redirect to 5350 16 | nft add rule ip nat dnstest udp dport 53 redirect to 5350 17 | -------------------------------------------------------------------------------- /_sys/Library/LaunchDaemons/info.kilabit.rescached.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Label 8 | info.kilabit.rescached 9 | ProgramArguments 10 | 11 | /usr/local/bin/rescached 12 | -config 13 | /etc/rescached/rescached.cfg 14 | 15 | RunAtLoad 16 | 17 | KeepAlive 18 | 19 | StandardOutPath 20 | /var/log/rescached.log 21 | StandardErrorPath 22 | /var/log/rescached.error 23 | 24 | 25 | -------------------------------------------------------------------------------- /_sys/etc/init.d/rescached.run: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | ## SPDX-FileCopyrightText: 2018 M. Shulhan 3 | ## SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | ### BEGIN INIT INFO 6 | # Provides: rescached 7 | # Required-Start: $syslog $remote_fs 8 | # Required-Stop: $syslog $remote_fs 9 | # Default-Start: 3 5 10 | # Default-Stop: 0 1 2 6 11 | # Short-Description: resolver cache daemon. 12 | # Description: resolver cache daemon. 13 | ### END INIT INFO 14 | 15 | RESCACHED_BIN=/usr/bin/rescached 16 | RESCACHED_CFG=/etc/rescached/rescached.cfg 17 | 18 | # 19 | # check if program exist. 20 | # 21 | test -x ${RESCACHED_BIN} || { 22 | echo "Program '${RESCACHED_BIN}' not installed"; 23 | if [ "$1" = "stop" ]; then 24 | exit 0; 25 | else 26 | exit 5; 27 | fi; 28 | } 29 | 30 | # 31 | # check if configuration file exist. 32 | # 33 | test -r ${RESCACHED_CFG} || { 34 | echo "File '${RESCACHED_CFG}' not existing"; 35 | if [ "$1" = "stop" ]; then 36 | exit 0; 37 | else 38 | exit 6; 39 | fi; 40 | } 41 | 42 | case "$1" in 43 | start) 44 | echo -n "Starting rescached " 45 | ${RESCACHED_BIN} ${RESCACHED_CFG} & 46 | if test $? = 0; then 47 | echo "[OK]"; 48 | else 49 | echo "[FAIL]"; 50 | fi; 51 | ;; 52 | 53 | stop) 54 | echo -n "Shutting down rescached " 55 | 56 | killall ${RESCACHED_PID}; 57 | 58 | if test $? = 0; then 59 | echo "[OK]"; 60 | else 61 | echo "[not running]"; 62 | fi; 63 | ;; 64 | 65 | restart) 66 | $0 stop 67 | $0 start 68 | ;; 69 | 70 | status) 71 | echo -n "Checking for service rescached " 72 | RESCACHED_PS=`ps -ef | grep "rescached -config"` 73 | if [[ "${RESCACHED_PS}" -ne "" ]]; then 74 | echo "[running]"; 75 | else 76 | echo "[not running]"; 77 | fi; 78 | ;; 79 | 80 | *) 81 | echo "Usage: $0 {start|stop|restart|status}" 82 | exit 1 83 | ;; 84 | esac 85 | -------------------------------------------------------------------------------- /_sys/etc/rescached/localhost.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDejCCAmICCQCnpfFMJ730PjANBgkqhkiG9w0BAQUFADB/MQswCQYDVQQGEwJJ 3 | RDEVMBMGA1UECgwMaW5mby5raWxhYml0MR8wHQYDVQQLDBZpbmZvLmtpbGFiaXQu 4 | cmVzY2FjaGVkMRgwFgYDVQQDDA9yZXNjYWNoZWQubG9jYWwxHjAcBgkqhkiG9w0B 5 | CQEWD21zQGtpbGFiaXQuaW5mbzAeFw0yMTAxMjQxNDQ3MDhaFw0zMTAxMjIxNDQ3 6 | MDhaMH8xCzAJBgNVBAYTAklEMRUwEwYDVQQKDAxpbmZvLmtpbGFiaXQxHzAdBgNV 7 | BAsMFmluZm8ua2lsYWJpdC5yZXNjYWNoZWQxGDAWBgNVBAMMD3Jlc2NhY2hlZC5s 8 | b2NhbDEeMBwGCSqGSIb3DQEJARYPbXNAa2lsYWJpdC5pbmZvMIIBIjANBgkqhkiG 9 | 9w0BAQEFAAOCAQ8AMIIBCgKCAQEAusynRTMarGqKB1T8RVSy8JGzw9h0AtuiQuUO 10 | tk5YD6XUxKRFndRwnnP1KsP6JleX8hE+SLRUn0uM+Neo+M+EyQ8Kn2QMkE6Fn04n 11 | o/y/n94xtSDJWmo5Z7qPzRVLfjVHe0PzRs0GZf6FSKOiFJ+rFJgPTnPprnJ1ghuR 12 | jr/1Oatolq/3CzIDrt/kqaQablvHTn7nk8IONLfqOK3YxAxxgk8bvmMCGlsRG2/p 13 | J1dNCEKpV6FedvHT2KiPS73H19Pp+jyAJa6+G+Q7riVxw1knWWamjE95JH6m6ju5 14 | rUFydX6V63U+65EJoQ1ieqIhfh7Fg+ZFqPv8wFuR1cw6Gr2dXQIDAQABMA0GCSqG 15 | SIb3DQEBBQUAA4IBAQChJw/+7NQtOgfNtykJzX7/M5i8uyG6vupT88C7Y7p0aINH 16 | EpbhrBx3WLQUHKAYE2gyLtBW5UtJFRUFRWy9h/FQxnilOrptF1UPiPlcsyvM160N 17 | T1tjz4mofhtL7kSYm1W2rFHzYH+acoq1p7GrcxNqzJbMmyQKYxzZeoeWRFyk442L 18 | lI4YV5/qRZidWoFH6pykN7YSKv0SO+m4U1f0v8CzgV+VyLcvXF4E9kWuKuSBiAUH 19 | KUvep4ECrcArKuCrAtmAtpUYUQFO60SPCTsuEqZ0SnBIovvwEpMFYK6zoOP6eP5R 20 | 8f2nAjyVsuon9Sz/Vh/InG3v3kbxy+EZKQRWEhFx 21 | -----END CERTIFICATE----- 22 | -------------------------------------------------------------------------------- /_sys/etc/rescached/localhost.pem.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEAusynRTMarGqKB1T8RVSy8JGzw9h0AtuiQuUOtk5YD6XUxKRF 3 | ndRwnnP1KsP6JleX8hE+SLRUn0uM+Neo+M+EyQ8Kn2QMkE6Fn04no/y/n94xtSDJ 4 | Wmo5Z7qPzRVLfjVHe0PzRs0GZf6FSKOiFJ+rFJgPTnPprnJ1ghuRjr/1Oatolq/3 5 | CzIDrt/kqaQablvHTn7nk8IONLfqOK3YxAxxgk8bvmMCGlsRG2/pJ1dNCEKpV6Fe 6 | dvHT2KiPS73H19Pp+jyAJa6+G+Q7riVxw1knWWamjE95JH6m6ju5rUFydX6V63U+ 7 | 65EJoQ1ieqIhfh7Fg+ZFqPv8wFuR1cw6Gr2dXQIDAQABAoIBAG/yrlw+YEHsJ4R1 8 | Xip+tC6QY1d/pScBUaEdfU+sbAIUtAqVGFOaOVP80nUqtgO8gwdDZjxUNlKxCG8p 9 | b86NL1r/dLJJV240YMg0InWYx46brtaKK6HP/082829Iz9F3RLuO4YEQ5kDB5EbA 10 | KiaJ+hGBf8rYlLdDSUEMHJOcXu6L0G5Qu+37zYneAo1qBr2o37JW98HLJ1qgU+Gf 11 | sflrlKh9N9xqYianXv8jRTdexk74SgnTIOnn3aBk2lHsy+nGF2dJsdv97JsRAL/j 12 | MjhTw+DsuYYc10VwUZy9Dxt9bzuzxZpa7NNVfxmAeiYw4nY47jaY5IhKAe2WBFNq 13 | vc1Kc9ECgYEA66hLHsRKdksGYQL32+t5Zd1xIEZLQ6g/TU7iiuzOwiNQ0IuQ3nuq 14 | ILWCqDremaUWGaK4Dr+T1OigXRg858yTcHg6/UH7YeBFWcaIPKdKiFRIsVSPgMlb 15 | pmQCoJWsYATcKN/DIv9v92P6hQ1g6ABmGekZ5+cb0f5X2z5JBgLv/vcCgYEAyuyq 16 | Y9kkvwUiJdkeQW7xGjfjCHOJPh0lbV8YKqma4Og/6hianYZ8jMLVDnvDQWPBvPrU 17 | H4dWcCPfdD/Qq1X8p2lVsKJ4Ww2ei3HLp2LG+WgiWRtXmGWEGZk89SlVcFfzbD/q 18 | EJD+JSWwoNgn542NDJhhI1/oLsaNJt/KNySnrUsCgYEAkFPwPhWmLTDh5UR2HSDo 19 | pvSqtkOXEQbYTjbEFKXYM5qBglgYD8rZdVL1hKcZcixjjqvT4mR+2+TlYl7X3ney 20 | zS01o6pnlZhPoR4wjkU/JqPIKaNKiGvKT+vsmAFTIzOWywnQb3zWTEPVSOvar/ye 21 | i7vx+8/VgBUwJbzN6HqgFh0CgYBOAYZKlcmLaMTEud7olmY2hu9Oa2OBriCaF6kp 22 | lUNFW+Jd8hFVpsIwNiFCzQ61D00FgYKTkCoJN7EJdhKYGpjiHhrjqMENd4HP5vG7 23 | qbwFWiOCD4Gvwq5yTLbjI32FjzmzDirDLYmU7BUm75D/cSmcguMsfwy5FnhiTjrk 24 | 0cFnWwKBgHXqW4SOvEDxw5Fsq9ULH9yTeo8VPiIDEqO6n9itRpl8PAfFARVxnRWt 25 | o6A/QDrgMlPhvG1VQKj3vZkJs4f8iP7cbAmS+eSP4EnQvN7YXH6UQVywha5KrBNw 26 | gBJrIOtcJyBcrmbHGmYsZdx+MzaC/HWSwUE1s43MDzDJiP+OAgfv 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /_sys/etc/rescached/rescached.cfg: -------------------------------------------------------------------------------- 1 | ## SPDX-FileCopyrightText: 2018 M. Shulhan 2 | ## SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | ## 5 | ## Rescached configuration. 6 | ## 7 | ## See rescached.cfg(5) for description of each options. 8 | ## 9 | 10 | [rescached] 11 | file.resolvconf= 12 | debug=0 13 | wui.listen = 127.0.0.1:5380 14 | 15 | [block.d "pgl.yoyo.org"] 16 | name = pgl.yoyo.org 17 | url = http://pgl.yoyo.org/adservers/serverlist.php?hostformat=hosts&showintro=0&startdate[day]=&startdate[month]=&startdate[year]=&mimetype=plaintext 18 | 19 | [block.d "someonewhocares.org"] 20 | name = someonewhocares.org 21 | url = http://someonewhocares.org/hosts/hosts 22 | 23 | [block.d "winhelp2002.mvps.org"] 24 | name = winhelp2002.mvps.org 25 | url = http://winhelp2002.mvps.org/hosts.txt 26 | 27 | [dns "server"] 28 | parent=udp://1.1.1.1 29 | ## DNS over TLS 30 | #parent=https://1.1.1.1 31 | ## DNS over HTTPS 32 | #parent=https://kilabit.info/dns-query 33 | 34 | listen = 127.0.0.1:53 35 | ## Uncomment line below if you want to serve DNS to other computers. 36 | #listen = 0.0.0.0:53 37 | 38 | #http.port = 443 39 | #tls.port = 853 40 | 41 | #tls.certificate = /etc/rescached/localhost.pem 42 | #tls.private_key = /etc/rescached/localhost.pem.key 43 | tls.allow_insecure = true 44 | #doh.behind_proxy = false 45 | 46 | #cache.prune_delay = 1h0m0s 47 | #cache.prune_threshold = -1h0m0s 48 | -------------------------------------------------------------------------------- /_sys/usr/lib/systemd/system/rescached.service: -------------------------------------------------------------------------------- 1 | ## SPDX-FileCopyrightText: 2020 M. Shulhan 2 | ## SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | [Unit] 5 | Description=Resolver Cache Daemon 6 | Wants=network-online.target nss-lookup.target 7 | After=network.target network-online.target 8 | Before=nss-lookup.target 9 | 10 | [Service] 11 | Type=simple 12 | ExecStart=/usr/bin/rescached -config /etc/rescached/rescached.cfg 13 | Restart=on-failure 14 | StandardError=journal 15 | 16 | [Install] 17 | WantedBy=multi-user.target 18 | -------------------------------------------------------------------------------- /_sys/usr/share/man/man1/rescached.1.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shuLhan/rescached/2817846b12942dc0624c6b6ef7de6e608c41c3f7/_sys/usr/share/man/man1/rescached.1.gz -------------------------------------------------------------------------------- /_sys/usr/share/man/man1/resolver.1.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shuLhan/rescached/2817846b12942dc0624c6b6ef7de6e608c41c3f7/_sys/usr/share/man/man1/resolver.1.gz -------------------------------------------------------------------------------- /_sys/usr/share/man/man5/rescached.cfg.5.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shuLhan/rescached/2817846b12942dc0624c6b6ef7de6e608c41c3f7/_sys/usr/share/man/man5/rescached.cfg.5.gz -------------------------------------------------------------------------------- /_test/etc/rescached/block.d/.a.block: -------------------------------------------------------------------------------- 1 | 127.0.0.1 a.block 2 | -------------------------------------------------------------------------------- /_test/etc/rescached/block.d/.b.block: -------------------------------------------------------------------------------- 1 | 127.0.0.1 b.block 2 | -------------------------------------------------------------------------------- /_test/etc/rescached/block.d/.c.block: -------------------------------------------------------------------------------- 1 | 127.0.0.1 c.block 2 | -------------------------------------------------------------------------------- /_test/etc/rescached/hosts.d/hosts: -------------------------------------------------------------------------------- 1 | # Static table lookup for hostnames. 2 | # See hosts(5) for details. 3 | 127.0.0.1 localhost 4 | ::1 localhost 5 | -------------------------------------------------------------------------------- /_test/etc/rescached/rescached.cfg: -------------------------------------------------------------------------------- 1 | ## SPDX-FileCopyrightText: 2020 M. Shulhan 2 | ## SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | ## 5 | ## Rescached configuration. 6 | ## 7 | ## See rescached.cfg(5) for description of each options. 8 | ## 9 | 10 | [rescached] 11 | file.resolvconf= 12 | debug=1 13 | wui.listen = 127.0.0.1:5381 14 | 15 | [block.d "a.block"] 16 | name = a.block 17 | url = http://127.0.0.1:11180/hosts/a 18 | 19 | [block.d "b.block"] 20 | name = b.block 21 | url = http://127.0.0.1:11180/hosts/b 22 | 23 | [block.d "c.block"] 24 | name = c.block 25 | url = http://127.0.0.1:11180/hosts/c 26 | 27 | [dns "server"] 28 | parent=udp://10.8.0.1 29 | #parent=tcp://62.171.181.13 30 | ## DNS over TLS 31 | #parent=https://10.8.0.1 32 | ## DNS over HTTPS 33 | #parent=https://kilabit.info/dns-query 34 | 35 | listen = 127.0.0.1:5350 36 | ## Uncomment line below if you want to serve DNS to other computers. 37 | #listen = 0.0.0.0:53 38 | 39 | #http.port = 443 40 | #tls.port = 853 41 | #tls.certificate = /etc/rescached/localhost.pem 42 | #tls.private_key = /etc/rescached/localhost.pem.key 43 | tls.allow_insecure = true 44 | #doh.behind_proxy = false 45 | 46 | #cache.prune_delay = 1h0m0s 47 | #cache.prune_threshold = -1h0m0s 48 | -------------------------------------------------------------------------------- /_www/block.d/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | rescached | hosts blocks 12 | 13 | 55 | 56 | 57 | 58 | 71 | 72 |
73 | 74 |

Configure the source of blocked hosts file.

75 | 76 |
77 |
78 | Enabled 79 | Name 80 |
81 |
82 |
83 | 84 |
85 | 86 |
87 | 88 | 89 | 90 | 167 | 168 | 169 | 170 | -------------------------------------------------------------------------------- /_www/doc/CHANGELOG.adoc: -------------------------------------------------------------------------------- 1 | ../../CHANGELOG.adoc -------------------------------------------------------------------------------- /_www/doc/README.adoc: -------------------------------------------------------------------------------- 1 | ../../README.adoc -------------------------------------------------------------------------------- /_www/doc/benchmark.adoc: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2020 M. Shulhan 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | = Benchmark 4 | 5 | Commit: e670b34 6 | Build: normal 7 | 8 | Config options, 9 | 10 | ---- 11 | dir.hosts=/etc/rescached/hosts.d 12 | dir.master=/etc/rescached/master.d 13 | debug = 0 14 | ---- 15 | 16 | == resolverbench 17 | 18 | Result of benchmarking with local blocked host file, 19 | 20 | ---- 21 | master ms 0 % ./resolverbench 127.0.0.1:53 scripts/hosts.block 22 | = Benchmarking with 27367 messages 23 | = Total: 27367 24 | = Failed: 0 25 | = Elapsed time: 1.053238347s 26 | ---- 27 | 28 | == dnstrace 29 | 30 | Result of benchmarking with 10000 query and 100 concurrent connections, 31 | 32 | ---- 33 | master ms 0 % dnstrace --recurse --codes --io-errors -s 127.0.0.1:53 -t A -n 10000 -c 100 redsift.io 34 | Benchmarking 127.0.0.1:53 via udp with 100 conncurrent requests 35 | 36 | Total requests: 1000000 of 1000000 (100.0%) 37 | DNS success codes: 1000000 38 | 39 | DNS response codes 40 | NOERROR: 1000000 41 | 42 | Time taken for tests: 10.318186376s 43 | Questions per second: 96916.3 44 | 45 | DNS timings, 1000000 datapoints 46 | min: 0s 47 | mean: 1.017194ms 48 | [+/-sd]: 770.525µs 49 | max: 39.845887ms 50 | 51 | DNS distribution, 1000000 datapoints 52 | LATENCY | | COUNT 53 | +-------------+---------------------------------------------+--------+ 54 | 131.071µs | | 1722 55 | 393.215µs | ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ | 115890 56 | 655.359µs | ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ | 185089 57 | 917.503µs | ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ | 316551 58 | 1.179647ms | ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ | 300305 59 | 1.441791ms | ▄▄▄▄ | 31218 60 | 1.703935ms | ▄▄ | 12005 61 | 1.966079ms | ▄ | 6387 62 | 2.228223ms | ▄ | 5007 63 | 2.490367ms | | 3196 64 | 2.752511ms | | 2573 65 | 3.014655ms | | 2486 66 | 3.276799ms | | 2012 67 | 3.538943ms | | 1814 68 | 3.801087ms | | 1806 69 | 4.063231ms | | 1512 70 | 4.325375ms | | 1099 71 | 4.587519ms | | 1077 72 | 4.849663ms | | 785 73 | 5.111807ms | | 759 74 | 5.373951ms | | 901 75 | 5.636095ms | | 765 76 | 5.898239ms | | 874 77 | 6.160383ms | | 654 78 | 6.422527ms | | 476 79 | 6.684671ms | | 351 80 | 6.946815ms | | 294 81 | 7.208959ms | | 245 82 | 7.471103ms | | 292 83 | 7.733247ms | | 261 84 | 7.995391ms | | 255 85 | 8.257535ms | | 132 86 | 8.650751ms | | 396 87 | 9.175039ms | | 193 88 | 9.699327ms | | 78 89 | 10.223615ms | | 51 90 | 10.747903ms | | 102 91 | 11.272191ms | | 23 92 | 11.796479ms | | 0 93 | 12.320767ms | | 0 94 | 12.845055ms | | 0 95 | 13.369343ms | | 0 96 | 13.893631ms | | 0 97 | 14.417919ms | | 0 98 | 14.942207ms | | 0 99 | 15.466495ms | | 0 100 | 15.990783ms | | 0 101 | 16.515071ms | | 0 102 | 17.301503ms | | 0 103 | 18.350079ms | | 0 104 | 19.398655ms | | 192 105 | 20.447231ms | | 112 106 | 21.495807ms | | 0 107 | 22.544383ms | | 0 108 | 23.592959ms | | 0 109 | 24.641535ms | | 12 110 | 25.690111ms | | 28 111 | 26.738687ms | | 14 112 | 27.787263ms | | 5 113 | 28.835839ms | | 0 114 | 29.884415ms | | 0 115 | 30.932991ms | | 0 116 | 31.981567ms | | 0 117 | 33.030143ms | | 0 118 | 34.603007ms | | 0 119 | 36.700159ms | | 0 120 | 38.797311ms | | 1 121 | ---- 122 | 123 | == Credits 124 | 125 | - https://github.com/redsift/dnstrace[dnstrace] 126 | -------------------------------------------------------------------------------- /_www/doc/html.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | rescached | doc 12 | 13 | 14 | 15 | 28 | 29 |
30 |
{{.Body}}
31 |
32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /_www/doc/images/Screenshot_wui_environment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shuLhan/rescached/2817846b12942dc0624c6b6ef7de6e608c41c3f7/_www/doc/images/Screenshot_wui_environment.png -------------------------------------------------------------------------------- /_www/doc/images/Screenshot_wui_frontpage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shuLhan/rescached/2817846b12942dc0624c6b6ef7de6e608c41c3f7/_www/doc/images/Screenshot_wui_frontpage.png -------------------------------------------------------------------------------- /_www/doc/images/Screenshot_wui_hosts_blocks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shuLhan/rescached/2817846b12942dc0624c6b6ef7de6e608c41c3f7/_www/doc/images/Screenshot_wui_hosts_blocks.png -------------------------------------------------------------------------------- /_www/doc/images/Screenshot_wui_hosts_d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shuLhan/rescached/2817846b12942dc0624c6b6ef7de6e608c41c3f7/_www/doc/images/Screenshot_wui_hosts_d.png -------------------------------------------------------------------------------- /_www/doc/images/Screenshot_wui_zone_d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shuLhan/rescached/2817846b12942dc0624c6b6ef7de6e608c41c3f7/_www/doc/images/Screenshot_wui_zone_d.png -------------------------------------------------------------------------------- /_www/doc/index.adoc: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 M. Shulhan 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | = rescached 4 | :toc: 5 | :sectanchors: 6 | :sectlinks: 7 | 8 | == Documentation 9 | 10 | link:benchmark.html[Benchmark^]:: The benchmark result of rescached server. 11 | 12 | link:CHANGELOG.html[CHANGELOG^]:: Log for each release. 13 | 14 | link:README.html[rescached^]:: Manual page for rescached program. 15 | 16 | link:rescached.cfg.html[rescached.cfg^]:: Manual page for rescached 17 | configuration. 18 | 19 | link:resolver.html[resolver^]:: Manual page for resolver. 20 | 21 | == Development 22 | 23 | https://git.sr.ht/~shulhan/rescached[Repository^]:: 24 | Link to the source code. 25 | 26 | https://todo.sr.ht/~shulhan/rescached[Issues^]:: 27 | List of open issues. 28 | 29 | https://lists.sr.ht/~shulhan/rescached[Patches^]:: 30 | Link to submit the patches. 31 | 32 | == Todo 33 | 34 | * Zoned create should fill default SOA 35 | 36 | * zone.d rr add - check for duplicate value. 37 | 38 | * Prioritize the order of hosts file to be loaded: 39 | ** block.d 40 | ** hosts.d 41 | ** zone.d 42 | 43 | * Generate unique ID for each RR in caches/zone for deletion. 44 | 45 | * Support DNSSec 46 | -------------------------------------------------------------------------------- /_www/doc/rescached.cfg.adoc: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2020 M. Shulhan 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | = RESCACHED.CONF(5) 4 | :doctype: manpage 5 | :man source: rescached.cfg 6 | :man version: 2020.05.10 7 | :man manual: rescached.cfg 8 | 9 | 10 | == NAME 11 | 12 | rescached.cfg - Configuration for rescached service 13 | 14 | 15 | == SYNOPSIS 16 | 17 | /etc/rescached/rescached.cfg 18 | 19 | 20 | == DESCRIPTION 21 | 22 | These file configure the behaviour of *rescached*(1) service. 23 | This section will explain more about each option and how they effect 24 | `rescached`. 25 | 26 | The configuration is using INI format where each options is grouped by header 27 | in square bracket: 28 | 29 | * `[rescached]` 30 | * `[dns "server"]` 31 | 32 | 33 | == OPTIONS 34 | 35 | === [rescached] 36 | 37 | This group of options contain the main configuration that related to 38 | rescached. 39 | 40 | [#wui-listen] 41 | ==== wui.listen 42 | 43 | Format:: [host]:port 44 | Default:: 127.0.0.1:5380 45 | Description:: The address to listen for web user interface. 46 | 47 | [#file-resolvconf] 48 | ==== file.resolvconf 49 | 50 | Format:: /any/path/to/file 51 | Default:: /etc/rescached/resolv.conf 52 | Description:: A path to dynamically generated *resolv.conf*(5) by 53 | *resolvconf*(8). 54 | + 55 | -- 56 | If set, the nameserver values in referenced file will be used as "parent" name 57 | server if no "parent" is defined in configuration file. 58 | 59 | To use this config, you must set either "dnsmasq_resolv", "pdnsd_resolv", or 60 | "unbound_conf" in "/etc/resolvconf.conf" to point to 61 | "/etc/rescached/resolv.conf". 62 | 63 | For example, 64 | ---- 65 | resolv_conf=/etc/resolv.conf 66 | name_servers=127.0.0.1 67 | dnsmasq_resolv=/etc/rescached/resolv.conf 68 | #pdnsd_resolv=/etc/rescached/resolv.conf 69 | #unbound_conf=/etc/rescached/resolv.conf 70 | ---- 71 | -- 72 | 73 | [#debug] 74 | ==== debug 75 | 76 | Value:: 77 | 0::: log startup and errors. 78 | 1::: log startup, errors, request, response, caches, and exit status. 79 | Format:: Number (0 or 1). 80 | Default:: 0 81 | Description:: This option only used for debugging program or if user want to 82 | monitor what kind of traffic goes in and out of rescached. 83 | 84 | [#dns_server] 85 | === [dns "server"] 86 | 87 | This group of options related to DNS server. 88 | 89 | [#parent] 90 | ==== parent 91 | 92 | Format:: 93 | 94 | ---- 95 | parent = "parent = " [ scheme "://"] ( ip-address / domain-name ) [ ":" port ] 96 | scheme = ( "udp" / "https") 97 | ---- 98 | 99 | Default:: 100 | * Address: udp://1.1.1.1 101 | * Port: 53 102 | Description:: List of parent DNS servers. 103 | + 104 | When `rescached` receive a query from client (for example, your browser) and 105 | when it does not have a cached answer for that query, it will forward the 106 | query to one of the parent name servers. 107 | + 108 | Using UDP as parent scheme, will automatically assume that the server also 109 | capable of handling query in TCP. 110 | This is required when client (for example, your browser) re-send the query 111 | after receiving truncated UDP answer. 112 | Any query received by `rescached` through TCP will forwarded to the parent 113 | name server as TCP too, using the same address and port defined in one of UDP 114 | parent. 115 | + 116 | Please, do not use OpenDNS server. 117 | If certain host-name not found (i.e. typo in host-name), OpenDNS will reply 118 | with its own address, instead of replying with empty answer. 119 | This will make `rescached` caching a false data and it may make your 120 | application open or consume unintended resources. 121 | + 122 | To check if your parent server reply the unknown host-name with no answer, use 123 | *resolver*(1) tool. 124 | 125 | Example:: 126 | ---- 127 | ## Using UDP connection to forward request to parent name server. 128 | parent = udp://1.1.1.1 129 | 130 | ## Using DNS over TLS to forward request to parent name server. 131 | parent = https://1.1.1.1 132 | 133 | ## Using DNS over HTTPS to forward request to parent name server. 134 | parent = https://kilabit.info/dns-query 135 | ---- 136 | 137 | [#listen] 138 | ==== listen 139 | 140 | Format:: : 141 | Default:: 127.0.0.1:53 142 | Description:: Address in local network where `rescached` will listening for 143 | query from client. 144 | + 145 | If you want rescached to serve a query from another host in your local 146 | network, change this value to `0.0.0.0:53`. 147 | 148 | [#http-port] 149 | ==== http.port 150 | 151 | Format:: Number 152 | Default:: 443 153 | Description:: Port to serve DNS over HTTP. 154 | 155 | [#tls-port] 156 | ==== tls.port 157 | 158 | Format:: Number 159 | Default:: 853 160 | Description:: Port to serve DNS over TLS. 161 | 162 | [#tls-certificate] 163 | ==== tls.certificate 164 | 165 | Format:: /path/to/file 166 | Default:: (empty) 167 | Description:: Path to certificate file to serve DNS over TLS and HTTPS. 168 | 169 | 170 | [#tls-private_key] 171 | ==== tls.private_key 172 | 173 | Format:: /path/to/file 174 | Default:: (empty) 175 | Description:: Path to certificate private key file to serve DNS over TLS and 176 | HTTPS. 177 | 178 | [#tls-allow_insecure] 179 | ==== tls.allow_insecure 180 | 181 | Format:: true | false 182 | Default:: false 183 | Description:: If its true, allow serving DoH and DoT with self-signed 184 | certificate. 185 | 186 | [#doh-behind_proxy] 187 | ==== doh.behind_proxy 188 | 189 | Format:: true | false 190 | Default:: false 191 | Description:: If its true, serve DNS over HTTP only, even if 192 | certificate files is defined. 193 | This allow serving DNS request forwarded by another proxy server. 194 | 195 | [#cache-prune_delay] 196 | ==== cache.prune_delay 197 | 198 | Format:: Duration with time unit. Valid time units are "s", "m", "h". 199 | Default:: 1h 200 | Description:: Delay for pruning caches. 201 | + 202 | Every N seconds/minutes/hours, rescached will traverse all 203 | caches and remove response that has not been accessed less than 204 | `cache.prune_threshold`. 205 | Its value must be equal or greater than 1 hour (3600 seconds). 206 | 207 | [#cache-prune_threshold] 208 | ==== cache.prune_threshold 209 | 210 | Format:: Duration with time unit. Valid time units are "s", "m", "h". 211 | Default:: -1h 212 | Description:: The duration when the cache will be considered expired. 213 | Its value must be negative and greater or equal than -1 hour (-3600 seconds). 214 | 215 | == FILES 216 | 217 | [#hosts-d] 218 | === /etc/rescached/hosts.d 219 | 220 | Path to hosts directory where rescached will load all hosts formatted files. 221 | 222 | 223 | [#zone-d] 224 | === /etc/rescached/zone.d 225 | 226 | Path to zone directory where rescached will load all zone files. 227 | 228 | 229 | == EXAMPLE 230 | 231 | Simple rescached configuration using dnscrypt-proxy that listen on port 54 as 232 | parent resolver, with prune delay set to 60 seconds and threshold also to 60 233 | seconds. 234 | 235 | ---- 236 | [dns "server"] 237 | parent=udp://127.0.0.1:54 238 | cache.prune_delay=60s 239 | cache.prune_threshold=60s 240 | ---- 241 | 242 | Save the above script into `rescached.cfg` and run it, 243 | 244 | $ sudo rescached -config rescached.cfg 245 | 246 | 247 | == AUTHOR 248 | 249 | `rescached` is developed by M. Shulhan (m.shulhan@gmail.com). 250 | 251 | 252 | == LICENSE 253 | 254 | Copyright 2018, M. Shulhan (m.shulhan@gmail.com). 255 | All rights reserved. 256 | 257 | Use of this source code is governed by a GPL-3.0 license that can be 258 | found in the COPYING file. 259 | 260 | 261 | == SEE ALSO 262 | 263 | *rescached*(1) 264 | -------------------------------------------------------------------------------- /_www/doc/resolver.adoc: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2020 M. Shulhan 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | = RESOLVER(1) 4 | :doctype: manpage 5 | :man source: resolver 6 | :man version: 2022.04.15 7 | :man manual: resolver 8 | 9 | 10 | == NAME 11 | 12 | resolver - command line interface (CLI) for DNS and rescached server. 13 | 14 | 15 | == SYNOPSIS 16 | 17 | resolver [-insecure] [-ns=] [-server=] [args...] 18 | 19 | 20 | == DESCRIPTION 21 | 22 | resolver is a tool to resolve hostname to IP address or to query services 23 | on hostname by type (MX, SOA, TXT, etc.) using standard DNS protocol with UDP, 24 | DNS over TLS (DoT), or DNS over HTTPS (DoH). 25 | 26 | It is also provide CLI to the rescached server to manage environment, block.d, 27 | hosts.d, and zone.d; as in the web user interface. 28 | 29 | 30 | == OPTIONS 31 | 32 | The following options affect the commands operation. 33 | 34 | `-insecure`:: 35 | + 36 | -- 37 | Ignore invalid server certificate when querying DoT, DoH, or rescached server. 38 | This option only affect the `query` command. 39 | -- 40 | 41 | `-ns=`:: 42 | + 43 | -- 44 | This option define the parent DNS server where the resolver send the query. 45 | This option only affect the `query` command. 46 | 47 | The nameserver is defined in the following format, 48 | 49 | ("udp"/"tcp"/"https") "://" (domain / ip-address) [":" port] 50 | 51 | Examples, 52 | 53 | * udp://194.233.68.184:53 for querying with UDP, 54 | * tcp://194.233.68.184:53 for querying with TCP, 55 | * https://194.233.68.184:853 for querying with DNS over TLS (DoT), and 56 | * https://kilabit.info/dns-query for querying with DNS over HTTPS (DoH). 57 | 58 | Default to one of "nameserver" in `/etc/resolv.conf`. 59 | -- 60 | 61 | `-server=`:: 62 | + 63 | -- 64 | Set the rescached HTTP server where commands, except query, will be send. 65 | The rescached-URL use HTTP scheme: 66 | 67 | ("http" / "https") "://" (domain / ip-address) [":" port] 68 | 69 | Default to https://127.0.0.1:5380 if its empty. 70 | -- 71 | 72 | == COMMANDS 73 | 74 | === GENERAL 75 | 76 | `help`:: 77 | + 78 | Print the general usage. 79 | 80 | `version`:: 81 | + 82 | Print the program version. 83 | 84 | === QUERY 85 | 86 | `query [type] [class]`:: 87 | + 88 | -- 89 | Query the domain or IP address with optional type and/or class. 90 | 91 | Unless the option "-ns" is given, the query command will use the 92 | nameserver defined in the system resolv.conf file. 93 | 94 | The "type" parameter define DNS record type to be queried. 95 | List of valid types, 96 | 97 | * A (1) - a host Address (default) 98 | * NS (2) - an authoritative Name Server 99 | * CNAME (5) - the Canonical NAME for an alias 100 | * SOA (6) - marks the Start of a zone of Authority 101 | * MB (7) - a MailBox domain name 102 | * MG (8) - a Mail Group member 103 | * MR (9) - a Mail Rename domain name 104 | * NULL (10) - a null resource record 105 | * WKS (11) - a Well Known Service description 106 | * PTR (12) - a domain name PoinTeR 107 | * HINFO (13) - Host INFOrmation 108 | * MINFO (14) - mailbox or mail list information 109 | * MX (15) - Mail Exchange 110 | * TXT (16) - TeXT strings 111 | * AAAA (28) - a host address in IPv6 112 | * SRV (33) - a SerViCe record 113 | 114 | The "class" parameter is optional, its either IN (default), CS, or HS. 115 | -- 116 | 117 | 118 | === MANAGING BLOCK.D 119 | 120 | `block.d`:: List all block.d hosts file. 121 | 122 | `block.d disable `:: Disable specific hosts on block.d. 123 | 124 | `block.d enable `:: Enable specific hosts on block.d. 125 | 126 | `block.d update `:: 127 | + 128 | -- 129 | Fetch the latest hosts file from remote block.d URL defined by 130 | its name. 131 | On success, the hosts file will be updated and the server will be 132 | restarted. 133 | -- 134 | 135 | 136 | === MANAGING CACHES 137 | 138 | caches:: 139 | + 140 | -- 141 | Fetch and print all caches from rescached server. 142 | -- 143 | 144 | 145 | caches search :: 146 | + 147 | -- 148 | Search the domain name in rescached caches. 149 | This command can also be used to inspect each DNS message on the caches. 150 | -- 151 | 152 | caches remove :: 153 | + 154 | -- 155 | Remove the domain name from rescached caches. 156 | If the parameter is "all", it will remove all caches. 157 | -- 158 | 159 | 160 | === MANAGING ENVIRONMENT 161 | 162 | env:: 163 | + 164 | -- 165 | Fetch the current server environment and print it as JSON format to stdout. 166 | -- 167 | 168 | env update :: 169 | + 170 | -- 171 | Update the server environment from JSON formatted file. 172 | If the argument is "-", the new environment is read from stdin. 173 | If the environment is valid, the server will be restarted. 174 | -- 175 | 176 | 177 | === MANAGING HOSTS.D 178 | 179 | hosts.d create :: 180 | + 181 | -- 182 | Create new hosts file inside the hosts.d directory with specific file 183 | name. 184 | -- 185 | 186 | hosts.d delete :: 187 | + 188 | -- 189 | Delete hosts file inside the hosts.d directory by file name. 190 | -- 191 | 192 | hosts.d get :: 193 | + 194 | -- 195 | Get the content of hosts file inside the hosts.d directory by file name. 196 | -- 197 | 198 | 199 | === MANAGING RECORD IN HOSTS.D 200 | 201 | hosts.d rr add :: 202 | + 203 | -- 204 | Insert a new record and save it to the hosts file identified by 205 | "name". 206 | If the domain name already exists, the new record will be appended 207 | instead of replaced. 208 | -- 209 | 210 | hosts.d rr delete :: 211 | + 212 | -- 213 | Delete record from hosts file "name" by domain name. 214 | -- 215 | 216 | 217 | === MANAGING ZONE.D 218 | 219 | `zone.d`:: 220 | + 221 | Fetch and print all zones in the server, including their SOA. 222 | 223 | zone.d create :: 224 | + 225 | Create new zone file inside the zone.d directory. 226 | 227 | zone.d delete :: 228 | + 229 | Delete zone file inside the zone.d directory. 230 | 231 | 232 | === MANAGING RECORD IN ZONE.D 233 | 234 | `zone.d rr get `:: 235 | 236 | Get and print all records in the zone. 237 | 238 | zone.d rr add <"@" | subdomain> ...:: 239 | + 240 | -- 241 | Add new record into the zone file. 242 | 243 | The domain name can be set to origin using "@" or empty string, subdomain 244 | (without ending with "."), or fully qualified domain name (end with "."). 245 | 246 | If ttl is set to 0, it will default to 604800 (7 days). 247 | 248 | List of valid type are A, NS, CNAME, PTR, MX, TXT, and AAAA. 249 | 250 | List of valid class are IN, CS, HS. 251 | 252 | The value parameter can be more than one, for example, the MX record 253 | we pass two parameters: 254 | 255 | 256 | 257 | See the example below for more information. 258 | -- 259 | 260 | `zone.d rr delete <"@" | subdomain> `:: 261 | + 262 | -- 263 | Delete record from zone by its subdomain, type, class, and value. 264 | -- 265 | 266 | 267 | == EXIT STATUS 268 | 269 | Upon exit and success +resolver+ will return 0, or 1 otherwise. 270 | 271 | 272 | == EXAMPLES 273 | 274 | === QUERY 275 | 276 | Query the IPv4 address for kilabit.info, 277 | 278 | $ resolver query kilabit.info 279 | 280 | Query the mail exchange (MX) for domain kilabit.info, 281 | 282 | $ resolver query kilabit.info MX 283 | 284 | Query the IPv4 address for kilabit.info using 127.0.0.1 at port 53 as 285 | name server, 286 | 287 | $ resolver -ns=udp://127.0.0.1:53 query kilabit.info 288 | 289 | Query the IPv4 address of domain name "kilabit.info" using DNS over TLS at 290 | name server 194.233.68.184, 291 | 292 | $ resolver -insecure -ns=https://194.233.68.184 query kilabit.info 293 | 294 | Query the IPv4 records of domain name "kilabit.info" using DNS over HTTPS on 295 | name server kilabit.info, 296 | 297 | $ resolver -ns=https://kilabit.info/dns-query query kilabit.info 298 | 299 | Inspect the rescached's caches on server at http://127.0.0.1:5380, 300 | 301 | $ resolver -server=http://127.0.0.1:5380 caches 302 | 303 | 304 | === MANAGING CACHES 305 | 306 | Search caches that contains "bit" on the domain name, 307 | 308 | $ resolver caches search bit 309 | 310 | Remove caches that contains domain name "kilabit.info", 311 | 312 | $ resolver caches remove kilabit.info 313 | 314 | Remove all caches in the server, 315 | 316 | $ resolver caches remove all 317 | 318 | 319 | === MANAGING ENVIRONMENT 320 | 321 | Fetch and print current server environment, 322 | 323 | $ resolver env 324 | 325 | Update the server environment from JSON file in /tmp/env.json, 326 | 327 | $ resolver env update /tmp/env.json 328 | 329 | Update the server environment by reading JSON from standard input, 330 | 331 | $ cat /tmp/env.json | resolver env update - 332 | 333 | 334 | === MANAGING HOSTS.D 335 | 336 | Create new hosts file named "myhosts" inside the hosts.d directory, 337 | 338 | $ resolver hosts.d create myhosts 339 | OK 340 | 341 | Delete hosts file named "myhosts" inside the hosts.d directory, 342 | 343 | $ resolver hosts.d delete myhosts 344 | OK 345 | 346 | Get the content of hosts file named "myhosts" inside the hosts.d directory, 347 | 348 | ---- 349 | $ resolver hosts.d get myhosts 350 | [ 351 | { 352 | "Value": "127.0.0.1", 353 | "Name": "localhost", 354 | "Type": 1, 355 | "Class": 1, 356 | "TTL": 604800 357 | }, 358 | { 359 | "Value": "::1", 360 | "Name": "localhost", 361 | "Type": 28, 362 | "Class": 1, 363 | "TTL": 604800 364 | } 365 | ] 366 | ---- 367 | 368 | === MANAGING RECORD IN HOSTS.D 369 | 370 | Add new record "127.0.0.1 my.hosts" to hosts file named "hosts", 371 | 372 | ---- 373 | $ resolver hosts.d rr add hosts my.hosts 127.0.0.1 374 | { 375 | "Value": "127.0.0.1", 376 | "Name": "my.hosts", 377 | "Type": 1, 378 | "Class": 1, 379 | "TTL": 604800 380 | } 381 | ---- 382 | 383 | Delete record "my.hosts" from hosts file "hosts", 384 | 385 | ---- 386 | $ resolver hosts.d rr delete hosts my.hosts 387 | { 388 | "Value": "127.0.0.1", 389 | "Name": "my.hosts", 390 | "Type": 1, 391 | "Class": 1, 392 | "TTL": 604800 393 | } 394 | ---- 395 | 396 | === MANAGING ZONE.D 397 | 398 | Print all zone in the server, 399 | 400 | ---- 401 | $ resolver zone.d 402 | my.zone 403 | SOA: {MName:my.zone RName: Serial:0 Refresh:0 Retry:0 Expire:0 Minimum:0} 404 | ---- 405 | 406 | 407 | === MANAGING RECORD IN ZONE.D 408 | 409 | Assume that we have create zone "my.zone". 410 | 411 | Get all records in the zone "my.zone", 412 | 413 | ---- 414 | $ resolver zone.d rr get my.zone 415 | my.zone 416 | 604800 MX IN map[Exchange:mail.my.zone Preference:10] 417 | 604800 A IN 127.0.0.2 418 | 604800 A IN 127.0.0.3 419 | www.my.zone 420 | 604800 A IN 192.168.1.2 421 | ---- 422 | 423 | Add IPv4 address "127.0.0.1" for domain my.zone, 424 | 425 | ---- 426 | $ resolver zone.d rr add my.zone @ 0 A IN 127.0.0.1 427 | ---- 428 | 429 | or 430 | 431 | ---- 432 | $ resolver zone.d rr add my.zone "" 0 A IN 127.0.0.1 433 | { 434 | "Value": "127.0.0.1", 435 | "Name": "my.zone", 436 | "Type": 1, 437 | "Class": 1, 438 | "TTL": 604800 439 | } 440 | ---- 441 | 442 | and to delete the above record, 443 | 444 | ---- 445 | $ resolver zone.d rr delete my.zone @ A IN 127.0.0.1 446 | OK 447 | ---- 448 | 449 | Add subdomain "www" with IPv4 address "192.168.1.2" to zone "my.zone", 450 | 451 | ---- 452 | $ resolver zone.d rr add my.zone www 0 A IN 192.168.1.2 453 | { 454 | "Value": "192.168.1.2", 455 | "Name": "www.my.zone", 456 | "Type": 1, 457 | "Class": 1, 458 | "TTL": 604800 459 | } 460 | ---- 461 | 462 | and to delete the above record, 463 | 464 | ---- 465 | $ resolver zone.d rr delete my.zone www A IN 192.168.1.2 466 | OK 467 | ---- 468 | 469 | == AUTHOR 470 | 471 | This software is developed by M. Shulhan (ms@kilabit.info). 472 | 473 | 474 | == LICENSE 475 | 476 | Copyright 2018, M. Shulhan (ms@kilabit.info). 477 | All rights reserved. 478 | 479 | Use of this source code is governed by a GPL 3.0 license that can be 480 | found in the COPYING file. 481 | 482 | 483 | == LINKS 484 | 485 | Source code repository: https://git.sr.ht/~shulhan/rescached 486 | 487 | 488 | == SEE ALSO 489 | 490 | *rescached*(1), *rescached.cfg*(5) 491 | -------------------------------------------------------------------------------- /_www/environment/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | rescached | Environment 12 | 13 | 54 | 55 | 56 | 57 | 70 | 71 |
72 | 73 |
74 |

75 | This page allow you to change the rescached environment. Upon save, the rescached service 76 | will be restarted. 77 |

78 | 79 |

rescached

80 | 81 |
82 | 83 | 84 | ? 85 | 90 |
91 | 92 |
93 | 94 | 95 | ? 96 | 100 |
101 | 102 |

DNS server

103 | 104 |
105 | 106 | ? 107 | 110 |
111 | 112 |
113 | 114 |
115 | 116 | 117 | ? 118 | 125 |
126 | 127 |
128 | 129 | 130 | ? 131 | 134 |
135 | 136 |
137 | 138 | 139 | ? 140 | 143 |
144 | 145 |
146 | 147 | 148 | ? 149 | 152 |
153 | 154 |
155 | 156 | 157 | ? 158 | 161 |
162 | 163 |
164 | 165 |
166 | 167 | Yes 168 |
169 | ? 170 | 173 |
174 | 175 |
176 | 177 |
178 | 179 | Yes 180 |
181 | ? 182 | 186 |
187 | 188 |
189 | 190 | 191 | ? 192 | 197 |
198 | 199 |
200 | 201 | 202 | ? 203 | 207 |
208 | 209 |
210 |
211 | 212 |
213 |
214 |
215 | 216 | 217 | 218 | 302 | 303 | 304 | 305 | -------------------------------------------------------------------------------- /_www/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shuLhan/rescached/2817846b12942dc0624c6b6ef7de6e608c41c3f7/_www/favicon.png -------------------------------------------------------------------------------- /_www/hosts.d/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | rescached | hosts.d 12 | 13 | 51 | 52 | 53 | 54 | 67 | 68 |
69 | 70 |
71 | 84 | 85 |
86 |

Select one of the hosts file to manage.

87 |
88 |
89 | 90 | 91 | 92 | 270 | 271 | 272 | 273 | -------------------------------------------------------------------------------- /_www/index.css: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2019 M. Shulhan 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | html, 5 | body { 6 | position: relative; 7 | width: 100%; 8 | height: 100%; 9 | } 10 | 11 | body { 12 | background-color: floralwhite; 13 | color: #333; 14 | margin: 0; 15 | padding: 8px; 16 | box-sizing: border-box; 17 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, 18 | Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; 19 | } 20 | 21 | a { 22 | color: rgb(0, 100, 200); 23 | text-decoration: none; 24 | } 25 | 26 | a:hover { 27 | text-decoration: underline; 28 | } 29 | 30 | a:visited { 31 | color: rgb(0, 80, 160); 32 | } 33 | 34 | input, 35 | button, 36 | select, 37 | textarea { 38 | font-family: inherit; 39 | font-size: inherit; 40 | padding: 0.4em; 41 | margin: 0.5em 0; 42 | box-sizing: border-box; 43 | border: 1px solid #ccc; 44 | border-radius: 2px; 45 | } 46 | 47 | input:disabled { 48 | color: #ccc; 49 | } 50 | 51 | input[type="range"] { 52 | height: 0; 53 | } 54 | 55 | button { 56 | color: #333; 57 | background-color: lavender; 58 | outline: none; 59 | } 60 | 61 | button:disabled { 62 | color: #999; 63 | } 64 | 65 | button:not(:disabled):active { 66 | background-color: #ddd; 67 | } 68 | 69 | button:focus { 70 | border-color: #666; 71 | } 72 | 73 | h1, 74 | h2 { 75 | color: #ff3e00; 76 | text-transform: uppercase; 77 | font-weight: 200; 78 | } 79 | 80 | body { 81 | margin: 0 auto; 82 | width: 800px; 83 | padding: 1em; 84 | } 85 | 86 | #notif { 87 | position: fixed; 88 | top: 1em; 89 | width: 70%; 90 | } 91 | 92 | #notif > .error { 93 | background-color: salmon; 94 | padding: 1em; 95 | } 96 | 97 | #notif > .info { 98 | background-color: lightblue; 99 | padding: 1em; 100 | } 101 | 102 | nav.menu { 103 | color: #ff3e00; 104 | text-transform: uppercase; 105 | margin-bottom: 2em; 106 | } 107 | 108 | .active { 109 | padding-bottom: 4px; 110 | border-bottom: 4px solid #ff3e00; 111 | } 112 | 113 | .input > label { 114 | width: 8em; 115 | display: inline-block; 116 | } 117 | 118 | .input > input, 119 | .input > select { 120 | width: calc(100% - 11em); 121 | display: inline-block; 122 | } 123 | 124 | .input > .input-info-toggler { 125 | border-radius: 50%; 126 | border: 1px solid grey; 127 | cursor: pointer; 128 | display: inline-block; 129 | font-size: 12px; 130 | height: 14px; 131 | line-height: 14px; 132 | padding: 2px; 133 | text-align: center; 134 | width: 14px; 135 | } 136 | 137 | .input > .input-info { 138 | background-color: #eee; 139 | margin: 8px 0px; 140 | padding: 1em; 141 | } 142 | 143 | @media (max-width: 900px) { 144 | body { 145 | width: calc(100% - 2em); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /_www/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | rescached 12 | 13 | 64 | 65 | 66 | 67 | 80 | 88 |
89 |
90 | 91 |
92 |
93 | 94 | 95 | 96 | 244 | 245 | 246 | 247 | -------------------------------------------------------------------------------- /_www/index.js: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 M. Shulhan 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | function notifError(msg) { 5 | displayNotif("error", msg); 6 | } 7 | 8 | function notifInfo(msg) { 9 | displayNotif("info", msg); 10 | } 11 | 12 | function displayNotif(className, msg) { 13 | let notif = document.getElementById("notif"); 14 | let el = document.createElement("div"); 15 | el.classList.add(className); 16 | el.innerHTML = msg; 17 | notif.appendChild(el); 18 | 19 | setTimeout(function () { 20 | notif.removeChild(notif.children[0]); 21 | }, 5000); 22 | } 23 | 24 | function toggleInfo(id) { 25 | let el = document.getElementById(id); 26 | if (el.style.display === "none") { 27 | el.style.display = "block"; 28 | } else { 29 | el.style.display = "none"; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /_www/rescached.js: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 M. Shulhan 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | const RRTypes = { 5 | 1: "A", 6 | 2: "NS", 7 | 3: "MD", 8 | 4: "MF", 9 | 5: "CNAME", 10 | 6: "SOA", 11 | 7: "MB", 12 | 8: "MG", 13 | 9: "MR", 14 | 10: "NULL", 15 | 11: "WKS", 16 | 12: "PTR", 17 | 13: "HINFO", 18 | 14: "MINFO", 19 | 15: "MX", 20 | 16: "TXT", 21 | 28: "AAAA", 22 | 33: "SRV", 23 | 41: "OPT", 24 | }; 25 | 26 | const contentTypeForm = "application/x-www-form-urlencoded"; 27 | const contentTypeJson = "application/json"; 28 | 29 | const paramNameName = "name"; 30 | 31 | const headerContentType = "Content-Type"; 32 | 33 | function getRRTypeName(k) { 34 | let v = RRTypes[k]; 35 | if (v === "") { 36 | return k; 37 | } 38 | return v; 39 | } 40 | 41 | class Rescached { 42 | static SERVER = ""; 43 | static nanoSeconds = 1000000000; 44 | 45 | static apiBlockd = Rescached.SERVER + "/api/block.d"; 46 | static apiBlockdFetch = Rescached.SERVER + "/api/block.d/fetch"; 47 | 48 | static apiCaches = Rescached.SERVER + "/api/caches"; 49 | static apiCachesSearch = Rescached.SERVER + "/api/caches/search"; 50 | 51 | static apiEnvironment = Rescached.SERVER + "/api/environment"; 52 | 53 | static apiHostsd = Rescached.SERVER + "/api/hosts.d"; 54 | static apiHostsdRR = Rescached.SERVER + "/api/hosts.d/rr"; 55 | 56 | static apiZoned = Rescached.SERVER + "/api/zone.d"; 57 | static apiZonedRR = Rescached.SERVER + "/api/zone.d/rr"; 58 | 59 | constructor(server) { 60 | this.blockd = {}; 61 | this.hostsd = {}; 62 | this.env = {}; 63 | this.server = server; 64 | this.zoned = {}; 65 | } 66 | 67 | // Blockd get list of block.d. 68 | async Blockd() { 69 | const httpRes = await fetch(Rescached.apiBlockd); 70 | const res = await httpRes.json(); 71 | if (res.code === 200) { 72 | this.blockd = res.data; 73 | } 74 | return res; 75 | } 76 | 77 | async BlockdFetch(name) { 78 | let params = new URLSearchParams(); 79 | params.set("name", name); 80 | 81 | const httpRes = await fetch(Rescached.apiBlockdFetch, { 82 | method: "POST", 83 | headers: { 84 | [headerContentType]: contentTypeForm, 85 | }, 86 | body: params.toString(), 87 | }); 88 | 89 | const res = await httpRes.json(); 90 | if (res.code === 200) { 91 | this.blockd[name] = res.data; 92 | } 93 | return res; 94 | } 95 | 96 | async BlockdUpdate(hostsBlocks) { 97 | const httpRes = await fetch(Rescached.apiBlockd, { 98 | method: "PUT", 99 | headers: { 100 | [headerContentType]: contentTypeJson, 101 | }, 102 | body: JSON.stringify(hostsBlocks), 103 | }); 104 | 105 | const res = await httpRes.json(); 106 | if (res.code === 200) { 107 | this.blockd = res.data; 108 | } 109 | return res; 110 | } 111 | 112 | async Caches() { 113 | const res = await fetch(Rescached.apiCaches, { 114 | headers: { 115 | Connection: "keep-alive", 116 | }, 117 | }); 118 | return await res.json(); 119 | } 120 | 121 | async CachesRemove(qname) { 122 | const res = await fetch(Rescached.apiCaches + "?name=" + qname, { 123 | method: "DELETE", 124 | }); 125 | return await res.json(); 126 | } 127 | 128 | async CachesSearch(query) { 129 | console.log("CachesSearch: ", query); 130 | const res = await fetch(Rescached.apiCachesSearch + "?query=" + query); 131 | return await res.json(); 132 | } 133 | 134 | async Environment() { 135 | const httpRes = await fetch(Rescached.apiEnvironment); 136 | const res = await httpRes.json(); 137 | 138 | if (httpRes.status === 200) { 139 | res.data.PruneDelay = res.data.PruneDelay / Rescached.nanoSeconds; 140 | res.data.PruneThreshold = res.data.PruneThreshold / Rescached.nanoSeconds; 141 | 142 | for (let k in res.data.HostsFiles) { 143 | if (!res.data.HostsFiles.hasOwnProperty(k)) { 144 | continue; 145 | } 146 | let hf = res.data.HostsFiles[k]; 147 | if (typeof hf.Records === "undefined") { 148 | hf.Records = []; 149 | } 150 | } 151 | this.env = res.data; 152 | } 153 | return res; 154 | } 155 | 156 | async EnvironmentUpdate() { 157 | let got = {}; 158 | 159 | Object.assign(got, this.env); 160 | 161 | got.PruneDelay = got.PruneDelay * Rescached.nanoSeconds; 162 | got.PruneThreshold = got.PruneThreshold * Rescached.nanoSeconds; 163 | 164 | const httpRes = await fetch("/api/environment", { 165 | method: "POST", 166 | headers: { 167 | [headerContentType]: contentTypeJson, 168 | }, 169 | body: JSON.stringify(got), 170 | }); 171 | 172 | return await httpRes.json(); 173 | } 174 | 175 | GetRRTypeName(k) { 176 | let v = RRTypes[k]; 177 | if (v === "") { 178 | return k; 179 | } 180 | return v; 181 | } 182 | 183 | async Hostsd() { 184 | const httpRes = await fetch(Rescached.apiHostsd); 185 | const res = await httpRes.json(); 186 | if (res.code === 200) { 187 | this.hostsd = res.data; 188 | } 189 | return res; 190 | } 191 | 192 | async HostsdCreate(name) { 193 | var params = new URLSearchParams(); 194 | params.set(paramNameName, name); 195 | 196 | const httpRes = await fetch(Rescached.apiHostsd, { 197 | method: "POST", 198 | headers: { 199 | [headerContentType]: contentTypeForm, 200 | }, 201 | body: params.toString(), 202 | }); 203 | const res = await httpRes.json(); 204 | if (res.code === 200) { 205 | this.hostsd[name] = { 206 | Name: name, 207 | Records: [], 208 | }; 209 | } 210 | return res; 211 | } 212 | 213 | async HostsdDelete(name) { 214 | var params = new URLSearchParams(); 215 | params.set(paramNameName, name); 216 | 217 | var url = Rescached.apiHostsd + "?" + params.toString(); 218 | const httpRes = await fetch(url, { 219 | method: "DELETE", 220 | }); 221 | const res = await httpRes.json(); 222 | if (res.code === 200) { 223 | delete this.hostsd[name]; 224 | } 225 | return res; 226 | } 227 | 228 | async HostsdGet(name) { 229 | var params = new URLSearchParams(); 230 | params.set(paramNameName, name); 231 | 232 | var url = Rescached.apiHostsd + "?" + params.toString(); 233 | const httpRes = await fetch(url); 234 | 235 | let res = await httpRes.json(); 236 | if (httpRes.Status === 200) { 237 | this.hostsd[name] = { 238 | Name: name, 239 | Records: res.data, 240 | }; 241 | } 242 | return res; 243 | } 244 | 245 | async HostsdRecordAdd(hostsFile, domain, value) { 246 | let params = new URLSearchParams(); 247 | params.set("name", hostsFile); 248 | params.set("domain", domain); 249 | params.set("value", value); 250 | 251 | const httpRes = await fetch(Rescached.apiHostsdRR, { 252 | method: "POST", 253 | headers: { 254 | [headerContentType]: contentTypeForm, 255 | }, 256 | body: params.toString(), 257 | }); 258 | const res = await httpRes.json(); 259 | if (httpRes.Status === 200) { 260 | let hf = this.hostsd[hostsFile]; 261 | hf.Records.push(res.data); 262 | } 263 | return res; 264 | } 265 | 266 | async HostsdRecordDelete(hostsFile, domain) { 267 | let params = new URLSearchParams(); 268 | params.set("name", hostsFile); 269 | params.set("domain", domain); 270 | 271 | const api = Rescached.apiHostsdRR + "?" + params.toString(); 272 | 273 | const httpRes = await fetch(api, { 274 | method: "DELETE", 275 | }); 276 | const res = await httpRes.json(); 277 | if (httpRes.Status === 200) { 278 | let hf = this.hostsd[hostsFile]; 279 | for (let x = 0; x < hf.Records.length; x++) { 280 | if (hf.Records[x].Name === domain) { 281 | hf.Records.splice(x, 1); 282 | } 283 | } 284 | } 285 | return res; 286 | } 287 | 288 | // Zoned fetch all of zones. 289 | async Zoned() { 290 | const httpRes = await fetch(Rescached.apiZoned); 291 | const res = await httpRes.json(); 292 | if (res.code === 200) { 293 | this.zoned = res.data; 294 | } 295 | return res; 296 | } 297 | 298 | async ZonedCreate(name) { 299 | let params = new URLSearchParams(); 300 | params.set(paramNameName, name); 301 | 302 | const httpRes = await fetch(Rescached.apiZoned, { 303 | method: "POST", 304 | headers: { 305 | [headerContentType]: contentTypeForm, 306 | }, 307 | body: params.toString(), 308 | }); 309 | 310 | const res = await httpRes.json(); 311 | if (res.code === 200) { 312 | this.zoned[name] = res.data; 313 | } 314 | return res; 315 | } 316 | 317 | async ZonedDelete(name) { 318 | let params = new URLSearchParams(); 319 | params.set(paramNameName, name); 320 | 321 | let url = Rescached.apiZoned + "?" + params.toString(); 322 | const httpRes = await fetch(url, { 323 | method: "DELETE", 324 | }); 325 | let res = await httpRes.json(); 326 | if (res.code == 200) { 327 | delete this.zoned[name]; 328 | } 329 | return res; 330 | } 331 | 332 | // ZonedRecords fetch all records on specific zone. 333 | async ZonedRecords(name) { 334 | let params = new URLSearchParams(); 335 | params.set(paramNameName, name); 336 | 337 | let url = Rescached.apiZonedRR + "?" + params.toString(); 338 | const httpRes = await fetch(url); 339 | const res = await httpRes.json(); 340 | if (res.code === 200) { 341 | this.zoned[name].Records = res.data; 342 | if (typeof this.zoned[name].SOA === "undefined") { 343 | this.zoned[name].SOA = {}; 344 | } 345 | } 346 | return res; 347 | } 348 | 349 | async ZonedRecordAdd(name, rr) { 350 | let req = { 351 | name: name, 352 | type: getRRTypeName(rr.Type), 353 | record: btoa(JSON.stringify(rr)), 354 | }; 355 | 356 | const httpRes = await fetch(Rescached.apiZonedRR, { 357 | method: "POST", 358 | headers: { 359 | [headerContentType]: contentTypeJson, 360 | }, 361 | body: JSON.stringify(req), 362 | }); 363 | 364 | let res = await httpRes.json(); 365 | if (httpRes.status === 200) { 366 | let zf = this.zoned[name]; 367 | if (rr.Type == 6) { 368 | zf.SOA = res.data; 369 | } else { 370 | let rr = res.data; 371 | if (typeof zf.Records === "undefined" || zf.Records == null) { 372 | zf.Records = {}; 373 | } 374 | zf.Records[rr.Name].push(rr); 375 | } 376 | } 377 | return res; 378 | } 379 | 380 | async ZonedRecordDelete(zone, rr) { 381 | let params = new URLSearchParams(); 382 | params.set(paramNameName, zone); 383 | params.set("type", getRRTypeName(rr.Type)); 384 | params.set("record", btoa(JSON.stringify(rr))); 385 | 386 | let api = Rescached.apiZonedRR + "?" + params.toString(); 387 | 388 | const httpRes = await fetch(api, { 389 | method: "DELETE", 390 | }); 391 | 392 | let res = await httpRes.json(); 393 | if (httpRes.status === 200) { 394 | this.zoned[zone].Records = res.data; 395 | } 396 | return res; 397 | } 398 | } 399 | -------------------------------------------------------------------------------- /blockd.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2020 M. Shulhan 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | package rescached 5 | 6 | import ( 7 | "bytes" 8 | "context" 9 | "fmt" 10 | "io" 11 | "log" 12 | "net/http" 13 | "os" 14 | "path/filepath" 15 | "time" 16 | ) 17 | 18 | const ( 19 | lastUpdatedFormat = "2006-01-02 15:04:05 MST" 20 | ) 21 | 22 | // Blockd define the container for each block.d section in configuration. 23 | type Blockd struct { 24 | lastUpdated time.Time 25 | 26 | Name string `ini:"::name"` // Derived from hostname in URL. 27 | URL string `ini:"::url"` 28 | 29 | file string 30 | fileDisabled string 31 | LastUpdated string 32 | 33 | IsEnabled bool // True if the hosts file un-hidden in block.d directory. 34 | isFileExist bool // True if the file exist and enabled or disabled. 35 | } 36 | 37 | // disable the hosts block by prefixing the file name with single dot. 38 | func (hb *Blockd) disable() (err error) { 39 | err = os.Rename(hb.file, hb.fileDisabled) 40 | if err != nil { 41 | return fmt.Errorf("disable: %w", err) 42 | } 43 | hb.IsEnabled = false 44 | return nil 45 | } 46 | 47 | // enable the hosts block file by removing the dot prefix from file name. 48 | func (hb *Blockd) enable() (err error) { 49 | if hb.isFileExist { 50 | err = os.Rename(hb.fileDisabled, hb.file) 51 | } else { 52 | err = os.WriteFile(hb.file, []byte(""), 0600) 53 | } 54 | if err != nil { 55 | return fmt.Errorf("enable: %w", err) 56 | } 57 | hb.IsEnabled = true 58 | hb.isFileExist = true 59 | return nil 60 | } 61 | 62 | func (hb *Blockd) init(pathDirBlock string) { 63 | var ( 64 | fi os.FileInfo 65 | err error 66 | ) 67 | 68 | hb.file = filepath.Join(pathDirBlock, hb.Name) 69 | hb.fileDisabled = filepath.Join(pathDirBlock, "."+hb.Name) 70 | 71 | fi, err = os.Stat(hb.file) 72 | if err != nil { 73 | hb.IsEnabled = false 74 | 75 | fi, err = os.Stat(hb.fileDisabled) 76 | if err != nil { 77 | return 78 | } 79 | 80 | hb.isFileExist = true 81 | } else { 82 | hb.IsEnabled = true 83 | hb.isFileExist = true 84 | } 85 | 86 | hb.lastUpdated = fi.ModTime() 87 | hb.LastUpdated = hb.lastUpdated.Format(lastUpdatedFormat) 88 | } 89 | 90 | // isOld will return true if the host file has not been updated since seven 91 | // days. 92 | func (hb *Blockd) isOld() bool { 93 | oneWeek := 7 * 24 * time.Hour 94 | lastWeek := time.Now().Add(-1 * oneWeek) 95 | 96 | return hb.lastUpdated.Before(lastWeek) 97 | } 98 | 99 | func (hb *Blockd) update() (err error) { 100 | if !hb.isOld() { 101 | return nil 102 | } 103 | 104 | var logp = `Blockd.update` 105 | 106 | fmt.Printf("%s %s: updating ...\n", logp, hb.Name) 107 | 108 | err = os.MkdirAll(filepath.Dir(hb.file), 0700) 109 | if err != nil { 110 | return fmt.Errorf("%s %s: %w", logp, hb.Name, err) 111 | } 112 | 113 | var ( 114 | req *http.Request 115 | res *http.Response 116 | ) 117 | req, err = http.NewRequestWithContext(context.Background(), http.MethodGet, hb.URL, nil) 118 | if err != nil { 119 | return fmt.Errorf(`%s %s: %w`, logp, hb.Name, err) 120 | } 121 | res, err = http.DefaultClient.Do(req) 122 | if err != nil { 123 | return fmt.Errorf("%s %s: %w", logp, hb.Name, err) 124 | } 125 | defer func() { 126 | var errClose = res.Body.Close() 127 | if errClose != nil { 128 | log.Printf("%s %q: %s", logp, hb.Name, err) 129 | } 130 | }() 131 | 132 | var body []byte 133 | 134 | body, err = io.ReadAll(res.Body) 135 | if err != nil { 136 | return fmt.Errorf("%s %q: %w", logp, hb.Name, err) 137 | } 138 | 139 | body = bytes.ReplaceAll(body, []byte("\r\n"), []byte("\n")) 140 | 141 | if hb.IsEnabled { 142 | err = os.WriteFile(hb.file, body, 0600) 143 | } else { 144 | err = os.WriteFile(hb.fileDisabled, body, 0600) 145 | } 146 | if err != nil { 147 | return fmt.Errorf("%s %q: %w", logp, hb.Name, err) 148 | } 149 | 150 | hb.lastUpdated = time.Now() 151 | hb.LastUpdated = hb.lastUpdated.Format(lastUpdatedFormat) 152 | 153 | return nil 154 | } 155 | -------------------------------------------------------------------------------- /blockd_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2020 M. Shulhan 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | package rescached 5 | 6 | import ( 7 | "os" 8 | "path/filepath" 9 | "testing" 10 | 11 | "git.sr.ht/~shulhan/pakakeh.go/lib/test" 12 | ) 13 | 14 | func TestBlockd_init(t *testing.T) { 15 | type testCase struct { 16 | desc string 17 | hb Blockd 18 | exp Blockd 19 | } 20 | 21 | var ( 22 | testDirBase = t.TempDir() 23 | pathDirBlock = filepath.Join(testDirBase, dirBlock) 24 | fileEnabled = "fileEnabled" 25 | fileDisabled = "fileDisabled" 26 | fileNotExist = "fileNotExist" 27 | hostsFileEnabled = filepath.Join(pathDirBlock, fileEnabled) 28 | hostsFileDisabled = filepath.Join(pathDirBlock, "."+fileDisabled) 29 | 30 | fiEnabled os.FileInfo 31 | fiDisabled os.FileInfo 32 | cases []testCase 33 | c testCase 34 | err error 35 | ) 36 | 37 | err = os.MkdirAll(pathDirBlock, 0700) 38 | if err != nil { 39 | t.Fatal(err) 40 | } 41 | 42 | err = os.WriteFile(hostsFileEnabled, []byte("127.0.0.2 localhost"), 0600) 43 | if err != nil { 44 | t.Fatal(err) 45 | } 46 | err = os.WriteFile(hostsFileDisabled, []byte("127.0.0.2 localhost"), 0600) 47 | if err != nil { 48 | t.Fatal(err) 49 | } 50 | 51 | fiEnabled, err = os.Stat(hostsFileEnabled) 52 | if err != nil { 53 | t.Fatal(err) 54 | } 55 | fiDisabled, err = os.Stat(hostsFileDisabled) 56 | if err != nil { 57 | t.Fatal(err) 58 | } 59 | 60 | cases = []testCase{{ 61 | desc: "With hosts block file enabled", 62 | hb: Blockd{ 63 | Name: fileEnabled, 64 | }, 65 | exp: Blockd{ 66 | Name: fileEnabled, 67 | lastUpdated: fiEnabled.ModTime(), 68 | LastUpdated: fiEnabled.ModTime().Format(lastUpdatedFormat), 69 | file: hostsFileEnabled, 70 | fileDisabled: filepath.Join(pathDirBlock, "."+fileEnabled), 71 | IsEnabled: true, 72 | isFileExist: true, 73 | }, 74 | }, { 75 | desc: "With hosts block file disabled", 76 | hb: Blockd{ 77 | Name: fileDisabled, 78 | }, 79 | exp: Blockd{ 80 | Name: fileDisabled, 81 | lastUpdated: fiDisabled.ModTime(), 82 | LastUpdated: fiDisabled.ModTime().Format(lastUpdatedFormat), 83 | file: filepath.Join(pathDirBlock, fileDisabled), 84 | fileDisabled: hostsFileDisabled, 85 | isFileExist: true, 86 | }, 87 | }, { 88 | desc: "With hosts block file not exist", 89 | hb: Blockd{ 90 | Name: fileNotExist, 91 | }, 92 | exp: Blockd{ 93 | Name: fileNotExist, 94 | file: filepath.Join(pathDirBlock, fileNotExist), 95 | fileDisabled: filepath.Join(pathDirBlock, "."+fileNotExist), 96 | }, 97 | }} 98 | 99 | for _, c = range cases { 100 | c.hb.init(pathDirBlock) 101 | 102 | test.Assert(t, c.desc, c.exp, c.hb) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 M. Shulhan 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | package rescached 5 | 6 | import ( 7 | "encoding/base64" 8 | "encoding/json" 9 | "fmt" 10 | "net/http" 11 | "net/url" 12 | 13 | "git.sr.ht/~shulhan/pakakeh.go/lib/dns" 14 | libhttp "git.sr.ht/~shulhan/pakakeh.go/lib/http" 15 | ) 16 | 17 | // Client for rescached. 18 | type Client struct { 19 | *libhttp.Client 20 | } 21 | 22 | // NewClient create new HTTP client that connect to rescached HTTP server. 23 | func NewClient(serverURL string, insecure bool) (cl *Client) { 24 | var ( 25 | httpcOpts = libhttp.ClientOptions{ 26 | ServerURL: serverURL, 27 | AllowInsecure: insecure, 28 | } 29 | ) 30 | cl = &Client{ 31 | Client: libhttp.NewClient(httpcOpts), 32 | } 33 | return cl 34 | } 35 | 36 | // Blockd return list of all block.d files on the server. 37 | func (cl *Client) Blockd() (hostBlockd map[string]*Blockd, err error) { 38 | var ( 39 | logp = `Blockd` 40 | clientReq = libhttp.ClientRequest{ 41 | Path: httpAPIBlockd, 42 | } 43 | clientResp *libhttp.ClientResponse 44 | ) 45 | 46 | clientResp, err = cl.Get(clientReq) 47 | if err != nil { 48 | return nil, fmt.Errorf("%s: %w", logp, err) 49 | } 50 | 51 | var res = libhttp.EndpointResponse{ 52 | Data: &hostBlockd, 53 | } 54 | 55 | err = json.Unmarshal(clientResp.Body, &res) 56 | if err != nil { 57 | return nil, fmt.Errorf("%s: %w", logp, err) 58 | } 59 | if res.Code != http.StatusOK { 60 | return nil, fmt.Errorf("%s: %d %s", logp, res.Code, res.Message) 61 | } 62 | 63 | return hostBlockd, nil 64 | } 65 | 66 | // BlockdDisable disable specific hosts on block.d. 67 | func (cl *Client) BlockdDisable(blockdName string) (blockd *Blockd, err error) { 68 | var ( 69 | logp = `BlockdDisable` 70 | clientReq = libhttp.ClientRequest{ 71 | Path: httpAPIBlockdDisable, 72 | Params: url.Values{ 73 | paramNameName: []string{blockdName}, 74 | }, 75 | } 76 | clientResp *libhttp.ClientResponse 77 | ) 78 | 79 | clientResp, err = cl.PostForm(clientReq) 80 | if err != nil { 81 | return nil, fmt.Errorf("%s: %w", logp, err) 82 | } 83 | 84 | var res = libhttp.EndpointResponse{ 85 | Data: &blockd, 86 | } 87 | 88 | err = json.Unmarshal(clientResp.Body, &res) 89 | if err != nil { 90 | return nil, fmt.Errorf("%s: %w", logp, err) 91 | } 92 | if res.Code != http.StatusOK { 93 | return nil, fmt.Errorf("%s: %d %s", logp, res.Code, res.Message) 94 | } 95 | 96 | return blockd, nil 97 | } 98 | 99 | // BlockdEnable enable specific hosts on block.d. 100 | func (cl *Client) BlockdEnable(blockdName string) (blockd *Blockd, err error) { 101 | var ( 102 | logp = `BlockdEnable` 103 | clientReq = libhttp.ClientRequest{ 104 | Path: httpAPIBlockdEnable, 105 | Params: url.Values{ 106 | paramNameName: []string{blockdName}, 107 | }, 108 | } 109 | clientResp *libhttp.ClientResponse 110 | ) 111 | 112 | clientResp, err = cl.PostForm(clientReq) 113 | if err != nil { 114 | return nil, fmt.Errorf("%s: %w", logp, err) 115 | } 116 | 117 | var res = libhttp.EndpointResponse{ 118 | Data: &blockd, 119 | } 120 | err = json.Unmarshal(clientResp.Body, &res) 121 | if err != nil { 122 | return nil, fmt.Errorf("%s: %w", logp, err) 123 | } 124 | if res.Code != http.StatusOK { 125 | return nil, fmt.Errorf("%s: %d %s", logp, res.Code, res.Message) 126 | } 127 | 128 | return blockd, nil 129 | } 130 | 131 | // BlockdFetch fetch the latest hosts file from the hosts block 132 | // provider based on registered URL. 133 | func (cl *Client) BlockdFetch(blockdName string) (blockd *Blockd, err error) { 134 | var ( 135 | logp = `BlockdFetch` 136 | req = libhttp.ClientRequest{ 137 | Path: httpAPIBlockdFetch, 138 | Params: url.Values{ 139 | paramNameName: []string{blockdName}, 140 | }, 141 | } 142 | clientResp *libhttp.ClientResponse 143 | ) 144 | 145 | clientResp, err = cl.PostForm(req) 146 | if err != nil { 147 | return nil, fmt.Errorf("%s: %w", logp, err) 148 | } 149 | 150 | var res = libhttp.EndpointResponse{ 151 | Data: &blockd, 152 | } 153 | err = json.Unmarshal(clientResp.Body, &res) 154 | if err != nil { 155 | return nil, fmt.Errorf("%s: %w", logp, err) 156 | } 157 | if res.Code != http.StatusOK { 158 | return nil, fmt.Errorf("%s: %d %s", logp, res.Code, res.Message) 159 | } 160 | 161 | return blockd, nil 162 | } 163 | 164 | // Caches fetch all of non-local caches from server. 165 | func (cl *Client) Caches() (answers []*dns.Answer, err error) { 166 | var ( 167 | logp = `Caches` 168 | clientReq = libhttp.ClientRequest{ 169 | Path: httpAPICaches, 170 | } 171 | clientResp *libhttp.ClientResponse 172 | ) 173 | 174 | clientResp, err = cl.Get(clientReq) 175 | if err != nil { 176 | return nil, fmt.Errorf("%s: %w", logp, err) 177 | } 178 | 179 | var res = libhttp.EndpointResponse{ 180 | Data: &answers, 181 | } 182 | err = json.Unmarshal(clientResp.Body, &res) 183 | if err != nil { 184 | return nil, fmt.Errorf("%s: %w", logp, err) 185 | } 186 | 187 | if res.Code != http.StatusOK { 188 | return nil, fmt.Errorf("%s: %d %s", logp, res.Code, res.Message) 189 | } 190 | 191 | return answers, nil 192 | } 193 | 194 | // CachesRemove request to remove caches by its domain name. 195 | func (cl *Client) CachesRemove(q string) (listAnswer []*dns.Answer, err error) { 196 | var ( 197 | logp = `CachesRemove` 198 | clientReq = libhttp.ClientRequest{ 199 | Path: httpAPICaches, 200 | Params: url.Values{ 201 | paramNameName: []string{q}, 202 | }, 203 | } 204 | clientResp *libhttp.ClientResponse 205 | ) 206 | 207 | clientResp, err = cl.Delete(clientReq) 208 | if err != nil { 209 | return nil, fmt.Errorf("%s: %w", logp, err) 210 | } 211 | 212 | var res = libhttp.EndpointResponse{ 213 | Data: &listAnswer, 214 | } 215 | 216 | err = json.Unmarshal(clientResp.Body, &res) 217 | if err != nil { 218 | return nil, fmt.Errorf("%s: %w", logp, err) 219 | } 220 | 221 | if res.Code != http.StatusOK { 222 | return nil, fmt.Errorf("%s: %d %s", logp, res.Code, res.Message) 223 | } 224 | 225 | return listAnswer, nil 226 | } 227 | 228 | // CachesSearch search the answer in caches by its domain name and return it 229 | // as DNS message. 230 | func (cl *Client) CachesSearch(q string) (listMsg []*dns.Message, err error) { 231 | var ( 232 | logp = `CachesSearch` 233 | clientReq = libhttp.ClientRequest{ 234 | Path: httpAPICachesSearch, 235 | Params: url.Values{ 236 | paramNameQuery: []string{q}, 237 | }, 238 | } 239 | clientResp *libhttp.ClientResponse 240 | ) 241 | 242 | clientResp, err = cl.Get(clientReq) 243 | if err != nil { 244 | return nil, fmt.Errorf("%s: %w", logp, err) 245 | } 246 | 247 | var res = libhttp.EndpointResponse{ 248 | Data: &listMsg, 249 | } 250 | err = json.Unmarshal(clientResp.Body, &res) 251 | if err != nil { 252 | return nil, fmt.Errorf("%s: %w", logp, err) 253 | } 254 | if res.Code != http.StatusOK { 255 | return nil, fmt.Errorf("%s: %d %s", logp, res.Code, res.Message) 256 | } 257 | 258 | return listMsg, nil 259 | } 260 | 261 | // Env get the server environment. 262 | func (cl *Client) Env() (env *Environment, err error) { 263 | var ( 264 | logp = `Env` 265 | clientReq = libhttp.ClientRequest{ 266 | Path: httpAPIEnvironment, 267 | } 268 | clientResp *libhttp.ClientResponse 269 | ) 270 | 271 | clientResp, err = cl.Get(clientReq) 272 | if err != nil { 273 | return nil, fmt.Errorf("%s: %w", logp, err) 274 | } 275 | 276 | var res = libhttp.EndpointResponse{ 277 | Data: &env, 278 | } 279 | err = json.Unmarshal(clientResp.Body, &res) 280 | if err != nil { 281 | return nil, fmt.Errorf("%s: %w", logp, err) 282 | } 283 | if res.Code != http.StatusOK { 284 | return nil, fmt.Errorf("%s: %d %s", logp, res.Code, res.Message) 285 | } 286 | return env, nil 287 | } 288 | 289 | // EnvUpdate update the server environment using new Environment. 290 | func (cl *Client) EnvUpdate(envIn *Environment) (envOut *Environment, err error) { 291 | var ( 292 | logp = `EnvUpdate` 293 | clientReq = libhttp.ClientRequest{ 294 | Path: httpAPIEnvironment, 295 | Params: envIn, 296 | } 297 | clientResp *libhttp.ClientResponse 298 | ) 299 | 300 | clientResp, err = cl.PostJSON(clientReq) 301 | if err != nil { 302 | return nil, fmt.Errorf("%s: %w", logp, err) 303 | } 304 | 305 | var res = libhttp.EndpointResponse{ 306 | Data: &envOut, 307 | } 308 | err = json.Unmarshal(clientResp.Body, &res) 309 | if err != nil { 310 | return nil, fmt.Errorf("%s: %w", logp, err) 311 | } 312 | if res.Code != http.StatusOK { 313 | return nil, fmt.Errorf("%s: %d %s", logp, res.Code, res.Message) 314 | } 315 | return envOut, nil 316 | } 317 | 318 | // HostsdCreate create new hosts file inside the hosts.d with requested name. 319 | func (cl *Client) HostsdCreate(name string) (hostsFile *dns.HostsFile, err error) { 320 | var ( 321 | logp = `HostsdCreate` 322 | clientReq = libhttp.ClientRequest{ 323 | Path: apiHostsd, 324 | Params: url.Values{ 325 | paramNameName: []string{name}, 326 | }, 327 | } 328 | clientResp *libhttp.ClientResponse 329 | ) 330 | 331 | clientResp, err = cl.PostForm(clientReq) 332 | if err != nil { 333 | return nil, fmt.Errorf("%s: %w", logp, err) 334 | } 335 | 336 | var res = libhttp.EndpointResponse{ 337 | Data: &hostsFile, 338 | } 339 | err = json.Unmarshal(clientResp.Body, &res) 340 | if err != nil { 341 | return nil, fmt.Errorf("%s: %w", logp, err) 342 | } 343 | if res.Code != http.StatusOK { 344 | return nil, fmt.Errorf("%s: %d %s", logp, res.Code, res.Message) 345 | } 346 | return hostsFile, nil 347 | } 348 | 349 | // HostsdDelete delete hosts file inside the hosts.d by file name. 350 | func (cl *Client) HostsdDelete(name string) (hostsFile *dns.HostsFile, err error) { 351 | var ( 352 | logp = `HostsdDelete` 353 | clientReq = libhttp.ClientRequest{ 354 | Path: apiHostsd, 355 | Params: url.Values{ 356 | paramNameName: []string{name}, 357 | }, 358 | } 359 | clientResp *libhttp.ClientResponse 360 | ) 361 | 362 | clientResp, err = cl.Delete(clientReq) 363 | if err != nil { 364 | return nil, fmt.Errorf("%s: %w", logp, err) 365 | } 366 | 367 | var res = libhttp.EndpointResponse{ 368 | Data: &hostsFile, 369 | } 370 | err = json.Unmarshal(clientResp.Body, &res) 371 | if err != nil { 372 | return nil, fmt.Errorf("%s: %w", logp, err) 373 | } 374 | if res.Code != http.StatusOK { 375 | return nil, fmt.Errorf("%s: %d %s", logp, res.Code, res.Message) 376 | } 377 | return hostsFile, nil 378 | } 379 | 380 | // HostsdGet get the content of hosts file inside the hosts.d by file name. 381 | func (cl *Client) HostsdGet(name string) (listrr []*dns.ResourceRecord, err error) { 382 | var ( 383 | logp = `HostsdGet` 384 | clientReq = libhttp.ClientRequest{ 385 | Path: apiHostsd, 386 | Params: url.Values{ 387 | paramNameName: []string{name}, 388 | }, 389 | } 390 | clientResp *libhttp.ClientResponse 391 | ) 392 | 393 | clientResp, err = cl.Get(clientReq) 394 | if err != nil { 395 | return nil, fmt.Errorf("%s: %w", logp, err) 396 | } 397 | 398 | var res = libhttp.EndpointResponse{ 399 | Data: &listrr, 400 | } 401 | err = json.Unmarshal(clientResp.Body, &res) 402 | if err != nil { 403 | return nil, fmt.Errorf("%s: %w", logp, err) 404 | } 405 | if res.Code != http.StatusOK { 406 | return nil, fmt.Errorf("%s: %d %s", logp, res.Code, res.Message) 407 | } 408 | return listrr, nil 409 | } 410 | 411 | // HostsdRecordAdd add new resource record to the hosts file. 412 | func (cl *Client) HostsdRecordAdd(hostsName, domain, value string) (record *dns.ResourceRecord, err error) { 413 | var ( 414 | logp = `HostsdRecordAdd` 415 | clientReq = libhttp.ClientRequest{ 416 | Path: apiHostsdRR, 417 | Params: url.Values{ 418 | paramNameName: []string{hostsName}, 419 | paramNameDomain: []string{domain}, 420 | paramNameValue: []string{value}, 421 | }, 422 | } 423 | clientResp *libhttp.ClientResponse 424 | ) 425 | 426 | clientResp, err = cl.PostForm(clientReq) 427 | if err != nil { 428 | return nil, fmt.Errorf("%s: %w", logp, err) 429 | } 430 | 431 | var res = libhttp.EndpointResponse{ 432 | Data: &record, 433 | } 434 | err = json.Unmarshal(clientResp.Body, &res) 435 | if err != nil { 436 | return nil, fmt.Errorf("%s: %w", logp, err) 437 | } 438 | if res.Code != http.StatusOK { 439 | return nil, fmt.Errorf("%s: %d %s", logp, res.Code, res.Message) 440 | } 441 | 442 | return record, nil 443 | } 444 | 445 | // HostsdRecordDelete delete a record from hosts file by domain name. 446 | func (cl *Client) HostsdRecordDelete(hostsName, domain string) (record *dns.ResourceRecord, err error) { 447 | var ( 448 | logp = `HostsdRecordDelete` 449 | clientReq = libhttp.ClientRequest{ 450 | Path: apiHostsdRR, 451 | Params: url.Values{ 452 | paramNameName: []string{hostsName}, 453 | paramNameDomain: []string{domain}, 454 | }, 455 | } 456 | clientResp *libhttp.ClientResponse 457 | ) 458 | 459 | clientResp, err = cl.Delete(clientReq) 460 | if err != nil { 461 | return nil, fmt.Errorf("%s: %w", logp, err) 462 | } 463 | 464 | var res = libhttp.EndpointResponse{ 465 | Data: &record, 466 | } 467 | err = json.Unmarshal(clientResp.Body, &res) 468 | if err != nil { 469 | return nil, fmt.Errorf("%s: %w", logp, err) 470 | } 471 | if res.Code != http.StatusOK { 472 | return nil, fmt.Errorf("%s: %d %s", logp, res.Code, res.Message) 473 | } 474 | 475 | return record, nil 476 | } 477 | 478 | // Zoned fetch and return list of zone managed on server. 479 | func (cl *Client) Zoned() (zones map[string]*dns.Zone, err error) { 480 | var ( 481 | logp = `Zoned` 482 | clientReq = libhttp.ClientRequest{ 483 | Path: apiZoned, 484 | } 485 | clientResp *libhttp.ClientResponse 486 | ) 487 | 488 | clientResp, err = cl.Get(clientReq) 489 | if err != nil { 490 | return nil, fmt.Errorf("%s: %w", logp, err) 491 | } 492 | 493 | var res = libhttp.EndpointResponse{ 494 | Data: &zones, 495 | } 496 | err = json.Unmarshal(clientResp.Body, &res) 497 | if err != nil { 498 | return nil, fmt.Errorf("%s: %w", logp, err) 499 | } 500 | 501 | return zones, nil 502 | } 503 | 504 | // ZonedCreate create new zone file. 505 | func (cl *Client) ZonedCreate(name string) (zone *dns.Zone, err error) { 506 | var ( 507 | logp = `ZonedCreate` 508 | clientReq = libhttp.ClientRequest{ 509 | Path: apiZoned, 510 | Params: url.Values{ 511 | paramNameName: []string{name}, 512 | }, 513 | } 514 | clientResp *libhttp.ClientResponse 515 | ) 516 | 517 | clientResp, err = cl.PostForm(clientReq) 518 | if err != nil { 519 | return nil, fmt.Errorf("%s: %w", logp, err) 520 | } 521 | 522 | var res = libhttp.EndpointResponse{ 523 | Data: &zone, 524 | } 525 | err = json.Unmarshal(clientResp.Body, &res) 526 | if err != nil { 527 | return nil, fmt.Errorf("%s: %w", logp, err) 528 | } 529 | if res.Code != http.StatusOK { 530 | return nil, fmt.Errorf("%s: %d %s", logp, res.Code, res.Message) 531 | } 532 | return zone, nil 533 | } 534 | 535 | // ZonedDelete delete zone file by name. 536 | func (cl *Client) ZonedDelete(name string) (zone *dns.Zone, err error) { 537 | var ( 538 | logp = `ZonedDelete` 539 | clientReq = libhttp.ClientRequest{ 540 | Path: apiZoned, 541 | Params: url.Values{ 542 | paramNameName: []string{name}, 543 | }, 544 | } 545 | clientResp *libhttp.ClientResponse 546 | ) 547 | 548 | clientResp, err = cl.Delete(clientReq) 549 | if err != nil { 550 | return nil, fmt.Errorf("%s: %w", logp, err) 551 | } 552 | if clientResp.HTTPResponse.StatusCode != http.StatusOK { 553 | return nil, fmt.Errorf("%s: %s", logp, clientResp.HTTPResponse.Status) 554 | } 555 | 556 | var res = libhttp.EndpointResponse{ 557 | Data: &zone, 558 | } 559 | err = json.Unmarshal(clientResp.Body, &res) 560 | if err != nil { 561 | return nil, fmt.Errorf("%s: %w", logp, err) 562 | } 563 | if res.Code != http.StatusOK { 564 | return nil, fmt.Errorf("%s: %d %s", logp, res.Code, res.Message) 565 | } 566 | return zone, nil 567 | } 568 | 569 | // ZonedRecords fetch the zone records by its name. 570 | func (cl *Client) ZonedRecords(zone string) (zoneRecords map[string][]*dns.ResourceRecord, err error) { 571 | var ( 572 | logp = `ZonedRecords` 573 | clientReq = libhttp.ClientRequest{ 574 | Path: apiZonedRR, 575 | Params: url.Values{ 576 | paramNameName: []string{zone}, 577 | }, 578 | } 579 | clientResp *libhttp.ClientResponse 580 | ) 581 | 582 | clientResp, err = cl.Get(clientReq) 583 | if err != nil { 584 | return nil, fmt.Errorf("%s: %w", logp, err) 585 | } 586 | 587 | var res = libhttp.EndpointResponse{ 588 | Data: &zoneRecords, 589 | } 590 | err = json.Unmarshal(clientResp.Body, &res) 591 | if err != nil { 592 | return nil, fmt.Errorf("%s: %w", logp, err) 593 | } 594 | 595 | return zoneRecords, nil 596 | } 597 | 598 | // ZonedRecordAdd add new record to zone file. 599 | func (cl *Client) ZonedRecordAdd(name string, rreq dns.ResourceRecord) (rres *dns.ResourceRecord, err error) { 600 | var ( 601 | logp = `ZonedRecordAdd` 602 | zrr = zoneRecordRequest{ 603 | Name: name, 604 | } 605 | ok bool 606 | ) 607 | 608 | zrr.Type, ok = dns.RecordTypeNames[rreq.Type] 609 | if !ok { 610 | return nil, fmt.Errorf("%s: unknown record type: %d", logp, rreq.Type) 611 | } 612 | 613 | var rawb []byte 614 | 615 | rawb, err = json.Marshal(rreq) 616 | if err != nil { 617 | return nil, fmt.Errorf("%s: %w", logp, err) 618 | } 619 | 620 | zrr.Record = base64.StdEncoding.EncodeToString(rawb) 621 | 622 | var clientReq = libhttp.ClientRequest{ 623 | Path: apiZonedRR, 624 | Params: zrr, 625 | } 626 | var clientResp *libhttp.ClientResponse 627 | 628 | clientResp, err = cl.PostJSON(clientReq) 629 | if err != nil { 630 | return nil, fmt.Errorf("%s: %w", logp, err) 631 | } 632 | 633 | rres = &dns.ResourceRecord{} 634 | var res = &libhttp.EndpointResponse{ 635 | Data: rres, 636 | } 637 | err = json.Unmarshal(clientResp.Body, res) 638 | if err != nil { 639 | return nil, fmt.Errorf("%s: %w", logp, err) 640 | } 641 | if res.Code != http.StatusOK { 642 | return nil, fmt.Errorf("%s: %d %s", logp, res.Code, res.Message) 643 | } 644 | 645 | return rres, nil 646 | } 647 | 648 | // ZonedRecordDelete delete record from zone file. 649 | func (cl *Client) ZonedRecordDelete(name string, rreq dns.ResourceRecord) (zoneRecords map[string][]*dns.ResourceRecord, err error) { 650 | var ( 651 | logp = `ZonedRecordDelete` 652 | params = url.Values{ 653 | paramNameName: []string{name}, 654 | } 655 | 656 | vstr string 657 | ok bool 658 | ) 659 | 660 | vstr, ok = dns.RecordTypeNames[rreq.Type] 661 | if !ok { 662 | return nil, fmt.Errorf("%s: unknown record type: %d", logp, rreq.Type) 663 | } 664 | params.Set(paramNameType, vstr) 665 | 666 | var rawb []byte 667 | 668 | rawb, err = json.Marshal(rreq) 669 | if err != nil { 670 | return nil, fmt.Errorf("%s: %w", logp, err) 671 | } 672 | vstr = base64.StdEncoding.EncodeToString(rawb) 673 | params.Set(paramNameRecord, vstr) 674 | 675 | var clientReq = libhttp.ClientRequest{ 676 | Path: apiZonedRR, 677 | Params: params, 678 | } 679 | var clientResp *libhttp.ClientResponse 680 | 681 | clientResp, err = cl.Delete(clientReq) 682 | if err != nil { 683 | return nil, fmt.Errorf("%s: %w", logp, err) 684 | } 685 | 686 | var res = &libhttp.EndpointResponse{ 687 | Data: &zoneRecords, 688 | } 689 | err = json.Unmarshal(clientResp.Body, res) 690 | if err != nil { 691 | return nil, fmt.Errorf("%s: %w", logp, err) 692 | } 693 | if res.Code != http.StatusOK { 694 | return nil, fmt.Errorf("%s: %d %s", logp, res.Code, res.Message) 695 | } 696 | 697 | return zoneRecords, nil 698 | } 699 | -------------------------------------------------------------------------------- /client_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 M. Shulhan 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | package rescached 5 | 6 | import ( 7 | "os" 8 | "testing" 9 | "time" 10 | 11 | "git.sr.ht/~shulhan/pakakeh.go/lib/test" 12 | ) 13 | 14 | func TestClient_BlockdEnable(t *testing.T) { 15 | type testCase struct { 16 | desc string 17 | name string 18 | expError string 19 | expBlockd Blockd 20 | } 21 | 22 | var ( 23 | cases []testCase 24 | c testCase 25 | gotBlockd *Blockd 26 | err error 27 | ) 28 | 29 | cases = []testCase{{ 30 | desc: "With invalid block.d name", 31 | name: "xxx", 32 | expError: "BlockdEnable: 400 hosts block.d name not found: xxx", 33 | }, { 34 | desc: "With valid block.d name", 35 | name: "a.block", 36 | expBlockd: Blockd{ 37 | Name: "a.block", 38 | URL: "http://127.0.0.1:11180/hosts/a", 39 | IsEnabled: true, 40 | }, 41 | }} 42 | 43 | for _, c = range cases { 44 | gotBlockd, err = resc.BlockdEnable(c.name) 45 | if err != nil { 46 | test.Assert(t, "error", c.expError, err.Error()) 47 | continue 48 | } 49 | 50 | gotBlockd.LastUpdated = "" 51 | 52 | test.Assert(t, c.desc, c.expBlockd, *gotBlockd) 53 | } 54 | } 55 | 56 | func TestClient_BlockdDisable(t *testing.T) { 57 | type testCase struct { 58 | desc string 59 | name string 60 | expError string 61 | expBlockd Blockd 62 | } 63 | 64 | var ( 65 | cases []testCase 66 | c testCase 67 | gotBlockd *Blockd 68 | err error 69 | ) 70 | 71 | cases = []testCase{{ 72 | desc: "With invalid block.d name", 73 | name: "xxx", 74 | expError: "BlockdDisable: 400 hosts block.d name not found: xxx", 75 | }, { 76 | desc: "With valid block.d name", 77 | name: "a.block", 78 | expBlockd: Blockd{ 79 | Name: "a.block", 80 | URL: "http://127.0.0.1:11180/hosts/a", 81 | IsEnabled: false, 82 | }, 83 | }} 84 | 85 | for _, c = range cases { 86 | gotBlockd, err = resc.BlockdDisable(c.name) 87 | if err != nil { 88 | test.Assert(t, "error", c.expError, err.Error()) 89 | continue 90 | } 91 | 92 | gotBlockd.LastUpdated = "" 93 | 94 | test.Assert(t, c.desc, c.expBlockd, *gotBlockd) 95 | } 96 | } 97 | 98 | func TestClient_BlockdFetch(t *testing.T) { 99 | var ( 100 | affectedBlockd = testEnv.HostBlockd["a.block"] 101 | 102 | gotBlockd *Blockd 103 | expBlockd *Blockd 104 | expString string 105 | gotBytes []byte 106 | err error 107 | ) 108 | 109 | // Revert the content of a.block. 110 | t.Cleanup(func() { 111 | err = os.WriteFile(affectedBlockd.fileDisabled, []byte("127.0.0.1 a.block\n"), 0600) 112 | }) 113 | 114 | expString = `BlockdFetch: 400 httpAPIBlockdFetch: unknown hosts block.d name: xxx` 115 | 116 | gotBlockd, err = resc.BlockdFetch("xxx") 117 | if err != nil { 118 | test.Assert(t, "error", expString, err.Error()) 119 | } else { 120 | test.Assert(t, "BlockdFetch", expBlockd, gotBlockd) 121 | } 122 | 123 | expBlockd = &Blockd{ 124 | Name: "a.block", 125 | URL: "http://127.0.0.1:11180/hosts/a", 126 | IsEnabled: false, 127 | } 128 | 129 | // Make the block.d last updated less than 7 days ago. 130 | testEnv.HostBlockd["a.block"].lastUpdated = time.Now().Add(-1 * 10 * 24 * time.Hour) 131 | 132 | gotBlockd, err = resc.BlockdFetch("a.block") 133 | if err != nil { 134 | t.Fatal(err) 135 | } 136 | 137 | expBlockd.LastUpdated = gotBlockd.LastUpdated 138 | 139 | test.Assert(t, "BlockdFetch", expBlockd, gotBlockd) 140 | 141 | gotBytes, err = os.ReadFile(affectedBlockd.fileDisabled) 142 | if err != nil { 143 | t.Fatal(err) 144 | } 145 | 146 | expString = "127.0.0.2 a.block\n" 147 | 148 | test.Assert(t, "BlockdFetch", expString, string(gotBytes)) 149 | } 150 | -------------------------------------------------------------------------------- /cmd/rescached/main.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2018 M. Shulhan 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | // Program rescached server that caching internet name and address on local 5 | // memory for speeding up DNS resolution. 6 | // 7 | // Rescached primary goal is only to caching DNS queries and answers, used by 8 | // personal or small group of users, to minimize unneeded traffic to outside 9 | // network. 10 | package main 11 | 12 | import ( 13 | "flag" 14 | "fmt" 15 | "log" 16 | "os" 17 | "os/signal" 18 | "strings" 19 | "syscall" 20 | "time" 21 | 22 | "git.sr.ht/~shulhan/ciigo" 23 | "git.sr.ht/~shulhan/pakakeh.go/lib/debug" 24 | "git.sr.ht/~shulhan/pakakeh.go/lib/memfs" 25 | 26 | "git.sr.ht/~shulhan/rescached" 27 | ) 28 | 29 | const ( 30 | cmdDev = `dev` // Command to run rescached for local development. 31 | cmdEmbed = `embed` // Command to generate embedded files. 32 | cmdVersion = `version` 33 | ) 34 | 35 | func main() { 36 | var ( 37 | dirBase string 38 | fileConfig string 39 | ) 40 | 41 | log.SetFlags(0) 42 | log.SetPrefix("rescached: ") 43 | 44 | flag.StringVar(&dirBase, "dir-base", "", "Base directory for reading and storing rescached data.") 45 | flag.StringVar(&fileConfig, "config", "", "Path to configuration.") 46 | flag.Parse() 47 | 48 | var ( 49 | env *rescached.Environment 50 | err error 51 | ) 52 | 53 | env, err = rescached.LoadEnvironment(dirBase, fileConfig) 54 | if err != nil { 55 | log.Fatal(err) 56 | } 57 | 58 | var ( 59 | cmd = strings.ToLower(flag.Arg(0)) 60 | running chan bool 61 | ) 62 | 63 | switch cmd { 64 | case cmdDev: 65 | running = make(chan bool) 66 | go watchWww(env, running) 67 | go watchWwwDoc() 68 | 69 | case cmdEmbed: 70 | err = env.HttpdOptions.Memfs.GoEmbed() 71 | if err != nil { 72 | log.Fatal(err) 73 | } 74 | return 75 | 76 | case cmdVersion: 77 | fmt.Printf("rescached version %s\n", rescached.Version) 78 | return 79 | } 80 | 81 | var rcd *rescached.Server 82 | 83 | rcd, err = rescached.New(env) 84 | if err != nil { 85 | log.Fatal(err) 86 | } 87 | 88 | err = rcd.Start() 89 | if err != nil { 90 | log.Fatal(err) 91 | } 92 | 93 | if debug.Value >= 4 { 94 | go debugRuntime() 95 | } 96 | 97 | var qsignal = make(chan os.Signal, 1) 98 | signal.Notify(qsignal, syscall.SIGQUIT, syscall.SIGSEGV, syscall.SIGTERM, syscall.SIGINT) 99 | <-qsignal 100 | if cmd == cmdDev { 101 | running <- false 102 | <-running 103 | } 104 | rcd.Stop() 105 | os.Exit(0) 106 | } 107 | 108 | func debugRuntime() { 109 | ticker := time.NewTicker(30 * time.Second) 110 | memHeap := debug.NewMemHeap() 111 | 112 | for range ticker.C { 113 | debug.WriteHeapProfile("rescached", true) 114 | 115 | memHeap.Collect() 116 | 117 | fmt.Printf("=== rescached: MemHeap{RelHeapAlloc:%d RelHeapObjects:%d DiffHeapObjects:%d}\n", 118 | memHeap.RelHeapAlloc, memHeap.RelHeapObjects, 119 | memHeap.DiffHeapObjects) 120 | } 121 | } 122 | 123 | // watchWww watch any changes to files inside _www directory and regenerate 124 | // the embed file "memfs_generate.go". 125 | func watchWww(env *rescached.Environment, running chan bool) { 126 | var ( 127 | logp = "watchWww" 128 | tick = time.NewTicker(3 * time.Second) 129 | isRunning = true 130 | 131 | dw *memfs.DirWatcher 132 | nChanges int 133 | err error 134 | ) 135 | 136 | dw, err = env.HttpdOptions.Memfs.Watch(memfs.WatchOptions{}) 137 | if err != nil { 138 | log.Fatalf("%s: %s", logp, err) 139 | } 140 | 141 | for isRunning { 142 | select { 143 | case <-dw.C: 144 | nChanges++ 145 | 146 | case <-tick.C: 147 | if nChanges == 0 { 148 | continue 149 | } 150 | 151 | fmt.Printf("--- %d changes\n", nChanges) 152 | err = env.HttpdOptions.Memfs.GoEmbed() 153 | if err != nil { 154 | log.Printf("%s", err) 155 | } 156 | nChanges = 0 157 | 158 | case <-running: 159 | isRunning = false 160 | } 161 | } 162 | 163 | // Run GoEmbed for the last time. 164 | if nChanges > 0 { 165 | fmt.Printf("--- %d changes\n", nChanges) 166 | err = env.HttpdOptions.Memfs.GoEmbed() 167 | if err != nil { 168 | log.Printf("%s", err) 169 | } 170 | } 171 | dw.Stop() 172 | running <- false 173 | } 174 | 175 | func watchWwwDoc() { 176 | var ( 177 | logp = "watchWwwDoc" 178 | convertOpts = ciigo.ConvertOptions{ 179 | Root: "_www/doc", 180 | HTMLTemplate: "_www/doc/html.tmpl", 181 | } 182 | 183 | err error 184 | ) 185 | 186 | err = ciigo.Watch(&convertOpts) 187 | if err != nil { 188 | log.Fatalf("%s: %s", logp, err) 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /cmd/resolver/doc.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 M. Shulhan 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | /* 5 | Program resolver is command line interface (CLI) for DNS and rescached server. 6 | 7 | # SYNOPSIS 8 | 9 | resolver [-insecure] [-ns=] [-server=] [args...] 10 | 11 | # DESCRIPTION 12 | 13 | resolver is a tool to resolve hostname to IP address or to query services 14 | on hostname by type (MX, SOA, TXT, etc.) using standard DNS protocol with UDP, 15 | DNS over TLS (DoT), or DNS over HTTPS (DoH). 16 | 17 | It is also provide CLI to the rescached server to manage environment, block.d, 18 | hosts.d, and zone.d; as in the web user interface. 19 | 20 | # OPTIONS 21 | 22 | The following options affect the command operation. 23 | 24 | -insecure 25 | 26 | Ignore invalid server certificate when querying DoT, DoH, or rescached 27 | server. 28 | This option only affect the `query` command. 29 | 30 | -ns= 31 | 32 | This option define the parent DNS server where the resolver send the query. 33 | This option only affect the `query` command. 34 | 35 | The nameserver is defined in the following format, 36 | 37 | ("udp"/"tcp"/"https") "://" (domain / ip-address) [":" port] 38 | 39 | Examples, 40 | 41 | - udp://194.233.68.184:53 for querying with UDP, 42 | - tcp://194.233.68.184:53 for querying with TCP, 43 | - https://194.233.68.184:853 for querying with DNS over TLS (DoT), and 44 | - https://kilabit.info/dns-query for querying with DNS over HTTPS (DoH). 45 | 46 | Default to one of "nameserver" in `/etc/resolv.conf`. 47 | 48 | -server= 49 | 50 | Set the rescached HTTP server where commands, except query, will be send. 51 | The rescached-URL use HTTP scheme: 52 | 53 | ("http" / "https") "://" (domain / ip-address) [":" port] 54 | 55 | Default to https://127.0.0.1:5380 if its empty. 56 | 57 | # COMMANDS 58 | 59 | General commands, 60 | 61 | help # Print this message. 62 | version # Print the program version. 63 | 64 | Query the DNS server, 65 | 66 | query [type] [class] 67 | 68 | Managing block.d files, 69 | 70 | block.d # List all hosts in block.d. 71 | block.d disable 72 | block.d enable 73 | block.d update 74 | 75 | Managing caches, 76 | 77 | caches 78 | caches search 79 | caches remove 80 | 81 | Managing environment, 82 | 83 | env 84 | env update 85 | 86 | Managing hosts.d files, 87 | 88 | hosts.d create 89 | hosts.d get 90 | 91 | Managing record in hosts.d file, 92 | 93 | hosts.d rr add 94 | hosts.d rr delete 95 | 96 | Managing zone.d files, 97 | 98 | zone.d 99 | zone.d create 100 | zone.d delete 101 | 102 | Managing record in zone.d, 103 | 104 | zone.d rr get 105 | zone.d rr add <"@" | subdomain> ... 106 | zone.d rr delete <"@" | subdomain> 107 | 108 | For more information see the manual page for resolver(1) or its HTML page at 109 | http://127.0.0.1:5380/doc/resolver.html. 110 | */ 111 | package main 112 | -------------------------------------------------------------------------------- /cmd/resolver/main.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2018 M. Shulhan 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | package main 5 | 6 | import ( 7 | "flag" 8 | "fmt" 9 | "log" 10 | "os" 11 | "strings" 12 | 13 | "git.sr.ht/~shulhan/rescached" 14 | ) 15 | 16 | // List of valid commands. 17 | const ( 18 | cmdBlockd = "block.d" 19 | cmdCaches = "caches" 20 | cmdEnv = "env" 21 | cmdHelp = "help" 22 | cmdHostsd = "hosts.d" 23 | cmdQuery = "query" 24 | cmdVersion = "version" 25 | cmdZoned = "zone.d" 26 | 27 | subCmdAdd = "add" 28 | subCmdCreate = "create" 29 | subCmdDelete = "delete" 30 | subCmdDisable = "disable" 31 | subCmdEnable = "enable" 32 | subCmdGet = "get" 33 | subCmdRR = "rr" 34 | subCmdRemove = "remove" 35 | subCmdSearch = "search" 36 | subCmdUpdate = "update" 37 | ) 38 | 39 | // Usage of program, overwritten by build. 40 | var Usage string 41 | 42 | func main() { 43 | var ( 44 | rsol = new(resolver) 45 | 46 | args []string 47 | ) 48 | 49 | log.SetFlags(0) 50 | 51 | flag.BoolVar(&rsol.insecure, "insecure", false, "Ignore invalid server certificate.") 52 | flag.StringVar(&rsol.nameserver, "ns", "", "Parent name server address using scheme based.") 53 | flag.StringVar(&rsol.rescachedURL, `server`, defRescachedURL, `Set the rescached HTTP server.`) 54 | 55 | flag.Parse() 56 | 57 | args = flag.Args() 58 | 59 | if len(args) == 0 { 60 | fmt.Println(Usage) 61 | os.Exit(1) 62 | } 63 | 64 | rsol.cmd = strings.ToLower(args[0]) 65 | 66 | switch rsol.cmd { 67 | case cmdBlockd: 68 | rsol.doCmdBlockd(args[1:]) 69 | 70 | case cmdCaches: 71 | rsol.doCmdCaches(args[1:]) 72 | 73 | case cmdEnv: 74 | rsol.doCmdEnv(args[1:]) 75 | 76 | case cmdHelp: 77 | fmt.Println(Usage) 78 | os.Exit(1) 79 | 80 | case cmdHostsd: 81 | rsol.doCmdHostsd(args[1:]) 82 | 83 | case cmdQuery: 84 | args = args[1:] 85 | if len(args) == 0 { 86 | log.Fatalf("resolver: %s: missing argument", rsol.cmd) 87 | } 88 | 89 | rsol.doCmdQuery(args) 90 | 91 | case cmdVersion: 92 | fmt.Println(rescached.Version) 93 | 94 | case cmdZoned: 95 | args = args[1:] 96 | rsol.doCmdZoned(args) 97 | 98 | default: 99 | log.Printf("resolver: unknown command: %s", rsol.cmd) 100 | os.Exit(2) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /cmd/resolverbench/main.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2018 M. Shulhan 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | // Program resolverbench program to benchmark DNS server or resolver. 5 | package main 6 | 7 | import ( 8 | "fmt" 9 | "log" 10 | "os" 11 | "time" 12 | 13 | "git.sr.ht/~shulhan/pakakeh.go/lib/dns" 14 | ) 15 | 16 | func usage() { 17 | fmt.Println("Usage: " + os.Args[0] + " ") 18 | os.Exit(1) 19 | } 20 | 21 | func main() { 22 | if len(os.Args) < 3 { 23 | usage() 24 | } 25 | 26 | log.SetFlags(0) 27 | 28 | cl, err := dns.NewUDPClient(os.Args[1]) 29 | if err != nil { 30 | log.Fatal(err) 31 | } 32 | 33 | hostsFile, err := dns.ParseHostsFile(os.Args[2]) 34 | if err != nil { 35 | log.Fatal(err) 36 | } 37 | 38 | var nfail int 39 | 40 | fmt.Printf("= Benchmarking with %d messages\n", len(hostsFile.Records)) 41 | 42 | timeStart := time.Now() 43 | q := dns.MessageQuestion{} 44 | for _, rr := range hostsFile.Records { 45 | q.Name = rr.Name 46 | q.Type = rr.Type 47 | q.Class = rr.Class 48 | res, err := cl.Lookup(q, true) 49 | if err != nil { 50 | nfail++ 51 | log.Println("! Send error: ", err) 52 | continue 53 | } 54 | 55 | exp := rr.Value.(string) 56 | got := "" 57 | found := false 58 | for x := 0; x < len(res.Answer); x++ { 59 | got = res.Answer[x].Value.(string) 60 | if exp == got { 61 | found = true 62 | break 63 | } 64 | } 65 | 66 | if !found { 67 | nfail++ 68 | log.Printf(`! Answer not matched %s: 69 | expecting: %s 70 | got: %s 71 | `, rr.String(), exp, got) 72 | } 73 | } 74 | timeEnd := time.Now() 75 | 76 | fmt.Printf("= Total: %d\n", len(hostsFile.Records)) 77 | fmt.Printf("= Failed: %d\n", nfail) 78 | fmt.Printf("= Elapsed time: %v\n", timeEnd.Sub(timeStart)) 79 | } 80 | -------------------------------------------------------------------------------- /environment.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2018 M. Shulhan 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | package rescached 5 | 6 | import ( 7 | "fmt" 8 | "io" 9 | "path/filepath" 10 | "strconv" 11 | "strings" 12 | 13 | "git.sr.ht/~shulhan/pakakeh.go/lib/debug" 14 | "git.sr.ht/~shulhan/pakakeh.go/lib/dns" 15 | libhttp "git.sr.ht/~shulhan/pakakeh.go/lib/http" 16 | "git.sr.ht/~shulhan/pakakeh.go/lib/ini" 17 | "git.sr.ht/~shulhan/pakakeh.go/lib/memfs" 18 | libnet "git.sr.ht/~shulhan/pakakeh.go/lib/net" 19 | libstrings "git.sr.ht/~shulhan/pakakeh.go/lib/strings" 20 | ) 21 | 22 | const ( 23 | defListenAddress = "127.0.0.1:53" 24 | defWuiAddress = "127.0.0.1:5380" 25 | ) 26 | 27 | const ( 28 | sectionNameBlockd = "block.d" 29 | sectionNameDNS = "dns" 30 | sectionNameRescached = "rescached" 31 | 32 | subNameServer = "server" 33 | 34 | keyDebug = "debug" 35 | keyFileResolvConf = "file.resolvconf" 36 | keyName = "name" 37 | keyURL = `url` 38 | 39 | keyCachePruneDelay = "cache.prune_delay" 40 | keyCachePruneThreshold = "cache.prune_threshold" 41 | keyDohBehindProxy = "doh.behind_proxy" 42 | keyHTTPPort = "http.port" 43 | keyListen = "listen" 44 | keyParent = "parent" 45 | keyWUIListen = "wui.listen" 46 | keyTLSAllowInsecure = "tls.allow_insecure" 47 | keyTLSCertificate = "tls.certificate" 48 | keyTLSPort = "tls.port" 49 | keyTLSPrivateKey = "tls.private_key" 50 | 51 | dirBlock = "/etc/rescached/block.d" 52 | dirCaches = "/var/cache/rescached/" 53 | dirHosts = "/etc/rescached/hosts.d" 54 | dirZone = "/etc/rescached/zone.d" 55 | 56 | fileCaches = "rescached.gob" 57 | ) 58 | 59 | var ( 60 | mfsWww *memfs.MemFS 61 | ) 62 | 63 | // Environment for running rescached. 64 | type Environment struct { 65 | dirBase string 66 | pathDirBlock string 67 | pathDirCaches string 68 | pathDirHosts string 69 | pathDirZone string 70 | pathFileCaches string 71 | 72 | fileConfig string 73 | FileResolvConf string `ini:"rescached::file.resolvconf"` 74 | WUIListen string `ini:"rescached::wui.listen"` 75 | 76 | HostBlockd map[string]*Blockd `ini:"block.d"` 77 | hostBlockdFile map[string]*dns.HostsFile 78 | hostsd map[string]*dns.HostsFile 79 | zoned map[string]*dns.Zone 80 | 81 | // The options for WUI HTTP server. 82 | HttpdOptions libhttp.ServerOptions `json:"-"` 83 | 84 | dns.ServerOptions 85 | 86 | Debug int `ini:"rescached::debug"` 87 | } 88 | 89 | // LoadEnvironment initialize environment from configuration file. 90 | func LoadEnvironment(dirBase, fileConfig string) (env *Environment, err error) { 91 | var ( 92 | logp = "LoadEnvironment" 93 | cfg *ini.Ini 94 | ) 95 | 96 | env = newEnvironment(dirBase, fileConfig) 97 | 98 | if len(fileConfig) > 0 { 99 | cfg, err = ini.Open(env.fileConfig) 100 | if err != nil { 101 | return nil, fmt.Errorf(`%s: %q: %w`, logp, env.fileConfig, err) 102 | } 103 | 104 | err = cfg.Unmarshal(env) 105 | if err != nil { 106 | return nil, fmt.Errorf(`%s: %q: %w`, logp, env.fileConfig, err) 107 | } 108 | } 109 | 110 | _ = env.init() 111 | 112 | return env, nil 113 | } 114 | 115 | // newEnvironment create and initialize options with default values. 116 | func newEnvironment(dirBase, fileConfig string) *Environment { 117 | return &Environment{ 118 | hostsd: make(map[string]*dns.HostsFile), 119 | zoned: make(map[string]*dns.Zone), 120 | 121 | dirBase: dirBase, 122 | pathDirBlock: filepath.Join(dirBase, dirBlock), 123 | pathDirCaches: filepath.Join(dirBase, dirCaches), 124 | pathDirHosts: filepath.Join(dirBase, dirHosts), 125 | pathDirZone: filepath.Join(dirBase, dirZone), 126 | pathFileCaches: filepath.Join(dirBase, dirCaches, fileCaches), 127 | 128 | fileConfig: filepath.Join(dirBase, fileConfig), 129 | hostBlockdFile: make(map[string]*dns.HostsFile), 130 | } 131 | } 132 | 133 | // init check and initialize the environment instance with default values. 134 | func (env *Environment) init() (err error) { 135 | if len(env.WUIListen) == 0 { 136 | env.WUIListen = defWuiAddress 137 | } 138 | if len(env.ServerOptions.ListenAddress) == 0 { 139 | env.ServerOptions.ListenAddress = defListenAddress 140 | } 141 | if len(env.FileResolvConf) > 0 { 142 | _, _ = env.loadResolvConf() 143 | } 144 | 145 | debug.Value = env.Debug 146 | 147 | if env.HttpdOptions.Memfs == nil { 148 | env.HttpdOptions.Memfs = mfsWww 149 | } 150 | if len(env.HttpdOptions.Address) == 0 { 151 | env.HttpdOptions.Address = env.WUIListen 152 | } 153 | 154 | if env.HttpdOptions.Memfs == nil { 155 | memfsWwwOpts := &memfs.Options{ 156 | Root: defHTTPDRootDir, 157 | Includes: []string{ 158 | `.*\.css$`, 159 | `.*\.html$`, 160 | `.*\.js$`, 161 | `.*\.png$`, 162 | }, 163 | Embed: memfs.EmbedOptions{ 164 | CommentHeader: `// SPDX-FileCopyrightText: 2021 M. Shulhan 165 | // SPDX-License-Identifier: GPL-3.0-or-later 166 | `, 167 | PackageName: "rescached", 168 | VarName: "mfsWww", 169 | GoFileName: "memfs_generate.go", 170 | }, 171 | } 172 | env.HttpdOptions.Memfs, err = memfs.New(memfsWwwOpts) 173 | if err != nil { 174 | return fmt.Errorf("Environment.init: %w", err) 175 | } 176 | } 177 | 178 | return nil 179 | } 180 | 181 | func (env *Environment) initHostsBlock() { 182 | var ( 183 | hb *Blockd 184 | ) 185 | for _, hb = range env.HostBlockd { 186 | hb.init(env.pathDirBlock) 187 | } 188 | } 189 | 190 | func (env *Environment) loadResolvConf() (ok bool, err error) { 191 | rc, err := libnet.NewResolvConf(env.FileResolvConf) 192 | if err != nil { 193 | return false, err 194 | } 195 | 196 | if debug.Value > 0 { 197 | fmt.Printf("loadResolvConf: %+v\n", rc) 198 | } 199 | 200 | if len(rc.NameServers) == 0 { 201 | return false, nil 202 | } 203 | 204 | for x := 0; x < len(rc.NameServers); x++ { 205 | rc.NameServers[x] = "udp://" + rc.NameServers[x] 206 | } 207 | 208 | if libstrings.IsEqual(env.NameServers, rc.NameServers) { 209 | return false, nil 210 | } 211 | 212 | if len(env.NameServers) == 0 { 213 | env.NameServers = rc.NameServers 214 | } 215 | 216 | return true, nil 217 | } 218 | 219 | func (env *Environment) save(file string) (in *ini.Ini, err error) { 220 | var ( 221 | logp = "save" 222 | 223 | hb *Blockd 224 | vstr string 225 | ) 226 | 227 | if len(file) == 0 { 228 | in = &ini.Ini{} 229 | } else { 230 | in, err = ini.Open(file) 231 | if err != nil { 232 | return nil, fmt.Errorf("%s: %w", logp, err) 233 | } 234 | } 235 | 236 | in.Set(sectionNameRescached, "", keyFileResolvConf, env.FileResolvConf) 237 | in.Set(sectionNameRescached, "", keyDebug, strconv.Itoa(env.Debug)) 238 | in.Set(sectionNameRescached, "", keyWUIListen, strings.TrimSpace(env.WUIListen)) 239 | 240 | for _, hb = range env.HostBlockd { 241 | in.Set(sectionNameBlockd, hb.Name, keyName, hb.Name) 242 | in.Set(sectionNameBlockd, hb.Name, keyURL, hb.URL) 243 | } 244 | 245 | in.UnsetAll(sectionNameDNS, subNameServer, keyParent) 246 | for _, vstr = range env.NameServers { 247 | in.Add(sectionNameDNS, subNameServer, keyParent, vstr) 248 | } 249 | 250 | in.Set(sectionNameDNS, subNameServer, keyListen, env.ListenAddress) 251 | 252 | in.Set(sectionNameDNS, subNameServer, keyHTTPPort, strconv.Itoa(int(env.HTTPPort))) 253 | 254 | in.Set(sectionNameDNS, subNameServer, keyTLSPort, strconv.Itoa(int(env.TLSPort))) 255 | in.Set(sectionNameDNS, subNameServer, keyTLSCertificate, env.TLSCertFile) 256 | in.Set(sectionNameDNS, subNameServer, keyTLSPrivateKey, env.TLSPrivateKey) 257 | in.Set(sectionNameDNS, subNameServer, keyTLSAllowInsecure, strconv.FormatBool(env.TLSAllowInsecure)) 258 | in.Set(sectionNameDNS, subNameServer, keyDohBehindProxy, strconv.FormatBool(env.DoHBehindProxy)) 259 | 260 | in.Set(sectionNameDNS, subNameServer, keyCachePruneDelay, env.PruneDelay.String()) 261 | in.Set(sectionNameDNS, subNameServer, keyCachePruneThreshold, env.PruneThreshold.String()) 262 | 263 | return in, nil 264 | } 265 | 266 | // Write the configuration as ini format to Writer w. 267 | func (env *Environment) Write(w io.Writer) (err error) { 268 | if w == nil { 269 | return nil 270 | } 271 | 272 | var ( 273 | logp = "Environment.Write" 274 | outb []byte 275 | ) 276 | 277 | outb, err = ini.Marshal(env) 278 | if err != nil { 279 | return fmt.Errorf("%s: %w", logp, err) 280 | } 281 | 282 | _, err = w.Write(outb) 283 | if err != nil { 284 | return fmt.Errorf("%s: %w", logp, err) 285 | } 286 | 287 | return nil 288 | } 289 | 290 | // write the options values back to file. 291 | func (env *Environment) write(file string) (err error) { 292 | var ( 293 | in *ini.Ini 294 | ) 295 | in, err = env.save(file) 296 | if err != nil { 297 | return err 298 | } 299 | if len(file) > 0 { 300 | return in.Save(file) 301 | } 302 | return nil 303 | } 304 | -------------------------------------------------------------------------------- /environment_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2019 M. Shulhan 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | package rescached 5 | 6 | import ( 7 | "bytes" 8 | "os" 9 | "path/filepath" 10 | "testing" 11 | 12 | "git.sr.ht/~shulhan/pakakeh.go/lib/dns" 13 | libhttp "git.sr.ht/~shulhan/pakakeh.go/lib/http" 14 | "git.sr.ht/~shulhan/pakakeh.go/lib/ini" 15 | "git.sr.ht/~shulhan/pakakeh.go/lib/test" 16 | ) 17 | 18 | func TestEnvironment(t *testing.T) { 19 | cases := []struct { 20 | desc string 21 | content string 22 | exp *Environment 23 | expError string 24 | }{{ 25 | desc: "With empty content", 26 | exp: &Environment{}, 27 | }, { 28 | desc: "With multiple parents", 29 | content: `[dns "server"] 30 | listen = 127.0.0.1:53 31 | parent = udp://35.240.172.103 32 | parent = https://kilabit.info/dns-query 33 | `, 34 | exp: &Environment{ 35 | ServerOptions: dns.ServerOptions{ 36 | ListenAddress: "127.0.0.1:53", 37 | NameServers: []string{ 38 | "udp://35.240.172.103", 39 | "https://kilabit.info/dns-query", 40 | }, 41 | }, 42 | }, 43 | }} 44 | 45 | for _, c := range cases { 46 | t.Log(c.desc) 47 | 48 | got := &Environment{ 49 | ServerOptions: dns.ServerOptions{}, 50 | } 51 | 52 | err := ini.Unmarshal([]byte(c.content), got) 53 | if err != nil { 54 | test.Assert(t, "error", c.expError, err.Error()) 55 | continue 56 | } 57 | 58 | test.Assert(t, "environment", c.exp, got) 59 | } 60 | } 61 | 62 | func TestLoadEnvironment(t *testing.T) { 63 | var ( 64 | testDirBase = "_test" 65 | expEnv = &Environment{ 66 | dirBase: testDirBase, 67 | pathDirBlock: filepath.Join(testDirBase, dirBlock), 68 | pathDirCaches: filepath.Join(testDirBase, dirCaches), 69 | pathDirHosts: filepath.Join(testDirBase, dirHosts), 70 | pathDirZone: filepath.Join(testDirBase, dirZone), 71 | pathFileCaches: filepath.Join(testDirBase, dirCaches, fileCaches), 72 | 73 | fileConfig: filepath.Join(testDirBase, "/etc/rescached/rescached.cfg"), 74 | 75 | WUIListen: "127.0.0.1:5381", 76 | HostBlockd: map[string]*Blockd{ 77 | "a.block": &Blockd{ 78 | Name: "a.block", 79 | URL: "http://127.0.0.1:11180/hosts/a", 80 | }, 81 | "b.block": &Blockd{ 82 | Name: "b.block", 83 | URL: "http://127.0.0.1:11180/hosts/b", 84 | }, 85 | "c.block": &Blockd{ 86 | Name: "c.block", 87 | URL: "http://127.0.0.1:11180/hosts/c", 88 | }, 89 | }, 90 | HttpdOptions: libhttp.ServerOptions{ 91 | Address: "127.0.0.1:5381", 92 | }, 93 | ServerOptions: dns.ServerOptions{ 94 | ListenAddress: "127.0.0.1:5350", 95 | NameServers: []string{ 96 | "udp://10.8.0.1", 97 | }, 98 | TLSAllowInsecure: true, 99 | }, 100 | Debug: 1, 101 | } 102 | 103 | expBuffer []byte 104 | gotEnv *Environment 105 | gotBuffer bytes.Buffer 106 | err error 107 | ) 108 | 109 | expBuffer, err = os.ReadFile("testdata/rescached.cfg.test.out") 110 | if err != nil { 111 | t.Fatal(err) 112 | } 113 | 114 | gotEnv, err = LoadEnvironment(testDirBase, "/etc/rescached/rescached.cfg") 115 | if err != nil { 116 | t.Fatal(err) 117 | } 118 | 119 | gotEnv.HttpdOptions.Memfs = nil 120 | 121 | test.Assert(t, "LoadEnvironment", expEnv, gotEnv) 122 | 123 | gotEnv.HostBlockd["test"] = &Blockd{ 124 | Name: "test", 125 | URL: "http://someurl", 126 | } 127 | 128 | err = gotEnv.Write(&gotBuffer) 129 | if err != nil { 130 | t.Fatal(err) 131 | } 132 | 133 | test.Assert(t, "Write", string(expBuffer), gotBuffer.String()) 134 | } 135 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2018 M. Shulhan 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | module git.sr.ht/~shulhan/rescached 5 | 6 | go 1.22.0 7 | 8 | require ( 9 | git.sr.ht/~shulhan/ciigo v0.13.2 10 | git.sr.ht/~shulhan/pakakeh.go v0.57.0 11 | ) 12 | 13 | require ( 14 | git.sr.ht/~shulhan/asciidoctor-go v0.6.0 // indirect 15 | github.com/yuin/goldmark v1.7.4 // indirect 16 | github.com/yuin/goldmark-meta v1.1.0 // indirect 17 | golang.org/x/net v0.29.0 // indirect 18 | golang.org/x/sys v0.25.0 // indirect 19 | gopkg.in/yaml.v2 v2.4.0 // indirect 20 | ) 21 | 22 | // replace git.sr.ht/~shulhan/pakakeh.go => ../pakakeh.go 23 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | git.sr.ht/~shulhan/asciidoctor-go v0.6.0 h1:UepFox79vims2UqJGsQEoqLCrxhIHsk0YT2/H/fl+Oc= 2 | git.sr.ht/~shulhan/asciidoctor-go v0.6.0/go.mod h1:kUikWOI/WkTyRZrGfKDqevCplz40yOlQRynxAdMneAg= 3 | git.sr.ht/~shulhan/ciigo v0.13.2 h1:Apfj8Hj+sBgYcbm45PB5TDOMtvrtvkALPISCNVli9X4= 4 | git.sr.ht/~shulhan/ciigo v0.13.2/go.mod h1:iopkwqIQKSH2T05cmHI8FjJ1Rz4xbPBuBivEJzQmDgg= 5 | git.sr.ht/~shulhan/pakakeh.go v0.57.0 h1:4ReTu2KQqF7NPKgAVjXUTaHiu7tY/UXVosZYvHpUs9s= 6 | git.sr.ht/~shulhan/pakakeh.go v0.57.0/go.mod h1:+vUHUOSgUP0oG40gKb8YlQySSZHkywyL4eTA0v/OJWo= 7 | github.com/yuin/goldmark v1.7.4 h1:BDXOHExt+A7gwPCJgPIIq7ENvceR7we7rOS9TNoLZeg= 8 | github.com/yuin/goldmark v1.7.4/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= 9 | github.com/yuin/goldmark-meta v1.1.0 h1:pWw+JLHGZe8Rk0EGsMVssiNb/AaPMHfSRszZeUeiOUc= 10 | github.com/yuin/goldmark-meta v1.1.0/go.mod h1:U4spWENafuA7Zyg+Lj5RqK/MF+ovMYtBvXi1lBb2VP0= 11 | golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= 12 | golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= 13 | golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= 14 | golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 15 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 16 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 17 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 18 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 19 | -------------------------------------------------------------------------------- /internal/cmd/www/main.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2024 M. Shulhan 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | // Package www provides an HTTP server that serve the _www directory for 5 | // testing. 6 | // The web user interface can be run using existing rescached server by 7 | // setting the SERVER value in class Rescached (_www/rescached.js). 8 | package main 9 | 10 | import ( 11 | "flag" 12 | "log" 13 | 14 | "git.sr.ht/~shulhan/ciigo" 15 | "git.sr.ht/~shulhan/pakakeh.go/lib/memfs" 16 | ) 17 | 18 | func main() { 19 | var flagAddress string 20 | 21 | flag.StringVar(&flagAddress, `address`, `127.0.0.1:6200`, `Listen address`) 22 | 23 | flag.Parse() 24 | 25 | var serveOpts = ciigo.ServeOptions{ 26 | Mfs: &memfs.MemFS{ 27 | Opts: &memfs.Options{ 28 | Root: `./_www`, 29 | TryDirect: true, 30 | }, 31 | }, 32 | ConvertOptions: ciigo.ConvertOptions{ 33 | Root: `./_www`, 34 | HTMLTemplate: `./_www/doc/html.tmpl`, 35 | }, 36 | Address: flagAddress, 37 | IsDevelopment: true, 38 | } 39 | 40 | var err = ciigo.Serve(&serveOpts) 41 | if err != nil { 42 | log.Fatal(err) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /rescached.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2018 M. Shulhan 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | // Package rescached implement DNS forwarder with cache. 5 | package rescached 6 | 7 | import ( 8 | "errors" 9 | "fmt" 10 | "log" 11 | "os" 12 | "sync" 13 | 14 | "git.sr.ht/~shulhan/pakakeh.go/lib/debug" 15 | "git.sr.ht/~shulhan/pakakeh.go/lib/dns" 16 | "git.sr.ht/~shulhan/pakakeh.go/lib/http" 17 | "git.sr.ht/~shulhan/pakakeh.go/lib/memfs" 18 | ) 19 | 20 | // Version of program, overwritten by build. 21 | var Version = `4.4.3` 22 | 23 | // Server implement caching DNS server. 24 | type Server struct { 25 | dns *dns.Server 26 | env *Environment 27 | rcWatcher *memfs.Watcher 28 | 29 | httpd *http.Server 30 | httpdRunner sync.Once 31 | } 32 | 33 | // New create and initialize new rescached server. 34 | func New(env *Environment) (srv *Server, err error) { 35 | if debug.Value >= 1 { 36 | fmt.Printf("--- rescached: config: %+v\n", env) 37 | } 38 | 39 | err = env.init() 40 | if err != nil { 41 | return nil, fmt.Errorf("rescached: New: %w", err) 42 | } 43 | 44 | env.initHostsBlock() 45 | 46 | srv = &Server{ 47 | env: env, 48 | } 49 | 50 | err = srv.httpdInit() 51 | if err != nil { 52 | return nil, err 53 | } 54 | 55 | return srv, nil 56 | } 57 | 58 | // Start the server, waiting for DNS query from clients, read it and response 59 | // it. 60 | func (srv *Server) Start() (err error) { 61 | var ( 62 | logp = "Start" 63 | 64 | fcaches *os.File 65 | hb *Blockd 66 | hfile *dns.HostsFile 67 | zone *dns.Zone 68 | answers []*dns.Answer 69 | ) 70 | 71 | srv.dns, err = dns.NewServer(&srv.env.ServerOptions) 72 | if err != nil { 73 | return err 74 | } 75 | 76 | fcaches, err = os.Open(srv.env.pathFileCaches) 77 | if err == nil { 78 | // Load stored caches from file. 79 | answers, err = srv.dns.Caches.ExternalLoad(fcaches) 80 | if err != nil { 81 | log.Printf("%s: %s", logp, err) 82 | } else { 83 | fmt.Printf("%s: %d caches loaded from %s\n", logp, len(answers), srv.env.pathFileCaches) 84 | } 85 | 86 | err = fcaches.Close() 87 | if err != nil { 88 | log.Printf("%s: %s", logp, err) 89 | } 90 | } 91 | 92 | for _, hb = range srv.env.HostBlockd { 93 | if !hb.IsEnabled { 94 | continue 95 | } 96 | 97 | hfile, err = dns.ParseHostsFile(hb.file) 98 | if err != nil { 99 | return fmt.Errorf("%s: %w", logp, err) 100 | } 101 | 102 | err = srv.dns.Caches.InternalPopulateRecords(hfile.Records, hfile.Path) 103 | if err != nil { 104 | return fmt.Errorf("%s: %w", logp, err) 105 | } 106 | 107 | srv.env.hostBlockdFile[hfile.Name] = hfile 108 | } 109 | 110 | srv.env.hostsd, err = dns.LoadHostsDir(srv.env.pathDirHosts) 111 | if err != nil { 112 | if !errors.Is(err, os.ErrNotExist) { 113 | return err 114 | } 115 | err = os.MkdirAll(srv.env.pathDirHosts, 0700) 116 | if err != nil { 117 | return err 118 | } 119 | } 120 | 121 | for _, hfile = range srv.env.hostsd { 122 | err = srv.dns.Caches.InternalPopulateRecords(hfile.Records, hfile.Path) 123 | if err != nil { 124 | return err 125 | } 126 | } 127 | 128 | srv.env.zoned, err = dns.LoadZoneDir(srv.env.pathDirZone) 129 | if err != nil { 130 | if !errors.Is(err, os.ErrNotExist) { 131 | return err 132 | } 133 | err = os.MkdirAll(srv.env.pathDirZone, 0700) 134 | if err != nil { 135 | return err 136 | } 137 | } 138 | for _, zone = range srv.env.zoned { 139 | srv.dns.Caches.InternalPopulateZone(zone) 140 | } 141 | 142 | if len(srv.env.FileResolvConf) > 0 { 143 | go srv.watchResolvConf() 144 | } 145 | 146 | go func() { 147 | srv.httpdRunner.Do(srv.httpdRun) 148 | }() 149 | 150 | go srv.run() 151 | 152 | return nil 153 | } 154 | 155 | func (srv *Server) run() { 156 | defer func() { 157 | err := recover() 158 | if err != nil { 159 | log.Println("panic: ", err) 160 | } 161 | }() 162 | 163 | err := srv.dns.ListenAndServe() 164 | if err != nil { 165 | log.Println(err) 166 | } 167 | } 168 | 169 | // Stop the server. 170 | func (srv *Server) Stop() { 171 | var ( 172 | logp = "Stop" 173 | 174 | fcaches *os.File 175 | err error 176 | n int 177 | ) 178 | 179 | if srv.rcWatcher != nil { 180 | srv.rcWatcher.Stop() 181 | } 182 | srv.dns.Stop() 183 | 184 | // Stores caches to file for next start. 185 | err = os.MkdirAll(srv.env.pathDirCaches, 0700) 186 | if err != nil { 187 | log.Printf("%s: %s", logp, err) 188 | return 189 | } 190 | 191 | fcaches, err = os.Create(srv.env.pathFileCaches) 192 | if err != nil { 193 | log.Printf("%s: %s", logp, err) 194 | return 195 | } 196 | n, err = srv.dns.Caches.ExternalSave(fcaches) 197 | if err != nil { 198 | log.Printf("%s: %s", logp, err) 199 | // fall-through for Close. 200 | } 201 | err = fcaches.Close() 202 | if err != nil { 203 | log.Printf("%s: %s", logp, err) 204 | } 205 | fmt.Printf("%s: %d caches stored to %s\n", logp, n, srv.env.pathFileCaches) 206 | } 207 | 208 | // watchResolvConf watch an update to file resolv.conf. 209 | func (srv *Server) watchResolvConf() { 210 | var ( 211 | logp = "watchResolvConf" 212 | 213 | ns memfs.NodeState 214 | err error 215 | ) 216 | 217 | srv.rcWatcher, err = memfs.NewWatcher(srv.env.FileResolvConf, 0) 218 | if err != nil { 219 | log.Fatalf("%s: %s", logp, err) 220 | } 221 | 222 | for ns = range srv.rcWatcher.C { 223 | switch ns.State { 224 | case memfs.FileStateDeleted: 225 | log.Printf("= %s: file %q deleted\n", logp, srv.env.FileResolvConf) 226 | return 227 | default: 228 | ok, err := srv.env.loadResolvConf() 229 | if err != nil { 230 | log.Printf("%s: %s", logp, err) 231 | break 232 | } 233 | if !ok { 234 | break 235 | } 236 | 237 | srv.dns.RestartForwarders(srv.env.NameServers) 238 | } 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /rescached_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 M. Shulhan 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | package rescached 5 | 6 | import ( 7 | "log" 8 | "os" 9 | "testing" 10 | "time" 11 | 12 | libhttp "git.sr.ht/~shulhan/pakakeh.go/lib/http" 13 | ) 14 | 15 | const ( 16 | blockdServerAddress = "127.0.0.1:11180" 17 | ) 18 | 19 | var ( 20 | testEnv *Environment 21 | testServer *Server 22 | resc *Client 23 | ) 24 | 25 | func TestMain(m *testing.M) { 26 | var ( 27 | err error 28 | testStatus int 29 | x int 30 | ) 31 | 32 | go mockBlockdServer() 33 | 34 | testEnv, err = LoadEnvironment("_test", "/etc/rescached/rescached.cfg") 35 | if err != nil { 36 | log.Fatal(err) 37 | } 38 | 39 | testServer, err = New(testEnv) 40 | if err != nil { 41 | log.Fatal(err) 42 | } 43 | 44 | err = testServer.Start() 45 | if err != nil { 46 | log.Fatal(err) 47 | } 48 | 49 | resc = NewClient("http://"+testEnv.WUIListen, false) 50 | 51 | // Loop 10 times until server ready for testing. 52 | for x = 0; x < 10; x++ { 53 | time.Sleep(500) 54 | _, err = resc.Env() 55 | if err != nil { 56 | continue 57 | } 58 | break 59 | } 60 | 61 | testStatus = m.Run() 62 | testServer.Stop() 63 | os.Exit(testStatus) 64 | } 65 | 66 | func mockBlockdServer() { 67 | var ( 68 | serverOpts = libhttp.ServerOptions{ 69 | Address: blockdServerAddress, 70 | } 71 | epHostsA = libhttp.Endpoint{ 72 | Path: "/hosts/a", 73 | Method: libhttp.RequestMethodGet, 74 | RequestType: libhttp.RequestTypeNone, 75 | ResponseType: libhttp.ResponseTypePlain, 76 | Call: func(_ *libhttp.EndpointRequest) ([]byte, error) { 77 | return []byte("127.0.0.2 a.block\n"), nil 78 | }, 79 | } 80 | 81 | mockServer *libhttp.Server 82 | err error 83 | ) 84 | 85 | mockServer, err = libhttp.NewServer(serverOpts) 86 | if err != nil { 87 | log.Fatal(err) 88 | } 89 | 90 | err = mockServer.RegisterEndpoint(epHostsA) 91 | if err != nil { 92 | log.Fatal(err) 93 | } 94 | 95 | defer func() { 96 | err = mockServer.Stop(0) 97 | if err != nil { 98 | log.Fatal(err) 99 | } 100 | }() 101 | 102 | err = mockServer.Start() 103 | if err != nil { 104 | log.Println(err) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /testdata/hosts.d/hosts: -------------------------------------------------------------------------------- 1 | 127.0.0.1 1.test 2 | -------------------------------------------------------------------------------- /testdata/hosts.d/hosts.2: -------------------------------------------------------------------------------- 1 | 127.0.0.2 2.test 2 | -------------------------------------------------------------------------------- /testdata/master.d/test: -------------------------------------------------------------------------------- 1 | $ORIGIN x 2 | 3 | test A 127.0.0.3 4 | -------------------------------------------------------------------------------- /testdata/rescached.cfg: -------------------------------------------------------------------------------- 1 | [rescached] 2 | 3 | [dns "server"] 4 | 5 | ## 6 | ## parent:: List of parent DNS servers, separated by commas. 7 | ## 8 | ## Format:: , ... 9 | ## Default address:: 35.240.172.103 10 | ## Default port:: 53 11 | ## 12 | 13 | #parent=35.240.172.103 14 | 15 | ## 16 | ## listen:: Local IP address that rescached will listening for client 17 | ## request. 18 | ## 19 | ## Format:: : 20 | ## Default:: 127.0.0.1:53 21 | ## 22 | 23 | listen=127.0.0.1:5353 24 | 25 | ## Uncomment line below if you want to serve rescached to other computers. 26 | #listen=0.0.0.0:53 27 | 28 | ## 29 | ## cache.prune_delay:: Delay for pruning worker. 30 | ## Every N seconds/minutes/hours, rescached will traverse all caches and 31 | ## remove response that has not been accessed less than "cache.threshold". 32 | ## 33 | ## Format:: Duration with time unit. Valid time units are "s", "m", "h". 34 | ## Default:: 1h 35 | ## 36 | 37 | #cache.prune_delay = 1h 38 | 39 | ## 40 | ## cache.threshold:: The duration when the cache will be considered expired. 41 | ## 42 | ## Format:: Duration. Valid time units are "s", "m", "h". 43 | ## Default:: -1h 44 | ## 45 | 46 | #cache.threshold = -1h 47 | 48 | ## 49 | ## dir.hosts:: If its set, rescached will load all (host) files in path. 50 | ## if its empty, it will skip loading hosts files event in default location. 51 | ## 52 | ## Format : string. 53 | ## Default : /etc/rescached/hosts.d 54 | ## 55 | 56 | #dir.hosts=/etc/rescached/hosts.d 57 | 58 | ## 59 | ## debug:: If its not zero, rescached will print debugging information to 60 | ## standard output. Valid values are, 61 | ## 62 | ## 0 - log error. 63 | ## 1 - log startup, request, response, and exit status. 64 | ## 65 | ## Format:: Number. 66 | ## Default:: 0 67 | ## 68 | 69 | debug=1 70 | -------------------------------------------------------------------------------- /testdata/rescached.cfg.test.out: -------------------------------------------------------------------------------- 1 | [rescached] 2 | file.resolvconf = 3 | wui.listen = 127.0.0.1:5381 4 | debug = 1 5 | 6 | [block.d "a.block"] 7 | name = a.block 8 | url = http://127.0.0.1:11180/hosts/a 9 | 10 | [block.d "b.block"] 11 | name = b.block 12 | url = http://127.0.0.1:11180/hosts/b 13 | 14 | [block.d "c.block"] 15 | name = c.block 16 | url = http://127.0.0.1:11180/hosts/c 17 | 18 | [block.d "test"] 19 | name = test 20 | url = http://someurl 21 | 22 | [dns "server"] 23 | listen = 127.0.0.1:5350 24 | tls.certificate = 25 | tls.private_key = 26 | parent = udp://10.8.0.1 27 | http.idle_timeout = 0s 28 | cache.prune_delay = 0s 29 | cache.prune_threshold = 0s 30 | debug = 0 31 | http.port = 0 32 | tls.port = 0 33 | tls.allow_insecure = true 34 | doh.behind_proxy = false 35 | -------------------------------------------------------------------------------- /testdata/resolv.conf: -------------------------------------------------------------------------------- 1 | nameserver 192.168.1.1 2 | -------------------------------------------------------------------------------- /testdata/resolv.conf.empty: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shuLhan/rescached/2817846b12942dc0624c6b6ef7de6e608c41c3f7/testdata/resolv.conf.empty -------------------------------------------------------------------------------- /zone_record_request.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2022 M. Shulhan 2 | // SPDX-License-Identifier: GPL-3.0-or-later 3 | 4 | package rescached 5 | 6 | import ( 7 | "git.sr.ht/~shulhan/pakakeh.go/lib/dns" 8 | ) 9 | 10 | // zoneRecordRequest contains the request parameters for adding or deleting 11 | // record on zone.d. 12 | type zoneRecordRequest struct { 13 | Name string `json:"name"` 14 | Type string `json:"type"` 15 | Record string `json:"record"` 16 | recordRaw []byte 17 | rtype dns.RecordType 18 | } 19 | --------------------------------------------------------------------------------