Compare commits

..

162 Commits

Author SHA1 Message Date
Eugene Bujak
d90da5d540 Bump version to v0.92-hotfix1 2019-01-04 22:18:53 +03:00
Eugene Bujak
6cd93139fd Add release.sh script that I've been using to build release binaries. 2019-01-04 22:12:43 +03:00
Andrey Meshkov
246f726115 Fix #502 2019-01-04 21:22:22 +03:00
Andrey Meshkov
5a6dc34ec0 Fix #519 2019-01-04 21:22:22 +03:00
Andrey Meshkov
ddcfe7c4bf Upgrade dnsproxy to 0.9.10
Fix #520 #505 #518
2019-01-04 21:22:22 +03:00
Ildar Kamalov
eb71f3ed8f Added Traditional Chinese
Closes #510
2019-01-04 21:22:22 +03:00
Mordy Ovits
fd629be6e6 Fix misspelling 2018-12-31 22:19:13 +03:00
Mordy Ovits
ce1aaea4ca Clarify install language 2018-12-31 22:19:13 +03:00
Mordy Ovits
fa8c038bc1 Add note re setcap installation 2018-12-31 22:19:13 +03:00
Mordy Ovits
9fdf946fc0 Update README with instructions for setcap non-root use
On Linux you can run it listening on port 53 without root privs.  This is the best option: clients still send on port 53 (no wonky configs) and AdGuard doesn't run as root (!).
2018-12-31 22:19:13 +03:00
Eugene Bujak
fd8860a389 Bump version to 0.92 2018-12-29 20:21:36 +03:00
Eugene Bujak
cbe83e2053 Merge pull request #126 in DNS/adguard-dns from feature/423 to master
* commit 'b0c4d88d5454f8dd5a92a73615cce3a31450f56b': (45 commits)
  Indicate that DHCP is experimental
  Update dnsproxy and dnscrypt, and run go mod tidy.
  Fix race conditions found by -race
  move log wrapper library outside into hmage/golibs/log
  Added check for active DHCP before enable
  Use new log wrapper and add more functions to it.
  Implement a log wrapper
  /dhcp/status -- give out hostname for UI
  dhcpd -- Remember hostname, for UI.
  Update comment why filter_conn.go is needed.
  Fixup of previous commit.
  /dhcp/find_active_dhcp -- use interface name from request body
  Don't try to start DHCP server if it's not enabled.
  Get rid of logrus, it's TTY output is not friendly or human parseable if we will want users to send us logs.
  Flag parser -- support options without values, move code for help and verbose into table.
  verbose output parameter
  Pretty-format leases so it shows human readable MAC address.
  Start DHCP on launch if it's enabled in config.
  Update makefile to detect changes in dhcpd/*.go
  DHCPD — don't forget to make Lease fields public.
  ...
2018-12-29 20:07:14 +03:00
Andrey Meshkov
b0c4d88d54 Indicate that DHCP is experimental 2018-12-29 20:00:20 +03:00
Eugene Bujak
ec0b8c687a Update dnsproxy and dnscrypt, and run go mod tidy. 2018-12-29 19:28:46 +03:00
Eugene Bujak
4d3f1b83a6 Fix race conditions found by -race 2018-12-29 19:13:00 +03:00
Eugene Bujak
368e2d1ebd move log wrapper library outside into hmage/golibs/log 2018-12-29 19:12:45 +03:00
Ildar Kamalov
568784b992 Added check for active DHCP before enable 2018-12-29 18:43:17 +03:00
Eugene Bujak
243603e04c Fix panic when DNS query doesn't have questions.
Closes #491.
2018-12-29 17:47:50 +03:00
Eugene Bujak
d8802a9709 Use new log wrapper and add more functions to it. 2018-12-29 17:37:18 +03:00
Eugene Bujak
7463e54258 Implement a log wrapper 2018-12-29 17:23:08 +03:00
Eugene Bujak
7acb107cbf /dhcp/status -- give out hostname for UI 2018-12-29 17:04:40 +03:00
Eugene Bujak
86d79ae232 dhcpd -- Remember hostname, for UI. 2018-12-29 16:44:07 +03:00
Eugene Bujak
fedfc3a1fd Update comment why filter_conn.go is needed. 2018-12-29 16:40:38 +03:00
Eugene Bujak
bf15a40248 Fixup of previous commit. 2018-12-29 16:40:29 +03:00
Eugene Bujak
4efa30edc4 /dhcp/find_active_dhcp -- use interface name from request body 2018-12-29 16:39:29 +03:00
Eugene Bujak
7ab03e9335 Don't try to start DHCP server if it's not enabled. 2018-12-29 15:04:14 +03:00
Eugene Bujak
55a7ff7447 Get rid of logrus, it's TTY output is not friendly or human parseable if we will want users to send us logs. 2018-12-29 14:55:35 +03:00
Eugene Bujak
a7e0f66492 Flag parser -- support options without values, move code for help and verbose into table. 2018-12-29 14:55:03 +03:00
Andrey Meshkov
f312575da4 verbose output parameter 2018-12-29 00:49:39 +03:00
Eugene Bujak
8fc5aebf12 Pretty-format leases so it shows human readable MAC address. 2018-12-28 21:01:31 +03:00
Eugene Bujak
03effab345 Start DHCP on launch if it's enabled in config. 2018-12-28 21:01:16 +03:00
Eugene Bujak
f868fdbf7a Update makefile to detect changes in dhcpd/*.go 2018-12-28 21:00:41 +03:00
Eugene Bujak
1b7db49062 DHCPD — don't forget to make Lease fields public. 2018-12-28 20:50:24 +03:00
Eugene Bujak
f5e7eed447 /dhcp/find_active_dhcp API — Don't return 'found' key when there's an error. And return error string. 2018-12-28 20:50:00 +03:00
Eugene Bujak
6fd9af3c60 /dhcp/set_config API — don't forget to save changed config to YAML 2018-12-28 20:49:27 +03:00
Ildar Kamalov
4aea91a70c Refresh status button 2018-12-28 19:48:02 +03:00
Eugene Bujak
8b4a1ca713 First implementation of DHCP server, compiles but not tested yet. 2018-12-28 18:28:46 +03:00
Eugene Bujak
73f71364b3 Add interface name to dhcp config 2018-12-28 18:26:57 +03:00
Ildar Kamalov
712493aafd UI for DHCP interfaces 2018-12-28 18:26:56 +03:00
Eugene Bujak
1270bbad1a fixup of previous commit -- Remove commented out code 2018-12-28 18:26:56 +03:00
Eugene Bujak
c073f9db7b fixup of previous commit -- make json fields lowercase_underscored 2018-12-28 18:26:56 +03:00
Eugene Bujak
87b3c92f71 Add /dhcp/interfaces API call to list available network interfaces. 2018-12-28 18:26:56 +03:00
Ildar Kamalov
9fa85a5c48 Added DHCP form strings to translations 2018-12-28 18:26:56 +03:00
Ildar Kamalov
52b81a27fb Send lease duration as number 2018-12-28 18:26:56 +03:00
Eugene Bujak
39bc55e430 Fixup of previous commit. 2018-12-28 18:26:56 +03:00
Eugene Bujak
59adad4d53 DHCP -- Use uint64 for lease duration 2018-12-28 18:26:56 +03:00
Ildar Kamalov
a74c2248fb Send dhcp/find_active_dhcp as POST request 2018-12-28 18:26:56 +03:00
Ildar Kamalov
d46b65f982 Add enable/disable for DHCP server 2018-12-28 18:26:56 +03:00
Eugene Bujak
96fbf7f134 Fix yaml marshalling panic. 2018-12-28 18:26:56 +03:00
Eugene Bujak
9294c9ecb2 Add DHCP API stubs for JS development. 2018-12-28 18:26:56 +03:00
Ildar Kamalov
dd21f497e3 Added initial layout for DHCP server config 2018-12-28 18:26:56 +03:00
Eugene Bujak
390883126c Change openapi doc port from 3000 to 4000 to avoid clashing with adguardhome. 2018-12-28 18:26:14 +03:00
Andrey Meshkov
fb24447915 Added version.json 2018-12-28 18:26:14 +03:00
Andrey Meshkov
fcf7b2185e Finished reworking openapi, added DHCP methods there 2018-12-28 18:26:14 +03:00
Andrey Meshkov
b91c829f4c Added more openapi definitions 2018-12-28 18:26:14 +03:00
Andrey Meshkov
7106a8eb35 Added more definitions 2018-12-28 18:26:14 +03:00
Andrey Meshkov
09702c724e Added swagger UI scripts 2018-12-28 18:26:14 +03:00
Andrey Meshkov
4623817894 Add dhcp methods to the openapi.yaml 2018-12-28 18:26:14 +03:00
Eugene Bujak
413bc75320 Merge pull request #124 in DNS/adguard-dns from fix/469 to master
* commit '1b84a9233d0056a53cc2fbe48a58e5c39547c178':
  Fix link
  Added filters link to the blocked_by translation
2018-12-26 14:45:15 +03:00
Ildar Kamalov
1b84a9233d Fix link 2018-12-26 11:42:55 +03:00
Ildar Kamalov
aed87ce741 Added filters link to the blocked_by translation
Closes #469
2018-12-26 11:22:15 +03:00
Eugene Bujak
2652ed34b1 Merge pull request #123 in DNS/adguard-dns from feature/dnsproxy_0.9.2 to master
* commit 'cc96593ebf2bf65c6b14761efbca060fc1f2e85a':
  upd to 0.9.3, removed jedist1/xsecretbox from dependencies
  upgraded dnsproxy to 0.9.2
2018-12-25 18:53:34 +03:00
Andrey Meshkov
cc96593ebf upd to 0.9.3, removed jedist1/xsecretbox from dependencies 2018-12-25 01:59:38 +03:00
Andrey Meshkov
3ade62301b upgraded dnsproxy to 0.9.2 2018-12-25 00:08:51 +03:00
Andrey Meshkov
62606db1af fix client IP address 2018-12-24 23:06:36 +03:00
Andrey Meshkov
8227970d39 Merge pull request #122 in DNS/adguard-dns from feature/dnsproxy to master
* commit '374a0dc2e5b8a93ada7e69242a909607756074c8':
  Fixing review comments
  fix imports
  changed to logrus
  Start using dnsproxy
2018-12-24 22:51:02 +03:00
Andrey Meshkov
374a0dc2e5 Fixing review comments 2018-12-24 18:47:33 +03:00
Andrey Meshkov
2bc1d737cc fix imports 2018-12-24 16:58:48 +03:00
Andrey Meshkov
bac2c39107 Merge pull request #121 in DNS/adguard-dns from fix/484 to master
* commit '9fe9baf7f4a68bc95adf8cfcb40f8e3346e46547':
  Added pagination to the Filters table
2018-12-24 16:09:27 +03:00
Andrey Meshkov
0a977fee87 changed to logrus 2018-12-24 15:27:14 +03:00
Andrey Meshkov
e711f6e5fe Start using dnsproxy 2018-12-24 15:19:52 +03:00
Ildar Kamalov
9fe9baf7f4 Added pagination to the Filters table
Closes #484
2018-12-24 12:08:39 +03:00
Andrey Meshkov
b195080012 Merge pull request #120 in DNS/adguard-dns from feature/284 to master
* commit '3d179079661e3c97041a8448ec3aecea40199032':
  upgrade dnscrypt client to v1.0.0
  Handle cert expiration or rotation
  Fix #284
2018-12-19 16:28:56 +03:00
Andrey Meshkov
3d17907966 upgrade dnscrypt client to v1.0.0 2018-12-18 13:24:15 +03:00
Andrey Meshkov
45626b139d Handle cert expiration or rotation 2018-12-18 01:45:19 +03:00
Andrey Meshkov
b30b6b1d66 Fix #284
Added DNSCrypt upstreams support
Added DNS Stamps support
2018-12-18 01:20:38 +03:00
Andrey Meshkov
6e6c321871 Merge pull request #119 in DNS/adguard-dns from fix/479 to master
* commit '6addc04b97bb46299fb7c76e2b11bb9f469c238c':
  Update locales
  Fixed logs page crush on filter removing
2018-12-17 12:13:54 +03:00
Ildar Kamalov
6addc04b97 Update locales 2018-12-17 11:30:52 +03:00
Ildar Kamalov
717a58a872 Fixed logs page crush on filter removing
Closes #479
2018-12-17 11:28:44 +03:00
Eugene Bujak
1c89e1df32 Resolve into a stub page when blocked by parental or safebrowsing.
Closes #475.
2018-12-11 15:09:07 +03:00
Eugene Bujak
5c4ec62d96 Check if protection is enabled before running the host through dnsfilter.
Closes #476.
2018-12-11 14:20:14 +03:00
Eugene Bujak
69a387547d Merge pull request #118 in DNS/adguard-dns from bugfix/472 to master
* commit '8411de88879e13acbe6f6a42d8659af91a511467':
  Don't log ANY requests if refuseAny is enabled.
2018-12-07 14:34:17 +03:00
Eugene Bujak
8411de8887 Don't log ANY requests if refuseAny is enabled.
Closes #472.
2018-12-07 14:12:26 +03:00
Andrey Meshkov
b5121c5754 Merge pull request #117 in DNS/adguard-dns from no_coredns to master
* commit '253d8a4016d66863ecee426b8f7d74841c4ed4de': (58 commits)
  Pointer for dnsfilter.Result in querylog didn't make things simpler, revert that change and all related changes.
  Fixup of previous commit -- remove unused import.
  Remove unused code.
  Use filter deduplication function.
  Small code review update -- use CamelCase
  readme -- Update config field descriptions and clarify about coredns.
  dnsforward -- fix panic on ANY request
  dnsfilter -- fix broken tests
  config -- Avoid deleting existing dns section if someone removes schema_version from yaml file.
  Rename coredns.go to dns.go
  Add support for bootstrapping upstream DNS servers by hostname.
  dnsforward -- support tcp:// schema
  dnsforward -- add upstream tests.
  Don't omit empty user rules in configfile -- otherwise users might not be able to find that it's customizable in configfile.
  Get rid of mentions of CoreDNS in code except for upgrading and in readme. Add config upgrade.
  dnsforward -- add a simple test that launches a server and queries well-known value through it
  Remove old entries from .gitignore
  Remove unused code. Goodbye CoreDNS.
  Use dnsforward for checking if upstream DNS server is working.
  dnsforward -- implement ratelimit and refuseany
  ...
2018-12-06 17:29:36 +03:00
Eugene Bujak
253d8a4016 Pointer for dnsfilter.Result in querylog didn't make things simpler, revert that change and all related changes. 2018-12-06 17:27:38 +03:00
Eugene Bujak
2ba5cb48b2 Fixup of previous commit -- remove unused import. 2018-12-06 17:19:57 +03:00
Eugene Bujak
e056fb2eb9 Remove unused code. 2018-12-06 17:19:04 +03:00
Eugene Bujak
8fb6f92753 Use filter deduplication function. 2018-12-06 17:19:04 +03:00
Eugene Bujak
e5c1211e17 Small code review update -- use CamelCase 2018-12-06 17:18:16 +03:00
Eugene Bujak
217124cb3b readme -- Update config field descriptions and clarify about coredns. 2018-12-06 17:17:50 +03:00
Eugene Bujak
15f3c82238 dnsforward -- fix panic on ANY request 2018-12-06 16:55:05 +03:00
Eugene Bujak
c82a5ac0cb dnsfilter -- fix broken tests 2018-12-06 16:54:48 +03:00
Eugene Bujak
250cc0ec0f config -- Avoid deleting existing dns section if someone removes schema_version from yaml file. 2018-12-06 00:29:38 +03:00
Eugene Bujak
3ad4b2864d Rename coredns.go to dns.go 2018-12-06 00:23:03 +03:00
Eugene Bujak
0f5dd661f5 Add support for bootstrapping upstream DNS servers by hostname. 2018-12-06 00:22:20 +03:00
Eugene Bujak
ff1c19cac5 dnsforward -- support tcp:// schema 2018-12-05 21:33:32 +03:00
Eugene Bujak
2a1059107a dnsforward -- add upstream tests. 2018-12-05 21:33:07 +03:00
Eugene Bujak
609523a59c Don't omit empty user rules in configfile -- otherwise users might not be able to find that it's customizable in configfile. 2018-12-05 21:09:37 +03:00
Eugene Bujak
e31905864b Get rid of mentions of CoreDNS in code except for upgrading and in readme. Add config upgrade. 2018-12-05 21:08:43 +03:00
Eugene Bujak
bb6c596b22 dnsforward -- add a simple test that launches a server and queries well-known value through it 2018-12-05 20:13:35 +03:00
Eugene Bujak
2745223dbf Remove old entries from .gitignore 2018-12-05 19:26:54 +03:00
Eugene Bujak
b847866310 Remove unused code. Goodbye CoreDNS. 2018-12-05 19:18:58 +03:00
Eugene Bujak
f6942213c8 Use dnsforward for checking if upstream DNS server is working. 2018-12-05 19:17:17 +03:00
Eugene Bujak
478ce03386 dnsforward -- implement ratelimit and refuseany 2018-12-05 18:49:19 +03:00
Eugene Bujak
15f0dee719 readme -- Cleanup, mention that coredns was removed. 2018-12-05 16:57:21 +03:00
Eugene Bujak
7ddc71006b stop DNS server properly when interrupted with ctrl+c, SIGTERM, SIGHUP or SIGQUIT 2018-12-05 16:57:21 +03:00
Eugene Bujak
b0149972cc dnsforward -- give only ip address to querylog, without port 2018-12-05 16:57:21 +03:00
Eugene Bujak
9b43e07d7f dnsforward -- flush querylog to file on server stop 2018-12-05 16:57:21 +03:00
Eugene Bujak
e357620740 Plug correct stats handler functions. 2018-12-05 16:57:21 +03:00
Eugene Bujak
052f975762 dnsforward -- Move querylog from coredns plugin, a more complex migration with proper API took too long so a simple move was used instead to save time. 2018-12-05 16:57:21 +03:00
Eugene Bujak
e5d2f883ac dnsforward -- Make Upstream interface give access to Address field. 2018-12-05 16:57:21 +03:00
Eugene Bujak
8396dc2fdb Update docs for formatting in godoc. 2018-12-05 16:57:21 +03:00
Eugene Bujak
09fb539875 Simplify two lines into one line. 2018-12-05 16:57:21 +03:00
Eugene Bujak
be4b65fdca dnsforward -- use dnsfilter before cache -- changed settings or filters would require cache invalidation otherwise 2018-12-05 16:57:21 +03:00
Eugene Bujak
0a4627f4f0 Fix engrish 2018-12-05 16:57:21 +03:00
Eugene Bujak
0502ef6cc7 dnsforward -- initialize all dnsfilter settings at start and reconfigure 2018-12-05 16:57:21 +03:00
Eugene Bujak
2281b60ebb dnsfilter -- add trace() 2018-12-05 16:57:21 +03:00
Eugene Bujak
7d2e39ed52 dnsfilter -- Add a convinience function to add all rules from all filters. 2018-12-05 16:57:21 +03:00
Eugene Bujak
e26837d9e8 dnsfilter -- Add parameter to New() to supply optional initial config. 2018-12-05 16:57:21 +03:00
Eugene Bujak
3ecc0ee24b Makefile -- don't dictate to go what binary to build, so it can append .exe if building for windows. 2018-12-05 16:57:21 +03:00
Eugene Bujak
057db71f3b Get rid of duplicate variable definitions 2018-12-05 16:57:21 +03:00
Eugene Bujak
ce615e1855 dnsfilter -- Get rid of accessors. 2018-12-05 16:57:21 +03:00
Eugene Bujak
87c54ebd4c Move Filter definition from dnsforward to dnsfilter, it belongs there. 2018-12-05 16:57:21 +03:00
Eugene Bujak
a6e0a17454 dnsforward -- trim dot in the end of hostname, dnsfilter does not expect it there 2018-12-05 16:56:11 +03:00
Eugene Bujak
9089122b56 Compress the packed static js and css to save some space. 2018-12-05 16:56:11 +03:00
Eugene Bujak
e0286ee85d Don't forget to give user filter to dns forwarding server 2018-12-05 16:56:11 +03:00
Eugene Bujak
31f77af534 Move user filter saving into writeAllConfigs() 2018-12-05 16:56:11 +03:00
Eugene Bujak
0d1478b635 Remove unused struct field 2018-12-05 16:56:11 +03:00
Eugene Bujak
d27fd0488d Move filter-related variables, types and methods to filter.go 2018-12-05 16:56:11 +03:00
Eugene Bujak
9c4b791621 coredns reload -> dnsServer.Reconfigure() 2018-12-05 16:56:11 +03:00
Eugene Bujak
9d87ae95e6 dnsforward -- if given addresses without ports, assign default ports 2018-12-05 16:56:11 +03:00
Eugene Bujak
8316d39b42 Move filtering setting fields from main app to dnsforward. 2018-12-05 16:56:11 +03:00
Eugene Bujak
7120f551c8 dnsforward -- rename BlockedTTL to BlockedResponseTTL to be in line with app's config variable. 2018-12-05 16:56:11 +03:00
Eugene Bujak
e4a3564706 Fix a logical race that wasn't detectable by -race -- we were closing a connection that was already reestablished. 2018-12-05 16:56:11 +03:00
Eugene Bujak
4eb122e973 Avoid duplication of fields in filter struct. 2018-12-05 16:56:11 +03:00
Eugene Bujak
feabc21864 Unplug coreDNS and plug dnsforward library. 2018-12-05 16:54:56 +03:00
Eugene Bujak
a904f85e61 dnsforward library -- default to plain DNS for high-performance testing. 2018-12-05 16:54:56 +03:00
Eugene Bujak
584f441141 dnsforward library -- introduce IsRunning() 2018-12-05 16:54:56 +03:00
Eugene Bujak
7944f23d95 dnsforward library -- consistently nullify and close listening socket when we're done with it. 2018-12-05 16:54:56 +03:00
Eugene Bujak
639b34c7d1 dnsforward library -- Fix race conditions found by -race 2018-12-05 16:54:56 +03:00
Eugene Bujak
ea1353422f User rules -- hold them as a slice of strings, which is how dns forwarding server will expect them. 2018-12-05 16:54:56 +03:00
Eugene Bujak
5a548be16c Add dns forwarding server library 2018-12-05 16:54:56 +03:00
Eugene Bujak
39eccc62b1 Fix that filter ID is uppercase while js expects it to be lowercase. 2018-12-05 16:50:06 +03:00
Eugene Bujak
ea25510a08 Travis -- separate js build time from go build time. 2018-11-29 15:05:29 +03:00
Eugene Bujak
45ae984f3b Fix incorrect cherry-pick in previous commit. 2018-11-29 14:58:25 +03:00
Eugene Bujak
2012e707d0 Fix race condition of trying to write YAML config simultaneously and failing. 2018-11-29 13:31:50 +03:00
Eugene Bujak
942cde79bd Merge pull request #116 in DNS/adguard-dns from fix/466 to master
* commit 'c37c3e0459eb71ffed40e2ff3a4239a1c258e02b':
  Fix #466
2018-11-29 13:19:21 +03:00
Andrey Meshkov
c37c3e0459 Fix #466 2018-11-29 11:24:27 +03:00
Eugene Bujak
cab73c0d68 Fix travis tests -- since we don't use modules there's no need to do git checkout 2018-11-28 14:34:28 +03:00
Eugene Bujak
58129543de Fix panic in upstream test if upstream returns with i/o timeout 2018-11-28 14:34:04 +03:00
Eugene Bujak
504aaddc32 Update README -- translations section had wrong level of heading 2018-11-28 13:39:48 +03:00
Eugene Bujak
6257ff123f Fix gometalinter warnings 2018-11-28 13:38:19 +03:00
Eugene Bujak
aa3f3e2c43 Make some traces into a log, remove others 2018-11-28 13:38:19 +03:00
Eugene Bujak
70c5afd6a5 Restore Engrish function names to normal English. 2018-11-28 13:38:19 +03:00
Eugene Bujak
701fd10c1c Protect against users deleting the filter ID's in the config file.
Incidentally, it also simplifies upgrade schema from 0 to 1.
2018-11-28 13:38:19 +03:00
Eugene Bujak
6cb991fe7f Clean up some code -- reorganize some structs and unexport some consts. 2018-11-28 13:38:19 +03:00
Eugene Bujak
ec7efcc9d6 Move config upgrade to separate upgrade.go 2018-11-28 13:38:19 +03:00
Ildar Kamalov
489c29b472 Merge pull request #114 in DNS/adguard-dns from fix/463 to master
* commit '5609e47c28d9481d75bf73d153bf369bca987137':
  Fixed filters table update on language change
  Add client translations
2018-11-28 11:51:03 +03:00
Ildar Kamalov
5609e47c28 Fixed filters table update on language change 2018-11-28 10:25:06 +03:00
Ildar Kamalov
8796a52c09 Add client translations 2018-11-28 10:23:50 +03:00
Eugene Bujak
12a8011fb3 Get rid of unnecessary duplicate type coreDnsFilter. 2018-11-27 16:48:57 +03:00
Eugene Bujak
47e2a1004d Remove IDE-specific noise from source code. 2018-11-27 16:05:43 +03:00
85 changed files with 5389 additions and 3896 deletions

8
.gitignore vendored
View File

@@ -1,15 +1,11 @@
.DS_Store
.vscode
.idea
debug
/.vscode
/.idea
/AdGuardHome
/AdGuardHome.yaml
/data/
/build/
/client/node_modules/
/coredns
/Corefile
/dnsfilter.txt
/querylog.json
/querylog.json.1
/scripts/translations/node_modules

View File

@@ -14,13 +14,17 @@ os:
- linux
- osx
before_install:
- nvm install node
- npm install -g npm
install:
- go get -v -d -t ./...
- npm --prefix client install
- go env
script:
- (cd `go env GOPATH`/src/github.com/prometheus/client_golang && git checkout -q v0.8.0)
- node -v
- npm -v
- go test ./...
- make build/static/index.html
- make

View File

@@ -2,7 +2,7 @@ FROM easypi/alpine-arm:latest
LABEL maintainer="Erik Rogers <erik.rogers@live.com>"
# AdGuard version
ARG ADGUARD_VERSION="0.91"
ARG ADGUARD_VERSION="0.92-hotfix1"
ENV ADGUARD_VERSION $ADGUARD_VERSION
# AdGuard architecture and package info

View File

@@ -2,7 +2,7 @@ FROM alpine:latest
LABEL maintainer="Erik Rogers <erik.rogers@live.com>"
# AdGuard version
ARG ADGUARD_VERSION="0.91"
ARG ADGUARD_VERSION="0.92-hotfix1"
ENV ADGUARD_VERSION $ADGUARD_VERSION
# AdGuard architecture and package info

View File

@@ -2,7 +2,7 @@ FROM alpine:latest
LABEL maintainer="Erik Rogers <erik.rogers@live.com>"
# AdGuard version
ARG ADGUARD_VERSION="0.91"
ARG ADGUARD_VERSION="0.92-hotfix1"
ENV ADGUARD_VERSION $ADGUARD_VERSION
# AdGuard architecture and package info

View File

@@ -19,9 +19,12 @@ client/node_modules: client/package.json client/package-lock.json
$(STATIC): $(JSFILES) client/node_modules
npm --prefix client run build-prod
$(TARGET): $(STATIC) *.go coredns_plugin/*.go dnsfilter/*.go
GOPATH=$(GOPATH) GOOS=$(NATIVE_GOOS) GOARCH=$(NATIVE_GOARCH) GO111MODULE=off go get -v github.com/gobuffalo/packr/...
GOPATH=$(GOPATH) PATH=$(GOPATH)/bin:$(PATH) packr build -ldflags="-X main.VersionString=$(GIT_VERSION)" -asmflags="-trimpath=$(PWD)" -gcflags="-trimpath=$(PWD)" -o $(TARGET)
$(TARGET): $(STATIC) *.go dhcpd/*.go dnsfilter/*.go dnsforward/*.go
go get -d .
GOOS=$(NATIVE_GOOS) GOARCH=$(NATIVE_GOARCH) GO111MODULE=off go get -v github.com/gobuffalo/packr/...
PATH=$(GOPATH)/bin:$(PATH) packr -z
CGO_ENABLED=0 go build -ldflags="-s -w -X main.VersionString=$(GIT_VERSION)" -asmflags="-trimpath=$(PWD)" -gcflags="-trimpath=$(PWD)"
PATH=$(GOPATH)/bin:$(PATH) packr clean
clean:
$(MAKE) cleanfast

104
README.md
View File

@@ -51,19 +51,33 @@ In the future, AdGuard Home is supposed to become more than just a DNS server.
### Mac
Download this file: [AdGuardHome_v0.91_MacOS.zip](https://github.com/AdguardTeam/AdGuardHome/releases/download/v0.91/AdGuardHome_v0.91_MacOS.zip), then unpack it and follow ["How to run"](#how-to-run) instructions below.
Download this file: [AdGuardHome_v0.92-hotfix1_MacOS.zip](https://github.com/AdguardTeam/AdGuardHome/releases/download/v0.92-hotfix1/AdGuardHome_v0.92-hotfix1_MacOS.zip), then unpack it and follow ["How to run"](#how-to-run) instructions below.
### Windows 64-bit
Download this file: [AdGuardHome_v0.92-hotfix1_Windows.zip](https://github.com/AdguardTeam/AdGuardHome/releases/download/v0.92-hotfix1/AdGuardHome_v0.92-hotfix1_Windows.zip), then unpack it and follow ["How to run"](#how-to-run) instructions below.
### Linux 64-bit Intel
Download this file: [AdGuardHome_v0.91_linux_amd64.tar.gz](https://github.com/AdguardTeam/AdGuardHome/releases/download/v0.91/AdGuardHome_v0.91_linux_amd64.tar.gz), then unpack it and follow ["How to run"](#how-to-run) instructions below.
Download this file: [AdGuardHome_v0.92-hotfix1_linux_amd64.tar.gz](https://github.com/AdguardTeam/AdGuardHome/releases/download/v0.92-hotfix1/AdGuardHome_v0.92-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.91_linux_386.tar.gz](https://github.com/AdguardTeam/AdGuardHome/releases/download/v0.91/AdGuardHome_v0.91_linux_386.tar.gz), then unpack it and follow ["How to run"](#how-to-run) instructions below.
Download this file: [AdGuardHome_v0.92-hotfix1_linux_386.tar.gz](https://github.com/AdguardTeam/AdGuardHome/releases/download/v0.92-hotfix1/AdGuardHome_v0.92-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.91_linux_arm.tar.gz](https://github.com/AdguardTeam/AdGuardHome/releases/download/v0.91/AdGuardHome_v0.91_linux_arm.tar.gz), then unpack it and follow ["How to run"](#how-to-run) instructions below.
Download this file: [AdGuardHome_v0.92-hotfix1_linux_arm.tar.gz](https://github.com/AdguardTeam/AdGuardHome/releases/download/v0.92-hotfix1/AdGuardHome_v0.92-hotfix1_linux_arm.tar.gz), then unpack it and follow ["How to run"](#how-to-run) instructions below.
## How to update
We have not yet implemented an auto-update of AdGuard Home, but it is planned for future versions: #448.
At the moment, the update procedure is manual:
1. Download the new AdGuard Home binary.
2. Replace the old file with the new one.
3. Restart AdGuard Home.
## How to run
@@ -77,10 +91,26 @@ Now open the browser and navigate to http://localhost:3000/ to control your AdGu
### 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:
You can run AdGuard Home without superuser privileges, but you need to either grant the binary a capability (on Linux) or instruct it to use a different port (all platforms).
#### Granting the CAP_NET_BIND_SERVICE capability (on Linux)
Note: using this method requires the `setcap` utility. You may need to install it using your Linux distribution's package manager.
To allow AdGuard Home running on Linux to listen on port 53 without superuser privileges, run:
```bash
sudo setcap CAP_NET_BIND_SERVICE=+eip ./AdGuardHome
```
Then run `./AdGuardHome` as a unprivileged user.
#### Changing the DNS listen port
To configure AdGuard Home to listen on a port that does not require superuser privileges, edit `AdGuardHome.yaml` and find these two lines:
```yaml
coredns:
dns:
port: 53
```
@@ -94,25 +124,32 @@ Upon the first execution, a file named `AdGuardHome.yaml` will be created, with
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)
* `bootstrap_dns` — DNS server used for initial hostnames resolution in case if upstream is DoH or DoT with a hostname
* `upstream_dns` — List of upstream DNS servers
* `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.
* `dns` — DNS configuration section.
* `port` — DNS server port to listen on.
* `protection_enabled`Whether any kind of filtering and protection should be done, when off it works as a plain dns forwarder.
* `filtering_enabled` — Filtering of DNS requests based on filter lists.
* `blocked_response_ttl` — For how many seconds the clients should cache a filtered response. Low values are useful on LAN if you change filters very often, high values are useful to increase performance and save traffic.
* `querylog_enabled`Query logging (also used to calculate top 50 clients, blocked domains and requested domains for statistical purposes).
* `ratelimit`DDoS protection, specifies in how many packets per second a client should receive. Anything above that is silently dropped. To disable set 0, default is 20. Safe to disable if DNS server is not available from internet.
* `ratelimit_whitelist` — If you want exclude some IP addresses from ratelimiting but keep ratelimiting on for others, put them here.
* `refuse_any` — Another DDoS protection mechanism. Requests of type ANY are rarely needed, so refusing to serve them mitigates against attackers trying to use your DNS as a reflection. Safe to disable if DNS server is not available from internet.
* `bootstrap_dns` — DNS server used for initial hostname resolution in case if upstream server name is a hostname.
* `parental_sensitivity` — Age group for parental control-based filtering, must be either 3, 10, 13 or 17 if enabled.
* `parental_enabled` — Parental control-based DNS requests filtering.
* `safesearch_enabled` — Enforcing "Safe search" option for search engines, when possible.
* `safebrowsing_enabled` — Filtering of DNS requests based on safebrowsing.
* `upstream_dns` — List of upstream DNS servers.
* `filters` — List of filters, each filter has the following values:
* `ID` - filter ID (must be unique)
* `url` — URL pointing to the filter contents (filtering rules)
* `enabled`Current filter's status (enabled/disabled)
* `user_rules` — User-specified filtering rules
* `enabled` — Current filter's status (enabled/disabled).
* `url` — URL pointing to the filter contents (filtering rules).
* `name`Name of the filter. If it's an adguard syntax filter it will get updated automatically, otherwise it stays unchanged.
* `last_updated` — Time when the filter was last updated from server.
* `ID` - filter ID (must be unique).
* `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.
@@ -141,8 +178,16 @@ cd AdGuardHome
make
```
## Contributing
You are welcome to fork this repository, make your changes and submit a pull request — https://github.com/AdguardTeam/AdGuardHome/pulls
### How to update translations
If you want to help with AdGuard Home translations, please learn more about translating AdGuard products here: https://kb.adguard.com/en/general/adguard-translations
Here is a direct link to AdGuard Home project: http://translate.adguard.com/collaboration/project?id=153384
Before updating translations you need to install dependencies:
```
cd scripts/translations
@@ -171,14 +216,6 @@ node upload.js
node download.js
```
## Contributing
You are welcome to fork this repository, make your changes and submit a pull request — https://github.com/AdguardTeam/AdGuardHome/pulls
If you want to help with AdGuard Home translations, please learn more about translating AdGuard products here: https://kb.adguard.com/en/general/adguard-translations
Here is a direct link to AdGuard Home project: http://translate.adguard.com/collaboration/project?id=153384
## Reporting issues
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.
@@ -188,7 +225,6 @@ If you run into any problem or have a suggestion, head to [this page](https://gi
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)
@@ -199,4 +235,6 @@ This software wouldn't have been possible without:
* And many more node.js packages.
* [whotracks.me data](https://github.com/cliqz-oss/whotracks.me)
You might have seen that [CoreDNS](https://coredns.io) was mentioned here before — we've stopped using it in AdGuardHome. While we still use it on our servers for [AdGuard DNS](https://adguard.com/adguard-dns/overview.html) service, it seemed like an overkill for Home as it impeded with Home features that we plan to implement.
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.

215
app.go
View File

@@ -3,14 +3,17 @@ package main
import (
"bufio"
"fmt"
"log"
"net"
"net/http"
"os"
"os/signal"
"path/filepath"
"strconv"
"syscall"
"time"
"github.com/gobuffalo/packr"
"github.com/hmage/golibs/log"
"golang.org/x/crypto/ssh/terminal"
)
@@ -41,27 +44,30 @@ func main() {
// 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
{
var printHelp func()
var configFilename *string
var bindHost *string
var bindPort *int
var opts = []struct {
longName string
shortName string
description string
callback func(value string)
longName string
shortName string
description string
callbackWithValue func(value string)
callbackNoValue func()
}{
{"config", "c", "path to config file", func(value string) { configFilename = &value }},
{"host", "h", "host address to bind HTTP server on", func(value string) { bindHost = &value }},
{"config", "c", "path to config file", func(value string) { configFilename = &value }, nil},
{"host", "h", "host address to bind HTTP server on", func(value string) { bindHost = &value }, nil},
{"port", "p", "port to serve HTTP pages on", func(value string) {
v, err := strconv.Atoi(value)
if err != nil {
panic("Got port that is not a number")
}
bindPort = &v
}},
{"help", "h", "print this help", nil},
}, nil},
{"verbose", "v", "enable verbose output", nil, func() { log.Verbose = true }},
{"help", "h", "print this help", nil, func() { printHelp(); os.Exit(64) }},
}
printHelp := func() {
printHelp = func() {
fmt.Printf("Usage:\n\n")
fmt.Printf("%s [options]\n\n", os.Args[0])
fmt.Printf("Options:\n")
@@ -71,30 +77,19 @@ func main() {
}
for i := 1; i < len(os.Args); i++ {
v := os.Args[i]
// short-circuit for help
if v == "--help" || v == "-h" {
printHelp()
os.Exit(64)
}
knownParam := false
for _, opt := range opts {
if v == "--"+opt.longName {
if i+1 > len(os.Args) {
log.Printf("ERROR: Got %s without argument\n", v)
os.Exit(64)
if v == "--"+opt.longName || v == "-"+opt.shortName {
if opt.callbackWithValue != nil {
if i+1 > len(os.Args) {
log.Printf("ERROR: Got %s without argument\n", v)
os.Exit(64)
}
i++
opt.callbackWithValue(os.Args[i])
} else if opt.callbackNoValue != nil {
opt.callbackNoValue()
}
i++
opt.callback(os.Args[i])
knownParam = true
break
}
if v == "-"+opt.shortName {
if i+1 > len(os.Args) {
log.Printf("ERROR: Got %s without argument\n", v)
os.Exit(64)
}
i++
opt.callback(os.Args[i])
knownParam = true
break
}
@@ -114,6 +109,12 @@ func main() {
log.Fatal(err)
}
// Do the upgrade if necessary
err = upgradeConfig()
if err != nil {
log.Fatal(err)
}
// parse from config file
err = parseConfig()
if err != nil {
@@ -129,36 +130,51 @@ func main() {
}
}
// Eat all args so that coredns can start happily
if len(os.Args) > 1 {
os.Args = os.Args[:1]
}
// Do the upgrade if necessary
err := upgradeConfig()
if err != nil {
log.Fatal(err)
}
// Save the updated config
err = writeConfig()
if err != nil {
log.Fatal(err)
}
// Load filters from the disk
// And if any filter has zero ID, assign a new one
for i := range config.Filters {
filter := &config.Filters[i]
err = filter.load()
filter := &config.Filters[i] // otherwise we're operating on a copy
if filter.ID == 0 {
filter.ID = assignUniqueFilterID()
}
err := filter.load()
if err != nil {
// This is okay for the first start, the filter will be loaded later
log.Printf("Couldn't load filter %d contents due to %s", filter.ID, err)
// clear LastUpdated so it gets fetched right away
}
if len(filter.Rules) == 0 {
filter.LastUpdated = time.Time{}
}
}
// Update filters we've just loaded right away, don't wait for periodic update timer
go func() {
refreshFiltersIfNeccessary(false)
// Save the updated config
err := config.write()
if err != nil {
log.Fatal(err)
}
}()
signalChannel := make(chan os.Signal)
signal.Notify(signalChannel, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT)
go func() {
<-signalChannel
cleanup()
os.Exit(0)
}()
// Save the updated config
err := config.write()
if err != nil {
log.Fatal(err)
}
address := net.JoinHostPort(config.BindHost, strconv.Itoa(config.BindPort))
runFiltersUpdatesTimer()
go periodicallyRefreshFilters()
http.Handle("/", optionalAuthHandler(http.FileServer(box)))
registerControlHandlers()
@@ -168,11 +184,23 @@ func main() {
log.Fatal(err)
}
err = startDHCPServer()
if err != nil {
log.Fatal(err)
}
URL := fmt.Sprintf("http://%s", address)
log.Println("Go to " + URL)
log.Fatal(http.ListenAndServe(address, nil))
}
func cleanup() {
err := stopDNSServer()
if err != nil {
log.Printf("Couldn't stop DNS server: %s", err)
}
}
func getInput() (string, error) {
scanner := bufio.NewScanner(os.Stdin)
scanner.Scan()
@@ -183,7 +211,7 @@ func getInput() (string, error) {
func promptAndGet(prompt string) (string, error) {
for {
fmt.Printf(prompt)
fmt.Print(prompt)
input, err := getInput()
if err != nil {
log.Printf("Failed to get input, aborting: %s", err)
@@ -198,9 +226,9 @@ func promptAndGet(prompt string) (string, error) {
func promptAndGetPassword(prompt string) (string, error) {
for {
fmt.Printf(prompt)
fmt.Print(prompt)
password, err := terminal.ReadPassword(int(os.Stdin.Fd()))
fmt.Printf("\n")
fmt.Print("\n")
if err != nil {
log.Printf("Failed to get input, aborting: %s", err)
return "", err
@@ -220,7 +248,6 @@ func askUsernamePasswordIfPossible() error {
_, 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())) {
@@ -260,81 +287,3 @@ func askUsernamePasswordIfPossible() error {
config.AuthPass = password
return nil
}
// Performs necessary upgrade operations if needed
func upgradeConfig() error {
if config.SchemaVersion == SchemaVersion {
// No upgrade, do nothing
return nil
}
if config.SchemaVersion > SchemaVersion {
// Unexpected -- the config file is newer than we expect
return fmt.Errorf("configuration file is supposed to be used with a newer version of AdGuard Home, schema=%d", config.SchemaVersion)
}
// Perform upgrade operations for each consecutive version upgrade
for oldVersion, newVersion := config.SchemaVersion, config.SchemaVersion+1; newVersion <= SchemaVersion; {
err := upgradeConfigSchema(oldVersion, newVersion)
if err != nil {
log.Fatal(err)
}
// Increment old and new versions
oldVersion++
newVersion++
}
// Save the current schema version
config.SchemaVersion = SchemaVersion
return nil
}
// Upgrade from oldVersion to newVersion
func upgradeConfigSchema(oldVersion int, newVersion int) error {
if oldVersion == 0 && newVersion == 1 {
log.Printf("Updating schema from %d to %d", oldVersion, newVersion)
// The first schema upgrade:
// Added "ID" field to "filter" -- we need to populate this field now
// Added "config.ourDataDir" -- where we will now store filters contents
for i := range config.Filters {
filter := &config.Filters[i] // otherwise we will be operating on a copy
// Set the filter ID
log.Printf("Seting ID=%d for filter %s", NextFilterId, filter.URL)
filter.ID = NextFilterId
NextFilterId++
// Forcibly update the filter
_, err := filter.update(true)
if err != nil {
log.Fatal(err)
}
// Saving it to the filters dir now
err = filter.save()
if err != nil {
log.Fatal(err)
}
}
// No more "dnsfilter.txt", filters are now loaded from config.ourDataDir/filters/
dnsFilterPath := filepath.Join(config.ourBinaryDir, "dnsfilter.txt")
_, err := os.Stat(dnsFilterPath)
if !os.IsNotExist(err) {
log.Printf("Deleting %s as we don't need it anymore", dnsFilterPath)
err = os.Remove(dnsFilterPath)
if err != nil {
log.Printf("Cannot remove %s due to %s", dnsFilterPath, err)
}
}
}
return nil
}

29
client/package-lock.json generated vendored
View File

@@ -4126,6 +4126,11 @@
"next-tick": "1"
}
},
"es6-error": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz",
"integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg=="
},
"es6-iterator": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz",
@@ -6588,7 +6593,7 @@
},
"html-webpack-plugin": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-3.2.0.tgz",
"resolved": "http://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-3.2.0.tgz",
"integrity": "sha1-sBq71yOsqqeze2r0SS69oD2d03s=",
"dev": true,
"requires": {
@@ -6638,7 +6643,7 @@
},
"readable-stream": {
"version": "1.0.34",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz",
"resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz",
"integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=",
"dev": true,
"requires": {
@@ -7387,8 +7392,7 @@
"is-promise": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz",
"integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=",
"dev": true
"integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o="
},
"is-regex": {
"version": "1.0.4",
@@ -13202,6 +13206,21 @@
"reduce-reducers": "^0.1.0"
}
},
"redux-form": {
"version": "7.4.2",
"resolved": "https://registry.npmjs.org/redux-form/-/redux-form-7.4.2.tgz",
"integrity": "sha512-QxC36s4Lelx5Cr8dbpxqvl23dwYOydeAX8c6YPmgkz/Dhj053C16S2qoyZN6LO6HJ2oUF00rKsAyE94GwOUhFA==",
"requires": {
"es6-error": "^4.1.1",
"hoist-non-react-statics": "^2.5.4",
"invariant": "^2.2.4",
"is-promise": "^2.1.0",
"lodash": "^4.17.10",
"lodash-es": "^4.17.10",
"prop-types": "^15.6.1",
"react-lifecycles-compat": "^3.0.4"
}
},
"redux-thunk": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.3.0.tgz",
@@ -15003,7 +15022,7 @@
},
"through": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
"resolved": "http://registry.npmjs.org/through/-/through-2.3.8.tgz",
"integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=",
"dev": true
},

1
client/package.json vendored
View File

@@ -31,6 +31,7 @@
"react-transition-group": "^2.4.0",
"redux": "^4.0.0",
"redux-actions": "^2.4.0",
"redux-form": "^7.4.2",
"redux-thunk": "^2.3.0",
"svg-url-loader": "^2.3.2",
"whatwg-fetch": "2.0.3"

View File

@@ -1,4 +1,30 @@
{
"check_dhcp_servers": "Check for DHCP servers",
"save_config": "Save config",
"enabled_dhcp": "DHCP server enabled",
"disabled_dhcp": "DHCP server disabled",
"dhcp_title": "DHCP server (experimental!)",
"dhcp_description": "If your router does not provide DHCP settings, you can use AdGuard's own built-in DHCP server.",
"dhcp_enable": "Enable DHCP server",
"dhcp_disable": "Disable DHCP server",
"dhcp_not_found": "No active DHCP servers found on the network. It is safe to enable the built-in DHCP server.",
"dhcp_found": "Found active DHCP servers found on the network. It is not safe to enable the built-in DHCP server.",
"dhcp_leases": "DHCP leases",
"dhcp_leases_not_found": "No DHCP leases found",
"dhcp_config_saved": "Saved DHCP server config",
"form_error_required": "Required field",
"form_error_ip_format": "Invalid IPv4 format",
"form_error_positive": "Must be greater than 0",
"dhcp_form_gateway_input": "Gateway IP",
"dhcp_form_subnet_input": "Subnet mask",
"dhcp_form_range_title": "Range of IP addresses",
"dhcp_form_range_start": "Range start",
"dhcp_form_range_end": "Range end",
"dhcp_form_lease_title": "DHCP lease time (in seconds)",
"dhcp_form_lease_input": "Lease duration",
"dhcp_interface_select": "Select DHCP interface",
"dhcp_hardware_address": "Hardware address",
"dhcp_ip_addresses": "IP addresses",
"back": "Back",
"dashboard": "Dashboard",
"settings": "Settings",
@@ -18,7 +44,7 @@
"disabled_protection": "Disabled protection",
"refresh_statics": "Refresh statistics",
"dns_query": "DNS Queries",
"blocked_by": "Blocked by",
"blocked_by": "Blocked by Filters",
"stats_malware_phishing": "Blocked malware\/phishing",
"stats_adult": "Blocked adult websites",
"stats_query_domain": "Top queried domains",
@@ -89,6 +115,7 @@
"example_upstream_regular": "regular DNS (over UDP)",
"example_upstream_dot": "encrypted <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_TLS' target='_blank'>DNS-over-TLS<\/a>",
"example_upstream_doh": "encrypted <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_HTTPS' target='_blank'>DNS-over-HTTPS<\/a>",
"example_upstream_sdns": "you can use <a href='https:\/\/dnscrypt.info\/stamps\/' target='_blank'>DNS Stamps<\/a> for <a href='https:\/\/dnscrypt.info\/' target='_blank'>DNSCrypt<\/a> or <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_HTTPS' target='_blank'>DNS-over-HTTPS<\/a> resolvers",
"example_upstream_tcp": "regular DNS (over TCP)",
"all_filters_up_to_date_toast": "All filters are already up-to-date",
"updated_upstream_dns_toast": "Updated the upstream DNS servers",
@@ -100,6 +127,7 @@
"domain_name_table_header": "Domain name",
"type_table_header": "Type",
"response_table_header": "Response",
"client_table_header": "Client",
"empty_response_status": "Empty",
"show_all_filter_type": "Show all",
"show_filtered_type": "Show filtered",
@@ -124,5 +152,6 @@
"found_in_known_domain_db": "Found in the known domains database.",
"category_label": "Category",
"rule_label": "Rule",
"filter_label": "Filter"
"filter_label": "Filter",
"unknown_filter": "Unknown filter {{filterId}}"
}

View File

@@ -1,4 +1,30 @@
{
"check_dhcp_servers": "Compruebe si hay servidores DHCP",
"save_config": "Guardar config",
"enabled_dhcp": "Servidor DHCP habilitado",
"disabled_dhcp": "Servidor DHCP deshabilitado",
"dhcp_title": "Servidor DHCP",
"dhcp_description": "Si su enrutador no proporciona la configuraci\u00f3n DHCP, puede utilizar el propio servidor DHCP incorporado de AdGuard.",
"dhcp_enable": "Habilitar servidor DHCP",
"dhcp_disable": "Deshabilitar el servidor DHCP",
"dhcp_not_found": "No se han encontrado servidores DHCP activos en la red. Es seguro habilitar el servidor DHCP incorporado.",
"dhcp_found": "Se encontraron servidores DHCP activos encontrados en la red. No es seguro habilitar el servidor DHCP incorporado.",
"dhcp_leases": "concesi\u00f3nes DHCP",
"dhcp_leases_not_found": "No se encontraron concesi\u00f3nes DHCP",
"dhcp_config_saved": "Configuraci\u00f3n del servidor DHCP guardada",
"form_error_required": "Campo obligatorio",
"form_error_ip_format": "Formato IPv4 no v\u00e1lido",
"form_error_positive": "Debe ser mayor que 0",
"dhcp_form_gateway_input": "IP de acceso",
"dhcp_form_subnet_input": "M\u00e1scara de subred",
"dhcp_form_range_title": "Rango de direcciones IP",
"dhcp_form_range_start": "Inicio de rango",
"dhcp_form_range_end": "Final de rango",
"dhcp_form_lease_title": "Tiempo de concesi\u00f3n DHCP (en segundos)",
"dhcp_form_lease_input": "duraci\u00f3n de la concesi\u00f3n",
"dhcp_interface_select": "Seleccione la interfaz DHCP",
"dhcp_hardware_address": "Direcci\u00f3n de hardware",
"dhcp_ip_addresses": "Direcciones IP",
"back": "Atr\u00e1s",
"dashboard": "Tablero de rendimiento",
"settings": "Ajustes",
@@ -9,42 +35,42 @@
"address": "direcci\u00f3n",
"on": "Activado",
"off": "Desactivado",
"copyright": "Copyright",
"copyright": "Derechos de autor",
"homepage": "P\u00e1gina de inicio",
"report_an_issue": "Reportar el error",
"report_an_issue": "Reportar un error",
"enable_protection": "Activar la protecci\u00f3n",
"enabled_protection": "Protecci\u00f3n activada",
"disable_protection": "Desactivar protecci\u00f3n",
"disabled_protection": "Protecci\u00f3n desactivada",
"refresh_statics": "Renovas estad\u00edsticas",
"dns_query": "DNS Queries",
"blocked_by": "Bloqueado por",
"refresh_statics": "Restablecer estad\u00edsticas",
"dns_query": "Consultas DNS",
"blocked_by": "Bloqueado por Filtros",
"stats_malware_phishing": "Malware\/phishing bloqueado",
"stats_adult": "Contenido adulto bloqueado",
"stats_adult": "Contenido para adultos bloqueado",
"stats_query_domain": "Dominios m\u00e1s solicitados",
"for_last_24_hours": "en las \u00faltimas 24 horas",
"no_domains_found": "No dominios encontrados",
"no_domains_found": "Dominios no encontrados",
"requests_count": "N\u00famero de solicitudes",
"top_blocked_domains": "Dominios m\u00e1s bloqueados",
"top_clients": "Clientes m\u00e1s populares",
"no_clients_found": "No hay clientes",
"general_statistics": "Estad\u00edstica general",
"general_statistics": "Estad\u00edsticas generales",
"number_of_dns_query_24_hours": "Una serie de consultas DNS procesadas durante las \u00faltimas 24 horas",
"number_of_dns_query_blocked_24_hours": "El n\u00famero de solicitudes de DNS bloqueadas por los filtros y listas de block",
"number_of_dns_query_blocked_24_hours_by_sec": "El n\u00famero de solicitudes de DNS bloqueadas por el m\u00f3dulo de navegaci\u00f3n segura de AdGuard",
"number_of_dns_query_blocked_24_hours_adult": "El n\u00famero de sitios para adultos bloqueados",
"number_of_dns_query_blocked_24_hours": "El n\u00famero de solicitudes de DNS bloqueadas por los filtros de publicidad y listas de bloqueo de hosts",
"number_of_dns_query_blocked_24_hours_by_sec": "Un n\u00famero de solicitudes de DNS bloqueadas por el m\u00f3dulo de navegaci\u00f3n segura de AdGuard",
"number_of_dns_query_blocked_24_hours_adult": "Un n\u00famero de sitios para adultos bloqueados",
"enforced_save_search": "B\u00fasqueda segura forzada",
"number_of_dns_query_to_safe_search": "Una serie de solicitudes de DNS a los motores de b\u00fasqueda para los que se aplic\u00f3 la B\u00fasqueda Segura",
"average_processing_time": "Tiempo medio de tratamiento",
"average_processing_time_hint": "Tiempo medio en milisegundos al procesar una solicitud DNS",
"average_processing_time": "Tiempo promedio de procesamiento",
"average_processing_time_hint": "Tiempo promedio en milisegundos al procesar una solicitud DNS",
"block_domain_use_filters_and_hosts": "Bloquear dominios usando filtros y archivos hosts",
"filters_block_toggle_hint": "Puede configurar las reglas de bloqueo en los ajustes <a href='#filters'>Filtros<\/a>.",
"use_adguard_browsing_sec": "Usar el servicio web de Seguridad de navegaci\u00f3n de AdGuard",
"use_adguard_browsing_sec_hint": "AdGuard Home comprobar\u00e1 si el dominio est\u00e1 en la lista negra del servicio web de seguridad de navegaci\u00f3n. Utilizar\u00e1 la API de b\u00fasqueda de privacidad para realizar la comprobaci\u00f3n: s\u00f3lo se env\u00eda al servidor un prefijo corto del hash del nombre de dominio SHA256.",
"use_adguard_browsing_sec_hint": "AdGuard Home comprobar\u00e1 si el dominio est\u00e1 en la lista negra del servicio web de seguridad de navegaci\u00f3n. Utilizar\u00e1 una API de b\u00fasqueda amigable con la privacidad para realizar la comprobaci\u00f3n: s\u00f3lo se env\u00eda al servidor un prefijo corto del hash del nombre de dominio SHA256.",
"use_adguard_parental": "Usar Control Parental de AdGuard ",
"use_adguard_parental_hint": "AdGuard Home comprobar\u00e1 si el dominio contiene materiales para adultos. Utiliza la misma API de privacidad que el servicio web de seguridad de navegaci\u00f3n.",
"enforce_safe_search": "Enforzar b\u00fasqueda segura",
"enforce_save_search_hint": "AdGuard Home puede hacer cumplir la b\u00fasqueda segura en los siguientes motores de b\u00fasqueda: Google, Youtube, Bing y Yandex.",
"use_adguard_parental_hint": "AdGuard Home comprobar\u00e1 si el dominio contiene materiales para adultos. Utiliza la misma API amigable con la privacidad que el servicio web de seguridad de navegaci\u00f3n.",
"enforce_safe_search": "Forzar b\u00fasqueda segura",
"enforce_save_search_hint": "AdGuard Home puede forzar la b\u00fasqueda segura en los siguientes motores de b\u00fasqueda: Google, Youtube, Bing y Yandex.",
"no_servers_specified": "No hay servidores especificados",
"no_settings": "No hay ajustes",
"general_settings": "Ajustes generales",
@@ -67,7 +93,7 @@
"last_time_updated_table_header": "\u00daltima actualizaci\u00f3n",
"actions_table_header": "Acciones",
"delete_table_action": "Eliminar",
"filters_and_hosts": "Filtros y hosts blocklists",
"filters_and_hosts": "Filtros y listas de bloqueo de hosts",
"filters_and_hosts_hint": "AdGuard Home entiende reglas b\u00e1sicas de bloqueo y la sintaxis de los archivos de hosts.",
"no_filters_added": "No hay filtros agregados",
"add_filter_btn": "Agregar filtro",
@@ -75,19 +101,24 @@
"enter_name_hint": "Ingresar nombre",
"enter_url_hint": "Ingresar URL",
"check_updates_btn": "Revisar si hay actualizaciones",
"new_filter_btn": "Nueva suscripci\u00f3n al filtro",
"enter_valid_filter_url": "Ingrese el URL v\u00e1lido para suscribirse o un archivo hosts.",
"new_filter_btn": "Nueva suscripci\u00f3n de filtro",
"enter_valid_filter_url": "Ingrese una URL v\u00e1lida para suscribirse o un archivo de hosts.",
"custom_filter_rules": "Personalizar reglas del filtrado",
"custom_filter_rules_hint": "Introduzca una regla en una l\u00ednea. Puede utilizar reglas de bloqueo de anuncios o sintaxis de archivos de hosts.",
"examples_title": "Ejemplos",
"example_meaning_filter_block": "bloquear acceso para el dominio ejemplo.org\ny todos sus subdominios ",
"example_meaning_filter_whitelist": "desbloquear el acceso para el dominio ejemplo.org y sus subdominios",
"example_meaning_filter_block": "bloquear acceso al dominio ejemplo.org\ny a todos sus subdominios",
"example_meaning_filter_whitelist": "desbloquear el acceso al dominio ejemplo.org y a sus subdominios",
"example_meaning_host_block": "AdGuard Home regresar\u00e1 la direcci\u00f3n 127.0.0.1 para el dominio ejemplo.org (pero no para sus subdominios).",
"example_comment": "! Aqu\u00ed va el comentario",
"example_comment": "! Aqu\u00ed va un comentario",
"example_comment_meaning": "solo un comentario",
"example_comment_hash": "# Tambi\u00e9n un comentario",
"example_upstream_regular": "DNS regular (a trav\u00e9s de UDP)",
"example_upstream_dot": "encriptado <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_TLS' target='_blank'>DNS-a-trav\u00e9s-de-TLS<\/a>",
"example_upstream_doh": "encriptado <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_HTTPS' target='_blank'>DNS-a-trav\u00e9s-de-TLS<\/a>",
"example_upstream_sdns": "puedes usar <a href='https:\/\/dnscrypt.info\/stamps\/' target='_blank'>DNS Stamps<\/a> para <a href='https:\/\/dnscrypt.info\/' target='_blank'>DNSCrypt<\/a> o <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_HTTPS' target='_blank'>DNS-over-HTTPS<\/a> resolutores",
"example_upstream_tcp": "DNS regular (a trav\u00e9s de TCP)",
"all_filters_up_to_date_toast": "Todos los filtros son actualizados",
"updated_upstream_dns_toast": "Servidores DNS upstream son actualizados",
"updated_upstream_dns_toast": "Servidores DNS upstream actualizados",
"dns_test_ok_toast": "Servidores DNS especificados funcionan correctamente",
"dns_test_not_ok_toast": "Servidor \"{{key}}\": no puede ser usado, por favor, revise si lo ha escrito correctamente",
"unblock_btn": "Desbloquear",
@@ -96,13 +127,14 @@
"domain_name_table_header": "Nombre de dominio",
"type_table_header": "Tipo",
"response_table_header": "Respuesta",
"client_table_header": "Cliente",
"empty_response_status": "Vac\u00edo",
"show_all_filter_type": "Mostrar todo",
"show_filtered_type": "Mostrar filtrados",
"no_logs_found": "No se han encontrado registros",
"disabled_log_btn": "Desactivar registro",
"download_log_file_btn": "Descargar el archivo de registro",
"refresh_btn": "Renovar",
"refresh_btn": "Refrescar",
"enabled_log_btn": "Activar registro",
"last_dns_queries": "\u00daltimas 500 solicitudes de DNS",
"previous_btn": "Anterior",
@@ -120,5 +152,6 @@
"found_in_known_domain_db": "Encontrado en la base de datos de dominios conocidos.",
"category_label": "Categor\u00eda",
"rule_label": "Regla",
"filter_label": "Filtro"
"filter_label": "Filtro",
"unknown_filter": "Filtro desconocido {{filterId}}"
}

View File

@@ -18,7 +18,7 @@
"disabled_protection": "Protection d\u00e9sactiv\u00e9e",
"refresh_statics": "Renouveler les statistiques",
"dns_query": "Requ\u00eates\u001c DNS",
"blocked_by": "Bloqu\u00e9 par",
"blocked_by": "Bloqu\u00e9 par Filtres",
"stats_malware_phishing": "Tentative de malware\/hamme\u00e7onnage bloqu\u00e9e",
"stats_adult": "Sites \u00e0 contenu adulte bloqu\u00e9s",
"stats_query_domain": "Domaines les plus recherch\u00e9s",
@@ -49,7 +49,7 @@
"no_settings": "Pas de param\u00e8tres",
"general_settings": "Param\u00e8tres g\u00e9n\u00e9raux",
"upstream_dns": "Serveurs DNS upstream",
"upstream_dns_hint": "Si vous laisses ce champ vide, AdGuard Home va utiliser <a href='https:\/\/1.1.1.1\/' target='_blank'>Cloudflare DNS<\/a> somme upstream. Utilisez le pr\u00e9fixe tls:\/\/ pour DNS via les serveurs TLS .",
"upstream_dns_hint": "Si vous laissez ce champ vide, AdGuard Home va utiliser <a href='https:\/\/1.1.1.1\/' target='_blank'>Cloudflare DNS<\/a> somme upstream. Utilisez le pr\u00e9fixe tls:\/\/ pour DNS via les serveurs TLS .",
"test_upstream_btn": "Tester les upstreams",
"apply_btn": "Appliquer",
"disabled_filtering_toast": "Filtrage d\u00e9sactiv\u00e9",
@@ -86,16 +86,21 @@
"example_comment": "! Voici comment ajouter une d\u00e9scription",
"example_comment_meaning": "commentaire",
"example_comment_hash": "# Et comme \u00e7a aussi on peut laisser des commentaires",
"example_upstream_regular": "DNS classique (au-dessus de UDP)",
"example_upstream_dot": "<a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_TLS' target='_blank'>DNS-au-dessus-de-TLS<\/a> chiffr\u00e9",
"example_upstream_doh": "<a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_HTTPS' target='_blank'>DNS-au-dessus-de-HTTPS<\/a> chiffr\u00e9",
"example_upstream_tcp": "DNS classique (au-dessus de TCP)",
"all_filters_up_to_date_toast": "Tous les filtres sont mis \u00e0 jour",
"updated_upstream_dns_toast": "Les serveurs DNS upstream sont mis \u00e0 jour",
"dns_test_ok_toast": "Les serveurs DNS sp\u00e9cifi\u00e9s fonctionnent de mani\u00e8re incorrecte",
"dns_test_not_ok_toast": "Server \"{{key}}\": could not be used, please check that you've written it correctly",
"dns_test_not_ok_toast": "Impossible d'utiliser le serveur \"{{key}}\": veuillez v\u00e9rifier si le nom saisi est bien correct",
"unblock_btn": "D\u00e9bloquer",
"block_btn": "Bloquer",
"time_table_header": "Temps",
"domain_name_table_header": "Nom de domaine",
"type_table_header": "Type",
"response_table_header": "R\u00e9ponse",
"client_table_header": "Client",
"empty_response_status": "Vide",
"show_all_filter_type": "Montrer tout",
"show_filtered_type": "Montrer les sites filtr\u00e9s",

View File

@@ -1,74 +1,100 @@
{
"check_dhcp_servers": "DHCP\u30b5\u30fc\u30d0\u3092\u30c1\u30a7\u30c3\u30af\u3059\u308b",
"save_config": "\u8a2d\u5b9a\u3092\u4fdd\u5b58\u3059\u308b",
"enabled_dhcp": "DHCP\u30b5\u30fc\u30d0\u3092\u6709\u52b9\u306b\u3057\u307e\u3057\u305f",
"disabled_dhcp": "DHCP\u30b5\u30fc\u30d0\u3092\u7121\u52b9\u306b\u3057\u307e\u3057\u305f",
"dhcp_title": "DHCP\u30b5\u30fc\u30d0",
"dhcp_description": "\u3042\u306a\u305f\u306e\u30eb\u30fc\u30bf\u304cDHCP\u306e\u8a2d\u5b9a\u3092\u63d0\u4f9b\u3057\u3066\u3044\u306a\u3044\u306e\u306a\u3089\u3001AdGuard\u306b\u5185\u8535\u3055\u308c\u305fDHCP\u30b5\u30fc\u30d0\u3092\u5229\u7528\u3067\u304d\u307e\u3059\u3002",
"dhcp_enable": "DHCP\u30b5\u30fc\u30d0\u3092\u6709\u52b9\u306b\u3059\u308b",
"dhcp_disable": "DHCP\u30b5\u30fc\u30d0\u3092\u7121\u52b9\u306b\u3059\u308b",
"dhcp_not_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u5185\u306b\u52d5\u4f5c\u3057\u3066\u3044\u308bDHCP\u30b5\u30fc\u30d0\u306f\u3042\u308a\u307e\u305b\u3093\u3002\u5185\u8535\u3055\u308c\u305fDHCP\u30b5\u30fc\u30d0\u3092\u6709\u52b9\u306b\u3057\u3066\u3082\u5b89\u5168\u3067\u3059\u3002",
"dhcp_found": "\u30cd\u30c3\u30c8\u30ef\u30fc\u30af\u5185\u306b\u6d3b\u52d5\u4e2d\u306eDHCP\u30b5\u30fc\u30d0\u3092\u898b\u3064\u3051\u307e\u3057\u305f\u3002\u5185\u81d3\u3055\u308c\u305fDHCP\u30b5\u30fc\u30d0\u3092\u6709\u52b9\u306b\u3059\u308b\u306b\u306f\u5b89\u5168\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3002",
"dhcp_leases": "DHCP\u5272\u5f53",
"dhcp_leases_not_found": "DHCP\u5272\u5f53\u306f\u3042\u308a\u307e\u305b\u3093",
"dhcp_config_saved": "DHCP\u30b5\u30fc\u30d0\u306e\u8a2d\u5b9a\u3092\u4fdd\u5b58\u3057\u307e\u3057\u305f",
"form_error_required": "\u5fc5\u9808\u9805\u76ee\u3067\u3059",
"form_error_ip_format": "IPv4\u30d5\u30a9\u30fc\u30de\u30c3\u30c8\u3067\u306f\u3042\u308a\u307e\u305b\u3093",
"form_error_positive": "0\u3088\u308a\u5927\u304d\u3044\u5fc5\u8981\u304c\u3042\u308a\u307e\u3059",
"dhcp_form_gateway_input": "\u30b2\u30fc\u30c8\u30a6\u30a7\u30a4IP",
"dhcp_form_subnet_input": "\u30b5\u30d6\u30cd\u30c3\u30c8\u30de\u30b9\u30af",
"dhcp_form_range_title": "IP\u30a2\u30c9\u30ec\u30b9\u306e\u7bc4\u56f2",
"dhcp_form_range_start": "\u7bc4\u56f2\u306e\u958b\u59cb",
"dhcp_form_range_end": "\u7bc4\u56f2\u306e\u7d42\u4e86",
"dhcp_form_lease_title": "DHCP\u5272\u5f53\u6642\u9593\uff08\u79d2\u5358\u4f4d\uff09",
"dhcp_form_lease_input": "\u5272\u5f53\u671f\u9593",
"dhcp_interface_select": "DHCP\u30a4\u30f3\u30bf\u30d5\u30a7\u30fc\u30b9\u306e\u9078\u629e",
"dhcp_hardware_address": "MAC\u30a2\u30c9\u30ec\u30b9",
"dhcp_ip_addresses": "IP\u30a2\u30c9\u30ec\u30b9",
"back": "\u623b\u308b",
"dashboard": "\u30c0\u30c3\u30b7\u30e5\u30dc\u30fc\u30c9",
"settings": "\u8a2d\u5b9a",
"filters": "\u30d5\u30a3\u30eb\u30bf",
"query_log": "\u30af\u30a8\u30ea\u30fb\u30ed\u30b0",
"faq": "FAQ",
"faq": "\u3088\u304f\u3042\u308b\u8cea\u554f",
"version": "\u30d0\u30fc\u30b8\u30e7\u30f3",
"address": "\u30a2\u30c9\u30ec\u30b9",
"on": "ON",
"off": "OFF",
"on": "\u30aa\u30f3",
"off": "\u30aa\u30d5",
"copyright": "\u8457\u4f5c\u6a29",
"homepage": "\u30db\u30fc\u30e0\u30da\u30fc\u30b8",
"report_an_issue": "\u554f\u984c\u3092\u5831\u544a\u3059\u308b",
"enable_protection": "\u4fdd\u8b77\u3092\u6709\u52b9\u306b\u3059\u308b",
"enabled_protection": "\u4fdd\u8b77\u6709\u52b9",
"enabled_protection": "\u4fdd\u8b77\u3092\u6709\u52b9\u306b\u3057\u307e\u3057\u305f",
"disable_protection": "\u4fdd\u8b77\u3092\u7121\u52b9\u306b\u3059\u308b",
"disabled_protection": "\u4fdd\u8b77\u7121\u52b9",
"refresh_statics": "\u7d71\u8a08\u30c7\u30fc\u30bf\u3092\u66f4\u65b0\u3059\u308b",
"disabled_protection": "\u4fdd\u8b77\u3092\u7121\u52b9\u306b\u3057\u307e\u3057\u305f",
"refresh_statics": "\u7d71\u8a08\u30c7\u30fc\u30bf\u3092\u6700\u65b0\u306b\u3059\u308b",
"dns_query": "DNS\u30af\u30a8\u30ea",
"blocked_by": "\u306b\u3088\u3063\u3066\u30d6\u30ed\u30c3\u30af\u6e08\u307f",
"stats_malware_phishing": "\u30d6\u30ed\u30c3\u30af\u6e08\u307f\u30de\u30eb\u30a6\u30a7\u30a2\uff0f\u30d5\u30a3\u30c3\u30b7\u30f3\u30b0",
"stats_adult": "\u30d6\u30ed\u30c3\u30af\u6e08\u307f\u30a2\u30c0\u30eb\u30c8\u30a6\u30a7\u30d6\u30b5\u30a4\u30c8",
"stats_query_domain": "\u983b\u7e41\u306b\u554f\u3044\u5408\u308f\u305b\u3055\u308c\u308b\u30c9\u30e1\u30a4\u30f3",
"for_last_24_hours": "\u904e\u53bb24\u6642\u9593\u5185",
"no_domains_found": "\u30c9\u30e1\u30a4\u30f3\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f",
"blocked_by": "\u30d5\u30a3\u30eb\u30bf\u306b\u30d6\u30ed\u30c3\u30af\u3055\u308c\u305fDNS\u30af\u30a8\u30ea",
"stats_malware_phishing": "\u30d6\u30ed\u30c3\u30af\u3055\u308c\u305f\u30de\u30eb\u30a6\u30a7\u30a2\uff0f\u30d5\u30a3\u30c3\u30b7\u30f3\u30b0",
"stats_adult": "\u30d6\u30ed\u30c3\u30af\u3055\u308c\u305f\u30a2\u30c0\u30eb\u30c8\u30a6\u30a7\u30d6\u30b5\u30a4\u30c8",
"stats_query_domain": "\u6700\u3082\u554f\u5408\u305b\u3055\u308c\u305f\u30c9\u30e1\u30a4\u30f3",
"for_last_24_hours": "\u904e\u53bb24\u6642\u9593\u4ee5\u5185",
"no_domains_found": "\u30c9\u30e1\u30a4\u30f3\u60c5\u5831\u306f\u3042\u308a\u307e\u305b\u3093",
"requests_count": "\u30ea\u30af\u30a8\u30b9\u30c8\u6570",
"top_blocked_domains": "\u6700\u3082\u30d6\u30ed\u30c3\u30af\u3055\u308c\u308b\u30c9\u30e1\u30a4\u30f3",
"top_blocked_domains": "\u6700\u3082\u30d6\u30ed\u30c3\u30af\u3055\u308c\u305f\u30c9\u30e1\u30a4\u30f3",
"top_clients": "\u30c8\u30c3\u30d7\u30af\u30e9\u30a4\u30a2\u30f3\u30c8",
"no_clients_found": "\u30af\u30e9\u30a4\u30a2\u30f3\u30c8\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f",
"general_statistics": "\u4e00\u822c\u7d71\u8a08",
"no_clients_found": "\u30af\u30e9\u30a4\u30a2\u30f3\u30c8\u60c5\u5831\u306f\u3042\u308a\u307e\u305b\u3093",
"general_statistics": "\u5168\u822c\u7684\u306a\u7d71\u8a08",
"number_of_dns_query_24_hours": "\u904e\u53bb24\u6642\u9593\u306b\u51e6\u7406\u3055\u308c\u305fDNS\u30af\u30a8\u30ea\u306e\u6570",
"number_of_dns_query_blocked_24_hours": "\u5e83\u544a\u30d6\u30ed\u30c3\u30af\u30d5\u30a3\u30eb\u30bf\u3068\u30db\u30b9\u30c8\u30d6\u30ed\u30c3\u30af\u30ea\u30b9\u30c8\u306b\u3088\u3063\u3066\u30d6\u30ed\u30c3\u30af\u3055\u308c\u305fDNS\u30ea\u30af\u30a8\u30b9\u30c8\u306e\u6570",
"number_of_dns_query_blocked_24_hours": "\u5e83\u544a\u30d6\u30ed\u30c3\u30af\u30d5\u30a3\u30eb\u30bf\u3068hosts\u30d6\u30ed\u30c3\u30af\u30ea\u30b9\u30c8\u306b\u3088\u3063\u3066\u30d6\u30ed\u30c3\u30af\u3055\u308c\u305fDNS\u30ea\u30af\u30a8\u30b9\u30c8\u306e\u6570",
"number_of_dns_query_blocked_24_hours_by_sec": "AdGuard\u30d6\u30e9\u30a6\u30b8\u30f3\u30b0\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3\u30e2\u30b8\u30e5\u30fc\u30eb\u306b\u3088\u3063\u3066\u30d6\u30ed\u30c3\u30af\u3055\u308c\u305fDNS\u30ea\u30af\u30a8\u30b9\u30c8\u306e\u6570",
"number_of_dns_query_blocked_24_hours_adult": "\u30d6\u30ed\u30c3\u30af\u6e08\u307f\u6210\u4eba\u5411\u3051\u30a6\u30a7\u30d6\u30b5\u30a4\u30c8\u306e\u6570",
"enforced_save_search": "\u30bb\u30fc\u30d5\u30b5\u30fc\u30c1\u9069\u7528\u6e08\u307f",
"number_of_dns_query_to_safe_search": "\u30bb\u30fc\u30d5\u30b5\u30fc\u30c1\u304c\u9069\u7528\u3055\u308c\u305f\u691c\u7d22\u30a8\u30f3\u30b8\u30f3\u306b\u5bfe\u3059\u308bDNS\u30ea\u30af\u30a8\u30b9\u30c8\u306e\u6570",
"number_of_dns_query_blocked_24_hours_adult": "\u30d6\u30ed\u30c3\u30af\u3055\u308c\u305f\u30a2\u30c0\u30eb\u30c8\u30a6\u30a7\u30d6\u30b5\u30a4\u30c8\u306e\u6570",
"enforced_save_search": "\u5f37\u5236\u3055\u308c\u305f\u30bb\u30fc\u30d5\u30b5\u30fc\u30c1",
"number_of_dns_query_to_safe_search": "\u30bb\u30fc\u30d5\u30b5\u30fc\u30c1\u304c\u5f37\u5236\u3055\u308c\u305f\u691c\u7d22\u30a8\u30f3\u30b8\u30f3\u306b\u5bfe\u3059\u308bDNS\u30ea\u30af\u30a8\u30b9\u30c8\u306e\u6570",
"average_processing_time": "\u5e73\u5747\u51e6\u7406\u6642\u9593",
"average_processing_time_hint": "DNS\u30ea\u30af\u30a8\u30b9\u30c8\u306e\u51e6\u7406\u306b\u304b\u304b\u308b\u5e73\u5747\u6642\u9593\uff08\u30df\u30ea\u79d2\u5358\u4f4d\uff09",
"block_domain_use_filters_and_hosts": "\u30d5\u30a3\u30eb\u30bf\u3068\u30db\u30b9\u30c8\u30d5\u30a1\u30a4\u30eb\u3092\u4f7f\u7528\u3057\u3066\u30c9\u30e1\u30a4\u30f3\u3092\u30d6\u30ed\u30c3\u30af\u3059\u308b",
"filters_block_toggle_hint": "<a href='#filters'>\u30d5\u30a3\u30eb\u30bf<\/a>\u306e\u8a2d\u5b9a\u3067\u30d6\u30ed\u30c3\u30ad\u30f3\u30b0\u30eb\u30fc\u30eb\u3092\u8a2d\u5b9a\u3059\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u3059\u3002",
"use_adguard_browsing_sec": "AdGuard\u30d6\u30e9\u30a6\u30b8\u30f3\u30b0\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3Web\u30b5\u30fc\u30d3\u30b9\u3092\u4f7f\u7528\u3059\u308b",
"use_adguard_browsing_sec_hint": "AdGuard Home\u306f\u3001\u30d6\u30e9\u30a6\u30b8\u30f3\u30b0\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3Web\u30b5\u30fc\u30d3\u30b9\u306b\u3088\u3063\u3066\u30c9\u30e1\u30a4\u30f3\u304c\u30d6\u30e9\u30c3\u30af\u30ea\u30b9\u30c8\u306b\u767b\u9332\u3055\u308c\u3066\u3044\u308b\u304b\u3069\u3046\u304b\u3092\u78ba\u8a8d\u3057\u307e\u3059\u3002 \u3053\u308c\u306f\u30d7\u30e9\u30a4\u30d0\u30b7\u30fc\u3092\u8003\u616e\u3057\u305fAPI\u3092\u4f7f\u7528\u3057\u3066\u30c1\u30a7\u30c3\u30af\u3092\u5b9f\u884c\u3057\u307e\u3059\u3002\u30c9\u30e1\u30a4\u30f3\u540dSHA256\u30cf\u30c3\u30b7\u30e5\u306e\u77ed\u3044\u30d7\u30ec\u30d5\u30a3\u30c3\u30af\u30b9\u306e\u307f\u304c\u30b5\u30fc\u30d0\u30fc\u306b\u9001\u4fe1\u3055\u308c\u307e\u3059\u3002",
"use_adguard_parental": "AdGuard\u30da\u30a2\u30ec\u30f3\u30bf\u30eb\u30b3\u30f3\u30c8\u30ed\u30fc\u30ebWeb\u30b5\u30fc\u30d3\u30b9\u3092\u4f7f\u7528\u3059\u308b",
"use_adguard_parental_hint": "AdGuard Home\u306f\u3001\u30c9\u30e1\u30a4\u30f3\u306b\u30a2\u30c0\u30eb\u30c8\u30b3\u30f3\u30c6\u30f3\u30c4\u304c\u542b\u307e\u308c\u3066\u3044\u308b\u304b\u3069\u3046\u304b\u3092\u78ba\u8a8d\u3057\u307e\u3059\u3002 \u30d6\u30e9\u30a6\u30b8\u30f3\u30b0\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3Web\u30b5\u30fc\u30d3\u30b9\u3068\u540c\u3058\u30d7\u30e9\u30a4\u30d0\u30b7\u30fc\u306b\u512a\u3057\u3044API\u3092\u4f7f\u7528\u3057\u307e\u3059\u3002",
"enforce_safe_search": "\u30bb\u30fc\u30d5\u30b5\u30fc\u30c1\u3092\u9069\u7528\u3059\u308b",
"enforce_save_search_hint": "AdGuard Home\u306f\u3001Google\u3001Youtube\u3001Bing\u3001Yandex\u306e\u691c\u7d22\u30a8\u30f3\u30b8\u30f3\u3067\u30bb\u30fc\u30d5\u30b5\u30fc\u30c1\u3092\u9069\u7528\u3067\u304d\u307e\u3059\u3002",
"no_servers_specified": "\u30b5\u30fc\u30d0\u30fc\u304c\u6307\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093",
"block_domain_use_filters_and_hosts": "\u30d5\u30a3\u30eb\u30bf\u3068hosts\u30d5\u30a1\u30a4\u30eb\u3092\u4f7f\u7528\u3057\u3066\u30c9\u30e1\u30a4\u30f3\u3092\u30d6\u30ed\u30c3\u30af\u3059\u308b",
"filters_block_toggle_hint": "<a href='#filters'>\u30d5\u30a3\u30eb\u30bf<\/a>\u306e\u8a2d\u5b9a\u3067\u30d6\u30ed\u30c3\u30af\u3059\u308b\u30eb\u30fc\u30eb\u3092\u8a2d\u5b9a\u3059\u308b\u3053\u3068\u304c\u3067\u304d\u307e\u3059\u3002",
"use_adguard_browsing_sec": "AdGuard\u30d6\u30e9\u30a6\u30b8\u30f3\u30b0\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3\u30fb\u30a6\u30a7\u30d6\u30b5\u30fc\u30d3\u30b9\u3092\u4f7f\u7528\u3059\u308b",
"use_adguard_browsing_sec_hint": "AdGuard Home\u306f\u3001\u30d6\u30e9\u30a6\u30b8\u30f3\u30b0\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3\u30fb\u30a6\u30a7\u30d6\u30b5\u30fc\u30d3\u30b9\u306b\u3088\u3063\u3066\u30c9\u30e1\u30a4\u30f3\u304c\u30d6\u30e9\u30c3\u30af\u30ea\u30b9\u30c8\u306b\u767b\u9332\u3055\u308c\u3066\u3044\u308b\u304b\u3069\u3046\u304b\u3092\u78ba\u8a8d\u3057\u307e\u3059\u3002 \u3053\u308c\u306f\u30d7\u30e9\u30a4\u30d0\u30b7\u30fc\u3092\u8003\u616e\u3057\u305fAPI\u3092\u4f7f\u7528\u3057\u3066\u30c1\u30a7\u30c3\u30af\u3092\u5b9f\u884c\u3057\u307e\u3059\u3002\u30c9\u30e1\u30a4\u30f3\u540dSHA256\u30cf\u30c3\u30b7\u30e5\u306e\u77ed\u3044\u30d7\u30ec\u30d5\u30a3\u30c3\u30af\u30b9\u306e\u307f\u304c\u30b5\u30fc\u30d0\u306b\u9001\u4fe1\u3055\u308c\u307e\u3059\u3002",
"use_adguard_parental": "AdGuard\u30da\u30a2\u30ec\u30f3\u30bf\u30eb\u30b3\u30f3\u30c8\u30ed\u30fc\u30eb\u30fb\u30a6\u30a7\u30d6\u30b5\u30fc\u30d3\u30b9\u3092\u4f7f\u7528\u3059\u308b",
"use_adguard_parental_hint": "AdGuard Home\u306f\u3001\u30c9\u30e1\u30a4\u30f3\u306b\u30a2\u30c0\u30eb\u30c8\u30b3\u30f3\u30c6\u30f3\u30c4\u304c\u542b\u307e\u308c\u3066\u3044\u308b\u304b\u3069\u3046\u304b\u3092\u78ba\u8a8d\u3057\u307e\u3059\u3002 \u30d6\u30e9\u30a6\u30b8\u30f3\u30b0\u30bb\u30ad\u30e5\u30ea\u30c6\u30a3\u30fb\u30a6\u30a7\u30d6\u30b5\u30fc\u30d3\u30b9\u3068\u540c\u3058\u30d7\u30e9\u30a4\u30d0\u30b7\u30fc\u306b\u512a\u3057\u3044API\u3092\u4f7f\u7528\u3057\u307e\u3059\u3002",
"enforce_safe_search": "\u30bb\u30fc\u30d5\u30b5\u30fc\u30c1\u3092\u5f37\u5236\u3059\u308b",
"enforce_save_search_hint": "AdGuard Home\u306f\u3001Google\u3001Youtube\u3001Bing\u3001Yandex\u306e\u691c\u7d22\u30a8\u30f3\u30b8\u30f3\u3067\u30bb\u30fc\u30d5\u30b5\u30fc\u30c1\u3092\u5f37\u5236\u3067\u304d\u307e\u3059\u3002",
"no_servers_specified": "\u30b5\u30fc\u30d0\u304c\u6307\u5b9a\u3055\u308c\u3066\u3044\u307e\u305b\u3093",
"no_settings": "\u8a2d\u5b9a\u306a\u3057",
"general_settings": "\u4e00\u822c\u8a2d\u5b9a",
"upstream_dns": "\u30a2\u30c3\u30d7\u30b9\u30c8\u30ea\u30fc\u30e0DNS\u30b5\u30fc\u30d0\u30fc",
"upstream_dns_hint": "\u3053\u306e\u30d5\u30a3\u30fc\u30eb\u30c9\u3092\u7a7a\u306e\u307e\u307e\u306b\u3059\u308b\u3068\u3001AdGuard Home\u306f<a href='https:\/\/1.1.1.1\/' target='_blank'>Cloudflare DNS<\/a>\u3092\u30a2\u30c3\u30d7\u30b9\u30c8\u30ea\u30fc\u30e0\u3068\u3057\u3066\u4f7f\u7528\u3057\u307e\u3059\u3002 TLS\u30b5\u30fc\u30d0\u30fc\u7d4c\u7531\u306eDNS\u306b\u306f\u3001\uff62tls:\/\/\u300d\u30d7\u30ec\u30d5\u30a3\u30c3\u30af\u30b9\u3092\u4f7f\u7528\u3057\u3066\u304f\u3060\u3055\u3044\u3002",
"test_upstream_btn": "\u30a2\u30c3\u30d7\u30b9\u30c8\u30ea\u30fc\u30e0\u30b5\u30fc\u30d0\u30fc\u30c6\u30b9\u30c8",
"apply_btn": "\u9069\u7528",
"disabled_filtering_toast": "\u30d5\u30a3\u30eb\u30bf\u30ea\u30f3\u30b0\u7121\u52b9",
"enabled_filtering_toast": "\u30d5\u30a3\u30eb\u30bf\u30ea\u30f3\u30b0\u6709\u52b9",
"disabled_safe_browsing_toast": "\u30bb\u30fc\u30d5\u30d6\u30e9\u30a6\u30b8\u30f3\u30b0\u7121\u52b9",
"enabled_safe_browsing_toast": "\u30bb\u30fc\u30d5\u30d6\u30e9\u30a6\u30b8\u30f3\u30b0\u6709\u52b9",
"disabled_parental_toast": "\u30da\u30a2\u30ec\u30f3\u30bf\u30eb\u30b3\u30f3\u30c8\u30ed\u30fc\u30eb\u7121\u52b9",
"enabled_parental_toast": "\u30da\u30a2\u30ec\u30f3\u30bf\u30eb\u30b3\u30f3\u30c8\u30ed\u30fc\u30eb\u6709\u52b9",
"disabled_safe_search_toast": "\u30bb\u30fc\u30d5\u30b5\u30fc\u30c1\u7121\u52b9",
"enabled_save_search_toast": "\u30bb\u30fc\u30d5\u30b5\u30fc\u30c1\u6709\u52b9",
"upstream_dns": "\u4e0a\u6d41DNS\u30b5\u30fc\u30d0",
"upstream_dns_hint": "\u3053\u306e\u30d5\u30a3\u30fc\u30eb\u30c9\u3092\u672a\u5165\u529b\u306e\u307e\u307e\u306b\u3059\u308b\u3068\u3001AdGuard Home\u306f\u4e0a\u6d41\u3068\u3057\u3066<a href='https:\/\/1.1.1.1\/' target='_blank'>Cloudflare DNS<\/a>\u3092\u4f7f\u7528\u3057\u307e\u3059\u3002DNS over TLS\u30b5\u30fc\u30d0\u306b\u306f\u3001\uff62tls:\/\/\u300d\u30d7\u30ec\u30d5\u30a3\u30c3\u30af\u30b9\u3092\u4f7f\u7528\u3057\u3066\u304f\u3060\u3055\u3044\u3002",
"test_upstream_btn": "\u4e0a\u6d41\u30b5\u30fc\u30d0\u3092\u30c6\u30b9\u30c8\u3059\u308b",
"apply_btn": "\u9069\u7528\u3059\u308b",
"disabled_filtering_toast": "\u30d5\u30a3\u30eb\u30bf\u30ea\u30f3\u30b0\u3092\u7121\u52b9\u306b\u3057\u307e\u3057\u305f",
"enabled_filtering_toast": "\u30d5\u30a3\u30eb\u30bf\u30ea\u30f3\u30b0\u3092\u6709\u52b9\u306b\u3057\u307e\u3057\u305f",
"disabled_safe_browsing_toast": "\u30bb\u30fc\u30d5\u30d6\u30e9\u30a6\u30b8\u30f3\u30b0\u3092\u7121\u52b9\u306b\u3057\u307e\u3057\u305f",
"enabled_safe_browsing_toast": "\u30bb\u30fc\u30d5\u30d6\u30e9\u30a6\u30b8\u30f3\u30b0\u3092\u6709\u52b9\u306b\u3057\u307e\u3057\u305f",
"disabled_parental_toast": "\u30da\u30a2\u30ec\u30f3\u30bf\u30eb\u30b3\u30f3\u30c8\u30ed\u30fc\u30eb\u3092\u7121\u52b9\u306b\u3057\u307e\u3057\u305f",
"enabled_parental_toast": "\u30da\u30a2\u30ec\u30f3\u30bf\u30eb\u30b3\u30f3\u30c8\u30ed\u30fc\u30eb\u3092\u6709\u52b9\u306b\u3057\u307e\u3057\u305f",
"disabled_safe_search_toast": "\u30bb\u30fc\u30d5\u30b5\u30fc\u30c1\u3092\u7121\u52b9\u306b\u3057\u307e\u3057\u305f",
"enabled_save_search_toast": "\u30bb\u30fc\u30d5\u30b5\u30fc\u30c1\u3092\u6709\u52b9\u306b\u3057\u307e\u3057\u305f",
"enabled_table_header": "\u6709\u52b9",
"name_table_header": "\u540d\u79f0",
"filter_url_table_header": "\u30d5\u30a3\u30eb\u30bf\u306eURL",
"rules_count_table_header": "\u30eb\u30fc\u30eb\u6570",
"last_time_updated_table_header": "\u6700\u7d42\u66f4\u65b0\u65e5",
"last_time_updated_table_header": "\u6700\u7d42\u66f4\u65b0\u6642\u523b",
"actions_table_header": "\u64cd\u4f5c",
"delete_table_action": "\u524a\u9664",
"filters_and_hosts": "\u30d5\u30a3\u30eb\u30bf\u3068\u30db\u30b9\u30c8\u30d6\u30ed\u30c3\u30af\u30ea\u30b9\u30c8",
"filters_and_hosts_hint": "AdGuard Home\u306f\u3001\u57fa\u672c\u7684\u306a\u5e83\u544a\u30d6\u30ed\u30c3\u30af\u30eb\u30fc\u30eb\u3068\u30db\u30b9\u30c8\u30d5\u30a1\u30a4\u30eb\u306e\u69cb\u6587\u3092\u7406\u89e3\u3057\u3066\u3044\u307e\u3059\u3002",
"delete_table_action": "\u524a\u9664\u3059\u308b",
"filters_and_hosts": "\u30d5\u30a3\u30eb\u30bf\u3068hosts\u30d6\u30ed\u30c3\u30af\u30ea\u30b9\u30c8",
"filters_and_hosts_hint": "AdGuard Home\u306f\u3001\u57fa\u672c\u7684\u306a\u5e83\u544a\u30d6\u30ed\u30c3\u30af\u30eb\u30fc\u30eb\u3068hosts\u30d5\u30a1\u30a4\u30eb\u306e\u69cb\u6587\u3092\u7406\u89e3\u3057\u307e\u3059\u3002",
"no_filters_added": "\u30d5\u30a3\u30eb\u30bf\u306f\u8ffd\u52a0\u3055\u308c\u307e\u305b\u3093\u3067\u3057\u305f",
"add_filter_btn": "\u30d5\u30a3\u30eb\u30bf\u3092\u8ffd\u52a0\u3059\u308b",
"cancel_btn": "\u30ad\u30e3\u30f3\u30bb\u30eb",
@@ -76,49 +102,56 @@
"enter_url_hint": "URL\u3092\u5165\u529b",
"check_updates_btn": "\u30a2\u30c3\u30d7\u30c7\u30fc\u30c8\u3092\u78ba\u8a8d\u3059\u308b",
"new_filter_btn": "\u65b0\u3057\u3044\u30d5\u30a3\u30eb\u30bf\u30fb\u30b5\u30d6\u30b9\u30af\u30ea\u30d7\u30b7\u30e7\u30f3",
"enter_valid_filter_url": "\u30d5\u30a3\u30eb\u30bf\u30fb\u30b5\u30d6\u30b9\u30af\u30ea\u30d7\u30b7\u30e7\u30f3\u304a\u3088\u3073\u30db\u30b9\u30c8\u30d5\u30a1\u30a4\u30eb\u306e\u6709\u52b9\u306aURL\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002",
"enter_valid_filter_url": "\u30d5\u30a3\u30eb\u30bf\u30fb\u30b5\u30d6\u30b9\u30af\u30ea\u30d7\u30b7\u30e7\u30f3\u3082\u3057\u304f\u306fhosts\u30d5\u30a1\u30a4\u30eb\u306e\u6709\u52b9\u306aURL\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002",
"custom_filter_rules": "\u30ab\u30b9\u30bf\u30e0\u30fb\u30d5\u30a3\u30eb\u30bf\u30ea\u30f3\u30b0\u30eb\u30fc\u30eb",
"custom_filter_rules_hint": "1\u3064\u306e\u884c\u306b1\u3064\u306e\u30eb\u30fc\u30eb\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002 \u5e83\u544a\u30d6\u30ed\u30c3\u30af\u30eb\u30fc\u30eb\u3084\u30db\u30b9\u30c8\u30d5\u30a1\u30a4\u30eb\u69cb\u6587\u3092\u4f7f\u7528\u3067\u304d\u307e\u3059\u3002",
"custom_filter_rules_hint": "1\u3064\u306e\u884c\u306b1\u3064\u306e\u30eb\u30fc\u30eb\u3092\u5165\u529b\u3057\u3066\u304f\u3060\u3055\u3044\u3002 \u5e83\u544a\u30d6\u30ed\u30c3\u30af\u30eb\u30fc\u30eb\u3084hosts\u30d5\u30a1\u30a4\u30eb\u69cb\u6587\u3092\u4f7f\u7528\u3067\u304d\u307e\u3059\u3002",
"examples_title": "\u4f8b",
"example_meaning_filter_block": "example.org\u30c9\u30e1\u30a4\u30f3\u3068\u305d\u306e\u3059\u3079\u3066\u306e\u30b5\u30d6\u30c9\u30e1\u30a4\u30f3\u3078\u306e\u30a2\u30af\u30bb\u30b9\u3092\u30d6\u30ed\u30c3\u30af\u3059\u308b",
"example_meaning_filter_whitelist": "example.org\u30c9\u30e1\u30a4\u30f3\u3068\u305d\u306e\u3059\u3079\u3066\u306e\u30b5\u30d6\u30c9\u30e1\u30a4\u30f3\u3078\u306e\u30a2\u30af\u30bb\u30b9\u306e\u30d6\u30ed\u30c3\u30af\u3092\u89e3\u9664\u3059\u308b",
"example_meaning_host_block": "AdGuard Home\u306f\u3001example.org\u30c9\u30e1\u30a4\u30f3\uff08\u30b5\u30d6\u30c9\u30e1\u30a4\u30f3\u306f\u9664\u304f\uff09\u306b\u5bfe\u3057\u3066127.0.0.1\u306e\u30a2\u30c9\u30ec\u30b9\u3092\u8fd4\u3059\u3088\u3046\u306b\u306a\u308a\u307e\u3057\u305f\u3002",
"example_meaning_host_block": "AdGuard Home\u306f\u3001example.org\u30c9\u30e1\u30a4\u30f3\uff08\u30b5\u30d6\u30c9\u30e1\u30a4\u30f3\u3092\u9664\u304f\uff09\u306b\u5bfe\u3057\u3066127.0.0.1\u306e\u30a2\u30c9\u30ec\u30b9\u3092\u8fd4\u3059\u3088\u3046\u306b\u306a\u308a\u307e\u3059\u3002",
"example_comment": "! \u3053\u3053\u306b\u306f\u30b3\u30e1\u30f3\u30c8\u304c\u5165\u308a\u307e\u3059",
"example_comment_meaning": "\u305f\u3060\u306e\u30b3\u30e1\u30f3\u30c8",
"example_comment_hash": "# \u3053\u3053\u3082\u30b3\u30e1\u30f3\u30c8",
"all_filters_up_to_date_toast": "\u30d5\u30a3\u30eb\u30bf\u306f\u65e2\u306b\u3059\u3079\u3066\u6700\u65b0\u3067\u3059",
"updated_upstream_dns_toast": "\u30a2\u30c3\u30d7\u30b9\u30c8\u30ea\u30fc\u30e0DNS\u30b5\u30fc\u30d0\u30fc\u3092\u66f4\u65b0\u3057\u307e\u3057\u305f",
"dns_test_ok_toast": "\u6307\u5b9a\u3055\u308c\u305fDNS\u30b5\u30fc\u30d0\u30fc\u306f\u6b63\u3057\u304f\u52d5\u4f5c\u3057\u3066\u3044\u307e\u3059",
"example_comment_meaning": "\u305f\u3060\u306e\u30b3\u30e1\u30f3\u30c8\u3067\u3059",
"example_comment_hash": "# \u3053\u3053\u3082\u30b3\u30e1\u30f3\u30c8\u3067\u3059",
"example_upstream_regular": "\u901a\u5e38\u306eDNS\uff08UDP\u3067\u306e\u554f\u3044\u5408\u308f\u305b\uff09",
"example_upstream_dot": "\u6697\u53f7\u5316\u3055\u308c\u3066\u3044\u308b <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_TLS' target='_blank'>DNS-over-TLS<\/a>",
"example_upstream_doh": "\u6697\u53f7\u5316\u3055\u308c\u3066\u3044\u308b <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_HTTPS' target='_blank'>DNS-over-HTTPS<\/a>",
"example_upstream_sdns": "<a href='https:\/\/dnscrypt.info\/' target='_blank'>DNSCrypt<\/a> \u307e\u305f\u306f <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_HTTPS' target='_blank'>DNS-over-HTTPS<\/a> \u30ea\u30be\u30eb\u30d0\u306e\u305f\u3081\u306b <a href='https:\/\/dnscrypt.info\/stamps\/' target='_blank'>DNS Stamps<\/a> \u3092\u4f7f\u3048\u307e\u3059",
"example_upstream_tcp": "\u901a\u5e38\u306eDNS\uff08TCP\u3067\u306e\u554f\u3044\u5408\u308f\u305b\uff09",
"all_filters_up_to_date_toast": "\u3059\u3079\u3066\u306e\u30d5\u30a3\u30eb\u30bf\u306f\u65e2\u306b\u6700\u65b0\u3067\u3059",
"updated_upstream_dns_toast": "\u4e0a\u6d41DNS\u30b5\u30fc\u30d0\u3092\u66f4\u65b0\u3057\u307e\u3057\u305f",
"dns_test_ok_toast": "\u6307\u5b9a\u3055\u308c\u305fDNS\u30b5\u30fc\u30d0\u306f\u6b63\u3057\u304f\u52d5\u4f5c\u3057\u3066\u3044\u307e\u3059",
"dns_test_not_ok_toast": "\u30b5\u30fc\u30d0 \"{{key}}\": \u4f7f\u7528\u3067\u304d\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u6b63\u3057\u304f\u5165\u529b\u3055\u308c\u3066\u3044\u308b\u304b\u3069\u3046\u304b\u3092\u78ba\u8a8d\u3057\u3066\u304f\u3060\u3055\u3044",
"unblock_btn": "\u30d6\u30ed\u30c3\u30af\u89e3\u9664",
"block_btn": "\u30d6\u30ed\u30c3\u30af",
"block_btn": "\u30d6\u30ed\u30c3\u30af\u3059\u308b",
"time_table_header": "\u6642\u523b",
"domain_name_table_header": "\u30c9\u30e1\u30a4\u30f3\u540d",
"type_table_header": "\u7a2e\u985e",
"response_table_header": "\u5fdc\u7b54",
"empty_response_status": "\u7a7a",
"show_all_filter_type": "\u5168\u3066\u8868\u793a",
"show_filtered_type": "\u30d5\u30a3\u30eb\u30bf\u30ea\u30f3\u30b0\u6e08\u307f\u3092\u8868\u793a",
"no_logs_found": "\u30ed\u30b0\u898b\u3064\u304b\u308a\u307e\u305b\u3093\u3067\u3057\u305f",
"client_table_header": "\u30af\u30e9\u30a4\u30a2\u30f3\u30c8",
"empty_response_status": "\u672a\u5b9a\u7fa9",
"show_all_filter_type": "\u3059\u3079\u3066\u8868\u793a",
"show_filtered_type": "\u30d5\u30a3\u30eb\u30bf\u3055\u308c\u305f\u30ed\u30b0\u3092\u8868\u793a",
"no_logs_found": "\u30ed\u30b0\u306f\u3042\u308a\u307e\u305b\u3093",
"disabled_log_btn": "\u30ed\u30b0\u3092\u7121\u52b9\u306b\u3059\u308b",
"download_log_file_btn": "\u30ed\u30b0\u30d5\u30a1\u30a4\u30eb\u3092\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9",
"refresh_btn": "\u66f4\u65b0",
"download_log_file_btn": "\u30ed\u30b0\u30d5\u30a1\u30a4\u30eb\u3092\u30c0\u30a6\u30f3\u30ed\u30fc\u30c9\u3059\u308b",
"refresh_btn": "\u6700\u65b0\u306b\u3059\u308b",
"enabled_log_btn": "\u30ed\u30b0\u3092\u6709\u52b9\u306b\u3059\u308b",
"last_dns_queries": "\u6700\u7d42\uff15\uff10\uff10\uff10\u672c\u306eDNS\u30af\u30a8\u30ea",
"previous_btn": "\u524d",
"last_dns_queries": "\u6700\u65b05000\u4ef6\u5206\u306eDNS\u30af\u30a8\u30ea",
"previous_btn": "\u524d\u3078",
"next_btn": "\u6b21\u3078",
"loading_table_status": "\u8aad\u307f\u8fbc\u307f\u4e2d\u2026",
"page_table_footer_text": "\u30da\u30fc\u30b8",
"of_table_footer_text": "\uff0f",
"rows_table_footer_text": "\u884c",
"updated_custom_filtering_toast": "\u30ab\u30b9\u30bf\u30e0\u30fb\u30d5\u30a3\u30eb\u30bf\u30ea\u30f3\u30b0\u30eb\u30fc\u30eb\u3092\u66f4\u65b0\u3057\u307e\u3057\u305f",
"rule_removed_from_custom_filtering_toast": "\u30ab\u30b9\u30bf\u30e0\u30fb\u30d5\u30a3\u30eb\u30bf\u30ea\u30f3\u30b0\u30eb\u30fc\u30eb\u304b\u3089\u30eb\u30fc\u30eb\u3092\u9664\u53bb\u3057\u307e\u3057\u305f",
"rule_added_to_custom_filtering_toast": "\u30ab\u30b9\u30bf\u30e0\u30fb\u30d5\u30a3\u30eb\u30bf\u30ea\u30f3\u30b0\u30eb\u30fc\u30eb\u306b\u30eb\u30fc\u30eb\u3092\u8ffd\u52a0\u3057\u307e\u3057\u305f",
"query_log_disabled_toast": "\u30af\u30a8\u30ea\u30fb\u30ed\u30b0\u7121\u52b9",
"query_log_enabled_toast": "\u30af\u30a8\u30ea\u30fb\u30ed\u30b0\u6709\u52b9",
"rule_removed_from_custom_filtering_toast": "\u30eb\u30fc\u30eb\u3092\u30ab\u30b9\u30bf\u30e0\u30fb\u30d5\u30a3\u30eb\u30bf\u30ea\u30f3\u30b0\u30eb\u30fc\u30eb\u304b\u3089\u9664\u53bb\u3057\u307e\u3057\u305f",
"rule_added_to_custom_filtering_toast": "\u30eb\u30fc\u30eb\u3092\u30ab\u30b9\u30bf\u30e0\u30fb\u30d5\u30a3\u30eb\u30bf\u30ea\u30f3\u30b0\u30eb\u30fc\u30eb\u306b\u8ffd\u52a0\u3057\u307e\u3057\u305f",
"query_log_disabled_toast": "\u30af\u30a8\u30ea\u30fb\u30ed\u30b0\u3092\u7121\u52b9\u306b\u3057\u307e\u3057\u305f",
"query_log_enabled_toast": "\u30af\u30a8\u30ea\u30fb\u30ed\u30b0\u3092\u6709\u52b9\u306b\u3057\u307e\u3057\u305f",
"source_label": "\u30bd\u30fc\u30b9",
"found_in_known_domain_db": "\u65e2\u77e5\u306e\u30c9\u30e1\u30a4\u30f3\u30c7\u30fc\u30bf\u30d9\u30fc\u30b9\u306b\u898b\u3064\u304b\u308a\u307e\u3057\u305f\u3002",
"category_label": "\u30ab\u30c6\u30b4\u30ea",
"rule_label": "\u30eb\u30fc\u30eb",
"filter_label": "\u30d5\u30a3\u30eb\u30bf"
"filter_label": "\u30d5\u30a3\u30eb\u30bf",
"unknown_filter": "\u4e0d\u660e\u306a\u30d5\u30a3\u30eb\u30bf {{filterId}}"
}

View File

@@ -1,4 +1,30 @@
{
"check_dhcp_servers": "Verifique se h\u00e1 servidores DHCP",
"save_config": "Salvar configura\u00e7\u00e3o",
"enabled_dhcp": "Servidor DHCP ativado",
"disabled_dhcp": "Servidor DHCP desativado",
"dhcp_title": "Servidor DHCP",
"dhcp_description": "Se o seu roteador n\u00e3o fornecer configura\u00e7\u00f5es de DHCP, voc\u00ea poder\u00e1 usar o servidor DHCP integrado do AdGuard.",
"dhcp_enable": "Ativar servidor DHCP",
"dhcp_disable": "Desativar servidor DHCP",
"dhcp_not_found": "Nenhum servidor DHCP ativo foi encontrado na sua rede. \u00c9 seguro ativar o servidor DHCP integrado.",
"dhcp_found": "Nenhum servidor DHCP ativo foi encontrado na sua rede. N\u00e3o \u00e9 seguro ativar o servidor DHCP integrado.",
"dhcp_leases": "Concess\u00f5es DHCP",
"dhcp_leases_not_found": "Nenhuma concess\u00e3o DHCP encontrada",
"dhcp_config_saved": "Salvar configura\u00e7\u00f5es do servidor DHCP",
"form_error_required": "Campo obrigat\u00f3rio",
"form_error_ip_format": "formato de endere\u00e7o IPv4 inv\u00e1lido",
"form_error_positive": "Deve ser maior que 0",
"dhcp_form_gateway_input": "IP do gateway",
"dhcp_form_subnet_input": "M\u00e1scara de sub-rede",
"dhcp_form_range_title": "Faixa de endere\u00e7os IP",
"dhcp_form_range_start": "In\u00edcio da faixa",
"dhcp_form_range_end": "Final da faixa",
"dhcp_form_lease_title": "Tempo de concess\u00e3o do DHCP (em segundos)",
"dhcp_form_lease_input": "Dura\u00e7\u00e3o da concess\u00e3o",
"dhcp_interface_select": "Selecione a interface DHCP",
"dhcp_hardware_address": "Endere\u00e7o de hardware",
"dhcp_ip_addresses": "Endere\u00e7o de IP",
"back": "Voltar",
"dashboard": "Painel",
"settings": "Configura\u00e7\u00f5es",
@@ -18,7 +44,7 @@
"disabled_protection": "Prote\u00e7\u00e3o desativada",
"refresh_statics": "Atualizar estat\u00edsticas",
"dns_query": "Consultas de DNS",
"blocked_by": "Bloqueador por",
"blocked_by": "Bloqueador por filtros",
"stats_malware_phishing": "Bloqueado malware\/phishing",
"stats_adult": "Bloqueado sites adultos",
"stats_query_domain": "Principais dom\u00ednios consultados",
@@ -86,6 +112,11 @@
"example_comment": "! Aqui vai um coment\u00e1rio",
"example_comment_meaning": "apenas um coment\u00e1rio",
"example_comment_hash": "# Tamb\u00e9m um coment\u00e1rio",
"example_upstream_regular": "DNS regular (atrav\u00e9s do UDP)",
"example_upstream_dot": "DNS criptografado <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_TLS' target='_blank'>atrav\u00e9s do TLS<\/a>",
"example_upstream_doh": "DNS criptografado <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_HTTPS' target='_blank'>atrav\u00e9s do HTTPS<\/a>",
"example_upstream_sdns": "Voc\u00ea pode usar <a href='https:\/\/dnscrypt.info\/stamps\/' target='_blank'>DNS Stamps<\/a>para o<a href='https:\/\/dnscrypt.info\/' target='_blank'>DNSCrypt<\/a>ou usar resolvedores<a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_HTTPS' target='_blank'>DNS-sobre-HTTPS<\/a>",
"example_upstream_tcp": "DNS regular (atrav\u00e9s do TCP)",
"all_filters_up_to_date_toast": "Todos os filtros j\u00e1 est\u00e3o atualizados",
"updated_upstream_dns_toast": "Atualizado os servidores DNS upstream",
"dns_test_ok_toast": "Os servidores DNS especificados est\u00e3o funcionando corretamente",
@@ -96,6 +127,7 @@
"domain_name_table_header": "Nome de dom\u00ednio",
"type_table_header": "Tipo",
"response_table_header": "Resposta",
"client_table_header": "Cliente",
"empty_response_status": "Vazio",
"show_all_filter_type": "Mostrar todos",
"show_filtered_type": "Mostrar filtrados",
@@ -120,5 +152,6 @@
"found_in_known_domain_db": "Encontrado no banco de dados de dom\u00ednios conhecidos.",
"category_label": "Categoria",
"rule_label": "Regra",
"filter_label": "Filtro"
"filter_label": "Filtro",
"unknown_filter": "Filtro desconhecido {{filterId}}"
}

View File

@@ -18,7 +18,7 @@
"disabled_protection": "\u0417\u0430\u0449\u0438\u0442\u0430 \u0432\u044b\u043a\u043b.",
"refresh_statics": "\u041e\u0431\u043d\u043e\u0432\u0438\u0442\u044c \u0441\u0442\u0430\u0442\u0438\u0441\u0442\u0438\u043a\u0443",
"dns_query": "DNS-\u0437\u0430\u043f\u0440\u043e\u0441\u044b",
"blocked_by": "\u0417\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043e ",
"blocked_by": "\u0417\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043e \u0424\u0438\u043b\u044c\u0442\u0440\u044b",
"stats_malware_phishing": "\u0417\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0435 \u0432\u0440\u0435\u0434\u043e\u043d\u043e\u0441\u043d\u044b\u0435 \u0438 \u0444\u0438\u0448\u0438\u043d\u0433\u043e\u0432\u044b\u0435 \u0441\u0430\u0439\u0442\u044b",
"stats_adult": "\u0417\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0435 \"\u0432\u0437\u0440\u043e\u0441\u043b\u044b\u0435\" \u0441\u0430\u0439\u0442\u044b",
"stats_query_domain": "\u0427\u0430\u0441\u0442\u043e \u0437\u0430\u043f\u0440\u0430\u0448\u0438\u0432\u0430\u0435\u043c\u044b\u0435 \u0434\u043e\u043c\u0435\u043d\u044b",
@@ -77,7 +77,7 @@
"check_updates_btn": "\u041f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f",
"new_filter_btn": "\u0414\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u043d\u043e\u0432\u043e\u0433\u043e \u0444\u0438\u043b\u044c\u0442\u0440\u0430",
"enter_valid_filter_url": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0434\u0435\u0439\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u0439 URL \u0434\u043b\u044f \u043f\u043e\u0434\u043f\u0438\u0441\u043a\u0438 \u043d\u0430 \u0444\u0438\u043b\u044c\u0442\u0440 \u0438\u043b\u0438 \u0444\u0430\u0439\u043b hosts.",
"custom_filter_rules": "\u041F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044C\u0441\u043A\u0438\u0439 \u0444\u0438\u043B\u044C\u0442\u0440",
"custom_filter_rules": "\u041f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u043e\u0435 \u043f\u0440\u0430\u0432\u0438\u043b\u043e \u0444\u0438\u043b\u044c\u0442\u0440\u0430\u0446\u0438\u0438",
"custom_filter_rules_hint": "\u0412\u0432\u043e\u0434\u0438\u0442\u0435 \u043f\u043e \u043e\u0434\u043d\u043e\u043c\u0443 \u043f\u0440\u0430\u0432\u0438\u043b\u0443 \u043d\u0430 \u0441\u0442\u0440\u043e\u0447\u043a\u0443. \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043f\u0440\u0430\u0432\u0438\u043b\u0430 \u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u043a\u0438 \u0438\u043b\u0438 \u0441\u0438\u043d\u0442\u0430\u043a\u0441\u0438\u0441 \u0444\u0430\u0439\u043b\u043e\u0432 hosts.",
"examples_title": "\u041f\u0440\u0438\u043c\u0435\u0440\u044b",
"example_meaning_filter_block": "\u0437\u0430\u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u0434\u043e\u043c\u0435\u043d\u0443 example.org \u0438 \u0432\u0441\u0435\u043c \u0435\u0433\u043e \u043f\u043e\u0434\u0434\u043e\u043c\u0435\u043d\u0430\u043c",
@@ -100,6 +100,7 @@
"domain_name_table_header": "\u0414\u043e\u043c\u0435\u043d",
"type_table_header": "\u0422\u0438\u043f",
"response_table_header": "\u041e\u0442\u0432\u0435\u0442",
"client_table_header": "\u041a\u043b\u0438\u0435\u043d\u0442",
"empty_response_status": "\u041f\u0443\u0441\u0442\u043e",
"show_all_filter_type": "\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u0432\u0441\u0435",
"show_filtered_type": "\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u043e\u0442\u0444\u0438\u043b\u044c\u0442\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u0435",

View File

@@ -1,4 +1,26 @@
{
"refresh_status": "Uppdatera status",
"save_config": "Spara inst\u00e4llningar",
"enabled_dhcp": "DHCP-server aktiverad",
"disabled_dhcp": "Dhcp-server avaktiverad",
"dhcp_title": "DHCP-server",
"dhcp_description": "Om din router inte har inst\u00e4llningar f\u00f6r DHCP kan du anv\u00e4nda AdGuards inbyggda server.",
"dhcp_enable": "Aktivera DHCP.-server",
"dhcp_disable": "Avaktivera DHCP-server",
"dhcp_not_found": "Ingen aktiv DHCP-server hittades i n\u00e4tverkat.",
"dhcp_leases": "DHCP-lease",
"dhcp_leases_not_found": "Ingen DHCP-lease hittad",
"dhcp_config_saved": "Sparade inst\u00e4llningar f\u00f6r DHCP-servern",
"form_error_required": "Obligatoriskt f\u00e4lt",
"form_error_ip_format": "Ogiltigt IPv4-format",
"form_error_positive": "M\u00e5ste vara st\u00f6rre \u00e4n noll",
"dhcp_form_gateway_input": "Gateway-IP",
"dhcp_form_subnet_input": "Subnetmask",
"dhcp_form_range_title": "IP-adressgr\u00e4nser",
"dhcp_form_range_start": "Startgr\u00e4ns",
"dhcp_form_range_end": "Gr\u00e4nsslut",
"dhcp_form_lease_title": "DHCP-leasetid (i sekunder)",
"dhcp_form_lease_input": "Leasetid",
"back": "Tiilbaka",
"dashboard": "Kontrollpanel",
"settings": "Inst\u00e4llningar",
@@ -18,7 +40,7 @@
"disabled_protection": "Kopplade bort skydd",
"refresh_statics": "Uppdatera statistik",
"dns_query": "DNS-f\u00f6rfr\u00e5gningar",
"blocked_by": "Blockerat av",
"blocked_by": "Blockerat av filter",
"stats_malware_phishing": "Blockerad skadekod\/phising",
"stats_adult": "Blockerade vuxensajter",
"stats_query_domain": "Mest efters\u00f6kta dom\u00e4ner",
@@ -86,6 +108,11 @@
"example_comment": "! H\u00e4r kommer en kommentar",
"example_comment_meaning": "Endast en kommentar",
"example_comment_hash": "# Ocks\u00e5 en kommentar",
"example_upstream_regular": "vanlig DNS (\u00f6ver UDP)",
"example_upstream_dot": "krypterat <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_TLS' target='_blank'>DNS-over-TLS<\/a>",
"example_upstream_doh": "krypterat <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_HTTPS' target='_blank'>DNS-over-HTTPS<\/a>",
"example_upstream_sdns": "Du kan anv\u00e4nda <a href='https:\/\/dnscrypt.info\/stamps\/' target='_blank'>DNS-stamps<\/a> f\u00f6r <a href='https:\/\/dnscrypt.info\/' target='_blank'>DNSCrypt<\/a> eller <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_HTTPS' target='_blank'>DNS-\u00f6ver-HTTPS<\/a>\n-resolvers",
"example_upstream_tcp": "vanlig DNS (\u00f6ver UDP)",
"all_filters_up_to_date_toast": "Alla filter \u00e4r redan aktuella",
"updated_upstream_dns_toast": "Uppdaterade uppstr\u00f6ms-dns-servrar",
"dns_test_ok_toast": "Angivna DNS servrar fungerar korrekt",
@@ -96,7 +123,8 @@
"domain_name_table_header": "Dom\u00e4nnamn",
"type_table_header": "Typ",
"response_table_header": "Svar",
"empty_response_status": "Empty",
"client_table_header": "Klient",
"empty_response_status": "Tomt",
"show_all_filter_type": "Visa alla",
"show_filtered_type": "Visa filtrerade",
"no_logs_found": "Inga logga funna",
@@ -120,5 +148,6 @@
"found_in_known_domain_db": "Hittad i dom\u00e4ndatabas.",
"category_label": "Kategori",
"rule_label": "Regel",
"filter_label": "Filter"
"filter_label": "Filter",
"unknown_filter": "Ok\u00e4nt filter {{filterId}}"
}

View File

@@ -18,7 +18,7 @@
"disabled_protection": "\u0110\u00e3 t\u1eaft b\u1ea3o v\u1ec7",
"refresh_statics": "L\u00e0m m\u1edbi th\u1ed1ng k\u00ea",
"dns_query": "Truy v\u1ea5n DNS",
"blocked_by": "Ch\u1eb7n b\u1edfi",
"blocked_by": "Ch\u1eb7n b\u1edfi B\u1ed9 l\u1ecdc",
"stats_malware_phishing": "M\u00e3 \u0111\u1ed9c\/l\u1eeba \u0111\u1ea3o \u0111\u00e3 ch\u1eb7n",
"stats_adult": "Website ng\u01b0\u1eddi l\u1edbn \u0111\u00e3 ch\u1eb7n",
"stats_query_domain": "T\u00ean mi\u1ec1n truy v\u1ea5n nhi\u1ec1u",
@@ -86,6 +86,10 @@
"example_comment": "! \u0110\u00e2y l\u00e0 m\u1ed9t ch\u00fa th\u00edch",
"example_comment_meaning": "Ch\u1ec9 l\u00e0 m\u1ed9t ch\u00fa th\u00edch",
"example_comment_hash": "# C\u0169ng l\u00e0 m\u1ed9t ch\u00fa th\u00edch",
"example_upstream_regular": "DNS th\u00f4ng th\u01b0\u1eddng (d\u00f9ng UDP)",
"example_upstream_dot": "\u0111\u01b0\u1ee3c m\u00e3 ho\u00e1 <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_TLS' target='_blank'>DNS-d\u1ef1a te-TLS<\/a>",
"example_upstream_doh": "\u0111\u01b0\u1ee3c m\u00e3 ho\u00e1 <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_HTTPS' target='_blank'>DNS-over-HTTPS<\/a>",
"example_upstream_tcp": "DNS th\u00f4ng th\u01b0\u1eddng(d\u00f9ng TCP)",
"all_filters_up_to_date_toast": "T\u1ea5t c\u1ea3 b\u1ed9 l\u1ecdc \u0111\u00e3 \u0111\u01b0\u1ee3c c\u1eadp nh\u1eadt",
"updated_upstream_dns_toast": "\u0110\u00e3 c\u1eadp nh\u1eadt m\u00e1y ch\u1ee7 DNS t\u00ecm ki\u1ebfm",
"dns_test_ok_toast": "M\u00e1y ch\u1ee7 DNS c\u00f3 th\u1ec3 s\u1eed d\u1ee5ng",
@@ -96,6 +100,7 @@
"domain_name_table_header": "T\u00ean mi\u1ec1n",
"type_table_header": "Lo\u1ea1i",
"response_table_header": "Ph\u1ea3n h\u1ed3i",
"client_table_header": "Ng\u01b0\u1eddi d\u00f9ng cu\u1ed1i",
"empty_response_status": "R\u1ed7ng",
"show_all_filter_type": "Hi\u1ec7n t\u1ea5t c\u1ea3",
"show_filtered_type": "Ch\u1ec9 hi\u1ec7n \u0111\u00e3 ch\u1eb7n",

View File

@@ -0,0 +1,157 @@
{
"check_dhcp_servers": "\u6aa2\u67e5\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u4f3a\u670d\u5668",
"save_config": "\u5132\u5b58\u914d\u7f6e",
"enabled_dhcp": "\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u4f3a\u670d\u5668\u5df2\u88ab\u555f\u7528",
"disabled_dhcp": "\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u4f3a\u670d\u5668\u5df2\u88ab\u7981\u7528",
"dhcp_title": "\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u4f3a\u670d\u5668",
"dhcp_description": "\u5982\u679c\u60a8\u7684\u8def\u7531\u5668\u672a\u63d0\u4f9b\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u8a2d\u5b9a\uff0c\u60a8\u53ef\u4f7f\u7528AdGuard\u81ea\u8eab\u5167\u5efa\u7684DHCP\u4f3a\u670d\u5668\u3002",
"dhcp_enable": "\u555f\u7528\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u4f3a\u670d\u5668",
"dhcp_disable": "\u7981\u7528\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u4f3a\u670d\u5668",
"dhcp_not_found": "\u65bc\u7db2\u8def\u4e0a\u7121\u5df2\u767c\u73fe\u4e4b\u6709\u6548\u7684\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u4f3a\u670d\u5668\u3002\u555f\u7528\u5167\u5efa\u7684DHCP\u4f3a\u670d\u5668\u70ba\u5b89\u5168\u7684\u3002",
"dhcp_found": "\u65bc\u7db2\u8def\u4e0a\u5df2\u767c\u73fe\u4e4b\u6709\u6548\u7684\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u4f3a\u670d\u5668\u3002\u555f\u7528\u5167\u5efa\u7684DHCP\u4f3a\u670d\u5668\u70ba\u4e0d\u5b89\u5168\u7684\u3002",
"dhcp_leases": "\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u79df\u8cc3",
"dhcp_leases_not_found": "\u7121\u5df2\u767c\u73fe\u4e4b\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u79df\u8cc3",
"dhcp_config_saved": "\u5df2\u5132\u5b58\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u4f3a\u670d\u5668\u914d\u7f6e",
"form_error_required": "\u5fc5\u586b\u7684\u6b04\u4f4d",
"form_error_ip_format": "\u7121\u6548\u7684IPv4\u683c\u5f0f",
"form_error_positive": "\u5fc5\u9808\u5927\u65bc0",
"dhcp_form_gateway_input": "\u9598\u9053 IP",
"dhcp_form_subnet_input": "\u5b50\u7db2\u8def\u906e\u7f69",
"dhcp_form_range_title": "IP\u4f4d\u5740\u7bc4\u570d",
"dhcp_form_range_start": "\u7bc4\u570d\u958b\u59cb",
"dhcp_form_range_end": "\u7bc4\u570d\u7d50\u675f",
"dhcp_form_lease_title": "\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u79df\u8cc3\u6642\u9593\uff08\u4ee5\u79d2\u6578\uff09",
"dhcp_form_lease_input": "\u79df\u8cc3\u6301\u7e8c\u6642\u9593",
"dhcp_interface_select": "\u9078\u64c7\u52d5\u614b\u4e3b\u6a5f\u8a2d\u5b9a\u5354\u5b9a\uff08DHCP\uff09\u4ecb\u9762",
"dhcp_hardware_address": "\u786c\u9ad4\u4f4d\u5740",
"dhcp_ip_addresses": "IP \u4f4d\u5740",
"back": "\u8fd4\u56de",
"dashboard": "\u5100\u8868\u677f",
"settings": "\u8a2d\u5b9a",
"filters": "\u904e\u6ffe\u5668",
"query_log": "\u67e5\u8a62\u8a18\u9304",
"faq": "\u5e38\u898b\u554f\u7b54\u96c6",
"version": "\u7248\u672c",
"address": "\u4f4d\u5740",
"on": "\u958b\u555f",
"off": "\u95dc\u9589",
"copyright": "\u7248\u6b0a",
"homepage": "\u9996\u9801",
"report_an_issue": "\u5831\u544a\u554f\u984c",
"enable_protection": "\u555f\u7528\u9632\u8b77",
"enabled_protection": "\u5df2\u555f\u7528\u9632\u8b77",
"disable_protection": "\u7981\u7528\u9632\u8b77",
"disabled_protection": "\u5df2\u7981\u7528\u9632\u8b77",
"refresh_statics": "\u91cd\u65b0\u6574\u7406\u7d71\u8a08\u8cc7\u6599",
"dns_query": "DNS \u67e5\u8a62",
"blocked_by": "\u5df2\u88ab\u904e\u6ffe\u5668\u5c01\u9396",
"stats_malware_phishing": "\u5df2\u5c01\u9396\u7684\u60e1\u610f\u8edf\u9ad4\/\u7db2\u8def\u91e3\u9b5a",
"stats_adult": "\u5df2\u5c01\u9396\u7684\u6210\u4eba\u7db2\u7ad9",
"stats_query_domain": "\u71b1\u9580\u5df2\u67e5\u8a62\u7684\u7db2\u57df",
"for_last_24_hours": "\u5728\u6700\u8fd1\u768424\u5c0f\u6642\u5167",
"no_domains_found": "\u7121\u5df2\u767c\u73fe\u4e4b\u7db2\u57df",
"requests_count": "\u8acb\u6c42\u7e3d\u6578",
"top_blocked_domains": "\u71b1\u9580\u5df2\u5c01\u9396\u7684\u7db2\u57df",
"top_clients": "\u71b1\u9580\u7528\u6236\u7aef",
"no_clients_found": "\u7121\u5df2\u767c\u73fe\u4e4b\u7528\u6236\u7aef",
"general_statistics": "\u4e00\u822c\u7684\u7d71\u8a08\u8cc7\u6599",
"number_of_dns_query_24_hours": "\u5728\u6700\u8fd1\u768424 \u5c0f\u6642\u5167\u5df2\u8655\u7406\u7684DNS\u67e5\u8a62\u4e4b\u6578\u91cf",
"number_of_dns_query_blocked_24_hours": "\u5df2\u88ab\u5ee3\u544a\u5c01\u9396\u904e\u6ffe\u5668\u548c\u4e3b\u6a5f\u5c01\u9396\u6e05\u55ae\u5c01\u9396\u7684DNS\u8acb\u6c42\u4e4b\u6578\u91cf",
"number_of_dns_query_blocked_24_hours_by_sec": "\u5df2\u88abAdGuard\u700f\u89bd\u5b89\u5168\u6a21\u7d44\u5c01\u9396\u7684DNS\u8acb\u6c42\u4e4b\u6578\u91cf",
"number_of_dns_query_blocked_24_hours_adult": "\u5df2\u5c01\u9396\u7684\u6210\u4eba\u7db2\u7ad9\u4e4b\u6578\u91cf",
"enforced_save_search": "\u5df2\u5f37\u5236\u57f7\u884c\u7684\u5b89\u5168\u641c\u5c0b",
"number_of_dns_query_to_safe_search": "\u5c0d\u65bc\u90a3\u4e9b\u5b89\u5168\u641c\u5c0b\u5df2\u88ab\u5f37\u5236\u57f7\u884c\u4e4b\u5c6c\u65bc\u641c\u5c0b\u5f15\u64ce\u7684DNS\u8acb\u6c42\u4e4b\u6578\u91cf",
"average_processing_time": "\u5e73\u5747\u7684\u8655\u7406\u6642\u9593",
"average_processing_time_hint": "\u65bc\u8655\u7406\u4e00\u9805DNS\u8acb\u6c42\u4e0a\u4ee5\u6beb\u79d2\uff08ms\uff09\u8a08\u4e4b\u5e73\u5747\u7684\u6642\u9593",
"block_domain_use_filters_and_hosts": "\u900f\u904e\u904e\u6ffe\u5668\u548c\u4e3b\u6a5f\u6a94\u6848\u5c01\u9396\u7db2\u57df",
"filters_block_toggle_hint": "\u60a8\u53ef\u5728<a href='#filters'>\u904e\u6ffe\u5668<\/a>\u8a2d\u5b9a\u4e2d\u8a2d\u7f6e\u5c01\u9396\u898f\u5247\u3002",
"use_adguard_browsing_sec": "\u4f7f\u7528AdGuard\u700f\u89bd\u5b89\u5168\u7db2\u8def\u670d\u52d9",
"use_adguard_browsing_sec_hint": "AdGuard Home\u5c07\u6aa2\u67e5\u7db2\u57df\u662f\u5426\u88ab\u700f\u89bd\u5b89\u5168\u7db2\u8def\u670d\u52d9\u5217\u5165\u9ed1\u540d\u55ae\u3002\u5b83\u5c07\u4f7f\u7528\u53cb\u597d\u7684\u96b1\u79c1\u67e5\u627e\u61c9\u7528\u7a0b\u5f0f\u4ecb\u9762\uff08API\uff09\u4ee5\u57f7\u884c\u6aa2\u67e5\uff1a\u50c5\u57df\u540dSHA256\u96dc\u6e4a\u7684\u77ed\u524d\u7db4\u88ab\u50b3\u9001\u5230\u4f3a\u670d\u5668\u3002",
"use_adguard_parental": "\u4f7f\u7528AdGuard\u5bb6\u9577\u76e3\u63a7\u4e4b\u7db2\u8def\u670d\u52d9",
"use_adguard_parental_hint": "AdGuard Home\u5c07\u6aa2\u67e5\u7db2\u57df\u662f\u5426\u5305\u542b\u6210\u4eba\u8cc7\u6599\u3002\u5b83\u4f7f\u7528\u5982\u540c\u700f\u89bd\u5b89\u5168\u7db2\u8def\u670d\u52d9\u4e00\u6a23\u4e4b\u53cb\u597d\u7684\u96b1\u79c1\u61c9\u7528\u7a0b\u5f0f\u4ecb\u9762\uff08API\uff09\u3002",
"enforce_safe_search": "\u5f37\u5236\u57f7\u884c\u5b89\u5168\u641c\u5c0b",
"enforce_save_search_hint": "AdGuard Home\u53ef\u5728\u4ee5\u4e0b\u7684\u641c\u5c0b\u5f15\u64ce\uff1aGoogle\u3001YouTube\u3001Bing\u548cYandex\u4e2d\u5f37\u5236\u57f7\u884c\u5b89\u5168\u641c\u5c0b\u3002",
"no_servers_specified": "\u7121\u5df2\u660e\u78ba\u6307\u5b9a\u7684\u4f3a\u670d\u5668",
"no_settings": "\u7121\u8a2d\u5b9a",
"general_settings": "\u4e00\u822c\u7684\u8a2d\u5b9a",
"upstream_dns": "\u4e0a\u6e38\u7684DNS\u4f3a\u670d\u5668",
"upstream_dns_hint": "\u5982\u679c\u60a8\u4fdd\u7559\u8a72\u6b04\u4f4d\u7a7a\u767d\u7684\uff0cAdGuard Home\u5c07\u4f7f\u7528<a href='https:\/\/1.1.1.1\/' target='_blank'>Cloudflare DNS<\/a>\u4f5c\u70ba\u4e0a\u6e38\u3002\u5c0d\u65bcDNS over TLS\u4f3a\u670d\u5668\u4f7f\u7528 tls:\/\/ \u524d\u7db4\u3002",
"test_upstream_btn": "\u6e2c\u8a66\u4e0a\u884c\u8cc7\u6599\u6d41",
"apply_btn": "\u5957\u7528",
"disabled_filtering_toast": "\u5df2\u7981\u7528\u904e\u6ffe",
"enabled_filtering_toast": "\u5df2\u555f\u7528\u904e\u6ffe",
"disabled_safe_browsing_toast": "\u5df2\u7981\u7528\u5b89\u5168\u700f\u89bd",
"enabled_safe_browsing_toast": "\u5df2\u555f\u7528\u5b89\u5168\u700f\u89bd",
"disabled_parental_toast": "\u5df2\u7981\u7528\u5bb6\u9577\u76e3\u63a7",
"enabled_parental_toast": "\u5df2\u555f\u7528\u5bb6\u9577\u76e3\u63a7",
"disabled_safe_search_toast": "\u5df2\u7981\u7528\u5b89\u5168\u641c\u5c0b",
"enabled_save_search_toast": "\u5df2\u555f\u7528\u5b89\u5168\u641c\u5c0b",
"enabled_table_header": "\u5df2\u555f\u7528\u7684",
"name_table_header": "\u540d\u7a31",
"filter_url_table_header": "\u904e\u6ffe\u5668\u7db2\u5740",
"rules_count_table_header": "\u898f\u5247\u7e3d\u6578",
"last_time_updated_table_header": "\u6700\u8fd1\u7684\u66f4\u65b0\u6642\u9593",
"actions_table_header": "\u884c\u52d5",
"delete_table_action": "\u522a\u9664",
"filters_and_hosts": "\u904e\u6ffe\u5668\u548c\u4e3b\u6a5f\u5c01\u9396\u6e05\u55ae",
"filters_and_hosts_hint": "AdGuard Home\u61c2\u5f97\u57fa\u672c\u7684\u5ee3\u544a\u5c01\u9396\u898f\u5247\u548c\u4e3b\u6a5f\u6a94\u6848\u8a9e\u6cd5\u3002",
"no_filters_added": "\u7121\u5df2\u52a0\u5165\u7684\u904e\u6ffe\u5668",
"add_filter_btn": "\u589e\u52a0\u904e\u6ffe\u5668",
"cancel_btn": "\u53d6\u6d88",
"enter_name_hint": "\u8f38\u5165\u540d\u7a31",
"enter_url_hint": "\u8f38\u5165\u7db2\u5740",
"check_updates_btn": "\u6aa2\u67e5\u66f4\u65b0",
"new_filter_btn": "\u65b0\u7684\u904e\u6ffe\u5668\u8a02\u95b1",
"enter_valid_filter_url": "\u8f38\u5165\u95dc\u65bc\u904e\u6ffe\u5668\u8a02\u95b1\u6216\u4e3b\u6a5f\u6a94\u6848\u4e4b\u6709\u6548\u7684\u7db2\u5740\u3002",
"custom_filter_rules": "\u81ea\u8a02\u7684\u904e\u6ffe\u898f\u5247",
"custom_filter_rules_hint": "\u65bc\u4e00\u884c\u4e0a\u8f38\u5165\u4e00\u500b\u898f\u5247\u3002\u60a8\u53ef\u4f7f\u7528\u5ee3\u544a\u5c01\u9396\u898f\u5247\u6216\u4e3b\u6a5f\u6a94\u6848\u8a9e\u6cd5\u3002",
"examples_title": "\u7bc4\u4f8b",
"example_meaning_filter_block": "\u5c01\u9396\u81f3example.org\u7db2\u57df\u53ca\u5176\u6240\u6709\u7684\u5b50\u7db2\u57df\u4e4b\u5b58\u53d6",
"example_meaning_filter_whitelist": "\u89e3\u9664\u5c01\u9396\u81f3example.org\u7db2\u57df\u53ca\u5176\u6240\u6709\u7684\u5b50\u7db2\u57df\u4e4b\u5b58\u53d6",
"example_meaning_host_block": "AdGuard Home\u73fe\u5728\u5c07\u5c0dexample.org\u7db2\u57df\u8fd4\u56de127.0.0.1\u4f4d\u5740\uff08\u4f46\u975e\u5176\u5b50\u7db2\u57df\uff09\u3002",
"example_comment": "! \u770b\uff0c\u4e00\u500b\u8a3b\u89e3",
"example_comment_meaning": "\u53ea\u662f\u4e00\u500b\u8a3b\u89e3",
"example_comment_hash": "# \u4e5f\u662f\u4e00\u500b\u8a3b\u89e3",
"example_upstream_regular": "\u4e00\u822c\u7684 DNS\uff08\u900f\u904eUDP\uff09",
"example_upstream_dot": "\u52a0\u5bc6\u7684 <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_TLS' target='_blank'>DNS-over-TLS<\/a>",
"example_upstream_doh": "\u52a0\u5bc6\u7684 <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_HTTPS' target='_blank'>DNS-over-HTTPS <\/a>",
"example_upstream_sdns": "\u60a8\u53ef\u4f7f\u7528\u5c0d\u65bc <a href='https:\/\/dnscrypt.info\/' target='_blank'>DNSCrypt<\/a> \u6216 <a href='https:\/\/en.wikipedia.org\/wiki\/DNS_over_HTTPS' target='_blank'>DNS-over-HTTPS<\/a> \u89e3\u6790\u5668\u4e4b <a href='https:\/\/dnscrypt.info\/stamps\/' target='_blank'>DNS \u6233\u8a18<\/a>",
"example_upstream_tcp": "\u4e00\u822c\u7684 DNS\uff08\u900f\u904eTCP\uff09",
"all_filters_up_to_date_toast": "\u6240\u6709\u7684\u904e\u6ffe\u5668\u5df2\u662f\u6700\u65b0\u7684",
"updated_upstream_dns_toast": "\u5df2\u66f4\u65b0\u4e0a\u6e38\u7684DNS\u4f3a\u670d\u5668",
"dns_test_ok_toast": "\u660e\u78ba\u6307\u5b9a\u7684DNS\u4f3a\u670d\u5668\u6b63\u78ba\u5730\u904b\u4f5c\u4e2d",
"dns_test_not_ok_toast": "\u4f3a\u670d\u5668 \"{{key}}\"\uff1a\u7121\u6cd5\u88ab\u4f7f\u7528\uff0c\u8acb\u6aa2\u67e5\u60a8\u5df2\u6b63\u78ba\u5730\u586b\u5beb\u5b83",
"unblock_btn": "\u89e3\u9664\u5c01\u9396",
"block_btn": "\u5c01\u9396",
"time_table_header": "\u6642\u9593",
"domain_name_table_header": "\u57df\u540d",
"type_table_header": "\u985e\u578b",
"response_table_header": "\u53cd\u61c9",
"client_table_header": "\u7528\u6236\u7aef",
"empty_response_status": "\u7a7a\u767d\u7684",
"show_all_filter_type": "\u986f\u793a\u6240\u6709",
"show_filtered_type": "\u986f\u793a\u5df2\u904e\u6ffe\u7684",
"no_logs_found": "\u7121\u5df2\u767c\u73fe\u4e4b\u8a18\u9304",
"disabled_log_btn": "\u7981\u7528\u8a18\u9304",
"download_log_file_btn": "\u4e0b\u8f09\u8a18\u9304\u6a94\u6848",
"refresh_btn": "\u91cd\u65b0\u6574\u7406",
"enabled_log_btn": "\u555f\u7528\u8a18\u9304",
"last_dns_queries": "\u6700\u8fd1\u76845000\u7b46DNS\u67e5\u8a62",
"previous_btn": "\u4e0a\u4e00\u9801",
"next_btn": "\u4e0b\u4e00\u9801",
"loading_table_status": "\u6b63\u5728\u8f09\u5165...",
"page_table_footer_text": "\u9801\u9762",
"of_table_footer_text": "\u4e4b",
"rows_table_footer_text": "\u5217",
"updated_custom_filtering_toast": "\u5df2\u66f4\u65b0\u81ea\u8a02\u7684\u904e\u6ffe\u898f\u5247",
"rule_removed_from_custom_filtering_toast": "\u898f\u5247\u5df2\u5f9e\u81ea\u8a02\u7684\u904e\u6ffe\u898f\u5247\u4e2d\u88ab\u79fb\u9664",
"rule_added_to_custom_filtering_toast": "\u898f\u5247\u5df2\u5f9e\u81ea\u8a02\u7684\u904e\u6ffe\u898f\u5247\u4e2d\u88ab\u52a0\u5165",
"query_log_disabled_toast": "\u67e5\u8a62\u8a18\u9304\u5df2\u88ab\u7981\u7528",
"query_log_enabled_toast": "\u67e5\u8a62\u8a18\u9304\u5df2\u88ab\u555f\u7528",
"source_label": "\u4f86\u6e90",
"found_in_known_domain_db": "\u5728\u5df2\u77e5\u7684\u57df\u540d\u8cc7\u6599\u5eab\u4e2d\u88ab\u767c\u73fe\u3002",
"category_label": "\u985e\u5225",
"rule_label": "\u898f\u5247",
"filter_label": "\u904e\u6ffe\u5668",
"unknown_filter": "\u672a\u77e5\u7684\u904e\u6ffe\u5668 {{filterId}}"
}

View File

@@ -522,3 +522,130 @@ export const getLanguage = () => async (dispatch) => {
dispatch(getLanguageFailure());
}
};
export const getDhcpStatusRequest = createAction('GET_DHCP_STATUS_REQUEST');
export const getDhcpStatusSuccess = createAction('GET_DHCP_STATUS_SUCCESS');
export const getDhcpStatusFailure = createAction('GET_DHCP_STATUS_FAILURE');
export const getDhcpStatus = () => async (dispatch) => {
dispatch(getDhcpStatusRequest());
try {
const status = await apiClient.getDhcpStatus();
dispatch(getDhcpStatusSuccess(status));
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(getDhcpStatusFailure());
}
};
export const getDhcpInterfacesRequest = createAction('GET_DHCP_INTERFACES_REQUEST');
export const getDhcpInterfacesSuccess = createAction('GET_DHCP_INTERFACES_SUCCESS');
export const getDhcpInterfacesFailure = createAction('GET_DHCP_INTERFACES_FAILURE');
export const getDhcpInterfaces = () => async (dispatch) => {
dispatch(getDhcpInterfacesRequest());
try {
const interfaces = await apiClient.getDhcpInterfaces();
dispatch(getDhcpInterfacesSuccess(interfaces));
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(getDhcpInterfacesFailure());
}
};
export const findActiveDhcpRequest = createAction('FIND_ACTIVE_DHCP_REQUEST');
export const findActiveDhcpSuccess = createAction('FIND_ACTIVE_DHCP_SUCCESS');
export const findActiveDhcpFailure = createAction('FIND_ACTIVE_DHCP_FAILURE');
export const findActiveDhcp = name => async (dispatch) => {
dispatch(findActiveDhcpRequest());
try {
const activeDhcp = await apiClient.findActiveDhcp(name);
dispatch(findActiveDhcpSuccess(activeDhcp));
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(findActiveDhcpFailure());
}
};
export const setDhcpConfigRequest = createAction('SET_DHCP_CONFIG_REQUEST');
export const setDhcpConfigSuccess = createAction('SET_DHCP_CONFIG_SUCCESS');
export const setDhcpConfigFailure = createAction('SET_DHCP_CONFIG_FAILURE');
// TODO rewrite findActiveDhcp part
export const setDhcpConfig = config => async (dispatch) => {
dispatch(setDhcpConfigRequest());
try {
if (config.interface_name) {
dispatch(findActiveDhcpRequest());
try {
const activeDhcp = await apiClient.findActiveDhcp(config.interface_name);
dispatch(findActiveDhcpSuccess(activeDhcp));
if (!activeDhcp.found) {
await apiClient.setDhcpConfig(config);
dispatch(addSuccessToast('dhcp_config_saved'));
dispatch(setDhcpConfigSuccess());
dispatch(getDhcpStatus());
} else {
dispatch(addErrorToast({ error: 'dhcp_found' }));
}
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(findActiveDhcpFailure());
}
} else {
await apiClient.setDhcpConfig(config);
dispatch(addSuccessToast('dhcp_config_saved'));
dispatch(setDhcpConfigSuccess());
dispatch(getDhcpStatus());
}
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(setDhcpConfigFailure());
}
};
export const toggleDhcpRequest = createAction('TOGGLE_DHCP_REQUEST');
export const toggleDhcpFailure = createAction('TOGGLE_DHCP_FAILURE');
export const toggleDhcpSuccess = createAction('TOGGLE_DHCP_SUCCESS');
// TODO rewrite findActiveDhcp part
export const toggleDhcp = config => async (dispatch) => {
dispatch(toggleDhcpRequest());
if (config.enabled) {
dispatch(addSuccessToast('disabled_dhcp'));
try {
await apiClient.setDhcpConfig({ ...config, enabled: false });
dispatch(toggleDhcpSuccess());
dispatch(getDhcpStatus());
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(toggleDhcpFailure());
}
} else {
dispatch(findActiveDhcpRequest());
try {
const activeDhcp = await apiClient.findActiveDhcp(config.interface_name);
dispatch(findActiveDhcpSuccess(activeDhcp));
if (!activeDhcp.found) {
try {
await apiClient.setDhcpConfig({ ...config, enabled: true });
dispatch(toggleDhcpSuccess());
dispatch(getDhcpStatus());
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(toggleDhcpFailure());
}
dispatch(addSuccessToast('enabled_dhcp'));
} else {
dispatch(addErrorToast({ error: 'dhcp_found' }));
}
} catch (error) {
dispatch(addErrorToast({ error }));
dispatch(findActiveDhcpFailure());
}
}
};

View File

@@ -302,4 +302,38 @@ export default class Api {
};
return this.makeRequest(path, method, parameters);
}
// DHCP
DHCP_STATUS = { path: 'dhcp/status', method: 'GET' };
DHCP_SET_CONFIG = { path: 'dhcp/set_config', method: 'POST' };
DHCP_FIND_ACTIVE = { path: 'dhcp/find_active_dhcp', method: 'POST' };
DHCP_INTERFACES = { path: 'dhcp/interfaces', method: 'GET' };
getDhcpStatus() {
const { path, method } = this.DHCP_STATUS;
return this.makeRequest(path, method);
}
getDhcpInterfaces() {
const { path, method } = this.DHCP_INTERFACES;
return this.makeRequest(path, method);
}
setDhcpConfig(config) {
const { path, method } = this.DHCP_SET_CONFIG;
const parameters = {
data: config,
headers: { 'Content-Type': 'application/json' },
};
return this.makeRequest(path, method, parameters);
}
findActiveDhcp(name) {
const { path, method } = this.DHCP_FIND_ACTIVE;
const parameters = {
data: name,
headers: { 'Content-Type': 'text/plain' },
};
return this.makeRequest(path, method, parameters);
}
}

View File

@@ -24,7 +24,9 @@ const Counters = props => (
</tr>
<tr>
<td>
<Trans>blocked_by</Trans> <a href="#filters"><Trans>filters</Trans></a>
<a href="#filters">
<Trans>blocked_by</Trans>
</a>
<Tooltip text={ props.t('number_of_dns_query_blocked_24_hours') } type={tooltipType} />
</td>
<td className="text-right">
@@ -90,7 +92,7 @@ Counters.propTypes = {
replacedSafesearch: PropTypes.number.isRequired,
avgProcessingTime: PropTypes.number.isRequired,
refreshButton: PropTypes.node.isRequired,
t: PropTypes.func,
t: PropTypes.func.isRequired,
};
export default withNamespaces()(Counters);

View File

@@ -49,7 +49,9 @@ class Statistics extends Component {
{getPercent(dnsQueries, blockedFiltering)}
</div>
<div className="card-title-stats">
<Trans>blocked_by</Trans><a href="#filters"> <Trans>filters</Trans></a>
<a href="#filters">
<Trans>blocked_by</Trans>
</a>
</div>
</div>
<div className="card-chart-bg">

View File

@@ -34,30 +34,30 @@ class Filters extends Component {
};
columns = [{
Header: this.props.t('enabled_table_header'),
Header: <Trans>enabled_table_header</Trans>,
accessor: 'enabled',
Cell: this.renderCheckbox,
width: 90,
className: 'text-center',
}, {
Header: this.props.t('name_table_header'),
Header: <Trans>name_table_header</Trans>,
accessor: 'name',
Cell: ({ value }) => (<div className="logs__row logs__row--overflow"><span className="logs__text" title={value}>{value}</span></div>),
}, {
Header: this.props.t('filter_url_table_header'),
Header: <Trans>filter_url_table_header</Trans>,
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: this.props.t('rules_count_table_header'),
Header: <Trans>rules_count_table_header</Trans>,
accessor: 'rulesCount',
className: 'text-center',
Cell: props => props.value.toLocaleString(),
}, {
Header: this.props.t('last_time_updated_table_header'),
Header: <Trans>last_time_updated_table_header</Trans>,
accessor: 'lastUpdated',
className: 'text-center',
}, {
Header: this.props.t('actions_table_header'),
Header: <Trans>actions_table_header</Trans>,
accessor: 'url',
Cell: ({ value }) => (<span title={ this.props.t('delete_table_action') } className='remove-icon fe fe-trash-2' onClick={() => this.props.removeFilter(value)}/>),
className: 'text-center',
@@ -82,7 +82,8 @@ class Filters extends Component {
<ReactTable
data={filters}
columns={this.columns}
showPagination={false}
showPagination={true}
defaultPageSize={10}
noDataText={ t('no_filters_added') }
minRows={4} // TODO find out what to show if rules.length is 0
/>

View File

@@ -131,7 +131,14 @@ class Logs extends Component {
} else {
const filterItem = Object.keys(filters)
.filter(key => filters[key].id === filterId);
filterName = filters[filterItem].name;
if (typeof filterItem !== 'undefined' && typeof filters[filterItem] !== 'undefined') {
filterName = filters[filterItem].name;
}
if (!filterName) {
filterName = t('unknown_filter', { filterId });
}
}
}
@@ -182,7 +189,7 @@ class Logs extends Component {
<option value="filtered">{ t('show_filtered_type') }</option>
</select>,
}, {
Header: t('Client'),
Header: t('client_table_header'),
accessor: 'client',
maxWidth: 250,
Cell: (row) => {

View File

@@ -0,0 +1,149 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import { Field, reduxForm } from 'redux-form';
import { withNamespaces, Trans } from 'react-i18next';
import flow from 'lodash/flow';
import { R_IPV4 } from '../../../helpers/constants';
const required = (value) => {
if (value || value === 0) {
return false;
}
return <Trans>form_error_required</Trans>;
};
const ipv4 = (value) => {
if (value && !new RegExp(R_IPV4).test(value)) {
return <Trans>form_error_ip_format</Trans>;
}
return false;
};
const isPositive = (value) => {
if ((value || value === 0) && (value <= 0)) {
return <Trans>form_error_positive</Trans>;
}
return false;
};
const toNumber = value => value && parseInt(value, 10);
const renderField = ({
input, className, placeholder, type, disabled, meta: { touched, error },
}) => (
<Fragment>
<input
{...input}
placeholder={placeholder}
type={type}
className={className}
disabled={disabled}
/>
{!disabled && touched && (error && <span className="form__message form__message--error">{error}</span>)}
</Fragment>
);
const Form = (props) => {
const {
t,
handleSubmit,
pristine,
submitting,
} = props;
return (
<form onSubmit={handleSubmit}>
<div className="row">
<div className="col-lg-6">
<div className="form__group form__group--dhcp">
<label>{t('dhcp_form_gateway_input')}</label>
<Field
name="gateway_ip"
component={renderField}
type="text"
className="form-control"
placeholder={t('dhcp_form_gateway_input')}
validate={[ipv4, required]}
/>
</div>
<div className="form__group form__group--dhcp">
<label>{t('dhcp_form_subnet_input')}</label>
<Field
name="subnet_mask"
component={renderField}
type="text"
className="form-control"
placeholder={t('dhcp_form_subnet_input')}
validate={[ipv4, required]}
/>
</div>
</div>
<div className="col-lg-6">
<div className="form__group form__group--dhcp">
<div className="row">
<div className="col-12">
<label>{t('dhcp_form_range_title')}</label>
</div>
<div className="col">
<Field
name="range_start"
component={renderField}
type="text"
className="form-control"
placeholder={t('dhcp_form_range_start')}
validate={[ipv4, required]}
/>
</div>
<div className="col">
<Field
name="range_end"
component={renderField}
type="text"
className="form-control"
placeholder={t('dhcp_form_range_end')}
validate={[ipv4, required]}
/>
</div>
</div>
</div>
<div className="form__group form__group--dhcp">
<label>{t('dhcp_form_lease_title')}</label>
<Field
name="lease_duration"
component={renderField}
type="number"
className="form-control"
placeholder={t('dhcp_form_lease_input')}
validate={[required, isPositive]}
normalize={toNumber}
/>
</div>
</div>
</div>
<button
type="submit"
className="btn btn-success btn-standart"
disabled={pristine || submitting}
>
{t('save_config')}
</button>
</form>
);
};
Form.propTypes = {
handleSubmit: PropTypes.func,
pristine: PropTypes.bool,
submitting: PropTypes.bool,
interfaces: PropTypes.object,
processing: PropTypes.bool,
initialValues: PropTypes.object,
t: PropTypes.func,
};
export default flow([
withNamespaces(),
reduxForm({ form: 'dhcpForm' }),
])(Form);

View File

@@ -0,0 +1,113 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { Field, reduxForm, formValueSelector } from 'redux-form';
import { withNamespaces, Trans } from 'react-i18next';
import flow from 'lodash/flow';
const renderInterfaces = (interfaces => (
Object.keys(interfaces).map((item) => {
const option = interfaces[item];
const { name } = option;
const onlyIPv6 = option.ip_addresses.every(ip => ip.includes(':'));
let interfaceIP = option.ip_addresses[0];
if (!onlyIPv6) {
option.ip_addresses.forEach((ip) => {
if (!ip.includes(':')) {
interfaceIP = ip;
}
});
}
return (
<option value={name} key={name} disabled={onlyIPv6}>
{name} - {interfaceIP}
</option>
);
})
));
const renderInterfaceValues = (interfaceValues => (
<ul className="list-unstyled mt-1 mb-0">
<li>
<span className="interface__title">MTU: </span>
{interfaceValues.mtu}
</li>
<li>
<span className="interface__title"><Trans>dhcp_hardware_address</Trans>: </span>
{interfaceValues.hardware_address}
</li>
<li>
<span className="interface__title"><Trans>dhcp_ip_addresses</Trans>: </span>
{
interfaceValues.ip_addresses
.map(ip => <span key={ip} className="interface__ip">{ip}</span>)
}
</li>
</ul>
));
let Interface = (props) => {
const {
t,
handleChange,
interfaces,
processing,
interfaceValue,
enabled,
} = props;
return (
<form>
{!processing && interfaces &&
<div className="row">
<div className="col-sm-12 col-md-6">
<div className="form__group form__group--dhcp">
<label>{t('dhcp_interface_select')}</label>
<Field
name="interface_name"
component="select"
className="form-control custom-select"
onChange={handleChange}
>
<option value="" disabled={enabled}>{t('dhcp_interface_select')}</option>
{renderInterfaces(interfaces)}
</Field>
</div>
</div>
{interfaceValue &&
<div className="col-sm-12 col-md-6">
{renderInterfaceValues(interfaces[interfaceValue])}
</div>
}
</div>
}
<hr/>
</form>
);
};
Interface.propTypes = {
handleChange: PropTypes.func,
interfaces: PropTypes.object,
processing: PropTypes.bool,
interfaceValue: PropTypes.string,
initialValues: PropTypes.object,
enabled: PropTypes.bool,
t: PropTypes.func,
};
const selector = formValueSelector('dhcpInterface');
Interface = connect((state) => {
const interfaceValue = selector(state, 'interface_name');
return {
interfaceValue,
};
})(Interface);
export default flow([
withNamespaces(),
reduxForm({ form: 'dhcpInterface' }),
])(Interface);

View File

@@ -0,0 +1,36 @@
import React from 'react';
import PropTypes from 'prop-types';
import ReactTable from 'react-table';
import { withNamespaces } from 'react-i18next';
const columns = [{
Header: 'MAC',
accessor: 'mac',
}, {
Header: 'IP',
accessor: 'ip',
}, {
Header: 'Hostname',
accessor: 'hostname',
}, {
Header: 'Expires',
accessor: 'expires',
}];
const Leases = props => (
<ReactTable
data={props.leases || []}
columns={columns}
showPagination={false}
noDataText={ props.t('dhcp_leases_not_found') }
minRows={6}
className="-striped -highlight card-table-overflow"
/>
);
Leases.propTypes = {
leases: PropTypes.array,
t: PropTypes.func,
};
export default withNamespaces()(Leases);

View File

@@ -0,0 +1,159 @@
import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { Trans, withNamespaces } from 'react-i18next';
import Form from './Form';
import Leases from './Leases';
import Interface from './Interface';
import Card from '../../ui/Card';
class Dhcp extends Component {
handleFormSubmit = (values) => {
this.props.setDhcpConfig(values);
};
handleFormChange = (value) => {
this.props.setDhcpConfig(value);
}
handleToggle = (config) => {
this.props.toggleDhcp(config);
this.props.findActiveDhcp(config.interface_name);
}
getToggleDhcpButton = () => {
const { config, active } = this.props.dhcp;
const activeDhcpFound = active && active.found;
const filledConfig = Object.keys(config).every((key) => {
if (key === 'enabled') {
return true;
}
return config[key];
});
if (config.enabled) {
return (
<button
type="button"
className="btn btn-standart mr-2 btn-gray"
onClick={() => this.props.toggleDhcp(config)}
>
<Trans>dhcp_disable</Trans>
</button>
);
}
return (
<button
type="button"
className="btn btn-standart mr-2 btn-success"
onClick={() => this.handleToggle(config)}
disabled={!filledConfig || activeDhcpFound}
>
<Trans>dhcp_enable</Trans>
</button>
);
}
getActiveDhcpMessage = () => {
const { active } = this.props.dhcp;
if (active) {
if (active.error) {
return (
<div className="text-danger">
{active.error}
</div>
);
}
return (
<Fragment>
{active.found ? (
<div className="text-danger">
<Trans>dhcp_found</Trans>
</div>
) : (
<div className="text-secondary">
<Trans>dhcp_not_found</Trans>
</div>
)}
</Fragment>
);
}
return '';
}
render() {
const { t, dhcp } = this.props;
const statusButtonClass = classnames({
'btn btn-primary btn-standart': true,
'btn btn-primary btn-standart btn-loading': dhcp.processingStatus,
});
return (
<Fragment>
<Card title={ t('dhcp_title') } subtitle={ t('dhcp_description') } bodyType="card-body box-body--settings">
<div className="dhcp">
{!dhcp.processing &&
<Fragment>
<Interface
onChange={this.handleFormChange}
initialValues={dhcp.config}
interfaces={dhcp.interfaces}
processing={dhcp.processingInterfaces}
enabled={dhcp.config.enabled}
/>
<Form
onSubmit={this.handleFormSubmit}
initialValues={dhcp.config}
interfaces={dhcp.interfaces}
processing={dhcp.processingInterfaces}
/>
<hr/>
<div className="card-actions mb-3">
{this.getToggleDhcpButton()}
<button
type="button"
className={statusButtonClass}
onClick={() =>
this.props.findActiveDhcp(dhcp.config.interface_name)
}
disabled={!dhcp.config.interface_name}
>
<Trans>check_dhcp_servers</Trans>
</button>
</div>
{this.getActiveDhcpMessage()}
</Fragment>
}
</div>
</Card>
{!dhcp.processing && dhcp.config.enabled &&
<Card title={ t('dhcp_leases') } bodyType="card-body box-body--settings">
<div className="row">
<div className="col">
<Leases leases={dhcp.leases} />
</div>
</div>
</Card>
}
</Fragment>
);
}
}
Dhcp.propTypes = {
dhcp: PropTypes.object,
toggleDhcp: PropTypes.func,
getDhcpStatus: PropTypes.func,
setDhcpConfig: PropTypes.func,
findActiveDhcp: PropTypes.func,
handleSubmit: PropTypes.func,
t: PropTypes.func,
};
export default withNamespaces()(Dhcp);

View File

@@ -1,4 +1,5 @@
.form__group {
position: relative;
margin-bottom: 15px;
}
@@ -6,6 +7,10 @@
margin-bottom: 0;
}
.form__group--dhcp:last-child {
margin-bottom: 15px;
}
.btn-standart {
padding-left: 20px;
padding-right: 20px;
@@ -18,3 +23,28 @@
.form-control--textarea-large {
min-height: 240px;
}
.form__message {
font-size: 11px;
}
.form__message--error {
color: #cd201f;
}
.interface__title {
font-size: 13px;
font-weight: 700;
}
.interface__ip:after {
content: ", ";
}
.interface__ip:last-child:after {
content: "";
}
.dhcp {
min-height: 450px;
}

View File

@@ -73,6 +73,9 @@ class Upstream extends Component {
<li>
<code>tcp://1.1.1.1</code> - { t('example_upstream_tcp') }
</li>
<li>
<code>sdns://...</code> - <span dangerouslySetInnerHTML={{ __html: t('example_upstream_sdns') }} />
</li>
</ol>
</div>
</div>

View File

@@ -2,6 +2,7 @@ import React, { Component, Fragment } from 'react';
import PropTypes from 'prop-types';
import { withNamespaces, Trans } from 'react-i18next';
import Upstream from './Upstream';
import Dhcp from './Dhcp';
import Checkbox from '../ui/Checkbox';
import Loading from '../ui/Loading';
import PageTitle from '../ui/PageTitle';
@@ -34,6 +35,8 @@ class Settings extends Component {
componentDidMount() {
this.props.initSettings(this.settings);
this.props.getDhcpStatus();
this.props.getDhcpInterfaces();
}
handleUpstreamChange = (value) => {
@@ -92,6 +95,13 @@ class Settings extends Component {
handleUpstreamSubmit={this.handleUpstreamSubmit}
handleUpstreamTest={this.handleUpstreamTest}
/>
<Dhcp
dhcp={this.props.dhcp}
toggleDhcp={this.props.toggleDhcp}
getDhcpStatus={this.props.getDhcpStatus}
findActiveDhcp={this.props.findActiveDhcp}
setDhcpConfig={this.props.setDhcpConfig}
/>
</div>
</div>
</div>

View File

@@ -1,10 +1,22 @@
import { connect } from 'react-redux';
import { initSettings, toggleSetting, handleUpstreamChange, setUpstream, testUpstream, addErrorToast } from '../actions';
import {
initSettings,
toggleSetting,
handleUpstreamChange,
setUpstream,
testUpstream,
addErrorToast,
toggleDhcp,
getDhcpStatus,
getDhcpInterfaces,
setDhcpConfig,
findActiveDhcp,
} from '../actions';
import Settings from '../components/Settings';
const mapStateToProps = (state) => {
const { settings, dashboard } = state;
const props = { settings, dashboard };
const { settings, dashboard, dhcp } = state;
const props = { settings, dashboard, dhcp };
return props;
};
@@ -15,6 +27,11 @@ const mapDispatchToProps = {
setUpstream,
testUpstream,
addErrorToast,
toggleDhcp,
getDhcpStatus,
getDhcpInterfaces,
setDhcpConfig,
findActiveDhcp,
};
export default connect(

View File

@@ -1,4 +1,5 @@
export const R_URL_REQUIRES_PROTOCOL = /^https?:\/\/\w[\w_\-.]*\.[a-z]{2,8}[^\s]*$/;
export const R_IPV4 = /^(?:(?:^|\.)(?:2(?:5[0-5]|[0-4]\d)|1?\d?\d)){4}$/g;
export const STATS_NAMES = {
avg_processing_time: 'average_processing_time',
@@ -54,4 +55,8 @@ export const LANGUAGES = [
key: 'ja',
name: '日本語',
},
{
key: 'zh-tw',
name: '正體中文',
},
];

View File

@@ -11,6 +11,7 @@ import fr from './__locales/fr.json';
import ja from './__locales/ja.json';
import sv from './__locales/sv.json';
import ptBR from './__locales/pt-br.json';
import zhTW from './__locales/zh-tw.json';
const resources = {
en: {
@@ -37,6 +38,9 @@ const resources = {
'pt-BR': {
translation: ptBR,
},
'zh-TW': {
translation: zhTW,
},
};
i18n

View File

@@ -2,6 +2,7 @@ import { combineReducers } from 'redux';
import { handleActions } from 'redux-actions';
import { loadingBarReducer } from 'react-redux-loading-bar';
import nanoid from 'nanoid';
import { reducer as formReducer } from 'redux-form';
import versionCompare from '../helpers/versionCompare';
import * as actions from '../actions';
@@ -35,6 +36,7 @@ const settings = handleActions({
processing: true,
processingTestUpstream: false,
processingSetUpstream: false,
processingDhcpStatus: false,
});
const dashboard = handleActions({
@@ -258,11 +260,61 @@ const toasts = handleActions({
},
}, { notices: [] });
const dhcp = handleActions({
[actions.getDhcpStatusRequest]: state => ({ ...state, processing: true }),
[actions.getDhcpStatusFailure]: state => ({ ...state, processing: false }),
[actions.getDhcpStatusSuccess]: (state, { payload }) => {
const newState = {
...state,
...payload,
processing: false,
};
return newState;
},
[actions.getDhcpInterfacesRequest]: state => ({ ...state, processingInterfaces: true }),
[actions.getDhcpInterfacesFailure]: state => ({ ...state, processingInterfaces: false }),
[actions.getDhcpInterfacesSuccess]: (state, { payload }) => {
const newState = {
...state,
interfaces: payload,
processingInterfaces: false,
};
return newState;
},
[actions.findActiveDhcpRequest]: state => ({ ...state, processingStatus: true }),
[actions.findActiveDhcpFailure]: state => ({ ...state, processingStatus: false }),
[actions.findActiveDhcpSuccess]: (state, { payload }) => ({
...state,
active: payload,
processingStatus: false,
}),
[actions.toggleDhcpSuccess]: (state) => {
const { config } = state;
const newConfig = { ...config, enabled: !config.enabled };
const newState = { ...state, config: newConfig };
return newState;
},
}, {
processing: true,
processingStatus: false,
processingInterfaces: false,
config: {
enabled: false,
},
active: null,
leases: [],
});
export default combineReducers({
settings,
dashboard,
queryLogs,
filtering,
toasts,
dhcp,
loadingBar: loadingBarReducer,
form: formReducer,
});

294
config.go
View File

@@ -1,90 +1,51 @@
package main
import (
"bytes"
"io/ioutil"
"log"
"os"
"path/filepath"
"regexp"
"sync"
"text/template"
"time"
"github.com/AdguardTeam/AdGuardHome/dhcpd"
"github.com/AdguardTeam/AdGuardHome/dnsfilter"
"github.com/AdguardTeam/AdGuardHome/dnsforward"
"github.com/hmage/golibs/log"
"gopkg.in/yaml.v2"
)
// Current schema version. We compare it with the value from
// the configuration file and perform necessary upgrade operations if needed
const SchemaVersion = 1
// Directory where we'll store all downloaded filters contents
const FiltersDir = "filters"
// User filter ID is always 0
const UserFilterId = 0
// Just a counter that we use for incrementing the filter ID
var NextFilterId = time.Now().Unix()
const (
dataDir = "data" // data storage
filterDir = "filters" // cache location for downloaded filters, it's under DataDir
)
// configuration is loaded from YAML
// field ordering is important -- yaml fields will mirror ordering from here
type configuration struct {
// Config filename (can be overriden via the command line arguments)
ourConfigFilename string
// Basically, this is our working directory
ourBinaryDir string
// Directory to store data (i.e. filters contents)
ourDataDir string
ourConfigFilename string // Config filename (can be overriden via the command line arguments)
ourBinaryDir string // Location of our directory, used to protect against CWD being somewhere else
// Schema version of the config file. This value is used when performing the app updates.
SchemaVersion int `yaml:"schema_version"`
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"`
Language string `yaml:"language"` // two-letter ISO 639-1 language code
BindHost string `yaml:"bind_host"`
BindPort int `yaml:"bind_port"`
AuthName string `yaml:"auth_name"`
AuthPass string `yaml:"auth_pass"`
Language string `yaml:"language"` // two-letter ISO 639-1 language code
DNS dnsConfig `yaml:"dns"`
Filters []filter `yaml:"filters"`
UserRules []string `yaml:"user_rules"`
DHCP dhcpd.ServerConfig `yaml:"dhcp"`
sync.RWMutex `yaml:"-"`
SchemaVersion int `yaml:"schema_version"` // keeping last so that users will be less tempted to change it -- used when upgrading between versions
}
type coreDnsFilter struct {
ID int64 `yaml:"-"`
Path string `yaml:"-"`
}
// field ordering is important -- yaml fields will mirror ordering from here
type dnsConfig struct {
Port int `yaml:"port"`
type coreDNSConfig struct {
binaryFile string
coreFile string
Filters []coreDnsFilter `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"`
Ratelimit int `yaml:"ratelimit"`
RefuseAny bool `yaml:"refuse_any"`
Pprof string `yaml:"-"`
Cache string `yaml:"-"`
Prometheus string `yaml:"-"`
BootstrapDNS string `yaml:"bootstrap_dns"`
UpstreamDNS []string `yaml:"upstream_dns"`
}
dnsforward.FilteringConfig `yaml:",inline"`
type filter struct {
ID int64 `json:"id" yaml:"id"` // auto-assigned when filter is added (see NextFilterId)
URL string `json:"url"`
Name string `json:"name" yaml:"name"`
Enabled bool `json:"enabled"`
RulesCount int `json:"rulesCount" yaml:"-"`
contents []byte
LastUpdated time.Time `json:"lastUpdated" yaml:"last_updated"`
UpstreamDNS []string `yaml:"upstream_dns"`
}
var defaultDNS = []string{"tls://1.1.1.1", "tls://1.0.0.1"}
@@ -92,51 +53,28 @@ 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: "AdGuardHome.yaml",
ourDataDir: "data",
BindPort: 3000,
BindHost: "127.0.0.1",
CoreDNS: coreDNSConfig{
Port: 53,
binaryFile: "coredns", // only filename, no path
coreFile: "Corefile", // only filename, no path
ProtectionEnabled: true,
FilteringEnabled: true,
SafeBrowsingEnabled: false,
BlockedResponseTTL: 10, // in seconds
QueryLogEnabled: true,
Ratelimit: 20,
RefuseAny: true,
BootstrapDNS: "8.8.8.8:53",
UpstreamDNS: defaultDNS,
Cache: "cache",
Prometheus: "prometheus :9153",
DNS: dnsConfig{
Port: 53,
FilteringConfig: dnsforward.FilteringConfig{
ProtectionEnabled: true, // whether or not use any of dnsfilter features
FilteringEnabled: true, // whether or not use filter lists
BlockedResponseTTL: 10, // in seconds
QueryLogEnabled: true,
Ratelimit: 20,
RefuseAny: true,
BootstrapDNS: "8.8.8.8:53",
},
UpstreamDNS: defaultDNS,
},
Filters: []filter{
{ID: 1, Enabled: true, URL: "https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt", Name: "AdGuard Simplified Domain Names filter"},
{ID: 2, Enabled: false, URL: "https://adaway.org/hosts.txt", Name: "AdAway"},
{ID: 3, Enabled: false, URL: "https://hosts-file.net/ad_servers.txt", Name: "hpHosts - Ad and Tracking servers only"},
{ID: 4, Enabled: false, URL: "http://www.malwaredomainlist.com/hostslist/hosts.txt", Name: "MalwareDomainList.com Hosts List"},
{Filter: dnsfilter.Filter{ID: 1}, Enabled: true, URL: "https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt", Name: "AdGuard Simplified Domain Names filter"},
{Filter: dnsfilter.Filter{ID: 2}, Enabled: false, URL: "https://adaway.org/hosts.txt", Name: "AdAway"},
{Filter: dnsfilter.Filter{ID: 3}, Enabled: false, URL: "https://hosts-file.net/ad_servers.txt", Name: "hpHosts - Ad and Tracking servers only"},
{Filter: dnsfilter.Filter{ID: 4}, Enabled: false, URL: "http://www.malwaredomainlist.com/hostslist/hosts.txt", Name: "MalwareDomainList.com Hosts List"},
},
}
// Creates a helper object for working with the user rules
func getUserFilter() filter {
// TODO: This should be calculated when UserRules are set
var contents []byte
for _, rule := range config.UserRules {
contents = append(contents, []byte(rule)...)
contents = append(contents, '\n')
}
userFilter := filter{
// User filter always has constant ID=0
ID: UserFilterId,
contents: contents,
Enabled: true,
}
return userFilter
SchemaVersion: currentSchemaVersion,
}
// Loads configuration from the YAML file
@@ -160,33 +98,17 @@ func parseConfig() error {
}
// Deduplicate filters
{
i := 0 // output index, used for deletion later
urls := map[string]bool{}
for _, filter := range config.Filters {
if _, ok := urls[filter.URL]; !ok {
// we didn't see it before, keep it
urls[filter.URL] = true // remember the URL
config.Filters[i] = filter
i++
}
}
// all entries we want to keep are at front, delete the rest
config.Filters = config.Filters[:i]
}
deduplicateFilters()
// Set the next filter ID to max(filter.ID) + 1
for i := range config.Filters {
if NextFilterId < config.Filters[i].ID {
NextFilterId = config.Filters[i].ID + 1
}
}
updateUniqueFilterID(config.Filters)
return nil
}
// Saves configuration to the YAML file and also saves the user filter contents to a file
func writeConfig() error {
func (c *configuration) write() error {
c.Lock()
defer c.Unlock()
configFile := filepath.Join(config.ourBinaryDir, config.ourConfigFilename)
log.Printf("Writing YAML file: %s", configFile)
yamlText, err := yaml.Marshal(&config)
@@ -194,13 +116,23 @@ func writeConfig() error {
log.Printf("Couldn't generate YAML file: %s", err)
return err
}
err = writeFileSafe(configFile, yamlText)
err = safeWriteFile(configFile, yamlText)
if err != nil {
log.Printf("Couldn't save YAML config: %s", err)
return err
}
userFilter := getUserFilter()
return nil
}
func writeAllConfigs() error {
err := config.write()
if err != nil {
log.Printf("Couldn't write config: %s", err)
return err
}
userFilter := userFilter()
err = userFilter.save()
if err != nil {
log.Printf("Couldn't save the user filter: %s", err)
@@ -209,107 +141,3 @@ func writeConfig() error {
return nil
}
// --------------
// coredns config
// --------------
func writeCoreDNSConfig() error {
coreFile := filepath.Join(config.ourBinaryDir, config.CoreDNS.coreFile)
log.Printf("Writing DNS config: %s", coreFile)
configText, err := generateCoreDNSConfigText()
if err != nil {
log.Printf("Couldn't generate DNS config: %s", err)
return err
}
err = writeFileSafe(coreFile, []byte(configText))
if err != nil {
log.Printf("Couldn't save DNS config: %s", err)
return err
}
return nil
}
func writeAllConfigs() error {
err := writeConfig()
if err != nil {
log.Printf("Couldn't write our config: %s", err)
return err
}
err = writeCoreDNSConfig()
if err != nil {
log.Printf("Couldn't write DNS config: %s", err)
return err
}
return nil
}
const coreDNSConfigTemplate = `.:{{.Port}} {
{{if .ProtectionEnabled}}dnsfilter {
{{if .SafeBrowsingEnabled}}safebrowsing{{end}}
{{if .ParentalEnabled}}parental {{.ParentalSensitivity}}{{end}}
{{if .SafeSearchEnabled}}safesearch{{end}}
{{if .QueryLogEnabled}}querylog{{end}}
blocked_ttl {{.BlockedResponseTTL}}
{{if .FilteringEnabled}}
{{range .Filters}}
filter {{.ID}} "{{.Path}}"
{{end}}
{{end}}
}{{end}}
{{.Pprof}}
{{if .RefuseAny}}refuseany{{end}}
{{if gt .Ratelimit 0}}ratelimit {{.Ratelimit}}{{end}}
hosts {
fallthrough
}
{{if .UpstreamDNS}}upstream {{range .UpstreamDNS}}{{.}} {{end}} { bootstrap {{.BootstrapDNS}} }{{end}}
{{.Cache}}
{{.Prometheus}}
}
`
var removeEmptyLines = regexp.MustCompile("([\t ]*\n)+")
// generate CoreDNS config text
func generateCoreDNSConfigText() (string, error) {
t, err := template.New("config").Parse(coreDNSConfigTemplate)
if err != nil {
log.Printf("Couldn't generate DNS config: %s", err)
return "", err
}
var configBytes bytes.Buffer
temporaryConfig := config.CoreDNS
// fill the list of filters
filters := make([]coreDnsFilter, 0)
// first of all, append the user filter
userFilter := getUserFilter()
if len(userFilter.contents) > 0 {
filters = append(filters, coreDnsFilter{ID: userFilter.ID, Path: userFilter.getFilterFilePath()})
}
// then go through other filters
for i := range config.Filters {
filter := &config.Filters[i]
if filter.Enabled && len(filter.contents) > 0 {
filters = append(filters, coreDnsFilter{ID: filter.ID, Path: filter.getFilterFilePath()})
}
}
temporaryConfig.Filters = filters
// run the template
err = t.Execute(&configBytes, &temporaryConfig)
if err != nil {
log.Printf("Couldn't generate DNS config: %s", err)
return "", err
}
configText := configBytes.String()
// remove empty lines from generated config
configText = removeEmptyLines.ReplaceAllString(configText, "\n")
return configText, nil
}

View File

@@ -1,29 +1,25 @@
package main
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net"
"net/http"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
"github.com/AdguardTeam/AdGuardHome/upstream"
corednsplugin "github.com/AdguardTeam/AdGuardHome/coredns_plugin"
"github.com/AdguardTeam/AdGuardHome/dnsforward"
"github.com/AdguardTeam/dnsproxy/upstream"
"github.com/hmage/golibs/log"
"github.com/miekg/dns"
"gopkg.in/asaskevich/govalidator.v4"
)
const updatePeriod = time.Minute * 30
var filterTitleRegexp = regexp.MustCompile(`^! Title: +(.*)$`)
// cached version.json to avoid hammering github.io for each page reload
var versionCheckJSON []byte
var versionCheckLastTime time.Time
@@ -36,24 +32,20 @@ var client = &http.Client{
}
// -------------------
// coredns run control
// dns run control
// -------------------
func tellCoreDNSToReload() {
corednsplugin.Reload <- true
}
func writeAllConfigsAndReloadCoreDNS() error {
func writeAllConfigsAndReloadDNS() error {
err := writeAllConfigs()
if err != nil {
log.Printf("Couldn't write all configs: %s", err)
return err
}
tellCoreDNSToReload()
reconfigureDNSServer()
return nil
}
func httpUpdateConfigReloadDNSReturnOK(w http.ResponseWriter, r *http.Request) {
err := writeAllConfigsAndReloadCoreDNS()
err := writeAllConfigsAndReloadDNS()
if err != nil {
errortext := fmt.Sprintf("Couldn't write config file: %s", err)
log.Println(errortext)
@@ -63,7 +55,6 @@ func httpUpdateConfigReloadDNSReturnOK(w http.ResponseWriter, r *http.Request) {
returnOK(w, r)
}
//noinspection GoUnusedParameter
func returnOK(w http.ResponseWriter, r *http.Request) {
_, err := fmt.Fprintf(w, "OK\n")
if err != nil {
@@ -73,16 +64,15 @@ func returnOK(w http.ResponseWriter, r *http.Request) {
}
}
//noinspection GoUnusedParameter
func handleStatus(w http.ResponseWriter, r *http.Request) {
data := map[string]interface{}{
"dns_address": config.BindHost,
"dns_port": config.CoreDNS.Port,
"protection_enabled": config.CoreDNS.ProtectionEnabled,
"querylog_enabled": config.CoreDNS.QueryLogEnabled,
"dns_port": config.DNS.Port,
"protection_enabled": config.DNS.ProtectionEnabled,
"querylog_enabled": config.DNS.QueryLogEnabled,
"running": isRunning(),
"bootstrap_dns": config.CoreDNS.BootstrapDNS,
"upstream_dns": config.CoreDNS.UpstreamDNS,
"bootstrap_dns": config.DNS.BootstrapDNS,
"upstream_dns": config.DNS.UpstreamDNS,
"version": VersionString,
"language": config.Language,
}
@@ -105,12 +95,12 @@ func handleStatus(w http.ResponseWriter, r *http.Request) {
}
func handleProtectionEnable(w http.ResponseWriter, r *http.Request) {
config.CoreDNS.ProtectionEnabled = true
config.DNS.ProtectionEnabled = true
httpUpdateConfigReloadDNSReturnOK(w, r)
}
func handleProtectionDisable(w http.ResponseWriter, r *http.Request) {
config.CoreDNS.ProtectionEnabled = false
config.DNS.ProtectionEnabled = false
httpUpdateConfigReloadDNSReturnOK(w, r)
}
@@ -118,12 +108,12 @@ func handleProtectionDisable(w http.ResponseWriter, r *http.Request) {
// stats
// -----
func handleQueryLogEnable(w http.ResponseWriter, r *http.Request) {
config.CoreDNS.QueryLogEnabled = true
config.DNS.QueryLogEnabled = true
httpUpdateConfigReloadDNSReturnOK(w, r)
}
func handleQueryLogDisable(w http.ResponseWriter, r *http.Request) {
config.CoreDNS.QueryLogEnabled = false
config.DNS.QueryLogEnabled = false
httpUpdateConfigReloadDNSReturnOK(w, r)
}
@@ -145,9 +135,9 @@ func handleSetUpstreamDNS(w http.ResponseWriter, r *http.Request) {
hosts := strings.Fields(string(body))
if len(hosts) == 0 {
config.CoreDNS.UpstreamDNS = defaultDNS
config.DNS.UpstreamDNS = defaultDNS
} else {
config.CoreDNS.UpstreamDNS = hosts
config.DNS.UpstreamDNS = hosts
}
err = writeAllConfigs()
@@ -157,7 +147,7 @@ func handleSetUpstreamDNS(w http.ResponseWriter, r *http.Request) {
http.Error(w, errorText, http.StatusInternalServerError)
return
}
tellCoreDNSToReload()
reconfigureDNSServer()
_, err = fmt.Fprintf(w, "OK %d servers\n", len(hosts))
if err != nil {
errorText := fmt.Sprintf("Couldn't write body: %s", err)
@@ -213,28 +203,35 @@ func handleTestUpstreamDNS(w http.ResponseWriter, r *http.Request) {
}
func checkDNS(input string) error {
u, err := upstream.NewUpstream(input, config.CoreDNS.BootstrapDNS)
log.Printf("Checking if DNS %s works...", input)
u, err := upstream.AddressToUpstream(input, "", dnsforward.DefaultTimeout)
if err != nil {
return err
return fmt.Errorf("Failed to choose upstream for %s: %s", input, err)
}
defer u.Close()
alive, err := upstream.IsAlive(u)
req := dns.Msg{}
req.Id = dns.Id()
req.RecursionDesired = true
req.Question = []dns.Question{
{Name: "google-public-dns-a.google.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET},
}
reply, err := u.Exchange(&req)
if err != nil {
return fmt.Errorf("couldn't communicate with DNS server %s: %s", input, err)
}
if !alive {
return fmt.Errorf("DNS server has not passed the healthcheck: %s", input)
if len(reply.Answer) != 1 {
return fmt.Errorf("DNS server %s returned wrong answer", input)
}
if t, ok := reply.Answer[0].(*dns.A); ok {
if !net.IPv4(8, 8, 8, 8).Equal(t.A) {
return fmt.Errorf("DNS server %s returned wrong answer: %v", input, t.A)
}
}
log.Printf("DNS %s works OK", input)
return nil
}
//noinspection GoUnusedParameter
func handleGetVersionJSON(w http.ResponseWriter, r *http.Request) {
now := time.Now()
if now.Sub(versionCheckLastTime) <= versionCheckPeriod && len(versionCheckJSON) != 0 {
@@ -246,7 +243,7 @@ func handleGetVersionJSON(w http.ResponseWriter, r *http.Request) {
resp, err := client.Get(versionCheckURL)
if err != nil {
errortext := fmt.Sprintf("Couldn't get querylog from coredns: %T %s\n", err, err)
errortext := fmt.Sprintf("Couldn't get version check json from %s: %T %s\n", versionCheckURL, err, err)
log.Println(errortext)
http.Error(w, errortext, http.StatusBadGateway)
return
@@ -258,7 +255,7 @@ func handleGetVersionJSON(w http.ResponseWriter, r *http.Request) {
// read the body entirely
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
errortext := fmt.Sprintf("Couldn't read response body: %s", err)
errortext := fmt.Sprintf("Couldn't read response body from %s: %s", versionCheckURL, err)
log.Println(errortext)
http.Error(w, errortext, http.StatusBadGateway)
return
@@ -281,19 +278,18 @@ func handleGetVersionJSON(w http.ResponseWriter, r *http.Request) {
// ---------
func handleFilteringEnable(w http.ResponseWriter, r *http.Request) {
config.CoreDNS.FilteringEnabled = true
config.DNS.FilteringEnabled = true
httpUpdateConfigReloadDNSReturnOK(w, r)
}
func handleFilteringDisable(w http.ResponseWriter, r *http.Request) {
config.CoreDNS.FilteringEnabled = false
config.DNS.FilteringEnabled = false
httpUpdateConfigReloadDNSReturnOK(w, r)
}
//noinspection GoUnusedParameter
func handleFilteringStatus(w http.ResponseWriter, r *http.Request) {
data := map[string]interface{}{
"enabled": config.CoreDNS.FilteringEnabled,
"enabled": config.DNS.FilteringEnabled,
}
config.RLock()
@@ -320,7 +316,6 @@ func handleFilteringStatus(w http.ResponseWriter, r *http.Request) {
}
func handleFilteringAddURL(w http.ResponseWriter, r *http.Request) {
filter := filter{}
err := json.NewDecoder(r.Body).Decode(&filter)
if err != nil {
@@ -349,9 +344,8 @@ func handleFilteringAddURL(w http.ResponseWriter, r *http.Request) {
}
// Set necessary properties
filter.ID = NextFilterId
filter.ID = assignUniqueFilterID()
filter.Enabled = true
NextFilterId++
// Download the filter contents
ok, err := filter.update(true)
@@ -383,7 +377,8 @@ func handleFilteringAddURL(w http.ResponseWriter, r *http.Request) {
return
}
// URL is deemed valid, append it to filters, update config, write new filter file and tell coredns to reload it
// URL is deemed valid, append it to filters, update config, write new filter file and tell dns to reload it
// TODO: since we directly feed filters in-memory, revisit if writing configs is always neccessary
config.Filters = append(config.Filters, filter)
err = writeAllConfigs()
if err != nil {
@@ -393,7 +388,7 @@ func handleFilteringAddURL(w http.ResponseWriter, r *http.Request) {
return
}
tellCoreDNSToReload()
reconfigureDNSServer()
_, err = fmt.Fprintf(w, "OK %d rules\n", filter.RulesCount)
if err != nil {
@@ -430,8 +425,8 @@ func handleFilteringRemoveURL(w http.ResponseWriter, r *http.Request) {
newFilters = append(newFilters, filter)
} else {
// Remove the filter file
err := os.Remove(filter.getFilterFilePath())
if err != nil {
err := os.Remove(filter.Path())
if err != nil && !os.IsNotExist(err) {
errorText := fmt.Sprintf("Couldn't remove the filter file: %s", err)
http.Error(w, errorText, http.StatusInternalServerError)
return
@@ -478,7 +473,7 @@ func handleFilteringEnableURL(w http.ResponseWriter, r *http.Request) {
}
// kick off refresh of rules from new URLs
checkFiltersUpdates(false)
refreshFiltersIfNeccessary(false)
httpUpdateConfigReloadDNSReturnOK(w, r)
}
@@ -534,205 +529,27 @@ func handleFilteringSetRules(w http.ResponseWriter, r *http.Request) {
func handleFilteringRefresh(w http.ResponseWriter, r *http.Request) {
force := r.URL.Query().Get("force")
updated := checkFiltersUpdates(force != "")
updated := refreshFiltersIfNeccessary(force != "")
fmt.Fprintf(w, "OK %d filters updated\n", updated)
}
// Sets up a timer that will be checking for filters updates periodically
func runFiltersUpdatesTimer() {
go func() {
for range time.Tick(time.Minute) {
checkFiltersUpdates(false)
}
}()
}
// Checks filters updates if necessary
// If force is true, it ignores the filter.LastUpdated field value
func checkFiltersUpdates(force bool) int {
config.Lock()
// fetch URLs
updateCount := 0
for i := range config.Filters {
filter := &config.Filters[i] // otherwise we will be operating on a copy
updated, err := filter.update(force)
if err != nil {
log.Printf("Failed to update filter %s: %s\n", filter.URL, err)
continue
}
if updated {
// Saving it to the filters dir now
err = filter.save()
if err != nil {
log.Printf("Failed to save the updated filter %d: %s", filter.ID, err)
continue
}
updateCount++
}
}
config.Unlock()
if updateCount > 0 {
tellCoreDNSToReload()
}
return updateCount
}
// A helper function that parses filter contents and returns a number of rules and a filter name (if there's any)
func parseFilterContents(contents []byte) (int, string) {
lines := strings.Split(string(contents), "\n")
rulesCount := 0
name := ""
seenTitle := false
// Count lines in the filter
for _, line := range lines {
line = strings.TrimSpace(line)
if len(line) > 0 && line[0] == '!' {
if m := filterTitleRegexp.FindAllStringSubmatch(line, -1); len(m) > 0 && len(m[0]) >= 2 && !seenTitle {
name = m[0][1]
seenTitle = true
}
} else if len(line) != 0 {
rulesCount++
}
}
return rulesCount, name
}
// Checks for filters updates
// If "force" is true -- does not check the filter's LastUpdated field
// Call "save" to persist the filter contents
func (filter *filter) update(force bool) (bool, error) {
if !filter.Enabled {
return false, nil
}
if !force && time.Since(filter.LastUpdated) <= updatePeriod {
return false, nil
}
log.Printf("Downloading update for filter %d from %s", filter.ID, filter.URL)
// use the same update period for failed filter downloads to avoid flooding with requests
filter.LastUpdated = time.Now()
resp, err := client.Get(filter.URL)
if resp != nil && resp.Body != nil {
defer resp.Body.Close()
}
if err != nil {
log.Printf("Couldn't request filter from URL %s, skipping: %s", filter.URL, err)
return false, err
}
if resp.StatusCode != 200 {
log.Printf("Got status code %d from URL %s, skipping", resp.StatusCode, filter.URL)
return false, fmt.Errorf("got status code != 200: %d", resp.StatusCode)
}
contentType := strings.ToLower(resp.Header.Get("content-type"))
if !strings.HasPrefix(contentType, "text/plain") {
log.Printf("Non-text response %s from %s, skipping", contentType, filter.URL)
return false, fmt.Errorf("non-text response %s", contentType)
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Printf("Couldn't fetch filter contents from URL %s, skipping: %s", filter.URL, err)
return false, err
}
// Extract filter name and count number of rules
rulesCount, filterName := parseFilterContents(body)
if filterName != "" {
filter.Name = filterName
}
// Check if the filter has been really changed
if bytes.Equal(filter.contents, body) {
log.Printf("The filter %d text has not changed", filter.ID)
return false, nil
}
log.Printf("Filter %d has been updated: %d bytes, %d rules", filter.ID, len(body), rulesCount)
filter.RulesCount = rulesCount
filter.contents = body
return true, nil
}
// saves filter contents to the file in config.ourDataDir
func (filter *filter) save() error {
filterFilePath := filter.getFilterFilePath()
log.Printf("Saving filter %d contents to: %s", filter.ID, filterFilePath)
err := writeFileSafe(filterFilePath, filter.contents)
if err != nil {
return err
}
return nil
}
// loads filter contents from the file in config.ourDataDir
func (filter *filter) load() error {
if !filter.Enabled {
// No need to load a filter that is not enabled
return nil
}
filterFilePath := filter.getFilterFilePath()
log.Printf("Loading filter %d contents to: %s", filter.ID, filterFilePath)
if _, err := os.Stat(filterFilePath); os.IsNotExist(err) {
// do nothing, file doesn't exist
return err
}
filterFileContents, err := ioutil.ReadFile(filterFilePath)
if err != nil {
return err
}
log.Printf("Filter %d length is %d", filter.ID, len(filterFileContents))
filter.contents = filterFileContents
// Now extract the rules count
rulesCount, _ := parseFilterContents(filter.contents)
filter.RulesCount = rulesCount
return nil
}
// Path to the filter contents
func (filter *filter) getFilterFilePath() string {
return filepath.Join(config.ourBinaryDir, config.ourDataDir, FiltersDir, strconv.FormatInt(filter.ID, 10)+".txt")
}
// ------------
// safebrowsing
// ------------
func handleSafeBrowsingEnable(w http.ResponseWriter, r *http.Request) {
config.CoreDNS.SafeBrowsingEnabled = true
config.DNS.SafeBrowsingEnabled = true
httpUpdateConfigReloadDNSReturnOK(w, r)
}
func handleSafeBrowsingDisable(w http.ResponseWriter, r *http.Request) {
config.CoreDNS.SafeBrowsingEnabled = false
config.DNS.SafeBrowsingEnabled = false
httpUpdateConfigReloadDNSReturnOK(w, r)
}
//noinspection GoUnusedParameter
func handleSafeBrowsingStatus(w http.ResponseWriter, r *http.Request) {
data := map[string]interface{}{
"enabled": config.CoreDNS.SafeBrowsingEnabled,
"enabled": config.DNS.SafeBrowsingEnabled,
}
jsonVal, err := json.Marshal(data)
if err != nil {
@@ -795,23 +612,22 @@ func handleParentalEnable(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Sensitivity must be set to valid value", 400)
return
}
config.CoreDNS.ParentalSensitivity = i
config.CoreDNS.ParentalEnabled = true
config.DNS.ParentalSensitivity = i
config.DNS.ParentalEnabled = true
httpUpdateConfigReloadDNSReturnOK(w, r)
}
func handleParentalDisable(w http.ResponseWriter, r *http.Request) {
config.CoreDNS.ParentalEnabled = false
config.DNS.ParentalEnabled = false
httpUpdateConfigReloadDNSReturnOK(w, r)
}
//noinspection GoUnusedParameter
func handleParentalStatus(w http.ResponseWriter, r *http.Request) {
data := map[string]interface{}{
"enabled": config.CoreDNS.ParentalEnabled,
"enabled": config.DNS.ParentalEnabled,
}
if config.CoreDNS.ParentalEnabled {
data["sensitivity"] = config.CoreDNS.ParentalSensitivity
if config.DNS.ParentalEnabled {
data["sensitivity"] = config.DNS.ParentalSensitivity
}
jsonVal, err := json.Marshal(data)
if err != nil {
@@ -836,19 +652,18 @@ func handleParentalStatus(w http.ResponseWriter, r *http.Request) {
// ------------
func handleSafeSearchEnable(w http.ResponseWriter, r *http.Request) {
config.CoreDNS.SafeSearchEnabled = true
config.DNS.SafeSearchEnabled = true
httpUpdateConfigReloadDNSReturnOK(w, r)
}
func handleSafeSearchDisable(w http.ResponseWriter, r *http.Request) {
config.CoreDNS.SafeSearchEnabled = false
config.DNS.SafeSearchEnabled = false
httpUpdateConfigReloadDNSReturnOK(w, r)
}
//noinspection GoUnusedParameter
func handleSafeSearchStatus(w http.ResponseWriter, r *http.Request) {
data := map[string]interface{}{
"enabled": config.CoreDNS.SafeSearchEnabled,
"enabled": config.DNS.SafeSearchEnabled,
}
jsonVal, err := json.Marshal(data)
if err != nil {
@@ -872,17 +687,17 @@ func registerControlHandlers() {
http.HandleFunc("/control/status", optionalAuth(ensureGET(handleStatus)))
http.HandleFunc("/control/enable_protection", optionalAuth(ensurePOST(handleProtectionEnable)))
http.HandleFunc("/control/disable_protection", optionalAuth(ensurePOST(handleProtectionDisable)))
http.HandleFunc("/control/querylog", optionalAuth(ensureGET(corednsplugin.HandleQueryLog)))
http.HandleFunc("/control/querylog", optionalAuth(ensureGET(dnsforward.HandleQueryLog)))
http.HandleFunc("/control/querylog_enable", optionalAuth(ensurePOST(handleQueryLogEnable)))
http.HandleFunc("/control/querylog_disable", optionalAuth(ensurePOST(handleQueryLogDisable)))
http.HandleFunc("/control/set_upstream_dns", optionalAuth(ensurePOST(handleSetUpstreamDNS)))
http.HandleFunc("/control/test_upstream_dns", optionalAuth(ensurePOST(handleTestUpstreamDNS)))
http.HandleFunc("/control/i18n/change_language", optionalAuth(ensurePOST(handleI18nChangeLanguage)))
http.HandleFunc("/control/i18n/current_language", optionalAuth(ensureGET(handleI18nCurrentLanguage)))
http.HandleFunc("/control/stats_top", optionalAuth(ensureGET(corednsplugin.HandleStatsTop)))
http.HandleFunc("/control/stats", optionalAuth(ensureGET(corednsplugin.HandleStats)))
http.HandleFunc("/control/stats_history", optionalAuth(ensureGET(corednsplugin.HandleStatsHistory)))
http.HandleFunc("/control/stats_reset", optionalAuth(ensurePOST(corednsplugin.HandleStatsReset)))
http.HandleFunc("/control/stats_top", optionalAuth(ensureGET(dnsforward.HandleStatsTop)))
http.HandleFunc("/control/stats", optionalAuth(ensureGET(dnsforward.HandleStats)))
http.HandleFunc("/control/stats_history", optionalAuth(ensureGET(dnsforward.HandleStatsHistory)))
http.HandleFunc("/control/stats_reset", optionalAuth(ensurePOST(dnsforward.HandleStatsReset)))
http.HandleFunc("/control/version.json", optionalAuth(handleGetVersionJSON))
http.HandleFunc("/control/filtering/enable", optionalAuth(ensurePOST(handleFilteringEnable)))
http.HandleFunc("/control/filtering/disable", optionalAuth(ensurePOST(handleFilteringDisable)))
@@ -902,4 +717,8 @@ func registerControlHandlers() {
http.HandleFunc("/control/safesearch/enable", optionalAuth(ensurePOST(handleSafeSearchEnable)))
http.HandleFunc("/control/safesearch/disable", optionalAuth(ensurePOST(handleSafeSearchDisable)))
http.HandleFunc("/control/safesearch/status", optionalAuth(ensureGET(handleSafeSearchStatus)))
http.HandleFunc("/control/dhcp/status", optionalAuth(ensureGET(handleDHCPStatus)))
http.HandleFunc("/control/dhcp/interfaces", optionalAuth(ensureGET(handleDHCPInterfaces)))
http.HandleFunc("/control/dhcp/set_config", optionalAuth(ensurePOST(handleDHCPSetConfig)))
http.HandleFunc("/control/dhcp/find_active_dhcp", optionalAuth(ensurePOST(handleDHCPFindActiveServer)))
}

View File

@@ -1,132 +0,0 @@
package main
import (
"fmt"
"log"
"os"
"path/filepath"
"sync" // Include all plugins.
_ "github.com/AdguardTeam/AdGuardHome/coredns_plugin"
_ "github.com/AdguardTeam/AdGuardHome/coredns_plugin/ratelimit"
_ "github.com/AdguardTeam/AdGuardHome/coredns_plugin/refuseany"
_ "github.com/AdguardTeam/AdGuardHome/upstream"
"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",
"refuseany",
"ratelimit",
"dnsfilter",
"dnstap",
"chaos",
"loadbalance",
"cache",
"rewrite",
"dnssec",
"autopath",
"template",
"hosts",
"file",
"auto",
"secondary",
"loop",
"forward",
"proxy",
"upstream",
"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
}
go coremain.Run()
return nil
}

View File

@@ -1,559 +0,0 @@
package dnsfilter
import (
"bufio"
"errors"
"fmt"
"log"
"net"
"os"
"strconv"
"strings"
"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"
"golang.org/x/net/context"
)
var defaultSOA = &dns.SOA{
// values copied from verisign's nonexistent .com domain
// their exact values are not important in our use case because they are used for domain transfers between primary/secondary DNS servers
Refresh: 1800,
Retry: 900,
Expire: 604800,
Minttl: 86400,
}
func init() {
caddy.RegisterPlugin("dnsfilter", caddy.Plugin{
ServerType: "dns",
Action: setup,
})
}
type plugFilter struct {
ID int64
Path string
}
type plugSettings struct {
SafeBrowsingBlockHost string
ParentalBlockHost string
QueryLogEnabled bool
BlockedTTL uint32 // in seconds, default 3600
Filters []plugFilter
}
type plug struct {
d *dnsfilter.Dnsfilter
Next plugin.Handler
upstream upstream.Upstream
settings plugSettings
sync.RWMutex
}
var defaultPluginSettings = plugSettings{
SafeBrowsingBlockHost: "safebrowsing.block.dns.adguard.com",
ParentalBlockHost: "family.block.dns.adguard.com",
BlockedTTL: 3600, // in seconds
Filters: make([]plugFilter, 0),
}
//
// coredns handling functions
//
func setupPlugin(c *caddy.Controller) (*plug, error) {
// create new Plugin and copy default values
p := &plug{
settings: defaultPluginSettings,
d: dnsfilter.New(),
}
log.Println("Initializing the CoreDNS plugin")
for c.Next() {
for c.NextBlock() {
blockValue := c.Val()
switch blockValue {
case "safebrowsing":
log.Println("Browsing security service is enabled")
p.d.EnableSafeBrowsing()
if c.NextArg() {
if len(c.Val()) == 0 {
return nil, c.ArgErr()
}
p.d.SetSafeBrowsingServer(c.Val())
}
case "safesearch":
log.Println("Safe search is enabled")
p.d.EnableSafeSearch()
case "parental":
if !c.NextArg() {
return nil, c.ArgErr()
}
sensitivity, err := strconv.Atoi(c.Val())
if err != nil {
return nil, c.ArgErr()
}
log.Println("Parental control is enabled")
err = p.d.EnableParental(sensitivity)
if err != nil {
return nil, c.ArgErr()
}
if c.NextArg() {
if len(c.Val()) == 0 {
return nil, c.ArgErr()
}
p.settings.ParentalBlockHost = c.Val()
}
case "blocked_ttl":
if !c.NextArg() {
return nil, c.ArgErr()
}
blockedTtl, err := strconv.ParseUint(c.Val(), 10, 32)
if err != nil {
return nil, c.ArgErr()
}
log.Printf("Blocked request TTL is %d", blockedTtl)
p.settings.BlockedTTL = uint32(blockedTtl)
case "querylog":
log.Println("Query log is enabled")
p.settings.QueryLogEnabled = true
case "filter":
if !c.NextArg() {
return nil, c.ArgErr()
}
filterId, err := strconv.ParseInt(c.Val(), 10, 64)
if err != nil {
return nil, c.ArgErr()
}
if !c.NextArg() {
return nil, c.ArgErr()
}
filterPath := c.Val()
// Initialize filter and add it to the list
p.settings.Filters = append(p.settings.Filters, plugFilter{
ID: filterId,
Path: filterPath,
})
}
}
}
for _, filter := range p.settings.Filters {
log.Printf("Loading rules from %s", filter.Path)
file, err := os.Open(filter.Path)
if err != nil {
return nil, err
}
//noinspection GoDeferInLoop
defer file.Close()
count := 0
scanner := bufio.NewScanner(file)
for scanner.Scan() {
text := scanner.Text()
err = p.d.AddRule(text, filter.ID)
if err == dnsfilter.ErrAlreadyExists || err == dnsfilter.ErrInvalidSyntax {
continue
}
if err != nil {
log.Printf("Cannot add rule %s: %s", text, err)
// Just ignore invalid rules
continue
}
count++
}
log.Printf("Added %d rules from filter ID=%d", count, filter.ID)
if err = scanner.Err(); err != nil {
return nil, err
}
}
log.Printf("Loading stats from querylog")
err := fillStatsFromQueryLog()
if err != nil {
log.Printf("Failed to load stats from querylog: %s", err)
return nil, err
}
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 p, nil
}
func setup(c *caddy.Controller) error {
p, err := setupPlugin(c)
if err != nil {
return err
}
config := dnsserver.GetConfig(c)
config.AddPlugin(func(next plugin.Handler) plugin.Handler {
p.Next = next
return p
})
c.OnStartup(func() error {
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(p.onShutdown)
c.OnFinalShutdown(p.onFinalShutdown)
return 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
}
type statsFunc func(ch interface{}, name string, text string, value float64, valueType prometheus.ValueType)
//noinspection GoUnusedParameter
func doDesc(ch interface{}, name string, text string, value float64, valueType prometheus.ValueType) {
realch, ok := ch.(chan<- *prometheus.Desc)
if !ok {
log.Printf("Couldn't convert ch to chan<- *prometheus.Desc\n")
return
}
realch <- prometheus.NewDesc(name, text, nil, nil)
}
func doMetric(ch interface{}, name string, text string, value float64, valueType prometheus.ValueType) {
realch, ok := ch.(chan<- prometheus.Metric)
if !ok {
log.Printf("Couldn't convert ch to chan<- prometheus.Metric\n")
return
}
desc := prometheus.NewDesc(name, text, nil, nil)
realch <- prometheus.MustNewConstMetric(desc, valueType, value)
}
func gen(ch interface{}, doFunc statsFunc, name string, text string, value float64, valueType prometheus.ValueType) {
doFunc(ch, name, text, value, valueType)
}
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 (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()
}
// Describe is called by prometheus handler to know stat types
func (p *plug) Describe(ch chan<- *prometheus.Desc) {
p.doStats(ch, doDesc)
}
// 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) // debug logging
if addr != nil {
// this is an IP address, return it
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)
}
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 := 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)
}
if result != nil {
for _, answer := range result.Answer {
answer.Header().Name = question.Name
}
records = result.Answer
}
}
m := new(dns.Msg)
m.SetReply(r)
m.Authoritative, m.RecursionAvailable, m.Compress = true, true, true
m.Answer = append(m.Answer, records...)
state := request.Request{W: w, Req: r, Context: ctx}
state.SizeAndDo(m)
err := state.W.WriteMsg(m)
if err != nil {
log.Printf("Got error %s\n", err)
return dns.RcodeServerFailure, fmt.Errorf("plugin/dnsfilter: %s", err)
}
return dns.RcodeSuccess, nil
}
// 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 (p *plug) genSOA(r *dns.Msg) []dns.RR {
zone := r.Question[0].Name
header := dns.RR_Header{Name: zone, Rrtype: dns.TypeSOA, Ttl: p.settings.BlockedTTL, Class: dns.ClassINET}
Mbox := "hostmaster."
if zone[0] != '.' {
Mbox += zone
}
Ns := "fake-for-negative-caching.adguard.com."
soa := *defaultSOA
soa.Hdr = header
soa.Mbox = Mbox
soa.Ns = Ns
soa.Serial = 100500 // faster than uint32(time.Now().Unix())
return []dns.RR{&soa}
}
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 = p.genSOA(r)
state.SizeAndDo(m)
err := state.W.WriteMsg(m)
if err != nil {
log.Printf("Got error %s\n", err)
return dns.RcodeServerFailure, err
}
return dns.RcodeNameError, nil
}
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, dnsfilter.Result{}, fmt.Errorf("got a DNS request with more than one Question")
}
for _, question := range r.Question {
host := strings.ToLower(strings.TrimSuffix(question.Name, "."))
// is it a safesearch domain?
p.RLock()
if val, ok := p.d.SafeSearchDomain(host); ok {
rcode, err := p.replaceHostWithValAndReply(ctx, w, r, host, val, question)
if err != nil {
p.RUnlock()
return rcode, dnsfilter.Result{}, err
}
p.RUnlock()
return rcode, dnsfilter.Result{Reason: dnsfilter.FilteredSafeSearch}, err
}
p.RUnlock()
// needs to be filtered instead
p.RLock()
result, err := p.d.CheckHost(host)
if err != nil {
log.Printf("plugin/dnsfilter: %s\n", err)
p.RUnlock()
return dns.RcodeServerFailure, dnsfilter.Result{}, fmt.Errorf("plugin/dnsfilter: %s", err)
}
p.RUnlock()
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:
if result.Ip == nil {
// return NXDomain
rcode, err := p.writeNXdomain(ctx, w, r)
if err != nil {
return rcode, dnsfilter.Result{}, err
}
return rcode, result, err
} else {
// This is a hosts-syntax rule
rcode, err := p.replaceHostWithValAndReply(ctx, w, r, host, result.Ip.String(), question)
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)
}
} 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)
}
}
}
rcode, err := plugin.NextOrFailure(p.Name(), p.Next, ctx, w, r)
return rcode, dnsfilter.Result{}, err
}
// 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, result, err := p.serveDNSInternal(ctx, rrw, r)
if rcode > 0 {
// actually send the answer if we have one
answer := new(dns.Msg)
answer.SetRcode(r, rcode)
state.SizeAndDo(answer)
err = w.WriteMsg(answer)
if err != nil {
return dns.RcodeServerFailure, err
}
}
// increment counters
switch {
case err != nil:
errorsTotal.Inc()
case result.Reason == dnsfilter.FilteredBlackList:
filtered.Inc()
filteredLists.Inc()
case result.Reason == dnsfilter.FilteredSafeBrowsing:
filtered.Inc()
filteredSafebrowsing.Inc()
case result.Reason == dnsfilter.FilteredParental:
filtered.Inc()
filteredParental.Inc()
case result.Reason == dnsfilter.FilteredInvalid:
filtered.Inc()
filteredInvalid.Inc()
case result.Reason == dnsfilter.FilteredSafeSearch:
// the request was passsed through but not filtered, don't increment filtered
safesearch.Inc()
case result.Reason == dnsfilter.NotFilteredWhiteList:
whitelisted.Inc()
case result.Reason == dnsfilter.NotFilteredNotFound:
// do nothing
case result.Reason == dnsfilter.NotFilteredError:
text := "SHOULD NOT HAPPEN: got DNSFILTER_NOTFILTERED_ERROR without err != nil!"
log.Println(text)
err = errors.New(text)
rcode = dns.RcodeServerFailure
}
// log
elapsed := time.Since(start)
elapsedTime.Observe(elapsed.Seconds())
if p.settings.QueryLogEnabled {
logRequest(r, rrw.Msg, result, time.Since(start), ip)
}
return rcode, err
}
// Name returns name of the plugin as seen in Corefile and plugin.cfg
func (p *plug) Name() string { return "dnsfilter" }
var onceHook sync.Once
var onceQueryLog sync.Once

View File

@@ -1,131 +0,0 @@
package dnsfilter
import (
"context"
"fmt"
"io/ioutil"
"net"
"os"
"testing"
"github.com/coredns/coredns/plugin"
"github.com/coredns/coredns/plugin/pkg/dnstest"
"github.com/coredns/coredns/plugin/test"
"github.com/mholt/caddy"
"github.com/miekg/dns"
)
func TestSetup(t *testing.T) {
for i, testcase := range []struct {
config string
failing bool
}{
{`dnsfilter`, false},
{`dnsfilter {
filter 0 /dev/nonexistent/abcdef
}`, true},
{`dnsfilter {
filter 0 ../tests/dns.txt
}`, false},
{`dnsfilter {
safebrowsing
filter 0 ../tests/dns.txt
}`, false},
{`dnsfilter {
parental
filter 0 ../tests/dns.txt
}`, true},
} {
c := caddy.NewTestController("dns", testcase.config)
err := setup(c)
if err != nil {
if !testcase.failing {
t.Fatalf("Test #%d expected no errors, but got: %v", i, err)
}
continue
}
if testcase.failing {
t.Fatalf("Test #%d expected to fail but it didn't", i)
}
}
}
func TestEtcHostsFilter(t *testing.T) {
text := []byte("127.0.0.1 doubleclick.net\n" + "127.0.0.1 example.org example.net www.example.org www.example.net")
tmpfile, err := ioutil.TempFile("", "")
if err != nil {
t.Fatal(err)
}
if _, err = tmpfile.Write(text); err != nil {
t.Fatal(err)
}
if err = tmpfile.Close(); err != nil {
t.Fatal(err)
}
defer os.Remove(tmpfile.Name())
configText := fmt.Sprintf("dnsfilter {\nfilter 0 %s\n}", tmpfile.Name())
c := caddy.NewTestController("dns", configText)
p, err := setupPlugin(c)
if err != nil {
t.Fatal(err)
}
p.Next = zeroTTLBackend()
ctx := context.TODO()
for _, testcase := range []struct {
host string
filtered bool
}{
{"www.doubleclick.net", false},
{"doubleclick.net", true},
{"www2.example.org", false},
{"www2.example.net", false},
{"test.www.example.org", false},
{"test.www.example.net", false},
{"example.org", true},
{"example.net", true},
{"www.example.org", true},
{"www.example.net", true},
} {
req := new(dns.Msg)
req.SetQuestion(testcase.host+".", dns.TypeA)
resp := test.ResponseWriter{}
rrw := dnstest.NewRecorder(&resp)
rcode, err := p.ServeDNS(ctx, rrw, req)
if err != nil {
t.Fatalf("ServeDNS returned error: %s", err)
}
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)
}
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 && testcase.filtered != filtered {
t.Fatalf("Host %s expected to be not filtered, instead it is filtered", testcase.host)
}
}
}
func zeroTTLBackend() plugin.Handler {
return plugin.HandlerFunc(func(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
m := new(dns.Msg)
m.SetReply(r)
m.Response, m.RecursionAvailable = true, true
m.Answer = []dns.RR{test.A("example.org. 0 IN A 127.0.0.53")}
w.WriteMsg(m)
return dns.RcodeSuccess, nil
})
}

View File

@@ -1,184 +0,0 @@
package ratelimit
import (
"errors"
"log"
"sort"
"strconv"
"time"
// ratelimiting and per-ip buckets
"github.com/beefsack/go-rate"
"github.com/patrickmn/go-cache"
// coredns plugin
"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/request"
"github.com/mholt/caddy"
"github.com/miekg/dns"
"github.com/prometheus/client_golang/prometheus"
"golang.org/x/net/context"
)
const defaultRatelimit = 30
const defaultResponseSize = 1000
var (
tokenBuckets = cache.New(time.Hour, time.Hour)
)
// 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)
if err != nil {
return 0, err
}
if !allow {
ratelimited.Inc()
return 0, nil
}
// Record response to get status code and size of the reply.
rw := dnstest.NewRecorder(w)
status, err := plugin.NextOrFailure(p.Name(), p.Next, ctx, rw, r)
size := rw.Len
if size > defaultResponseSize && state.Proto() == "udp" {
// For large UDP responses we call allowRequest more times
// The exact number of times depends on the response size
for i := 0; i < size/defaultResponseSize; i++ {
p.allowRequest(ip)
}
}
return status, err
}
func (p *plug) allowRequest(ip string) (bool, error) {
if len(p.whitelist) > 0 {
i := sort.SearchStrings(p.whitelist, ip)
if i < len(p.whitelist) && p.whitelist[i] == ip {
return true, nil
}
}
if _, found := tokenBuckets.Get(ip); !found {
tokenBuckets.Set(ip, rate.New(p.ratelimit, time.Second), time.Hour)
}
value, found := tokenBuckets.Get(ip)
if !found {
// should not happen since we've just inserted it
text := "SHOULD NOT HAPPEN: just-inserted ratelimiter disappeared"
log.Println(text)
err := errors.New(text)
return true, err
}
rl, ok := value.(*rate.RateLimiter)
if !ok {
text := "SHOULD NOT HAPPEN: non-bool entry found in safebrowsing lookup cache"
log.Println(text)
err := errors.New(text)
return true, err
}
allow, _ := rl.Try()
return allow, nil
}
//
// helper functions
//
func init() {
caddy.RegisterPlugin("ratelimit", caddy.Plugin{
ServerType: "dns",
Action: setup,
})
}
type plug struct {
Next plugin.Handler
// configuration for creating above
ratelimit int // in requests per second per IP
whitelist []string // a list of whitelisted IP addresses
}
func setupPlugin(c *caddy.Controller) (*plug, error) {
p := &plug{ratelimit: defaultRatelimit}
for c.Next() {
args := c.RemainingArgs()
if len(args) > 0 {
ratelimit, err := strconv.Atoi(args[0])
if err != nil {
return nil, c.ArgErr()
}
p.ratelimit = ratelimit
}
for c.NextBlock() {
switch c.Val() {
case "whitelist":
p.whitelist = c.RemainingArgs()
if len(p.whitelist) > 0 {
sort.Strings(p.whitelist)
}
}
}
}
return p, nil
}
func setup(c *caddy.Controller) error {
p, err := setupPlugin(c)
if err != nil {
return err
}
config := dnsserver.GetConfig(c)
config.AddPlugin(func(next plugin.Handler) plugin.Handler {
p.Next = next
return p
})
c.OnStartup(func() error {
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 {
return prometheus.NewCounter(prometheus.CounterOpts{
Namespace: plugin.Namespace,
Subsystem: "ratelimit",
Name: name,
Help: help,
})
}
var (
ratelimited = newDNSCounter("dropped_total", "Count of requests that have been dropped because of rate limit")
)
// Name returns name of the plugin as seen in Corefile and plugin.cfg
func (p *plug) Name() string { return "ratelimit" }

View File

@@ -1,82 +0,0 @@
package ratelimit
import (
"testing"
"github.com/mholt/caddy"
)
func TestSetup(t *testing.T) {
for i, testcase := range []struct {
config string
failing bool
}{
{`ratelimit`, false},
{`ratelimit 100`, false},
{`ratelimit {
whitelist 127.0.0.1
}`, false},
{`ratelimit 50 {
whitelist 127.0.0.1 176.103.130.130
}`, false},
{`ratelimit test`, true},
} {
c := caddy.NewTestController("dns", testcase.config)
err := setup(c)
if err != nil {
if !testcase.failing {
t.Fatalf("Test #%d expected no errors, but got: %v", i, err)
}
continue
}
if testcase.failing {
t.Fatalf("Test #%d expected to fail but it didn't", i)
}
}
}
func TestRatelimiting(t *testing.T) {
// rate limit is 1 per sec
c := caddy.NewTestController("dns", `ratelimit 1`)
p, err := setupPlugin(c)
if err != nil {
t.Fatal("Failed to initialize the plugin")
}
allowed, err := p.allowRequest("127.0.0.1")
if err != nil || !allowed {
t.Fatal("First request must have been allowed")
}
allowed, err = p.allowRequest("127.0.0.1")
if err != nil || allowed {
t.Fatal("Second request must have been ratelimited")
}
}
func TestWhitelist(t *testing.T) {
// rate limit is 1 per sec
c := caddy.NewTestController("dns", `ratelimit 1 { whitelist 127.0.0.2 127.0.0.1 127.0.0.125 }`)
p, err := setupPlugin(c)
if err != nil {
t.Fatal("Failed to initialize the plugin")
}
allowed, err := p.allowRequest("127.0.0.1")
if err != nil || !allowed {
t.Fatal("First request must have been allowed")
}
allowed, err = p.allowRequest("127.0.0.1")
if err != nil || !allowed {
t.Fatal("Second request must have been allowed due to whitelist")
}
}

View File

@@ -1,91 +0,0 @@
package refuseany
import (
"fmt"
"log"
"github.com/coredns/coredns/core/dnsserver"
"github.com/coredns/coredns/plugin"
"github.com/coredns/coredns/plugin/metrics"
"github.com/coredns/coredns/request"
"github.com/mholt/caddy"
"github.com/miekg/dns"
"github.com/prometheus/client_golang/prometheus"
"golang.org/x/net/context"
)
type plug struct {
Next plugin.Handler
}
// 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")
}
q := r.Question[0]
if q.Qtype == dns.TypeANY {
state := request.Request{W: w, Req: r, Context: ctx}
rcode := dns.RcodeNotImplemented
m := new(dns.Msg)
m.SetRcode(r, rcode)
state.SizeAndDo(m)
err := state.W.WriteMsg(m)
if err != nil {
log.Printf("Got error %s\n", err)
return dns.RcodeServerFailure, err
}
return rcode, nil
}
return plugin.NextOrFailure(p.Name(), p.Next, ctx, w, r)
}
func init() {
caddy.RegisterPlugin("refuseany", caddy.Plugin{
ServerType: "dns",
Action: setup,
})
}
func setup(c *caddy.Controller) error {
p := &plug{}
config := dnsserver.GetConfig(c)
config.AddPlugin(func(next plugin.Handler) plugin.Handler {
p.Next = next
return p
})
c.OnStartup(func() error {
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 {
return prometheus.NewCounter(prometheus.CounterOpts{
Namespace: plugin.Namespace,
Subsystem: "refuseany",
Name: name,
Help: help,
})
}
var (
ratelimited = newDNSCounter("refusedany_total", "Count of ANY requests that have been dropped")
)
// Name returns name of the plugin as seen in Corefile and plugin.cfg
func (p *plug) Name() string { return "refuseany" }

View File

@@ -1,36 +0,0 @@
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
}

169
dhcp.go Normal file
View File

@@ -0,0 +1,169 @@
package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"net"
"net/http"
"strings"
"time"
"github.com/AdguardTeam/AdGuardHome/dhcpd"
"github.com/hmage/golibs/log"
"github.com/joomcode/errorx"
)
var dhcpServer = dhcpd.Server{}
func handleDHCPStatus(w http.ResponseWriter, r *http.Request) {
rawLeases := dhcpServer.Leases()
leases := []map[string]string{}
for i := range rawLeases {
lease := map[string]string{
"mac": rawLeases[i].HWAddr.String(),
"ip": rawLeases[i].IP.String(),
"hostname": rawLeases[i].Hostname,
"expires": rawLeases[i].Expiry.Format(time.RFC3339),
}
leases = append(leases, lease)
}
status := map[string]interface{}{
"config": config.DHCP,
"leases": leases,
}
w.Header().Set("Content-Type", "application/json")
err := json.NewEncoder(w).Encode(status)
if err != nil {
httpError(w, http.StatusInternalServerError, "Unable to marshal DHCP status json: %s", err)
return
}
}
func handleDHCPSetConfig(w http.ResponseWriter, r *http.Request) {
newconfig := dhcpd.ServerConfig{}
err := json.NewDecoder(r.Body).Decode(&newconfig)
if err != nil {
httpError(w, http.StatusBadRequest, "Failed to parse new DHCP config json: %s", err)
return
}
if newconfig.Enabled {
err := dhcpServer.Start(&newconfig)
if err != nil {
httpError(w, http.StatusBadRequest, "Failed to start DHCP server: %s", err)
return
}
}
if !newconfig.Enabled {
dhcpServer.Stop()
}
config.DHCP = newconfig
httpUpdateConfigReloadDNSReturnOK(w, r)
}
func handleDHCPInterfaces(w http.ResponseWriter, r *http.Request) {
response := map[string]interface{}{}
ifaces, err := net.Interfaces()
if err != nil {
httpError(w, http.StatusInternalServerError, "Couldn't get list of interfaces: %s", err)
return
}
type address struct {
IP string
Netmask string
}
type responseInterface struct {
Name string `json:"name"`
MTU int `json:"mtu"`
HardwareAddr string `json:"hardware_address"`
Addresses []string `json:"ip_addresses"`
}
for i := range ifaces {
if ifaces[i].Flags&net.FlagLoopback != 0 {
// it's a loopback, skip it
continue
}
if ifaces[i].Flags&net.FlagBroadcast == 0 {
// this interface doesn't support broadcast, skip it
continue
}
if ifaces[i].Flags&net.FlagPointToPoint != 0 {
// this interface is ppp, don't do dhcp over it
continue
}
iface := responseInterface{
Name: ifaces[i].Name,
MTU: ifaces[i].MTU,
HardwareAddr: ifaces[i].HardwareAddr.String(),
}
addrs, err := ifaces[i].Addrs()
if err != nil {
httpError(w, http.StatusInternalServerError, "Failed to get addresses for interface %v: %s", ifaces[i].Name, err)
return
}
for _, addr := range addrs {
iface.Addresses = append(iface.Addresses, addr.String())
}
if len(iface.Addresses) == 0 {
// this interface has no addresses, skip it
continue
}
response[ifaces[i].Name] = iface
}
err = json.NewEncoder(w).Encode(response)
if err != nil {
httpError(w, http.StatusInternalServerError, "Failed to marshal json with available interfaces: %s", err)
return
}
}
func handleDHCPFindActiveServer(w http.ResponseWriter, r *http.Request) {
body, err := ioutil.ReadAll(r.Body)
if err != nil {
errorText := fmt.Sprintf("failed to read request body: %s", err)
log.Println(errorText)
http.Error(w, errorText, http.StatusBadRequest)
return
}
interfaceName := strings.TrimSpace(string(body))
if interfaceName == "" {
errorText := fmt.Sprintf("empty interface name specified")
log.Println(errorText)
http.Error(w, errorText, http.StatusBadRequest)
return
}
found, err := dhcpd.CheckIfOtherDHCPServersPresent(interfaceName)
result := map[string]interface{}{}
if err != nil {
result["error"] = err.Error()
} else {
result["found"] = found
}
w.Header().Set("Content-Type", "application/json")
err = json.NewEncoder(w).Encode(result)
if err != nil {
httpError(w, http.StatusInternalServerError, "Failed to marshal DHCP found json: %s", err)
return
}
}
func startDHCPServer() error {
if config.DHCP.Enabled == false {
// not enabled, don't do anything
return nil
}
err := dhcpServer.Start(&config.DHCP)
if err != nil {
return errorx.Decorate(err, "Couldn't start DHCP server")
}
return nil
}

144
dhcpd/check_other_dhcp.go Normal file
View File

@@ -0,0 +1,144 @@
package dhcpd
import (
"crypto/rand"
"encoding/binary"
"fmt"
"math"
"net"
"os"
"time"
"github.com/hmage/golibs/log"
"github.com/krolaw/dhcp4"
)
func CheckIfOtherDHCPServersPresent(ifaceName string) (bool, error) {
iface, err := net.InterfaceByName(ifaceName)
if err != nil {
return false, wrapErrPrint(err, "Couldn't find interface by name %s", ifaceName)
}
// get ipv4 address of an interface
ifaceIPNet := getIfaceIPv4(iface)
if ifaceIPNet == nil {
return false, fmt.Errorf("Couldn't find IPv4 address of interface %s %+v", ifaceName, iface)
}
srcIP := ifaceIPNet.IP
src := net.JoinHostPort(srcIP.String(), "68")
dst := "255.255.255.255:67"
// form a DHCP request packet, try to emulate existing client as much as possible
xId := make([]byte, 8)
n, err := rand.Read(xId)
if n != 8 && err == nil {
err = fmt.Errorf("Generated less than 8 bytes")
}
if err != nil {
return false, wrapErrPrint(err, "Couldn't generate 8 random bytes")
}
hostname, err := os.Hostname()
if err != nil {
return false, wrapErrPrint(err, "Couldn't get hostname")
}
requestList := []byte{
byte(dhcp4.OptionSubnetMask),
byte(dhcp4.OptionClasslessRouteFormat),
byte(dhcp4.OptionRouter),
byte(dhcp4.OptionDomainNameServer),
byte(dhcp4.OptionDomainName),
byte(dhcp4.OptionDomainSearch),
252, // private/proxy autodiscovery
95, // LDAP
byte(dhcp4.OptionNetBIOSOverTCPIPNameServer),
byte(dhcp4.OptionNetBIOSOverTCPIPNodeType),
}
maxUDPsizeRaw := make([]byte, 2)
binary.BigEndian.PutUint16(maxUDPsizeRaw, 1500)
leaseTimeRaw := make([]byte, 4)
leaseTime := uint32(math.RoundToEven(time.Duration(time.Hour * 24 * 90).Seconds()))
binary.BigEndian.PutUint32(leaseTimeRaw, leaseTime)
options := []dhcp4.Option{
{dhcp4.OptionParameterRequestList, requestList},
{dhcp4.OptionMaximumDHCPMessageSize, maxUDPsizeRaw},
{dhcp4.OptionClientIdentifier, append([]byte{0x01}, iface.HardwareAddr...)},
{dhcp4.OptionIPAddressLeaseTime, leaseTimeRaw},
{dhcp4.OptionHostName, []byte(hostname)},
}
packet := dhcp4.RequestPacket(dhcp4.Discover, iface.HardwareAddr, nil, xId, false, options)
// resolve 0.0.0.0:68
udpAddr, err := net.ResolveUDPAddr("udp4", src)
if err != nil {
return false, wrapErrPrint(err, "Couldn't resolve UDP address %s", src)
}
// spew.Dump(udpAddr, err)
if !udpAddr.IP.To4().Equal(srcIP) {
return false, wrapErrPrint(err, "Resolved UDP address is not %s", src)
}
// resolve 255.255.255.255:67
dstAddr, err := net.ResolveUDPAddr("udp4", dst)
if err != nil {
return false, wrapErrPrint(err, "Couldn't resolve UDP address %s", dst)
}
// bind to 0.0.0.0:68
log.Tracef("Listening to udp4 %+v", udpAddr)
c, err := net.ListenPacket("udp4", src)
if c != nil {
defer c.Close()
}
// spew.Dump(c, err)
// spew.Printf("net.ListenUDP returned %v, %v\n", c, err)
if err != nil {
return false, wrapErrPrint(err, "Couldn't listen to %s", src)
}
// send to 255.255.255.255:67
n, err = c.WriteTo(packet, dstAddr)
// spew.Dump(n, err)
if err != nil {
return false, wrapErrPrint(err, "Couldn't send a packet to %s", dst)
}
// wait for answer
log.Tracef("Waiting %v for an answer", defaultDiscoverTime)
// TODO: replicate dhclient's behaviour of retrying several times with progressively bigger timeouts
b := make([]byte, 1500)
c.SetReadDeadline(time.Now().Add(defaultDiscoverTime))
n, _, err = c.ReadFrom(b)
if isTimeout(err) {
// timed out -- no DHCP servers
return false, nil
}
if err != nil {
return false, wrapErrPrint(err, "Couldn't receive packet")
}
if n > 0 {
b = b[:n]
}
// spew.Dump(n, fromAddr, err, b)
if n < 240 {
// packet too small for dhcp
return false, wrapErrPrint(err, "got packet that's too small for DHCP")
}
response := dhcp4.Packet(b[:n])
if response.HLen() > 16 {
// invalid size
return false, wrapErrPrint(err, "got malformed packet with HLen() > 16")
}
parsedOptions := response.ParseOptions()
_, ok := parsedOptions[dhcp4.OptionDHCPMessageType]
if !ok {
return false, wrapErrPrint(err, "got malformed packet without DHCP message type")
}
// that's a DHCP server there
return true, nil
}

398
dhcpd/dhcpd.go Normal file
View File

@@ -0,0 +1,398 @@
package dhcpd
import (
"bytes"
"fmt"
"net"
"sync"
"time"
"github.com/hmage/golibs/log"
"github.com/krolaw/dhcp4"
)
const defaultDiscoverTime = time.Second * 3
// field ordering is important -- yaml fields will mirror ordering from here
type Lease struct {
HWAddr net.HardwareAddr `json:"mac" yaml:"hwaddr"`
IP net.IP `json:"ip"`
Hostname string `json:"hostname"`
Expiry time.Time `json:"expires"`
}
// field ordering is important -- yaml fields will mirror ordering from here
type ServerConfig struct {
Enabled bool `json:"enabled" yaml:"enabled"`
InterfaceName string `json:"interface_name" yaml:"interface_name"` // eth0, en0 and so on
GatewayIP string `json:"gateway_ip" yaml:"gateway_ip"`
SubnetMask string `json:"subnet_mask" yaml:"subnet_mask"`
RangeStart string `json:"range_start" yaml:"range_start"`
RangeEnd string `json:"range_end" yaml:"range_end"`
LeaseDuration uint `json:"lease_duration" yaml:"lease_duration"` // in seconds
}
type Server struct {
conn *filterConn // listening UDP socket
ipnet *net.IPNet // if interface name changes, this needs to be reset
// leases
leases []*Lease
leaseStart net.IP // parsed from config RangeStart
leaseStop net.IP // parsed from config RangeEnd
leaseTime time.Duration // parsed from config LeaseDuration
leaseOptions dhcp4.Options // parsed from config GatewayIP and SubnetMask
// IP address pool -- if entry is in the pool, then it's attached to a lease
IPpool map[[4]byte]net.HardwareAddr
ServerConfig
sync.RWMutex
}
// Start will listen on port 67 and serve DHCP requests.
// Even though config can be nil, it is not optional (at least for now), since there are no default values (yet).
func (s *Server) Start(config *ServerConfig) error {
if config != nil {
s.ServerConfig = *config
}
iface, err := net.InterfaceByName(s.InterfaceName)
if err != nil {
s.closeConn() // in case it was already started
return wrapErrPrint(err, "Couldn't find interface by name %s", s.InterfaceName)
}
// get ipv4 address of an interface
s.ipnet = getIfaceIPv4(iface)
if s.ipnet == nil {
s.closeConn() // in case it was already started
return wrapErrPrint(err, "Couldn't find IPv4 address of interface %s %+v", s.InterfaceName, iface)
}
if s.LeaseDuration == 0 {
s.leaseTime = time.Hour * 2
s.LeaseDuration = uint(s.leaseTime.Seconds())
} else {
s.leaseTime = time.Second * time.Duration(s.LeaseDuration)
}
s.leaseStart, err = parseIPv4(s.RangeStart)
if err != nil {
s.closeConn() // in case it was already started
return wrapErrPrint(err, "Failed to parse range start address %s", s.RangeStart)
}
s.leaseStop, err = parseIPv4(s.RangeEnd)
if err != nil {
s.closeConn() // in case it was already started
return wrapErrPrint(err, "Failed to parse range end address %s", s.RangeEnd)
}
subnet, err := parseIPv4(s.SubnetMask)
if err != nil {
s.closeConn() // in case it was already started
return wrapErrPrint(err, "Failed to parse subnet mask %s", s.SubnetMask)
}
// if !bytes.Equal(subnet, s.ipnet.Mask) {
// s.closeConn() // in case it was already started
// return wrapErrPrint(err, "specified subnet mask %s does not meatch interface %s subnet mask %s", s.SubnetMask, s.InterfaceName, s.ipnet.Mask)
// }
router, err := parseIPv4(s.GatewayIP)
if err != nil {
s.closeConn() // in case it was already started
return wrapErrPrint(err, "Failed to parse gateway IP %s", s.GatewayIP)
}
s.leaseOptions = dhcp4.Options{
dhcp4.OptionSubnetMask: subnet,
dhcp4.OptionRouter: router,
dhcp4.OptionDomainNameServer: s.ipnet.IP,
}
// TODO: don't close if interface and addresses are the same
if s.conn != nil {
s.closeConn()
}
c, err := newFilterConn(*iface, ":67") // it has to be bound to 0.0.0.0:67, otherwise it won't see DHCP discover/request packets
if err != nil {
return wrapErrPrint(err, "Couldn't start listening socket on 0.0.0.0:67")
}
s.conn = c
go func() {
// operate on c instead of c.conn because c.conn can change over time
err := dhcp4.Serve(c, s)
if err != nil {
log.Printf("dhcp4.Serve() returned with error: %s", err)
}
c.Close() // in case Serve() exits for other reason than listening socket closure
}()
return nil
}
func (s *Server) Stop() error {
if s.conn == nil {
// nothing to do, return silently
return nil
}
err := s.closeConn()
if err != nil {
return wrapErrPrint(err, "Couldn't close UDP listening socket")
}
return nil
}
// closeConn will close the connection and set it to zero
func (s *Server) closeConn() error {
if s.conn == nil {
return nil
}
err := s.conn.Close()
s.conn = nil
return err
}
func (s *Server) reserveLease(p dhcp4.Packet) (*Lease, error) {
// WARNING: do not remove copy()
// the given hwaddr by p.CHAddr() in the packet survives only during ServeDHCP() call
// since we need to retain it we need to make our own copy
hwaddrCOW := p.CHAddr()
hwaddr := make(net.HardwareAddr, len(hwaddrCOW))
copy(hwaddr, hwaddrCOW)
foundLease := s.locateLease(p)
if foundLease != nil {
// log.Tracef("found lease for %s: %+v", hwaddr, foundLease)
return foundLease, nil
}
// not assigned a lease, create new one, find IP from LRU
log.Tracef("Lease not found for %s: creating new one", hwaddr)
ip, err := s.findFreeIP(p, hwaddr)
if err != nil {
return nil, wrapErrPrint(err, "Couldn't find free IP for the lease %s", hwaddr.String())
}
log.Tracef("Assigning to %s IP address %s", hwaddr, ip.String())
hostname := p.ParseOptions()[dhcp4.OptionHostName]
lease := &Lease{HWAddr: hwaddr, IP: ip, Hostname: string(hostname)}
s.Lock()
s.leases = append(s.leases, lease)
s.Unlock()
return lease, nil
}
func (s *Server) locateLease(p dhcp4.Packet) *Lease {
hwaddr := p.CHAddr()
for i := range s.leases {
if bytes.Equal([]byte(hwaddr), []byte(s.leases[i].HWAddr)) {
// log.Tracef("bytes.Equal(%s, %s) returned true", hwaddr, s.leases[i].hwaddr)
return s.leases[i]
}
}
return nil
}
func (s *Server) findFreeIP(p dhcp4.Packet, hwaddr net.HardwareAddr) (net.IP, error) {
// if IP pool is nil, lazy initialize it
if s.IPpool == nil {
s.IPpool = make(map[[4]byte]net.HardwareAddr)
}
// go from start to end, find unreserved IP
var foundIP net.IP
for i := 0; i < dhcp4.IPRange(s.leaseStart, s.leaseStop); i++ {
newIP := dhcp4.IPAdd(s.leaseStart, i)
foundHWaddr := s.getIPpool(newIP)
log.Tracef("tried IP %v, got hwaddr %v", newIP, foundHWaddr)
if foundHWaddr != nil && len(foundHWaddr) != 0 {
// if !bytes.Equal(foundHWaddr, hwaddr) {
// log.Tracef("SHOULD NOT HAPPEN: hwaddr in IP pool %s is not equal to hwaddr in lease %s", foundHWaddr, hwaddr)
// }
log.Tracef("will try again")
continue
}
foundIP = newIP
break
}
if foundIP == nil {
// TODO: LRU
return nil, fmt.Errorf("Couldn't find free entry in IP pool")
}
s.reserveIP(foundIP, hwaddr)
return foundIP, nil
}
func (s *Server) getIPpool(ip net.IP) net.HardwareAddr {
rawIP := []byte(ip)
IP4 := [4]byte{rawIP[0], rawIP[1], rawIP[2], rawIP[3]}
return s.IPpool[IP4]
}
func (s *Server) reserveIP(ip net.IP, hwaddr net.HardwareAddr) {
rawIP := []byte(ip)
IP4 := [4]byte{rawIP[0], rawIP[1], rawIP[2], rawIP[3]}
s.IPpool[IP4] = hwaddr
}
func (s *Server) unreserveIP(ip net.IP) {
rawIP := []byte(ip)
IP4 := [4]byte{rawIP[0], rawIP[1], rawIP[2], rawIP[3]}
delete(s.IPpool, IP4)
}
func (s *Server) ServeDHCP(p dhcp4.Packet, msgType dhcp4.MessageType, options dhcp4.Options) dhcp4.Packet {
log.Tracef("Got %v message", msgType)
log.Tracef("Leases:")
for i, lease := range s.leases {
log.Tracef("Lease #%d: hwaddr %s, ip %s, expiry %s", i, lease.HWAddr, lease.IP, lease.Expiry)
}
log.Tracef("IP pool:")
for ip, hwaddr := range s.IPpool {
log.Tracef("IP pool entry %s -> %s", net.IPv4(ip[0], ip[1], ip[2], ip[3]), hwaddr)
}
// spew.Dump(s.leases, s.IPpool)
// log.Printf("Called with msgType = %v, options = %+v", msgType, options)
// spew.Dump(p)
// log.Printf("%14s %v", "p.Broadcast", p.Broadcast()) // false
// log.Printf("%14s %v", "p.CHAddr", p.CHAddr()) // 2c:f0:a2:f2:31:00
// log.Printf("%14s %v", "p.CIAddr", p.CIAddr()) // 0.0.0.0
// log.Printf("%14s %v", "p.Cookie", p.Cookie()) // [99 130 83 99]
// log.Printf("%14s %v", "p.File", p.File()) // []
// log.Printf("%14s %v", "p.Flags", p.Flags()) // [0 0]
// log.Printf("%14s %v", "p.GIAddr", p.GIAddr()) // 0.0.0.0
// log.Printf("%14s %v", "p.HLen", p.HLen()) // 6
// log.Printf("%14s %v", "p.HType", p.HType()) // 1
// log.Printf("%14s %v", "p.Hops", p.Hops()) // 0
// log.Printf("%14s %v", "p.OpCode", p.OpCode()) // BootRequest
// log.Printf("%14s %v", "p.Options", p.Options()) // [53 1 1 55 10 1 121 3 6 15 119 252 95 44 46 57 2 5 220 61 7 1 44 240 162 242 49 0 51 4 0 118 167 0 12 4 119 104 109 100 255 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
// log.Printf("%14s %v", "p.ParseOptions", p.ParseOptions()) // map[OptionParameterRequestList:[1 121 3 6 15 119 252 95 44 46] OptionDHCPMessageType:[1] OptionMaximumDHCPMessageSize:[5 220] OptionClientIdentifier:[1 44 240 162 242 49 0] OptionIPAddressLeaseTime:[0 118 167 0] OptionHostName:[119 104 109 100]]
// log.Printf("%14s %v", "p.SIAddr", p.SIAddr()) // 0.0.0.0
// log.Printf("%14s %v", "p.SName", p.SName()) // []
// log.Printf("%14s %v", "p.Secs", p.Secs()) // [0 8]
// log.Printf("%14s %v", "p.XId", p.XId()) // [211 184 20 44]
// log.Printf("%14s %v", "p.YIAddr", p.YIAddr()) // 0.0.0.0
switch msgType {
case dhcp4.Discover: // Broadcast Packet From Client - Can I have an IP?
// find a lease, but don't update lease time
log.Tracef("Got from client: Discover")
lease, err := s.reserveLease(p)
if err != nil {
log.Tracef("Couldn't find free lease: %s", err)
// couldn't find lease, don't respond
return nil
}
reply := dhcp4.ReplyPacket(p, dhcp4.Offer, s.ipnet.IP, lease.IP, s.leaseTime, s.leaseOptions.SelectOrderOrAll(options[dhcp4.OptionParameterRequestList]))
log.Tracef("Replying with offer: offered IP %v for %v with options %+v", lease.IP, s.leaseTime, reply.ParseOptions())
return reply
case dhcp4.Request: // Broadcast From Client - I'll take that IP (Also start for renewals)
// start/renew a lease -- update lease time
// some clients (OSX) just go right ahead and do Request first from previously known IP, if they get NAK, they restart full cycle with Discover then Request
log.Tracef("Got from client: Request")
if server, ok := options[dhcp4.OptionServerIdentifier]; ok && !net.IP(server).Equal(s.ipnet.IP) {
log.Tracef("Request message not for this DHCP server (%v vs %v)", server, s.ipnet.IP)
return nil // Message not for this dhcp server
}
reqIP := net.IP(options[dhcp4.OptionRequestedIPAddress])
if reqIP == nil {
reqIP = net.IP(p.CIAddr())
}
if reqIP.To4() == nil {
log.Tracef("Replying with NAK: request IP isn't valid IPv4: %s", reqIP)
return dhcp4.ReplyPacket(p, dhcp4.NAK, s.ipnet.IP, nil, 0, nil)
}
if reqIP.Equal(net.IPv4zero) {
log.Tracef("Replying with NAK: request IP is 0.0.0.0")
return dhcp4.ReplyPacket(p, dhcp4.NAK, s.ipnet.IP, nil, 0, nil)
}
log.Tracef("requested IP is %s", reqIP)
lease, err := s.reserveLease(p)
if err != nil {
log.Tracef("Couldn't find free lease: %s", err)
// couldn't find lease, don't respond
return nil
}
if lease.IP.Equal(reqIP) {
// IP matches lease IP, nothing else to do
lease.Expiry = time.Now().Add(s.leaseTime)
log.Tracef("Replying with ACK: request IP matches lease IP, nothing else to do. IP %v for %v", lease.IP, p.CHAddr())
return dhcp4.ReplyPacket(p, dhcp4.ACK, s.ipnet.IP, lease.IP, s.leaseTime, s.leaseOptions.SelectOrderOrAll(options[dhcp4.OptionParameterRequestList]))
}
//
// requested IP different from lease
//
log.Tracef("lease IP is different from requested IP: %s vs %s", lease.IP, reqIP)
hwaddr := s.getIPpool(reqIP)
if hwaddr == nil {
// not in pool, check if it's in DHCP range
if dhcp4.IPInRange(s.leaseStart, s.leaseStop, reqIP) {
// okay, we can give it to our client -- it's in our DHCP range and not taken, so let them use their IP
log.Tracef("Replying with ACK: request IP %v is not taken, so assigning lease IP %v to it, for %v", reqIP, lease.IP, p.CHAddr())
s.unreserveIP(lease.IP)
lease.IP = reqIP
s.reserveIP(reqIP, p.CHAddr())
lease.Expiry = time.Now().Add(s.leaseTime)
return dhcp4.ReplyPacket(p, dhcp4.ACK, s.ipnet.IP, lease.IP, s.leaseTime, s.leaseOptions.SelectOrderOrAll(options[dhcp4.OptionParameterRequestList]))
}
}
if hwaddr != nil && !bytes.Equal(hwaddr, lease.HWAddr) {
log.Printf("SHOULD NOT HAPPEN: IP pool hwaddr does not match lease hwaddr: %s vs %s", hwaddr, lease.HWAddr)
}
// requsted IP is not sufficient, reply with NAK
if hwaddr != nil {
log.Tracef("Replying with NAK: request IP %s is taken, asked by %v", reqIP, p.CHAddr())
return dhcp4.ReplyPacket(p, dhcp4.NAK, s.ipnet.IP, nil, 0, nil)
}
// requested IP is outside of DHCP range
log.Tracef("Replying with NAK: request IP %s is outside of DHCP range [%s, %s], asked by %v", reqIP, s.leaseStart, s.leaseStop, p.CHAddr())
return dhcp4.ReplyPacket(p, dhcp4.NAK, s.ipnet.IP, nil, 0, nil)
case dhcp4.Decline: // Broadcast From Client - Sorry I can't use that IP
log.Tracef("Got from client: Decline")
case dhcp4.Release: // From Client, I don't need that IP anymore
log.Tracef("Got from client: Release")
case dhcp4.Inform: // From Client, I have this IP and there's nothing you can do about it
log.Tracef("Got from client: Inform")
// do nothing
// from server -- ignore those but enumerate just in case
case dhcp4.Offer: // Broadcast From Server - Here's an IP
log.Printf("SHOULD NOT HAPPEN -- FROM ANOTHER DHCP SERVER: Offer")
case dhcp4.ACK: // From Server, Yes you can have that IP
log.Printf("SHOULD NOT HAPPEN -- FROM ANOTHER DHCP SERVER: ACK")
case dhcp4.NAK: // From Server, No you cannot have that IP
log.Printf("SHOULD NOT HAPPEN -- FROM ANOTHER DHCP SERVER: NAK")
default:
log.Printf("Unknown DHCP packet detected, ignoring: %v", msgType)
return nil
}
return nil
}
func (s *Server) Leases() []*Lease {
s.RLock()
result := s.leases
s.RUnlock()
return result
}

64
dhcpd/filter_conn.go Normal file
View File

@@ -0,0 +1,64 @@
package dhcpd
import (
"net"
"github.com/joomcode/errorx"
"golang.org/x/net/ipv4"
)
// filterConn listens to 0.0.0.0:67, but accepts packets only from specific interface
// This is neccessary for DHCP daemon to work, since binding to IP address doesn't
// us access to see Discover/Request packets from clients.
//
// TODO: on windows, controlmessage does not work, try to find out another way
// https://github.com/golang/net/blob/master/ipv4/payload.go#L13
type filterConn struct {
iface net.Interface
conn *ipv4.PacketConn
}
func newFilterConn(iface net.Interface, address string) (*filterConn, error) {
c, err := net.ListenPacket("udp4", address)
if err != nil {
return nil, errorx.Decorate(err, "Couldn't listen to %s on UDP4", address)
}
p := ipv4.NewPacketConn(c)
err = p.SetControlMessage(ipv4.FlagInterface, true)
if err != nil {
c.Close()
return nil, errorx.Decorate(err, "Couldn't set control message FlagInterface on connection")
}
return &filterConn{iface: iface, conn: p}, nil
}
func (f *filterConn) ReadFrom(b []byte) (int, net.Addr, error) {
for { // read until we find a suitable packet
n, cm, addr, err := f.conn.ReadFrom(b)
if err != nil {
return 0, addr, errorx.Decorate(err, "Error when reading from socket")
}
if cm == nil {
// no controlmessage was passed, so pass the packet to the caller
return n, addr, nil
}
if cm.IfIndex == f.iface.Index {
return n, addr, nil
}
// packet doesn't match criteria, drop it
}
return 0, nil, nil
}
func (f *filterConn) WriteTo(b []byte, addr net.Addr) (int, error) {
cm := ipv4.ControlMessage{
IfIndex: f.iface.Index,
}
return f.conn.WriteTo(b, &cm, addr)
}
func (f *filterConn) Close() error {
return f.conn.Close()
}

84
dhcpd/helpers.go Normal file
View File

@@ -0,0 +1,84 @@
package dhcpd
import (
"fmt"
"net"
"strings"
"github.com/hmage/golibs/log"
"github.com/joomcode/errorx"
)
func isTimeout(err error) bool {
operr, ok := err.(*net.OpError)
if !ok {
return false
}
return operr.Timeout()
}
// return first IPv4 address of an interface, if there is any
func getIfaceIPv4(iface *net.Interface) *net.IPNet {
ifaceAddrs, err := iface.Addrs()
if err != nil {
panic(err)
}
for _, addr := range ifaceAddrs {
ipnet, ok := addr.(*net.IPNet)
if !ok {
// not an IPNet, should not happen
log.Fatalf("SHOULD NOT HAPPEN: got iface.Addrs() element %s that is not net.IPNet", addr)
}
if ipnet.IP.To4() == nil {
log.Printf("Got IP that is not IPv4: %v", ipnet.IP)
continue
}
log.Printf("Got IP that is IPv4: %v", ipnet.IP)
return &net.IPNet{
IP: ipnet.IP.To4(),
Mask: ipnet.Mask,
}
}
return nil
}
func isConnClosed(err error) bool {
if err == nil {
return false
}
nerr, ok := err.(*net.OpError)
if !ok {
return false
}
if strings.Contains(nerr.Err.Error(), "use of closed network connection") {
return true
}
return false
}
func wrapErrPrint(err error, message string, args ...interface{}) error {
var errx error
if err == nil {
errx = fmt.Errorf(message, args...)
} else {
errx = errorx.Decorate(err, message, args...)
}
log.Println(errx.Error())
return errx
}
func parseIPv4(text string) (net.IP, error) {
result := net.ParseIP(text)
if result == nil {
return nil, fmt.Errorf("%s is not an IP address", text)
}
if result.To4() == nil {
return nil, fmt.Errorf("%s is not an IPv4 address", text)
}
return result.To4(), nil
}

111
dhcpd/standalone/main.go Normal file
View File

@@ -0,0 +1,111 @@
package main
import (
"net"
"os"
"os/signal"
"syscall"
"time"
"github.com/AdguardTeam/AdGuardHome/dhcpd"
"github.com/hmage/golibs/log"
"github.com/krolaw/dhcp4"
)
func main() {
if len(os.Args) < 2 {
log.Printf("Usage: %s <interface name>", os.Args[0])
os.Exit(64)
}
ifaceName := os.Args[1]
present, err := dhcpd.CheckIfOtherDHCPServersPresent(ifaceName)
if err != nil {
panic(err)
}
log.Printf("Found DHCP server? %v", present)
if present {
log.Printf("Will not start DHCP server because there's already running one on the network")
os.Exit(1)
}
iface, err := net.InterfaceByName(ifaceName)
if err != nil {
panic(err)
}
// get ipv4 address of an interface
ifaceIPNet := getIfaceIPv4(iface)
if ifaceIPNet == nil {
panic(err)
}
// append 10 to server's IP address as start
start := dhcp4.IPAdd(ifaceIPNet.IP, 10)
// lease range is 100 IP's, but TODO: don't go beyond end of subnet mask
stop := dhcp4.IPAdd(start, 100)
server := dhcpd.Server{}
config := dhcpd.ServerConfig{
InterfaceName: ifaceName,
RangeStart: start.String(),
RangeEnd: stop.String(),
SubnetMask: "255.255.255.0",
GatewayIP: "192.168.7.1",
}
log.Printf("Starting DHCP server")
err = server.Start(&config)
if err != nil {
panic(err)
}
time.Sleep(time.Second)
log.Printf("Stopping DHCP server")
err = server.Stop()
if err != nil {
panic(err)
}
log.Printf("Starting DHCP server")
err = server.Start(&config)
if err != nil {
panic(err)
}
log.Printf("Starting DHCP server while it's already running")
err = server.Start(&config)
if err != nil {
panic(err)
}
log.Printf("Now serving DHCP")
signal_channel := make(chan os.Signal)
signal.Notify(signal_channel, syscall.SIGINT, syscall.SIGTERM)
<-signal_channel
}
// return first IPv4 address of an interface, if there is any
func getIfaceIPv4(iface *net.Interface) *net.IPNet {
ifaceAddrs, err := iface.Addrs()
if err != nil {
panic(err)
}
for _, addr := range ifaceAddrs {
ipnet, ok := addr.(*net.IPNet)
if !ok {
// not an IPNet, should not happen
log.Fatalf("SHOULD NOT HAPPEN: got iface.Addrs() element %s that is not net.IPNet", addr)
}
if ipnet.IP.To4() == nil {
log.Printf("Got IP that is not IPv4: %v", ipnet.IP)
continue
}
log.Printf("Got IP that is IPv4: %v", ipnet.IP)
return &net.IPNet{
IP: ipnet.IP.To4(),
Mask: ipnet.Mask,
}
}
return nil
}

91
dns.go Normal file
View File

@@ -0,0 +1,91 @@
package main
import (
"fmt"
"net"
"github.com/AdguardTeam/AdGuardHome/dnsfilter"
"github.com/AdguardTeam/AdGuardHome/dnsforward"
"github.com/AdguardTeam/dnsproxy/upstream"
"github.com/hmage/golibs/log"
"github.com/joomcode/errorx"
)
var dnsServer = dnsforward.Server{}
func isRunning() bool {
return dnsServer.IsRunning()
}
func generateServerConfig() dnsforward.ServerConfig {
filters := []dnsfilter.Filter{}
userFilter := userFilter()
filters = append(filters, dnsfilter.Filter{
ID: userFilter.ID,
Rules: userFilter.Rules,
})
for _, filter := range config.Filters {
filters = append(filters, dnsfilter.Filter{
ID: filter.ID,
Rules: filter.Rules,
})
}
newconfig := dnsforward.ServerConfig{
UDPListenAddr: &net.UDPAddr{Port: config.DNS.Port},
FilteringConfig: config.DNS.FilteringConfig,
Filters: filters,
}
for _, u := range config.DNS.UpstreamDNS {
upstream, err := upstream.AddressToUpstream(u, config.DNS.BootstrapDNS, dnsforward.DefaultTimeout)
if err != nil {
log.Printf("Couldn't get upstream: %s", err)
// continue, just ignore the upstream
continue
}
newconfig.Upstreams = append(newconfig.Upstreams, upstream)
}
return newconfig
}
func startDNSServer() error {
if isRunning() {
return fmt.Errorf("Unable to start forwarding DNS server: Already running")
}
newconfig := generateServerConfig()
err := dnsServer.Start(&newconfig)
if err != nil {
return errorx.Decorate(err, "Couldn't start forwarding DNS server")
}
return nil
}
func reconfigureDNSServer() error {
if !isRunning() {
return fmt.Errorf("Refusing to reconfigure forwarding DNS server: not running")
}
config := generateServerConfig()
err := dnsServer.Reconfigure(&config)
if err != nil {
return errorx.Decorate(err, "Couldn't start forwarding DNS server")
}
return nil
}
func stopDNSServer() error {
if !isRunning() {
return fmt.Errorf("Refusing to stop forwarding DNS server: not running")
}
err := dnsServer.Stop()
if err != nil {
return errorx.Decorate(err, "Couldn't stop forwarding DNS server")
}
return nil
}

View File

@@ -8,7 +8,6 @@ import (
"errors"
"fmt"
"io/ioutil"
"log"
"net"
"net/http"
"regexp"
@@ -18,6 +17,7 @@ import (
"time"
"github.com/bluele/gcache"
"github.com/hmage/golibs/log"
"golang.org/x/net/publicsuffix"
)
@@ -38,21 +38,22 @@ var ErrInvalidSyntax = errors.New("dnsfilter: invalid rule syntax")
// ErrInvalidSyntax is returned by AddRule when the rule was already added to the filter
var ErrAlreadyExists = errors.New("dnsfilter: rule was already added")
// 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
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 {
parentalServer string
parentalSensitivity int // must be either 3, 10, 13 or 17
parentalEnabled bool
safeSearchEnabled bool
safeBrowsingEnabled bool
safeBrowsingServer string
// Config allows you to configure DNS filtering with New() or just change variables directly.
type Config struct {
ParentalSensitivity int `yaml:"parental_sensitivity"` // must be either 3, 10, 13 or 17
ParentalEnabled bool `yaml:"parental_enabled"`
SafeSearchEnabled bool `yaml:"safesearch_enabled"`
SafeBrowsingEnabled bool `yaml:"safebrowsing_enabled"`
}
type privateConfig struct {
parentalServer string // access via methods
safeBrowsingServer string // access via methods
}
type rule struct {
@@ -110,7 +111,13 @@ 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 // for direct access by library users, even a = assignment
privateConfig
}
type Filter struct {
ID int64 `json:"id"` // auto-assigned when filter is added (see nextFilterID), json by default keeps ID uppercase but we need lowercase
Rules []string `json:"-" yaml:"-"` // not in yaml or json
}
//go:generate stringer -type=Reason
@@ -171,7 +178,7 @@ func (d *Dnsfilter) CheckHost(host string) (Result, error) {
}
// check safebrowsing if no match
if d.config.safeBrowsingEnabled {
if d.SafeBrowsingEnabled {
result, err = d.checkSafeBrowsing(host)
if err != nil {
// failed to do HTTP lookup -- treat it as if we got empty response, but don't save cache
@@ -184,7 +191,7 @@ func (d *Dnsfilter) CheckHost(host string) (Result, error) {
}
// check parental if no match
if d.config.parentalEnabled {
if d.ParentalEnabled {
result, err = d.checkParental(host)
if err != nil {
// failed to do HTTP lookup -- treat it as if we got empty response, but don't save cache
@@ -225,13 +232,10 @@ func (r *rulesTable) Add(rule *rule) {
if rule.ip != nil {
// Hosts syntax
r.rulesByHost[rule.text] = rule
} else if //noinspection GoBoolExpressions
len(rule.shortcut) == shortcutLength && enableFastLookup {
} else if len(rule.shortcut) == shortcutLength && enableFastLookup {
// Adblock syntax with a shortcut
r.rulesByShortcut[rule.shortcut] = append(r.rulesByShortcut[rule.shortcut], rule)
} else {
// Adblock syntax -- too short to have a shortcut
r.rulesLeftovers = append(r.rulesLeftovers, rule)
}
@@ -239,7 +243,6 @@ func (r *rulesTable) Add(rule *rule) {
}
func (r *rulesTable) matchByHost(host string) (Result, error) {
// First: examine the hosts-syntax rules
res, err := r.searchByHost(host)
if err != nil {
@@ -271,7 +274,6 @@ func (r *rulesTable) matchByHost(host string) (Result, error) {
}
func (r *rulesTable) searchByHost(host string) (Result, error) {
rule, ok := r.rulesByHost[host]
if ok {
@@ -574,11 +576,11 @@ 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 {
if host == d.safeBrowsingServer {
return Result{}, nil
}
format := func(hashparam string) string {
url := fmt.Sprintf(defaultSafebrowsingURL, d.config.safeBrowsingServer, hashparam)
url := fmt.Sprintf(defaultSafebrowsingURL, d.safeBrowsingServer, hashparam)
return url
}
handleBody := func(body []byte, hashes map[string]bool) (Result, error) {
@@ -615,11 +617,11 @@ func (d *Dnsfilter) checkSafeBrowsing(host string) (Result, error) {
func (d *Dnsfilter) checkParental(host string) (Result, error) {
// prevent recursion -- checking the host of parental safety server makes no sense
if host == d.config.parentalServer {
if host == d.parentalServer {
return Result{}, nil
}
format := func(hashparam string) string {
url := fmt.Sprintf(defaultParentalURL, d.config.parentalServer, hashparam, d.config.parentalSensitivity)
url := fmt.Sprintf(defaultParentalURL, d.parentalServer, hashparam, d.ParentalSensitivity)
return url
}
handleBody := func(body []byte, hashes map[string]bool) (Result, error) {
@@ -732,6 +734,24 @@ func (d *Dnsfilter) lookupCommon(host string, lookupstats *LookupStats, cache gc
// Adding rule and matching against the rules
//
// AddRules is a convinience function to add an array of filters in one call
func (d *Dnsfilter) AddRules(filters []Filter) error {
for _, f := range filters {
for _, rule := range f.Rules {
err := d.AddRule(rule, f.ID)
if err == ErrAlreadyExists || err == ErrInvalidSyntax {
continue
}
if err != nil {
log.Printf("Cannot add rule %s: %s", rule, err)
// Just ignore invalid rules
continue
}
}
}
return nil
}
// AddRule adds a rule, checking if it is a valid rule first and if it wasn't added already
func (d *Dnsfilter) AddRule(input string, filterListID int64) error {
input = strings.TrimSpace(input)
@@ -773,7 +793,6 @@ func (d *Dnsfilter) AddRule(input string, filterListID int64) error {
rule.extractShortcut()
//noinspection GoBoolExpressions
if !enableDelayedCompilation {
err := rule.compile()
if err != nil {
@@ -852,7 +871,7 @@ func (d *Dnsfilter) matchHost(host string) (Result, error) {
//
// New creates properly initialized DNS Filter that is ready to be used
func New() *Dnsfilter {
func New(c *Config) *Dnsfilter {
d := new(Dnsfilter)
d.storage = make(map[string]bool)
@@ -873,8 +892,11 @@ func New() *Dnsfilter {
Transport: d.transport,
Timeout: defaultHTTPTimeout,
}
d.config.safeBrowsingServer = defaultSafebrowsingServer
d.config.parentalServer = defaultParentalServer
d.safeBrowsingServer = defaultSafebrowsingServer
d.parentalServer = defaultParentalServer
if c != nil {
d.Config = *c
}
return d
}
@@ -891,35 +913,21 @@ func (d *Dnsfilter) Destroy() {
// 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 {
// IsParentalSensitivityValid checks if sensitivity is valid value
func IsParentalSensitivityValid(sensitivity int) bool {
switch sensitivity {
case 3, 10, 13, 17:
d.config.parentalSensitivity = sensitivity
d.config.parentalEnabled = true
return nil
default:
return ErrInvalidParental
return true
}
}
// 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
return false
}
// SetSafeBrowsingServer lets you optionally change hostname of safesearch lookup
func (d *Dnsfilter) SetSafeBrowsingServer(host string) {
if len(host) == 0 {
d.config.safeBrowsingServer = defaultSafebrowsingServer
d.safeBrowsingServer = defaultSafebrowsingServer
} else {
d.config.safeBrowsingServer = host
d.safeBrowsingServer = host
}
}
@@ -935,7 +943,7 @@ func (d *Dnsfilter) ResetHTTPTimeout() {
// SafeSearchDomain returns replacement address for search engine
func (d *Dnsfilter) SafeSearchDomain(host string) (string, bool) {
if d.config.safeSearchEnabled {
if d.SafeSearchEnabled {
val, ok := safeSearchDomains[host]
return val, ok
}

View File

@@ -17,6 +17,7 @@ import (
"os"
"runtime"
"github.com/hmage/golibs/log"
"github.com/shirou/gopsutil/process"
"go.uber.org/goleak"
)
@@ -24,7 +25,7 @@ import (
// 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)
log.Tracef("RSS before loading rules - %d kB\n", start/1024)
dumpMemProfile(_Func() + "1.pprof")
d := NewForTest()
@@ -35,7 +36,7 @@ func TestLotsOfRulesMemoryUsage(t *testing.T) {
}
afterLoad := getRSS()
trace("RSS after loading rules - %d kB (%d kB diff)\n", afterLoad/1024, (afterLoad-start)/1024)
log.Tracef("RSS after loading rules - %d kB (%d kB diff)\n", afterLoad/1024, (afterLoad-start)/1024)
dumpMemProfile(_Func() + "2.pprof")
tests := []struct {
@@ -58,7 +59,7 @@ func TestLotsOfRulesMemoryUsage(t *testing.T) {
}
}
afterMatch := getRSS()
trace("RSS after matching - %d kB (%d kB diff)\n", afterMatch/1024, (afterMatch-afterLoad)/1024)
log.Tracef("RSS after matching - %d kB (%d kB diff)\n", afterMatch/1024, (afterMatch-afterLoad)/1024)
dumpMemProfile(_Func() + "3.pprof")
}
@@ -88,20 +89,20 @@ func dumpMemProfile(name string) {
const topHostsFilename = "../tests/top-1m.csv"
func fetchTopHostsFromNet() {
trace("Fetching top hosts from network")
log.Tracef("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")
log.Tracef("Reading zipfile body")
zipfile, err := ioutil.ReadAll(resp.Body)
if err != nil {
panic(err)
}
trace("Opening zipfile")
log.Tracef("Opening zipfile")
r, err := zip.NewReader(bytes.NewReader(zipfile), int64(len(zipfile)))
if err != nil {
panic(err)
@@ -111,19 +112,19 @@ func fetchTopHostsFromNet() {
panic(fmt.Errorf("zipfile must have only one entry: %+v", r))
}
f := r.File[0]
trace("Unpacking file %s from zipfile", f.Name)
log.Tracef("Unpacking file %s from zipfile", f.Name)
rc, err := f.Open()
if err != nil {
panic(err)
}
trace("Reading file %s contents", f.Name)
log.Tracef("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)
log.Tracef("Writing file %s contents to disk", f.Name)
err = ioutil.WriteFile(topHostsFilename+".tmp", body, 0644)
if err != nil {
panic(err)
@@ -144,16 +145,16 @@ func getTopHosts() {
func TestLotsOfRulesLotsOfHostsMemoryUsage(t *testing.T) {
start := getRSS()
trace("RSS before loading rules - %d kB\n", start/1024)
log.Tracef("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())
log.Tracef("Have %d rules", d.Count())
afterLoad := getRSS()
trace("RSS after loading rules - %d kB (%d kB diff)\n", afterLoad/1024, (afterLoad-start)/1024)
log.Tracef("RSS after loading rules - %d kB (%d kB diff)\n", afterLoad/1024, (afterLoad-start)/1024)
dumpMemProfile(_Func() + "2.pprof")
getTopHosts()
@@ -163,7 +164,7 @@ func TestLotsOfRulesLotsOfHostsMemoryUsage(t *testing.T) {
}
defer hostnames.Close()
afterHosts := getRSS()
trace("RSS after loading hosts - %d kB (%d kB diff)\n", afterHosts/1024, (afterHosts-afterLoad)/1024)
log.Tracef("RSS after loading hosts - %d kB (%d kB diff)\n", afterHosts/1024, (afterHosts-afterLoad)/1024)
dumpMemProfile(_Func() + "2.pprof")
{
@@ -182,7 +183,7 @@ func TestLotsOfRulesLotsOfHostsMemoryUsage(t *testing.T) {
}
afterMatch := getRSS()
trace("RSS after matching - %d kB (%d kB diff)\n", afterMatch/1024, (afterMatch-afterLoad)/1024)
log.Tracef("RSS after matching - %d kB (%d kB diff)\n", afterMatch/1024, (afterMatch-afterLoad)/1024)
dumpMemProfile(_Func() + "3.pprof")
}
@@ -236,7 +237,7 @@ func TestSuffixRule(t *testing.T) {
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)
// log.Tracef("\"%s\": %v: %s", testcase.rule, isSuffix, suffix)
}
}
@@ -338,7 +339,7 @@ func mustLoadTestRules(d *Dnsfilter) {
}
func NewForTest() *Dnsfilter {
d := New()
d := New(nil)
purgeCaches()
return d
}
@@ -542,7 +543,7 @@ func TestSafeBrowsing(t *testing.T) {
t.Run(fmt.Sprintf("%s in %s", tc, _Func()), func(t *testing.T) {
d := NewForTest()
defer d.Destroy()
d.EnableSafeBrowsing()
d.SafeBrowsingEnabled = true
stats.Safebrowsing.Requests = 0
d.checkMatch(t, "wmconvirus.narod.ru")
d.checkMatch(t, "wmconvirus.narod.ru")
@@ -570,7 +571,7 @@ func TestSafeBrowsing(t *testing.T) {
func TestParallelSB(t *testing.T) {
d := NewForTest()
defer d.Destroy()
d.EnableSafeBrowsing()
d.SafeBrowsingEnabled = true
t.Run("group", func(t *testing.T) {
for i := 0; i < 100; i++ {
t.Run(fmt.Sprintf("aaa%d", i), func(t *testing.T) {
@@ -597,7 +598,7 @@ func TestSafeBrowsingCustomServerFail(t *testing.T) {
defer ts.Close()
address := ts.Listener.Addr().String()
d.EnableSafeBrowsing()
d.SafeBrowsingEnabled = true
d.SetHTTPTimeout(time.Second * 5)
d.SetSafeBrowsingServer(address) // this will ensure that test fails
d.checkMatchEmpty(t, "wmconvirus.narod.ru")
@@ -606,7 +607,8 @@ func TestSafeBrowsingCustomServerFail(t *testing.T) {
func TestParentalControl(t *testing.T) {
d := NewForTest()
defer d.Destroy()
d.EnableParental(3)
d.ParentalEnabled = true
d.ParentalSensitivity = 3
d.checkMatch(t, "pornhub.com")
d.checkMatch(t, "pornhub.com")
if stats.Parental.Requests != 1 {
@@ -637,7 +639,7 @@ func TestSafeSearch(t *testing.T) {
if ok {
t.Errorf("Expected safesearch to error when disabled")
}
d.EnableSafeSearch()
d.SafeSearchEnabled = true
val, ok := d.SafeSearchDomain("www.google.com")
if !ok {
t.Errorf("Expected safesearch to find result for www.google.com")
@@ -924,7 +926,7 @@ func BenchmarkLotsOfRulesLotsOfHostsParallel(b *testing.B) {
func BenchmarkSafeBrowsing(b *testing.B) {
d := NewForTest()
defer d.Destroy()
d.EnableSafeBrowsing()
d.SafeBrowsingEnabled = true
for n := 0; n < b.N; n++ {
hostname := "wmconvirus.narod.ru"
ret, err := d.CheckHost(hostname)
@@ -940,7 +942,7 @@ func BenchmarkSafeBrowsing(b *testing.B) {
func BenchmarkSafeBrowsingParallel(b *testing.B) {
d := NewForTest()
defer d.Destroy()
d.EnableSafeBrowsing()
d.SafeBrowsingEnabled = true
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
hostname := "wmconvirus.narod.ru"
@@ -958,7 +960,7 @@ func BenchmarkSafeBrowsingParallel(b *testing.B) {
func BenchmarkSafeSearch(b *testing.B) {
d := NewForTest()
defer d.Destroy()
d.EnableSafeSearch()
d.SafeSearchEnabled = true
for n := 0; n < b.N; n++ {
val, ok := d.SafeSearchDomain("www.google.com")
if !ok {
@@ -973,7 +975,7 @@ func BenchmarkSafeSearch(b *testing.B) {
func BenchmarkSafeSearchParallel(b *testing.B) {
d := NewForTest()
defer d.Destroy()
d.EnableSafeSearch()
d.SafeSearchEnabled = true
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
val, ok := d.SafeSearchDomain("www.google.com")
@@ -1009,17 +1011,3 @@ func _Func() string {
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())
}

400
dnsforward/dnsforward.go Normal file
View File

@@ -0,0 +1,400 @@
package dnsforward
import (
"errors"
"fmt"
"net"
"strings"
"sync"
"time"
"github.com/AdguardTeam/AdGuardHome/dnsfilter"
"github.com/AdguardTeam/dnsproxy/proxy"
"github.com/AdguardTeam/dnsproxy/upstream"
"github.com/hmage/golibs/log"
"github.com/joomcode/errorx"
"github.com/miekg/dns"
)
// DefaultTimeout is the default upstream timeout
const DefaultTimeout = 10 * time.Second
const (
safeBrowsingBlockHost = "standard-block.dns.adguard.com"
parentalBlockHost = "family-block.dns.adguard.com"
)
// Server is the main way to start a DNS server.
//
// Example:
// s := dnsforward.Server{}
// err := s.Start(nil) // will start a DNS server listening on default port 53, in a goroutine
// err := s.Reconfigure(ServerConfig{UDPListenAddr: &net.UDPAddr{Port: 53535}}) // will reconfigure running DNS server to listen on UDP port 53535
// err := s.Stop() // will stop listening on port 53535 and cancel all goroutines
// err := s.Start(nil) // will start listening again, on port 53535, in a goroutine
//
// The zero Server is empty and ready for use.
type Server struct {
dnsProxy *proxy.Proxy // DNS proxy instance
dnsFilter *dnsfilter.Dnsfilter // DNS filter instance
sync.RWMutex
ServerConfig
}
// FilteringConfig represents the DNS filtering configuration of AdGuard Home
type FilteringConfig struct {
ProtectionEnabled bool `yaml:"protection_enabled"` // whether or not use any of dnsfilter features
FilteringEnabled bool `yaml:"filtering_enabled"` // whether or not use filter lists
BlockedResponseTTL uint32 `yaml:"blocked_response_ttl"` // if 0, then default is used (3600)
QueryLogEnabled bool `yaml:"querylog_enabled"`
Ratelimit int `yaml:"ratelimit"`
RatelimitWhitelist []string `yaml:"ratelimit_whitelist"`
RefuseAny bool `yaml:"refuse_any"`
BootstrapDNS string `yaml:"bootstrap_dns"`
dnsfilter.Config `yaml:",inline"`
}
// ServerConfig represents server configuration.
// The zero ServerConfig is empty and ready for use.
type ServerConfig struct {
UDPListenAddr *net.UDPAddr // UDP listen address
Upstreams []upstream.Upstream // Configured upstreams
Filters []dnsfilter.Filter // A list of filters to use
FilteringConfig
}
// if any of ServerConfig values are zero, then default values from below are used
var defaultValues = ServerConfig{
UDPListenAddr: &net.UDPAddr{Port: 53},
FilteringConfig: FilteringConfig{BlockedResponseTTL: 3600},
}
func init() {
defaultDNS := []string{"8.8.8.8:53", "8.8.4.4:53"}
defaultUpstreams := make([]upstream.Upstream, 0)
for _, addr := range defaultDNS {
u, err := upstream.AddressToUpstream(addr, "", DefaultTimeout)
if err == nil {
defaultUpstreams = append(defaultUpstreams, u)
}
}
defaultValues.Upstreams = defaultUpstreams
}
// Start starts the DNS server
func (s *Server) Start(config *ServerConfig) error {
s.Lock()
defer s.Unlock()
return s.startInternal(config)
}
// startInternal starts without locking
func (s *Server) startInternal(config *ServerConfig) error {
if config != nil {
s.ServerConfig = *config
}
if s.dnsFilter != nil || s.dnsProxy != nil {
return errors.New("DNS server is already started")
}
err := s.initDNSFilter()
if err != nil {
return err
}
log.Printf("Loading stats from querylog")
err = fillStatsFromQueryLog()
if err != nil {
return errorx.Decorate(err, "failed to load stats from querylog")
}
once.Do(func() {
go periodicQueryLogRotate()
go periodicHourlyTopRotate()
go statsRotator()
})
// TODO: Add TCPListenAddr
proxyConfig := proxy.Config{
UDPListenAddr: s.UDPListenAddr,
Ratelimit: s.Ratelimit,
RatelimitWhitelist: s.RatelimitWhitelist,
RefuseAny: s.RefuseAny,
CacheEnabled: true,
Upstreams: s.Upstreams,
Handler: s.handleDNSRequest,
}
if proxyConfig.UDPListenAddr == nil {
proxyConfig.UDPListenAddr = defaultValues.UDPListenAddr
}
if len(proxyConfig.Upstreams) == 0 {
proxyConfig.Upstreams = defaultValues.Upstreams
}
// Initialize and start the DNS proxy
s.dnsProxy = &proxy.Proxy{Config: proxyConfig}
return s.dnsProxy.Start()
}
// Initializes the DNS filter
func (s *Server) initDNSFilter() error {
log.Printf("Creating dnsfilter")
s.dnsFilter = dnsfilter.New(&s.Config)
// add rules only if they are enabled
if s.FilteringEnabled {
err := s.dnsFilter.AddRules(s.Filters)
if err != nil {
return errorx.Decorate(err, "could not initialize dnsfilter")
}
}
return nil
}
// Stop stops the DNS server
func (s *Server) Stop() error {
s.Lock()
defer s.Unlock()
return s.stopInternal()
}
// stopInternal stops without locking
func (s *Server) stopInternal() error {
if s.dnsProxy != nil {
err := s.dnsProxy.Stop()
s.dnsProxy = nil
if err != nil {
return errorx.Decorate(err, "could not stop the DNS server properly")
}
}
if s.dnsFilter != nil {
s.dnsFilter.Destroy()
s.dnsFilter = nil
}
// flush remainder to file
logBufferLock.Lock()
flushBuffer := logBuffer
logBuffer = nil
logBufferLock.Unlock()
err := flushToFile(flushBuffer)
if err != nil {
log.Printf("Saving querylog to file failed: %s", err)
return err
}
return nil
}
// IsRunning returns true if the DNS server is running
func (s *Server) IsRunning() bool {
s.RLock()
isRunning := true
if s.dnsProxy == nil {
isRunning = false
}
s.RUnlock()
return isRunning
}
// Reconfigure applies the new configuration to the DNS server
func (s *Server) Reconfigure(config *ServerConfig) error {
s.Lock()
defer s.Unlock()
log.Print("Start reconfiguring the server")
err := s.stopInternal()
if err != nil {
return errorx.Decorate(err, "could not reconfigure the server")
}
err = s.startInternal(config)
if err != nil {
return errorx.Decorate(err, "could not reconfigure the server")
}
return nil
}
// handleDNSRequest filters the incoming DNS requests and writes them to the query log
func (s *Server) handleDNSRequest(p *proxy.Proxy, d *proxy.DNSContext) error {
start := time.Now()
// use dnsfilter before cache -- changed settings or filters would require cache invalidation otherwise
res, err := s.filterDNSRequest(d)
if err != nil {
return err
}
if d.Res == nil {
// request was not filtered so let it be processed further
err = p.Resolve(d)
if err != nil {
return err
}
}
shouldLog := true
msg := d.Req
// don't log ANY request if refuseAny is enabled
if len(msg.Question) >= 1 && msg.Question[0].Qtype == dns.TypeANY && s.RefuseAny {
shouldLog = false
}
if s.QueryLogEnabled && shouldLog {
elapsed := time.Since(start)
upstreamAddr := ""
if d.Upstream != nil {
upstreamAddr = d.Upstream.Address()
}
logRequest(msg, d.Res, res, elapsed, d.Addr, upstreamAddr)
}
return nil
}
// filterDNSRequest applies the dnsFilter and sets d.Res if the request was filtered
func (s *Server) filterDNSRequest(d *proxy.DNSContext) (*dnsfilter.Result, error) {
msg := d.Req
host := strings.TrimSuffix(msg.Question[0].Name, ".")
s.RLock()
protectionEnabled := s.ProtectionEnabled
dnsFilter := s.dnsFilter
s.RUnlock()
if !protectionEnabled {
return nil, nil
}
var res dnsfilter.Result
var err error
res, err = dnsFilter.CheckHost(host)
if err != nil {
// Return immediately if there's an error
return nil, errorx.Decorate(err, "dnsfilter failed to check host '%s'", host)
} else if res.IsFiltered {
// log.Tracef("Host %s is filtered, reason - '%s', matched rule: '%s'", host, res.Reason, res.Rule)
d.Res = s.genDNSFilterMessage(d, &res)
}
return &res, err
}
// genDNSFilterMessage generates a DNS message corresponding to the filtering result
func (s *Server) genDNSFilterMessage(d *proxy.DNSContext, result *dnsfilter.Result) *dns.Msg {
m := d.Req
if m.Question[0].Qtype != dns.TypeA {
return s.genNXDomain(m)
}
switch result.Reason {
case dnsfilter.FilteredSafeBrowsing:
return s.genBlockedHost(m, safeBrowsingBlockHost, d.Upstream)
case dnsfilter.FilteredParental:
return s.genBlockedHost(m, parentalBlockHost, d.Upstream)
default:
if result.Ip != nil {
return s.genARecord(m, result.Ip)
}
return s.genNXDomain(m)
}
}
func (s *Server) genServerFailure(request *dns.Msg) *dns.Msg {
resp := dns.Msg{}
resp.SetRcode(request, dns.RcodeServerFailure)
resp.RecursionAvailable = true
return &resp
}
func (s *Server) genARecord(request *dns.Msg, ip net.IP) *dns.Msg {
resp := dns.Msg{}
resp.SetReply(request)
answer, err := dns.NewRR(fmt.Sprintf("%s %d A %s", request.Question[0].Name, s.BlockedResponseTTL, ip.String()))
if err != nil {
log.Printf("Couldn't generate A record for replacement host '%s': %s", ip.String(), err)
return s.genServerFailure(request)
}
resp.Answer = append(resp.Answer, answer)
return &resp
}
func (s *Server) genBlockedHost(request *dns.Msg, newAddr string, upstream upstream.Upstream) *dns.Msg {
// look up the hostname, TODO: cache
replReq := dns.Msg{}
replReq.SetQuestion(dns.Fqdn(newAddr), request.Question[0].Qtype)
replReq.RecursionDesired = true
reply, err := upstream.Exchange(&replReq)
if err != nil {
log.Printf("Couldn't look up replacement host '%s' on upstream %s: %s", newAddr, upstream.Address(), err)
return s.genServerFailure(request)
}
resp := dns.Msg{}
resp.SetReply(request)
resp.Authoritative, resp.RecursionAvailable = true, true
if reply != nil {
for _, answer := range reply.Answer {
answer.Header().Name = request.Question[0].Name
resp.Answer = append(resp.Answer, answer)
}
}
return &resp
}
func (s *Server) genNXDomain(request *dns.Msg) *dns.Msg {
resp := dns.Msg{}
resp.SetRcode(request, dns.RcodeNameError)
resp.RecursionAvailable = true
resp.Ns = s.genSOA(request)
return &resp
}
func (s *Server) genSOA(request *dns.Msg) []dns.RR {
zone := ""
if len(request.Question) > 0 {
zone = request.Question[0].Name
}
soa := dns.SOA{
// values copied from verisign's nonexistent .com domain
// their exact values are not important in our use case because they are used for domain transfers between primary/secondary DNS servers
Refresh: 1800,
Retry: 900,
Expire: 604800,
Minttl: 86400,
// copied from AdGuard DNS
Ns: "fake-for-negative-caching.adguard.com.",
Serial: 100500,
// rest is request-specific
Hdr: dns.RR_Header{
Name: zone,
Rrtype: dns.TypeSOA,
Ttl: s.BlockedResponseTTL,
Class: dns.ClassINET,
},
Mbox: "hostmaster.", // zone will be appended later if it's not empty or "."
}
if soa.Hdr.Ttl == 0 {
soa.Hdr.Ttl = defaultValues.BlockedResponseTTL
}
if len(zone) > 0 && zone[0] != '.' {
soa.Mbox += zone
}
return []dns.RR{&soa}
}
var once sync.Once

View File

@@ -0,0 +1,214 @@
package dnsforward
import (
"net"
"testing"
"time"
"github.com/AdguardTeam/AdGuardHome/dnsfilter"
"github.com/miekg/dns"
)
func TestServer(t *testing.T) {
s := Server{}
s.UDPListenAddr = &net.UDPAddr{Port: 0}
err := s.Start(nil)
if err != nil {
t.Fatalf("Failed to start server: %s", err)
}
// server is running, send a message
addr := s.dnsProxy.Addr("udp")
req := dns.Msg{}
req.Id = dns.Id()
req.RecursionDesired = true
req.Question = []dns.Question{
{Name: "google-public-dns-a.google.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET},
}
reply, err := dns.Exchange(&req, addr.String())
if err != nil {
t.Fatalf("Couldn't talk to server %s: %s", addr, err)
}
if len(reply.Answer) != 1 {
t.Fatalf("DNS server %s returned reply with wrong number of answers - %d", addr, len(reply.Answer))
}
if a, ok := reply.Answer[0].(*dns.A); ok {
if !net.IPv4(8, 8, 8, 8).Equal(a.A) {
t.Fatalf("DNS server %s returned wrong answer instead of 8.8.8.8: %v", addr, a.A)
}
} else {
t.Fatalf("DNS server %s returned wrong answer type instead of A: %v", addr, reply.Answer[0])
}
err = s.Stop()
if err != nil {
t.Fatalf("DNS server failed to stop: %s", err)
}
}
func TestInvalidRequest(t *testing.T) {
s := Server{}
s.UDPListenAddr = &net.UDPAddr{Port: 0}
err := s.Start(nil)
if err != nil {
t.Fatalf("Failed to start server: %s", err)
}
// server is running, send a message
addr := s.dnsProxy.Addr("udp")
req := dns.Msg{}
req.Id = dns.Id()
req.RecursionDesired = true
// send a DNS request without question
client := dns.Client{Net: "udp", Timeout: 500 * time.Millisecond}
_, _, err = client.Exchange(&req, addr.String())
if err != nil {
t.Fatalf("got a response to an invalid query")
}
err = s.Stop()
if err != nil {
t.Fatalf("DNS server failed to stop: %s", err)
}
}
func TestBlockedRequest(t *testing.T) {
s := createTestServer()
err := s.Start(nil)
if err != nil {
t.Fatalf("Failed to start server: %s", err)
}
addr := s.dnsProxy.Addr("udp")
//
// NXDomain blocking
//
req := dns.Msg{}
req.Id = dns.Id()
req.RecursionDesired = true
req.Question = []dns.Question{
{Name: "nxdomain.example.org.", Qtype: dns.TypeA, Qclass: dns.ClassINET},
}
reply, err := dns.Exchange(&req, addr.String())
if err != nil {
t.Fatalf("Couldn't talk to server %s: %s", addr, err)
}
if reply.Rcode != dns.RcodeNameError {
t.Fatalf("Wrong response: %s", reply.String())
}
err = s.Stop()
if err != nil {
t.Fatalf("DNS server failed to stop: %s", err)
}
}
func TestBlockedByHosts(t *testing.T) {
s := createTestServer()
err := s.Start(nil)
if err != nil {
t.Fatalf("Failed to start server: %s", err)
}
addr := s.dnsProxy.Addr("udp")
//
// Hosts blocking
//
req := dns.Msg{}
req.Id = dns.Id()
req.RecursionDesired = true
req.Question = []dns.Question{
{Name: "host.example.org.", Qtype: dns.TypeA, Qclass: dns.ClassINET},
}
reply, err := dns.Exchange(&req, addr.String())
if err != nil {
t.Fatalf("Couldn't talk to server %s: %s", addr, err)
}
if len(reply.Answer) != 1 {
t.Fatalf("DNS server %s returned reply with wrong number of answers - %d", addr, len(reply.Answer))
}
if a, ok := reply.Answer[0].(*dns.A); ok {
if !net.IPv4(127, 0, 0, 1).Equal(a.A) {
t.Fatalf("DNS server %s returned wrong answer instead of 8.8.8.8: %v", addr, a.A)
}
} else {
t.Fatalf("DNS server %s returned wrong answer type instead of A: %v", addr, reply.Answer[0])
}
err = s.Stop()
if err != nil {
t.Fatalf("DNS server failed to stop: %s", err)
}
}
func TestBlockedBySafeBrowsing(t *testing.T) {
s := createTestServer()
err := s.Start(nil)
if err != nil {
t.Fatalf("Failed to start server: %s", err)
}
addr := s.dnsProxy.Addr("udp")
//
// Safebrowsing blocking
//
req := dns.Msg{}
req.Id = dns.Id()
req.RecursionDesired = true
req.Question = []dns.Question{
{Name: "wmconvirus.narod.ru.", Qtype: dns.TypeA, Qclass: dns.ClassINET},
}
reply, err := dns.Exchange(&req, addr.String())
if err != nil {
t.Fatalf("Couldn't talk to server %s: %s", addr, err)
}
if len(reply.Answer) != 1 {
t.Fatalf("DNS server %s returned reply with wrong number of answers - %d", addr, len(reply.Answer))
}
if a, ok := reply.Answer[0].(*dns.A); ok {
addrs, lookupErr := net.LookupHost(safeBrowsingBlockHost)
if lookupErr != nil {
t.Fatalf("cannot resolve %s due to %s", safeBrowsingBlockHost, lookupErr)
}
found := false
for _, blockAddr := range addrs {
if blockAddr == a.A.String() {
found = true
}
}
if !found {
t.Fatalf("DNS server %s returned wrong answer: %v", addr, a.A)
}
} else {
t.Fatalf("DNS server %s returned wrong answer type instead of A: %v", addr, reply.Answer[0])
}
err = s.Stop()
if err != nil {
t.Fatalf("DNS server failed to stop: %s", err)
}
}
func createTestServer() *Server {
s := Server{}
s.UDPListenAddr = &net.UDPAddr{Port: 0}
s.FilteringConfig.FilteringEnabled = true
s.FilteringConfig.ProtectionEnabled = true
s.FilteringConfig.SafeBrowsingEnabled = true
s.Filters = make([]dnsfilter.Filter, 0)
rules := []string{
"||nxdomain.example.org^",
"127.0.0.1 host.example.org",
}
filter := dnsfilter.Filter{ID: 1, Rules: rules}
s.Filters = append(s.Filters, filter)
return &s
}

View File

@@ -1,20 +1,17 @@
package dnsfilter
package dnsforward
import (
"encoding/json"
"fmt"
"log"
"net"
"net/http"
"os"
"path"
"runtime"
"strconv"
"strings"
"sync"
"time"
"github.com/AdguardTeam/AdGuardHome/dnsfilter"
"github.com/coredns/coredns/plugin/pkg/response"
"github.com/hmage/golibs/log"
"github.com/miekg/dns"
)
@@ -42,12 +39,14 @@ type logEntry struct {
Time time.Time
Elapsed time.Duration
IP string
Upstream string `json:",omitempty"` // if empty, means it was cached
}
func logRequest(question *dns.Msg, answer *dns.Msg, result dnsfilter.Result, elapsed time.Duration, ip string) {
func logRequest(question *dns.Msg, answer *dns.Msg, result *dnsfilter.Result, elapsed time.Duration, addr net.Addr, upstream string) {
var q []byte
var a []byte
var err error
ip := getIPString(addr)
if question != nil {
q, err = question.Pack()
@@ -56,6 +55,7 @@ func logRequest(question *dns.Msg, answer *dns.Msg, result dnsfilter.Result, ela
return
}
}
if answer != nil {
a, err = answer.Pack()
if err != nil {
@@ -64,14 +64,19 @@ func logRequest(question *dns.Msg, answer *dns.Msg, result dnsfilter.Result, ela
}
}
if result == nil {
result = &dnsfilter.Result{}
}
now := time.Now()
entry := logEntry{
Question: q,
Answer: a,
Result: result,
Result: *result,
Time: now,
Elapsed: elapsed,
IP: ip,
Upstream: upstream,
}
var flushBuffer []*logEntry
@@ -97,6 +102,8 @@ func logRequest(question *dns.Msg, answer *dns.Msg, result dnsfilter.Result, ela
// don't do failure, just log
}
incrementCounters(&entry)
// if buffer needs to be flushed to disk, do it now
if len(flushBuffer) > 0 {
// write to file
@@ -105,7 +112,6 @@ func logRequest(question *dns.Msg, answer *dns.Msg, result dnsfilter.Result, ela
}
}
//noinspection GoUnusedParameter
func HandleQueryLog(w http.ResponseWriter, r *http.Request) {
queryLogLock.RLock()
values := make([]*logEntry, len(queryLogCache))
@@ -140,10 +146,10 @@ func HandleQueryLog(w http.ResponseWriter, r *http.Request) {
}
jsonEntry := map[string]interface{}{
"reason": entry.Result.Reason.String(),
"reason": entry.Result.Reason.String(),
"elapsedMs": strconv.FormatFloat(entry.Elapsed.Seconds()*1000, 'f', -1, 64),
"time": entry.Time.Format(time.RFC3339),
"client": entry.IP,
"time": entry.Time.Format(time.RFC3339),
"client": entry.IP,
}
if q != nil {
jsonEntry["question"] = map[string]interface{}{
@@ -154,8 +160,7 @@ func HandleQueryLog(w http.ResponseWriter, r *http.Request) {
}
if a != nil {
status, _ := response.Typify(a, time.Now().UTC())
jsonEntry["status"] = status.String()
jsonEntry["status"] = dns.RcodeToString[a.Rcode]
}
if len(entry.Result.Rule) > 0 {
jsonEntry["rule"] = entry.Result.Rule
@@ -225,16 +230,13 @@ func HandleQueryLog(w http.ResponseWriter, r *http.Request) {
}
}
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')
// getIPString is a helper function that extracts IP address from net.Addr
func getIPString(addr net.Addr) string {
switch addr := addr.(type) {
case *net.UDPAddr:
return addr.IP.String()
case *net.TCPAddr:
return addr.IP.String()
}
fmt.Fprint(os.Stderr, buf.String())
return ""
}

View File

@@ -1,16 +1,16 @@
package dnsfilter
package dnsforward
import (
"bytes"
"compress/gzip"
"encoding/json"
"fmt"
"log"
"os"
"sync"
"time"
"github.com/go-test/deep"
"github.com/hmage/golibs/log"
)
var (
@@ -191,15 +191,12 @@ func genericLoader(onEntry func(entry *logEntry) error, needMore func() bool, ti
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)
@@ -224,7 +221,7 @@ func genericLoader(onEntry func(entry *logEntry) error, needMore func() bool, ti
}
if now.Sub(entry.Time) > timeWindow {
// trace("skipping entry") // debug logging
// log.Tracef("skipping entry") // debug logging
continue
}
@@ -251,41 +248,3 @@ func genericLoader(onEntry func(entry *logEntry) error, needMore func() bool, ti
}
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

@@ -1,9 +1,8 @@
package dnsfilter
package dnsforward
import (
"bytes"
"fmt"
"log"
"net/http"
"os"
"path"
@@ -14,8 +13,8 @@ import (
"sync"
"time"
"github.com/AdguardTeam/AdGuardHome/dnsfilter"
"github.com/bluele/gcache"
"github.com/hmage/golibs/log"
"github.com/miekg/dns"
)
@@ -158,6 +157,11 @@ func (r *dayTop) addEntry(entry *logEntry, q *dns.Msg, now time.Time) error {
return nil
}
// if a DNS query doesn't have questions, do nothing
if len(q.Question) == 0 {
return nil
}
hostname := strings.ToLower(strings.TrimSuffix(q.Question[0].Name, "."))
// get value, if not set, crate one
@@ -231,27 +235,7 @@ func fillStatsFromQueryLog() error {
}
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)
incrementCounters(entry)
return nil
}

View File

@@ -1,28 +1,27 @@
package dnsfilter
package dnsforward
import (
"encoding/json"
"fmt"
"log"
"net/http"
"sync"
"time"
"github.com/coredns/coredns/plugin"
"github.com/prometheus/client_golang/prometheus"
"github.com/AdguardTeam/AdGuardHome/dnsfilter"
"github.com/hmage/golibs/log"
)
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.")
requests = newDNSCounter("requests_total")
filtered = newDNSCounter("filtered_total")
filteredLists = newDNSCounter("filtered_lists_total")
filteredSafebrowsing = newDNSCounter("filtered_safebrowsing_total")
filteredParental = newDNSCounter("filtered_parental_total")
filteredInvalid = newDNSCounter("filtered_invalid_total")
whitelisted = newDNSCounter("whitelisted_total")
safesearch = newDNSCounter("safesearch_total")
errorsTotal = newDNSCounter("errors_total")
elapsedTime = newDNSHistogram("request_duration")
)
// entries for single time period (for example all per-second entries)
@@ -70,7 +69,7 @@ func purgeStats() {
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)
// log.Tracef("%s: %v as %v -> [%v]", name, time.Since(when), p.period, elapsed)
if elapsed >= statsHistoryElements {
return // outside of our timeframe
}
@@ -84,7 +83,7 @@ func (p *periodicStats) Inc(name string, when time.Time) {
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)
// log.Tracef("%s: %v as %v -> [%v]", name, time.Since(when), p.period, elapsed)
if elapsed >= statsHistoryElements {
return // outside of our timeframe
}
@@ -93,7 +92,7 @@ func (p *periodicStats) Observe(name string, when time.Time, value float64) {
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)
// log.Tracef("Will change p.Entries[%s][%d] from %v to %v", countname, elapsed, value, value+1)
value += 1
currentValues[elapsed] = value
p.Entries[countname] = currentValues
@@ -143,21 +142,15 @@ func statsRotator() {
type counter struct {
name string // used as key in periodic stats
value int64
prom prometheus.Counter
sync.Mutex
}
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 newDNSCounter(name string) *counter {
// log.Tracef("called")
return &counter{
name: name,
}
}
func (c *counter) IncWithTime(when time.Time) {
@@ -165,41 +158,27 @@ func (c *counter) IncWithTime(when time.Time) {
statistics.PerMinute.Inc(c.name, when)
statistics.PerHour.Inc(c.name, when)
statistics.PerDay.Inc(c.name, when)
c.Lock()
c.value++
c.prom.Inc()
c.Unlock()
}
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
sync.Mutex
}
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 newDNSHistogram(name string) *histogram {
return &histogram{
name: name,
}
}
func (h *histogram) ObserveWithTime(value float64, when time.Time) {
@@ -207,26 +186,44 @@ func (h *histogram) ObserveWithTime(value float64, when time.Time) {
statistics.PerMinute.Observe(h.name, when, value)
statistics.PerHour.Observe(h.name, when, value)
statistics.PerDay.Observe(h.name, when, value)
h.Lock()
h.count++
h.total += value
h.prom.Observe(value)
h.Unlock()
}
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 incrementCounters(entry *logEntry) {
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)
}
func HandleStats(w http.ResponseWriter, r *http.Request) {
const numHours = 24
histrical := generateMapFromStats(&statistics.PerHour, 0, numHours)

251
filter.go Normal file
View File

@@ -0,0 +1,251 @@
package main
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"reflect"
"regexp"
"strconv"
"strings"
"time"
"github.com/AdguardTeam/AdGuardHome/dnsfilter"
"github.com/hmage/golibs/log"
)
var (
nextFilterID = time.Now().Unix() // semi-stable way to generate an unique ID
filterTitleRegexp = regexp.MustCompile(`^! Title: +(.*)$`)
)
// field ordering is important -- yaml fields will mirror ordering from here
type filter struct {
Enabled bool `json:"enabled"`
URL string `json:"url"`
Name string `json:"name" yaml:"name"`
RulesCount int `json:"rulesCount" yaml:"-"`
LastUpdated time.Time `json:"lastUpdated,omitempty" yaml:"last_updated,omitempty"`
dnsfilter.Filter `yaml:",inline"`
}
// Creates a helper object for working with the user rules
func userFilter() filter {
return filter{
// User filter always has constant ID=0
Enabled: true,
Filter: dnsfilter.Filter{
Rules: config.UserRules,
},
}
}
func deduplicateFilters() {
// Deduplicate filters
i := 0 // output index, used for deletion later
urls := map[string]bool{}
for _, filter := range config.Filters {
if _, ok := urls[filter.URL]; !ok {
// we didn't see it before, keep it
urls[filter.URL] = true // remember the URL
config.Filters[i] = filter
i++
}
}
// all entries we want to keep are at front, delete the rest
config.Filters = config.Filters[:i]
}
// Set the next filter ID to max(filter.ID) + 1
func updateUniqueFilterID(filters []filter) {
for _, filter := range filters {
if nextFilterID < filter.ID {
nextFilterID = filter.ID + 1
}
}
}
func assignUniqueFilterID() int64 {
value := nextFilterID
nextFilterID += 1
return value
}
// Sets up a timer that will be checking for filters updates periodically
func periodicallyRefreshFilters() {
for range time.Tick(time.Minute) {
refreshFiltersIfNeccessary(false)
}
}
// Checks filters updates if necessary
// If force is true, it ignores the filter.LastUpdated field value
func refreshFiltersIfNeccessary(force bool) int {
config.Lock()
// fetch URLs
updateCount := 0
for i := range config.Filters {
filter := &config.Filters[i] // otherwise we will be operating on a copy
if filter.ID == 0 { // protect against users modifying the yaml and removing the ID
filter.ID = assignUniqueFilterID()
}
updated, err := filter.update(force)
if err != nil {
log.Printf("Failed to update filter %s: %s\n", filter.URL, err)
continue
}
if updated {
// Saving it to the filters dir now
err = filter.save()
if err != nil {
log.Printf("Failed to save the updated filter %d: %s", filter.ID, err)
continue
}
updateCount++
}
}
config.Unlock()
if updateCount > 0 {
reconfigureDNSServer()
}
return updateCount
}
// A helper function that parses filter contents and returns a number of rules and a filter name (if there's any)
func parseFilterContents(contents []byte) (int, string, []string) {
lines := strings.Split(string(contents), "\n")
rulesCount := 0
name := ""
seenTitle := false
// Count lines in the filter
for _, line := range lines {
line = strings.TrimSpace(line)
if len(line) > 0 && line[0] == '!' {
if m := filterTitleRegexp.FindAllStringSubmatch(line, -1); len(m) > 0 && len(m[0]) >= 2 && !seenTitle {
name = m[0][1]
seenTitle = true
}
} else if len(line) != 0 {
rulesCount++
}
}
return rulesCount, name, lines
}
// Checks for filters updates
// If "force" is true -- does not check the filter's LastUpdated field
// Call "save" to persist the filter contents
func (filter *filter) update(force bool) (bool, error) {
if filter.ID == 0 { // protect against users deleting the ID
filter.ID = assignUniqueFilterID()
}
if !filter.Enabled {
return false, nil
}
if !force && time.Since(filter.LastUpdated) <= updatePeriod {
return false, nil
}
log.Printf("Downloading update for filter %d from %s", filter.ID, filter.URL)
// use the same update period for failed filter downloads to avoid flooding with requests
filter.LastUpdated = time.Now()
resp, err := client.Get(filter.URL)
if resp != nil && resp.Body != nil {
defer resp.Body.Close()
}
if err != nil {
log.Printf("Couldn't request filter from URL %s, skipping: %s", filter.URL, err)
return false, err
}
if resp.StatusCode != 200 {
log.Printf("Got status code %d from URL %s, skipping", resp.StatusCode, filter.URL)
return false, fmt.Errorf("got status code != 200: %d", resp.StatusCode)
}
contentType := strings.ToLower(resp.Header.Get("content-type"))
if !strings.HasPrefix(contentType, "text/plain") {
log.Printf("Non-text response %s from %s, skipping", contentType, filter.URL)
return false, fmt.Errorf("non-text response %s", contentType)
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Printf("Couldn't fetch filter contents from URL %s, skipping: %s", filter.URL, err)
return false, err
}
// Extract filter name and count number of rules
rulesCount, filterName, rules := parseFilterContents(body)
if filterName != "" {
filter.Name = filterName
}
// Check if the filter has been really changed
if reflect.DeepEqual(filter.Rules, rules) {
log.Printf("Filter #%d at URL %s hasn't changed, not updating it", filter.ID, filter.URL)
return false, nil
}
log.Printf("Filter %d has been updated: %d bytes, %d rules", filter.ID, len(body), rulesCount)
filter.RulesCount = rulesCount
filter.Rules = rules
return true, nil
}
// saves filter contents to the file in dataDir
func (filter *filter) save() error {
filterFilePath := filter.Path()
log.Printf("Saving filter %d contents to: %s", filter.ID, filterFilePath)
body := []byte(strings.Join(filter.Rules, "\n"))
return safeWriteFile(filterFilePath, body)
}
// loads filter contents from the file in dataDir
func (filter *filter) load() error {
if !filter.Enabled {
// No need to load a filter that is not enabled
return nil
}
filterFilePath := filter.Path()
log.Printf("Loading filter %d contents to: %s", filter.ID, filterFilePath)
if _, err := os.Stat(filterFilePath); os.IsNotExist(err) {
// do nothing, file doesn't exist
return err
}
filterFileContents, err := ioutil.ReadFile(filterFilePath)
if err != nil {
return err
}
log.Printf("File %s, id %d, length %d", filterFilePath, filter.ID, len(filterFileContents))
rulesCount, _, rules := parseFilterContents(filterFileContents)
filter.RulesCount = rulesCount
filter.Rules = rules
return nil
}
// Path to the filter contents
func (filter *filter) Path() string {
return filepath.Join(config.ourBinaryDir, dataDir, filterDir, strconv.FormatInt(filter.ID, 10)+".txt")
}

29
go.mod
View File

@@ -1,36 +1,21 @@
module github.com/AdguardTeam/AdGuardHome
require (
github.com/AdguardTeam/dnsproxy v0.9.10
github.com/StackExchange/wmi v0.0.0-20180725035823-b12b22c5341f // indirect
github.com/beefsack/go-rate v0.0.0-20180408011153-efa7637bb9b6
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 // indirect
github.com/bluele/gcache v0.0.0-20171010155617-472614239ac7
github.com/coredns/coredns v1.2.6
github.com/dnstap/golang-dnstap v0.0.0-20170829151710-2cf77a2b5e11 // indirect
github.com/farsightsec/golang-framestream v0.0.0-20181102145529-8a0cb8ba8710 // indirect
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect
github.com/go-ole/go-ole v1.2.1 // indirect
github.com/go-test/deep v1.0.1
github.com/gobuffalo/packr v1.19.0
github.com/google/uuid v1.0.0 // indirect
github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/mholt/caddy v0.11.0
github.com/miekg/dns v1.0.15
github.com/opentracing/opentracing-go v1.0.2 // indirect
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/pkg/errors v0.8.0
github.com/prometheus/client_golang v0.9.0-pre1
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 // indirect
github.com/prometheus/common v0.0.0-20181109100915-0b1957f9d949 // indirect
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d // indirect
github.com/hmage/golibs v0.0.0-20181229160906-c8491df0bfc4
github.com/joomcode/errorx v0.1.0
github.com/krolaw/dhcp4 v0.0.0-20180925202202-7cead472c414
github.com/miekg/dns v1.1.1
github.com/shirou/gopsutil v2.18.10+incompatible
github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4 // indirect
go.uber.org/goleak v0.10.0
golang.org/x/crypto v0.0.0-20181106171534-e4dc69e5b2fd
golang.org/x/net v0.0.0-20181108082009-03003ca0c849
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8 // indirect
google.golang.org/grpc v1.16.0 // indirect
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9
golang.org/x/net v0.0.0-20181220203305-927f97764cc3
gopkg.in/asaskevich/govalidator.v4 v4.0.0-20160518190739-766470278477
gopkg.in/yaml.v2 v2.2.1
)

94
go.sum
View File

@@ -1,23 +1,21 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/AdguardTeam/dnsproxy v0.9.10 h1:q364WlTvC+CS8kJbMy7TCyt4Niqixxw584MQJtCGhJU=
github.com/AdguardTeam/dnsproxy v0.9.10/go.mod h1:IqBhopgNpzB168kMurbjXf86dn50geasBIuGVxY63j0=
github.com/StackExchange/wmi v0.0.0-20180725035823-b12b22c5341f h1:5ZfJxyXo8KyX8DgGXC5B7ILL8y51fci/qYz2B4j8iLY=
github.com/StackExchange/wmi v0.0.0-20180725035823-b12b22c5341f/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY=
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA=
github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635 h1:52m0LGchQBBVqJRyYYufQuIbVqRawmubW3OFGqK1ekw=
github.com/aead/poly1305 v0.0.0-20180717145839-3fee0db0b635/go.mod h1:lmLxL+FV291OopO93Bwf9fQLQeLyt33VJRUg5VJ30us=
github.com/ameshkov/dnscrypt v1.0.4 h1:vtwHm5m4R2dhcCx23wiI+gNBoy7qm4h7+kZ4Pucw/vE=
github.com/ameshkov/dnscrypt v1.0.4/go.mod h1:hVW52S6r0QvUpIwsyfZ1ifYYpfGu5pewD3pl7afMJcQ=
github.com/ameshkov/dnsstamps v1.0.1 h1:LhGvgWDzhNJh+kBQd/AfUlq1vfVe109huiXw4JhnPug=
github.com/ameshkov/dnsstamps v1.0.1/go.mod h1:Ii3eUu73dx4Vw5O4wjzmT5+lkCwovjzaEZZ4gKyIH5A=
github.com/beefsack/go-rate v0.0.0-20180408011153-efa7637bb9b6 h1:KXlsf+qt/X5ttPGEjR0tPH1xaWWoKBEg9Q1THAj2h3I=
github.com/beefsack/go-rate v0.0.0-20180408011153-efa7637bb9b6/go.mod h1:6YNgTHLutezwnBvyneBbwvB8C82y3dcoOj5EQJIdGXA=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/bluele/gcache v0.0.0-20171010155617-472614239ac7 h1:NpQ+gkFOH27AyDypSCJ/LdsIi/b4rdnEb1N5+IpFfYs=
github.com/bluele/gcache v0.0.0-20171010155617-472614239ac7/go.mod h1:8c4/i2VlovMO2gBnHGQPN5EJw+H0lx1u/5p+cgsXtCk=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/coredns/coredns v1.2.6 h1:QIAOkBqVE44Zx0ttrFqgE5YhCEn64XPIngU60JyuTGM=
github.com/coredns/coredns v1.2.6/go.mod h1:zASH/MVDgR6XZTbxvOnsZfffS+31vg6Ackf/wo1+AM0=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dnstap/golang-dnstap v0.0.0-20170829151710-2cf77a2b5e11 h1:m8nX8hsUghn853BJ5qB0lX+VvS6LTJPksWyILFZRYN4=
github.com/dnstap/golang-dnstap v0.0.0-20170829151710-2cf77a2b5e11/go.mod h1:s1PfVYYVmTMgCSPtho4LKBDecEHJWtiVDPNv78Z985U=
github.com/farsightsec/golang-framestream v0.0.0-20181102145529-8a0cb8ba8710 h1:QdyRyGZWLEvJG5Kw3VcVJvhXJ5tZ1MkRgqpJOEZSySM=
github.com/farsightsec/golang-framestream v0.0.0-20181102145529-8a0cb8ba8710/go.mod h1:eNde4IQyEiA5br02AouhEHCu3p3UzrCdFR4LuQHklMI=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/go-ole/go-ole v1.2.1 h1:2lOsA72HgjxAuMlKpFiCbHTvu44PIVkZ5hqm3RSdI/E=
github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8=
github.com/go-test/deep v1.0.1 h1:UQhStjbkDClarlmv0am7OXXO4/GaPdCGiUiMTvi28sg=
@@ -28,44 +26,29 @@ github.com/gobuffalo/packd v0.0.0-20181031195726-c82734870264 h1:roWyi0eEdiFreSq
github.com/gobuffalo/packd v0.0.0-20181031195726-c82734870264/go.mod h1:Yf2toFaISlyQrr5TfO3h6DB9pl9mZRmyvBGQb/aQ/pI=
github.com/gobuffalo/packr v1.19.0 h1:3UDmBDxesCOPF8iZdMDBBWKfkBoYujIMIZePnobqIUI=
github.com/gobuffalo/packr v1.19.0/go.mod h1:MstrNkfCQhd5o+Ct4IJ0skWlxN8emOq8DsoT1G98VIU=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/uuid v1.0.0 h1:b4Gk+7WdP/d3HZH8EJsZpvV7EtDOgaZLtnaNGIu1adA=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 h1:MJG/KsmcqMwFAkh8mTnAwhyKoB+sTAnY4CACC110tbU=
github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645/go.mod h1:6iZfnjpejD4L/4DwD7NryNaJyCQdzwWwH2MWhCA90Kw=
github.com/hmage/golibs v0.0.0-20181229160906-c8491df0bfc4 h1:FMAReGTEDNr4AdbScv/PqzjMQUpkkVHiF/t8sDHQQVQ=
github.com/hmage/golibs v0.0.0-20181229160906-c8491df0bfc4/go.mod h1:H6Ev6svFxUVPFThxLtdnFfcE9e3GWufpfmcVFpqV6HM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jmcvetta/randutil v0.0.0-20150817122601-2bb1b664bcff h1:6NvhExg4omUC9NfA+l4Oq3ibNNeJUdiAF3iBVB0PlDk=
github.com/jmcvetta/randutil v0.0.0-20150817122601-2bb1b664bcff/go.mod h1:ddfPX8Z28YMjiqoaJhNBzWHapTHXejnB5cDCUWDwriw=
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/joomcode/errorx v0.1.0 h1:QmJMiI1DE1UFje2aI1ZWO/VMT5a32qBoXUclGOt8vsc=
github.com/joomcode/errorx v0.1.0/go.mod h1:kgco15ekB6cs+4Xjzo7SPeXzx38PbJzBwbnu9qfVNHQ=
github.com/krolaw/dhcp4 v0.0.0-20180925202202-7cead472c414 h1:6wnYc2S/lVM7BvR32BM74ph7bPgqMztWopMYKgVyEho=
github.com/krolaw/dhcp4 v0.0.0-20180925202202-7cead472c414/go.mod h1:0AqAH3ZogsCrvrtUpvc6EtVKbc3w6xwZhkvGLuqyi3o=
github.com/markbates/oncer v0.0.0-20181014194634-05fccaae8fc4 h1:Mlji5gkcpzkqTROyE4ZxZ8hN7osunMb2RuGVrbvMvCc=
github.com/markbates/oncer v0.0.0-20181014194634-05fccaae8fc4/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mholt/caddy v0.11.0 h1:cuhEyR7So/SBBRiAaiRBe9BoccDu6uveIPuM9FMMavg=
github.com/mholt/caddy v0.11.0/go.mod h1:Wb1PlT4DAYSqOEd03MsqkdkXnTxA8v9pKjdpxbqM1kY=
github.com/miekg/dns v1.0.15 h1:9+UupePBQCG6zf1q/bGmTO1vumoG13jsrbWOSX1W6Tw=
github.com/miekg/dns v1.0.15/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/opentracing/opentracing-go v1.0.2 h1:3jA2P6O1F9UOrWVpwrIo17pu01KWvNWg4X946/Y5Zwg=
github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/miekg/dns v1.1.1 h1:DVkblRdiScEnEr0LR9nTnEQqHYycjkXW9bOjd+2EL2o=
github.com/miekg/dns v1.1.1/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v0.9.0-pre1 h1:AWTOhsOI9qxeirTuA0A4By/1Es1+y9EcCGY6bBZ2fhM=
github.com/prometheus/client_golang v0.9.0-pre1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 h1:idejC8f05m9MGOsuEi1ATq9shN03HrxNkD/luQvxCv8=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/common v0.0.0-20181109100915-0b1957f9d949 h1:MVbUQq1a49hMEISI29UcAUjywT3FyvDwx5up90OvVa4=
github.com/prometheus/common v0.0.0-20181109100915-0b1957f9d949/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d h1:GoAlyOgbOEIFdaDqxJVlbOQ1DtGmZWs/Qau0hIlk+WQ=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/shirou/gopsutil v2.18.10+incompatible h1:cy84jW6EVRPa5g9HAHrlbxMSIjBhDSX0OFYyMYminYs=
github.com/shirou/gopsutil v2.18.10+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4 h1:udFKJ0aHUL60LboW/A+DfgoHVedieIzIXE8uylPue0U=
@@ -78,31 +61,26 @@ github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
go.uber.org/goleak v0.10.0 h1:G3eWbSNIskeRqtsN/1uI5B+eP73y3JUuBsv9AZjehb4=
go.uber.org/goleak v0.10.0/go.mod h1:VCZuO8V8mFPlL0F5J5GK1rtHV3DrFcQ1R8ryq7FK0aI=
golang.org/x/crypto v0.0.0-20181106171534-e4dc69e5b2fd h1:VtIkGDhk0ph3t+THbvXHfMZ8QHgsBO39Nh52+74pq7w=
golang.org/x/crypto v0.0.0-20181106171534-e4dc69e5b2fd/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 h1:mKdxBk7AujPs8kU4m80U72y/zjbZ3UcXC7dClwKbUI0=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/net v0.0.0-20181102091132-c10e9556a7bc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181108082009-03003ca0c849 h1:FSqE2GGG7wzsYUsWiQ8MZrvEd1EOyU3NCF0AW3Wtltg=
golang.org/x/net v0.0.0-20181108082009-03003ca0c849/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/net v0.0.0-20181213202711-891ebc4b82d6 h1:gT0Y6H7hbVPUtvtk0YGxMXPgN+p8fYlqWkgJeUCZcaQ=
golang.org/x/net v0.0.0-20181213202711-891ebc4b82d6/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3 h1:eH6Eip3UpmR+yM/qI9Ijluzb1bNv/cAU/n+6l8tRSis=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8 h1:YoY1wS6JYVRpIfFngRf2HHo9R9dAne3xbkGOQ5rJXjU=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/grpc v1.16.0 h1:dz5IJGuC2BB7qXR5AyHNwAUBhZscK2xVez7mznh72sY=
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f h1:Bl/8QSvNqXvPGPGXa2z5xUTmV7VDcZyvRZ+QQXkXTZQ=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20181213200352-4d1cda033e06 h1:0oC8rFnE+74kEmuHZ46F6KHsMr5Gx2gUQPuNz28iQZM=
golang.org/x/sys v0.0.0-20181213200352-4d1cda033e06/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb h1:pf3XwC90UUdNPYWZdFjhGBE7DUFuK3Ct1zWmZ65QN30=
golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
gopkg.in/asaskevich/govalidator.v4 v4.0.0-20160518190739-766470278477 h1:5xUJw+lg4zao9W4HIDzlFbMYgSgtvNVHh00MEHvbGpQ=
gopkg.in/asaskevich/govalidator.v4 v4.0.0-20160518190739-766470278477/go.mod h1:QDV1vrFSrowdoOba0UM8VJPUZONT7dnfdLsM+GG53Z8=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

@@ -3,7 +3,6 @@ package main
import (
"bufio"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
@@ -19,8 +18,7 @@ import (
// ----------------------------------
// Writes data first to a temporary file and then renames it to what's specified in path
func writeFileSafe(path string, data []byte) error {
func safeWriteFile(path string, data []byte) error {
dir := filepath.Dir(path)
err := os.MkdirAll(dir, 0755)
if err != nil {
@@ -32,12 +30,7 @@ func writeFileSafe(path string, data []byte) error {
if err != nil {
return err
}
err = os.Rename(tmpPath, path)
if err != nil {
return err
}
return nil
return os.Rename(tmpPath, path)
}
// ----------------------------------
@@ -135,16 +128,9 @@ func parseParametersFromBody(r io.Reader) (map[string]string, error) {
// ---------------------
// debug logging helpers
// ---------------------
func trace(format string, args ...interface{}) {
func _Func() string {
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())
return path.Base(f.Name())
}

View File

@@ -3,9 +3,10 @@ package main
import (
"fmt"
"io/ioutil"
"log"
"net/http"
"strings"
"github.com/hmage/golibs/log"
)
// --------------------
@@ -20,14 +21,12 @@ var allowedLanguages = map[string]bool{
"ja": true,
"sv": true,
"pt-br": true,
"zh-tw": true,
}
func isLanguageAllowed(language string) bool {
l := strings.ToLower(language)
if allowedLanguages[l] {
return true
}
return false
return allowedLanguages[l]
}
func handleI18nCurrentLanguage(w http.ResponseWriter, r *http.Request) {

View File

@@ -1,622 +0,0 @@
swagger: '2.0'
info:
title: 'AdGuard Home'
description: 'Control AdGuard Home server with this API'
version: 0.0.0
basePath: /control
schemes:
- http
produces:
- application/json
tags:
-
name: global
description: 'DNS server controls'
-
name: filtering
description: 'Rule-based filtering'
-
name: safebrowsing
description: 'Malware/hazardous sites'
-
name: parental
description: 'Sites inappropriate for children'
-
name: safesearch
description: 'Enforce family-friendly results in search engines'
paths:
/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
bootstrap_dns: 8.8.8.8:53
upstream_dns:
- 1.1.1.1
- 1.0.0.1
version: "v0.1"
language: "en"
/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
/querylog:
get:
tags:
- global
operationId: queryLog
summary: 'Get DNS server query log'
parameters:
- in: query
name: download
type: boolean
description: 'If any value is set, make the browser download the query instead of displaying it by setting Content-Disposition header'
responses:
200:
description: OK
examples:
application/json:
- answer:
- ttl: 55
type: A
value: 217.69.139.201
- ttl: 55
type: A
value: 94.100.180.200
- ttl: 55
type: A
value: 94.100.180.201
- ttl: 55
type: A
value: 217.69.139.200
elapsedMs: '65.469556'
question:
class: IN
host: mail.ru
type: A
reason: DNSFILTER_NOTFILTERED_NOTFOUND
status: NOERROR
time: '2018-07-16T22:24:02+03:00'
- elapsedMs: '0.15716999999999998'
question:
class: IN
host: doubleclick.net
type: A
reason: DNSFILTER_FILTERED_BLACKLIST
rule: "||doubleclick.net^"
status: NXDOMAIN
time: '2018-07-16T22:24:02+03:00'
- answer:
- ttl: 299
type: A
value: 176.103.133.78
elapsedMs: '132.110929'
question:
class: IN
host: wmconvirus.narod.ru
type: A
reason: DNSFILTER_FILTERED_SAFEBROWSING
rule: adguard-malware-shavar
filterId: 1
status: NOERROR
time: '2018-07-16T22:24:02+03:00'
/querylog_enable:
post:
tags:
- global
operationId: querylogEnable
summary: 'Enable querylog'
responses:
200:
description: OK
/querylog_disable:
post:
tags:
- global
operationId: querylogDisable
summary: 'Disable filtering'
responses:
200:
description: OK
/set_upstream_dns:
post:
tags:
- global
operationId: setUpstreamDNS
summary: 'Set upstream DNS for coredns, empty value will reset it to default values'
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: 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"
/i18n/change_language:
post:
tags:
- i18n
operationId: changeLanguage
summary: "Change current language. Argument must be an ISO 639-1 two-letter code"
consumes:
- text/plain
parameters:
- in: body
name: language
description: "New language. It must be known to the server and must be an ISO 639-1 two-letter code"
schema:
type: string
example: en
/i18n/current_language:
get:
tags:
- i18n
operationId: currentLanguage
summary: "Get currently set language. Result is ISO 639-1 two-letter code. Empty result means default language."
responses:
200:
description: OK
examples:
text/plain:
en
/stats_top:
get:
tags:
- global
operationId: statusTop
summary: 'Get DNS server top client, domain and blocked statistics'
responses:
200:
description: OK
examples:
application/json:
top_queried_domains:
example.org: 12312
example.com: 12312
example.net: 12312
example.ru: 12312
top_clients:
127.0.0.1: 12312
192.168.0.1: 13211
192.168.0.2: 13211
192.168.0.3: 13211
192.168.0.4: 13211
192.168.0.5: 13211
192.168.0.6: 13211
top_blocked_domains:
example.org: 12312
example.com: 12312
example.net: 12312
example.ru: 12312
/stats:
get:
tags:
- global
operationId: stats
summary: 'Get DNS server statistics'
responses:
200:
description: 'Gives statistics since start of the server'
examples:
application/json:
dns_queries: 1201
blocked_filtering: 123
replaced_safebrowsing: 5
replaced_parental: 18
replaced_safesearch: 94
avg_processing_time: 123
/stats_history:
get:
tags:
- global
operationId: stats_history
summary: 'Get historical DNS server statistics'
parameters:
-
name: start_time
in: query
type: string
description: 'Start time in ISO8601 (example: `2018-05-04T17:55:33+00:00`)'
required: true
-
name: end_time
in: query
type: string
description: 'End time in ISO8601 (example: `2018-05-04T17:55:33+00:00`)'
required: true
-
name: time_unit
in: query
type: string
description: 'Time unit (`minutes` or `hours`)'
required: true
enum:
- minutes
- hours
responses:
501:
description: 'Requested time window is outside of supported range. It will be supported later, but not now.'
200:
description: 'Gives statistics since start of the server. Example below is for 5 minutes. Values are from oldest to newest.'
examples:
application/json:
dns_queries:
- 1201
- 1201
- 1201
- 1201
- 1201
blocked_filtering:
- 123
- 123
- 123
- 123
- 123
replaced_safebrowsing:
- 5
- 5
- 5
- 5
- 5
replaced_parental:
- 18
- 18
- 18
- 18
- 18
replaced_safesearch:
- 94
- 94
- 94
- 94
- 94
avg_processing_time:
- 123
- 123
- 123
- 123
- 123
/stats_reset:
post:
tags:
-global
operationId: statsReset
summary: "Reset all statistics to zeroes"
responses:
200:
description: OK
/filtering/enable:
post:
tags:
- filtering
operationId: filteringEnable
summary: 'Enable filtering'
responses:
200:
description: OK
/filtering/disable:
post:
tags:
- filtering
operationId: filteringDisable
summary: 'Disable filtering'
responses:
200:
description: OK
/filtering/add_url:
put:
tags:
- filtering
operationId: filteringAddURL
summary: 'Add filter URL'
consumes:
- text/plain
parameters:
- in: body
name: url
description: 'URL containing filtering rules'
required: true
schema:
type: string
example: 'url=https://filters.adtidy.org/windows/filters/15.txt'
responses:
200:
description: OK
/filtering/remove_url:
delete:
tags:
- filtering
operationId: filteringRemoveURL
summary: 'Remove filter URL'
consumes:
- text/plain
parameters:
- in: body
name: url
description: 'Previously added URL containing filtering rules'
required: true
schema:
type: string
example: 'url=https://filters.adtidy.org/windows/filters/15.txt'
responses:
200:
description: OK
/filtering/enable_url:
post:
tags:
- filtering
operationId: filteringEnableURL
summary: 'Enable filter URL'
consumes:
- text/plain
parameters:
- in: body
name: url
description: 'Previously added URL containing filtering rules'
required: true
schema:
type: string
example: 'url=https://filters.adtidy.org/windows/filters/15.txt'
responses:
200:
description: OK
/filtering/disable_url:
post:
tags:
- filtering
operationId: filteringDisableURL
summary: 'Disable filter URL'
consumes:
- text/plain
parameters:
- in: body
name: url
description: 'Previously added URL containing filtering rules'
required: true
schema:
type: string
example: 'url=https://filters.adtidy.org/windows/filters/15.txt'
responses:
200:
description: OK
/filtering/refresh:
post:
tags:
- filtering
operationId: filteringRefresh
summary: |
Reload filtering rules from URLs
This might be needed if new URL was just added and you dont want to wait for automatic refresh to kick in.
This API request is ratelimited, so you can call it freely as often as you like, it wont create unneccessary burden on servers that host the URL.
This should work as intended, a `force` parameter is offered as last-resort attempt to make filter lists fresh.
If you ever find yourself using `force` to make something work that otherwise wont, this is a bug and report it accordingly.
parameters:
-
name: force
in: query
type: boolean
description: 'If any value is set, ignore cache and force re-download of all filters'
responses:
200:
description: OK with how many filters were actually updated
/filtering/status:
get:
tags:
- filtering
operationId: filteringStatus
summary: 'Get status of rules-based filter'
responses:
200:
description: OK
examples:
application/json:
enabled: false
- filters:
enabled: true
id: 1
lastUpdated: "2018-10-30T12:18:57.223101822+03:00"
name: "AdGuard Simplified Domain Names filter"
rulesCount: 24896
url: "https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt"
rules:
- '@@||yandex.ru^|'
/filtering/set_rules:
put:
tags:
- filtering
operationId: filteringSetRules
summary: 'Set user-defined filter rules'
consumes:
- text/plain
parameters:
- in: body
name: rules
description: 'All filtering rules, one line per rule'
schema:
type: string
example: '@@||yandex.ru^|'
responses:
200:
description: OK
/safebrowsing/enable:
post:
tags:
- safebrowsing
operationId: safebrowsingEnable
summary: 'Enable safebrowsing'
responses:
200:
description: OK
/safebrowsing/disable:
post:
tags:
- safebrowsing
operationId: safebrowsingDisable
summary: 'Disable safebrowsing'
responses:
200:
description: OK
/safebrowsing/status:
get:
tags:
- safebrowsing
operationId: safebrowsingStatus
summary: 'Get safebrowsing status'
responses:
200:
description: OK
examples:
application/json:
enabled: false
/parental/enable:
post:
tags:
- parental
operationId: parentalEnable
summary: 'Enable parental filtering'
consumes:
- text/plain
parameters:
- in: body
name: sensitivity
description: |
Age sensitivity for parental filtering,
EARLY_CHILDHOOD is 3
YOUNG is 10
TEEN is 13
MATURE is 17
required: true
schema:
type: string
enum:
- EARLY_CHILDHOOD
- YOUNG
- TEEN
- MATURE
example: 'sensitivity=TEEN'
responses:
200:
description: OK
/parental/disable:
post:
tags:
- parental
operationId: parentalDisable
summary: 'Disable parental filtering'
responses:
200:
description: OK
/parental/status:
get:
tags:
- parental
operationId: parentalStatus
summary: 'Get parental filtering status'
responses:
200:
description: OK
examples:
application/json:
enabled: true
sensitivity: 13
/safesearch/enable:
post:
tags:
- safesearch
operationId: safesearchEnable
summary: 'Enable safesearch'
responses:
200:
description: OK
/safesearch/disable:
post:
tags:
- safesearch
operationId: safesearchDisable
summary: 'Disable safesearch'
responses:
200:
description: OK
/safesearch/status:
get:
tags:
- safesearch
operationId: safesearchStatus
summary: 'Get safesearch status'
responses:
200:
description: OK
examples:
application/json:
enabled: false
definitions:
rule:
type: string

2
openapi/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
.DS_Store
node_modules

13
openapi/README.md Normal file
View File

@@ -0,0 +1,13 @@
## AdGuard Home OpenAPI
We are using [OpenAPI specification](https://swagger.io/docs/specification/about/) to generate AdGuard Home API specification.
### How to edit the API spec
The easiest way would be to use [Swagger Editor](http://editor.swagger.io/) and just copy/paste the YAML file there.
### How to read the API doc
1. `yarn install`
2. `yarn start`
3. Open `http://localhost:4000/`

60
openapi/index.html Normal file
View File

@@ -0,0 +1,60 @@
<!-- HTML for static distribution bundle build -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>AdGuard Home API</title>
<link rel="stylesheet" type="text/css" href="./node_modules/swagger-ui-dist/swagger-ui.css" >
<link rel="icon" type="image/png" href="./node_modules/swagger-ui-dist/favicon-32x32.png" sizes="32x32" />
<link rel="icon" type="image/png" href="./node_modules/swagger-ui-dist/favicon-16x16.png" sizes="16x16" />
<style>
html
{
box-sizing: border-box;
overflow: -moz-scrollbars-vertical;
overflow-y: scroll;
}
*,
*:before,
*:after
{
box-sizing: inherit;
}
body
{
margin:0;
background: #fafafa;
}
</style>
</head>
<body>
<div id="swagger-ui"></div>
<script src="./node_modules/swagger-ui-dist/swagger-ui-bundle.js"> </script>
<script src="./node_modules/swagger-ui-dist/swagger-ui-standalone-preset.js"> </script>
<script>
window.onload = function() {
// Begin Swagger UI call region
const ui = SwaggerUIBundle({
url: "./openapi.yaml",
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout"
})
// End Swagger UI call region
window.ui = ui
}
</script>
</body>
</html>

8
openapi/index.js Normal file
View File

@@ -0,0 +1,8 @@
const express = require('express')
const app = express()
app.use(express.static(__dirname))
console.log('Open http://localhost:4000/ to examine the API spec')
app.listen(4000)

1066
openapi/openapi.yaml Normal file

File diff suppressed because it is too large Load Diff

12
openapi/package.json Normal file
View File

@@ -0,0 +1,12 @@
{
"name": "adguard-home-api",
"version": "0.1.0",
"private": true,
"scripts": {
"start": "node index.js"
},
"dependencies": {
"express": "^4.16.4",
"swagger-ui-dist": "^3.20.1"
}
}

349
openapi/yarn.lock Normal file
View File

@@ -0,0 +1,349 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
accepts@~1.3.5:
version "1.3.5"
resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.5.tgz#eb777df6011723a3b14e8a72c0805c8e86746bd2"
integrity sha1-63d99gEXI6OxTopywIBcjoZ0a9I=
dependencies:
mime-types "~2.1.18"
negotiator "0.6.1"
array-flatten@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=
body-parser@1.18.3:
version "1.18.3"
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.18.3.tgz#5b292198ffdd553b3a0f20ded0592b956955c8b4"
integrity sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=
dependencies:
bytes "3.0.0"
content-type "~1.0.4"
debug "2.6.9"
depd "~1.1.2"
http-errors "~1.6.3"
iconv-lite "0.4.23"
on-finished "~2.3.0"
qs "6.5.2"
raw-body "2.3.3"
type-is "~1.6.16"
bytes@3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048"
integrity sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=
content-disposition@0.5.2:
version "0.5.2"
resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4"
integrity sha1-DPaLud318r55YcOoUXjLhdunjLQ=
content-type@~1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==
cookie-signature@1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw=
cookie@0.3.1:
version "0.3.1"
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb"
integrity sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=
debug@2.6.9:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
dependencies:
ms "2.0.0"
depd@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=
destroy@~1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80"
integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=
ee-first@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
encodeurl@~1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=
escape-html@~1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=
etag@~1.8.1:
version "1.8.1"
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
express@^4.16.4:
version "4.16.4"
resolved "https://registry.yarnpkg.com/express/-/express-4.16.4.tgz#fddef61926109e24c515ea97fd2f1bdbf62df12e"
integrity sha512-j12Uuyb4FMrd/qQAm6uCHAkPtO8FDTRJZBDd5D2KOL2eLaz1yUNdUB/NOIyq0iU4q4cFarsUCrnFDPBcnksuOg==
dependencies:
accepts "~1.3.5"
array-flatten "1.1.1"
body-parser "1.18.3"
content-disposition "0.5.2"
content-type "~1.0.4"
cookie "0.3.1"
cookie-signature "1.0.6"
debug "2.6.9"
depd "~1.1.2"
encodeurl "~1.0.2"
escape-html "~1.0.3"
etag "~1.8.1"
finalhandler "1.1.1"
fresh "0.5.2"
merge-descriptors "1.0.1"
methods "~1.1.2"
on-finished "~2.3.0"
parseurl "~1.3.2"
path-to-regexp "0.1.7"
proxy-addr "~2.0.4"
qs "6.5.2"
range-parser "~1.2.0"
safe-buffer "5.1.2"
send "0.16.2"
serve-static "1.13.2"
setprototypeof "1.1.0"
statuses "~1.4.0"
type-is "~1.6.16"
utils-merge "1.0.1"
vary "~1.1.2"
finalhandler@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.1.tgz#eebf4ed840079c83f4249038c9d703008301b105"
integrity sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==
dependencies:
debug "2.6.9"
encodeurl "~1.0.2"
escape-html "~1.0.3"
on-finished "~2.3.0"
parseurl "~1.3.2"
statuses "~1.4.0"
unpipe "~1.0.0"
forwarded@~0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84"
integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=
fresh@0.5.2:
version "0.5.2"
resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=
http-errors@1.6.3, http-errors@~1.6.2, http-errors@~1.6.3:
version "1.6.3"
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d"
integrity sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=
dependencies:
depd "~1.1.2"
inherits "2.0.3"
setprototypeof "1.1.0"
statuses ">= 1.4.0 < 2"
iconv-lite@0.4.23:
version "0.4.23"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.23.tgz#297871f63be507adcfbfca715d0cd0eed84e9a63"
integrity sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==
dependencies:
safer-buffer ">= 2.1.2 < 3"
inherits@2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
ipaddr.js@1.8.0:
version "1.8.0"
resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.8.0.tgz#eaa33d6ddd7ace8f7f6fe0c9ca0440e706738b1e"
integrity sha1-6qM9bd16zo9/b+DJygRA5wZzix4=
media-typer@0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
merge-descriptors@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=
methods@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=
mime-db@~1.37.0:
version "1.37.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.37.0.tgz#0b6a0ce6fdbe9576e25f1f2d2fde8830dc0ad0d8"
integrity sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg==
mime-types@~2.1.18:
version "2.1.21"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.21.tgz#28995aa1ecb770742fe6ae7e58f9181c744b3f96"
integrity sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg==
dependencies:
mime-db "~1.37.0"
mime@1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6"
integrity sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ==
ms@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
negotiator@0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9"
integrity sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=
on-finished@~2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=
dependencies:
ee-first "1.1.1"
parseurl@~1.3.2:
version "1.3.2"
resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.2.tgz#fc289d4ed8993119460c156253262cdc8de65bf3"
integrity sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=
path-to-regexp@0.1.7:
version "0.1.7"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=
proxy-addr@~2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.4.tgz#ecfc733bf22ff8c6f407fa275327b9ab67e48b93"
integrity sha512-5erio2h9jp5CHGwcybmxmVqHmnCBZeewlfJ0pex+UW7Qny7OOZXTtH56TGNyBizkgiOwhJtMKrVzDTeKcySZwA==
dependencies:
forwarded "~0.1.2"
ipaddr.js "1.8.0"
qs@6.5.2:
version "6.5.2"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==
range-parser@~1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e"
integrity sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=
raw-body@2.3.3:
version "2.3.3"
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.3.tgz#1b324ece6b5706e153855bc1148c65bb7f6ea0c3"
integrity sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==
dependencies:
bytes "3.0.0"
http-errors "1.6.3"
iconv-lite "0.4.23"
unpipe "1.0.0"
safe-buffer@5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
"safer-buffer@>= 2.1.2 < 3":
version "2.1.2"
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
send@0.16.2:
version "0.16.2"
resolved "https://registry.yarnpkg.com/send/-/send-0.16.2.tgz#6ecca1e0f8c156d141597559848df64730a6bbc1"
integrity sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==
dependencies:
debug "2.6.9"
depd "~1.1.2"
destroy "~1.0.4"
encodeurl "~1.0.2"
escape-html "~1.0.3"
etag "~1.8.1"
fresh "0.5.2"
http-errors "~1.6.2"
mime "1.4.1"
ms "2.0.0"
on-finished "~2.3.0"
range-parser "~1.2.0"
statuses "~1.4.0"
serve-static@1.13.2:
version "1.13.2"
resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.13.2.tgz#095e8472fd5b46237db50ce486a43f4b86c6cec1"
integrity sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==
dependencies:
encodeurl "~1.0.2"
escape-html "~1.0.3"
parseurl "~1.3.2"
send "0.16.2"
setprototypeof@1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656"
integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==
"statuses@>= 1.4.0 < 2":
version "1.5.0"
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=
statuses@~1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087"
integrity sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==
swagger-ui-dist@^3.20.1:
version "3.20.1"
resolved "https://registry.yarnpkg.com/swagger-ui-dist/-/swagger-ui-dist-3.20.1.tgz#2e871faf29984bc5f253b8641ba38b244001cfa4"
integrity sha512-iqFNNmJWH24leUj/ohS5iZTHLZSPZse8c9F+WSCMi6ZJcRBgYKcT413c8BR5BEdKvU1kkIwvYy7C8DOjTRq9hQ==
type-is@~1.6.16:
version "1.6.16"
resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.16.tgz#f89ce341541c672b25ee7ae3c73dee3b2be50194"
integrity sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==
dependencies:
media-typer "0.3.0"
mime-types "~2.1.18"
unpipe@1.0.0, unpipe@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=
utils-merge@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=
vary@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=

31
release.sh Executable file
View File

@@ -0,0 +1,31 @@
#!/usr/bin/env bash
set -eE
set -o pipefail
set -x
version=`git describe --abbrev=4 --dirty --always --tags`
f() {
make cleanfast; CGO_DISABLED=1 make
if [[ $GOOS == darwin ]]; then
rm -f ../AdGuardHome_"$version"_MacOS.zip
zip ../AdGuardHome_"$version"_MacOS.zip AdGuardHome README.md LICENSE.TXT
elif [[ $GOOS == windows ]]; then
rm -f ../AdGuardHome_"$version"_Windows.zip
zip ../AdGuardHome_"$version"_Windows.zip AdGuardHome.exe README.md LICENSE.TXT
else
pushd ..
tar zcvf AdGuardHome_"$version"_"$GOOS"_"$GOARCH".tar.gz AdGuardHome/{AdGuardHome,LICENSE.TXT,README.md}
popd
fi
}
#make clean
#make
GOOS=darwin GOARCH=amd64 f
GOOS=linux GOARCH=amd64 f
GOOS=linux GOARCH=386 f
GOOS=linux GOARCH=arm GOARM=6 f
GOOS=linux GOARCH=arm64 GOARM=6 f
GOOS=windows GOARCH=amd64 f

View File

@@ -4,7 +4,17 @@ const crypto = require('crypto');
const requestPromise = require('request-promise');
const LOCALES_DIR = '../../client/src/__locales';
const LOCALES_LIST = ['en', 'ru', 'vi', 'es', 'fr', 'ja', 'sv', 'pt-br'];
const LOCALES_LIST = [
'en',
'ru',
'vi',
'es',
'fr',
'ja',
'sv',
'pt-br',
'zh-tw',
];
/**
* Hash content

146
upgrade.go Normal file
View File

@@ -0,0 +1,146 @@
package main
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"github.com/hmage/golibs/log"
"gopkg.in/yaml.v2"
)
const currentSchemaVersion = 2 // used for upgrading from old configs to new config
// Performs necessary upgrade operations if needed
func upgradeConfig() error {
// read a config file into an interface map, so we can manipulate values without losing any
configFile := filepath.Join(config.ourBinaryDir, config.ourConfigFilename)
if _, err := os.Stat(configFile); os.IsNotExist(err) {
log.Printf("config file %s does not exist, nothing to upgrade", configFile)
return nil
}
diskConfig := map[string]interface{}{}
body, err := ioutil.ReadFile(configFile)
if err != nil {
log.Printf("Couldn't read config file '%s': %s", configFile, err)
return err
}
err = yaml.Unmarshal(body, &diskConfig)
if err != nil {
log.Printf("Couldn't parse config file '%s': %s", configFile, err)
return err
}
schemaVersionInterface, ok := diskConfig["schema_version"]
log.Printf("%s(): got schema version %v", _Func(), schemaVersionInterface)
if !ok {
// no schema version, set it to 0
schemaVersionInterface = 0
}
schemaVersion, ok := schemaVersionInterface.(int)
if !ok {
err = fmt.Errorf("configuration file contains non-integer schema_version, abort")
log.Println(err)
return err
}
if schemaVersion == currentSchemaVersion {
// do nothing
return nil
}
return upgradeConfigSchema(schemaVersion, &diskConfig)
}
// Upgrade from oldVersion to newVersion
func upgradeConfigSchema(oldVersion int, diskConfig *map[string]interface{}) error {
switch oldVersion {
case 0:
err := upgradeSchema0to2(diskConfig)
if err != nil {
return err
}
case 1:
err := upgradeSchema1to2(diskConfig)
if err != nil {
return err
}
default:
err := fmt.Errorf("configuration file contains unknown schema_version, abort")
log.Println(err)
return err
}
configFile := filepath.Join(config.ourBinaryDir, config.ourConfigFilename)
body, err := yaml.Marshal(diskConfig)
if err != nil {
log.Printf("Couldn't generate YAML file: %s", err)
return err
}
err = safeWriteFile(configFile, body)
if err != nil {
log.Printf("Couldn't save YAML config: %s", err)
return err
}
return nil
}
// The first schema upgrade:
// No more "dnsfilter.txt", filters are now kept in data/filters/
func upgradeSchema0to1(diskConfig *map[string]interface{}) error {
log.Printf("%s(): called", _Func())
dnsFilterPath := filepath.Join(config.ourBinaryDir, "dnsfilter.txt")
if _, err := os.Stat(dnsFilterPath); !os.IsNotExist(err) {
log.Printf("Deleting %s as we don't need it anymore", dnsFilterPath)
err = os.Remove(dnsFilterPath)
if err != nil {
log.Printf("Cannot remove %s due to %s", dnsFilterPath, err)
// not fatal, move on
}
}
(*diskConfig)["schema_version"] = 1
return nil
}
// Second schema upgrade:
// coredns is now dns in config
// delete 'Corefile', since we don't use that anymore
func upgradeSchema1to2(diskConfig *map[string]interface{}) error {
log.Printf("%s(): called", _Func())
coreFilePath := filepath.Join(config.ourBinaryDir, "Corefile")
if _, err := os.Stat(coreFilePath); !os.IsNotExist(err) {
log.Printf("Deleting %s as we don't need it anymore", coreFilePath)
err = os.Remove(coreFilePath)
if err != nil {
log.Printf("Cannot remove %s due to %s", coreFilePath, err)
// not fatal, move on
}
}
if _, ok := (*diskConfig)["dns"]; !ok {
(*diskConfig)["dns"] = (*diskConfig)["coredns"]
delete((*diskConfig), "coredns")
}
(*diskConfig)["schema_version"] = 2
return nil
}
// jump two schemas at once -- this time we just do it sequentially
func upgradeSchema0to2(diskConfig *map[string]interface{}) error {
err := upgradeSchema0to1(diskConfig)
if err != nil {
return err
}
return upgradeSchema1to2(diskConfig)
}

View File

@@ -1,109 +0,0 @@
package upstream
import (
"crypto/tls"
"time"
"github.com/miekg/dns"
"golang.org/x/net/context"
)
// DnsUpstream is a very simple upstream implementation for plain DNS
type DnsUpstream struct {
endpoint string // IP:port
timeout time.Duration // Max read and write timeout
proto string // Protocol (tcp, tcp-tls, or udp)
transport *Transport // Persistent connections cache
}
// NewDnsUpstream creates a new DNS upstream
func NewDnsUpstream(endpoint string, proto string, tlsServerName string) (Upstream, error) {
u := &DnsUpstream{
endpoint: endpoint,
timeout: defaultTimeout,
proto: proto,
}
var tlsConfig *tls.Config
if proto == "tcp-tls" {
tlsConfig = new(tls.Config)
tlsConfig.ServerName = tlsServerName
}
// Initialize the connections cache
u.transport = NewTransport(endpoint)
u.transport.tlsConfig = tlsConfig
u.transport.Start()
return u, nil
}
// Exchange provides an implementation for the Upstream interface
func (u *DnsUpstream) Exchange(ctx context.Context, query *dns.Msg) (*dns.Msg, error) {
resp, err := u.exchange(u.proto, query)
// Retry over TCP if response is truncated
if err == dns.ErrTruncated && u.proto == "udp" {
resp, err = u.exchange("tcp", query)
} else if err == dns.ErrTruncated && resp != nil {
// Reassemble something to be sent to client
m := new(dns.Msg)
m.SetReply(query)
m.Truncated = true
m.Authoritative = true
m.Rcode = dns.RcodeSuccess
return m, nil
}
if err != nil {
resp = &dns.Msg{}
resp.SetRcode(resp, dns.RcodeServerFailure)
}
return resp, err
}
// Clear resources
func (u *DnsUpstream) Close() error {
// Close active connections
u.transport.Stop()
return nil
}
// Performs a synchronous query. It sends the message m via the conn
// c and waits for a reply. The conn c is not closed.
func (u *DnsUpstream) exchange(proto string, query *dns.Msg) (r *dns.Msg, err error) {
// Establish a connection if needed (or reuse cached)
conn, err := u.transport.Dial(proto)
if err != nil {
return nil, err
}
// Write the request with a timeout
conn.SetWriteDeadline(time.Now().Add(u.timeout))
if err = conn.WriteMsg(query); err != nil {
conn.Close() // Not giving it back
return nil, err
}
// Write response with a timeout
conn.SetReadDeadline(time.Now().Add(u.timeout))
r, err = conn.ReadMsg()
if err != nil {
conn.Close() // Not giving it back
} else if err == nil && r.Id != query.Id {
err = dns.ErrId
conn.Close() // Not giving it back
}
if err == nil {
// Return it back to the connections cache if there were no errors
u.transport.Yield(conn)
}
return r, err
}

View File

@@ -1,101 +0,0 @@
package upstream
import (
"net"
"strings"
"github.com/miekg/dns"
"golang.org/x/net/context"
)
// Detects the upstream type from the specified url and creates a proper Upstream object
func NewUpstream(url string, bootstrap string) (Upstream, error) {
proto := "udp"
prefix := ""
switch {
case strings.HasPrefix(url, "tcp://"):
proto = "tcp"
prefix = "tcp://"
case strings.HasPrefix(url, "tls://"):
proto = "tcp-tls"
prefix = "tls://"
case strings.HasPrefix(url, "https://"):
return NewHttpsUpstream(url, bootstrap)
}
hostname := strings.TrimPrefix(url, prefix)
host, port, err := net.SplitHostPort(hostname)
if err != nil {
// Set port depending on the protocol
switch proto {
case "udp":
port = "53"
case "tcp":
port = "53"
case "tcp-tls":
port = "853"
}
// Set host = hostname
host = hostname
}
// Try to resolve the host address (or check if it's an IP address)
bootstrapResolver := CreateResolver(bootstrap)
ips, err := bootstrapResolver.LookupIPAddr(context.Background(), host)
if err != nil || len(ips) == 0 {
return nil, err
}
addr := ips[0].String()
endpoint := net.JoinHostPort(addr, port)
tlsServerName := ""
if proto == "tcp-tls" && host != addr {
// Check if we need to specify TLS server name
tlsServerName = host
}
return NewDnsUpstream(endpoint, proto, tlsServerName)
}
func CreateResolver(bootstrap string) *net.Resolver {
bootstrapResolver := net.DefaultResolver
if bootstrap != "" {
bootstrapResolver = &net.Resolver{
PreferGo: true,
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
var d net.Dialer
return d.DialContext(ctx, network, bootstrap)
},
}
}
return bootstrapResolver
}
// Performs a simple health-check of the specified upstream
func IsAlive(u Upstream) (bool, error) {
// Using ipv4only.arpa. domain as it is a part of DNS64 RFC and it should exist everywhere
ping := new(dns.Msg)
ping.SetQuestion("ipv4only.arpa.", dns.TypeA)
resp, err := u.Exchange(context.Background(), ping)
// If we got a header, we're alright, basically only care about I/O errors 'n stuff.
if err != nil && resp != nil {
// Silly check, something sane came back.
if resp.Rcode != dns.RcodeServerFailure {
err = nil
}
}
return err == nil, err
}

View File

@@ -1,128 +0,0 @@
package upstream
import (
"bytes"
"crypto/tls"
"fmt"
"io/ioutil"
"log"
"net"
"net/http"
"net/url"
"time"
"github.com/miekg/dns"
"github.com/pkg/errors"
"golang.org/x/net/context"
"golang.org/x/net/http2"
)
const (
dnsMessageContentType = "application/dns-message"
defaultKeepAlive = 30 * time.Second
)
// HttpsUpstream is the upstream implementation for DNS-over-HTTPS
type HttpsUpstream struct {
client *http.Client
endpoint *url.URL
}
// NewHttpsUpstream creates a new DNS-over-HTTPS upstream from the specified url
func NewHttpsUpstream(endpoint string, bootstrap string) (Upstream, error) {
u, err := url.Parse(endpoint)
if err != nil {
return nil, err
}
// Initialize bootstrap resolver
bootstrapResolver := CreateResolver(bootstrap)
dialer := &net.Dialer{
Timeout: defaultTimeout,
KeepAlive: defaultKeepAlive,
DualStack: true,
Resolver: bootstrapResolver,
}
// Update TLS and HTTP client configuration
tlsConfig := &tls.Config{ServerName: u.Hostname()}
transport := &http.Transport{
TLSClientConfig: tlsConfig,
DisableCompression: true,
MaxIdleConns: 1,
DialContext: dialer.DialContext,
}
http2.ConfigureTransport(transport)
client := &http.Client{
Timeout: defaultTimeout,
Transport: transport,
}
return &HttpsUpstream{client: client, endpoint: u}, nil
}
// Exchange provides an implementation for the Upstream interface
func (u *HttpsUpstream) Exchange(ctx context.Context, query *dns.Msg) (*dns.Msg, error) {
queryBuf, err := query.Pack()
if err != nil {
return nil, errors.Wrap(err, "failed to pack DNS query")
}
// No content negotiation for now, use DNS wire format
buf, backendErr := u.exchangeWireformat(queryBuf)
if backendErr == nil {
response := &dns.Msg{}
if err := response.Unpack(buf); err != nil {
return nil, errors.Wrap(err, "failed to unpack DNS response from body")
}
response.Id = query.Id
return response, nil
}
log.Printf("failed to connect to an HTTPS backend %q due to %s", u.endpoint, backendErr)
return nil, backendErr
}
// Perform message exchange with the default UDP wireformat defined in current draft
// https://tools.ietf.org/html/draft-ietf-doh-dns-over-https-10
func (u *HttpsUpstream) exchangeWireformat(msg []byte) ([]byte, error) {
req, err := http.NewRequest("POST", u.endpoint.String(), bytes.NewBuffer(msg))
if err != nil {
return nil, errors.Wrap(err, "failed to create an HTTPS request")
}
req.Header.Add("Content-Type", dnsMessageContentType)
req.Header.Add("Accept", dnsMessageContentType)
req.Host = u.endpoint.Hostname()
resp, err := u.client.Do(req)
if err != nil {
return nil, errors.Wrap(err, "failed to perform an HTTPS request")
}
// Check response status code
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("returned status code %d", resp.StatusCode)
}
contentType := resp.Header.Get("Content-Type")
if contentType != dnsMessageContentType {
return nil, fmt.Errorf("return wrong content type %s", contentType)
}
// Read application/dns-message response from the body
buf, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, errors.Wrap(err, "failed to read the response body")
}
return buf, nil
}
// Clear resources
func (u *HttpsUpstream) Close() error {
return nil
}

View File

@@ -1,210 +0,0 @@
package upstream
import (
"crypto/tls"
"net"
"sort"
"sync/atomic"
"time"
"github.com/miekg/dns"
)
// Persistent connections cache -- almost similar to the same used in the CoreDNS forward plugin
const (
defaultExpire = 10 * time.Second
minDialTimeout = 100 * time.Millisecond
maxDialTimeout = 30 * time.Second
defaultDialTimeout = 30 * time.Second
cumulativeAvgWeight = 4
)
// a persistConn hold the dns.Conn and the last used time.
type persistConn struct {
c *dns.Conn
used time.Time
}
// Transport hold the persistent cache.
type Transport struct {
avgDialTime int64 // kind of average time of dial time
conns map[string][]*persistConn // Buckets for udp, tcp and tcp-tls.
expire time.Duration // After this duration a connection is expired.
addr string
tlsConfig *tls.Config
dial chan string
yield chan *dns.Conn
ret chan *dns.Conn
stop chan bool
}
// Dial dials the address configured in transport, potentially reusing a connection or creating a new one.
func (t *Transport) Dial(proto string) (*dns.Conn, error) {
// If tls has been configured; use it.
if t.tlsConfig != nil {
proto = "tcp-tls"
}
t.dial <- proto
c := <-t.ret
if c != nil {
return c, nil
}
reqTime := time.Now()
timeout := t.dialTimeout()
if proto == "tcp-tls" {
conn, err := dns.DialTimeoutWithTLS(proto, t.addr, t.tlsConfig, timeout)
t.updateDialTimeout(time.Since(reqTime))
return conn, err
}
conn, err := dns.DialTimeout(proto, t.addr, timeout)
t.updateDialTimeout(time.Since(reqTime))
return conn, err
}
// Yield return the connection to transport for reuse.
func (t *Transport) Yield(c *dns.Conn) { t.yield <- c }
// Start starts the transport's connection manager.
func (t *Transport) Start() { go t.connManager() }
// Stop stops the transport's connection manager.
func (t *Transport) Stop() { close(t.stop) }
// SetExpire sets the connection expire time in transport.
func (t *Transport) SetExpire(expire time.Duration) { t.expire = expire }
// SetTLSConfig sets the TLS config in transport.
func (t *Transport) SetTLSConfig(cfg *tls.Config) { t.tlsConfig = cfg }
func NewTransport(addr string) *Transport {
t := &Transport{
avgDialTime: int64(defaultDialTimeout / 2),
conns: make(map[string][]*persistConn),
expire: defaultExpire,
addr: addr,
dial: make(chan string),
yield: make(chan *dns.Conn),
ret: make(chan *dns.Conn),
stop: make(chan bool),
}
return t
}
func averageTimeout(currentAvg *int64, observedDuration time.Duration, weight int64) {
dt := time.Duration(atomic.LoadInt64(currentAvg))
atomic.AddInt64(currentAvg, int64(observedDuration-dt)/weight)
}
func (t *Transport) dialTimeout() time.Duration {
return limitTimeout(&t.avgDialTime, minDialTimeout, maxDialTimeout)
}
func (t *Transport) updateDialTimeout(newDialTime time.Duration) {
averageTimeout(&t.avgDialTime, newDialTime, cumulativeAvgWeight)
}
// limitTimeout is a utility function to auto-tune timeout values
// average observed time is moved towards the last observed delay moderated by a weight
// next timeout to use will be the double of the computed average, limited by min and max frame.
func limitTimeout(currentAvg *int64, minValue time.Duration, maxValue time.Duration) time.Duration {
rt := time.Duration(atomic.LoadInt64(currentAvg))
if rt < minValue {
return minValue
}
if rt < maxValue/2 {
return 2 * rt
}
return maxValue
}
// connManagers manages the persistent connection cache for UDP and TCP.
func (t *Transport) connManager() {
ticker := time.NewTicker(t.expire)
Wait:
for {
select {
case proto := <-t.dial:
// take the last used conn - complexity O(1)
if stack := t.conns[proto]; len(stack) > 0 {
pc := stack[len(stack)-1]
if time.Since(pc.used) < t.expire {
// Found one, remove from pool and return this conn.
t.conns[proto] = stack[:len(stack)-1]
t.ret <- pc.c
continue Wait
}
// clear entire cache if the last conn is expired
t.conns[proto] = nil
// now, the connections being passed to closeConns() are not reachable from
// transport methods anymore. So, it's safe to close them in a separate goroutine
go closeConns(stack)
}
t.ret <- nil
case conn := <-t.yield:
// no proto here, infer from config and conn
if _, ok := conn.Conn.(*net.UDPConn); ok {
t.conns["udp"] = append(t.conns["udp"], &persistConn{conn, time.Now()})
continue Wait
}
if t.tlsConfig == nil {
t.conns["tcp"] = append(t.conns["tcp"], &persistConn{conn, time.Now()})
continue Wait
}
t.conns["tcp-tls"] = append(t.conns["tcp-tls"], &persistConn{conn, time.Now()})
case <-ticker.C:
t.cleanup(false)
case <-t.stop:
t.cleanup(true)
close(t.ret)
return
}
}
}
// closeConns closes connections.
func closeConns(conns []*persistConn) {
for _, pc := range conns {
pc.c.Close()
}
}
// cleanup removes connections from cache.
func (t *Transport) cleanup(all bool) {
staleTime := time.Now().Add(-t.expire)
for proto, stack := range t.conns {
if len(stack) == 0 {
continue
}
if all {
t.conns[proto] = nil
// now, the connections being passed to closeConns() are not reachable from
// transport methods anymore. So, it's safe to close them in a separate goroutine
go closeConns(stack)
continue
}
if stack[0].used.After(staleTime) {
continue
}
// connections in stack are sorted by "used"
good := sort.Search(len(stack), func(i int) bool {
return stack[i].used.After(staleTime)
})
t.conns[proto] = stack[good:]
// now, the connections being passed to closeConns() are not reachable from
// transport methods anymore. So, it's safe to close them in a separate goroutine
go closeConns(stack[:good])
}
}

View File

@@ -1,84 +0,0 @@
package upstream
import (
"log"
"github.com/coredns/coredns/core/dnsserver"
"github.com/coredns/coredns/plugin"
"github.com/mholt/caddy"
)
func init() {
caddy.RegisterPlugin("upstream", caddy.Plugin{
ServerType: "dns",
Action: setup,
})
}
// Read the configuration and initialize upstreams
func setup(c *caddy.Controller) error {
p, err := setupPlugin(c)
if err != nil {
return err
}
config := dnsserver.GetConfig(c)
config.AddPlugin(func(next plugin.Handler) plugin.Handler {
p.Next = next
return p
})
c.OnShutdown(p.onShutdown)
return nil
}
// Read the configuration
func setupPlugin(c *caddy.Controller) (*UpstreamPlugin, error) {
p := New()
log.Println("Initializing the Upstream plugin")
bootstrap := ""
upstreamUrls := []string{}
for c.Next() {
args := c.RemainingArgs()
if len(args) > 0 {
upstreamUrls = append(upstreamUrls, args...)
}
for c.NextBlock() {
switch c.Val() {
case "bootstrap":
if !c.NextArg() {
return nil, c.ArgErr()
}
bootstrap = c.Val()
}
}
}
for _, url := range upstreamUrls {
u, err := NewUpstream(url, bootstrap)
if err != nil {
log.Printf("Cannot initialize upstream %s", url)
return nil, err
}
p.Upstreams = append(p.Upstreams, u)
}
return p, nil
}
func (p *UpstreamPlugin) onShutdown() error {
for i := range p.Upstreams {
u := p.Upstreams[i]
err := u.Close()
if err != nil {
log.Printf("Error while closing the upstream: %s", err)
}
}
return nil
}

View File

@@ -1,30 +0,0 @@
package upstream
import (
"testing"
"github.com/mholt/caddy"
)
func TestSetup(t *testing.T) {
var tests = []struct {
config string
}{
{`upstream 8.8.8.8`},
{`upstream 8.8.8.8 {
bootstrap 8.8.8.8:53
}`},
{`upstream tls://1.1.1.1 8.8.8.8 {
bootstrap 1.1.1.1
}`},
}
for _, test := range tests {
c := caddy.NewTestController("dns", test.config)
err := setup(c)
if err != nil {
t.Fatalf("Test failed")
}
}
}

View File

@@ -1,57 +0,0 @@
package upstream
import (
"time"
"github.com/coredns/coredns/plugin"
"github.com/miekg/dns"
"github.com/pkg/errors"
"golang.org/x/net/context"
)
const (
defaultTimeout = 5 * time.Second
)
// Upstream is a simplified interface for proxy destination
type Upstream interface {
Exchange(ctx context.Context, query *dns.Msg) (*dns.Msg, error)
Close() error
}
// UpstreamPlugin is a simplified DNS proxy using a generic upstream interface
type UpstreamPlugin struct {
Upstreams []Upstream
Next plugin.Handler
}
// Initialize the upstream plugin
func New() *UpstreamPlugin {
p := &UpstreamPlugin{
Upstreams: []Upstream{},
}
return p
}
// ServeDNS implements interface for CoreDNS plugin
func (p *UpstreamPlugin) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
var reply *dns.Msg
var backendErr error
for i := range p.Upstreams {
upstream := p.Upstreams[i]
reply, backendErr = upstream.Exchange(ctx, r)
if backendErr == nil {
w.WriteMsg(reply)
return 0, nil
}
}
return dns.RcodeServerFailure, errors.Wrap(backendErr, "failed to contact any of the upstreams")
}
// Name implements interface for CoreDNS plugin
func (p *UpstreamPlugin) Name() string {
return "upstream"
}

View File

@@ -1,194 +0,0 @@
package upstream
import (
"net"
"testing"
"github.com/miekg/dns"
"golang.org/x/net/context"
)
func TestDnsUpstreamIsAlive(t *testing.T) {
var tests = []struct {
url string
bootstrap string
}{
{"8.8.8.8:53", "8.8.8.8:53"},
{"1.1.1.1", ""},
{"tcp://1.1.1.1:53", ""},
{"176.103.130.130:5353", ""},
}
for _, test := range tests {
u, err := NewUpstream(test.url, test.bootstrap)
if err != nil {
t.Errorf("cannot create a DNS upstream")
}
testUpstreamIsAlive(t, u)
}
}
func TestHttpsUpstreamIsAlive(t *testing.T) {
var tests = []struct {
url string
bootstrap string
}{
{"https://cloudflare-dns.com/dns-query", "8.8.8.8:53"},
{"https://dns.google.com/experimental", "8.8.8.8:53"},
{"https://doh.cleanbrowsing.org/doh/security-filter/", ""},
}
for _, test := range tests {
u, err := NewUpstream(test.url, test.bootstrap)
if err != nil {
t.Errorf("cannot create a DNS-over-HTTPS upstream")
}
testUpstreamIsAlive(t, u)
}
}
func TestDnsOverTlsIsAlive(t *testing.T) {
var tests = []struct {
url string
bootstrap string
}{
{"tls://1.1.1.1", ""},
{"tls://9.9.9.9:853", ""},
{"tls://security-filter-dns.cleanbrowsing.org", "8.8.8.8:53"},
{"tls://adult-filter-dns.cleanbrowsing.org:853", "8.8.8.8:53"},
}
for _, test := range tests {
u, err := NewUpstream(test.url, test.bootstrap)
if err != nil {
t.Errorf("cannot create a DNS-over-TLS upstream")
}
testUpstreamIsAlive(t, u)
}
}
func TestDnsUpstream(t *testing.T) {
var tests = []struct {
url string
bootstrap string
}{
{"8.8.8.8:53", "8.8.8.8:53"},
{"1.1.1.1", ""},
{"tcp://1.1.1.1:53", ""},
{"176.103.130.130:5353", ""},
}
for _, test := range tests {
u, err := NewUpstream(test.url, test.bootstrap)
if err != nil {
t.Errorf("cannot create a DNS upstream")
}
testUpstream(t, u)
}
}
func TestHttpsUpstream(t *testing.T) {
var tests = []struct {
url string
bootstrap string
}{
{"https://cloudflare-dns.com/dns-query", "8.8.8.8:53"},
{"https://dns.google.com/experimental", "8.8.8.8:53"},
{"https://doh.cleanbrowsing.org/doh/security-filter/", ""},
}
for _, test := range tests {
u, err := NewUpstream(test.url, test.bootstrap)
if err != nil {
t.Errorf("cannot create a DNS-over-HTTPS upstream")
}
testUpstream(t, u)
}
}
func TestDnsOverTlsUpstream(t *testing.T) {
var tests = []struct {
url string
bootstrap string
}{
{"tls://1.1.1.1", ""},
{"tls://9.9.9.9:853", ""},
{"tls://security-filter-dns.cleanbrowsing.org", "8.8.8.8:53"},
{"tls://adult-filter-dns.cleanbrowsing.org:853", "8.8.8.8:53"},
}
for _, test := range tests {
u, err := NewUpstream(test.url, test.bootstrap)
if err != nil {
t.Errorf("cannot create a DNS-over-TLS upstream")
}
testUpstream(t, u)
}
}
func testUpstreamIsAlive(t *testing.T, u Upstream) {
alive, err := IsAlive(u)
if !alive || err != nil {
t.Errorf("Upstream is not alive")
}
u.Close()
}
func testUpstream(t *testing.T, u Upstream) {
var tests = []struct {
name string
expected net.IP
}{
{"google-public-dns-a.google.com.", net.IPv4(8, 8, 8, 8)},
{"google-public-dns-b.google.com.", net.IPv4(8, 8, 4, 4)},
}
for _, test := range tests {
req := dns.Msg{}
req.Id = dns.Id()
req.RecursionDesired = true
req.Question = []dns.Question{
{Name: test.name, Qtype: dns.TypeA, Qclass: dns.ClassINET},
}
resp, err := u.Exchange(context.Background(), &req)
if err != nil {
t.Errorf("error while making an upstream request: %s", err)
}
if len(resp.Answer) != 1 {
t.Errorf("no answer section in the response")
}
if answer, ok := resp.Answer[0].(*dns.A); ok {
if !test.expected.Equal(answer.A) {
t.Errorf("wrong IP in the response: %v", answer.A)
}
}
}
err := u.Close()
if err != nil {
t.Errorf("Error while closing the upstream: %s", err)
}
}

View File

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