Compare commits

..

284 Commits

Author SHA1 Message Date
Eugene Bujak
50d2c0a8d3 v0.9-hotfix1 2018-10-18 16:00:51 +03:00
Erik Rogers
4ad29ee65d Add Dockerfiles 2018-10-18 15:34:29 +03:00
Eugene Bujak
b2998d77f0 Merge pull request #88 in DNS/adguard-dns from bugfix/381 to master
* commit 'a528ed9f947d42f4324cd4f2263a015d34d7341f':
  Stop requiring current working directory to be the location of AdGuardHome.
2018-10-17 20:46:40 +03:00
Eugene Bujak
a528ed9f94 Stop requiring current working directory to be the location of AdGuardHome.
Fixes #381.
2018-10-17 20:43:26 +03:00
Eugene Bujak
a1bc008190 Merge pull request #87 in DNS/adguard-dns from feature/378 to master
* commit 'd3a6a8625406bfc452c545ed2f5eff6340bb86c5':
  Makefile -- add support for providing different GOPATH in command line.
  Do not use port 8618, it's a leftover from a time when we had two binaries.
2018-10-17 19:01:50 +03:00
Eugene Bujak
d3a6a86254 Makefile -- add support for providing different GOPATH in command line. 2018-10-17 18:57:47 +03:00
Eugene Bujak
5437a9d3a6 Do not use port 8618, it's a leftover from a time when we had two binaries.
Should fix 378 but needs testing from users having the problem since couldn't reproduce it here yet.
2018-10-17 18:55:27 +03:00
Ildar Kamalov
bdfb141d36 Fix logo 2018-10-17 14:44:42 +03:00
Andrey Meshkov
550dc3b129 fix gh language 2018-10-17 13:14:45 +03:00
Eugene Bujak
bacc465ebd README -- fix broken links, part deux. 2018-10-17 02:33:01 +03:00
Eugene Bujak
e606d63525 README -- fix broken links 2018-10-17 00:01:22 +03:00
Eugene Bujak
dbde07eea2 version.json -- point to actual files 2018-10-17 00:00:07 +03:00
Eugene Bujak
fc2f01f933 Release v0.9 2018-10-16 15:39:27 +03:00
Eugene Bujak
a267dbf625 Merge pull request #85 in DNS/adguard-dns from feature/361 to master
* commit 'c46fcce87ddc71c201079dba3df6267a80493a27':
  README -- Add whotracks.me to acknowledgments
  README -- add acknowledgements and fix release badge
2018-10-16 15:17:02 +03:00
Eugene Bujak
c46fcce87d README -- Add whotracks.me to acknowledgments 2018-10-15 22:21:42 +03:00
Eugene Bujak
af9c47c40a README -- add acknowledgements and fix release badge 2018-10-15 22:04:38 +03:00
Eugene Bujak
59323b2008 Merge pull request #84 in DNS/adguard-dns from avg_time_fix to master
* commit 'f0823f119573ac17f0bc1cacd10b527227b128c3':
  Fixup of previous commit.
2018-10-15 19:36:10 +03:00
Eugene Bujak
f0823f1195 Fixup of previous commit. 2018-10-15 19:34:31 +03:00
Eugene Bujak
dca9aebccb Merge pull request #83 in DNS/adguard-dns from avg_time_fix to master
* commit '1ed9faa0c22c1ae9ec2043c80e697133688cea40':
  Fix API returning wrong average request time
2018-10-15 19:31:33 +03:00
Eugene Bujak
1ed9faa0c2 Fix API returning wrong average request time 2018-10-15 19:30:10 +03:00
Andrey Meshkov
0d44e3ccdc Merge pull request #82 in DNS/adguard-dns from fix/update_readme to master
* commit 'cf9414c107a06de8b277d417387cd6a835c94b9d':
  Update the README
2018-10-15 17:29:04 +03:00
Andrey Meshkov
cf9414c107 Update the README 2018-10-15 17:20:57 +03:00
Eugene Bujak
47ce0f3e98 Merge pull request #81 in DNS/adguard-dns from hotfix to master
* commit '2e1acc2bac2b30219065afeeb66390d7f2b83f2c':
  Hotfix -- fix broken links in readme
2018-10-15 16:33:50 +03:00
Eugene Bujak
2e1acc2bac Hotfix -- fix broken links in readme 2018-10-15 16:32:38 +03:00
Eugene Bujak
96142b4164 Merge pull request #80 in DNS/adguard-dns from rename to master
* commit '1af11c4e45e13bcbeb9fe37f7867760f09ae8c2b':
  Name migration -- rename config from AdguardDNS.yaml to AdGuardHome.yaml
  Rename from 'Adguard DNS' to 'AdGuard Home'.
2018-10-15 16:15:37 +03:00
Eugene Bujak
1af11c4e45 Name migration -- rename config from AdguardDNS.yaml to AdGuardHome.yaml
It's done only if user didn't specify it in parameters and target filename doesn't exist yet.
2018-10-15 16:13:03 +03:00
Eugene Bujak
3e2a3afc52 Rename from 'Adguard DNS' to 'AdGuard Home'. 2018-10-15 16:02:19 +03:00
Andrey Meshkov
40d1b18b28 Merge pull request #79 in DNS/adguard-dns from fix/370 to master
* commit 'c2ba8de2062775decf09c94360d303e742e4ff44':
  Fix column max width
2018-10-15 15:06:20 +03:00
Eugene Bujak
a126a3868c Merge pull request #78 in DNS/adguard-dns from fix/368 to master
* commit 'aa691a068add3a29dd06b65d595b633750120eeb':
  Fix #368
2018-10-15 15:00:03 +03:00
Ildar Kamalov
c2ba8de206 Fix column max width 2018-10-15 14:57:36 +03:00
Andrey Meshkov
82269bcf33 Merge pull request #77 in DNS/adguard-dns from fix/365 to master
* commit 'bdaea88bf0b79d84da717393a447f71bd78e3cff':
  Add "FAQ" link to the header
  more trackers
  fix url
  tooltip
  capitalize category name
  fix gitignore
  remove extra file
  Add trackers, rework some text
  Fix some UI issues, rename DNS->Home
2018-10-15 14:46:18 +03:00
Andrey Meshkov
aa691a068a Fix #368 2018-10-15 14:31:21 +03:00
Ildar Kamalov
bdaea88bf0 Add "FAQ" link to the header
Closes #370
2018-10-15 13:49:07 +03:00
Andrey Meshkov
8c11449d23 more trackers 2018-10-15 00:08:38 +03:00
Andrey Meshkov
0e04954673 fix url 2018-10-15 00:00:18 +03:00
Andrey Meshkov
1059669b57 tooltip 2018-10-14 23:50:13 +03:00
Andrey Meshkov
c9736ec0fa capitalize category name 2018-10-14 23:42:25 +03:00
Andrey Meshkov
dcbee729fb fix gitignore 2018-10-14 23:26:00 +03:00
Andrey Meshkov
c35f260e53 remove extra file 2018-10-14 23:25:40 +03:00
Andrey Meshkov
2f61b42e90 Add trackers, rework some text 2018-10-14 23:24:11 +03:00
Andrey Meshkov
e67695df8b Fix some UI issues, rename DNS->Home 2018-10-14 17:49:07 +03:00
Andrey Meshkov
e356540872 fix crash 2018-10-12 20:36:57 +03:00
Andrey Meshkov
3d01c3512e Merge pull request #76 in DNS/adguard-dns from single_binary to master
* commit '880ad362a849cc53729128b614a4d4cc1d696750':
  Makefile -- use variable for target binary
  coredns plugin -- remove debug logging
  single binary -- coredns also tries to parse arguments, it kills itself on unknown flags
  Update makefile to build only one binary instead of two
  WIP -- single binary -- works, replies to DNS, but need to check what got broken
2018-10-12 19:58:33 +03:00
Andrey Meshkov
49567219dc Merge pull request #74 in DNS/adguard-dns from feature/363 to master
* commit 'd2e5692694e4537038434b56020f4e6c9e782955':
  Remove debug logging added by previous commit.
  If running from terminal, ask for username/password if config file does not exists
2018-10-12 19:56:41 +03:00
Eugene Bujak
3f85625dc6 Merge pull request #75 in DNS/adguard-dns from feature/364 to master
* commit 'd0d98ba762e9482f91a385488d22865ea00ff2a5':
  Add whotracksme info popover
  trackers module
  Added a script for updating the whotracksme database
2018-10-12 19:56:33 +03:00
Ildar Kamalov
d0d98ba762 Add whotracksme info popover
Closes #364
2018-10-12 19:52:19 +03:00
Eugene Bujak
d2e5692694 Remove debug logging added by previous commit.
Closes #363.
2018-10-12 19:49:17 +03:00
Eugene Bujak
9d030f38b7 If running from terminal, ask for username/password if config file does not exists 2018-10-12 19:43:09 +03:00
Andrey Meshkov
5fb603f6c9 trackers module 2018-10-12 17:32:38 +03:00
Andrey Meshkov
11e8853a34 Added a script for updating the whotracksme database 2018-10-12 17:32:38 +03:00
Eugene Bujak
880ad362a8 Makefile -- use variable for target binary 2018-10-12 17:11:57 +03:00
Eugene Bujak
5192e95a0d coredns plugin -- remove debug logging 2018-10-12 17:11:57 +03:00
Eugene Bujak
ac6e0add31 single binary -- coredns also tries to parse arguments, it kills itself on unknown flags 2018-10-12 17:11:57 +03:00
Eugene Bujak
7ff89baf45 Update makefile to build only one binary instead of two 2018-10-12 17:11:57 +03:00
Eugene Bujak
bad88961e9 WIP -- single binary -- works, replies to DNS, but need to check what got broken 2018-10-12 17:11:57 +03:00
Andrey Meshkov
838406353b Merge pull request #73 in DNS/adguard-dns from fix/362 to master
* commit '1cdbe3f879566f8e08ad0fa88daad5b568750820':
  Fix sorting issue and show loader
2018-10-12 17:05:58 +03:00
Ildar Kamalov
1cdbe3f879 Fix sorting issue and show loader
Closes #362
2018-10-12 16:58:48 +03:00
Andrey Meshkov
47a9c6555e Merge pull request #72 in DNS/adguard-dns from feature/360 to master
* commit '35368619b0d7ed0fb838d749cd7b2f63ae9c9aa3':
  Fix footer links
  Add progress bar to the stats tables
  Add new logo
  Replace the main Statistics graph with 4 blocks instead
  Clean static folder on build
2018-10-12 16:38:03 +03:00
Ildar Kamalov
35368619b0 Fix footer links
Closes #360
2018-10-12 16:27:59 +03:00
Ildar Kamalov
6ca881ee86 Add progress bar to the stats tables 2018-10-12 16:02:01 +03:00
Ildar Kamalov
1233901822 Add new logo 2018-10-12 15:23:57 +03:00
Ildar Kamalov
bc11f872fa Replace the main Statistics graph with 4 blocks instead 2018-10-12 15:23:21 +03:00
Ildar Kamalov
599426a1f9 Clean static folder on build 2018-10-12 15:20:59 +03:00
Eugene Bujak
fb2d90832c Merge pull request #68 in DNS/adguard-dns from feature/357 to master
* commit '8d13770b0d4b6d8fde0a61bbfdaa7bf9817142f6':
  Remove unneeded debug prints
  API filtering/add_url -- accept JSON instead of name=value lines
  Remove unused module
  Send json for addFilter request
  Fix params
  Add name field to the filter subscription dialog
2018-10-11 18:35:30 +03:00
Eugene Bujak
8d13770b0d Remove unneeded debug prints 2018-10-11 18:33:56 +03:00
Eugene Bujak
751be05a31 API filtering/add_url -- accept JSON instead of name=value lines 2018-10-11 18:33:56 +03:00
Ildar Kamalov
33958c5a25 Remove unused module 2018-10-11 18:33:56 +03:00
Ildar Kamalov
1e18235c1d Send json for addFilter request 2018-10-11 18:33:56 +03:00
Ildar Kamalov
4b8ee9ce83 Fix params 2018-10-11 18:33:56 +03:00
Ildar Kamalov
5be66e7dc7 Add name field to the filter subscription dialog 2018-10-11 18:33:56 +03:00
Eugene Bujak
8cf898e8d9 Merge pull request #70 in DNS/adguard-dns from hotfix to master
* commit '4995c1a1a8411445390a77235f2ebfbc97b12ff3':
  Hotfix -- fix querylog verifier that got broken by 5ae2a32d6e
2018-10-11 18:12:28 +03:00
Eugene Bujak
4995c1a1a8 Hotfix -- fix querylog verifier that got broken by 5ae2a32d6e 2018-10-11 18:06:33 +03:00
Eugene Bujak
557c2268dc Merge pull request #67 in DNS/adguard-dns from feature/333 to master
* commit '383f1c2fb38437e682ec9fc6623672730dae4581':
  Hide badge if core is not running
  Add client requests for toggle protection
  API backend -- implement ability to turn toggle all protection in one go, helpful to temporarily disable all kinds of filtering
2018-10-11 16:23:01 +03:00
Eugene Bujak
aa7b99d78c Merge pull request #69 in DNS/adguard-dns from fix/blocked_ttl_10 to master
* commit 'e7b6ab47501cf14d3a7b5cf212c88bde0c6604d7':
  Change blocked ttl to 10 sec, we don't need it to be large in a home network
2018-10-11 15:11:43 +03:00
Andrey Meshkov
e7b6ab4750 Change blocked ttl to 10 sec, we don't need it to be large in a home network 2018-10-11 13:24:06 +03:00
Ildar Kamalov
383f1c2fb3 Hide badge if core is not running 2018-10-11 11:08:07 +03:00
Ildar Kamalov
3a74dfdfa4 Add client requests for toggle protection
Closes #333
2018-10-11 10:57:36 +03:00
Eugene Bujak
413228e6ec API backend -- implement ability to turn toggle all protection in one go, helpful to temporarily disable all kinds of filtering 2018-10-10 20:13:03 +03:00
Eugene Bujak
c3df81bb8d Merge pull request #66 in DNS/adguard-dns from bugfix/356 to master
* commit 'e689c7d940e9a20bc13f024e18b86f3c1e5ba759':
  Do not lose filter name when saving to yaml
  coredns querylog -- since we read entire querylog json once at startup, fill querylog cache from it and then rotate it on each incoming DNS query
  Cache DNS lookups when resolving safebrowsing or parental servers, also cache replacement hostnames as well.
2018-10-10 19:50:20 +03:00
Eugene Bujak
e689c7d940 Do not lose filter name when saving to yaml 2018-10-10 19:49:18 +03:00
Eugene Bujak
5ae2a32d6e coredns querylog -- since we read entire querylog json once at startup, fill querylog cache from it and then rotate it on each incoming DNS query 2018-10-10 19:44:07 +03:00
Eugene Bujak
a5d1053520 Cache DNS lookups when resolving safebrowsing or parental servers, also cache replacement hostnames as well. 2018-10-10 19:10:38 +03:00
Eugene Bujak
e2295c1a11 Merge pull request #65 in DNS/adguard-dns from fix/strings to master
* commit 'd591ea6264bb287a6e57f815a0bc75b7b920bb87':
  Makefile -- run npm build whenever any .js file changes inside client/
  fix strings
  Fix strings
  Fix strings
2018-10-10 17:57:36 +03:00
Eugene Bujak
d591ea6264 Makefile -- run npm build whenever any .js file changes inside client/ 2018-10-10 17:56:48 +03:00
Andrey Meshkov
ee8759f063 tMerge branch 'master' into fix/strings 2018-10-10 17:55:03 +03:00
Andrey Meshkov
151944bc27 fix strings 2018-10-10 17:54:21 +03:00
Andrey Meshkov
a6172d1966 Fix strings 2018-10-10 17:32:36 +03:00
Andrey Meshkov
90bef94500 Fix strings 2018-10-10 17:24:12 +03:00
Eugene Bujak
f5deff63ba Merge pull request #64 in DNS/adguard-dns from remove_old_stats to master
* commit '903b20dcab855f0dec7af3b305e8362fa4255b8a':
  Remove dead code
2018-10-10 16:19:30 +03:00
Eugene Bujak
903b20dcab Remove dead code 2018-10-10 15:47:08 +03:00
Eugene Bujak
945bd24f67 Merge pull request #63 in DNS/adguard-dns from hotfix to master
* commit 'ae9964c445e200d22b159a47bfc6c00af990653e':
  Makefile -- fix build failure on systems where /bin/sh is not alias to /bin/bash
2018-10-10 01:26:21 +03:00
Eugene Bujak
ae9964c445 Makefile -- fix build failure on systems where /bin/sh is not alias to /bin/bash 2018-10-10 01:23:32 +03:00
Andrey Meshkov
3a5ecb9fc1 Merge pull request #62 in DNS/adguard-dns from hotfix to master
* commit 'c499c435c3c91960abd695b4919554937a3df5d1':
  Fixup of previous merge -- fix build failure.
2018-10-10 01:13:38 +03:00
Eugene Bujak
c499c435c3 Fixup of previous merge -- fix build failure. 2018-10-10 01:13:00 +03:00
Andrey Meshkov
2113bb5436 Merge pull request #60 in DNS/adguard-dns from feature/342 to master
* commit '8503f76747d47160d5d8239e275a7d6d8965dacd':
  Add default disabled hosts filters.
  Change default filter URL to github-hosted version
2018-10-10 01:05:58 +03:00
Eugene Bujak
8503f76747 Add default disabled hosts filters. 2018-10-10 00:59:37 +03:00
Eugene Bujak
c2be5917ef Change default filter URL to github-hosted version 2018-10-10 00:44:39 +03:00
Eugene Bujak
a54984f688 Merge pull request #58 in DNS/adguard-dns from feature/348 to master
* commit '5533b434dac98ba610028ca98327992d18078da0':
  coredns plugin -- give out to browser last entries from querylog file, not first
  Update .gitignore to ignore non-gzipped querylog
  Makefile -- Fix bug introduced by 93c451cb0c
  coredns plugin -- Increase querylog given out to web UI from 1000 to 5000.
2018-10-10 00:35:58 +03:00
Eugene Bujak
5533b434da coredns plugin -- give out to browser last entries from querylog file, not first 2018-10-10 00:23:15 +03:00
Eugene Bujak
4984c55bce Update .gitignore to ignore non-gzipped querylog 2018-10-10 00:05:48 +03:00
Eugene Bujak
9b489c8ddb Makefile -- Fix bug introduced by 93c451cb0c
make would always run webpack, even if output was generated already.
2018-10-10 00:05:48 +03:00
Eugene Bujak
eb5f66ad9e coredns plugin -- Increase querylog given out to web UI from 1000 to 5000. 2018-10-09 22:53:19 +03:00
Eugene Bujak
75d74a017b Merge pull request #56 in DNS/adguard-dns from feature/348 to master
* commit 'ca794aed6344cf0f5570bb359c731a96ca90739f':
  querylog file -- disable gzip compression
  Implement online stats calculation in coredns plugin instead of scraping prometheus.
2018-10-09 21:16:03 +03:00
Eugene Bujak
93c451cb0c Merge pull request #57 in DNS/adguard-dns from feature/354 to master
* commit '0545aeff3fb8368238e8c61d43589ef8b4a6d8e8':
  Fix variable
  Add hash to the static JS/CSS
  Fix tooltip width
  Fix default filtering for query log
  Fix textarea width
2018-10-09 21:15:26 +03:00
Ildar Kamalov
0545aeff3f Fix variable 2018-10-09 11:00:48 +03:00
Ildar Kamalov
814005021c Add hash to the static JS/CSS
Closes #354
2018-10-09 10:25:21 +03:00
Eugene Bujak
ca794aed63 querylog file -- disable gzip compression 2018-10-09 05:02:16 +03:00
Eugene Bujak
37f6d38c49 Implement online stats calculation in coredns plugin instead of scraping prometheus. 2018-10-09 04:45:05 +03:00
Konstantin 🦄 Zamyakin
165722585f Merge pull request #55 in DNS/adguard-dns from hotfix to master
* commit '7dea729656fe60aefcd81b7c1b808866d3c334a7':
  Fix build failure of coredns plugin introduced by previous merge
2018-10-08 21:30:47 +03:00
Eugene Bujak
7dea729656 Fix build failure of coredns plugin introduced by previous merge 2018-10-08 20:35:22 +03:00
Eugene Bujak
16b1a343a0 Merge pull request #54 in DNS/adguard-dns from feature/348 to master
* commit 'a15f21ca1cd0acabf77a4e9750dd77b2b870a6f4':
  code review -- move constants into named constants
  coredns plugin -- Cache /querylog API result
  coredns plugin -- Final fix for deadlock during coredns reload
  coredns plugin -- change rlock to lock when loading top stats to avoid doing it in parallel
  coredns plugin -- Fix deadlock during coredns reload
  stats -- Clamp number of rotations to sane value and prevent from going into (very long) loop
  querylog API -- when manually generating json, don't forget to escape strings
  coredns plugin -- don't reload from querylog on SIGUSR, we already have it in memory
  Fix some lint warnings
  coredns plugin -- Calculate top for domains, clients and blocked both from querylog and running requests.
  Fix more race conditions found by race detector
  Querylog -- Omit empty fields when writing json
  Querylog -- Read from querylog files when answering to /querylog API, it now survives restarts.
  querylog -- Add querylog files to gitignore
  if coredns unexpectedly quits, restart it
  Fix race conditions found by go's race detector
  Querylog -- Implement file writing and update /querylog handler for changed structures.
2018-10-08 20:15:06 +03:00
Eugene Bujak
a15f21ca1c code review -- move constants into named constants 2018-10-08 20:04:36 +03:00
Eugene Bujak
a15c59e24e coredns plugin -- Cache /querylog API result 2018-10-08 19:51:43 +03:00
Ildar Kamalov
5718f55b9a Fix tooltip width 2018-10-08 18:55:30 +03:00
Ildar Kamalov
6de0871f2c Fix default filtering for query log 2018-10-08 18:44:12 +03:00
Ildar Kamalov
6a90efe957 Fix textarea width 2018-10-08 18:42:55 +03:00
Eugene Bujak
763dcc46e9 coredns plugin -- Final fix for deadlock during coredns reload 2018-10-08 17:49:08 +03:00
Eugene Bujak
3109529dbb coredns plugin -- change rlock to lock when loading top stats to avoid doing it in parallel 2018-10-08 17:14:11 +03:00
Eugene Bujak
2c84cd6448 coredns plugin -- Fix deadlock during coredns reload 2018-10-08 17:07:45 +03:00
Eugene Bujak
0440ef016a stats -- Clamp number of rotations to sane value and prevent from going into (very long) loop 2018-10-08 05:55:33 +03:00
Eugene Bujak
182fa37e5f querylog API -- when manually generating json, don't forget to escape strings 2018-10-08 05:07:02 +03:00
Eugene Bujak
ea1125f57d coredns plugin -- don't reload from querylog on SIGUSR, we already have it in memory 2018-10-08 04:24:37 +03:00
Eugene Bujak
4ecb84f9ad Fix some lint warnings 2018-10-07 23:43:24 +03:00
Eugene Bujak
a2434d4574 coredns plugin -- Calculate top for domains, clients and blocked both from querylog and running requests.
This moves the functionality from frontend to coredns plugin.
2018-10-07 23:42:17 +03:00
Eugene Bujak
3b1faa1365 Fix more race conditions found by race detector 2018-10-07 21:24:22 +03:00
Eugene Bujak
dc1042c3e9 Querylog -- Omit empty fields when writing json 2018-10-07 02:21:47 +03:00
Eugene Bujak
a63fe958ae Querylog -- Read from querylog files when answering to /querylog API, it now survives restarts. 2018-10-07 02:21:33 +03:00
Eugene Bujak
0ee112e8a0 querylog -- Add querylog files to gitignore 2018-10-07 02:21:27 +03:00
Eugene Bujak
656d092ad6 if coredns unexpectedly quits, restart it 2018-10-07 02:21:27 +03:00
Eugene Bujak
2244c21b76 Fix race conditions found by go's race detector 2018-10-07 02:21:27 +03:00
Eugene Bujak
2c33905a79 Querylog -- Implement file writing and update /querylog handler for changed structures. 2018-10-07 02:21:12 +03:00
Eugene Bujak
16fd1359cd Merge pull request #53 in DNS/adguard-dns from bugfix/shoult_not_happen_spam to master
* commit '3a7a80f15f0180836077b4f63e504a659133adbb':
  coredns plugin -- fix SHOULD NOT HAPPEN spam when incoming request is for root servers
  Makefile -- update pprof plugin to survive coredns reloads
2018-10-05 17:12:24 +03:00
Eugene Bujak
3a7a80f15f coredns plugin -- fix SHOULD NOT HAPPEN spam when incoming request is for root servers 2018-10-05 07:36:03 +03:00
Eugene Bujak
5b9a5fff97 Makefile -- update pprof plugin to survive coredns reloads 2018-10-05 07:25:44 +03:00
Eugene Bujak
3f8450337f Merge pull request #52 in DNS/adguard-dns from feature/persistent-stats to master
* commit '19e76b693803220dffcd0a1fb1fe1e654309a11a':
  Add API call to reset stats
  Periodically flush stats.json
  Web UI -- persistent stats by writing them into stats.json at exit
2018-10-04 14:53:05 +03:00
Eugene Bujak
19e76b6938 Add API call to reset stats 2018-10-04 14:29:17 +03:00
Eugene Bujak
856e26edcf Periodically flush stats.json 2018-10-04 14:29:17 +03:00
Eugene Bujak
51ec58b0ce Web UI -- persistent stats by writing them into stats.json at exit 2018-10-04 14:29:16 +03:00
Eugene Bujak
c6eabb5b67 Merge pull request #51 in DNS/adguard-dns from feature/regexp_leak to master
* commit '1cc1e3749df6ccefb741232d7949fd5893d84f66':
  dnsfilter -- lazily initialize safebrowsing and parental lookup cache
  dnsfilter -- avoid using regexps when simple suffix match is enough.
2018-10-04 13:52:31 +03:00
Eugene Bujak
1cc1e3749d dnsfilter -- lazily initialize safebrowsing and parental lookup cache 2018-10-04 13:38:52 +03:00
Eugene Bujak
cb97a254a5 dnsfilter -- avoid using regexps when simple suffix match is enough.
This covers 96.98% of all adguard dns rules.
2018-10-04 13:19:43 +03:00
Eugene Bujak
9e939e5754 Merge pull request #49 in DNS/adguard-dns from features/memleak-test to master
* commit '3aac7e7bc9b4bb3ecff697b7748499a14bc64a0d':
  Add a test to demonstrate huge memory usage due from having too many regexps
2018-10-04 12:51:09 +03:00
Eugene Bujak
b72d6f68e6 Merge pull request #47 in DNS/adguard-dns from feature/349 to master
* commit '57ade2c3c3804d24857a45a8ab31c10534154dc7':
  Increase querylog size from 1000 to 10000 -- that'll use 32MB of memory.
  Web UI API -- Give out 24-hour stat instead of last 3 minutes.
2018-10-04 12:16:43 +03:00
Eugene Bujak
3aac7e7bc9 Add a test to demonstrate huge memory usage due from having too many regexps 2018-10-04 02:06:23 +03:00
Eugene Bujak
57ade2c3c3 Increase querylog size from 1000 to 10000 -- that'll use 32MB of memory. 2018-10-03 22:44:57 +03:00
Eugene Bujak
7d7360c700 Web UI API -- Give out 24-hour stat instead of last 3 minutes. 2018-10-03 22:44:50 +03:00
Ildar Kamalov
8c76e17b1b Merge pull request #46 in DNS/adguard-dns from feature/332 to master
* commit '991574f236ba691548839104a4218d749fbef10a':
  Fix row original
  Add query log filtering
2018-10-03 12:38:14 +03:00
Ildar Kamalov
991574f236 Fix row original 2018-10-02 18:30:34 +03:00
Ildar Kamalov
d7596fe860 Add query log filtering
Closes #322
2018-10-02 18:14:41 +03:00
Eugene Bujak
0c3c8dba9b Merge pull request #43 in DNS/adguard-dns from feature/341 to master
* commit 'e20bfe9d08d6c60c8f37ec49dcda2f446bdf0ce5':
  Replace line endings on save
  Add "block" and "unblock" buttons to the Query Log
2018-09-28 20:07:38 +03:00
Eugene Bujak
04e9f74435 Merge pull request #45 in DNS/adguard-dns from less-chatty to master
* commit '7b7f7138806b0b743d4fb1c4fef3c40f513be8b4':
  Be less noisy during long periods of time
2018-09-28 20:04:25 +03:00
Eugene Bujak
7b7f713880 Be less noisy during long periods of time 2018-09-28 18:08:26 +03:00
Ildar Kamalov
e20bfe9d08 Replace line endings on save 2018-09-28 17:47:34 +03:00
Ildar Kamalov
c40f7b4d5c Add "block" and "unblock" buttons to the Query Log 2018-09-28 16:30:52 +03:00
Eugene Bujak
d7039d9222 Merge pull request #42 in DNS/adguard-dns from feature/344 to master
* commit '2c720350006f607958540a672d2fa4cf927010bb':
  Add list of upstream servers
2018-09-26 18:55:28 +03:00
Eugene Bujak
3282a45978 Merge pull request #41 in DNS/adguard-dns from feature/346 to master
* commit '98994916b58faddb210b0776bdd7b5b6de43a8dc':
  Code review request -- set safebrowsing default to disabled
  web backend -- generate corefile with blocked_ttl config parameter
  coredns plugin -- Add option "blocked_ttl" that can change default nxdomain response TTL
  Makefile -- avoid stale copy of this repo inside build/gopath
2018-09-26 18:52:30 +03:00
Eugene Bujak
98994916b5 Code review request -- set safebrowsing default to disabled 2018-09-26 18:41:45 +03:00
Eugene Bujak
f1ae5d78d2 web backend -- generate corefile with blocked_ttl config parameter
Closes #346.
2018-09-26 18:38:35 +03:00
Ildar Kamalov
2c72035000 Add list of upstream servers
Closes #344
2018-09-26 18:38:06 +03:00
Eugene Bujak
c7790a8d9f coredns plugin -- Add option "blocked_ttl" that can change default nxdomain response TTL 2018-09-26 18:38:06 +03:00
Eugene Bujak
c9e10c9de7 Makefile -- avoid stale copy of this repo inside build/gopath 2018-09-26 18:38:06 +03:00
Eugene Bujak
de7b2d5e6b Merge pull request #40 in DNS/adguard-dns from feature/347 to master
* commit 'ff86d6b7dc31e463651c11f02330630e35676e05':
  Set default servers to tls://1.1.1.1 and tls://1.0.0.1
2018-09-26 18:00:34 +03:00
Eugene Bujak
ff86d6b7dc Set default servers to tls://1.1.1.1 and tls://1.0.0.1
Also add support for tls:// in webUI API
2018-09-26 17:47:23 +03:00
Eugene Bujak
3afd8fccc7 Merge pull request #39 in DNS/adguard-dns from feature/333 to master
* commit '2cf22898dd1418d1659340a95c94c8c9a6a7cf04':
  Add button to the dashboard page for enable/disable filtering
2018-09-26 17:24:30 +03:00
Ildar Kamalov
2cf22898dd Add button to the dashboard page for enable/disable filtering
Closes #333
2018-09-26 17:12:31 +03:00
Eugene Bujak
381b96a4b1 Merge pull request #38 in DNS/adguard-dns from bugfix/344 to master
* commit 'a65a40c6beb00176f46e7187ba0c4b678b17f6d8':
  Update /status to return currently set upstream DNS servers.
  web UI -- Fix engrish when checking upstream DNS servers succeeds
2018-09-25 20:54:35 +03:00
Eugene Bujak
a65a40c6be Update /status to return currently set upstream DNS servers. 2018-09-25 19:53:36 +03:00
Eugene Bujak
da62fac76e web UI -- Fix engrish when checking upstream DNS servers succeeds 2018-09-25 19:52:50 +03:00
Eugene Bujak
6a53dd0f00 Merge pull request #37 in DNS/adguard-dns from bugfix/333 to master
* commit '09a39cce03f69b1f9801e66d763d7b5208411336':
  Allow disabling of filtering but keeping querylog, safebrowsing, safesearch and parental working.
  Makefile -- make it a bit less noisy during build and much less noisy during clean
2018-09-25 19:44:07 +03:00
Eugene Bujak
09a39cce03 Allow disabling of filtering but keeping querylog, safebrowsing, safesearch and parental working. 2018-09-25 19:26:26 +03:00
Eugene Bujak
50b188a086 Makefile -- make it a bit less noisy during build and much less noisy during clean 2018-09-25 19:25:54 +03:00
Eugene Bujak
dd8396cec1 Merge pull request #36 in DNS/adguard-dns from bugfix/343 to master
* commit 'ea320f5ee35fda7744e86a1bd77b948e534eeb1e':
  Fix test failures introduced by previous commit afd1fe21f6.
2018-09-25 19:14:59 +03:00
Eugene Bujak
ea320f5ee3 Fix test failures introduced by previous commit afd1fe21f6. 2018-09-25 19:12:50 +03:00
Eugene Bujak
afd1fe21f6 Merge pull request #35 in DNS/adguard-dns from bugfix/343 to master
* commit '119d38fa8e8b5c5193fe20ad215a6daac833354b':
  Add trace() for debugging
  coredns -- don't try to be smart and replace 127.0.0.1 with NXDOMAIN yet -- need research on that first
  Fix 'index out of range' panic when adding a filter URL that has empty line in contents
  web UI -- Fix description of hosts rule syntax, it's other way around
2018-09-25 18:44:41 +03:00
Eugene Bujak
119d38fa8e Add trace() for debugging 2018-09-25 18:34:34 +03:00
Eugene Bujak
620212ad37 coredns -- don't try to be smart and replace 127.0.0.1 with NXDOMAIN yet -- need research on that first 2018-09-25 18:34:01 +03:00
Eugene Bujak
bd0fa4cc4f Fix 'index out of range' panic when adding a filter URL that has empty line in contents 2018-09-25 18:23:02 +03:00
Eugene Bujak
b0549a8e5b web UI -- Fix description of hosts rule syntax, it's other way around 2018-09-25 18:22:41 +03:00
Eugene Bujak
92399b8ebf Merge pull request #34 in DNS/adguard-dns from better-builds to master
* commit 'd8fbb2cd3b688b3890d60bc3923db50696dd9d59':
  Remove leftover from old internal repo
  Rewrite Makefile
2018-09-21 20:16:56 +03:00
Eugene Bujak
d8fbb2cd3b Remove leftover from old internal repo 2018-09-21 20:06:33 +03:00
Eugene Bujak
469b93eaa4 Rewrite Makefile
* fixes building outdated coredns plugin from inside GOPATH
 * make clean now cleans all build output, including node_modules and webpack output
 * smarter invocation of `npm install` -- only if package.json or package-lock.json changed
 * use separate gopath because coredns build system requires custom checkout of prometheus dependency
2018-09-21 20:01:55 +03:00
Ildar Kamalov
92b681cb41 Merge pull request #33 in DNS/adguard-dns from feature/321 to master
* commit '1c1b952d485e572eb2320b641429e07757b2d65f':
  Fix message checking
  Check upstream length in component
  Add a test upstreams button
2018-09-21 19:06:25 +03:00
Ildar Kamalov
1c1b952d48 Fix message checking 2018-09-21 18:57:27 +03:00
Ildar Kamalov
c2a2b3ea6a Check upstream length in component 2018-09-21 18:50:06 +03:00
Ildar Kamalov
f727f999f9 Add a test upstreams button
Closes #321
2018-09-21 18:08:39 +03:00
Eugene Bujak
02b28f4511 Merge pull request #32 in DNS/adguard-dns from feature/338 to master
* commit '43fcf4117db0e1e26085a0cc20a574edc8bd6255':
  Add update check
2018-09-21 15:32:11 +03:00
Ildar Kamalov
43fcf4117d Add update check
Closes #338
2018-09-21 15:20:41 +03:00
Eugene Bujak
68422b8399 Merge pull request #31 in DNS/adguard-dns from feature/338 to master
* commit 'c3f6a96f2f36f751d1315d83d65bff9396ea71d4':
  Add API endpoint to fetch version.json from github.io
2018-09-21 12:13:11 +03:00
Eugene Bujak
c3f6a96f2f Add API endpoint to fetch version.json from github.io 2018-09-20 20:02:25 +03:00
Eugene Bujak
2c2b951fd6 Merge pull request #30 in DNS/adguard-dns from feature/339 to master
* commit 'fba70b8b730187b8a60e724c1495ecc72e6f2d5e':
  Add version.json -- contains v0.1
2018-09-20 18:42:09 +03:00
Eugene Bujak
fba70b8b73 Add version.json -- contains v0.1 2018-09-20 18:35:09 +03:00
Eugene Bujak
38cfe95280 Merge pull request #29 in DNS/adguard-dns from readme to master
* commit 'a76fd7618abce834aa4eced0200e3ecfe77298e0':
  Proofreading by @vbagirov
  Update readme.
2018-09-20 18:27:15 +03:00
Eugene Bujak
a76fd7618a Proofreading by @vbagirov 2018-09-20 18:15:52 +03:00
Eugene Bujak
8d23e29190 Update readme.
* Link directly to binary downloads
 * Move "how to run" section right after binaries.
 * Add "How does AdGuard DNS work" and "How is this different from public AdGuard DNS servers" sections.
 * Update phrasing to emphasize that it's "your AdGuard DNS" server.
 * Remove yarn from `brew install` line.
 * Mention terminal for bash commands.
 * Add 32-bit ARM and 32-bit Intel binary links.
2018-09-20 15:21:02 +03:00
Eugene Bujak
a185161ad4 Merge pull request #28 in DNS/adguard-dns from feature/321 to master
* commit 'e733c1950484969c553b92e80ced6e585cc07bea':
  Implement API to test for upstream DNS servers.
2018-09-20 13:38:12 +03:00
Ildar Kamalov
81c7dbbc16 Merge pull request #27 in DNS/adguard-dns from feature/316 to master
* commit '0e173d2f705752c335c7fa78fdd5c578420e8a43':
  add progress bar and filters notifications
2018-09-19 19:14:19 +03:00
Eugene Bujak
e733c19504 Implement API to test for upstream DNS servers. 2018-09-19 19:12:09 +03:00
Ildar Kamalov
0e173d2f70 add progress bar and filters notifications 2018-09-19 18:58:55 +03:00
Eugene Bujak
0292d2b32b Merge pull request #26 in DNS/adguard-dns from basicauth to master
* commit 'ba56d6c01d7b241829d3115e1ce87241737cf571':
  Reorganize config file.
  Update README to explain config file settings
  Implement simple basic auth.
2018-09-19 15:54:49 +03:00
Eugene Bujak
ba56d6c01d Reorganize config file. 2018-09-19 15:51:44 +03:00
Eugene Bujak
b8213bf88a Update README to explain config file settings 2018-09-19 15:51:28 +03:00
Eugene Bujak
4548eb8d11 Implement simple basic auth.
Closes #326.
2018-09-18 20:59:41 +03:00
Eugene Bujak
a2f06aadc0 Merge pull request #25 in DNS/adguard-dns from feature/331 to master
* commit 'df12038f33b7acf6a3e9329054f1b5ad4cb02cd8':
  Add refresh button to querylog page
2018-09-17 19:03:45 +03:00
Ildar Kamalov
df12038f33 Add refresh button to querylog page
Closes #331
2018-09-17 17:44:32 +03:00
Eugene Bujak
c2aa39efe5 Merge pull request #23 in DNS/adguard-dns from gometalinter to master
* commit '076c9de68e73bbddc63cf6f7212818c91f3e5c08':
  Fix many lint warnings found by gometalinter
2018-09-17 11:26:17 +03:00
Eugene Bujak
5d046c5c16 Merge pull request #24 in DNS/adguard-dns from parental_fullhash to master
* commit 'dcbe3dd4051f421553eb7782af67378f3d9ce85b':
  dnsfilter -- compare full hashes when parsing parental lookup result.
2018-09-17 01:50:09 +03:00
Eugene Bujak
ae50a2f827 Merge pull request #20 in DNS/adguard-dns from feature/315 to master
* commit 'ded02d112c0e5b6d9585ec5506f24746abffdff3':
  Add console error
  Fix timeout
  Handle settings errors
  Show toast on failed request
  Fix clear interval
  Add alert on failed requests
2018-09-17 01:44:48 +03:00
Eugene Bujak
dcbe3dd405 dnsfilter -- compare full hashes when parsing parental lookup result.
Closes #337.
2018-09-17 01:42:01 +03:00
Ildar Kamalov
ded02d112c Add console error 2018-09-14 21:31:20 +03:00
Eugene Bujak
076c9de68e Fix many lint warnings found by gometalinter 2018-09-14 18:40:05 +03:00
Ildar Kamalov
d237df6389 Fix timeout 2018-09-14 16:43:27 +03:00
Ildar Kamalov
22a5abb7b8 Handle settings errors 2018-09-14 16:41:34 +03:00
Ildar Kamalov
828bb40084 Show toast on failed request 2018-09-14 15:37:35 +03:00
Eugene Bujak
548010e002 Merge pull request #22 in DNS/adguard-dns from fixtravis to master
* commit '5c6aa910efc9d90c5435a9eda19866a7d48be032':
  Fix a missed argument that breaks go test (which invokes go vet and fails if that fails)
2018-09-14 14:48:30 +03:00
Eugene Bujak
5c6aa910ef Fix a missed argument that breaks go test (which invokes go vet and fails if that fails) 2018-09-14 14:47:27 +03:00
Eugene Bujak
b9999f155e Merge pull request #21 in DNS/adguard-dns from add_url_verifier to master
* commit '3b44efc8e3f8fac534fbec37e1537ee4bd646141':
  /add_url -- it fetches the URL and checks if contents are valid filter, fails if it is not, and returns number of rules if it is
2018-09-14 11:23:51 +03:00
Eugene Bujak
3b44efc8e3 /add_url -- it fetches the URL and checks if contents are valid filter, fails if it is not, and returns number of rules if it is 2018-09-14 04:33:54 +03:00
Ildar Kamalov
9258fada47 Fix clear interval
Closes #315
2018-09-12 15:38:54 +03:00
Ildar Kamalov
6c70d8ca37 Add alert on failed requests 2018-09-12 12:58:55 +03:00
Eugene Bujak
5554643cd0 Merge pull request #19 in DNS/adguard-dns from version to master
* commit '7c71d4b44597ca7b22d3db60960c67b6e23ff2a6':
  web interface -- avoid having 'v.v0.1', saying 'version v0.1' seems more natural than that
2018-09-11 19:08:27 +03:00
Eugene Bujak
7c71d4b445 web interface -- avoid having 'v.v0.1', saying 'version v0.1' seems more natural than that 2018-09-11 19:04:49 +03:00
Eugene Bujak
3a92520764 Merge pull request #18 in DNS/adguard-dns from nonfqdn to master
* commit 'aa2e5500e72864727a0dcd196f37e84931cfa30a':
  coredns plugin -- do not filter out non-FQDN's -- otherwise it breaks serving /etc/hosts
2018-09-11 18:00:06 +03:00
Eugene Bujak
aa2e5500e7 coredns plugin -- do not filter out non-FQDN's -- otherwise it breaks serving /etc/hosts 2018-09-11 17:57:20 +03:00
Eugene Bujak
e2cf9ffd84 Merge pull request #17 in DNS/adguard-dns from feature/328 to master
* commit 'd8a3ee36764e4c3e33f5c73a3c5f9e73cdd5ec13':
  change graph stats to 24 hours
2018-09-11 15:05:47 +03:00
Ildar Kamalov
d8a3ee3676 change graph stats to 24 hours
Closes #328
2018-09-11 12:40:01 +03:00
Eugene Bujak
46e447589c Merge pull request #16 in DNS/adguard-dns from footer to master
* commit '97161ab4f0694367458933bc6ba44efe4d5e0509':
  web interface -- Update footer from placeholder to actual values.
2018-09-10 21:17:57 +03:00
Eugene Bujak
97161ab4f0 web interface -- Update footer from placeholder to actual values. 2018-09-10 21:14:28 +03:00
Eugene Bujak
3901dda39c Merge pull request #14 in DNS/adguard-dns from dnsfilter_recursion to master
* commit 'd49e3769a105f4dee639b9dec1112b123b7a23aa':
  dnsfilter -- do not check lookup hosts against themselves to avoid recursion
  Add support for serving /etc/hosts
  Makefile -- Fix cross-compilation
2018-09-10 20:52:20 +03:00
Eugene Bujak
d49e3769a1 dnsfilter -- do not check lookup hosts against themselves to avoid recursion 2018-09-10 20:43:22 +03:00
Eugene Bujak
c1e16cc584 Add support for serving /etc/hosts 2018-09-10 20:43:22 +03:00
Eugene Bujak
9c1dc6d373 Makefile -- Fix cross-compilation 2018-09-10 17:46:42 +03:00
Eugene Bujak
fced9178b8 Merge pull request #13 in DNS/adguard-dns from fix_coredns_build to master
* commit 'd34049b5135ff5dac2015dd93a5a624abdfe2cb4':
  travis -- Fix go test failure.
2018-09-10 16:10:44 +03:00
Eugene Bujak
d34049b513 travis -- Fix go test failure. 2018-09-10 15:57:47 +03:00
Eugene Bujak
43dbef8935 Merge pull request #12 in DNS/adguard-dns from fix_coredns_build to master
* commit 'f6d7d6a37ac9f8a184794d08c5b8a43bdc24e75a':
  Fix coredns build failure.
2018-09-10 15:33:05 +03:00
Eugene Bujak
f6d7d6a37a Fix coredns build failure. 2018-09-10 15:27:46 +03:00
Eugene Bujak
b54f9a7a36 Merge pull request #11 in DNS/adguard-dns from feature/313 to master
* commit '9e6ed7f273e996b2a89d9eda2302117adcc2389d':
  fix filtered reason overflow
  fix table cell overflow in filters
2018-09-07 19:12:05 +03:00
Ildar Kamalov
9e6ed7f273 fix filtered reason overflow 2018-09-07 19:06:09 +03:00
Ildar Kamalov
04faff3e2c fix table cell overflow in filters 2018-09-07 18:58:48 +03:00
Eugene Bujak
5fdaf7cb66 Merge pull request #10 in DNS/adguard-dns from feature/313 to master
* commit '76f98e2950463d4e176f9a5e2bf2c2c1c8aee51c':
  fix default page size in query logs
2018-09-07 18:44:34 +03:00
Ildar Kamalov
76f98e2950 fix default page size in query logs
Closes #313
2018-09-07 18:41:53 +03:00
Konstantin 🦄 Zamyakin
ba836220b8 Merge pull request #9 in DNS/adguard-dns from consistent-stats to master
* commit '31893410892bd047c9f6ea8f602717e6996c9491':
  web interface -- Make refresh buttons reload all data, not just counters
  web interface -- change text from 'general counters' to 'general statistics'
  Fixup of previous commit -- errand keystroke crept in
  API /stats_top -- sort top entries by value
  API /stats_top -- show only top entries for last 3 minutes
2018-09-07 18:21:46 +03:00
Eugene Bujak
3189341089 web interface -- Make refresh buttons reload all data, not just counters 2018-09-07 18:05:10 +03:00
Eugene Bujak
4ba8293c06 web interface -- change text from 'general counters' to 'general statistics' 2018-09-07 18:05:02 +03:00
Eugene Bujak
7094ed4f28 Fixup of previous commit -- errand keystroke crept in 2018-09-07 17:59:24 +03:00
Eugene Bujak
f623c3d909 API /stats_top -- sort top entries by value 2018-09-07 17:50:03 +03:00
Eugene Bujak
8198b65f29 API /stats_top -- show only top entries for last 3 minutes 2018-09-07 17:49:33 +03:00
Konstantin 🦄 Zamyakin
38b3fe6718 Merge pull request #8 in DNS/adguard-dns from parental_metrics to master
* commit '9682dc6bc19ea940cf71911f6281450c7027eb16':
  travis -- npm installation of dependencies belongs in install section
  makefile -- use npm --prefix syntax instead of doing cd into subdir
  travis -- don't use slow master or tip builds, just specify 1.x for latest go version
  travis -- move dependency installation to install section, simplify go test invocation to test all subdirs in one go
  dnsfilter -- small code cleanup
  coredns plugin metrics -- deduplicate code
  dnsfilter metrics -- parental cache hits were counted as safebrowsing cache hits
2018-09-07 17:11:21 +03:00
Eugene Bujak
9682dc6bc1 travis -- npm installation of dependencies belongs in install section 2018-09-07 16:14:43 +03:00
Eugene Bujak
659b530381 makefile -- use npm --prefix syntax instead of doing cd into subdir 2018-09-07 16:14:25 +03:00
Eugene Bujak
1b5748e328 travis -- don't use slow master or tip builds, just specify 1.x for latest go version 2018-09-07 16:13:23 +03:00
Eugene Bujak
ebf2380af4 travis -- move dependency installation to install section, simplify go test invocation to test all subdirs in one go 2018-09-07 16:13:03 +03:00
Eugene Bujak
6fc50cd743 dnsfilter -- small code cleanup 2018-09-07 16:10:43 +03:00
Eugene Bujak
3b9aaff861 coredns plugin metrics -- deduplicate code 2018-09-07 16:10:11 +03:00
Eugene Bujak
c572c7b0e9 dnsfilter metrics -- parental cache hits were counted as safebrowsing cache hits 2018-09-07 15:46:38 +03:00
Eugene Bujak
74275bebdc Merge pull request #7 in DNS/adguard-dns from metrics to master
* commit '1f0fdef8d6b2ce324e7009bb3f95626d87438d61':
  Fix invalid element order for historical stats -- in API declaration values are from oldest to newest, not other way around.
  Rewrite how historical stats are stored and calculated.
  coredns plugin -- convert logic into switch, logging unexpected non-covered cases
  After filters were redownloaded and deemed to be fresh, tell coredns server to reload
  coredns plugin -- on server reload, metrics disappeared, therefore they must be registered on each reload instead of once
  coredns plugin -- give feedback how many rules were in rulefile
  dnsfilter -- Update tests to check for expected filter/nofilter reason as well.
  Remove debug logging during checks if coredns is alive
  Be more atomic during writing of files -- this prevents other processes from seeing empty or impartial files
  Start coredns on launch before we serve HTTP -- this checks if port is available
  Move starting of coredns server into separate function
  sometimes answer can be empty, therefore question could be lost -- pass both to querylog
  Reduce binary size of coredns by 60% by removing orchestration plugins like kubernetes, route53, trace, etcd and federation
  Fix registration of metrics if querylog is enabled
2018-09-06 14:44:40 +03:00
Eugene Bujak
1f0fdef8d6 Fix invalid element order for historical stats -- in API declaration values are from oldest to newest, not other way around. 2018-09-06 02:20:51 +03:00
Eugene Bujak
04562dece3 Rewrite how historical stats are stored and calculated.
Closes #310.
2018-09-06 02:11:36 +03:00
Eugene Bujak
c7a5275d42 coredns plugin -- convert logic into switch, logging unexpected non-covered cases 2018-09-06 02:09:57 +03:00
Eugene Bujak
fe397943d6 After filters were redownloaded and deemed to be fresh, tell coredns server to reload 2018-09-06 02:09:05 +03:00
Eugene Bujak
876854d403 coredns plugin -- on server reload, metrics disappeared, therefore they must be registered on each reload instead of once 2018-09-06 02:08:49 +03:00
Eugene Bujak
c143e3d57f coredns plugin -- give feedback how many rules were in rulefile 2018-09-06 02:07:23 +03:00
Eugene Bujak
1102963fa0 dnsfilter -- Update tests to check for expected filter/nofilter reason as well. 2018-09-06 02:06:40 +03:00
Eugene Bujak
f2621c4a9a Remove debug logging during checks if coredns is alive 2018-09-06 02:04:16 +03:00
Eugene Bujak
859f1590dd Be more atomic during writing of files -- this prevents other processes from seeing empty or impartial files 2018-09-06 02:03:03 +03:00
Eugene Bujak
0ce40fd46e Start coredns on launch before we serve HTTP -- this checks if port is available 2018-09-06 02:00:57 +03:00
Eugene Bujak
33fbccf0ba Move starting of coredns server into separate function 2018-09-06 02:00:44 +03:00
Eugene Bujak
e122d9138b sometimes answer can be empty, therefore question could be lost -- pass both to querylog 2018-09-05 21:25:11 +03:00
Eugene Bujak
606bed9d20 Reduce binary size of coredns by 60% by removing orchestration plugins like kubernetes, route53, trace, etcd and federation 2018-09-05 21:23:08 +03:00
Eugene Bujak
3b11648e14 Fix registration of metrics if querylog is enabled 2018-09-05 21:21:46 +03:00
Konstantin 🦄 Zamyakin
e62050fb7e Merge pull request #6 in DNS/adguard-dns from feature/319 to master
* commit 'fa8bc570827cae708266901754712fb0c9a1f1ea':
  add reason status
  fix tooltip styles
  add client column and tooltip to blocked requests
2018-09-05 19:30:33 +03:00
Maxim Topchu
fa8bc57082 add reason status 2018-09-05 18:20:38 +03:00
Ildar Kamalov
0e99a65687 fix tooltip styles 2018-09-04 12:43:13 +03:00
Maxim Topchu
bed92f89f0 add client column and tooltip to blocked requests 2018-09-03 15:55:20 +03:00
Konstantin 🦄 Zamyakin
f12ef5d504 Merge pull request #5 in DNS/adguard-dns from querylog_client to master
* commit '379e14c28b18c0632e42760aca94aa7b72b05885':
  coredns plugin -- forgot to pass client's IP to querylog, fix that.
2018-08-31 21:08:20 +03:00
Eugene Bujak
379e14c28b coredns plugin -- forgot to pass client's IP to querylog, fix that.
Closes #311
2018-08-31 19:59:04 +03:00
Maxim Topchu
2ca1a0e586 fix link and name 2018-08-31 18:48:14 +03:00
Eugene Bujak
30553c6a9a Show 50 top blocked/requestsed/clients instead of 3.
Closes #312
2018-08-31 18:21:07 +03:00
Eugene Bujak
7bf513b638 Readme -- remove yarn from prerequisites since it's no longer needed 2018-08-31 18:13:45 +03:00
Konstantin Zamyakin
c4fefa10b0 Travis -- fix build on OSX, simplify travis and get rid of yarn 2018-08-31 18:11:45 +03:00
Eugene Bujak
3af62e463a travis -- install lts/* version of node via travis configuration instead of brew 2018-08-31 13:30:41 +03:00
Eugene Bujak
ad91ba8f43 travis -- upgrade nodejs on osx 2018-08-31 13:07:37 +03:00
Eugene Bujak
f054dcede4 travis -- build binaries as a test as well 2018-08-30 19:47:29 +03:00
Eugene Bujak
d53f9bafe9 Makefile -- replace sed with perl, add check that plugin.cfg has dnsfilter 2018-08-30 19:05:15 +03:00
Eugene Bujak
0421e1f4f8 Add travis testing 2018-08-30 18:18:18 +03:00
94 changed files with 31187 additions and 11425 deletions

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
client/* linguist-vendored

9
.gitignore vendored
View File

@@ -1,7 +1,12 @@
/AdguardDNS
/AdguardDNS.yaml
.DS_Store
.vscode
debug
/AdGuardHome
/AdGuardHome.yaml
/build/
/client/node_modules/
/coredns
/Corefile
/dnsfilter.txt
/querylog.json
/querylog.json.1

20
.travis.yml Normal file
View File

@@ -0,0 +1,20 @@
language: go
sudo: false
go:
- 1.10.x
- 1.11.x
- 1.x
os:
- linux
- osx
install:
- go get -v -d -t ./...
- npm --prefix client install
script:
- (cd `go env GOPATH`/src/github.com/prometheus/client_golang && git checkout -q v0.8.0)
- go test ./...
- make

48
Dockerfile.arm Normal file
View File

@@ -0,0 +1,48 @@
FROM easypi/alpine-arm:latest
LABEL maintainer="Erik Rogers <erik.rogers@live.com>"
# AdGuard version
ARG ADGUARD_VERSION="0.9"
ENV ADGUARD_VERSION $ADGUARD_VERSION
# AdGuard architecture and package info
ARG ADGUARD_ARCH="linux_arm"
ENV ADGUARD_ARCH ${ADGUARD_ARCH}
ENV ADGUARD_PACKAGE "AdGuardHome_v${ADGUARD_VERSION}_${ADGUARD_ARCH}"
# AdGuard release info
ARG ADGUARD_ARCHIVE="${ADGUARD_PACKAGE}.tar.gz"
ENV ADGUARD_ARCHIVE ${ADGUARD_ARCHIVE}
ARG ADGUARD_RELEASE="https://github.com/AdguardTeam/AdGuardHome/releases/download/v${ADGUARD_VERSION}/${ADGUARD_ARCHIVE}"
ENV ADGUARD_RELEASE ${ADGUARD_RELEASE}
# AdGuard directory
ARG ADGUARD_DIR="/data/adguard"
ENV ADGUARD_DIR ${ADGUARD_DIR}
# Update CA certs and download AdGuard binaries
RUN apk --no-cache --update add ca-certificates \
&& cd /tmp \
&& wget ${ADGUARD_RELEASE} \
&& tar xvf ${ADGUARD_ARCHIVE} \
&& mkdir -p "${ADGUARD_DIR}" \
&& cp "AdGuardHome/AdGuardHome" "${ADGUARD_DIR}" \
&& chmod +x "${ADGUARD_DIR}/AdGuardHome" \
&& rm -rf "AdGuardHome" \
&& rm ${ADGUARD_ARCHIVE}
# Expose DNS port 53
EXPOSE 53
# Expose UI port 3000
ARG ADGUARD_UI_HOST="0.0.0.0"
ENV ADGUARD_UI_HOST ${ADGUARD_UI_HOST}
ARG ADGUARD_UI_PORT="3000"
ENV ADGUARD_UI_PORT ${ADGUARD_UI_PORT}
EXPOSE ${ADGUARD_UI_PORT}
# Run AdGuardHome
WORKDIR ${ADGUARD_DIR}
VOLUME ${ADGUARD_DIR}
ENTRYPOINT ./AdGuardHome --host ${ADGUARD_UI_HOST} --port ${ADGUARD_UI_PORT}

48
Dockerfile.linux Normal file
View File

@@ -0,0 +1,48 @@
FROM alpine:latest
LABEL maintainer="Erik Rogers <erik.rogers@live.com>"
# AdGuard version
ARG ADGUARD_VERSION="0.9"
ENV ADGUARD_VERSION $ADGUARD_VERSION
# AdGuard architecture and package info
ARG ADGUARD_ARCH="linux_386"
ENV ADGUARD_ARCH ${ADGUARD_ARCH}
ENV ADGUARD_PACKAGE "AdGuardHome_v${ADGUARD_VERSION}_${ADGUARD_ARCH}"
# AdGuard release info
ARG ADGUARD_ARCHIVE="${ADGUARD_PACKAGE}.tar.gz"
ENV ADGUARD_ARCHIVE ${ADGUARD_ARCHIVE}
ARG ADGUARD_RELEASE="https://github.com/AdguardTeam/AdGuardHome/releases/download/v${ADGUARD_VERSION}/${ADGUARD_ARCHIVE}"
ENV ADGUARD_RELEASE ${ADGUARD_RELEASE}
# AdGuard directory
ARG ADGUARD_DIR="/data/adguard"
ENV ADGUARD_DIR ${ADGUARD_DIR}
# Update CA certs and download AdGuard binaries
RUN apk --no-cache --update add ca-certificates \
&& cd /tmp \
&& wget ${ADGUARD_RELEASE} \
&& tar xvf ${ADGUARD_ARCHIVE} \
&& mkdir -p "${ADGUARD_DIR}" \
&& cp "AdGuardHome/AdGuardHome" "${ADGUARD_DIR}" \
&& chmod +x "${ADGUARD_DIR}/AdGuardHome" \
&& rm -rf "AdGuardHome" \
&& rm ${ADGUARD_ARCHIVE}
# Expose DNS port 53
EXPOSE 53
# Expose UI port 3000
ARG ADGUARD_UI_HOST="0.0.0.0"
ENV ADGUARD_UI_HOST ${ADGUARD_UI_HOST}
ARG ADGUARD_UI_PORT="3000"
ENV ADGUARD_UI_PORT ${ADGUARD_UI_PORT}
EXPOSE ${ADGUARD_UI_PORT}
# Run AdGuardHome
WORKDIR ${ADGUARD_DIR}
VOLUME ${ADGUARD_DIR}
ENTRYPOINT ./AdGuardHome --host ${ADGUARD_UI_HOST} --port ${ADGUARD_UI_PORT}

48
Dockerfile.linux64 Normal file
View File

@@ -0,0 +1,48 @@
FROM alpine:latest
LABEL maintainer="Erik Rogers <erik.rogers@live.com>"
# AdGuard version
ARG ADGUARD_VERSION="0.9"
ENV ADGUARD_VERSION $ADGUARD_VERSION
# AdGuard architecture and package info
ARG ADGUARD_ARCH="linux_amd64"
ENV ADGUARD_ARCH ${ADGUARD_ARCH}
ENV ADGUARD_PACKAGE "AdGuardHome_v${ADGUARD_VERSION}_${ADGUARD_ARCH}"
# AdGuard release info
ARG ADGUARD_ARCHIVE="${ADGUARD_PACKAGE}.tar.gz"
ENV ADGUARD_ARCHIVE ${ADGUARD_ARCHIVE}
ARG ADGUARD_RELEASE="https://github.com/AdguardTeam/AdGuardHome/releases/download/v${ADGUARD_VERSION}/${ADGUARD_ARCHIVE}"
ENV ADGUARD_RELEASE ${ADGUARD_RELEASE}
# AdGuard directory
ARG ADGUARD_DIR="/data/adguard"
ENV ADGUARD_DIR ${ADGUARD_DIR}
# Update CA certs and download AdGuard binaries
RUN apk --no-cache --update add ca-certificates \
&& cd /tmp \
&& wget ${ADGUARD_RELEASE} \
&& tar xvf ${ADGUARD_ARCHIVE} \
&& mkdir -p "${ADGUARD_DIR}" \
&& cp "AdGuardHome/AdGuardHome" "${ADGUARD_DIR}" \
&& chmod +x "${ADGUARD_DIR}/AdGuardHome" \
&& rm -rf "AdGuardHome" \
&& rm ${ADGUARD_ARCHIVE}
# Expose DNS port 53
EXPOSE 53
# Expose UI port 3000
ARG ADGUARD_UI_HOST="0.0.0.0"
ENV ADGUARD_UI_HOST ${ADGUARD_UI_HOST}
ARG ADGUARD_UI_PORT="3000"
ENV ADGUARD_UI_PORT ${ADGUARD_UI_PORT}
EXPOSE ${ADGUARD_UI_PORT}
# Run AdGuardHome
WORKDIR ${ADGUARD_DIR}
VOLUME ${ADGUARD_DIR}
ENTRYPOINT ./AdGuardHome --host ${ADGUARD_UI_HOST} --port ${ADGUARD_UI_PORT}

View File

@@ -1,33 +1,41 @@
GIT_VERSION := $(shell git describe --abbrev=4 --dirty --always --tags)
GOPATH := $(shell go env GOPATH)
NATIVE_GOOS = $(shell unset GOOS; go env GOOS)
NATIVE_GOARCH = $(shell unset GOARCH; go env GOARCH)
mkfile_path := $(abspath $(lastword $(MAKEFILE_LIST)))
mkfile_dir := $(patsubst %/,%,$(dir $(mkfile_path)))
STATIC := build/static/bundle.css build/static/bundle.js build/static/index.html
GOPATH := $(mkfile_dir)/build/gopath
JSFILES = $(shell find client -path client/node_modules -prune -o -type f -name '*.js')
STATIC = build/static/index.html
TARGET=AdGuardHome
.PHONY: all build clean
all: build
build: AdguardDNS coredns
build: $(TARGET)
$(STATIC):
yarn --cwd client install
yarn --cwd client run build-prod
client/node_modules: client/package.json client/package-lock.json
npm --prefix client install
touch client/node_modules
AdguardDNS: $(STATIC) *.go
echo mkfile_dir = $(mkfile_dir)
go get -v -d .
GOOS=$(NATIVE_GOOS) GOARCH=$(NATIVE_GOARCH) go get -v github.com/gobuffalo/packr/...
PATH=$(GOPATH)/bin:$(PATH) packr build -ldflags="-X main.VersionString=$(GIT_VERSION)" -o AdguardDNS
$(STATIC): $(JSFILES) client/node_modules
npm --prefix client run build-prod
coredns: coredns_plugin/*.go dnsfilter/*.go
echo mkfile_dir = $(mkfile_dir)
go get -v -d github.com/coredns/coredns
cd $(GOPATH)/src/github.com/coredns/coredns && grep -q 'dnsfilter:' plugin.cfg || sed -E -i.bak $$'s|^log:log|log:log\\\ndnsfilter:github.com/AdguardTeam/AdguardDNS/coredns_plugin|g' plugin.cfg
cd $(GOPATH)/src/github.com/coredns/coredns && GOOS=$(NATIVE_GOOS) GOARCH=$(NATIVE_GOARCH) go generate
cd $(GOPATH)/src/github.com/coredns/coredns && go get -v -d .
cd $(GOPATH)/src/github.com/coredns/coredns && go build -o $(mkfile_dir)/coredns
$(TARGET): $(STATIC) *.go coredns_plugin/*.go dnsfilter/*.go
mkdir -p $(GOPATH)/src/github.com/AdguardTeam
if [ ! -h $(GOPATH)/src/github.com/AdguardTeam/AdGuardHome ]; then rm -rf $(GOPATH)/src/github.com/AdguardTeam/AdGuardHome && ln -fs $(mkfile_dir) $(GOPATH)/src/github.com/AdguardTeam/AdGuardHome; fi
GOPATH=$(GOPATH) go get -v -d .
GOPATH=$(GOPATH) GOOS=$(NATIVE_GOOS) GOARCH=$(NATIVE_GOARCH) go get -v github.com/gobuffalo/packr/...
mkdir -p $(GOPATH)/src/github.com/AdguardTeam/AdGuardHome/build/static ## work around packr bug
cd $(GOPATH)/src/github.com/prometheus/client_golang && git reset --hard v0.8.0
perl -0777 -p -i.bak -e 's/pprofOnce.Do\(func\(\) {(.*)}\)/\1/ms' $(GOPATH)/src/github.com/coredns/coredns/plugin/pprof/setup.go
perl -0777 -p -i.bak -e 's/c.OnShutdown/c.OnRestart/' $(GOPATH)/src/github.com/coredns/coredns/plugin/pprof/setup.go
GOPATH=$(GOPATH) PATH=$(GOPATH)/bin:$(PATH) packr build -ldflags="-X main.VersionString=$(GIT_VERSION)" -o $(TARGET)
clean:
rm -vf coredns AdguardDNS
$(MAKE) cleanfast
rm -rf build
rm -rf client/node_modules
cleanfast:
rm -f $(TARGET)

172
README.md
View File

@@ -1,66 +1,166 @@
# Self-hosted AdGuard DNS
&nbsp;
<p align="center">
<img src="https://cdn.adguard.com/public/Adguard/Common/adguard_home.svg" width="300px" alt="AdGuard Home" />
</p>
<h3 align="center">Privacy protection center for you and your devices</h3>
<p align="center">
Free and open source, powerful network-wide ads & trackers blocking DNS server.
</p>
AdGuard DNS is an ad-filtering DNS server with built-in phishing protection and optional family-friendly protection.
<p align="center">
<a href="https://adguard.com/">AdGuard.com</a> |
<a href="https://github.com/AdguardTeam/AdGuardHome/wiki">Wiki</a> |
<a href="https://reddit.com/r/Adguard">Reddit</a> |
<a href="https://twitter.com/AdGuard">Twitter</a>
<br /><br />
<a href="https://travis-ci.org/AdguardTeam/AdGuardHome">
<img src="https://travis-ci.org/AdguardTeam/AdGuardHome.svg" alt="Build status" />
</a>
<a href="https://github.com/AdguardTeam/AdGuardHome/releases">
<img src="https://img.shields.io/github/release/AdguardTeam/AdGuardHome/all.svg" alt="Latest release" />
</a>
</p>
This repository describes how to set up and run your self-hosted instance of AdGuard DNS -- it comes with a web dashboard that can be accessed from browser to control the DNS server and change its settings, it also allows you to add your filters in both AdGuard and hosts format.
<br />
If this seems too complicated, you can always use AdGuard DNS servers that provide same functionality — https://adguard.com/en/adguard-dns/overview.html
<p align="center">
<img src="https://cdn.adguard.com/public/Adguard/Common/adguard_home.gif" width="800" />
</p>
<hr />
# AdGuard Home
AdGuard Home is a network-wide software for blocking ads & tracking. After you set it up, it'll cover ALL your home devices, and you don't need any client-side software for that.
## How does AdGuard Home work?
AdGuard Home operates as a DNS server that re-routes tracking domains to a "black hole," thus preventing your devices from connecting to those servers. It's based on software we use for our public [AdGuard DNS](https://adguard.com/en/adguard-dns/overview.html) servers -- both share a lot of common code.
## How is this different from public AdGuard DNS servers?
Running your own AdGuard Home server allows you to do much more than using a public DNS server.
* Choose what exactly will the server block or not block;
* Monitor your network activity;
* Add your own custom filtering rules;
In the future, AdGuard Home is supposed to become more than just a DNS server.
## Installation
Go to https://github.com/AdguardTeam/AdguardDNS/releases and download the binaries for your platform:
### Mac
Download file `AdguardDNS_*_darwin_amd64.tar.gz`, then unpack it and follow [how to run](#How-to-run) instructions below.
### Linux
Download file `AdguardDNS_*_linux_amd64.tar.gz`, then unpack it and follow [how to run](#How-to-run) instructions below.
Download this file: [AdGuardHome_v0.9-hotfix1_MacOS.zip](https://github.com/AdguardTeam/AdGuardHome/releases/download/v0.9-hotfix1/AdGuardHome_v0.9-hotfix1_MacOS.zip), then unpack it and follow ["How to run"](#how-to-run) instructions below.
## How to build your own
### Linux 64-bit Intel
Download this file: [AdGuardHome_v0.9-hotfix1_linux_amd64.tar.gz](https://github.com/AdguardTeam/AdGuardHome/releases/download/v0.9-hotfix1/AdGuardHome_v0.9-hotfix1_linux_amd64.tar.gz), then unpack it and follow ["How to run"](#how-to-run) instructions below.
### Linux 32-bit Intel
Download this file: [AdGuardHome_v0.9-hotfix1_linux_386.tar.gz](https://github.com/AdguardTeam/AdGuardHome/releases/download/v0.9-hotfix1/AdGuardHome_v0.9-hotfix1_linux_386.tar.gz), then unpack it and follow ["How to run"](#how-to-run) instructions below.
### Raspberry Pi (32-bit ARM)
Download this file: [AdGuardHome_v0.9-hotfix1_linux_arm.tar.gz](https://github.com/AdguardTeam/AdGuardHome/releases/download/v0.9-hotfix1/AdGuardHome_v0.9-hotfix1_linux_arm.tar.gz), then unpack it and follow ["How to run"](#how-to-run) instructions below.
## How to run
DNS works on port 53, which requires superuser privileges. Therefore, you need to run it with `sudo` in terminal:
```bash
sudo ./AdGuardHome
```
Now open the browser and navigate to http://localhost:3000/ to control your AdGuard Home service.
### Running without superuser
You can run AdGuard Home without superuser privileges, but you need to instruct it to use a different port rather than 53. You can do that by editing `AdGuardHome.yaml` and finding these two lines:
```yaml
coredns:
port: 53
```
You can change port 53 to anything above 1024 to avoid requiring superuser privileges.
If the file does not exist, create it in the same folder, type these two lines down and save.
### Additional configuration
Upon the first execution, a file named `AdGuardHome.yaml` will be created, with default values written in it. You can modify the file while your AdGuard Home service is not running. Otherwise, any changes to the file will be lost because the running program will overwrite them.
Settings are stored in [YAML format](https://en.wikipedia.org/wiki/YAML), possible parameters that you can configure are listed below:
* `bind_host` — Web interface IP address to listen on
* `bind_port` — Web interface IP port to listen on
* `auth_name` — Web interface optional authorization username
* `auth_pass` — Web interface optional authorization password
* `coredns` — CoreDNS configuration section
* `port` — DNS server port to listen on
* `filtering_enabled` — Filtering of DNS requests based on filter lists
* `safebrowsing_enabled` — Filtering of DNS requests based on safebrowsing
* `safesearch_enabled` — Enforcing "Safe search" option for search engines, when possible
* `parental_enabled` — Parental control-based DNS requests filtering
* `parental_sensitivity` — Age group for parental control-based filtering, must be either 3, 10, 13 or 17
* `querylog_enabled` — Query logging (also used to calculate top 50 clients, blocked domains and requested domains for statistic purposes)
* `upstream_dns` — List of upstream DNS servers
* `filters` — List of filters, each filter has the following values:
* `url` — URL pointing to the filter contents (filtering rules)
* `enabled` — Current filter's status (enabled/disabled)
* `user_rules` — User-specified filtering rules
Removing an entry from settings file will reset it to the default value. Deleting the file will reset all settings to the default values.
## How to build from source
### Prerequisites
You will need:
* [go](https://golang.org/dl/)
* [node.js](https://nodejs.org/en/download/)
* [yarn](https://yarnpkg.com/en/docs/install)
You can either install it from these websites or use [brew.sh](https://brew.sh/) if you're on Mac:
You can either install it via the provided links or use [brew.sh](https://brew.sh/) if you're on Mac:
```bash
brew install go node yarn
brew install go node
```
### Building
Open Terminal and execute these commands:
```bash
git clone https://github.com/AdguardTeam/AdguardDNS
cd AdguardDNS
git clone https://github.com/AdguardTeam/AdGuardHome
cd AdGuardHome
make
```
## How to run
DNS works on port 53, which requires superuser privileges. Therefore, you need to run it with sudo:
```bash
sudo ./AdguardDNS
```
Now open the browser and point it to http://localhost:3000/ to control AdGuard DNS server.
## Running without superuser
You can run it without superuser privileges, but you need to instruct it to use other port rather than 53. You can do that by opening `AdguardDNS.yaml` and adding this line:
```yaml
coredns:
port: 53535
```
If the file does not exist, create it and put these two lines down.
## Contributing
You are welcome to fork this repository, make your changes and submit a pull request — https://github.com/AdguardTeam/AdguardDNS/pulls
You are welcome to fork this repository, make your changes and submit a pull request — https://github.com/AdguardTeam/AdGuardHome/pulls
## Reporting issues
If you come across any problem, or have a suggestion, head to [this page](https://github.com/AdguardTeam/AdguardDNS/issues) and click on the New issue button.
If you run into any problem or have a suggestion, head to [this page](https://github.com/AdguardTeam/AdGuardHome/issues) and click on the `New issue` button.
## Acknowledgments
This software wouldn't have been possible without:
* [Go](https://golang.org/dl/) and it's libraries:
* [CoreDNS](https://coredns.io)
* [packr](https://github.com/gobuffalo/packr)
* [gcache](https://github.com/bluele/gcache)
* [miekg's dns](https://github.com/miekg/dns)
* [go-yaml](https://github.com/go-yaml/yaml)
* [Node.js](https://nodejs.org/) and it's libraries:
* [React.js](https://reactjs.org)
* [Tabler](https://github.com/tabler/tabler)
* And many more node.js packages.
* [whotracks.me data](https://github.com/cliqz-oss/whotracks.me)
For a full list of all node.js packages in use, please take a look at [client/package.json](https://github.com/AdguardTeam/AdGuardHome/blob/master/client/package.json) file.

148
app.go
View File

@@ -1,6 +1,7 @@
package main
import (
"bufio"
"fmt"
"log"
"net"
@@ -10,13 +11,14 @@ import (
"strconv"
"github.com/gobuffalo/packr"
"golang.org/x/crypto/ssh/terminal"
)
// VersionString will be set through ldflags, contains current version
var VersionString = "undefined"
func main() {
log.Printf("AdGuard DNS web interface backend, version %s\n", VersionString)
log.Printf("AdGuard Home web interface backend, version %s\n", VersionString)
box := packr.NewBox("build/static")
{
executable, err := os.Executable()
@@ -26,6 +28,8 @@ func main() {
config.ourBinaryDir = filepath.Dir(executable)
}
doConfigRename := true
// config can be specified, which reads options from there, but other command line flags have to override config values
// therefore, we must do it manually instead of using a lib
{
@@ -94,10 +98,25 @@ func main() {
}
}
if configFilename != nil {
// config was manually specified, don't do anything
doConfigRename = false
config.ourConfigFilename = *configFilename
}
if doConfigRename {
err := renameOldConfigIfNeccessary()
if err != nil {
panic(err)
}
}
err := askUsernamePasswordIfPossible()
if err != nil {
log.Fatal(err)
}
// parse from config file
err := parseConfig()
err = parseConfig()
if err != nil {
log.Fatal(err)
}
@@ -109,6 +128,11 @@ func main() {
}
}
// eat all args so that coredns can start happily
if len(os.Args) > 1 {
os.Args = os.Args[:1]
}
err := writeConfig()
if err != nil {
log.Fatal(err)
@@ -116,13 +140,129 @@ func main() {
address := net.JoinHostPort(config.BindHost, strconv.Itoa(config.BindPort))
runStatsCollectors()
runFilterRefreshers()
http.Handle("/", http.FileServer(box))
http.Handle("/", optionalAuthHandler(http.FileServer(box)))
registerControlHandlers()
err = startDNSServer()
if err != nil {
log.Fatal(err)
}
URL := fmt.Sprintf("http://%s", address)
log.Println("Go to " + URL)
log.Fatal(http.ListenAndServe(address, nil))
}
func getInput() (string, error) {
scanner := bufio.NewScanner(os.Stdin)
scanner.Scan()
text := scanner.Text()
err := scanner.Err()
return text, err
}
func promptAndGet(prompt string) (string, error) {
for {
fmt.Printf(prompt)
input, err := getInput()
if err != nil {
log.Printf("Failed to get input, aborting: %s", err)
return "", err
}
if len(input) != 0 {
return input, nil
}
// try again
}
return "", nil
}
func promptAndGetPassword(prompt string) (string, error) {
for {
fmt.Printf(prompt)
password, err := terminal.ReadPassword(int(os.Stdin.Fd()))
fmt.Printf("\n")
if err != nil {
log.Printf("Failed to get input, aborting: %s", err)
return "", err
}
if len(password) != 0 {
return string(password), nil
}
// try again
}
}
func askUsernamePasswordIfPossible() error {
configfile := filepath.Join(config.ourBinaryDir, config.ourConfigFilename)
_, err := os.Stat(configfile)
if !os.IsNotExist(err) {
// do nothing, file exists
trace("File %s exists, won't ask for password", configfile)
return nil
}
if !terminal.IsTerminal(int(os.Stdin.Fd())) {
return nil // do nothing
}
if !terminal.IsTerminal(int(os.Stdout.Fd())) {
return nil // do nothing
}
fmt.Printf("Would you like to set user/password for the web interface authentication (yes/no)?\n")
yesno, err := promptAndGet("Please type 'yes' or 'no': ")
if err != nil {
return err
}
if yesno[0] != 'y' && yesno[0] != 'Y' {
return nil
}
username, err := promptAndGet("Please enter the username: ")
if err != nil {
return err
}
password, err := promptAndGetPassword("Please enter the password: ")
if err != nil {
return err
}
password2, err := promptAndGetPassword("Please enter password again: ")
if err != nil {
return err
}
if password2 != password {
fmt.Printf("Passwords do not match! Aborting\n")
os.Exit(1)
}
config.AuthName = username
config.AuthPass = password
return nil
}
func renameOldConfigIfNeccessary() error {
oldConfigFile := filepath.Join(config.ourBinaryDir, "AdguardDNS.yaml")
_, err := os.Stat(oldConfigFile)
if os.IsNotExist(err) {
// do nothing, file doesn't exist
trace("File %s doesn't exist, nothing to do", oldConfigFile)
return nil
}
newConfigFile := filepath.Join(config.ourBinaryDir, config.ourConfigFilename)
_, err = os.Stat(newConfigFile)
if !os.IsNotExist(err) {
// do nothing, file doesn't exist
trace("File %s already exists, will not overwrite", newConfigFile)
return nil
}
err = os.Rename(oldConfigFile, newConfigFile)
if err != nil {
log.Printf("Failed to rename %s to %s: %s", oldConfigFile, newConfigFile, err)
return err
}
return nil
}

9
client/.eslintrc vendored
View File

@@ -13,6 +13,13 @@
"commonjs": true
},
"settings": {
"react": {
"pragma": "React",
"version": "16.4"
}
},
"rules": {
"indent": ["error", 4, {
"SwitchCase": 1,
@@ -43,6 +50,6 @@
}],
"no-console": ["warn", { "allow": ["warn", "error"] }],
"import/no-extraneous-dependencies": ["error", { "devDependencies": true }],
"import/prefer-default-export": "off",
"import/prefer-default-export": "off"
}
}

16614
client/package-lock.json generated vendored Normal file

File diff suppressed because it is too large Load Diff

14
client/package.json vendored
View File

@@ -3,31 +3,34 @@
"version": "0.1.0",
"private": true,
"scripts": {
"build-dev": "NODE_ENV=development webpack --config webpack.dev.js",
"watch": "NODE_ENV=development webpack --config webpack.dev.js --watch",
"build-prod": "NODE_ENV=production webpack --config webpack.prod.js",
"build-dev": "NODE_ENV=development ./node_modules/.bin/webpack --config webpack.dev.js",
"watch": "NODE_ENV=development ./node_modules/.bin/webpack --config webpack.dev.js --watch",
"build-prod": "NODE_ENV=production ./node_modules/.bin/webpack --config webpack.prod.js",
"lint": "eslint frontend/"
},
"dependencies": {
"@nivo/line": "^0.42.1",
"@nivo/line": "^0.49.1",
"axios": "^0.18.0",
"classnames": "^2.2.6",
"date-fns": "^1.29.0",
"file-saver": "^1.3.8",
"lodash": "^4.17.10",
"nanoid": "^1.2.3",
"prop-types": "^15.6.1",
"react": "^16.4.0",
"react-click-outside": "^3.0.1",
"react-dom": "^16.4.0",
"react-modal": "^3.4.5",
"react-redux": "^5.0.7",
"react-redux-loading-bar": "^4.0.7",
"react-router-dom": "^4.2.2",
"react-table": "^6.8.6",
"react-transition-group": "^2.4.0",
"redux": "^4.0.0",
"redux-actions": "^2.4.0",
"redux-thunk": "^2.3.0",
"svg-url-loader": "^2.3.2",
"tabler-react": "^1.10.0",
"tiny-version-compare": "^0.9.1",
"whatwg-fetch": "2.0.3"
},
"devDependencies": {
@@ -41,6 +44,7 @@
"babel-preset-react": "^6.24.1",
"babel-preset-stage-2": "^6.24.1",
"babel-runtime": "6.26.0",
"clean-webpack-plugin": "^0.1.19",
"compression-webpack-plugin": "^1.1.11",
"css-loader": "^0.28.11",
"eslint": "^4.19.1",

View File

@@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<link rel="shortcut icon" href="https://adguard.com/img/favicons/favicon.ico">
<title>AdGuard DNS</title>
<title>AdGuard Home</title>
</head>
<body>
<noscript>

View File

@@ -1,50 +1,70 @@
import { createAction } from 'redux-actions';
import round from 'lodash/round';
import { showLoading, hideLoading } from 'react-redux-loading-bar';
import { normalizeHistory, normalizeFilteringStatus } from '../helpers/helpers';
import { normalizeHistory, normalizeFilteringStatus, normalizeLogs } from '../helpers/helpers';
import Api from '../api/Api';
const apiClient = new Api();
export const addErrorToast = createAction('ADD_ERROR_TOAST');
export const addSuccessToast = createAction('ADD_SUCCESS_TOAST');
export const removeToast = createAction('REMOVE_TOAST');
export const toggleSettingStatus = createAction('SETTING_STATUS_TOGGLE');
export const showSettingsFailure = createAction('SETTINGS_FAILURE_SHOW');
export const toggleSetting = (settingKey, status) => async (dispatch) => {
switch (settingKey) {
case 'filtering':
if (status) {
await apiClient.disableFiltering();
} else {
await apiClient.enableFiltering();
}
dispatch(toggleSettingStatus({ settingKey }));
break;
case 'safebrowsing':
if (status) {
await apiClient.disableSafebrowsing();
} else {
await apiClient.enableSafebrowsing();
}
dispatch(toggleSettingStatus({ settingKey }));
break;
case 'parental':
if (status) {
await apiClient.disableParentalControl();
} else {
await apiClient.enableParentalControl();
}
dispatch(toggleSettingStatus({ settingKey }));
break;
case 'safesearch':
if (status) {
await apiClient.disableSafesearch();
} else {
await apiClient.enableSafesearch();
}
dispatch(toggleSettingStatus({ settingKey }));
break;
default:
break;
let successMessage = '';
try {
// TODO move setting keys to constants
switch (settingKey) {
case 'filtering':
if (status) {
successMessage = 'Disabled filtering';
await apiClient.disableFiltering();
} else {
successMessage = 'Enabled filtering';
await apiClient.enableFiltering();
}
dispatch(toggleSettingStatus({ settingKey }));
break;
case 'safebrowsing':
if (status) {
successMessage = 'Disabled safebrowsing';
await apiClient.disableSafebrowsing();
} else {
successMessage = 'Enabled safebrowsing';
await apiClient.enableSafebrowsing();
}
dispatch(toggleSettingStatus({ settingKey }));
break;
case 'parental':
if (status) {
successMessage = 'Disabled parental control';
await apiClient.disableParentalControl();
} else {
successMessage = 'Enabled parental control';
await apiClient.enableParentalControl();
}
dispatch(toggleSettingStatus({ settingKey }));
break;
case 'safesearch':
if (status) {
successMessage = 'Disabled safe search';
await apiClient.disableSafesearch();
} else {
successMessage = 'Enabled safe search';
await apiClient.enableSafesearch();
}
dispatch(toggleSettingStatus({ settingKey }));
break;
default:
break;
}
dispatch(addSuccessToast(successMessage));
} catch (error) {
dispatch(addErrorToast({ error }));
}
};
@@ -73,11 +93,51 @@ export const initSettings = settingsList => async (dispatch) => {
};
dispatch(initSettingsSuccess({ settingsList: newSettingsList }));
} catch (error) {
console.error(error);
dispatch(addErrorToast({ error }));
dispatch(initSettingsFailure());
}
};
export const getFilteringRequest = createAction('GET_FILTERING_REQUEST');
export const getFilteringFailure = createAction('GET_FILTERING_FAILURE');
export const getFilteringSuccess = createAction('GET_FILTERING_SUCCESS');
export const getFiltering = () => async (dispatch) => {
dispatch(getFilteringRequest());
try {
const filteringStatus = await apiClient.getFilteringStatus();
dispatch(getFilteringSuccess(filteringStatus.enabled));
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(getFilteringFailure());
}
};
export const toggleProtectionRequest = createAction('TOGGLE_PROTECTION_REQUEST');
export const toggleProtectionFailure = createAction('TOGGLE_PROTECTION_FAILURE');
export const toggleProtectionSuccess = createAction('TOGGLE_PROTECTION_SUCCESS');
export const toggleProtection = status => async (dispatch) => {
dispatch(toggleProtectionRequest());
let successMessage = '';
try {
if (status) {
successMessage = 'Disabled protection';
await apiClient.disableGlobalProtection();
} else {
successMessage = 'Enabled protection';
await apiClient.enableGlobalProtection();
}
dispatch(addSuccessToast(successMessage));
dispatch(toggleProtectionSuccess());
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(toggleProtectionFailure());
}
};
export const dnsStatusRequest = createAction('DNS_STATUS_REQUEST');
export const dnsStatusFailure = createAction('DNS_STATUS_FAILURE');
export const dnsStatusSuccess = createAction('DNS_STATUS_SUCCESS');
@@ -88,7 +148,7 @@ export const getDnsStatus = () => async (dispatch) => {
const dnsStatus = await apiClient.getGlobalStatus();
dispatch(dnsStatusSuccess(dnsStatus));
} catch (error) {
console.error(error);
dispatch(addErrorToast({ error }));
dispatch(initSettingsFailure());
}
};
@@ -103,7 +163,7 @@ export const enableDns = () => async (dispatch) => {
await apiClient.startGlobalFiltering();
dispatch(enableDnsSuccess());
} catch (error) {
console.error(error);
dispatch(addErrorToast({ error }));
dispatch(enableDnsFailure());
}
};
@@ -118,8 +178,8 @@ export const disableDns = () => async (dispatch) => {
await apiClient.stopGlobalFiltering();
dispatch(disableDnsSuccess());
} catch (error) {
console.error(error);
dispatch(disableDnsFailure());
dispatch(disableDnsFailure(error));
dispatch(addErrorToast({ error }));
}
};
@@ -139,30 +199,45 @@ export const getStats = () => async (dispatch) => {
dispatch(getStatsSuccess(processedStats));
} catch (error) {
console.error(error);
dispatch(addErrorToast({ error }));
dispatch(getStatsFailure());
}
};
export const getVersionRequest = createAction('GET_VERSION_REQUEST');
export const getVersionFailure = createAction('GET_VERSION_FAILURE');
export const getVersionSuccess = createAction('GET_VERSION_SUCCESS');
export const getVersion = () => async (dispatch) => {
dispatch(getVersionRequest());
try {
const newVersion = await apiClient.getGlobalVersion();
dispatch(getVersionSuccess(newVersion));
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(getVersionFailure());
}
};
export const getTopStatsRequest = createAction('GET_TOP_STATS_REQUEST');
export const getTopStatsFailure = createAction('GET_TOP_STATS_FAILURE');
export const getTopStatsSuccess = createAction('GET_TOP_STATS_SUCCESS');
export const getTopStats = () => async (dispatch, getState) => {
dispatch(getTopStatsRequest());
try {
const timer = setInterval(async () => {
const state = getState();
const timer = setInterval(async () => {
if (state.dashboard.isCoreRunning) {
if (state.dashboard.isCoreRunning) {
clearInterval(timer);
try {
const stats = await apiClient.getGlobalStatsTop();
dispatch(getTopStatsSuccess(stats));
clearInterval(timer);
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(getTopStatsFailure(error));
}
}, 100);
} catch (error) {
console.error(error);
dispatch(getTopStatsFailure());
}
}
}, 100);
};
export const getLogsRequest = createAction('GET_LOGS_REQUEST');
@@ -171,19 +246,19 @@ export const getLogsSuccess = createAction('GET_LOGS_SUCCESS');
export const getLogs = () => async (dispatch, getState) => {
dispatch(getLogsRequest());
try {
const timer = setInterval(async () => {
const state = getState();
const timer = setInterval(async () => {
if (state.dashboard.isCoreRunning) {
const logs = await apiClient.getQueryLog();
if (state.dashboard.isCoreRunning) {
clearInterval(timer);
try {
const logs = normalizeLogs(await apiClient.getQueryLog());
dispatch(getLogsSuccess(logs));
clearInterval(timer);
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(getLogsFailure(error));
}
}, 100);
} catch (error) {
console.error(error);
dispatch(getLogsFailure());
}
}
}, 100);
};
export const toggleLogStatusRequest = createAction('TOGGLE_LOGS_REQUEST');
@@ -193,16 +268,20 @@ export const toggleLogStatusSuccess = createAction('TOGGLE_LOGS_SUCCESS');
export const toggleLogStatus = queryLogEnabled => async (dispatch) => {
dispatch(toggleLogStatusRequest());
let toggleMethod;
let successMessage;
if (queryLogEnabled) {
toggleMethod = apiClient.disableQueryLog.bind(apiClient);
successMessage = 'disabled';
} else {
toggleMethod = apiClient.enableQueryLog.bind(apiClient);
successMessage = 'enabled';
}
try {
await toggleMethod();
dispatch(addSuccessToast(`Query log ${successMessage}`));
dispatch(toggleLogStatusSuccess());
} catch (error) {
console.error(error);
dispatch(addErrorToast({ error }));
dispatch(toggleLogStatusFailure());
}
};
@@ -214,10 +293,14 @@ export const setRulesSuccess = createAction('SET_RULES_SUCCESS');
export const setRules = rules => async (dispatch) => {
dispatch(setRulesRequest());
try {
await apiClient.setRules(rules);
const replacedLineEndings = rules
.replace(/^\n/g, '')
.replace(/\n\s*\n/g, '\n');
await apiClient.setRules(replacedLineEndings);
dispatch(addSuccessToast('Updated the custom filtering rules'));
dispatch(setRulesSuccess());
} catch (error) {
console.error(error);
dispatch(addErrorToast({ error }));
dispatch(setRulesFailure());
}
};
@@ -232,7 +315,7 @@ export const getFilteringStatus = () => async (dispatch) => {
const status = await apiClient.getFilteringStatus();
dispatch(getFilteringStatusSuccess({ status: normalizeFilteringStatus(status) }));
} catch (error) {
console.error(error);
dispatch(addErrorToast({ error }));
dispatch(getFilteringStatusFailure());
}
};
@@ -258,7 +341,7 @@ export const toggleFilterStatus = url => async (dispatch, getState) => {
dispatch(toggleFilterSuccess(url));
dispatch(getFilteringStatus());
} catch (error) {
console.error(error);
dispatch(addErrorToast({ error }));
dispatch(toggleFilterFailure());
}
};
@@ -269,13 +352,27 @@ export const refreshFiltersSuccess = createAction('FILTERING_REFRESH_SUCCESS');
export const refreshFilters = () => async (dispatch) => {
dispatch(refreshFiltersRequest);
dispatch(showLoading());
try {
await apiClient.refreshFilters();
const refreshText = await apiClient.refreshFilters();
dispatch(refreshFiltersSuccess);
if (refreshText.includes('OK')) {
if (refreshText.includes('OK 0')) {
dispatch(addSuccessToast('All filters are already up-to-date'));
} else {
dispatch(addSuccessToast(refreshText.replace(/OK /g, '')));
}
} else {
dispatch(addErrorToast({ error: refreshText }));
}
dispatch(getFilteringStatus());
dispatch(hideLoading());
} catch (error) {
console.error(error);
dispatch(addErrorToast({ error }));
dispatch(refreshFiltersFailure());
dispatch(hideLoading());
}
};
@@ -292,7 +389,7 @@ export const getStatsHistory = () => async (dispatch) => {
const normalizedHistory = normalizeHistory(statsHistory);
dispatch(getStatsHistorySuccess(normalizedHistory));
} catch (error) {
console.error(error);
dispatch(addErrorToast({ error }));
dispatch(getStatsHistoryFailure());
}
};
@@ -301,14 +398,14 @@ export const addFilterRequest = createAction('ADD_FILTER_REQUEST');
export const addFilterFailure = createAction('ADD_FILTER_FAILURE');
export const addFilterSuccess = createAction('ADD_FILTER_SUCCESS');
export const addFilter = url => async (dispatch) => {
export const addFilter = (url, name) => async (dispatch) => {
dispatch(addFilterRequest());
try {
await apiClient.addFilter(url);
await apiClient.addFilter(url, name);
dispatch(addFilterSuccess(url));
dispatch(getFilteringStatus());
} catch (error) {
console.error(error);
dispatch(addErrorToast({ error }));
dispatch(addFilterFailure());
}
};
@@ -325,7 +422,7 @@ export const removeFilter = url => async (dispatch) => {
dispatch(removeFilterSuccess(url));
dispatch(getFilteringStatus());
} catch (error) {
console.error(error);
dispatch(addErrorToast({ error }));
dispatch(removeFilterFailure());
}
};
@@ -344,7 +441,7 @@ export const downloadQueryLog = () => async (dispatch) => {
data = await apiClient.downloadQueryLog();
dispatch(downloadQueryLogSuccess());
} catch (error) {
console.error(error);
dispatch(addErrorToast({ error }));
dispatch(downloadQueryLogFailure());
}
return data;
@@ -359,9 +456,38 @@ export const setUpstream = url => async (dispatch) => {
dispatch(setUpstreamRequest());
try {
await apiClient.setUpstream(url);
dispatch(addSuccessToast('Updated the upstream DNS servers'));
dispatch(setUpstreamSuccess());
} catch (error) {
console.error(error);
dispatch(addErrorToast({ error }));
dispatch(setUpstreamFailure());
}
};
export const testUpstreamRequest = createAction('TEST_UPSTREAM_REQUEST');
export const testUpstreamFailure = createAction('TEST_UPSTREAM_FAILURE');
export const testUpstreamSuccess = createAction('TEST_UPSTREAM_SUCCESS');
export const testUpstream = servers => async (dispatch) => {
dispatch(testUpstreamRequest());
try {
const upstreamResponse = await apiClient.testUpstream(servers);
const testMessages = Object.keys(upstreamResponse).map((key) => {
const message = upstreamResponse[key];
if (message !== 'OK') {
dispatch(addErrorToast({ error: `Server "${key}": could not be used, please check that you've written it correctly` }));
}
return message;
});
if (testMessages.every(message => message === 'OK')) {
dispatch(addSuccessToast('Specified DNS servers are working correctly'));
}
dispatch(testUpstreamSuccess());
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(testUpstreamFailure());
}
};

View File

@@ -1,18 +1,22 @@
import axios from 'axios';
import startOfToday from 'date-fns/start_of_today';
import endOfToday from 'date-fns/end_of_today';
import subHours from 'date-fns/sub_hours';
import dateFormat from 'date-fns/format';
export default class Api {
baseUrl = 'control';
async makeRequest(path, method = 'POST', config) {
const response = await axios({
url: `${this.baseUrl}/${path}`,
method,
...config,
});
return response.data;
try {
const response = await axios({
url: `${this.baseUrl}/${path}`,
method,
...config,
});
return response.data;
} catch (error) {
console.error(error);
throw new Error(`${this.baseUrl}/${path} | ${error.response.data} | ${error.response.status}`);
}
}
// Global methods
@@ -27,6 +31,10 @@ export default class Api {
GLOBAL_QUERY_LOG_ENABLE = { path: 'querylog_enable', method: 'POST' };
GLOBAL_QUERY_LOG_DISABLE = { path: 'querylog_disable', method: 'POST' };
GLOBAL_SET_UPSTREAM_DNS = { path: 'set_upstream_dns', method: 'POST' };
GLOBAL_TEST_UPSTREAM_DNS = { path: 'test_upstream_dns', method: 'POST' };
GLOBAL_VERSION = { path: 'version.json', method: 'GET' };
GLOBAL_ENABLE_PROTECTION = { path: 'enable_protection', method: 'POST' };
GLOBAL_DISABLE_PROTECTION = { path: 'disable_protection', method: 'POST' };
restartGlobalFiltering() {
const { path, method } = this.GLOBAL_RESTART;
@@ -51,13 +59,12 @@ export default class Api {
getGlobalStatsHistory() {
const { path, method } = this.GLOBAL_STATS_HISTORY;
const format = 'YYYY-MM-DDTHH:mm:ssZ';
const todayStart = dateFormat(startOfToday(), format);
const todayEnd = dateFormat(endOfToday(), format);
const dateNow = Date.now();
const config = {
params: {
start_time: todayStart,
end_time: todayEnd,
start_time: dateFormat(subHours(dateNow, 24), format),
end_time: dateFormat(dateNow, format),
time_unit: 'hours',
},
};
@@ -104,6 +111,30 @@ export default class Api {
return this.makeRequest(path, method, config);
}
testUpstream(servers) {
const { path, method } = this.GLOBAL_TEST_UPSTREAM_DNS;
const config = {
data: servers,
header: { 'Content-Type': 'text/plain' },
};
return this.makeRequest(path, method, config);
}
getGlobalVersion() {
const { path, method } = this.GLOBAL_VERSION;
return this.makeRequest(path, method);
}
enableGlobalProtection() {
const { path, method } = this.GLOBAL_ENABLE_PROTECTION;
return this.makeRequest(path, method);
}
disableGlobalProtection() {
const { path, method } = this.GLOBAL_DISABLE_PROTECTION;
return this.makeRequest(path, method);
}
// Filtering
FILTERING_STATUS = { path: 'filtering/status', method: 'GET' };
FILTERING_ENABLE = { path: 'filtering/enable', method: 'POST' };
@@ -136,13 +167,13 @@ export default class Api {
return this.makeRequest(path, method);
}
addFilter(url) {
addFilter(url, name) {
const { path, method } = this.FILTERING_ADD_FILTER;
const parameter = 'url';
const requestBody = `${parameter}=${url}`;
const config = {
data: requestBody,
header: { 'Content-Type': 'text/plain' },
data: {
name,
url,
},
};
return this.makeRequest(path, method, config);
}

View File

@@ -17,3 +17,10 @@ body {
min-height: calc(100vh - 117px);
}
}
.loading-bar {
position: absolute;
z-index: 103;
height: 3px;
background: linear-gradient(45deg, rgba(99, 125, 120, 1) 0%, rgba(88, 177, 101, 1) 100%);
}

View File

@@ -1,6 +1,7 @@
import React, { Component, Fragment } from 'react';
import { HashRouter, Route } from 'react-router-dom';
import PropTypes from 'prop-types';
import LoadingBar from 'react-redux-loading-bar';
import 'react-table/react-table.css';
import '../ui/Tabler.css';
@@ -13,12 +14,14 @@ import Settings from '../../containers/Settings';
import Filters from '../../containers/Filters';
import Logs from '../../containers/Logs';
import Footer from '../ui/Footer';
import Toasts from '../Toasts';
import Status from '../ui/Status';
import Update from '../ui/Update';
class App extends Component {
componentDidMount() {
this.props.getDnsStatus();
this.props.getVersion();
}
handleStatusChange = () => {
@@ -27,9 +30,21 @@ class App extends Component {
render() {
const { dashboard } = this.props;
const updateAvailable =
!dashboard.processingVersions &&
dashboard.isCoreRunning &&
dashboard.isUpdateAvailable;
return (
<HashRouter hashType='noslash'>
<Fragment>
{updateAvailable &&
<Update
announcement={dashboard.announcement}
announcementUrl={dashboard.announcementUrl}
/>
}
<LoadingBar className="loading-bar" updateTime={1000} />
<Route component={Header} />
<div className="container container--wrap">
{!dashboard.processing && !dashboard.isCoreRunning &&
@@ -49,6 +64,7 @@ class App extends Component {
}
</div>
<Footer />
<Toasts />
</Fragment>
</HashRouter>
);
@@ -60,6 +76,8 @@ App.propTypes = {
enableDns: PropTypes.func,
dashboard: PropTypes.object,
isCoreRunning: PropTypes.bool,
error: PropTypes.string,
getVersion: PropTypes.func,
};
export default App;

View File

@@ -1,34 +1,76 @@
import React from 'react';
import React, { Component } from 'react';
import ReactTable from 'react-table';
import PropTypes from 'prop-types';
import map from 'lodash/map';
import Card from '../ui/Card';
import Cell from '../ui/Cell';
import Popover from '../ui/Popover';
const Clients = props => (
<Card title="Top blocked domains" subtitle="in the last 3 minutes" bodyType="card-table" refresh={props.refreshButton}>
<ReactTable
data={map(props.topBlockedDomains, (value, prop) => (
{ ip: prop, domain: value }
))}
columns={[{
Header: 'IP',
accessor: 'ip',
}, {
Header: 'Domain name',
accessor: 'domain',
}]}
showPagination={false}
noDataText="No domains found"
minRows={6}
className="-striped -highlight card-table-overflow"
/>
</Card>
);
import { getTrackerData } from '../../helpers/trackers/trackers';
import { getPercent } from '../../helpers/helpers';
import { STATUS_COLORS } from '../../helpers/constants';
Clients.propTypes = {
class BlockedDomains extends Component {
columns = [{
Header: 'IP',
accessor: 'ip',
Cell: (row) => {
const { value } = row;
const trackerData = getTrackerData(value);
return (
<div className="logs__row" title={value}>
<div className="logs__text">
{value}
</div>
{trackerData && <Popover data={trackerData} />}
</div>
);
},
}, {
Header: 'Requests count',
accessor: 'domain',
maxWidth: 190,
Cell: ({ value }) => {
const {
blockedFiltering,
replacedSafebrowsing,
replacedParental,
} = this.props;
const blocked = blockedFiltering + replacedSafebrowsing + replacedParental;
const percent = getPercent(blocked, value);
return (
<Cell value={value} percent={percent} color={STATUS_COLORS.red} />
);
},
}];
render() {
return (
<Card title="Top blocked domains" subtitle="for the last 24 hours" bodyType="card-table" refresh={this.props.refreshButton}>
<ReactTable
data={map(this.props.topBlockedDomains, (value, prop) => (
{ ip: prop, domain: value }
))}
columns={this.columns}
showPagination={false}
noDataText="No domains found"
minRows={6}
className="-striped -highlight card-table-overflow stats__table"
/>
</Card>
);
}
}
BlockedDomains.propTypes = {
topBlockedDomains: PropTypes.object.isRequired,
refreshButton: PropTypes.node,
blockedFiltering: PropTypes.number.isRequired,
replacedSafebrowsing: PropTypes.number.isRequired,
replacedParental: PropTypes.number.isRequired,
refreshButton: PropTypes.node.isRequired,
};
export default Clients;
export default BlockedDomains;

View File

@@ -1,34 +1,63 @@
import React from 'react';
import React, { Component } from 'react';
import ReactTable from 'react-table';
import PropTypes from 'prop-types';
import map from 'lodash/map';
import Card from '../ui/Card';
import Cell from '../ui/Cell';
const Clients = props => (
<Card title="Top clients" subtitle="in the last 3 minutes" bodyType="card-table" refresh={props.refreshButton}>
<ReactTable
data={map(props.topClients, (value, prop) => (
{ ip: prop, count: value }
))}
columns={[{
Header: 'IP',
accessor: 'ip',
}, {
Header: 'Request count',
accessor: 'count',
}]}
showPagination={false}
noDataText="No clients found"
minRows={6}
className="-striped -highlight card-table-overflow"
/>
</Card>
);
import { getPercent } from '../../helpers/helpers';
import { STATUS_COLORS } from '../../helpers/constants';
class Clients extends Component {
getPercentColor = (percent) => {
if (percent > 50) {
return STATUS_COLORS.green;
} else if (percent > 10) {
return STATUS_COLORS.yellow;
}
return STATUS_COLORS.red;
}
columns = [{
Header: 'IP',
accessor: 'ip',
Cell: ({ value }) => (<div className="logs__row logs__row--overflow"><span className="logs__text" title={value}>{value}</span></div>),
}, {
Header: 'Requests count',
accessor: 'count',
Cell: ({ value }) => {
const percent = getPercent(this.props.dnsQueries, value);
const percentColor = this.getPercentColor(percent);
return (
<Cell value={value} percent={percent} color={percentColor} />
);
},
}];
render() {
return (
<Card title="Top clients" subtitle="for the last 24 hours" bodyType="card-table" refresh={this.props.refreshButton}>
<ReactTable
data={map(this.props.topClients, (value, prop) => (
{ ip: prop, count: value }
))}
columns={this.columns}
showPagination={false}
noDataText="No clients found"
minRows={6}
className="-striped -highlight card-table-overflow"
/>
</Card>
);
}
}
Clients.propTypes = {
topClients: PropTypes.object.isRequired,
refreshButton: PropTypes.node,
dnsQueries: PropTypes.number.isRequired,
refreshButton: PropTypes.node.isRequired,
};
export default Clients;

View File

@@ -4,14 +4,16 @@ import PropTypes from 'prop-types';
import Card from '../ui/Card';
import Tooltip from '../ui/Tooltip';
const tooltipType = 'tooltip-custom--narrow';
const Counters = props => (
<Card title="General counters" subtitle="in the last 3 minutes" bodyType="card-table" refresh={props.refreshButton}>
<Card title="General statistics" subtitle="for the last 24 hours" bodyType="card-table" refresh={props.refreshButton}>
<table className="table card-table">
<tbody>
<tr>
<td>
DNS Queries
<Tooltip text="A number of DNS quieries processed in the last 3 minutes" />
<Tooltip text="A number of DNS quieries processed for the last 24 hours" type={tooltipType} />
</td>
<td className="text-right">
<span className="text-muted">
@@ -21,8 +23,8 @@ const Counters = props => (
</tr>
<tr>
<td>
Blocked by filters
<Tooltip text="A number of DNS requests blocked by filters" />
Blocked by <a href="#filters">Filters</a>
<Tooltip text="A number of DNS requests blocked by adblock filters and hosts blocklists" type={tooltipType} />
</td>
<td className="text-right">
<span className="text-muted">
@@ -33,7 +35,7 @@ const Counters = props => (
<tr>
<td>
Blocked malware/phishing
<Tooltip text="A number of DNS requests blocked" />
<Tooltip text="A number of DNS requests blocked by the AdGuard browsing security module" type={tooltipType} />
</td>
<td className="text-right">
<span className="text-muted">
@@ -44,7 +46,7 @@ const Counters = props => (
<tr>
<td>
Blocked adult websites
<Tooltip text="A number of adult websites blocked" />
<Tooltip text="A number of adult websites blocked" type={tooltipType} />
</td>
<td className="text-right">
<span className="text-muted">
@@ -55,7 +57,7 @@ const Counters = props => (
<tr>
<td>
Enforced safe search
<Tooltip text="A number of DNS requests to search engines for which Safe Search was enforced" />
<Tooltip text="A number of DNS requests to search engines for which Safe Search was enforced" type={tooltipType} />
</td>
<td className="text-right">
<span className="text-muted">
@@ -66,7 +68,7 @@ const Counters = props => (
<tr>
<td>
Average processing time
<Tooltip text="Average time in milliseconds on processing a DNS request" />
<Tooltip text="Average time in milliseconds on processing a DNS request" type={tooltipType} />
</td>
<td className="text-right">
<span className="text-muted">
@@ -86,7 +88,7 @@ Counters.propTypes = {
replacedParental: PropTypes.number.isRequired,
replacedSafesearch: PropTypes.number.isRequired,
avgProcessingTime: PropTypes.number.isRequired,
refreshButton: PropTypes.node,
refreshButton: PropTypes.node.isRequired,
};
export default Counters;

View File

@@ -0,0 +1,22 @@
.stats__table .popover__body {
left: 0;
transform: none;
}
.stats__table .popover__body:after {
left: 13px;
}
.stats__table .rt-tr-group:first-child .popover__body,
.stats__table .rt-tr-group:nth-child(2) .popover__body {
top: calc(100% + 5px);
bottom: initial;
z-index: 1;
}
.stats__table .rt-tr-group:first-child .popover__body:after,
.stats__table .rt-tr-group:nth-child(2) .popover__body:after {
top: -11px;
border-top: 6px solid transparent;
border-bottom: 6px solid #585965;
}

View File

@@ -1,34 +1,78 @@
import React from 'react';
import React, { Component } from 'react';
import ReactTable from 'react-table';
import PropTypes from 'prop-types';
import map from 'lodash/map';
import Card from '../ui/Card';
import Cell from '../ui/Cell';
import Popover from '../ui/Popover';
const QueriedDomains = props => (
<Card title="Top queried domains" subtitle="in the last 3 minutes" bodyType="card-table" refresh={props.refreshButton}>
<ReactTable
data={map(props.topQueriedDomains, (value, prop) => (
{ ip: prop, count: value }
))}
columns={[{
Header: 'IP',
accessor: 'ip',
}, {
Header: 'Request count',
accessor: 'count',
}]}
showPagination={false}
noDataText="No domains found"
minRows={6}
className="-striped -highlight card-table-overflow"
/>
</Card>
);
import { getTrackerData } from '../../helpers/trackers/trackers';
import { getPercent } from '../../helpers/helpers';
import { STATUS_COLORS } from '../../helpers/constants';
class QueriedDomains extends Component {
getPercentColor = (percent) => {
if (percent > 10) {
return STATUS_COLORS.red;
} else if (percent > 5) {
return STATUS_COLORS.yellow;
}
return STATUS_COLORS.green;
}
columns = [{
Header: 'IP',
accessor: 'ip',
Cell: (row) => {
const { value } = row;
const trackerData = getTrackerData(value);
return (
<div className="logs__row" title={value}>
<div className="logs__text">
{value}
</div>
{trackerData && <Popover data={trackerData} />}
</div>
);
},
}, {
Header: 'Requests count',
accessor: 'count',
maxWidth: 190,
Cell: ({ value }) => {
const percent = getPercent(this.props.dnsQueries, value);
const percentColor = this.getPercentColor(percent);
return (
<Cell value={value} percent={percent} color={percentColor} />
);
},
}];
render() {
return (
<Card title="Top queried domains" subtitle="for the last 24 hours" bodyType="card-table" refresh={this.props.refreshButton}>
<ReactTable
data={map(this.props.topQueriedDomains, (value, prop) => (
{ ip: prop, count: value }
))}
columns={this.columns}
showPagination={false}
noDataText="No domains found"
minRows={6}
className="-striped -highlight card-table-overflow stats__table"
/>
</Card>
);
}
}
QueriedDomains.propTypes = {
topQueriedDomains: PropTypes.object.isRequired,
refreshButton: PropTypes.node,
dnsQueries: PropTypes.number.isRequired,
refreshButton: PropTypes.node.isRequired,
};
export default QueriedDomains;

View File

@@ -1,61 +1,109 @@
import React from 'react';
import { ResponsiveLine } from '@nivo/line';
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import Card from '../ui/Card';
import Line from '../ui/Line';
const Statistics = props => (
<Card title="Statistics" subtitle="Today" bodyType="card-graph" refresh={props.refreshButton}>
{props.history ?
<ResponsiveLine
data={props.history}
margin={{
top: 50,
right: 40,
bottom: 80,
left: 80,
}}
minY="auto"
stacked={false}
curve='monotoneX'
axisBottom={{
orient: 'bottom',
tickSize: 5,
tickPadding: 5,
tickRotation: -45,
legend: 'time',
legendOffset: 50,
legendPosition: 'center',
}}
axisLeft={{
orient: 'left',
tickSize: 5,
tickPadding: 5,
tickRotation: 0,
legend: 'count',
legendOffset: -40,
legendPosition: 'center',
}}
enableArea={true}
dotSize={10}
dotColor="inherit:darker(0.3)"
dotBorderWidth={2}
dotBorderColor="#ffffff"
dotLabel="y"
dotLabelYOffset={-12}
animate={true}
motionStiffness={90}
motionDamping={15}
/>
:
<h2 className="text-muted">Empty data</h2>
}
</Card>
);
import { getPercent } from '../../helpers/helpers';
import { STATUS_COLORS } from '../../helpers/constants';
class Statistics extends Component {
render() {
const {
dnsQueries,
blockedFiltering,
replacedSafebrowsing,
replacedParental,
} = this.props;
const filteringData = [this.props.history[1]];
const queriesData = [this.props.history[2]];
const parentalData = [this.props.history[3]];
const safebrowsingData = [this.props.history[4]];
return (
<div className="row">
<div className="col-sm-6 col-lg-3">
<Card bodyType="card-wrap">
<div className="card-body-stats">
<div className="card-value card-value-stats text-blue">
{dnsQueries}
</div>
<div className="card-title-stats">
DNS Queries
</div>
</div>
<div className="card-chart-bg">
<Line data={queriesData} color={STATUS_COLORS.blue}/>
</div>
</Card>
</div>
<div className="col-sm-6 col-lg-3">
<Card bodyType="card-wrap">
<div className="card-body-stats">
<div className="card-value card-value-stats text-red">
{blockedFiltering}
</div>
<div className="card-value card-value-percent text-red">
{getPercent(dnsQueries, blockedFiltering)}
</div>
<div className="card-title-stats">
Blocked by <a href="#filters">Filters</a>
</div>
</div>
<div className="card-chart-bg">
<Line data={filteringData} color={STATUS_COLORS.red}/>
</div>
</Card>
</div>
<div className="col-sm-6 col-lg-3">
<Card bodyType="card-wrap">
<div className="card-body-stats">
<div className="card-value card-value-stats text-green">
{replacedSafebrowsing}
</div>
<div className="card-value card-value-percent text-green">
{getPercent(dnsQueries, replacedSafebrowsing)}
</div>
<div className="card-title-stats">
Blocked malware/phishing
</div>
</div>
<div className="card-chart-bg">
<Line data={safebrowsingData} color={STATUS_COLORS.green}/>
</div>
</Card>
</div>
<div className="col-sm-6 col-lg-3">
<Card bodyType="card-wrap">
<div className="card-body-stats">
<div className="card-value card-value-stats text-yellow">
{replacedParental}
</div>
<div className="card-value card-value-percent text-yellow">
{getPercent(dnsQueries, replacedParental)}
</div>
<div className="card-title-stats">
Blocked adult websites
</div>
</div>
<div className="card-chart-bg">
<Line data={parentalData} color={STATUS_COLORS.yellow}/>
</div>
</Card>
</div>
</div>
);
}
}
Statistics.propTypes = {
history: PropTypes.array.isRequired,
refreshButton: PropTypes.node,
dnsQueries: PropTypes.number.isRequired,
blockedFiltering: PropTypes.number.isRequired,
replacedSafebrowsing: PropTypes.number.isRequired,
replacedParental: PropTypes.number.isRequired,
refreshButton: PropTypes.node.isRequired,
};
export default Statistics;

View File

@@ -10,14 +10,31 @@ import BlockedDomains from './BlockedDomains';
import PageTitle from '../ui/PageTitle';
import Loading from '../ui/Loading';
import './Dashboard.css';
class Dashboard extends Component {
componentDidMount() {
this.getAllStats();
}
getAllStats = () => {
this.props.getStats();
this.props.getStatsHistory();
this.props.getTopStats();
}
getToggleFilteringButton = () => {
const { protectionEnabled } = this.props.dashboard;
const buttonText = protectionEnabled ? 'Disable' : 'Enable';
const buttonClass = protectionEnabled ? 'btn-gray' : 'btn-success';
return (
<button type="button" className={`btn btn-sm mr-2 ${buttonClass}`} onClick={() => this.props.toggleProtection(protectionEnabled)}>
{buttonText} protection
</button>
);
}
render() {
const { dashboard } = this.props;
const dashboardProcessing =
@@ -26,15 +43,14 @@ class Dashboard extends Component {
dashboard.processingStatsHistory ||
dashboard.processingTopStats;
const disableButton = <button type="button" className="btn btn-outline-secondary btn-sm mr-2" onClick={() => this.props.disableDns()}>Disable DNS</button>;
const refreshFullButton = <button type="button" className="btn btn-outline-primary btn-sm" onClick={() => this.props.getStats()}>Refresh statistics</button>;
const refreshButton = <button type="button" className="btn btn-outline-primary btn-sm card-refresh" onClick={() => this.props.getStats()}></button>;
const refreshFullButton = <button type="button" className="btn btn-outline-primary btn-sm" onClick={() => this.getAllStats()}>Refresh statistics</button>;
const refreshButton = <button type="button" className="btn btn-outline-primary btn-sm card-refresh" onClick={() => this.getAllStats()} />;
return (
<Fragment>
<PageTitle title="Dashboard">
<div className="page-title__actions">
{disableButton}
{this.getToggleFilteringButton()}
{refreshFullButton}
</div>
</PageTitle>
@@ -46,6 +62,10 @@ class Dashboard extends Component {
<Statistics
history={dashboard.statsHistory}
refreshButton={refreshButton}
dnsQueries={dashboard.stats.dns_queries}
blockedFiltering={dashboard.stats.blocked_filtering}
replacedSafebrowsing={dashboard.stats.replaced_safebrowsing}
replacedParental={dashboard.stats.replaced_parental}
/>
</div>
}
@@ -66,12 +86,14 @@ class Dashboard extends Component {
<Fragment>
<div className="col-lg-6">
<Clients
dnsQueries={dashboard.stats.dns_queries}
refreshButton={refreshButton}
topClients={dashboard.topStats.top_clients}
/>
</div>
<div className="col-lg-6">
<QueriedDomains
dnsQueries={dashboard.stats.dns_queries}
refreshButton={refreshButton}
topQueriedDomains={dashboard.topStats.top_queried_domains}
/>
@@ -80,6 +102,9 @@ class Dashboard extends Component {
<BlockedDomains
refreshButton={refreshButton}
topBlockedDomains={dashboard.topStats.top_blocked_domains}
blockedFiltering={dashboard.stats.blocked_filtering}
replacedSafebrowsing={dashboard.stats.replaced_safebrowsing}
replacedParental={dashboard.stats.replaced_parental}
/>
</div>
</Fragment>
@@ -95,9 +120,10 @@ Dashboard.propTypes = {
getStats: PropTypes.func,
getStatsHistory: PropTypes.func,
getTopStats: PropTypes.func,
disableDns: PropTypes.func,
dashboard: PropTypes.object,
isCoreRunning: PropTypes.bool,
getFiltering: PropTypes.func,
toggleProtection: PropTypes.func,
};
export default Dashboard;

View File

@@ -20,14 +20,14 @@ export default class UserRules extends Component {
subtitle="Enter one rule on a line. You can use either adblock rules or hosts files syntax."
>
<form onSubmit={this.handleSubmit}>
<textarea className="form-control" value={this.props.userRules} onChange={this.handleChange} />
<textarea className="form-control form-control--textarea-large" value={this.props.userRules} onChange={this.handleChange} />
<div className="card-actions">
<button
className="btn btn-success btn-standart"
type="submit"
onClick={this.handleSubmit}
>
Apply...
Apply
</button>
</div>
</form>
@@ -44,7 +44,7 @@ export default class UserRules extends Component {
domain and all its subdomains
</li>
<li>
<code>example.org 127.0.0.1</code> - AdGuard DNS will now return
<code>127.0.0.1 example.org</code> - AdGuard Home will now return
127.0.0.1 address for the example.org domain (but not its subdomains).
</li>
<li>

View File

@@ -39,17 +39,19 @@ class Filters extends Component {
width: 90,
className: 'text-center',
}, {
Header: 'Filter name',
Header: 'Name',
accessor: 'name',
Cell: ({ value }) => (<div className="logs__row logs__row--overflow"><span className="logs__text" title={value}>{value}</span></div>),
}, {
Header: 'Host file URL',
Header: 'Filter URL',
accessor: 'url',
Cell: ({ value }) => (<div className="logs__row logs__row--overflow"><a href={value} target='_blank' rel='noopener noreferrer' className="link logs__text">{value}</a></div>),
}, {
Header: 'Rules count',
accessor: 'rulesCount',
className: 'text-center',
}, {
Header: 'Last time update',
Header: 'Last time updated',
accessor: 'lastUpdated',
className: 'text-center',
}, {
@@ -71,8 +73,8 @@ class Filters extends Component {
<div className="row">
<div className="col-md-12">
<Card
title="Blocking filters and hosts files"
subtitle="AdGuard DNS understands basic adblock rules and hosts files syntax."
title="Filters and hosts blocklists"
subtitle="AdGuard Home understands basic adblock rules and hosts files syntax."
>
<ReactTable
data={filters}
@@ -102,7 +104,7 @@ class Filters extends Component {
addFilter={this.props.addFilter}
isFilterAdded={this.props.filtering.isFilterAdded}
title="New filter subscription"
inputDescription="Enter valid URL or file path of the filter into field above. You will be subscribed to that filter."
inputDescription="Enter a valid URL to a filter subscription or a hosts file."
/>
</div>
);

View File

@@ -67,13 +67,17 @@
}
.nav-version {
padding: 16px 0;
font-size: 0.85rem;
padding: 7px 0;
font-size: 0.80rem;
text-align: right;
}
.nav-version__value {
font-weight: 600;
}
.header-brand-img {
height: 26px;
height: 32px;
}
@media screen and (min-width: 992px) {
@@ -103,7 +107,7 @@
.nav-version {
padding: 0;
font-size: 0.9rem;
font-size: 0.85rem;
}
}

View File

@@ -4,6 +4,8 @@ import PropTypes from 'prop-types';
import enhanceWithClickOutside from 'react-click-outside';
import classnames from 'classnames';
import { REPOSITORY } from '../../helpers/constants';
class Menu extends Component {
handleClickOutside = () => {
this.props.closeMenu();
@@ -53,6 +55,12 @@ class Menu extends Component {
Query Log
</NavLink>
</li>
<li className="nav-item">
<a href={`${REPOSITORY.URL}/wiki`} className="nav-link" target="_blank" rel="noopener noreferrer">
<svg className="nav-icon" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#66b574" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12" y2="17"></line></svg>
FAQ
</a>
</li>
</ul>
</div>
</Fragment>

View File

@@ -5,7 +5,12 @@ export default function Version(props) {
const { dnsVersion, dnsAddress, dnsPort } = props;
return (
<div className="nav-version">
v.{dnsVersion} / address: {dnsAddress}:{dnsPort}
<div className="nav-version__text">
version: <span className="nav-version__value">{dnsVersion}</span>
</div>
<div className="nav-version__text">
address: <span className="nav-version__value">{dnsAddress}:{dnsPort}</span>
</div>
</div>
);
}

View File

@@ -26,8 +26,8 @@ class Header extends Component {
const { dashboard } = this.props;
const badgeClass = classnames({
'badge dns-status': true,
'badge-success': dashboard.isCoreRunning,
'badge-danger': !dashboard.isCoreRunning,
'badge-success': dashboard.protectionEnabled,
'badge-danger': !dashboard.protectionEnabled,
});
return (
@@ -42,9 +42,9 @@ class Header extends Component {
<Link to="/" className="nav-link pl-0 pr-1">
<img src={logo} alt="" className="header-brand-img" />
</Link>
{!dashboard.proccessing &&
{!dashboard.proccessing && dashboard.isCoreRunning &&
<span className={badgeClass}>
{dashboard.isCoreRunning ? 'ON' : 'OFF'}
{dashboard.protectionEnabled ? 'ON' : 'OFF'}
</span>
}
</div>

View File

@@ -1 +1 @@
<svg viewBox="0 0 118 26" xmlns="http://www.w3.org/2000/svg"><path fill="#232323" d="M92.535 18.314l-.897-2.259h-4.47l-.849 2.259h-3.034L88.13 6.809h2.708l4.796 11.505h-3.1zm-3.1-8.434l-1.468 3.949h2.904L89.435 9.88zm-6.607 4.095c0 .693-.117 1.324-.35 1.893a4.115 4.115 0 0 1-1.004 1.463 4.63 4.63 0 0 1-1.574.95c-.614.228-1.297.341-2.047.341-.761 0-1.447-.113-2.056-.34a4.468 4.468 0 0 1-1.55-.951 4.126 4.126 0 0 1-.978-1.463 5.038 5.038 0 0 1-.343-1.893V6.809H75.7v6.939c0 .314.041.612.123.893.081.282.206.534.375.756.169.222.392.398.669.528s.612.195 1.003.195c.392 0 .726-.065 1.003-.195a1.83 1.83 0 0 0 .677-.528 2.1 2.1 0 0 0 .376-.756c.076-.281.114-.58.114-.893v-6.94h2.79v7.167zm-11.446 3.64a8.898 8.898 0 0 1-1.982.715 10.43 10.43 0 0 1-2.472.276c-.924 0-1.775-.146-2.553-.439a5.895 5.895 0 0 1-2.006-1.235 5.63 5.63 0 0 1-1.314-1.909c-.315-.742-.473-1.568-.473-2.478 0-.92.16-1.755.482-2.502a5.567 5.567 0 0 1 1.33-1.91 5.893 5.893 0 0 1 1.99-1.21 7.044 7.044 0 0 1 2.463-.423c.913 0 1.762.138 2.545.414.783.277 1.419.648 1.908 1.114l-1.762 1.998a3.05 3.05 0 0 0-1.076-.772c-.446-.2-.952-.3-1.517-.3-.49 0-.941.09-1.354.268a3.256 3.256 0 0 0-1.077.747 3.39 3.39 0 0 0-.71 1.138 3.977 3.977 0 0 0-.253 1.438c0 .53.077 1.018.229 1.463.152.444.378.826.677 1.145.299.32.669.569 1.11.748.44.178.943.268 1.508.268.326 0 .636-.025.93-.073.294-.05.566-.128.816-.236v-2.096h-2.203V11.52h4.764v6.094zm46.107-5.086c0 1.007-.188 1.877-.563 2.608a5.262 5.262 0 0 1-1.484 1.804 6.199 6.199 0 0 1-2.08 1.04 8.459 8.459 0 0 1-2.35.333h-4.306V6.809h4.176c.816 0 1.62.095 2.414.284.794.19 1.501.504 2.121.943.62.438 1.12 1.026 1.5 1.763.382.736.572 1.646.572 2.73zm-2.904 0c0-.65-.106-1.19-.318-1.617a2.724 2.724 0 0 0-.848-1.024 3.4 3.4 0 0 0-1.208-.544 5.955 5.955 0 0 0-1.394-.163h-1.387v6.728h1.321c.5 0 .982-.057 1.444-.17.462-.115.87-.301 1.224-.562a2.78 2.78 0 0 0 .848-1.04c.212-.433.318-.97.318-1.608zm-55.226 0c0 1.007-.188 1.877-.563 2.608a5.262 5.262 0 0 1-1.484 1.804 6.199 6.199 0 0 1-2.08 1.04 8.459 8.459 0 0 1-2.35.333h-4.306V6.809h4.176c.816 0 1.62.095 2.414.284.794.19 1.501.504 2.121.943.62.438 1.12 1.026 1.5 1.763.382.736.572 1.646.572 2.73zm-2.904 0c0-.65-.106-1.19-.318-1.617a2.724 2.724 0 0 0-.848-1.024 3.4 3.4 0 0 0-1.207-.544 5.955 5.955 0 0 0-1.395-.163H51.3v6.728h1.321c.5 0 .982-.057 1.444-.17.462-.115.87-.301 1.224-.562a2.78 2.78 0 0 0 .848-1.04c.212-.433.318-.97.318-1.608zm-11.86 5.785l-.897-2.259h-4.47l-.848 2.259h-3.034L40.19 6.809h2.708l4.796 11.505h-3.1zm-3.1-8.434l-1.467 3.949h2.903L41.496 9.88zm61.203 8.434l-2.496-4.566h-.946v4.566h-2.74V6.809h4.404c.555 0 1.096.057 1.623.17.528.114 1 .306 1.42.577.418.271.752.629 1.003 1.073.25.444.375.996.375 1.657 0 .78-.212 1.436-.636 1.966-.425.531-1.012.91-1.762 1.138l3.018 4.924h-3.263zm-.114-7.979c0-.27-.057-.49-.171-.658a1.172 1.172 0 0 0-.44-.39 1.919 1.919 0 0 0-.604-.187 4.469 4.469 0 0 0-.645-.049H99.24v2.681h1.321c.228 0 .462-.018.701-.056.24-.038.457-.106.653-.204.196-.097.356-.238.481-.422s.188-.422.188-.715z"/><path fill="#68bc71" d="M12.651 0C8.697 0 3.927.93 0 2.977c0 4.42-.054 15.433 12.651 22.958C25.357 18.41 25.303 7.397 25.303 2.977 21.376.93 16.606 0 12.651 0z"/><path fill="#67b279" d="M12.638 25.927C-.054 18.403 0 7.396 0 2.977 3.923.932 8.687.002 12.638 0v25.927z"/><path fill="#fff" d="M12.19 17.305l7.65-10.311c-.56-.45-1.052-.133-1.323.113h-.01l-6.379 6.636-2.403-2.892c-1.147-1.325-2.705-.314-3.07-.047l5.535 6.5"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="164" height="41" viewBox="0 0 164 41"><g fill-rule="evenodd"><path d="M129.984 22l-1.162-2.945h-5.792L121.931 22H118l6.277-15h3.509L134 22h-4.016zm-4.016-10.996l-1.902 5.149h3.762l-1.86-5.149zM117 16.1c0 .88-.153 1.682-.46 2.404a5.223 5.223 0 0 1-1.318 1.857c-.57.516-1.26.918-2.066 1.207-.807.289-1.703.433-2.688.433-1 0-1.9-.144-2.699-.433-.8-.29-1.477-.691-2.034-1.207a5.232 5.232 0 0 1-1.285-1.857c-.3-.722-.45-1.524-.45-2.404V7h3.64v8.81c0 .4.054.777.161 1.135.108.358.272.677.493.96.221.281.514.505.878.67.364.165.803.248 1.317.248.514 0 .953-.083 1.317-.248.365-.165.66-.389.89-.67.228-.283.392-.602.492-.96.1-.358.15-.736.15-1.135V7H117v9.099zm-16 4.673c-.733.362-1.59.658-2.57.886-.98.228-2.047.342-3.203.342-1.199 0-2.302-.181-3.31-.544-1.008-.362-1.875-.872-2.601-1.53a6.977 6.977 0 0 1-1.703-2.366c-.409-.92-.613-1.943-.613-3.07 0-1.141.208-2.175.624-3.1a6.903 6.903 0 0 1 1.723-2.367 7.71 7.71 0 0 1 2.58-1.5C92.914 7.174 93.98 7 95.121 7c1.184 0 2.284.171 3.299.513 1.015.343 1.84.802 2.474 1.38l-2.284 2.476c-.352-.39-.817-.708-1.395-.956-.579-.249-1.234-.373-1.967-.373-.635 0-1.22.111-1.756.332a4.23 4.23 0 0 0-1.395.927 4.178 4.178 0 0 0-.92 1.41 4.734 4.734 0 0 0-.328 1.78c0 .659.099 1.263.296 1.813.197.55.49 1.024.878 1.42.387.395.867.704 1.438.926.57.221 1.223.332 1.956.332.423 0 .825-.03 1.205-.09.381-.061.733-.158 1.058-.293V16h-2.855v-2.779H101v7.55zm63-6.314c0 1.313-.244 2.447-.73 3.4a6.855 6.855 0 0 1-1.928 2.352 8.035 8.035 0 0 1-2.7 1.356 10.94 10.94 0 0 1-3.05.434H150V7h5.422c1.06 0 2.104.124 3.135.37a7.866 7.866 0 0 1 2.753 1.23c.805.572 1.454 1.338 1.949 2.298.494.96.741 2.147.741 3.56zm-3.77 0c0-.848-.138-1.55-.413-2.108a3.549 3.549 0 0 0-1.101-1.335 4.405 4.405 0 0 0-1.568-.71 7.7 7.7 0 0 0-1.81-.212h-1.8v8.771h1.715c.65 0 1.274-.074 1.874-.222a4.43 4.43 0 0 0 1.589-.731 3.62 3.62 0 0 0 1.1-1.356c.276-.565.414-1.264.414-2.097zm-75.23 0c0 1.313-.244 2.447-.73 3.4a6.855 6.855 0 0 1-1.928 2.352 8.035 8.035 0 0 1-2.7 1.356 10.94 10.94 0 0 1-3.05.434H71V7h5.422c1.06 0 2.104.124 3.135.37A7.866 7.866 0 0 1 82.31 8.6c.805.572 1.454 1.338 1.949 2.298.494.96.741 2.147.741 3.56zm-3.77 0c0-.848-.138-1.55-.413-2.108a3.549 3.549 0 0 0-1.101-1.335 4.405 4.405 0 0 0-1.568-.71 7.7 7.7 0 0 0-1.81-.212h-1.8v8.771h1.715c.65 0 1.274-.074 1.874-.222a4.43 4.43 0 0 0 1.589-.731 3.62 3.62 0 0 0 1.1-1.356c.276-.565.414-1.264.414-2.097zM65.984 22l-1.162-2.945H59.03L57.931 22H54l6.277-15h3.509L70 22h-4.016zm-4.016-10.996l-1.902 5.149h3.762l-1.86-5.149zM143.855 22l-3.171-5.953h-1.202V22H136V7h5.596c.705 0 1.392.074 2.062.222.67.149 1.271.4 1.803.753a3.9 3.9 0 0 1 1.275 1.398c.318.579.476 1.3.476 2.16 0 1.018-.269 1.872-.808 2.564-.539.693-1.285 1.187-2.238 1.484L148 22h-4.145zm-.145-10.403c0-.353-.073-.639-.218-.858a1.502 1.502 0 0 0-.56-.508 2.393 2.393 0 0 0-.766-.244 5.535 5.535 0 0 0-.819-.063h-1.886v3.495h1.679c.29 0 .587-.024.891-.074.304-.05.58-.137.83-.264.248-.128.452-.311.61-.551.16-.24.239-.551.239-.933zM55 37.851v-8.702h.951v3.866h4.866V29.15h.952v8.702h-.952v-3.916h-4.866v3.916H55zM68.068 38c-2.565 0-4.288-2.076-4.288-4.5 0-2.4 1.747-4.5 4.312-4.5 2.565 0 4.288 2.076 4.288 4.5 0 2.4-1.747 4.5-4.312 4.5zm.024-.907c1.927 0 3.3-1.592 3.3-3.593 0-1.977-1.397-3.593-3.324-3.593-1.927 0-3.3 1.592-3.3 3.593 0 1.977 1.397 3.593 3.324 3.593zm6.3.758v-8.702h.963l3.07 4.749 3.072-4.749h.964v8.702h-.952v-7.049l-3.071 4.662h-.048l-3.071-4.65v7.037h-.928zm10.453 0v-8.702h6.095v.895h-5.143v2.971h4.6v.895h-4.6v3.046H91v.895h-6.155z"/><path fill-rule="nonzero" d="M2.831 14.045c.775 4.287 2.266 8.333 4.685 12.143 2.958 4.659 7.21 8.797 12.984 12.319 5.774-3.522 10.026-7.66 12.984-12.319 2.42-3.81 3.91-7.856 4.685-12.143.489-2.706.644-4.844.672-8.003C33.368 3.522 26.636 2.14 20.5 2.14c-6.137 0-12.869 1.381-18.341 3.9.028 3.16.183 5.298.672 8.004zM20.5 0C26.908 0 34.637 1.47 41 4.706c0 6.988.087 24.398-20.5 36.294C-.088 29.104 0 11.694 0 4.706 6.363 1.47 14.092 0 20.5 0z"/><path d="M20.234 27L33 11.344c-.935-.682-1.756-.2-2.208.172l-.016.001-10.644 10.076-4.01-4.392c-1.913-2.011-4.514-.477-5.122-.072L20.234 27"/></g></svg>

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -0,0 +1,89 @@
.logs__row {
position: relative;
display: flex;
align-items: center;
min-height: 26px;
}
.logs__row--overflow {
overflow: hidden;
}
.logs__row .list-unstyled {
margin-bottom: 0;
overflow: hidden;
}
.logs__text,
.logs__row .list-unstyled li {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.logs__row .tooltip-custom {
top: 0;
margin-left: 0;
margin-right: 5px;
}
.logs__action {
position: absolute;
top: 10px;
right: 15px;
background-color: #fff;
border-radius: 4px;
transition: opacity 0.2s ease, visibility 0.2s ease;
visibility: hidden;
opacity: 0;
}
.logs__table .rt-td {
position: relative;
}
.logs__table .rt-tr:hover .logs__action {
visibility: visible;
opacity: 1;
}
.logs__table .rt-tr-group:first-child .tooltip-custom:before {
top: calc(100% + 12px);
bottom: initial;
z-index: 1;
}
.logs__table .rt-tr-group:first-child .tooltip-custom:after {
top: initial;
bottom: -4px;
border-top: 6px solid transparent;
border-bottom: 6px solid #585965;
}
.logs__table .rt-tr-group:first-child .popover__body {
top: calc(100% + 5px);
bottom: initial;
z-index: 1;
}
.logs__table .rt-tr-group:first-child .popover__body:after {
top: -11px;
border-top: 6px solid transparent;
border-bottom: 6px solid #585965;
}
.logs__table .rt-thead.-filters input,
.logs__table .rt-thead.-filters select {
padding: 6px 7px;
border-radius: 3px;
font-size: 0.9375rem;
line-height: 1.6;
color: #495057;
border: 1px solid rgba(0, 40, 100, 0.12);
}
.logs__table .rt-thead.-filters input:focus,
.logs__table .rt-thead.-filters select:focus {
border-color: #1991eb;
box-shadow: 0 0 0 2px rgba(70, 127, 207, 0.25);
}

View File

@@ -1,21 +1,25 @@
import React, { Component } from 'react';
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import ReactTable from 'react-table';
import { saveAs } from 'file-saver/FileSaver';
import escapeRegExp from 'lodash/escapeRegExp';
import endsWith from 'lodash/endsWith';
import { formatTime } from '../../helpers/helpers';
import { getTrackerData } from '../../helpers/trackers/trackers';
import PageTitle from '../ui/PageTitle';
import Card from '../ui/Card';
import Loading from '../ui/Loading';
import { normalizeLogs } from '../../helpers/helpers';
import Tooltip from '../ui/Tooltip';
import Popover from '../ui/Popover';
import './Logs.css';
const DOWNLOAD_LOG_FILENAME = 'dns-logs.txt';
class Logs extends Component {
componentDidMount() {
// get logs on initialization if queryLogIsEnabled
if (this.props.dashboard.queryLogEnabled) {
this.props.getLogs();
}
this.getLogs();
this.props.getFilteringStatus();
}
componentDidUpdate(prevProps) {
@@ -25,46 +29,189 @@ class Logs extends Component {
}
}
getLogs = () => {
// get logs on initialization if queryLogIsEnabled
if (this.props.dashboard.queryLogEnabled) {
this.props.getLogs();
}
}
renderTooltip(isFiltered, rule) {
if (rule) {
return (isFiltered && <Tooltip text={rule}/>);
}
return '';
}
toggleBlocking = (type, domain) => {
const { userRules } = this.props.filtering;
const lineEnding = !endsWith(userRules, '\n') ? '\n' : '';
const baseRule = `||${domain}^$important`;
const baseUnblocking = `@@${baseRule}`;
const blockingRule = type === 'block' ? baseUnblocking : baseRule;
const unblockingRule = type === 'block' ? baseRule : baseUnblocking;
const preparedBlockingRule = new RegExp(`(^|\n)${escapeRegExp(blockingRule)}($|\n)`);
const preparedUnblockingRule = new RegExp(`(^|\n)${escapeRegExp(unblockingRule)}($|\n)`);
if (userRules.match(preparedBlockingRule)) {
this.props.setRules(userRules.replace(`${blockingRule}`, ''));
this.props.addSuccessToast(`Rule removed from the custom filtering rules: ${blockingRule}`);
} else if (!userRules.match(preparedUnblockingRule)) {
this.props.setRules(`${userRules}${lineEnding}${unblockingRule}\n`);
this.props.addSuccessToast(`Rule added to the custom filtering rules: ${unblockingRule}`);
}
this.props.getFilteringStatus();
}
renderBlockingButton(isFiltered, domain) {
const buttonClass = isFiltered ? 'btn-outline-secondary' : 'btn-outline-danger';
const buttonText = isFiltered ? 'Unblock' : 'Block';
return (
<div className="logs__action">
<button
type="button"
className={`btn btn-sm ${buttonClass}`}
onClick={() => this.toggleBlocking(buttonText.toLowerCase(), domain)}
>
{buttonText}
</button>
</div>
);
}
renderLogs(logs) {
const columns = [{
Header: 'Time',
accessor: 'time',
maxWidth: 150,
maxWidth: 110,
filterable: false,
Cell: ({ value }) => (<div className="logs__row"><span className="logs__text" title={value}>{formatTime(value)}</span></div>),
}, {
Header: 'Domain name',
accessor: 'domain',
Cell: (row) => {
const response = row.value;
const trackerData = getTrackerData(response);
return (
<div className="logs__row" title={response}>
<div className="logs__text">
{response}
</div>
{trackerData && <Popover data={trackerData}/>}
</div>
);
},
}, {
Header: 'Type',
accessor: 'type',
maxWidth: 100,
maxWidth: 60,
}, {
Header: 'Response',
accessor: 'response',
Cell: (row) => {
const responses = row.value;
const { reason } = row.original;
const isFiltered = row ? reason.indexOf('Filtered') === 0 : false;
const parsedFilteredReason = reason.replace('Filtered', 'Filtered by ');
const rule = row && row.original && row.original.rule;
if (isFiltered) {
return (
<div className="logs__row">
{this.renderTooltip(isFiltered, rule)}
<span className="logs__text" title={parsedFilteredReason}>
{parsedFilteredReason}
</span>
</div>
);
}
if (responses.length > 0) {
const liNodes = responses.map((response, index) =>
(<li key={index}>{response}</li>));
return (<ul className="list-unstyled">{liNodes}</ul>);
(<li key={index} title={response}>{response}</li>));
return (
<div className="logs__row">
{this.renderTooltip(isFiltered, rule)}
<ul className="list-unstyled">{liNodes}</ul>
</div>
);
}
return 'Empty';
return (
<div className="logs__row">
{this.renderTooltip(isFiltered, rule)}
<span>Empty</span>
</div>
);
},
}];
filterMethod: (filter, row) => {
if (filter.value === 'filtered') {
// eslint-disable-next-line no-underscore-dangle
return row._original.reason.indexOf('Filtered') === 0;
}
return true;
},
Filter: ({ filter, onChange }) =>
<select
onChange={event => onChange(event.target.value)}
className="form-control"
value={filter ? filter.value : 'all'}
>
<option value="all">Show all</option>
<option value="filtered">Show filtered</option>
</select>,
}, {
Header: 'Client',
accessor: 'client',
maxWidth: 250,
Cell: (row) => {
const { reason } = row.original;
const isFiltered = row ? reason.indexOf('Filtered') === 0 : false;
return (
<Fragment>
<div className="logs__row">
{row.value}
</div>
{this.renderBlockingButton(isFiltered, row.original.domain)}
</Fragment>
);
},
},
];
if (logs) {
const normalizedLogs = normalizeLogs(logs);
return (<ReactTable
data={normalizedLogs}
className='logs__table'
filterable
data={logs}
columns={columns}
showPagination={false}
showPagination={true}
defaultPageSize={50}
minRows={7}
noDataText="No logs found"
defaultFilterMethod={(filter, row) => {
const id = filter.pivotId || filter.id;
return row[id] !== undefined ?
String(row[id]).indexOf(filter.value) !== -1 : true;
}}
defaultSorted={[
{
id: 'time',
desc: true,
},
]}
getTrProps={(_state, rowInfo) => {
// highlight filtered requests
if (!rowInfo) {
return {};
}
return {
className: (rowInfo.original.reason.indexOf('Filtered') === 0 ? 'red' : ''),
};
}}
/>);
}
return undefined;
@@ -79,34 +226,53 @@ class Logs extends Component {
};
renderButtons(queryLogEnabled) {
return (<div className="card-actions-top">
<button
className="btn btn-success btn-standart mr-2"
type="submit"
onClick={() => this.props.toggleLogStatus(queryLogEnabled)}
>{queryLogEnabled ? 'Disable log' : 'Enable log'}</button>
{queryLogEnabled &&
<button
className="btn btn-primary btn-standart"
type="submit"
onClick={this.handleDownloadButton}
>Download log file</button> }
</div>);
if (queryLogEnabled) {
return (
<Fragment>
<button
className="btn btn-gray btn-sm mr-2"
type="submit"
onClick={() => this.props.toggleLogStatus(queryLogEnabled)}
>Disable log</button>
<button
className="btn btn-primary btn-sm mr-2"
type="submit"
onClick={this.handleDownloadButton}
>Download log file</button>
<button
className="btn btn-outline-primary btn-sm"
type="submit"
onClick={this.getLogs}
>Refresh</button>
</Fragment>
);
}
return (
<button
className="btn btn-success btn-sm mr-2"
type="submit"
onClick={() => this.props.toggleLogStatus(queryLogEnabled)}
>Enable log</button>
);
}
render() {
const { queryLogs, dashboard } = this.props;
const { queryLogEnabled } = dashboard;
return (
<div>
<PageTitle title="Query Log" subtitle="DNS queries log" />
<Fragment>
<PageTitle title="Query Log" subtitle="Last 5000 DNS queries">
<div className="page-title__actions">
{this.renderButtons(queryLogEnabled)}
</div>
</PageTitle>
<Card>
{this.renderButtons(queryLogEnabled)}
{queryLogEnabled && queryLogs.processing && <Loading />}
{queryLogEnabled && !queryLogs.processing &&
{queryLogEnabled && queryLogs.getLogsProcessing && <Loading />}
{queryLogEnabled && !queryLogs.getLogsProcessing &&
this.renderLogs(queryLogs.logs)}
</Card>
</div>
</Fragment>
);
}
}
@@ -117,6 +283,11 @@ Logs.propTypes = {
dashboard: PropTypes.object,
toggleLogStatus: PropTypes.func,
downloadQueryLog: PropTypes.func,
getFilteringStatus: PropTypes.func,
filtering: PropTypes.object,
userRules: PropTypes.string,
setRules: PropTypes.func,
addSuccessToast: PropTypes.func,
};
export default Logs;

View File

@@ -10,3 +10,11 @@
padding-left: 20px;
padding-right: 20px;
}
.form-control--textarea {
min-height: 110px;
}
.form-control--textarea-large {
min-height: 240px;
}

View File

@@ -1,5 +1,6 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import Card from '../ui/Card';
export default class Upstream extends Component {
@@ -13,22 +14,38 @@ export default class Upstream extends Component {
this.props.handleUpstreamSubmit();
};
handleTest = () => {
this.props.handleUpstreamTest();
}
render() {
const testButtonClass = classnames({
'btn btn-primary btn-standart mr-2': true,
'btn btn-primary btn-standart mr-2 btn-loading': this.props.processingTestUpstream,
});
return (
<Card
title="Upstream DNS servers"
subtitle="If you keep this field empty, AdGuard will use <a href='https://1.1.1.1/' target='_blank'>Cloudflare DNS</a> as an upstream."
subtitle="If you keep this field empty, AdGuard Home will use <a href='https://1.1.1.1/' target='_blank'>Cloudflare DNS</a> as an upstream. Use tls:// prefix for DNS over TLS servers."
bodyType="card-body box-body--settings"
>
<div className="row">
<div className="col">
<form>
<textarea
className="form-control"
value={this.props.upstream}
className="form-control form-control--textarea"
value={this.props.upstreamDns}
onChange={this.handleChange}
/>
<div className="card-actions">
<button
className={testButtonClass}
type="button"
onClick={this.handleTest}
>
Test upstreams
</button>
<button
className="btn btn-success btn-standart"
type="submit"
@@ -46,7 +63,9 @@ export default class Upstream extends Component {
}
Upstream.propTypes = {
upstream: PropTypes.string,
upstreamDns: PropTypes.string,
processingTestUpstream: PropTypes.bool,
handleUpstreamChange: PropTypes.func,
handleUpstreamSubmit: PropTypes.func,
handleUpstreamTest: PropTypes.func,
};

View File

@@ -17,17 +17,17 @@ export default class Settings extends Component {
safebrowsing: {
enabled: false,
title: 'Use AdGuard browsing security web service',
subtitle: 'AdGuard DNS will check if domain is blacklisted by the browsing security web service (sb.adtidy.org). It will use privacy-safe lookup API to do the check.',
subtitle: 'AdGuard Home will check if domain is blacklisted by the browsing security web service. It will use privacy-friendly lookup API to perform the check: only a short prefix of the domain name SHA256 hash is sent to the server.',
},
parental: {
enabled: false,
title: 'Use AdGuard parental control web service',
subtitle: 'AdGuard DNS will check if domain contains adult materials. It uses the same privacy-friendly API as the browsing security web service.',
subtitle: 'AdGuard Home will check if domain contains adult materials. It uses the same privacy-friendly API as the browsing security web service.',
},
safesearch: {
enabled: false,
title: 'Enforce safe search',
subtitle: 'AdGuard DNS can enforce safe search in the major search engines: Google, Bing, Yandex.',
subtitle: 'AdGuard Home can enforce safe search in the following search engines: Google, Bing, Yandex.',
},
};
@@ -36,11 +36,19 @@ export default class Settings extends Component {
}
handleUpstreamChange = (value) => {
this.props.handleUpstreamChange({ upstream: value });
this.props.handleUpstreamChange({ upstreamDns: value });
};
handleUpstreamSubmit = () => {
this.props.setUpstream(this.props.settings.upstream);
this.props.setUpstream(this.props.dashboard.upstreamDns);
};
handleUpstreamTest = () => {
if (this.props.dashboard.upstreamDns.length > 0) {
this.props.testUpstream(this.props.dashboard.upstreamDns);
} else {
this.props.addErrorToast({ error: 'No servers specified' });
}
};
renderSettings = (settings) => {
@@ -61,7 +69,8 @@ export default class Settings extends Component {
}
render() {
const { settings, upstream } = this.props;
const { settings } = this.props;
const { upstreamDns } = this.props.dashboard;
return (
<Fragment>
<PageTitle title="Settings" />
@@ -76,9 +85,11 @@ export default class Settings extends Component {
</div>
</Card>
<Upstream
upstream={upstream}
upstreamDns={upstreamDns}
processingTestUpstream={settings.processingTestUpstream}
handleUpstreamChange={this.handleUpstreamChange}
handleUpstreamSubmit={this.handleUpstreamSubmit}
handleUpstreamTest={this.handleUpstreamTest}
/>
</div>
</div>

View File

@@ -0,0 +1,60 @@
.toasts {
position: fixed;
right: 24px;
bottom: 24px;
z-index: 10;
width: 345px;
}
.toast {
display: flex;
align-items: flex-start;
margin-bottom: 12px;
padding: 16px;
font-weight: 600;
color: #ffffff;
border-radius: 4px;
background-color: rgba(236, 53, 53, 0.75);
}
.toast--success {
background-color: rgba(90, 173, 99, 0.75);
}
.toast:last-child {
margin-bottom: 0;
}
.toast__content {
flex: 1 1 auto;
margin: 0 12px 0 0;
text-overflow: ellipsis;
overflow: hidden;
}
.toast__dismiss {
display: block;
flex: 0 0 auto;
padding: 0;
background: transparent;
border: 0;
cursor: pointer;
}
.toast-enter {
opacity: 0.01;
}
.toast-enter-active {
opacity: 1;
transition: all 0.3s ease-out;
}
.toast-exit {
opacity: 1;
}
.toast-exit-active {
opacity: 0.01;
transition: all 0.3s ease-out;
}

View File

@@ -0,0 +1,38 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
class Toast extends Component {
componentDidMount() {
const timeout = this.props.type === 'error' ? 30000 : 5000;
setTimeout(() => {
this.props.removeToast(this.props.id);
}, timeout);
}
shouldComponentUpdate() {
return false;
}
render() {
return (
<div className={`toast toast--${this.props.type}`}>
<p className="toast__content">
{this.props.message}
</p>
<button className="toast__dismiss" onClick={() => this.props.removeToast(this.props.id)}>
<svg stroke="#fff" fill="none" width="20" height="20" strokeWidth="2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="m18 6-12 12"/><path d="m6 6 12 12"/></svg>
</button>
</div>
);
}
}
Toast.propTypes = {
id: PropTypes.string.isRequired,
message: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
removeToast: PropTypes.func.isRequired,
};
export default Toast;

View File

@@ -0,0 +1,42 @@
import { connect } from 'react-redux';
import React from 'react';
import PropTypes from 'prop-types';
import { CSSTransition, TransitionGroup } from 'react-transition-group';
import * as actionCreators from '../../actions';
import Toast from './Toast';
import './Toast.css';
const Toasts = props => (
<TransitionGroup className="toasts">
{props.toasts.notices && props.toasts.notices.map((toast) => {
const { id } = toast;
return (
<CSSTransition
key={id}
timeout={500}
classNames="toast"
>
<Toast removeToast={props.removeToast} {...toast} />
</CSSTransition>
);
})}
</TransitionGroup>
);
Toasts.propTypes = {
toasts: PropTypes.object,
removeToast: PropTypes.func,
};
const mapStateToProps = (state) => {
const { toasts } = state;
const props = { toasts };
return props;
};
export default connect(
mapStateToProps,
actionCreators,
)(Toasts);

View File

@@ -26,7 +26,6 @@
display: flex;
align-items: center;
justify-content: center;
height: 400px;
}
.card-body--status {
@@ -40,11 +39,48 @@
background-size: 14px;
background-position: center;
background-repeat: no-repeat;
background-image: url('data:image/svg+xml;base64,PHN2ZyBmaWxsPSJub25lIiBoZWlnaHQ9IjI0IiBzdHJva2U9IiM0NjdmY2YiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSIyIiB2aWV3Qm94PSIwIDAgMjQgMjQiIHdpZHRoPSIyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJtMjMgNHY2aC02Ii8+PHBhdGggZD0ibTEgMjB2LTZoNiIvPjxwYXRoIGQ9Im0zLjUxIDlhOSA5IDAgMCAxIDE0Ljg1LTMuMzZsNC42NCA0LjM2bS0yMiA0IDQuNjQgNC4zNmE5IDkgMCAwIDAgMTQuODUtMy4zNiIvPjwvc3ZnPg==');
background-image: url("data:image/svg+xml;base64,PHN2ZyBmaWxsPSJub25lIiBoZWlnaHQ9IjI0IiBzdHJva2U9IiM0NjdmY2YiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSIyIiB2aWV3Qm94PSIwIDAgMjQgMjQiIHdpZHRoPSIyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJtMjMgNHY2aC02Ii8+PHBhdGggZD0ibTEgMjB2LTZoNiIvPjxwYXRoIGQ9Im0zLjUxIDlhOSA5IDAgMCAxIDE0Ljg1LTMuMzZsNC42NCA0LjM2bS0yMiA0IDQuNjQgNC4zNmE5IDkgMCAwIDAgMTQuODUtMy4zNiIvPjwvc3ZnPg==");
}
.card-refresh:hover,
.card-refresh:not(:disabled):not(.disabled):active,
.card-refresh:focus:active {
background-image: url('data:image/svg+xml;base64,PHN2ZyBmaWxsPSJub25lIiBoZWlnaHQ9IjI0IiBzdHJva2U9IiNmZmYiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSIyIiB2aWV3Qm94PSIwIDAgMjQgMjQiIHdpZHRoPSIyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJtMjMgNHY2aC02Ii8+PHBhdGggZD0ibTEgMjB2LTZoNiIvPjxwYXRoIGQ9Im0zLjUxIDlhOSA5IDAgMCAxIDE0Ljg1LTMuMzZsNC42NCA0LjM2bS0yMiA0IDQuNjQgNC4zNmE5IDkgMCAwIDAgMTQuODUtMy4zNiIvPjwvc3ZnPg==');
background-image: url("data:image/svg+xml;base64,PHN2ZyBmaWxsPSJub25lIiBoZWlnaHQ9IjI0IiBzdHJva2U9IiNmZmYiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSIyIiB2aWV3Qm94PSIwIDAgMjQgMjQiIHdpZHRoPSIyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJtMjMgNHY2aC02Ii8+PHBhdGggZD0ibTEgMjB2LTZoNiIvPjxwYXRoIGQ9Im0zLjUxIDlhOSA5IDAgMCAxIDE0Ljg1LTMuMzZsNC42NCA0LjM2bS0yMiA0IDQuNjQgNC4zNmE5IDkgMCAwIDAgMTQuODUtMy4zNiIvPjwvc3ZnPg==");
}
.card-title-stats {
color: #9aa0ac;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.card-body-stats {
position: relative;
flex: 1 1 auto;
margin: 0;
padding: 1rem 1.5rem;
}
.card-value-stats {
display: block;
font-size: 2.1rem;
line-height: 2.7rem;
height: 2.7rem;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.card-value-percent {
position: absolute;
top: 15px;
right: 15px;
font-size: 0.9rem;
line-height: 1;
height: auto;
}
.card-value-percent:after {
content: "%";
}

View File

@@ -0,0 +1,30 @@
import React from 'react';
import PropTypes from 'prop-types';
const Cell = props => (
<div className="stats__row">
<div className="stats__row-value mb-1">
<strong>{props.value}</strong>
<small className="ml-3 text-muted">
{props.percent}%
</small>
</div>
<div className="progress progress-xs">
<div
className="progress-bar"
style={{
width: `${props.percent}%`,
backgroundColor: props.color,
}}
/>
</div>
</div>
);
Cell.propTypes = {
value: PropTypes.number.isRequired,
percent: PropTypes.number.isRequired,
color: PropTypes.string.isRequired,
};
export default Cell;

View File

@@ -1,4 +1,5 @@
import React, { Component } from 'react';
import { REPOSITORY } from '../../helpers/constants';
class Footer extends Component {
getYear = () => {
@@ -10,22 +11,25 @@ class Footer extends Component {
return (
<footer className="footer">
<div className="container">
<div className="row align-items-center flex-row-reverse">
<div className="col-12 col-lg-auto ml-lg-auto">
<ul className="list-inline list-inline-dots text-center mb-0">
<li className="list-inline-item">
<a href="https://adguard.com/welcome.html" target="_blank" rel="noopener noreferrer">Homepage</a>
</li>
<li className="list-inline-item">
<a href="https://github.com/AdguardTeam/" target="_blank" rel="noopener noreferrer">Github</a>
</li>
<li className="list-inline-item">
<a href="https://adguard.com/privacy.html" target="_blank" rel="noopener noreferrer">Privacy Policy</a>
</li>
</ul>
</div>
<div className="row align-items-center flex-row">
<div className="col-12 col-lg-auto mt-3 mt-lg-0 text-center">
© AdGuard {this.getYear()}
<div className="row align-items-center justify-content-center">
<div className="col-auto">
Copyright © {this.getYear()} <a href="https://adguard.com/">AdGuard</a>
</div>
<div className="col-auto">
<ul className="list-inline text-center mb-0">
<li className="list-inline-item">
<a href={REPOSITORY.URL} target="_blank" rel="noopener noreferrer">Homepage</a>
</li>
</ul>
</div>
<div className="col-auto">
<a href={`${REPOSITORY.URL}/issues/new`} className="btn btn-outline-primary btn-sm" target="_blank" rel="noopener noreferrer">
Report an issue
</a>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,9 @@
.line__tooltip {
padding: 2px 10px 7px;
line-height: 1.1;
color: #fff;
}
.line__tooltip-text {
font-size: 0.7rem;
}

View File

@@ -0,0 +1,64 @@
import React from 'react';
import PropTypes from 'prop-types';
import { ResponsiveLine } from '@nivo/line';
import './Line.css';
const Line = props => (
props.data &&
<ResponsiveLine
data={props.data}
margin={{
top: 15,
right: 0,
bottom: 1,
left: 0,
}}
minY="auto"
stacked={false}
curve='linear'
axisBottom={{
tickSize: 0,
tickPadding: 0,
}}
axisLeft={{
tickSize: 0,
tickPadding: 0,
}}
enableGridX={false}
enableGridY={false}
enableDots={false}
enableArea={true}
animate={false}
colorBy={() => (props.color)}
tooltip={slice => (
<div>
{slice.data.map(d => (
<div key={d.serie.id} className="line__tooltip">
<span className="line__tooltip-text">
<strong>{d.data.y}</strong>
<br/>
<small>{d.data.x}</small>
</span>
</div>
))}
</div>
)}
theme={{
tooltip: {
container: {
padding: '0',
background: '#333',
borderRadius: '4px',
},
},
}}
/>
);
Line.propTypes = {
data: PropTypes.array.isRequired,
color: PropTypes.string.isRequired,
};
export default Line;

View File

@@ -7,11 +7,14 @@ import './Modal.css';
ReactModal.setAppElement('#root');
const initialState = {
url: '',
name: '',
isUrlValid: false,
};
export default class Modal extends Component {
state = {
url: '',
isUrlValid: false,
};
state = initialState;
// eslint-disable-next-line
isUrlValid = url => {
@@ -27,33 +30,48 @@ export default class Modal extends Component {
}
};
handleNameChange = (e) => {
const { value: name } = e.currentTarget;
this.setState({ ...this.state, name });
};
handleNext = () => {
this.props.addFilter(this.state.url);
this.props.addFilter(this.state.url, this.state.name);
setTimeout(() => {
if (this.props.isFilterAdded) {
this.props.toggleModal();
this.closeModal();
}
}, 2000);
};
closeModal = () => {
this.props.toggleModal();
this.setState({ ...this.state, ...initialState });
}
render() {
const {
isOpen,
toggleModal,
title,
inputDescription,
} = this.props;
const { isUrlValid, url } = this.state;
const inputClass = classnames({
const { isUrlValid, url, name } = this.state;
const inputUrlClass = classnames({
'form-control mb-2': true,
'is-invalid': url.length > 0 && !isUrlValid,
'is-valid': url.length > 0 && isUrlValid,
});
const inputNameClass = classnames({
'form-control mb-2': true,
'is-valid': name.length > 0,
});
const renderBody = () => {
if (!this.props.isFilterAdded) {
return (
<React.Fragment>
<input type="text" className={inputClass} placeholder="Enter URL or path" onChange={this.handleUrlChange}/>
<input type="text" className={inputNameClass} placeholder="Enter name" onChange={this.handleNameChange} />
<input type="text" className={inputUrlClass} placeholder="Enter URL" onChange={this.handleUrlChange} />
{inputDescription &&
<div className="description">
{inputDescription}
@@ -68,21 +86,21 @@ export default class Modal extends Component {
);
};
const isValidForSubmit = !(url.length > 0 && isUrlValid);
const isValidForSubmit = !(url.length > 0 && isUrlValid && name.length > 0);
return (
<ReactModal
className="Modal__Bootstrap modal-dialog modal-dialog-centered"
closeTimeoutMS={0}
isOpen={ isOpen }
onRequestClose={toggleModal}
onRequestClose={this.closeModal}
>
<div className="modal-content">
<div className="modal-header">
<h4 className="modal-title">
{title}
</h4>
<button type="button" className="close" onClick={toggleModal}>
<button type="button" className="close" onClick={this.closeModal}>
<span className="sr-only">Close</span>
</button>
</div>
@@ -92,7 +110,7 @@ export default class Modal extends Component {
{
!this.props.isFilterAdded &&
<div className="modal-footer">
<button type="button" className="btn btn-secondary" onClick={toggleModal}>Cancel</button>
<button type="button" className="btn btn-secondary" onClick={this.closeModal}>Cancel</button>
<button type="button" className="btn btn-success" onClick={this.handleNext} disabled={isValidForSubmit}>Add filter</button>
</div>
}

View File

@@ -0,0 +1,85 @@
.popover-wrap {
position: relative;
display: inline-block;
vertical-align: middle;
}
.popover__trigger {
position: relative;
top: 3px;
margin: 0 8px;
cursor: pointer;
}
.popover__trigger:after {
content: "";
position: absolute;
top: -6px;
left: -3px;
width: 26px;
height: 24px;
}
.popover__body {
content: "";
display: flex;
position: absolute;
min-width: 275px;
bottom: calc(100% + 3px);
left: 50%;
padding: 10px 15px;
font-size: 0.8rem;
white-space: normal;
color: #fff;
background-color: #585965;
border-radius: 3px;
transition: opacity 0.2s ease-in-out, visibility 0.2s ease-in-out;
transform: translateX(-50%);
visibility: hidden;
opacity: 0;
}
.popover__body:after {
content: "";
position: absolute;
bottom: -5px;
left: calc(50% - 6px);
width: 0;
height: 0;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-top: 6px solid #585965;
}
.popover__trigger:hover + .popover__body,
.popover__body:hover {
visibility: visible;
opacity: 1;
}
.popover__icon {
width: 20px;
height: 20px;
stroke: #9aa0ac;
}
.popover__list-title {
margin-bottom: 3px;
}
.popover__list-item {
margin-bottom: 2px;
}
.popover__list-item:last-child {
margin-bottom: 0;
}
.popover__link {
color: #66b586;
}
.popover__link:hover,
.popover__link:focus {
color: #66b586;
}

View File

@@ -0,0 +1,54 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { getSourceData } from '../../helpers/trackers/trackers';
import { captitalizeWords } from '../../helpers/helpers';
import './Popover.css';
class Popover extends Component {
render() {
const { data } = this.props;
const sourceData = getSourceData(data);
const source = (
<div className="popover__list-item">
Source: <a className="popover__link" target="_blank" rel="noopener noreferrer" href={sourceData.url}><strong>{sourceData.name}</strong></a>
</div>
);
const tracker = (
<div className="popover__list-item">
Name: <a className="popover__link" target="_blank" rel="noopener noreferrer" href={data.url}><strong>{data.name}</strong></a>
</div>
);
const categoryName = captitalizeWords(data.category);
return (
<div className="popover-wrap">
<div className="popover__trigger">
<svg className="popover__icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg>
</div>
<div className="popover__body">
<div className="popover__list">
<div className="popover__list-title">
Found in the known domains database.
</div>
{tracker}
<div className="popover__list-item">
Category: <strong>{categoryName}</strong>
</div>
{source}
</div>
</div>
</div>
);
}
}
Popover.propTypes = {
data: PropTypes.object.isRequired,
};
export default Popover;

View File

@@ -1,4 +1,13 @@
.ReactTable .rt-th,
.ReactTable .rt-td {
padding: 10px 15px;
overflow: visible;
}
.ReactTable .rt-tbody {
overflow: visible;
}
.rt-tr-group .red {
background-color: #fff4f2;
}

View File

@@ -7,10 +7,10 @@ const Status = props => (
<div className="status">
<Card bodyType="card-body card-body--status">
<div className="h4 font-weight-light mb-4">
You are currently not using AdGuard DNS
You are currently not using AdGuard Home
</div>
<button className="btn btn-success" onClick={props.handleStatusChange}>
Enable AdGuard DNS
Enable AdGuard Home
</button>
</Card>
</div>

View File

@@ -5,6 +5,7 @@
vertical-align: middle;
width: 18px;
height: 18px;
flex-shrink: 0;
margin-left: 5px;
background-image: url("./svg/help-circle.svg");
background-size: 100%;
@@ -15,15 +16,15 @@
content: attr(data-tooltip);
display: block;
position: absolute;
bottom: calc(100% + 12px);
left: calc(50% - 103px);
width: 206px;
bottom: calc(100% + 10px);
left: 50%;
padding: 10px 15px;
font-size: 0.85rem;
text-align: center;
color: #fff;
background-color: #585965;
border-radius: 3px;
transform: translateX(-50%);
visibility: hidden;
opacity: 0;
}
@@ -31,7 +32,7 @@
.tooltip-custom:after {
content: "";
position: relative;
top: -9px;
top: -7px;
left: calc(50% - 6px);
visibility: hidden;
opacity: 0;
@@ -47,3 +48,7 @@
visibility: visible;
opacity: 1;
}
.tooltip-custom--narrow:before {
width: 206px;
}

View File

@@ -4,11 +4,12 @@ import PropTypes from 'prop-types';
import './Tooltip.css';
const Tooltip = props => (
<div data-tooltip={props.text} className="tooltip-custom"></div>
<div data-tooltip={props.text} className={`tooltip-custom ${props.type || ''}`}></div>
);
Tooltip.propTypes = {
text: PropTypes.string.isRequired,
type: PropTypes.string,
};
export default Tooltip;

View File

@@ -0,0 +1,6 @@
.update {
position: relative;
z-index: 102;
margin-bottom: 0;
padding: 0.75rem 0;
}

View File

@@ -0,0 +1,19 @@
import React from 'react';
import PropTypes from 'prop-types';
import './Update.css';
const Update = props => (
<div className="alert alert-info update">
<div className="container">
{props.announcement} <a href={props.announcementUrl} target="_blank" rel="noopener noreferrer">Click here</a> for more info.
</div>
</div>
);
Update.propTypes = {
announcement: PropTypes.string.isRequired,
announcementUrl: PropTypes.string.isRequired,
};
export default Update;

View File

@@ -1,10 +1,10 @@
import { connect } from 'react-redux';
import { getLogs, toggleLogStatus, downloadQueryLog } from '../actions';
import { getLogs, toggleLogStatus, downloadQueryLog, getFilteringStatus, setRules, addSuccessToast } from '../actions';
import Logs from '../components/Logs';
const mapStateToProps = (state) => {
const { queryLogs, dashboard } = state;
const props = { queryLogs, dashboard };
const { queryLogs, dashboard, filtering } = state;
const props = { queryLogs, dashboard, filtering };
return props;
};
@@ -12,6 +12,9 @@ const mapDispatchToProps = {
getLogs,
toggleLogStatus,
downloadQueryLog,
getFilteringStatus,
setRules,
addSuccessToast,
};
export default connect(

View File

@@ -1,10 +1,10 @@
import { connect } from 'react-redux';
import { initSettings, toggleSetting, handleUpstreamChange, setUpstream } from '../actions';
import { initSettings, toggleSetting, handleUpstreamChange, setUpstream, testUpstream, addErrorToast } from '../actions';
import Settings from '../components/Settings';
const mapStateToProps = (state) => {
const { settings } = state;
const props = { settings };
const { settings, dashboard } = state;
const props = { settings, dashboard };
return props;
};
@@ -13,6 +13,8 @@ const mapDispatchToProps = {
toggleSetting,
handleUpstreamChange,
setUpstream,
testUpstream,
addErrorToast,
};
export default connect(

View File

@@ -1 +1,22 @@
export const R_URL_REQUIRES_PROTOCOL = /^https?:\/\/\w[\w_\-.]*\.[a-z]{2,8}[^\s]*$/;
export const STATS_NAMES = {
avg_processing_time: 'Average processing time',
blocked_filtering: 'Blocked by filters',
dns_queries: 'DNS queries',
replaced_parental: 'Blocked adult websites',
replaced_safebrowsing: 'Blocked malware/phishing',
replaced_safesearch: 'Enforced safe search',
};
export const STATUS_COLORS = {
blue: '#467fcf',
red: '#cd201f',
green: '#5eba00',
yellow: '#f1c40f',
};
export const REPOSITORY = {
URL: 'https://github.com/AdguardTeam/AdGuardHome',
TRACKERS_DB: 'https://github.com/AdguardTeam/AdGuardHome/tree/master/client/src/helpers/trackers/adguard.json',
};

View File

@@ -1,10 +1,12 @@
import dateParse from 'date-fns/parse';
import dateFormat from 'date-fns/format';
import startOfToday from 'date-fns/start_of_today';
import subHours from 'date-fns/sub_hours';
import addHours from 'date-fns/add_hours';
import round from 'lodash/round';
const formatTime = (time) => {
import { STATS_NAMES } from './constants';
export const formatTime = (time) => {
const parsedTime = dateParse(time);
return dateFormat(parsedTime, 'HH:mm:ss');
};
@@ -14,6 +16,9 @@ export const normalizeLogs = logs => logs.map((log) => {
time,
question,
answer: response,
reason,
client,
rule,
} = log;
const { host: domain, type } = question;
const responsesArray = response ? response.map((response) => {
@@ -21,20 +26,26 @@ export const normalizeLogs = logs => logs.map((log) => {
return `${type}: ${value} (ttl=${ttl})`;
}) : [];
return {
time: formatTime(time),
time,
domain,
type,
response: responsesArray,
reason,
client,
rule,
};
});
export const normalizeHistory = history => Object.keys(history).map((key) => {
const id = key.replace(/_/g, ' ').replace(/^\w/, c => c.toUpperCase());
let id = STATS_NAMES[key];
if (!id) {
id = key.replace(/_/g, ' ').replace(/^\w/, c => c.toUpperCase());
}
const today = startOfToday();
const dayAgo = subHours(Date.now(), 24);
const data = history[key].map((item, index) => {
const formatHour = dateFormat(addHours(today, index), 'HH:mm');
const formatHour = dateFormat(addHours(dayAgo, index), 'ddd HH:00');
const roundValue = round(item, 2);
return {
@@ -63,3 +74,12 @@ export const normalizeFilteringStatus = (filteringStatus) => {
const newUserRules = Array.isArray(userRules) ? userRules.join('\n') : '';
return { enabled, userRules: newUserRules, filters: newFilters };
};
export const getPercent = (amount, number) => {
if (amount > 0 && number > 0) {
return round(100 / (amount / number), 2);
}
return 0;
};
export const captitalizeWords = text => text.split(/[ -_]/g).map(str => str.charAt(0).toUpperCase() + str.substr(1)).join(' ');

View File

@@ -0,0 +1,77 @@
{
"timeUpdated": "2018-10-14",
"categories": {
"0": "audio_video_player",
"1": "comments",
"2": "customer_interaction",
"3": "pornvertising",
"4": "advertising",
"5": "essential",
"6": "site_analytics",
"7": "social_media",
"8": "misc",
"9": "cdn",
"10": "hosting",
"11": "unknown",
"12": "extensions",
"101": "mobile_analytics"
},
"trackers": {
"facebook_audience": {
"name": "Facebook Audience Network",
"categoryId": 4,
"url": "https://www.facebook.com/business/products/audience-network"
},
"crashlytics": {
"name": "Crashlytics",
"categoryId": 101,
"url": "https://crashlytics.com/"
},
"flurry": {
"name": "Flurry",
"categoryId": 101,
"url": "http://www.flurry.com/"
},
"hockeyapp": {
"name": "HockeyApp",
"categoryId": 101,
"url": "https://hockeyapp.net/"
},
"firebase": {
"name": "Firebase",
"categoryId": 101,
"url": "https://firebase.google.com/"
},
"appsflyer": {
"name": "AppsFlyer",
"categoryId": 101,
"url": "https://www.appsflyer.com/"
},
"yandex_appmetrica": {
"name": "Yandex AppMetrica",
"categoryId": 101,
"url": "https://appmetrica.yandex.com/"
},
"adjust": {
"name": "Adjust",
"categoryId": 101,
"url": "https://www.adjust.com/"
},
"branch": {
"name": "Branch.io",
"categoryId": 101,
"url": "https://branch.io/"
}
},
"trackerDomains": {
"graph.facebook.com": "facebook_audience",
"crashlytics.com": "crashlytics",
"flurry.com": "flurry",
"hockeyapp.net": "hockeyapp",
"app-measurement.com": "firebase",
"appsflyer.com": "appsflyer",
"appmetrica.yandex.com": "yandex_appmetrica",
"adjust.com": "adjust",
"mobileapptracking.com": "branch"
}
}

View File

@@ -0,0 +1,103 @@
import whotracksmeDb from './whotracksme.json';
import adguardDb from './adguard.json';
import { REPOSITORY } from '../constants';
/**
@typedef TrackerData
@type {object}
@property {string} id - tracker ID.
@property {string} name - tracker name.
@property {string} url - tracker website url.
@property {number} category - tracker category.
@property {source} source - tracker data source.
*/
/**
* Tracker data sources
*/
export const sources = {
WHOTRACKSME: 1,
ADGUARD: 2,
};
/**
* Gets tracker data in the specified database
*
* @param {String} domainName domain name to check
* @param {*} trackersDb trackers database
* @param {number} source source ID
* @returns {TrackerData} tracker data or null if no matching tracker found
*/
const getTrackerDataFromDb = (domainName, trackersDb, source) => {
if (!domainName) {
return null;
}
const parts = domainName.split(/\./g).reverse();
let hostToCheck = '';
// Check every subdomain
for (let i = 0; i < parts.length; i += 1) {
hostToCheck = parts[i] + (i > 0 ? '.' : '') + hostToCheck;
const trackerId = trackersDb.trackerDomains[hostToCheck];
if (trackerId) {
const trackerData = trackersDb.trackers[trackerId];
const categoryName = trackersDb.categories[trackerData.categoryId];
return {
id: trackerId,
name: trackerData.name,
url: trackerData.url,
category: categoryName,
source,
};
}
}
// No tracker found for the specified domain
return null;
};
/**
* Gets the source metadata for the specified tracker
* @param {TrackerData} trackerData tracker data
*/
export const getSourceData = (trackerData) => {
if (!trackerData || !trackerData.source) {
return null;
}
if (trackerData.source === sources.WHOTRACKSME) {
return {
name: 'Whotracks.me',
url: `https://whotracks.me/trackers/${trackerData.id}.html`,
};
} else if (trackerData.source === sources.ADGUARD) {
return {
name: 'AdGuard',
url: REPOSITORY.TRACKERS_DB,
};
}
return null;
};
/**
* Gets tracker data from the trackers database
*
* @param {String} domainName domain name to check
* @returns {TrackerData} tracker data or null if no matching tracker found
*/
export const getTrackerData = (domainName) => {
if (!domainName) {
return null;
}
let data = getTrackerDataFromDb(domainName, adguardDb, sources.ADGUARD);
if (!data) {
data = getTrackerDataFromDb(domainName, whotracksmeDb, sources.WHOTRACKSME);
}
return data;
};

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,8 @@
import { combineReducers } from 'redux';
import { handleActions } from 'redux-actions';
import { loadingBarReducer } from 'react-redux-loading-bar';
import versionCompare from 'tiny-version-compare';
import nanoid from 'nanoid';
import * as actions from '../actions';
@@ -24,14 +27,14 @@ const settings = handleActions({
[actions.setUpstreamRequest]: state => ({ ...state, processingUpstream: true }),
[actions.setUpstreamFailure]: state => ({ ...state, processingUpstream: false }),
[actions.setUpstreamSuccess]: state => ({ ...state, processingUpstream: false }),
[actions.handleUpstreamChange]: (state, { payload }) => {
const { upstream } = payload;
return { ...state, upstream };
},
[actions.testUpstreamRequest]: state => ({ ...state, processingTestUpstream: true }),
[actions.testUpstreamFailure]: state => ({ ...state, processingTestUpstream: false }),
[actions.testUpstreamSuccess]: state => ({ ...state, processingTestUpstream: false }),
}, {
processing: true,
processingUpstream: true,
upstream: '',
processingTestUpstream: false,
processingSetUpstream: false,
});
const dashboard = handleActions({
@@ -44,6 +47,8 @@ const dashboard = handleActions({
dns_port: dnsPort,
dns_address: dnsAddress,
querylog_enabled: queryLogEnabled,
upstream_dns: upstreamDns,
protection_enabled: protectionEnabled,
} = payload;
const newState = {
...state,
@@ -53,6 +58,8 @@ const dashboard = handleActions({
dnsPort,
dnsAddress,
queryLogEnabled,
upstreamDns: upstreamDns.join('\n'),
protectionEnabled,
};
return newState;
},
@@ -98,12 +105,56 @@ const dashboard = handleActions({
const { queryLogEnabled } = state;
return ({ ...state, queryLogEnabled: !queryLogEnabled, logStatusProcessing: false });
},
[actions.getVersionRequest]: state => ({ ...state, processingVersion: true }),
[actions.getVersionFailure]: state => ({ ...state, processingVersion: false }),
[actions.getVersionSuccess]: (state, { payload }) => {
const currentVersion = state.dnsVersion === 'undefined' ? 0 : state.dnsVersion;
if (versionCompare(currentVersion, payload.version) === -1) {
const {
announcement,
announcement_url: announcementUrl,
} = payload;
const newState = {
...state,
announcement,
announcementUrl,
isUpdateAvailable: true,
};
return newState;
}
return state;
},
[actions.getFilteringRequest]: state => ({ ...state, processingFiltering: true }),
[actions.getFilteringFailure]: state => ({ ...state, processingFiltering: false }),
[actions.getFilteringSuccess]: (state, { payload }) => {
const newState = { ...state, isFilteringEnabled: payload, processingFiltering: false };
return newState;
},
[actions.toggleProtectionSuccess]: (state) => {
const newState = { ...state, protectionEnabled: !state.protectionEnabled };
return newState;
},
[actions.handleUpstreamChange]: (state, { payload }) => {
const { upstreamDns } = payload;
return { ...state, upstreamDns };
},
}, {
processing: true,
isCoreRunning: false,
processingTopStats: true,
processingStats: true,
logStatusProcessing: false,
processingVersion: true,
processingFiltering: true,
upstreamDns: [],
protectionEnabled: false,
});
const queryLogs = handleActions({
@@ -172,9 +223,39 @@ const filtering = handleActions({
userRules: '',
});
const toasts = handleActions({
[actions.addErrorToast]: (state, { payload }) => {
const errorToast = {
id: nanoid(),
message: payload.error.toString(),
type: 'error',
};
const newState = { ...state, notices: [...state.notices, errorToast] };
return newState;
},
[actions.addSuccessToast]: (state, { payload }) => {
const successToast = {
id: nanoid(),
message: payload,
type: 'success',
};
const newState = { ...state, notices: [...state.notices, successToast] };
return newState;
},
[actions.removeToast]: (state, { payload }) => {
const filtered = state.notices.filter(notice => notice.id !== payload);
const newState = { ...state, notices: filtered };
return newState;
},
}, { notices: [] });
export default combineReducers({
settings,
dashboard,
queryLogs,
filtering,
toasts,
loadingBar: loadingBarReducer,
});

View File

@@ -4,6 +4,7 @@ const HtmlWebpackPlugin = require('html-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const webpack = require('webpack');
const flexBugsFixes = require('postcss-flexbugs-fixes');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const RESOURCES_PATH = path.resolve(__dirname);
const ENTRY_REACT = path.resolve(RESOURCES_PATH, 'src/index.js');
@@ -19,7 +20,7 @@ const config = {
},
output: {
path: PUBLIC_PATH,
filename: '[name].js',
filename: '[name].[chunkhash].js',
},
resolve: {
modules: ['node_modules'],
@@ -92,12 +93,18 @@ const config = {
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
}),
new CleanWebpackPlugin(['*.*'], {
root: PUBLIC_PATH,
verbose: false,
dry: false,
}),
new HtmlWebpackPlugin({
inject: true,
cache: false,
template: HTML_PATH,
}),
new ExtractTextPlugin({
filename: '[name].css',
filename: '[name].[contenthash].css',
}),
],
};

9626
client/yarn.lock vendored

File diff suppressed because it is too large Load Diff

View File

@@ -21,44 +21,48 @@ type configuration struct {
BindHost string `yaml:"bind_host"`
BindPort int `yaml:"bind_port"`
AuthName string `yaml:"auth_name"`
AuthPass string `yaml:"auth_pass"`
CoreDNS coreDNSConfig `yaml:"coredns"`
Filters []filter `yaml:"filters"`
UserRules []string `yaml:"user_rules"`
sync.Mutex `yaml:"-"`
sync.RWMutex `yaml:"-"`
}
type coreDNSConfig struct {
Port int `yaml:"port"`
binaryFile string
coreFile string
FilterFile string `yaml:"-"`
Port int `yaml:"port"`
ProtectionEnabled bool `yaml:"protection_enabled"`
FilteringEnabled bool `yaml:"filtering_enabled"`
SafeBrowsingEnabled bool `yaml:"safebrowsing_enabled"`
SafeSearchEnabled bool `yaml:"safesearch_enabled"`
ParentalEnabled bool `yaml:"parental_enabled"`
ParentalSensitivity int `yaml:"parental_sensitivity"`
BlockedResponseTTL int `yaml:"blocked_response_ttl"`
QueryLogEnabled bool `yaml:"querylog_enabled"`
Pprof string `yaml:"pprof"`
Pprof string `yaml:"-"`
Cache string `yaml:"-"`
Prometheus string `yaml:"-"`
UpstreamDNS []string `yaml:"upstream_dns"`
Cache string `yaml:"cache"`
Prometheus string `yaml:"prometheus"`
}
type filter struct {
Enabled bool `json:"enabled"`
URL string `json:"url"`
Name string `json:"name" yaml:"name"`
Enabled bool `json:"enabled"`
RulesCount int `json:"rules_count" yaml:"-"`
Name string `json:"name" yaml:"-"`
contents []byte
LastUpdated time.Time `json:"last_updated" yaml:"-"`
}
var defaultDNS = []string{"1.1.1.1", "1.0.0.1"}
var defaultDNS = []string{"tls://1.1.1.1", "tls://1.0.0.1"}
// initialize to default values, will be changed later when reading config or parsing command line
var config = configuration{
ourConfigFilename: "AdguardDNS.yaml",
ourConfigFilename: "AdGuardHome.yaml",
BindPort: 3000,
BindHost: "127.0.0.1",
CoreDNS: coreDNSConfig{
@@ -66,15 +70,20 @@ var config = configuration{
binaryFile: "coredns", // only filename, no path
coreFile: "Corefile", // only filename, no path
FilterFile: "dnsfilter.txt", // only filename, no path
ProtectionEnabled: true,
FilteringEnabled: true,
SafeBrowsingEnabled: true,
SafeBrowsingEnabled: false,
BlockedResponseTTL: 10, // in seconds
QueryLogEnabled: true,
UpstreamDNS: defaultDNS,
Cache: "cache",
Prometheus: "prometheus :9153",
},
Filters: []filter{
{Enabled: true, URL: "https://filters.adtidy.org/windows/filters/15.txt"},
{Enabled: true, URL: "https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt"},
{Enabled: false, URL: "https://adaway.org/hosts.txt", Name: "AdAway"},
{Enabled: false, URL: "https://hosts-file.net/ad_servers.txt", Name: "hpHosts - Ad and Tracking servers only"},
{Enabled: false, URL: "http://www.malwaredomainlist.com/hostslist/hosts.txt", Name: "MalwareDomainList.com Hosts List"},
},
}
@@ -108,11 +117,16 @@ func writeConfig() error {
log.Printf("Couldn't generate YAML file: %s", err)
return err
}
err = ioutil.WriteFile(configfile, yamlText, 0644)
err = ioutil.WriteFile(configfile+".tmp", yamlText, 0644)
if err != nil {
log.Printf("Couldn't write YAML config: %s", err)
return err
}
err = os.Rename(configfile+".tmp", configfile)
if err != nil {
log.Printf("Couldn't rename YAML config: %s", err)
return err
}
return nil
}
@@ -127,10 +141,14 @@ func writeCoreDNSConfig() error {
log.Printf("Couldn't generate DNS config: %s", err)
return err
}
err = ioutil.WriteFile(corefile, []byte(configtext), 0644)
err = ioutil.WriteFile(corefile+".tmp", []byte(configtext), 0644)
if err != nil {
log.Printf("Couldn't write DNS config: %s", err)
}
err = os.Rename(corefile+".tmp", corefile)
if err != nil {
log.Printf("Couldn't rename DNS config: %s", err)
}
return err
}
@@ -148,14 +166,18 @@ func writeAllConfigs() error {
return nil
}
const coreDNSConfigTemplate = `. {
{{if .FilteringEnabled}}dnsfilter {{.FilterFile}} {
const coreDNSConfigTemplate = `.:{{.Port}} {
{{if .ProtectionEnabled}}dnsfilter {{if .FilteringEnabled}}{{.FilterFile}}{{end}} {
{{if .SafeBrowsingEnabled}}safebrowsing{{end}}
{{if .ParentalEnabled}}parental {{.ParentalSensitivity}}{{end}}
{{if .SafeSearchEnabled}}safesearch{{end}}
{{if .QueryLogEnabled}}querylog{{end}}
blocked_ttl {{.BlockedResponseTTL}}
}{{end}}
{{.Pprof}}
hosts {
fallthrough
}
{{if .UpstreamDNS}}forward . {{range .UpstreamDNS}}{{.}} {{end}}{{end}}
{{.Cache}}
{{.Prometheus}}
@@ -173,8 +195,10 @@ func generateCoreDNSConfigText() (string, error) {
}
var configBytes bytes.Buffer
temporaryConfig := config.CoreDNS
temporaryConfig.FilterFile = filepath.Join(config.ourBinaryDir, config.CoreDNS.FilterFile)
// run the template
err = t.Execute(&configBytes, config.CoreDNS)
err = t.Execute(&configBytes, &temporaryConfig)
if err != nil {
log.Printf("Couldn't generate DNS config: %s", err)
return "", err

File diff suppressed because it is too large Load Diff

132
coredns.go Normal file
View File

@@ -0,0 +1,132 @@
package main
import (
"fmt"
"log"
"os"
"path/filepath"
"sync" // Include all plugins.
_ "github.com/AdguardTeam/AdGuardHome/coredns_plugin"
"github.com/coredns/coredns/core/dnsserver"
"github.com/coredns/coredns/coremain"
_ "github.com/coredns/coredns/plugin/auto"
_ "github.com/coredns/coredns/plugin/autopath"
_ "github.com/coredns/coredns/plugin/bind"
_ "github.com/coredns/coredns/plugin/cache"
_ "github.com/coredns/coredns/plugin/chaos"
_ "github.com/coredns/coredns/plugin/debug"
_ "github.com/coredns/coredns/plugin/dnssec"
_ "github.com/coredns/coredns/plugin/dnstap"
_ "github.com/coredns/coredns/plugin/erratic"
_ "github.com/coredns/coredns/plugin/errors"
_ "github.com/coredns/coredns/plugin/file"
_ "github.com/coredns/coredns/plugin/forward"
_ "github.com/coredns/coredns/plugin/health"
_ "github.com/coredns/coredns/plugin/hosts"
_ "github.com/coredns/coredns/plugin/loadbalance"
_ "github.com/coredns/coredns/plugin/log"
_ "github.com/coredns/coredns/plugin/loop"
_ "github.com/coredns/coredns/plugin/metadata"
_ "github.com/coredns/coredns/plugin/metrics"
_ "github.com/coredns/coredns/plugin/nsid"
_ "github.com/coredns/coredns/plugin/pprof"
_ "github.com/coredns/coredns/plugin/proxy"
_ "github.com/coredns/coredns/plugin/reload"
_ "github.com/coredns/coredns/plugin/rewrite"
_ "github.com/coredns/coredns/plugin/root"
_ "github.com/coredns/coredns/plugin/secondary"
_ "github.com/coredns/coredns/plugin/template"
_ "github.com/coredns/coredns/plugin/tls"
_ "github.com/coredns/coredns/plugin/whoami"
_ "github.com/mholt/caddy/onevent"
)
// Directives are registered in the order they should be
// executed.
//
// Ordering is VERY important. Every plugin will
// feel the effects of all other plugin below
// (after) them during a request, but they must not
// care what plugin above them are doing.
var directives = []string{
"metadata",
"tls",
"reload",
"nsid",
"root",
"bind",
"debug",
"health",
"pprof",
"prometheus",
"errors",
"log",
"dnsfilter",
"dnstap",
"chaos",
"loadbalance",
"cache",
"rewrite",
"dnssec",
"autopath",
"template",
"hosts",
"file",
"auto",
"secondary",
"loop",
"forward",
"proxy",
"erratic",
"whoami",
"on",
}
func init() {
dnsserver.Directives = directives
}
var (
isCoreDNSRunningLock sync.Mutex
isCoreDNSRunning = false
)
func isRunning() bool {
isCoreDNSRunningLock.Lock()
value := isCoreDNSRunning
isCoreDNSRunningLock.Unlock()
return value
}
func startDNSServer() error {
isCoreDNSRunningLock.Lock()
if isCoreDNSRunning {
isCoreDNSRunningLock.Unlock()
return fmt.Errorf("Unable to start coreDNS: Already running")
}
isCoreDNSRunning = true
isCoreDNSRunningLock.Unlock()
configpath := filepath.Join(config.ourBinaryDir, config.CoreDNS.coreFile)
os.Args = os.Args[:1]
os.Args = append(os.Args, "-conf")
os.Args = append(os.Args, configpath)
err := writeCoreDNSConfig()
if err != nil {
errortext := fmt.Errorf("Unable to write coredns config: %s", err)
log.Println(errortext)
return errortext
}
err = writeFilterFile()
if err != nil {
errortext := fmt.Errorf("Couldn't write filter file: %s", err)
log.Println(errortext)
return errortext
}
go coremain.Run()
return nil
}

View File

@@ -12,20 +12,16 @@ import (
"sync"
"time"
"github.com/AdguardTeam/AdGuardHome/dnsfilter"
"github.com/coredns/coredns/core/dnsserver"
"github.com/coredns/coredns/plugin"
"github.com/coredns/coredns/plugin/metrics"
"github.com/coredns/coredns/plugin/pkg/dnstest"
"github.com/coredns/coredns/plugin/pkg/upstream"
"github.com/coredns/coredns/request"
"github.com/mholt/caddy"
"github.com/miekg/dns"
"github.com/prometheus/client_golang/prometheus"
"github.com/AdguardTeam/AdguardDNS/dnsfilter"
"golang.org/x/net/context"
)
@@ -45,73 +41,68 @@ func init() {
})
}
type Plugin struct {
type cacheEntry struct {
answer []dns.RR
lastUpdated time.Time
}
var (
lookupCacheTime = time.Minute * 30
lookupCache = map[string]cacheEntry{}
)
type plugSettings struct {
SafeBrowsingBlockHost string
ParentalBlockHost string
QueryLogEnabled bool
BlockedTTL uint32 // in seconds, default 3600
}
type plug struct {
d *dnsfilter.Dnsfilter
Next plugin.Handler
upstream upstream.Upstream
hosts map[string]net.IP
settings plugSettings
SafeBrowsingBlockHost string
ParentalBlockHost string
QueryLogEnabled bool
sync.RWMutex
}
var defaultPlugin = Plugin{
var defaultPluginSettings = plugSettings{
SafeBrowsingBlockHost: "safebrowsing.block.dns.adguard.com",
ParentalBlockHost: "family.block.dns.adguard.com",
BlockedTTL: 3600, // in seconds
}
func newDnsCounter(name string, help string) prometheus.Counter {
return prometheus.NewCounter(prometheus.CounterOpts{
Namespace: plugin.Namespace,
Subsystem: "dnsfilter",
Name: name,
Help: help,
})
}
var (
requests = newDnsCounter("requests_total", "Count of requests seen by dnsfilter.")
filtered = newDnsCounter("filtered_total", "Count of requests filtered by dnsfilter.")
filteredLists = newDnsCounter("filtered_lists_total", "Count of requests filtered by dnsfilter using lists.")
filteredSafebrowsing = newDnsCounter("filtered_safebrowsing_total", "Count of requests filtered by dnsfilter using safebrowsing.")
filteredParental = newDnsCounter("filtered_parental_total", "Count of requests filtered by dnsfilter using parental.")
filteredInvalid = newDnsCounter("filtered_invalid_total", "Count of requests filtered by dnsfilter because they were invalid.")
whitelisted = newDnsCounter("whitelisted_total", "Count of requests not filtered by dnsfilter because they are whitelisted.")
safesearch = newDnsCounter("safesearch_total", "Count of requests replaced by dnsfilter safesearch.")
errorsTotal = newDnsCounter("errors_total", "Count of requests that dnsfilter couldn't process because of transitive errors.")
)
//
// coredns handling functions
//
func setupPlugin(c *caddy.Controller) (*Plugin, error) {
func setupPlugin(c *caddy.Controller) (*plug, error) {
// create new Plugin and copy default values
var d = new(Plugin)
*d = defaultPlugin
d.d = dnsfilter.New()
d.hosts = make(map[string]net.IP)
p := &plug{
settings: defaultPluginSettings,
d: dnsfilter.New(),
hosts: make(map[string]net.IP),
}
var filterFileName string
filterFileNames := []string{}
for c.Next() {
args := c.RemainingArgs()
if len(args) == 0 {
// must have at least one argument
return nil, c.ArgErr()
if len(args) > 0 {
filterFileNames = append(filterFileNames, args...)
}
filterFileName = args[0]
for c.NextBlock() {
switch c.Val() {
case "safebrowsing":
d.d.EnableSafeBrowsing()
p.d.EnableSafeBrowsing()
if c.NextArg() {
if len(c.Val()) == 0 {
return nil, c.ArgErr()
}
d.d.SetSafeBrowsingServer(c.Val())
p.d.SetSafeBrowsingServer(c.Val())
}
case "safesearch":
d.d.EnableSafeSearch()
p.d.EnableSafeSearch()
case "parental":
if !c.NextArg() {
return nil, c.ArgErr()
@@ -120,7 +111,7 @@ func setupPlugin(c *caddy.Controller) (*Plugin, error) {
if err != nil {
return nil, c.ArgErr()
}
err = d.d.EnableParental(sensitivity)
err = p.d.EnableParental(sensitivity)
if err != nil {
return nil, c.ArgErr()
}
@@ -128,87 +119,119 @@ func setupPlugin(c *caddy.Controller) (*Plugin, error) {
if len(c.Val()) == 0 {
return nil, c.ArgErr()
}
d.ParentalBlockHost = c.Val()
p.settings.ParentalBlockHost = c.Val()
}
case "blocked_ttl":
if !c.NextArg() {
return nil, c.ArgErr()
}
blockttl, err := strconv.ParseUint(c.Val(), 10, 32)
if err != nil {
return nil, c.ArgErr()
}
p.settings.BlockedTTL = uint32(blockttl)
case "querylog":
d.QueryLogEnabled = true
once.Do(func() {
go startQueryLogServer() // TODO: how to handle errors?
})
p.settings.QueryLogEnabled = true
}
}
}
file, err := os.Open(filterFileName)
if err != nil {
return nil, err
}
defer file.Close()
log.Printf("filterFileNames = %+v", filterFileNames)
scanner := bufio.NewScanner(file)
for scanner.Scan() {
text := scanner.Text()
if d.parseEtcHosts(text) {
continue
}
err = d.d.AddRule(text, 0)
if err == dnsfilter.ErrInvalidSyntax {
continue
}
for i, filterFileName := range filterFileNames {
file, err := os.Open(filterFileName)
if err != nil {
return nil, err
}
defer file.Close()
count := 0
scanner := bufio.NewScanner(file)
for scanner.Scan() {
text := scanner.Text()
if p.parseEtcHosts(text) {
continue
}
err = p.d.AddRule(text, uint32(i))
if err == dnsfilter.ErrInvalidSyntax {
continue
}
if err != nil {
return nil, err
}
count++
}
log.Printf("Added %d rules from %s", count, filterFileName)
if err = scanner.Err(); err != nil {
return nil, err
}
}
if err = scanner.Err(); err != nil {
log.Printf("Loading stats from querylog")
err := fillStatsFromQueryLog()
if err != nil {
log.Printf("Failed to load stats from querylog: %s", err)
return nil, err
}
d.upstream, err = upstream.New(nil)
if p.settings.QueryLogEnabled {
onceQueryLog.Do(func() {
go periodicQueryLogRotate()
go periodicHourlyTopRotate()
go statsRotator()
})
}
onceHook.Do(func() {
caddy.RegisterEventHook("dnsfilter-reload", hook)
})
p.upstream, err = upstream.New(nil)
if err != nil {
return nil, err
}
return d, nil
return p, nil
}
func setup(c *caddy.Controller) error {
d, err := setupPlugin(c)
p, err := setupPlugin(c)
if err != nil {
return err
}
config := dnsserver.GetConfig(c)
config.AddPlugin(func(next plugin.Handler) plugin.Handler {
d.Next = next
return d
p.Next = next
return p
})
c.OnStartup(func() error {
once.Do(func() {
m := dnsserver.GetConfig(c).Handler("prometheus")
if m == nil {
return
}
if x, ok := m.(*metrics.Metrics); ok {
x.MustRegister(requests)
x.MustRegister(filtered)
x.MustRegister(filteredLists)
x.MustRegister(filteredSafebrowsing)
x.MustRegister(filteredParental)
x.MustRegister(whitelisted)
x.MustRegister(safesearch)
x.MustRegister(errorsTotal)
x.MustRegister(d)
}
})
m := dnsserver.GetConfig(c).Handler("prometheus")
if m == nil {
return nil
}
if x, ok := m.(*metrics.Metrics); ok {
x.MustRegister(requests)
x.MustRegister(filtered)
x.MustRegister(filteredLists)
x.MustRegister(filteredSafebrowsing)
x.MustRegister(filteredParental)
x.MustRegister(whitelisted)
x.MustRegister(safesearch)
x.MustRegister(errorsTotal)
x.MustRegister(elapsedTime)
x.MustRegister(p)
}
return nil
})
c.OnShutdown(d.OnShutdown)
c.OnShutdown(p.onShutdown)
c.OnFinalShutdown(p.onFinalShutdown)
return nil
}
func (d *Plugin) parseEtcHosts(text string) bool {
func (p *plug) parseEtcHosts(text string) bool {
if pos := strings.IndexByte(text, '#'); pos != -1 {
text = text[0:pos]
}
@@ -221,17 +244,31 @@ func (d *Plugin) parseEtcHosts(text string) bool {
return false
}
for _, host := range fields[1:] {
if val, ok := d.hosts[host]; ok {
log.Printf("warning: host %s already has value %s, will overwrite it with %s", host, val, addr)
}
d.hosts[host] = addr
// debug logging for duplicate values, pretty common if you subscribe to many hosts files
// if val, ok := p.hosts[host]; ok {
// log.Printf("warning: host %s already has value %s, will overwrite it with %s", host, val, addr)
// }
p.hosts[host] = addr
}
return true
}
func (d *Plugin) OnShutdown() error {
d.d.Destroy()
d.d = nil
func (p *plug) onShutdown() error {
p.Lock()
p.d.Destroy()
p.d = nil
p.Unlock()
return nil
}
func (p *plug) onFinalShutdown() error {
logBufferLock.Lock()
err := flushToFile(logBuffer)
if err != nil {
log.Printf("failed to flush to file: %s", err)
return err
}
logBufferLock.Unlock()
return nil
}
@@ -239,7 +276,7 @@ type statsFunc func(ch interface{}, name string, text string, value float64, val
func doDesc(ch interface{}, name string, text string, value float64, valueType prometheus.ValueType) {
realch, ok := ch.(chan<- *prometheus.Desc)
if ok == false {
if !ok {
log.Printf("Couldn't convert ch to chan<- *prometheus.Desc\n")
return
}
@@ -248,7 +285,7 @@ func doDesc(ch interface{}, name string, text string, value float64, valueType p
func doMetric(ch interface{}, name string, text string, value float64, valueType prometheus.ValueType) {
realch, ok := ch.(chan<- prometheus.Metric)
if ok == false {
if !ok {
log.Printf("Couldn't convert ch to chan<- prometheus.Metric\n")
return
}
@@ -260,34 +297,39 @@ func gen(ch interface{}, doFunc statsFunc, name string, text string, value float
doFunc(ch, name, text, value, valueType)
}
func (d *Plugin) doStats(ch interface{}, doFunc statsFunc) {
stats := d.d.GetStats()
gen(ch, doFunc, "coredns_dnsfilter_safebrowsing_requests", "Number of safebrowsing HTTP requests that were sent", float64(stats.Safebrowsing.Requests), prometheus.CounterValue)
gen(ch, doFunc, "coredns_dnsfilter_safebrowsing_cachehits", "Number of safebrowsing lookups that didn't need HTTP requests", float64(stats.Safebrowsing.CacheHits), prometheus.CounterValue)
gen(ch, doFunc, "coredns_dnsfilter_safebrowsing_pending", "Number of currently pending safebrowsing HTTP requests", float64(stats.Safebrowsing.Pending), prometheus.GaugeValue)
gen(ch, doFunc, "coredns_dnsfilter_safebrowsing_pending_max", "Maximum number of pending safebrowsing HTTP requests", float64(stats.Safebrowsing.PendingMax), prometheus.GaugeValue)
gen(ch, doFunc, "coredns_dnsfilter_parental_requests", "Number of parental HTTP requests that were sent", float64(stats.Parental.Requests), prometheus.CounterValue)
gen(ch, doFunc, "coredns_dnsfilter_parental_cachehits", "Number of parental lookups that didn't need HTTP requests", float64(stats.Parental.CacheHits), prometheus.CounterValue)
gen(ch, doFunc, "coredns_dnsfilter_parental_pending", "Number of currently pending parental HTTP requests", float64(stats.Parental.Pending), prometheus.GaugeValue)
gen(ch, doFunc, "coredns_dnsfilter_parental_pending_max", "Maximum number of pending parental HTTP requests", float64(stats.Parental.PendingMax), prometheus.GaugeValue)
func doStatsLookup(ch interface{}, doFunc statsFunc, name string, lookupstats *dnsfilter.LookupStats) {
gen(ch, doFunc, fmt.Sprintf("coredns_dnsfilter_%s_requests", name), fmt.Sprintf("Number of %s HTTP requests that were sent", name), float64(lookupstats.Requests), prometheus.CounterValue)
gen(ch, doFunc, fmt.Sprintf("coredns_dnsfilter_%s_cachehits", name), fmt.Sprintf("Number of %s lookups that didn't need HTTP requests", name), float64(lookupstats.CacheHits), prometheus.CounterValue)
gen(ch, doFunc, fmt.Sprintf("coredns_dnsfilter_%s_pending", name), fmt.Sprintf("Number of currently pending %s HTTP requests", name), float64(lookupstats.Pending), prometheus.GaugeValue)
gen(ch, doFunc, fmt.Sprintf("coredns_dnsfilter_%s_pending_max", name), fmt.Sprintf("Maximum number of pending %s HTTP requests", name), float64(lookupstats.PendingMax), prometheus.GaugeValue)
}
func (d *Plugin) Describe(ch chan<- *prometheus.Desc) {
d.doStats(ch, doDesc)
func (p *plug) doStats(ch interface{}, doFunc statsFunc) {
p.RLock()
stats := p.d.GetStats()
doStatsLookup(ch, doFunc, "safebrowsing", &stats.Safebrowsing)
doStatsLookup(ch, doFunc, "parental", &stats.Parental)
p.RUnlock()
}
func (d *Plugin) Collect(ch chan<- prometheus.Metric) {
d.doStats(ch, doMetric)
// Describe is called by prometheus handler to know stat types
func (p *plug) Describe(ch chan<- *prometheus.Desc) {
p.doStats(ch, doDesc)
}
func (d *Plugin) replaceHostWithValAndReply(ctx context.Context, w dns.ResponseWriter, r *dns.Msg, host string, val string, question dns.Question) (int, error) {
// Collect is called by prometheus handler to collect stats
func (p *plug) Collect(ch chan<- prometheus.Metric) {
p.doStats(ch, doMetric)
}
func (p *plug) replaceHostWithValAndReply(ctx context.Context, w dns.ResponseWriter, r *dns.Msg, host string, val string, question dns.Question) (int, error) {
// check if it's a domain name or IP address
addr := net.ParseIP(val)
var records []dns.RR
log.Println("Will give", val, "instead of", host)
// log.Println("Will give", val, "instead of", host) // debug logging
if addr != nil {
// this is an IP address, return it
result, err := dns.NewRR(host + " A " + val)
result, err := dns.NewRR(fmt.Sprintf("%s %d A %s", host, p.settings.BlockedTTL, val))
if err != nil {
log.Printf("Got error %s\n", err)
return dns.RcodeServerFailure, fmt.Errorf("plugin/dnsfilter: %s", err)
@@ -295,20 +337,29 @@ func (d *Plugin) replaceHostWithValAndReply(ctx context.Context, w dns.ResponseW
records = append(records, result)
} else {
// this is a domain name, need to look it up
req := new(dns.Msg)
req.SetQuestion(dns.Fqdn(val), question.Qtype)
req.RecursionDesired = true
reqstate := request.Request{W: w, Req: req, Context: ctx}
result, err := d.upstream.Lookup(reqstate, dns.Fqdn(val), reqstate.QType())
if err != nil {
log.Printf("Got error %s\n", err)
return dns.RcodeServerFailure, fmt.Errorf("plugin/dnsfilter: %s", err)
}
if result != nil {
for _, answer := range result.Answer {
answer.Header().Name = question.Name
cacheentry := lookupCache[val]
if time.Since(cacheentry.lastUpdated) > lookupCacheTime {
req := new(dns.Msg)
req.SetQuestion(dns.Fqdn(val), question.Qtype)
req.RecursionDesired = true
reqstate := request.Request{W: w, Req: req, Context: ctx}
result, err := p.upstream.Lookup(reqstate, dns.Fqdn(val), reqstate.QType())
if err != nil {
log.Printf("Got error %s\n", err)
return dns.RcodeServerFailure, fmt.Errorf("plugin/dnsfilter: %s", err)
}
records = result.Answer
if result != nil {
for _, answer := range result.Answer {
answer.Header().Name = question.Name
}
records = result.Answer
cacheentry.answer = result.Answer
cacheentry.lastUpdated = time.Now()
lookupCache[val] = cacheentry
}
} else {
// get from cache
records = cacheentry.answer
}
}
m := new(dns.Msg)
@@ -327,9 +378,9 @@ func (d *Plugin) replaceHostWithValAndReply(ctx context.Context, w dns.ResponseW
// generate SOA record that makes DNS clients cache NXdomain results
// the only value that is important is TTL in header, other values like refresh, retry, expire and minttl are irrelevant
func genSOA(r *dns.Msg) []dns.RR {
func (p *plug) genSOA(r *dns.Msg) []dns.RR {
zone := r.Question[0].Name
header := dns.RR_Header{Name: zone, Rrtype: dns.TypeSOA, Ttl: 3600, Class: dns.ClassINET}
header := dns.RR_Header{Name: zone, Rrtype: dns.TypeSOA, Ttl: p.settings.BlockedTTL, Class: dns.ClassINET}
Mbox := "hostmaster."
if zone[0] != '.' {
@@ -337,20 +388,20 @@ func genSOA(r *dns.Msg) []dns.RR {
}
Ns := "fake-for-negative-caching.adguard.com."
soa := defaultSOA
soa := *defaultSOA
soa.Hdr = header
soa.Mbox = Mbox
soa.Ns = Ns
soa.Serial = uint32(time.Now().Unix())
return []dns.RR{soa}
soa.Serial = 100500 // faster than uint32(time.Now().Unix())
return []dns.RR{&soa}
}
func writeNXdomain(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
func (p *plug) writeNXdomain(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
state := request.Request{W: w, Req: r, Context: ctx}
m := new(dns.Msg)
m.SetRcode(state.Req, dns.RcodeNameError)
m.Authoritative, m.RecursionAvailable, m.Compress = true, true, true
m.Ns = genSOA(r)
m.Ns = p.genSOA(r)
state.SizeAndDo(m)
err := state.W.WriteMsg(m)
@@ -361,108 +412,127 @@ func writeNXdomain(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int,
return dns.RcodeNameError, nil
}
func (d *Plugin) serveDNSInternal(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error, dnsfilter.Result) {
func (p *plug) serveDNSInternal(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, dnsfilter.Result, error) {
if len(r.Question) != 1 {
// google DNS, bind and others do the same
return dns.RcodeFormatError, fmt.Errorf("Got DNS request with != 1 questions"), dnsfilter.Result{}
return dns.RcodeFormatError, dnsfilter.Result{}, fmt.Errorf("Got DNS request with != 1 questions")
}
for _, question := range r.Question {
host := strings.ToLower(strings.TrimSuffix(question.Name, "."))
// if input is empty host, filter it out right away
if index := strings.IndexByte(host, byte('.')); index == -1 {
rcode, err := writeNXdomain(ctx, w, r)
if err != nil {
return rcode, err, dnsfilter.Result{}
}
return rcode, err, dnsfilter.Result{Reason: dnsfilter.FilteredInvalid}
}
// is it a safesearch domain?
if val, ok := d.d.SafeSearchDomain(host); ok {
rcode, err := d.replaceHostWithValAndReply(ctx, w, r, host, val, question)
p.RLock()
if val, ok := p.d.SafeSearchDomain(host); ok {
rcode, err := p.replaceHostWithValAndReply(ctx, w, r, host, val, question)
if err != nil {
return rcode, err, dnsfilter.Result{}
p.RUnlock()
return rcode, dnsfilter.Result{}, err
}
return rcode, err, dnsfilter.Result{Reason: dnsfilter.FilteredSafeSearch}
p.RUnlock()
return rcode, dnsfilter.Result{Reason: dnsfilter.FilteredSafeSearch}, err
}
p.RUnlock()
// is it in hosts?
if val, ok := d.hosts[host]; ok {
if val, ok := p.hosts[host]; ok {
// it is, if it's a loopback host, reply with NXDOMAIN
if val.IsLoopback() {
rcode, err := writeNXdomain(ctx, w, r)
// TODO: research if it's better than 127.0.0.1
if false && val.IsLoopback() {
rcode, err := p.writeNXdomain(ctx, w, r)
if err != nil {
return rcode, err, dnsfilter.Result{}
return rcode, dnsfilter.Result{}, err
}
return rcode, err, dnsfilter.Result{Reason: dnsfilter.FilteredInvalid}
return rcode, dnsfilter.Result{Reason: dnsfilter.FilteredInvalid}, err
}
// it's not a loopback host, replace it with value specified
rcode, err := d.replaceHostWithValAndReply(ctx, w, r, host, val.String(), question)
rcode, err := p.replaceHostWithValAndReply(ctx, w, r, host, val.String(), question)
if err != nil {
return rcode, err, dnsfilter.Result{}
return rcode, dnsfilter.Result{}, err
}
return rcode, err, dnsfilter.Result{Reason: dnsfilter.FilteredSafeSearch}
// TODO: This must be handled in the dnsfilter and not here!
rule := val.String() + " " + host
return rcode, dnsfilter.Result{Reason: dnsfilter.FilteredBlackList, Rule: rule}, err
}
// needs to be filtered instead
result, err := d.d.CheckHost(host)
p.RLock()
result, err := p.d.CheckHost(host)
if err != nil {
log.Printf("plugin/dnsfilter: %s\n", err)
return dns.RcodeServerFailure, fmt.Errorf("plugin/dnsfilter: %s", err), dnsfilter.Result{}
p.RUnlock()
return dns.RcodeServerFailure, dnsfilter.Result{}, fmt.Errorf("plugin/dnsfilter: %s", err)
}
p.RUnlock()
// safebrowsing
if result.IsFiltered == true && result.Reason == dnsfilter.FilteredSafeBrowsing {
// return cname safebrowsing.block.dns.adguard.com
val := d.SafeBrowsingBlockHost
rcode, err := d.replaceHostWithValAndReply(ctx, w, r, host, val, question)
if err != nil {
return rcode, err, dnsfilter.Result{}
if result.IsFiltered {
switch result.Reason {
case dnsfilter.FilteredSafeBrowsing:
// return cname safebrowsing.block.dns.adguard.com
val := p.settings.SafeBrowsingBlockHost
rcode, err := p.replaceHostWithValAndReply(ctx, w, r, host, val, question)
if err != nil {
return rcode, dnsfilter.Result{}, err
}
return rcode, result, err
case dnsfilter.FilteredParental:
// return cname family.block.dns.adguard.com
val := p.settings.ParentalBlockHost
rcode, err := p.replaceHostWithValAndReply(ctx, w, r, host, val, question)
if err != nil {
return rcode, dnsfilter.Result{}, err
}
return rcode, result, err
case dnsfilter.FilteredBlackList:
// return NXdomain
rcode, err := p.writeNXdomain(ctx, w, r)
if err != nil {
return rcode, dnsfilter.Result{}, err
}
return rcode, result, err
case dnsfilter.FilteredInvalid:
// return NXdomain
rcode, err := p.writeNXdomain(ctx, w, r)
if err != nil {
return rcode, dnsfilter.Result{}, err
}
return rcode, result, err
default:
log.Printf("SHOULD NOT HAPPEN -- got unknown reason for filtering host \"%s\": %v, %+v", host, result.Reason, result)
}
return rcode, err, result
}
// parental
if result.IsFiltered == true && result.Reason == dnsfilter.FilteredParental {
// return cname
val := d.ParentalBlockHost
rcode, err := d.replaceHostWithValAndReply(ctx, w, r, host, val, question)
if err != nil {
return rcode, err, dnsfilter.Result{}
} else {
switch result.Reason {
case dnsfilter.NotFilteredWhiteList:
rcode, err := plugin.NextOrFailure(p.Name(), p.Next, ctx, w, r)
return rcode, result, err
case dnsfilter.NotFilteredNotFound:
// do nothing, pass through to lower code
default:
log.Printf("SHOULD NOT HAPPEN -- got unknown reason for not filtering host \"%s\": %v, %+v", host, result.Reason, result)
}
return rcode, err, result
}
// blacklist
if result.IsFiltered == true && result.Reason == dnsfilter.FilteredBlackList {
rcode, err := writeNXdomain(ctx, w, r)
if err != nil {
return rcode, err, dnsfilter.Result{}
}
return rcode, err, result
}
if result.IsFiltered == false && result.Reason == dnsfilter.NotFilteredWhiteList {
rcode, err := plugin.NextOrFailure(d.Name(), d.Next, ctx, w, r)
return rcode, err, result
}
}
rcode, err := plugin.NextOrFailure(d.Name(), d.Next, ctx, w, r)
return rcode, err, dnsfilter.Result{}
rcode, err := plugin.NextOrFailure(p.Name(), p.Next, ctx, w, r)
return rcode, dnsfilter.Result{}, err
}
func (d *Plugin) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
// ServeDNS handles the DNS request and refuses if it's in filterlists
func (p *plug) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
start := time.Now()
requests.Inc()
state := request.Request{W: w, Req: r}
ip := state.IP()
// capture the written answer
rrw := dnstest.NewRecorder(w)
rcode, err, result := d.serveDNSInternal(ctx, rrw, r)
rcode, result, err := p.serveDNSInternal(ctx, rrw, r)
if rcode > 0 {
// actually send the answer if we have one
state := request.Request{W: w, Req: r}
answer := new(dns.Msg)
answer.SetRcode(r, rcode)
state.SizeAndDo(answer)
w.WriteMsg(answer)
err = w.WriteMsg(answer)
if err != nil {
return dns.RcodeServerFailure, err
}
}
// increment counters
@@ -496,12 +566,16 @@ func (d *Plugin) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg)
}
// log
if d.QueryLogEnabled {
logRequest(rrw.Msg, result, time.Since(start))
elapsed := time.Since(start)
elapsedTime.Observe(elapsed.Seconds())
if p.settings.QueryLogEnabled {
logRequest(r, rrw.Msg, result, time.Since(start), ip)
}
return rcode, err
}
func (d *Plugin) Name() string { return "dnsfilter" }
// Name returns name of the plugin as seen in Corefile and plugin.cfg
func (p *plug) Name() string { return "dnsfilter" }
var once sync.Once
var onceHook sync.Once
var onceQueryLog sync.Once

View File

@@ -20,7 +20,8 @@ func TestSetup(t *testing.T) {
config string
failing bool
}{
{`dnsfilter`, true},
{`dnsfilter`, false},
{`dnsfilter /dev/nonexistent/abcdef`, true},
{`dnsfilter ../tests/dns.txt`, false},
{`dnsfilter ../tests/dns.txt { safebrowsing }`, false},
{`dnsfilter ../tests/dns.txt { parental }`, true},
@@ -46,10 +47,10 @@ func TestEtcHostsParse(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if _, err := tmpfile.Write(text); err != nil {
if _, err = tmpfile.Write(text); err != nil {
t.Fatal(err)
}
if err := tmpfile.Close(); err != nil {
if err = tmpfile.Close(); err != nil {
t.Fatal(err)
}
@@ -80,10 +81,10 @@ func TestEtcHostsFilter(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if _, err := tmpfile.Write(text); err != nil {
if _, err = tmpfile.Write(text); err != nil {
t.Fatal(err)
}
if err := tmpfile.Close(); err != nil {
if err = tmpfile.Close(); err != nil {
t.Fatal(err)
}
@@ -126,11 +127,16 @@ func TestEtcHostsFilter(t *testing.T) {
if rcode != rrw.Rcode {
t.Fatalf("ServeDNS return value for host %s has rcode %d that does not match captured rcode %d", testcase.host, rcode, rrw.Rcode)
}
filtered := rcode == dns.RcodeNameError
if testcase.filtered == true && testcase.filtered != filtered {
A, ok := rrw.Msg.Answer[0].(*dns.A)
if !ok {
t.Fatalf("Host %s expected to have result A", testcase.host)
}
ip := net.IPv4(127, 0, 0, 1)
filtered := ip.Equal(A.A)
if testcase.filtered && testcase.filtered != filtered {
t.Fatalf("Host %s expected to be filtered, instead it is not filtered", testcase.host)
}
if testcase.filtered == false && testcase.filtered != filtered {
if !testcase.filtered && testcase.filtered != filtered {
t.Fatalf("Host %s expected to be not filtered, instead it is filtered", testcase.host)
}
}

View File

@@ -0,0 +1,410 @@
package dnsfilter
import (
"encoding/json"
"fmt"
"log"
"net/http"
"sync"
"time"
"github.com/coredns/coredns/plugin"
"github.com/prometheus/client_golang/prometheus"
)
var (
requests = newDNSCounter("requests_total", "Count of requests seen by dnsfilter.")
filtered = newDNSCounter("filtered_total", "Count of requests filtered by dnsfilter.")
filteredLists = newDNSCounter("filtered_lists_total", "Count of requests filtered by dnsfilter using lists.")
filteredSafebrowsing = newDNSCounter("filtered_safebrowsing_total", "Count of requests filtered by dnsfilter using safebrowsing.")
filteredParental = newDNSCounter("filtered_parental_total", "Count of requests filtered by dnsfilter using parental.")
filteredInvalid = newDNSCounter("filtered_invalid_total", "Count of requests filtered by dnsfilter because they were invalid.")
whitelisted = newDNSCounter("whitelisted_total", "Count of requests not filtered by dnsfilter because they are whitelisted.")
safesearch = newDNSCounter("safesearch_total", "Count of requests replaced by dnsfilter safesearch.")
errorsTotal = newDNSCounter("errors_total", "Count of requests that dnsfilter couldn't process because of transitive errors.")
elapsedTime = newDNSHistogram("request_duration", "Histogram of the time (in seconds) each request took.")
)
// entries for single time period (for example all per-second entries)
type statsEntries map[string][statsHistoryElements]float64
// how far back to keep the stats
const statsHistoryElements = 60 + 1 // +1 for calculating delta
// each periodic stat is a map of arrays
type periodicStats struct {
Entries statsEntries
period time.Duration // how long one entry lasts
LastRotate time.Time // last time this data was rotated
sync.RWMutex
}
type stats struct {
PerSecond periodicStats
PerMinute periodicStats
PerHour periodicStats
PerDay periodicStats
}
// per-second/per-minute/per-hour/per-day stats
var statistics stats
func initPeriodicStats(periodic *periodicStats, period time.Duration) {
periodic.Entries = statsEntries{}
periodic.LastRotate = time.Now()
periodic.period = period
}
func init() {
purgeStats()
}
func purgeStats() {
initPeriodicStats(&statistics.PerSecond, time.Second)
initPeriodicStats(&statistics.PerMinute, time.Minute)
initPeriodicStats(&statistics.PerHour, time.Hour)
initPeriodicStats(&statistics.PerDay, time.Hour*24)
}
func (p *periodicStats) Inc(name string, when time.Time) {
// calculate how many periods ago this happened
elapsed := int64(time.Since(when) / p.period)
// trace("%s: %v as %v -> [%v]", name, time.Since(when), p.period, elapsed)
if elapsed >= statsHistoryElements {
return // outside of our timeframe
}
p.Lock()
currentValues := p.Entries[name]
currentValues[elapsed]++
p.Entries[name] = currentValues
p.Unlock()
}
func (p *periodicStats) Observe(name string, when time.Time, value float64) {
// calculate how many periods ago this happened
elapsed := int64(time.Since(when) / p.period)
// trace("%s: %v as %v -> [%v]", name, time.Since(when), p.period, elapsed)
if elapsed >= statsHistoryElements {
return // outside of our timeframe
}
p.Lock()
{
countname := name + "_count"
currentValues := p.Entries[countname]
value := currentValues[elapsed]
// trace("Will change p.Entries[%s][%d] from %v to %v", countname, elapsed, value, value+1)
value += 1
currentValues[elapsed] = value
p.Entries[countname] = currentValues
}
{
totalname := name + "_sum"
currentValues := p.Entries[totalname]
currentValues[elapsed] += value
p.Entries[totalname] = currentValues
}
p.Unlock()
}
func (p *periodicStats) statsRotate(now time.Time) {
p.Lock()
rotations := int64(now.Sub(p.LastRotate) / p.period)
if rotations > statsHistoryElements {
rotations = statsHistoryElements
}
// calculate how many times we should rotate
for r := int64(0); r < rotations; r++ {
for key, values := range p.Entries {
newValues := [statsHistoryElements]float64{}
for i := 1; i < len(values); i++ {
newValues[i] = values[i-1]
}
p.Entries[key] = newValues
}
}
if rotations > 0 {
p.LastRotate = now
}
p.Unlock()
}
func statsRotator() {
for range time.Tick(time.Second) {
now := time.Now()
statistics.PerSecond.statsRotate(now)
statistics.PerMinute.statsRotate(now)
statistics.PerHour.statsRotate(now)
statistics.PerDay.statsRotate(now)
}
}
// counter that wraps around prometheus Counter but also adds to periodic stats
type counter struct {
name string // used as key in periodic stats
value int64
prom prometheus.Counter
}
func newDNSCounter(name string, help string) *counter {
// trace("called")
c := &counter{}
c.prom = prometheus.NewCounter(prometheus.CounterOpts{
Namespace: plugin.Namespace,
Subsystem: "dnsfilter",
Name: name,
Help: help,
})
c.name = name
return c
}
func (c *counter) IncWithTime(when time.Time) {
statistics.PerSecond.Inc(c.name, when)
statistics.PerMinute.Inc(c.name, when)
statistics.PerHour.Inc(c.name, when)
statistics.PerDay.Inc(c.name, when)
c.value++
c.prom.Inc()
}
func (c *counter) Inc() {
c.IncWithTime(time.Now())
}
func (c *counter) Describe(ch chan<- *prometheus.Desc) {
c.prom.Describe(ch)
}
func (c *counter) Collect(ch chan<- prometheus.Metric) {
c.prom.Collect(ch)
}
type histogram struct {
name string // used as key in periodic stats
count int64
total float64
prom prometheus.Histogram
}
func newDNSHistogram(name string, help string) *histogram {
// trace("called")
h := &histogram{}
h.prom = prometheus.NewHistogram(prometheus.HistogramOpts{
Namespace: plugin.Namespace,
Subsystem: "dnsfilter",
Name: name,
Help: help,
})
h.name = name
return h
}
func (h *histogram) ObserveWithTime(value float64, when time.Time) {
statistics.PerSecond.Observe(h.name, when, value)
statistics.PerMinute.Observe(h.name, when, value)
statistics.PerHour.Observe(h.name, when, value)
statistics.PerDay.Observe(h.name, when, value)
h.count++
h.total += value
h.prom.Observe(value)
}
func (h *histogram) Observe(value float64) {
h.ObserveWithTime(value, time.Now())
}
func (h *histogram) Describe(ch chan<- *prometheus.Desc) {
h.prom.Describe(ch)
}
func (h *histogram) Collect(ch chan<- prometheus.Metric) {
h.prom.Collect(ch)
}
// -----
// stats
// -----
func HandleStats(w http.ResponseWriter, r *http.Request) {
const numHours = 24
histrical := generateMapFromStats(&statistics.PerHour, 0, numHours)
// sum them up
summed := map[string]interface{}{}
for key, values := range histrical {
summedValue := 0.0
floats, ok := values.([]float64)
if !ok {
continue
}
for _, v := range floats {
summedValue += v
}
summed[key] = summedValue
}
// don't forget to divide by number of elements in returned slice
if val, ok := summed["avg_processing_time"]; ok {
if flval, flok := val.(float64); flok {
flval /= numHours
summed["avg_processing_time"] = flval
}
}
summed["stats_period"] = "24 hours"
json, err := json.Marshal(summed)
if err != nil {
errortext := fmt.Sprintf("Unable to marshal status json: %s", err)
log.Println(errortext)
http.Error(w, errortext, 500)
return
}
w.Header().Set("Content-Type", "application/json")
_, err = w.Write(json)
if err != nil {
errortext := fmt.Sprintf("Unable to write response json: %s", err)
log.Println(errortext)
http.Error(w, errortext, 500)
return
}
}
func generateMapFromStats(stats *periodicStats, start int, end int) map[string]interface{} {
// clamp
start = clamp(start, 0, statsHistoryElements)
end = clamp(end, 0, statsHistoryElements)
avgProcessingTime := make([]float64, 0)
count := getReversedSlice(stats.Entries[elapsedTime.name+"_count"], start, end)
sum := getReversedSlice(stats.Entries[elapsedTime.name+"_sum"], start, end)
for i := 0; i < len(count); i++ {
var avg float64
if count[i] != 0 {
avg = sum[i] / count[i]
avg *= 1000
}
avgProcessingTime = append(avgProcessingTime, avg)
}
result := map[string]interface{}{
"dns_queries": getReversedSlice(stats.Entries[requests.name], start, end),
"blocked_filtering": getReversedSlice(stats.Entries[filtered.name], start, end),
"replaced_safebrowsing": getReversedSlice(stats.Entries[filteredSafebrowsing.name], start, end),
"replaced_safesearch": getReversedSlice(stats.Entries[safesearch.name], start, end),
"replaced_parental": getReversedSlice(stats.Entries[filteredParental.name], start, end),
"avg_processing_time": avgProcessingTime,
}
return result
}
func HandleStatsHistory(w http.ResponseWriter, r *http.Request) {
// handle time unit and prepare our time window size
now := time.Now()
timeUnitString := r.URL.Query().Get("time_unit")
var stats *periodicStats
var timeUnit time.Duration
switch timeUnitString {
case "seconds":
timeUnit = time.Second
stats = &statistics.PerSecond
case "minutes":
timeUnit = time.Minute
stats = &statistics.PerMinute
case "hours":
timeUnit = time.Hour
stats = &statistics.PerHour
case "days":
timeUnit = time.Hour * 24
stats = &statistics.PerDay
default:
http.Error(w, "Must specify valid time_unit parameter", 400)
return
}
// parse start and end time
startTime, err := time.Parse(time.RFC3339, r.URL.Query().Get("start_time"))
if err != nil {
errortext := fmt.Sprintf("Must specify valid start_time parameter: %s", err)
log.Println(errortext)
http.Error(w, errortext, 400)
return
}
endTime, err := time.Parse(time.RFC3339, r.URL.Query().Get("end_time"))
if err != nil {
errortext := fmt.Sprintf("Must specify valid end_time parameter: %s", err)
log.Println(errortext)
http.Error(w, errortext, 400)
return
}
// check if start and time times are within supported time range
timeRange := timeUnit * statsHistoryElements
if startTime.Add(timeRange).Before(now) {
http.Error(w, "start_time parameter is outside of supported range", 501)
return
}
if endTime.Add(timeRange).Before(now) {
http.Error(w, "end_time parameter is outside of supported range", 501)
return
}
// calculate start and end of our array
// basically it's how many hours/minutes/etc have passed since now
start := int(now.Sub(endTime) / timeUnit)
end := int(now.Sub(startTime) / timeUnit)
// swap them around if they're inverted
if start > end {
start, end = end, start
}
data := generateMapFromStats(stats, start, end)
json, err := json.Marshal(data)
if err != nil {
errortext := fmt.Sprintf("Unable to marshal status json: %s", err)
log.Println(errortext)
http.Error(w, errortext, 500)
return
}
w.Header().Set("Content-Type", "application/json")
_, err = w.Write(json)
if err != nil {
errortext := fmt.Sprintf("Unable to write response json: %s", err)
log.Println(errortext)
http.Error(w, errortext, 500)
return
}
}
func HandleStatsReset(w http.ResponseWriter, r *http.Request) {
purgeStats()
_, err := fmt.Fprintf(w, "OK\n")
if err != nil {
errortext := fmt.Sprintf("Couldn't write body: %s", err)
log.Println(errortext)
http.Error(w, errortext, http.StatusInternalServerError)
}
}
func clamp(value, low, high int) int {
if value < low {
return low
}
if value > high {
return high
}
return value
}
// --------------------------
// helper functions for stats
// --------------------------
func getReversedSlice(input [statsHistoryElements]float64, start int, end int) []float64 {
output := make([]float64, 0)
for i := start; i <= end; i++ {
output = append([]float64{input[i]}, output...)
}
return output
}

View File

@@ -5,70 +5,166 @@ import (
"fmt"
"log"
"net/http"
"os"
"path"
"runtime"
"strconv"
"strings"
"sync"
"time"
"github.com/AdguardTeam/AdguardDNS/dnsfilter"
"github.com/AdguardTeam/AdGuardHome/dnsfilter"
"github.com/coredns/coredns/plugin/pkg/response"
"github.com/miekg/dns"
"github.com/zfjagann/golang-ring"
)
var logBuffer = ring.Ring{}
const (
logBufferCap = 5000 // maximum capacity of logBuffer before it's flushed to disk
queryLogTimeLimit = time.Hour * 24 // how far in the past we care about querylogs
queryLogRotationPeriod = time.Hour * 24 // rotate the log every 24 hours
queryLogFileName = "querylog.json" // .gz added during compression
queryLogSize = 5000 // maximum API response for /querylog
queryLogTopSize = 500 // Keep in memory only top N values
queryLogAPIPort = "8618" // 8618 is sha512sum of "querylog" then each byte summed
)
var (
logBufferLock sync.RWMutex
logBuffer []*logEntry
queryLogCache []*logEntry
queryLogLock sync.RWMutex
queryLogTime time.Time
)
type logEntry struct {
R *dns.Msg
Result dnsfilter.Result
Time time.Time
Elapsed time.Duration
Question []byte
Answer []byte `json:",omitempty"` // sometimes empty answers happen like binerdunt.top or rev2.globalrootservers.net
Result dnsfilter.Result
Time time.Time
Elapsed time.Duration
IP string
}
func init() {
logBuffer.SetCapacity(1000)
}
func logRequest(question *dns.Msg, answer *dns.Msg, result dnsfilter.Result, elapsed time.Duration, ip string) {
var q []byte
var a []byte
var err error
func logRequest(r *dns.Msg, result dnsfilter.Result, elapsed time.Duration) {
entry := logEntry{
R: r,
Result: result,
Time: time.Now(),
Elapsed: elapsed,
if question != nil {
q, err = question.Pack()
if err != nil {
log.Printf("failed to pack question for querylog: %s", err)
return
}
}
if answer != nil {
a, err = answer.Pack()
if err != nil {
log.Printf("failed to pack answer for querylog: %s", err)
return
}
}
now := time.Now()
entry := logEntry{
Question: q,
Answer: a,
Result: result,
Time: now,
Elapsed: elapsed,
IP: ip,
}
var flushBuffer []*logEntry
logBufferLock.Lock()
logBuffer = append(logBuffer, &entry)
if len(logBuffer) >= logBufferCap {
flushBuffer = logBuffer
logBuffer = nil
}
logBufferLock.Unlock()
queryLogLock.Lock()
queryLogCache = append(queryLogCache, &entry)
if len(queryLogCache) > queryLogSize {
toremove := len(queryLogCache) - queryLogSize
queryLogCache = queryLogCache[toremove:]
}
queryLogLock.Unlock()
// add it to running top
err = runningTop.addEntry(&entry, question, now)
if err != nil {
log.Printf("Failed to add entry to running top: %s", err)
// don't do failure, just log
}
// if buffer needs to be flushed to disk, do it now
if len(flushBuffer) > 0 {
// write to file
// do it in separate goroutine -- we are stalling DNS response this whole time
go flushToFile(flushBuffer)
}
logBuffer.Enqueue(entry)
}
func handler(w http.ResponseWriter, r *http.Request) {
values := logBuffer.Values()
func HandleQueryLog(w http.ResponseWriter, r *http.Request) {
queryLogLock.RLock()
values := make([]*logEntry, len(queryLogCache))
copy(values, queryLogCache)
queryLogLock.RUnlock()
// reverse it so that newest is first
for left, right := 0, len(values)-1; left < right; left, right = left+1, right-1 {
values[left], values[right] = values[right], values[left]
}
var data = []map[string]interface{}{}
for _, value := range values {
entry, ok := value.(logEntry)
if !ok {
continue
for _, entry := range values {
var q *dns.Msg
var a *dns.Msg
if len(entry.Question) > 0 {
q = new(dns.Msg)
if err := q.Unpack(entry.Question); err != nil {
// ignore, log and move on
log.Printf("Failed to unpack dns message question: %s", err)
q = nil
}
}
if len(entry.Answer) > 0 {
a = new(dns.Msg)
if err := a.Unpack(entry.Answer); err != nil {
// ignore, log and move on
log.Printf("Failed to unpack dns message question: %s", err)
a = nil
}
}
jsonentry := map[string]interface{}{
"reason": entry.Result.Reason.String(),
"elapsed_ms": strconv.FormatFloat(entry.Elapsed.Seconds()*1000, 'f', -1, 64),
"time": entry.Time.Format(time.RFC3339),
"client": entry.IP,
}
question := map[string]interface{}{
"host": strings.ToLower(strings.TrimSuffix(entry.R.Question[0].Name, ".")),
"type": dns.Type(entry.R.Question[0].Qtype).String(),
"class": dns.Class(entry.R.Question[0].Qclass).String(),
if q != nil {
jsonentry["question"] = map[string]interface{}{
"host": strings.ToLower(strings.TrimSuffix(q.Question[0].Name, ".")),
"type": dns.Type(q.Question[0].Qtype).String(),
"class": dns.Class(q.Question[0].Qclass).String(),
}
}
jsonentry["question"] = question
status, _ := response.Typify(entry.R, time.Now().UTC())
jsonentry["status"] = status.String()
if a != nil {
status, _ := response.Typify(a, time.Now().UTC())
jsonentry["status"] = status.String()
}
if len(entry.Result.Rule) > 0 {
jsonentry["rule"] = entry.Result.Rule
}
if len(entry.R.Answer) > 0 {
if a != nil && len(a.Answer) > 0 {
var answers = []map[string]interface{}{}
for _, k := range entry.R.Answer {
for _, k := range a.Answer {
header := k.Header()
answer := map[string]interface{}{
"type": dns.TypeToString[header.Rrtype],
@@ -125,22 +221,20 @@ func handler(w http.ResponseWriter, r *http.Request) {
if err != nil {
errortext := fmt.Sprintf("Unable to write response json: %s", err)
log.Println(errortext)
http.Error(w, errortext, 500)
http.Error(w, errortext, http.StatusInternalServerError)
}
}
func startQueryLogServer() {
listenAddr := "127.0.0.1:8618" // sha512sum of "querylog" then each byte summed
http.HandleFunc("/querylog", handler)
if err := http.ListenAndServe(listenAddr, nil); err != nil {
log.Fatalf("error in ListenAndServe: %s", err)
}
}
func trace(text string) {
func trace(format string, args ...interface{}) {
pc := make([]uintptr, 10) // at least 1 entry needed
runtime.Callers(2, pc)
f := runtime.FuncForPC(pc[0])
log.Printf("%s(): %s\n", f.Name(), text)
var buf strings.Builder
buf.WriteString(fmt.Sprintf("%s(): ", path.Base(f.Name())))
text := fmt.Sprintf(format, args...)
buf.WriteString(text)
if len(text) == 0 || text[len(text)-1] != '\n' {
buf.WriteRune('\n')
}
fmt.Fprint(os.Stderr, buf.String())
}

View File

@@ -0,0 +1,291 @@
package dnsfilter
import (
"bytes"
"compress/gzip"
"encoding/json"
"fmt"
"log"
"os"
"sync"
"time"
"github.com/go-test/deep"
)
var (
fileWriteLock sync.Mutex
)
const enableGzip = false
func flushToFile(buffer []*logEntry) error {
if len(buffer) == 0 {
return nil
}
start := time.Now()
var b bytes.Buffer
e := json.NewEncoder(&b)
for _, entry := range buffer {
err := e.Encode(entry)
if err != nil {
log.Printf("Failed to marshal entry: %s", err)
return err
}
}
elapsed := time.Since(start)
log.Printf("%d elements serialized via json in %v: %d kB, %v/entry, %v/entry", len(buffer), elapsed, b.Len()/1024, float64(b.Len())/float64(len(buffer)), elapsed/time.Duration(len(buffer)))
err := checkBuffer(buffer, b)
if err != nil {
log.Printf("failed to check buffer: %s", err)
return err
}
var zb bytes.Buffer
filename := queryLogFileName
// gzip enabled?
if enableGzip {
filename += ".gz"
zw := gzip.NewWriter(&zb)
zw.Name = queryLogFileName
zw.ModTime = time.Now()
_, err = zw.Write(b.Bytes())
if err != nil {
log.Printf("Couldn't compress to gzip: %s", err)
zw.Close()
return err
}
if err = zw.Close(); err != nil {
log.Printf("Couldn't close gzip writer: %s", err)
return err
}
} else {
zb = b
}
fileWriteLock.Lock()
defer fileWriteLock.Unlock()
f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
if err != nil {
log.Printf("failed to create file \"%s\": %s", filename, err)
return err
}
defer f.Close()
n, err := f.Write(zb.Bytes())
if err != nil {
log.Printf("Couldn't write to file: %s", err)
return err
}
log.Printf("ok \"%s\": %v bytes written", filename, n)
return nil
}
func checkBuffer(buffer []*logEntry, b bytes.Buffer) error {
l := len(buffer)
d := json.NewDecoder(&b)
i := 0
for d.More() {
entry := &logEntry{}
err := d.Decode(entry)
if err != nil {
log.Printf("Failed to decode: %s", err)
return err
}
if diff := deep.Equal(entry, buffer[i]); diff != nil {
log.Printf("decoded buffer differs: %s", diff)
return fmt.Errorf("decoded buffer differs: %s", diff)
}
i++
}
if i != l {
err := fmt.Errorf("check fail: %d vs %d entries", l, i)
log.Print(err)
return err
}
log.Printf("check ok: %d entries", i)
return nil
}
func rotateQueryLog() error {
from := queryLogFileName
to := queryLogFileName + ".1"
if enableGzip {
from = queryLogFileName + ".gz"
to = queryLogFileName + ".gz.1"
}
if _, err := os.Stat(from); os.IsNotExist(err) {
// do nothing, file doesn't exist
return nil
}
err := os.Rename(from, to)
if err != nil {
log.Printf("Failed to rename querylog: %s", err)
return err
}
log.Printf("Rotated from %s to %s successfully", from, to)
return nil
}
func periodicQueryLogRotate() {
for range time.Tick(queryLogRotationPeriod) {
err := rotateQueryLog()
if err != nil {
log.Printf("Failed to rotate querylog: %s", err)
// do nothing, continue rotating
}
}
}
func genericLoader(onEntry func(entry *logEntry) error, needMore func() bool, timeWindow time.Duration) error {
now := time.Now()
// read from querylog files, try newest file first
files := []string{}
if enableGzip {
files = []string{
queryLogFileName + ".gz",
queryLogFileName + ".gz.1",
}
} else {
files = []string{
queryLogFileName,
queryLogFileName + ".1",
}
}
// read from all files
for _, file := range files {
if !needMore() {
break
}
if _, err := os.Stat(file); os.IsNotExist(err) {
// do nothing, file doesn't exist
continue
}
f, err := os.Open(file)
if err != nil {
log.Printf("Failed to open file \"%s\": %s", file, err)
// try next file
continue
}
defer f.Close()
var d *json.Decoder
if enableGzip {
trace("Creating gzip reader")
zr, err := gzip.NewReader(f)
if err != nil {
log.Printf("Failed to create gzip reader: %s", err)
continue
}
defer zr.Close()
trace("Creating json decoder")
d = json.NewDecoder(zr)
} else {
d = json.NewDecoder(f)
}
i := 0
over := 0
max := 10000 * time.Second
var sum time.Duration
// entries on file are in oldest->newest order
// we want maxLen newest
for d.More() {
if !needMore() {
break
}
var entry logEntry
err := d.Decode(&entry)
if err != nil {
log.Printf("Failed to decode: %s", err)
// next entry can be fine, try more
continue
}
if now.Sub(entry.Time) > timeWindow {
// trace("skipping entry") // debug logging
continue
}
if entry.Elapsed > max {
over++
} else {
sum += entry.Elapsed
}
i++
err = onEntry(&entry)
if err != nil {
return err
}
}
elapsed := time.Since(now)
var perunit time.Duration
var avg time.Duration
if i > 0 {
perunit = elapsed / time.Duration(i)
avg = sum / time.Duration(i)
}
log.Printf("file \"%s\": read %d entries in %v, %v/entry, %v over %v, %v avg", file, i, elapsed, perunit, over, max, avg)
}
return nil
}
func appendFromLogFile(values []*logEntry, maxLen int, timeWindow time.Duration) []*logEntry {
a := []*logEntry{}
onEntry := func(entry *logEntry) error {
a = append(a, entry)
if len(a) > maxLen {
toskip := len(a) - maxLen
a = a[toskip:]
}
return nil
}
needMore := func() bool {
return true
}
err := genericLoader(onEntry, needMore, timeWindow)
if err != nil {
log.Printf("Failed to load entries from querylog: %s", err)
return values
}
// now that we've read all eligible entries, reverse the slice to make it go from newest->oldest
for left, right := 0, len(a)-1; left < right; left, right = left+1, right-1 {
a[left], a[right] = a[right], a[left]
}
// append it to values
values = append(values, a...)
// then cut off of it is bigger than maxLen
if len(values) > maxLen {
values = values[:maxLen]
}
return values
}

View File

@@ -0,0 +1,386 @@
package dnsfilter
import (
"bytes"
"fmt"
"log"
"net/http"
"os"
"path"
"runtime"
"sort"
"strconv"
"strings"
"sync"
"time"
"github.com/AdguardTeam/AdGuardHome/dnsfilter"
"github.com/bluele/gcache"
"github.com/miekg/dns"
)
type hourTop struct {
domains gcache.Cache
blocked gcache.Cache
clients gcache.Cache
mutex sync.RWMutex
}
func (top *hourTop) init() {
top.domains = gcache.New(queryLogTopSize).LRU().Build()
top.blocked = gcache.New(queryLogTopSize).LRU().Build()
top.clients = gcache.New(queryLogTopSize).LRU().Build()
}
type dayTop struct {
hours []*hourTop
hoursLock sync.RWMutex // writelock this lock ONLY WHEN rotating or intializing hours!
loaded bool
loadedLock sync.Mutex
}
var runningTop dayTop
func init() {
runningTop.hoursWriteLock()
for i := 0; i < 24; i++ {
hour := hourTop{}
hour.init()
runningTop.hours = append(runningTop.hours, &hour)
}
runningTop.hoursWriteUnlock()
}
func rotateHourlyTop() {
log.Printf("Rotating hourly top")
hour := &hourTop{}
hour.init()
runningTop.hoursWriteLock()
runningTop.hours = append([]*hourTop{hour}, runningTop.hours...)
runningTop.hours = runningTop.hours[:24]
runningTop.hoursWriteUnlock()
}
func periodicHourlyTopRotate() {
t := time.Hour
for range time.Tick(t) {
rotateHourlyTop()
}
}
func (top *hourTop) incrementValue(key string, cache gcache.Cache) error {
top.Lock()
defer top.Unlock()
ivalue, err := cache.Get(key)
if err == gcache.KeyNotFoundError {
// we just set it and we're done
err = cache.Set(key, 1)
if err != nil {
log.Printf("Failed to set hourly top value: %s", err)
return err
}
return nil
}
if err != nil {
log.Printf("gcache encountered an error during get: %s", err)
return err
}
cachedValue, ok := ivalue.(int)
if !ok {
err = fmt.Errorf("SHOULD NOT HAPPEN: gcache has non-int as value: %v", ivalue)
log.Println(err)
return err
}
err = cache.Set(key, cachedValue+1)
if err != nil {
log.Printf("Failed to set hourly top value: %s", err)
return err
}
return nil
}
func (top *hourTop) incrementDomains(key string) error {
return top.incrementValue(key, top.domains)
}
func (top *hourTop) incrementBlocked(key string) error {
return top.incrementValue(key, top.blocked)
}
func (top *hourTop) incrementClients(key string) error {
return top.incrementValue(key, top.clients)
}
// if does not exist -- return 0
func (top *hourTop) lockedGetValue(key string, cache gcache.Cache) (int, error) {
ivalue, err := cache.Get(key)
if err == gcache.KeyNotFoundError {
return 0, nil
}
if err != nil {
log.Printf("gcache encountered an error during get: %s", err)
return 0, err
}
value, ok := ivalue.(int)
if !ok {
err := fmt.Errorf("SHOULD NOT HAPPEN: gcache has non-int as value: %v", ivalue)
log.Println(err)
return 0, err
}
return value, nil
}
func (top *hourTop) lockedGetDomains(key string) (int, error) {
return top.lockedGetValue(key, top.domains)
}
func (top *hourTop) lockedGetBlocked(key string) (int, error) {
return top.lockedGetValue(key, top.blocked)
}
func (top *hourTop) lockedGetClients(key string) (int, error) {
return top.lockedGetValue(key, top.clients)
}
func (r *dayTop) addEntry(entry *logEntry, q *dns.Msg, now time.Time) error {
// figure out which hour bucket it belongs to
hour := int(now.Sub(entry.Time).Hours())
if hour >= 24 {
log.Printf("t %v is >24 hours ago, ignoring", entry.Time)
return nil
}
hostname := strings.ToLower(strings.TrimSuffix(q.Question[0].Name, "."))
// get value, if not set, crate one
runningTop.hoursReadLock()
defer runningTop.hoursReadUnlock()
err := runningTop.hours[hour].incrementDomains(hostname)
if err != nil {
log.Printf("Failed to increment value: %s", err)
return err
}
if entry.Result.IsFiltered {
err := runningTop.hours[hour].incrementBlocked(hostname)
if err != nil {
log.Printf("Failed to increment value: %s", err)
return err
}
}
if len(entry.IP) > 0 {
err := runningTop.hours[hour].incrementClients(entry.IP)
if err != nil {
log.Printf("Failed to increment value: %s", err)
return err
}
}
return nil
}
func fillStatsFromQueryLog() error {
now := time.Now()
runningTop.loadedWriteLock()
defer runningTop.loadedWriteUnlock()
if runningTop.loaded {
return nil
}
onEntry := func(entry *logEntry) error {
if len(entry.Question) == 0 {
log.Printf("entry question is absent, skipping")
return nil
}
if entry.Time.After(now) {
log.Printf("t %v vs %v is in the future, ignoring", entry.Time, now)
return nil
}
q := new(dns.Msg)
if err := q.Unpack(entry.Question); err != nil {
log.Printf("failed to unpack dns message question: %s", err)
return err
}
if len(q.Question) != 1 {
log.Printf("malformed dns message, has no questions, skipping")
return nil
}
err := runningTop.addEntry(entry, q, now)
if err != nil {
log.Printf("Failed to add entry to running top: %s", err)
return err
}
queryLogLock.Lock()
queryLogCache = append(queryLogCache, entry)
if len(queryLogCache) > queryLogSize {
toremove := len(queryLogCache) - queryLogSize
queryLogCache = queryLogCache[toremove:]
}
queryLogLock.Unlock()
requests.IncWithTime(entry.Time)
if entry.Result.IsFiltered {
filtered.IncWithTime(entry.Time)
}
switch entry.Result.Reason {
case dnsfilter.NotFilteredWhiteList:
whitelisted.IncWithTime(entry.Time)
case dnsfilter.NotFilteredError:
errorsTotal.IncWithTime(entry.Time)
case dnsfilter.FilteredBlackList:
filteredLists.IncWithTime(entry.Time)
case dnsfilter.FilteredSafeBrowsing:
filteredSafebrowsing.IncWithTime(entry.Time)
case dnsfilter.FilteredParental:
filteredParental.IncWithTime(entry.Time)
case dnsfilter.FilteredInvalid:
// do nothing
case dnsfilter.FilteredSafeSearch:
safesearch.IncWithTime(entry.Time)
}
elapsedTime.ObserveWithTime(entry.Elapsed.Seconds(), entry.Time)
return nil
}
needMore := func() bool { return true }
err := genericLoader(onEntry, needMore, queryLogTimeLimit)
if err != nil {
log.Printf("Failed to load entries from querylog: %s", err)
return err
}
runningTop.loaded = true
return nil
}
func HandleStatsTop(w http.ResponseWriter, r *http.Request) {
domains := map[string]int{}
blocked := map[string]int{}
clients := map[string]int{}
do := func(keys []interface{}, getter func(key string) (int, error), result map[string]int) {
for _, ikey := range keys {
key, ok := ikey.(string)
if !ok {
continue
}
value, err := getter(key)
if err != nil {
log.Printf("Failed to get top domains value for %v: %s", key, err)
return
}
result[key] += value
}
}
runningTop.hoursReadLock()
for hour := 0; hour < 24; hour++ {
runningTop.hours[hour].RLock()
do(runningTop.hours[hour].domains.Keys(), runningTop.hours[hour].lockedGetDomains, domains)
do(runningTop.hours[hour].blocked.Keys(), runningTop.hours[hour].lockedGetBlocked, blocked)
do(runningTop.hours[hour].clients.Keys(), runningTop.hours[hour].lockedGetClients, clients)
runningTop.hours[hour].RUnlock()
}
runningTop.hoursReadUnlock()
// use manual json marshalling because we want maps to be sorted by value
json := bytes.Buffer{}
json.WriteString("{\n")
gen := func(json *bytes.Buffer, name string, top map[string]int, addComma bool) {
json.WriteString(" ")
json.WriteString(fmt.Sprintf("%q", name))
json.WriteString(": {\n")
sorted := sortByValue(top)
// no more than 50 entries
if len(sorted) > 50 {
sorted = sorted[:50]
}
for i, key := range sorted {
json.WriteString(" ")
json.WriteString(fmt.Sprintf("%q", key))
json.WriteString(": ")
json.WriteString(strconv.Itoa(top[key]))
if i+1 != len(sorted) {
json.WriteByte(',')
}
json.WriteByte('\n')
}
json.WriteString(" }")
if addComma {
json.WriteByte(',')
}
json.WriteByte('\n')
}
gen(&json, "top_queried_domains", domains, true)
gen(&json, "top_blocked_domains", blocked, true)
gen(&json, "top_clients", clients, true)
json.WriteString(" \"stats_period\": \"24 hours\"\n")
json.WriteString("}\n")
w.Header().Set("Content-Type", "application/json")
_, err := w.Write(json.Bytes())
if err != nil {
errortext := fmt.Sprintf("Couldn't write body: %s", err)
log.Println(errortext)
http.Error(w, errortext, http.StatusInternalServerError)
}
}
// helper function for querylog API
func sortByValue(m map[string]int) []string {
type kv struct {
k string
v int
}
var ss []kv
for k, v := range m {
ss = append(ss, kv{k, v})
}
sort.Slice(ss, func(l, r int) bool {
return ss[l].v > ss[r].v
})
sorted := []string{}
for _, v := range ss {
sorted = append(sorted, v.k)
}
return sorted
}
func (d *dayTop) hoursWriteLock() { tracelock(); d.hoursLock.Lock() }
func (d *dayTop) hoursWriteUnlock() { tracelock(); d.hoursLock.Unlock() }
func (d *dayTop) hoursReadLock() { tracelock(); d.hoursLock.RLock() }
func (d *dayTop) hoursReadUnlock() { tracelock(); d.hoursLock.RUnlock() }
func (d *dayTop) loadedWriteLock() { tracelock(); d.loadedLock.Lock() }
func (d *dayTop) loadedWriteUnlock() { tracelock(); d.loadedLock.Unlock() }
func (h *hourTop) Lock() { tracelock(); h.mutex.Lock() }
func (h *hourTop) RLock() { tracelock(); h.mutex.RLock() }
func (h *hourTop) RUnlock() { tracelock(); h.mutex.RUnlock() }
func (h *hourTop) Unlock() { tracelock(); h.mutex.Unlock() }
func tracelock() {
if false { // not commented out to make code checked during compilation
pc := make([]uintptr, 10) // at least 1 entry needed
runtime.Callers(2, pc)
f := path.Base(runtime.FuncForPC(pc[1]).Name())
lockf := path.Base(runtime.FuncForPC(pc[0]).Name())
fmt.Fprintf(os.Stderr, "%s(): %s\n", f, lockf)
}
}

View File

@@ -4,7 +4,6 @@ import (
"errors"
"log"
"strconv"
"sync"
"time"
// ratelimiting and per-ip buckets
@@ -29,8 +28,8 @@ var (
tokenBuckets = cache.New(time.Hour, time.Hour)
)
// main function
func (p *Plugin) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
// ServeDNS handles the DNS request and refuses if it's an beyind specified ratelimit
func (p *plug) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
state := request.Request{W: w, Req: r}
ip := state.IP()
allow, err := p.allowRequest(ip)
@@ -44,7 +43,7 @@ func (p *Plugin) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg)
return plugin.NextOrFailure(p.Name(), p.Next, ctx, w, r)
}
func (p *Plugin) allowRequest(ip string) (bool, error) {
func (p *plug) allowRequest(ip string) (bool, error) {
if _, found := tokenBuckets.Get(ip); !found {
tokenBuckets.Set(ip, rate.New(p.ratelimit, time.Second), time.Hour)
}
@@ -59,7 +58,7 @@ func (p *Plugin) allowRequest(ip string) (bool, error) {
}
rl, ok := value.(*rate.RateLimiter)
if ok == false {
if !ok {
text := "SHOULD NOT HAPPEN: non-bool entry found in safebrowsing lookup cache"
log.Println(text)
err := errors.New(text)
@@ -80,7 +79,7 @@ func init() {
})
}
type Plugin struct {
type plug struct {
Next plugin.Handler
// configuration for creating above
@@ -88,7 +87,7 @@ type Plugin struct {
}
func setup(c *caddy.Controller) error {
p := &Plugin{ratelimit: defaultRatelimit}
p := &plug{ratelimit: defaultRatelimit}
config := dnsserver.GetConfig(c)
for c.Next() {
@@ -109,22 +108,20 @@ func setup(c *caddy.Controller) error {
})
c.OnStartup(func() error {
once.Do(func() {
m := dnsserver.GetConfig(c).Handler("prometheus")
if m == nil {
return
}
if x, ok := m.(*metrics.Metrics); ok {
x.MustRegister(ratelimited)
}
})
m := dnsserver.GetConfig(c).Handler("prometheus")
if m == nil {
return nil
}
if x, ok := m.(*metrics.Metrics); ok {
x.MustRegister(ratelimited)
}
return nil
})
return nil
}
func newDnsCounter(name string, help string) prometheus.Counter {
func newDNSCounter(name string, help string) prometheus.Counter {
return prometheus.NewCounter(prometheus.CounterOpts{
Namespace: plugin.Namespace,
Subsystem: "ratelimit",
@@ -134,9 +131,8 @@ func newDnsCounter(name string, help string) prometheus.Counter {
}
var (
ratelimited = newDnsCounter("dropped_total", "Count of requests that have been dropped because of rate limit")
ratelimited = newDNSCounter("dropped_total", "Count of requests that have been dropped because of rate limit")
)
func (d *Plugin) Name() string { return "ratelimit" }
var once sync.Once
// Name returns name of the plugin as seen in Corefile and plugin.cfg
func (p *plug) Name() string { return "ratelimit" }

View File

@@ -3,7 +3,6 @@ package refuseany
import (
"fmt"
"log"
"sync"
"github.com/coredns/coredns/core/dnsserver"
"github.com/coredns/coredns/plugin"
@@ -15,11 +14,12 @@ import (
"golang.org/x/net/context"
)
type Plugin struct {
type plug struct {
Next plugin.Handler
}
func (p *Plugin) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
// ServeDNS handles the DNS request and refuses if it's an ANY request
func (p *plug) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
if len(r.Question) != 1 {
// google DNS, bind and others do the same
return dns.RcodeFormatError, fmt.Errorf("Got DNS request with != 1 questions")
@@ -41,9 +41,9 @@ func (p *Plugin) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg)
return dns.RcodeServerFailure, err
}
return rcode, nil
} else {
return plugin.NextOrFailure(p.Name(), p.Next, ctx, w, r)
}
return plugin.NextOrFailure(p.Name(), p.Next, ctx, w, r)
}
func init() {
@@ -54,7 +54,7 @@ func init() {
}
func setup(c *caddy.Controller) error {
p := &Plugin{}
p := &plug{}
config := dnsserver.GetConfig(c)
config.AddPlugin(func(next plugin.Handler) plugin.Handler {
@@ -63,22 +63,20 @@ func setup(c *caddy.Controller) error {
})
c.OnStartup(func() error {
once.Do(func() {
m := dnsserver.GetConfig(c).Handler("prometheus")
if m == nil {
return
}
if x, ok := m.(*metrics.Metrics); ok {
x.MustRegister(ratelimited)
}
})
m := dnsserver.GetConfig(c).Handler("prometheus")
if m == nil {
return nil
}
if x, ok := m.(*metrics.Metrics); ok {
x.MustRegister(ratelimited)
}
return nil
})
return nil
}
func newDnsCounter(name string, help string) prometheus.Counter {
func newDNSCounter(name string, help string) prometheus.Counter {
return prometheus.NewCounter(prometheus.CounterOpts{
Namespace: plugin.Namespace,
Subsystem: "refuseany",
@@ -88,9 +86,8 @@ func newDnsCounter(name string, help string) prometheus.Counter {
}
var (
ratelimited = newDnsCounter("refusedany_total", "Count of ANY requests that have been dropped")
ratelimited = newDNSCounter("refusedany_total", "Count of ANY requests that have been dropped")
)
func (d *Plugin) Name() string { return "refuseany" }
var once sync.Once
// Name returns name of the plugin as seen in Corefile and plugin.cfg
func (p *plug) Name() string { return "refuseany" }

36
coredns_plugin/reload.go Normal file
View File

@@ -0,0 +1,36 @@
package dnsfilter
import (
"log"
"github.com/mholt/caddy"
)
var Reload = make(chan bool)
func hook(event caddy.EventName, info interface{}) error {
if event != caddy.InstanceStartupEvent {
return nil
}
// this should be an instance. ok to panic if not
instance := info.(*caddy.Instance)
go func() {
for range Reload {
corefile, err := caddy.LoadCaddyfile(instance.Caddyfile().ServerType())
if err != nil {
continue
}
_, err = instance.Restart(corefile)
if err != nil {
log.Printf("Corefile changed but reload failed: %s", err)
continue
}
// hook will be called again from new instance
return
}
}()
return nil
}

View File

@@ -1,9 +1,9 @@
# AdGuard DNS Go library
# AdGuard Home's DNS filtering go library
Example use:
```bash
[ -z "$GOPATH" ] && export GOPATH=$HOME/go
go get -d github.com/AdguardTeam/AdguardDNS/dnsfilter
go get -d github.com/AdguardTeam/AdGuardHome/dnsfilter
```
Create file filter.go
@@ -11,7 +11,7 @@ Create file filter.go
package main
import (
"github.com/AdguardTeam/AdguardDNS/dnsfilter"
"github.com/AdguardTeam/AdGuardHome/dnsfilter"
"log"
)
@@ -48,7 +48,7 @@ You can also enable checking against AdGuard's SafeBrowsing:
package main
import (
"github.com/AdguardTeam/AdguardDNS/dnsfilter"
"github.com/AdguardTeam/AdGuardHome/dnsfilter"
"log"
)

View File

@@ -16,6 +16,7 @@ import (
"sync/atomic"
"time"
_ "github.com/benburkert/dns/init"
"github.com/bluele/gcache"
"golang.org/x/net/publicsuffix"
)
@@ -28,9 +29,13 @@ const defaultHTTPMaxIdleConnections = 100
const defaultSafebrowsingServer = "sb.adtidy.org"
const defaultSafebrowsingURL = "http://%s/safebrowsing-lookup-hash.html?prefixes=%s"
const defaultParentalURL = "http://pctrl.adguard.com/check-parental-control-hash?prefixes=%s&sensitivity=%d"
const defaultParentalServer = "pctrl.adguard.com"
const defaultParentalURL = "http://%s/check-parental-control-hash?prefixes=%s&sensitivity=%d"
// ErrInvalidSyntax is returned by AddRule when rule is invalid
var ErrInvalidSyntax = errors.New("dnsfilter: invalid rule syntax")
// ErrInvalidParental is returned by EnableParental when sensitivity is not a valid value
var ErrInvalidParental = errors.New("dnsfilter: invalid parental sensitivity, must be either 3, 10, 13 or 17")
const shortcutLength = 6 // used for rule search optimization, 6 hits the sweet spot
@@ -38,15 +43,16 @@ const shortcutLength = 6 // used for rule search optimization, 6 hits the sweet
const enableFastLookup = true // flag for debugging, must be true in production for faster performance
const enableDelayedCompilation = true // flag for debugging, must be true in production for faster performance
type Config struct {
type config struct {
parentalServer string
parentalSensitivity int // must be either 3, 10, 13 or 17
parentalEnabled bool
safeSearchEnabled bool
safeBrowsingEnabled bool
safeBrowsingServer string
parentalEnabled bool
parentalSensitivity int // must be either 3, 10, 13 or 17
}
type Rule struct {
type rule struct {
text string // text without @@ decorators or $ options
shortcut string // for speeding up lookup
originalText string // original text for reporting back to applications
@@ -55,19 +61,24 @@ type Rule struct {
options []string // optional options after $
// parsed options
apps []string
isWhitelist bool
isImportant bool
apps []string
// user-supplied data
listID uint32
// suffix matching
isSuffix bool
suffix string
// compiled regexp
compiled *regexp.Regexp
sync.RWMutex
}
// LookupStats store stats collected during safebrowsing or parental checks
type LookupStats struct {
Requests uint64 // number of HTTP requests that were sent
CacheHits uint64 // number of lookups that didn't need HTTP requests
@@ -75,6 +86,7 @@ type LookupStats struct {
PendingMax int64 // maximum number of pending HTTP requests
}
// Stats store LookupStats for both safebrowsing and parental
type Stats struct {
Safebrowsing LookupStats
Parental LookupStats
@@ -82,7 +94,7 @@ type Stats struct {
// Dnsfilter holds added rules and performs hostname matches against the rules
type Dnsfilter struct {
storage map[string]*Rule // rule storage, not used for matching, needs to be key->value
storage map[string]*rule // rule storage, not used for matching, needs to be key->value
storageMutex sync.RWMutex
// rules are checked against these lists in the order defined here
@@ -94,12 +106,12 @@ type Dnsfilter struct {
client http.Client // handle for http client -- single instance as recommended by docs
transport *http.Transport // handle for http transport used by http client
config Config
config config
}
//go:generate stringer -type=Reason
// filtered/notfiltered reason
// Reason holds an enum detailing why it was filtered or not filtered
type Reason int
const (
@@ -119,27 +131,29 @@ const (
// these variables need to survive coredns reload
var (
stats Stats
safebrowsingCache = gcache.New(defaultCacheSize).LRU().Expiration(defaultCacheTime).Build()
parentalCache = gcache.New(defaultCacheSize).LRU().Expiration(defaultCacheTime).Build()
safebrowsingCache gcache.Cache
parentalCache gcache.Cache
)
// search result
// Result holds state of hostname check
type Result struct {
IsFiltered bool
Reason Reason
Rule string
IsFiltered bool `json:",omitempty"`
Reason Reason `json:",omitempty"`
Rule string `json:",omitempty"`
}
// Matched can be used to see if any match at all was found, no matter filtered or not
func (r Reason) Matched() bool {
return r != NotFilteredNotFound
}
// CheckHost tries to match host against rules, then safebrowsing and parental if they are enabled
func (d *Dnsfilter) CheckHost(host string) (Result, error) {
// sometimes DNS clients will try to resolve ".", which in turns transforms into "" when it reaches here
// sometimes DNS clients will try to resolve ".", which is a request to get root servers
if host == "" {
return Result{Reason: FilteredInvalid}, nil
return Result{Reason: NotFilteredNotFound}, nil
}
host = strings.ToLower(host)
// try filter lists first
result, err := d.matchHost(host)
@@ -185,19 +199,19 @@ func (d *Dnsfilter) CheckHost(host string) (Result, error) {
//
type rulesTable struct {
rulesByShortcut map[string][]*Rule
rulesLeftovers []*Rule
rulesByShortcut map[string][]*rule
rulesLeftovers []*rule
sync.RWMutex
}
func newRulesTable() *rulesTable {
return &rulesTable{
rulesByShortcut: make(map[string][]*Rule),
rulesLeftovers: make([]*Rule, 0),
rulesByShortcut: make(map[string][]*rule),
rulesLeftovers: make([]*rule, 0),
}
}
func (r *rulesTable) Add(rule *Rule) {
func (r *rulesTable) Add(rule *rule) {
r.Lock()
if len(rule.shortcut) == shortcutLength && enableFastLookup {
r.rulesByShortcut[rule.shortcut] = append(r.rulesByShortcut[rule.shortcut], rule)
@@ -292,7 +306,7 @@ func findOptionIndex(text string) int {
return -1
}
func (rule *Rule) extractOptions() error {
func (rule *rule) extractOptions() error {
optIndex := findOptionIndex(rule.text)
if optIndex == 0 { // starts with $
return ErrInvalidSyntax
@@ -330,7 +344,7 @@ func (rule *Rule) extractOptions() error {
return nil
}
func (rule *Rule) parseOptions() error {
func (rule *rule) parseOptions() error {
err := rule.extractOptions()
if err != nil {
return err
@@ -351,7 +365,7 @@ func (rule *Rule) parseOptions() error {
return nil
}
func (rule *Rule) extractShortcut() {
func (rule *rule) extractShortcut() {
// regex rules have no shortcuts
if rule.text[0] == '/' && rule.text[len(rule.text)-1] == '/' {
return
@@ -376,14 +390,23 @@ func (rule *Rule) extractShortcut() {
rule.shortcut = strings.ToLower(longestField)
}
func (rule *Rule) compile() error {
func (rule *rule) compile() error {
rule.RLock()
isCompiled := rule.compiled != nil
isCompiled := rule.isSuffix || rule.compiled != nil
rule.RUnlock()
if isCompiled {
return nil
}
isSuffix, suffix := getSuffix(rule.text)
if isSuffix {
rule.Lock()
rule.isSuffix = isSuffix
rule.suffix = suffix
rule.Unlock()
return nil
}
expr, err := ruleToRegexp(rule.text)
if err != nil {
return err
@@ -401,14 +424,23 @@ func (rule *Rule) compile() error {
return nil
}
func (rule *Rule) match(host string) (Result, error) {
func (rule *rule) match(host string) (Result, error) {
res := Result{}
err := rule.compile()
if err != nil {
return res, err
}
rule.RLock()
matched := rule.compiled.MatchString(host)
matched := false
if rule.isSuffix {
if host == rule.suffix {
matched = true
} else if strings.HasSuffix(host, "."+rule.suffix) {
matched = true
}
} else {
matched = rule.compiled.MatchString(host)
}
rule.RUnlock()
if matched {
res.Reason = FilteredBlackList
@@ -439,7 +471,7 @@ func getCachedReason(cache gcache.Cache, host string) (result Result, isFound bo
// since it can be something else, validate that it belongs to proper type
cachedValue, ok := rawValue.(Result)
if ok == false {
if !ok {
// this is not our type -- error
text := "SHOULD NOT HAPPEN: entry with invalid type was found in lookup cache"
log.Println(text)
@@ -455,7 +487,7 @@ func hostnameToHashParam(host string, addslash bool) (string, map[string]bool) {
var hashparam bytes.Buffer
hashes := map[string]bool{}
tld, icann := publicsuffix.PublicSuffix(host)
if icann == false {
if !icann {
// private suffixes like cloudfront.net
tld = ""
}
@@ -487,6 +519,10 @@ func hostnameToHashParam(host string, addslash bool) (string, map[string]bool) {
}
func (d *Dnsfilter) checkSafeBrowsing(host string) (Result, error) {
// prevent recursion -- checking the host of safebrowsing server makes no sense
if host == d.config.safeBrowsingServer {
return Result{}, nil
}
format := func(hashparam string) string {
url := fmt.Sprintf(defaultSafebrowsingURL, d.config.safeBrowsingServer, hashparam)
return url
@@ -516,21 +552,29 @@ func (d *Dnsfilter) checkSafeBrowsing(host string) (Result, error) {
}
return result, nil
}
if safebrowsingCache == nil {
safebrowsingCache = gcache.New(defaultCacheSize).LRU().Expiration(defaultCacheTime).Build()
}
result, err := d.lookupCommon(host, &stats.Safebrowsing, safebrowsingCache, true, format, handleBody)
return result, err
}
func (d *Dnsfilter) checkParental(host string) (Result, error) {
format2 := func(hashparam string) string {
url := fmt.Sprintf(defaultParentalURL, hashparam, d.config.parentalSensitivity)
// prevent recursion -- checking the host of parental safety server makes no sense
if host == d.config.parentalServer {
return Result{}, nil
}
format := func(hashparam string) string {
url := fmt.Sprintf(defaultParentalURL, d.config.parentalServer, hashparam, d.config.parentalSensitivity)
return url
}
handleBody2 := func(body []byte, hashes map[string]bool) (Result, error) {
handleBody := func(body []byte, hashes map[string]bool) (Result, error) {
// parse json
var m []struct {
Blocked bool `json:"blocked"`
ClientTTL int `json:"clientTtl"`
Reason string `json:"reason"`
Hash string `json:"hash"`
}
err := json.Unmarshal(body, &m)
if err != nil {
@@ -542,6 +586,9 @@ func (d *Dnsfilter) checkParental(host string) (Result, error) {
result := Result{}
for i := range m {
if !hashes[m[i].Hash] {
continue
}
if m[i].Blocked {
result.IsFiltered = true
result.Reason = FilteredParental
@@ -551,7 +598,10 @@ func (d *Dnsfilter) checkParental(host string) (Result, error) {
}
return result, nil
}
result, err := d.lookupCommon(host, &stats.Parental, parentalCache, false, format2, handleBody2)
if parentalCache == nil {
parentalCache = gcache.New(defaultCacheSize).LRU().Expiration(defaultCacheTime).Build()
}
result, err := d.lookupCommon(host, &stats.Parental, parentalCache, false, format, handleBody)
return result, err
}
@@ -563,7 +613,7 @@ func (d *Dnsfilter) lookupCommon(host string, lookupstats *LookupStats, cache gc
// check cache
cachedValue, isFound, err := getCachedReason(cache, host)
if isFound {
atomic.AddUint64(&stats.Safebrowsing.CacheHits, 1)
atomic.AddUint64(&lookupstats.CacheHits, 1)
return cachedValue, nil
}
if err != nil {
@@ -601,7 +651,10 @@ func (d *Dnsfilter) lookupCommon(host string, lookupstats *LookupStats, cache gc
switch {
case resp.StatusCode == 204:
// empty result, save cache
cache.Set(host, Result{})
err = cache.Set(host, Result{})
if err != nil {
return Result{}, err
}
return Result{}, nil
case resp.StatusCode != 200:
// error, don't save cache
@@ -614,7 +667,10 @@ func (d *Dnsfilter) lookupCommon(host string, lookupstats *LookupStats, cache gc
return Result{}, err
}
cache.Set(host, result)
err = cache.Set(host, result)
if err != nil {
return Result{}, err
}
return result, nil
}
@@ -637,7 +693,7 @@ func (d *Dnsfilter) AddRule(input string, filterListID uint32) error {
return ErrInvalidSyntax
}
rule := Rule{
rule := rule{
text: input, // will be modified
originalText: input,
listID: filterListID,
@@ -701,10 +757,11 @@ func (d *Dnsfilter) matchHost(host string) (Result, error) {
// lifecycle helper functions
//
// New creates properly initialized DNS Filter that is ready to be used
func New() *Dnsfilter {
d := new(Dnsfilter)
d.storage = make(map[string]*Rule)
d.storage = make(map[string]*rule)
d.important = newRulesTable()
d.whiteList = newRulesTable()
d.blackList = newRulesTable()
@@ -723,21 +780,29 @@ func New() *Dnsfilter {
Timeout: defaultHTTPTimeout,
}
d.config.safeBrowsingServer = defaultSafebrowsingServer
d.config.parentalServer = defaultParentalServer
return d
}
// Destroy is optional if you want to tidy up goroutines without waiting for them to die off
// right now it closes idle HTTP connections if there are any
func (d *Dnsfilter) Destroy() {
d.transport.CloseIdleConnections()
if d != nil && d.transport != nil {
d.transport.CloseIdleConnections()
}
}
//
// config manipulation helpers
//
// EnableSafeBrowsing turns on checking hostnames in malware/phishing database
func (d *Dnsfilter) EnableSafeBrowsing() {
d.config.safeBrowsingEnabled = true
}
// EnableParental turns on checking hostnames for containing adult content
func (d *Dnsfilter) EnableParental(sensitivity int) error {
switch sensitivity {
case 3, 10, 13, 17:
@@ -749,10 +814,13 @@ func (d *Dnsfilter) EnableParental(sensitivity int) error {
}
}
// EnableSafeSearch turns on enforcing safesearch in search engines
// only used in coredns plugin and requires caller to use SafeSearchDomain()
func (d *Dnsfilter) EnableSafeSearch() {
d.config.safeSearchEnabled = true
}
// SetSafeBrowsingServer lets you optionally change hostname of safesearch lookup
func (d *Dnsfilter) SetSafeBrowsingServer(host string) {
if len(host) == 0 {
d.config.safeBrowsingServer = defaultSafebrowsingServer
@@ -761,38 +829,35 @@ func (d *Dnsfilter) SetSafeBrowsingServer(host string) {
}
}
// SetHTTPTimeout lets you optionally change timeout during lookups
func (d *Dnsfilter) SetHTTPTimeout(t time.Duration) {
d.client.Timeout = t
}
// ResetHTTPTimeout resets lookup timeouts
func (d *Dnsfilter) ResetHTTPTimeout() {
d.client.Timeout = defaultHTTPTimeout
}
// SafeSearchDomain returns replacement address for search engine
func (d *Dnsfilter) SafeSearchDomain(host string) (string, bool) {
if d.config.safeSearchEnabled == false {
return "", false
if d.config.safeSearchEnabled {
val, ok := safeSearchDomains[host]
return val, ok
}
val, ok := safeSearchDomains[host]
return val, ok
return "", false
}
//
// stats
//
// GetStats return dns filtering stats since startup
func (d *Dnsfilter) GetStats() Stats {
return stats
}
// Count returns number of rules added to filter
func (d *Dnsfilter) Count() int {
return len(d.storage)
}
//
// cache control, right now needed only for tests
//
func purgeCaches() {
safebrowsingCache.Purge()
parentalCache.Purge()
}

View File

@@ -1,8 +1,14 @@
package dnsfilter
import (
"archive/zip"
"bytes"
"io/ioutil"
"net/http"
"net/http/httptest"
"path"
"runtime/pprof"
"strings"
"testing"
"time"
@@ -11,9 +17,175 @@ import (
"os"
"runtime"
"github.com/shirou/gopsutil/process"
"go.uber.org/goleak"
)
// first in file because it must be run first
func TestLotsOfRulesMemoryUsage(t *testing.T) {
start := getRSS()
trace("RSS before loading rules - %d kB\n", start/1024)
dumpMemProfile(_Func() + "1.pprof")
d := NewForTest()
defer d.Destroy()
err := loadTestRules(d)
if err != nil {
t.Error(err)
}
afterLoad := getRSS()
trace("RSS after loading rules - %d kB (%d kB diff)\n", afterLoad/1024, (afterLoad-start)/1024)
dumpMemProfile(_Func() + "2.pprof")
tests := []struct {
host string
match bool
}{
{"asdasdasd_adsajdasda_asdasdjashdkasdasdasdasd_adsajdasda_asdasdjashdkasd.thisistesthost.com", false},
{"asdasdasd_adsajdasda_asdasdjashdkasdasdasdasd_adsajdasda_asdasdjashdkasd.ad.doubleclick.net", true},
}
for _, testcase := range tests {
ret, err := d.CheckHost(testcase.host)
if err != nil {
t.Errorf("Error while matching host %s: %s", testcase.host, err)
}
if !ret.IsFiltered && ret.IsFiltered != testcase.match {
t.Errorf("Expected hostname %s to not match", testcase.host)
}
if ret.IsFiltered && ret.IsFiltered != testcase.match {
t.Errorf("Expected hostname %s to match", testcase.host)
}
}
afterMatch := getRSS()
trace("RSS after matching - %d kB (%d kB diff)\n", afterMatch/1024, (afterMatch-afterLoad)/1024)
dumpMemProfile(_Func() + "3.pprof")
}
func getRSS() uint64 {
proc, err := process.NewProcess(int32(os.Getpid()))
if err != nil {
panic(err)
}
minfo, err := proc.MemoryInfo()
return minfo.RSS
}
func dumpMemProfile(name string) {
runtime.GC()
f, err := os.Create(name)
if err != nil {
panic(err)
}
defer f.Close()
runtime.GC() // update the stats before writing them
err = pprof.WriteHeapProfile(f)
if err != nil {
panic(err)
}
}
const topHostsFilename = "../tests/top-1m.csv"
func fetchTopHostsFromNet() {
trace("Fetching top hosts from network")
resp, err := http.Get("http://s3-us-west-1.amazonaws.com/umbrella-static/top-1m.csv.zip")
if err != nil {
panic(err)
}
defer resp.Body.Close()
trace("Reading zipfile body")
zipfile, err := ioutil.ReadAll(resp.Body)
if err != nil {
panic(err)
}
trace("Opening zipfile")
r, err := zip.NewReader(bytes.NewReader(zipfile), int64(len(zipfile)))
if err != nil {
panic(err)
}
if len(r.File) != 1 {
panic(fmt.Errorf("zipfile must have only one entry: %+v", r))
}
f := r.File[0]
trace("Unpacking file %s from zipfile", f.Name)
rc, err := f.Open()
if err != nil {
panic(err)
}
trace("Reading file %s contents", f.Name)
body, err := ioutil.ReadAll(rc)
if err != nil {
panic(err)
}
rc.Close()
trace("Writing file %s contents to disk", f.Name)
err = ioutil.WriteFile(topHostsFilename+".tmp", body, 0644)
if err != nil {
panic(err)
}
err = os.Rename(topHostsFilename+".tmp", topHostsFilename)
if err != nil {
panic(err)
}
}
func getTopHosts() {
// if file doesn't exist, fetch it
if _, err := os.Stat(topHostsFilename); os.IsNotExist(err) {
// file does not exist, fetch it
fetchTopHostsFromNet()
}
}
func TestLotsOfRulesLotsOfHostsMemoryUsage(t *testing.T) {
start := getRSS()
trace("RSS before loading rules - %d kB\n", start/1024)
dumpMemProfile(_Func() + "1.pprof")
d := NewForTest()
defer d.Destroy()
mustLoadTestRules(d)
trace("Have %d rules", d.Count())
afterLoad := getRSS()
trace("RSS after loading rules - %d kB (%d kB diff)\n", afterLoad/1024, (afterLoad-start)/1024)
dumpMemProfile(_Func() + "2.pprof")
getTopHosts()
hostnames, err := os.Open(topHostsFilename)
if err != nil {
t.Fatal(err)
}
defer hostnames.Close()
afterHosts := getRSS()
trace("RSS after loading hosts - %d kB (%d kB diff)\n", afterHosts/1024, (afterHosts-afterLoad)/1024)
dumpMemProfile(_Func() + "2.pprof")
{
scanner := bufio.NewScanner(hostnames)
for scanner.Scan() {
line := scanner.Text()
records := strings.Split(line, ",")
ret, err := d.CheckHost(records[1] + "." + records[1])
if err != nil {
t.Error(err)
}
if ret.Reason.Matched() {
// log.Printf("host \"%s\" mathed. Rule \"%s\", reason: %v", host, ret.Rule, ret.Reason)
}
}
}
afterMatch := getRSS()
trace("RSS after matching - %d kB (%d kB diff)\n", afterMatch/1024, (afterMatch-afterLoad)/1024)
dumpMemProfile(_Func() + "3.pprof")
}
func TestRuleToRegexp(t *testing.T) {
tests := []struct {
rule string
@@ -23,7 +195,7 @@ func TestRuleToRegexp(t *testing.T) {
{"/doubleclick/", "doubleclick", nil},
{"/", "", ErrInvalidSyntax},
{`|double*?.+[]|(){}#$\|`, `^double.*\?\.\+\[\]\|\(\)\{\}\#\$\\$`, nil},
{`||doubleclick.net^`, `^([a-z0-9-_.]+\.)?doubleclick\.net([^ a-zA-Z0-9.%]|$)`, nil},
{`||doubleclick.net^`, `(?:^|\.)doubleclick\.net$`, nil},
}
for _, testcase := range tests {
converted, err := ruleToRegexp(testcase.rule)
@@ -36,6 +208,38 @@ func TestRuleToRegexp(t *testing.T) {
}
}
func TestSuffixRule(t *testing.T) {
for _, testcase := range []struct {
rule string
isSuffix bool
suffix string
}{
{`||doubleclick.net^`, true, `doubleclick.net`}, // entire string or subdomain match
{`||doubleclick.net|`, true, `doubleclick.net`}, // entire string or subdomain match
{`|doubleclick.net^`, false, ``}, // TODO: ends with doubleclick.net
{`*doubleclick.net^`, false, ``}, // TODO: ends with doubleclick.net
{`doubleclick.net^`, false, ``}, // TODO: ends with doubleclick.net
{`|*doubleclick.net^`, false, ``}, // TODO: ends with doubleclick.net
{`||*doubleclick.net^`, false, ``}, // TODO: ends with doubleclick.net
{`||*doubleclick.net|`, false, ``}, // TODO: ends with doubleclick.net
{`||*doublec*lick.net^`, false, ``}, // has a wildcard inside, has to be regexp
{`||*doublec|lick.net^`, false, ``}, // has a special symbol inside, has to be regexp
{`/abracadabra/`, false, ``}, // regexp, not anchored
{`/abracadabra$/`, false, ``}, // TODO: simplify simple suffix regexes
} {
isSuffix, suffix := getSuffix(testcase.rule)
if testcase.isSuffix != isSuffix {
t.Errorf("Results do not match for \"%s\": got %v expected %v", testcase.rule, isSuffix, testcase.isSuffix)
continue
}
if testcase.isSuffix && testcase.suffix != suffix {
t.Errorf("Result suffix does not match for \"%s\": got \"%s\" expected \"%s\"", testcase.rule, suffix, testcase.suffix)
continue
}
// trace("\"%s\": %v: %s", testcase.rule, isSuffix, suffix)
}
}
//
// helper functions
//
@@ -112,6 +316,13 @@ func loadTestRules(d *Dnsfilter) error {
return err
}
func mustLoadTestRules(d *Dnsfilter) {
err := loadTestRules(d)
if err != nil {
panic(err)
}
}
func NewForTest() *Dnsfilter {
d := New()
purgeCaches()
@@ -124,7 +335,9 @@ func NewForTest() *Dnsfilter {
func TestSanityCheck(t *testing.T) {
d := NewForTest()
defer d.Destroy()
d.checkAddRule(t, "||doubleclick.net^")
d.checkMatch(t, "doubleclick.net")
d.checkMatch(t, "www.doubleclick.net")
d.checkMatchEmpty(t, "nodoubleclick.net")
d.checkMatchEmpty(t, "doubleclick.net.ru")
@@ -132,6 +345,72 @@ func TestSanityCheck(t *testing.T) {
d.checkAddRuleFail(t, "lkfaojewhoawehfwacoefawr$@#$@3413841384")
}
func TestSuffixMatching1(t *testing.T) {
d := NewForTest()
defer d.Destroy()
d.checkAddRule(t, "||doubleclick.net^")
d.checkMatch(t, "doubleclick.net")
d.checkMatch(t, "www.doubleclick.net")
d.checkMatchEmpty(t, "nodoubleclick.net")
d.checkMatchEmpty(t, "doubleclick.net.ru")
}
func TestSuffixMatching2(t *testing.T) {
d := NewForTest()
defer d.Destroy()
d.checkAddRule(t, "|doubleclick.net^")
d.checkMatch(t, "doubleclick.net")
d.checkMatchEmpty(t, "www.doubleclick.net")
d.checkMatchEmpty(t, "nodoubleclick.net")
d.checkMatchEmpty(t, "doubleclick.net.ru")
}
func TestSuffixMatching3(t *testing.T) {
d := NewForTest()
defer d.Destroy()
d.checkAddRule(t, "doubleclick.net^")
d.checkMatch(t, "doubleclick.net")
d.checkMatch(t, "www.doubleclick.net")
d.checkMatch(t, "nodoubleclick.net")
d.checkMatchEmpty(t, "doubleclick.net.ru")
}
func TestSuffixMatching4(t *testing.T) {
d := NewForTest()
defer d.Destroy()
d.checkAddRule(t, "*doubleclick.net^")
d.checkMatch(t, "doubleclick.net")
d.checkMatch(t, "www.doubleclick.net")
d.checkMatch(t, "nodoubleclick.net")
d.checkMatchEmpty(t, "doubleclick.net.ru")
}
func TestSuffixMatching5(t *testing.T) {
d := NewForTest()
defer d.Destroy()
d.checkAddRule(t, "|*doubleclick.net^")
d.checkMatch(t, "doubleclick.net")
d.checkMatch(t, "www.doubleclick.net")
d.checkMatch(t, "nodoubleclick.net")
d.checkMatchEmpty(t, "doubleclick.net.ru")
}
func TestSuffixMatching6(t *testing.T) {
d := NewForTest()
defer d.Destroy()
d.checkAddRule(t, "||*doubleclick.net^")
d.checkMatch(t, "doubleclick.net")
d.checkMatch(t, "www.doubleclick.net")
d.checkMatch(t, "nodoubleclick.net")
d.checkMatchEmpty(t, "doubleclick.net.ru")
}
func TestCount(t *testing.T) {
d := NewForTest()
defer d.Destroy()
@@ -217,52 +496,6 @@ func TestAddRuleFail(t *testing.T) {
d.checkAddRuleFail(t, "lkfaojewhoawehfwacoefawr$@#$@3413841384")
}
func printMemStats(r runtime.MemStats) {
fmt.Printf("Alloc: %.2f, HeapAlloc: %.2f Mb, Sys: %.2f Mb, HeapSys: %.2f Mb\n",
float64(r.Alloc)/1024.0/1024.0, float64(r.HeapAlloc)/1024.0/1024.0,
float64(r.Sys)/1024.0/1024.0, float64(r.HeapSys)/1024.0/1024.0)
}
func TestLotsOfRulesMemoryUsage(t *testing.T) {
var start, afterLoad, end runtime.MemStats
runtime.GC()
runtime.ReadMemStats(&start)
fmt.Printf("Memory usage before loading rules - %d kB alloc, %d kB sys\n", start.Alloc/1024, start.Sys/1024)
d := NewForTest()
defer d.Destroy()
err := loadTestRules(d)
if err != nil {
t.Error(err)
}
runtime.GC()
runtime.ReadMemStats(&afterLoad)
fmt.Printf("Memory usage after loading rules - %d kB alloc, %d kB sys\n", afterLoad.Alloc/1024, afterLoad.Sys/1024)
tests := []struct {
host string
match bool
}{
{"asdasdasd_adsajdasda_asdasdjashdkasdasdasdasd_adsajdasda_asdasdjashdkasd.thisistesthost.com", false},
{"asdasdasd_adsajdasda_asdasdjashdkasdasdasdasd_adsajdasda_asdasdjashdkasd.ad.doubleclick.net", true},
}
for _, testcase := range tests {
ret, err := d.CheckHost(testcase.host)
if err != nil {
t.Errorf("Error while matching host %s: %s", testcase.host, err)
}
if ret.IsFiltered == false && ret.IsFiltered != testcase.match {
t.Errorf("Expected hostname %s to not match", testcase.host)
}
if ret.IsFiltered == true && ret.IsFiltered != testcase.match {
t.Errorf("Expected hostname %s to match", testcase.host)
}
}
runtime.GC()
runtime.ReadMemStats(&end)
fmt.Printf("Memory usage after matching - %d kB alloc, %d kB sys\n", afterLoad.Alloc/1024, afterLoad.Sys/1024)
}
func TestSafeBrowsing(t *testing.T) {
testCases := []string{
"",
@@ -356,6 +589,8 @@ func TestParentalControl(t *testing.T) {
if stats.Parental.Requests != l {
t.Errorf("Parental lookup negative cache is not working")
}
d.checkMatchEmpty(t, "api.jquery.com")
}
func TestSafeSearch(t *testing.T) {
@@ -385,43 +620,44 @@ var regexRules = []string{"/example\\.org/", "@@||test.example.org^"}
var maskRules = []string{"test*.example.org^", "exam*.com"}
var tests = []struct {
testname string
rules []string
hostname string
result bool
testname string
rules []string
hostname string
isFiltered bool
reason Reason
}{
{"sanity", []string{"||doubleclick.net^"}, "www.doubleclick.net", true},
{"sanity", []string{"||doubleclick.net^"}, "nodoubleclick.net", false},
{"sanity", []string{"||doubleclick.net^"}, "doubleclick.net.ru", false},
{"sanity", []string{"||doubleclick.net^"}, "wmconvirus.narod.ru", false},
{"blocking", blockingRules, "example.org", true},
{"blocking", blockingRules, "test.example.org", true},
{"blocking", blockingRules, "test.test.example.org", true},
{"blocking", blockingRules, "testexample.org", false},
{"blocking", blockingRules, "onemoreexample.org", false},
{"whitelist", whitelistRules, "example.org", true},
{"whitelist", whitelistRules, "test.example.org", false},
{"whitelist", whitelistRules, "test.test.example.org", false},
{"whitelist", whitelistRules, "testexample.org", false},
{"whitelist", whitelistRules, "onemoreexample.org", false},
{"important", importantRules, "example.org", false},
{"important", importantRules, "test.example.org", true},
{"important", importantRules, "test.test.example.org", true},
{"important", importantRules, "testexample.org", false},
{"important", importantRules, "onemoreexample.org", false},
{"regex", regexRules, "example.org", true},
{"regex", regexRules, "test.example.org", false},
{"regex", regexRules, "test.test.example.org", false},
{"regex", regexRules, "testexample.org", true},
{"regex", regexRules, "onemoreexample.org", true},
{"mask", maskRules, "test.example.org", true},
{"mask", maskRules, "test2.example.org", true},
{"mask", maskRules, "example.com", true},
{"mask", maskRules, "exampleeee.com", true},
{"mask", maskRules, "onemoreexamsite.com", true},
{"mask", maskRules, "example.org", false},
{"mask", maskRules, "testexample.org", false},
{"mask", maskRules, "example.co.uk", false},
{"sanity", []string{"||doubleclick.net^"}, "www.doubleclick.net", true, FilteredBlackList},
{"sanity", []string{"||doubleclick.net^"}, "nodoubleclick.net", false, NotFilteredNotFound},
{"sanity", []string{"||doubleclick.net^"}, "doubleclick.net.ru", false, NotFilteredNotFound},
{"sanity", []string{"||doubleclick.net^"}, "wmconvirus.narod.ru", false, NotFilteredNotFound},
{"blocking", blockingRules, "example.org", true, FilteredBlackList},
{"blocking", blockingRules, "test.example.org", true, FilteredBlackList},
{"blocking", blockingRules, "test.test.example.org", true, FilteredBlackList},
{"blocking", blockingRules, "testexample.org", false, NotFilteredNotFound},
{"blocking", blockingRules, "onemoreexample.org", false, NotFilteredNotFound},
{"whitelist", whitelistRules, "example.org", true, FilteredBlackList},
{"whitelist", whitelistRules, "test.example.org", false, NotFilteredWhiteList},
{"whitelist", whitelistRules, "test.test.example.org", false, NotFilteredWhiteList},
{"whitelist", whitelistRules, "testexample.org", false, NotFilteredNotFound},
{"whitelist", whitelistRules, "onemoreexample.org", false, NotFilteredNotFound},
{"important", importantRules, "example.org", false, NotFilteredWhiteList},
{"important", importantRules, "test.example.org", true, FilteredBlackList},
{"important", importantRules, "test.test.example.org", true, FilteredBlackList},
{"important", importantRules, "testexample.org", false, NotFilteredNotFound},
{"important", importantRules, "onemoreexample.org", false, NotFilteredNotFound},
{"regex", regexRules, "example.org", true, FilteredBlackList},
{"regex", regexRules, "test.example.org", false, NotFilteredWhiteList},
{"regex", regexRules, "test.test.example.org", false, NotFilteredWhiteList},
{"regex", regexRules, "testexample.org", true, FilteredBlackList},
{"regex", regexRules, "onemoreexample.org", true, FilteredBlackList},
{"mask", maskRules, "test.example.org", true, FilteredBlackList},
{"mask", maskRules, "test2.example.org", true, FilteredBlackList},
{"mask", maskRules, "example.com", true, FilteredBlackList},
{"mask", maskRules, "exampleeee.com", true, FilteredBlackList},
{"mask", maskRules, "onemoreexamsite.com", true, FilteredBlackList},
{"mask", maskRules, "example.org", false, NotFilteredNotFound},
{"mask", maskRules, "testexample.org", false, NotFilteredNotFound},
{"mask", maskRules, "example.co.uk", false, NotFilteredNotFound},
}
func TestMatching(t *testing.T) {
@@ -439,8 +675,11 @@ func TestMatching(t *testing.T) {
if err != nil {
t.Errorf("Error while matching host %s: %s", test.hostname, err)
}
if ret.IsFiltered != test.result {
t.Errorf("Hostname %s has wrong result (%v must be %v)", test.hostname, ret, test.result)
if ret.IsFiltered != test.isFiltered {
t.Errorf("Hostname %s has wrong result (%v must be %v)", test.hostname, ret.IsFiltered, test.isFiltered)
}
if ret.Reason != test.reason {
t.Errorf("Hostname %s has wrong reason (%v must be %v)", test.hostname, ret.Reason.String(), test.reason.String())
}
})
}
@@ -569,6 +808,80 @@ func BenchmarkLotsOfRulesMatchParallel(b *testing.B) {
})
}
func BenchmarkLotsOfRulesLotsOfHosts(b *testing.B) {
d := NewForTest()
defer d.Destroy()
mustLoadTestRules(d)
getTopHosts()
hostnames, err := os.Open(topHostsFilename)
if err != nil {
b.Fatal(err)
}
defer hostnames.Close()
scanner := bufio.NewScanner(hostnames)
b.ResetTimer()
for n := 0; n < b.N; n++ {
havedata := scanner.Scan()
if !havedata {
hostnames.Seek(0, 0)
scanner = bufio.NewScanner(hostnames)
havedata = scanner.Scan()
}
if !havedata {
b.Fatal(scanner.Err())
}
line := scanner.Text()
records := strings.Split(line, ",")
ret, err := d.CheckHost(records[1] + "." + records[1])
if err != nil {
b.Error(err)
}
if ret.Reason.Matched() {
// log.Printf("host \"%s\" mathed. Rule \"%s\", reason: %v", host, ret.Rule, ret.Reason)
}
}
}
func BenchmarkLotsOfRulesLotsOfHostsParallel(b *testing.B) {
d := NewForTest()
defer d.Destroy()
mustLoadTestRules(d)
getTopHosts()
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
hostnames, err := os.Open(topHostsFilename)
if err != nil {
b.Fatal(err)
}
defer hostnames.Close()
scanner := bufio.NewScanner(hostnames)
for pb.Next() {
havedata := scanner.Scan()
if !havedata {
hostnames.Seek(0, 0)
scanner = bufio.NewScanner(hostnames)
havedata = scanner.Scan()
}
if !havedata {
b.Fatal(scanner.Err())
}
line := scanner.Text()
records := strings.Split(line, ",")
ret, err := d.CheckHost(records[1] + "." + records[1])
if err != nil {
b.Error(err)
}
if ret.Reason.Matched() {
// log.Printf("host \"%s\" mathed. Rule \"%s\", reason: %v", host, ret.Rule, ret.Reason)
}
}
})
}
func BenchmarkSafeBrowsing(b *testing.B) {
d := NewForTest()
defer d.Destroy()
@@ -638,3 +951,36 @@ func BenchmarkSafeSearchParallel(b *testing.B) {
func TestMain(m *testing.M) {
goleak.VerifyTestMain(m)
}
//
// helper functions for debugging and testing
//
func purgeCaches() {
if safebrowsingCache != nil {
safebrowsingCache.Purge()
}
if parentalCache != nil {
parentalCache.Purge()
}
}
func _Func() string {
pc := make([]uintptr, 10) // at least 1 entry needed
runtime.Callers(2, pc)
f := runtime.FuncForPC(pc[0])
return path.Base(f.Name())
}
func trace(format string, args ...interface{}) {
pc := make([]uintptr, 10) // at least 1 entry needed
runtime.Callers(2, pc)
f := runtime.FuncForPC(pc[0])
var buf strings.Builder
buf.WriteString(fmt.Sprintf("%s(): ", path.Base(f.Name())))
text := fmt.Sprintf(format, args...)
buf.WriteString(text)
if len(text) == 0 || text[len(text)-1] != '\n' {
buf.WriteRune('\n')
}
fmt.Print(buf.String())
}

View File

@@ -1,9 +1,6 @@
package dnsfilter
import (
"fmt"
"path"
"runtime"
"strings"
"sync/atomic"
)
@@ -49,33 +46,9 @@ func updateMax(valuePtr *int64, maxPtr *int64) {
break
}
swapped := atomic.CompareAndSwapInt64(maxPtr, max, current)
if swapped == true {
if swapped {
break
}
// swapping failed because value has changed after reading, try again
}
}
//
// helper functions for debugging and testing
//
func _Func() string {
pc := make([]uintptr, 10) // at least 1 entry needed
runtime.Callers(2, pc)
f := runtime.FuncForPC(pc[0])
return path.Base(f.Name())
}
func trace(format string, args ...interface{}) {
pc := make([]uintptr, 10) // at least 1 entry needed
runtime.Callers(2, pc)
f := runtime.FuncForPC(pc[0])
var buf strings.Builder
buf.WriteString(fmt.Sprintf("%s(): ", path.Base(f.Name())))
text := fmt.Sprintf(format, args...)
buf.WriteString(text)
if len(text) == 0 || text[len(text)-1] != '\n' {
buf.WriteRune('\n')
}
fmt.Print(buf.String())
}

View File

@@ -5,8 +5,8 @@ import (
)
func ruleToRegexp(rule string) (string, error) {
const hostStart = "^([a-z0-9-_.]+\\.)?"
const hostEnd = "([^ a-zA-Z0-9.%]|$)"
const hostStart = `(?:^|\.)`
const hostEnd = `$`
// empty or short rule -- do nothing
if !isValidRule(rule) {
@@ -49,3 +49,38 @@ func ruleToRegexp(rule string) (string, error) {
return sb.String(), nil
}
// handle suffix rule ||example.com^ -- either entire string is example.com or *.example.com
func getSuffix(rule string) (bool, string) {
// if starts with / and ends with /, it's already a regexp
// TODO: if a regexp is simple `/abracadabra$/`, then simplify it maybe?
if rule[0] == '/' && rule[len(rule)-1] == '/' {
return false, ""
}
// must start with ||
if rule[0] != '|' || rule[1] != '|' {
return false, ""
}
rule = rule[2:]
// suffix rule must end with ^ or |
lastChar := rule[len(rule)-1]
if lastChar != '^' && lastChar != '|' {
return false, ""
}
// last char was checked, eat it
rule = rule[:len(rule)-1]
// check that it doesn't have any special characters inside
for _, r := range rule {
switch r {
case '|':
return false, ""
case '*':
return false, ""
}
}
return true, rule
}

View File

@@ -3,9 +3,12 @@ package main
import (
"bufio"
"errors"
"fmt"
"io"
"net/http"
"sort"
"os"
"path"
"runtime"
"strings"
)
@@ -48,142 +51,44 @@ func ensureDELETE(handler func(http.ResponseWriter, *http.Request)) func(http.Re
return ensure("DELETE", handler)
}
// --------------------------
// helper functions for stats
// --------------------------
func computeRate(input []float64) []float64 {
output := make([]float64, 0)
for i := len(input) - 2; i >= 0; i-- {
value := input[i]
diff := value - input[i+1]
output = append([]float64{diff}, output...)
}
return output
}
func generateMapFromSnap(snap statsSnapshot) map[string]interface{} {
var avgProcessingTime float64
if snap.processingTimeCount > 0 {
avgProcessingTime = snap.processingTimeSum / snap.processingTimeCount
}
result := map[string]interface{}{
"dns_queries": snap.totalRequests,
"blocked_filtering": snap.filteredLists,
"replaced_safebrowsing": snap.filteredSafebrowsing,
"replaced_safesearch": snap.filteredSafesearch,
"replaced_parental": snap.filteredParental,
"avg_processing_time": avgProcessingTime,
}
return result
}
func generateMapFromStats(stats *periodicStats, start int, end int) map[string]interface{} {
// clamp
start = clamp(start, 0, statsHistoryElements)
end = clamp(end, 0, statsHistoryElements)
avgProcessingTime := make([]float64, 0)
count := computeRate(stats.processingTimeCount[start:end])
sum := computeRate(stats.processingTimeSum[start:end])
for i := 0; i < len(count); i++ {
var avg float64
if count[i] != 0 {
avg = sum[i] / count[i]
avg *= 1000
func optionalAuth(handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
if config.AuthName == "" || config.AuthPass == "" {
handler(w, r)
return
}
avgProcessingTime = append(avgProcessingTime, avg)
}
result := map[string]interface{}{
"dns_queries": computeRate(stats.totalRequests[start:end]),
"blocked_filtering": computeRate(stats.filteredLists[start:end]),
"replaced_safebrowsing": computeRate(stats.filteredSafebrowsing[start:end]),
"replaced_safesearch": computeRate(stats.filteredSafesearch[start:end]),
"replaced_parental": computeRate(stats.filteredParental[start:end]),
"avg_processing_time": avgProcessingTime,
}
return result
}
func produceTop(m map[string]int, top int) map[string]int {
toMarshal := map[string]int{}
topKeys := sortByValue(m)
for i, k := range topKeys {
if i == top {
break
user, pass, ok := r.BasicAuth()
if !ok || user != config.AuthName || pass != config.AuthPass {
w.Header().Set("WWW-Authenticate", `Basic realm="dnsfilter"`)
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte("Unauthorised.\n"))
return
}
toMarshal[k] = m[k]
handler(w, r)
}
return toMarshal
}
// -------------------------------------
// helper functions for querylog parsing
// -------------------------------------
func sortByValue(m map[string]int) []string {
type kv struct {
k string
v int
}
var ss []kv
for k, v := range m {
ss = append(ss, kv{k, v})
}
sort.Slice(ss, func(l, r int) bool {
return ss[l].v > ss[r].v
})
sorted := []string{}
for _, v := range ss {
sorted = append(sorted, v.k)
}
return sorted
type authHandler struct {
handler http.Handler
}
func getHost(entry map[string]interface{}) string {
q, ok := entry["question"]
if !ok {
return ""
func (a *authHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if config.AuthName == "" || config.AuthPass == "" {
a.handler.ServeHTTP(w, r)
return
}
question, ok := q.(map[string]interface{})
if !ok {
return ""
user, pass, ok := r.BasicAuth()
if !ok || user != config.AuthName || pass != config.AuthPass {
w.Header().Set("WWW-Authenticate", `Basic realm="dnsfilter"`)
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte("Unauthorised.\n"))
return
}
h, ok := question["host"]
if !ok {
return ""
}
host, ok := h.(string)
if !ok {
return ""
}
return host
a.handler.ServeHTTP(w, r)
}
func getReason(entry map[string]interface{}) string {
r, ok := entry["reason"]
if !ok {
return ""
}
reason, ok := r.(string)
if !ok {
return ""
}
return reason
}
func getClient(entry map[string]interface{}) string {
c, ok := entry["client"]
if !ok {
return ""
}
client, ok := c.(string)
if !ok {
return ""
}
return client
func optionalAuthHandler(handler http.Handler) http.Handler {
return &authHandler{handler}
}
// -------------------------------------------------
@@ -208,3 +113,27 @@ func parseParametersFromBody(r io.Reader) (map[string]string, error) {
return parameters, nil
}
// ---------------------
// debug logging helpers
// ---------------------
func _Func() string {
pc := make([]uintptr, 10) // at least 1 entry needed
runtime.Callers(2, pc)
f := runtime.FuncForPC(pc[0])
return path.Base(f.Name())
}
func trace(format string, args ...interface{}) {
pc := make([]uintptr, 10) // at least 1 entry needed
runtime.Callers(2, pc)
f := runtime.FuncForPC(pc[0])
var buf strings.Builder
buf.WriteString(fmt.Sprintf("%s(): ", path.Base(f.Name())))
text := fmt.Sprintf(format, args...)
buf.WriteString(text)
if len(text) == 0 || text[len(text)-1] != '\n' {
buf.WriteRune('\n')
}
fmt.Fprint(os.Stderr, buf.String())
}

View File

@@ -29,7 +29,7 @@
| Description | Value |
| -------------- | ------------ |
| Version of AdGuard DNS server:| (e.g. v1.0)
| Version of AdGuard Home server:| (e.g. v1.0)
| How did you setup DNS configuration:| (System/Router/IoT)
| If it's a router or IoT, please write device model:| (e.g. Raspberry Pi 3 Model B)
| Operating system and version:| (e.g. Ubuntu 18.04.1)

View File

@@ -1,7 +1,7 @@
swagger: '2.0'
info:
title: 'AdGuard DNS'
description: 'Control AdGuard DNS server with this API'
title: 'AdGuard Home'
description: 'Control AdGuard Home server with this API'
version: 0.0.0
basePath: /control
schemes:
@@ -25,39 +25,41 @@ tags:
name: safesearch
description: 'Enforce family-friendly results in search engines'
paths:
/start:
post:
tags:
- global
operationId: start
summary: 'Start DNS server'
responses:
200:
description: OK
/stop:
post:
tags:
- global
operationId: stop
summary: 'Stop DNS server'
responses:
200:
description: OK
/restart:
post:
tags:
- global
operationId: restart
summary: 'Restart DNS server'
responses:
200:
description: OK
/status:
get:
tags:
- global
operationId: status
summary: 'Get DNS server status'
responses:
200:
description: OK
examples:
application/json:
dns_address: 127.0.0.1
dns_port: 53
protection_enabled: true
querylog_enabled: true
running: true
upstream_dns:
- 1.1.1.1
- 1.0.0.1
version: "v0.1"
/enable_protection:
post:
tags:
-global
operationId: enableProtection
summary: "Enable protection (turns on dnsfilter module in coredns)"
responses:
200:
description: OK
/disable_protection:
post:
tags:
-global
operationId: disableProtection
summary: "Disable protection (turns off filtering, sb, parental, safesearch temporarily by disabling dnsfilter module in coredns)"
responses:
200:
description: OK
@@ -160,6 +162,35 @@ paths:
responses:
200:
description: OK
/test_upstream_dns:
post:
tags:
- global
operationId: testUpstreamDNS
summary: 'Test upstream DNS'
consumes:
- text/plain
parameters:
- in: body
name: upstream
description: 'Upstream servers, separated by newline or space, port is optional after colon'
schema:
type: string
example: |
1.1.1.1
1.0.0.1
8.8.8.8 8.8.4.4
192.168.1.104:53535
responses:
200:
description: 'Status of testing each requested server, with "OK" meaning that server works, any other text means an error.'
examples:
application/json:
1.1.1.1: OK
1.0.0.1: OK
8.8.8.8: OK
8.8.4.4: OK
"192.168.1.104:53535": "Couldn't communicate with DNS server"
/stats_top:
get:
tags:
@@ -277,6 +308,15 @@ paths:
- 123
- 123
- 123
/stats_reset:
post:
tags:
-global
operationId: statsReset
summary: "Reset all statistics to zeroes"
responses:
200:
description: OK
/filtering/enable:
post:
tags:

View File

@@ -1,3 +0,0 @@
#!/bin/bash
set -e -x -o pipefail
echo "executing $0"

View File

@@ -1,3 +0,0 @@
#!/bin/bash
set -e -x -o pipefail
echo "executing $0"

View File

@@ -1,3 +0,0 @@
#!/bin/bash
set -e -x -o pipefail
echo "executing $0"

View File

@@ -1,3 +0,0 @@
#!/bin/bash
set -e -x -o pipefail
echo "executing $0"

2
scripts/whotracksme/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
node_modules
whotracksme.json

View File

@@ -0,0 +1,12 @@
## Whotracks.me database converter
A simple script that converts the Ghostery/Cliqz trackers database to a json format.
### Usage
```
yarn install
node index.js
```
You'll find the output in the `whotracksmedb.json` file.

View File

@@ -0,0 +1,76 @@
const fs = require('fs');
const sqlite3 = require('sqlite3').verbose();
const downloadFileSync = require('download-file-sync');
const INPUT_SQL_URL = 'https://raw.githubusercontent.com/cliqz-oss/whotracks.me/master/whotracksme/data/assets/trackerdb.sql';
const OUTPUT_PATH = 'whotracksme.json';
console.log('Downloading ' + INPUT_SQL_URL);
let trackersDbSql = downloadFileSync(INPUT_SQL_URL).toString();
let transformToSqlite = function(sql) {
sql = sql.trim();
if (sql.indexOf("CREATE TABLE") >= 0) {
sql = sql.replace(/UNIQUE/g, '');
}
return sql;
}
let whotracksme = {
timeUpdated: new Date().toISOString(),
categories: {},
trackers: {},
trackerDomains: {}
};
console.log('Initializing the in-memory trackers database');
let db = new sqlite3.Database(':memory:');
db.serialize(function() {
trackersDbSql.split(/;\s*$/gm).forEach(function(sql) {
sql = transformToSqlite(sql);
db.run(sql, function() {});
});
db.each("SELECT * FROM categories", function(err, row) {
if (err) {
console.error(err);
return;
}
whotracksme.categories[row.id] = row.name;
});
db.each("SELECT * FROM trackers", function(err, row) {
if (err) {
console.error(err);
return;
}
whotracksme.trackers[row.id] = {
"name": row.name,
"categoryId": row.category_id,
"url": row.website_url
};
});
db.each("SELECT * FROM tracker_domains", function(err, row) {
if (err) {
console.error(err);
return;
}
whotracksme.trackerDomains[row.domain] = row.tracker;
});
});
db.close(function(err) {
if (err) {
console.error(err);
return;
}
fs.writeFileSync(OUTPUT_PATH, JSON.stringify(whotracksme, 0, 4));
console.log('Trackers json file has been updated: ' + OUTPUT_PATH);
});

View File

@@ -0,0 +1,15 @@
{
"name": "whotracksme-converter",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"download-file-sync": "^1.0.4",
"sqlite3": "^4.0.2"
}
}

View File

@@ -0,0 +1,674 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
abbrev@1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
ajv@^5.3.0:
version "5.5.2"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965"
dependencies:
co "^4.6.0"
fast-deep-equal "^1.0.0"
fast-json-stable-stringify "^2.0.0"
json-schema-traverse "^0.3.0"
ansi-regex@^2.0.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"
ansi-regex@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998"
aproba@^1.0.3:
version "1.2.0"
resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a"
are-we-there-yet@~1.1.2:
version "1.1.5"
resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21"
dependencies:
delegates "^1.0.0"
readable-stream "^2.0.6"
asn1@~0.2.3:
version "0.2.4"
resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136"
dependencies:
safer-buffer "~2.1.0"
assert-plus@1.0.0, assert-plus@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"
asynckit@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
aws-sign2@~0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
aws4@^1.8.0:
version "1.8.0"
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f"
balanced-match@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
bcrypt-pbkdf@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e"
dependencies:
tweetnacl "^0.14.3"
brace-expansion@^1.1.7:
version "1.1.11"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
dependencies:
balanced-match "^1.0.0"
concat-map "0.0.1"
caseless@~0.12.0:
version "0.12.0"
resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
chownr@^1.0.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.1.tgz#54726b8b8fff4df053c42187e801fb4412df1494"
co@^4.6.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
code-point-at@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
combined-stream@1.0.6:
version "1.0.6"
resolved "http://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz#723e7df6e801ac5613113a7e445a9b69cb632818"
dependencies:
delayed-stream "~1.0.0"
combined-stream@~1.0.6:
version "1.0.7"
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.7.tgz#2d1d24317afb8abe95d6d2c0b07b57813539d828"
dependencies:
delayed-stream "~1.0.0"
concat-map@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
console-control-strings@^1.0.0, console-control-strings@~1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e"
core-util-is@1.0.2, core-util-is@~1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
dashdash@^1.12.0:
version "1.14.1"
resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
dependencies:
assert-plus "^1.0.0"
debug@^2.1.2:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
dependencies:
ms "2.0.0"
deep-extend@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac"
delayed-stream@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
delegates@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
detect-libc@^1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
download-file-sync@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/download-file-sync/-/download-file-sync-1.0.4.tgz#d3e3c543f836f41039455b9034c72e355b036019"
ecc-jsbn@~0.1.1:
version "0.1.2"
resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9"
dependencies:
jsbn "~0.1.0"
safer-buffer "^2.1.0"
extend@~3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
extsprintf@1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
extsprintf@^1.2.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
fast-deep-equal@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614"
fast-json-stable-stringify@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2"
forever-agent@~0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
form-data@~2.3.2:
version "2.3.2"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.2.tgz#4970498be604c20c005d4f5c23aecd21d6b49099"
dependencies:
asynckit "^0.4.0"
combined-stream "1.0.6"
mime-types "^2.1.12"
fs-minipass@^1.2.5:
version "1.2.5"
resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.5.tgz#06c277218454ec288df77ada54a03b8702aacb9d"
dependencies:
minipass "^2.2.1"
fs.realpath@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
gauge@~2.7.3:
version "2.7.4"
resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"
dependencies:
aproba "^1.0.3"
console-control-strings "^1.0.0"
has-unicode "^2.0.0"
object-assign "^4.1.0"
signal-exit "^3.0.0"
string-width "^1.0.1"
strip-ansi "^3.0.1"
wide-align "^1.1.0"
getpass@^0.1.1:
version "0.1.7"
resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa"
dependencies:
assert-plus "^1.0.0"
glob@^7.0.5:
version "7.1.3"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1"
dependencies:
fs.realpath "^1.0.0"
inflight "^1.0.4"
inherits "2"
minimatch "^3.0.4"
once "^1.3.0"
path-is-absolute "^1.0.0"
har-schema@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
har-validator@~5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.0.tgz#44657f5688a22cfd4b72486e81b3a3fb11742c29"
dependencies:
ajv "^5.3.0"
har-schema "^2.0.0"
has-unicode@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9"
http-signature@~1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1"
dependencies:
assert-plus "^1.0.0"
jsprim "^1.2.2"
sshpk "^1.7.0"
iconv-lite@^0.4.4:
version "0.4.24"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
dependencies:
safer-buffer ">= 2.1.2 < 3"
ignore-walk@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.1.tgz#a83e62e7d272ac0e3b551aaa82831a19b69f82f8"
dependencies:
minimatch "^3.0.4"
inflight@^1.0.4:
version "1.0.6"
resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
dependencies:
once "^1.3.0"
wrappy "1"
inherits@2, inherits@~2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
ini@~1.3.0:
version "1.3.5"
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
is-fullwidth-code-point@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb"
dependencies:
number-is-nan "^1.0.0"
is-fullwidth-code-point@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f"
is-typedarray@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
isarray@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
isstream@~0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
jsbn@~0.1.0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
json-schema-traverse@^0.3.0:
version "0.3.1"
resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340"
json-schema@0.2.3:
version "0.2.3"
resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13"
json-stringify-safe@~5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
jsprim@^1.2.2:
version "1.4.1"
resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"
dependencies:
assert-plus "1.0.0"
extsprintf "1.3.0"
json-schema "0.2.3"
verror "1.10.0"
mime-db@~1.36.0:
version "1.36.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.36.0.tgz#5020478db3c7fe93aad7bbcc4dcf869c43363397"
mime-types@^2.1.12, mime-types@~2.1.19:
version "2.1.20"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.20.tgz#930cb719d571e903738520f8470911548ca2cc19"
dependencies:
mime-db "~1.36.0"
minimatch@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
dependencies:
brace-expansion "^1.1.7"
minimist@0.0.8:
version "0.0.8"
resolved "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
minimist@^1.2.0:
version "1.2.0"
resolved "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
minipass@^2.2.1, minipass@^2.3.3:
version "2.3.4"
resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.3.4.tgz#4768d7605ed6194d6d576169b9e12ef71e9d9957"
dependencies:
safe-buffer "^5.1.2"
yallist "^3.0.0"
minizlib@^1.1.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.1.1.tgz#6734acc045a46e61d596a43bb9d9cd326e19cc42"
dependencies:
minipass "^2.2.1"
mkdirp@^0.5.0, mkdirp@^0.5.1:
version "0.5.1"
resolved "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
dependencies:
minimist "0.0.8"
ms@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
nan@~2.10.0:
version "2.10.0"
resolved "http://registry.npmjs.org/nan/-/nan-2.10.0.tgz#96d0cd610ebd58d4b4de9cc0c6828cda99c7548f"
needle@^2.2.1:
version "2.2.4"
resolved "https://registry.yarnpkg.com/needle/-/needle-2.2.4.tgz#51931bff82533b1928b7d1d69e01f1b00ffd2a4e"
dependencies:
debug "^2.1.2"
iconv-lite "^0.4.4"
sax "^1.2.4"
node-pre-gyp@^0.10.3:
version "0.10.3"
resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.10.3.tgz#3070040716afdc778747b61b6887bf78880b80fc"
dependencies:
detect-libc "^1.0.2"
mkdirp "^0.5.1"
needle "^2.2.1"
nopt "^4.0.1"
npm-packlist "^1.1.6"
npmlog "^4.0.2"
rc "^1.2.7"
rimraf "^2.6.1"
semver "^5.3.0"
tar "^4"
nopt@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d"
dependencies:
abbrev "1"
osenv "^0.1.4"
npm-bundled@^1.0.1:
version "1.0.5"
resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.0.5.tgz#3c1732b7ba936b3a10325aef616467c0ccbcc979"
npm-packlist@^1.1.6:
version "1.1.12"
resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.1.12.tgz#22bde2ebc12e72ca482abd67afc51eb49377243a"
dependencies:
ignore-walk "^3.0.1"
npm-bundled "^1.0.1"
npmlog@^4.0.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b"
dependencies:
are-we-there-yet "~1.1.2"
console-control-strings "~1.1.0"
gauge "~2.7.3"
set-blocking "~2.0.0"
number-is-nan@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
oauth-sign@~0.9.0:
version "0.9.0"
resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455"
object-assign@^4.1.0:
version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
once@^1.3.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
dependencies:
wrappy "1"
os-homedir@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3"
os-tmpdir@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
osenv@^0.1.4:
version "0.1.5"
resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410"
dependencies:
os-homedir "^1.0.0"
os-tmpdir "^1.0.0"
path-is-absolute@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
performance-now@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
process-nextick-args@~2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa"
psl@^1.1.24:
version "1.1.29"
resolved "https://registry.yarnpkg.com/psl/-/psl-1.1.29.tgz#60f580d360170bb722a797cc704411e6da850c67"
punycode@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
qs@~6.5.2:
version "6.5.2"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
rc@^1.2.7:
version "1.2.8"
resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
dependencies:
deep-extend "^0.6.0"
ini "~1.3.0"
minimist "^1.2.0"
strip-json-comments "~2.0.1"
readable-stream@^2.0.6:
version "2.3.6"
resolved "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf"
dependencies:
core-util-is "~1.0.0"
inherits "~2.0.3"
isarray "~1.0.0"
process-nextick-args "~2.0.0"
safe-buffer "~5.1.1"
string_decoder "~1.1.1"
util-deprecate "~1.0.1"
request@^2.87.0:
version "2.88.0"
resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef"
dependencies:
aws-sign2 "~0.7.0"
aws4 "^1.8.0"
caseless "~0.12.0"
combined-stream "~1.0.6"
extend "~3.0.2"
forever-agent "~0.6.1"
form-data "~2.3.2"
har-validator "~5.1.0"
http-signature "~1.2.0"
is-typedarray "~1.0.0"
isstream "~0.1.2"
json-stringify-safe "~5.0.1"
mime-types "~2.1.19"
oauth-sign "~0.9.0"
performance-now "^2.1.0"
qs "~6.5.2"
safe-buffer "^5.1.2"
tough-cookie "~2.4.3"
tunnel-agent "^0.6.0"
uuid "^3.3.2"
rimraf@^2.6.1:
version "2.6.2"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36"
dependencies:
glob "^7.0.5"
safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
version "5.1.2"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0:
version "2.1.2"
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
sax@^1.2.4:
version "1.2.4"
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
semver@^5.3.0:
version "5.6.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004"
set-blocking@~2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
signal-exit@^3.0.0:
version "3.0.2"
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
sqlite3@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/sqlite3/-/sqlite3-4.0.2.tgz#1bbeb68b03ead5d499e42a3a1b140064791c5a64"
dependencies:
nan "~2.10.0"
node-pre-gyp "^0.10.3"
request "^2.87.0"
sshpk@^1.7.0:
version "1.15.1"
resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.15.1.tgz#b79a089a732e346c6e0714830f36285cd38191a2"
dependencies:
asn1 "~0.2.3"
assert-plus "^1.0.0"
bcrypt-pbkdf "^1.0.0"
dashdash "^1.12.0"
ecc-jsbn "~0.1.1"
getpass "^0.1.1"
jsbn "~0.1.0"
safer-buffer "^2.0.2"
tweetnacl "~0.14.0"
string-width@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3"
dependencies:
code-point-at "^1.0.0"
is-fullwidth-code-point "^1.0.0"
strip-ansi "^3.0.0"
"string-width@^1.0.2 || 2":
version "2.1.1"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
dependencies:
is-fullwidth-code-point "^2.0.0"
strip-ansi "^4.0.0"
string_decoder@~1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
dependencies:
safe-buffer "~5.1.0"
strip-ansi@^3.0.0, strip-ansi@^3.0.1:
version "3.0.1"
resolved "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
dependencies:
ansi-regex "^2.0.0"
strip-ansi@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f"
dependencies:
ansi-regex "^3.0.0"
strip-json-comments@~2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
tar@^4:
version "4.4.6"
resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.6.tgz#63110f09c00b4e60ac8bcfe1bf3c8660235fbc9b"
dependencies:
chownr "^1.0.1"
fs-minipass "^1.2.5"
minipass "^2.3.3"
minizlib "^1.1.0"
mkdirp "^0.5.0"
safe-buffer "^5.1.2"
yallist "^3.0.2"
tough-cookie@~2.4.3:
version "2.4.3"
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781"
dependencies:
psl "^1.1.24"
punycode "^1.4.1"
tunnel-agent@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"
dependencies:
safe-buffer "^5.0.1"
tweetnacl@^0.14.3, tweetnacl@~0.14.0:
version "0.14.5"
resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
util-deprecate@~1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
uuid@^3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"
verror@1.10.0:
version "1.10.0"
resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400"
dependencies:
assert-plus "^1.0.0"
core-util-is "1.0.2"
extsprintf "^1.2.0"
wide-align@^1.1.0:
version "1.1.3"
resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457"
dependencies:
string-width "^1.0.2 || 2"
wrappy@1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
yallist@^3.0.0, yallist@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.0.2.tgz#8452b4bb7e83c7c188d8041c1a837c773d6d8bb9"

218
stats.go
View File

@@ -1,218 +0,0 @@
package main
import (
"bufio"
"io/ioutil"
"log"
"net"
"net/http"
"net/url"
"os"
"regexp"
"strconv"
"strings"
"syscall"
"time"
)
type periodicStats struct {
totalRequests []float64
filteredTotal []float64
filteredLists []float64
filteredSafebrowsing []float64
filteredSafesearch []float64
filteredParental []float64
processingTimeSum []float64
processingTimeCount []float64
lastRotate time.Time // last time this data was rotated
}
type statsSnapshot struct {
totalRequests float64
filteredTotal float64
filteredLists float64
filteredSafebrowsing float64
filteredSafesearch float64
filteredParental float64
processingTimeSum float64
processingTimeCount float64
}
type statsCollection struct {
perSecond periodicStats
perMinute periodicStats
perHour periodicStats
perDay periodicStats
lastsnap statsSnapshot
}
var statistics statsCollection
var client = &http.Client{
Timeout: time.Second * 30,
}
const statsHistoryElements = 60 + 1 // +1 for calculating delta
var requestCountTotalRegex = regexp.MustCompile(`^coredns_dns_request_count_total`)
var requestDurationSecondsSum = regexp.MustCompile(`^coredns_dns_request_duration_seconds_sum`)
var requestDurationSecondsCount = regexp.MustCompile(`^coredns_dns_request_duration_seconds_count`)
func initPeriodicStats(stats *periodicStats) {
stats.totalRequests = make([]float64, statsHistoryElements)
stats.filteredTotal = make([]float64, statsHistoryElements)
stats.filteredLists = make([]float64, statsHistoryElements)
stats.filteredSafebrowsing = make([]float64, statsHistoryElements)
stats.filteredSafesearch = make([]float64, statsHistoryElements)
stats.filteredParental = make([]float64, statsHistoryElements)
stats.processingTimeSum = make([]float64, statsHistoryElements)
stats.processingTimeCount = make([]float64, statsHistoryElements)
}
func init() {
initPeriodicStats(&statistics.perSecond)
initPeriodicStats(&statistics.perMinute)
initPeriodicStats(&statistics.perHour)
initPeriodicStats(&statistics.perDay)
}
func runStatsCollectors() {
go statsCollector(time.Second)
}
func statsCollector(t time.Duration) {
for range time.Tick(t) {
collectStats()
}
}
func isConnRefused(err error) bool {
if err != nil {
if uerr, ok := err.(*url.Error); ok {
if noerr, ok := uerr.Err.(*net.OpError); ok {
if scerr, ok := noerr.Err.(*os.SyscallError); ok {
if scerr.Err == syscall.ECONNREFUSED {
return true
}
}
}
}
}
return false
}
func sliceRotate(slice *[]float64) {
a := (*slice)[:len(*slice)-1]
*slice = append([]float64{0}, a...)
}
func statsRotate(stats *periodicStats, now time.Time) {
sliceRotate(&stats.totalRequests)
sliceRotate(&stats.filteredTotal)
sliceRotate(&stats.filteredLists)
sliceRotate(&stats.filteredSafebrowsing)
sliceRotate(&stats.filteredSafesearch)
sliceRotate(&stats.filteredParental)
sliceRotate(&stats.processingTimeSum)
sliceRotate(&stats.processingTimeCount)
stats.lastRotate = now
}
func handleValue(input string, target *float64) {
value, err := strconv.ParseFloat(input, 64)
if err != nil {
log.Println("Failed to parse number input:", err)
return
}
*target = value
}
// called every second, accumulates stats for each second, minute, hour and day
func collectStats() {
now := time.Now()
// rotate each second
// NOTE: since we are called every second, always rotate, otherwise aliasing problems cause the rotation to skip
if true {
statsRotate(&statistics.perSecond, now)
}
// if minute elapsed, rotate
if now.Sub(statistics.perMinute.lastRotate).Minutes() >= 1 {
statsRotate(&statistics.perMinute, now)
}
// if hour elapsed, rotate
if now.Sub(statistics.perHour.lastRotate).Hours() >= 1 {
statsRotate(&statistics.perHour, now)
}
// if day elapsed, rotate
if now.Sub(statistics.perDay.lastRotate).Hours()/24.0 >= 1 {
statsRotate(&statistics.perDay, now)
}
// grab HTTP from prometheus
resp, err := client.Get("http://127.0.0.1:9153/metrics")
if resp != nil && resp.Body != nil {
defer resp.Body.Close()
}
if err != nil {
if isConnRefused(err) == false {
log.Printf("Couldn't get coredns metrics: %T %s\n", err, err)
}
return
}
// read the body entirely
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Println("Couldn't read response body:", err)
return
}
// handle body
scanner := bufio.NewScanner(strings.NewReader(string(body)))
for scanner.Scan() {
line := scanner.Text()
// ignore comments
if line[0] == '#' {
continue
}
splitted := strings.Split(line, " ")
switch {
case splitted[0] == "coredns_dnsfilter_filtered_total":
handleValue(splitted[1], &statistics.lastsnap.filteredTotal)
case splitted[0] == "coredns_dnsfilter_filtered_lists_total":
handleValue(splitted[1], &statistics.lastsnap.filteredLists)
case splitted[0] == "coredns_dnsfilter_filtered_safebrowsing_total":
handleValue(splitted[1], &statistics.lastsnap.filteredSafebrowsing)
case splitted[0] == "coredns_dnsfilter_filtered_parental_total":
handleValue(splitted[1], &statistics.lastsnap.filteredParental)
case requestCountTotalRegex.MatchString(splitted[0]):
handleValue(splitted[1], &statistics.lastsnap.totalRequests)
case requestDurationSecondsSum.MatchString(splitted[0]):
handleValue(splitted[1], &statistics.lastsnap.processingTimeSum)
case requestDurationSecondsCount.MatchString(splitted[0]):
handleValue(splitted[1], &statistics.lastsnap.processingTimeCount)
}
}
// put the snap into per-second, per-minute, per-hour and per-day
assignSnapToStats(&statistics.perSecond)
assignSnapToStats(&statistics.perMinute)
assignSnapToStats(&statistics.perHour)
assignSnapToStats(&statistics.perDay)
}
func assignSnapToStats(stats *periodicStats) {
stats.totalRequests[0] = statistics.lastsnap.totalRequests
stats.filteredTotal[0] = statistics.lastsnap.filteredTotal
stats.filteredLists[0] = statistics.lastsnap.filteredLists
stats.filteredSafebrowsing[0] = statistics.lastsnap.filteredSafebrowsing
stats.filteredSafesearch[0] = statistics.lastsnap.filteredSafesearch
stats.filteredParental[0] = statistics.lastsnap.filteredParental
stats.processingTimeSum[0] = statistics.lastsnap.processingTimeSum
stats.processingTimeCount[0] = statistics.lastsnap.processingTimeCount
}

10
version.json Normal file
View File

@@ -0,0 +1,10 @@
{
"version": "v0.9",
"announcement": "AdGuard Home v0.9 is now available!",
"announcement_url": "https://github.com/AdguardTeam/AdGuardHome/releases/tag/v0.9",
"download_darwin_amd64": "https://github.com/AdguardTeam/AdGuardHome/releases/download/v0.9/AdGuardHome_v0.9_MacOS.zip",
"download_linux_amd64": "https://github.com/AdguardTeam/AdGuardHome/releases/download/v0.9/AdGuardHome_v0.9_linux_amd64.tar.gz",
"download_linux_386": "https://github.com/AdguardTeam/AdGuardHome/releases/download/v0.9/AdGuardHome_v0.9_linux_386.tar.gz",
"download_linux_arm": "https://github.com/AdguardTeam/AdGuardHome/releases/download/v0.9/AdGuardHome_v0.9_linux_arm.tar.gz",
"selfupdate_min_version": "v0.0"
}